@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,148 @@
1
+ // `brain dashboard [push|on|off|status] [--quiet] [--no-open]`: this machine's report for the Brain view of the 100xprompt
2
+ // console (https://100xprompt.com/dashboard?view=brain). The dashboard is on the website only; brain sends the report
3
+ // (report.ts, send.ts) and opens the page.
4
+ import type { Reader } from "../adapters/types.ts"
5
+ import type { Env } from "../gateway/common.ts"
6
+ import { openInBrowser } from "../gateway/signin.ts"
7
+ import { readConversions } from "./conversions.ts"
8
+ import { DASHBOARD_URL, gatewayUrl, noticeOnce, reportUrl, sendingOff, sendReport, signedIn, startScheduled } from "./send.ts"
9
+ import type { BrainReport } from "./report.ts"
10
+ import { readCache, readSettings, updateSettings } from "./state.ts"
11
+
12
+ const USAGE = [
13
+ "brain dashboard [--no-open] count this machine's sessions, send its report to your 100xprompt account now, and open the Brain view",
14
+ "brain dashboard push [--quiet] send the report now, without counting sessions (the last counts are reused)",
15
+ "brain dashboard off | on stop or start sending reports from this machine (kept in ~/.brain/dashboard.json)",
16
+ "brain dashboard status whether reports are sent, and when the last one went out",
17
+ ]
18
+
19
+ const FLAGS = new Set(["--quiet", "--no-open", "--at", "--help", "-h"])
20
+
21
+ function ago(iso: string | undefined, now = Date.now()): string {
22
+ const t = iso ? Date.parse(iso) : NaN
23
+ if (!Number.isFinite(t)) return ""
24
+ const m = Math.max(0, Math.round((now - t) / 60_000))
25
+ return m < 1 ? "just now" : m < 60 ? `${m}m ago` : m < 48 * 60 ? `${Math.round(m / 60)}h ago` : `${Math.round(m / 1440)}d ago`
26
+ }
27
+
28
+ const plural = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
29
+
30
+ /** One line on what a report holds. */
31
+ export function summary(r: BrainReport): string {
32
+ const counted = r.harnesses.filter((h) => h.sessions !== null)
33
+ const sessions = counted.reduce((n, h) => n + (h.sessions ?? 0), 0)
34
+ const connected = r.harnesses.filter((h) => h.gateway === "connected").length
35
+ return `${plural(r.harnesses.length, "tool")} (${connected} connected to the gateway)${counted.length ? `, ${plural(sessions, "session")}` : ""}, ${plural(r.totals.conversions, "conversion")}`
36
+ }
37
+
38
+ export async function dashboardCommand(
39
+ args: string[],
40
+ home: string,
41
+ io: { out(l: string): void; err(l: string): void } = { out: (l) => process.stdout.write(`${l}\n`), err: (l) => process.stderr.write(`${l}\n`) },
42
+ deps: { env?: Env; fetchImpl?: typeof fetch; open?: (url: string) => void; countTimeoutMs?: number; readers?: Reader[] } = {},
43
+ ): Promise<number> {
44
+ const env = deps.env ?? process.env
45
+ const atAt = args.indexOf("--at")
46
+ const words = args.filter((a, i) => !a.startsWith("-") && !(atAt >= 0 && i === atAt + 1))
47
+ const sub = words[0]
48
+ const quiet = args.includes("--quiet")
49
+ const say = (l: string) => {
50
+ if (!quiet) io.out(l)
51
+ }
52
+ const unknown = args.find((a) => a.startsWith("-") && !FLAGS.has(a))
53
+ if (args.includes("--help") || args.includes("-h") || sub === "help") {
54
+ USAGE.forEach((l) => io.out(l))
55
+ return 0
56
+ }
57
+ if (unknown || words.length > 1) {
58
+ io.out(unknown ? `unknown option ${unknown}` : `unexpected "${words[1]}"`)
59
+ USAGE.forEach((l) => io.out(`usage: ${l}`))
60
+ return 2
61
+ }
62
+ const notSignedIn = "not signed in to the gateway: run `brain gateway login` first, then `brain dashboard`"
63
+ const send = async (counts: boolean): Promise<void> => {
64
+ // The notice goes where the person reads it: the output, or stderr when the output is asked to stay quiet.
65
+ noticeOnce(home, quiet ? io.err : io.out)
66
+ const { buildReport, countAndKeep } = await import("./report.ts")
67
+ const report = await buildReport(home, {
68
+ env,
69
+ ...(counts ? { counts: await countAndKeep(home, { timeoutMs: deps.countTimeoutMs ?? 20_000, ...(deps.readers ? { readers: deps.readers } : {}) }) } : {}),
70
+ })
71
+ const r = await sendReport(home, report, deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {})
72
+ say(r.ok ? `sent this machine's report to ${gatewayUrl(home)}: ${summary(report)}` : `could not send the report: ${r.why}; the dashboard keeps the last report it got`)
73
+ }
74
+
75
+ switch (sub) {
76
+ case undefined: {
77
+ if (!signedIn(home)) {
78
+ io.out(notSignedIn)
79
+ return 1
80
+ }
81
+ const off = sendingOff(home, env)
82
+ if (off) say(`reports are off (${off}): nothing was sent${readSettings(home).off ? "; `brain dashboard on` starts them again" : ""}`)
83
+ else await send(true)
84
+ if (args.includes("--no-open")) say(`the dashboard: ${DASHBOARD_URL}`)
85
+ else {
86
+ ;(deps.open ?? openInBrowser)(DASHBOARD_URL)
87
+ say(`opening ${DASHBOARD_URL}`)
88
+ }
89
+ return 0
90
+ }
91
+ case "push": {
92
+ // `--at <ms>`: a background push started by another command waits for its turn (send.ts), then reads the state.
93
+ const at = atAt >= 0 ? Number(args[atAt + 1]) : NaN
94
+ if (Number.isFinite(at)) await startScheduled(home, at, env)
95
+ if (!signedIn(home)) {
96
+ say(notSignedIn)
97
+ return quiet ? 0 : 1
98
+ }
99
+ const off = sendingOff(home, env)
100
+ if (off) {
101
+ say(`reports are off (${off}): nothing was sent`)
102
+ return 0
103
+ }
104
+ await send(false)
105
+ return 0
106
+ }
107
+ case "off":
108
+ updateSettings(home, (s) => {
109
+ s.off = true
110
+ })
111
+ say("reports are off: brain sends nothing from this machine to your 100xprompt account. The console keeps the last report until you remove this machine there; `brain dashboard on` starts them again.")
112
+ return 0
113
+ case "on": {
114
+ updateSettings(home, (s) => {
115
+ delete s.off
116
+ })
117
+ const notes = [sendingOff(home, env) ? "BRAIN_NO_REPORT is set here, so nothing is sent while it is" : "", signedIn(home) ? "" : "sign in first: brain gateway login"].filter(Boolean)
118
+ say(`reports are on: brain sends this machine's report after commands that change it, and with \`brain dashboard\`${notes.length ? ` (${notes.join("; ")})` : ""}.`)
119
+ return 0
120
+ }
121
+ case "status": {
122
+ const { gatewayState, machineName } = await import("./report.ts")
123
+ const { machineId } = await import("./state.ts")
124
+ const off = sendingOff(home, env)
125
+ const push = readCache(home).push ?? {}
126
+ const counts = readCache(home).counts
127
+ const email = gatewayState(home).email
128
+ const lines = [
129
+ `reports: ${off ? `off (${off})` : "on"}`,
130
+ `signed in: ${signedIn(home) ? `yes${typeof email === "string" && email ? `, as ${email}` : ""}` : "no (brain gateway login)"}`,
131
+ `sends to: ${reportUrl(home)}`,
132
+ `last sent: ${push.lastSentAt ? `${push.lastSentAt} (${ago(push.lastSentAt)})` : "never"}`,
133
+ ...(push.lastError && push.lastAttemptAt ? [`last attempt: ${push.lastAttemptAt} (${ago(push.lastAttemptAt)}) failed: ${push.lastError}`] : []),
134
+ ...(typeof push.pendingUntilMs === "number" && push.pendingUntilMs > Date.now() ? [`next: a report waits to go out at ${new Date(push.pendingUntilMs).toISOString()}`] : []),
135
+ `sessions counted: ${counts?.at ? `${counts.at} (${ago(counts.at)}; \`brain dashboard\` counts them again)` : "never (`brain dashboard` counts them)"}`,
136
+ `conversions logged: ${readConversions(home, 0).total}`,
137
+ `machine: ${machineName()} (${machineId(home)})`,
138
+ `view: ${DASHBOARD_URL}`,
139
+ ]
140
+ lines.forEach((l) => io.out(l))
141
+ return 0
142
+ }
143
+ default:
144
+ io.out(`unknown dashboard command "${sub}"`)
145
+ USAGE.forEach((l) => io.out(`usage: ${l}`))
146
+ return 2
147
+ }
148
+ }
@@ -0,0 +1,170 @@
1
+ // The conversion log: one line per session brain carried from one tool into another (`session continue` / `handoff`, not
2
+ // dry runs), for the Brain view of the console. <home>/.brain/logs/conversions.jsonl (0600), append-only. What a line
3
+ // holds is exactly a conversion of the dashboard report (docs/dashboard-contract.md): which tools, how, when, the chat's
4
+ // title and its project FOLDER NAME. Never the chat text, never a full path.
5
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs"
6
+ import { homedir } from "node:os"
7
+ import { basename, join, resolve } from "node:path"
8
+ import type { Way } from "../adapters/types.ts"
9
+ import { redact } from "../memory/redact.ts"
10
+
11
+ export const conversionLog = (home: string) => join(home, ".brain", "logs", "conversions.jsonl")
12
+
13
+ export const TITLE_MAX = 200
14
+ export const PROJECT_MAX = 120
15
+ const LABEL_MAX = 80
16
+ const WAYS: readonly Way[] = ["native", "import", "brief"]
17
+
18
+ export interface Conversion {
19
+ /** Made when the conversion happened: the gateway drops a conversion it already has. */
20
+ id: string
21
+ at: string
22
+ from: string
23
+ fromLabel: string
24
+ to: string
25
+ toLabel: string
26
+ way: Way
27
+ title: string | null
28
+ /** The project folder's name only (basename), never its path. */
29
+ project: string | null
30
+ turns: number | null
31
+ /** Estimated tokens of the converted session. */
32
+ tokens: number | null
33
+ /** Big tool outputs were replaced by digests. */
34
+ lean: boolean
35
+ }
36
+
37
+ /** At most `n` UTF-16 units, never ending in half of a surrogate pair. */
38
+ export function clip(s: string, n: number): string {
39
+ if (s.length <= n) return s
40
+ const cut = s.slice(0, n)
41
+ return /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut
42
+ }
43
+
44
+ /** A title as it may be sent: one line, secrets taken out, at most 200 characters; null when there is none. */
45
+ export function cleanTitle(t: unknown): string | null {
46
+ if (typeof t !== "string") return null
47
+ const one = redact(t.replace(/\s+/g, " ").trim())
48
+ return one ? clip(one, TITLE_MAX) : null
49
+ }
50
+
51
+ /** The project folder's name, from a folder path or a name; null for none, the root, or a home folder (not a project). */
52
+ export function projectName(cwd: unknown, homes: string[] = []): string | null {
53
+ if (typeof cwd !== "string" || !cwd.trim()) return null
54
+ const p = cwd.trim()
55
+ if (p.includes("/") || p.includes("\\")) {
56
+ const full = resolve(p)
57
+ if (full === "/" || homes.some((h) => h && resolve(h) === full)) return null
58
+ }
59
+ const name = basename(p.replace(/[\\/]+$/, "")).split("\\").pop() ?? ""
60
+ return name ? clip(name, PROJECT_MAX) : null
61
+ }
62
+
63
+ const count = (n: unknown): number | null => (typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.floor(n) : null)
64
+ const label = (s: unknown, fallback: string) => clip(typeof s === "string" && s.trim() ? s.trim() : fallback, LABEL_MAX)
65
+
66
+ /** What `continue` knows when a writer has written: the tools, the way, and the source session's header and size. */
67
+ export interface ConversionInput {
68
+ from: string
69
+ fromLabel: string
70
+ to: string
71
+ toLabel: string
72
+ way: Way
73
+ title?: string
74
+ /** The session's project folder (only its name is kept). */
75
+ cwd?: string
76
+ turns?: number
77
+ tokens?: number
78
+ lean: boolean
79
+ at?: string
80
+ }
81
+
82
+ /** "cursor" is what `continue --in` calls Cursor; everywhere else (readers, hooks, harnesses) it is "cursor-ide". */
83
+ export const toolId = (tool: string) => (tool === "cursor" ? "cursor-ide" : tool)
84
+
85
+ /** One line for the log, from what `continue` knows. */
86
+ export function conversionOf(c: ConversionInput, homes: string[] = [process.env.BRAIN_HOME ?? "", process.env.HOME ?? "", homedir()]): Conversion {
87
+ return {
88
+ id: crypto.randomUUID(),
89
+ at: c.at ?? new Date().toISOString(),
90
+ from: toolId(c.from),
91
+ fromLabel: label(c.fromLabel, c.from),
92
+ to: toolId(c.to),
93
+ toLabel: label(c.toLabel.replace(/^the /, ""), c.to),
94
+ way: c.way,
95
+ title: cleanTitle(c.title),
96
+ project: projectName(c.cwd, homes),
97
+ turns: count(c.turns),
98
+ tokens: count(c.tokens),
99
+ lean: !!c.lean,
100
+ }
101
+ }
102
+
103
+ /** Appends one conversion. Never throws: a log must not break a conversion that already happened. */
104
+ export function logConversion(home: string, c: ConversionInput): Conversion | undefined {
105
+ try {
106
+ const line = conversionOf(c)
107
+ const f = conversionLog(home)
108
+ mkdirSync(join(home, ".brain", "logs"), { recursive: true, mode: 0o700 })
109
+ const fresh = !existsSync(f)
110
+ appendFileSync(f, `${JSON.stringify(line)}\n`, { mode: 0o600 })
111
+ if (fresh) chmodSync(f, 0o600)
112
+ return line
113
+ } catch {
114
+ return undefined
115
+ }
116
+ }
117
+
118
+ const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/
119
+
120
+ /** A line of the log as it may be sent, or undefined for a line that is not a conversion (a damaged or foreign line). */
121
+ export function parseConversion(line: string): Conversion | undefined {
122
+ let v: any
123
+ try {
124
+ v = JSON.parse(line)
125
+ } catch {
126
+ return undefined
127
+ }
128
+ if (!v || typeof v !== "object" || typeof v.id !== "string" || !v.id || typeof v.at !== "string" || !ISO.test(v.at)) return undefined
129
+ if (typeof v.from !== "string" || !v.from || typeof v.to !== "string" || !v.to || !WAYS.includes(v.way)) return undefined
130
+ // Read back through the same filters it was written with: whatever is in the file, only these fields, clipped, leave.
131
+ return {
132
+ id: clip(v.id, 64),
133
+ at: v.at,
134
+ from: clip(v.from, LABEL_MAX),
135
+ fromLabel: label(v.fromLabel, v.from),
136
+ to: clip(v.to, LABEL_MAX),
137
+ toLabel: label(v.toLabel, v.to),
138
+ way: v.way,
139
+ title: cleanTitle(v.title),
140
+ project: projectName(v.project),
141
+ turns: count(v.turns),
142
+ tokens: count(v.tokens),
143
+ lean: v.lean === true,
144
+ }
145
+ }
146
+
147
+ /** Months (UTC, "YYYY-MM") counted in a report's totals.byMonth: the latest twelve that have any conversion. */
148
+ export const MONTHS_KEPT = 12
149
+
150
+ /** The newest `limit` conversions (newest first), how many the log holds in all, and how many per month (UTC). */
151
+ export function readConversions(home: string, limit = 500): { list: Conversion[]; total: number; byMonth: Record<string, number> } {
152
+ let text = ""
153
+ try {
154
+ text = readFileSync(conversionLog(home), "utf8")
155
+ } catch {
156
+ return { list: [], total: 0, byMonth: {} }
157
+ }
158
+ const all: Conversion[] = []
159
+ for (const line of text.split("\n")) {
160
+ if (!line.trim()) continue
161
+ const c = parseConversion(line)
162
+ if (c) all.push(c)
163
+ }
164
+ // The file is in the order they happened; a sort by time keeps that for lines written by machines with a skewed clock.
165
+ const newest = all.map((c, i) => ({ c, i })).sort((a, b) => b.c.at.localeCompare(a.c.at) || b.i - a.i).map((x) => x.c)
166
+ const months = new Map<string, number>()
167
+ for (const c of all) months.set(c.at.slice(0, 7), (months.get(c.at.slice(0, 7)) ?? 0) + 1)
168
+ const byMonth = Object.fromEntries([...months].sort((a, b) => b[0].localeCompare(a[0])).slice(0, MONTHS_KEPT))
169
+ return { list: newest.slice(0, Math.max(0, limit)), total: all.length, byMonth }
170
+ }
@@ -0,0 +1,261 @@
1
+ // The report of this machine for the Brain view of the 100xprompt console, built exactly as docs/dashboard-contract.md
2
+ // says: the machine, the coding tools present here (sessions, gateway, MCP tools, hooks), the gateway settings, memory
3
+ // counts, and the conversions brain made. Summaries plus each chat's title and project folder name: never chat text, file
4
+ // contents, full paths or keys.
5
+ import { existsSync, readFileSync } from "node:fs"
6
+ import { hostname } from "node:os"
7
+ import { join } from "node:path"
8
+ import type { Reader } from "../adapters/types.ts"
9
+ import { toolLabel } from "../brief.ts"
10
+ import { DEFAULT_MODEL, DEFAULT_ROUTE, type Env, type GatewaySettings, modelName, statePath } from "../gateway/common.ts"
11
+ import { CONNECTORS, hasKey, installedHere } from "../gateway/connect.ts"
12
+ import { HOOK_TOOLS, hookInstalled } from "../hooks.ts"
13
+ import { MCP_TOOLS, statusMcp } from "../memory/mcp-install.ts"
14
+ import { chittaCandidates, memoryDbPath, openMemory } from "../memory/store.ts"
15
+ import { readOrgConfig } from "../org/config.ts"
16
+ import { VERSION } from "../self.ts"
17
+ import { clip, type Conversion, readConversions, toolId } from "./conversions.ts"
18
+ import { machineId, readCache, type ToolCount, updateCache } from "./state.ts"
19
+
20
+ export const SCHEMA = "brain-report/v1"
21
+ /** The gateway takes at most 500 conversions and 512 KB; a report is cut to fit below that. */
22
+ export const MAX_CONVERSIONS = 500
23
+ export const MAX_BYTES = 500_000
24
+
25
+ export interface Harness {
26
+ id: string
27
+ label: string
28
+ /** Sessions brain can read for it here; null when never counted (`brain dashboard` counts them). */
29
+ sessions: number | null
30
+ lastSessionAt: string | null
31
+ gateway: "connected" | "available" | "unsupported"
32
+ /** brain_recall / brain_expand installed in it; null when brain has no MCP installer for it. */
33
+ mcp: boolean | null
34
+ /** The session-start hook installed; null when brain has no hook for it. */
35
+ hooks: boolean | null
36
+ }
37
+
38
+ export interface BrainReport {
39
+ schema: typeof SCHEMA
40
+ sentAt: string
41
+ machine: { id: string; name: string; os: string; brainVersion: string }
42
+ harnesses: Harness[]
43
+ gateway: {
44
+ signedIn: boolean
45
+ email?: string
46
+ route: string
47
+ model: string
48
+ contextEdit: boolean
49
+ connected: Array<{ tool: string; label: string; since: string | null }>
50
+ }
51
+ memory: { available: true; projects: number; items: number } | { available: false }
52
+ conversions: Conversion[]
53
+ /** byMonth: conversions per month, UTC "YYYY-MM", the latest twelve months that have any. */
54
+ totals: { conversions: number; byMonth: Record<string, number> }
55
+ }
56
+
57
+ /** The computer's name: os.hostname() without ".local". */
58
+ export const machineName = (h: string = hostname()) => clip(h.replace(/\.local\.?$/i, "").trim(), 80) || "this machine"
59
+
60
+ const isoOrNull = (v: unknown): string | null => {
61
+ if (typeof v !== "string" && typeof v !== "number") return null
62
+ const t = typeof v === "number" ? v : Date.parse(v)
63
+ return Number.isFinite(t) ? new Date(t).toISOString() : null
64
+ }
65
+
66
+ interface GatewayState {
67
+ settings?: Partial<GatewaySettings>
68
+ email?: string
69
+ tools?: Record<string, { at?: string }>
70
+ }
71
+
72
+ /** <home>/.brain/gateway.json as `brain gateway` keeps it (read only here). */
73
+ export function gatewayState(home: string): GatewayState {
74
+ try {
75
+ const s = JSON.parse(readFileSync(statePath(home), "utf8"))
76
+ return s && typeof s === "object" ? s : {}
77
+ } catch {
78
+ return {}
79
+ }
80
+ }
81
+
82
+ /** What the MCP installer calls a tool, as the readers (and so the report) call it. */
83
+ const MCP_IDS: Record<string, string> = { cursor: "cursor-ide", gemini: "gemini-cli", vscode: "vscode-copilot" }
84
+ const mcpToolId = (id: string) => MCP_IDS[id] ?? id
85
+
86
+ async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T | undefined> {
87
+ let timer: ReturnType<typeof setTimeout> | undefined
88
+ const late = new Promise<undefined>((r) => {
89
+ timer = setTimeout(() => r(undefined), ms)
90
+ })
91
+ try {
92
+ return await Promise.race([p.catch(() => undefined), late])
93
+ } finally {
94
+ clearTimeout(timer)
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Sessions per tool, from every reader at once, within `timeoutMs`; a reader that fails or takes longer is left out (its
100
+ * last count stays in the cache). Some readers copy a big database to read it, so this runs only for `brain dashboard`.
101
+ */
102
+ export async function countSessions(home: string, o: { timeoutMs?: number; readers?: Reader[] } = {}): Promise<Record<string, ToolCount>> {
103
+ const readers = o.readers ?? (await import("../adapters/index.ts")).READERS
104
+ const out: Record<string, ToolCount> = {}
105
+ let timer: ReturnType<typeof setTimeout> | undefined
106
+ const deadline = new Promise<"late">((r) => {
107
+ timer = setTimeout(r, o.timeoutMs ?? 20_000, "late" as const)
108
+ })
109
+ try {
110
+ await Promise.all(
111
+ readers.map(async (r) => {
112
+ const refs = await Promise.race([r.list(home).catch(() => undefined), deadline])
113
+ if (!refs || refs === "late") return
114
+ const id = toolId(r.tool)
115
+ const newest = refs.map((x) => isoOrNull(x.updatedAt)).filter((x): x is string => !!x).sort().pop() ?? null
116
+ const had = out[id]
117
+ out[id] = { sessions: (had?.sessions ?? 0) + refs.length, lastSessionAt: [had?.lastSessionAt, newest].filter((x): x is string => !!x).sort().pop() ?? null }
118
+ }),
119
+ )
120
+ } finally {
121
+ clearTimeout(timer)
122
+ }
123
+ return out
124
+ }
125
+
126
+ /** Counts sessions now and keeps the counts for later background reports (merged over the last ones). */
127
+ export async function countAndKeep(home: string, o: { timeoutMs?: number; readers?: Reader[] } = {}): Promise<Record<string, ToolCount>> {
128
+ const fresh = await countSessions(home, o)
129
+ const c = updateCache(home, (c) => {
130
+ c.counts = { at: new Date().toISOString(), tools: { ...(c.counts?.tools ?? {}), ...fresh } }
131
+ })
132
+ return c.counts!.tools
133
+ }
134
+
135
+ async function memorySummary(home: string, env: Env): Promise<BrainReport["memory"]> {
136
+ try {
137
+ const org = env.BRAIN_ORG !== "off" && readOrgConfig(home)
138
+ // No store yet: say whether memory can be used, without creating one.
139
+ if (!org && !existsSync(memoryDbPath(home))) return chittaCandidates().some((f) => existsSync(f)) ? { available: true, projects: 0, items: 0 } : { available: false }
140
+ const mem = openMemory(home)
141
+ try {
142
+ const r = await withTimeout(
143
+ (async () => {
144
+ const s = await mem.status()
145
+ if (!s.available) return { available: false as const }
146
+ const projects = await mem.projects()
147
+ return { available: true as const, projects: projects.length, items: projects.reduce((n, p) => n + (Number(p.items) || 0), 0) }
148
+ })(),
149
+ 5_000,
150
+ )
151
+ return r ?? { available: false }
152
+ } finally {
153
+ mem.close()
154
+ }
155
+ } catch {
156
+ return { available: false }
157
+ }
158
+ }
159
+
160
+ /** Whether a tool the MCP installer knows has run here (its config or folder exists), as the installer decides it. */
161
+ function mcpToolHere(t: (typeof MCP_TOOLS)[number], home: string, env: Env): boolean {
162
+ if (t.id === "claude-code" && env.CLAUDE_CONFIG_DIR && existsSync(env.CLAUDE_CONFIG_DIR)) return true
163
+ return t.probe.some((p) => existsSync(join(home, p)))
164
+ }
165
+
166
+ /**
167
+ * Every coding tool brain knows (its readers, gateway connectors, MCP installer and hooks) that is present here: found by
168
+ * the connector's or the installer's probe, connected, with brain's MCP tools or hook in it, or with sessions counted.
169
+ */
170
+ export async function harnesses(home: string, counts: Record<string, ToolCount> | undefined, env: Env = process.env): Promise<Harness[]> {
171
+ const { READERS } = await import("../adapters/index.ts")
172
+ const st = gatewayState(home)
173
+ const connected = st.tools && typeof st.tools === "object" ? st.tools : {}
174
+ let mcpInstalled = new Map<string, boolean>()
175
+ try {
176
+ mcpInstalled = new Map(statusMcp(home).map((c) => [mcpToolId(c.tool), c.action === "installed"]))
177
+ } catch {}
178
+ const hookOn = (id: string) => {
179
+ try {
180
+ return hookInstalled(id, home)
181
+ } catch {
182
+ return false
183
+ }
184
+ }
185
+ const known = new Map<string, { label: string; present: boolean }>()
186
+ const add = (id: string, label: string, present: boolean) => {
187
+ const k = known.get(id)
188
+ if (k) k.present ||= present
189
+ else known.set(id, { label, present })
190
+ }
191
+ for (const r of READERS) add(toolId(r.tool), r.label, (counts?.[toolId(r.tool)]?.sessions ?? 0) > 0)
192
+ for (const c of CONNECTORS) {
193
+ let here = false
194
+ try {
195
+ here = installedHere(c, home, env)
196
+ } catch {}
197
+ add(c.id, c.label, here || !!connected[c.id])
198
+ }
199
+ for (const t of MCP_TOOLS) add(mcpToolId(t.id), t.label, mcpToolHere(t, home, env) || mcpInstalled.get(mcpToolId(t.id)) === true)
200
+ for (const id of HOOK_TOOLS) add(id, toolLabel(id), hookOn(id))
201
+ const mcpIds = new Set(MCP_TOOLS.map((t) => mcpToolId(t.id)))
202
+ const connectorIds = new Set(CONNECTORS.map((c) => c.id))
203
+ return [...known.entries()]
204
+ .filter(([, k]) => k.present)
205
+ .map(([id, k]): Harness => ({
206
+ id,
207
+ label: clip(k.label, 80),
208
+ sessions: counts?.[id] ? counts[id]!.sessions : null,
209
+ lastSessionAt: counts?.[id]?.lastSessionAt ?? null,
210
+ gateway: connected[id] ? "connected" : connectorIds.has(id) ? "available" : "unsupported",
211
+ mcp: mcpIds.has(id) ? mcpInstalled.get(id) === true : null,
212
+ hooks: HOOK_TOOLS.includes(id) ? hookOn(id) : null,
213
+ }))
214
+ .sort((a, b) => a.label.localeCompare(b.label) || a.id.localeCompare(b.id))
215
+ }
216
+
217
+ /** The gateway part: signed in or not, the settings `brain gateway status` shows, and the tools connected to it. */
218
+ export function gatewayPart(home: string): BrainReport["gateway"] {
219
+ const st = gatewayState(home)
220
+ const s = st.settings && typeof st.settings === "object" ? st.settings : {}
221
+ const label = (id: string) => CONNECTORS.find((c) => c.id === id)?.label ?? toolLabel(id)
222
+ return {
223
+ signedIn: hasKey(home),
224
+ ...(typeof st.email === "string" && st.email ? { email: clip(st.email, 200) } : {}),
225
+ route: typeof s.route === "string" && s.route ? s.route : DEFAULT_ROUTE,
226
+ model: modelName(typeof s.model === "string" && s.model ? s.model : DEFAULT_MODEL),
227
+ contextEdit: typeof s.contextEdit === "boolean" ? s.contextEdit : true,
228
+ connected: Object.entries(st.tools && typeof st.tools === "object" ? st.tools : {}).map(([tool, rec]) => ({ tool, label: clip(label(tool), 80), since: isoOrNull(rec?.at) })),
229
+ }
230
+ }
231
+
232
+ /**
233
+ * The whole report. Session counts are `counts` when given (`brain dashboard` just counted), else the last ones kept
234
+ * (null for a tool never counted); nothing here lists sessions, so a background report stays cheap.
235
+ */
236
+ export async function buildReport(home: string, o: { counts?: Record<string, ToolCount>; now?: Date; env?: Env } = {}): Promise<BrainReport> {
237
+ const env = o.env ?? process.env
238
+ const counts = o.counts ?? readCache(home).counts?.tools
239
+ const { list, total, byMonth } = readConversions(home, MAX_CONVERSIONS)
240
+ return {
241
+ schema: SCHEMA,
242
+ sentAt: (o.now ?? new Date()).toISOString(),
243
+ machine: { id: machineId(home), name: machineName(), os: process.platform, brainVersion: VERSION },
244
+ harnesses: await harnesses(home, counts, env),
245
+ gateway: gatewayPart(home),
246
+ memory: await memorySummary(home, env),
247
+ conversions: list,
248
+ totals: { conversions: total, byMonth },
249
+ }
250
+ }
251
+
252
+ /** The report as it is sent: at most MAX_BYTES of JSON, the oldest conversions dropped first if it is bigger. */
253
+ export function reportBody(r: BrainReport, maxBytes = MAX_BYTES): string {
254
+ let body = JSON.stringify(r)
255
+ let list = r.conversions
256
+ while (Buffer.byteLength(body) > maxBytes && list.length) {
257
+ list = list.slice(0, Math.floor(list.length * 0.9))
258
+ body = JSON.stringify({ ...r, conversions: list })
259
+ }
260
+ return body
261
+ }