@stackmemoryai/stackmemory 1.2.8 → 1.3.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/README.md +2 -2
- package/dist/src/cli/commands/daemon.js +306 -2
- package/dist/src/cli/commands/desires.js +117 -0
- package/dist/src/cli/commands/setup.js +369 -39
- package/dist/src/cli/commands/symphony.js +267 -0
- package/dist/src/cli/commands/team.js +168 -0
- package/dist/src/cli/index.js +6 -0
- package/dist/src/core/context/frame-handoff-manager.js +7 -4
- package/dist/src/core/database/database-adapter.js +7 -0
- package/dist/src/core/database/migration-manager.js +6 -19
- package/dist/src/core/database/sqlite-adapter.js +18 -0
- package/dist/src/integrations/mcp/server.js +157 -12
- package/dist/src/integrations/mcp/tool-definitions.js +41 -1
- package/dist/src/utils/hook-installer.js +35 -0
- package/package.json +2 -2
- package/scripts/install-claude-hooks-auto.js +35 -0
- package/templates/claude-hooks/daemon-auto-start.js +125 -0
- package/templates/claude-hooks/desire-path-trace.js +118 -0
- package/templates/claude-hooks/team-subagent-stop.js +77 -0
- package/templates/claude-hooks/team-task-complete.js +83 -0
- package/templates/claude-hooks/team-teammate-idle.js +96 -0
|
@@ -4,14 +4,20 @@ const __filename = __fileURLToPath(import.meta.url);
|
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import chalk from "chalk";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
rmSync
|
|
14
|
+
} from "fs";
|
|
8
15
|
import { join } from "path";
|
|
9
16
|
import { homedir } from "os";
|
|
10
17
|
import { execSync } from "child_process";
|
|
11
18
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
12
19
|
const CLAUDE_CONFIG_FILE = join(CLAUDE_DIR, "config.json");
|
|
13
20
|
const MCP_CONFIG_FILE = join(CLAUDE_DIR, "stackmemory-mcp.json");
|
|
14
|
-
const HOOKS_JSON = join(CLAUDE_DIR, "hooks.json");
|
|
15
21
|
function createSetupMCPCommand() {
|
|
16
22
|
return new Command("setup-mcp").description("Auto-configure Claude Code MCP integration").option("--dry-run", "Show what would be configured without making changes").option("--reset", "Reset MCP configuration to defaults").action(async (options) => {
|
|
17
23
|
console.log(chalk.cyan("\nStackMemory MCP Setup\n"));
|
|
@@ -199,56 +205,369 @@ function createDoctorCommand() {
|
|
|
199
205
|
fix: "Run: stackmemory setup-mcp"
|
|
200
206
|
});
|
|
201
207
|
}
|
|
202
|
-
|
|
208
|
+
{
|
|
209
|
+
const settingsFile = join(homedir(), ".claude", "settings.json");
|
|
210
|
+
let hookCount = 0;
|
|
211
|
+
if (existsSync(settingsFile)) {
|
|
212
|
+
try {
|
|
213
|
+
const settings = JSON.parse(readFileSync(settingsFile, "utf8"));
|
|
214
|
+
if (settings.hooks) {
|
|
215
|
+
for (const groups of Object.values(settings.hooks)) {
|
|
216
|
+
for (const group of groups) {
|
|
217
|
+
hookCount += group.hooks.length;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (hookCount > 0) {
|
|
225
|
+
results.push({
|
|
226
|
+
name: "Claude Hooks",
|
|
227
|
+
status: "ok",
|
|
228
|
+
message: `${hookCount} hooks registered in settings.json`
|
|
229
|
+
});
|
|
230
|
+
} else {
|
|
231
|
+
results.push({
|
|
232
|
+
name: "Claude Hooks",
|
|
233
|
+
status: "warn",
|
|
234
|
+
message: "No hooks registered in settings.json",
|
|
235
|
+
fix: "Run: stackmemory hooks install"
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const { MCPToolDefinitions } = await import("../../integrations/mcp/tool-definitions.js");
|
|
241
|
+
const toolDefs = new MCPToolDefinitions();
|
|
242
|
+
const allTools = toolDefs.getAllToolDefinitions();
|
|
243
|
+
const toolNames = allTools.map((t) => t.name);
|
|
244
|
+
const expectedTools = [
|
|
245
|
+
"sm_digest",
|
|
246
|
+
"cord_spawn",
|
|
247
|
+
"team_search",
|
|
248
|
+
"get_context",
|
|
249
|
+
"create_task"
|
|
250
|
+
];
|
|
251
|
+
const missing = expectedTools.filter((t) => !toolNames.includes(t));
|
|
252
|
+
if (missing.length === 0) {
|
|
253
|
+
results.push({
|
|
254
|
+
name: "MCP Tools",
|
|
255
|
+
status: "ok",
|
|
256
|
+
message: `${allTools.length} tool definitions loaded`
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
results.push({
|
|
260
|
+
name: "MCP Tools",
|
|
261
|
+
status: "warn",
|
|
262
|
+
message: `${allTools.length} tools loaded, missing: ${missing.join(", ")}`
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
results.push({
|
|
267
|
+
name: "MCP Tools",
|
|
268
|
+
status: "error",
|
|
269
|
+
message: `Failed to load tool definitions: ${error.message}`
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
{
|
|
273
|
+
let linearTokenFound = false;
|
|
274
|
+
let linearTokenSource = "";
|
|
275
|
+
if (process.env["LINEAR_API_KEY"]) {
|
|
276
|
+
linearTokenFound = true;
|
|
277
|
+
linearTokenSource = "process.env";
|
|
278
|
+
}
|
|
279
|
+
if (!linearTokenFound) {
|
|
280
|
+
const envPath = join(process.cwd(), ".env");
|
|
281
|
+
if (existsSync(envPath)) {
|
|
282
|
+
try {
|
|
283
|
+
const envContent = readFileSync(envPath, "utf8");
|
|
284
|
+
if (/^LINEAR_API_KEY\s*=/m.test(envContent)) {
|
|
285
|
+
linearTokenFound = true;
|
|
286
|
+
linearTokenSource = ".env";
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!linearTokenFound) {
|
|
293
|
+
const envLocalPath = join(process.cwd(), ".env.local");
|
|
294
|
+
if (existsSync(envLocalPath)) {
|
|
295
|
+
try {
|
|
296
|
+
const envContent = readFileSync(envLocalPath, "utf8");
|
|
297
|
+
if (/^LINEAR_API_KEY\s*=/m.test(envContent)) {
|
|
298
|
+
linearTokenFound = true;
|
|
299
|
+
linearTokenSource = ".env.local";
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (linearTokenFound) {
|
|
306
|
+
results.push({
|
|
307
|
+
name: "Linear API Token",
|
|
308
|
+
status: "ok",
|
|
309
|
+
message: `Token found via ${linearTokenSource}`
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
results.push({
|
|
313
|
+
name: "Linear API Token",
|
|
314
|
+
status: "warn",
|
|
315
|
+
message: "LINEAR_API_KEY not found (checked process.env, .env, .env.local)",
|
|
316
|
+
fix: "Add LINEAR_API_KEY=lin_api_... to your .env file"
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
{
|
|
321
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
322
|
+
const projectHooksDir = join(process.cwd(), ".claude", "hooks");
|
|
323
|
+
const globalHooksDir = join(homedir(), ".claude", "hooks");
|
|
324
|
+
const expectedHookScripts = [
|
|
325
|
+
"session-rescue.sh",
|
|
326
|
+
"stop-checkpoint.js",
|
|
327
|
+
"chime-on-stop.sh",
|
|
328
|
+
"auto-checkpoint.js",
|
|
329
|
+
"cord-trace.js"
|
|
330
|
+
];
|
|
331
|
+
const registeredHooks = [];
|
|
332
|
+
if (existsSync(settingsPath)) {
|
|
333
|
+
try {
|
|
334
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
335
|
+
if (settings.hooks) {
|
|
336
|
+
for (const eventType of Object.keys(settings.hooks)) {
|
|
337
|
+
const groups = settings.hooks[eventType];
|
|
338
|
+
for (const group of groups) {
|
|
339
|
+
for (const hook of group.hooks) {
|
|
340
|
+
registeredHooks.push(hook.command);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const presentScripts = [];
|
|
349
|
+
const missingScripts = [];
|
|
350
|
+
for (const script of expectedHookScripts) {
|
|
351
|
+
const inGlobal = existsSync(join(globalHooksDir, script));
|
|
352
|
+
const inProject = existsSync(join(projectHooksDir, script));
|
|
353
|
+
if (inGlobal || inProject) {
|
|
354
|
+
presentScripts.push(script);
|
|
355
|
+
} else {
|
|
356
|
+
missingScripts.push(script);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const unregisteredScripts = expectedHookScripts.filter(
|
|
360
|
+
(script) => !registeredHooks.some((cmd) => cmd.includes(script))
|
|
361
|
+
);
|
|
362
|
+
if (presentScripts.length === expectedHookScripts.length && unregisteredScripts.length === 0) {
|
|
363
|
+
results.push({
|
|
364
|
+
name: "Hook Scripts",
|
|
365
|
+
status: "ok",
|
|
366
|
+
message: `All ${expectedHookScripts.length} hook scripts present and registered`
|
|
367
|
+
});
|
|
368
|
+
} else if (presentScripts.length > 0) {
|
|
369
|
+
const parts = [];
|
|
370
|
+
if (missingScripts.length > 0) {
|
|
371
|
+
parts.push(`missing files: ${missingScripts.join(", ")}`);
|
|
372
|
+
}
|
|
373
|
+
if (unregisteredScripts.length > 0) {
|
|
374
|
+
parts.push(
|
|
375
|
+
`not in settings.json: ${unregisteredScripts.join(", ")}`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
results.push({
|
|
379
|
+
name: "Hook Scripts",
|
|
380
|
+
status: "warn",
|
|
381
|
+
message: `${presentScripts.length}/${expectedHookScripts.length} hooks present; ${parts.join("; ")}`,
|
|
382
|
+
fix: "Run: stackmemory hooks install"
|
|
383
|
+
});
|
|
384
|
+
} else {
|
|
385
|
+
results.push({
|
|
386
|
+
name: "Hook Scripts",
|
|
387
|
+
status: "warn",
|
|
388
|
+
message: "No StackMemory hook scripts found",
|
|
389
|
+
fix: "Run: stackmemory hooks install"
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
{
|
|
394
|
+
const nodeVersion = process.version;
|
|
395
|
+
const major = parseInt(nodeVersion.slice(1), 10);
|
|
396
|
+
if (major >= 20) {
|
|
397
|
+
results.push({
|
|
398
|
+
name: "Node.js Version",
|
|
399
|
+
status: "ok",
|
|
400
|
+
message: `${nodeVersion} (requires >= 20.0.0)`
|
|
401
|
+
});
|
|
402
|
+
} else {
|
|
403
|
+
results.push({
|
|
404
|
+
name: "Node.js Version",
|
|
405
|
+
status: "error",
|
|
406
|
+
message: `${nodeVersion} is too old (requires >= 20.0.0)`,
|
|
407
|
+
fix: "Upgrade Node.js: https://nodejs.org/"
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
{
|
|
203
412
|
try {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
413
|
+
const npmVersion = execSync("npm --version", {
|
|
414
|
+
encoding: "utf-8",
|
|
415
|
+
timeout: 5e3
|
|
416
|
+
}).trim();
|
|
417
|
+
const npmMajor = parseInt(npmVersion.split(".")[0], 10);
|
|
418
|
+
if (npmMajor >= 10) {
|
|
207
419
|
results.push({
|
|
208
|
-
name: "
|
|
420
|
+
name: "npm Version",
|
|
209
421
|
status: "ok",
|
|
210
|
-
message:
|
|
422
|
+
message: `v${npmVersion} (requires >= 10.0.0)`
|
|
211
423
|
});
|
|
212
424
|
} else {
|
|
213
425
|
results.push({
|
|
214
|
-
name: "
|
|
426
|
+
name: "npm Version",
|
|
215
427
|
status: "warn",
|
|
216
|
-
message:
|
|
217
|
-
fix: "
|
|
428
|
+
message: `v${npmVersion} is below recommended >= 10.0.0`,
|
|
429
|
+
fix: "Upgrade npm: npm install -g npm@latest"
|
|
218
430
|
});
|
|
219
431
|
}
|
|
220
432
|
} catch {
|
|
221
433
|
results.push({
|
|
222
|
-
name: "
|
|
434
|
+
name: "npm Version",
|
|
223
435
|
status: "warn",
|
|
224
|
-
message: "Could not
|
|
436
|
+
message: "Could not detect npm version",
|
|
437
|
+
fix: "Ensure npm is installed and in PATH"
|
|
225
438
|
});
|
|
226
439
|
}
|
|
227
|
-
} else {
|
|
228
|
-
results.push({
|
|
229
|
-
name: "Claude Hooks",
|
|
230
|
-
status: "warn",
|
|
231
|
-
message: "Claude hooks not installed (optional)",
|
|
232
|
-
fix: "Run: stackmemory hooks install"
|
|
233
|
-
});
|
|
234
440
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
441
|
+
{
|
|
442
|
+
const configJsonPath = join(homedir(), ".claude", "config.json");
|
|
443
|
+
const mcpJsonPath = join(homedir(), ".claude", "stackmemory-mcp.json");
|
|
444
|
+
let mcpRegistered = false;
|
|
445
|
+
let mcpServerConfigured = false;
|
|
446
|
+
if (existsSync(configJsonPath)) {
|
|
447
|
+
try {
|
|
448
|
+
const configJson = JSON.parse(readFileSync(configJsonPath, "utf8"));
|
|
449
|
+
const configFiles = configJson?.mcp?.configFiles || [];
|
|
450
|
+
mcpRegistered = configFiles.some(
|
|
451
|
+
(f) => f.includes("stackmemory")
|
|
452
|
+
);
|
|
453
|
+
} catch {
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (existsSync(mcpJsonPath)) {
|
|
457
|
+
try {
|
|
458
|
+
const mcpConfig = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
|
|
459
|
+
mcpServerConfigured = !!mcpConfig?.mcpServers?.stackmemory;
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (mcpRegistered && mcpServerConfigured) {
|
|
241
464
|
results.push({
|
|
242
|
-
name:
|
|
465
|
+
name: "MCP Registration",
|
|
243
466
|
status: "ok",
|
|
244
|
-
message: "
|
|
467
|
+
message: "StackMemory MCP registered in Claude config.json and server configured"
|
|
245
468
|
});
|
|
246
|
-
} else if (!
|
|
469
|
+
} else if (mcpServerConfigured && !mcpRegistered) {
|
|
247
470
|
results.push({
|
|
248
|
-
name:
|
|
249
|
-
status: "
|
|
250
|
-
message: "
|
|
251
|
-
fix:
|
|
471
|
+
name: "MCP Registration",
|
|
472
|
+
status: "warn",
|
|
473
|
+
message: "MCP server config exists but not referenced in config.json",
|
|
474
|
+
fix: "Run: stackmemory setup-mcp"
|
|
475
|
+
});
|
|
476
|
+
} else if (mcpRegistered && !mcpServerConfigured) {
|
|
477
|
+
results.push({
|
|
478
|
+
name: "MCP Registration",
|
|
479
|
+
status: "warn",
|
|
480
|
+
message: "config.json references MCP but server config missing",
|
|
481
|
+
fix: "Run: stackmemory setup-mcp"
|
|
482
|
+
});
|
|
483
|
+
} else {
|
|
484
|
+
results.push({
|
|
485
|
+
name: "MCP Registration",
|
|
486
|
+
status: "warn",
|
|
487
|
+
message: "StackMemory not registered in Claude MCP settings",
|
|
488
|
+
fix: "Run: stackmemory setup-mcp"
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
{
|
|
493
|
+
const desireDir = join(homedir(), ".stackmemory", "desire-paths");
|
|
494
|
+
if (existsSync(desireDir)) {
|
|
495
|
+
try {
|
|
496
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
497
|
+
const files = readdirSync(desireDir).filter(
|
|
498
|
+
(f) => f.startsWith("desire-") && f.endsWith(".jsonl")
|
|
499
|
+
);
|
|
500
|
+
let totalFailures = 0;
|
|
501
|
+
let unknownTools = 0;
|
|
502
|
+
for (const file of files) {
|
|
503
|
+
const lines = readFileSync(join(desireDir, file), "utf-8").split("\n").filter(Boolean);
|
|
504
|
+
for (const line of lines) {
|
|
505
|
+
try {
|
|
506
|
+
const entry = JSON.parse(line);
|
|
507
|
+
if (new Date(entry.ts).getTime() < cutoff) continue;
|
|
508
|
+
totalFailures++;
|
|
509
|
+
if (entry.category === "unknown_tool") unknownTools++;
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (totalFailures > 0) {
|
|
515
|
+
results.push({
|
|
516
|
+
name: "Desire Paths",
|
|
517
|
+
status: unknownTools > 0 ? "warn" : "ok",
|
|
518
|
+
message: `${totalFailures} tool failures in last 7d (${unknownTools} unknown tools)`,
|
|
519
|
+
fix: unknownTools > 0 ? "Run: stackmemory desires summary" : void 0
|
|
520
|
+
});
|
|
521
|
+
} else {
|
|
522
|
+
results.push({
|
|
523
|
+
name: "Desire Paths",
|
|
524
|
+
status: "ok",
|
|
525
|
+
message: "No tool failures in last 7d"
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
} catch {
|
|
529
|
+
results.push({
|
|
530
|
+
name: "Desire Paths",
|
|
531
|
+
status: "ok",
|
|
532
|
+
message: "Desire path logging active (no data yet)"
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
results.push({
|
|
537
|
+
name: "Desire Paths",
|
|
538
|
+
status: "ok",
|
|
539
|
+
message: "Desire path logging not yet active"
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
{
|
|
544
|
+
try {
|
|
545
|
+
const { readDaemonStatus } = await import("../../daemon/daemon-config.js");
|
|
546
|
+
const status = readDaemonStatus();
|
|
547
|
+
if (status.running) {
|
|
548
|
+
const uptime = status.startedAt ? Math.round((Date.now() - status.startedAt) / 1e3 / 60) : 0;
|
|
549
|
+
const svcCount = Object.values(status.services || {}).filter(
|
|
550
|
+
(s) => s.enabled
|
|
551
|
+
).length;
|
|
552
|
+
results.push({
|
|
553
|
+
name: "Background Daemon",
|
|
554
|
+
status: "ok",
|
|
555
|
+
message: `Running (PID: ${status.pid}, ${svcCount} services, ${uptime}min uptime)`
|
|
556
|
+
});
|
|
557
|
+
} else {
|
|
558
|
+
results.push({
|
|
559
|
+
name: "Background Daemon",
|
|
560
|
+
status: "warn",
|
|
561
|
+
message: "Daemon not running \u2014 context auto-save and maintenance disabled",
|
|
562
|
+
fix: "Run: stackmemory daemon start"
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
} catch {
|
|
566
|
+
results.push({
|
|
567
|
+
name: "Background Daemon",
|
|
568
|
+
status: "warn",
|
|
569
|
+
message: "Could not check daemon status",
|
|
570
|
+
fix: "Run: stackmemory daemon start"
|
|
252
571
|
});
|
|
253
572
|
}
|
|
254
573
|
}
|
|
@@ -282,12 +601,23 @@ function createDoctorCommand() {
|
|
|
282
601
|
if (result.fix) {
|
|
283
602
|
console.log(chalk.cyan(` Fix: ${result.fix}`));
|
|
284
603
|
if (options.fix && result.status !== "ok") {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
604
|
+
const fixCmds = [
|
|
605
|
+
"stackmemory setup-mcp",
|
|
606
|
+
"stackmemory hooks install",
|
|
607
|
+
"stackmemory daemon start"
|
|
608
|
+
];
|
|
609
|
+
for (const cmd of fixCmds) {
|
|
610
|
+
if (result.fix.includes(cmd)) {
|
|
611
|
+
console.log(chalk.gray(` Attempting: ${cmd}...`));
|
|
612
|
+
try {
|
|
613
|
+
execSync(cmd, {
|
|
614
|
+
stdio: "inherit",
|
|
615
|
+
timeout: 15e3
|
|
616
|
+
});
|
|
617
|
+
} catch {
|
|
618
|
+
console.log(chalk.red(" Auto-fix failed"));
|
|
619
|
+
}
|
|
620
|
+
break;
|
|
291
621
|
}
|
|
292
622
|
}
|
|
293
623
|
}
|