dskcode 0.1.20 → 0.1.22

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 CHANGED
@@ -1,15 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DeepSeekProvider,
4
- ModelNotSupportedError
5
- } from "./chunk-BA7UJPKJ.js";
6
- import {
4
+ ModelNotSupportedError,
7
5
  SUPPORTED_MODELS,
8
6
  calculateCost,
9
7
  estimateTokens,
10
8
  getModelMeta,
11
9
  isSupportedModel
12
- } from "./chunk-7QS2XBBX.js";
10
+ } from "./chunk-I6HEIOSU.js";
13
11
 
14
12
  // src/cli/index.tsx
15
13
  import { Command } from "commander";
@@ -818,7 +816,7 @@ var LOGO_LINES = [
818
816
  // src/ui/ChatSession.tsx
819
817
  import { Box as Box8, Text as Text9, useInput, Static } from "ink";
820
818
  import TextInput from "ink-text-input";
821
- import { useEffect as useEffect3, useState as useState3, useCallback as useCallback2, useRef as useRef2 } from "react";
819
+ import { useEffect as useEffect3, useState as useState3, useCallback as useCallback2, useMemo, useRef as useRef2 } from "react";
822
820
 
823
821
  // src/ui/useDoubleCtrlC.ts
824
822
  import { useState as useState2, useCallback, useEffect as useEffect2, useRef } from "react";
@@ -848,6 +846,9 @@ function useDoubleCtrlC(onExit) {
848
846
  return { doubleCtrlC, handleCtrlC };
849
847
  }
850
848
 
849
+ // src/ui/ChatSession.tsx
850
+ import InkSpinner2 from "ink-spinner";
851
+
851
852
  // src/ui/AssistantMessage.tsx
852
853
  import { Box as Box4, Text as Text5 } from "ink";
853
854
 
@@ -1182,38 +1183,58 @@ function estimateMessageTokens(msg) {
1182
1183
  }
1183
1184
  return estimateTokens(text) + 10;
1184
1185
  }
1186
+ function groupIntoTurns(messages) {
1187
+ const turns = [];
1188
+ let current = null;
1189
+ for (const msg of messages) {
1190
+ if (msg.role === "user") {
1191
+ if (current) turns.push(current);
1192
+ current = [msg];
1193
+ } else {
1194
+ if (!current) current = [];
1195
+ current.push(msg);
1196
+ }
1197
+ }
1198
+ if (current && current.length > 0) turns.push(current);
1199
+ return turns;
1200
+ }
1201
+ function estimateTurnTokens(turn) {
1202
+ let sum = 0;
1203
+ for (const msg of turn) sum += estimateMessageTokens(msg);
1204
+ return sum;
1205
+ }
1185
1206
  function trimMessages(messages, opts) {
1186
1207
  const meta = getModelMeta(opts.model);
1187
1208
  const maxInputTokens = meta.contextWindow - opts.reservedForOutput;
1188
1209
  const systemTokens = estimateTokens(opts.systemPrompt);
1189
1210
  let remaining = maxInputTokens - systemTokens;
1190
- const preserved = [];
1211
+ const turns = groupIntoTurns(messages);
1212
+ const preservedTurns = [];
1191
1213
  let roundsPreserved = 0;
1192
- for (let i = messages.length - 1; i >= 0 && roundsPreserved < opts.preserveRecentRounds; i--) {
1193
- preserved.unshift(messages[i]);
1194
- if (messages[i].role === "user") {
1195
- roundsPreserved++;
1196
- }
1214
+ for (let i = turns.length - 1; i >= 0 && roundsPreserved < opts.preserveRecentRounds; i--) {
1215
+ preservedTurns.unshift(turns[i]);
1216
+ roundsPreserved++;
1197
1217
  }
1218
+ const preserved = preservedTurns.flat();
1198
1219
  for (const msg of preserved) {
1199
1220
  remaining -= estimateMessageTokens(msg);
1200
1221
  }
1201
1222
  if (remaining < 0) {
1202
- while (preserved.length > 1 && remaining < 0) {
1203
- const removed = preserved.shift();
1204
- remaining += estimateMessageTokens(removed);
1223
+ while (preservedTurns.length > 1 && remaining < 0) {
1224
+ const dropped = preservedTurns.shift();
1225
+ remaining += estimateTurnTokens(dropped);
1205
1226
  }
1206
- return [preserved, true];
1227
+ return [preservedTurns.flat(), true];
1207
1228
  }
1208
- const olderMessages = messages.slice(0, messages.length - preserved.length);
1209
- const kept = [];
1210
- for (let i = olderMessages.length - 1; i >= 0; i--) {
1211
- const cost = estimateMessageTokens(olderMessages[i]);
1229
+ const olderTurns = turns.slice(0, turns.length - preservedTurns.length);
1230
+ const keptTurns = [];
1231
+ for (let i = olderTurns.length - 1; i >= 0; i--) {
1232
+ const cost = estimateTurnTokens(olderTurns[i]);
1212
1233
  if (remaining - cost < 0) break;
1213
1234
  remaining -= cost;
1214
- kept.unshift(olderMessages[i]);
1235
+ keptTurns.unshift(olderTurns[i]);
1215
1236
  }
1216
- const result = [...kept, ...preserved];
1237
+ const result = [...keptTurns, ...preservedTurns].flat();
1217
1238
  const trimmed = result.length < messages.length;
1218
1239
  return [result, trimmed];
1219
1240
  }
@@ -1420,6 +1441,44 @@ function createProvider(config) {
1420
1441
  return defaultRegistry.get(config.name, config);
1421
1442
  }
1422
1443
 
1444
+ // src/tool/types.ts
1445
+ function eraseTool(tool) {
1446
+ return {
1447
+ get name() {
1448
+ return tool.name;
1449
+ },
1450
+ get description() {
1451
+ return tool.description;
1452
+ },
1453
+ get kind() {
1454
+ return tool.kind;
1455
+ },
1456
+ get parameters() {
1457
+ return tool.parameters;
1458
+ },
1459
+ get supportsInputStreaming() {
1460
+ return tool.supportsInputStreaming ?? false;
1461
+ },
1462
+ get supportedProviders() {
1463
+ return tool.supportedProviders ?? [];
1464
+ },
1465
+ async execute(args, ctx) {
1466
+ return tool.execute(args, ctx);
1467
+ },
1468
+ initialTitle(args) {
1469
+ return tool.initialTitle?.(args) ?? `${tool.name}`;
1470
+ }
1471
+ };
1472
+ }
1473
+ function isReadOnly(kind) {
1474
+ return kind === "read" /* Read */;
1475
+ }
1476
+ var AlwaysAllowGate = class {
1477
+ check(_toolName, _args) {
1478
+ return true;
1479
+ }
1480
+ };
1481
+
1423
1482
  // src/agent/extra-prompt.ts
1424
1483
  var EXTRA_PROMPT = [
1425
1484
  "\u3010\u7EC8\u7AEF\u8F93\u51FA\u7EA6\u675F\u3011",
@@ -1522,14 +1581,28 @@ ${opts.projectContext}`);
1522
1581
  var ToolRegistry = class {
1523
1582
  #tools = /* @__PURE__ */ new Map();
1524
1583
  #disabledNames;
1584
+ #featureFlagChecker;
1585
+ #provider;
1586
+ /** 所有已注册工具的名称列表(编译期等效) */
1587
+ static ALL_TOOL_NAMES = [];
1525
1588
  constructor(opts) {
1526
1589
  this.#disabledNames = new Set(opts?.disabledTools ?? []);
1590
+ this.#featureFlagChecker = opts?.featureFlagChecker ?? (() => true);
1591
+ this.#provider = opts?.provider;
1527
1592
  }
1528
1593
  /**
1529
- * 注册一个工具。
1594
+ * 注册一个类型安全的 AgentTool。
1595
+ * 自动擦除类型参数后存储。
1530
1596
  * 如果同名工具已存在则抛出错误。
1531
1597
  */
1532
1598
  register(tool) {
1599
+ return this.registerErased(eraseTool(tool));
1600
+ }
1601
+ /**
1602
+ * 注册一个已经擦除类型 AnyAgentTool。
1603
+ * 如果同名工具已存在则抛出错误。
1604
+ */
1605
+ registerErased(tool) {
1533
1606
  if (this.#tools.has(tool.name)) {
1534
1607
  throw new Error(`\u5DE5\u5177 "${tool.name}" \u5DF2\u6CE8\u518C\uFF0C\u4E0D\u80FD\u91CD\u590D\u6CE8\u518C`);
1535
1608
  }
@@ -1541,7 +1614,7 @@ var ToolRegistry = class {
1541
1614
  */
1542
1615
  registerAll(tools) {
1543
1616
  for (const tool of tools) {
1544
- this.register(tool);
1617
+ this.registerErased(tool);
1545
1618
  }
1546
1619
  return this;
1547
1620
  }
@@ -1552,25 +1625,44 @@ var ToolRegistry = class {
1552
1625
  return this.#tools.delete(name);
1553
1626
  }
1554
1627
  /**
1555
- * 按名称获取工具。
1556
- * 如果工具被禁用或不存在,返回 undefined。
1628
+ * 按名称获取工具(未擦除类型版本,调用方自行断言类型)。
1629
+ * 如果工具被禁用、被 feature flag 拦截、或不支持当前 provider,返回 undefined。
1557
1630
  */
1558
1631
  get(name) {
1559
- if (this.#disabledNames.has(name)) return void 0;
1632
+ if (!this.#isToolEnabled(name)) return void 0;
1560
1633
  return this.#tools.get(name);
1561
1634
  }
1562
1635
  /**
1563
1636
  * 获取所有启用的工具列表。
1637
+ * 依次应用:禁用列表 → Feature Flag → Provider 过滤。
1564
1638
  */
1565
1639
  list() {
1566
1640
  const result = [];
1567
1641
  for (const [name, tool] of this.#tools) {
1568
- if (!this.#disabledNames.has(name)) {
1642
+ if (this.#isToolEnabled(name)) {
1569
1643
  result.push(tool);
1570
1644
  }
1571
1645
  }
1572
1646
  return result;
1573
1647
  }
1648
+ /**
1649
+ * 按 ToolKind 分类获取工具列表。
1650
+ */
1651
+ listByKind(kind) {
1652
+ return this.list().filter((t) => t.kind === kind);
1653
+ }
1654
+ /**
1655
+ * 获取读工具列表(可并行执行)。
1656
+ */
1657
+ listReadTools() {
1658
+ return this.listByKind("read" /* Read */);
1659
+ }
1660
+ /**
1661
+ * 获取写工具列表(需串行执行)。
1662
+ */
1663
+ listWriteTools() {
1664
+ return this.list().filter((t) => !isReadOnly(t.kind));
1665
+ }
1574
1666
  /**
1575
1667
  * 获取所有已注册的工具名称(含禁用的)。
1576
1668
  */
@@ -1584,10 +1676,10 @@ var ToolRegistry = class {
1584
1676
  return this.#tools.has(name);
1585
1677
  }
1586
1678
  /**
1587
- * 检查工具是否已启用。
1679
+ * 检查工具是否已启用(注册 + 未禁用 + 通过 feature flag + 支持 provider)。
1588
1680
  */
1589
1681
  isEnabled(name) {
1590
- return this.#tools.has(name) && !this.#disabledNames.has(name);
1682
+ return this.#tools.has(name) && this.#isToolEnabled(name);
1591
1683
  }
1592
1684
  /**
1593
1685
  * 禁用一个工具。
@@ -1601,6 +1693,12 @@ var ToolRegistry = class {
1601
1693
  enable(name) {
1602
1694
  this.#disabledNames.delete(name);
1603
1695
  }
1696
+ /**
1697
+ * 获取工具的分类语义。
1698
+ */
1699
+ kindOf(name) {
1700
+ return this.#tools.get(name)?.kind;
1701
+ }
1604
1702
  /**
1605
1703
  * 执行指定工具。
1606
1704
  *
@@ -1626,11 +1724,21 @@ var ToolRegistry = class {
1626
1724
  };
1627
1725
  }
1628
1726
  }
1629
- };
1630
-
1631
- // src/tool/types.ts
1632
- var AlwaysAllowGate = class {
1633
- check(_toolName, _args) {
1727
+ // ---------------------------------------------------------------------------
1728
+ // 内部方法
1729
+ // ---------------------------------------------------------------------------
1730
+ /**
1731
+ * 判断工具是否应该启用。
1732
+ * 依次检查:是否在禁用列表 → 是否通过 Feature Flag → 是否支持当前 Provider。
1733
+ */
1734
+ #isToolEnabled(name) {
1735
+ const tool = this.#tools.get(name);
1736
+ if (!tool) return false;
1737
+ if (this.#disabledNames.has(name)) return false;
1738
+ if (!this.#featureFlagChecker(name)) return false;
1739
+ if (this.#provider && tool.supportedProviders.length > 0) {
1740
+ if (!tool.supportedProviders.includes(this.#provider)) return false;
1741
+ }
1634
1742
  return true;
1635
1743
  }
1636
1744
  };
@@ -1698,7 +1806,7 @@ var Session = class {
1698
1806
  * d. 如果没有工具调用 → 退出循环
1699
1807
  * 3. yield done 事件
1700
1808
  */
1701
- async *chat(userInput) {
1809
+ async *chat(userInput, opts) {
1702
1810
  this.#messages.push({ role: "user", content: userInput });
1703
1811
  const startTime = Date.now();
1704
1812
  let toolRounds = 0;
@@ -1718,7 +1826,11 @@ var Session = class {
1718
1826
  const toolDefs = this.#buildToolDefinitions();
1719
1827
  const stream = this.#provider.chat(apiMessages, {
1720
1828
  signal: this.#abortController.signal,
1721
- tools: toolDefs.length > 0 ? toolDefs : void 0
1829
+ tools: toolDefs.length > 0 ? toolDefs : void 0,
1830
+ thinkingAllowed: opts?.thinkingAllowed,
1831
+ thinkingEffort: opts?.thinkingEffort,
1832
+ responseFormat: opts?.responseFormat,
1833
+ toolChoice: opts?.toolChoice
1722
1834
  });
1723
1835
  let accumulatedText = "";
1724
1836
  let lastUsage;
@@ -1815,11 +1927,12 @@ ${item.result.diff.patch}`;
1815
1927
  async #executeBatch(calls) {
1816
1928
  const toolCtx = {
1817
1929
  cwd: this.#options.cwd,
1818
- signal: this.#abortController.signal
1930
+ signal: this.#abortController.signal,
1931
+ writeRoots: this.#options.writeRoots
1819
1932
  };
1820
1933
  const allReadOnly = calls.every((tc) => {
1821
1934
  const tool = this.#toolRegistry.get(tc.name);
1822
- return tool?.readOnly === true;
1935
+ return tool ? isReadOnly(tool.kind) : true;
1823
1936
  });
1824
1937
  if (allReadOnly && calls.length > 1) {
1825
1938
  const MAX_PARALLEL = 8;
@@ -1873,11 +1986,11 @@ ${item.result.diff.patch}`;
1873
1986
  record: { name: toolName, success: false, error: "GATE_DENIED", timestamp }
1874
1987
  };
1875
1988
  }
1876
- if (!tool.readOnly) {
1877
- const maybePreviewer = tool;
1878
- if (typeof maybePreviewer.preview === "function") {
1989
+ if (!isReadOnly(tool.kind)) {
1990
+ const maybePreview = tool.preview;
1991
+ if (typeof maybePreview === "function") {
1879
1992
  try {
1880
- await maybePreviewer.preview(toolArgs, ctx);
1993
+ await maybePreview(toolArgs, ctx);
1881
1994
  } catch {
1882
1995
  }
1883
1996
  }
@@ -1977,6 +2090,39 @@ function resolvePath(inputPath, cwd) {
1977
2090
  const resolved = isAbsolute(inputPath) ? inputPath : resolve(cwd, inputPath);
1978
2091
  return resolve(resolved);
1979
2092
  }
2093
+ async function realPath(target) {
2094
+ try {
2095
+ return await realpath2(target);
2096
+ } catch {
2097
+ const parent = resolve(target, "..");
2098
+ try {
2099
+ const realParent = await realpath2(parent);
2100
+ return resolve(realParent, relative(parent, target));
2101
+ } catch {
2102
+ return resolve(target);
2103
+ }
2104
+ }
2105
+ }
2106
+ async function confine(allowedRoots, target) {
2107
+ if (allowedRoots.length === 0) {
2108
+ return { ok: true };
2109
+ }
2110
+ const realTarget = await realPath(target);
2111
+ for (const root of allowedRoots) {
2112
+ const realRoot = await realPath(root);
2113
+ const rel = relative(realRoot, realTarget);
2114
+ if (!rel.startsWith("..") && rel !== "" && !rel.startsWith("/") && !rel.startsWith("\\")) {
2115
+ return { ok: true };
2116
+ }
2117
+ if (realTarget === realRoot) {
2118
+ return { ok: true };
2119
+ }
2120
+ }
2121
+ return {
2122
+ ok: false,
2123
+ error: `\u8DEF\u5F84 "${target}" \u4E0D\u5728\u5141\u8BB8\u7684\u5199\u5165\u8303\u56F4\u5185 ${allowedRoots.join(", ")}`
2124
+ };
2125
+ }
1980
2126
  function truncateOutput(content, maxLength = DEFAULT_MAX_OUTPUT_LENGTH) {
1981
2127
  if (content.length <= maxLength) return content;
1982
2128
  const truncated = content.slice(0, maxLength);
@@ -2320,29 +2466,45 @@ function computeFileDiff(oldContent, newContent, filePath) {
2320
2466
  };
2321
2467
  }
2322
2468
 
2469
+ // src/tool/eol.ts
2470
+ import { writeFile as writeFile3 } from "fs/promises";
2471
+ function detectEol(text) {
2472
+ if (text.length === 0) return "\n";
2473
+ const crlfIdx = text.indexOf("\r\n");
2474
+ const lfIdx = text.indexOf("\n");
2475
+ if (crlfIdx !== -1 && (lfIdx === -1 || crlfIdx <= lfIdx)) {
2476
+ return "\r\n";
2477
+ }
2478
+ return "\n";
2479
+ }
2480
+ function hasTrailingNewline(text) {
2481
+ return text.endsWith("\n") || text.endsWith("\r\n");
2482
+ }
2483
+ function normalizeEol(originalContent, newContent) {
2484
+ const targetEol = detectEol(originalContent);
2485
+ const lines = newContent.replace(/\r\n/g, "\n").split("\n");
2486
+ const originalHasTrailing = hasTrailingNewline(originalContent);
2487
+ const newHasTrailing = lines.length > 0 && lines[lines.length - 1] === "";
2488
+ let result = lines.join(targetEol);
2489
+ if (originalHasTrailing && !newHasTrailing) {
2490
+ result += targetEol;
2491
+ }
2492
+ if (!originalHasTrailing && newHasTrailing) {
2493
+ if (result.endsWith(targetEol)) {
2494
+ result = result.slice(0, -targetEol.length);
2495
+ }
2496
+ }
2497
+ return result;
2498
+ }
2499
+ async function writeFileWithEol(filePath, originalContent, newContent) {
2500
+ const content = originalContent.length > 0 ? normalizeEol(originalContent, newContent) : newContent;
2501
+ await writeFile3(filePath, content, "utf-8");
2502
+ }
2503
+
2323
2504
  // src/tool/builtins/read-file.ts
2324
2505
  import { readFile as readFile4, stat } from "fs/promises";
2325
2506
  import { open } from "fs/promises";
2326
2507
  import { relative as relative2 } from "path";
2327
- var readFileSchema = {
2328
- type: "object",
2329
- properties: {
2330
- path: {
2331
- type: "string",
2332
- description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09"
2333
- },
2334
- startLine: {
2335
- type: "number",
2336
- description: "\u8D77\u59CB\u884C\u53F7\uFF08\u4ECE 1 \u5F00\u59CB\uFF09\uFF0C\u9ED8\u8BA4\u4E3A 1"
2337
- },
2338
- endLine: {
2339
- type: "number",
2340
- description: "\u7ED3\u675F\u884C\u53F7\uFF08\u5305\u542B\uFF09\uFF0C\u9ED8\u8BA4\u5230\u6587\u4EF6\u672B\u5C3E"
2341
- }
2342
- },
2343
- required: ["path"],
2344
- additionalProperties: false
2345
- };
2346
2508
  async function checkBinary(filePath) {
2347
2509
  const fileHandle = await open(filePath, "r");
2348
2510
  try {
@@ -2355,15 +2517,32 @@ async function checkBinary(filePath) {
2355
2517
  }
2356
2518
  var readFileTool = {
2357
2519
  name: "read_file",
2520
+ kind: "read" /* Read */,
2358
2521
  description: "\u8BFB\u53D6\u6307\u5B9A\u8DEF\u5F84\u7684\u6587\u4EF6\u5185\u5BB9\u3002\u652F\u6301\u884C\u53F7\u8303\u56F4\u9009\u62E9\uFF0C\u8F93\u51FA\u5E26\u884C\u53F7\u3002\u9002\u7528\u4E8E\u67E5\u770B\u6E90\u4EE3\u7801\u3001\u914D\u7F6E\u6587\u4EF6\u7B49\u6587\u672C\u6587\u4EF6\u3002\u81EA\u52A8\u62D2\u7EDD\u4E8C\u8FDB\u5236\u6587\u4EF6\u3002",
2359
- parameters: readFileSchema,
2360
- readOnly: true,
2522
+ parameters: {
2523
+ type: "object",
2524
+ properties: {
2525
+ path: {
2526
+ type: "string",
2527
+ description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09"
2528
+ },
2529
+ startLine: {
2530
+ type: "number",
2531
+ description: "\u8D77\u59CB\u884C\u53F7\uFF08\u4ECE 1 \u5F00\u59CB\uFF09\uFF0C\u9ED8\u8BA4\u4E3A 1"
2532
+ },
2533
+ endLine: {
2534
+ type: "number",
2535
+ description: "\u7ED3\u675F\u884C\u53F7\uFF08\u5305\u542B\uFF09\uFF0C\u9ED8\u8BA4\u5230\u6587\u4EF6\u672B\u5C3E"
2536
+ }
2537
+ },
2538
+ required: ["path"],
2539
+ additionalProperties: false
2540
+ },
2361
2541
  async execute(args, ctx) {
2362
- const params = args;
2363
- if (!params?.path || typeof params.path !== "string") {
2542
+ if (!args?.path || typeof args.path !== "string") {
2364
2543
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 path", error: "INVALID_ARGS" };
2365
2544
  }
2366
- const filePath = resolvePath(params.path, ctx.cwd);
2545
+ const filePath = resolvePath(args.path, ctx.cwd);
2367
2546
  try {
2368
2547
  const fileStat = await stat(filePath);
2369
2548
  const maxSize = 10 * 1024 * 1024;
@@ -2396,8 +2575,8 @@ var readFileTool = {
2396
2575
  if (lines.length > 0 && lines[lines.length - 1] === "" && content.endsWith("\n")) {
2397
2576
  lines.pop();
2398
2577
  }
2399
- const startLine = Math.max(1, params.startLine ?? 1) - 1;
2400
- const endLine = params.endLine ? Math.min(params.endLine, lines.length) : lines.length;
2578
+ const startLine = Math.max(1, args.startLine ?? 1) - 1;
2579
+ const endLine = args.endLine ? Math.min(args.endLine, lines.length) : lines.length;
2401
2580
  const selectedLines = lines.slice(startLine, endLine);
2402
2581
  const lineNumWidth = String(endLine).length;
2403
2582
  const result = selectedLines.map((line, i) => {
@@ -2427,38 +2606,42 @@ var readFileTool = {
2427
2606
  };
2428
2607
 
2429
2608
  // src/tool/builtins/write-file.ts
2430
- import { writeFile as writeFile3, mkdir as mkdir4, readFile as readFile5 } from "fs/promises";
2609
+ import { mkdir as mkdir4, readFile as readFile5 } from "fs/promises";
2431
2610
  import { dirname, relative as relative3, basename } from "path";
2432
- var writeFileSchema = {
2433
- type: "object",
2434
- properties: {
2435
- path: {
2436
- type: "string",
2437
- description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09"
2438
- },
2439
- content: {
2440
- type: "string",
2441
- description: "\u8981\u5199\u5165\u7684\u6587\u4EF6\u5185\u5BB9"
2442
- }
2443
- },
2444
- required: ["path", "content"],
2445
- additionalProperties: false
2446
- };
2447
2611
  var writeFileTool = {
2448
2612
  name: "write_file",
2613
+ kind: "edit" /* Edit */,
2449
2614
  description: "\u521B\u5EFA\u6216\u8986\u76D6\u6587\u4EF6\u3002\u5982\u679C\u7236\u76EE\u5F55\u4E0D\u5B58\u5728\u4F1A\u81EA\u52A8\u521B\u5EFA\u3002\u9002\u7528\u4E8E\u521B\u5EFA\u65B0\u6587\u4EF6\u6216\u5B8C\u5168\u66FF\u6362\u6587\u4EF6\u5185\u5BB9\u3002\u8BF7\u52FF\u7528\u6B64\u5DE5\u5177\u521B\u5EFA _temp_\u3001_debug_ \u7B49\u7528\u4E8E\u8BCA\u65AD/\u8C03\u8BD5\u7684\u4E34\u65F6\u6587\u4EF6\u2014\u2014\u5982\u9700\u8BCA\u65AD\u8BF7\u6539\u7528 bash \u5DE5\u5177\u5185\u8054\u811A\u672C\uFF08node -e\uFF09\u3002",
2450
- parameters: writeFileSchema,
2451
- readOnly: false,
2615
+ parameters: {
2616
+ type: "object",
2617
+ properties: {
2618
+ path: {
2619
+ type: "string",
2620
+ description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09"
2621
+ },
2622
+ content: {
2623
+ type: "string",
2624
+ description: "\u8981\u5199\u5165\u7684\u6587\u4EF6\u5185\u5BB9"
2625
+ }
2626
+ },
2627
+ required: ["path", "content"],
2628
+ additionalProperties: false
2629
+ },
2452
2630
  async execute(args, ctx) {
2453
- const params = args;
2454
- if (!params?.path || typeof params.path !== "string") {
2631
+ if (!args?.path || typeof args.path !== "string") {
2455
2632
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 path", error: "INVALID_ARGS" };
2456
2633
  }
2457
- if (params.content === void 0 || params.content === null) {
2634
+ if (args.content === void 0 || args.content === null) {
2458
2635
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 content", error: "INVALID_ARGS" };
2459
2636
  }
2460
- const filePath = resolvePath(params.path, ctx.cwd);
2637
+ const filePath = resolvePath(args.path, ctx.cwd);
2461
2638
  try {
2639
+ if (ctx.writeRoots && ctx.writeRoots.length > 0) {
2640
+ const conf = await confine(ctx.writeRoots, filePath);
2641
+ if (!conf.ok) {
2642
+ return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
2643
+ }
2644
+ }
2462
2645
  let oldContent = "";
2463
2646
  let existedBefore = false;
2464
2647
  try {
@@ -2467,8 +2650,8 @@ var writeFileTool = {
2467
2650
  } catch {
2468
2651
  }
2469
2652
  await mkdir4(dirname(filePath), { recursive: true });
2470
- const content = String(params.content);
2471
- await writeFile3(filePath, content, "utf-8");
2653
+ const content = String(args.content);
2654
+ await writeFileWithEol(filePath, oldContent, content);
2472
2655
  const diff = computeFileDiff(oldContent, content, filePath);
2473
2656
  diff.existedBefore = existedBefore;
2474
2657
  const lineCount = content.split("\n").length;
@@ -2495,47 +2678,51 @@ var writeFileTool = {
2495
2678
  };
2496
2679
 
2497
2680
  // src/tool/builtins/edit-file.ts
2498
- import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
2681
+ import { readFile as readFile6 } from "fs/promises";
2499
2682
  import { basename as basename2 } from "path";
2500
- var editFileSchema = {
2501
- type: "object",
2502
- properties: {
2503
- path: {
2504
- type: "string",
2505
- description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09"
2506
- },
2507
- old_text: {
2508
- type: "string",
2509
- description: "\u8981\u67E5\u627E\u7684\u539F\u59CB\u6587\u672C\uFF08\u7CBE\u786E\u5339\u914D\uFF09"
2510
- },
2511
- new_text: {
2512
- type: "string",
2513
- description: "\u66FF\u6362\u540E\u7684\u65B0\u6587\u672C"
2514
- }
2515
- },
2516
- required: ["path", "old_text", "new_text"],
2517
- additionalProperties: false
2518
- };
2519
2683
  var editFileTool = {
2520
2684
  name: "edit_file",
2685
+ kind: "edit" /* Edit */,
2521
2686
  description: "\u5BF9\u6587\u4EF6\u8FDB\u884C\u7CBE\u786E\u5B57\u7B26\u4E32\u66FF\u6362\u3002\u67E5\u627E\u6587\u4EF6\u4E2D\u7684 old_text \u5E76\u66FF\u6362\u4E3A new_text\u3002\u5982\u679C old_text \u51FA\u73B0\u591A\u6B21\u6216\u672A\u627E\u5230\u5219\u62A5\u9519\u3002\u9002\u7528\u4E8E\u5C0F\u8303\u56F4\u7CBE\u786E\u4FEE\u6539\u3002",
2522
- parameters: editFileSchema,
2523
- readOnly: false,
2687
+ parameters: {
2688
+ type: "object",
2689
+ properties: {
2690
+ path: {
2691
+ type: "string",
2692
+ description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09"
2693
+ },
2694
+ old_text: {
2695
+ type: "string",
2696
+ description: "\u8981\u67E5\u627E\u7684\u539F\u59CB\u6587\u672C\uFF08\u7CBE\u786E\u5339\u914D\uFF09"
2697
+ },
2698
+ new_text: {
2699
+ type: "string",
2700
+ description: "\u66FF\u6362\u540E\u7684\u65B0\u6587\u672C"
2701
+ }
2702
+ },
2703
+ required: ["path", "old_text", "new_text"],
2704
+ additionalProperties: false
2705
+ },
2524
2706
  async execute(args, ctx) {
2525
- const params = args;
2526
- if (!params?.path || typeof params.path !== "string") {
2707
+ if (!args?.path || typeof args.path !== "string") {
2527
2708
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 path", error: "INVALID_ARGS" };
2528
2709
  }
2529
- if (typeof params.old_text !== "string") {
2710
+ if (typeof args.old_text !== "string") {
2530
2711
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 old_text", error: "INVALID_ARGS" };
2531
2712
  }
2532
- if (typeof params.new_text !== "string") {
2713
+ if (typeof args.new_text !== "string") {
2533
2714
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 new_text", error: "INVALID_ARGS" };
2534
2715
  }
2535
- const filePath = resolvePath(params.path, ctx.cwd);
2716
+ const filePath = resolvePath(args.path, ctx.cwd);
2717
+ if (ctx.writeRoots && ctx.writeRoots.length > 0) {
2718
+ const conf = await confine(ctx.writeRoots, filePath);
2719
+ if (!conf.ok) {
2720
+ return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
2721
+ }
2722
+ }
2536
2723
  try {
2537
2724
  const content = await readFile6(filePath, "utf-8");
2538
- const firstIndex = content.indexOf(params.old_text);
2725
+ const firstIndex = content.indexOf(args.old_text);
2539
2726
  if (firstIndex === -1) {
2540
2727
  return {
2541
2728
  success: false,
@@ -2543,7 +2730,7 @@ var editFileTool = {
2543
2730
  error: "TEXT_NOT_FOUND"
2544
2731
  };
2545
2732
  }
2546
- const secondIndex = content.indexOf(params.old_text, firstIndex + 1);
2733
+ const secondIndex = content.indexOf(args.old_text, firstIndex + 1);
2547
2734
  if (secondIndex !== -1) {
2548
2735
  return {
2549
2736
  success: false,
@@ -2551,14 +2738,14 @@ var editFileTool = {
2551
2738
  error: "TEXT_MULTIPLE_MATCHES"
2552
2739
  };
2553
2740
  }
2554
- const newContent = content.replace(params.old_text, params.new_text);
2555
- await writeFile4(filePath, newContent, "utf-8");
2741
+ const newContent = content.replace(args.old_text, args.new_text);
2742
+ await writeFileWithEol(filePath, content, newContent);
2556
2743
  const diff = computeFileDiff(content, newContent, filePath);
2557
2744
  diff.existedBefore = true;
2558
2745
  const beforeText = content.slice(0, firstIndex);
2559
2746
  const startLine = beforeText.split("\n").length;
2560
- const oldLines = params.old_text.split("\n").length;
2561
- const newLines = params.new_text.split("\n").length;
2747
+ const oldLines = args.old_text.split("\n").length;
2748
+ const newLines = args.new_text.split("\n").length;
2562
2749
  const diffSummary = `+${diff.additions} -${diff.deletions}`;
2563
2750
  const summary = `\u{1F4DD} \u4FEE\u6539: ${basename2(filePath)} (+${diff.additions} -${diff.deletions})`;
2564
2751
  return {
@@ -2582,52 +2769,56 @@ ${oldLines} \u884C \u2192 ${newLines} \u884C
2582
2769
  };
2583
2770
 
2584
2771
  // src/tool/builtins/multi-edit.ts
2585
- import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
2772
+ import { readFile as readFile7 } from "fs/promises";
2586
2773
  import { basename as basename3 } from "path";
2587
- var multiEditSchema = {
2588
- type: "object",
2589
- properties: {
2590
- path: {
2591
- type: "string",
2592
- description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09"
2593
- },
2594
- edits: {
2595
- type: "array",
2596
- description: "\u6709\u5E8F\u7684\u7F16\u8F91\u6B65\u9AA4\u5217\u8868\u3002\u6BCF\u4E2A\u6B65\u9AA4\u5305\u542B oldText\uFF08\u7CBE\u786E\u5339\u914D\uFF09\u3001newText\uFF08\u66FF\u6362\u5185\u5BB9\uFF09\u548C\u53EF\u9009\u7684 replaceAll\uFF08\u662F\u5426\u66FF\u6362\u6240\u6709\u5339\u914D\u9879\uFF09",
2597
- items: {
2598
- type: "object",
2599
- properties: {
2600
- oldText: { type: "string", description: "\u8981\u67E5\u627E\u7684\u539F\u59CB\u6587\u672C\uFF08\u7CBE\u786E\u5339\u914D\uFF09" },
2601
- newText: { type: "string", description: "\u66FF\u6362\u540E\u7684\u65B0\u6587\u672C" },
2602
- replaceAll: { type: "boolean", description: "\u662F\u5426\u66FF\u6362\u6240\u6709\u5339\u914D\u9879\uFF0C\u9ED8\u8BA4 false" }
2603
- },
2604
- required: ["oldText", "newText"],
2605
- additionalProperties: false
2606
- }
2607
- }
2608
- },
2609
- required: ["path", "edits"],
2610
- additionalProperties: false
2611
- };
2612
2774
  var multiEditTool = {
2613
2775
  name: "multi_edit",
2776
+ kind: "edit" /* Edit */,
2614
2777
  description: "\u5BF9\u6587\u4EF6\u8FDB\u884C\u539F\u5B50\u6279\u91CF\u66FF\u6362\u7F16\u8F91\u3002\u5728\u4E00\u4E2A\u8BF7\u6C42\u4E2D\u6267\u884C\u591A\u4E2A\u7CBE\u786E\u66FF\u6362\uFF0C\u4EFB\u4E00\u5931\u8D25\u5219\u5168\u90E8\u56DE\u6EDA\u3002\u652F\u6301 replaceAll \u53C2\u6570\u505A\u5168\u5C40\u66FF\u6362\u3002\u9002\u7528\u4E8E\u9700\u8981\u540C\u65F6\u4FEE\u6539\u6587\u4EF6\u591A\u5904\u7684\u573A\u666F\u3002",
2615
- parameters: multiEditSchema,
2616
- readOnly: false,
2778
+ parameters: {
2779
+ type: "object",
2780
+ properties: {
2781
+ path: {
2782
+ type: "string",
2783
+ description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09"
2784
+ },
2785
+ edits: {
2786
+ type: "array",
2787
+ description: "\u6709\u5E8F\u7684\u7F16\u8F91\u6B65\u9AA4\u5217\u8868\u3002\u6BCF\u4E2A\u6B65\u9AA4\u5305\u542B oldText\uFF08\u7CBE\u786E\u5339\u914D\uFF09\u3001newText\uFF08\u66FF\u6362\u5185\u5BB9\uFF09\u548C\u53EF\u9009\u7684 replaceAll\uFF08\u662F\u5426\u66FF\u6362\u6240\u6709\u5339\u914D\u9879\uFF09",
2788
+ items: {
2789
+ type: "object",
2790
+ properties: {
2791
+ oldText: { type: "string", description: "\u8981\u67E5\u627E\u7684\u539F\u59CB\u6587\u672C\uFF08\u7CBE\u786E\u5339\u914D\uFF09" },
2792
+ newText: { type: "string", description: "\u66FF\u6362\u540E\u7684\u65B0\u6587\u672C" },
2793
+ replaceAll: { type: "boolean", description: "\u662F\u5426\u66FF\u6362\u6240\u6709\u5339\u914D\u9879\uFF0C\u9ED8\u8BA4 false" }
2794
+ },
2795
+ required: ["oldText", "newText"],
2796
+ additionalProperties: false
2797
+ }
2798
+ }
2799
+ },
2800
+ required: ["path", "edits"],
2801
+ additionalProperties: false
2802
+ },
2617
2803
  async execute(args, ctx) {
2618
- const params = args;
2619
- if (!params?.path || typeof params.path !== "string") {
2804
+ if (!args?.path || typeof args.path !== "string") {
2620
2805
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 path", error: "INVALID_ARGS" };
2621
2806
  }
2622
- if (!Array.isArray(params.edits) || params.edits.length === 0) {
2807
+ if (!Array.isArray(args.edits) || args.edits.length === 0) {
2623
2808
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 edits\uFF08\u975E\u7A7A\u6570\u7EC4\uFF09", error: "INVALID_ARGS" };
2624
2809
  }
2625
- const filePath = resolvePath(params.path, ctx.cwd);
2810
+ const filePath = resolvePath(args.path, ctx.cwd);
2811
+ if (ctx.writeRoots && ctx.writeRoots.length > 0) {
2812
+ const conf = await confine(ctx.writeRoots, filePath);
2813
+ if (!conf.ok) {
2814
+ return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
2815
+ }
2816
+ }
2626
2817
  try {
2627
2818
  const originalContent = await readFile7(filePath, "utf-8");
2628
2819
  let currentContent = originalContent;
2629
- for (let idx = 0; idx < params.edits.length; idx++) {
2630
- const step = params.edits[idx];
2820
+ for (let idx = 0; idx < args.edits.length; idx++) {
2821
+ const step = args.edits[idx];
2631
2822
  if (typeof step.oldText !== "string") {
2632
2823
  return {
2633
2824
  success: false,
@@ -2669,15 +2860,15 @@ var multiEditTool = {
2669
2860
  currentContent = currentContent.replace(step.oldText, step.newText);
2670
2861
  }
2671
2862
  }
2672
- await writeFile5(filePath, currentContent, "utf-8");
2863
+ await writeFileWithEol(filePath, originalContent, currentContent);
2673
2864
  const diff = computeFileDiff(originalContent, currentContent, filePath);
2674
2865
  diff.existedBefore = true;
2675
2866
  return {
2676
2867
  success: true,
2677
2868
  data: `\u6587\u4EF6\u5DF2\u7F16\u8F91\uFF1A${filePath}
2678
- \u5171\u6267\u884C ${params.edits.length} \u6B65\u66FF\u6362
2869
+ \u5171\u6267\u884C ${args.edits.length} \u6B65\u66FF\u6362
2679
2870
  \u53D8\u66F4\uFF1A+${diff.additions} -${diff.deletions}`,
2680
- summary: `\u{1F4DD} \u4FEE\u6539: ${basename3(filePath)} (${params.edits.length} \u6B65, +${diff.additions} -${diff.deletions})`,
2871
+ summary: `\u{1F4DD} \u4FEE\u6539: ${basename3(filePath)} (${args.edits.length} \u6B65, +${diff.additions} -${diff.deletions})`,
2681
2872
  diff
2682
2873
  };
2683
2874
  } catch (err) {
@@ -2692,31 +2883,8 @@ var multiEditTool = {
2692
2883
  };
2693
2884
 
2694
2885
  // src/tool/builtins/delete-range.ts
2695
- import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
2886
+ import { readFile as readFile8 } from "fs/promises";
2696
2887
  import { basename as basename4 } from "path";
2697
- var deleteRangeSchema = {
2698
- type: "object",
2699
- properties: {
2700
- path: {
2701
- type: "string",
2702
- description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09"
2703
- },
2704
- startAnchor: {
2705
- type: "string",
2706
- description: "\u5220\u9664\u8303\u56F4\u7684\u8D77\u59CB\u6807\u8BB0\u884C\u5185\u5BB9\uFF08\u5FC5\u987B\u5728\u6587\u4EF6\u4E2D\u552F\u4E00\uFF0C\u7CBE\u786E\u5339\u914D\u6574\u884C\uFF09"
2707
- },
2708
- endAnchor: {
2709
- type: "string",
2710
- description: "\u5220\u9664\u8303\u56F4\u7684\u7ED3\u675F\u6807\u8BB0\u884C\u5185\u5BB9\uFF08\u5FC5\u987B\u5728\u6587\u4EF6\u4E2D\u552F\u4E00\uFF0C\u7CBE\u786E\u5339\u914D\u6574\u884C\uFF0C\u987B\u5728 start_anchor \u4E4B\u540E\uFF09"
2711
- },
2712
- inclusive: {
2713
- type: "boolean",
2714
- description: "\u662F\u5426\u5305\u542B\u951A\u70B9\u884C\u672C\u8EAB\uFF0C\u9ED8\u8BA4 false\uFF08\u53EA\u5220\u9664\u4E24\u884C\u4E4B\u95F4\u7684\u5185\u5BB9\uFF09"
2715
- }
2716
- },
2717
- required: ["path", "startAnchor", "endAnchor"],
2718
- additionalProperties: false
2719
- };
2720
2888
  function findUniqueLine(lines, anchor, label) {
2721
2889
  const matches = [];
2722
2890
  for (let i = 0; i < lines.length; i++) {
@@ -2734,30 +2902,57 @@ function findUniqueLine(lines, anchor, label) {
2734
2902
  }
2735
2903
  var deleteRangeTool = {
2736
2904
  name: "delete_range",
2905
+ kind: "edit" /* Edit */,
2737
2906
  description: "\u5220\u9664\u6587\u4EF6\u4E2D\u4E24\u4E2A\u951A\u70B9\u884C\u4E4B\u95F4\u7684\u5185\u5BB9\u3002\u901A\u8FC7\u4E24\u4E2A\u552F\u4E00\u7684\u884C\u5185\u5BB9\u7CBE\u786E\u5B9A\u4F4D\u8303\u56F4\u3002\u9002\u7528\u4E8E\u5220\u9664\u65B9\u6CD5\u3001\u4EE3\u7801\u5757\u3001\u914D\u7F6E\u6BB5\u7B49\u3002",
2738
- parameters: deleteRangeSchema,
2739
- readOnly: false,
2907
+ parameters: {
2908
+ type: "object",
2909
+ properties: {
2910
+ path: {
2911
+ type: "string",
2912
+ description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09"
2913
+ },
2914
+ startAnchor: {
2915
+ type: "string",
2916
+ description: "\u5220\u9664\u8303\u56F4\u7684\u8D77\u59CB\u6807\u8BB0\u884C\u5185\u5BB9\uFF08\u5FC5\u987B\u5728\u6587\u4EF6\u4E2D\u552F\u4E00\uFF0C\u7CBE\u786E\u5339\u914D\u6574\u884C\uFF09"
2917
+ },
2918
+ endAnchor: {
2919
+ type: "string",
2920
+ description: "\u5220\u9664\u8303\u56F4\u7684\u7ED3\u675F\u6807\u8BB0\u884C\u5185\u5BB9\uFF08\u5FC5\u987B\u5728\u6587\u4EF6\u4E2D\u552F\u4E00\uFF0C\u7CBE\u786E\u5339\u914D\u6574\u884C\uFF0C\u987B\u5728 start_anchor \u4E4B\u540E\uFF09"
2921
+ },
2922
+ inclusive: {
2923
+ type: "boolean",
2924
+ description: "\u662F\u5426\u5305\u542B\u951A\u70B9\u884C\u672C\u8EAB\uFF0C\u9ED8\u8BA4 false\uFF08\u53EA\u5220\u9664\u4E24\u884C\u4E4B\u95F4\u7684\u5185\u5BB9\uFF09"
2925
+ }
2926
+ },
2927
+ required: ["path", "startAnchor", "endAnchor"],
2928
+ additionalProperties: false
2929
+ },
2740
2930
  async execute(args, ctx) {
2741
- const params = args;
2742
- if (!params?.path || typeof params.path !== "string") {
2931
+ if (!args?.path || typeof args.path !== "string") {
2743
2932
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 path", error: "INVALID_ARGS" };
2744
2933
  }
2745
- if (typeof params.startAnchor !== "string") {
2934
+ if (typeof args.startAnchor !== "string") {
2746
2935
  return { success: false, data: "\u7F3A\u5C11\u6709\u6548\u53C2\u6570 startAnchor", error: "INVALID_ARGS" };
2747
2936
  }
2748
- if (typeof params.endAnchor !== "string") {
2937
+ if (typeof args.endAnchor !== "string") {
2749
2938
  return { success: false, data: "\u7F3A\u5C11\u6709\u6548\u53C2\u6570 endAnchor", error: "INVALID_ARGS" };
2750
2939
  }
2751
- const filePath = resolvePath(params.path, ctx.cwd);
2752
- const inclusive = params.inclusive ?? false;
2940
+ const filePath = resolvePath(args.path, ctx.cwd);
2941
+ const inclusive = args.inclusive ?? false;
2942
+ if (ctx.writeRoots && ctx.writeRoots.length > 0) {
2943
+ const conf = await confine(ctx.writeRoots, filePath);
2944
+ if (!conf.ok) {
2945
+ return { success: false, data: conf.error, error: "OUTSIDE_WRITE_ROOTS" };
2946
+ }
2947
+ }
2753
2948
  try {
2754
2949
  const content = await readFile8(filePath, "utf-8");
2755
2950
  const lines = content.split("\n");
2756
- const startResult = findUniqueLine(lines, params.startAnchor, "start_anchor");
2951
+ const startResult = findUniqueLine(lines, args.startAnchor, "start_anchor");
2757
2952
  if ("error" in startResult) {
2758
2953
  return { success: false, data: startResult.error, error: "ANCHOR_NOT_FOUND" };
2759
2954
  }
2760
- const endResult = findUniqueLine(lines, params.endAnchor, "end_anchor");
2955
+ const endResult = findUniqueLine(lines, args.endAnchor, "end_anchor");
2761
2956
  if ("error" in endResult) {
2762
2957
  return { success: false, data: endResult.error, error: "ANCHOR_NOT_FOUND" };
2763
2958
  }
@@ -2781,14 +2976,8 @@ var deleteRangeTool = {
2781
2976
  }
2782
2977
  const newLines = [...lines.slice(0, rangeStart), ...lines.slice(rangeEnd + 1)];
2783
2978
  const newContent = newLines.join("\n");
2784
- let writeContent = newContent;
2785
- if (content.endsWith("\n") && !newContent.endsWith("\n")) {
2786
- writeContent = newContent + "\n";
2787
- } else if (content.endsWith("\r\n") && !newContent.endsWith("\r\n") && !newContent.endsWith("\n")) {
2788
- writeContent = newContent + "\r\n";
2789
- }
2790
- await writeFile6(filePath, writeContent, "utf-8");
2791
- const diff = computeFileDiff(content, writeContent, filePath);
2979
+ await writeFileWithEol(filePath, content, newContent);
2980
+ const diff = computeFileDiff(content, newContent, filePath);
2792
2981
  diff.existedBefore = true;
2793
2982
  const deletedLines = rangeEnd - rangeStart + 1;
2794
2983
  const summary = `\u{1F4DD} \u4FEE\u6539: ${basename4(filePath)} (\u5220 ${deletedLines} \u884C, +${diff.additions} -${diff.deletions})`;
@@ -2814,36 +3003,33 @@ var deleteRangeTool = {
2814
3003
  // src/tool/builtins/bash.ts
2815
3004
  import process3 from "process";
2816
3005
  var isWindows2 = process3.platform === "win32";
2817
- var bashSchema = {
2818
- type: "object",
2819
- properties: {
2820
- command: {
2821
- type: "string",
2822
- description: "\u8981\u6267\u884C\u7684 shell \u547D\u4EE4"
2823
- },
2824
- timeout: {
2825
- type: "number",
2826
- description: "\u6267\u884C\u8D85\u65F6\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF09\uFF0C\u9ED8\u8BA4 30000"
2827
- }
2828
- },
2829
- required: ["command"],
2830
- additionalProperties: false
2831
- };
2832
3006
  var bashTool = {
2833
3007
  name: "bash",
3008
+ kind: "other" /* Other */,
2834
3009
  description: "\u5728 shell \u4E2D\u6267\u884C\u547D\u4EE4\u3002\u8FD4\u56DE\u6807\u51C6\u8F93\u51FA\u3001\u6807\u51C6\u9519\u8BEF\u548C\u9000\u51FA\u7801\u3002\u652F\u6301\u8D85\u65F6\u63A7\u5236\u548C\u4FE1\u53F7\u4E2D\u6B62\u3002\u9002\u7528\u4E8E\u8FD0\u884C\u6784\u5EFA\u3001\u6D4B\u8BD5\u3001Git \u64CD\u4F5C\u7B49\u547D\u4EE4\u3002",
2835
- parameters: bashSchema,
2836
- readOnly: false,
2837
- // bash 有副作用,不能并行执行
3010
+ parameters: {
3011
+ type: "object",
3012
+ properties: {
3013
+ command: {
3014
+ type: "string",
3015
+ description: "\u8981\u6267\u884C\u7684 shell \u547D\u4EE4"
3016
+ },
3017
+ timeout: {
3018
+ type: "number",
3019
+ description: "\u6267\u884C\u8D85\u65F6\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF09\uFF0C\u9ED8\u8BA4 30000"
3020
+ }
3021
+ },
3022
+ required: ["command"],
3023
+ additionalProperties: false
3024
+ },
2838
3025
  async execute(args, ctx) {
2839
- const params = args;
2840
- if (!params?.command || typeof params.command !== "string") {
3026
+ if (!args?.command || typeof args.command !== "string") {
2841
3027
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 command", error: "INVALID_ARGS" };
2842
3028
  }
2843
- const timeout = params.timeout ?? ctx.timeout ?? getDefaultTimeout();
3029
+ const timeout = args.timeout ?? ctx.timeout ?? getDefaultTimeout();
2844
3030
  try {
2845
3031
  const shellCommand = isWindows2 ? "cmd" : "sh";
2846
- const shellArgs = isWindows2 ? ["/c", params.command] : ["-c", params.command];
3032
+ const shellArgs = isWindows2 ? ["/c", args.command] : ["-c", args.command];
2847
3033
  const result = await execCommand(
2848
3034
  shellCommand,
2849
3035
  shellArgs,
@@ -2851,7 +3037,6 @@ var bashTool = {
2851
3037
  timeout,
2852
3038
  ctx.signal,
2853
3039
  true
2854
- // isShellCommand — 已指定 shell 程序,不需要二次包装
2855
3040
  );
2856
3041
  const parts = [];
2857
3042
  if (result.stdout) {
@@ -2863,7 +3048,7 @@ ${truncateOutput(result.stderr)}`);
2863
3048
  }
2864
3049
  const success = result.exitCode === 0;
2865
3050
  const output = parts.length > 0 ? parts.join("\n") : "(\u65E0\u8F93\u51FA)";
2866
- const cmdPreview = params.command.length > 60 ? params.command.slice(0, 57) + "..." : params.command;
3051
+ const cmdPreview = args.command.length > 60 ? args.command.slice(0, 57) + "..." : args.command;
2867
3052
  const summary = `\u{1F527} $ ${cmdPreview}\uFF08exit ${result.exitCode ?? "\u672A\u77E5"}\uFF09`;
2868
3053
  return {
2869
3054
  success,
@@ -2886,21 +3071,6 @@ ${truncateOutput(result.stderr)}`);
2886
3071
  // src/tool/builtins/glob.ts
2887
3072
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
2888
3073
  import { join as join4, relative as relative4, isAbsolute as isAbsolute2 } from "path";
2889
- var globSchema = {
2890
- type: "object",
2891
- properties: {
2892
- pattern: {
2893
- type: "string",
2894
- description: "\u641C\u7D22\u6A21\u5F0F\uFF08\u652F\u6301 * \u548C ** \u901A\u914D\u7B26\uFF0C\u5982 **/*.ts\u3001src/**/*.test.ts\uFF09"
2895
- },
2896
- directory: {
2897
- type: "string",
2898
- description: "\u641C\u7D22\u7684\u8D77\u59CB\u76EE\u5F55\uFF0C\u9ED8\u8BA4\u4E3A\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55"
2899
- }
2900
- },
2901
- required: ["pattern"],
2902
- additionalProperties: false
2903
- };
2904
3074
  function globToRegex(pattern) {
2905
3075
  let regexStr = pattern;
2906
3076
  regexStr = regexStr.replace(/\*\*\//g, "<<GLOBSTAR_SLASH>>");
@@ -2937,16 +3107,29 @@ async function walkDir(dir, baseDir) {
2937
3107
  }
2938
3108
  var globTool = {
2939
3109
  name: "glob",
3110
+ kind: "read" /* Read */,
2940
3111
  description: "\u6309\u6A21\u5F0F\u641C\u7D22\u6587\u4EF6\u8DEF\u5F84\u3002\u652F\u6301 *\uFF08\u5339\u914D\u6587\u4EF6\u540D\u90E8\u5206\uFF09\u548C **\uFF08\u5339\u914D\u591A\u5C42\u76EE\u5F55\uFF09\u901A\u914D\u7B26\u3002\u81EA\u52A8\u8DF3\u8FC7 node_modules \u548C .git \u76EE\u5F55\u3002",
2941
- parameters: globSchema,
2942
- readOnly: true,
3112
+ parameters: {
3113
+ type: "object",
3114
+ properties: {
3115
+ pattern: {
3116
+ type: "string",
3117
+ description: "\u641C\u7D22\u6A21\u5F0F\uFF08\u652F\u6301 * \u548C ** \u901A\u914D\u7B26\uFF0C\u5982 **/*.ts\u3001src/**/*.test.ts\uFF09"
3118
+ },
3119
+ directory: {
3120
+ type: "string",
3121
+ description: "\u641C\u7D22\u7684\u8D77\u59CB\u76EE\u5F55\uFF0C\u9ED8\u8BA4\u4E3A\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55"
3122
+ }
3123
+ },
3124
+ required: ["pattern"],
3125
+ additionalProperties: false
3126
+ },
2943
3127
  async execute(args, ctx) {
2944
- const params = args;
2945
- if (!params?.pattern || typeof params.pattern !== "string") {
3128
+ if (!args?.pattern || typeof args.pattern !== "string") {
2946
3129
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 pattern", error: "INVALID_ARGS" };
2947
3130
  }
2948
- const searchDir = params.directory ? isAbsolute2(params.directory) ? params.directory : join4(ctx.cwd, params.directory) : ctx.cwd;
2949
- const regex = globToRegex(params.pattern);
3131
+ const searchDir = args.directory ? isAbsolute2(args.directory) ? args.directory : join4(ctx.cwd, args.directory) : ctx.cwd;
3132
+ const regex = globToRegex(args.pattern);
2950
3133
  try {
2951
3134
  const dirStat = await stat2(searchDir);
2952
3135
  if (!dirStat.isDirectory()) {
@@ -2957,8 +3140,8 @@ var globTool = {
2957
3140
  if (matched.length === 0) {
2958
3141
  return {
2959
3142
  success: true,
2960
- data: `\u672A\u627E\u5230\u5339\u914D "${params.pattern}" \u7684\u6587\u4EF6`,
2961
- summary: `${params.pattern} \u2192 0 \u4E2A\u6587\u4EF6`
3143
+ data: `\u672A\u627E\u5230\u5339\u914D "${args.pattern}" \u7684\u6587\u4EF6`,
3144
+ summary: `${args.pattern} \u2192 0 \u4E2A\u6587\u4EF6`
2962
3145
  };
2963
3146
  }
2964
3147
  const limit = 200;
@@ -2970,7 +3153,7 @@ var globTool = {
2970
3153
  return {
2971
3154
  success: true,
2972
3155
  data: truncateOutput(output + suffix),
2973
- summary: `${params.pattern} \u2192 ${matched.length} \u4E2A\u6587\u4EF6`
3156
+ summary: `${args.pattern} \u2192 ${matched.length} \u4E2A\u6587\u4EF6`
2974
3157
  };
2975
3158
  } catch (err) {
2976
3159
  const message = err instanceof Error ? err.message : String(err);
@@ -2986,33 +3169,6 @@ var globTool = {
2986
3169
  // src/tool/builtins/grep.ts
2987
3170
  import { readdir as readdir3, readFile as readFile9, stat as stat3 } from "fs/promises";
2988
3171
  import { join as join5, relative as relative5, isAbsolute as isAbsolute3 } from "path";
2989
- var grepSchema = {
2990
- type: "object",
2991
- properties: {
2992
- pattern: {
2993
- type: "string",
2994
- description: "\u6B63\u5219\u8868\u8FBE\u5F0F\u641C\u7D22\u6A21\u5F0F"
2995
- },
2996
- directory: {
2997
- type: "string",
2998
- description: "\u641C\u7D22\u7684\u76EE\u5F55\u8DEF\u5F84\uFF0C\u9ED8\u8BA4\u4E3A\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55"
2999
- },
3000
- include: {
3001
- type: "string",
3002
- description: "\u6587\u4EF6\u6269\u5C55\u540D\u8FC7\u6EE4\uFF08\u5982 ts\u3001json\uFF09\uFF0C\u4E0D\u542B\u70B9\u53F7"
3003
- },
3004
- case_sensitive: {
3005
- type: "boolean",
3006
- description: "\u662F\u5426\u5927\u5C0F\u5199\u654F\u611F\uFF0C\u9ED8\u8BA4 false"
3007
- },
3008
- max_files: {
3009
- type: "number",
3010
- description: "\u6700\u5927\u641C\u7D22\u6587\u4EF6\u6570\uFF0C\u9ED8\u8BA4 200"
3011
- }
3012
- },
3013
- required: ["pattern"],
3014
- additionalProperties: false
3015
- };
3016
3172
  async function collectFiles(dir, baseDir, extension, maxFiles = 200) {
3017
3173
  const results = [];
3018
3174
  let entries;
@@ -3040,24 +3196,49 @@ async function collectFiles(dir, baseDir, extension, maxFiles = 200) {
3040
3196
  }
3041
3197
  var grepTool = {
3042
3198
  name: "grep",
3199
+ kind: "read" /* Read */,
3043
3200
  description: "\u5728\u6587\u4EF6\u5185\u5BB9\u4E2D\u641C\u7D22\u6B63\u5219\u8868\u8FBE\u5F0F\u3002\u8FD4\u56DE\u5339\u914D\u884C\u7684\u6587\u4EF6\u8DEF\u5F84\u3001\u884C\u53F7\u548C\u5185\u5BB9\u3002\u652F\u6301\u5927\u5C0F\u5199\u654F\u611F\u3001\u6587\u4EF6\u6269\u5C55\u540D\u8FC7\u6EE4\u3002",
3044
- parameters: grepSchema,
3045
- readOnly: true,
3201
+ parameters: {
3202
+ type: "object",
3203
+ properties: {
3204
+ pattern: {
3205
+ type: "string",
3206
+ description: "\u6B63\u5219\u8868\u8FBE\u5F0F\u641C\u7D22\u6A21\u5F0F"
3207
+ },
3208
+ directory: {
3209
+ type: "string",
3210
+ description: "\u641C\u7D22\u7684\u76EE\u5F55\u8DEF\u5F84\uFF0C\u9ED8\u8BA4\u4E3A\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55"
3211
+ },
3212
+ include: {
3213
+ type: "string",
3214
+ description: "\u6587\u4EF6\u6269\u5C55\u540D\u8FC7\u6EE4\uFF08\u5982 ts\u3001json\uFF09\uFF0C\u4E0D\u542B\u70B9\u53F7"
3215
+ },
3216
+ case_sensitive: {
3217
+ type: "boolean",
3218
+ description: "\u662F\u5426\u5927\u5C0F\u5199\u654F\u611F\uFF0C\u9ED8\u8BA4 false"
3219
+ },
3220
+ max_files: {
3221
+ type: "number",
3222
+ description: "\u6700\u5927\u641C\u7D22\u6587\u4EF6\u6570\uFF0C\u9ED8\u8BA4 200"
3223
+ }
3224
+ },
3225
+ required: ["pattern"],
3226
+ additionalProperties: false
3227
+ },
3046
3228
  async execute(args, ctx) {
3047
- const params = args;
3048
- if (!params?.pattern || typeof params.pattern !== "string") {
3229
+ if (!args?.pattern || typeof args.pattern !== "string") {
3049
3230
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 pattern", error: "INVALID_ARGS" };
3050
3231
  }
3051
- const searchDir = params.directory ? isAbsolute3(params.directory) ? params.directory : join5(ctx.cwd, params.directory) : ctx.cwd;
3052
- const maxFiles = params.max_files ?? 200;
3232
+ const searchDir = args.directory ? isAbsolute3(args.directory) ? args.directory : join5(ctx.cwd, args.directory) : ctx.cwd;
3233
+ const maxFiles = args.max_files ?? 200;
3053
3234
  try {
3054
- const flags = params.case_sensitive ? "g" : "gi";
3055
- const regex = new RegExp(params.pattern, flags);
3235
+ const flags = args.case_sensitive ? "g" : "gi";
3236
+ const regex = new RegExp(args.pattern, flags);
3056
3237
  const dirStat = await stat3(searchDir);
3057
3238
  if (!dirStat.isDirectory()) {
3058
3239
  return { success: false, data: `\u8DEF\u5F84\u4E0D\u662F\u76EE\u5F55\uFF1A${searchDir}`, error: "NOT_DIRECTORY" };
3059
3240
  }
3060
- const files = await collectFiles(searchDir, searchDir, params.include, maxFiles);
3241
+ const files = await collectFiles(searchDir, searchDir, args.include, maxFiles);
3061
3242
  const matches = [];
3062
3243
  for (const filePath of files) {
3063
3244
  try {
@@ -3083,13 +3264,13 @@ var grepTool = {
3083
3264
  if (matches.length === 0) {
3084
3265
  return {
3085
3266
  success: true,
3086
- data: `\u672A\u627E\u5230\u5339\u914D "${params.pattern}" \u7684\u5185\u5BB9`,
3087
- summary: `\u{1F50D} "${params.pattern}" \u2192 0 \u6761\u547D\u4E2D`
3267
+ data: `\u672A\u627E\u5230\u5339\u914D "${args.pattern}" \u7684\u5185\u5BB9`,
3268
+ summary: `\u{1F50D} "${args.pattern}" \u2192 0 \u6761\u547D\u4E2D`
3088
3269
  };
3089
3270
  }
3090
3271
  const output = matches.map((m) => `${m.file}:${m.line}: ${m.content}`).join("\n");
3091
3272
  const fileSet = new Set(matches.map((m) => m.file));
3092
- const summary = `\u{1F50D} "${params.pattern}" \u2192 ${matches.length} \u6761\u547D\u4E2D / ${fileSet.size} \u4E2A\u6587\u4EF6`;
3273
+ const summary = `\u{1F50D} "${args.pattern}" \u2192 ${matches.length} \u6761\u547D\u4E2D / ${fileSet.size} \u4E2A\u6587\u4EF6`;
3093
3274
  return {
3094
3275
  success: true,
3095
3276
  data: truncateOutput(output),
@@ -3109,30 +3290,28 @@ var grepTool = {
3109
3290
  // src/tool/builtins/ls.ts
3110
3291
  import { readdir as readdir4, stat as stat4 } from "fs/promises";
3111
3292
  import { join as join6, relative as relative6 } from "path";
3112
- var lsSchema = {
3113
- type: "object",
3114
- properties: {
3115
- path: {
3116
- type: "string",
3117
- description: "\u76EE\u5F55\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09\uFF0C\u9ED8\u8BA4\u4E3A\u5F53\u524D\u76EE\u5F55"
3118
- },
3119
- all: {
3120
- type: "boolean",
3121
- description: "\u662F\u5426\u663E\u793A\u9690\u85CF\u6587\u4EF6\uFF08\u4EE5 . \u5F00\u5934\u7684\u6587\u4EF6\uFF09\uFF0C\u9ED8\u8BA4 false"
3122
- }
3123
- },
3124
- required: [],
3125
- additionalProperties: false
3126
- };
3127
3293
  var lsTool = {
3128
3294
  name: "ls",
3295
+ kind: "read" /* Read */,
3129
3296
  description: "\u5217\u51FA\u76EE\u5F55\u5185\u5BB9\u3002\u663E\u793A\u6761\u76EE\u7C7B\u578B\uFF08\u6587\u4EF6/\u76EE\u5F55/\u94FE\u63A5\uFF09\u548C\u5927\u5C0F\u3002\u53EF\u9009\u62E9\u662F\u5426\u663E\u793A\u9690\u85CF\u6587\u4EF6\u3002",
3130
- parameters: lsSchema,
3131
- readOnly: true,
3297
+ parameters: {
3298
+ type: "object",
3299
+ properties: {
3300
+ path: {
3301
+ type: "string",
3302
+ description: "\u76EE\u5F55\u8DEF\u5F84\uFF08\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09\uFF0C\u9ED8\u8BA4\u4E3A\u5F53\u524D\u76EE\u5F55"
3303
+ },
3304
+ all: {
3305
+ type: "boolean",
3306
+ description: "\u662F\u5426\u663E\u793A\u9690\u85CF\u6587\u4EF6\uFF08\u4EE5 . \u5F00\u5934\u7684\u6587\u4EF6\uFF09\uFF0C\u9ED8\u8BA4 false"
3307
+ }
3308
+ },
3309
+ required: [],
3310
+ additionalProperties: false
3311
+ },
3132
3312
  async execute(args, ctx) {
3133
- const params = args ?? {};
3134
- const dirPath = params.path ? resolvePath(params.path, ctx.cwd) : ctx.cwd;
3135
- const showAll = params.all ?? false;
3313
+ const dirPath = args.path ? resolvePath(args.path, ctx.cwd) : ctx.cwd;
3314
+ const showAll = args.all ?? false;
3136
3315
  try {
3137
3316
  const entries = await readdir4(dirPath, { withFileTypes: true });
3138
3317
  const lines = [];
@@ -3185,58 +3364,56 @@ ${lines.join("\n")}`),
3185
3364
  };
3186
3365
 
3187
3366
  // src/tool/builtins/fetch.ts
3188
- var fetchSchema = {
3189
- type: "object",
3190
- properties: {
3191
- url: {
3192
- type: "string",
3193
- description: "\u8BF7\u6C42\u7684 URL"
3194
- },
3195
- method: {
3196
- type: "string",
3197
- description: "HTTP \u65B9\u6CD5\uFF08GET\u3001POST\u3001PUT\u3001DELETE \u7B49\uFF09\uFF0C\u9ED8\u8BA4 GET"
3198
- },
3199
- headers: {
3200
- type: "object",
3201
- description: "\u8BF7\u6C42\u5934\u952E\u503C\u5BF9",
3202
- additionalProperties: { type: "string" }
3203
- },
3204
- body: {
3205
- type: "string",
3206
- description: "\u8BF7\u6C42\u4F53\u5185\u5BB9\uFF08POST/PUT \u65F6\u4F7F\u7528\uFF09"
3207
- },
3208
- max_length: {
3209
- type: "number",
3210
- description: "\u54CD\u5E94\u5185\u5BB9\u6700\u5927\u957F\u5EA6\uFF08\u5B57\u7B26\uFF09\uFF0C\u9ED8\u8BA4 50000"
3211
- }
3212
- },
3213
- required: ["url"],
3214
- additionalProperties: false
3215
- };
3216
3367
  var fetchTool = {
3217
3368
  name: "fetch",
3369
+ kind: "read" /* Read */,
3218
3370
  description: "\u53D1\u8D77 HTTP \u8BF7\u6C42\u5E76\u8FD4\u56DE\u54CD\u5E94\u5185\u5BB9\u3002\u652F\u6301\u81EA\u5B9A\u4E49\u65B9\u6CD5\u548C\u8BF7\u6C42\u5934\u3002\u9002\u7528\u4E8E\u83B7\u53D6\u7F51\u9875\u5185\u5BB9\u3001API \u8C03\u7528\u7B49\u573A\u666F\u3002",
3219
- parameters: fetchSchema,
3220
- readOnly: true,
3371
+ parameters: {
3372
+ type: "object",
3373
+ properties: {
3374
+ url: {
3375
+ type: "string",
3376
+ description: "\u8BF7\u6C42\u7684 URL"
3377
+ },
3378
+ method: {
3379
+ type: "string",
3380
+ description: "HTTP \u65B9\u6CD5\uFF08GET\u3001POST\u3001PUT\u3001DELETE \u7B49\uFF09\uFF0C\u9ED8\u8BA4 GET"
3381
+ },
3382
+ headers: {
3383
+ type: "object",
3384
+ description: "\u8BF7\u6C42\u5934\u952E\u503C\u5BF9",
3385
+ additionalProperties: { type: "string" }
3386
+ },
3387
+ body: {
3388
+ type: "string",
3389
+ description: "\u8BF7\u6C42\u4F53\u5185\u5BB9\uFF08POST/PUT \u65F6\u4F7F\u7528\uFF09"
3390
+ },
3391
+ max_length: {
3392
+ type: "number",
3393
+ description: "\u54CD\u5E94\u5185\u5BB9\u6700\u5927\u957F\u5EA6\uFF08\u5B57\u7B26\uFF09\uFF0C\u9ED8\u8BA4 50000"
3394
+ }
3395
+ },
3396
+ required: ["url"],
3397
+ additionalProperties: false
3398
+ },
3221
3399
  async execute(args, ctx) {
3222
- const params = args;
3223
- if (!params?.url || typeof params.url !== "string") {
3400
+ if (!args?.url || typeof args.url !== "string") {
3224
3401
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 url", error: "INVALID_ARGS" };
3225
3402
  }
3226
- const method = (params.method ?? "GET").toUpperCase();
3403
+ const method = (args.method ?? "GET").toUpperCase();
3227
3404
  const timeout = 3e4;
3228
- const maxLength = params.max_length ?? 5e4;
3405
+ const maxLength = args.max_length ?? 5e4;
3229
3406
  try {
3230
3407
  const fetchOptions = {
3231
3408
  method,
3232
- headers: params.headers ?? {},
3409
+ headers: args.headers ?? {},
3233
3410
  signal: ctx.signal ?? AbortSignal.timeout(timeout)
3234
3411
  };
3235
- if (params.body && (method === "POST" || method === "PUT" || method === "PATCH")) {
3412
+ if (args.body && (method === "POST" || method === "PUT" || method === "PATCH")) {
3236
3413
  fetchOptions.headers["Content-Type"] ??= "application/json";
3237
- fetchOptions.body = params.body;
3414
+ fetchOptions.body = args.body;
3238
3415
  }
3239
- const response = await fetch(params.url, fetchOptions);
3416
+ const response = await fetch(args.url, fetchOptions);
3240
3417
  const contentType = response.headers.get("content-type") ?? "unknown";
3241
3418
  const statusText = `${response.status} ${response.statusText}`;
3242
3419
  let body;
@@ -3249,7 +3426,7 @@ var fetchTool = {
3249
3426
  const header = `\u72B6\u6001: ${statusText}
3250
3427
  \u5185\u5BB9\u7C7B\u578B: ${contentType}`;
3251
3428
  const separator = body.length > 0 ? "\n---\n" : "";
3252
- const urlPreview = params.url.length > 60 ? params.url.slice(0, 57) + "..." : params.url;
3429
+ const urlPreview = args.url.length > 60 ? args.url.slice(0, 57) + "..." : args.url;
3253
3430
  const summary = `\u{1F310} ${method} ${urlPreview} \u2192 ${response.status}`;
3254
3431
  return {
3255
3432
  success: response.ok,
@@ -3276,16 +3453,16 @@ var fetchTool = {
3276
3453
 
3277
3454
  // src/tool/builtins/index.ts
3278
3455
  var builtinTools = [
3279
- readFileTool,
3280
- writeFileTool,
3281
- editFileTool,
3282
- multiEditTool,
3283
- deleteRangeTool,
3284
- bashTool,
3285
- globTool,
3286
- grepTool,
3287
- lsTool,
3288
- fetchTool
3456
+ eraseTool(readFileTool),
3457
+ eraseTool(writeFileTool),
3458
+ eraseTool(editFileTool),
3459
+ eraseTool(multiEditTool),
3460
+ eraseTool(deleteRangeTool),
3461
+ eraseTool(bashTool),
3462
+ eraseTool(globTool),
3463
+ eraseTool(grepTool),
3464
+ eraseTool(lsTool),
3465
+ eraseTool(fetchTool)
3289
3466
  ];
3290
3467
 
3291
3468
  // src/utils/gradient.ts
@@ -3376,6 +3553,8 @@ registerCommand("/help", {
3376
3553
  registerCommand("/clear", { desc: "\u6E05\u7A7A\u5BF9\u8BDD\u5386\u53F2", handler: () => ({ kind: "clear" }) });
3377
3554
  registerCommand("/version", { desc: "\u663E\u793A\u7248\u672C\u4FE1\u606F", handler: () => ({ kind: "text", content: "dskcode v0.1.10" }) });
3378
3555
  registerCommand("/model", { desc: "\u5207\u6362\u6A21\u578B", handler: () => ({ kind: "text", content: "\u8BF7\u76F4\u63A5\u8F93\u5165 /model \u8FDB\u5165\u9009\u62E9\u754C\u9762" }) });
3556
+ registerCommand("/thinking", { desc: "\u5207\u6362\u6DF1\u5EA6\u601D\u8003\u6A21\u5F0F", handler: () => ({ kind: "text", content: "\u8BF7\u76F4\u63A5\u8F93\u5165 /thinking \u5207\u6362" }) });
3557
+ registerCommand("/effort", { desc: "\u5207\u6362\u63A8\u7406\u7B49\u7EA7 High/Max", handler: () => ({ kind: "text", content: "\u8BF7\u76F4\u63A5\u8F93\u5165 /effort \u5207\u6362" }) });
3379
3558
  registerCommand("/game", { desc: "\u542F\u52A8\u6E38\u620F", handler: () => ({ kind: "navigate", target: "game" }) });
3380
3559
  registerCommand("/stock", { desc: "\u67E5\u770B\u80A1\u7968\u884C\u60C5", handler: () => ({ kind: "navigate", target: "stock" }) });
3381
3560
  var STREAMING_PLACEHOLDERS = [
@@ -3434,6 +3613,19 @@ function ChatSession({
3434
3613
  const [activeModel, setActiveModel] = useState3(model);
3435
3614
  const [streamingModel, setStreamingModel] = useState3(void 0);
3436
3615
  const [streamError, setStreamError] = useState3(void 0);
3616
+ const [thinkingEnabled, setThinkingEnabled] = useState3(true);
3617
+ const [thinkingEffort, setThinkingEffort] = useState3("high");
3618
+ const [responseFormat, setResponseFormat] = useState3("text");
3619
+ const [toolChoice, setToolChoice] = useState3(void 0);
3620
+ const sessionCost = useMemo(() => {
3621
+ return displayMessages.reduce((sum, msg) => {
3622
+ if (msg.assistantDetail?.cost) {
3623
+ return sum + msg.assistantDetail.cost;
3624
+ }
3625
+ return sum;
3626
+ }, 0);
3627
+ }, [displayMessages]);
3628
+ const hasConversationStarted = displayMessages.length > 0;
3437
3629
  const cmdTips = Array.from(getRegisteredCommands()).filter(([name]) => name !== "/exit" && name !== "/quit").map(([name, cmd]) => ({ name, desc: cmd.desc }));
3438
3630
  const [cmdTipIndex, setCmdTipIndex] = useState3(0);
3439
3631
  const [cmdTipGradientColors, setCmdTipGradientColors] = useState3([]);
@@ -3565,6 +3757,11 @@ function ChatSession({
3565
3757
  return;
3566
3758
  }
3567
3759
  }
3760
+ if (selectingModel) {
3761
+ if (key.upArrow || key.downArrow) {
3762
+ }
3763
+ return;
3764
+ }
3568
3765
  if (key.ctrl && _input === "c") {
3569
3766
  if (isStreaming) {
3570
3767
  abortRef.current?.abort();
@@ -3584,7 +3781,7 @@ function ChatSession({
3584
3781
  if (cmdTips.length <= 1) return;
3585
3782
  const timer = setInterval(() => {
3586
3783
  setCmdTipIndex((prev) => (prev + 1) % cmdTips.length);
3587
- }, 5e3);
3784
+ }, 2e3);
3588
3785
  return () => clearInterval(timer);
3589
3786
  }, [cmdTips.length]);
3590
3787
  useEffect3(() => {
@@ -3631,7 +3828,7 @@ function ChatSession({
3631
3828
  if (!apiKey || !baseUrl) return;
3632
3829
  let cancelled = false;
3633
3830
  setBalanceLoading(true);
3634
- import("./deepseek-UJXV2D6K.js").then(({ DeepSeekProvider: DeepSeekProvider2 }) => {
3831
+ import("./deepseek-YTT76XDE.js").then(({ DeepSeekProvider: DeepSeekProvider2 }) => {
3635
3832
  const provider = new DeepSeekProvider2({
3636
3833
  apiKey,
3637
3834
  baseUrl,
@@ -3700,14 +3897,48 @@ function ChatSession({
3700
3897
  const trimmed = value.trim();
3701
3898
  if (!trimmed) return;
3702
3899
  if (trimmed.startsWith("/") && trimmed.length > 1) {
3703
- if (trimmed.toLowerCase() === "/model") {
3900
+ const cmdLower = trimmed.toLowerCase();
3901
+ if (cmdLower === "/model") {
3704
3902
  const curIdx = modelOptions.indexOf(activeModel);
3705
3903
  setModelSelectIndex(curIdx >= 0 ? curIdx : 0);
3706
3904
  setSelectingModel(true);
3707
3905
  setInput("");
3708
3906
  return;
3709
3907
  }
3710
- const cmd = commandRegistry.get(trimmed.toLowerCase());
3908
+ if (cmdLower === "/thinking") {
3909
+ setThinkingEnabled((prev) => !prev);
3910
+ setDisplayMessages((prev) => [
3911
+ ...prev,
3912
+ { role: "user", content: trimmed },
3913
+ { role: "assistant", content: `\u6DF1\u5EA6\u601D\u8003\u5DF2${thinkingEnabled ? "\u5173\u95ED" : "\u5F00\u542F"}` }
3914
+ ]);
3915
+ setInput("");
3916
+ return;
3917
+ }
3918
+ if (cmdLower === "/effort") {
3919
+ const next = thinkingEffort === "high" ? "max" : "high";
3920
+ setThinkingEffort(next);
3921
+ setDisplayMessages((prev) => [
3922
+ ...prev,
3923
+ { role: "user", content: trimmed },
3924
+ { role: "assistant", content: `\u63A8\u7406\u7B49\u7EA7\u5DF2\u5207\u6362\u4E3A ${next === "high" ? "High" : "Max"}` }
3925
+ ]);
3926
+ setInput("");
3927
+ return;
3928
+ }
3929
+ if (cmdLower === "/tools") {
3930
+ const next = toolChoice === void 0 ? "none" : toolChoice === "none" ? "required" : toolChoice === "required" ? "auto" : void 0;
3931
+ setToolChoice(next);
3932
+ const label = next === void 0 ? "\u81EA\u52A8\uFF08\u9ED8\u8BA4\uFF09" : next === "none" ? "\u7981\u6B62\u8C03\u7528" : next === "required" ? "\u5F3A\u5236\u8C03\u7528" : "\u81EA\u52A8\uFF08\u9ED8\u8BA4\uFF09";
3933
+ setDisplayMessages((prev) => [
3934
+ ...prev,
3935
+ { role: "user", content: trimmed },
3936
+ { role: "assistant", content: `\u5DE5\u5177\u8C03\u7528\u7B56\u7565\u5DF2\u5207\u6362\u4E3A ${label}` }
3937
+ ]);
3938
+ setInput("");
3939
+ return;
3940
+ }
3941
+ const cmd = commandRegistry.get(cmdLower);
3711
3942
  if (cmd) {
3712
3943
  const result = cmd.handler();
3713
3944
  switch (result.kind) {
@@ -3782,7 +4013,12 @@ function ChatSession({
3782
4013
  const abortController = new AbortController();
3783
4014
  abortRef.current = abortController;
3784
4015
  try {
3785
- for await (const event of session.chat(trimmed)) {
4016
+ for await (const event of session.chat(trimmed, {
4017
+ thinkingAllowed: thinkingEnabled || void 0,
4018
+ thinkingEffort: thinkingEnabled ? thinkingEffort : void 0,
4019
+ responseFormat: responseFormat !== "text" ? responseFormat : void 0,
4020
+ toolChoice
4021
+ })) {
3786
4022
  if (abortController.signal.aborted) break;
3787
4023
  switch (event.type) {
3788
4024
  case "text_delta":
@@ -3820,6 +4056,11 @@ function ChatSession({
3820
4056
  setStreamingModel(event.model);
3821
4057
  currentUsageRef.current = event.usage;
3822
4058
  currentModelRef.current = event.model;
4059
+ {
4060
+ const cost = calculateCost(event.usage, event.model);
4061
+ setCurrentCost(cost.totalCost);
4062
+ currentCostRef.current = cost.totalCost;
4063
+ }
3823
4064
  break;
3824
4065
  case "done":
3825
4066
  setCurrentElapsed(event.elapsed);
@@ -3861,23 +4102,14 @@ function ChatSession({
3861
4102
  ]);
3862
4103
  }
3863
4104
  }
3864
- }, [onLaunchGame, onLaunchStock, currentContent, currentToolCalls, skills, skillSelectIndex, getFilteredSkills]);
4105
+ }, [onLaunchGame, onLaunchStock, currentContent, currentToolCalls, skills, skillSelectIndex, getFilteredSkills, thinkingEnabled, thinkingEffort, responseFormat, toolChoice, activeModel]);
3865
4106
  useEffect3(() => {
3866
4107
  if (!isStreaming && externalCostTracker) {
3867
4108
  setTodayCost(externalCostTracker.todayTotalCost);
3868
4109
  }
3869
4110
  }, [isStreaming, externalCostTracker]);
3870
- useEffect3(() => {
3871
- if (currentUsage && streamingModel && !currentCost) {
3872
- import("./models-OL7DHYGK.js").then(({ calculateCost: calculateCost2 }) => {
3873
- const cost = calculateCost2(currentUsage, streamingModel);
3874
- setCurrentCost(cost.totalCost);
3875
- currentCostRef.current = cost.totalCost;
3876
- });
3877
- }
3878
- }, [currentUsage, streamingModel, currentCost]);
3879
4111
  return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [
3880
- /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", marginBottom: 1, children: [
4112
+ !hasConversationStarted && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", marginBottom: 1, children: [
3881
4113
  /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", marginRight: 4, children: LOGO_LINES.map((line, i) => {
3882
4114
  const colorIndex = (i + offset) % CYBER_PALETTE.length;
3883
4115
  return /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsx8(Text9, { bold: true, color: CYBER_PALETTE[colorIndex], children: line }) }, i);
@@ -3899,6 +4131,15 @@ function ChatSession({
3899
4131
  " \u{1F527} \u6A21\u578B ",
3900
4132
  SUPPORTED_MODELS[activeModel]?.displayName ?? activeModel
3901
4133
  ] }),
4134
+ thinkingEnabled && /* @__PURE__ */ jsxs8(Text9, { color: "#ff9800", children: [
4135
+ " \u{1F9E0} \u6DF1\u5EA6\u601D\u8003 ",
4136
+ thinkingEffort === "max" ? "Max" : "High"
4137
+ ] }),
4138
+ responseFormat === "json_object" && /* @__PURE__ */ jsx8(Text9, { color: "#4caf50", children: " \u{1F4C4} JSON" }),
4139
+ toolChoice !== void 0 && /* @__PURE__ */ jsxs8(Text9, { color: "#e91e63", children: [
4140
+ " \u{1F6E0} ",
4141
+ toolChoice === "none" ? "\u7981\u6B62\u5DE5\u5177" : toolChoice === "required" ? "\u5F3A\u5236\u5DE5\u5177" : ""
4142
+ ] }),
3902
4143
  cmdTips.length > 0 && (() => {
3903
4144
  const tip = cmdTips[cmdTipIndex % cmdTips.length];
3904
4145
  if (!tip) return null;
@@ -4006,7 +4247,21 @@ function ChatSession({
4006
4247
  ] }),
4007
4248
  /* @__PURE__ */ jsx8(Text9, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) })
4008
4249
  ] }) : /* @__PURE__ */ jsxs8(Fragment, { children: [
4009
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text9, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) }) }),
4250
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: hasConversationStarted && (balance !== null || sessionCost > 0 || isStreaming) ? /* @__PURE__ */ jsxs8(Text9, { color: "#00ffff", dimColor: true, children: [
4251
+ "\u2500".repeat(Math.max(dividerWidth - 35, 10)),
4252
+ balance !== null && /* @__PURE__ */ jsxs8(Text9, { color: "yellow", children: [
4253
+ " \u{1F4B0} \u4F59\u989D \xA5",
4254
+ balance.toFixed(2)
4255
+ ] }),
4256
+ isStreaming ? /* @__PURE__ */ jsxs8(Text9, { color: "cyan", children: [
4257
+ " \u{1F4CA} \u672C\u6B21 \xA5",
4258
+ sessionCost > 0 ? sessionCost.toFixed(4) + " " : "",
4259
+ /* @__PURE__ */ jsx8(InkSpinner2, { type: "dots" })
4260
+ ] }) : sessionCost > 0 ? /* @__PURE__ */ jsxs8(Text9, { color: "cyan", children: [
4261
+ " \u{1F4CA} \u672C\u6B21 \xA5",
4262
+ sessionCost.toFixed(4)
4263
+ ] }) : null
4264
+ ] }) : /* @__PURE__ */ jsx8(Text9, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) }) }),
4010
4265
  /* @__PURE__ */ jsxs8(Box8, { children: [
4011
4266
  /* @__PURE__ */ jsx8(Box8, { width: 4, flexShrink: 0, children: /* @__PURE__ */ jsx8(Text9, { bold: true, color: "#00ff41", children: "\u26A1" }) }),
4012
4267
  /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, children: !input && !isStreaming && idlePlaceholder && gradientColors.length > 0 ? /* @__PURE__ */ jsx8(Text9, { children: idlePlaceholder.split("").map((ch, i) => /* @__PURE__ */ jsx8(Text9, { color: gradientColors[i] ?? void 0, children: ch }, i)) }) : !input && isStreaming && streamingPlaceholder && streamingGradientColors.length > 0 ? /* @__PURE__ */ jsx8(Text9, { children: streamingPlaceholder.split("").map((ch, i) => /* @__PURE__ */ jsx8(Text9, { color: streamingGradientColors[i] ?? void 0, children: ch }, i)) }) : /* @__PURE__ */ jsx8(
@@ -4224,7 +4479,7 @@ function BrickBreakerGame({ onExit: _onExit }) {
4224
4479
  const [initialLevel, setInitialLevel] = useState5(1);
4225
4480
  const [selectingLevel, setSelectingLevel] = useState5(true);
4226
4481
  const stateRef = useRef3(createInitialState(initialLevel));
4227
- const [tick, setTick] = useState5(0);
4482
+ const [tick2, setTick] = useState5(0);
4228
4483
  const onExitRef = useRef3(_onExit);
4229
4484
  onExitRef.current = _onExit;
4230
4485
  useEffect4(() => {
@@ -4279,7 +4534,7 @@ function BrickBreakerGame({ onExit: _onExit }) {
4279
4534
  const aliveCount = s.bricks.filter((b) => b.alive).length;
4280
4535
  const board = buildBoard(s);
4281
4536
  const def = getLevel(s.level);
4282
- void tick;
4537
+ void tick2;
4283
4538
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
4284
4539
  /* @__PURE__ */ jsxs10(Box10, { flexDirection: "row", children: [
4285
4540
  /* @__PURE__ */ jsx10(Box10, { width: 20, children: /* @__PURE__ */ jsxs10(Text11, { children: [
@@ -4754,7 +5009,7 @@ function buildGameView(s, scoreLines, scoreColor, message) {
4754
5009
  }
4755
5010
  function CoderCheck({ onExit: _onExit }) {
4756
5011
  const stateRef = useRef4(createInitialState2());
4757
- const [tick, setTick] = useState6(0);
5012
+ const [tick2, setTick] = useState6(0);
4758
5013
  const [colorOffset, setColorOffset] = useState6(0);
4759
5014
  const onExitRef = useRef4(_onExit);
4760
5015
  onExitRef.current = _onExit;
@@ -4832,7 +5087,7 @@ function CoderCheck({ onExit: _onExit }) {
4832
5087
  const scoreStr = String(s.score).padStart(5, "0");
4833
5088
  const scoreLines = buildScoreLines(scoreStr);
4834
5089
  const view = buildGameView(s, scoreLines, scoreColor, s.message);
4835
- void tick;
5090
+ void tick2;
4836
5091
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, children: [
4837
5092
  /* @__PURE__ */ jsx11(Box11, { flexDirection: "row", children: /* @__PURE__ */ jsxs11(Text12, { children: [
4838
5093
  "\u751F\u547D ",
@@ -4889,16 +5144,375 @@ var coder_check_default = {
4889
5144
  }
4890
5145
  };
4891
5146
 
5147
+ // src/game/snake/engine.ts
5148
+ function isOpposite(a, b) {
5149
+ return a === "UP" && b === "DOWN" || a === "DOWN" && b === "UP" || a === "LEFT" && b === "RIGHT" || a === "RIGHT" && b === "LEFT";
5150
+ }
5151
+ function samePoint(a, b) {
5152
+ return a.x === b.x && a.y === b.y;
5153
+ }
5154
+ function movePoint(p, dir) {
5155
+ switch (dir) {
5156
+ case "UP":
5157
+ return { x: p.x, y: p.y - 1 };
5158
+ case "DOWN":
5159
+ return { x: p.x, y: p.y + 1 };
5160
+ case "LEFT":
5161
+ return { x: p.x - 1, y: p.y };
5162
+ case "RIGHT":
5163
+ return { x: p.x + 1, y: p.y };
5164
+ }
5165
+ }
5166
+ function wrapPoint(p, config) {
5167
+ return {
5168
+ x: (p.x % config.width + config.width) % config.width,
5169
+ y: (p.y % config.height + config.height) % config.height
5170
+ };
5171
+ }
5172
+ function generateFood(snake, config, type, excludePositions = []) {
5173
+ const occupied = new Set(snake.map((p) => `${p.x},${p.y}`));
5174
+ for (const p of excludePositions) {
5175
+ occupied.add(`${p.x},${p.y}`);
5176
+ }
5177
+ const totalCells = config.width * config.height;
5178
+ if (occupied.size >= totalCells) return null;
5179
+ let pos;
5180
+ do {
5181
+ pos = {
5182
+ x: Math.floor(Math.random() * config.width),
5183
+ y: Math.floor(Math.random() * config.height)
5184
+ };
5185
+ } while (occupied.has(`${pos.x},${pos.y}`));
5186
+ return {
5187
+ position: pos,
5188
+ type,
5189
+ ...type === "special" ? { spawnedAt: Date.now() } : {}
5190
+ };
5191
+ }
5192
+ function createInitialState3(config) {
5193
+ const startX = Math.floor(config.width / 2);
5194
+ const startY = Math.floor(config.height / 2);
5195
+ const snake = [
5196
+ { x: startX, y: startY },
5197
+ { x: startX - 1, y: startY },
5198
+ { x: startX - 2, y: startY }
5199
+ ];
5200
+ const food = generateFood(snake, config, "normal");
5201
+ return {
5202
+ snake,
5203
+ direction: "RIGHT",
5204
+ nextDirection: "RIGHT",
5205
+ food,
5206
+ specialFood: null,
5207
+ score: 0,
5208
+ speedLevel: 1,
5209
+ isGameOver: false,
5210
+ isPaused: false,
5211
+ wallWrap: Math.random() < 0.5,
5212
+ tickInterval: config.initialSpeed
5213
+ };
5214
+ }
5215
+ function tick(state, config) {
5216
+ state.direction = state.nextDirection;
5217
+ const head = state.snake[0];
5218
+ let newHead = movePoint(head, state.direction);
5219
+ const outOfBounds = newHead.x < 0 || newHead.x >= config.width || newHead.y < 0 || newHead.y >= config.height;
5220
+ if (outOfBounds) {
5221
+ if (state.wallWrap) {
5222
+ newHead = wrapPoint(newHead, config);
5223
+ } else {
5224
+ state.isGameOver = true;
5225
+ return;
5226
+ }
5227
+ }
5228
+ const ateNormal = samePoint(newHead, state.food.position);
5229
+ const ateSpecial = state.specialFood !== null && samePoint(newHead, state.specialFood.position);
5230
+ const willEat = ateNormal || ateSpecial;
5231
+ const bodyToCheck = willEat ? state.snake : state.snake.slice(0, -1);
5232
+ for (const segment of bodyToCheck) {
5233
+ if (samePoint(newHead, segment)) {
5234
+ state.isGameOver = true;
5235
+ return;
5236
+ }
5237
+ }
5238
+ state.snake.unshift(newHead);
5239
+ if (ateNormal) {
5240
+ state.score += 1;
5241
+ updateSpeed(state, config);
5242
+ const exclude = state.specialFood ? [state.specialFood.position] : [];
5243
+ const newFood = generateFood(state.snake, config, "normal", exclude);
5244
+ if (newFood) {
5245
+ state.food = newFood;
5246
+ } else {
5247
+ state.isGameOver = true;
5248
+ return;
5249
+ }
5250
+ if (state.specialFood === null && Math.random() < 0.25) {
5251
+ state.specialFood = generateFood(state.snake, config, "special", [state.food.position]);
5252
+ }
5253
+ } else if (ateSpecial) {
5254
+ state.score += 3;
5255
+ updateSpeed(state, config);
5256
+ state.specialFood = null;
5257
+ } else {
5258
+ state.snake.pop();
5259
+ }
5260
+ if (state.specialFood?.spawnedAt !== void 0) {
5261
+ if (Date.now() - state.specialFood.spawnedAt > 5e3) {
5262
+ state.specialFood = null;
5263
+ }
5264
+ }
5265
+ }
5266
+ function updateSpeed(state, config) {
5267
+ const newLevel = Math.floor(state.score / config.scorePerLevel) + 1;
5268
+ state.speedLevel = newLevel;
5269
+ state.tickInterval = Math.max(
5270
+ config.minSpeed,
5271
+ config.initialSpeed - (newLevel - 1) * config.speedDecrement
5272
+ );
5273
+ }
5274
+ function changeDirection(state, newDir) {
5275
+ if (!isOpposite(newDir, state.direction)) {
5276
+ state.nextDirection = newDir;
5277
+ }
5278
+ }
5279
+
5280
+ // src/game/snake/renderer.ts
5281
+ import chalk4 from "chalk";
5282
+ var CELL_W = 2;
5283
+ var EMPTY = " ";
5284
+ var BODY_CHAR = "\u2593";
5285
+ var HEAD_CHAR = "\u2593";
5286
+ var NORMAL_FOOD_CHAR = "\u25C6";
5287
+ var SPECIAL_FOOD_CHAR = "\u2605";
5288
+ var CSI = "\x1B[";
5289
+ var CURSOR_HIDE = `${CSI}?25l`;
5290
+ var CURSOR_SHOW = `${CSI}?25h`;
5291
+ var CURSOR_HOME = `${CSI}H`;
5292
+ function hideCursor() {
5293
+ process.stdout.write(CURSOR_HIDE);
5294
+ }
5295
+ function showCursor() {
5296
+ process.stdout.write(CURSOR_SHOW);
5297
+ }
5298
+ function clearScreen() {
5299
+ process.stdout.write(`${CSI}2J${CURSOR_HOME}`);
5300
+ }
5301
+ function cellColor(char, type) {
5302
+ switch (type) {
5303
+ case "snake-head":
5304
+ return chalk4.greenBright(char);
5305
+ case "snake-body":
5306
+ return chalk4.green(char);
5307
+ case "normal-food":
5308
+ return chalk4.red(char);
5309
+ case "special-food":
5310
+ return chalk4.yellow(char);
5311
+ case "empty":
5312
+ return char;
5313
+ }
5314
+ }
5315
+ function renderToTerminal(state, config) {
5316
+ const lines = buildRenderLines(state, config);
5317
+ process.stdout.write(`${CSI}2J${CURSOR_HOME}` + lines.join("\n"));
5318
+ }
5319
+ function buildRenderLines(state, config) {
5320
+ const { width: W, height: H } = config;
5321
+ const lines = [];
5322
+ const grid = Array.from({ length: H }, () => Array(W).fill("empty"));
5323
+ for (let i = 1; i < state.snake.length; i++) {
5324
+ const seg = state.snake[i];
5325
+ if (seg.y >= 0 && seg.y < H && seg.x >= 0 && seg.x < W) {
5326
+ grid[seg.y][seg.x] = "snake-body";
5327
+ }
5328
+ }
5329
+ const head = state.snake[0];
5330
+ if (head.y >= 0 && head.y < H && head.x >= 0 && head.x < W) {
5331
+ grid[head.y][head.x] = "snake-head";
5332
+ }
5333
+ const fp = state.food.position;
5334
+ if (grid[fp.y][fp.x] === "empty") {
5335
+ grid[fp.y][fp.x] = "normal-food";
5336
+ }
5337
+ if (state.specialFood) {
5338
+ const sp = state.specialFood.position;
5339
+ if (grid[sp.y][sp.x] === "empty") {
5340
+ grid[sp.y][sp.x] = "special-food";
5341
+ }
5342
+ }
5343
+ lines.push(" " + chalk4.gray("\u2554" + "\u2550\u2550".repeat(W) + "\u2557"));
5344
+ for (let y = 0; y < H; y++) {
5345
+ let row = " " + chalk4.gray("\u2551");
5346
+ for (let x = 0; x < W; x++) {
5347
+ const cellType = grid[y][x];
5348
+ let char;
5349
+ switch (cellType) {
5350
+ case "snake-head":
5351
+ char = HEAD_CHAR.repeat(CELL_W);
5352
+ break;
5353
+ case "snake-body":
5354
+ char = BODY_CHAR.repeat(CELL_W);
5355
+ break;
5356
+ case "normal-food":
5357
+ char = NORMAL_FOOD_CHAR + " ";
5358
+ break;
5359
+ case "special-food":
5360
+ char = SPECIAL_FOOD_CHAR + " ";
5361
+ break;
5362
+ default:
5363
+ char = EMPTY;
5364
+ }
5365
+ row += cellColor(char, cellType);
5366
+ }
5367
+ row += chalk4.gray("\u2551");
5368
+ lines.push(row);
5369
+ }
5370
+ lines.push(" " + chalk4.gray("\u255A" + "\u2550\u2550".repeat(W) + "\u255D"));
5371
+ lines.push("");
5372
+ lines.push(
5373
+ ` ${chalk4.cyan("\u5206\u6570")}: ${state.score} ${chalk4.cyan("\u957F\u5EA6")}: ${state.snake.length} ${chalk4.cyan("\u901F\u5EA6")}: Lv.${state.speedLevel} ${chalk4.cyan("\u8FB9\u754C")}: ${state.wallWrap ? "\u7A7F\u5899" : "\u649E\u5899"}`
5374
+ );
5375
+ if (state.specialFood?.spawnedAt !== void 0) {
5376
+ const remaining = Math.ceil((5e3 - (Date.now() - state.specialFood.spawnedAt)) / 1e3);
5377
+ if (remaining > 0) {
5378
+ lines.push(` ${chalk4.yellow(`\u2B50 \u7279\u6B8A\u98DF\u7269 ${remaining} \u79D2\u540E\u6D88\u5931`)}`);
5379
+ }
5380
+ }
5381
+ lines.push(` ${chalk4.gray("W/A/S/D \u6216 \u65B9\u5411\u952E\u79FB\u52A8 | P \u6682\u505C | M \u5207\u6362\u6A21\u5F0F | Q \u9000\u51FA")}`);
5382
+ if (state.isPaused) {
5383
+ lines.push("");
5384
+ lines.push(` ${chalk4.yellow("\u23F8 \u5DF2\u6682\u505C \u2014 \u6309 P \u7EE7\u7EED")}`);
5385
+ }
5386
+ if (state.isGameOver) {
5387
+ lines.push("");
5388
+ lines.push(` ${chalk4.red.bold("\u{1F480} \u6E38\u620F\u7ED3\u675F!")} ${chalk4.cyan("\u6700\u7EC8\u5206\u6570: " + state.score)}`);
5389
+ lines.push(` ${chalk4.gray("\u6309 R \u91CD\u65B0\u5F00\u59CB | \u6309 Q \u9000\u51FA")}`);
5390
+ }
5391
+ return lines;
5392
+ }
5393
+
5394
+ // src/game/snake/input.ts
5395
+ import * as readline from "readline";
5396
+ var KEY_TO_DIR = {
5397
+ up: "UP",
5398
+ w: "UP",
5399
+ W: "UP",
5400
+ down: "DOWN",
5401
+ s: "DOWN",
5402
+ S: "DOWN",
5403
+ left: "LEFT",
5404
+ a: "LEFT",
5405
+ A: "LEFT",
5406
+ right: "RIGHT",
5407
+ d: "RIGHT",
5408
+ D: "RIGHT"
5409
+ };
5410
+ function setupInput(ctx, onQuit, onRestart) {
5411
+ readline.emitKeypressEvents(process.stdin);
5412
+ if (process.stdin.isTTY) {
5413
+ process.stdin.setRawMode(true);
5414
+ }
5415
+ const handler = (_, key) => {
5416
+ if (!key || !key.name) return;
5417
+ const { name, ctrl } = key;
5418
+ if (ctrl && (name === "c" || name === "d")) {
5419
+ onQuit();
5420
+ return;
5421
+ }
5422
+ if (name === "q") {
5423
+ onQuit();
5424
+ return;
5425
+ }
5426
+ const state = ctx.state;
5427
+ if (state.isGameOver) {
5428
+ if (name === "r") {
5429
+ ctx.state = onRestart();
5430
+ }
5431
+ return;
5432
+ }
5433
+ if (name === "p") {
5434
+ state.isPaused = !state.isPaused;
5435
+ return;
5436
+ }
5437
+ if (name === "m") {
5438
+ state.wallWrap = !state.wallWrap;
5439
+ return;
5440
+ }
5441
+ const dir = KEY_TO_DIR[name];
5442
+ if (dir) {
5443
+ changeDirection(state, dir);
5444
+ }
5445
+ };
5446
+ process.stdin.on("keypress", handler);
5447
+ return () => {
5448
+ process.stdin.removeListener("keypress", handler);
5449
+ if (process.stdin.isTTY) {
5450
+ process.stdin.setRawMode(false);
5451
+ }
5452
+ };
5453
+ }
5454
+
5455
+ // src/game/snake/index.ts
5456
+ var CONFIG = {
5457
+ width: 20,
5458
+ height: 16,
5459
+ initialSpeed: 180,
5460
+ minSpeed: 60,
5461
+ speedDecrement: 10,
5462
+ scorePerLevel: 5
5463
+ };
5464
+ var IDLE_INTERVAL = 200;
5465
+ var snakeGame = {
5466
+ id: "snake",
5467
+ name: "Big Snake",
5468
+ description: "\u7ECF\u5178\u8D2A\u5403\u86C7\uFF0C\u53CC\u98DF\u7269\u7CFB\u7EDF\u3001\u96BE\u5EA6\u9012\u589E\u3001\u7A7F\u5899/\u649E\u5899\u6A21\u5F0F",
5469
+ play: async () => {
5470
+ const ctx = { state: createInitialState3(CONFIG) };
5471
+ return new Promise((resolve2) => {
5472
+ const cleanupInput = setupInput(
5473
+ ctx,
5474
+ () => {
5475
+ stop = true;
5476
+ clearTimeout(timerId);
5477
+ showCursor();
5478
+ cleanupInput();
5479
+ resolve2();
5480
+ },
5481
+ () => {
5482
+ return createInitialState3(CONFIG);
5483
+ }
5484
+ );
5485
+ clearScreen();
5486
+ hideCursor();
5487
+ let stop = false;
5488
+ let timerId;
5489
+ function loop() {
5490
+ if (stop) return;
5491
+ const s = ctx.state;
5492
+ if (!s.isGameOver && !s.isPaused) {
5493
+ tick(s, CONFIG);
5494
+ }
5495
+ renderToTerminal(s, CONFIG);
5496
+ const interval = s.isGameOver || s.isPaused ? IDLE_INTERVAL : s.tickInterval;
5497
+ timerId = setTimeout(loop, interval);
5498
+ }
5499
+ timerId = setTimeout(loop, 0);
5500
+ });
5501
+ }
5502
+ };
5503
+ var snake_default = snakeGame;
5504
+
4892
5505
  // src/game/registry.ts
4893
5506
  function initGames() {
4894
5507
  registerGame(brick_breaker_default);
4895
5508
  registerGame(coder_check_default);
5509
+ registerGame(snake_default);
4896
5510
  return listGames();
4897
5511
  }
4898
5512
 
4899
5513
  // src/cli/index.tsx
4900
5514
  import { render as render4 } from "ink";
4901
- import chalk4 from "chalk";
5515
+ import chalk5 from "chalk";
4902
5516
 
4903
5517
  // src/stock/StockList.tsx
4904
5518
  import { Box as Box12, Text as Text13, useInput as useInput5 } from "ink";
@@ -5331,7 +5945,7 @@ function createCli() {
5331
5945
  const key = await promptForApiKey();
5332
5946
  if (!key) process.exit(1);
5333
5947
  const savedPath = await saveApiKey(key);
5334
- console.log(` ${chalk4.green("\u2714")} API Key \u5DF2\u4FDD\u5B58\u5230 ${chalk4.dim(savedPath)}
5948
+ console.log(` ${chalk5.green("\u2714")} API Key \u5DF2\u4FDD\u5B58\u5230 ${chalk5.dim(savedPath)}
5335
5949
  `);
5336
5950
  const result = await loadAndValidate();
5337
5951
  ctx = { ...ctx, config: result.config };
@@ -5478,8 +6092,8 @@ compdef _dskcode_completion dskcode`);
5478
6092
  { code: "sh601899" }
5479
6093
  ];
5480
6094
  const savedPath = await saveStockConfig(defaultSymbols);
5481
- console.log(`${chalk4.green("\u2714")} \u5DF2\u751F\u6210\u81EA\u9009\u80A1\u914D\u7F6E: ${chalk4.dim(savedPath)}`);
5482
- console.log(`${chalk4.dim(" \u63D0\u793A: \u53EF\u7F16\u8F91\u4E0A\u8FF0\u6587\u4EF6\u81EA\u5B9A\u4E49\u81EA\u9009\u80A1\u5217\u8868")}
6095
+ console.log(`${chalk5.green("\u2714")} \u5DF2\u751F\u6210\u81EA\u9009\u80A1\u914D\u7F6E: ${chalk5.dim(savedPath)}`);
6096
+ console.log(`${chalk5.dim(" \u63D0\u793A: \u53EF\u7F16\u8F91\u4E0A\u8FF0\u6587\u4EF6\u81EA\u5B9A\u4E49\u81EA\u9009\u80A1\u5217\u8868")}
5483
6097
  `);
5484
6098
  }
5485
6099
  const freshResult = await loadAndValidate();
@@ -5533,7 +6147,7 @@ compdef _dskcode_completion dskcode`);
5533
6147
  });
5534
6148
  if (selectedGame) {
5535
6149
  console.log(`
5536
- \u542F\u52A8\u6E38\u620F: ${chalk4.green(selectedGame.name)}
6150
+ \u542F\u52A8\u6E38\u620F: ${chalk5.green(selectedGame.name)}
5537
6151
  `);
5538
6152
  await selectedGame.play();
5539
6153
  }