@stackmemoryai/stackmemory 1.5.4 → 1.5.6

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.
@@ -60,6 +60,55 @@ function formatElapsed(ms) {
60
60
  const days = Math.floor(hours / 24);
61
61
  return `${days}d ago`;
62
62
  }
63
+ function fmtTokens(n) {
64
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
65
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
66
+ return String(n);
67
+ }
68
+ function budgetBar(pct, width = 30) {
69
+ const filled = Math.min(Math.round(pct / 100 * width), width);
70
+ const empty = width - filled;
71
+ const color = pct >= 75 ? "\x1B[31m" : pct >= 50 ? "\x1B[33m" : "\x1B[32m";
72
+ const dim = "\x1B[2m";
73
+ const rst = "\x1B[0m";
74
+ return `${color}${"\u2588".repeat(filled)}${dim}${"\u2591".repeat(empty)}${rst} ${String(pct).padStart(3)}%`;
75
+ }
76
+ function fmtMinutes(m) {
77
+ if (m < 0) return "N/A";
78
+ if (m >= 60) return `${Math.floor(m / 60)}h ${m % 60}m`;
79
+ return `${m}m`;
80
+ }
81
+ function printUsageSummary(u) {
82
+ const totalTokens = u.totalTokens || 0;
83
+ const inputTokens = u.inputTokens || 0;
84
+ const outputTokens = u.outputTokens || 0;
85
+ const estMessages = u.estimatedMessages || 0;
86
+ const tokensPerMin = u.tokensPerMin || 0;
87
+ const budgetPct5x = u.budgetPct5x || 0;
88
+ const budgetPct20x = u.budgetPct20x || 0;
89
+ const mins5x = u.minutesRemaining5x ?? -1;
90
+ const mins20x = u.minutesRemaining20x ?? -1;
91
+ const cacheHitRate = u.cacheHitRate || 0;
92
+ const b = "\x1B[1m";
93
+ const d = "\x1B[2m";
94
+ const w = "\x1B[37m";
95
+ const r = "\x1B[0m";
96
+ console.log(`${b}Token Usage${r}`);
97
+ console.log(
98
+ ` Input ${w}${fmtTokens(inputTokens)}${r} ${d}|${r} Output ${w}${fmtTokens(outputTokens)}${r} ${d}|${r} Total ${w}${fmtTokens(totalTokens)}${r}`
99
+ );
100
+ console.log(
101
+ ` Rate ${w}${fmtTokens(tokensPerMin)}/min${r} ${d}|${r} Messages ${w}${estMessages}${r} ${d}|${r} Cache hit ${w}${cacheHitRate}%${r}`
102
+ );
103
+ console.log("");
104
+ console.log(`${b}Budget (Max plan, 5h window)${r}`);
105
+ console.log(
106
+ ` 5x (225 msgs) ${budgetBar(budgetPct5x)} ${d}~${fmtMinutes(mins5x)} left${r}`
107
+ );
108
+ console.log(
109
+ ` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${d}~${fmtMinutes(mins20x)} left${r}`
110
+ );
111
+ }
63
112
  function createConductorCommands() {
64
113
  const cmd = new Command("conductor");
65
114
  cmd.description("Conductor \u2014 autonomous agent orchestration via Linear");
@@ -334,6 +383,54 @@ function createConductorCommands() {
334
383
  process.on("SIGTERM", forward);
335
384
  });
336
385
  });
386
+ cmd.command("usage").description("Show token usage, budget, and time-to-exhaustion").option("--json", "Output as JSON", false).option("--scan", "Scan Claude Code JSONL logs for historical data", false).action(async (options) => {
387
+ const statusPath = join(
388
+ process.cwd(),
389
+ ".stackmemory",
390
+ "conductor-status.json"
391
+ );
392
+ if (options.scan) {
393
+ const conductor = new Conductor({ repoRoot: process.cwd() });
394
+ await conductor.scanUsageLogs();
395
+ const summary = conductor.getUsageSummary();
396
+ if (options.json) {
397
+ console.log(JSON.stringify(summary, null, 2));
398
+ return;
399
+ }
400
+ printUsageSummary(summary);
401
+ return;
402
+ }
403
+ if (!existsSync(statusPath)) {
404
+ console.log(
405
+ "No conductor-status.json found. Run with --scan to check JSONL logs, or start the conductor first."
406
+ );
407
+ return;
408
+ }
409
+ try {
410
+ const data = JSON.parse(readFileSync(statusPath, "utf-8"));
411
+ const usage = data.usage || {};
412
+ if (options.json) {
413
+ console.log(JSON.stringify(usage, null, 2));
414
+ return;
415
+ }
416
+ printUsageSummary(usage);
417
+ const rl = data.rateLimit;
418
+ if (rl) {
419
+ console.log("");
420
+ if (rl.inBackoff) {
421
+ console.log(
422
+ `Rate limit: \x1B[31mBACKOFF\x1B[0m (${rl.backoffRemainingSec}s remaining, ${rl.totalHits} total hits)`
423
+ );
424
+ } else {
425
+ console.log(
426
+ `Rate limit: \x1B[32mOK\x1B[0m (${rl.totalHits} total hits)`
427
+ );
428
+ }
429
+ }
430
+ } catch (err) {
431
+ console.error("Failed to read status file:", err.message);
432
+ }
433
+ });
337
434
  cmd.command("start").description("Start the orchestrator daemon").option("--team <id>", "Linear team ID").option(
338
435
  "--states <states>",
339
436
  "Comma-separated issue states to pick up",
@@ -367,6 +464,305 @@ function createConductorCommands() {
367
464
  });
368
465
  await conductor.start();
369
466
  });
467
+ cmd.command("monitor").description("Interactive TUI dashboard for conductor monitoring").option("--interval <seconds>", "Auto-refresh interval in seconds", "10").option(
468
+ "--phase <phase>",
469
+ "Filter by phase (reading, planning, implementing, testing, committing)"
470
+ ).option("--no-interactive", "Disable interactive keys (CI/pipe mode)").action(async (options) => {
471
+ const interval = parseInt(options.interval, 10) * 1e3;
472
+ const interactive = options.interactive !== false;
473
+ let currentMode = "dashboard";
474
+ let paused = false;
475
+ let refreshInterval = interval;
476
+ let phaseFilter = options.phase || null;
477
+ const b = "\x1B[1m";
478
+ const d = "\x1B[2m";
479
+ const cyan = "\x1B[36m";
480
+ const green = "\x1B[32m";
481
+ const yellow = "\x1B[33m";
482
+ const red = "\x1B[31m";
483
+ const r = "\x1B[0m";
484
+ function readStatuses() {
485
+ const agentsDir = join(
486
+ homedir(),
487
+ ".stackmemory",
488
+ "conductor",
489
+ "agents"
490
+ );
491
+ if (!existsSync(agentsDir)) return [];
492
+ const entries = readdirSync(agentsDir, { withFileTypes: true });
493
+ const statuses = [];
494
+ for (const entry of entries) {
495
+ if (!entry.isDirectory()) continue;
496
+ const statusPath = join(agentsDir, entry.name, "status.json");
497
+ if (!existsSync(statusPath)) continue;
498
+ try {
499
+ statuses.push(JSON.parse(readFileSync(statusPath, "utf-8")));
500
+ } catch {
501
+ }
502
+ }
503
+ statuses.sort(
504
+ (a, b2) => new Date(b2.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()
505
+ );
506
+ if (phaseFilter) {
507
+ return statuses.filter((s) => s.phase === phaseFilter);
508
+ }
509
+ return statuses;
510
+ }
511
+ function printStatusTable(statuses) {
512
+ if (statuses.length === 0) {
513
+ const filterNote = phaseFilter ? ` (filter: ${phaseFilter})` : "";
514
+ console.log(` No active agents${filterNote}`);
515
+ return;
516
+ }
517
+ const header = `${"Issue".padEnd(12)}${"Phase".padEnd(16)}${"Tools".padStart(7)}${"Files".padStart(7)}${"Tokens".padStart(9)} Last Update`;
518
+ console.log(header);
519
+ for (const s of statuses) {
520
+ const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
521
+ const line = `${s.issue.padEnd(12)}${s.phase.padEnd(16)}${String(s.toolCalls).padStart(7)}${String(s.filesModified).padStart(7)}${String(s.tokensUsed).padStart(9)} ${formatElapsed(elapsed)}`;
522
+ console.log(line);
523
+ }
524
+ }
525
+ function getWorktreeFiles(workspacePath) {
526
+ if (!workspacePath || !existsSync(workspacePath)) return [];
527
+ try {
528
+ const output = execSync("git status --short 2>/dev/null", {
529
+ cwd: workspacePath,
530
+ timeout: 5e3,
531
+ encoding: "utf-8"
532
+ });
533
+ return output.trim().split("\n").filter((l) => l.length > 0);
534
+ } catch {
535
+ return [];
536
+ }
537
+ }
538
+ function printFilesView(statuses) {
539
+ if (statuses.length === 0) {
540
+ const filterNote = phaseFilter ? ` (filter: ${phaseFilter})` : "";
541
+ console.log(` No active agents${filterNote}`);
542
+ return;
543
+ }
544
+ for (const s of statuses) {
545
+ const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
546
+ const phaseColor = s.phase === "committing" ? green : s.phase === "testing" ? yellow : s.phase === "implementing" ? cyan : "";
547
+ console.log(
548
+ `${b}${s.issue}${r} ${phaseColor}${s.phase}${r} ${d}${formatElapsed(elapsed)}${r}`
549
+ );
550
+ const files = getWorktreeFiles(s.workspacePath || "");
551
+ if (files.length === 0) {
552
+ console.log(` ${d}(no file changes detected)${r}`);
553
+ } else {
554
+ for (const f of files) {
555
+ const status = f.substring(0, 2);
556
+ const path = f.substring(3);
557
+ let color = "";
558
+ if (status.includes("M")) color = yellow;
559
+ else if (status.includes("A") || status.includes("?"))
560
+ color = green;
561
+ else if (status.includes("D")) color = red;
562
+ console.log(` ${color}${status}${r} ${path}`);
563
+ }
564
+ }
565
+ console.log("");
566
+ }
567
+ }
568
+ async function getUsage() {
569
+ const conductor = new Conductor({ repoRoot: process.cwd() });
570
+ await conductor.scanUsageLogs();
571
+ return conductor.getUsageSummary();
572
+ }
573
+ async function render() {
574
+ process.stdout.write("\x1B[2J\x1B[H");
575
+ const pauseTag = paused ? " [PAUSED]" : "";
576
+ const intervalSec = Math.round(refreshInterval / 1e3);
577
+ console.log(
578
+ `${b}\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550${r}`
579
+ );
580
+ console.log(
581
+ `${b} Conductor Monitor${r} ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${pauseTag}`
582
+ );
583
+ console.log(
584
+ ` Mode: ${cyan}${currentMode}${r} | Refresh: ${intervalSec}s`
585
+ );
586
+ const filterNote = phaseFilter ? ` Filter: ${cyan}${phaseFilter}${r}` : "";
587
+ if (interactive) {
588
+ console.log(
589
+ ` ${d}[s]tatus [u]sage [f]iles [d]ashboard [j]son [l]ogs [r]efresh [p]ause [1-5]phase [0]clear [+/-] [q]uit${r}`
590
+ );
591
+ }
592
+ if (filterNote) console.log(filterNote);
593
+ console.log(
594
+ `${b}\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550${r}`
595
+ );
596
+ console.log("");
597
+ const statuses = readStatuses();
598
+ switch (currentMode) {
599
+ case "dashboard": {
600
+ printStatusTable(statuses);
601
+ console.log("");
602
+ const usage = await getUsage();
603
+ printUsageSummary(usage);
604
+ break;
605
+ }
606
+ case "status":
607
+ printStatusTable(statuses);
608
+ break;
609
+ case "usage": {
610
+ const usage = await getUsage();
611
+ printUsageSummary(usage);
612
+ break;
613
+ }
614
+ case "json": {
615
+ const usage = await getUsage();
616
+ console.log(JSON.stringify(usage, null, 2));
617
+ break;
618
+ }
619
+ case "files":
620
+ printFilesView(statuses);
621
+ break;
622
+ }
623
+ console.log("");
624
+ console.log(
625
+ `${d}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${r}`
626
+ );
627
+ }
628
+ await render();
629
+ if (!interactive) {
630
+ const timer = setInterval(async () => {
631
+ if (!paused) await render();
632
+ }, refreshInterval);
633
+ await new Promise((resolve) => {
634
+ process.on("SIGINT", () => {
635
+ clearInterval(timer);
636
+ resolve();
637
+ });
638
+ process.on("SIGTERM", () => {
639
+ clearInterval(timer);
640
+ resolve();
641
+ });
642
+ });
643
+ return;
644
+ }
645
+ if (process.stdin.isTTY) {
646
+ process.stdin.setRawMode(true);
647
+ }
648
+ process.stdin.resume();
649
+ process.stdin.setEncoding("utf-8");
650
+ let refreshTimer = null;
651
+ function scheduleRefresh() {
652
+ if (refreshTimer) clearTimeout(refreshTimer);
653
+ refreshTimer = setTimeout(async () => {
654
+ if (!paused) await render();
655
+ scheduleRefresh();
656
+ }, refreshInterval);
657
+ }
658
+ scheduleRefresh();
659
+ process.stdin.on("data", async (key) => {
660
+ switch (key) {
661
+ case "s":
662
+ currentMode = "status";
663
+ await render();
664
+ break;
665
+ case "u":
666
+ currentMode = "usage";
667
+ await render();
668
+ break;
669
+ case "d":
670
+ currentMode = "dashboard";
671
+ await render();
672
+ break;
673
+ case "f":
674
+ currentMode = "files";
675
+ await render();
676
+ break;
677
+ case "j":
678
+ currentMode = "json";
679
+ await render();
680
+ break;
681
+ case "1":
682
+ phaseFilter = "reading";
683
+ await render();
684
+ break;
685
+ case "2":
686
+ phaseFilter = "planning";
687
+ await render();
688
+ break;
689
+ case "3":
690
+ phaseFilter = "implementing";
691
+ await render();
692
+ break;
693
+ case "4":
694
+ phaseFilter = "testing";
695
+ await render();
696
+ break;
697
+ case "5":
698
+ phaseFilter = "committing";
699
+ await render();
700
+ break;
701
+ case "0":
702
+ phaseFilter = null;
703
+ await render();
704
+ break;
705
+ case "l": {
706
+ process.stdout.write("\x1B[2J\x1B[H");
707
+ const statuses = readStatuses();
708
+ if (statuses.length === 0) {
709
+ console.log("No active agents to show logs for.");
710
+ } else {
711
+ console.log("Active issues:");
712
+ console.log("");
713
+ for (const s of statuses) {
714
+ console.log(` ${s.issue} (${s.phase})`);
715
+ }
716
+ console.log("");
717
+ console.log("Use: stackmemory conductor logs <ISSUE-ID> -f");
718
+ }
719
+ console.log("\nPress any key to return...");
720
+ await new Promise((resolve) => {
721
+ process.stdin.once("data", () => resolve());
722
+ });
723
+ await render();
724
+ break;
725
+ }
726
+ case "r":
727
+ await render();
728
+ break;
729
+ case "p":
730
+ paused = !paused;
731
+ await render();
732
+ break;
733
+ case "+":
734
+ case "=":
735
+ refreshInterval += 5e3;
736
+ await render();
737
+ scheduleRefresh();
738
+ break;
739
+ case "-":
740
+ case "_":
741
+ if (refreshInterval > 5e3) refreshInterval -= 5e3;
742
+ await render();
743
+ scheduleRefresh();
744
+ break;
745
+ case "q":
746
+ case "":
747
+ if (refreshTimer) clearTimeout(refreshTimer);
748
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
749
+ process.exit(0);
750
+ break;
751
+ }
752
+ });
753
+ await new Promise((resolve) => {
754
+ process.on("SIGINT", () => {
755
+ if (refreshTimer) clearTimeout(refreshTimer);
756
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
757
+ resolve();
758
+ });
759
+ process.on("SIGTERM", () => {
760
+ if (refreshTimer) clearTimeout(refreshTimer);
761
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
762
+ resolve();
763
+ });
764
+ });
765
+ });
370
766
  return cmd;
371
767
  }
372
768
  export {