@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.
- package/dist/commands/dashboard.js +147 -57
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +54 -16
- package/dist/commands/run.js +163 -44
- package/dist/commands/status.d.ts +4 -1
- package/dist/commands/status.js +165 -27
- package/dist/commands/synk.d.ts +7 -0
- package/dist/commands/synk.js +339 -0
- package/dist/deploy/settings.d.ts +6 -5
- package/dist/deploy/settings.js +27 -17
- package/dist/index.js +12 -82
- package/dist/lib/usage-parser.d.ts +1 -1
- package/dist/lib/usage-parser.js +5 -3
- package/dist/local/index.d.ts +14 -0
- package/dist/local/index.js +28 -0
- package/dist/local/local-embeddings.d.ts +49 -0
- package/dist/local/local-embeddings.js +232 -0
- package/dist/local/offline-fallback.d.ts +44 -0
- package/dist/local/offline-fallback.js +159 -0
- package/dist/local/sqlite-store.d.ts +126 -0
- package/dist/local/sqlite-store.js +393 -0
- package/dist/local/sync-engine.d.ts +42 -0
- package/dist/local/sync-engine.js +223 -0
- package/dist/synk/api.d.ts +22 -0
- package/dist/synk/api.js +133 -0
- package/dist/synk/auth.d.ts +7 -0
- package/dist/synk/auth.js +30 -0
- package/dist/synk/config.d.ts +18 -0
- package/dist/synk/config.js +37 -0
- package/dist/synk/daemon/control-client.d.ts +11 -0
- package/dist/synk/daemon/control-client.js +101 -0
- package/dist/synk/daemon/control-server.d.ts +24 -0
- package/dist/synk/daemon/control-server.js +91 -0
- package/dist/synk/daemon/run.d.ts +14 -0
- package/dist/synk/daemon/run.js +338 -0
- package/dist/synk/encryption.d.ts +17 -0
- package/dist/synk/encryption.js +133 -0
- package/dist/synk/index.d.ts +13 -0
- package/dist/synk/index.js +36 -0
- package/dist/synk/machine-client.d.ts +42 -0
- package/dist/synk/machine-client.js +218 -0
- package/dist/synk/persistence.d.ts +51 -0
- package/dist/synk/persistence.js +211 -0
- package/dist/synk/qr.d.ts +5 -0
- package/dist/synk/qr.js +33 -0
- package/dist/synk/session-bridge.d.ts +58 -0
- package/dist/synk/session-bridge.js +171 -0
- package/dist/synk/session-client.d.ts +46 -0
- package/dist/synk/session-client.js +240 -0
- package/dist/synk/types.d.ts +574 -0
- package/dist/synk/types.js +74 -0
- package/dist/utils/platform.d.ts +5 -1
- package/dist/utils/platform.js +24 -4
- package/dist/utils/proxy-url.d.ts +10 -0
- package/dist/utils/proxy-url.js +19 -0
- package/dist/utils/state.d.ts +1 -1
- package/dist/utils/state.js +11 -3
- package/package.json +13 -4
- package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +0 -819
- package/templates/claude-plugins-admin/README.md +0 -446
- package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +0 -595
- package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/backend-agent/commands/backend.md +0 -798
- package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +0 -554
- package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +0 -881
- package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +0 -85
- package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +0 -569
- package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/qa-agent/commands/qa.md +0 -863
- package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +0 -732
- package/templates/hooks-node/lib/state.js +0 -187
- package/templates/hooks-node/stop.js +0 -416
- package/templates/hooks-node/user-prompt-submit.js +0 -337
- package/templates/rules/00-hooks-contract.mdc +0 -89
- package/templates/rules/30-ekkos-core.mdc +0 -188
- 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;
|