coolhand-cli 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +5 -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 +152 -0
  23. package/dist/commands/capture-turn.js.map +1 -0
  24. package/dist/commands/flush-pending.d.ts.map +1 -1
  25. package/dist/commands/flush-pending.js +1 -30
  26. package/dist/commands/flush-pending.js.map +1 -1
  27. package/dist/commands/update.d.ts +9 -0
  28. package/dist/commands/update.d.ts.map +1 -0
  29. package/dist/commands/update.js +63 -0
  30. package/dist/commands/update.js.map +1 -0
  31. package/dist/config.d.ts +2 -0
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js +25 -7
  34. package/dist/config.js.map +1 -1
  35. package/dist/errors.d.ts +1 -1
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js.map +1 -1
  38. package/dist/hooks/adapters/claude.d.ts +12 -0
  39. package/dist/hooks/adapters/claude.d.ts.map +1 -0
  40. package/dist/hooks/adapters/claude.js +98 -0
  41. package/dist/hooks/adapters/claude.js.map +1 -0
  42. package/dist/hooks/adapters/copilot.d.ts +15 -0
  43. package/dist/hooks/adapters/copilot.d.ts.map +1 -0
  44. package/dist/hooks/adapters/copilot.js +96 -0
  45. package/dist/hooks/adapters/copilot.js.map +1 -0
  46. package/dist/hooks/adapters/index.d.ts +5 -0
  47. package/dist/hooks/adapters/index.d.ts.map +1 -0
  48. package/dist/hooks/adapters/index.js +8 -0
  49. package/dist/hooks/adapters/index.js.map +1 -0
  50. package/dist/hooks/adapters/shared.d.ts +25 -0
  51. package/dist/hooks/adapters/shared.d.ts.map +1 -0
  52. package/dist/hooks/adapters/shared.js +37 -0
  53. package/dist/hooks/adapters/shared.js.map +1 -0
  54. package/dist/hooks/disabled.d.ts +3 -0
  55. package/dist/hooks/disabled.d.ts.map +1 -0
  56. package/dist/hooks/disabled.js +5 -0
  57. package/dist/hooks/disabled.js.map +1 -0
  58. package/dist/hooks/hook-log.d.ts +7 -0
  59. package/dist/hooks/hook-log.d.ts.map +1 -0
  60. package/dist/hooks/hook-log.js +34 -0
  61. package/dist/hooks/hook-log.js.map +1 -0
  62. package/dist/hooks/identity.d.ts +21 -0
  63. package/dist/hooks/identity.d.ts.map +1 -0
  64. package/dist/hooks/identity.js +121 -0
  65. package/dist/hooks/identity.js.map +1 -0
  66. package/dist/hooks/log-turn.d.ts +23 -0
  67. package/dist/hooks/log-turn.d.ts.map +1 -0
  68. package/dist/hooks/log-turn.js +48 -0
  69. package/dist/hooks/log-turn.js.map +1 -0
  70. package/dist/hooks/send-turn.d.ts +32 -0
  71. package/dist/hooks/send-turn.d.ts.map +1 -0
  72. package/dist/hooks/send-turn.js +305 -0
  73. package/dist/hooks/send-turn.js.map +1 -0
  74. package/dist/hooks/silence.d.ts +7 -0
  75. package/dist/hooks/silence.d.ts.map +1 -0
  76. package/dist/hooks/silence.js +22 -0
  77. package/dist/hooks/silence.js.map +1 -0
  78. package/dist/hooks/stdin.d.ts +11 -0
  79. package/dist/hooks/stdin.d.ts.map +1 -0
  80. package/dist/hooks/stdin.js +51 -0
  81. package/dist/hooks/stdin.js.map +1 -0
  82. package/dist/hooks/turn-state.d.ts +40 -0
  83. package/dist/hooks/turn-state.d.ts.map +1 -0
  84. package/dist/hooks/turn-state.js +97 -0
  85. package/dist/hooks/turn-state.js.map +1 -0
  86. package/dist/hooks/types.d.ts +74 -0
  87. package/dist/hooks/types.d.ts.map +1 -0
  88. package/dist/hooks/types.js +3 -0
  89. package/dist/hooks/types.js.map +1 -0
  90. package/dist/pending-store.d.ts +22 -2
  91. package/dist/pending-store.d.ts.map +1 -1
  92. package/dist/pending-store.js +25 -3
  93. package/dist/pending-store.js.map +1 -1
  94. package/dist/pending-upload.d.ts +12 -0
  95. package/dist/pending-upload.d.ts.map +1 -0
  96. package/dist/pending-upload.js +51 -0
  97. package/dist/pending-upload.js.map +1 -0
  98. package/dist/sessions/blocks.d.ts +51 -0
  99. package/dist/sessions/blocks.d.ts.map +1 -0
  100. package/dist/sessions/blocks.js +193 -0
  101. package/dist/sessions/blocks.js.map +1 -0
  102. package/dist/sessions/claude-scanner.d.ts +60 -1
  103. package/dist/sessions/claude-scanner.d.ts.map +1 -1
  104. package/dist/sessions/claude-scanner.js +277 -1
  105. package/dist/sessions/claude-scanner.js.map +1 -1
  106. package/dist/sessions/copilot-scanner.d.ts +27 -0
  107. package/dist/sessions/copilot-scanner.d.ts.map +1 -0
  108. package/dist/sessions/copilot-scanner.js +171 -0
  109. package/dist/sessions/copilot-scanner.js.map +1 -0
  110. package/dist/sessions/redact-secrets.d.ts.map +1 -1
  111. package/dist/sessions/redact-secrets.js +43 -10
  112. package/dist/sessions/redact-secrets.js.map +1 -1
  113. package/dist/sessions/turn-envelope.d.ts +62 -0
  114. package/dist/sessions/turn-envelope.d.ts.map +1 -0
  115. package/dist/sessions/turn-envelope.js +182 -0
  116. package/dist/sessions/turn-envelope.js.map +1 -0
  117. package/dist/sessions/usage.d.ts +23 -0
  118. package/dist/sessions/usage.d.ts.map +1 -0
  119. package/dist/sessions/usage.js +56 -0
  120. package/dist/sessions/usage.js.map +1 -0
  121. package/dist/types.d.ts +21 -0
  122. package/dist/types.d.ts.map +1 -1
  123. package/dist/types.js.map +1 -1
  124. package/dist/version.d.ts +2 -2
  125. package/dist/version.js +1 -1
  126. package/package.json +3 -2
@@ -0,0 +1,74 @@
1
+ import type { RawMessage } from '../sessions/blocks.js';
2
+ import type { SubagentUsage, Usage } from '../sessions/usage.js';
3
+ /** Agents whose hook payloads `capture-turn` understands. */
4
+ export declare const HOOK_AGENTS: readonly ["claude", "copilot"];
5
+ export type HookAgent = (typeof HOOK_AGENTS)[number];
6
+ /** One completed user→assistant exchange, normalised across agents. */
7
+ export interface HookTurn {
8
+ /** The agent's own session/conversation id. Stable across every turn of a session. */
9
+ sessionId: string;
10
+ /** What the user typed. Missing when the agent's payload and transcript don't expose it. */
11
+ userText?: string;
12
+ /** The assistant's final reply for the turn. Always non-empty. */
13
+ assistantText: string;
14
+ model?: string;
15
+ /** Identifies this exact turn when the agent exposes something unique to it. */
16
+ turnKey?: string;
17
+ /** Working directory of the session, when the payload carries one. */
18
+ cwd?: string;
19
+ /** When this turn's user line was written (ISO 8601), if the transcript says. */
20
+ exchangeTimestamp?: string;
21
+ /**
22
+ * The exchanges before this turn that the agent's transcript still shows, oldest first, including
23
+ * ones from before the skill was invoked. `prepareTurnMessages` keeps only those at or after the
24
+ * session's anchor (the first captured turn).
25
+ */
26
+ earlier?: EarlierExchange[];
27
+ /**
28
+ * Timestamps of EVERY earlier exchange the transcript shows, including ones with no text of their
29
+ * own (an image-only prompt), which `earlier` leaves out. Used only to tell whether the anchor's
30
+ * exchange is still in view.
31
+ */
32
+ earlierStamps?: string[];
33
+ /** Tokens the main-thread assistant messages of THIS exchange spent, as far as the transcript shows. */
34
+ usage?: Usage;
35
+ /** What subagents spent during this exchange, per model. */
36
+ subagents?: SubagentUsage;
37
+ /**
38
+ * This turn's own tool loop: everything after its prompt up to, not including, its final reply (the
39
+ * transcript is the source; empty when the turn used no tools or the transcript hasn't caught up).
40
+ */
41
+ currentMessages?: RawMessage[];
42
+ /** The raw blocks (thinking, text) of this turn's final reply, when the transcript shows it. */
43
+ finalReply?: unknown[];
44
+ /**
45
+ * Skill bodies the transcript shows being injected, with when. The caller picks the one for its skill: `name` (the
46
+ * declared name, when the agent records it) or the name of `dir` can match `--skill`.
47
+ */
48
+ skillBodies?: Array<{
49
+ dir: string;
50
+ text: string;
51
+ timestamp?: string;
52
+ name?: string;
53
+ }>;
54
+ }
55
+ /** A user message and the assistant's final text reply to it, from earlier in the session. */
56
+ export interface EarlierExchange {
57
+ /** When the user line was written (ISO 8601); an exchange without one can't be placed relative to the anchor. */
58
+ timestamp?: string;
59
+ userText: string;
60
+ assistantText?: string;
61
+ usage?: Usage;
62
+ subagents?: SubagentUsage;
63
+ /** What came after the prompt: thinking, tool calls and results, replies. Absent for a hand-built exchange (text only). */
64
+ messages?: RawMessage[];
65
+ }
66
+ export type { TurnMessage } from '../sessions/blocks.js';
67
+ /** A parsed hook payload: whatever JSON the agent wrote to stdin. */
68
+ export type HookInput = Record<string, unknown>;
69
+ /** What an adapter needs besides the hook payload. */
70
+ export interface AdapterContext {
71
+ /** The `--skill` the hook was installed for. */
72
+ skill?: string;
73
+ }
74
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/hooks/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAEjE,6DAA6D;AAC7D,eAAO,MAAM,WAAW,gCAAiC,CAAC;AAC1D,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,uEAAuE;AACvE,MAAM,WAAW,QAAQ;IACvB,sFAAsF;IACtF,SAAS,EAAE,MAAM,CAAC;IAClB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,wGAAwG;IACxG,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,4DAA4D;IAC5D,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B;;;OAGG;IACH,eAAe,CAAC,EAAE,UAAU,EAAE,CAAC;IAC/B,gGAAgG;IAChG,UAAU,CAAC,EAAE,OAAO,EAAE,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvF;AAED,8FAA8F;AAC9F,MAAM,WAAW,eAAe;IAC9B,iHAAiH;IACjH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,2HAA2H;IAC3H,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;CACzB;AAED,YAAY,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,qEAAqE;AACrE,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEhD,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
@@ -0,0 +1,3 @@
1
+ /** Agents whose hook payloads `capture-turn` understands. */
2
+ export const HOOK_AGENTS = ['claude', 'copilot'];
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/hooks/types.ts"],"names":[],"mappings":"AAGA,6DAA6D;AAC7D,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAU,CAAC"}
@@ -1,5 +1,5 @@
1
1
  /** What kind of SDK call the flush worker should make to upload a record. */
2
- export type PendingKind = 'feedback';
2
+ export type PendingKind = 'feedback' | 'turn';
3
3
  export interface PendingRecord {
4
4
  /** Human-readable command name, e.g. "report-blocker". Used in the filename. */
5
5
  command: string;
@@ -9,6 +9,15 @@ export interface PendingRecord {
9
9
  payload: unknown;
10
10
  /** Optional stored-client id to resolve the api key/base url at flush time. */
11
11
  clientId?: string;
12
+ /** Public ingest key and base url supplied by a hook (no stored client). Wins over `clientId`. */
13
+ apiKey?: string;
14
+ baseUrl?: string;
15
+ /** Replay attempts started so far (counted before the upload, so a hook killed mid-upload still uses one). Hook-spooled turns are dropped after a few: with no server-side dedup, every retry that actually landed is a duplicate log. */
16
+ attempts?: number;
17
+ /** Short label put in the file name, e.g. to find records of one hook session without reading them. [a-z0-9-] only. */
18
+ fileTag?: string;
19
+ /** The send timed out, so the server may have stored the turn: a replay of this record can duplicate it. */
20
+ ambiguous?: boolean;
12
21
  /** ISO timestamp of when the record was saved locally. */
13
22
  savedAt: string;
14
23
  }
@@ -19,8 +28,19 @@ export declare function pendingDir(): string;
19
28
  * saves never collide.
20
29
  */
21
30
  export declare function savePendingRecord(record: PendingRecord): Promise<string>;
22
- /** Absolute paths of every saved record, sorted oldest-first by filename. */
31
+ /**
32
+ * `savePendingRecord` names files after `record.command`, so hook-captured turns saved with this
33
+ * command are `capture-turn-…`. The hook owns and replays them; the user-facing flow ignores them.
34
+ */
35
+ export declare const TURN_RECORD_COMMAND = "capture-turn";
36
+ /**
37
+ * Absolute paths of every saved record the user-facing flow owns (blocker feedback), oldest-first.
38
+ * Hook-captured turns are excluded: they replay themselves and must not inflate the "you have N
39
+ * locally-saved records" prompts shown at login or on the next command.
40
+ */
23
41
  export declare function listPendingFiles(): Promise<string[]>;
42
+ /** Absolute paths of the turns `capture-turn` spooled, oldest-first. */
43
+ export declare function listTurnFiles(): Promise<string[]>;
24
44
  export declare function countPending(): Promise<number>;
25
45
  export declare function readPendingRecord(filePath: string): Promise<PendingRecord>;
26
46
  export declare function removePendingRecord(filePath: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"pending-store.d.ts","sourceRoot":"","sources":["../src/pending-store.ts"],"names":[],"mappings":"AAiBA,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC;AAErC,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,IAAI,EAAE,WAAW,CAAC;IAClB,wEAAwE;IACxE,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAQD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ9E;AAED,6EAA6E;AAC7E,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAc1D;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAEpD;AAED,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAGhF;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEzE;AAED,0EAA0E;AAC1E,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAGrD;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEtD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAOpD"}
1
+ {"version":3,"file":"pending-store.d.ts","sourceRoot":"","sources":["../src/pending-store.ts"],"names":[],"mappings":"AAqBA,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,MAAM,CAAC;AAE9C,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,IAAI,EAAE,WAAW,CAAC;IAClB,wEAAwE;IACxE,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kGAAkG;IAClG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0OAA0O;IAC1O,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uHAAuH;IACvH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4GAA4G;IAC5G,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAQD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAS9E;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAmBlD;;;;GAIG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAE1D;AAED,wEAAwE;AACxE,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAEvD;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAEpD;AAED,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAGhF;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEzE;AAED,0EAA0E;AAC1E,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAGrD;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEtD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAOpD"}
@@ -6,6 +6,10 @@ import { configDir, atomicWriteFile } from './config.js';
6
6
  * Local fallback store for records that could not be sent to Coolhand because the
7
7
  * user was not logged in. Records live under `~/.coolhand/pending/` and are uploaded
8
8
  * by the background flush on the next `coolhand login` (see `commands/flush-pending`).
9
+ *
10
+ * It also holds turns the `capture-turn` hook could not deliver (`kind: 'turn'`, saved after a
11
+ * network or server failure and carrying their own key). Those are replayed by the hook itself,
12
+ * not by login or `flush-pending`, and are kept out of `listPendingFiles` (see `TURN_RECORD_COMMAND`).
9
13
  */
10
14
  const PENDING_SUBDIR = 'pending';
11
15
  const DIR_MODE = 0o700;
@@ -29,13 +33,19 @@ export async function savePendingRecord(record) {
29
33
  const dir = await ensurePendingDir();
30
34
  const safeCommand = record.command.replace(/[^a-zA-Z0-9_-]/g, '_');
31
35
  const safeStamp = record.savedAt.replace(/[:.]/g, '-');
32
- const fileName = `${safeCommand}-${safeStamp}-${randomBytes(4).toString('hex')}.json`;
36
+ const safeTag = record.fileTag ? `.${record.fileTag.replace(/[^a-z0-9-]/g, '')}` : '';
37
+ const fileName = `${safeCommand}-${safeStamp}-${randomBytes(4).toString('hex')}${safeTag}.json`;
33
38
  const filePath = path.join(dir, fileName);
34
39
  await atomicWriteFile(filePath, `${JSON.stringify(record, null, 2)}\n`, FILE_MODE);
35
40
  return filePath;
36
41
  }
37
- /** Absolute paths of every saved record, sorted oldest-first by filename. */
38
- export async function listPendingFiles() {
42
+ /**
43
+ * `savePendingRecord` names files after `record.command`, so hook-captured turns saved with this
44
+ * command are `capture-turn-…`. The hook owns and replays them; the user-facing flow ignores them.
45
+ */
46
+ export const TURN_RECORD_COMMAND = 'capture-turn';
47
+ const TURN_FILE_PREFIX = `${TURN_RECORD_COMMAND}-`;
48
+ async function listRecordFiles() {
39
49
  let entries;
40
50
  try {
41
51
  entries = await fs.readdir(pendingDir());
@@ -51,6 +61,18 @@ export async function listPendingFiles() {
51
61
  .sort()
52
62
  .map((name) => path.join(pendingDir(), name));
53
63
  }
64
+ /**
65
+ * Absolute paths of every saved record the user-facing flow owns (blocker feedback), oldest-first.
66
+ * Hook-captured turns are excluded: they replay themselves and must not inflate the "you have N
67
+ * locally-saved records" prompts shown at login or on the next command.
68
+ */
69
+ export async function listPendingFiles() {
70
+ return (await listRecordFiles()).filter((file) => !path.basename(file).startsWith(TURN_FILE_PREFIX));
71
+ }
72
+ /** Absolute paths of the turns `capture-turn` spooled, oldest-first. */
73
+ export async function listTurnFiles() {
74
+ return (await listRecordFiles()).filter((file) => path.basename(file).startsWith(TURN_FILE_PREFIX));
75
+ }
54
76
  export async function countPending() {
55
77
  return (await listPendingFiles()).length;
56
78
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pending-store.js","sourceRoot":"","sources":["../src/pending-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pending-store.js","sourceRoot":"","sources":["../src/pending-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEzD;;;;;;;;GAQG;AAEH,MAAM,cAAc,GAAG,SAAS,CAAC;AACjC,MAAM,QAAQ,GAAG,KAAK,CAAC;AACvB,MAAM,SAAS,GAAG,KAAK,CAAC;AACxB,kFAAkF;AAClF,MAAM,mBAAmB,GAAG,eAAe,CAAC;AA2B5C,MAAM,UAAU,UAAU;IACxB,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,cAAc,CAAC,CAAC;AAChD,CAAC;AAED,KAAK,UAAU,gBAAgB;IAC7B,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;IACzD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAqB;IAC3D,MAAM,GAAG,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACrC,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;IACnE,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACtF,MAAM,QAAQ,GAAG,GAAG,WAAW,IAAI,SAAS,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,OAAO,OAAO,CAAC;IAChG,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC1C,MAAM,eAAe,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACnF,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAClD,MAAM,gBAAgB,GAAG,GAAG,mBAAmB,GAAG,CAAC;AAEnD,KAAK,UAAU,eAAe;IAC5B,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAC3C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrD,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,OAAO,OAAO;SACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;SACjE,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;AAClD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,OAAO,CAAC,MAAM,eAAe,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC;AACvG,CAAC;AAED,wEAAwE;AACxE,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,OAAO,CAAC,MAAM,eAAe,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC;AACtG,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,OAAO,CAAC,MAAM,gBAAgB,EAAE,CAAC,CAAC,MAAM,CAAC;AAC3C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAgB;IACtD,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAChD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkB,CAAC;AAC1C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAgB;IACxD,MAAM,EAAE,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACzC,CAAC;AAED,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,MAAM,gBAAgB,EAAE,CAAC;IACzB,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,mBAAmB,CAAC,EAAE,EAAE,EAAE,SAAS,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,MAAM,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,mBAAmB,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAC7E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,mBAAmB,CAAC,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { PendingRecord } from './pending-store.js';
2
+ /**
3
+ * Send a single saved record to Coolhand. Resolves the api key/base url from the record itself when
4
+ * it carries one (hook-spooled turns do), otherwise from the stored client (or env) at flush time —
5
+ * by now the user is logged in. Returns true only when the server confirms the write; the SDK
6
+ * returns null (it does not throw) on a failed write, so a falsy result means "keep the file and
7
+ * retry later".
8
+ */
9
+ export declare function uploadRecord(record: PendingRecord, opts?: {
10
+ timeoutMs?: number;
11
+ }): Promise<boolean>;
12
+ //# sourceMappingURL=pending-upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-upload.d.ts","sourceRoot":"","sources":["../src/pending-upload.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CA4C7G"}
@@ -0,0 +1,51 @@
1
+ import { Coolhand } from 'coolhand-node';
2
+ import { logTurn } from './hooks/log-turn.js';
3
+ import { loadConfig, getClient } from './config.js';
4
+ import { DEFAULT_BASE_URL } from './types.js';
5
+ /**
6
+ * Send a single saved record to Coolhand. Resolves the api key/base url from the record itself when
7
+ * it carries one (hook-spooled turns do), otherwise from the stored client (or env) at flush time —
8
+ * by now the user is logged in. Returns true only when the server confirms the write; the SDK
9
+ * returns null (it does not throw) on a failed write, so a falsy result means "keep the file and
10
+ * retry later".
11
+ */
12
+ export async function uploadRecord(record, opts = {}) {
13
+ // A record that carries its own key (hook-spooled turns) never touches the stored config, so an
14
+ // unreadable config.json can't stop it from being replayed.
15
+ const client = record.apiKey ? undefined : getClient(await loadConfig(), record.clientId);
16
+ const apiKey = record.apiKey ?? client?.api_key ?? process.env.COOLHAND_API_KEY;
17
+ if (!apiKey) {
18
+ return false;
19
+ }
20
+ const baseUrl = record.baseUrl ?? client?.base_url ?? DEFAULT_BASE_URL;
21
+ let coolhand;
22
+ try {
23
+ // requestTimeoutMs makes the SDK abort the request itself, not just stop us waiting for it.
24
+ coolhand = new Coolhand({ apiKey, baseUrl, silent: true, ...(opts.timeoutMs ? { requestTimeoutMs: opts.timeoutMs } : {}) });
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ if (record.kind === 'feedback') {
30
+ const result = await coolhand.createFeedback(record.payload);
31
+ return result !== null && result !== undefined;
32
+ }
33
+ if (record.kind === 'turn') {
34
+ const { envelope, collector, metadata, declared } = record.payload;
35
+ // A replay may duplicate a turn that actually landed (the server does not dedup), so mark it for consumers.
36
+ const result = await logTurn(coolhand, envelope, {
37
+ collector,
38
+ declared,
39
+ metadata: {
40
+ ...metadata,
41
+ replayed: true,
42
+ replay_attempt: (record.attempts ?? 0) + 1,
43
+ // The original send timed out rather than being refused, so it may already be stored.
44
+ ...(record.ambiguous ? { possible_duplicate: true } : {}),
45
+ },
46
+ });
47
+ return result !== null && result !== undefined;
48
+ }
49
+ return false;
50
+ }
51
+ //# sourceMappingURL=pending-upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-upload.js","sourceRoot":"","sources":["../src/pending-upload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAoD,MAAM,eAAe,CAAC;AAC3F,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAqB,EAAE,OAA+B,EAAE;IACzF,gGAAgG;IAChG,4DAA4D;IAC5D,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,UAAU,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC1F,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,EAAE,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAChF,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,MAAM,EAAE,QAAQ,IAAI,gBAAgB,CAAC;IAEvE,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,4FAA4F;QAC5F,QAAQ,GAAG,IAAI,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9H,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,OAAgC,CAAC,CAAC;QACtF,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,CAAC;IACjD,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC3B,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,OAK1D,CAAC;QACF,4GAA4G;QAC5G,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE;YAC/C,SAAS;YACT,QAAQ;YACR,QAAQ,EAAE;gBACR,GAAG,QAAQ;gBACX,QAAQ,EAAE,IAAI;gBACd,cAAc,EAAE,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC;gBAC1C,sFAAsF;gBACtF,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC1D;SACF,CAAC,CAAC;QACH,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,CAAC;IACjD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,51 @@
1
+ /** Per-block caps. Redaction runs before truncation, so a secret is never cut in half and left half-visible. */
2
+ export declare const MAX_TURN_TEXT_CHARS = 100000;
3
+ /** A message as it sits in the transcript: raw blocks, not yet redacted or capped. */
4
+ export interface RawMessage {
5
+ role: 'user' | 'assistant';
6
+ /** The request id of the model call (assistant messages), used to merge its content-block lines. */
7
+ key?: string;
8
+ blocks: unknown[];
9
+ }
10
+ /** The blocks that are sent. Text only in the sense of no binary data; thinking, tool calls and results included. */
11
+ export type ContentBlock = {
12
+ type: 'text';
13
+ text: string;
14
+ } | {
15
+ type: 'thinking';
16
+ thinking: string;
17
+ } | {
18
+ type: 'tool_use';
19
+ id?: string;
20
+ name: string;
21
+ input: unknown;
22
+ } | {
23
+ type: 'tool_result';
24
+ tool_use_id?: string;
25
+ content: string;
26
+ is_error?: boolean;
27
+ };
28
+ export interface TurnMessage {
29
+ role: 'user' | 'assistant';
30
+ content: string | ContentBlock[];
31
+ }
32
+ /** Redacted and capped to `max` characters, with a marker when cut: the one way any captured text is sanitized. */
33
+ export declare function cleanText(text: string, max?: number): string;
34
+ /** Redact and cap the strings of a tool call's input, keeping its shape; bound arrays, keys and depth. */
35
+ export declare function sanitizeInput(value: unknown, depth?: number, secret?: boolean, redact?: boolean): unknown;
36
+ /**
37
+ * One transcript block as it is sent: redacted, capped, and stripped of anything opaque. Thinking keeps
38
+ * its text but not its `signature` (a long blob of no use to a reader); redacted/encrypted thinking, and
39
+ * block types we don't know, are left out.
40
+ */
41
+ export declare function sanitizeBlock(block: unknown): ContentBlock | undefined;
42
+ /**
43
+ * A transcript message as it is sent. An assistant message that is nothing but one text block is sent as
44
+ * a plain string, like an ordinary chat; anything else keeps its blocks. `undefined` when nothing survives.
45
+ */
46
+ export declare function sanitizeMessage(raw: RawMessage): TurnMessage | undefined;
47
+ /** Close to the characters `sanitizeMessage` would keep, computed without redacting anything. */
48
+ export declare function messageSize(raw: RawMessage): number;
49
+ /** The text of the text blocks of a raw message, joined the way the exchange parser joins them. */
50
+ export declare function rawMessageText(raw: RawMessage): string;
51
+ //# sourceMappingURL=blocks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../../src/sessions/blocks.ts"],"names":[],"mappings":"AAEA,gHAAgH;AAChH,eAAO,MAAM,mBAAmB,SAAU,CAAC;AA4B3C,sFAAsF;AACtF,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,oGAAoG;IACpG,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB;AAED,qHAAqH;AACrH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEvF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,CAAC;CAClC;AAkBD,mHAAmH;AACnH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAsB,GAAG,MAAM,CAEzE;AAED,0GAA0G;AAC1G,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAI,EAAE,MAAM,UAAQ,EAAE,MAAM,UAAO,GAAG,OAAO,CAkC/F;AAsBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CA8BtE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,UAAU,GAAG,WAAW,GAAG,SAAS,CASxE;AAUD,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAkBnD;AAED,mGAAmG;AACnG,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAMtD"}
@@ -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: string;
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.