jinzd-ai-cli 0.1.29 → 0.1.31
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/CLAUDE.md
CHANGED
|
@@ -343,6 +343,83 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
343
343
|
- [x] **web_fetch DNS 解析时 SSRF 防护**(v0.1.25):新增 `resolveAndCheck()` 函数,用 `dns.promises.lookup()` 预解析域名,检查结果 IP 是否为私有地址。初始 URL 和 redirect 目标均校验。
|
|
344
344
|
- [ ] **`persistentCwd` 全局状态**:bash 工具的当前工作目录是模块级全局变量,多 session 并发时可能串扰。现阶段单 session REPL 无影响,GUI 多会话扩展时需重构为 per-session 状态。
|
|
345
345
|
|
|
346
|
+
## 本轮开发完成记录(2026-03-01,v0.1.26 → v0.1.30)
|
|
347
|
+
|
|
348
|
+
### 安全修复续篇:低危 L2–L7
|
|
349
|
+
|
|
350
|
+
**L2**(`src/tools/builtin/web-fetch.ts`):`htmlToText()` 函数新增 200 KB HTML 大小上限(`HTML_REGEX_LIMIT = 200_000`),超出则截断后再做正则替换,防止恶意大 HTML 导致正则性能崩溃。
|
|
351
|
+
|
|
352
|
+
**L3**(`src/session/session-manager.ts`):新增 `safeDate(value: unknown): Date` 辅助函数,对无效日期字符串返回 `new Date(0)` 而非 `Invalid Date`,防止 `listSessions()` 和 `searchMessages()` 的日期比较静默失败。
|
|
353
|
+
|
|
354
|
+
**L4**(`src/tools/builtin/ask-user.ts` + `src/tools/builtin/google-search.ts`):为模块级全局上下文对象(`askUserContext` / `googleSearchContext`)添加架构说明注释,提示 GUI 多会话扩展时需重构为 per-session 状态。
|
|
355
|
+
|
|
356
|
+
**L5**(`src/config/schema.ts`):为 `timeouts` 字段补充三层优先级文档注释:
|
|
357
|
+
1. `modelParams[modelId].timeout` — 最高
|
|
358
|
+
2. `timeouts[providerId]` — provider 级默认
|
|
359
|
+
3. 内置 provider 硬编码默认值 — 最低兜底
|
|
360
|
+
|
|
361
|
+
**L6/L7**:确认已安全(`call.arguments['path']` 已用 `String(... ?? '')` 兜底;主循环 `line` handler 已有 try/catch)。
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
### 新增功能 1:多模态图片输入(P0)
|
|
366
|
+
|
|
367
|
+
**背景**:用户希望通过 `@image.png 描述这张图` 语法将图片发送给各 AI Provider。
|
|
368
|
+
|
|
369
|
+
**类型层**(已有,无需改动):`src/core/types.ts` 的 `ImageContentPart { type: 'image_url', image_url: { url: 'data:mime;base64,...' } }` 与 OpenAI 格式完全兼容。
|
|
370
|
+
|
|
371
|
+
**Claude Provider**(`src/providers/claude.ts`):
|
|
372
|
+
- 新增 `contentToClaudeParts()` 私有方法:将内部 `image_url` 格式转换为 Anthropic SDK 期望的 `{ type: 'image', source: { type: 'base64', media_type, data } }` 格式
|
|
373
|
+
- 应用到 `chat()`、`chatStream()`、`chatWithTools()` 的消息构建
|
|
374
|
+
|
|
375
|
+
**Gemini Provider**(`src/providers/gemini.ts`):
|
|
376
|
+
- 移除错误的 `getContentText()` 调用(会丢弃图片)
|
|
377
|
+
- 新增 `contentToGeminiParts()` 私有方法:转换为 Gemini SDK 格式 `{ inlineData: { mimeType, data } }`
|
|
378
|
+
- 修复 `toGeminiHistory()`、`chat()`、`chatStream()`、`chatWithTools()` 的消息构建
|
|
379
|
+
- `chatWithTools()` 中变量 `lastMessage: string` 重命名为 `lastMsgParts: Part[]`,历史嵌入改为 `{ role: 'user', parts: lastMsgParts }`
|
|
380
|
+
|
|
381
|
+
**OpenAI 兼容 Provider**:已就绪,格式一致,无需修改。
|
|
382
|
+
|
|
383
|
+
**REPL 层**(`src/repl/repl.ts`):
|
|
384
|
+
- 新增常量 `MAX_IMAGE_BYTES = 10 * 1024 * 1024`(10 MB)
|
|
385
|
+
- `parseAtReferences()` 图片读取前用 `statSync` 校验大小,超限时加入 `refs` 类型为 `'toolarge'`,不内联
|
|
386
|
+
- `handleChat()` 新增 `toolarge` 分支:打印黄色警告 `⚠ Image too large (> 10 MB): <path>`
|
|
387
|
+
- 修复 `getVisionModelHint()`:Claude/Gemini 返回 `null`(原生支持,无需警告);DeepSeek 显示明确不支持提示;zhipu 推荐 `glm-4.6v`;kimi `moonshot-v1-*` 推荐对应 vision 版本
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
### 新增功能 2:Escape/Ctrl+C 中断 AI 流式输出(P0)
|
|
392
|
+
|
|
393
|
+
**背景**:流式生成期间无法中断,必须等待 AI 返回完整内容。
|
|
394
|
+
|
|
395
|
+
**架构设计**:两个流式生成入口——`handleChatSimple()` 和 `handleChatWithTools()` 的 tee streaming 分支(`save_last_response` 工具)——均支持中断。主路径(`chatWithTools()` → `renderResponse()`)为非流式,暂不支持。
|
|
396
|
+
|
|
397
|
+
**`src/core/types.ts`**:`ChatRequest` 新增 `signal?: AbortSignal`,透传给 Provider API 调用。
|
|
398
|
+
|
|
399
|
+
**Provider 层**:
|
|
400
|
+
- `claude.ts`:`messages.stream()` 第二参数传入 `{ signal: request.signal }`(Anthropic SDK 支持)
|
|
401
|
+
- `openai-compatible.ts`:流式 `create()` 第二参数传入 `{ signal: request.signal }`(OpenAI SDK 支持)
|
|
402
|
+
- `gemini.ts`:生成器循环开头检查 `if (request.signal?.aborted) break`(兼容性保险)
|
|
403
|
+
|
|
404
|
+
**`src/repl/renderer.ts`**:`renderStream()` 的 `options` 新增 `signal?: AbortSignal`:
|
|
405
|
+
- `for await` 循环开头检查 `signal.aborted` → 标记 `interrupted = true` 并 `break`
|
|
406
|
+
- 捕获 SDK 抛出的 `AbortError`(name 检测)→ 同样标记 `interrupted`
|
|
407
|
+
- 中断时 `flushBuf()` 输出残留缓冲,打印灰色 `[interrupted]` 提示
|
|
408
|
+
- 返回值不变(`{ content, usage, tokensShown }`),`content` 为已生成的部分内容
|
|
409
|
+
|
|
410
|
+
**`src/repl/repl.ts`**:
|
|
411
|
+
- 新增类属性 `streamAbortController: AbortController | null` + `_escHandler: ((d: Buffer) => void) | null`
|
|
412
|
+
- 新增 `setupStreamInterrupt()` 方法:创建 `AbortController`,`process.stdin.resume()`(绕过 `rl.pause()` 暂停),注册原始字节监听器检测纯 ESC(`0x1b`,单字节,区别于 ESC 序列)和 Ctrl+C(`0x03`)
|
|
413
|
+
- 新增 `teardownStreamInterrupt()` 方法:移除监听器,`process.stdin.pause()`,清空引用
|
|
414
|
+
- SIGINT 处理器最顶部新增分支:`streamAbortController` 非空时 `abort()` 并 return,不退出程序
|
|
415
|
+
- `handleChatSimple()` 流式分支和 tee streaming 分支均用 `setupStreamInterrupt()`/`teardownStreamInterrupt()` 包裹 + `signal` 传入
|
|
416
|
+
|
|
417
|
+
**版本与收尾**
|
|
418
|
+
- `src/core/constants.ts`:VERSION `0.1.26` → `0.1.30`(合并前几次 bump)
|
|
419
|
+
- `package.json`:version `0.1.29` → `0.1.30`
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
346
423
|
## 本轮开发完成记录(2026-03-01,v0.1.25 → v0.1.26)
|
|
347
424
|
|
|
348
425
|
### 新增功能:Context 自动管理 + 测试报告 + 脚手架
|
|
@@ -871,8 +948,8 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
871
948
|
### ❌ 缺失功能路线图(新增)
|
|
872
949
|
|
|
873
950
|
#### P0 — 核心竞争力缺口
|
|
874
|
-
- [
|
|
875
|
-
- [
|
|
951
|
+
- [x] **中断生成**(v0.1.30):`Escape` 键或 `Ctrl+C` 立即停止 AI 流式输出,不退出程序,显示 `[interrupted]` 后恢复提示符;已生成内容保留到 session
|
|
952
|
+
- [x] **多模态输入(图片)**(v0.1.30):`@image.png 描述这张图` 语法发送图片;Claude/Gemini/OpenAI 兼容格式自动转换;10MB 大小限制;`getVisionModelHint()` 正确识别 Claude/Gemini 原生支持视觉
|
|
876
953
|
- [ ] **`/add-dir` 命令**:运行时动态添加目录到上下文
|
|
877
954
|
- [ ] **并行工具调用**:AI 一次返回多个工具同时执行(当前为分组串行)
|
|
878
955
|
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
28
28
|
VERSION,
|
|
29
29
|
runTestsTool
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-IW6VVPO4.js";
|
|
31
31
|
|
|
32
32
|
// src/index.ts
|
|
33
33
|
import { program } from "commander";
|
|
@@ -409,7 +409,7 @@ var ClaudeProvider = class extends BaseProvider {
|
|
|
409
409
|
messages,
|
|
410
410
|
system: request.systemPrompt,
|
|
411
411
|
max_tokens: request.maxTokens ?? 8192
|
|
412
|
-
});
|
|
412
|
+
}, { signal: request.signal });
|
|
413
413
|
for await (const event of stream) {
|
|
414
414
|
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
415
415
|
yield { delta: event.delta.text, done: false };
|
|
@@ -650,6 +650,7 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
650
650
|
const chat = genModel.startChat({ history });
|
|
651
651
|
const result = await chat.sendMessageStream(lastMsgParts);
|
|
652
652
|
for await (const chunk of result.stream) {
|
|
653
|
+
if (request.signal?.aborted) break;
|
|
653
654
|
yield { delta: chunk.text(), done: false };
|
|
654
655
|
}
|
|
655
656
|
const finalResponse = await result.response;
|
|
@@ -857,7 +858,8 @@ var OpenAICompatibleProvider = class extends BaseProvider {
|
|
|
857
858
|
stream_options: { include_usage: true },
|
|
858
859
|
...request.thinking ? { thinking: { type: "enabled" } } : {}
|
|
859
860
|
}, {
|
|
860
|
-
timeout: request.timeout ?? this.defaultTimeout
|
|
861
|
+
timeout: request.timeout ?? this.defaultTimeout,
|
|
862
|
+
signal: request.signal
|
|
861
863
|
});
|
|
862
864
|
for await (const chunk of stream) {
|
|
863
865
|
const choice = chunk.choices[0];
|
|
@@ -1661,8 +1663,8 @@ var SessionManager = class {
|
|
|
1661
1663
|
|
|
1662
1664
|
// src/repl/repl.ts
|
|
1663
1665
|
import * as readline from "readline";
|
|
1664
|
-
import { existsSync as
|
|
1665
|
-
import { join as
|
|
1666
|
+
import { existsSync as existsSync19, readFileSync as readFileSync13, readdirSync as readdirSync9, statSync as statSync7 } from "fs";
|
|
1667
|
+
import { join as join14, resolve as resolve4, extname as extname4, dirname as dirname5, basename as basename5 } from "path";
|
|
1666
1668
|
import chalk10 from "chalk";
|
|
1667
1669
|
|
|
1668
1670
|
// src/repl/renderer.ts
|
|
@@ -1850,19 +1852,36 @@ var Renderer = class {
|
|
|
1850
1852
|
if (out) process.stdout.write(out);
|
|
1851
1853
|
buf = "";
|
|
1852
1854
|
};
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1855
|
+
let interrupted = false;
|
|
1856
|
+
try {
|
|
1857
|
+
for await (const chunk of stream) {
|
|
1858
|
+
if (options?.signal?.aborted) {
|
|
1859
|
+
interrupted = true;
|
|
1860
|
+
break;
|
|
1861
|
+
}
|
|
1862
|
+
if (chunk.usage) {
|
|
1863
|
+
usage = chunk.usage;
|
|
1864
|
+
}
|
|
1865
|
+
if (chunk.done) {
|
|
1866
|
+
flushBuf();
|
|
1867
|
+
break;
|
|
1868
|
+
}
|
|
1869
|
+
if (!chunk.delta) continue;
|
|
1870
|
+
fullContent += chunk.delta;
|
|
1871
|
+
buf += chunk.delta;
|
|
1872
|
+
if (fileStream) fileStream.write(chunk.delta);
|
|
1873
|
+
flushBuf();
|
|
1856
1874
|
}
|
|
1857
|
-
|
|
1875
|
+
} catch (err) {
|
|
1876
|
+
if (err?.name === "AbortError") {
|
|
1877
|
+
interrupted = true;
|
|
1858
1878
|
flushBuf();
|
|
1859
|
-
|
|
1879
|
+
} else {
|
|
1880
|
+
throw err;
|
|
1860
1881
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
if (fileStream) fileStream.write(chunk.delta);
|
|
1865
|
-
flushBuf();
|
|
1882
|
+
}
|
|
1883
|
+
if (interrupted) {
|
|
1884
|
+
process.stdout.write(chalk.dim(" [interrupted]\n"));
|
|
1866
1885
|
}
|
|
1867
1886
|
let tokensShown = false;
|
|
1868
1887
|
if (options?.showTokens) {
|
|
@@ -1963,10 +1982,10 @@ Error: ${message}
|
|
|
1963
1982
|
};
|
|
1964
1983
|
|
|
1965
1984
|
// src/repl/commands/index.ts
|
|
1966
|
-
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as
|
|
1967
|
-
import { execSync as
|
|
1985
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync6, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1986
|
+
import { execSync as execSync3 } from "child_process";
|
|
1968
1987
|
import { platform } from "os";
|
|
1969
|
-
import { resolve, dirname as dirname2, join as
|
|
1988
|
+
import { resolve, dirname as dirname2, join as join5, basename } from "path";
|
|
1970
1989
|
import chalk2 from "chalk";
|
|
1971
1990
|
|
|
1972
1991
|
// src/tools/git-context.ts
|
|
@@ -2129,6 +2148,93 @@ var UndoStack = class {
|
|
|
2129
2148
|
};
|
|
2130
2149
|
var undoStack = new UndoStack();
|
|
2131
2150
|
|
|
2151
|
+
// src/repl/clipboard.ts
|
|
2152
|
+
import { execSync as execSync2 } from "child_process";
|
|
2153
|
+
import { existsSync as existsSync5, statSync, unlinkSync as unlinkSync2 } from "fs";
|
|
2154
|
+
import { tmpdir } from "os";
|
|
2155
|
+
import { join as join4 } from "path";
|
|
2156
|
+
var CLIPBOARD_TIMEOUT = 5e3;
|
|
2157
|
+
function readClipboardImage() {
|
|
2158
|
+
const outPath = join4(tmpdir(), `aicli-paste-${Date.now()}.png`);
|
|
2159
|
+
try {
|
|
2160
|
+
if (process.platform === "win32") {
|
|
2161
|
+
_readWin32(outPath);
|
|
2162
|
+
} else if (process.platform === "darwin") {
|
|
2163
|
+
_readDarwin(outPath);
|
|
2164
|
+
} else {
|
|
2165
|
+
_readLinux(outPath);
|
|
2166
|
+
}
|
|
2167
|
+
} catch {
|
|
2168
|
+
_cleanup(outPath);
|
|
2169
|
+
return null;
|
|
2170
|
+
}
|
|
2171
|
+
if (!existsSync5(outPath) || statSync(outPath).size === 0) {
|
|
2172
|
+
_cleanup(outPath);
|
|
2173
|
+
return null;
|
|
2174
|
+
}
|
|
2175
|
+
return outPath;
|
|
2176
|
+
}
|
|
2177
|
+
function _readWin32(outPath) {
|
|
2178
|
+
const escapedPath = outPath.replace(/\\/g, "\\\\");
|
|
2179
|
+
const script = [
|
|
2180
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
2181
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
2182
|
+
"$img = [System.Windows.Forms.Clipboard]::GetImage()",
|
|
2183
|
+
`if ($null -ne $img) { $img.Save('${escapedPath}', [System.Drawing.Imaging.ImageFormat]::Png); exit 0 } else { exit 1 }`
|
|
2184
|
+
].join("; ");
|
|
2185
|
+
execSync2(`powershell -NoProfile -NonInteractive -Command "${script}"`, {
|
|
2186
|
+
stdio: "pipe",
|
|
2187
|
+
timeout: CLIPBOARD_TIMEOUT
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
function _readDarwin(outPath) {
|
|
2191
|
+
try {
|
|
2192
|
+
execSync2(`pngpaste "${outPath}"`, { stdio: "pipe", timeout: CLIPBOARD_TIMEOUT });
|
|
2193
|
+
return;
|
|
2194
|
+
} catch {
|
|
2195
|
+
}
|
|
2196
|
+
const script = [
|
|
2197
|
+
"try",
|
|
2198
|
+
" set d to (the clipboard as \xABclass PNGf\xBB)",
|
|
2199
|
+
` set fp to open for access POSIX file "${outPath}" with write permission`,
|
|
2200
|
+
" write d to fp",
|
|
2201
|
+
" close access fp",
|
|
2202
|
+
"end try"
|
|
2203
|
+
].join("\n");
|
|
2204
|
+
execSync2(`osascript -e '${script}'`, { stdio: "pipe", timeout: CLIPBOARD_TIMEOUT });
|
|
2205
|
+
}
|
|
2206
|
+
function _readLinux(outPath) {
|
|
2207
|
+
try {
|
|
2208
|
+
execSync2(`xclip -selection clipboard -t image/png -o > "${outPath}"`, {
|
|
2209
|
+
shell: "/bin/sh",
|
|
2210
|
+
stdio: "pipe",
|
|
2211
|
+
timeout: CLIPBOARD_TIMEOUT
|
|
2212
|
+
});
|
|
2213
|
+
return;
|
|
2214
|
+
} catch {
|
|
2215
|
+
}
|
|
2216
|
+
execSync2(`wl-paste --type image/png > "${outPath}"`, {
|
|
2217
|
+
shell: "/bin/sh",
|
|
2218
|
+
stdio: "pipe",
|
|
2219
|
+
timeout: CLIPBOARD_TIMEOUT
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
function _cleanup(path) {
|
|
2223
|
+
try {
|
|
2224
|
+
if (existsSync5(path)) unlinkSync2(path);
|
|
2225
|
+
} catch {
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
function getClipboardHint() {
|
|
2229
|
+
if (process.platform === "darwin") {
|
|
2230
|
+
return "\u5982 pngpaste \u672A\u5B89\u88C5\uFF0C\u8BF7\u8FD0\u884C\uFF1Abrew install pngpaste";
|
|
2231
|
+
}
|
|
2232
|
+
if (process.platform === "linux") {
|
|
2233
|
+
return "\u8BF7\u5B89\u88C5 xclip\uFF08X11\uFF09\uFF1Asudo apt install xclip\n\u6216 wl-paste\uFF08Wayland\uFF09\uFF1Asudo apt install wl-clipboard";
|
|
2234
|
+
}
|
|
2235
|
+
return "";
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2132
2238
|
// src/repl/commands/index.ts
|
|
2133
2239
|
function fmtCtx(tokens) {
|
|
2134
2240
|
if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
|
|
@@ -2176,19 +2282,19 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
|
|
|
2176
2282
|
}
|
|
2177
2283
|
const filtered = entries.filter((e) => !e.startsWith(".") && !SCAN_SKIP_DIRS.has(e));
|
|
2178
2284
|
const sorted = filtered.sort((a, b) => {
|
|
2179
|
-
const aIsDir =
|
|
2180
|
-
const bIsDir =
|
|
2285
|
+
const aIsDir = statSync2(join5(d, a)).isDirectory();
|
|
2286
|
+
const bIsDir = statSync2(join5(d, b)).isDirectory();
|
|
2181
2287
|
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
|
|
2182
2288
|
return a.localeCompare(b);
|
|
2183
2289
|
});
|
|
2184
2290
|
for (let i = 0; i < sorted.length && count < maxEntries; i++) {
|
|
2185
2291
|
const name = sorted[i];
|
|
2186
|
-
const fullPath =
|
|
2292
|
+
const fullPath = join5(d, name);
|
|
2187
2293
|
const isLast = i === sorted.length - 1;
|
|
2188
2294
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
2189
2295
|
let isDir;
|
|
2190
2296
|
try {
|
|
2191
|
-
isDir =
|
|
2297
|
+
isDir = statSync2(fullPath).isDirectory();
|
|
2192
2298
|
} catch {
|
|
2193
2299
|
continue;
|
|
2194
2300
|
}
|
|
@@ -2210,7 +2316,7 @@ function scanProject(cwd) {
|
|
|
2210
2316
|
configFiles: [],
|
|
2211
2317
|
directoryStructure: ""
|
|
2212
2318
|
};
|
|
2213
|
-
const check = (file) =>
|
|
2319
|
+
const check = (file) => existsSync6(join5(cwd, file));
|
|
2214
2320
|
const configCandidates = [
|
|
2215
2321
|
"package.json",
|
|
2216
2322
|
"tsconfig.json",
|
|
@@ -2237,7 +2343,7 @@ function scanProject(cwd) {
|
|
|
2237
2343
|
info.type = "node";
|
|
2238
2344
|
info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
|
|
2239
2345
|
try {
|
|
2240
|
-
const pkg = JSON.parse(readFileSync4(
|
|
2346
|
+
const pkg = JSON.parse(readFileSync4(join5(cwd, "package.json"), "utf-8"));
|
|
2241
2347
|
const scripts = pkg.scripts ?? {};
|
|
2242
2348
|
info.buildCommand = scripts.build ? `npm run build` : void 0;
|
|
2243
2349
|
info.testCommand = scripts.test ? `npm test` : void 0;
|
|
@@ -2318,14 +2424,14 @@ ${info.directoryStructure}
|
|
|
2318
2424
|
function copyToClipboard(text) {
|
|
2319
2425
|
const plat = platform();
|
|
2320
2426
|
if (plat === "win32") {
|
|
2321
|
-
|
|
2427
|
+
execSync3("clip", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
2322
2428
|
} else if (plat === "darwin") {
|
|
2323
|
-
|
|
2429
|
+
execSync3("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
2324
2430
|
} else {
|
|
2325
2431
|
try {
|
|
2326
|
-
|
|
2432
|
+
execSync3("xclip -selection clipboard", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
2327
2433
|
} catch {
|
|
2328
|
-
|
|
2434
|
+
execSync3("xsel --clipboard --input", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
2329
2435
|
}
|
|
2330
2436
|
}
|
|
2331
2437
|
}
|
|
@@ -3026,9 +3132,9 @@ ${text}
|
|
|
3026
3132
|
const cwd = process.cwd();
|
|
3027
3133
|
const gitRoot = getGitRoot(cwd);
|
|
3028
3134
|
const targetDir = gitRoot ?? cwd;
|
|
3029
|
-
const targetPath =
|
|
3135
|
+
const targetPath = join5(targetDir, "AICLI.md");
|
|
3030
3136
|
const force = args.includes("--force");
|
|
3031
|
-
if (
|
|
3137
|
+
if (existsSync6(targetPath) && !force) {
|
|
3032
3138
|
ctx.renderer.printInfo(`AICLI.md already exists at ${targetPath}`);
|
|
3033
3139
|
ctx.renderer.printInfo("Use /init --force to overwrite, or edit it manually.");
|
|
3034
3140
|
return;
|
|
@@ -3075,6 +3181,25 @@ ${text}
|
|
|
3075
3181
|
}
|
|
3076
3182
|
}
|
|
3077
3183
|
},
|
|
3184
|
+
{
|
|
3185
|
+
name: "paste",
|
|
3186
|
+
description: "Read image from clipboard and send with optional description",
|
|
3187
|
+
usage: "/paste [description]",
|
|
3188
|
+
async execute(args, ctx) {
|
|
3189
|
+
const imgPath = readClipboardImage();
|
|
3190
|
+
if (!imgPath) {
|
|
3191
|
+
const hint = getClipboardHint();
|
|
3192
|
+
ctx.renderer.renderError(
|
|
3193
|
+
"\u526A\u8D34\u677F\u4E2D\u6CA1\u6709\u56FE\u7247\u3002\u8BF7\u5148\u5728\u5176\u4ED6\u7A0B\u5E8F\u4E2D\u590D\u5236\u4E00\u5F20\u56FE\u7247\uFF0C\u518D\u8FD0\u884C /paste\u3002" + (hint ? `
|
|
3194
|
+
${hint}` : "")
|
|
3195
|
+
);
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
const description = args.join(" ").trim() || "\u8BF7\u63CF\u8FF0\u8FD9\u5F20\u56FE\u7247";
|
|
3199
|
+
ctx.renderer.printSuccess(`\u{1F4CB} \u56FE\u7247\u5DF2\u8BFB\u53D6\uFF1A${basename(imgPath)}`);
|
|
3200
|
+
await ctx.sendAsChat(`@${imgPath} ${description}`);
|
|
3201
|
+
}
|
|
3202
|
+
},
|
|
3078
3203
|
{
|
|
3079
3204
|
name: "cost",
|
|
3080
3205
|
description: "Show session token usage summary",
|
|
@@ -3189,7 +3314,7 @@ ${text}
|
|
|
3189
3314
|
let diff;
|
|
3190
3315
|
try {
|
|
3191
3316
|
const cmd = staged ? "git diff --staged" : "git diff";
|
|
3192
|
-
diff =
|
|
3317
|
+
diff = execSync3(cmd, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
3193
3318
|
} catch {
|
|
3194
3319
|
ctx.renderer.renderError("Failed to run git diff.");
|
|
3195
3320
|
return;
|
|
@@ -3256,7 +3381,7 @@ ${text}
|
|
|
3256
3381
|
description: "Run project tests and show structured report",
|
|
3257
3382
|
usage: "/test [command|filter]",
|
|
3258
3383
|
async execute(args, _ctx) {
|
|
3259
|
-
const { executeTests } = await import("./run-tests-
|
|
3384
|
+
const { executeTests } = await import("./run-tests-VVR5SMST.js");
|
|
3260
3385
|
const argStr = args.join(" ").trim();
|
|
3261
3386
|
let testArgs = {};
|
|
3262
3387
|
if (argStr) {
|
|
@@ -3479,8 +3604,8 @@ function selectFromList(prompt, items, initialIndex = 0) {
|
|
|
3479
3604
|
}
|
|
3480
3605
|
|
|
3481
3606
|
// src/tools/builtin/bash.ts
|
|
3482
|
-
import { execSync as
|
|
3483
|
-
import { existsSync as
|
|
3607
|
+
import { execSync as execSync4 } from "child_process";
|
|
3608
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3484
3609
|
import { platform as platform2 } from "os";
|
|
3485
3610
|
import { resolve as resolve2 } from "path";
|
|
3486
3611
|
var IS_WINDOWS = platform2() === "win32";
|
|
@@ -3529,7 +3654,7 @@ var bashTool = {
|
|
|
3529
3654
|
let effectiveCwd = persistentCwd;
|
|
3530
3655
|
if (cwdArg) {
|
|
3531
3656
|
const resolved = resolve2(persistentCwd, cwdArg);
|
|
3532
|
-
if (!
|
|
3657
|
+
if (!existsSync7(resolved)) {
|
|
3533
3658
|
throw new Error(
|
|
3534
3659
|
`cwd directory does not exist: "${resolved}". Create it first (e.g. mkdir -p "${resolved}") before specifying it as cwd.`
|
|
3535
3660
|
);
|
|
@@ -3545,7 +3670,7 @@ var bashTool = {
|
|
|
3545
3670
|
actualCommand = command;
|
|
3546
3671
|
}
|
|
3547
3672
|
try {
|
|
3548
|
-
const output =
|
|
3673
|
+
const output = execSync4(actualCommand, {
|
|
3549
3674
|
timeout,
|
|
3550
3675
|
encoding: IS_WINDOWS ? "buffer" : "utf-8",
|
|
3551
3676
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -3601,7 +3726,7 @@ function updateCwdFromCommand(command, baseCwd) {
|
|
|
3601
3726
|
if (!target || target.startsWith("$") || target === "~") return;
|
|
3602
3727
|
try {
|
|
3603
3728
|
const newDir = resolve2(baseCwd, target);
|
|
3604
|
-
if (
|
|
3729
|
+
if (existsSync7(newDir)) {
|
|
3605
3730
|
persistentCwd = newDir;
|
|
3606
3731
|
}
|
|
3607
3732
|
} catch {
|
|
@@ -3609,7 +3734,7 @@ function updateCwdFromCommand(command, baseCwd) {
|
|
|
3609
3734
|
}
|
|
3610
3735
|
|
|
3611
3736
|
// src/tools/builtin/read-file.ts
|
|
3612
|
-
import { readFileSync as readFileSync5, existsSync as
|
|
3737
|
+
import { readFileSync as readFileSync5, existsSync as existsSync8, statSync as statSync3 } from "fs";
|
|
3613
3738
|
import { extname, resolve as resolve3, basename as basename2, sep } from "path";
|
|
3614
3739
|
import { homedir as homedir2 } from "os";
|
|
3615
3740
|
var MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
@@ -3702,8 +3827,8 @@ var readFileTool = {
|
|
|
3702
3827
|
const encoding = args["encoding"] ?? "utf-8";
|
|
3703
3828
|
if (!filePath) throw new Error("path is required");
|
|
3704
3829
|
const normalizedPath = resolve3(filePath);
|
|
3705
|
-
if (!
|
|
3706
|
-
const { size } =
|
|
3830
|
+
if (!existsSync8(normalizedPath)) throw new Error(`File not found: ${filePath}`);
|
|
3831
|
+
const { size } = statSync3(normalizedPath);
|
|
3707
3832
|
if (size > MAX_FILE_BYTES) {
|
|
3708
3833
|
const mb = (size / 1024 / 1024).toFixed(1);
|
|
3709
3834
|
return `[File too large: ${filePath} (${mb} MB)]
|
|
@@ -3797,7 +3922,7 @@ var writeFileTool = {
|
|
|
3797
3922
|
};
|
|
3798
3923
|
|
|
3799
3924
|
// src/tools/builtin/edit-file.ts
|
|
3800
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as
|
|
3925
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "fs";
|
|
3801
3926
|
var editFileTool = {
|
|
3802
3927
|
definition: {
|
|
3803
3928
|
name: "edit_file",
|
|
@@ -3855,7 +3980,7 @@ var editFileTool = {
|
|
|
3855
3980
|
const filePath = String(args["path"] ?? "");
|
|
3856
3981
|
const encoding = args["encoding"] ?? "utf-8";
|
|
3857
3982
|
if (!filePath) throw new Error("path is required");
|
|
3858
|
-
if (!
|
|
3983
|
+
if (!existsSync9(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
3859
3984
|
const original = readFileSync6(filePath, encoding);
|
|
3860
3985
|
if (args["old_str"] !== void 0) {
|
|
3861
3986
|
const oldStr = String(args["old_str"]);
|
|
@@ -3921,8 +4046,8 @@ function truncatePreview(str, maxLen = 80) {
|
|
|
3921
4046
|
}
|
|
3922
4047
|
|
|
3923
4048
|
// src/tools/builtin/list-dir.ts
|
|
3924
|
-
import { readdirSync as readdirSync3, statSync as
|
|
3925
|
-
import { join as
|
|
4049
|
+
import { readdirSync as readdirSync3, statSync as statSync4, existsSync as existsSync10 } from "fs";
|
|
4050
|
+
import { join as join6 } from "path";
|
|
3926
4051
|
var listDirTool = {
|
|
3927
4052
|
definition: {
|
|
3928
4053
|
name: "list_dir",
|
|
@@ -3944,7 +4069,7 @@ var listDirTool = {
|
|
|
3944
4069
|
async execute(args) {
|
|
3945
4070
|
const dirPath = String(args["path"] ?? process.cwd());
|
|
3946
4071
|
const recursive = Boolean(args["recursive"] ?? false);
|
|
3947
|
-
if (!
|
|
4072
|
+
if (!existsSync10(dirPath)) throw new Error(`Directory not found: ${dirPath}`);
|
|
3948
4073
|
const lines = [`Directory: ${dirPath}
|
|
3949
4074
|
`];
|
|
3950
4075
|
listRecursive(dirPath, "", recursive, lines);
|
|
@@ -3973,11 +4098,11 @@ function listRecursive(basePath, indent, recursive, lines) {
|
|
|
3973
4098
|
if (entry.isDirectory()) {
|
|
3974
4099
|
lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
|
|
3975
4100
|
if (recursive) {
|
|
3976
|
-
listRecursive(
|
|
4101
|
+
listRecursive(join6(basePath, entry.name), indent + " ", true, lines);
|
|
3977
4102
|
}
|
|
3978
4103
|
} else {
|
|
3979
4104
|
try {
|
|
3980
|
-
const stat =
|
|
4105
|
+
const stat = statSync4(join6(basePath, entry.name));
|
|
3981
4106
|
const size = formatSize(stat.size);
|
|
3982
4107
|
lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
|
|
3983
4108
|
} catch {
|
|
@@ -3993,8 +4118,8 @@ function formatSize(bytes) {
|
|
|
3993
4118
|
}
|
|
3994
4119
|
|
|
3995
4120
|
// src/tools/builtin/grep-files.ts
|
|
3996
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as
|
|
3997
|
-
import { join as
|
|
4121
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync5, existsSync as existsSync11 } from "fs";
|
|
4122
|
+
import { join as join7, relative } from "path";
|
|
3998
4123
|
var grepFilesTool = {
|
|
3999
4124
|
definition: {
|
|
4000
4125
|
name: "grep_files",
|
|
@@ -4045,7 +4170,7 @@ var grepFilesTool = {
|
|
|
4045
4170
|
const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
|
|
4046
4171
|
const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
|
|
4047
4172
|
if (!pattern) throw new Error("pattern is required");
|
|
4048
|
-
if (!
|
|
4173
|
+
if (!existsSync11(rootPath)) throw new Error(`Path not found: ${rootPath}`);
|
|
4049
4174
|
let regex;
|
|
4050
4175
|
try {
|
|
4051
4176
|
regex = new RegExp(pattern, ignoreCase ? "gi" : "g");
|
|
@@ -4054,7 +4179,7 @@ var grepFilesTool = {
|
|
|
4054
4179
|
regex = new RegExp(escaped, ignoreCase ? "gi" : "g");
|
|
4055
4180
|
}
|
|
4056
4181
|
const results = [];
|
|
4057
|
-
const stat =
|
|
4182
|
+
const stat = statSync5(rootPath);
|
|
4058
4183
|
if (stat.isFile()) {
|
|
4059
4184
|
searchInFile(rootPath, rootPath, regex, contextLines, maxResults, results);
|
|
4060
4185
|
} else {
|
|
@@ -4118,11 +4243,11 @@ function collectFiles(dirPath, filePattern, results, regex, contextLines, maxRes
|
|
|
4118
4243
|
if (results.length >= maxResults) return;
|
|
4119
4244
|
if (entry.isDirectory()) {
|
|
4120
4245
|
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
4121
|
-
collectFiles(
|
|
4246
|
+
collectFiles(join7(dirPath, entry.name), filePattern, results, regex, contextLines, maxResults, rootPath);
|
|
4122
4247
|
} else if (entry.isFile()) {
|
|
4123
4248
|
if (isBinary(entry.name)) continue;
|
|
4124
4249
|
if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
|
|
4125
|
-
const fullPath =
|
|
4250
|
+
const fullPath = join7(dirPath, entry.name);
|
|
4126
4251
|
const relPath = relative(rootPath, fullPath);
|
|
4127
4252
|
searchInFile(fullPath, relPath, regex, contextLines, maxResults, results);
|
|
4128
4253
|
}
|
|
@@ -4163,8 +4288,8 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
|
|
|
4163
4288
|
}
|
|
4164
4289
|
|
|
4165
4290
|
// src/tools/builtin/glob-files.ts
|
|
4166
|
-
import { readdirSync as readdirSync5, statSync as
|
|
4167
|
-
import { join as
|
|
4291
|
+
import { readdirSync as readdirSync5, statSync as statSync6, existsSync as existsSync12 } from "fs";
|
|
4292
|
+
import { join as join8, relative as relative2, basename as basename3 } from "path";
|
|
4168
4293
|
var globFilesTool = {
|
|
4169
4294
|
definition: {
|
|
4170
4295
|
name: "glob_files",
|
|
@@ -4197,7 +4322,7 @@ var globFilesTool = {
|
|
|
4197
4322
|
const rootPath = String(args["path"] ?? process.cwd());
|
|
4198
4323
|
const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
|
|
4199
4324
|
if (!pattern) throw new Error("pattern is required");
|
|
4200
|
-
if (!
|
|
4325
|
+
if (!existsSync12(rootPath)) throw new Error(`Path not found: ${rootPath}`);
|
|
4201
4326
|
const regex = globToRegex(pattern);
|
|
4202
4327
|
const matches = [];
|
|
4203
4328
|
collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
|
|
@@ -4274,7 +4399,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
|
|
|
4274
4399
|
}
|
|
4275
4400
|
for (const entry of entries) {
|
|
4276
4401
|
if (results.length >= maxResults) break;
|
|
4277
|
-
const fullPath =
|
|
4402
|
+
const fullPath = join8(dirPath, entry.name);
|
|
4278
4403
|
if (entry.isDirectory()) {
|
|
4279
4404
|
if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
4280
4405
|
collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
|
|
@@ -4282,7 +4407,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
|
|
|
4282
4407
|
const relPath = relative2(rootPath, fullPath).replace(/\\/g, "/");
|
|
4283
4408
|
if (regex.test(relPath) || regex.test(basename3(relPath))) {
|
|
4284
4409
|
try {
|
|
4285
|
-
const stat =
|
|
4410
|
+
const stat = statSync6(fullPath);
|
|
4286
4411
|
results.push({ relPath, absPath: fullPath, mtime: stat.mtimeMs });
|
|
4287
4412
|
} catch {
|
|
4288
4413
|
results.push({ relPath, absPath: fullPath, mtime: 0 });
|
|
@@ -4666,11 +4791,11 @@ var saveLastResponseTool = {
|
|
|
4666
4791
|
};
|
|
4667
4792
|
|
|
4668
4793
|
// src/tools/builtin/save-memory.ts
|
|
4669
|
-
import { existsSync as
|
|
4670
|
-
import { join as
|
|
4794
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8, appendFileSync as appendFileSync2, mkdirSync as mkdirSync7 } from "fs";
|
|
4795
|
+
import { join as join9 } from "path";
|
|
4671
4796
|
import { homedir as homedir3 } from "os";
|
|
4672
4797
|
function getMemoryFilePath() {
|
|
4673
|
-
return
|
|
4798
|
+
return join9(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
|
|
4674
4799
|
}
|
|
4675
4800
|
function formatTimestamp() {
|
|
4676
4801
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -4694,8 +4819,8 @@ var saveMemoryTool = {
|
|
|
4694
4819
|
const content = String(args["content"] ?? "").trim();
|
|
4695
4820
|
if (!content) throw new Error("content is required");
|
|
4696
4821
|
const memoryPath = getMemoryFilePath();
|
|
4697
|
-
const configDir =
|
|
4698
|
-
if (!
|
|
4822
|
+
const configDir = join9(homedir3(), CONFIG_DIR_NAME);
|
|
4823
|
+
if (!existsSync13(configDir)) {
|
|
4699
4824
|
mkdirSync7(configDir, { recursive: true });
|
|
4700
4825
|
}
|
|
4701
4826
|
const timestamp = formatTimestamp();
|
|
@@ -5285,8 +5410,8 @@ var spawnAgentTool = {
|
|
|
5285
5410
|
|
|
5286
5411
|
// src/tools/registry.ts
|
|
5287
5412
|
import { pathToFileURL } from "url";
|
|
5288
|
-
import { existsSync as
|
|
5289
|
-
import { join as
|
|
5413
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync6 } from "fs";
|
|
5414
|
+
import { join as join10 } from "path";
|
|
5290
5415
|
var ToolRegistry = class {
|
|
5291
5416
|
tools = /* @__PURE__ */ new Map();
|
|
5292
5417
|
pluginToolNames = /* @__PURE__ */ new Set();
|
|
@@ -5359,7 +5484,7 @@ var ToolRegistry = class {
|
|
|
5359
5484
|
* Returns the number of successfully loaded plugins.
|
|
5360
5485
|
*/
|
|
5361
5486
|
async loadPlugins(pluginsDir, allowPlugins = false) {
|
|
5362
|
-
if (!
|
|
5487
|
+
if (!existsSync14(pluginsDir)) {
|
|
5363
5488
|
try {
|
|
5364
5489
|
mkdirSync8(pluginsDir, { recursive: true });
|
|
5365
5490
|
} catch {
|
|
@@ -5385,12 +5510,12 @@ var ToolRegistry = class {
|
|
|
5385
5510
|
process.stderr.write(
|
|
5386
5511
|
`
|
|
5387
5512
|
[plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
|
|
5388
|
-
` + files.map((f) => ` + ${
|
|
5513
|
+
` + files.map((f) => ` + ${join10(pluginsDir, f)}`).join("\n") + "\n\n"
|
|
5389
5514
|
);
|
|
5390
5515
|
let loaded = 0;
|
|
5391
5516
|
for (const file of files) {
|
|
5392
5517
|
try {
|
|
5393
|
-
const fileUrl = pathToFileURL(
|
|
5518
|
+
const fileUrl = pathToFileURL(join10(pluginsDir, file)).href;
|
|
5394
5519
|
const mod = await import(fileUrl);
|
|
5395
5520
|
const tool = mod.tool ?? mod.default?.tool ?? mod.default;
|
|
5396
5521
|
if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
|
|
@@ -5419,7 +5544,7 @@ var ToolRegistry = class {
|
|
|
5419
5544
|
|
|
5420
5545
|
// src/tools/executor.ts
|
|
5421
5546
|
import chalk8 from "chalk";
|
|
5422
|
-
import { existsSync as
|
|
5547
|
+
import { existsSync as existsSync15, readFileSync as readFileSync9 } from "fs";
|
|
5423
5548
|
|
|
5424
5549
|
// src/tools/diff-utils.ts
|
|
5425
5550
|
import chalk7 from "chalk";
|
|
@@ -5575,7 +5700,7 @@ function simpleDiff(oldLines, newLines) {
|
|
|
5575
5700
|
}
|
|
5576
5701
|
|
|
5577
5702
|
// src/tools/hooks.ts
|
|
5578
|
-
import { execSync as
|
|
5703
|
+
import { execSync as execSync5 } from "child_process";
|
|
5579
5704
|
function runHook(template, vars) {
|
|
5580
5705
|
if (!template) return;
|
|
5581
5706
|
let cmd = template;
|
|
@@ -5584,7 +5709,7 @@ function runHook(template, vars) {
|
|
|
5584
5709
|
cmd = cmd.replace(/\{args\}/g, vars.args ?? "");
|
|
5585
5710
|
cmd = cmd.replace(/\{status\}/g, vars.status ?? "");
|
|
5586
5711
|
try {
|
|
5587
|
-
|
|
5712
|
+
execSync5(cmd, {
|
|
5588
5713
|
timeout: 5e3,
|
|
5589
5714
|
stdio: ["pipe", "pipe", "pipe"],
|
|
5590
5715
|
encoding: "utf-8"
|
|
@@ -5909,7 +6034,7 @@ var ToolExecutor = class {
|
|
|
5909
6034
|
const filePath = String(call.arguments["path"] ?? "");
|
|
5910
6035
|
const newContent = String(call.arguments["content"] ?? "");
|
|
5911
6036
|
if (!filePath) return;
|
|
5912
|
-
if (
|
|
6037
|
+
if (existsSync15(filePath)) {
|
|
5913
6038
|
let oldContent;
|
|
5914
6039
|
try {
|
|
5915
6040
|
oldContent = readFileSync9(filePath, "utf-8");
|
|
@@ -5935,7 +6060,7 @@ var ToolExecutor = class {
|
|
|
5935
6060
|
}
|
|
5936
6061
|
} else if (call.name === "edit_file") {
|
|
5937
6062
|
const filePath = String(call.arguments["path"] ?? "");
|
|
5938
|
-
if (!filePath || !
|
|
6063
|
+
if (!filePath || !existsSync15(filePath)) return;
|
|
5939
6064
|
const oldStr = call.arguments["old_str"];
|
|
5940
6065
|
const newStr = call.arguments["new_str"];
|
|
5941
6066
|
if (oldStr !== void 0) {
|
|
@@ -6275,9 +6400,9 @@ Managing ${displayName} API Key`);
|
|
|
6275
6400
|
};
|
|
6276
6401
|
|
|
6277
6402
|
// src/repl/custom-commands.ts
|
|
6278
|
-
import { existsSync as
|
|
6279
|
-
import { join as
|
|
6280
|
-
import { execSync as
|
|
6403
|
+
import { existsSync as existsSync16, readFileSync as readFileSync10, readdirSync as readdirSync7, mkdirSync as mkdirSync9 } from "fs";
|
|
6404
|
+
import { join as join11, extname as extname3 } from "path";
|
|
6405
|
+
import { execSync as execSync6 } from "child_process";
|
|
6281
6406
|
function parseSimpleYaml(text) {
|
|
6282
6407
|
const result = {};
|
|
6283
6408
|
for (const line of text.split("\n")) {
|
|
@@ -6322,7 +6447,7 @@ function expandTemplate(template, args) {
|
|
|
6322
6447
|
});
|
|
6323
6448
|
result = result.replace(/\{\{git-diff\}\}/g, () => {
|
|
6324
6449
|
try {
|
|
6325
|
-
return
|
|
6450
|
+
return execSync6("git diff", { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
6326
6451
|
} catch {
|
|
6327
6452
|
return "[No git diff available]";
|
|
6328
6453
|
}
|
|
@@ -6340,14 +6465,14 @@ var CustomCommandManager = class {
|
|
|
6340
6465
|
commands = /* @__PURE__ */ new Map();
|
|
6341
6466
|
loadCommands() {
|
|
6342
6467
|
this.commands.clear();
|
|
6343
|
-
if (!
|
|
6468
|
+
if (!existsSync16(this.commandsDir)) {
|
|
6344
6469
|
mkdirSync9(this.commandsDir, { recursive: true });
|
|
6345
6470
|
return 0;
|
|
6346
6471
|
}
|
|
6347
6472
|
let count = 0;
|
|
6348
6473
|
for (const file of readdirSync7(this.commandsDir)) {
|
|
6349
6474
|
if (extname3(file) !== ".md") continue;
|
|
6350
|
-
const cmd = parseCommandFile(
|
|
6475
|
+
const cmd = parseCommandFile(join11(this.commandsDir, file));
|
|
6351
6476
|
if (cmd) {
|
|
6352
6477
|
this.commands.set(cmd.meta.name, cmd);
|
|
6353
6478
|
count++;
|
|
@@ -6364,8 +6489,8 @@ var CustomCommandManager = class {
|
|
|
6364
6489
|
};
|
|
6365
6490
|
|
|
6366
6491
|
// src/repl/dev-state.ts
|
|
6367
|
-
import { existsSync as
|
|
6368
|
-
import { join as
|
|
6492
|
+
import { existsSync as existsSync17, readFileSync as readFileSync11, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3, mkdirSync as mkdirSync10 } from "fs";
|
|
6493
|
+
import { join as join12 } from "path";
|
|
6369
6494
|
import { homedir as homedir4 } from "os";
|
|
6370
6495
|
var DEV_STATE_MAX_CHARS = 4e3;
|
|
6371
6496
|
var SNAPSHOT_PROMPT = `You are about to be replaced by a different AI model. Please generate a structured development state snapshot so the next model can continue seamlessly.
|
|
@@ -6404,11 +6529,11 @@ function sessionHasMeaningfulContent(messages) {
|
|
|
6404
6529
|
return hasUser && hasAssistant;
|
|
6405
6530
|
}
|
|
6406
6531
|
function getDevStatePath() {
|
|
6407
|
-
return
|
|
6532
|
+
return join12(homedir4(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
|
|
6408
6533
|
}
|
|
6409
6534
|
function saveDevState(content) {
|
|
6410
|
-
const configDir =
|
|
6411
|
-
if (!
|
|
6535
|
+
const configDir = join12(homedir4(), CONFIG_DIR_NAME);
|
|
6536
|
+
if (!existsSync17(configDir)) {
|
|
6412
6537
|
mkdirSync10(configDir, { recursive: true });
|
|
6413
6538
|
}
|
|
6414
6539
|
let trimmed = content.trim();
|
|
@@ -6424,15 +6549,15 @@ function saveDevState(content) {
|
|
|
6424
6549
|
}
|
|
6425
6550
|
function loadDevState() {
|
|
6426
6551
|
const path = getDevStatePath();
|
|
6427
|
-
if (!
|
|
6552
|
+
if (!existsSync17(path)) return null;
|
|
6428
6553
|
const content = readFileSync11(path, "utf-8").trim();
|
|
6429
6554
|
return content || null;
|
|
6430
6555
|
}
|
|
6431
6556
|
function clearDevState() {
|
|
6432
6557
|
const path = getDevStatePath();
|
|
6433
|
-
if (
|
|
6558
|
+
if (existsSync17(path)) {
|
|
6434
6559
|
try {
|
|
6435
|
-
|
|
6560
|
+
unlinkSync3(path);
|
|
6436
6561
|
} catch {
|
|
6437
6562
|
}
|
|
6438
6563
|
}
|
|
@@ -6851,8 +6976,8 @@ var McpManager = class {
|
|
|
6851
6976
|
};
|
|
6852
6977
|
|
|
6853
6978
|
// src/skills/manager.ts
|
|
6854
|
-
import { existsSync as
|
|
6855
|
-
import { join as
|
|
6979
|
+
import { existsSync as existsSync18, readdirSync as readdirSync8, mkdirSync as mkdirSync11 } from "fs";
|
|
6980
|
+
import { join as join13 } from "path";
|
|
6856
6981
|
|
|
6857
6982
|
// src/skills/types.ts
|
|
6858
6983
|
import { readFileSync as readFileSync12 } from "fs";
|
|
@@ -6917,7 +7042,7 @@ var SkillManager = class {
|
|
|
6917
7042
|
/** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
|
|
6918
7043
|
loadSkills() {
|
|
6919
7044
|
this.skills.clear();
|
|
6920
|
-
if (!
|
|
7045
|
+
if (!existsSync18(this.skillsDir)) {
|
|
6921
7046
|
try {
|
|
6922
7047
|
mkdirSync11(this.skillsDir, { recursive: true });
|
|
6923
7048
|
} catch {
|
|
@@ -6932,7 +7057,7 @@ var SkillManager = class {
|
|
|
6932
7057
|
}
|
|
6933
7058
|
for (const entry of entries) {
|
|
6934
7059
|
if (!entry.endsWith(".md")) continue;
|
|
6935
|
-
const filePath =
|
|
7060
|
+
const filePath = join13(this.skillsDir, entry);
|
|
6936
7061
|
const skill = parseSkillFile(filePath);
|
|
6937
7062
|
if (skill) {
|
|
6938
7063
|
this.skills.set(skill.meta.name, skill);
|
|
@@ -7000,12 +7125,12 @@ function parseAtReferences(input2, cwd) {
|
|
|
7000
7125
|
const absPath = resolve4(cwd, rawPath);
|
|
7001
7126
|
const ext = extname4(rawPath).toLowerCase();
|
|
7002
7127
|
const mime = IMAGE_MIME[ext];
|
|
7003
|
-
if (!
|
|
7128
|
+
if (!existsSync19(absPath)) {
|
|
7004
7129
|
refs.push({ path: rawPath, type: "notfound" });
|
|
7005
7130
|
continue;
|
|
7006
7131
|
}
|
|
7007
7132
|
if (mime) {
|
|
7008
|
-
const fileSize =
|
|
7133
|
+
const fileSize = statSync7(absPath).size;
|
|
7009
7134
|
if (fileSize > MAX_IMAGE_BYTES) {
|
|
7010
7135
|
refs.push({ path: rawPath, type: "toolarge" });
|
|
7011
7136
|
continue;
|
|
@@ -7102,6 +7227,10 @@ var Repl = class {
|
|
|
7102
7227
|
/** 技能管理器 */
|
|
7103
7228
|
skillManager = null;
|
|
7104
7229
|
customCommandManager = null;
|
|
7230
|
+
/** 流式生成中断控制器(ESC/Ctrl+C 中断时使用) */
|
|
7231
|
+
streamAbortController = null;
|
|
7232
|
+
/** ESC 键监听器引用(用于 removeListener 时取消注册) */
|
|
7233
|
+
_escHandler = null;
|
|
7105
7234
|
/**
|
|
7106
7235
|
* 交互式列表选择器进行中标志。
|
|
7107
7236
|
* 与 toolExecutor.confirming 类似:主循环 line handler 在此为 true 时忽略 line 事件,
|
|
@@ -7114,8 +7243,8 @@ var Repl = class {
|
|
|
7114
7243
|
*/
|
|
7115
7244
|
findContextFile(dir, candidates = CONTEXT_FILE_CANDIDATES) {
|
|
7116
7245
|
for (const candidate of candidates) {
|
|
7117
|
-
const fullPath =
|
|
7118
|
-
if (
|
|
7246
|
+
const fullPath = join14(dir, candidate);
|
|
7247
|
+
if (existsSync19(fullPath)) {
|
|
7119
7248
|
const content = readFileSync13(fullPath, "utf-8").trim();
|
|
7120
7249
|
if (content) return { filePath: fullPath, content };
|
|
7121
7250
|
}
|
|
@@ -7148,7 +7277,7 @@ var Repl = class {
|
|
|
7148
7277
|
);
|
|
7149
7278
|
return { layers: [], mergedContent: "" };
|
|
7150
7279
|
}
|
|
7151
|
-
if (
|
|
7280
|
+
if (existsSync19(fullPath)) {
|
|
7152
7281
|
const content = readFileSync13(fullPath, "utf-8").trim();
|
|
7153
7282
|
if (content) {
|
|
7154
7283
|
const layer = {
|
|
@@ -7206,8 +7335,8 @@ var Repl = class {
|
|
|
7206
7335
|
* 超过 MEMORY_MAX_CHARS 时只取末尾最新部分。
|
|
7207
7336
|
*/
|
|
7208
7337
|
loadMemoryContent() {
|
|
7209
|
-
const memoryPath =
|
|
7210
|
-
if (!
|
|
7338
|
+
const memoryPath = join14(this.config.getConfigDir(), MEMORY_FILE_NAME);
|
|
7339
|
+
if (!existsSync19(memoryPath)) return null;
|
|
7211
7340
|
let content = readFileSync13(memoryPath, "utf-8").trim();
|
|
7212
7341
|
if (!content) return null;
|
|
7213
7342
|
if (content.length > MEMORY_MAX_CHARS) {
|
|
@@ -7484,14 +7613,14 @@ ${response.content.trim()}
|
|
|
7484
7613
|
process.stdout.write(chalk10.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
|
|
7485
7614
|
`));
|
|
7486
7615
|
}
|
|
7487
|
-
const skillsDir =
|
|
7616
|
+
const skillsDir = join14(this.config.getConfigDir(), SKILLS_DIR_NAME);
|
|
7488
7617
|
this.skillManager = new SkillManager(skillsDir);
|
|
7489
7618
|
const skillCount = this.skillManager.loadSkills();
|
|
7490
7619
|
if (skillCount > 0) {
|
|
7491
7620
|
process.stdout.write(chalk10.dim(` \u{1F3AF} Skills: ${skillCount} available (use /skill to manage)
|
|
7492
7621
|
`));
|
|
7493
7622
|
}
|
|
7494
|
-
const commandsDir =
|
|
7623
|
+
const commandsDir = join14(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
|
|
7495
7624
|
this.customCommandManager = new CustomCommandManager(commandsDir);
|
|
7496
7625
|
const customCmdCount = this.customCommandManager.loadCommands();
|
|
7497
7626
|
if (customCmdCount > 0) {
|
|
@@ -7515,7 +7644,12 @@ ${response.content.trim()}
|
|
|
7515
7644
|
);
|
|
7516
7645
|
}
|
|
7517
7646
|
}
|
|
7647
|
+
this.setupClipboardPaste();
|
|
7518
7648
|
this.rl.on("SIGINT", () => {
|
|
7649
|
+
if (this.streamAbortController) {
|
|
7650
|
+
this.streamAbortController.abort();
|
|
7651
|
+
return;
|
|
7652
|
+
}
|
|
7519
7653
|
if (this.toolExecutor.confirming) {
|
|
7520
7654
|
this.toolExecutor.cancelConfirm();
|
|
7521
7655
|
return;
|
|
@@ -7830,15 +7964,15 @@ ${response.content.trim()}
|
|
|
7830
7964
|
const dir = normalized.includes("/") ? dirname5(normalized) : ".";
|
|
7831
7965
|
const prefix = normalized.includes("/") ? basename5(normalized) : normalized;
|
|
7832
7966
|
const absDir = resolve4(process.cwd(), dir);
|
|
7833
|
-
if (!
|
|
7967
|
+
if (!existsSync19(absDir)) return [];
|
|
7834
7968
|
const entries = readdirSync9(absDir);
|
|
7835
7969
|
const results = [];
|
|
7836
7970
|
for (const entry of entries) {
|
|
7837
7971
|
if (entry.startsWith(".")) continue;
|
|
7838
7972
|
if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
|
|
7839
7973
|
try {
|
|
7840
|
-
const fullPath =
|
|
7841
|
-
const stat =
|
|
7974
|
+
const fullPath = join14(absDir, entry);
|
|
7975
|
+
const stat = statSync7(fullPath);
|
|
7842
7976
|
const rel = dir === "." ? entry : `${dir}/${entry}`;
|
|
7843
7977
|
results.push(stat.isDirectory() ? `${rel}/` : rel);
|
|
7844
7978
|
} catch {
|
|
@@ -7853,36 +7987,106 @@ ${response.content.trim()}
|
|
|
7853
7987
|
shouldShowTokens() {
|
|
7854
7988
|
return this.config.get("ui").showTokenCount;
|
|
7855
7989
|
}
|
|
7990
|
+
/**
|
|
7991
|
+
* 流式生成开始前调用:创建 AbortController,监听 ESC 键(0x1b)和 Ctrl+C(0x03)原始字节。
|
|
7992
|
+
* rl.pause() 会暂停 stdin,这里临时恢复以接收原始键盘字节。
|
|
7993
|
+
*/
|
|
7994
|
+
setupStreamInterrupt() {
|
|
7995
|
+
const ac = new AbortController();
|
|
7996
|
+
this.streamAbortController = ac;
|
|
7997
|
+
process.stdin.resume();
|
|
7998
|
+
const handler = (data) => {
|
|
7999
|
+
if (data[0] === 27 && data.length === 1 || data[0] === 3) {
|
|
8000
|
+
ac.abort();
|
|
8001
|
+
}
|
|
8002
|
+
};
|
|
8003
|
+
this._escHandler = handler;
|
|
8004
|
+
process.stdin.on("data", handler);
|
|
8005
|
+
return ac;
|
|
8006
|
+
}
|
|
8007
|
+
/**
|
|
8008
|
+
* 流式生成结束后调用(finally 块中):清理 ESC 监听器,还原 stdin 暂停状态。
|
|
8009
|
+
*/
|
|
8010
|
+
teardownStreamInterrupt() {
|
|
8011
|
+
if (this._escHandler) {
|
|
8012
|
+
process.stdin.removeListener("data", this._escHandler);
|
|
8013
|
+
this._escHandler = null;
|
|
8014
|
+
}
|
|
8015
|
+
process.stdin.pause();
|
|
8016
|
+
this.streamAbortController = null;
|
|
8017
|
+
}
|
|
8018
|
+
/**
|
|
8019
|
+
* 注册 Ctrl+V 剪贴板图片粘贴快捷键。
|
|
8020
|
+
*
|
|
8021
|
+
* 使用 prependListener 先于 readline 的 quotedInsert 处理器运行:
|
|
8022
|
+
* 1. 我们的 handler 在 setImmediate 中调用 rl.write('@imgPath ')
|
|
8023
|
+
* 2. readline 的 quotedInsert 消费第一个字符('@'),字面插入 '@' — 结果相同
|
|
8024
|
+
* 3. 后续字符正常插入
|
|
8025
|
+
* 最终 readline 缓冲区包含完整的 @路径,用户可继续追加描述文字后回车发送。
|
|
8026
|
+
*/
|
|
8027
|
+
setupClipboardPaste() {
|
|
8028
|
+
process.stdin.prependListener("keypress", (_ch, key) => {
|
|
8029
|
+
if (!key?.ctrl || key?.name !== "v") return;
|
|
8030
|
+
if (this.streamAbortController || this.toolExecutor.confirming || askUserContext.prompting || this.selecting) return;
|
|
8031
|
+
const imgPath = readClipboardImage();
|
|
8032
|
+
if (imgPath) {
|
|
8033
|
+
setImmediate(() => {
|
|
8034
|
+
this.rl.write(`@${imgPath} `);
|
|
8035
|
+
process.stdout.write(chalk10.dim(`
|
|
8036
|
+
\u{1F4CB} \u56FE\u7247\u5DF2\u7C98\u8D34\uFF0C\u8BF7\u6DFB\u52A0\u63CF\u8FF0\u540E\u56DE\u8F66\u53D1\u9001
|
|
8037
|
+
`));
|
|
8038
|
+
this.showPrompt();
|
|
8039
|
+
});
|
|
8040
|
+
} else {
|
|
8041
|
+
setImmediate(() => {
|
|
8042
|
+
const hint = getClipboardHint();
|
|
8043
|
+
process.stdout.write(
|
|
8044
|
+
chalk10.dim(`
|
|
8045
|
+
\u2139 \u526A\u8D34\u677F\u4E2D\u6CA1\u6709\u56FE\u7247\u3002\u53EF\u4F7F\u7528 @\u8DEF\u5F84 \u5F15\u7528\u672C\u5730\u56FE\u7247\u6587\u4EF6\u3002`) + (hint ? chalk10.dim(`
|
|
8046
|
+
${hint}`) : "") + "\n"
|
|
8047
|
+
);
|
|
8048
|
+
this.showPrompt();
|
|
8049
|
+
});
|
|
8050
|
+
}
|
|
8051
|
+
});
|
|
8052
|
+
}
|
|
7856
8053
|
async handleChatSimple(provider, messages) {
|
|
7857
8054
|
const session = this.sessions.current;
|
|
7858
8055
|
const useStreaming = this.config.get("ui").streaming;
|
|
7859
8056
|
const modelParams = this.getModelParams();
|
|
7860
8057
|
if (useStreaming) {
|
|
7861
|
-
const
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
if (
|
|
7884
|
-
this.
|
|
8058
|
+
const ac = this.setupStreamInterrupt();
|
|
8059
|
+
try {
|
|
8060
|
+
const stream = provider.chatStream({
|
|
8061
|
+
messages,
|
|
8062
|
+
model: this.currentModel,
|
|
8063
|
+
systemPrompt: this.buildCurrentSystemPrompt(),
|
|
8064
|
+
stream: true,
|
|
8065
|
+
temperature: modelParams.temperature,
|
|
8066
|
+
maxTokens: modelParams.maxTokens,
|
|
8067
|
+
timeout: modelParams.timeout,
|
|
8068
|
+
thinking: modelParams.thinking,
|
|
8069
|
+
signal: ac.signal
|
|
8070
|
+
});
|
|
8071
|
+
const showTokens = this.shouldShowTokens();
|
|
8072
|
+
const { content, usage, tokensShown } = await this.renderer.renderStream(stream, {
|
|
8073
|
+
showTokens,
|
|
8074
|
+
sessionTotal: showTokens ? { ...this.sessionTokenUsage } : void 0,
|
|
8075
|
+
signal: ac.signal
|
|
8076
|
+
});
|
|
8077
|
+
lastResponseStore.content = content;
|
|
8078
|
+
session.addMessage({ role: "assistant", content, timestamp: /* @__PURE__ */ new Date() });
|
|
8079
|
+
this.events.emit("message.after", { content });
|
|
8080
|
+
if (usage) {
|
|
8081
|
+
this.sessionTokenUsage.inputTokens += usage.inputTokens;
|
|
8082
|
+
this.sessionTokenUsage.outputTokens += usage.outputTokens;
|
|
8083
|
+
session.addTokenUsage(usage);
|
|
8084
|
+
if (showTokens && !tokensShown) {
|
|
8085
|
+
this.renderer.renderUsage(usage, this.sessionTokenUsage);
|
|
8086
|
+
}
|
|
7885
8087
|
}
|
|
8088
|
+
} finally {
|
|
8089
|
+
this.teardownStreamInterrupt();
|
|
7886
8090
|
}
|
|
7887
8091
|
} else {
|
|
7888
8092
|
const spinner = this.renderer.showSpinner("Thinking...");
|
|
@@ -7984,46 +8188,52 @@ ${response.content.trim()}
|
|
|
7984
8188
|
const saveToFile = String(saveLastResponseCall.arguments["path"] ?? "");
|
|
7985
8189
|
if (!saveToFile) {
|
|
7986
8190
|
} else {
|
|
7987
|
-
const
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
this.
|
|
8191
|
+
const teeAc = this.setupStreamInterrupt();
|
|
8192
|
+
try {
|
|
8193
|
+
const genStream = provider.chatStream({
|
|
8194
|
+
messages: apiMessages,
|
|
8195
|
+
model: this.currentModel,
|
|
8196
|
+
systemPrompt,
|
|
8197
|
+
stream: true,
|
|
8198
|
+
temperature: modelParams.temperature,
|
|
8199
|
+
maxTokens: modelParams.maxTokens,
|
|
8200
|
+
timeout: modelParams.timeout,
|
|
8201
|
+
thinking: modelParams.thinking,
|
|
8202
|
+
signal: teeAc.signal,
|
|
8203
|
+
...extraMessages.length > 0 ? { _extraMessages: extraMessages } : {}
|
|
8204
|
+
});
|
|
8205
|
+
const teeShowTokens = this.shouldShowTokens();
|
|
8206
|
+
const { content: genContent, usage: genUsage, tokensShown: teeTokShown } = await this.renderer.renderStream(
|
|
8207
|
+
genStream,
|
|
8208
|
+
{ saveToFile, showTokens: teeShowTokens, sessionTotal: teeShowTokens ? { ...this.sessionTokenUsage } : void 0, signal: teeAc.signal }
|
|
8209
|
+
);
|
|
8210
|
+
lastResponseStore.content = genContent;
|
|
8211
|
+
if (genUsage) {
|
|
8212
|
+
roundUsage.inputTokens += genUsage.inputTokens;
|
|
8213
|
+
roundUsage.outputTokens += genUsage.outputTokens;
|
|
8214
|
+
}
|
|
8215
|
+
session.addMessage({ role: "assistant", content: genContent, timestamp: /* @__PURE__ */ new Date() });
|
|
8216
|
+
this.events.emit("message.after", { content: genContent });
|
|
8217
|
+
const lines = genContent.split("\n").length;
|
|
8218
|
+
const bytes = Buffer.byteLength(genContent, "utf-8");
|
|
8219
|
+
const syntheticResults = result.toolCalls.map((tc) => ({
|
|
8220
|
+
callId: tc.id,
|
|
8221
|
+
content: tc.name === "save_last_response" ? `File saved: ${saveToFile} (${lines} lines, ${bytes} bytes)` : `[skipped: file already saved by tee streaming]`,
|
|
8222
|
+
isError: false
|
|
8223
|
+
}));
|
|
8224
|
+
const reasoningContent2 = "reasoningContent" in result ? result.reasoningContent : void 0;
|
|
8225
|
+
const newMsgs2 = provider.buildToolResultMessages(result.toolCalls, syntheticResults, reasoningContent2);
|
|
8226
|
+
extraMessages.push(...newMsgs2);
|
|
8227
|
+
if (roundUsage.inputTokens > 0 || roundUsage.outputTokens > 0) {
|
|
8228
|
+
this.sessionTokenUsage.inputTokens += roundUsage.inputTokens;
|
|
8229
|
+
this.sessionTokenUsage.outputTokens += roundUsage.outputTokens;
|
|
8230
|
+
session.addTokenUsage(roundUsage);
|
|
8231
|
+
if (teeShowTokens && !teeTokShown) {
|
|
8232
|
+
this.renderer.renderUsage(roundUsage, this.sessionTokenUsage);
|
|
8233
|
+
}
|
|
8026
8234
|
}
|
|
8235
|
+
} finally {
|
|
8236
|
+
this.teardownStreamInterrupt();
|
|
8027
8237
|
}
|
|
8028
8238
|
return;
|
|
8029
8239
|
}
|