@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.
- package/LICENSE +48 -0
- package/README.md +235 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +351 -0
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- package/src/gateway/connectors/zed.ts +29 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- 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
|
+
}
|