praxis-agent 0.31.0 → 0.32.0
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/application/session-service.d.ts +19 -0
- package/dist/application/session-service.js +310 -60
- package/dist/application/subagent-service.js +6 -0
- package/dist/cli/interactive.d.ts +3 -1
- package/dist/cli/interactive.js +16 -1
- package/dist/cli/tui/hook-settings.js +14 -1
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/cli-runtime.js +18 -0
- package/dist/compatibility/claude/context.d.ts +6 -1
- package/dist/compatibility/claude/context.js +14 -1
- package/dist/core/runtime.d.ts +4 -1
- package/dist/core/runtime.js +9 -1
- package/dist/hooks/claude-file-change-watcher.d.ts +43 -0
- package/dist/hooks/claude-file-change-watcher.js +185 -0
- package/dist/hooks/claude-hook-tools.d.ts +2 -0
- package/dist/hooks/claude-hook-tools.js +45 -6
- package/dist/hooks/claude-hooks.d.ts +14 -2
- package/dist/hooks/claude-hooks.js +230 -5
- package/dist/hooks/claude-session-environment.d.ts +16 -0
- package/dist/hooks/claude-session-environment.js +112 -0
- package/dist/tools/claude-task-tools.d.ts +5 -0
- package/dist/tools/claude-task-tools.js +15 -1
- package/dist/tools/local-tools.d.ts +2 -0
- package/dist/tools/local-tools.js +11 -4
- package/package.json +1 -1
|
@@ -110,7 +110,20 @@ const TUI_HOOK_EVENT_DEFINITIONS = [
|
|
|
110
110
|
detail: [],
|
|
111
111
|
},
|
|
112
112
|
];
|
|
113
|
-
|
|
113
|
+
const OBSERVED_TUI_EVENTS_2_1_208 = new Set([
|
|
114
|
+
'PreToolUse',
|
|
115
|
+
'PostToolUse',
|
|
116
|
+
'PostToolUseFailure',
|
|
117
|
+
'UserPromptSubmit',
|
|
118
|
+
'SessionStart',
|
|
119
|
+
'Stop',
|
|
120
|
+
'SubagentStart',
|
|
121
|
+
'SubagentStop',
|
|
122
|
+
'SessionEnd',
|
|
123
|
+
'PermissionRequest',
|
|
124
|
+
]);
|
|
125
|
+
export const TUI_HOOK_EVENTS = TUI_HOOK_EVENT_DEFINITIONS.filter((definition) => OBSERVED_TUI_EVENTS_2_1_208.has(definition.name) &&
|
|
126
|
+
HOOK_EVENTS.includes(definition.name));
|
|
114
127
|
function isRecord(value) {
|
|
115
128
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
116
129
|
}
|
package/dist/cli-runtime.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ interface SessionCommands {
|
|
|
39
39
|
rewindFiles?(sessionId: string, userMessageId: string): Promise<void>;
|
|
40
40
|
rewindPoints?(sessionId: string): Promise<RewindPoint[]>;
|
|
41
41
|
changeCwd?(sessionId: string | undefined, cwd: string): Promise<string>;
|
|
42
|
+
notify?(sessionId: string | undefined, message: string, notificationType: string, title?: string): void;
|
|
42
43
|
recordCdUsage?(sessionId: string): Promise<void>;
|
|
43
44
|
approveRecentlyDenied?(sessionId: string, display: string): Promise<void>;
|
|
44
45
|
retryRecentlyDenied?(sessionId: string, display: string, signal?: AbortSignal): Promise<SessionRunResult>;
|
package/dist/cli-runtime.js
CHANGED
|
@@ -33,6 +33,7 @@ import { createClaudeModelAutoClassifier, defaultClaudeAutoModeConfig, loadClaud
|
|
|
33
33
|
import { ClaudeExtensionPermissionResolver, ClaudeExtensionToolRegistry, } from './extensions/claude-extension-tools.js';
|
|
34
34
|
import { ClaudeExtensionCatalog } from './extensions/claude-extensions.js';
|
|
35
35
|
import { ClaudeHookRunner } from './hooks/claude-hooks.js';
|
|
36
|
+
import { ClaudeSessionEnvironment } from './hooks/claude-session-environment.js';
|
|
36
37
|
import { ClaudeMcpToolRegistry, } from './mcp/claude-mcp-tools.js';
|
|
37
38
|
import { ClaudeMcpManagement, filterDisabledMcpResources, mcpScope, } from './mcp/claude-mcp-management.js';
|
|
38
39
|
import { authenticateMcpServer, ClaudeMcpOAuthStore, mcpOAuthServerIdentity, readMcpClientSecret, } from './mcp/claude-mcp-oauth.js';
|
|
@@ -1226,6 +1227,15 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1226
1227
|
: {}),
|
|
1227
1228
|
}), extensions);
|
|
1228
1229
|
const permissions = permissionResolverForMode(cli.dangerouslySkipPermissions ? 'bypassPermissions' : cli.permissionMode);
|
|
1230
|
+
const hookSessionEnvironment = new ClaudeSessionEnvironment({
|
|
1231
|
+
stateRoot: resolveDataPlanePaths({
|
|
1232
|
+
dataPlane,
|
|
1233
|
+
root: configRoot,
|
|
1234
|
+
cwd,
|
|
1235
|
+
sessionId: '00000000-0000-4000-8000-000000000000',
|
|
1236
|
+
}).stateRoot,
|
|
1237
|
+
warn: (message) => runtimeEventSink({ type: 'warning', message }),
|
|
1238
|
+
});
|
|
1229
1239
|
const localTools = new LocalToolRegistry({
|
|
1230
1240
|
cwd,
|
|
1231
1241
|
cwdProvider: () => workspace.cwd(),
|
|
@@ -1237,6 +1247,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1237
1247
|
homeDirectory: sandboxEnvironment.HOME ?? sandboxEnvironment.USERPROFILE ?? homedir(),
|
|
1238
1248
|
configRoot,
|
|
1239
1249
|
dataPlane,
|
|
1250
|
+
sessionEnvironment: (sessionId) => hookSessionEnvironment.environment(sessionId),
|
|
1240
1251
|
...(environment ? { environment } : {}),
|
|
1241
1252
|
});
|
|
1242
1253
|
const runtimeMcpResources = async (candidates) => {
|
|
@@ -1437,6 +1448,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1437
1448
|
settings,
|
|
1438
1449
|
cwd,
|
|
1439
1450
|
onEvent: (event) => runtimeEventSink({ type: 'hook', event }),
|
|
1451
|
+
sessionEnvironment: hookSessionEnvironment,
|
|
1440
1452
|
});
|
|
1441
1453
|
const interactiveTools = selectedInteractiveTools.length > 0 && askUser && approvePlan
|
|
1442
1454
|
? new ClaudeInteractiveToolManager({
|
|
@@ -1454,6 +1466,11 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1454
1466
|
: undefined;
|
|
1455
1467
|
contextAssembler = new ClaudeContextAssembler({
|
|
1456
1468
|
loadResources: loadContextResources,
|
|
1469
|
+
onInstructionsLoaded: async (instructions, context) => {
|
|
1470
|
+
if (!context.lifecycleId)
|
|
1471
|
+
return;
|
|
1472
|
+
await service.instructionsLoaded(context.lifecycleId, instructions, context.reason);
|
|
1473
|
+
},
|
|
1457
1474
|
loadDynamicContext: (runtimeCwd = workspace.cwd()) => loadClaudeDynamicContext({
|
|
1458
1475
|
cwd: runtimeCwd,
|
|
1459
1476
|
...(memoryDirectory ? { memoryDirectory } : {}),
|
|
@@ -1624,6 +1641,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1624
1641
|
rewindFiles: (sessionId, userMessageId) => service.rewindFiles(sessionId, userMessageId),
|
|
1625
1642
|
rewindPoints: (sessionId) => service.rewindPoints(sessionId),
|
|
1626
1643
|
changeCwd: (sessionId, cwd) => service.changeCwd(sessionId, cwd),
|
|
1644
|
+
notify: (sessionId, message, notificationType, title) => service.notifyDetached(sessionId, message, notificationType, title),
|
|
1627
1645
|
recordCdUsage: (sessionId) => service.recordCdUsage(sessionId),
|
|
1628
1646
|
approveRecentlyDenied: (sessionId, display) => service.approveRecentlyDenied(sessionId, display),
|
|
1629
1647
|
retryRecentlyDenied: (sessionId, display, retrySignal) => service.retryRecentlyDenied(sessionId, display, retrySignal),
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { AssembledContext, ContextAssembler, ContextAssemblyOptions, ContextInvalidationOptions } from '../../core/context.js';
|
|
2
2
|
import { type ClaudeDynamicContextSections } from './dynamic-context.js';
|
|
3
|
-
import type { ClaudeContextResources, ClaudeConditionalRule } from './shared-resources.js';
|
|
3
|
+
import type { ClaudeContextResources, ClaudeConditionalRule, ClaudeTextResource } from './shared-resources.js';
|
|
4
4
|
export interface ClaudeContextAssemblerOptions {
|
|
5
5
|
loadResources(cwd?: string): Promise<ClaudeContextResources>;
|
|
6
|
+
onInstructionsLoaded?(resources: readonly ClaudeTextResource[], context: {
|
|
7
|
+
lifecycleId?: string;
|
|
8
|
+
cwd?: string;
|
|
9
|
+
reason: 'session_start' | 'compact' | 'resource_reload';
|
|
10
|
+
}): Promise<void>;
|
|
6
11
|
loadDynamicContext?(cwd?: string): Promise<ClaudeDynamicContextSections>;
|
|
7
12
|
loadMcpInstructions?(): Promise<readonly ClaudeMcpInstruction[]>;
|
|
8
13
|
loadSessionGuidance?(): Promise<string | undefined>;
|
|
@@ -171,6 +171,8 @@ ${sections.join('\n\n')}`,
|
|
|
171
171
|
if (options.reason === 'resource-reload' ||
|
|
172
172
|
options.reason === 'compact') {
|
|
173
173
|
delete snapshot.resources;
|
|
174
|
+
snapshot.resourceLoadReason =
|
|
175
|
+
options.reason === 'compact' ? 'compact' : 'resource_reload';
|
|
174
176
|
continue;
|
|
175
177
|
}
|
|
176
178
|
if (options.reason === 'tool-pool') {
|
|
@@ -199,7 +201,18 @@ ${sections.join('\n\n')}`,
|
|
|
199
201
|
if (!snapshot)
|
|
200
202
|
return this.options.loadResources(cwd);
|
|
201
203
|
if (!snapshot.resources) {
|
|
202
|
-
const
|
|
204
|
+
const reason = snapshot.resourceLoadReason ?? 'session_start';
|
|
205
|
+
delete snapshot.resourceLoadReason;
|
|
206
|
+
const pending = this.options
|
|
207
|
+
.loadResources(cwd)
|
|
208
|
+
.then(async (resources) => {
|
|
209
|
+
await this.options.onInstructionsLoaded?.(resources.instructions, {
|
|
210
|
+
lifecycleId: snapshot.lifecycleId,
|
|
211
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
212
|
+
reason,
|
|
213
|
+
});
|
|
214
|
+
return resources;
|
|
215
|
+
});
|
|
203
216
|
snapshot.resources = pending;
|
|
204
217
|
void pending.catch(() => {
|
|
205
218
|
if (snapshot.resources === pending)
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -319,6 +319,7 @@ export interface ToolExecutionResult {
|
|
|
319
319
|
}
|
|
320
320
|
export interface ToolExecutionContext {
|
|
321
321
|
cwd: string;
|
|
322
|
+
sessionId?: string;
|
|
322
323
|
messages?: readonly ModelMessage[];
|
|
323
324
|
signal?: AbortSignal;
|
|
324
325
|
toolResultDirectory?: string;
|
|
@@ -407,6 +408,7 @@ export type PermissionDecision = {
|
|
|
407
408
|
} | {
|
|
408
409
|
behavior: 'deny';
|
|
409
410
|
reason: string;
|
|
411
|
+
followUpUserMessages?: readonly string[];
|
|
410
412
|
source?: PermissionDecisionSource;
|
|
411
413
|
};
|
|
412
414
|
export interface PermissionResolver {
|
|
@@ -455,6 +457,7 @@ export interface AgentRuntimeOptions {
|
|
|
455
457
|
}
|
|
456
458
|
export interface AgentRunRequest {
|
|
457
459
|
messages: readonly ModelMessage[];
|
|
460
|
+
sessionId?: string;
|
|
458
461
|
/** Number of leading system messages that form the stable prompt prefix. */
|
|
459
462
|
stableSystemMessageCount?: number;
|
|
460
463
|
cwd?: string;
|
|
@@ -481,7 +484,7 @@ export interface AgentRunRequest {
|
|
|
481
484
|
maxModelTurns?: number;
|
|
482
485
|
betas?: readonly string[];
|
|
483
486
|
}
|
|
484
|
-
export interface AgentToolRecoveryRequest extends Pick<AgentRunRequest, 'approveTool' | 'cwd' | 'observer' | 'onPermissionUpdates' | 'permissionUpdates' | 'signal' | 'toolResultDirectory'> {
|
|
487
|
+
export interface AgentToolRecoveryRequest extends Pick<AgentRunRequest, 'approveTool' | 'cwd' | 'observer' | 'onPermissionUpdates' | 'permissionUpdates' | 'signal' | 'sessionId' | 'toolResultDirectory'> {
|
|
485
488
|
approveRecovery?: (call: ModelToolCall) => boolean | Promise<boolean>;
|
|
486
489
|
messages?: readonly ModelMessage[];
|
|
487
490
|
}
|
package/dist/core/runtime.js
CHANGED
|
@@ -845,6 +845,8 @@ export class AgentRuntime {
|
|
|
845
845
|
permissionPhase: 'request',
|
|
846
846
|
permissionUpdates: request.permissionUpdates ?? [],
|
|
847
847
|
};
|
|
848
|
+
if (request.sessionId)
|
|
849
|
+
context.sessionId = request.sessionId;
|
|
848
850
|
if (request.signal)
|
|
849
851
|
context.signal = request.signal;
|
|
850
852
|
if (request.toolResultDirectory) {
|
|
@@ -926,7 +928,13 @@ export class AgentRuntime {
|
|
|
926
928
|
: decision.behavior === 'ask'
|
|
927
929
|
? (decision.reason ?? 'Permission approval was not provided')
|
|
928
930
|
: 'Permission approval was not provided');
|
|
929
|
-
return {
|
|
931
|
+
return {
|
|
932
|
+
content: reason,
|
|
933
|
+
isError: true,
|
|
934
|
+
...(decision.behavior === 'deny' && decision.followUpUserMessages
|
|
935
|
+
? { followUpUserMessages: decision.followUpUserMessages }
|
|
936
|
+
: {}),
|
|
937
|
+
};
|
|
930
938
|
}
|
|
931
939
|
if (request.signal?.aborted) {
|
|
932
940
|
throw request.signal.reason ?? new AgentRunCancelledError();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type ClaudeFileChangeEvent = 'add' | 'change' | 'unlink';
|
|
2
|
+
interface DirectoryWatcher {
|
|
3
|
+
close(): void;
|
|
4
|
+
on(event: 'error', listener: (error: Error) => void): DirectoryWatcher;
|
|
5
|
+
unref?(): void;
|
|
6
|
+
}
|
|
7
|
+
type WatchDirectory = (directory: string, listener: (filename: string | null) => void) => DirectoryWatcher;
|
|
8
|
+
export interface ClaudeFileChangeWatcherOptions {
|
|
9
|
+
cwd: string;
|
|
10
|
+
staticPaths: readonly string[];
|
|
11
|
+
onFileChanged(filePath: string, event: ClaudeFileChangeEvent, signal: AbortSignal): Promise<readonly string[] | undefined>;
|
|
12
|
+
warn(message: string): void;
|
|
13
|
+
debounceMs?: number;
|
|
14
|
+
pathExists?(path: string): boolean;
|
|
15
|
+
watchDirectory?: WatchDirectory;
|
|
16
|
+
}
|
|
17
|
+
export declare class ClaudeFileChangeWatcher {
|
|
18
|
+
private readonly options;
|
|
19
|
+
private cwd;
|
|
20
|
+
private staticPaths;
|
|
21
|
+
private dynamicPaths;
|
|
22
|
+
private readonly watchers;
|
|
23
|
+
private readonly knownExistence;
|
|
24
|
+
private readonly pending;
|
|
25
|
+
private readonly inFlight;
|
|
26
|
+
private readonly debounceMs;
|
|
27
|
+
private readonly pathExists;
|
|
28
|
+
private readonly watchDirectory;
|
|
29
|
+
private generation;
|
|
30
|
+
private closed;
|
|
31
|
+
constructor(options: ClaudeFileChangeWatcherOptions);
|
|
32
|
+
updateForCwd(cwd: string, staticPaths: readonly string[], dynamicPaths: readonly string[]): void;
|
|
33
|
+
updateDynamicPaths(paths: readonly string[]): void;
|
|
34
|
+
close(timeoutMs?: number): Promise<void>;
|
|
35
|
+
private validDynamicPaths;
|
|
36
|
+
private targetPaths;
|
|
37
|
+
private rebuild;
|
|
38
|
+
private schedule;
|
|
39
|
+
private dispatch;
|
|
40
|
+
private clearResources;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=claude-file-change-watcher.d.ts.map
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { existsSync, watch } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
3
|
+
function defaultWatchDirectory(directory, listener) {
|
|
4
|
+
return watch(directory, (_event, filename) => {
|
|
5
|
+
listener(filename);
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export class ClaudeFileChangeWatcher {
|
|
9
|
+
options;
|
|
10
|
+
cwd;
|
|
11
|
+
staticPaths;
|
|
12
|
+
dynamicPaths = [];
|
|
13
|
+
watchers = new Map();
|
|
14
|
+
knownExistence = new Map();
|
|
15
|
+
pending = new Map();
|
|
16
|
+
inFlight = new Map();
|
|
17
|
+
debounceMs;
|
|
18
|
+
pathExists;
|
|
19
|
+
watchDirectory;
|
|
20
|
+
generation = 0;
|
|
21
|
+
closed = false;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.cwd = options.cwd;
|
|
25
|
+
this.staticPaths = options.staticPaths;
|
|
26
|
+
this.debounceMs = options.debounceMs ?? 500;
|
|
27
|
+
this.pathExists = options.pathExists ?? existsSync;
|
|
28
|
+
this.watchDirectory = options.watchDirectory ?? defaultWatchDirectory;
|
|
29
|
+
this.rebuild();
|
|
30
|
+
}
|
|
31
|
+
updateForCwd(cwd, staticPaths, dynamicPaths) {
|
|
32
|
+
if (this.closed)
|
|
33
|
+
return;
|
|
34
|
+
this.cwd = cwd;
|
|
35
|
+
this.staticPaths = staticPaths;
|
|
36
|
+
this.dynamicPaths = this.validDynamicPaths(dynamicPaths);
|
|
37
|
+
this.rebuild();
|
|
38
|
+
}
|
|
39
|
+
updateDynamicPaths(paths) {
|
|
40
|
+
if (this.closed)
|
|
41
|
+
return;
|
|
42
|
+
const next = this.validDynamicPaths(paths);
|
|
43
|
+
if (next.length === this.dynamicPaths.length &&
|
|
44
|
+
next.every((path, index) => path === this.dynamicPaths[index])) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.dynamicPaths = next;
|
|
48
|
+
this.rebuild();
|
|
49
|
+
}
|
|
50
|
+
async close(timeoutMs = 5_000) {
|
|
51
|
+
if (this.closed)
|
|
52
|
+
return;
|
|
53
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
54
|
+
throw new Error('FileChanged watcher close timeout must be non-negative');
|
|
55
|
+
}
|
|
56
|
+
this.closed = true;
|
|
57
|
+
this.generation += 1;
|
|
58
|
+
this.clearResources();
|
|
59
|
+
const pending = [...this.inFlight.keys()];
|
|
60
|
+
if (pending.length === 0)
|
|
61
|
+
return;
|
|
62
|
+
let timer;
|
|
63
|
+
try {
|
|
64
|
+
if ((await Promise.race([
|
|
65
|
+
Promise.allSettled(pending).then(() => 'settled'),
|
|
66
|
+
new Promise((resolveTimeout) => {
|
|
67
|
+
timer = setTimeout(() => resolveTimeout('timeout'), timeoutMs);
|
|
68
|
+
}),
|
|
69
|
+
])) === 'timeout') {
|
|
70
|
+
for (const controller of this.inFlight.values())
|
|
71
|
+
controller.abort();
|
|
72
|
+
this.inFlight.clear();
|
|
73
|
+
await Promise.resolve();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
if (timer)
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
validDynamicPaths(paths) {
|
|
82
|
+
const valid = [];
|
|
83
|
+
for (const path of paths) {
|
|
84
|
+
if (!isAbsolute(path)) {
|
|
85
|
+
this.options.warn(`FileChanged hook ignored non-absolute watch path: ${path}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
valid.push(resolve(path));
|
|
89
|
+
}
|
|
90
|
+
return [...new Set(valid)];
|
|
91
|
+
}
|
|
92
|
+
targetPaths() {
|
|
93
|
+
return [
|
|
94
|
+
...new Set([
|
|
95
|
+
...this.staticPaths.map((path) => isAbsolute(path) ? resolve(path) : resolve(this.cwd, path)),
|
|
96
|
+
...this.dynamicPaths,
|
|
97
|
+
]),
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
rebuild() {
|
|
101
|
+
this.generation += 1;
|
|
102
|
+
this.clearResources();
|
|
103
|
+
const targets = this.targetPaths();
|
|
104
|
+
const byDirectory = new Map();
|
|
105
|
+
for (const target of targets) {
|
|
106
|
+
this.knownExistence.set(target, this.pathExists(target));
|
|
107
|
+
const directory = dirname(target);
|
|
108
|
+
const names = byDirectory.get(directory) ?? new Set();
|
|
109
|
+
names.add(basename(target));
|
|
110
|
+
byDirectory.set(directory, names);
|
|
111
|
+
}
|
|
112
|
+
for (const [directory, names] of byDirectory) {
|
|
113
|
+
try {
|
|
114
|
+
const watcher = this.watchDirectory(directory, (filename) => {
|
|
115
|
+
if (filename === null) {
|
|
116
|
+
for (const name of names)
|
|
117
|
+
this.schedule(resolve(directory, name));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (names.has(filename))
|
|
121
|
+
this.schedule(resolve(directory, filename));
|
|
122
|
+
});
|
|
123
|
+
watcher.on('error', (error) => {
|
|
124
|
+
this.options.warn(`FileChanged watcher failed for ${directory}: ${error.message}`);
|
|
125
|
+
});
|
|
126
|
+
watcher.unref?.();
|
|
127
|
+
this.watchers.set(directory, watcher);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
this.options.warn(`FileChanged watcher could not watch ${directory}: ${error instanceof Error ? error.message : String(error)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
schedule(path) {
|
|
135
|
+
const previous = this.pending.get(path);
|
|
136
|
+
if (previous)
|
|
137
|
+
clearTimeout(previous);
|
|
138
|
+
const generation = this.generation;
|
|
139
|
+
const timer = setTimeout(() => {
|
|
140
|
+
this.pending.delete(path);
|
|
141
|
+
if (this.closed || generation !== this.generation)
|
|
142
|
+
return;
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const dispatch = this.dispatch(path, generation, controller.signal);
|
|
145
|
+
this.inFlight.set(dispatch, controller);
|
|
146
|
+
void dispatch.finally(() => this.inFlight.delete(dispatch));
|
|
147
|
+
}, this.debounceMs);
|
|
148
|
+
timer.unref?.();
|
|
149
|
+
this.pending.set(path, timer);
|
|
150
|
+
}
|
|
151
|
+
async dispatch(path, generation, signal) {
|
|
152
|
+
const existed = this.knownExistence.get(path) ?? false;
|
|
153
|
+
const exists = this.pathExists(path);
|
|
154
|
+
this.knownExistence.set(path, exists);
|
|
155
|
+
if (!existed && !exists)
|
|
156
|
+
return;
|
|
157
|
+
const event = !existed
|
|
158
|
+
? 'add'
|
|
159
|
+
: !exists
|
|
160
|
+
? 'unlink'
|
|
161
|
+
: 'change';
|
|
162
|
+
try {
|
|
163
|
+
const watchPaths = await this.options.onFileChanged(path, event, signal);
|
|
164
|
+
if (!this.closed &&
|
|
165
|
+
generation === this.generation &&
|
|
166
|
+
watchPaths &&
|
|
167
|
+
watchPaths.length > 0) {
|
|
168
|
+
this.updateDynamicPaths(watchPaths);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
this.options.warn(`FileChanged hook failed for ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
clearResources() {
|
|
176
|
+
for (const timer of this.pending.values())
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
this.pending.clear();
|
|
179
|
+
for (const watcher of this.watchers.values())
|
|
180
|
+
watcher.close();
|
|
181
|
+
this.watchers.clear();
|
|
182
|
+
this.knownExistence.clear();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=claude-file-change-watcher.js.map
|
|
@@ -12,6 +12,7 @@ export interface ClaudeHookToolCoordinatorOptions {
|
|
|
12
12
|
hooks: ClaudeHookRunner;
|
|
13
13
|
session: ClaudeHookSessionInput;
|
|
14
14
|
recordOutcome(outcome: ClaudeHookOutcome, deferUntilApproval?: boolean): Promise<void>;
|
|
15
|
+
warn?(message: string): void;
|
|
15
16
|
deferPreToolUseOutcome?(call: ModelToolCall): boolean;
|
|
16
17
|
}
|
|
17
18
|
export declare class ClaudeHookToolCoordinator implements ToolRegistry, PermissionResolver {
|
|
@@ -25,6 +26,7 @@ export declare class ClaudeHookToolCoordinator implements ToolRegistry, Permissi
|
|
|
25
26
|
};
|
|
26
27
|
prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
|
|
27
28
|
resolve(call: ModelToolCall, context?: PermissionResolutionContext): Promise<PermissionDecision>;
|
|
29
|
+
private withPermissionDeniedHook;
|
|
28
30
|
execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
|
|
29
31
|
}
|
|
30
32
|
//# sourceMappingURL=claude-hook-tools.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { permissionDecisionSource } from '../core/runtime.js';
|
|
1
2
|
export class ClaudeHookToolCoordinator {
|
|
2
3
|
options;
|
|
3
4
|
prepared = new Map();
|
|
@@ -57,11 +58,13 @@ export class ClaudeHookToolCoordinator {
|
|
|
57
58
|
}
|
|
58
59
|
async resolve(call, context) {
|
|
59
60
|
const prepared = this.prepared.get(call.id);
|
|
60
|
-
if (prepared?.permissionDecision)
|
|
61
|
-
return prepared.permissionDecision;
|
|
61
|
+
if (prepared?.permissionDecision) {
|
|
62
|
+
return this.withPermissionDeniedHook(call, prepared.permissionDecision, prepared.signal);
|
|
63
|
+
}
|
|
62
64
|
const decision = await this.options.permissions.resolve(call, context);
|
|
63
|
-
if (decision.behavior !== 'ask')
|
|
64
|
-
return decision;
|
|
65
|
+
if (decision.behavior !== 'ask') {
|
|
66
|
+
return this.withPermissionDeniedHook(call, decision, prepared?.signal);
|
|
67
|
+
}
|
|
65
68
|
const outcome = await this.options.hooks.run({
|
|
66
69
|
...this.options.session,
|
|
67
70
|
hook_event_name: 'PermissionRequest',
|
|
@@ -72,17 +75,53 @@ export class ClaudeHookToolCoordinator {
|
|
|
72
75
|
}, call.name, prepared?.signal);
|
|
73
76
|
await this.options.recordOutcome(outcome);
|
|
74
77
|
if (outcome.blockedReason || outcome.permissionDecision === 'deny') {
|
|
75
|
-
return {
|
|
78
|
+
return this.withPermissionDeniedHook(call, {
|
|
76
79
|
behavior: 'deny',
|
|
77
80
|
reason: outcome.blockedReason ??
|
|
78
81
|
outcome.permissionDecisionReason ??
|
|
79
82
|
`PermissionRequest:${call.name} hook denied tool`,
|
|
80
|
-
};
|
|
83
|
+
}, prepared?.signal);
|
|
81
84
|
}
|
|
82
85
|
if (outcome.permissionDecision === 'allow')
|
|
83
86
|
return { behavior: 'allow' };
|
|
84
87
|
return decision;
|
|
85
88
|
}
|
|
89
|
+
async withPermissionDeniedHook(call, decision, signal) {
|
|
90
|
+
const decisionSource = permissionDecisionSource(decision);
|
|
91
|
+
if (decision.behavior !== 'deny' || decisionSource !== 'auto-classifier') {
|
|
92
|
+
return decision;
|
|
93
|
+
}
|
|
94
|
+
const prepared = this.prepared.get(call.id);
|
|
95
|
+
try {
|
|
96
|
+
const outcome = await this.options.hooks.run({
|
|
97
|
+
...this.options.session,
|
|
98
|
+
hook_event_name: 'PermissionDenied',
|
|
99
|
+
tool_name: call.name,
|
|
100
|
+
tool_input: prepared?.hookInput ?? call.input,
|
|
101
|
+
tool_use_id: call.id,
|
|
102
|
+
reason: decision.reason,
|
|
103
|
+
}, call.name, signal);
|
|
104
|
+
await this.options.recordOutcome(outcome);
|
|
105
|
+
if (outcome.retry) {
|
|
106
|
+
return {
|
|
107
|
+
...decision,
|
|
108
|
+
followUpUserMessages: [
|
|
109
|
+
...(decision.followUpUserMessages ?? []),
|
|
110
|
+
'The PermissionDenied hook indicated this command is now approved. You may retry it if you would like.',
|
|
111
|
+
],
|
|
112
|
+
source: decisionSource,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (signal?.aborted ||
|
|
118
|
+
(error instanceof Error && error.name === 'AbortError')) {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
this.options.warn?.(`PermissionDenied:${call.name} hook failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
122
|
+
}
|
|
123
|
+
return decision;
|
|
124
|
+
}
|
|
86
125
|
async execute(call, context) {
|
|
87
126
|
const prepared = this.prepared.get(call.id);
|
|
88
127
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ClaudeJsonResource } from '../compatibility/claude/shared-resources.js';
|
|
2
2
|
import type { PermissionBehavior } from '../core/runtime.js';
|
|
3
|
-
|
|
3
|
+
import type { ClaudeSessionEnvironment } from './claude-session-environment.js';
|
|
4
|
+
export type ClaudeHookEventName = 'Setup' | 'SessionStart' | 'SubagentStart' | 'UserPromptSubmit' | 'PreToolUse' | 'PermissionRequest' | 'PermissionDenied' | 'PostToolUse' | 'PostToolUseFailure' | 'Notification' | 'Stop' | 'StopFailure' | 'SubagentStop' | 'PreCompact' | 'PostCompact' | 'TaskCreated' | 'TaskCompleted' | 'ConfigChange' | 'InstructionsLoaded' | 'WorktreeCreate' | 'WorktreeRemove' | 'CwdChanged' | 'FileChanged' | 'SessionEnd';
|
|
4
5
|
export interface ClaudeHookInput extends Record<string, unknown> {
|
|
5
6
|
session_id: string;
|
|
6
7
|
transcript_path: string;
|
|
@@ -53,6 +54,9 @@ export interface ClaudeHookOutcome {
|
|
|
53
54
|
updatedInput?: Record<string, unknown>;
|
|
54
55
|
permissionDecision?: PermissionBehavior;
|
|
55
56
|
permissionDecisionReason?: string;
|
|
57
|
+
retry?: boolean;
|
|
58
|
+
watchPaths?: readonly string[];
|
|
59
|
+
systemMessages?: readonly string[];
|
|
56
60
|
blockedReason?: string;
|
|
57
61
|
}
|
|
58
62
|
interface ProcessResult {
|
|
@@ -71,6 +75,7 @@ export interface ClaudeHookRunnerOptions {
|
|
|
71
75
|
maxTimeoutMs?: number;
|
|
72
76
|
executeCommand?: ClaudeHookCommandExecutor;
|
|
73
77
|
onEvent?: (event: ClaudeHookStreamEvent) => void;
|
|
78
|
+
sessionEnvironment?: ClaudeSessionEnvironment;
|
|
74
79
|
}
|
|
75
80
|
export declare const HOOK_EVENTS: readonly ClaudeHookEventName[];
|
|
76
81
|
export declare function validateClaudeHooks(settings: readonly ClaudeJsonResource[], maxTimeoutMs?: number): void;
|
|
@@ -81,9 +86,16 @@ export declare class ClaudeHookRunner {
|
|
|
81
86
|
private readonly executeCommand;
|
|
82
87
|
private readonly onEvent;
|
|
83
88
|
private readonly cwd;
|
|
84
|
-
|
|
89
|
+
private readonly sessionEnvironment;
|
|
90
|
+
private readonly asynchronousHooks;
|
|
91
|
+
constructor(options: ClaudeHookRunnerOptions, asynchronousHooks?: Map<Promise<void>, AbortController>);
|
|
85
92
|
withAdditionalSettings(settings: readonly ClaudeJsonResource[]): ClaudeHookRunner;
|
|
93
|
+
fileChangedWatchPaths(cwd?: string): readonly string[];
|
|
86
94
|
run(input: ClaudeHookInput, matcherValue?: string, signal?: AbortSignal): Promise<ClaudeHookOutcome>;
|
|
95
|
+
drainAsync(timeoutMs: number): Promise<void>;
|
|
96
|
+
clearCwdEnvironment(sessionId: string): Promise<void>;
|
|
97
|
+
private executionEnvironment;
|
|
98
|
+
private invalidateSessionEnvironment;
|
|
87
99
|
private runCommand;
|
|
88
100
|
}
|
|
89
101
|
export {};
|