@love-moon/conductor-cli 0.2.11 → 0.2.13

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/conductor.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * daemon - Start long-running daemon for task orchestration
9
9
  * config - Interactive configuration setup
10
10
  * update - Update the CLI to the latest version
11
+ * diagnose - Diagnose a task in production/backend
11
12
  */
12
13
 
13
14
  import { fileURLToPath } from "node:url";
@@ -43,7 +44,7 @@ if (argv[0] === "--version" || argv[0] === "-v") {
43
44
  const subcommand = argv[0];
44
45
 
45
46
  // Valid subcommands
46
- const validSubcommands = ["fire", "daemon", "config", "update"];
47
+ const validSubcommands = ["fire", "daemon", "config", "update", "diagnose"];
47
48
 
48
49
  if (!validSubcommands.includes(subcommand)) {
49
50
  console.error(`Error: Unknown subcommand '${subcommand}'`);
@@ -85,6 +86,7 @@ Subcommands:
85
86
  daemon Start long-running daemon for task orchestration
86
87
  config Interactive configuration setup
87
88
  update Update the CLI to the latest version
89
+ diagnose Diagnose a task and print likely root cause
88
90
 
89
91
  Options:
90
92
  -h, --help Show this help message
@@ -94,6 +96,7 @@ Examples:
94
96
  conductor fire -- "fix the bug"
95
97
  conductor fire --backend claude -- "add feature"
96
98
  conductor daemon --config-file ~/.conductor/config.yaml
99
+ conductor diagnose <task-id>
97
100
  conductor config
98
101
  conductor update
99
102
 
@@ -102,6 +105,7 @@ For subcommand-specific help:
102
105
  conductor daemon --help
103
106
  conductor config --help
104
107
  conductor update --help
108
+ conductor diagnose --help
105
109
 
106
110
  Version: ${pkgJson.version}
107
111
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "conductor": "bin/conductor.js"
@@ -16,8 +16,8 @@
16
16
  "test": "node --test"
17
17
  },
18
18
  "dependencies": {
19
- "@love-moon/tui-driver": "0.2.11",
20
- "@love-moon/conductor-sdk": "0.2.11",
19
+ "@love-moon/tui-driver": "0.2.13",
20
+ "@love-moon/conductor-sdk": "0.2.13",
21
21
  "dotenv": "^16.4.5",
22
22
  "enquirer": "^2.4.1",
23
23
  "js-yaml": "^4.1.1",
package/src/daemon.js CHANGED
@@ -16,6 +16,11 @@ const __dirname = path.dirname(__filename);
16
16
  const CLI_PATH = path.resolve(__dirname, "..", "bin", "conductor-fire.js");
17
17
  const DAEMON_LOG_DIR = path.join(os.homedir(), ".conductor", "logs");
18
18
  const DAEMON_LOG_PATH = path.join(DAEMON_LOG_DIR, "conductor-daemon.log");
19
+ const PLAN_LIMIT_MESSAGES = {
20
+ manual_fire_active_task: "Free plan limit reached: only 1 active fire task is allowed.",
21
+ app_active_task: "Free plan limit reached: only 1 active app task is allowed.",
22
+ daemon_active_connection: "Free plan limit reached: only 1 active daemon connection is allowed.",
23
+ };
19
24
 
20
25
  function appendDaemonLog(line) {
21
26
  try {
@@ -57,6 +62,51 @@ function getUserConfig(configFilePath) {
57
62
  return {};
58
63
  }
59
64
 
65
+ function normalizePlanLimitType(limitType) {
66
+ if (typeof limitType !== "string") {
67
+ return null;
68
+ }
69
+ const normalized = limitType.trim().toLowerCase();
70
+ if (!normalized) {
71
+ return null;
72
+ }
73
+ if (normalized === "free_daemon_connection" || normalized === "free-daemon-limit") {
74
+ return "daemon_active_connection";
75
+ }
76
+ return normalized;
77
+ }
78
+
79
+ function inferPlanLimitMessage(text) {
80
+ if (typeof text !== "string" || !text.trim()) {
81
+ return null;
82
+ }
83
+ const lower = text.trim().toLowerCase();
84
+ if (lower.includes("one active manual fire task")) {
85
+ return PLAN_LIMIT_MESSAGES.manual_fire_active_task;
86
+ }
87
+ if (lower.includes("one active app task")) {
88
+ return PLAN_LIMIT_MESSAGES.app_active_task;
89
+ }
90
+ if (lower.includes("one active daemon connection")) {
91
+ return PLAN_LIMIT_MESSAGES.daemon_active_connection;
92
+ }
93
+ if (lower.includes("free plan task limit reached")) {
94
+ return "Free plan task limit reached: only 1 active fire task and 1 active app task are allowed.";
95
+ }
96
+ return null;
97
+ }
98
+
99
+ function getPlanLimitMessage(payload) {
100
+ if (!payload || typeof payload !== "object") {
101
+ return null;
102
+ }
103
+ const normalizedType = normalizePlanLimitType(payload.limit_type);
104
+ if (normalizedType && PLAN_LIMIT_MESSAGES[normalizedType]) {
105
+ return PLAN_LIMIT_MESSAGES[normalizedType];
106
+ }
107
+ return inferPlanLimitMessage(payload.message) || inferPlanLimitMessage(payload.error);
108
+ }
109
+
60
110
  // Default CLI commands for supported backends
61
111
  const DEFAULT_CLI_LIST = {
62
112
  codex: "codex --dangerously-bypass-approvals-and-sandbox",
@@ -493,6 +543,22 @@ export function startDaemon(config = {}, deps = {}) {
493
543
  }
494
544
 
495
545
  function handleEvent(event) {
546
+ if (event.type === "error") {
547
+ const payload = event?.payload && typeof event.payload === "object" ? event.payload : {};
548
+ const planLimitMessage = getPlanLimitMessage(payload);
549
+ if (planLimitMessage) {
550
+ logError(planLimitMessage);
551
+ return;
552
+ }
553
+ const backendMessage = typeof payload.message === "string" ? payload.message.trim() : "";
554
+ if (backendMessage) {
555
+ logError(`Backend error: ${backendMessage}`);
556
+ } else {
557
+ logError("Backend returned an error event");
558
+ }
559
+ return;
560
+ }
561
+
496
562
  if (event.type === "create_task") {
497
563
  handleCreateTask(event.payload);
498
564
  return;
@@ -125,6 +125,73 @@ export async function selectHistorySession(provider, options = {}) {
125
125
  return selected || null;
126
126
  }
127
127
 
128
+ export async function findSessionPath(provider, sessionId, options = {}) {
129
+ const normalizedProvider = String(provider || "").trim().toLowerCase();
130
+ if (normalizedProvider === "codex") {
131
+ return findCodexSessionPath(sessionId, options);
132
+ }
133
+ if (normalizedProvider === "claude") {
134
+ return findClaudeSessionPath(sessionId, options);
135
+ }
136
+ if (normalizedProvider === "copilot") {
137
+ return findCopilotSessionPath(sessionId, options);
138
+ }
139
+ throw new Error(`Unsupported provider: ${provider}`);
140
+ }
141
+
142
+ export async function findCodexSessionPath(sessionId, options = {}) {
143
+ const normalizedSessionId = normalizeSessionId(sessionId);
144
+ if (!normalizedSessionId) {
145
+ return null;
146
+ }
147
+ const homeDir = resolveHomeDir(options);
148
+ const sessionsDir = options.codexSessionsDir || path.join(homeDir, ".codex", "sessions");
149
+ return findCodexSessionFile(sessionsDir, normalizedSessionId);
150
+ }
151
+
152
+ export async function findClaudeSessionPath(sessionId, options = {}) {
153
+ const normalizedSessionId = normalizeSessionId(sessionId);
154
+ if (!normalizedSessionId) {
155
+ return null;
156
+ }
157
+
158
+ const homeDir = resolveHomeDir(options);
159
+ const projectsDir = options.claudeProjectsDir || path.join(homeDir, ".claude", "projects");
160
+ const sessionEntries = await findClaudeSessionEntries(projectsDir, normalizedSessionId);
161
+ if (sessionEntries.length > 0) {
162
+ return sessionEntries[0]?.source || null;
163
+ }
164
+
165
+ const tasksDir = options.claudeTasksDir || path.join(homeDir, ".claude", "tasks");
166
+ const directTaskDir = path.join(tasksDir, normalizedSessionId);
167
+ if (await pathExists(directTaskDir, "directory")) {
168
+ return directTaskDir;
169
+ }
170
+
171
+ return null;
172
+ }
173
+
174
+ export async function findCopilotSessionPath(sessionId, options = {}) {
175
+ const normalizedSessionId = normalizeSessionId(sessionId);
176
+ if (!normalizedSessionId) {
177
+ return null;
178
+ }
179
+
180
+ const homeDir = resolveHomeDir(options);
181
+ const sessionStateDir = options.copilotSessionStateDir || path.join(homeDir, ".copilot", "session-state");
182
+ const directJsonlPath = path.join(sessionStateDir, `${normalizedSessionId}.jsonl`);
183
+ if (await pathExists(directJsonlPath, "file")) {
184
+ return directJsonlPath;
185
+ }
186
+
187
+ const directSessionDir = path.join(sessionStateDir, normalizedSessionId);
188
+ if (await pathExists(directSessionDir, "directory")) {
189
+ return directSessionDir;
190
+ }
191
+
192
+ return findPathByName(sessionStateDir, normalizedSessionId);
193
+ }
194
+
128
195
  function resolveHomeDir(options) {
129
196
  if (options?.homeDir) {
130
197
  return options.homeDir;
@@ -132,6 +199,10 @@ function resolveHomeDir(options) {
132
199
  return os.homedir();
133
200
  }
134
201
 
202
+ function normalizeSessionId(sessionId) {
203
+ return typeof sessionId === "string" ? sessionId.trim() : "";
204
+ }
205
+
135
206
  function trimHistory(history, limit) {
136
207
  const max = Number.isFinite(limit) ? Math.max(1, limit) : DEFAULT_HISTORY_LIMIT;
137
208
  if (history.length <= max) {
@@ -473,6 +544,44 @@ async function findJsonlFiles(rootDir) {
473
544
  return files;
474
545
  }
475
546
 
547
+ async function findPathByName(rootDir, sessionId) {
548
+ const queue = [rootDir];
549
+ while (queue.length) {
550
+ const current = queue.pop();
551
+ let entries = [];
552
+ try {
553
+ entries = await fsp.readdir(current, { withFileTypes: true });
554
+ } catch {
555
+ continue;
556
+ }
557
+ for (const entry of entries) {
558
+ const fullPath = path.join(current, entry.name);
559
+ if (entry.name.includes(sessionId)) {
560
+ return fullPath;
561
+ }
562
+ if (entry.isDirectory()) {
563
+ queue.push(fullPath);
564
+ }
565
+ }
566
+ }
567
+ return null;
568
+ }
569
+
570
+ async function pathExists(targetPath, expectedType) {
571
+ try {
572
+ const stats = await fsp.stat(targetPath);
573
+ if (expectedType === "file") {
574
+ return stats.isFile();
575
+ }
576
+ if (expectedType === "directory") {
577
+ return stats.isDirectory();
578
+ }
579
+ return true;
580
+ } catch {
581
+ return false;
582
+ }
583
+ }
584
+
476
585
  async function parseCodexSessionFile(filePath) {
477
586
  const fileStream = fs.createReadStream(filePath);
478
587
  const rl = readline.createInterface({