@enruana/claude-orka 0.3.2 → 0.4.1
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/dist/cli.js +154 -65
- package/dist/electron/main/main.js +1792 -153
- package/dist/electron/main/main.js.map +1 -1
- package/dist/electron/preload/preload.js +19 -14
- package/dist/electron/preload/preload.js.map +1 -1
- package/dist/electron/renderer/assets/main-86v1OCiP.js +144 -0
- package/dist/electron/renderer/assets/main-BhBDXIbL.css +1 -0
- package/dist/electron/renderer/index.html +13 -0
- package/dist/src/core/ClaudeOrka.d.ts +2 -1
- package/dist/src/core/ClaudeOrka.d.ts.map +1 -1
- package/dist/src/core/ClaudeOrka.js +4 -2
- package/dist/src/core/ClaudeOrka.js.map +1 -1
- package/dist/src/core/SessionManager.d.ts +1 -1
- package/dist/src/core/SessionManager.d.ts.map +1 -1
- package/dist/src/core/SessionManager.js +100 -23
- package/dist/src/core/SessionManager.js.map +1 -1
- package/dist/src/models/Fork.d.ts +3 -1
- package/dist/src/models/Fork.d.ts.map +1 -1
- package/dist/src/models/Summary.d.ts +1 -1
- package/dist/src/models/Summary.d.ts.map +1 -1
- package/dist/src/utils/logger.d.ts +3 -0
- package/dist/src/utils/logger.d.ts.map +1 -1
- package/dist/src/utils/logger.js +25 -0
- package/dist/src/utils/logger.js.map +1 -1
- package/package.json +4 -3
|
@@ -1,171 +1,1810 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
1
|
+
// electron/main/main.ts
|
|
2
|
+
import { app, BrowserWindow, ipcMain, shell } from "electron";
|
|
3
|
+
import path5 from "path";
|
|
4
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5
|
+
|
|
6
|
+
// src/core/StateManager.ts
|
|
7
|
+
import path3 from "path";
|
|
8
|
+
import fs3 from "fs-extra";
|
|
9
|
+
|
|
10
|
+
// src/utils/tmux.ts
|
|
11
|
+
import execa from "execa";
|
|
12
|
+
|
|
13
|
+
// src/utils/logger.ts
|
|
14
|
+
import fs from "fs-extra";
|
|
15
|
+
import path from "path";
|
|
16
|
+
var Logger = class {
|
|
17
|
+
level = 1 /* INFO */;
|
|
18
|
+
logFilePath = null;
|
|
19
|
+
setLevel(level) {
|
|
20
|
+
this.level = level;
|
|
21
|
+
}
|
|
22
|
+
setLogFile(projectPath) {
|
|
23
|
+
const logDir = path.join(projectPath, ".claude-orka");
|
|
24
|
+
fs.ensureDirSync(logDir);
|
|
25
|
+
this.logFilePath = path.join(logDir, "orka.log");
|
|
26
|
+
}
|
|
27
|
+
writeToFile(level, ...args) {
|
|
28
|
+
if (!this.logFilePath) return;
|
|
29
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
30
|
+
const message = args.map(
|
|
31
|
+
(arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
|
|
32
|
+
).join(" ");
|
|
33
|
+
const logLine = `${timestamp} [${level}] ${message}
|
|
34
|
+
`;
|
|
35
|
+
try {
|
|
36
|
+
fs.appendFileSync(this.logFilePath, logLine);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
debug(...args) {
|
|
41
|
+
if (this.level <= 0 /* DEBUG */) {
|
|
42
|
+
console.log("[DEBUG]", ...args);
|
|
43
|
+
this.writeToFile("DEBUG", ...args);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
info(...args) {
|
|
47
|
+
if (this.level <= 1 /* INFO */) {
|
|
48
|
+
console.log("[INFO]", ...args);
|
|
49
|
+
this.writeToFile("INFO", ...args);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
warn(...args) {
|
|
53
|
+
if (this.level <= 2 /* WARN */) {
|
|
54
|
+
console.warn("[WARN]", ...args);
|
|
55
|
+
this.writeToFile("WARN", ...args);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
error(...args) {
|
|
59
|
+
if (this.level <= 3 /* ERROR */) {
|
|
60
|
+
console.error("[ERROR]", ...args);
|
|
61
|
+
this.writeToFile("ERROR", ...args);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var logger = new Logger();
|
|
66
|
+
|
|
67
|
+
// src/utils/tmux.ts
|
|
68
|
+
var TmuxError = class extends Error {
|
|
69
|
+
constructor(message, command, originalError) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.command = command;
|
|
72
|
+
this.originalError = originalError;
|
|
73
|
+
this.name = "TmuxError";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var TmuxCommands = class {
|
|
77
|
+
/**
|
|
78
|
+
* Verificar si tmux está disponible
|
|
79
|
+
*/
|
|
80
|
+
static async isAvailable() {
|
|
81
|
+
try {
|
|
82
|
+
await execa("which", ["tmux"]);
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Crear una nueva sesión tmux en modo detached
|
|
90
|
+
*/
|
|
91
|
+
static async createSession(name, projectPath) {
|
|
92
|
+
try {
|
|
93
|
+
logger.debug(`Creating tmux session: ${name} at ${projectPath}`);
|
|
94
|
+
await execa("tmux", ["new-session", "-d", "-s", name, "-c", projectPath]);
|
|
95
|
+
logger.info(`Tmux session created: ${name}`);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new TmuxError(
|
|
98
|
+
`Failed to create tmux session: ${name}`,
|
|
99
|
+
`tmux new-session -d -s ${name} -c ${projectPath}`,
|
|
100
|
+
error
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Abrir una terminal que se adjunte a una sesión tmux existente
|
|
106
|
+
* (Solo macOS por ahora)
|
|
107
|
+
*/
|
|
108
|
+
static async openTerminalWindow(sessionName) {
|
|
109
|
+
try {
|
|
110
|
+
logger.debug(`Opening terminal window for session: ${sessionName}`);
|
|
111
|
+
const platform = process.platform;
|
|
112
|
+
if (platform === "darwin") {
|
|
113
|
+
const script = `tell application "Terminal"
|
|
114
|
+
do script "tmux attach -t ${sessionName}"
|
|
115
|
+
activate
|
|
116
|
+
end tell`;
|
|
117
|
+
await execa("osascript", ["-e", script]);
|
|
118
|
+
logger.info("Terminal window opened (Terminal.app)");
|
|
119
|
+
} else if (platform === "linux") {
|
|
66
120
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
console.error('Error watching state file:', error);
|
|
121
|
+
await execa("gnome-terminal", ["--", "tmux", "attach", "-t", sessionName]);
|
|
122
|
+
logger.info("Terminal window opened (gnome-terminal)");
|
|
123
|
+
} catch {
|
|
124
|
+
try {
|
|
125
|
+
await execa("xterm", ["-e", `tmux attach -t ${sessionName}`]);
|
|
126
|
+
logger.info("Terminal window opened (xterm)");
|
|
127
|
+
} catch {
|
|
128
|
+
logger.warn("Could not open terminal window on Linux");
|
|
129
|
+
throw new Error("No suitable terminal emulator found");
|
|
130
|
+
}
|
|
78
131
|
}
|
|
132
|
+
} else {
|
|
133
|
+
logger.warn(`Platform ${platform} not supported for opening terminal windows`);
|
|
134
|
+
throw new Error(`Platform ${platform} not supported`);
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new TmuxError(
|
|
138
|
+
`Failed to open terminal window for session: ${sessionName}`,
|
|
139
|
+
`osascript/terminal`,
|
|
140
|
+
error
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Cerrar una sesión tmux
|
|
146
|
+
*/
|
|
147
|
+
static async killSession(sessionName) {
|
|
148
|
+
try {
|
|
149
|
+
logger.debug(`Killing tmux session: ${sessionName}`);
|
|
150
|
+
await execa("tmux", ["kill-session", "-t", sessionName]);
|
|
151
|
+
logger.info(`Tmux session killed: ${sessionName}`);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new TmuxError(
|
|
154
|
+
`Failed to kill tmux session: ${sessionName}`,
|
|
155
|
+
`tmux kill-session -t ${sessionName}`,
|
|
156
|
+
error
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Verificar si una sesión existe
|
|
162
|
+
*/
|
|
163
|
+
static async sessionExists(sessionName) {
|
|
164
|
+
try {
|
|
165
|
+
await execa("tmux", ["has-session", "-t", sessionName]);
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Obtener el ID del pane principal de una sesión
|
|
173
|
+
*/
|
|
174
|
+
static async getMainPaneId(sessionName) {
|
|
175
|
+
try {
|
|
176
|
+
logger.debug(`Getting main pane ID for session: ${sessionName}`);
|
|
177
|
+
const { stdout } = await execa("tmux", [
|
|
178
|
+
"list-panes",
|
|
179
|
+
"-t",
|
|
180
|
+
sessionName,
|
|
181
|
+
"-F",
|
|
182
|
+
"#{pane_id}"
|
|
183
|
+
]);
|
|
184
|
+
const paneId = stdout.split("\n")[0];
|
|
185
|
+
logger.debug(`Main pane ID: ${paneId}`);
|
|
186
|
+
return paneId;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new TmuxError(
|
|
189
|
+
`Failed to get main pane ID for session: ${sessionName}`,
|
|
190
|
+
`tmux list-panes -t ${sessionName} -F '#{pane_id}'`,
|
|
191
|
+
error
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Obtener el ID de la ventana principal de una sesión
|
|
197
|
+
*/
|
|
198
|
+
static async getMainWindowId(sessionName) {
|
|
199
|
+
try {
|
|
200
|
+
logger.debug(`Getting main window ID for session: ${sessionName}`);
|
|
201
|
+
const { stdout } = await execa("tmux", [
|
|
202
|
+
"list-windows",
|
|
203
|
+
"-t",
|
|
204
|
+
sessionName,
|
|
205
|
+
"-F",
|
|
206
|
+
"#{window_id}"
|
|
207
|
+
]);
|
|
208
|
+
const windowId = stdout.split("\n")[0];
|
|
209
|
+
logger.debug(`Main window ID: ${windowId}`);
|
|
210
|
+
return windowId;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new TmuxError(
|
|
213
|
+
`Failed to get main window ID for session: ${sessionName}`,
|
|
214
|
+
`tmux list-windows -t ${sessionName} -F '#{window_id}'`,
|
|
215
|
+
error
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Dividir un pane (crear fork)
|
|
221
|
+
* @param sessionName Nombre de la sesión
|
|
222
|
+
* @param vertical Si es true, divide verticalmente (-h), si es false horizontalmente (-v)
|
|
223
|
+
* @returns ID del nuevo pane creado
|
|
224
|
+
*/
|
|
225
|
+
static async splitPane(sessionName, vertical = false) {
|
|
226
|
+
try {
|
|
227
|
+
const direction = vertical ? "-h" : "-v";
|
|
228
|
+
logger.debug(`Splitting pane in session ${sessionName} (${vertical ? "vertical" : "horizontal"})`);
|
|
229
|
+
await execa("tmux", ["split-window", "-t", sessionName, direction]);
|
|
230
|
+
const { stdout } = await execa("tmux", [
|
|
231
|
+
"list-panes",
|
|
232
|
+
"-t",
|
|
233
|
+
sessionName,
|
|
234
|
+
"-F",
|
|
235
|
+
"#{pane_id}"
|
|
236
|
+
]);
|
|
237
|
+
const panes = stdout.split("\n");
|
|
238
|
+
const newPaneId = panes[panes.length - 1];
|
|
239
|
+
logger.info(`New pane created: ${newPaneId}`);
|
|
240
|
+
return newPaneId;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
throw new TmuxError(
|
|
243
|
+
`Failed to split pane in session: ${sessionName}`,
|
|
244
|
+
`tmux split-window -t ${sessionName} ${vertical ? "-h" : "-v"}`,
|
|
245
|
+
error
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Listar todos los panes de una sesión
|
|
251
|
+
* @param sessionName Nombre de la sesión
|
|
252
|
+
* @returns Array de IDs de panes
|
|
253
|
+
*/
|
|
254
|
+
static async listPanes(sessionName) {
|
|
255
|
+
try {
|
|
256
|
+
const { stdout } = await execa("tmux", [
|
|
257
|
+
"list-panes",
|
|
258
|
+
"-t",
|
|
259
|
+
sessionName,
|
|
260
|
+
"-F",
|
|
261
|
+
"#{pane_id}"
|
|
262
|
+
]);
|
|
263
|
+
return stdout.trim().split("\n").filter(Boolean);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
throw new TmuxError(
|
|
266
|
+
`Failed to list panes for session: ${sessionName}`,
|
|
267
|
+
`tmux list-panes -t ${sessionName}`,
|
|
268
|
+
error
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Cerrar un pane específico
|
|
274
|
+
*/
|
|
275
|
+
static async killPane(paneId) {
|
|
276
|
+
try {
|
|
277
|
+
logger.debug(`Killing pane: ${paneId}`);
|
|
278
|
+
await execa("tmux", ["kill-pane", "-t", paneId]);
|
|
279
|
+
logger.info(`Pane killed: ${paneId}`);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
throw new TmuxError(
|
|
282
|
+
`Failed to kill pane: ${paneId}`,
|
|
283
|
+
`tmux kill-pane -t ${paneId}`,
|
|
284
|
+
error
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Enviar texto a un pane (SIN Enter)
|
|
290
|
+
* IMPORTANTE: No envía Enter, debe llamarse a sendEnter() por separado
|
|
291
|
+
*/
|
|
292
|
+
static async sendKeys(paneId, text) {
|
|
293
|
+
try {
|
|
294
|
+
logger.debug(`Sending keys to pane ${paneId}: ${text.substring(0, 50)}...`);
|
|
295
|
+
await execa("tmux", ["send-keys", "-t", paneId, text]);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw new TmuxError(
|
|
298
|
+
`Failed to send keys to pane: ${paneId}`,
|
|
299
|
+
`tmux send-keys -t ${paneId} "${text}"`,
|
|
300
|
+
error
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Enviar SOLO Enter a un pane
|
|
306
|
+
*/
|
|
307
|
+
static async sendEnter(paneId) {
|
|
308
|
+
try {
|
|
309
|
+
logger.debug(`Sending Enter to pane: ${paneId}`);
|
|
310
|
+
await execa("tmux", ["send-keys", "-t", paneId, "Enter"]);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
throw new TmuxError(
|
|
313
|
+
`Failed to send Enter to pane: ${paneId}`,
|
|
314
|
+
`tmux send-keys -t ${paneId} Enter`,
|
|
315
|
+
error
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Enviar teclas especiales (flechas, escape, etc.)
|
|
321
|
+
* @param paneId ID del pane
|
|
322
|
+
* @param key Nombre de la tecla: 'Up', 'Down', 'Left', 'Right', 'Escape', 'Space', etc.
|
|
323
|
+
*/
|
|
324
|
+
static async sendSpecialKey(paneId, key) {
|
|
325
|
+
try {
|
|
326
|
+
logger.debug(`Sending special key '${key}' to pane: ${paneId}`);
|
|
327
|
+
await execa("tmux", ["send-keys", "-t", paneId, key]);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
throw new TmuxError(
|
|
330
|
+
`Failed to send special key '${key}' to pane: ${paneId}`,
|
|
331
|
+
`tmux send-keys -t ${paneId} ${key}`,
|
|
332
|
+
error
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Capturar el contenido de un pane
|
|
338
|
+
* @param paneId ID del pane
|
|
339
|
+
* @param startLine Línea desde donde empezar a capturar (negativo = desde el final)
|
|
340
|
+
* @returns Contenido del pane
|
|
341
|
+
*/
|
|
342
|
+
static async capturePane(paneId, startLine = -100) {
|
|
343
|
+
try {
|
|
344
|
+
logger.debug(`Capturing pane ${paneId} from line ${startLine}`);
|
|
345
|
+
const { stdout } = await execa("tmux", [
|
|
346
|
+
"capture-pane",
|
|
347
|
+
"-t",
|
|
348
|
+
paneId,
|
|
349
|
+
"-p",
|
|
350
|
+
"-S",
|
|
351
|
+
startLine.toString()
|
|
352
|
+
]);
|
|
353
|
+
return stdout;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
throw new TmuxError(
|
|
356
|
+
`Failed to capture pane: ${paneId}`,
|
|
357
|
+
`tmux capture-pane -t ${paneId} -p -S ${startLine}`,
|
|
358
|
+
error
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Listar todas las sesiones tmux
|
|
364
|
+
*/
|
|
365
|
+
static async listSessions() {
|
|
366
|
+
try {
|
|
367
|
+
const { stdout } = await execa("tmux", ["list-sessions", "-F", "#{session_id}:#{session_name}"]);
|
|
368
|
+
return stdout.split("\n").map((line) => {
|
|
369
|
+
const [id, name] = line.split(":");
|
|
370
|
+
return { id, name };
|
|
371
|
+
});
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (error.stderr?.includes("no server running")) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
throw new TmuxError(
|
|
377
|
+
"Failed to list tmux sessions",
|
|
378
|
+
"tmux list-sessions",
|
|
379
|
+
error
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// src/utils/claude-history.ts
|
|
386
|
+
import fs2 from "fs-extra";
|
|
387
|
+
import os from "os";
|
|
388
|
+
import path2 from "path";
|
|
389
|
+
var CLAUDE_HISTORY_PATH = path2.join(os.homedir(), ".claude", "history.jsonl");
|
|
390
|
+
async function readClaudeHistory() {
|
|
391
|
+
try {
|
|
392
|
+
const exists = await fs2.pathExists(CLAUDE_HISTORY_PATH);
|
|
393
|
+
if (!exists) {
|
|
394
|
+
logger.warn(`Claude history file not found: ${CLAUDE_HISTORY_PATH}`);
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
const content = await fs2.readFile(CLAUDE_HISTORY_PATH, "utf-8");
|
|
398
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
399
|
+
const entries = [];
|
|
400
|
+
for (const line of lines) {
|
|
401
|
+
try {
|
|
402
|
+
const entry = JSON.parse(line);
|
|
403
|
+
entries.push(entry);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
logger.warn(`Failed to parse history line: ${line}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return entries;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
logger.error(`Error reading Claude history: ${error}`);
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function getExistingSessionIds() {
|
|
415
|
+
const entries = await readClaudeHistory();
|
|
416
|
+
return new Set(entries.map((e) => e.sessionId));
|
|
417
|
+
}
|
|
418
|
+
async function detectNewSessionId(previousIds, maxWaitMs = 1e4, pollIntervalMs = 500) {
|
|
419
|
+
const startTime = Date.now();
|
|
420
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
421
|
+
const currentIds = await getExistingSessionIds();
|
|
422
|
+
for (const id of currentIds) {
|
|
423
|
+
if (!previousIds.has(id)) {
|
|
424
|
+
logger.info(`Detected new Claude session: ${id}`);
|
|
425
|
+
return id;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
429
|
+
}
|
|
430
|
+
logger.warn("Timeout waiting for new Claude session ID");
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/core/StateManager.ts
|
|
435
|
+
var StateManager = class {
|
|
436
|
+
projectPath;
|
|
437
|
+
orkaDir;
|
|
438
|
+
statePath;
|
|
439
|
+
constructor(projectPath) {
|
|
440
|
+
this.projectPath = path3.resolve(projectPath);
|
|
441
|
+
this.orkaDir = path3.join(this.projectPath, ".claude-orka");
|
|
442
|
+
this.statePath = path3.join(this.orkaDir, "state.json");
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Initialize StateManager
|
|
446
|
+
* Creates necessary folders if they don't exist
|
|
447
|
+
*/
|
|
448
|
+
async initialize() {
|
|
449
|
+
logger.debug("Initializing StateManager");
|
|
450
|
+
await this.ensureDirectories();
|
|
451
|
+
if (!await fs3.pathExists(this.statePath)) {
|
|
452
|
+
logger.info("Creating initial state.json");
|
|
453
|
+
const initialState = {
|
|
454
|
+
version: "1.0.0",
|
|
455
|
+
projectPath: this.projectPath,
|
|
456
|
+
sessions: [],
|
|
457
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
458
|
+
};
|
|
459
|
+
await this.save(initialState);
|
|
460
|
+
}
|
|
461
|
+
logger.info("StateManager initialized");
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Create directory structure
|
|
465
|
+
*/
|
|
466
|
+
async ensureDirectories() {
|
|
467
|
+
await fs3.ensureDir(this.orkaDir);
|
|
468
|
+
logger.debug("Directories ensured");
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Read current state
|
|
472
|
+
*/
|
|
473
|
+
async read() {
|
|
474
|
+
try {
|
|
475
|
+
const content = await fs3.readFile(this.statePath, "utf-8");
|
|
476
|
+
return JSON.parse(content);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
logger.error("Failed to read state:", error);
|
|
479
|
+
throw new Error(`Failed to read state: ${error.message}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Save state
|
|
484
|
+
*/
|
|
485
|
+
async save(state) {
|
|
486
|
+
try {
|
|
487
|
+
state.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
488
|
+
await fs3.writeFile(this.statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
489
|
+
logger.debug("State saved");
|
|
490
|
+
} catch (error) {
|
|
491
|
+
logger.error("Failed to save state:", error);
|
|
492
|
+
throw new Error(`Failed to save state: ${error.message}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// --- OPERACIONES DE SESIONES ---
|
|
496
|
+
/**
|
|
497
|
+
* Obtener el estado completo
|
|
498
|
+
*/
|
|
499
|
+
async getState() {
|
|
500
|
+
return await this.read();
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Listar todas las sesiones con filtros opcionales
|
|
504
|
+
*/
|
|
505
|
+
async listSessions(filters) {
|
|
506
|
+
const state = await this.read();
|
|
507
|
+
let sessions = state.sessions;
|
|
508
|
+
if (filters?.status) {
|
|
509
|
+
sessions = sessions.filter((s) => s.status === filters.status);
|
|
510
|
+
}
|
|
511
|
+
if (filters?.name) {
|
|
512
|
+
sessions = sessions.filter(
|
|
513
|
+
(s) => s.name.toLowerCase().includes(filters.name.toLowerCase())
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return sessions;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Agregar una nueva sesión
|
|
520
|
+
*/
|
|
521
|
+
async addSession(session) {
|
|
522
|
+
const state = await this.read();
|
|
523
|
+
state.sessions.push(session);
|
|
524
|
+
await this.save(state);
|
|
525
|
+
logger.info(`Session added: ${session.id}`);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Obtener una sesión por ID
|
|
529
|
+
*/
|
|
530
|
+
async getSession(sessionId) {
|
|
531
|
+
const state = await this.read();
|
|
532
|
+
return state.sessions.find((s) => s.id === sessionId) || null;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Obtener todas las sesiones con filtros opcionales
|
|
536
|
+
*/
|
|
537
|
+
async getAllSessions(filters) {
|
|
538
|
+
const state = await this.read();
|
|
539
|
+
let sessions = state.sessions;
|
|
540
|
+
if (filters?.status) {
|
|
541
|
+
sessions = sessions.filter((s) => s.status === filters.status);
|
|
542
|
+
}
|
|
543
|
+
if (filters?.name) {
|
|
544
|
+
sessions = sessions.filter((s) => s.name.includes(filters.name));
|
|
545
|
+
}
|
|
546
|
+
return sessions;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Actualizar el estado de una sesión
|
|
550
|
+
*/
|
|
551
|
+
async updateSessionStatus(sessionId, status) {
|
|
552
|
+
const state = await this.read();
|
|
553
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
554
|
+
if (!session) {
|
|
555
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
556
|
+
}
|
|
557
|
+
session.status = status;
|
|
558
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
559
|
+
await this.save(state);
|
|
560
|
+
logger.info(`Session ${sessionId} status updated to: ${status}`);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Actualizar una sesión completa
|
|
564
|
+
*/
|
|
565
|
+
async updateSession(sessionId, updates) {
|
|
566
|
+
const state = await this.read();
|
|
567
|
+
const sessionIndex = state.sessions.findIndex((s) => s.id === sessionId);
|
|
568
|
+
if (sessionIndex === -1) {
|
|
569
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
570
|
+
}
|
|
571
|
+
state.sessions[sessionIndex] = {
|
|
572
|
+
...state.sessions[sessionIndex],
|
|
573
|
+
...updates,
|
|
574
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString()
|
|
575
|
+
};
|
|
576
|
+
await this.save(state);
|
|
577
|
+
logger.debug(`Session ${sessionId} updated`);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Reemplazar una sesión completa
|
|
581
|
+
*/
|
|
582
|
+
async replaceSession(session) {
|
|
583
|
+
const state = await this.read();
|
|
584
|
+
const sessionIndex = state.sessions.findIndex((s) => s.id === session.id);
|
|
585
|
+
if (sessionIndex === -1) {
|
|
586
|
+
throw new Error(`Session not found: ${session.id}`);
|
|
587
|
+
}
|
|
588
|
+
state.sessions[sessionIndex] = session;
|
|
589
|
+
await this.save(state);
|
|
590
|
+
logger.debug(`Session ${session.id} replaced`);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Eliminar una sesión permanentemente
|
|
594
|
+
*/
|
|
595
|
+
async deleteSession(sessionId) {
|
|
596
|
+
const state = await this.read();
|
|
597
|
+
state.sessions = state.sessions.filter((s) => s.id !== sessionId);
|
|
598
|
+
await this.save(state);
|
|
599
|
+
logger.info(`Session deleted: ${sessionId}`);
|
|
600
|
+
}
|
|
601
|
+
// --- OPERACIONES DE FORKS ---
|
|
602
|
+
/**
|
|
603
|
+
* Agregar un fork a una sesión
|
|
604
|
+
*/
|
|
605
|
+
async addFork(sessionId, fork) {
|
|
606
|
+
const state = await this.read();
|
|
607
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
608
|
+
if (!session) {
|
|
609
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
610
|
+
}
|
|
611
|
+
session.forks.push(fork);
|
|
612
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
613
|
+
await this.save(state);
|
|
614
|
+
logger.info(`Fork added to session ${sessionId}: ${fork.id}`);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Obtener un fork específico
|
|
618
|
+
*/
|
|
619
|
+
async getFork(sessionId, forkId) {
|
|
620
|
+
const session = await this.getSession(sessionId);
|
|
621
|
+
if (!session) return null;
|
|
622
|
+
return session.forks.find((f) => f.id === forkId) || null;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Actualizar el estado de un fork
|
|
626
|
+
*/
|
|
627
|
+
async updateForkStatus(sessionId, forkId, status) {
|
|
628
|
+
const state = await this.read();
|
|
629
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
630
|
+
if (!session) {
|
|
631
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
632
|
+
}
|
|
633
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
634
|
+
if (!fork) {
|
|
635
|
+
throw new Error(`Fork not found: ${forkId}`);
|
|
636
|
+
}
|
|
637
|
+
fork.status = status;
|
|
638
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
639
|
+
await this.save(state);
|
|
640
|
+
logger.info(`Fork ${forkId} status updated to: ${status}`);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Actualizar el path del contexto de un fork
|
|
644
|
+
*/
|
|
645
|
+
async updateForkContext(sessionId, forkId, contextPath) {
|
|
646
|
+
const state = await this.read();
|
|
647
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
648
|
+
if (!session) {
|
|
649
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
650
|
+
}
|
|
651
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
652
|
+
if (!fork) {
|
|
653
|
+
throw new Error(`Fork not found: ${forkId}`);
|
|
654
|
+
}
|
|
655
|
+
fork.contextPath = contextPath;
|
|
656
|
+
await this.save(state);
|
|
657
|
+
logger.debug(`Fork ${forkId} context updated: ${contextPath}`);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Actualizar un fork completo
|
|
661
|
+
*/
|
|
662
|
+
async updateFork(sessionId, forkId, updates) {
|
|
663
|
+
const state = await this.read();
|
|
664
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
665
|
+
if (!session) {
|
|
666
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
667
|
+
}
|
|
668
|
+
const forkIndex = session.forks.findIndex((f) => f.id === forkId);
|
|
669
|
+
if (forkIndex === -1) {
|
|
670
|
+
throw new Error(`Fork not found: ${forkId}`);
|
|
671
|
+
}
|
|
672
|
+
session.forks[forkIndex] = {
|
|
673
|
+
...session.forks[forkIndex],
|
|
674
|
+
...updates
|
|
675
|
+
};
|
|
676
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
677
|
+
await this.save(state);
|
|
678
|
+
logger.debug(`Fork ${forkId} updated`);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Eliminar un fork
|
|
682
|
+
*/
|
|
683
|
+
async deleteFork(sessionId, forkId) {
|
|
684
|
+
const state = await this.read();
|
|
685
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
686
|
+
if (!session) {
|
|
687
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
688
|
+
}
|
|
689
|
+
const forkIndex = session.forks.findIndex((f) => f.id === forkId);
|
|
690
|
+
if (forkIndex === -1) {
|
|
691
|
+
throw new Error(`Fork not found: ${forkId}`);
|
|
692
|
+
}
|
|
693
|
+
session.forks.splice(forkIndex, 1);
|
|
694
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
695
|
+
await this.save(state);
|
|
696
|
+
logger.debug(`Fork ${forkId} deleted`);
|
|
697
|
+
}
|
|
698
|
+
// --- OPERACIONES DE CONTEXTOS ---
|
|
699
|
+
/**
|
|
700
|
+
* Guardar un contexto en archivo
|
|
701
|
+
*/
|
|
702
|
+
async saveContext(type, id, content) {
|
|
703
|
+
const contextPath = type === "session" ? this.getSessionContextPath(id) : this.getForkContextPath(id);
|
|
704
|
+
const fullPath = path3.join(this.projectPath, contextPath);
|
|
705
|
+
await fs3.writeFile(fullPath, content, "utf-8");
|
|
706
|
+
logger.info(`Context saved: ${contextPath}`);
|
|
707
|
+
return contextPath;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Leer un contexto desde archivo
|
|
711
|
+
*/
|
|
712
|
+
async readContext(contextPath) {
|
|
713
|
+
const fullPath = path3.join(this.projectPath, contextPath);
|
|
714
|
+
if (!await fs3.pathExists(fullPath)) {
|
|
715
|
+
throw new Error(`Context file not found: ${contextPath}`);
|
|
716
|
+
}
|
|
717
|
+
return await fs3.readFile(fullPath, "utf-8");
|
|
718
|
+
}
|
|
719
|
+
// --- HELPERS ---
|
|
720
|
+
/**
|
|
721
|
+
* Obtener el path para el contexto de una sesión
|
|
722
|
+
*/
|
|
723
|
+
getSessionContextPath(sessionId) {
|
|
724
|
+
return `.claude-orka/sessions/${sessionId}.md`;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Obtener el path para el contexto de un fork
|
|
728
|
+
*/
|
|
729
|
+
getForkContextPath(forkId) {
|
|
730
|
+
return `.claude-orka/forks/${forkId}.md`;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Obtener el path para un export manual
|
|
734
|
+
*/
|
|
735
|
+
getExportPath(forkId, name) {
|
|
736
|
+
return `.claude-orka/exports/${forkId}-${name}.md`;
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// src/core/SessionManager.ts
|
|
741
|
+
import { v4 as uuidv4 } from "uuid";
|
|
742
|
+
import path4 from "path";
|
|
743
|
+
import { fileURLToPath } from "url";
|
|
744
|
+
import { dirname } from "path";
|
|
745
|
+
import fs4 from "fs-extra";
|
|
746
|
+
import { spawn } from "child_process";
|
|
747
|
+
import { createRequire } from "module";
|
|
748
|
+
var require2 = createRequire(import.meta.url);
|
|
749
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
750
|
+
var __dirname = dirname(__filename);
|
|
751
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
752
|
+
var SessionManager = class {
|
|
753
|
+
stateManager;
|
|
754
|
+
projectPath;
|
|
755
|
+
constructor(projectPath) {
|
|
756
|
+
this.projectPath = projectPath;
|
|
757
|
+
this.stateManager = new StateManager(projectPath);
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Initialize el manager
|
|
761
|
+
*/
|
|
762
|
+
async initialize() {
|
|
763
|
+
await this.stateManager.initialize();
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Obtener el state
|
|
767
|
+
*/
|
|
768
|
+
async getState() {
|
|
769
|
+
return await this.stateManager.getState();
|
|
770
|
+
}
|
|
771
|
+
// ==========================================
|
|
772
|
+
// SESIONES
|
|
773
|
+
// ==========================================
|
|
774
|
+
/**
|
|
775
|
+
* Crear una nueva sesión de Claude Code
|
|
776
|
+
*/
|
|
777
|
+
async createSession(name, openTerminal = true) {
|
|
778
|
+
const sessionId = uuidv4();
|
|
779
|
+
const sessionName = name || `Session-${Date.now()}`;
|
|
780
|
+
const tmuxSessionId = `orka-${sessionId}`;
|
|
781
|
+
logger.info(`Creating session: ${sessionName}`);
|
|
782
|
+
await TmuxCommands.createSession(tmuxSessionId, this.projectPath);
|
|
783
|
+
if (openTerminal) {
|
|
784
|
+
await TmuxCommands.openTerminalWindow(tmuxSessionId);
|
|
785
|
+
}
|
|
786
|
+
await sleep(2e3);
|
|
787
|
+
const paneId = await TmuxCommands.getMainPaneId(tmuxSessionId);
|
|
788
|
+
logger.debug(`Main pane ID: ${paneId}`);
|
|
789
|
+
const claudeSessionId = uuidv4();
|
|
790
|
+
await this.initializeClaude(paneId, {
|
|
791
|
+
type: "new",
|
|
792
|
+
sessionId: claudeSessionId,
|
|
793
|
+
sessionName
|
|
79
794
|
});
|
|
80
|
-
|
|
81
|
-
|
|
795
|
+
const session = {
|
|
796
|
+
id: sessionId,
|
|
797
|
+
name: sessionName,
|
|
798
|
+
tmuxSessionId,
|
|
799
|
+
status: "active",
|
|
800
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
801
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
802
|
+
main: {
|
|
803
|
+
claudeSessionId,
|
|
804
|
+
tmuxPaneId: paneId,
|
|
805
|
+
status: "active"
|
|
806
|
+
},
|
|
807
|
+
forks: []
|
|
808
|
+
};
|
|
809
|
+
await this.stateManager.addSession(session);
|
|
810
|
+
logger.info(`Session created: ${sessionName} (${sessionId})`);
|
|
811
|
+
if (openTerminal) {
|
|
812
|
+
await this.launchUI(sessionId);
|
|
813
|
+
}
|
|
814
|
+
return session;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Restaurar una sesión guardada
|
|
818
|
+
*/
|
|
819
|
+
async resumeSession(sessionId, openTerminal = true) {
|
|
820
|
+
const session = await this.getSession(sessionId);
|
|
821
|
+
if (!session) {
|
|
822
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
823
|
+
}
|
|
824
|
+
logger.info(`Resuming session: ${session.name}`);
|
|
825
|
+
const tmuxSessionId = `orka-${sessionId}`;
|
|
826
|
+
const tmuxExists = await TmuxCommands.sessionExists(tmuxSessionId);
|
|
827
|
+
if (tmuxExists) {
|
|
828
|
+
logger.info(`Tmux session exists, reconnecting...`);
|
|
829
|
+
if (openTerminal) {
|
|
830
|
+
await TmuxCommands.openTerminalWindow(tmuxSessionId);
|
|
831
|
+
await this.launchUI(sessionId);
|
|
832
|
+
}
|
|
833
|
+
if (session.status === "saved") {
|
|
834
|
+
const paneId2 = await TmuxCommands.getMainPaneId(tmuxSessionId);
|
|
835
|
+
session.main.tmuxPaneId = paneId2;
|
|
836
|
+
session.main.status = "active";
|
|
837
|
+
session.status = "active";
|
|
838
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
839
|
+
await this.stateManager.replaceSession(session);
|
|
840
|
+
}
|
|
841
|
+
return session;
|
|
842
|
+
}
|
|
843
|
+
logger.info(`Tmux session not found, recovering from Claude session...`);
|
|
844
|
+
await TmuxCommands.createSession(tmuxSessionId, this.projectPath);
|
|
845
|
+
if (openTerminal) {
|
|
846
|
+
await TmuxCommands.openTerminalWindow(tmuxSessionId);
|
|
847
|
+
}
|
|
848
|
+
await sleep(2e3);
|
|
849
|
+
const paneId = await TmuxCommands.getMainPaneId(tmuxSessionId);
|
|
850
|
+
await this.initializeClaude(paneId, {
|
|
851
|
+
type: "resume",
|
|
852
|
+
resumeSessionId: session.main.claudeSessionId,
|
|
853
|
+
sessionName: session.name
|
|
82
854
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
855
|
+
session.tmuxSessionId = tmuxSessionId;
|
|
856
|
+
session.main.tmuxPaneId = paneId;
|
|
857
|
+
session.main.status = "active";
|
|
858
|
+
session.status = "active";
|
|
859
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
860
|
+
await this.stateManager.replaceSession(session);
|
|
861
|
+
const forksToRestore = session.forks.filter((f) => f.status !== "merged");
|
|
862
|
+
if (forksToRestore.length > 0) {
|
|
863
|
+
logger.info(`Restoring ${forksToRestore.length} fork(s)...`);
|
|
864
|
+
for (const fork of forksToRestore) {
|
|
865
|
+
await this.resumeFork(sessionId, fork.id);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
logger.info(`Session resumed: ${session.name}`);
|
|
869
|
+
if (openTerminal) {
|
|
870
|
+
await this.launchUI(sessionId);
|
|
871
|
+
}
|
|
92
872
|
return session;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (!session)
|
|
102
|
-
return;
|
|
103
|
-
// Get tmux pane ID for the selected node
|
|
104
|
-
let tmuxPaneId;
|
|
105
|
-
if (nodeId === 'main') {
|
|
106
|
-
tmuxPaneId = session.main?.tmuxPaneId;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Close a session (save and kill tmux)
|
|
876
|
+
*/
|
|
877
|
+
async closeSession(sessionId) {
|
|
878
|
+
const session = await this.getSession(sessionId);
|
|
879
|
+
if (!session) {
|
|
880
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
107
881
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
882
|
+
logger.info(`Closing session: ${session.name}`);
|
|
883
|
+
const activeForks = session.forks.filter((f) => f.status === "active");
|
|
884
|
+
for (const fork of activeForks) {
|
|
885
|
+
await this.closeFork(sessionId, fork.id);
|
|
111
886
|
}
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
await execa('tmux', ['select-pane', '-t', tmuxPaneId]);
|
|
887
|
+
if (session.tmuxSessionId) {
|
|
888
|
+
await TmuxCommands.killSession(session.tmuxSessionId);
|
|
115
889
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
890
|
+
session.main.status = "saved";
|
|
891
|
+
session.main.tmuxPaneId = void 0;
|
|
892
|
+
session.status = "saved";
|
|
893
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
894
|
+
await this.stateManager.replaceSession(session);
|
|
895
|
+
logger.info(`Session closed: ${session.name}`);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Eliminar una sesión permanentemente
|
|
899
|
+
*/
|
|
900
|
+
async deleteSession(sessionId) {
|
|
901
|
+
const session = await this.getSession(sessionId);
|
|
902
|
+
if (!session) {
|
|
903
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
904
|
+
}
|
|
905
|
+
logger.info(`Deleting session: ${session.name}`);
|
|
906
|
+
if (session.status === "active") {
|
|
907
|
+
await this.closeSession(sessionId);
|
|
908
|
+
}
|
|
909
|
+
await this.stateManager.deleteSession(sessionId);
|
|
910
|
+
logger.info(`Session deleted: ${session.name}`);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Listar sesiones con filtros opcionales
|
|
914
|
+
*/
|
|
915
|
+
async listSessions(filters) {
|
|
916
|
+
return await this.stateManager.listSessions(filters);
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Obtener una sesión por ID
|
|
920
|
+
*/
|
|
921
|
+
async getSession(sessionId) {
|
|
922
|
+
return await this.stateManager.getSession(sessionId);
|
|
923
|
+
}
|
|
924
|
+
// ==========================================
|
|
925
|
+
// FORKS
|
|
926
|
+
// ==========================================
|
|
927
|
+
/**
|
|
928
|
+
* Crear un fork (rama de conversación)
|
|
929
|
+
*/
|
|
930
|
+
async createFork(sessionId, name, parentId = "main", vertical = false) {
|
|
931
|
+
const session = await this.getSession(sessionId);
|
|
932
|
+
if (!session) {
|
|
933
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
120
934
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
935
|
+
const forkId = uuidv4();
|
|
936
|
+
const forkName = name || `Fork-${session.forks.length + 1}`;
|
|
937
|
+
logger.info(`Creating fork: ${forkName} from parent ${parentId} in session ${session.name}`);
|
|
938
|
+
let parentClaudeSessionId;
|
|
939
|
+
if (parentId === "main") {
|
|
940
|
+
parentClaudeSessionId = session.main.claudeSessionId;
|
|
941
|
+
} else {
|
|
942
|
+
const parentFork = session.forks.find((f) => f.id === parentId);
|
|
943
|
+
if (!parentFork) {
|
|
944
|
+
throw new Error(`Parent fork ${parentId} not found`);
|
|
945
|
+
}
|
|
946
|
+
parentClaudeSessionId = parentFork.claudeSessionId;
|
|
947
|
+
}
|
|
948
|
+
logger.debug(`Parent Claude session ID: ${parentClaudeSessionId}`);
|
|
949
|
+
await TmuxCommands.splitPane(session.tmuxSessionId, vertical);
|
|
950
|
+
await sleep(1e3);
|
|
951
|
+
const allPanes = await TmuxCommands.listPanes(session.tmuxSessionId);
|
|
952
|
+
const forkPaneId = allPanes[allPanes.length - 1];
|
|
953
|
+
logger.debug(`Fork pane ID: ${forkPaneId}`);
|
|
954
|
+
const existingIds = await getExistingSessionIds();
|
|
955
|
+
logger.debug(`Existing sessions before fork: ${existingIds.size}`);
|
|
956
|
+
await this.initializeClaude(forkPaneId, {
|
|
957
|
+
type: "fork",
|
|
958
|
+
parentSessionId: parentClaudeSessionId,
|
|
959
|
+
forkName
|
|
960
|
+
});
|
|
961
|
+
logger.info("Detecting fork session ID from history...");
|
|
962
|
+
const detectedForkId = await detectNewSessionId(existingIds, 3e4, 500);
|
|
963
|
+
if (!detectedForkId) {
|
|
964
|
+
throw new Error(
|
|
965
|
+
"Failed to detect fork session ID. Fork may not have been created. Check if the parent session is valid."
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
logger.info(`Fork session ID detected: ${detectedForkId}`);
|
|
969
|
+
const fork = {
|
|
970
|
+
id: forkId,
|
|
971
|
+
name: forkName,
|
|
972
|
+
parentId,
|
|
973
|
+
claudeSessionId: detectedForkId,
|
|
974
|
+
// ✅ ID real detectado
|
|
975
|
+
tmuxPaneId: forkPaneId,
|
|
976
|
+
status: "active",
|
|
977
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
978
|
+
};
|
|
979
|
+
session.forks.push(fork);
|
|
980
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
981
|
+
await this.stateManager.replaceSession(session);
|
|
982
|
+
logger.info(`Fork created: ${forkName} (${forkId})`);
|
|
124
983
|
return fork;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Restaurar un fork guardado
|
|
987
|
+
*/
|
|
988
|
+
async resumeFork(sessionId, forkId) {
|
|
989
|
+
const session = await this.getSession(sessionId);
|
|
990
|
+
if (!session) {
|
|
991
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
992
|
+
}
|
|
993
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
994
|
+
if (!fork) {
|
|
995
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
996
|
+
}
|
|
997
|
+
logger.info(`Resuming fork: ${fork.name}`);
|
|
998
|
+
if (fork.status === "active" && fork.tmuxPaneId) {
|
|
999
|
+
try {
|
|
1000
|
+
const allPanes2 = await TmuxCommands.listPanes(session.tmuxSessionId);
|
|
1001
|
+
if (allPanes2.includes(fork.tmuxPaneId)) {
|
|
1002
|
+
logger.info(`Fork pane already exists, no need to resume`);
|
|
1003
|
+
return fork;
|
|
1004
|
+
}
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
logger.warn(`Fork was marked active but pane not found, recreating...`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
await TmuxCommands.splitPane(session.tmuxSessionId, false);
|
|
1010
|
+
await sleep(1e3);
|
|
1011
|
+
const allPanes = await TmuxCommands.listPanes(session.tmuxSessionId);
|
|
1012
|
+
const forkPaneId = allPanes[allPanes.length - 1];
|
|
1013
|
+
await this.initializeClaude(forkPaneId, {
|
|
1014
|
+
type: "resume",
|
|
1015
|
+
resumeSessionId: fork.claudeSessionId,
|
|
1016
|
+
sessionName: fork.name
|
|
1017
|
+
});
|
|
1018
|
+
fork.tmuxPaneId = forkPaneId;
|
|
1019
|
+
fork.status = "active";
|
|
1020
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1021
|
+
await this.stateManager.replaceSession(session);
|
|
1022
|
+
logger.info(`Fork resumed: ${fork.name}`);
|
|
1023
|
+
return fork;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Cerrar un fork
|
|
1027
|
+
*/
|
|
1028
|
+
async closeFork(sessionId, forkId) {
|
|
1029
|
+
const session = await this.getSession(sessionId);
|
|
1030
|
+
if (!session) {
|
|
1031
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1032
|
+
}
|
|
1033
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
1034
|
+
if (!fork) {
|
|
1035
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
1036
|
+
}
|
|
1037
|
+
logger.info(`Closing fork: ${fork.name}`);
|
|
1038
|
+
if (fork.tmuxPaneId) {
|
|
1039
|
+
await TmuxCommands.killPane(fork.tmuxPaneId);
|
|
1040
|
+
}
|
|
1041
|
+
fork.status = "closed";
|
|
1042
|
+
fork.tmuxPaneId = void 0;
|
|
1043
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1044
|
+
await this.stateManager.replaceSession(session);
|
|
1045
|
+
logger.info(`Fork closed: ${fork.name}`);
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Eliminar un fork permanentemente
|
|
1049
|
+
*/
|
|
1050
|
+
async deleteFork(sessionId, forkId) {
|
|
1051
|
+
const session = await this.getSession(sessionId);
|
|
1052
|
+
if (!session) {
|
|
1053
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1054
|
+
}
|
|
1055
|
+
const forkIndex = session.forks.findIndex((f) => f.id === forkId);
|
|
1056
|
+
if (forkIndex === -1) {
|
|
1057
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
1058
|
+
}
|
|
1059
|
+
const fork = session.forks[forkIndex];
|
|
1060
|
+
logger.info(`Deleting fork: ${fork.name}`);
|
|
1061
|
+
if (fork.status === "active") {
|
|
1062
|
+
await this.closeFork(sessionId, forkId);
|
|
1063
|
+
}
|
|
1064
|
+
session.forks.splice(forkIndex, 1);
|
|
1065
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1066
|
+
await this.stateManager.replaceSession(session);
|
|
1067
|
+
logger.info(`Fork deleted: ${fork.name}`);
|
|
1068
|
+
}
|
|
1069
|
+
// ==========================================
|
|
1070
|
+
// COMANDOS
|
|
1071
|
+
// ==========================================
|
|
1072
|
+
/**
|
|
1073
|
+
* Enviar comando a main
|
|
1074
|
+
*/
|
|
1075
|
+
async sendToMain(sessionId, command) {
|
|
1076
|
+
const session = await this.getSession(sessionId);
|
|
1077
|
+
if (!session) {
|
|
1078
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1079
|
+
}
|
|
1080
|
+
if (!session.main.tmuxPaneId) {
|
|
1081
|
+
throw new Error("Main pane is not active");
|
|
1082
|
+
}
|
|
1083
|
+
logger.info(`Sending command to main: ${command}`);
|
|
1084
|
+
await TmuxCommands.sendKeys(session.main.tmuxPaneId, command);
|
|
1085
|
+
await TmuxCommands.sendEnter(session.main.tmuxPaneId);
|
|
1086
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1087
|
+
await this.stateManager.replaceSession(session);
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Enviar comando a un fork
|
|
1091
|
+
*/
|
|
1092
|
+
async sendToFork(sessionId, forkId, command) {
|
|
1093
|
+
const session = await this.getSession(sessionId);
|
|
1094
|
+
if (!session) {
|
|
1095
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1096
|
+
}
|
|
1097
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
1098
|
+
if (!fork) {
|
|
1099
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
1100
|
+
}
|
|
1101
|
+
if (!fork.tmuxPaneId) {
|
|
1102
|
+
throw new Error("Fork pane is not active");
|
|
1103
|
+
}
|
|
1104
|
+
logger.info(`Sending command to fork ${fork.name}: ${command}`);
|
|
1105
|
+
await TmuxCommands.sendKeys(fork.tmuxPaneId, command);
|
|
1106
|
+
await TmuxCommands.sendEnter(fork.tmuxPaneId);
|
|
1107
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1108
|
+
await this.stateManager.replaceSession(session);
|
|
1109
|
+
}
|
|
1110
|
+
// ==========================================
|
|
1111
|
+
// EXPORT & MERGE
|
|
1112
|
+
// ==========================================
|
|
1113
|
+
/**
|
|
1114
|
+
* Generar export de un fork con resumen
|
|
1115
|
+
* Envía un prompt a Claude pidiendo que genere resumen y exporte
|
|
1116
|
+
*/
|
|
1117
|
+
async generateForkExport(sessionId, forkId) {
|
|
1118
|
+
const session = await this.getSession(sessionId);
|
|
1119
|
+
if (!session) {
|
|
1120
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1121
|
+
}
|
|
1122
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
1123
|
+
if (!fork) {
|
|
1124
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
1125
|
+
}
|
|
1126
|
+
logger.info(`Generating export for fork: ${fork.name}`);
|
|
1127
|
+
const exportsDir = path4.join(this.projectPath, ".claude-orka", "exports");
|
|
1128
|
+
await fs4.ensureDir(exportsDir);
|
|
1129
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1130
|
+
const exportName = `fork-${fork.name}-${timestamp}.md`;
|
|
1131
|
+
const relativeExportPath = `.claude-orka/exports/${exportName}`;
|
|
1132
|
+
const absoluteExportPath = path4.join(this.projectPath, relativeExportPath);
|
|
1133
|
+
const prompt = `
|
|
1134
|
+
Please generate a complete summary of this fork conversation "${fork.name}" and save it to the file:
|
|
1135
|
+
\`${absoluteExportPath}\`
|
|
1136
|
+
|
|
1137
|
+
The summary should include:
|
|
1138
|
+
|
|
1139
|
+
## Executive Summary
|
|
1140
|
+
- What was attempted to achieve in this fork
|
|
1141
|
+
- Why this exploration branch was created
|
|
1142
|
+
|
|
1143
|
+
## Changes Made
|
|
1144
|
+
- Detailed list of changes, modified files, written code
|
|
1145
|
+
- Technical decisions made
|
|
1146
|
+
|
|
1147
|
+
## Results
|
|
1148
|
+
- What works correctly
|
|
1149
|
+
- What problems were encountered
|
|
1150
|
+
- What remains pending
|
|
1151
|
+
|
|
1152
|
+
## Recommendations
|
|
1153
|
+
- Suggested next steps
|
|
1154
|
+
- How to integrate this to main
|
|
1155
|
+
- Important considerations
|
|
1156
|
+
|
|
1157
|
+
Write the summary in Markdown format and save it to the specified file.
|
|
1158
|
+
`.trim();
|
|
1159
|
+
if (!fork.tmuxPaneId) {
|
|
1160
|
+
throw new Error("Fork pane is not active. Cannot send export command.");
|
|
1161
|
+
}
|
|
1162
|
+
await TmuxCommands.sendKeys(fork.tmuxPaneId, prompt);
|
|
1163
|
+
await TmuxCommands.sendEnter(fork.tmuxPaneId);
|
|
1164
|
+
fork.contextPath = relativeExportPath;
|
|
1165
|
+
await this.stateManager.replaceSession(session);
|
|
1166
|
+
logger.info(`Export generation requested`);
|
|
1167
|
+
logger.info(` Filename: ${exportName}`);
|
|
1168
|
+
logger.info(` Relative path (saved in state): ${relativeExportPath}`);
|
|
1169
|
+
logger.info(` Absolute path (sent to Claude): ${absoluteExportPath}`);
|
|
1170
|
+
logger.warn("IMPORTANT: Wait for Claude to complete before calling merge()");
|
|
1171
|
+
return relativeExportPath;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Hacer merge de un fork a main
|
|
1175
|
+
* PREREQUISITO: Debes llamar a generateForkExport() primero y esperar
|
|
1176
|
+
*/
|
|
1177
|
+
async mergeFork(sessionId, forkId) {
|
|
1178
|
+
const session = await this.getSession(sessionId);
|
|
1179
|
+
if (!session) {
|
|
1180
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1181
|
+
}
|
|
1182
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
1183
|
+
if (!fork) {
|
|
1184
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
1185
|
+
}
|
|
1186
|
+
if (!fork.contextPath) {
|
|
1187
|
+
throw new Error(
|
|
1188
|
+
"Fork does not have an exported context. Call generateForkExport() first."
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
const parentId = fork.parentId;
|
|
1192
|
+
const parentName = parentId === "main" ? "MAIN" : session.forks.find((f) => f.id === parentId)?.name || parentId;
|
|
1193
|
+
logger.info(`Merging fork ${fork.name} to parent ${parentName}`);
|
|
1194
|
+
let parentTmuxPaneId;
|
|
1195
|
+
if (parentId === "main") {
|
|
1196
|
+
parentTmuxPaneId = session.main.tmuxPaneId;
|
|
1197
|
+
} else {
|
|
1198
|
+
const parentFork = session.forks.find((f) => f.id === parentId);
|
|
1199
|
+
if (!parentFork) {
|
|
1200
|
+
throw new Error(`Parent fork ${parentId} not found`);
|
|
1201
|
+
}
|
|
1202
|
+
parentTmuxPaneId = parentFork.tmuxPaneId;
|
|
1203
|
+
}
|
|
1204
|
+
if (!parentTmuxPaneId) {
|
|
1205
|
+
throw new Error(`Parent ${parentName} is not active. Cannot send merge command.`);
|
|
1206
|
+
}
|
|
1207
|
+
let contextPath = fork.contextPath;
|
|
1208
|
+
let fullPath = path4.join(this.projectPath, contextPath);
|
|
1209
|
+
let exists = await fs4.pathExists(fullPath);
|
|
1210
|
+
if (!exists) {
|
|
1211
|
+
logger.warn(`Export file not found: ${contextPath}. Looking for most recent export...`);
|
|
1212
|
+
const exportsDir = path4.join(this.projectPath, ".claude-orka", "exports");
|
|
1213
|
+
const files = await fs4.readdir(exportsDir);
|
|
1214
|
+
const forkExports = files.filter((f) => f.startsWith(`fork-${fork.name}-`) && f.endsWith(".md")).sort().reverse();
|
|
1215
|
+
if (forkExports.length > 0) {
|
|
1216
|
+
contextPath = `.claude-orka/exports/${forkExports[0]}`;
|
|
1217
|
+
fullPath = path4.join(this.projectPath, contextPath);
|
|
1218
|
+
exists = await fs4.pathExists(fullPath);
|
|
1219
|
+
logger.info(`Using most recent export: ${contextPath}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (!exists) {
|
|
1223
|
+
throw new Error(
|
|
1224
|
+
`No export file found for fork "${fork.name}". Please run Export first and wait for Claude to complete.`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
const mergePrompt = `
|
|
1228
|
+
I have completed work on the fork "${fork.name}".
|
|
1229
|
+
Please read the file \`${contextPath}\` which contains:
|
|
1230
|
+
1. An executive summary of the work completed
|
|
1231
|
+
2. The complete context of the fork conversation
|
|
1232
|
+
|
|
1233
|
+
Analyze the content and help me integrate the changes and learnings from the fork into this conversation.
|
|
1234
|
+
`.trim();
|
|
1235
|
+
await TmuxCommands.sendKeys(parentTmuxPaneId, mergePrompt);
|
|
1236
|
+
await TmuxCommands.sendEnter(parentTmuxPaneId);
|
|
1237
|
+
fork.status = "merged";
|
|
1238
|
+
fork.mergedToMain = true;
|
|
1239
|
+
fork.mergedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1240
|
+
if (fork.tmuxPaneId) {
|
|
1241
|
+
await TmuxCommands.killPane(fork.tmuxPaneId);
|
|
1242
|
+
fork.tmuxPaneId = void 0;
|
|
1243
|
+
}
|
|
1244
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1245
|
+
await this.stateManager.replaceSession(session);
|
|
1246
|
+
logger.info(`Fork ${fork.name} merged to main`);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Export manual de un fork (deprecated - usa generateForkExport)
|
|
1250
|
+
*/
|
|
1251
|
+
async exportFork(sessionId, forkId) {
|
|
1252
|
+
logger.warn("exportFork() is deprecated. Use generateForkExport() instead.");
|
|
1253
|
+
return await this.generateForkExport(sessionId, forkId);
|
|
1254
|
+
}
|
|
1255
|
+
// ==========================================
|
|
1256
|
+
// HELPERS PRIVADOS
|
|
1257
|
+
// ==========================================
|
|
1258
|
+
/**
|
|
1259
|
+
* Initialize Claude en un pane con prompt inicial
|
|
1260
|
+
*/
|
|
1261
|
+
async initializeClaude(paneId, options) {
|
|
1262
|
+
const { type, sessionId, resumeSessionId, parentSessionId, sessionName, forkName } = options;
|
|
1263
|
+
await TmuxCommands.sendKeys(paneId, `cd ${this.projectPath}`);
|
|
1264
|
+
await TmuxCommands.sendEnter(paneId);
|
|
1265
|
+
await sleep(500);
|
|
1266
|
+
let command = "";
|
|
1267
|
+
switch (type) {
|
|
1268
|
+
case "new":
|
|
1269
|
+
const newPrompt = `Hello, this is a new main session called "${sessionName}". We are working on the project.`;
|
|
1270
|
+
command = `claude --session-id ${sessionId} "${newPrompt}"`;
|
|
1271
|
+
break;
|
|
1272
|
+
case "resume":
|
|
1273
|
+
const resumePrompt = `Resuming session "${sessionName}".`;
|
|
1274
|
+
command = `claude --resume ${resumeSessionId} "${resumePrompt}"`;
|
|
1275
|
+
break;
|
|
1276
|
+
case "fork":
|
|
1277
|
+
const forkPrompt = `This is a fork called "${forkName}". Keep in mind we are exploring an alternative to the main conversation.`;
|
|
1278
|
+
command = `claude --resume ${parentSessionId} --fork-session "${forkPrompt}"`;
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
logger.info(`Executing: ${command}`);
|
|
1282
|
+
await TmuxCommands.sendKeys(paneId, command);
|
|
1283
|
+
await TmuxCommands.sendEnter(paneId);
|
|
1284
|
+
await sleep(8e3);
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Launch Electron UI for a session
|
|
1288
|
+
*/
|
|
1289
|
+
async launchUI(sessionId) {
|
|
1290
|
+
try {
|
|
1291
|
+
let electronPath;
|
|
1292
|
+
try {
|
|
1293
|
+
electronPath = require2("electron");
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
logger.warn("Electron not available, skipping UI launch");
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
let mainPath = path4.join(__dirname, "../../electron/main/main.js");
|
|
1299
|
+
if (!fs4.existsSync(mainPath)) {
|
|
1300
|
+
mainPath = path4.join(__dirname, "../../dist/electron/main/main.js");
|
|
1301
|
+
}
|
|
1302
|
+
if (!fs4.existsSync(mainPath)) {
|
|
1303
|
+
logger.warn(`Electron main.js not found at ${mainPath}, skipping UI launch`);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const electronProcess = spawn(
|
|
1307
|
+
electronPath,
|
|
1308
|
+
[mainPath, "--session-id", sessionId, "--project-path", this.projectPath],
|
|
1309
|
+
{
|
|
1310
|
+
detached: true,
|
|
1311
|
+
stdio: "ignore"
|
|
1312
|
+
}
|
|
1313
|
+
);
|
|
1314
|
+
electronProcess.unref();
|
|
1315
|
+
logger.info(`Launched UI for session ${sessionId}`);
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
logger.warn(`Failed to launch UI: ${error}`);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
// src/core/ClaudeOrka.ts
|
|
1323
|
+
var ClaudeOrka = class {
|
|
1324
|
+
sessionManager;
|
|
1325
|
+
/**
|
|
1326
|
+
* Create a ClaudeOrka instance
|
|
1327
|
+
* @param projectPath Absolute path to the project
|
|
1328
|
+
*/
|
|
1329
|
+
constructor(projectPath) {
|
|
1330
|
+
logger.setLogFile(projectPath);
|
|
1331
|
+
this.sessionManager = new SessionManager(projectPath);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Initialize ClaudeOrka
|
|
1335
|
+
* Creates the .claude-orka/ structure if it doesn't exist
|
|
1336
|
+
*/
|
|
1337
|
+
async initialize() {
|
|
1338
|
+
logger.info("Initializing ClaudeOrka");
|
|
1339
|
+
await this.sessionManager.initialize();
|
|
1340
|
+
}
|
|
1341
|
+
// --- SESSIONS ---
|
|
1342
|
+
/**
|
|
1343
|
+
* Create a new Claude Code session
|
|
1344
|
+
* @param name Optional name for the session
|
|
1345
|
+
* @param openTerminal Whether to open a terminal window (default: true)
|
|
1346
|
+
* @returns Created session
|
|
1347
|
+
*/
|
|
1348
|
+
async createSession(name, openTerminal) {
|
|
1349
|
+
return await this.sessionManager.createSession(name, openTerminal);
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Resume a saved session
|
|
1353
|
+
* @param sessionId Session ID to resume
|
|
1354
|
+
* @param openTerminal Whether to open a terminal window (default: true)
|
|
1355
|
+
* @returns Resumed session
|
|
1356
|
+
*/
|
|
1357
|
+
async resumeSession(sessionId, openTerminal) {
|
|
1358
|
+
return await this.sessionManager.resumeSession(sessionId, openTerminal);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Close a session
|
|
1362
|
+
* @param sessionId Session ID
|
|
1363
|
+
*/
|
|
1364
|
+
async closeSession(sessionId) {
|
|
1365
|
+
await this.sessionManager.closeSession(sessionId);
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Permanently delete a session
|
|
1369
|
+
* @param sessionId Session ID
|
|
1370
|
+
*/
|
|
1371
|
+
async deleteSession(sessionId) {
|
|
1372
|
+
await this.sessionManager.deleteSession(sessionId);
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* List sessions with optional filters
|
|
1376
|
+
* @param filters Optional filters (status, name)
|
|
1377
|
+
* @returns Array of sessions
|
|
1378
|
+
*/
|
|
1379
|
+
async listSessions(filters) {
|
|
1380
|
+
return await this.sessionManager.listSessions(filters);
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Get a session by ID
|
|
1384
|
+
* @param sessionId Session ID
|
|
1385
|
+
* @returns Session or null if not found
|
|
1386
|
+
*/
|
|
1387
|
+
async getSession(sessionId) {
|
|
1388
|
+
return await this.sessionManager.getSession(sessionId);
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Get complete project summary
|
|
1392
|
+
* Includes statistics of all sessions and their forks
|
|
1393
|
+
* @returns Project summary with all sessions and statistics
|
|
1394
|
+
*/
|
|
1395
|
+
async getProjectSummary() {
|
|
1396
|
+
const sessions = await this.sessionManager.listSessions();
|
|
1397
|
+
const state = await this.sessionManager.getState();
|
|
1398
|
+
const sessionSummaries = sessions.map((session) => {
|
|
1399
|
+
const forkSummaries = session.forks.map((fork) => ({
|
|
1400
|
+
id: fork.id,
|
|
1401
|
+
name: fork.name,
|
|
1402
|
+
claudeSessionId: fork.claudeSessionId,
|
|
1403
|
+
status: fork.status,
|
|
1404
|
+
createdAt: fork.createdAt,
|
|
1405
|
+
hasContext: !!fork.contextPath,
|
|
1406
|
+
contextPath: fork.contextPath,
|
|
1407
|
+
mergedToMain: fork.mergedToMain || false,
|
|
1408
|
+
mergedAt: fork.mergedAt
|
|
1409
|
+
}));
|
|
1410
|
+
const activeForks = session.forks.filter((f) => f.status === "active").length;
|
|
1411
|
+
const savedForks = session.forks.filter((f) => f.status === "saved").length;
|
|
1412
|
+
const mergedForks = session.forks.filter((f) => f.status === "merged").length;
|
|
1413
|
+
return {
|
|
1414
|
+
id: session.id,
|
|
1415
|
+
name: session.name,
|
|
1416
|
+
claudeSessionId: session.main.claudeSessionId,
|
|
1417
|
+
status: session.status,
|
|
1418
|
+
createdAt: session.createdAt,
|
|
1419
|
+
lastActivity: session.lastActivity,
|
|
1420
|
+
totalForks: session.forks.length,
|
|
1421
|
+
activeForks,
|
|
1422
|
+
savedForks,
|
|
1423
|
+
mergedForks,
|
|
1424
|
+
forks: forkSummaries
|
|
1425
|
+
};
|
|
1426
|
+
});
|
|
1427
|
+
const activeSessions = sessions.filter((s) => s.status === "active").length;
|
|
1428
|
+
const savedSessions = sessions.filter((s) => s.status === "saved").length;
|
|
1429
|
+
return {
|
|
1430
|
+
projectPath: state.projectPath,
|
|
1431
|
+
totalSessions: sessions.length,
|
|
1432
|
+
activeSessions,
|
|
1433
|
+
savedSessions,
|
|
1434
|
+
sessions: sessionSummaries,
|
|
1435
|
+
lastUpdated: state.lastUpdated
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
// --- FORKS ---
|
|
1439
|
+
/**
|
|
1440
|
+
* Create a fork (conversation branch)
|
|
1441
|
+
* @param sessionId Session ID
|
|
1442
|
+
* @param name Optional fork name
|
|
1443
|
+
* @param parentId Parent fork/session ID (default: 'main')
|
|
1444
|
+
* @param vertical Whether to split vertically (default: false = horizontal)
|
|
1445
|
+
* @returns Created fork
|
|
1446
|
+
*/
|
|
1447
|
+
async createFork(sessionId, name, parentId = "main", vertical) {
|
|
1448
|
+
return await this.sessionManager.createFork(sessionId, name, parentId, vertical);
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Close a fork
|
|
1452
|
+
* @param sessionId Session ID
|
|
1453
|
+
* @param forkId Fork ID
|
|
1454
|
+
*/
|
|
1455
|
+
async closeFork(sessionId, forkId) {
|
|
1456
|
+
await this.sessionManager.closeFork(sessionId, forkId);
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Resume a saved fork
|
|
1460
|
+
* @param sessionId Session ID
|
|
1461
|
+
* @param forkId Fork ID
|
|
1462
|
+
* @returns Resumed fork
|
|
1463
|
+
*/
|
|
1464
|
+
async resumeFork(sessionId, forkId) {
|
|
1465
|
+
return await this.sessionManager.resumeFork(sessionId, forkId);
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Permanently delete a fork
|
|
1469
|
+
* @param sessionId Session ID
|
|
1470
|
+
* @param forkId Fork ID
|
|
1471
|
+
*/
|
|
1472
|
+
async deleteFork(sessionId, forkId) {
|
|
1473
|
+
await this.sessionManager.deleteFork(sessionId, forkId);
|
|
1474
|
+
}
|
|
1475
|
+
// --- COMANDOS ---
|
|
1476
|
+
/**
|
|
1477
|
+
* Send command to a session or fork
|
|
1478
|
+
* @param sessionId Session ID
|
|
1479
|
+
* @param command Command to send
|
|
1480
|
+
* @param target Fork ID (optional, if not specified goes to main)
|
|
1481
|
+
*/
|
|
1482
|
+
async send(sessionId, command, target) {
|
|
1483
|
+
if (target) {
|
|
1484
|
+
await this.sessionManager.sendToFork(sessionId, target, command);
|
|
1485
|
+
} else {
|
|
1486
|
+
await this.sessionManager.sendToMain(sessionId, command);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
// --- EXPORT & MERGE ---
|
|
1490
|
+
/**
|
|
1491
|
+
* Export fork context (old method - uses manual capture)
|
|
1492
|
+
* @deprecated Use generateForkExport() instead for Claude to generate the summary
|
|
1493
|
+
* @param sessionId Session ID
|
|
1494
|
+
* @param forkId Fork ID
|
|
1495
|
+
* @returns Path to the exported file
|
|
1496
|
+
*/
|
|
1497
|
+
async export(sessionId, forkId) {
|
|
1498
|
+
return await this.sessionManager.exportFork(sessionId, forkId);
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Generate fork export with summary
|
|
1502
|
+
*
|
|
1503
|
+
* Sends a prompt to Claude requesting:
|
|
1504
|
+
* 1. Generate executive summary of the conversation
|
|
1505
|
+
* 2. Export using /export to the specified path
|
|
1506
|
+
*
|
|
1507
|
+
* IMPORTANT: This method is async but returns immediately.
|
|
1508
|
+
* Claude will execute tasks in the background. Wait a few seconds before calling merge().
|
|
1509
|
+
*
|
|
1510
|
+
* @param sessionId Session ID
|
|
1511
|
+
* @param forkId Fork ID
|
|
1512
|
+
* @returns Path where Claude will save the export
|
|
1513
|
+
*/
|
|
1514
|
+
async generateForkExport(sessionId, forkId) {
|
|
1515
|
+
return await this.sessionManager.generateForkExport(sessionId, forkId);
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Merge a fork to main
|
|
1519
|
+
*
|
|
1520
|
+
* PREREQUISITE: You must call generateForkExport() first and wait for Claude to complete
|
|
1521
|
+
*
|
|
1522
|
+
* @param sessionId Session ID
|
|
1523
|
+
* @param forkId Fork ID
|
|
1524
|
+
*/
|
|
1525
|
+
async merge(sessionId, forkId) {
|
|
1526
|
+
await this.sessionManager.mergeFork(sessionId, forkId);
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Generate export and merge a fork to main (recommended method)
|
|
1530
|
+
*
|
|
1531
|
+
* Workflow:
|
|
1532
|
+
* 1. Generates export with summary (Claude does the work)
|
|
1533
|
+
* 2. Wait for the file to be created
|
|
1534
|
+
* 3. Merge to main
|
|
1535
|
+
*
|
|
1536
|
+
* @param sessionId Session ID
|
|
1537
|
+
* @param forkId Fork ID
|
|
1538
|
+
* @param waitTime Wait time in ms for Claude to complete (default: 15000)
|
|
1539
|
+
*/
|
|
1540
|
+
async generateExportAndMerge(sessionId, forkId, waitTime = 15e3) {
|
|
1541
|
+
await this.generateForkExport(sessionId, forkId);
|
|
1542
|
+
logger.info(`Waiting ${waitTime}ms for Claude to complete export...`);
|
|
1543
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
1544
|
+
await this.merge(sessionId, forkId);
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Generate export, merge and close a fork (complete flow)
|
|
1548
|
+
*
|
|
1549
|
+
* @param sessionId Session ID
|
|
1550
|
+
* @param forkId Fork ID
|
|
1551
|
+
* @param waitTime Wait time in ms for Claude to complete (default: 15000)
|
|
1552
|
+
*/
|
|
1553
|
+
async generateExportMergeAndClose(sessionId, forkId, waitTime = 15e3) {
|
|
1554
|
+
await this.generateExportAndMerge(sessionId, forkId, waitTime);
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Export and merge a fork to main (old method)
|
|
1558
|
+
* @deprecated Usa generateExportAndMerge() en su lugar
|
|
1559
|
+
*/
|
|
1560
|
+
async exportAndMerge(sessionId, forkId) {
|
|
1561
|
+
await this.export(sessionId, forkId);
|
|
1562
|
+
await this.merge(sessionId, forkId);
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Export, merge and close a fork (old method)
|
|
1566
|
+
* @deprecated Usa generateExportMergeAndClose() en su lugar
|
|
1567
|
+
*/
|
|
1568
|
+
async mergeAndClose(sessionId, forkId) {
|
|
1569
|
+
await this.exportAndMerge(sessionId, forkId);
|
|
1570
|
+
await this.closeFork(sessionId, forkId);
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
// electron/main/main.ts
|
|
1575
|
+
import chokidar from "chokidar";
|
|
1576
|
+
import execa2 from "execa";
|
|
1577
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
1578
|
+
var __dirname2 = path5.dirname(__filename2);
|
|
1579
|
+
var windows = /* @__PURE__ */ new Map();
|
|
1580
|
+
var currentSessionId = null;
|
|
1581
|
+
var currentProjectPath = null;
|
|
1582
|
+
function createWindow(sessionId, projectPath) {
|
|
1583
|
+
if (windows.has(projectPath)) {
|
|
1584
|
+
const existingWindow = windows.get(projectPath);
|
|
1585
|
+
existingWindow.focus();
|
|
1586
|
+
return existingWindow;
|
|
1587
|
+
}
|
|
1588
|
+
const projectName = path5.basename(projectPath);
|
|
1589
|
+
const mainWindow = new BrowserWindow({
|
|
1590
|
+
width: 600,
|
|
1591
|
+
height: 800,
|
|
1592
|
+
minWidth: 500,
|
|
1593
|
+
minHeight: 600,
|
|
1594
|
+
frame: false,
|
|
1595
|
+
transparent: true,
|
|
1596
|
+
alwaysOnTop: true,
|
|
1597
|
+
resizable: true,
|
|
1598
|
+
title: `Claude Orka - ${projectName}`,
|
|
1599
|
+
webPreferences: {
|
|
1600
|
+
preload: path5.join(__dirname2, "../preload/preload.js"),
|
|
1601
|
+
contextIsolation: true,
|
|
1602
|
+
nodeIntegration: false
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
currentSessionId = sessionId;
|
|
1606
|
+
currentProjectPath = projectPath;
|
|
1607
|
+
if (process.env.NODE_ENV === "development") {
|
|
1608
|
+
mainWindow.loadURL("http://localhost:5173");
|
|
1609
|
+
} else {
|
|
1610
|
+
const indexPath = path5.join(__dirname2, "../renderer/index.html");
|
|
1611
|
+
mainWindow.loadFile(indexPath);
|
|
1612
|
+
}
|
|
1613
|
+
watchStateFile(projectPath, mainWindow);
|
|
1614
|
+
windows.set(projectPath, mainWindow);
|
|
1615
|
+
mainWindow.on("closed", () => {
|
|
1616
|
+
windows.delete(projectPath);
|
|
1617
|
+
});
|
|
1618
|
+
return mainWindow;
|
|
1619
|
+
}
|
|
1620
|
+
function watchStateFile(projectPath, window) {
|
|
1621
|
+
const statePath = path5.join(projectPath, ".claude-orka/state.json");
|
|
1622
|
+
const watcher = chokidar.watch(statePath, {
|
|
1623
|
+
persistent: true,
|
|
1624
|
+
ignoreInitial: true
|
|
1625
|
+
});
|
|
1626
|
+
watcher.on("change", async () => {
|
|
1627
|
+
try {
|
|
1628
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1629
|
+
await orka.initialize();
|
|
1630
|
+
if (currentSessionId) {
|
|
1631
|
+
const session = await orka.getSession(currentSessionId);
|
|
1632
|
+
if (session) {
|
|
1633
|
+
window.webContents.send("state-updated", session);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
console.error("Error watching state file:", error);
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
window.on("closed", () => {
|
|
1641
|
+
watcher.close();
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
ipcMain.handle("get-session", async () => {
|
|
1645
|
+
if (!currentSessionId || !currentProjectPath) {
|
|
1646
|
+
throw new Error("No active session");
|
|
1647
|
+
}
|
|
1648
|
+
const orka = new ClaudeOrka(currentProjectPath);
|
|
1649
|
+
await orka.initialize();
|
|
1650
|
+
const session = await orka.getSession(currentSessionId);
|
|
1651
|
+
return session;
|
|
125
1652
|
});
|
|
126
|
-
ipcMain.handle(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
1653
|
+
ipcMain.handle("select-node", async (_, nodeId) => {
|
|
1654
|
+
if (!currentSessionId || !currentProjectPath) {
|
|
1655
|
+
throw new Error("No active session");
|
|
1656
|
+
}
|
|
1657
|
+
const orka = new ClaudeOrka(currentProjectPath);
|
|
1658
|
+
await orka.initialize();
|
|
1659
|
+
const session = await orka.getSession(currentSessionId);
|
|
1660
|
+
if (!session) return;
|
|
1661
|
+
let tmuxPaneId;
|
|
1662
|
+
if (nodeId === "main") {
|
|
1663
|
+
tmuxPaneId = session.main?.tmuxPaneId;
|
|
1664
|
+
} else {
|
|
1665
|
+
const fork = session.forks.find((f) => f.id === nodeId);
|
|
1666
|
+
tmuxPaneId = fork?.tmuxPaneId;
|
|
1667
|
+
}
|
|
1668
|
+
if (tmuxPaneId) {
|
|
1669
|
+
await execa2("tmux", ["select-pane", "-t", tmuxPaneId]);
|
|
1670
|
+
}
|
|
134
1671
|
});
|
|
135
|
-
ipcMain.handle(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
1672
|
+
ipcMain.handle("create-fork", async (_, sessionId, name, parentId) => {
|
|
1673
|
+
if (!currentProjectPath) {
|
|
1674
|
+
throw new Error("No active project");
|
|
1675
|
+
}
|
|
1676
|
+
const orka = new ClaudeOrka(currentProjectPath);
|
|
1677
|
+
await orka.initialize();
|
|
1678
|
+
const fork = await orka.createFork(sessionId, name, parentId);
|
|
1679
|
+
return fork;
|
|
142
1680
|
});
|
|
143
|
-
ipcMain.
|
|
144
|
-
|
|
145
|
-
|
|
1681
|
+
ipcMain.handle("export-fork", async (_, sessionId, forkId) => {
|
|
1682
|
+
if (!currentProjectPath) {
|
|
1683
|
+
throw new Error("No active project");
|
|
1684
|
+
}
|
|
1685
|
+
const orka = new ClaudeOrka(currentProjectPath);
|
|
1686
|
+
await orka.initialize();
|
|
1687
|
+
const summary = await orka.generateForkExport(sessionId, forkId);
|
|
1688
|
+
return summary;
|
|
146
1689
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
1690
|
+
ipcMain.handle("merge-fork", async (_, sessionId, forkId) => {
|
|
1691
|
+
if (!currentProjectPath) {
|
|
1692
|
+
throw new Error("No active project");
|
|
1693
|
+
}
|
|
1694
|
+
const orka = new ClaudeOrka(currentProjectPath);
|
|
1695
|
+
await orka.initialize();
|
|
1696
|
+
await orka.merge(sessionId, forkId);
|
|
1697
|
+
});
|
|
1698
|
+
ipcMain.handle("close-fork", async (_, sessionId, forkId) => {
|
|
1699
|
+
if (!currentProjectPath) {
|
|
1700
|
+
throw new Error("No active project");
|
|
1701
|
+
}
|
|
1702
|
+
const orka = new ClaudeOrka(currentProjectPath);
|
|
1703
|
+
await orka.initialize();
|
|
1704
|
+
await orka.closeFork(sessionId, forkId);
|
|
1705
|
+
});
|
|
1706
|
+
ipcMain.handle("open-export-file", async (_, exportPath) => {
|
|
1707
|
+
if (!currentProjectPath) {
|
|
1708
|
+
throw new Error("No active project");
|
|
1709
|
+
}
|
|
1710
|
+
const fullPath = path5.join(currentProjectPath, exportPath);
|
|
1711
|
+
await shell.openPath(fullPath);
|
|
1712
|
+
});
|
|
1713
|
+
ipcMain.handle("open-project-folder", async () => {
|
|
1714
|
+
if (!currentProjectPath) {
|
|
1715
|
+
throw new Error("No active project");
|
|
1716
|
+
}
|
|
1717
|
+
try {
|
|
1718
|
+
await execa2("cursor", [currentProjectPath]);
|
|
1719
|
+
return;
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
}
|
|
1722
|
+
try {
|
|
1723
|
+
await execa2("code", [currentProjectPath]);
|
|
1724
|
+
return;
|
|
1725
|
+
} catch (error) {
|
|
1726
|
+
}
|
|
1727
|
+
await shell.openPath(currentProjectPath);
|
|
1728
|
+
});
|
|
1729
|
+
ipcMain.handle("focus-terminal", async () => {
|
|
1730
|
+
const terminalApps = ["Terminal", "iTerm"];
|
|
1731
|
+
for (const app2 of terminalApps) {
|
|
1732
|
+
try {
|
|
1733
|
+
await execa2("osascript", ["-e", `tell application "${app2}" to activate`]);
|
|
1734
|
+
return;
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
try {
|
|
1739
|
+
await execa2("open", ["-a", "Terminal"]);
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
console.error("Failed to open terminal:", error);
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
ipcMain.handle("save-and-close", async () => {
|
|
1745
|
+
if (currentSessionId && currentProjectPath) {
|
|
1746
|
+
try {
|
|
1747
|
+
const orka = new ClaudeOrka(currentProjectPath);
|
|
1748
|
+
await orka.initialize();
|
|
1749
|
+
const session = await orka.getSession(currentSessionId);
|
|
1750
|
+
if (session?.tmuxSessionId) {
|
|
1751
|
+
console.log("Save and close: detaching from tmux session:", session.tmuxSessionId);
|
|
1752
|
+
try {
|
|
1753
|
+
await execa2("tmux", ["detach-client", "-s", session.tmuxSessionId]);
|
|
1754
|
+
console.log("Detached from tmux session (session remains alive)");
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
console.log("Error detaching from tmux:", error);
|
|
163
1757
|
}
|
|
164
|
-
|
|
1758
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1759
|
+
try {
|
|
1760
|
+
await execa2("osascript", [
|
|
1761
|
+
"-e",
|
|
1762
|
+
`tell application "Terminal" to close (first window whose name contains "${session.tmuxSessionId}")`
|
|
1763
|
+
]);
|
|
1764
|
+
console.log("Closed specific Terminal window");
|
|
1765
|
+
} catch (error) {
|
|
1766
|
+
console.log("Could not close specific window with AppleScript");
|
|
1767
|
+
try {
|
|
1768
|
+
const { stdout } = await execa2("osascript", [
|
|
1769
|
+
"-e",
|
|
1770
|
+
'tell application "Terminal" to count windows'
|
|
1771
|
+
]);
|
|
1772
|
+
const windowCount = parseInt(stdout.trim());
|
|
1773
|
+
if (windowCount === 1) {
|
|
1774
|
+
await execa2("osascript", ["-e", 'tell application "Terminal" to quit']);
|
|
1775
|
+
console.log("Quit Terminal (was last window)");
|
|
1776
|
+
} else {
|
|
1777
|
+
console.log("Multiple Terminal windows open, cannot close safely");
|
|
1778
|
+
}
|
|
1779
|
+
} catch (countError) {
|
|
1780
|
+
console.log("Could not determine Terminal window count:", countError);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
console.error("Error in save-and-close:", error);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
app.quit();
|
|
1789
|
+
});
|
|
1790
|
+
ipcMain.on("close-window", async (event) => {
|
|
1791
|
+
const window = BrowserWindow.fromWebContents(event.sender);
|
|
1792
|
+
window?.close();
|
|
165
1793
|
});
|
|
166
|
-
app.
|
|
167
|
-
|
|
168
|
-
|
|
1794
|
+
app.whenReady().then(() => {
|
|
1795
|
+
const args = process.argv.slice(2);
|
|
1796
|
+
const sessionIdIndex = args.indexOf("--session-id");
|
|
1797
|
+
const projectPathIndex = args.indexOf("--project-path");
|
|
1798
|
+
if (sessionIdIndex !== -1 && args[sessionIdIndex + 1]) {
|
|
1799
|
+
const sessionId = args[sessionIdIndex + 1];
|
|
1800
|
+
const projectPath = projectPathIndex !== -1 && args[projectPathIndex + 1] ? args[projectPathIndex + 1] : process.cwd();
|
|
1801
|
+
createWindow(sessionId, projectPath);
|
|
1802
|
+
}
|
|
1803
|
+
app.on("activate", () => {
|
|
1804
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
169
1805
|
}
|
|
1806
|
+
});
|
|
1807
|
+
});
|
|
1808
|
+
app.on("window-all-closed", () => {
|
|
1809
|
+
app.quit();
|
|
170
1810
|
});
|
|
171
|
-
//# sourceMappingURL=main.js.map
|