jinzd-ai-cli 0.4.62 → 0.4.64
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/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/{chunk-6OTF2ILP.js → chunk-JSAPO5GI.js} +6 -2
- package/dist/{chunk-YUUCUJHU.js → chunk-NJEMUAJD.js} +1 -1
- package/dist/{chunk-LLI6COMK.js → chunk-S6P5MYUF.js} +1 -1
- package/dist/{chunk-672OV76Z.js → chunk-YF3Z47AT.js} +41 -17
- package/dist/{hub-2XLQSZY2.js → hub-KEBKTLLR.js} +1 -1
- package/dist/index.js +201 -34
- package/dist/{run-tests-ORVJAUJG.js → run-tests-WNHU6QH3.js} +1 -1
- package/dist/{run-tests-HHIQ3HLM.js → run-tests-ZABNMYY2.js} +1 -1
- package/dist/{server-HBAOUOIC.js → server-R7OJYTSP.js} +50 -24
- package/dist/{task-orchestrator-2ATTBG2S.js → task-orchestrator-ZTMZIHNF.js} +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/jinzd-ai-cli)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://nodejs.org)
|
|
10
|
-
[]()
|
|
11
11
|
[](https://github.com/jinzhengdong/ai-cli/releases)
|
|
12
12
|
[](https://github.com/jinzhengdong/ai-cli/actions/workflows/ci.yml)
|
|
13
13
|
|
|
@@ -378,11 +378,11 @@ The Web UI (`aicli web`) provides a full-featured browser interface:
|
|
|
378
378
|
## Testing
|
|
379
379
|
|
|
380
380
|
```bash
|
|
381
|
-
npm test # Run all
|
|
381
|
+
npm test # Run all 396 tests
|
|
382
382
|
npm run test:watch # Watch mode
|
|
383
383
|
```
|
|
384
384
|
|
|
385
|
-
|
|
385
|
+
26 test suites covering: authentication, sessions, tool types & danger levels, permissions, output truncation, diff rendering, edit-file similarity, error hierarchy, config management, env loading, provider registry, web-fetch, grep-files, hub renderer, hub discussion, hub presets, dev-state, token estimator, tool registry budget, parallel tool execution, cost tracker, session tool history.
|
|
386
386
|
|
|
387
387
|
## Documentation
|
|
388
388
|
|
package/README.zh-CN.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/jinzd-ai-cli)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://nodejs.org)
|
|
10
|
-
[]()
|
|
11
11
|
[](https://github.com/jinzhengdong/ai-cli/releases)
|
|
12
12
|
[](https://github.com/jinzhengdong/ai-cli/actions/workflows/ci.yml)
|
|
13
13
|
|
|
@@ -391,11 +391,11 @@ Web UI(`aicli web`)提供功能完备的浏览器界面:
|
|
|
391
391
|
## 测试
|
|
392
392
|
|
|
393
393
|
```bash
|
|
394
|
-
npm test # 运行全部
|
|
394
|
+
npm test # 运行全部 396 个测试
|
|
395
395
|
npm run test:watch # 监听模式
|
|
396
396
|
```
|
|
397
397
|
|
|
398
|
-
|
|
398
|
+
26 个测试套件覆盖:认证、会话、工具类型与危险级别、权限、输出截断、diff 渲染、edit-file 相似度、错误层级、配置管理、环境变量、Provider 注册、web-fetch、grep-files、Hub 渲染、Hub 讨论、Hub 预设、开发状态、Token 估算、工具注册表预算、并行工具执行、费用追踪、会话工具历史。
|
|
399
399
|
|
|
400
400
|
## 文档
|
|
401
401
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
RateLimitError,
|
|
9
9
|
schemaToJsonSchema,
|
|
10
10
|
truncateForPersist
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-YF3Z47AT.js";
|
|
12
12
|
import {
|
|
13
13
|
APP_NAME,
|
|
14
14
|
CONFIG_DIR_NAME,
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
MCP_TOOL_PREFIX,
|
|
22
22
|
PLUGINS_DIR_NAME,
|
|
23
23
|
VERSION
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-S6P5MYUF.js";
|
|
25
25
|
|
|
26
26
|
// src/config/config-manager.ts
|
|
27
27
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
@@ -182,6 +182,10 @@ var ConfigSchema = z.object({
|
|
|
182
182
|
// 实际上限还会受模型 contextWindow 动态约束(取 contextWindow/4 作为下限)。
|
|
183
183
|
// 设置为 0 或未配置时使用默认值;不建议设为小于 12_000 或大于模型 contextWindow/2。
|
|
184
184
|
maxToolOutputChars: z.number().int().min(0).default(5e5),
|
|
185
|
+
// 月度成本预算(USD)。
|
|
186
|
+
// 设置后,每次 AI 回复后会跟踪成本,接近或超过预算时在 /status 和 /cost 中显示警告。
|
|
187
|
+
// 默认 0 = 不限制。例:50 表示每月最多花 $50。
|
|
188
|
+
monthlyBudget: z.number().min(0).default(0),
|
|
185
189
|
// 插件加载开关(安全控制)
|
|
186
190
|
// 默认 false:不自动加载 ~/.aicli/plugins/ 中的插件文件。
|
|
187
191
|
// 插件以完整 Node.js 权限在主进程中执行(可读写文件、访问网络、执行命令),
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
SUBAGENT_DEFAULT_MAX_ROUNDS,
|
|
11
11
|
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
12
12
|
runTestsTool
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-S6P5MYUF.js";
|
|
14
14
|
|
|
15
15
|
// src/tools/builtin/bash.ts
|
|
16
16
|
import { execSync } from "child_process";
|
|
@@ -1365,14 +1365,19 @@ var ToolExecutor = class {
|
|
|
1365
1365
|
}
|
|
1366
1366
|
}
|
|
1367
1367
|
async executeAll(calls) {
|
|
1368
|
-
const
|
|
1368
|
+
const safeParallel = [];
|
|
1369
|
+
const safeBash = [];
|
|
1369
1370
|
const fileWriteCalls = [];
|
|
1370
1371
|
const otherCalls = [];
|
|
1371
1372
|
for (let i = 0; i < calls.length; i++) {
|
|
1372
1373
|
const call = calls[i];
|
|
1373
1374
|
const level = getDangerLevel(call.name, call.arguments);
|
|
1374
1375
|
if (level === "safe") {
|
|
1375
|
-
|
|
1376
|
+
if (call.name === "bash") {
|
|
1377
|
+
safeBash.push({ idx: i, call });
|
|
1378
|
+
} else {
|
|
1379
|
+
safeParallel.push({ idx: i, call });
|
|
1380
|
+
}
|
|
1376
1381
|
} else if (isFileWriteTool(call.name) && level === "write") {
|
|
1377
1382
|
fileWriteCalls.push({ idx: i, call });
|
|
1378
1383
|
} else {
|
|
@@ -1380,11 +1385,20 @@ var ToolExecutor = class {
|
|
|
1380
1385
|
}
|
|
1381
1386
|
}
|
|
1382
1387
|
const results = new Array(calls.length);
|
|
1383
|
-
|
|
1384
|
-
|
|
1388
|
+
const t0 = Date.now();
|
|
1389
|
+
const parallelPhase = safeParallel.length > 0 ? Promise.all(safeParallel.map(async ({ idx, call }) => {
|
|
1390
|
+
results[idx] = await this.execute(call);
|
|
1391
|
+
})) : Promise.resolve();
|
|
1392
|
+
const bashPhase = (async () => {
|
|
1393
|
+
for (const { idx, call } of safeBash) {
|
|
1385
1394
|
results[idx] = await this.execute(call);
|
|
1386
|
-
}
|
|
1387
|
-
);
|
|
1395
|
+
}
|
|
1396
|
+
})();
|
|
1397
|
+
await Promise.all([parallelPhase, bashPhase]);
|
|
1398
|
+
if (safeParallel.length >= 2) {
|
|
1399
|
+
const elapsed = Date.now() - t0;
|
|
1400
|
+
console.log(theme.dim(` \u26A1 ${safeParallel.length} tools executed in parallel (${elapsed}ms)`));
|
|
1401
|
+
}
|
|
1388
1402
|
if (fileWriteCalls.length === 1) {
|
|
1389
1403
|
const { idx, call } = fileWriteCalls[0];
|
|
1390
1404
|
results[idx] = await this.execute(call);
|
|
@@ -1418,31 +1432,41 @@ var ToolExecutor = class {
|
|
|
1418
1432
|
if (this.sessionAutoApprove) {
|
|
1419
1433
|
console.log(theme.warning(" \u26A1 All auto-approved (session /yolo mode)"));
|
|
1420
1434
|
}
|
|
1421
|
-
const results =
|
|
1435
|
+
const results = new Array(calls.length);
|
|
1436
|
+
const approvedIndices = [];
|
|
1422
1437
|
for (let i = 0; i < calls.length; i++) {
|
|
1423
|
-
const call = calls[i];
|
|
1424
1438
|
const approved = decision === "all" || decision !== "none" && decision.has(i + 1);
|
|
1425
1439
|
if (approved) {
|
|
1440
|
+
approvedIndices.push(i);
|
|
1441
|
+
} else {
|
|
1442
|
+
console.log(theme.dim(` [${i + 1}] `) + theme.dim("rejected"));
|
|
1443
|
+
results[i] = { callId: calls[i].id, content: `[User rejected] The user rejected this ${calls[i].name} operation. Do not retry without asking.`, isError: true };
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
const t0 = Date.now();
|
|
1447
|
+
await Promise.all(
|
|
1448
|
+
approvedIndices.map(async (i) => {
|
|
1449
|
+
const call = calls[i];
|
|
1426
1450
|
const tool = this.registry.get(call.name);
|
|
1427
1451
|
if (!tool) {
|
|
1428
|
-
results
|
|
1429
|
-
|
|
1452
|
+
results[i] = { callId: call.id, content: `Unknown tool: ${call.name}`, isError: true };
|
|
1453
|
+
return;
|
|
1430
1454
|
}
|
|
1431
1455
|
try {
|
|
1432
1456
|
const rawContent = await tool.execute(call.arguments);
|
|
1433
1457
|
const content = truncateOutput(rawContent, call.name);
|
|
1434
1458
|
const wasTruncated = content !== rawContent;
|
|
1435
1459
|
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
1436
|
-
results
|
|
1460
|
+
results[i] = { callId: call.id, content, isError: false };
|
|
1437
1461
|
} catch (err) {
|
|
1438
1462
|
const message = err instanceof Error ? err.message : String(err);
|
|
1439
1463
|
this.printToolResult(call.name, message, true, false);
|
|
1440
|
-
results
|
|
1464
|
+
results[i] = { callId: call.id, content: message, isError: true };
|
|
1441
1465
|
}
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
}
|
|
1466
|
+
})
|
|
1467
|
+
);
|
|
1468
|
+
if (approvedIndices.length >= 2) {
|
|
1469
|
+
console.log(theme.dim(` \u26A1 ${approvedIndices.length} file writes executed in parallel (${Date.now() - t0}ms)`));
|
|
1446
1470
|
}
|
|
1447
1471
|
return results;
|
|
1448
1472
|
}
|
|
@@ -385,7 +385,7 @@ ${content}`);
|
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
async function runTaskMode(config, providers, configManager, topic) {
|
|
388
|
-
const { TaskOrchestrator } = await import("./task-orchestrator-
|
|
388
|
+
const { TaskOrchestrator } = await import("./task-orchestrator-ZTMZIHNF.js");
|
|
389
389
|
const orchestrator = new TaskOrchestrator(config, providers, configManager);
|
|
390
390
|
let interrupted = false;
|
|
391
391
|
const onSigint = () => {
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
saveDevState,
|
|
32
32
|
sessionHasMeaningfulContent,
|
|
33
33
|
setupProxy
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-JSAPO5GI.js";
|
|
35
35
|
import {
|
|
36
36
|
ToolExecutor,
|
|
37
37
|
ToolRegistry,
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
spawnAgentContext,
|
|
47
47
|
theme,
|
|
48
48
|
undoStack
|
|
49
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-YF3Z47AT.js";
|
|
50
50
|
import {
|
|
51
51
|
fileCheckpoints
|
|
52
52
|
} from "./chunk-4BKXL7SM.js";
|
|
@@ -71,15 +71,15 @@ import {
|
|
|
71
71
|
SKILLS_DIR_NAME,
|
|
72
72
|
VERSION,
|
|
73
73
|
buildUserIdentityPrompt
|
|
74
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-S6P5MYUF.js";
|
|
75
75
|
|
|
76
76
|
// src/index.ts
|
|
77
77
|
import { program } from "commander";
|
|
78
78
|
|
|
79
79
|
// src/repl/repl.ts
|
|
80
80
|
import * as readline from "readline";
|
|
81
|
-
import { existsSync as
|
|
82
|
-
import { join as
|
|
81
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
82
|
+
import { join as join5, resolve as resolve2, extname as extname2, dirname as dirname3, basename as basename2 } from "path";
|
|
83
83
|
import chalk4 from "chalk";
|
|
84
84
|
|
|
85
85
|
// src/repl/renderer.ts
|
|
@@ -249,7 +249,7 @@ var Renderer = class {
|
|
|
249
249
|
console.log(theme.dim(" /yolo /exit"));
|
|
250
250
|
console.log(HR);
|
|
251
251
|
console.log(theme.dim(" Key Features:"));
|
|
252
|
-
console.log(feat("Agentic loop (up to
|
|
252
|
+
console.log(feat("Agentic loop (up to 200 tool-call rounds, configurable via config/CLI, final answer streamed)"));
|
|
253
253
|
console.log(feat("Multimodal input: @filepath to inline images (base64) or text into messages"));
|
|
254
254
|
console.log(feat("Git context awareness: auto-inject branch name and file change status on startup"));
|
|
255
255
|
console.log(feat("Project context files: auto-load AICLI.md / CLAUDE.md (3 levels: global/project/subdir)"));
|
|
@@ -306,6 +306,11 @@ var Renderer = class {
|
|
|
306
306
|
console.log(feat("Context injection: aicli hub -c doc.md \u2014 inject external documents for all agents"));
|
|
307
307
|
console.log(feat("Task Mode: aicli hub --task \u2014 agents plan, write code, and execute with tools (plan\u2192approve\u2192execute\u2192review)"));
|
|
308
308
|
console.log(feat("Ollama local models: built-in provider, no API key, auto-discovers installed models via /v1/models"));
|
|
309
|
+
console.log(feat("MCP tool budget: auto-trim MCP tool definitions when exceeding 20% context window, prioritize used tools"));
|
|
310
|
+
console.log(feat("Smart compact: tool-history-aware compression preserves full tool call rounds (no mid-round splits)"));
|
|
311
|
+
console.log(feat("Session size control: auto-trim old tool output when session exceeds 2MB, keep recent rounds intact"));
|
|
312
|
+
console.log(feat("Crash recovery: detect incomplete agentic loops on /resume, warn and offer continuation"));
|
|
313
|
+
console.log(feat("Cost dashboard: /cost history shows cross-session daily/weekly/monthly spend with budget progress bar"));
|
|
309
314
|
console.log();
|
|
310
315
|
}
|
|
311
316
|
printPrompt(provider, _model) {
|
|
@@ -938,7 +943,7 @@ function createDefaultCommands() {
|
|
|
938
943
|
" /config [set|get|show] - Config wizard, or get/set values, show all",
|
|
939
944
|
" /copy - Copy last AI response to clipboard",
|
|
940
945
|
" /paste [description] - Read image from clipboard and send to AI",
|
|
941
|
-
" /cost [reset]
|
|
946
|
+
" /cost [reset | history] - Show session token usage, reset, or cross-session cost dashboard",
|
|
942
947
|
" /init [--force] - Generate AICLI.md by scanning project structure",
|
|
943
948
|
" /skill [name|off|list] - Manage agent skills (reusable prompt packs)",
|
|
944
949
|
" /checkpoint [save|restore|delete] <name> - Session checkpoints",
|
|
@@ -1843,7 +1848,7 @@ ${hint}` : "")
|
|
|
1843
1848
|
{
|
|
1844
1849
|
name: "cost",
|
|
1845
1850
|
description: "Show session token usage, prompt-cache hits, and USD cost",
|
|
1846
|
-
usage: "/cost [reset]",
|
|
1851
|
+
usage: "/cost [reset | history]",
|
|
1847
1852
|
execute(args, ctx) {
|
|
1848
1853
|
const sub = args[0]?.toLowerCase();
|
|
1849
1854
|
if (sub === "reset") {
|
|
@@ -1851,6 +1856,28 @@ ${hint}` : "")
|
|
|
1851
1856
|
ctx.renderer.printSuccess("Session token counters reset.");
|
|
1852
1857
|
return;
|
|
1853
1858
|
}
|
|
1859
|
+
if (sub === "history" || sub === "h") {
|
|
1860
|
+
const tracker = ctx.getCostTracker();
|
|
1861
|
+
const budget = ctx.config.get("monthlyBudget");
|
|
1862
|
+
console.log();
|
|
1863
|
+
console.log(theme.heading(" \u{1F4B0} Cross-Session Cost Dashboard"));
|
|
1864
|
+
console.log(theme.dim(" " + "\u2500".repeat(48)));
|
|
1865
|
+
const summary = tracker.formatSummary(budget);
|
|
1866
|
+
for (const line of summary.split("\n")) {
|
|
1867
|
+
console.log(theme.dim(" ") + chalk2.white(line));
|
|
1868
|
+
}
|
|
1869
|
+
console.log(theme.dim(" " + "\u2500".repeat(48)));
|
|
1870
|
+
const warning = tracker.checkBudget(budget);
|
|
1871
|
+
if (warning) {
|
|
1872
|
+
console.log(theme.warning(` ${warning}`));
|
|
1873
|
+
} else if (budget && budget > 0) {
|
|
1874
|
+
console.log(theme.success(" \u2713 Within monthly budget"));
|
|
1875
|
+
} else {
|
|
1876
|
+
console.log(theme.dim(' Tip: set "monthlyBudget" in config (e.g., 50 for $50/month)'));
|
|
1877
|
+
}
|
|
1878
|
+
console.log();
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1854
1881
|
const session = ctx.sessions.current;
|
|
1855
1882
|
const usage = session?.tokenUsage ?? {
|
|
1856
1883
|
inputTokens: 0,
|
|
@@ -2166,7 +2193,7 @@ ${hint}` : "")
|
|
|
2166
2193
|
usage: "/test [command|filter]",
|
|
2167
2194
|
async execute(args, ctx) {
|
|
2168
2195
|
try {
|
|
2169
|
-
const { executeTests } = await import("./run-tests-
|
|
2196
|
+
const { executeTests } = await import("./run-tests-WNHU6QH3.js");
|
|
2170
2197
|
const argStr = args.join(" ").trim();
|
|
2171
2198
|
let testArgs = {};
|
|
2172
2199
|
if (argStr) {
|
|
@@ -3230,6 +3257,125 @@ var CustomCommandManager = class {
|
|
|
3230
3257
|
}
|
|
3231
3258
|
};
|
|
3232
3259
|
|
|
3260
|
+
// src/core/cost-tracker.ts
|
|
3261
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
3262
|
+
import { join as join4 } from "path";
|
|
3263
|
+
var CostTracker = class {
|
|
3264
|
+
filePath;
|
|
3265
|
+
records = [];
|
|
3266
|
+
dirty = false;
|
|
3267
|
+
constructor(configDir) {
|
|
3268
|
+
this.filePath = join4(configDir, "cost-history.json");
|
|
3269
|
+
this.load();
|
|
3270
|
+
}
|
|
3271
|
+
load() {
|
|
3272
|
+
try {
|
|
3273
|
+
if (existsSync4(this.filePath)) {
|
|
3274
|
+
const data = JSON.parse(readFileSync3(this.filePath, "utf-8"));
|
|
3275
|
+
if (data.version === 1 && Array.isArray(data.records)) {
|
|
3276
|
+
this.records = data.records;
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
} catch {
|
|
3280
|
+
this.records = [];
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
/** Save to disk (atomic write). */
|
|
3284
|
+
save() {
|
|
3285
|
+
if (!this.dirty) return;
|
|
3286
|
+
const data = { version: 1, records: this.records };
|
|
3287
|
+
const tmp = this.filePath + ".tmp";
|
|
3288
|
+
writeFileSync2(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
3289
|
+
renameSync(tmp, this.filePath);
|
|
3290
|
+
this.dirty = false;
|
|
3291
|
+
}
|
|
3292
|
+
/**
|
|
3293
|
+
* Record cost from a completed session/interaction.
|
|
3294
|
+
*/
|
|
3295
|
+
addCost(provider, model, usage) {
|
|
3296
|
+
const cost = computeCost(provider, model, usage);
|
|
3297
|
+
if (cost === null || cost === 0) return;
|
|
3298
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3299
|
+
let record = this.records.find((r) => r.date === today);
|
|
3300
|
+
if (!record) {
|
|
3301
|
+
record = { date: today, cost: 0, sessions: 0, inputTokens: 0, outputTokens: 0 };
|
|
3302
|
+
this.records.push(record);
|
|
3303
|
+
}
|
|
3304
|
+
record.cost += cost;
|
|
3305
|
+
record.sessions += 1;
|
|
3306
|
+
record.inputTokens += usage.inputTokens;
|
|
3307
|
+
record.outputTokens += usage.outputTokens;
|
|
3308
|
+
this.dirty = true;
|
|
3309
|
+
if (this.records.length > 90) {
|
|
3310
|
+
this.records = this.records.slice(-90);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
/** Get total cost for a given month ("2026-04"). */
|
|
3314
|
+
getMonthlyCost(yearMonth) {
|
|
3315
|
+
const prefix = yearMonth ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
3316
|
+
return this.records.filter((r) => r.date.startsWith(prefix)).reduce((sum, r) => sum + r.cost, 0);
|
|
3317
|
+
}
|
|
3318
|
+
/** Get today's cost. */
|
|
3319
|
+
getTodayCost() {
|
|
3320
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3321
|
+
return this.records.find((r) => r.date === today)?.cost ?? 0;
|
|
3322
|
+
}
|
|
3323
|
+
/** Get total cost for last N days. */
|
|
3324
|
+
getRecentCost(days) {
|
|
3325
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
3326
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
3327
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
3328
|
+
return this.records.filter((r) => r.date >= cutoffStr).reduce((sum, r) => sum + r.cost, 0);
|
|
3329
|
+
}
|
|
3330
|
+
/** Get all records for display. */
|
|
3331
|
+
getRecords() {
|
|
3332
|
+
return [...this.records];
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Check if monthly cost exceeds budget, return warning message or null.
|
|
3336
|
+
*/
|
|
3337
|
+
checkBudget(monthlyBudget) {
|
|
3338
|
+
if (!monthlyBudget || monthlyBudget <= 0) return null;
|
|
3339
|
+
const monthlyCost = this.getMonthlyCost();
|
|
3340
|
+
const ratio = monthlyCost / monthlyBudget;
|
|
3341
|
+
if (ratio >= 1) {
|
|
3342
|
+
return `\u{1F6A8} Monthly budget exceeded: ${formatCost(monthlyCost)} / ${formatCost(monthlyBudget)} (${Math.round(ratio * 100)}%)`;
|
|
3343
|
+
}
|
|
3344
|
+
if (ratio >= 0.8) {
|
|
3345
|
+
return `\u26A0 Monthly budget warning: ${formatCost(monthlyCost)} / ${formatCost(monthlyBudget)} (${Math.round(ratio * 100)}%)`;
|
|
3346
|
+
}
|
|
3347
|
+
return null;
|
|
3348
|
+
}
|
|
3349
|
+
/**
|
|
3350
|
+
* Format a cost summary for display.
|
|
3351
|
+
*/
|
|
3352
|
+
formatSummary(monthlyBudget) {
|
|
3353
|
+
const today = this.getTodayCost();
|
|
3354
|
+
const monthly = this.getMonthlyCost();
|
|
3355
|
+
const yearMonth = (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
3356
|
+
const lines = [
|
|
3357
|
+
`Today: ${formatCost(today)}`,
|
|
3358
|
+
`This month: ${formatCost(monthly)} (${yearMonth})`
|
|
3359
|
+
];
|
|
3360
|
+
if (monthlyBudget && monthlyBudget > 0) {
|
|
3361
|
+
const ratio = monthly / monthlyBudget;
|
|
3362
|
+
const bar = "\u2588".repeat(Math.min(20, Math.round(ratio * 20))) + "\u2591".repeat(Math.max(0, 20 - Math.round(ratio * 20)));
|
|
3363
|
+
lines.push(`Budget: ${formatCost(monthlyBudget)} [${bar}] ${Math.round(ratio * 100)}%`);
|
|
3364
|
+
}
|
|
3365
|
+
const last7 = [];
|
|
3366
|
+
for (let i = 6; i >= 0; i--) {
|
|
3367
|
+
const d = /* @__PURE__ */ new Date();
|
|
3368
|
+
d.setDate(d.getDate() - i);
|
|
3369
|
+
const dateStr = d.toISOString().slice(0, 10);
|
|
3370
|
+
const record = this.records.find((r) => r.date === dateStr);
|
|
3371
|
+
const dayLabel = dateStr.slice(5);
|
|
3372
|
+
last7.push(`${dayLabel}: ${record ? formatCost(record.cost) : "$0.00"}`);
|
|
3373
|
+
}
|
|
3374
|
+
lines.push(`Last 7 days: ${last7.join(" ")}`);
|
|
3375
|
+
return lines.join("\n");
|
|
3376
|
+
}
|
|
3377
|
+
};
|
|
3378
|
+
|
|
3233
3379
|
// src/repl/notify.ts
|
|
3234
3380
|
import { spawn } from "child_process";
|
|
3235
3381
|
import { platform as platform2 } from "os";
|
|
@@ -3303,7 +3449,7 @@ function parseAtReferences(input2, cwd) {
|
|
|
3303
3449
|
const absPath = resolve2(cwd, rawPath);
|
|
3304
3450
|
const ext = extname2(rawPath).toLowerCase();
|
|
3305
3451
|
const mime = IMAGE_MIME[ext];
|
|
3306
|
-
if (!
|
|
3452
|
+
if (!existsSync5(absPath)) {
|
|
3307
3453
|
refs.push({ path: rawPath, type: "notfound" });
|
|
3308
3454
|
continue;
|
|
3309
3455
|
}
|
|
@@ -3313,7 +3459,7 @@ function parseAtReferences(input2, cwd) {
|
|
|
3313
3459
|
refs.push({ path: rawPath, type: "toolarge" });
|
|
3314
3460
|
continue;
|
|
3315
3461
|
}
|
|
3316
|
-
const data =
|
|
3462
|
+
const data = readFileSync4(absPath).toString("base64");
|
|
3317
3463
|
imageParts.push({
|
|
3318
3464
|
type: "image_url",
|
|
3319
3465
|
image_url: { url: `data:${mime};base64,${data}` }
|
|
@@ -3321,7 +3467,7 @@ function parseAtReferences(input2, cwd) {
|
|
|
3321
3467
|
refs.push({ path: rawPath, type: "image" });
|
|
3322
3468
|
textBody = textBody.replace(match[0], "").trim();
|
|
3323
3469
|
} else {
|
|
3324
|
-
const content =
|
|
3470
|
+
const content = readFileSync4(absPath, "utf-8");
|
|
3325
3471
|
const inlined = `
|
|
3326
3472
|
|
|
3327
3473
|
[File: ${rawPath}]
|
|
@@ -3390,6 +3536,7 @@ var Repl = class {
|
|
|
3390
3536
|
if (options?.blockedTools) this.blockedTools = options.blockedTools;
|
|
3391
3537
|
if (options?.resumeSessionId) this.resumeSessionId = options.resumeSessionId;
|
|
3392
3538
|
if (options?.maxToolRoundsOverride !== void 0) this.maxToolRoundsOverride = options.maxToolRoundsOverride;
|
|
3539
|
+
this.costTracker = new CostTracker(this.config.getConfigDir());
|
|
3393
3540
|
}
|
|
3394
3541
|
rl;
|
|
3395
3542
|
currentProvider;
|
|
@@ -3405,12 +3552,13 @@ var Repl = class {
|
|
|
3405
3552
|
contextLayers = [];
|
|
3406
3553
|
/** 本次会话累计 token 用量 */
|
|
3407
3554
|
sessionTokenUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
|
|
3408
|
-
/** Fold a single-request TokenUsage (with optional cache fields) into sessionTokenUsage. */
|
|
3555
|
+
/** Fold a single-request TokenUsage (with optional cache fields) into sessionTokenUsage + cost tracker. */
|
|
3409
3556
|
addSessionUsage(u) {
|
|
3410
3557
|
this.sessionTokenUsage.inputTokens += u.inputTokens;
|
|
3411
3558
|
this.sessionTokenUsage.outputTokens += u.outputTokens;
|
|
3412
3559
|
this.sessionTokenUsage.cacheCreationTokens += u.cacheCreationTokens ?? 0;
|
|
3413
3560
|
this.sessionTokenUsage.cacheReadTokens += u.cacheReadTokens ?? 0;
|
|
3561
|
+
this.costTracker.addCost(this.currentProvider, this.currentModel, u);
|
|
3414
3562
|
}
|
|
3415
3563
|
/** 启动时检测到的 Git 分支(无 git 仓库时为 null) */
|
|
3416
3564
|
gitBranch = null;
|
|
@@ -3452,6 +3600,8 @@ var Repl = class {
|
|
|
3452
3600
|
selecting = false;
|
|
3453
3601
|
/** CLI --max-tool-rounds 覆盖值;未指定时从 config.maxToolRounds 读取 */
|
|
3454
3602
|
maxToolRoundsOverride;
|
|
3603
|
+
/** 跨 session 成本追踪器 */
|
|
3604
|
+
costTracker;
|
|
3455
3605
|
// ── /add-dir 目录上下文支持 ────────────────────────────────────────────────
|
|
3456
3606
|
/**
|
|
3457
3607
|
* 扫描目录内容,返回格式化字符串(含目录树 + 关键文件内容)。
|
|
@@ -3531,7 +3681,7 @@ var Repl = class {
|
|
|
3531
3681
|
const filtered = entries.filter((e) => !SKIP_DIRS_SET.has(e));
|
|
3532
3682
|
for (let i = 0; i < filtered.length && entryCount < MAX_TREE_ENTRIES; i++) {
|
|
3533
3683
|
const name = filtered[i];
|
|
3534
|
-
const fullPath =
|
|
3684
|
+
const fullPath = join5(dir, name);
|
|
3535
3685
|
const isLast = i === filtered.length - 1;
|
|
3536
3686
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
3537
3687
|
let isDir;
|
|
@@ -3565,7 +3715,7 @@ ${treeLines.join("\n")}`
|
|
|
3565
3715
|
for (const name of entries) {
|
|
3566
3716
|
if (totalChars >= MAX_TOTAL_CHARS) break;
|
|
3567
3717
|
if (SKIP_DIRS_SET.has(name)) continue;
|
|
3568
|
-
const fullPath =
|
|
3718
|
+
const fullPath = join5(dir, name);
|
|
3569
3719
|
let st;
|
|
3570
3720
|
try {
|
|
3571
3721
|
st = statSync3(fullPath);
|
|
@@ -3580,7 +3730,7 @@ ${treeLines.join("\n")}`
|
|
|
3580
3730
|
if (!TEXT_EXTS.has(ext) && !isSpecial) continue;
|
|
3581
3731
|
if (st.size > MAX_FILE_CHARS * 3) continue;
|
|
3582
3732
|
try {
|
|
3583
|
-
let content =
|
|
3733
|
+
let content = readFileSync4(fullPath, "utf-8");
|
|
3584
3734
|
if (content.length > MAX_FILE_CHARS) {
|
|
3585
3735
|
content = content.slice(0, MAX_FILE_CHARS) + `
|
|
3586
3736
|
... (truncated, ${content.length} chars total)`;
|
|
@@ -3610,7 +3760,7 @@ ${content}
|
|
|
3610
3760
|
*/
|
|
3611
3761
|
addExtraContextDir(dirPath) {
|
|
3612
3762
|
const absPath = resolve2(dirPath);
|
|
3613
|
-
if (!
|
|
3763
|
+
if (!existsSync5(absPath)) {
|
|
3614
3764
|
return { success: false, charCount: 0, added: false, error: `Directory not found: ${dirPath}` };
|
|
3615
3765
|
}
|
|
3616
3766
|
let isDir;
|
|
@@ -3644,9 +3794,9 @@ ${content}
|
|
|
3644
3794
|
*/
|
|
3645
3795
|
findContextFile(dir, candidates = CONTEXT_FILE_CANDIDATES) {
|
|
3646
3796
|
for (const candidate of candidates) {
|
|
3647
|
-
const fullPath =
|
|
3648
|
-
if (
|
|
3649
|
-
const content =
|
|
3797
|
+
const fullPath = join5(dir, candidate);
|
|
3798
|
+
if (existsSync5(fullPath)) {
|
|
3799
|
+
const content = readFileSync4(fullPath, "utf-8").trim();
|
|
3650
3800
|
if (content) return { filePath: fullPath, content };
|
|
3651
3801
|
}
|
|
3652
3802
|
}
|
|
@@ -3674,10 +3824,10 @@ ${content}
|
|
|
3674
3824
|
const cwd = process.cwd();
|
|
3675
3825
|
const gitRoot = getGitRoot(cwd);
|
|
3676
3826
|
const projectRoot = gitRoot ?? cwd;
|
|
3677
|
-
const mcpPath =
|
|
3678
|
-
if (!
|
|
3827
|
+
const mcpPath = join5(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
3828
|
+
if (!existsSync5(mcpPath)) return null;
|
|
3679
3829
|
try {
|
|
3680
|
-
const raw = JSON.parse(
|
|
3830
|
+
const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
|
|
3681
3831
|
const servers = raw?.mcpServers;
|
|
3682
3832
|
if (!servers || typeof servers !== "object") {
|
|
3683
3833
|
process.stderr.write(
|
|
@@ -3723,8 +3873,8 @@ ${content}
|
|
|
3723
3873
|
);
|
|
3724
3874
|
return { layers: [], mergedContent: "" };
|
|
3725
3875
|
}
|
|
3726
|
-
if (
|
|
3727
|
-
const content =
|
|
3876
|
+
if (existsSync5(fullPath)) {
|
|
3877
|
+
const content = readFileSync4(fullPath, "utf-8").trim();
|
|
3728
3878
|
if (content) {
|
|
3729
3879
|
const layer = {
|
|
3730
3880
|
level: "project",
|
|
@@ -3781,9 +3931,9 @@ ${content}
|
|
|
3781
3931
|
* 超过 MEMORY_MAX_CHARS 时只取末尾最新部分。
|
|
3782
3932
|
*/
|
|
3783
3933
|
loadMemoryContent() {
|
|
3784
|
-
const memoryPath =
|
|
3785
|
-
if (!
|
|
3786
|
-
let content =
|
|
3934
|
+
const memoryPath = join5(this.config.getConfigDir(), MEMORY_FILE_NAME);
|
|
3935
|
+
if (!existsSync5(memoryPath)) return null;
|
|
3936
|
+
let content = readFileSync4(memoryPath, "utf-8").trim();
|
|
3787
3937
|
if (!content) return null;
|
|
3788
3938
|
if (content.length > MEMORY_MAX_CHARS) {
|
|
3789
3939
|
content = content.slice(-MEMORY_MAX_CHARS);
|
|
@@ -4094,6 +4244,16 @@ Session '${this.resumeSessionId}' not found.
|
|
|
4094
4244
|
process.stdout.write(
|
|
4095
4245
|
theme.dim(` \u{1F4C2} Resumed session: ${session.id.slice(0, 8)} `) + theme.dim(`(${session.messages.length} messages`) + (session.title ? theme.dim(`, "${session.title}"`) : "") + theme.dim(")\n")
|
|
4096
4246
|
);
|
|
4247
|
+
const msgs = session.messages;
|
|
4248
|
+
if (msgs.length > 0) {
|
|
4249
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
4250
|
+
const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
|
|
4251
|
+
if (isIncomplete) {
|
|
4252
|
+
process.stdout.write(
|
|
4253
|
+
theme.warning(" \u26A0 Session appears to have been interrupted mid-task (last message is tool output).\n") + theme.dim(" The AI will see the tool history and can continue where it left off.\n") + theme.dim(' Tip: type "continue where you left off" or describe what to do next.\n')
|
|
4254
|
+
);
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4097
4257
|
}
|
|
4098
4258
|
if (layers.length > 0) {
|
|
4099
4259
|
if (layers.length === 1) {
|
|
@@ -4134,14 +4294,14 @@ Session '${this.resumeSessionId}' not found.
|
|
|
4134
4294
|
process.stdout.write(theme.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
|
|
4135
4295
|
`));
|
|
4136
4296
|
}
|
|
4137
|
-
const skillsDir =
|
|
4297
|
+
const skillsDir = join5(this.config.getConfigDir(), SKILLS_DIR_NAME);
|
|
4138
4298
|
this.skillManager = new SkillManager(skillsDir);
|
|
4139
4299
|
const skillCount = this.skillManager.loadSkills();
|
|
4140
4300
|
if (skillCount > 0) {
|
|
4141
4301
|
process.stdout.write(theme.dim(` \u{1F3AF} Skills: ${skillCount} available (use /skill to manage)
|
|
4142
4302
|
`));
|
|
4143
4303
|
}
|
|
4144
|
-
const commandsDir =
|
|
4304
|
+
const commandsDir = join5(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
|
|
4145
4305
|
this.customCommandManager = new CustomCommandManager(commandsDir);
|
|
4146
4306
|
const customCmdCount = this.customCommandManager.loadCommands();
|
|
4147
4307
|
if (customCmdCount > 0) {
|
|
@@ -4329,6 +4489,12 @@ Session '${this.resumeSessionId}' not found.
|
|
|
4329
4489
|
}
|
|
4330
4490
|
await this.sessions.save();
|
|
4331
4491
|
}
|
|
4492
|
+
this.costTracker.save();
|
|
4493
|
+
const budgetWarning = this.costTracker.checkBudget(this.config.get("monthlyBudget"));
|
|
4494
|
+
if (budgetWarning) {
|
|
4495
|
+
process.stdout.write(theme.warning(` ${budgetWarning}
|
|
4496
|
+
`));
|
|
4497
|
+
}
|
|
4332
4498
|
const elapsed = Date.now() - t0;
|
|
4333
4499
|
const threshold = this.config.get("ui").notificationThreshold;
|
|
4334
4500
|
if (threshold > 0 && elapsed >= threshold) {
|
|
@@ -4592,14 +4758,14 @@ Session '${this.resumeSessionId}' not found.
|
|
|
4592
4758
|
const dir = normalized.includes("/") ? dirname3(normalized) : ".";
|
|
4593
4759
|
const prefix = normalized.includes("/") ? basename2(normalized) : normalized;
|
|
4594
4760
|
const absDir = resolve2(process.cwd(), dir);
|
|
4595
|
-
if (!
|
|
4761
|
+
if (!existsSync5(absDir)) return [];
|
|
4596
4762
|
const entries = readdirSync3(absDir);
|
|
4597
4763
|
const results = [];
|
|
4598
4764
|
for (const entry of entries) {
|
|
4599
4765
|
if (entry.startsWith(".")) continue;
|
|
4600
4766
|
if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
|
|
4601
4767
|
try {
|
|
4602
|
-
const fullPath =
|
|
4768
|
+
const fullPath = join5(absDir, entry);
|
|
4603
4769
|
const stat = statSync3(fullPath);
|
|
4604
4770
|
const rel = dir === "." ? entry : `${dir}/${entry}`;
|
|
4605
4771
|
results.push(stat.isDirectory() ? `${rel}/` : rel);
|
|
@@ -5634,6 +5800,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
|
|
|
5634
5800
|
listContextDirs: () => [...this.extraContextDirs],
|
|
5635
5801
|
forkSession: (messageCount, title) => this.sessions.forkSession(messageCount, title),
|
|
5636
5802
|
getToolExecutor: () => this.toolExecutor,
|
|
5803
|
+
getCostTracker: () => this.costTracker,
|
|
5637
5804
|
exit: () => this.handleExit()
|
|
5638
5805
|
};
|
|
5639
5806
|
await cmd.execute(args, ctx);
|
|
@@ -5738,7 +5905,7 @@ program.command("web").description("Start Web UI server with browser-based chat
|
|
|
5738
5905
|
console.error("Error: Invalid port number. Must be between 1 and 65535.");
|
|
5739
5906
|
process.exit(1);
|
|
5740
5907
|
}
|
|
5741
|
-
const { startWebServer } = await import("./server-
|
|
5908
|
+
const { startWebServer } = await import("./server-R7OJYTSP.js");
|
|
5742
5909
|
await startWebServer({ port, host: options.host });
|
|
5743
5910
|
});
|
|
5744
5911
|
program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
|
|
@@ -5971,7 +6138,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
|
|
|
5971
6138
|
}),
|
|
5972
6139
|
config.get("customProviders")
|
|
5973
6140
|
);
|
|
5974
|
-
const { startHub } = await import("./hub-
|
|
6141
|
+
const { startHub } = await import("./hub-KEBKTLLR.js");
|
|
5975
6142
|
await startHub(
|
|
5976
6143
|
{
|
|
5977
6144
|
topic: topic ?? "",
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
persistToolRound,
|
|
22
22
|
rebuildExtraMessages,
|
|
23
23
|
setupProxy
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-JSAPO5GI.js";
|
|
25
25
|
import {
|
|
26
26
|
AuthManager
|
|
27
27
|
} from "./chunk-BYNY5JPB.js";
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
spawnAgentContext,
|
|
42
42
|
truncateOutput,
|
|
43
43
|
undoStack
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-YF3Z47AT.js";
|
|
45
45
|
import "./chunk-4BKXL7SM.js";
|
|
46
46
|
import {
|
|
47
47
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
SKILLS_DIR_NAME,
|
|
62
62
|
VERSION,
|
|
63
63
|
buildUserIdentityPrompt
|
|
64
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-S6P5MYUF.js";
|
|
65
65
|
|
|
66
66
|
// src/web/server.ts
|
|
67
67
|
import express from "express";
|
|
@@ -325,14 +325,19 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
async executeAll(calls) {
|
|
328
|
-
const
|
|
328
|
+
const safeParallel = [];
|
|
329
|
+
const safeBash = [];
|
|
329
330
|
const fileWriteCalls = [];
|
|
330
331
|
const otherCalls = [];
|
|
331
332
|
for (let i = 0; i < calls.length; i++) {
|
|
332
333
|
const call = calls[i];
|
|
333
334
|
const level = getDangerLevel(call.name, call.arguments);
|
|
334
335
|
if (level === "safe") {
|
|
335
|
-
|
|
336
|
+
if (call.name === "bash") {
|
|
337
|
+
safeBash.push({ idx: i, call });
|
|
338
|
+
} else {
|
|
339
|
+
safeParallel.push({ idx: i, call });
|
|
340
|
+
}
|
|
336
341
|
} else if (isFileWriteTool(call.name) && level === "write") {
|
|
337
342
|
fileWriteCalls.push({ idx: i, call });
|
|
338
343
|
} else {
|
|
@@ -340,11 +345,15 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
340
345
|
}
|
|
341
346
|
}
|
|
342
347
|
const results = new Array(calls.length);
|
|
343
|
-
|
|
344
|
-
|
|
348
|
+
const parallelPhase = safeParallel.length > 0 ? Promise.all(safeParallel.map(async ({ idx, call }) => {
|
|
349
|
+
results[idx] = await this.execute(call);
|
|
350
|
+
})) : Promise.resolve();
|
|
351
|
+
const bashPhase = (async () => {
|
|
352
|
+
for (const { idx, call } of safeBash) {
|
|
345
353
|
results[idx] = await this.execute(call);
|
|
346
|
-
}
|
|
347
|
-
);
|
|
354
|
+
}
|
|
355
|
+
})();
|
|
356
|
+
await Promise.all([parallelPhase, bashPhase]);
|
|
348
357
|
if (fileWriteCalls.length === 1) {
|
|
349
358
|
const { idx, call } = fileWriteCalls[0];
|
|
350
359
|
results[idx] = await this.execute(call);
|
|
@@ -362,33 +371,39 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
362
371
|
async executeBatchFileWrites(items) {
|
|
363
372
|
const calls = items.map((i) => i.call);
|
|
364
373
|
const decision = this.sessionAutoApprove ? "all" : await this.batchConfirm(calls);
|
|
365
|
-
const results =
|
|
374
|
+
const results = new Array(calls.length);
|
|
375
|
+
const approvedIndices = [];
|
|
366
376
|
for (let i = 0; i < calls.length; i++) {
|
|
367
|
-
const call = calls[i];
|
|
368
377
|
if (decision === "all" || decision instanceof Set && decision.has(i + 1)) {
|
|
378
|
+
approvedIndices.push(i);
|
|
379
|
+
} else {
|
|
380
|
+
results[i] = {
|
|
381
|
+
callId: calls[i].id,
|
|
382
|
+
content: `[User rejected] File write rejected by user. Do not retry without asking.`,
|
|
383
|
+
isError: true
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
await Promise.all(
|
|
388
|
+
approvedIndices.map(async (i) => {
|
|
389
|
+
const call = calls[i];
|
|
369
390
|
const tool = this.registry.get(call.name);
|
|
370
391
|
if (!tool) {
|
|
371
|
-
results
|
|
372
|
-
|
|
392
|
+
results[i] = { callId: call.id, content: `Unknown tool: ${call.name}`, isError: true };
|
|
393
|
+
return;
|
|
373
394
|
}
|
|
374
395
|
try {
|
|
375
396
|
const rawContent = await tool.execute(call.arguments);
|
|
376
397
|
const content = truncateOutput(rawContent, call.name);
|
|
377
398
|
this.sendToolCallResult(call, rawContent, false);
|
|
378
|
-
results
|
|
399
|
+
results[i] = { callId: call.id, content, isError: false };
|
|
379
400
|
} catch (err) {
|
|
380
401
|
const message = err instanceof Error ? err.message : String(err);
|
|
381
402
|
this.sendToolCallResult(call, message, true);
|
|
382
|
-
results
|
|
403
|
+
results[i] = { callId: call.id, content: message, isError: true };
|
|
383
404
|
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
callId: call.id,
|
|
387
|
-
content: `[User rejected] File write rejected by user. Do not retry without asking.`,
|
|
388
|
-
isError: true
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
}
|
|
405
|
+
})
|
|
406
|
+
);
|
|
392
407
|
return results;
|
|
393
408
|
}
|
|
394
409
|
};
|
|
@@ -1327,6 +1342,17 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
|
|
|
1327
1342
|
this.sessions.loadSession(found.id);
|
|
1328
1343
|
this.resetWebSessionUsage();
|
|
1329
1344
|
this.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
|
|
1345
|
+
const loadedSession = this.sessions.current;
|
|
1346
|
+
if (loadedSession && loadedSession.messages.length > 0) {
|
|
1347
|
+
const lastMsg = loadedSession.messages[loadedSession.messages.length - 1];
|
|
1348
|
+
const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
|
|
1349
|
+
if (isIncomplete) {
|
|
1350
|
+
this.send({
|
|
1351
|
+
type: "info",
|
|
1352
|
+
message: '\u26A0 This session appears to have been interrupted mid-task. The AI will see the tool history and can continue where it left off. Type "continue where you left off" to resume.'
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1330
1356
|
this.sendSessionMessages();
|
|
1331
1357
|
this.sendStatus();
|
|
1332
1358
|
this.sendSessionList();
|
|
@@ -1933,7 +1959,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
1933
1959
|
case "test": {
|
|
1934
1960
|
this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
1935
1961
|
try {
|
|
1936
|
-
const { executeTests } = await import("./run-tests-
|
|
1962
|
+
const { executeTests } = await import("./run-tests-WNHU6QH3.js");
|
|
1937
1963
|
const argStr = args.join(" ").trim();
|
|
1938
1964
|
let testArgs = {};
|
|
1939
1965
|
if (argStr) {
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
getDangerLevel,
|
|
5
5
|
googleSearchContext,
|
|
6
6
|
truncateOutput
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-YF3Z47AT.js";
|
|
8
8
|
import "./chunk-4BKXL7SM.js";
|
|
9
9
|
import {
|
|
10
10
|
SUBAGENT_ALLOWED_TOOLS
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-S6P5MYUF.js";
|
|
12
12
|
|
|
13
13
|
// src/hub/task-orchestrator.ts
|
|
14
14
|
import { createInterface } from "readline";
|