@100xprompt/brain 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +235 -2
  3. package/package.json +48 -4
  4. package/src/adapters/aider/index.ts +8 -0
  5. package/src/adapters/aider/reader.ts +129 -0
  6. package/src/adapters/aider/records.ts +220 -0
  7. package/src/adapters/aider/writer.ts +269 -0
  8. package/src/adapters/amazon-q-cli/index.ts +8 -0
  9. package/src/adapters/amazon-q-cli/reader.ts +84 -0
  10. package/src/adapters/amazon-q-cli/records.ts +191 -0
  11. package/src/adapters/amazon-q-cli/writer.ts +370 -0
  12. package/src/adapters/amazon-q-ide/index.ts +9 -0
  13. package/src/adapters/amazon-q-ide/reader.ts +131 -0
  14. package/src/adapters/amazon-q-ide/records.ts +141 -0
  15. package/src/adapters/amazon-q-ide/writer.ts +325 -0
  16. package/src/adapters/antigravity/index.ts +9 -0
  17. package/src/adapters/antigravity/proto.ts +194 -0
  18. package/src/adapters/antigravity/reader.ts +424 -0
  19. package/src/adapters/antigravity/records.ts +236 -0
  20. package/src/adapters/antigravity/writer.ts +463 -0
  21. package/src/adapters/auggie/index.ts +36 -0
  22. package/src/adapters/claude-code/index.ts +8 -0
  23. package/src/adapters/claude-code/reader.ts +639 -0
  24. package/src/adapters/claude-code/records.ts +88 -0
  25. package/src/adapters/claude-code/writer.ts +329 -0
  26. package/src/adapters/cline/index.ts +12 -0
  27. package/src/adapters/cline/reader.ts +432 -0
  28. package/src/adapters/cline/records.ts +151 -0
  29. package/src/adapters/cline/vscdb.ts +56 -0
  30. package/src/adapters/cline/writer.ts +656 -0
  31. package/src/adapters/cline/xml.ts +177 -0
  32. package/src/adapters/cline4/index.ts +9 -0
  33. package/src/adapters/cline4/reader.ts +179 -0
  34. package/src/adapters/cline4/records.ts +92 -0
  35. package/src/adapters/cline4/writer.ts +361 -0
  36. package/src/adapters/codebuff/index.ts +8 -0
  37. package/src/adapters/codebuff/project.ts +207 -0
  38. package/src/adapters/codebuff/reader.ts +215 -0
  39. package/src/adapters/codebuff/records.ts +173 -0
  40. package/src/adapters/codebuff/writer.ts +383 -0
  41. package/src/adapters/codex/index.ts +8 -0
  42. package/src/adapters/codex/native.ts +239 -0
  43. package/src/adapters/codex/reader.ts +747 -0
  44. package/src/adapters/codex/records.ts +114 -0
  45. package/src/adapters/codex/writer.ts +479 -0
  46. package/src/adapters/continue/index.ts +9 -0
  47. package/src/adapters/continue/reader.ts +68 -0
  48. package/src/adapters/continue/records.ts +418 -0
  49. package/src/adapters/continue/writer.ts +162 -0
  50. package/src/adapters/copilot-cli/index.ts +9 -0
  51. package/src/adapters/copilot-cli/reader.ts +186 -0
  52. package/src/adapters/copilot-cli/records.ts +241 -0
  53. package/src/adapters/copilot-cli/writer.ts +353 -0
  54. package/src/adapters/crush/index.ts +8 -0
  55. package/src/adapters/crush/reader.ts +180 -0
  56. package/src/adapters/crush/records.ts +169 -0
  57. package/src/adapters/crush/writer.ts +333 -0
  58. package/src/adapters/cursor-cli/index.ts +405 -0
  59. package/src/adapters/cursor-ide/agentkv.ts +95 -0
  60. package/src/adapters/cursor-ide/index.ts +501 -0
  61. package/src/adapters/cursor-ide/transcripts.ts +122 -0
  62. package/src/adapters/cursor-ide/workspaces.ts +119 -0
  63. package/src/adapters/cursor-ide/writer.ts +170 -0
  64. package/src/adapters/devin/index.ts +8 -0
  65. package/src/adapters/devin/reader.ts +305 -0
  66. package/src/adapters/devin/records.ts +277 -0
  67. package/src/adapters/devin/writer.ts +523 -0
  68. package/src/adapters/factory-droid/index.ts +12 -0
  69. package/src/adapters/factory-droid/reader.ts +184 -0
  70. package/src/adapters/factory-droid/records.ts +79 -0
  71. package/src/adapters/factory-droid/writer.ts +350 -0
  72. package/src/adapters/gemini-cli/exchanges.ts +228 -0
  73. package/src/adapters/gemini-cli/index.ts +396 -0
  74. package/src/adapters/gemini-cli/records.ts +521 -0
  75. package/src/adapters/gemini-cli/writer.ts +354 -0
  76. package/src/adapters/goose/index.ts +8 -0
  77. package/src/adapters/goose/reader.ts +138 -0
  78. package/src/adapters/goose/records.ts +199 -0
  79. package/src/adapters/goose/writer.ts +340 -0
  80. package/src/adapters/grok-cli/image.ts +44 -0
  81. package/src/adapters/grok-cli/index.ts +8 -0
  82. package/src/adapters/grok-cli/reader.ts +302 -0
  83. package/src/adapters/grok-cli/records.ts +172 -0
  84. package/src/adapters/grok-cli/writer.ts +329 -0
  85. package/src/adapters/hermes/index.ts +8 -0
  86. package/src/adapters/hermes/reader.ts +249 -0
  87. package/src/adapters/hermes/records.ts +285 -0
  88. package/src/adapters/hermes/writer.ts +452 -0
  89. package/src/adapters/iflow-cli/index.ts +72 -0
  90. package/src/adapters/iflow-cli/records.ts +215 -0
  91. package/src/adapters/iflow-cli/writer.ts +204 -0
  92. package/src/adapters/index.ts +112 -0
  93. package/src/adapters/kilo-cli/index.ts +21 -0
  94. package/src/adapters/kilo-code/index.ts +11 -0
  95. package/src/adapters/kilo-code/writer.ts +92 -0
  96. package/src/adapters/kimi/index.ts +9 -0
  97. package/src/adapters/kimi/reader.ts +92 -0
  98. package/src/adapters/kimi/records.ts +221 -0
  99. package/src/adapters/kimi/writer.ts +367 -0
  100. package/src/adapters/kiro-cli/index.ts +8 -0
  101. package/src/adapters/kiro-cli/reader.ts +87 -0
  102. package/src/adapters/kiro-cli/records.ts +260 -0
  103. package/src/adapters/kiro-cli/writer.ts +305 -0
  104. package/src/adapters/kiro-ide/index.ts +8 -0
  105. package/src/adapters/kiro-ide/writer.ts +52 -0
  106. package/src/adapters/letta/index.ts +8 -0
  107. package/src/adapters/letta/reader.ts +177 -0
  108. package/src/adapters/letta/records.ts +119 -0
  109. package/src/adapters/letta/writer.ts +413 -0
  110. package/src/adapters/mastracode/index.ts +8 -0
  111. package/src/adapters/mastracode/reader.ts +323 -0
  112. package/src/adapters/mastracode/records.ts +336 -0
  113. package/src/adapters/mastracode/writer.ts +385 -0
  114. package/src/adapters/mistral-vibe/index.ts +9 -0
  115. package/src/adapters/mistral-vibe/reader.ts +62 -0
  116. package/src/adapters/mistral-vibe/records.ts +104 -0
  117. package/src/adapters/mistral-vibe/writer.ts +359 -0
  118. package/src/adapters/muse/index.ts +10 -0
  119. package/src/adapters/muse/reader.ts +332 -0
  120. package/src/adapters/muse/records.ts +104 -0
  121. package/src/adapters/muse/writer.ts +387 -0
  122. package/src/adapters/omp/index.ts +13 -0
  123. package/src/adapters/omp/reader.ts +314 -0
  124. package/src/adapters/omp/records.ts +153 -0
  125. package/src/adapters/omp/writer.ts +216 -0
  126. package/src/adapters/opencode/ids.ts +52 -0
  127. package/src/adapters/opencode/index.ts +31 -0
  128. package/src/adapters/opencode/note.ts +8 -0
  129. package/src/adapters/opencode/reader.ts +452 -0
  130. package/src/adapters/opencode/tools.ts +174 -0
  131. package/src/adapters/opencode/v2.ts +367 -0
  132. package/src/adapters/opencode/writer.ts +379 -0
  133. package/src/adapters/openhands/index.ts +8 -0
  134. package/src/adapters/openhands/reader.ts +222 -0
  135. package/src/adapters/openhands/records.ts +136 -0
  136. package/src/adapters/openhands/writer.ts +404 -0
  137. package/src/adapters/pearai/index.ts +10 -0
  138. package/src/adapters/pi/index.ts +28 -0
  139. package/src/adapters/pi/reader.ts +261 -0
  140. package/src/adapters/pi/records.ts +99 -0
  141. package/src/adapters/pi/writer.ts +362 -0
  142. package/src/adapters/qoder-cli/index.ts +8 -0
  143. package/src/adapters/qoder-cli/reader.ts +83 -0
  144. package/src/adapters/qoder-cli/records.ts +45 -0
  145. package/src/adapters/qoder-cli/writer.ts +72 -0
  146. package/src/adapters/qwen-code/index.ts +84 -0
  147. package/src/adapters/qwen-code/records.ts +194 -0
  148. package/src/adapters/qwen-code/writer.ts +224 -0
  149. package/src/adapters/tabnine-cli/index.ts +8 -0
  150. package/src/adapters/tabnine-cli/reader.ts +106 -0
  151. package/src/adapters/tabnine-cli/records.ts +33 -0
  152. package/src/adapters/tabnine-cli/writer.ts +156 -0
  153. package/src/adapters/types.ts +73 -0
  154. package/src/adapters/void/index.ts +9 -0
  155. package/src/adapters/void/reader.ts +48 -0
  156. package/src/adapters/void/records.ts +285 -0
  157. package/src/adapters/void/writer.ts +93 -0
  158. package/src/adapters/vscode-copilot/index.ts +9 -0
  159. package/src/adapters/vscode-copilot/reader.ts +212 -0
  160. package/src/adapters/vscode-copilot/records.ts +388 -0
  161. package/src/adapters/vscode-copilot/shared.ts +68 -0
  162. package/src/adapters/vscode-copilot/writer.ts +546 -0
  163. package/src/adapters/zed/index.ts +8 -0
  164. package/src/adapters/zed/reader.ts +198 -0
  165. package/src/adapters/zed/records.ts +87 -0
  166. package/src/adapters/zed/writer.ts +341 -0
  167. package/src/brief-writers.ts +71 -0
  168. package/src/brief.ts +238 -0
  169. package/src/cli.ts +351 -0
  170. package/src/dashboard/cli.ts +148 -0
  171. package/src/dashboard/conversions.ts +170 -0
  172. package/src/dashboard/report.ts +261 -0
  173. package/src/dashboard/send.ts +213 -0
  174. package/src/dashboard/state.ts +95 -0
  175. package/src/fsutil.ts +133 -0
  176. package/src/gateway/common.ts +232 -0
  177. package/src/gateway/connect.ts +847 -0
  178. package/src/gateway/connectors/aider.ts +356 -0
  179. package/src/gateway/connectors/amazon-q-cli.ts +6 -0
  180. package/src/gateway/connectors/amazon-q-ide.ts +6 -0
  181. package/src/gateway/connectors/antigravity.ts +6 -0
  182. package/src/gateway/connectors/auggie.ts +6 -0
  183. package/src/gateway/connectors/cline-sdk.ts +85 -0
  184. package/src/gateway/connectors/cline.ts +34 -0
  185. package/src/gateway/connectors/codebuff.ts +6 -0
  186. package/src/gateway/connectors/continue.ts +579 -0
  187. package/src/gateway/connectors/copilot-cli.ts +138 -0
  188. package/src/gateway/connectors/crush.ts +313 -0
  189. package/src/gateway/connectors/cursor-cli.ts +6 -0
  190. package/src/gateway/connectors/cursor.ts +6 -0
  191. package/src/gateway/connectors/devin.ts +6 -0
  192. package/src/gateway/connectors/factory-droid.ts +308 -0
  193. package/src/gateway/connectors/goose.ts +79 -0
  194. package/src/gateway/connectors/grok-cli.ts +82 -0
  195. package/src/gateway/connectors/hermes.ts +230 -0
  196. package/src/gateway/connectors/iflow-cli.ts +50 -0
  197. package/src/gateway/connectors/index.ts +86 -0
  198. package/src/gateway/connectors/kilo-cli.ts +434 -0
  199. package/src/gateway/connectors/kilo-code.ts +32 -0
  200. package/src/gateway/connectors/kimi-code.ts +241 -0
  201. package/src/gateway/connectors/kiro-cli.ts +6 -0
  202. package/src/gateway/connectors/kiro-ide.ts +6 -0
  203. package/src/gateway/connectors/letta-code.ts +170 -0
  204. package/src/gateway/connectors/mastracode.ts +91 -0
  205. package/src/gateway/connectors/mistral-vibe.ts +495 -0
  206. package/src/gateway/connectors/muse.ts +27 -0
  207. package/src/gateway/connectors/omp.ts +279 -0
  208. package/src/gateway/connectors/openhands.ts +296 -0
  209. package/src/gateway/connectors/pearai.ts +52 -0
  210. package/src/gateway/connectors/pi.ts +357 -0
  211. package/src/gateway/connectors/qoder-cli.ts +6 -0
  212. package/src/gateway/connectors/qwen-code.ts +411 -0
  213. package/src/gateway/connectors/roo.ts +43 -0
  214. package/src/gateway/connectors/tabnine-cli.ts +6 -0
  215. package/src/gateway/connectors/void.ts +6 -0
  216. package/src/gateway/connectors/vscode-copilot.ts +286 -0
  217. package/src/gateway/connectors/windsurf.ts +6 -0
  218. package/src/gateway/connectors/zed.ts +29 -0
  219. package/src/gateway/signin.ts +323 -0
  220. package/src/handoff.ts +104 -0
  221. package/src/hooks.ts +455 -0
  222. package/src/memory/audit.ts +514 -0
  223. package/src/memory/blobs.ts +94 -0
  224. package/src/memory/cli.ts +117 -0
  225. package/src/memory/distill.ts +381 -0
  226. package/src/memory/externalize.ts +435 -0
  227. package/src/memory/lookups.ts +70 -0
  228. package/src/memory/mcp-install.ts +401 -0
  229. package/src/memory/mcp-server.ts +296 -0
  230. package/src/memory/prepare.ts +179 -0
  231. package/src/memory/preview.ts +154 -0
  232. package/src/memory/project.ts +35 -0
  233. package/src/memory/redact.ts +72 -0
  234. package/src/memory/relevance.ts +187 -0
  235. package/src/memory/repomap.ts +516 -0
  236. package/src/memory/store.ts +270 -0
  237. package/src/memory/sync.ts +166 -0
  238. package/src/memory/tidy.ts +193 -0
  239. package/src/memory/types.ts +118 -0
  240. package/src/org/admin-ops.ts +96 -0
  241. package/src/org/auth.ts +112 -0
  242. package/src/org/backup.ts +115 -0
  243. package/src/org/cli.ts +438 -0
  244. package/src/org/client.ts +404 -0
  245. package/src/org/config.ts +44 -0
  246. package/src/org/crypto.ts +537 -0
  247. package/src/org/db.ts +435 -0
  248. package/src/org/graph.ts +318 -0
  249. package/src/org/oidc.ts +435 -0
  250. package/src/org/policy.ts +398 -0
  251. package/src/org/server.ts +920 -0
  252. package/src/org/sso.ts +316 -0
  253. package/src/org/tls.ts +66 -0
  254. package/src/org/types.ts +193 -0
  255. package/src/self.ts +68 -0
  256. package/src/session/attachments.ts +53 -0
  257. package/src/session/peekcache.ts +50 -0
  258. package/src/session/types.ts +104 -0
  259. package/src/session/util.ts +130 -0
  260. package/src/sqlite.ts +48 -0
@@ -0,0 +1,285 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { randomBytes } from "node:crypto"
3
+ import { join } from "node:path"
4
+ import { exists, versionOf } from "../../fsutil.ts"
5
+ import type { ToolKind, Turn } from "../../session/types.ts"
6
+ import { asText, toolKindOf } from "../../session/util.ts"
7
+ import { asRecord, safeToolName } from "../opencode/tools.ts"
8
+
9
+ // Hermes Agent (NousResearch/hermes-agent): where its sessions live and what its rows look like, shared by the
10
+ // reader and the writer. The semantics are ported from session-migrate (xhluca/session-migrate, MIT licence,
11
+ // copyright (c) 2026 xhluca): docs/hermes-format.md and src/session_migrate/formats/hermes.py, pinned to Hermes
12
+ // 0.20.6 (schema 26), whose real capture is test/fixtures/native-corpus/hermes. Checked as well against the
13
+ // Hermes 0.18.2 source (hermes_state.py SessionDB, schema 19; agent/chat_completion_helpers.py
14
+ // build_assistant_message; agent/context_compressor.py; cli.py), which keeps the same tables with fewer columns.
15
+ //
16
+ // Every session of every surface (CLI, TUI, gateway, cron) is a row of $HERMES_HOME/state.db (HERMES_HOME
17
+ // defaults to ~/.hermes; like the other adapters, this one reads the default and does not follow the variable).
18
+ // A session's id is the local start time plus six hex digits (YYYYMMDD_HHMMSS_abcdef); its messages are rows in
19
+ // OpenAI chat shape: role user/assistant/tool, an assistant's calls as a JSON `tool_calls` list
20
+ // ({id, call_id, response_item_id, type: "function", function: {name, arguments}}), each result a `tool` row
21
+ // with tool_call_id and tool_name. Hermes replays only rows with active = 1, in id order: rewound turns are
22
+ // active = 0, and turns a compaction replaced are active = 0 with compacted = 1. A compaction summary is a
23
+ // message whose text starts with Hermes's summary prefix (flagged _compressed_summary = 1 since schema 2x).
24
+ // FTS5 tables (messages_fts, messages_fts_trigram) are kept in sync by triggers on messages.
25
+
26
+ export const TOOL = "hermes"
27
+ export const LABEL = "Hermes Agent"
28
+
29
+ /** Hermes versions whose store the writer was checked against: the capture's, and the one installed here. */
30
+ export const TESTED_VERSIONS = ["0.18.2", "0.20.6"]
31
+ /** The store schemas those versions keep (schema_version.version): 19 for 0.18.2, 26 for 0.20.6. */
32
+ export const MIN_SCHEMA = 19
33
+ export const MAX_SCHEMA = 26
34
+
35
+ export const hermesDir = (home: string) => join(home, ".hermes")
36
+ export const storePath = (home: string) => join(hermesDir(home), "state.db")
37
+
38
+ /** The hermes launcher: the installer's ~/.local/bin/hermes first, else the one on PATH (as PATH is now). */
39
+ export function hermesBin(home: string): { path: string; command: string } | undefined {
40
+ const onPath = Bun.which("hermes", { PATH: process.env.PATH ?? "" })
41
+ const own = join(home, ".local", "bin", "hermes")
42
+ if (exists(own)) return { path: own, command: onPath === own ? "hermes" : own }
43
+ return onPath ? { path: onPath, command: "hermes" } : undefined
44
+ }
45
+
46
+ /** "0.18.2" from `hermes --version` ("Hermes Agent v0.18.2 (2026.7.7.2) ..."). */
47
+ export function hermesVersion(bin: string): string | undefined {
48
+ const line = versionOf(bin)
49
+ return line?.match(/Hermes Agent v(\d+\.\d+\.\d+)/)?.[1] ?? line?.match(/\b(\d+\.\d+\.\d+)\b/)?.[1]
50
+ }
51
+
52
+ const pad = (n: number, w = 2) => String(n).padStart(w, "0")
53
+
54
+ /** A new session id as the CLI makes one: local start time, then six hex digits (cli.py). */
55
+ export function newSessionId(now: Date): string {
56
+ const hex = randomBytes(3).toString("hex")
57
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}_${hex}`
58
+ }
59
+
60
+ export const SESSION_ID = /^\d{8}_\d{6}_[0-9a-f]{6,8}$/
61
+
62
+ /** The columns a table has in this store: the reader and writer work with both schema 19 and 26. */
63
+ export function columnsOf(d: Database, table: string): Set<string> {
64
+ return new Set((d.query(`PRAGMA table_info("${table}")`).all() as Array<{ name: string }>).map((c) => c.name))
65
+ }
66
+
67
+ export function schemaVersion(d: Database): number | undefined {
68
+ try {
69
+ const v = (d.query("SELECT version FROM schema_version LIMIT 1").get() as { version: unknown } | null)?.version
70
+ return typeof v === "number" ? v : undefined
71
+ } catch {
72
+ return undefined
73
+ }
74
+ }
75
+
76
+ // --- Compaction summaries (agent/context_compressor.py) ---
77
+
78
+ /** The prefix Hermes still recognises from its first summaries; session-migrate writes summaries with it. */
79
+ export const LEGACY_SUMMARY_PREFIX = "[CONTEXT SUMMARY]:"
80
+ /** The current and historical prefixes all open with this, on one line, before the summary on the next. */
81
+ const SUMMARY_PREFIX_START = "[CONTEXT COMPACTION"
82
+ export const SUMMARY_END_MARKER = "--- END OF CONTEXT SUMMARY — respond to the message below, not the summary above ---"
83
+ const MERGED_HEADER = "[PRIOR CONTEXT — for reference only; not a new message]"
84
+ const MERGED_DELIMITER = "[END OF PRIOR CONTEXT — COMPACTION SUMMARY BELOW]"
85
+
86
+ /**
87
+ * A compaction summary in a message's text: the summary itself, and the message it was merged into when Hermes
88
+ * appended it to the first kept message instead of giving it its own (that message's own text comes first).
89
+ */
90
+ export function summaryOf(text: string, flagged: boolean): { summary: string; prior?: string } | undefined {
91
+ let body = text.trimStart()
92
+ let prior: string | undefined
93
+ const merged = body.indexOf(MERGED_DELIMITER)
94
+ if (merged >= 0) {
95
+ prior = body.slice(0, merged).replace(MERGED_HEADER, "").trim()
96
+ body = body.slice(merged + MERGED_DELIMITER.length).trimStart()
97
+ }
98
+ if (body.startsWith(LEGACY_SUMMARY_PREFIX)) body = body.slice(LEGACY_SUMMARY_PREFIX.length)
99
+ else if (body.startsWith(SUMMARY_PREFIX_START)) body = body.includes("\n") ? body.slice(body.indexOf("\n") + 1) : ""
100
+ else if (!flagged) return undefined
101
+ body = body.trim()
102
+ if (body.endsWith(SUMMARY_END_MARKER)) body = body.slice(0, -SUMMARY_END_MARKER.length).trimEnd()
103
+ return { summary: body, ...(prior ? { prior } : {}) }
104
+ }
105
+
106
+ /** A summary as Hermes keeps one: the legacy prefix both 0.18 and 0.20 recognise, and the end marker. */
107
+ export const summaryText = (summary: string) => `${LEGACY_SUMMARY_PREFIX}\n${summary.trim()}\n\n${SUMMARY_END_MARKER}`
108
+
109
+ // --- Message content ---
110
+
111
+ /** Structured content (text + image parts) is stored as this prefix and JSON (SessionDB._encode_content). */
112
+ const CONTENT_JSON_PREFIX = "\u0000json:"
113
+
114
+ export type Part = { type?: unknown; text?: unknown; image_url?: unknown; url?: unknown }
115
+
116
+ /** A message's content: its text, or its parts when it was stored structured. */
117
+ export function decodeContent(raw: unknown): string | Part[] {
118
+ if (typeof raw !== "string") return raw === null || raw === undefined ? "" : asText(raw)
119
+ let json: string | undefined
120
+ if (raw.startsWith(CONTENT_JSON_PREFIX)) json = raw.slice(CONTENT_JSON_PREFIX.length)
121
+ else if (raw.trimStart().startsWith("[{")) json = raw
122
+ if (json === undefined) return raw
123
+ try {
124
+ const v = JSON.parse(json)
125
+ // Only a list of typed parts is structured content; anything else is text that happens to look like JSON.
126
+ if (Array.isArray(v) && v.length && v.every((p) => p && typeof p === "object" && typeof p.type === "string")) return v as Part[]
127
+ } catch {}
128
+ return raw.startsWith(CONTENT_JSON_PREFIX) ? json : raw
129
+ }
130
+
131
+ export const partsText = (c: string | Part[]) =>
132
+ typeof c === "string"
133
+ ? c
134
+ : c
135
+ .map((p) => (typeof p.text === "string" ? p.text : ""))
136
+ .filter(Boolean)
137
+ .join("\n")
138
+
139
+ /** An image part's URL (image_url as {url} or as a string; input_image). */
140
+ export function imageUrlOf(p: Part): string | undefined {
141
+ if (p.type !== "image_url" && p.type !== "input_image") return undefined
142
+ const v = p.image_url
143
+ if (v && typeof v === "object" && typeof (v as { url?: unknown }).url === "string") return (v as { url: string }).url
144
+ if (typeof v === "string") return v
145
+ return typeof p.url === "string" ? p.url : undefined
146
+ }
147
+
148
+ // --- Tool results ---
149
+
150
+ /**
151
+ * A tool row's text and whether the call worked. Hermes's terminal (and several other tools) answer with a JSON
152
+ * object: {"output", "exit_code", "error"}; a truthy error or a non-zero exit code is a failure.
153
+ */
154
+ export function toolOutput(c: string | Part[]): { text: string; ok: boolean } {
155
+ const text = partsText(c)
156
+ let v: unknown
157
+ try {
158
+ v = JSON.parse(text)
159
+ } catch {
160
+ return { text, ok: true }
161
+ }
162
+ if (!v || typeof v !== "object" || Array.isArray(v)) return { text, ok: true }
163
+ const o = v as { output?: unknown; error?: unknown; exit_code?: unknown }
164
+ const failed = !!o.error || (typeof o.exit_code === "number" && o.exit_code !== 0)
165
+ if (typeof o.output === "string") return { text: o.output, ok: !failed }
166
+ if (failed && typeof o.error === "string" && Object.keys(o).every((k) => k === "error" || k === "exit_code" || k === "output")) return { text: o.error, ok: false }
167
+ return { text, ok: !failed }
168
+ }
169
+
170
+ /** Whether plain text would read back as one of the JSON results above (it then has to be wrapped). */
171
+ const looksLikeResult = (text: string) => {
172
+ try {
173
+ const v = JSON.parse(text)
174
+ return !!v && typeof v === "object" && !Array.isArray(v) && ("error" in v || "exit_code" in v || "output" in v)
175
+ } catch {
176
+ return false
177
+ }
178
+ }
179
+
180
+ /**
181
+ * A result as a tool row's content: the terminal's own JSON for terminal calls, plain text for other
182
+ * successful calls, and the JSON envelope session-migrate uses when plain text cannot say it (a failure, a NUL).
183
+ */
184
+ export function toolContent(text: string, ok: boolean, terminal: boolean): string {
185
+ if (terminal) return pyJson({ output: text, exit_code: ok ? 0 : 1, error: null })
186
+ if (ok && text && !text.includes("\u0000") && !looksLikeResult(text)) return text
187
+ return pyJson({ output: text, error: ok ? null : text || true, exit_code: ok ? 0 : 1 })
188
+ }
189
+
190
+ // --- Tool calls ---
191
+
192
+ /** What a Hermes tool does: its file tools are read_file, write_file, patch and search_files, its shell terminal. */
193
+ export function hermesToolKind(name: string): ToolKind {
194
+ if (name === "patch") return "edit"
195
+ if (name === "web_extract") return "web"
196
+ if (name === "execute_code") return "shell"
197
+ if (name === "delegate_task") return "task"
198
+ return toolKindOf(name)
199
+ }
200
+
201
+ type Call = Extract<Turn, { kind: "tool_call" }>
202
+ const str = (v: unknown) => (typeof v === "string" && v ? v : undefined)
203
+ const firstOf = (i: Record<string, unknown>, keys: string[]) => keys.map((k) => str(i[k])).find(Boolean)
204
+ const FILE_KEYS = ["path", "file_path", "filePath", "target_file", "absolute_path", "file"]
205
+
206
+ /** A shell command as one line: Codex passes ["bash", "-lc", "…"]. */
207
+ function commandOf(v: unknown): string | undefined {
208
+ if (typeof v === "string") return v || undefined
209
+ if (!Array.isArray(v) || !v.length || !v.every((x) => typeof x === "string")) return undefined
210
+ const [sh, flag, script] = v as string[]
211
+ if (v.length === 3 && /^(ba|z)?sh$/.test(sh!.split("/").pop()!) && /^-l?c$/.test(flag!)) return script
212
+ return v.join(" ")
213
+ }
214
+
215
+ /**
216
+ * A call as Hermes's own tools take it (tools/terminal_tool.py, tools/file_tools.py), when its arguments say what
217
+ * the tool needs; any other call keeps its name (made safe for providers) and its input.
218
+ */
219
+ export function hermesCall(c: Call): { name: string; args: Record<string, unknown>; mapped: boolean } {
220
+ const i = asRecord(c.input)
221
+ const native = ((): { name: string; args: Record<string, unknown> } | undefined => {
222
+ switch (c.toolKind) {
223
+ case "shell": {
224
+ const command = commandOf(i.command ?? i.cmd)
225
+ if (!command) return undefined
226
+ const workdir = firstOf(i, ["workdir", "cwd", "working_dir", "workingDirectory"])
227
+ return { name: "terminal", args: { command, ...(workdir ? { workdir } : {}) } }
228
+ }
229
+ case "read": {
230
+ const path = firstOf(i, FILE_KEYS)
231
+ if (!path) return undefined
232
+ const range = Object.fromEntries(["offset", "limit"].filter((k) => typeof i[k] === "number").map((k) => [k, i[k]]))
233
+ return { name: "read_file", args: { path, ...range } }
234
+ }
235
+ case "write": {
236
+ const path = firstOf(i, FILE_KEYS)
237
+ const content = [i.content, i.contents, i.file_text].find((v) => typeof v === "string")
238
+ return path && content !== undefined ? { name: "write_file", args: { path, content } } : undefined
239
+ }
240
+ case "edit": {
241
+ const path = firstOf(i, FILE_KEYS)
242
+ const before = [i.old_string, i.oldString, i.old_str].find((v) => typeof v === "string")
243
+ const after = [i.new_string, i.newString, i.new_str].find((v) => typeof v === "string")
244
+ if (!path || before === undefined || after === undefined) return undefined
245
+ const all = [i.replace_all, i.replaceAll].find((v) => typeof v === "boolean")
246
+ return { name: "patch", args: { mode: "replace", path, old_string: before, new_string: after, ...(all !== undefined ? { replace_all: all } : {}) } }
247
+ }
248
+ case "search": {
249
+ const pattern = firstOf(i, ["pattern", "query", "regex", "glob_pattern"])
250
+ if (!pattern) return undefined
251
+ const path = firstOf(i, ["path", "dir", "directory", "target_directory"])
252
+ const files = /glob|find_?files|file_search/i.test(c.name) || i.target === "files"
253
+ if (!files && !/grep|ripgrep|search_files|grep_search|^rg$/i.test(c.name)) return undefined
254
+ return { name: "search_files", args: { pattern, target: files ? "files" : "content", ...(path ? { path } : {}) } }
255
+ }
256
+ default:
257
+ return undefined
258
+ }
259
+ })()
260
+ if (native) return { ...native, mapped: native.name !== c.name || pyJson(native.args) !== pyJson(i) }
261
+ return { name: safeToolName(c.name), args: i, mapped: false }
262
+ }
263
+
264
+ /** The Responses item id Hermes derives from a call id (codex_responses_adapter._derive_responses_function_call_id). */
265
+ export function responseItemId(callId: string): string {
266
+ if (callId.startsWith("fc_")) return callId
267
+ if (callId.startsWith("call_") && callId.length > 5) return `fc_${callId.slice(5)}`
268
+ const clean = callId.replace(/[^A-Za-z0-9_-]/g, "")
269
+ return `fc_${clean.slice(0, 48) || "call"}`
270
+ }
271
+
272
+ /** JSON as Python's json.dumps writes it by default (", " and ": ", non-ASCII escaped), as Hermes stores tool_calls. */
273
+ export function pyJson(v: unknown): string {
274
+ if (v === null || v === undefined) return "null"
275
+ if (typeof v === "string") return JSON.stringify(v).replace(/[\u0080-￿]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`)
276
+ if (typeof v === "number") return Number.isFinite(v) ? String(v) : "null"
277
+ if (typeof v === "boolean") return v ? "true" : "false"
278
+ if (Array.isArray(v)) return `[${v.map(pyJson).join(", ")}]`
279
+ if (typeof v === "object")
280
+ return `{${Object.entries(v as Record<string, unknown>)
281
+ .filter(([, x]) => x !== undefined)
282
+ .map(([k, x]) => `${pyJson(k)}: ${pyJson(x)}`)
283
+ .join(", ")}}`
284
+ return pyJson(String(v))
285
+ }