@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.
@@ -1,171 +1,1810 @@
1
- import { app, BrowserWindow, ipcMain } from 'electron';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
- import { ClaudeOrka } from '../../src/core/ClaudeOrka';
5
- import chokidar from 'chokidar';
6
- import execa from 'execa';
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
- // Store windows by project path
10
- const windows = new Map();
11
- // Store active sessions
12
- let currentSessionId = null;
13
- let currentProjectPath = null;
14
- function createWindow(sessionId, projectPath) {
15
- // If window already exists for this project, focus it
16
- if (windows.has(projectPath)) {
17
- const existingWindow = windows.get(projectPath);
18
- existingWindow.focus();
19
- return existingWindow;
20
- }
21
- const mainWindow = new BrowserWindow({
22
- width: 600,
23
- height: 800,
24
- minWidth: 500,
25
- minHeight: 600,
26
- frame: false,
27
- transparent: true,
28
- alwaysOnTop: true,
29
- resizable: true,
30
- webPreferences: {
31
- preload: path.join(__dirname, '../preload/preload.js'),
32
- contextIsolation: true,
33
- nodeIntegration: false,
34
- },
35
- });
36
- // Store session info
37
- currentSessionId = sessionId;
38
- currentProjectPath = projectPath;
39
- // Load UI
40
- if (process.env.NODE_ENV === 'development') {
41
- // Dev mode - load from Vite dev server
42
- mainWindow.loadURL('http://localhost:5173');
43
- mainWindow.webContents.openDevTools();
44
- }
45
- else {
46
- // Production - load from built files
47
- const indexPath = path.join(__dirname, '../renderer/index.html');
48
- mainWindow.loadFile(indexPath);
49
- }
50
- // Watch state.json for changes
51
- watchStateFile(projectPath, mainWindow);
52
- // Store window
53
- windows.set(projectPath, mainWindow);
54
- mainWindow.on('closed', () => {
55
- windows.delete(projectPath);
56
- });
57
- return mainWindow;
58
- }
59
- function watchStateFile(projectPath, window) {
60
- const statePath = path.join(projectPath, '.claude-orka/state.json');
61
- const watcher = chokidar.watch(statePath, {
62
- persistent: true,
63
- ignoreInitial: true,
64
- });
65
- watcher.on('change', async () => {
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
- const orka = new ClaudeOrka(projectPath);
68
- await orka.initialize();
69
- if (currentSessionId) {
70
- const session = await orka.getSession(currentSessionId);
71
- if (session) {
72
- window.webContents.send('state-updated', session);
73
- }
74
- }
75
- }
76
- catch (error) {
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
- window.on('closed', () => {
81
- watcher.close();
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
- // IPC Handlers
85
- ipcMain.handle('get-session', async () => {
86
- if (!currentSessionId || !currentProjectPath) {
87
- throw new Error('No active session');
88
- }
89
- const orka = new ClaudeOrka(currentProjectPath);
90
- await orka.initialize();
91
- const session = await orka.getSession(currentSessionId);
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
- ipcMain.handle('select-node', async (_, nodeId) => {
95
- if (!currentSessionId || !currentProjectPath) {
96
- throw new Error('No active session');
97
- }
98
- const orka = new ClaudeOrka(currentProjectPath);
99
- await orka.initialize();
100
- const session = await orka.getSession(currentSessionId);
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
- else {
109
- const fork = session.forks.find((f) => f.id === nodeId);
110
- tmuxPaneId = fork?.tmuxPaneId;
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 (tmuxPaneId) {
113
- // Focus the tmux pane
114
- await execa('tmux', ['select-pane', '-t', tmuxPaneId]);
887
+ if (session.tmuxSessionId) {
888
+ await TmuxCommands.killSession(session.tmuxSessionId);
115
889
  }
116
- });
117
- ipcMain.handle('create-fork', async (_, sessionId, name) => {
118
- if (!currentProjectPath) {
119
- throw new Error('No active project');
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 orka = new ClaudeOrka(currentProjectPath);
122
- await orka.initialize();
123
- const fork = await orka.createFork(sessionId, name);
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('export-fork', async (_, sessionId, forkId) => {
127
- if (!currentProjectPath) {
128
- throw new Error('No active project');
129
- }
130
- const orka = new ClaudeOrka(currentProjectPath);
131
- await orka.initialize();
132
- const summary = await orka.generateForkExport(sessionId, forkId);
133
- return summary;
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('merge-fork', async (_, sessionId, forkId) => {
136
- if (!currentProjectPath) {
137
- throw new Error('No active project');
138
- }
139
- const orka = new ClaudeOrka(currentProjectPath);
140
- await orka.initialize();
141
- await orka.generateExportAndMerge(sessionId, forkId);
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.on('close-window', (event) => {
144
- const window = BrowserWindow.fromWebContents(event.sender);
145
- window?.close();
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
- // App lifecycle
148
- app.whenReady().then(() => {
149
- // Get session ID and project path from command line args
150
- const args = process.argv.slice(2);
151
- const sessionIdIndex = args.indexOf('--session-id');
152
- const projectPathIndex = args.indexOf('--project-path');
153
- if (sessionIdIndex !== -1 && args[sessionIdIndex + 1]) {
154
- const sessionId = args[sessionIdIndex + 1];
155
- const projectPath = projectPathIndex !== -1 && args[projectPathIndex + 1]
156
- ? args[projectPathIndex + 1]
157
- : process.cwd();
158
- createWindow(sessionId, projectPath);
159
- }
160
- app.on('activate', () => {
161
- if (BrowserWindow.getAllWindows().length === 0) {
162
- // Don't create window on activate if no session
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.on('window-all-closed', () => {
167
- if (process.platform !== 'darwin') {
168
- app.quit();
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