neural-loom 0.2.60 → 0.2.61

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 (185) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +3 -0
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/cache/.previewinfo +1 -1
  5. package/.next/cache/.rscinfo +1 -1
  6. package/.next/cache/.tsbuildinfo +1 -1
  7. package/.next/diagnostics/route-bundle-stats.json +2 -2
  8. package/.next/fallback-build-manifest.json +3 -3
  9. package/.next/prerender-manifest.json +3 -3
  10. package/.next/routes-manifest.json +18 -0
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  17. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  22. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  23. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  24. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  25. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/.next/server/app/api/configs/route/app-paths-manifest.json +3 -0
  27. package/.next/server/app/api/configs/route/build-manifest.json +9 -0
  28. package/.next/server/app/api/configs/route/server-reference-manifest.json +4 -0
  29. package/.next/server/app/api/configs/route.js +6 -0
  30. package/.next/server/app/api/configs/route.js.map +5 -0
  31. package/.next/server/app/api/configs/route.js.nft.json +1 -0
  32. package/.next/server/app/api/configs/route_client-reference-manifest.js +3 -0
  33. package/.next/server/app/api/context/aider/route.js.nft.json +1 -1
  34. package/.next/server/app/api/context/claude/route.js.nft.json +1 -1
  35. package/.next/server/app/api/context/route.js.nft.json +1 -1
  36. package/.next/server/app/api/context/ssh/route.js.nft.json +1 -1
  37. package/.next/server/app/api/recents/route.js.nft.json +1 -1
  38. package/.next/server/app/api/sessions/inject/route.js +2 -2
  39. package/.next/server/app/api/sessions/inject/route.js.nft.json +1 -1
  40. package/.next/server/app/api/sessions/input/route.js +2 -2
  41. package/.next/server/app/api/sessions/input/route.js.nft.json +1 -1
  42. package/.next/server/app/api/sessions/launch/route.js +2 -2
  43. package/.next/server/app/api/sessions/launch/route.js.nft.json +1 -1
  44. package/.next/server/app/api/sessions/route.js +2 -2
  45. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  46. package/.next/server/app/api/sessions/scope/route.js +2 -2
  47. package/.next/server/app/api/sessions/scope/route.js.nft.json +1 -1
  48. package/.next/server/app/api/sessions/start/route.js +2 -2
  49. package/.next/server/app/api/sessions/start/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/stats/route.js +2 -2
  51. package/.next/server/app/api/sessions/stats/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/stop/route.js +2 -2
  53. package/.next/server/app/api/sessions/stop/route.js.nft.json +1 -1
  54. package/.next/server/app/api/sessions/upload-image/route/app-paths-manifest.json +3 -0
  55. package/.next/server/app/api/sessions/upload-image/route/build-manifest.json +9 -0
  56. package/.next/server/app/api/sessions/upload-image/route/server-reference-manifest.json +4 -0
  57. package/.next/server/app/api/sessions/upload-image/route.js +7 -0
  58. package/.next/server/app/api/sessions/upload-image/route.js.map +5 -0
  59. package/.next/server/app/api/sessions/upload-image/route.js.nft.json +1 -0
  60. package/.next/server/app/api/sessions/upload-image/route_client-reference-manifest.js +3 -0
  61. package/.next/server/app/api/sql/connections/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sql/mcp/route.js.nft.json +1 -1
  63. package/.next/server/app/api/sql/query/route.js.nft.json +1 -1
  64. package/.next/server/app/api/sql/schema/route.js.nft.json +1 -1
  65. package/.next/server/app/api/telemetry/route/app-paths-manifest.json +3 -0
  66. package/.next/server/app/api/telemetry/route/build-manifest.json +9 -0
  67. package/.next/server/app/api/telemetry/route/server-reference-manifest.json +4 -0
  68. package/.next/server/app/api/telemetry/route.js +6 -0
  69. package/.next/server/app/api/telemetry/route.js.map +5 -0
  70. package/.next/server/app/api/telemetry/route.js.nft.json +1 -0
  71. package/.next/server/app/api/telemetry/route_client-reference-manifest.js +3 -0
  72. package/.next/server/app/index.html +1 -1
  73. package/.next/server/app/index.rsc +3 -3
  74. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  75. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  76. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  77. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  78. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  79. package/.next/server/app/page/react-loadable-manifest.json +5 -5
  80. package/.next/server/app/page_client-reference-manifest.js +1 -1
  81. package/.next/server/app-paths-manifest.json +3 -0
  82. package/.next/server/chunks/[root-of-the-server]__0.ptwor._.js +1 -1
  83. package/.next/server/chunks/[root-of-the-server]__0.ptwor._.js.map +1 -1
  84. package/.next/server/chunks/[root-of-the-server]__00-sqou._.js +15 -0
  85. package/.next/server/chunks/{[root-of-the-server]__0thsozo._.js.map → [root-of-the-server]__00-sqou._.js.map} +1 -1
  86. package/.next/server/chunks/[root-of-the-server]__00l5btc._.js +1 -1
  87. package/.next/server/chunks/[root-of-the-server]__00l5btc._.js.map +1 -1
  88. package/.next/server/chunks/[root-of-the-server]__01621nz._.js +3 -0
  89. package/.next/server/chunks/[root-of-the-server]__01621nz._.js.map +1 -0
  90. package/.next/server/chunks/[root-of-the-server]__06men8g._.js +1 -1
  91. package/.next/server/chunks/[root-of-the-server]__06men8g._.js.map +1 -1
  92. package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js +3 -0
  93. package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js.map +1 -0
  94. package/.next/server/chunks/[root-of-the-server]__097vtn1._.js +15 -0
  95. package/.next/server/chunks/{[root-of-the-server]__0em6a~~._.js.map → [root-of-the-server]__097vtn1._.js.map} +1 -1
  96. package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js +15 -0
  97. package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js.map +1 -0
  98. package/.next/server/chunks/[root-of-the-server]__0co.g5k._.js +1 -1
  99. package/.next/server/chunks/[root-of-the-server]__0co.g5k._.js.map +1 -1
  100. package/.next/server/chunks/[root-of-the-server]__0e03ite._.js +15 -0
  101. package/.next/server/chunks/{[root-of-the-server]__10wwca8._.js.map → [root-of-the-server]__0e03ite._.js.map} +1 -1
  102. package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js +15 -0
  103. package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js.map +1 -0
  104. package/.next/server/chunks/[root-of-the-server]__0h.~hv6._.js +15 -0
  105. package/.next/server/chunks/{[root-of-the-server]__0s0is_9._.js.map → [root-of-the-server]__0h.~hv6._.js.map} +1 -1
  106. package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js +33 -0
  107. package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js.map +1 -0
  108. package/.next/server/chunks/[root-of-the-server]__0s1axek._.js +15 -0
  109. package/.next/server/chunks/{[root-of-the-server]__0i~01cv._.js.map → [root-of-the-server]__0s1axek._.js.map} +1 -1
  110. package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js +15 -0
  111. package/.next/server/chunks/{[root-of-the-server]__0_o7_jj._.js.map → [root-of-the-server]__0tq0maq._.js.map} +1 -1
  112. package/.next/server/chunks/[root-of-the-server]__0viil6b._.js +15 -0
  113. package/.next/server/chunks/{[root-of-the-server]__07zx04e._.js.map → [root-of-the-server]__0viil6b._.js.map} +1 -1
  114. package/.next/server/chunks/_next-internal_server_app_api_configs_route_actions_087k_4j.js +3 -0
  115. package/.next/server/chunks/_next-internal_server_app_api_configs_route_actions_087k_4j.js.map +1 -0
  116. package/.next/server/chunks/_next-internal_server_app_api_sessions_upload-image_route_actions_09tfn3a.js +3 -0
  117. package/.next/server/chunks/_next-internal_server_app_api_sessions_upload-image_route_actions_09tfn3a.js.map +1 -0
  118. package/.next/server/chunks/_next-internal_server_app_api_telemetry_route_actions_0.yo5qr.js +3 -0
  119. package/.next/server/chunks/_next-internal_server_app_api_telemetry_route_actions_0.yo5qr.js.map +1 -0
  120. package/.next/server/chunks/ssr/_0t7oqy6._.js +1 -1
  121. package/.next/server/chunks/ssr/_0t7oqy6._.js.map +1 -1
  122. package/.next/server/middleware-build-manifest.js +3 -3
  123. package/.next/server/pages/404.html +1 -1
  124. package/.next/server/pages/500.html +1 -1
  125. package/.next/server/server-reference-manifest.js +1 -1
  126. package/.next/server/server-reference-manifest.json +1 -1
  127. package/.next/static/chunks/07lj0adi0kt5p.css +1 -0
  128. package/.next/static/chunks/0xhaghhxllgzs.js +1 -0
  129. package/.next/static/chunks/128c-kly7grxg.js +4 -0
  130. package/.next/static/chunks/{0y~7jjqbr14.5.js → 15_baizhaiq8c.js} +10 -7
  131. package/.next/static/chunks/15sigqif_v.mu.js +1 -0
  132. package/.next/static/chunks/17ud2wmsyqknc.js +11 -0
  133. package/.next/trace +1 -1
  134. package/.next/trace-build +1 -1
  135. package/.next/types/routes.d.ts +4 -1
  136. package/.next/types/validator.ts +27 -0
  137. package/bin/cli.js +4 -4
  138. package/bin/fix-pwa-ubuntu.sh +50 -0
  139. package/package.json +1 -1
  140. package/src/app/api/configs/route.ts +40 -0
  141. package/src/app/api/sessions/launch/route.ts +3 -3
  142. package/src/app/api/sessions/route.ts +2 -2
  143. package/src/app/api/sessions/start/route.ts +2 -2
  144. package/src/app/api/sessions/upload-image/route.ts +57 -0
  145. package/src/app/api/sql/connections/route.ts +9 -6
  146. package/src/app/api/sql/mcp/route.ts +3 -2
  147. package/src/app/api/sql/query/route.ts +3 -2
  148. package/src/app/api/sql/schema/route.ts +3 -2
  149. package/src/app/api/telemetry/route.ts +18 -0
  150. package/src/app/components/AiderWizard.tsx +46 -2
  151. package/src/app/components/ClaudeWizard.tsx +30 -0
  152. package/src/app/components/IdeLayout.tsx +566 -70
  153. package/src/app/components/TerminalConsole.tsx +54 -0
  154. package/src/app/page.module.css +8 -2
  155. package/src/app/page.tsx +534 -401
  156. package/src/lib/agents/AgentRunner.ts +88 -5
  157. package/src/lib/agents/AiderRunner.ts +4 -18
  158. package/src/lib/agents/ClaudeRunner.ts +16 -26
  159. package/src/lib/agents/DockerRunner.ts +16 -26
  160. package/src/lib/agents/MockRunner.ts +4 -16
  161. package/src/lib/agents/SSHRunner.ts +16 -26
  162. package/src/lib/agents/SessionConfig.ts +79 -0
  163. package/src/lib/agents/SessionManager.ts +16 -16
  164. package/src/lib/agents/SessionPersister.ts +13 -9
  165. package/src/lib/agents/ShellRunner.ts +14 -21
  166. package/src/lib/agents/WebSocketServer.ts +2 -2
  167. package/.next/server/chunks/[root-of-the-server]__01.zh3b._.js +0 -15
  168. package/.next/server/chunks/[root-of-the-server]__01.zh3b._.js.map +0 -1
  169. package/.next/server/chunks/[root-of-the-server]__07zx04e._.js +0 -15
  170. package/.next/server/chunks/[root-of-the-server]__0_o7_jj._.js +0 -15
  171. package/.next/server/chunks/[root-of-the-server]__0d0pykb._.js +0 -33
  172. package/.next/server/chunks/[root-of-the-server]__0d0pykb._.js.map +0 -1
  173. package/.next/server/chunks/[root-of-the-server]__0em6a~~._.js +0 -15
  174. package/.next/server/chunks/[root-of-the-server]__0i~01cv._.js +0 -15
  175. package/.next/server/chunks/[root-of-the-server]__0s0is_9._.js +0 -15
  176. package/.next/server/chunks/[root-of-the-server]__0thsozo._.js +0 -15
  177. package/.next/server/chunks/[root-of-the-server]__10wwca8._.js +0 -15
  178. package/.next/static/chunks/076_b43_kienv.js +0 -1
  179. package/.next/static/chunks/07r_au7dcyu8l.js +0 -11
  180. package/.next/static/chunks/0hoyc~vbuqe0t.css +0 -1
  181. package/.next/static/chunks/0k2co3uj4w81_.js +0 -1
  182. package/.next/static/chunks/0sqeixl35qoac.js +0 -1
  183. /package/.next/static/{PdRX_M_W5hqhNmHxYt-oH → b48zoI5QUg8m7v1asTiCM}/_buildManifest.js +0 -0
  184. /package/.next/static/{PdRX_M_W5hqhNmHxYt-oH → b48zoI5QUg8m7v1asTiCM}/_clientMiddlewareManifest.js +0 -0
  185. /package/.next/static/{PdRX_M_W5hqhNmHxYt-oH → b48zoI5QUg8m7v1asTiCM}/_ssgManifest.js +0 -0
@@ -1,3 +1,6 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
1
4
  export interface RunnerStatus {
2
5
  id: string;
3
6
  name: string;
@@ -12,6 +15,19 @@ export interface RunnerStatus {
12
15
  agentState?: 'idle' | 'thinking' | 'complete' | 'needs-input';
13
16
  }
14
17
 
18
+ export interface TokenTransaction {
19
+ timestamp: string;
20
+ sessionId: string;
21
+ sessionName: string;
22
+ type: string;
23
+ inputTokens: number;
24
+ outputTokens: number;
25
+ cost: number;
26
+ totalInput: number;
27
+ totalOutput: number;
28
+ totalCost: number;
29
+ }
30
+
15
31
  export abstract class AgentRunner {
16
32
  abstract id: string;
17
33
  abstract name: string;
@@ -25,6 +41,57 @@ export abstract class AgentRunner {
25
41
  tokenUsage: { input: number; output: number; cost: number } = { input: 0, output: 0, cost: 0 };
26
42
  agentState: 'idle' | 'thinking' | 'complete' | 'needs-input' = 'idle';
27
43
  private stateTimer: NodeJS.Timeout | null = null;
44
+ private lastLoggedUsage = { input: 0, output: 0, cost: 0 };
45
+
46
+ protected logTokenUsageUpdate(): void {
47
+ if (
48
+ this.tokenUsage.input === this.lastLoggedUsage.input &&
49
+ this.tokenUsage.output === this.lastLoggedUsage.output &&
50
+ this.tokenUsage.cost === this.lastLoggedUsage.cost
51
+ ) {
52
+ return;
53
+ }
54
+
55
+ const diffInput = this.tokenUsage.input - this.lastLoggedUsage.input;
56
+ const diffOutput = this.tokenUsage.output - this.lastLoggedUsage.output;
57
+ const diffCost = this.tokenUsage.cost - this.lastLoggedUsage.cost;
58
+
59
+ if (diffInput > 0 || diffOutput > 0 || diffCost > 0) {
60
+ try {
61
+ const configDir = path.join(process.cwd(), ".ai", "neural-loom");
62
+ if (!fs.existsSync(configDir)) {
63
+ fs.mkdirSync(configDir, { recursive: true });
64
+ }
65
+ const historyFile = path.join(configDir, "token-history.json");
66
+ let history: TokenTransaction[] = [];
67
+ if (fs.existsSync(historyFile)) {
68
+ try {
69
+ history = JSON.parse(fs.readFileSync(historyFile, "utf8")) || [];
70
+ } catch {
71
+ history = [];
72
+ }
73
+ }
74
+
75
+ history.push({
76
+ timestamp: new Date().toISOString(),
77
+ sessionId: this.id,
78
+ sessionName: this.name,
79
+ type: this.type,
80
+ inputTokens: diffInput > 0 ? diffInput : 0,
81
+ outputTokens: diffOutput > 0 ? diffOutput : 0,
82
+ cost: diffCost > 0 ? diffCost : 0,
83
+ totalInput: this.tokenUsage.input,
84
+ totalOutput: this.tokenUsage.output,
85
+ totalCost: this.tokenUsage.cost
86
+ });
87
+
88
+ fs.writeFileSync(historyFile, JSON.stringify(history, null, 2), "utf8");
89
+ this.lastLoggedUsage = { ...this.tokenUsage };
90
+ } catch (err) {
91
+ console.error("[AgentRunner] Failed to log token usage transaction:", err);
92
+ }
93
+ }
94
+ }
28
95
 
29
96
  protected parseTokenUsage(data: string): void {
30
97
  const cleanData = data.replace(/\x1b\[[0-9;]*m/g, ""); // Strip ANSI colors
@@ -68,14 +135,30 @@ export abstract class AgentRunner {
68
135
  if (this.tokenUsage.cost === 0 && (this.tokenUsage.input > 0 || this.tokenUsage.output > 0)) {
69
136
  this.tokenUsage.cost = (this.tokenUsage.input * 0.000003) + (this.tokenUsage.output * 0.000015);
70
137
  }
138
+ this.logTokenUsageUpdate();
71
139
  }
72
140
 
73
- abstract start(): Promise<void>;
141
+ abstract start(resume?: boolean): Promise<void>;
74
142
  abstract stop(): Promise<void>;
75
143
  abstract sendInput(text: string, isRaw?: boolean): Promise<void>;
76
- abstract getStatus(): RunnerStatus;
144
+ getStatus(): RunnerStatus {
145
+ this.analyzeState();
146
+ return {
147
+ id: this.id,
148
+ name: this.name,
149
+ type: this.type,
150
+ status: this.status,
151
+ createdAt: this.createdAt,
152
+ logsCount: this.logs.length,
153
+ workspaceRoot: this.workspaceRoot || (this.type === 'shell' ? (process.env.ORIGINAL_CWD || process.cwd()) : undefined),
154
+ scopedDirs: this.scopedDirs,
155
+ lastSyncedWorkspaceFolders: this.lastSyncedWorkspaceFolders,
156
+ tokenUsage: this.tokenUsage,
157
+ agentState: this.agentState
158
+ };
159
+ }
77
160
 
78
- resize(cols: number, rows: number): void {
161
+ resize(_cols: number, _rows: number): void {
79
162
  // No-op by default, overridden by PTY runners
80
163
  }
81
164
 
@@ -129,14 +212,14 @@ export abstract class AgentRunner {
129
212
  }, 800);
130
213
  }
131
214
 
132
- private analyzeState(): void {
215
+ protected analyzeState(): void {
133
216
  if (this.status !== 'running') {
134
217
  this.agentState = 'idle';
135
218
  return;
136
219
  }
137
220
 
138
221
  const tailLines = this.logs.slice(-5).join("");
139
- const cleanTail = tailLines.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").trim();
222
+ const cleanTail = tailLines.replace(/\x1b(?:\[[0-9;]*[a-zA-Z]|\([a-zA-Z]|\)[a-zA-Z])/g, "").trim();
140
223
 
141
224
  const isQuestion =
142
225
  /(\[y\/n\]|\(y\/n\)|\?|confirm|allow|should I|would you|press enter|proceed)/i.test(cleanTail);
@@ -3,7 +3,7 @@ import path from "path";
3
3
  import { exec } from "child_process";
4
4
  import { getConfigDir } from "../config";
5
5
  import type { IPty } from "node-pty";
6
- import { AgentRunner, RunnerStatus } from "./AgentRunner";
6
+ import { AgentRunner } from "./AgentRunner";
7
7
  import { SessionPersister } from "./SessionPersister";
8
8
 
9
9
  function isWindows(): boolean {
@@ -71,7 +71,7 @@ function getFilesForAider(dir: string): string {
71
71
  export class AiderRunner extends AgentRunner {
72
72
  id: string;
73
73
  name: string;
74
- type: 'aider' | 'aider-docker';
74
+ type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker' | 'shell';
75
75
  status: 'idle' | 'running' | 'stopped' = 'idle';
76
76
  createdAt: string;
77
77
  logs: string[] = [];
@@ -97,7 +97,7 @@ export class AiderRunner extends AgentRunner {
97
97
  SessionPersister.saveSession(this);
98
98
  }
99
99
 
100
- async start(): Promise<void> {
100
+ async start(resume?: boolean): Promise<void> {
101
101
  if (this.status === 'running') return;
102
102
 
103
103
  this.status = 'running';
@@ -304,19 +304,5 @@ export class AiderRunner extends AgentRunner {
304
304
  }
305
305
  }
306
306
  }
307
-
308
- getStatus(): RunnerStatus {
309
- return {
310
- id: this.id,
311
- name: this.name,
312
- type: this.type,
313
- status: this.status,
314
- createdAt: this.createdAt,
315
- logsCount: this.logs.length,
316
- workspaceRoot: this.workspaceRoot,
317
- scopedDirs: this.scopedDirs,
318
- tokenUsage: this.tokenUsage,
319
- agentState: this.agentState
320
- };
321
- }
322
307
  }
308
+
@@ -4,7 +4,7 @@ import os from "os";
4
4
  import { execSync } from "child_process";
5
5
  import { getConfigDir } from "../config";
6
6
  import type { IPty } from "node-pty";
7
- import { AgentRunner, RunnerStatus } from "./AgentRunner";
7
+ import { AgentRunner } from "./AgentRunner";
8
8
  import { SessionPersister } from "./SessionPersister";
9
9
 
10
10
  function isWindows(): boolean {
@@ -32,7 +32,7 @@ function findGitBash(): string | null {
32
32
  export class ClaudeRunner extends AgentRunner {
33
33
  id: string;
34
34
  name: string;
35
- type: 'mock' | 'claude' = 'claude';
35
+ type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker' | 'shell' = 'claude';
36
36
  status: 'idle' | 'running' | 'stopped' = 'idle';
37
37
  createdAt: string;
38
38
  logs: string[] = [];
@@ -57,7 +57,7 @@ export class ClaudeRunner extends AgentRunner {
57
57
  SessionPersister.saveSession(this);
58
58
  }
59
59
 
60
- async start(): Promise<void> {
60
+ async start(resume?: boolean): Promise<void> {
61
61
  if (this.status === 'running') return;
62
62
 
63
63
  // Detect Git Bash on Windows
@@ -87,15 +87,19 @@ export class ClaudeRunner extends AgentRunner {
87
87
 
88
88
  // Load Claude configuration to check for extraFlags (like --resume)
89
89
  let extraFlags = "";
90
- const claudeConfigPath = path.join(getConfigDir(), "claude.json");
91
- if (fs.existsSync(claudeConfigPath)) {
92
- try {
93
- const config = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"));
94
- if (config.extraFlags) {
95
- extraFlags = config.extraFlags;
90
+ if (resume) {
91
+ extraFlags = "--resume";
92
+ } else {
93
+ const claudeConfigPath = path.join(getConfigDir(), "claude.json");
94
+ if (fs.existsSync(claudeConfigPath)) {
95
+ try {
96
+ const config = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"));
97
+ if (config.extraFlags) {
98
+ extraFlags = config.extraFlags;
99
+ }
100
+ } catch (err) {
101
+ console.error("Failed to parse Claude configuration:", err);
96
102
  }
97
- } catch (err) {
98
- console.error("Failed to parse Claude configuration:", err);
99
103
  }
100
104
  }
101
105
 
@@ -194,19 +198,5 @@ export class ClaudeRunner extends AgentRunner {
194
198
  }
195
199
  }
196
200
  }
197
-
198
- getStatus(): RunnerStatus {
199
- return {
200
- id: this.id,
201
- name: this.name,
202
- type: this.type,
203
- status: this.status,
204
- createdAt: this.createdAt,
205
- logsCount: this.logs.length,
206
- workspaceRoot: this.workspaceRoot,
207
- scopedDirs: this.scopedDirs,
208
- tokenUsage: this.tokenUsage,
209
- agentState: this.agentState
210
- };
211
- }
212
201
  }
202
+
@@ -2,7 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { getConfigDir } from "../config";
4
4
  import type { IPty } from "node-pty";
5
- import { AgentRunner, RunnerStatus } from "./AgentRunner";
5
+ import { AgentRunner } from "./AgentRunner";
6
6
  import { SessionPersister } from "./SessionPersister";
7
7
 
8
8
  function isWindows(): boolean {
@@ -30,7 +30,7 @@ function findGitBash(): string | null {
30
30
  export class DockerRunner extends AgentRunner {
31
31
  id: string;
32
32
  name: string;
33
- type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker' = 'claude-docker';
33
+ type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker' | 'shell' = 'claude-docker';
34
34
  status: 'idle' | 'running' | 'stopped' = 'idle';
35
35
  createdAt: string;
36
36
  logs: string[] = [];
@@ -55,7 +55,7 @@ export class DockerRunner extends AgentRunner {
55
55
  SessionPersister.saveSession(this);
56
56
  }
57
57
 
58
- async start(): Promise<void> {
58
+ async start(resume?: boolean): Promise<void> {
59
59
  if (this.status === 'running') return;
60
60
 
61
61
  this.status = 'running';
@@ -123,15 +123,19 @@ export class DockerRunner extends AgentRunner {
123
123
 
124
124
  // Load Claude configuration to check for extraFlags (like --resume)
125
125
  let extraFlags = "";
126
- const claudeConfigPath = path.join(getConfigDir(), "claude.json");
127
- if (fs.existsSync(claudeConfigPath)) {
128
- try {
129
- const config = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"));
130
- if (config.extraFlags) {
131
- extraFlags = config.extraFlags;
126
+ if (resume) {
127
+ extraFlags = "--resume";
128
+ } else {
129
+ const claudeConfigPath = path.join(getConfigDir(), "claude.json");
130
+ if (fs.existsSync(claudeConfigPath)) {
131
+ try {
132
+ const config = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"));
133
+ if (config.extraFlags) {
134
+ extraFlags = config.extraFlags;
135
+ }
136
+ } catch (err) {
137
+ console.error("Failed to parse Claude configuration:", err);
132
138
  }
133
- } catch (err) {
134
- console.error("Failed to parse Claude configuration:", err);
135
139
  }
136
140
  }
137
141
 
@@ -198,19 +202,5 @@ export class DockerRunner extends AgentRunner {
198
202
  }
199
203
  }
200
204
  }
201
-
202
- getStatus(): RunnerStatus {
203
- return {
204
- id: this.id,
205
- name: this.name,
206
- type: this.type,
207
- status: this.status,
208
- createdAt: this.createdAt,
209
- logsCount: this.logs.length,
210
- workspaceRoot: this.workspaceRoot,
211
- scopedDirs: this.scopedDirs,
212
- tokenUsage: this.tokenUsage,
213
- agentState: this.agentState
214
- };
215
- }
216
205
  }
206
+
@@ -1,10 +1,10 @@
1
- import { AgentRunner, RunnerStatus } from "./AgentRunner";
1
+ import { AgentRunner } from "./AgentRunner";
2
2
  import { SessionPersister } from "./SessionPersister";
3
3
 
4
4
  export class MockRunner extends AgentRunner {
5
5
  id: string;
6
6
  name: string;
7
- type: 'mock' | 'claude' = 'mock';
7
+ type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker' | 'shell' = 'mock';
8
8
  status: 'idle' | 'running' | 'stopped' = 'idle';
9
9
  createdAt: string;
10
10
  logs: string[] = [];
@@ -33,7 +33,7 @@ export class MockRunner extends AgentRunner {
33
33
  SessionPersister.saveSession(this);
34
34
  }
35
35
 
36
- async start(): Promise<void> {
36
+ async start(resume?: boolean): Promise<void> {
37
37
  if (this.status === 'running') return;
38
38
  this.status = 'running';
39
39
 
@@ -111,17 +111,5 @@ export class MockRunner extends AgentRunner {
111
111
  }
112
112
  }, 400);
113
113
  }
114
-
115
- getStatus(): RunnerStatus {
116
- return {
117
- id: this.id,
118
- name: this.name,
119
- type: this.type,
120
- status: this.status,
121
- createdAt: this.createdAt,
122
- logsCount: this.logs.length,
123
- tokenUsage: this.tokenUsage,
124
- agentState: this.agentState
125
- };
126
- }
127
114
  }
115
+
@@ -2,7 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { getConfigDir } from "../config";
4
4
  import type { IPty } from "node-pty";
5
- import { AgentRunner, RunnerStatus } from "./AgentRunner";
5
+ import { AgentRunner } from "./AgentRunner";
6
6
  import { SessionPersister } from "./SessionPersister";
7
7
 
8
8
  function isWindows(): boolean {
@@ -37,7 +37,7 @@ interface SSHConfig {
37
37
  export class SSHRunner extends AgentRunner {
38
38
  id: string;
39
39
  name: string;
40
- type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker' = 'claude-ssh';
40
+ type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker' | 'shell' = 'claude-ssh';
41
41
  status: 'idle' | 'running' | 'stopped' = 'idle';
42
42
  createdAt: string;
43
43
  logs: string[] = [];
@@ -62,7 +62,7 @@ export class SSHRunner extends AgentRunner {
62
62
  SessionPersister.saveSession(this);
63
63
  }
64
64
 
65
- async start(): Promise<void> {
65
+ async start(resume?: boolean): Promise<void> {
66
66
  if (this.status === 'running') return;
67
67
 
68
68
  this.status = 'running';
@@ -121,15 +121,19 @@ export class SSHRunner extends AgentRunner {
121
121
 
122
122
  // Load Claude configuration to check for extraFlags (like --resume)
123
123
  let extraFlags = "";
124
- const claudeConfigPath = path.join(getConfigDir(), "claude.json");
125
- if (fs.existsSync(claudeConfigPath)) {
126
- try {
127
- const config = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"));
128
- if (config.extraFlags) {
129
- extraFlags = config.extraFlags;
124
+ if (resume) {
125
+ extraFlags = "--resume";
126
+ } else {
127
+ const claudeConfigPath = path.join(getConfigDir(), "claude.json");
128
+ if (fs.existsSync(claudeConfigPath)) {
129
+ try {
130
+ const config = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"));
131
+ if (config.extraFlags) {
132
+ extraFlags = config.extraFlags;
133
+ }
134
+ } catch (err) {
135
+ console.error("Failed to parse Claude configuration:", err);
130
136
  }
131
- } catch (err) {
132
- console.error("Failed to parse Claude configuration:", err);
133
137
  }
134
138
  }
135
139
 
@@ -196,19 +200,5 @@ export class SSHRunner extends AgentRunner {
196
200
  }
197
201
  }
198
202
  }
199
-
200
- getStatus(): RunnerStatus {
201
- return {
202
- id: this.id,
203
- name: this.name,
204
- type: this.type,
205
- status: this.status,
206
- createdAt: this.createdAt,
207
- logsCount: this.logs.length,
208
- workspaceRoot: this.workspaceRoot,
209
- scopedDirs: this.scopedDirs,
210
- tokenUsage: this.tokenUsage,
211
- agentState: this.agentState
212
- };
213
- }
214
203
  }
204
+
@@ -0,0 +1,79 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { getConfigDir } from "../config";
4
+ import crypto from "crypto";
5
+
6
+ export interface SessionConfig {
7
+ id: string;
8
+ name: string;
9
+ type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker' | 'shell';
10
+ workspaceRoot?: string;
11
+ scopedDirs?: string[];
12
+ extraFlags?: string;
13
+ createdAt: string;
14
+ }
15
+
16
+ export class SessionConfigManager {
17
+ private static getConfigFile(): string {
18
+ return path.join(getConfigDir(), "configs.json");
19
+ }
20
+
21
+ public static loadAllConfigs(): SessionConfig[] {
22
+ try {
23
+ const file = this.getConfigFile();
24
+ if (!fs.existsSync(file)) {
25
+ return [];
26
+ }
27
+ const raw = fs.readFileSync(file, "utf8");
28
+ return JSON.parse(raw) || [];
29
+ } catch (err) {
30
+ console.error("[SessionConfigManager] Failed to load configs:", err);
31
+ return [];
32
+ }
33
+ }
34
+
35
+ public static saveConfig(config: Omit<SessionConfig, "id" | "createdAt"> & { id?: string }): SessionConfig {
36
+ const configs = this.loadAllConfigs();
37
+ const id = config.id || crypto.randomUUID();
38
+ const newConfig: SessionConfig = {
39
+ id,
40
+ name: config.name,
41
+ type: config.type,
42
+ workspaceRoot: config.workspaceRoot,
43
+ scopedDirs: config.scopedDirs,
44
+ extraFlags: config.extraFlags,
45
+ createdAt: new Date().toISOString(),
46
+ };
47
+
48
+ const idx = configs.findIndex(c => c.id === id);
49
+ if (idx !== -1) {
50
+ configs[idx] = newConfig;
51
+ } else {
52
+ configs.push(newConfig);
53
+ }
54
+
55
+ try {
56
+ fs.writeFileSync(this.getConfigFile(), JSON.stringify(configs, null, 2), "utf8");
57
+ } catch (err) {
58
+ console.error("[SessionConfigManager] Failed to save config to disk:", err);
59
+ }
60
+
61
+ return newConfig;
62
+ }
63
+
64
+ public static deleteConfig(id: string): boolean {
65
+ const configs = this.loadAllConfigs();
66
+ const filtered = configs.filter(c => c.id !== id);
67
+ if (configs.length === filtered.length) {
68
+ return false;
69
+ }
70
+
71
+ try {
72
+ fs.writeFileSync(this.getConfigFile(), JSON.stringify(filtered, null, 2), "utf8");
73
+ return true;
74
+ } catch (err) {
75
+ console.error("[SessionConfigManager] Failed to delete config from disk:", err);
76
+ return false;
77
+ }
78
+ }
79
+ }
@@ -33,15 +33,7 @@ export class SessionManager {
33
33
  try {
34
34
  const saved = SessionPersister.loadAllSessions();
35
35
  for (const data of saved) {
36
- // Automatically clean up stopped sessions older than 24 hours to avoid clutter
37
- if (data.status === "stopped") {
38
- const createdTime = new Date(data.createdAt).getTime();
39
- const ageHours = (Date.now() - createdTime) / (1000 * 60 * 60);
40
- if (ageHours > 24) {
41
- SessionPersister.deleteSession(data.id);
42
- continue;
43
- }
44
- }
36
+ // Preserved persisted session
45
37
 
46
38
  let runner: AgentRunner;
47
39
  if (data.type === "mock") {
@@ -52,6 +44,8 @@ export class SessionManager {
52
44
  runner = new SSHRunner(data.id, data.name);
53
45
  } else if (data.type === "aider" || data.type === "aider-docker") {
54
46
  runner = new AiderRunner(data.id, data.type as 'aider' | 'aider-docker', data.name);
47
+ } else if (data.type === "shell") {
48
+ runner = new ShellRunner(data.id, data.name, data.workspaceRoot);
55
49
  } else {
56
50
  runner = new ClaudeRunner(data.id, data.name);
57
51
  }
@@ -73,8 +67,9 @@ export class SessionManager {
73
67
  public static getInstance(): SessionManager {
74
68
  if (!globalThis.sessionManager) {
75
69
  globalThis.sessionManager = new SessionManager();
76
- // Initialize WebSocket gateway server on port 3001
77
- initWebSocketServer(3001, (id) => globalThis.sessionManager!.getSession(id));
70
+ // Initialize WebSocket gateway server
71
+ const wsPort = Number(process.env.WS_PORT) || 9601;
72
+ initWebSocketServer(wsPort, (id) => globalThis.sessionManager!.getSession(id));
78
73
  }
79
74
  return globalThis.sessionManager;
80
75
  }
@@ -84,12 +79,15 @@ export class SessionManager {
84
79
  }
85
80
 
86
81
  public async createSession(
87
- type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker',
82
+ type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker' | 'shell',
88
83
  name?: string,
89
84
  workspaceRoot?: string,
90
- scopedDirs?: string[]
85
+ scopedDirs?: string[],
86
+ parentSessionId?: string
91
87
  ): Promise<AgentRunner> {
92
- const id = this.generateId();
88
+ const id = (type === 'shell' && parentSessionId)
89
+ ? `shell-${parentSessionId}-${crypto.randomUUID()}`
90
+ : this.generateId();
93
91
  let runner: AgentRunner;
94
92
 
95
93
  if (type === 'mock') {
@@ -100,6 +98,8 @@ export class SessionManager {
100
98
  runner = new SSHRunner(id, name);
101
99
  } else if (type === 'aider' || type === 'aider-docker') {
102
100
  runner = new AiderRunner(id, type, name);
101
+ } else if (type === 'shell') {
102
+ runner = new ShellRunner(id, name, workspaceRoot);
103
103
  } else {
104
104
  runner = new ClaudeRunner(id, name);
105
105
  }
@@ -177,10 +177,10 @@ export class SessionManager {
177
177
  }
178
178
  }
179
179
 
180
- public async startSession(id: string): Promise<boolean> {
180
+ public async startSession(id: string, resume?: boolean): Promise<boolean> {
181
181
  const runner = this.sessions.get(id);
182
182
  if (!runner) return false;
183
- await runner.start();
183
+ await runner.start(resume);
184
184
  SessionPersister.saveSession(runner, true);
185
185
  return true;
186
186
  }
@@ -94,16 +94,20 @@ export class SessionPersister {
94
94
  for (const file of files) {
95
95
  if (file.endsWith(".json")) {
96
96
  const filePath = path.join(dir, file);
97
- const raw = fs.readFileSync(filePath, "utf8");
98
- const data: PersistedSession = JSON.parse(raw);
99
- // On server restart, active PTY processes (which are dead) are marked stopped
100
- if (data.status === "running" && data.type !== "mock") {
101
- data.status = "stopped";
102
- data.logs.push("\r\n\x1b[1;31m[System] Server restarted. Terminal connection lost.\x1b[0m\r\n");
103
- // Save the updated status back to disk
104
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
97
+ try {
98
+ const raw = fs.readFileSync(filePath, "utf8");
99
+ const data: PersistedSession = JSON.parse(raw);
100
+ // On server restart, active PTY processes (which are dead) are marked stopped
101
+ if (data.status === "running" && data.type !== "mock") {
102
+ data.status = "stopped";
103
+ data.logs.push("\r\n\x1b[1;31m[System] Server restarted. Terminal connection lost.\x1b[0m\r\n");
104
+ // Save the updated status back to disk
105
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
106
+ }
107
+ sessions.push(data);
108
+ } catch (fileErr) {
109
+ console.error(`[SessionPersister] Failed to load session file ${file}:`, fileErr);
105
110
  }
106
- sessions.push(data);
107
111
  }
108
112
  }
109
113
  return sessions;