@zhongqian97-code/ecode 0.5.13 → 0.5.15

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.
Files changed (2) hide show
  1. package/dist/index.js +518 -152
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -552,6 +552,77 @@ function executeBash(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
552
552
  });
553
553
  }
554
554
 
555
+ // src/prompts.ts
556
+ var DEFAULT_SYSTEM_PROMPT = `You are ecode, a terminal-based coding assistant focused on software engineering work.
557
+
558
+ Your job is to help the user complete coding tasks accurately, efficiently, and safely. Prefer doing useful work over discussing work abstractly, but do not pretend to have done things you did not do.
559
+
560
+ # Core behavior
561
+
562
+ - Treat the user's request as a real software engineering task unless they clearly ask for pure explanation.
563
+ - Read relevant code before proposing or making changes.
564
+ - Understand the local context before editing. Do not guess how the codebase works when you can inspect it.
565
+ - Prefer the simplest approach that fully solves the requested problem.
566
+ - Do not add features, refactors, configurability, abstractions, or cleanups that were not requested unless they are necessary to complete the task correctly.
567
+ - Do not make speculative improvements for hypothetical future needs.
568
+ - If an approach fails, diagnose the reason before switching tactics. Do not blindly retry the same failing action.
569
+
570
+ # Editing discipline
571
+
572
+ - Prefer dedicated file tools over shell commands when available.
573
+ - Use read-like tools to inspect files, edit/patch tools to modify files, and write tools only when replacing full file content is the right choice.
574
+ - Do not use shell tricks as a substitute for structured file operations when a dedicated tool exists.
575
+ - Make the smallest coherent change that solves the problem.
576
+ - Do not create new files unless they are actually needed.
577
+ - Do not rewrite large files when a targeted edit is enough.
578
+ - Preserve existing code style unless the task explicitly requires style changes.
579
+ - Do not add comments unless they help explain a non-obvious constraint or decision.
580
+
581
+ # Tool usage
582
+
583
+ - Prefer dedicated tools over bash for file reading, file editing, search, patching, and task tracking.
584
+ - Use bash for commands that are genuinely shell-oriented: tests, builds, git inspection, package manager commands, environment inspection, and other terminal operations.
585
+ - When there are multiple independent lookups, do them efficiently. When steps depend on each other, do them in order.
586
+ - Treat tool output as evidence. Base conclusions on what you actually observed.
587
+ - If tool output is noisy, extract and carry forward only the important facts.
588
+
589
+ # Safety and confirmation
590
+
591
+ - Be careful with destructive, irreversible, or externally visible actions.
592
+ - Ask before actions like deleting important files, overwriting significant uncommitted work, force-pushing, changing remote state, sending messages, or performing other hard-to-reverse operations.
593
+ - Do not treat a previous approval as blanket approval for future risky actions.
594
+ - If the user denies a risky action, do not immediately retry the same action. Adjust your plan.
595
+ - Safety checks are a convenience layer, not a guarantee. Act cautiously even when a command appears allowed.
596
+
597
+ # Verification and honesty
598
+
599
+ - When you change code, verify the result when practical: run tests, type checks, linters, builds, or the narrowest useful validation step.
600
+ - Prefer the smallest meaningful verification rather than expensive blanket verification when the task is local and narrow.
601
+ - If you could not verify something, say so plainly.
602
+ - Never claim success, passing tests, or completed work unless the evidence supports it.
603
+ - If a command failed, a test failed, or verification is incomplete, report that accurately.
604
+
605
+ # Communication style
606
+
607
+ - Be concise, direct, and useful.
608
+ - Give short progress updates when starting multi-step work, when you discover something important, when changing direction, or when blocked.
609
+ - Do not narrate every obvious tool call.
610
+ - Do not pad responses with filler, hype, or unnecessary repetition.
611
+ - When reporting completion, lead with what changed, then include relevant verification or blockers.
612
+ - When you need user input, ask for the single most important missing decision.
613
+
614
+ # Scope control
615
+
616
+ - Solve the requested problem completely, but do not quietly widen scope.
617
+ - If you notice an adjacent issue that matters, mention it briefly and separate it from the requested task.
618
+ - Favor correctness and clarity over cleverness.
619
+
620
+ # Failure mode handling
621
+
622
+ - If paths, commands, or assumptions are wrong, correct course based on inspection rather than defending the original plan.
623
+ - If you are blocked by missing information, say exactly what is missing.
624
+ - If you are blocked by tool limits, choose the next best bounded action instead of stalling.`;
625
+
555
626
  // src/tools/read.ts
556
627
  import * as fs from "fs/promises";
557
628
  var READ_TOOL = {
@@ -1205,8 +1276,160 @@ function todo(params) {
1205
1276
  }
1206
1277
  }
1207
1278
 
1208
- // src/repl.ts
1209
- var SKIP_MESSAGE = "Command skipped by user.";
1279
+ // src/sessions/metadata.ts
1280
+ import * as crypto from "crypto";
1281
+ import * as fs7 from "fs";
1282
+ import * as path5 from "path";
1283
+ function metadataPathFromLogFile(logFilePath2) {
1284
+ const base = path5.basename(logFilePath2, ".jsonl");
1285
+ const dir = path5.dirname(logFilePath2);
1286
+ return path5.join(dir, `${base}-session.json`);
1287
+ }
1288
+ function createSessionMetadata(logFilePath2, model) {
1289
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1290
+ return {
1291
+ id: crypto.randomUUID(),
1292
+ startTime: now,
1293
+ lastActivity: now,
1294
+ cwd: process.cwd(),
1295
+ model,
1296
+ title: "",
1297
+ turnCount: 0,
1298
+ totalTokens: 0,
1299
+ logFile: path5.basename(logFilePath2)
1300
+ };
1301
+ }
1302
+ function writeSessionMetadata(logFilePath2, metadata) {
1303
+ const metaPath = metadataPathFromLogFile(logFilePath2);
1304
+ try {
1305
+ fs7.writeFileSync(metaPath, JSON.stringify(metadata, null, 2) + "\n");
1306
+ } catch (err) {
1307
+ process.stderr.write(`[sessions] Failed to write metadata: ${err}
1308
+ `);
1309
+ }
1310
+ }
1311
+ function readSessionMetadata(metaFilePath) {
1312
+ try {
1313
+ const raw = fs7.readFileSync(metaFilePath, "utf-8");
1314
+ return JSON.parse(raw);
1315
+ } catch {
1316
+ return null;
1317
+ }
1318
+ }
1319
+ function updateSessionMetadata(logFilePath2, partial) {
1320
+ const metaPath = metadataPathFromLogFile(logFilePath2);
1321
+ let existing = null;
1322
+ try {
1323
+ const raw = fs7.readFileSync(metaPath, "utf-8");
1324
+ existing = JSON.parse(raw);
1325
+ } catch {
1326
+ return;
1327
+ }
1328
+ writeSessionMetadata(logFilePath2, { ...existing, ...partial });
1329
+ }
1330
+ function listSessions(logDir) {
1331
+ try {
1332
+ const files = fs7.readdirSync(logDir);
1333
+ const metaFiles = files.filter((f) => f.endsWith("-session.json"));
1334
+ const sessions = [];
1335
+ for (const file of metaFiles) {
1336
+ const meta = readSessionMetadata(path5.join(logDir, file));
1337
+ if (meta) sessions.push(meta);
1338
+ }
1339
+ return sessions.sort(
1340
+ (a, b) => b.lastActivity.localeCompare(a.lastActivity)
1341
+ );
1342
+ } catch {
1343
+ return [];
1344
+ }
1345
+ }
1346
+ function findSession(logDir, idOrPrefix) {
1347
+ const sessions = listSessions(logDir);
1348
+ return sessions.find(
1349
+ (s) => s.id === idOrPrefix || s.id.startsWith(idOrPrefix)
1350
+ ) ?? null;
1351
+ }
1352
+ function generateTitle(firstUserMessage) {
1353
+ const oneLine = firstUserMessage.replace(/\n+/g, " ").trim();
1354
+ return oneLine.length > 50 ? oneLine.slice(0, 47) + "..." : oneLine;
1355
+ }
1356
+
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
+ // src/tools/task.ts
1387
+ var TASK_TOOL = {
1388
+ type: "function",
1389
+ function: {
1390
+ name: "task",
1391
+ description: "\u628A\u4E00\u4E2A\u5B50\u4EFB\u52A1\u59D4\u6258\u7ED9\u9694\u79BB\u7684\u5B50 agent\u3002\u9002\u5408\u4EE3\u7801\u8C03\u7814\u3001\u5C40\u90E8\u5B9E\u73B0\u3001\u6587\u4EF6\u4FEE\u6539\u6216\u7ED3\u679C\u6C47\u603B\u3002\u8FD4\u56DE\u5355\u6761\u6700\u7EC8\u7ED3\u679C\u3002",
1392
+ parameters: {
1393
+ type: "object",
1394
+ properties: {
1395
+ description: {
1396
+ type: "string",
1397
+ description: "3-8 \u4E2A\u8BCD\u7684\u77ED\u63CF\u8FF0\uFF0C\u7528\u4E8E\u6807\u8BC6\u8FD9\u4E2A\u5B50\u4EFB\u52A1\u3002"
1398
+ },
1399
+ prompt: {
1400
+ type: "string",
1401
+ description: "\u4EA4\u7ED9\u5B50 agent \u7684\u5B8C\u6574\u4EFB\u52A1\u8BF4\u660E\u3002"
1402
+ },
1403
+ context: {
1404
+ type: "string",
1405
+ description: "\u53EF\u9009\u3002\u989D\u5916\u8865\u5145\u7ED9\u5B50 agent \u7684\u4E0A\u4E0B\u6587\u8BF4\u660E\u3002"
1406
+ },
1407
+ model: {
1408
+ type: "string",
1409
+ description: "\u53EF\u9009\u3002\u8986\u76D6\u5B50 agent \u4F7F\u7528\u7684\u6A21\u578B\u3002"
1410
+ },
1411
+ cwd: {
1412
+ type: "string",
1413
+ description: "\u53EF\u9009\u3002\u5B50 agent \u7684\u5DE5\u4F5C\u76EE\u5F55\uFF0C\u9ED8\u8BA4\u7EE7\u627F\u5F53\u524D process.cwd()\u3002"
1414
+ },
1415
+ max_turns: {
1416
+ type: "number",
1417
+ description: "\u53EF\u9009\u3002\u5B50 agent \u6700\u591A\u53EF\u6267\u884C\u591A\u5C11\u8F6E\u5DE5\u5177\u5FAA\u73AF\uFF0C\u9ED8\u8BA4 8\u3002"
1418
+ }
1419
+ },
1420
+ required: ["description", "prompt"]
1421
+ }
1422
+ }
1423
+ };
1424
+ var SUBAGENT_SYSTEM_PROMPT = [
1425
+ DEFAULT_SYSTEM_PROMPT,
1426
+ "",
1427
+ "You are a focused subagent working for another ecode session.",
1428
+ "Finish the delegated task independently when possible.",
1429
+ "Use tools directly instead of asking the human for routine lookups.",
1430
+ "Return a concise final result for the parent agent.",
1431
+ "If blocked, clearly state the blocker and the smallest next step."
1432
+ ].join("\n");
1210
1433
  var BASH_TOOL = {
1211
1434
  type: "function",
1212
1435
  function: {
@@ -1221,6 +1444,249 @@ var BASH_TOOL = {
1221
1444
  }
1222
1445
  }
1223
1446
  };
1447
+ var SUBAGENT_TOOLS = [
1448
+ {
1449
+ type: BASH_TOOL.type,
1450
+ function: BASH_TOOL.function
1451
+ },
1452
+ READ_TOOL,
1453
+ WRITE_TOOL,
1454
+ EDIT_TOOL,
1455
+ GLOB_TOOL,
1456
+ GREP_TOOL,
1457
+ APPLY_PATCH_TOOL,
1458
+ TODO_TOOL
1459
+ ];
1460
+ function clip(text, maxChars) {
1461
+ return text.length <= maxChars ? text : `${text.slice(0, maxChars - 3)}...`;
1462
+ }
1463
+ function messageToContextLine(msg) {
1464
+ if (msg.role === "system") return null;
1465
+ if (msg.role === "user") return `user: ${clip(msg.content, 500)}`;
1466
+ if (msg.role === "tool") return `tool(${msg.tool_call_id}): ${clip(msg.content, 700)}`;
1467
+ const parts = [];
1468
+ if (msg.content) parts.push(clip(msg.content, 700));
1469
+ if (msg.tool_calls?.length) {
1470
+ parts.push(
1471
+ `[tool_calls: ${msg.tool_calls.map((tc) => tc.function.name).join(", ")}]`
1472
+ );
1473
+ }
1474
+ return `assistant: ${parts.join(" ")}`.trim();
1475
+ }
1476
+ function buildParentContext(parentMessages, extraContext) {
1477
+ const lines = [];
1478
+ if (extraContext?.trim()) {
1479
+ lines.push("## Caller-provided context");
1480
+ lines.push(extraContext.trim());
1481
+ }
1482
+ const tail = (parentMessages ?? []).slice(-8).map(messageToContextLine).filter((line) => Boolean(line));
1483
+ if (tail.length > 0) {
1484
+ if (lines.length > 0) lines.push("");
1485
+ lines.push("## Recent parent-session context");
1486
+ lines.push(...tail);
1487
+ }
1488
+ if (lines.length === 0) return void 0;
1489
+ return clip(lines.join("\n"), 4e3);
1490
+ }
1491
+ function appendLog(logger, entry) {
1492
+ if (!logger) return;
1493
+ logger.append({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
1494
+ }
1495
+ var SKIP_MESSAGE = "Command skipped by user.";
1496
+ async function runNonInteractiveBash(command, autoApproveNormal) {
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)";
1507
+ }
1508
+ async function executeSubagentToolCall(name, args, autoApproveNormal) {
1509
+ if (name === "bash") {
1510
+ let parsed;
1511
+ try {
1512
+ parsed = JSON.parse(args);
1513
+ } catch {
1514
+ parsed = { command: "" };
1515
+ }
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
+ }
1524
+ if (name === "edit") {
1525
+ return editFile(JSON.parse(args));
1526
+ }
1527
+ if (name === "glob") {
1528
+ return globFiles(JSON.parse(args));
1529
+ }
1530
+ if (name === "grep") {
1531
+ return grepFiles(JSON.parse(args));
1532
+ }
1533
+ if (name === "apply_patch") {
1534
+ return applyPatch(JSON.parse(args));
1535
+ }
1536
+ if (name === "todo") {
1537
+ return todo(JSON.parse(args));
1538
+ }
1539
+ return `Unknown tool: ${name}`;
1540
+ }
1541
+ async function runTaskTool(params, deps) {
1542
+ const maxTurns = Math.max(1, Math.min(32, params.max_turns ?? 8));
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;
1577
+ try {
1578
+ process.chdir(childCwd);
1579
+ for (let turn = 0; turn < maxTurns; turn++) {
1580
+ let assistantText = "";
1581
+ let assistantReasoning;
1582
+ const toolCalls = [];
1583
+ for await (const chunk of llm.stream(messages, SUBAGENT_TOOLS)) {
1584
+ if (chunk.text) assistantText += chunk.text;
1585
+ if (chunk.done) {
1586
+ if (chunk.toolCalls) toolCalls.push(...chunk.toolCalls);
1587
+ if (chunk.reasoning) assistantReasoning = chunk.reasoning;
1588
+ }
1589
+ }
1590
+ if (toolCalls.length === 0) {
1591
+ if (assistantText) {
1592
+ messages.push({
1593
+ role: "assistant",
1594
+ content: assistantText,
1595
+ ...assistantReasoning ? { reasoning_content: assistantReasoning } : {}
1596
+ });
1597
+ appendLog(logger, { role: "assistant", content: assistantText });
1598
+ }
1599
+ if (logger) {
1600
+ updateSessionMetadata(logger.filePath, {
1601
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
1602
+ turnCount: turn + 1
1603
+ });
1604
+ }
1605
+ const taskId = sessionMeta?.id ?? "ephemeral";
1606
+ return [
1607
+ `task_id: ${taskId}`,
1608
+ `description: ${params.description}`,
1609
+ `model: ${params.model ?? profile.model}`,
1610
+ logger ? `log_file: ${logger.filePath}` : void 0,
1611
+ "",
1612
+ "<task_result>",
1613
+ assistantText || "(no final text)",
1614
+ "</task_result>"
1615
+ ].filter((line) => line !== void 0).join("\n");
1616
+ }
1617
+ const assistantMsg = {
1618
+ role: "assistant",
1619
+ content: assistantText || null,
1620
+ tool_calls: toolCalls.map((tc) => ({
1621
+ id: tc.id,
1622
+ type: "function",
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
+ });
1642
+ }
1643
+ if (logger) {
1644
+ updateSessionMetadata(logger.filePath, {
1645
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
1646
+ turnCount: turn + 1
1647
+ });
1648
+ }
1649
+ }
1650
+ return [
1651
+ `description: ${params.description}`,
1652
+ `model: ${params.model ?? profile.model}`,
1653
+ logger ? `log_file: ${logger.filePath}` : void 0,
1654
+ "",
1655
+ "<task_result>",
1656
+ `Subagent stopped after reaching max_turns=${maxTurns} without producing a final answer.`,
1657
+ "</task_result>"
1658
+ ].filter((line) => line !== void 0).join("\n");
1659
+ } catch (err) {
1660
+ 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");
1669
+ } finally {
1670
+ process.chdir(originalCwd);
1671
+ }
1672
+ }
1673
+
1674
+ // src/repl.ts
1675
+ var SKIP_MESSAGE2 = "Command skipped by user.";
1676
+ var BASH_TOOL2 = {
1677
+ type: "function",
1678
+ function: {
1679
+ name: "bash",
1680
+ description: "Execute a shell command and return its output.",
1681
+ parameters: {
1682
+ type: "object",
1683
+ properties: {
1684
+ command: { type: "string", description: "The shell command to run." }
1685
+ },
1686
+ required: ["command"]
1687
+ }
1688
+ }
1689
+ };
1224
1690
  async function handleBashTool(command, deps) {
1225
1691
  const { confirm, print, dangerousPatterns, autoApproveNormal } = deps;
1226
1692
  const cls = classifyCommand(command, dangerousPatterns);
@@ -1228,16 +1694,16 @@ async function handleBashTool(command, deps) {
1228
1694
  if (!autoApproveNormal) {
1229
1695
  const ok = await confirm(`Execute command: ${command}
1230
1696
  Proceed? (y/n) `);
1231
- if (!ok) return SKIP_MESSAGE;
1697
+ if (!ok) return SKIP_MESSAGE2;
1232
1698
  }
1233
1699
  } else if (cls === "danger") {
1234
1700
  print(`\u26A0\uFE0F DANGEROUS COMMAND: ${command}`);
1235
1701
  const first = await confirm("Are you sure? (y/n) ");
1236
- if (!first) return SKIP_MESSAGE;
1702
+ if (!first) return SKIP_MESSAGE2;
1237
1703
  const second = await confirm(
1238
1704
  "Confirm again \u2014 this is destructive. Continue? (y/n) "
1239
1705
  );
1240
- if (!second) return SKIP_MESSAGE;
1706
+ if (!second) return SKIP_MESSAGE2;
1241
1707
  }
1242
1708
  const result = await deps.executeBash(command);
1243
1709
  let output = "";
@@ -1248,35 +1714,6 @@ Proceed? (y/n) `);
1248
1714
  return output || "(no output)";
1249
1715
  }
1250
1716
 
1251
- // src/logger.ts
1252
- import * as fs7 from "fs";
1253
- import * as path5 from "path";
1254
- function createLogger(logDir, sessionStart) {
1255
- fs7.mkdirSync(logDir, { recursive: true });
1256
- const filename = sessionStart.toISOString().replace(/:/g, "-").replace(/\..+/, "") + ".jsonl";
1257
- const filePath = path5.join(logDir, filename);
1258
- return {
1259
- filePath,
1260
- /**
1261
- * 将单条日志条目序列化为 JSON 并同步追加到文件(末尾加换行符)。
1262
- *
1263
- * 使用 appendFileSync 而非 appendFile(异步版本)的原因:
1264
- * 进程崩溃或 Ctrl-C 退出时,异步写操作可能尚未完成,导致最后几条记录丢失。
1265
- * 同步写入虽然阻塞事件循环,但日志条目通常很小(< 1KB),延迟可忽略。
1266
- *
1267
- * 写入失败时输出到 stderr 而非抛出异常,防止日志错误中断正常业务流程。
1268
- */
1269
- append(entry) {
1270
- try {
1271
- fs7.appendFileSync(filePath, JSON.stringify(entry) + "\n");
1272
- } catch (err) {
1273
- process.stderr.write(`[logger] Failed to write log entry: ${err}
1274
- `);
1275
- }
1276
- }
1277
- };
1278
- }
1279
-
1280
1717
  // src/skills/resolver.ts
1281
1718
  function isSkillCommand(input) {
1282
1719
  return input.length > 1 && input.startsWith("/");
@@ -1312,12 +1749,12 @@ ${args}` : skill.body;
1312
1749
 
1313
1750
  // src/skills/executor.ts
1314
1751
  import { exec as exec2 } from "child_process";
1315
- import { dirname as dirname4 } from "path";
1752
+ import { dirname as dirname5 } from "path";
1316
1753
  import { promisify } from "util";
1317
1754
 
1318
1755
  // src/skills/loader.ts
1319
1756
  import { readFile as readFile6, readdir as readdir3, stat as stat3 } from "fs/promises";
1320
- import { join as join5, dirname as dirname3, basename, resolve as resolve3, sep as sep3 } from "path";
1757
+ import { join as join6, dirname as dirname4, basename as basename2, resolve as resolve3, sep as sep3 } from "path";
1321
1758
  function parseFrontmatter(content) {
1322
1759
  const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
1323
1760
  const match = FRONTMATTER_RE.exec(content);
@@ -1357,7 +1794,7 @@ async function fileExists(filePath) {
1357
1794
  }
1358
1795
  }
1359
1796
  async function loadTools(skillDir) {
1360
- const toolsJsonPath = join5(skillDir, "tools.json");
1797
+ const toolsJsonPath = join6(skillDir, "tools.json");
1361
1798
  if (!await fileExists(toolsJsonPath)) return [];
1362
1799
  let raw;
1363
1800
  try {
@@ -1380,7 +1817,7 @@ async function loadTools(skillDir) {
1380
1817
  name: entry["name"],
1381
1818
  description: entry["description"],
1382
1819
  parameters: entry["parameters"] ?? {},
1383
- scriptPath: join5(skillDir, `${entry["name"]}.sh`)
1820
+ scriptPath: join6(skillDir, `${entry["name"]}.sh`)
1384
1821
  });
1385
1822
  }
1386
1823
  }
@@ -1389,12 +1826,12 @@ async function loadTools(skillDir) {
1389
1826
  async function loadSkillFile(skillMdPath) {
1390
1827
  const content = await readFile6(skillMdPath, "utf-8");
1391
1828
  const { data, body } = parseFrontmatter(content);
1392
- const skillDir = dirname3(skillMdPath);
1393
- const dirName = basename(skillDir);
1829
+ const skillDir = dirname4(skillMdPath);
1830
+ const dirName = basename2(skillDir);
1394
1831
  const [tools, hasPreScript, hasPostScript] = await Promise.all([
1395
1832
  loadTools(skillDir),
1396
- fileExists(join5(skillDir, "pre.sh")),
1397
- fileExists(join5(skillDir, "post.sh"))
1833
+ fileExists(join6(skillDir, "pre.sh")),
1834
+ fileExists(join6(skillDir, "post.sh"))
1398
1835
  ]);
1399
1836
  return {
1400
1837
  name: data["name"] ?? dirName,
@@ -1402,8 +1839,8 @@ async function loadSkillFile(skillMdPath) {
1402
1839
  body,
1403
1840
  source: skillMdPath,
1404
1841
  tools,
1405
- preScript: hasPreScript ? join5(skillDir, "pre.sh") : null,
1406
- postScript: hasPostScript ? join5(skillDir, "post.sh") : null
1842
+ preScript: hasPreScript ? join6(skillDir, "pre.sh") : null,
1843
+ postScript: hasPostScript ? join6(skillDir, "post.sh") : null
1407
1844
  };
1408
1845
  }
1409
1846
  async function loadSkillsFromDir(dir) {
@@ -1415,7 +1852,7 @@ async function loadSkillsFromDir(dir) {
1415
1852
  }
1416
1853
  const skills = [];
1417
1854
  for (const entry of entries) {
1418
- const entryPath = join5(dir, entry);
1855
+ const entryPath = join6(dir, entry);
1419
1856
  let entryStat;
1420
1857
  try {
1421
1858
  entryStat = await stat3(entryPath);
@@ -1423,7 +1860,7 @@ async function loadSkillsFromDir(dir) {
1423
1860
  continue;
1424
1861
  }
1425
1862
  if (!entryStat.isDirectory()) continue;
1426
- const skillMdPath = join5(entryPath, "SKILL.md");
1863
+ const skillMdPath = join6(entryPath, "SKILL.md");
1427
1864
  try {
1428
1865
  await stat3(skillMdPath);
1429
1866
  } catch {
@@ -1453,7 +1890,7 @@ async function executePreScript(skill, trustedDirs) {
1453
1890
  if (!skill.preScript) {
1454
1891
  throw new Error("No pre script configured for this skill");
1455
1892
  }
1456
- const skillDir = dirname4(skill.source);
1893
+ const skillDir = dirname5(skill.source);
1457
1894
  if (!isTrustedSkillPath(skillDir, trustedDirs)) {
1458
1895
  throw new SecurityError(
1459
1896
  `Untrusted skill path: ${skillDir} is not in trusted dirs`
@@ -1462,7 +1899,7 @@ async function executePreScript(skill, trustedDirs) {
1462
1899
  return runScript(skill.preScript, []);
1463
1900
  }
1464
1901
  async function executeSkillTool(tool, args, trustedDirs) {
1465
- const scriptDir = dirname4(tool.scriptPath);
1902
+ const scriptDir = dirname5(tool.scriptPath);
1466
1903
  if (!isTrustedSkillPath(scriptDir, trustedDirs)) {
1467
1904
  throw new SecurityError(
1468
1905
  `Untrusted tool script path: ${tool.scriptPath}`
@@ -2008,8 +2445,8 @@ function dismiss(state) {
2008
2445
  }
2009
2446
 
2010
2447
  // src/ui/fileCompletion.ts
2011
- import * as fs8 from "fs/promises";
2012
- import * as path6 from "path";
2448
+ import * as fs9 from "fs/promises";
2449
+ import * as path7 from "path";
2013
2450
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
2014
2451
  function isHidden(name) {
2015
2452
  return name.startsWith(".");
@@ -2018,24 +2455,24 @@ async function walkDir2(dir, root, results, maxResults) {
2018
2455
  if (results.length >= maxResults) return;
2019
2456
  let entries;
2020
2457
  try {
2021
- entries = await fs8.readdir(dir, { withFileTypes: true });
2458
+ entries = await fs9.readdir(dir, { withFileTypes: true });
2022
2459
  } catch {
2023
2460
  return;
2024
2461
  }
2025
2462
  for (const entry of entries) {
2026
2463
  if (results.length >= maxResults) return;
2027
2464
  if (SKIP_DIRS.has(entry.name) || isHidden(entry.name)) continue;
2028
- const relPath = path6.relative(root, path6.join(dir, entry.name));
2465
+ const relPath = path7.relative(root, path7.join(dir, entry.name));
2029
2466
  if (entry.isDirectory()) {
2030
2467
  results.push({ path: relPath, isDir: true });
2031
- await walkDir2(path6.join(dir, entry.name), root, results, maxResults);
2468
+ await walkDir2(path7.join(dir, entry.name), root, results, maxResults);
2032
2469
  } else {
2033
2470
  results.push({ path: relPath, isDir: false });
2034
2471
  }
2035
2472
  }
2036
2473
  }
2037
2474
  async function listFilesForQuery(query, cwd, maxResults = 50) {
2038
- if (path6.isAbsolute(query)) {
2475
+ if (path7.isAbsolute(query)) {
2039
2476
  return listAbsolute(query, maxResults);
2040
2477
  }
2041
2478
  const all = [];
@@ -2047,18 +2484,18 @@ async function listAbsolute(query, maxResults) {
2047
2484
  let dir = query;
2048
2485
  let filter = "";
2049
2486
  try {
2050
- const stat5 = await fs8.stat(dir);
2487
+ const stat5 = await fs9.stat(dir);
2051
2488
  if (!stat5.isDirectory()) {
2052
- filter = path6.basename(dir);
2053
- dir = path6.dirname(dir);
2489
+ filter = path7.basename(dir);
2490
+ dir = path7.dirname(dir);
2054
2491
  }
2055
2492
  } catch {
2056
- filter = path6.basename(dir);
2057
- dir = path6.dirname(dir);
2493
+ filter = path7.basename(dir);
2494
+ dir = path7.dirname(dir);
2058
2495
  }
2059
2496
  let entries;
2060
2497
  try {
2061
- entries = await fs8.readdir(dir, { withFileTypes: true });
2498
+ entries = await fs9.readdir(dir, { withFileTypes: true });
2062
2499
  } catch {
2063
2500
  return [];
2064
2501
  }
@@ -2067,7 +2504,7 @@ async function listAbsolute(query, maxResults) {
2067
2504
  if (SKIP_DIRS.has(entry.name) || isHidden(entry.name)) continue;
2068
2505
  if (filter && !entry.name.includes(filter)) continue;
2069
2506
  results.push({
2070
- path: path6.join(dir, entry.name),
2507
+ path: path7.join(dir, entry.name),
2071
2508
  isDir: entry.isDirectory()
2072
2509
  });
2073
2510
  if (results.length >= maxResults) break;
@@ -2089,10 +2526,10 @@ async function expandFileRefs(text, cwd) {
2089
2526
  atPattern.lastIndex = 0;
2090
2527
  while ((match = atPattern.exec(text)) !== null) {
2091
2528
  const filePath = match[1];
2092
- const fullPath = path6.isAbsolute(filePath) ? filePath : path6.join(cwd, filePath);
2529
+ const fullPath = path7.isAbsolute(filePath) ? filePath : path7.join(cwd, filePath);
2093
2530
  let replacement;
2094
2531
  try {
2095
- const content = await fs8.readFile(fullPath, "utf8");
2532
+ const content = await fs9.readFile(fullPath, "utf8");
2096
2533
  replacement = `\`\`\`
2097
2534
  // @${filePath}
2098
2535
  ${content}
@@ -2174,9 +2611,9 @@ import { join as join11 } from "path";
2174
2611
 
2175
2612
  // src/automation/store.ts
2176
2613
  import { readFile as readFile8, writeFile as writeFile5, mkdir as mkdir2 } from "fs/promises";
2177
- import { join as join7 } from "path";
2614
+ import { join as join8 } from "path";
2178
2615
  function jobsFilePath(dataDir) {
2179
- return join7(dataDir, "jobs.json");
2616
+ return join8(dataDir, "jobs.json");
2180
2617
  }
2181
2618
  async function loadJobs(dataDir) {
2182
2619
  try {
@@ -2211,13 +2648,13 @@ async function removeJob(dataDir, id) {
2211
2648
  }
2212
2649
 
2213
2650
  // src/automation/runtime.ts
2214
- import { randomUUID } from "crypto";
2651
+ import { randomUUID as randomUUID2 } from "crypto";
2215
2652
 
2216
2653
  // src/automation/log.ts
2217
2654
  import { readFile as readFile9, appendFile, mkdir as mkdir3 } from "fs/promises";
2218
- import { join as join8 } from "path";
2655
+ import { join as join9 } from "path";
2219
2656
  function logFilePath(logDir) {
2220
- return join8(logDir, "automation-runs.jsonl");
2657
+ return join9(logDir, "automation-runs.jsonl");
2221
2658
  }
2222
2659
  async function appendRunLog(logDir, entry) {
2223
2660
  await mkdir3(logDir, { recursive: true });
@@ -2230,7 +2667,7 @@ async function executeJob(job, config2) {
2230
2667
  return null;
2231
2668
  }
2232
2669
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2233
- const runId = randomUUID();
2670
+ const runId = randomUUID2();
2234
2671
  let summaryText;
2235
2672
  let errorMsg;
2236
2673
  let result;
@@ -2437,7 +2874,7 @@ var LoopScheduler = class {
2437
2874
  };
2438
2875
 
2439
2876
  // src/automation/loop/command.ts
2440
- import { randomUUID as randomUUID2 } from "crypto";
2877
+ import { randomUUID as randomUUID3 } from "crypto";
2441
2878
 
2442
2879
  // src/automation/loop/parse.ts
2443
2880
  var DEFAULT_INTERVAL_MS = 6e5;
@@ -2528,7 +2965,7 @@ async function cmdLoop(args, deps) {
2528
2965
  }
2529
2966
  const now = /* @__PURE__ */ new Date();
2530
2967
  const job = {
2531
- id: randomUUID2(),
2968
+ id: randomUUID3(),
2532
2969
  kind: "loop",
2533
2970
  title: prompt.slice(0, 60),
2534
2971
  createdAt: now.toISOString(),
@@ -2572,7 +3009,7 @@ async function cmdUnloop(idOrPrefix, deps) {
2572
3009
  }
2573
3010
 
2574
3011
  // src/automation/goal/command.ts
2575
- import { randomUUID as randomUUID3 } from "crypto";
3012
+ import { randomUUID as randomUUID4 } from "crypto";
2576
3013
  async function cmdGoal(args, deps) {
2577
3014
  const condition = args.trim();
2578
3015
  if (!condition) {
@@ -2580,7 +3017,7 @@ async function cmdGoal(args, deps) {
2580
3017
  }
2581
3018
  const now = /* @__PURE__ */ new Date();
2582
3019
  const job = {
2583
- id: randomUUID3(),
3020
+ id: randomUUID4(),
2584
3021
  kind: "goal",
2585
3022
  title: condition.slice(0, 60),
2586
3023
  createdAt: now.toISOString(),
@@ -2832,84 +3269,6 @@ var AutomationManager = class {
2832
3269
  }
2833
3270
  };
2834
3271
 
2835
- // src/sessions/metadata.ts
2836
- import * as crypto from "crypto";
2837
- import * as fs9 from "fs";
2838
- import * as path7 from "path";
2839
- function metadataPathFromLogFile(logFilePath2) {
2840
- const base = path7.basename(logFilePath2, ".jsonl");
2841
- const dir = path7.dirname(logFilePath2);
2842
- return path7.join(dir, `${base}-session.json`);
2843
- }
2844
- function createSessionMetadata(logFilePath2, model) {
2845
- const now = (/* @__PURE__ */ new Date()).toISOString();
2846
- return {
2847
- id: crypto.randomUUID(),
2848
- startTime: now,
2849
- lastActivity: now,
2850
- cwd: process.cwd(),
2851
- model,
2852
- title: "",
2853
- turnCount: 0,
2854
- totalTokens: 0,
2855
- logFile: path7.basename(logFilePath2)
2856
- };
2857
- }
2858
- function writeSessionMetadata(logFilePath2, metadata) {
2859
- const metaPath = metadataPathFromLogFile(logFilePath2);
2860
- try {
2861
- fs9.writeFileSync(metaPath, JSON.stringify(metadata, null, 2) + "\n");
2862
- } catch (err) {
2863
- process.stderr.write(`[sessions] Failed to write metadata: ${err}
2864
- `);
2865
- }
2866
- }
2867
- function readSessionMetadata(metaFilePath) {
2868
- try {
2869
- const raw = fs9.readFileSync(metaFilePath, "utf-8");
2870
- return JSON.parse(raw);
2871
- } catch {
2872
- return null;
2873
- }
2874
- }
2875
- function updateSessionMetadata(logFilePath2, partial) {
2876
- const metaPath = metadataPathFromLogFile(logFilePath2);
2877
- let existing = null;
2878
- try {
2879
- const raw = fs9.readFileSync(metaPath, "utf-8");
2880
- existing = JSON.parse(raw);
2881
- } catch {
2882
- return;
2883
- }
2884
- writeSessionMetadata(logFilePath2, { ...existing, ...partial });
2885
- }
2886
- function listSessions(logDir) {
2887
- try {
2888
- const files = fs9.readdirSync(logDir);
2889
- const metaFiles = files.filter((f) => f.endsWith("-session.json"));
2890
- const sessions = [];
2891
- for (const file of metaFiles) {
2892
- const meta = readSessionMetadata(path7.join(logDir, file));
2893
- if (meta) sessions.push(meta);
2894
- }
2895
- return sessions.sort(
2896
- (a, b) => b.lastActivity.localeCompare(a.lastActivity)
2897
- );
2898
- } catch {
2899
- return [];
2900
- }
2901
- }
2902
- function findSession(logDir, idOrPrefix) {
2903
- const sessions = listSessions(logDir);
2904
- return sessions.find(
2905
- (s) => s.id === idOrPrefix || s.id.startsWith(idOrPrefix)
2906
- ) ?? null;
2907
- }
2908
- function generateTitle(firstUserMessage) {
2909
- const oneLine = firstUserMessage.replace(/\n+/g, " ").trim();
2910
- return oneLine.length > 50 ? oneLine.slice(0, 47) + "..." : oneLine;
2911
- }
2912
-
2913
3272
  // src/meta_skill/index.ts
2914
3273
  import * as fs11 from "fs";
2915
3274
  import * as path9 from "path";
@@ -4093,7 +4452,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
4093
4452
  function: { name: t.name, description: t.description, parameters: t.parameters }
4094
4453
  }));
4095
4454
  try {
4096
- 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, ...dynamicTools], abortController.signal)) {
4455
+ for await (const chunk of llmRef.current.stream(currentMessages, [BASH_TOOL2, READ_TOOL, WRITE_TOOL, EDIT_TOOL, GLOB_TOOL, GREP_TOOL, APPLY_PATCH_TOOL, TODO_TOOL, TASK_TOOL, ...dynamicTools], abortController.signal)) {
4097
4456
  if (chunk.text) {
4098
4457
  assistantText += chunk.text;
4099
4458
  setMessages((prev) => {
@@ -4194,6 +4553,13 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
4194
4553
  } else if (tc.name === "todo") {
4195
4554
  const args = JSON.parse(tc.arguments);
4196
4555
  toolResult = todo(args);
4556
+ } else if (tc.name === "task") {
4557
+ const args = JSON.parse(tc.arguments);
4558
+ toolResult = await runTaskTool(args, {
4559
+ config: config2,
4560
+ parentMessages: currentMessages,
4561
+ autoApproveNormal: autoMode2
4562
+ });
4197
4563
  } else {
4198
4564
  const skillTool = skillToolsRef.current.find((t) => t.name === tc.name);
4199
4565
  if (skillTool) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhongqian97-code/ecode",
3
- "version": "0.5.13",
3
+ "version": "0.5.15",
4
4
  "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
5
  "type": "module",
6
6
  "author": "zhongqian97-code",