@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 +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/team.js +168 -0
- package/dist/src/cli/index.js +4 -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
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
|
-
- **
|
|
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
|
|
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
|
+
};
|