coolhand-cli 0.12.0 → 0.13.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/README.md +6 -1
- package/dist/api/help-center-client.d.ts.map +1 -1
- package/dist/api/help-center-client.js +5 -4
- package/dist/api/help-center-client.js.map +1 -1
- package/dist/auto-update/index.d.ts +61 -0
- package/dist/auto-update/index.d.ts.map +1 -0
- package/dist/auto-update/index.js +355 -0
- package/dist/auto-update/index.js.map +1 -0
- package/dist/auto-update/select.d.ts +28 -0
- package/dist/auto-update/select.d.ts.map +1 -0
- package/dist/auto-update/select.js +45 -0
- package/dist/auto-update/select.js.map +1 -0
- package/dist/auto-update/state.d.ts +25 -0
- package/dist/auto-update/state.d.ts.map +1 -0
- package/dist/auto-update/state.js +81 -0
- package/dist/auto-update/state.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +97 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/capture-turn.d.ts +18 -0
- package/dist/commands/capture-turn.d.ts.map +1 -0
- package/dist/commands/capture-turn.js +163 -0
- package/dist/commands/capture-turn.js.map +1 -0
- package/dist/commands/download-client-file.js +1 -1
- package/dist/commands/download-client-file.js.map +1 -1
- package/dist/commands/flush-pending.d.ts.map +1 -1
- package/dist/commands/flush-pending.js +1 -30
- package/dist/commands/flush-pending.js.map +1 -1
- package/dist/commands/logout.js +2 -2
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/update.d.ts +9 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +63 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +25 -7
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/hooks/adapters/claude.d.ts +12 -0
- package/dist/hooks/adapters/claude.d.ts.map +1 -0
- package/dist/hooks/adapters/claude.js +98 -0
- package/dist/hooks/adapters/claude.js.map +1 -0
- package/dist/hooks/adapters/copilot.d.ts +16 -0
- package/dist/hooks/adapters/copilot.d.ts.map +1 -0
- package/dist/hooks/adapters/copilot.js +115 -0
- package/dist/hooks/adapters/copilot.js.map +1 -0
- package/dist/hooks/adapters/index.d.ts +5 -0
- package/dist/hooks/adapters/index.d.ts.map +1 -0
- package/dist/hooks/adapters/index.js +8 -0
- package/dist/hooks/adapters/index.js.map +1 -0
- package/dist/hooks/adapters/shared.d.ts +32 -0
- package/dist/hooks/adapters/shared.d.ts.map +1 -0
- package/dist/hooks/adapters/shared.js +45 -0
- package/dist/hooks/adapters/shared.js.map +1 -0
- package/dist/hooks/disabled.d.ts +3 -0
- package/dist/hooks/disabled.d.ts.map +1 -0
- package/dist/hooks/disabled.js +5 -0
- package/dist/hooks/disabled.js.map +1 -0
- package/dist/hooks/hook-log.d.ts +7 -0
- package/dist/hooks/hook-log.d.ts.map +1 -0
- package/dist/hooks/hook-log.js +34 -0
- package/dist/hooks/hook-log.js.map +1 -0
- package/dist/hooks/identity.d.ts +21 -0
- package/dist/hooks/identity.d.ts.map +1 -0
- package/dist/hooks/identity.js +121 -0
- package/dist/hooks/identity.js.map +1 -0
- package/dist/hooks/log-turn.d.ts +23 -0
- package/dist/hooks/log-turn.d.ts.map +1 -0
- package/dist/hooks/log-turn.js +48 -0
- package/dist/hooks/log-turn.js.map +1 -0
- package/dist/hooks/send-turn.d.ts +32 -0
- package/dist/hooks/send-turn.d.ts.map +1 -0
- package/dist/hooks/send-turn.js +305 -0
- package/dist/hooks/send-turn.js.map +1 -0
- package/dist/hooks/silence.d.ts +7 -0
- package/dist/hooks/silence.d.ts.map +1 -0
- package/dist/hooks/silence.js +22 -0
- package/dist/hooks/silence.js.map +1 -0
- package/dist/hooks/stdin.d.ts +11 -0
- package/dist/hooks/stdin.d.ts.map +1 -0
- package/dist/hooks/stdin.js +51 -0
- package/dist/hooks/stdin.js.map +1 -0
- package/dist/hooks/turn-state.d.ts +40 -0
- package/dist/hooks/turn-state.d.ts.map +1 -0
- package/dist/hooks/turn-state.js +97 -0
- package/dist/hooks/turn-state.js.map +1 -0
- package/dist/hooks/types.d.ts +76 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +3 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/pending-store.d.ts +22 -2
- package/dist/pending-store.d.ts.map +1 -1
- package/dist/pending-store.js +25 -3
- package/dist/pending-store.js.map +1 -1
- package/dist/pending-upload.d.ts +12 -0
- package/dist/pending-upload.d.ts.map +1 -0
- package/dist/pending-upload.js +51 -0
- package/dist/pending-upload.js.map +1 -0
- package/dist/proxy/interceptor.d.ts.map +1 -1
- package/dist/proxy/interceptor.js +1 -0
- package/dist/proxy/interceptor.js.map +1 -1
- package/dist/sessions/blocks.d.ts +51 -0
- package/dist/sessions/blocks.d.ts.map +1 -0
- package/dist/sessions/blocks.js +193 -0
- package/dist/sessions/blocks.js.map +1 -0
- package/dist/sessions/claude-scanner.d.ts +60 -1
- package/dist/sessions/claude-scanner.d.ts.map +1 -1
- package/dist/sessions/claude-scanner.js +277 -1
- package/dist/sessions/claude-scanner.js.map +1 -1
- package/dist/sessions/copilot-scanner.d.ts +27 -0
- package/dist/sessions/copilot-scanner.d.ts.map +1 -0
- package/dist/sessions/copilot-scanner.js +171 -0
- package/dist/sessions/copilot-scanner.js.map +1 -0
- package/dist/sessions/redact-secrets.d.ts.map +1 -1
- package/dist/sessions/redact-secrets.js +43 -10
- package/dist/sessions/redact-secrets.js.map +1 -1
- package/dist/sessions/turn-envelope.d.ts +62 -0
- package/dist/sessions/turn-envelope.d.ts.map +1 -0
- package/dist/sessions/turn-envelope.js +182 -0
- package/dist/sessions/turn-envelope.js.map +1 -0
- package/dist/sessions/usage.d.ts +23 -0
- package/dist/sessions/usage.d.ts.map +1 -0
- package/dist/sessions/usage.js +56 -0
- package/dist/sessions/usage.js.map +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { redactSecrets } from './redact-secrets.js';
|
|
2
|
+
/** Per-block caps. Redaction runs before truncation, so a secret is never cut in half and left half-visible. */
|
|
3
|
+
export const MAX_TURN_TEXT_CHARS = 100000;
|
|
4
|
+
const MAX_THINKING_CHARS = 4000;
|
|
5
|
+
const MAX_TOOL_STRING_CHARS = 2000;
|
|
6
|
+
const MAX_TOOL_RESULT_CHARS = 2000;
|
|
7
|
+
/**
|
|
8
|
+
* Redaction is run on at most `max + REDACT_EXTRA_CHARS` of a string (it is quadratic on unterminated PEM
|
|
9
|
+
* blocks, and the rest would be cut off anyway). When a string is longer, the last `REDACT_TAIL_CHARS` of what
|
|
10
|
+
* was redacted are dropped too: a secret cut by the end of the window is not matched, and must not be left
|
|
11
|
+
* half-visible.
|
|
12
|
+
*/
|
|
13
|
+
const REDACT_EXTRA_CHARS = 50000;
|
|
14
|
+
const REDACT_TAIL_CHARS = 8000;
|
|
15
|
+
const MAX_KEY_CHARS = 200;
|
|
16
|
+
const MAX_INPUT_ARRAY_ITEMS = 50;
|
|
17
|
+
const MAX_INPUT_KEYS = 50;
|
|
18
|
+
const MAX_INPUT_DEPTH = 6;
|
|
19
|
+
const TRUNCATED = '…[truncated]';
|
|
20
|
+
const REDACTED = '[REDACTED]';
|
|
21
|
+
/** A numeric field with one of these names is a count or a limit (`max_tokens`), not a secret. */
|
|
22
|
+
const COUNT_KEY = /tokens|limit|count|length|size/i;
|
|
23
|
+
/** A command-line flag that takes a secret as its next argument (`['--password', 'hunter2']`). */
|
|
24
|
+
const SECRET_FLAG = /^--?(?:(?:[a-z]{1,20}-){0,3}(?:password|passwd|token|secret|api-?key|auth(?:orization)?|access-?key|private-?key)(?:-(?:key|value))?|pass|pwd)$/i;
|
|
25
|
+
/**
|
|
26
|
+
* Names of tool-input fields whose value is a secret whatever it looks like. `redactSecrets` finds
|
|
27
|
+
* `password=...` in text, but a structured input (`{"password": "hunter2"}`) has the key and the value apart.
|
|
28
|
+
*/
|
|
29
|
+
const SECRET_KEY = /api[_-]?key|secret(?!ary)|token(?!izer)|passw(?:or)?d|pwd|(?<![a-z])pass(?![a-z])|passphrase|access[_-]?key|private[_-]?key|ssh[_-]?key|auth(?!ors?(?![a-z])|ored|ority)|bearer|jwt|dsn|connection[_-]?string|credential|signing[_-]?key|encryption[_-]?key|cookie/i;
|
|
30
|
+
/** Redact, then cap (`redact: false` only measures what would be kept). The marker says text was cut. */
|
|
31
|
+
function clean(text, max, redact = true) {
|
|
32
|
+
if (!redact) {
|
|
33
|
+
const kept = text.slice(0, max).trim();
|
|
34
|
+
return text.length > max ? `${kept}${TRUNCATED}` : kept;
|
|
35
|
+
}
|
|
36
|
+
const window = max + REDACT_EXTRA_CHARS;
|
|
37
|
+
const windowed = text.length > window;
|
|
38
|
+
const body = redactSecrets(windowed ? text.slice(0, window) : text).trim();
|
|
39
|
+
if (!windowed && body.length <= max) {
|
|
40
|
+
return body;
|
|
41
|
+
}
|
|
42
|
+
const keep = windowed ? Math.min(max, body.length - REDACT_TAIL_CHARS) : max;
|
|
43
|
+
return `${body.slice(0, Math.max(keep, 0))}${TRUNCATED}`;
|
|
44
|
+
}
|
|
45
|
+
/** Redacted and capped to `max` characters, with a marker when cut: the one way any captured text is sanitized. */
|
|
46
|
+
export function cleanText(text, max = MAX_TURN_TEXT_CHARS) {
|
|
47
|
+
return clean(text, max);
|
|
48
|
+
}
|
|
49
|
+
/** Redact and cap the strings of a tool call's input, keeping its shape; bound arrays, keys and depth. */
|
|
50
|
+
export function sanitizeInput(value, depth = 0, secret = false, redact = true) {
|
|
51
|
+
if (typeof value === 'string') {
|
|
52
|
+
return secret && redact ? REDACTED : clean(value, MAX_TOOL_STRING_CHARS, redact);
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === 'number') {
|
|
55
|
+
return secret && redact ? REDACTED : value;
|
|
56
|
+
}
|
|
57
|
+
if (value === null || typeof value === 'boolean') {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
if (depth >= MAX_INPUT_DEPTH) {
|
|
61
|
+
return TRUNCATED;
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
// The element after a secret-named flag (`['--password', 'hunter2']`) is the secret.
|
|
65
|
+
return value
|
|
66
|
+
.slice(0, MAX_INPUT_ARRAY_ITEMS)
|
|
67
|
+
.map((item, i, items) => sanitizeInput(item, depth + 1, secret || (i > 0 && typeof items[i - 1] === 'string' && SECRET_FLAG.test(items[i - 1])), redact));
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === 'object') {
|
|
70
|
+
const record = value;
|
|
71
|
+
// A {name, value} pair (an HTTP header, an env var) names the secret in one field and holds it in the other.
|
|
72
|
+
const namesSecret = ['name', 'key', 'header', 'variable'].some((k) => typeof record[k] === 'string' && SECRET_KEY.test(record[k]));
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const [key, inner] of Object.entries(record).slice(0, MAX_INPUT_KEYS)) {
|
|
75
|
+
const name = clean(key, MAX_KEY_CHARS, redact);
|
|
76
|
+
if (name === '__proto__' || name === 'constructor' || name === 'prototype') {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
out[name] = sanitizeInput(inner, depth + 1, secret || (SECRET_KEY.test(key) && !(typeof inner === 'number' && COUNT_KEY.test(key))) || (namesSecret && (key === 'value' || key === 'val')), redact);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/** A tool result's content is a string or an array of blocks: the text of it, with images as a placeholder. */
|
|
86
|
+
function resultText(content) {
|
|
87
|
+
if (typeof content === 'string') {
|
|
88
|
+
return content;
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(content)) {
|
|
91
|
+
return content
|
|
92
|
+
.map((b) => {
|
|
93
|
+
if (!b || typeof b !== 'object') {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
const type = b.type;
|
|
97
|
+
return type === 'text' ? String(b.text ?? '') : type === 'image' ? '[image]' : '';
|
|
98
|
+
})
|
|
99
|
+
.filter((s) => s.length > 0)
|
|
100
|
+
.join('\n');
|
|
101
|
+
}
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* One transcript block as it is sent: redacted, capped, and stripped of anything opaque. Thinking keeps
|
|
106
|
+
* its text but not its `signature` (a long blob of no use to a reader); redacted/encrypted thinking, and
|
|
107
|
+
* block types we don't know, are left out.
|
|
108
|
+
*/
|
|
109
|
+
export function sanitizeBlock(block) {
|
|
110
|
+
if (!block || typeof block !== 'object') {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const b = block;
|
|
114
|
+
switch (b.type) {
|
|
115
|
+
case 'text': {
|
|
116
|
+
const text = typeof b.text === 'string' ? clean(b.text, MAX_TURN_TEXT_CHARS) : '';
|
|
117
|
+
return text ? { type: 'text', text } : undefined;
|
|
118
|
+
}
|
|
119
|
+
case 'thinking': {
|
|
120
|
+
const thinking = typeof b.thinking === 'string' ? clean(b.thinking, MAX_THINKING_CHARS) : '';
|
|
121
|
+
return thinking ? { type: 'thinking', thinking } : undefined;
|
|
122
|
+
}
|
|
123
|
+
case 'tool_use': {
|
|
124
|
+
if (typeof b.name !== 'string' || !b.name) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
return { type: 'tool_use', ...(typeof b.id === 'string' ? { id: b.id.slice(0, 200) } : {}), name: b.name.slice(0, 200), input: sanitizeInput(b.input) ?? {} };
|
|
128
|
+
}
|
|
129
|
+
case 'tool_result':
|
|
130
|
+
return {
|
|
131
|
+
type: 'tool_result',
|
|
132
|
+
...(typeof b.tool_use_id === 'string' ? { tool_use_id: b.tool_use_id.slice(0, 200) } : {}),
|
|
133
|
+
content: clean(resultText(b.content), MAX_TOOL_RESULT_CHARS),
|
|
134
|
+
...(b.is_error === true ? { is_error: true } : {}),
|
|
135
|
+
};
|
|
136
|
+
default:
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A transcript message as it is sent. An assistant message that is nothing but one text block is sent as
|
|
142
|
+
* a plain string, like an ordinary chat; anything else keeps its blocks. `undefined` when nothing survives.
|
|
143
|
+
*/
|
|
144
|
+
export function sanitizeMessage(raw) {
|
|
145
|
+
const blocks = raw.blocks.map(sanitizeBlock).filter((b) => b !== undefined);
|
|
146
|
+
if (blocks.length === 0) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
if (blocks.length === 1 && blocks[0].type === 'text') {
|
|
150
|
+
return { role: raw.role, content: blocks[0].text };
|
|
151
|
+
}
|
|
152
|
+
return { role: raw.role, content: blocks };
|
|
153
|
+
}
|
|
154
|
+
function jsonLength(value) {
|
|
155
|
+
try {
|
|
156
|
+
return JSON.stringify(value).length;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return MAX_TOOL_STRING_CHARS;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Close to the characters `sanitizeMessage` would keep, computed without redacting anything. */
|
|
163
|
+
export function messageSize(raw) {
|
|
164
|
+
let size = 0;
|
|
165
|
+
for (const block of raw.blocks) {
|
|
166
|
+
if (!block || typeof block !== 'object') {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const b = block;
|
|
170
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
171
|
+
size += Math.min(b.text.length, MAX_TURN_TEXT_CHARS);
|
|
172
|
+
}
|
|
173
|
+
else if (b.type === 'thinking' && typeof b.thinking === 'string') {
|
|
174
|
+
size += Math.min(b.thinking.length, MAX_THINKING_CHARS);
|
|
175
|
+
}
|
|
176
|
+
else if (b.type === 'tool_use') {
|
|
177
|
+
size += jsonLength(sanitizeInput(b.input ?? {}, 0, false, false)) + 50;
|
|
178
|
+
}
|
|
179
|
+
else if (b.type === 'tool_result') {
|
|
180
|
+
size += Math.min(resultText(b.content).length, MAX_TOOL_RESULT_CHARS) + 50;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return size;
|
|
184
|
+
}
|
|
185
|
+
/** The text of the text blocks of a raw message, joined the way the exchange parser joins them. */
|
|
186
|
+
export function rawMessageText(raw) {
|
|
187
|
+
return raw.blocks
|
|
188
|
+
.map((b) => (b && typeof b === 'object' && b.type === 'text' && typeof b.text === 'string' ? b.text : ''))
|
|
189
|
+
.filter((t) => t.trim().length > 0)
|
|
190
|
+
.join('\n')
|
|
191
|
+
.trim();
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=blocks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocks.js","sourceRoot":"","sources":["../../src/sessions/blocks.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { type ContentBlock, type RawMessage } from './blocks.js';
|
|
2
|
+
import { type SubagentUsage, type Usage } from './usage.js';
|
|
1
3
|
import type { SessionFileMeta } from './session-filter.js';
|
|
2
4
|
/** One message in the reconstructed conversation. */
|
|
3
5
|
export interface ConversationMessage {
|
|
4
6
|
role: string;
|
|
5
|
-
content
|
|
7
|
+
/** A plain string, or content blocks (thinking, tool calls and results) for hook-captured turns. */
|
|
8
|
+
content: string | ContentBlock[];
|
|
6
9
|
}
|
|
7
10
|
/**
|
|
8
11
|
* An Anthropic-shaped envelope for ONE whole Claude Code session (a conversation). The server's
|
|
@@ -18,6 +21,8 @@ export interface CaptureEnvelope {
|
|
|
18
21
|
status_code: number;
|
|
19
22
|
request_body: {
|
|
20
23
|
model?: string;
|
|
24
|
+
/** The skill's text, for hook-captured turns. */
|
|
25
|
+
system?: string;
|
|
21
26
|
messages: ConversationMessage[];
|
|
22
27
|
};
|
|
23
28
|
response_body: {
|
|
@@ -57,6 +62,60 @@ export declare function defaultProjectsDir(): string;
|
|
|
57
62
|
* exactly once, matching benchmark/lib/transcript.mjs.
|
|
58
63
|
*/
|
|
59
64
|
export declare function parseTranscript(content: string, sessionId: string): CaptureEnvelope | null;
|
|
65
|
+
/** The most recent user prompt and the assistant's final reply to it, as plain text. */
|
|
66
|
+
export interface LastTurn {
|
|
67
|
+
userText?: string;
|
|
68
|
+
assistantText?: string;
|
|
69
|
+
model?: string;
|
|
70
|
+
/** The final assistant message's request id: unique per model call, so it tells a repeated
|
|
71
|
+
* identical turn from a hook that fired twice for one turn. */
|
|
72
|
+
assistantKey?: string;
|
|
73
|
+
}
|
|
74
|
+
/** One user prompt and the assistant's final text reply to it, as plain text. */
|
|
75
|
+
export interface Exchange {
|
|
76
|
+
/** Undefined for a prompt with no text of its own (e.g. an image), or an assistant reply with no prompt in view. */
|
|
77
|
+
userText?: string;
|
|
78
|
+
/** When the user line was written (ISO 8601), straight from the transcript. */
|
|
79
|
+
timestamp?: string;
|
|
80
|
+
assistantText?: string;
|
|
81
|
+
/** Request id of the message `assistantText` came from. */
|
|
82
|
+
assistantKey?: string;
|
|
83
|
+
/** Tokens the main-thread assistant messages of this exchange spent (each message counted once). */
|
|
84
|
+
usage?: Usage;
|
|
85
|
+
/** What subagents (Agent tool calls) made during this exchange spent, per model. */
|
|
86
|
+
subagents?: SubagentUsage;
|
|
87
|
+
/**
|
|
88
|
+
* Everything after the user's prompt, in order, as the transcript has it: each model call's blocks
|
|
89
|
+
* (thinking, text, tool calls) merged into one assistant message, and each batch of tool results as a
|
|
90
|
+
* user message. Raw: redacted and capped only when sent (see `sanitizeMessage`).
|
|
91
|
+
*/
|
|
92
|
+
messages?: RawMessage[];
|
|
93
|
+
/** The skill body Claude Code injected after a slash command (`/skill`), if this exchange invoked one. */
|
|
94
|
+
skill?: {
|
|
95
|
+
dir: string;
|
|
96
|
+
text: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export interface ParsedExchanges {
|
|
100
|
+
exchanges: Exchange[];
|
|
101
|
+
/** The latest real model name seen on an assistant line. */
|
|
102
|
+
model?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Walk a transcript (or a window of one — a leading partial line is skipped like any other
|
|
106
|
+
* unparseable line) and return its user→assistant exchanges in order. Each exchange carries its prompt and reply text and
|
|
107
|
+
* its `messages` (raw blocks: thinking, tool calls and results included). Synthetic `isMeta` user lines (injected skill bodies,
|
|
108
|
+
* caveats), compaction summaries, interrupt markers and subagent (sidechain) traffic are not prompts.
|
|
109
|
+
* A typed slash command is recovered as typed. Pure function, never throws.
|
|
110
|
+
*/
|
|
111
|
+
export declare function parseExchanges(content: string): ParsedExchanges;
|
|
112
|
+
/** The latest exchange of already-parsed exchanges, in the shape the Stop-hook adapter wants. */
|
|
113
|
+
export declare function lastTurnFrom(parsed: ParsedExchanges): LastTurn;
|
|
114
|
+
/**
|
|
115
|
+
* Pull the latest user prompt and the final assistant message out of a transcript (or a window of
|
|
116
|
+
* one). Text only, the final reply's blocks aside. Pure function, never throws.
|
|
117
|
+
*/
|
|
118
|
+
export declare function extractLastTurn(content: string): LastTurn;
|
|
60
119
|
/**
|
|
61
120
|
* Find every Claude Code transcript under `projectsDir` (default `~/.claude/projects`) and build one
|
|
62
121
|
* conversation envelope per session. A missing directory simply yields zero sessions.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-scanner.d.ts","sourceRoot":"","sources":["../../src/sessions/claude-scanner.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,qDAAqD;AACrD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"claude-scanner.d.ts","sourceRoot":"","sources":["../../src/sessions/claude-scanner.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,8 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { redactSecrets } from './redact-secrets.js';
|
|
5
|
+
import { sanitizeInput } from './blocks.js';
|
|
6
|
+
import { addSubagent, addUsage, emptySubagentUsage, emptyUsage } from './usage.js';
|
|
5
7
|
/** Cap on a single serialised tool input/output. Edit/Write inputs carry whole file bodies. */
|
|
6
8
|
const MAX_BLOCK_CHARS = 2000;
|
|
7
9
|
export function defaultProjectsDir() {
|
|
@@ -113,7 +115,10 @@ function sanitizeResponseBlocks(blocks) {
|
|
|
113
115
|
if (type === 'thinking' || type === 'redacted_thinking') {
|
|
114
116
|
continue;
|
|
115
117
|
}
|
|
116
|
-
|
|
118
|
+
const redacted = redactDeep(block);
|
|
119
|
+
// A tool call's input needs the key-aware scrub: `redactDeep` only sees each string alone, so
|
|
120
|
+
// `{ password: 'x' }` would pass.
|
|
121
|
+
out.push(type === 'tool_use' && 'input' in redacted ? { ...redacted, input: sanitizeInput(block.input) } : redacted);
|
|
117
122
|
}
|
|
118
123
|
return out;
|
|
119
124
|
}
|
|
@@ -255,6 +260,277 @@ export function parseTranscript(content, sessionId) {
|
|
|
255
260
|
projectPath,
|
|
256
261
|
};
|
|
257
262
|
}
|
|
263
|
+
function isToolResultBlock(block) {
|
|
264
|
+
return !!block && typeof block === 'object' && block.type === 'tool_result';
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Claude Code stores a typed slash command as markup, e.g.
|
|
268
|
+
* <command-message>loop-review</command-message>
|
|
269
|
+
* <command-name>/loop-review</command-name>
|
|
270
|
+
* <command-args>high --max-rounds 3</command-args>
|
|
271
|
+
* (tags in either order, `<command-args>` present but empty when nothing was typed). The prompt the
|
|
272
|
+
* user actually typed is `/loop-review high --max-rounds 3`, so that is what is recorded.
|
|
273
|
+
*/
|
|
274
|
+
function slashCommandPrompt(text) {
|
|
275
|
+
// Claude Code's own line starts with the markup; a prompt that merely quotes it is left alone.
|
|
276
|
+
const head = text.trimStart();
|
|
277
|
+
if (!head.startsWith('<command-name>') && !head.startsWith('<command-message>')) {
|
|
278
|
+
return text;
|
|
279
|
+
}
|
|
280
|
+
const tag = (name) => new RegExp(`<${name}>([\\s\\S]*?)</${name}>`).exec(text)?.[1]?.trim();
|
|
281
|
+
const command = tag('command-name') ?? (tag('command-message') ? `/${tag('command-message')}` : undefined);
|
|
282
|
+
if (!command) {
|
|
283
|
+
return text;
|
|
284
|
+
}
|
|
285
|
+
const args = tag('command-args');
|
|
286
|
+
return args ? `${command} ${args}` : command;
|
|
287
|
+
}
|
|
288
|
+
/** Lines Claude Code writes as user messages that the user didn't type. */
|
|
289
|
+
function isInterruptMarker(text) {
|
|
290
|
+
return text.startsWith('[Request interrupted by user');
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The OUTPUT of a local command (`/compact`, `/context`, `/status`, `/cost`, …) or of bash mode
|
|
294
|
+
* (`!cmd`) is stored as a non-meta user line. It is not something the user typed and can hold account
|
|
295
|
+
* details, paths and tool config, so it is never a prompt and is never uploaded.
|
|
296
|
+
*/
|
|
297
|
+
const COMMAND_OUTPUT_TAGS = ['<local-command-stdout>', '<local-command-stderr>', '<bash-input>', '<bash-stdout>', '<bash-stderr>'];
|
|
298
|
+
function isCommandOutput(text) {
|
|
299
|
+
const head = text.trimStart();
|
|
300
|
+
return COMMAND_OUTPUT_TAGS.some((tag) => head.startsWith(tag));
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Host- and harness-injected context that rides in user lines but was never typed: Conductor's
|
|
304
|
+
* `<system_instruction>`, background-task `<task-notification>`s, `<system-reminder>`s and the IDE
|
|
305
|
+
* extension's `<ide_opened_file>` / `<ide_selection>` blocks. They carry file paths, selected code
|
|
306
|
+
* and workspace details, and a hook that re-uploads the conversation every turn would send them again
|
|
307
|
+
* and again. Removed before anything is recorded.
|
|
308
|
+
*/
|
|
309
|
+
const INJECTED_TAGS = '(?:system_instruction|system-reminder|task-notification|local-command-caveat|user-prompt-submit-hook|ide_[a-z_]+)';
|
|
310
|
+
/**
|
|
311
|
+
* Remove `<tag>…</tag>` elements of the injected kinds above. A single left-to-right pass: for each
|
|
312
|
+
* opening tag it looks once, forwards, for that tag's closer; an opening tag with no closer takes the
|
|
313
|
+
* rest of the text with it (fail closed). Linear in the length of the text — a regex with a lazy
|
|
314
|
+
* `[\s\S]*?` per opener is quadratic on a pasted log full of unclosed tags, and this runs over up to
|
|
315
|
+
* 8 MB of transcript inside a hook with a timeout. It also means a prompt that merely discusses one of
|
|
316
|
+
* these tags loses the rest of that text: the cost of failing closed.
|
|
317
|
+
*/
|
|
318
|
+
function stripInjectedContext(text) {
|
|
319
|
+
const opener = new RegExp(`<(${INJECTED_TAGS})>`, 'g');
|
|
320
|
+
let out = '';
|
|
321
|
+
let from = 0;
|
|
322
|
+
for (;;) {
|
|
323
|
+
opener.lastIndex = from;
|
|
324
|
+
const match = opener.exec(text);
|
|
325
|
+
if (!match) {
|
|
326
|
+
out += text.slice(from);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
out += text.slice(from, match.index);
|
|
330
|
+
const closer = `</${match[1]}>`;
|
|
331
|
+
const end = text.indexOf(closer, match.index + match[0].length);
|
|
332
|
+
if (end < 0) {
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
from = end + closer.length;
|
|
336
|
+
}
|
|
337
|
+
return out.trim();
|
|
338
|
+
}
|
|
339
|
+
/** All the text of a message's content, untouched: a string, or the `text` blocks of an array. */
|
|
340
|
+
function rawText(content) {
|
|
341
|
+
if (typeof content === 'string') {
|
|
342
|
+
return content;
|
|
343
|
+
}
|
|
344
|
+
if (Array.isArray(content)) {
|
|
345
|
+
return content
|
|
346
|
+
.map((b) => (b && typeof b === 'object' && b.type === 'text' ? String(b.text ?? '') : ''))
|
|
347
|
+
.join('\n');
|
|
348
|
+
}
|
|
349
|
+
return '';
|
|
350
|
+
}
|
|
351
|
+
const SKILL_BODY_PREFIX = 'Base directory for this skill:';
|
|
352
|
+
/**
|
|
353
|
+
* Claude Code injects an invoked skill as a meta user line: `Base directory for this skill: <dir>`,
|
|
354
|
+
* a blank line, then the skill's text. The directory (a local path) is kept apart from the text.
|
|
355
|
+
*/
|
|
356
|
+
function parseSkillBody(text) {
|
|
357
|
+
const trimmed = text.trimStart();
|
|
358
|
+
if (!trimmed.startsWith(SKILL_BODY_PREFIX)) {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
const newline = trimmed.indexOf('\n');
|
|
362
|
+
const dir = (newline < 0 ? trimmed : trimmed.slice(0, newline)).slice(SKILL_BODY_PREFIX.length).trim();
|
|
363
|
+
const body = newline < 0 ? '' : trimmed.slice(newline + 1).trim();
|
|
364
|
+
return dir && body ? { dir, text: body } : undefined;
|
|
365
|
+
}
|
|
366
|
+
/** Plain text of a REAL user prompt: a string, or the `text` blocks of an array. Tool results are not prompts. */
|
|
367
|
+
function promptText(content) {
|
|
368
|
+
if (typeof content === 'string') {
|
|
369
|
+
return stripInjectedContext(content);
|
|
370
|
+
}
|
|
371
|
+
if (Array.isArray(content)) {
|
|
372
|
+
return content
|
|
373
|
+
.map((block) => block && typeof block === 'object' && block.type === 'text'
|
|
374
|
+
? stripInjectedContext(String(block.text ?? ''))
|
|
375
|
+
: '')
|
|
376
|
+
.filter((part) => part.length > 0)
|
|
377
|
+
.join('\n');
|
|
378
|
+
}
|
|
379
|
+
return '';
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Walk a transcript (or a window of one — a leading partial line is skipped like any other
|
|
383
|
+
* unparseable line) and return its user→assistant exchanges in order. Each exchange carries its prompt and reply text and
|
|
384
|
+
* its `messages` (raw blocks: thinking, tool calls and results included). Synthetic `isMeta` user lines (injected skill bodies,
|
|
385
|
+
* caveats), compaction summaries, interrupt markers and subagent (sidechain) traffic are not prompts.
|
|
386
|
+
* A typed slash command is recovered as typed. Pure function, never throws.
|
|
387
|
+
*/
|
|
388
|
+
export function parseExchanges(content) {
|
|
389
|
+
const exchanges = [];
|
|
390
|
+
let model;
|
|
391
|
+
let current;
|
|
392
|
+
let groupKey;
|
|
393
|
+
let parts = [];
|
|
394
|
+
// Claude Code repeats a message's usage on every content-block line of it: count each message, and each subagent run, once.
|
|
395
|
+
const countedMessages = new Set();
|
|
396
|
+
const countedAgents = new Set();
|
|
397
|
+
const begin = (userText) => {
|
|
398
|
+
current = { userText };
|
|
399
|
+
exchanges.push(current);
|
|
400
|
+
groupKey = undefined;
|
|
401
|
+
parts = [];
|
|
402
|
+
};
|
|
403
|
+
for (const line of content.split('\n')) {
|
|
404
|
+
const trimmed = line.trim();
|
|
405
|
+
if (!trimmed) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
let event;
|
|
409
|
+
try {
|
|
410
|
+
event = JSON.parse(trimmed);
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const message = event?.message;
|
|
416
|
+
if (!message || typeof message !== 'object') {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
// Subagent (sidechain) traffic is not the conversation the user is having.
|
|
420
|
+
if (event.isSidechain === true) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (event.type === 'user') {
|
|
424
|
+
// An Agent tool call's result reports what the subagent spent, and on which model.
|
|
425
|
+
const result = event.toolUseResult;
|
|
426
|
+
if (current && result && typeof result === 'object' && !Array.isArray(result)) {
|
|
427
|
+
const run = result;
|
|
428
|
+
const agentId = typeof run.agentId === 'string' ? run.agentId : '';
|
|
429
|
+
if (agentId && run.usage && typeof run.usage === 'object' && !countedAgents.has(agentId)) {
|
|
430
|
+
countedAgents.add(agentId);
|
|
431
|
+
const model = typeof run.resolvedModel === 'string' && run.resolvedModel ? run.resolvedModel : 'unknown';
|
|
432
|
+
addSubagent((current.subagents ?? (current.subagents = emptySubagentUsage())), model, run.usage);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Tool results come back as user lines; they are part of the exchange's conversation.
|
|
436
|
+
if (current && Array.isArray(message.content)) {
|
|
437
|
+
const results = message.content.filter(isToolResultBlock);
|
|
438
|
+
if (results.length > 0) {
|
|
439
|
+
(current.messages ?? (current.messages = [])).push({ role: 'user', blocks: results });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// The expanded skill body, injected right after the slash command that invoked it.
|
|
443
|
+
if (event.isMeta === true && current && !current.skill) {
|
|
444
|
+
current.skill = parseSkillBody(rawText(message.content));
|
|
445
|
+
}
|
|
446
|
+
// Meta lines are injected context and compaction summaries are generated, not typed. A line
|
|
447
|
+
// carrying any tool_result is the tool loop talking, whatever else rides along with it.
|
|
448
|
+
if (event.isMeta === true ||
|
|
449
|
+
event.isCompactSummary === true ||
|
|
450
|
+
(Array.isArray(message.content) && message.content.some(isToolResultBlock))) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const text = slashCommandPrompt(promptText(message.content).trim());
|
|
454
|
+
if (isCommandOutput(text)) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
// A prompt with no text of its own (e.g. an image) still starts a new turn: the reply that
|
|
458
|
+
// follows must not be paired with the previous prompt.
|
|
459
|
+
// (A line whose every text block was injected context and has nothing else in it is not a prompt.)
|
|
460
|
+
const textlessPrompt = !text &&
|
|
461
|
+
Array.isArray(message.content) &&
|
|
462
|
+
message.content.some((b) => !!b && typeof b === 'object' && b.type !== 'text');
|
|
463
|
+
if ((text && !isInterruptMarker(text)) || textlessPrompt) {
|
|
464
|
+
// A new prompt starts a new exchange; whatever assistant text we had belonged to the old one.
|
|
465
|
+
begin(text || undefined);
|
|
466
|
+
if (typeof event.timestamp === 'string' && event.timestamp) {
|
|
467
|
+
current.timestamp = event.timestamp;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (event.type === 'assistant' && message.role === 'assistant') {
|
|
473
|
+
if (!current) {
|
|
474
|
+
begin(undefined);
|
|
475
|
+
}
|
|
476
|
+
const exchange = current;
|
|
477
|
+
const key = turnKey(event, message, '');
|
|
478
|
+
if (key && !countedMessages.has(key) && message.usage && typeof message.usage === 'object') {
|
|
479
|
+
countedMessages.add(key);
|
|
480
|
+
addUsage((exchange.usage ?? (exchange.usage = emptyUsage())), message.usage);
|
|
481
|
+
}
|
|
482
|
+
if (key !== groupKey) {
|
|
483
|
+
groupKey = key;
|
|
484
|
+
parts = [];
|
|
485
|
+
}
|
|
486
|
+
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
487
|
+
// One assistant message per model call: its content-block lines share a request id.
|
|
488
|
+
const sequence = (exchange.messages ?? (exchange.messages = []));
|
|
489
|
+
const lastMessage = sequence[sequence.length - 1];
|
|
490
|
+
if (lastMessage && lastMessage.role === 'assistant' && lastMessage.key === key && key) {
|
|
491
|
+
lastMessage.blocks.push(...blocks);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
sequence.push({ role: 'assistant', key: key || undefined, blocks: [...blocks] });
|
|
495
|
+
}
|
|
496
|
+
for (const block of blocks) {
|
|
497
|
+
if (block && typeof block === 'object' && block.type === 'text') {
|
|
498
|
+
const text = block.text;
|
|
499
|
+
if (typeof text === 'string' && text.trim()) {
|
|
500
|
+
parts.push(text);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (parts.length > 0) {
|
|
505
|
+
exchange.assistantText = parts.join('\n').trim();
|
|
506
|
+
// The key of the message the text came from, not of a later tool-only message.
|
|
507
|
+
exchange.assistantKey = key || undefined;
|
|
508
|
+
}
|
|
509
|
+
// Claude Code's own placeholder messages (interrupts, API errors) carry a pseudo-model.
|
|
510
|
+
if (typeof message.model === 'string' && message.model && message.model !== '<synthetic>') {
|
|
511
|
+
model = message.model;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return { exchanges, model };
|
|
516
|
+
}
|
|
517
|
+
/** The latest exchange of already-parsed exchanges, in the shape the Stop-hook adapter wants. */
|
|
518
|
+
export function lastTurnFrom(parsed) {
|
|
519
|
+
const last = parsed.exchanges[parsed.exchanges.length - 1];
|
|
520
|
+
return {
|
|
521
|
+
userText: last?.userText,
|
|
522
|
+
assistantText: last?.assistantText,
|
|
523
|
+
model: parsed.model,
|
|
524
|
+
assistantKey: last?.assistantText ? last.assistantKey : undefined,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Pull the latest user prompt and the final assistant message out of a transcript (or a window of
|
|
529
|
+
* one). Text only, the final reply's blocks aside. Pure function, never throws.
|
|
530
|
+
*/
|
|
531
|
+
export function extractLastTurn(content) {
|
|
532
|
+
return lastTurnFrom(parseExchanges(content));
|
|
533
|
+
}
|
|
258
534
|
/**
|
|
259
535
|
* Find every Claude Code transcript under `projectsDir` (default `~/.claude/projects`) and build one
|
|
260
536
|
* conversation envelope per session. A missing directory simply yields zero sessions.
|