jinzd-ai-cli 0.4.285 → 0.4.286
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/{batch-R563COWE.js → batch-GY3I53DE.js} +13 -10
- package/dist/{chat-index-TDU3S2MG.js → chat-index-DGUOUUQW.js} +2 -2
- package/dist/{chunk-UW33KJW5.js → chunk-2YDLBM3Z.js} +1 -1
- package/dist/{chunk-3LFB4YZA.js → chunk-3ZQZO5F7.js} +3 -3
- package/dist/chunk-53V3RQY5.js +5684 -0
- package/dist/{chunk-XGRZA5V6.js → chunk-6SCELZG6.js} +3 -3
- package/dist/{chunk-DV5D7RXZ.js → chunk-7G7RZHJI.js} +1 -1
- package/dist/{chunk-KLBMKONV.js → chunk-BRLLV3FX.js} +4 -4
- package/dist/chunk-CGLYJRFA.js +95 -0
- package/dist/{chunk-WBF4AOHJ.js → chunk-D7HYLMCC.js} +18 -110
- package/dist/{chunk-5CC5HQFW.js → chunk-EYUCJBGS.js} +30 -30
- package/dist/{chunk-LF6SPK7I.js → chunk-GIJL4EFA.js} +10 -2
- package/dist/{chunk-VLM4SKKX.js → chunk-L4FDTQ42.js} +1 -1
- package/dist/{chunk-DPTEC6AQ.js → chunk-LWPWBRAC.js} +12 -10
- package/dist/chunk-RS7SKYTS.js +45 -0
- package/dist/{chunk-6G7Y2VG4.js → chunk-S7ABWTDV.js} +1 -1
- package/dist/{chunk-PHK7QQH3.js → chunk-THUXZNJL.js} +1 -1
- package/dist/{chunk-AOF4K5N7.js → chunk-UKITSNSH.js} +1 -1
- package/dist/{ci-E4CYVEOW.js → ci-JBUIVM7P.js} +4 -4
- package/dist/{ci-format-7XTLOAB5.js → ci-format-B4AHV5SI.js} +2 -2
- package/dist/{constants-L7D3IXWV.js → constants-5FYUQLKN.js} +1 -1
- package/dist/constants-P75MLFV7.js +80 -0
- package/dist/{doctor-cli-JLROIGLO.js → doctor-cli-VLFE46PX.js} +5 -4
- package/dist/electron-server.js +3549 -253
- package/dist/git-context-VNCA2GU7.js +10 -0
- package/dist/{hub-7UDPLWBI.js → hub-CNORVRYW.js} +1 -1
- package/dist/index.js +315 -3655
- package/dist/{persistent-memory-CNJOZ7U2.js → persistent-memory-3KL4N27E.js} +3 -2
- package/dist/{persistent-memory-DRXWYBJS.js → persistent-memory-LVX2OT4L.js} +2 -2
- package/dist/{pr-3W5547IS.js → pr-2YUV4KII.js} +4 -4
- package/dist/{run-tests-LP2CI4MT.js → run-tests-ERD6X534.js} +2 -2
- package/dist/{run-tests-7B7N4YAE.js → run-tests-KZ2IUZHK.js} +2 -2
- package/dist/{semantic-FF6DDJI6.js → semantic-VPNKXKKK.js} +2 -2
- package/dist/{server-NIPB62XP.js → server-7VSLW446.js} +6 -5
- package/dist/{server-YBMWBJVG.js → server-A3OX52UC.js} +21 -15
- package/dist/{task-orchestrator-RYG4ZTDO.js → task-orchestrator-XAMKOFUE.js} +6 -5
- package/dist/{usage-YDVLCZDU.js → usage-ZXNYCAZB.js} +4 -3
- package/package.json +1 -1
- package/dist/chunk-HBAYEVVJ.js +0 -2362
- package/dist/{chunk-AEAYUKSY.js → chunk-7XU4BATV.js} +3 -3
- package/dist/{chunk-YOVHMQEJ.js → chunk-I4GQRMFV.js} +3 -3
package/dist/chunk-HBAYEVVJ.js
DELETED
|
@@ -1,2362 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
computeCost
|
|
4
|
-
} from "./chunk-DPTEC6AQ.js";
|
|
5
|
-
import {
|
|
6
|
-
estimateTokens,
|
|
7
|
-
truncateForPersist
|
|
8
|
-
} from "./chunk-KLBMKONV.js";
|
|
9
|
-
import {
|
|
10
|
-
isMemoryExpired,
|
|
11
|
-
isMemoryStale
|
|
12
|
-
} from "./chunk-AOF4K5N7.js";
|
|
13
|
-
import {
|
|
14
|
-
getContentText
|
|
15
|
-
} from "./chunk-IUVDZVZ4.js";
|
|
16
|
-
import {
|
|
17
|
-
isCleanDocumentBody,
|
|
18
|
-
stripOuterCodeFence
|
|
19
|
-
} from "./chunk-TEXT4DAT.js";
|
|
20
|
-
import {
|
|
21
|
-
atomicWriteFileSync
|
|
22
|
-
} from "./chunk-IW3Q7AE5.js";
|
|
23
|
-
import {
|
|
24
|
-
APP_NAME,
|
|
25
|
-
CONFIG_DIR_NAME,
|
|
26
|
-
CONTEXT_PRESSURE_THRESHOLD,
|
|
27
|
-
DEV_STATE_FILE_NAME,
|
|
28
|
-
MCP_CALL_TIMEOUT,
|
|
29
|
-
MCP_CONNECT_TIMEOUT,
|
|
30
|
-
MCP_PROTOCOL_VERSION,
|
|
31
|
-
MCP_TOOL_PREFIX,
|
|
32
|
-
VERSION
|
|
33
|
-
} from "./chunk-PHK7QQH3.js";
|
|
34
|
-
|
|
35
|
-
// src/mcp/client.ts
|
|
36
|
-
import { spawn } from "child_process";
|
|
37
|
-
var McpClient = class {
|
|
38
|
-
serverId;
|
|
39
|
-
config;
|
|
40
|
-
process = null;
|
|
41
|
-
nextId = 1;
|
|
42
|
-
// M8: wraps at MAX_SAFE_INTEGER via getNextId()
|
|
43
|
-
connected = false;
|
|
44
|
-
serverInfo = null;
|
|
45
|
-
/** stderr 收集(最多保留最后 2KB,用于错误报告) */
|
|
46
|
-
stderrBuffer = "";
|
|
47
|
-
/** 缓存已发现的工具列表 */
|
|
48
|
-
cachedTools = [];
|
|
49
|
-
/** 错误信息(连接失败时设置) */
|
|
50
|
-
errorMessage = null;
|
|
51
|
-
// ── JSON-RPC 请求/响应匹配 ──────────────────────────────────────
|
|
52
|
-
pendingRequests = /* @__PURE__ */ new Map();
|
|
53
|
-
/** stdout 残余缓冲区(处理不完整的 JSON 行) */
|
|
54
|
-
stdoutBuffer = "";
|
|
55
|
-
constructor(serverId, config) {
|
|
56
|
-
this.serverId = serverId;
|
|
57
|
-
this.config = config;
|
|
58
|
-
}
|
|
59
|
-
get isConnected() {
|
|
60
|
-
return this.connected;
|
|
61
|
-
}
|
|
62
|
-
get serverName() {
|
|
63
|
-
return this.serverInfo?.name ?? this.serverId;
|
|
64
|
-
}
|
|
65
|
-
get tools() {
|
|
66
|
-
return this.cachedTools;
|
|
67
|
-
}
|
|
68
|
-
// ══════════════════════════════════════════════════════════════════
|
|
69
|
-
// 连接与初始化
|
|
70
|
-
// ══════════════════════════════════════════════════════════════════
|
|
71
|
-
async connect() {
|
|
72
|
-
const timeout = this.config.timeout ?? MCP_CONNECT_TIMEOUT;
|
|
73
|
-
try {
|
|
74
|
-
this.process = spawn(this.config.command, this.config.args ?? [], {
|
|
75
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
76
|
-
env: { ...process.env, ...this.config.env },
|
|
77
|
-
// Windows 上 npx 等是 .cmd 脚本,需要 shell 模式
|
|
78
|
-
shell: process.platform === "win32",
|
|
79
|
-
// 不让子进程阻止父进程退出
|
|
80
|
-
detached: false
|
|
81
|
-
});
|
|
82
|
-
this.process.on("error", (err) => {
|
|
83
|
-
this.errorMessage = err.message;
|
|
84
|
-
this.connected = false;
|
|
85
|
-
this.rejectAllPending(new Error(`MCP server [${this.serverId}] process error: ${err.message}`));
|
|
86
|
-
});
|
|
87
|
-
this.process.on("exit", (code, signal) => {
|
|
88
|
-
this.connected = false;
|
|
89
|
-
const reason = signal ? `signal ${signal}` : `code ${code}`;
|
|
90
|
-
this.rejectAllPending(new Error(`MCP server [${this.serverId}] exited: ${reason}`));
|
|
91
|
-
});
|
|
92
|
-
this.process.stdout.setEncoding("utf-8");
|
|
93
|
-
this.process.stdout.on("data", (chunk) => this.handleStdoutData(chunk));
|
|
94
|
-
this.process.stderr.setEncoding("utf-8");
|
|
95
|
-
this.process.stderr.on("data", (chunk) => {
|
|
96
|
-
this.stderrBuffer += chunk;
|
|
97
|
-
if (this.stderrBuffer.length > 2048) {
|
|
98
|
-
this.stderrBuffer = this.stderrBuffer.slice(-2048);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
const initResult = await this.withTimeout(
|
|
102
|
-
this.sendRequest("initialize", {
|
|
103
|
-
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
104
|
-
capabilities: {},
|
|
105
|
-
clientInfo: { name: APP_NAME, version: VERSION }
|
|
106
|
-
}),
|
|
107
|
-
timeout,
|
|
108
|
-
"initialize handshake"
|
|
109
|
-
);
|
|
110
|
-
this.serverInfo = initResult.serverInfo;
|
|
111
|
-
this.sendNotification("notifications/initialized");
|
|
112
|
-
this.connected = true;
|
|
113
|
-
await this.refreshTools();
|
|
114
|
-
} catch (err) {
|
|
115
|
-
this.errorMessage = err instanceof Error ? err.message : String(err);
|
|
116
|
-
this.connected = false;
|
|
117
|
-
this.killProcess();
|
|
118
|
-
throw err;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
// ══════════════════════════════════════════════════════════════════
|
|
122
|
-
// 工具操作
|
|
123
|
-
// ══════════════════════════════════════════════════════════════════
|
|
124
|
-
/** 刷新工具列表(tools/list) */
|
|
125
|
-
async refreshTools() {
|
|
126
|
-
this.ensureConnected();
|
|
127
|
-
const result = await this.withTimeout(
|
|
128
|
-
this.sendRequest("tools/list", {}),
|
|
129
|
-
MCP_CALL_TIMEOUT,
|
|
130
|
-
"tools/list"
|
|
131
|
-
);
|
|
132
|
-
this.cachedTools = result.tools ?? [];
|
|
133
|
-
return this.cachedTools;
|
|
134
|
-
}
|
|
135
|
-
/** 调用工具(tools/call) */
|
|
136
|
-
async callTool(name, args) {
|
|
137
|
-
this.ensureConnected();
|
|
138
|
-
return this.withTimeout(
|
|
139
|
-
this.sendRequest("tools/call", { name, arguments: args }),
|
|
140
|
-
MCP_CALL_TIMEOUT,
|
|
141
|
-
`tools/call(${name})`
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
// ══════════════════════════════════════════════════════════════════
|
|
145
|
-
// 关闭连接
|
|
146
|
-
// ══════════════════════════════════════════════════════════════════
|
|
147
|
-
async close() {
|
|
148
|
-
this.connected = false;
|
|
149
|
-
this.rejectAllPending(new Error("Client closing"));
|
|
150
|
-
this.killProcess();
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* 断线重连:清理旧状态后重新执行完整的 connect() 流程。
|
|
154
|
-
* 用于 MCP 服务器子进程意外退出后的自动或手动恢复。
|
|
155
|
-
*/
|
|
156
|
-
async reconnect() {
|
|
157
|
-
this.connected = false;
|
|
158
|
-
this.rejectAllPending(new Error("Reconnecting"));
|
|
159
|
-
this.killProcess();
|
|
160
|
-
this.errorMessage = null;
|
|
161
|
-
this.stderrBuffer = "";
|
|
162
|
-
this.stdoutBuffer = "";
|
|
163
|
-
this.cachedTools = [];
|
|
164
|
-
await this.connect();
|
|
165
|
-
}
|
|
166
|
-
// ══════════════════════════════════════════════════════════════════
|
|
167
|
-
// 内部方法:JSON-RPC 通信
|
|
168
|
-
// ══════════════════════════════════════════════════════════════════
|
|
169
|
-
sendRequest(method, params) {
|
|
170
|
-
return new Promise((resolve, reject) => {
|
|
171
|
-
if (!this.process?.stdin?.writable) {
|
|
172
|
-
return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
|
|
173
|
-
}
|
|
174
|
-
const id = this.nextId++;
|
|
175
|
-
if (this.nextId > Number.MAX_SAFE_INTEGER - 1) this.nextId = 1;
|
|
176
|
-
const request = {
|
|
177
|
-
jsonrpc: "2.0",
|
|
178
|
-
id,
|
|
179
|
-
method,
|
|
180
|
-
...params !== void 0 ? { params } : {}
|
|
181
|
-
};
|
|
182
|
-
const cleanup = () => {
|
|
183
|
-
this.pendingRequests.delete(id);
|
|
184
|
-
clearTimeout(timer);
|
|
185
|
-
};
|
|
186
|
-
const timer = setTimeout(() => {
|
|
187
|
-
cleanup();
|
|
188
|
-
reject(new Error(`MCP request [${method}] timed out (internal)`));
|
|
189
|
-
}, MCP_CALL_TIMEOUT * 2);
|
|
190
|
-
this.pendingRequests.set(id, {
|
|
191
|
-
resolve: (result) => {
|
|
192
|
-
cleanup();
|
|
193
|
-
resolve(result);
|
|
194
|
-
},
|
|
195
|
-
reject: (error) => {
|
|
196
|
-
cleanup();
|
|
197
|
-
reject(error);
|
|
198
|
-
},
|
|
199
|
-
timer
|
|
200
|
-
});
|
|
201
|
-
const json = JSON.stringify(request) + "\n";
|
|
202
|
-
this.process.stdin.write(json, (err) => {
|
|
203
|
-
if (err) {
|
|
204
|
-
cleanup();
|
|
205
|
-
reject(new Error(`MCP write error: ${err.message}`));
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
sendNotification(method, params) {
|
|
211
|
-
if (!this.process?.stdin?.writable) return;
|
|
212
|
-
const notification = {
|
|
213
|
-
jsonrpc: "2.0",
|
|
214
|
-
method,
|
|
215
|
-
...params !== void 0 ? { params } : {}
|
|
216
|
-
};
|
|
217
|
-
this.process.stdin.write(JSON.stringify(notification) + "\n");
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* 处理 stdout 数据:按行分割,每行解析为 JSON-RPC 响应。
|
|
221
|
-
* 处理不完整行:残余数据保留在 stdoutBuffer 中。
|
|
222
|
-
*/
|
|
223
|
-
handleStdoutData(chunk) {
|
|
224
|
-
this.stdoutBuffer += chunk;
|
|
225
|
-
if (this.stdoutBuffer.length > 1048576) {
|
|
226
|
-
process.stderr.write(`[mcp] stdout buffer exceeded 1MB \u2014 clearing
|
|
227
|
-
`);
|
|
228
|
-
this.stdoutBuffer = "";
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const lines = this.stdoutBuffer.split("\n");
|
|
232
|
-
this.stdoutBuffer = lines.pop() ?? "";
|
|
233
|
-
for (const line of lines) {
|
|
234
|
-
const trimmed = line.trim();
|
|
235
|
-
if (!trimmed) continue;
|
|
236
|
-
try {
|
|
237
|
-
const msg = JSON.parse(trimmed);
|
|
238
|
-
this.handleMessage(msg);
|
|
239
|
-
} catch {
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
/** 处理收到的 JSON-RPC 消息 */
|
|
244
|
-
handleMessage(msg) {
|
|
245
|
-
if ("id" in msg && typeof msg.id === "number") {
|
|
246
|
-
const pending = this.pendingRequests.get(msg.id);
|
|
247
|
-
if (!pending) return;
|
|
248
|
-
const response = msg;
|
|
249
|
-
if (response.error) {
|
|
250
|
-
pending.reject(new Error(
|
|
251
|
-
`MCP error [${response.error.code}]: ${response.error.message}`
|
|
252
|
-
));
|
|
253
|
-
} else {
|
|
254
|
-
pending.resolve(response.result);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
// ══════════════════════════════════════════════════════════════════
|
|
259
|
-
// 辅助方法
|
|
260
|
-
// ══════════════════════════════════════════════════════════════════
|
|
261
|
-
ensureConnected() {
|
|
262
|
-
if (!this.connected) {
|
|
263
|
-
throw new Error(
|
|
264
|
-
`MCP server [${this.serverId}] is not connected` + (this.errorMessage ? `: ${this.errorMessage}` : "")
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
/** Promise 超时包装 */
|
|
269
|
-
withTimeout(promise, ms, label) {
|
|
270
|
-
return new Promise((resolve, reject) => {
|
|
271
|
-
const timer = setTimeout(() => {
|
|
272
|
-
reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
|
|
273
|
-
}, ms);
|
|
274
|
-
promise.then((val) => {
|
|
275
|
-
clearTimeout(timer);
|
|
276
|
-
resolve(val);
|
|
277
|
-
}).catch((err) => {
|
|
278
|
-
clearTimeout(timer);
|
|
279
|
-
reject(err);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
/** 拒绝所有挂起的请求。pending.reject() 内部的 cleanup() 已包含 clearTimeout。 */
|
|
284
|
-
rejectAllPending(error) {
|
|
285
|
-
for (const [, pending] of this.pendingRequests) {
|
|
286
|
-
pending.reject(error);
|
|
287
|
-
}
|
|
288
|
-
this.pendingRequests.clear();
|
|
289
|
-
}
|
|
290
|
-
/** 杀掉子进程,并移除所有事件监听器防止僵尸引用 */
|
|
291
|
-
killProcess() {
|
|
292
|
-
if (this.process) {
|
|
293
|
-
this.process.removeAllListeners("error");
|
|
294
|
-
this.process.removeAllListeners("exit");
|
|
295
|
-
this.process.stdout?.removeAllListeners("data");
|
|
296
|
-
this.process.stderr?.removeAllListeners("data");
|
|
297
|
-
try {
|
|
298
|
-
this.process.stdin?.end();
|
|
299
|
-
this.process.kill();
|
|
300
|
-
} catch {
|
|
301
|
-
}
|
|
302
|
-
this.process = null;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
// src/mcp/manager.ts
|
|
308
|
-
var McpManager = class {
|
|
309
|
-
clients = /* @__PURE__ */ new Map();
|
|
310
|
-
/**
|
|
311
|
-
* 连接所有配置的 MCP 服务器(并发连接,单个失败不阻塞其他)。
|
|
312
|
-
* 连接结果通过 getStatus() 查看。
|
|
313
|
-
*
|
|
314
|
-
* @param options.quiet 抑制每个 server 的成功/失败 stderr 输出。REPL 在
|
|
315
|
-
* 后台连接时启用 quiet,避免日志和已绘制的 readline prompt 互相穿插;
|
|
316
|
-
* 连接完成后调用方自己用 getStatus() 渲染摘要(含失败列表)。
|
|
317
|
-
* server.ts 等同步启动场景保留默认(详细日志)。
|
|
318
|
-
*/
|
|
319
|
-
async connectAll(servers, options = {}) {
|
|
320
|
-
const entries = Object.entries(servers);
|
|
321
|
-
if (entries.length === 0) return;
|
|
322
|
-
const promises = entries.map(async ([serverId, config]) => {
|
|
323
|
-
const client = new McpClient(serverId, config);
|
|
324
|
-
this.clients.set(serverId, client);
|
|
325
|
-
try {
|
|
326
|
-
await client.connect();
|
|
327
|
-
if (!options.quiet) {
|
|
328
|
-
process.stderr.write(`[mcp] \u2713 ${serverId}: connected (${client.serverName}, ${client.tools.length} tools)
|
|
329
|
-
`);
|
|
330
|
-
}
|
|
331
|
-
} catch (err) {
|
|
332
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
333
|
-
if (!options.quiet) {
|
|
334
|
-
process.stderr.write(`[mcp] \u2717 ${serverId}: ${msg}
|
|
335
|
-
`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
await Promise.allSettled(promises);
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* 获取所有已连接服务器的工具,转换为 ai-cli 的 Tool 接口。
|
|
343
|
-
* 工具名格式:mcp__<serverId>__<toolName>
|
|
344
|
-
*/
|
|
345
|
-
getAllTools() {
|
|
346
|
-
const tools = [];
|
|
347
|
-
for (const [serverId, client] of this.clients) {
|
|
348
|
-
if (!client.isConnected) continue;
|
|
349
|
-
for (const mcpTool of client.tools) {
|
|
350
|
-
tools.push(this.wrapMcpTool(serverId, client, mcpTool));
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return tools;
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* 获取连接状态摘要。
|
|
357
|
-
*/
|
|
358
|
-
getStatus() {
|
|
359
|
-
const statuses = [];
|
|
360
|
-
for (const [serverId, client] of this.clients) {
|
|
361
|
-
statuses.push({
|
|
362
|
-
serverId,
|
|
363
|
-
serverName: client.serverName,
|
|
364
|
-
toolCount: client.tools.length,
|
|
365
|
-
connected: client.isConnected,
|
|
366
|
-
error: client.errorMessage ?? void 0
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
return statuses;
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* 获取已连接服务器数量。
|
|
373
|
-
*/
|
|
374
|
-
getConnectedCount() {
|
|
375
|
-
let count = 0;
|
|
376
|
-
for (const client of this.clients.values()) {
|
|
377
|
-
if (client.isConnected) count++;
|
|
378
|
-
}
|
|
379
|
-
return count;
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* 获取所有 MCP 工具的总数。
|
|
383
|
-
*/
|
|
384
|
-
getTotalToolCount() {
|
|
385
|
-
let count = 0;
|
|
386
|
-
for (const client of this.clients.values()) {
|
|
387
|
-
if (client.isConnected) count += client.tools.length;
|
|
388
|
-
}
|
|
389
|
-
return count;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* 重连指定 MCP 服务器。
|
|
393
|
-
* @returns 重连是否成功
|
|
394
|
-
*/
|
|
395
|
-
async reconnectServer(serverId) {
|
|
396
|
-
const client = this.clients.get(serverId);
|
|
397
|
-
if (!client) return false;
|
|
398
|
-
try {
|
|
399
|
-
await client.reconnect();
|
|
400
|
-
process.stderr.write(`[mcp] \u2713 ${serverId}: reconnected (${client.serverName}, ${client.tools.length} tools)
|
|
401
|
-
`);
|
|
402
|
-
return true;
|
|
403
|
-
} catch (err) {
|
|
404
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
405
|
-
process.stderr.write(`[mcp] \u2717 ${serverId}: reconnect failed: ${msg}
|
|
406
|
-
`);
|
|
407
|
-
return false;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* 重连所有已断开的 MCP 服务器。
|
|
412
|
-
* @returns 成功重连的服务器数量
|
|
413
|
-
*/
|
|
414
|
-
async reconnectAll() {
|
|
415
|
-
let reconnected = 0;
|
|
416
|
-
for (const [serverId, client] of this.clients) {
|
|
417
|
-
if (!client.isConnected) {
|
|
418
|
-
const ok = await this.reconnectServer(serverId);
|
|
419
|
-
if (ok) reconnected++;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
return reconnected;
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* 关闭所有 MCP 服务器连接。
|
|
426
|
-
*/
|
|
427
|
-
async closeAll() {
|
|
428
|
-
const promises = [...this.clients.entries()].map(
|
|
429
|
-
([id, c]) => c.close().catch((err) => {
|
|
430
|
-
process.stderr.write(`[mcp] Failed to close ${id}: ${err instanceof Error ? err.message : err}
|
|
431
|
-
`);
|
|
432
|
-
})
|
|
433
|
-
);
|
|
434
|
-
await Promise.allSettled(promises);
|
|
435
|
-
this.clients.clear();
|
|
436
|
-
}
|
|
437
|
-
// ══════════════════════════════════════════════════════════════════
|
|
438
|
-
// 内部方法
|
|
439
|
-
// ══════════════════════════════════════════════════════════════════
|
|
440
|
-
/**
|
|
441
|
-
* 将 MCP 工具包装为 ai-cli 的 Tool 接口。
|
|
442
|
-
* execute() 方法委托给对应 McpClient.callTool()。
|
|
443
|
-
*/
|
|
444
|
-
wrapMcpTool(serverId, client, mcpTool) {
|
|
445
|
-
const toolName = `${MCP_TOOL_PREFIX}${serverId}__${mcpTool.name}`;
|
|
446
|
-
const definition = this.convertDefinition(toolName, serverId, mcpTool);
|
|
447
|
-
return {
|
|
448
|
-
definition,
|
|
449
|
-
execute: async (args) => {
|
|
450
|
-
if (!client.isConnected) {
|
|
451
|
-
process.stderr.write(`[mcp] ${serverId}: disconnected, attempting reconnect...
|
|
452
|
-
`);
|
|
453
|
-
try {
|
|
454
|
-
await client.reconnect();
|
|
455
|
-
process.stderr.write(`[mcp] \u2713 ${serverId}: reconnected (${client.tools.length} tools)
|
|
456
|
-
`);
|
|
457
|
-
} catch {
|
|
458
|
-
throw new Error(
|
|
459
|
-
`MCP server [${serverId}] is disconnected and reconnect failed` + (client.errorMessage ? `: ${client.errorMessage}` : "") + `. Use /mcp reconnect to retry, or restart ai-cli.`
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
try {
|
|
464
|
-
const result = await client.callTool(mcpTool.name, args);
|
|
465
|
-
if (result.isError) {
|
|
466
|
-
const errorText = this.extractText(result.content);
|
|
467
|
-
throw new Error(errorText || "MCP tool returned error with no message");
|
|
468
|
-
}
|
|
469
|
-
return this.extractText(result.content) || "(no output)";
|
|
470
|
-
} catch (err) {
|
|
471
|
-
throw new Error(`MCP [${serverId}/${mcpTool.name}]: ${err instanceof Error ? err.message : String(err)}`);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* 转换 MCP 工具定义为 ai-cli 的 ToolDefinition。
|
|
478
|
-
* MCP 使用 JSON Schema 的 inputSchema,ai-cli 使用 Record<string, ToolParameterSchema>。
|
|
479
|
-
* M12 修复:递归转换嵌套 object/array schema,保留完整结构供 AI 正确传参。
|
|
480
|
-
*/
|
|
481
|
-
convertDefinition(toolName, serverId, mcpTool) {
|
|
482
|
-
const parameters = {};
|
|
483
|
-
const props = mcpTool.inputSchema?.properties ?? {};
|
|
484
|
-
const required = new Set(mcpTool.inputSchema?.required ?? []);
|
|
485
|
-
for (const [key, schema] of Object.entries(props)) {
|
|
486
|
-
parameters[key] = {
|
|
487
|
-
...this.convertSchema(schema),
|
|
488
|
-
...required.has(key) ? { required: true } : {}
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
return {
|
|
492
|
-
name: toolName,
|
|
493
|
-
description: (mcpTool.description ?? mcpTool.name) + ` [MCP: ${serverId}]`,
|
|
494
|
-
parameters
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* 递归转换单个 JSON Schema 属性为 ToolParameterSchema。
|
|
499
|
-
* 支持嵌套 object(含 properties)和 array(含 items)。
|
|
500
|
-
* 递归深度受 MCP 工具实际 schema 复杂度约束(通常 2-3 层)。
|
|
501
|
-
*/
|
|
502
|
-
convertSchema(schema) {
|
|
503
|
-
const type = this.normalizeType(String(schema["type"] ?? "string"));
|
|
504
|
-
const result = {
|
|
505
|
-
type,
|
|
506
|
-
description: String(schema["description"] ?? ""),
|
|
507
|
-
...schema["enum"] ? { enum: schema["enum"] } : {}
|
|
508
|
-
};
|
|
509
|
-
if (type === "object" && schema["properties"]) {
|
|
510
|
-
const props = schema["properties"];
|
|
511
|
-
result.properties = {};
|
|
512
|
-
for (const [k, v] of Object.entries(props)) {
|
|
513
|
-
result.properties[k] = this.convertSchema(v);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
if (type === "array" && schema["items"]) {
|
|
517
|
-
const items = schema["items"];
|
|
518
|
-
result.items = this.convertSchema(items);
|
|
519
|
-
}
|
|
520
|
-
return result;
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* 将 JSON Schema 类型映射到 ai-cli 的 ToolParameterType。
|
|
524
|
-
* integer → number(ai-cli 不区分整数/浮点)
|
|
525
|
-
*/
|
|
526
|
-
normalizeType(jsonSchemaType) {
|
|
527
|
-
switch (jsonSchemaType) {
|
|
528
|
-
case "string":
|
|
529
|
-
return "string";
|
|
530
|
-
case "number":
|
|
531
|
-
case "integer":
|
|
532
|
-
return "number";
|
|
533
|
-
case "boolean":
|
|
534
|
-
return "boolean";
|
|
535
|
-
case "array":
|
|
536
|
-
return "array";
|
|
537
|
-
case "object":
|
|
538
|
-
return "object";
|
|
539
|
-
default:
|
|
540
|
-
return "string";
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* 从 MCP 响应的 content blocks 中提取纯文本。
|
|
545
|
-
* 只处理 type="text" 的块,其他类型(image 等)跳过。
|
|
546
|
-
*/
|
|
547
|
-
extractText(content) {
|
|
548
|
-
return content.filter((block) => block.type === "text" && block.text).map((block) => block.text).join("\n");
|
|
549
|
-
}
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
// src/skills/manager.ts
|
|
553
|
-
import { existsSync, readdirSync, mkdirSync, statSync } from "fs";
|
|
554
|
-
import { join } from "path";
|
|
555
|
-
|
|
556
|
-
// src/skills/types.ts
|
|
557
|
-
import { readFileSync } from "fs";
|
|
558
|
-
import { basename } from "path";
|
|
559
|
-
function parseSimpleYaml(yaml) {
|
|
560
|
-
const result = {};
|
|
561
|
-
for (const line of yaml.split("\n")) {
|
|
562
|
-
const match = line.replace(/\r$/, "").match(/^(\w+)\s*:\s*(.+)$/);
|
|
563
|
-
if (match) {
|
|
564
|
-
result[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, "");
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
return result;
|
|
568
|
-
}
|
|
569
|
-
function parseYamlArray(value) {
|
|
570
|
-
const bracketMatch = value.match(/^\[(.+)]$/);
|
|
571
|
-
if (bracketMatch) {
|
|
572
|
-
return bracketMatch[1].split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, ""));
|
|
573
|
-
}
|
|
574
|
-
return [value.trim()];
|
|
575
|
-
}
|
|
576
|
-
function parseSkillFile(filePath) {
|
|
577
|
-
let raw;
|
|
578
|
-
try {
|
|
579
|
-
raw = readFileSync(filePath, "utf-8");
|
|
580
|
-
} catch {
|
|
581
|
-
return null;
|
|
582
|
-
}
|
|
583
|
-
const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
584
|
-
if (!frontmatterMatch) {
|
|
585
|
-
return {
|
|
586
|
-
meta: {
|
|
587
|
-
name: basename(filePath, ".md"),
|
|
588
|
-
description: ""
|
|
589
|
-
},
|
|
590
|
-
content: raw.trim(),
|
|
591
|
-
filePath
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
const [, yaml, content] = frontmatterMatch;
|
|
595
|
-
const parsed = parseSimpleYaml(yaml);
|
|
596
|
-
return {
|
|
597
|
-
meta: {
|
|
598
|
-
name: parsed["name"] ?? basename(filePath, ".md"),
|
|
599
|
-
description: parsed["description"] ?? "",
|
|
600
|
-
tools: parsed["tools"] ? parseYamlArray(parsed["tools"]) : void 0
|
|
601
|
-
},
|
|
602
|
-
content: content.trim(),
|
|
603
|
-
filePath
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// src/skills/manager.ts
|
|
608
|
-
var SKILL_CONTENT_WARN_CHARS_DEFAULT = 1e4;
|
|
609
|
-
var SkillManager = class {
|
|
610
|
-
skills = /* @__PURE__ */ new Map();
|
|
611
|
-
activeSkill = null;
|
|
612
|
-
skillsDir;
|
|
613
|
-
/** 超大技能文件警告阈值,由调用方从 config 传入;0 = 静默,undefined = 用默认 10000 */
|
|
614
|
-
warnThreshold;
|
|
615
|
-
constructor(skillsDir, warnThreshold) {
|
|
616
|
-
this.skillsDir = skillsDir;
|
|
617
|
-
this.warnThreshold = warnThreshold ?? SKILL_CONTENT_WARN_CHARS_DEFAULT;
|
|
618
|
-
}
|
|
619
|
-
/** 发现并加载 skillsDir 以及额外插件目录下的技能,返回加载数量 */
|
|
620
|
-
loadSkills(extraDirs = []) {
|
|
621
|
-
this.skills.clear();
|
|
622
|
-
this.loadSkillDir(this.skillsDir, true);
|
|
623
|
-
for (const dir of extraDirs) this.loadSkillDir(dir, false);
|
|
624
|
-
return this.skills.size;
|
|
625
|
-
}
|
|
626
|
-
loadSkillDir(dir, createIfMissing) {
|
|
627
|
-
if (!existsSync(dir)) {
|
|
628
|
-
if (createIfMissing) {
|
|
629
|
-
try {
|
|
630
|
-
mkdirSync(dir, { recursive: true });
|
|
631
|
-
} catch {
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
let entries;
|
|
637
|
-
try {
|
|
638
|
-
entries = readdirSync(dir);
|
|
639
|
-
} catch {
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
for (const entry of entries) {
|
|
643
|
-
let filePath;
|
|
644
|
-
const fullPath = join(dir, entry);
|
|
645
|
-
if (entry.endsWith(".md")) {
|
|
646
|
-
filePath = fullPath;
|
|
647
|
-
} else {
|
|
648
|
-
try {
|
|
649
|
-
if (statSync(fullPath).isDirectory()) {
|
|
650
|
-
const skillMd = join(fullPath, "SKILL.md");
|
|
651
|
-
if (existsSync(skillMd)) filePath = skillMd;
|
|
652
|
-
else continue;
|
|
653
|
-
} else {
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
} catch {
|
|
657
|
-
continue;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
const skill = parseSkillFile(filePath);
|
|
661
|
-
if (skill) {
|
|
662
|
-
if (skill.meta.name === "SKILL" && !entry.endsWith(".md")) skill.meta.name = entry;
|
|
663
|
-
if (!this.skills.has(skill.meta.name)) this.skills.set(skill.meta.name, skill);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
/** 列出所有已加载的技能 */
|
|
668
|
-
listSkills() {
|
|
669
|
-
return [...this.skills.values()];
|
|
670
|
-
}
|
|
671
|
-
/** 按 name 激活技能。返回被激活的 Skill,未找到返回 null。 */
|
|
672
|
-
activate(name) {
|
|
673
|
-
const skill = this.skills.get(name);
|
|
674
|
-
if (!skill) return null;
|
|
675
|
-
this.activeSkill = skill;
|
|
676
|
-
if (this.warnThreshold > 0 && skill.content.length > this.warnThreshold) {
|
|
677
|
-
process.stderr.write(
|
|
678
|
-
`\u26A0 Skill '${skill.meta.name}' is ${skill.content.length} chars (>${this.warnThreshold}) \u2014 this will be injected into the system prompt now. Adjust threshold with: /config set ui.skillSizeWarn <n|0>
|
|
679
|
-
`
|
|
680
|
-
);
|
|
681
|
-
}
|
|
682
|
-
return skill;
|
|
683
|
-
}
|
|
684
|
-
/** 停用当前技能 */
|
|
685
|
-
deactivate() {
|
|
686
|
-
this.activeSkill = null;
|
|
687
|
-
}
|
|
688
|
-
/** 获取当前激活的技能(null 表示无激活技能) */
|
|
689
|
-
getActive() {
|
|
690
|
-
return this.activeSkill;
|
|
691
|
-
}
|
|
692
|
-
/** 获取激活技能的 prompt 内容(null 表示无激活技能) */
|
|
693
|
-
getActivePromptContent() {
|
|
694
|
-
return this.activeSkill?.content ?? null;
|
|
695
|
-
}
|
|
696
|
-
/** 获取激活技能的工具白名单 Set(null 表示无限制) */
|
|
697
|
-
getActiveToolFilter() {
|
|
698
|
-
if (!this.activeSkill?.meta.tools) return null;
|
|
699
|
-
return new Set(this.activeSkill.meta.tools);
|
|
700
|
-
}
|
|
701
|
-
/** 获取技能目录路径 */
|
|
702
|
-
getSkillsDir() {
|
|
703
|
-
return this.skillsDir;
|
|
704
|
-
}
|
|
705
|
-
};
|
|
706
|
-
|
|
707
|
-
// src/core/proxy.ts
|
|
708
|
-
async function setupProxy(configProxy) {
|
|
709
|
-
const proxyUrl = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? process.env.https_proxy ?? process.env.http_proxy ?? configProxy;
|
|
710
|
-
if (!proxyUrl) return;
|
|
711
|
-
try {
|
|
712
|
-
const { ProxyAgent, setGlobalDispatcher } = await import("undici");
|
|
713
|
-
setGlobalDispatcher(new ProxyAgent({ uri: proxyUrl }));
|
|
714
|
-
} catch {
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// src/repl/commands/project-init.ts
|
|
719
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
720
|
-
import { join as join2 } from "path";
|
|
721
|
-
var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
722
|
-
"node_modules",
|
|
723
|
-
".git",
|
|
724
|
-
"dist",
|
|
725
|
-
"build",
|
|
726
|
-
"out",
|
|
727
|
-
"target",
|
|
728
|
-
".next",
|
|
729
|
-
".nuxt",
|
|
730
|
-
"__pycache__",
|
|
731
|
-
".venv",
|
|
732
|
-
"venv",
|
|
733
|
-
".tox",
|
|
734
|
-
".mypy_cache",
|
|
735
|
-
".pytest_cache",
|
|
736
|
-
".gradle",
|
|
737
|
-
".idea",
|
|
738
|
-
".vscode",
|
|
739
|
-
".vs",
|
|
740
|
-
"coverage",
|
|
741
|
-
".cache",
|
|
742
|
-
".parcel-cache",
|
|
743
|
-
"dist-cjs",
|
|
744
|
-
"release",
|
|
745
|
-
".output",
|
|
746
|
-
".turbo",
|
|
747
|
-
"vendor"
|
|
748
|
-
]);
|
|
749
|
-
function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
|
|
750
|
-
const lines = [];
|
|
751
|
-
let count = 0;
|
|
752
|
-
const walk = (d, prefix, depth) => {
|
|
753
|
-
if (depth > maxDepth || count >= maxEntries) return;
|
|
754
|
-
let entries;
|
|
755
|
-
try {
|
|
756
|
-
entries = readdirSync2(d);
|
|
757
|
-
} catch {
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
const filtered = entries.filter((e) => !e.startsWith(".") && !SCAN_SKIP_DIRS.has(e));
|
|
761
|
-
const sorted = filtered.sort((a, b) => {
|
|
762
|
-
let aIsDir = false, bIsDir = false;
|
|
763
|
-
try {
|
|
764
|
-
aIsDir = statSync2(join2(d, a)).isDirectory();
|
|
765
|
-
} catch {
|
|
766
|
-
}
|
|
767
|
-
try {
|
|
768
|
-
bIsDir = statSync2(join2(d, b)).isDirectory();
|
|
769
|
-
} catch {
|
|
770
|
-
}
|
|
771
|
-
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
|
|
772
|
-
return a.localeCompare(b);
|
|
773
|
-
});
|
|
774
|
-
for (let i = 0; i < sorted.length && count < maxEntries; i++) {
|
|
775
|
-
const name = sorted[i];
|
|
776
|
-
const fullPath = join2(d, name);
|
|
777
|
-
const isLast = i === sorted.length - 1;
|
|
778
|
-
const connector = isLast ? "+-- " : "|-- ";
|
|
779
|
-
let isDir;
|
|
780
|
-
try {
|
|
781
|
-
isDir = statSync2(fullPath).isDirectory();
|
|
782
|
-
} catch {
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
lines.push(prefix + connector + name + (isDir ? "/" : ""));
|
|
786
|
-
count++;
|
|
787
|
-
if (isDir) {
|
|
788
|
-
walk(fullPath, prefix + (isLast ? " " : "| "), depth + 1);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
};
|
|
792
|
-
walk(dir, "", 0);
|
|
793
|
-
if (count >= maxEntries) lines.push("... (truncated)");
|
|
794
|
-
return lines.join("\n");
|
|
795
|
-
}
|
|
796
|
-
function scanProject(cwd) {
|
|
797
|
-
const info = {
|
|
798
|
-
type: "unknown",
|
|
799
|
-
language: "unknown",
|
|
800
|
-
configFiles: [],
|
|
801
|
-
directoryStructure: ""
|
|
802
|
-
};
|
|
803
|
-
const check = (file) => existsSync2(join2(cwd, file));
|
|
804
|
-
const configCandidates = [
|
|
805
|
-
"package.json",
|
|
806
|
-
"tsconfig.json",
|
|
807
|
-
"Cargo.toml",
|
|
808
|
-
"pyproject.toml",
|
|
809
|
-
"setup.py",
|
|
810
|
-
"requirements.txt",
|
|
811
|
-
"go.mod",
|
|
812
|
-
"pom.xml",
|
|
813
|
-
"build.gradle",
|
|
814
|
-
"build.gradle.kts",
|
|
815
|
-
"CMakeLists.txt",
|
|
816
|
-
"Makefile",
|
|
817
|
-
".csproj",
|
|
818
|
-
".sln",
|
|
819
|
-
"composer.json",
|
|
820
|
-
"Gemfile",
|
|
821
|
-
"mix.exs",
|
|
822
|
-
"deno.json",
|
|
823
|
-
"bun.lockb"
|
|
824
|
-
];
|
|
825
|
-
info.configFiles = configCandidates.filter(check);
|
|
826
|
-
if (check("package.json")) {
|
|
827
|
-
info.type = "node";
|
|
828
|
-
info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
|
|
829
|
-
try {
|
|
830
|
-
const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf-8"));
|
|
831
|
-
const scripts = pkg.scripts ?? {};
|
|
832
|
-
info.buildCommand = scripts.build ? "npm run build" : void 0;
|
|
833
|
-
info.testCommand = scripts.test ? "npm test" : void 0;
|
|
834
|
-
info.devCommand = scripts.dev ? "npm run dev" : void 0;
|
|
835
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
836
|
-
if (allDeps["react"]) info.framework = "React";
|
|
837
|
-
else if (allDeps["vue"]) info.framework = "Vue";
|
|
838
|
-
else if (allDeps["@angular/core"]) info.framework = "Angular";
|
|
839
|
-
else if (allDeps["next"]) info.framework = "Next.js";
|
|
840
|
-
else if (allDeps["nuxt"]) info.framework = "Nuxt";
|
|
841
|
-
else if (allDeps["express"]) info.framework = "Express";
|
|
842
|
-
else if (allDeps["fastify"]) info.framework = "Fastify";
|
|
843
|
-
else if (allDeps["svelte"]) info.framework = "Svelte";
|
|
844
|
-
} catch {
|
|
845
|
-
}
|
|
846
|
-
} else if (check("Cargo.toml")) {
|
|
847
|
-
info.type = "rust";
|
|
848
|
-
info.language = "Rust";
|
|
849
|
-
info.buildCommand = "cargo build";
|
|
850
|
-
info.testCommand = "cargo test";
|
|
851
|
-
} else if (check("pyproject.toml") || check("setup.py") || check("requirements.txt")) {
|
|
852
|
-
info.type = "python";
|
|
853
|
-
info.language = "Python";
|
|
854
|
-
info.testCommand = check("pytest.ini") || check("pyproject.toml") ? "pytest" : "python -m unittest";
|
|
855
|
-
} else if (check("go.mod")) {
|
|
856
|
-
info.type = "go";
|
|
857
|
-
info.language = "Go";
|
|
858
|
-
info.buildCommand = "go build ./...";
|
|
859
|
-
info.testCommand = "go test ./...";
|
|
860
|
-
} else if (check("pom.xml")) {
|
|
861
|
-
info.type = "java";
|
|
862
|
-
info.language = "Java";
|
|
863
|
-
info.buildCommand = "mvn package";
|
|
864
|
-
info.testCommand = "mvn test";
|
|
865
|
-
} else if (check("build.gradle") || check("build.gradle.kts")) {
|
|
866
|
-
info.type = "java";
|
|
867
|
-
info.language = "Java/Kotlin";
|
|
868
|
-
info.buildCommand = "./gradlew build";
|
|
869
|
-
info.testCommand = "./gradlew test";
|
|
870
|
-
}
|
|
871
|
-
info.directoryStructure = scanDirTree(cwd);
|
|
872
|
-
return info;
|
|
873
|
-
}
|
|
874
|
-
function buildInitPrompt(info, cwd) {
|
|
875
|
-
const parts = [
|
|
876
|
-
"Please generate an AICLI.md context file (Markdown format) for the following project. This file will be injected into the AI conversation system prompt to help the AI understand the project structure and coding conventions.",
|
|
877
|
-
"\n## Project Info\n",
|
|
878
|
-
`- Working directory: ${cwd}`,
|
|
879
|
-
`- Type: ${info.type}`,
|
|
880
|
-
`- Language: ${info.language}`
|
|
881
|
-
];
|
|
882
|
-
if (info.framework) parts.push(`- Framework: ${info.framework}`);
|
|
883
|
-
if (info.buildCommand) parts.push(`- Build command: ${info.buildCommand}`);
|
|
884
|
-
if (info.testCommand) parts.push(`- Test command: ${info.testCommand}`);
|
|
885
|
-
if (info.devCommand) parts.push(`- Dev command: ${info.devCommand}`);
|
|
886
|
-
parts.push(`
|
|
887
|
-
## Detected Config Files
|
|
888
|
-
${info.configFiles.map((f) => `- ${f}`).join("\n")}`);
|
|
889
|
-
parts.push(`
|
|
890
|
-
## Directory Structure
|
|
891
|
-
\`\`\`
|
|
892
|
-
${info.directoryStructure}
|
|
893
|
-
\`\`\``);
|
|
894
|
-
parts.push(`
|
|
895
|
-
## Requirements
|
|
896
|
-
Please generate a structured Markdown file containing:
|
|
897
|
-
1. Project overview (one-sentence summary)
|
|
898
|
-
2. Tech stack
|
|
899
|
-
3. Project structure description (based on the directory structure above)
|
|
900
|
-
4. Common commands (build, test, dev, etc.)
|
|
901
|
-
5. Code style and conventions (inferred from config files)
|
|
902
|
-
|
|
903
|
-
Output the Markdown content directly, do not wrap the entire file in a code block. Keep it concise, within 200 lines.`);
|
|
904
|
-
return parts.join("\n");
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// src/core/memory-filters.ts
|
|
908
|
-
function parseFlags(items) {
|
|
909
|
-
const flags = {};
|
|
910
|
-
const text = [];
|
|
911
|
-
for (let i = 0; i < items.length; i++) {
|
|
912
|
-
const item = items[i];
|
|
913
|
-
if (item.startsWith("--")) {
|
|
914
|
-
const key = item.slice(2);
|
|
915
|
-
const next = items[i + 1];
|
|
916
|
-
if (next && !next.startsWith("--")) {
|
|
917
|
-
flags[key] = next;
|
|
918
|
-
i++;
|
|
919
|
-
} else {
|
|
920
|
-
flags[key] = true;
|
|
921
|
-
}
|
|
922
|
-
} else {
|
|
923
|
-
text.push(item);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
return { flags, text: text.join(" ").trim() };
|
|
927
|
-
}
|
|
928
|
-
var SCOPES = /* @__PURE__ */ new Set(["personal", "project", "session", "team"]);
|
|
929
|
-
var BOOLEAN_FLAGS = ["pending", "orphaned"];
|
|
930
|
-
function parseMemoryShowFilters(args) {
|
|
931
|
-
const { flags, text } = parseFlags(args);
|
|
932
|
-
const strayTerms = [];
|
|
933
|
-
for (const key of BOOLEAN_FLAGS) {
|
|
934
|
-
const v = flags[key];
|
|
935
|
-
if (typeof v === "string") {
|
|
936
|
-
strayTerms.push(v);
|
|
937
|
-
flags[key] = true;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
const scopeRaw = flags["scope"];
|
|
941
|
-
const freeText = [text, ...strayTerms].filter(Boolean).join(" ").trim();
|
|
942
|
-
const searchRaw = typeof flags["search"] === "string" ? flags["search"] : freeText || void 0;
|
|
943
|
-
const staleRaw = flags["stale"];
|
|
944
|
-
let staleDays;
|
|
945
|
-
let invalidStale = false;
|
|
946
|
-
if (staleRaw !== void 0) {
|
|
947
|
-
const n = typeof staleRaw === "string" ? Number(staleRaw) : NaN;
|
|
948
|
-
if (!Number.isFinite(n) || n <= 0) invalidStale = true;
|
|
949
|
-
else staleDays = n;
|
|
950
|
-
}
|
|
951
|
-
return {
|
|
952
|
-
pending: flags["pending"] === true,
|
|
953
|
-
orphaned: flags["orphaned"] === true,
|
|
954
|
-
scope: typeof scopeRaw === "string" && SCOPES.has(scopeRaw) ? scopeRaw : void 0,
|
|
955
|
-
search: searchRaw?.toLowerCase(),
|
|
956
|
-
staleDays,
|
|
957
|
-
invalidStale
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
function filterMemoryEntries(entries, filters) {
|
|
961
|
-
let out = entries;
|
|
962
|
-
const applied = [];
|
|
963
|
-
if (filters.orphaned) applied.push("orphaned");
|
|
964
|
-
if (filters.pending) {
|
|
965
|
-
out = out.filter((m) => !m.approved && !isMemoryExpired(m));
|
|
966
|
-
applied.push("pending");
|
|
967
|
-
}
|
|
968
|
-
if (filters.scope) {
|
|
969
|
-
out = out.filter((m) => m.scope === filters.scope);
|
|
970
|
-
applied.push(`scope:${filters.scope}`);
|
|
971
|
-
}
|
|
972
|
-
if (filters.search) {
|
|
973
|
-
const q = filters.search;
|
|
974
|
-
out = out.filter((m) => m.content.toLowerCase().includes(q) || m.id.startsWith(q));
|
|
975
|
-
applied.push(`search:"${q}"`);
|
|
976
|
-
}
|
|
977
|
-
if (filters.staleDays !== void 0) {
|
|
978
|
-
const days = filters.staleDays;
|
|
979
|
-
out = out.filter((m) => isMemoryStale(m, days));
|
|
980
|
-
applied.push(`stale>${days}d`);
|
|
981
|
-
}
|
|
982
|
-
return { entries: out, applied };
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// src/core/session-export.ts
|
|
986
|
-
function parseExportArgs(args) {
|
|
987
|
-
let format = "md";
|
|
988
|
-
let outFile = null;
|
|
989
|
-
for (const arg of args) {
|
|
990
|
-
if (arg === "md" || arg === "json") format = arg;
|
|
991
|
-
else if (arg) outFile = arg;
|
|
992
|
-
}
|
|
993
|
-
return { format, outFile };
|
|
994
|
-
}
|
|
995
|
-
function defaultExportFilename(session, format) {
|
|
996
|
-
const dateTag = session.created.toISOString().slice(0, 10);
|
|
997
|
-
return `session-${dateTag}-${session.id.slice(0, 8)}.${format === "json" ? "json" : "md"}`;
|
|
998
|
-
}
|
|
999
|
-
function markdownHeading(msg) {
|
|
1000
|
-
switch (msg.role) {
|
|
1001
|
-
case "user":
|
|
1002
|
-
return "## \u{1F464} You";
|
|
1003
|
-
case "assistant":
|
|
1004
|
-
return "## \u{1F916} Assistant";
|
|
1005
|
-
case "tool":
|
|
1006
|
-
return `### \u{1F527} Tool result: ${msg.toolName ?? "tool"}${msg.isError ? " (error)" : ""}`;
|
|
1007
|
-
default:
|
|
1008
|
-
return `## \u2699 ${msg.role}`;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
function buildSessionExport(session, format, outFile) {
|
|
1012
|
-
const filename = outFile || defaultExportFilename(session, format);
|
|
1013
|
-
if (format === "json") {
|
|
1014
|
-
const payload = session.toJSON ? session.toJSON() : {
|
|
1015
|
-
id: session.id,
|
|
1016
|
-
title: session.title ?? null,
|
|
1017
|
-
provider: session.provider,
|
|
1018
|
-
model: session.model,
|
|
1019
|
-
created: session.created.toISOString(),
|
|
1020
|
-
messages: session.messages
|
|
1021
|
-
};
|
|
1022
|
-
return { filename, content: JSON.stringify(payload, null, 2), messageCount: session.messages.length };
|
|
1023
|
-
}
|
|
1024
|
-
const dateStr = session.created.toISOString().replace("T", " ").slice(0, 19);
|
|
1025
|
-
let md = `# ${session.title ?? "Conversation Export"}
|
|
1026
|
-
|
|
1027
|
-
`;
|
|
1028
|
-
md += `| Field | Value |
|
|
1029
|
-
|----------|-------|
|
|
1030
|
-
`;
|
|
1031
|
-
md += `| Provider | ${session.provider} |
|
|
1032
|
-
`;
|
|
1033
|
-
md += `| Model | ${session.model} |
|
|
1034
|
-
`;
|
|
1035
|
-
md += `| Date | ${dateStr} |
|
|
1036
|
-
`;
|
|
1037
|
-
md += `| Session | ${session.id.slice(0, 8)} |
|
|
1038
|
-
|
|
1039
|
-
---
|
|
1040
|
-
|
|
1041
|
-
`;
|
|
1042
|
-
for (const msg of session.messages) {
|
|
1043
|
-
const text = getContentText(msg.content);
|
|
1044
|
-
md += `${markdownHeading(msg)}
|
|
1045
|
-
|
|
1046
|
-
`;
|
|
1047
|
-
if (text.trim()) md += `${text}
|
|
1048
|
-
|
|
1049
|
-
`;
|
|
1050
|
-
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
1051
|
-
for (const tc of msg.toolCalls) {
|
|
1052
|
-
md += `\`\`\`json
|
|
1053
|
-
${JSON.stringify({ tool: tc.name, arguments: tc.arguments ?? {} }, null, 2)}
|
|
1054
|
-
\`\`\`
|
|
1055
|
-
|
|
1056
|
-
`;
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
md += `---
|
|
1060
|
-
|
|
1061
|
-
`;
|
|
1062
|
-
}
|
|
1063
|
-
return { filename, content: md, messageCount: session.messages.length };
|
|
1064
|
-
}
|
|
1065
|
-
function parseCheckpointCommand(args) {
|
|
1066
|
-
const sub = args[0]?.toLowerCase();
|
|
1067
|
-
if (!sub || sub === "list") return { kind: "list" };
|
|
1068
|
-
const name = args.slice(1).join(" ").trim();
|
|
1069
|
-
if (sub === "save" || sub === "restore" || sub === "delete") {
|
|
1070
|
-
if (!name) return { kind: "usage", message: `Usage: /checkpoint ${sub} <name>` };
|
|
1071
|
-
return { kind: sub, name };
|
|
1072
|
-
}
|
|
1073
|
-
return { kind: "unknown", message: `Unknown subcommand: ${sub}. Use save/restore/list/delete.` };
|
|
1074
|
-
}
|
|
1075
|
-
function formatCheckpointList(checkpoints) {
|
|
1076
|
-
if (checkpoints.length === 0) {
|
|
1077
|
-
return ["No checkpoints. Use /checkpoint save <name> to create one."];
|
|
1078
|
-
}
|
|
1079
|
-
return [
|
|
1080
|
-
`Checkpoints (${checkpoints.length}):`,
|
|
1081
|
-
...checkpoints.map((cp) => ` ${cp.name.padEnd(20)} msg #${cp.messageIndex} ${cp.timestamp.toLocaleString()}`)
|
|
1082
|
-
];
|
|
1083
|
-
}
|
|
1084
|
-
var checkpointSavedMessage = (name, messageCount) => `\u2713 Checkpoint "${name}" saved at message #${messageCount}`;
|
|
1085
|
-
var checkpointRestoredMessage = (name, messageCount) => `\u2713 Restored to checkpoint "${name}" (${messageCount} messages, token usage reset)`;
|
|
1086
|
-
var checkpointDeletedMessage = (name) => `\u2713 Deleted checkpoint "${name}"`;
|
|
1087
|
-
var checkpointNotFoundMessage = (name) => `Checkpoint "${name}" not found.`;
|
|
1088
|
-
|
|
1089
|
-
// src/session/tool-history.ts
|
|
1090
|
-
var SESSION_SIZE_LIMIT = 2 * 1024 * 1024;
|
|
1091
|
-
function persistToolRound(session, toolCalls, toolResults, opts) {
|
|
1092
|
-
session.addMessage({
|
|
1093
|
-
role: "assistant",
|
|
1094
|
-
content: opts?.assistantContent ?? "",
|
|
1095
|
-
toolCalls,
|
|
1096
|
-
reasoningContent: opts?.reasoningContent,
|
|
1097
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
1098
|
-
});
|
|
1099
|
-
for (let i = 0; i < toolCalls.length; i++) {
|
|
1100
|
-
const tc = toolCalls[i];
|
|
1101
|
-
const tr = toolResults[i];
|
|
1102
|
-
if (!tr) continue;
|
|
1103
|
-
session.addMessage({
|
|
1104
|
-
role: "tool",
|
|
1105
|
-
content: truncateForPersist(tr.content),
|
|
1106
|
-
toolCallId: tr.callId,
|
|
1107
|
-
toolName: tc.name,
|
|
1108
|
-
isError: tr.isError,
|
|
1109
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
function trimOldToolOutput(messages, keepRecentRounds = 10) {
|
|
1114
|
-
const roundStarts = [];
|
|
1115
|
-
for (let i = 0; i < messages.length; i++) {
|
|
1116
|
-
if (messages[i].role === "assistant" && messages[i].toolCalls?.length) {
|
|
1117
|
-
roundStarts.push(i);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
if (roundStarts.length <= keepRecentRounds) return 0;
|
|
1121
|
-
const cutoffRoundIdx = roundStarts.length - keepRecentRounds;
|
|
1122
|
-
let trimmed = 0;
|
|
1123
|
-
for (let r = 0; r < cutoffRoundIdx; r++) {
|
|
1124
|
-
const start = roundStarts[r];
|
|
1125
|
-
const end = r + 1 < roundStarts.length ? roundStarts[r + 1] : messages.length;
|
|
1126
|
-
const assistantMsg = messages[start];
|
|
1127
|
-
if (typeof assistantMsg.content === "string" && assistantMsg.content.length > 200) {
|
|
1128
|
-
assistantMsg.content = assistantMsg.content.slice(0, 100) + "\u2026 [trimmed for size]";
|
|
1129
|
-
trimmed++;
|
|
1130
|
-
}
|
|
1131
|
-
for (let j = start + 1; j < end; j++) {
|
|
1132
|
-
const msg = messages[j];
|
|
1133
|
-
if (msg.role === "tool") {
|
|
1134
|
-
const status = msg.isError ? "\u2717 error" : "\u2713 ok";
|
|
1135
|
-
const name = msg.toolName ?? "unknown";
|
|
1136
|
-
const currentContent = typeof msg.content === "string" ? msg.content : "";
|
|
1137
|
-
if (currentContent.length > 200) {
|
|
1138
|
-
msg.content = `[${name}: ${status}] (output trimmed for size \u2014 ${currentContent.length} chars)`;
|
|
1139
|
-
trimmed++;
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
return trimmed;
|
|
1145
|
-
}
|
|
1146
|
-
function cloneMessages(messages) {
|
|
1147
|
-
return messages.map((m) => ({ ...m }));
|
|
1148
|
-
}
|
|
1149
|
-
function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
|
|
1150
|
-
const originalSize = JSON.stringify(session.toJSON()).length;
|
|
1151
|
-
if (originalSize <= sizeLimit) return false;
|
|
1152
|
-
const snapshotLen = session.messages.length;
|
|
1153
|
-
const working = cloneMessages(session.messages.slice(0, snapshotLen));
|
|
1154
|
-
let committedSize = originalSize;
|
|
1155
|
-
let anyTrim = false;
|
|
1156
|
-
let keepRecent = 10;
|
|
1157
|
-
while (keepRecent >= 2) {
|
|
1158
|
-
const trimmedCount = trimOldToolOutput(working, keepRecent);
|
|
1159
|
-
if (trimmedCount === 0) break;
|
|
1160
|
-
anyTrim = true;
|
|
1161
|
-
const originalMessages = session.messages;
|
|
1162
|
-
session.messages = [...working, ...originalMessages.slice(snapshotLen)];
|
|
1163
|
-
const newSize = JSON.stringify(session.toJSON()).length;
|
|
1164
|
-
session.messages = originalMessages;
|
|
1165
|
-
committedSize = newSize;
|
|
1166
|
-
if (newSize <= sizeLimit) break;
|
|
1167
|
-
keepRecent = Math.max(2, Math.floor(keepRecent / 2));
|
|
1168
|
-
if (keepRecent < 2) break;
|
|
1169
|
-
}
|
|
1170
|
-
if (!anyTrim) return false;
|
|
1171
|
-
const appended = session.messages.slice(snapshotLen);
|
|
1172
|
-
session.messages.splice(0, session.messages.length, ...working, ...appended);
|
|
1173
|
-
return committedSize < originalSize;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// src/core/context-pressure.ts
|
|
1177
|
-
function estimateConversationTokens(input) {
|
|
1178
|
-
let total = 0;
|
|
1179
|
-
if (input.systemPrompt) total += estimateTokens(input.systemPrompt);
|
|
1180
|
-
for (const msg of input.messages) {
|
|
1181
|
-
if (typeof msg.content === "string") {
|
|
1182
|
-
total += estimateTokens(msg.content);
|
|
1183
|
-
} else if (Array.isArray(msg.content)) {
|
|
1184
|
-
for (const part of msg.content) {
|
|
1185
|
-
if (part.type === "text" && part.text) total += estimateTokens(part.text);
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
if (msg.toolCalls) {
|
|
1189
|
-
for (const tc of msg.toolCalls) {
|
|
1190
|
-
total += estimateTokens(JSON.stringify(tc.arguments));
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
return total;
|
|
1195
|
-
}
|
|
1196
|
-
function evaluateContextPressure(input) {
|
|
1197
|
-
if (input.contextWindow <= 0) return { shouldCompact: false, ratio: null, percent: null };
|
|
1198
|
-
const ratio = input.estimatedTokens / input.contextWindow;
|
|
1199
|
-
const threshold = input.threshold ?? CONTEXT_PRESSURE_THRESHOLD;
|
|
1200
|
-
return {
|
|
1201
|
-
shouldCompact: input.autoCompactEnabled && ratio >= threshold,
|
|
1202
|
-
ratio,
|
|
1203
|
-
percent: Math.round(ratio * 100)
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
var AUTO_COMPACT_INSTRUCTION = "Preserve key context, compress old content, ensure seamless continuation";
|
|
1207
|
-
|
|
1208
|
-
// src/core/model-router.ts
|
|
1209
|
-
var TAG_REGEX = /(?:^|\s)#([a-zA-Z][\w-]{0,31})\b/g;
|
|
1210
|
-
function extractTags(message) {
|
|
1211
|
-
const tags = /* @__PURE__ */ new Set();
|
|
1212
|
-
let m;
|
|
1213
|
-
TAG_REGEX.lastIndex = 0;
|
|
1214
|
-
while ((m = TAG_REGEX.exec(message)) !== null) {
|
|
1215
|
-
tags.add(m[1].toLowerCase());
|
|
1216
|
-
}
|
|
1217
|
-
return tags;
|
|
1218
|
-
}
|
|
1219
|
-
function matches(message, matcher) {
|
|
1220
|
-
const trimmed = message.trim();
|
|
1221
|
-
const lower = trimmed.toLowerCase();
|
|
1222
|
-
if (matcher.tag) {
|
|
1223
|
-
const tags = extractTags(trimmed);
|
|
1224
|
-
if (!tags.has(matcher.tag.toLowerCase())) return false;
|
|
1225
|
-
}
|
|
1226
|
-
if (matcher.contains && matcher.contains.length > 0) {
|
|
1227
|
-
const hit = matcher.contains.some((kw) => lower.includes(kw.toLowerCase()));
|
|
1228
|
-
if (!hit) return false;
|
|
1229
|
-
}
|
|
1230
|
-
if (typeof matcher.maxLength === "number") {
|
|
1231
|
-
if (trimmed.length > matcher.maxLength) return false;
|
|
1232
|
-
}
|
|
1233
|
-
if (typeof matcher.minLength === "number") {
|
|
1234
|
-
if (trimmed.length < matcher.minLength) return false;
|
|
1235
|
-
}
|
|
1236
|
-
const hasAnyCondition = !!matcher.tag || matcher.contains && matcher.contains.length > 0 || typeof matcher.maxLength === "number" || typeof matcher.minLength === "number";
|
|
1237
|
-
return !!hasAnyCondition;
|
|
1238
|
-
}
|
|
1239
|
-
function pickModel(message, currentModel, config, availableModels = []) {
|
|
1240
|
-
if (!config.enabled || config.rules.length === 0) {
|
|
1241
|
-
return { model: currentModel, reason: "routing disabled", overridden: false };
|
|
1242
|
-
}
|
|
1243
|
-
const isAvailable = (m) => availableModels.length === 0 || availableModels.includes(m);
|
|
1244
|
-
for (let i = 0; i < config.rules.length; i++) {
|
|
1245
|
-
const rule = config.rules[i];
|
|
1246
|
-
if (!matches(message, rule.match)) continue;
|
|
1247
|
-
if (!isAvailable(rule.model)) continue;
|
|
1248
|
-
if (rule.model === currentModel) {
|
|
1249
|
-
return {
|
|
1250
|
-
model: currentModel,
|
|
1251
|
-
reason: `rule "${rule.name ?? `#${i}`}" matched (same as current)`,
|
|
1252
|
-
overridden: false,
|
|
1253
|
-
ruleIdx: i
|
|
1254
|
-
};
|
|
1255
|
-
}
|
|
1256
|
-
return {
|
|
1257
|
-
model: rule.model,
|
|
1258
|
-
reason: `rule "${rule.name ?? `#${i}`}" matched`,
|
|
1259
|
-
overridden: true,
|
|
1260
|
-
ruleIdx: i
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
if (config.fallback && config.fallback !== currentModel && isAvailable(config.fallback)) {
|
|
1264
|
-
return {
|
|
1265
|
-
model: config.fallback,
|
|
1266
|
-
reason: "fallback",
|
|
1267
|
-
overridden: true
|
|
1268
|
-
};
|
|
1269
|
-
}
|
|
1270
|
-
return { model: currentModel, reason: "no rule matched", overridden: false };
|
|
1271
|
-
}
|
|
1272
|
-
function stripRoutingTags(message) {
|
|
1273
|
-
return message.replace(/(?:^|\s)#(fast|deep|default)\b/gi, " ").replace(/\s{2,}/g, " ").trim();
|
|
1274
|
-
}
|
|
1275
|
-
function decideModel(input) {
|
|
1276
|
-
if (!input.routingConfig || !input.routingConfig.enabled) {
|
|
1277
|
-
return { model: input.currentModel, reason: "routing disabled", overridden: false };
|
|
1278
|
-
}
|
|
1279
|
-
return pickModel(input.userInput, input.currentModel, input.routingConfig, input.availableModels);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
// src/repl/dev-state.ts
|
|
1283
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, unlinkSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1284
|
-
import { join as join3 } from "path";
|
|
1285
|
-
import { homedir } from "os";
|
|
1286
|
-
var DEV_STATE_MAX_CHARS = 6e3;
|
|
1287
|
-
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.
|
|
1288
|
-
|
|
1289
|
-
CRITICAL: Be SPECIFIC and DETAILED. Include exact values, file paths, format requirements, and constraints \u2014 vague summaries are useless to the next model.
|
|
1290
|
-
|
|
1291
|
-
Output ONLY the snapshot in the following exact format (no preamble, no explanation):
|
|
1292
|
-
|
|
1293
|
-
## Development State Snapshot
|
|
1294
|
-
|
|
1295
|
-
### Current Task
|
|
1296
|
-
[1-2 sentence summary of the primary task/goal the user is working on]
|
|
1297
|
-
|
|
1298
|
-
### Key Parameters & Constraints
|
|
1299
|
-
- [List ALL specific parameters, values, format requirements discovered during this conversation]
|
|
1300
|
-
- [Include exact numbers, dimensions, scoring rules, naming conventions, etc.]
|
|
1301
|
-
- [Example: "Exam duration 90 minutes, total score 200, 40 single-choice x2 pts + 20 multi-choice x4 pts + 20 true/false x2 pts"]
|
|
1302
|
-
- [Example: "File naming format YYYYMMDD-NN-mock-difficulty.md, saved to exam_papers/ directory"]
|
|
1303
|
-
|
|
1304
|
-
### Completed Steps
|
|
1305
|
-
- [List each completed step with specific details (file paths, key outcomes)]
|
|
1306
|
-
|
|
1307
|
-
### In-Progress Work
|
|
1308
|
-
- [List any work that was started but not finished]
|
|
1309
|
-
|
|
1310
|
-
### Critical Reference Files
|
|
1311
|
-
- [List file paths that the next model MUST read before doing any work]
|
|
1312
|
-
- [Include brief description of each file's purpose]
|
|
1313
|
-
- [Example: "Exam2025.md \u2014 reference format for official past exams (90min/200pts/question type distribution)"]
|
|
1314
|
-
- [Example: "style-guide.md \u2014 style guidelines (character setup/scenario building/current events)"]
|
|
1315
|
-
|
|
1316
|
-
### Modified/Created Files
|
|
1317
|
-
- [List any files that were created or modified, with brief notes on content]
|
|
1318
|
-
|
|
1319
|
-
### Key Decisions & Context
|
|
1320
|
-
- [Important decisions, user preferences, or constraints established]
|
|
1321
|
-
|
|
1322
|
-
### Next Steps
|
|
1323
|
-
- [What should be done next to continue this work]
|
|
1324
|
-
- [Include specific instructions the next model should follow]
|
|
1325
|
-
|
|
1326
|
-
### Important Notes
|
|
1327
|
-
- [Any warnings, caveats, or critical context the next model needs to know]
|
|
1328
|
-
- [Things that went wrong or should be avoided]
|
|
1329
|
-
|
|
1330
|
-
If any section has no content, write "(none)" for that section. Be thorough \u2014 the next model may have access to our conversation messages, but the detailed tool call results (file contents, command outputs) are NOT preserved. This snapshot is the primary source of specific details and context.`;
|
|
1331
|
-
function sessionHasMeaningfulContent(messages) {
|
|
1332
|
-
if (messages.length < 2) return false;
|
|
1333
|
-
const hasUser = messages.some((m) => m.role === "user");
|
|
1334
|
-
const hasAssistant = messages.some((m) => m.role === "assistant");
|
|
1335
|
-
return hasUser && hasAssistant;
|
|
1336
|
-
}
|
|
1337
|
-
function getDevStatePath() {
|
|
1338
|
-
return join3(homedir(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
|
|
1339
|
-
}
|
|
1340
|
-
function saveDevState(content) {
|
|
1341
|
-
const configDir = join3(homedir(), CONFIG_DIR_NAME);
|
|
1342
|
-
if (!existsSync3(configDir)) {
|
|
1343
|
-
mkdirSync2(configDir, { recursive: true });
|
|
1344
|
-
}
|
|
1345
|
-
let trimmed = content.trim();
|
|
1346
|
-
if (trimmed.length > DEV_STATE_MAX_CHARS) {
|
|
1347
|
-
trimmed = trimmed.slice(0, DEV_STATE_MAX_CHARS);
|
|
1348
|
-
const lastNewline = trimmed.lastIndexOf("\n");
|
|
1349
|
-
if (lastNewline > 0) {
|
|
1350
|
-
trimmed = trimmed.slice(0, lastNewline);
|
|
1351
|
-
}
|
|
1352
|
-
trimmed += "\n\n[...truncated]";
|
|
1353
|
-
}
|
|
1354
|
-
atomicWriteFileSync(getDevStatePath(), trimmed);
|
|
1355
|
-
}
|
|
1356
|
-
function loadDevState() {
|
|
1357
|
-
const path = getDevStatePath();
|
|
1358
|
-
if (!existsSync3(path)) return null;
|
|
1359
|
-
const content = readFileSync3(path, "utf-8").trim();
|
|
1360
|
-
return content || null;
|
|
1361
|
-
}
|
|
1362
|
-
function clearDevState() {
|
|
1363
|
-
const path = getDevStatePath();
|
|
1364
|
-
if (existsSync3(path)) {
|
|
1365
|
-
try {
|
|
1366
|
-
unlinkSync(path);
|
|
1367
|
-
} catch {
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// src/tools/deferred-save.ts
|
|
1373
|
-
import { statSync as statSync3 } from "fs";
|
|
1374
|
-
import { basename as basename2 } from "path";
|
|
1375
|
-
function snapshotFile(path) {
|
|
1376
|
-
try {
|
|
1377
|
-
const st = statSync3(path);
|
|
1378
|
-
return { exists: true, size: st.size, mtimeMs: st.mtimeMs };
|
|
1379
|
-
} catch {
|
|
1380
|
-
return { exists: false, size: 0, mtimeMs: 0 };
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
function armDeferredSave(path) {
|
|
1384
|
-
return { path, armedSnapshot: snapshotFile(path) };
|
|
1385
|
-
}
|
|
1386
|
-
function snapshotChanged(a, b) {
|
|
1387
|
-
return a.exists !== b.exists || a.size !== b.size || a.mtimeMs !== b.mtimeMs;
|
|
1388
|
-
}
|
|
1389
|
-
var SAVE_CLAIM_PATTERNS = [
|
|
1390
|
-
/(?:已|均已|都已)[^\n]{0,12}(?:保存|写入|生成|创建|落盘)/u,
|
|
1391
|
-
/(?:保存|写入|生成|创建)(?:到|至|于)\s*[`'"]/u,
|
|
1392
|
-
/\b(?:saved|written|created|generated)\s+(?:to|at|as|into)\b/i
|
|
1393
|
-
];
|
|
1394
|
-
function narratesSavedTarget(content, path) {
|
|
1395
|
-
const base = basename2(path);
|
|
1396
|
-
if (!base || !content.includes(base)) return null;
|
|
1397
|
-
for (const re of SAVE_CLAIM_PATTERNS) {
|
|
1398
|
-
if (re.test(content)) return re.source;
|
|
1399
|
-
}
|
|
1400
|
-
return null;
|
|
1401
|
-
}
|
|
1402
|
-
function decideDeferredSave(params) {
|
|
1403
|
-
const { content, path, armedSnapshot, currentSnapshot } = params;
|
|
1404
|
-
const body = stripOuterCodeFence(content);
|
|
1405
|
-
if (snapshotChanged(armedSnapshot, currentSnapshot)) {
|
|
1406
|
-
return {
|
|
1407
|
-
kind: "skip",
|
|
1408
|
-
reason: "target-already-written",
|
|
1409
|
-
message: `\u21AA \u8DF3\u8FC7 deferred \u843D\u76D8\uFF1A${path} \u5DF2\u5728\u672C\u8F6E\u88AB\u5DE5\u5177\u5199\u5165\uFF08${currentSnapshot.size} bytes\uFF09\uFF0C\u4E0D\u7528\u6700\u7EC8\u56DE\u590D\u8986\u76D6\u5B83\u3002`
|
|
1410
|
-
};
|
|
1411
|
-
}
|
|
1412
|
-
const narrated = narratesSavedTarget(body, path);
|
|
1413
|
-
if (narrated) {
|
|
1414
|
-
return {
|
|
1415
|
-
kind: "skip",
|
|
1416
|
-
reason: "narrates-target",
|
|
1417
|
-
message: `\u21AA \u8DF3\u8FC7 deferred \u843D\u76D8\uFF1A\u6700\u7EC8\u56DE\u590D\u662F\u5173\u4E8E ${basename2(path)} \u7684\u6C47\u62A5\u800C\u975E\u6587\u6863\u6B63\u6587\uFF08matched: ${narrated}\uFF09\u3002`
|
|
1418
|
-
};
|
|
1419
|
-
}
|
|
1420
|
-
if (!isCleanDocumentBody(body)) {
|
|
1421
|
-
return {
|
|
1422
|
-
kind: "skip",
|
|
1423
|
-
reason: "not-document-body",
|
|
1424
|
-
message: `\u21AA \u8DF3\u8FC7 deferred \u843D\u76D8\uFF1A\u6700\u7EC8\u56DE\u590D\u4E0D\u50CF\u53EF\u76F4\u63A5\u843D\u76D8\u7684\u6587\u6863\u6B63\u6587\uFF08${path} \u672A\u6539\u52A8\uFF09\u3002`
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1427
|
-
return { kind: "save", body };
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
// src/core/think-command.ts
|
|
1431
|
-
var EFFORTS = /* @__PURE__ */ new Set(["low", "high", "max"]);
|
|
1432
|
-
function resolveEffective(state) {
|
|
1433
|
-
if (state.runtimeThinking !== null) return { effective: state.runtimeThinking, origin: "runtime override" };
|
|
1434
|
-
if (state.configuredThinking !== void 0) return { effective: state.configuredThinking, origin: "config" };
|
|
1435
|
-
return { effective: false, origin: "default" };
|
|
1436
|
-
}
|
|
1437
|
-
function effortLine(state, effort) {
|
|
1438
|
-
if (!state.supportsEffort) return `Reasoning effort: n/a for ${state.provider}`;
|
|
1439
|
-
const resolved = effort ?? state.runtimeEffort ?? state.configuredEffort;
|
|
1440
|
-
return `Reasoning effort: ${resolved ?? "high (provider default)"}`;
|
|
1441
|
-
}
|
|
1442
|
-
function applyThinkCommand(sub, state) {
|
|
1443
|
-
const key = (sub ?? "toggle").toLowerCase();
|
|
1444
|
-
const current = resolveEffective(state);
|
|
1445
|
-
const stay = (summary, details = []) => ({
|
|
1446
|
-
runtimeThinking: state.runtimeThinking,
|
|
1447
|
-
runtimeEffort: state.runtimeEffort,
|
|
1448
|
-
effective: current.effective,
|
|
1449
|
-
origin: current.origin,
|
|
1450
|
-
summary,
|
|
1451
|
-
details,
|
|
1452
|
-
changed: false
|
|
1453
|
-
});
|
|
1454
|
-
const enable = (effort) => ({
|
|
1455
|
-
runtimeThinking: true,
|
|
1456
|
-
runtimeEffort: effort ?? state.runtimeEffort,
|
|
1457
|
-
effective: true,
|
|
1458
|
-
origin: "runtime override",
|
|
1459
|
-
summary: effort && state.supportsEffort ? `Extended Thinking: ON (effort: ${effort})` : "Extended Thinking: ON",
|
|
1460
|
-
details: effort && !state.supportsEffort ? [`Note: ${state.provider} has no reasoning-effort levels \u2014 the level is ignored.`] : [],
|
|
1461
|
-
changed: true
|
|
1462
|
-
});
|
|
1463
|
-
const disable = () => ({
|
|
1464
|
-
runtimeThinking: false,
|
|
1465
|
-
// 刻意保留档位:重新开启时不该丢掉用户选过的档
|
|
1466
|
-
runtimeEffort: state.runtimeEffort,
|
|
1467
|
-
effective: false,
|
|
1468
|
-
origin: "runtime override",
|
|
1469
|
-
summary: "Extended Thinking: OFF",
|
|
1470
|
-
details: [],
|
|
1471
|
-
changed: true
|
|
1472
|
-
});
|
|
1473
|
-
if (key === "status") {
|
|
1474
|
-
const details = current.effective ? [effortLine(state, null)] : [
|
|
1475
|
-
"Enable with /think on | low | high | max, or set",
|
|
1476
|
-
"modelParams.<model>.thinking in ~/.aicli/config.json"
|
|
1477
|
-
];
|
|
1478
|
-
return stay(
|
|
1479
|
-
`Extended Thinking: ${current.effective ? "ON" : "OFF"} (${current.origin})`,
|
|
1480
|
-
details
|
|
1481
|
-
);
|
|
1482
|
-
}
|
|
1483
|
-
if (key === "on") return enable(null);
|
|
1484
|
-
if (key === "off") return disable();
|
|
1485
|
-
if (EFFORTS.has(key)) return enable(key);
|
|
1486
|
-
if (key === "toggle") {
|
|
1487
|
-
return current.effective ? disable() : enable(null);
|
|
1488
|
-
}
|
|
1489
|
-
return stay(`Unknown /think option "${sub}" \u2014 expected on | off | low | high | max | status`);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
// src/core/provider-command.ts
|
|
1493
|
-
function preservedSuffix(messageCount) {
|
|
1494
|
-
return messageCount > 0 ? ` (conversation preserved: ${messageCount} messages)` : "";
|
|
1495
|
-
}
|
|
1496
|
-
function resolveProviderSwitch(input) {
|
|
1497
|
-
const id = input.targetId;
|
|
1498
|
-
if (!input.configured) {
|
|
1499
|
-
return {
|
|
1500
|
-
status: "not-configured",
|
|
1501
|
-
message: `Provider '${id}' is not configured. Run: aicli config`
|
|
1502
|
-
};
|
|
1503
|
-
}
|
|
1504
|
-
if (input.availableModels.length === 0) {
|
|
1505
|
-
return {
|
|
1506
|
-
status: "no-models",
|
|
1507
|
-
message: `Provider '${id}' has no available models. ` + (id === "ollama" ? "Make sure Ollama is running (`ollama serve`) and has models (`ollama pull <model>`)." : "Check provider configuration.")
|
|
1508
|
-
};
|
|
1509
|
-
}
|
|
1510
|
-
if (id === input.currentProvider) {
|
|
1511
|
-
return { status: "unchanged", message: `Already using provider: ${id}` };
|
|
1512
|
-
}
|
|
1513
|
-
const model = input.configuredDefaultModel || input.providerDefaultModel;
|
|
1514
|
-
return {
|
|
1515
|
-
status: "switched",
|
|
1516
|
-
providerId: id,
|
|
1517
|
-
model,
|
|
1518
|
-
message: `Switched to provider: ${id} (${model})` + preservedSuffix(input.messageCount)
|
|
1519
|
-
};
|
|
1520
|
-
}
|
|
1521
|
-
function parseModelCommand(args, currentProvider) {
|
|
1522
|
-
const raw = args[0];
|
|
1523
|
-
if (!raw) return { kind: "select" };
|
|
1524
|
-
const sub = raw.toLowerCase();
|
|
1525
|
-
if (sub === "refresh") return { kind: "refresh", providerId: args[1] ?? currentProvider };
|
|
1526
|
-
if (sub === "cache") {
|
|
1527
|
-
if ((args[1] ?? "").toLowerCase() === "clear") return { kind: "cache-clear", providerId: args[2] };
|
|
1528
|
-
return { kind: "cache-show" };
|
|
1529
|
-
}
|
|
1530
|
-
return { kind: "set", modelId: raw };
|
|
1531
|
-
}
|
|
1532
|
-
function resolveModelSwitch(input) {
|
|
1533
|
-
if (input.targetModel === input.currentModel) {
|
|
1534
|
-
return { status: "unchanged", message: `Already using model: ${input.targetModel}` };
|
|
1535
|
-
}
|
|
1536
|
-
const known = input.knownModels.includes(input.targetModel);
|
|
1537
|
-
return {
|
|
1538
|
-
status: "switched",
|
|
1539
|
-
model: input.targetModel,
|
|
1540
|
-
message: `Switched to model: ${input.targetModel}` + preservedSuffix(input.messageCount),
|
|
1541
|
-
warning: known ? void 0 : `'${input.targetModel}' is not in the known model list for ${input.provider}. If the provider rejects it, check the spelling or run /model refresh ${input.provider}.`
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1544
|
-
var PLAN_MODE_TOOL_HINT = [
|
|
1545
|
-
"read_file \xB7 list_dir \xB7 grep_files \xB7 glob_files",
|
|
1546
|
-
"web_fetch \xB7 google_search \xB7 ask_user \xB7 write_todos"
|
|
1547
|
-
];
|
|
1548
|
-
function applyPlanCommand(sub, active) {
|
|
1549
|
-
const key = (sub ?? "enter").toLowerCase() === "toggle" ? active ? "exit" : "enter" : (sub ?? "enter").toLowerCase();
|
|
1550
|
-
if (key === "enter") {
|
|
1551
|
-
if (active) {
|
|
1552
|
-
return {
|
|
1553
|
-
planMode: true,
|
|
1554
|
-
changed: false,
|
|
1555
|
-
status: "already-active",
|
|
1556
|
-
summary: "Already in Plan Mode. Use /plan execute to start executing, or /plan exit to leave.",
|
|
1557
|
-
details: []
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
return {
|
|
1561
|
-
planMode: true,
|
|
1562
|
-
changed: true,
|
|
1563
|
-
status: "entered",
|
|
1564
|
-
summary: "\u{1F4CB} Plan Mode activated",
|
|
1565
|
-
details: [
|
|
1566
|
-
"AI can now ONLY use read-only tools:",
|
|
1567
|
-
...PLAN_MODE_TOOL_HINT,
|
|
1568
|
-
"Describe your task and the AI will analyze the codebase and create a detailed implementation plan.",
|
|
1569
|
-
"When ready: /plan execute | To cancel: /plan exit"
|
|
1570
|
-
]
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
if (key === "execute") {
|
|
1574
|
-
if (!active) {
|
|
1575
|
-
return { planMode: false, changed: false, status: "not-active", summary: "Not in Plan Mode. Enter first with /plan", details: [] };
|
|
1576
|
-
}
|
|
1577
|
-
return {
|
|
1578
|
-
planMode: false,
|
|
1579
|
-
changed: true,
|
|
1580
|
-
status: "executing",
|
|
1581
|
-
summary: "Plan Mode ended \u2014 switching to execute mode. AI now has full tool access.",
|
|
1582
|
-
details: []
|
|
1583
|
-
};
|
|
1584
|
-
}
|
|
1585
|
-
if (key === "exit" || key === "cancel") {
|
|
1586
|
-
if (!active) {
|
|
1587
|
-
return { planMode: false, changed: false, status: "not-active", summary: "Not in Plan Mode.", details: [] };
|
|
1588
|
-
}
|
|
1589
|
-
return { planMode: false, changed: true, status: "cancelled", summary: "Plan Mode cancelled. Returning to normal mode.", details: [] };
|
|
1590
|
-
}
|
|
1591
|
-
if (key === "status" || key === "show") {
|
|
1592
|
-
return {
|
|
1593
|
-
planMode: active,
|
|
1594
|
-
changed: false,
|
|
1595
|
-
status: "status",
|
|
1596
|
-
summary: `Plan Mode: ${active ? "ACTIVE \u{1F4CB}" : "inactive"}`,
|
|
1597
|
-
details: active ? ["Tools restricted to read-only set.", "Use /plan execute to start executing, or /plan exit to cancel."] : []
|
|
1598
|
-
};
|
|
1599
|
-
}
|
|
1600
|
-
return {
|
|
1601
|
-
planMode: active,
|
|
1602
|
-
changed: false,
|
|
1603
|
-
status: "unknown",
|
|
1604
|
-
summary: `Unknown /plan option "${sub}" \u2014 expected execute | exit | status`,
|
|
1605
|
-
details: []
|
|
1606
|
-
};
|
|
1607
|
-
}
|
|
1608
|
-
function parseRouteCommand(args) {
|
|
1609
|
-
const sub = (args[0] ?? "show").toLowerCase();
|
|
1610
|
-
if (sub === "on" || sub === "enable") return { kind: "enable" };
|
|
1611
|
-
if (sub === "off" || sub === "disable") return { kind: "disable" };
|
|
1612
|
-
if (sub === "test") {
|
|
1613
|
-
const message = args.slice(1).join(" ").trim();
|
|
1614
|
-
return message ? { kind: "test", message } : { kind: "usage", message: "Usage: /route test <message>" };
|
|
1615
|
-
}
|
|
1616
|
-
if (sub === "show" || sub === "status") return { kind: "show" };
|
|
1617
|
-
return { kind: "unknown", message: `Unknown subcommand: ${sub}. Usage: /route [on|off|show|test <message>]` };
|
|
1618
|
-
}
|
|
1619
|
-
function routingEmptyRulesNote(routing) {
|
|
1620
|
-
if (routing && routing.rules.length > 0) return null;
|
|
1621
|
-
return 'No rules configured yet. Add rules under `routing.rules` in ~/.aicli/config.json.\nExample: { match: { tag: "fast" }, model: "claude-haiku-4-5" }';
|
|
1622
|
-
}
|
|
1623
|
-
function describeRoutingRule(rule, index) {
|
|
1624
|
-
const parts = [];
|
|
1625
|
-
if (rule.match.tag) parts.push(`tag=#${rule.match.tag}`);
|
|
1626
|
-
if (rule.match.contains && rule.match.contains.length > 0) {
|
|
1627
|
-
const shown = rule.match.contains.slice(0, 3).join(", ");
|
|
1628
|
-
parts.push(`contains=[${shown}${rule.match.contains.length > 3 ? ", \u2026" : ""}]`);
|
|
1629
|
-
}
|
|
1630
|
-
if (typeof rule.match.maxLength === "number") parts.push(`maxLen=${rule.match.maxLength}`);
|
|
1631
|
-
if (typeof rule.match.minLength === "number") parts.push(`minLen=${rule.match.minLength}`);
|
|
1632
|
-
const empty = parts.length === 0;
|
|
1633
|
-
return {
|
|
1634
|
-
index,
|
|
1635
|
-
name: rule.name,
|
|
1636
|
-
condition: empty ? "(empty \u2014 never matches)" : parts.join(" & "),
|
|
1637
|
-
model: rule.model,
|
|
1638
|
-
empty
|
|
1639
|
-
};
|
|
1640
|
-
}
|
|
1641
|
-
function describeRouting(input) {
|
|
1642
|
-
const rules = input.routing?.rules ?? [];
|
|
1643
|
-
return {
|
|
1644
|
-
enabled: input.routing?.enabled === true,
|
|
1645
|
-
provider: input.provider,
|
|
1646
|
-
currentModel: input.currentModel,
|
|
1647
|
-
fallback: input.routing?.fallback,
|
|
1648
|
-
rules: rules.map((r, i) => describeRoutingRule(r, i)),
|
|
1649
|
-
noRulesNote: rules.length === 0 ? "(no rules configured \u2014 edit ~/.aicli/config.json `routing.rules`)" : null,
|
|
1650
|
-
hint: "Commands: /route on | off | test <msg> | show"
|
|
1651
|
-
};
|
|
1652
|
-
}
|
|
1653
|
-
function routeDecisionRows(message, currentModel, decision) {
|
|
1654
|
-
const rows = [
|
|
1655
|
-
{ label: "Input", value: message },
|
|
1656
|
-
{ label: "Current", value: currentModel },
|
|
1657
|
-
{ label: "Decision", value: `${decision.model} ${decision.overridden ? "\u2192 ROUTED" : "(unchanged)"}` },
|
|
1658
|
-
{ label: "Reason", value: decision.reason }
|
|
1659
|
-
];
|
|
1660
|
-
if (typeof decision.ruleIdx === "number") rows.push({ label: "Rule", value: `#${decision.ruleIdx}` });
|
|
1661
|
-
return rows;
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
// src/core/session-command.ts
|
|
1665
|
-
var DEFAULT_SESSION_LIST_LIMIT = 50;
|
|
1666
|
-
function parseSessionListArgs(args) {
|
|
1667
|
-
let limit = DEFAULT_SESSION_LIST_LIMIT;
|
|
1668
|
-
let all = false;
|
|
1669
|
-
const filter = {};
|
|
1670
|
-
const warnings = [];
|
|
1671
|
-
let search = null;
|
|
1672
|
-
let days = null;
|
|
1673
|
-
let since = null;
|
|
1674
|
-
let sort = { field: "updated", order: "desc" };
|
|
1675
|
-
let json = false;
|
|
1676
|
-
let stale = null;
|
|
1677
|
-
let verbose = false;
|
|
1678
|
-
for (let i = 1; i < args.length; i++) {
|
|
1679
|
-
const a = args[i];
|
|
1680
|
-
if (a === "--all" || a === "-a") {
|
|
1681
|
-
all = true;
|
|
1682
|
-
} else if ((a === "--limit" || a === "-n") && i + 1 < args.length) {
|
|
1683
|
-
const n = Number(args[i + 1]);
|
|
1684
|
-
if (Number.isFinite(n) && n > 0) {
|
|
1685
|
-
limit = n;
|
|
1686
|
-
i++;
|
|
1687
|
-
}
|
|
1688
|
-
} else if (a === "--filter" && i + 1 < args.length) {
|
|
1689
|
-
const val = args[++i];
|
|
1690
|
-
const eq = val.indexOf("=");
|
|
1691
|
-
if (eq > 0) {
|
|
1692
|
-
const key = val.slice(0, eq);
|
|
1693
|
-
const vals = val.slice(eq + 1).split(",").map((s) => s.trim().toLowerCase());
|
|
1694
|
-
if (key === "provider") filter.provider = vals;
|
|
1695
|
-
else if (key === "model") filter.model = vals;
|
|
1696
|
-
else if (key === "messages") {
|
|
1697
|
-
const n = Number(vals[0]);
|
|
1698
|
-
if (Number.isFinite(n) && n >= 0) filter.messages = n;
|
|
1699
|
-
else warnings.push(`Invalid filter value for "messages": expected a non-negative integer, got "${vals[0]}"`);
|
|
1700
|
-
} else {
|
|
1701
|
-
warnings.push(`Unknown filter key: "${key}". Supported keys: provider, model, messages`);
|
|
1702
|
-
}
|
|
1703
|
-
} else {
|
|
1704
|
-
warnings.push(`Invalid --filter syntax: "${val}". Expected key=value`);
|
|
1705
|
-
}
|
|
1706
|
-
} else if ((a === "--search" || a === "-s") && i + 1 < args.length) {
|
|
1707
|
-
search = args[++i];
|
|
1708
|
-
} else if (a === "--days" && i + 1 < args.length) {
|
|
1709
|
-
const n = Number(args[++i]);
|
|
1710
|
-
if (Number.isFinite(n) && n > 0) days = n;
|
|
1711
|
-
} else if (a === "--since" && i + 1 < args.length) {
|
|
1712
|
-
since = args[++i];
|
|
1713
|
-
} else if (a === "--sort" && i + 1 < args.length) {
|
|
1714
|
-
const sv = args[++i];
|
|
1715
|
-
const colon = sv.indexOf(":");
|
|
1716
|
-
const field = colon > 0 ? sv.slice(0, colon) : sv;
|
|
1717
|
-
const order = colon > 0 ? sv.slice(colon + 1) : "desc";
|
|
1718
|
-
if (field === "updated" || field === "created" || field === "messages" || field === "size") {
|
|
1719
|
-
sort = { field, order: order === "asc" ? "asc" : "desc" };
|
|
1720
|
-
}
|
|
1721
|
-
} else if (a === "--json") {
|
|
1722
|
-
json = true;
|
|
1723
|
-
} else if (a === "--verbose" || a === "-v") {
|
|
1724
|
-
verbose = true;
|
|
1725
|
-
} else if (a === "--stale" && i + 1 < args.length) {
|
|
1726
|
-
const n = Number(args[++i]);
|
|
1727
|
-
if (Number.isFinite(n) && n > 0) stale = n;
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
if (all) limit = -1;
|
|
1731
|
-
return { limit, all, filter, search, days, since, sort, json, stale, verbose, warnings };
|
|
1732
|
-
}
|
|
1733
|
-
function sessionListNeedsFileSize(opts) {
|
|
1734
|
-
return opts.verbose || opts.sort.field === "size";
|
|
1735
|
-
}
|
|
1736
|
-
function selectSessions(sessions, opts, now = Date.now()) {
|
|
1737
|
-
let matched = sessions;
|
|
1738
|
-
const hasFilters = !!opts.filter.provider || !!opts.filter.model || opts.filter.messages !== void 0;
|
|
1739
|
-
const hasSearch = !!opts.search;
|
|
1740
|
-
const hasTimeFilter = opts.days !== null || opts.since !== null;
|
|
1741
|
-
const hasStale = opts.stale !== null;
|
|
1742
|
-
if (hasFilters) {
|
|
1743
|
-
const p = opts.filter.provider;
|
|
1744
|
-
const m = opts.filter.model;
|
|
1745
|
-
const msg = opts.filter.messages;
|
|
1746
|
-
matched = matched.filter((s) => {
|
|
1747
|
-
if (p && !p.includes(s.provider.toLowerCase())) return false;
|
|
1748
|
-
if (m && !m.some((kw) => s.model.toLowerCase().includes(kw))) return false;
|
|
1749
|
-
if (msg !== void 0 && s.messageCount !== msg) return false;
|
|
1750
|
-
return true;
|
|
1751
|
-
});
|
|
1752
|
-
}
|
|
1753
|
-
if (hasSearch) {
|
|
1754
|
-
const q = opts.search.toLowerCase();
|
|
1755
|
-
matched = matched.filter(
|
|
1756
|
-
(s) => (s.title ?? "").toLowerCase().includes(q) || s.id.toLowerCase().includes(q)
|
|
1757
|
-
);
|
|
1758
|
-
}
|
|
1759
|
-
if (hasTimeFilter) {
|
|
1760
|
-
let cutoff = 0;
|
|
1761
|
-
if (opts.since) cutoff = new Date(opts.since).getTime();
|
|
1762
|
-
else if (opts.days !== null) cutoff = now - opts.days * 24 * 60 * 60 * 1e3;
|
|
1763
|
-
if (!Number.isNaN(cutoff)) {
|
|
1764
|
-
matched = matched.filter((s) => s.updated.getTime() >= cutoff);
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
const staleCutoff = hasStale ? now - opts.stale * 24 * 60 * 60 * 1e3 : null;
|
|
1768
|
-
if (staleCutoff !== null) {
|
|
1769
|
-
matched = matched.filter((s) => s.updated.getTime() < staleCutoff);
|
|
1770
|
-
}
|
|
1771
|
-
const defaultSort = opts.sort.field === "updated" && opts.sort.order === "desc";
|
|
1772
|
-
if (!defaultSort) {
|
|
1773
|
-
const mul = opts.sort.order === "asc" ? 1 : -1;
|
|
1774
|
-
matched = [...matched].sort((a, b) => {
|
|
1775
|
-
switch (opts.sort.field) {
|
|
1776
|
-
case "created":
|
|
1777
|
-
return (a.created.getTime() - b.created.getTime()) * mul;
|
|
1778
|
-
case "messages":
|
|
1779
|
-
return (a.messageCount - b.messageCount) * mul;
|
|
1780
|
-
case "size":
|
|
1781
|
-
return ((a.fileSize ?? 0) - (b.fileSize ?? 0)) * mul;
|
|
1782
|
-
default:
|
|
1783
|
-
return (a.updated.getTime() - b.updated.getTime()) * mul;
|
|
1784
|
-
}
|
|
1785
|
-
});
|
|
1786
|
-
}
|
|
1787
|
-
const filterTags = [];
|
|
1788
|
-
if (hasFilters) filterTags.push("filtered");
|
|
1789
|
-
if (hasSearch) filterTags.push("searched");
|
|
1790
|
-
if (hasTimeFilter) filterTags.push("time-limited");
|
|
1791
|
-
if (hasStale) filterTags.push(`stale >${opts.stale}d`);
|
|
1792
|
-
return {
|
|
1793
|
-
matched,
|
|
1794
|
-
shown: opts.limit > 0 ? matched.slice(0, opts.limit) : matched,
|
|
1795
|
-
totalCount: matched.length,
|
|
1796
|
-
totalMessages: matched.reduce((sum, s) => sum + s.messageCount, 0),
|
|
1797
|
-
staleCutoff,
|
|
1798
|
-
filterTags,
|
|
1799
|
-
needsFileSize: sessionListNeedsFileSize(opts)
|
|
1800
|
-
};
|
|
1801
|
-
}
|
|
1802
|
-
function sessionListSummary(view) {
|
|
1803
|
-
const tag = view.filterTags.length > 0 ? ` (${view.filterTags.join(", ")})` : "";
|
|
1804
|
-
return `${view.shown.length} of ${view.totalCount} sessions${tag} \xB7 ${view.totalMessages} messages total`;
|
|
1805
|
-
}
|
|
1806
|
-
function sessionListJsonRow(s, includeFileSize) {
|
|
1807
|
-
return {
|
|
1808
|
-
id: s.id,
|
|
1809
|
-
title: s.title ?? null,
|
|
1810
|
-
provider: s.provider,
|
|
1811
|
-
model: s.model,
|
|
1812
|
-
messageCount: s.messageCount,
|
|
1813
|
-
created: s.created.toISOString(),
|
|
1814
|
-
updated: s.updated.toISOString(),
|
|
1815
|
-
...includeFileSize ? { fileSize: s.fileSize } : {},
|
|
1816
|
-
...s.branchCount !== void 0 ? { branchCount: s.branchCount, activeBranch: s.activeBranch } : {}
|
|
1817
|
-
};
|
|
1818
|
-
}
|
|
1819
|
-
function resolveSessionRef(sessions, prefix, policy) {
|
|
1820
|
-
const matches2 = sessions.filter((s) => s.id.startsWith(prefix));
|
|
1821
|
-
if (matches2.length === 0) {
|
|
1822
|
-
return { status: "not-found", message: `Session '${prefix}' not found.` };
|
|
1823
|
-
}
|
|
1824
|
-
if (matches2.length > 1 && policy === "strict") {
|
|
1825
|
-
return {
|
|
1826
|
-
status: "ambiguous",
|
|
1827
|
-
matches: matches2,
|
|
1828
|
-
message: `Ambiguous prefix '${prefix}' matches ${matches2.length} sessions \u2014 be more specific:`
|
|
1829
|
-
};
|
|
1830
|
-
}
|
|
1831
|
-
return {
|
|
1832
|
-
status: "ok",
|
|
1833
|
-
match: matches2[0],
|
|
1834
|
-
ambiguousWith: matches2.length > 1 ? matches2 : [],
|
|
1835
|
-
message: matches2.length > 1 ? `\u26A0 Ambiguous prefix '${prefix}' matches ${matches2.length} sessions \u2014 loading most recent:` : void 0
|
|
1836
|
-
};
|
|
1837
|
-
}
|
|
1838
|
-
function formatSessionSummaryLine(s, titleWidth = 60) {
|
|
1839
|
-
const title = s.title ?? "(untitled)";
|
|
1840
|
-
const shown = title.length > titleWidth ? `${title.slice(0, titleWidth - 1)}\u2026` : title;
|
|
1841
|
-
return `${s.id.slice(0, 12)} ${s.provider}/${s.model} ${String(s.messageCount).padStart(4)} msgs ${s.updated.toISOString().slice(0, 10)} ${shown}`;
|
|
1842
|
-
}
|
|
1843
|
-
function parseBatchDeleteArgs(args) {
|
|
1844
|
-
let messages;
|
|
1845
|
-
let dryRun = false;
|
|
1846
|
-
for (let i = 1; i < args.length; i++) {
|
|
1847
|
-
if (args[i] === "--filter" && i + 1 < args.length) {
|
|
1848
|
-
const val = args[++i];
|
|
1849
|
-
const eq = val.indexOf("=");
|
|
1850
|
-
if (eq <= 0) {
|
|
1851
|
-
return { dryRun, error: `Invalid --filter syntax: "${val}". Expected key=value (e.g., messages=0)` };
|
|
1852
|
-
}
|
|
1853
|
-
const key = val.slice(0, eq);
|
|
1854
|
-
const vals = val.slice(eq + 1).split(",").map((s) => s.trim().toLowerCase());
|
|
1855
|
-
if (key !== "messages") {
|
|
1856
|
-
return { dryRun, error: `Unsupported filter key for delete: "${key}". Only "messages" is supported.` };
|
|
1857
|
-
}
|
|
1858
|
-
const n = Number(vals[0]);
|
|
1859
|
-
if (!Number.isFinite(n) || n < 0) {
|
|
1860
|
-
return { dryRun, error: `Invalid filter value for "messages": expected a non-negative integer, got "${vals[0]}"` };
|
|
1861
|
-
}
|
|
1862
|
-
messages = n;
|
|
1863
|
-
} else if (args[i] === "--dry-run") {
|
|
1864
|
-
dryRun = true;
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
if (messages === void 0) {
|
|
1868
|
-
return { dryRun, error: "Usage: /session delete --filter messages=N [--dry-run]" };
|
|
1869
|
-
}
|
|
1870
|
-
return { messages, dryRun };
|
|
1871
|
-
}
|
|
1872
|
-
function parseCleanArgs(args) {
|
|
1873
|
-
let staleDays = 90;
|
|
1874
|
-
let dryRun = false;
|
|
1875
|
-
for (let i = 1; i < args.length; i++) {
|
|
1876
|
-
if (args[i] === "--stale" && i + 1 < args.length) {
|
|
1877
|
-
const n = Number(args[++i]);
|
|
1878
|
-
if (Number.isFinite(n) && n > 0) staleDays = n;
|
|
1879
|
-
} else if (args[i] === "--dry-run") {
|
|
1880
|
-
dryRun = true;
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
return { staleDays, dryRun };
|
|
1884
|
-
}
|
|
1885
|
-
function selectStaleSessions(sessions, staleDays, now = Date.now()) {
|
|
1886
|
-
const cutoff = now - staleDays * 24 * 60 * 60 * 1e3;
|
|
1887
|
-
return sessions.filter((s) => s.updated.getTime() < cutoff);
|
|
1888
|
-
}
|
|
1889
|
-
function clearedSessionMessage(priorSessionId, priorMessageCount = 0) {
|
|
1890
|
-
return priorSessionId && priorMessageCount > 0 ? `Started a new session. Previous conversation saved as ${priorSessionId.slice(0, 8)} (${priorMessageCount} messages) \u2014 reload it with /session load ${priorSessionId.slice(0, 8)}.` : "Started a new session.";
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
// src/core/agent-command.ts
|
|
1894
|
-
var AGENT_USAGE = "Usage: /agent list|switch <name>|stop <id|all>|summary";
|
|
1895
|
-
function parseAgentCommand(args) {
|
|
1896
|
-
const sub = (args[0] ?? "summary").toLowerCase();
|
|
1897
|
-
switch (sub) {
|
|
1898
|
-
case "list":
|
|
1899
|
-
return { kind: "list" };
|
|
1900
|
-
case "switch": {
|
|
1901
|
-
const name = args[1];
|
|
1902
|
-
return name ? { kind: "switch", name } : { kind: "usage", message: "Usage: /agent switch <name>" };
|
|
1903
|
-
}
|
|
1904
|
-
case "stop":
|
|
1905
|
-
return { kind: "stop", target: args[1] ?? "all" };
|
|
1906
|
-
case "summary":
|
|
1907
|
-
return { kind: "summary" };
|
|
1908
|
-
default:
|
|
1909
|
-
return { kind: "usage", message: AGENT_USAGE };
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
function formatAgentList(agents, preferred) {
|
|
1913
|
-
const lines = [`Agents (${agents.length})`, ""];
|
|
1914
|
-
for (const agent of agents) {
|
|
1915
|
-
const active = agent.name === preferred ? " *" : " ";
|
|
1916
|
-
const model = [agent.provider, agent.model].filter(Boolean).join("/") || "inherit";
|
|
1917
|
-
lines.push(`${active} ${agent.name} [${agent.source ?? "builtin"}] \u2014 ${agent.description ?? ""}`);
|
|
1918
|
-
lines.push(
|
|
1919
|
-
` model: ${model} \xB7 permission: ${agent.permissionProfile ?? "workspace-write"} \xB7 maxRounds: ${agent.maxToolRounds ?? "inherit"}`
|
|
1920
|
-
);
|
|
1921
|
-
}
|
|
1922
|
-
lines.push("", "Config dirs: ~/.aicli/agents/ and .aicli/agents/");
|
|
1923
|
-
return lines;
|
|
1924
|
-
}
|
|
1925
|
-
function formatAgentRuns(runs) {
|
|
1926
|
-
if (runs.length === 0) return ["No sub-agent runs yet."];
|
|
1927
|
-
const lines = [`Recent agent runs (${runs.length})`, ""];
|
|
1928
|
-
for (const run of runs.slice(0, 20)) {
|
|
1929
|
-
lines.push(`${run.id} ${run.status} ${run.agentName} ${run.startedAt}`);
|
|
1930
|
-
lines.push(` task: ${run.task.slice(0, 120)}${run.task.length > 120 ? "..." : ""}`);
|
|
1931
|
-
if (run.toolNames.length > 0) lines.push(` tools: ${run.toolNames.join(", ")}`);
|
|
1932
|
-
if (run.summary) lines.push(` summary: ${run.summary.slice(0, 180)}${run.summary.length > 180 ? "..." : ""}`);
|
|
1933
|
-
if (run.error) lines.push(` error: ${run.error}`);
|
|
1934
|
-
}
|
|
1935
|
-
return lines;
|
|
1936
|
-
}
|
|
1937
|
-
var agentStopMessage = (count) => count > 0 ? `Stop requested for ${count} running agent(s).` : "No matching running agents.";
|
|
1938
|
-
var agentUnknownMessage = (name, available) => `Unknown agent: ${name}
|
|
1939
|
-
Available: ${available.join(", ")}`;
|
|
1940
|
-
function parseSkillCommand(args) {
|
|
1941
|
-
const sub = (args[0] ?? "list").toLowerCase();
|
|
1942
|
-
if (!args[0] || sub === "list") return { kind: "list" };
|
|
1943
|
-
if (sub === "reload") return { kind: "reload" };
|
|
1944
|
-
if (sub === "off" || sub === "none" || sub === "deactivate") return { kind: "deactivate" };
|
|
1945
|
-
return { kind: "activate", name: args.join(" ").trim() };
|
|
1946
|
-
}
|
|
1947
|
-
function skillHistoryNote(fromLabel, toLabel, messageCount) {
|
|
1948
|
-
if (messageCount <= 0) return null;
|
|
1949
|
-
return `Switching skill: ${fromLabel} \u2192 ${toLabel} with ${messageCount} message(s) still in context. The previous skill's turns can bias the new one \u2014 use /clear to start a fresh session, or /branch new 0 <title> to fork before continuing.`;
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
// src/core/profile-command.ts
|
|
1953
|
-
var PROFILE_FIELDS = ["name", "nickname", "role", "bio", "interests", "locale", "extra"];
|
|
1954
|
-
function stripSurroundingQuotes(value) {
|
|
1955
|
-
const pairs = [['"', '"'], ["'", "'"], ["\u201C", "\u201D"], ["\u2018", "\u2019"], ["\u300C", "\u300D"]];
|
|
1956
|
-
for (const [open, close] of pairs) {
|
|
1957
|
-
if (value.length >= 2 && value.startsWith(open) && value.endsWith(close)) {
|
|
1958
|
-
return value.slice(open.length, value.length - close.length).trim();
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
return value;
|
|
1962
|
-
}
|
|
1963
|
-
function showRows(profile) {
|
|
1964
|
-
const rows = [];
|
|
1965
|
-
if (profile.name) rows.push(["Name", profile.name]);
|
|
1966
|
-
if (profile.nickname) rows.push(["Nickname", profile.nickname]);
|
|
1967
|
-
if (profile.role) rows.push(["Role", profile.role]);
|
|
1968
|
-
if (profile.bio) rows.push(["Bio", profile.bio]);
|
|
1969
|
-
if (profile.interests?.length) rows.push(["Interests", profile.interests.join(", ")]);
|
|
1970
|
-
if (profile.locale) rows.push(["Locale", profile.locale]);
|
|
1971
|
-
if (profile.extra) {
|
|
1972
|
-
rows.push(["Extra", profile.extra.length > 80 ? `${profile.extra.slice(0, 80)}\u2026` : profile.extra]);
|
|
1973
|
-
}
|
|
1974
|
-
return rows;
|
|
1975
|
-
}
|
|
1976
|
-
function applyProfileCommand(args, profile) {
|
|
1977
|
-
const sub = (args[0] ?? "show").toLowerCase();
|
|
1978
|
-
if (sub === "show" || sub === "view") {
|
|
1979
|
-
const rows = showRows(profile);
|
|
1980
|
-
if (rows.length === 0) {
|
|
1981
|
-
return {
|
|
1982
|
-
summary: "No profile configured yet.",
|
|
1983
|
-
rows: [],
|
|
1984
|
-
details: ["Set one with /profile set <field> <value> \u2014 it is injected into every conversation."],
|
|
1985
|
-
changed: false,
|
|
1986
|
-
isError: false
|
|
1987
|
-
};
|
|
1988
|
-
}
|
|
1989
|
-
return {
|
|
1990
|
-
summary: "\u{1F464} Your Profile",
|
|
1991
|
-
rows,
|
|
1992
|
-
details: ["This identity is injected into every AI conversation."],
|
|
1993
|
-
changed: false,
|
|
1994
|
-
isError: false
|
|
1995
|
-
};
|
|
1996
|
-
}
|
|
1997
|
-
if (sub === "clear") {
|
|
1998
|
-
return {
|
|
1999
|
-
profile: {},
|
|
2000
|
-
summary: "Profile cleared. The AI will no longer know who you are.",
|
|
2001
|
-
rows: [],
|
|
2002
|
-
details: [],
|
|
2003
|
-
changed: true,
|
|
2004
|
-
isError: false
|
|
2005
|
-
};
|
|
2006
|
-
}
|
|
2007
|
-
if (sub === "set") {
|
|
2008
|
-
const field = args[1];
|
|
2009
|
-
const value = args.slice(2).join(" ").trim();
|
|
2010
|
-
if (!field || !PROFILE_FIELDS.includes(field)) {
|
|
2011
|
-
return {
|
|
2012
|
-
summary: "Usage: /profile set <field> <value>",
|
|
2013
|
-
rows: [],
|
|
2014
|
-
details: [`Fields: ${PROFILE_FIELDS.join(", ")}`],
|
|
2015
|
-
changed: false,
|
|
2016
|
-
isError: true
|
|
2017
|
-
};
|
|
2018
|
-
}
|
|
2019
|
-
if (!value) {
|
|
2020
|
-
return {
|
|
2021
|
-
summary: `Usage: /profile set ${field} <value>`,
|
|
2022
|
-
rows: [],
|
|
2023
|
-
details: [],
|
|
2024
|
-
changed: false,
|
|
2025
|
-
isError: true
|
|
2026
|
-
};
|
|
2027
|
-
}
|
|
2028
|
-
const updated = { ...profile };
|
|
2029
|
-
if (field === "interests") {
|
|
2030
|
-
updated.interests = value.split(",").map((s) => stripSurroundingQuotes(s.trim())).filter(Boolean);
|
|
2031
|
-
} else {
|
|
2032
|
-
updated[field] = stripSurroundingQuotes(value);
|
|
2033
|
-
}
|
|
2034
|
-
const shown = field === "interests" ? (updated.interests ?? []).join(", ") : stripSurroundingQuotes(value);
|
|
2035
|
-
return {
|
|
2036
|
-
profile: updated,
|
|
2037
|
-
summary: `Profile updated: ${field} = ${shown}`,
|
|
2038
|
-
rows: [],
|
|
2039
|
-
details: [],
|
|
2040
|
-
changed: true,
|
|
2041
|
-
isError: false
|
|
2042
|
-
};
|
|
2043
|
-
}
|
|
2044
|
-
if (sub === "edit") {
|
|
2045
|
-
return {
|
|
2046
|
-
summary: "Interactive editing lives in the setup wizard.",
|
|
2047
|
-
rows: [],
|
|
2048
|
-
details: [
|
|
2049
|
-
"Run `aicli config` in a terminal, or use /profile set <field> <value> here.",
|
|
2050
|
-
`Fields: ${PROFILE_FIELDS.join(", ")}`
|
|
2051
|
-
],
|
|
2052
|
-
changed: false,
|
|
2053
|
-
isError: false
|
|
2054
|
-
};
|
|
2055
|
-
}
|
|
2056
|
-
return {
|
|
2057
|
-
summary: `Unknown /profile option "${args[0]}" \u2014 expected show | set | clear | edit`,
|
|
2058
|
-
rows: [],
|
|
2059
|
-
details: [],
|
|
2060
|
-
changed: false,
|
|
2061
|
-
isError: true
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
// src/core/git-diff.ts
|
|
2066
|
-
import { execFileSync } from "child_process";
|
|
2067
|
-
function readGitDiff(options = {}) {
|
|
2068
|
-
const args = ["diff"];
|
|
2069
|
-
if (options.staged) args.push("--staged");
|
|
2070
|
-
return execFileSync("git", args, {
|
|
2071
|
-
encoding: "utf-8",
|
|
2072
|
-
timeout: options.timeoutMs ?? 1e4
|
|
2073
|
-
}).trim();
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
// src/core/session-history.ts
|
|
2077
|
-
function parseRewindArgs(args, messageCount) {
|
|
2078
|
-
const sub = args[0];
|
|
2079
|
-
if (!sub || sub.toLowerCase() === "list") return { kind: "list" };
|
|
2080
|
-
const n = Number.parseInt(sub, 10);
|
|
2081
|
-
if (Number.isNaN(n) || n < 1 || n > messageCount) {
|
|
2082
|
-
return { kind: "invalid", message: `Invalid message number: ${sub}. Range: 1-${messageCount}` };
|
|
2083
|
-
}
|
|
2084
|
-
return { kind: "rewind", target: n };
|
|
2085
|
-
}
|
|
2086
|
-
function buildRewindList(messages, checkpointIndices, previewWidth = 70) {
|
|
2087
|
-
return messages.map((m, i) => ({
|
|
2088
|
-
index: i + 1,
|
|
2089
|
-
role: m.role,
|
|
2090
|
-
preview: getContentText(m.content).replace(/\s+/g, " ").trim().slice(0, previewWidth),
|
|
2091
|
-
hasCheckpoint: checkpointIndices.includes(i)
|
|
2092
|
-
}));
|
|
2093
|
-
}
|
|
2094
|
-
function formatRewindReport(report) {
|
|
2095
|
-
const lines = [
|
|
2096
|
-
`\u2713 Rewound to message ${report.target}`,
|
|
2097
|
-
` Messages removed: ${report.messagesRemoved}`
|
|
2098
|
-
];
|
|
2099
|
-
if (report.filesRestored > 0 || report.filesDeleted > 0) {
|
|
2100
|
-
lines.push(` Files restored: ${report.filesRestored}, files deleted: ${report.filesDeleted}`);
|
|
2101
|
-
for (const f of report.files) lines.push(` ${f}`);
|
|
2102
|
-
} else {
|
|
2103
|
-
lines.push(" No file changes to revert.");
|
|
2104
|
-
}
|
|
2105
|
-
return lines;
|
|
2106
|
-
}
|
|
2107
|
-
function resolveForkPoint(session, checkpointName) {
|
|
2108
|
-
const name = checkpointName?.trim();
|
|
2109
|
-
if (!name) {
|
|
2110
|
-
return { ok: true, messageCount: session.messages.length, fromLabel: "current position" };
|
|
2111
|
-
}
|
|
2112
|
-
const cp = session.checkpoints.find((c) => c.name === name);
|
|
2113
|
-
if (!cp) {
|
|
2114
|
-
const available = session.checkpoints.map((c) => c.name);
|
|
2115
|
-
return {
|
|
2116
|
-
ok: false,
|
|
2117
|
-
message: available.length > 0 ? `Checkpoint "${name}" not found. Available: ${available.join(", ")}` : `Checkpoint "${name}" not found. No checkpoints saved. Use /checkpoint save <name> first.`
|
|
2118
|
-
};
|
|
2119
|
-
}
|
|
2120
|
-
return {
|
|
2121
|
-
ok: true,
|
|
2122
|
-
messageCount: cp.messageIndex,
|
|
2123
|
-
fromLabel: `checkpoint "${cp.name}" (${cp.messageIndex} messages)`
|
|
2124
|
-
};
|
|
2125
|
-
}
|
|
2126
|
-
function formatForkReport(report) {
|
|
2127
|
-
return [
|
|
2128
|
-
"Session Forked",
|
|
2129
|
-
` Original: ${report.originalId.slice(0, 8)} "${report.originalTitle}"`,
|
|
2130
|
-
` Forked: ${report.forkedId.slice(0, 8)} "${report.forkedTitle}"`,
|
|
2131
|
-
` From: ${report.fromLabel}`,
|
|
2132
|
-
` Messages: ${report.messageCount} copied, ${report.checkpointCount} checkpoint(s) preserved`,
|
|
2133
|
-
` Use /session load ${report.originalId.slice(0, 8)} to switch back to the original.`
|
|
2134
|
-
];
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
// src/core/memory-command.ts
|
|
2138
|
-
var MEMORY_USAGE = "Governed persistent memory: show [--pending|--orphaned|--stale <days>|--scope <s>|--search <text>] | add | edit <id> <text> | approve/reject/delete/expire <id> | rebind <id> <path> | clear | backup [--out <file>] | restore <file> | export [--format json|md] [--out <file>] | path\nChat memory index (cross-session recall): rebuild | refresh | status | recall <query> | index-clear";
|
|
2139
|
-
function parseMemoryCommand(args) {
|
|
2140
|
-
const sub = (args[0] ?? "show").toLowerCase();
|
|
2141
|
-
const rest = args.slice(1);
|
|
2142
|
-
switch (sub) {
|
|
2143
|
-
case "rebuild":
|
|
2144
|
-
return { kind: "rebuild", full: true };
|
|
2145
|
-
case "refresh":
|
|
2146
|
-
return { kind: "rebuild", full: false };
|
|
2147
|
-
case "status":
|
|
2148
|
-
return { kind: "status" };
|
|
2149
|
-
case "recall": {
|
|
2150
|
-
const query = rest.join(" ").trim();
|
|
2151
|
-
return query ? { kind: "recall", query } : { kind: "usage", message: "Usage: /memory recall <query>" };
|
|
2152
|
-
}
|
|
2153
|
-
case "index-clear":
|
|
2154
|
-
return { kind: "index-clear" };
|
|
2155
|
-
case "show":
|
|
2156
|
-
case "view":
|
|
2157
|
-
case "list":
|
|
2158
|
-
return { kind: "show", rest };
|
|
2159
|
-
case "add":
|
|
2160
|
-
return { kind: "add", rest };
|
|
2161
|
-
case "edit":
|
|
2162
|
-
return { kind: "edit", rest };
|
|
2163
|
-
case "approve":
|
|
2164
|
-
case "reject":
|
|
2165
|
-
case "delete":
|
|
2166
|
-
case "expire":
|
|
2167
|
-
return { kind: "manage", action: sub, rest };
|
|
2168
|
-
case "rebind":
|
|
2169
|
-
return { kind: "rebind", rest };
|
|
2170
|
-
case "export":
|
|
2171
|
-
return { kind: "export", rest };
|
|
2172
|
-
case "clear":
|
|
2173
|
-
return { kind: "clear", rest };
|
|
2174
|
-
case "backup":
|
|
2175
|
-
return { kind: "backup", rest };
|
|
2176
|
-
case "restore":
|
|
2177
|
-
return { kind: "restore", rest };
|
|
2178
|
-
case "path":
|
|
2179
|
-
return { kind: "path" };
|
|
2180
|
-
default:
|
|
2181
|
-
return { kind: "usage", message: `Unknown /memory subcommand "${args[0]}".
|
|
2182
|
-
${MEMORY_USAGE}` };
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
var mb = (n) => `${(n / 1024 / 1024).toFixed(2)} MB`;
|
|
2186
|
-
function buildMemoryStatusView(input) {
|
|
2187
|
-
const governed = [
|
|
2188
|
-
{ label: "active", value: String(input.active) }
|
|
2189
|
-
];
|
|
2190
|
-
governed.push(
|
|
2191
|
-
input.pending > 0 ? {
|
|
2192
|
-
label: "pending",
|
|
2193
|
-
// 照着敲就能用(v0.4.250 定的口径):跳到待批准列表 + 点明 id 是 8 位
|
|
2194
|
-
value: `${input.pending} \u2014 run /memory show --pending, then /memory approve <8-char-id>`,
|
|
2195
|
-
warn: true
|
|
2196
|
-
} : { label: "pending", value: "0" }
|
|
2197
|
-
);
|
|
2198
|
-
if (input.injected) {
|
|
2199
|
-
const note = input.injected.truncated ? ` \u26A0 truncated \u2014 only newest ${input.injected.entryCount} of ${input.injected.totalCount} injected` : "";
|
|
2200
|
-
governed.push({
|
|
2201
|
-
label: "injected",
|
|
2202
|
-
value: `${input.injected.entryCount} of ${input.injected.totalCount} (${input.injected.chars} chars)${note}`,
|
|
2203
|
-
warn: input.injected.truncated
|
|
2204
|
-
});
|
|
2205
|
-
} else {
|
|
2206
|
-
governed.push({ label: "injected", value: "0" });
|
|
2207
|
-
}
|
|
2208
|
-
governed.push({ label: "store", value: input.storePath });
|
|
2209
|
-
if (!input.chatIndex.exists) {
|
|
2210
|
-
return { governed, chatIndex: [], chatIndexHint: "not built yet \u2014 run /memory rebuild" };
|
|
2211
|
-
}
|
|
2212
|
-
return {
|
|
2213
|
-
governed,
|
|
2214
|
-
chatIndex: [
|
|
2215
|
-
{ label: "chunks", value: String(input.chatIndex.chunks) },
|
|
2216
|
-
{ label: "sessions", value: String(input.chatIndex.sessions) },
|
|
2217
|
-
{ label: "built", value: input.chatIndex.built?.replace("T", " ").slice(0, 19) ?? "-" },
|
|
2218
|
-
{ label: "model", value: input.chatIndex.model ?? "-" },
|
|
2219
|
-
{ label: "vec file", value: mb(input.chatIndex.vecFileSizeBytes) },
|
|
2220
|
-
{ label: "chunks file", value: mb(input.chatIndex.chunksFileSizeBytes) }
|
|
2221
|
-
],
|
|
2222
|
-
chatIndexHint: null
|
|
2223
|
-
};
|
|
2224
|
-
}
|
|
2225
|
-
function rebuildStartMessage(full) {
|
|
2226
|
-
return full ? "\u{1F9E0} Rebuilding chat memory index from scratch (this may take a while on first run \u2014 embedding model ~117 MB)\u2026" : "\u{1F9E0} Refreshing chat memory index (incremental)\u2026";
|
|
2227
|
-
}
|
|
2228
|
-
function rebuildDoneMessage(stats) {
|
|
2229
|
-
return `\u2713 Chat index: ${stats.chunksTotal} chunks (${stats.chunksAdded} new, ${stats.chunksRemoved} removed) across ${stats.sessionsIndexed + stats.sessionsSkipped} sessions (${stats.sessionsIndexed} re-indexed, ${stats.sessionsSkipped} cached) in ${stats.durationMs}ms`;
|
|
2230
|
-
}
|
|
2231
|
-
var clip = (text, max) => text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
2232
|
-
function buildRecallView(query, persistent, chat) {
|
|
2233
|
-
const view = {
|
|
2234
|
-
persistentHeading: persistent.length > 0 ? `\u{1F4DD} Persistent memory \u2014 ${persistent.length} hit(s):` : null,
|
|
2235
|
-
persistent: persistent.map((h, i) => ({
|
|
2236
|
-
label: `[${i + 1}] id ${h.id.slice(0, 8)} \xB7 ${h.scope} \xB7 coverage ${h.score.toFixed(2)}`,
|
|
2237
|
-
body: h.content
|
|
2238
|
-
})),
|
|
2239
|
-
chatHeading: chat.length > 0 ? `\u{1F9E0} Chat history recall for "${query}" \u2014 ${chat.length} hit(s):` : null,
|
|
2240
|
-
chat: chat.map((h, i) => ({
|
|
2241
|
-
label: `[${i + 1}] ${h.timestamp.slice(0, 16).replace("T", " ")} \xB7 ${h.title} \xB7 score ${h.score.toFixed(3)}`,
|
|
2242
|
-
body: clip(h.text, 400)
|
|
2243
|
-
})),
|
|
2244
|
-
note: null
|
|
2245
|
-
};
|
|
2246
|
-
if (chat.length === 0) {
|
|
2247
|
-
view.note = persistent.length === 0 ? `No memories matched "${query}" (persistent + chat index).` : "(no chat-history hits above score 0.20)";
|
|
2248
|
-
}
|
|
2249
|
-
return view;
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
// src/core/session-cost.ts
|
|
2253
|
-
function buildSessionCostReport(input) {
|
|
2254
|
-
const { usage } = input;
|
|
2255
|
-
const totalTokens = usage.inputTokens + usage.outputTokens + usage.cacheCreationTokens + usage.cacheReadTokens;
|
|
2256
|
-
if (totalTokens === 0) {
|
|
2257
|
-
return { hasUsage: false, totalTokens: 0, cacheHitPct: null, cost: null, costSource: "unknown", savings: null };
|
|
2258
|
-
}
|
|
2259
|
-
const liveCost = computeCost(input.provider, input.model, usage);
|
|
2260
|
-
const useAccrual = input.accruedUsd > 0;
|
|
2261
|
-
const cost = useAccrual ? input.accruedUsd : liveCost;
|
|
2262
|
-
const costSource = useAccrual ? input.accrualIncomplete ? "accrued-partial" : "accrued" : liveCost != null ? "estimated" : "unknown";
|
|
2263
|
-
let savings = null;
|
|
2264
|
-
if (liveCost != null && usage.cacheReadTokens > 0) {
|
|
2265
|
-
const withoutCache = computeCost(input.provider, input.model, {
|
|
2266
|
-
inputTokens: usage.inputTokens + usage.cacheReadTokens,
|
|
2267
|
-
outputTokens: usage.outputTokens,
|
|
2268
|
-
cacheCreationTokens: usage.cacheCreationTokens,
|
|
2269
|
-
cacheReadTokens: 0
|
|
2270
|
-
});
|
|
2271
|
-
if (withoutCache != null) savings = withoutCache - liveCost;
|
|
2272
|
-
}
|
|
2273
|
-
const cacheHitPct = usage.cacheReadTokens > 0 ? Math.round(usage.cacheReadTokens / (usage.cacheReadTokens + usage.inputTokens) * 100) : null;
|
|
2274
|
-
return { hasUsage: true, totalTokens, cacheHitPct, cost, costSource, savings };
|
|
2275
|
-
}
|
|
2276
|
-
function costSourceNote(source) {
|
|
2277
|
-
switch (source) {
|
|
2278
|
-
case "accrued-partial":
|
|
2279
|
-
return "(partial \u2014 some turns had unknown pricing)";
|
|
2280
|
-
case "estimated":
|
|
2281
|
-
return "(estimated at current rates)";
|
|
2282
|
-
default:
|
|
2283
|
-
return null;
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
export {
|
|
2288
|
-
applyThinkCommand,
|
|
2289
|
-
resolveProviderSwitch,
|
|
2290
|
-
parseModelCommand,
|
|
2291
|
-
resolveModelSwitch,
|
|
2292
|
-
applyPlanCommand,
|
|
2293
|
-
parseRouteCommand,
|
|
2294
|
-
routingEmptyRulesNote,
|
|
2295
|
-
describeRouting,
|
|
2296
|
-
routeDecisionRows,
|
|
2297
|
-
parseSessionListArgs,
|
|
2298
|
-
sessionListNeedsFileSize,
|
|
2299
|
-
selectSessions,
|
|
2300
|
-
sessionListSummary,
|
|
2301
|
-
sessionListJsonRow,
|
|
2302
|
-
resolveSessionRef,
|
|
2303
|
-
formatSessionSummaryLine,
|
|
2304
|
-
parseBatchDeleteArgs,
|
|
2305
|
-
parseCleanArgs,
|
|
2306
|
-
selectStaleSessions,
|
|
2307
|
-
clearedSessionMessage,
|
|
2308
|
-
parseAgentCommand,
|
|
2309
|
-
formatAgentList,
|
|
2310
|
-
formatAgentRuns,
|
|
2311
|
-
agentStopMessage,
|
|
2312
|
-
agentUnknownMessage,
|
|
2313
|
-
parseSkillCommand,
|
|
2314
|
-
skillHistoryNote,
|
|
2315
|
-
applyProfileCommand,
|
|
2316
|
-
readGitDiff,
|
|
2317
|
-
parseRewindArgs,
|
|
2318
|
-
buildRewindList,
|
|
2319
|
-
formatRewindReport,
|
|
2320
|
-
resolveForkPoint,
|
|
2321
|
-
formatForkReport,
|
|
2322
|
-
scanDirTree,
|
|
2323
|
-
scanProject,
|
|
2324
|
-
buildInitPrompt,
|
|
2325
|
-
MEMORY_USAGE,
|
|
2326
|
-
parseMemoryCommand,
|
|
2327
|
-
buildMemoryStatusView,
|
|
2328
|
-
rebuildStartMessage,
|
|
2329
|
-
rebuildDoneMessage,
|
|
2330
|
-
buildRecallView,
|
|
2331
|
-
parseMemoryShowFilters,
|
|
2332
|
-
filterMemoryEntries,
|
|
2333
|
-
parseExportArgs,
|
|
2334
|
-
buildSessionExport,
|
|
2335
|
-
parseCheckpointCommand,
|
|
2336
|
-
formatCheckpointList,
|
|
2337
|
-
checkpointSavedMessage,
|
|
2338
|
-
checkpointRestoredMessage,
|
|
2339
|
-
checkpointDeletedMessage,
|
|
2340
|
-
checkpointNotFoundMessage,
|
|
2341
|
-
buildSessionCostReport,
|
|
2342
|
-
costSourceNote,
|
|
2343
|
-
parseSimpleYaml,
|
|
2344
|
-
persistToolRound,
|
|
2345
|
-
autoTrimSessionIfNeeded,
|
|
2346
|
-
estimateConversationTokens,
|
|
2347
|
-
evaluateContextPressure,
|
|
2348
|
-
AUTO_COMPACT_INSTRUCTION,
|
|
2349
|
-
stripRoutingTags,
|
|
2350
|
-
decideModel,
|
|
2351
|
-
SNAPSHOT_PROMPT,
|
|
2352
|
-
sessionHasMeaningfulContent,
|
|
2353
|
-
saveDevState,
|
|
2354
|
-
loadDevState,
|
|
2355
|
-
clearDevState,
|
|
2356
|
-
McpManager,
|
|
2357
|
-
SkillManager,
|
|
2358
|
-
snapshotFile,
|
|
2359
|
-
armDeferredSave,
|
|
2360
|
-
decideDeferredSave,
|
|
2361
|
-
setupProxy
|
|
2362
|
-
};
|