diff --git a/.changeset/append-system-prompt-cli.md b/.changeset/append-system-prompt-cli.md new file mode 100644 index 00000000000..0dd14155464 --- /dev/null +++ b/.changeset/append-system-prompt-cli.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Add `--append-system-prompt` CLI option to append custom instructions to the system prompt diff --git a/cli/src/__tests__/append-system-prompt.test.ts b/cli/src/__tests__/append-system-prompt.test.ts new file mode 100644 index 00000000000..7372335407f --- /dev/null +++ b/cli/src/__tests__/append-system-prompt.test.ts @@ -0,0 +1,109 @@ +import { describe, it, expect } from "vitest" +import type { CLIOptions } from "../types/cli.js" + +describe("Append System Prompt CLI Option", () => { + describe("CLIOptions type", () => { + it("should accept appendSystemPrompt as a string option", () => { + const options: CLIOptions = { + mode: "code", + workspace: "/test/workspace", + appendSystemPrompt: "Custom instructions here", + } + + expect(options.appendSystemPrompt).toBe("Custom instructions here") + }) + + it("should allow appendSystemPrompt to be undefined", () => { + const options: CLIOptions = { + mode: "code", + workspace: "/test/workspace", + } + + expect(options.appendSystemPrompt).toBeUndefined() + }) + + it("should handle empty string for appendSystemPrompt", () => { + const options: CLIOptions = { + mode: "code", + workspace: "/test/workspace", + appendSystemPrompt: "", + } + + expect(options.appendSystemPrompt).toBe("") + }) + + it("should handle multi-line appendSystemPrompt", () => { + const multiLinePrompt = `Line 1 +Line 2 +Line 3` + const options: CLIOptions = { + mode: "code", + workspace: "/test/workspace", + appendSystemPrompt: multiLinePrompt, + } + + expect(options.appendSystemPrompt).toBe(multiLinePrompt) + }) + }) + + describe("CLI flag parsing", () => { + it("should parse --append-system-prompt flag with value", () => { + // This test validates the expected behavior when the flag is parsed + const mockArgs = ["--append-system-prompt", "Custom instructions"] + const expectedValue = "Custom instructions" + + // Simulate what commander.js would do + const parsedValue = mockArgs[1] + expect(parsedValue).toBe(expectedValue) + }) + + it("should handle --append-system-prompt with quoted multi-word value", () => { + const mockArgs = ["--append-system-prompt", "Always use TypeScript strict mode"] + const expectedValue = "Always use TypeScript strict mode" + + const parsedValue = mockArgs[1] + expect(parsedValue).toBe(expectedValue) + }) + }) + + describe("System prompt integration", () => { + it("should append custom text to system prompt when provided", () => { + const basePrompt = "You are Kilo Code, an AI assistant." + const appendText = "Always write tests first." + const expectedPrompt = `${basePrompt} + +${appendText}` + + const result = `${basePrompt}\n\n${appendText}` + expect(result).toBe(expectedPrompt) + }) + + it("should not modify system prompt when appendSystemPrompt is undefined", () => { + const basePrompt = "You are Kilo Code, an AI assistant." + const appendText = undefined + + const result = appendText ? `${basePrompt}\n\n${appendText}` : basePrompt + expect(result).toBe(basePrompt) + }) + + it("should not modify system prompt when appendSystemPrompt is empty string", () => { + const basePrompt = "You are Kilo Code, an AI assistant." + const appendText = "" + + const result = appendText ? `${basePrompt}\n\n${appendText}` : basePrompt + expect(result).toBe(basePrompt) + }) + + it("should properly format appended text with newlines", () => { + const basePrompt = "You are Kilo Code." + const appendText = "Rule 1: Test first\nRule 2: Keep it simple" + const expectedPrompt = `You are Kilo Code. + +Rule 1: Test first +Rule 2: Keep it simple` + + const result = `${basePrompt}\n\n${appendText}` + expect(result).toBe(expectedPrompt) + }) + }) +}) diff --git a/cli/src/cli.ts b/cli/src/cli.ts index ac5a59ec2a8..06071559acd 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -117,6 +117,10 @@ export class CLI { serviceOptions.customModes = this.options.customModes } + if (this.options.appendSystemPrompt) { + serviceOptions.appendSystemPrompt = this.options.appendSystemPrompt + } + this.service = createExtensionService(serviceOptions) logs.debug("ExtensionService created with identity", "CLI", { hasIdentity: !!identity, diff --git a/cli/src/host/ExtensionHost.ts b/cli/src/host/ExtensionHost.ts index 81ad448d97a..2067199925e 100644 --- a/cli/src/host/ExtensionHost.ts +++ b/cli/src/host/ExtensionHost.ts @@ -11,6 +11,7 @@ export interface ExtensionHostOptions { extensionRootPath: string // Root path for extension assets identity?: IdentityInfo // Identity information for VSCode environment customModes?: ModeConfig[] // Custom modes configuration + appendSystemPrompt?: string // Custom text to append to system prompt } // Extension module interface @@ -786,6 +787,8 @@ export class ExtensionHost extends EventEmitter { imageGeneration: false, runSlashCommand: false, }, + // Add appendSystemPrompt from CLI options + ...(this.options.appendSystemPrompt && { appendSystemPrompt: this.options.appendSystemPrompt }), } // The CLI will inject the actual configuration through updateState @@ -1037,6 +1040,20 @@ export class ExtensionHost extends EventEmitter { settings: Object.keys(autoApprovalSettings), }) } + + // Sync appendSystemPrompt to extension + // This setting is passed from CLI options and needs to be stored in the extension's + // contextProxy so it's available when generating the system prompt + const appendSystemPrompt = configState.appendSystemPrompt || this.options.appendSystemPrompt + if (appendSystemPrompt) { + await this.sendWebviewMessage({ + type: "updateSettings", + updatedSettings: { appendSystemPrompt }, + }) + logs.debug("appendSystemPrompt synchronized to extension", "ExtensionHost", { + length: appendSystemPrompt.length, + }) + } } /** diff --git a/cli/src/index.ts b/cli/src/index.ts index 03514c94357..a1bf4c97bf6 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -48,6 +48,7 @@ program .option("-s, --session ", "Restore a session by ID") .option("-f, --fork ", "Fork a session by ID") .option("--nosplash", "Disable the welcome message and update notifications", false) + .option("--append-system-prompt ", "Append custom instructions to the system prompt") .argument("[prompt]", "The prompt or command to execute") .action(async (prompt, options) => { // Validate that --existing-branch requires --parallel @@ -228,6 +229,7 @@ program session: options.session, fork: options.fork, noSplash: options.nosplash, + appendSystemPrompt: options.appendSystemPrompt, }) await cli.start() await cli.dispose() diff --git a/cli/src/services/extension.ts b/cli/src/services/extension.ts index ca2dcb96c59..20c72fa4fe0 100644 --- a/cli/src/services/extension.ts +++ b/cli/src/services/extension.ts @@ -22,6 +22,8 @@ export interface ExtensionServiceOptions { extensionRootPath?: string /** Identity information for VSCode environment */ identity?: IdentityInfo + /** Custom text to append to system prompt */ + appendSystemPrompt?: string } /** @@ -73,9 +75,10 @@ export interface ExtensionServiceEvents { export class ExtensionService extends EventEmitter { private extensionHost: ExtensionHost private messageBridge: MessageBridge - private options: Required> & { + private options: Required> & { identity?: IdentityInfo customModes?: ModeConfig[] + appendSystemPrompt?: string } private isInitialized = false private isDisposed = false @@ -95,6 +98,7 @@ export class ExtensionService extends EventEmitter { extensionRootPath: options.extensionRootPath || extensionPaths.extensionRootPath, ...(options.identity && { identity: options.identity }), ...(options.customModes && { customModes: options.customModes }), + ...(options.appendSystemPrompt && { appendSystemPrompt: options.appendSystemPrompt }), } // Create extension host @@ -109,6 +113,9 @@ export class ExtensionService extends EventEmitter { if (this.options.customModes) { hostOptions.customModes = this.options.customModes } + if (this.options.appendSystemPrompt) { + hostOptions.appendSystemPrompt = this.options.appendSystemPrompt + } this.extensionHost = createExtensionHost(hostOptions) // Create message bridge diff --git a/cli/src/types/cli.ts b/cli/src/types/cli.ts index 8f055904114..eb8544a0d72 100644 --- a/cli/src/types/cli.ts +++ b/cli/src/types/cli.ts @@ -43,4 +43,5 @@ export interface CLIOptions { session?: string fork?: string noSplash?: boolean + appendSystemPrompt?: string } diff --git a/cli/src/types/messages.ts b/cli/src/types/messages.ts index 1eeced51d96..85d82b61225 100644 --- a/cli/src/types/messages.ts +++ b/cli/src/types/messages.ts @@ -87,6 +87,7 @@ export interface ExtensionState { cwd?: string organizationAllowList?: OrganizationAllowList routerModels?: RouterModels + appendSystemPrompt?: string // Custom text to append to system prompt (CLI only) [key: string]: unknown } diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 112a145ff55..0fe51f944bc 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -235,6 +235,7 @@ export const globalSettingsSchema = z.object({ hasOpenedModeSelector: z.boolean().optional(), lastModeExportPath: z.string().optional(), lastModeImportPath: z.string().optional(), + appendSystemPrompt: z.string().optional(), // kilocode_change: Custom text to append to system prompt (CLI only) }) export type GlobalSettings = z.infer diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 5196937ab7a..572bad5d087 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -156,6 +156,13 @@ ${await addCustomInstructions(baseInstructions, globalCustomInstructions || "", settings, })}` + // kilocode_change start: Append custom system prompt from CLI if provided + const appendSystemPrompt = clineProviderState?.appendSystemPrompt + if (appendSystemPrompt) { + return `${basePrompt}\n\n${appendSystemPrompt}` + } + // kilocode_change end + return basePrompt } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 1a6201f6a29..6475143e2d0 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2705,6 +2705,7 @@ export class ClineProvider return false } })(), + appendSystemPrompt: stateValues.appendSystemPrompt, // kilocode_change: CLI append system prompt } } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 1139b4864d4..3e8482f103a 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -543,6 +543,7 @@ export type ExtensionState = Pick< showTimestamps?: boolean // kilocode_change: Show timestamps in chat messages debug?: boolean speechToTextStatus?: { available: boolean; reason?: "openaiKeyMissing" | "ffmpegNotInstalled" } // kilocode_change: Speech-to-text availability status with failure reason + appendSystemPrompt?: string // kilocode_change: Custom text to append to system prompt (CLI only) } export interface ClineSayTool {