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.
Files changed (156) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +84 -0
  3. package/dist/adapters/claude.d.ts +2 -0
  4. package/dist/adapters/claude.js +50 -0
  5. package/dist/adapters/claude.js.map +1 -0
  6. package/dist/adapters/codex.d.ts +2 -0
  7. package/dist/adapters/codex.js +52 -0
  8. package/dist/adapters/codex.js.map +1 -0
  9. package/dist/adapters/content.d.ts +6 -0
  10. package/dist/adapters/content.js +68 -0
  11. package/dist/adapters/content.js.map +1 -0
  12. package/dist/adapters/grok.d.ts +2 -0
  13. package/dist/adapters/grok.js +44 -0
  14. package/dist/adapters/grok.js.map +1 -0
  15. package/dist/adapters/index.d.ts +31 -0
  16. package/dist/adapters/index.js +111 -0
  17. package/dist/adapters/index.js.map +1 -0
  18. package/dist/adapters/jsonl.d.ts +6 -0
  19. package/dist/adapters/jsonl.js +26 -0
  20. package/dist/adapters/jsonl.js.map +1 -0
  21. package/dist/adapters/types.d.ts +10 -0
  22. package/dist/adapters/types.js +2 -0
  23. package/dist/adapters/types.js.map +1 -0
  24. package/dist/auth/account-status.d.ts +50 -0
  25. package/dist/auth/account-status.js +59 -0
  26. package/dist/auth/account-status.js.map +1 -0
  27. package/dist/auth/google-account.d.ts +21 -0
  28. package/dist/auth/google-account.js +45 -0
  29. package/dist/auth/google-account.js.map +1 -0
  30. package/dist/auth/google-oauth.d.ts +35 -0
  31. package/dist/auth/google-oauth.js +191 -0
  32. package/dist/auth/google-oauth.js.map +1 -0
  33. package/dist/auth/oauth-loopback.d.ts +10 -0
  34. package/dist/auth/oauth-loopback.js +81 -0
  35. package/dist/auth/oauth-loopback.js.map +1 -0
  36. package/dist/auth/official-oauth-client.d.ts +5 -0
  37. package/dist/auth/official-oauth-client.js +6 -0
  38. package/dist/auth/official-oauth-client.js.map +1 -0
  39. package/dist/auth/platform-secrets.d.ts +14 -0
  40. package/dist/auth/platform-secrets.js +81 -0
  41. package/dist/auth/platform-secrets.js.map +1 -0
  42. package/dist/auth/secret-store-factory.d.ts +11 -0
  43. package/dist/auth/secret-store-factory.js +37 -0
  44. package/dist/auth/secret-store-factory.js.map +1 -0
  45. package/dist/auth/secret-store.d.ts +13 -0
  46. package/dist/auth/secret-store.js +40 -0
  47. package/dist/auth/secret-store.js.map +1 -0
  48. package/dist/capture/images.d.ts +11 -0
  49. package/dist/capture/images.js +27 -0
  50. package/dist/capture/images.js.map +1 -0
  51. package/dist/capture/normalize.d.ts +11 -0
  52. package/dist/capture/normalize.js +43 -0
  53. package/dist/capture/normalize.js.map +1 -0
  54. package/dist/capture/redact.d.ts +10 -0
  55. package/dist/capture/redact.js +60 -0
  56. package/dist/capture/redact.js.map +1 -0
  57. package/dist/cli/index.d.ts +20 -0
  58. package/dist/cli/index.js +781 -0
  59. package/dist/cli/index.js.map +1 -0
  60. package/dist/clients/registry.d.ts +24 -0
  61. package/dist/clients/registry.js +105 -0
  62. package/dist/clients/registry.js.map +1 -0
  63. package/dist/domain/config-io.d.ts +15 -0
  64. package/dist/domain/config-io.js +168 -0
  65. package/dist/domain/config-io.js.map +1 -0
  66. package/dist/domain/config.d.ts +74 -0
  67. package/dist/domain/config.js +130 -0
  68. package/dist/domain/config.js.map +1 -0
  69. package/dist/domain/errors.d.ts +6 -0
  70. package/dist/domain/errors.js +11 -0
  71. package/dist/domain/errors.js.map +1 -0
  72. package/dist/domain/session.d.ts +62 -0
  73. package/dist/domain/session.js +47 -0
  74. package/dist/domain/session.js.map +1 -0
  75. package/dist/drive/drive-port.d.ts +51 -0
  76. package/dist/drive/drive-port.js +2 -0
  77. package/dist/drive/drive-port.js.map +1 -0
  78. package/dist/drive/google-drive.d.ts +28 -0
  79. package/dist/drive/google-drive.js +501 -0
  80. package/dist/drive/google-drive.js.map +1 -0
  81. package/dist/drive/memory-drive.d.ts +19 -0
  82. package/dist/drive/memory-drive.js +148 -0
  83. package/dist/drive/memory-drive.js.map +1 -0
  84. package/dist/drive/scoped-drive.d.ts +17 -0
  85. package/dist/drive/scoped-drive.js +115 -0
  86. package/dist/drive/scoped-drive.js.map +1 -0
  87. package/dist/mcp/server.d.ts +55 -0
  88. package/dist/mcp/server.js +133 -0
  89. package/dist/mcp/server.js.map +1 -0
  90. package/dist/portrait/obsidian.d.ts +7 -0
  91. package/dist/portrait/obsidian.js +16 -0
  92. package/dist/portrait/obsidian.js.map +1 -0
  93. package/dist/portrait/portrait-service.d.ts +12 -0
  94. package/dist/portrait/portrait-service.js +18 -0
  95. package/dist/portrait/portrait-service.js.map +1 -0
  96. package/dist/portrait/portrait-sync-service.d.ts +21 -0
  97. package/dist/portrait/portrait-sync-service.js +43 -0
  98. package/dist/portrait/portrait-sync-service.js.map +1 -0
  99. package/dist/runtime/command.d.ts +4 -0
  100. package/dist/runtime/command.js +2 -0
  101. package/dist/runtime/command.js.map +1 -0
  102. package/dist/runtime/container.d.ts +72 -0
  103. package/dist/runtime/container.js +361 -0
  104. package/dist/runtime/container.js.map +1 -0
  105. package/dist/scheduler/jobs.d.ts +2 -0
  106. package/dist/scheduler/jobs.js +7 -0
  107. package/dist/scheduler/jobs.js.map +1 -0
  108. package/dist/scheduler/launchd.d.ts +7 -0
  109. package/dist/scheduler/launchd.js +40 -0
  110. package/dist/scheduler/launchd.js.map +1 -0
  111. package/dist/scheduler/manager.d.ts +26 -0
  112. package/dist/scheduler/manager.js +113 -0
  113. package/dist/scheduler/manager.js.map +1 -0
  114. package/dist/search/chunk-text.d.ts +11 -0
  115. package/dist/search/chunk-text.js +40 -0
  116. package/dist/search/chunk-text.js.map +1 -0
  117. package/dist/search/e5-embedder.d.ts +19 -0
  118. package/dist/search/e5-embedder.js +71 -0
  119. package/dist/search/e5-embedder.js.map +1 -0
  120. package/dist/search/embedder.d.ts +8 -0
  121. package/dist/search/embedder.js +5 -0
  122. package/dist/search/embedder.js.map +1 -0
  123. package/dist/search/model-cache.d.ts +7 -0
  124. package/dist/search/model-cache.js +38 -0
  125. package/dist/search/model-cache.js.map +1 -0
  126. package/dist/search/search-service.d.ts +46 -0
  127. package/dist/search/search-service.js +319 -0
  128. package/dist/search/search-service.js.map +1 -0
  129. package/dist/setup/backfill-service.d.ts +33 -0
  130. package/dist/setup/backfill-service.js +47 -0
  131. package/dist/setup/backfill-service.js.map +1 -0
  132. package/dist/setup/setup-service.d.ts +65 -0
  133. package/dist/setup/setup-service.js +90 -0
  134. package/dist/setup/setup-service.js.map +1 -0
  135. package/dist/setup/uninstall-service.d.ts +34 -0
  136. package/dist/setup/uninstall-service.js +91 -0
  137. package/dist/setup/uninstall-service.js.map +1 -0
  138. package/dist/state/sqlite-store.d.ts +19 -0
  139. package/dist/state/sqlite-store.js +299 -0
  140. package/dist/state/sqlite-store.js.map +1 -0
  141. package/dist/state/store.d.ts +52 -0
  142. package/dist/state/store.js +2 -0
  143. package/dist/state/store.js.map +1 -0
  144. package/dist/status/status-service.d.ts +38 -0
  145. package/dist/status/status-service.js +93 -0
  146. package/dist/status/status-service.js.map +1 -0
  147. package/dist/update/update-check-service.d.ts +38 -0
  148. package/dist/update/update-check-service.js +105 -0
  149. package/dist/update/update-check-service.js.map +1 -0
  150. package/dist/upload/lock.d.ts +7 -0
  151. package/dist/upload/lock.js +68 -0
  152. package/dist/upload/lock.js.map +1 -0
  153. package/dist/upload/upload-service.d.ts +36 -0
  154. package/dist/upload/upload-service.js +360 -0
  155. package/dist/upload/upload-service.js.map +1 -0
  156. 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