@dmsdc-ai/aigentry-brain 0.0.1

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 (136) hide show
  1. package/bin/aigentry-brain-mcp.mjs +17 -0
  2. package/bin/aigentry-brain-setup.mjs +17 -0
  3. package/bin/aigentry-brain.mjs +17 -0
  4. package/dist/audit/AuditTrail.d.ts +15 -0
  5. package/dist/audit/AuditTrail.js +43 -0
  6. package/dist/audit/AuditTrail.js.map +1 -0
  7. package/dist/backend/GitBackend.d.ts +27 -0
  8. package/dist/backend/GitBackend.js +201 -0
  9. package/dist/backend/GitBackend.js.map +1 -0
  10. package/dist/backend/SyncBackend.d.ts +13 -0
  11. package/dist/backend/SyncBackend.js +2 -0
  12. package/dist/backend/SyncBackend.js.map +1 -0
  13. package/dist/cli/AutoUpdater.d.ts +7 -0
  14. package/dist/cli/AutoUpdater.js +275 -0
  15. package/dist/cli/AutoUpdater.js.map +1 -0
  16. package/dist/cli/ConfigStore.d.ts +17 -0
  17. package/dist/cli/ConfigStore.js +50 -0
  18. package/dist/cli/ConfigStore.js.map +1 -0
  19. package/dist/cli/GhIntegration.d.ts +55 -0
  20. package/dist/cli/GhIntegration.js +192 -0
  21. package/dist/cli/GhIntegration.js.map +1 -0
  22. package/dist/cli/Prerequisites.d.ts +48 -0
  23. package/dist/cli/Prerequisites.js +182 -0
  24. package/dist/cli/Prerequisites.js.map +1 -0
  25. package/dist/cli/SetupWizard.d.ts +26 -0
  26. package/dist/cli/SetupWizard.js +407 -0
  27. package/dist/cli/SetupWizard.js.map +1 -0
  28. package/dist/cli/braincli.d.ts +2 -0
  29. package/dist/cli/braincli.js +260 -0
  30. package/dist/cli/braincli.js.map +1 -0
  31. package/dist/cli/setup.d.ts +25 -0
  32. package/dist/cli/setup.js +238 -0
  33. package/dist/cli/setup.js.map +1 -0
  34. package/dist/context/ConfidenceGuard.d.ts +11 -0
  35. package/dist/context/ConfidenceGuard.js +32 -0
  36. package/dist/context/ConfidenceGuard.js.map +1 -0
  37. package/dist/context/ContextBudgetPolicy.d.ts +21 -0
  38. package/dist/context/ContextBudgetPolicy.js +136 -0
  39. package/dist/context/ContextBudgetPolicy.js.map +1 -0
  40. package/dist/context/ContextPacker.d.ts +30 -0
  41. package/dist/context/ContextPacker.js +219 -0
  42. package/dist/context/ContextPacker.js.map +1 -0
  43. package/dist/context/ContextRestoreService.d.ts +39 -0
  44. package/dist/context/ContextRestoreService.js +134 -0
  45. package/dist/context/ContextRestoreService.js.map +1 -0
  46. package/dist/contract/BrainContract.d.ts +93 -0
  47. package/dist/contract/BrainContract.js +245 -0
  48. package/dist/contract/BrainContract.js.map +1 -0
  49. package/dist/core/ContentDedup.d.ts +43 -0
  50. package/dist/core/ContentDedup.js +145 -0
  51. package/dist/core/ContentDedup.js.map +1 -0
  52. package/dist/core/EntityGraph.d.ts +66 -0
  53. package/dist/core/EntityGraph.js +196 -0
  54. package/dist/core/EntityGraph.js.map +1 -0
  55. package/dist/core/EntrySchema.d.ts +46 -0
  56. package/dist/core/EntrySchema.js +4 -0
  57. package/dist/core/EntrySchema.js.map +1 -0
  58. package/dist/core/GraphStore.d.ts +16 -0
  59. package/dist/core/GraphStore.js +29 -0
  60. package/dist/core/GraphStore.js.map +1 -0
  61. package/dist/core/MemoryDecay.d.ts +49 -0
  62. package/dist/core/MemoryDecay.js +113 -0
  63. package/dist/core/MemoryDecay.js.map +1 -0
  64. package/dist/core/ProfileFormat.d.ts +38 -0
  65. package/dist/core/ProfileFormat.js +254 -0
  66. package/dist/core/ProfileFormat.js.map +1 -0
  67. package/dist/core/ProfileManager.d.ts +80 -0
  68. package/dist/core/ProfileManager.js +269 -0
  69. package/dist/core/ProfileManager.js.map +1 -0
  70. package/dist/core/SchemaMigration.d.ts +12 -0
  71. package/dist/core/SchemaMigration.js +34 -0
  72. package/dist/core/SchemaMigration.js.map +1 -0
  73. package/dist/crypto/AgeCrypto.d.ts +26 -0
  74. package/dist/crypto/AgeCrypto.js +101 -0
  75. package/dist/crypto/AgeCrypto.js.map +1 -0
  76. package/dist/index.d.ts +38 -0
  77. package/dist/index.js +30 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/mcp/BrainMcpServer.d.ts +13 -0
  80. package/dist/mcp/BrainMcpServer.js +223 -0
  81. package/dist/mcp/BrainMcpServer.js.map +1 -0
  82. package/dist/mcp/cli.d.ts +2 -0
  83. package/dist/mcp/cli.js +107 -0
  84. package/dist/mcp/cli.js.map +1 -0
  85. package/dist/metrics/ResumeMetrics.d.ts +20 -0
  86. package/dist/metrics/ResumeMetrics.js +46 -0
  87. package/dist/metrics/ResumeMetrics.js.map +1 -0
  88. package/dist/peers/PeerStore.d.ts +17 -0
  89. package/dist/peers/PeerStore.js +65 -0
  90. package/dist/peers/PeerStore.js.map +1 -0
  91. package/dist/policy/PolicyEngine.d.ts +16 -0
  92. package/dist/policy/PolicyEngine.js +70 -0
  93. package/dist/policy/PolicyEngine.js.map +1 -0
  94. package/dist/policy/PolicyEnvelope.d.ts +7 -0
  95. package/dist/policy/PolicyEnvelope.js +2 -0
  96. package/dist/policy/PolicyEnvelope.js.map +1 -0
  97. package/dist/policy/RetentionParser.d.ts +6 -0
  98. package/dist/policy/RetentionParser.js +48 -0
  99. package/dist/policy/RetentionParser.js.map +1 -0
  100. package/dist/shared/AtomicWrite.d.ts +1 -0
  101. package/dist/shared/AtomicWrite.js +11 -0
  102. package/dist/shared/AtomicWrite.js.map +1 -0
  103. package/dist/shared/Clock.d.ts +11 -0
  104. package/dist/shared/Clock.js +17 -0
  105. package/dist/shared/Clock.js.map +1 -0
  106. package/dist/shared/Config.d.ts +15 -0
  107. package/dist/shared/Config.js +67 -0
  108. package/dist/shared/Config.js.map +1 -0
  109. package/dist/shared/Encryption.d.ts +19 -0
  110. package/dist/shared/Encryption.js +47 -0
  111. package/dist/shared/Encryption.js.map +1 -0
  112. package/dist/shared/Logger.d.ts +12 -0
  113. package/dist/shared/Logger.js +40 -0
  114. package/dist/shared/Logger.js.map +1 -0
  115. package/dist/shared/Mutex.d.ts +7 -0
  116. package/dist/shared/Mutex.js +34 -0
  117. package/dist/shared/Mutex.js.map +1 -0
  118. package/dist/shared/SetupErrors.d.ts +31 -0
  119. package/dist/shared/SetupErrors.js +155 -0
  120. package/dist/shared/SetupErrors.js.map +1 -0
  121. package/dist/status/SyncStatusStore.d.ts +17 -0
  122. package/dist/status/SyncStatusStore.js +48 -0
  123. package/dist/status/SyncStatusStore.js.map +1 -0
  124. package/dist/sync/AutoCommitter.d.ts +17 -0
  125. package/dist/sync/AutoCommitter.js +68 -0
  126. package/dist/sync/AutoCommitter.js.map +1 -0
  127. package/dist/sync/ConflictResolver.d.ts +7 -0
  128. package/dist/sync/ConflictResolver.js +56 -0
  129. package/dist/sync/ConflictResolver.js.map +1 -0
  130. package/dist/sync/PushPullManager.d.ts +23 -0
  131. package/dist/sync/PushPullManager.js +155 -0
  132. package/dist/sync/PushPullManager.js.map +1 -0
  133. package/dist/sync/SyncEngine.d.ts +43 -0
  134. package/dist/sync/SyncEngine.js +145 -0
  135. package/dist/sync/SyncEngine.js.map +1 -0
  136. package/package.json +65 -0
@@ -0,0 +1,34 @@
1
+ export class Mutex {
2
+ _locked = false;
3
+ _queue = [];
4
+ async acquire() {
5
+ return new Promise((resolve) => {
6
+ const tryAcquire = () => {
7
+ if (!this._locked) {
8
+ this._locked = true;
9
+ resolve(() => this.release());
10
+ }
11
+ else {
12
+ this._queue.push(tryAcquire);
13
+ }
14
+ };
15
+ tryAcquire();
16
+ });
17
+ }
18
+ release() {
19
+ this._locked = false;
20
+ const next = this._queue.shift();
21
+ if (next)
22
+ next();
23
+ }
24
+ async runExclusive(fn) {
25
+ const release = await this.acquire();
26
+ try {
27
+ return await fn();
28
+ }
29
+ finally {
30
+ release();
31
+ }
32
+ }
33
+ }
34
+ //# sourceMappingURL=Mutex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Mutex.js","sourceRoot":"","sources":["../../src/shared/Mutex.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,KAAK;IACR,OAAO,GAAG,KAAK,CAAC;IAChB,MAAM,GAAsB,EAAE,CAAC;IAEvC,KAAK,CAAC,OAAO;QACX,OAAO,IAAI,OAAO,CAAa,CAAC,OAAO,EAAE,EAAE;YACzC,MAAM,UAAU,GAAG,GAAG,EAAE;gBACtB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;oBAClB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;oBACpB,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;gBAChC,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gBAC/B,CAAC;YACH,CAAC,CAAC;YACF,UAAU,EAAE,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,OAAO;QACb,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACrB,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACjC,IAAI,IAAI;YAAE,IAAI,EAAE,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,YAAY,CAAI,EAAoB;QACxC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;gBAAS,CAAC;YACT,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,31 @@
1
+ export declare enum SetupError {
2
+ GH_NOT_INSTALLED = "GH_NOT_INSTALLED",
3
+ GH_NOT_AUTHENTICATED = "GH_NOT_AUTHENTICATED",
4
+ AGE_KEY_MISSING = "AGE_KEY_MISSING",
5
+ AGE_KEY_ERROR = "AGE_KEY_ERROR",
6
+ AGE_DECRYPT_FAILED = "AGE_DECRYPT_FAILED",
7
+ REPO_CLONE_FAILED = "REPO_CLONE_FAILED",
8
+ REPO_PUSH_FAILED = "REPO_PUSH_FAILED",
9
+ NETWORK_UNREACHABLE = "NETWORK_UNREACHABLE",
10
+ BREW_NOT_AVAILABLE = "BREW_NOT_AVAILABLE",
11
+ PERMISSION_DENIED = "PERMISSION_DENIED"
12
+ }
13
+ export interface ErrorGuide {
14
+ error: SetupError;
15
+ userMessage: string;
16
+ autoFixCommand?: string;
17
+ manualFixSteps: string[];
18
+ docUrl?: string;
19
+ }
20
+ /**
21
+ * Get the guide for a specific setup error.
22
+ */
23
+ export declare function getErrorGuide(error: SetupError): ErrorGuide;
24
+ /**
25
+ * Format an error guide as a user-friendly string for CON/terminal output.
26
+ */
27
+ export declare function formatErrorGuide(error: SetupError): string;
28
+ /**
29
+ * Classify an error from a catch block into a SetupError.
30
+ */
31
+ export declare function classifyError(err: unknown, context?: string): SetupError | null;
@@ -0,0 +1,155 @@
1
+ export var SetupError;
2
+ (function (SetupError) {
3
+ SetupError["GH_NOT_INSTALLED"] = "GH_NOT_INSTALLED";
4
+ SetupError["GH_NOT_AUTHENTICATED"] = "GH_NOT_AUTHENTICATED";
5
+ SetupError["AGE_KEY_MISSING"] = "AGE_KEY_MISSING";
6
+ SetupError["AGE_KEY_ERROR"] = "AGE_KEY_ERROR";
7
+ SetupError["AGE_DECRYPT_FAILED"] = "AGE_DECRYPT_FAILED";
8
+ SetupError["REPO_CLONE_FAILED"] = "REPO_CLONE_FAILED";
9
+ SetupError["REPO_PUSH_FAILED"] = "REPO_PUSH_FAILED";
10
+ SetupError["NETWORK_UNREACHABLE"] = "NETWORK_UNREACHABLE";
11
+ SetupError["BREW_NOT_AVAILABLE"] = "BREW_NOT_AVAILABLE";
12
+ SetupError["PERMISSION_DENIED"] = "PERMISSION_DENIED";
13
+ })(SetupError || (SetupError = {}));
14
+ const ERROR_GUIDES = {
15
+ [SetupError.GH_NOT_INSTALLED]: {
16
+ userMessage: 'GitHub CLI (gh) is not installed.',
17
+ autoFixCommand: 'brew install gh',
18
+ manualFixSteps: [
19
+ 'macOS: brew install gh',
20
+ 'Linux: sudo apt install gh OR sudo dnf install gh',
21
+ 'Windows: winget install --id GitHub.cli',
22
+ ],
23
+ docUrl: 'https://cli.github.com/manual/installation',
24
+ },
25
+ [SetupError.GH_NOT_AUTHENTICATED]: {
26
+ userMessage: 'GitHub CLI is installed but not authenticated.',
27
+ autoFixCommand: 'gh auth login --web',
28
+ manualFixSteps: [
29
+ 'Run: gh auth login --web',
30
+ 'This opens a browser for GitHub OAuth.',
31
+ 'For headless environments: gh auth login --with-token < token.txt',
32
+ ],
33
+ docUrl: 'https://cli.github.com/manual/gh_auth_login',
34
+ },
35
+ [SetupError.AGE_KEY_MISSING]: {
36
+ userMessage: 'No age encryption key found.',
37
+ autoFixCommand: 'aigentry-brain-setup',
38
+ manualFixSteps: [
39
+ 'Run: aigentry-brain-setup (auto-generates key)',
40
+ 'Or manually: mkdir -p ~/.config/sops/age && age-keygen -o ~/.config/sops/age/keys.txt',
41
+ ],
42
+ },
43
+ [SetupError.AGE_KEY_ERROR]: {
44
+ userMessage: 'Age encryption key is invalid or corrupted.',
45
+ manualFixSteps: [
46
+ 'Check key file: cat ~/.config/sops/age/keys.txt',
47
+ 'Key should contain AGE-SECRET-KEY-... and age1... lines.',
48
+ 'Regenerate: rm ~/.config/sops/age/keys.txt && aigentry-brain-setup',
49
+ ],
50
+ },
51
+ [SetupError.AGE_DECRYPT_FAILED]: {
52
+ userMessage: 'Failed to decrypt profile data.',
53
+ manualFixSteps: [
54
+ 'This usually means a new device needs re-encryption from an existing device.',
55
+ 'Run aigentry-brain status to check encryption state.',
56
+ 'If on a new device, wait for an existing device to sync and re-encrypt.',
57
+ 'To bypass encryption: export AIGENTRY_ENCRYPTION=disabled',
58
+ ],
59
+ },
60
+ [SetupError.REPO_CLONE_FAILED]: {
61
+ userMessage: 'Failed to clone the profile repository.',
62
+ manualFixSteps: [
63
+ 'Check your internet connection.',
64
+ 'Verify GitHub authentication: gh auth status',
65
+ 'Verify the repo exists: gh repo view {repo-name}',
66
+ 'Try cloning manually: git clone {repo-url}',
67
+ ],
68
+ },
69
+ [SetupError.REPO_PUSH_FAILED]: {
70
+ userMessage: 'Failed to push changes to the profile repository.',
71
+ manualFixSteps: [
72
+ 'Check your internet connection.',
73
+ 'Verify GitHub authentication: gh auth status',
74
+ 'Check for permission issues: gh repo view {repo-name}',
75
+ 'Try pushing manually: cd ~/.aigentry/repo && git push',
76
+ ],
77
+ },
78
+ [SetupError.NETWORK_UNREACHABLE]: {
79
+ userMessage: 'Cannot reach GitHub. Check your internet connection.',
80
+ manualFixSteps: [
81
+ 'Check internet connectivity: ping github.com',
82
+ 'If behind a proxy, configure git proxy: git config --global http.proxy {proxy-url}',
83
+ 'aigentry-brain works offline — sync will resume when connectivity is restored.',
84
+ ],
85
+ },
86
+ [SetupError.BREW_NOT_AVAILABLE]: {
87
+ userMessage: 'Homebrew is not available on this system.',
88
+ manualFixSteps: [
89
+ 'Install Homebrew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
90
+ 'Or install tools directly from GitHub Releases.',
91
+ ],
92
+ docUrl: 'https://brew.sh',
93
+ },
94
+ [SetupError.PERMISSION_DENIED]: {
95
+ userMessage: 'Permission denied. You may need elevated permissions.',
96
+ manualFixSteps: [
97
+ 'Try running with sudo if on Linux.',
98
+ 'Check file permissions on ~/.aigentry/ directory.',
99
+ 'On macOS, check System Preferences > Security & Privacy.',
100
+ ],
101
+ },
102
+ };
103
+ /**
104
+ * Get the guide for a specific setup error.
105
+ */
106
+ export function getErrorGuide(error) {
107
+ const guide = ERROR_GUIDES[error];
108
+ return { error, ...guide };
109
+ }
110
+ /**
111
+ * Format an error guide as a user-friendly string for CON/terminal output.
112
+ */
113
+ export function formatErrorGuide(error) {
114
+ const guide = getErrorGuide(error);
115
+ const lines = [];
116
+ lines.push(`\u26A0\uFE0F ${guide.userMessage}`);
117
+ if (guide.autoFixCommand) {
118
+ lines.push(` Auto-fix: ${guide.autoFixCommand}`);
119
+ }
120
+ lines.push(' Manual steps:');
121
+ for (const step of guide.manualFixSteps) {
122
+ lines.push(` \u2022 ${step}`);
123
+ }
124
+ if (guide.docUrl) {
125
+ lines.push(` Docs: ${guide.docUrl}`);
126
+ }
127
+ return lines.join('\n');
128
+ }
129
+ /**
130
+ * Classify an error from a catch block into a SetupError.
131
+ */
132
+ export function classifyError(err, context) {
133
+ const msg = err instanceof Error ? err.message : String(err);
134
+ const lower = msg.toLowerCase();
135
+ if (lower.includes('enoent') && context?.includes('gh'))
136
+ return SetupError.GH_NOT_INSTALLED;
137
+ if (lower.includes('agekeyerror') || (lower.includes('age key') && lower.includes('not found')))
138
+ return SetupError.AGE_KEY_MISSING;
139
+ if (lower.includes('could not parse age key'))
140
+ return SetupError.AGE_KEY_ERROR;
141
+ if (lower.includes('agedecrypterror') || lower.includes('decryption failed'))
142
+ return SetupError.AGE_DECRYPT_FAILED;
143
+ if (lower.includes('not logged in') || lower.includes('authentication'))
144
+ return SetupError.GH_NOT_AUTHENTICATED;
145
+ if (lower.includes('permission denied') || lower.includes('eacces'))
146
+ return SetupError.PERMISSION_DENIED;
147
+ if (lower.includes('could not resolve') || lower.includes('network') || lower.includes('etimedout'))
148
+ return SetupError.NETWORK_UNREACHABLE;
149
+ if (lower.includes('clone') || lower.includes('repository not found'))
150
+ return SetupError.REPO_CLONE_FAILED;
151
+ if (lower.includes('push') || lower.includes('rejected'))
152
+ return SetupError.REPO_PUSH_FAILED;
153
+ return null;
154
+ }
155
+ //# sourceMappingURL=SetupErrors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SetupErrors.js","sourceRoot":"","sources":["../../src/shared/SetupErrors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { Clock } from '../shared/Clock.js';
2
+ export type SyncStatus = 'synced' | 'pending' | 'degraded' | 'offline';
3
+ export interface SyncStatusFile {
4
+ status: SyncStatus;
5
+ last_sync: string;
6
+ pending_count: number;
7
+ error?: string;
8
+ }
9
+ export declare class SyncStatusStore {
10
+ private filePath;
11
+ private clock;
12
+ private listeners;
13
+ constructor(filePath: string, clock?: Clock);
14
+ onChange(cb: (status: SyncStatus) => void): void;
15
+ get(): Promise<SyncStatusFile>;
16
+ set(status: SyncStatus, extra?: Partial<SyncStatusFile>): Promise<void>;
17
+ }
@@ -0,0 +1,48 @@
1
+ import { readFile, mkdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { SystemClock } from '../shared/Clock.js';
5
+ import { atomicWrite } from '../shared/AtomicWrite.js';
6
+ export class SyncStatusStore {
7
+ filePath;
8
+ clock;
9
+ listeners = [];
10
+ constructor(filePath, clock) {
11
+ this.filePath = filePath;
12
+ this.clock = clock ?? SystemClock;
13
+ }
14
+ onChange(cb) {
15
+ this.listeners.push(cb);
16
+ }
17
+ async get() {
18
+ if (!existsSync(this.filePath)) {
19
+ return { status: 'offline', last_sync: '', pending_count: 0 };
20
+ }
21
+ try {
22
+ const content = await readFile(this.filePath, 'utf8');
23
+ return JSON.parse(content);
24
+ }
25
+ catch {
26
+ return { status: 'offline', last_sync: '', pending_count: 0 };
27
+ }
28
+ }
29
+ async set(status, extra) {
30
+ const current = await this.get();
31
+ const updated = {
32
+ ...current,
33
+ ...extra,
34
+ status,
35
+ };
36
+ if (status === 'synced') {
37
+ updated.last_sync = this.clock.now().toISOString();
38
+ updated.pending_count = 0;
39
+ delete updated.error;
40
+ }
41
+ await mkdir(dirname(this.filePath), { recursive: true });
42
+ await atomicWrite(this.filePath, JSON.stringify(updated, null, 2));
43
+ for (const cb of this.listeners) {
44
+ cb(status);
45
+ }
46
+ }
47
+ }
48
+ //# sourceMappingURL=SyncStatusStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SyncStatusStore.js","sourceRoot":"","sources":["../../src/status/SyncStatusStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAWvD,MAAM,OAAO,eAAe;IAClB,QAAQ,CAAS;IACjB,KAAK,CAAQ;IACb,SAAS,GAAwC,EAAE,CAAC;IAE5D,YAAY,QAAgB,EAAE,KAAa;QACzC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,KAAK,IAAI,WAAW,CAAC;IACpC,CAAC;IAED,QAAQ,CAAC,EAAgC;QACvC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,GAAG;QACP,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/B,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;QAChE,CAAC;QACD,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YACtD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAmB,CAAC;QAC/C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;QAChE,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,MAAkB,EAAE,KAA+B;QAC3D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;QACjC,MAAM,OAAO,GAAmB;YAC9B,GAAG,OAAO;YACV,GAAG,KAAK;YACR,MAAM;SACP,CAAC;QACF,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACxB,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;YACnD,OAAO,CAAC,aAAa,GAAG,CAAC,CAAC;YAC1B,OAAO,OAAO,CAAC,KAAK,CAAC;QACvB,CAAC;QACD,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,MAAM,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACnE,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAChC,EAAE,CAAC,MAAM,CAAC,CAAC;QACb,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,17 @@
1
+ import type { SyncBackend } from '../backend/SyncBackend.js';
2
+ import type { PushPullManager } from './PushPullManager.js';
3
+ import type { Clock } from '../shared/Clock.js';
4
+ export declare class AutoCommitter {
5
+ private debounceTimer;
6
+ private inFlight;
7
+ private backend;
8
+ private pushPullManager;
9
+ private clock;
10
+ private debounceMs;
11
+ constructor(backend: SyncBackend, pushPullManager: PushPullManager, clock: Clock, debounceMs?: number);
12
+ scheduleCommit(): void;
13
+ private executeCommit;
14
+ flush(): Promise<void>;
15
+ stop(): void;
16
+ isInFlight(): boolean;
17
+ }
@@ -0,0 +1,68 @@
1
+ export class AutoCommitter {
2
+ debounceTimer = null;
3
+ inFlight = false;
4
+ backend;
5
+ pushPullManager;
6
+ clock;
7
+ debounceMs;
8
+ constructor(backend, pushPullManager, clock, debounceMs = 2000) {
9
+ this.backend = backend;
10
+ this.pushPullManager = pushPullManager;
11
+ this.clock = clock;
12
+ this.debounceMs = debounceMs;
13
+ }
14
+ scheduleCommit() {
15
+ if (this.debounceTimer)
16
+ clearTimeout(this.debounceTimer);
17
+ this.debounceTimer = setTimeout(() => this.executeCommit(), this.debounceMs);
18
+ }
19
+ async executeCommit() {
20
+ if (this.inFlight)
21
+ return;
22
+ this.inFlight = true;
23
+ try {
24
+ const hasChanges = await this.backend.hasChanges();
25
+ if (!hasChanges)
26
+ return;
27
+ const now = this.clock.now().toISOString();
28
+ await this.backend.commit(`chore: sync ${now}`);
29
+ await this.pushPullManager.enqueuePush();
30
+ }
31
+ catch {
32
+ // Commit failed — will retry on next schedule
33
+ }
34
+ finally {
35
+ this.inFlight = false;
36
+ }
37
+ }
38
+ async flush() {
39
+ this.stop(); // Cancel any pending debounce
40
+ if (this.inFlight)
41
+ return; // Already executing
42
+ this.inFlight = true;
43
+ try {
44
+ const hasChanges = await this.backend.hasChanges();
45
+ if (!hasChanges)
46
+ return;
47
+ const now = this.clock.now().toISOString();
48
+ await this.backend.commit(`chore: sync ${now}`);
49
+ await this.pushPullManager.enqueuePush();
50
+ }
51
+ catch {
52
+ // Best-effort flush
53
+ }
54
+ finally {
55
+ this.inFlight = false;
56
+ }
57
+ }
58
+ stop() {
59
+ if (this.debounceTimer) {
60
+ clearTimeout(this.debounceTimer);
61
+ this.debounceTimer = null;
62
+ }
63
+ }
64
+ isInFlight() {
65
+ return this.inFlight;
66
+ }
67
+ }
68
+ //# sourceMappingURL=AutoCommitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AutoCommitter.js","sourceRoot":"","sources":["../../src/sync/AutoCommitter.ts"],"names":[],"mappings":"AAIA,MAAM,OAAO,aAAa;IAChB,aAAa,GAAyC,IAAI,CAAC;IAC3D,QAAQ,GAAG,KAAK,CAAC;IACjB,OAAO,CAAc;IACrB,eAAe,CAAkB;IACjC,KAAK,CAAQ;IACb,UAAU,CAAS;IAE3B,YACE,OAAoB,EACpB,eAAgC,EAChC,KAAY,EACZ,UAAU,GAAG,IAAI;QAEjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAED,cAAc;QACZ,IAAI,IAAI,CAAC,aAAa;YAAE,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACzD,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC/E,CAAC;IAEO,KAAK,CAAC,aAAa;QACzB,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC1B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;YACnD,IAAI,CAAC,UAAU;gBAAE,OAAO;YAExB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;YAC3C,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,eAAe,GAAG,EAAE,CAAC,CAAC;YAChD,MAAM,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,8CAA8C;QAChD,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACxB,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,8BAA8B;QAC3C,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,CAAC,oBAAoB;QAC/C,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;YACnD,IAAI,CAAC,UAAU;gBAAE,OAAO;YACxB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;YAC3C,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,eAAe,GAAG,EAAE,CAAC,CAAC;YAChD,MAAM,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,oBAAoB;QACtB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACxB,CAAC;IACH,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YACjC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ import type { Entry } from '../core/EntrySchema.js';
2
+ import type { Clock } from '../shared/Clock.js';
3
+ /** Maximum number of conflict_log entries to retain per entry. */
4
+ export declare const CONFLICT_LOG_MAX = 200;
5
+ export declare class ConflictResolver {
6
+ merge(local: Entry[], remote: Entry[], clock: Clock): Entry[];
7
+ }
@@ -0,0 +1,56 @@
1
+ /** Maximum number of conflict_log entries to retain per entry. */
2
+ export const CONFLICT_LOG_MAX = 200;
3
+ function pickWinner(a, b) {
4
+ // 1st: LWW by updated timestamp
5
+ if (a.updated > b.updated)
6
+ return { winner: a, loser: b, resolution: 'lww' };
7
+ if (b.updated > a.updated)
8
+ return { winner: b, loser: a, resolution: 'lww' };
9
+ // 2nd: deleted:false wins (preservation principle)
10
+ if (!a.deleted && b.deleted)
11
+ return { winner: a, loser: b, resolution: 'tie_deleted' };
12
+ if (!b.deleted && a.deleted)
13
+ return { winner: b, loser: a, resolution: 'tie_deleted' };
14
+ // 3rd: id lexicographic (fully deterministic fallback)
15
+ return a.id < b.id
16
+ ? { winner: a, loser: b, resolution: 'tie_id' }
17
+ : { winner: b, loser: a, resolution: 'tie_id' };
18
+ }
19
+ export class ConflictResolver {
20
+ merge(local, remote, clock) {
21
+ const map = new Map();
22
+ // Load local entries
23
+ for (const entry of local) {
24
+ map.set(entry.id, { ...entry });
25
+ }
26
+ // Merge with remote
27
+ for (const remoteEntry of remote) {
28
+ const localEntry = map.get(remoteEntry.id);
29
+ if (!localEntry) {
30
+ // Append: new entry from remote
31
+ map.set(remoteEntry.id, { ...remoteEntry });
32
+ continue;
33
+ }
34
+ // Same entry exists on both sides — pick winner
35
+ const { winner, loser, resolution } = pickWinner(localEntry, remoteEntry);
36
+ // Add conflict log entry to the winner
37
+ const logEntry = {
38
+ at: clock.now().toISOString(),
39
+ loser_device: loser.device_id ?? 'unknown',
40
+ loser_updated: loser.updated,
41
+ resolution,
42
+ };
43
+ const fullLog = [...(winner.conflict_log ?? []), logEntry];
44
+ const merged = {
45
+ ...winner,
46
+ conflict_log: fullLog.length > CONFLICT_LOG_MAX
47
+ ? fullLog.slice(fullLog.length - CONFLICT_LOG_MAX)
48
+ : fullLog,
49
+ };
50
+ map.set(remoteEntry.id, merged);
51
+ }
52
+ // Sort by id for deterministic output
53
+ return Array.from(map.values()).sort((a, b) => a.id.localeCompare(b.id));
54
+ }
55
+ }
56
+ //# sourceMappingURL=ConflictResolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConflictResolver.js","sourceRoot":"","sources":["../../src/sync/ConflictResolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { SyncBackend } from '../backend/SyncBackend.js';
2
+ import type { ConflictResolver } from './ConflictResolver.js';
3
+ import type { ProfileManager } from '../core/ProfileManager.js';
4
+ import type { SyncStatusStore } from '../status/SyncStatusStore.js';
5
+ import type { Clock } from '../shared/Clock.js';
6
+ export declare class PushPullManager {
7
+ private retryCount;
8
+ private pushTimer;
9
+ private backend;
10
+ private resolver;
11
+ private profileManager;
12
+ private statusStore;
13
+ private clock;
14
+ constructor(backend: SyncBackend, resolver: ConflictResolver, profileManager: ProfileManager, statusStore: SyncStatusStore, clock: Clock);
15
+ startSync(): Promise<void>;
16
+ syncPull(): Promise<void>;
17
+ private retryPull;
18
+ private pullWithRecovery;
19
+ enqueuePush(): Promise<void>;
20
+ private executePush;
21
+ stop(): void;
22
+ getRetryCount(): number;
23
+ }
@@ -0,0 +1,155 @@
1
+ import { ProfileFormat } from '../core/ProfileFormat.js';
2
+ const MAX_RETRY = 3;
3
+ function backoffMs(attempt) {
4
+ return Math.min(1000 * Math.pow(2, attempt), 30000);
5
+ }
6
+ export class PushPullManager {
7
+ retryCount = 0;
8
+ pushTimer = null;
9
+ backend;
10
+ resolver;
11
+ profileManager;
12
+ statusStore;
13
+ clock;
14
+ constructor(backend, resolver, profileManager, statusStore, clock) {
15
+ this.backend = backend;
16
+ this.resolver = resolver;
17
+ this.profileManager = profileManager;
18
+ this.statusStore = statusStore;
19
+ this.clock = clock;
20
+ }
21
+ async startSync() {
22
+ await this.pullWithRecovery();
23
+ }
24
+ async syncPull() {
25
+ await this.pullWithRecovery();
26
+ }
27
+ async retryPull() {
28
+ for (let attempt = 0; attempt < MAX_RETRY; attempt++) {
29
+ try {
30
+ await this.backend.pull();
31
+ return true;
32
+ }
33
+ catch {
34
+ if (attempt < MAX_RETRY - 1) {
35
+ await new Promise(r => setTimeout(r, backoffMs(attempt)));
36
+ }
37
+ }
38
+ }
39
+ return false;
40
+ }
41
+ async pullWithRecovery() {
42
+ const pulled = await this.retryPull();
43
+ if (pulled) {
44
+ await this.statusStore.set('synced');
45
+ return;
46
+ }
47
+ // All retries failed — attempt conflict recovery
48
+ try {
49
+ await this.backend.rebaseAbort();
50
+ await this.backend.fetch();
51
+ const headProfile = await ProfileFormat.readFromFile(this.profileManager.getProfilePath());
52
+ const localEntries = headProfile.entries;
53
+ // Read remote profile via git show
54
+ let remoteEntries = [];
55
+ try {
56
+ const profilePath = this.profileManager.getProfilePath();
57
+ const repoPath = this.backend.getRepoPath();
58
+ const relativePath = profilePath.startsWith(repoPath)
59
+ ? profilePath.slice(repoPath.length + 1)
60
+ : profilePath;
61
+ const remoteBranchRef = `origin/${this.backend.getBranch()}`;
62
+ // Try .enc.json first (v2 format), then fall back to .yaml (legacy)
63
+ const encJsonRelPath = relativePath.replace(/\.yaml$/, '.enc.json');
64
+ let remoteContent = null;
65
+ let isEncJson = false;
66
+ try {
67
+ remoteContent = await this.backend.readFileAtRef(remoteBranchRef, encJsonRelPath);
68
+ isEncJson = true;
69
+ }
70
+ catch {
71
+ try {
72
+ remoteContent = await this.backend.readFileAtRef(remoteBranchRef, relativePath);
73
+ }
74
+ catch {
75
+ // Neither exists — treat as empty
76
+ }
77
+ }
78
+ if (remoteContent) {
79
+ if (isEncJson) {
80
+ // Parse enc.json: extract entries from plaintext-fallback or decrypt age-block
81
+ const encProfile = JSON.parse(remoteContent);
82
+ if (encProfile._aigentry?.format === 'plaintext-fallback') {
83
+ for (const json of Object.values(encProfile.entries)) {
84
+ try {
85
+ remoteEntries.push(JSON.parse(json));
86
+ }
87
+ catch { /* skip */ }
88
+ }
89
+ }
90
+ else {
91
+ // age-block entries — cannot decrypt in conflict recovery context
92
+ // Fall back to deserializing as best-effort
93
+ const remoteProfile = ProfileFormat.deserialize(remoteContent);
94
+ remoteEntries = remoteProfile.entries;
95
+ }
96
+ }
97
+ else {
98
+ const remoteProfile = ProfileFormat.deserialize(remoteContent);
99
+ remoteEntries = remoteProfile.entries;
100
+ }
101
+ }
102
+ }
103
+ catch {
104
+ // Remote file doesn't exist yet — treat as empty
105
+ }
106
+ const merged = this.resolver.merge(localEntries, remoteEntries, this.clock);
107
+ // Reset local branch to remote tip so the merge commit is fast-forward
108
+ const remoteBranch = `origin/${this.backend.getBranch()}`;
109
+ await this.backend.resetToRef(remoteBranch);
110
+ await this.profileManager.save(merged);
111
+ await this.backend.commit('chore: merge conflict recovery');
112
+ await this.enqueuePush();
113
+ }
114
+ catch (recoveryErr) {
115
+ await this.statusStore.set('degraded', {
116
+ error: `Recovery failed: ${String(recoveryErr)}`,
117
+ });
118
+ }
119
+ }
120
+ async enqueuePush() {
121
+ await this.statusStore.set('pending', { pending_count: this.retryCount + 1 });
122
+ await this.executePush();
123
+ }
124
+ async executePush() {
125
+ try {
126
+ await this.backend.push();
127
+ this.retryCount = 0;
128
+ await this.statusStore.set('synced');
129
+ }
130
+ catch {
131
+ this.retryCount++;
132
+ if (this.retryCount >= MAX_RETRY) {
133
+ await this.statusStore.set('degraded', {
134
+ error: `Push failed after ${MAX_RETRY} retries`,
135
+ pending_count: this.retryCount,
136
+ });
137
+ }
138
+ else {
139
+ const delay = backoffMs(this.retryCount);
140
+ this.pushTimer = setTimeout(() => this.executePush(), delay);
141
+ await this.statusStore.set('pending', { pending_count: this.retryCount });
142
+ }
143
+ }
144
+ }
145
+ stop() {
146
+ if (this.pushTimer) {
147
+ clearTimeout(this.pushTimer);
148
+ this.pushTimer = null;
149
+ }
150
+ }
151
+ getRetryCount() {
152
+ return this.retryCount;
153
+ }
154
+ }
155
+ //# sourceMappingURL=PushPullManager.js.map