@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
package/src/org/cli.ts ADDED
@@ -0,0 +1,438 @@
1
+ // `brain org ...`: log in to an organisation's brain server, see who you are, move your own memories between scopes, and (for admins)
2
+ // run and manage the server. Plain output; errors say what to do next. A token is printed only right after it is created.
3
+ import { existsSync, readFileSync, realpathSync } from "node:fs"
4
+ import { randomUUID } from "node:crypto"
5
+ import { resolve as resolvePath } from "node:path"
6
+ import { gitInfo } from "../fsutil.ts"
7
+ import { projectKey } from "../memory/project.ts"
8
+ import type { ProjectKey } from "../memory/types.ts"
9
+ import { OrgClient, OrgHttpError, checkServerUrl, isLoopbackHost } from "./client.ts"
10
+ import { openInBrowser, ssoLoginFlow } from "./oidc.ts"
11
+ import { adminOps, keyForNewDatabase } from "./admin-ops.ts"
12
+ import { dbState, resolveKey } from "./crypto.ts"
13
+ import { clearOrgConfig, orgConfigPath, readOrgConfig, writeOrgConfig } from "./config.ts"
14
+ import { SCOPES, type OrgConfig, type Role, type Scope } from "./types.ts"
15
+
16
+ const USAGE = `usage: brain org <command>
17
+ login --server <url> --token <t> [--ca <pem of a private certificate authority>] [--default-scope private|project|team|org] (or BRAIN_ORG_TOKEN=<t>) | logout | whoami | status
18
+ promote (--ids a,b | --source <session id>) --to project|team|org [--team id] [--project p]
19
+ demote (--ids a,b | --source <session id>) --to private|project|team [--team id] [--project p]
20
+ forget (--mine | --ids a,b) [--source id] [--project p]
21
+ admin init --db <path> --org-id <id> --org-name <name> --admin-id <id> --admin-name <name> [--encrypt [--key-file <path>]]
22
+ admin serve --db <path> [--port 4500 (or $PORT)] [--host 127.0.0.1] [--allow-remote] [--trust-proxy] [--key-file <path>] [--tls-cert <pem> --tls-key <pem>] (key: BRAIN_ORG_KEY or --key-file)
23
+ admin encrypt --db <path> [--key-file <path>] [--remove-backup] | admin rekey --db <path> --key-file <current> --new-key-file <new>
24
+ admin backup --db <path> --out <file> [--key-file <path>] | admin restore --from <backup> --db <path> [--key-file <path>] [--force]
25
+ admin user add --id <id> --name <name> [--role viewer|editor|admin] [--teams a,b] | list | disable <id>
26
+ admin token create --user <id> [--expires-days n] | list | revoke <id>
27
+ admin team add --id <id> --name <name> --members a,b
28
+ admin project members --project <id> [--add users] [--add-teams t] [--remove users] [--remove-teams t]
29
+ admin policy get | set --file policy.json
30
+ login --server <url> --sso [--no-open] sign in through the organisation's identity provider (browser)
31
+ admin sso set --issuer <url> --client-id <id> [--client-secret-env VAR] --domains a.com,b.com --default-role viewer|editor|admin|none
32
+ [--role-claim groups --role-map "eng=editor,platform=admin"] [--token-days 30] [--token-auth basic|post] | get | disable
33
+ admin audit [--since <iso date>] [--limit n] | admin retention`
34
+
35
+ const BOOLEAN = new Set(["mine", "allow-remote", "trust-proxy", "sso", "no-open", "encrypt", "remove-backup", "force"])
36
+
37
+ interface Parsed {
38
+ flags: Record<string, string>
39
+ bools: Set<string>
40
+ pos: string[]
41
+ }
42
+
43
+ function parse(args: string[]): Parsed {
44
+ const flags: Record<string, string> = {}
45
+ const bools = new Set<string>()
46
+ const pos: string[] = []
47
+ for (let i = 0; i < args.length; i++) {
48
+ const a = args[i]!
49
+ if (!a.startsWith("--")) pos.push(a)
50
+ else if (BOOLEAN.has(a.slice(2))) bools.add(a.slice(2))
51
+ else if (i + 1 < args.length) flags[a.slice(2)] = args[++i]!
52
+ else flags[a.slice(2)] = ""
53
+ }
54
+ return { flags, bools, pos }
55
+ }
56
+
57
+ const list = (s: string | undefined): string[] => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean)
58
+
59
+ export interface OrgIo {
60
+ out(line: string): void
61
+ /** For tests: replaces the server functions (otherwise loaded from src/org/db.ts, auth.ts, server.ts). */
62
+ deps?: {
63
+ initOrg?: (dbPath: string, org: { id: string; name: string }, admin: { id: string; name: string }, opts?: { key?: string }) => { token: string }
64
+ startOrgServer?: (o: { dbPath: string; port?: number; host?: string; trustProxy?: boolean; key?: string; tlsCertFile?: string; tlsKeyFile?: string }) => Promise<{ url: string; port: number; stop(): Promise<void> }>
65
+ /** For tests: replaces the browser launch of `login --sso`. */
66
+ openBrowser?: (url: string) => void
67
+ }
68
+ /** For tests: `admin serve` returns (after stopping the server) when this resolves, instead of waiting for Ctrl-C. */
69
+ stopWhen?: Promise<void>
70
+ }
71
+
72
+ async function serverFn<T>(name: "initOrg" | "startOrgServer"): Promise<T> {
73
+ for (const f of ["./db.ts", "./auth.ts", "./server.ts"]) {
74
+ try {
75
+ const mod = (await import(f)) as Record<string, unknown>
76
+ if (typeof mod[name] === "function") return mod[name] as T
77
+ } catch {}
78
+ }
79
+ throw new Error(`the organisation server code (${name}) is not available in this build`)
80
+ }
81
+
82
+ function hint(e: OrgHttpError): string {
83
+ if (e.status === 401) return "your token was rejected (wrong, revoked, expired or the user is disabled): ask an admin for a new one and run `brain org login`"
84
+ if (e.status === 403) return "your role does not allow this"
85
+ if (e.status === 429) return "too many requests: wait a minute and try again"
86
+ if (e.code === "unreachable" || e.code === "timeout") return "check the server address (`brain org status`) and your network"
87
+ if (e.code === "insecure_transport") return ""
88
+ return ""
89
+ }
90
+
91
+ function currentProject(): ProjectKey {
92
+ const cwd = (() => {
93
+ try {
94
+ return realpathSync(process.cwd())
95
+ } catch {
96
+ return process.cwd()
97
+ }
98
+ })()
99
+ return projectKey({ cwd, ...gitInfo(cwd) })
100
+ }
101
+
102
+ export async function orgCommand(args: string[], home: string, io: OrgIo = { out: console.log }): Promise<number> {
103
+ const out = io.out
104
+ const [sub, ...rest] = args
105
+ const p = parse(rest)
106
+ const fail = (msg: string, code = 1): number => (out(msg), code)
107
+
108
+ try {
109
+ if (!sub || sub === "help" || sub === "--help") return fail(USAGE, sub ? 0 : 2)
110
+
111
+ if (sub === "login") {
112
+ const server = p.flags.server
113
+ if (p.bools.has("sso")) {
114
+ if (!server) return fail("usage: brain org login --server <url> --sso [--no-open] [--default-scope private|project|team|org]", 2)
115
+ const ds = p.flags["default-scope"]
116
+ if (ds !== undefined && !(SCOPES as string[]).includes(ds)) return fail(`--default-scope must be one of ${SCOPES.join(", ")}`, 2)
117
+ const base = checkServerUrl(server)
118
+ const probe = { server: base, token: randomUUID(), ...(p.flags.ca ? { ca: resolvePath(p.flags.ca) } : {}) } as OrgConfig // never sent: the sign-in calls carry no token
119
+ const anon = new OrgClient(probe)
120
+ const got = await ssoLoginFlow({ config: () => anon.ssoConfig(), exchange: (r) => anon.ssoExchange(r), open: p.bools.has("no-open") ? undefined : (io.deps?.openBrowser ?? openInBrowser), print: out })
121
+ const cfg: OrgConfig = { server: base, token: got.token, ...(p.flags.ca ? { ca: resolvePath(p.flags.ca) } : {}), ...(ds ? { defaultScope: ds as Scope } : {}) }
122
+ const who = await new OrgClient(cfg).whoami()
123
+ writeOrgConfig(home, cfg)
124
+ out(`logged in to ${cfg.server} with single sign-on`)
125
+ out(`organisation: ${who.org.name} (${who.org.id})`)
126
+ out(`you are ${who.user.id} (${who.user.name}), role ${who.user.role}${who.user.teams.length ? `, teams ${who.user.teams.join(", ")}` : ""}`)
127
+ out(`this sign-in is valid until ${got.expiresAt}; run \`brain org login --sso\` again then`)
128
+ out(`saved to ${orgConfigPath(home)} (readable only by you)`)
129
+ return 0
130
+ }
131
+ const token = p.flags.token ?? process.env.BRAIN_ORG_TOKEN
132
+ if (!server || !token) return fail("usage: brain org login --server <url> --token <token> [--default-scope private|project|team|org]\n(or pass the token in BRAIN_ORG_TOKEN to keep it out of your shell history)", 2)
133
+ const ds = p.flags["default-scope"]
134
+ if (ds !== undefined && !(SCOPES as string[]).includes(ds)) return fail(`--default-scope must be one of ${SCOPES.join(", ")}`, 2)
135
+ const cfg: OrgConfig = { server: checkServerUrl(server), token, ...(p.flags.ca ? { ca: resolvePath(p.flags.ca) } : {}), ...(ds ? { defaultScope: ds as Scope } : {}) }
136
+ const who = await new OrgClient(cfg).whoami()
137
+ writeOrgConfig(home, cfg)
138
+ out(`logged in to ${cfg.server}`)
139
+ out(`organisation: ${who.org.name} (${who.org.id})`)
140
+ out(`you are ${who.user.id} (${who.user.name}), role ${who.user.role}${who.user.teams.length ? `, teams ${who.user.teams.join(", ")}` : ""}`)
141
+ out(`new memories default to scope ${cfg.defaultScope ?? who.policy.defaultScope}; nothing is shared wider unless you promote it`)
142
+ out(`saved to ${orgConfigPath(home)} (readable only by you)`)
143
+ return 0
144
+ }
145
+
146
+ if (sub === "logout") {
147
+ out(clearOrgConfig(home) ? "logged out: this machine uses its local memory again (the token is still valid on the server; ask an admin to revoke it if the machine is lost)" : "not logged in")
148
+ return 0
149
+ }
150
+
151
+ if (sub === "admin") {
152
+ const local = await orgAdminLocal(args, io)
153
+ if (local !== undefined) return local
154
+ }
155
+ const cfg = readOrgConfig(home)
156
+ if (sub === "status" && !cfg) return out("organisation: not configured (memory is local to this machine). Join one with `brain org login`."), 0
157
+ if (!cfg) return fail("not logged in: run `brain org login --server <url> --token <token>` first", 1)
158
+ const client = new OrgClient(cfg)
159
+
160
+ if (sub === "whoami" || sub === "status") {
161
+ const who = await client.whoami()
162
+ out(`server: ${cfg.server}`)
163
+ out(`organisation: ${who.org.name} (${who.org.id})`)
164
+ out(`user: ${who.user.id} (${who.user.name}), role ${who.user.role}${who.user.teams.length ? `, teams ${who.user.teams.join(", ")}` : ""}`)
165
+ const pol = who.policy
166
+ out(`policy: widest scope ${pol.maxScope}, default scope ${cfg.defaultScope ?? pol.defaultScope}${pol.retentionDays ? `, kept ${pol.retentionDays} days` : ""}${pol.allowedKinds ? `, kinds ${pol.allowedKinds.join("/")}` : ""}, ${pol.denyPaths.length} denied paths, ${pol.denyTerms.length} denied terms`)
167
+ if (sub === "status") {
168
+ const projects = await client.projects()
169
+ out(`you can see ${projects.reduce((n, x) => n + x.items, 0)} items in ${projects.length} projects`)
170
+ for (const x of projects) out(` ${x.label} ${x.items} items (${x.id})`)
171
+ }
172
+ return 0
173
+ }
174
+
175
+ if (sub === "promote" || sub === "demote") {
176
+ const to = p.flags.to
177
+ const allowed = sub === "promote" ? ["project", "team", "org"] : ["private", "project", "team"]
178
+ if (!to || !allowed.includes(to) || (!p.flags.ids && !p.flags.source)) return fail(`usage: brain org ${sub} (--ids a,b | --source <session id>) --to ${allowed.join("|")} [--team id] [--project p]`, 2)
179
+ if (to === "team" && !p.flags.team) return fail("scope team needs --team <team id>", 2)
180
+ const project = p.flags.project ? { id: p.flags.project, label: p.flags.project } : currentProject()
181
+ const ids = list(p.flags.ids)
182
+ const req = ids.length ? { ids } : { source: p.flags.source!, project }
183
+ const changed = await client.promote({ ...req, to: to as Scope, ...(p.flags.team ? { team: p.flags.team } : {}) })
184
+ out(`${sub === "promote" ? "promoted" : "demoted"} ${changed} items${ids.length ? "" : ` from ${p.flags.source} in ${project.label}`} to ${to}${p.flags.team ? ` (team ${p.flags.team})` : ""}`)
185
+ return 0
186
+ }
187
+ if (sub === "forget") {
188
+ if (!p.bools.has("mine") && !p.flags.ids) return fail("usage: brain org forget (--mine | --ids a,b) [--source id] [--project p]\n--mine removes your own memories (of the current project, or --project); --ids removes those items (admins: any item)", 2)
189
+ const project = p.flags.project ? { id: p.flags.project, label: p.flags.project } : p.flags.ids ? undefined : currentProject()
190
+ const removed = await client.forget({ ...(project ? { project } : {}), ...(p.flags.source ? { source: p.flags.source } : {}), ...(p.flags.ids ? { ids: list(p.flags.ids) } : {}), mine: p.bools.has("mine") })
191
+ out(`forgot ${removed} items`)
192
+ return 0
193
+ }
194
+
195
+ if (sub === "admin") return await admin(rest, home, io, cfg, client)
196
+
197
+ return fail(USAGE, 2)
198
+ } catch (e) {
199
+ if (e instanceof OrgHttpError) {
200
+ out(`error: ${e.message}${e.code && e.status ? ` (${e.code})` : ""}`)
201
+ const h = hint(e)
202
+ if (h) out(h)
203
+ return 1
204
+ }
205
+ out(`error: ${(e as Error).message}`)
206
+ return 1
207
+ }
208
+ }
209
+
210
+ async function admin(args: string[], home: string, io: OrgIo, cfg: OrgConfig, client: OrgClient): Promise<number> {
211
+ const out = io.out
212
+ const [what, verb, ...rest] = args
213
+ const p = parse(rest)
214
+ const id = (): string | undefined => p.flags.id ?? p.pos[0]
215
+ const usage = (s: string): number => (out(`usage: brain org admin ${s}`), 2)
216
+
217
+ if (what === "user") {
218
+ if (verb === "add") {
219
+ const role = (p.flags.role ?? "editor") as Role
220
+ if (!p.flags.id || !p.flags.name) return usage("user add --id <id> --name <name> [--role viewer|editor|admin] [--teams a,b]")
221
+ if (!["viewer", "editor", "admin"].includes(role)) return usage("user add: --role must be viewer, editor or admin")
222
+ const u = await client.addUser({ id: p.flags.id, name: p.flags.name, role, ...(p.flags.teams ? { teams: list(p.flags.teams) } : {}) })
223
+ out(`added ${u.id} (${u.name}) as ${u.role}; create a token with: brain org admin token create --user ${u.id}`)
224
+ return 0
225
+ }
226
+ if (verb === "list") {
227
+ const users = await client.listUsers()
228
+ out(`${users.length} users`)
229
+ for (const u of users) out(` ${u.id} ${u.name} ${u.role}${u.teams.length ? ` teams ${u.teams.join(",")}` : ""}${u.disabled ? " DISABLED" : ""}`)
230
+ return 0
231
+ }
232
+ if (verb === "disable") {
233
+ if (!id()) return usage("user disable <id>")
234
+ await client.disableUser(id()!)
235
+ out(`disabled ${id()}: every token of that user is rejected from now on`)
236
+ return 0
237
+ }
238
+ return usage("user add|list|disable")
239
+ }
240
+
241
+ if (what === "token") {
242
+ if (verb === "create") {
243
+ const user = p.flags.user ?? p.pos[0]
244
+ if (!user) return usage("token create --user <id> [--expires-days n]")
245
+ const days = p.flags["expires-days"] ? Number(p.flags["expires-days"]) : undefined
246
+ if (days !== undefined && !(Number.isInteger(days) && days > 0)) return usage("token create: --expires-days must be a whole number above 0")
247
+ const t = await client.createToken(user, days)
248
+ out(`token ${t.id} for ${user}${days ? ` (expires in ${days} days)` : " (does not expire)"}:`)
249
+ out(t.token)
250
+ out("This is the ONLY time the token is shown: the server keeps just a hash. Give it to the user over a private channel; they run `brain org login --server <url> --token <it>`.")
251
+ return 0
252
+ }
253
+ if (verb === "list") {
254
+ const toks = await client.listTokens()
255
+ out(`${toks.length} tokens`)
256
+ for (const t of toks) out(` ${Object.entries(t).map(([k, v]) => `${k}=${v === null || v === undefined || v === "" ? "-" : String(v)}`).join(" ")}`)
257
+ return 0
258
+ }
259
+ if (verb === "revoke") {
260
+ if (!id()) return usage("token revoke <id>")
261
+ await client.revokeToken(id()!)
262
+ out(`revoked ${id()}: it is rejected from now on`)
263
+ return 0
264
+ }
265
+ return usage("token create|list|revoke")
266
+ }
267
+
268
+ if (what === "team" && verb === "add") {
269
+ if (!p.flags.id || !p.flags.name) return usage("team add --id <id> --name <name> --members a,b")
270
+ await client.addTeam({ id: p.flags.id, name: p.flags.name, members: list(p.flags.members) })
271
+ out(`team ${p.flags.id} saved with ${list(p.flags.members).length} members`)
272
+ return 0
273
+ }
274
+
275
+ if (what === "project" && verb === "members") {
276
+ const q = p
277
+ const project = q.flags.project ?? q.pos[0]
278
+ const grp = (users?: string, teams?: string) => (users || teams ? { ...(users ? { users: list(users) } : {}), ...(teams ? { teams: list(teams) } : {}) } : undefined)
279
+ const add = grp(q.flags.add ?? q.flags["add-users"], q.flags["add-teams"])
280
+ const remove = grp(q.flags.remove ?? q.flags["remove-users"], q.flags["remove-teams"])
281
+ if (!project || (!add && !remove)) return usage("project members --project <id> [--add users | --add-users u1,u2 | --add-teams t1] [--remove ... likewise]")
282
+ await client.projectMembers(project, { ...(add ? { add } : {}), ...(remove ? { remove } : {}) })
283
+ const show = (g?: { users?: string[]; teams?: string[] }) => [...(g?.users ?? []), ...(g?.teams ?? []).map((t) => `team ${t}`)].join(", ")
284
+ out(`updated members of ${project}${add ? `; added ${show(add)}` : ""}${remove ? `; removed ${show(remove)}` : ""}`)
285
+ return 0
286
+ }
287
+
288
+ if (what === "policy") {
289
+ if (verb === "get") {
290
+ out(JSON.stringify(await client.getPolicy(), null, 2))
291
+ return 0
292
+ }
293
+ if (verb === "set") {
294
+ if (!p.flags.file) return usage("policy set --file policy.json")
295
+ const file = resolvePath(p.flags.file)
296
+ if (!existsSync(file)) return (out(`no such file: ${file}`), 1)
297
+ let raw: unknown
298
+ try {
299
+ raw = JSON.parse(readFileSync(file, "utf8"))
300
+ } catch (e) {
301
+ return (out(`${file} is not valid JSON: ${(e as Error).message}`), 1)
302
+ }
303
+ try {
304
+ const mod = (await import("./policy.ts")) as { normalizePolicy?: (r: unknown) => unknown }
305
+ raw = mod.normalizePolicy ? mod.normalizePolicy(raw) : raw
306
+ } catch (e) {
307
+ if (!/Cannot find module|not found/i.test((e as Error).message)) return (out(`policy rejected: ${(e as Error).message}`), 1)
308
+ }
309
+ await client.setPolicy(raw)
310
+ out("policy saved; it applies from the next request of every user")
311
+ return 0
312
+ }
313
+ return usage("policy get | set --file policy.json")
314
+ }
315
+
316
+ if (what === "sso") {
317
+ if (verb === "get") {
318
+ const c = await client.getSso()
319
+ if (!c.configured) return (out("single sign-on is not configured; set it up with `brain org admin sso set --issuer ... --client-id ... --domains a.com`"), 0)
320
+ out(JSON.stringify(c, null, 2))
321
+ return 0
322
+ }
323
+ if (verb === "disable") {
324
+ const c = await client.getSso()
325
+ if (!c.configured) return (out("single sign-on is not configured"), 0)
326
+ await client.setSso({ ...c, configured: undefined, clientSecretSet: undefined, enabled: false })
327
+ out("single sign-on disabled: no new sign-ins; tokens already issued keep working until they expire (revoke them with `admin token revoke`)")
328
+ return 0
329
+ }
330
+ if (verb === "set") {
331
+ const f = p.flags
332
+ const bad = (m: string): number => (out(`admin sso set: ${m}`), 2)
333
+ if (!f.issuer || !f["client-id"] || !f.domains) return usage("sso set --issuer <url> --client-id <id> [--client-secret-env VAR] --domains a.com,b.com --default-role viewer|editor|admin|none [--role-claim groups --role-map \"eng=editor,platform=admin\"] [--token-days 30] [--scopes \"openid email profile\"]")
334
+ const body: Record<string, unknown> = { issuer: f.issuer, clientId: f["client-id"], allowedDomains: list(f.domains), defaultRole: f["default-role"] ?? "viewer" }
335
+ if (f["client-secret-env"]) {
336
+ const v = process.env[f["client-secret-env"]]
337
+ if (!v) return bad(`the environment variable ${f["client-secret-env"]} is empty or not set (the secret is read from the environment so it stays out of your shell history)`)
338
+ body.clientSecret = v
339
+ }
340
+ if (f["role-claim"] || f["role-map"]) {
341
+ if (!f["role-claim"] || !f["role-map"]) return bad("--role-claim and --role-map go together")
342
+ const map: Record<string, string> = {}
343
+ for (const pair of list(f["role-map"])) {
344
+ const i = pair.lastIndexOf("=")
345
+ if (i < 1) return bad(`--role-map entries look like group=role, got "${pair}"`)
346
+ map[pair.slice(0, i)] = pair.slice(i + 1)
347
+ }
348
+ body.roleMap = { claim: f["role-claim"], map }
349
+ }
350
+ if (f["token-days"]) body.tokenDays = Number(f["token-days"])
351
+ if (f["token-auth"]) {
352
+ const m = f["token-auth"] === "basic" ? "client_secret_basic" : f["token-auth"] === "post" ? "client_secret_post" : undefined
353
+ if (!m) return bad("--token-auth must be basic (the OAuth default) or post")
354
+ body.tokenAuth = m
355
+ }
356
+ if (f.scopes) body.scopes = f.scopes
357
+ const r = await client.setSso(body)
358
+ out(`single sign-on ${r.enabled ? "enabled" : "saved (disabled)"} for ${String(r.issuer)}; people sign in with: brain org login --server <url> --sso`)
359
+ return 0
360
+ }
361
+ return usage("sso set|get|disable")
362
+ }
363
+
364
+ if (what === "audit") {
365
+ const q = parse(args.slice(1))
366
+ const limit = q.flags.limit ? Number(q.flags.limit) : 100
367
+ const events = await client.audit({ ...(q.flags.since ? { since: q.flags.since } : {}), limit })
368
+ out(`${events.length} events`)
369
+ for (const e of events) out(`${e.at} ${e.actor} ${e.action}${e.project ? ` ${e.project}` : ""}${e.scope ? ` ${e.scope}` : ""}${e.count !== undefined ? ` x${e.count}` : ""}${e.detail ? ` ${e.detail}` : ""}`)
370
+ return 0
371
+ }
372
+
373
+ if (what === "retention") {
374
+ const r = await client.runRetention()
375
+ out(`retention run done${r && typeof r === "object" ? `: ${JSON.stringify(r)}` : ""}`)
376
+ return 0
377
+ }
378
+
379
+ // init and serve are local: they need no login (the server and its database are on this machine).
380
+ return usage("user|token|team|project|policy|audit|retention ...")
381
+ }
382
+
383
+ /** `brain org admin init|serve`: run on the server's machine, no login needed. Called by orgCommand before it asks for a login. */
384
+ export async function orgAdminLocal(args: string[], io: OrgIo): Promise<number | undefined> {
385
+ const [admin, verb, ...rest] = args
386
+ if (admin !== "admin" || !verb || !["init", "serve", "encrypt", "rekey", "backup", "restore"].includes(verb)) return undefined
387
+ const out = io.out
388
+ const p = parse(rest)
389
+ if (verb === "encrypt" || verb === "rekey" || verb === "backup" || verb === "restore") return await adminOps(verb, p, out)
390
+ if (verb === "init") {
391
+ const f = p.flags
392
+ if (!f.db || !f["org-id"] || !f["org-name"] || !f["admin-id"] || !f["admin-name"]) return (out("usage: brain org admin init --db <path> --org-id <id> --org-name <name> --admin-id <id> --admin-name <name>"), 2)
393
+ const db = resolvePath(f.db)
394
+ if (existsSync(db)) return (out(`${db} already exists; refusing to overwrite an organisation database. Pick a new --db path, or delete it if you really want a fresh start.`), 1)
395
+ try {
396
+ const initOrg = io.deps?.initOrg ?? (await serverFn<NonNullable<NonNullable<OrgIo["deps"]>["initOrg"]>>("initOrg"))
397
+ const key = p.bools.has("encrypt") ? keyForNewDatabase(db, f["key-file"], out) : undefined // says where the key file is, never the key
398
+ const { token } = key ? initOrg(db, { id: f["org-id"], name: f["org-name"] }, { id: f["admin-id"], name: f["admin-name"] }, { key }) : initOrg(db, { id: f["org-id"], name: f["org-name"] }, { id: f["admin-id"], name: f["admin-name"] })
399
+ out(`organisation ${f["org-name"]} created in ${db}${key ? " (encrypted at rest)" : ""}; first admin: ${f["admin-id"]}`)
400
+ out("admin token (shown ONCE, the database keeps only a hash; store it in a password manager):")
401
+ out(token)
402
+ // An encrypted database needs its key file again to start: say so (the path is the one printed above, not the key).
403
+ const keyHint = key ? ` --key-file ${resolvePath(f["key-file"] ?? `${db}.key`)}` : ""
404
+ out(`next: brain org admin serve --db ${db}${keyHint} then brain org login --server http://127.0.0.1:4500 --token <the token above>`)
405
+ return 0
406
+ } catch (e) {
407
+ return (out(`error: ${(e as Error).message}`), 1)
408
+ }
409
+ }
410
+ // serve
411
+ if (!p.flags.db) return (out("usage: brain org admin serve --db <path> [--port 4500 (or $PORT)] [--host 127.0.0.1] [--allow-remote] [--trust-proxy]"), 2)
412
+ const host = p.flags.host ?? "127.0.0.1"
413
+ // A cloud host tells the process its port in $PORT.
414
+ const port = p.flags.port !== undefined ? Number(p.flags.port) : process.env.PORT ? Number(process.env.PORT) : 4500
415
+ if (!Number.isInteger(port) || port < 0 || port > 65535) return (out("--port must be a number from 0 to 65535"), 2)
416
+ const tlsCert = p.flags["tls-cert"]
417
+ const tlsKey = p.flags["tls-key"]
418
+ if (!tlsCert !== !tlsKey) return (out("TLS needs both --tls-cert <pem> and --tls-key <pem>"), 2)
419
+ if (!isLoopbackHost(host) && !p.bools.has("allow-remote")) return (out(tlsCert ? `refusing to listen on ${host} without --allow-remote (you asked for a non-loopback address: pass --allow-remote to confirm).` : `refusing to listen on ${host}: the server speaks plain HTTP and tokens would cross the network in clear text.\nUse --tls-cert/--tls-key so the server terminates TLS itself, or keep the default 127.0.0.1 and put a TLS-terminating proxy (nginx, Caddy, a cloud load balancer) in front, or pass --allow-remote if you already are behind one.`), 1)
420
+ const db = resolvePath(p.flags.db)
421
+ if (!existsSync(db)) return (out(`no database at ${db}; create it with: brain org admin init --db ${db} ...`), 1)
422
+ try {
423
+ const start = io.deps?.startOrgServer ?? (await serverFn<NonNullable<NonNullable<OrgIo["deps"]>["startOrgServer"]>>("startOrgServer"))
424
+ // the key comes from --key-file or BRAIN_ORG_KEY (never a command-line value); an unusable one is an error here, not a silent plain start
425
+ const k = resolveKey({ keyFile: p.flags["key-file"] ? resolvePath(p.flags["key-file"]) : undefined })
426
+ if (k.warning) out(`warning: ${k.warning}`)
427
+ if (!k.key && dbState(db) === "plain") out(`warning: the database is not encrypted at rest: anyone who can read ${db} can read every item. Convert it with: brain org admin encrypt --db ${db}`)
428
+ const srv = await start({ dbPath: db, port, host, ...(p.bools.has("trust-proxy") ? { trustProxy: true } : {}), ...(k.key ? { key: k.key } : {}), ...(tlsCert && tlsKey ? { tlsCertFile: resolvePath(tlsCert), tlsKeyFile: resolvePath(tlsKey) } : {}) })
429
+ out(`brain org server listening on ${srv.url} (database ${db}${k.key ? ", encrypted" : ""}); Ctrl-C to stop`)
430
+ if (!isLoopbackHost(host) && !tlsCert) out("warning: not loopback and plain HTTP: make sure TLS is terminated in front of this port")
431
+ await (io.stopWhen ?? new Promise<void>((res) => { process.once("SIGINT", () => res()); process.once("SIGTERM", () => res()) }))
432
+ await srv.stop()
433
+ out("stopped")
434
+ return 0
435
+ } catch (e) {
436
+ return (out(`error: ${(e as Error).message}`), 1)
437
+ }
438
+ }