@scotthuang/agent-knock-knock 0.2.47 → 0.2.49
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/CHANGELOG.md +46 -0
- package/README.md +114 -147
- package/dist/src/approval-policy.d.ts +2 -1
- package/dist/src/approval-policy.js +14 -6
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/claude-hook-protocol.js.map +1 -1
- package/dist/src/claude-hook-store.js +17 -6
- package/dist/src/claude-hook-store.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.d.ts +40 -0
- package/dist/src/claude-local-transcript-provider.js +804 -0
- package/dist/src/claude-local-transcript-provider.js.map +1 -0
- package/dist/src/claude-terminal-agent-adapter.d.ts +3 -0
- package/dist/src/claude-terminal-agent-adapter.js +146 -47
- package/dist/src/claude-terminal-agent-adapter.js.map +1 -1
- package/dist/src/cli.js +2473 -834
- package/dist/src/cli.js.map +1 -1
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/codex-store-adapter.js.map +1 -1
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/doctor-capabilities.d.ts +22 -0
- package/dist/src/doctor-capabilities.js +29 -0
- package/dist/src/doctor-capabilities.js.map +1 -0
- package/dist/src/executors.js.map +1 -1
- package/dist/src/openclaw-plugin-helpers.d.ts +49 -0
- package/dist/src/openclaw-plugin-helpers.js +246 -0
- package/dist/src/openclaw-plugin-helpers.js.map +1 -0
- package/dist/src/openclaw-plugin.js +94 -271
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/runtime-log.js +54 -2
- package/dist/src/runtime-log.js.map +1 -1
- package/dist/src/store.js +432 -12
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +3 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +51 -3
- package/dist/src/terminal-agent-bridge.js +276 -39
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-agent-registry.js.map +1 -1
- package/dist/src/terminal-control-provider.d.ts +3 -1
- package/dist/src/terminal-control-provider.js +41 -37
- package/dist/src/terminal-control-provider.js.map +1 -1
- package/dist/src/terminal-process-source.d.ts +12 -3
- package/dist/src/terminal-process-source.js +37 -6
- package/dist/src/terminal-process-source.js.map +1 -1
- package/dist/src/transcript.js.map +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +11 -6
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +10 -24
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { redactString } from "./runtime-log.js";
|
|
6
|
+
const CLAUDE_TRANSCRIPT_ANCHOR_VERSION = 1;
|
|
7
|
+
const CLAUDE_TRANSCRIPT_MAX_TURN_BYTES = 64 * 1024 * 1024;
|
|
8
|
+
const CLAUDE_SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
9
|
+
const SUPPORTED_CLAUDE_TRANSCRIPT_VERSION = "2.1.198";
|
|
10
|
+
const NO_FOLLOW_FLAG = typeof fs.constants.O_NOFOLLOW === "number"
|
|
11
|
+
? fs.constants.O_NOFOLLOW
|
|
12
|
+
: 0;
|
|
13
|
+
export function defaultClaudeHome() {
|
|
14
|
+
const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
15
|
+
return configured || path.join(os.homedir(), ".claude");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Records the immutable file boundary immediately before AKK submits a turn.
|
|
19
|
+
* No transcript contents are retained in conversation state.
|
|
20
|
+
*/
|
|
21
|
+
export function captureClaudeTranscriptAnchor(options) {
|
|
22
|
+
const sessionId = nonEmptyString(options.sessionId);
|
|
23
|
+
const cwd = nonEmptyString(options.cwd);
|
|
24
|
+
const pid = positiveInteger(options.pid);
|
|
25
|
+
if (!sessionId || !cwd || pid === undefined || !CLAUDE_SESSION_ID_PATTERN.test(sessionId)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const agent = exactInteractiveAgent(options.agentRows, pid);
|
|
29
|
+
const agentStartedAtMs = positiveInteger(agent?.startedAt);
|
|
30
|
+
if (!agent ||
|
|
31
|
+
agentStartedAtMs === undefined ||
|
|
32
|
+
agent.sessionId !== sessionId ||
|
|
33
|
+
normalizePath(agent.cwd) !== normalizePath(cwd) ||
|
|
34
|
+
agent.status !== "idle") {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const claudeHome = path.resolve(options.claudeHome ?? defaultClaudeHome());
|
|
38
|
+
const projectsRoot = projectsRootPath(claudeHome);
|
|
39
|
+
if (!isRealDirectory(projectsRoot)) {
|
|
40
|
+
if (lstatOrUndefined(projectsRoot) !== undefined ||
|
|
41
|
+
!isRealDirectory(claudeHome)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
schema_version: CLAUDE_TRANSCRIPT_ANCHOR_VERSION,
|
|
46
|
+
session_id: sessionId,
|
|
47
|
+
cwd: path.resolve(cwd),
|
|
48
|
+
pid,
|
|
49
|
+
agent_started_at_ms: agentStartedAtMs,
|
|
50
|
+
captured_at: (options.now ?? new Date()).toISOString(),
|
|
51
|
+
relative_path: expectedTranscriptRelativePath(sessionId, cwd),
|
|
52
|
+
offset_bytes: 0,
|
|
53
|
+
file_existed: false
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const located = locateTranscript(projectsRoot, sessionId);
|
|
57
|
+
const relativePath = located?.relativePath ?? expectedTranscriptRelativePath(sessionId, cwd);
|
|
58
|
+
if (!located) {
|
|
59
|
+
return {
|
|
60
|
+
schema_version: CLAUDE_TRANSCRIPT_ANCHOR_VERSION,
|
|
61
|
+
session_id: sessionId,
|
|
62
|
+
cwd: path.resolve(cwd),
|
|
63
|
+
pid,
|
|
64
|
+
agent_started_at_ms: agentStartedAtMs,
|
|
65
|
+
captured_at: (options.now ?? new Date()).toISOString(),
|
|
66
|
+
relative_path: relativePath,
|
|
67
|
+
offset_bytes: 0,
|
|
68
|
+
file_existed: false
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
if (located.stat.size > 0 && !fileEndsWithNewline(located.fd, located.stat.size)) {
|
|
73
|
+
throw new Error("Claude transcript did not end at a complete JSONL record before send");
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
schema_version: CLAUDE_TRANSCRIPT_ANCHOR_VERSION,
|
|
77
|
+
session_id: sessionId,
|
|
78
|
+
cwd: path.resolve(cwd),
|
|
79
|
+
pid,
|
|
80
|
+
agent_started_at_ms: agentStartedAtMs,
|
|
81
|
+
captured_at: (options.now ?? new Date()).toISOString(),
|
|
82
|
+
relative_path: located.relativePath,
|
|
83
|
+
offset_bytes: located.stat.size,
|
|
84
|
+
file_existed: true,
|
|
85
|
+
device: String(located.stat.dev),
|
|
86
|
+
inode: String(located.stat.ino)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
fs.closeSync(located.fd);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Detects one completed Claude Code turn from the append-only local transcript.
|
|
95
|
+
* The detector intentionally fails closed on identity, schema, rotation, prompt,
|
|
96
|
+
* background-work, and chain ambiguity.
|
|
97
|
+
*/
|
|
98
|
+
export function detectClaudeTranscriptCompletion(request, options) {
|
|
99
|
+
const anchor = transcriptAnchorFromContext(request.context);
|
|
100
|
+
if (!anchor) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const sessionId = nonEmptyString(request.sessionId);
|
|
104
|
+
const cwd = nonEmptyString(request.cwd);
|
|
105
|
+
const expectedRequestHash = nonEmptyString(request.requestHash);
|
|
106
|
+
const requestTextHash = requestFingerprint(request.requestText);
|
|
107
|
+
const expectedPromptText = exactPromptText(request.requestText);
|
|
108
|
+
const startedAtMs = validTimestampMs(request.startedAt);
|
|
109
|
+
const capturedAtMs = validTimestampMs(anchor.captured_at);
|
|
110
|
+
if (!sessionId ||
|
|
111
|
+
!cwd ||
|
|
112
|
+
!expectedRequestHash ||
|
|
113
|
+
!requestTextHash ||
|
|
114
|
+
!expectedPromptText ||
|
|
115
|
+
expectedRequestHash !== requestTextHash ||
|
|
116
|
+
startedAtMs === undefined ||
|
|
117
|
+
capturedAtMs === undefined) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
if (anchor.schema_version !== CLAUDE_TRANSCRIPT_ANCHOR_VERSION ||
|
|
121
|
+
anchor.session_id !== sessionId ||
|
|
122
|
+
normalizePath(anchor.cwd) !== normalizePath(cwd) ||
|
|
123
|
+
!CLAUDE_SESSION_ID_PATTERN.test(sessionId)) {
|
|
124
|
+
throw new Error("Claude transcript anchor does not match the managed terminal turn");
|
|
125
|
+
}
|
|
126
|
+
const runtimePid = runtimePidFromContext(request.context);
|
|
127
|
+
if (runtimePid === undefined || runtimePid !== anchor.pid) {
|
|
128
|
+
throw new Error("Claude transcript anchor PID does not match the active terminal runtime");
|
|
129
|
+
}
|
|
130
|
+
const agent = exactInteractiveAgent(options.agentRows, anchor.pid);
|
|
131
|
+
if (!agent) {
|
|
132
|
+
throw new Error("the exact Claude process is absent from the local agent registry");
|
|
133
|
+
}
|
|
134
|
+
if (agent.startedAt !== anchor.agent_started_at_ms ||
|
|
135
|
+
agent.sessionId !== sessionId ||
|
|
136
|
+
normalizePath(agent.cwd) !== normalizePath(cwd)) {
|
|
137
|
+
throw new Error("the Claude process session identity changed after the managed send");
|
|
138
|
+
}
|
|
139
|
+
if (agent.status !== "idle") {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const projectsRoot = projectsRootPath(path.resolve(options.claudeHome ?? defaultClaudeHome()));
|
|
143
|
+
if (!isRealDirectory(projectsRoot)) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const opened = openAnchoredTranscript(projectsRoot, anchor);
|
|
147
|
+
if (!opened) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
if (anchor.file_existed &&
|
|
152
|
+
(String(opened.stat.dev) !== anchor.device || String(opened.stat.ino) !== anchor.inode)) {
|
|
153
|
+
throw new Error("Claude transcript was replaced or rotated after the managed send");
|
|
154
|
+
}
|
|
155
|
+
if (opened.stat.size < anchor.offset_bytes) {
|
|
156
|
+
throw new Error("Claude transcript was truncated after the managed send");
|
|
157
|
+
}
|
|
158
|
+
const bytesToRead = opened.stat.size - anchor.offset_bytes;
|
|
159
|
+
if (bytesToRead === 0) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const maxTurnBytes = positiveInteger(options.maxTurnBytes) ??
|
|
163
|
+
CLAUDE_TRANSCRIPT_MAX_TURN_BYTES;
|
|
164
|
+
if (bytesToRead > maxTurnBytes) {
|
|
165
|
+
throw new Error("Claude transcript turn exceeded the bounded local read limit");
|
|
166
|
+
}
|
|
167
|
+
const records = readCompleteJsonlRecords(opened.fd, anchor.offset_bytes, bytesToRead);
|
|
168
|
+
const stableStat = fs.fstatSync(opened.fd);
|
|
169
|
+
if (stableStat.dev !== opened.stat.dev ||
|
|
170
|
+
stableStat.ino !== opened.stat.ino ||
|
|
171
|
+
stableStat.size !== opened.stat.size ||
|
|
172
|
+
stableStat.mtimeMs !== opened.stat.mtimeMs) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
if (records.length === 0) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
return completionFromRecords({
|
|
179
|
+
records,
|
|
180
|
+
sessionId,
|
|
181
|
+
cwd,
|
|
182
|
+
expectedRequestHash,
|
|
183
|
+
expectedPromptText,
|
|
184
|
+
fileIdentity: `${opened.stat.dev}:${opened.stat.ino}`
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
fs.closeSync(opened.fd);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function completionFromRecords({ records, sessionId, cwd, expectedRequestHash, expectedPromptText, fileIdentity }) {
|
|
192
|
+
const promptCandidates = records.filter((record) => {
|
|
193
|
+
const promptText = userPromptText(record);
|
|
194
|
+
return record.type === "user" &&
|
|
195
|
+
isRecord(record.message) &&
|
|
196
|
+
record.message.role === "user" &&
|
|
197
|
+
record.isSidechain !== true &&
|
|
198
|
+
nonEmptyString(record.agentId) === undefined &&
|
|
199
|
+
record.sessionId === sessionId &&
|
|
200
|
+
normalizePath(record.cwd) === normalizePath(cwd) &&
|
|
201
|
+
validTimestampMs(record.timestamp) !== undefined &&
|
|
202
|
+
promptText !== undefined &&
|
|
203
|
+
exactPromptText(promptText) === expectedPromptText &&
|
|
204
|
+
requestFingerprint(promptText) === expectedRequestHash;
|
|
205
|
+
});
|
|
206
|
+
if (promptCandidates.length === 0) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
if (promptCandidates.length !== 1) {
|
|
210
|
+
throw new Error("multiple Claude transcript prompts matched the managed request");
|
|
211
|
+
}
|
|
212
|
+
const prompt = promptCandidates[0];
|
|
213
|
+
const promptUuid = uuidValue(prompt.uuid);
|
|
214
|
+
if (!promptUuid) {
|
|
215
|
+
throw new Error("matched Claude transcript prompt has no stable UUID");
|
|
216
|
+
}
|
|
217
|
+
assertSupportedRecord(prompt, sessionId, cwd);
|
|
218
|
+
const promptIndex = records.indexOf(prompt);
|
|
219
|
+
const nextHumanPromptIndex = records.findIndex((record, index) => index > promptIndex &&
|
|
220
|
+
record.type === "user" &&
|
|
221
|
+
isRecord(record.message) &&
|
|
222
|
+
record.message.role === "user" &&
|
|
223
|
+
record.isSidechain !== true &&
|
|
224
|
+
nonEmptyString(record.agentId) === undefined &&
|
|
225
|
+
userPromptText(record) !== undefined);
|
|
226
|
+
const turnRecords = records.slice(promptIndex, nextHumanPromptIndex < 0 ? records.length : nextHumanPromptIndex);
|
|
227
|
+
const recordsByUuid = new Map();
|
|
228
|
+
for (const record of turnRecords) {
|
|
229
|
+
const uuid = uuidValue(record.uuid);
|
|
230
|
+
if (!uuid) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (recordsByUuid.has(uuid)) {
|
|
234
|
+
throw new Error("Claude transcript contains a duplicate record UUID");
|
|
235
|
+
}
|
|
236
|
+
recordsByUuid.set(uuid, record);
|
|
237
|
+
}
|
|
238
|
+
assertParentsPrecedeChildren(turnRecords, recordsByUuid);
|
|
239
|
+
const descendants = turnRecords.filter((record) => uuidValue(record.uuid) !== undefined &&
|
|
240
|
+
descendantChain(recordsByUuid, promptUuid, record) !== undefined);
|
|
241
|
+
if (descendants.length !== recordsByUuid.size) {
|
|
242
|
+
throw new Error("Claude transcript turn contains an unlinked UUID branch");
|
|
243
|
+
}
|
|
244
|
+
for (const record of descendants) {
|
|
245
|
+
assertSupportedRecord(record, sessionId);
|
|
246
|
+
}
|
|
247
|
+
if (descendants.some((record) => record.isSidechain === true ||
|
|
248
|
+
nonEmptyString(record.agentId) !== undefined ||
|
|
249
|
+
hasUnresolvedBackgroundWork(record))) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
if (hasUnresolvedToolUse(descendants)) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
if (descendants.some(hasBlockingStopSummary)) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
const durations = descendantRecords(turnRecords, recordsByUuid, promptUuid, (record) => record.type === "system" && record.subtype === "turn_duration");
|
|
259
|
+
if (durations.length > 1) {
|
|
260
|
+
throw new Error("Claude transcript turn contains multiple turn_duration records");
|
|
261
|
+
}
|
|
262
|
+
const duration = durations[0];
|
|
263
|
+
if (!duration) {
|
|
264
|
+
const failures = descendantRecords(turnRecords, recordsByUuid, promptUuid, (record) => record.type === "assistant" &&
|
|
265
|
+
record.isApiErrorMessage === true &&
|
|
266
|
+
nonEmptyString(record.error) !== undefined);
|
|
267
|
+
const failure = failures.at(-1);
|
|
268
|
+
const lastDescendant = [...turnRecords].reverse().find((record) => uuidValue(record.uuid) !== undefined &&
|
|
269
|
+
descendantChain(recordsByUuid, promptUuid, record) !== undefined);
|
|
270
|
+
if (!failure || failure !== lastDescendant) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
assertSupportedRecord(failure, sessionId);
|
|
274
|
+
assertSameClaudeVersion(prompt, failure);
|
|
275
|
+
const error = safeErrorCode(failure.error);
|
|
276
|
+
const assistantText = assistantTextForMessage(turnRecords, recordsByUuid, promptUuid, failure);
|
|
277
|
+
return {
|
|
278
|
+
source: "durable",
|
|
279
|
+
outcome: "failure",
|
|
280
|
+
text: boundedRedactedText(assistantText || `Claude Code stopped with ${error}.`),
|
|
281
|
+
timestamp: nonEmptyString(failure.timestamp),
|
|
282
|
+
id: uuidValue(failure.uuid),
|
|
283
|
+
confidence: "high",
|
|
284
|
+
metadata: {
|
|
285
|
+
match: "claude_transcript_api_error",
|
|
286
|
+
session_id: sessionId,
|
|
287
|
+
prompt_uuid: promptUuid,
|
|
288
|
+
error,
|
|
289
|
+
transcript_schema: "claude_code_jsonl_v2",
|
|
290
|
+
transcript_file_id: transcriptFileId(sessionId, fileIdentity)
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
assertSupportedRecord(duration, sessionId);
|
|
295
|
+
if (validTimestampMs(duration.timestamp) === undefined) {
|
|
296
|
+
throw new Error("Claude turn_duration has no valid timestamp");
|
|
297
|
+
}
|
|
298
|
+
const chain = descendantChain(recordsByUuid, promptUuid, duration);
|
|
299
|
+
if (!chain) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
const finalAssistant = [...chain].reverse().find((record) => record.type === "assistant" &&
|
|
303
|
+
isRecord(record.message) &&
|
|
304
|
+
record.message.role === "assistant" &&
|
|
305
|
+
record.message.stop_reason === "end_turn");
|
|
306
|
+
if (!finalAssistant) {
|
|
307
|
+
throw new Error("Claude turn_duration was not linked to an end_turn assistant record");
|
|
308
|
+
}
|
|
309
|
+
assertSupportedRecord(finalAssistant, sessionId);
|
|
310
|
+
assertSameClaudeVersion(prompt, finalAssistant, duration);
|
|
311
|
+
const finalMessage = isRecord(finalAssistant.message)
|
|
312
|
+
? finalAssistant.message
|
|
313
|
+
: undefined;
|
|
314
|
+
const messageId = uuidValue(finalMessage?.id);
|
|
315
|
+
if (!messageId) {
|
|
316
|
+
throw new Error("Claude final assistant message has no stable UUID");
|
|
317
|
+
}
|
|
318
|
+
const assistantText = assistantTextForMessage(turnRecords, recordsByUuid, promptUuid, finalAssistant);
|
|
319
|
+
if (!assistantText) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
const promptId = nonEmptyString(prompt.promptId);
|
|
323
|
+
return {
|
|
324
|
+
source: "durable",
|
|
325
|
+
outcome: "success",
|
|
326
|
+
text: boundedRedactedText(assistantText),
|
|
327
|
+
timestamp: nonEmptyString(duration.timestamp),
|
|
328
|
+
id: uuidValue(duration.uuid),
|
|
329
|
+
confidence: "high",
|
|
330
|
+
metadata: {
|
|
331
|
+
match: "claude_transcript_turn_duration",
|
|
332
|
+
session_id: sessionId,
|
|
333
|
+
prompt_uuid: promptUuid,
|
|
334
|
+
...(promptId ? { prompt_id: promptId } : {}),
|
|
335
|
+
assistant_message_id: messageId,
|
|
336
|
+
claude_version: nonEmptyString(finalAssistant.version),
|
|
337
|
+
transcript_schema: "claude_code_jsonl_v2",
|
|
338
|
+
transcript_file_id: transcriptFileId(sessionId, fileIdentity)
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function transcriptAnchorFromContext(context) {
|
|
343
|
+
if (!isRecord(context)) {
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
const nativeTakeover = isRecord(context.nativeTakeover)
|
|
347
|
+
? context.nativeTakeover
|
|
348
|
+
: undefined;
|
|
349
|
+
const value = nativeTakeover?.claude_transcript_anchor ?? context.claudeTranscriptAnchor;
|
|
350
|
+
if (!isRecord(value)) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
const schemaVersion = Number(value.schema_version);
|
|
354
|
+
const sessionId = nonEmptyString(value.session_id);
|
|
355
|
+
const cwd = nonEmptyString(value.cwd);
|
|
356
|
+
const pid = positiveInteger(value.pid);
|
|
357
|
+
const agentStartedAtMs = positiveInteger(value.agent_started_at_ms);
|
|
358
|
+
const capturedAt = nonEmptyString(value.captured_at);
|
|
359
|
+
const relativePath = nonEmptyString(value.relative_path);
|
|
360
|
+
const offsetBytes = nonNegativeInteger(value.offset_bytes);
|
|
361
|
+
if (schemaVersion !== CLAUDE_TRANSCRIPT_ANCHOR_VERSION ||
|
|
362
|
+
!sessionId ||
|
|
363
|
+
!cwd ||
|
|
364
|
+
pid === undefined ||
|
|
365
|
+
agentStartedAtMs === undefined ||
|
|
366
|
+
!capturedAt ||
|
|
367
|
+
!relativePath ||
|
|
368
|
+
offsetBytes === undefined ||
|
|
369
|
+
typeof value.file_existed !== "boolean") {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
schema_version: CLAUDE_TRANSCRIPT_ANCHOR_VERSION,
|
|
374
|
+
session_id: sessionId,
|
|
375
|
+
cwd,
|
|
376
|
+
pid,
|
|
377
|
+
agent_started_at_ms: agentStartedAtMs,
|
|
378
|
+
captured_at: capturedAt,
|
|
379
|
+
relative_path: relativePath,
|
|
380
|
+
offset_bytes: offsetBytes,
|
|
381
|
+
file_existed: value.file_existed,
|
|
382
|
+
...(nonEmptyString(value.device) ? { device: nonEmptyString(value.device) } : {}),
|
|
383
|
+
...(nonEmptyString(value.inode) ? { inode: nonEmptyString(value.inode) } : {})
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function openAnchoredTranscript(projectsRoot, anchor) {
|
|
387
|
+
if (path.basename(anchor.relative_path) !== `${anchor.session_id}.jsonl`) {
|
|
388
|
+
throw new Error("Claude transcript anchor filename does not match its session");
|
|
389
|
+
}
|
|
390
|
+
const candidates = locateTranscriptCandidates(projectsRoot, anchor.session_id);
|
|
391
|
+
if (candidates.length > 1) {
|
|
392
|
+
for (const candidate of candidates) {
|
|
393
|
+
fs.closeSync(candidate.fd);
|
|
394
|
+
}
|
|
395
|
+
throw new Error("multiple local Claude transcripts matched the active session");
|
|
396
|
+
}
|
|
397
|
+
const candidate = candidates[0];
|
|
398
|
+
if (candidate) {
|
|
399
|
+
if (anchor.file_existed && candidate.relativePath !== anchor.relative_path) {
|
|
400
|
+
fs.closeSync(candidate.fd);
|
|
401
|
+
throw new Error("Claude transcript moved after the managed send");
|
|
402
|
+
}
|
|
403
|
+
return candidate;
|
|
404
|
+
}
|
|
405
|
+
if (anchor.file_existed) {
|
|
406
|
+
throw new Error("Claude transcript disappeared after the managed send");
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
function locateTranscript(projectsRoot, sessionId) {
|
|
411
|
+
const candidates = locateTranscriptCandidates(projectsRoot, sessionId);
|
|
412
|
+
if (candidates.length > 1) {
|
|
413
|
+
for (const candidate of candidates) {
|
|
414
|
+
fs.closeSync(candidate.fd);
|
|
415
|
+
}
|
|
416
|
+
throw new Error("multiple local Claude transcripts matched the active session");
|
|
417
|
+
}
|
|
418
|
+
return candidates[0];
|
|
419
|
+
}
|
|
420
|
+
function locateTranscriptCandidates(projectsRoot, sessionId) {
|
|
421
|
+
const candidates = [];
|
|
422
|
+
for (const entry of fs.readdirSync(projectsRoot, { withFileTypes: true })) {
|
|
423
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const relativePath = path.join(entry.name, `${sessionId}.jsonl`);
|
|
427
|
+
const candidate = openRelativeTranscript(projectsRoot, relativePath);
|
|
428
|
+
if (candidate) {
|
|
429
|
+
candidates.push(candidate);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return candidates;
|
|
433
|
+
}
|
|
434
|
+
function openRelativeTranscript(projectsRoot, relativePath) {
|
|
435
|
+
const segments = relativePath.split(/[\\/]/u);
|
|
436
|
+
if (segments.length !== 2 ||
|
|
437
|
+
!segments[0] ||
|
|
438
|
+
segments[0] === "." ||
|
|
439
|
+
segments[0] === ".." ||
|
|
440
|
+
!CLAUDE_SESSION_ID_PATTERN.test(path.basename(segments[1], ".jsonl")) ||
|
|
441
|
+
segments[1] !== `${path.basename(segments[1], ".jsonl")}.jsonl`) {
|
|
442
|
+
throw new Error("Claude transcript anchor contains an invalid relative path");
|
|
443
|
+
}
|
|
444
|
+
const projectDirectory = path.join(projectsRoot, segments[0]);
|
|
445
|
+
const directoryStat = lstatOrUndefined(projectDirectory);
|
|
446
|
+
if (!directoryStat || !directoryStat.isDirectory() || directoryStat.isSymbolicLink()) {
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
const transcriptPath = path.join(projectDirectory, segments[1]);
|
|
450
|
+
const fileStat = lstatOrUndefined(transcriptPath);
|
|
451
|
+
if (!fileStat) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
|
|
455
|
+
throw new Error("Claude transcript must be a non-symlink regular file");
|
|
456
|
+
}
|
|
457
|
+
const fd = fs.openSync(transcriptPath, fs.constants.O_RDONLY | NO_FOLLOW_FLAG);
|
|
458
|
+
try {
|
|
459
|
+
const stat = fs.fstatSync(fd);
|
|
460
|
+
if (!stat.isFile()) {
|
|
461
|
+
throw new Error("Claude transcript must be a regular file");
|
|
462
|
+
}
|
|
463
|
+
assertPrivateTranscriptFile(stat);
|
|
464
|
+
return {
|
|
465
|
+
fd,
|
|
466
|
+
stat,
|
|
467
|
+
relativePath: path.join(segments[0], segments[1])
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
fs.closeSync(fd);
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function readCompleteJsonlRecords(fd, offset, length) {
|
|
476
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
477
|
+
let readTotal = 0;
|
|
478
|
+
while (readTotal < length) {
|
|
479
|
+
const bytesRead = fs.readSync(fd, buffer, readTotal, length - readTotal, offset + readTotal);
|
|
480
|
+
if (bytesRead === 0) {
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
readTotal += bytesRead;
|
|
484
|
+
}
|
|
485
|
+
if (readTotal !== length) {
|
|
486
|
+
throw new Error("Claude transcript changed while it was being read");
|
|
487
|
+
}
|
|
488
|
+
if (buffer.length === 0 || buffer[buffer.length - 1] !== 0x0a) {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
const text = buffer.subarray(0, buffer.length - 1).toString("utf8");
|
|
492
|
+
const records = [];
|
|
493
|
+
for (const line of text.split("\n")) {
|
|
494
|
+
if (!line.trim()) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
let parsed;
|
|
498
|
+
try {
|
|
499
|
+
parsed = JSON.parse(line);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
throw new Error("Claude transcript contains an invalid complete JSONL record");
|
|
503
|
+
}
|
|
504
|
+
if (!isRecord(parsed)) {
|
|
505
|
+
throw new Error("Claude transcript contains a non-object JSONL record");
|
|
506
|
+
}
|
|
507
|
+
records.push(parsed);
|
|
508
|
+
}
|
|
509
|
+
return records;
|
|
510
|
+
}
|
|
511
|
+
function descendantRecords(records, recordsByUuid, ancestorUuid, predicate) {
|
|
512
|
+
return records.filter((record) => predicate(record) &&
|
|
513
|
+
descendantChain(recordsByUuid, ancestorUuid, record) !== undefined);
|
|
514
|
+
}
|
|
515
|
+
function assertParentsPrecedeChildren(records, recordsByUuid) {
|
|
516
|
+
const indexes = new Map(records.map((record, index) => [record, index]));
|
|
517
|
+
for (const record of records) {
|
|
518
|
+
const parentUuid = uuidValue(record.parentUuid);
|
|
519
|
+
const parent = parentUuid ? recordsByUuid.get(parentUuid) : undefined;
|
|
520
|
+
if (parent && (indexes.get(parent) ?? Number.POSITIVE_INFINITY) >=
|
|
521
|
+
(indexes.get(record) ?? Number.NEGATIVE_INFINITY)) {
|
|
522
|
+
throw new Error("Claude transcript parent UUID does not precede its child record");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function descendantChain(recordsByUuid, ancestorUuid, descendant) {
|
|
527
|
+
const reversed = [];
|
|
528
|
+
let current = descendant;
|
|
529
|
+
const visited = new Set();
|
|
530
|
+
while (current) {
|
|
531
|
+
const currentUuid = uuidValue(current.uuid);
|
|
532
|
+
if (!currentUuid || visited.has(currentUuid)) {
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
535
|
+
visited.add(currentUuid);
|
|
536
|
+
reversed.push(current);
|
|
537
|
+
if (currentUuid === ancestorUuid) {
|
|
538
|
+
return reversed.reverse();
|
|
539
|
+
}
|
|
540
|
+
const parentUuid = uuidValue(current.parentUuid);
|
|
541
|
+
current = parentUuid ? recordsByUuid.get(parentUuid) : undefined;
|
|
542
|
+
}
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
function assistantTextForMessage(records, recordsByUuid, promptUuid, finalAssistant) {
|
|
546
|
+
const message = isRecord(finalAssistant.message) ? finalAssistant.message : undefined;
|
|
547
|
+
const messageId = nonEmptyString(message?.id);
|
|
548
|
+
if (!messageId) {
|
|
549
|
+
return textFromAssistantRecord(finalAssistant);
|
|
550
|
+
}
|
|
551
|
+
const parts = records.flatMap((record) => {
|
|
552
|
+
const candidateMessage = isRecord(record.message) ? record.message : undefined;
|
|
553
|
+
if (record.type !== "assistant" ||
|
|
554
|
+
candidateMessage?.role !== "assistant" ||
|
|
555
|
+
candidateMessage.id !== messageId ||
|
|
556
|
+
descendantChain(recordsByUuid, promptUuid, record) === undefined) {
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
const text = textFromAssistantRecord(record);
|
|
560
|
+
return text ? [text] : [];
|
|
561
|
+
});
|
|
562
|
+
const joined = parts.join("\n").trim();
|
|
563
|
+
return joined || undefined;
|
|
564
|
+
}
|
|
565
|
+
function textFromAssistantRecord(record) {
|
|
566
|
+
const message = isRecord(record.message) ? record.message : undefined;
|
|
567
|
+
const content = message?.content;
|
|
568
|
+
if (typeof content === "string") {
|
|
569
|
+
return content.trim() || undefined;
|
|
570
|
+
}
|
|
571
|
+
if (!Array.isArray(content)) {
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
574
|
+
const text = content.flatMap((block) => isRecord(block) && block.type === "text" && typeof block.text === "string"
|
|
575
|
+
? [block.text]
|
|
576
|
+
: []).join("\n").trim();
|
|
577
|
+
return text || undefined;
|
|
578
|
+
}
|
|
579
|
+
function userPromptText(record) {
|
|
580
|
+
const message = isRecord(record.message) ? record.message : undefined;
|
|
581
|
+
const content = message?.content;
|
|
582
|
+
return typeof content === "string" ? content : undefined;
|
|
583
|
+
}
|
|
584
|
+
function hasUnresolvedBackgroundWork(record) {
|
|
585
|
+
if (record.type === "assistant" &&
|
|
586
|
+
isRecord(record.message) &&
|
|
587
|
+
Array.isArray(record.message.content)) {
|
|
588
|
+
for (const block of record.message.content) {
|
|
589
|
+
if (!isRecord(block) || block.type !== "tool_use") {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const toolName = nonEmptyString(block.name)?.toLowerCase();
|
|
593
|
+
if (toolName?.startsWith("cron") ||
|
|
594
|
+
toolName === "agent" ||
|
|
595
|
+
toolName === "sendmessage" ||
|
|
596
|
+
structuredBoolean(block.input, ["run_in_background", "runInBackground"]) === true) {
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const toolUseResult = isRecord(record.toolUseResult)
|
|
602
|
+
? record.toolUseResult
|
|
603
|
+
: undefined;
|
|
604
|
+
const backgroundStatus = nonEmptyString(toolUseResult?.status);
|
|
605
|
+
return nonEmptyString(toolUseResult?.backgroundTaskId) !== undefined ||
|
|
606
|
+
structuredBoolean(toolUseResult, ["isAsync"]) === true ||
|
|
607
|
+
(backgroundStatus !== undefined &&
|
|
608
|
+
["async_launched", "remote_launched", "teammate_spawned"].includes(backgroundStatus)) ||
|
|
609
|
+
structuredBoolean(toolUseResult, [
|
|
610
|
+
"run_in_background",
|
|
611
|
+
"runInBackground",
|
|
612
|
+
"is_background",
|
|
613
|
+
"isBackground",
|
|
614
|
+
"backgroundedByUser",
|
|
615
|
+
"assistantAutoBackgrounded"
|
|
616
|
+
]) === true;
|
|
617
|
+
}
|
|
618
|
+
function hasUnresolvedToolUse(records) {
|
|
619
|
+
const toolUses = new Map();
|
|
620
|
+
const toolResults = new Map();
|
|
621
|
+
let malformed = false;
|
|
622
|
+
for (const record of records) {
|
|
623
|
+
const message = isRecord(record.message) ? record.message : undefined;
|
|
624
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
625
|
+
for (const block of content) {
|
|
626
|
+
if (!isRecord(block)) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (block.type === "tool_use") {
|
|
630
|
+
const id = nonEmptyString(block.id);
|
|
631
|
+
if (id) {
|
|
632
|
+
const existing = toolUses.get(id);
|
|
633
|
+
toolUses.set(id, {
|
|
634
|
+
count: (existing?.count ?? 0) + 1,
|
|
635
|
+
ownerUuid: uuidValue(record.uuid)
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
malformed = true;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (block.type === "tool_result") {
|
|
643
|
+
const id = nonEmptyString(block.tool_use_id);
|
|
644
|
+
if (id) {
|
|
645
|
+
const existing = toolResults.get(id);
|
|
646
|
+
toolResults.set(id, {
|
|
647
|
+
count: (existing?.count ?? 0) + 1,
|
|
648
|
+
parentUuid: uuidValue(record.parentUuid),
|
|
649
|
+
sourceAssistantUuid: uuidValue(record.sourceToolAssistantUUID)
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
malformed = true;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return malformed ||
|
|
659
|
+
[...toolUses].some(([id, toolUse]) => {
|
|
660
|
+
const result = toolResults.get(id);
|
|
661
|
+
return toolUse.count !== 1 ||
|
|
662
|
+
result?.count !== 1 ||
|
|
663
|
+
!toolUse.ownerUuid ||
|
|
664
|
+
result.parentUuid !== toolUse.ownerUuid ||
|
|
665
|
+
result.sourceAssistantUuid !== toolUse.ownerUuid;
|
|
666
|
+
}) ||
|
|
667
|
+
[...toolResults].some(([id, result]) => result.count !== 1 || toolUses.get(id)?.count !== 1);
|
|
668
|
+
}
|
|
669
|
+
function hasBlockingStopSummary(record) {
|
|
670
|
+
return record.type === "system" &&
|
|
671
|
+
record.subtype === "stop_hook_summary" &&
|
|
672
|
+
(record.preventedContinuation === true ||
|
|
673
|
+
(Array.isArray(record.hookErrors) && record.hookErrors.length > 0));
|
|
674
|
+
}
|
|
675
|
+
function structuredBoolean(value, keys) {
|
|
676
|
+
if (!isRecord(value)) {
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
for (const key of keys) {
|
|
680
|
+
if (typeof value[key] === "boolean") {
|
|
681
|
+
return value[key];
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
function assertSupportedRecord(record, sessionId, cwd) {
|
|
687
|
+
if (record.sessionId !== sessionId ||
|
|
688
|
+
nonEmptyString(record.cwd) === undefined ||
|
|
689
|
+
(cwd !== undefined && normalizePath(record.cwd) !== normalizePath(cwd)) ||
|
|
690
|
+
record.isSidechain !== false ||
|
|
691
|
+
record.entrypoint !== "cli" ||
|
|
692
|
+
record.version !== SUPPORTED_CLAUDE_TRANSCRIPT_VERSION) {
|
|
693
|
+
throw new Error("Claude transcript completion record uses an unsupported schema or identity");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function assertSameClaudeVersion(...records) {
|
|
697
|
+
const versions = new Set(records.map((record) => nonEmptyString(record.version)));
|
|
698
|
+
if (versions.size !== 1 || versions.has(undefined)) {
|
|
699
|
+
throw new Error("Claude transcript turn changed schema versions while it was running");
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function exactInteractiveAgent(rows, pid) {
|
|
703
|
+
const matches = rows.filter((row) => row.pid === pid && (row.kind === undefined || row.kind === "interactive"));
|
|
704
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
705
|
+
}
|
|
706
|
+
function runtimePidFromContext(context) {
|
|
707
|
+
if (!isRecord(context)) {
|
|
708
|
+
return undefined;
|
|
709
|
+
}
|
|
710
|
+
return positiveInteger(context.pid);
|
|
711
|
+
}
|
|
712
|
+
function expectedTranscriptRelativePath(sessionId, cwd) {
|
|
713
|
+
return path.join(cwd.replace(/[^A-Za-z0-9]/gu, "-"), `${sessionId}.jsonl`);
|
|
714
|
+
}
|
|
715
|
+
function projectsRootPath(claudeHome) {
|
|
716
|
+
return path.join(claudeHome, "projects");
|
|
717
|
+
}
|
|
718
|
+
function fileEndsWithNewline(fd, size) {
|
|
719
|
+
const buffer = Buffer.allocUnsafe(1);
|
|
720
|
+
return fs.readSync(fd, buffer, 0, 1, size - 1) === 1 && buffer[0] === 0x0a;
|
|
721
|
+
}
|
|
722
|
+
function isRealDirectory(value) {
|
|
723
|
+
const stat = lstatOrUndefined(value);
|
|
724
|
+
return Boolean(stat?.isDirectory() && !stat.isSymbolicLink());
|
|
725
|
+
}
|
|
726
|
+
function lstatOrUndefined(value) {
|
|
727
|
+
try {
|
|
728
|
+
return fs.lstatSync(value);
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
if (isRecord(error) && error.code === "ENOENT") {
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function assertPrivateTranscriptFile(stat) {
|
|
738
|
+
if (process.platform === "win32") {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const getuid = process.getuid;
|
|
742
|
+
if (typeof getuid === "function" && stat.uid !== getuid.call(process)) {
|
|
743
|
+
throw new Error("Claude transcript is not owned by the current user");
|
|
744
|
+
}
|
|
745
|
+
if ((stat.mode & 0o077) !== 0) {
|
|
746
|
+
throw new Error("Claude transcript permissions are broader than owner-only");
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function safeErrorCode(value) {
|
|
750
|
+
const error = nonEmptyString(value);
|
|
751
|
+
return error && /^[A-Za-z0-9_.:-]{1,80}$/u.test(error)
|
|
752
|
+
? error
|
|
753
|
+
: "claude_api_error";
|
|
754
|
+
}
|
|
755
|
+
function requestFingerprint(value) {
|
|
756
|
+
const text = String(value ?? "").replace(/\s+/gu, " ").trim();
|
|
757
|
+
return text ? createHash("sha256").update(text).digest("hex") : undefined;
|
|
758
|
+
}
|
|
759
|
+
function exactPromptText(value) {
|
|
760
|
+
const text = String(value ?? "")
|
|
761
|
+
.replace(/\r\n/gu, "\n")
|
|
762
|
+
.replace(/[\r\n]+$/u, "");
|
|
763
|
+
return text.length > 0 ? text : undefined;
|
|
764
|
+
}
|
|
765
|
+
function transcriptFileId(sessionId, fileIdentity) {
|
|
766
|
+
return createHash("sha256")
|
|
767
|
+
.update(`${sessionId}\0${fileIdentity}`)
|
|
768
|
+
.digest("hex")
|
|
769
|
+
.slice(0, 24);
|
|
770
|
+
}
|
|
771
|
+
function boundedRedactedText(value) {
|
|
772
|
+
return redactString(value).trim().slice(0, 4000);
|
|
773
|
+
}
|
|
774
|
+
function normalizePath(value) {
|
|
775
|
+
const text = nonEmptyString(value);
|
|
776
|
+
return text ? path.resolve(text) : undefined;
|
|
777
|
+
}
|
|
778
|
+
function validTimestampMs(value) {
|
|
779
|
+
const timestamp = nonEmptyString(value);
|
|
780
|
+
if (!timestamp) {
|
|
781
|
+
return undefined;
|
|
782
|
+
}
|
|
783
|
+
const parsed = Date.parse(timestamp);
|
|
784
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
785
|
+
}
|
|
786
|
+
function positiveInteger(value) {
|
|
787
|
+
const parsed = Number(value);
|
|
788
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
789
|
+
}
|
|
790
|
+
function nonNegativeInteger(value) {
|
|
791
|
+
const parsed = Number(value);
|
|
792
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined;
|
|
793
|
+
}
|
|
794
|
+
function uuidValue(value) {
|
|
795
|
+
const text = nonEmptyString(value);
|
|
796
|
+
return text && CLAUDE_SESSION_ID_PATTERN.test(text) ? text : undefined;
|
|
797
|
+
}
|
|
798
|
+
function nonEmptyString(value) {
|
|
799
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
800
|
+
}
|
|
801
|
+
function isRecord(value) {
|
|
802
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
803
|
+
}
|
|
804
|
+
//# sourceMappingURL=claude-local-transcript-provider.js.map
|