@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,49 @@
1
+ /**
2
+ * Local Embeddings using ONNX Runtime
3
+ *
4
+ * Uses bge-small-en-v1.5 (384 dimensions) for offline similarity search.
5
+ * Same model OMEGA uses — lightweight, good quality.
6
+ *
7
+ * Note: Local embeddings are 384-dim (bge-small-en-v1.5)
8
+ * Cloud embeddings are 1536-dim (OpenAI text-embedding-3-small)
9
+ * On sync: re-embed with cloud model for consistency
10
+ *
11
+ * Optional dependency: @xenova/transformers
12
+ * If not installed, falls back to TF-IDF keyword similarity.
13
+ */
14
+ export interface SimilarityResult {
15
+ id: string;
16
+ score: number;
17
+ }
18
+ /**
19
+ * Generate a 384-dimensional embedding vector using bge-small-en-v1.5.
20
+ * Falls back to an empty array if @xenova/transformers is not installed.
21
+ * Callers should check array length before using for similarity.
22
+ */
23
+ export declare function generateLocalEmbedding(text: string): Promise<number[]>;
24
+ /**
25
+ * Cosine similarity between two dense numeric vectors.
26
+ * Returns a value in [-1, 1] (1 = identical direction).
27
+ */
28
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
29
+ /**
30
+ * Rank candidates by semantic similarity to the query.
31
+ *
32
+ * Tries ONNX embedding first; falls back to TF-IDF keyword similarity if
33
+ * @xenova/transformers is not installed or if embedding fails.
34
+ *
35
+ * @param query - Search query string
36
+ * @param candidates - Items to rank (each has an `id` and `text` for embedding)
37
+ * @param topK - How many results to return (default 5)
38
+ *
39
+ * @returns Ranked array of { id, score } sorted by descending similarity
40
+ */
41
+ export declare function searchByEmbedding(query: string, candidates: {
42
+ id: string;
43
+ text: string;
44
+ }[], topK?: number): Promise<SimilarityResult[]>;
45
+ /**
46
+ * Check whether the ONNX embedding pipeline is available.
47
+ * Returns false if @xenova/transformers is not installed.
48
+ */
49
+ export declare function isEmbeddingAvailable(): Promise<boolean>;
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ /**
3
+ * Local Embeddings using ONNX Runtime
4
+ *
5
+ * Uses bge-small-en-v1.5 (384 dimensions) for offline similarity search.
6
+ * Same model OMEGA uses — lightweight, good quality.
7
+ *
8
+ * Note: Local embeddings are 384-dim (bge-small-en-v1.5)
9
+ * Cloud embeddings are 1536-dim (OpenAI text-embedding-3-small)
10
+ * On sync: re-embed with cloud model for consistency
11
+ *
12
+ * Optional dependency: @xenova/transformers
13
+ * If not installed, falls back to TF-IDF keyword similarity.
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.generateLocalEmbedding = generateLocalEmbedding;
50
+ exports.cosineSimilarity = cosineSimilarity;
51
+ exports.searchByEmbedding = searchByEmbedding;
52
+ exports.isEmbeddingAvailable = isEmbeddingAvailable;
53
+ let _pipeline = null;
54
+ let _pipelineLoading = null;
55
+ let _transformersAvailable = null; // null = not yet checked
56
+ async function getTransformerPipeline() {
57
+ if (_transformersAvailable === false)
58
+ return null;
59
+ if (_pipeline)
60
+ return _pipeline;
61
+ if (_pipelineLoading)
62
+ return _pipelineLoading;
63
+ _pipelineLoading = (async () => {
64
+ try {
65
+ // Dynamic import — fails gracefully if package not installed
66
+ const mod = await Promise.resolve(`${'@xenova/transformers'}`).then(s => __importStar(require(s)));
67
+ const { pipeline } = mod;
68
+ // Load the bge-small-en-v1.5 model (384 dims, ~33MB)
69
+ const featureExtractor = await pipeline('feature-extraction', 'Xenova/bge-small-en-v1.5', {
70
+ quantized: true, // use int8 quantised version for speed
71
+ });
72
+ // Wrap in a normalised shape so callers get consistent output
73
+ const wrapped = async (text) => {
74
+ const output = await featureExtractor(text, { pooling: 'mean', normalize: true });
75
+ return output;
76
+ };
77
+ _pipeline = wrapped;
78
+ _transformersAvailable = true;
79
+ return wrapped;
80
+ }
81
+ catch {
82
+ _transformersAvailable = false;
83
+ return null;
84
+ }
85
+ })();
86
+ return _pipelineLoading;
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // TF-IDF fallback
90
+ // ---------------------------------------------------------------------------
91
+ /**
92
+ * Build a simple term-frequency vector from a piece of text.
93
+ * Returns a sparse Map<term, tf> (normalised by document length).
94
+ */
95
+ function buildTfVector(text) {
96
+ const tokens = tokenise(text);
97
+ const tf = new Map();
98
+ for (const token of tokens) {
99
+ tf.set(token, (tf.get(token) ?? 0) + 1);
100
+ }
101
+ // Normalise by document length
102
+ for (const [term, count] of tf) {
103
+ tf.set(term, count / tokens.length);
104
+ }
105
+ return tf;
106
+ }
107
+ function tokenise(text) {
108
+ return text
109
+ .toLowerCase()
110
+ .replace(/[^a-z0-9\s]/g, ' ')
111
+ .split(/\s+/)
112
+ .filter(t => t.length > 1 && !STOP_WORDS.has(t));
113
+ }
114
+ /** Minimal English stop-word list */
115
+ const STOP_WORDS = new Set([
116
+ 'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
117
+ 'of', 'is', 'it', 'its', 'be', 'by', 'as', 'are', 'was', 'were', 'with',
118
+ 'this', 'that', 'from', 'not', 'we', 'i', 'you', 'he', 'she', 'they',
119
+ 'do', 'does', 'did', 'have', 'has', 'had', 'can', 'will', 'would', 'should',
120
+ 'so', 'if', 'up', 'out', 'no', 'my', 'your', 'our', 'all', 'more', 'also',
121
+ ]);
122
+ /**
123
+ * Cosine similarity between two TF sparse vectors.
124
+ */
125
+ function sparseCosine(a, b) {
126
+ let dot = 0;
127
+ let normA = 0;
128
+ let normB = 0;
129
+ for (const [term, va] of a) {
130
+ normA += va * va;
131
+ const vb = b.get(term) ?? 0;
132
+ dot += va * vb;
133
+ }
134
+ for (const [, vb] of b) {
135
+ normB += vb * vb;
136
+ }
137
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
138
+ return denom === 0 ? 0 : dot / denom;
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // Exported functions
142
+ // ---------------------------------------------------------------------------
143
+ /**
144
+ * Generate a 384-dimensional embedding vector using bge-small-en-v1.5.
145
+ * Falls back to an empty array if @xenova/transformers is not installed.
146
+ * Callers should check array length before using for similarity.
147
+ */
148
+ async function generateLocalEmbedding(text) {
149
+ const pipe = await getTransformerPipeline();
150
+ if (!pipe)
151
+ return [];
152
+ try {
153
+ const result = await pipe(text);
154
+ return Array.from(result.data);
155
+ }
156
+ catch (err) {
157
+ console.warn('[LocalEmbeddings] generateLocalEmbedding failed:', err.message);
158
+ return [];
159
+ }
160
+ }
161
+ /**
162
+ * Cosine similarity between two dense numeric vectors.
163
+ * Returns a value in [-1, 1] (1 = identical direction).
164
+ */
165
+ function cosineSimilarity(a, b) {
166
+ if (a.length === 0 || b.length === 0 || a.length !== b.length)
167
+ return 0;
168
+ let dot = 0;
169
+ let normA = 0;
170
+ let normB = 0;
171
+ for (let i = 0; i < a.length; i++) {
172
+ dot += a[i] * b[i];
173
+ normA += a[i] * a[i];
174
+ normB += b[i] * b[i];
175
+ }
176
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
177
+ return denom === 0 ? 0 : dot / denom;
178
+ }
179
+ /**
180
+ * Rank candidates by semantic similarity to the query.
181
+ *
182
+ * Tries ONNX embedding first; falls back to TF-IDF keyword similarity if
183
+ * @xenova/transformers is not installed or if embedding fails.
184
+ *
185
+ * @param query - Search query string
186
+ * @param candidates - Items to rank (each has an `id` and `text` for embedding)
187
+ * @param topK - How many results to return (default 5)
188
+ *
189
+ * @returns Ranked array of { id, score } sorted by descending similarity
190
+ */
191
+ async function searchByEmbedding(query, candidates, topK = 5) {
192
+ if (candidates.length === 0)
193
+ return [];
194
+ const pipe = await getTransformerPipeline();
195
+ if (pipe) {
196
+ // --- ONNX path ---
197
+ try {
198
+ const queryVec = await generateLocalEmbedding(query);
199
+ if (queryVec.length === 0)
200
+ throw new Error('Empty query embedding');
201
+ const scored = await Promise.all(candidates.map(async (c) => {
202
+ const vec = await generateLocalEmbedding(c.text);
203
+ return { id: c.id, score: cosineSimilarity(queryVec, vec) };
204
+ }));
205
+ return scored
206
+ .sort((a, b) => b.score - a.score)
207
+ .slice(0, topK);
208
+ }
209
+ catch (err) {
210
+ console.warn('[LocalEmbeddings] ONNX search failed, falling back to TF-IDF:', err.message);
211
+ }
212
+ }
213
+ // --- TF-IDF keyword fallback ---
214
+ const queryTf = buildTfVector(query);
215
+ const scored = candidates.map((c) => ({
216
+ id: c.id,
217
+ score: sparseCosine(queryTf, buildTfVector(c.text)),
218
+ }));
219
+ return scored
220
+ .sort((a, b) => b.score - a.score)
221
+ .slice(0, topK);
222
+ }
223
+ /**
224
+ * Check whether the ONNX embedding pipeline is available.
225
+ * Returns false if @xenova/transformers is not installed.
226
+ */
227
+ async function isEmbeddingAvailable() {
228
+ if (_transformersAvailable !== null)
229
+ return _transformersAvailable;
230
+ const pipe = await getTransformerPipeline();
231
+ return pipe !== null;
232
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Offline MCP Fallback
3
+ *
4
+ * When the remote API is unreachable, falls back to local SQLite.
5
+ * Transparent to callers — same interface, different backend.
6
+ *
7
+ * Tool coverage:
8
+ * ekkOS_Search → localStore.searchPatterns
9
+ * ekkOS_Forge → localStore.forgePattern
10
+ * ekkOS_Track → localStore.trackApplication
11
+ * ekkOS_Outcome → localStore.recordOutcome
12
+ * ekkOS_Directive→ localStore.createDirective
13
+ * ekkOS_Stats → localStore.getStats
14
+ */
15
+ declare const OFFLINE_CAPABLE_TOOLS: readonly ["ekkOS_Search", "ekkOS_Forge", "ekkOS_Track", "ekkOS_Outcome", "ekkOS_Directive", "ekkOS_Stats"];
16
+ export interface ToolCallResult {
17
+ success: boolean;
18
+ data?: any;
19
+ error?: string;
20
+ /** Present when the call was served from local store rather than the cloud */
21
+ _degraded?: boolean;
22
+ /** Present when the call was served from local store */
23
+ _local?: boolean;
24
+ }
25
+ export type RemoteCallFn = () => Promise<any>;
26
+ /**
27
+ * Attempt the remote MCP call. If it times out or throws, transparently fall
28
+ * back to the local SQLite store for supported tools.
29
+ *
30
+ * @param tool - ekkOS tool name (e.g. 'ekkOS_Search')
31
+ * @param args - Arguments to pass to the tool / local equivalent
32
+ * @param remoteCall - Zero-arg async function that performs the actual remote call
33
+ *
34
+ * @returns ToolCallResult with optional `_local: true` flag when served locally
35
+ */
36
+ export declare function callWithFallback(tool: string, args: Record<string, any>, remoteCall: RemoteCallFn): Promise<ToolCallResult>;
37
+ /**
38
+ * Convenience check: is the offline fallback layer ready to serve requests?
39
+ */
40
+ export declare function isOfflineFallbackReady(): boolean;
41
+ /**
42
+ * Exported constant for consumers that need to know which tools work offline.
43
+ */
44
+ export { OFFLINE_CAPABLE_TOOLS };
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ /**
3
+ * Offline MCP Fallback
4
+ *
5
+ * When the remote API is unreachable, falls back to local SQLite.
6
+ * Transparent to callers — same interface, different backend.
7
+ *
8
+ * Tool coverage:
9
+ * ekkOS_Search → localStore.searchPatterns
10
+ * ekkOS_Forge → localStore.forgePattern
11
+ * ekkOS_Track → localStore.trackApplication
12
+ * ekkOS_Outcome → localStore.recordOutcome
13
+ * ekkOS_Directive→ localStore.createDirective
14
+ * ekkOS_Stats → localStore.getStats
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.OFFLINE_CAPABLE_TOOLS = void 0;
18
+ exports.callWithFallback = callWithFallback;
19
+ exports.isOfflineFallbackReady = isOfflineFallbackReady;
20
+ const sqlite_store_1 = require("./sqlite-store");
21
+ // ---------------------------------------------------------------------------
22
+ // Constants
23
+ // ---------------------------------------------------------------------------
24
+ const MCP_TIMEOUT = 3000; // 3 seconds
25
+ const OFFLINE_CAPABLE_TOOLS = [
26
+ 'ekkOS_Search',
27
+ 'ekkOS_Forge',
28
+ 'ekkOS_Track',
29
+ 'ekkOS_Outcome',
30
+ 'ekkOS_Directive',
31
+ 'ekkOS_Stats',
32
+ ];
33
+ exports.OFFLINE_CAPABLE_TOOLS = OFFLINE_CAPABLE_TOOLS;
34
+ // ---------------------------------------------------------------------------
35
+ // Helpers
36
+ // ---------------------------------------------------------------------------
37
+ function isOfflineCapable(tool) {
38
+ return OFFLINE_CAPABLE_TOOLS.includes(tool);
39
+ }
40
+ function withTimeout(promise, ms) {
41
+ return Promise.race([
42
+ promise,
43
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)),
44
+ ]);
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Local tool dispatch
48
+ // ---------------------------------------------------------------------------
49
+ function dispatchLocal(tool, args) {
50
+ if (!sqlite_store_1.localStore.isAvailable()) {
51
+ return {
52
+ success: false,
53
+ error: 'Local store not available (better-sqlite3 not installed)',
54
+ _degraded: true,
55
+ };
56
+ }
57
+ try {
58
+ switch (tool) {
59
+ case 'ekkOS_Search': {
60
+ const { user_id, query, limit } = args;
61
+ const patterns = sqlite_store_1.localStore.searchPatterns(user_id, query, limit);
62
+ return {
63
+ success: true,
64
+ data: { patterns, source: 'local', total: patterns.length },
65
+ _local: true,
66
+ };
67
+ }
68
+ case 'ekkOS_Forge': {
69
+ const { user_id, title, problem, solution, tags } = args;
70
+ const patternId = sqlite_store_1.localStore.forgePattern(user_id, { title, problem, solution, tags });
71
+ return {
72
+ success: true,
73
+ data: { pattern_id: patternId, source: 'local' },
74
+ _local: true,
75
+ };
76
+ }
77
+ case 'ekkOS_Track': {
78
+ const { pattern_id } = args;
79
+ sqlite_store_1.localStore.trackApplication(pattern_id);
80
+ return { success: true, data: { tracked: true, source: 'local' }, _local: true };
81
+ }
82
+ case 'ekkOS_Outcome': {
83
+ const { pattern_id, success } = args;
84
+ sqlite_store_1.localStore.recordOutcome(pattern_id, success);
85
+ return { success: true, data: { recorded: true, source: 'local' }, _local: true };
86
+ }
87
+ case 'ekkOS_Directive': {
88
+ const { user_id, type, rule, reason, priority, scope } = args;
89
+ const id = sqlite_store_1.localStore.createDirective(user_id, { type, rule, reason, priority, scope });
90
+ return {
91
+ success: true,
92
+ data: { id, source: 'local' },
93
+ _local: true,
94
+ };
95
+ }
96
+ case 'ekkOS_Stats': {
97
+ const { user_id } = args;
98
+ const stats = sqlite_store_1.localStore.getStats(user_id);
99
+ return { success: true, data: { ...stats, source: 'local' }, _local: true };
100
+ }
101
+ default: {
102
+ // TypeScript exhaustiveness guard — should never reach here
103
+ const _never = tool;
104
+ return {
105
+ success: false,
106
+ error: `Unknown offline-capable tool: ${_never}`,
107
+ _degraded: true,
108
+ };
109
+ }
110
+ }
111
+ }
112
+ catch (err) {
113
+ return {
114
+ success: false,
115
+ error: `Local store error: ${err.message}`,
116
+ _degraded: true,
117
+ };
118
+ }
119
+ }
120
+ // ---------------------------------------------------------------------------
121
+ // Main export: callWithFallback
122
+ // ---------------------------------------------------------------------------
123
+ /**
124
+ * Attempt the remote MCP call. If it times out or throws, transparently fall
125
+ * back to the local SQLite store for supported tools.
126
+ *
127
+ * @param tool - ekkOS tool name (e.g. 'ekkOS_Search')
128
+ * @param args - Arguments to pass to the tool / local equivalent
129
+ * @param remoteCall - Zero-arg async function that performs the actual remote call
130
+ *
131
+ * @returns ToolCallResult with optional `_local: true` flag when served locally
132
+ */
133
+ async function callWithFallback(tool, args, remoteCall) {
134
+ // Attempt remote call with timeout
135
+ try {
136
+ const data = await withTimeout(remoteCall(), MCP_TIMEOUT);
137
+ return { success: true, data };
138
+ }
139
+ catch (err) {
140
+ const reason = err.message;
141
+ // Check whether we can serve this tool locally
142
+ if (!isOfflineCapable(tool)) {
143
+ return {
144
+ success: false,
145
+ error: `Offline — tool not available locally (${reason})`,
146
+ _degraded: true,
147
+ };
148
+ }
149
+ // Log the fallback so callers / users are aware
150
+ console.warn(`[OfflineFallback] Remote call failed for ${tool} (${reason}) — using local store`);
151
+ return dispatchLocal(tool, args);
152
+ }
153
+ }
154
+ /**
155
+ * Convenience check: is the offline fallback layer ready to serve requests?
156
+ */
157
+ function isOfflineFallbackReady() {
158
+ return sqlite_store_1.localStore.isAvailable();
159
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Local SQLite Memory Store
3
+ *
4
+ * Provides offline-capable memory storage using SQLite with WAL mode.
5
+ * Mirrors the Supabase schema for seamless sync.
6
+ * Location: ~/.ekkos/memory.db
7
+ *
8
+ * Dependency: better-sqlite3
9
+ * Add to package.json: "better-sqlite3": "^9.4.0"
10
+ * Dev dep: "@types/better-sqlite3": "^7.6.8"
11
+ */
12
+ export interface Pattern {
13
+ pattern_id: string;
14
+ user_id: string;
15
+ title: string;
16
+ problem: string;
17
+ solution: string;
18
+ status: string;
19
+ applied_count: number;
20
+ success_rate: number;
21
+ tags: string[];
22
+ quarantined: boolean;
23
+ created_at: string;
24
+ updated_at: string;
25
+ synced_at: string | null;
26
+ }
27
+ export interface Directive {
28
+ id: string;
29
+ user_id: string;
30
+ type: 'MUST' | 'NEVER' | 'PREFER' | 'AVOID';
31
+ rule: string;
32
+ reason: string;
33
+ priority: number;
34
+ scope: string | null;
35
+ is_active: boolean;
36
+ created_at: string;
37
+ synced_at: string | null;
38
+ }
39
+ export interface EpisodicMemory {
40
+ id: string;
41
+ user_id: string;
42
+ problem: string;
43
+ solution: string;
44
+ session_name: string | null;
45
+ quality_score: number;
46
+ created_at: string;
47
+ synced_at: string | null;
48
+ }
49
+ export interface SyncQueueItem {
50
+ id: number;
51
+ table_name: string;
52
+ record_id: string;
53
+ operation: 'insert' | 'update' | 'delete';
54
+ payload: any;
55
+ created_at: string;
56
+ synced_at: string | null;
57
+ }
58
+ export interface StoreStats {
59
+ pattern_count: number;
60
+ active_pattern_count: number;
61
+ directive_count: number;
62
+ episodic_count: number;
63
+ pending_sync_count: number;
64
+ db_path: string;
65
+ }
66
+ export declare class LocalMemoryStore {
67
+ private db;
68
+ constructor();
69
+ private init;
70
+ private get database();
71
+ private migrate;
72
+ private rowToPattern;
73
+ private parseTags;
74
+ /**
75
+ * Full-text search across title, problem, solution, and tags using LIKE.
76
+ * Falls back gracefully when FTS5 is unavailable.
77
+ */
78
+ searchPatterns(userId: string, query: string, limit?: number): Pattern[];
79
+ /**
80
+ * Insert a new pattern and enqueue it for sync.
81
+ * Returns the generated pattern_id.
82
+ */
83
+ forgePattern(userId: string, input: {
84
+ title: string;
85
+ problem: string;
86
+ solution: string;
87
+ tags?: string[];
88
+ }): string;
89
+ /**
90
+ * Increment applied_count and mark as needing sync.
91
+ */
92
+ trackApplication(patternId: string): void;
93
+ /**
94
+ * Update success_rate using a weighted moving average and enqueue sync.
95
+ * Weight: 0.1 for new observation (exponential moving average).
96
+ */
97
+ recordOutcome(patternId: string, success: boolean): void;
98
+ /**
99
+ * Get active directives for a user, optionally filtered by type.
100
+ */
101
+ getDirectives(userId: string, types?: string[]): Directive[];
102
+ /**
103
+ * Insert a new directive and enqueue for sync.
104
+ * Returns the generated id.
105
+ */
106
+ createDirective(userId: string, input: {
107
+ type: string;
108
+ rule: string;
109
+ reason: string;
110
+ priority?: number;
111
+ scope?: string;
112
+ }): string;
113
+ /**
114
+ * Soft-delete a directive by setting is_active = 0.
115
+ */
116
+ deleteDirective(id: string): void;
117
+ private rowToDirective;
118
+ getStats(userId: string): StoreStats;
119
+ getPendingSync(): SyncQueueItem[];
120
+ markSynced(id: number): void;
121
+ getMeta(key: string): string | null;
122
+ setMeta(key: string, value: string): void;
123
+ private queueSync;
124
+ isAvailable(): boolean;
125
+ }
126
+ export declare const localStore: LocalMemoryStore;