@stackmemoryai/stackmemory 1.5.5 → 1.5.7

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/bin/gemini-sm ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Gemini-SM CLI Launcher (ESM)
4
+ * Delegates to built CLI in dist without requiring tsx.
5
+ */
6
+ import('../dist/src/cli/gemini-sm.js');
@@ -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 {
@@ -0,0 +1,481 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath as __fileURLToPath } from 'url';
3
+ import { dirname as __pathDirname } from 'path';
4
+ const __filename = __fileURLToPath(import.meta.url);
5
+ const __dirname = __pathDirname(__filename);
6
+ import { config as loadDotenv } from "dotenv";
7
+ loadDotenv({ override: true, debug: false });
8
+ import { spawn, execSync, execFileSync } from "child_process";
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import * as os from "os";
12
+ import { program } from "commander";
13
+ import { v4 as uuidv4 } from "uuid";
14
+ import chalk from "chalk";
15
+ import { initializeTracing, trace } from "../core/trace/index.js";
16
+ const DEFAULT_SM_CONFIG = {
17
+ defaultWorktree: false,
18
+ defaultTracing: true
19
+ };
20
+ function getConfigPath() {
21
+ return path.join(os.homedir(), ".stackmemory", "gemini-sm.json");
22
+ }
23
+ function loadSMConfig() {
24
+ try {
25
+ const configPath = getConfigPath();
26
+ if (fs.existsSync(configPath)) {
27
+ const content = fs.readFileSync(configPath, "utf8");
28
+ return { ...DEFAULT_SM_CONFIG, ...JSON.parse(content) };
29
+ }
30
+ } catch {
31
+ }
32
+ return { ...DEFAULT_SM_CONFIG };
33
+ }
34
+ function saveSMConfig(config) {
35
+ const configPath = getConfigPath();
36
+ const dir = path.dirname(configPath);
37
+ if (!fs.existsSync(dir)) {
38
+ fs.mkdirSync(dir, { recursive: true });
39
+ }
40
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
41
+ }
42
+ class GeminiSM {
43
+ config;
44
+ stackmemoryPath;
45
+ smConfig;
46
+ constructor() {
47
+ this.smConfig = loadSMConfig();
48
+ this.config = {
49
+ instanceId: this.generateInstanceId(),
50
+ useWorktree: this.smConfig.defaultWorktree,
51
+ contextEnabled: true,
52
+ tracingEnabled: this.smConfig.defaultTracing,
53
+ verboseTracing: false,
54
+ sessionStartTime: Date.now()
55
+ };
56
+ this.stackmemoryPath = this.findStackMemory();
57
+ }
58
+ getRepoRoot() {
59
+ try {
60
+ const root = execSync("git rev-parse --show-toplevel", {
61
+ encoding: "utf8"
62
+ }).trim();
63
+ return root || null;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ ensureInitialized() {
69
+ try {
70
+ const root = this.getRepoRoot();
71
+ const dir = root || process.cwd();
72
+ const dbPath = path.join(dir, ".stackmemory", "context.db");
73
+ if (!fs.existsSync(dbPath)) {
74
+ console.log(chalk.blue("Initializing StackMemory for this project..."));
75
+ execSync(`${this.stackmemoryPath} init`, {
76
+ cwd: dir,
77
+ stdio: ["ignore", "ignore", "ignore"]
78
+ });
79
+ }
80
+ } catch {
81
+ }
82
+ }
83
+ generateInstanceId() {
84
+ return uuidv4().substring(0, 8);
85
+ }
86
+ findStackMemory() {
87
+ const possiblePaths = [
88
+ path.join(os.homedir(), ".stackmemory", "bin", "stackmemory"),
89
+ "/usr/local/bin/stackmemory",
90
+ "/opt/homebrew/bin/stackmemory",
91
+ "stackmemory"
92
+ ];
93
+ for (const smPath of possiblePaths) {
94
+ try {
95
+ execFileSync("which", [smPath], { stdio: "ignore" });
96
+ return smPath;
97
+ } catch {
98
+ }
99
+ }
100
+ return "stackmemory";
101
+ }
102
+ isGitRepo() {
103
+ try {
104
+ execSync("git rev-parse --git-dir", { stdio: "ignore" });
105
+ return true;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+ getCurrentBranch() {
111
+ try {
112
+ return execSync("git rev-parse --abbrev-ref HEAD", {
113
+ encoding: "utf8"
114
+ }).trim();
115
+ } catch {
116
+ return "main";
117
+ }
118
+ }
119
+ hasUncommittedChanges() {
120
+ try {
121
+ const status = execSync("git status --porcelain", { encoding: "utf8" });
122
+ return status.length > 0;
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
127
+ resolveGeminiBin() {
128
+ if (this.config.geminiBin && this.config.geminiBin.trim()) {
129
+ return this.config.geminiBin.trim();
130
+ }
131
+ const envBin = process.env["GEMINI_BIN"];
132
+ if (envBin && envBin.trim()) return envBin.trim();
133
+ const possiblePaths = [
134
+ path.join(
135
+ os.homedir(),
136
+ ".nvm",
137
+ "versions",
138
+ "node",
139
+ "v22.22.0",
140
+ "bin",
141
+ "gemini"
142
+ ),
143
+ "/usr/local/bin/gemini",
144
+ "/opt/homebrew/bin/gemini"
145
+ ];
146
+ for (const binPath of possiblePaths) {
147
+ if (fs.existsSync(binPath)) {
148
+ return binPath;
149
+ }
150
+ }
151
+ try {
152
+ execSync("which gemini", { stdio: "ignore" });
153
+ return "gemini";
154
+ } catch {
155
+ }
156
+ return null;
157
+ }
158
+ setupWorktree() {
159
+ if (!this.config.useWorktree || !this.isGitRepo()) {
160
+ return null;
161
+ }
162
+ console.log(chalk.blue("Setting up isolated worktree..."));
163
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
164
+ const branch = this.config.branch || `gemini-${this.config.task || "work"}-${timestamp}-${this.config.instanceId}`;
165
+ const repoName = path.basename(process.cwd());
166
+ const worktreePath = path.join(
167
+ path.dirname(process.cwd()),
168
+ `${repoName}--${branch}`
169
+ );
170
+ try {
171
+ const cmd = `git worktree add -b "${branch}" "${worktreePath}"`;
172
+ execSync(cmd, { stdio: "inherit" });
173
+ console.log(chalk.green(`Worktree created: ${worktreePath}`));
174
+ console.log(chalk.gray(`Branch: ${branch}`));
175
+ const configPath = path.join(worktreePath, ".gemini-instance.json");
176
+ const configData = {
177
+ instanceId: this.config.instanceId,
178
+ worktreePath,
179
+ branch,
180
+ task: this.config.task,
181
+ created: (/* @__PURE__ */ new Date()).toISOString(),
182
+ parentRepo: process.cwd()
183
+ };
184
+ fs.writeFileSync(configPath, JSON.stringify(configData, null, 2));
185
+ const envFiles = [".env", ".env.local", ".mise.toml", ".tool-versions"];
186
+ for (const file of envFiles) {
187
+ const srcPath = path.join(process.cwd(), file);
188
+ if (fs.existsSync(srcPath)) {
189
+ fs.copyFileSync(srcPath, path.join(worktreePath, file));
190
+ }
191
+ }
192
+ return worktreePath;
193
+ } catch (err) {
194
+ console.error(chalk.red("Failed to create worktree:"), err);
195
+ return null;
196
+ }
197
+ }
198
+ saveContext(message, metadata = {}) {
199
+ if (!this.config.contextEnabled) return;
200
+ try {
201
+ const contextData = {
202
+ message,
203
+ metadata: {
204
+ ...metadata,
205
+ instanceId: this.config.instanceId,
206
+ worktree: this.config.worktreePath,
207
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
208
+ tool: "gemini"
209
+ }
210
+ };
211
+ const cmd = `${this.stackmemoryPath} context save --json '${JSON.stringify(contextData)}'`;
212
+ execSync(cmd, { stdio: "ignore" });
213
+ } catch {
214
+ }
215
+ }
216
+ loadContext() {
217
+ if (!this.config.contextEnabled) return;
218
+ try {
219
+ console.log(chalk.blue("Loading previous context..."));
220
+ const cmd = `${this.stackmemoryPath} context show`;
221
+ const output = execSync(cmd, {
222
+ encoding: "utf8",
223
+ stdio: ["pipe", "pipe", "pipe"]
224
+ });
225
+ const lines = output.trim().split("\n").filter((l) => l.trim());
226
+ if (lines.length > 3) {
227
+ console.log(chalk.gray("Context stack loaded"));
228
+ }
229
+ } catch {
230
+ }
231
+ }
232
+ detectMultipleInstances() {
233
+ try {
234
+ const lockDir = path.join(process.cwd(), ".gemini-worktree-locks");
235
+ if (!fs.existsSync(lockDir)) return false;
236
+ const locks = fs.readdirSync(lockDir).filter((f) => f.endsWith(".lock"));
237
+ const activeLocks = locks.filter((lockFile) => {
238
+ const lockPath = path.join(lockDir, lockFile);
239
+ const lockData = JSON.parse(fs.readFileSync(lockPath, "utf8"));
240
+ const lockAge = Date.now() - new Date(lockData.created).getTime();
241
+ return lockAge < 24 * 60 * 60 * 1e3;
242
+ });
243
+ return activeLocks.length > 0;
244
+ } catch {
245
+ return false;
246
+ }
247
+ }
248
+ suggestWorktreeMode() {
249
+ if (this.hasUncommittedChanges()) {
250
+ console.log(chalk.yellow("Uncommitted changes detected"));
251
+ console.log(chalk.gray("Consider using --worktree to work in isolation"));
252
+ }
253
+ if (this.detectMultipleInstances()) {
254
+ console.log(chalk.yellow("Other Gemini instances detected"));
255
+ console.log(
256
+ chalk.gray("Using --worktree is recommended to avoid conflicts")
257
+ );
258
+ }
259
+ }
260
+ async run(args) {
261
+ const geminiArgs = [];
262
+ let i = 0;
263
+ while (i < args.length) {
264
+ const arg = args[i];
265
+ switch (arg) {
266
+ case "--worktree":
267
+ case "-w":
268
+ this.config.useWorktree = true;
269
+ break;
270
+ case "--no-worktree":
271
+ case "-W":
272
+ this.config.useWorktree = false;
273
+ break;
274
+ case "--no-context":
275
+ this.config.contextEnabled = false;
276
+ break;
277
+ case "--no-trace":
278
+ this.config.tracingEnabled = false;
279
+ break;
280
+ case "--verbose-trace":
281
+ this.config.verboseTracing = true;
282
+ break;
283
+ case "--branch":
284
+ case "-b":
285
+ i++;
286
+ this.config.branch = args[i];
287
+ break;
288
+ case "--task":
289
+ case "-t":
290
+ i++;
291
+ this.config.task = args[i];
292
+ break;
293
+ case "--gemini-bin":
294
+ i++;
295
+ this.config.geminiBin = args[i];
296
+ process.env["GEMINI_BIN"] = this.config.geminiBin;
297
+ break;
298
+ case "--auto":
299
+ case "-a":
300
+ if (this.isGitRepo()) {
301
+ this.config.useWorktree = this.hasUncommittedChanges() || this.detectMultipleInstances();
302
+ }
303
+ break;
304
+ default:
305
+ geminiArgs.push(arg);
306
+ }
307
+ i++;
308
+ }
309
+ if (this.config.tracingEnabled) {
310
+ process.env["DEBUG_TRACE"] = "true";
311
+ process.env["STACKMEMORY_DEBUG"] = "true";
312
+ process.env["TRACE_OUTPUT"] = "file";
313
+ process.env["TRACE_MASK_SENSITIVE"] = "true";
314
+ if (this.config.verboseTracing) {
315
+ process.env["TRACE_VERBOSITY"] = "full";
316
+ process.env["TRACE_PARAMS"] = "true";
317
+ process.env["TRACE_RESULTS"] = "true";
318
+ process.env["TRACE_MEMORY"] = "true";
319
+ } else {
320
+ process.env["TRACE_VERBOSITY"] = "summary";
321
+ process.env["TRACE_PARAMS"] = "true";
322
+ process.env["TRACE_RESULTS"] = "false";
323
+ }
324
+ initializeTracing();
325
+ trace.command(
326
+ "gemini-sm",
327
+ {
328
+ instanceId: this.config.instanceId,
329
+ worktree: this.config.useWorktree,
330
+ task: this.config.task
331
+ },
332
+ async () => {
333
+ }
334
+ );
335
+ }
336
+ console.log(chalk.cyan("Gemini + StackMemory"));
337
+ console.log();
338
+ this.ensureInitialized();
339
+ if (this.isGitRepo()) {
340
+ const branch = this.getCurrentBranch();
341
+ console.log(chalk.gray(`Branch: ${branch}`));
342
+ if (!this.config.useWorktree) {
343
+ this.suggestWorktreeMode();
344
+ }
345
+ }
346
+ if (this.config.useWorktree) {
347
+ const worktreePath = this.setupWorktree();
348
+ if (worktreePath) {
349
+ this.config.worktreePath = worktreePath;
350
+ process.chdir(worktreePath);
351
+ this.saveContext("Created worktree for Gemini instance", {
352
+ action: "worktree_created",
353
+ path: worktreePath,
354
+ branch: this.config.branch
355
+ });
356
+ }
357
+ }
358
+ this.loadContext();
359
+ process.env["GEMINI_INSTANCE_ID"] = this.config.instanceId;
360
+ if (this.config.worktreePath) {
361
+ process.env["GEMINI_WORKTREE_PATH"] = this.config.worktreePath;
362
+ }
363
+ console.log(chalk.gray(`Instance: ${this.config.instanceId}`));
364
+ console.log(chalk.gray(`Working in: ${process.cwd()}`));
365
+ if (this.config.tracingEnabled) {
366
+ console.log(
367
+ chalk.gray(`Tracing enabled (logs to ~/.stackmemory/traces/)`)
368
+ );
369
+ }
370
+ console.log();
371
+ console.log(chalk.gray("Starting Gemini CLI..."));
372
+ console.log(chalk.gray("-".repeat(40)));
373
+ const geminiBin = this.resolveGeminiBin();
374
+ if (!geminiBin) {
375
+ console.error(chalk.red("Gemini CLI not found."));
376
+ console.log(
377
+ chalk.gray(
378
+ "Install Gemini CLI or set an override:\n npm install -g @anthropic-ai/gemini-cli\n export GEMINI_BIN=/path/to/gemini\n gemini-sm --help"
379
+ )
380
+ );
381
+ process.exit(1);
382
+ return;
383
+ }
384
+ const gemini = spawn(geminiBin, geminiArgs, {
385
+ stdio: "inherit",
386
+ env: process.env
387
+ });
388
+ gemini.on("error", (err) => {
389
+ console.error(chalk.red("Failed to launch Gemini CLI."));
390
+ if (err.code === "ENOENT") {
391
+ console.error(
392
+ chalk.gray("Not found. Set GEMINI_BIN or install gemini.")
393
+ );
394
+ } else {
395
+ console.error(chalk.gray(`${err.message}`));
396
+ }
397
+ process.exit(1);
398
+ });
399
+ gemini.on("exit", async (code) => {
400
+ this.saveContext("Gemini session ended", {
401
+ action: "session_end",
402
+ exitCode: code
403
+ });
404
+ if (this.config.tracingEnabled) {
405
+ const summary = trace.getExecutionSummary();
406
+ console.log();
407
+ console.log(chalk.gray("-".repeat(40)));
408
+ console.log(chalk.cyan("Trace Summary:"));
409
+ console.log(chalk.gray(summary));
410
+ }
411
+ if (this.config.worktreePath) {
412
+ console.log();
413
+ console.log(chalk.gray("-".repeat(40)));
414
+ console.log(chalk.cyan("Session ended in worktree:"));
415
+ console.log(chalk.gray(` ${this.config.worktreePath}`));
416
+ }
417
+ process.exit(code || 0);
418
+ });
419
+ process.on("SIGINT", () => {
420
+ this.saveContext("Gemini session interrupted", {
421
+ action: "session_interrupt"
422
+ });
423
+ gemini.kill("SIGINT");
424
+ });
425
+ process.on("SIGTERM", () => {
426
+ this.saveContext("Gemini session terminated", {
427
+ action: "session_terminate"
428
+ });
429
+ gemini.kill("SIGTERM");
430
+ });
431
+ }
432
+ }
433
+ program.name("gemini-sm").description("Gemini CLI with StackMemory context and worktree isolation").version("1.0.0");
434
+ const configCmd = program.command("config").description("Manage gemini-sm defaults");
435
+ configCmd.command("show").description("Show current default settings").action(() => {
436
+ const config = loadSMConfig();
437
+ console.log(chalk.cyan("gemini-sm defaults:"));
438
+ console.log(
439
+ ` defaultWorktree: ${config.defaultWorktree ? chalk.green("true") : chalk.gray("false")}`
440
+ );
441
+ console.log(
442
+ ` defaultTracing: ${config.defaultTracing ? chalk.green("true") : chalk.gray("false")}`
443
+ );
444
+ console.log(chalk.gray(`
445
+ Config: ${getConfigPath()}`));
446
+ });
447
+ configCmd.command("set <key> <value>").description("Set a default (e.g., set worktree true)").action((key, value) => {
448
+ const config = loadSMConfig();
449
+ const boolValue = value === "true" || value === "1" || value === "on";
450
+ const keyMap = {
451
+ worktree: "defaultWorktree",
452
+ tracing: "defaultTracing"
453
+ };
454
+ const configKey = keyMap[key];
455
+ if (!configKey) {
456
+ console.log(chalk.red(`Unknown key: ${key}`));
457
+ console.log(chalk.gray("Valid keys: worktree, tracing"));
458
+ process.exit(1);
459
+ }
460
+ config[configKey] = boolValue;
461
+ saveSMConfig(config);
462
+ console.log(chalk.green(`Set ${key} = ${boolValue}`));
463
+ });
464
+ configCmd.command("worktree-on").description("Enable worktree mode by default").action(() => {
465
+ const config = loadSMConfig();
466
+ config.defaultWorktree = true;
467
+ saveSMConfig(config);
468
+ console.log(chalk.green("Worktree mode enabled by default"));
469
+ });
470
+ configCmd.command("worktree-off").description("Disable worktree mode by default").action(() => {
471
+ const config = loadSMConfig();
472
+ config.defaultWorktree = false;
473
+ saveSMConfig(config);
474
+ console.log(chalk.green("Worktree mode disabled by default"));
475
+ });
476
+ program.option("-w, --worktree", "Create isolated worktree for this instance").option("-W, --no-worktree", "Disable worktree (override default)").option("-a, --auto", "Automatically detect and apply best settings").option("-b, --branch <name>", "Specify branch name for worktree").option("-t, --task <desc>", "Task description for context").option("--gemini-bin <path>", "Path to gemini CLI (or use GEMINI_BIN)").option("--no-context", "Disable StackMemory context integration").option("--no-trace", "Disable debug tracing (enabled by default)").option("--verbose-trace", "Enable verbose debug tracing with full details").helpOption("-h, --help", "Display help").allowUnknownOption(true).action(async (_options) => {
477
+ const geminiSM = new GeminiSM();
478
+ const args = process.argv.slice(2);
479
+ await geminiSM.run(args);
480
+ });
481
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
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",
@@ -14,7 +14,8 @@
14
14
  "codex-smd": "bin/codex-smd",
15
15
  "claude-sm": "bin/claude-sm",
16
16
  "claude-smd": "bin/claude-smd",
17
- "opencode-sm": "bin/opencode-sm"
17
+ "opencode-sm": "bin/opencode-sm",
18
+ "gemini-sm": "bin/gemini-sm"
18
19
  },
19
20
  "files": [
20
21
  "bin",