@jsonstudio/llms 0.6.1739 → 0.6.1890
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversion/compat/actions/deepseek-web-request.d.ts +3 -0
- package/dist/conversion/compat/actions/deepseek-web-request.js +350 -0
- package/dist/conversion/compat/actions/deepseek-web-response.d.ts +3 -0
- package/dist/conversion/compat/actions/deepseek-web-response.js +886 -0
- package/dist/conversion/compat/actions/gemini-cli-request.js +3 -1
- package/dist/conversion/compat/profiles/chat-deepseek-web.json +18 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +166 -2
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +169 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +12 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +4 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +365 -144
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +9 -0
- package/dist/conversion/hub/policy/policy-engine.d.ts +2 -0
- package/dist/conversion/hub/policy/policy-engine.js +8 -0
- package/dist/conversion/hub/process/chat-process.js +466 -16
- package/dist/conversion/hub/response/provider-response.js +0 -35
- package/dist/conversion/responses/responses-openai-bridge.d.ts +2 -0
- package/dist/conversion/responses/responses-openai-bridge.js +166 -8
- package/dist/conversion/shared/anthropic-message-utils.js +10 -1
- package/dist/conversion/shared/protocol-field-allowlists.d.ts +2 -2
- package/dist/conversion/shared/protocol-field-allowlists.js +4 -0
- package/dist/conversion/shared/tool-governor.js +102 -0
- package/dist/guidance/index.js +17 -0
- package/dist/router/virtual-router/bootstrap.js +46 -1
- package/dist/router/virtual-router/classifier.js +59 -4
- package/dist/router/virtual-router/engine/health/index.js +6 -6
- package/dist/router/virtual-router/engine/routing-state/store.js +16 -3
- package/dist/router/virtual-router/engine-logging.js +62 -24
- package/dist/router/virtual-router/engine-selection/route-utils.js +20 -20
- package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -2
- package/dist/router/virtual-router/engine.d.ts +3 -1
- package/dist/router/virtual-router/engine.js +359 -39
- package/dist/router/virtual-router/features.js +2 -1
- package/dist/router/virtual-router/pre-command-file-resolver.d.ts +2 -0
- package/dist/router/virtual-router/pre-command-file-resolver.js +90 -0
- package/dist/router/virtual-router/provider-registry.js +3 -1
- package/dist/router/virtual-router/routing-instructions.d.ts +15 -1
- package/dist/router/virtual-router/routing-instructions.js +110 -151
- package/dist/router/virtual-router/routing-pre-command-actions.d.ts +3 -0
- package/dist/router/virtual-router/routing-pre-command-actions.js +26 -0
- package/dist/router/virtual-router/routing-pre-command-parser.d.ts +2 -0
- package/dist/router/virtual-router/routing-pre-command-parser.js +85 -0
- package/dist/router/virtual-router/routing-pre-command-state-codec.d.ts +3 -0
- package/dist/router/virtual-router/routing-pre-command-state-codec.js +24 -0
- package/dist/router/virtual-router/routing-stop-message-actions.d.ts +2 -0
- package/dist/router/virtual-router/routing-stop-message-actions.js +96 -0
- package/dist/router/virtual-router/routing-stop-message-parser.d.ts +3 -0
- package/dist/router/virtual-router/routing-stop-message-parser.js +142 -0
- package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +4 -0
- package/dist/router/virtual-router/routing-stop-message-state-codec.js +85 -0
- package/dist/router/virtual-router/sticky-session-store.js +206 -57
- package/dist/router/virtual-router/stop-message-stage-template-files.d.ts +12 -0
- package/dist/router/virtual-router/stop-message-stage-template-files.js +67 -0
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
- package/dist/router/virtual-router/stop-message-state-sync.js +5 -0
- package/dist/router/virtual-router/token-file-scanner.d.ts +9 -0
- package/dist/router/virtual-router/token-file-scanner.js +64 -3
- package/dist/router/virtual-router/tool-signals.d.ts +5 -0
- package/dist/router/virtual-router/tool-signals.js +42 -3
- package/dist/router/virtual-router/types.d.ts +19 -1
- package/dist/router/virtual-router/types.js +1 -0
- package/dist/servertool/clock/config.d.ts +1 -1
- package/dist/servertool/clock/config.js +27 -4
- package/dist/servertool/clock/state.js +41 -2
- package/dist/servertool/clock/task-store.d.ts +2 -2
- package/dist/servertool/clock/task-store.js +1 -1
- package/dist/servertool/clock/tasks.d.ts +3 -1
- package/dist/servertool/clock/tasks.js +209 -18
- package/dist/servertool/clock/types.d.ts +17 -0
- package/dist/servertool/continue-execution/log.d.ts +3 -0
- package/dist/servertool/continue-execution/log.js +13 -0
- package/dist/servertool/engine.js +414 -68
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +6 -6
- package/dist/servertool/handlers/clock-auto.js +54 -71
- package/dist/servertool/handlers/clock.js +121 -6
- package/dist/servertool/handlers/continue-execution.d.ts +1 -0
- package/dist/servertool/handlers/continue-execution.js +91 -0
- package/dist/servertool/handlers/followup-request-builder.js +13 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -1
- package/dist/servertool/handlers/iflow-model-error-retry.js +1 -1
- package/dist/servertool/handlers/recursive-detection-guard.js +1 -1
- package/dist/servertool/handlers/stop-message-auto.js +386 -257
- package/dist/servertool/handlers/stop-message-stage-policy.d.ts +43 -0
- package/dist/servertool/handlers/stop-message-stage-policy.js +684 -0
- package/dist/servertool/handlers/vision.js +1 -1
- package/dist/servertool/log/progress-file.d.ts +14 -0
- package/dist/servertool/log/progress-file.js +88 -0
- package/dist/servertool/pre-command-hooks.d.ts +17 -0
- package/dist/servertool/pre-command-hooks.js +491 -0
- package/dist/servertool/registry.d.ts +23 -6
- package/dist/servertool/registry.js +66 -1
- package/dist/servertool/server-side-tools.d.ts +1 -0
- package/dist/servertool/server-side-tools.js +216 -14
- package/dist/servertool/stop-gateway-context.d.ts +14 -0
- package/dist/servertool/stop-gateway-context.js +167 -0
- package/dist/servertool/stop-message-compare-context.d.ts +24 -0
- package/dist/servertool/stop-message-compare-context.js +133 -0
- package/dist/servertool/types.d.ts +12 -0
- package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -0
- package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +36 -1
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +3 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +3 -0
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +118 -1
- package/dist/tools/apply-patch/args-normalizer/default-actions.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ServerToolProgressFileEvent = {
|
|
2
|
+
requestId: string;
|
|
3
|
+
flowId: string;
|
|
4
|
+
tool: string;
|
|
5
|
+
stage: string;
|
|
6
|
+
result: string;
|
|
7
|
+
message: string;
|
|
8
|
+
step: number;
|
|
9
|
+
entryEndpoint?: string;
|
|
10
|
+
providerProtocol?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function appendServerToolProgressFileEvent(event: ServerToolProgressFileEvent): void;
|
|
13
|
+
export declare function resetServerToolProgressFileLoggerForTests(): void;
|
|
14
|
+
export declare function flushServerToolProgressFileLoggerForTests(): Promise<void>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
const truthy = new Set(['1', 'true', 'yes', 'on']);
|
|
5
|
+
const falsy = new Set(['0', 'false', 'no', 'off']);
|
|
6
|
+
const DEFAULT_LOG_PATH = path.join(os.homedir(), '.routecodex', 'logs', 'servertool-events.jsonl');
|
|
7
|
+
let cachedEnabled = null;
|
|
8
|
+
let cachedLogPath = null;
|
|
9
|
+
let writeQueue = Promise.resolve();
|
|
10
|
+
const ensuredDirs = new Set();
|
|
11
|
+
function isDevMode() {
|
|
12
|
+
const nodeEnv = String(process.env.NODE_ENV || '').trim().toLowerCase();
|
|
13
|
+
if (nodeEnv === 'development') {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
const buildMode = String(process.env.ROUTECODEX_BUILD_MODE ??
|
|
17
|
+
process.env.RCC_BUILD_MODE ??
|
|
18
|
+
process.env.BUILD_MODE ??
|
|
19
|
+
process.env.LLMSWITCH_BUILD_MODE ??
|
|
20
|
+
'')
|
|
21
|
+
.trim()
|
|
22
|
+
.toLowerCase();
|
|
23
|
+
return buildMode === 'dev' || buildMode === 'development';
|
|
24
|
+
}
|
|
25
|
+
function resolveEnabled() {
|
|
26
|
+
if (cachedEnabled !== null) {
|
|
27
|
+
return cachedEnabled;
|
|
28
|
+
}
|
|
29
|
+
const raw = String(process.env.ROUTECODEX_SERVERTOOL_FILE_LOG ??
|
|
30
|
+
process.env.RCC_SERVERTOOL_FILE_LOG ??
|
|
31
|
+
process.env.LLMSWITCH_SERVERTOOL_FILE_LOG ??
|
|
32
|
+
'')
|
|
33
|
+
.trim()
|
|
34
|
+
.toLowerCase();
|
|
35
|
+
if (truthy.has(raw)) {
|
|
36
|
+
cachedEnabled = true;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (falsy.has(raw)) {
|
|
40
|
+
cachedEnabled = false;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
cachedEnabled = isDevMode();
|
|
44
|
+
return cachedEnabled;
|
|
45
|
+
}
|
|
46
|
+
function resolveLogPath() {
|
|
47
|
+
if (cachedLogPath) {
|
|
48
|
+
return cachedLogPath;
|
|
49
|
+
}
|
|
50
|
+
const raw = String(process.env.ROUTECODEX_SERVERTOOL_FILE_LOG_PATH ??
|
|
51
|
+
process.env.RCC_SERVERTOOL_FILE_LOG_PATH ??
|
|
52
|
+
process.env.LLMSWITCH_SERVERTOOL_FILE_LOG_PATH ??
|
|
53
|
+
'').trim();
|
|
54
|
+
cachedLogPath = raw || DEFAULT_LOG_PATH;
|
|
55
|
+
return cachedLogPath;
|
|
56
|
+
}
|
|
57
|
+
async function ensureParentDir(logPath) {
|
|
58
|
+
const dir = path.dirname(logPath);
|
|
59
|
+
if (ensuredDirs.has(dir)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await fs.mkdir(dir, { recursive: true });
|
|
63
|
+
ensuredDirs.add(dir);
|
|
64
|
+
}
|
|
65
|
+
export function appendServerToolProgressFileEvent(event) {
|
|
66
|
+
if (!resolveEnabled()) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const logPath = resolveLogPath();
|
|
70
|
+
const line = `${JSON.stringify({ ...event, ts: new Date().toISOString() })}\n`;
|
|
71
|
+
writeQueue = writeQueue
|
|
72
|
+
.then(async () => {
|
|
73
|
+
await ensureParentDir(logPath);
|
|
74
|
+
await fs.appendFile(logPath, line, 'utf8');
|
|
75
|
+
})
|
|
76
|
+
.catch(() => {
|
|
77
|
+
// best-effort file logging
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export function resetServerToolProgressFileLoggerForTests() {
|
|
81
|
+
cachedEnabled = null;
|
|
82
|
+
cachedLogPath = null;
|
|
83
|
+
ensuredDirs.clear();
|
|
84
|
+
writeQueue = Promise.resolve();
|
|
85
|
+
}
|
|
86
|
+
export async function flushServerToolProgressFileLoggerForTests() {
|
|
87
|
+
await writeQueue;
|
|
88
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ServerToolAutoHookTraceEvent } from './types.js';
|
|
2
|
+
export interface PreCommandHookRunOptions {
|
|
3
|
+
requestId: string;
|
|
4
|
+
entryEndpoint: string;
|
|
5
|
+
providerProtocol: string;
|
|
6
|
+
toolName: string;
|
|
7
|
+
toolCallId: string;
|
|
8
|
+
toolArguments: string;
|
|
9
|
+
preCommandState?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface PreCommandHookRunResult {
|
|
12
|
+
toolArguments: string;
|
|
13
|
+
changed: boolean;
|
|
14
|
+
traces: ServerToolAutoHookTraceEvent[];
|
|
15
|
+
}
|
|
16
|
+
export declare function runPreCommandHooks(options: PreCommandHookRunOptions): PreCommandHookRunResult;
|
|
17
|
+
export declare function resetPreCommandHooksCacheForTests(): void;
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { isPreCommandScriptPathAllowed } from '../router/virtual-router/pre-command-file-resolver.js';
|
|
6
|
+
const DEFAULT_PRE_COMMAND_HOOKS_FILE = path.join(os.homedir(), '.routecodex', 'hooks', 'pre-command-hooks.json');
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 2000;
|
|
8
|
+
const DEFAULT_TOOLS = ['exec_command', 'shell', 'shell_command'];
|
|
9
|
+
let cachedConfig = null;
|
|
10
|
+
export function runPreCommandHooks(options) {
|
|
11
|
+
const runtimeRule = resolveRuntimePreCommandRule(options.preCommandState);
|
|
12
|
+
const config = runtimeRule
|
|
13
|
+
? { enabled: true, hooks: [runtimeRule] }
|
|
14
|
+
: loadPreCommandHooksConfig();
|
|
15
|
+
if (!config.enabled || config.hooks.length === 0) {
|
|
16
|
+
return {
|
|
17
|
+
toolArguments: options.toolArguments,
|
|
18
|
+
changed: false,
|
|
19
|
+
traces: []
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const traces = [];
|
|
23
|
+
let changed = false;
|
|
24
|
+
let currentArguments = options.toolArguments;
|
|
25
|
+
let currentParsedArgs = parseToolArgumentsObject(currentArguments);
|
|
26
|
+
let currentCommandText = extractCommandText(currentParsedArgs, currentArguments);
|
|
27
|
+
const normalizedTool = normalizeToolName(options.toolName);
|
|
28
|
+
for (const hook of config.hooks) {
|
|
29
|
+
const traceBase = {
|
|
30
|
+
hookId: hook.id,
|
|
31
|
+
phase: 'pre_command',
|
|
32
|
+
priority: hook.priority,
|
|
33
|
+
queue: 'A_optional',
|
|
34
|
+
queueIndex: 0,
|
|
35
|
+
queueTotal: config.hooks.length
|
|
36
|
+
};
|
|
37
|
+
if (!hook.toolNames.has(normalizedTool)) {
|
|
38
|
+
traces.push({ ...traceBase, result: 'miss', reason: 'tool_mismatch' });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (hook.cmdRegex && !hook.cmdRegex.test(currentCommandText)) {
|
|
42
|
+
traces.push({ ...traceBase, result: 'miss', reason: 'cmd_regex_mismatch' });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
let matched = false;
|
|
47
|
+
if (hook.runtimeScriptPath && hook.runtimeScriptPath.trim()) {
|
|
48
|
+
const runtimeOutput = runRuntimeScriptHook(hook.runtimeScriptPath, {
|
|
49
|
+
requestId: options.requestId,
|
|
50
|
+
entryEndpoint: options.entryEndpoint,
|
|
51
|
+
providerProtocol: options.providerProtocol,
|
|
52
|
+
toolName: normalizedTool,
|
|
53
|
+
toolCallId: options.toolCallId,
|
|
54
|
+
arguments: currentParsedArgs ?? { args_raw: currentArguments },
|
|
55
|
+
command: currentCommandText,
|
|
56
|
+
hookId: hook.id
|
|
57
|
+
}, hook.timeoutMs);
|
|
58
|
+
if (typeof runtimeOutput === 'string' && runtimeOutput !== currentArguments) {
|
|
59
|
+
changed = true;
|
|
60
|
+
currentArguments = runtimeOutput;
|
|
61
|
+
currentParsedArgs = parseToolArgumentsObject(currentArguments);
|
|
62
|
+
currentCommandText = extractCommandText(currentParsedArgs, currentArguments);
|
|
63
|
+
}
|
|
64
|
+
matched = true;
|
|
65
|
+
}
|
|
66
|
+
if (hook.jqExpression && hook.jqExpression.trim()) {
|
|
67
|
+
const jqInput = currentParsedArgs ?? { args_raw: currentArguments };
|
|
68
|
+
const transformed = runJqTransform(hook.jqExpression, jqInput, hook.timeoutMs);
|
|
69
|
+
currentParsedArgs = transformed;
|
|
70
|
+
const nextArgs = JSON.stringify(transformed);
|
|
71
|
+
if (nextArgs !== currentArguments) {
|
|
72
|
+
changed = true;
|
|
73
|
+
currentArguments = nextArgs;
|
|
74
|
+
currentCommandText = extractCommandText(currentParsedArgs, currentArguments);
|
|
75
|
+
}
|
|
76
|
+
matched = true;
|
|
77
|
+
}
|
|
78
|
+
if (hook.shellCommand && hook.shellCommand.trim()) {
|
|
79
|
+
runShellCommandHook(hook.shellCommand, {
|
|
80
|
+
requestId: options.requestId,
|
|
81
|
+
entryEndpoint: options.entryEndpoint,
|
|
82
|
+
providerProtocol: options.providerProtocol,
|
|
83
|
+
toolName: normalizedTool,
|
|
84
|
+
toolCallId: options.toolCallId,
|
|
85
|
+
arguments: currentParsedArgs ?? { args_raw: currentArguments },
|
|
86
|
+
command: currentCommandText,
|
|
87
|
+
hookId: hook.id
|
|
88
|
+
}, hook.timeoutMs);
|
|
89
|
+
matched = true;
|
|
90
|
+
}
|
|
91
|
+
traces.push({
|
|
92
|
+
...traceBase,
|
|
93
|
+
result: 'match',
|
|
94
|
+
reason: matched ? (changed ? 'applied' : 'matched') : 'no_action'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error ?? 'unknown_error');
|
|
99
|
+
traces.push({
|
|
100
|
+
...traceBase,
|
|
101
|
+
result: 'error',
|
|
102
|
+
reason: message
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
toolArguments: currentArguments,
|
|
108
|
+
changed,
|
|
109
|
+
traces
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function resetPreCommandHooksCacheForTests() {
|
|
113
|
+
cachedConfig = null;
|
|
114
|
+
}
|
|
115
|
+
function resolveRuntimePreCommandRule(rawState) {
|
|
116
|
+
if (!rawState || typeof rawState !== 'object' || Array.isArray(rawState)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const record = rawState;
|
|
120
|
+
const scriptPath = readString(record.preCommandScriptPath ?? record.scriptPath);
|
|
121
|
+
if (!scriptPath) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (!isPreCommandScriptPathAllowed(scriptPath)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const timeoutMs = normalizeTimeoutMs(record.timeoutMs ?? record.timeout_ms ?? process.env.ROUTECODEX_PRE_COMMAND_TIMEOUT_MS);
|
|
128
|
+
return {
|
|
129
|
+
id: `runtime_precommand:${sanitizeHookId(path.basename(scriptPath) || 'script')}`,
|
|
130
|
+
toolNames: new Set(DEFAULT_TOOLS),
|
|
131
|
+
runtimeScriptPath: scriptPath,
|
|
132
|
+
timeoutMs,
|
|
133
|
+
priority: -1000,
|
|
134
|
+
order: -1
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function resolvePreCommandHooksFilePath() {
|
|
138
|
+
const configured = process.env.ROUTECODEX_PRE_COMMAND_HOOKS_FILE ||
|
|
139
|
+
process.env.RCC_PRE_COMMAND_HOOKS_FILE ||
|
|
140
|
+
process.env.LLMSWITCH_PRE_COMMAND_HOOKS_FILE;
|
|
141
|
+
if (typeof configured === 'string' && configured.trim()) {
|
|
142
|
+
return path.resolve(configured.trim());
|
|
143
|
+
}
|
|
144
|
+
return DEFAULT_PRE_COMMAND_HOOKS_FILE;
|
|
145
|
+
}
|
|
146
|
+
function loadPreCommandHooksConfig() {
|
|
147
|
+
const filePath = resolvePreCommandHooksFilePath();
|
|
148
|
+
let stat;
|
|
149
|
+
try {
|
|
150
|
+
stat = fs.statSync(filePath);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
cachedConfig = {
|
|
154
|
+
filePath,
|
|
155
|
+
mtimeMs: 0,
|
|
156
|
+
size: 0,
|
|
157
|
+
config: { enabled: false, hooks: [] }
|
|
158
|
+
};
|
|
159
|
+
return cachedConfig.config;
|
|
160
|
+
}
|
|
161
|
+
if (cachedConfig &&
|
|
162
|
+
cachedConfig.filePath === filePath &&
|
|
163
|
+
cachedConfig.mtimeMs === stat.mtimeMs &&
|
|
164
|
+
cachedConfig.size === stat.size) {
|
|
165
|
+
return cachedConfig.config;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
169
|
+
const parsed = JSON.parse(content);
|
|
170
|
+
const config = normalizePreCommandHooksConfig(parsed);
|
|
171
|
+
cachedConfig = {
|
|
172
|
+
filePath,
|
|
173
|
+
mtimeMs: stat.mtimeMs,
|
|
174
|
+
size: stat.size,
|
|
175
|
+
config
|
|
176
|
+
};
|
|
177
|
+
return config;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
cachedConfig = {
|
|
181
|
+
filePath,
|
|
182
|
+
mtimeMs: stat.mtimeMs,
|
|
183
|
+
size: stat.size,
|
|
184
|
+
config: { enabled: false, hooks: [] }
|
|
185
|
+
};
|
|
186
|
+
return cachedConfig.config;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function normalizePreCommandHooksConfig(raw) {
|
|
190
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
191
|
+
return { enabled: false, hooks: [] };
|
|
192
|
+
}
|
|
193
|
+
const record = raw;
|
|
194
|
+
const enabled = record.enabled !== false;
|
|
195
|
+
if (!enabled) {
|
|
196
|
+
return { enabled: false, hooks: [] };
|
|
197
|
+
}
|
|
198
|
+
const hooksRaw = Array.isArray(record.hooks) ? record.hooks : [];
|
|
199
|
+
const hooks = [];
|
|
200
|
+
for (let idx = 0; idx < hooksRaw.length; idx += 1) {
|
|
201
|
+
const normalized = normalizePreCommandHookRule(hooksRaw[idx], idx);
|
|
202
|
+
if (normalized) {
|
|
203
|
+
hooks.push(normalized);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
hooks.sort((left, right) => {
|
|
207
|
+
if (left.priority !== right.priority) {
|
|
208
|
+
return left.priority - right.priority;
|
|
209
|
+
}
|
|
210
|
+
if (left.order !== right.order) {
|
|
211
|
+
return left.order - right.order;
|
|
212
|
+
}
|
|
213
|
+
return left.id.localeCompare(right.id);
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
enabled: true,
|
|
217
|
+
hooks
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function normalizePreCommandHookRule(raw, order) {
|
|
221
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const record = raw;
|
|
225
|
+
if (record.enabled === false) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const id = normalizeHookId(record.id, order);
|
|
229
|
+
const toolNames = normalizeToolSet(record.tool ?? record.tools);
|
|
230
|
+
const cmdRegex = parseRegex(record.cmdRegex ?? record.commandRegex ?? record.matchCommand);
|
|
231
|
+
const jqExpression = readString(record.jq ?? record.jqTransform ?? record.expression);
|
|
232
|
+
const shellCommand = readString(record.shell ?? record.command);
|
|
233
|
+
const hasAction = Boolean(jqExpression || shellCommand);
|
|
234
|
+
if (!hasAction) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const timeoutMs = normalizeTimeoutMs(record.timeoutMs ?? record.timeout_ms);
|
|
238
|
+
const priority = normalizePriority(record.priority);
|
|
239
|
+
return {
|
|
240
|
+
id,
|
|
241
|
+
toolNames,
|
|
242
|
+
cmdRegex,
|
|
243
|
+
jqExpression,
|
|
244
|
+
shellCommand,
|
|
245
|
+
timeoutMs,
|
|
246
|
+
priority,
|
|
247
|
+
order
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function sanitizeHookId(value) {
|
|
251
|
+
return value.replace(/[^a-zA-Z0-9_.-]+/g, '_');
|
|
252
|
+
}
|
|
253
|
+
function normalizeHookId(value, order) {
|
|
254
|
+
const text = readString(value);
|
|
255
|
+
if (!text) {
|
|
256
|
+
return `pre_command_hook_${order + 1}`;
|
|
257
|
+
}
|
|
258
|
+
return sanitizeHookId(text);
|
|
259
|
+
}
|
|
260
|
+
function normalizeToolName(value) {
|
|
261
|
+
return (value || '').trim().toLowerCase();
|
|
262
|
+
}
|
|
263
|
+
function normalizeToolSet(raw) {
|
|
264
|
+
const out = new Set();
|
|
265
|
+
const push = (value) => {
|
|
266
|
+
if (typeof value !== 'string') {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const normalized = normalizeToolName(value);
|
|
270
|
+
if (normalized) {
|
|
271
|
+
out.add(normalized);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
if (Array.isArray(raw)) {
|
|
275
|
+
for (const item of raw) {
|
|
276
|
+
push(item);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
push(raw);
|
|
281
|
+
}
|
|
282
|
+
if (out.size === 0) {
|
|
283
|
+
for (const tool of DEFAULT_TOOLS) {
|
|
284
|
+
out.add(tool);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
function parseRegex(raw) {
|
|
290
|
+
if (typeof raw !== 'string' || !raw.trim()) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
const value = raw.trim();
|
|
294
|
+
const slashMatch = value.match(/^\/(.*)\/([a-z]*)$/i);
|
|
295
|
+
if (slashMatch) {
|
|
296
|
+
const pattern = slashMatch[1];
|
|
297
|
+
const flags = slashMatch[2] || 'i';
|
|
298
|
+
try {
|
|
299
|
+
return new RegExp(pattern, flags);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
return new RegExp(value, 'i');
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function normalizeTimeoutMs(raw) {
|
|
313
|
+
const num = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number.parseInt(raw, 10) : NaN;
|
|
314
|
+
if (!Number.isFinite(num) || num <= 0) {
|
|
315
|
+
return DEFAULT_TIMEOUT_MS;
|
|
316
|
+
}
|
|
317
|
+
return Math.floor(Math.min(num, 30_000));
|
|
318
|
+
}
|
|
319
|
+
function normalizePriority(raw) {
|
|
320
|
+
const num = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number.parseInt(raw, 10) : NaN;
|
|
321
|
+
if (!Number.isFinite(num)) {
|
|
322
|
+
return 100;
|
|
323
|
+
}
|
|
324
|
+
return Math.floor(num);
|
|
325
|
+
}
|
|
326
|
+
function readString(raw) {
|
|
327
|
+
return typeof raw === 'string' ? raw.trim() : '';
|
|
328
|
+
}
|
|
329
|
+
function parseToolArgumentsObject(raw) {
|
|
330
|
+
if (typeof raw !== 'string' || !raw.trim()) {
|
|
331
|
+
return {};
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const parsed = JSON.parse(raw);
|
|
335
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return parsed;
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function extractCommandText(args, rawArgs) {
|
|
345
|
+
if (args && typeof args.cmd === 'string' && args.cmd.trim()) {
|
|
346
|
+
return args.cmd.trim();
|
|
347
|
+
}
|
|
348
|
+
if (args && typeof args.command === 'string' && args.command.trim()) {
|
|
349
|
+
return args.command.trim();
|
|
350
|
+
}
|
|
351
|
+
if (typeof rawArgs === 'string' && rawArgs.trim()) {
|
|
352
|
+
return rawArgs.trim();
|
|
353
|
+
}
|
|
354
|
+
return '';
|
|
355
|
+
}
|
|
356
|
+
function runJqTransform(expression, input, timeoutMs) {
|
|
357
|
+
const result = spawnSync('jq', ['-c', expression], {
|
|
358
|
+
input: JSON.stringify(input),
|
|
359
|
+
encoding: 'utf8',
|
|
360
|
+
timeout: timeoutMs,
|
|
361
|
+
maxBuffer: 1024 * 1024
|
|
362
|
+
});
|
|
363
|
+
if (result.error) {
|
|
364
|
+
const code = result.error.code;
|
|
365
|
+
if (code === 'ENOENT') {
|
|
366
|
+
throw new Error('jq_not_found');
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`jq_error:${result.error.message}`);
|
|
369
|
+
}
|
|
370
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
371
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
|
|
372
|
+
throw new Error(`jq_failed:${stderr || result.status}`);
|
|
373
|
+
}
|
|
374
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout.trim() : '';
|
|
375
|
+
if (!stdout) {
|
|
376
|
+
throw new Error('jq_empty_output');
|
|
377
|
+
}
|
|
378
|
+
const lines = stdout
|
|
379
|
+
.split(/\r?\n/)
|
|
380
|
+
.map((line) => line.trim())
|
|
381
|
+
.filter((line) => line.length > 0);
|
|
382
|
+
const payload = lines.length > 0 ? lines[lines.length - 1] : stdout;
|
|
383
|
+
let parsed;
|
|
384
|
+
try {
|
|
385
|
+
parsed = JSON.parse(payload);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
throw new Error('jq_invalid_json_output');
|
|
389
|
+
}
|
|
390
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
391
|
+
throw new Error('jq_non_object_output');
|
|
392
|
+
}
|
|
393
|
+
return parsed;
|
|
394
|
+
}
|
|
395
|
+
function runShellCommandHook(command, eventPayload, timeoutMs) {
|
|
396
|
+
const result = spawnSync(command, {
|
|
397
|
+
shell: true,
|
|
398
|
+
timeout: timeoutMs,
|
|
399
|
+
encoding: 'utf8',
|
|
400
|
+
input: JSON.stringify(eventPayload),
|
|
401
|
+
env: {
|
|
402
|
+
...process.env,
|
|
403
|
+
ROUTECODEX_PRE_COMMAND_HOOK_EVENT: JSON.stringify(eventPayload)
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
if (result.error) {
|
|
407
|
+
throw new Error(`shell_hook_error:${result.error.message}`);
|
|
408
|
+
}
|
|
409
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
410
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
|
|
411
|
+
throw new Error(`shell_hook_failed:${stderr || result.status}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function runRuntimeScriptHook(scriptPath, eventPayload, timeoutMs) {
|
|
415
|
+
const payloadText = JSON.stringify(eventPayload);
|
|
416
|
+
let result = spawnSync(scriptPath, [], {
|
|
417
|
+
timeout: timeoutMs,
|
|
418
|
+
encoding: 'utf8',
|
|
419
|
+
input: payloadText,
|
|
420
|
+
env: {
|
|
421
|
+
...process.env,
|
|
422
|
+
ROUTECODEX_PRE_COMMAND_HOOK_EVENT: payloadText
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
if (result.error) {
|
|
426
|
+
const code = result.error.code;
|
|
427
|
+
if ((code === 'EACCES' || code === 'ENOEXEC') && process.platform !== 'win32') {
|
|
428
|
+
result = spawnSync('/bin/bash', [scriptPath], {
|
|
429
|
+
timeout: timeoutMs,
|
|
430
|
+
encoding: 'utf8',
|
|
431
|
+
input: payloadText,
|
|
432
|
+
env: {
|
|
433
|
+
...process.env,
|
|
434
|
+
ROUTECODEX_PRE_COMMAND_HOOK_EVENT: payloadText
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (result.error) {
|
|
440
|
+
throw new Error(`runtime_precommand_error:${result.error.message}`);
|
|
441
|
+
}
|
|
442
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
443
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
|
|
444
|
+
throw new Error(`runtime_precommand_failed:${stderr || result.status}`);
|
|
445
|
+
}
|
|
446
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout.trim() : '';
|
|
447
|
+
if (!stdout) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
const lines = stdout
|
|
451
|
+
.split(/\r?\n/)
|
|
452
|
+
.map((line) => line.trim())
|
|
453
|
+
.filter((line) => line.length > 0);
|
|
454
|
+
const payload = lines.length > 0 ? lines[lines.length - 1] : stdout;
|
|
455
|
+
let parsed;
|
|
456
|
+
try {
|
|
457
|
+
parsed = JSON.parse(payload);
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
const fallback = payload.replace(/\\"/g, '"').trim();
|
|
461
|
+
const unwrapped = fallback.startsWith('"') && fallback.endsWith('"') && fallback.length > 1
|
|
462
|
+
? fallback.slice(1, -1)
|
|
463
|
+
: fallback;
|
|
464
|
+
try {
|
|
465
|
+
parsed = JSON.parse(unwrapped);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
throw new Error('runtime_precommand_invalid_json:' + payload.slice(0, 200));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (typeof parsed === 'string' && parsed.trim()) {
|
|
472
|
+
return parsed;
|
|
473
|
+
}
|
|
474
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
const record = parsed;
|
|
478
|
+
if (typeof record.toolArguments === 'string' && record.toolArguments.trim()) {
|
|
479
|
+
return record.toolArguments;
|
|
480
|
+
}
|
|
481
|
+
if (Object.prototype.hasOwnProperty.call(record, 'arguments')) {
|
|
482
|
+
const argValue = record.arguments;
|
|
483
|
+
if (typeof argValue === 'string') {
|
|
484
|
+
return argValue;
|
|
485
|
+
}
|
|
486
|
+
if (argValue && typeof argValue === 'object' && !Array.isArray(argValue)) {
|
|
487
|
+
return JSON.stringify(argValue);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return JSON.stringify(record);
|
|
491
|
+
}
|
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
import type { ServerToolHandler } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* ServerToolRegistry:全局注册表,按 tool name 管理 ServerTool handler。
|
|
4
|
-
* - key 统一使用小写名称(如 "web_search");
|
|
5
|
-
* - 不关注具体协议(Chat / Responses),只负责分发到 handler。
|
|
6
|
-
*/
|
|
7
2
|
type TriggerMode = 'tool_call' | 'auto';
|
|
8
|
-
|
|
3
|
+
type AutoHookPhase = 'pre' | 'default' | 'post';
|
|
4
|
+
interface ServerToolAutoHookSpec {
|
|
5
|
+
id: string;
|
|
6
|
+
phase: AutoHookPhase;
|
|
7
|
+
priority: number;
|
|
8
|
+
order: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ServerToolHandlerEntry {
|
|
9
11
|
name: string;
|
|
10
12
|
trigger: TriggerMode;
|
|
11
13
|
handler: ServerToolHandler;
|
|
14
|
+
autoHook?: ServerToolAutoHookSpec;
|
|
15
|
+
}
|
|
16
|
+
export interface ServerToolAutoHookDescriptor {
|
|
17
|
+
id: string;
|
|
18
|
+
phase: AutoHookPhase;
|
|
19
|
+
priority: number;
|
|
20
|
+
order: number;
|
|
21
|
+
handler: ServerToolHandler;
|
|
12
22
|
}
|
|
13
23
|
export declare function registerServerToolHandler(name: string, handler: ServerToolHandler, options?: {
|
|
14
24
|
trigger?: TriggerMode;
|
|
25
|
+
priority?: number;
|
|
26
|
+
phase?: AutoHookPhase | string;
|
|
27
|
+
hook?: {
|
|
28
|
+
priority?: number;
|
|
29
|
+
phase?: AutoHookPhase | string;
|
|
30
|
+
};
|
|
15
31
|
}): void;
|
|
16
32
|
export declare function getServerToolHandler(name: string): ServerToolHandlerEntry | undefined;
|
|
17
33
|
export declare function listAutoServerToolHandlers(): ServerToolHandlerEntry[];
|
|
34
|
+
export declare function listAutoServerToolHooks(): ServerToolAutoHookDescriptor[];
|
|
18
35
|
export {};
|