@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,421 @@
1
+ // The Grok Build source: a Grok Build session log becomes the same capture records a Claude Code transcript does.
2
+ //
3
+ // FIXTURE-DERIVED: written from the public source of the CLI (github.com/xai-org/grok-build @ 2bdd1d6a6369de0e8c68132ea4539e9abd9e14a8, Apache-2.0, Rust,
4
+ // under crates/codegen/) and the Agent Client Protocol schema it implements (github.com/agentclientprotocol/agent-client-protocol @ 0e486bcd, schema/v1/
5
+ // schema.json), not from a live store. Nothing was copied; the reader is original. No vendor text promises the on-disk format (the user guide, `17-sessions.md`,
6
+ // steers readers to `grok usage` instead of the files), so `docs/sessions-store.md` says what a live run would still have to prove.
7
+ //
8
+ // Where it reads from (`grokRoot`): `$GROK_HOME`, else `~/.grok` (`%USERPROFILE%\.grok` on Windows) (xai-dirs lib.rs:51-57; user guide 17-sessions.md:18). A
9
+ // session is `sessions/<percent-encoded cwd>/<session id>/` (17-sessions.md:22-40), holding
10
+ // summary.json {info: {id, cwd}, created_at, current_model_id, num_messages, session_kind?, forked_at?}: the authoritative cwd (the folder name can be a hash for a
11
+ // long path), the model the session ended on, and what kind of session it is
12
+ // updates.jsonl "the authoritative conversation log" (17-sessions.md:42): one `{timestamp, method, params: {sessionId, update}}` per line, `timestamp` epoch
13
+ // SECONDS (serde default 0), `update` an ACP SessionUpdate tagged by `sessionUpdate`:
14
+ // user_message_chunk content {type: "text", text}: the operator's prompt (a block that is an image counts as the turn and is written as an image block with no data, turn.rs:960-962, storage/mod.rs:416); chunks merge into one prompt until the agent speaks or `_meta.promptIndex`
15
+ // changes, including between marked and unmarked (turn.rs:890-895, storage/mod.rs:1009, :1038-1039). `_meta.hostTurn` (a slash command's turn), `_meta.hideFromScrollback` (a turn the harness started:
16
+ // a finished task, subagent, workflow, notification, or goal check, turn.rs:328, :896-901) and `_meta.interjection` (a mid-turn steer whose text is a
17
+ // model-facing frame) are not prompts (storage/mod.rs:966, :974); the first two end the turn, and `hostTurn` marks an agent chunk as well
18
+ // (acp_session.rs:1353-1372), whose text is dropped as upstream's own rebuilders do (storage/mod.rs:429-432)
19
+ // agent_message_chunk content {type: "text", text}: the response, chunks merged
20
+ // tool_call {toolCallId, title, kind, rawInput}: `title` IS the tool's name (tool_calls.rs:1487), except a backend-hosted search, which is titled for display ("Web search:") and named by `rawInput.variant`
21
+ // when `rawInput.backend` is true (tool_dispatch.rs:94-105). A model-issued call always has `kind` other here (tool_calls.rs:1488); its real kind
22
+ // arrives only on a later tool_call_update (tool_calls.rs:2734). A shell call is therefore a name in SHELL_TOOLS, or the user's `!cmd`, the one path that
23
+ // writes `kind` execute on the call itself (`bash-mode-<uuid>` titled "Execute `cmd`", tool_dispatch.rs:213, :245-246)
24
+ // tool_call_update {toolCallId, status, content[], rawOutput}: the outcome. A shell call's `rawOutput` is `{type: "Bash", exit_code, output: bytes,
25
+ // output_for_prompt, signal, timed_out, current_dir}` (types/output.rs:384-405); the reader takes the `output` bytes, because on the model-issued
26
+ // path `output_for_prompt` is the formatted prompt (`exit: N` header or the concise tool's `Exit code: N` fence, bash/mod.rs:2246). The agent path marks a shell call `failed` only on a timeout
27
+ // or a signal, so exit 2 arrives `completed` (acp_conversion.rs:241-250): the exit code decides, not the status. Any other tool's error text is its
28
+ // `content[]` text, except a failed MCP call, which has none and keeps its text in `rawOutput.output.Error` (acp_conversion.rs:325-338); `list_dir`, `grep_files`,
29
+ // `get_task_output`, `kill_task`, and a failed backend-hosted search carry neither (Known gaps). A `rawOutput` is otherwise a tagged wrapper that is never the error line. `current_dir` is the cwd the writer resolved BEFORE the command ran
30
+ // (grok_build/bash/mod.rs:1876, stored at :2240), so a `cd` inside a command never shows; it moves only when the session's own folder moved
31
+ // usage.json {turns: [{endedAt, inputTokens, outputTokens, cachedReadTokens, cacheCreationTokens, reasoningTokens, primaryModelId}]} (usage_file.rs:10-80): per-TURN
32
+ // deltas (`session` is their sum, :230-300), so every turn is added once. `inputTokens` is the FULL prompt, cache reads and writes included (xai-grok-sampling-types
33
+ // conversation.rs:771-773: `prompt_tokens` "is always the FULL prompt size"), and capture's fields are disjoint, so the uncached part is
34
+ // input - cache reads - cache writes. A turn's totals land on the last response before its `endedAt` (turn numbers are not used: a rewind renumbers).
35
+ // A fork copies its parent's lines with their old timestamps and its parent's usage.json (copy.rs:229, persistence.rs:2608); `summary.forked_at` is the cut, so
36
+ // older lines and turns are not this session's. A subagent (`session_kind` subagent or subagent_fork) is its own session in the same tree: it is not listed, and the
37
+ // parent's `task` call is the record. `events.jsonl` (telemetry), `chat_history.jsonl` (the model-facing copy), and the rest of the folder are not read.
38
+ import { readdir, readFile, stat } from 'node:fs/promises';
39
+ import { homedir } from 'node:os';
40
+ import { basename, join, posix, win32 } from 'node:path';
41
+ /** The Grok home for an environment; a test passes its own so it never reads the real one. */
42
+ export const grokRoot = (env, home = homedir()) => {
43
+ const set = env.GROK_HOME;
44
+ return set === undefined || set === '' ? join(home, '.grok') : 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 UPDATES = 'updates.jsonl';
59
+ const SUMMARY = 'summary.json';
60
+ const USAGE = 'usage.json';
61
+ /** A subagent is its own session in the same tree (`session_kind`, persistence.rs:3016, subagent/mod.rs:1136). */
62
+ const SUBAGENT_KINDS = new Set(['subagent', 'subagent_fork']);
63
+ /** The tools that run a command; a call of `kind` execute is one whatever its name. */
64
+ const SHELL_TOOLS = new Set(['run_terminal_cmd', 'run_terminal_command', 'bash']);
65
+ /** The tools that change files, named so a failed edit is still an edit. Their path keys are `file_path` (`filePath` for `edit`) and, for `apply_patch`, the `*** <Add|Update|Delete> File:` headers of `patch`. */
66
+ const EDIT_TOOLS = new Set(['search_replace', 'apply_patch', 'edit', 'write', 'hashline_edit']);
67
+ /** The backend-hosted searches' `rawInput.variant`, by the tool name each stands for (acp_session_impl/tool_dispatch.rs:94-105). */
68
+ const BACKEND_NAMES = new Map([['WebSearch', 'web_search'], ['XSearch', 'x_search']]);
69
+ const PATCH_HEADER = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
70
+ /** An error's message with `file` cut to its base name, so a reason that is logged names no folder. */
71
+ const reasonOf = (error, file) => (error instanceof Error ? error.message : String(error)).split(file).join(basename(file));
72
+ const missing = (error) => error instanceof Error && 'code' in error && error.code === 'ENOENT';
73
+ /** A JSON object file; undefined when it does not exist, a throw when it is not an object. */
74
+ const readObject = async (file) => {
75
+ let text;
76
+ try {
77
+ text = await readFile(file, 'utf8');
78
+ }
79
+ catch (error) {
80
+ if (missing(error))
81
+ return undefined;
82
+ throw error;
83
+ }
84
+ const value = parse(text);
85
+ if (!isObj(value))
86
+ throw new Error(`${basename(file)} is not a JSON object`);
87
+ return value;
88
+ };
89
+ /** The non-blank lines that are not JSON (a torn write), kept so `captureSession` counts them and the deposit line reports them. */
90
+ export const grokBadLines = (text) => text.split('\n').filter((raw) => raw.trim() !== '' && parse(raw) === undefined);
91
+ /** A time as ISO text, or undefined when it is not an absolute time after 2000. */
92
+ const isoOf = (ms) => (ms >= Date.parse('2000-01-01T00:00:00Z') ? new Date(ms).toISOString() : undefined);
93
+ const timeOf = (value) => {
94
+ const text = str(value);
95
+ const ms = text === undefined ? NaN : Date.parse(text);
96
+ return Number.isNaN(ms) || isoOf(ms) === undefined ? undefined : ms;
97
+ };
98
+ /** A failed shell call's error line: the output, but a nonzero exit says so first; a negative one is no exit status, so the output stands. */
99
+ const failureOf = (exit, text) => (exit < 0 && text.trim() !== '' ? text : `Exit code ${String(exit)}\n${text}`);
100
+ /** The text parts of a tool update's `content[]` (ACP ToolCallContent `content`/`text`), the error text of a tool that failed. */
101
+ const contentText = (update) => {
102
+ const parts = (Array.isArray(update.content) ? update.content : []).flatMap((part) => (isObj(part) && part.type === 'content' && isObj(part.content) && str(part.content.text) !== undefined ? [String(part.content.text)] : []));
103
+ return parts.length === 0 ? undefined : parts.join('\n');
104
+ };
105
+ /** An ANSI CSI sequence (ESC [ parameters, intermediates, final byte); the output bytes are the raw terminal output. */
106
+ const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, 'g');
107
+ /**
108
+ * A shell call's output: the `output` bytes (always present, types/output.rs:385) with ANSI sequences stripped, else the update's text. `output_for_prompt` is not read:
109
+ * on the model-issued path it is the formatted prompt, `exit: N [annotations]\n<output>` (grok_build/bash/mod.rs:2246, :386-417) or the concise tool's `Exit code: N` and a
110
+ * fenced block (grok_build_concise/bash.rs:37-58, :187), a header the error line must not become; only the user's `!cmd` keeps it plain (tool_dispatch.rs:304).
111
+ */
112
+ const shellText = (raw, update) => {
113
+ const bytes = Array.isArray(raw.output) ? raw.output.filter((byte) => typeof byte === 'number') : [];
114
+ return str(Buffer.from(bytes).toString('utf8').replace(ANSI, '')) ?? contentText(update) ?? '';
115
+ };
116
+ /** The files a request names: `file_path`, or `filePath` for the opencode `edit` tool (camelCase, opencode/edit/mod.rs:64, :138), or the headers of an `apply_patch` patch. */
117
+ const requestedPaths = (input) => {
118
+ const path = str(input.file_path) ?? str(input.filePath);
119
+ if (path !== undefined)
120
+ return [path];
121
+ const patch = str(input.patch);
122
+ return patch === undefined ? [] : [...patch.matchAll(PATCH_HEADER)].map((match) => String(match[1]).trim());
123
+ };
124
+ /**
125
+ * A path the model named, made absolute against the session's folder. The completion's `diff` parts carry the absolute path (`absolute_path`, acp_conversion.rs:202-204),
126
+ * and the model's own `file_path` may be relative, so only the same form tells that two lines name one file. The folder's own style (POSIX or Windows) decides the join.
127
+ */
128
+ const absolute = (path, cwd) => {
129
+ if (cwd === undefined || posix.isAbsolute(path) || win32.isAbsolute(path))
130
+ return path;
131
+ return (cwd.startsWith('/') ? posix : win32).join(cwd, path);
132
+ };
133
+ /** The files a completion reports having changed: the `diff` parts of `content[]` (ACP ToolCallContent `diff`, `path`). */
134
+ const diffPaths = (update) => (Array.isArray(update.content) ? update.content : []).flatMap((part) => (isObj(part) && part.type === 'diff' && str(part.path) !== undefined ? [String(part.path)] : []));
135
+ /** One session as the lines `captureSession` reads. */
136
+ export const grokLines = (text, fallbackId, summary, usage) => {
137
+ const nonblank = text.split('\n').filter((raw) => raw.trim() !== '');
138
+ if (nonblank.length > 0 && !nonblank.some((raw) => isObj(parse(raw))))
139
+ throw new Error('no parseable line');
140
+ const info = isObj(summary.info) ? summary.info : {};
141
+ const model = str(summary.current_model_id);
142
+ const forkedAt = timeOf(summary.forked_at);
143
+ const lines = [];
144
+ const base = { sessionId: str(info.id) ?? fallbackId, ...(str(info.cwd) === undefined ? {} : { cwd: str(info.cwd) }) };
145
+ let at = timeOf(summary.created_at) ?? 0;
146
+ let sequence = 0;
147
+ // The prompt being read; `index` is its `_meta.promptIndex` when the chunks carry one.
148
+ let pending;
149
+ let open;
150
+ const responses = [];
151
+ const calls = new Map();
152
+ const resolved = new Set();
153
+ const stamp = (seconds) => {
154
+ const value = num(seconds);
155
+ // A missing, zero, or earlier time (the vendor's serde default is 0) says nothing about when: the latest time so far stands.
156
+ if (value !== undefined && value * 1000 > at)
157
+ at = value * 1000;
158
+ return at;
159
+ };
160
+ const iso = (ms) => new Date(ms).toISOString();
161
+ const flushPrompt = () => {
162
+ if (pending === undefined)
163
+ return;
164
+ sequence += 1;
165
+ lines.push({ ...base, type: 'user', uuid: `${fallbackId}-u${String(sequence)}`, timestamp: iso(pending.when), message: { role: 'user', content: [pending.text === '' ? { type: 'image' } : { type: 'text', text: pending.text }] } });
166
+ pending = undefined;
167
+ };
168
+ const response = (when) => {
169
+ if (open !== undefined)
170
+ return open;
171
+ sequence += 1;
172
+ const content = [];
173
+ const message = { role: 'assistant', content };
174
+ // Every assistant line carries a requestId (`verify --strict` needs one), and each response has its own, so its usage counts once.
175
+ const line = { ...base, type: 'assistant', uuid: `${fallbackId}-a${String(sequence)}`, requestId: `${fallbackId}-resp${String(sequence)}`, timestamp: iso(when), ...(model === undefined ? {} : { requestedModel: model }), message };
176
+ lines.push(line);
177
+ open = { line, message, content, at: when };
178
+ responses.push(open);
179
+ return open;
180
+ };
181
+ const result = (id, failure, when, output = '') => {
182
+ sequence += 1;
183
+ lines.push({
184
+ ...base,
185
+ type: 'user',
186
+ uuid: `${fallbackId}-r${String(sequence)}`,
187
+ timestamp: iso(when),
188
+ // The completion is stamped with its own time, but the call's start is not paired to it here, so no duration is claimed.
189
+ noDuration: true,
190
+ // 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.
191
+ message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, is_error: failure !== undefined, content: failure ?? (output === '' ? '' : `Exit code 0\n${output}`) }] },
192
+ });
193
+ };
194
+ for (const raw of nonblank) {
195
+ const entry = parse(raw);
196
+ if (!isObj(entry) || !isObj(entry.params) || !isObj(entry.params.update))
197
+ continue;
198
+ const update = entry.params.update;
199
+ const kind = str(update.sessionUpdate);
200
+ const seconds = num(entry.timestamp);
201
+ // A fork's file starts with its parent's lines, each with the parent's time; the cut is the second the fork was made.
202
+ if (kind === undefined || (forkedAt !== undefined && seconds !== undefined && seconds > 0 && seconds < Math.floor(forkedAt / 1000)))
203
+ continue;
204
+ const when = stamp(seconds);
205
+ const meta = isObj(update._meta) ? update._meta : {};
206
+ const part = isObj(update.content) ? str(update.content.text) : undefined;
207
+ if (kind === 'user_message_chunk') {
208
+ if (meta.hostTurn === true || meta.hideFromScrollback === true) {
209
+ // The harness started this turn (a slash command, or a finished task, subagent, workflow, notification, or goal check): it is not the operator's prompt,
210
+ // and it ends the turn, so whatever the agent says next is a new response, not more of the last one.
211
+ flushPrompt();
212
+ open = undefined;
213
+ continue;
214
+ }
215
+ // A prompt block that is an image has no text but is still the operator's turn (storage/mod.rs:416 rebuilds it; summary.rs:51 names an "image-only message"):
216
+ // it takes part in the prompt boundary below, and is written as an image block with no data. Any other block type is skipped.
217
+ const image = isObj(update.content) && update.content.type === 'image';
218
+ if (meta.interjection === true || (part === undefined && !image))
219
+ continue;
220
+ // A change of `promptIndex` is a new prompt even when no reply fell between them (a prompt cancelled before its first reply, then a retry), and so is a change
221
+ // between marked and unmarked (the user's `!cmd` carries only a model id): upstream opens a new run on either (storage/mod.rs:1038-1039). Two unmarked chunks merge.
222
+ const index = num(meta.promptIndex);
223
+ if (pending !== undefined && index !== pending.index)
224
+ flushPrompt();
225
+ open = undefined;
226
+ pending = { text: (pending?.text ?? '') + (part ?? ''), when: pending?.when ?? when, index };
227
+ }
228
+ else if (kind === 'agent_message_chunk') {
229
+ flushPrompt();
230
+ if (meta.hostTurn === true) {
231
+ // A slash command's output is persisted as an agent chunk with the marker; upstream's own rebuilders drop it and treat it as a boundary.
232
+ open = undefined;
233
+ continue;
234
+ }
235
+ if (part === undefined)
236
+ continue;
237
+ const current = response(when);
238
+ const last = current.content[current.content.length - 1];
239
+ if (last?.type === 'text')
240
+ last.text = String(last.text) + part;
241
+ else
242
+ current.content.push({ type: 'text', text: part });
243
+ }
244
+ else if (kind === 'tool_call') {
245
+ flushPrompt();
246
+ const id = str(update.toolCallId);
247
+ const shownTitle = str(update.title);
248
+ if (id === undefined || shownTitle === undefined)
249
+ continue;
250
+ const current = response(when);
251
+ const input = isObj(update.rawInput) ? update.rawInput : {};
252
+ // A backend-hosted search is titled for display ("Web search:", "X search:"), with its kind in `rawInput.variant` (acp_session_impl/tool_dispatch.rs:94-105; sampling_events.rs:541-553).
253
+ const title = input.backend === true ? (BACKEND_NAMES.get(str(input.variant) ?? '') ?? shownTitle) : shownTitle;
254
+ // An MCP tool is reached two ways: titled `<server>__<tool>` itself, or through the `use_tool` wrapper, whose `rawInput.tool_name` is the qualified name
255
+ // (use_tool/input.rs:19-20, use_tool/mod.rs:308) and `tool_input` its arguments.
256
+ const target = title === 'use_tool' ? str(input.tool_name) : undefined;
257
+ const wrapped = target !== undefined && target.includes('__');
258
+ const block = {
259
+ type: 'tool_use',
260
+ id,
261
+ name: wrapped ? `mcp__${target}` : title.includes('__') ? `mcp__${title}` : title,
262
+ input: wrapped ? (isObj(input.tool_input) ? input.tool_input : {}) : input,
263
+ };
264
+ const shell = SHELL_TOOLS.has(title) || update.kind === 'execute';
265
+ const paths = EDIT_TOOLS.has(title) ? requestedPaths(input).map((path) => absolute(path, str(base.cwd))) : [];
266
+ if (shell) {
267
+ const command = str(input.command);
268
+ // The user's `!cmd` is titled "Execute `cmd`", which has no table row: it is the `bash` row's call.
269
+ block.native = SHELL_TOOLS.has(title) ? title : 'bash';
270
+ block.name = 'Bash';
271
+ block.input = command === undefined ? {} : { command };
272
+ }
273
+ else if (title === 'skill') {
274
+ // The opencode `skill` tool's input is `{name}` (opencode/skill/mod.rs:50); the table's skill.run reads `input.skill`.
275
+ const skill = str(input.name);
276
+ block.input = skill === undefined ? {} : { skill };
277
+ }
278
+ else if (EDIT_TOOLS.has(title)) {
279
+ const path = paths[0];
280
+ block.native = title;
281
+ block.name = 'Edit';
282
+ block.input = path === undefined ? {} : { file_path: path };
283
+ }
284
+ current.content.push(block);
285
+ calls.set(id, { block, content: current.content, shell, workdir: str(input.workdir), paths });
286
+ }
287
+ else if (kind === 'tool_call_update') {
288
+ const id = str(update.toolCallId);
289
+ const status = str(update.status);
290
+ const known = id === undefined ? undefined : calls.get(id);
291
+ // Only an outcome settles a call; a progress update leaves it `none`, and the call settles once.
292
+ if (id === undefined || known === undefined || resolved.has(id) || (status !== 'completed' && status !== 'failed'))
293
+ continue;
294
+ resolved.add(id);
295
+ open = undefined;
296
+ const output = isObj(update.rawOutput) ? update.rawOutput : {};
297
+ let failure;
298
+ let shown = '';
299
+ if (known.shell) {
300
+ const exit = num(output.exit_code);
301
+ const dir = str(output.current_dir);
302
+ // With a `workdir` the opencode `bash` tool reports that directory as `current_dir` (opencode/bash/mod.rs:376, :454), and the next relative `write` or `edit` still resolves against
303
+ // the session's own (opencode/write/mod.rs:117, opencode/edit/mod.rs:210), so only a call without one follows `current_dir`.
304
+ if (dir !== undefined && known.workdir === undefined)
305
+ base.cwd = dir;
306
+ shown = shellText(output, update);
307
+ if (exit !== undefined && exit !== 0)
308
+ failure = failureOf(exit, shown);
309
+ else if (status === 'failed')
310
+ failure = shown === '' ? 'tool failed' : shown;
311
+ }
312
+ else if (status === 'failed') {
313
+ // A failed MCP call has no `content[]`: its text rides `rawOutput.output.Error` (acp_conversion.rs:325-338; types/output.rs:1112-1158).
314
+ const mcp = output.type === 'MCP' && isObj(output.output) ? str(output.output.Error) : undefined;
315
+ failure = contentText(update) ?? mcp ?? 'tool failed';
316
+ }
317
+ result(id, failure, when, shown);
318
+ if (EDIT_TOOLS.has(str(known.block.native) ?? '')) {
319
+ // The first file rides the call's own block; any other gets its own, in the same response.
320
+ const paths = [...new Set([...known.paths, ...diffPaths(update)])];
321
+ if (paths[0] !== undefined)
322
+ known.block.input = { file_path: paths[0] };
323
+ for (const [index, path] of paths.slice(1).entries()) {
324
+ const extra = `${id}:${String(index + 2)}`;
325
+ known.content.push({ type: 'tool_use', id: extra, name: 'Edit', native: known.block.native, input: { file_path: path } });
326
+ result(extra, failure, when);
327
+ }
328
+ }
329
+ }
330
+ }
331
+ flushPrompt();
332
+ // Each turn's totals land on the last response before the turn ended, else the first. `turns[]` are per-turn deltas (usage_file.rs:230-300), so each is added
333
+ // once; a fork copied its parent's turns, which end at or before `forked_at`.
334
+ const turns = (Array.isArray(usage?.turns) ? usage.turns : []).flatMap((turn) => {
335
+ const ended = isObj(turn) ? timeOf(turn.endedAt) : undefined;
336
+ return isObj(turn) && ended !== undefined && (forkedAt === undefined || ended > forkedAt) ? [{ turn, ended }] : [];
337
+ });
338
+ const landed = new Map();
339
+ for (const { turn, ended } of turns.sort((a, b) => a.ended - b.ended)) {
340
+ const target = responses.findLast((candidate) => candidate.at <= ended) ?? responses[0];
341
+ if (target === undefined)
342
+ continue;
343
+ const held = landed.get(target) ?? { input: 0, read: 0, write: 0, output: 0, thinking: 0 };
344
+ const read = num(turn.cachedReadTokens) ?? 0;
345
+ const write = num(turn.cacheCreationTokens) ?? 0;
346
+ // `inputTokens` includes the cache reads and writes (conversation.rs:771-773), and capture's fields are disjoint, so only the uncached part is input.
347
+ held.input += Math.max((num(turn.inputTokens) ?? 0) - read - write, 0);
348
+ held.read += read;
349
+ held.write += write;
350
+ held.output += num(turn.outputTokens) ?? 0;
351
+ held.thinking += num(turn.reasoningTokens) ?? 0;
352
+ landed.set(target, held);
353
+ const turnModel = str(turn.primaryModelId);
354
+ if (turnModel !== undefined)
355
+ target.line.requestedModel = turnModel;
356
+ }
357
+ for (const [target, held] of landed) {
358
+ target.message.usage = { input_tokens: held.input, cache_read_input_tokens: held.read, cache_creation_input_tokens: held.write, output_tokens: held.output, output_tokens_details: { thinking_tokens: held.thinking } };
359
+ }
360
+ return lines;
361
+ };
362
+ export const openGrokStore = async (root) => {
363
+ const base = join(root, 'sessions');
364
+ const unreadable = [];
365
+ let folders;
366
+ try {
367
+ folders = await readdir(base, { withFileTypes: true });
368
+ }
369
+ catch (error) {
370
+ if (missing(error))
371
+ return undefined;
372
+ return { sessions: [], transcript: () => Promise.reject(new Error('no sessions')), rejected: 0, unreadable: [{ file: base, reason: reasonOf(error, base) }] };
373
+ }
374
+ const sessions = [];
375
+ let rejected = 0;
376
+ for (const folder of folders.filter((candidate) => candidate.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
377
+ let entries;
378
+ try {
379
+ entries = await readdir(join(base, folder.name), { withFileTypes: true });
380
+ }
381
+ catch (error) {
382
+ unreadable.push({ file: join(base, folder.name), reason: reasonOf(error, join(base, folder.name)) });
383
+ continue;
384
+ }
385
+ for (const entry of entries.filter((candidate) => candidate.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
386
+ if (!SAFE_ID.test(entry.name)) {
387
+ rejected += 1;
388
+ continue;
389
+ }
390
+ const dir = join(base, folder.name, entry.name);
391
+ try {
392
+ const summary = await readObject(join(dir, SUMMARY));
393
+ const updates = await stat(join(dir, UPDATES));
394
+ // A folder with no summary or no log (a started, never-used session) is no session; a subagent is its parent's `task` call; an unused one holds nothing.
395
+ if (summary === undefined || (str(summary.session_kind) !== undefined && SUBAGENT_KINDS.has(String(summary.session_kind))) || summary.num_messages === 0)
396
+ continue;
397
+ const used = await stat(join(dir, USAGE)).catch(() => undefined);
398
+ const info = isObj(summary.info) ? summary.info : {};
399
+ sessions.push({ id: entry.name, folder: str(info.cwd) ?? null, dir, size: updates.size + (used?.size ?? 0), mtimeMs: Math.floor(Math.max(updates.mtimeMs, used?.mtimeMs ?? 0)) });
400
+ }
401
+ catch (error) {
402
+ if (!missing(error))
403
+ unreadable.push({ file: dir, reason: reasonOf(error, dir) });
404
+ }
405
+ }
406
+ }
407
+ sessions.sort((a, b) => a.mtimeMs - b.mtimeMs || a.id.localeCompare(b.id));
408
+ return {
409
+ sessions,
410
+ rejected,
411
+ unreadable,
412
+ transcript: async (session) => {
413
+ const text = await readFile(join(session.dir, UPDATES), 'utf8');
414
+ const summary = await readObject(join(session.dir, SUMMARY));
415
+ if (summary === undefined)
416
+ throw new Error(`${SUMMARY} is gone`);
417
+ const lines = [...grokLines(text, session.id, summary, await readObject(join(session.dir, USAGE))).map((line) => JSON.stringify(line)), ...grokBadLines(text)];
418
+ return lines.join('\n') + (lines.length === 0 ? '' : '\n');
419
+ },
420
+ };
421
+ };
@@ -0,0 +1,3 @@
1
+ import type { HookCall } from './schema.js';
2
+ export declare const appendHookCall: (storeDir: string, call: HookCall) => Promise<void>;
3
+ export declare const readHookCalls: (storeDir: string) => Promise<HookCall[]>;
@@ -0,0 +1,47 @@
1
+ // The hook counters: `hook-calls.jsonl`, one line per hook invocation, appended by the hook and read by `measure`.
2
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
3
+ import { paths } from './store.js';
4
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
5
+ const eventOf = (value) => {
6
+ if (value === 'session-start' || value === 'pre-tool' || value === 'post-tool' || value === 'stop' || value === 'nightly')
7
+ return value;
8
+ return undefined;
9
+ };
10
+ export const appendHookCall = async (storeDir, call) => {
11
+ await mkdir(storeDir, { recursive: true });
12
+ await appendFile(paths(storeDir).hookCalls, `${JSON.stringify(call)}\n`, { mode: 0o600 });
13
+ };
14
+ export const readHookCalls = async (storeDir) => {
15
+ let text = '';
16
+ try {
17
+ text = await readFile(paths(storeDir).hookCalls, 'utf8');
18
+ }
19
+ catch (error) {
20
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
21
+ throw error;
22
+ }
23
+ const calls = [];
24
+ for (const line of text.split('\n')) {
25
+ if (line === '')
26
+ continue;
27
+ try {
28
+ const parsed = JSON.parse(line);
29
+ const event = isRecord(parsed) ? eventOf(parsed.event) : undefined;
30
+ if (isRecord(parsed) && event !== undefined && typeof parsed.at === 'string' && typeof parsed.hit === 'boolean' && typeof parsed.ms === 'number') {
31
+ calls.push({
32
+ at: parsed.at,
33
+ event,
34
+ session: typeof parsed.session === 'string' ? parsed.session : null,
35
+ hit: parsed.hit,
36
+ ms: parsed.ms,
37
+ ...(typeof parsed.startMs === 'number' ? { startMs: parsed.startMs } : {}),
38
+ error: typeof parsed.error === 'string' ? parsed.error : null,
39
+ });
40
+ }
41
+ }
42
+ catch {
43
+ // A torn last line is skipped.
44
+ }
45
+ }
46
+ return calls;
47
+ };
@@ -0,0 +1,53 @@
1
+ import type { Read } from './read.js';
2
+ import { type RecallRead } from './recall-reads.js';
3
+ import { type Store } from './store.js';
4
+ /** A signature is "recurring" once it has been seen in this many sessions. */
5
+ export declare const RECURRING_SESSIONS = 2;
6
+ /** SessionStart ranks by sessions within this many latest sessions of its repository: the `measure --last 300 --repo` window the G1 reading uses. */
7
+ export declare const RECENT_SESSIONS = 300;
8
+ export interface IndexedSignature {
9
+ sig: string;
10
+ sessions: number;
11
+ /** Sessions with the signature among the latest `RECENT_SESSIONS` of the session's repository. */
12
+ recent: number;
13
+ /** Tools that most often followed, most common first. */
14
+ followed: Array<[string, number]>;
15
+ harness: string[];
16
+ }
17
+ export interface HookIndex {
18
+ at: string;
19
+ /** Every `--redact-domain` in force, so a hook masks an error the way the log was masked. */
20
+ redactDomains: string[];
21
+ /** Recurring signatures by project folder name, most recent sessions first. */
22
+ projects: {
23
+ [project: string]: IndexedSignature[];
24
+ };
25
+ /** Every signature seen in at least two sessions. */
26
+ signatures: {
27
+ [sig: string]: {
28
+ sessions: number;
29
+ followed: Array<[string, number]>;
30
+ };
31
+ };
32
+ /** The call shapes over the fit floors and `PUSH_MIN_LIFT`, by read ID. An index a version before this wrote has none, and a pre-tool hook then pushes nothing. */
33
+ fit?: {
34
+ [readId: string]: RecallRead;
35
+ };
36
+ }
37
+ export declare const buildIndex: (store: Store, redactDomains: readonly string[], at: string, reads?: readonly Read[]) => HookIndex;
38
+ export declare const indexPath: (storeDir: string) => string;
39
+ /** The index as written, or undefined when there is none (or it is torn): a hook then says nothing. */
40
+ export declare const readIndex: (storeDir: string) => Promise<HookIndex | undefined>;
41
+ export interface ContextOptions {
42
+ /** The session's working directory; its repository root selects the project folders. */
43
+ cwd: string;
44
+ harness?: string;
45
+ top: number;
46
+ maxChars: number;
47
+ }
48
+ /** `<root>/.claude/worktrees/<name>` belongs to `<root>`, so a worktree session sees its repository's history. */
49
+ export declare const repositoryRoot: (cwd: string) => string;
50
+ /** True when a project folder slug belongs to the repository `root` sits in (a worktree path counts as its repository). */
51
+ export declare const inRepository: (project: string, root: string) => boolean;
52
+ /** The top recurring signatures for the repository, as a short block for injection; undefined when there is nothing to say. */
53
+ export declare const contextBlock: (index: HookIndex, options: ContextOptions) => string | undefined;