@nexrall/code-core 1.4.71 → 1.4.73
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 +14 -6
- 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 -75
- package/dist/agent/iterationPolicy.d.ts.map +1 -1
- package/dist/agent/iterationPolicy.js +26 -23
- package/dist/agent/lifecycleHost.d.ts.map +1 -1
- package/dist/agent/lifecycleHost.js +35 -11
- package/dist/agent/loop.d.ts +22 -4
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +238 -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 +4 -2
- 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 +6 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +64 -31
- 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 +22 -7
- 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 +42 -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 +35 -0
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +135 -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 +454 -168
- 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 +3 -1
- 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,15 +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
|
-
|
|
686
|
-
|
|
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');
|
|
687
784
|
// Abort bridge: streamChat polls a plain {aborted} object, and a hook must never hang
|
|
688
785
|
// the turn — the timer flips it at the hook's own timeout, the outer signal aborts too.
|
|
689
786
|
const ctl = { aborted: false };
|
|
690
|
-
const timer = setTimeout(() => {
|
|
787
|
+
const timer = setTimeout(() => {
|
|
788
|
+
ctl.aborted = true;
|
|
789
|
+
}, Math.max(1000, opts.timeoutMs));
|
|
691
790
|
const outerPoll = opts.abortSignal
|
|
692
|
-
? setInterval(() => {
|
|
693
|
-
|
|
791
|
+
? setInterval(() => {
|
|
792
|
+
if (opts.abortSignal?.aborted)
|
|
793
|
+
ctl.aborted = true;
|
|
794
|
+
}, 100)
|
|
694
795
|
: undefined;
|
|
695
796
|
if (opts.abortSignal?.aborted) {
|
|
696
797
|
clearTimeout(timer);
|
|
@@ -707,10 +808,17 @@ async function promptHook(hook, payload, opts) {
|
|
|
707
808
|
clearInterval(outerPoll);
|
|
708
809
|
}
|
|
709
810
|
async function runModel() {
|
|
710
|
-
const messages = [
|
|
811
|
+
const messages = [
|
|
812
|
+
{
|
|
711
813
|
role: 'user',
|
|
712
|
-
content: [
|
|
713
|
-
|
|
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
|
+
];
|
|
714
822
|
const isAgent = hook.type === 'agent';
|
|
715
823
|
try {
|
|
716
824
|
let replyText = '';
|
|
@@ -724,11 +832,18 @@ async function promptHook(hook, payload, opts) {
|
|
|
724
832
|
// read-only tools and a prompt hook sees none (the marker matches nothing).
|
|
725
833
|
toolAllowlist: isAgent ? HOOK_AGENT_TOOLS.map((t) => t.name) : [NO_TOOLS_MARKER],
|
|
726
834
|
extraTools: isAgent ? HOOK_AGENT_TOOLS : undefined,
|
|
727
|
-
}, () => {
|
|
728
|
-
|
|
835
|
+
}, () => {
|
|
836
|
+
/* streaming not needed; the final message carries the text */
|
|
837
|
+
});
|
|
838
|
+
const blocks = Array.isArray(msg.content)
|
|
839
|
+
? msg.content
|
|
840
|
+
: [];
|
|
729
841
|
const toolUses = blocks.filter((b) => b.type === 'tool_use');
|
|
730
842
|
if (!toolUses.length || !isAgent) {
|
|
731
|
-
replyText = blocks
|
|
843
|
+
replyText = blocks
|
|
844
|
+
.filter((b) => b.type === 'text' && typeof b.text === 'string')
|
|
845
|
+
.map((b) => b.text)
|
|
846
|
+
.join('\n');
|
|
732
847
|
break;
|
|
733
848
|
}
|
|
734
849
|
// Run the read-only tool round and feed the results back.
|
|
@@ -739,7 +854,9 @@ async function promptHook(hook, payload, opts) {
|
|
|
739
854
|
try {
|
|
740
855
|
out = getHookToolExecutors().includes(String(tu.name))
|
|
741
856
|
? await (0, executor_1.executeTool)(String(tu.name), tu.input ?? {}, ctl, undefined, opts.cwd)
|
|
742
|
-
: {
|
|
857
|
+
: {
|
|
858
|
+
error: `Tool ${String(tu.name)} is not available to a hook — only ${getHookToolExecutors().join(', ')}.`,
|
|
859
|
+
};
|
|
743
860
|
}
|
|
744
861
|
catch (err) {
|
|
745
862
|
out = { error: err.message || 'tool failed' };
|
|
@@ -747,7 +864,11 @@ async function promptHook(hook, payload, opts) {
|
|
|
747
864
|
results.push({
|
|
748
865
|
type: 'tool_result',
|
|
749
866
|
tool_use_id: String(tu.id),
|
|
750
|
-
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 ?? ''),
|
|
751
872
|
is_error: out.error !== undefined,
|
|
752
873
|
});
|
|
753
874
|
}
|
|
@@ -763,7 +884,9 @@ async function promptHook(hook, payload, opts) {
|
|
|
763
884
|
if (decided)
|
|
764
885
|
return { status: 2, stdout: '', stderr: parsed.reason || `Blocked by ${hook.type} hook` };
|
|
765
886
|
// Allow — carry additionalContext through the same JSON channel callers parse.
|
|
766
|
-
const passthrough = parsed.additionalContext
|
|
887
|
+
const passthrough = parsed.additionalContext
|
|
888
|
+
? JSON.stringify({ additionalContext: parsed.additionalContext })
|
|
889
|
+
: '';
|
|
767
890
|
return { status: 0, stdout: passthrough, stderr: '' };
|
|
768
891
|
}
|
|
769
892
|
catch (err) {
|
|
@@ -842,7 +965,11 @@ function hookTimeoutMs(hook, fallbackMs = 60000) {
|
|
|
842
965
|
function hookMatches(matcher, toolName) {
|
|
843
966
|
if (!matcher || matcher === '*')
|
|
844
967
|
return true;
|
|
845
|
-
return matcher
|
|
968
|
+
return matcher
|
|
969
|
+
.split('|')
|
|
970
|
+
.map((m) => m.trim())
|
|
971
|
+
.filter(Boolean)
|
|
972
|
+
.some((alt) => {
|
|
846
973
|
const norm = (0, agentTypes_1.normaliseToolName)(alt);
|
|
847
974
|
return norm === toolName || (norm === alt && toolName.includes(alt));
|
|
848
975
|
});
|
|
@@ -914,7 +1041,9 @@ async function runToolHooks(entries, phase, toolName, input, workDir, result, ho
|
|
|
914
1041
|
outcome.context = (outcome.context ? outcome.context + '\n' : '') + j.additionalContext;
|
|
915
1042
|
}
|
|
916
1043
|
}
|
|
917
|
-
catch {
|
|
1044
|
+
catch {
|
|
1045
|
+
/* not a directive — ignore */
|
|
1046
|
+
}
|
|
918
1047
|
}
|
|
919
1048
|
// Exit code 2 → hard block; stderr is the reason fed back to the model
|
|
920
1049
|
if (r.status === 2) {
|
|
@@ -941,10 +1070,23 @@ async function runSimpleHooks(defs, workDir, extraEnv, hookOpts, event = 'hook')
|
|
|
941
1070
|
if (gate.skip)
|
|
942
1071
|
continue;
|
|
943
1072
|
if (gate.background) {
|
|
944
|
-
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
|
+
});
|
|
945
1081
|
continue;
|
|
946
1082
|
}
|
|
947
|
-
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
|
+
});
|
|
948
1090
|
if (gate.statusId !== undefined)
|
|
949
1091
|
hookStatusEnd(gate.statusId);
|
|
950
1092
|
if (r.status === 0 && gate.key)
|
|
@@ -964,7 +1106,13 @@ function flattenHookDefs(items, matchValue) {
|
|
|
964
1106
|
const m = it.matcher;
|
|
965
1107
|
// For these events the matcher (if any) selects on a plain value (e.g. the compact
|
|
966
1108
|
// trigger "auto"/"manual"); empty or "*" matches all.
|
|
967
|
-
if (m &&
|
|
1109
|
+
if (m &&
|
|
1110
|
+
m !== '*' &&
|
|
1111
|
+
matchValue !== undefined &&
|
|
1112
|
+
!m
|
|
1113
|
+
.split('|')
|
|
1114
|
+
.map((x) => x.trim())
|
|
1115
|
+
.includes(matchValue))
|
|
968
1116
|
continue;
|
|
969
1117
|
out.push(...it.hooks);
|
|
970
1118
|
}
|
|
@@ -1013,19 +1161,30 @@ async function runLifecycleHooks(items, event, workDir, payload, matchValue, hoo
|
|
|
1013
1161
|
continue;
|
|
1014
1162
|
}
|
|
1015
1163
|
const timeout = hookTimeoutMs(hook, hook.type === 'agent' ? 60000 : hook.type === 'prompt' ? 30000 : 30000);
|
|
1016
|
-
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
|
+
});
|
|
1017
1171
|
const out = (r.stdout ?? '').toString().trim();
|
|
1018
1172
|
if (out.startsWith('{')) {
|
|
1019
1173
|
try {
|
|
1020
1174
|
const j = JSON.parse(out);
|
|
1021
1175
|
// PermissionRequest answers, in nex's flat form or Claude Code's hookSpecificOutput form.
|
|
1022
1176
|
if (event === 'PermissionRequest') {
|
|
1023
|
-
const d = j.hookSpecificOutput?.decision?.behavior ??
|
|
1177
|
+
const d = j.hookSpecificOutput?.decision?.behavior ??
|
|
1178
|
+
(typeof j.decision === 'string' ? j.decision : j.decision?.behavior);
|
|
1024
1179
|
if (d === 'allow' || d === 'approve')
|
|
1025
1180
|
outcome.allow = true;
|
|
1026
1181
|
else if (d === 'deny' || d === 'block') {
|
|
1027
1182
|
outcome.block = true;
|
|
1028
|
-
outcome.reason =
|
|
1183
|
+
outcome.reason =
|
|
1184
|
+
j.reason ??
|
|
1185
|
+
j.hookSpecificOutput?.decision?.message ??
|
|
1186
|
+
outcome.reason ??
|
|
1187
|
+
'Denied by PermissionRequest hook';
|
|
1029
1188
|
}
|
|
1030
1189
|
}
|
|
1031
1190
|
if (j.decision === 'block') {
|
|
@@ -1040,7 +1199,9 @@ async function runLifecycleHooks(items, event, workDir, payload, matchValue, hoo
|
|
|
1040
1199
|
if (typeof j.systemMessage === 'string' && j.systemMessage)
|
|
1041
1200
|
outcome.systemMessage = j.systemMessage;
|
|
1042
1201
|
}
|
|
1043
|
-
catch {
|
|
1202
|
+
catch {
|
|
1203
|
+
/* not a directive */
|
|
1204
|
+
}
|
|
1044
1205
|
}
|
|
1045
1206
|
else if (out && (event === 'SessionStart' || event === 'UserPromptSubmit') && r.status === 0) {
|
|
1046
1207
|
// Claude Code parity: plain stdout from these two events is added as context.
|
|
@@ -1074,7 +1235,9 @@ async function runPermissionRequestHooks(workDir, tool, input, sessionId = '') {
|
|
|
1074
1235
|
if (o.allow)
|
|
1075
1236
|
return { answer: 'allow' };
|
|
1076
1237
|
}
|
|
1077
|
-
catch {
|
|
1238
|
+
catch {
|
|
1239
|
+
/* a hook must never break the prompt */
|
|
1240
|
+
}
|
|
1078
1241
|
return { answer: null };
|
|
1079
1242
|
}
|
|
1080
1243
|
/** `hookSpecificOutput:{action,content}` (Claude Code's shape) or the flat form; null = no answer. */
|
|
@@ -1110,7 +1273,13 @@ async function runElicitationHooks(workDir, server, input, sessionId = '') {
|
|
|
1110
1273
|
const defs = flattenHookDefs(items, server);
|
|
1111
1274
|
if (!defs.length)
|
|
1112
1275
|
return null;
|
|
1113
|
-
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
|
+
});
|
|
1114
1283
|
for (const hook of defs) {
|
|
1115
1284
|
if (!isRunnableHook(hook))
|
|
1116
1285
|
continue;
|
|
@@ -1118,10 +1287,22 @@ async function runElicitationHooks(workDir, server, input, sessionId = '') {
|
|
|
1118
1287
|
if (gate.skip)
|
|
1119
1288
|
continue;
|
|
1120
1289
|
if (gate.background) {
|
|
1121
|
-
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
|
+
});
|
|
1122
1298
|
continue;
|
|
1123
1299
|
}
|
|
1124
|
-
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
|
+
});
|
|
1125
1306
|
if (gate.statusId !== undefined)
|
|
1126
1307
|
hookStatusEnd(gate.statusId);
|
|
1127
1308
|
if (r.status === 0 && gate.key)
|
|
@@ -1133,7 +1314,9 @@ async function runElicitationHooks(workDir, server, input, sessionId = '') {
|
|
|
1133
1314
|
return answer;
|
|
1134
1315
|
}
|
|
1135
1316
|
}
|
|
1136
|
-
catch {
|
|
1317
|
+
catch {
|
|
1318
|
+
/* a hook must never break elicitation */
|
|
1319
|
+
}
|
|
1137
1320
|
return null;
|
|
1138
1321
|
}
|
|
1139
1322
|
/**
|
|
@@ -1157,14 +1340,32 @@ async function runElicitationResultHooks(workDir, server, answer, meta = {}) {
|
|
|
1157
1340
|
if (gate.skip)
|
|
1158
1341
|
continue;
|
|
1159
1342
|
const payload = JSON.stringify({
|
|
1160
|
-
session_id: meta.sessionId ?? '',
|
|
1161
|
-
|
|
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,
|
|
1162
1351
|
});
|
|
1163
1352
|
if (gate.background) {
|
|
1164
|
-
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
|
+
});
|
|
1165
1361
|
continue;
|
|
1166
1362
|
}
|
|
1167
|
-
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
|
+
});
|
|
1168
1369
|
if (gate.statusId !== undefined)
|
|
1169
1370
|
hookStatusEnd(gate.statusId);
|
|
1170
1371
|
if (r.status === 0 && gate.key)
|
|
@@ -1213,9 +1414,13 @@ function fireNotificationHooks(workDir, message, type = 'permission_prompt') {
|
|
|
1213
1414
|
const items = loadHooks(workDir).Notification;
|
|
1214
1415
|
if (!items?.length)
|
|
1215
1416
|
return;
|
|
1216
|
-
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 */
|
|
1217
1423
|
}
|
|
1218
|
-
catch { /* ignore */ }
|
|
1219
1424
|
}
|
|
1220
1425
|
/** For hosts that run their own manual /compact: fire PreCompact (trigger "manual"), awaited, never blocking. */
|
|
1221
1426
|
/** The manual /compact counterpart of the auto PostCompact hook (trigger "manual"). */
|
|
@@ -1226,7 +1431,9 @@ async function fireManualPostCompactHook(workDir, sessionId, messagesSummarized)
|
|
|
1226
1431
|
return;
|
|
1227
1432
|
await runLifecycleHooks(items, 'PostCompact', workDir, { trigger: 'manual', session_id: sessionId, messages_summarized: messagesSummarized }, 'manual');
|
|
1228
1433
|
}
|
|
1229
|
-
catch {
|
|
1434
|
+
catch {
|
|
1435
|
+
/* a hook must never break compaction */
|
|
1436
|
+
}
|
|
1230
1437
|
}
|
|
1231
1438
|
async function fireManualPreCompactHook(workDir, sessionId, messagesToSummarize) {
|
|
1232
1439
|
try {
|
|
@@ -1235,6 +1442,8 @@ async function fireManualPreCompactHook(workDir, sessionId, messagesToSummarize)
|
|
|
1235
1442
|
return;
|
|
1236
1443
|
await runLifecycleHooks(items, 'PreCompact', workDir, { trigger: 'manual', session_id: sessionId, messages_to_summarize: messagesToSummarize }, 'manual');
|
|
1237
1444
|
}
|
|
1238
|
-
catch {
|
|
1445
|
+
catch {
|
|
1446
|
+
/* a hook must never break compaction */
|
|
1447
|
+
}
|
|
1239
1448
|
}
|
|
1240
1449
|
//# sourceMappingURL=hooks.js.map
|