@stackmemoryai/stackmemory 1.2.8 → 1.2.9

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 CHANGED
@@ -14,7 +14,7 @@ Lossless, project-scoped memory for AI coding tools. **[Website](https://stackme
14
14
  StackMemory is a **production-ready memory runtime** for AI coding tools that preserves full project context across sessions:
15
15
 
16
16
  - **Zero-config setup** — `stackmemory init` just works
17
- - **55 MCP tools** for Claude Code integration (context, tasks, Linear, traces, discovery, cord, team)
17
+ - **56 MCP tools** for Claude Code integration (context, tasks, Linear, traces, discovery, cord, team, planning, providers, and more)
18
18
  - **FTS5 full-text search** with BM25 scoring and hybrid retrieval
19
19
  - **Full Linear integration** with bidirectional sync and OAuth/API key support
20
20
  - **Context persistence** that survives `/clear` operations
@@ -356,7 +356,7 @@ See [docs/cli.md](https://github.com/stackmemoryai/stackmemory/blob/main/docs/cl
356
356
  ## Documentation
357
357
 
358
358
  - [Getting Started](./docs/GETTING_STARTED.md) — Quick start guide (5 minutes)
359
- - [MCP Tools Reference](https://stackmemoryai.github.io/stackmemory/tools.html) — All 55 MCP tools
359
+ - [MCP Tools Reference](https://stackmemoryai.github.io/stackmemory/tools.html) — All 56 MCP tools
360
360
  - [CLI Reference](./docs/cli.md) — Full command reference
361
361
  - [Setup Guide](./docs/SETUP.md) — Advanced setup options
362
362
  - [Development Guide](./docs/DEVELOPMENT.md) — Contributing and development
@@ -5,8 +5,8 @@ const __dirname = __pathDirname(__filename);
5
5
  import { Command } from "commander";
6
6
  import chalk from "chalk";
7
7
  import ora from "ora";
8
- import { existsSync, readFileSync, unlinkSync } from "fs";
9
- import { spawn } from "child_process";
8
+ import { existsSync, readFileSync, unlinkSync, statSync } from "fs";
9
+ import { spawn, execSync } from "child_process";
10
10
  import { join } from "path";
11
11
  import {
12
12
  loadDaemonConfig,
@@ -23,6 +23,7 @@ Examples:
23
23
  stackmemory daemon start Start the daemon
24
24
  stackmemory daemon stop Stop the daemon
25
25
  stackmemory daemon status Check daemon status
26
+ stackmemory daemon health Check daemon health metrics
26
27
  stackmemory daemon logs View daemon logs
27
28
  stackmemory daemon config Show/edit configuration
28
29
 
@@ -302,6 +303,100 @@ The daemon provides:
302
303
  console.log(chalk.bold("To start: stackmemory daemon start"));
303
304
  }
304
305
  });
306
+ cmd.command("health").description("Check daemon health metrics").option("--json", "Output as JSON").action((options) => {
307
+ const status = readDaemonStatus();
308
+ const config = loadDaemonConfig();
309
+ const paths = getDaemonPaths();
310
+ if (options.json) {
311
+ const health = buildHealthReport(status, config, paths);
312
+ console.log(JSON.stringify(health, null, 2));
313
+ return;
314
+ }
315
+ console.log(chalk.bold("\nDaemon Health Check\n"));
316
+ if (!status.running) {
317
+ console.log(`${chalk.red("[DOWN]")} Daemon Process`);
318
+ console.log(chalk.gray(" Daemon is not running"));
319
+ console.log(chalk.cyan(" Fix: stackmemory daemon start"));
320
+ console.log("");
321
+ console.log(`Overall: ${chalk.red("RED")} - daemon not running`);
322
+ return;
323
+ }
324
+ const checks = [];
325
+ checks.push({
326
+ name: "Daemon Process",
327
+ status: "ok",
328
+ detail: `PID ${status.pid}`
329
+ });
330
+ if (status.uptime) {
331
+ const uptimeStr = formatDuration(status.uptime);
332
+ const uptimeStatus = status.uptime < 6e4 ? "warn" : "ok";
333
+ checks.push({
334
+ name: "Uptime",
335
+ status: uptimeStatus,
336
+ detail: uptimeStr
337
+ });
338
+ }
339
+ if (status.pid) {
340
+ const memInfo = getProcessMemory(status.pid);
341
+ if (memInfo) {
342
+ const mbUsed = Math.round(memInfo / 1024 / 1024);
343
+ const memStatus = mbUsed > 512 ? "error" : mbUsed > 256 ? "warn" : "ok";
344
+ checks.push({
345
+ name: "Memory (RSS)",
346
+ status: memStatus,
347
+ detail: `${mbUsed} MB`
348
+ });
349
+ } else {
350
+ checks.push({
351
+ name: "Memory (RSS)",
352
+ status: "warn",
353
+ detail: "Could not read process memory"
354
+ });
355
+ }
356
+ }
357
+ const serviceChecks = getServiceHealthChecks(status, config);
358
+ checks.push(...serviceChecks);
359
+ const errorInfo = countRecentErrors(paths.logFile);
360
+ const errorStatus = errorInfo.count > 10 ? "error" : errorInfo.count > 0 ? "warn" : "ok";
361
+ checks.push({
362
+ name: "Recent Errors (1h)",
363
+ status: errorStatus,
364
+ detail: errorInfo.count === 0 ? "None" : `${errorInfo.count} error${errorInfo.count !== 1 ? "s" : ""}${errorInfo.lastError ? ` (latest: ${errorInfo.lastError.slice(0, 60)})` : ""}`
365
+ });
366
+ if (existsSync(paths.logFile)) {
367
+ try {
368
+ const stat = statSync(paths.logFile);
369
+ const sizeMb = (stat.size / 1024 / 1024).toFixed(1);
370
+ const logStatus = stat.size > 100 * 1024 * 1024 ? "error" : stat.size > 50 * 1024 * 1024 ? "warn" : "ok";
371
+ checks.push({
372
+ name: "Log File Size",
373
+ status: logStatus,
374
+ detail: `${sizeMb} MB`
375
+ });
376
+ } catch {
377
+ }
378
+ }
379
+ const maxNameLen = Math.max(...checks.map((c) => c.name.length));
380
+ for (const check of checks) {
381
+ const icon = check.status === "ok" ? chalk.green("[OK] ") : check.status === "warn" ? chalk.yellow("[WARN] ") : chalk.red("[ERROR]");
382
+ const paddedName = check.name.padEnd(maxNameLen + 2);
383
+ console.log(` ${icon} ${paddedName} ${chalk.gray(check.detail)}`);
384
+ }
385
+ const hasError = checks.some((c) => c.status === "error");
386
+ const hasWarn = checks.some((c) => c.status === "warn");
387
+ console.log("");
388
+ if (hasError) {
389
+ console.log(
390
+ `Overall: ${chalk.red("RED")} - one or more critical issues detected`
391
+ );
392
+ } else if (hasWarn) {
393
+ console.log(
394
+ `Overall: ${chalk.yellow("YELLOW")} - healthy with warnings`
395
+ );
396
+ } else {
397
+ console.log(`Overall: ${chalk.green("GREEN")} - all services healthy`);
398
+ }
399
+ });
305
400
  cmd.command("logs").description("View daemon logs").option("-n, --lines <number>", "Number of lines to show", "50").option("-f, --follow", "Follow log output").option("--level <level>", "Filter by log level").action((options) => {
306
401
  const { logFile } = getDaemonPaths();
307
402
  if (!existsSync(logFile)) {
@@ -442,6 +537,215 @@ function getDaemonScriptPath() {
442
537
  }
443
538
  return candidates[0];
444
539
  }
540
+ function formatDuration(ms) {
541
+ const totalSecs = Math.round(ms / 1e3);
542
+ const days = Math.floor(totalSecs / 86400);
543
+ const hours = Math.floor(totalSecs % 86400 / 3600);
544
+ const mins = Math.floor(totalSecs % 3600 / 60);
545
+ const secs = totalSecs % 60;
546
+ const parts = [];
547
+ if (days > 0) parts.push(`${days}d`);
548
+ if (hours > 0) parts.push(`${hours}h`);
549
+ if (mins > 0) parts.push(`${mins}m`);
550
+ if (parts.length === 0 || secs > 0) parts.push(`${secs}s`);
551
+ return parts.join(" ");
552
+ }
553
+ function formatTimeAgo(timestamp) {
554
+ const ago = Date.now() - timestamp;
555
+ if (ago < 6e4) return `${Math.round(ago / 1e3)}s ago`;
556
+ if (ago < 36e5) return `${Math.round(ago / 6e4)}m ago`;
557
+ if (ago < 864e5) return `${(ago / 36e5).toFixed(1)}h ago`;
558
+ return `${(ago / 864e5).toFixed(1)}d ago`;
559
+ }
560
+ function getProcessMemory(pid) {
561
+ try {
562
+ const output = execSync(`ps -o rss= -p ${pid}`, {
563
+ encoding: "utf8",
564
+ timeout: 5e3,
565
+ stdio: "pipe"
566
+ }).trim();
567
+ const rssKb = parseInt(output, 10);
568
+ if (isNaN(rssKb)) return null;
569
+ return rssKb * 1024;
570
+ } catch {
571
+ return null;
572
+ }
573
+ }
574
+ function getServiceHealthChecks(status, config) {
575
+ const checks = [];
576
+ const ctx = status.services.context;
577
+ if (ctx.enabled) {
578
+ const intervalMs = config.context.interval * 6e4;
579
+ const overdue = ctx.lastRun ? Date.now() - ctx.lastRun > intervalMs * 2 : false;
580
+ checks.push({
581
+ name: "Context Service",
582
+ status: overdue ? "warn" : "ok",
583
+ detail: ctx.lastRun ? `Last save: ${formatTimeAgo(ctx.lastRun)} | Saves: ${ctx.saveCount ?? 0}` : `Enabled (interval: ${config.context.interval}m) | No saves yet`
584
+ });
585
+ } else {
586
+ checks.push({
587
+ name: "Context Service",
588
+ status: "ok",
589
+ detail: "Disabled"
590
+ });
591
+ }
592
+ const lin = status.services.linear;
593
+ if (lin.enabled) {
594
+ const intervalMs = config.linear.interval * 6e4;
595
+ const overdue = lin.lastRun ? Date.now() - lin.lastRun > intervalMs * 2 : false;
596
+ checks.push({
597
+ name: "Linear Service",
598
+ status: overdue ? "warn" : "ok",
599
+ detail: lin.lastRun ? `Last sync: ${formatTimeAgo(lin.lastRun)} | Syncs: ${lin.syncCount ?? 0}` : `Enabled (interval: ${config.linear.interval}m) | No syncs yet`
600
+ });
601
+ } else {
602
+ checks.push({
603
+ name: "Linear Service",
604
+ status: "ok",
605
+ detail: "Disabled"
606
+ });
607
+ }
608
+ const maint = status.services.maintenance;
609
+ if (maint?.enabled) {
610
+ const intervalMs = config.maintenance.interval * 6e4;
611
+ const overdue = maint.lastRun ? Date.now() - maint.lastRun > intervalMs * 2 : false;
612
+ checks.push({
613
+ name: "Maintenance Service",
614
+ status: overdue ? "warn" : "ok",
615
+ detail: maint.lastRun ? `Last run: ${formatTimeAgo(maint.lastRun)} | FTS rebuilds: ${maint.ftsRebuilds ?? 0} | Stale cleaned: ${maint.staleFramesCleaned ?? 0}` : `Enabled (interval: ${config.maintenance.interval}m) | No runs yet`
616
+ });
617
+ } else {
618
+ checks.push({
619
+ name: "Maintenance Service",
620
+ status: "ok",
621
+ detail: "Disabled"
622
+ });
623
+ }
624
+ const mem = status.services.memory;
625
+ if (mem?.enabled) {
626
+ const ramStr = mem.currentRamPercent !== void 0 ? `RAM: ${Math.round(mem.currentRamPercent * 100)}%` : "RAM: n/a";
627
+ const triggerStr = `Triggers: ${mem.triggerCount ?? 0}`;
628
+ checks.push({
629
+ name: "Memory Service",
630
+ status: "ok",
631
+ detail: `${ramStr} | ${triggerStr}`
632
+ });
633
+ } else {
634
+ checks.push({
635
+ name: "Memory Service",
636
+ status: "ok",
637
+ detail: "Disabled"
638
+ });
639
+ }
640
+ const fw = status.services.fileWatch;
641
+ checks.push({
642
+ name: "FileWatch Service",
643
+ status: "ok",
644
+ detail: fw.enabled ? `Active | Events: ${fw.eventsProcessed ?? 0}` : "Disabled"
645
+ });
646
+ return checks;
647
+ }
648
+ function countRecentErrors(logFile) {
649
+ if (!existsSync(logFile)) {
650
+ return { count: 0 };
651
+ }
652
+ try {
653
+ const content = readFileSync(logFile, "utf8");
654
+ const lines = content.trim().split("\n");
655
+ const oneHourAgo = Date.now() - 36e5;
656
+ let count = 0;
657
+ let lastError;
658
+ for (let i = lines.length - 1; i >= 0; i--) {
659
+ try {
660
+ const entry = JSON.parse(lines[i]);
661
+ const timestamp = new Date(entry.timestamp).getTime();
662
+ if (timestamp < oneHourAgo) break;
663
+ if (entry.level === "ERROR") {
664
+ count++;
665
+ if (!lastError) {
666
+ lastError = entry.message;
667
+ }
668
+ }
669
+ } catch {
670
+ }
671
+ }
672
+ return { count, lastError };
673
+ } catch {
674
+ return { count: 0 };
675
+ }
676
+ }
677
+ function buildHealthReport(status, config, paths) {
678
+ if (!status.running) {
679
+ return {
680
+ overall: "RED",
681
+ running: false,
682
+ services: {},
683
+ errors: { recentCount: 0 }
684
+ };
685
+ }
686
+ const errorInfo = countRecentErrors(paths.logFile);
687
+ let memoryMb;
688
+ if (status.pid) {
689
+ const memBytes = getProcessMemory(status.pid);
690
+ if (memBytes) memoryMb = Math.round(memBytes / 1024 / 1024);
691
+ }
692
+ const services = {};
693
+ const svcEntries = [
694
+ {
695
+ key: "context",
696
+ enabled: status.services.context.enabled,
697
+ lastRun: status.services.context.lastRun
698
+ },
699
+ {
700
+ key: "linear",
701
+ enabled: status.services.linear.enabled,
702
+ lastRun: status.services.linear.lastRun
703
+ },
704
+ {
705
+ key: "maintenance",
706
+ enabled: status.services.maintenance?.enabled ?? false,
707
+ lastRun: status.services.maintenance?.lastRun
708
+ },
709
+ {
710
+ key: "memory",
711
+ enabled: status.services.memory?.enabled ?? false,
712
+ lastRun: status.services.memory?.lastTrigger
713
+ },
714
+ {
715
+ key: "fileWatch",
716
+ enabled: status.services.fileWatch.enabled
717
+ }
718
+ ];
719
+ for (const svc of svcEntries) {
720
+ services[svc.key] = {
721
+ enabled: svc.enabled,
722
+ status: svc.enabled ? "running" : "disabled",
723
+ lastRun: svc.lastRun,
724
+ lastRunAgo: svc.lastRun ? formatTimeAgo(svc.lastRun) : void 0
725
+ };
726
+ }
727
+ let logFileSizeMb;
728
+ if (existsSync(paths.logFile)) {
729
+ try {
730
+ const stat = statSync(paths.logFile);
731
+ logFileSizeMb = parseFloat((stat.size / 1024 / 1024).toFixed(1));
732
+ } catch {
733
+ }
734
+ }
735
+ const hasError = errorInfo.count > 10 || memoryMb !== void 0 && memoryMb > 512 || logFileSizeMb !== void 0 && logFileSizeMb > 100;
736
+ const hasWarn = errorInfo.count > 0 || memoryMb !== void 0 && memoryMb > 256 || status.uptime !== void 0 && status.uptime < 6e4 || logFileSizeMb !== void 0 && logFileSizeMb > 50;
737
+ return {
738
+ overall: hasError ? "RED" : hasWarn ? "YELLOW" : "GREEN",
739
+ running: true,
740
+ pid: status.pid,
741
+ uptime: status.uptime,
742
+ uptimeFormatted: status.uptime ? formatDuration(status.uptime) : void 0,
743
+ memoryMb,
744
+ services,
745
+ errors: { recentCount: errorInfo.count, lastError: errorInfo.lastError },
746
+ logFileSizeMb
747
+ };
748
+ }
445
749
  var daemon_default = createDaemonCommand();
446
750
  export {
447
751
  createDaemonCommand,
@@ -0,0 +1,117 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { Command } from "commander";
6
+ import { readdirSync, readFileSync, existsSync } from "fs";
7
+ import { join } from "path";
8
+ import { homedir } from "os";
9
+ import chalk from "chalk";
10
+ const DESIRE_DIR = join(homedir(), ".stackmemory", "desire-paths");
11
+ function loadEntries(since) {
12
+ if (!existsSync(DESIRE_DIR)) return [];
13
+ const files = readdirSync(DESIRE_DIR).filter((f) => f.startsWith("desire-") && f.endsWith(".jsonl")).sort();
14
+ const entries = [];
15
+ for (const file of files) {
16
+ const lines = readFileSync(join(DESIRE_DIR, file), "utf-8").split("\n").filter(Boolean);
17
+ for (const line of lines) {
18
+ try {
19
+ const entry = JSON.parse(line);
20
+ if (since && new Date(entry.ts).getTime() < since) continue;
21
+ entries.push(entry);
22
+ } catch {
23
+ }
24
+ }
25
+ }
26
+ return entries;
27
+ }
28
+ function createDesiresCommands() {
29
+ const desires = new Command("desires").description(
30
+ "Analyze desire path logs \u2014 failed tool calls and unmet agent needs"
31
+ );
32
+ desires.command("summary").description("Show aggregated failure counts by tool").action(() => {
33
+ const entries = loadEntries();
34
+ if (entries.length === 0) {
35
+ console.log(chalk.yellow("No desire path data found."));
36
+ console.log(chalk.gray(` Logs dir: ${DESIRE_DIR}`));
37
+ return;
38
+ }
39
+ const byTool = /* @__PURE__ */ new Map();
40
+ for (const e of entries) {
41
+ const existing = byTool.get(e.tool);
42
+ if (!existing || e.ts > existing.lastSeen) {
43
+ byTool.set(e.tool, {
44
+ count: (existing?.count || 0) + 1,
45
+ category: e.category,
46
+ lastSeen: e.ts
47
+ });
48
+ } else {
49
+ existing.count++;
50
+ }
51
+ }
52
+ const sorted = [...byTool.entries()].sort(
53
+ (a, b) => b[1].count - a[1].count
54
+ );
55
+ console.log(chalk.bold("Desire Path Summary"));
56
+ console.log(chalk.gray(`Total failures: ${entries.length}
57
+ `));
58
+ const toolW = 30;
59
+ const countW = 7;
60
+ const catW = 16;
61
+ console.log(
62
+ chalk.gray(
63
+ `${"Tool".padEnd(toolW)} ${"Count".padStart(countW)} ${"Category".padEnd(catW)} Last Seen`
64
+ )
65
+ );
66
+ console.log(chalk.gray("-".repeat(toolW + countW + catW + 22)));
67
+ for (const [tool, data] of sorted) {
68
+ const catColor = data.category === "unknown_tool" ? chalk.red : data.category === "invalid_params" ? chalk.yellow : chalk.gray;
69
+ const lastDate = data.lastSeen.slice(0, 19).replace("T", " ");
70
+ console.log(
71
+ `${tool.padEnd(toolW)} ${String(data.count).padStart(countW)} ${catColor(data.category.padEnd(catW))} ${chalk.gray(lastDate)}`
72
+ );
73
+ }
74
+ const byCat = /* @__PURE__ */ new Map();
75
+ for (const e of entries) {
76
+ byCat.set(e.category, (byCat.get(e.category) || 0) + 1);
77
+ }
78
+ console.log(chalk.bold("\nBy Category:"));
79
+ for (const [cat, count] of [...byCat.entries()].sort(
80
+ (a, b) => b[1] - a[1]
81
+ )) {
82
+ console.log(` ${cat}: ${count}`);
83
+ }
84
+ });
85
+ desires.command("list").description("Show recent failures with details").option("-l, --limit <n>", "Max entries to show", "20").option("-s, --since <epoch>", "Filter entries after this epoch (ms)").option("-u, --unknown-only", "Show only unknown_tool category").action(
86
+ (options) => {
87
+ const since = options.since ? parseInt(options.since, 10) : void 0;
88
+ let entries = loadEntries(since);
89
+ if (options.unknownOnly) {
90
+ entries = entries.filter((e) => e.category === "unknown_tool");
91
+ }
92
+ if (entries.length === 0) {
93
+ console.log(chalk.yellow("No desire path data found."));
94
+ console.log(chalk.gray(` Logs dir: ${DESIRE_DIR}`));
95
+ return;
96
+ }
97
+ const limit = parseInt(options.limit, 10) || 20;
98
+ const recent = entries.sort((a, b) => b.ts.localeCompare(a.ts)).slice(0, limit);
99
+ console.log(
100
+ chalk.bold(`Recent Desire Paths (${recent.length}/${entries.length})`)
101
+ );
102
+ console.log("");
103
+ for (const e of recent) {
104
+ const catColor = e.category === "unknown_tool" ? chalk.red : e.category === "invalid_params" ? chalk.yellow : chalk.gray;
105
+ const ts = e.ts.slice(0, 19).replace("T", " ");
106
+ console.log(
107
+ `${chalk.gray(ts)} ${chalk.bold(e.tool)} ${catColor(`[${e.category}]`)}`
108
+ );
109
+ console.log(` ${chalk.gray(e.error.slice(0, 120))}`);
110
+ }
111
+ }
112
+ );
113
+ return desires;
114
+ }
115
+ export {
116
+ createDesiresCommands
117
+ };