@enruana/claude-orka 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -13,36 +13,61 @@ import { fileURLToPath as fileURLToPath2 } from "url";
13
13
  import { dirname as dirname2, join } from "path";
14
14
 
15
15
  // src/core/StateManager.ts
16
- import path2 from "path";
17
- import fs2 from "fs-extra";
16
+ import path3 from "path";
17
+ import fs3 from "fs-extra";
18
18
 
19
19
  // src/utils/tmux.ts
20
20
  import execa from "execa";
21
21
 
22
22
  // src/utils/logger.ts
23
+ import fs from "fs-extra";
24
+ import path from "path";
23
25
  var Logger = class {
24
26
  level = 1 /* INFO */;
27
+ logFilePath = null;
25
28
  setLevel(level) {
26
29
  this.level = level;
27
30
  }
31
+ setLogFile(projectPath) {
32
+ const logDir = path.join(projectPath, ".claude-orka");
33
+ fs.ensureDirSync(logDir);
34
+ this.logFilePath = path.join(logDir, "orka.log");
35
+ }
36
+ writeToFile(level, ...args) {
37
+ if (!this.logFilePath) return;
38
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
39
+ const message = args.map(
40
+ (arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
41
+ ).join(" ");
42
+ const logLine = `${timestamp} [${level}] ${message}
43
+ `;
44
+ try {
45
+ fs.appendFileSync(this.logFilePath, logLine);
46
+ } catch (error) {
47
+ }
48
+ }
28
49
  debug(...args) {
29
50
  if (this.level <= 0 /* DEBUG */) {
30
51
  console.log("[DEBUG]", ...args);
52
+ this.writeToFile("DEBUG", ...args);
31
53
  }
32
54
  }
33
55
  info(...args) {
34
56
  if (this.level <= 1 /* INFO */) {
35
57
  console.log("[INFO]", ...args);
58
+ this.writeToFile("INFO", ...args);
36
59
  }
37
60
  }
38
61
  warn(...args) {
39
62
  if (this.level <= 2 /* WARN */) {
40
63
  console.warn("[WARN]", ...args);
64
+ this.writeToFile("WARN", ...args);
41
65
  }
42
66
  }
43
67
  error(...args) {
44
68
  if (this.level <= 3 /* ERROR */) {
45
69
  console.error("[ERROR]", ...args);
70
+ this.writeToFile("ERROR", ...args);
46
71
  }
47
72
  }
48
73
  };
@@ -367,18 +392,18 @@ var TmuxCommands = class {
367
392
  };
368
393
 
369
394
  // src/utils/claude-history.ts
370
- import fs from "fs-extra";
395
+ import fs2 from "fs-extra";
371
396
  import os from "os";
372
- import path from "path";
373
- var CLAUDE_HISTORY_PATH = path.join(os.homedir(), ".claude", "history.jsonl");
397
+ import path2 from "path";
398
+ var CLAUDE_HISTORY_PATH = path2.join(os.homedir(), ".claude", "history.jsonl");
374
399
  async function readClaudeHistory() {
375
400
  try {
376
- const exists = await fs.pathExists(CLAUDE_HISTORY_PATH);
401
+ const exists = await fs2.pathExists(CLAUDE_HISTORY_PATH);
377
402
  if (!exists) {
378
403
  logger.warn(`Claude history file not found: ${CLAUDE_HISTORY_PATH}`);
379
404
  return [];
380
405
  }
381
- const content = await fs.readFile(CLAUDE_HISTORY_PATH, "utf-8");
406
+ const content = await fs2.readFile(CLAUDE_HISTORY_PATH, "utf-8");
382
407
  const lines = content.trim().split("\n").filter(Boolean);
383
408
  const entries = [];
384
409
  for (const line of lines) {
@@ -421,9 +446,9 @@ var StateManager = class {
421
446
  orkaDir;
422
447
  statePath;
423
448
  constructor(projectPath) {
424
- this.projectPath = path2.resolve(projectPath);
425
- this.orkaDir = path2.join(this.projectPath, ".claude-orka");
426
- this.statePath = path2.join(this.orkaDir, "state.json");
449
+ this.projectPath = path3.resolve(projectPath);
450
+ this.orkaDir = path3.join(this.projectPath, ".claude-orka");
451
+ this.statePath = path3.join(this.orkaDir, "state.json");
427
452
  }
428
453
  /**
429
454
  * Initialize StateManager
@@ -432,7 +457,7 @@ var StateManager = class {
432
457
  async initialize() {
433
458
  logger.debug("Initializing StateManager");
434
459
  await this.ensureDirectories();
435
- if (!await fs2.pathExists(this.statePath)) {
460
+ if (!await fs3.pathExists(this.statePath)) {
436
461
  logger.info("Creating initial state.json");
437
462
  const initialState = {
438
463
  version: "1.0.0",
@@ -448,7 +473,7 @@ var StateManager = class {
448
473
  * Create directory structure
449
474
  */
450
475
  async ensureDirectories() {
451
- await fs2.ensureDir(this.orkaDir);
476
+ await fs3.ensureDir(this.orkaDir);
452
477
  logger.debug("Directories ensured");
453
478
  }
454
479
  /**
@@ -456,7 +481,7 @@ var StateManager = class {
456
481
  */
457
482
  async read() {
458
483
  try {
459
- const content = await fs2.readFile(this.statePath, "utf-8");
484
+ const content = await fs3.readFile(this.statePath, "utf-8");
460
485
  return JSON.parse(content);
461
486
  } catch (error) {
462
487
  logger.error("Failed to read state:", error);
@@ -469,7 +494,7 @@ var StateManager = class {
469
494
  async save(state) {
470
495
  try {
471
496
  state.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
472
- await fs2.writeFile(this.statePath, JSON.stringify(state, null, 2), "utf-8");
497
+ await fs3.writeFile(this.statePath, JSON.stringify(state, null, 2), "utf-8");
473
498
  logger.debug("State saved");
474
499
  } catch (error) {
475
500
  logger.error("Failed to save state:", error);
@@ -685,8 +710,8 @@ var StateManager = class {
685
710
  */
686
711
  async saveContext(type, id, content) {
687
712
  const contextPath = type === "session" ? this.getSessionContextPath(id) : this.getForkContextPath(id);
688
- const fullPath = path2.join(this.projectPath, contextPath);
689
- await fs2.writeFile(fullPath, content, "utf-8");
713
+ const fullPath = path3.join(this.projectPath, contextPath);
714
+ await fs3.writeFile(fullPath, content, "utf-8");
690
715
  logger.info(`Context saved: ${contextPath}`);
691
716
  return contextPath;
692
717
  }
@@ -694,11 +719,11 @@ var StateManager = class {
694
719
  * Leer un contexto desde archivo
695
720
  */
696
721
  async readContext(contextPath) {
697
- const fullPath = path2.join(this.projectPath, contextPath);
698
- if (!await fs2.pathExists(fullPath)) {
722
+ const fullPath = path3.join(this.projectPath, contextPath);
723
+ if (!await fs3.pathExists(fullPath)) {
699
724
  throw new Error(`Context file not found: ${contextPath}`);
700
725
  }
701
- return await fs2.readFile(fullPath, "utf-8");
726
+ return await fs3.readFile(fullPath, "utf-8");
702
727
  }
703
728
  // --- HELPERS ---
704
729
  /**
@@ -723,10 +748,10 @@ var StateManager = class {
723
748
 
724
749
  // src/core/SessionManager.ts
725
750
  import { v4 as uuidv4 } from "uuid";
726
- import path3 from "path";
751
+ import path4 from "path";
727
752
  import { fileURLToPath } from "url";
728
753
  import { dirname } from "path";
729
- import fs3 from "fs-extra";
754
+ import fs4 from "fs-extra";
730
755
  import { spawn } from "child_process";
731
756
  import { createRequire } from "module";
732
757
  var require2 = createRequire(import.meta.url);
@@ -901,14 +926,25 @@ var SessionManager = class {
901
926
  /**
902
927
  * Crear un fork (rama de conversación)
903
928
  */
904
- async createFork(sessionId, name, vertical = false) {
929
+ async createFork(sessionId, name, parentId = "main", vertical = false) {
905
930
  const session = await this.getSession(sessionId);
906
931
  if (!session) {
907
932
  throw new Error(`Session ${sessionId} not found`);
908
933
  }
909
934
  const forkId = uuidv4();
910
935
  const forkName = name || `Fork-${session.forks.length + 1}`;
911
- logger.info(`Creating fork: ${forkName} in session ${session.name}`);
936
+ logger.info(`Creating fork: ${forkName} from parent ${parentId} in session ${session.name}`);
937
+ let parentClaudeSessionId;
938
+ if (parentId === "main") {
939
+ parentClaudeSessionId = session.main.claudeSessionId;
940
+ } else {
941
+ const parentFork = session.forks.find((f) => f.id === parentId);
942
+ if (!parentFork) {
943
+ throw new Error(`Parent fork ${parentId} not found`);
944
+ }
945
+ parentClaudeSessionId = parentFork.claudeSessionId;
946
+ }
947
+ logger.debug(`Parent Claude session ID: ${parentClaudeSessionId}`);
912
948
  await TmuxCommands.splitPane(session.tmuxSessionId, vertical);
913
949
  await sleep(1e3);
914
950
  const allPanes = await TmuxCommands.listPanes(session.tmuxSessionId);
@@ -918,7 +954,7 @@ var SessionManager = class {
918
954
  logger.debug(`Existing sessions before fork: ${existingIds.size}`);
919
955
  await this.initializeClaude(forkPaneId, {
920
956
  type: "fork",
921
- parentSessionId: session.main.claudeSessionId,
957
+ parentSessionId: parentClaudeSessionId,
922
958
  forkName
923
959
  });
924
960
  logger.info("Detecting fork session ID from history...");
@@ -932,6 +968,7 @@ var SessionManager = class {
932
968
  const fork = {
933
969
  id: forkId,
934
970
  name: forkName,
971
+ parentId,
935
972
  claudeSessionId: detectedForkId,
936
973
  // ✅ ID real detectado
937
974
  tmuxPaneId: forkPaneId,
@@ -989,7 +1026,7 @@ var SessionManager = class {
989
1026
  if (fork.tmuxPaneId) {
990
1027
  await TmuxCommands.killPane(fork.tmuxPaneId);
991
1028
  }
992
- fork.status = "saved";
1029
+ fork.status = "closed";
993
1030
  fork.tmuxPaneId = void 0;
994
1031
  session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
995
1032
  await this.stateManager.replaceSession(session);
@@ -1075,14 +1112,15 @@ var SessionManager = class {
1075
1112
  throw new Error(`Fork ${forkId} not found`);
1076
1113
  }
1077
1114
  logger.info(`Generating export for fork: ${fork.name}`);
1078
- const exportsDir = path3.join(this.projectPath, ".claude-orka", "exports");
1079
- await fs3.ensureDir(exportsDir);
1115
+ const exportsDir = path4.join(this.projectPath, ".claude-orka", "exports");
1116
+ await fs4.ensureDir(exportsDir);
1080
1117
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1081
1118
  const exportName = `fork-${fork.name}-${timestamp}.md`;
1082
1119
  const relativeExportPath = `.claude-orka/exports/${exportName}`;
1120
+ const absoluteExportPath = path4.join(this.projectPath, relativeExportPath);
1083
1121
  const prompt = `
1084
1122
  Please generate a complete summary of this fork conversation "${fork.name}" and save it to the file:
1085
- \`${relativeExportPath}\`
1123
+ \`${absoluteExportPath}\`
1086
1124
 
1087
1125
  The summary should include:
1088
1126
 
@@ -1113,7 +1151,10 @@ Write the summary in Markdown format and save it to the specified file.
1113
1151
  await TmuxCommands.sendEnter(fork.tmuxPaneId);
1114
1152
  fork.contextPath = relativeExportPath;
1115
1153
  await this.stateManager.replaceSession(session);
1116
- logger.info(`Export generation requested. Path: ${relativeExportPath}`);
1154
+ logger.info(`Export generation requested`);
1155
+ logger.info(` Filename: ${exportName}`);
1156
+ logger.info(` Relative path (saved in state): ${relativeExportPath}`);
1157
+ logger.info(` Absolute path (sent to Claude): ${absoluteExportPath}`);
1117
1158
  logger.warn("IMPORTANT: Wait for Claude to complete before calling merge()");
1118
1159
  return relativeExportPath;
1119
1160
  }
@@ -1135,27 +1176,52 @@ Write the summary in Markdown format and save it to the specified file.
1135
1176
  "Fork does not have an exported context. Call generateForkExport() first."
1136
1177
  );
1137
1178
  }
1138
- logger.info(`Merging fork ${fork.name} to main`);
1139
- const fullPath = path3.join(this.projectPath, fork.contextPath);
1140
- const exists = await fs3.pathExists(fullPath);
1179
+ const parentId = fork.parentId;
1180
+ const parentName = parentId === "main" ? "MAIN" : session.forks.find((f) => f.id === parentId)?.name || parentId;
1181
+ logger.info(`Merging fork ${fork.name} to parent ${parentName}`);
1182
+ let parentTmuxPaneId;
1183
+ if (parentId === "main") {
1184
+ parentTmuxPaneId = session.main.tmuxPaneId;
1185
+ } else {
1186
+ const parentFork = session.forks.find((f) => f.id === parentId);
1187
+ if (!parentFork) {
1188
+ throw new Error(`Parent fork ${parentId} not found`);
1189
+ }
1190
+ parentTmuxPaneId = parentFork.tmuxPaneId;
1191
+ }
1192
+ if (!parentTmuxPaneId) {
1193
+ throw new Error(`Parent ${parentName} is not active. Cannot send merge command.`);
1194
+ }
1195
+ let contextPath = fork.contextPath;
1196
+ let fullPath = path4.join(this.projectPath, contextPath);
1197
+ let exists = await fs4.pathExists(fullPath);
1198
+ if (!exists) {
1199
+ logger.warn(`Export file not found: ${contextPath}. Looking for most recent export...`);
1200
+ const exportsDir = path4.join(this.projectPath, ".claude-orka", "exports");
1201
+ const files = await fs4.readdir(exportsDir);
1202
+ const forkExports = files.filter((f) => f.startsWith(`fork-${fork.name}-`) && f.endsWith(".md")).sort().reverse();
1203
+ if (forkExports.length > 0) {
1204
+ contextPath = `.claude-orka/exports/${forkExports[0]}`;
1205
+ fullPath = path4.join(this.projectPath, contextPath);
1206
+ exists = await fs4.pathExists(fullPath);
1207
+ logger.info(`Using most recent export: ${contextPath}`);
1208
+ }
1209
+ }
1141
1210
  if (!exists) {
1142
1211
  throw new Error(
1143
- `Export file not found: ${fork.contextPath}. Make sure generateForkExport() completed.`
1212
+ `No export file found for fork "${fork.name}". Please run Export first and wait for Claude to complete.`
1144
1213
  );
1145
1214
  }
1146
1215
  const mergePrompt = `
1147
1216
  I have completed work on the fork "${fork.name}".
1148
- Please read the file \`${fork.contextPath}\` which contains:
1217
+ Please read the file \`${contextPath}\` which contains:
1149
1218
  1. An executive summary of the work completed
1150
1219
  2. The complete context of the fork conversation
1151
1220
 
1152
- Analyze the content and help me integrate the changes and learnings from the fork into this main conversation.
1221
+ Analyze the content and help me integrate the changes and learnings from the fork into this conversation.
1153
1222
  `.trim();
1154
- if (!session.main.tmuxPaneId) {
1155
- throw new Error("Main pane is not active. Cannot send merge command.");
1156
- }
1157
- await TmuxCommands.sendKeys(session.main.tmuxPaneId, mergePrompt);
1158
- await TmuxCommands.sendEnter(session.main.tmuxPaneId);
1223
+ await TmuxCommands.sendKeys(parentTmuxPaneId, mergePrompt);
1224
+ await TmuxCommands.sendEnter(parentTmuxPaneId);
1159
1225
  fork.status = "merged";
1160
1226
  fork.mergedToMain = true;
1161
1227
  fork.mergedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -1217,11 +1283,11 @@ Analyze the content and help me integrate the changes and learnings from the for
1217
1283
  logger.warn("Electron not available, skipping UI launch");
1218
1284
  return;
1219
1285
  }
1220
- let mainPath = path3.join(__dirname, "../../electron/main/main.js");
1221
- if (!fs3.existsSync(mainPath)) {
1222
- mainPath = path3.join(__dirname, "../../dist/electron/main/main.js");
1286
+ let mainPath = path4.join(__dirname, "../../electron/main/main.js");
1287
+ if (!fs4.existsSync(mainPath)) {
1288
+ mainPath = path4.join(__dirname, "../../dist/electron/main/main.js");
1223
1289
  }
1224
- if (!fs3.existsSync(mainPath)) {
1290
+ if (!fs4.existsSync(mainPath)) {
1225
1291
  logger.warn(`Electron main.js not found at ${mainPath}, skipping UI launch`);
1226
1292
  return;
1227
1293
  }
@@ -1249,6 +1315,7 @@ var ClaudeOrka = class {
1249
1315
  * @param projectPath Absolute path to the project
1250
1316
  */
1251
1317
  constructor(projectPath) {
1318
+ logger.setLogFile(projectPath);
1252
1319
  this.sessionManager = new SessionManager(projectPath);
1253
1320
  }
1254
1321
  /**
@@ -1361,11 +1428,12 @@ var ClaudeOrka = class {
1361
1428
  * Create a fork (conversation branch)
1362
1429
  * @param sessionId Session ID
1363
1430
  * @param name Optional fork name
1431
+ * @param parentId Parent fork/session ID (default: 'main')
1364
1432
  * @param vertical Whether to split vertically (default: false = horizontal)
1365
1433
  * @returns Created fork
1366
1434
  */
1367
- async createFork(sessionId, name, vertical) {
1368
- return await this.sessionManager.createFork(sessionId, name, vertical);
1435
+ async createFork(sessionId, name, parentId = "main", vertical) {
1436
+ return await this.sessionManager.createFork(sessionId, name, parentId, vertical);
1369
1437
  }
1370
1438
  /**
1371
1439
  * Close a fork
@@ -1727,8 +1795,8 @@ ${statusEmoji} ${chalk.bold(session.name)}`);
1727
1795
 
1728
1796
  // src/cli/utils/errors.ts
1729
1797
  import chalk2 from "chalk";
1730
- import fs4 from "fs";
1731
- import path4 from "path";
1798
+ import fs5 from "fs";
1799
+ import path5 from "path";
1732
1800
  var CLIError = class extends Error {
1733
1801
  constructor(message, exitCode = 1) {
1734
1802
  super(message);
@@ -1760,8 +1828,8 @@ function validateForkId(forkId) {
1760
1828
  }
1761
1829
  }
1762
1830
  function validateInitialized(projectPath) {
1763
- const orkaDir = path4.join(projectPath, ".claude-orka");
1764
- if (!fs4.existsSync(orkaDir)) {
1831
+ const orkaDir = path5.join(projectPath, ".claude-orka");
1832
+ if (!fs5.existsSync(orkaDir)) {
1765
1833
  throw new CLIError(
1766
1834
  'Project not initialized. Run "orka init" first.',
1767
1835
  2
@@ -2154,8 +2222,8 @@ function mergeCommand(program2) {
2154
2222
 
2155
2223
  // src/cli/commands/doctor.ts
2156
2224
  import execa2 from "execa";
2157
- import fs5 from "fs-extra";
2158
- import path5 from "path";
2225
+ import fs6 from "fs-extra";
2226
+ import path6 from "path";
2159
2227
  import chalk4 from "chalk";
2160
2228
  function doctorCommand(program2) {
2161
2229
  program2.command("doctor").description("Check system dependencies and configuration").action(async () => {
@@ -2246,11 +2314,11 @@ async function checkClaude() {
2246
2314
  }
2247
2315
  async function checkProjectInit() {
2248
2316
  const projectPath = process.cwd();
2249
- const orkaDir = path5.join(projectPath, ".claude-orka");
2250
- const stateFile = path5.join(orkaDir, "state.json");
2317
+ const orkaDir = path6.join(projectPath, ".claude-orka");
2318
+ const stateFile = path6.join(orkaDir, "state.json");
2251
2319
  try {
2252
- const dirExists = await fs5.pathExists(orkaDir);
2253
- const stateExists = await fs5.pathExists(stateFile);
2320
+ const dirExists = await fs6.pathExists(orkaDir);
2321
+ const stateExists = await fs6.pathExists(stateFile);
2254
2322
  if (dirExists && stateExists) {
2255
2323
  return {
2256
2324
  name: "Project initialization",
@@ -2287,9 +2355,9 @@ async function checkProjectInit() {
2287
2355
  async function checkWritePermissions() {
2288
2356
  const projectPath = process.cwd();
2289
2357
  try {
2290
- const testFile = path5.join(projectPath, ".claude-orka-write-test");
2291
- await fs5.writeFile(testFile, "test");
2292
- await fs5.remove(testFile);
2358
+ const testFile = path6.join(projectPath, ".claude-orka-write-test");
2359
+ await fs6.writeFile(testFile, "test");
2360
+ await fs6.remove(testFile);
2293
2361
  return {
2294
2362
  name: "Write permissions",
2295
2363
  status: "pass",
@@ -2308,11 +2376,11 @@ async function checkWritePermissions() {
2308
2376
  }
2309
2377
  async function checkClaudeDir() {
2310
2378
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
2311
- const claudeDir = path5.join(homeDir, ".claude");
2312
- const historyFile = path5.join(claudeDir, "history.jsonl");
2379
+ const claudeDir = path6.join(homeDir, ".claude");
2380
+ const historyFile = path6.join(claudeDir, "history.jsonl");
2313
2381
  try {
2314
- const dirExists = await fs5.pathExists(claudeDir);
2315
- const historyExists = await fs5.pathExists(historyFile);
2382
+ const dirExists = await fs6.pathExists(claudeDir);
2383
+ const historyExists = await fs6.pathExists(historyFile);
2316
2384
  if (dirExists && historyExists) {
2317
2385
  return {
2318
2386
  name: "Claude directory",