@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
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
|
+
}
|