specmem-hardwicksoftware 3.6.1 → 3.7.1
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.
|
@@ -41,27 +41,72 @@ const HOOKS_DIR = path.join(CLAUDE_CONFIG_DIR, 'hooks');
|
|
|
41
41
|
const COMMANDS_DIR = path.join(CLAUDE_CONFIG_DIR, 'commands');
|
|
42
42
|
// stores per-project MCP configs in ~/.claude.json under "projects" key
|
|
43
43
|
const CLAUDE_JSON_PATH = path.join(HOME_DIR, '.claude.json');
|
|
44
|
-
// SpecMem directory detection -
|
|
44
|
+
// SpecMem directory detection - dynamically resolves from how specmem was launched
|
|
45
|
+
// Handles bootstrap.cjs AND bootstrap.js, works with ANY install location
|
|
46
|
+
function hasBootstrap(dir) {
|
|
47
|
+
return fs.existsSync(path.join(dir, 'bootstrap.cjs')) ||
|
|
48
|
+
fs.existsSync(path.join(dir, 'bootstrap.js'));
|
|
49
|
+
}
|
|
45
50
|
function getSpecmemRoot() {
|
|
46
|
-
// Check environment variable
|
|
47
|
-
if (process.env.SPECMEM_ROOT) {
|
|
51
|
+
// 1. Check environment variable (explicit override)
|
|
52
|
+
if (process.env.SPECMEM_ROOT && hasBootstrap(process.env.SPECMEM_ROOT)) {
|
|
48
53
|
return process.env.SPECMEM_ROOT;
|
|
49
54
|
}
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
// 2. Detect from current file location (__dirname is most reliable)
|
|
56
|
+
// dist/init/claudeConfigInjector.js -> go up 2 levels to package root
|
|
57
|
+
const fromThisFile = path.resolve(__dirname, '..', '..');
|
|
58
|
+
if (hasBootstrap(fromThisFile)) {
|
|
59
|
+
return fromThisFile;
|
|
60
|
+
}
|
|
61
|
+
// 3. Detect from process.argv - what script launched us
|
|
62
|
+
// e.g. node /usr/local/lib/.../bootstrap.cjs or /usr/lib/.../bin/specmem-cli.cjs
|
|
63
|
+
for (const arg of process.argv) {
|
|
64
|
+
if (typeof arg === 'string' && arg.includes('specmem')) {
|
|
65
|
+
// Resolve symlinks to get real path
|
|
66
|
+
try {
|
|
67
|
+
const realArg = fs.realpathSync(arg);
|
|
68
|
+
// Walk up from the script to find package root
|
|
69
|
+
let candidate = path.dirname(realArg);
|
|
70
|
+
for (let i = 0; i < 4; i++) {
|
|
71
|
+
if (hasBootstrap(candidate)) return candidate;
|
|
72
|
+
if (fs.existsSync(path.join(candidate, 'package.json'))) {
|
|
73
|
+
try {
|
|
74
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(candidate, 'package.json'), 'utf-8'));
|
|
75
|
+
if (pkg.name === 'specmem-hardwicksoftware') return candidate;
|
|
76
|
+
} catch { /* ignore */ }
|
|
77
|
+
}
|
|
78
|
+
candidate = path.dirname(candidate);
|
|
79
|
+
}
|
|
80
|
+
} catch { /* ignore resolve errors */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// 4. Try resolving the `specmem` command via PATH (execSync imported at top)
|
|
84
|
+
try {
|
|
85
|
+
const whichResult = execSync('which specmem 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
86
|
+
if (whichResult) {
|
|
87
|
+
const realBin = fs.realpathSync(whichResult);
|
|
88
|
+
// specmem binary is at <root>/bin/specmem-cli.cjs -> go up 2 levels
|
|
89
|
+
const candidate = path.resolve(path.dirname(realBin), '..');
|
|
90
|
+
if (hasBootstrap(candidate)) return candidate;
|
|
91
|
+
}
|
|
92
|
+
} catch { /* which not available or specmem not in PATH */ }
|
|
93
|
+
// 5. Fallback to cwd (dev mode - running from source)
|
|
94
|
+
if (hasBootstrap(process.cwd())) {
|
|
95
|
+
return process.cwd();
|
|
96
|
+
}
|
|
97
|
+
// 6. Last resort - return __dirname-based path even without bootstrap
|
|
98
|
+
return fromThisFile;
|
|
99
|
+
}
|
|
100
|
+
// Find the actual bootstrap file (cjs or js)
|
|
101
|
+
function findBootstrapPath(root) {
|
|
102
|
+
for (const name of ['bootstrap.cjs', 'bootstrap.js']) {
|
|
103
|
+
const p = path.join(root, name);
|
|
104
|
+
if (fs.existsSync(p)) return p;
|
|
105
|
+
}
|
|
106
|
+
return path.join(root, 'bootstrap.cjs'); // default
|
|
62
107
|
}
|
|
63
108
|
const SPECMEM_ROOT = getSpecmemRoot();
|
|
64
|
-
const BOOTSTRAP_PATH =
|
|
109
|
+
const BOOTSTRAP_PATH = findBootstrapPath(SPECMEM_ROOT);
|
|
65
110
|
const SOURCE_HOOKS_DIR = path.join(SPECMEM_ROOT, 'claude-hooks');
|
|
66
111
|
const SOURCE_COMMANDS_DIR = path.join(SPECMEM_ROOT, 'commands');
|
|
67
112
|
// ============================================================================
|
|
@@ -155,7 +200,7 @@ export function isSpecmemMcpConfigured(projectPath) {
|
|
|
155
200
|
return false;
|
|
156
201
|
}
|
|
157
202
|
const specmem = config.mcpServers.specmem;
|
|
158
|
-
// Check if it points to a valid bootstrap
|
|
203
|
+
// Check if it points to a valid bootstrap file (cjs or js)
|
|
159
204
|
if (!specmem.args || specmem.args.length < 2) {
|
|
160
205
|
return false;
|
|
161
206
|
}
|
|
@@ -167,8 +212,8 @@ export function isSpecmemMcpConfigured(projectPath) {
|
|
|
167
212
|
// If projectPath specified, check if env has correct project path
|
|
168
213
|
if (projectPath) {
|
|
169
214
|
const configuredPath = specmem.env?.SPECMEM_PROJECT_PATH;
|
|
170
|
-
// ${PWD}
|
|
171
|
-
if (configuredPath && configuredPath !== '${PWD}' && configuredPath !== projectPath) {
|
|
215
|
+
// ${PWD} and ${cwd} are expanded at runtime by Claude Code, so they're valid
|
|
216
|
+
if (configuredPath && configuredPath !== '${PWD}' && configuredPath !== '${cwd}' && configuredPath !== projectPath) {
|
|
172
217
|
return false;
|
|
173
218
|
}
|
|
174
219
|
}
|
|
@@ -179,11 +224,11 @@ export function isSpecmemMcpConfigured(projectPath) {
|
|
|
179
224
|
* Returns true if changes were made
|
|
180
225
|
*/
|
|
181
226
|
function configureMcpServer() {
|
|
182
|
-
// Verify bootstrap
|
|
227
|
+
// Verify bootstrap file exists (cjs or js)
|
|
183
228
|
if (!fs.existsSync(BOOTSTRAP_PATH)) {
|
|
184
229
|
return {
|
|
185
230
|
configured: false,
|
|
186
|
-
error: `bootstrap
|
|
231
|
+
error: `bootstrap not found at ${BOOTSTRAP_PATH}`
|
|
187
232
|
};
|
|
188
233
|
}
|
|
189
234
|
const config = safeReadJson(CONFIG_PATH, {});
|
|
@@ -265,8 +310,9 @@ function fixProjectMcpConfigs() {
|
|
|
265
310
|
// Scan all project entries
|
|
266
311
|
for (const [projectPath, projectConfig] of Object.entries(claudeJson.projects)) {
|
|
267
312
|
const config = projectConfig;
|
|
268
|
-
|
|
269
|
-
|
|
313
|
+
if (!config) continue;
|
|
314
|
+
// Case 1: Project has specmem MCP config but with outdated path
|
|
315
|
+
if (config.mcpServers?.specmem) {
|
|
270
316
|
const specmem = config.mcpServers.specmem;
|
|
271
317
|
const args = specmem.args || [];
|
|
272
318
|
// Check if the args contain an outdated specmem path
|
|
@@ -275,28 +321,49 @@ function fixProjectMcpConfigs() {
|
|
|
275
321
|
if (typeof arg === 'string' &&
|
|
276
322
|
arg.includes('specmem') &&
|
|
277
323
|
arg !== BOOTSTRAP_PATH &&
|
|
278
|
-
(arg.endsWith('index.js') || arg.endsWith('bootstrap.js'))) {
|
|
324
|
+
(arg.endsWith('index.js') || arg.endsWith('bootstrap.js') || arg.endsWith('bootstrap.cjs'))) {
|
|
279
325
|
needsUpdate = true;
|
|
280
326
|
return BOOTSTRAP_PATH;
|
|
281
327
|
}
|
|
282
328
|
return arg;
|
|
283
329
|
});
|
|
284
330
|
if (needsUpdate) {
|
|
285
|
-
// Update the args
|
|
286
331
|
specmem.args = updatedArgs;
|
|
287
|
-
// Ensure SPECMEM_PROJECT_PATH is set to actual project path
|
|
288
|
-
// CRITICAL: ${PWD} doesn't get expanded by Code, use literal path
|
|
289
332
|
if (!specmem.env) {
|
|
290
333
|
specmem.env = {};
|
|
291
334
|
}
|
|
292
335
|
if (!specmem.env.SPECMEM_PROJECT_PATH || specmem.env.SPECMEM_PROJECT_PATH === '${PWD}' || specmem.env.SPECMEM_PROJECT_PATH === '${cwd}') {
|
|
293
|
-
specmem.env.SPECMEM_PROJECT_PATH = projectPath;
|
|
336
|
+
specmem.env.SPECMEM_PROJECT_PATH = projectPath;
|
|
294
337
|
}
|
|
295
338
|
logger.info({ projectPath, oldArgs: args, newArgs: updatedArgs }, '[ConfigInjector] Fixed outdated specmem path in project config');
|
|
296
339
|
fixed++;
|
|
297
340
|
modified = true;
|
|
298
341
|
}
|
|
299
342
|
}
|
|
343
|
+
// Case 2: Project has mcpServers but NO specmem entry (empty {} or missing key)
|
|
344
|
+
// This empty override hides the global config.json MCP server, so we inject it
|
|
345
|
+
else if (config.mcpServers && !config.mcpServers.specmem) {
|
|
346
|
+
// Don't clobber other MCP servers - only add specmem
|
|
347
|
+
config.mcpServers.specmem = {
|
|
348
|
+
command: 'node',
|
|
349
|
+
args: ['--max-old-space-size=250', BOOTSTRAP_PATH],
|
|
350
|
+
env: {
|
|
351
|
+
HOME: HOME_DIR,
|
|
352
|
+
SPECMEM_PROJECT_PATH: '${cwd}',
|
|
353
|
+
SPECMEM_WATCHER_ROOT_PATH: '${cwd}',
|
|
354
|
+
SPECMEM_CODEBASE_PATH: '${cwd}',
|
|
355
|
+
SPECMEM_DB_HOST: process.env.SPECMEM_DB_HOST || 'localhost',
|
|
356
|
+
SPECMEM_DB_PORT: process.env.SPECMEM_DB_PORT || '5432',
|
|
357
|
+
SPECMEM_SESSION_WATCHER_ENABLED: 'true',
|
|
358
|
+
SPECMEM_WATCHER_ENABLED: 'true',
|
|
359
|
+
SPECMEM_DASHBOARD_ENABLED: 'true',
|
|
360
|
+
SPECMEM_DASHBOARD_PORT: process.env.SPECMEM_DASHBOARD_PORT || '8595',
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
logger.info({ projectPath }, '[ConfigInjector] Injected specmem MCP server into project with empty mcpServers');
|
|
364
|
+
fixed++;
|
|
365
|
+
modified = true;
|
|
366
|
+
}
|
|
300
367
|
}
|
|
301
368
|
// Write back if modified
|
|
302
369
|
if (modified) {
|
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.1",
|
|
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",
|