@love-moon/conductor-cli 0.2.10 → 0.2.12
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-config.js +17 -26
- package/bin/conductor-fire.js +823 -161
- package/bin/conductor-update.js +307 -0
- package/bin/conductor.js +8 -4
- package/package.json +3 -3
- package/src/daemon.js +83 -2
- package/src/fire/history.js +109 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* conductor update - Check for and install updates to the CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import { execSync, spawn } from "node:child_process";
|
|
12
|
+
import process from "node:process";
|
|
13
|
+
import readline from "node:readline/promises";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const PKG_ROOT = path.join(__dirname, "..");
|
|
19
|
+
|
|
20
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
|
|
21
|
+
const PACKAGE_NAME = pkgJson.name;
|
|
22
|
+
const CURRENT_VERSION = pkgJson.version;
|
|
23
|
+
|
|
24
|
+
// ANSI 颜色代码
|
|
25
|
+
const COLORS = {
|
|
26
|
+
yellow: "\x1b[33m",
|
|
27
|
+
green: "\x1b[32m",
|
|
28
|
+
cyan: "\x1b[36m",
|
|
29
|
+
red: "\x1b[31m",
|
|
30
|
+
reset: "\x1b[0m",
|
|
31
|
+
bold: "\x1b[1m"
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function colorize(text, color) {
|
|
35
|
+
return `${COLORS[color] || ""}${text}${COLORS.reset}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
// 解析命令行参数
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const forceUpdate = args.includes("--force") || args.includes("-f");
|
|
42
|
+
const skipConfirm = args.includes("--yes") || args.includes("-y");
|
|
43
|
+
const showHelp = args.includes("--help") || args.includes("-h");
|
|
44
|
+
|
|
45
|
+
if (showHelp) {
|
|
46
|
+
showHelpMessage();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(colorize(`📦 ${PACKAGE_NAME}`, "cyan"));
|
|
51
|
+
console.log(` Current version: ${CURRENT_VERSION}`);
|
|
52
|
+
console.log("");
|
|
53
|
+
|
|
54
|
+
// 检查远程最新版本
|
|
55
|
+
console.log("🔍 Checking for updates...");
|
|
56
|
+
|
|
57
|
+
let latestVersion;
|
|
58
|
+
try {
|
|
59
|
+
latestVersion = await getLatestVersion();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(colorize(`❌ Failed to check for updates: ${error.message}`, "red"));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(` Latest version: ${latestVersion}`);
|
|
66
|
+
console.log("");
|
|
67
|
+
|
|
68
|
+
// 比较版本
|
|
69
|
+
if (!forceUpdate && !isNewerVersion(latestVersion, CURRENT_VERSION)) {
|
|
70
|
+
console.log(colorize("✅ You are already on the latest version!", "green"));
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (forceUpdate) {
|
|
75
|
+
console.log(colorize("⚡ Force update requested", "yellow"));
|
|
76
|
+
} else {
|
|
77
|
+
console.log(colorize(`⬆️ Update available: ${CURRENT_VERSION} → ${latestVersion}`, "green"));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log("");
|
|
81
|
+
|
|
82
|
+
// 确认更新
|
|
83
|
+
if (!skipConfirm) {
|
|
84
|
+
const shouldUpdate = await confirmUpdate(latestVersion);
|
|
85
|
+
if (!shouldUpdate) {
|
|
86
|
+
console.log("Update cancelled.");
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 执行更新
|
|
92
|
+
console.log("");
|
|
93
|
+
console.log(colorize("🚀 Installing update...", "cyan"));
|
|
94
|
+
console.log("");
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await performUpdate();
|
|
98
|
+
console.log("");
|
|
99
|
+
console.log(colorize("✅ Update completed successfully!", "green"));
|
|
100
|
+
console.log("");
|
|
101
|
+
console.log(" Run 'conductor --version' to verify the new version.");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error("");
|
|
104
|
+
console.error(colorize(`❌ Update failed: ${error.message}`, "red"));
|
|
105
|
+
console.error("");
|
|
106
|
+
console.error(" You can try updating manually with:");
|
|
107
|
+
console.error(` npm install -g ${PACKAGE_NAME}@latest`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function getLatestVersion() {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
// 使用 npm view 获取最新版本
|
|
115
|
+
try {
|
|
116
|
+
const result = execSync(`npm view ${PACKAGE_NAME} version --json`, {
|
|
117
|
+
encoding: "utf-8",
|
|
118
|
+
timeout: 10000,
|
|
119
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// npm view 返回的是带引号的字符串 JSON
|
|
123
|
+
const version = JSON.parse(result.trim());
|
|
124
|
+
resolve(version);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// 如果失败,尝试从 registry API 获取
|
|
127
|
+
fetchLatestFromRegistry()
|
|
128
|
+
.then(resolve)
|
|
129
|
+
.catch(reject);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function fetchLatestFromRegistry() {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const https = require("https");
|
|
137
|
+
const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
138
|
+
|
|
139
|
+
https.get(url, { timeout: 10000 }, (res) => {
|
|
140
|
+
let data = "";
|
|
141
|
+
|
|
142
|
+
res.on("data", (chunk) => {
|
|
143
|
+
data += chunk;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
res.on("end", () => {
|
|
147
|
+
try {
|
|
148
|
+
const json = JSON.parse(data);
|
|
149
|
+
if (json.version) {
|
|
150
|
+
resolve(json.version);
|
|
151
|
+
} else {
|
|
152
|
+
reject(new Error("Invalid response from registry"));
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
reject(new Error(`Failed to parse registry response: ${error.message}`));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}).on("error", (error) => {
|
|
159
|
+
reject(new Error(`Network error: ${error.message}`));
|
|
160
|
+
}).on("timeout", () => {
|
|
161
|
+
reject(new Error("Request timed out"));
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isNewerVersion(latest, current) {
|
|
167
|
+
// 简单的版本比较
|
|
168
|
+
const parseVersion = (v) => v.replace(/^v/, "").split(".").map(Number);
|
|
169
|
+
|
|
170
|
+
const latestParts = parseVersion(latest);
|
|
171
|
+
const currentParts = parseVersion(current);
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) {
|
|
174
|
+
const l = latestParts[i] || 0;
|
|
175
|
+
const c = currentParts[i] || 0;
|
|
176
|
+
|
|
177
|
+
if (l > c) return true;
|
|
178
|
+
if (l < c) return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return false; // 版本相同
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function confirmUpdate(version) {
|
|
185
|
+
const rl = readline.createInterface({
|
|
186
|
+
input: process.stdin,
|
|
187
|
+
output: process.stdout,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const answer = await rl.question(
|
|
192
|
+
colorize(`Do you want to update to version ${version}? (Y/n): `, "yellow")
|
|
193
|
+
);
|
|
194
|
+
const normalized = answer.trim().toLowerCase();
|
|
195
|
+
return normalized === "" || normalized === "y" || normalized === "yes";
|
|
196
|
+
} finally {
|
|
197
|
+
rl.close();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function performUpdate() {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
// 检测使用的包管理器
|
|
204
|
+
const packageManager = detectPackageManager();
|
|
205
|
+
console.log(` Using package manager: ${colorize(packageManager, "cyan")}`);
|
|
206
|
+
console.log("");
|
|
207
|
+
|
|
208
|
+
let cmd, args;
|
|
209
|
+
|
|
210
|
+
switch (packageManager) {
|
|
211
|
+
case "pnpm":
|
|
212
|
+
cmd = "pnpm";
|
|
213
|
+
args = ["add", "-g", `${PACKAGE_NAME}@latest`];
|
|
214
|
+
break;
|
|
215
|
+
case "yarn":
|
|
216
|
+
cmd = "yarn";
|
|
217
|
+
args = ["global", "add", `${PACKAGE_NAME}@latest`];
|
|
218
|
+
break;
|
|
219
|
+
case "npm":
|
|
220
|
+
default:
|
|
221
|
+
cmd = "npm";
|
|
222
|
+
args = ["install", "-g", `${PACKAGE_NAME}@latest`];
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log(` Running: ${colorize(`${cmd} ${args.join(" ")}`, "cyan")}`);
|
|
227
|
+
console.log("");
|
|
228
|
+
|
|
229
|
+
const child = spawn(cmd, args, {
|
|
230
|
+
stdio: "inherit",
|
|
231
|
+
shell: true
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
child.on("close", (code) => {
|
|
235
|
+
if (code === 0) {
|
|
236
|
+
resolve();
|
|
237
|
+
} else {
|
|
238
|
+
reject(new Error(`Exit code ${code}`));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
child.on("error", (error) => {
|
|
243
|
+
reject(error);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function detectPackageManager() {
|
|
249
|
+
// 通过分析 conductor 命令的路径来推断包管理器
|
|
250
|
+
try {
|
|
251
|
+
const conductorPath = execSync("which conductor || where conductor", {
|
|
252
|
+
encoding: "utf-8",
|
|
253
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
254
|
+
}).trim();
|
|
255
|
+
|
|
256
|
+
if (conductorPath.includes("pnpm")) {
|
|
257
|
+
return "pnpm";
|
|
258
|
+
}
|
|
259
|
+
if (conductorPath.includes("yarn")) {
|
|
260
|
+
return "yarn";
|
|
261
|
+
}
|
|
262
|
+
if (conductorPath.includes(".npm") || conductorPath.includes("npm")) {
|
|
263
|
+
return "npm";
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
// 忽略错误,使用默认检测
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 检查哪个包管理器可用
|
|
270
|
+
try {
|
|
271
|
+
execSync("pnpm --version", { stdio: "pipe" });
|
|
272
|
+
return "pnpm";
|
|
273
|
+
} catch {
|
|
274
|
+
// pnpm 不可用
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
execSync("yarn --version", { stdio: "pipe" });
|
|
279
|
+
return "yarn";
|
|
280
|
+
} catch {
|
|
281
|
+
// yarn 不可用
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return "npm"; // 默认使用 npm
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function showHelpMessage() {
|
|
288
|
+
console.log(`conductor update - Update the CLI to the latest version
|
|
289
|
+
|
|
290
|
+
Usage: conductor update [options]
|
|
291
|
+
|
|
292
|
+
Options:
|
|
293
|
+
-f, --force Force update even if already on latest version
|
|
294
|
+
-y, --yes Skip confirmation prompt
|
|
295
|
+
-h, --help Show this help message
|
|
296
|
+
|
|
297
|
+
Examples:
|
|
298
|
+
conductor update Check for updates and prompt to install
|
|
299
|
+
conductor update --yes Update without prompting
|
|
300
|
+
conductor update --force Force reinstall the latest version
|
|
301
|
+
`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
main().catch((error) => {
|
|
305
|
+
console.error(colorize(`Unexpected error: ${error?.message || error}`, "red"));
|
|
306
|
+
process.exit(1);
|
|
307
|
+
});
|
package/bin/conductor.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* conductor - Main CLI entry point with subcommand routing.
|
|
5
5
|
*
|
|
6
6
|
* Subcommands:
|
|
7
|
-
* fire
|
|
8
|
-
* daemon
|
|
9
|
-
* config
|
|
7
|
+
* fire - Run AI coding agents with Conductor integration
|
|
8
|
+
* daemon - Start long-running daemon for task orchestration
|
|
9
|
+
* config - Interactive configuration setup
|
|
10
|
+
* update - Update the CLI to the latest version
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -42,7 +43,7 @@ if (argv[0] === "--version" || argv[0] === "-v") {
|
|
|
42
43
|
const subcommand = argv[0];
|
|
43
44
|
|
|
44
45
|
// Valid subcommands
|
|
45
|
-
const validSubcommands = ["fire", "daemon", "config"];
|
|
46
|
+
const validSubcommands = ["fire", "daemon", "config", "update"];
|
|
46
47
|
|
|
47
48
|
if (!validSubcommands.includes(subcommand)) {
|
|
48
49
|
console.error(`Error: Unknown subcommand '${subcommand}'`);
|
|
@@ -83,6 +84,7 @@ Subcommands:
|
|
|
83
84
|
fire Run AI coding agents with Conductor integration
|
|
84
85
|
daemon Start long-running daemon for task orchestration
|
|
85
86
|
config Interactive configuration setup
|
|
87
|
+
update Update the CLI to the latest version
|
|
86
88
|
|
|
87
89
|
Options:
|
|
88
90
|
-h, --help Show this help message
|
|
@@ -93,11 +95,13 @@ Examples:
|
|
|
93
95
|
conductor fire --backend claude -- "add feature"
|
|
94
96
|
conductor daemon --config-file ~/.conductor/config.yaml
|
|
95
97
|
conductor config
|
|
98
|
+
conductor update
|
|
96
99
|
|
|
97
100
|
For subcommand-specific help:
|
|
98
101
|
conductor fire --help
|
|
99
102
|
conductor daemon --help
|
|
100
103
|
conductor config --help
|
|
104
|
+
conductor update --help
|
|
101
105
|
|
|
102
106
|
Version: ${pkgJson.version}
|
|
103
107
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
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.
|
|
20
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
19
|
+
"@love-moon/tui-driver": "0.2.12",
|
|
20
|
+
"@love-moon/conductor-sdk": "0.2.12",
|
|
21
21
|
"dotenv": "^16.4.5",
|
|
22
22
|
"enquirer": "^2.4.1",
|
|
23
23
|
"js-yaml": "^4.1.1",
|
package/src/daemon.js
CHANGED
|
@@ -14,15 +14,35 @@ dotenv.config();
|
|
|
14
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
15
|
const __dirname = path.dirname(__filename);
|
|
16
16
|
const CLI_PATH = path.resolve(__dirname, "..", "bin", "conductor-fire.js");
|
|
17
|
+
const DAEMON_LOG_DIR = path.join(os.homedir(), ".conductor", "logs");
|
|
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
|
+
};
|
|
24
|
+
|
|
25
|
+
function appendDaemonLog(line) {
|
|
26
|
+
try {
|
|
27
|
+
fs.mkdirSync(DAEMON_LOG_DIR, { recursive: true });
|
|
28
|
+
fs.appendFileSync(DAEMON_LOG_PATH, line);
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore file log errors
|
|
31
|
+
}
|
|
32
|
+
}
|
|
17
33
|
|
|
18
34
|
function log(message) {
|
|
19
35
|
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
20
|
-
|
|
36
|
+
const line = `[conductor-daemon ${ts}] ${message}\n`;
|
|
37
|
+
process.stdout.write(line);
|
|
38
|
+
appendDaemonLog(line);
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
function logError(message) {
|
|
24
42
|
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
25
|
-
|
|
43
|
+
const line = `[conductor-daemon ${ts}] ${message}\n`;
|
|
44
|
+
process.stderr.write(line);
|
|
45
|
+
appendDaemonLog(line);
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
function getUserConfig(configFilePath) {
|
|
@@ -42,6 +62,51 @@ function getUserConfig(configFilePath) {
|
|
|
42
62
|
return {};
|
|
43
63
|
}
|
|
44
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
|
+
|
|
45
110
|
// Default CLI commands for supported backends
|
|
46
111
|
const DEFAULT_CLI_LIST = {
|
|
47
112
|
codex: "codex --dangerously-bypass-approvals-and-sandbox",
|
|
@@ -478,6 +543,22 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
478
543
|
}
|
|
479
544
|
|
|
480
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
|
+
|
|
481
562
|
if (event.type === "create_task") {
|
|
482
563
|
handleCreateTask(event.payload);
|
|
483
564
|
return;
|
package/src/fire/history.js
CHANGED
|
@@ -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({
|