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,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