brainhub-mcp 0.1.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/LICENSE +201 -0
- package/README.md +84 -0
- package/dist/adapters/claude.d.ts +2 -0
- package/dist/adapters/claude.js +50 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex.d.ts +2 -0
- package/dist/adapters/codex.js +52 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/content.d.ts +6 -0
- package/dist/adapters/content.js +68 -0
- package/dist/adapters/content.js.map +1 -0
- package/dist/adapters/grok.d.ts +2 -0
- package/dist/adapters/grok.js +44 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/index.d.ts +31 -0
- package/dist/adapters/index.js +111 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/jsonl.d.ts +6 -0
- package/dist/adapters/jsonl.js +26 -0
- package/dist/adapters/jsonl.js.map +1 -0
- package/dist/adapters/types.d.ts +10 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/auth/account-status.d.ts +50 -0
- package/dist/auth/account-status.js +59 -0
- package/dist/auth/account-status.js.map +1 -0
- package/dist/auth/google-account.d.ts +21 -0
- package/dist/auth/google-account.js +45 -0
- package/dist/auth/google-account.js.map +1 -0
- package/dist/auth/google-oauth.d.ts +35 -0
- package/dist/auth/google-oauth.js +191 -0
- package/dist/auth/google-oauth.js.map +1 -0
- package/dist/auth/oauth-loopback.d.ts +10 -0
- package/dist/auth/oauth-loopback.js +81 -0
- package/dist/auth/oauth-loopback.js.map +1 -0
- package/dist/auth/official-oauth-client.d.ts +5 -0
- package/dist/auth/official-oauth-client.js +6 -0
- package/dist/auth/official-oauth-client.js.map +1 -0
- package/dist/auth/platform-secrets.d.ts +14 -0
- package/dist/auth/platform-secrets.js +81 -0
- package/dist/auth/platform-secrets.js.map +1 -0
- package/dist/auth/secret-store-factory.d.ts +11 -0
- package/dist/auth/secret-store-factory.js +37 -0
- package/dist/auth/secret-store-factory.js.map +1 -0
- package/dist/auth/secret-store.d.ts +13 -0
- package/dist/auth/secret-store.js +40 -0
- package/dist/auth/secret-store.js.map +1 -0
- package/dist/capture/images.d.ts +11 -0
- package/dist/capture/images.js +27 -0
- package/dist/capture/images.js.map +1 -0
- package/dist/capture/normalize.d.ts +11 -0
- package/dist/capture/normalize.js +43 -0
- package/dist/capture/normalize.js.map +1 -0
- package/dist/capture/redact.d.ts +10 -0
- package/dist/capture/redact.js +60 -0
- package/dist/capture/redact.js.map +1 -0
- package/dist/cli/index.d.ts +20 -0
- package/dist/cli/index.js +781 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/clients/registry.d.ts +24 -0
- package/dist/clients/registry.js +105 -0
- package/dist/clients/registry.js.map +1 -0
- package/dist/domain/config-io.d.ts +15 -0
- package/dist/domain/config-io.js +168 -0
- package/dist/domain/config-io.js.map +1 -0
- package/dist/domain/config.d.ts +74 -0
- package/dist/domain/config.js +130 -0
- package/dist/domain/config.js.map +1 -0
- package/dist/domain/errors.d.ts +6 -0
- package/dist/domain/errors.js +11 -0
- package/dist/domain/errors.js.map +1 -0
- package/dist/domain/session.d.ts +62 -0
- package/dist/domain/session.js +47 -0
- package/dist/domain/session.js.map +1 -0
- package/dist/drive/drive-port.d.ts +51 -0
- package/dist/drive/drive-port.js +2 -0
- package/dist/drive/drive-port.js.map +1 -0
- package/dist/drive/google-drive.d.ts +28 -0
- package/dist/drive/google-drive.js +501 -0
- package/dist/drive/google-drive.js.map +1 -0
- package/dist/drive/memory-drive.d.ts +19 -0
- package/dist/drive/memory-drive.js +148 -0
- package/dist/drive/memory-drive.js.map +1 -0
- package/dist/drive/scoped-drive.d.ts +17 -0
- package/dist/drive/scoped-drive.js +115 -0
- package/dist/drive/scoped-drive.js.map +1 -0
- package/dist/mcp/server.d.ts +55 -0
- package/dist/mcp/server.js +133 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/portrait/obsidian.d.ts +7 -0
- package/dist/portrait/obsidian.js +16 -0
- package/dist/portrait/obsidian.js.map +1 -0
- package/dist/portrait/portrait-service.d.ts +12 -0
- package/dist/portrait/portrait-service.js +18 -0
- package/dist/portrait/portrait-service.js.map +1 -0
- package/dist/portrait/portrait-sync-service.d.ts +21 -0
- package/dist/portrait/portrait-sync-service.js +43 -0
- package/dist/portrait/portrait-sync-service.js.map +1 -0
- package/dist/runtime/command.d.ts +4 -0
- package/dist/runtime/command.js +2 -0
- package/dist/runtime/command.js.map +1 -0
- package/dist/runtime/container.d.ts +72 -0
- package/dist/runtime/container.js +361 -0
- package/dist/runtime/container.js.map +1 -0
- package/dist/scheduler/jobs.d.ts +2 -0
- package/dist/scheduler/jobs.js +7 -0
- package/dist/scheduler/jobs.js.map +1 -0
- package/dist/scheduler/launchd.d.ts +7 -0
- package/dist/scheduler/launchd.js +40 -0
- package/dist/scheduler/launchd.js.map +1 -0
- package/dist/scheduler/manager.d.ts +26 -0
- package/dist/scheduler/manager.js +113 -0
- package/dist/scheduler/manager.js.map +1 -0
- package/dist/search/chunk-text.d.ts +11 -0
- package/dist/search/chunk-text.js +40 -0
- package/dist/search/chunk-text.js.map +1 -0
- package/dist/search/e5-embedder.d.ts +19 -0
- package/dist/search/e5-embedder.js +71 -0
- package/dist/search/e5-embedder.js.map +1 -0
- package/dist/search/embedder.d.ts +8 -0
- package/dist/search/embedder.js +5 -0
- package/dist/search/embedder.js.map +1 -0
- package/dist/search/model-cache.d.ts +7 -0
- package/dist/search/model-cache.js +38 -0
- package/dist/search/model-cache.js.map +1 -0
- package/dist/search/search-service.d.ts +46 -0
- package/dist/search/search-service.js +319 -0
- package/dist/search/search-service.js.map +1 -0
- package/dist/setup/backfill-service.d.ts +33 -0
- package/dist/setup/backfill-service.js +47 -0
- package/dist/setup/backfill-service.js.map +1 -0
- package/dist/setup/setup-service.d.ts +65 -0
- package/dist/setup/setup-service.js +90 -0
- package/dist/setup/setup-service.js.map +1 -0
- package/dist/setup/uninstall-service.d.ts +34 -0
- package/dist/setup/uninstall-service.js +91 -0
- package/dist/setup/uninstall-service.js.map +1 -0
- package/dist/state/sqlite-store.d.ts +19 -0
- package/dist/state/sqlite-store.js +299 -0
- package/dist/state/sqlite-store.js.map +1 -0
- package/dist/state/store.d.ts +52 -0
- package/dist/state/store.js +2 -0
- package/dist/state/store.js.map +1 -0
- package/dist/status/status-service.d.ts +38 -0
- package/dist/status/status-service.js +93 -0
- package/dist/status/status-service.js.map +1 -0
- package/dist/update/update-check-service.d.ts +38 -0
- package/dist/update/update-check-service.js +105 -0
- package/dist/update/update-check-service.js.map +1 -0
- package/dist/upload/lock.d.ts +7 -0
- package/dist/upload/lock.js +68 -0
- package/dist/upload/lock.js.map +1 -0
- package/dist/upload/upload-service.d.ts +36 -0
- package/dist/upload/upload-service.js +360 -0
- package/dist/upload/upload-service.js.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdir, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir, hostname } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { google } from "googleapis";
|
|
9
|
+
import { resolveGoogleAccountStatus, resolveReusableGoogleAccount, } from "../auth/account-status.js";
|
|
10
|
+
import { applyGoogleConnection, clearGoogleConnection, connectGoogleAccount, readGoogleDriveAccount, } from "../auth/google-account.js";
|
|
11
|
+
import { GoogleOAuth, revokeGoogleCredential, } from "../auth/google-oauth.js";
|
|
12
|
+
import { createConfigSecretStore, } from "../auth/secret-store-factory.js";
|
|
13
|
+
import { ClientRegistry, launchWithConfig, mergeClaudeDesktopConfig, } from "../clients/registry.js";
|
|
14
|
+
import { loadConfig, writeConfig, } from "../domain/config-io.js";
|
|
15
|
+
import { BrainHubError } from "../domain/errors.js";
|
|
16
|
+
import { serveMcp } from "../mcp/server.js";
|
|
17
|
+
import { BrainHubRuntime } from "../runtime/container.js";
|
|
18
|
+
import { SchedulerManager } from "../scheduler/manager.js";
|
|
19
|
+
import { E5Embedder } from "../search/e5-embedder.js";
|
|
20
|
+
import { BackfillService, } from "../setup/backfill-service.js";
|
|
21
|
+
import { SetupService } from "../setup/setup-service.js";
|
|
22
|
+
import { UninstallService, localCleanupTargets, } from "../setup/uninstall-service.js";
|
|
23
|
+
import { discoverPublishDirectory } from "../portrait/obsidian.js";
|
|
24
|
+
import { SqliteStateStore } from "../state/sqlite-store.js";
|
|
25
|
+
export function isAffirmativeConfirmation(value) {
|
|
26
|
+
return ["y", "yes"].includes(value.trim().toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
function sourceList(value) {
|
|
29
|
+
const allowed = new Set([
|
|
30
|
+
"claude-code",
|
|
31
|
+
"codex",
|
|
32
|
+
"grok-build",
|
|
33
|
+
]);
|
|
34
|
+
const values = value.split(",").map((item) => item.trim());
|
|
35
|
+
if (values.some((item) => !allowed.has(item)))
|
|
36
|
+
throw new Error("Unknown source in --sources");
|
|
37
|
+
return values;
|
|
38
|
+
}
|
|
39
|
+
async function directorySize(path) {
|
|
40
|
+
let total = 0;
|
|
41
|
+
try {
|
|
42
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
43
|
+
const child = join(path, entry.name);
|
|
44
|
+
total += entry.isDirectory()
|
|
45
|
+
? await directorySize(child)
|
|
46
|
+
: (await stat(child)).size;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code !== "ENOENT")
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
return total;
|
|
54
|
+
}
|
|
55
|
+
export async function runCli(argv = process.argv, dependencies = {}) {
|
|
56
|
+
const writeOutput = dependencies.writeOutput ??
|
|
57
|
+
((value) => process.stdout.write(value));
|
|
58
|
+
const secretStoreFactory = dependencies.secretStoreFactory ?? createConfigSecretStore;
|
|
59
|
+
const oauthFactory = dependencies.oauthFactory ??
|
|
60
|
+
((clientFile, secrets) => new GoogleOAuth(clientFile, secrets));
|
|
61
|
+
const driveFactory = dependencies.driveFactory ??
|
|
62
|
+
((auth) => google.drive({ version: "v3", auth }));
|
|
63
|
+
const persistConfig = dependencies.writeConfig ?? writeConfig;
|
|
64
|
+
const print = (value, json = false) => {
|
|
65
|
+
if (json || typeof value !== "string")
|
|
66
|
+
writeOutput(`${JSON.stringify(value, null, 2)}\n`);
|
|
67
|
+
else
|
|
68
|
+
writeOutput(`${value}\n`);
|
|
69
|
+
};
|
|
70
|
+
const program = new Command();
|
|
71
|
+
const launch = {
|
|
72
|
+
command: process.execPath,
|
|
73
|
+
args: [resolve(argv[1] ?? process.argv[1] ?? "brainhub-mcp")],
|
|
74
|
+
};
|
|
75
|
+
program
|
|
76
|
+
.name("brainhub-mcp")
|
|
77
|
+
.description("BrainHub local session MCP")
|
|
78
|
+
.version("0.1.0")
|
|
79
|
+
.option("--config <path>", "configuration file");
|
|
80
|
+
const load = async () => loadConfig({
|
|
81
|
+
homeDir: homedir(),
|
|
82
|
+
hostname: hostname(),
|
|
83
|
+
platform: process.platform,
|
|
84
|
+
env: process.env,
|
|
85
|
+
...(program.opts().config
|
|
86
|
+
? { configFile: resolve(program.opts().config) }
|
|
87
|
+
: {}),
|
|
88
|
+
});
|
|
89
|
+
const runtime = async () => {
|
|
90
|
+
const loaded = await load();
|
|
91
|
+
return new BrainHubRuntime({
|
|
92
|
+
config: loaded.config,
|
|
93
|
+
paths: loaded.paths,
|
|
94
|
+
configFile: loaded.configFile,
|
|
95
|
+
homeDir: homedir(),
|
|
96
|
+
platform: process.platform,
|
|
97
|
+
executable: launch.command,
|
|
98
|
+
executableArgs: launch.args,
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
const secretsFor = (loaded) => secretStoreFactory({
|
|
102
|
+
platform: process.platform,
|
|
103
|
+
configFile: loaded.configFile,
|
|
104
|
+
legacyAccount: loaded.config.device.name,
|
|
105
|
+
});
|
|
106
|
+
const rollbackAndRethrow = async (staged, error) => {
|
|
107
|
+
try {
|
|
108
|
+
await staged.rollback();
|
|
109
|
+
}
|
|
110
|
+
catch (rollbackError) {
|
|
111
|
+
throw new AggregateError([error, rollbackError], "Google account connection and rollback both failed", { cause: rollbackError });
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
};
|
|
115
|
+
const runBackfill = dependencies.runBackfill ??
|
|
116
|
+
(async (options) => {
|
|
117
|
+
const loaded = await load();
|
|
118
|
+
const state = new SqliteStateStore(loaded.paths.stateFile, {
|
|
119
|
+
...(loaded.paths.legacyStateFile
|
|
120
|
+
? { legacyPath: loaded.paths.legacyStateFile }
|
|
121
|
+
: {}),
|
|
122
|
+
});
|
|
123
|
+
const service = new BackfillService({
|
|
124
|
+
state,
|
|
125
|
+
inspect: async () => {
|
|
126
|
+
const instance = await runtime();
|
|
127
|
+
try {
|
|
128
|
+
const discovery = await instance.discover({});
|
|
129
|
+
const bytes = discovery.sessions.reduce((total, session) => total +
|
|
130
|
+
session.turns.reduce((turnTotal, turn) => turnTotal +
|
|
131
|
+
Buffer.byteLength(turn.text) +
|
|
132
|
+
turn.images.reduce((imageTotal, image) => imageTotal +
|
|
133
|
+
Buffer.byteLength(image.kind === "embedded" ? image.data : image.url), 0), 0), 0);
|
|
134
|
+
return { sessions: discovery.sessions.length, bytes };
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
instance.close();
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
confirm: async (summary) => {
|
|
141
|
+
if (options.yes)
|
|
142
|
+
return true;
|
|
143
|
+
if (!process.stdin.isTTY) {
|
|
144
|
+
throw new Error("Backfill confirmation requires a terminal or --yes");
|
|
145
|
+
}
|
|
146
|
+
const prompt = createInterface({
|
|
147
|
+
input: process.stdin,
|
|
148
|
+
output: process.stdout,
|
|
149
|
+
});
|
|
150
|
+
try {
|
|
151
|
+
const answer = await prompt.question(`回填 ${summary.sessions} 个会话(${summary.bytes} bytes)?[Y/n] `);
|
|
152
|
+
return !["n", "no"].includes(answer.trim().toLowerCase());
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
prompt.close();
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
upload: async () => {
|
|
159
|
+
const instance = await runtime();
|
|
160
|
+
try {
|
|
161
|
+
const result = await instance.uploadSessions({ backfill: true });
|
|
162
|
+
return { uploaded: result.uploaded, pending: result.pending };
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
instance.close();
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
report: (summary) => {
|
|
169
|
+
if (!options.json) {
|
|
170
|
+
print(`发现 ${summary.sessions} 个可回填会话,共 ${summary.bytes} bytes。`);
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
return await service.run();
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
state.close();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
program
|
|
182
|
+
.command("setup")
|
|
183
|
+
.description("configure BrainHub MCP on macOS")
|
|
184
|
+
.option("--yes", "accept the default session backfill")
|
|
185
|
+
.option("--drive-root-id <id>", "choose an existing brain-hub folder")
|
|
186
|
+
.option("--obsidian-vault <path>", "publish the daily portrait to this vault")
|
|
187
|
+
.option("--json", "machine-readable final output")
|
|
188
|
+
.action(async (options) => {
|
|
189
|
+
let lastModelPercent = -1;
|
|
190
|
+
const service = new SetupService({
|
|
191
|
+
platform: process.platform,
|
|
192
|
+
ensureConfig: async () => {
|
|
193
|
+
const loaded = await load();
|
|
194
|
+
const nextConfig = options.obsidianVault
|
|
195
|
+
? {
|
|
196
|
+
...loaded.config,
|
|
197
|
+
publish: {
|
|
198
|
+
fallbackPath: join(resolve(options.obsidianVault), "BrainHub"),
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
: loaded.config;
|
|
202
|
+
await persistConfig(loaded.configFile, nextConfig);
|
|
203
|
+
},
|
|
204
|
+
connectGoogle: async () => {
|
|
205
|
+
const loaded = await load();
|
|
206
|
+
const secrets = secretsFor(loaded);
|
|
207
|
+
const credential = await secrets.get();
|
|
208
|
+
const reusable = await resolveReusableGoogleAccount({
|
|
209
|
+
config: loaded.config,
|
|
210
|
+
credential,
|
|
211
|
+
loadAccount: async () => {
|
|
212
|
+
const authClient = await oauthFactory(loaded.config.drive.oauthClientFile, secrets).getClient({ interactive: false });
|
|
213
|
+
return readGoogleDriveAccount(driveFactory(authClient));
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
if (reusable)
|
|
217
|
+
return reusable;
|
|
218
|
+
if (!options.json) {
|
|
219
|
+
print("即将打开浏览器。请选择 BrainHub 要连接的 Google 账号并同意授权。");
|
|
220
|
+
}
|
|
221
|
+
const staged = await oauthFactory(loaded.config.drive.oauthClientFile, secrets).beginInteractiveAuthorization();
|
|
222
|
+
try {
|
|
223
|
+
const connection = await connectGoogleAccount({
|
|
224
|
+
authClient: staged.client,
|
|
225
|
+
drive: driveFactory,
|
|
226
|
+
rootFolderName: loaded.config.drive.rootFolderName,
|
|
227
|
+
...(options.driveRootId
|
|
228
|
+
? { rootFolderId: options.driveRootId }
|
|
229
|
+
: {}),
|
|
230
|
+
});
|
|
231
|
+
const nextConfig = applyGoogleConnection(loaded.config, connection);
|
|
232
|
+
await staged.commit();
|
|
233
|
+
await persistConfig(loaded.configFile, nextConfig);
|
|
234
|
+
return { email: connection.account.email };
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
return rollbackAndRethrow(staged, error);
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
prepareModel: async (progress) => {
|
|
241
|
+
const loaded = await load();
|
|
242
|
+
const embedder = new E5Embedder({
|
|
243
|
+
model: loaded.config.search.model,
|
|
244
|
+
revision: loaded.config.search.modelRevision,
|
|
245
|
+
dimensions: loaded.config.search.dimensions,
|
|
246
|
+
cacheDir: loaded.paths.modelCache,
|
|
247
|
+
});
|
|
248
|
+
await embedder.prepare(progress);
|
|
249
|
+
},
|
|
250
|
+
runBackfill: () => runBackfill({
|
|
251
|
+
yes: options.yes ?? false,
|
|
252
|
+
json: options.json ?? false,
|
|
253
|
+
}),
|
|
254
|
+
inspectClients: async () => {
|
|
255
|
+
const loaded = await load();
|
|
256
|
+
const registry = new ClientRegistry(launchWithConfig(launch, loaded.configFile));
|
|
257
|
+
return Object.fromEntries(await Promise.all(["claude", "codex", "grok"].map(async (client) => [
|
|
258
|
+
client,
|
|
259
|
+
await registry.status(client),
|
|
260
|
+
])));
|
|
261
|
+
},
|
|
262
|
+
registerClient: async (client) => {
|
|
263
|
+
const loaded = await load();
|
|
264
|
+
await new ClientRegistry(launchWithConfig(launch, loaded.configFile)).mutate(client, "install");
|
|
265
|
+
},
|
|
266
|
+
installScheduler: async ({ portrait }) => {
|
|
267
|
+
const loaded = await load();
|
|
268
|
+
await new SchedulerManager({
|
|
269
|
+
platform: process.platform,
|
|
270
|
+
homeDir: homedir(),
|
|
271
|
+
command: launch.command,
|
|
272
|
+
args: [...launch.args, "--config", loaded.configFile],
|
|
273
|
+
}).install(loaded.config.scheduler.at, loaded.config.scheduler.syncAt, { portrait });
|
|
274
|
+
},
|
|
275
|
+
inspectObsidian: async () => {
|
|
276
|
+
const loaded = await load();
|
|
277
|
+
const path = await discoverPublishDirectory({
|
|
278
|
+
platform: process.platform,
|
|
279
|
+
homeDir: homedir(),
|
|
280
|
+
fallbackPath: loaded.config.publish.fallbackPath,
|
|
281
|
+
});
|
|
282
|
+
return path ? { configured: true, path } : { configured: false };
|
|
283
|
+
},
|
|
284
|
+
report: (progress) => {
|
|
285
|
+
if (options.json)
|
|
286
|
+
return;
|
|
287
|
+
if (progress.type === "backfill-summary") {
|
|
288
|
+
print(`发现 ${progress.sessions ?? 0} 个可回填会话,共 ${progress.bytes ?? 0} bytes。`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const percent = Math.floor(progress.percent ?? 0);
|
|
292
|
+
if (percent === lastModelPercent)
|
|
293
|
+
return;
|
|
294
|
+
lastModelPercent = percent;
|
|
295
|
+
print(`模型下载 ${percent}%${progress.file ? ` ${progress.file}` : ""}`);
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
print(await service.run(), options.json);
|
|
299
|
+
});
|
|
300
|
+
program
|
|
301
|
+
.command("serve")
|
|
302
|
+
.description("run the stdio MCP server")
|
|
303
|
+
.action(async () => {
|
|
304
|
+
const instance = await runtime();
|
|
305
|
+
await serveMcp(instance);
|
|
306
|
+
});
|
|
307
|
+
const config = program
|
|
308
|
+
.command("config")
|
|
309
|
+
.description("manage BrainHub configuration");
|
|
310
|
+
config
|
|
311
|
+
.command("show")
|
|
312
|
+
.option("--json")
|
|
313
|
+
.action(async (options) => {
|
|
314
|
+
const loaded = await load();
|
|
315
|
+
print({ file: loaded.configFile, config: loaded.config, paths: loaded.paths }, options.json);
|
|
316
|
+
});
|
|
317
|
+
config
|
|
318
|
+
.command("init")
|
|
319
|
+
.option("--json")
|
|
320
|
+
.action(async (options) => {
|
|
321
|
+
const loaded = await load();
|
|
322
|
+
await writeConfig(loaded.configFile, loaded.config);
|
|
323
|
+
print({ configured: true, file: loaded.configFile }, options.json);
|
|
324
|
+
});
|
|
325
|
+
program
|
|
326
|
+
.command("upload")
|
|
327
|
+
.description("scan and upload local sessions")
|
|
328
|
+
.option("--dry-run", "plan without Drive or local state writes")
|
|
329
|
+
.option("--backfill", "scan all available history")
|
|
330
|
+
.option("--include-subagents", "include sidechains and subagents")
|
|
331
|
+
.option("--sources <sources>", "comma-separated sources", sourceList)
|
|
332
|
+
.option("--json", "machine-readable output")
|
|
333
|
+
.action(async (options) => {
|
|
334
|
+
const instance = await runtime();
|
|
335
|
+
try {
|
|
336
|
+
const result = await instance.uploadSessions({
|
|
337
|
+
...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),
|
|
338
|
+
...(options.backfill !== undefined
|
|
339
|
+
? { backfill: options.backfill }
|
|
340
|
+
: {}),
|
|
341
|
+
...(options.includeSubagents !== undefined
|
|
342
|
+
? { includeSubagents: options.includeSubagents }
|
|
343
|
+
: {}),
|
|
344
|
+
...(options.sources ? { sources: options.sources } : {}),
|
|
345
|
+
});
|
|
346
|
+
print(result, options.json);
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
instance.close();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
const auth = program.command("auth").description("manage Google OAuth");
|
|
353
|
+
auth
|
|
354
|
+
.command("login")
|
|
355
|
+
.alias("switch")
|
|
356
|
+
.option("--yes", "accept the default session backfill")
|
|
357
|
+
.option("--drive-root-id <id>", "choose an existing brain-hub folder")
|
|
358
|
+
.option("--json")
|
|
359
|
+
.action(async (options) => {
|
|
360
|
+
const loaded = await load();
|
|
361
|
+
if (!options.json) {
|
|
362
|
+
print("A browser will open. Choose the Google account BrainHub should use.");
|
|
363
|
+
}
|
|
364
|
+
const secrets = secretsFor(loaded);
|
|
365
|
+
const staged = await oauthFactory(loaded.config.drive.oauthClientFile, secrets).beginInteractiveAuthorization();
|
|
366
|
+
const connection = await (async () => {
|
|
367
|
+
try {
|
|
368
|
+
const connected = await connectGoogleAccount({
|
|
369
|
+
authClient: staged.client,
|
|
370
|
+
drive: driveFactory,
|
|
371
|
+
rootFolderName: loaded.config.drive.rootFolderName,
|
|
372
|
+
...(options.driveRootId
|
|
373
|
+
? { rootFolderId: options.driveRootId }
|
|
374
|
+
: {}),
|
|
375
|
+
});
|
|
376
|
+
const nextConfig = applyGoogleConnection(loaded.config, connected);
|
|
377
|
+
await staged.commit();
|
|
378
|
+
await persistConfig(loaded.configFile, nextConfig);
|
|
379
|
+
return { connected, nextConfig };
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
return rollbackAndRethrow(staged, error);
|
|
383
|
+
}
|
|
384
|
+
})();
|
|
385
|
+
const backfill = await runBackfill({
|
|
386
|
+
yes: options.yes ?? false,
|
|
387
|
+
json: options.json ?? false,
|
|
388
|
+
});
|
|
389
|
+
print({
|
|
390
|
+
authenticated: true,
|
|
391
|
+
account: {
|
|
392
|
+
email: connection.connected.account.email,
|
|
393
|
+
displayName: connection.connected.account.displayName,
|
|
394
|
+
},
|
|
395
|
+
drive: {
|
|
396
|
+
rootFolderId: connection.connected.rootFolderId,
|
|
397
|
+
rootFolderName: connection.nextConfig.drive.rootFolderName,
|
|
398
|
+
},
|
|
399
|
+
backfill,
|
|
400
|
+
}, options.json);
|
|
401
|
+
});
|
|
402
|
+
auth
|
|
403
|
+
.command("status")
|
|
404
|
+
.option("--json")
|
|
405
|
+
.action(async (options) => {
|
|
406
|
+
const loaded = await load();
|
|
407
|
+
const secrets = secretsFor(loaded);
|
|
408
|
+
const credential = await secrets.get();
|
|
409
|
+
const status = await resolveGoogleAccountStatus({
|
|
410
|
+
config: loaded.config,
|
|
411
|
+
credential,
|
|
412
|
+
loadAccount: async () => {
|
|
413
|
+
const authClient = await oauthFactory(loaded.config.drive.oauthClientFile, secrets).getClient({ interactive: false });
|
|
414
|
+
return readGoogleDriveAccount(driveFactory(authClient));
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
print(status, options.json);
|
|
418
|
+
});
|
|
419
|
+
auth
|
|
420
|
+
.command("logout")
|
|
421
|
+
.option("--json")
|
|
422
|
+
.action(async (options) => {
|
|
423
|
+
const loaded = await load();
|
|
424
|
+
const secrets = secretsFor(loaded);
|
|
425
|
+
const previousCredential = await secrets.get();
|
|
426
|
+
await secrets.delete();
|
|
427
|
+
try {
|
|
428
|
+
await persistConfig(loaded.configFile, clearGoogleConnection(loaded.config));
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
try {
|
|
432
|
+
if (previousCredential)
|
|
433
|
+
await secrets.set(previousCredential);
|
|
434
|
+
}
|
|
435
|
+
catch (restoreError) {
|
|
436
|
+
throw new AggregateError([error, restoreError], "Google logout and credential restore both failed", { cause: restoreError });
|
|
437
|
+
}
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
const warnings = [];
|
|
441
|
+
if (previousCredential) {
|
|
442
|
+
try {
|
|
443
|
+
await revokeGoogleCredential(previousCredential);
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
warnings.push({
|
|
447
|
+
code: "OAUTH_REVOCATION_FAILED",
|
|
448
|
+
message: error instanceof Error ? error.message : "OAuth revoke failed",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
print({
|
|
453
|
+
authenticated: false,
|
|
454
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
455
|
+
}, options.json);
|
|
456
|
+
});
|
|
457
|
+
const drive = program
|
|
458
|
+
.command("drive")
|
|
459
|
+
.description("manage the BrainHub Drive root");
|
|
460
|
+
drive
|
|
461
|
+
.command("init")
|
|
462
|
+
.option("--drive-root-id <id>", "choose an existing brain-hub folder")
|
|
463
|
+
.option("--json")
|
|
464
|
+
.action(async (options) => {
|
|
465
|
+
const loaded = await load();
|
|
466
|
+
const secrets = secretsFor(loaded);
|
|
467
|
+
const oauth = await oauthFactory(loaded.config.drive.oauthClientFile, secrets).getClient({ interactive: false });
|
|
468
|
+
const connection = await connectGoogleAccount({
|
|
469
|
+
authClient: oauth,
|
|
470
|
+
drive: driveFactory,
|
|
471
|
+
rootFolderName: loaded.config.drive.rootFolderName,
|
|
472
|
+
...(options.driveRootId ? { rootFolderId: options.driveRootId } : {}),
|
|
473
|
+
});
|
|
474
|
+
await persistConfig(loaded.configFile, applyGoogleConnection(loaded.config, connection));
|
|
475
|
+
print({
|
|
476
|
+
initialized: true,
|
|
477
|
+
account: {
|
|
478
|
+
email: connection.account.email,
|
|
479
|
+
displayName: connection.account.displayName,
|
|
480
|
+
},
|
|
481
|
+
rootFolderId: connection.rootFolderId,
|
|
482
|
+
}, options.json);
|
|
483
|
+
});
|
|
484
|
+
drive
|
|
485
|
+
.command("status")
|
|
486
|
+
.option("--json")
|
|
487
|
+
.action(async (options) => {
|
|
488
|
+
const loaded = await load();
|
|
489
|
+
const exists = Boolean(loaded.config.drive.rootFolderId);
|
|
490
|
+
print({
|
|
491
|
+
configured: exists,
|
|
492
|
+
rootFolderId: loaded.config.drive.rootFolderId || undefined,
|
|
493
|
+
accountEmail: loaded.config.drive.accountEmail || undefined,
|
|
494
|
+
}, options.json);
|
|
495
|
+
});
|
|
496
|
+
const search = program
|
|
497
|
+
.command("search")
|
|
498
|
+
.description("hybrid semantic search");
|
|
499
|
+
search
|
|
500
|
+
.command("query <query>")
|
|
501
|
+
.option("--limit <number>", "maximum results", Number)
|
|
502
|
+
.option("--json")
|
|
503
|
+
.action(async (query, options) => {
|
|
504
|
+
const instance = await runtime();
|
|
505
|
+
try {
|
|
506
|
+
print(await instance.searchSessions({
|
|
507
|
+
query,
|
|
508
|
+
...(options.limit ? { limit: options.limit } : {}),
|
|
509
|
+
}), options.json);
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
instance.close();
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
search
|
|
516
|
+
.command("sync")
|
|
517
|
+
.option("--json")
|
|
518
|
+
.action(async (options) => {
|
|
519
|
+
const instance = await runtime();
|
|
520
|
+
try {
|
|
521
|
+
const result = await instance
|
|
522
|
+
.searchService(await instance.drive(false))
|
|
523
|
+
.sync();
|
|
524
|
+
print({ synced: true, ...result }, options.json);
|
|
525
|
+
}
|
|
526
|
+
finally {
|
|
527
|
+
instance.close();
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
search
|
|
531
|
+
.command("reindex")
|
|
532
|
+
.option("--json")
|
|
533
|
+
.action(async (options) => {
|
|
534
|
+
const instance = await runtime();
|
|
535
|
+
try {
|
|
536
|
+
const drivePort = await instance.drive(false);
|
|
537
|
+
await rm(instance.paths.searchIndexFile, { force: true });
|
|
538
|
+
const result = await instance.searchService(drivePort).sync();
|
|
539
|
+
print({ reindexed: true, ...result }, options.json);
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
instance.close();
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
const model = search.command("model");
|
|
546
|
+
model
|
|
547
|
+
.command("status")
|
|
548
|
+
.option("--json")
|
|
549
|
+
.action(async (options) => {
|
|
550
|
+
const loaded = await load();
|
|
551
|
+
print({
|
|
552
|
+
cachePath: loaded.paths.modelCache,
|
|
553
|
+
bytes: await directorySize(loaded.paths.modelCache),
|
|
554
|
+
}, options.json);
|
|
555
|
+
});
|
|
556
|
+
model
|
|
557
|
+
.command("clear")
|
|
558
|
+
.option("--json")
|
|
559
|
+
.action(async (options) => {
|
|
560
|
+
const loaded = await load();
|
|
561
|
+
await rm(loaded.paths.modelCache, { recursive: true, force: true });
|
|
562
|
+
print({ cleared: true, cachePath: loaded.paths.modelCache }, options.json);
|
|
563
|
+
});
|
|
564
|
+
const portrait = program.command("portrait");
|
|
565
|
+
portrait
|
|
566
|
+
.command("get")
|
|
567
|
+
.option("--json")
|
|
568
|
+
.action(async (options) => {
|
|
569
|
+
const instance = await runtime();
|
|
570
|
+
try {
|
|
571
|
+
print(await instance.getPortrait(), options.json);
|
|
572
|
+
}
|
|
573
|
+
finally {
|
|
574
|
+
instance.close();
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
portrait
|
|
578
|
+
.command("sync")
|
|
579
|
+
.option("--json")
|
|
580
|
+
.action(async (options) => {
|
|
581
|
+
const instance = await runtime();
|
|
582
|
+
try {
|
|
583
|
+
print(await instance.syncPortrait(), options.json);
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
instance.close();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
program
|
|
590
|
+
.command("status")
|
|
591
|
+
.option("--json")
|
|
592
|
+
.action(async (options) => {
|
|
593
|
+
const instance = await runtime();
|
|
594
|
+
try {
|
|
595
|
+
print(await instance.hubStatus(), options.json);
|
|
596
|
+
}
|
|
597
|
+
finally {
|
|
598
|
+
instance.close();
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
const clients = program.command("clients");
|
|
602
|
+
for (const action of ["install", "uninstall"]) {
|
|
603
|
+
const clientCommand = clients
|
|
604
|
+
.command(`${action} [client]`)
|
|
605
|
+
.option("--all")
|
|
606
|
+
.option("--desktop", "also merge Claude Desktop config");
|
|
607
|
+
if (action === "install") {
|
|
608
|
+
clientCommand.option("--no-scheduler", "register clients without enabling daily uploads");
|
|
609
|
+
}
|
|
610
|
+
clientCommand.option("--json").action(async (client, options) => {
|
|
611
|
+
const registry = new ClientRegistry(launch);
|
|
612
|
+
const targets = options.all
|
|
613
|
+
? ["claude", "codex", "grok"]
|
|
614
|
+
: client
|
|
615
|
+
? [client]
|
|
616
|
+
: [];
|
|
617
|
+
if (targets.length === 0)
|
|
618
|
+
throw new Error("Specify a client or --all");
|
|
619
|
+
for (const target of targets)
|
|
620
|
+
await registry.mutate(target, action);
|
|
621
|
+
if (action === "install" && options.desktop) {
|
|
622
|
+
const desktopPath = process.platform === "darwin"
|
|
623
|
+
? join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json")
|
|
624
|
+
: join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
625
|
+
await mergeClaudeDesktopConfig(desktopPath, launch);
|
|
626
|
+
}
|
|
627
|
+
let scheduler;
|
|
628
|
+
if (action === "install") {
|
|
629
|
+
if (options.scheduler === false) {
|
|
630
|
+
scheduler = { installed: false };
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
const loaded = await load();
|
|
634
|
+
const at = loaded.config.scheduler.at;
|
|
635
|
+
const syncAt = loaded.config.scheduler.syncAt;
|
|
636
|
+
await new SchedulerManager({
|
|
637
|
+
platform: process.platform,
|
|
638
|
+
homeDir: homedir(),
|
|
639
|
+
command: launch.command,
|
|
640
|
+
args: [...launch.args, "--config", loaded.configFile],
|
|
641
|
+
}).install(at, syncAt);
|
|
642
|
+
scheduler = { installed: true, at, syncAt };
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
print({
|
|
646
|
+
action,
|
|
647
|
+
clients: targets,
|
|
648
|
+
desktop: Boolean(options.desktop),
|
|
649
|
+
...(scheduler ? { scheduler } : {}),
|
|
650
|
+
}, options.json);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
clients
|
|
654
|
+
.command("status")
|
|
655
|
+
.option("--json")
|
|
656
|
+
.action(async (options) => {
|
|
657
|
+
const registry = new ClientRegistry(launch);
|
|
658
|
+
const result = Object.fromEntries(await Promise.all(["claude", "codex", "grok"].map(async (name) => [
|
|
659
|
+
name,
|
|
660
|
+
await registry.status(name),
|
|
661
|
+
])));
|
|
662
|
+
print(result, options.json);
|
|
663
|
+
});
|
|
664
|
+
const scheduler = program.command("scheduler");
|
|
665
|
+
scheduler
|
|
666
|
+
.command("install")
|
|
667
|
+
.option("--at <time>")
|
|
668
|
+
.option("--sync-at <time>")
|
|
669
|
+
.option("--json")
|
|
670
|
+
.action(async (options) => {
|
|
671
|
+
const loaded = await load();
|
|
672
|
+
const at = options.at ?? loaded.config.scheduler.at;
|
|
673
|
+
const syncAt = options.syncAt ?? loaded.config.scheduler.syncAt;
|
|
674
|
+
const manager = new SchedulerManager({
|
|
675
|
+
platform: process.platform,
|
|
676
|
+
homeDir: homedir(),
|
|
677
|
+
command: launch.command,
|
|
678
|
+
args: [...launch.args, "--config", loaded.configFile],
|
|
679
|
+
});
|
|
680
|
+
await manager.install(at, syncAt);
|
|
681
|
+
print({ installed: true, at, syncAt }, options.json);
|
|
682
|
+
});
|
|
683
|
+
scheduler
|
|
684
|
+
.command("uninstall")
|
|
685
|
+
.option("--json")
|
|
686
|
+
.action(async (options) => {
|
|
687
|
+
const manager = new SchedulerManager({
|
|
688
|
+
platform: process.platform,
|
|
689
|
+
homeDir: homedir(),
|
|
690
|
+
command: launch.command,
|
|
691
|
+
args: launch.args,
|
|
692
|
+
});
|
|
693
|
+
await manager.uninstall();
|
|
694
|
+
print({ installed: false }, options.json);
|
|
695
|
+
});
|
|
696
|
+
scheduler
|
|
697
|
+
.command("status")
|
|
698
|
+
.option("--json")
|
|
699
|
+
.action(async (options) => {
|
|
700
|
+
const manager = new SchedulerManager({
|
|
701
|
+
platform: process.platform,
|
|
702
|
+
homeDir: homedir(),
|
|
703
|
+
command: launch.command,
|
|
704
|
+
args: launch.args,
|
|
705
|
+
});
|
|
706
|
+
print(await manager.status(), options.json);
|
|
707
|
+
});
|
|
708
|
+
program
|
|
709
|
+
.command("uninstall")
|
|
710
|
+
.description("remove BrainHub MCP integrations and local state")
|
|
711
|
+
.option("--yes", "confirm removal without an interactive prompt")
|
|
712
|
+
.option("--json")
|
|
713
|
+
.action(async (options) => {
|
|
714
|
+
if (!options.yes) {
|
|
715
|
+
if (!process.stdin.isTTY) {
|
|
716
|
+
throw new BrainHubError("CONFIRMATION_REQUIRED", "Run `brainhub-mcp uninstall --yes` to confirm local data removal");
|
|
717
|
+
}
|
|
718
|
+
const prompt = createInterface({
|
|
719
|
+
input: process.stdin,
|
|
720
|
+
output: process.stdout,
|
|
721
|
+
});
|
|
722
|
+
try {
|
|
723
|
+
const answer = await prompt.question("移除客户端注册、授权、Keychain、配置、索引和模型缓存?Drive 与 Obsidian 内容会保留。[y/N] ");
|
|
724
|
+
if (!isAffirmativeConfirmation(answer)) {
|
|
725
|
+
print({ uninstalled: false, cancelled: true }, options.json);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
finally {
|
|
730
|
+
prompt.close();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const loaded = await load();
|
|
734
|
+
const secrets = secretsFor(loaded);
|
|
735
|
+
const credential = await secrets.get();
|
|
736
|
+
const registry = new ClientRegistry(launch);
|
|
737
|
+
const scheduler = new SchedulerManager({
|
|
738
|
+
platform: process.platform,
|
|
739
|
+
homeDir: homedir(),
|
|
740
|
+
command: launch.command,
|
|
741
|
+
args: [...launch.args, "--config", loaded.configFile],
|
|
742
|
+
});
|
|
743
|
+
const result = await new UninstallService({
|
|
744
|
+
platform: process.platform,
|
|
745
|
+
inspectClients: async () => Object.fromEntries(await Promise.all(["claude", "codex", "grok"].map(async (client) => [
|
|
746
|
+
client,
|
|
747
|
+
await registry.status(client),
|
|
748
|
+
]))),
|
|
749
|
+
unregisterClient: (client) => registry.mutate(client, "uninstall"),
|
|
750
|
+
uninstallScheduler: () => scheduler.uninstall(),
|
|
751
|
+
revokeGoogle: () => credential
|
|
752
|
+
? revokeGoogleCredential(credential)
|
|
753
|
+
: Promise.resolve(false),
|
|
754
|
+
clearKeychain: () => secrets.delete(),
|
|
755
|
+
removeLocalState: async () => {
|
|
756
|
+
const targets = localCleanupTargets(loaded.paths.stateFile, loaded.paths.modelCache);
|
|
757
|
+
await Promise.all([
|
|
758
|
+
rm(loaded.configFile, { force: true }),
|
|
759
|
+
rm(targets.dataDirectory, { recursive: true, force: true }),
|
|
760
|
+
rm(targets.modelCache, { recursive: true, force: true }),
|
|
761
|
+
]);
|
|
762
|
+
},
|
|
763
|
+
}).run();
|
|
764
|
+
print(result, options.json);
|
|
765
|
+
});
|
|
766
|
+
await program.parseAsync(argv);
|
|
767
|
+
}
|
|
768
|
+
const invokedDirectly = process.argv[1] &&
|
|
769
|
+
import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
|
|
770
|
+
if (invokedDirectly) {
|
|
771
|
+
runCli().catch((error) => {
|
|
772
|
+
const candidate = error;
|
|
773
|
+
const code = typeof candidate.code === "string" ? candidate.code : "INTERNAL_ERROR";
|
|
774
|
+
const message = typeof candidate.message === "string"
|
|
775
|
+
? candidate.message
|
|
776
|
+
: "BrainHub command failed";
|
|
777
|
+
process.stderr.write(`${code}: ${message}\n`);
|
|
778
|
+
process.exitCode = 1;
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
//# sourceMappingURL=index.js.map
|