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.
Files changed (133) hide show
  1. package/README.md +6 -1
  2. package/dist/api/help-center-client.d.ts.map +1 -1
  3. package/dist/api/help-center-client.js +5 -4
  4. package/dist/api/help-center-client.js.map +1 -1
  5. package/dist/auto-update/index.d.ts +61 -0
  6. package/dist/auto-update/index.d.ts.map +1 -0
  7. package/dist/auto-update/index.js +355 -0
  8. package/dist/auto-update/index.js.map +1 -0
  9. package/dist/auto-update/select.d.ts +28 -0
  10. package/dist/auto-update/select.d.ts.map +1 -0
  11. package/dist/auto-update/select.js +45 -0
  12. package/dist/auto-update/select.js.map +1 -0
  13. package/dist/auto-update/state.d.ts +25 -0
  14. package/dist/auto-update/state.d.ts.map +1 -0
  15. package/dist/auto-update/state.js +81 -0
  16. package/dist/auto-update/state.js.map +1 -0
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +97 -2
  19. package/dist/cli.js.map +1 -1
  20. package/dist/commands/capture-turn.d.ts +18 -0
  21. package/dist/commands/capture-turn.d.ts.map +1 -0
  22. package/dist/commands/capture-turn.js +163 -0
  23. package/dist/commands/capture-turn.js.map +1 -0
  24. package/dist/commands/download-client-file.js +1 -1
  25. package/dist/commands/download-client-file.js.map +1 -1
  26. package/dist/commands/flush-pending.d.ts.map +1 -1
  27. package/dist/commands/flush-pending.js +1 -30
  28. package/dist/commands/flush-pending.js.map +1 -1
  29. package/dist/commands/logout.js +2 -2
  30. package/dist/commands/logout.js.map +1 -1
  31. package/dist/commands/update.d.ts +9 -0
  32. package/dist/commands/update.d.ts.map +1 -0
  33. package/dist/commands/update.js +63 -0
  34. package/dist/commands/update.js.map +1 -0
  35. package/dist/config.d.ts +2 -0
  36. package/dist/config.d.ts.map +1 -1
  37. package/dist/config.js +25 -7
  38. package/dist/config.js.map +1 -1
  39. package/dist/errors.d.ts +1 -1
  40. package/dist/errors.d.ts.map +1 -1
  41. package/dist/errors.js.map +1 -1
  42. package/dist/hooks/adapters/claude.d.ts +12 -0
  43. package/dist/hooks/adapters/claude.d.ts.map +1 -0
  44. package/dist/hooks/adapters/claude.js +98 -0
  45. package/dist/hooks/adapters/claude.js.map +1 -0
  46. package/dist/hooks/adapters/copilot.d.ts +16 -0
  47. package/dist/hooks/adapters/copilot.d.ts.map +1 -0
  48. package/dist/hooks/adapters/copilot.js +115 -0
  49. package/dist/hooks/adapters/copilot.js.map +1 -0
  50. package/dist/hooks/adapters/index.d.ts +5 -0
  51. package/dist/hooks/adapters/index.d.ts.map +1 -0
  52. package/dist/hooks/adapters/index.js +8 -0
  53. package/dist/hooks/adapters/index.js.map +1 -0
  54. package/dist/hooks/adapters/shared.d.ts +32 -0
  55. package/dist/hooks/adapters/shared.d.ts.map +1 -0
  56. package/dist/hooks/adapters/shared.js +45 -0
  57. package/dist/hooks/adapters/shared.js.map +1 -0
  58. package/dist/hooks/disabled.d.ts +3 -0
  59. package/dist/hooks/disabled.d.ts.map +1 -0
  60. package/dist/hooks/disabled.js +5 -0
  61. package/dist/hooks/disabled.js.map +1 -0
  62. package/dist/hooks/hook-log.d.ts +7 -0
  63. package/dist/hooks/hook-log.d.ts.map +1 -0
  64. package/dist/hooks/hook-log.js +34 -0
  65. package/dist/hooks/hook-log.js.map +1 -0
  66. package/dist/hooks/identity.d.ts +21 -0
  67. package/dist/hooks/identity.d.ts.map +1 -0
  68. package/dist/hooks/identity.js +121 -0
  69. package/dist/hooks/identity.js.map +1 -0
  70. package/dist/hooks/log-turn.d.ts +23 -0
  71. package/dist/hooks/log-turn.d.ts.map +1 -0
  72. package/dist/hooks/log-turn.js +48 -0
  73. package/dist/hooks/log-turn.js.map +1 -0
  74. package/dist/hooks/send-turn.d.ts +32 -0
  75. package/dist/hooks/send-turn.d.ts.map +1 -0
  76. package/dist/hooks/send-turn.js +305 -0
  77. package/dist/hooks/send-turn.js.map +1 -0
  78. package/dist/hooks/silence.d.ts +7 -0
  79. package/dist/hooks/silence.d.ts.map +1 -0
  80. package/dist/hooks/silence.js +22 -0
  81. package/dist/hooks/silence.js.map +1 -0
  82. package/dist/hooks/stdin.d.ts +11 -0
  83. package/dist/hooks/stdin.d.ts.map +1 -0
  84. package/dist/hooks/stdin.js +51 -0
  85. package/dist/hooks/stdin.js.map +1 -0
  86. package/dist/hooks/turn-state.d.ts +40 -0
  87. package/dist/hooks/turn-state.d.ts.map +1 -0
  88. package/dist/hooks/turn-state.js +97 -0
  89. package/dist/hooks/turn-state.js.map +1 -0
  90. package/dist/hooks/types.d.ts +76 -0
  91. package/dist/hooks/types.d.ts.map +1 -0
  92. package/dist/hooks/types.js +3 -0
  93. package/dist/hooks/types.js.map +1 -0
  94. package/dist/pending-store.d.ts +22 -2
  95. package/dist/pending-store.d.ts.map +1 -1
  96. package/dist/pending-store.js +25 -3
  97. package/dist/pending-store.js.map +1 -1
  98. package/dist/pending-upload.d.ts +12 -0
  99. package/dist/pending-upload.d.ts.map +1 -0
  100. package/dist/pending-upload.js +51 -0
  101. package/dist/pending-upload.js.map +1 -0
  102. package/dist/proxy/interceptor.d.ts.map +1 -1
  103. package/dist/proxy/interceptor.js +1 -0
  104. package/dist/proxy/interceptor.js.map +1 -1
  105. package/dist/sessions/blocks.d.ts +51 -0
  106. package/dist/sessions/blocks.d.ts.map +1 -0
  107. package/dist/sessions/blocks.js +193 -0
  108. package/dist/sessions/blocks.js.map +1 -0
  109. package/dist/sessions/claude-scanner.d.ts +60 -1
  110. package/dist/sessions/claude-scanner.d.ts.map +1 -1
  111. package/dist/sessions/claude-scanner.js +277 -1
  112. package/dist/sessions/claude-scanner.js.map +1 -1
  113. package/dist/sessions/copilot-scanner.d.ts +27 -0
  114. package/dist/sessions/copilot-scanner.d.ts.map +1 -0
  115. package/dist/sessions/copilot-scanner.js +171 -0
  116. package/dist/sessions/copilot-scanner.js.map +1 -0
  117. package/dist/sessions/redact-secrets.d.ts.map +1 -1
  118. package/dist/sessions/redact-secrets.js +43 -10
  119. package/dist/sessions/redact-secrets.js.map +1 -1
  120. package/dist/sessions/turn-envelope.d.ts +62 -0
  121. package/dist/sessions/turn-envelope.d.ts.map +1 -0
  122. package/dist/sessions/turn-envelope.js +182 -0
  123. package/dist/sessions/turn-envelope.js.map +1 -0
  124. package/dist/sessions/usage.d.ts +23 -0
  125. package/dist/sessions/usage.d.ts.map +1 -0
  126. package/dist/sessions/usage.js +56 -0
  127. package/dist/sessions/usage.js.map +1 -0
  128. package/dist/types.d.ts +21 -0
  129. package/dist/types.d.ts.map +1 -1
  130. package/dist/types.js.map +1 -1
  131. package/dist/version.d.ts +2 -2
  132. package/dist/version.js +1 -1
  133. package/package.json +3 -2
@@ -0,0 +1,98 @@
1
+ import { promises as fs } from 'fs';
2
+ import { lastTurnFrom, parseExchanges } from '../../sessions/claude-scanner.js';
3
+ import { earlierFrom, earlierStampsFrom, sameText, splitCurrent, str } from './shared.js';
4
+ /**
5
+ * How much of the transcript's tail to read. A turn full of tool output can run to megabytes, and the
6
+ * earlier exchanges are rebuilt from the same window, so anything older than this is simply not seen
7
+ * (the turn's `history_truncated` flag says when that cost the conversation some of its history).
8
+ */
9
+ const TRANSCRIPT_TAIL_BYTES = 8 * 1024 * 1024;
10
+ async function readTail(filePath, maxBytes) {
11
+ const handle = await fs.open(filePath, 'r');
12
+ try {
13
+ const { size } = await handle.stat();
14
+ const length = Math.min(size, maxBytes);
15
+ const buffer = Buffer.alloc(length);
16
+ const { bytesRead } = await handle.read(buffer, 0, length, size - length);
17
+ return buffer.subarray(0, bytesRead).toString('utf8');
18
+ }
19
+ finally {
20
+ await handle.close();
21
+ }
22
+ }
23
+ /**
24
+ * Is the transcript's latest exchange this turn? Yes when its reply is the one the hook was given, or
25
+ * when it has a prompt but no reply yet (the transcript is written asynchronously and can lag the hook
26
+ * by exactly the reply). No when it shows some other reply: that is an earlier turn.
27
+ */
28
+ function transcriptShowsThisTurn(tail, assistantText) {
29
+ if (tail.assistantText === undefined) {
30
+ return tail.userText !== undefined;
31
+ }
32
+ return sameText(tail.assistantText, assistantText);
33
+ }
34
+ /**
35
+ * Claude Code `Stop` hook → a turn. The assistant text comes from `last_assistant_message` (the
36
+ * transcript file is written asynchronously and can lag it). The user text, and the model, come
37
+ * from the tail of `transcript_path` — that is also what covers the turn that invoked the skill,
38
+ * whose prompt was submitted before the skill's hooks registered. If the transcript can't be read
39
+ * the turn is still captured, just without the user half.
40
+ *
41
+ * Returns `null` for any other event or when there is nothing to send.
42
+ */
43
+ export async function claudeTurn(input) {
44
+ const event = str(input.hook_event_name);
45
+ if (event !== undefined && event !== 'Stop') {
46
+ return null;
47
+ }
48
+ const sessionId = str(input.session_id);
49
+ if (!sessionId) {
50
+ return null;
51
+ }
52
+ let tail = {};
53
+ let earlier = [];
54
+ let exchangeTimestamp;
55
+ let current;
56
+ let skillBodies;
57
+ let earlierStamps = [];
58
+ const transcriptPath = str(input.transcript_path);
59
+ if (transcriptPath) {
60
+ try {
61
+ const parsed = parseExchanges(await readTail(transcriptPath, TRANSCRIPT_TAIL_BYTES));
62
+ tail = lastTurnFrom(parsed);
63
+ // Everything before the latest exchange that has a prompt of its own, oldest first.
64
+ exchangeTimestamp = parsed.exchanges[parsed.exchanges.length - 1]?.timestamp;
65
+ earlierStamps = earlierStampsFrom(parsed.exchanges);
66
+ earlier = earlierFrom(parsed.exchanges);
67
+ current = parsed.exchanges[parsed.exchanges.length - 1];
68
+ skillBodies = parsed.exchanges.flatMap((e) => (e.skill ? [{ ...e.skill, timestamp: e.timestamp }] : []));
69
+ }
70
+ catch {
71
+ // Unreadable transcript: fall through with whatever the payload itself carries.
72
+ }
73
+ }
74
+ const assistantText = str(input.last_assistant_message) ?? tail.assistantText;
75
+ if (!assistantText) {
76
+ return null;
77
+ }
78
+ return {
79
+ sessionId,
80
+ userText: tail.userText,
81
+ assistantText,
82
+ model: tail.model,
83
+ // The transcript can lag the payload; only trust its request id if it is for the same reply.
84
+ turnKey: sameText(tail.assistantText, assistantText) ? tail.assistantKey : undefined,
85
+ cwd: str(input.cwd),
86
+ // Only trust the transcript's latest exchange as THIS turn (and so as the start of the skill's
87
+ // conversation, if this is turn 1) when it shows this turn's reply, or shows its prompt and no
88
+ // reply yet. A lagging transcript would otherwise make an earlier, pre-skill prompt the anchor.
89
+ exchangeTimestamp: transcriptShowsThisTurn(tail, assistantText) ? exchangeTimestamp : undefined,
90
+ earlier,
91
+ earlierStamps,
92
+ usage: current?.usage,
93
+ subagents: current?.subagents,
94
+ skillBodies,
95
+ ...splitCurrent(current?.messages, assistantText),
96
+ };
97
+ }
98
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../../src/hooks/adapters/claude.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { AdapterContext, HookInput, HookTurn } from '../types.js';
2
+ /**
3
+ * VS Code Copilot `Stop` hook → a turn. The payload carries only the session id and `transcript_path`
4
+ * (`~/.copilot/session-state/<id>/events.jsonl`), so the prompt, the reply, the tool loop and the
5
+ * token usage all come from that file. Both the PascalCase (`session_id`, `transcript_path`) and the
6
+ * camelCase (`sessionId`, `transcriptPath`) spellings of the payload are understood.
7
+ *
8
+ * A Copilot hook runs for EVERY chat in the workspace, so a turn is sent only when the transcript shows
9
+ * the `--skill` skill was invoked in this session (a `skill.invoked` event), at or before this turn.
10
+ * `--skill` may be a pattern (`*`, `acme-*`): then any matching skill counts, and the first one invoked in
11
+ * the session is returned as `skill`. Without `--skill`, or for a chat that never invoked it, nothing is sent.
12
+ *
13
+ * Returns `null` for any other event, an unrelated chat, or when there is nothing to send.
14
+ */
15
+ export declare function copilotTurn(input: HookInput, ctx?: AdapterContext): Promise<HookTurn | null>;
16
+ //# sourceMappingURL=copilot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copilot.d.ts","sourceRoot":"","sources":["../../../src/hooks/adapters/copilot.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAiDvE;;;;;;;;;;;;GAYG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAwDtG"}
@@ -0,0 +1,115 @@
1
+ import { createReadStream } from 'fs';
2
+ import { logHookEvent } from '../hook-log.js';
3
+ import { createInterface } from 'readline';
4
+ import { isWantedLine, parseCopilotExchanges } from '../../sessions/copilot-scanner.js';
5
+ import { earlierFrom, earlierStampsFrom, isSkillPattern, skillMatchesPattern, splitCurrent, str } from './shared.js';
6
+ /**
7
+ * The transcript is written asynchronously and Claude's can lag its `Stop` by the reply. Copilot's was in the file
8
+ * 15 to 20 ms BEFORE `Stop` in the three recorded turns, so this is a precaution: wait this long, this many times, for it.
9
+ */
10
+ const LAG_WAIT_MS = 250;
11
+ const LAG_RETRIES = 4;
12
+ /**
13
+ * The lines of the transcript that matter. Copilot snapshots the whole conversation into the file on
14
+ * every model call, so it is streamed and filtered rather than read whole.
15
+ */
16
+ async function readEvents(filePath) {
17
+ // `parseCopilotExchanges` applies the same filter, so it stays safe when called on a whole file; this pass only keeps memory small.
18
+ const kept = [];
19
+ const lines = createInterface({ input: createReadStream(filePath, { encoding: 'utf8' }), crlfDelay: Infinity });
20
+ for await (const line of lines) {
21
+ if (isWantedLine(line)) {
22
+ kept.push(line);
23
+ }
24
+ }
25
+ return kept.join('\n');
26
+ }
27
+ function wait(ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+ /** Does this exchange's skill invocation name `skill`: by its declared name or by its directory's name, or a `--skill` pattern. */
31
+ function invokes(e, skill) {
32
+ return (e.skills ?? []).some((s) => skillMatchesPattern(skill, s));
33
+ }
34
+ /** The skill a `--skill` value matched in this session: for a pattern the first one invoked (never the pattern itself), for a plain name that name. */
35
+ function resolveSkill(exchanges, skill) {
36
+ if (!isSkillPattern(skill)) {
37
+ return skill;
38
+ }
39
+ for (const e of exchanges) {
40
+ const hit = (e.skills ?? []).find((s) => skillMatchesPattern(skill, s));
41
+ if (hit) {
42
+ return hit.name;
43
+ }
44
+ }
45
+ return undefined;
46
+ }
47
+ /**
48
+ * VS Code Copilot `Stop` hook → a turn. The payload carries only the session id and `transcript_path`
49
+ * (`~/.copilot/session-state/<id>/events.jsonl`), so the prompt, the reply, the tool loop and the
50
+ * token usage all come from that file. Both the PascalCase (`session_id`, `transcript_path`) and the
51
+ * camelCase (`sessionId`, `transcriptPath`) spellings of the payload are understood.
52
+ *
53
+ * A Copilot hook runs for EVERY chat in the workspace, so a turn is sent only when the transcript shows
54
+ * the `--skill` skill was invoked in this session (a `skill.invoked` event), at or before this turn.
55
+ * `--skill` may be a pattern (`*`, `acme-*`): then any matching skill counts, and the first one invoked in
56
+ * the session is returned as `skill`. Without `--skill`, or for a chat that never invoked it, nothing is sent.
57
+ *
58
+ * Returns `null` for any other event, an unrelated chat, or when there is nothing to send.
59
+ */
60
+ export async function copilotTurn(input, ctx = {}) {
61
+ const event = str(input.hook_event_name);
62
+ if (event !== undefined && event !== 'Stop' && event !== 'agentStop') {
63
+ return null;
64
+ }
65
+ const sessionId = str(input.session_id) ?? str(input.sessionId);
66
+ const transcriptPath = str(input.transcript_path) ?? str(input.transcriptPath);
67
+ if (!sessionId || !transcriptPath || !ctx.skill) {
68
+ return null;
69
+ }
70
+ const skill = ctx.skill;
71
+ let parsed;
72
+ try {
73
+ for (let attempt = 0;; attempt++) {
74
+ parsed = parseCopilotExchanges(await readEvents(transcriptPath));
75
+ if (parsed.exchanges[parsed.exchanges.length - 1]?.assistantText || attempt >= LAG_RETRIES) {
76
+ break;
77
+ }
78
+ // A chat that never used the skill has nothing to wait for.
79
+ if (!parsed.exchanges.some((e) => invokes(e, skill))) {
80
+ return null;
81
+ }
82
+ await wait(LAG_WAIT_MS);
83
+ }
84
+ }
85
+ catch (err) {
86
+ // Not the normal "unrelated chat" path: say why nothing was captured, so a broken hook can be told from a quiet one.
87
+ await logHookEvent(`capture-turn (copilot): could not read the transcript: ${err.code ?? err.message}`);
88
+ return null;
89
+ }
90
+ const { exchanges } = parsed;
91
+ const current = exchanges[exchanges.length - 1];
92
+ if (!current?.assistantText || !exchanges.some((e) => invokes(e, skill))) {
93
+ return null;
94
+ }
95
+ const resolved = resolveSkill(exchanges, skill);
96
+ if (!resolved) {
97
+ return null;
98
+ }
99
+ return {
100
+ sessionId,
101
+ skill: resolved,
102
+ userText: current.userText,
103
+ assistantText: current.assistantText,
104
+ model: parsed.model,
105
+ turnKey: current.assistantKey,
106
+ cwd: str(input.cwd),
107
+ exchangeTimestamp: current.timestamp,
108
+ earlier: earlierFrom(exchanges),
109
+ earlierStamps: earlierStampsFrom(exchanges),
110
+ usage: current.usage,
111
+ skillBodies: exchanges.flatMap((e) => (e.skills ?? []).map((s) => ({ dir: s.dir, text: s.text, name: s.name, timestamp: e.timestamp }))),
112
+ ...splitCurrent(current.messages, current.assistantText),
113
+ };
114
+ }
115
+ //# sourceMappingURL=copilot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copilot.js","sourceRoot":"","sources":["../../../src/hooks/adapters/copilot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { AdapterContext, HookAgent, HookInput, HookTurn } from '../types.js';
2
+ export type HookAdapter = (input: HookInput, ctx?: AdapterContext) => Promise<HookTurn | null>;
3
+ /** One adapter per agent: turns that agent's hook payload (and transcript) into a `HookTurn`. */
4
+ export declare const ADAPTERS: Record<HookAgent, HookAdapter>;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIlF,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;AAE/F,iGAAiG;AACjG,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,SAAS,EAAE,WAAW,CAGnD,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { claudeTurn } from './claude.js';
2
+ import { copilotTurn } from './copilot.js';
3
+ /** One adapter per agent: turns that agent's hook payload (and transcript) into a `HookTurn`. */
4
+ export const ADAPTERS = {
5
+ claude: (input) => claudeTurn(input),
6
+ copilot: copilotTurn,
7
+ };
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/hooks/adapters/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAI3C,iGAAiG;AACjG,MAAM,CAAC,MAAM,QAAQ,GAAmC;IACtD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC;IACpC,OAAO,EAAE,WAAW;CACrB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { type RawMessage } from '../../sessions/blocks.js';
2
+ import type { Exchange } from '../../sessions/claude-scanner.js';
3
+ import type { EarlierExchange } from '../types.js';
4
+ /** Equal once whitespace is collapsed: the payload and the transcript may join a reply's text blocks differently. */
5
+ export declare function sameText(a: string | undefined, b: string): boolean;
6
+ /**
7
+ * Split this turn's messages into its tool loop and its final reply. The final reply is the last
8
+ * assistant message, and only when its text is the reply the hook was given (otherwise the transcript
9
+ * hasn't caught up, and the reply is sent as the text the hook has, with the loop as far as it shows).
10
+ */
11
+ export declare function splitCurrent(messages: RawMessage[] | undefined, reply: string): {
12
+ currentMessages: RawMessage[];
13
+ finalReply?: unknown[];
14
+ };
15
+ export declare function str(value: unknown): string | undefined;
16
+ /** The exchanges before the latest one that have a prompt of their own, as `earlier` history (oldest first). */
17
+ export declare function earlierFrom(exchanges: Exchange[]): EarlierExchange[];
18
+ /** Timestamps of EVERY exchange before the latest, including ones with no prompt text of their own. */
19
+ export declare function earlierStampsFrom(exchanges: Exchange[]): string[];
20
+ /** Does a skill (its declared `name` and the directory it was loaded from) answer to `--skill`: by name or by its directory's name. */
21
+ export declare function skillMatches(skill: string, candidate: {
22
+ name?: string;
23
+ dir: string;
24
+ }): boolean;
25
+ /** A `--skill` value that ends in `*` is a pattern: `*` is any skill, `acme-*` any skill whose declared name starts with `acme-`. */
26
+ export declare function isSkillPattern(skill: string): boolean;
27
+ /** Does a skill answer to a `--skill` value: by name or directory, or, for a pattern, by the prefix of its declared name. */
28
+ export declare function skillMatchesPattern(skill: string, candidate: {
29
+ name?: string;
30
+ dir: string;
31
+ }): boolean;
32
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../src/hooks/adapters/shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,qHAAqH;AACrH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAElE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,eAAe,EAAE,UAAU,EAAE,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,CAOzI;AAED,wBAAgB,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEtD;AAED,gHAAgH;AAChH,wBAAgB,WAAW,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,eAAe,EAAE,CAKpE;AAED,uGAAuG;AACvG,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,MAAM,EAAE,CAEjE;AAED,uIAAuI;AACvI,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAE9F;AAED,qIAAqI;AACrI,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,6HAA6H;AAC7H,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAErG"}
@@ -0,0 +1,45 @@
1
+ import { rawMessageText } from '../../sessions/blocks.js';
2
+ /** Equal once whitespace is collapsed: the payload and the transcript may join a reply's text blocks differently. */
3
+ export function sameText(a, b) {
4
+ return a !== undefined && a.replace(/\s+/g, ' ').trim() === b.replace(/\s+/g, ' ').trim();
5
+ }
6
+ /**
7
+ * Split this turn's messages into its tool loop and its final reply. The final reply is the last
8
+ * assistant message, and only when its text is the reply the hook was given (otherwise the transcript
9
+ * hasn't caught up, and the reply is sent as the text the hook has, with the loop as far as it shows).
10
+ */
11
+ export function splitCurrent(messages, reply) {
12
+ const all = messages ?? [];
13
+ const last = all[all.length - 1];
14
+ if (last && last.role === 'assistant' && sameText(rawMessageText(last), reply)) {
15
+ return { currentMessages: all.slice(0, -1), finalReply: last.blocks };
16
+ }
17
+ return { currentMessages: all };
18
+ }
19
+ export function str(value) {
20
+ return typeof value === 'string' && value.trim() ? value : undefined;
21
+ }
22
+ /** The exchanges before the latest one that have a prompt of their own, as `earlier` history (oldest first). */
23
+ export function earlierFrom(exchanges) {
24
+ return exchanges
25
+ .slice(0, -1)
26
+ .filter((e) => typeof e.userText === 'string')
27
+ .map((e) => ({ timestamp: e.timestamp, userText: e.userText, assistantText: e.assistantText, usage: e.usage, subagents: e.subagents, messages: e.messages }));
28
+ }
29
+ /** Timestamps of EVERY exchange before the latest, including ones with no prompt text of their own. */
30
+ export function earlierStampsFrom(exchanges) {
31
+ return exchanges.slice(0, -1).flatMap((e) => (e.timestamp ? [e.timestamp] : []));
32
+ }
33
+ /** Does a skill (its declared `name` and the directory it was loaded from) answer to `--skill`: by name or by its directory's name. */
34
+ export function skillMatches(skill, candidate) {
35
+ return candidate.name === skill || candidate.dir.split(/[\\/]/).filter(Boolean).pop() === skill;
36
+ }
37
+ /** A `--skill` value that ends in `*` is a pattern: `*` is any skill, `acme-*` any skill whose declared name starts with `acme-`. */
38
+ export function isSkillPattern(skill) {
39
+ return skill.endsWith('*');
40
+ }
41
+ /** Does a skill answer to a `--skill` value: by name or directory, or, for a pattern, by the prefix of its declared name. */
42
+ export function skillMatchesPattern(skill, candidate) {
43
+ return isSkillPattern(skill) ? (candidate.name ?? '').startsWith(skill.slice(0, -1)) : skillMatches(skill, candidate);
44
+ }
45
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../../src/hooks/adapters/shared.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** `COOLHAND_CAPTURE_DISABLED=1|true|yes`: the kill switch for people who didn't choose to run a skill's hook. */
2
+ export declare function isCaptureDisabled(): boolean;
3
+ //# sourceMappingURL=disabled.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disabled.d.ts","sourceRoot":"","sources":["../../src/hooks/disabled.ts"],"names":[],"mappings":"AAAA,kHAAkH;AAClH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C"}
@@ -0,0 +1,5 @@
1
+ /** `COOLHAND_CAPTURE_DISABLED=1|true|yes`: the kill switch for people who didn't choose to run a skill's hook. */
2
+ export function isCaptureDisabled() {
3
+ return /^(1|true|yes)$/i.test(process.env.COOLHAND_CAPTURE_DISABLED ?? '');
4
+ }
5
+ //# sourceMappingURL=disabled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disabled.js","sourceRoot":"","sources":["../../src/hooks/disabled.ts"],"names":[],"mappings":"AAAA,kHAAkH;AAClH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,IAAI,EAAE,CAAC,CAAC;AAC7E,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Append one line to `~/.coolhand/capture-turns/hook.log`. A hook has to stay silent — anything it
3
+ * prints reaches the agent — so this file is the only place its failures can be found. Best-effort
4
+ * and bounded (once it passes 256 KB it is cut back to its last 64 KB); never throws.
5
+ */
6
+ export declare function logHookEvent(message: string): Promise<void>;
7
+ //# sourceMappingURL=hook-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-log.d.ts","sourceRoot":"","sources":["../../src/hooks/hook-log.ts"],"names":[],"mappings":"AAUA;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBjE"}
@@ -0,0 +1,34 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ import { atomicWriteFile } from '../config.js';
4
+ import { redact } from '../logger.js';
5
+ import { turnsDir } from './turn-state.js';
6
+ const MAX_LOG_BYTES = 256 * 1024;
7
+ /** What survives a trim: the most recent failures are the ones worth reading. */
8
+ const KEEP_LOG_BYTES = 64 * 1024;
9
+ /**
10
+ * Append one line to `~/.coolhand/capture-turns/hook.log`. A hook has to stay silent — anything it
11
+ * prints reaches the agent — so this file is the only place its failures can be found. Best-effort
12
+ * and bounded (once it passes 256 KB it is cut back to its last 64 KB); never throws.
13
+ */
14
+ export async function logHookEvent(message) {
15
+ try {
16
+ const file = path.join(turnsDir(), 'hook.log');
17
+ await fs.mkdir(turnsDir(), { recursive: true, mode: 0o700 });
18
+ try {
19
+ if ((await fs.stat(file)).size > MAX_LOG_BYTES) {
20
+ const tail = (await fs.readFile(file, 'utf8')).slice(-KEEP_LOG_BYTES);
21
+ // Drop the partial first line, then replace the file atomically.
22
+ await atomicWriteFile(file, tail.slice(tail.indexOf('\n') + 1), 0o600);
23
+ }
24
+ }
25
+ catch {
26
+ // No log yet.
27
+ }
28
+ await fs.appendFile(file, `${new Date().toISOString()} ${redact(message).replace(/[\r\n]+/g, ' ')}\n`, { mode: 0o600 });
29
+ }
30
+ catch {
31
+ // Nowhere left to report to.
32
+ }
33
+ }
34
+ //# sourceMappingURL=hook-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-log.js","sourceRoot":"","sources":["../../src/hooks/hook-log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3C,MAAM,aAAa,GAAG,GAAG,GAAG,IAAI,CAAC;AACjC,iFAAiF;AACjF,MAAM,cAAc,GAAG,EAAE,GAAG,IAAI,CAAC;AAEjC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAe;IAChD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,UAAU,CAAC,CAAC;QAC/C,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,aAAa,EAAE,CAAC;gBAC/C,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,CAAC;gBACtE,iEAAiE;gBACjE,MAAM,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;YACzE,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,cAAc;QAChB,CAAC;QACD,MAAM,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1H,CAAC;IAAC,MAAM,CAAC;QACP,6BAA6B;IAC/B,CAAC;AACH,CAAC"}
@@ -0,0 +1,21 @@
1
+ export interface TesterIdentity {
2
+ id: string;
3
+ /** Where `id` came from: an explicit override, the git email, or the anonymous machine id. */
4
+ source: 'env' | 'git' | 'machine';
5
+ }
6
+ /** A random UUID minted on first use and kept in `~/.coolhand/machine-id`. Identifies a machine, not a person. */
7
+ export declare function machineId(): Promise<string>;
8
+ /**
9
+ * Who ran this session, for attributing turns captured with one shared public key.
10
+ *
11
+ * `anonymous` wins over everything: it returns the machine id without consulting the environment or
12
+ * git, so nothing identifying can leave the machine. Otherwise: `COOLHAND_CAPTURE_USER`, then the git
13
+ * email (read from the session's own working directory, so a repo-local `user.email` is honoured),
14
+ * then the machine id. `capture-turn` calls this once per session and keeps the answer in the
15
+ * session's state file.
16
+ */
17
+ export declare function resolveTesterIdentity(opts?: {
18
+ anonymous?: boolean;
19
+ cwd?: string;
20
+ }): Promise<TesterIdentity>;
21
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/hooks/identity.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,8FAA8F;IAC9F,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,CAAC;CACnC;AAMD,kHAAkH;AAClH,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CA+BjD;AA0DD;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/C,OAAO,CAAC,cAAc,CAAC,CAazB"}
@@ -0,0 +1,121 @@
1
+ import { execFile } from 'child_process';
2
+ import { promises as fs, constants as fsConstants } from 'fs';
3
+ import * as path from 'path';
4
+ import { randomUUID } from 'crypto';
5
+ import { configDir, atomicWriteFile } from '../config.js';
6
+ const DIR_MODE = 0o700;
7
+ const FILE_MODE = 0o600;
8
+ const GIT_TIMEOUT_MS = 2000;
9
+ function machineIdPath() {
10
+ return path.join(configDir(), 'machine-id');
11
+ }
12
+ /** A random UUID minted on first use and kept in `~/.coolhand/machine-id`. Identifies a machine, not a person. */
13
+ export async function machineId() {
14
+ try {
15
+ const existing = (await fs.readFile(machineIdPath(), 'utf8')).trim();
16
+ if (existing) {
17
+ return existing;
18
+ }
19
+ }
20
+ catch {
21
+ // Not minted yet.
22
+ }
23
+ const id = randomUUID();
24
+ await fs.mkdir(configDir(), { recursive: true, mode: DIR_MODE });
25
+ try {
26
+ // 'wx': when two first-ever runs race, exactly one creates the file and the other adopts its id.
27
+ await fs.writeFile(machineIdPath(), `${id}\n`, { flag: 'wx', mode: FILE_MODE });
28
+ return id;
29
+ }
30
+ catch (err) {
31
+ if (err.code !== 'EEXIST') {
32
+ throw err;
33
+ }
34
+ // The winner creates the file and then writes it; a reader can catch it between the two.
35
+ for (let attempt = 0; attempt < 5; attempt++) {
36
+ const winner = (await fs.readFile(machineIdPath(), 'utf8')).trim();
37
+ if (winner) {
38
+ return winner;
39
+ }
40
+ await new Promise((resolve) => setTimeout(resolve, 10));
41
+ }
42
+ // Still empty: a crash left a half-made file. Repair it so the id stops changing on every run.
43
+ await atomicWriteFile(machineIdPath(), `${id}\n`, FILE_MODE);
44
+ return id;
45
+ }
46
+ }
47
+ /** The machine id, or a throwaway one when the config dir can't be written: attribution is not worth a lost turn. */
48
+ async function machineIdentity() {
49
+ try {
50
+ return { id: await machineId(), source: 'machine' };
51
+ }
52
+ catch {
53
+ return { id: randomUUID(), source: 'machine' };
54
+ }
55
+ }
56
+ /**
57
+ * Find an executable on PATH, ignoring empty and relative entries. The session's cwd is an arbitrary,
58
+ * possibly untrusted repository, and Windows searches the working directory (and a `.` PATH entry)
59
+ * when launching a bare command name, so a repo could plant its own `git.exe`. An absolute path
60
+ * resolved here avoids that on every platform.
61
+ */
62
+ async function findOnPath(name) {
63
+ const suffixes = process.platform === 'win32' ? ['.exe'] : [''];
64
+ for (const dir of (process.env.PATH ?? '').split(path.delimiter)) {
65
+ if (!dir || !path.isAbsolute(dir)) {
66
+ continue;
67
+ }
68
+ for (const suffix of suffixes) {
69
+ const candidate = path.join(dir, `${name}${suffix}`);
70
+ try {
71
+ await fs.access(candidate, fsConstants.X_OK);
72
+ return candidate;
73
+ }
74
+ catch {
75
+ // Not here.
76
+ }
77
+ }
78
+ }
79
+ return undefined;
80
+ }
81
+ async function gitEmail(cwd) {
82
+ const git = await findOnPath('git');
83
+ if (!git) {
84
+ return undefined;
85
+ }
86
+ return new Promise((resolve) => {
87
+ try {
88
+ execFile(git, ['config', 'user.email'], { cwd, timeout: GIT_TIMEOUT_MS, windowsHide: true }, (err, stdout) => {
89
+ const email = err ? '' : String(stdout).trim();
90
+ resolve(email || undefined);
91
+ });
92
+ }
93
+ catch {
94
+ resolve(undefined);
95
+ }
96
+ });
97
+ }
98
+ /**
99
+ * Who ran this session, for attributing turns captured with one shared public key.
100
+ *
101
+ * `anonymous` wins over everything: it returns the machine id without consulting the environment or
102
+ * git, so nothing identifying can leave the machine. Otherwise: `COOLHAND_CAPTURE_USER`, then the git
103
+ * email (read from the session's own working directory, so a repo-local `user.email` is honoured),
104
+ * then the machine id. `capture-turn` calls this once per session and keeps the answer in the
105
+ * session's state file.
106
+ */
107
+ export async function resolveTesterIdentity(opts = {}) {
108
+ if (opts.anonymous) {
109
+ return machineIdentity();
110
+ }
111
+ const override = process.env.COOLHAND_CAPTURE_USER?.trim();
112
+ if (override) {
113
+ return { id: override, source: 'env' };
114
+ }
115
+ const email = await gitEmail(opts.cwd);
116
+ if (email) {
117
+ return { id: email, source: 'git' };
118
+ }
119
+ return machineIdentity();
120
+ }
121
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/hooks/identity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { type Coolhand, type CoolhandLogResponse, type DeclaredContext } from 'coolhand-node';
2
+ export interface LogTurnOptions {
3
+ collector?: string;
4
+ metadata: Record<string, unknown>;
5
+ /**
6
+ * The workload this log belongs to, sent as `llm_request_log.declared`. The server find-or-creates
7
+ * the workload by key and attaches the log directly, skipping its own matching. Only a workload is
8
+ * declared: no template (the server derives the skill's single default template from the workload).
9
+ */
10
+ declared?: DeclaredContext;
11
+ }
12
+ /**
13
+ * Send one turn through the SDK, declared as `opts.declared` when given. Both the live send and the
14
+ * replay of a spooled turn go through here, so a replay is declared exactly like the original.
15
+ *
16
+ * - `withContext` throws synchronously for a malformed declaration (a blank, over-long or control-
17
+ * character key, say). That must not cost the turn: it is written to hook.log and the turn is sent
18
+ * undeclared, so it still lands, just unmatched.
19
+ * - The server answers `warnings` when it ignores a declaration (for example the public-key cap on
20
+ * declared workloads): the turn still landed, but not as declared. They go to hook.log.
21
+ */
22
+ export declare function logTurn(coolhand: Coolhand, envelope: unknown, opts: LogTurnOptions): Promise<CoolhandLogResponse | null>;
23
+ //# sourceMappingURL=log-turn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-turn.d.ts","sourceRoot":"","sources":["../../src/hooks/log-turn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAyB,KAAK,mBAAmB,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGlI,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAWD;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CA2B9H"}