ex-brain 0.2.7 → 0.3.0

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.
@@ -0,0 +1,190 @@
1
+ import { Command } from "commander";
2
+ import { addDryRun, isDryRun, withRepo, isJson, print } from "./shared";
3
+ import { readMaybeStdin } from "../markdown/io";
4
+
5
+ export function registerTagCommand(program: Command): void {
6
+ const tagCmd = program
7
+ .command("tag")
8
+ .description("manage tags on a page");
9
+
10
+ tagCmd
11
+ .command("list")
12
+ .argument("<slug>", "page slug")
13
+ .description("list tags on a page")
14
+ .addHelpText("after", `
15
+ Examples:
16
+ ebrain tag list docs/api
17
+ `)
18
+ .action(async (slug: string) => {
19
+ await withRepo(program, async (repo) => {
20
+ const tags = await repo.tags(slug);
21
+ print(program, tags);
22
+ });
23
+ });
24
+
25
+ addDryRun(
26
+ tagCmd
27
+ .command("add")
28
+ .argument("<slug>", "page slug")
29
+ .argument("<tag>", "tag to add")
30
+ .description("add a tag to a page (idempotent)")
31
+ .addHelpText("after", `
32
+ Examples:
33
+ ebrain tag add docs/api rest
34
+ ebrain tag add docs/api rest --dry-run
35
+ `),
36
+ ).action(async (slug: string, tag: string, opts: { dryRun?: boolean }) => {
37
+ if (isDryRun(opts)) {
38
+ print(program, { dryRun: true, action: "tag-add", slug, tag });
39
+ return;
40
+ }
41
+ await withRepo(program, async (repo) => {
42
+ await repo.tag(slug, tag);
43
+ print(program, { ok: true, action: "tag-add", slug, tag });
44
+ });
45
+ });
46
+
47
+ addDryRun(
48
+ tagCmd
49
+ .command("remove")
50
+ .argument("<slug>", "page slug")
51
+ .argument("<tag>", "tag to remove")
52
+ .description("remove a tag from a page")
53
+ .addHelpText("after", `
54
+ Examples:
55
+ ebrain tag remove docs/api outdated
56
+ ebrain tag remove docs/api outdated --dry-run
57
+ `),
58
+ ).action(async (slug: string, tag: string, opts: { dryRun?: boolean }) => {
59
+ if (isDryRun(opts)) {
60
+ print(program, { dryRun: true, action: "tag-remove", slug, tag });
61
+ return;
62
+ }
63
+ await withRepo(program, async (repo) => {
64
+ await repo.untag(slug, tag);
65
+ print(program, { ok: true, action: "tag-remove", slug, tag });
66
+ });
67
+ });
68
+ }
69
+
70
+ export function registerRawCommand(program: Command): void {
71
+ const rawCmd = program
72
+ .command("raw")
73
+ .description("manage raw source data for a page");
74
+
75
+ rawCmd
76
+ .command("get")
77
+ .argument("<slug>", "page slug")
78
+ .option("--source <source>", "filter by source name")
79
+ .description("read raw source data for a page")
80
+ .addHelpText("after", `
81
+ Examples:
82
+ ebrain raw get ingest/report
83
+ ebrain raw get ingest/report --source crm
84
+ `)
85
+ .action(async (slug: string, opts: { source?: string }) => {
86
+ await withRepo(program, async (repo) => {
87
+ const rows = await repo.readRaw(slug, opts.source);
88
+ print(program, rows);
89
+ });
90
+ });
91
+
92
+ addDryRun(
93
+ rawCmd
94
+ .command("set")
95
+ .argument("<slug>", "page slug")
96
+ .requiredOption("--source <source>", "source name")
97
+ .option("--data <json>", "JSON string")
98
+ .option("--stdin", "read JSON from stdin", false)
99
+ .description("write raw source data for a page")
100
+ .addHelpText("after", `
101
+ Examples:
102
+ ebrain raw set ingest/report --source crm --data '{"rev": 1000}'
103
+ echo '{"rev": 1000}' | ebrain raw set ingest/report --source crm --stdin
104
+ ebrain raw set ingest/report --source crm --data '{"rev": 1000}' --dry-run
105
+ `),
106
+ ).action(async (slug: string, opts: {
107
+ source: string;
108
+ data?: string;
109
+ stdin?: boolean;
110
+ dryRun?: boolean;
111
+ }) => {
112
+ let data: unknown;
113
+ if (opts.data) {
114
+ data = JSON.parse(opts.data);
115
+ } else if (opts.stdin) {
116
+ const raw = await readMaybeStdin();
117
+ if (!raw?.trim()) throw new Error("empty stdin - pipe JSON");
118
+ data = JSON.parse(raw);
119
+ } else {
120
+ throw new Error("provide --data <json> or --stdin");
121
+ }
122
+
123
+ if (isDryRun(opts)) {
124
+ print(program, {
125
+ dryRun: true,
126
+ action: "raw-set",
127
+ slug,
128
+ source: opts.source,
129
+ });
130
+ return;
131
+ }
132
+
133
+ await withRepo(program, async (repo) => {
134
+ await repo.writeRaw(slug, opts.source, data);
135
+ print(program, {
136
+ ok: true,
137
+ action: "raw-set",
138
+ slug,
139
+ source: opts.source,
140
+ });
141
+ });
142
+ });
143
+ }
144
+
145
+ export function registerLinkCommand(program: Command): void {
146
+ addDryRun(
147
+ program
148
+ .command("link")
149
+ .argument("<from>", "source page slug")
150
+ .argument("<to>", "target page slug")
151
+ .option("--context <text>", "link context", "")
152
+ .description("create a cross-link between pages (idempotent)")
153
+ .addHelpText("after", `
154
+ Examples:
155
+ ebrain link docs/api docs/getting-started
156
+ ebrain link people/john projects/alpha --context "lead"
157
+ ebrain link docs/api docs/getting-started --dry-run
158
+ `),
159
+ ).action(async (from: string, to: string, opts: { context?: string; dryRun?: boolean }) => {
160
+ if (isDryRun(opts)) {
161
+ print(program, {
162
+ dryRun: true,
163
+ action: "link",
164
+ from,
165
+ to,
166
+ context: opts.context ?? "",
167
+ });
168
+ return;
169
+ }
170
+ await withRepo(program, async (repo) => {
171
+ await repo.link(from, to, opts.context ?? "");
172
+ print(program, { ok: true, from, to });
173
+ });
174
+ });
175
+
176
+ program
177
+ .command("backlinks")
178
+ .argument("<slug>", "target page slug")
179
+ .description("list pages that link to this page")
180
+ .addHelpText("after", `
181
+ Examples:
182
+ ebrain backlinks docs/api
183
+ `)
184
+ .action(async (slug: string) => {
185
+ await withRepo(program, async (repo) => {
186
+ const links = await repo.backlinks(slug);
187
+ print(program, links);
188
+ });
189
+ });
190
+ }
@@ -0,0 +1,252 @@
1
+ import { resolve } from "node:path";
2
+ import { Command } from "commander";
3
+ import { withRepo, isJson, print, addDryRun, isDryRun } from "./shared";
4
+ import { success, warning, header, keyValue, separator, info, subItem, createSpinner } from "../utils/cli-output";
5
+ import { formatDuration } from "../utils/progress";
6
+ import { loadSettings, SETTINGS_PATH } from "../settings";
7
+ import { BrainDb } from "../db/client";
8
+ import { fileExists, ensureDir, slugToPath, writeTextFile } from "../markdown/io";
9
+ import { renderPageMarkdown } from "../markdown/parser";
10
+
11
+ export function registerExportCommand(program: Command): void {
12
+ program
13
+ .command("export")
14
+ .option("--dir <dir>", "output directory", resolve(process.cwd(), "export"))
15
+ .description("export all pages as markdown files")
16
+ .addHelpText("after", `
17
+ Examples:
18
+ ebrain export
19
+ ebrain export --dir ./backup
20
+ `)
21
+ .action(async (opts: { dir: string }) => {
22
+ await withRepo(program, async (repo) => {
23
+ const dir = resolve(opts.dir);
24
+ await ensureDir(dir);
25
+ const pages = await repo.listPages({ limit: 100000 });
26
+ const jsonOut = isJson(program);
27
+ for (let i = 0; i < pages.length; i += 1) {
28
+ const page = pages[i]!;
29
+ if (!jsonOut) process.stderr.write(`[${i + 1}/${pages.length}] export ${page.slug}\n`);
30
+ const tags = await repo.tags(page.slug);
31
+ const fm = { ...page.frontmatter, type: page.type, title: page.title };
32
+ if (tags.length > 0) (fm as Record<string, unknown>).tags = tags;
33
+ const md = renderPageMarkdown(fm, page.compiledTruth, page.timeline);
34
+ await writeTextFile(slugToPath(page.slug, dir), md);
35
+ }
36
+ print(program, { exported: pages.length, dir });
37
+ });
38
+ });
39
+ }
40
+
41
+ export function registerEmbedCommand(program: Command): void {
42
+ addDryRun(
43
+ program
44
+ .command("embed")
45
+ .argument("[slug]", "page slug (omit with --all)")
46
+ .option("--all", "embed all pages")
47
+ .description("refresh page embedding(s)")
48
+ .addHelpText("after", `
49
+ Examples:
50
+ ebrain embed docs/api
51
+ ebrain embed --all
52
+ ebrain embed --all --dry-run
53
+ `),
54
+ ).action(async (slug: string | undefined, opts: { all?: boolean; dryRun?: boolean }) => {
55
+ if (opts.all) {
56
+ if (isDryRun(opts)) {
57
+ await withRepo(program, async (repo) => {
58
+ const pages = await repo.listPages({ limit: 100000 });
59
+ print(program, { dryRun: true, action: "embed", mode: "all", pagesFound: pages.length });
60
+ });
61
+ return;
62
+ }
63
+ await withRepo(program, async (repo) => {
64
+ const jsonOut = isJson(program);
65
+ const spinner = createSpinner();
66
+ const startTime = Date.now();
67
+ if (!jsonOut) {
68
+ header("Embed All Pages");
69
+ spinner.start(`Loading pages...`);
70
+ }
71
+ const pages = await repo.listPages({ limit: 100000 });
72
+ if (!jsonOut) spinner.update(`Embedding ${pages.length} pages...`);
73
+ const count = await repo.embedAll();
74
+ if (!jsonOut) {
75
+ const duration = formatDuration(Date.now() - startTime);
76
+ spinner.succeed(`Embedded ${count} pages`);
77
+ keyValue("Duration", duration);
78
+ }
79
+ print(program, { embedded: count, mode: "all" });
80
+ });
81
+ return;
82
+ }
83
+ if (!slug) throw new Error("provide <slug> or --all");
84
+ if (isDryRun(opts)) {
85
+ print(program, { dryRun: true, action: "embed", slug });
86
+ return;
87
+ }
88
+ await withRepo(program, async (repo) => {
89
+ const jsonOut = isJson(program);
90
+ const spinner = createSpinner();
91
+ if (!jsonOut) {
92
+ header(`Embed: ${slug}`);
93
+ spinner.start(`Generating embedding for page...`);
94
+ }
95
+ await repo.syncPageToSearch(slug);
96
+ if (!jsonOut) spinner.succeed(`Page embedded: ${slug}`);
97
+ print(program, { embedded: 1, slug });
98
+ });
99
+ });
100
+ }
101
+
102
+ export function registerInitCommand(program: Command): void {
103
+ program
104
+ .command("init")
105
+ .description("initialize ebrain: create config, database, and show setup guide")
106
+ .addHelpText("after", `
107
+ Examples:
108
+ ebrain init
109
+ ebrain init --db ./my.db
110
+ `)
111
+ .action(async () => {
112
+ const jsonOut = isJson(program);
113
+ const settings = await loadSettings();
114
+ const cliDb = program.opts().db;
115
+ const dbPath = cliDb ?? settings.dbPath;
116
+
117
+ if (!jsonOut) header("ebrain init");
118
+
119
+ const { createDefaultSettings } = await import("../settings");
120
+ const settingsCreated = await createDefaultSettings();
121
+
122
+ if (!jsonOut) {
123
+ if (settingsCreated) success(`Created config: ${SETTINGS_PATH}`);
124
+ else success(`Config already exists: ${SETTINGS_PATH}`);
125
+ }
126
+
127
+ const dbExists = await fileExists(dbPath);
128
+ let dbInitialized = false;
129
+
130
+ if (dbExists) {
131
+ if (!jsonOut) success(`Database already exists: ${dbPath}`);
132
+ dbInitialized = true;
133
+ } else {
134
+ try {
135
+ const db = await BrainDb.connect(dbPath, settings, { skipCollection: true });
136
+ await db.close();
137
+ await new Promise((r) => setTimeout(r, 200));
138
+ dbInitialized = true;
139
+ if (!jsonOut) success(`Database initialized: ${dbPath}`);
140
+ } catch {
141
+ if (!jsonOut) warning(`Database will be auto-created on first use`);
142
+ }
143
+ }
144
+
145
+ if (!jsonOut) {
146
+ console.log("");
147
+ separator();
148
+ info("Quick Start Guide");
149
+ console.log("");
150
+ subItem("1. Configure LLM (for AI queries):", 0);
151
+ subItem(` Edit ${SETTINGS_PATH}`, 4);
152
+ subItem(` Set llm.baseURL to your OpenAI-compatible API endpoint`, 4);
153
+ subItem(` Set llm.apiKey or export DASHSCOPE_API_KEY`, 4);
154
+ console.log("");
155
+ subItem("2. Add your first page:", 0);
156
+ subItem(" echo '# Hello' | ebrain put hello --stdin", 4);
157
+ console.log("");
158
+ subItem("3. Import a directory of markdown files:", 0);
159
+ subItem(" ebrain import ./docs", 4);
160
+ console.log("");
161
+ subItem("4. Query with AI:", 0);
162
+ subItem(' ebrain query "What did we ship in Q4?" --llm', 4);
163
+ console.log("");
164
+ subItem("5. Visualize your knowledge graph:", 0);
165
+ subItem(" ebrain graph", 4);
166
+ console.log("");
167
+ separator();
168
+ }
169
+
170
+ print(program, { ok: true, settingsPath: SETTINGS_PATH, settingsCreated, dbPath, dbInitialized });
171
+ process.exit(0);
172
+ });
173
+ }
174
+
175
+ export function registerStatsCommand(program: Command): void {
176
+ program
177
+ .command("stats")
178
+ .description("show knowledge base statistics")
179
+ .addHelpText("after", `
180
+ Examples:
181
+ ebrain stats
182
+ ebrain stats --json
183
+ `)
184
+ .action(async () => {
185
+ await withRepo(program, async (repo) => {
186
+ const jsonOut = isJson(program);
187
+ const stats = await repo.stats();
188
+ if (!jsonOut) {
189
+ header("Knowledge Base Statistics");
190
+ keyValue("Pages", String(stats.pages));
191
+ keyValue("Links", String(stats.links));
192
+ keyValue("Tags", String(stats.tags));
193
+ keyValue("Timeline entries", String(stats.timelineEntries));
194
+ keyValue("Raw data rows", String(stats.rawRows));
195
+ }
196
+ print(program, stats);
197
+ });
198
+ });
199
+ }
200
+
201
+ export function registerConfigCommand(program: Command): void {
202
+ program
203
+ .command("config")
204
+ .description("show resolved configuration")
205
+ .action(async () => {
206
+ const settings = await loadSettings();
207
+ const cliDb = program.opts().db;
208
+ const effectiveDb = cliDb ?? settings.dbPath;
209
+ print(program, {
210
+ settingsFile: SETTINGS_PATH,
211
+ dbPath: effectiveDb,
212
+ mode: settings.remote ? "remote" : "local",
213
+ remote: settings.remote ?? null,
214
+ embed: {
215
+ provider: settings.embed.provider,
216
+ baseURL: settings.embed.baseURL,
217
+ model: settings.embed.model,
218
+ dimensions: settings.embed.dimensions,
219
+ hasApiKey: !!settings.embed.apiKey || !!process.env[settings.embed.apiKeyEnv],
220
+ },
221
+ llm: {
222
+ baseURL: settings.llm.baseURL || "(not configured)",
223
+ model: settings.llm.model,
224
+ hasApiKey: !!settings.llm.apiKey || !!process.env[settings.llm.apiKeyEnv],
225
+ },
226
+ });
227
+ });
228
+ }
229
+
230
+ export function registerServeCommand(program: Command): void {
231
+ program
232
+ .command("serve")
233
+ .description("start MCP server over stdio (for AI tool integration)")
234
+ .addHelpText("after", `
235
+ Examples:
236
+ ebrain serve
237
+ `)
238
+ .action(async () => {
239
+ const { startMcpServer } = await import("../mcp/server");
240
+ const dbPath = String(program.opts().db);
241
+ await startMcpServer(dbPath);
242
+ });
243
+
244
+ program
245
+ .command("tools-json")
246
+ .description("print MCP tools discovery JSON")
247
+ .action(() => {
248
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
249
+ const { TOOL_MANIFEST } = require("../mcp/server");
250
+ console.log(JSON.stringify({ tools: TOOL_MANIFEST }, null, 2));
251
+ });
252
+ }