sessionlog 0.0.1
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/LICENSE +21 -0
- package/README.md +388 -0
- package/dist/agent/agents/claude-code.d.ts +76 -0
- package/dist/agent/agents/claude-code.d.ts.map +1 -0
- package/dist/agent/agents/claude-code.js +769 -0
- package/dist/agent/agents/claude-code.js.map +1 -0
- package/dist/agent/agents/cursor.d.ts +35 -0
- package/dist/agent/agents/cursor.d.ts.map +1 -0
- package/dist/agent/agents/cursor.js +294 -0
- package/dist/agent/agents/cursor.js.map +1 -0
- package/dist/agent/agents/gemini-cli.d.ts +62 -0
- package/dist/agent/agents/gemini-cli.d.ts.map +1 -0
- package/dist/agent/agents/gemini-cli.js +474 -0
- package/dist/agent/agents/gemini-cli.js.map +1 -0
- package/dist/agent/agents/opencode.d.ts +100 -0
- package/dist/agent/agents/opencode.d.ts.map +1 -0
- package/dist/agent/agents/opencode.js +423 -0
- package/dist/agent/agents/opencode.js.map +1 -0
- package/dist/agent/registry.d.ts +54 -0
- package/dist/agent/registry.d.ts.map +1 -0
- package/dist/agent/registry.js +123 -0
- package/dist/agent/registry.js.map +1 -0
- package/dist/agent/session-types.d.ts +45 -0
- package/dist/agent/session-types.d.ts.map +1 -0
- package/dist/agent/session-types.js +48 -0
- package/dist/agent/session-types.js.map +1 -0
- package/dist/agent/types.d.ts +126 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +40 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +425 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/clean.d.ts +30 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +98 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/disable.d.ts +23 -0
- package/dist/commands/disable.d.ts.map +1 -0
- package/dist/commands/disable.js +57 -0
- package/dist/commands/disable.js.map +1 -0
- package/dist/commands/doctor.d.ts +43 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +97 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/enable.d.ts +37 -0
- package/dist/commands/enable.d.ts.map +1 -0
- package/dist/commands/enable.js +133 -0
- package/dist/commands/enable.js.map +1 -0
- package/dist/commands/explain.d.ts +68 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +182 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/reset.d.ts +23 -0
- package/dist/commands/reset.d.ts.map +1 -0
- package/dist/commands/reset.js +68 -0
- package/dist/commands/reset.js.map +1 -0
- package/dist/commands/resume.d.ts +42 -0
- package/dist/commands/resume.d.ts.map +1 -0
- package/dist/commands/resume.js +133 -0
- package/dist/commands/resume.js.map +1 -0
- package/dist/commands/rewind.d.ts +34 -0
- package/dist/commands/rewind.d.ts.map +1 -0
- package/dist/commands/rewind.js +155 -0
- package/dist/commands/rewind.js.map +1 -0
- package/dist/commands/status.d.ts +51 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +112 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +127 -0
- package/dist/config.js.map +1 -0
- package/dist/git-operations.d.ts +191 -0
- package/dist/git-operations.d.ts.map +1 -0
- package/dist/git-operations.js +462 -0
- package/dist/git-operations.js.map +1 -0
- package/dist/hooks/git-hooks.d.ts +22 -0
- package/dist/hooks/git-hooks.d.ts.map +1 -0
- package/dist/hooks/git-hooks.js +139 -0
- package/dist/hooks/git-hooks.js.map +1 -0
- package/dist/hooks/lifecycle.d.ts +21 -0
- package/dist/hooks/lifecycle.d.ts.map +1 -0
- package/dist/hooks/lifecycle.js +179 -0
- package/dist/hooks/lifecycle.js.map +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +166 -0
- package/dist/index.js.map +1 -0
- package/dist/security/redaction.d.ts +35 -0
- package/dist/security/redaction.d.ts.map +1 -0
- package/dist/security/redaction.js +239 -0
- package/dist/security/redaction.js.map +1 -0
- package/dist/session/state-machine.d.ts +90 -0
- package/dist/session/state-machine.d.ts.map +1 -0
- package/dist/session/state-machine.js +345 -0
- package/dist/session/state-machine.js.map +1 -0
- package/dist/store/checkpoint-store.d.ts +59 -0
- package/dist/store/checkpoint-store.d.ts.map +1 -0
- package/dist/store/checkpoint-store.js +321 -0
- package/dist/store/checkpoint-store.js.map +1 -0
- package/dist/store/native-store.d.ts +14 -0
- package/dist/store/native-store.d.ts.map +1 -0
- package/dist/store/native-store.js +159 -0
- package/dist/store/native-store.js.map +1 -0
- package/dist/store/provider-types.d.ts +78 -0
- package/dist/store/provider-types.d.ts.map +1 -0
- package/dist/store/provider-types.js +12 -0
- package/dist/store/provider-types.js.map +1 -0
- package/dist/store/session-store.d.ts +36 -0
- package/dist/store/session-store.d.ts.map +1 -0
- package/dist/store/session-store.js +193 -0
- package/dist/store/session-store.js.map +1 -0
- package/dist/strategy/attribution.d.ts +39 -0
- package/dist/strategy/attribution.d.ts.map +1 -0
- package/dist/strategy/attribution.js +225 -0
- package/dist/strategy/attribution.js.map +1 -0
- package/dist/strategy/common.d.ts +57 -0
- package/dist/strategy/common.d.ts.map +1 -0
- package/dist/strategy/common.js +156 -0
- package/dist/strategy/common.js.map +1 -0
- package/dist/strategy/content-overlap.d.ts +33 -0
- package/dist/strategy/content-overlap.d.ts.map +1 -0
- package/dist/strategy/content-overlap.js +176 -0
- package/dist/strategy/content-overlap.js.map +1 -0
- package/dist/strategy/manual-commit.d.ts +36 -0
- package/dist/strategy/manual-commit.d.ts.map +1 -0
- package/dist/strategy/manual-commit.js +717 -0
- package/dist/strategy/manual-commit.js.map +1 -0
- package/dist/strategy/types.d.ts +163 -0
- package/dist/strategy/types.d.ts.map +1 -0
- package/dist/strategy/types.js +48 -0
- package/dist/strategy/types.js.map +1 -0
- package/dist/summarize/claude-generator.d.ts +25 -0
- package/dist/summarize/claude-generator.d.ts.map +1 -0
- package/dist/summarize/claude-generator.js +87 -0
- package/dist/summarize/claude-generator.js.map +1 -0
- package/dist/summarize/summarize.d.ts +52 -0
- package/dist/summarize/summarize.d.ts.map +1 -0
- package/dist/summarize/summarize.js +335 -0
- package/dist/summarize/summarize.js.map +1 -0
- package/dist/types.d.ts +293 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +94 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/chunk-files.d.ts +25 -0
- package/dist/utils/chunk-files.d.ts.map +1 -0
- package/dist/utils/chunk-files.js +47 -0
- package/dist/utils/chunk-files.js.map +1 -0
- package/dist/utils/commit-message.d.ts +11 -0
- package/dist/utils/commit-message.d.ts.map +1 -0
- package/dist/utils/commit-message.js +54 -0
- package/dist/utils/commit-message.js.map +1 -0
- package/dist/utils/detect-agent.d.ts +19 -0
- package/dist/utils/detect-agent.d.ts.map +1 -0
- package/dist/utils/detect-agent.js +34 -0
- package/dist/utils/detect-agent.js.map +1 -0
- package/dist/utils/hook-managers.d.ts +24 -0
- package/dist/utils/hook-managers.d.ts.map +1 -0
- package/dist/utils/hook-managers.js +96 -0
- package/dist/utils/hook-managers.js.map +1 -0
- package/dist/utils/ide-tags.d.ts +12 -0
- package/dist/utils/ide-tags.d.ts.map +1 -0
- package/dist/utils/ide-tags.js +30 -0
- package/dist/utils/ide-tags.js.map +1 -0
- package/dist/utils/paths.d.ts +32 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +55 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/preview-rewind.d.ts +23 -0
- package/dist/utils/preview-rewind.d.ts.map +1 -0
- package/dist/utils/preview-rewind.js +63 -0
- package/dist/utils/preview-rewind.js.map +1 -0
- package/dist/utils/rewind-conflict.d.ts +52 -0
- package/dist/utils/rewind-conflict.d.ts.map +1 -0
- package/dist/utils/rewind-conflict.js +79 -0
- package/dist/utils/rewind-conflict.js.map +1 -0
- package/dist/utils/shadow-branch.d.ts +44 -0
- package/dist/utils/shadow-branch.d.ts.map +1 -0
- package/dist/utils/shadow-branch.js +93 -0
- package/dist/utils/shadow-branch.js.map +1 -0
- package/dist/utils/string-utils.d.ts +24 -0
- package/dist/utils/string-utils.d.ts.map +1 -0
- package/dist/utils/string-utils.js +47 -0
- package/dist/utils/string-utils.js.map +1 -0
- package/dist/utils/todo-extract.d.ts +52 -0
- package/dist/utils/todo-extract.d.ts.map +1 -0
- package/dist/utils/todo-extract.js +167 -0
- package/dist/utils/todo-extract.js.map +1 -0
- package/dist/utils/trailers.d.ts +36 -0
- package/dist/utils/trailers.d.ts.map +1 -0
- package/dist/utils/trailers.js +148 -0
- package/dist/utils/trailers.js.map +1 -0
- package/dist/utils/transcript-parse.d.ts +57 -0
- package/dist/utils/transcript-parse.d.ts.map +1 -0
- package/dist/utils/transcript-parse.js +126 -0
- package/dist/utils/transcript-parse.js.map +1 -0
- package/dist/utils/transcript-timestamp.d.ts +22 -0
- package/dist/utils/transcript-timestamp.d.ts.map +1 -0
- package/dist/utils/transcript-timestamp.js +56 -0
- package/dist/utils/transcript-timestamp.js.map +1 -0
- package/dist/utils/tree-ops.d.ts +47 -0
- package/dist/utils/tree-ops.d.ts.map +1 -0
- package/dist/utils/tree-ops.js +145 -0
- package/dist/utils/tree-ops.js.map +1 -0
- package/dist/utils/tty.d.ts +25 -0
- package/dist/utils/tty.d.ts.map +1 -0
- package/dist/utils/tty.js +70 -0
- package/dist/utils/tty.js.map +1 -0
- package/dist/utils/validation.d.ts +31 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +59 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils/worktree.d.ts +16 -0
- package/dist/utils/worktree.d.ts.map +1 -0
- package/dist/utils/worktree.js +50 -0
- package/dist/utils/worktree.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Agent
|
|
3
|
+
*
|
|
4
|
+
* Implementation of the Entire agent interface for Anthropic's Claude Code.
|
|
5
|
+
* Handles JSONL transcript format, Claude-specific hook installation,
|
|
6
|
+
* and session lifecycle management.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import * as crypto from 'node:crypto';
|
|
12
|
+
import { AGENT_NAMES, AGENT_TYPES, EventType, emptyTokenUsage, } from '../../types.js';
|
|
13
|
+
import { registerAgent } from '../registry.js';
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Constants
|
|
16
|
+
// ============================================================================
|
|
17
|
+
const CLAUDE_DIR = '.claude';
|
|
18
|
+
const CLAUDE_SETTINGS_FILE = '.claude/settings.json';
|
|
19
|
+
const HOOK_NAMES = [
|
|
20
|
+
'session-start',
|
|
21
|
+
'session-end',
|
|
22
|
+
'stop',
|
|
23
|
+
'user-prompt-submit',
|
|
24
|
+
'pre-task',
|
|
25
|
+
'post-task',
|
|
26
|
+
'post-todo',
|
|
27
|
+
];
|
|
28
|
+
/** Tools that modify files (detected in transcript) */
|
|
29
|
+
const FILE_MODIFICATION_TOOLS = new Set([
|
|
30
|
+
'Write',
|
|
31
|
+
'Edit',
|
|
32
|
+
'NotebookEdit',
|
|
33
|
+
'mcp__acp__Write',
|
|
34
|
+
'mcp__acp__Edit',
|
|
35
|
+
]);
|
|
36
|
+
/** Deny rule to prevent agents from reading metadata */
|
|
37
|
+
const METADATA_DENY_RULE = 'Read(./.entire/metadata/**)';
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Claude Code Agent Implementation
|
|
40
|
+
// ============================================================================
|
|
41
|
+
class ClaudeCodeAgent {
|
|
42
|
+
name = AGENT_NAMES.CLAUDE_CODE;
|
|
43
|
+
type = AGENT_TYPES.CLAUDE_CODE;
|
|
44
|
+
description = 'Anthropic Claude Code CLI';
|
|
45
|
+
isPreview = false;
|
|
46
|
+
protectedDirs = [CLAUDE_DIR];
|
|
47
|
+
async detectPresence(cwd) {
|
|
48
|
+
const repoRoot = cwd ?? process.cwd();
|
|
49
|
+
const claudeDir = path.join(repoRoot, CLAUDE_DIR);
|
|
50
|
+
try {
|
|
51
|
+
const stat = await fs.promises.stat(claudeDir);
|
|
52
|
+
return stat.isDirectory();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async getSessionDir(repoPath) {
|
|
59
|
+
// Claude Code stores sessions in ~/.claude/projects/<sanitized-path>/
|
|
60
|
+
const sanitized = sanitizePathForClaude(repoPath);
|
|
61
|
+
return path.join(os.homedir(), '.claude', 'projects', sanitized);
|
|
62
|
+
}
|
|
63
|
+
getSessionID(input) {
|
|
64
|
+
return input.sessionID;
|
|
65
|
+
}
|
|
66
|
+
resolveSessionFile(sessionDir, agentSessionID) {
|
|
67
|
+
return path.join(sessionDir, `${agentSessionID}.jsonl`);
|
|
68
|
+
}
|
|
69
|
+
async readTranscript(sessionRef) {
|
|
70
|
+
return fs.promises.readFile(sessionRef);
|
|
71
|
+
}
|
|
72
|
+
formatResumeCommand(sessionID) {
|
|
73
|
+
return `claude --resume ${sessionID}`;
|
|
74
|
+
}
|
|
75
|
+
// ===========================================================================
|
|
76
|
+
// HookSupport
|
|
77
|
+
// ===========================================================================
|
|
78
|
+
hookNames() {
|
|
79
|
+
return [...HOOK_NAMES];
|
|
80
|
+
}
|
|
81
|
+
parseHookEvent(hookName, stdin) {
|
|
82
|
+
try {
|
|
83
|
+
const data = JSON.parse(stdin);
|
|
84
|
+
switch (hookName) {
|
|
85
|
+
case 'session-start':
|
|
86
|
+
return {
|
|
87
|
+
type: EventType.SessionStart,
|
|
88
|
+
sessionID: String(data.session_id ?? data.sessionID ?? ''),
|
|
89
|
+
sessionRef: String(data.transcript_path ?? data.transcriptPath ?? ''),
|
|
90
|
+
timestamp: new Date(),
|
|
91
|
+
};
|
|
92
|
+
case 'user-prompt-submit':
|
|
93
|
+
return {
|
|
94
|
+
type: EventType.TurnStart,
|
|
95
|
+
sessionID: String(data.session_id ?? data.sessionID ?? ''),
|
|
96
|
+
sessionRef: String(data.transcript_path ?? data.transcriptPath ?? ''),
|
|
97
|
+
prompt: String(data.prompt ?? ''),
|
|
98
|
+
timestamp: new Date(),
|
|
99
|
+
};
|
|
100
|
+
case 'stop':
|
|
101
|
+
return {
|
|
102
|
+
type: EventType.TurnEnd,
|
|
103
|
+
sessionID: String(data.session_id ?? data.sessionID ?? ''),
|
|
104
|
+
sessionRef: String(data.transcript_path ?? data.transcriptPath ?? ''),
|
|
105
|
+
timestamp: new Date(),
|
|
106
|
+
};
|
|
107
|
+
case 'session-end':
|
|
108
|
+
return {
|
|
109
|
+
type: EventType.SessionEnd,
|
|
110
|
+
sessionID: String(data.session_id ?? data.sessionID ?? ''),
|
|
111
|
+
sessionRef: String(data.transcript_path ?? data.transcriptPath ?? ''),
|
|
112
|
+
timestamp: new Date(),
|
|
113
|
+
};
|
|
114
|
+
case 'pre-task':
|
|
115
|
+
return {
|
|
116
|
+
type: EventType.SubagentStart,
|
|
117
|
+
sessionID: String(data.session_id ?? data.sessionID ?? ''),
|
|
118
|
+
sessionRef: String(data.transcript_path ?? data.transcriptPath ?? ''),
|
|
119
|
+
toolUseID: String(data.tool_use_id ?? data.toolUseID ?? ''),
|
|
120
|
+
toolInput: data.tool_input ?? data.toolInput,
|
|
121
|
+
timestamp: new Date(),
|
|
122
|
+
};
|
|
123
|
+
case 'post-task':
|
|
124
|
+
return {
|
|
125
|
+
type: EventType.SubagentEnd,
|
|
126
|
+
sessionID: String(data.session_id ?? data.sessionID ?? ''),
|
|
127
|
+
sessionRef: String(data.transcript_path ?? data.transcriptPath ?? ''),
|
|
128
|
+
toolUseID: String(data.tool_use_id ?? data.toolUseID ?? ''),
|
|
129
|
+
subagentID: data.tool_response?.agentId,
|
|
130
|
+
timestamp: new Date(),
|
|
131
|
+
};
|
|
132
|
+
case 'post-todo':
|
|
133
|
+
return {
|
|
134
|
+
type: EventType.Compaction,
|
|
135
|
+
sessionID: String(data.session_id ?? data.sessionID ?? ''),
|
|
136
|
+
sessionRef: String(data.transcript_path ?? data.transcriptPath ?? ''),
|
|
137
|
+
timestamp: new Date(),
|
|
138
|
+
};
|
|
139
|
+
default:
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async installHooks(repoPath, force = false) {
|
|
148
|
+
const settingsPath = path.join(repoPath, CLAUDE_SETTINGS_FILE);
|
|
149
|
+
let settings = {};
|
|
150
|
+
// Read existing settings
|
|
151
|
+
try {
|
|
152
|
+
const content = await fs.promises.readFile(settingsPath, 'utf-8');
|
|
153
|
+
settings = JSON.parse(content);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// No existing settings
|
|
157
|
+
}
|
|
158
|
+
if (!settings.hooks)
|
|
159
|
+
settings.hooks = {};
|
|
160
|
+
let installed = 0;
|
|
161
|
+
// Install lifecycle hooks
|
|
162
|
+
const hookMappings = [
|
|
163
|
+
{ settingsKey: 'SessionStart', hookName: 'session-start' },
|
|
164
|
+
{ settingsKey: 'SessionEnd', hookName: 'session-end' },
|
|
165
|
+
{ settingsKey: 'UserPromptSubmit', hookName: 'user-prompt-submit' },
|
|
166
|
+
{ settingsKey: 'Stop', hookName: 'stop' },
|
|
167
|
+
];
|
|
168
|
+
// Task hooks (pre/post tool use for Task tool)
|
|
169
|
+
const taskHookMappings = [
|
|
170
|
+
{ settingsKey: 'PreToolUse', hookName: 'pre-task', matcher: 'Task' },
|
|
171
|
+
{ settingsKey: 'PostToolUse', hookName: 'post-task', matcher: 'Task' },
|
|
172
|
+
{ settingsKey: 'PostToolUse', hookName: 'post-todo', matcher: 'TodoWrite' },
|
|
173
|
+
];
|
|
174
|
+
for (const { settingsKey, hookName } of hookMappings) {
|
|
175
|
+
const existing = settings.hooks[settingsKey] ?? [];
|
|
176
|
+
if (force) {
|
|
177
|
+
// Remove existing entire hooks
|
|
178
|
+
const filtered = existing.filter((m) => !m.hooks.some((h) => h.command.includes('entire ')));
|
|
179
|
+
settings.hooks[settingsKey] = filtered;
|
|
180
|
+
}
|
|
181
|
+
// Check if already installed
|
|
182
|
+
const hasEntireHook = (settings.hooks[settingsKey] ?? []).some((m) => m.hooks.some((h) => h.command.includes('entire ')));
|
|
183
|
+
if (!hasEntireHook) {
|
|
184
|
+
const matchers = settings.hooks[settingsKey] ?? [];
|
|
185
|
+
matchers.push({
|
|
186
|
+
matcher: '',
|
|
187
|
+
hooks: [
|
|
188
|
+
{
|
|
189
|
+
type: 'command',
|
|
190
|
+
command: `entire hooks claude-code ${hookName}`,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
settings.hooks[settingsKey] = matchers;
|
|
195
|
+
installed++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const { settingsKey, hookName, matcher } of taskHookMappings) {
|
|
199
|
+
const existing = settings.hooks[settingsKey] ?? [];
|
|
200
|
+
if (force) {
|
|
201
|
+
const filtered = existing.filter((m) => !(m.matcher === matcher && m.hooks.some((h) => h.command.includes('entire '))));
|
|
202
|
+
settings.hooks[settingsKey] = filtered;
|
|
203
|
+
}
|
|
204
|
+
const hasEntireHook = (settings.hooks[settingsKey] ?? []).some((m) => m.matcher === matcher && m.hooks.some((h) => h.command.includes('entire ')));
|
|
205
|
+
if (!hasEntireHook) {
|
|
206
|
+
const matchers = settings.hooks[settingsKey] ?? [];
|
|
207
|
+
matchers.push({
|
|
208
|
+
matcher,
|
|
209
|
+
hooks: [
|
|
210
|
+
{
|
|
211
|
+
type: 'command',
|
|
212
|
+
command: `entire hooks claude-code ${hookName}`,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
settings.hooks[settingsKey] = matchers;
|
|
217
|
+
installed++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Add metadata deny rule
|
|
221
|
+
if (!settings.permissions)
|
|
222
|
+
settings.permissions = {};
|
|
223
|
+
if (!settings.permissions.deny)
|
|
224
|
+
settings.permissions.deny = [];
|
|
225
|
+
if (!settings.permissions.deny.includes(METADATA_DENY_RULE)) {
|
|
226
|
+
settings.permissions.deny.push(METADATA_DENY_RULE);
|
|
227
|
+
}
|
|
228
|
+
// Write settings
|
|
229
|
+
const dir = path.dirname(settingsPath);
|
|
230
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
231
|
+
await fs.promises.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
232
|
+
return installed;
|
|
233
|
+
}
|
|
234
|
+
async uninstallHooks(repoPath) {
|
|
235
|
+
const settingsPath = path.join(repoPath, CLAUDE_SETTINGS_FILE);
|
|
236
|
+
try {
|
|
237
|
+
const content = await fs.promises.readFile(settingsPath, 'utf-8');
|
|
238
|
+
const settings = JSON.parse(content);
|
|
239
|
+
if (settings.hooks) {
|
|
240
|
+
for (const key of Object.keys(settings.hooks)) {
|
|
241
|
+
const matchers = settings.hooks[key];
|
|
242
|
+
if (!matchers)
|
|
243
|
+
continue;
|
|
244
|
+
settings.hooks[key] = matchers.filter((m) => !m.hooks.some((h) => h.command.includes('entire ')));
|
|
245
|
+
if (settings.hooks[key].length === 0) {
|
|
246
|
+
delete settings.hooks[key];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
250
|
+
delete settings.hooks;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Remove deny rule
|
|
254
|
+
if (settings.permissions?.deny) {
|
|
255
|
+
settings.permissions.deny = settings.permissions.deny.filter((d) => d !== METADATA_DENY_RULE);
|
|
256
|
+
if (settings.permissions.deny.length === 0) {
|
|
257
|
+
delete settings.permissions.deny;
|
|
258
|
+
}
|
|
259
|
+
if (Object.keys(settings.permissions).length === 0) {
|
|
260
|
+
delete settings.permissions;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
await fs.promises.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// No settings to modify
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async areHooksInstalled(repoPath) {
|
|
270
|
+
const settingsPath = path.join(repoPath, CLAUDE_SETTINGS_FILE);
|
|
271
|
+
try {
|
|
272
|
+
const content = await fs.promises.readFile(settingsPath, 'utf-8');
|
|
273
|
+
const settings = JSON.parse(content);
|
|
274
|
+
if (!settings.hooks)
|
|
275
|
+
return false;
|
|
276
|
+
// Check for at least the session-start hook
|
|
277
|
+
const sessionStart = settings.hooks.SessionStart ?? [];
|
|
278
|
+
return sessionStart.some((m) => m.hooks.some((h) => h.command.includes('entire ')));
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// ===========================================================================
|
|
285
|
+
// TranscriptPreparer — wait for Claude Code's async transcript flush
|
|
286
|
+
// ===========================================================================
|
|
287
|
+
async prepareTranscript(sessionRef) {
|
|
288
|
+
await waitForTranscriptFlush(sessionRef);
|
|
289
|
+
}
|
|
290
|
+
// ===========================================================================
|
|
291
|
+
// TranscriptAnalyzer
|
|
292
|
+
// ===========================================================================
|
|
293
|
+
async getTranscriptPosition(transcriptPath) {
|
|
294
|
+
try {
|
|
295
|
+
const content = await fs.promises.readFile(transcriptPath, 'utf-8');
|
|
296
|
+
const lines = content.split('\n').filter(Boolean);
|
|
297
|
+
return lines.length;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async extractModifiedFilesFromOffset(transcriptPath, startOffset) {
|
|
304
|
+
const content = await fs.promises.readFile(transcriptPath, 'utf-8');
|
|
305
|
+
const lines = content.split('\n').filter(Boolean);
|
|
306
|
+
const files = new Set();
|
|
307
|
+
for (let i = startOffset; i < lines.length; i++) {
|
|
308
|
+
try {
|
|
309
|
+
const line = JSON.parse(lines[i]);
|
|
310
|
+
if (line.type === 'assistant') {
|
|
311
|
+
const contentBlocks = extractContentBlocks(line.message);
|
|
312
|
+
for (const block of contentBlocks) {
|
|
313
|
+
if (block.type === 'tool_use' &&
|
|
314
|
+
block.name &&
|
|
315
|
+
FILE_MODIFICATION_TOOLS.has(block.name)) {
|
|
316
|
+
const filePath = block.input?.file_path;
|
|
317
|
+
if (filePath)
|
|
318
|
+
files.add(filePath);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// Skip malformed lines
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { files: Array.from(files), currentPosition: lines.length };
|
|
328
|
+
}
|
|
329
|
+
async extractPrompts(sessionRef, fromOffset) {
|
|
330
|
+
const content = await fs.promises.readFile(sessionRef, 'utf-8');
|
|
331
|
+
const lines = content.split('\n').filter(Boolean);
|
|
332
|
+
const prompts = [];
|
|
333
|
+
for (let i = fromOffset; i < lines.length; i++) {
|
|
334
|
+
try {
|
|
335
|
+
const line = JSON.parse(lines[i]);
|
|
336
|
+
if (line.type === 'user') {
|
|
337
|
+
const text = extractUserText(line.message);
|
|
338
|
+
if (text)
|
|
339
|
+
prompts.push(text);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// Skip malformed lines
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return prompts;
|
|
347
|
+
}
|
|
348
|
+
async extractSummary(sessionRef) {
|
|
349
|
+
const prompts = await this.extractPrompts(sessionRef, 0);
|
|
350
|
+
if (prompts.length === 0)
|
|
351
|
+
return '';
|
|
352
|
+
// Return the first prompt as a summary, truncated
|
|
353
|
+
return prompts[0].slice(0, 200);
|
|
354
|
+
}
|
|
355
|
+
// ===========================================================================
|
|
356
|
+
// TokenCalculator
|
|
357
|
+
// ===========================================================================
|
|
358
|
+
async calculateTokenUsage(transcriptData, fromOffset) {
|
|
359
|
+
const content = transcriptData.toString('utf-8');
|
|
360
|
+
const lines = content.split('\n').filter(Boolean);
|
|
361
|
+
// Deduplicate by message ID — streaming may produce multiple rows per message.
|
|
362
|
+
// Keep the entry with the highest output_tokens (final streaming state).
|
|
363
|
+
const usageByMessageID = new Map();
|
|
364
|
+
for (let i = fromOffset; i < lines.length; i++) {
|
|
365
|
+
try {
|
|
366
|
+
const raw = JSON.parse(lines[i]);
|
|
367
|
+
if (raw.type === 'assistant') {
|
|
368
|
+
const msg = raw.message;
|
|
369
|
+
const msgID = msg?.id;
|
|
370
|
+
const msgUsage = msg?.usage;
|
|
371
|
+
if (msgUsage && msgID) {
|
|
372
|
+
const existing = usageByMessageID.get(msgID);
|
|
373
|
+
if (!existing || (msgUsage.output_tokens ?? 0) > (existing.output_tokens ?? 0)) {
|
|
374
|
+
usageByMessageID.set(msgID, msgUsage);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else if (msgUsage) {
|
|
378
|
+
// No message ID — count each occurrence
|
|
379
|
+
usageByMessageID.set(`_anon_${i}`, msgUsage);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// Skip malformed lines
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const usage = emptyTokenUsage();
|
|
388
|
+
usage.apiCallCount = usageByMessageID.size;
|
|
389
|
+
for (const u of usageByMessageID.values()) {
|
|
390
|
+
usage.inputTokens += u.input_tokens ?? 0;
|
|
391
|
+
usage.cacheCreationTokens += u.cache_creation_input_tokens ?? 0;
|
|
392
|
+
usage.cacheReadTokens += u.cache_read_input_tokens ?? 0;
|
|
393
|
+
usage.outputTokens += u.output_tokens ?? 0;
|
|
394
|
+
}
|
|
395
|
+
return usage;
|
|
396
|
+
}
|
|
397
|
+
// ===========================================================================
|
|
398
|
+
// SubagentAwareExtractor
|
|
399
|
+
// ===========================================================================
|
|
400
|
+
async extractAllModifiedFiles(transcriptData, fromOffset, subagentsDir) {
|
|
401
|
+
if (transcriptData.length === 0)
|
|
402
|
+
return [];
|
|
403
|
+
const content = transcriptData.toString('utf-8');
|
|
404
|
+
const allLines = content.split('\n').filter(Boolean);
|
|
405
|
+
const sliced = allLines.slice(fromOffset);
|
|
406
|
+
const parsed = sliced
|
|
407
|
+
.map((line) => {
|
|
408
|
+
try {
|
|
409
|
+
return JSON.parse(line);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
.filter((l) => l !== null);
|
|
416
|
+
// Collect modified files from main agent
|
|
417
|
+
const fileSet = new Set();
|
|
418
|
+
for (const f of extractModifiedFiles(parsed)) {
|
|
419
|
+
fileSet.add(f);
|
|
420
|
+
}
|
|
421
|
+
// Find spawned subagents and collect their modified files
|
|
422
|
+
const agentIDs = extractSpawnedAgentIDs(parsed);
|
|
423
|
+
if (subagentsDir) {
|
|
424
|
+
for (const agentID of agentIDs.keys()) {
|
|
425
|
+
const agentPath = path.join(subagentsDir, `agent-${agentID}.jsonl`);
|
|
426
|
+
try {
|
|
427
|
+
const agentContent = await fs.promises.readFile(agentPath, 'utf-8');
|
|
428
|
+
const agentLines = agentContent
|
|
429
|
+
.split('\n')
|
|
430
|
+
.filter(Boolean)
|
|
431
|
+
.map((line) => {
|
|
432
|
+
try {
|
|
433
|
+
return JSON.parse(line);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
.filter((l) => l !== null);
|
|
440
|
+
for (const f of extractModifiedFiles(agentLines)) {
|
|
441
|
+
fileSet.add(f);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// Subagent transcript may not exist yet
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return Array.from(fileSet);
|
|
450
|
+
}
|
|
451
|
+
async calculateTotalTokenUsage(transcriptData, fromOffset, subagentsDir) {
|
|
452
|
+
if (transcriptData.length === 0)
|
|
453
|
+
return emptyTokenUsage();
|
|
454
|
+
// Calculate main session token usage
|
|
455
|
+
const mainUsage = await this.calculateTokenUsage(transcriptData, fromOffset);
|
|
456
|
+
// Extract spawned agent IDs from the transcript
|
|
457
|
+
const content = transcriptData.toString('utf-8');
|
|
458
|
+
const allLines = content.split('\n').filter(Boolean);
|
|
459
|
+
const sliced = allLines.slice(fromOffset);
|
|
460
|
+
const parsed = sliced
|
|
461
|
+
.map((line) => {
|
|
462
|
+
try {
|
|
463
|
+
return JSON.parse(line);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
.filter((l) => l !== null);
|
|
470
|
+
const agentIDs = extractSpawnedAgentIDs(parsed);
|
|
471
|
+
// Calculate subagent token usage
|
|
472
|
+
if (agentIDs.size > 0 && subagentsDir) {
|
|
473
|
+
const subagentUsage = emptyTokenUsage();
|
|
474
|
+
let hasSubagentUsage = false;
|
|
475
|
+
for (const agentID of agentIDs.keys()) {
|
|
476
|
+
const agentPath = path.join(subagentsDir, `agent-${agentID}.jsonl`);
|
|
477
|
+
try {
|
|
478
|
+
const agentData = await fs.promises.readFile(agentPath);
|
|
479
|
+
const agentUsage = await this.calculateTokenUsage(agentData, 0);
|
|
480
|
+
subagentUsage.inputTokens += agentUsage.inputTokens;
|
|
481
|
+
subagentUsage.cacheCreationTokens += agentUsage.cacheCreationTokens;
|
|
482
|
+
subagentUsage.cacheReadTokens += agentUsage.cacheReadTokens;
|
|
483
|
+
subagentUsage.outputTokens += agentUsage.outputTokens;
|
|
484
|
+
subagentUsage.apiCallCount += agentUsage.apiCallCount;
|
|
485
|
+
hasSubagentUsage = true;
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// Agent transcript may not exist yet
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (hasSubagentUsage && subagentUsage.apiCallCount > 0) {
|
|
492
|
+
mainUsage.subagentTokens = subagentUsage;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return mainUsage;
|
|
496
|
+
}
|
|
497
|
+
// ===========================================================================
|
|
498
|
+
// TranscriptChunker
|
|
499
|
+
// ===========================================================================
|
|
500
|
+
async chunkTranscript(content, maxSize) {
|
|
501
|
+
return chunkJSONL(content, maxSize);
|
|
502
|
+
}
|
|
503
|
+
async reassembleTranscript(chunks) {
|
|
504
|
+
return Buffer.concat(chunks);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// ============================================================================
|
|
508
|
+
// Transcript Parsing Helpers
|
|
509
|
+
// ============================================================================
|
|
510
|
+
function extractContentBlocks(message) {
|
|
511
|
+
if (!message || typeof message !== 'object')
|
|
512
|
+
return [];
|
|
513
|
+
const msg = message;
|
|
514
|
+
const content = msg.content;
|
|
515
|
+
if (Array.isArray(content)) {
|
|
516
|
+
return content;
|
|
517
|
+
}
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
function extractUserText(message) {
|
|
521
|
+
if (typeof message === 'string')
|
|
522
|
+
return message;
|
|
523
|
+
if (!message || typeof message !== 'object')
|
|
524
|
+
return '';
|
|
525
|
+
const msg = message;
|
|
526
|
+
// Content can be string or array of blocks
|
|
527
|
+
if (typeof msg.content === 'string')
|
|
528
|
+
return msg.content;
|
|
529
|
+
if (Array.isArray(msg.content)) {
|
|
530
|
+
return msg.content
|
|
531
|
+
.filter((b) => b.type === 'text' && b.text)
|
|
532
|
+
.map((b) => b.text)
|
|
533
|
+
.join('\n');
|
|
534
|
+
}
|
|
535
|
+
return '';
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Parse a JSONL transcript into structured lines
|
|
539
|
+
*/
|
|
540
|
+
export function parseTranscript(content) {
|
|
541
|
+
const lines = [];
|
|
542
|
+
for (const rawLine of content.split('\n').filter(Boolean)) {
|
|
543
|
+
try {
|
|
544
|
+
const parsed = JSON.parse(rawLine);
|
|
545
|
+
if (parsed.type === 'user' || parsed.type === 'assistant') {
|
|
546
|
+
lines.push(parsed);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
// Skip malformed lines
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return lines;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Extract all modified files from transcript lines
|
|
557
|
+
*/
|
|
558
|
+
export function extractModifiedFiles(lines) {
|
|
559
|
+
const files = new Set();
|
|
560
|
+
for (const line of lines) {
|
|
561
|
+
if (line.type !== 'assistant')
|
|
562
|
+
continue;
|
|
563
|
+
const blocks = extractContentBlocks(line.message);
|
|
564
|
+
for (const block of blocks) {
|
|
565
|
+
if (block.type === 'tool_use' && block.name && FILE_MODIFICATION_TOOLS.has(block.name)) {
|
|
566
|
+
const input = block.input;
|
|
567
|
+
const filePath = (input?.file_path ?? input?.notebook_path);
|
|
568
|
+
if (filePath)
|
|
569
|
+
files.add(filePath);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return Array.from(files);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Extract the last user prompt from transcript lines
|
|
577
|
+
*/
|
|
578
|
+
export function extractLastUserPrompt(lines) {
|
|
579
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
580
|
+
if (lines[i].type === 'user') {
|
|
581
|
+
return extractUserText(lines[i].message);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return '';
|
|
585
|
+
}
|
|
586
|
+
// ============================================================================
|
|
587
|
+
// Subagent ID Extraction
|
|
588
|
+
// ============================================================================
|
|
589
|
+
/**
|
|
590
|
+
* Extract spawned agent IDs from Task tool results in a transcript.
|
|
591
|
+
* When a Task tool completes, the tool_result contains "agentId: <id>".
|
|
592
|
+
* Returns a map of agentID → toolUseID.
|
|
593
|
+
*/
|
|
594
|
+
export function extractSpawnedAgentIDs(lines) {
|
|
595
|
+
const agentIDs = new Map();
|
|
596
|
+
for (const line of lines) {
|
|
597
|
+
if (line.type !== 'user')
|
|
598
|
+
continue;
|
|
599
|
+
const msg = line.message;
|
|
600
|
+
if (!msg)
|
|
601
|
+
continue;
|
|
602
|
+
const content = msg.content;
|
|
603
|
+
if (!Array.isArray(content))
|
|
604
|
+
continue;
|
|
605
|
+
for (const block of content) {
|
|
606
|
+
if (block.type !== 'tool_result')
|
|
607
|
+
continue;
|
|
608
|
+
const toolUseID = String(block.tool_use_id ?? '');
|
|
609
|
+
let textContent = '';
|
|
610
|
+
// Content can be a string or array of text blocks
|
|
611
|
+
if (typeof block.content === 'string') {
|
|
612
|
+
textContent = block.content;
|
|
613
|
+
}
|
|
614
|
+
else if (Array.isArray(block.content)) {
|
|
615
|
+
for (const tb of block.content) {
|
|
616
|
+
if (tb.type === 'text' && typeof tb.text === 'string') {
|
|
617
|
+
textContent += tb.text + '\n';
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const agentID = extractAgentIDFromText(textContent);
|
|
622
|
+
if (agentID) {
|
|
623
|
+
agentIDs.set(agentID, toolUseID);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return agentIDs;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Extract an agent ID from text containing "agentId: <id>".
|
|
631
|
+
*/
|
|
632
|
+
function extractAgentIDFromText(text) {
|
|
633
|
+
const prefix = 'agentId: ';
|
|
634
|
+
const idx = text.indexOf(prefix);
|
|
635
|
+
if (idx === -1)
|
|
636
|
+
return '';
|
|
637
|
+
const start = idx + prefix.length;
|
|
638
|
+
let end = start;
|
|
639
|
+
while (end < text.length && /[a-zA-Z0-9]/.test(text[end])) {
|
|
640
|
+
end++;
|
|
641
|
+
}
|
|
642
|
+
return end > start ? text.slice(start, end) : '';
|
|
643
|
+
}
|
|
644
|
+
// ============================================================================
|
|
645
|
+
// JSONL Chunking
|
|
646
|
+
// ============================================================================
|
|
647
|
+
function chunkJSONL(content, maxSize) {
|
|
648
|
+
if (content.length <= maxSize)
|
|
649
|
+
return [content];
|
|
650
|
+
const str = content.toString('utf-8');
|
|
651
|
+
const lines = str.split('\n');
|
|
652
|
+
const chunks = [];
|
|
653
|
+
let current = [];
|
|
654
|
+
let currentSize = 0;
|
|
655
|
+
for (const line of lines) {
|
|
656
|
+
const lineSize = Buffer.byteLength(line + '\n');
|
|
657
|
+
if (currentSize + lineSize > maxSize && current.length > 0) {
|
|
658
|
+
chunks.push(Buffer.from(current.join('\n') + '\n'));
|
|
659
|
+
current = [];
|
|
660
|
+
currentSize = 0;
|
|
661
|
+
}
|
|
662
|
+
current.push(line);
|
|
663
|
+
currentSize += lineSize;
|
|
664
|
+
}
|
|
665
|
+
if (current.length > 0) {
|
|
666
|
+
const remaining = current.join('\n');
|
|
667
|
+
if (remaining.trim()) {
|
|
668
|
+
chunks.push(Buffer.from(remaining + '\n'));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return chunks;
|
|
672
|
+
}
|
|
673
|
+
// ============================================================================
|
|
674
|
+
// Transcript Flush Sentinel
|
|
675
|
+
// ============================================================================
|
|
676
|
+
/**
|
|
677
|
+
* String that appears in Claude Code's hook_progress entry when the stop hook
|
|
678
|
+
* has been invoked, indicating the transcript is fully flushed.
|
|
679
|
+
*/
|
|
680
|
+
const STOP_HOOK_SENTINEL = 'hooks claude-code stop';
|
|
681
|
+
const FLUSH_MAX_WAIT_MS = 3000;
|
|
682
|
+
const FLUSH_POLL_INTERVAL_MS = 50;
|
|
683
|
+
const FLUSH_TAIL_BYTES = 4096;
|
|
684
|
+
const FLUSH_MAX_SKEW_MS = 2000;
|
|
685
|
+
/**
|
|
686
|
+
* Poll the transcript file for the stop hook sentinel.
|
|
687
|
+
* Falls back silently after a timeout.
|
|
688
|
+
*/
|
|
689
|
+
async function waitForTranscriptFlush(transcriptPath) {
|
|
690
|
+
const hookStartTime = Date.now();
|
|
691
|
+
const deadline = hookStartTime + FLUSH_MAX_WAIT_MS;
|
|
692
|
+
while (Date.now() < deadline) {
|
|
693
|
+
if (checkStopSentinel(transcriptPath, hookStartTime)) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
await sleep(FLUSH_POLL_INTERVAL_MS);
|
|
697
|
+
}
|
|
698
|
+
// Timeout — proceed anyway
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Read the tail of the transcript file and look for the stop hook sentinel
|
|
702
|
+
* with a timestamp within the acceptable skew window.
|
|
703
|
+
*/
|
|
704
|
+
function checkStopSentinel(filePath, hookStartTime) {
|
|
705
|
+
let fd;
|
|
706
|
+
try {
|
|
707
|
+
fd = fs.openSync(filePath, 'r');
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const stat = fs.fstatSync(fd);
|
|
714
|
+
const offset = Math.max(0, stat.size - FLUSH_TAIL_BYTES);
|
|
715
|
+
const buf = Buffer.alloc(stat.size - offset);
|
|
716
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
717
|
+
const lines = buf.toString('utf-8').split('\n');
|
|
718
|
+
for (const line of lines) {
|
|
719
|
+
const trimmed = line.trim();
|
|
720
|
+
if (!trimmed || !trimmed.includes(STOP_HOOK_SENTINEL))
|
|
721
|
+
continue;
|
|
722
|
+
try {
|
|
723
|
+
const entry = JSON.parse(trimmed);
|
|
724
|
+
if (!entry.timestamp)
|
|
725
|
+
continue;
|
|
726
|
+
const ts = new Date(entry.timestamp).getTime();
|
|
727
|
+
if (isNaN(ts))
|
|
728
|
+
continue;
|
|
729
|
+
const lowerBound = hookStartTime - FLUSH_MAX_SKEW_MS;
|
|
730
|
+
const upperBound = hookStartTime + FLUSH_MAX_SKEW_MS;
|
|
731
|
+
if (ts > lowerBound && ts < upperBound) {
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
finally {
|
|
742
|
+
fs.closeSync(fd);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function sleep(ms) {
|
|
746
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
747
|
+
}
|
|
748
|
+
// ============================================================================
|
|
749
|
+
// Path Helpers
|
|
750
|
+
// ============================================================================
|
|
751
|
+
/**
|
|
752
|
+
* Sanitize a filesystem path for use as a Claude project directory name
|
|
753
|
+
*/
|
|
754
|
+
function sanitizePathForClaude(repoPath) {
|
|
755
|
+
// Claude uses a hash-based directory naming scheme
|
|
756
|
+
return crypto.createHash('sha256').update(repoPath).digest('hex').slice(0, 16);
|
|
757
|
+
}
|
|
758
|
+
// ============================================================================
|
|
759
|
+
// Registration
|
|
760
|
+
// ============================================================================
|
|
761
|
+
/**
|
|
762
|
+
* Create and return a new Claude Code agent instance
|
|
763
|
+
*/
|
|
764
|
+
export function createClaudeCodeAgent() {
|
|
765
|
+
return new ClaudeCodeAgent();
|
|
766
|
+
}
|
|
767
|
+
// Auto-register when imported
|
|
768
|
+
registerAgent(AGENT_NAMES.CLAUDE_CODE, () => new ClaudeCodeAgent());
|
|
769
|
+
//# sourceMappingURL=claude-code.js.map
|