@zhongqian97-code/ecode 0.5.14 → 0.5.16
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/index.js +1356 -393
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -7,10 +7,10 @@ import { resolve as resolve5, dirname as dirname9 } from "path";
|
|
|
7
7
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
8
|
import React4 from "react";
|
|
9
9
|
import { render } from "ink";
|
|
10
|
-
import { readFileSync as
|
|
10
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
13
|
-
import { existsSync, readFileSync } from "fs";
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
14
14
|
import { homedir } from "os";
|
|
15
15
|
import { join } from "path";
|
|
16
16
|
var MODEL_CONTEXT_LIMITS = {
|
|
@@ -642,13 +642,13 @@ var READ_TOOL = {
|
|
|
642
642
|
}
|
|
643
643
|
};
|
|
644
644
|
async function readFile2(params) {
|
|
645
|
-
const { path:
|
|
645
|
+
const { path: path12, offset = 0, limit } = params;
|
|
646
646
|
let raw;
|
|
647
647
|
try {
|
|
648
|
-
raw = await fs.readFile(
|
|
648
|
+
raw = await fs.readFile(path12, "utf8");
|
|
649
649
|
} catch (err) {
|
|
650
650
|
const msg = err instanceof Error ? err.message : String(err);
|
|
651
|
-
return `Error reading ${
|
|
651
|
+
return `Error reading ${path12}: ${msg}`;
|
|
652
652
|
}
|
|
653
653
|
const lines = raw.split("\n");
|
|
654
654
|
const sliced = limit !== void 0 ? lines.slice(offset, offset + limit) : lines.slice(offset);
|
|
@@ -704,28 +704,28 @@ var EDIT_TOOL = {
|
|
|
704
704
|
}
|
|
705
705
|
};
|
|
706
706
|
async function editFile(params) {
|
|
707
|
-
const { path:
|
|
707
|
+
const { path: path12, old_string, new_string } = params;
|
|
708
708
|
let content;
|
|
709
709
|
try {
|
|
710
|
-
content = await fs3.readFile(
|
|
710
|
+
content = await fs3.readFile(path12, "utf8");
|
|
711
711
|
} catch (err) {
|
|
712
712
|
const msg = err instanceof Error ? err.message : String(err);
|
|
713
|
-
return `Error reading ${
|
|
713
|
+
return `Error reading ${path12}: ${msg}`;
|
|
714
714
|
}
|
|
715
715
|
const count = countOccurrences(content, old_string);
|
|
716
716
|
if (count === 0) {
|
|
717
|
-
return `Error: old_string not found in ${
|
|
717
|
+
return `Error: old_string not found in ${path12}`;
|
|
718
718
|
}
|
|
719
719
|
if (count > 1) {
|
|
720
|
-
return `Error: old_string appears ${count} times in ${
|
|
720
|
+
return `Error: old_string appears ${count} times in ${path12} (ambiguous \u2014 add more context)`;
|
|
721
721
|
}
|
|
722
722
|
const updated = content.replace(old_string, new_string);
|
|
723
723
|
try {
|
|
724
|
-
await fs3.writeFile(
|
|
725
|
-
return `Edited ${
|
|
724
|
+
await fs3.writeFile(path12, updated, "utf8");
|
|
725
|
+
return `Edited ${path12}`;
|
|
726
726
|
} catch (err) {
|
|
727
727
|
const msg = err instanceof Error ? err.message : String(err);
|
|
728
|
-
return `Error writing ${
|
|
728
|
+
return `Error writing ${path12}: ${msg}`;
|
|
729
729
|
}
|
|
730
730
|
}
|
|
731
731
|
function countOccurrences(haystack, needle) {
|
|
@@ -1276,19 +1276,96 @@ function todo(params) {
|
|
|
1276
1276
|
}
|
|
1277
1277
|
}
|
|
1278
1278
|
|
|
1279
|
-
// src/
|
|
1280
|
-
import * as
|
|
1279
|
+
// src/tools/task.ts
|
|
1280
|
+
import * as crypto3 from "crypto";
|
|
1281
|
+
import * as path7 from "path";
|
|
1282
|
+
|
|
1283
|
+
// src/subagent/runtime.ts
|
|
1284
|
+
async function runSubagentRuntime(options) {
|
|
1285
|
+
const { messages: initial, tools, provider, maxTurns, executeToolCall: executeToolCall2, onOutput } = options;
|
|
1286
|
+
const messages = [...initial];
|
|
1287
|
+
let turnCount = 0;
|
|
1288
|
+
while (turnCount < maxTurns) {
|
|
1289
|
+
turnCount++;
|
|
1290
|
+
let assistantText = "";
|
|
1291
|
+
const toolCalls = [];
|
|
1292
|
+
for await (const chunk of provider.stream(messages, tools)) {
|
|
1293
|
+
if (chunk.text) {
|
|
1294
|
+
assistantText += chunk.text;
|
|
1295
|
+
onOutput?.(chunk.text);
|
|
1296
|
+
}
|
|
1297
|
+
if (chunk.done && chunk.toolCalls) {
|
|
1298
|
+
toolCalls.push(...chunk.toolCalls);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (toolCalls.length === 0) {
|
|
1302
|
+
messages.push({ role: "assistant", content: assistantText });
|
|
1303
|
+
return { success: true, finalText: assistantText, turnCount };
|
|
1304
|
+
}
|
|
1305
|
+
messages.push({
|
|
1306
|
+
role: "assistant",
|
|
1307
|
+
content: assistantText || null,
|
|
1308
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
1309
|
+
id: tc.id,
|
|
1310
|
+
type: "function",
|
|
1311
|
+
function: { name: tc.name, arguments: tc.arguments }
|
|
1312
|
+
}))
|
|
1313
|
+
});
|
|
1314
|
+
for (const tc of toolCalls) {
|
|
1315
|
+
const result = await executeToolCall2(tc);
|
|
1316
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return {
|
|
1320
|
+
success: false,
|
|
1321
|
+
finalText: "",
|
|
1322
|
+
turnCount,
|
|
1323
|
+
error: `Subagent exceeded max turns (${maxTurns})`
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/logger.ts
|
|
1281
1328
|
import * as fs7 from "fs";
|
|
1282
1329
|
import * as path5 from "path";
|
|
1330
|
+
function createLogger(logDir, sessionStart) {
|
|
1331
|
+
fs7.mkdirSync(logDir, { recursive: true });
|
|
1332
|
+
const filename = sessionStart.toISOString().replace(/:/g, "-").replace(/\..+/, "") + ".jsonl";
|
|
1333
|
+
const filePath = path5.join(logDir, filename);
|
|
1334
|
+
return {
|
|
1335
|
+
filePath,
|
|
1336
|
+
/**
|
|
1337
|
+
* 将单条日志条目序列化为 JSON 并同步追加到文件(末尾加换行符)。
|
|
1338
|
+
*
|
|
1339
|
+
* 使用 appendFileSync 而非 appendFile(异步版本)的原因:
|
|
1340
|
+
* 进程崩溃或 Ctrl-C 退出时,异步写操作可能尚未完成,导致最后几条记录丢失。
|
|
1341
|
+
* 同步写入虽然阻塞事件循环,但日志条目通常很小(< 1KB),延迟可忽略。
|
|
1342
|
+
*
|
|
1343
|
+
* 写入失败时输出到 stderr 而非抛出异常,防止日志错误中断正常业务流程。
|
|
1344
|
+
*/
|
|
1345
|
+
append(entry) {
|
|
1346
|
+
try {
|
|
1347
|
+
fs7.appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
process.stderr.write(`[logger] Failed to write log entry: ${err}
|
|
1350
|
+
`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/sessions/metadata.ts
|
|
1357
|
+
import * as crypto2 from "crypto";
|
|
1358
|
+
import * as fs8 from "fs";
|
|
1359
|
+
import * as path6 from "path";
|
|
1283
1360
|
function metadataPathFromLogFile(logFilePath2) {
|
|
1284
|
-
const base =
|
|
1285
|
-
const dir =
|
|
1286
|
-
return
|
|
1361
|
+
const base = path6.basename(logFilePath2, ".jsonl");
|
|
1362
|
+
const dir = path6.dirname(logFilePath2);
|
|
1363
|
+
return path6.join(dir, `${base}-session.json`);
|
|
1287
1364
|
}
|
|
1288
1365
|
function createSessionMetadata(logFilePath2, model) {
|
|
1289
1366
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1290
1367
|
return {
|
|
1291
|
-
id:
|
|
1368
|
+
id: crypto2.randomUUID(),
|
|
1292
1369
|
startTime: now,
|
|
1293
1370
|
lastActivity: now,
|
|
1294
1371
|
cwd: process.cwd(),
|
|
@@ -1296,13 +1373,13 @@ function createSessionMetadata(logFilePath2, model) {
|
|
|
1296
1373
|
title: "",
|
|
1297
1374
|
turnCount: 0,
|
|
1298
1375
|
totalTokens: 0,
|
|
1299
|
-
logFile:
|
|
1376
|
+
logFile: path6.basename(logFilePath2)
|
|
1300
1377
|
};
|
|
1301
1378
|
}
|
|
1302
1379
|
function writeSessionMetadata(logFilePath2, metadata) {
|
|
1303
1380
|
const metaPath = metadataPathFromLogFile(logFilePath2);
|
|
1304
1381
|
try {
|
|
1305
|
-
|
|
1382
|
+
fs8.writeFileSync(metaPath, JSON.stringify(metadata, null, 2) + "\n");
|
|
1306
1383
|
} catch (err) {
|
|
1307
1384
|
process.stderr.write(`[sessions] Failed to write metadata: ${err}
|
|
1308
1385
|
`);
|
|
@@ -1310,7 +1387,7 @@ function writeSessionMetadata(logFilePath2, metadata) {
|
|
|
1310
1387
|
}
|
|
1311
1388
|
function readSessionMetadata(metaFilePath) {
|
|
1312
1389
|
try {
|
|
1313
|
-
const raw =
|
|
1390
|
+
const raw = fs8.readFileSync(metaFilePath, "utf-8");
|
|
1314
1391
|
return JSON.parse(raw);
|
|
1315
1392
|
} catch {
|
|
1316
1393
|
return null;
|
|
@@ -1320,7 +1397,7 @@ function updateSessionMetadata(logFilePath2, partial) {
|
|
|
1320
1397
|
const metaPath = metadataPathFromLogFile(logFilePath2);
|
|
1321
1398
|
let existing = null;
|
|
1322
1399
|
try {
|
|
1323
|
-
const raw =
|
|
1400
|
+
const raw = fs8.readFileSync(metaPath, "utf-8");
|
|
1324
1401
|
existing = JSON.parse(raw);
|
|
1325
1402
|
} catch {
|
|
1326
1403
|
return;
|
|
@@ -1329,11 +1406,11 @@ function updateSessionMetadata(logFilePath2, partial) {
|
|
|
1329
1406
|
}
|
|
1330
1407
|
function listSessions(logDir) {
|
|
1331
1408
|
try {
|
|
1332
|
-
const files =
|
|
1409
|
+
const files = fs8.readdirSync(logDir);
|
|
1333
1410
|
const metaFiles = files.filter((f) => f.endsWith("-session.json"));
|
|
1334
1411
|
const sessions = [];
|
|
1335
1412
|
for (const file of metaFiles) {
|
|
1336
|
-
const meta = readSessionMetadata(
|
|
1413
|
+
const meta = readSessionMetadata(path6.join(logDir, file));
|
|
1337
1414
|
if (meta) sessions.push(meta);
|
|
1338
1415
|
}
|
|
1339
1416
|
return sessions.sort(
|
|
@@ -1354,101 +1431,54 @@ function generateTitle(firstUserMessage) {
|
|
|
1354
1431
|
return oneLine.length > 50 ? oneLine.slice(0, 47) + "..." : oneLine;
|
|
1355
1432
|
}
|
|
1356
1433
|
|
|
1357
|
-
// src/logger.ts
|
|
1358
|
-
import * as fs8 from "fs";
|
|
1359
|
-
import * as path6 from "path";
|
|
1360
|
-
function createLogger(logDir, sessionStart) {
|
|
1361
|
-
fs8.mkdirSync(logDir, { recursive: true });
|
|
1362
|
-
const filename = sessionStart.toISOString().replace(/:/g, "-").replace(/\..+/, "") + ".jsonl";
|
|
1363
|
-
const filePath = path6.join(logDir, filename);
|
|
1364
|
-
return {
|
|
1365
|
-
filePath,
|
|
1366
|
-
/**
|
|
1367
|
-
* 将单条日志条目序列化为 JSON 并同步追加到文件(末尾加换行符)。
|
|
1368
|
-
*
|
|
1369
|
-
* 使用 appendFileSync 而非 appendFile(异步版本)的原因:
|
|
1370
|
-
* 进程崩溃或 Ctrl-C 退出时,异步写操作可能尚未完成,导致最后几条记录丢失。
|
|
1371
|
-
* 同步写入虽然阻塞事件循环,但日志条目通常很小(< 1KB),延迟可忽略。
|
|
1372
|
-
*
|
|
1373
|
-
* 写入失败时输出到 stderr 而非抛出异常,防止日志错误中断正常业务流程。
|
|
1374
|
-
*/
|
|
1375
|
-
append(entry) {
|
|
1376
|
-
try {
|
|
1377
|
-
fs8.appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
1378
|
-
} catch (err) {
|
|
1379
|
-
process.stderr.write(`[logger] Failed to write log entry: ${err}
|
|
1380
|
-
`);
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
};
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
1434
|
// src/tools/task.ts
|
|
1387
1435
|
var TASK_TOOL = {
|
|
1388
1436
|
type: "function",
|
|
1389
1437
|
function: {
|
|
1390
1438
|
name: "task",
|
|
1391
|
-
description: "
|
|
1439
|
+
description: "Delegate a well-defined subtask to an isolated child agent. The child agent runs its own agentic loop, executes tools, and returns a single result. Use this when a subtask is independent, well-scoped, and benefits from a clean context.",
|
|
1392
1440
|
parameters: {
|
|
1393
1441
|
type: "object",
|
|
1394
1442
|
properties: {
|
|
1395
1443
|
description: {
|
|
1396
1444
|
type: "string",
|
|
1397
|
-
description: "
|
|
1445
|
+
description: "Short title for the subtask (used in logs and status output)."
|
|
1398
1446
|
},
|
|
1399
1447
|
prompt: {
|
|
1400
1448
|
type: "string",
|
|
1401
|
-
description: "
|
|
1449
|
+
description: "Full task instructions for the child agent."
|
|
1402
1450
|
},
|
|
1403
1451
|
context: {
|
|
1404
1452
|
type: "string",
|
|
1405
|
-
description: "
|
|
1453
|
+
description: "Optional additional context to pass to the child agent."
|
|
1406
1454
|
},
|
|
1407
|
-
|
|
1455
|
+
cwd: {
|
|
1408
1456
|
type: "string",
|
|
1409
|
-
description: "
|
|
1457
|
+
description: "Working directory for the child agent (defaults to current directory)."
|
|
1410
1458
|
},
|
|
1411
|
-
|
|
1459
|
+
model: {
|
|
1412
1460
|
type: "string",
|
|
1413
|
-
description: "
|
|
1461
|
+
description: "Optional model override for the child agent."
|
|
1414
1462
|
},
|
|
1415
1463
|
max_turns: {
|
|
1416
1464
|
type: "number",
|
|
1417
|
-
description: "
|
|
1465
|
+
description: "Maximum agentic loop turns for the child agent (default: 8)."
|
|
1418
1466
|
}
|
|
1419
1467
|
},
|
|
1420
1468
|
required: ["description", "prompt"]
|
|
1421
1469
|
}
|
|
1422
1470
|
}
|
|
1423
1471
|
};
|
|
1424
|
-
var SUBAGENT_SYSTEM_PROMPT =
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
].join("\n");
|
|
1433
|
-
var BASH_TOOL = {
|
|
1434
|
-
type: "function",
|
|
1435
|
-
function: {
|
|
1436
|
-
name: "bash",
|
|
1437
|
-
description: "Execute a shell command and return its output.",
|
|
1438
|
-
parameters: {
|
|
1439
|
-
type: "object",
|
|
1440
|
-
properties: {
|
|
1441
|
-
command: { type: "string", description: "The shell command to run." }
|
|
1442
|
-
},
|
|
1443
|
-
required: ["command"]
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
};
|
|
1472
|
+
var SUBAGENT_SYSTEM_PROMPT = `You are a focused subagent executing a delegated task on behalf of a parent agent.
|
|
1473
|
+
|
|
1474
|
+
Rules:
|
|
1475
|
+
- Complete the task independently using the tools available to you.
|
|
1476
|
+
- Do not ask the human clarifying questions for routine operations \u2014 make reasonable decisions.
|
|
1477
|
+
- When done, return a clear, concise result.
|
|
1478
|
+
- If you are blocked, describe the blocker and the minimum next step needed.
|
|
1479
|
+
- You cannot delegate tasks to other agents.`;
|
|
1447
1480
|
var SUBAGENT_TOOLS = [
|
|
1448
|
-
|
|
1449
|
-
type: BASH_TOOL.type,
|
|
1450
|
-
function: BASH_TOOL.function
|
|
1451
|
-
},
|
|
1481
|
+
BASH_TOOL,
|
|
1452
1482
|
READ_TOOL,
|
|
1453
1483
|
WRITE_TOOL,
|
|
1454
1484
|
EDIT_TOOL,
|
|
@@ -1457,223 +1487,399 @@ var SUBAGENT_TOOLS = [
|
|
|
1457
1487
|
APPLY_PATCH_TOOL,
|
|
1458
1488
|
TODO_TOOL
|
|
1459
1489
|
];
|
|
1460
|
-
function
|
|
1461
|
-
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
if (
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
);
|
|
1490
|
+
async function handleTaskTool(args, deps) {
|
|
1491
|
+
const { description, prompt, context, max_turns = 8 } = args;
|
|
1492
|
+
const { provider, print = () => void 0, logDir } = deps;
|
|
1493
|
+
const taskId = crypto3.randomUUID().slice(0, 8);
|
|
1494
|
+
let logger;
|
|
1495
|
+
if (logDir) {
|
|
1496
|
+
logger = createLogger(logDir, /* @__PURE__ */ new Date());
|
|
1497
|
+
const meta = createSessionMetadata(logger.filePath, "subagent");
|
|
1498
|
+
writeSessionMetadata(logger.filePath, {
|
|
1499
|
+
...meta,
|
|
1500
|
+
title: `[subagent] ${description}`
|
|
1501
|
+
});
|
|
1473
1502
|
}
|
|
1474
|
-
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1503
|
+
const userContent = context ? `Context:
|
|
1504
|
+
${context}
|
|
1505
|
+
|
|
1506
|
+
Task:
|
|
1507
|
+
${prompt}` : prompt;
|
|
1508
|
+
const messages = [
|
|
1509
|
+
{ role: "system", content: SUBAGENT_SYSTEM_PROMPT },
|
|
1510
|
+
{ role: "user", content: userContent }
|
|
1511
|
+
];
|
|
1512
|
+
if (logger) {
|
|
1513
|
+
logger.append({ ts: (/* @__PURE__ */ new Date()).toISOString(), role: "user", content: userContent });
|
|
1514
|
+
}
|
|
1515
|
+
const bashDeps = {
|
|
1516
|
+
executeBash,
|
|
1517
|
+
confirm: async () => false,
|
|
1518
|
+
// danger commands will be denied
|
|
1519
|
+
print,
|
|
1520
|
+
autoApproveNormal: true
|
|
1521
|
+
};
|
|
1522
|
+
const executeToolCall2 = async (tc) => {
|
|
1523
|
+
let result;
|
|
1524
|
+
if (tc.name === "bash") {
|
|
1525
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1526
|
+
result = await handleBashTool(tcArgs.command, bashDeps);
|
|
1527
|
+
} else if (tc.name === "read") {
|
|
1528
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1529
|
+
result = await readFile2(tcArgs);
|
|
1530
|
+
} else if (tc.name === "write") {
|
|
1531
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1532
|
+
result = await writeFile2(tcArgs);
|
|
1533
|
+
} else if (tc.name === "edit") {
|
|
1534
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1535
|
+
result = await editFile(tcArgs);
|
|
1536
|
+
} else if (tc.name === "glob") {
|
|
1537
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1538
|
+
result = await globFiles(tcArgs);
|
|
1539
|
+
} else if (tc.name === "grep") {
|
|
1540
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1541
|
+
result = await grepFiles(tcArgs);
|
|
1542
|
+
} else if (tc.name === "apply_patch") {
|
|
1543
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1544
|
+
result = await applyPatch(tcArgs);
|
|
1545
|
+
} else if (tc.name === "todo") {
|
|
1546
|
+
const tcArgs = JSON.parse(tc.arguments);
|
|
1547
|
+
result = todo(tcArgs);
|
|
1548
|
+
} else {
|
|
1549
|
+
result = `Unknown tool: ${tc.name}`;
|
|
1550
|
+
}
|
|
1551
|
+
if (logger) {
|
|
1552
|
+
logger.append({
|
|
1553
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1554
|
+
role: "tool",
|
|
1555
|
+
content: result,
|
|
1556
|
+
tool_call_id: tc.id
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
return result;
|
|
1560
|
+
};
|
|
1561
|
+
const runtime = await runSubagentRuntime({
|
|
1562
|
+
messages,
|
|
1563
|
+
tools: SUBAGENT_TOOLS,
|
|
1564
|
+
provider,
|
|
1565
|
+
maxTurns: max_turns,
|
|
1566
|
+
executeToolCall: executeToolCall2,
|
|
1567
|
+
onOutput: (text) => print(text)
|
|
1568
|
+
});
|
|
1569
|
+
if (logger) {
|
|
1570
|
+
logger.append({
|
|
1571
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1572
|
+
role: "assistant",
|
|
1573
|
+
content: runtime.finalText || runtime.error || null
|
|
1574
|
+
});
|
|
1575
|
+
updateSessionMetadata(logger.filePath, {
|
|
1576
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1577
|
+
turnCount: runtime.turnCount,
|
|
1578
|
+
title: `[subagent] ${description}`
|
|
1579
|
+
});
|
|
1481
1580
|
}
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1581
|
+
const logLine = logger ? `log_file: ${path7.basename(logger.filePath)}
|
|
1582
|
+
` : "";
|
|
1583
|
+
if (!runtime.success) {
|
|
1584
|
+
return [
|
|
1585
|
+
`task_id: ${taskId}`,
|
|
1586
|
+
`description: ${description}`,
|
|
1587
|
+
logLine.trim(),
|
|
1588
|
+
"",
|
|
1589
|
+
`<task_result>`,
|
|
1590
|
+
`Error: ${runtime.error ?? "Subagent failed"}`,
|
|
1591
|
+
`</task_result>`
|
|
1592
|
+
].filter((l) => l !== "" || l === "").join("\n").replace(/\n{3,}/g, "\n\n");
|
|
1487
1593
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
const cls = classifyCommand(command);
|
|
1498
|
-
if (cls === "danger") return SKIP_MESSAGE;
|
|
1499
|
-
if (cls === "normal" && !autoApproveNormal) return SKIP_MESSAGE;
|
|
1500
|
-
const result = await executeBash(command);
|
|
1501
|
-
let output = "";
|
|
1502
|
-
if (result.stdout) output += result.stdout;
|
|
1503
|
-
if (result.stderr) output += result.stderr;
|
|
1504
|
-
if (result.exitCode !== 0) output += `
|
|
1505
|
-
[exit code: ${result.exitCode}]`;
|
|
1506
|
-
return output || "(no output)";
|
|
1594
|
+
return [
|
|
1595
|
+
`task_id: ${taskId}`,
|
|
1596
|
+
`description: ${description}`,
|
|
1597
|
+
logLine.trim(),
|
|
1598
|
+
"",
|
|
1599
|
+
`<task_result>`,
|
|
1600
|
+
runtime.finalText,
|
|
1601
|
+
`</task_result>`
|
|
1602
|
+
].join("\n").replace(/\n{3,}/g, "\n\n");
|
|
1507
1603
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1604
|
+
|
|
1605
|
+
// src/tools/web_fetch.ts
|
|
1606
|
+
var WEB_FETCH_TOOL = {
|
|
1607
|
+
type: "function",
|
|
1608
|
+
function: {
|
|
1609
|
+
name: "web_fetch",
|
|
1610
|
+
description: "Fetch a URL and extract its text content. Supports HTML, plain text and markdown pages. Returns a structured response with headers and extracted content.",
|
|
1611
|
+
parameters: {
|
|
1612
|
+
type: "object",
|
|
1613
|
+
properties: {
|
|
1614
|
+
url: { type: "string", description: "The URL to fetch (http or https only)." },
|
|
1615
|
+
extract_mode: {
|
|
1616
|
+
type: "string",
|
|
1617
|
+
enum: ["text", "markdown"],
|
|
1618
|
+
description: "How to extract content from HTML: 'markdown' (default) preserves headings/lists as markdown, 'text' strips all tags."
|
|
1619
|
+
},
|
|
1620
|
+
max_chars: {
|
|
1621
|
+
type: "number",
|
|
1622
|
+
description: "Maximum characters to return (default 20000, max 100000)."
|
|
1623
|
+
},
|
|
1624
|
+
timeout_ms: {
|
|
1625
|
+
type: "number",
|
|
1626
|
+
description: "Request timeout in milliseconds (default 20000, min 1000, max 60000)."
|
|
1627
|
+
}
|
|
1628
|
+
},
|
|
1629
|
+
required: ["url"]
|
|
1515
1630
|
}
|
|
1516
|
-
return runNonInteractiveBash(parsed.command, autoApproveNormal);
|
|
1517
|
-
}
|
|
1518
|
-
if (name === "read") {
|
|
1519
|
-
return readFile2(JSON.parse(args));
|
|
1520
|
-
}
|
|
1521
|
-
if (name === "write") {
|
|
1522
|
-
return writeFile2(JSON.parse(args));
|
|
1523
1631
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
if (
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1632
|
+
};
|
|
1633
|
+
var CACHE_TTL = 6e5;
|
|
1634
|
+
var CACHE_MAX = 64;
|
|
1635
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1636
|
+
function cacheGet(key) {
|
|
1637
|
+
const entry = cache.get(key);
|
|
1638
|
+
if (!entry) return void 0;
|
|
1639
|
+
if (Date.now() - entry.at > CACHE_TTL) {
|
|
1640
|
+
cache.delete(key);
|
|
1641
|
+
return void 0;
|
|
1642
|
+
}
|
|
1643
|
+
return entry.result;
|
|
1644
|
+
}
|
|
1645
|
+
function cacheSet(key, result) {
|
|
1646
|
+
if (cache.size >= CACHE_MAX) {
|
|
1647
|
+
const oldest = cache.keys().next().value;
|
|
1648
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
1649
|
+
}
|
|
1650
|
+
cache.set(key, { at: Date.now(), result });
|
|
1651
|
+
}
|
|
1652
|
+
function isPrivateHost(hostname) {
|
|
1653
|
+
if (hostname === "localhost" || hostname === "0.0.0.0" || hostname === "::1") return true;
|
|
1654
|
+
if (hostname.endsWith(".local")) return true;
|
|
1655
|
+
const v4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
1656
|
+
if (v4) {
|
|
1657
|
+
const [, a, b] = v4.map(Number);
|
|
1658
|
+
if (a === 127) return true;
|
|
1659
|
+
if (a === 10) return true;
|
|
1660
|
+
if (a === 192 && b === 168) return true;
|
|
1661
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
1662
|
+
if (a === 0) return true;
|
|
1538
1663
|
}
|
|
1539
|
-
return
|
|
1664
|
+
return false;
|
|
1540
1665
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
const contextBlock = buildParentContext(deps.parentMessages, params.context);
|
|
1544
|
-
const originalCwd = process.cwd();
|
|
1545
|
-
const childCwd = params.cwd ?? originalCwd;
|
|
1546
|
-
const profile = resolveActiveProfile(deps.config);
|
|
1547
|
-
const llm = deps.llm ?? createProvider({
|
|
1548
|
-
...profile,
|
|
1549
|
-
model: params.model ?? profile.model
|
|
1550
|
-
});
|
|
1551
|
-
const messages = [{ role: "system", content: SUBAGENT_SYSTEM_PROMPT }];
|
|
1552
|
-
if (contextBlock) {
|
|
1553
|
-
messages.push({
|
|
1554
|
-
role: "user",
|
|
1555
|
-
content: "Parent session context for this delegated task:\n\n" + contextBlock
|
|
1556
|
-
});
|
|
1557
|
-
}
|
|
1558
|
-
messages.push({ role: "user", content: params.prompt });
|
|
1559
|
-
let logger = null;
|
|
1560
|
-
let sessionMeta = null;
|
|
1561
|
-
if (deps.config.logDir) {
|
|
1562
|
-
logger = createLogger(deps.config.logDir, /* @__PURE__ */ new Date());
|
|
1563
|
-
sessionMeta = createSessionMetadata(
|
|
1564
|
-
logger.filePath,
|
|
1565
|
-
params.model ?? profile.model
|
|
1566
|
-
);
|
|
1567
|
-
sessionMeta.title = generateTitle(`[subagent] ${params.description}`);
|
|
1568
|
-
writeSessionMetadata(logger.filePath, sessionMeta);
|
|
1569
|
-
}
|
|
1570
|
-
for (const msg of messages) {
|
|
1571
|
-
appendLog(logger, {
|
|
1572
|
-
role: msg.role,
|
|
1573
|
-
content: "content" in msg ? msg.content : null
|
|
1574
|
-
});
|
|
1575
|
-
}
|
|
1576
|
-
const autoApproveNormal = deps.autoApproveNormal ?? true;
|
|
1666
|
+
function validateWebFetchUrl(rawUrl) {
|
|
1667
|
+
let parsed;
|
|
1577
1668
|
try {
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1669
|
+
parsed = new URL(rawUrl);
|
|
1670
|
+
} catch {
|
|
1671
|
+
return { ok: false, error: `invalid URL: ${rawUrl}` };
|
|
1672
|
+
}
|
|
1673
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1674
|
+
return { ok: false, error: `unsupported protocol: ${parsed.protocol}` };
|
|
1675
|
+
}
|
|
1676
|
+
if (parsed.username || parsed.password) {
|
|
1677
|
+
return { ok: false, error: "URL must not contain credentials" };
|
|
1678
|
+
}
|
|
1679
|
+
if (isPrivateHost(parsed.hostname)) {
|
|
1680
|
+
return { ok: false, error: `host is private/local: ${parsed.hostname}` };
|
|
1681
|
+
}
|
|
1682
|
+
return { ok: true, parsed };
|
|
1683
|
+
}
|
|
1684
|
+
function rootHost(hostname) {
|
|
1685
|
+
const parts = hostname.split(".");
|
|
1686
|
+
return parts.length > 2 ? parts.slice(-2).join(".") : hostname;
|
|
1687
|
+
}
|
|
1688
|
+
function isSameOrWwwHost(a, b) {
|
|
1689
|
+
if (a === b) return true;
|
|
1690
|
+
const ra = rootHost(a);
|
|
1691
|
+
const rb = rootHost(b);
|
|
1692
|
+
return ra === rb && (a === `www.${ra}` || b === `www.${ra}` || a === ra || b === ra);
|
|
1693
|
+
}
|
|
1694
|
+
function extractHtmlText(html, mode) {
|
|
1695
|
+
let s = html;
|
|
1696
|
+
s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
|
|
1697
|
+
s = s.replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
1698
|
+
s = s.replace(/<!--[\s\S]*?-->/g, "");
|
|
1699
|
+
if (mode === "markdown") {
|
|
1700
|
+
s = s.replace(/<pre[^>]*>\s*<code[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_, code) => {
|
|
1701
|
+
const decoded = decodeEntities(code);
|
|
1702
|
+
return `
|
|
1703
|
+
|
|
1704
|
+
\`\`\`
|
|
1705
|
+
${decoded}
|
|
1706
|
+
\`\`\`
|
|
1707
|
+
|
|
1708
|
+
`;
|
|
1709
|
+
});
|
|
1710
|
+
s = s.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, code) => `\`${code}\``);
|
|
1711
|
+
for (let n = 1; n <= 6; n++) {
|
|
1712
|
+
const hashes = "#".repeat(n);
|
|
1713
|
+
s = s.replace(new RegExp(`<h${n}[^>]*>`, "gi"), `
|
|
1714
|
+
|
|
1715
|
+
${hashes} `);
|
|
1716
|
+
s = s.replace(new RegExp(`</h${n}>`, "gi"), "\n\n");
|
|
1717
|
+
}
|
|
1718
|
+
s = s.replace(/<li[^>]*>/gi, "\n- ");
|
|
1719
|
+
} else {
|
|
1720
|
+
for (let n = 1; n <= 6; n++) {
|
|
1721
|
+
s = s.replace(new RegExp(`<h${n}[^>]*>`, "gi"), "\n\n");
|
|
1722
|
+
s = s.replace(new RegExp(`</h${n}>`, "gi"), "\n\n");
|
|
1723
|
+
}
|
|
1724
|
+
s = s.replace(/<li[^>]*>/gi, "\n");
|
|
1725
|
+
}
|
|
1726
|
+
s = s.replace(/<\/p>/gi, "\n\n");
|
|
1727
|
+
s = s.replace(/<p[^>]*>/gi, "\n\n");
|
|
1728
|
+
s = s.replace(/<\/div>/gi, "\n");
|
|
1729
|
+
s = s.replace(/<br\s*\/?>/gi, "\n");
|
|
1730
|
+
s = s.replace(/<\/li>/gi, "\n");
|
|
1731
|
+
s = s.replace(/<[^>]+>/g, "");
|
|
1732
|
+
s = decodeEntities(s);
|
|
1733
|
+
s = s.replace(/[ \t]+/g, " ");
|
|
1734
|
+
s = s.replace(/\n{3,}/g, "\n\n");
|
|
1735
|
+
return s.trim();
|
|
1736
|
+
}
|
|
1737
|
+
function decodeEntities(s) {
|
|
1738
|
+
return s.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
1739
|
+
}
|
|
1740
|
+
function statusText(status) {
|
|
1741
|
+
const map = {
|
|
1742
|
+
200: "OK",
|
|
1743
|
+
201: "Created",
|
|
1744
|
+
204: "No Content",
|
|
1745
|
+
301: "Moved Permanently",
|
|
1746
|
+
302: "Found",
|
|
1747
|
+
303: "See Other",
|
|
1748
|
+
307: "Temporary Redirect",
|
|
1749
|
+
308: "Permanent Redirect",
|
|
1750
|
+
400: "Bad Request",
|
|
1751
|
+
401: "Unauthorized",
|
|
1752
|
+
403: "Forbidden",
|
|
1753
|
+
404: "Not Found",
|
|
1754
|
+
405: "Method Not Allowed",
|
|
1755
|
+
429: "Too Many Requests",
|
|
1756
|
+
500: "Internal Server Error",
|
|
1757
|
+
502: "Bad Gateway",
|
|
1758
|
+
503: "Service Unavailable"
|
|
1759
|
+
};
|
|
1760
|
+
return map[status] ?? String(status);
|
|
1761
|
+
}
|
|
1762
|
+
async function fetchWithRedirects(url, timeoutMs, signal) {
|
|
1763
|
+
let current = url;
|
|
1764
|
+
let hops = 0;
|
|
1765
|
+
const MAX_REDIRECTS = 5;
|
|
1766
|
+
const originalHost = new URL(url).hostname;
|
|
1767
|
+
while (hops <= MAX_REDIRECTS) {
|
|
1768
|
+
const response = await fetch(current, {
|
|
1769
|
+
redirect: "manual",
|
|
1770
|
+
signal,
|
|
1771
|
+
headers: {
|
|
1772
|
+
Accept: "text/html, text/plain, text/markdown, application/xhtml+xml, */*",
|
|
1773
|
+
"User-Agent": "ecode-web-fetch/0.1"
|
|
1616
1774
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
function: { name: tc.name, arguments: tc.arguments }
|
|
1624
|
-
})),
|
|
1625
|
-
...assistantReasoning ? { reasoning_content: assistantReasoning } : {}
|
|
1626
|
-
};
|
|
1627
|
-
messages.push(assistantMsg);
|
|
1628
|
-
appendLog(logger, {
|
|
1629
|
-
role: "assistant",
|
|
1630
|
-
content: assistantText || null,
|
|
1631
|
-
tool_calls: assistantMsg.tool_calls
|
|
1632
|
-
});
|
|
1633
|
-
for (const tc of toolCalls) {
|
|
1634
|
-
const toolResult = await executeSubagentToolCall(tc.name, tc.arguments, autoApproveNormal);
|
|
1635
|
-
const finalResult = toolResult || SKIP_MESSAGE;
|
|
1636
|
-
messages.push({ role: "tool", tool_call_id: tc.id, content: finalResult });
|
|
1637
|
-
appendLog(logger, {
|
|
1638
|
-
role: "tool",
|
|
1639
|
-
content: finalResult,
|
|
1640
|
-
tool_call_id: tc.id
|
|
1641
|
-
});
|
|
1775
|
+
});
|
|
1776
|
+
const status = response.status;
|
|
1777
|
+
if (status >= 300 && status < 400) {
|
|
1778
|
+
const location = response.headers.get("location");
|
|
1779
|
+
if (!location) {
|
|
1780
|
+
return { response, finalUrl: current };
|
|
1642
1781
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
});
|
|
1782
|
+
const nextUrl = new URL(location, current).toString();
|
|
1783
|
+
const nextHost = new URL(nextUrl).hostname;
|
|
1784
|
+
if (!isSameOrWwwHost(originalHost, nextHost)) {
|
|
1785
|
+
throw new Error(`redirect to different host is not allowed automatically (-> ${nextUrl})`);
|
|
1648
1786
|
}
|
|
1787
|
+
current = nextUrl;
|
|
1788
|
+
hops++;
|
|
1789
|
+
continue;
|
|
1649
1790
|
}
|
|
1650
|
-
return
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1791
|
+
return { response, finalUrl: current };
|
|
1792
|
+
}
|
|
1793
|
+
throw new Error(`too many redirects (> ${MAX_REDIRECTS})`);
|
|
1794
|
+
}
|
|
1795
|
+
async function webFetch(params) {
|
|
1796
|
+
const {
|
|
1797
|
+
url,
|
|
1798
|
+
extract_mode = "markdown",
|
|
1799
|
+
max_chars: rawMaxChars = 2e4,
|
|
1800
|
+
timeout_ms: rawTimeout = 2e4
|
|
1801
|
+
} = params;
|
|
1802
|
+
const maxChars = Math.min(Math.max(rawMaxChars, 1), 1e5);
|
|
1803
|
+
const timeoutMs = Math.min(Math.max(rawTimeout, 1e3), 6e4);
|
|
1804
|
+
const validation = validateWebFetchUrl(url);
|
|
1805
|
+
if (!validation.ok) {
|
|
1806
|
+
return `Error fetching ${url}: ${validation.error}`;
|
|
1807
|
+
}
|
|
1808
|
+
const cacheKey = `${url}:${extract_mode}:${maxChars}`;
|
|
1809
|
+
const cached = cacheGet(cacheKey);
|
|
1810
|
+
if (cached !== void 0) return cached;
|
|
1811
|
+
const controller = new AbortController();
|
|
1812
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1813
|
+
let response;
|
|
1814
|
+
let finalUrl;
|
|
1815
|
+
try {
|
|
1816
|
+
({ response, finalUrl } = await fetchWithRedirects(url, timeoutMs, controller.signal));
|
|
1659
1817
|
} catch (err) {
|
|
1818
|
+
clearTimeout(timer);
|
|
1819
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1820
|
+
return `Error fetching ${url}: request timed out after ${timeoutMs}ms`;
|
|
1821
|
+
}
|
|
1660
1822
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1661
|
-
return
|
|
1662
|
-
`description: ${params.description}`,
|
|
1663
|
-
logger ? `log_file: ${logger.filePath}` : void 0,
|
|
1664
|
-
"",
|
|
1665
|
-
"<task_result>",
|
|
1666
|
-
`Subagent failed: ${msg}`,
|
|
1667
|
-
"</task_result>"
|
|
1668
|
-
].filter((line) => line !== void 0).join("\n");
|
|
1823
|
+
return `Error fetching ${url}: ${msg}`;
|
|
1669
1824
|
} finally {
|
|
1670
|
-
|
|
1825
|
+
clearTimeout(timer);
|
|
1671
1826
|
}
|
|
1827
|
+
if (response.url) {
|
|
1828
|
+
let resolvedHost;
|
|
1829
|
+
try {
|
|
1830
|
+
resolvedHost = new URL(response.url).hostname;
|
|
1831
|
+
} catch {
|
|
1832
|
+
resolvedHost = validation.parsed.hostname;
|
|
1833
|
+
}
|
|
1834
|
+
if (!isSameOrWwwHost(validation.parsed.hostname, resolvedHost)) {
|
|
1835
|
+
return `Error fetching ${url}: redirect to different host is not allowed automatically (-> ${response.url})`;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const status = response.status;
|
|
1839
|
+
const contentTypeHeader = response.headers.get("content-type") ?? "";
|
|
1840
|
+
const mimeType = contentTypeHeader.split(";")[0].trim().toLowerCase();
|
|
1841
|
+
if (status >= 400) {
|
|
1842
|
+
return `Error fetching ${url}: HTTP ${status} ${statusText(status)}`;
|
|
1843
|
+
}
|
|
1844
|
+
const supportedTypes = ["text/html", "text/plain", "text/markdown", "application/xhtml+xml"];
|
|
1845
|
+
if (mimeType && !supportedTypes.includes(mimeType)) {
|
|
1846
|
+
return `Error fetching ${url}: unsupported content-type ${mimeType}`;
|
|
1847
|
+
}
|
|
1848
|
+
let body;
|
|
1849
|
+
try {
|
|
1850
|
+
body = await response.text();
|
|
1851
|
+
} catch (err) {
|
|
1852
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1853
|
+
return `Error fetching ${url}: failed to read response body: ${msg}`;
|
|
1854
|
+
}
|
|
1855
|
+
let extracted;
|
|
1856
|
+
if (mimeType === "text/plain" || mimeType === "text/markdown") {
|
|
1857
|
+
extracted = body;
|
|
1858
|
+
} else {
|
|
1859
|
+
extracted = extractHtmlText(body, extract_mode);
|
|
1860
|
+
}
|
|
1861
|
+
const truncated = extracted.length > maxChars;
|
|
1862
|
+
const content = truncated ? extracted.slice(0, maxChars) : extracted;
|
|
1863
|
+
const result = [
|
|
1864
|
+
`URL: ${url}`,
|
|
1865
|
+
`Final-URL: ${finalUrl}`,
|
|
1866
|
+
`Status: ${status} ${statusText(status)}`,
|
|
1867
|
+
`Content-Type: ${contentTypeHeader || "(none)"}`,
|
|
1868
|
+
`Bytes: ${body.length}`,
|
|
1869
|
+
`Extract-Mode: ${extract_mode}`,
|
|
1870
|
+
`Truncated: ${truncated ? "yes" : "no"}`,
|
|
1871
|
+
"",
|
|
1872
|
+
"<content>",
|
|
1873
|
+
content,
|
|
1874
|
+
"</content>"
|
|
1875
|
+
].join("\n");
|
|
1876
|
+
cacheSet(cacheKey, result);
|
|
1877
|
+
return result;
|
|
1672
1878
|
}
|
|
1673
1879
|
|
|
1674
1880
|
// src/repl.ts
|
|
1675
|
-
var
|
|
1676
|
-
var
|
|
1881
|
+
var SKIP_MESSAGE = "Command skipped by user.";
|
|
1882
|
+
var BASH_TOOL = {
|
|
1677
1883
|
type: "function",
|
|
1678
1884
|
function: {
|
|
1679
1885
|
name: "bash",
|
|
@@ -1694,16 +1900,16 @@ async function handleBashTool(command, deps) {
|
|
|
1694
1900
|
if (!autoApproveNormal) {
|
|
1695
1901
|
const ok = await confirm(`Execute command: ${command}
|
|
1696
1902
|
Proceed? (y/n) `);
|
|
1697
|
-
if (!ok) return
|
|
1903
|
+
if (!ok) return SKIP_MESSAGE;
|
|
1698
1904
|
}
|
|
1699
1905
|
} else if (cls === "danger") {
|
|
1700
1906
|
print(`\u26A0\uFE0F DANGEROUS COMMAND: ${command}`);
|
|
1701
1907
|
const first = await confirm("Are you sure? (y/n) ");
|
|
1702
|
-
if (!first) return
|
|
1908
|
+
if (!first) return SKIP_MESSAGE;
|
|
1703
1909
|
const second = await confirm(
|
|
1704
1910
|
"Confirm again \u2014 this is destructive. Continue? (y/n) "
|
|
1705
1911
|
);
|
|
1706
|
-
if (!second) return
|
|
1912
|
+
if (!second) return SKIP_MESSAGE;
|
|
1707
1913
|
}
|
|
1708
1914
|
const result = await deps.executeBash(command);
|
|
1709
1915
|
let output = "";
|
|
@@ -1730,12 +1936,12 @@ function parseSkillCommand(input) {
|
|
|
1730
1936
|
}
|
|
1731
1937
|
|
|
1732
1938
|
// src/skills/handler.ts
|
|
1733
|
-
function handleSkillInput(input,
|
|
1939
|
+
function handleSkillInput(input, registry) {
|
|
1734
1940
|
if (!isSkillCommand(input)) return { type: "passthrough" };
|
|
1735
1941
|
const { name, args } = parseSkillCommand(input);
|
|
1736
|
-
const skill =
|
|
1942
|
+
const skill = registry.find(name);
|
|
1737
1943
|
if (!skill) {
|
|
1738
|
-
const available =
|
|
1944
|
+
const available = registry.list().map((s) => s.name).join(", ") || "none";
|
|
1739
1945
|
return {
|
|
1740
1946
|
type: "error",
|
|
1741
1947
|
message: `Unknown skill: /${name}. Available: ${available}`
|
|
@@ -1754,7 +1960,7 @@ import { promisify } from "util";
|
|
|
1754
1960
|
|
|
1755
1961
|
// src/skills/loader.ts
|
|
1756
1962
|
import { readFile as readFile6, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
1757
|
-
import { join as join6, dirname as dirname4, basename as
|
|
1963
|
+
import { join as join6, dirname as dirname4, basename as basename3, resolve as resolve3, sep as sep3 } from "path";
|
|
1758
1964
|
function parseFrontmatter(content) {
|
|
1759
1965
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
1760
1966
|
const match = FRONTMATTER_RE.exec(content);
|
|
@@ -1827,7 +2033,7 @@ async function loadSkillFile(skillMdPath) {
|
|
|
1827
2033
|
const content = await readFile6(skillMdPath, "utf-8");
|
|
1828
2034
|
const { data, body } = parseFrontmatter(content);
|
|
1829
2035
|
const skillDir = dirname4(skillMdPath);
|
|
1830
|
-
const dirName =
|
|
2036
|
+
const dirName = basename3(skillDir);
|
|
1831
2037
|
const [tools, hasPreScript, hasPostScript] = await Promise.all([
|
|
1832
2038
|
loadTools(skillDir),
|
|
1833
2039
|
fileExists(join6(skillDir, "pre.sh")),
|
|
@@ -2446,7 +2652,7 @@ function dismiss(state) {
|
|
|
2446
2652
|
|
|
2447
2653
|
// src/ui/fileCompletion.ts
|
|
2448
2654
|
import * as fs9 from "fs/promises";
|
|
2449
|
-
import * as
|
|
2655
|
+
import * as path8 from "path";
|
|
2450
2656
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
2451
2657
|
function isHidden(name) {
|
|
2452
2658
|
return name.startsWith(".");
|
|
@@ -2462,17 +2668,17 @@ async function walkDir2(dir, root, results, maxResults) {
|
|
|
2462
2668
|
for (const entry of entries) {
|
|
2463
2669
|
if (results.length >= maxResults) return;
|
|
2464
2670
|
if (SKIP_DIRS.has(entry.name) || isHidden(entry.name)) continue;
|
|
2465
|
-
const relPath =
|
|
2671
|
+
const relPath = path8.relative(root, path8.join(dir, entry.name));
|
|
2466
2672
|
if (entry.isDirectory()) {
|
|
2467
2673
|
results.push({ path: relPath, isDir: true });
|
|
2468
|
-
await walkDir2(
|
|
2674
|
+
await walkDir2(path8.join(dir, entry.name), root, results, maxResults);
|
|
2469
2675
|
} else {
|
|
2470
2676
|
results.push({ path: relPath, isDir: false });
|
|
2471
2677
|
}
|
|
2472
2678
|
}
|
|
2473
2679
|
}
|
|
2474
2680
|
async function listFilesForQuery(query, cwd, maxResults = 50) {
|
|
2475
|
-
if (
|
|
2681
|
+
if (path8.isAbsolute(query)) {
|
|
2476
2682
|
return listAbsolute(query, maxResults);
|
|
2477
2683
|
}
|
|
2478
2684
|
const all = [];
|
|
@@ -2486,12 +2692,12 @@ async function listAbsolute(query, maxResults) {
|
|
|
2486
2692
|
try {
|
|
2487
2693
|
const stat5 = await fs9.stat(dir);
|
|
2488
2694
|
if (!stat5.isDirectory()) {
|
|
2489
|
-
filter =
|
|
2490
|
-
dir =
|
|
2695
|
+
filter = path8.basename(dir);
|
|
2696
|
+
dir = path8.dirname(dir);
|
|
2491
2697
|
}
|
|
2492
2698
|
} catch {
|
|
2493
|
-
filter =
|
|
2494
|
-
dir =
|
|
2699
|
+
filter = path8.basename(dir);
|
|
2700
|
+
dir = path8.dirname(dir);
|
|
2495
2701
|
}
|
|
2496
2702
|
let entries;
|
|
2497
2703
|
try {
|
|
@@ -2504,7 +2710,7 @@ async function listAbsolute(query, maxResults) {
|
|
|
2504
2710
|
if (SKIP_DIRS.has(entry.name) || isHidden(entry.name)) continue;
|
|
2505
2711
|
if (filter && !entry.name.includes(filter)) continue;
|
|
2506
2712
|
results.push({
|
|
2507
|
-
path:
|
|
2713
|
+
path: path8.join(dir, entry.name),
|
|
2508
2714
|
isDir: entry.isDirectory()
|
|
2509
2715
|
});
|
|
2510
2716
|
if (results.length >= maxResults) break;
|
|
@@ -2526,7 +2732,7 @@ async function expandFileRefs(text, cwd) {
|
|
|
2526
2732
|
atPattern.lastIndex = 0;
|
|
2527
2733
|
while ((match = atPattern.exec(text)) !== null) {
|
|
2528
2734
|
const filePath = match[1];
|
|
2529
|
-
const fullPath =
|
|
2735
|
+
const fullPath = path8.isAbsolute(filePath) ? filePath : path8.join(cwd, filePath);
|
|
2530
2736
|
let replacement;
|
|
2531
2737
|
try {
|
|
2532
2738
|
const content = await fs9.readFile(fullPath, "utf8");
|
|
@@ -2648,7 +2854,7 @@ async function removeJob(dataDir, id) {
|
|
|
2648
2854
|
}
|
|
2649
2855
|
|
|
2650
2856
|
// src/automation/runtime.ts
|
|
2651
|
-
import { randomUUID as
|
|
2857
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2652
2858
|
|
|
2653
2859
|
// src/automation/log.ts
|
|
2654
2860
|
import { readFile as readFile9, appendFile, mkdir as mkdir3 } from "fs/promises";
|
|
@@ -2667,7 +2873,7 @@ async function executeJob(job, config2) {
|
|
|
2667
2873
|
return null;
|
|
2668
2874
|
}
|
|
2669
2875
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2670
|
-
const runId =
|
|
2876
|
+
const runId = randomUUID3();
|
|
2671
2877
|
let summaryText;
|
|
2672
2878
|
let errorMsg;
|
|
2673
2879
|
let result;
|
|
@@ -2874,7 +3080,7 @@ var LoopScheduler = class {
|
|
|
2874
3080
|
};
|
|
2875
3081
|
|
|
2876
3082
|
// src/automation/loop/command.ts
|
|
2877
|
-
import { randomUUID as
|
|
3083
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2878
3084
|
|
|
2879
3085
|
// src/automation/loop/parse.ts
|
|
2880
3086
|
var DEFAULT_INTERVAL_MS = 6e5;
|
|
@@ -2965,7 +3171,7 @@ async function cmdLoop(args, deps) {
|
|
|
2965
3171
|
}
|
|
2966
3172
|
const now = /* @__PURE__ */ new Date();
|
|
2967
3173
|
const job = {
|
|
2968
|
-
id:
|
|
3174
|
+
id: randomUUID4(),
|
|
2969
3175
|
kind: "loop",
|
|
2970
3176
|
title: prompt.slice(0, 60),
|
|
2971
3177
|
createdAt: now.toISOString(),
|
|
@@ -3009,7 +3215,7 @@ async function cmdUnloop(idOrPrefix, deps) {
|
|
|
3009
3215
|
}
|
|
3010
3216
|
|
|
3011
3217
|
// src/automation/goal/command.ts
|
|
3012
|
-
import { randomUUID as
|
|
3218
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3013
3219
|
async function cmdGoal(args, deps) {
|
|
3014
3220
|
const condition = args.trim();
|
|
3015
3221
|
if (!condition) {
|
|
@@ -3017,7 +3223,7 @@ async function cmdGoal(args, deps) {
|
|
|
3017
3223
|
}
|
|
3018
3224
|
const now = /* @__PURE__ */ new Date();
|
|
3019
3225
|
const job = {
|
|
3020
|
-
id:
|
|
3226
|
+
id: randomUUID5(),
|
|
3021
3227
|
kind: "goal",
|
|
3022
3228
|
title: condition.slice(0, 60),
|
|
3023
3229
|
createdAt: now.toISOString(),
|
|
@@ -3271,7 +3477,7 @@ var AutomationManager = class {
|
|
|
3271
3477
|
|
|
3272
3478
|
// src/meta_skill/index.ts
|
|
3273
3479
|
import * as fs11 from "fs";
|
|
3274
|
-
import * as
|
|
3480
|
+
import * as path10 from "path";
|
|
3275
3481
|
import { fileURLToPath } from "url";
|
|
3276
3482
|
|
|
3277
3483
|
// src/meta_skill/task-classifier.ts
|
|
@@ -3982,23 +4188,23 @@ function materialize(policy, profile, input) {
|
|
|
3982
4188
|
|
|
3983
4189
|
// src/meta_skill/learning-store.ts
|
|
3984
4190
|
import * as fs10 from "fs";
|
|
3985
|
-
import * as
|
|
4191
|
+
import * as path9 from "path";
|
|
3986
4192
|
import * as os from "os";
|
|
3987
|
-
var STORE_PATH =
|
|
4193
|
+
var STORE_PATH = path9.resolve(os.homedir(), ".ecode", "learning-records.jsonl");
|
|
3988
4194
|
|
|
3989
4195
|
// src/meta_skill/index.ts
|
|
3990
4196
|
function resolveBuiltinSkillsDir() {
|
|
3991
4197
|
try {
|
|
3992
|
-
let dir =
|
|
4198
|
+
let dir = path10.dirname(fileURLToPath(import.meta.url));
|
|
3993
4199
|
for (let i = 0; i < 4; i++) {
|
|
3994
|
-
const candidate =
|
|
4200
|
+
const candidate = path10.join(dir, "skills");
|
|
3995
4201
|
if (fs11.existsSync(candidate) && fs11.statSync(candidate).isDirectory()) {
|
|
3996
4202
|
const hasSkillContent = fs11.readdirSync(candidate).some(
|
|
3997
|
-
(entry) => fs11.existsSync(
|
|
4203
|
+
(entry) => fs11.existsSync(path10.join(candidate, entry, "SKILL.md"))
|
|
3998
4204
|
);
|
|
3999
4205
|
if (hasSkillContent) return candidate;
|
|
4000
4206
|
}
|
|
4001
|
-
const parent =
|
|
4207
|
+
const parent = path10.dirname(dir);
|
|
4002
4208
|
if (parent === dir) break;
|
|
4003
4209
|
dir = parent;
|
|
4004
4210
|
}
|
|
@@ -4009,16 +4215,16 @@ function resolveBuiltinSkillsDir() {
|
|
|
4009
4215
|
function runMetaAlign(input) {
|
|
4010
4216
|
const taskType = input.task.type && isValidTaskType(input.task.type) ? input.task.type : classifyTask(input.task.goal);
|
|
4011
4217
|
const inheritedSkills = getBaselineSkills(taskType);
|
|
4012
|
-
const
|
|
4218
|
+
const builtinSkillsDir = resolveBuiltinSkillsDir();
|
|
4013
4219
|
const allSteps = [];
|
|
4014
4220
|
for (const skillName of inheritedSkills) {
|
|
4015
4221
|
const candidates = [];
|
|
4016
|
-
if (
|
|
4017
|
-
candidates.push(
|
|
4018
|
-
candidates.push(
|
|
4222
|
+
if (builtinSkillsDir) {
|
|
4223
|
+
candidates.push(path10.join(builtinSkillsDir, skillName, "SKILL.md"));
|
|
4224
|
+
candidates.push(path10.join(builtinSkillsDir, skillName + ".md"));
|
|
4019
4225
|
}
|
|
4020
|
-
candidates.push(
|
|
4021
|
-
candidates.push(
|
|
4226
|
+
candidates.push(path10.join(process.cwd(), "skills", skillName, "SKILL.md"));
|
|
4227
|
+
candidates.push(path10.join(process.cwd(), "skills", skillName + ".md"));
|
|
4022
4228
|
let body = "";
|
|
4023
4229
|
for (const p of candidates) {
|
|
4024
4230
|
if (fs11.existsSync(p)) {
|
|
@@ -4179,11 +4385,11 @@ function formatMetaAlignOutput(result, model) {
|
|
|
4179
4385
|
);
|
|
4180
4386
|
return lines.join("\n");
|
|
4181
4387
|
}
|
|
4182
|
-
function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry
|
|
4388
|
+
function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry, trustedSkillDirs = [], initialMessages = [], llmClient }) {
|
|
4183
4389
|
const { stdout } = useStdout();
|
|
4184
4390
|
const { stdin } = useStdin();
|
|
4185
4391
|
const historyMaxHeight = Math.max(5, (stdout?.rows ?? 24) - 4);
|
|
4186
|
-
const [messages, setMessages] = useState3(
|
|
4392
|
+
const [messages, setMessages] = useState3(initialMessages);
|
|
4187
4393
|
const [status, setStatus] = useState3("idle");
|
|
4188
4394
|
const contextLimit = getContextLimit(config2.model, config2.contextLimit);
|
|
4189
4395
|
const [tokenUsage, setTokenUsage] = useState3({
|
|
@@ -4347,7 +4553,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
4347
4553
|
return;
|
|
4348
4554
|
}
|
|
4349
4555
|
}
|
|
4350
|
-
const skillList =
|
|
4556
|
+
const skillList = registry?.list() ?? [];
|
|
4351
4557
|
const suggestions = computeSuggestions(skillList, acState);
|
|
4352
4558
|
const open = isOpen(acState, suggestions);
|
|
4353
4559
|
if (open) {
|
|
@@ -4452,7 +4658,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
4452
4658
|
function: { name: t.name, description: t.description, parameters: t.parameters }
|
|
4453
4659
|
}));
|
|
4454
4660
|
try {
|
|
4455
|
-
for await (const chunk of llmRef.current.stream(currentMessages, [
|
|
4661
|
+
for await (const chunk of llmRef.current.stream(currentMessages, [BASH_TOOL, READ_TOOL, WRITE_TOOL, EDIT_TOOL, GLOB_TOOL, GREP_TOOL, APPLY_PATCH_TOOL, TODO_TOOL, TASK_TOOL, WEB_FETCH_TOOL, ...dynamicTools], abortController.signal)) {
|
|
4456
4662
|
if (chunk.text) {
|
|
4457
4663
|
assistantText += chunk.text;
|
|
4458
4664
|
setMessages((prev) => {
|
|
@@ -4555,11 +4761,14 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
4555
4761
|
toolResult = todo(args);
|
|
4556
4762
|
} else if (tc.name === "task") {
|
|
4557
4763
|
const args = JSON.parse(tc.arguments);
|
|
4558
|
-
toolResult = await
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4764
|
+
toolResult = await handleTaskTool(args, {
|
|
4765
|
+
provider: llmRef.current,
|
|
4766
|
+
print: (text) => process.stdout.write(text),
|
|
4767
|
+
logDir: config2.logDir
|
|
4562
4768
|
});
|
|
4769
|
+
} else if (tc.name === "web_fetch") {
|
|
4770
|
+
const args = JSON.parse(tc.arguments);
|
|
4771
|
+
toolResult = await webFetch(args);
|
|
4563
4772
|
} else {
|
|
4564
4773
|
const skillTool = skillToolsRef.current.find((t) => t.name === tc.name);
|
|
4565
4774
|
if (skillTool) {
|
|
@@ -4568,7 +4777,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
4568
4777
|
toolArgs = JSON.parse(tc.arguments);
|
|
4569
4778
|
} catch {
|
|
4570
4779
|
}
|
|
4571
|
-
toolResult = await executeSkillTool(skillTool, toolArgs,
|
|
4780
|
+
toolResult = await executeSkillTool(skillTool, toolArgs, trustedSkillDirs);
|
|
4572
4781
|
} else {
|
|
4573
4782
|
toolResult = `Unknown tool: ${tc.name}`;
|
|
4574
4783
|
}
|
|
@@ -4670,8 +4879,8 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
4670
4879
|
return;
|
|
4671
4880
|
}
|
|
4672
4881
|
}
|
|
4673
|
-
if (
|
|
4674
|
-
const skillResult = handleSkillInput(trimmed,
|
|
4882
|
+
if (registry) {
|
|
4883
|
+
const skillResult = handleSkillInput(trimmed, registry);
|
|
4675
4884
|
if (skillResult.type === "error") {
|
|
4676
4885
|
setMessages((prev) => [
|
|
4677
4886
|
...prev,
|
|
@@ -4684,7 +4893,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
4684
4893
|
setSkillTools(skill.tools);
|
|
4685
4894
|
if (skill.preScript) {
|
|
4686
4895
|
try {
|
|
4687
|
-
await executePreScript(skill,
|
|
4896
|
+
await executePreScript(skill, trustedSkillDirs);
|
|
4688
4897
|
} catch (preErr) {
|
|
4689
4898
|
setMessages((prev) => [
|
|
4690
4899
|
...prev,
|
|
@@ -4741,7 +4950,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
|
|
|
4741
4950
|
setFileAcState((prev) => handleInputChange2(prev, text));
|
|
4742
4951
|
}
|
|
4743
4952
|
}, [status]);
|
|
4744
|
-
const skillSuggestions =
|
|
4953
|
+
const skillSuggestions = registry ? computeSuggestions(registry.list(), acState) : [];
|
|
4745
4954
|
const acOpen = isOpen(acState, skillSuggestions);
|
|
4746
4955
|
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: "100%", children: [
|
|
4747
4956
|
/* @__PURE__ */ jsx6(Box6, { flexGrow: 1, flexDirection: "column", justifyContent: "flex-end", children: /* @__PURE__ */ jsx6(
|
|
@@ -4876,7 +5085,7 @@ var SkillRegistry = class {
|
|
|
4876
5085
|
|
|
4877
5086
|
// src/pipe.ts
|
|
4878
5087
|
var PIPE_SYSTEM_PROMPT = "You are a helpful assistant running in headless pipe mode. You have access to read-only file tools (read, glob, grep). Answer concisely and accurately. Follow the user's requested language, format, and length exactly. Do not reveal chain-of-thought or emit raw <think> / </think> tags in the final answer; if you must refer to them, describe them in plain language instead.";
|
|
4879
|
-
var PIPE_TOOLS = [READ_TOOL, GLOB_TOOL, GREP_TOOL];
|
|
5088
|
+
var PIPE_TOOLS = [READ_TOOL, GLOB_TOOL, GREP_TOOL, WEB_FETCH_TOOL];
|
|
4880
5089
|
function emit(out, event) {
|
|
4881
5090
|
out.write(JSON.stringify(event) + "\n");
|
|
4882
5091
|
}
|
|
@@ -4893,6 +5102,10 @@ async function executeToolCall(name, args) {
|
|
|
4893
5102
|
const parsed = JSON.parse(args);
|
|
4894
5103
|
return grepFiles(parsed);
|
|
4895
5104
|
}
|
|
5105
|
+
if (name === "web_fetch") {
|
|
5106
|
+
const parsed = JSON.parse(args);
|
|
5107
|
+
return webFetch(parsed);
|
|
5108
|
+
}
|
|
4896
5109
|
return `Unknown tool: ${name}`;
|
|
4897
5110
|
}
|
|
4898
5111
|
async function runPipe(prompt, llm, out = process.stdout, systemPrompt) {
|
|
@@ -4963,7 +5176,7 @@ function readStdin() {
|
|
|
4963
5176
|
}
|
|
4964
5177
|
|
|
4965
5178
|
// src/sessions/command.ts
|
|
4966
|
-
import * as
|
|
5179
|
+
import * as path11 from "path";
|
|
4967
5180
|
function cmdSessionsList(logDir) {
|
|
4968
5181
|
const sessions = listSessions(logDir);
|
|
4969
5182
|
if (sessions.length === 0) return "(no sessions found)";
|
|
@@ -4985,13 +5198,13 @@ function cmdSessionsInspect(logDir, idOrPrefix) {
|
|
|
4985
5198
|
function cmdSessionsReplay(logDir, idOrPrefix) {
|
|
4986
5199
|
const session = findSession(logDir, idOrPrefix);
|
|
4987
5200
|
if (!session) return `Session not found: ${idOrPrefix}`;
|
|
4988
|
-
const logPath =
|
|
5201
|
+
const logPath = path11.join(logDir, session.logFile);
|
|
4989
5202
|
return `ecode --replay ${logPath}`;
|
|
4990
5203
|
}
|
|
4991
5204
|
function cmdSessionsFork(logDir, idOrPrefix, turn) {
|
|
4992
5205
|
const session = findSession(logDir, idOrPrefix);
|
|
4993
5206
|
if (!session) return `Session not found: ${idOrPrefix}`;
|
|
4994
|
-
const logPath =
|
|
5207
|
+
const logPath = path11.join(logDir, session.logFile);
|
|
4995
5208
|
const turnStr = turn !== void 0 ? turn : session.turnCount;
|
|
4996
5209
|
return `ecode --fork ${logPath}:${turnStr}`;
|
|
4997
5210
|
}
|
|
@@ -5005,10 +5218,718 @@ function formatInspect(session, logDir) {
|
|
|
5005
5218
|
`last active: ${new Date(session.lastActivity).toLocaleString()}`,
|
|
5006
5219
|
`turns: ${session.turnCount}`,
|
|
5007
5220
|
`tokens: ${session.totalTokens.toLocaleString()}`,
|
|
5008
|
-
`log file: ${
|
|
5221
|
+
`log file: ${path11.join(logDir, session.logFile)}`
|
|
5009
5222
|
].join("\n");
|
|
5010
5223
|
}
|
|
5011
5224
|
|
|
5225
|
+
// src/web/server.ts
|
|
5226
|
+
import Fastify from "fastify";
|
|
5227
|
+
import cors from "@fastify/cors";
|
|
5228
|
+
import websocket from "@fastify/websocket";
|
|
5229
|
+
|
|
5230
|
+
// src/web/auth.ts
|
|
5231
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
5232
|
+
function generateAccessToken() {
|
|
5233
|
+
return randomUUID6();
|
|
5234
|
+
}
|
|
5235
|
+
function createAuthHook(token) {
|
|
5236
|
+
return function authHook(request, reply, done) {
|
|
5237
|
+
const incoming = request.headers["x-ecode-token"];
|
|
5238
|
+
if (incoming !== token) {
|
|
5239
|
+
reply.code(401).send({ success: false, error: "Unauthorized" });
|
|
5240
|
+
return;
|
|
5241
|
+
}
|
|
5242
|
+
done();
|
|
5243
|
+
};
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
// src/web/routes/status.ts
|
|
5247
|
+
async function statusRoutes(app, opts) {
|
|
5248
|
+
app.get(
|
|
5249
|
+
"/api/status",
|
|
5250
|
+
async (_request, _reply) => {
|
|
5251
|
+
const runningSessions = opts.manager.listRunning().length;
|
|
5252
|
+
return {
|
|
5253
|
+
version: opts.version,
|
|
5254
|
+
cwd: process.cwd(),
|
|
5255
|
+
model: opts.config.model,
|
|
5256
|
+
logDir: opts.config.logDir,
|
|
5257
|
+
defaultProvider: opts.config.defaultProvider,
|
|
5258
|
+
runningSessions
|
|
5259
|
+
};
|
|
5260
|
+
}
|
|
5261
|
+
);
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5264
|
+
// src/web/routes/sessions.ts
|
|
5265
|
+
import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
|
|
5266
|
+
import { join as join13 } from "path";
|
|
5267
|
+
async function sessionsRoutes(app, opts) {
|
|
5268
|
+
app.get("/api/sessions", async (_request, reply) => {
|
|
5269
|
+
if (!opts.config.logDir) {
|
|
5270
|
+
return reply.send([]);
|
|
5271
|
+
}
|
|
5272
|
+
return listSessions(opts.config.logDir);
|
|
5273
|
+
});
|
|
5274
|
+
app.get(
|
|
5275
|
+
"/api/sessions/:id/messages",
|
|
5276
|
+
async (request, reply) => {
|
|
5277
|
+
const { id } = request.params;
|
|
5278
|
+
if (!opts.config.logDir) {
|
|
5279
|
+
return reply.code(404).send({ success: false, error: "Log directory not configured" });
|
|
5280
|
+
}
|
|
5281
|
+
const session = findSession(opts.config.logDir, id);
|
|
5282
|
+
if (!session) {
|
|
5283
|
+
return reply.code(404).send({ success: false, error: `Session not found: ${id}` });
|
|
5284
|
+
}
|
|
5285
|
+
const logFilePath2 = join13(opts.config.logDir, session.logFile);
|
|
5286
|
+
if (!existsSync4(logFilePath2)) {
|
|
5287
|
+
return reply.code(404).send({ success: false, error: `Log file not found: ${session.logFile}` });
|
|
5288
|
+
}
|
|
5289
|
+
try {
|
|
5290
|
+
const content = readFileSync5(logFilePath2, "utf-8");
|
|
5291
|
+
return reply.header("Content-Type", "text/plain; charset=utf-8").send(content);
|
|
5292
|
+
} catch (_err) {
|
|
5293
|
+
return reply.code(404).send({ success: false, error: `Log file not found: ${session.logFile}` });
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
);
|
|
5297
|
+
app.get(
|
|
5298
|
+
"/api/sessions/:id/replay-command",
|
|
5299
|
+
async (request, reply) => {
|
|
5300
|
+
const { id } = request.params;
|
|
5301
|
+
if (!opts.config.logDir) {
|
|
5302
|
+
return reply.code(404).send({ success: false, error: "Log directory not configured" });
|
|
5303
|
+
}
|
|
5304
|
+
const logDir = opts.config.logDir;
|
|
5305
|
+
const session = findSession(logDir, id);
|
|
5306
|
+
if (!session) {
|
|
5307
|
+
return reply.code(404).send({ success: false, error: `Session not found: ${id}` });
|
|
5308
|
+
}
|
|
5309
|
+
const command = cmdSessionsReplay(logDir, id);
|
|
5310
|
+
return reply.send({ success: true, command });
|
|
5311
|
+
}
|
|
5312
|
+
);
|
|
5313
|
+
app.get(
|
|
5314
|
+
"/api/sessions/:id/fork-command",
|
|
5315
|
+
async (request, reply) => {
|
|
5316
|
+
const { id } = request.params;
|
|
5317
|
+
if (!opts.config.logDir) {
|
|
5318
|
+
return reply.code(404).send({ success: false, error: "Log directory not configured" });
|
|
5319
|
+
}
|
|
5320
|
+
const logDir = opts.config.logDir;
|
|
5321
|
+
const session = findSession(logDir, id);
|
|
5322
|
+
if (!session) {
|
|
5323
|
+
return reply.code(404).send({ success: false, error: `Session not found: ${id}` });
|
|
5324
|
+
}
|
|
5325
|
+
let turn;
|
|
5326
|
+
if (request.query.turn !== void 0) {
|
|
5327
|
+
const parsed = parseInt(request.query.turn, 10);
|
|
5328
|
+
if (isNaN(parsed)) {
|
|
5329
|
+
return reply.code(400).send({ success: false, error: `Invalid turn value: ${request.query.turn}` });
|
|
5330
|
+
}
|
|
5331
|
+
turn = parsed;
|
|
5332
|
+
}
|
|
5333
|
+
const command = cmdSessionsFork(logDir, id, turn);
|
|
5334
|
+
return reply.send({ success: true, command });
|
|
5335
|
+
}
|
|
5336
|
+
);
|
|
5337
|
+
}
|
|
5338
|
+
|
|
5339
|
+
// src/web/routes/config.ts
|
|
5340
|
+
function toSafeConfig(cfg) {
|
|
5341
|
+
const { apiKey: _stripped, ...safe } = cfg;
|
|
5342
|
+
return safe;
|
|
5343
|
+
}
|
|
5344
|
+
async function configRoutes(app, opts) {
|
|
5345
|
+
app.get("/api/config", async (_request, _reply) => {
|
|
5346
|
+
const { apiKey: _stripped, ...safeConfig } = opts.config;
|
|
5347
|
+
return safeConfig;
|
|
5348
|
+
});
|
|
5349
|
+
app.put(
|
|
5350
|
+
"/api/config",
|
|
5351
|
+
async (request, reply) => {
|
|
5352
|
+
const body = request.body;
|
|
5353
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
5354
|
+
return reply.status(400).send(toSafeConfig(opts.config));
|
|
5355
|
+
}
|
|
5356
|
+
const merged = { ...opts.config, ...body };
|
|
5357
|
+
opts.save?.(merged);
|
|
5358
|
+
Object.assign(opts.config, merged);
|
|
5359
|
+
return toSafeConfig(merged);
|
|
5360
|
+
}
|
|
5361
|
+
);
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
// src/web/routes/automation.ts
|
|
5365
|
+
async function automationRoutes(app, opts) {
|
|
5366
|
+
app.get(
|
|
5367
|
+
"/api/automation/jobs",
|
|
5368
|
+
async (_request, reply) => {
|
|
5369
|
+
if (!opts.config.logDir) {
|
|
5370
|
+
return reply.send([]);
|
|
5371
|
+
}
|
|
5372
|
+
const jobs = await loadJobs(opts.config.logDir);
|
|
5373
|
+
return jobs;
|
|
5374
|
+
}
|
|
5375
|
+
);
|
|
5376
|
+
app.post(
|
|
5377
|
+
"/api/automation/jobs/:id/pause",
|
|
5378
|
+
async (request, reply) => {
|
|
5379
|
+
const logDir = opts.config.logDir;
|
|
5380
|
+
if (!logDir) {
|
|
5381
|
+
return reply.status(404).send({ error: "logDir not configured" });
|
|
5382
|
+
}
|
|
5383
|
+
const { id } = request.params;
|
|
5384
|
+
const jobs = await loadJobs(logDir);
|
|
5385
|
+
const job = jobs.find((j) => j.id === id);
|
|
5386
|
+
if (!job) {
|
|
5387
|
+
return reply.status(404).send({ error: "job not found" });
|
|
5388
|
+
}
|
|
5389
|
+
if (job.state === "paused") {
|
|
5390
|
+
return reply.status(409).send({ error: "job is already paused" });
|
|
5391
|
+
}
|
|
5392
|
+
const updated = {
|
|
5393
|
+
...job,
|
|
5394
|
+
state: "paused",
|
|
5395
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5396
|
+
};
|
|
5397
|
+
await upsertJob(logDir, updated);
|
|
5398
|
+
return reply.status(200).send({ success: true });
|
|
5399
|
+
}
|
|
5400
|
+
);
|
|
5401
|
+
app.post(
|
|
5402
|
+
"/api/automation/jobs/:id/resume",
|
|
5403
|
+
async (request, reply) => {
|
|
5404
|
+
const logDir = opts.config.logDir;
|
|
5405
|
+
if (!logDir) {
|
|
5406
|
+
return reply.status(404).send({ error: "logDir not configured" });
|
|
5407
|
+
}
|
|
5408
|
+
const { id } = request.params;
|
|
5409
|
+
const jobs = await loadJobs(logDir);
|
|
5410
|
+
const job = jobs.find((j) => j.id === id);
|
|
5411
|
+
if (!job) {
|
|
5412
|
+
return reply.status(404).send({ error: "job not found" });
|
|
5413
|
+
}
|
|
5414
|
+
if (job.state !== "paused") {
|
|
5415
|
+
return reply.status(409).send({ error: "job is not paused" });
|
|
5416
|
+
}
|
|
5417
|
+
const updated = {
|
|
5418
|
+
...job,
|
|
5419
|
+
state: "scheduled",
|
|
5420
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5421
|
+
};
|
|
5422
|
+
await upsertJob(logDir, updated);
|
|
5423
|
+
return reply.status(200).send({ success: true });
|
|
5424
|
+
}
|
|
5425
|
+
);
|
|
5426
|
+
app.delete(
|
|
5427
|
+
"/api/automation/jobs/:id",
|
|
5428
|
+
async (request, reply) => {
|
|
5429
|
+
const logDir = opts.config.logDir;
|
|
5430
|
+
if (!logDir) {
|
|
5431
|
+
return reply.status(404).send({ error: "logDir not configured" });
|
|
5432
|
+
}
|
|
5433
|
+
const { id } = request.params;
|
|
5434
|
+
const removed = await removeJob(logDir, id);
|
|
5435
|
+
if (!removed) {
|
|
5436
|
+
return reply.status(404).send({ error: "job not found" });
|
|
5437
|
+
}
|
|
5438
|
+
return reply.status(200).send({ success: true });
|
|
5439
|
+
}
|
|
5440
|
+
);
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
// src/web/routes/chat.ts
|
|
5444
|
+
var BUSY_STATUSES = /* @__PURE__ */ new Set(["thinking", "tool_calling", "awaiting_confirm"]);
|
|
5445
|
+
async function chatRoutes(app, opts) {
|
|
5446
|
+
const { manager } = opts;
|
|
5447
|
+
app.post("/api/chat/sessions", async (request, reply) => {
|
|
5448
|
+
const body = request.body ?? {};
|
|
5449
|
+
const sessionOpts = {};
|
|
5450
|
+
if (typeof body.systemPrompt === "string") {
|
|
5451
|
+
sessionOpts.systemPrompt = body.systemPrompt;
|
|
5452
|
+
}
|
|
5453
|
+
const runtime = await manager.createSession(sessionOpts);
|
|
5454
|
+
return reply.code(201).send({ success: true, session: runtime.snapshot() });
|
|
5455
|
+
});
|
|
5456
|
+
app.post("/api/chat/sessions/:id/submit", async (request, reply) => {
|
|
5457
|
+
const { id } = request.params;
|
|
5458
|
+
const body = request.body ?? {};
|
|
5459
|
+
if (typeof body.message !== "string" || body.message.trim() === "") {
|
|
5460
|
+
return reply.code(400).send({ success: false, error: "message is required and must be a non-empty string" });
|
|
5461
|
+
}
|
|
5462
|
+
const session = manager.getSession(id);
|
|
5463
|
+
if (!session) {
|
|
5464
|
+
return reply.code(404).send({ success: false, error: `Session not found: ${id}` });
|
|
5465
|
+
}
|
|
5466
|
+
if (BUSY_STATUSES.has(session.status)) {
|
|
5467
|
+
return reply.code(409).send({ success: false, error: `Session is busy: ${session.status}` });
|
|
5468
|
+
}
|
|
5469
|
+
session.submit(body.message).catch((err) => {
|
|
5470
|
+
app.log.error({ err, sessionId: id }, "submit \u610F\u5916\u5931\u8D25");
|
|
5471
|
+
});
|
|
5472
|
+
return reply.code(202).send({ success: true, queued: true });
|
|
5473
|
+
});
|
|
5474
|
+
app.post(
|
|
5475
|
+
"/api/chat/sessions/:id/interrupt",
|
|
5476
|
+
async (request, reply) => {
|
|
5477
|
+
const { id } = request.params;
|
|
5478
|
+
const session = manager.getSession(id);
|
|
5479
|
+
if (!session) {
|
|
5480
|
+
return reply.code(404).send({ success: false, error: `Session not found: ${id}` });
|
|
5481
|
+
}
|
|
5482
|
+
session.interrupt();
|
|
5483
|
+
return reply.send({ success: true });
|
|
5484
|
+
}
|
|
5485
|
+
);
|
|
5486
|
+
app.post(
|
|
5487
|
+
"/api/chat/sessions/:id/approve",
|
|
5488
|
+
async (request, reply) => {
|
|
5489
|
+
const { id } = request.params;
|
|
5490
|
+
const body = request.body ?? {};
|
|
5491
|
+
if (typeof body.requestId !== "string") {
|
|
5492
|
+
return reply.code(400).send({ success: false, error: "requestId is required and must be a string" });
|
|
5493
|
+
}
|
|
5494
|
+
if (typeof body.approved !== "boolean") {
|
|
5495
|
+
return reply.code(400).send({ success: false, error: "approved is required and must be a boolean" });
|
|
5496
|
+
}
|
|
5497
|
+
const session = manager.getSession(id);
|
|
5498
|
+
if (!session) {
|
|
5499
|
+
return reply.code(404).send({ success: false, error: `Session not found: ${id}` });
|
|
5500
|
+
}
|
|
5501
|
+
if (session.status !== "awaiting_confirm") {
|
|
5502
|
+
return reply.code(409).send({
|
|
5503
|
+
success: false,
|
|
5504
|
+
error: `Session is not awaiting confirmation: ${session.status}`
|
|
5505
|
+
});
|
|
5506
|
+
}
|
|
5507
|
+
session.approve(body.requestId, body.approved);
|
|
5508
|
+
return reply.send({ success: true });
|
|
5509
|
+
}
|
|
5510
|
+
);
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5513
|
+
// src/web/ws/session-hub.ts
|
|
5514
|
+
async function sessionHubRoutes(app, opts) {
|
|
5515
|
+
app.get(
|
|
5516
|
+
"/api/ws/sessions/:id",
|
|
5517
|
+
{ websocket: true },
|
|
5518
|
+
(socket, request) => {
|
|
5519
|
+
const { id } = request.params;
|
|
5520
|
+
const session = opts.manager.getSession(id);
|
|
5521
|
+
if (!session) {
|
|
5522
|
+
socket.close(4404, "session not found");
|
|
5523
|
+
return;
|
|
5524
|
+
}
|
|
5525
|
+
const unsubscribe = session.subscribe((event) => {
|
|
5526
|
+
if (socket.readyState === socket.OPEN) {
|
|
5527
|
+
socket.send(JSON.stringify(event));
|
|
5528
|
+
}
|
|
5529
|
+
});
|
|
5530
|
+
socket.on("close", () => {
|
|
5531
|
+
unsubscribe();
|
|
5532
|
+
});
|
|
5533
|
+
socket.on("message", (_msg) => {
|
|
5534
|
+
});
|
|
5535
|
+
}
|
|
5536
|
+
);
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
// src/web/server.ts
|
|
5540
|
+
async function buildServer(opts) {
|
|
5541
|
+
const app = Fastify({ logger: false });
|
|
5542
|
+
await app.register(cors, { origin: true });
|
|
5543
|
+
await app.register(websocket);
|
|
5544
|
+
const authHook = createAuthHook(opts.token);
|
|
5545
|
+
app.addHook("preHandler", function(request, reply, done) {
|
|
5546
|
+
if (request.url === "/health") {
|
|
5547
|
+
done();
|
|
5548
|
+
return;
|
|
5549
|
+
}
|
|
5550
|
+
authHook(request, reply, done);
|
|
5551
|
+
});
|
|
5552
|
+
app.get("/health", async () => ({ ok: true }));
|
|
5553
|
+
await app.register(statusRoutes, {
|
|
5554
|
+
config: opts.config,
|
|
5555
|
+
manager: opts.manager,
|
|
5556
|
+
version: opts.version
|
|
5557
|
+
});
|
|
5558
|
+
await app.register(sessionsRoutes, { config: opts.config });
|
|
5559
|
+
await app.register(configRoutes, { config: opts.config });
|
|
5560
|
+
await app.register(automationRoutes, { config: opts.config });
|
|
5561
|
+
await app.register(chatRoutes, { config: opts.config, manager: opts.manager });
|
|
5562
|
+
await app.register(sessionHubRoutes, { manager: opts.manager });
|
|
5563
|
+
return app;
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5566
|
+
// src/runtime/events.ts
|
|
5567
|
+
var EventBus = class {
|
|
5568
|
+
listeners = /* @__PURE__ */ new Set();
|
|
5569
|
+
/** 发布事件,同步调用所有当前订阅者 */
|
|
5570
|
+
emit(event) {
|
|
5571
|
+
for (const listener of this.listeners) {
|
|
5572
|
+
listener(event);
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
/** 添加事件监听器,返回取消订阅函数 */
|
|
5576
|
+
subscribe(listener) {
|
|
5577
|
+
this.listeners.add(listener);
|
|
5578
|
+
return () => {
|
|
5579
|
+
this.listeners.delete(listener);
|
|
5580
|
+
};
|
|
5581
|
+
}
|
|
5582
|
+
/** 当前订阅者数量(测试用) */
|
|
5583
|
+
get size() {
|
|
5584
|
+
return this.listeners.size;
|
|
5585
|
+
}
|
|
5586
|
+
/** 清除所有订阅者 */
|
|
5587
|
+
clear() {
|
|
5588
|
+
this.listeners.clear();
|
|
5589
|
+
}
|
|
5590
|
+
};
|
|
5591
|
+
|
|
5592
|
+
// src/runtime/approvals.ts
|
|
5593
|
+
var ApprovalQueue = class {
|
|
5594
|
+
pending = /* @__PURE__ */ new Map();
|
|
5595
|
+
/**
|
|
5596
|
+
* 发起一个审批请求,返回 Promise。
|
|
5597
|
+
* 调用方 await 此 Promise,直到 resolve() 被外部调用。
|
|
5598
|
+
*/
|
|
5599
|
+
request(kind, prompt) {
|
|
5600
|
+
const id = crypto.randomUUID();
|
|
5601
|
+
const promise = new Promise((resolve6) => {
|
|
5602
|
+
this.pending.set(id, {
|
|
5603
|
+
meta: { id, kind, prompt },
|
|
5604
|
+
resolve: resolve6
|
|
5605
|
+
});
|
|
5606
|
+
});
|
|
5607
|
+
return { id, promise };
|
|
5608
|
+
}
|
|
5609
|
+
/**
|
|
5610
|
+
* 外部(CLI readline / Web API)调用此方法来解决一个待处理请求。
|
|
5611
|
+
* 若 id 不存在则静默忽略。
|
|
5612
|
+
*/
|
|
5613
|
+
resolve(id, approved) {
|
|
5614
|
+
const entry = this.pending.get(id);
|
|
5615
|
+
if (!entry) return;
|
|
5616
|
+
this.pending.delete(id);
|
|
5617
|
+
entry.resolve(approved);
|
|
5618
|
+
}
|
|
5619
|
+
/** 获取所有待处理请求的只读视图(Web API 用于 GET 查询) */
|
|
5620
|
+
listPending() {
|
|
5621
|
+
return [...this.pending.values()].map((e) => e.meta);
|
|
5622
|
+
}
|
|
5623
|
+
/** 取消所有待处理请求(中断会话时调用),全部视为拒绝 */
|
|
5624
|
+
cancelAll() {
|
|
5625
|
+
for (const entry of this.pending.values()) {
|
|
5626
|
+
entry.resolve(false);
|
|
5627
|
+
}
|
|
5628
|
+
this.pending.clear();
|
|
5629
|
+
}
|
|
5630
|
+
get size() {
|
|
5631
|
+
return this.pending.size;
|
|
5632
|
+
}
|
|
5633
|
+
};
|
|
5634
|
+
|
|
5635
|
+
// src/runtime/session.ts
|
|
5636
|
+
var SESSION_TOOLS = [
|
|
5637
|
+
BASH_TOOL,
|
|
5638
|
+
READ_TOOL,
|
|
5639
|
+
WRITE_TOOL,
|
|
5640
|
+
EDIT_TOOL,
|
|
5641
|
+
GLOB_TOOL,
|
|
5642
|
+
GREP_TOOL,
|
|
5643
|
+
APPLY_PATCH_TOOL,
|
|
5644
|
+
TODO_TOOL,
|
|
5645
|
+
TASK_TOOL,
|
|
5646
|
+
WEB_FETCH_TOOL
|
|
5647
|
+
];
|
|
5648
|
+
var SessionRuntime = class {
|
|
5649
|
+
id;
|
|
5650
|
+
_status = "idle";
|
|
5651
|
+
bus = new EventBus();
|
|
5652
|
+
approvals = new ApprovalQueue();
|
|
5653
|
+
llm;
|
|
5654
|
+
messages;
|
|
5655
|
+
config;
|
|
5656
|
+
model;
|
|
5657
|
+
abortController = null;
|
|
5658
|
+
_turnCount = 0;
|
|
5659
|
+
_totalTokens = 0;
|
|
5660
|
+
startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5661
|
+
lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
5662
|
+
title = "New Session";
|
|
5663
|
+
constructor(config2, opts = {}) {
|
|
5664
|
+
this.id = opts.sessionId ?? crypto.randomUUID();
|
|
5665
|
+
this.config = config2;
|
|
5666
|
+
this.llm = opts.llm ?? createProvider(resolveActiveProfile(config2));
|
|
5667
|
+
this.model = config2.model;
|
|
5668
|
+
const systemContent = opts.systemPrompt ?? (config2.systemPrompt ?? DEFAULT_SYSTEM_PROMPT);
|
|
5669
|
+
this.messages = systemContent ? [{ role: "system", content: systemContent }] : [];
|
|
5670
|
+
if (opts.initialMessages) {
|
|
5671
|
+
this.messages.push(...opts.initialMessages);
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
get status() {
|
|
5675
|
+
return this._status;
|
|
5676
|
+
}
|
|
5677
|
+
/** 订阅事件流,返回取消订阅函数(调用方负责在不需要时取消以防内存泄漏) */
|
|
5678
|
+
subscribe(listener) {
|
|
5679
|
+
return this.bus.subscribe(listener);
|
|
5680
|
+
}
|
|
5681
|
+
/**
|
|
5682
|
+
* 提交用户输入,驱动一轮完整的 agentic 循环。
|
|
5683
|
+
*
|
|
5684
|
+
* 流程:
|
|
5685
|
+
* 1. 将用户消息写入历史
|
|
5686
|
+
* 2. 启动 AbortController(供 interrupt() 使用)
|
|
5687
|
+
* 3. 运行 agentic 循环直到:纯文本回复 / 中断 / 错误
|
|
5688
|
+
* 4. 更新计数器并发出 session.idle
|
|
5689
|
+
*/
|
|
5690
|
+
async submit(userInput) {
|
|
5691
|
+
this.messages.push({ role: "user", content: userInput });
|
|
5692
|
+
this.abortController = new AbortController();
|
|
5693
|
+
const signal = this.abortController.signal;
|
|
5694
|
+
try {
|
|
5695
|
+
await this.runAgenticLoop(signal);
|
|
5696
|
+
} catch {
|
|
5697
|
+
if (!signal.aborted) {
|
|
5698
|
+
this._status = "error";
|
|
5699
|
+
this.bus.emit({ type: "session.error", sessionId: this.id, error: "Unexpected error in agentic loop" });
|
|
5700
|
+
}
|
|
5701
|
+
return;
|
|
5702
|
+
} finally {
|
|
5703
|
+
this.abortController = null;
|
|
5704
|
+
}
|
|
5705
|
+
this._turnCount++;
|
|
5706
|
+
this.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
5707
|
+
this._status = "idle";
|
|
5708
|
+
this.bus.emit({ type: "session.idle", sessionId: this.id });
|
|
5709
|
+
}
|
|
5710
|
+
/**
|
|
5711
|
+
* agentic 循环:持续调用 LLM 并处理工具调用,直到模型给出纯文本回复。
|
|
5712
|
+
*
|
|
5713
|
+
* 每次迭代:
|
|
5714
|
+
* 1. 调用 LLM 流并实时发出 message.delta 事件
|
|
5715
|
+
* 2. 若收到工具调用 → 执行工具 → 追加历史 → 继续循环
|
|
5716
|
+
* 3. 若无工具调用 → 追加 assistant 消息 → 退出循环
|
|
5717
|
+
*/
|
|
5718
|
+
async runAgenticLoop(signal) {
|
|
5719
|
+
while (!signal.aborted) {
|
|
5720
|
+
this._status = "thinking";
|
|
5721
|
+
const messageId = crypto.randomUUID();
|
|
5722
|
+
const uiMsg = {
|
|
5723
|
+
id: messageId,
|
|
5724
|
+
role: "assistant",
|
|
5725
|
+
content: null,
|
|
5726
|
+
toolCalls: [],
|
|
5727
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5728
|
+
};
|
|
5729
|
+
this.bus.emit({ type: "message.created", message: uiMsg });
|
|
5730
|
+
let assistantText = "";
|
|
5731
|
+
let assistantReasoning;
|
|
5732
|
+
let lastReasoningDetails;
|
|
5733
|
+
let lastUsage;
|
|
5734
|
+
const toolCalls = [];
|
|
5735
|
+
for await (const chunk of this.llm.stream(this.messages, SESSION_TOOLS, signal)) {
|
|
5736
|
+
if (signal.aborted) break;
|
|
5737
|
+
if (chunk.text) {
|
|
5738
|
+
this.bus.emit({ type: "message.delta", messageId, text: chunk.text });
|
|
5739
|
+
assistantText += chunk.text;
|
|
5740
|
+
}
|
|
5741
|
+
if (chunk.reasoning) {
|
|
5742
|
+
this.bus.emit({ type: "message.reasoning", messageId, text: chunk.reasoning });
|
|
5743
|
+
}
|
|
5744
|
+
if (chunk.done) {
|
|
5745
|
+
if (chunk.toolCalls) toolCalls.push(...chunk.toolCalls);
|
|
5746
|
+
if (chunk.usage) lastUsage = chunk.usage;
|
|
5747
|
+
if (chunk.reasoning) assistantReasoning = chunk.reasoning;
|
|
5748
|
+
if (chunk.reasoningDetails) lastReasoningDetails = chunk.reasoningDetails;
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
if (signal.aborted) break;
|
|
5752
|
+
this.bus.emit({ type: "message.completed", messageId, usage: lastUsage });
|
|
5753
|
+
if (lastUsage) {
|
|
5754
|
+
this._totalTokens += lastUsage.totalTokens;
|
|
5755
|
+
}
|
|
5756
|
+
if (toolCalls.length > 0) {
|
|
5757
|
+
this._status = "tool_calling";
|
|
5758
|
+
this.messages.push({
|
|
5759
|
+
role: "assistant",
|
|
5760
|
+
content: assistantText || null,
|
|
5761
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
5762
|
+
id: tc.id,
|
|
5763
|
+
type: "function",
|
|
5764
|
+
function: { name: tc.name, arguments: tc.arguments }
|
|
5765
|
+
})),
|
|
5766
|
+
...assistantReasoning ? { reasoning_content: assistantReasoning } : {},
|
|
5767
|
+
...lastReasoningDetails ? { reasoning_details: lastReasoningDetails } : {}
|
|
5768
|
+
});
|
|
5769
|
+
for (const tc of toolCalls) {
|
|
5770
|
+
if (signal.aborted) break;
|
|
5771
|
+
this.bus.emit({ type: "tool.started", callId: tc.id, toolName: tc.name, argsPreview: tc.arguments });
|
|
5772
|
+
const toolResult = await this.executeToolCall(tc, signal);
|
|
5773
|
+
this.bus.emit({ type: "tool.completed", callId: tc.id, toolName: tc.name, output: toolResult });
|
|
5774
|
+
this.messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
|
|
5775
|
+
}
|
|
5776
|
+
} else {
|
|
5777
|
+
if (assistantText) {
|
|
5778
|
+
this.messages.push({
|
|
5779
|
+
role: "assistant",
|
|
5780
|
+
content: assistantText,
|
|
5781
|
+
...assistantReasoning ? { reasoning_content: assistantReasoning } : {}
|
|
5782
|
+
});
|
|
5783
|
+
}
|
|
5784
|
+
break;
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
/**
|
|
5789
|
+
* 执行单个工具调用并返回结果字符串。
|
|
5790
|
+
*
|
|
5791
|
+
* bash 工具:先通过 ApprovalQueue 请求用户确认(normal/danger 级别),
|
|
5792
|
+
* 再执行并将 stdout/stderr/exitCode 格式化为字符串返回。
|
|
5793
|
+
* 其他工具:直接执行,不需要审批流程。
|
|
5794
|
+
*/
|
|
5795
|
+
async executeToolCall(tc, signal) {
|
|
5796
|
+
const { name, arguments: args } = tc;
|
|
5797
|
+
try {
|
|
5798
|
+
if (name === "bash") {
|
|
5799
|
+
const parsed = JSON.parse(args);
|
|
5800
|
+
const cls = classifyCommand(parsed.command, this.config.dangerousPatterns);
|
|
5801
|
+
if (cls === "normal" || cls === "danger") {
|
|
5802
|
+
this._status = "awaiting_confirm";
|
|
5803
|
+
const kind = cls === "danger" ? "danger" : "normal";
|
|
5804
|
+
const prompt = cls === "danger" ? `\u26A0\uFE0F DANGEROUS COMMAND: ${parsed.command}` : `Execute command: ${parsed.command}
|
|
5805
|
+
Proceed?`;
|
|
5806
|
+
const { id: reqId, promise } = this.approvals.request(kind, prompt);
|
|
5807
|
+
this.bus.emit({ type: "approval.requested", requestId: reqId, kind, prompt });
|
|
5808
|
+
const approved = await promise;
|
|
5809
|
+
this._status = "tool_calling";
|
|
5810
|
+
if (!approved) return SKIP_MESSAGE;
|
|
5811
|
+
}
|
|
5812
|
+
if (signal.aborted) return SKIP_MESSAGE;
|
|
5813
|
+
const result = await executeBash(parsed.command);
|
|
5814
|
+
let output = "";
|
|
5815
|
+
if (result.stdout) output += result.stdout;
|
|
5816
|
+
if (result.stderr) output += result.stderr;
|
|
5817
|
+
if (result.exitCode !== 0) output += `
|
|
5818
|
+
[exit code: ${result.exitCode}]`;
|
|
5819
|
+
return output || "(no output)";
|
|
5820
|
+
}
|
|
5821
|
+
if (name === "read") {
|
|
5822
|
+
const parsed = JSON.parse(args);
|
|
5823
|
+
return await readFile2(parsed);
|
|
5824
|
+
}
|
|
5825
|
+
if (name === "write") {
|
|
5826
|
+
const parsed = JSON.parse(args);
|
|
5827
|
+
return await writeFile2(parsed);
|
|
5828
|
+
}
|
|
5829
|
+
if (name === "edit") {
|
|
5830
|
+
const parsed = JSON.parse(args);
|
|
5831
|
+
return await editFile(parsed);
|
|
5832
|
+
}
|
|
5833
|
+
if (name === "glob") {
|
|
5834
|
+
const parsed = JSON.parse(args);
|
|
5835
|
+
return await globFiles(parsed);
|
|
5836
|
+
}
|
|
5837
|
+
if (name === "grep") {
|
|
5838
|
+
const parsed = JSON.parse(args);
|
|
5839
|
+
return await grepFiles(parsed);
|
|
5840
|
+
}
|
|
5841
|
+
if (name === "apply_patch") {
|
|
5842
|
+
const parsed = JSON.parse(args);
|
|
5843
|
+
return await applyPatch(parsed);
|
|
5844
|
+
}
|
|
5845
|
+
if (name === "todo") {
|
|
5846
|
+
const parsed = JSON.parse(args);
|
|
5847
|
+
return todo(parsed);
|
|
5848
|
+
}
|
|
5849
|
+
if (name === "task") {
|
|
5850
|
+
const parsed = JSON.parse(args);
|
|
5851
|
+
return await handleTaskTool(parsed, {
|
|
5852
|
+
provider: this.llm,
|
|
5853
|
+
print: () => {
|
|
5854
|
+
},
|
|
5855
|
+
logDir: this.config.logDir
|
|
5856
|
+
});
|
|
5857
|
+
}
|
|
5858
|
+
if (name === "web_fetch") {
|
|
5859
|
+
const parsed = JSON.parse(args);
|
|
5860
|
+
return await webFetch(parsed);
|
|
5861
|
+
}
|
|
5862
|
+
return `Unknown tool: ${name}`;
|
|
5863
|
+
} catch (err) {
|
|
5864
|
+
return `Tool error: ${String(err)}`;
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
/**
|
|
5868
|
+
* 中断当前正在进行的生成。
|
|
5869
|
+
*
|
|
5870
|
+
* 通过 AbortController 通知流式生成停止,
|
|
5871
|
+
* 同时取消所有待处理的审批(防止 await promise 永久阻塞)。
|
|
5872
|
+
* 在 idle 状态调用是安全的(无 active controller 时静默忽略中断)。
|
|
5873
|
+
*/
|
|
5874
|
+
interrupt() {
|
|
5875
|
+
this.abortController?.abort();
|
|
5876
|
+
this.approvals.cancelAll();
|
|
5877
|
+
this._status = "idle";
|
|
5878
|
+
this.bus.emit({ type: "session.interrupted", sessionId: this.id });
|
|
5879
|
+
}
|
|
5880
|
+
/** 审批或拒绝一个待处理的确认请求(由 Web API 或 CLI readline 调用) */
|
|
5881
|
+
approve(requestId, approved) {
|
|
5882
|
+
this.approvals.resolve(requestId, approved);
|
|
5883
|
+
this.bus.emit({ type: "approval.resolved", requestId, approved });
|
|
5884
|
+
}
|
|
5885
|
+
/** 返回当前会话的只读快照(用于列表展示和 WebSocket 初始状态同步) */
|
|
5886
|
+
snapshot() {
|
|
5887
|
+
return {
|
|
5888
|
+
id: this.id,
|
|
5889
|
+
title: this.title,
|
|
5890
|
+
model: this.model,
|
|
5891
|
+
status: this._status,
|
|
5892
|
+
turnCount: this._turnCount,
|
|
5893
|
+
totalTokens: this._totalTokens,
|
|
5894
|
+
startedAt: this.startedAt,
|
|
5895
|
+
lastActivity: this.lastActivity
|
|
5896
|
+
};
|
|
5897
|
+
}
|
|
5898
|
+
};
|
|
5899
|
+
|
|
5900
|
+
// src/runtime/manager.ts
|
|
5901
|
+
var SessionManager = class {
|
|
5902
|
+
sessions = /* @__PURE__ */ new Map();
|
|
5903
|
+
config;
|
|
5904
|
+
constructor(config2) {
|
|
5905
|
+
this.config = config2;
|
|
5906
|
+
}
|
|
5907
|
+
/** 创建新会话,注册到内存 Map 并返回 ISessionRuntime */
|
|
5908
|
+
async createSession(opts = {}) {
|
|
5909
|
+
const runtime = new SessionRuntime(this.config, opts);
|
|
5910
|
+
this.sessions.set(runtime.id, runtime);
|
|
5911
|
+
return runtime;
|
|
5912
|
+
}
|
|
5913
|
+
/**
|
|
5914
|
+
* 按 ID 恢复已有会话。
|
|
5915
|
+
* 当前实现仅从内存 Map 查找;若进程重启后需要恢复,
|
|
5916
|
+
* 可扩展为从 sessions/metadata.ts 加载历史消息重建会话。
|
|
5917
|
+
*/
|
|
5918
|
+
async resumeSession(sessionId) {
|
|
5919
|
+
const existing = this.sessions.get(sessionId);
|
|
5920
|
+
if (existing) return existing;
|
|
5921
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
5922
|
+
}
|
|
5923
|
+
/** 按 ID 查找会话,不存在时返回 undefined(不抛错) */
|
|
5924
|
+
getSession(sessionId) {
|
|
5925
|
+
return this.sessions.get(sessionId);
|
|
5926
|
+
}
|
|
5927
|
+
/** 返回所有活跃会话的快照列表(只读,用于列表展示) */
|
|
5928
|
+
listRunning() {
|
|
5929
|
+
return [...this.sessions.values()].map((s) => s.snapshot());
|
|
5930
|
+
}
|
|
5931
|
+
};
|
|
5932
|
+
|
|
5012
5933
|
// src/index.ts
|
|
5013
5934
|
var require2 = createRequire(import.meta.url);
|
|
5014
5935
|
var { version } = require2("../package.json");
|
|
@@ -5102,61 +6023,103 @@ if (rawArgs[0] === "sessions") {
|
|
|
5102
6023
|
console.log(output);
|
|
5103
6024
|
process.exit(0);
|
|
5104
6025
|
}
|
|
5105
|
-
if (
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
)
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
}
|
|
5120
|
-
} else if (forkSpec) {
|
|
5121
|
-
try {
|
|
5122
|
-
const raw = readFileSync5(forkSpec.file, "utf-8");
|
|
5123
|
-
initialMessages = truncateToTurn(parseReplayLog(raw), forkSpec.turn);
|
|
5124
|
-
} catch (err) {
|
|
5125
|
-
console.error(`Error reading fork file: ${err}`);
|
|
5126
|
-
process.exit(1);
|
|
6026
|
+
if (rawArgs[0] === "web") {
|
|
6027
|
+
let webPort = 4310;
|
|
6028
|
+
let webHost = "0.0.0.0";
|
|
6029
|
+
for (let i = 1; i < rawArgs.length; i++) {
|
|
6030
|
+
const arg = rawArgs[i];
|
|
6031
|
+
const next = rawArgs[i + 1];
|
|
6032
|
+
if (arg === "--port" && next && !next.startsWith("-")) {
|
|
6033
|
+
const parsed = parseInt(next, 10);
|
|
6034
|
+
if (!isNaN(parsed)) webPort = parsed;
|
|
6035
|
+
i++;
|
|
6036
|
+
} else if (arg === "--host" && next && !next.startsWith("-")) {
|
|
6037
|
+
webHost = next;
|
|
6038
|
+
i++;
|
|
6039
|
+
}
|
|
5127
6040
|
}
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
const skills = await loadSkillsFromDir(dir);
|
|
5136
|
-
for (const skill of skills) registry.register(skill);
|
|
5137
|
-
}
|
|
5138
|
-
var trustedSkillDirs = [builtinSkillsDir, userSkillsDir, projectSkillsDir];
|
|
5139
|
-
if (pipeMode) {
|
|
5140
|
-
const prompt = await readStdin();
|
|
5141
|
-
const llm = createProvider(resolveActiveProfile(finalConfig));
|
|
5142
|
-
await runPipe(prompt, llm);
|
|
5143
|
-
process.exit(0);
|
|
5144
|
-
}
|
|
5145
|
-
if (process.stdout.isTTY) {
|
|
5146
|
-
process.stdout.write("\x1B[?1049h");
|
|
5147
|
-
const exitAltScreen = () => process.stdout.write("\x1B[?1049l");
|
|
5148
|
-
process.on("exit", exitAltScreen);
|
|
5149
|
-
process.on("SIGINT", () => {
|
|
5150
|
-
exitAltScreen();
|
|
5151
|
-
process.exit(0);
|
|
6041
|
+
const token = generateAccessToken();
|
|
6042
|
+
const manager = new SessionManager(finalConfig);
|
|
6043
|
+
const server = await buildServer({
|
|
6044
|
+
config: finalConfig,
|
|
6045
|
+
manager,
|
|
6046
|
+
token,
|
|
6047
|
+
version: VERSION
|
|
5152
6048
|
});
|
|
5153
|
-
|
|
5154
|
-
|
|
6049
|
+
await server.listen({ port: webPort, host: webHost });
|
|
6050
|
+
const displayHost = webHost === "0.0.0.0" ? "localhost" : webHost;
|
|
6051
|
+
console.log(`
|
|
6052
|
+
ecode web admin started`);
|
|
6053
|
+
console.log(` URL: http://${displayHost}:${webPort}`);
|
|
6054
|
+
console.log(` Token: ${token}`);
|
|
6055
|
+
console.log(`
|
|
6056
|
+
Press Ctrl+C to stop.
|
|
6057
|
+
`);
|
|
6058
|
+
process.on("SIGINT", async () => {
|
|
6059
|
+
await server.close();
|
|
5155
6060
|
process.exit(0);
|
|
5156
6061
|
});
|
|
5157
|
-
process.on("
|
|
5158
|
-
|
|
6062
|
+
process.on("SIGTERM", async () => {
|
|
6063
|
+
await server.close();
|
|
5159
6064
|
process.exit(0);
|
|
5160
6065
|
});
|
|
6066
|
+
} else {
|
|
6067
|
+
if (!finalConfig.apiKey) {
|
|
6068
|
+
console.error(
|
|
6069
|
+
"Error: no API key configured.\nSet ECODE_API_KEY or add apiKey to ~/.ecode/config.json"
|
|
6070
|
+
);
|
|
6071
|
+
process.exit(1);
|
|
6072
|
+
}
|
|
6073
|
+
let initialMessages = [];
|
|
6074
|
+
if (replayFile) {
|
|
6075
|
+
try {
|
|
6076
|
+
const raw = readFileSync6(replayFile, "utf-8");
|
|
6077
|
+
initialMessages = parseReplayLog(raw);
|
|
6078
|
+
} catch (err) {
|
|
6079
|
+
console.error(`Error reading replay file: ${err}`);
|
|
6080
|
+
process.exit(1);
|
|
6081
|
+
}
|
|
6082
|
+
} else if (forkSpec) {
|
|
6083
|
+
try {
|
|
6084
|
+
const raw = readFileSync6(forkSpec.file, "utf-8");
|
|
6085
|
+
initialMessages = truncateToTurn(parseReplayLog(raw), forkSpec.turn);
|
|
6086
|
+
} catch (err) {
|
|
6087
|
+
console.error(`Error reading fork file: ${err}`);
|
|
6088
|
+
process.exit(1);
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
6091
|
+
const __dirname = dirname9(fileURLToPath2(import.meta.url));
|
|
6092
|
+
const builtinSkillsDir = resolve5(__dirname, "../skills");
|
|
6093
|
+
const userSkillsDir = resolve5(process.env.HOME ?? "~", ".ecode/skills");
|
|
6094
|
+
const projectSkillsDir = resolve5(process.cwd(), ".ecode/skills");
|
|
6095
|
+
const registry = new SkillRegistry();
|
|
6096
|
+
for (const dir of [builtinSkillsDir, userSkillsDir, projectSkillsDir]) {
|
|
6097
|
+
const skills = await loadSkillsFromDir(dir);
|
|
6098
|
+
for (const skill of skills) registry.register(skill);
|
|
6099
|
+
}
|
|
6100
|
+
const trustedSkillDirs = [builtinSkillsDir, userSkillsDir, projectSkillsDir];
|
|
6101
|
+
if (pipeMode) {
|
|
6102
|
+
const prompt = await readStdin();
|
|
6103
|
+
const llm = createProvider(resolveActiveProfile(finalConfig));
|
|
6104
|
+
await runPipe(prompt, llm);
|
|
6105
|
+
process.exit(0);
|
|
6106
|
+
}
|
|
6107
|
+
if (process.stdout.isTTY) {
|
|
6108
|
+
process.stdout.write("\x1B[?1049h");
|
|
6109
|
+
const exitAltScreen = () => process.stdout.write("\x1B[?1049l");
|
|
6110
|
+
process.on("exit", exitAltScreen);
|
|
6111
|
+
process.on("SIGINT", () => {
|
|
6112
|
+
exitAltScreen();
|
|
6113
|
+
process.exit(0);
|
|
6114
|
+
});
|
|
6115
|
+
process.on("SIGTERM", () => {
|
|
6116
|
+
exitAltScreen();
|
|
6117
|
+
process.exit(0);
|
|
6118
|
+
});
|
|
6119
|
+
process.on("SIGHUP", () => {
|
|
6120
|
+
exitAltScreen();
|
|
6121
|
+
process.exit(0);
|
|
6122
|
+
});
|
|
6123
|
+
}
|
|
6124
|
+
render(React4.createElement(App, { config: finalConfig, version: VERSION, autoMode, registry, trustedSkillDirs, initialMessages }));
|
|
5161
6125
|
}
|
|
5162
|
-
render(React4.createElement(App, { config: finalConfig, version: VERSION, autoMode, registry, trustedSkillDirs, initialMessages }));
|