mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 18:15:48 +00:00
235 lines
6.3 KiB
TypeScript
235 lines
6.3 KiB
TypeScript
/**
|
|
* Best-effort WebSearch trace helpers.
|
|
*
|
|
* Writes opt-in JSONL trace records to ~/.ccs/logs/websearch-trace.jsonl so
|
|
* CCS can explain launch intent, MCP exposure, provider selection, and likely
|
|
* bypass scenarios without polluting Claude/MCP stdout.
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { getCcsDir } from '../config-manager';
|
|
|
|
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.';
|
|
|
|
function parseToolValue(rawValue: string): string[] {
|
|
return rawValue
|
|
.split(',')
|
|
.map((value) => value.trim())
|
|
.filter((value) => value.length > 0);
|
|
}
|
|
|
|
function getImmediateFlagValue(args: string[], index: number): string | null {
|
|
const value = args[index + 1];
|
|
if (value === undefined || value === '--' || value.startsWith('--')) {
|
|
return null;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function hasToolInFlag(args: string[], flag: string, toolName: string): boolean {
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const arg = args[index];
|
|
|
|
if (arg === flag) {
|
|
const value = getImmediateFlagValue(args, index);
|
|
if (value && parseToolValue(value).includes(toolName)) {
|
|
return true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (arg.startsWith(`${flag}=`)) {
|
|
const rawValue = arg.slice(flag.length + 1);
|
|
if (parseToolValue(rawValue).includes(toolName)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function hasExactFlagValue(args: string[], flag: string, expectedValue: string): boolean {
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const arg = args[index];
|
|
|
|
if (arg === flag) {
|
|
if (getImmediateFlagValue(args, index) === expectedValue) {
|
|
return true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (arg === `${flag}=${expectedValue}`) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function normalizeSafePrefix(inputPath: string): string {
|
|
return `${path.resolve(inputPath)}${path.sep}`;
|
|
}
|
|
|
|
function getSafeTracePrefixes(): string[] {
|
|
return [
|
|
normalizeSafePrefix(path.join(getCcsDir(), 'logs')),
|
|
normalizeSafePrefix(os.tmpdir()),
|
|
normalizeSafePrefix('/var/log'),
|
|
];
|
|
}
|
|
|
|
export function resolveAllowedWebSearchTraceFile(
|
|
env: NodeJS.ProcessEnv = process.env
|
|
): string | null {
|
|
const configured = env.CCS_WEBSEARCH_TRACE_FILE?.trim();
|
|
if (!configured) {
|
|
return null;
|
|
}
|
|
|
|
const resolved = path.resolve(configured);
|
|
if (getSafeTracePrefixes().some((prefix) => resolved.startsWith(prefix))) {
|
|
return resolved;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getTraceFilePath(env: NodeJS.ProcessEnv): string {
|
|
return resolveAllowedWebSearchTraceFile(env) ?? path.join(getCcsDir(), 'logs', TRACE_FILE_NAME);
|
|
}
|
|
|
|
export function isWebSearchTraceEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
return env.CCS_WEBSEARCH_TRACE === '1' || env.CCS_DEBUG === '1';
|
|
}
|
|
|
|
export function appendWebSearchTrace(
|
|
event: string,
|
|
payload: Record<string, unknown> = {},
|
|
env: NodeJS.ProcessEnv = process.env
|
|
): void {
|
|
if (!isWebSearchTraceEnabled(env)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const traceFilePath = getTraceFilePath(env);
|
|
fs.mkdirSync(path.dirname(traceFilePath), { recursive: true });
|
|
fs.appendFileSync(
|
|
traceFilePath,
|
|
JSON.stringify({
|
|
at: new Date().toISOString(),
|
|
event,
|
|
launchId: env.CCS_WEBSEARCH_TRACE_LAUNCH_ID || null,
|
|
launcher: env.CCS_WEBSEARCH_TRACE_LAUNCHER || null,
|
|
profileType: env.CCS_PROFILE_TYPE || null,
|
|
pid: process.pid,
|
|
...payload,
|
|
}) + '\n',
|
|
'utf8'
|
|
);
|
|
} catch {
|
|
// Tracing must never affect launch behavior.
|
|
}
|
|
}
|
|
|
|
export function readWebSearchTraceRecords(
|
|
launchId: string,
|
|
env: NodeJS.ProcessEnv = process.env
|
|
): Array<Record<string, unknown>> {
|
|
if (!launchId) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const traceFilePath = getTraceFilePath(env);
|
|
if (!fs.existsSync(traceFilePath)) {
|
|
return [];
|
|
}
|
|
|
|
return fs
|
|
.readFileSync(traceFilePath, 'utf8')
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.filter((line) => line.length > 0)
|
|
.map((line) => JSON.parse(line) as Record<string, unknown>)
|
|
.filter((record) => record.launchId === launchId);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function buildLaunchId(): string {
|
|
const random = Math.random().toString(36).slice(2, 10);
|
|
return `websearch-${Date.now()}-${process.pid}-${random}`;
|
|
}
|
|
|
|
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
|
|
),
|
|
};
|
|
}
|
|
|
|
export function createWebSearchTraceContext(params: {
|
|
launcher: string;
|
|
args: string[];
|
|
cwd?: string;
|
|
profile?: string;
|
|
profileType?: string;
|
|
settingsPath?: string;
|
|
claudeConfigDir?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
}): Record<string, string> {
|
|
const env = params.env ?? process.env;
|
|
if (!isWebSearchTraceEnabled(env)) {
|
|
return {};
|
|
}
|
|
|
|
const launchId = buildLaunchId();
|
|
const traceEnv: Record<string, string> = {
|
|
CCS_WEBSEARCH_TRACE: '1',
|
|
CCS_WEBSEARCH_TRACE_LAUNCH_ID: launchId,
|
|
CCS_WEBSEARCH_TRACE_LAUNCHER: params.launcher,
|
|
};
|
|
|
|
const traceFileOverride = resolveAllowedWebSearchTraceFile(env);
|
|
if (traceFileOverride) {
|
|
traceEnv.CCS_WEBSEARCH_TRACE_FILE = traceFileOverride;
|
|
}
|
|
|
|
appendWebSearchTrace(
|
|
'ccs_websearch_launch',
|
|
{
|
|
launcher: params.launcher,
|
|
profile: params.profile || null,
|
|
profileType: params.profileType || null,
|
|
cwd: params.cwd || null,
|
|
settingsPath: params.settingsPath || null,
|
|
claudeConfigDir: params.claudeConfigDir || null,
|
|
...summarizeLaunchArgs(params.args),
|
|
},
|
|
{
|
|
...env,
|
|
...traceEnv,
|
|
CCS_PROFILE_TYPE: params.profileType || env.CCS_PROFILE_TYPE || '',
|
|
}
|
|
);
|
|
|
|
return traceEnv;
|
|
}
|