@stackmemoryai/stackmemory 1.12.0 → 1.14.0

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.
Files changed (162) hide show
  1. package/LICENSE +131 -64
  2. package/README.md +3 -1
  3. package/bin/claude-sm +16 -1
  4. package/bin/claude-smd +16 -1
  5. package/bin/codex-smd +16 -1
  6. package/bin/gemini-sm +16 -1
  7. package/bin/hermes-sm +21 -0
  8. package/bin/hermes-smd +21 -0
  9. package/bin/opencode-sm +16 -1
  10. package/dist/src/cli/codex-sm.js +51 -11
  11. package/dist/src/cli/commands/brain.js +206 -0
  12. package/dist/src/cli/commands/company-os.js +184 -0
  13. package/dist/src/cli/commands/context.js +5 -0
  14. package/dist/src/cli/commands/operator.js +127 -0
  15. package/dist/src/cli/commands/orchestrate.js +2 -0
  16. package/dist/src/cli/commands/orchestrator.js +3 -2
  17. package/dist/src/cli/commands/patterns.js +254 -0
  18. package/dist/src/cli/commands/portal.js +161 -0
  19. package/dist/src/cli/commands/scaffold.js +92 -0
  20. package/dist/src/cli/commands/setup.js +1 -4
  21. package/dist/src/cli/commands/sync.js +253 -0
  22. package/dist/src/cli/commands/tasks.js +130 -1
  23. package/dist/src/cli/commands/vision.js +221 -0
  24. package/dist/src/cli/hermes-sm.js +224 -0
  25. package/dist/src/cli/index.js +15 -10
  26. package/dist/src/cli/utils/real-cli-bin.js +72 -0
  27. package/dist/src/core/brain/brain-store.js +187 -0
  28. package/dist/src/core/brain/brain-sync.js +193 -0
  29. package/dist/src/core/brain/index.js +78 -0
  30. package/dist/src/core/brain/types.js +10 -0
  31. package/dist/src/core/cache/token-estimator.js +24 -1
  32. package/dist/src/core/config/feature-flags.js +2 -6
  33. package/dist/src/core/context/frame-database.js +44 -0
  34. package/dist/src/core/context/recursive-context-manager.js +1 -1
  35. package/dist/src/core/context/rehydration.js +2 -1
  36. package/dist/src/core/database/sqlite-adapter.js +14 -1
  37. package/dist/src/core/models/model-router.js +33 -1
  38. package/dist/src/core/models/provider-pricing.js +58 -4
  39. package/dist/src/core/patterns/index.js +22 -0
  40. package/dist/src/core/patterns/pattern-applier.js +39 -0
  41. package/dist/src/core/patterns/pattern-observer.js +157 -0
  42. package/dist/src/core/patterns/pattern-store.js +259 -0
  43. package/dist/src/core/patterns/types.js +19 -0
  44. package/dist/src/core/retrieval/llm-context-retrieval.js +5 -4
  45. package/dist/src/core/retrieval/unified-context-assembler.js +11 -66
  46. package/dist/src/core/skill-packs/types.js +14 -1
  47. package/dist/src/core/storage/cloud-sync-manager.js +116 -0
  48. package/dist/src/core/storage/cloud-sync.js +574 -0
  49. package/dist/src/core/storage/two-tier-storage.js +5 -1
  50. package/dist/src/core/tasks/master-tasks-template.js +43 -0
  51. package/dist/src/core/tasks/md-task-parser.js +138 -0
  52. package/dist/src/core/vision/index.js +27 -0
  53. package/dist/src/core/vision/signals.js +79 -0
  54. package/dist/src/core/vision/types.js +22 -0
  55. package/dist/src/core/vision/vision-file.js +146 -0
  56. package/dist/src/core/vision/vision-loop.js +220 -0
  57. package/dist/src/core/wiki/wiki-compiler.js +103 -1
  58. package/dist/src/daemon/daemon-config.js +45 -0
  59. package/dist/src/daemon/services/desire-path-service.js +566 -0
  60. package/dist/src/daemon/services/research-stream-service.js +320 -0
  61. package/dist/src/daemon/services/telemetry-service.js +192 -0
  62. package/dist/src/daemon/unified-daemon.js +28 -1
  63. package/dist/src/features/browser/cli-browser-agent.js +417 -0
  64. package/dist/src/features/browser/stagehand-workflows.js +578 -0
  65. package/dist/src/features/operator/adapter-factory.js +62 -0
  66. package/dist/src/features/operator/browser-adapter.js +109 -0
  67. package/dist/src/features/operator/desktop-adapter.js +125 -0
  68. package/dist/src/features/operator/index.js +39 -0
  69. package/dist/src/features/operator/llm-decision.js +137 -0
  70. package/dist/src/features/operator/operator-logger.js +92 -0
  71. package/dist/src/features/operator/overnight-runner.js +327 -0
  72. package/dist/src/features/operator/screen-adapter.js +91 -0
  73. package/dist/src/features/operator/session-manager.js +127 -0
  74. package/dist/src/features/operator/state-machine.js +227 -0
  75. package/dist/src/features/operator/task-queue.js +81 -0
  76. package/dist/src/features/portal/index.js +26 -0
  77. package/dist/src/features/portal/server.js +240 -0
  78. package/dist/src/features/portal/types.js +14 -0
  79. package/dist/src/features/portal/ui.js +195 -0
  80. package/dist/src/features/tasks/task-aware-context.js +2 -1
  81. package/dist/src/features/tui/simple-monitor.js +0 -23
  82. package/dist/src/features/tui/swarm-monitor.js +8 -66
  83. package/dist/src/{integrations/diffmem/index.js → features/web/client/hooks/use-socket.js} +6 -5
  84. package/dist/src/features/web/client/lib/utils.js +12 -0
  85. package/dist/src/features/web/client/stores/session-store.js +12 -0
  86. package/dist/src/features/web/server/gcp-billing.js +76 -0
  87. package/dist/src/features/web/server/index.js +10 -0
  88. package/dist/src/features/web/server/spend-calculator.js +228 -0
  89. package/dist/src/hooks/schemas.js +4 -1
  90. package/dist/src/integrations/anthropic/client.js +3 -2
  91. package/dist/src/integrations/claude-code/agent-bridge.js +0 -3
  92. package/dist/src/integrations/claude-code/subagent-client.js +218 -11
  93. package/dist/src/integrations/claude-code/task-coordinator.js +2 -1
  94. package/dist/src/integrations/linear/webhook-retry.js +196 -0
  95. package/dist/src/integrations/linear/webhook-server.js +18 -22
  96. package/dist/src/integrations/mcp/handlers/cloud-sync-handlers.js +101 -0
  97. package/dist/src/integrations/mcp/handlers/index.js +27 -52
  98. package/dist/src/integrations/mcp/server.js +122 -335
  99. package/dist/src/integrations/mcp/tool-alias-registry.js +0 -73
  100. package/dist/src/integrations/mcp/tool-definitions.js +111 -510
  101. package/dist/src/mcp/stackmemory-mcp-server.js +404 -379
  102. package/dist/src/orchestrators/multimodal/determinism.js +2 -1
  103. package/dist/src/orchestrators/multimodal/harness.js +2 -1
  104. package/dist/src/skills/recursive-agent-orchestrator.js +2 -4
  105. package/dist/src/utils/process-cleanup.js +1 -7
  106. package/docs/README.md +42 -0
  107. package/docs/guides/README_INSTALL.md +208 -0
  108. package/package.json +18 -9
  109. package/scripts/claude-code-wrapper.sh +11 -0
  110. package/scripts/claude-sm-setup.sh +12 -1
  111. package/scripts/codex-wrapper.sh +11 -0
  112. package/scripts/git-hooks/branch-context-manager.sh +11 -0
  113. package/scripts/git-hooks/post-checkout-stackmemory.sh +11 -0
  114. package/scripts/git-hooks/post-commit-stackmemory.sh +11 -0
  115. package/scripts/git-hooks/pre-commit-stackmemory.sh +11 -0
  116. package/scripts/hooks/cleanup-shell.sh +12 -1
  117. package/scripts/hooks/task-complete.sh +12 -1
  118. package/scripts/install-code-execution-hooks.sh +12 -1
  119. package/scripts/install-sweep-hook.sh +12 -0
  120. package/scripts/install.sh +11 -0
  121. package/scripts/opencode-wrapper.sh +11 -0
  122. package/scripts/portal/cloud-init.yaml +69 -0
  123. package/scripts/portal/setup.sh +69 -0
  124. package/scripts/portal/stackmemory-portal.service +34 -0
  125. package/scripts/setup-claude-integration.sh +12 -1
  126. package/scripts/smoke-init-db.sh +23 -0
  127. package/scripts/stackmemory-daemon.sh +11 -0
  128. package/scripts/verify-dist.cjs +11 -4
  129. package/dist/src/cli/commands/ralph.js +0 -1053
  130. package/dist/src/hooks/diffmem-hooks.js +0 -376
  131. package/dist/src/integrations/diffmem/client.js +0 -208
  132. package/dist/src/integrations/diffmem/config.js +0 -14
  133. package/dist/src/integrations/greptile/client.js +0 -101
  134. package/dist/src/integrations/greptile/config.js +0 -14
  135. package/dist/src/integrations/greptile/index.js +0 -11
  136. package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +0 -188
  137. package/dist/src/integrations/mcp/handlers/diffmem-handlers.js +0 -455
  138. package/dist/src/integrations/mcp/handlers/greptile-handlers.js +0 -456
  139. package/dist/src/integrations/mcp/handlers/provider-handlers.js +0 -227
  140. package/dist/src/integrations/ralph/bridge/ralph-stackmemory-bridge.js +0 -863
  141. package/dist/src/integrations/ralph/context/context-budget-manager.js +0 -308
  142. package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +0 -354
  143. package/dist/src/integrations/ralph/index.js +0 -17
  144. package/dist/src/integrations/ralph/learning/pattern-learner.js +0 -416
  145. package/dist/src/integrations/ralph/lifecycle/iteration-lifecycle.js +0 -448
  146. package/dist/src/integrations/ralph/loopmax.js +0 -488
  147. package/dist/src/integrations/ralph/monitoring/swarm-dashboard.js +0 -293
  148. package/dist/src/integrations/ralph/monitoring/swarm-registry.js +0 -107
  149. package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +0 -508
  150. package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +0 -407
  151. package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +0 -495
  152. package/dist/src/integrations/ralph/patterns/oracle-worker-pattern.js +0 -387
  153. package/dist/src/integrations/ralph/performance/performance-optimizer.js +0 -357
  154. package/dist/src/integrations/ralph/recovery/crash-recovery.js +0 -461
  155. package/dist/src/integrations/ralph/state/state-reconciler.js +0 -420
  156. package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +0 -444
  157. package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -1005
  158. package/dist/src/integrations/ralph/visualization/ralph-debugger.js +0 -635
  159. package/scripts/ralph-loop-implementation.js +0 -404
  160. /package/dist/src/{integrations/diffmem/types.js → core/storage/cloud-sync-types.js} +0 -0
  161. /package/dist/src/{integrations/greptile → features/operator}/types.js +0 -0
  162. /package/dist/src/{integrations/ralph/types.js → features/web/client/next-env.d.js} +0 -0
@@ -0,0 +1,327 @@
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 { existsSync, unlinkSync } from "fs";
6
+ import { execSync } from "child_process";
7
+ import { join } from "path";
8
+ import { homedir } from "os";
9
+ import {
10
+ detectState,
11
+ decideAction,
12
+ detectCompletion
13
+ } from "./state-machine.js";
14
+ import { TaskQueue } from "./task-queue.js";
15
+ import { SessionManager } from "./session-manager.js";
16
+ import { OperatorLogger } from "./operator-logger.js";
17
+ import {
18
+ classifyScreenState,
19
+ generateNudge
20
+ } from "./llm-decision.js";
21
+ const STOP_SIGNAL_DIR = join(homedir(), ".stackmemory", "operator");
22
+ const STOP_SIGNAL_FILE = join(STOP_SIGNAL_DIR, "stop-signal");
23
+ class OvernightRunner {
24
+ queue;
25
+ session;
26
+ logger;
27
+ config;
28
+ adapter;
29
+ llmConfig;
30
+ checkpoint;
31
+ lastScreen = "";
32
+ lastChangeAt = Date.now();
33
+ stopped = false;
34
+ constructor(config) {
35
+ this.config = config;
36
+ this.queue = new TaskQueue(config.taskFilePath);
37
+ this.session = new SessionManager({
38
+ sessionName: config.sessionName,
39
+ cwd: config.cwd,
40
+ model: config.model
41
+ });
42
+ this.logger = new OperatorLogger(config.logDir, STOP_SIGNAL_DIR);
43
+ this.llmConfig = config.anthropicApiKey ? { apiKey: config.anthropicApiKey, model: config.llmModel } : void 0;
44
+ this.checkpoint = this.freshCheckpoint();
45
+ }
46
+ /** Main entry point — runs until queue drained or stopped */
47
+ async run() {
48
+ this.session.preflight();
49
+ if (this.queue.isEmpty()) {
50
+ process.stderr.write(
51
+ "[operator] No actionable tasks in queue. Nothing to do.\n"
52
+ );
53
+ return;
54
+ }
55
+ const onSignal = () => {
56
+ this.stopped = true;
57
+ };
58
+ process.on("SIGINT", onSignal);
59
+ process.on("SIGTERM", onSignal);
60
+ this.logger.logEvent("start", {
61
+ taskFile: this.config.taskFilePath,
62
+ cwd: this.config.cwd,
63
+ session: this.config.sessionName,
64
+ tasksRemaining: this.queue.remaining()
65
+ });
66
+ try {
67
+ this.adapter = this.session.start();
68
+ this.lastChangeAt = Date.now();
69
+ while (!this.stopped) {
70
+ await this.tick();
71
+ if (existsSync(STOP_SIGNAL_FILE)) {
72
+ this.logger.logEvent("shutdown", { reason: "stop signal" });
73
+ try {
74
+ unlinkSync(STOP_SIGNAL_FILE);
75
+ } catch {
76
+ }
77
+ this.stopped = true;
78
+ break;
79
+ }
80
+ if (!this.checkpoint.currentTaskId && this.queue.isEmpty()) {
81
+ this.logger.logEvent("queue_drained");
82
+ break;
83
+ }
84
+ await this.sleep(this.config.pollIntervalMs);
85
+ }
86
+ } finally {
87
+ this.session.stop();
88
+ process.off("SIGINT", onSignal);
89
+ process.off("SIGTERM", onSignal);
90
+ this.logger.printSummary({
91
+ totalRuntimeMs: Date.now() - this.checkpoint.startedAt,
92
+ tasksCompleted: this.checkpoint.tasksCompleted.length,
93
+ tasksBlocked: this.checkpoint.tasksBlocked.length,
94
+ tasksRemaining: this.queue.remaining(),
95
+ totalRestarts: this.checkpoint.totalRestarts,
96
+ totalPermissionApprovals: this.checkpoint.totalPermissionApprovals,
97
+ totalRateLimitHits: this.checkpoint.totalRateLimitHits
98
+ });
99
+ }
100
+ }
101
+ // ── Tick ─────────────────────────────────────────────
102
+ async tick() {
103
+ if (!this.adapter) return;
104
+ const screen = this.adapter.readScreen();
105
+ if (screen !== this.lastScreen) {
106
+ this.lastChangeAt = Date.now();
107
+ this.lastScreen = screen;
108
+ }
109
+ const detection = detectState(
110
+ screen,
111
+ this.checkpoint.currentState,
112
+ this.lastChangeAt,
113
+ this.config.stuckTimeoutMs
114
+ );
115
+ if (detection.state === "IDLE" && this.checkpoint.currentTaskId) {
116
+ const completion = detectCompletion(screen);
117
+ if (completion.completed) {
118
+ this.handleTaskComplete(this.checkpoint.currentTaskId);
119
+ return;
120
+ }
121
+ if (completion.blocked) {
122
+ this.handleTaskBlocked(
123
+ this.checkpoint.currentTaskId,
124
+ completion.blockedReason ?? "unknown"
125
+ );
126
+ return;
127
+ }
128
+ }
129
+ if (detection.state === "UNKNOWN" && this.llmConfig) {
130
+ const screenshot = this.adapter.readScreenshot?.();
131
+ const llmResult = await classifyScreenState(
132
+ screenshot ?? screen,
133
+ this.llmConfig
134
+ );
135
+ if (llmResult.state !== "UNKNOWN") {
136
+ detection.state = llmResult.state;
137
+ detection.confidence = llmResult.confidence;
138
+ detection.detail = `llm: ${llmResult.detail}`;
139
+ }
140
+ }
141
+ if (detection.state === "STUCK" && this.llmConfig && this.checkpoint.currentTaskId) {
142
+ const taskContent = this.queue.dequeue();
143
+ const nudge = await generateNudge(
144
+ screen,
145
+ taskContent?.task ?? this.checkpoint.currentTaskId,
146
+ this.llmConfig
147
+ );
148
+ if (nudge) {
149
+ this.adapter.sendInput(nudge);
150
+ this.lastChangeAt = Date.now();
151
+ this.logger.logEvent("stuck_detected", { action: "nudge", nudge });
152
+ return;
153
+ }
154
+ }
155
+ const nextTask = this.checkpoint.currentTaskId ? void 0 : this.queue.dequeue();
156
+ const action = decideAction(detection, this.checkpoint, nextTask);
157
+ this.logger.logTick({
158
+ state: detection.state,
159
+ confidence: detection.confidence,
160
+ action: action.type,
161
+ currentTask: this.checkpoint.currentTaskId ?? void 0,
162
+ screenSnippet: screen.slice(-200)
163
+ });
164
+ this.executeAction(action);
165
+ this.checkpoint.currentState = detection.state;
166
+ this.checkpoint.lastTickAt = Date.now();
167
+ this.logger.writeCheckpoint(this.checkpoint);
168
+ }
169
+ // ── Action Execution ────────────────────────────────
170
+ executeAction(action) {
171
+ if (!this.adapter) return;
172
+ switch (action.type) {
173
+ case "INJECT_TASK":
174
+ this.injectTask(action.task);
175
+ break;
176
+ case "AUTO_APPROVE":
177
+ this.adapter.sendKey("y");
178
+ this.adapter.sendKey("Enter");
179
+ this.checkpoint.totalPermissionApprovals++;
180
+ this.logger.logEvent("permission_approved");
181
+ break;
182
+ case "NUDGE":
183
+ this.adapter.sendInput(action.message);
184
+ this.lastChangeAt = Date.now();
185
+ this.checkpoint.nudgeCount++;
186
+ this.logger.logEvent("stuck_detected", {
187
+ action: "nudge",
188
+ nudgeCount: this.checkpoint.nudgeCount,
189
+ message: action.message
190
+ });
191
+ break;
192
+ case "BACKOFF":
193
+ this.checkpoint.totalRateLimitHits++;
194
+ this.checkpoint.consecutiveRateLimitHits++;
195
+ this.logger.logEvent("rate_limit_backoff", {
196
+ durationMs: action.durationMs
197
+ });
198
+ this.sleepSync(action.durationMs);
199
+ break;
200
+ case "RESTART_SESSION":
201
+ this.checkpoint.totalRestarts++;
202
+ this.checkpoint.consecutiveRestarts++;
203
+ this.logger.logEvent("session_restarted", {
204
+ consecutive: this.checkpoint.consecutiveRestarts
205
+ });
206
+ this.adapter = this.session.restart();
207
+ this.lastChangeAt = Date.now();
208
+ this.lastScreen = "";
209
+ break;
210
+ case "KILL_AND_RESTART":
211
+ this.adapter.sendKey("C-c");
212
+ this.sleepSync(2e3);
213
+ this.checkpoint.totalRestarts++;
214
+ this.logger.logEvent("stuck_detected");
215
+ this.adapter = this.session.restart();
216
+ this.lastChangeAt = Date.now();
217
+ this.lastScreen = "";
218
+ break;
219
+ case "MARK_COMPLETE":
220
+ this.handleTaskComplete(action.taskId);
221
+ break;
222
+ case "MARK_BLOCKED":
223
+ this.handleTaskBlocked(action.taskId, action.reason);
224
+ break;
225
+ case "LOG_ERROR":
226
+ this.logger.logEvent("error", { error: action.error });
227
+ if (action.error.includes("max consecutive restarts")) {
228
+ this.stopped = true;
229
+ }
230
+ break;
231
+ case "WAIT":
232
+ this.sleepSync(action.durationMs);
233
+ break;
234
+ case "NOOP":
235
+ if (this.checkpoint.currentState === "WORKING") {
236
+ this.checkpoint.consecutiveRateLimitHits = 0;
237
+ this.checkpoint.consecutiveRestarts = 0;
238
+ }
239
+ break;
240
+ }
241
+ }
242
+ // ── Task Lifecycle ──────────────────────────────────
243
+ injectTask(task) {
244
+ if (!this.adapter) return;
245
+ this.adapter.clearHistory();
246
+ const prompt = this.queue.buildTaskPrompt(task);
247
+ this.adapter.sendInput(prompt);
248
+ this.queue.markActive(task.id);
249
+ this.checkpoint.currentTaskId = task.id;
250
+ this.checkpoint.nudgeCount = 0;
251
+ this.lastChangeAt = Date.now();
252
+ this.logger.logEvent("task_injected", {
253
+ taskId: task.id,
254
+ priority: task.priority,
255
+ task: task.task
256
+ });
257
+ }
258
+ handleTaskComplete(taskId) {
259
+ this.queue.markDone(taskId);
260
+ this.checkpoint.tasksCompleted.push(taskId);
261
+ this.checkpoint.currentTaskId = null;
262
+ this.checkpoint.consecutiveRestarts = 0;
263
+ this.logger.logEvent("task_completed", { taskId });
264
+ if (this.config.autoCommit) {
265
+ this.autoCommit(taskId);
266
+ }
267
+ }
268
+ handleTaskBlocked(taskId, reason) {
269
+ this.queue.markBlocked(taskId, reason);
270
+ this.checkpoint.tasksBlocked.push({ id: taskId, reason });
271
+ this.checkpoint.currentTaskId = null;
272
+ this.logger.logEvent("task_blocked", { taskId, reason });
273
+ }
274
+ autoCommit(taskId) {
275
+ try {
276
+ const status = execSync("git status --porcelain", {
277
+ cwd: this.config.cwd,
278
+ encoding: "utf-8",
279
+ timeout: 1e4
280
+ }).trim();
281
+ if (!status) return;
282
+ execSync("git add -A", {
283
+ cwd: this.config.cwd,
284
+ stdio: "ignore",
285
+ timeout: 1e4
286
+ });
287
+ execSync(`git commit -m "operator: complete ${taskId}"`, {
288
+ cwd: this.config.cwd,
289
+ stdio: "ignore",
290
+ timeout: 3e4
291
+ });
292
+ } catch {
293
+ }
294
+ }
295
+ // ── Helpers ─────────────────────────────────────────
296
+ freshCheckpoint() {
297
+ return {
298
+ startedAt: Date.now(),
299
+ lastTickAt: Date.now(),
300
+ currentState: "UNKNOWN",
301
+ currentTaskId: null,
302
+ tasksCompleted: [],
303
+ tasksBlocked: [],
304
+ totalRestarts: 0,
305
+ consecutiveRestarts: 0,
306
+ totalPermissionApprovals: 0,
307
+ totalRateLimitHits: 0,
308
+ consecutiveRateLimitHits: 0,
309
+ nudgeCount: 0
310
+ };
311
+ }
312
+ sleep(ms) {
313
+ return new Promise((resolve) => {
314
+ const timer = setTimeout(resolve, ms);
315
+ timer.unref();
316
+ });
317
+ }
318
+ sleepSync(ms) {
319
+ try {
320
+ execSync(`sleep ${ms / 1e3}`, { stdio: "ignore" });
321
+ } catch {
322
+ }
323
+ }
324
+ }
325
+ export {
326
+ OvernightRunner
327
+ };
@@ -0,0 +1,91 @@
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 { execSync } from "child_process";
6
+ function shellEscape(str) {
7
+ return "'" + str.replace(/'/g, "'\\''") + "'";
8
+ }
9
+ class TmuxAdapter {
10
+ constructor(session, pane = "0") {
11
+ this.session = session;
12
+ this.pane = pane;
13
+ }
14
+ adapterType = "tmux";
15
+ readScreen() {
16
+ try {
17
+ return execSync(
18
+ `tmux capture-pane -t ${this.session}:${this.pane} -p -S -200`,
19
+ { encoding: "utf-8", timeout: 5e3 }
20
+ ).trimEnd();
21
+ } catch {
22
+ return "";
23
+ }
24
+ }
25
+ sendInput(text, opts) {
26
+ try {
27
+ if (opts?.raw) {
28
+ execSync(
29
+ `tmux send-keys -t ${this.session}:${this.pane} -l ${shellEscape(text)}`,
30
+ { stdio: "ignore", timeout: 5e3 }
31
+ );
32
+ } else {
33
+ execSync(
34
+ `tmux send-keys -t ${this.session}:${this.pane} ${shellEscape(text)} Enter`,
35
+ { stdio: "ignore", timeout: 5e3 }
36
+ );
37
+ }
38
+ } catch {
39
+ }
40
+ }
41
+ sendKey(key) {
42
+ try {
43
+ execSync(`tmux send-keys -t ${this.session}:${this.pane} ${key}`, {
44
+ stdio: "ignore",
45
+ timeout: 5e3
46
+ });
47
+ } catch {
48
+ }
49
+ }
50
+ isAlive() {
51
+ try {
52
+ execSync(`tmux has-session -t ${this.session}`, { stdio: "ignore" });
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+ clearHistory() {
59
+ try {
60
+ execSync(`tmux clear-history -t ${this.session}:${this.pane}`, {
61
+ stdio: "ignore",
62
+ timeout: 5e3
63
+ });
64
+ } catch {
65
+ }
66
+ }
67
+ }
68
+ class ScreenshotAdapter {
69
+ adapterType = "desktop";
70
+ readScreen() {
71
+ throw new Error(
72
+ "ScreenshotAdapter not implemented \u2014 use TmuxAdapter for CLI mode"
73
+ );
74
+ }
75
+ sendInput(_text, _opts) {
76
+ throw new Error("ScreenshotAdapter not implemented");
77
+ }
78
+ sendKey(_key) {
79
+ throw new Error("ScreenshotAdapter not implemented");
80
+ }
81
+ isAlive() {
82
+ throw new Error("ScreenshotAdapter not implemented");
83
+ }
84
+ clearHistory() {
85
+ throw new Error("ScreenshotAdapter not implemented");
86
+ }
87
+ }
88
+ export {
89
+ ScreenshotAdapter,
90
+ TmuxAdapter
91
+ };
@@ -0,0 +1,127 @@
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 { execSync } from "child_process";
6
+ import {
7
+ isTmuxAvailable,
8
+ sessionExists,
9
+ killTmuxSession,
10
+ sendCtrlC
11
+ } from "../workers/tmux-manager.js";
12
+ import { TmuxAdapter } from "./screen-adapter.js";
13
+ class SessionManager {
14
+ constructor(config) {
15
+ this.config = config;
16
+ }
17
+ restartCount = 0;
18
+ /** Preflight: check tmux is available */
19
+ preflight() {
20
+ if (!isTmuxAvailable()) {
21
+ throw new Error(
22
+ "tmux is required but not found. Install with: brew install tmux"
23
+ );
24
+ }
25
+ }
26
+ /** Start a new Claude Code session in tmux. Returns a ScreenAdapter. */
27
+ start() {
28
+ const { sessionName, cwd } = this.config;
29
+ if (sessionExists(sessionName)) {
30
+ try {
31
+ killTmuxSession(sessionName);
32
+ } catch {
33
+ }
34
+ }
35
+ execSync(`tmux new-session -d -s ${sessionName} -c ${shellEscape(cwd)}`, {
36
+ stdio: "ignore",
37
+ timeout: 1e4
38
+ });
39
+ const claudeCmd = this.buildClaudeCommand();
40
+ execSync(
41
+ `tmux send-keys -t ${sessionName}:0 ${shellEscape(claudeCmd)} Enter`,
42
+ { stdio: "ignore", timeout: 5e3 }
43
+ );
44
+ const adapter = new TmuxAdapter(sessionName, "0");
45
+ this.waitForReady(adapter, 3e4);
46
+ this.restartCount = 0;
47
+ return adapter;
48
+ }
49
+ /** Stop the Claude session gracefully */
50
+ stop() {
51
+ const { sessionName } = this.config;
52
+ if (!sessionExists(sessionName)) return;
53
+ try {
54
+ sendCtrlC(sessionName, "0");
55
+ this.sleep(2e3);
56
+ execSync(`tmux send-keys -t ${sessionName}:0 '/exit' Enter`, {
57
+ stdio: "ignore",
58
+ timeout: 5e3
59
+ });
60
+ this.sleep(1e3);
61
+ } catch {
62
+ }
63
+ try {
64
+ killTmuxSession(sessionName);
65
+ } catch {
66
+ }
67
+ }
68
+ /** Restart with exponential cooldown */
69
+ restart() {
70
+ this.restartCount++;
71
+ const cooldownMs = Math.min(
72
+ 5e3 * Math.pow(2, this.restartCount - 1),
73
+ 3e5
74
+ );
75
+ this.sleep(cooldownMs);
76
+ this.stop();
77
+ return this.start();
78
+ }
79
+ /** Get consecutive restart count */
80
+ getRestartCount() {
81
+ return this.restartCount;
82
+ }
83
+ /** Reset restart counter (called when work succeeds) */
84
+ resetRestartCount() {
85
+ this.restartCount = 0;
86
+ }
87
+ /** Attach user terminal to the tmux session for live observation */
88
+ attach() {
89
+ const { sessionName } = this.config;
90
+ if (!sessionExists(sessionName)) {
91
+ throw new Error(
92
+ `No operator session '${sessionName}' running. Start one first.`
93
+ );
94
+ }
95
+ execSync(`tmux attach-session -t ${sessionName}`, { stdio: "inherit" });
96
+ }
97
+ // ── Private ───────────────────────────────────────────
98
+ buildClaudeCommand() {
99
+ const parts = ["claude"];
100
+ if (this.config.model) {
101
+ parts.push("--model", this.config.model);
102
+ }
103
+ return parts.join(" ");
104
+ }
105
+ waitForReady(adapter, timeoutMs) {
106
+ const start = Date.now();
107
+ while (Date.now() - start < timeoutMs) {
108
+ const screen = adapter.readScreen();
109
+ if (/^>\s*$/m.test(screen) || /How can I help/i.test(screen)) {
110
+ return;
111
+ }
112
+ this.sleep(500);
113
+ }
114
+ }
115
+ sleep(ms) {
116
+ try {
117
+ execSync(`sleep ${ms / 1e3}`, { stdio: "ignore" });
118
+ } catch {
119
+ }
120
+ }
121
+ }
122
+ function shellEscape(str) {
123
+ return "'" + str.replace(/'/g, "'\\''") + "'";
124
+ }
125
+ export {
126
+ SessionManager
127
+ };