diff --git a/packages/cli/__tests__/commands/migrate.test.ts b/packages/cli/__tests__/commands/migrate.test.ts index 931e896..d87d80d 100644 --- a/packages/cli/__tests__/commands/migrate.test.ts +++ b/packages/cli/__tests__/commands/migrate.test.ts @@ -89,7 +89,7 @@ describe('migrate ', () => { expect(mockIntegration.launchChat).toHaveBeenCalledWith( expect.objectContaining({ - prompt: expect.stringContaining('Statsig'), + userPrompt: expect.stringContaining('Statsig'), cwd: process.cwd(), }), ); @@ -101,9 +101,9 @@ describe('migrate ', () => { await run(['migrate', 'statsig', '--ide', 'claude']); - const { prompt } = vi.mocked(mockIntegration.launchChat).mock.calls[0][0]; - expect(prompt).toContain('migrate-statsig'); - expect(prompt).toContain('(statsig)'); + const { userPrompt } = vi.mocked(mockIntegration.launchChat).mock.calls[0][0]; + expect(userPrompt).toContain('migrate-statsig'); + expect(userPrompt).toContain('(statsig)'); }); it('fails when plugin is not installed', async () => { diff --git a/packages/cli/__tests__/features/sdk/install.test.ts b/packages/cli/__tests__/features/sdk/install.test.ts new file mode 100644 index 0000000..ad0b79c --- /dev/null +++ b/packages/cli/__tests__/features/sdk/install.test.ts @@ -0,0 +1,212 @@ +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import type { FrameworkConfig } from '@spotify-confidence/core'; +import { createProjectDir } from '@spotify-confidence/testing/scaffold'; +import { runSdkInstall } from '@features/sdk/install.js'; + +const mockExecFile = vi.fn<() => Promise>(); +const mockDetectFramework = vi.fn<() => Promise>(); +const mockMessage = vi.fn(); + +vi.mock('ora', () => ({ + default: () => ({ start: () => ({ succeed: vi.fn(), fail: vi.fn() }) }), +})); + +vi.mock('@spotify-confidence/core', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + detectFramework: (...args: unknown[]) => mockDetectFramework(...(args as [])), + execFile: (...args: unknown[]) => mockExecFile(...(args as [])), + }; +}); + +vi.mock('@output/print.js', () => ({ + message: (...args: unknown[]) => mockMessage(...args), + fail: vi.fn(), +})); + +const REACT_FRAMEWORK: FrameworkConfig = { + id: 'react', + name: 'React', + docsUrl: 'https://confidence.spotify.com/docs/sdk/react', + sdkPackage: '@spotify-confidence/sdk', + detect: async () => true, +}; + +const PYTHON_FRAMEWORK: FrameworkConfig = { + id: 'python', + name: 'Python', + docsUrl: 'https://confidence.spotify.com/docs/sdk/python', + sdkPackage: 'spotify-confidence-sdk', + detect: async () => true, +}; + +beforeEach(() => { + mockExecFile.mockResolvedValue(undefined); + mockDetectFramework.mockResolvedValue(REACT_FRAMEWORK); +}); + +describe('runSdkInstall workspace root handling', () => { + it('passes -w when pnpm-workspace.yaml is present', async () => { + using project = createProjectDir('empty'); + writeFileSync(join(project.path, 'pnpm-lock.yaml'), ''); + writeFileSync(join(project.path, 'pnpm-workspace.yaml'), 'packages:\n - packages/*'); + + await runSdkInstall({ dir: project.path }); + + expect(mockExecFile).toHaveBeenCalledWith( + 'pnpm', + ['add', '-w', '@spotify-confidence/sdk'], + expect.objectContaining({ cwd: project.path }), + ); + }); + + it('omits -w for pnpm without workspace config', async () => { + using project = createProjectDir('empty'); + writeFileSync(join(project.path, 'pnpm-lock.yaml'), ''); + + await runSdkInstall({ dir: project.path }); + + expect(mockExecFile).toHaveBeenCalledWith( + 'pnpm', + ['add', '@spotify-confidence/sdk'], + expect.objectContaining({ cwd: project.path }), + ); + }); + + it('passes -W for yarn classic with workspaces', async () => { + using project = createProjectDir('empty'); + writeFileSync(join(project.path, 'yarn.lock'), ''); + writeFileSync( + join(project.path, 'package.json'), + JSON.stringify({ workspaces: ['packages/*'] }), + ); + + await runSdkInstall({ dir: project.path }); + + expect(mockExecFile).toHaveBeenCalledWith( + 'yarn', + ['add', '-W', '@spotify-confidence/sdk'], + expect.objectContaining({ cwd: project.path }), + ); + }); + + it('omits -W for yarn berry with workspaces', async () => { + using project = createProjectDir('empty'); + writeFileSync(join(project.path, 'yarn.lock'), ''); + writeFileSync( + join(project.path, 'package.json'), + JSON.stringify({ workspaces: ['packages/*'] }), + ); + writeFileSync(join(project.path, '.yarnrc.yml'), 'nodeLinker: node-modules'); + + await runSdkInstall({ dir: project.path }); + + expect(mockExecFile).toHaveBeenCalledWith( + 'yarn', + ['add', '@spotify-confidence/sdk'], + expect.objectContaining({ cwd: project.path }), + ); + }); + + it('omits -W for yarn without workspaces', async () => { + using project = createProjectDir('empty'); + writeFileSync(join(project.path, 'yarn.lock'), ''); + + await runSdkInstall({ dir: project.path }); + + expect(mockExecFile).toHaveBeenCalledWith( + 'yarn', + ['add', '@spotify-confidence/sdk'], + expect.objectContaining({ cwd: project.path }), + ); + }); + + it('does not add flags for npm at a workspace root', async () => { + using project = createProjectDir('empty'); + writeFileSync( + join(project.path, 'package.json'), + JSON.stringify({ workspaces: ['packages/*'] }), + ); + + await runSdkInstall({ dir: project.path }); + + expect(mockExecFile).toHaveBeenCalledWith( + 'npm', + ['add', '@spotify-confidence/sdk'], + expect.objectContaining({ cwd: project.path }), + ); + }); + + it('does not add flags for bun at a workspace root', async () => { + using project = createProjectDir('empty'); + writeFileSync(join(project.path, 'bun.lockb'), ''); + writeFileSync( + join(project.path, 'package.json'), + JSON.stringify({ workspaces: ['packages/*'] }), + ); + + await runSdkInstall({ dir: project.path }); + + expect(mockExecFile).toHaveBeenCalledWith( + 'bun', + ['add', '@spotify-confidence/sdk'], + expect.objectContaining({ cwd: project.path }), + ); + }); +}); + +describe('runSdkInstall Python package manager detection', () => { + beforeEach(() => { + mockDetectFramework.mockResolvedValue(PYTHON_FRAMEWORK); + }); + + it('uses poetry add when poetry.lock is present', async () => { + using project = createProjectDir('empty'); + writeFileSync(join(project.path, 'poetry.lock'), ''); + + await runSdkInstall({ dir: project.path }); + + expect(mockExecFile).toHaveBeenCalledWith( + 'poetry', + ['add', 'spotify-confidence-sdk'], + expect.objectContaining({ cwd: project.path }), + ); + }); + + it('uses uv add when uv.lock is present', async () => { + using project = createProjectDir('empty'); + writeFileSync(join(project.path, 'uv.lock'), ''); + + await runSdkInstall({ dir: project.path }); + + expect(mockExecFile).toHaveBeenCalledWith( + 'uv', + ['add', 'spotify-confidence-sdk'], + expect.objectContaining({ cwd: project.path }), + ); + }); + + it('uses pipenv install when Pipfile is present', async () => { + using project = createProjectDir('empty'); + writeFileSync(join(project.path, 'Pipfile'), ''); + + await runSdkInstall({ dir: project.path }); + + expect(mockExecFile).toHaveBeenCalledWith( + 'pipenv', + ['install', 'spotify-confidence-sdk'], + expect.objectContaining({ cwd: project.path }), + ); + }); + + it('falls back to manual install when no Python PM is detected', async () => { + using project = createProjectDir('empty'); + + await runSdkInstall({ dir: project.path }); + + expect(mockExecFile).not.toHaveBeenCalled(); + expect(mockMessage).toHaveBeenCalledWith(expect.stringContaining('pip install')); + }); +}); diff --git a/packages/cli/bin/cli.ts b/packages/cli/bin/cli.ts index 9285e46..bc5f061 100644 --- a/packages/cli/bin/cli.ts +++ b/packages/cli/bin/cli.ts @@ -10,6 +10,7 @@ import { eventsCommand, flagsCommand, recordingsCommand, + sdkCommand, quickstartCommand, mcpCommand, updateCommand, @@ -70,6 +71,7 @@ const cli = yargs(hideBin(process.argv)) .command(eventsCommand) .command(flagsCommand) .command(recordingsCommand) + .command(sdkCommand) .command(quickstartCommand) .command(mcpCommand) .command(updateCommand) diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 6fb4489..1eeff33 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -5,6 +5,7 @@ export { configCommand } from './config.js'; export { eventsCommand } from './events.js'; export { flagsCommand } from './flags.js'; export { recordingsCommand } from './recordings.js'; +export { sdkCommand } from './sdk.js'; export { quickstartCommand } from './quickstart.js'; export { mcpCommand } from './mcp.js'; export { pluginCommand } from './plugin.js'; diff --git a/packages/cli/src/commands/sdk.ts b/packages/cli/src/commands/sdk.ts new file mode 100644 index 0000000..10ecb06 --- /dev/null +++ b/packages/cli/src/commands/sdk.ts @@ -0,0 +1,41 @@ +import type { Argv } from 'yargs'; +import { runSdkInstall, runSdkSetup } from '@features/sdk/index.js'; +import { safely } from '../utils/safely.js'; + +export const sdkCommand = { + command: 'sdk ', + describe: 'Manage SDK installation and setup', + builder(yargs: Argv) { + return yargs + .option('dir', { + type: 'string', + describe: 'Target project directory', + normalize: true, + }) + .command( + 'install', + 'Detect framework and install the Confidence SDK', + () => {}, + safely(runSdkInstall), + ) + .command( + 'setup', + 'Set up the Confidence SDK in your project', + (y: Argv) => + y + .option('ide', { + type: 'string', + choices: ['claude', 'cursor', 'codex'] as const, + describe: 'AI coding agent to use', + }) + .option('profile', { + type: 'string', + describe: 'Auth profile to use', + }), + safely(runSdkSetup), + ) + .demandCommand(1, 'Run "confidence sdk --help" to see available actions.') + .strict(); + }, + handler() {}, +}; diff --git a/packages/cli/src/features/migrate/launch.ts b/packages/cli/src/features/migrate/launch.ts index 9ec7ef3..fd7208b 100644 --- a/packages/cli/src/features/migrate/launch.ts +++ b/packages/cli/src/features/migrate/launch.ts @@ -35,6 +35,6 @@ export async function launchMigration( const token = await resolveAuthToken({ profile: resolveFlag('profile', argv) }); if (!token) return; - const prompt = buildMigrationPrompt(provider); - integration.launchChat({ prompt, cwd: projectDir, token }); + const userPrompt = buildMigrationPrompt(provider); + integration.launchChat({ userPrompt, cwd: projectDir, token }); } diff --git a/packages/cli/src/features/sdk/index.ts b/packages/cli/src/features/sdk/index.ts new file mode 100644 index 0000000..4bc2f85 --- /dev/null +++ b/packages/cli/src/features/sdk/index.ts @@ -0,0 +1,2 @@ +export { runSdkInstall } from './install.js'; +export { runSdkSetup } from './setup.js'; diff --git a/packages/cli/src/features/sdk/install-jvm.ts b/packages/cli/src/features/sdk/install-jvm.ts new file mode 100644 index 0000000..2b7bca8 --- /dev/null +++ b/packages/cli/src/features/sdk/install-jvm.ts @@ -0,0 +1,29 @@ +import type { InstallCommand } from './install-types.js'; + +export function buildKotlinInstall(pkg: string): InstallCommand { + return { + type: 'manual', + snippet: + `Add the following to your module's build.gradle.kts:\n\n` + + ` dependencies {\n` + + ` implementation("${pkg}:")\n` + + ` }`, + }; +} + +export function buildJavaInstall(pkg: string): InstallCommand { + const [groupId, artifactId] = pkg.split(':'); + return { + type: 'manual', + snippet: + `Add the following to your build.gradle.kts or pom.xml:\n\n` + + ` Gradle:\n` + + ` implementation("${pkg}:")\n\n` + + ` Maven:\n` + + ` \n` + + ` ${groupId}\n` + + ` ${artifactId}\n` + + ` \n` + + ` `, + }; +} diff --git a/packages/cli/src/features/sdk/install-native.ts b/packages/cli/src/features/sdk/install-native.ts new file mode 100644 index 0000000..3833fca --- /dev/null +++ b/packages/cli/src/features/sdk/install-native.ts @@ -0,0 +1,17 @@ +import type { InstallCommand } from './install-types.js'; + +export function buildGoInstall(pkg: string): InstallCommand { + return { type: 'auto', cmd: 'go', args: ['get', pkg] }; +} + +export function buildSwiftInstall(pkg: string): InstallCommand { + return { + type: 'manual', + snippet: + `Add the following to your Package.swift dependencies:\n\n` + + ` .package(\n` + + ` url: "https://github.com/spotify/${pkg}.git",\n` + + ` from: ""\n` + + ` )`, + }; +} diff --git a/packages/cli/src/features/sdk/install-node.ts b/packages/cli/src/features/sdk/install-node.ts new file mode 100644 index 0000000..a9611dd --- /dev/null +++ b/packages/cli/src/features/sdk/install-node.ts @@ -0,0 +1,38 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import type { InstallCommand } from './install-types.js'; + +type NodePM = 'npm' | 'pnpm' | 'yarn' | 'bun'; + +function detectNodePM(dir: string): NodePM { + if (existsSync(join(dir, 'pnpm-lock.yaml'))) return 'pnpm'; + if (existsSync(join(dir, 'yarn.lock'))) return 'yarn'; + if (existsSync(join(dir, 'bun.lockb')) || existsSync(join(dir, 'bun.lock'))) return 'bun'; + return 'npm'; +} + +function hasPackageJsonWorkspaces(dir: string): boolean { + const pkgPath = join(dir, 'package.json'); + if (!existsSync(pkgPath)) return false; + + try { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); + return Array.isArray(pkg.workspaces) || typeof pkg.workspaces === 'object'; + } catch { + return false; + } +} + +function workspaceRootArgs(pm: NodePM, dir: string): string[] { + if (pm === 'pnpm' && existsSync(join(dir, 'pnpm-workspace.yaml'))) return ['-w']; + if (pm === 'yarn' && hasPackageJsonWorkspaces(dir) && !existsSync(join(dir, '.yarnrc.yml'))) { + return ['-W']; + } + + return []; +} + +export function buildNodeInstall(pkg: string, dir: string): InstallCommand { + const pm = detectNodePM(dir); + return { type: 'auto', cmd: pm, args: ['add', ...workspaceRootArgs(pm, dir), pkg] }; +} diff --git a/packages/cli/src/features/sdk/install-python.ts b/packages/cli/src/features/sdk/install-python.ts new file mode 100644 index 0000000..3f84265 --- /dev/null +++ b/packages/cli/src/features/sdk/install-python.ts @@ -0,0 +1,33 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import type { InstallCommand } from './install-types.js'; + +type PythonPM = 'poetry' | 'uv' | 'pipenv' | 'pip'; + +function detectPythonPM(dir: string): PythonPM { + if (existsSync(join(dir, 'poetry.lock'))) return 'poetry'; + if (existsSync(join(dir, 'uv.lock'))) return 'uv'; + if (existsSync(join(dir, 'Pipfile')) || existsSync(join(dir, 'Pipfile.lock'))) return 'pipenv'; + return 'pip'; +} + +export function buildPythonInstall(pkg: string, dir: string): InstallCommand { + const pm = detectPythonPM(dir); + switch (pm) { + case 'poetry': + return { type: 'auto', cmd: 'poetry', args: ['add', pkg] }; + case 'uv': + return { type: 'auto', cmd: 'uv', args: ['add', pkg] }; + case 'pipenv': + return { type: 'auto', cmd: 'pipenv', args: ['install', pkg] }; + case 'pip': + return { + type: 'manual', + snippet: `Add ${pkg} to your project dependencies and install it:\n\n pip install ${pkg}`, + }; + default: { + const _exhaustive: never = pm; + throw new Error(`Unknown Python PM: ${_exhaustive}`); + } + } +} diff --git a/packages/cli/src/features/sdk/install-types.ts b/packages/cli/src/features/sdk/install-types.ts new file mode 100644 index 0000000..4ccb8a1 --- /dev/null +++ b/packages/cli/src/features/sdk/install-types.ts @@ -0,0 +1,4 @@ +type AutoInstall = { type: 'auto'; cmd: string; args: string[] }; +type ManualInstall = { type: 'manual'; snippet: string }; + +export type InstallCommand = AutoInstall | ManualInstall; diff --git a/packages/cli/src/features/sdk/install.ts b/packages/cli/src/features/sdk/install.ts new file mode 100644 index 0000000..1ad7846 --- /dev/null +++ b/packages/cli/src/features/sdk/install.ts @@ -0,0 +1,74 @@ +import ora from 'ora'; +import { + detectFramework, + execFile, + getFrameworks, + type FrameworkConfig, +} from '@spotify-confidence/core'; +import { message, fail } from '@output/print.js'; +import { resolveProjectDir } from '@features/ide/index.js'; +import type { InstallCommand } from './install-types.js'; +import { buildNodeInstall } from './install-node.js'; +import { buildPythonInstall } from './install-python.js'; +import { buildJavaInstall, buildKotlinInstall } from './install-jvm.js'; +import { buildGoInstall, buildSwiftInstall } from './install-native.js'; + +function buildInstallCommand(fw: FrameworkConfig, dir: string): InstallCommand { + switch (fw.id) { + case 'react': + case 'nextjs': + case 'node': + return buildNodeInstall(fw.sdkPackage, dir); + case 'python': + return buildPythonInstall(fw.sdkPackage, dir); + case 'go': + return buildGoInstall(fw.sdkPackage); + case 'kotlin': + return buildKotlinInstall(fw.sdkPackage); + case 'java': + return buildJavaInstall(fw.sdkPackage); + case 'swift': + return buildSwiftInstall(fw.sdkPackage); + default: { + const _exhaustive: never = fw.id; + throw new Error(`Unknown framework: ${_exhaustive}`); + } + } +} + +export async function runSdkInstall(argv: Record): Promise { + const dir = resolveProjectDir(argv); + const spinner = ora('Detecting framework...').start(); + + const fw = await detectFramework(dir); + + if (!fw) { + spinner.fail('Could not detect a framework.'); + const names = getFrameworks().map((f) => f.name); + fail( + `Supported frameworks: ${names.join(', ')}.\n` + + `Make sure you are in a project directory, or use --dir to specify one.`, + ); + return; + } + + spinner.succeed(`Detected ${fw.name}.`); + + const install = buildInstallCommand(fw, dir); + + if (install.type === 'manual') { + message(`\n${install.snippet}\n\nDocs: ${fw.docsUrl}`); + return; + } + + const installSpinner = ora(`Installing ${fw.sdkPackage}...`).start(); + + try { + await execFile(install.cmd, install.args, { cwd: dir }); + installSpinner.succeed(`Installed ${fw.sdkPackage}.`); + } catch (err) { + installSpinner.fail(`Failed to install ${fw.sdkPackage}.`); + const msg = err instanceof Error ? err.message : String(err); + fail(`${install.cmd} ${install.args.join(' ')} failed:\n${msg}`); + } +} diff --git a/packages/cli/src/features/sdk/setup.ts b/packages/cli/src/features/sdk/setup.ts new file mode 100644 index 0000000..d9ebfa6 --- /dev/null +++ b/packages/cli/src/features/sdk/setup.ts @@ -0,0 +1,42 @@ +import { getIntegration } from '@spotify-confidence/core'; +import { fail } from '@output/print.js'; +import { resolveIde, resolveFlag, resolveProjectDir } from '@features/ide/index.js'; +import { resolveAuthToken } from '@features/mcp/index.js'; + +const SDK_SETUP_PROMPT = `Set up the Confidence SDK in this project. +Detect the project's framework, then use the getCodeSnippetAndSdkIntegrationTips tool from the confidence-docs MCP server to get the integration guide for that SDK. +Install the SDK package and create a working configuration file following the guide. +Only set up the SDK — do not create feature flags, event tracking, or session recordings.`; + +export async function runSdkSetup(argv: Record): Promise { + const ideId = await resolveIde(resolveFlag('ide', argv)); + const projectDir = resolveProjectDir(argv); + const integration = getIntegration(ideId); + + const plugin = await integration.detectPlugin(projectDir); + if (!plugin) { + fail( + 'Confidence AI plugin not installed. Run "confidence plugin install" to install it or "confidence quickstart" to set up your project first.', + ); + return; + } + + const statuses = await integration.detectMcpStatuses(projectDir); + if (Object.values(statuses).some((s) => s === 'not-installed')) { + fail('MCP servers not installed. Run "confidence mcp install" to set them up.'); + return; + } + + if (Object.values(statuses).some((s) => s === 'auth-expired')) { + fail('MCP server auth expired. Run "confidence mcp auth" to re-authenticate.'); + return; + } + + const token = await resolveAuthToken({ profile: resolveFlag('profile', argv) }); + if (!token) { + fail('Not authenticated. Run "confidence login" to sign in.'); + return; + } + + integration.launchChat({ userPrompt: SDK_SETUP_PROMPT, cwd: projectDir, token }); +} diff --git a/packages/core/src/frameworks/index.ts b/packages/core/src/frameworks/index.ts index a09514f..57fa699 100644 --- a/packages/core/src/frameworks/index.ts +++ b/packages/core/src/frameworks/index.ts @@ -8,7 +8,7 @@ import { javaFramework } from './java/index.js'; import { goFramework } from './go/index.js'; import { pythonFramework } from './python/index.js'; -export type { FrameworkConfig } from './types.js'; +export type { FrameworkConfig, FrameworkId } from './types.js'; const FRAMEWORKS: FrameworkConfig[] = [ nextjsFramework, diff --git a/packages/core/src/frameworks/types.ts b/packages/core/src/frameworks/types.ts index 51a06e3..ab677f4 100644 --- a/packages/core/src/frameworks/types.ts +++ b/packages/core/src/frameworks/types.ts @@ -1,5 +1,8 @@ +export type FrameworkId = + 'react' | 'nextjs' | 'node' | 'python' | 'go' | 'kotlin' | 'java' | 'swift'; + export type FrameworkConfig = { - id: string; + id: FrameworkId; name: string; docsUrl: string; sdkPackage: string; diff --git a/packages/core/src/integrations/chat.ts b/packages/core/src/integrations/chat.ts index c5002c1..6702a06 100644 --- a/packages/core/src/integrations/chat.ts +++ b/packages/core/src/integrations/chat.ts @@ -46,7 +46,7 @@ function buildChatPrompt(session: WizardSession): string { export function launchChatSession(session: WizardSession, ide: IdeId): void { const integration = getIntegration(ide); integration.launchChat({ - prompt: buildChatPrompt(session), + systemPrompt: buildChatPrompt(session), cwd: session.projectDir, token: session.authState.token, }); diff --git a/packages/core/src/integrations/claude/chat.ts b/packages/core/src/integrations/claude/chat.ts index 4ee8783..33b5b50 100644 --- a/packages/core/src/integrations/claude/chat.ts +++ b/packages/core/src/integrations/claude/chat.ts @@ -1,8 +1,13 @@ import { spawn } from '../../exec/exec.js'; import type { ChatOpts } from '../types.js'; -export function launchChat({ prompt, cwd }: ChatOpts): void { - spawn('claude', ['--append-system-prompt', prompt], { +export function launchChat({ userPrompt, systemPrompt, cwd }: ChatOpts): void { + const args: string[] = []; + + if (systemPrompt) args.push('--append-system-prompt', systemPrompt); + if (userPrompt) args.push(userPrompt); + + spawn('claude', args, { cwd, stdio: 'inherit', detached: false, diff --git a/packages/core/src/integrations/codex/chat.ts b/packages/core/src/integrations/codex/chat.ts index 1d6c05d..65d5ee6 100644 --- a/packages/core/src/integrations/codex/chat.ts +++ b/packages/core/src/integrations/codex/chat.ts @@ -1,8 +1,9 @@ import { spawn } from '../../exec/exec.js'; import type { ChatOpts } from '../types.js'; -export function launchChat({ prompt, cwd, token }: ChatOpts): void { +export function launchChat({ userPrompt, systemPrompt, cwd, token }: ChatOpts): void { const env = token ? { ...globalThis.process.env, CONFIDENCE_ACCESS_TOKEN: token } : undefined; + const prompt = [systemPrompt, userPrompt].filter(Boolean).join('\n\n'); spawn('codex', ['-C', cwd, prompt], { cwd, diff --git a/packages/core/src/integrations/cursor/chat.ts b/packages/core/src/integrations/cursor/chat.ts index adf2686..f541ea5 100644 --- a/packages/core/src/integrations/cursor/chat.ts +++ b/packages/core/src/integrations/cursor/chat.ts @@ -1,8 +1,9 @@ import { spawn } from '../../exec/exec.js'; import type { ChatOpts } from '../types.js'; -export function launchChat({ prompt, cwd, token }: ChatOpts): void { +export function launchChat({ userPrompt, systemPrompt, cwd, token }: ChatOpts): void { const env = token ? { ...globalThis.process.env, CONFIDENCE_ACCESS_TOKEN: token } : undefined; + const prompt = [systemPrompt, userPrompt].filter(Boolean).join('\n\n'); spawn('cursor', ['agent', prompt, '--approve-mcps'], { cwd, diff --git a/packages/core/src/integrations/types.ts b/packages/core/src/integrations/types.ts index fe27357..6378a9b 100644 --- a/packages/core/src/integrations/types.ts +++ b/packages/core/src/integrations/types.ts @@ -40,7 +40,8 @@ export type OnboardingOpts = { }; export type ChatOpts = { - prompt: string; + userPrompt?: string; + systemPrompt?: string; cwd: string; token?: string; };