@antaif3ng/til-work 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +573 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/agent.d.ts +78 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +372 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/compaction.d.ts +40 -0
- package/dist/core/compaction.d.ts.map +1 -0
- package/dist/core/compaction.js +228 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/config.d.ts +54 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +257 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/llm.d.ts +29 -0
- package/dist/core/llm.d.ts.map +1 -0
- package/dist/core/llm.js +553 -0
- package/dist/core/llm.js.map +1 -0
- package/dist/core/markdown.d.ts +20 -0
- package/dist/core/markdown.d.ts.map +1 -0
- package/dist/core/markdown.js +173 -0
- package/dist/core/markdown.js.map +1 -0
- package/dist/core/memory.d.ts +30 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +163 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/pricing.d.ts +21 -0
- package/dist/core/pricing.d.ts.map +1 -0
- package/dist/core/pricing.js +70 -0
- package/dist/core/pricing.js.map +1 -0
- package/dist/core/session-manager.d.ts +83 -0
- package/dist/core/session-manager.d.ts.map +1 -0
- package/dist/core/session-manager.js +250 -0
- package/dist/core/session-manager.js.map +1 -0
- package/dist/core/session.d.ts +76 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +270 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/skills.d.ts +49 -0
- package/dist/core/skills.d.ts.map +1 -0
- package/dist/core/skills.js +232 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/system-prompt.d.ts +17 -0
- package/dist/core/system-prompt.d.ts.map +1 -0
- package/dist/core/system-prompt.js +77 -0
- package/dist/core/system-prompt.js.map +1 -0
- package/dist/core/tool-permissions.d.ts +12 -0
- package/dist/core/tool-permissions.d.ts.map +1 -0
- package/dist/core/tool-permissions.js +71 -0
- package/dist/core/tool-permissions.js.map +1 -0
- package/dist/core/types.d.ts +157 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +21 -0
- package/dist/core/types.js.map +1 -0
- package/dist/extensions/builtin/mcp.d.ts +61 -0
- package/dist/extensions/builtin/mcp.d.ts.map +1 -0
- package/dist/extensions/builtin/mcp.js +407 -0
- package/dist/extensions/builtin/mcp.js.map +1 -0
- package/dist/extensions/index.d.ts +4 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/index.js +3 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/extensions/loader.d.ts +19 -0
- package/dist/extensions/loader.d.ts.map +1 -0
- package/dist/extensions/loader.js +118 -0
- package/dist/extensions/loader.js.map +1 -0
- package/dist/extensions/runner.d.ts +28 -0
- package/dist/extensions/runner.d.ts.map +1 -0
- package/dist/extensions/runner.js +77 -0
- package/dist/extensions/runner.js.map +1 -0
- package/dist/extensions/types.d.ts +84 -0
- package/dist/extensions/types.d.ts.map +1 -0
- package/dist/extensions/types.js +6 -0
- package/dist/extensions/types.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +355 -0
- package/dist/main.js.map +1 -0
- package/dist/modes/interactive.d.ts +6 -0
- package/dist/modes/interactive.d.ts.map +1 -0
- package/dist/modes/interactive.js +961 -0
- package/dist/modes/interactive.js.map +1 -0
- package/dist/modes/oneshot.d.ts +8 -0
- package/dist/modes/oneshot.d.ts.map +1 -0
- package/dist/modes/oneshot.js +71 -0
- package/dist/modes/oneshot.js.map +1 -0
- package/dist/tools/bash.d.ts +3 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +104 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit.d.ts +3 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +63 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/file-manager.d.ts +3 -0
- package/dist/tools/file-manager.d.ts.map +1 -0
- package/dist/tools/file-manager.js +85 -0
- package/dist/tools/file-manager.js.map +1 -0
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +42 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +3 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +65 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/system-info.d.ts +3 -0
- package/dist/tools/system-info.d.ts.map +1 -0
- package/dist/tools/system-info.js +101 -0
- package/dist/tools/system-info.js.map +1 -0
- package/dist/tools/web-fetch.d.ts +3 -0
- package/dist/tools/web-fetch.d.ts.map +1 -0
- package/dist/tools/web-fetch.js +117 -0
- package/dist/tools/web-fetch.js.map +1 -0
- package/dist/tools/web-search.d.ts +9 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +124 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/tools/write.d.ts +3 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +30 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/utils/autocomplete.d.ts +23 -0
- package/dist/utils/autocomplete.d.ts.map +1 -0
- package/dist/utils/autocomplete.js +191 -0
- package/dist/utils/autocomplete.js.map +1 -0
- package/dist/utils/file-processor.d.ts +19 -0
- package/dist/utils/file-processor.d.ts.map +1 -0
- package/dist/utils/file-processor.js +86 -0
- package/dist/utils/file-processor.js.map +1 -0
- package/dist/utils/path.d.ts +8 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/dist/utils/path.js +60 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/shell.d.ts +6 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +64 -0
- package/dist/utils/shell.js.map +1 -0
- package/dist/utils/truncate.d.ts +12 -0
- package/dist/utils/truncate.d.ts.map +1 -0
- package/dist/utils/truncate.js +46 -0
- package/dist/utils/truncate.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive REPL mode with streaming output and tool execution display.
|
|
3
|
+
*/
|
|
4
|
+
import * as readline from "node:readline";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { TilSession } from "../core/session.js";
|
|
8
|
+
import { getKnownModels, loadConfig, saveConfig, guessProvider } from "../core/config.js";
|
|
9
|
+
import { createToolCallRequestHandler } from "../core/tool-permissions.js";
|
|
10
|
+
import { formatTokenCount } from "../core/pricing.js";
|
|
11
|
+
import { renderMarkdown } from "../core/markdown.js";
|
|
12
|
+
import { extractAtPrefix, getFileSuggestions } from "../utils/autocomplete.js";
|
|
13
|
+
import { extractAtReferences } from "../utils/file-processor.js";
|
|
14
|
+
const COMMANDS = {
|
|
15
|
+
"/help": "显示帮助信息",
|
|
16
|
+
"/clear": "清空对话历史",
|
|
17
|
+
"/model": "切换模型(如 /model gpt-4o)",
|
|
18
|
+
"/models": "查看可用模型列表",
|
|
19
|
+
"/skills": "查看已加载的技能",
|
|
20
|
+
"/mcp": "查看已连接的 MCP 服务和工具",
|
|
21
|
+
"/extensions": "查看已加载的扩展和 MCP 服务",
|
|
22
|
+
"/memory": "查看当前记忆内容",
|
|
23
|
+
"/sessions": "查看历史会话列表",
|
|
24
|
+
"/usage": "查看 Token 用量",
|
|
25
|
+
"/config": "查看当前配置",
|
|
26
|
+
"/exit": "退出 TIL",
|
|
27
|
+
};
|
|
28
|
+
// ── UI helpers ──
|
|
29
|
+
function getBorderWidth() {
|
|
30
|
+
return Math.min(process.stdout.columns || 80, 80);
|
|
31
|
+
}
|
|
32
|
+
function printBorder(ch = "─") {
|
|
33
|
+
console.log(chalk.dim(ch.repeat(getBorderWidth())));
|
|
34
|
+
}
|
|
35
|
+
// ── Interactive Popup Menu ──
|
|
36
|
+
const PROMPT_WIDTH = 3; // visible width of " › "
|
|
37
|
+
function getCommandEntries(session) {
|
|
38
|
+
const allCmds = Object.keys(COMMANDS);
|
|
39
|
+
const skillCmds = session.getSkillNames().map((n) => `/skill:${n}`);
|
|
40
|
+
const skillDescs = {};
|
|
41
|
+
for (const s of session.skills) {
|
|
42
|
+
skillDescs[`/skill:${s.name}`] = s.description.slice(0, 50);
|
|
43
|
+
}
|
|
44
|
+
return [
|
|
45
|
+
...allCmds.map((c) => ({ label: c, detail: COMMANDS[c], value: c })),
|
|
46
|
+
...skillCmds.map((c) => ({ label: c, detail: skillDescs[c] || "加载技能", value: c })),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
function filterEntries(entries, filter) {
|
|
50
|
+
if (filter === "/")
|
|
51
|
+
return entries;
|
|
52
|
+
return entries.filter((e) => e.label.startsWith(filter));
|
|
53
|
+
}
|
|
54
|
+
function getFileMenuEntries(prefix, cwd) {
|
|
55
|
+
const suggestions = getFileSuggestions(prefix, cwd);
|
|
56
|
+
return suggestions.slice(0, 10).map((s) => ({
|
|
57
|
+
label: s.display,
|
|
58
|
+
detail: s.isDirectory ? "📁 目录" : "📄 文件",
|
|
59
|
+
value: s.value,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Render the popup menu below the input line using relative cursor movement.
|
|
64
|
+
* Unlike \x1b[s / \x1b[u (save/restore absolute position), relative movement
|
|
65
|
+
* via \x1b[nA works correctly even when the terminal scrolls during output.
|
|
66
|
+
*/
|
|
67
|
+
function renderMenu(menu, cursorCol) {
|
|
68
|
+
if (!menu.visible || menu.items.length === 0)
|
|
69
|
+
return;
|
|
70
|
+
const totalLinesToClear = Math.max(menu.items.length, menu.renderedLines);
|
|
71
|
+
const padWidth = menu.type === "file" ? 24 : 16;
|
|
72
|
+
let output = "\n";
|
|
73
|
+
for (let i = 0; i < menu.items.length; i++) {
|
|
74
|
+
const { label, detail } = menu.items[i];
|
|
75
|
+
const isSelected = i === menu.selectedIndex;
|
|
76
|
+
if (isSelected) {
|
|
77
|
+
output += ` ${chalk.bgCyan.black(` ${label.padEnd(padWidth)}`)} ${chalk.white(detail)}`;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
output += ` ${chalk.cyan(` ${label.padEnd(padWidth)}`)} ${chalk.dim(detail)}`;
|
|
81
|
+
}
|
|
82
|
+
output += "\x1b[K\n";
|
|
83
|
+
}
|
|
84
|
+
for (let i = menu.items.length; i < menu.renderedLines; i++) {
|
|
85
|
+
output += "\x1b[K\n";
|
|
86
|
+
}
|
|
87
|
+
menu.renderedLines = Math.max(menu.renderedLines, menu.items.length);
|
|
88
|
+
const linesToGoUp = 1 + totalLinesToClear;
|
|
89
|
+
output += `\x1b[${linesToGoUp}A\r`;
|
|
90
|
+
if (cursorCol > 0) {
|
|
91
|
+
output += `\x1b[${cursorCol}C`;
|
|
92
|
+
}
|
|
93
|
+
process.stdout.write(output);
|
|
94
|
+
}
|
|
95
|
+
function clearMenu(menu, cursorCol = 0) {
|
|
96
|
+
if (menu.renderedLines === 0)
|
|
97
|
+
return;
|
|
98
|
+
let output = "";
|
|
99
|
+
const totalLines = menu.renderedLines + 1;
|
|
100
|
+
for (let i = 0; i < totalLines; i++) {
|
|
101
|
+
output += "\n\x1b[K";
|
|
102
|
+
}
|
|
103
|
+
output += `\x1b[${totalLines}A\r`;
|
|
104
|
+
if (cursorCol > 0) {
|
|
105
|
+
output += `\x1b[${cursorCol}C`;
|
|
106
|
+
}
|
|
107
|
+
process.stdout.write(output);
|
|
108
|
+
menu.visible = false;
|
|
109
|
+
menu.items = [];
|
|
110
|
+
menu.selectedIndex = 0;
|
|
111
|
+
menu.renderedLines = 0;
|
|
112
|
+
}
|
|
113
|
+
// ── Spinner ──
|
|
114
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
115
|
+
class Spinner {
|
|
116
|
+
timer = null;
|
|
117
|
+
frameIdx = 0;
|
|
118
|
+
text;
|
|
119
|
+
constructor(text) {
|
|
120
|
+
this.text = text;
|
|
121
|
+
}
|
|
122
|
+
start() {
|
|
123
|
+
this.frameIdx = 0;
|
|
124
|
+
this.render();
|
|
125
|
+
this.timer = setInterval(() => this.render(), 80);
|
|
126
|
+
}
|
|
127
|
+
update(text) {
|
|
128
|
+
this.text = text;
|
|
129
|
+
}
|
|
130
|
+
stop(finalText) {
|
|
131
|
+
if (this.timer) {
|
|
132
|
+
clearInterval(this.timer);
|
|
133
|
+
this.timer = null;
|
|
134
|
+
}
|
|
135
|
+
process.stdout.write("\r\x1b[K");
|
|
136
|
+
if (finalText) {
|
|
137
|
+
process.stdout.write(finalText + "\n");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
render() {
|
|
141
|
+
const frame = SPINNER_FRAMES[this.frameIdx % SPINNER_FRAMES.length];
|
|
142
|
+
process.stdout.write(`\r\x1b[K${chalk.cyan(frame)} ${chalk.dim(this.text)}`);
|
|
143
|
+
this.frameIdx++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ── Exit message ──
|
|
147
|
+
function printSessionTip(session) {
|
|
148
|
+
const shortId = session.sessionManager.shortId;
|
|
149
|
+
console.log(chalk.dim(` 会话: ${chalk.cyan(shortId)} | 恢复: ${chalk.cyan(`til --resume ${shortId}`)}`));
|
|
150
|
+
}
|
|
151
|
+
async function printExitMessage(session) {
|
|
152
|
+
const shortId = session.sessionManager.shortId;
|
|
153
|
+
console.log();
|
|
154
|
+
console.log(chalk.dim(`Bye! 会话已保存 (${chalk.cyan(shortId)})`));
|
|
155
|
+
console.log(chalk.dim(`恢复此会话: ${chalk.cyan(`til --resume ${shortId}`)}`));
|
|
156
|
+
console.log(chalk.dim(`继续最近会话: ${chalk.cyan("til --continue")}`));
|
|
157
|
+
await session.shutdown();
|
|
158
|
+
}
|
|
159
|
+
// ── Main interactive mode ──
|
|
160
|
+
export async function runInteractiveMode(options) {
|
|
161
|
+
// Pending permission prompts queue — resolved by the readline prompt
|
|
162
|
+
let pendingPermissionResolve = null;
|
|
163
|
+
const permissionPrompt = (question) => {
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
pendingPermissionResolve = resolve;
|
|
166
|
+
process.stdout.write("\n" + chalk.yellow(question));
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
if (!options.onToolCallRequest) {
|
|
170
|
+
options.onToolCallRequest = createToolCallRequestHandler(true, permissionPrompt);
|
|
171
|
+
}
|
|
172
|
+
const session = await TilSession.create(options);
|
|
173
|
+
printBanner(session);
|
|
174
|
+
let currentOutput = "";
|
|
175
|
+
let streamingRawText = "";
|
|
176
|
+
let isStreaming = false;
|
|
177
|
+
let spinner = null;
|
|
178
|
+
let firstTokenReceived = false;
|
|
179
|
+
session.subscribe((event) => {
|
|
180
|
+
switch (event.type) {
|
|
181
|
+
case "agent_start":
|
|
182
|
+
currentOutput = "";
|
|
183
|
+
streamingRawText = "";
|
|
184
|
+
isStreaming = true;
|
|
185
|
+
firstTokenReceived = false;
|
|
186
|
+
spinner = new Spinner("思考中...");
|
|
187
|
+
spinner.start();
|
|
188
|
+
break;
|
|
189
|
+
case "message_update": {
|
|
190
|
+
if (event.delta) {
|
|
191
|
+
if (!firstTokenReceived) {
|
|
192
|
+
firstTokenReceived = true;
|
|
193
|
+
if (spinner) {
|
|
194
|
+
spinner.stop();
|
|
195
|
+
spinner = null;
|
|
196
|
+
}
|
|
197
|
+
process.stdout.write("\n");
|
|
198
|
+
}
|
|
199
|
+
// Stream raw text for responsiveness
|
|
200
|
+
process.stdout.write(chalk.white(event.delta));
|
|
201
|
+
streamingRawText += event.delta;
|
|
202
|
+
currentOutput += event.delta;
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case "message_end": {
|
|
207
|
+
if (event.message.role === "assistant") {
|
|
208
|
+
const assistantMsg = event.message;
|
|
209
|
+
const fullText = assistantMsg.content
|
|
210
|
+
.filter((c) => c.type === "text")
|
|
211
|
+
.map((c) => c.text)
|
|
212
|
+
.join("");
|
|
213
|
+
if (fullText.trim() && streamingRawText.trim()) {
|
|
214
|
+
const termWidth = process.stdout.columns || 80;
|
|
215
|
+
const visualLines = countVisualLines(streamingRawText, termWidth);
|
|
216
|
+
for (let i = 0; i < visualLines; i++) {
|
|
217
|
+
process.stdout.write("\x1b[2K\x1b[1A");
|
|
218
|
+
}
|
|
219
|
+
process.stdout.write("\x1b[2K\r");
|
|
220
|
+
try {
|
|
221
|
+
const rendered = renderMarkdown(fullText);
|
|
222
|
+
process.stdout.write(rendered + "\n");
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
process.stdout.write(fullText + "\n");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
streamingRawText = "";
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case "tool_execution_start":
|
|
233
|
+
if (spinner) {
|
|
234
|
+
spinner.stop();
|
|
235
|
+
spinner = null;
|
|
236
|
+
}
|
|
237
|
+
firstTokenReceived = true;
|
|
238
|
+
process.stdout.write(chalk.dim(`\n⚙ ${chalk.cyan(event.toolName)}`) +
|
|
239
|
+
chalk.dim(formatToolArgs(event.toolName, event.args)) +
|
|
240
|
+
"\n");
|
|
241
|
+
spinner = new Spinner(`执行 ${event.toolName} ...`);
|
|
242
|
+
spinner.start();
|
|
243
|
+
break;
|
|
244
|
+
case "tool_execution_end":
|
|
245
|
+
if (spinner) {
|
|
246
|
+
if (event.isError) {
|
|
247
|
+
spinner.stop(chalk.red(` ✗ 错误: ${formatToolResult(event.result)}`));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
spinner.stop(chalk.green(" ✓ 完成"));
|
|
251
|
+
}
|
|
252
|
+
spinner = null;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
if (event.isError) {
|
|
256
|
+
process.stdout.write(chalk.red(` ✗ 错误: ${formatToolResult(event.result)}\n`));
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
process.stdout.write(chalk.green(" ✓ 完成\n"));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
case "compaction_start":
|
|
264
|
+
if (spinner) {
|
|
265
|
+
spinner.stop();
|
|
266
|
+
spinner = null;
|
|
267
|
+
}
|
|
268
|
+
spinner = new Spinner(event.reason === "overflow" ? "上下文溢出,正在压缩..." : "上下文较长,正在压缩...");
|
|
269
|
+
spinner.start();
|
|
270
|
+
break;
|
|
271
|
+
case "compaction_end":
|
|
272
|
+
if (spinner) {
|
|
273
|
+
spinner.stop(chalk.dim(` ◆ 上下文已压缩: ${formatTokenCount(event.tokensBefore)} → ${formatTokenCount(event.tokensAfter)} tokens`));
|
|
274
|
+
spinner = null;
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
case "agent_end": {
|
|
278
|
+
if (spinner) {
|
|
279
|
+
spinner.stop();
|
|
280
|
+
spinner = null;
|
|
281
|
+
}
|
|
282
|
+
isStreaming = false;
|
|
283
|
+
// Show per-turn usage summary
|
|
284
|
+
const turnUsage = session.getLastTurnUsageSummary();
|
|
285
|
+
const ctxInfo = session.getContextUsagePercent();
|
|
286
|
+
if (turnUsage) {
|
|
287
|
+
let ctxColor = chalk.dim;
|
|
288
|
+
if (ctxInfo.percent > 90)
|
|
289
|
+
ctxColor = chalk.red;
|
|
290
|
+
else if (ctxInfo.percent > 70)
|
|
291
|
+
ctxColor = chalk.yellow;
|
|
292
|
+
process.stdout.write(chalk.dim(`\n ${turnUsage} ctx:${ctxColor(ctxInfo.display)}`));
|
|
293
|
+
}
|
|
294
|
+
process.stdout.write("\n");
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
const menu = {
|
|
300
|
+
visible: false,
|
|
301
|
+
items: [],
|
|
302
|
+
selectedIndex: 0,
|
|
303
|
+
renderedLines: 0,
|
|
304
|
+
type: "command",
|
|
305
|
+
};
|
|
306
|
+
const completer = (line) => {
|
|
307
|
+
return [[], line];
|
|
308
|
+
};
|
|
309
|
+
const rl = readline.createInterface({
|
|
310
|
+
input: process.stdin,
|
|
311
|
+
output: process.stdout,
|
|
312
|
+
terminal: true,
|
|
313
|
+
completer,
|
|
314
|
+
});
|
|
315
|
+
const getCursorCol = () => PROMPT_WIDTH + (rl.cursor ?? 0);
|
|
316
|
+
const allEntries = getCommandEntries(session);
|
|
317
|
+
const originalTtyWrite = rl._ttyWrite;
|
|
318
|
+
rl._ttyWrite = function (s, key) {
|
|
319
|
+
if (pendingPermissionResolve && s) {
|
|
320
|
+
const resolve = pendingPermissionResolve;
|
|
321
|
+
pendingPermissionResolve = null;
|
|
322
|
+
process.stdout.write(s + "\n");
|
|
323
|
+
resolve(s);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (isStreaming) {
|
|
327
|
+
if (key && key.name === "escape") {
|
|
328
|
+
if (spinner) {
|
|
329
|
+
spinner.stop();
|
|
330
|
+
spinner = null;
|
|
331
|
+
}
|
|
332
|
+
session.abort();
|
|
333
|
+
process.stdout.write(chalk.yellow("\n⚡ 已中止 (Esc)\n"));
|
|
334
|
+
printSessionTip(session);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
return originalTtyWrite.call(this, s, key);
|
|
338
|
+
}
|
|
339
|
+
const cursorCol = getCursorCol();
|
|
340
|
+
if (menu.visible) {
|
|
341
|
+
if (key && key.name === "up") {
|
|
342
|
+
menu.selectedIndex = Math.max(0, menu.selectedIndex - 1);
|
|
343
|
+
renderMenu(menu, cursorCol);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (key && key.name === "down") {
|
|
347
|
+
menu.selectedIndex = Math.min(menu.items.length - 1, menu.selectedIndex + 1);
|
|
348
|
+
renderMenu(menu, cursorCol);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (key && (key.name === "return" || key.name === "tab")) {
|
|
352
|
+
const selected = menu.items[menu.selectedIndex];
|
|
353
|
+
const menuType = menu.type;
|
|
354
|
+
if (selected) {
|
|
355
|
+
clearMenu(menu, cursorCol);
|
|
356
|
+
if (menuType === "command") {
|
|
357
|
+
rl.line = "";
|
|
358
|
+
rl.cursor = 0;
|
|
359
|
+
process.stdout.write("\r\x1b[K");
|
|
360
|
+
process.stdout.write(chalk.cyan(" › "));
|
|
361
|
+
if (key.name === "return") {
|
|
362
|
+
rl.line = selected.value;
|
|
363
|
+
rl.cursor = selected.value.length;
|
|
364
|
+
process.stdout.write(selected.value);
|
|
365
|
+
return originalTtyWrite.call(this, "", { name: "return" });
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
rl.line = selected.value;
|
|
369
|
+
rl.cursor = selected.value.length;
|
|
370
|
+
process.stdout.write(selected.value);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else if (menuType === "file") {
|
|
375
|
+
const line = rl.line ?? "";
|
|
376
|
+
const atPrefix = extractAtPrefix(line);
|
|
377
|
+
if (atPrefix !== null) {
|
|
378
|
+
const atToken = `@${atPrefix}`;
|
|
379
|
+
const before = line.slice(0, line.length - atToken.length);
|
|
380
|
+
const newLine = before + selected.value + " ";
|
|
381
|
+
rl.line = newLine;
|
|
382
|
+
rl.cursor = newLine.length;
|
|
383
|
+
process.stdout.write("\r\x1b[K");
|
|
384
|
+
process.stdout.write(chalk.cyan(" › "));
|
|
385
|
+
process.stdout.write(newLine);
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (key && key.name === "escape") {
|
|
392
|
+
clearMenu(menu, cursorCol);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
originalTtyWrite.call(this, s, key);
|
|
397
|
+
const newLine = rl.line ?? "";
|
|
398
|
+
const newCursorCol = getCursorCol();
|
|
399
|
+
if (newLine.startsWith("/") && !newLine.includes(" ")) {
|
|
400
|
+
const filtered = filterEntries(allEntries, newLine);
|
|
401
|
+
if (filtered.length > 0) {
|
|
402
|
+
menu.visible = true;
|
|
403
|
+
menu.items = filtered;
|
|
404
|
+
menu.type = "command";
|
|
405
|
+
menu.selectedIndex = 0;
|
|
406
|
+
renderMenu(menu, newCursorCol);
|
|
407
|
+
}
|
|
408
|
+
else if (menu.visible) {
|
|
409
|
+
clearMenu(menu, newCursorCol);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
const atPrefix = extractAtPrefix(newLine);
|
|
414
|
+
if (atPrefix !== null) {
|
|
415
|
+
const entries = getFileMenuEntries(atPrefix, session.cwd);
|
|
416
|
+
if (entries.length > 0) {
|
|
417
|
+
menu.visible = true;
|
|
418
|
+
menu.items = entries;
|
|
419
|
+
menu.type = "file";
|
|
420
|
+
menu.selectedIndex = 0;
|
|
421
|
+
renderMenu(menu, newCursorCol);
|
|
422
|
+
}
|
|
423
|
+
else if (menu.visible) {
|
|
424
|
+
clearMenu(menu, newCursorCol);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else if (menu.visible) {
|
|
428
|
+
clearMenu(menu, newCursorCol);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
if (process.stdin.isTTY) {
|
|
433
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
434
|
+
}
|
|
435
|
+
const promptUser = () => {
|
|
436
|
+
return new Promise((resolve) => {
|
|
437
|
+
clearMenu(menu);
|
|
438
|
+
printBorder();
|
|
439
|
+
rl.question(chalk.cyan(" › "), (answer) => {
|
|
440
|
+
clearMenu(menu);
|
|
441
|
+
printBorder();
|
|
442
|
+
resolve(answer);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
process.on("SIGINT", () => {
|
|
447
|
+
if (isStreaming) {
|
|
448
|
+
if (spinner) {
|
|
449
|
+
spinner.stop();
|
|
450
|
+
spinner = null;
|
|
451
|
+
}
|
|
452
|
+
session.abort();
|
|
453
|
+
process.stdout.write(chalk.yellow("\n⚡ 已中止 (Ctrl+C)\n"));
|
|
454
|
+
printSessionTip(session);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
console.log(chalk.dim("\n(输入 /exit 或 Ctrl+D 退出)"));
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
rl.on("close", () => {
|
|
461
|
+
printExitMessage(session).finally(() => process.exit(0));
|
|
462
|
+
});
|
|
463
|
+
// Main REPL loop
|
|
464
|
+
while (true) {
|
|
465
|
+
const input = await promptUser();
|
|
466
|
+
const trimmed = input.trim();
|
|
467
|
+
if (!trimmed)
|
|
468
|
+
continue;
|
|
469
|
+
if (trimmed.startsWith("/")) {
|
|
470
|
+
const handled = await handleCommand(trimmed, session);
|
|
471
|
+
if (handled === "exit") {
|
|
472
|
+
rl.close();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
// Expand @path references into file contents
|
|
479
|
+
const { expandedText, fileContent } = extractAtReferences(trimmed, session.cwd);
|
|
480
|
+
const finalInput = fileContent ? fileContent + "\n" + expandedText : expandedText;
|
|
481
|
+
await session.prompt(finalInput);
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
process.stdout.write(chalk.red(`\n错误: ${err.message}\n\n`));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// ── Command handler ──
|
|
489
|
+
async function handleCommand(input, session) {
|
|
490
|
+
const [cmd, ...args] = input.split(/\s+/);
|
|
491
|
+
switch (cmd) {
|
|
492
|
+
case "/help":
|
|
493
|
+
console.log(chalk.bold("\n命令列表:\n"));
|
|
494
|
+
for (const [name, desc] of Object.entries(COMMANDS)) {
|
|
495
|
+
console.log(` ${chalk.cyan(name.padEnd(16))} ${desc}`);
|
|
496
|
+
}
|
|
497
|
+
console.log();
|
|
498
|
+
break;
|
|
499
|
+
case "/clear":
|
|
500
|
+
session.clearMessages();
|
|
501
|
+
console.log(chalk.dim("对话已清空。\n"));
|
|
502
|
+
break;
|
|
503
|
+
case "/model": {
|
|
504
|
+
const modelId = args[0];
|
|
505
|
+
if (!modelId) {
|
|
506
|
+
console.log(chalk.yellow(`当前模型: ${session.model.id}`));
|
|
507
|
+
console.log(chalk.dim("用法: /model <model-id>\n"));
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
session.switchModel(modelId);
|
|
512
|
+
console.log(chalk.green(`已切换到: ${modelId}\n`));
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
console.log(chalk.red(`切换失败: ${err.message}\n`));
|
|
516
|
+
}
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case "/models": {
|
|
520
|
+
const cfg = await loadConfig();
|
|
521
|
+
const builtinModels = getKnownModels();
|
|
522
|
+
console.log(chalk.bold("\n内置模型:\n"));
|
|
523
|
+
for (const [id, m] of Object.entries(builtinModels)) {
|
|
524
|
+
const marker = id === session.model.id ? chalk.green(" ●") : " ";
|
|
525
|
+
const ctx = m.contextWindow ? chalk.dim(` ${formatTokenCount(m.contextWindow)} ctx`) : "";
|
|
526
|
+
console.log(`${marker} ${id.padEnd(35)} ${chalk.dim(m.provider)}${ctx}`);
|
|
527
|
+
}
|
|
528
|
+
if (cfg.customModels && Object.keys(cfg.customModels).length > 0) {
|
|
529
|
+
console.log(chalk.bold("\n自定义模型:\n"));
|
|
530
|
+
for (const [id, m] of Object.entries(cfg.customModels)) {
|
|
531
|
+
const marker = id === session.model.id ? chalk.green(" ●") : " ";
|
|
532
|
+
const ctx = m.contextWindow ? chalk.dim(` ${formatTokenCount(m.contextWindow)} ctx`) : "";
|
|
533
|
+
console.log(`${marker} ${id.padEnd(35)} ${chalk.dim(m.provider)}${ctx}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
console.log(chalk.dim(`\n 当前: ${session.model.id} | 切换: /model <id> | 添加: /config add model <id> <provider>\n`));
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
case "/memory": {
|
|
540
|
+
const mem = session.getMemoryContent();
|
|
541
|
+
if (!mem) {
|
|
542
|
+
console.log(chalk.dim("\n 暂无记忆。模型可通过 write/edit 工具写入 MEMORY.md 来保存记忆。\n"));
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
console.log(chalk.bold("\n当前记忆:\n"));
|
|
546
|
+
console.log(mem);
|
|
547
|
+
console.log();
|
|
548
|
+
}
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
case "/sessions": {
|
|
552
|
+
const sessions = TilSession.listSessions(session.cwd);
|
|
553
|
+
if (sessions.length === 0) {
|
|
554
|
+
console.log(chalk.dim("\n 暂无历史会话。\n"));
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
console.log(chalk.bold(`\n历史会话 (${sessions.length}):\n`));
|
|
558
|
+
const currentId = session.sessionManager.shortId;
|
|
559
|
+
for (let i = 0; i < Math.min(sessions.length, 20); i++) {
|
|
560
|
+
const s = sessions[i];
|
|
561
|
+
const date = new Date(s.timestamp).toLocaleString();
|
|
562
|
+
const isCurrent = s.shortId === currentId ? chalk.green(" ●") : " ";
|
|
563
|
+
console.log(`${isCurrent} ${chalk.cyan(s.shortId)} ${chalk.dim(date)} ${chalk.white(s.model)} ${chalk.dim(`${s.messageCount} msgs`)}`);
|
|
564
|
+
}
|
|
565
|
+
console.log(chalk.dim(`\n 恢复会话: til --resume <id>\n`));
|
|
566
|
+
}
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
case "/config": {
|
|
570
|
+
const config = await loadConfig();
|
|
571
|
+
const sub = args[0];
|
|
572
|
+
if (!sub) {
|
|
573
|
+
console.log(chalk.bold("\n当前配置:\n"));
|
|
574
|
+
console.log(` 模型: ${chalk.cyan(session.model.id)}`);
|
|
575
|
+
console.log(` Provider: ${chalk.cyan(session.model.provider)}`);
|
|
576
|
+
console.log(` Base URL: ${chalk.cyan(session.model.baseUrl || "(默认)")}`);
|
|
577
|
+
console.log(` API Key: ${session.model.apiKey ? chalk.green("已配置") : chalk.red("未设置")}`);
|
|
578
|
+
console.log(` Session: ${chalk.cyan(session.sessionManager.shortId)}`);
|
|
579
|
+
if (session.model.headers && Object.keys(session.model.headers).length > 0) {
|
|
580
|
+
console.log(` Headers: ${chalk.dim(Object.keys(session.model.headers).join(", "))}`);
|
|
581
|
+
}
|
|
582
|
+
console.log(chalk.bold("\n 所有 Provider:"));
|
|
583
|
+
for (const [name, prov] of Object.entries(config.providers)) {
|
|
584
|
+
const parts = [];
|
|
585
|
+
if (prov.apiKey)
|
|
586
|
+
parts.push(chalk.green("key ✓"));
|
|
587
|
+
else
|
|
588
|
+
parts.push(chalk.dim("无 key"));
|
|
589
|
+
if (prov.baseUrl)
|
|
590
|
+
parts.push(prov.baseUrl);
|
|
591
|
+
console.log(` ${chalk.cyan(name.padEnd(20))} ${parts.join(" ")}`);
|
|
592
|
+
}
|
|
593
|
+
if (config.customModels && Object.keys(config.customModels).length > 0) {
|
|
594
|
+
console.log(chalk.bold("\n 自定义模型:"));
|
|
595
|
+
for (const [id, m] of Object.entries(config.customModels)) {
|
|
596
|
+
console.log(` ${chalk.cyan(id.padEnd(30))} ${chalk.dim(m.provider)}${m.contextWindow ? ` ctx:${formatTokenCount(m.contextWindow)}` : ""}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
console.log(chalk.dim("\n 用法: /config set model|key|url|add-model (查看 /config help)\n"));
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
if (sub === "help") {
|
|
603
|
+
console.log(chalk.bold("\n/config 子命令:\n"));
|
|
604
|
+
console.log(` ${chalk.cyan("/config")} 查看当前配置`);
|
|
605
|
+
console.log(` ${chalk.cyan("/config set model <id>")} 设置默认模型 (持久化)`);
|
|
606
|
+
console.log(` ${chalk.cyan("/config set key <provider> <key>")} 设置 Provider API Key`);
|
|
607
|
+
console.log(` ${chalk.cyan("/config set url <provider> <url>")} 设置 Provider Base URL`);
|
|
608
|
+
console.log(` ${chalk.cyan("/config add model <id> <provider>")} 添加自定义模型`);
|
|
609
|
+
console.log(` ${chalk.cyan("/config rm model <id>")} 删除自定义模型`);
|
|
610
|
+
console.log();
|
|
611
|
+
console.log(chalk.dim(" 示例:"));
|
|
612
|
+
console.log(chalk.dim(" /config set model gpt-4o"));
|
|
613
|
+
console.log(chalk.dim(" /config set key openai sk-xxx"));
|
|
614
|
+
console.log(chalk.dim(" /config set url openai https://my-proxy.com/v1"));
|
|
615
|
+
console.log(chalk.dim(" /config add model deepseek-chat openai-compatible"));
|
|
616
|
+
console.log();
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
if (sub === "set") {
|
|
620
|
+
const field = args[1];
|
|
621
|
+
if (field === "model") {
|
|
622
|
+
const modelId = args[2];
|
|
623
|
+
if (!modelId) {
|
|
624
|
+
console.log(chalk.red("\n 用法: /config set model <model-id>\n"));
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
const provider = guessProvider(modelId);
|
|
628
|
+
config.defaultModel = { provider, id: modelId };
|
|
629
|
+
await saveConfig(config);
|
|
630
|
+
session.switchModel(modelId);
|
|
631
|
+
console.log(chalk.green(`\n 默认模型已设为: ${modelId} (provider: ${provider})`));
|
|
632
|
+
console.log(chalk.dim(` 已同步切换当前会话模型\n`));
|
|
633
|
+
}
|
|
634
|
+
else if (field === "key") {
|
|
635
|
+
const provider = args[2];
|
|
636
|
+
const key = args[3];
|
|
637
|
+
if (!provider || !key) {
|
|
638
|
+
console.log(chalk.red("\n 用法: /config set key <provider> <api-key>\n"));
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
if (!config.providers[provider])
|
|
642
|
+
config.providers[provider] = {};
|
|
643
|
+
config.providers[provider].apiKey = key;
|
|
644
|
+
await saveConfig(config);
|
|
645
|
+
console.log(chalk.green(`\n 已设置 ${provider} 的 API Key\n`));
|
|
646
|
+
}
|
|
647
|
+
else if (field === "url") {
|
|
648
|
+
const provider = args[2];
|
|
649
|
+
const url = args[3];
|
|
650
|
+
if (!provider || !url) {
|
|
651
|
+
console.log(chalk.red("\n 用法: /config set url <provider> <base-url>\n"));
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
if (!config.providers[provider])
|
|
655
|
+
config.providers[provider] = {};
|
|
656
|
+
config.providers[provider].baseUrl = url;
|
|
657
|
+
await saveConfig(config);
|
|
658
|
+
console.log(chalk.green(`\n 已设置 ${provider} 的 Base URL: ${url}\n`));
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
console.log(chalk.red(`\n 未知字段: ${field}。支持: model, key, url`));
|
|
662
|
+
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
663
|
+
}
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
if (sub === "add") {
|
|
667
|
+
const what = args[1];
|
|
668
|
+
if (what === "model") {
|
|
669
|
+
const modelId = args[2];
|
|
670
|
+
const provider = args[3];
|
|
671
|
+
if (!modelId || !provider) {
|
|
672
|
+
console.log(chalk.red("\n 用法: /config add model <model-id> <provider>\n"));
|
|
673
|
+
console.log(chalk.dim(" provider 可选: anthropic, openai, openai-compatible, google\n"));
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
if (!config.customModels)
|
|
677
|
+
config.customModels = {};
|
|
678
|
+
config.customModels[modelId] = {
|
|
679
|
+
provider,
|
|
680
|
+
name: modelId,
|
|
681
|
+
maxTokens: 8192,
|
|
682
|
+
};
|
|
683
|
+
await saveConfig(config);
|
|
684
|
+
console.log(chalk.green(`\n 已添加自定义模型: ${modelId} (provider: ${provider})`));
|
|
685
|
+
console.log(chalk.dim(` 切换使用: /model ${modelId}\n`));
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
console.log(chalk.red(`\n 未知类型: ${what}。支持: model`));
|
|
689
|
+
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
690
|
+
}
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
if (sub === "rm") {
|
|
694
|
+
const what = args[1];
|
|
695
|
+
if (what === "model") {
|
|
696
|
+
const modelId = args[2];
|
|
697
|
+
if (!modelId) {
|
|
698
|
+
console.log(chalk.red("\n 用法: /config rm model <model-id>\n"));
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
if (!config.customModels?.[modelId]) {
|
|
702
|
+
console.log(chalk.red(`\n 自定义模型 "${modelId}" 不存在 (内置模型不可删除)\n`));
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
delete config.customModels[modelId];
|
|
706
|
+
await saveConfig(config);
|
|
707
|
+
console.log(chalk.green(`\n 已删除自定义模型: ${modelId}\n`));
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
console.log(chalk.red(`\n 未知类型: ${what}。支持: model`));
|
|
711
|
+
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
712
|
+
}
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
console.log(chalk.red(`\n 未知子命令: ${sub}`));
|
|
716
|
+
console.log(chalk.dim(" 查看帮助: /config help\n"));
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
case "/skills": {
|
|
720
|
+
const skills = session.skills;
|
|
721
|
+
if (skills.length === 0) {
|
|
722
|
+
console.log(chalk.dim("\n 暂无技能。将 SKILL.md 文件放入 ~/.til/skills/ 或 .til/skills/ 即可加载。\n"));
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
console.log(chalk.bold(`\n已加载技能 (${skills.length}):\n`));
|
|
726
|
+
for (const s of skills) {
|
|
727
|
+
console.log(` ${chalk.cyan(s.name.padEnd(24))} ${chalk.dim(s.description.slice(0, 60))}`);
|
|
728
|
+
console.log(` ${chalk.dim(s.filePath)}`);
|
|
729
|
+
}
|
|
730
|
+
console.log(chalk.dim("\n 使用 /skill:<名称> 查看技能详情。\n"));
|
|
731
|
+
}
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
case "/mcp": {
|
|
735
|
+
const cfg = await loadConfig();
|
|
736
|
+
const mcpServers = cfg.mcpServers ?? {};
|
|
737
|
+
const serverNames = Object.keys(mcpServers);
|
|
738
|
+
if (serverNames.length === 0) {
|
|
739
|
+
console.log(chalk.dim("\n 暂无 MCP 服务。\n"));
|
|
740
|
+
console.log(chalk.dim(" 在 ~/.til/config.json 中配置 mcpServers:"));
|
|
741
|
+
console.log(chalk.dim(' { "mcpServers": { "name": { "transport": "streamable_http", "url": "..." } } }\n'));
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
const mcpExt = session.extensionRunner?.loadedExtensions.find((e) => e.name === "mcp");
|
|
745
|
+
const mcpTools = mcpExt ? [...mcpExt.tools.keys()] : [];
|
|
746
|
+
console.log(chalk.bold(`\nMCP 服务 (${serverNames.length}):\n`));
|
|
747
|
+
for (const name of serverNames) {
|
|
748
|
+
const server = mcpServers[name];
|
|
749
|
+
const rawTransport = server.transport ?? server.type;
|
|
750
|
+
const transport = (rawTransport === "http" || rawTransport === "streamable_http" || rawTransport === "sse")
|
|
751
|
+
? "http"
|
|
752
|
+
: rawTransport === "stdio" ? "stdio"
|
|
753
|
+
: server.url ? "http" : "stdio";
|
|
754
|
+
const serverTools = mcpTools.filter((t) => t.startsWith(`mcp_${name}_`));
|
|
755
|
+
const status = serverTools.length > 0 ? chalk.green("✓ 已连接") : chalk.red("✗ 未连接");
|
|
756
|
+
console.log(` ${chalk.cyan(name.padEnd(20))} ${status} ${chalk.dim(transport)}`);
|
|
757
|
+
if (server.url)
|
|
758
|
+
console.log(` ${chalk.dim("URL:")} ${server.url}`);
|
|
759
|
+
if (server.command)
|
|
760
|
+
console.log(` ${chalk.dim("CMD:")} ${server.command} ${(server.args ?? []).join(" ")}`);
|
|
761
|
+
if (serverTools.length > 0) {
|
|
762
|
+
console.log(` ${chalk.dim(`工具 (${serverTools.length}):`)}`);
|
|
763
|
+
for (const toolKey of serverTools) {
|
|
764
|
+
const shortName = toolKey.replace(`mcp_${name}_`, "");
|
|
765
|
+
const toolDef = mcpExt?.tools.get(toolKey);
|
|
766
|
+
const desc = toolDef ? chalk.dim(` — ${(toolDef.description ?? "").slice(0, 50)}`) : "";
|
|
767
|
+
console.log(` ${chalk.white(shortName)}${desc}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
console.log();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
case "/extensions": {
|
|
776
|
+
const extInfo = session.getExtensionInfo();
|
|
777
|
+
if (extInfo.length === 0) {
|
|
778
|
+
console.log(chalk.dim("\n 暂无扩展。在 ~/.til/config.json 中配置 mcpServers 或 extensions 字段。\n"));
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
console.log(chalk.bold(`\n已加载扩展 (${extInfo.length}):\n`));
|
|
782
|
+
for (const ext of extInfo) {
|
|
783
|
+
const parts = [];
|
|
784
|
+
if (ext.toolCount > 0)
|
|
785
|
+
parts.push(`${ext.toolCount} 个工具`);
|
|
786
|
+
if (ext.commandCount > 0)
|
|
787
|
+
parts.push(`${ext.commandCount} 个命令`);
|
|
788
|
+
console.log(` ${chalk.cyan(ext.name.padEnd(20))} ${chalk.dim(parts.join(", ") || "无注册项")}`);
|
|
789
|
+
console.log(` ${chalk.dim(ext.path)}`);
|
|
790
|
+
}
|
|
791
|
+
console.log();
|
|
792
|
+
}
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
case "/usage": {
|
|
796
|
+
const usage = session.cumulativeUsage;
|
|
797
|
+
const ctxInfo = session.getContextUsagePercent();
|
|
798
|
+
console.log(chalk.bold("\nToken 用量统计:\n"));
|
|
799
|
+
console.log(` 输入 tokens: ${chalk.cyan(formatTokenCount(usage.input))}`);
|
|
800
|
+
console.log(` 输出 tokens: ${chalk.cyan(formatTokenCount(usage.output))}`);
|
|
801
|
+
if (usage.cacheRead)
|
|
802
|
+
console.log(` 缓存读取: ${chalk.cyan(formatTokenCount(usage.cacheRead))}`);
|
|
803
|
+
if (usage.cacheWrite)
|
|
804
|
+
console.log(` 缓存写入: ${chalk.cyan(formatTokenCount(usage.cacheWrite))}`);
|
|
805
|
+
console.log(` 上下文占用: ${chalk.cyan(ctxInfo.display)} (窗口: ${session.model.contextWindow ? formatTokenCount(session.model.contextWindow) : "?"})`);
|
|
806
|
+
console.log();
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
case "/exit":
|
|
810
|
+
case "/quit":
|
|
811
|
+
return "exit";
|
|
812
|
+
default:
|
|
813
|
+
if (cmd.startsWith("/skill:")) {
|
|
814
|
+
const skillName = cmd.slice("/skill:".length);
|
|
815
|
+
const skill = session.skills.find((s) => s.name === skillName);
|
|
816
|
+
if (!skill) {
|
|
817
|
+
console.log(chalk.yellow(`\n 技能未找到: ${skillName}。使用 /skills 查看列表。\n`));
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
try {
|
|
821
|
+
const content = readFileSync(skill.filePath, "utf-8");
|
|
822
|
+
console.log(chalk.bold(`\n── ${skill.name} ──`));
|
|
823
|
+
console.log(chalk.dim(skill.filePath));
|
|
824
|
+
console.log();
|
|
825
|
+
console.log(content);
|
|
826
|
+
console.log();
|
|
827
|
+
}
|
|
828
|
+
catch (err) {
|
|
829
|
+
console.log(chalk.red(`\n 读取失败: ${err.message}\n`));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
// Try extension-registered commands
|
|
835
|
+
const extCmd = cmd.slice(1);
|
|
836
|
+
const extCommands = session.getExtensionCommands();
|
|
837
|
+
if (extCommands.has(extCmd)) {
|
|
838
|
+
try {
|
|
839
|
+
const result = await extCommands.get(extCmd).execute(args);
|
|
840
|
+
if (result)
|
|
841
|
+
console.log(result);
|
|
842
|
+
}
|
|
843
|
+
catch (err) {
|
|
844
|
+
console.log(chalk.red(`扩展命令错误: ${err.message}\n`));
|
|
845
|
+
}
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
console.log(chalk.yellow(`未知命令: ${cmd}。输入 /help 查看所有命令。\n`));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
// ── Formatting helpers ──
|
|
852
|
+
function formatToolArgs(toolName, args) {
|
|
853
|
+
if (!args)
|
|
854
|
+
return "";
|
|
855
|
+
switch (toolName) {
|
|
856
|
+
case "bash":
|
|
857
|
+
return ` ${chalk.dim("$")} ${chalk.white(args.command || "")}`;
|
|
858
|
+
case "read":
|
|
859
|
+
case "write":
|
|
860
|
+
case "edit":
|
|
861
|
+
return ` ${chalk.white(args.path || "")}`;
|
|
862
|
+
case "file_manager":
|
|
863
|
+
return ` ${chalk.white(args.action || "")} ${chalk.white(args.path || "")}`;
|
|
864
|
+
case "system_info":
|
|
865
|
+
return ` ${chalk.white(args.category || "all")}`;
|
|
866
|
+
case "web_search":
|
|
867
|
+
return ` ${chalk.white(`"${args.query || ""}"`)}`;
|
|
868
|
+
case "web_fetch":
|
|
869
|
+
return ` ${chalk.white(args.url || "")}`;
|
|
870
|
+
default:
|
|
871
|
+
return "";
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function formatToolResult(result) {
|
|
875
|
+
if (!result)
|
|
876
|
+
return "";
|
|
877
|
+
if (Array.isArray(result)) {
|
|
878
|
+
return result
|
|
879
|
+
.filter((c) => c.type === "text")
|
|
880
|
+
.map((c) => c.text)
|
|
881
|
+
.join("\n")
|
|
882
|
+
.slice(0, 200);
|
|
883
|
+
}
|
|
884
|
+
return String(result).slice(0, 200);
|
|
885
|
+
}
|
|
886
|
+
// ── Banner ──
|
|
887
|
+
function isFullWidthCodePoint(code) {
|
|
888
|
+
return ((code >= 0x1100 && code <= 0x115F) ||
|
|
889
|
+
(code >= 0x2E80 && code <= 0xA4CF && code !== 0x303F) ||
|
|
890
|
+
(code >= 0xAC00 && code <= 0xD7A3) ||
|
|
891
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
892
|
+
(code >= 0xFE10 && code <= 0xFE19) ||
|
|
893
|
+
(code >= 0xFE30 && code <= 0xFE6F) ||
|
|
894
|
+
(code >= 0xFF00 && code <= 0xFF60) ||
|
|
895
|
+
(code >= 0xFFE0 && code <= 0xFFE6) ||
|
|
896
|
+
(code >= 0x20000 && code <= 0x2FFFD) ||
|
|
897
|
+
(code >= 0x30000 && code <= 0x3FFFD));
|
|
898
|
+
}
|
|
899
|
+
function getVisualWidth(str) {
|
|
900
|
+
// eslint-disable-next-line no-control-regex
|
|
901
|
+
const clean = str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
902
|
+
let w = 0;
|
|
903
|
+
for (const ch of clean) {
|
|
904
|
+
w += isFullWidthCodePoint(ch.codePointAt(0)) ? 2 : 1;
|
|
905
|
+
}
|
|
906
|
+
return w;
|
|
907
|
+
}
|
|
908
|
+
function countVisualLines(text, termWidth) {
|
|
909
|
+
if (termWidth <= 0)
|
|
910
|
+
return text.split("\n").length;
|
|
911
|
+
const lines = text.split("\n");
|
|
912
|
+
let total = 0;
|
|
913
|
+
for (const line of lines) {
|
|
914
|
+
const w = getVisualWidth(line);
|
|
915
|
+
total += w === 0 ? 1 : Math.ceil(w / termWidth);
|
|
916
|
+
}
|
|
917
|
+
return total;
|
|
918
|
+
}
|
|
919
|
+
function printBanner(session) {
|
|
920
|
+
const VERSION = "v0.1.0";
|
|
921
|
+
const modelId = session.model.name || session.model.id;
|
|
922
|
+
const dir = session.cwd.replace(process.env.HOME || "", "~");
|
|
923
|
+
const sm = session.sessionManager;
|
|
924
|
+
console.log();
|
|
925
|
+
console.log(chalk.bold.cyan(">_") + " " + chalk.bold("欢迎使用千岛湖 Agent 工具 TIL") + " " + chalk.dim(VERSION));
|
|
926
|
+
console.log();
|
|
927
|
+
const sessionLabel = sm.isResumed
|
|
928
|
+
? `${chalk.white(sm.shortId)} ${chalk.dim(`(已恢复, ${sm.restoredMessageCount} 条消息)`)}`
|
|
929
|
+
: chalk.white(sm.shortId);
|
|
930
|
+
const ctxLabel = session.model.contextWindow
|
|
931
|
+
? chalk.dim(`(${formatTokenCount(session.model.contextWindow)} ctx)`)
|
|
932
|
+
: "";
|
|
933
|
+
console.log(` ${chalk.dim("session")} ${sessionLabel}`);
|
|
934
|
+
console.log(` ${chalk.dim("model")} ${chalk.white(modelId)} ${ctxLabel}`);
|
|
935
|
+
console.log(` ${chalk.dim("directory")} ${chalk.white(dir)}`);
|
|
936
|
+
if (session.skills.length > 0) {
|
|
937
|
+
console.log(` ${chalk.dim("skills")} ${chalk.white(String(session.skills.length))} 个已加载`);
|
|
938
|
+
}
|
|
939
|
+
const extInfo = session.getExtensionInfo();
|
|
940
|
+
if (extInfo.length > 0) {
|
|
941
|
+
const totalTools = extInfo.reduce((sum, e) => sum + e.toolCount, 0);
|
|
942
|
+
console.log(` ${chalk.dim("extensions")} ${chalk.white(String(extInfo.length))} 个已加载 (${totalTools} 工具)`);
|
|
943
|
+
}
|
|
944
|
+
const tips = [
|
|
945
|
+
"输入 / 查看所有可用命令,方向键选择",
|
|
946
|
+
"使用 /model 切换模型",
|
|
947
|
+
"按 Ctrl+C 或 Esc 中止正在执行的请求",
|
|
948
|
+
"使用 /memory 查看记忆内容",
|
|
949
|
+
"使用 /sessions 查看历史会话",
|
|
950
|
+
"输入 @ 引用文件内容(支持搜索和方向键选择)",
|
|
951
|
+
];
|
|
952
|
+
if (session.skills.length > 0) {
|
|
953
|
+
tips.push("使用 /skills 查看已加载的技能");
|
|
954
|
+
}
|
|
955
|
+
if (extInfo.length > 0) {
|
|
956
|
+
tips.push("使用 /extensions 查看已加载的扩展");
|
|
957
|
+
}
|
|
958
|
+
const tip = tips[Math.floor(Math.random() * tips.length)];
|
|
959
|
+
console.log(`\n ${chalk.bold("Tip:")} ${chalk.italic(tip)}\n`);
|
|
960
|
+
}
|
|
961
|
+
//# sourceMappingURL=interactive.js.map
|