@nexrall/code-core 1.4.70 → 1.4.72
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/agent/agentRegistry.d.ts.map +1 -1
- package/dist/agent/agentRegistry.js +28 -10
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +159 -48
- package/dist/agent/askOnce.d.ts.map +1 -1
- package/dist/agent/askOnce.js +14 -5
- package/dist/agent/backgroundAgents.d.ts.map +1 -1
- package/dist/agent/backgroundAgents.js +22 -8
- package/dist/agent/claimEvidence.d.ts.map +1 -1
- package/dist/agent/claimEvidence.js +2 -1
- package/dist/agent/compaction.d.ts.map +1 -1
- package/dist/agent/compaction.js +31 -10
- package/dist/agent/crossProcessLock.d.ts.map +1 -1
- package/dist/agent/crossProcessLock.js +15 -7
- package/dist/agent/editCompleteness.d.ts.map +1 -1
- package/dist/agent/fileLocks.d.ts.map +1 -1
- package/dist/agent/fileLocks.js +14 -5
- package/dist/agent/flaky.d.ts.map +1 -1
- package/dist/agent/hooks.d.ts.map +1 -1
- package/dist/agent/hooks.js +284 -74
- package/dist/agent/iterationPolicy.d.ts.map +1 -1
- package/dist/agent/iterationPolicy.js +28 -25
- package/dist/agent/lifecycleHost.d.ts.map +1 -1
- package/dist/agent/lifecycleHost.js +36 -12
- package/dist/agent/loop.d.ts +22 -4
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +223 -51
- package/dist/agent/memory.d.ts.map +1 -1
- package/dist/agent/memory.js +13 -7
- package/dist/agent/modelCatalogue.d.ts.map +1 -1
- package/dist/agent/modelCatalogue.js +4 -1
- package/dist/agent/nestedInstructions.d.ts +122 -0
- package/dist/agent/nestedInstructions.d.ts.map +1 -0
- package/dist/agent/nestedInstructions.js +696 -0
- package/dist/agent/peerRegistry.d.ts.map +1 -1
- package/dist/agent/peerRegistry.js +44 -6
- package/dist/agent/peerTransport.d.ts.map +1 -1
- package/dist/agent/peerTransport.js +18 -6
- package/dist/agent/planMode.d.ts.map +1 -1
- package/dist/agent/planMode.js +194 -41
- package/dist/agent/readDedupe.d.ts.map +1 -1
- package/dist/agent/readDedupe.js +3 -1
- package/dist/agent/securityLint.d.ts.map +1 -1
- package/dist/agent/sharedTasks.d.ts.map +1 -1
- package/dist/agent/sharedTasks.js +20 -5
- package/dist/agent/skills.d.ts +79 -0
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +403 -14
- package/dist/agent/subAgentBudget.d.ts.map +1 -1
- package/dist/agent/subTask.d.ts.map +1 -1
- package/dist/agent/subTask.js +87 -37
- package/dist/agent/subTaskSupport.d.ts.map +1 -1
- package/dist/agent/subTaskSupport.js +43 -14
- package/dist/agent/testIntegrity.d.ts.map +1 -1
- package/dist/agent/testIntegrity.js +5 -3
- package/dist/agent/toolDescriptions.d.ts.map +1 -1
- package/dist/agent/toolPrefetch.d.ts.map +1 -1
- package/dist/agent/toolPrefetch.js +15 -11
- package/dist/agent/trust.d.ts.map +1 -1
- package/dist/agent/trust.js +6 -2
- package/dist/agent/worktree.d.ts.map +1 -1
- package/dist/agent/worktree.js +70 -16
- package/dist/agent/worktreeEnforcement.d.ts.map +1 -1
- package/dist/agent/worktreeEnforcement.js +9 -4
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +67 -32
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -1
- package/dist/checkpoint/manager.d.ts.map +1 -1
- package/dist/checkpoint/manager.js +23 -8
- package/dist/commands/loader.d.ts.map +1 -1
- package/dist/commands/loader.js +6 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/mcp/client.d.ts +34 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +89 -24
- package/dist/mcp/httpClient.d.ts +24 -1
- package/dist/mcp/httpClient.d.ts.map +1 -1
- package/dist/mcp/httpClient.js +80 -14
- package/dist/mcp/instructions.d.ts +11 -0
- package/dist/mcp/instructions.d.ts.map +1 -0
- package/dist/mcp/instructions.js +42 -0
- package/dist/mcp/manager.d.ts +24 -0
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +117 -24
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +48 -14
- package/dist/mcp/sseClient.d.ts +9 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +43 -10
- package/dist/mcp/stats.d.ts.map +1 -1
- package/dist/permissions/bashClassify.d.ts.map +1 -1
- package/dist/permissions/bashClassify.js +25 -3
- package/dist/permissions/destructive.d.ts.map +1 -1
- package/dist/permissions/destructive.js +7 -2
- package/dist/permissions/destructiveTokens.d.ts.map +1 -1
- package/dist/permissions/destructiveTokens.js +104 -24
- package/dist/permissions/modePolicy.d.ts.map +1 -1
- package/dist/permissions/modePolicy.js +40 -17
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +7 -5
- package/dist/plugins/data.d.ts.map +1 -1
- package/dist/plugins/data.js +6 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +18 -7
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +17 -10
- package/dist/plugins/sources.d.ts.map +1 -1
- package/dist/plugins/sources.js +9 -3
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +470 -173
- package/dist/tools/sandbox.d.ts.map +1 -1
- package/dist/tools/sandbox.js +5 -1
- package/dist/tools/symbols.d.ts.map +1 -1
- package/dist/tools/symbols.js +30 -8
- package/dist/tools/tsLangService.d.ts.map +1 -1
- package/dist/tools/tsLangService.js +12 -4
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/util/frontmatter.d.ts.map +1 -1
- package/dist/util/miniYaml.d.ts.map +1 -1
- package/dist/util/miniYaml.js +4 -2
- package/package.json +1 -1
package/dist/agent/hooks.js
CHANGED
|
@@ -85,7 +85,9 @@ function loadHooks(workDir) {
|
|
|
85
85
|
if (fs.existsSync(p))
|
|
86
86
|
fromSettings = JSON.parse(fs.readFileSync(p, 'utf-8')).hooks ?? {};
|
|
87
87
|
}
|
|
88
|
-
catch {
|
|
88
|
+
catch {
|
|
89
|
+
/* ignore */
|
|
90
|
+
}
|
|
89
91
|
// `--settings`: hooks from the run's own settings object run after the project's.
|
|
90
92
|
const sess = (0, rules_1.getSessionSettings)()?.hooks;
|
|
91
93
|
if (sess && typeof sess === 'object') {
|
|
@@ -103,10 +105,7 @@ function loadHooks(workDir) {
|
|
|
103
105
|
const extra = fromPlugins[phase];
|
|
104
106
|
if (!Array.isArray(extra) || !extra.length)
|
|
105
107
|
continue;
|
|
106
|
-
merged[phase] = [
|
|
107
|
-
...((merged[phase]) ?? []),
|
|
108
|
-
...extra,
|
|
109
|
-
];
|
|
108
|
+
merged[phase] = [...(merged[phase] ?? []), ...extra];
|
|
110
109
|
}
|
|
111
110
|
// One normalising pass over every entry-list phase, whatever source it came from.
|
|
112
111
|
for (const phase of ENTRY_LIST_PHASES) {
|
|
@@ -119,13 +118,34 @@ function loadHooks(workDir) {
|
|
|
119
118
|
}
|
|
120
119
|
/** The phases whose value is an array of ENTRIES (each carrying a matcher + handlers). */
|
|
121
120
|
const ENTRY_LIST_PHASES = [
|
|
122
|
-
'PreToolUse',
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'
|
|
126
|
-
'
|
|
127
|
-
'
|
|
128
|
-
'
|
|
121
|
+
'PreToolUse',
|
|
122
|
+
'PostToolUse',
|
|
123
|
+
'SessionStart',
|
|
124
|
+
'UserPromptSubmit',
|
|
125
|
+
'PreCompact',
|
|
126
|
+
'PermissionRequest',
|
|
127
|
+
'Notification',
|
|
128
|
+
'PostToolUseFailure',
|
|
129
|
+
'SubagentStart',
|
|
130
|
+
'PostCompact',
|
|
131
|
+
'SessionEnd',
|
|
132
|
+
'FileChanged',
|
|
133
|
+
'InstructionsLoaded',
|
|
134
|
+
'StopFailure',
|
|
135
|
+
'PermissionDenied',
|
|
136
|
+
'PreModelSwitch',
|
|
137
|
+
'PostModelSwitch',
|
|
138
|
+
'CwdChanged',
|
|
139
|
+
'DirectoryAdded',
|
|
140
|
+
'ConfigChange',
|
|
141
|
+
'WorktreeCreate',
|
|
142
|
+
'WorktreeRemove',
|
|
143
|
+
'Setup',
|
|
144
|
+
'PostToolBatch',
|
|
145
|
+
'TaskCreated',
|
|
146
|
+
'TaskCompleted',
|
|
147
|
+
'Elicitation',
|
|
148
|
+
'ElicitationResult',
|
|
129
149
|
];
|
|
130
150
|
/**
|
|
131
151
|
* Accept BOTH config shapes for the same list, so a hook can never be silently dropped:
|
|
@@ -159,11 +179,17 @@ const _hookDeliveries = [];
|
|
|
159
179
|
let _hookWakeListener = null;
|
|
160
180
|
let _hookStatusListener = null;
|
|
161
181
|
/** Called (if set) the moment a wake lands, so an idle host can react at once. */
|
|
162
|
-
function setHookWakeListener(fn) {
|
|
182
|
+
function setHookWakeListener(fn) {
|
|
183
|
+
_hookWakeListener = fn;
|
|
184
|
+
}
|
|
163
185
|
/** Where `statusMessage` goes. The message is null when no hook is running. */
|
|
164
|
-
function setHookStatusListener(fn) {
|
|
186
|
+
function setHookStatusListener(fn) {
|
|
187
|
+
_hookStatusListener = fn;
|
|
188
|
+
}
|
|
165
189
|
/** Everything queued so far — the loop calls this before each request. */
|
|
166
|
-
function drainHookDeliveries() {
|
|
190
|
+
function drainHookDeliveries() {
|
|
191
|
+
return _hookDeliveries.splice(0);
|
|
192
|
+
}
|
|
167
193
|
/** Only the wakes — an idle host must not start a turn just for async context. */
|
|
168
194
|
function takeHookWakes() {
|
|
169
195
|
const out = _hookDeliveries.filter((d) => d.kind === 'wake');
|
|
@@ -172,7 +198,9 @@ function takeHookWakes() {
|
|
|
172
198
|
_hookDeliveries.splice(i, 1);
|
|
173
199
|
return out;
|
|
174
200
|
}
|
|
175
|
-
function hasPendingHookWake() {
|
|
201
|
+
function hasPendingHookWake() {
|
|
202
|
+
return _hookDeliveries.some((d) => d.kind === 'wake');
|
|
203
|
+
}
|
|
176
204
|
/** The text a delivery batch becomes in the conversation — the same envelope sync hooks use. */
|
|
177
205
|
function formatHookDeliveries(list) {
|
|
178
206
|
return list.map((d) => `<hook-context source="${d.source}">\n${d.text}\n</hook-context>`).join('\n\n');
|
|
@@ -189,7 +217,9 @@ function hookIdentity(event, hook, matcher, dir) {
|
|
|
189
217
|
return `${dir ?? ''}|${event}|${matcher ?? ''}|${hook.plugin ?? ''}|${body}`;
|
|
190
218
|
}
|
|
191
219
|
/** Forget which `once` hooks have run (a new session: /clear, a fresh run). */
|
|
192
|
-
function resetHookOnceState() {
|
|
220
|
+
function resetHookOnceState() {
|
|
221
|
+
_hookOnceDone.clear();
|
|
222
|
+
}
|
|
193
223
|
// While any hook with a statusMessage is running the UI shows the NEWEST of those
|
|
194
224
|
// messages — null once they all finish. Ref-counted because parallel sub-agents run
|
|
195
225
|
// hooks concurrently, and "the first hook to finish" must not clear the label of one
|
|
@@ -213,22 +243,30 @@ function hookStatusEnd(id) {
|
|
|
213
243
|
}
|
|
214
244
|
let _hookMcpCaller = null;
|
|
215
245
|
/** Hosts with connected MCP servers register their caller once; null = mcp_tool hooks report "no MCP client". */
|
|
216
|
-
function setHookMcpCaller(fn) {
|
|
246
|
+
function setHookMcpCaller(fn) {
|
|
247
|
+
_hookMcpCaller = fn;
|
|
248
|
+
}
|
|
217
249
|
let _hookEventListener = null;
|
|
218
250
|
/**
|
|
219
251
|
* Install (or clear) the sink that receives every hook run's lifecycle records.
|
|
220
252
|
* One per process: hooks fire from the loop, the lifecycle host and the permission
|
|
221
253
|
* gate, but they all belong to the same session's stream.
|
|
222
254
|
*/
|
|
223
|
-
function setHookEventListener(fn) {
|
|
255
|
+
function setHookEventListener(fn) {
|
|
256
|
+
_hookEventListener = fn;
|
|
257
|
+
}
|
|
224
258
|
/** True when a sink is installed, for hosts skipping work that would only feed the stream. */
|
|
225
|
-
function hasHookEventListener() {
|
|
259
|
+
function hasHookEventListener() {
|
|
260
|
+
return _hookEventListener !== null;
|
|
261
|
+
}
|
|
226
262
|
let _hookIdSeq = 0;
|
|
227
263
|
function emitHookLifecycle(evt) {
|
|
228
264
|
try {
|
|
229
265
|
_hookEventListener?.(evt);
|
|
230
266
|
}
|
|
231
|
-
catch {
|
|
267
|
+
catch {
|
|
268
|
+
/* the host's listener must not break the hook */
|
|
269
|
+
}
|
|
232
270
|
}
|
|
233
271
|
/** The stream's hook_name: what the handler IS, in the config author's terms. */
|
|
234
272
|
function hookDisplayName(hook) {
|
|
@@ -276,7 +314,15 @@ async function runWithHookEvents(hook, event, run) {
|
|
|
276
314
|
pendingOut = '';
|
|
277
315
|
pendingErr = '';
|
|
278
316
|
lastFlush = Date.now();
|
|
279
|
-
emitHookLifecycle({
|
|
317
|
+
emitHookLifecycle({
|
|
318
|
+
kind: 'progress',
|
|
319
|
+
hookId,
|
|
320
|
+
hookName,
|
|
321
|
+
hookEvent: event,
|
|
322
|
+
stdout,
|
|
323
|
+
stderr,
|
|
324
|
+
output: stdout + stderr,
|
|
325
|
+
});
|
|
280
326
|
};
|
|
281
327
|
const onData = (d) => {
|
|
282
328
|
if (Date.now() - startedAt < HOOK_PROGRESS_AFTER_MS)
|
|
@@ -301,7 +347,16 @@ async function runWithHookEvents(hook, event, run) {
|
|
|
301
347
|
if (flushTimer)
|
|
302
348
|
clearTimeout(flushTimer);
|
|
303
349
|
const message = err?.message ?? String(err);
|
|
304
|
-
emitHookLifecycle({
|
|
350
|
+
emitHookLifecycle({
|
|
351
|
+
kind: 'response',
|
|
352
|
+
hookId,
|
|
353
|
+
hookName,
|
|
354
|
+
hookEvent: event,
|
|
355
|
+
stdout: '',
|
|
356
|
+
stderr: message,
|
|
357
|
+
output: message,
|
|
358
|
+
outcome: 'error',
|
|
359
|
+
});
|
|
305
360
|
throw err;
|
|
306
361
|
}
|
|
307
362
|
if (flushTimer)
|
|
@@ -349,11 +404,17 @@ function startBackgroundHook(hook, payload, opts) {
|
|
|
349
404
|
const out = (r.stdout ?? '').toString().trim();
|
|
350
405
|
if (hook.asyncRewake && r.status === 2) {
|
|
351
406
|
const text = (r.stderr ?? '').toString().trim() || out;
|
|
352
|
-
_hookDeliveries.push({
|
|
407
|
+
_hookDeliveries.push({
|
|
408
|
+
kind: 'wake',
|
|
409
|
+
text: text || 'Hook asked the session to wake (exit 2).',
|
|
410
|
+
source: opts.eventLabel,
|
|
411
|
+
});
|
|
353
412
|
try {
|
|
354
413
|
_hookWakeListener?.();
|
|
355
414
|
}
|
|
356
|
-
catch {
|
|
415
|
+
catch {
|
|
416
|
+
/* the host's listener must not break the hook */
|
|
417
|
+
}
|
|
357
418
|
return;
|
|
358
419
|
}
|
|
359
420
|
const context = directiveContext(out);
|
|
@@ -362,8 +423,12 @@ function startBackgroundHook(hook, payload, opts) {
|
|
|
362
423
|
};
|
|
363
424
|
// Through runHookHandler, not spawnHook directly: background runs get the same
|
|
364
425
|
// start/finish lifecycle records, and command dispatch stays in one place.
|
|
365
|
-
void runHookHandler(hook, payload, {
|
|
366
|
-
|
|
426
|
+
void runHookHandler(hook, payload, {
|
|
427
|
+
cwd: opts.cwd,
|
|
428
|
+
timeoutMs: opts.timeoutMs,
|
|
429
|
+
env: opts.env,
|
|
430
|
+
event: opts.event,
|
|
431
|
+
}).then(done, () => done({ status: null, stdout: '', stderr: '' }));
|
|
367
432
|
}
|
|
368
433
|
/** additionalContext + systemMessage from a background hook's JSON stdout — both go to the model. */
|
|
369
434
|
function directiveContext(out) {
|
|
@@ -410,7 +475,12 @@ function spawnHook(command, opts) {
|
|
|
410
475
|
};
|
|
411
476
|
let child;
|
|
412
477
|
try {
|
|
413
|
-
child = (0, child_process_1.spawn)(command, {
|
|
478
|
+
child = (0, child_process_1.spawn)(command, {
|
|
479
|
+
shell: true,
|
|
480
|
+
cwd: opts.cwd,
|
|
481
|
+
env: opts.env ?? process.env,
|
|
482
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
483
|
+
});
|
|
414
484
|
}
|
|
415
485
|
catch {
|
|
416
486
|
resolve({ status: null, stdout: '', stderr: '' });
|
|
@@ -419,10 +489,17 @@ function spawnHook(command, opts) {
|
|
|
419
489
|
// timeout === null = no timer at all: plain `async` hooks are not bounded
|
|
420
490
|
// (Claude Code enforces no timeout on them), and a clamped setTimeout would
|
|
421
491
|
// fire immediately and kill the hook instead of never firing.
|
|
422
|
-
const timer = opts.timeout === null
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
492
|
+
const timer = opts.timeout === null
|
|
493
|
+
? null
|
|
494
|
+
: setTimeout(() => {
|
|
495
|
+
try {
|
|
496
|
+
child.kill('SIGTERM');
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
/* gone */
|
|
500
|
+
}
|
|
501
|
+
done(null);
|
|
502
|
+
}, opts.timeout);
|
|
426
503
|
const cap = 16 * 1024 * 1024;
|
|
427
504
|
// `onData` is how `--include-hook-events` sees a long hook's output as it
|
|
428
505
|
// arrives; without such a listener it is undefined and costs one check.
|
|
@@ -440,7 +517,9 @@ function spawnHook(command, opts) {
|
|
|
440
517
|
});
|
|
441
518
|
child.on('error', () => done(null));
|
|
442
519
|
child.on('close', (code) => done(code));
|
|
443
|
-
child.stdin?.on('error', () => {
|
|
520
|
+
child.stdin?.on('error', () => {
|
|
521
|
+
/* hook ignored stdin */
|
|
522
|
+
});
|
|
444
523
|
child.stdin?.end(opts.input ?? '');
|
|
445
524
|
});
|
|
446
525
|
}
|
|
@@ -463,7 +542,7 @@ async function httpHook(hook, payload, timeoutMs) {
|
|
|
463
542
|
for (const [k, v] of Object.entries(hook.headers ?? {})) {
|
|
464
543
|
// Unlisted variables expand to nothing rather than leaking the whole environment
|
|
465
544
|
// into a request header.
|
|
466
|
-
headers[k] = String(v).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_m, a1, a2) =>
|
|
545
|
+
headers[k] = String(v).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_m, a1, a2) => allowed.has(a1 ?? a2) ? (process.env[a1 ?? a2] ?? '') : '');
|
|
467
546
|
}
|
|
468
547
|
try {
|
|
469
548
|
const res = await fetch(url, { method: 'POST', headers, body: payload, signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -526,7 +605,9 @@ function mcpHookInput(input, payload) {
|
|
|
526
605
|
if (j && typeof j === 'object' && !Array.isArray(j))
|
|
527
606
|
root = j;
|
|
528
607
|
}
|
|
529
|
-
catch {
|
|
608
|
+
catch {
|
|
609
|
+
/* non-JSON payload: nothing to substitute from */
|
|
610
|
+
}
|
|
530
611
|
const nav = (parts) => {
|
|
531
612
|
let cur = root;
|
|
532
613
|
for (const p of parts) {
|
|
@@ -537,7 +618,10 @@ function mcpHookInput(input, payload) {
|
|
|
537
618
|
return cur;
|
|
538
619
|
};
|
|
539
620
|
const lookup = (path) => {
|
|
540
|
-
const parts = path
|
|
621
|
+
const parts = path
|
|
622
|
+
.split('.')
|
|
623
|
+
.map((s) => s.trim())
|
|
624
|
+
.filter(Boolean);
|
|
541
625
|
if (!parts.length)
|
|
542
626
|
return undefined;
|
|
543
627
|
const candidates = [parts];
|
|
@@ -591,7 +675,13 @@ async function runHookHandler(hook, payload, opts) {
|
|
|
591
675
|
if (hook.type === 'prompt' || hook.type === 'agent') {
|
|
592
676
|
return promptHook(hook, payload, { ...opts, timeoutMs: opts.timeoutMs ?? 60000, envCtx: opts.hookEnv });
|
|
593
677
|
}
|
|
594
|
-
return spawnHook(String(hook.command ?? ''), {
|
|
678
|
+
return spawnHook(String(hook.command ?? ''), {
|
|
679
|
+
cwd: opts.cwd,
|
|
680
|
+
timeout: opts.timeoutMs,
|
|
681
|
+
input: payload,
|
|
682
|
+
env,
|
|
683
|
+
onData,
|
|
684
|
+
});
|
|
595
685
|
});
|
|
596
686
|
}
|
|
597
687
|
// ─── Prompt / agent hooks ──────────────────────────────────────────────────────
|
|
@@ -646,7 +736,11 @@ const HOOK_AGENT_TOOLS = [
|
|
|
646
736
|
{
|
|
647
737
|
name: 'glob',
|
|
648
738
|
description: 'Find files matching a glob pattern, e.g. "**/*.ts".',
|
|
649
|
-
input_schema: {
|
|
739
|
+
input_schema: {
|
|
740
|
+
type: 'object',
|
|
741
|
+
properties: { pattern: { type: 'string' }, path: { type: 'string' } },
|
|
742
|
+
required: ['pattern'],
|
|
743
|
+
},
|
|
650
744
|
},
|
|
651
745
|
];
|
|
652
746
|
/** A model name that cannot exist — the allowlist filter then sends NO tools at all. */
|
|
@@ -674,7 +768,7 @@ function parsePromptHookReply(reply) {
|
|
|
674
768
|
}
|
|
675
769
|
}
|
|
676
770
|
const PROMPT_HOOK_RULES = 'You are a hook evaluator for a coding agent. The hook input JSON is given below, followed by ' +
|
|
677
|
-
|
|
771
|
+
"the hook's own instruction. Decide whether the agent action described in the input should " +
|
|
678
772
|
'proceed. Reply with ONLY one JSON object and nothing else: {"ok":true} to allow, or ' +
|
|
679
773
|
'{"ok":false,"reason":"short explanation"} to block.';
|
|
680
774
|
async function promptHook(hook, payload, opts) {
|
|
@@ -682,14 +776,22 @@ async function promptHook(hook, payload, opts) {
|
|
|
682
776
|
if (!instruction)
|
|
683
777
|
return { status: 1, stdout: '', stderr: 'prompt hook has no "prompt"' };
|
|
684
778
|
// `\$ARGUMENTS` escapes the substitution (Claude Code's own rule).
|
|
685
|
-
const body = instruction
|
|
779
|
+
const body = instruction
|
|
780
|
+
.replace(/\\\$ARGUMENTS/g, '\u0001')
|
|
781
|
+
.replace(/\$ARGUMENTS/g, payload)
|
|
782
|
+
// eslint-disable-next-line no-control-regex -- U+0001 sentinel protects a literal $ARGUMENTS from being substituted twice
|
|
783
|
+
.replace(/\u0001/g, '$ARGUMENTS');
|
|
686
784
|
// Abort bridge: streamChat polls a plain {aborted} object, and a hook must never hang
|
|
687
785
|
// the turn — the timer flips it at the hook's own timeout, the outer signal aborts too.
|
|
688
786
|
const ctl = { aborted: false };
|
|
689
|
-
const timer = setTimeout(() => {
|
|
787
|
+
const timer = setTimeout(() => {
|
|
788
|
+
ctl.aborted = true;
|
|
789
|
+
}, Math.max(1000, opts.timeoutMs));
|
|
690
790
|
const outerPoll = opts.abortSignal
|
|
691
|
-
? setInterval(() => {
|
|
692
|
-
|
|
791
|
+
? setInterval(() => {
|
|
792
|
+
if (opts.abortSignal?.aborted)
|
|
793
|
+
ctl.aborted = true;
|
|
794
|
+
}, 100)
|
|
693
795
|
: undefined;
|
|
694
796
|
if (opts.abortSignal?.aborted) {
|
|
695
797
|
clearTimeout(timer);
|
|
@@ -706,10 +808,17 @@ async function promptHook(hook, payload, opts) {
|
|
|
706
808
|
clearInterval(outerPoll);
|
|
707
809
|
}
|
|
708
810
|
async function runModel() {
|
|
709
|
-
const messages = [
|
|
811
|
+
const messages = [
|
|
812
|
+
{
|
|
710
813
|
role: 'user',
|
|
711
|
-
content: [
|
|
712
|
-
|
|
814
|
+
content: [
|
|
815
|
+
{
|
|
816
|
+
type: 'text',
|
|
817
|
+
text: `${PROMPT_HOOK_RULES}\n\nHook input:\n${payload}\n\nHook instruction:\n${body}`,
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
},
|
|
821
|
+
];
|
|
713
822
|
const isAgent = hook.type === 'agent';
|
|
714
823
|
try {
|
|
715
824
|
let replyText = '';
|
|
@@ -723,11 +832,18 @@ async function promptHook(hook, payload, opts) {
|
|
|
723
832
|
// read-only tools and a prompt hook sees none (the marker matches nothing).
|
|
724
833
|
toolAllowlist: isAgent ? HOOK_AGENT_TOOLS.map((t) => t.name) : [NO_TOOLS_MARKER],
|
|
725
834
|
extraTools: isAgent ? HOOK_AGENT_TOOLS : undefined,
|
|
726
|
-
}, () => {
|
|
727
|
-
|
|
835
|
+
}, () => {
|
|
836
|
+
/* streaming not needed; the final message carries the text */
|
|
837
|
+
});
|
|
838
|
+
const blocks = Array.isArray(msg.content)
|
|
839
|
+
? msg.content
|
|
840
|
+
: [];
|
|
728
841
|
const toolUses = blocks.filter((b) => b.type === 'tool_use');
|
|
729
842
|
if (!toolUses.length || !isAgent) {
|
|
730
|
-
replyText = blocks
|
|
843
|
+
replyText = blocks
|
|
844
|
+
.filter((b) => b.type === 'text' && typeof b.text === 'string')
|
|
845
|
+
.map((b) => b.text)
|
|
846
|
+
.join('\n');
|
|
731
847
|
break;
|
|
732
848
|
}
|
|
733
849
|
// Run the read-only tool round and feed the results back.
|
|
@@ -738,7 +854,9 @@ async function promptHook(hook, payload, opts) {
|
|
|
738
854
|
try {
|
|
739
855
|
out = getHookToolExecutors().includes(String(tu.name))
|
|
740
856
|
? await (0, executor_1.executeTool)(String(tu.name), tu.input ?? {}, ctl, undefined, opts.cwd)
|
|
741
|
-
: {
|
|
857
|
+
: {
|
|
858
|
+
error: `Tool ${String(tu.name)} is not available to a hook — only ${getHookToolExecutors().join(', ')}.`,
|
|
859
|
+
};
|
|
742
860
|
}
|
|
743
861
|
catch (err) {
|
|
744
862
|
out = { error: err.message || 'tool failed' };
|
|
@@ -746,7 +864,11 @@ async function promptHook(hook, payload, opts) {
|
|
|
746
864
|
results.push({
|
|
747
865
|
type: 'tool_result',
|
|
748
866
|
tool_use_id: String(tu.id),
|
|
749
|
-
content: out.error
|
|
867
|
+
content: out.error
|
|
868
|
+
? out.output
|
|
869
|
+
? `Error: ${out.error}\n\n${out.output}`
|
|
870
|
+
: `Error: ${out.error}`
|
|
871
|
+
: (out.output ?? ''),
|
|
750
872
|
is_error: out.error !== undefined,
|
|
751
873
|
});
|
|
752
874
|
}
|
|
@@ -762,7 +884,9 @@ async function promptHook(hook, payload, opts) {
|
|
|
762
884
|
if (decided)
|
|
763
885
|
return { status: 2, stdout: '', stderr: parsed.reason || `Blocked by ${hook.type} hook` };
|
|
764
886
|
// Allow — carry additionalContext through the same JSON channel callers parse.
|
|
765
|
-
const passthrough = parsed.additionalContext
|
|
887
|
+
const passthrough = parsed.additionalContext
|
|
888
|
+
? JSON.stringify({ additionalContext: parsed.additionalContext })
|
|
889
|
+
: '';
|
|
766
890
|
return { status: 0, stdout: passthrough, stderr: '' };
|
|
767
891
|
}
|
|
768
892
|
catch (err) {
|
|
@@ -841,7 +965,11 @@ function hookTimeoutMs(hook, fallbackMs = 60000) {
|
|
|
841
965
|
function hookMatches(matcher, toolName) {
|
|
842
966
|
if (!matcher || matcher === '*')
|
|
843
967
|
return true;
|
|
844
|
-
return matcher
|
|
968
|
+
return matcher
|
|
969
|
+
.split('|')
|
|
970
|
+
.map((m) => m.trim())
|
|
971
|
+
.filter(Boolean)
|
|
972
|
+
.some((alt) => {
|
|
845
973
|
const norm = (0, agentTypes_1.normaliseToolName)(alt);
|
|
846
974
|
return norm === toolName || (norm === alt && toolName.includes(alt));
|
|
847
975
|
});
|
|
@@ -913,7 +1041,9 @@ async function runToolHooks(entries, phase, toolName, input, workDir, result, ho
|
|
|
913
1041
|
outcome.context = (outcome.context ? outcome.context + '\n' : '') + j.additionalContext;
|
|
914
1042
|
}
|
|
915
1043
|
}
|
|
916
|
-
catch {
|
|
1044
|
+
catch {
|
|
1045
|
+
/* not a directive — ignore */
|
|
1046
|
+
}
|
|
917
1047
|
}
|
|
918
1048
|
// Exit code 2 → hard block; stderr is the reason fed back to the model
|
|
919
1049
|
if (r.status === 2) {
|
|
@@ -940,10 +1070,23 @@ async function runSimpleHooks(defs, workDir, extraEnv, hookOpts, event = 'hook')
|
|
|
940
1070
|
if (gate.skip)
|
|
941
1071
|
continue;
|
|
942
1072
|
if (gate.background) {
|
|
943
|
-
startBackgroundHook(hook, '', {
|
|
1073
|
+
startBackgroundHook(hook, '', {
|
|
1074
|
+
cwd: workDir,
|
|
1075
|
+
env,
|
|
1076
|
+
eventLabel: event,
|
|
1077
|
+
event,
|
|
1078
|
+
timeoutMs: hook.asyncRewake ? hookTimeoutMs(hook, 10000) : null,
|
|
1079
|
+
key: gate.key,
|
|
1080
|
+
});
|
|
944
1081
|
continue;
|
|
945
1082
|
}
|
|
946
|
-
const r = await runHookHandler(hook, '', {
|
|
1083
|
+
const r = await runHookHandler(hook, '', {
|
|
1084
|
+
cwd: workDir,
|
|
1085
|
+
timeoutMs: hookTimeoutMs(hook, 10000),
|
|
1086
|
+
env,
|
|
1087
|
+
event,
|
|
1088
|
+
...hookOpts,
|
|
1089
|
+
});
|
|
947
1090
|
if (gate.statusId !== undefined)
|
|
948
1091
|
hookStatusEnd(gate.statusId);
|
|
949
1092
|
if (r.status === 0 && gate.key)
|
|
@@ -963,7 +1106,13 @@ function flattenHookDefs(items, matchValue) {
|
|
|
963
1106
|
const m = it.matcher;
|
|
964
1107
|
// For these events the matcher (if any) selects on a plain value (e.g. the compact
|
|
965
1108
|
// trigger "auto"/"manual"); empty or "*" matches all.
|
|
966
|
-
if (m &&
|
|
1109
|
+
if (m &&
|
|
1110
|
+
m !== '*' &&
|
|
1111
|
+
matchValue !== undefined &&
|
|
1112
|
+
!m
|
|
1113
|
+
.split('|')
|
|
1114
|
+
.map((x) => x.trim())
|
|
1115
|
+
.includes(matchValue))
|
|
967
1116
|
continue;
|
|
968
1117
|
out.push(...it.hooks);
|
|
969
1118
|
}
|
|
@@ -1012,19 +1161,30 @@ async function runLifecycleHooks(items, event, workDir, payload, matchValue, hoo
|
|
|
1012
1161
|
continue;
|
|
1013
1162
|
}
|
|
1014
1163
|
const timeout = hookTimeoutMs(hook, hook.type === 'agent' ? 60000 : hook.type === 'prompt' ? 30000 : 30000);
|
|
1015
|
-
const r = await runHookHandler(hook, input, {
|
|
1164
|
+
const r = await runHookHandler(hook, input, {
|
|
1165
|
+
cwd: workDir,
|
|
1166
|
+
timeoutMs: timeout,
|
|
1167
|
+
env: { ...process.env, NEXRALL_HOOK_PHASE: event },
|
|
1168
|
+
event,
|
|
1169
|
+
...hookOpts,
|
|
1170
|
+
});
|
|
1016
1171
|
const out = (r.stdout ?? '').toString().trim();
|
|
1017
1172
|
if (out.startsWith('{')) {
|
|
1018
1173
|
try {
|
|
1019
1174
|
const j = JSON.parse(out);
|
|
1020
1175
|
// PermissionRequest answers, in nex's flat form or Claude Code's hookSpecificOutput form.
|
|
1021
1176
|
if (event === 'PermissionRequest') {
|
|
1022
|
-
const d = j.hookSpecificOutput?.decision?.behavior ??
|
|
1177
|
+
const d = j.hookSpecificOutput?.decision?.behavior ??
|
|
1178
|
+
(typeof j.decision === 'string' ? j.decision : j.decision?.behavior);
|
|
1023
1179
|
if (d === 'allow' || d === 'approve')
|
|
1024
1180
|
outcome.allow = true;
|
|
1025
1181
|
else if (d === 'deny' || d === 'block') {
|
|
1026
1182
|
outcome.block = true;
|
|
1027
|
-
outcome.reason =
|
|
1183
|
+
outcome.reason =
|
|
1184
|
+
j.reason ??
|
|
1185
|
+
j.hookSpecificOutput?.decision?.message ??
|
|
1186
|
+
outcome.reason ??
|
|
1187
|
+
'Denied by PermissionRequest hook';
|
|
1028
1188
|
}
|
|
1029
1189
|
}
|
|
1030
1190
|
if (j.decision === 'block') {
|
|
@@ -1039,7 +1199,9 @@ async function runLifecycleHooks(items, event, workDir, payload, matchValue, hoo
|
|
|
1039
1199
|
if (typeof j.systemMessage === 'string' && j.systemMessage)
|
|
1040
1200
|
outcome.systemMessage = j.systemMessage;
|
|
1041
1201
|
}
|
|
1042
|
-
catch {
|
|
1202
|
+
catch {
|
|
1203
|
+
/* not a directive */
|
|
1204
|
+
}
|
|
1043
1205
|
}
|
|
1044
1206
|
else if (out && (event === 'SessionStart' || event === 'UserPromptSubmit') && r.status === 0) {
|
|
1045
1207
|
// Claude Code parity: plain stdout from these two events is added as context.
|
|
@@ -1073,7 +1235,9 @@ async function runPermissionRequestHooks(workDir, tool, input, sessionId = '') {
|
|
|
1073
1235
|
if (o.allow)
|
|
1074
1236
|
return { answer: 'allow' };
|
|
1075
1237
|
}
|
|
1076
|
-
catch {
|
|
1238
|
+
catch {
|
|
1239
|
+
/* a hook must never break the prompt */
|
|
1240
|
+
}
|
|
1077
1241
|
return { answer: null };
|
|
1078
1242
|
}
|
|
1079
1243
|
/** `hookSpecificOutput:{action,content}` (Claude Code's shape) or the flat form; null = no answer. */
|
|
@@ -1109,7 +1273,13 @@ async function runElicitationHooks(workDir, server, input, sessionId = '') {
|
|
|
1109
1273
|
const defs = flattenHookDefs(items, server);
|
|
1110
1274
|
if (!defs.length)
|
|
1111
1275
|
return null;
|
|
1112
|
-
const payload = JSON.stringify({
|
|
1276
|
+
const payload = JSON.stringify({
|
|
1277
|
+
session_id: sessionId,
|
|
1278
|
+
cwd: workDir,
|
|
1279
|
+
hook_event_name: 'Elicitation',
|
|
1280
|
+
mcp_server_name: server,
|
|
1281
|
+
...input,
|
|
1282
|
+
});
|
|
1113
1283
|
for (const hook of defs) {
|
|
1114
1284
|
if (!isRunnableHook(hook))
|
|
1115
1285
|
continue;
|
|
@@ -1117,10 +1287,22 @@ async function runElicitationHooks(workDir, server, input, sessionId = '') {
|
|
|
1117
1287
|
if (gate.skip)
|
|
1118
1288
|
continue;
|
|
1119
1289
|
if (gate.background) {
|
|
1120
|
-
startBackgroundHook(hook, payload, {
|
|
1290
|
+
startBackgroundHook(hook, payload, {
|
|
1291
|
+
cwd: workDir,
|
|
1292
|
+
env: { ...process.env, NEXRALL_HOOK_PHASE: 'Elicitation' },
|
|
1293
|
+
eventLabel: 'Elicitation',
|
|
1294
|
+
event: 'Elicitation',
|
|
1295
|
+
timeoutMs: hook.asyncRewake ? hookTimeoutMs(hook, 60000) : null,
|
|
1296
|
+
key: gate.key,
|
|
1297
|
+
});
|
|
1121
1298
|
continue;
|
|
1122
1299
|
}
|
|
1123
|
-
const r = await runHookHandler(hook, payload, {
|
|
1300
|
+
const r = await runHookHandler(hook, payload, {
|
|
1301
|
+
cwd: workDir,
|
|
1302
|
+
timeoutMs: hookTimeoutMs(hook, 30000),
|
|
1303
|
+
env: { ...process.env, NEXRALL_HOOK_PHASE: 'Elicitation' },
|
|
1304
|
+
event: 'Elicitation',
|
|
1305
|
+
});
|
|
1124
1306
|
if (gate.statusId !== undefined)
|
|
1125
1307
|
hookStatusEnd(gate.statusId);
|
|
1126
1308
|
if (r.status === 0 && gate.key)
|
|
@@ -1132,7 +1314,9 @@ async function runElicitationHooks(workDir, server, input, sessionId = '') {
|
|
|
1132
1314
|
return answer;
|
|
1133
1315
|
}
|
|
1134
1316
|
}
|
|
1135
|
-
catch {
|
|
1317
|
+
catch {
|
|
1318
|
+
/* a hook must never break elicitation */
|
|
1319
|
+
}
|
|
1136
1320
|
return null;
|
|
1137
1321
|
}
|
|
1138
1322
|
/**
|
|
@@ -1156,14 +1340,32 @@ async function runElicitationResultHooks(workDir, server, answer, meta = {}) {
|
|
|
1156
1340
|
if (gate.skip)
|
|
1157
1341
|
continue;
|
|
1158
1342
|
const payload = JSON.stringify({
|
|
1159
|
-
session_id: meta.sessionId ?? '',
|
|
1160
|
-
|
|
1343
|
+
session_id: meta.sessionId ?? '',
|
|
1344
|
+
cwd: workDir,
|
|
1345
|
+
hook_event_name: 'ElicitationResult',
|
|
1346
|
+
mcp_server_name: server,
|
|
1347
|
+
action: current.action,
|
|
1348
|
+
content: current.content,
|
|
1349
|
+
mode: meta.mode,
|
|
1350
|
+
elicitation_id: meta.elicitationId,
|
|
1161
1351
|
});
|
|
1162
1352
|
if (gate.background) {
|
|
1163
|
-
startBackgroundHook(hook, payload, {
|
|
1353
|
+
startBackgroundHook(hook, payload, {
|
|
1354
|
+
cwd: workDir,
|
|
1355
|
+
env: { ...process.env, NEXRALL_HOOK_PHASE: 'ElicitationResult' },
|
|
1356
|
+
eventLabel: 'ElicitationResult',
|
|
1357
|
+
event: 'ElicitationResult',
|
|
1358
|
+
timeoutMs: hook.asyncRewake ? hookTimeoutMs(hook, 60000) : null,
|
|
1359
|
+
key: gate.key,
|
|
1360
|
+
});
|
|
1164
1361
|
continue;
|
|
1165
1362
|
}
|
|
1166
|
-
const r = await runHookHandler(hook, payload, {
|
|
1363
|
+
const r = await runHookHandler(hook, payload, {
|
|
1364
|
+
cwd: workDir,
|
|
1365
|
+
timeoutMs: hookTimeoutMs(hook, 30000),
|
|
1366
|
+
env: { ...process.env, NEXRALL_HOOK_PHASE: 'ElicitationResult' },
|
|
1367
|
+
event: 'ElicitationResult',
|
|
1368
|
+
});
|
|
1167
1369
|
if (gate.statusId !== undefined)
|
|
1168
1370
|
hookStatusEnd(gate.statusId);
|
|
1169
1371
|
if (r.status === 0 && gate.key)
|
|
@@ -1212,9 +1414,13 @@ function fireNotificationHooks(workDir, message, type = 'permission_prompt') {
|
|
|
1212
1414
|
const items = loadHooks(workDir).Notification;
|
|
1213
1415
|
if (!items?.length)
|
|
1214
1416
|
return;
|
|
1215
|
-
void runLifecycleHooks(items, 'Notification', workDir, { message, notification_type: type }).catch(() => {
|
|
1417
|
+
void runLifecycleHooks(items, 'Notification', workDir, { message, notification_type: type }).catch(() => {
|
|
1418
|
+
/* a hook must never break the prompt */
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
catch {
|
|
1422
|
+
/* ignore */
|
|
1216
1423
|
}
|
|
1217
|
-
catch { /* ignore */ }
|
|
1218
1424
|
}
|
|
1219
1425
|
/** For hosts that run their own manual /compact: fire PreCompact (trigger "manual"), awaited, never blocking. */
|
|
1220
1426
|
/** The manual /compact counterpart of the auto PostCompact hook (trigger "manual"). */
|
|
@@ -1225,7 +1431,9 @@ async function fireManualPostCompactHook(workDir, sessionId, messagesSummarized)
|
|
|
1225
1431
|
return;
|
|
1226
1432
|
await runLifecycleHooks(items, 'PostCompact', workDir, { trigger: 'manual', session_id: sessionId, messages_summarized: messagesSummarized }, 'manual');
|
|
1227
1433
|
}
|
|
1228
|
-
catch {
|
|
1434
|
+
catch {
|
|
1435
|
+
/* a hook must never break compaction */
|
|
1436
|
+
}
|
|
1229
1437
|
}
|
|
1230
1438
|
async function fireManualPreCompactHook(workDir, sessionId, messagesToSummarize) {
|
|
1231
1439
|
try {
|
|
@@ -1234,6 +1442,8 @@ async function fireManualPreCompactHook(workDir, sessionId, messagesToSummarize)
|
|
|
1234
1442
|
return;
|
|
1235
1443
|
await runLifecycleHooks(items, 'PreCompact', workDir, { trigger: 'manual', session_id: sessionId, messages_to_summarize: messagesToSummarize }, 'manual');
|
|
1236
1444
|
}
|
|
1237
|
-
catch {
|
|
1445
|
+
catch {
|
|
1446
|
+
/* a hook must never break compaction */
|
|
1447
|
+
}
|
|
1238
1448
|
}
|
|
1239
1449
|
//# sourceMappingURL=hooks.js.map
|