specmem-hardwicksoftware 3.7.17 â 3.7.20
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/README.md +87 -8
- package/dist/codebase/codeAnalyzer.js +1155 -0
- package/dist/codebase/codebaseIndexer.js +1 -1
- package/dist/database.js +12 -1
- package/dist/mcp/toolRegistry.js +4 -2
- package/dist/tools/goofy/exportProjectMemories.js +243 -0
- package/dist/tools/goofy/findWhatISaid.js +1 -1
- package/dist/tools/goofy/importProjectMemories.js +9 -9
- package/embedding-sandbox/frankenstein-embeddings.py +32 -16
- package/embedding-sandbox/server.mjs +40 -7
- package/mcp-proxy.cjs +92 -35
- package/package.json +14 -3
- package/scripts/specmem-init.cjs +1 -1
|
@@ -900,7 +900,7 @@ export class CodebaseIndexer {
|
|
|
900
900
|
const analyzableLanguages = [
|
|
901
901
|
'typescript', 'typescript-react', 'javascript', 'javascript-react',
|
|
902
902
|
'python', 'go', 'rust', 'java', 'kotlin', 'scala',
|
|
903
|
-
'ruby', 'php', 'c', 'cpp', 'swift'
|
|
903
|
+
'ruby', 'php', 'c', 'cpp', 'swift', 'html'
|
|
904
904
|
];
|
|
905
905
|
return analyzableLanguages.includes(language);
|
|
906
906
|
}
|
package/dist/database.js
CHANGED
|
@@ -724,8 +724,16 @@ export class DatabaseManager {
|
|
|
724
724
|
let success = true;
|
|
725
725
|
let errorMsg;
|
|
726
726
|
let rowsAffected;
|
|
727
|
+
// CRITICAL FIX: Use dedicated client with ensureSearchPath to prevent
|
|
728
|
+
// schema cross-contamination. pool.query() can race with the fire-and-forget
|
|
729
|
+
// search_path set in pool.on('connect'), causing writes to wrong schema.
|
|
730
|
+
const client = await this.pool.connect();
|
|
727
731
|
try {
|
|
728
|
-
|
|
732
|
+
if (this.currentSchema) {
|
|
733
|
+
const safeSchema = '"' + this.currentSchema.replace(/"/g, '""') + '"';
|
|
734
|
+
await client.query('SET search_path TO ' + safeSchema + ', public');
|
|
735
|
+
}
|
|
736
|
+
const result = await client.query(text, params);
|
|
729
737
|
rowsAffected = result.rowCount ?? undefined;
|
|
730
738
|
const duration = Date.now() - start;
|
|
731
739
|
// Emit db:query:complete event via LWJEB
|
|
@@ -747,6 +755,9 @@ export class DatabaseManager {
|
|
|
747
755
|
coordinator?.emitDBQueryComplete(queryId, queryType, duration, false, undefined, errorMsg);
|
|
748
756
|
throw error;
|
|
749
757
|
}
|
|
758
|
+
finally {
|
|
759
|
+
client.release();
|
|
760
|
+
}
|
|
750
761
|
}
|
|
751
762
|
/**
|
|
752
763
|
* Execute query with GUARANTEED schema isolation.
|
package/dist/mcp/toolRegistry.js
CHANGED
|
@@ -54,8 +54,9 @@ import { SmartSearch } from '../tools/goofy/smartSearch.js';
|
|
|
54
54
|
// Import memory drilldown tools - gallery view + full drill-down
|
|
55
55
|
import { FindMemoryGallery } from '../tools/goofy/findMemoryGallery.js';
|
|
56
56
|
import { GetMemoryFull } from '../tools/goofy/getMemoryFull.js';
|
|
57
|
-
// Import project memory import
|
|
57
|
+
// Import project memory import/export tools - carry context across projects
|
|
58
58
|
import { ImportProjectMemories } from '../tools/goofy/importProjectMemories.js';
|
|
59
|
+
import { ExportProjectMemories } from '../tools/goofy/exportProjectMemories.js';
|
|
59
60
|
// Import MCP-based team communication tools (NEW - replaces HTTP team member comms)
|
|
60
61
|
import { createTeamCommTools } from './tools/teamComms.js';
|
|
61
62
|
// Import embedding server control tools (Phase 4 - user start/stop/status)
|
|
@@ -502,8 +503,9 @@ export function createToolRegistry(db, embeddingProvider) {
|
|
|
502
503
|
// Camera roll drilldown tools - zoom in/out on memories and code
|
|
503
504
|
registry.register(new DrillDown(db));
|
|
504
505
|
registry.register(new GetMemoryByDrilldownID(db));
|
|
505
|
-
// Project memory import
|
|
506
|
+
// Project memory import/export tools - carry context across projects
|
|
506
507
|
registry.register(new ImportProjectMemories(db, cachingProvider));
|
|
508
|
+
registry.register(new ExportProjectMemories(db));
|
|
507
509
|
// Team communication tools - multi-team member coordination
|
|
508
510
|
const teamCommTools = createTeamCommTools();
|
|
509
511
|
for (const tool of teamCommTools) {
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exportProjectMemories - export memories from current project to JSON
|
|
3
|
+
*
|
|
4
|
+
* dumps memories from the current project schema so you can
|
|
5
|
+
* back them up, share them, or import them elsewhere
|
|
6
|
+
*/
|
|
7
|
+
import { logger } from '../../utils/logger.js';
|
|
8
|
+
import { getProjectPathForInsert } from '../../services/ProjectContext.js';
|
|
9
|
+
|
|
10
|
+
export class ExportProjectMemories {
|
|
11
|
+
db;
|
|
12
|
+
name = 'export_project_memories';
|
|
13
|
+
description = 'Export memories from the current project to JSON. Use for backups, sharing, or transferring memories between machines. Returns JSON array of memories.';
|
|
14
|
+
inputSchema = {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
query: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Optional semantic search query to filter which memories to export. If omitted, exports all (up to limit).'
|
|
20
|
+
},
|
|
21
|
+
tags: {
|
|
22
|
+
type: 'array',
|
|
23
|
+
items: { type: 'string' },
|
|
24
|
+
description: 'Optional tag filter - only export memories with these tags'
|
|
25
|
+
},
|
|
26
|
+
memoryTypes: {
|
|
27
|
+
type: 'array',
|
|
28
|
+
items: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
enum: ['episodic', 'semantic', 'procedural', 'working', 'consolidated']
|
|
31
|
+
},
|
|
32
|
+
description: 'Optional memory type filter'
|
|
33
|
+
},
|
|
34
|
+
importance: {
|
|
35
|
+
type: 'array',
|
|
36
|
+
items: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
enum: ['critical', 'high', 'medium', 'low', 'trivial']
|
|
39
|
+
},
|
|
40
|
+
description: 'Optional importance filter'
|
|
41
|
+
},
|
|
42
|
+
limit: {
|
|
43
|
+
type: 'number',
|
|
44
|
+
default: 500,
|
|
45
|
+
minimum: 1,
|
|
46
|
+
maximum: 50000,
|
|
47
|
+
description: 'Max number of memories to export (default: 500)'
|
|
48
|
+
},
|
|
49
|
+
outputPath: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description: 'Optional file path to write JSON output. If omitted, returns in response.'
|
|
52
|
+
},
|
|
53
|
+
includeEmbeddings: {
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
default: false,
|
|
56
|
+
description: 'Include embedding vectors in export (large, usually not needed)'
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
required: []
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
constructor(db) {
|
|
63
|
+
this.db = db;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async execute(params) {
|
|
67
|
+
const { query, tags, memoryTypes, importance, outputPath, includeEmbeddings = false } = params;
|
|
68
|
+
const limit = Math.min(params.limit || 500, 50000);
|
|
69
|
+
const startTime = Date.now();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const currentSchema = this.db.getProjectSchemaName();
|
|
73
|
+
const currentProjectPath = getProjectPathForInsert();
|
|
74
|
+
|
|
75
|
+
logger.info({
|
|
76
|
+
currentSchema,
|
|
77
|
+
currentProjectPath,
|
|
78
|
+
limit,
|
|
79
|
+
query: query?.slice(0, 50),
|
|
80
|
+
tags,
|
|
81
|
+
outputPath
|
|
82
|
+
}, 'Starting memory export');
|
|
83
|
+
|
|
84
|
+
// Build query
|
|
85
|
+
const conditions = [];
|
|
86
|
+
const queryParams = [];
|
|
87
|
+
let paramIndex = 1;
|
|
88
|
+
|
|
89
|
+
if (tags && tags.length > 0) {
|
|
90
|
+
conditions.push(`tags && $${paramIndex}::text[]`);
|
|
91
|
+
queryParams.push(tags);
|
|
92
|
+
paramIndex++;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (memoryTypes && memoryTypes.length > 0) {
|
|
96
|
+
conditions.push(`memory_type = ANY($${paramIndex}::text[])`);
|
|
97
|
+
queryParams.push(memoryTypes);
|
|
98
|
+
paramIndex++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (importance && importance.length > 0) {
|
|
102
|
+
conditions.push(`importance = ANY($${paramIndex}::text[])`);
|
|
103
|
+
queryParams.push(importance);
|
|
104
|
+
paramIndex++;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const embedSelect = includeEmbeddings ? ', embedding' : '';
|
|
108
|
+
let selectQuery;
|
|
109
|
+
|
|
110
|
+
if (query) {
|
|
111
|
+
// Semantic search - need embedding
|
|
112
|
+
let embedding;
|
|
113
|
+
try {
|
|
114
|
+
const embProvider = this.db._embeddingProvider || null;
|
|
115
|
+
if (embProvider) {
|
|
116
|
+
embedding = await embProvider.generateEmbedding(query);
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
logger.warn({ error: e?.message }, 'Could not generate embedding for query filter');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (embedding) {
|
|
123
|
+
conditions.push(`embedding IS NOT NULL`);
|
|
124
|
+
const whereClause = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
|
|
125
|
+
selectQuery = `
|
|
126
|
+
SELECT id, content, memory_type, importance, tags, metadata,
|
|
127
|
+
project_path, created_at, updated_at, expires_at,
|
|
128
|
+
1 - (embedding <=> $${paramIndex}::vector) as similarity
|
|
129
|
+
${embedSelect}
|
|
130
|
+
FROM memories
|
|
131
|
+
${whereClause}
|
|
132
|
+
ORDER BY embedding <=> $${paramIndex}::vector
|
|
133
|
+
LIMIT $${paramIndex + 1}
|
|
134
|
+
`;
|
|
135
|
+
queryParams.push(`[${embedding.join(',')}]`);
|
|
136
|
+
queryParams.push(limit);
|
|
137
|
+
} else {
|
|
138
|
+
// Fallback to text search
|
|
139
|
+
conditions.push(`content ILIKE $${paramIndex}`);
|
|
140
|
+
queryParams.push(`%${query}%`);
|
|
141
|
+
paramIndex++;
|
|
142
|
+
const whereClause = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
|
|
143
|
+
selectQuery = `
|
|
144
|
+
SELECT id, content, memory_type, importance, tags, metadata,
|
|
145
|
+
project_path, created_at, updated_at, expires_at
|
|
146
|
+
${embedSelect}
|
|
147
|
+
FROM memories
|
|
148
|
+
${whereClause}
|
|
149
|
+
ORDER BY created_at DESC
|
|
150
|
+
LIMIT $${paramIndex}
|
|
151
|
+
`;
|
|
152
|
+
queryParams.push(limit);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
const whereClause = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
|
|
156
|
+
selectQuery = `
|
|
157
|
+
SELECT id, content, memory_type, importance, tags, metadata,
|
|
158
|
+
project_path, created_at, updated_at, expires_at
|
|
159
|
+
${embedSelect}
|
|
160
|
+
FROM memories
|
|
161
|
+
${whereClause}
|
|
162
|
+
ORDER BY created_at DESC
|
|
163
|
+
LIMIT $${paramIndex}
|
|
164
|
+
`;
|
|
165
|
+
queryParams.push(limit);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const result = await this.db.query(selectQuery, queryParams);
|
|
169
|
+
const memories = result.rows.map(row => ({
|
|
170
|
+
id: row.id,
|
|
171
|
+
content: row.content,
|
|
172
|
+
memory_type: row.memory_type,
|
|
173
|
+
importance: row.importance,
|
|
174
|
+
tags: row.tags,
|
|
175
|
+
metadata: row.metadata,
|
|
176
|
+
project_path: row.project_path,
|
|
177
|
+
created_at: row.created_at,
|
|
178
|
+
updated_at: row.updated_at,
|
|
179
|
+
expires_at: row.expires_at,
|
|
180
|
+
...(row.similarity !== undefined ? { similarity: Math.round(row.similarity * 1000) / 1000 } : {}),
|
|
181
|
+
...(includeEmbeddings && row.embedding ? { embedding: row.embedding } : {})
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
const duration = Date.now() - startTime;
|
|
185
|
+
|
|
186
|
+
// Write to file if outputPath specified
|
|
187
|
+
if (outputPath) {
|
|
188
|
+
const fs = await import('fs');
|
|
189
|
+
const exportData = {
|
|
190
|
+
exportedAt: new Date().toISOString(),
|
|
191
|
+
sourceProject: currentProjectPath,
|
|
192
|
+
sourceSchema: currentSchema,
|
|
193
|
+
totalExported: memories.length,
|
|
194
|
+
filters: { query, tags, memoryTypes, importance, limit },
|
|
195
|
+
memories
|
|
196
|
+
};
|
|
197
|
+
fs.writeFileSync(outputPath, JSON.stringify(exportData, null, 2));
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
content: [{
|
|
201
|
+
type: 'text',
|
|
202
|
+
text: JSON.stringify({
|
|
203
|
+
success: true,
|
|
204
|
+
exported: memories.length,
|
|
205
|
+
outputPath,
|
|
206
|
+
sourceSchema: currentSchema,
|
|
207
|
+
duration: `${duration}ms`,
|
|
208
|
+
fileSizeKB: Math.round(fs.statSync(outputPath).size / 1024)
|
|
209
|
+
}, null, 2)
|
|
210
|
+
}]
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Return inline
|
|
215
|
+
return {
|
|
216
|
+
content: [{
|
|
217
|
+
type: 'text',
|
|
218
|
+
text: JSON.stringify({
|
|
219
|
+
success: true,
|
|
220
|
+
exported: memories.length,
|
|
221
|
+
sourceProject: currentProjectPath,
|
|
222
|
+
sourceSchema: currentSchema,
|
|
223
|
+
duration: `${duration}ms`,
|
|
224
|
+
memories
|
|
225
|
+
}, null, 2)
|
|
226
|
+
}]
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
} catch (err) {
|
|
230
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
231
|
+
logger.error({ error: errMsg }, 'Memory export failed');
|
|
232
|
+
return {
|
|
233
|
+
content: [{
|
|
234
|
+
type: 'text',
|
|
235
|
+
text: JSON.stringify({
|
|
236
|
+
error: 'Memory export failed',
|
|
237
|
+
details: errMsg
|
|
238
|
+
}, null, 2)
|
|
239
|
+
}]
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -821,7 +821,7 @@ export class FindWhatISaid {
|
|
|
821
821
|
});
|
|
822
822
|
}
|
|
823
823
|
catch (embeddingError) {
|
|
824
|
-
|
|
824
|
+
// embeddingTimeoutId is scoped inside withEmbeddingRetry â already cleared there
|
|
825
825
|
const embeddingDuration = Date.now() - embeddingStartTime;
|
|
826
826
|
const err = embeddingError;
|
|
827
827
|
// ============================================================================
|
|
@@ -104,12 +104,12 @@ export class ImportProjectMemories {
|
|
|
104
104
|
return {
|
|
105
105
|
content: [{
|
|
106
106
|
type: 'text',
|
|
107
|
-
text:
|
|
107
|
+
text: JSON.stringify({
|
|
108
108
|
error: `Source schema '${sourceSchema}' not found`,
|
|
109
109
|
sourceProject,
|
|
110
110
|
availableSchemas: schemaList || 'none',
|
|
111
111
|
hint: 'Make sure the source project path is correct and has been used with SpecMem before'
|
|
112
|
-
})
|
|
112
|
+
}, null, 2)
|
|
113
113
|
}]
|
|
114
114
|
};
|
|
115
115
|
}
|
|
@@ -176,12 +176,12 @@ export class ImportProjectMemories {
|
|
|
176
176
|
return {
|
|
177
177
|
content: [{
|
|
178
178
|
type: 'text',
|
|
179
|
-
text:
|
|
179
|
+
text: JSON.stringify({
|
|
180
180
|
result: 'No memories found matching criteria in source project',
|
|
181
181
|
sourceProject,
|
|
182
182
|
sourceSchema,
|
|
183
183
|
filters: { tags, memoryTypes, importance, query: query?.slice(0, 50) }
|
|
184
|
-
})
|
|
184
|
+
}, null, 2)
|
|
185
185
|
}]
|
|
186
186
|
};
|
|
187
187
|
}
|
|
@@ -200,7 +200,7 @@ export class ImportProjectMemories {
|
|
|
200
200
|
return {
|
|
201
201
|
content: [{
|
|
202
202
|
type: 'text',
|
|
203
|
-
text:
|
|
203
|
+
text: JSON.stringify({
|
|
204
204
|
dryRun: true,
|
|
205
205
|
wouldImport: sourceMemories.rows.length,
|
|
206
206
|
sourceProject,
|
|
@@ -210,7 +210,7 @@ export class ImportProjectMemories {
|
|
|
210
210
|
previewNote: sourceMemories.rows.length > 10
|
|
211
211
|
? `Showing 10 of ${sourceMemories.rows.length} memories`
|
|
212
212
|
: undefined
|
|
213
|
-
})
|
|
213
|
+
}, null, 2)
|
|
214
214
|
}]
|
|
215
215
|
};
|
|
216
216
|
}
|
|
@@ -315,7 +315,7 @@ export class ImportProjectMemories {
|
|
|
315
315
|
return {
|
|
316
316
|
content: [{
|
|
317
317
|
type: 'text',
|
|
318
|
-
text:
|
|
318
|
+
text: JSON.stringify(result, null, 2)
|
|
319
319
|
}]
|
|
320
320
|
};
|
|
321
321
|
|
|
@@ -325,11 +325,11 @@ export class ImportProjectMemories {
|
|
|
325
325
|
return {
|
|
326
326
|
content: [{
|
|
327
327
|
type: 'text',
|
|
328
|
-
text:
|
|
328
|
+
text: JSON.stringify({
|
|
329
329
|
error: 'Memory import failed',
|
|
330
330
|
details: errMsg,
|
|
331
331
|
sourceProject
|
|
332
|
-
})
|
|
332
|
+
}, null, 2)
|
|
333
333
|
}]
|
|
334
334
|
};
|
|
335
335
|
}
|
|
@@ -293,32 +293,48 @@ def _detect_best_onnx_file():
|
|
|
293
293
|
"""
|
|
294
294
|
Detect CPU features and return the best ONNX model file name.
|
|
295
295
|
Priority: avx512_vnni > avx512 > avx2 > default
|
|
296
|
+
Falls back to whatever .onnx file exists if the optimal one isn't found.
|
|
296
297
|
"""
|
|
298
|
+
# Ordered by preference (best first)
|
|
299
|
+
candidates = []
|
|
300
|
+
|
|
297
301
|
try:
|
|
298
302
|
with open('/proc/cpuinfo', 'r') as f:
|
|
299
303
|
cpuinfo = f.read().lower()
|
|
300
304
|
|
|
301
|
-
# Check for AVX512 VNNI (best for INT8)
|
|
302
305
|
if 'avx512_vnni' in cpuinfo or 'avx512vnni' in cpuinfo:
|
|
303
|
-
|
|
304
|
-
return "onnx/model_qint8_avx512_vnni.onnx"
|
|
305
|
-
|
|
306
|
-
# Check for AVX512 (good INT8 support)
|
|
306
|
+
candidates.append(("onnx/model_qint8_avx512_vnni.onnx", "AVX512-VNNI"))
|
|
307
307
|
if 'avx512f' in cpuinfo or 'avx512' in cpuinfo:
|
|
308
|
-
|
|
309
|
-
return "onnx/model_qint8_avx512.onnx"
|
|
310
|
-
|
|
311
|
-
# Check for AVX2 (common, decent performance)
|
|
308
|
+
candidates.append(("onnx/model_qint8_avx512.onnx", "AVX512"))
|
|
312
309
|
if 'avx2' in cpuinfo:
|
|
313
|
-
|
|
314
|
-
return "onnx/model_quint8_avx2.onnx"
|
|
315
|
-
|
|
316
|
-
# Fallback to unoptimized
|
|
317
|
-
print("âšī¸ Using default ONNX model (no AVX optimization)", file=sys.stderr)
|
|
318
|
-
return "onnx/model.onnx"
|
|
310
|
+
candidates.append(("onnx/model_quint8_avx2.onnx", "AVX2"))
|
|
319
311
|
except Exception as e:
|
|
320
312
|
print(f"â ī¸ Could not detect CPU features: {e}", file=sys.stderr)
|
|
321
|
-
|
|
313
|
+
|
|
314
|
+
# Always add standard fallbacks
|
|
315
|
+
candidates.append(("onnx/model_quantized.onnx", "quantized"))
|
|
316
|
+
candidates.append(("onnx/model.onnx", "default"))
|
|
317
|
+
|
|
318
|
+
# Check which files actually exist in the bundled model dir
|
|
319
|
+
bundled_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'models', 'all-MiniLM-L6-v2')
|
|
320
|
+
for onnx_file, label in candidates:
|
|
321
|
+
full_path = os.path.join(bundled_dir, onnx_file)
|
|
322
|
+
if os.path.isfile(full_path):
|
|
323
|
+
print(f"đ Using {label} ONNX model: {onnx_file}", file=sys.stderr)
|
|
324
|
+
return onnx_file
|
|
325
|
+
|
|
326
|
+
# Last resort: find ANY .onnx file in the bundled dir
|
|
327
|
+
onnx_dir = os.path.join(bundled_dir, 'onnx')
|
|
328
|
+
if os.path.isdir(onnx_dir):
|
|
329
|
+
for f in os.listdir(onnx_dir):
|
|
330
|
+
if f.endswith('.onnx'):
|
|
331
|
+
result = f"onnx/{f}"
|
|
332
|
+
print(f"đ Auto-detected ONNX model: {result}", file=sys.stderr)
|
|
333
|
+
return result
|
|
334
|
+
|
|
335
|
+
# Nothing found - return default and let SentenceTransformer handle it
|
|
336
|
+
print("âšī¸ No bundled ONNX model found - using default", file=sys.stderr)
|
|
337
|
+
return "onnx/model.onnx"
|
|
322
338
|
|
|
323
339
|
_BEST_ONNX_FILE = _detect_best_onnx_file()
|
|
324
340
|
|
|
@@ -57,6 +57,13 @@ const getMachineSocketPath = () => {
|
|
|
57
57
|
const SOCKET_PATH = process.env.SOCKET_PATH || getMachineSocketPath();
|
|
58
58
|
const MODEL_NAME = 'Xenova/all-MiniLM-L6-v2';
|
|
59
59
|
|
|
60
|
+
// Bundled model: shipped with npm package, used as fallback when HF cache unavailable
|
|
61
|
+
import { fileURLToPath } from 'url';
|
|
62
|
+
import { dirname } from 'path';
|
|
63
|
+
const __filename_esm = fileURLToPath(import.meta.url);
|
|
64
|
+
const __dirname_esm = dirname(__filename_esm);
|
|
65
|
+
const BUNDLED_MODEL_DIR = join(__dirname_esm, 'models', 'all-MiniLM-L6-v2');
|
|
66
|
+
|
|
60
67
|
// Dynamic dimensions - detected from model and database
|
|
61
68
|
let NATIVE_DIM = null;
|
|
62
69
|
let TARGET_DIM = null;
|
|
@@ -78,13 +85,39 @@ async function loadModel() {
|
|
|
78
85
|
try {
|
|
79
86
|
console.log('[Sandbox] Loading model from local cache...');
|
|
80
87
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
// Try HF cache first, fall back to bundled model
|
|
89
|
+
let modelSource = MODEL_NAME;
|
|
90
|
+
try {
|
|
91
|
+
extractor = await pipeline('feature-extraction', MODEL_NAME, {
|
|
92
|
+
local_files_only: true,
|
|
93
|
+
device: 'cpu'
|
|
94
|
+
});
|
|
95
|
+
} catch (hfErr) {
|
|
96
|
+
// HF cache miss â try bundled model shipped with npm package
|
|
97
|
+
if (existsSync(BUNDLED_MODEL_DIR)) {
|
|
98
|
+
console.log(`[Sandbox] HF cache miss, loading bundled model: ${BUNDLED_MODEL_DIR}`);
|
|
99
|
+
// Ensure model.onnx exists (bundled may only have model_quint8_avx2.onnx)
|
|
100
|
+
const onnxDir = join(BUNDLED_MODEL_DIR, 'onnx');
|
|
101
|
+
const modelOnnx = join(onnxDir, 'model.onnx');
|
|
102
|
+
if (!existsSync(modelOnnx) && existsSync(onnxDir)) {
|
|
103
|
+
// Find any .onnx file and symlink as model.onnx
|
|
104
|
+
const { readdirSync, symlinkSync } = await import('fs');
|
|
105
|
+
const onnxFiles = readdirSync(onnxDir).filter(f => f.endsWith('.onnx'));
|
|
106
|
+
if (onnxFiles.length > 0) {
|
|
107
|
+
try { symlinkSync(onnxFiles[0], modelOnnx); } catch {}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
extractor = await pipeline('feature-extraction', BUNDLED_MODEL_DIR, {
|
|
111
|
+
local_files_only: true,
|
|
112
|
+
device: 'cpu'
|
|
113
|
+
});
|
|
114
|
+
modelSource = BUNDLED_MODEL_DIR;
|
|
115
|
+
} else {
|
|
116
|
+
throw hfErr;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Skip the duplicate pipeline call below â extractor is already loaded
|
|
88
121
|
|
|
89
122
|
modelReady = true;
|
|
90
123
|
|