specmem-hardwicksoftware 3.6.1 → 3.7.0
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/mcp/toolRegistry.js
CHANGED
|
@@ -54,6 +54,8 @@ 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
|
|
58
|
+
import { ImportProjectMemories } from '../tools/goofy/importProjectMemories.js';
|
|
57
59
|
// Import MCP-based team communication tools (NEW - replaces HTTP team member comms)
|
|
58
60
|
import { createTeamCommTools } from './tools/teamComms.js';
|
|
59
61
|
// Import embedding server control tools (Phase 4 - user start/stop/status)
|
|
@@ -500,6 +502,8 @@ export function createToolRegistry(db, embeddingProvider) {
|
|
|
500
502
|
// Camera roll drilldown tools - zoom in/out on memories and code
|
|
501
503
|
registry.register(new DrillDown(db));
|
|
502
504
|
registry.register(new GetMemoryByDrilldownID(db));
|
|
505
|
+
// Project memory import tool - import memories from other projects
|
|
506
|
+
registry.register(new ImportProjectMemories(db, cachingProvider));
|
|
503
507
|
// Team communication tools - multi-team member coordination
|
|
504
508
|
const teamCommTools = createTeamCommTools();
|
|
505
509
|
for (const tool of teamCommTools) {
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* importProjectMemories - import memories from another project schema
|
|
3
|
+
*
|
|
4
|
+
* copies memories from project A into the current project
|
|
5
|
+
* so you can carry context across projects fr fr
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { logger } from '../../utils/logger.js';
|
|
9
|
+
import { getProjectSchema } from '../../db/projectNamespacing.js';
|
|
10
|
+
import { getProjectPathForInsert } from '../../services/ProjectContext.js';
|
|
11
|
+
import { formatHumanReadable } from '../../utils/humanReadableOutput.js';
|
|
12
|
+
|
|
13
|
+
export class ImportProjectMemories {
|
|
14
|
+
db;
|
|
15
|
+
embeddingProvider;
|
|
16
|
+
name = 'import_project_memories';
|
|
17
|
+
description = 'Import memories from another project into the current project. Use this to carry context across projects - e.g. import /specmem memories into /AEGIS_AI.';
|
|
18
|
+
inputSchema = {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
sourceProject: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Absolute path of the source project to import from (e.g. "/specmem", "/AEGIS_AI")'
|
|
24
|
+
},
|
|
25
|
+
query: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Optional semantic search query to filter which memories to import. If omitted, imports all (up to limit).'
|
|
28
|
+
},
|
|
29
|
+
tags: {
|
|
30
|
+
type: 'array',
|
|
31
|
+
items: { type: 'string' },
|
|
32
|
+
description: 'Optional tag filter - only import memories with these tags'
|
|
33
|
+
},
|
|
34
|
+
memoryTypes: {
|
|
35
|
+
type: 'array',
|
|
36
|
+
items: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
enum: ['episodic', 'semantic', 'procedural', 'working', 'consolidated']
|
|
39
|
+
},
|
|
40
|
+
description: 'Optional memory type filter'
|
|
41
|
+
},
|
|
42
|
+
importance: {
|
|
43
|
+
type: 'array',
|
|
44
|
+
items: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
enum: ['critical', 'high', 'medium', 'low', 'trivial']
|
|
47
|
+
},
|
|
48
|
+
description: 'Optional importance filter'
|
|
49
|
+
},
|
|
50
|
+
limit: {
|
|
51
|
+
type: 'number',
|
|
52
|
+
default: 100,
|
|
53
|
+
minimum: 1,
|
|
54
|
+
maximum: 1000,
|
|
55
|
+
description: 'Max number of memories to import (default: 100)'
|
|
56
|
+
},
|
|
57
|
+
dryRun: {
|
|
58
|
+
type: 'boolean',
|
|
59
|
+
default: false,
|
|
60
|
+
description: 'Preview what would be imported without actually importing'
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
required: ['sourceProject']
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
constructor(db, embeddingProvider) {
|
|
67
|
+
this.db = db;
|
|
68
|
+
this.embeddingProvider = embeddingProvider;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async execute(params) {
|
|
72
|
+
const { sourceProject, query, tags, memoryTypes, importance, dryRun = false } = params;
|
|
73
|
+
const limit = Math.min(params.limit || 100, 1000);
|
|
74
|
+
const startTime = Date.now();
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Get schema names
|
|
78
|
+
const sourceSchema = getProjectSchema(sourceProject);
|
|
79
|
+
const currentSchema = this.db.getProjectSchemaName();
|
|
80
|
+
const currentProjectPath = getProjectPathForInsert();
|
|
81
|
+
|
|
82
|
+
logger.info({
|
|
83
|
+
sourceProject,
|
|
84
|
+
sourceSchema,
|
|
85
|
+
currentSchema,
|
|
86
|
+
limit,
|
|
87
|
+
dryRun,
|
|
88
|
+
query: query?.slice(0, 50),
|
|
89
|
+
tags
|
|
90
|
+
}, 'Starting memory import');
|
|
91
|
+
|
|
92
|
+
// Verify source schema exists
|
|
93
|
+
const schemaCheck = await this.db.query(
|
|
94
|
+
`SELECT schema_name FROM information_schema.schemata WHERE schema_name = $1`,
|
|
95
|
+
[sourceSchema]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (schemaCheck.rows.length === 0) {
|
|
99
|
+
// List available schemas for helpful error
|
|
100
|
+
const available = await this.db.query(
|
|
101
|
+
`SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'specmem_%' ORDER BY schema_name`
|
|
102
|
+
);
|
|
103
|
+
const schemaList = available.rows.map(r => r.schema_name).join(', ');
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: formatHumanReadable({
|
|
108
|
+
error: `Source schema '${sourceSchema}' not found`,
|
|
109
|
+
sourceProject,
|
|
110
|
+
availableSchemas: schemaList || 'none',
|
|
111
|
+
hint: 'Make sure the source project path is correct and has been used with SpecMem before'
|
|
112
|
+
})
|
|
113
|
+
}]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Build query to fetch memories from source schema
|
|
118
|
+
const conditions = [];
|
|
119
|
+
const queryParams = [];
|
|
120
|
+
let paramIndex = 1;
|
|
121
|
+
|
|
122
|
+
// Filter by tags
|
|
123
|
+
if (tags && tags.length > 0) {
|
|
124
|
+
conditions.push(`tags && $${paramIndex}::text[]`);
|
|
125
|
+
queryParams.push(tags);
|
|
126
|
+
paramIndex++;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Filter by memory types
|
|
130
|
+
if (memoryTypes && memoryTypes.length > 0) {
|
|
131
|
+
conditions.push(`memory_type = ANY($${paramIndex}::text[])`);
|
|
132
|
+
queryParams.push(memoryTypes);
|
|
133
|
+
paramIndex++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Filter by importance
|
|
137
|
+
if (importance && importance.length > 0) {
|
|
138
|
+
conditions.push(`importance = ANY($${paramIndex}::text[])`);
|
|
139
|
+
queryParams.push(importance);
|
|
140
|
+
paramIndex++;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Build the SELECT query against source schema
|
|
144
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
145
|
+
|
|
146
|
+
let selectQuery;
|
|
147
|
+
if (query && this.embeddingProvider) {
|
|
148
|
+
// Semantic search - generate embedding for query, order by similarity
|
|
149
|
+
const embedding = await this.embeddingProvider.generateEmbedding(query);
|
|
150
|
+
const embeddingStr = `[${embedding.join(',')}]`;
|
|
151
|
+
queryParams.push(embeddingStr);
|
|
152
|
+
selectQuery = `
|
|
153
|
+
SELECT id, content, memory_type, importance, tags, metadata,
|
|
154
|
+
embedding, role, created_at, updated_at, expires_at,
|
|
155
|
+
1 - (embedding <=> $${paramIndex}::vector) as similarity
|
|
156
|
+
FROM "${sourceSchema}".memories
|
|
157
|
+
${whereClause}
|
|
158
|
+
ORDER BY embedding <=> $${paramIndex}::vector
|
|
159
|
+
LIMIT ${limit}
|
|
160
|
+
`;
|
|
161
|
+
paramIndex++;
|
|
162
|
+
} else {
|
|
163
|
+
selectQuery = `
|
|
164
|
+
SELECT id, content, memory_type, importance, tags, metadata,
|
|
165
|
+
embedding, role, created_at, updated_at, expires_at
|
|
166
|
+
FROM "${sourceSchema}".memories
|
|
167
|
+
${whereClause}
|
|
168
|
+
ORDER BY created_at DESC
|
|
169
|
+
LIMIT ${limit}
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const sourceMemories = await this.db.query(selectQuery, queryParams);
|
|
174
|
+
|
|
175
|
+
if (sourceMemories.rows.length === 0) {
|
|
176
|
+
return {
|
|
177
|
+
content: [{
|
|
178
|
+
type: 'text',
|
|
179
|
+
text: formatHumanReadable({
|
|
180
|
+
result: 'No memories found matching criteria in source project',
|
|
181
|
+
sourceProject,
|
|
182
|
+
sourceSchema,
|
|
183
|
+
filters: { tags, memoryTypes, importance, query: query?.slice(0, 50) }
|
|
184
|
+
})
|
|
185
|
+
}]
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Dry run - just show what would be imported
|
|
190
|
+
if (dryRun) {
|
|
191
|
+
const preview = sourceMemories.rows.slice(0, 10).map(m => ({
|
|
192
|
+
content: m.content?.slice(0, 100) + (m.content?.length > 100 ? '...' : ''),
|
|
193
|
+
type: m.memory_type,
|
|
194
|
+
importance: m.importance,
|
|
195
|
+
tags: m.tags,
|
|
196
|
+
similarity: m.similarity ? Math.round(m.similarity * 100) + '%' : undefined,
|
|
197
|
+
created: m.created_at
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
content: [{
|
|
202
|
+
type: 'text',
|
|
203
|
+
text: formatHumanReadable({
|
|
204
|
+
dryRun: true,
|
|
205
|
+
wouldImport: sourceMemories.rows.length,
|
|
206
|
+
sourceProject,
|
|
207
|
+
sourceSchema,
|
|
208
|
+
targetSchema: currentSchema,
|
|
209
|
+
preview,
|
|
210
|
+
previewNote: sourceMemories.rows.length > 10
|
|
211
|
+
? `Showing 10 of ${sourceMemories.rows.length} memories`
|
|
212
|
+
: undefined
|
|
213
|
+
})
|
|
214
|
+
}]
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Actually import - insert into current schema with new UUIDs
|
|
219
|
+
let imported = 0;
|
|
220
|
+
let skipped = 0;
|
|
221
|
+
const errors = [];
|
|
222
|
+
|
|
223
|
+
for (const memory of sourceMemories.rows) {
|
|
224
|
+
try {
|
|
225
|
+
const newId = randomUUID();
|
|
226
|
+
const importTag = `imported_from:${sourceProject}`;
|
|
227
|
+
const newTags = Array.isArray(memory.tags)
|
|
228
|
+
? [...new Set([...memory.tags, importTag])]
|
|
229
|
+
: [importTag];
|
|
230
|
+
|
|
231
|
+
// Merge metadata with import info
|
|
232
|
+
const newMetadata = {
|
|
233
|
+
...(memory.metadata || {}),
|
|
234
|
+
imported: {
|
|
235
|
+
from: sourceProject,
|
|
236
|
+
originalId: memory.id,
|
|
237
|
+
importedAt: new Date().toISOString()
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Check for duplicate content in target schema
|
|
242
|
+
const dupCheck = await this.db.query(
|
|
243
|
+
`SELECT id FROM memories WHERE content = $1 LIMIT 1`,
|
|
244
|
+
[memory.content]
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (dupCheck.rows.length > 0) {
|
|
248
|
+
skipped++;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Insert into current schema
|
|
253
|
+
const insertQuery = `
|
|
254
|
+
INSERT INTO memories (
|
|
255
|
+
id, content, memory_type, importance, tags, metadata,
|
|
256
|
+
embedding, project_path, role, created_at, updated_at
|
|
257
|
+
) VALUES (
|
|
258
|
+
$1, $2, $3, $4, $5, $6,
|
|
259
|
+
$7, $8, $9, $10, $11
|
|
260
|
+
)
|
|
261
|
+
`;
|
|
262
|
+
|
|
263
|
+
await this.db.query(insertQuery, [
|
|
264
|
+
newId,
|
|
265
|
+
memory.content,
|
|
266
|
+
memory.memory_type || 'semantic',
|
|
267
|
+
memory.importance || 'medium',
|
|
268
|
+
newTags,
|
|
269
|
+
JSON.stringify(newMetadata),
|
|
270
|
+
memory.embedding, // preserve original embedding vector
|
|
271
|
+
currentProjectPath,
|
|
272
|
+
memory.role || 'user',
|
|
273
|
+
memory.created_at || new Date(),
|
|
274
|
+
new Date()
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
imported++;
|
|
278
|
+
} catch (err) {
|
|
279
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
280
|
+
errors.push({ id: memory.id, error: errMsg });
|
|
281
|
+
logger.warn({ memoryId: memory.id, error: errMsg }, 'Failed to import memory');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const duration = Date.now() - startTime;
|
|
286
|
+
|
|
287
|
+
logger.info({
|
|
288
|
+
imported,
|
|
289
|
+
skipped,
|
|
290
|
+
errors: errors.length,
|
|
291
|
+
duration,
|
|
292
|
+
sourceProject,
|
|
293
|
+
sourceSchema,
|
|
294
|
+
currentSchema
|
|
295
|
+
}, 'Memory import completed');
|
|
296
|
+
|
|
297
|
+
const result = {
|
|
298
|
+
imported,
|
|
299
|
+
skipped,
|
|
300
|
+
errors: errors.length,
|
|
301
|
+
total: sourceMemories.rows.length,
|
|
302
|
+
sourceProject,
|
|
303
|
+
sourceSchema,
|
|
304
|
+
targetSchema: currentSchema,
|
|
305
|
+
duration: `${duration}ms`
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (errors.length > 0 && errors.length <= 5) {
|
|
309
|
+
result.errorDetails = errors;
|
|
310
|
+
} else if (errors.length > 5) {
|
|
311
|
+
result.errorDetails = errors.slice(0, 5);
|
|
312
|
+
result.moreErrors = errors.length - 5;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
content: [{
|
|
317
|
+
type: 'text',
|
|
318
|
+
text: formatHumanReadable(result)
|
|
319
|
+
}]
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
} catch (err) {
|
|
323
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
324
|
+
logger.error({ error: errMsg, sourceProject }, 'Memory import failed');
|
|
325
|
+
return {
|
|
326
|
+
content: [{
|
|
327
|
+
type: 'text',
|
|
328
|
+
text: formatHumanReadable({
|
|
329
|
+
error: 'Memory import failed',
|
|
330
|
+
details: errMsg,
|
|
331
|
+
sourceProject
|
|
332
|
+
})
|
|
333
|
+
}]
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specmem-hardwicksoftware",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Persistent memory system for coding sessions - semantic search with pgvector, token compression, team coordination, file watching. Needs root: installs system-wide hooks, manages docker/PostgreSQL, writes global configs, handles screen sessions. justcalljon.pro",
|
|
6
6
|
"main": "dist/index.js",
|