@enruana/claude-orka 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/orka.js +14 -12
- package/dist/cli.js +2433 -0
- package/package.json +5 -2
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/cli/index.ts
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
|
|
12
|
+
// src/core/StateManager.ts
|
|
13
|
+
import path2 from "path";
|
|
14
|
+
import fs2 from "fs-extra";
|
|
15
|
+
|
|
16
|
+
// src/utils/tmux.ts
|
|
17
|
+
import execa from "execa";
|
|
18
|
+
|
|
19
|
+
// src/utils/logger.ts
|
|
20
|
+
var Logger = class {
|
|
21
|
+
level = 1 /* INFO */;
|
|
22
|
+
setLevel(level) {
|
|
23
|
+
this.level = level;
|
|
24
|
+
}
|
|
25
|
+
debug(...args) {
|
|
26
|
+
if (this.level <= 0 /* DEBUG */) {
|
|
27
|
+
console.log("[DEBUG]", ...args);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
info(...args) {
|
|
31
|
+
if (this.level <= 1 /* INFO */) {
|
|
32
|
+
console.log("[INFO]", ...args);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
warn(...args) {
|
|
36
|
+
if (this.level <= 2 /* WARN */) {
|
|
37
|
+
console.warn("[WARN]", ...args);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
error(...args) {
|
|
41
|
+
if (this.level <= 3 /* ERROR */) {
|
|
42
|
+
console.error("[ERROR]", ...args);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var logger = new Logger();
|
|
47
|
+
|
|
48
|
+
// src/utils/tmux.ts
|
|
49
|
+
var TmuxError = class extends Error {
|
|
50
|
+
constructor(message, command, originalError) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.command = command;
|
|
53
|
+
this.originalError = originalError;
|
|
54
|
+
this.name = "TmuxError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var TmuxCommands = class {
|
|
58
|
+
/**
|
|
59
|
+
* Verificar si tmux está disponible
|
|
60
|
+
*/
|
|
61
|
+
static async isAvailable() {
|
|
62
|
+
try {
|
|
63
|
+
await execa("which", ["tmux"]);
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Crear una nueva sesión tmux en modo detached
|
|
71
|
+
*/
|
|
72
|
+
static async createSession(name, projectPath) {
|
|
73
|
+
try {
|
|
74
|
+
logger.debug(`Creating tmux session: ${name} at ${projectPath}`);
|
|
75
|
+
await execa("tmux", ["new-session", "-d", "-s", name, "-c", projectPath]);
|
|
76
|
+
logger.info(`Tmux session created: ${name}`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw new TmuxError(
|
|
79
|
+
`Failed to create tmux session: ${name}`,
|
|
80
|
+
`tmux new-session -d -s ${name} -c ${projectPath}`,
|
|
81
|
+
error
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Abrir una terminal que se adjunte a una sesión tmux existente
|
|
87
|
+
* (Solo macOS por ahora)
|
|
88
|
+
*/
|
|
89
|
+
static async openTerminalWindow(sessionName) {
|
|
90
|
+
try {
|
|
91
|
+
logger.debug(`Opening terminal window for session: ${sessionName}`);
|
|
92
|
+
const platform = process.platform;
|
|
93
|
+
if (platform === "darwin") {
|
|
94
|
+
const script = `tell application "Terminal"
|
|
95
|
+
do script "tmux attach -t ${sessionName}"
|
|
96
|
+
activate
|
|
97
|
+
end tell`;
|
|
98
|
+
await execa("osascript", ["-e", script]);
|
|
99
|
+
logger.info("Terminal window opened (Terminal.app)");
|
|
100
|
+
} else if (platform === "linux") {
|
|
101
|
+
try {
|
|
102
|
+
await execa("gnome-terminal", ["--", "tmux", "attach", "-t", sessionName]);
|
|
103
|
+
logger.info("Terminal window opened (gnome-terminal)");
|
|
104
|
+
} catch {
|
|
105
|
+
try {
|
|
106
|
+
await execa("xterm", ["-e", `tmux attach -t ${sessionName}`]);
|
|
107
|
+
logger.info("Terminal window opened (xterm)");
|
|
108
|
+
} catch {
|
|
109
|
+
logger.warn("Could not open terminal window on Linux");
|
|
110
|
+
throw new Error("No suitable terminal emulator found");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
logger.warn(`Platform ${platform} not supported for opening terminal windows`);
|
|
115
|
+
throw new Error(`Platform ${platform} not supported`);
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new TmuxError(
|
|
119
|
+
`Failed to open terminal window for session: ${sessionName}`,
|
|
120
|
+
`osascript/terminal`,
|
|
121
|
+
error
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Cerrar una sesión tmux
|
|
127
|
+
*/
|
|
128
|
+
static async killSession(sessionName) {
|
|
129
|
+
try {
|
|
130
|
+
logger.debug(`Killing tmux session: ${sessionName}`);
|
|
131
|
+
await execa("tmux", ["kill-session", "-t", sessionName]);
|
|
132
|
+
logger.info(`Tmux session killed: ${sessionName}`);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw new TmuxError(
|
|
135
|
+
`Failed to kill tmux session: ${sessionName}`,
|
|
136
|
+
`tmux kill-session -t ${sessionName}`,
|
|
137
|
+
error
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Verificar si una sesión existe
|
|
143
|
+
*/
|
|
144
|
+
static async sessionExists(sessionName) {
|
|
145
|
+
try {
|
|
146
|
+
await execa("tmux", ["has-session", "-t", sessionName]);
|
|
147
|
+
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Obtener el ID del pane principal de una sesión
|
|
154
|
+
*/
|
|
155
|
+
static async getMainPaneId(sessionName) {
|
|
156
|
+
try {
|
|
157
|
+
logger.debug(`Getting main pane ID for session: ${sessionName}`);
|
|
158
|
+
const { stdout } = await execa("tmux", [
|
|
159
|
+
"list-panes",
|
|
160
|
+
"-t",
|
|
161
|
+
sessionName,
|
|
162
|
+
"-F",
|
|
163
|
+
"#{pane_id}"
|
|
164
|
+
]);
|
|
165
|
+
const paneId = stdout.split("\n")[0];
|
|
166
|
+
logger.debug(`Main pane ID: ${paneId}`);
|
|
167
|
+
return paneId;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw new TmuxError(
|
|
170
|
+
`Failed to get main pane ID for session: ${sessionName}`,
|
|
171
|
+
`tmux list-panes -t ${sessionName} -F '#{pane_id}'`,
|
|
172
|
+
error
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Obtener el ID de la ventana principal de una sesión
|
|
178
|
+
*/
|
|
179
|
+
static async getMainWindowId(sessionName) {
|
|
180
|
+
try {
|
|
181
|
+
logger.debug(`Getting main window ID for session: ${sessionName}`);
|
|
182
|
+
const { stdout } = await execa("tmux", [
|
|
183
|
+
"list-windows",
|
|
184
|
+
"-t",
|
|
185
|
+
sessionName,
|
|
186
|
+
"-F",
|
|
187
|
+
"#{window_id}"
|
|
188
|
+
]);
|
|
189
|
+
const windowId = stdout.split("\n")[0];
|
|
190
|
+
logger.debug(`Main window ID: ${windowId}`);
|
|
191
|
+
return windowId;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new TmuxError(
|
|
194
|
+
`Failed to get main window ID for session: ${sessionName}`,
|
|
195
|
+
`tmux list-windows -t ${sessionName} -F '#{window_id}'`,
|
|
196
|
+
error
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Dividir un pane (crear fork)
|
|
202
|
+
* @param sessionName Nombre de la sesión
|
|
203
|
+
* @param vertical Si es true, divide verticalmente (-h), si es false horizontalmente (-v)
|
|
204
|
+
* @returns ID del nuevo pane creado
|
|
205
|
+
*/
|
|
206
|
+
static async splitPane(sessionName, vertical = false) {
|
|
207
|
+
try {
|
|
208
|
+
const direction = vertical ? "-h" : "-v";
|
|
209
|
+
logger.debug(`Splitting pane in session ${sessionName} (${vertical ? "vertical" : "horizontal"})`);
|
|
210
|
+
await execa("tmux", ["split-window", "-t", sessionName, direction]);
|
|
211
|
+
const { stdout } = await execa("tmux", [
|
|
212
|
+
"list-panes",
|
|
213
|
+
"-t",
|
|
214
|
+
sessionName,
|
|
215
|
+
"-F",
|
|
216
|
+
"#{pane_id}"
|
|
217
|
+
]);
|
|
218
|
+
const panes = stdout.split("\n");
|
|
219
|
+
const newPaneId = panes[panes.length - 1];
|
|
220
|
+
logger.info(`New pane created: ${newPaneId}`);
|
|
221
|
+
return newPaneId;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw new TmuxError(
|
|
224
|
+
`Failed to split pane in session: ${sessionName}`,
|
|
225
|
+
`tmux split-window -t ${sessionName} ${vertical ? "-h" : "-v"}`,
|
|
226
|
+
error
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Listar todos los panes de una sesión
|
|
232
|
+
* @param sessionName Nombre de la sesión
|
|
233
|
+
* @returns Array de IDs de panes
|
|
234
|
+
*/
|
|
235
|
+
static async listPanes(sessionName) {
|
|
236
|
+
try {
|
|
237
|
+
const { stdout } = await execa("tmux", [
|
|
238
|
+
"list-panes",
|
|
239
|
+
"-t",
|
|
240
|
+
sessionName,
|
|
241
|
+
"-F",
|
|
242
|
+
"#{pane_id}"
|
|
243
|
+
]);
|
|
244
|
+
return stdout.trim().split("\n").filter(Boolean);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
throw new TmuxError(
|
|
247
|
+
`Failed to list panes for session: ${sessionName}`,
|
|
248
|
+
`tmux list-panes -t ${sessionName}`,
|
|
249
|
+
error
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Cerrar un pane específico
|
|
255
|
+
*/
|
|
256
|
+
static async killPane(paneId) {
|
|
257
|
+
try {
|
|
258
|
+
logger.debug(`Killing pane: ${paneId}`);
|
|
259
|
+
await execa("tmux", ["kill-pane", "-t", paneId]);
|
|
260
|
+
logger.info(`Pane killed: ${paneId}`);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw new TmuxError(
|
|
263
|
+
`Failed to kill pane: ${paneId}`,
|
|
264
|
+
`tmux kill-pane -t ${paneId}`,
|
|
265
|
+
error
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Enviar texto a un pane (SIN Enter)
|
|
271
|
+
* IMPORTANTE: No envía Enter, debe llamarse a sendEnter() por separado
|
|
272
|
+
*/
|
|
273
|
+
static async sendKeys(paneId, text) {
|
|
274
|
+
try {
|
|
275
|
+
logger.debug(`Sending keys to pane ${paneId}: ${text.substring(0, 50)}...`);
|
|
276
|
+
await execa("tmux", ["send-keys", "-t", paneId, text]);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
throw new TmuxError(
|
|
279
|
+
`Failed to send keys to pane: ${paneId}`,
|
|
280
|
+
`tmux send-keys -t ${paneId} "${text}"`,
|
|
281
|
+
error
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Enviar SOLO Enter a un pane
|
|
287
|
+
*/
|
|
288
|
+
static async sendEnter(paneId) {
|
|
289
|
+
try {
|
|
290
|
+
logger.debug(`Sending Enter to pane: ${paneId}`);
|
|
291
|
+
await execa("tmux", ["send-keys", "-t", paneId, "Enter"]);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
throw new TmuxError(
|
|
294
|
+
`Failed to send Enter to pane: ${paneId}`,
|
|
295
|
+
`tmux send-keys -t ${paneId} Enter`,
|
|
296
|
+
error
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Enviar teclas especiales (flechas, escape, etc.)
|
|
302
|
+
* @param paneId ID del pane
|
|
303
|
+
* @param key Nombre de la tecla: 'Up', 'Down', 'Left', 'Right', 'Escape', 'Space', etc.
|
|
304
|
+
*/
|
|
305
|
+
static async sendSpecialKey(paneId, key) {
|
|
306
|
+
try {
|
|
307
|
+
logger.debug(`Sending special key '${key}' to pane: ${paneId}`);
|
|
308
|
+
await execa("tmux", ["send-keys", "-t", paneId, key]);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
throw new TmuxError(
|
|
311
|
+
`Failed to send special key '${key}' to pane: ${paneId}`,
|
|
312
|
+
`tmux send-keys -t ${paneId} ${key}`,
|
|
313
|
+
error
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Capturar el contenido de un pane
|
|
319
|
+
* @param paneId ID del pane
|
|
320
|
+
* @param startLine Línea desde donde empezar a capturar (negativo = desde el final)
|
|
321
|
+
* @returns Contenido del pane
|
|
322
|
+
*/
|
|
323
|
+
static async capturePane(paneId, startLine = -100) {
|
|
324
|
+
try {
|
|
325
|
+
logger.debug(`Capturing pane ${paneId} from line ${startLine}`);
|
|
326
|
+
const { stdout } = await execa("tmux", [
|
|
327
|
+
"capture-pane",
|
|
328
|
+
"-t",
|
|
329
|
+
paneId,
|
|
330
|
+
"-p",
|
|
331
|
+
"-S",
|
|
332
|
+
startLine.toString()
|
|
333
|
+
]);
|
|
334
|
+
return stdout;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
throw new TmuxError(
|
|
337
|
+
`Failed to capture pane: ${paneId}`,
|
|
338
|
+
`tmux capture-pane -t ${paneId} -p -S ${startLine}`,
|
|
339
|
+
error
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Listar todas las sesiones tmux
|
|
345
|
+
*/
|
|
346
|
+
static async listSessions() {
|
|
347
|
+
try {
|
|
348
|
+
const { stdout } = await execa("tmux", ["list-sessions", "-F", "#{session_id}:#{session_name}"]);
|
|
349
|
+
return stdout.split("\n").map((line) => {
|
|
350
|
+
const [id, name] = line.split(":");
|
|
351
|
+
return { id, name };
|
|
352
|
+
});
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error.stderr?.includes("no server running")) {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
throw new TmuxError(
|
|
358
|
+
"Failed to list tmux sessions",
|
|
359
|
+
"tmux list-sessions",
|
|
360
|
+
error
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// src/utils/claude-history.ts
|
|
367
|
+
import fs from "fs-extra";
|
|
368
|
+
import os from "os";
|
|
369
|
+
import path from "path";
|
|
370
|
+
var CLAUDE_HISTORY_PATH = path.join(os.homedir(), ".claude", "history.jsonl");
|
|
371
|
+
async function readClaudeHistory() {
|
|
372
|
+
try {
|
|
373
|
+
const exists = await fs.pathExists(CLAUDE_HISTORY_PATH);
|
|
374
|
+
if (!exists) {
|
|
375
|
+
logger.warn(`Claude history file not found: ${CLAUDE_HISTORY_PATH}`);
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
const content = await fs.readFile(CLAUDE_HISTORY_PATH, "utf-8");
|
|
379
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
380
|
+
const entries = [];
|
|
381
|
+
for (const line of lines) {
|
|
382
|
+
try {
|
|
383
|
+
const entry = JSON.parse(line);
|
|
384
|
+
entries.push(entry);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
logger.warn(`Failed to parse history line: ${line}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return entries;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
logger.error(`Error reading Claude history: ${error}`);
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function getExistingSessionIds() {
|
|
396
|
+
const entries = await readClaudeHistory();
|
|
397
|
+
return new Set(entries.map((e) => e.sessionId));
|
|
398
|
+
}
|
|
399
|
+
async function detectNewSessionId(previousIds, maxWaitMs = 1e4, pollIntervalMs = 500) {
|
|
400
|
+
const startTime = Date.now();
|
|
401
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
402
|
+
const currentIds = await getExistingSessionIds();
|
|
403
|
+
for (const id of currentIds) {
|
|
404
|
+
if (!previousIds.has(id)) {
|
|
405
|
+
logger.info(`Detected new Claude session: ${id}`);
|
|
406
|
+
return id;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
410
|
+
}
|
|
411
|
+
logger.warn("Timeout waiting for new Claude session ID");
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/core/StateManager.ts
|
|
416
|
+
var StateManager = class {
|
|
417
|
+
projectPath;
|
|
418
|
+
orkaDir;
|
|
419
|
+
statePath;
|
|
420
|
+
constructor(projectPath) {
|
|
421
|
+
this.projectPath = path2.resolve(projectPath);
|
|
422
|
+
this.orkaDir = path2.join(this.projectPath, ".claude-orka");
|
|
423
|
+
this.statePath = path2.join(this.orkaDir, "state.json");
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Initialize StateManager
|
|
427
|
+
* Creates necessary folders if they don't exist
|
|
428
|
+
*/
|
|
429
|
+
async initialize() {
|
|
430
|
+
logger.debug("Initializing StateManager");
|
|
431
|
+
await this.ensureDirectories();
|
|
432
|
+
if (!await fs2.pathExists(this.statePath)) {
|
|
433
|
+
logger.info("Creating initial state.json");
|
|
434
|
+
const initialState = {
|
|
435
|
+
version: "1.0.0",
|
|
436
|
+
projectPath: this.projectPath,
|
|
437
|
+
sessions: [],
|
|
438
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
439
|
+
};
|
|
440
|
+
await this.save(initialState);
|
|
441
|
+
}
|
|
442
|
+
logger.info("StateManager initialized");
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Create directory structure
|
|
446
|
+
*/
|
|
447
|
+
async ensureDirectories() {
|
|
448
|
+
await fs2.ensureDir(this.orkaDir);
|
|
449
|
+
logger.debug("Directories ensured");
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Read current state
|
|
453
|
+
*/
|
|
454
|
+
async read() {
|
|
455
|
+
try {
|
|
456
|
+
const content = await fs2.readFile(this.statePath, "utf-8");
|
|
457
|
+
return JSON.parse(content);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
logger.error("Failed to read state:", error);
|
|
460
|
+
throw new Error(`Failed to read state: ${error.message}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Save state
|
|
465
|
+
*/
|
|
466
|
+
async save(state) {
|
|
467
|
+
try {
|
|
468
|
+
state.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
469
|
+
await fs2.writeFile(this.statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
470
|
+
logger.debug("State saved");
|
|
471
|
+
} catch (error) {
|
|
472
|
+
logger.error("Failed to save state:", error);
|
|
473
|
+
throw new Error(`Failed to save state: ${error.message}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// --- OPERACIONES DE SESIONES ---
|
|
477
|
+
/**
|
|
478
|
+
* Obtener el estado completo
|
|
479
|
+
*/
|
|
480
|
+
async getState() {
|
|
481
|
+
return await this.read();
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Listar todas las sesiones con filtros opcionales
|
|
485
|
+
*/
|
|
486
|
+
async listSessions(filters) {
|
|
487
|
+
const state = await this.read();
|
|
488
|
+
let sessions = state.sessions;
|
|
489
|
+
if (filters?.status) {
|
|
490
|
+
sessions = sessions.filter((s) => s.status === filters.status);
|
|
491
|
+
}
|
|
492
|
+
if (filters?.name) {
|
|
493
|
+
sessions = sessions.filter(
|
|
494
|
+
(s) => s.name.toLowerCase().includes(filters.name.toLowerCase())
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
return sessions;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Agregar una nueva sesión
|
|
501
|
+
*/
|
|
502
|
+
async addSession(session) {
|
|
503
|
+
const state = await this.read();
|
|
504
|
+
state.sessions.push(session);
|
|
505
|
+
await this.save(state);
|
|
506
|
+
logger.info(`Session added: ${session.id}`);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Obtener una sesión por ID
|
|
510
|
+
*/
|
|
511
|
+
async getSession(sessionId) {
|
|
512
|
+
const state = await this.read();
|
|
513
|
+
return state.sessions.find((s) => s.id === sessionId) || null;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Obtener todas las sesiones con filtros opcionales
|
|
517
|
+
*/
|
|
518
|
+
async getAllSessions(filters) {
|
|
519
|
+
const state = await this.read();
|
|
520
|
+
let sessions = state.sessions;
|
|
521
|
+
if (filters?.status) {
|
|
522
|
+
sessions = sessions.filter((s) => s.status === filters.status);
|
|
523
|
+
}
|
|
524
|
+
if (filters?.name) {
|
|
525
|
+
sessions = sessions.filter((s) => s.name.includes(filters.name));
|
|
526
|
+
}
|
|
527
|
+
return sessions;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Actualizar el estado de una sesión
|
|
531
|
+
*/
|
|
532
|
+
async updateSessionStatus(sessionId, status) {
|
|
533
|
+
const state = await this.read();
|
|
534
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
535
|
+
if (!session) {
|
|
536
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
537
|
+
}
|
|
538
|
+
session.status = status;
|
|
539
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
540
|
+
await this.save(state);
|
|
541
|
+
logger.info(`Session ${sessionId} status updated to: ${status}`);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Actualizar una sesión completa
|
|
545
|
+
*/
|
|
546
|
+
async updateSession(sessionId, updates) {
|
|
547
|
+
const state = await this.read();
|
|
548
|
+
const sessionIndex = state.sessions.findIndex((s) => s.id === sessionId);
|
|
549
|
+
if (sessionIndex === -1) {
|
|
550
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
551
|
+
}
|
|
552
|
+
state.sessions[sessionIndex] = {
|
|
553
|
+
...state.sessions[sessionIndex],
|
|
554
|
+
...updates,
|
|
555
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString()
|
|
556
|
+
};
|
|
557
|
+
await this.save(state);
|
|
558
|
+
logger.debug(`Session ${sessionId} updated`);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Reemplazar una sesión completa
|
|
562
|
+
*/
|
|
563
|
+
async replaceSession(session) {
|
|
564
|
+
const state = await this.read();
|
|
565
|
+
const sessionIndex = state.sessions.findIndex((s) => s.id === session.id);
|
|
566
|
+
if (sessionIndex === -1) {
|
|
567
|
+
throw new Error(`Session not found: ${session.id}`);
|
|
568
|
+
}
|
|
569
|
+
state.sessions[sessionIndex] = session;
|
|
570
|
+
await this.save(state);
|
|
571
|
+
logger.debug(`Session ${session.id} replaced`);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Eliminar una sesión permanentemente
|
|
575
|
+
*/
|
|
576
|
+
async deleteSession(sessionId) {
|
|
577
|
+
const state = await this.read();
|
|
578
|
+
state.sessions = state.sessions.filter((s) => s.id !== sessionId);
|
|
579
|
+
await this.save(state);
|
|
580
|
+
logger.info(`Session deleted: ${sessionId}`);
|
|
581
|
+
}
|
|
582
|
+
// --- OPERACIONES DE FORKS ---
|
|
583
|
+
/**
|
|
584
|
+
* Agregar un fork a una sesión
|
|
585
|
+
*/
|
|
586
|
+
async addFork(sessionId, fork) {
|
|
587
|
+
const state = await this.read();
|
|
588
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
589
|
+
if (!session) {
|
|
590
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
591
|
+
}
|
|
592
|
+
session.forks.push(fork);
|
|
593
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
594
|
+
await this.save(state);
|
|
595
|
+
logger.info(`Fork added to session ${sessionId}: ${fork.id}`);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Obtener un fork específico
|
|
599
|
+
*/
|
|
600
|
+
async getFork(sessionId, forkId) {
|
|
601
|
+
const session = await this.getSession(sessionId);
|
|
602
|
+
if (!session) return null;
|
|
603
|
+
return session.forks.find((f) => f.id === forkId) || null;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Actualizar el estado de un fork
|
|
607
|
+
*/
|
|
608
|
+
async updateForkStatus(sessionId, forkId, status) {
|
|
609
|
+
const state = await this.read();
|
|
610
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
611
|
+
if (!session) {
|
|
612
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
613
|
+
}
|
|
614
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
615
|
+
if (!fork) {
|
|
616
|
+
throw new Error(`Fork not found: ${forkId}`);
|
|
617
|
+
}
|
|
618
|
+
fork.status = status;
|
|
619
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
620
|
+
await this.save(state);
|
|
621
|
+
logger.info(`Fork ${forkId} status updated to: ${status}`);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Actualizar el path del contexto de un fork
|
|
625
|
+
*/
|
|
626
|
+
async updateForkContext(sessionId, forkId, contextPath) {
|
|
627
|
+
const state = await this.read();
|
|
628
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
629
|
+
if (!session) {
|
|
630
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
631
|
+
}
|
|
632
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
633
|
+
if (!fork) {
|
|
634
|
+
throw new Error(`Fork not found: ${forkId}`);
|
|
635
|
+
}
|
|
636
|
+
fork.contextPath = contextPath;
|
|
637
|
+
await this.save(state);
|
|
638
|
+
logger.debug(`Fork ${forkId} context updated: ${contextPath}`);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Actualizar un fork completo
|
|
642
|
+
*/
|
|
643
|
+
async updateFork(sessionId, forkId, updates) {
|
|
644
|
+
const state = await this.read();
|
|
645
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
646
|
+
if (!session) {
|
|
647
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
648
|
+
}
|
|
649
|
+
const forkIndex = session.forks.findIndex((f) => f.id === forkId);
|
|
650
|
+
if (forkIndex === -1) {
|
|
651
|
+
throw new Error(`Fork not found: ${forkId}`);
|
|
652
|
+
}
|
|
653
|
+
session.forks[forkIndex] = {
|
|
654
|
+
...session.forks[forkIndex],
|
|
655
|
+
...updates
|
|
656
|
+
};
|
|
657
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
658
|
+
await this.save(state);
|
|
659
|
+
logger.debug(`Fork ${forkId} updated`);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Eliminar un fork
|
|
663
|
+
*/
|
|
664
|
+
async deleteFork(sessionId, forkId) {
|
|
665
|
+
const state = await this.read();
|
|
666
|
+
const session = state.sessions.find((s) => s.id === sessionId);
|
|
667
|
+
if (!session) {
|
|
668
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
669
|
+
}
|
|
670
|
+
const forkIndex = session.forks.findIndex((f) => f.id === forkId);
|
|
671
|
+
if (forkIndex === -1) {
|
|
672
|
+
throw new Error(`Fork not found: ${forkId}`);
|
|
673
|
+
}
|
|
674
|
+
session.forks.splice(forkIndex, 1);
|
|
675
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
676
|
+
await this.save(state);
|
|
677
|
+
logger.debug(`Fork ${forkId} deleted`);
|
|
678
|
+
}
|
|
679
|
+
// --- OPERACIONES DE CONTEXTOS ---
|
|
680
|
+
/**
|
|
681
|
+
* Guardar un contexto en archivo
|
|
682
|
+
*/
|
|
683
|
+
async saveContext(type, id, content) {
|
|
684
|
+
const contextPath = type === "session" ? this.getSessionContextPath(id) : this.getForkContextPath(id);
|
|
685
|
+
const fullPath = path2.join(this.projectPath, contextPath);
|
|
686
|
+
await fs2.writeFile(fullPath, content, "utf-8");
|
|
687
|
+
logger.info(`Context saved: ${contextPath}`);
|
|
688
|
+
return contextPath;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Leer un contexto desde archivo
|
|
692
|
+
*/
|
|
693
|
+
async readContext(contextPath) {
|
|
694
|
+
const fullPath = path2.join(this.projectPath, contextPath);
|
|
695
|
+
if (!await fs2.pathExists(fullPath)) {
|
|
696
|
+
throw new Error(`Context file not found: ${contextPath}`);
|
|
697
|
+
}
|
|
698
|
+
return await fs2.readFile(fullPath, "utf-8");
|
|
699
|
+
}
|
|
700
|
+
// --- HELPERS ---
|
|
701
|
+
/**
|
|
702
|
+
* Obtener el path para el contexto de una sesión
|
|
703
|
+
*/
|
|
704
|
+
getSessionContextPath(sessionId) {
|
|
705
|
+
return `.claude-orka/sessions/${sessionId}.md`;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Obtener el path para el contexto de un fork
|
|
709
|
+
*/
|
|
710
|
+
getForkContextPath(forkId) {
|
|
711
|
+
return `.claude-orka/forks/${forkId}.md`;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Obtener el path para un export manual
|
|
715
|
+
*/
|
|
716
|
+
getExportPath(forkId, name) {
|
|
717
|
+
return `.claude-orka/exports/${forkId}-${name}.md`;
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// src/core/SessionManager.ts
|
|
722
|
+
import { v4 as uuidv4 } from "uuid";
|
|
723
|
+
import path3 from "path";
|
|
724
|
+
import fs3 from "fs-extra";
|
|
725
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
726
|
+
var SessionManager = class {
|
|
727
|
+
stateManager;
|
|
728
|
+
projectPath;
|
|
729
|
+
constructor(projectPath) {
|
|
730
|
+
this.projectPath = projectPath;
|
|
731
|
+
this.stateManager = new StateManager(projectPath);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Initialize el manager
|
|
735
|
+
*/
|
|
736
|
+
async initialize() {
|
|
737
|
+
await this.stateManager.initialize();
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Obtener el state
|
|
741
|
+
*/
|
|
742
|
+
async getState() {
|
|
743
|
+
return await this.stateManager.getState();
|
|
744
|
+
}
|
|
745
|
+
// ==========================================
|
|
746
|
+
// SESIONES
|
|
747
|
+
// ==========================================
|
|
748
|
+
/**
|
|
749
|
+
* Crear una nueva sesión de Claude Code
|
|
750
|
+
*/
|
|
751
|
+
async createSession(name, openTerminal = true) {
|
|
752
|
+
const sessionId = uuidv4();
|
|
753
|
+
const sessionName = name || `Session-${Date.now()}`;
|
|
754
|
+
const tmuxSessionId = `orka-${sessionId}`;
|
|
755
|
+
logger.info(`Creating session: ${sessionName}`);
|
|
756
|
+
await TmuxCommands.createSession(tmuxSessionId, this.projectPath);
|
|
757
|
+
if (openTerminal) {
|
|
758
|
+
await TmuxCommands.openTerminalWindow(tmuxSessionId);
|
|
759
|
+
}
|
|
760
|
+
await sleep(2e3);
|
|
761
|
+
const paneId = await TmuxCommands.getMainPaneId(tmuxSessionId);
|
|
762
|
+
logger.debug(`Main pane ID: ${paneId}`);
|
|
763
|
+
const claudeSessionId = uuidv4();
|
|
764
|
+
await this.initializeClaude(paneId, {
|
|
765
|
+
type: "new",
|
|
766
|
+
sessionId: claudeSessionId,
|
|
767
|
+
sessionName
|
|
768
|
+
});
|
|
769
|
+
const session = {
|
|
770
|
+
id: sessionId,
|
|
771
|
+
name: sessionName,
|
|
772
|
+
tmuxSessionId,
|
|
773
|
+
status: "active",
|
|
774
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
775
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
776
|
+
main: {
|
|
777
|
+
claudeSessionId,
|
|
778
|
+
tmuxPaneId: paneId,
|
|
779
|
+
status: "active"
|
|
780
|
+
},
|
|
781
|
+
forks: []
|
|
782
|
+
};
|
|
783
|
+
await this.stateManager.addSession(session);
|
|
784
|
+
logger.info(`Session created: ${sessionName} (${sessionId})`);
|
|
785
|
+
return session;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Restaurar una sesión guardada
|
|
789
|
+
*/
|
|
790
|
+
async resumeSession(sessionId, openTerminal = true) {
|
|
791
|
+
const session = await this.getSession(sessionId);
|
|
792
|
+
if (!session) {
|
|
793
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
794
|
+
}
|
|
795
|
+
logger.info(`Resuming session: ${session.name}`);
|
|
796
|
+
const tmuxSessionId = `orka-${sessionId}`;
|
|
797
|
+
await TmuxCommands.createSession(tmuxSessionId, this.projectPath);
|
|
798
|
+
if (openTerminal) {
|
|
799
|
+
await TmuxCommands.openTerminalWindow(tmuxSessionId);
|
|
800
|
+
}
|
|
801
|
+
await sleep(2e3);
|
|
802
|
+
const paneId = await TmuxCommands.getMainPaneId(tmuxSessionId);
|
|
803
|
+
await this.initializeClaude(paneId, {
|
|
804
|
+
type: "resume",
|
|
805
|
+
resumeSessionId: session.main.claudeSessionId,
|
|
806
|
+
sessionName: session.name
|
|
807
|
+
});
|
|
808
|
+
session.tmuxSessionId = tmuxSessionId;
|
|
809
|
+
session.main.tmuxPaneId = paneId;
|
|
810
|
+
session.main.status = "active";
|
|
811
|
+
session.status = "active";
|
|
812
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
813
|
+
await this.stateManager.replaceSession(session);
|
|
814
|
+
const forksToRestore = session.forks.filter((f) => f.status !== "merged");
|
|
815
|
+
if (forksToRestore.length > 0) {
|
|
816
|
+
logger.info(`Restoring ${forksToRestore.length} fork(s)...`);
|
|
817
|
+
for (const fork of forksToRestore) {
|
|
818
|
+
await this.resumeFork(sessionId, fork.id);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
logger.info(`Session resumed: ${session.name}`);
|
|
822
|
+
return session;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Close a session (save and kill tmux)
|
|
826
|
+
*/
|
|
827
|
+
async closeSession(sessionId) {
|
|
828
|
+
const session = await this.getSession(sessionId);
|
|
829
|
+
if (!session) {
|
|
830
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
831
|
+
}
|
|
832
|
+
logger.info(`Closing session: ${session.name}`);
|
|
833
|
+
const activeForks = session.forks.filter((f) => f.status === "active");
|
|
834
|
+
for (const fork of activeForks) {
|
|
835
|
+
await this.closeFork(sessionId, fork.id);
|
|
836
|
+
}
|
|
837
|
+
if (session.tmuxSessionId) {
|
|
838
|
+
await TmuxCommands.killSession(session.tmuxSessionId);
|
|
839
|
+
}
|
|
840
|
+
session.main.status = "saved";
|
|
841
|
+
session.main.tmuxPaneId = void 0;
|
|
842
|
+
session.status = "saved";
|
|
843
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
844
|
+
await this.stateManager.replaceSession(session);
|
|
845
|
+
logger.info(`Session closed: ${session.name}`);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Eliminar una sesión permanentemente
|
|
849
|
+
*/
|
|
850
|
+
async deleteSession(sessionId) {
|
|
851
|
+
const session = await this.getSession(sessionId);
|
|
852
|
+
if (!session) {
|
|
853
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
854
|
+
}
|
|
855
|
+
logger.info(`Deleting session: ${session.name}`);
|
|
856
|
+
if (session.status === "active") {
|
|
857
|
+
await this.closeSession(sessionId);
|
|
858
|
+
}
|
|
859
|
+
await this.stateManager.deleteSession(sessionId);
|
|
860
|
+
logger.info(`Session deleted: ${session.name}`);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Listar sesiones con filtros opcionales
|
|
864
|
+
*/
|
|
865
|
+
async listSessions(filters) {
|
|
866
|
+
return await this.stateManager.listSessions(filters);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Obtener una sesión por ID
|
|
870
|
+
*/
|
|
871
|
+
async getSession(sessionId) {
|
|
872
|
+
return await this.stateManager.getSession(sessionId);
|
|
873
|
+
}
|
|
874
|
+
// ==========================================
|
|
875
|
+
// FORKS
|
|
876
|
+
// ==========================================
|
|
877
|
+
/**
|
|
878
|
+
* Crear un fork (rama de conversación)
|
|
879
|
+
*/
|
|
880
|
+
async createFork(sessionId, name, vertical = false) {
|
|
881
|
+
const session = await this.getSession(sessionId);
|
|
882
|
+
if (!session) {
|
|
883
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
884
|
+
}
|
|
885
|
+
const forkId = uuidv4();
|
|
886
|
+
const forkName = name || `Fork-${session.forks.length + 1}`;
|
|
887
|
+
logger.info(`Creating fork: ${forkName} in session ${session.name}`);
|
|
888
|
+
await TmuxCommands.splitPane(session.tmuxSessionId, vertical);
|
|
889
|
+
await sleep(1e3);
|
|
890
|
+
const allPanes = await TmuxCommands.listPanes(session.tmuxSessionId);
|
|
891
|
+
const forkPaneId = allPanes[allPanes.length - 1];
|
|
892
|
+
logger.debug(`Fork pane ID: ${forkPaneId}`);
|
|
893
|
+
const existingIds = await getExistingSessionIds();
|
|
894
|
+
logger.debug(`Existing sessions before fork: ${existingIds.size}`);
|
|
895
|
+
await this.initializeClaude(forkPaneId, {
|
|
896
|
+
type: "fork",
|
|
897
|
+
parentSessionId: session.main.claudeSessionId,
|
|
898
|
+
forkName
|
|
899
|
+
});
|
|
900
|
+
logger.info("Detecting fork session ID from history...");
|
|
901
|
+
const detectedForkId = await detectNewSessionId(existingIds, 3e4, 500);
|
|
902
|
+
if (!detectedForkId) {
|
|
903
|
+
throw new Error(
|
|
904
|
+
"Failed to detect fork session ID. Fork may not have been created. Check if the parent session is valid."
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
logger.info(`Fork session ID detected: ${detectedForkId}`);
|
|
908
|
+
const fork = {
|
|
909
|
+
id: forkId,
|
|
910
|
+
name: forkName,
|
|
911
|
+
claudeSessionId: detectedForkId,
|
|
912
|
+
// ✅ ID real detectado
|
|
913
|
+
tmuxPaneId: forkPaneId,
|
|
914
|
+
status: "active",
|
|
915
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
916
|
+
};
|
|
917
|
+
session.forks.push(fork);
|
|
918
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
919
|
+
await this.stateManager.replaceSession(session);
|
|
920
|
+
logger.info(`Fork created: ${forkName} (${forkId})`);
|
|
921
|
+
return fork;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Restaurar un fork guardado
|
|
925
|
+
*/
|
|
926
|
+
async resumeFork(sessionId, forkId) {
|
|
927
|
+
const session = await this.getSession(sessionId);
|
|
928
|
+
if (!session) {
|
|
929
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
930
|
+
}
|
|
931
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
932
|
+
if (!fork) {
|
|
933
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
934
|
+
}
|
|
935
|
+
logger.info(`Resuming fork: ${fork.name}`);
|
|
936
|
+
await TmuxCommands.splitPane(session.tmuxSessionId, false);
|
|
937
|
+
await sleep(1e3);
|
|
938
|
+
const allPanes = await TmuxCommands.listPanes(session.tmuxSessionId);
|
|
939
|
+
const forkPaneId = allPanes[allPanes.length - 1];
|
|
940
|
+
await this.initializeClaude(forkPaneId, {
|
|
941
|
+
type: "resume",
|
|
942
|
+
resumeSessionId: fork.claudeSessionId,
|
|
943
|
+
sessionName: fork.name
|
|
944
|
+
});
|
|
945
|
+
fork.tmuxPaneId = forkPaneId;
|
|
946
|
+
fork.status = "active";
|
|
947
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
948
|
+
await this.stateManager.replaceSession(session);
|
|
949
|
+
logger.info(`Fork resumed: ${fork.name}`);
|
|
950
|
+
return fork;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Cerrar un fork
|
|
954
|
+
*/
|
|
955
|
+
async closeFork(sessionId, forkId) {
|
|
956
|
+
const session = await this.getSession(sessionId);
|
|
957
|
+
if (!session) {
|
|
958
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
959
|
+
}
|
|
960
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
961
|
+
if (!fork) {
|
|
962
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
963
|
+
}
|
|
964
|
+
logger.info(`Closing fork: ${fork.name}`);
|
|
965
|
+
if (fork.tmuxPaneId) {
|
|
966
|
+
await TmuxCommands.killPane(fork.tmuxPaneId);
|
|
967
|
+
}
|
|
968
|
+
fork.status = "saved";
|
|
969
|
+
fork.tmuxPaneId = void 0;
|
|
970
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
971
|
+
await this.stateManager.replaceSession(session);
|
|
972
|
+
logger.info(`Fork closed: ${fork.name}`);
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Eliminar un fork permanentemente
|
|
976
|
+
*/
|
|
977
|
+
async deleteFork(sessionId, forkId) {
|
|
978
|
+
const session = await this.getSession(sessionId);
|
|
979
|
+
if (!session) {
|
|
980
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
981
|
+
}
|
|
982
|
+
const forkIndex = session.forks.findIndex((f) => f.id === forkId);
|
|
983
|
+
if (forkIndex === -1) {
|
|
984
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
985
|
+
}
|
|
986
|
+
const fork = session.forks[forkIndex];
|
|
987
|
+
logger.info(`Deleting fork: ${fork.name}`);
|
|
988
|
+
if (fork.status === "active") {
|
|
989
|
+
await this.closeFork(sessionId, forkId);
|
|
990
|
+
}
|
|
991
|
+
session.forks.splice(forkIndex, 1);
|
|
992
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
993
|
+
await this.stateManager.replaceSession(session);
|
|
994
|
+
logger.info(`Fork deleted: ${fork.name}`);
|
|
995
|
+
}
|
|
996
|
+
// ==========================================
|
|
997
|
+
// COMANDOS
|
|
998
|
+
// ==========================================
|
|
999
|
+
/**
|
|
1000
|
+
* Enviar comando a main
|
|
1001
|
+
*/
|
|
1002
|
+
async sendToMain(sessionId, command) {
|
|
1003
|
+
const session = await this.getSession(sessionId);
|
|
1004
|
+
if (!session) {
|
|
1005
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1006
|
+
}
|
|
1007
|
+
if (!session.main.tmuxPaneId) {
|
|
1008
|
+
throw new Error("Main pane is not active");
|
|
1009
|
+
}
|
|
1010
|
+
logger.info(`Sending command to main: ${command}`);
|
|
1011
|
+
await TmuxCommands.sendKeys(session.main.tmuxPaneId, command);
|
|
1012
|
+
await TmuxCommands.sendEnter(session.main.tmuxPaneId);
|
|
1013
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1014
|
+
await this.stateManager.replaceSession(session);
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Enviar comando a un fork
|
|
1018
|
+
*/
|
|
1019
|
+
async sendToFork(sessionId, forkId, command) {
|
|
1020
|
+
const session = await this.getSession(sessionId);
|
|
1021
|
+
if (!session) {
|
|
1022
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1023
|
+
}
|
|
1024
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
1025
|
+
if (!fork) {
|
|
1026
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
1027
|
+
}
|
|
1028
|
+
if (!fork.tmuxPaneId) {
|
|
1029
|
+
throw new Error("Fork pane is not active");
|
|
1030
|
+
}
|
|
1031
|
+
logger.info(`Sending command to fork ${fork.name}: ${command}`);
|
|
1032
|
+
await TmuxCommands.sendKeys(fork.tmuxPaneId, command);
|
|
1033
|
+
await TmuxCommands.sendEnter(fork.tmuxPaneId);
|
|
1034
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1035
|
+
await this.stateManager.replaceSession(session);
|
|
1036
|
+
}
|
|
1037
|
+
// ==========================================
|
|
1038
|
+
// EXPORT & MERGE
|
|
1039
|
+
// ==========================================
|
|
1040
|
+
/**
|
|
1041
|
+
* Generar export de un fork con resumen
|
|
1042
|
+
* Envía un prompt a Claude pidiendo que genere resumen y exporte
|
|
1043
|
+
*/
|
|
1044
|
+
async generateForkExport(sessionId, forkId) {
|
|
1045
|
+
const session = await this.getSession(sessionId);
|
|
1046
|
+
if (!session) {
|
|
1047
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1048
|
+
}
|
|
1049
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
1050
|
+
if (!fork) {
|
|
1051
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
1052
|
+
}
|
|
1053
|
+
logger.info(`Generating export for fork: ${fork.name}`);
|
|
1054
|
+
const exportsDir = path3.join(this.projectPath, ".claude-orka", "exports");
|
|
1055
|
+
await fs3.ensureDir(exportsDir);
|
|
1056
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1057
|
+
const exportName = `fork-${fork.name}-${timestamp}.md`;
|
|
1058
|
+
const relativeExportPath = `.claude-orka/exports/${exportName}`;
|
|
1059
|
+
const prompt = `
|
|
1060
|
+
Please generate a complete summary of this fork conversation "${fork.name}" and save it to the file:
|
|
1061
|
+
\`${relativeExportPath}\`
|
|
1062
|
+
|
|
1063
|
+
The summary should include:
|
|
1064
|
+
|
|
1065
|
+
## Executive Summary
|
|
1066
|
+
- What was attempted to achieve in this fork
|
|
1067
|
+
- Why this exploration branch was created
|
|
1068
|
+
|
|
1069
|
+
## Changes Made
|
|
1070
|
+
- Detailed list of changes, modified files, written code
|
|
1071
|
+
- Technical decisions made
|
|
1072
|
+
|
|
1073
|
+
## Results
|
|
1074
|
+
- What works correctly
|
|
1075
|
+
- What problems were encountered
|
|
1076
|
+
- What remains pending
|
|
1077
|
+
|
|
1078
|
+
## Recommendations
|
|
1079
|
+
- Suggested next steps
|
|
1080
|
+
- How to integrate this to main
|
|
1081
|
+
- Important considerations
|
|
1082
|
+
|
|
1083
|
+
Write the summary in Markdown format and save it to the specified file.
|
|
1084
|
+
`.trim();
|
|
1085
|
+
if (!fork.tmuxPaneId) {
|
|
1086
|
+
throw new Error("Fork pane is not active. Cannot send export command.");
|
|
1087
|
+
}
|
|
1088
|
+
await TmuxCommands.sendKeys(fork.tmuxPaneId, prompt);
|
|
1089
|
+
await TmuxCommands.sendEnter(fork.tmuxPaneId);
|
|
1090
|
+
fork.contextPath = relativeExportPath;
|
|
1091
|
+
await this.stateManager.replaceSession(session);
|
|
1092
|
+
logger.info(`Export generation requested. Path: ${relativeExportPath}`);
|
|
1093
|
+
logger.warn("IMPORTANT: Wait for Claude to complete before calling merge()");
|
|
1094
|
+
return relativeExportPath;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Hacer merge de un fork a main
|
|
1098
|
+
* PREREQUISITO: Debes llamar a generateForkExport() primero y esperar
|
|
1099
|
+
*/
|
|
1100
|
+
async mergeFork(sessionId, forkId) {
|
|
1101
|
+
const session = await this.getSession(sessionId);
|
|
1102
|
+
if (!session) {
|
|
1103
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1104
|
+
}
|
|
1105
|
+
const fork = session.forks.find((f) => f.id === forkId);
|
|
1106
|
+
if (!fork) {
|
|
1107
|
+
throw new Error(`Fork ${forkId} not found`);
|
|
1108
|
+
}
|
|
1109
|
+
if (!fork.contextPath) {
|
|
1110
|
+
throw new Error(
|
|
1111
|
+
"Fork does not have an exported context. Call generateForkExport() first."
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
logger.info(`Merging fork ${fork.name} to main`);
|
|
1115
|
+
const fullPath = path3.join(this.projectPath, fork.contextPath);
|
|
1116
|
+
const exists = await fs3.pathExists(fullPath);
|
|
1117
|
+
if (!exists) {
|
|
1118
|
+
throw new Error(
|
|
1119
|
+
`Export file not found: ${fork.contextPath}. Make sure generateForkExport() completed.`
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
const mergePrompt = `
|
|
1123
|
+
I have completed work on the fork "${fork.name}".
|
|
1124
|
+
Please read the file \`${fork.contextPath}\` which contains:
|
|
1125
|
+
1. An executive summary of the work completed
|
|
1126
|
+
2. The complete context of the fork conversation
|
|
1127
|
+
|
|
1128
|
+
Analyze the content and help me integrate the changes and learnings from the fork into this main conversation.
|
|
1129
|
+
`.trim();
|
|
1130
|
+
if (!session.main.tmuxPaneId) {
|
|
1131
|
+
throw new Error("Main pane is not active. Cannot send merge command.");
|
|
1132
|
+
}
|
|
1133
|
+
await TmuxCommands.sendKeys(session.main.tmuxPaneId, mergePrompt);
|
|
1134
|
+
await TmuxCommands.sendEnter(session.main.tmuxPaneId);
|
|
1135
|
+
fork.status = "merged";
|
|
1136
|
+
fork.mergedToMain = true;
|
|
1137
|
+
fork.mergedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1138
|
+
if (fork.tmuxPaneId) {
|
|
1139
|
+
await TmuxCommands.killPane(fork.tmuxPaneId);
|
|
1140
|
+
fork.tmuxPaneId = void 0;
|
|
1141
|
+
}
|
|
1142
|
+
session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1143
|
+
await this.stateManager.replaceSession(session);
|
|
1144
|
+
logger.info(`Fork ${fork.name} merged to main`);
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Export manual de un fork (deprecated - usa generateForkExport)
|
|
1148
|
+
*/
|
|
1149
|
+
async exportFork(sessionId, forkId) {
|
|
1150
|
+
logger.warn("exportFork() is deprecated. Use generateForkExport() instead.");
|
|
1151
|
+
return await this.generateForkExport(sessionId, forkId);
|
|
1152
|
+
}
|
|
1153
|
+
// ==========================================
|
|
1154
|
+
// HELPERS PRIVADOS
|
|
1155
|
+
// ==========================================
|
|
1156
|
+
/**
|
|
1157
|
+
* Initialize Claude en un pane con prompt inicial
|
|
1158
|
+
*/
|
|
1159
|
+
async initializeClaude(paneId, options) {
|
|
1160
|
+
const { type, sessionId, resumeSessionId, parentSessionId, sessionName, forkName } = options;
|
|
1161
|
+
await TmuxCommands.sendKeys(paneId, `cd ${this.projectPath}`);
|
|
1162
|
+
await TmuxCommands.sendEnter(paneId);
|
|
1163
|
+
await sleep(500);
|
|
1164
|
+
let command = "";
|
|
1165
|
+
switch (type) {
|
|
1166
|
+
case "new":
|
|
1167
|
+
const newPrompt = `Hello, this is a new main session called "${sessionName}". We are working on the project.`;
|
|
1168
|
+
command = `claude --session-id ${sessionId} "${newPrompt}"`;
|
|
1169
|
+
break;
|
|
1170
|
+
case "resume":
|
|
1171
|
+
const resumePrompt = `Resuming session "${sessionName}".`;
|
|
1172
|
+
command = `claude --resume ${resumeSessionId} "${resumePrompt}"`;
|
|
1173
|
+
break;
|
|
1174
|
+
case "fork":
|
|
1175
|
+
const forkPrompt = `This is a fork called "${forkName}". Keep in mind we are exploring an alternative to the main conversation.`;
|
|
1176
|
+
command = `claude --resume ${parentSessionId} --fork-session "${forkPrompt}"`;
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
logger.info(`Executing: ${command}`);
|
|
1180
|
+
await TmuxCommands.sendKeys(paneId, command);
|
|
1181
|
+
await TmuxCommands.sendEnter(paneId);
|
|
1182
|
+
await sleep(8e3);
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
// src/core/ClaudeOrka.ts
|
|
1187
|
+
var ClaudeOrka = class {
|
|
1188
|
+
sessionManager;
|
|
1189
|
+
/**
|
|
1190
|
+
* Create a ClaudeOrka instance
|
|
1191
|
+
* @param projectPath Absolute path to the project
|
|
1192
|
+
*/
|
|
1193
|
+
constructor(projectPath) {
|
|
1194
|
+
this.sessionManager = new SessionManager(projectPath);
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Initialize ClaudeOrka
|
|
1198
|
+
* Creates the .claude-orka/ structure if it doesn't exist
|
|
1199
|
+
*/
|
|
1200
|
+
async initialize() {
|
|
1201
|
+
logger.info("Initializing ClaudeOrka");
|
|
1202
|
+
await this.sessionManager.initialize();
|
|
1203
|
+
}
|
|
1204
|
+
// --- SESSIONS ---
|
|
1205
|
+
/**
|
|
1206
|
+
* Create a new Claude Code session
|
|
1207
|
+
* @param name Optional name for the session
|
|
1208
|
+
* @param openTerminal Whether to open a terminal window (default: true)
|
|
1209
|
+
* @returns Created session
|
|
1210
|
+
*/
|
|
1211
|
+
async createSession(name, openTerminal) {
|
|
1212
|
+
return await this.sessionManager.createSession(name, openTerminal);
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Resume a saved session
|
|
1216
|
+
* @param sessionId Session ID to resume
|
|
1217
|
+
* @param openTerminal Whether to open a terminal window (default: true)
|
|
1218
|
+
* @returns Resumed session
|
|
1219
|
+
*/
|
|
1220
|
+
async resumeSession(sessionId, openTerminal) {
|
|
1221
|
+
return await this.sessionManager.resumeSession(sessionId, openTerminal);
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Close a session
|
|
1225
|
+
* @param sessionId Session ID
|
|
1226
|
+
*/
|
|
1227
|
+
async closeSession(sessionId) {
|
|
1228
|
+
await this.sessionManager.closeSession(sessionId);
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Permanently delete a session
|
|
1232
|
+
* @param sessionId Session ID
|
|
1233
|
+
*/
|
|
1234
|
+
async deleteSession(sessionId) {
|
|
1235
|
+
await this.sessionManager.deleteSession(sessionId);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* List sessions with optional filters
|
|
1239
|
+
* @param filters Optional filters (status, name)
|
|
1240
|
+
* @returns Array of sessions
|
|
1241
|
+
*/
|
|
1242
|
+
async listSessions(filters) {
|
|
1243
|
+
return await this.sessionManager.listSessions(filters);
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Get a session by ID
|
|
1247
|
+
* @param sessionId Session ID
|
|
1248
|
+
* @returns Session or null if not found
|
|
1249
|
+
*/
|
|
1250
|
+
async getSession(sessionId) {
|
|
1251
|
+
return await this.sessionManager.getSession(sessionId);
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Get complete project summary
|
|
1255
|
+
* Includes statistics of all sessions and their forks
|
|
1256
|
+
* @returns Project summary with all sessions and statistics
|
|
1257
|
+
*/
|
|
1258
|
+
async getProjectSummary() {
|
|
1259
|
+
const sessions = await this.sessionManager.listSessions();
|
|
1260
|
+
const state = await this.sessionManager.getState();
|
|
1261
|
+
const sessionSummaries = sessions.map((session) => {
|
|
1262
|
+
const forkSummaries = session.forks.map((fork) => ({
|
|
1263
|
+
id: fork.id,
|
|
1264
|
+
name: fork.name,
|
|
1265
|
+
claudeSessionId: fork.claudeSessionId,
|
|
1266
|
+
status: fork.status,
|
|
1267
|
+
createdAt: fork.createdAt,
|
|
1268
|
+
hasContext: !!fork.contextPath,
|
|
1269
|
+
contextPath: fork.contextPath,
|
|
1270
|
+
mergedToMain: fork.mergedToMain || false,
|
|
1271
|
+
mergedAt: fork.mergedAt
|
|
1272
|
+
}));
|
|
1273
|
+
const activeForks = session.forks.filter((f) => f.status === "active").length;
|
|
1274
|
+
const savedForks = session.forks.filter((f) => f.status === "saved").length;
|
|
1275
|
+
const mergedForks = session.forks.filter((f) => f.status === "merged").length;
|
|
1276
|
+
return {
|
|
1277
|
+
id: session.id,
|
|
1278
|
+
name: session.name,
|
|
1279
|
+
claudeSessionId: session.main.claudeSessionId,
|
|
1280
|
+
status: session.status,
|
|
1281
|
+
createdAt: session.createdAt,
|
|
1282
|
+
lastActivity: session.lastActivity,
|
|
1283
|
+
totalForks: session.forks.length,
|
|
1284
|
+
activeForks,
|
|
1285
|
+
savedForks,
|
|
1286
|
+
mergedForks,
|
|
1287
|
+
forks: forkSummaries
|
|
1288
|
+
};
|
|
1289
|
+
});
|
|
1290
|
+
const activeSessions = sessions.filter((s) => s.status === "active").length;
|
|
1291
|
+
const savedSessions = sessions.filter((s) => s.status === "saved").length;
|
|
1292
|
+
return {
|
|
1293
|
+
projectPath: state.projectPath,
|
|
1294
|
+
totalSessions: sessions.length,
|
|
1295
|
+
activeSessions,
|
|
1296
|
+
savedSessions,
|
|
1297
|
+
sessions: sessionSummaries,
|
|
1298
|
+
lastUpdated: state.lastUpdated
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
// --- FORKS ---
|
|
1302
|
+
/**
|
|
1303
|
+
* Create a fork (conversation branch)
|
|
1304
|
+
* @param sessionId Session ID
|
|
1305
|
+
* @param name Optional fork name
|
|
1306
|
+
* @param vertical Whether to split vertically (default: false = horizontal)
|
|
1307
|
+
* @returns Created fork
|
|
1308
|
+
*/
|
|
1309
|
+
async createFork(sessionId, name, vertical) {
|
|
1310
|
+
return await this.sessionManager.createFork(sessionId, name, vertical);
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Close a fork
|
|
1314
|
+
* @param sessionId Session ID
|
|
1315
|
+
* @param forkId Fork ID
|
|
1316
|
+
*/
|
|
1317
|
+
async closeFork(sessionId, forkId) {
|
|
1318
|
+
await this.sessionManager.closeFork(sessionId, forkId);
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Resume a saved fork
|
|
1322
|
+
* @param sessionId Session ID
|
|
1323
|
+
* @param forkId Fork ID
|
|
1324
|
+
* @returns Resumed fork
|
|
1325
|
+
*/
|
|
1326
|
+
async resumeFork(sessionId, forkId) {
|
|
1327
|
+
return await this.sessionManager.resumeFork(sessionId, forkId);
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Permanently delete a fork
|
|
1331
|
+
* @param sessionId Session ID
|
|
1332
|
+
* @param forkId Fork ID
|
|
1333
|
+
*/
|
|
1334
|
+
async deleteFork(sessionId, forkId) {
|
|
1335
|
+
await this.sessionManager.deleteFork(sessionId, forkId);
|
|
1336
|
+
}
|
|
1337
|
+
// --- COMANDOS ---
|
|
1338
|
+
/**
|
|
1339
|
+
* Send command to a session or fork
|
|
1340
|
+
* @param sessionId Session ID
|
|
1341
|
+
* @param command Command to send
|
|
1342
|
+
* @param target Fork ID (optional, if not specified goes to main)
|
|
1343
|
+
*/
|
|
1344
|
+
async send(sessionId, command, target) {
|
|
1345
|
+
if (target) {
|
|
1346
|
+
await this.sessionManager.sendToFork(sessionId, target, command);
|
|
1347
|
+
} else {
|
|
1348
|
+
await this.sessionManager.sendToMain(sessionId, command);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
// --- EXPORT & MERGE ---
|
|
1352
|
+
/**
|
|
1353
|
+
* Export fork context (old method - uses manual capture)
|
|
1354
|
+
* @deprecated Use generateForkExport() instead for Claude to generate the summary
|
|
1355
|
+
* @param sessionId Session ID
|
|
1356
|
+
* @param forkId Fork ID
|
|
1357
|
+
* @returns Path to the exported file
|
|
1358
|
+
*/
|
|
1359
|
+
async export(sessionId, forkId) {
|
|
1360
|
+
return await this.sessionManager.exportFork(sessionId, forkId);
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Generate fork export with summary
|
|
1364
|
+
*
|
|
1365
|
+
* Sends a prompt to Claude requesting:
|
|
1366
|
+
* 1. Generate executive summary of the conversation
|
|
1367
|
+
* 2. Export using /export to the specified path
|
|
1368
|
+
*
|
|
1369
|
+
* IMPORTANT: This method is async but returns immediately.
|
|
1370
|
+
* Claude will execute tasks in the background. Wait a few seconds before calling merge().
|
|
1371
|
+
*
|
|
1372
|
+
* @param sessionId Session ID
|
|
1373
|
+
* @param forkId Fork ID
|
|
1374
|
+
* @returns Path where Claude will save the export
|
|
1375
|
+
*/
|
|
1376
|
+
async generateForkExport(sessionId, forkId) {
|
|
1377
|
+
return await this.sessionManager.generateForkExport(sessionId, forkId);
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Merge a fork to main
|
|
1381
|
+
*
|
|
1382
|
+
* PREREQUISITE: You must call generateForkExport() first and wait for Claude to complete
|
|
1383
|
+
*
|
|
1384
|
+
* @param sessionId Session ID
|
|
1385
|
+
* @param forkId Fork ID
|
|
1386
|
+
*/
|
|
1387
|
+
async merge(sessionId, forkId) {
|
|
1388
|
+
await this.sessionManager.mergeFork(sessionId, forkId);
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Generate export and merge a fork to main (recommended method)
|
|
1392
|
+
*
|
|
1393
|
+
* Workflow:
|
|
1394
|
+
* 1. Generates export with summary (Claude does the work)
|
|
1395
|
+
* 2. Wait for the file to be created
|
|
1396
|
+
* 3. Merge to main
|
|
1397
|
+
*
|
|
1398
|
+
* @param sessionId Session ID
|
|
1399
|
+
* @param forkId Fork ID
|
|
1400
|
+
* @param waitTime Wait time in ms for Claude to complete (default: 15000)
|
|
1401
|
+
*/
|
|
1402
|
+
async generateExportAndMerge(sessionId, forkId, waitTime = 15e3) {
|
|
1403
|
+
await this.generateForkExport(sessionId, forkId);
|
|
1404
|
+
logger.info(`Waiting ${waitTime}ms for Claude to complete export...`);
|
|
1405
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
1406
|
+
await this.merge(sessionId, forkId);
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Generate export, merge and close a fork (complete flow)
|
|
1410
|
+
*
|
|
1411
|
+
* @param sessionId Session ID
|
|
1412
|
+
* @param forkId Fork ID
|
|
1413
|
+
* @param waitTime Wait time in ms for Claude to complete (default: 15000)
|
|
1414
|
+
*/
|
|
1415
|
+
async generateExportMergeAndClose(sessionId, forkId, waitTime = 15e3) {
|
|
1416
|
+
await this.generateExportAndMerge(sessionId, forkId, waitTime);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Export and merge a fork to main (old method)
|
|
1420
|
+
* @deprecated Usa generateExportAndMerge() en su lugar
|
|
1421
|
+
*/
|
|
1422
|
+
async exportAndMerge(sessionId, forkId) {
|
|
1423
|
+
await this.export(sessionId, forkId);
|
|
1424
|
+
await this.merge(sessionId, forkId);
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Export, merge and close a fork (old method)
|
|
1428
|
+
* @deprecated Usa generateExportMergeAndClose() en su lugar
|
|
1429
|
+
*/
|
|
1430
|
+
async mergeAndClose(sessionId, forkId) {
|
|
1431
|
+
await this.exportAndMerge(sessionId, forkId);
|
|
1432
|
+
await this.closeFork(sessionId, forkId);
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
// src/cli/utils/output.ts
|
|
1437
|
+
import chalk from "chalk";
|
|
1438
|
+
import Table from "cli-table3";
|
|
1439
|
+
var Output = class {
|
|
1440
|
+
/**
|
|
1441
|
+
* Success message
|
|
1442
|
+
*/
|
|
1443
|
+
static success(message) {
|
|
1444
|
+
console.log(chalk.green("\u2713"), message);
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Error message
|
|
1448
|
+
*/
|
|
1449
|
+
static error(message) {
|
|
1450
|
+
console.error(chalk.red("\u2717"), message);
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Warning message
|
|
1454
|
+
*/
|
|
1455
|
+
static warn(message) {
|
|
1456
|
+
console.warn(chalk.yellow("\u26A0"), message);
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Info message
|
|
1460
|
+
*/
|
|
1461
|
+
static info(message) {
|
|
1462
|
+
console.log(chalk.blue("\u2139"), message);
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Header
|
|
1466
|
+
*/
|
|
1467
|
+
static header(message) {
|
|
1468
|
+
console.log("\n" + chalk.bold.cyan(message));
|
|
1469
|
+
console.log(chalk.gray("\u2500".repeat(message.length)));
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Section
|
|
1473
|
+
*/
|
|
1474
|
+
static section(title) {
|
|
1475
|
+
console.log("\n" + chalk.bold(title));
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Display session details
|
|
1479
|
+
*/
|
|
1480
|
+
static session(session) {
|
|
1481
|
+
const statusColor = session.status === "active" ? chalk.green : chalk.yellow;
|
|
1482
|
+
const statusEmoji = session.status === "active" ? "\u2713" : "\u{1F4BE}";
|
|
1483
|
+
console.log(`
|
|
1484
|
+
${statusEmoji} ${chalk.bold(session.name)}`);
|
|
1485
|
+
console.log(` ${chalk.gray("ID:")} ${session.id}`);
|
|
1486
|
+
console.log(` ${chalk.gray("Claude Session:")} ${session.main.claudeSessionId}`);
|
|
1487
|
+
console.log(` ${chalk.gray("Status:")} ${statusColor(session.status)}`);
|
|
1488
|
+
console.log(` ${chalk.gray("Created:")} ${new Date(session.createdAt).toLocaleString()}`);
|
|
1489
|
+
console.log(` ${chalk.gray("Last Activity:")} ${new Date(session.lastActivity).toLocaleString()}`);
|
|
1490
|
+
if (session.status === "active") {
|
|
1491
|
+
console.log(` ${chalk.gray("Tmux Session:")} ${session.tmuxSessionId}`);
|
|
1492
|
+
}
|
|
1493
|
+
if (session.forks.length > 0) {
|
|
1494
|
+
console.log(` ${chalk.gray("Forks:")} ${session.forks.length}`);
|
|
1495
|
+
console.log(
|
|
1496
|
+
` ${chalk.green("Active:")} ${session.forks.filter((f) => f.status === "active").length}`
|
|
1497
|
+
);
|
|
1498
|
+
console.log(
|
|
1499
|
+
` ${chalk.yellow("Saved:")} ${session.forks.filter((f) => f.status === "saved").length}`
|
|
1500
|
+
);
|
|
1501
|
+
console.log(
|
|
1502
|
+
` ${chalk.blue("Merged:")} ${session.forks.filter((f) => f.status === "merged").length}`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Display fork details
|
|
1508
|
+
*/
|
|
1509
|
+
static fork(fork) {
|
|
1510
|
+
const statusColor = fork.status === "active" ? chalk.green : fork.status === "merged" ? chalk.blue : chalk.yellow;
|
|
1511
|
+
const statusEmoji = fork.status === "active" ? "\u2713" : fork.status === "merged" ? "\u{1F500}" : "\u{1F4BE}";
|
|
1512
|
+
console.log(`
|
|
1513
|
+
${statusEmoji} ${chalk.bold(fork.name)}`);
|
|
1514
|
+
console.log(` ${chalk.gray("ID:")} ${fork.id}`);
|
|
1515
|
+
console.log(` ${chalk.gray("Claude Session:")} ${fork.claudeSessionId}`);
|
|
1516
|
+
console.log(` ${chalk.gray("Status:")} ${statusColor(fork.status)}`);
|
|
1517
|
+
console.log(` ${chalk.gray("Created:")} ${new Date(fork.createdAt).toLocaleString()}`);
|
|
1518
|
+
if (fork.status === "active" && fork.tmuxPaneId) {
|
|
1519
|
+
console.log(` ${chalk.gray("Tmux Pane:")} ${fork.tmuxPaneId}`);
|
|
1520
|
+
}
|
|
1521
|
+
if (fork.contextPath) {
|
|
1522
|
+
console.log(` ${chalk.gray("Export:")} ${fork.contextPath}`);
|
|
1523
|
+
}
|
|
1524
|
+
if (fork.mergedToMain && fork.mergedAt) {
|
|
1525
|
+
console.log(
|
|
1526
|
+
` ${chalk.gray("Merged:")} ${new Date(fork.mergedAt).toLocaleString()}`
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Display sessions table
|
|
1532
|
+
*/
|
|
1533
|
+
static sessionsTable(sessions) {
|
|
1534
|
+
if (sessions.length === 0) {
|
|
1535
|
+
this.warn("No sessions found");
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
const table = new Table({
|
|
1539
|
+
head: [
|
|
1540
|
+
chalk.bold("Name"),
|
|
1541
|
+
chalk.bold("Status"),
|
|
1542
|
+
chalk.bold("Forks"),
|
|
1543
|
+
chalk.bold("Created"),
|
|
1544
|
+
chalk.bold("ID")
|
|
1545
|
+
],
|
|
1546
|
+
colWidths: [25, 12, 20, 20, 40]
|
|
1547
|
+
});
|
|
1548
|
+
for (const session of sessions) {
|
|
1549
|
+
const statusColor = session.status === "active" ? chalk.green : chalk.yellow;
|
|
1550
|
+
const activeForks = session.forks.filter((f) => f.status === "active").length;
|
|
1551
|
+
const savedForks = session.forks.filter((f) => f.status === "saved").length;
|
|
1552
|
+
const mergedForks = session.forks.filter((f) => f.status === "merged").length;
|
|
1553
|
+
table.push([
|
|
1554
|
+
session.name,
|
|
1555
|
+
statusColor(session.status),
|
|
1556
|
+
`${chalk.green(activeForks)}/${chalk.yellow(savedForks)}/${chalk.blue(mergedForks)}`,
|
|
1557
|
+
new Date(session.createdAt).toLocaleDateString(),
|
|
1558
|
+
chalk.gray(session.id.substring(0, 8) + "...")
|
|
1559
|
+
]);
|
|
1560
|
+
}
|
|
1561
|
+
console.log(table.toString());
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Display forks table
|
|
1565
|
+
*/
|
|
1566
|
+
static forksTable(forks) {
|
|
1567
|
+
if (forks.length === 0) {
|
|
1568
|
+
this.warn("No forks found");
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const table = new Table({
|
|
1572
|
+
head: [
|
|
1573
|
+
chalk.bold("Name"),
|
|
1574
|
+
chalk.bold("Status"),
|
|
1575
|
+
chalk.bold("Export"),
|
|
1576
|
+
chalk.bold("Created"),
|
|
1577
|
+
chalk.bold("ID")
|
|
1578
|
+
],
|
|
1579
|
+
colWidths: [25, 12, 15, 20, 40]
|
|
1580
|
+
});
|
|
1581
|
+
for (const fork of forks) {
|
|
1582
|
+
const statusColor = fork.status === "active" ? chalk.green : fork.status === "merged" ? chalk.blue : chalk.yellow;
|
|
1583
|
+
const hasExport = fork.contextPath ? chalk.green("\u2713") : chalk.gray("\u2717");
|
|
1584
|
+
table.push([
|
|
1585
|
+
fork.name,
|
|
1586
|
+
statusColor(fork.status),
|
|
1587
|
+
hasExport,
|
|
1588
|
+
new Date(fork.createdAt).toLocaleDateString(),
|
|
1589
|
+
chalk.gray(fork.id.substring(0, 8) + "...")
|
|
1590
|
+
]);
|
|
1591
|
+
}
|
|
1592
|
+
console.log(table.toString());
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Display project summary
|
|
1596
|
+
*/
|
|
1597
|
+
static projectSummary(summary) {
|
|
1598
|
+
this.header("\u{1F4CA} Project Summary");
|
|
1599
|
+
console.log(`
|
|
1600
|
+
${chalk.gray("Project Path:")} ${summary.projectPath}`);
|
|
1601
|
+
console.log(`${chalk.gray("Total Sessions:")} ${summary.totalSessions}`);
|
|
1602
|
+
console.log(` ${chalk.green("Active:")} ${summary.activeSessions}`);
|
|
1603
|
+
console.log(` ${chalk.yellow("Saved:")} ${summary.savedSessions}`);
|
|
1604
|
+
console.log(
|
|
1605
|
+
`${chalk.gray("Last Updated:")} ${new Date(summary.lastUpdated).toLocaleString()}`
|
|
1606
|
+
);
|
|
1607
|
+
if (summary.sessions.length === 0) {
|
|
1608
|
+
console.log("\n" + chalk.gray("No sessions available"));
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
this.section("\n\u{1F4DD} Sessions:");
|
|
1612
|
+
for (const session of summary.sessions) {
|
|
1613
|
+
const statusEmoji = session.status === "active" ? "\u2713" : "\u{1F4BE}";
|
|
1614
|
+
const statusColor = session.status === "active" ? chalk.green : chalk.yellow;
|
|
1615
|
+
console.log(`
|
|
1616
|
+
${statusEmoji} ${chalk.bold(session.name)}`);
|
|
1617
|
+
console.log(` ${chalk.gray("ID:")} ${session.id}`);
|
|
1618
|
+
console.log(` ${chalk.gray("Claude Session:")} ${session.claudeSessionId}`);
|
|
1619
|
+
console.log(` ${chalk.gray("Status:")} ${statusColor(session.status)}`);
|
|
1620
|
+
console.log(
|
|
1621
|
+
` ${chalk.gray("Created:")} ${new Date(session.createdAt).toLocaleString()}`
|
|
1622
|
+
);
|
|
1623
|
+
console.log(
|
|
1624
|
+
` ${chalk.gray("Last Activity:")} ${new Date(session.lastActivity).toLocaleString()}`
|
|
1625
|
+
);
|
|
1626
|
+
console.log(` ${chalk.gray("Total Forks:")} ${session.totalForks}`);
|
|
1627
|
+
console.log(
|
|
1628
|
+
` ${chalk.green("Active:")} ${session.activeForks} | ${chalk.yellow("Saved:")} ${session.savedForks} | ${chalk.blue("Merged:")} ${session.mergedForks}`
|
|
1629
|
+
);
|
|
1630
|
+
if (session.forks.length > 0) {
|
|
1631
|
+
console.log(`
|
|
1632
|
+
${chalk.bold("Forks:")}`);
|
|
1633
|
+
for (const fork of session.forks) {
|
|
1634
|
+
const forkEmoji = fork.status === "active" ? "\u2713" : fork.status === "merged" ? "\u{1F500}" : "\u{1F4BE}";
|
|
1635
|
+
const forkColor = fork.status === "active" ? chalk.green : fork.status === "merged" ? chalk.blue : chalk.yellow;
|
|
1636
|
+
console.log(`
|
|
1637
|
+
${forkEmoji} ${chalk.bold(fork.name)}`);
|
|
1638
|
+
console.log(` ${chalk.gray("ID:")} ${fork.id}`);
|
|
1639
|
+
console.log(` ${chalk.gray("Claude Session:")} ${fork.claudeSessionId}`);
|
|
1640
|
+
console.log(` ${chalk.gray("Status:")} ${forkColor(fork.status)}`);
|
|
1641
|
+
console.log(
|
|
1642
|
+
` ${chalk.gray("Created:")} ${new Date(fork.createdAt).toLocaleString()}`
|
|
1643
|
+
);
|
|
1644
|
+
if (fork.hasContext) {
|
|
1645
|
+
console.log(` ${chalk.gray("Export:")} ${chalk.green("\u2713")} ${fork.contextPath}`);
|
|
1646
|
+
}
|
|
1647
|
+
if (fork.mergedToMain && fork.mergedAt) {
|
|
1648
|
+
console.log(
|
|
1649
|
+
` ${chalk.gray("Merged:")} ${new Date(fork.mergedAt).toLocaleString()}`
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Display JSON output
|
|
1658
|
+
*/
|
|
1659
|
+
static json(data) {
|
|
1660
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Display empty line
|
|
1664
|
+
*/
|
|
1665
|
+
static newline() {
|
|
1666
|
+
console.log();
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
// src/cli/utils/errors.ts
|
|
1671
|
+
import chalk2 from "chalk";
|
|
1672
|
+
import fs4 from "fs";
|
|
1673
|
+
import path4 from "path";
|
|
1674
|
+
var CLIError = class extends Error {
|
|
1675
|
+
constructor(message, exitCode = 1) {
|
|
1676
|
+
super(message);
|
|
1677
|
+
this.exitCode = exitCode;
|
|
1678
|
+
this.name = "CLIError";
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
function handleError(error) {
|
|
1682
|
+
if (error instanceof CLIError) {
|
|
1683
|
+
Output.error(error.message);
|
|
1684
|
+
process.exit(error.exitCode);
|
|
1685
|
+
}
|
|
1686
|
+
if (error instanceof Error) {
|
|
1687
|
+
Output.error(`Unexpected error: ${error.message}`);
|
|
1688
|
+
console.error(chalk2.gray(error.stack));
|
|
1689
|
+
process.exit(1);
|
|
1690
|
+
}
|
|
1691
|
+
Output.error("An unknown error occurred");
|
|
1692
|
+
process.exit(1);
|
|
1693
|
+
}
|
|
1694
|
+
function validateSessionId(sessionId) {
|
|
1695
|
+
if (!sessionId || sessionId.length === 0) {
|
|
1696
|
+
throw new CLIError("Session ID is required");
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function validateForkId(forkId) {
|
|
1700
|
+
if (!forkId || forkId.length === 0) {
|
|
1701
|
+
throw new CLIError("Fork ID is required");
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
function validateInitialized(projectPath) {
|
|
1705
|
+
const orkaDir = path4.join(projectPath, ".claude-orka");
|
|
1706
|
+
if (!fs4.existsSync(orkaDir)) {
|
|
1707
|
+
throw new CLIError(
|
|
1708
|
+
'Project not initialized. Run "orka init" first.',
|
|
1709
|
+
2
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/cli/commands/init.ts
|
|
1715
|
+
function initCommand(program2) {
|
|
1716
|
+
program2.command("init").description("Initialize Claude-Orka in the current project").action(async () => {
|
|
1717
|
+
try {
|
|
1718
|
+
const projectPath = process.cwd();
|
|
1719
|
+
Output.info(`Initializing Claude-Orka in: ${projectPath}`);
|
|
1720
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1721
|
+
await orka.initialize();
|
|
1722
|
+
Output.success("Claude-Orka initialized successfully!");
|
|
1723
|
+
Output.info("You can now create sessions with: orka session create");
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
handleError(error);
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/cli/commands/status.ts
|
|
1731
|
+
function statusCommand(program2) {
|
|
1732
|
+
program2.command("status").description("Show project status and all sessions").option("-j, --json", "Output as JSON").action(async (options) => {
|
|
1733
|
+
try {
|
|
1734
|
+
const projectPath = process.cwd();
|
|
1735
|
+
validateInitialized(projectPath);
|
|
1736
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1737
|
+
await orka.initialize();
|
|
1738
|
+
const summary = await orka.getProjectSummary();
|
|
1739
|
+
if (options.json) {
|
|
1740
|
+
Output.json(summary);
|
|
1741
|
+
} else {
|
|
1742
|
+
Output.projectSummary(summary);
|
|
1743
|
+
}
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
handleError(error);
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/cli/commands/session.ts
|
|
1751
|
+
import ora from "ora";
|
|
1752
|
+
function sessionCommand(program2) {
|
|
1753
|
+
const session = program2.command("session").description("Manage Claude sessions");
|
|
1754
|
+
session.command("create [name]").description("Create a new Claude session").option("--no-terminal", "Do not open terminal window").action(async (name, options) => {
|
|
1755
|
+
try {
|
|
1756
|
+
const projectPath = process.cwd();
|
|
1757
|
+
validateInitialized(projectPath);
|
|
1758
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1759
|
+
await orka.initialize();
|
|
1760
|
+
const spinner = ora("Creating session...").start();
|
|
1761
|
+
const newSession = await orka.createSession(name, options.terminal);
|
|
1762
|
+
spinner.succeed("Session created!");
|
|
1763
|
+
Output.session(newSession);
|
|
1764
|
+
Output.newline();
|
|
1765
|
+
Output.info(`You can now interact with Claude in the tmux window.`);
|
|
1766
|
+
Output.info(`To create a fork: orka fork create ${newSession.id}`);
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
handleError(error);
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
session.command("list").description("List all sessions").option("-s, --status <status>", "Filter by status (active, saved)").option("-j, --json", "Output as JSON").action(async (options) => {
|
|
1772
|
+
try {
|
|
1773
|
+
const projectPath = process.cwd();
|
|
1774
|
+
validateInitialized(projectPath);
|
|
1775
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1776
|
+
await orka.initialize();
|
|
1777
|
+
const filters = options.status ? { status: options.status } : void 0;
|
|
1778
|
+
const sessions = await orka.listSessions(filters);
|
|
1779
|
+
if (options.json) {
|
|
1780
|
+
Output.json(sessions);
|
|
1781
|
+
} else {
|
|
1782
|
+
Output.header("\u{1F4CB} Sessions");
|
|
1783
|
+
Output.sessionsTable(sessions);
|
|
1784
|
+
}
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
handleError(error);
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
session.command("get <session-id>").description("Get session details").option("-j, --json", "Output as JSON").action(async (sessionId, options) => {
|
|
1790
|
+
try {
|
|
1791
|
+
const projectPath = process.cwd();
|
|
1792
|
+
validateInitialized(projectPath);
|
|
1793
|
+
validateSessionId(sessionId);
|
|
1794
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1795
|
+
await orka.initialize();
|
|
1796
|
+
const session2 = await orka.getSession(sessionId);
|
|
1797
|
+
if (!session2) {
|
|
1798
|
+
Output.error(`Session not found: ${sessionId}`);
|
|
1799
|
+
process.exit(1);
|
|
1800
|
+
}
|
|
1801
|
+
if (options.json) {
|
|
1802
|
+
Output.json(session2);
|
|
1803
|
+
} else {
|
|
1804
|
+
Output.session(session2);
|
|
1805
|
+
if (session2.forks.length > 0) {
|
|
1806
|
+
Output.section("\n\u{1F33F} Forks:");
|
|
1807
|
+
for (const fork of session2.forks) {
|
|
1808
|
+
Output.fork(fork);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
handleError(error);
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
session.command("resume <session-id>").description("Resume a saved session").option("--no-terminal", "Do not open terminal window").action(async (sessionId, options) => {
|
|
1817
|
+
try {
|
|
1818
|
+
const projectPath = process.cwd();
|
|
1819
|
+
validateInitialized(projectPath);
|
|
1820
|
+
validateSessionId(sessionId);
|
|
1821
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1822
|
+
await orka.initialize();
|
|
1823
|
+
const spinner = ora("Resuming session...").start();
|
|
1824
|
+
const resumedSession = await orka.resumeSession(sessionId, options.terminal);
|
|
1825
|
+
spinner.succeed("Session resumed!");
|
|
1826
|
+
Output.session(resumedSession);
|
|
1827
|
+
Output.newline();
|
|
1828
|
+
Output.info("Session and all saved forks have been restored.");
|
|
1829
|
+
Output.info("Claude will remember the context of all conversations.");
|
|
1830
|
+
if (resumedSession.forks.length > 0) {
|
|
1831
|
+
const activeForks = resumedSession.forks.filter((f) => f.status === "active");
|
|
1832
|
+
if (activeForks.length > 0) {
|
|
1833
|
+
Output.newline();
|
|
1834
|
+
Output.section("Restored forks:");
|
|
1835
|
+
for (const fork of activeForks) {
|
|
1836
|
+
Output.fork(fork);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
handleError(error);
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
session.command("close <session-id>").description("Close a session (saves it for later)").action(async (sessionId) => {
|
|
1845
|
+
try {
|
|
1846
|
+
const projectPath = process.cwd();
|
|
1847
|
+
validateInitialized(projectPath);
|
|
1848
|
+
validateSessionId(sessionId);
|
|
1849
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1850
|
+
await orka.initialize();
|
|
1851
|
+
const spinner = ora("Closing session...").start();
|
|
1852
|
+
await orka.closeSession(sessionId);
|
|
1853
|
+
spinner.succeed("Session closed!");
|
|
1854
|
+
Output.info("Session has been saved. You can resume it later with:");
|
|
1855
|
+
Output.info(` orka session resume ${sessionId}`);
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
handleError(error);
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
session.command("delete <session-id>").description("Permanently delete a session").action(async (sessionId) => {
|
|
1861
|
+
try {
|
|
1862
|
+
const projectPath = process.cwd();
|
|
1863
|
+
validateInitialized(projectPath);
|
|
1864
|
+
validateSessionId(sessionId);
|
|
1865
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1866
|
+
await orka.initialize();
|
|
1867
|
+
const spinner = ora("Deleting session...").start();
|
|
1868
|
+
await orka.deleteSession(sessionId);
|
|
1869
|
+
spinner.succeed("Session deleted!");
|
|
1870
|
+
Output.warn("Session has been permanently deleted.");
|
|
1871
|
+
} catch (error) {
|
|
1872
|
+
handleError(error);
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// src/cli/commands/fork.ts
|
|
1878
|
+
import ora2 from "ora";
|
|
1879
|
+
function forkCommand(program2) {
|
|
1880
|
+
const fork = program2.command("fork").description("Manage conversation forks");
|
|
1881
|
+
fork.command("create <session-id> [name]").description("Create a new fork in a session").option("-v, --vertical", "Split vertically instead of horizontally").action(async (sessionId, name, options) => {
|
|
1882
|
+
try {
|
|
1883
|
+
const projectPath = process.cwd();
|
|
1884
|
+
validateInitialized(projectPath);
|
|
1885
|
+
validateSessionId(sessionId);
|
|
1886
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1887
|
+
await orka.initialize();
|
|
1888
|
+
const spinner = ora2("Creating fork...").start();
|
|
1889
|
+
spinner.text = "Creating fork and detecting session ID...";
|
|
1890
|
+
const newFork = await orka.createFork(sessionId, name, options.vertical);
|
|
1891
|
+
spinner.succeed("Fork created!");
|
|
1892
|
+
Output.fork(newFork);
|
|
1893
|
+
Output.newline();
|
|
1894
|
+
Output.info("Fork created in a new tmux pane.");
|
|
1895
|
+
Output.info("You can now explore an alternative approach in this fork.");
|
|
1896
|
+
Output.newline();
|
|
1897
|
+
Output.info(`To merge this fork: orka merge ${sessionId} ${newFork.id}`);
|
|
1898
|
+
} catch (error) {
|
|
1899
|
+
handleError(error);
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
fork.command("list <session-id>").description("List all forks in a session").option("-s, --status <status>", "Filter by status (active, saved, merged)").option("-j, --json", "Output as JSON").action(async (sessionId, options) => {
|
|
1903
|
+
try {
|
|
1904
|
+
const projectPath = process.cwd();
|
|
1905
|
+
validateInitialized(projectPath);
|
|
1906
|
+
validateSessionId(sessionId);
|
|
1907
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1908
|
+
await orka.initialize();
|
|
1909
|
+
const session = await orka.getSession(sessionId);
|
|
1910
|
+
if (!session) {
|
|
1911
|
+
Output.error(`Session not found: ${sessionId}`);
|
|
1912
|
+
process.exit(1);
|
|
1913
|
+
}
|
|
1914
|
+
let forks = session.forks;
|
|
1915
|
+
if (options.status) {
|
|
1916
|
+
forks = forks.filter((f) => f.status === options.status);
|
|
1917
|
+
}
|
|
1918
|
+
if (options.json) {
|
|
1919
|
+
Output.json(forks);
|
|
1920
|
+
} else {
|
|
1921
|
+
Output.header(`\u{1F33F} Forks in ${session.name}`);
|
|
1922
|
+
Output.forksTable(forks);
|
|
1923
|
+
}
|
|
1924
|
+
} catch (error) {
|
|
1925
|
+
handleError(error);
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
fork.command("resume <session-id> <fork-id>").description("Resume a saved fork").action(async (sessionId, forkId) => {
|
|
1929
|
+
try {
|
|
1930
|
+
const projectPath = process.cwd();
|
|
1931
|
+
validateInitialized(projectPath);
|
|
1932
|
+
validateSessionId(sessionId);
|
|
1933
|
+
validateForkId(forkId);
|
|
1934
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1935
|
+
await orka.initialize();
|
|
1936
|
+
const spinner = ora2("Resuming fork...").start();
|
|
1937
|
+
const resumedFork = await orka.resumeFork(sessionId, forkId);
|
|
1938
|
+
spinner.succeed("Fork resumed!");
|
|
1939
|
+
Output.fork(resumedFork);
|
|
1940
|
+
Output.newline();
|
|
1941
|
+
Output.info("Fork has been restored in a new tmux pane.");
|
|
1942
|
+
Output.info("Claude will remember the context of this conversation.");
|
|
1943
|
+
} catch (error) {
|
|
1944
|
+
handleError(error);
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
fork.command("close <session-id> <fork-id>").description("Close a fork (saves it for later)").action(async (sessionId, forkId) => {
|
|
1948
|
+
try {
|
|
1949
|
+
const projectPath = process.cwd();
|
|
1950
|
+
validateInitialized(projectPath);
|
|
1951
|
+
validateSessionId(sessionId);
|
|
1952
|
+
validateForkId(forkId);
|
|
1953
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1954
|
+
await orka.initialize();
|
|
1955
|
+
const spinner = ora2("Closing fork...").start();
|
|
1956
|
+
await orka.closeFork(sessionId, forkId);
|
|
1957
|
+
spinner.succeed("Fork closed!");
|
|
1958
|
+
Output.info("Fork has been saved. You can resume it later with:");
|
|
1959
|
+
Output.info(` orka fork resume ${sessionId} ${forkId}`);
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
handleError(error);
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
fork.command("delete <session-id> <fork-id>").description("Permanently delete a fork").action(async (sessionId, forkId) => {
|
|
1965
|
+
try {
|
|
1966
|
+
const projectPath = process.cwd();
|
|
1967
|
+
validateInitialized(projectPath);
|
|
1968
|
+
validateSessionId(sessionId);
|
|
1969
|
+
validateForkId(forkId);
|
|
1970
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1971
|
+
await orka.initialize();
|
|
1972
|
+
const spinner = ora2("Deleting fork...").start();
|
|
1973
|
+
await orka.deleteFork(sessionId, forkId);
|
|
1974
|
+
spinner.succeed("Fork deleted!");
|
|
1975
|
+
Output.warn("Fork has been permanently deleted.");
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
handleError(error);
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// src/cli/commands/merge.ts
|
|
1983
|
+
import ora3 from "ora";
|
|
1984
|
+
function mergeCommand(program2) {
|
|
1985
|
+
const merge = program2.command("merge").description("Merge and export operations");
|
|
1986
|
+
merge.command("export <session-id> <fork-id>").description("Generate export summary for a fork").action(async (sessionId, forkId) => {
|
|
1987
|
+
try {
|
|
1988
|
+
const projectPath = process.cwd();
|
|
1989
|
+
validateInitialized(projectPath);
|
|
1990
|
+
validateSessionId(sessionId);
|
|
1991
|
+
validateForkId(forkId);
|
|
1992
|
+
const orka = new ClaudeOrka(projectPath);
|
|
1993
|
+
await orka.initialize();
|
|
1994
|
+
Output.info("Generating export summary...");
|
|
1995
|
+
Output.warn("This will send a prompt to Claude to generate a summary.");
|
|
1996
|
+
Output.warn("Wait for Claude to finish before running the merge command.");
|
|
1997
|
+
const exportPath = await orka.generateForkExport(sessionId, forkId);
|
|
1998
|
+
Output.success("Export prompt sent to Claude!");
|
|
1999
|
+
Output.info(`Export will be saved to: ${exportPath}`);
|
|
2000
|
+
Output.newline();
|
|
2001
|
+
Output.info("Next steps:");
|
|
2002
|
+
Output.info(" 1. Wait for Claude to generate and save the summary (~15-30 seconds)");
|
|
2003
|
+
Output.info(" 2. Run: orka merge do " + sessionId + " " + forkId);
|
|
2004
|
+
} catch (error) {
|
|
2005
|
+
handleError(error);
|
|
2006
|
+
}
|
|
2007
|
+
});
|
|
2008
|
+
merge.command("do <session-id> <fork-id>").description("Merge a fork to main (requires export first)").action(async (sessionId, forkId) => {
|
|
2009
|
+
try {
|
|
2010
|
+
const projectPath = process.cwd();
|
|
2011
|
+
validateInitialized(projectPath);
|
|
2012
|
+
validateSessionId(sessionId);
|
|
2013
|
+
validateForkId(forkId);
|
|
2014
|
+
const orka = new ClaudeOrka(projectPath);
|
|
2015
|
+
await orka.initialize();
|
|
2016
|
+
const spinner = ora3("Merging fork to main...").start();
|
|
2017
|
+
await orka.merge(sessionId, forkId);
|
|
2018
|
+
spinner.succeed("Fork merged to main!");
|
|
2019
|
+
Output.info("The fork context has been sent to the main conversation.");
|
|
2020
|
+
Output.info("Claude in main now has access to the fork exploration.");
|
|
2021
|
+
Output.warn("Fork has been closed and marked as merged.");
|
|
2022
|
+
} catch (error) {
|
|
2023
|
+
handleError(error);
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
merge.command("auto <session-id> <fork-id>").description("Generate export and merge automatically").option("-w, --wait <ms>", "Wait time in milliseconds for Claude to complete export", "15000").action(async (sessionId, forkId, options) => {
|
|
2027
|
+
try {
|
|
2028
|
+
const projectPath = process.cwd();
|
|
2029
|
+
validateInitialized(projectPath);
|
|
2030
|
+
validateSessionId(sessionId);
|
|
2031
|
+
validateForkId(forkId);
|
|
2032
|
+
const orka = new ClaudeOrka(projectPath);
|
|
2033
|
+
await orka.initialize();
|
|
2034
|
+
const waitTime = parseInt(options.wait);
|
|
2035
|
+
Output.info("Starting automatic export and merge...");
|
|
2036
|
+
const spinner = ora3("Generating export summary...").start();
|
|
2037
|
+
await orka.generateExportAndMerge(sessionId, forkId, waitTime);
|
|
2038
|
+
spinner.succeed("Fork merged to main!");
|
|
2039
|
+
Output.info("The fork context has been sent to the main conversation.");
|
|
2040
|
+
Output.info("Claude in main now has access to the fork exploration.");
|
|
2041
|
+
Output.warn("Fork has been closed and marked as merged.");
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
handleError(error);
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// src/cli/commands/doctor.ts
|
|
2049
|
+
import execa2 from "execa";
|
|
2050
|
+
import fs5 from "fs-extra";
|
|
2051
|
+
import path5 from "path";
|
|
2052
|
+
import chalk3 from "chalk";
|
|
2053
|
+
function doctorCommand(program2) {
|
|
2054
|
+
program2.command("doctor").description("Check system dependencies and configuration").action(async () => {
|
|
2055
|
+
try {
|
|
2056
|
+
console.log(chalk3.bold.cyan("\n\u{1F50D} Claude-Orka Doctor\n"));
|
|
2057
|
+
console.log("Checking system dependencies and configuration...\n");
|
|
2058
|
+
const results = [];
|
|
2059
|
+
results.push(await checkNodeVersion());
|
|
2060
|
+
results.push(await checkTmux());
|
|
2061
|
+
results.push(await checkClaude());
|
|
2062
|
+
results.push(await checkProjectInit());
|
|
2063
|
+
results.push(await checkWritePermissions());
|
|
2064
|
+
results.push(await checkClaudeDir());
|
|
2065
|
+
displayResults(results);
|
|
2066
|
+
const criticalFailures = results.filter((r) => r.status === "fail");
|
|
2067
|
+
if (criticalFailures.length > 0) {
|
|
2068
|
+
process.exit(1);
|
|
2069
|
+
}
|
|
2070
|
+
} catch (error) {
|
|
2071
|
+
handleError(error);
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
async function checkNodeVersion() {
|
|
2076
|
+
try {
|
|
2077
|
+
const version = process.version;
|
|
2078
|
+
const major = parseInt(version.slice(1).split(".")[0]);
|
|
2079
|
+
if (major >= 18) {
|
|
2080
|
+
return {
|
|
2081
|
+
name: "Node.js",
|
|
2082
|
+
status: "pass",
|
|
2083
|
+
message: `${version} (>= 18.0.0)`
|
|
2084
|
+
};
|
|
2085
|
+
} else {
|
|
2086
|
+
return {
|
|
2087
|
+
name: "Node.js",
|
|
2088
|
+
status: "fail",
|
|
2089
|
+
message: `${version} (requires >= 18.0.0)`,
|
|
2090
|
+
fix: "Install Node.js 18 or higher from https://nodejs.org"
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
} catch (error) {
|
|
2094
|
+
return {
|
|
2095
|
+
name: "Node.js",
|
|
2096
|
+
status: "fail",
|
|
2097
|
+
message: "Not found",
|
|
2098
|
+
fix: "Install Node.js from https://nodejs.org"
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
async function checkTmux() {
|
|
2103
|
+
try {
|
|
2104
|
+
const { stdout } = await execa2("tmux", ["-V"]);
|
|
2105
|
+
const version = stdout.trim();
|
|
2106
|
+
return {
|
|
2107
|
+
name: "tmux",
|
|
2108
|
+
status: "pass",
|
|
2109
|
+
message: version
|
|
2110
|
+
};
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
return {
|
|
2113
|
+
name: "tmux",
|
|
2114
|
+
status: "fail",
|
|
2115
|
+
message: "Not found",
|
|
2116
|
+
details: "tmux is required for session management",
|
|
2117
|
+
fix: "Install tmux:\n macOS: brew install tmux\n Ubuntu: sudo apt-get install tmux"
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
async function checkClaude() {
|
|
2122
|
+
try {
|
|
2123
|
+
const { stdout } = await execa2("claude", ["--version"]);
|
|
2124
|
+
const version = stdout.trim();
|
|
2125
|
+
return {
|
|
2126
|
+
name: "Claude CLI",
|
|
2127
|
+
status: "pass",
|
|
2128
|
+
message: version
|
|
2129
|
+
};
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
return {
|
|
2132
|
+
name: "Claude CLI",
|
|
2133
|
+
status: "fail",
|
|
2134
|
+
message: "Not found",
|
|
2135
|
+
details: "Claude CLI is required for AI sessions",
|
|
2136
|
+
fix: "Install Claude CLI from https://claude.ai/download"
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
async function checkProjectInit() {
|
|
2141
|
+
const projectPath = process.cwd();
|
|
2142
|
+
const orkaDir = path5.join(projectPath, ".claude-orka");
|
|
2143
|
+
const stateFile = path5.join(orkaDir, "state.json");
|
|
2144
|
+
try {
|
|
2145
|
+
const dirExists = await fs5.pathExists(orkaDir);
|
|
2146
|
+
const stateExists = await fs5.pathExists(stateFile);
|
|
2147
|
+
if (dirExists && stateExists) {
|
|
2148
|
+
return {
|
|
2149
|
+
name: "Project initialization",
|
|
2150
|
+
status: "pass",
|
|
2151
|
+
message: "Initialized",
|
|
2152
|
+
details: ".claude-orka/ directory and state.json found"
|
|
2153
|
+
};
|
|
2154
|
+
} else if (dirExists) {
|
|
2155
|
+
return {
|
|
2156
|
+
name: "Project initialization",
|
|
2157
|
+
status: "warn",
|
|
2158
|
+
message: "Partially initialized",
|
|
2159
|
+
details: ".claude-orka/ exists but state.json is missing",
|
|
2160
|
+
fix: "Run: orka init"
|
|
2161
|
+
};
|
|
2162
|
+
} else {
|
|
2163
|
+
return {
|
|
2164
|
+
name: "Project initialization",
|
|
2165
|
+
status: "warn",
|
|
2166
|
+
message: "Not initialized",
|
|
2167
|
+
details: "Project is not initialized",
|
|
2168
|
+
fix: "Run: orka init"
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
return {
|
|
2173
|
+
name: "Project initialization",
|
|
2174
|
+
status: "fail",
|
|
2175
|
+
message: "Error checking",
|
|
2176
|
+
details: error.message
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
async function checkWritePermissions() {
|
|
2181
|
+
const projectPath = process.cwd();
|
|
2182
|
+
try {
|
|
2183
|
+
const testFile = path5.join(projectPath, ".claude-orka-write-test");
|
|
2184
|
+
await fs5.writeFile(testFile, "test");
|
|
2185
|
+
await fs5.remove(testFile);
|
|
2186
|
+
return {
|
|
2187
|
+
name: "Write permissions",
|
|
2188
|
+
status: "pass",
|
|
2189
|
+
message: "OK",
|
|
2190
|
+
details: "Can write to project directory"
|
|
2191
|
+
};
|
|
2192
|
+
} catch (error) {
|
|
2193
|
+
return {
|
|
2194
|
+
name: "Write permissions",
|
|
2195
|
+
status: "fail",
|
|
2196
|
+
message: "Cannot write",
|
|
2197
|
+
details: `No write permission in ${projectPath}`,
|
|
2198
|
+
fix: "Check directory permissions"
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
async function checkClaudeDir() {
|
|
2203
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
2204
|
+
const claudeDir = path5.join(homeDir, ".claude");
|
|
2205
|
+
const historyFile = path5.join(claudeDir, "history.jsonl");
|
|
2206
|
+
try {
|
|
2207
|
+
const dirExists = await fs5.pathExists(claudeDir);
|
|
2208
|
+
const historyExists = await fs5.pathExists(historyFile);
|
|
2209
|
+
if (dirExists && historyExists) {
|
|
2210
|
+
return {
|
|
2211
|
+
name: "Claude directory",
|
|
2212
|
+
status: "pass",
|
|
2213
|
+
message: "Found",
|
|
2214
|
+
details: `~/.claude/history.jsonl exists`
|
|
2215
|
+
};
|
|
2216
|
+
} else if (dirExists) {
|
|
2217
|
+
return {
|
|
2218
|
+
name: "Claude directory",
|
|
2219
|
+
status: "warn",
|
|
2220
|
+
message: "History file missing",
|
|
2221
|
+
details: "~/.claude/ exists but history.jsonl not found",
|
|
2222
|
+
fix: "Run Claude CLI at least once to create history"
|
|
2223
|
+
};
|
|
2224
|
+
} else {
|
|
2225
|
+
return {
|
|
2226
|
+
name: "Claude directory",
|
|
2227
|
+
status: "warn",
|
|
2228
|
+
message: "Not found",
|
|
2229
|
+
details: "~/.claude/ directory not found",
|
|
2230
|
+
fix: "Run Claude CLI at least once to create the directory"
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
return {
|
|
2235
|
+
name: "Claude directory",
|
|
2236
|
+
status: "fail",
|
|
2237
|
+
message: "Error checking",
|
|
2238
|
+
details: error.message
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
function displayResults(results) {
|
|
2243
|
+
console.log(chalk3.bold("Results:\n"));
|
|
2244
|
+
for (const result of results) {
|
|
2245
|
+
let icon;
|
|
2246
|
+
let color;
|
|
2247
|
+
switch (result.status) {
|
|
2248
|
+
case "pass":
|
|
2249
|
+
icon = "\u2713";
|
|
2250
|
+
color = chalk3.green;
|
|
2251
|
+
break;
|
|
2252
|
+
case "warn":
|
|
2253
|
+
icon = "\u26A0";
|
|
2254
|
+
color = chalk3.yellow;
|
|
2255
|
+
break;
|
|
2256
|
+
case "fail":
|
|
2257
|
+
icon = "\u2717";
|
|
2258
|
+
color = chalk3.red;
|
|
2259
|
+
break;
|
|
2260
|
+
}
|
|
2261
|
+
console.log(
|
|
2262
|
+
`${color(icon)} ${chalk3.bold(result.name)}: ${color(result.message)}`
|
|
2263
|
+
);
|
|
2264
|
+
if (result.details) {
|
|
2265
|
+
console.log(` ${chalk3.gray(result.details)}`);
|
|
2266
|
+
}
|
|
2267
|
+
if (result.fix) {
|
|
2268
|
+
console.log(` ${chalk3.cyan("Fix:")} ${result.fix}`);
|
|
2269
|
+
}
|
|
2270
|
+
console.log();
|
|
2271
|
+
}
|
|
2272
|
+
const passed = results.filter((r) => r.status === "pass").length;
|
|
2273
|
+
const warned = results.filter((r) => r.status === "warn").length;
|
|
2274
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
2275
|
+
console.log(chalk3.bold("Summary:"));
|
|
2276
|
+
console.log(` ${chalk3.green("\u2713")} Passed: ${passed}`);
|
|
2277
|
+
console.log(` ${chalk3.yellow("\u26A0")} Warnings: ${warned}`);
|
|
2278
|
+
console.log(` ${chalk3.red("\u2717")} Failed: ${failed}`);
|
|
2279
|
+
console.log();
|
|
2280
|
+
if (failed === 0 && warned === 0) {
|
|
2281
|
+
Output.success("All checks passed! Claude-Orka is ready to use.");
|
|
2282
|
+
} else if (failed === 0) {
|
|
2283
|
+
Output.warn("Some warnings found. Claude-Orka should work but check the warnings above.");
|
|
2284
|
+
} else {
|
|
2285
|
+
Output.error("Some critical checks failed. Fix the errors above before using Claude-Orka.");
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// src/cli/commands/prepare.ts
|
|
2290
|
+
import execa3 from "execa";
|
|
2291
|
+
import chalk4 from "chalk";
|
|
2292
|
+
import ora4 from "ora";
|
|
2293
|
+
function prepareCommand(program2) {
|
|
2294
|
+
program2.command("prepare").description("Install and configure system dependencies").option("-y, --yes", "Skip confirmation prompts").action(async (options) => {
|
|
2295
|
+
try {
|
|
2296
|
+
console.log(chalk4.bold.cyan("\n\u{1F527} Claude-Orka Preparation\n"));
|
|
2297
|
+
console.log("This will help you install required dependencies:\n");
|
|
2298
|
+
console.log(" \u2022 tmux (terminal multiplexer)");
|
|
2299
|
+
console.log(" \u2022 Claude CLI (if needed)\n");
|
|
2300
|
+
if (!options.yes) {
|
|
2301
|
+
const readline = __require("readline").createInterface({
|
|
2302
|
+
input: process.stdin,
|
|
2303
|
+
output: process.stdout
|
|
2304
|
+
});
|
|
2305
|
+
const answer = await new Promise((resolve) => {
|
|
2306
|
+
readline.question("Continue? (y/n): ", resolve);
|
|
2307
|
+
});
|
|
2308
|
+
readline.close();
|
|
2309
|
+
if (answer.toLowerCase() !== "y") {
|
|
2310
|
+
Output.warn("Installation cancelled");
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
const system = await detectSystem();
|
|
2315
|
+
console.log(chalk4.gray(`
|
|
2316
|
+
Detected: ${system.platform}`));
|
|
2317
|
+
await installTmux(system);
|
|
2318
|
+
await checkClaudeCLI();
|
|
2319
|
+
console.log(chalk4.bold.green("\n\u2713 Preparation complete!\n"));
|
|
2320
|
+
console.log("Run " + chalk4.cyan("orka doctor") + " to verify everything is working.");
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
handleError(error);
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
async function detectSystem() {
|
|
2327
|
+
const platform = process.platform;
|
|
2328
|
+
const info = { platform };
|
|
2329
|
+
if (platform === "darwin") {
|
|
2330
|
+
try {
|
|
2331
|
+
await execa3("which", ["brew"]);
|
|
2332
|
+
info.hasHomebrew = true;
|
|
2333
|
+
info.packageManager = "brew";
|
|
2334
|
+
} catch {
|
|
2335
|
+
info.hasHomebrew = false;
|
|
2336
|
+
}
|
|
2337
|
+
} else if (platform === "linux") {
|
|
2338
|
+
try {
|
|
2339
|
+
await execa3("which", ["apt-get"]);
|
|
2340
|
+
info.hasApt = true;
|
|
2341
|
+
info.packageManager = "apt";
|
|
2342
|
+
} catch {
|
|
2343
|
+
try {
|
|
2344
|
+
await execa3("which", ["yum"]);
|
|
2345
|
+
info.hasYum = true;
|
|
2346
|
+
info.packageManager = "yum";
|
|
2347
|
+
} catch {
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
return info;
|
|
2352
|
+
}
|
|
2353
|
+
async function installTmux(system) {
|
|
2354
|
+
console.log(chalk4.bold("\n\u{1F4E6} Installing tmux...\n"));
|
|
2355
|
+
try {
|
|
2356
|
+
await execa3("which", ["tmux"]);
|
|
2357
|
+
const { stdout } = await execa3("tmux", ["-V"]);
|
|
2358
|
+
Output.success(`tmux is already installed: ${stdout}`);
|
|
2359
|
+
return;
|
|
2360
|
+
} catch {
|
|
2361
|
+
}
|
|
2362
|
+
const spinner = ora4("Installing tmux...").start();
|
|
2363
|
+
try {
|
|
2364
|
+
if (system.platform === "darwin") {
|
|
2365
|
+
if (!system.hasHomebrew) {
|
|
2366
|
+
spinner.fail("Homebrew is not installed");
|
|
2367
|
+
console.log(chalk4.yellow("\nPlease install Homebrew first:"));
|
|
2368
|
+
console.log(
|
|
2369
|
+
chalk4.cyan(
|
|
2370
|
+
'/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
|
2371
|
+
)
|
|
2372
|
+
);
|
|
2373
|
+
console.log("\nThen run: " + chalk4.cyan("brew install tmux"));
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
await execa3("brew", ["install", "tmux"]);
|
|
2377
|
+
spinner.succeed("tmux installed via Homebrew");
|
|
2378
|
+
} else if (system.platform === "linux") {
|
|
2379
|
+
if (system.hasApt) {
|
|
2380
|
+
await execa3("sudo", ["apt-get", "update"]);
|
|
2381
|
+
await execa3("sudo", ["apt-get", "install", "-y", "tmux"]);
|
|
2382
|
+
spinner.succeed("tmux installed via apt");
|
|
2383
|
+
} else if (system.hasYum) {
|
|
2384
|
+
await execa3("sudo", ["yum", "install", "-y", "tmux"]);
|
|
2385
|
+
spinner.succeed("tmux installed via yum");
|
|
2386
|
+
} else {
|
|
2387
|
+
spinner.fail("Unknown package manager");
|
|
2388
|
+
console.log(chalk4.yellow("\nPlease install tmux manually:"));
|
|
2389
|
+
console.log(chalk4.cyan(" https://github.com/tmux/tmux/wiki/Installing"));
|
|
2390
|
+
}
|
|
2391
|
+
} else {
|
|
2392
|
+
spinner.fail(`Unsupported platform: ${system.platform}`);
|
|
2393
|
+
console.log(chalk4.yellow("\nPlease install tmux manually:"));
|
|
2394
|
+
console.log(chalk4.cyan(" https://github.com/tmux/tmux/wiki/Installing"));
|
|
2395
|
+
}
|
|
2396
|
+
} catch (error) {
|
|
2397
|
+
spinner.fail("Failed to install tmux");
|
|
2398
|
+
console.log(chalk4.red(`
|
|
2399
|
+
Error: ${error.message}`));
|
|
2400
|
+
console.log(chalk4.yellow("\nPlease install tmux manually:"));
|
|
2401
|
+
console.log(chalk4.cyan(" macOS: brew install tmux"));
|
|
2402
|
+
console.log(chalk4.cyan(" Ubuntu: sudo apt-get install tmux"));
|
|
2403
|
+
console.log(chalk4.cyan(" CentOS: sudo yum install tmux"));
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
async function checkClaudeCLI() {
|
|
2407
|
+
console.log(chalk4.bold("\n\u{1F916} Checking Claude CLI...\n"));
|
|
2408
|
+
try {
|
|
2409
|
+
const { stdout } = await execa3("claude", ["--version"]);
|
|
2410
|
+
Output.success(`Claude CLI is installed: ${stdout}`);
|
|
2411
|
+
} catch {
|
|
2412
|
+
Output.warn("Claude CLI is not installed");
|
|
2413
|
+
console.log(chalk4.yellow("\nClaude CLI is required for Claude-Orka to work."));
|
|
2414
|
+
console.log(chalk4.cyan("\nInstallation options:"));
|
|
2415
|
+
console.log(" 1. Visit: https://claude.ai/download");
|
|
2416
|
+
console.log(" 2. Or use npm: npm install -g @anthropic-ai/claude-cli");
|
|
2417
|
+
console.log(
|
|
2418
|
+
chalk4.gray("\nNote: You may need to restart your terminal after installation.")
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
// src/cli/index.ts
|
|
2424
|
+
var program = new Command();
|
|
2425
|
+
program.name("orka").description("Claude-Orka: Orchestrate Claude Code sessions with tmux").version("1.0.0");
|
|
2426
|
+
prepareCommand(program);
|
|
2427
|
+
initCommand(program);
|
|
2428
|
+
doctorCommand(program);
|
|
2429
|
+
statusCommand(program);
|
|
2430
|
+
sessionCommand(program);
|
|
2431
|
+
forkCommand(program);
|
|
2432
|
+
mergeCommand(program);
|
|
2433
|
+
program.parse();
|