@manyoars/oars 0.2.0 → 0.3.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 (241) hide show
  1. package/README.md +132 -28
  2. package/dist/api.js +1 -1
  3. package/dist/bin.js +6 -29
  4. package/dist/cli.d.ts +6 -3
  5. package/dist/cli.js +367 -88
  6. package/dist/commands/keys.d.ts +31 -0
  7. package/dist/commands/keys.js +65 -0
  8. package/dist/commands/login.js +2 -2
  9. package/dist/errors.d.ts +13 -13
  10. package/dist/errors.js +28 -11
  11. package/dist/gate/checks.d.ts +68 -0
  12. package/dist/gate/checks.js +584 -0
  13. package/dist/gate/command.d.ts +14 -0
  14. package/dist/gate/command.js +61 -0
  15. package/dist/gate/hook-fill-main.d.ts +1 -0
  16. package/dist/gate/hook-fill-main.js +16 -0
  17. package/dist/gate/hook-fill.d.ts +38 -0
  18. package/dist/gate/hook-fill.js +94 -0
  19. package/dist/gate/hook-main.d.ts +1 -0
  20. package/dist/gate/hook-main.js +35 -0
  21. package/dist/gate/hook-process.d.ts +14 -0
  22. package/dist/gate/hook-process.js +64 -0
  23. package/dist/gate/hook.d.ts +46 -0
  24. package/dist/gate/hook.js +256 -0
  25. package/dist/gate/install.d.ts +25 -0
  26. package/dist/gate/install.js +126 -0
  27. package/dist/gate/local-bin.d.ts +8 -0
  28. package/dist/gate/local-bin.js +36 -0
  29. package/dist/gate/notes.d.ts +19 -0
  30. package/dist/gate/notes.js +27 -0
  31. package/dist/gate/novelty.d.ts +35 -0
  32. package/dist/gate/novelty.js +99 -0
  33. package/dist/gate/policy.d.ts +25 -0
  34. package/dist/gate/policy.js +73 -0
  35. package/dist/gate/record.d.ts +120 -0
  36. package/dist/gate/record.js +125 -0
  37. package/dist/gate/registry-state.d.ts +10 -0
  38. package/dist/gate/registry-state.js +30 -0
  39. package/dist/gate/registry.d.ts +56 -0
  40. package/dist/gate/registry.js +94 -0
  41. package/dist/gate/replay.d.ts +20 -0
  42. package/dist/gate/replay.js +257 -0
  43. package/dist/gate/report.d.ts +50 -0
  44. package/dist/gate/report.js +89 -0
  45. package/dist/gate/scope.d.ts +7 -0
  46. package/dist/gate/scope.js +27 -0
  47. package/dist/gate/transcripts.d.ts +68 -0
  48. package/dist/gate/transcripts.js +232 -0
  49. package/dist/hook-bin.d.ts +1 -0
  50. package/dist/hook-bin.js +25 -0
  51. package/dist/issuer.d.ts +7 -1
  52. package/dist/issuer.js +18 -1
  53. package/dist/main.d.ts +1 -0
  54. package/dist/main.js +64 -0
  55. package/dist/oidc.js +1 -1
  56. package/dist/sessions/actions.d.ts +35 -0
  57. package/dist/sessions/actions.js +351 -0
  58. package/dist/sessions/allow.d.ts +14 -0
  59. package/dist/sessions/allow.js +51 -0
  60. package/dist/sessions/archive.d.ts +11 -0
  61. package/dist/sessions/archive.js +38 -0
  62. package/dist/sessions/audit.d.ts +42 -0
  63. package/dist/sessions/audit.js +104 -0
  64. package/dist/sessions/baseline.d.ts +62 -0
  65. package/dist/sessions/baseline.js +188 -0
  66. package/dist/sessions/capture-runtime.d.ts +10 -0
  67. package/dist/sessions/capture-runtime.js +12 -0
  68. package/dist/sessions/cell-increments.d.ts +43 -0
  69. package/dist/sessions/cell-increments.js +209 -0
  70. package/dist/sessions/cells.d.ts +79 -0
  71. package/dist/sessions/cells.js +181 -0
  72. package/dist/sessions/codex.d.ts +53 -0
  73. package/dist/sessions/codex.js +679 -0
  74. package/dist/sessions/command.d.ts +117 -0
  75. package/dist/sessions/command.js +373 -0
  76. package/dist/sessions/copilot.d.ts +40 -0
  77. package/dist/sessions/copilot.js +432 -0
  78. package/dist/sessions/cursor.d.ts +24 -0
  79. package/dist/sessions/cursor.js +324 -0
  80. package/dist/sessions/derive.d.ts +57 -0
  81. package/dist/sessions/derive.js +320 -0
  82. package/dist/sessions/exclusion.d.ts +81 -0
  83. package/dist/sessions/exclusion.js +353 -0
  84. package/dist/sessions/extract.d.ts +179 -0
  85. package/dist/sessions/extract.js +1774 -0
  86. package/dist/sessions/fold.d.ts +17 -0
  87. package/dist/sessions/fold.js +71 -0
  88. package/dist/sessions/frame.d.ts +24 -0
  89. package/dist/sessions/frame.js +95 -0
  90. package/dist/sessions/gemini.d.ts +37 -0
  91. package/dist/sessions/gemini.js +613 -0
  92. package/dist/sessions/gold-arms.d.ts +67 -0
  93. package/dist/sessions/gold-arms.js +172 -0
  94. package/dist/sessions/gold.d.ts +197 -0
  95. package/dist/sessions/gold.js +463 -0
  96. package/dist/sessions/graph.d.ts +12 -0
  97. package/dist/sessions/graph.js +34 -0
  98. package/dist/sessions/grok.d.ts +36 -0
  99. package/dist/sessions/grok.js +421 -0
  100. package/dist/sessions/hook-calls.d.ts +3 -0
  101. package/dist/sessions/hook-calls.js +47 -0
  102. package/dist/sessions/hook-index.d.ts +53 -0
  103. package/dist/sessions/hook-index.js +157 -0
  104. package/dist/sessions/hooks.d.ts +13 -0
  105. package/dist/sessions/hooks.js +170 -0
  106. package/dist/sessions/identity.d.ts +22 -0
  107. package/dist/sessions/identity.js +101 -0
  108. package/dist/sessions/install.d.ts +54 -0
  109. package/dist/sessions/install.js +201 -0
  110. package/dist/sessions/lineage.d.ts +19 -0
  111. package/dist/sessions/lineage.js +118 -0
  112. package/dist/sessions/mcp.d.ts +6 -0
  113. package/dist/sessions/mcp.js +153 -0
  114. package/dist/sessions/merkle.d.ts +7 -0
  115. package/dist/sessions/merkle.js +28 -0
  116. package/dist/sessions/motif.d.ts +43 -0
  117. package/dist/sessions/motif.js +59 -0
  118. package/dist/sessions/package-resolve.d.ts +30 -0
  119. package/dist/sessions/package-resolve.js +183 -0
  120. package/dist/sessions/pending.d.ts +34 -0
  121. package/dist/sessions/pending.js +142 -0
  122. package/dist/sessions/pool-command.d.ts +44 -0
  123. package/dist/sessions/pool-command.js +542 -0
  124. package/dist/sessions/pool-delta.d.ts +54 -0
  125. package/dist/sessions/pool-delta.js +190 -0
  126. package/dist/sessions/pool-outbox.d.ts +63 -0
  127. package/dist/sessions/pool-outbox.js +260 -0
  128. package/dist/sessions/pool-print.d.ts +26 -0
  129. package/dist/sessions/pool-print.js +68 -0
  130. package/dist/sessions/pool-wire.d.ts +225 -0
  131. package/dist/sessions/pool-wire.js +620 -0
  132. package/dist/sessions/pool.d.ts +32 -0
  133. package/dist/sessions/pool.js +164 -0
  134. package/dist/sessions/projection.d.ts +75 -0
  135. package/dist/sessions/projection.js +165 -0
  136. package/dist/sessions/public-vocabulary.d.ts +73 -0
  137. package/dist/sessions/public-vocabulary.js +302 -0
  138. package/dist/sessions/query.d.ts +111 -0
  139. package/dist/sessions/query.js +246 -0
  140. package/dist/sessions/read-adoption.d.ts +2 -0
  141. package/dist/sessions/read-adoption.js +27 -0
  142. package/dist/sessions/read-call-shape.d.ts +2 -0
  143. package/dist/sessions/read-call-shape.js +25 -0
  144. package/dist/sessions/read-change.d.ts +2 -0
  145. package/dist/sessions/read-change.js +30 -0
  146. package/dist/sessions/read-cost.d.ts +4 -0
  147. package/dist/sessions/read-cost.js +37 -0
  148. package/dist/sessions/read-day-move.d.ts +3 -0
  149. package/dist/sessions/read-day-move.js +65 -0
  150. package/dist/sessions/read-fit.d.ts +7 -0
  151. package/dist/sessions/read-fit.js +41 -0
  152. package/dist/sessions/read-interaction.d.ts +5 -0
  153. package/dist/sessions/read-interaction.js +62 -0
  154. package/dist/sessions/read-lead-lag.d.ts +15 -0
  155. package/dist/sessions/read-lead-lag.js +81 -0
  156. package/dist/sessions/read-live.d.ts +6 -0
  157. package/dist/sessions/read-live.js +38 -0
  158. package/dist/sessions/read-method.d.ts +2 -0
  159. package/dist/sessions/read-method.js +29 -0
  160. package/dist/sessions/read-next-step.d.ts +2 -0
  161. package/dist/sessions/read-next-step.js +35 -0
  162. package/dist/sessions/read-order.d.ts +54 -0
  163. package/dist/sessions/read-order.js +62 -0
  164. package/dist/sessions/read-readable.d.ts +8 -0
  165. package/dist/sessions/read-readable.js +24 -0
  166. package/dist/sessions/read-recovery.d.ts +20 -0
  167. package/dist/sessions/read-recovery.js +97 -0
  168. package/dist/sessions/read-rows.d.ts +13 -0
  169. package/dist/sessions/read-rows.js +47 -0
  170. package/dist/sessions/read-substitution.d.ts +5 -0
  171. package/dist/sessions/read-substitution.js +51 -0
  172. package/dist/sessions/read-support.d.ts +48 -0
  173. package/dist/sessions/read-support.js +73 -0
  174. package/dist/sessions/read-tool-day.d.ts +2 -0
  175. package/dist/sessions/read-tool-day.js +26 -0
  176. package/dist/sessions/read-version-boundary.d.ts +31 -0
  177. package/dist/sessions/read-version-boundary.js +95 -0
  178. package/dist/sessions/read.d.ts +79 -0
  179. package/dist/sessions/read.js +84 -0
  180. package/dist/sessions/recall-consequence.d.ts +45 -0
  181. package/dist/sessions/recall-consequence.js +179 -0
  182. package/dist/sessions/recall-object.d.ts +137 -0
  183. package/dist/sessions/recall-object.js +77 -0
  184. package/dist/sessions/recall-pool.d.ts +19 -0
  185. package/dist/sessions/recall-pool.js +96 -0
  186. package/dist/sessions/recall-push.d.ts +26 -0
  187. package/dist/sessions/recall-push.js +95 -0
  188. package/dist/sessions/recall-reads.d.ts +117 -0
  189. package/dist/sessions/recall-reads.js +410 -0
  190. package/dist/sessions/recall-render.d.ts +19 -0
  191. package/dist/sessions/recall-render.js +168 -0
  192. package/dist/sessions/redact.d.ts +6 -0
  193. package/dist/sessions/redact.js +54 -0
  194. package/dist/sessions/repo-root.d.ts +10 -0
  195. package/dist/sessions/repo-root.js +101 -0
  196. package/dist/sessions/schema.d.ts +644 -0
  197. package/dist/sessions/schema.js +148 -0
  198. package/dist/sessions/session-shape.d.ts +73 -0
  199. package/dist/sessions/session-shape.js +342 -0
  200. package/dist/sessions/sources.d.ts +50 -0
  201. package/dist/sessions/sources.js +159 -0
  202. package/dist/sessions/steps.d.ts +105 -0
  203. package/dist/sessions/steps.js +250 -0
  204. package/dist/sessions/store.d.ts +297 -0
  205. package/dist/sessions/store.js +1478 -0
  206. package/dist/sessions/strata.d.ts +26 -0
  207. package/dist/sessions/strata.js +77 -0
  208. package/dist/sessions/survival.d.ts +25 -0
  209. package/dist/sessions/survival.js +230 -0
  210. package/dist/sessions/verify.d.ts +53 -0
  211. package/dist/sessions/verify.js +286 -0
  212. package/dist/sessions/vocab-load.d.ts +26 -0
  213. package/dist/sessions/vocab-load.js +61 -0
  214. package/dist/sessions/vscode-copilot.d.ts +38 -0
  215. package/dist/sessions/vscode-copilot.js +477 -0
  216. package/package.json +4 -5
  217. package/vocab/crates.json +1 -0
  218. package/vocab/go.json +1 -0
  219. package/vocab/harness.json +1 -0
  220. package/vocab/homebrew.json +1 -0
  221. package/vocab/manifest.json +1 -0
  222. package/vocab/maven.json +1 -0
  223. package/vocab/models.json +1 -0
  224. package/vocab/npm.json +1 -0
  225. package/vocab/programs.json +1 -0
  226. package/vocab/pypi.json +1 -0
  227. package/vocab/rubygems.json +1 -0
  228. package/dist/commands/deploy-input.d.ts +0 -38
  229. package/dist/commands/deploy-input.js +0 -150
  230. package/dist/commands/deploy.d.ts +0 -22
  231. package/dist/commands/deploy.js +0 -124
  232. package/dist/commands/status.d.ts +0 -7
  233. package/dist/commands/status.js +0 -18
  234. package/dist/commands/templates.d.ts +0 -11
  235. package/dist/commands/templates.js +0 -32
  236. package/dist/commands/test-charge.d.ts +0 -11
  237. package/dist/commands/test-charge.js +0 -34
  238. package/dist/journal.d.ts +0 -26
  239. package/dist/journal.js +0 -58
  240. package/parity/deploy-plans/paid-api.json +0 -34
  241. package/parity/deploy-plans/per-task-agent.json +0 -34
@@ -0,0 +1,432 @@
1
+ // The GitHub Copilot CLI source: a Copilot CLI session log becomes the same capture records a Claude Code transcript does.
2
+ //
3
+ // FIXTURE-DERIVED: written from the public SDK's generated event types (github.com/github/copilot-sdk @ d28a136, MIT,
4
+ // nodejs/src/generated/session-events.ts, "generated from session-events.schema.json") and its docs, not from a live store. Nothing was copied; the
5
+ // reader is original. The CLI itself (github/copilot-cli) is closed source, so the schema is the only statement of the file's shape, and no vendor
6
+ // text promises the on-disk format. `docs/sessions-store.md` says what a live run would still have to prove.
7
+ //
8
+ // Where it reads from (`copilotRoot`): `$COPILOT_HOME`, else `~/.copilot` (`%USERPROFILE%\.copilot` on Windows). Inside it,
9
+ // `session-state/<sessionId>/events.jsonl` is one session (docs/features/session-persistence.md, "Linked event journals", which names the file; the
10
+ // SDK's `session.resume.eventsFileSizeBytes`, session-events.ts, is a second mention; the changelog's 0.0.342 entry moved new sessions there). The older
11
+ // `history-session-state` is migrated by the CLI on resume and is not read. A rotated archive `events.N.jsonl` is read, oldest N first, then the live
12
+ // file, as one session (`joinFiles`). Its head file holds a `session.journal_link` and `checkpoint: true` events that replay earlier state; with the archives
13
+ // read those replays are dropped (and any repeated event id), and `eventLines` itself reads a lone head like any other line (a fixture in copilot.test.ts holds one) and ignores `session.journal_link`.
14
+ // A line is `{id, parentId, timestamp, type, data, agentId?, ephemeral?, checkpoint?}`. An `ephemeral` event is never written to disk (session-events.ts,
15
+ // the `ephemeral` field), which is why `assistant.usage` is not read: it is not there. Like codex.ts, the reader writes each session out as the
16
+ // transcript lines `captureSession` reads, so redaction, the 1,000-character line cap, the salted hashes, and the skeleton are the Claude Code
17
+ // path's own code:
18
+ // session.start data {sessionId, copilotVersion, selectedModel, context.cwd}: the id, the harness version, the model, and the working directory
19
+ // session.model_change data {newModel}: the model from then on
20
+ // session.resume data {selectedModel, context.cwd}, and session.context_changed data {cwd}: the model and working directory from then on
21
+ // user.message data {content}: a prompt; one with `isAutopilotContinuation`, or a `source` starting `skill-` (a hidden skill injection) or `agent-`
22
+ // (a prompt from another agent), is the harness writing, not the operator, and is not read
23
+ // assistant.message data {content, model, outputTokens, requestId, apiCallId, messageId, toolRequests[{toolCallId, name, arguments, mcpServerName, mcpToolName}]}
24
+ // one model call may be several chunks sharing `requestId`/`apiCallId`: they are one response. A request for an edit tool
25
+ // (`edit`, `str_replace_editor`, `apply_patch`, `write_file`) is an `Edit` by name, so a failed one is still an edit
26
+ // tool.execution_start data {toolCallId, toolName, shellToolInfo}: a call with `shellToolInfo` is a shell call whatever its name (the schema lists no tool
27
+ // names and the vendor's own hook examples disagree), so a shell call is read by shape
28
+ // tool.execution_complete data {toolCallId, success, error.message, result.content, shellExecution.exitCode, fileEdits[{kind, path}]}
29
+ // the outcome: `shellExecution` (which also marks a shell call the start did not) gives the real exit code and it decides, whatever
30
+ // `success` says; `success` decides only a call with no exit code. `fileEdits` makes a call an edit and overrides the path
31
+ // session.shutdown data {agentMetrics.main.modelMetrics, else modelMetrics: [model].usage{inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens}}:
32
+ // per-model totals (the main conversation's when the runtime writes them, since the session-wide ones include subagents), the only
33
+ // place this reader takes them from (a `session.usage_checkpoint` may hold per-model totals; see the docs); they land once on the first line of the model's last response
34
+ // Events that carry an `agentId` (or the deprecated `parentToolCallId`) are a subagent's own and are not read; the parent's call that spawned it is. `session.error` and `abort` are not read:
35
+ // a call they cut short has no completion event and stays `none`.
36
+ import { createReadStream } from 'node:fs';
37
+ import { readdir, readFile, stat } from 'node:fs/promises';
38
+ import { homedir } from 'node:os';
39
+ import { basename, join } from 'node:path';
40
+ import { createInterface } from 'node:readline';
41
+ /** The Copilot home for an environment; a test passes its own so it never reads the real one. */
42
+ export const copilotRoot = (env, home = homedir()) => {
43
+ const set = env.COPILOT_HOME;
44
+ return set === undefined || set === '' ? join(home, '.copilot') : set;
45
+ };
46
+ const isObj = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
47
+ const str = (value) => (typeof value === 'string' && value !== '' ? value : undefined);
48
+ const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined);
49
+ const parse = (text) => {
50
+ try {
51
+ return JSON.parse(text);
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ };
57
+ const SAFE_ID = /^(?!\.+$)[A-Za-z0-9._-]+$/;
58
+ const EVENTS = 'events.jsonl';
59
+ /** A rotated archive's name; N counts up as the log rotates (docs/features/session-persistence.md, "Linked event journals"); the reader orders by N and does not follow each head's `previousFile`. */
60
+ const ARCHIVE = /^events\.(\d+)\.jsonl$/;
61
+ /** How many leading lines are searched for `session.start`; a rotated head may begin with a `session.journal_link` and checkpoint events. */
62
+ const HEAD_LINES = 8;
63
+ /** An error's message with `file` cut to its base name, so a reason that is logged names no folder. */
64
+ const reasonOf = (error, file) => (error instanceof Error ? error.message : String(error)).split(file).join(basename(file));
65
+ const missing = (error) => error instanceof Error && 'code' in error && error.code === 'ENOENT';
66
+ /** The working directory `session.start` names, read from the first lines only so the cost does not grow with the file. */
67
+ const startFolder = async (file) => {
68
+ const stream = createReadStream(file, { encoding: 'utf8' });
69
+ const lines = createInterface({ input: stream, crlfDelay: Infinity });
70
+ try {
71
+ let seen = 0;
72
+ for await (const line of lines) {
73
+ const entry = parse(line);
74
+ if (isObj(entry) && entry.type === 'session.start' && isObj(entry.data) && isObj(entry.data.context))
75
+ return str(entry.data.context.cwd) ?? null;
76
+ seen += 1;
77
+ if (seen >= HEAD_LINES)
78
+ break;
79
+ }
80
+ return null;
81
+ }
82
+ finally {
83
+ lines.close();
84
+ stream.destroy();
85
+ }
86
+ };
87
+ const iso = (value) => {
88
+ const text = str(value);
89
+ // An absolute time only: a missing or relative one (not a date after 2000) says nothing about when.
90
+ return text !== undefined && Date.parse(text) >= Date.parse('2000-01-01T00:00:00Z') ? text : undefined;
91
+ };
92
+ /** The non-blank lines that are not JSON (a torn write), kept so `captureSession` counts them and the deposit line reports them. */
93
+ export const eventBadLines = (text) => text.split('\n').filter((raw) => raw.trim() !== '' && parse(raw) === undefined);
94
+ /**
95
+ * The exit code of a shell call: `shellExecution.exitCode` (session-events.ts, ToolExecutionCompleteData, "Experimental shell completion facts"). The
96
+ * schema's `result.contents` also has `shell_exit` parts, but it says `shellExecution` is captured before persisted result contents are stripped, and no
97
+ * source shows a persisted line holding them, so they are not read.
98
+ */
99
+ const exitOf = (data) => (isObj(data.shellExecution) ? num(data.shellExecution.exitCode) : undefined);
100
+ /** A nonzero exit code is a failure whatever `success` says; a negative one is no exit status, so the output stands as the error line. */
101
+ const failureOf = (exit, text) => (exit < 0 && text.trim() !== '' ? text : `Exit code ${String(exit)}\n${text}`);
102
+ /**
103
+ * Splits `total` over lines in proportion to their `capacities`, whole tokens, none above its capacity; what the capacities cannot hold
104
+ * (a total above their sum) is dropped. The remainder after rounding down goes to the first lines with room, so the split is stable.
105
+ */
106
+ const shareOut = (total, capacities) => {
107
+ const room = capacities.reduce((sum, capacity) => sum + capacity, 0);
108
+ const wanted = Math.min(total, room);
109
+ const shares = capacities.map((capacity) => (room === 0 ? 0 : Math.floor((wanted * capacity) / room)));
110
+ let rest = wanted - shares.reduce((sum, share) => sum + share, 0);
111
+ for (let index = 0; index < shares.length && rest > 0; index += 1) {
112
+ const add = Math.min(rest, (capacities[index] ?? 0) - (shares[index] ?? 0));
113
+ shares[index] = (shares[index] ?? 0) + add;
114
+ rest -= add;
115
+ }
116
+ return shares;
117
+ };
118
+ /** Tools that are edits by name, for when the runtime writes no `fileEdits` or the edit failed. */
119
+ const EDIT_TOOLS = new Set(['edit', 'str_replace_editor', 'apply_patch', 'write_file']);
120
+ /** One session as the lines `captureSession` reads. */
121
+ export const eventLines = (text, fallbackId) => {
122
+ const nonblank = text.split('\n').filter((raw) => raw.trim() !== '');
123
+ if (nonblank.length > 0 && !nonblank.some((raw) => isObj(parse(raw))))
124
+ throw new Error('no parseable line');
125
+ const lines = [];
126
+ const base = { sessionId: fallbackId };
127
+ let at;
128
+ let model;
129
+ let sequence = 0;
130
+ // Each model's latest shutdown totals. Upstream's accounting is session-wide and kept for resume (the SDK usage doc, session-events.ts:4450), so a later
131
+ // shutdown replaces an earlier one instead of adding to it (inferred, see the docs: no line states that a resumed count continues).
132
+ const totals = new Map();
133
+ const responses = [];
134
+ const calls = new Map();
135
+ const resolved = new Set();
136
+ const stamp = (value) => {
137
+ const candidate = iso(value);
138
+ if (candidate !== undefined && (at === undefined || Date.parse(candidate) >= Date.parse(at)))
139
+ at = candidate;
140
+ return at ?? new Date(0).toISOString();
141
+ };
142
+ const result = (id, failure, when, output = '') => {
143
+ sequence += 1;
144
+ lines.push({
145
+ ...base,
146
+ type: 'user',
147
+ uuid: `${fallbackId}-r${String(sequence)}`,
148
+ timestamp: when,
149
+ // The completion event is stamped with its own time, but the start is not paired to it here, so no duration is claimed.
150
+ noDuration: true,
151
+ // The output follows an `Exit code 0` line, the shape a Claude Code Bash result has, so a `[branch sha]` line is still found for commit survival.
152
+ message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, is_error: failure !== undefined, content: failure ?? (output === '' ? '' : `Exit code 0\n${output}`) }] },
153
+ });
154
+ };
155
+ const usageOf = (usage) => {
156
+ return {
157
+ // `inputTokens` already includes the cache reads and writes (the SDK usage doc, line 58), and capture's fields are disjoint, so only the uncached part is input.
158
+ input_tokens: Math.max((num(usage.inputTokens) ?? 0) - (num(usage.cacheReadTokens) ?? 0) - (num(usage.cacheWriteTokens) ?? 0), 0),
159
+ cache_read_input_tokens: num(usage.cacheReadTokens) ?? 0,
160
+ cache_creation_input_tokens: num(usage.cacheWriteTokens) ?? 0,
161
+ };
162
+ };
163
+ // The tool's own words say it is a shell call: a start with `shellToolInfo`, or a completion with `shellExecution`.
164
+ const asShell = (known, command) => {
165
+ known.shell = true;
166
+ known.block.native = known.block.native ?? known.block.name;
167
+ known.block.name = 'Bash';
168
+ known.block.input = command === undefined ? {} : { command };
169
+ };
170
+ for (const raw of nonblank) {
171
+ const entry = parse(raw);
172
+ if (!isObj(entry) || !isObj(entry.data) || str(entry.type) === undefined)
173
+ continue;
174
+ // A subagent's own events are inline in the file; the parent's call that spawned it is the record.
175
+ // `parentToolCallId` is the deprecated spelling of the same fact (session-events.ts, "when this event originates from a sub-agent").
176
+ if (str(entry.agentId) !== undefined || str(entry.data.parentToolCallId) !== undefined)
177
+ continue;
178
+ const data = entry.data;
179
+ const when = stamp(entry.timestamp);
180
+ const type = entry.type;
181
+ if (type === 'session.start') {
182
+ base.sessionId = str(data.sessionId) ?? base.sessionId;
183
+ base.version = str(data.copilotVersion) ?? base.version;
184
+ model = str(data.selectedModel) ?? model;
185
+ const cwd = isObj(data.context) ? str(data.context.cwd) : undefined;
186
+ if (cwd !== undefined)
187
+ base.cwd = cwd;
188
+ stamp(data.startTime);
189
+ }
190
+ else if (type === 'session.resume') {
191
+ // A resume may happen elsewhere: its context and model are the session's from here on.
192
+ const cwd = isObj(data.context) ? str(data.context.cwd) : undefined;
193
+ if (cwd !== undefined)
194
+ base.cwd = cwd;
195
+ model = str(data.selectedModel) ?? model;
196
+ }
197
+ else if (type === 'session.context_changed') {
198
+ base.cwd = str(data.cwd) ?? base.cwd;
199
+ }
200
+ else if (type === 'session.model_change') {
201
+ model = str(data.newModel) ?? model;
202
+ }
203
+ else if (type === 'user.message') {
204
+ const content = str(data.content);
205
+ // `source` marks text the harness wrote: a skill injected as a hidden message (`skill-<name>`) or a prompt from another agent (`agent-<id>`).
206
+ const source = str(data.source) ?? '';
207
+ if (content !== undefined && data.isAutopilotContinuation !== true && !source.startsWith('skill-') && !source.startsWith('agent-')) {
208
+ sequence += 1;
209
+ lines.push({ ...base, type: 'user', uuid: `${fallbackId}-u${String(sequence)}`, timestamp: when, message: { role: 'user', content: [{ type: 'text', text: content }] } });
210
+ }
211
+ }
212
+ else if (type === 'assistant.message') {
213
+ const content = [];
214
+ const text = str(data.content);
215
+ if (text !== undefined)
216
+ content.push({ type: 'text', text });
217
+ for (const request of Array.isArray(data.toolRequests) ? data.toolRequests : []) {
218
+ const id = isObj(request) ? str(request.toolCallId) : undefined;
219
+ const name = isObj(request) ? str(request.name) : undefined;
220
+ if (!isObj(request) || id === undefined || name === undefined)
221
+ continue;
222
+ const server = str(request.mcpServerName);
223
+ const tool = str(request.mcpToolName);
224
+ const input = isObj(request.arguments) ? request.arguments : {};
225
+ const block = { type: 'tool_use', id, name: server !== undefined && tool !== undefined ? `mcp__${server}__${tool}` : name, input };
226
+ // A tool known by name as an edit is one even when it fails or the runtime writes no `fileEdits`; `fileEdits` still overrides the path on completion.
227
+ if (EDIT_TOOLS.has(name) && server === undefined) {
228
+ const path = str(input.path) ?? str(input.file_path);
229
+ block.native = name;
230
+ block.name = 'Edit';
231
+ block.input = path === undefined ? {} : { file_path: path };
232
+ }
233
+ content.push(block);
234
+ calls.set(id, { block, shell: false });
235
+ }
236
+ if (content.length > 0) {
237
+ sequence += 1;
238
+ const modelOf = str(data.model) ?? model;
239
+ const output = num(data.outputTokens);
240
+ const message = { role: 'assistant', content, ...(output === undefined ? {} : { usage: { output_tokens: output } }) };
241
+ // One model call can be written as several chunks (`chunkCount`, `chunkIndex`) that share `requestId` and `apiCallId`; the response is keyed on the
242
+ // first of those the line has, else its own message id, so the chunks of one call are one response and its totals land once.
243
+ const requestId = str(data.requestId) ?? str(data.apiCallId) ?? str(data.messageId) ?? `${String(base.sessionId)}-resp${String(sequence)}`;
244
+ lines.push({ ...base, type: 'assistant', uuid: `${fallbackId}-a${String(sequence)}`, requestId, timestamp: when, ...(modelOf === undefined ? {} : { requestedModel: modelOf }), message });
245
+ const held = { key: requestId, model: modelOf, message, own: output ?? 0, residual: 0 };
246
+ responses.push(held);
247
+ }
248
+ }
249
+ else if (type === 'tool.execution_start') {
250
+ const id = str(data.toolCallId);
251
+ const known = id === undefined ? undefined : calls.get(id);
252
+ if (known !== undefined && isObj(data.shellToolInfo)) {
253
+ const args = isObj(data.arguments) ? data.arguments : {};
254
+ const command = str(args.command) ?? str(data.shellToolInfo.displayCommand);
255
+ asShell(known, command);
256
+ }
257
+ }
258
+ else if (type === 'tool.execution_complete') {
259
+ const id = str(data.toolCallId);
260
+ const known = id === undefined ? undefined : calls.get(id);
261
+ if (id === undefined || known === undefined || resolved.has(id))
262
+ continue;
263
+ resolved.add(id);
264
+ const output = isObj(data.result) ? (str(data.result.content) ?? str(data.result.detailedContent) ?? '') : '';
265
+ const failed = data.success === false;
266
+ const message = isObj(data.error) ? str(data.error.message) : undefined;
267
+ const edits = (Array.isArray(data.fileEdits) ? data.fileEdits : []).flatMap((edit) => (isObj(edit) && str(edit.path) !== undefined ? [String(edit.path)] : []));
268
+ if (edits.length > 0) {
269
+ // A call that changed files is an edit whatever the tool is called; the first path rides the call's own block and any other gets its own, in the same line.
270
+ known.block.native = known.block.native ?? known.block.name;
271
+ known.block.name = 'Edit';
272
+ known.block.input = { file_path: edits[0] };
273
+ }
274
+ // The completion can name a shell call the start did not (`shellToolInfo` is optional); its exit code then decides like any other.
275
+ const exit = exitOf(data);
276
+ if (exit !== undefined && !known.shell && edits.length === 0) {
277
+ const input = isObj(known.block.input) ? known.block.input : {};
278
+ asShell(known, str(input.command));
279
+ }
280
+ let failure;
281
+ if (exit !== undefined && exit !== 0)
282
+ failure = failureOf(exit, output === '' ? (message ?? '') : output);
283
+ else if (exit === undefined && failed)
284
+ failure = message ?? (output === '' ? 'tool failed' : output);
285
+ result(id, failure, when, known.shell ? output : '');
286
+ for (const [index, path] of edits.slice(1).entries()) {
287
+ const extra = `${id}:${String(index + 2)}`;
288
+ const line = lines.find((candidate) => isObj(candidate.message) && Array.isArray(candidate.message.content) && candidate.message.content.includes(known.block));
289
+ if (line !== undefined && isObj(line.message) && Array.isArray(line.message.content))
290
+ line.message.content.push({ type: 'tool_use', id: extra, name: 'Edit', native: known.block.native, input: { file_path: path } });
291
+ result(extra, failure, when);
292
+ }
293
+ }
294
+ else if (type === 'session.shutdown') {
295
+ // The session-wide totals include subagents; the main conversation's own are under `agentMetrics.main` when the runtime writes them.
296
+ const agents = isObj(data.agentMetrics) ? data.agentMetrics : {};
297
+ const main = isObj(agents.main) && isObj(agents.main.modelMetrics) ? agents.main.modelMetrics : undefined;
298
+ const metrics = main ?? (isObj(data.modelMetrics) ? data.modelMetrics : {});
299
+ for (const [name, value] of Object.entries(metrics)) {
300
+ if (isObj(value) && isObj(value.usage))
301
+ totals.set(name, value.usage);
302
+ }
303
+ }
304
+ }
305
+ // Each model's last totals go on the first line of that model's last response; output beyond what all of its responses reported is the residual.
306
+ const reasoning = new Map();
307
+ for (const [name, usage] of totals) {
308
+ const last = responses.findLast((candidate) => candidate.model === name);
309
+ const first = last === undefined ? undefined : responses.find((candidate) => candidate.key === last.key);
310
+ if (first === undefined)
311
+ continue;
312
+ first.message.usage = { ...usageOf(usage), output_tokens: 0 };
313
+ const thinking = num(usage.reasoningTokens);
314
+ if (thinking !== undefined)
315
+ reasoning.set(name, thinking);
316
+ first.residual = Math.max(0, (num(usage.outputTokens) ?? 0) - responses.filter((candidate) => candidate.model === name).reduce((sum, candidate) => sum + candidate.own, 0));
317
+ }
318
+ // Extract counts a response's usage once, from its first line, so that line carries the output of every chunk of the response.
319
+ for (const first of responses) {
320
+ if (responses.find((candidate) => candidate.key === first.key) !== first)
321
+ continue;
322
+ const output = responses.filter((candidate) => candidate.key === first.key).reduce((sum, candidate) => sum + candidate.own, 0) + first.residual;
323
+ if (output > 0 || isObj(first.message.usage))
324
+ first.message.usage = { ...(isObj(first.message.usage) ? first.message.usage : {}), output_tokens: output };
325
+ }
326
+ // The shutdown totals hold one reasoning figure per model, not one per response, and verify holds each line's thinking to its own output. So the
327
+ // figure is shared out over the model's responses by their output (the model cannot think more than it wrote, so the total fits), and no line
328
+ // reads thinking above output.
329
+ for (const [name, total] of reasoning) {
330
+ const firsts = responses.filter((candidate) => candidate.model === name && responses.find((other) => other.key === candidate.key) === candidate && isObj(candidate.message.usage));
331
+ const outputs = firsts.map((candidate) => (isObj(candidate.message.usage) ? (num(candidate.message.usage.output_tokens) ?? 0) : 0));
332
+ shareOut(total, outputs).forEach((thinking, index) => {
333
+ const usage = firsts[index]?.message.usage;
334
+ if (isObj(usage))
335
+ usage.output_tokens_details = { thinking_tokens: thinking };
336
+ });
337
+ }
338
+ return lines;
339
+ };
340
+ /**
341
+ * The session's files as one transcript text. A rotated head replays earlier state as `checkpoint: true` events, which the archives already hold in full,
342
+ * so a checkpoint line in a file after the first is dropped, and any other line whose event `id` was already seen is too: each event is read once.
343
+ * A line that is not JSON is kept, so it is still counted.
344
+ */
345
+ const joinFiles = (texts) => {
346
+ const seen = new Set();
347
+ const kept = [];
348
+ texts.forEach((text, index) => {
349
+ for (const raw of text.split('\n')) {
350
+ if (raw.trim() === '')
351
+ continue;
352
+ const entry = parse(raw);
353
+ if (isObj(entry)) {
354
+ if (index > 0 && entry.checkpoint === true)
355
+ continue;
356
+ const id = str(entry.id);
357
+ if (id !== undefined) {
358
+ if (seen.has(id))
359
+ continue;
360
+ seen.add(id);
361
+ }
362
+ }
363
+ kept.push(raw);
364
+ }
365
+ });
366
+ return kept.join('\n') + (kept.length === 0 ? '' : '\n');
367
+ };
368
+ export const openCopilotStore = async (root) => {
369
+ const base = join(root, 'session-state');
370
+ const unreadable = [];
371
+ let entries;
372
+ try {
373
+ entries = await readdir(base, { withFileTypes: true });
374
+ }
375
+ catch (error) {
376
+ if (missing(error))
377
+ return undefined;
378
+ return { sessions: [], transcript: () => Promise.reject(new Error('no sessions')), rejected: 0, unreadable: [{ file: base, reason: reasonOf(error, base) }] };
379
+ }
380
+ const sessions = [];
381
+ let rejected = 0;
382
+ for (const entry of entries.filter((candidate) => candidate.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
383
+ if (!SAFE_ID.test(entry.name)) {
384
+ rejected += 1;
385
+ continue;
386
+ }
387
+ const file = join(base, entry.name, EVENTS);
388
+ try {
389
+ const info = await stat(file);
390
+ const archives = (await readdir(join(base, entry.name)))
391
+ .flatMap((name) => {
392
+ const n = ARCHIVE.exec(name)?.[1];
393
+ return n === undefined ? [] : [{ n: Number(n), path: join(base, entry.name, name) }];
394
+ })
395
+ .sort((a, b) => a.n - b.n)
396
+ .map((archive) => archive.path);
397
+ let size = info.size;
398
+ let mtimeMs = info.mtimeMs;
399
+ for (const archive of archives) {
400
+ const archived = await stat(archive);
401
+ size += archived.size;
402
+ mtimeMs = Math.max(mtimeMs, archived.mtimeMs);
403
+ }
404
+ // The oldest file holds the real `session.start`; the live head of a rotated log only replays it.
405
+ let folder = null;
406
+ for (const candidate of [...archives, file]) {
407
+ folder = await startFolder(candidate);
408
+ if (folder !== null)
409
+ break;
410
+ }
411
+ sessions.push({ id: entry.name, folder, file, archives, size, mtimeMs: Math.floor(mtimeMs) });
412
+ }
413
+ catch (error) {
414
+ // A folder with no events file (only a workspace.yaml or a plan) is no session; any other failure is reported.
415
+ if (!missing(error))
416
+ unreadable.push({ file, reason: reasonOf(error, file) });
417
+ }
418
+ }
419
+ sessions.sort((a, b) => a.mtimeMs - b.mtimeMs || a.id.localeCompare(b.id));
420
+ return {
421
+ sessions,
422
+ rejected,
423
+ unreadable,
424
+ transcript: async (session) => {
425
+ const texts = await Promise.all([...session.archives, session.file].map((path) => readFile(path, 'utf8')));
426
+ // A session with no archives is read as it is; only a rotated one has replayed events to drop.
427
+ const text = session.archives.length === 0 ? (texts[0] ?? '') : joinFiles(texts);
428
+ const lines = [...eventLines(text, session.id).map((line) => JSON.stringify(line)), ...eventBadLines(text)];
429
+ return lines.join('\n') + (lines.length === 0 ? '' : '\n');
430
+ },
431
+ };
432
+ };
@@ -0,0 +1,24 @@
1
+ /** The Cursor `User` folder for a platform; the caller passes the environment so a test never reads the real one. */
2
+ export declare const cursorRoot: (platform: NodeJS.Platform, env: {
3
+ [name: string]: string | undefined;
4
+ }, home?: string) => string;
5
+ /** A composer session found in the store, before its messages are read. */
6
+ export interface CursorSession {
7
+ composerId: string;
8
+ /** The workspace folder (a path) the session was opened in; null when no workspace lists it or the workspace is remote. */
9
+ folder: string | null;
10
+ createdAt: number;
11
+ lastUpdatedAt: number;
12
+ /** Messages the session holds: the cursor `state.json` keeps, with `lastUpdatedAt`, to tell a changed session from one already deposited. */
13
+ messages: number;
14
+ }
15
+ export interface CursorStore {
16
+ sessions: CursorSession[];
17
+ /** The session as transcript text for `captureSession`. */
18
+ transcript: (session: CursorSession) => string;
19
+ /** Composers skipped because their id is not a safe file name (it names a skeleton), counted so the skip is never silent. */
20
+ rejected: number;
21
+ close: () => void;
22
+ }
23
+ /** Opens the Cursor store under `root`; undefined when it holds no global store, so a machine without Cursor is skipped silently. */
24
+ export declare const openCursorStore: (root: string) => Promise<CursorStore | undefined>;