mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 06:18:55 +00:00
Merge pull request #976 from thepa1n/feat/cli-append-system-prompt-file
feat(cli): add --append-system-prompt-file support
This commit is contained in:
@@ -1050,6 +1050,7 @@ async function main(): Promise<void> {
|
||||
: getSettingsPath(profileInfo.name));
|
||||
const settings = resolvedSettings ?? loadSettings(expandedSettingsPath);
|
||||
const cliproxyBridge = resolvedCliproxyBridge ?? resolveCliproxyBridgeMetadata(settings);
|
||||
|
||||
let imageAnalysisFallbackHookReady: boolean | undefined;
|
||||
if (resolvedTarget === 'claude') {
|
||||
if (imageAnalysisMcpReady) {
|
||||
@@ -1315,6 +1316,7 @@ async function main(): Promise<void> {
|
||||
profileType: profileInfo.type,
|
||||
settingsPath: expandedSettingsPath,
|
||||
});
|
||||
|
||||
execClaude(claudeCli, launchArgs, { ...envVars, ...traceEnv });
|
||||
} else if (profileInfo.type === 'account') {
|
||||
// NEW FLOW: Account-based profile (work, personal)
|
||||
|
||||
@@ -1,31 +1,48 @@
|
||||
/**
|
||||
* Claude launch argument helpers for first-class Image Analysis.
|
||||
*
|
||||
* Uses the same prompt injection mode as the user to avoid mixing
|
||||
* `--append-system-prompt` and `--append-system-prompt-file` in one request.
|
||||
*/
|
||||
|
||||
import {
|
||||
hasExactFlagValue as hasExactClaudeFlagValue,
|
||||
splitArgsAtTerminator as splitClaudeArgsAtTerminator,
|
||||
} from '../claude-tool-args';
|
||||
import {
|
||||
buildSteeringArg,
|
||||
hasManagedPromptFileArg,
|
||||
PROMPT_FLAG_INLINE,
|
||||
} from '../prompt-injection-strategy';
|
||||
|
||||
const APPEND_SYSTEM_PROMPT_FLAG = '--append-system-prompt';
|
||||
const IMAGE_ANALYSIS_STEERING_PROMPT =
|
||||
'For local image or PDF files, prefer the CCS MCP tool ImageAnalysis instead of Read. Use Read for text, code, and other plain files. If the user asks a specific question about the visual, pass that question as the focus field when useful. If ImageAnalysis is unavailable or fails, you may fall back to Read.';
|
||||
const IMAGE_ANALYSIS_STEERING_PROMPT = {
|
||||
name: 'ccs-prompt-image-analysis-tool',
|
||||
content:
|
||||
'For local image or PDF files, prefer the CCS MCP tool ImageAnalysis instead of Read. Use Read for text, code, and other plain files. If the user asks a specific question about the visual, pass that question as the focus field when useful. If ImageAnalysis is unavailable or fails, you may fall back to Read.',
|
||||
};
|
||||
|
||||
function ensureImageAnalysisSteeringPrompt(args: string[]): string[] {
|
||||
const { optionArgs, trailingArgs } = splitClaudeArgsAtTerminator(args);
|
||||
|
||||
if (
|
||||
hasExactClaudeFlagValue(optionArgs, APPEND_SYSTEM_PROMPT_FLAG, IMAGE_ANALYSIS_STEERING_PROMPT)
|
||||
hasExactClaudeFlagValue(optionArgs, PROMPT_FLAG_INLINE, IMAGE_ANALYSIS_STEERING_PROMPT.content)
|
||||
) {
|
||||
return args;
|
||||
}
|
||||
|
||||
return [
|
||||
...optionArgs,
|
||||
APPEND_SYSTEM_PROMPT_FLAG,
|
||||
IMAGE_ANALYSIS_STEERING_PROMPT,
|
||||
...trailingArgs,
|
||||
];
|
||||
if (
|
||||
hasManagedPromptFileArg({ args: optionArgs, promptName: IMAGE_ANALYSIS_STEERING_PROMPT.name })
|
||||
) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const steeringArg = buildSteeringArg({
|
||||
args: optionArgs,
|
||||
promptName: IMAGE_ANALYSIS_STEERING_PROMPT.name,
|
||||
promptContent: IMAGE_ANALYSIS_STEERING_PROMPT.content,
|
||||
});
|
||||
|
||||
return [...optionArgs, ...steeringArg, ...trailingArgs];
|
||||
}
|
||||
|
||||
export function appendThirdPartyImageAnalysisToolArgs(args: string[]): string[] {
|
||||
@@ -33,5 +50,5 @@ export function appendThirdPartyImageAnalysisToolArgs(args: string[]): string[]
|
||||
}
|
||||
|
||||
export function getImageAnalysisSteeringPrompt(): string {
|
||||
return IMAGE_ANALYSIS_STEERING_PROMPT;
|
||||
return IMAGE_ANALYSIS_STEERING_PROMPT.content;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Shared prompt injection strategy.
|
||||
*
|
||||
* Detects which prompt injection mode the user is using and ensures CCS
|
||||
* always uses the SAME mode so Claude CLI never receives mixed
|
||||
* `--append-system-prompt` and `--append-system-prompt-file` flags.
|
||||
*
|
||||
* Rules:
|
||||
* - User passes `--append-system-prompt` → all CCS prompts use inline
|
||||
* - User passes `--append-system-prompt-file` → all CCS prompts use file
|
||||
* - Neither present → default to inline (`--append-system-prompt`)
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getCcsDir } from './config-manager';
|
||||
|
||||
export type PromptInjectionMode = 'inline' | 'file';
|
||||
|
||||
/** `--append-system-prompt` — inline prompt text */
|
||||
export const PROMPT_FLAG_INLINE = '--append-system-prompt';
|
||||
/** `--append-system-prompt-file` — prompt read from file */
|
||||
export const PROMPT_FLAG_FILE = '--append-system-prompt-file';
|
||||
|
||||
function getManagedPromptsDir(): string {
|
||||
return path.join(getCcsDir(), 'prompts');
|
||||
}
|
||||
|
||||
export function getManagedPromptFileName(promptName: string): string {
|
||||
return `${promptName}.txt`;
|
||||
}
|
||||
|
||||
export function getManagedPromptFilePath(promptName: string): string {
|
||||
return path.join(getManagedPromptsDir(), getManagedPromptFileName(promptName));
|
||||
}
|
||||
|
||||
export function hasManagedPromptFileArg(params: { args: string[]; promptName: string }): boolean {
|
||||
const expectedPath = path.resolve(getManagedPromptFilePath(params.promptName));
|
||||
|
||||
for (let index = 0; index < params.args.length; index += 1) {
|
||||
const arg = params.args[index];
|
||||
|
||||
if (arg === PROMPT_FLAG_FILE) {
|
||||
const filePath = params.args[index + 1];
|
||||
if (filePath && path.resolve(filePath) === expectedPath) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
arg.startsWith(`${PROMPT_FLAG_FILE}=`) &&
|
||||
path.resolve(arg.slice(PROMPT_FLAG_FILE.length + 1)) === expectedPath
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect which prompt injection mode to use based on user-provided args.
|
||||
*
|
||||
* - `--append-system-prompt-file` found (space or `=` form) → 'file'
|
||||
* - `--append-system-prompt` found (space or `=` form) → 'inline'
|
||||
* - Neither → 'inline' (default)
|
||||
*/
|
||||
export function detectPromptInjectionMode(args: string[]): PromptInjectionMode {
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === PROMPT_FLAG_FILE || arg.startsWith(`${PROMPT_FLAG_FILE}=`)) {
|
||||
return 'file';
|
||||
}
|
||||
}
|
||||
|
||||
return 'inline';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `--append-system-prompt <text>` arg pair.
|
||||
*/
|
||||
export function buildInlineSteeringArg(params: { promptContent: string }): string[] {
|
||||
return [PROMPT_FLAG_INLINE, params.promptContent];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `--append-system-prompt-file <path>` arg pair.
|
||||
* Writes the prompt to a temp file first.
|
||||
*/
|
||||
export function buildFileSteeringArg(params: {
|
||||
promptFileName: string;
|
||||
promptContent: string;
|
||||
}): string[] {
|
||||
const promptsFolder = getManagedPromptsDir();
|
||||
|
||||
if (!fs.existsSync(promptsFolder)) {
|
||||
fs.mkdirSync(promptsFolder, { recursive: true });
|
||||
}
|
||||
|
||||
const promptFile = path.join(promptsFolder, params.promptFileName);
|
||||
|
||||
fs.writeFileSync(promptFile, params.promptContent);
|
||||
|
||||
return [PROMPT_FLAG_FILE, promptFile];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build steering prompt args in the given mode.
|
||||
*/
|
||||
export function buildSteeringArg(params: {
|
||||
args: string[];
|
||||
promptName: string;
|
||||
promptContent: string;
|
||||
}): string[] {
|
||||
const mode = detectPromptInjectionMode(params.args);
|
||||
|
||||
if (mode === 'file') {
|
||||
return buildFileSteeringArg({
|
||||
promptFileName: getManagedPromptFileName(params.promptName),
|
||||
promptContent: params.promptContent,
|
||||
});
|
||||
}
|
||||
|
||||
return buildInlineSteeringArg({ promptContent: params.promptContent });
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
/**
|
||||
* Claude launch argument helpers for third-party WebSearch.
|
||||
*
|
||||
* Uses the same prompt injection mode as the user to avoid mixing
|
||||
* `--append-system-prompt` and `--append-system-prompt-file` in one request.
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -7,12 +10,19 @@ import {
|
||||
hasExactFlagValue as hasExactClaudeFlagValue,
|
||||
splitArgsAtTerminator as splitClaudeArgsAtTerminator,
|
||||
} from '../claude-tool-args';
|
||||
import {
|
||||
buildSteeringArg,
|
||||
hasManagedPromptFileArg,
|
||||
PROMPT_FLAG_INLINE,
|
||||
} from '../prompt-injection-strategy';
|
||||
|
||||
const NATIVE_WEBSEARCH_TOOL = 'WebSearch';
|
||||
const DISALLOWED_TOOLS_FLAG = '--disallowedTools';
|
||||
const APPEND_SYSTEM_PROMPT_FLAG = '--append-system-prompt';
|
||||
const THIRD_PARTY_WEBSEARCH_STEERING_PROMPT =
|
||||
'For web lookup or current-information requests, prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches. If the user explicitly wants shell commands, or WebSearch is unavailable or fails, you may fall back to Bash/network tools.';
|
||||
export const THIRD_PARTY_WEBSEARCH_STEERING_PROMPT = {
|
||||
name: 'ccs-prompt-websearch-tool',
|
||||
content:
|
||||
'For web lookup or current-information requests, prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches. If the user explicitly wants shell commands, or WebSearch is unavailable or fails, you may fall back to Bash/network tools.',
|
||||
};
|
||||
|
||||
function parseToolValue(rawValue: string): string[] {
|
||||
return rawValue
|
||||
@@ -99,19 +109,29 @@ function ensureWebSearchSteeringPrompt(args: string[]): string[] {
|
||||
if (
|
||||
hasExactClaudeFlagValue(
|
||||
optionArgs,
|
||||
APPEND_SYSTEM_PROMPT_FLAG,
|
||||
THIRD_PARTY_WEBSEARCH_STEERING_PROMPT
|
||||
PROMPT_FLAG_INLINE,
|
||||
THIRD_PARTY_WEBSEARCH_STEERING_PROMPT.content
|
||||
)
|
||||
) {
|
||||
return args;
|
||||
}
|
||||
|
||||
return [
|
||||
...optionArgs,
|
||||
APPEND_SYSTEM_PROMPT_FLAG,
|
||||
THIRD_PARTY_WEBSEARCH_STEERING_PROMPT,
|
||||
...trailingArgs,
|
||||
];
|
||||
if (
|
||||
hasManagedPromptFileArg({
|
||||
args: optionArgs,
|
||||
promptName: THIRD_PARTY_WEBSEARCH_STEERING_PROMPT.name,
|
||||
})
|
||||
) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const steeringArgs = buildSteeringArg({
|
||||
args: optionArgs,
|
||||
promptName: THIRD_PARTY_WEBSEARCH_STEERING_PROMPT.name,
|
||||
promptContent: THIRD_PARTY_WEBSEARCH_STEERING_PROMPT.content,
|
||||
});
|
||||
|
||||
return [...optionArgs, ...steeringArgs, ...trailingArgs];
|
||||
}
|
||||
|
||||
export function appendThirdPartyWebSearchToolArgs(args: string[]): string[] {
|
||||
|
||||
@@ -11,13 +11,12 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { getCcsDir } from '../config-manager';
|
||||
import { createLogger } from '../../services/logging';
|
||||
import { hasManagedPromptFileArg, PROMPT_FLAG_INLINE } from '../prompt-injection-strategy';
|
||||
import { THIRD_PARTY_WEBSEARCH_STEERING_PROMPT } from './claude-tool-args';
|
||||
|
||||
const TRACE_FILE_NAME = 'websearch-trace.jsonl';
|
||||
const NATIVE_WEBSEARCH_TOOL = 'WebSearch';
|
||||
const DISALLOWED_TOOLS_FLAG = '--disallowedTools';
|
||||
const APPEND_SYSTEM_PROMPT_FLAG = '--append-system-prompt';
|
||||
const THIRD_PARTY_WEBSEARCH_STEERING_PROMPT =
|
||||
'For web lookup or current-information requests, prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches. If the user explicitly wants shell commands, or WebSearch is unavailable or fails, you may fall back to Bash/network tools.';
|
||||
const logger = createLogger('websearch');
|
||||
|
||||
function parseToolValue(rawValue: string): string[] {
|
||||
@@ -58,14 +57,23 @@ function hasToolInFlag(args: string[], flag: string, toolName: string): boolean
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasExactFlagValue(args: string[], flag: string, expectedValue: string): boolean {
|
||||
function hasExactFlagValue(params: {
|
||||
args: string[];
|
||||
flag: string;
|
||||
expectedValue: string;
|
||||
}): boolean {
|
||||
const { args, flag, expectedValue } = params;
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
|
||||
if (arg === flag) {
|
||||
if (getImmediateFlagValue(args, index) === expectedValue) {
|
||||
const immediateFlagValue = getImmediateFlagValue(args, index);
|
||||
|
||||
if (immediateFlagValue === expectedValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -179,16 +187,30 @@ function buildLaunchId(): string {
|
||||
return `websearch-${Date.now()}-${process.pid}-${random}`;
|
||||
}
|
||||
|
||||
function hasSteeringPromptInArgs(args: string[]): boolean {
|
||||
if (
|
||||
hasExactFlagValue({
|
||||
args,
|
||||
flag: PROMPT_FLAG_INLINE,
|
||||
expectedValue: THIRD_PARTY_WEBSEARCH_STEERING_PROMPT.content,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasManagedPromptFileArg({ args, promptName: THIRD_PARTY_WEBSEARCH_STEERING_PROMPT.name })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function summarizeLaunchArgs(args: string[]): Record<string, unknown> {
|
||||
return {
|
||||
argCount: args.length,
|
||||
hasSettingsFlag: args.includes('--settings'),
|
||||
nativeWebSearchDisallowed: hasToolInFlag(args, DISALLOWED_TOOLS_FLAG, NATIVE_WEBSEARCH_TOOL),
|
||||
steeringPromptApplied: hasExactFlagValue(
|
||||
args,
|
||||
APPEND_SYSTEM_PROMPT_FLAG,
|
||||
THIRD_PARTY_WEBSEARCH_STEERING_PROMPT
|
||||
),
|
||||
steeringPromptApplied: hasSteeringPromptInArgs(args),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,4 +36,47 @@ describe('appendThirdPartyImageAnalysisToolArgs', () => {
|
||||
'extra',
|
||||
]);
|
||||
});
|
||||
|
||||
// File mode: --append-system-prompt-file when user passes --append-system-prompt-file
|
||||
|
||||
it('uses --append-system-prompt-file when user passes --append-system-prompt-file', () => {
|
||||
const result = appendThirdPartyImageAnalysisToolArgs([
|
||||
'-p',
|
||||
'describe',
|
||||
'--append-system-prompt-file',
|
||||
'/tmp/user-prompt.txt',
|
||||
]);
|
||||
|
||||
expect(result).toContain('--append-system-prompt-file');
|
||||
expect(result).not.toContain('--append-system-prompt');
|
||||
const fileFlags = result.filter((arg) => arg === '--append-system-prompt-file');
|
||||
expect(fileFlags.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('uses --append-system-prompt-file when user passes equals form', () => {
|
||||
const result = appendThirdPartyImageAnalysisToolArgs([
|
||||
'-p',
|
||||
'describe',
|
||||
'--append-system-prompt-file=/tmp/user-prompt.txt',
|
||||
]);
|
||||
|
||||
expect(result).not.toContain('--append-system-prompt');
|
||||
const fileFlags = result.filter(
|
||||
(arg) => arg === '--append-system-prompt-file' || arg.startsWith('--append-system-prompt-file=')
|
||||
);
|
||||
expect(fileFlags.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('does not treat unrelated user prompt files as the managed CCS steering prompt', () => {
|
||||
const result = appendThirdPartyImageAnalysisToolArgs([
|
||||
'-p',
|
||||
'describe',
|
||||
'--append-system-prompt-file',
|
||||
'/tmp/user-ccs-prompt-image-analysis-tool-notes.txt',
|
||||
]);
|
||||
|
||||
const filePaths = result.filter((arg, index) => result[index - 1] === '--append-system-prompt-file');
|
||||
expect(filePaths).toContain('/tmp/user-ccs-prompt-image-analysis-tool-notes.txt');
|
||||
expect(filePaths.some((filePath) => filePath.endsWith('/ccs-prompt-image-analysis-tool.txt'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
buildInlineSteeringArg,
|
||||
buildFileSteeringArg,
|
||||
buildSteeringArg,
|
||||
detectPromptInjectionMode,
|
||||
getManagedPromptFilePath,
|
||||
hasManagedPromptFileArg,
|
||||
PROMPT_FLAG_INLINE,
|
||||
PROMPT_FLAG_FILE,
|
||||
} from '../../../src/utils/prompt-injection-strategy';
|
||||
|
||||
let originalCcsHome: string | undefined;
|
||||
let tempHome: string;
|
||||
|
||||
beforeEach(() => {
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-prompt-strategy-'));
|
||||
process.env.CCS_HOME = tempHome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCcsHome === undefined) {
|
||||
delete process.env.CCS_HOME;
|
||||
} else {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
}
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('detectPromptInjectionMode', () => {
|
||||
it('returns inline when no prompt flags present', () => {
|
||||
expect(detectPromptInjectionMode(['-p', 'hello'])).toBe('inline');
|
||||
});
|
||||
|
||||
it('returns inline when only --append-system-prompt is present', () => {
|
||||
expect(detectPromptInjectionMode(['--append-system-prompt', 'test'])).toBe('inline');
|
||||
});
|
||||
|
||||
it('returns inline when --append-system-prompt equals form is present', () => {
|
||||
expect(detectPromptInjectionMode(['--append-system-prompt=test'])).toBe('inline');
|
||||
});
|
||||
|
||||
it('returns file when --append-system-prompt-file is present', () => {
|
||||
expect(detectPromptInjectionMode(['--append-system-prompt-file', '/tmp/p.txt'])).toBe('file');
|
||||
});
|
||||
|
||||
it('returns file when --append-system-prompt-file equals form is present', () => {
|
||||
expect(detectPromptInjectionMode(['--append-system-prompt-file=/tmp/p.txt'])).toBe('file');
|
||||
});
|
||||
|
||||
it('returns file even when --append-system-prompt is also present', () => {
|
||||
expect(
|
||||
detectPromptInjectionMode([
|
||||
'--append-system-prompt',
|
||||
'inline-text',
|
||||
'--append-system-prompt-file',
|
||||
'/tmp/p.txt',
|
||||
])
|
||||
).toBe('file');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildInlineSteeringArg', () => {
|
||||
it('returns inline flag and prompt text', () => {
|
||||
expect(buildInlineSteeringArg({promptContent: 'hello world'})).toEqual(['--append-system-prompt', 'hello world']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildFileSteeringArg', () => {
|
||||
it('returns file flag and writes the prompt into the isolated CCS home', () => {
|
||||
const result = buildFileSteeringArg({
|
||||
promptFileName: 'ccs-test-prompt.txt',
|
||||
promptContent: 'hello world',
|
||||
});
|
||||
|
||||
expect(result[0]).toBe('--append-system-prompt-file');
|
||||
expect(result[1]).toBe(path.join(tempHome, '.ccs', 'prompts', 'ccs-test-prompt.txt'));
|
||||
expect(fs.readFileSync(result[1], 'utf8')).toBe('hello world');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasManagedPromptFileArg', () => {
|
||||
it('returns true for the exact CCS-managed prompt path', () => {
|
||||
expect(
|
||||
hasManagedPromptFileArg({
|
||||
args: [PROMPT_FLAG_FILE, getManagedPromptFilePath('ccs-test')],
|
||||
promptName: 'ccs-test',
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for unrelated user files that only contain the prompt name', () => {
|
||||
expect(
|
||||
hasManagedPromptFileArg({
|
||||
args: [PROMPT_FLAG_FILE, '/tmp/user-ccs-test-notes.txt'],
|
||||
promptName: 'ccs-test',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildSteeringArg', () => {
|
||||
it('delegates to inline in inline mode', () => {
|
||||
expect(
|
||||
buildSteeringArg({
|
||||
args: [PROMPT_FLAG_INLINE],
|
||||
promptName: 'ignored.txt',
|
||||
promptContent: 'hello',
|
||||
})
|
||||
).toEqual(['--append-system-prompt', 'hello']);
|
||||
});
|
||||
|
||||
it('delegates to file in file mode', () => {
|
||||
const result = buildSteeringArg({
|
||||
args: [PROMPT_FLAG_FILE],
|
||||
promptName: 'ccs-test',
|
||||
promptContent: 'hello',
|
||||
});
|
||||
expect(result[0]).toBe('--append-system-prompt-file');
|
||||
expect(result[1]).toBe(getManagedPromptFilePath('ccs-test'));
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ const STEERING_PROMPT =
|
||||
'For web lookup or current-information requests, prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches. If the user explicitly wants shell commands, or WebSearch is unavailable or fails, you may fall back to Bash/network tools.';
|
||||
|
||||
describe('appendThirdPartyWebSearchToolArgs', () => {
|
||||
it('appends native WebSearch suppression and steering prompt when no tool flags are present', () => {
|
||||
it('appends native WebSearch suppression and inline steering prompt when no prompt flags are present', () => {
|
||||
expect(appendThirdPartyWebSearchToolArgs(['smoke'])).toEqual([
|
||||
'smoke',
|
||||
'--disallowedTools',
|
||||
@@ -129,4 +129,44 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
|
||||
STEERING_PROMPT,
|
||||
]);
|
||||
});
|
||||
|
||||
// File mode: --append-system-prompt-file when user passes --append-system-prompt-file
|
||||
|
||||
it('uses --append-system-prompt-file when user passes --append-system-prompt-file', () => {
|
||||
const result = appendThirdPartyWebSearchToolArgs([
|
||||
'smoke',
|
||||
'--append-system-prompt-file',
|
||||
'/tmp/user-prompt.txt',
|
||||
]);
|
||||
expect(result).toContain('--disallowedTools');
|
||||
expect(result).toContain('WebSearch');
|
||||
const fileFlags = result.filter((arg) => arg === '--append-system-prompt-file');
|
||||
expect(fileFlags.length).toBeGreaterThanOrEqual(2);
|
||||
// No inline flag should be present
|
||||
expect(result).not.toContain('--append-system-prompt');
|
||||
});
|
||||
|
||||
it('uses --append-system-prompt-file when user passes --append-system-prompt-file= form', () => {
|
||||
const result = appendThirdPartyWebSearchToolArgs([
|
||||
'smoke',
|
||||
'--append-system-prompt-file=/tmp/user-prompt.txt',
|
||||
]);
|
||||
const fileFlags = result.filter(
|
||||
(arg) => arg === '--append-system-prompt-file' || arg.startsWith('--append-system-prompt-file=')
|
||||
);
|
||||
expect(fileFlags.length).toBeGreaterThanOrEqual(2);
|
||||
expect(result).not.toContain('--append-system-prompt');
|
||||
});
|
||||
|
||||
it('does not treat unrelated user prompt files as the managed CCS steering prompt', () => {
|
||||
const result = appendThirdPartyWebSearchToolArgs([
|
||||
'smoke',
|
||||
'--append-system-prompt-file',
|
||||
'/tmp/user-ccs-prompt-websearch-tool-notes.txt',
|
||||
]);
|
||||
|
||||
const filePaths = result.filter((arg, index) => result[index - 1] === '--append-system-prompt-file');
|
||||
expect(filePaths).toContain('/tmp/user-ccs-prompt-websearch-tool-notes.txt');
|
||||
expect(filePaths.some((filePath) => filePath.endsWith('/ccs-prompt-websearch-tool.txt'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user