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,47 @@
|
|
|
1
|
+
function result(state) {
|
|
2
|
+
return {
|
|
3
|
+
sessions: state.sessions,
|
|
4
|
+
bytes: state.bytes,
|
|
5
|
+
confirmed: state.decision === "accepted",
|
|
6
|
+
uploaded: state.uploaded,
|
|
7
|
+
completed: state.completedAt !== null,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export class BackfillService {
|
|
11
|
+
dependencies;
|
|
12
|
+
#now;
|
|
13
|
+
constructor(dependencies) {
|
|
14
|
+
this.dependencies = dependencies;
|
|
15
|
+
this.#now = dependencies.now ?? (() => new Date().toISOString());
|
|
16
|
+
}
|
|
17
|
+
async run() {
|
|
18
|
+
let state = this.dependencies.state.getBackfillState();
|
|
19
|
+
if (state?.completedAt)
|
|
20
|
+
return result(state);
|
|
21
|
+
if (!state) {
|
|
22
|
+
const summary = await this.dependencies.inspect();
|
|
23
|
+
this.dependencies.report(summary);
|
|
24
|
+
const accepted = summary.sessions === 0 || (await this.dependencies.confirm(summary));
|
|
25
|
+
this.dependencies.state.recordBackfillDecision({
|
|
26
|
+
decision: accepted ? "accepted" : "declined",
|
|
27
|
+
...summary,
|
|
28
|
+
decidedAt: this.#now(),
|
|
29
|
+
});
|
|
30
|
+
state = this.dependencies.state.getBackfillState();
|
|
31
|
+
if (!state)
|
|
32
|
+
throw new Error("Backfill decision was not persisted");
|
|
33
|
+
}
|
|
34
|
+
if (state.decision === "declined")
|
|
35
|
+
return result(state);
|
|
36
|
+
const uploaded = await this.dependencies.upload();
|
|
37
|
+
this.dependencies.state.recordBackfillUpload({
|
|
38
|
+
...uploaded,
|
|
39
|
+
recordedAt: this.#now(),
|
|
40
|
+
});
|
|
41
|
+
const updated = this.dependencies.state.getBackfillState();
|
|
42
|
+
if (!updated)
|
|
43
|
+
throw new Error("Backfill upload result was not persisted");
|
|
44
|
+
return result(updated);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=backfill-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backfill-service.js","sourceRoot":"","sources":["../../src/setup/backfill-service.ts"],"names":[],"mappings":"AAgCA,SAAS,MAAM,CAAC,KAAoB;IAClC,OAAO;QACL,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,SAAS,EAAE,KAAK,CAAC,QAAQ,KAAK,UAAU;QACxC,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,SAAS,EAAE,KAAK,CAAC,WAAW,KAAK,IAAI;KACtC,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,eAAe;IAGL;IAFZ,IAAI,CAAe;IAE5B,YAAqB,YAAkC;QAAlC,iBAAY,GAAZ,YAAY,CAAsB;QACrD,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,GAAG;QACP,IAAI,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC;QACvD,IAAI,KAAK,EAAE,WAAW;YAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;QAE7C,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;YAClD,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAClC,MAAM,QAAQ,GACZ,OAAO,CAAC,QAAQ,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;YACvE,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,sBAAsB,CAAC;gBAC7C,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU;gBAC5C,GAAG,OAAO;gBACV,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE;aACvB,CAAC,CAAC;YACH,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC;YACnD,IAAI,CAAC,KAAK;gBAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACrE,CAAC;QAED,IAAI,KAAK,CAAC,QAAQ,KAAK,UAAU;YAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;QAExD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;QAClD,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,oBAAoB,CAAC;YAC3C,GAAG,QAAQ;YACX,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE;SACxB,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC;QAC3D,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAC1E,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ClientName } from "../clients/registry.js";
|
|
2
|
+
import type { BackfillResult } from "./backfill-service.js";
|
|
3
|
+
export type { BackfillSummary } from "./backfill-service.js";
|
|
4
|
+
export interface SetupProgress {
|
|
5
|
+
type: "model-progress" | "backfill-summary";
|
|
6
|
+
percent?: number;
|
|
7
|
+
file?: string;
|
|
8
|
+
sessions?: number;
|
|
9
|
+
bytes?: number;
|
|
10
|
+
}
|
|
11
|
+
type ClientStatuses = Record<ClientName, {
|
|
12
|
+
available: boolean;
|
|
13
|
+
registered: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
export interface SetupDependencies {
|
|
16
|
+
platform: NodeJS.Platform;
|
|
17
|
+
ensureConfig(): Promise<void>;
|
|
18
|
+
connectGoogle(): Promise<{
|
|
19
|
+
email: string;
|
|
20
|
+
}>;
|
|
21
|
+
prepareModel(progress: (value: {
|
|
22
|
+
percent?: number;
|
|
23
|
+
file?: string;
|
|
24
|
+
}) => void): Promise<void>;
|
|
25
|
+
runBackfill(): Promise<BackfillResult>;
|
|
26
|
+
inspectClients(): Promise<ClientStatuses>;
|
|
27
|
+
registerClient(client: ClientName): Promise<void>;
|
|
28
|
+
installScheduler(options: {
|
|
29
|
+
portrait: boolean;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
inspectObsidian(): Promise<{
|
|
32
|
+
configured: boolean;
|
|
33
|
+
path?: string;
|
|
34
|
+
}>;
|
|
35
|
+
report(progress: SetupProgress): void;
|
|
36
|
+
}
|
|
37
|
+
export interface SetupOutput {
|
|
38
|
+
account: {
|
|
39
|
+
email: string;
|
|
40
|
+
};
|
|
41
|
+
model: {
|
|
42
|
+
ready: boolean;
|
|
43
|
+
};
|
|
44
|
+
backfill: BackfillResult;
|
|
45
|
+
clients: {
|
|
46
|
+
registered: ClientName[];
|
|
47
|
+
skipped: ClientName[];
|
|
48
|
+
};
|
|
49
|
+
scheduler: {
|
|
50
|
+
installed: boolean;
|
|
51
|
+
};
|
|
52
|
+
obsidian: {
|
|
53
|
+
configured: boolean;
|
|
54
|
+
path?: string;
|
|
55
|
+
};
|
|
56
|
+
warnings: Array<{
|
|
57
|
+
code: string;
|
|
58
|
+
message: string;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
export declare class SetupService {
|
|
62
|
+
readonly dependencies: SetupDependencies;
|
|
63
|
+
constructor(dependencies: SetupDependencies);
|
|
64
|
+
run(): Promise<SetupOutput>;
|
|
65
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { BrainHubError } from "../domain/errors.js";
|
|
2
|
+
function message(error) {
|
|
3
|
+
return error instanceof Error ? error.message : "Unknown setup error";
|
|
4
|
+
}
|
|
5
|
+
export class SetupService {
|
|
6
|
+
dependencies;
|
|
7
|
+
constructor(dependencies) {
|
|
8
|
+
this.dependencies = dependencies;
|
|
9
|
+
}
|
|
10
|
+
async run() {
|
|
11
|
+
if (this.dependencies.platform !== "darwin") {
|
|
12
|
+
throw new BrainHubError("PLATFORM_UNSUPPORTED", "BrainHub MCP v1 supports macOS only");
|
|
13
|
+
}
|
|
14
|
+
const warnings = [];
|
|
15
|
+
await this.dependencies.ensureConfig();
|
|
16
|
+
const account = await this.dependencies.connectGoogle();
|
|
17
|
+
let modelReady = true;
|
|
18
|
+
try {
|
|
19
|
+
await this.dependencies.prepareModel((progress) => this.dependencies.report({ type: "model-progress", ...progress }));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
modelReady = false;
|
|
23
|
+
warnings.push({
|
|
24
|
+
code: "MODEL_DOWNLOAD_FAILED",
|
|
25
|
+
message: message(error),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const backfill = await this.dependencies.runBackfill();
|
|
29
|
+
if (backfill.confirmed && !backfill.completed) {
|
|
30
|
+
warnings.push({
|
|
31
|
+
code: "BACKFILL_INCOMPLETE",
|
|
32
|
+
message: "History backfill has retryable uploads remaining",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const statuses = await this.dependencies.inspectClients();
|
|
36
|
+
const registered = [];
|
|
37
|
+
const skipped = [];
|
|
38
|
+
for (const client of ["claude", "codex", "grok"]) {
|
|
39
|
+
const status = statuses[client];
|
|
40
|
+
if (!status.available || status.registered) {
|
|
41
|
+
skipped.push(client);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await this.dependencies.registerClient(client);
|
|
46
|
+
registered.push(client);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
warnings.push({
|
|
50
|
+
code: "CLIENT_REGISTRATION_FAILED",
|
|
51
|
+
message: `${client}: ${message(error)}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let obsidian;
|
|
56
|
+
try {
|
|
57
|
+
obsidian = await this.dependencies.inspectObsidian();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
obsidian = { configured: false };
|
|
61
|
+
warnings.push({
|
|
62
|
+
code: "OBSIDIAN_DISCOVERY_FAILED",
|
|
63
|
+
message: message(error),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
let schedulerInstalled = true;
|
|
67
|
+
try {
|
|
68
|
+
await this.dependencies.installScheduler({
|
|
69
|
+
portrait: obsidian.configured,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
schedulerInstalled = false;
|
|
74
|
+
warnings.push({
|
|
75
|
+
code: "SCHEDULER_INSTALL_FAILED",
|
|
76
|
+
message: message(error),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
account,
|
|
81
|
+
model: { ready: modelReady },
|
|
82
|
+
backfill,
|
|
83
|
+
clients: { registered, skipped },
|
|
84
|
+
scheduler: { installed: schedulerInstalled },
|
|
85
|
+
obsidian,
|
|
86
|
+
warnings,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=setup-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup-service.js","sourceRoot":"","sources":["../../src/setup/setup-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ClientName } from "../clients/registry.js";
|
|
2
|
+
type ClientStatuses = Record<ClientName, {
|
|
3
|
+
available: boolean;
|
|
4
|
+
registered: boolean;
|
|
5
|
+
}>;
|
|
6
|
+
interface UninstallDependencies {
|
|
7
|
+
platform: NodeJS.Platform;
|
|
8
|
+
inspectClients(): Promise<ClientStatuses>;
|
|
9
|
+
unregisterClient(client: ClientName): Promise<void>;
|
|
10
|
+
uninstallScheduler(): Promise<void>;
|
|
11
|
+
revokeGoogle(): Promise<boolean>;
|
|
12
|
+
clearKeychain(): Promise<void>;
|
|
13
|
+
removeLocalState(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface UninstallOutput {
|
|
16
|
+
uninstalled: boolean;
|
|
17
|
+
clients: ClientName[];
|
|
18
|
+
oauthRevoked: boolean;
|
|
19
|
+
warnings: Array<{
|
|
20
|
+
code: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export declare function brainHubDataDirectory(accountStateFile: string): string;
|
|
25
|
+
export declare function localCleanupTargets(accountStateFile: string, modelCache: string): {
|
|
26
|
+
dataDirectory: string;
|
|
27
|
+
modelCache: string;
|
|
28
|
+
};
|
|
29
|
+
export declare class UninstallService {
|
|
30
|
+
readonly dependencies: UninstallDependencies;
|
|
31
|
+
constructor(dependencies: UninstallDependencies);
|
|
32
|
+
run(): Promise<UninstallOutput>;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { BrainHubError } from "../domain/errors.js";
|
|
3
|
+
export function brainHubDataDirectory(accountStateFile) {
|
|
4
|
+
return dirname(dirname(dirname(accountStateFile)));
|
|
5
|
+
}
|
|
6
|
+
export function localCleanupTargets(accountStateFile, modelCache) {
|
|
7
|
+
return {
|
|
8
|
+
dataDirectory: brainHubDataDirectory(accountStateFile),
|
|
9
|
+
modelCache,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function message(error) {
|
|
13
|
+
return error instanceof Error ? error.message : "Unknown uninstall error";
|
|
14
|
+
}
|
|
15
|
+
export class UninstallService {
|
|
16
|
+
dependencies;
|
|
17
|
+
constructor(dependencies) {
|
|
18
|
+
this.dependencies = dependencies;
|
|
19
|
+
}
|
|
20
|
+
async run() {
|
|
21
|
+
if (this.dependencies.platform !== "darwin") {
|
|
22
|
+
throw new BrainHubError("PLATFORM_UNSUPPORTED", "BrainHub MCP v1 supports macOS only");
|
|
23
|
+
}
|
|
24
|
+
const warnings = [];
|
|
25
|
+
const removedClients = [];
|
|
26
|
+
try {
|
|
27
|
+
const statuses = await this.dependencies.inspectClients();
|
|
28
|
+
for (const client of ["claude", "codex", "grok"]) {
|
|
29
|
+
if (!statuses[client].registered)
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
await this.dependencies.unregisterClient(client);
|
|
33
|
+
removedClients.push(client);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
warnings.push({
|
|
37
|
+
code: "CLIENT_UNINSTALL_FAILED",
|
|
38
|
+
message: `${client}: ${message(error)}`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
warnings.push({
|
|
45
|
+
code: "CLIENT_STATUS_FAILED",
|
|
46
|
+
message: message(error),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await this.dependencies.uninstallScheduler();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
warnings.push({
|
|
54
|
+
code: "SCHEDULER_UNINSTALL_FAILED",
|
|
55
|
+
message: message(error),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
let oauthRevoked = false;
|
|
59
|
+
try {
|
|
60
|
+
oauthRevoked = await this.dependencies.revokeGoogle();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
warnings.push({
|
|
64
|
+
code: "OAUTH_REVOCATION_FAILED",
|
|
65
|
+
message: message(error),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
let localCleanupComplete = true;
|
|
69
|
+
try {
|
|
70
|
+
await this.dependencies.clearKeychain();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
localCleanupComplete = false;
|
|
74
|
+
warnings.push({ code: "KEYCHAIN_CLEAR_FAILED", message: message(error) });
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await this.dependencies.removeLocalState();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
localCleanupComplete = false;
|
|
81
|
+
warnings.push({ code: "LOCAL_CLEANUP_FAILED", message: message(error) });
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
uninstalled: localCleanupComplete,
|
|
85
|
+
clients: removedClients,
|
|
86
|
+
oauthRevoked,
|
|
87
|
+
warnings,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=uninstall-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uninstall-service.js","sourceRoot":"","sources":["../../src/setup/uninstall-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BackfillDecisionInput, BackfillState, BackfillUploadInput, DeviceState, PendingSession, SessionState, StateStore } from "./store.js";
|
|
2
|
+
export declare class SqliteStateStore implements StateStore {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(path: string, options?: {
|
|
5
|
+
legacyPath?: string;
|
|
6
|
+
});
|
|
7
|
+
getOrCreateDevice(name: string): DeviceState;
|
|
8
|
+
markPending(session: PendingSession): boolean;
|
|
9
|
+
markFailed(conversationKey: string, errorCode: string, retryable: boolean): void;
|
|
10
|
+
markUploaded(conversationKey: string, driveFileId: string, uploadedAt: string): void;
|
|
11
|
+
getSession(conversationKey: string): SessionState | null;
|
|
12
|
+
listPending(limit: number): SessionState[];
|
|
13
|
+
getDiscoveryWatermark(source: SessionState["source"]): string | null;
|
|
14
|
+
setDiscoveryWatermark(source: SessionState["source"], scannedAt: string): void;
|
|
15
|
+
getBackfillState(): BackfillState | null;
|
|
16
|
+
recordBackfillDecision(input: BackfillDecisionInput): void;
|
|
17
|
+
recordBackfillUpload(input: BackfillUploadInput): void;
|
|
18
|
+
close(): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
function mapSession(row) {
|
|
6
|
+
return {
|
|
7
|
+
conversationKey: row.conversation_key,
|
|
8
|
+
source: row.source,
|
|
9
|
+
conversationId: row.conversation_id,
|
|
10
|
+
sourcePath: row.source_path,
|
|
11
|
+
sourceUpdatedAt: row.source_updated_at,
|
|
12
|
+
contentSha256: row.content_sha256,
|
|
13
|
+
status: row.status,
|
|
14
|
+
attempts: row.attempts,
|
|
15
|
+
retryable: row.retryable === 1,
|
|
16
|
+
driveFileId: row.drive_file_id,
|
|
17
|
+
uploadedAt: row.uploaded_at,
|
|
18
|
+
lastErrorCode: row.last_error_code,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export class SqliteStateStore {
|
|
22
|
+
#database;
|
|
23
|
+
constructor(path, options = {}) {
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
25
|
+
this.#database = new Database(path);
|
|
26
|
+
this.#database.pragma("journal_mode = WAL");
|
|
27
|
+
this.#database.pragma("foreign_keys = ON");
|
|
28
|
+
this.#migrate();
|
|
29
|
+
if (options.legacyPath)
|
|
30
|
+
this.#migrateLegacy(options.legacyPath, path);
|
|
31
|
+
}
|
|
32
|
+
#migrate() {
|
|
33
|
+
this.#database.exec(`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS device (
|
|
35
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
36
|
+
id TEXT NOT NULL,
|
|
37
|
+
name TEXT NOT NULL,
|
|
38
|
+
created_at TEXT NOT NULL
|
|
39
|
+
);
|
|
40
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
41
|
+
conversation_key TEXT PRIMARY KEY,
|
|
42
|
+
source TEXT NOT NULL,
|
|
43
|
+
conversation_id TEXT NOT NULL,
|
|
44
|
+
source_path TEXT NOT NULL,
|
|
45
|
+
source_updated_at TEXT NOT NULL,
|
|
46
|
+
content_sha256 TEXT NOT NULL,
|
|
47
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'uploaded', 'failed')),
|
|
48
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
retryable INTEGER NOT NULL DEFAULT 1,
|
|
50
|
+
drive_file_id TEXT,
|
|
51
|
+
uploaded_at TEXT,
|
|
52
|
+
last_error_code TEXT
|
|
53
|
+
);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS sessions_pending
|
|
55
|
+
ON sessions(status, retryable, source_updated_at);
|
|
56
|
+
CREATE TABLE IF NOT EXISTS discovery_watermarks (
|
|
57
|
+
source TEXT PRIMARY KEY,
|
|
58
|
+
scanned_at TEXT NOT NULL
|
|
59
|
+
);
|
|
60
|
+
CREATE TABLE IF NOT EXISTS state_migrations (
|
|
61
|
+
name TEXT PRIMARY KEY,
|
|
62
|
+
applied_at TEXT NOT NULL
|
|
63
|
+
);
|
|
64
|
+
CREATE TABLE IF NOT EXISTS setup_backfill (
|
|
65
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
66
|
+
decision TEXT NOT NULL CHECK (decision IN ('accepted', 'declined')),
|
|
67
|
+
sessions INTEGER NOT NULL,
|
|
68
|
+
bytes INTEGER NOT NULL,
|
|
69
|
+
uploaded INTEGER NOT NULL DEFAULT 0,
|
|
70
|
+
decided_at TEXT NOT NULL,
|
|
71
|
+
completed_at TEXT
|
|
72
|
+
);
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
#migrateLegacy(legacyPath, currentPath) {
|
|
76
|
+
const migration = "legacy-account-state-v1";
|
|
77
|
+
const sourceClaim = `${migration}-claimed`;
|
|
78
|
+
if (resolve(legacyPath) === resolve(currentPath) ||
|
|
79
|
+
!existsSync(legacyPath)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.#database.prepare("ATTACH DATABASE ? AS legacy_state").run(legacyPath);
|
|
83
|
+
try {
|
|
84
|
+
const requiredTables = ["device", "sessions", "discovery_watermarks"];
|
|
85
|
+
const available = new Set(this.#database
|
|
86
|
+
.prepare("SELECT name FROM legacy_state.sqlite_master WHERE type = 'table'")
|
|
87
|
+
.all().map((row) => row.name));
|
|
88
|
+
if (!requiredTables.every((table) => available.has(table)))
|
|
89
|
+
return;
|
|
90
|
+
this.#database.exec(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS legacy_state.state_migrations (
|
|
92
|
+
name TEXT PRIMARY KEY,
|
|
93
|
+
applied_at TEXT NOT NULL
|
|
94
|
+
);
|
|
95
|
+
`);
|
|
96
|
+
const targetAlreadyMigrated = Boolean(this.#database
|
|
97
|
+
.prepare("SELECT 1 FROM state_migrations WHERE name = ?")
|
|
98
|
+
.get(migration));
|
|
99
|
+
if (targetAlreadyMigrated) {
|
|
100
|
+
this.#database
|
|
101
|
+
.prepare("INSERT OR IGNORE INTO legacy_state.state_migrations(name, applied_at) VALUES (?, ?)")
|
|
102
|
+
.run(sourceClaim, new Date().toISOString());
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.#database.transaction(() => {
|
|
106
|
+
const claimed = this.#database
|
|
107
|
+
.prepare("INSERT OR IGNORE INTO legacy_state.state_migrations(name, applied_at) VALUES (?, ?)")
|
|
108
|
+
.run(sourceClaim, new Date().toISOString());
|
|
109
|
+
if (claimed.changes === 0)
|
|
110
|
+
return;
|
|
111
|
+
this.#database.exec(`
|
|
112
|
+
INSERT OR REPLACE INTO device(singleton, id, name, created_at)
|
|
113
|
+
SELECT singleton, id, name, created_at FROM legacy_state.device;
|
|
114
|
+
|
|
115
|
+
INSERT OR IGNORE INTO sessions(
|
|
116
|
+
conversation_key, source, conversation_id, source_path,
|
|
117
|
+
source_updated_at, content_sha256, status, attempts, retryable,
|
|
118
|
+
drive_file_id, uploaded_at, last_error_code
|
|
119
|
+
)
|
|
120
|
+
SELECT
|
|
121
|
+
conversation_key, source, conversation_id, source_path,
|
|
122
|
+
source_updated_at, content_sha256, status, attempts, retryable,
|
|
123
|
+
drive_file_id, uploaded_at, last_error_code
|
|
124
|
+
FROM legacy_state.sessions;
|
|
125
|
+
|
|
126
|
+
INSERT INTO discovery_watermarks(source, scanned_at)
|
|
127
|
+
SELECT source, scanned_at FROM legacy_state.discovery_watermarks
|
|
128
|
+
WHERE true
|
|
129
|
+
ON CONFLICT(source) DO UPDATE SET scanned_at =
|
|
130
|
+
MIN(discovery_watermarks.scanned_at, excluded.scanned_at);
|
|
131
|
+
`);
|
|
132
|
+
this.#database
|
|
133
|
+
.prepare("INSERT INTO state_migrations(name, applied_at) VALUES (?, ?)")
|
|
134
|
+
.run(migration, new Date().toISOString());
|
|
135
|
+
})();
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
this.#database.exec("DETACH DATABASE legacy_state");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
getOrCreateDevice(name) {
|
|
142
|
+
const existing = this.#database
|
|
143
|
+
.prepare("SELECT id, name, created_at AS createdAt FROM device WHERE singleton = 1")
|
|
144
|
+
.get();
|
|
145
|
+
if (existing) {
|
|
146
|
+
if (existing.name !== name) {
|
|
147
|
+
this.#database
|
|
148
|
+
.prepare("UPDATE device SET name = ? WHERE singleton = 1")
|
|
149
|
+
.run(name);
|
|
150
|
+
}
|
|
151
|
+
return { ...existing, name };
|
|
152
|
+
}
|
|
153
|
+
const device = {
|
|
154
|
+
id: randomUUID(),
|
|
155
|
+
name,
|
|
156
|
+
createdAt: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
this.#database
|
|
159
|
+
.prepare("INSERT INTO device(singleton, id, name, created_at) VALUES (1, ?, ?, ?)")
|
|
160
|
+
.run(device.id, device.name, device.createdAt);
|
|
161
|
+
return device;
|
|
162
|
+
}
|
|
163
|
+
markPending(session) {
|
|
164
|
+
const current = this.getSession(session.conversationKey);
|
|
165
|
+
if (current?.status === "uploaded" &&
|
|
166
|
+
current.contentSha256 === session.contentSha256) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
this.#database
|
|
170
|
+
.prepare(`
|
|
171
|
+
INSERT INTO sessions(
|
|
172
|
+
conversation_key, source, conversation_id, source_path,
|
|
173
|
+
source_updated_at, content_sha256, status, attempts, retryable
|
|
174
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'pending', 0, 1)
|
|
175
|
+
ON CONFLICT(conversation_key) DO UPDATE SET
|
|
176
|
+
source = excluded.source,
|
|
177
|
+
conversation_id = excluded.conversation_id,
|
|
178
|
+
source_path = excluded.source_path,
|
|
179
|
+
source_updated_at = excluded.source_updated_at,
|
|
180
|
+
content_sha256 = excluded.content_sha256,
|
|
181
|
+
status = 'pending',
|
|
182
|
+
attempts = CASE
|
|
183
|
+
WHEN sessions.content_sha256 = excluded.content_sha256 THEN sessions.attempts
|
|
184
|
+
ELSE 0
|
|
185
|
+
END,
|
|
186
|
+
retryable = 1,
|
|
187
|
+
drive_file_id = CASE
|
|
188
|
+
WHEN sessions.content_sha256 = excluded.content_sha256 THEN sessions.drive_file_id
|
|
189
|
+
ELSE NULL
|
|
190
|
+
END,
|
|
191
|
+
uploaded_at = CASE
|
|
192
|
+
WHEN sessions.content_sha256 = excluded.content_sha256 THEN sessions.uploaded_at
|
|
193
|
+
ELSE NULL
|
|
194
|
+
END,
|
|
195
|
+
last_error_code = NULL
|
|
196
|
+
`)
|
|
197
|
+
.run(session.conversationKey, session.source, session.conversationId, session.sourcePath, session.sourceUpdatedAt, session.contentSha256);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
markFailed(conversationKey, errorCode, retryable) {
|
|
201
|
+
this.#database
|
|
202
|
+
.prepare(`
|
|
203
|
+
UPDATE sessions
|
|
204
|
+
SET status = 'failed', attempts = attempts + 1,
|
|
205
|
+
retryable = ?, last_error_code = ?
|
|
206
|
+
WHERE conversation_key = ?
|
|
207
|
+
`)
|
|
208
|
+
.run(retryable ? 1 : 0, errorCode, conversationKey);
|
|
209
|
+
}
|
|
210
|
+
markUploaded(conversationKey, driveFileId, uploadedAt) {
|
|
211
|
+
this.#database
|
|
212
|
+
.prepare(`
|
|
213
|
+
UPDATE sessions
|
|
214
|
+
SET status = 'uploaded', retryable = 0, drive_file_id = ?,
|
|
215
|
+
uploaded_at = ?, last_error_code = NULL
|
|
216
|
+
WHERE conversation_key = ?
|
|
217
|
+
`)
|
|
218
|
+
.run(driveFileId, uploadedAt, conversationKey);
|
|
219
|
+
}
|
|
220
|
+
getSession(conversationKey) {
|
|
221
|
+
const row = this.#database
|
|
222
|
+
.prepare("SELECT * FROM sessions WHERE conversation_key = ?")
|
|
223
|
+
.get(conversationKey);
|
|
224
|
+
return row ? mapSession(row) : null;
|
|
225
|
+
}
|
|
226
|
+
listPending(limit) {
|
|
227
|
+
const rows = this.#database
|
|
228
|
+
.prepare(`
|
|
229
|
+
SELECT * FROM sessions
|
|
230
|
+
WHERE status = 'pending' OR (status = 'failed' AND retryable = 1)
|
|
231
|
+
ORDER BY source_updated_at, conversation_key
|
|
232
|
+
LIMIT ?
|
|
233
|
+
`)
|
|
234
|
+
.all(limit);
|
|
235
|
+
return rows.map(mapSession);
|
|
236
|
+
}
|
|
237
|
+
getDiscoveryWatermark(source) {
|
|
238
|
+
const row = this.#database
|
|
239
|
+
.prepare("SELECT scanned_at AS scannedAt FROM discovery_watermarks WHERE source = ?")
|
|
240
|
+
.get(source);
|
|
241
|
+
return row?.scannedAt ?? null;
|
|
242
|
+
}
|
|
243
|
+
setDiscoveryWatermark(source, scannedAt) {
|
|
244
|
+
this.#database
|
|
245
|
+
.prepare(`
|
|
246
|
+
INSERT INTO discovery_watermarks(source, scanned_at) VALUES (?, ?)
|
|
247
|
+
ON CONFLICT(source) DO UPDATE SET scanned_at = excluded.scanned_at
|
|
248
|
+
`)
|
|
249
|
+
.run(source, scannedAt);
|
|
250
|
+
}
|
|
251
|
+
getBackfillState() {
|
|
252
|
+
const row = this.#database
|
|
253
|
+
.prepare(`
|
|
254
|
+
SELECT decision, sessions, bytes, uploaded,
|
|
255
|
+
decided_at AS decidedAt, completed_at AS completedAt
|
|
256
|
+
FROM setup_backfill WHERE singleton = 1
|
|
257
|
+
`)
|
|
258
|
+
.get();
|
|
259
|
+
return row ?? null;
|
|
260
|
+
}
|
|
261
|
+
recordBackfillDecision(input) {
|
|
262
|
+
this.#database.transaction(() => {
|
|
263
|
+
const inserted = this.#database
|
|
264
|
+
.prepare(`
|
|
265
|
+
INSERT OR IGNORE INTO setup_backfill(
|
|
266
|
+
singleton, decision, sessions, bytes, uploaded,
|
|
267
|
+
decided_at, completed_at
|
|
268
|
+
) VALUES (1, ?, ?, ?, 0, ?, ?)
|
|
269
|
+
`)
|
|
270
|
+
.run(input.decision, input.sessions, input.bytes, input.decidedAt, input.decision === "declined" ? input.decidedAt : null);
|
|
271
|
+
if (inserted.changes === 0 || input.decision !== "declined")
|
|
272
|
+
return;
|
|
273
|
+
const watermark = this.#database.prepare(`
|
|
274
|
+
INSERT INTO discovery_watermarks(source, scanned_at) VALUES (?, ?)
|
|
275
|
+
ON CONFLICT(source) DO UPDATE SET scanned_at = excluded.scanned_at
|
|
276
|
+
`);
|
|
277
|
+
for (const source of ["claude-code", "codex", "grok-build"]) {
|
|
278
|
+
watermark.run(source, input.decidedAt);
|
|
279
|
+
}
|
|
280
|
+
})();
|
|
281
|
+
}
|
|
282
|
+
recordBackfillUpload(input) {
|
|
283
|
+
const updated = this.#database
|
|
284
|
+
.prepare(`
|
|
285
|
+
UPDATE setup_backfill
|
|
286
|
+
SET uploaded = uploaded + ?,
|
|
287
|
+
completed_at = CASE WHEN ? = 0 THEN ? ELSE completed_at END
|
|
288
|
+
WHERE singleton = 1 AND decision = 'accepted'
|
|
289
|
+
`)
|
|
290
|
+
.run(input.uploaded, input.pending, input.recordedAt);
|
|
291
|
+
if (updated.changes === 0) {
|
|
292
|
+
throw new Error("Accepted backfill decision is required before upload");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
close() {
|
|
296
|
+
this.#database.close();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=sqlite-store.js.map
|