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.
@@ -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
- const result = await this.pool.query(text, params);
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.
@@ -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 tool - carry context across projects
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 tool - import memories from other projects
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
- clearTimeout(embeddingTimeoutId); // Prevent dangling timer on error path
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: formatHumanReadable({
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: formatHumanReadable({
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: formatHumanReadable({
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: formatHumanReadable(result)
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: formatHumanReadable({
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
- print("🚀 CPU supports AVX512-VNNI - using optimized INT8 model", file=sys.stderr)
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
- print("🚀 CPU supports AVX512 - using INT8 quantized model", file=sys.stderr)
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
- print("🚀 CPU supports AVX2 - using UINT8 quantized model", file=sys.stderr)
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
- return "onnx/model.onnx"
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
- // Force local-only mode - will fail if model not pre-downloaded
82
- extractor = await pipeline('feature-extraction', MODEL_NAME, {
83
- // Use local cache only - no downloads allowed
84
- local_files_only: true,
85
- // Use CPU only (safer, no GPU driver access)
86
- device: 'cpu'
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