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.
- package/package.json +1 -1
- package/src/ai/ax-pipeline.ts +114 -0
- package/src/ai/compiler.ts +118 -113
- package/src/ai/entity-link.ts +96 -78
- package/src/ai/timeline-extractor.ts +110 -99
- package/src/commands/compile-cmd.ts +1 -1
- package/src/commands/entity-links.ts +105 -0
- package/src/commands/import-cmd.ts +464 -0
- package/src/commands/index.ts +30 -2314
- package/src/commands/misc-cmds.ts +190 -0
- package/src/commands/misc-commands.ts +252 -0
- package/src/commands/put-cmd.ts +525 -0
- package/src/commands/query-cmd.ts +486 -0
- package/src/commands/shared.ts +109 -0
- package/src/commands/timeline-cmd.ts +159 -0
- package/src/config/index.ts +53 -0
- package/src/config/init.ts +50 -0
- package/src/config/paths.ts +21 -0
- package/src/config/schema.ts +121 -0
- package/src/config/settings.ts +168 -0
- package/src/db/client.ts +1 -1
- package/src/markdown/document-loader.ts +30 -2
- package/src/repositories/brain-repo.ts +43 -1
- package/src/settings.ts +27 -282
- /package/src/{config.ts → slug-utils.ts} +0 -0
|
@@ -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
|
+
}
|