jinzd-ai-cli 0.4.62 → 0.4.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-672OV76Z.js → chunk-E45EGVSY.js} +41 -17
- package/dist/{chunk-6OTF2ILP.js → chunk-EYRPTNVI.js} +6 -2
- package/dist/{chunk-YUUCUJHU.js → chunk-GUD733DE.js} +1 -1
- package/dist/{chunk-LLI6COMK.js → chunk-MLEM56CR.js} +1 -1
- package/dist/{hub-2XLQSZY2.js → hub-R6ID4F6J.js} +1 -1
- package/dist/index.js +194 -32
- package/dist/{run-tests-HHIQ3HLM.js → run-tests-A53KBZPJ.js} +1 -1
- package/dist/{run-tests-ORVJAUJG.js → run-tests-KRGPFYPF.js} +1 -1
- package/dist/{server-HBAOUOIC.js → server-BGMUORHT.js} +50 -24
- package/dist/{task-orchestrator-2ATTBG2S.js → task-orchestrator-LNJWNTSJ.js} +2 -2
- package/package.json +1 -1
|
@@ -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-MLEM56CR.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
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
RateLimitError,
|
|
9
9
|
schemaToJsonSchema,
|
|
10
10
|
truncateForPersist
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-E45EGVSY.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-MLEM56CR.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 权限在主进程中执行(可读写文件、访问网络、执行命令),
|
|
@@ -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-LNJWNTSJ.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-EYRPTNVI.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-E45EGVSY.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-MLEM56CR.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
|
|
@@ -1843,7 +1843,7 @@ ${hint}` : "")
|
|
|
1843
1843
|
{
|
|
1844
1844
|
name: "cost",
|
|
1845
1845
|
description: "Show session token usage, prompt-cache hits, and USD cost",
|
|
1846
|
-
usage: "/cost [reset]",
|
|
1846
|
+
usage: "/cost [reset | history]",
|
|
1847
1847
|
execute(args, ctx) {
|
|
1848
1848
|
const sub = args[0]?.toLowerCase();
|
|
1849
1849
|
if (sub === "reset") {
|
|
@@ -1851,6 +1851,28 @@ ${hint}` : "")
|
|
|
1851
1851
|
ctx.renderer.printSuccess("Session token counters reset.");
|
|
1852
1852
|
return;
|
|
1853
1853
|
}
|
|
1854
|
+
if (sub === "history" || sub === "h") {
|
|
1855
|
+
const tracker = ctx.getCostTracker();
|
|
1856
|
+
const budget = ctx.config.get("monthlyBudget");
|
|
1857
|
+
console.log();
|
|
1858
|
+
console.log(theme.heading(" \u{1F4B0} Cross-Session Cost Dashboard"));
|
|
1859
|
+
console.log(theme.dim(" " + "\u2500".repeat(48)));
|
|
1860
|
+
const summary = tracker.formatSummary(budget);
|
|
1861
|
+
for (const line of summary.split("\n")) {
|
|
1862
|
+
console.log(theme.dim(" ") + chalk2.white(line));
|
|
1863
|
+
}
|
|
1864
|
+
console.log(theme.dim(" " + "\u2500".repeat(48)));
|
|
1865
|
+
const warning = tracker.checkBudget(budget);
|
|
1866
|
+
if (warning) {
|
|
1867
|
+
console.log(theme.warning(` ${warning}`));
|
|
1868
|
+
} else if (budget && budget > 0) {
|
|
1869
|
+
console.log(theme.success(" \u2713 Within monthly budget"));
|
|
1870
|
+
} else {
|
|
1871
|
+
console.log(theme.dim(' Tip: set "monthlyBudget" in config (e.g., 50 for $50/month)'));
|
|
1872
|
+
}
|
|
1873
|
+
console.log();
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1854
1876
|
const session = ctx.sessions.current;
|
|
1855
1877
|
const usage = session?.tokenUsage ?? {
|
|
1856
1878
|
inputTokens: 0,
|
|
@@ -2166,7 +2188,7 @@ ${hint}` : "")
|
|
|
2166
2188
|
usage: "/test [command|filter]",
|
|
2167
2189
|
async execute(args, ctx) {
|
|
2168
2190
|
try {
|
|
2169
|
-
const { executeTests } = await import("./run-tests-
|
|
2191
|
+
const { executeTests } = await import("./run-tests-KRGPFYPF.js");
|
|
2170
2192
|
const argStr = args.join(" ").trim();
|
|
2171
2193
|
let testArgs = {};
|
|
2172
2194
|
if (argStr) {
|
|
@@ -3230,6 +3252,125 @@ var CustomCommandManager = class {
|
|
|
3230
3252
|
}
|
|
3231
3253
|
};
|
|
3232
3254
|
|
|
3255
|
+
// src/core/cost-tracker.ts
|
|
3256
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
3257
|
+
import { join as join4 } from "path";
|
|
3258
|
+
var CostTracker = class {
|
|
3259
|
+
filePath;
|
|
3260
|
+
records = [];
|
|
3261
|
+
dirty = false;
|
|
3262
|
+
constructor(configDir) {
|
|
3263
|
+
this.filePath = join4(configDir, "cost-history.json");
|
|
3264
|
+
this.load();
|
|
3265
|
+
}
|
|
3266
|
+
load() {
|
|
3267
|
+
try {
|
|
3268
|
+
if (existsSync4(this.filePath)) {
|
|
3269
|
+
const data = JSON.parse(readFileSync3(this.filePath, "utf-8"));
|
|
3270
|
+
if (data.version === 1 && Array.isArray(data.records)) {
|
|
3271
|
+
this.records = data.records;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
} catch {
|
|
3275
|
+
this.records = [];
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
/** Save to disk (atomic write). */
|
|
3279
|
+
save() {
|
|
3280
|
+
if (!this.dirty) return;
|
|
3281
|
+
const data = { version: 1, records: this.records };
|
|
3282
|
+
const tmp = this.filePath + ".tmp";
|
|
3283
|
+
writeFileSync2(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
3284
|
+
renameSync(tmp, this.filePath);
|
|
3285
|
+
this.dirty = false;
|
|
3286
|
+
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Record cost from a completed session/interaction.
|
|
3289
|
+
*/
|
|
3290
|
+
addCost(provider, model, usage) {
|
|
3291
|
+
const cost = computeCost(provider, model, usage);
|
|
3292
|
+
if (cost === null || cost === 0) return;
|
|
3293
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3294
|
+
let record = this.records.find((r) => r.date === today);
|
|
3295
|
+
if (!record) {
|
|
3296
|
+
record = { date: today, cost: 0, sessions: 0, inputTokens: 0, outputTokens: 0 };
|
|
3297
|
+
this.records.push(record);
|
|
3298
|
+
}
|
|
3299
|
+
record.cost += cost;
|
|
3300
|
+
record.sessions += 1;
|
|
3301
|
+
record.inputTokens += usage.inputTokens;
|
|
3302
|
+
record.outputTokens += usage.outputTokens;
|
|
3303
|
+
this.dirty = true;
|
|
3304
|
+
if (this.records.length > 90) {
|
|
3305
|
+
this.records = this.records.slice(-90);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
/** Get total cost for a given month ("2026-04"). */
|
|
3309
|
+
getMonthlyCost(yearMonth) {
|
|
3310
|
+
const prefix = yearMonth ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
3311
|
+
return this.records.filter((r) => r.date.startsWith(prefix)).reduce((sum, r) => sum + r.cost, 0);
|
|
3312
|
+
}
|
|
3313
|
+
/** Get today's cost. */
|
|
3314
|
+
getTodayCost() {
|
|
3315
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3316
|
+
return this.records.find((r) => r.date === today)?.cost ?? 0;
|
|
3317
|
+
}
|
|
3318
|
+
/** Get total cost for last N days. */
|
|
3319
|
+
getRecentCost(days) {
|
|
3320
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
3321
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
3322
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
3323
|
+
return this.records.filter((r) => r.date >= cutoffStr).reduce((sum, r) => sum + r.cost, 0);
|
|
3324
|
+
}
|
|
3325
|
+
/** Get all records for display. */
|
|
3326
|
+
getRecords() {
|
|
3327
|
+
return [...this.records];
|
|
3328
|
+
}
|
|
3329
|
+
/**
|
|
3330
|
+
* Check if monthly cost exceeds budget, return warning message or null.
|
|
3331
|
+
*/
|
|
3332
|
+
checkBudget(monthlyBudget) {
|
|
3333
|
+
if (!monthlyBudget || monthlyBudget <= 0) return null;
|
|
3334
|
+
const monthlyCost = this.getMonthlyCost();
|
|
3335
|
+
const ratio = monthlyCost / monthlyBudget;
|
|
3336
|
+
if (ratio >= 1) {
|
|
3337
|
+
return `\u{1F6A8} Monthly budget exceeded: ${formatCost(monthlyCost)} / ${formatCost(monthlyBudget)} (${Math.round(ratio * 100)}%)`;
|
|
3338
|
+
}
|
|
3339
|
+
if (ratio >= 0.8) {
|
|
3340
|
+
return `\u26A0 Monthly budget warning: ${formatCost(monthlyCost)} / ${formatCost(monthlyBudget)} (${Math.round(ratio * 100)}%)`;
|
|
3341
|
+
}
|
|
3342
|
+
return null;
|
|
3343
|
+
}
|
|
3344
|
+
/**
|
|
3345
|
+
* Format a cost summary for display.
|
|
3346
|
+
*/
|
|
3347
|
+
formatSummary(monthlyBudget) {
|
|
3348
|
+
const today = this.getTodayCost();
|
|
3349
|
+
const monthly = this.getMonthlyCost();
|
|
3350
|
+
const yearMonth = (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
3351
|
+
const lines = [
|
|
3352
|
+
`Today: ${formatCost(today)}`,
|
|
3353
|
+
`This month: ${formatCost(monthly)} (${yearMonth})`
|
|
3354
|
+
];
|
|
3355
|
+
if (monthlyBudget && monthlyBudget > 0) {
|
|
3356
|
+
const ratio = monthly / monthlyBudget;
|
|
3357
|
+
const bar = "\u2588".repeat(Math.min(20, Math.round(ratio * 20))) + "\u2591".repeat(Math.max(0, 20 - Math.round(ratio * 20)));
|
|
3358
|
+
lines.push(`Budget: ${formatCost(monthlyBudget)} [${bar}] ${Math.round(ratio * 100)}%`);
|
|
3359
|
+
}
|
|
3360
|
+
const last7 = [];
|
|
3361
|
+
for (let i = 6; i >= 0; i--) {
|
|
3362
|
+
const d = /* @__PURE__ */ new Date();
|
|
3363
|
+
d.setDate(d.getDate() - i);
|
|
3364
|
+
const dateStr = d.toISOString().slice(0, 10);
|
|
3365
|
+
const record = this.records.find((r) => r.date === dateStr);
|
|
3366
|
+
const dayLabel = dateStr.slice(5);
|
|
3367
|
+
last7.push(`${dayLabel}: ${record ? formatCost(record.cost) : "$0.00"}`);
|
|
3368
|
+
}
|
|
3369
|
+
lines.push(`Last 7 days: ${last7.join(" ")}`);
|
|
3370
|
+
return lines.join("\n");
|
|
3371
|
+
}
|
|
3372
|
+
};
|
|
3373
|
+
|
|
3233
3374
|
// src/repl/notify.ts
|
|
3234
3375
|
import { spawn } from "child_process";
|
|
3235
3376
|
import { platform as platform2 } from "os";
|
|
@@ -3303,7 +3444,7 @@ function parseAtReferences(input2, cwd) {
|
|
|
3303
3444
|
const absPath = resolve2(cwd, rawPath);
|
|
3304
3445
|
const ext = extname2(rawPath).toLowerCase();
|
|
3305
3446
|
const mime = IMAGE_MIME[ext];
|
|
3306
|
-
if (!
|
|
3447
|
+
if (!existsSync5(absPath)) {
|
|
3307
3448
|
refs.push({ path: rawPath, type: "notfound" });
|
|
3308
3449
|
continue;
|
|
3309
3450
|
}
|
|
@@ -3313,7 +3454,7 @@ function parseAtReferences(input2, cwd) {
|
|
|
3313
3454
|
refs.push({ path: rawPath, type: "toolarge" });
|
|
3314
3455
|
continue;
|
|
3315
3456
|
}
|
|
3316
|
-
const data =
|
|
3457
|
+
const data = readFileSync4(absPath).toString("base64");
|
|
3317
3458
|
imageParts.push({
|
|
3318
3459
|
type: "image_url",
|
|
3319
3460
|
image_url: { url: `data:${mime};base64,${data}` }
|
|
@@ -3321,7 +3462,7 @@ function parseAtReferences(input2, cwd) {
|
|
|
3321
3462
|
refs.push({ path: rawPath, type: "image" });
|
|
3322
3463
|
textBody = textBody.replace(match[0], "").trim();
|
|
3323
3464
|
} else {
|
|
3324
|
-
const content =
|
|
3465
|
+
const content = readFileSync4(absPath, "utf-8");
|
|
3325
3466
|
const inlined = `
|
|
3326
3467
|
|
|
3327
3468
|
[File: ${rawPath}]
|
|
@@ -3390,6 +3531,7 @@ var Repl = class {
|
|
|
3390
3531
|
if (options?.blockedTools) this.blockedTools = options.blockedTools;
|
|
3391
3532
|
if (options?.resumeSessionId) this.resumeSessionId = options.resumeSessionId;
|
|
3392
3533
|
if (options?.maxToolRoundsOverride !== void 0) this.maxToolRoundsOverride = options.maxToolRoundsOverride;
|
|
3534
|
+
this.costTracker = new CostTracker(this.config.getConfigDir());
|
|
3393
3535
|
}
|
|
3394
3536
|
rl;
|
|
3395
3537
|
currentProvider;
|
|
@@ -3405,12 +3547,13 @@ var Repl = class {
|
|
|
3405
3547
|
contextLayers = [];
|
|
3406
3548
|
/** 本次会话累计 token 用量 */
|
|
3407
3549
|
sessionTokenUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
|
|
3408
|
-
/** Fold a single-request TokenUsage (with optional cache fields) into sessionTokenUsage. */
|
|
3550
|
+
/** Fold a single-request TokenUsage (with optional cache fields) into sessionTokenUsage + cost tracker. */
|
|
3409
3551
|
addSessionUsage(u) {
|
|
3410
3552
|
this.sessionTokenUsage.inputTokens += u.inputTokens;
|
|
3411
3553
|
this.sessionTokenUsage.outputTokens += u.outputTokens;
|
|
3412
3554
|
this.sessionTokenUsage.cacheCreationTokens += u.cacheCreationTokens ?? 0;
|
|
3413
3555
|
this.sessionTokenUsage.cacheReadTokens += u.cacheReadTokens ?? 0;
|
|
3556
|
+
this.costTracker.addCost(this.currentProvider, this.currentModel, u);
|
|
3414
3557
|
}
|
|
3415
3558
|
/** 启动时检测到的 Git 分支(无 git 仓库时为 null) */
|
|
3416
3559
|
gitBranch = null;
|
|
@@ -3452,6 +3595,8 @@ var Repl = class {
|
|
|
3452
3595
|
selecting = false;
|
|
3453
3596
|
/** CLI --max-tool-rounds 覆盖值;未指定时从 config.maxToolRounds 读取 */
|
|
3454
3597
|
maxToolRoundsOverride;
|
|
3598
|
+
/** 跨 session 成本追踪器 */
|
|
3599
|
+
costTracker;
|
|
3455
3600
|
// ── /add-dir 目录上下文支持 ────────────────────────────────────────────────
|
|
3456
3601
|
/**
|
|
3457
3602
|
* 扫描目录内容,返回格式化字符串(含目录树 + 关键文件内容)。
|
|
@@ -3531,7 +3676,7 @@ var Repl = class {
|
|
|
3531
3676
|
const filtered = entries.filter((e) => !SKIP_DIRS_SET.has(e));
|
|
3532
3677
|
for (let i = 0; i < filtered.length && entryCount < MAX_TREE_ENTRIES; i++) {
|
|
3533
3678
|
const name = filtered[i];
|
|
3534
|
-
const fullPath =
|
|
3679
|
+
const fullPath = join5(dir, name);
|
|
3535
3680
|
const isLast = i === filtered.length - 1;
|
|
3536
3681
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
3537
3682
|
let isDir;
|
|
@@ -3565,7 +3710,7 @@ ${treeLines.join("\n")}`
|
|
|
3565
3710
|
for (const name of entries) {
|
|
3566
3711
|
if (totalChars >= MAX_TOTAL_CHARS) break;
|
|
3567
3712
|
if (SKIP_DIRS_SET.has(name)) continue;
|
|
3568
|
-
const fullPath =
|
|
3713
|
+
const fullPath = join5(dir, name);
|
|
3569
3714
|
let st;
|
|
3570
3715
|
try {
|
|
3571
3716
|
st = statSync3(fullPath);
|
|
@@ -3580,7 +3725,7 @@ ${treeLines.join("\n")}`
|
|
|
3580
3725
|
if (!TEXT_EXTS.has(ext) && !isSpecial) continue;
|
|
3581
3726
|
if (st.size > MAX_FILE_CHARS * 3) continue;
|
|
3582
3727
|
try {
|
|
3583
|
-
let content =
|
|
3728
|
+
let content = readFileSync4(fullPath, "utf-8");
|
|
3584
3729
|
if (content.length > MAX_FILE_CHARS) {
|
|
3585
3730
|
content = content.slice(0, MAX_FILE_CHARS) + `
|
|
3586
3731
|
... (truncated, ${content.length} chars total)`;
|
|
@@ -3610,7 +3755,7 @@ ${content}
|
|
|
3610
3755
|
*/
|
|
3611
3756
|
addExtraContextDir(dirPath) {
|
|
3612
3757
|
const absPath = resolve2(dirPath);
|
|
3613
|
-
if (!
|
|
3758
|
+
if (!existsSync5(absPath)) {
|
|
3614
3759
|
return { success: false, charCount: 0, added: false, error: `Directory not found: ${dirPath}` };
|
|
3615
3760
|
}
|
|
3616
3761
|
let isDir;
|
|
@@ -3644,9 +3789,9 @@ ${content}
|
|
|
3644
3789
|
*/
|
|
3645
3790
|
findContextFile(dir, candidates = CONTEXT_FILE_CANDIDATES) {
|
|
3646
3791
|
for (const candidate of candidates) {
|
|
3647
|
-
const fullPath =
|
|
3648
|
-
if (
|
|
3649
|
-
const content =
|
|
3792
|
+
const fullPath = join5(dir, candidate);
|
|
3793
|
+
if (existsSync5(fullPath)) {
|
|
3794
|
+
const content = readFileSync4(fullPath, "utf-8").trim();
|
|
3650
3795
|
if (content) return { filePath: fullPath, content };
|
|
3651
3796
|
}
|
|
3652
3797
|
}
|
|
@@ -3674,10 +3819,10 @@ ${content}
|
|
|
3674
3819
|
const cwd = process.cwd();
|
|
3675
3820
|
const gitRoot = getGitRoot(cwd);
|
|
3676
3821
|
const projectRoot = gitRoot ?? cwd;
|
|
3677
|
-
const mcpPath =
|
|
3678
|
-
if (!
|
|
3822
|
+
const mcpPath = join5(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
3823
|
+
if (!existsSync5(mcpPath)) return null;
|
|
3679
3824
|
try {
|
|
3680
|
-
const raw = JSON.parse(
|
|
3825
|
+
const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
|
|
3681
3826
|
const servers = raw?.mcpServers;
|
|
3682
3827
|
if (!servers || typeof servers !== "object") {
|
|
3683
3828
|
process.stderr.write(
|
|
@@ -3723,8 +3868,8 @@ ${content}
|
|
|
3723
3868
|
);
|
|
3724
3869
|
return { layers: [], mergedContent: "" };
|
|
3725
3870
|
}
|
|
3726
|
-
if (
|
|
3727
|
-
const content =
|
|
3871
|
+
if (existsSync5(fullPath)) {
|
|
3872
|
+
const content = readFileSync4(fullPath, "utf-8").trim();
|
|
3728
3873
|
if (content) {
|
|
3729
3874
|
const layer = {
|
|
3730
3875
|
level: "project",
|
|
@@ -3781,9 +3926,9 @@ ${content}
|
|
|
3781
3926
|
* 超过 MEMORY_MAX_CHARS 时只取末尾最新部分。
|
|
3782
3927
|
*/
|
|
3783
3928
|
loadMemoryContent() {
|
|
3784
|
-
const memoryPath =
|
|
3785
|
-
if (!
|
|
3786
|
-
let content =
|
|
3929
|
+
const memoryPath = join5(this.config.getConfigDir(), MEMORY_FILE_NAME);
|
|
3930
|
+
if (!existsSync5(memoryPath)) return null;
|
|
3931
|
+
let content = readFileSync4(memoryPath, "utf-8").trim();
|
|
3787
3932
|
if (!content) return null;
|
|
3788
3933
|
if (content.length > MEMORY_MAX_CHARS) {
|
|
3789
3934
|
content = content.slice(-MEMORY_MAX_CHARS);
|
|
@@ -4094,6 +4239,16 @@ Session '${this.resumeSessionId}' not found.
|
|
|
4094
4239
|
process.stdout.write(
|
|
4095
4240
|
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
4241
|
);
|
|
4242
|
+
const msgs = session.messages;
|
|
4243
|
+
if (msgs.length > 0) {
|
|
4244
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
4245
|
+
const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
|
|
4246
|
+
if (isIncomplete) {
|
|
4247
|
+
process.stdout.write(
|
|
4248
|
+
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')
|
|
4249
|
+
);
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4097
4252
|
}
|
|
4098
4253
|
if (layers.length > 0) {
|
|
4099
4254
|
if (layers.length === 1) {
|
|
@@ -4134,14 +4289,14 @@ Session '${this.resumeSessionId}' not found.
|
|
|
4134
4289
|
process.stdout.write(theme.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
|
|
4135
4290
|
`));
|
|
4136
4291
|
}
|
|
4137
|
-
const skillsDir =
|
|
4292
|
+
const skillsDir = join5(this.config.getConfigDir(), SKILLS_DIR_NAME);
|
|
4138
4293
|
this.skillManager = new SkillManager(skillsDir);
|
|
4139
4294
|
const skillCount = this.skillManager.loadSkills();
|
|
4140
4295
|
if (skillCount > 0) {
|
|
4141
4296
|
process.stdout.write(theme.dim(` \u{1F3AF} Skills: ${skillCount} available (use /skill to manage)
|
|
4142
4297
|
`));
|
|
4143
4298
|
}
|
|
4144
|
-
const commandsDir =
|
|
4299
|
+
const commandsDir = join5(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
|
|
4145
4300
|
this.customCommandManager = new CustomCommandManager(commandsDir);
|
|
4146
4301
|
const customCmdCount = this.customCommandManager.loadCommands();
|
|
4147
4302
|
if (customCmdCount > 0) {
|
|
@@ -4329,6 +4484,12 @@ Session '${this.resumeSessionId}' not found.
|
|
|
4329
4484
|
}
|
|
4330
4485
|
await this.sessions.save();
|
|
4331
4486
|
}
|
|
4487
|
+
this.costTracker.save();
|
|
4488
|
+
const budgetWarning = this.costTracker.checkBudget(this.config.get("monthlyBudget"));
|
|
4489
|
+
if (budgetWarning) {
|
|
4490
|
+
process.stdout.write(theme.warning(` ${budgetWarning}
|
|
4491
|
+
`));
|
|
4492
|
+
}
|
|
4332
4493
|
const elapsed = Date.now() - t0;
|
|
4333
4494
|
const threshold = this.config.get("ui").notificationThreshold;
|
|
4334
4495
|
if (threshold > 0 && elapsed >= threshold) {
|
|
@@ -4592,14 +4753,14 @@ Session '${this.resumeSessionId}' not found.
|
|
|
4592
4753
|
const dir = normalized.includes("/") ? dirname3(normalized) : ".";
|
|
4593
4754
|
const prefix = normalized.includes("/") ? basename2(normalized) : normalized;
|
|
4594
4755
|
const absDir = resolve2(process.cwd(), dir);
|
|
4595
|
-
if (!
|
|
4756
|
+
if (!existsSync5(absDir)) return [];
|
|
4596
4757
|
const entries = readdirSync3(absDir);
|
|
4597
4758
|
const results = [];
|
|
4598
4759
|
for (const entry of entries) {
|
|
4599
4760
|
if (entry.startsWith(".")) continue;
|
|
4600
4761
|
if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
|
|
4601
4762
|
try {
|
|
4602
|
-
const fullPath =
|
|
4763
|
+
const fullPath = join5(absDir, entry);
|
|
4603
4764
|
const stat = statSync3(fullPath);
|
|
4604
4765
|
const rel = dir === "." ? entry : `${dir}/${entry}`;
|
|
4605
4766
|
results.push(stat.isDirectory() ? `${rel}/` : rel);
|
|
@@ -5634,6 +5795,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
|
|
|
5634
5795
|
listContextDirs: () => [...this.extraContextDirs],
|
|
5635
5796
|
forkSession: (messageCount, title) => this.sessions.forkSession(messageCount, title),
|
|
5636
5797
|
getToolExecutor: () => this.toolExecutor,
|
|
5798
|
+
getCostTracker: () => this.costTracker,
|
|
5637
5799
|
exit: () => this.handleExit()
|
|
5638
5800
|
};
|
|
5639
5801
|
await cmd.execute(args, ctx);
|
|
@@ -5738,7 +5900,7 @@ program.command("web").description("Start Web UI server with browser-based chat
|
|
|
5738
5900
|
console.error("Error: Invalid port number. Must be between 1 and 65535.");
|
|
5739
5901
|
process.exit(1);
|
|
5740
5902
|
}
|
|
5741
|
-
const { startWebServer } = await import("./server-
|
|
5903
|
+
const { startWebServer } = await import("./server-BGMUORHT.js");
|
|
5742
5904
|
await startWebServer({ port, host: options.host });
|
|
5743
5905
|
});
|
|
5744
5906
|
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 +6133,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
|
|
|
5971
6133
|
}),
|
|
5972
6134
|
config.get("customProviders")
|
|
5973
6135
|
);
|
|
5974
|
-
const { startHub } = await import("./hub-
|
|
6136
|
+
const { startHub } = await import("./hub-R6ID4F6J.js");
|
|
5975
6137
|
await startHub(
|
|
5976
6138
|
{
|
|
5977
6139
|
topic: topic ?? "",
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
persistToolRound,
|
|
22
22
|
rebuildExtraMessages,
|
|
23
23
|
setupProxy
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-EYRPTNVI.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-E45EGVSY.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-MLEM56CR.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-KRGPFYPF.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-E45EGVSY.js";
|
|
8
8
|
import "./chunk-4BKXL7SM.js";
|
|
9
9
|
import {
|
|
10
10
|
SUBAGENT_ALLOWED_TOOLS
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-MLEM56CR.js";
|
|
12
12
|
|
|
13
13
|
// src/hub/task-orchestrator.ts
|
|
14
14
|
import { createInterface } from "readline";
|