@stackmemoryai/stackmemory 1.5.5 → 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.
@@ -464,6 +464,305 @@ function createConductorCommands() {
464
464
  });
465
465
  await conductor.start();
466
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
+ });
467
766
  return cmd;
468
767
  }
469
768
  export {
@@ -406,7 +406,8 @@ class Conductor {
406
406
  phase: run.phase,
407
407
  filesModified: run.filesModified,
408
408
  toolCalls: run.toolCalls,
409
- tokensUsed: run.tokensUsed
409
+ tokensUsed: run.tokensUsed,
410
+ workspacePath: run.workspacePath || void 0
410
411
  };
411
412
  writeFileSync(join(dir, "status.json"), JSON.stringify(status, null, 2));
412
413
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "1.5.5",
3
+ "version": "1.5.6",
4
4
  "description": "Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, and automatic hooks.",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",