@ekkos/cli 1.3.1 → 1.3.2

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 (82) hide show
  1. package/dist/commands/dashboard.js +147 -57
  2. package/dist/commands/init.d.ts +1 -0
  3. package/dist/commands/init.js +54 -16
  4. package/dist/commands/run.js +163 -44
  5. package/dist/commands/status.d.ts +4 -1
  6. package/dist/commands/status.js +165 -27
  7. package/dist/commands/synk.d.ts +7 -0
  8. package/dist/commands/synk.js +339 -0
  9. package/dist/deploy/settings.d.ts +6 -5
  10. package/dist/deploy/settings.js +27 -17
  11. package/dist/index.js +12 -82
  12. package/dist/lib/usage-parser.d.ts +1 -1
  13. package/dist/lib/usage-parser.js +5 -3
  14. package/dist/local/index.d.ts +14 -0
  15. package/dist/local/index.js +28 -0
  16. package/dist/local/local-embeddings.d.ts +49 -0
  17. package/dist/local/local-embeddings.js +232 -0
  18. package/dist/local/offline-fallback.d.ts +44 -0
  19. package/dist/local/offline-fallback.js +159 -0
  20. package/dist/local/sqlite-store.d.ts +126 -0
  21. package/dist/local/sqlite-store.js +393 -0
  22. package/dist/local/sync-engine.d.ts +42 -0
  23. package/dist/local/sync-engine.js +223 -0
  24. package/dist/synk/api.d.ts +22 -0
  25. package/dist/synk/api.js +133 -0
  26. package/dist/synk/auth.d.ts +7 -0
  27. package/dist/synk/auth.js +30 -0
  28. package/dist/synk/config.d.ts +18 -0
  29. package/dist/synk/config.js +37 -0
  30. package/dist/synk/daemon/control-client.d.ts +11 -0
  31. package/dist/synk/daemon/control-client.js +101 -0
  32. package/dist/synk/daemon/control-server.d.ts +24 -0
  33. package/dist/synk/daemon/control-server.js +91 -0
  34. package/dist/synk/daemon/run.d.ts +14 -0
  35. package/dist/synk/daemon/run.js +338 -0
  36. package/dist/synk/encryption.d.ts +17 -0
  37. package/dist/synk/encryption.js +133 -0
  38. package/dist/synk/index.d.ts +13 -0
  39. package/dist/synk/index.js +36 -0
  40. package/dist/synk/machine-client.d.ts +42 -0
  41. package/dist/synk/machine-client.js +218 -0
  42. package/dist/synk/persistence.d.ts +51 -0
  43. package/dist/synk/persistence.js +211 -0
  44. package/dist/synk/qr.d.ts +5 -0
  45. package/dist/synk/qr.js +33 -0
  46. package/dist/synk/session-bridge.d.ts +58 -0
  47. package/dist/synk/session-bridge.js +171 -0
  48. package/dist/synk/session-client.d.ts +46 -0
  49. package/dist/synk/session-client.js +240 -0
  50. package/dist/synk/types.d.ts +574 -0
  51. package/dist/synk/types.js +74 -0
  52. package/dist/utils/platform.d.ts +5 -1
  53. package/dist/utils/platform.js +24 -4
  54. package/dist/utils/proxy-url.d.ts +10 -0
  55. package/dist/utils/proxy-url.js +19 -0
  56. package/dist/utils/state.d.ts +1 -1
  57. package/dist/utils/state.js +11 -3
  58. package/package.json +13 -4
  59. package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +0 -819
  60. package/templates/claude-plugins-admin/README.md +0 -446
  61. package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +0 -8
  62. package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +0 -595
  63. package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +0 -8
  64. package/templates/claude-plugins-admin/backend-agent/commands/backend.md +0 -798
  65. package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +0 -8
  66. package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +0 -554
  67. package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +0 -8
  68. package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +0 -881
  69. package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +0 -8
  70. package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +0 -85
  71. package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +0 -8
  72. package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +0 -569
  73. package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +0 -8
  74. package/templates/claude-plugins-admin/qa-agent/commands/qa.md +0 -863
  75. package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +0 -8
  76. package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +0 -732
  77. package/templates/hooks-node/lib/state.js +0 -187
  78. package/templates/hooks-node/stop.js +0 -416
  79. package/templates/hooks-node/user-prompt-submit.js +0 -337
  80. package/templates/rules/00-hooks-contract.mdc +0 -89
  81. package/templates/rules/30-ekkos-core.mdc +0 -188
  82. package/templates/rules/31-ekkos-messages.mdc +0 -78
@@ -0,0 +1,393 @@
1
+ "use strict";
2
+ /**
3
+ * Local SQLite Memory Store
4
+ *
5
+ * Provides offline-capable memory storage using SQLite with WAL mode.
6
+ * Mirrors the Supabase schema for seamless sync.
7
+ * Location: ~/.ekkos/memory.db
8
+ *
9
+ * Dependency: better-sqlite3
10
+ * Add to package.json: "better-sqlite3": "^9.4.0"
11
+ * Dev dep: "@types/better-sqlite3": "^7.6.8"
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.localStore = exports.LocalMemoryStore = void 0;
15
+ // NOTE: better-sqlite3 is a native dependency that must be added to package.json:
16
+ // "better-sqlite3": "^9.4.0"
17
+ // "@types/better-sqlite3": "^7.6.8" (devDependencies)
18
+ //
19
+ // The Database type is declared inline below so TypeScript doesn't error when
20
+ // the package is absent. At runtime the module is loaded via require() inside
21
+ // init() and handled gracefully if missing.
22
+ const os_1 = require("os");
23
+ const path_1 = require("path");
24
+ const fs_1 = require("fs");
25
+ const crypto_1 = require("crypto");
26
+ const EKKOS_DIR = (0, path_1.join)((0, os_1.homedir)(), '.ekkos');
27
+ const DB_PATH = (0, path_1.join)(EKKOS_DIR, 'memory.db');
28
+ // ---------------------------------------------------------------------------
29
+ // LocalMemoryStore
30
+ // ---------------------------------------------------------------------------
31
+ class LocalMemoryStore {
32
+ constructor() {
33
+ this.db = null;
34
+ this.init();
35
+ }
36
+ // -------------------------------------------------------------------------
37
+ // Initialisation
38
+ // -------------------------------------------------------------------------
39
+ init() {
40
+ try {
41
+ // Ensure ~/.ekkos exists
42
+ if (!(0, fs_1.existsSync)(EKKOS_DIR)) {
43
+ (0, fs_1.mkdirSync)(EKKOS_DIR, { recursive: true });
44
+ }
45
+ // Dynamic require so the module gracefully fails if better-sqlite3 isn't installed
46
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
47
+ const DatabaseConstructor = require('better-sqlite3');
48
+ this.db = new DatabaseConstructor(DB_PATH);
49
+ // WAL mode for concurrent reads and crash safety
50
+ this.db.pragma('journal_mode = WAL');
51
+ this.db.pragma('foreign_keys = ON');
52
+ this.migrate();
53
+ }
54
+ catch (err) {
55
+ // If better-sqlite3 is not installed, store operates in no-op mode.
56
+ // The offline-fallback layer will handle the error gracefully.
57
+ console.warn('[LocalMemoryStore] better-sqlite3 not available — local store disabled:', err.message);
58
+ this.db = null;
59
+ }
60
+ }
61
+ get database() {
62
+ if (!this.db) {
63
+ throw new Error('Local SQLite store is not available. Install better-sqlite3 to enable offline mode.');
64
+ }
65
+ return this.db;
66
+ }
67
+ // -------------------------------------------------------------------------
68
+ // Schema migrations
69
+ // -------------------------------------------------------------------------
70
+ migrate() {
71
+ const db = this.database;
72
+ db.exec(`
73
+ CREATE TABLE IF NOT EXISTS patterns (
74
+ pattern_id TEXT PRIMARY KEY,
75
+ user_id TEXT NOT NULL,
76
+ title TEXT NOT NULL,
77
+ problem TEXT NOT NULL,
78
+ solution TEXT NOT NULL,
79
+ status TEXT NOT NULL DEFAULT 'active',
80
+ applied_count INTEGER NOT NULL DEFAULT 0,
81
+ success_rate REAL NOT NULL DEFAULT 0.5,
82
+ tags TEXT NOT NULL DEFAULT '[]',
83
+ quarantined INTEGER NOT NULL DEFAULT 0,
84
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
85
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
86
+ synced_at TEXT
87
+ );
88
+
89
+ CREATE TABLE IF NOT EXISTS directives (
90
+ id TEXT PRIMARY KEY,
91
+ user_id TEXT NOT NULL,
92
+ type TEXT NOT NULL,
93
+ rule TEXT NOT NULL,
94
+ reason TEXT NOT NULL,
95
+ priority INTEGER NOT NULL DEFAULT 50,
96
+ scope TEXT,
97
+ is_active INTEGER NOT NULL DEFAULT 1,
98
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
99
+ synced_at TEXT
100
+ );
101
+
102
+ CREATE TABLE IF NOT EXISTS episodic_memory (
103
+ id TEXT PRIMARY KEY,
104
+ user_id TEXT NOT NULL,
105
+ problem TEXT NOT NULL,
106
+ solution TEXT NOT NULL,
107
+ session_name TEXT,
108
+ quality_score REAL NOT NULL DEFAULT 0.5,
109
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
110
+ synced_at TEXT
111
+ );
112
+
113
+ CREATE TABLE IF NOT EXISTS sync_queue (
114
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
115
+ table_name TEXT NOT NULL,
116
+ record_id TEXT NOT NULL,
117
+ operation TEXT NOT NULL,
118
+ payload TEXT,
119
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
120
+ synced_at TEXT
121
+ );
122
+
123
+ CREATE TABLE IF NOT EXISTS meta (
124
+ key TEXT PRIMARY KEY,
125
+ value TEXT NOT NULL,
126
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
127
+ );
128
+
129
+ -- Indexes
130
+ CREATE INDEX IF NOT EXISTS idx_patterns_user_id ON patterns(user_id);
131
+ CREATE INDEX IF NOT EXISTS idx_patterns_status ON patterns(status);
132
+ CREATE INDEX IF NOT EXISTS idx_patterns_tags ON patterns(tags);
133
+ CREATE INDEX IF NOT EXISTS idx_directives_user ON directives(user_id, is_active);
134
+ CREATE INDEX IF NOT EXISTS idx_episodic_user ON episodic_memory(user_id);
135
+ CREATE INDEX IF NOT EXISTS idx_sync_queue_pending ON sync_queue(synced_at);
136
+ `);
137
+ }
138
+ // -------------------------------------------------------------------------
139
+ // Pattern helpers
140
+ // -------------------------------------------------------------------------
141
+ rowToPattern(row) {
142
+ return {
143
+ ...row,
144
+ tags: this.parseTags(row.tags),
145
+ quarantined: row.quarantined === 1,
146
+ };
147
+ }
148
+ parseTags(raw) {
149
+ try {
150
+ const parsed = JSON.parse(raw);
151
+ return Array.isArray(parsed) ? parsed : [];
152
+ }
153
+ catch {
154
+ return [];
155
+ }
156
+ }
157
+ // -------------------------------------------------------------------------
158
+ // Pattern operations
159
+ // -------------------------------------------------------------------------
160
+ /**
161
+ * Full-text search across title, problem, solution, and tags using LIKE.
162
+ * Falls back gracefully when FTS5 is unavailable.
163
+ */
164
+ searchPatterns(userId, query, limit = 10) {
165
+ const db = this.database;
166
+ const terms = query
167
+ .toLowerCase()
168
+ .split(/\s+/)
169
+ .filter(Boolean)
170
+ .map(t => `%${t}%`);
171
+ if (terms.length === 0) {
172
+ // Return recent active patterns when no query provided
173
+ const stmt = db.prepare(`
174
+ SELECT * FROM patterns
175
+ WHERE user_id = ? AND status = 'active' AND quarantined = 0
176
+ ORDER BY applied_count DESC, updated_at DESC
177
+ LIMIT ?
178
+ `);
179
+ return stmt.all(userId, limit).map(r => this.rowToPattern(r));
180
+ }
181
+ // Build WHERE clause: each term must match at least one of the searchable columns
182
+ const conditions = terms
183
+ .map(() => `(LOWER(title) LIKE ? OR LOWER(problem) LIKE ? OR LOWER(solution) LIKE ? OR LOWER(tags) LIKE ?)`)
184
+ .join(' AND ');
185
+ const params = terms.flatMap(t => [t, t, t, t]);
186
+ const stmt = db.prepare(`
187
+ SELECT * FROM patterns
188
+ WHERE user_id = ?
189
+ AND status = 'active'
190
+ AND quarantined = 0
191
+ AND (${conditions})
192
+ ORDER BY applied_count DESC, updated_at DESC
193
+ LIMIT ?
194
+ `);
195
+ return stmt.all(userId, ...params, limit).map(r => this.rowToPattern(r));
196
+ }
197
+ /**
198
+ * Insert a new pattern and enqueue it for sync.
199
+ * Returns the generated pattern_id.
200
+ */
201
+ forgePattern(userId, input) {
202
+ const db = this.database;
203
+ const patternId = (0, crypto_1.randomUUID)();
204
+ const tagsJson = JSON.stringify(input.tags ?? []);
205
+ const now = new Date().toISOString();
206
+ const stmt = db.prepare(`
207
+ INSERT INTO patterns (pattern_id, user_id, title, problem, solution, tags, created_at, updated_at)
208
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
209
+ `);
210
+ stmt.run(patternId, userId, input.title, input.problem, input.solution, tagsJson, now, now);
211
+ this.queueSync('patterns', patternId, 'insert', {
212
+ pattern_id: patternId,
213
+ user_id: userId,
214
+ title: input.title,
215
+ problem: input.problem,
216
+ solution: input.solution,
217
+ tags: input.tags ?? [],
218
+ created_at: now,
219
+ updated_at: now,
220
+ });
221
+ return patternId;
222
+ }
223
+ /**
224
+ * Increment applied_count and mark as needing sync.
225
+ */
226
+ trackApplication(patternId) {
227
+ const db = this.database;
228
+ const now = new Date().toISOString();
229
+ db.prepare(`
230
+ UPDATE patterns
231
+ SET applied_count = applied_count + 1, updated_at = ?, synced_at = NULL
232
+ WHERE pattern_id = ?
233
+ `).run(now, patternId);
234
+ const row = db.prepare('SELECT applied_count FROM patterns WHERE pattern_id = ?').get(patternId);
235
+ this.queueSync('patterns', patternId, 'update', { applied_count: row?.applied_count, updated_at: now });
236
+ }
237
+ /**
238
+ * Update success_rate using a weighted moving average and enqueue sync.
239
+ * Weight: 0.1 for new observation (exponential moving average).
240
+ */
241
+ recordOutcome(patternId, success) {
242
+ const db = this.database;
243
+ const now = new Date().toISOString();
244
+ const weight = 0.1;
245
+ const observation = success ? 1.0 : 0.0;
246
+ const row = db.prepare('SELECT success_rate FROM patterns WHERE pattern_id = ?').get(patternId);
247
+ if (!row)
248
+ return;
249
+ const newRate = row.success_rate * (1 - weight) + observation * weight;
250
+ db.prepare(`
251
+ UPDATE patterns
252
+ SET success_rate = ?, updated_at = ?, synced_at = NULL
253
+ WHERE pattern_id = ?
254
+ `).run(newRate, now, patternId);
255
+ this.queueSync('patterns', patternId, 'update', { success_rate: newRate, updated_at: now });
256
+ }
257
+ // -------------------------------------------------------------------------
258
+ // Directive operations
259
+ // -------------------------------------------------------------------------
260
+ /**
261
+ * Get active directives for a user, optionally filtered by type.
262
+ */
263
+ getDirectives(userId, types) {
264
+ const db = this.database;
265
+ if (types && types.length > 0) {
266
+ const placeholders = types.map(() => '?').join(', ');
267
+ const stmt = db.prepare(`
268
+ SELECT * FROM directives
269
+ WHERE user_id = ? AND is_active = 1 AND type IN (${placeholders})
270
+ ORDER BY priority DESC, created_at ASC
271
+ `);
272
+ return stmt.all(userId, ...types).map(r => this.rowToDirective(r));
273
+ }
274
+ const stmt = db.prepare(`
275
+ SELECT * FROM directives
276
+ WHERE user_id = ? AND is_active = 1
277
+ ORDER BY priority DESC, created_at ASC
278
+ `);
279
+ return stmt.all(userId).map(r => this.rowToDirective(r));
280
+ }
281
+ /**
282
+ * Insert a new directive and enqueue for sync.
283
+ * Returns the generated id.
284
+ */
285
+ createDirective(userId, input) {
286
+ const db = this.database;
287
+ const id = (0, crypto_1.randomUUID)();
288
+ const priority = input.priority ?? 50;
289
+ const now = new Date().toISOString();
290
+ db.prepare(`
291
+ INSERT INTO directives (id, user_id, type, rule, reason, priority, scope, created_at)
292
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
293
+ `).run(id, userId, input.type, input.rule, input.reason, priority, input.scope ?? null, now);
294
+ this.queueSync('directives', id, 'insert', { id, user_id: userId, ...input, priority, created_at: now });
295
+ return id;
296
+ }
297
+ /**
298
+ * Soft-delete a directive by setting is_active = 0.
299
+ */
300
+ deleteDirective(id) {
301
+ const db = this.database;
302
+ const now = new Date().toISOString();
303
+ db.prepare(`
304
+ UPDATE directives SET is_active = 0, synced_at = NULL WHERE id = ?
305
+ `).run(id);
306
+ this.queueSync('directives', id, 'update', { is_active: 0, updated_at: now });
307
+ }
308
+ rowToDirective(row) {
309
+ return {
310
+ ...row,
311
+ type: row.type,
312
+ is_active: row.is_active === 1,
313
+ };
314
+ }
315
+ // -------------------------------------------------------------------------
316
+ // Stats
317
+ // -------------------------------------------------------------------------
318
+ getStats(userId) {
319
+ const db = this.database;
320
+ const patternCount = db.prepare('SELECT COUNT(*) as n FROM patterns WHERE user_id = ?').get(userId).n;
321
+ const activeCount = db.prepare("SELECT COUNT(*) as n FROM patterns WHERE user_id = ? AND status = 'active' AND quarantined = 0").get(userId).n;
322
+ const dirCount = db.prepare('SELECT COUNT(*) as n FROM directives WHERE user_id = ? AND is_active = 1').get(userId).n;
323
+ const episodicCount = db.prepare('SELECT COUNT(*) as n FROM episodic_memory WHERE user_id = ?').get(userId).n;
324
+ const pendingSync = db.prepare('SELECT COUNT(*) as n FROM sync_queue WHERE synced_at IS NULL').get().n;
325
+ return {
326
+ pattern_count: patternCount,
327
+ active_pattern_count: activeCount,
328
+ directive_count: dirCount,
329
+ episodic_count: episodicCount,
330
+ pending_sync_count: pendingSync,
331
+ db_path: DB_PATH,
332
+ };
333
+ }
334
+ // -------------------------------------------------------------------------
335
+ // Sync queue
336
+ // -------------------------------------------------------------------------
337
+ getPendingSync() {
338
+ const db = this.database;
339
+ const rows = db.prepare('SELECT * FROM sync_queue WHERE synced_at IS NULL ORDER BY id ASC').all();
340
+ return rows.map(r => ({
341
+ ...r,
342
+ operation: r.operation,
343
+ payload: r.payload ? JSON.parse(r.payload) : null,
344
+ }));
345
+ }
346
+ markSynced(id) {
347
+ const db = this.database;
348
+ db.prepare('UPDATE sync_queue SET synced_at = ? WHERE id = ?').run(new Date().toISOString(), id);
349
+ }
350
+ // -------------------------------------------------------------------------
351
+ // Meta key/value store
352
+ // -------------------------------------------------------------------------
353
+ getMeta(key) {
354
+ const db = this.database;
355
+ const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key);
356
+ return row ? row.value : null;
357
+ }
358
+ setMeta(key, value) {
359
+ const db = this.database;
360
+ db.prepare(`
361
+ INSERT INTO meta (key, value, updated_at)
362
+ VALUES (?, ?, datetime('now'))
363
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
364
+ `).run(key, value);
365
+ }
366
+ // -------------------------------------------------------------------------
367
+ // Private helpers
368
+ // -------------------------------------------------------------------------
369
+ queueSync(tableName, recordId, operation, payload) {
370
+ try {
371
+ const db = this.database;
372
+ db.prepare(`
373
+ INSERT INTO sync_queue (table_name, record_id, operation, payload)
374
+ VALUES (?, ?, ?, ?)
375
+ `).run(tableName, recordId, operation, payload ? JSON.stringify(payload) : null);
376
+ }
377
+ catch (err) {
378
+ // Non-fatal — sync will pick up the record on next full reconciliation
379
+ console.warn('[LocalMemoryStore] Failed to queue sync item:', err.message);
380
+ }
381
+ }
382
+ // -------------------------------------------------------------------------
383
+ // Connectivity check
384
+ // -------------------------------------------------------------------------
385
+ isAvailable() {
386
+ return this.db !== null;
387
+ }
388
+ }
389
+ exports.LocalMemoryStore = LocalMemoryStore;
390
+ // ---------------------------------------------------------------------------
391
+ // Singleton export
392
+ // ---------------------------------------------------------------------------
393
+ exports.localStore = new LocalMemoryStore();
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Sync Engine
3
+ *
4
+ * Bidirectional sync between local SQLite and cloud Supabase.
5
+ * Strategy: Push local changes first, then pull remote changes.
6
+ * Conflict resolution: Last-write-wins for patterns, merge for tags/stats.
7
+ */
8
+ export interface SyncResult {
9
+ pushed: number;
10
+ pulled: number;
11
+ conflicts: number;
12
+ errors: string[];
13
+ duration_ms: number;
14
+ }
15
+ export declare class SyncEngine {
16
+ private readonly apiUrl;
17
+ private readonly apiKey;
18
+ /** Key used to persist last successful sync timestamp in the meta table */
19
+ private static readonly LAST_SYNC_KEY;
20
+ constructor(apiUrl: string, apiKey: string);
21
+ /**
22
+ * Perform a full push → pull sync cycle.
23
+ * Returns a summary of what was transferred.
24
+ */
25
+ sync(): Promise<SyncResult>;
26
+ /**
27
+ * Health-check: can we reach the remote API?
28
+ */
29
+ isOnline(): Promise<boolean>;
30
+ private push;
31
+ private pull;
32
+ private applyRemotePattern;
33
+ private applyRemoteDirective;
34
+ private mcpCall;
35
+ private authHeaders;
36
+ private resolveToolForTable;
37
+ }
38
+ /**
39
+ * Create a SyncEngine using environment variables or explicit config.
40
+ * Falls back to EKKOS_API_URL / EKKOS_API_KEY env vars.
41
+ */
42
+ export declare function createSyncEngine(apiUrl?: string, apiKey?: string): SyncEngine;
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ /**
3
+ * Sync Engine
4
+ *
5
+ * Bidirectional sync between local SQLite and cloud Supabase.
6
+ * Strategy: Push local changes first, then pull remote changes.
7
+ * Conflict resolution: Last-write-wins for patterns, merge for tags/stats.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.SyncEngine = void 0;
11
+ exports.createSyncEngine = createSyncEngine;
12
+ const sqlite_store_1 = require("./sqlite-store");
13
+ // ---------------------------------------------------------------------------
14
+ // SyncEngine
15
+ // ---------------------------------------------------------------------------
16
+ class SyncEngine {
17
+ constructor(apiUrl, apiKey) {
18
+ this.apiUrl = apiUrl.replace(/\/$/, ''); // strip trailing slash
19
+ this.apiKey = apiKey;
20
+ }
21
+ // -------------------------------------------------------------------------
22
+ // Public API
23
+ // -------------------------------------------------------------------------
24
+ /**
25
+ * Perform a full push → pull sync cycle.
26
+ * Returns a summary of what was transferred.
27
+ */
28
+ async sync() {
29
+ const start = Date.now();
30
+ const result = { pushed: 0, pulled: 0, conflicts: 0, errors: [], duration_ms: 0 };
31
+ if (!sqlite_store_1.localStore.isAvailable()) {
32
+ result.errors.push('Local store not available');
33
+ result.duration_ms = Date.now() - start;
34
+ return result;
35
+ }
36
+ const online = await this.isOnline();
37
+ if (!online) {
38
+ result.errors.push('Remote API unreachable — skipping sync');
39
+ result.duration_ms = Date.now() - start;
40
+ return result;
41
+ }
42
+ // 1. Push local changes to cloud
43
+ await this.push(result);
44
+ // 2. Pull remote changes since last sync
45
+ await this.pull(result);
46
+ // 3. Record sync timestamp
47
+ sqlite_store_1.localStore.setMeta(SyncEngine.LAST_SYNC_KEY, new Date().toISOString());
48
+ result.duration_ms = Date.now() - start;
49
+ return result;
50
+ }
51
+ /**
52
+ * Health-check: can we reach the remote API?
53
+ */
54
+ async isOnline() {
55
+ try {
56
+ const controller = new AbortController();
57
+ const timeout = setTimeout(() => controller.abort(), 3000);
58
+ const res = await fetch(`${this.apiUrl}/health`, {
59
+ method: 'GET',
60
+ signal: controller.signal,
61
+ headers: this.authHeaders(),
62
+ });
63
+ clearTimeout(timeout);
64
+ return res.ok;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ // -------------------------------------------------------------------------
71
+ // Push
72
+ // -------------------------------------------------------------------------
73
+ async push(result) {
74
+ const pending = sqlite_store_1.localStore.getPendingSync();
75
+ for (const item of pending) {
76
+ try {
77
+ const payload = {
78
+ tool: this.resolveToolForTable(item.table_name, item.operation),
79
+ args: item.payload ?? {},
80
+ };
81
+ const res = await this.mcpCall(payload);
82
+ if (res.ok) {
83
+ sqlite_store_1.localStore.markSynced(item.id);
84
+ result.pushed++;
85
+ }
86
+ else {
87
+ const body = await res.text();
88
+ result.errors.push(`Push failed for ${item.table_name}/${item.record_id}: ${body}`);
89
+ }
90
+ }
91
+ catch (err) {
92
+ result.errors.push(`Push error for ${item.table_name}/${item.record_id}: ${err.message}`);
93
+ }
94
+ }
95
+ }
96
+ // -------------------------------------------------------------------------
97
+ // Pull
98
+ // -------------------------------------------------------------------------
99
+ async pull(result) {
100
+ const since = sqlite_store_1.localStore.getMeta(SyncEngine.LAST_SYNC_KEY);
101
+ try {
102
+ const res = await this.mcpCall({
103
+ tool: 'ekkOS_GetChanges',
104
+ args: { since: since ?? '1970-01-01T00:00:00.000Z' },
105
+ });
106
+ if (!res.ok) {
107
+ result.errors.push(`Pull failed: ${await res.text()}`);
108
+ return;
109
+ }
110
+ const remote = await res.json();
111
+ // Apply remote patterns
112
+ for (const rp of remote.patterns ?? []) {
113
+ this.applyRemotePattern(rp, result);
114
+ }
115
+ // Apply remote directives
116
+ for (const rd of remote.directives ?? []) {
117
+ this.applyRemoteDirective(rd, result);
118
+ }
119
+ }
120
+ catch (err) {
121
+ result.errors.push(`Pull error: ${err.message}`);
122
+ }
123
+ }
124
+ // -------------------------------------------------------------------------
125
+ // Conflict resolution — last-write-wins
126
+ // -------------------------------------------------------------------------
127
+ applyRemotePattern(remote, result) {
128
+ // We attempt to find the local record's updated_at to compare timestamps.
129
+ // If local is newer, skip. If remote is newer (or record doesn't exist locally), apply.
130
+ try {
131
+ // Upsert via the local store's raw DB access isn't exposed publicly,
132
+ // so we use forgePattern for new records and a direct SQL update for existing ones.
133
+ // For full correctness the local DB would need an upsert-with-timestamp method;
134
+ // this scaffolding queues a conflict marker so the host app can resolve it.
135
+ // Simple heuristic: forge with remote data tagged as pulled from cloud.
136
+ // The sync_queue item will be marked synced immediately (it came FROM the cloud).
137
+ sqlite_store_1.localStore.forgePattern(remote.user_id, {
138
+ title: remote.title,
139
+ problem: remote.problem,
140
+ solution: remote.solution,
141
+ tags: remote.tags,
142
+ });
143
+ // Mark the queued sync item as already synced so it doesn't bounce back
144
+ const pending = sqlite_store_1.localStore.getPendingSync();
145
+ const last = pending[pending.length - 1];
146
+ if (last) {
147
+ sqlite_store_1.localStore.markSynced(last.id);
148
+ }
149
+ result.pulled++;
150
+ }
151
+ catch (err) {
152
+ result.conflicts++;
153
+ result.errors.push(`Conflict applying remote pattern ${remote.pattern_id}: ${err.message}`);
154
+ }
155
+ }
156
+ applyRemoteDirective(remote, result) {
157
+ try {
158
+ sqlite_store_1.localStore.createDirective(remote.user_id, {
159
+ type: remote.type,
160
+ rule: remote.rule,
161
+ reason: remote.reason,
162
+ priority: remote.priority,
163
+ scope: remote.scope ?? undefined,
164
+ });
165
+ // Mark the enqueued sync item as pre-synced (came from cloud)
166
+ const pending = sqlite_store_1.localStore.getPendingSync();
167
+ const last = pending[pending.length - 1];
168
+ if (last) {
169
+ sqlite_store_1.localStore.markSynced(last.id);
170
+ }
171
+ result.pulled++;
172
+ }
173
+ catch (err) {
174
+ result.conflicts++;
175
+ result.errors.push(`Conflict applying remote directive ${remote.id}: ${err.message}`);
176
+ }
177
+ }
178
+ // -------------------------------------------------------------------------
179
+ // HTTP helpers
180
+ // -------------------------------------------------------------------------
181
+ async mcpCall(payload) {
182
+ return fetch(`${this.apiUrl}/api/v1/mcp/call`, {
183
+ method: 'POST',
184
+ headers: {
185
+ 'Content-Type': 'application/json',
186
+ ...this.authHeaders(),
187
+ },
188
+ body: JSON.stringify(payload),
189
+ });
190
+ }
191
+ authHeaders() {
192
+ return {
193
+ Authorization: `Bearer ${this.apiKey}`,
194
+ 'X-ekkOS-Client': 'cli-sync-engine',
195
+ };
196
+ }
197
+ // -------------------------------------------------------------------------
198
+ // Table → MCP tool mapping
199
+ // -------------------------------------------------------------------------
200
+ resolveToolForTable(tableName, operation) {
201
+ const map = {
202
+ patterns: 'ekkOS_Forge',
203
+ directives: 'ekkOS_Directive',
204
+ episodic_memory: 'ekkOS_Capture',
205
+ };
206
+ return map[tableName] ?? `ekkOS_Sync_${tableName}_${operation}`;
207
+ }
208
+ }
209
+ exports.SyncEngine = SyncEngine;
210
+ /** Key used to persist last successful sync timestamp in the meta table */
211
+ SyncEngine.LAST_SYNC_KEY = 'last_sync_at';
212
+ // ---------------------------------------------------------------------------
213
+ // Factory helper
214
+ // ---------------------------------------------------------------------------
215
+ /**
216
+ * Create a SyncEngine using environment variables or explicit config.
217
+ * Falls back to EKKOS_API_URL / EKKOS_API_KEY env vars.
218
+ */
219
+ function createSyncEngine(apiUrl, apiKey) {
220
+ const url = apiUrl ?? process.env.EKKOS_API_URL ?? 'https://api.ekkos.dev';
221
+ const key = apiKey ?? process.env.EKKOS_API_KEY ?? '';
222
+ return new SyncEngine(url, key);
223
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * API client for ekkOS_synk server
3
+ */
4
+ import type { AgentState, Metadata, Session, Machine, MachineMetadata, DaemonState } from './types';
5
+ import type { Credentials } from './persistence';
6
+ export declare class ApiClient {
7
+ private readonly credential;
8
+ static create(credential: Credentials): Promise<ApiClient>;
9
+ private constructor();
10
+ /** Create a new session or load existing one with the given tag */
11
+ getOrCreateSession(opts: {
12
+ tag: string;
13
+ metadata: Metadata;
14
+ state: AgentState | null;
15
+ }): Promise<Session | null>;
16
+ /** Register or update machine with the server */
17
+ getOrCreateMachine(opts: {
18
+ machineId: string;
19
+ metadata: MachineMetadata;
20
+ daemonState?: DaemonState;
21
+ }): Promise<Machine>;
22
+ }