@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,71 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { join } from "node:path"
3
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "./adapters/types.ts"
4
+ import { BRIEF_BUDGET, LEAN_BUDGET, buildBrief, toolLabel } from "./brief.ts"
5
+ import { gitInfo } from "./fsutil.ts"
6
+ import { handoffDir, markConsumed, saveHandoff } from "./handoff.ts"
7
+ import { hookInstalled } from "./hooks.ts"
8
+ import type { Session } from "./session/types.ts"
9
+
10
+ // Continuing a session in any tool by a brief: the universal fallback, and the only way into tools that
11
+ // keep sessions on their servers or in formats nothing else can write. The brief is saved as a pending
12
+ // handoff (for the target's session-start hook, when installed) and as a Markdown file to paste.
13
+ // Cursor gets its own route: its prompt deeplink opens the editor with the brief in the chat box,
14
+ // ready to send, without writing to Cursor's database.
15
+
16
+ const CURSOR_DEEPLINK_LIMIT = 10_000
17
+
18
+ export const BRIEF_TARGETS = ["claude-code", "codex", "cursor", "cursor-cli", "gemini-cli", "vscode-copilot", "copilot-cli", "cline", "opencode", "100xprompt", "antigravity", "any"]
19
+
20
+ /** `cursor://` deeplink that opens Cursor's chat with `text` filled in (Cursor asks before sending). */
21
+ export function cursorPromptLink(text: string): string {
22
+ return `cursor://anysphere.cursor-deeplink/prompt?text=${encodeURIComponent(text.slice(0, CURSOR_DEEPLINK_LIMIT))}`
23
+ }
24
+
25
+ export function briefWriter(target: string, named?: string): Writer {
26
+ const label = named ?? (target === "any" ? "any tool" : target === "cursor" ? "Cursor" : toolLabel(target))
27
+ return {
28
+ tool: target,
29
+ label,
30
+ way: "brief",
31
+ async ready(): Promise<Readiness> {
32
+ return { ok: true }
33
+ },
34
+ async write(session: Session, opts: WriteOptions): Promise<WriteResult> {
35
+ const lean = !!opts.brief?.lean
36
+ const budget = lean ? LEAN_BUDGET : BRIEF_BUDGET[target] ?? BRIEF_BUDGET.default!
37
+ const brief = buildBrief(session, { budget, lean, ...(opts.brief?.memory?.length ? { memory: opts.brief.memory } : {}), ...(opts.brief?.map ? { map: opts.brief.map } : {}), fullRecord: `brain session show ${session.header.id}` })
38
+ const id = `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`
39
+ const briefPath = join(handoffDir(opts.home), `${id}.md`)
40
+ const written = [briefPath, join(handoffDir(opts.home), `${id}.json`)]
41
+ if (!opts.dryRun) {
42
+ saveHandoff(opts.home, {
43
+ id,
44
+ fromTool: session.header.tool,
45
+ sessionId: session.header.id,
46
+ ...(session.header.title ? { title: session.header.title } : {}),
47
+ cwd: opts.cwd,
48
+ ...(gitInfo(opts.cwd).gitRemote ? { gitRemote: gitInfo(opts.cwd).gitRemote } : {}),
49
+ target: target === "cursor" ? "cursor-ide" : target,
50
+ brief,
51
+ })
52
+ }
53
+ let open: string
54
+ if (target === "cursor" && hookInstalled("cursor-ide", opts.home)) {
55
+ // Cursor's session-start hook delivers it; the deeplink as well would give the next chat the brief twice.
56
+ open = `open -a Cursor ${JSON.stringify(opts.cwd)}, then start a new chat: Cursor's session-start hook gives the model the brief.`
57
+ } else if (target === "cursor") {
58
+ open = `open -a Cursor ${JSON.stringify(opts.cwd)} && open ${JSON.stringify(cursorPromptLink(`Continue this work.\n\n${brief}`))}`
59
+ // Delivered by the deeplink: claimed now, so a hook installed later does not deliver it again.
60
+ if (!opts.dryRun) markConsumed(opts.home, id, "cursor-ide")
61
+ } else if (hookInstalled(target, opts.home)) {
62
+ open = `Start ${label} in ${opts.cwd}: its session-start hook gives the model the brief.`
63
+ } else {
64
+ open = `Start ${label} in ${opts.cwd} and paste the brief as your first message: ${briefPath}`
65
+ }
66
+ return { tool: target, way: "brief", written, open, lost: [lean ? "everything but the brief: the model gets the essentials and what the project remembers; the rest is stored and reachable on demand" : "everything but the brief: the model gets a summary and the latest turns, not the full history"], dryRun: !!opts.dryRun }
67
+ },
68
+ }
69
+ }
70
+
71
+ export const briefWriters: Writer[] = BRIEF_TARGETS.map((t) => briefWriter(t))
package/src/brief.ts ADDED
@@ -0,0 +1,238 @@
1
+ import type { Session, Turn } from "./session/types.ts"
2
+ import { asText, firstUserText } from "./session/util.ts"
3
+
4
+ // The continuation brief: what a model needs to carry on a session it never saw, in a fixed budget.
5
+ // Built without a model call, from the portable session, essentials first: where it came from, the goal,
6
+ // where it stands, files changed, commands run, the latest requests, then as many recent turns as fit.
7
+ // Design: docs/sessions/2026-11-08-session-switcher.md, section 3.2.
8
+
9
+ /** Characters each target takes at session start (Codex hook output ~2,500 tokens; Claude Code SessionStart 10,000 characters; Cursor's prompt deeplink 10,000). */
10
+ export const BRIEF_BUDGET: Record<string, number> = {
11
+ "claude-code": 9_500,
12
+ codex: 8_000,
13
+ cursor: 8_000,
14
+ default: 8_000,
15
+ }
16
+
17
+ const LABEL: Record<string, string> = {
18
+ "claude-code": "Claude Code",
19
+ codex: "Codex",
20
+ "cursor-ide": "Cursor",
21
+ "cursor-cli": "the Cursor CLI",
22
+ "gemini-cli": "Gemini CLI",
23
+ "vscode-copilot": "Copilot Chat in VS Code",
24
+ "copilot-cli": "the Copilot CLI",
25
+ cline: "Cline",
26
+ roo: "Roo Code",
27
+ opencode: "OpenCode",
28
+ "100xprompt": "100xprompt",
29
+ cursor: "Cursor",
30
+ aider: "Aider",
31
+ antigravity: "Antigravity",
32
+ devin: "Devin CLI",
33
+ hermes: "Hermes Agent",
34
+ mastracode: "Mastra Code",
35
+ muse: "Muse Code",
36
+ omp: "Oh My Pi",
37
+ "amazon-q-cli": "the Amazon Q CLI",
38
+ "amazon-q-ide": "Amazon Q in the IDE",
39
+ auggie: "Auggie",
40
+ "cline-sdk": "Cline",
41
+ "cline-cursor": "Cline in Cursor",
42
+ "cline-windsurf": "Cline in Windsurf",
43
+ "roo-cursor": "Roo Code in Cursor",
44
+ "roo-windsurf": "Roo Code in Windsurf",
45
+ codebuff: "Codebuff",
46
+ continue: "Continue",
47
+ crush: "Crush",
48
+ "factory-droid": "Factory Droid",
49
+ goose: "Goose",
50
+ "grok-cli": "Grok CLI",
51
+ "iflow-cli": "iFlow CLI",
52
+ "kilo-cli": "Kilo CLI",
53
+ "kilo-code": "Kilo Code",
54
+ "kilo-code-cursor": "Kilo Code in Cursor",
55
+ "kilo-code-windsurf": "Kilo Code in Windsurf",
56
+ "kimi-code": "Kimi Code",
57
+ "kiro-cli": "Kiro CLI",
58
+ "kiro-ide": "Kiro IDE",
59
+ "letta-code": "Letta Code",
60
+ "mistral-vibe": "Mistral Vibe",
61
+ openhands: "OpenHands",
62
+ pearai: "PearAI",
63
+ pi: "Pi",
64
+ "qoder-cli": "Qoder CLI",
65
+ "qwen-code": "Qwen Code",
66
+ "tabnine-cli": "Tabnine CLI",
67
+ void: "Void",
68
+ zed: "Zed",
69
+ any: "any tool",
70
+ }
71
+ export const toolLabel = (tool: string) => LABEL[tool] ?? tool
72
+
73
+ const cut = (s: string, n: number) => (s.length <= n ? s : `${s.slice(0, n - 1).trimEnd()}…`)
74
+ const oneLine = (s: string) => s.replace(/\s+/g, " ").trim()
75
+
76
+ function ago(when: string | undefined, now: number): string {
77
+ if (!when) return "at an unknown time"
78
+ const m = Math.round((now - Date.parse(when)) / 60_000)
79
+ if (!Number.isFinite(m) || m < 0) return `at ${when}`
80
+ if (m < 2) return "just now"
81
+ if (m < 90) return `${m} minutes ago`
82
+ if (m < 48 * 60) return `${Math.round(m / 60)} hours ago`
83
+ return `${Math.round(m / 1440)} days ago`
84
+ }
85
+
86
+ /** Files the session changed, from file_edit turns and edit/write tool calls, in order of first change. */
87
+ export function changedFiles(s: Session): string[] {
88
+ const out: string[] = []
89
+ const add = (p: unknown) => {
90
+ if (typeof p === "string" && p && !out.includes(p)) out.push(p)
91
+ }
92
+ for (const t of s.turns) {
93
+ if (t.kind === "file_edit") add(t.path)
94
+ if (t.kind === "tool_call" && (t.toolKind === "edit" || t.toolKind === "write")) {
95
+ const i = (t.input ?? {}) as Record<string, unknown>
96
+ add(i.file_path ?? i.path ?? i.target_file ?? i.filePath ?? i.relativeWorkspacePath ?? i.file)
97
+ }
98
+ }
99
+ return out
100
+ }
101
+
102
+ /** Shell commands the session ran, in order. */
103
+ export function commandsRun(s: Session): string[] {
104
+ const out: string[] = []
105
+ for (const t of s.turns) {
106
+ if (t.kind !== "tool_call" || t.toolKind !== "shell") continue
107
+ const i = (t.input ?? {}) as Record<string, unknown>
108
+ const c = i.command ?? i.cmd ?? i.commandLine
109
+ const text = Array.isArray(c) ? c.join(" ") : typeof c === "string" ? c : undefined
110
+ if (text) out.push(oneLine(text))
111
+ }
112
+ return out
113
+ }
114
+
115
+ function renderTurn(t: Turn, calls: Map<string, string>): string | undefined {
116
+ switch (t.kind) {
117
+ case "user":
118
+ return `USER: ${cut(t.text.trim(), 1_200)}`
119
+ case "assistant":
120
+ return t.text.trim() ? `ASSISTANT: ${cut(t.text.trim(), 1_200)}` : undefined
121
+ case "tool_call":
122
+ return `TOOL ${t.name}: ${cut(oneLine(asText(t.input)), 240)}`
123
+ case "tool_result":
124
+ return `RESULT${t.ok ? "" : " (error)"} of ${calls.get(t.callId) ?? "tool"}: ${cut(oneLine(t.output), 300)}`
125
+ case "compaction":
126
+ return `EARLIER (summarised): ${cut(t.summary.trim(), 1_500)}`
127
+ case "reasoning":
128
+ return undefined
129
+ case "file_edit":
130
+ return `EDITED ${t.path}`
131
+ case "subagent":
132
+ return `SUBAGENT${t.description ? `: ${cut(t.description, 200)}` : ""}`
133
+ }
134
+ }
135
+
136
+ /** The remembered items as a section, at most MEMORY_CHARS characters, one line each. */
137
+ function memorySection(items: BriefOptions["memory"]): string | undefined {
138
+ if (!items?.length) return undefined
139
+ const lines: string[] = []
140
+ let used = 0
141
+ for (const m of items) {
142
+ const line = `- ${m.kind ? `[${m.kind}] ` : ""}${cut(oneLine(m.text), 400)}`
143
+ if (used + line.length + 1 > MEMORY_CHARS) break
144
+ lines.push(line)
145
+ used += line.length + 1
146
+ }
147
+ return lines.length ? `\nREMEMBERED FOR THIS PROJECT (from earlier sessions, any tool):\n${lines.join("\n")}` : undefined
148
+ }
149
+
150
+ export interface BriefOptions {
151
+ budget?: number
152
+ now?: number
153
+ /** How the person can see the whole session, shown at the end. */
154
+ fullRecord?: string
155
+ /**
156
+ * What the project's memory holds that matters for the first request (brain memory, already limited to a token
157
+ * budget and redacted). Shown as its own section after the goal and state.
158
+ */
159
+ memory?: Array<{ kind?: string; text: string; source?: string }>
160
+ /**
161
+ * Lean: the essentials plus memory and only the last few turns, in a smaller budget. The rest stays reachable through
162
+ * the brain tools (`brain_recall`, `brain_expand`) or the CLI, so it costs tokens only when the model asks.
163
+ */
164
+ lean?: boolean
165
+ /** Name the on-demand tools in the closing line. Default true when lean. */
166
+ mention?: boolean
167
+ /** A compact repo map (see memory/repomap.ts), shown in its own section in lean briefs only, at most MAP_CHARS characters. */
168
+ map?: string
169
+ }
170
+
171
+ /** Characters of a lean brief, and how many recent turns it keeps. */
172
+ export const LEAN_BUDGET = 4_500
173
+ const LEAN_TURNS = 6
174
+ /** The most characters the memory section may take. */
175
+ const MEMORY_CHARS = 1_800
176
+ /** The most characters the project map may take of a lean brief. */
177
+ export const MAP_CHARS = 1_200
178
+
179
+ /** The map as a section: whole lines only, at most MAP_CHARS characters; shorter text is used as it is. */
180
+ export function mapSection(map: string | undefined): string | undefined {
181
+ const t = map?.trim()
182
+ if (!t) return undefined
183
+ const head = "\nPROJECT MAP (regenerated from the code):\n"
184
+ const room = MAP_CHARS - head.length + 1
185
+ let body = t
186
+ if (body.length > room) {
187
+ const lines = body.slice(0, room).split("\n")
188
+ lines.pop() // the last line may be cut mid-way
189
+ body = lines.join("\n")
190
+ }
191
+ return body ? `${head}${body}` : undefined
192
+ }
193
+
194
+ /** A continuation brief for `s`, at most `budget` characters. */
195
+ export function buildBrief(s: Session, opts: BriefOptions = {}): string {
196
+ const budget = opts.budget ?? (opts.lean ? LEAN_BUDGET : BRIEF_BUDGET.default!)
197
+ const now = opts.now ?? Date.now()
198
+ const h = s.header
199
+ const where = [h.project.cwd, h.project.gitBranch && `branch ${h.project.gitBranch}`].filter(Boolean).join(", ")
200
+ const head: string[] = [
201
+ `You are continuing a session that started in ${toolLabel(h.tool)}${h.title ? ` ("${cut(oneLine(h.title), 120)}")` : ""}, last active ${ago(h.updatedAt, now)}${where ? `, in ${where}` : ""}. The person switched tools and wants to carry on where it left off. Treat what follows as the history of this work; check files before relying on details, since they may have changed.`,
202
+ ]
203
+ const goal = firstUserText(s)
204
+ if (goal) head.push(`\nGOAL (the first request):\n${cut(goal.trim(), 900)}`)
205
+ const lastAssistant = [...s.turns].reverse().find((t) => t.kind === "assistant" && t.text.trim()) as Extract<Turn, { kind: "assistant" }> | undefined
206
+ if (lastAssistant) head.push(`\nWHERE IT STANDS (the last reply):\n${cut(lastAssistant.text.trim(), 1_200)}`)
207
+ const memory = memorySection(opts.memory)
208
+ if (memory) head.push(memory)
209
+ const mapText = opts.lean ? mapSection(opts.map) : undefined
210
+ if (mapText) head.push(mapText)
211
+ const files = changedFiles(s)
212
+ if (files.length) head.push(`\nFILES CHANGED (${files.length}):\n${files.slice(0, 30).map((f) => `- ${f}`).join("\n")}${files.length > 30 ? `\n- and ${files.length - 30} more` : ""}`)
213
+ const cmds = commandsRun(s)
214
+ if (cmds.length) head.push(`\nLAST COMMANDS RUN:\n${cmds.slice(-8).map((c) => `- ${cut(c, 200)}`).join("\n")}`)
215
+ const users = s.turns.filter((t) => t.kind === "user" && t.text.trim()) as Array<Extract<Turn, { kind: "user" }>>
216
+ if (users.length > 1) head.push(`\nLATEST REQUESTS:\n${users.slice(-3).map((u) => `- ${cut(oneLine(u.text), 400)}`).join("\n")}`)
217
+ const mention = opts.mention ?? !!opts.lean
218
+ const tail = `\n${opts.fullRecord ? `The whole session: ${opts.fullRecord}. ` : ""}${mention ? "More is stored than shown here: ask with the brain_recall tool (what the project knows) or brain_expand (a full tool output shown as \"output stored\"). " : ""}Pick up from the latest request; ask if the next step is unclear.`
219
+
220
+ let text = head.join("\n")
221
+ const room = budget - text.length - tail.length - 40
222
+ if (room > 200) {
223
+ // The most recent turns, newest last, as many as fit.
224
+ const calls = new Map(s.turns.filter((t) => t.kind === "tool_call").map((t) => [(t as Extract<Turn, { kind: "tool_call" }>).id, (t as Extract<Turn, { kind: "tool_call" }>).name]))
225
+ const lines: string[] = []
226
+ let used = 0
227
+ for (const t of [...s.turns].reverse()) {
228
+ const line = renderTurn(t, calls)
229
+ if (!line) continue
230
+ if (used + line.length + 1 > room || (opts.lean && lines.length >= LEAN_TURNS)) break
231
+ lines.unshift(line)
232
+ used += line.length + 1
233
+ }
234
+ if (lines.length) text += `\n\nRECENT TURNS:\n${lines.join("\n")}`
235
+ }
236
+ text += tail
237
+ return text.length <= budget ? text : cut(text, budget)
238
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env bun
2
+ import { writeFileSync } from "node:fs"
3
+ import { resolve } from "node:path"
4
+ import { parseArgs } from "node:util"
5
+ import type { SessionRef, Way, WriteResult } from "./adapters/types.ts"
6
+ import { changedFiles, toolLabel } from "./brief.ts"
7
+ import { gitInfo, homeDir } from "./fsutil.ts"
8
+ import { secureSelf } from "./self.ts"
9
+ import { HOOK_TOOLS, installHooks, runHook, uninstallHooks } from "./hooks.ts"
10
+ import type { Session } from "./session/types.ts"
11
+ import { prepare, sessionTokens } from "./memory/prepare.ts"
12
+ import { countTurns, firstUserText, toJsonl, unpaired } from "./session/util.ts"
13
+
14
+ // brain session: one working session, any AI coding tool.
15
+ // Design: docs/sessions/2026-11-08-session-switcher.md.
16
+
17
+ // Adapters load only for commands that use them: every tool's session-start hook runs this file, and
18
+ // one adapter failing to load (or loading slowly) must not hold up or break a session in another tool.
19
+ let loaded: typeof import("./adapters/index.ts") | undefined
20
+ const adapters = async () => (loaded ??= await import("./adapters/index.ts"))
21
+
22
+ const USAGE = `brain session <command>
23
+
24
+ list [--tool <id>] [--here] [--limit 30] [--json] every session on this machine, newest first
25
+ show <id> [--json] a session's header, size and what would be lost
26
+ export <id> [-o file] the portable session (brain-session/v1 JSONL)
27
+ continue <id> --in <tool> [--way native|import|brief] [--cwd dir] [--dry-run] [--no-open]
28
+ handoff --in <tool> [--from <tool>] [--dry-run] [--no-open] the newest session in this folder, to <tool>
29
+ continue/handoff also take [--lean | --full] [--budget <tokens>] [--no-memory] [--no-tidy] lean: big tool output becomes digests (automatic on long sessions); budget: mask in levels until the session fits
30
+ targets where a session can go, and how
31
+ hooks install|uninstall|status [--tool <id>] [--dry-run] session-start hooks that deliver briefs
32
+ hook <event> --tool <id> (run by a tool's hook; reads the hook's JSON on stdin)
33
+
34
+ <id> is <tool>:<session id> as \`list\` prints it, or a unique start of the session id.
35
+
36
+ brain memory ingest <id> | search <query> [--project p] | status | forget [--project p] [--source id] | sync [--here] [--limit n] what projects remember, across tools
37
+ brain tokens audit [--dir <folder>] static, read-only check of what a project reloads into every request (instruction files, MCP servers, ignore files)
38
+ brain tokens lookups how often each kind of stubbed output is asked back (brain_expand); a high rate means its preview is too thin
39
+ brain org login --server <url> --token <t> | logout | whoami | status | promote|demote (--ids a,b | --source <id>) --to <scope> | forget --mine
40
+ brain org admin init|serve|user|token|team|project|policy|audit|retention shared memory for an organisation (roles, scopes, audit)
41
+ brain mcp serve | install | uninstall | status [--tool <id>] the brain_recall / brain_expand tools for your AI tools
42
+ brain gateway login [--no-browser] | logout | connect --tool <id> | disconnect --tool <id> | status | tools sign in to 100xprompt and route a coding tool through its gateway (and back)
43
+ brain dashboard [push|on|off|status] [--quiet] [--no-open] send this machine's report (tools, gateway, conversions; never chat text) to your 100xprompt account and open its Brain view
44
+ brain --version the version, and where this brain runs from
45
+ brain expand <blob id> the full text of a tool output that was replaced by a digest`
46
+
47
+ const home = homeDir()
48
+ const out = (s: string) => process.stdout.write(`${s}\n`)
49
+ // Output piped into something that stops reading early (`brain session list | head`) ends quietly, not with a trace.
50
+ process.stdout.on("error", (e: NodeJS.ErrnoException) => {
51
+ if (e.code === "EPIPE") process.exit(0)
52
+ throw e
53
+ })
54
+ secureSelf()
55
+ const fail = (s: string): never => {
56
+ process.stderr.write(`brain: ${s}\n`)
57
+ process.exit(1)
58
+ }
59
+ /** After a command that changes what the dashboard report says: a report in a detached process (src/dashboard/send.ts). */
60
+ async function reportLater() {
61
+ try {
62
+ const { reportInBackground } = await import("./dashboard/send.ts")
63
+ reportInBackground(home)
64
+ } catch {}
65
+ }
66
+
67
+ async function allRefs(tool?: string): Promise<{ refs: SessionRef[]; errors: string[] }> {
68
+ const { READERS } = await adapters()
69
+ const readers = tool ? READERS.filter((r) => r.tool === tool) : READERS
70
+ if (tool && !readers.length) fail(`no reader for "${tool}"; readers: ${READERS.map((r) => r.tool).join(", ")}`)
71
+ const errors: string[] = []
72
+ const lists = await Promise.all(readers.map(async (r) => {
73
+ try {
74
+ return await r.list(home)
75
+ } catch (e) {
76
+ errors.push(`${r.tool}: ${(e as Error).message}`)
77
+ return []
78
+ }
79
+ }))
80
+ return { refs: lists.flat().sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")), errors }
81
+ }
82
+
83
+ async function resolveRef(given: string): Promise<SessionRef> {
84
+ // An id copied from an older, shortened list ends in "…": what comes before it is still a unique start.
85
+ const id = given.replace(/…$/, "")
86
+ const { readerFor } = await adapters()
87
+ const colon = id.indexOf(":")
88
+ const tool = colon > 0 && readerFor(id.slice(0, colon)) ? id.slice(0, colon) : undefined
89
+ const want = tool ? id.slice(colon + 1) : id
90
+ const { refs } = await allRefs(tool)
91
+ const exact = refs.filter((r) => r.sourceId === want)
92
+ const matches = exact.length ? exact : refs.filter((r) => r.sourceId.startsWith(want))
93
+ if (!matches.length) fail(`no session "${id}"; see \`brain session list\``)
94
+ if (matches.length > 1) fail(`"${id}" matches ${matches.length} sessions (${matches.slice(0, 4).map((r) => `${r.tool}:${r.sourceId}`).join(", ")}…); give more of the id`)
95
+ return matches[0]!
96
+ }
97
+
98
+ async function readRef(ref: SessionRef): Promise<Session> {
99
+ const { readerFor } = await adapters()
100
+ return readerFor(ref.tool)!.read(ref, home)
101
+ }
102
+
103
+ /**
104
+ * Whether a session that ran in `cwd` belongs to the folder `here`: the same folder or one inside it, or a
105
+ * folder above it in the same git repository. A folder above that is not the same repository (/tmp, home)
106
+ * is a different project, so a session there is never "here" and never handed off.
107
+ */
108
+ function inProject(cwd: string | undefined, here: string): boolean {
109
+ if (!cwd) return false
110
+ if (cwd === here || cwd.startsWith(`${here}/`)) return true
111
+ if (!here.startsWith(`${cwd}/`) || cwd === home || home.startsWith(`${cwd}/`)) return false
112
+ const a = gitInfo(cwd).gitRemote
113
+ return !!a && a === gitInfo(here).gitRemote
114
+ }
115
+
116
+ const ago = (when?: string) => {
117
+ if (!when) return "?"
118
+ const m = (Date.now() - Date.parse(when)) / 60_000
119
+ return m < 60 ? `${Math.max(0, Math.round(m))}m ago` : m < 48 * 60 ? `${Math.round(m / 60)}h ago` : `${Math.round(m / 1440)}d ago`
120
+ }
121
+ const short = (p?: string) => (p ? p.replace(home, "~") : "")
122
+ const cut = (s: string, n: number) => (s.length <= n ? s : `${s.slice(0, n - 1)}…`)
123
+
124
+ function describe(s: Session): string {
125
+ const c = countTurns(s)
126
+ const u = unpaired(s)
127
+ const h = s.header
128
+ const lines = [
129
+ `${h.id} (${toolLabel(h.tool)}${h.toolVersion ? ` ${h.toolVersion}` : ""})`,
130
+ h.title ? `title ${h.title}` : "",
131
+ `project ${short(h.project.cwd)}${h.project.gitBranch ? ` @ ${h.project.gitBranch}` : ""}`,
132
+ `when ${h.startedAt ?? "?"} → ${h.updatedAt ?? "?"}`,
133
+ `models ${h.models.join(", ") || "?"}`,
134
+ `turns ${c.user} user, ${c.assistant} assistant, ${c.tool_call} tool calls, ${c.tool_result} results, ${c.reasoning} reasoning, ${c.compaction} compactions, ${c.subagent} subagents${s.children?.length ? ` (${s.children.length} read)` : ""}`,
135
+ `files ${changedFiles(s).length} changed`,
136
+ u.calls.length || u.results.length ? `unpaired ${u.calls.length} calls without a result, ${u.results.length} results without a call` : "",
137
+ `lost ${h.lost.length ? h.lost.join("; ") : "nothing"}`,
138
+ `source ${short(h.sourcePath)}`,
139
+ firstUserText(s) ? `first ask ${cut(firstUserText(s)!.replace(/\s+/g, " "), 160)}` : "",
140
+ ]
141
+ return lines.filter(Boolean).join("\n")
142
+ }
143
+
144
+ function report(r: WriteResult) {
145
+ out(`${r.dryRun ? "Would continue" : "Continued"} in ${r.tool} by ${r.way === "native" ? "a native session" : r.way === "import" ? "its own import" : "a brief"}${r.sessionId ? ` (session ${r.sessionId})` : ""}.`)
146
+ for (const w of r.written) out(` ${r.dryRun ? "would write" : "wrote"} ${short(w)}`)
147
+ if (r.lost.length) out(` not carried over: ${r.lost.join("; ")}`)
148
+ out(`Next: ${r.open}`)
149
+ if (r.note) out(` then: ${r.note}`)
150
+ }
151
+
152
+ async function continueIn(source: Session, target: string, opts: { way?: Way; cwd?: string; dryRun: boolean; open: boolean; lean?: boolean; noMemory?: boolean; budget?: number; tidy?: false }) {
153
+ const { writersFor } = await adapters()
154
+ const candidates = writersFor(target).filter((w) => !opts.way || w.way === opts.way)
155
+ if (!candidates.length) fail(`can't continue in "${target}"${opts.way ? ` by ${opts.way}` : ""}; see \`brain session targets\``)
156
+ const cwd = resolve(opts.cwd ?? source.header.project.cwd ?? process.cwd())
157
+ const prepared = await prepare(source, { home, cwd, dryRun: opts.dryRun, ...(opts.lean !== undefined ? { lean: opts.lean } : {}), ...(opts.noMemory ? { noMemory: true } : {}), ...(opts.budget ? { budget: opts.budget } : {}), ...(opts.tidy === false ? { tidy: false } : {}) })
158
+ const session = prepared.session
159
+ for (const n of prepared.notes) out(n)
160
+ const wopts = { home, cwd, dryRun: opts.dryRun, brief: prepared.brief }
161
+ const skipped: string[] = []
162
+ for (const w of candidates) {
163
+ const ready = await w.ready(wopts)
164
+ if (!ready.ok) {
165
+ skipped.push(`${w.way}: ${ready.why}`)
166
+ continue
167
+ }
168
+ for (const s of skipped) out(`(${s}; using the next way)`)
169
+ const r = await w.write(session, wopts)
170
+ report(r)
171
+ if (!r.dryRun) {
172
+ // The dashboard's conversion log: tools, way, title, project folder name and sizes; never the chat (src/dashboard/conversions.ts).
173
+ const { logConversion } = await import("./dashboard/conversions.ts")
174
+ const { readerFor } = await adapters()
175
+ const to = r.tool === "cursor" ? "cursor-ide" : r.tool
176
+ logConversion(home, { from: source.header.tool, fromLabel: readerFor(source.header.tool)?.label ?? toolLabel(source.header.tool), to: r.tool, toLabel: readerFor(to)?.label ?? w.label, way: r.way, title: source.header.title, cwd: source.header.project.cwd ?? cwd, turns: source.turns.length, tokens: sessionTokens(session), lean: prepared.lean })
177
+ }
178
+ if (!r.dryRun && r.way === "brief") {
179
+ const brief = Bun.file(r.written[0]!)
180
+ // The clipboard where the system has one (pbcopy on macOS, wl-copy or xclip on Linux, clip on Windows); a missing tool is fine.
181
+ const copy = process.platform === "darwin" ? ["pbcopy"] : process.platform === "win32" ? ["clip"] : Bun.which("wl-copy") ? ["wl-copy"] : ["xclip", "-selection", "clipboard"]
182
+ let copied = false
183
+ try {
184
+ if (await brief.exists()) copied = Bun.spawnSync(copy, { stdin: Buffer.from(await brief.text()) }).exitCode === 0
185
+ } catch {}
186
+ if (copied) out(" (the brief is also on the clipboard)")
187
+ }
188
+ if (!r.dryRun && opts.open && target === "cursor") Bun.spawnSync(["/bin/sh", "-c", r.open])
189
+ if (!r.dryRun) await reportLater()
190
+ return
191
+ }
192
+ fail(`no way to continue in "${target}" right now: ${skipped.join("; ")}`)
193
+ }
194
+
195
+ /** --lean / --full into the option (neither: decided by the session's size). */
196
+ const leanOf = (v: { lean?: boolean; full?: boolean }): { lean?: boolean } => (v.lean && v.full ? fail("--lean and --full together?") : v.lean ? { lean: true } : v.full ? { lean: false } : {})
197
+
198
+ /** --budget <tokens> into a positive integer (or a clear failure). */
199
+ const budgetOf = (v: { budget?: string }): { budget?: number } => {
200
+ if (v.budget === undefined) return {}
201
+ const n = Number(v.budget)
202
+ if (!Number.isInteger(n) || n < 1_000) fail("--budget needs a whole number of tokens, at least 1000 (for example --budget 80000)")
203
+ return { budget: n }
204
+ }
205
+
206
+ async function main() {
207
+ const [cmd, ...rest] = process.argv.slice(2)
208
+ if (cmd === "--help" || cmd === "-h" || cmd === "help") return out(USAGE)
209
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
210
+ const { CLI_PATH, PACKAGE, VERSION, isEphemeral } = await import("./self.ts")
211
+ out(`${PACKAGE} ${VERSION}`)
212
+ out(`running from ${CLI_PATH}${isEphemeral() ? " (a temporary bunx copy: hooks and MCP entries it installs run the pinned package)" : ""}`)
213
+ return
214
+ }
215
+ // Commands with their own flags: `mcp serve` must keep stdout clean for the protocol, so they run before option parsing.
216
+ if (cmd === "gateway") {
217
+ const { gatewayCommand } = await import("./gateway/connect.ts")
218
+ const code = await gatewayCommand(rest, home)
219
+ if (code === 0 && ["login", "logout", "connect", "disconnect"].includes(rest[0] ?? "") && !rest.includes("--dry-run")) await reportLater()
220
+ process.exit(code)
221
+ }
222
+ if (cmd === "mcp") {
223
+ const { mcpCommand } = await import("./memory/mcp-install.ts")
224
+ const code = await mcpCommand(rest, home)
225
+ if (code === 0 && ["install", "uninstall"].includes(rest[0] ?? "") && !rest.includes("--dry-run")) await reportLater()
226
+ process.exit(code)
227
+ }
228
+ if (cmd === "dashboard") {
229
+ const { dashboardCommand } = await import("./dashboard/cli.ts")
230
+ process.exit(await dashboardCommand(rest, home))
231
+ }
232
+ if (cmd === "memory" && rest[0] === "sync") {
233
+ const { syncMemory } = await import("./memory/sync.ts")
234
+ const here = rest.includes("--here")
235
+ const limitAt = rest.indexOf("--limit")
236
+ const r = await syncMemory({ home, ...(here ? { cwd: process.cwd() } : {}), ...(limitAt >= 0 ? { limit: Number(rest[limitAt + 1]) || 8 } : {}) })
237
+ if (!rest.includes("--quiet")) {
238
+ if (r.unavailable) out(`memory sync: memory is not available (${r.unavailable})`)
239
+ else if (r.busy) out("memory sync: another sync is running")
240
+ else out(`memory sync: ${r.ingested} session${r.ingested === 1 ? "" : "s"} (${r.items} items) added, ${r.upToDate} already up to date${r.deferred ? `, ${r.deferred} left for the next run` : ""}${r.errors.length ? `; could not read ${r.errors.length}` : ""} (${(r.ms / 1000).toFixed(1)}s)`)
241
+ }
242
+ process.exit(0)
243
+ }
244
+ if (cmd === "memory") {
245
+ const { memoryCommand } = await import("./memory/cli.ts")
246
+ process.exit(await memoryCommand(rest, home, async (id) => readRef(await resolveRef(id)), out))
247
+ }
248
+ if (cmd === "tokens") {
249
+ const { tokensCommand } = await import("./memory/audit.ts")
250
+ process.exit(await tokensCommand(rest, home, { out }))
251
+ }
252
+ if (cmd === "org") {
253
+ const { orgCommand } = await import("./org/cli.ts")
254
+ process.exit(await orgCommand(rest, home, { out }))
255
+ }
256
+ if (cmd === "expand") {
257
+ const id = rest[0] ?? fail("expand needs a blob id (from a digest: \"id=...\")")
258
+ const { openBlobStore } = await import("./memory/blobs.ts")
259
+ const { expandStub } = await import("./memory/externalize.ts")
260
+ const text = openBlobStore(home).get(expandStub(id) ?? id)
261
+ if (text === undefined) fail(`no stored output "${id}" (it may have been pruned)`)
262
+ process.stdout.write(text!)
263
+ process.exit(0)
264
+ }
265
+ const sub = cmd === "session" ? rest.shift() : cmd
266
+ const args = cmd === "session" ? rest : rest
267
+ const { values, positionals } = parseArgs({
268
+ args,
269
+ allowPositionals: true,
270
+ options: {
271
+ tool: { type: "string" }, here: { type: "boolean" }, limit: { type: "string" }, json: { type: "boolean" },
272
+ in: { type: "string" }, way: { type: "string" }, cwd: { type: "string" }, from: { type: "string" },
273
+ "dry-run": { type: "boolean" }, "no-open": { type: "boolean" }, lean: { type: "boolean" }, full: { type: "boolean" }, "no-memory": { type: "boolean" }, budget: { type: "string" }, "no-tidy": { type: "boolean" }, o: { type: "string" }, help: { type: "boolean", short: "h" },
274
+ },
275
+ })
276
+ if (!sub || values.help) return out(USAGE)
277
+
278
+ if (sub === "list") {
279
+ const { refs, errors } = await allRefs(values.tool)
280
+ const here = values.here ? process.cwd() : undefined
281
+ const shown = refs.filter((r) => !here || inProject(r.cwd, here)).slice(0, Number(values.limit ?? 30))
282
+ if (values.json) return out(JSON.stringify(shown, null, 1))
283
+ // The id column is never cut: it is what `show` and `continue` take, so it must be copyable whole.
284
+ const idw = Math.max(48, ...shown.map((r) => r.tool.length + 1 + r.sourceId.length))
285
+ for (const r of shown) out(`${`${r.tool}:${r.sourceId}`.padEnd(idw)} ${ago(r.updatedAt).padStart(8)} ${cut(short(r.cwd), 34).padEnd(34)} ${cut((r.title ?? "").replace(/\s+/g, " "), 60)}`)
286
+ out(`${shown.length} of ${refs.length} sessions${errors.length ? `; could not list: ${errors.join("; ")}` : ""}`)
287
+ return
288
+ }
289
+ if (sub === "show" || sub === "export") {
290
+ const id = positionals[0] ?? fail(`${sub} needs a session id`)
291
+ const s = await readRef(await resolveRef(id))
292
+ if (sub === "export") {
293
+ if (values.o) {
294
+ writeFileSync(values.o, toJsonl(s))
295
+ return out(`wrote ${values.o}`)
296
+ }
297
+ return process.stdout.write(toJsonl(s))
298
+ }
299
+ return out(values.json ? JSON.stringify(s.header, null, 1) : describe(s))
300
+ }
301
+ if (sub === "continue") {
302
+ const id = positionals[0] ?? fail("continue needs a session id")
303
+ const target = values.in ?? fail("continue needs --in <tool>")
304
+ const s = await readRef(await resolveRef(id))
305
+ return continueIn(s, target, { ...(values.way ? { way: values.way as Way } : {}), ...(values.cwd ? { cwd: values.cwd } : {}), dryRun: !!values["dry-run"], open: !values["no-open"], ...leanOf(values), ...budgetOf(values), ...(values["no-tidy"] ? { tidy: false as const } : {}), ...(values["no-memory"] ? { noMemory: true } : {}) })
306
+ }
307
+ if (sub === "handoff") {
308
+ const target = values.in ?? fail("handoff needs --in <tool>")
309
+ const here = process.cwd()
310
+ const { refs } = await allRefs(values.from)
311
+ const ref = refs.find((r) => r.tool !== target && inProject(r.cwd, here))
312
+ if (!ref) fail(`no session from another tool in ${short(here)}`)
313
+ out(`Handing off ${ref!.tool}:${ref!.sourceId}${ref!.title ? ` ("${cut(ref!.title, 60)}")` : ""}, ${ago(ref!.updatedAt)}.`)
314
+ return continueIn(await readRef(ref!), target, { ...(values.way ? { way: values.way as Way } : {}), cwd: here, dryRun: !!values["dry-run"], open: !values["no-open"], ...leanOf(values), ...budgetOf(values), ...(values["no-tidy"] ? { tidy: false as const } : {}), ...(values["no-memory"] ? { noMemory: true } : {}) })
315
+ }
316
+ if (sub === "targets") {
317
+ const { READERS, WRITERS, writersFor } = await adapters()
318
+ const targets = [...new Set(WRITERS.map((w) => w.tool))]
319
+ for (const t of targets) {
320
+ const ways = await Promise.all(writersFor(t).map(async (w) => {
321
+ const r = await w.ready({ home, cwd: process.cwd(), dryRun: true })
322
+ return `${w.way}${r.ok ? "" : ` (not now: ${r.why})`}`
323
+ }))
324
+ out(`${t.padEnd(16)} ${ways.join(", ")}`)
325
+ }
326
+ out(`\nreaders: ${READERS.map((r) => r.tool).join(", ")}`)
327
+ return
328
+ }
329
+ if (sub === "hooks") {
330
+ const action = positionals[0] ?? "status"
331
+ const tools = values.tool ? [values.tool] : HOOK_TOOLS
332
+ const changes = action === "install" ? await installHooks(tools, home, { dryRun: !!values["dry-run"] })
333
+ : action === "uninstall" ? await uninstallHooks(tools, home, { dryRun: !!values["dry-run"] })
334
+ : action === "status" ? await installHooks(tools, home, { dryRun: true })
335
+ : fail(`hooks ${action}? use install, uninstall or status`)
336
+ for (const c of changes) out(`${c.tool.padEnd(14)} ${c.action}${c.why ? ` (${c.why})` : ""} ${short(c.path)}`)
337
+ if (!changes.length) out("no tools with session-start hooks found")
338
+ if ((action === "install" || action === "uninstall") && !values["dry-run"]) await reportLater()
339
+ return
340
+ }
341
+ if (sub === "hook") {
342
+ const event = positionals[0] ?? fail("hook needs an event")
343
+ const tool = values.tool ?? fail("hook needs --tool")
344
+ const text = await runHook(event, tool, await Bun.stdin.text(), home)
345
+ if (text) process.stdout.write(text)
346
+ return
347
+ }
348
+ fail(`unknown command "${sub}"\n\n${USAGE}`)
349
+ }
350
+
351
+ await main()