@antaif3ng/til-work 0.2.0 → 0.4.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 +92 -33
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +33 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/pricing.d.ts.map +1 -1
- package/dist/core/pricing.js +0 -2
- package/dist/core/pricing.js.map +1 -1
- package/dist/core/session.d.ts +10 -3
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +109 -8
- package/dist/core/session.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +3 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/modes/interactive.d.ts.map +1 -1
- package/dist/modes/interactive.js +699 -167
- package/dist/modes/interactive.js.map +1 -1
- package/dist/tools/browser.d.ts +5 -5
- package/dist/tools/browser.d.ts.map +1 -1
- package/dist/tools/browser.js +185 -135
- package/dist/tools/browser.js.map +1 -1
- package/dist/utils/file-processor.d.ts +2 -2
- package/dist/utils/file-processor.d.ts.map +1 -1
- package/dist/utils/file-processor.js +7 -10
- package/dist/utils/file-processor.js.map +1 -1
- package/package.json +3 -2
- package/skills/agent-browser/SKILL.md +89 -0
- package/skills/excel-xlsx/SKILL.md +103 -0
- package/skills/nano-pdf/SKILL.md +20 -0
- package/skills/word-docx/SKILL.md +104 -0
- package/skills/playwright-mcp/SKILL.md +0 -90
|
@@ -5,7 +5,7 @@ import * as readline from "node:readline";
|
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { TilSession } from "../core/session.js";
|
|
8
|
-
import { getConfigPath, loadConfig, saveConfig } from "../core/config.js";
|
|
8
|
+
import { getConfigPath, loadConfig, saveConfig, saveProjectMcpServers, loadProjectMcpServers } from "../core/config.js";
|
|
9
9
|
import { VERSION } from "../version.js";
|
|
10
10
|
import { createToolCallRequestHandler } from "../core/tool-permissions.js";
|
|
11
11
|
import { formatTokenCount } from "../core/pricing.js";
|
|
@@ -16,8 +16,8 @@ const COMMANDS = {
|
|
|
16
16
|
"/help": "显示帮助信息",
|
|
17
17
|
"/clear": "清空对话历史",
|
|
18
18
|
"/model": "查看/切换/管理模型",
|
|
19
|
-
"/skills": "
|
|
20
|
-
"/mcp": "
|
|
19
|
+
"/skills": "查看/管理技能 (reload/install/enable/disable/rm)",
|
|
20
|
+
"/mcp": "查看/管理 MCP 服务 (add/rm/reload)",
|
|
21
21
|
"/extensions": "查看已加载的扩展和 MCP 服务",
|
|
22
22
|
"/memory": "查看当前记忆内容",
|
|
23
23
|
"/sessions": "查看历史会话列表",
|
|
@@ -25,6 +25,15 @@ const COMMANDS = {
|
|
|
25
25
|
"/config": "查看当前配置",
|
|
26
26
|
"/exit": "退出 TIL",
|
|
27
27
|
};
|
|
28
|
+
const TIPS = [
|
|
29
|
+
"输入 / 查看所有可用命令,方向键选择",
|
|
30
|
+
"使用 /model 切换模型",
|
|
31
|
+
"按 Ctrl+C 或 Esc 中止正在执行的请求",
|
|
32
|
+
"使用 /memory 查看记忆内容",
|
|
33
|
+
"使用 /sessions 查看历史会话",
|
|
34
|
+
"输入 @ 引用文件内容(支持搜索和方向键选择)",
|
|
35
|
+
"换行: 行尾输入 \\ 再回车,或 Ctrl+J / Alt+Enter",
|
|
36
|
+
];
|
|
28
37
|
// ── UI helpers ──
|
|
29
38
|
function getBorderWidth() {
|
|
30
39
|
return Math.min(process.stdout.columns || 80, 80);
|
|
@@ -32,8 +41,16 @@ function getBorderWidth() {
|
|
|
32
41
|
function printBorder(ch = "─") {
|
|
33
42
|
console.log(chalk.dim(ch.repeat(getBorderWidth())));
|
|
34
43
|
}
|
|
44
|
+
function shortenDir(cwd) {
|
|
45
|
+
return cwd.replace(process.env.HOME || "", "~");
|
|
46
|
+
}
|
|
35
47
|
// ── Interactive Popup Menu ──
|
|
36
|
-
const PROMPT_WIDTH = 3; // visible width of " › "
|
|
48
|
+
const PROMPT_WIDTH = 3; // visible width of " › " or " … "
|
|
49
|
+
function resetMenu(menu) {
|
|
50
|
+
menu.visible = false;
|
|
51
|
+
menu.items = [];
|
|
52
|
+
menu.selectedIndex = 0;
|
|
53
|
+
}
|
|
37
54
|
function getCommandEntries(session) {
|
|
38
55
|
const allCmds = Object.keys(COMMANDS);
|
|
39
56
|
const skillCmds = session.getSkillNames().map((n) => `/skill:${n}`);
|
|
@@ -54,13 +71,11 @@ function filterEntries(entries, filter) {
|
|
|
54
71
|
function getModelMenuEntries(filter, session) {
|
|
55
72
|
const entries = [];
|
|
56
73
|
const currentId = session.model.id;
|
|
57
|
-
// Current model first
|
|
58
74
|
entries.push({
|
|
59
75
|
label: currentId,
|
|
60
76
|
detail: "当前",
|
|
61
77
|
value: `/model ${currentId}`,
|
|
62
78
|
});
|
|
63
|
-
// Load config synchronously — config.models is already migrated at startup
|
|
64
79
|
const cfg = session.config;
|
|
65
80
|
if (cfg.models) {
|
|
66
81
|
for (const [id, m] of Object.entries(cfg.models)) {
|
|
@@ -88,44 +103,63 @@ function getFileMenuEntries(prefix, cwd) {
|
|
|
88
103
|
value: s.value,
|
|
89
104
|
}));
|
|
90
105
|
}
|
|
106
|
+
const PLACEHOLDER = "Ctrl+J 或 \\+Enter 换行";
|
|
91
107
|
/**
|
|
92
|
-
* Render the
|
|
93
|
-
*
|
|
94
|
-
*
|
|
108
|
+
* Render the area below the input cursor: popup menu (if any) +
|
|
109
|
+
* underline + status line. Only called at prompt start and on menu
|
|
110
|
+
* state changes — NEVER on every keystroke — so CJK IME is not disturbed.
|
|
95
111
|
*/
|
|
96
|
-
function
|
|
97
|
-
if (!
|
|
112
|
+
function renderBelowArea(menu, below, session, cursorCol) {
|
|
113
|
+
if (!below.active)
|
|
98
114
|
return;
|
|
99
|
-
const
|
|
100
|
-
const
|
|
115
|
+
const menuItemCount = menu.visible && menu.items.length > 0 ? menu.items.length : 0;
|
|
116
|
+
const fixedLines = 2; // underline + status
|
|
117
|
+
const contentLines = menuItemCount + fixedLines;
|
|
118
|
+
const totalLinesToProcess = Math.max(contentLines, below.renderedLines);
|
|
119
|
+
// Pre-scroll: ensure enough vertical space below cursor so that
|
|
120
|
+
// writing \n never causes unexpected scrolling after rendering.
|
|
121
|
+
let preScroll = "";
|
|
122
|
+
for (let i = 0; i < totalLinesToProcess + 1; i++)
|
|
123
|
+
preScroll += "\n";
|
|
124
|
+
preScroll += `\x1b[${totalLinesToProcess + 1}A`;
|
|
101
125
|
let output = "\n";
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
// Menu items
|
|
127
|
+
if (menuItemCount > 0) {
|
|
128
|
+
const padWidth = menu.type === "file" ? 24 : 16;
|
|
129
|
+
for (let i = 0; i < menu.items.length; i++) {
|
|
130
|
+
const { label, detail } = menu.items[i];
|
|
131
|
+
const isSelected = i === menu.selectedIndex;
|
|
132
|
+
if (isSelected) {
|
|
133
|
+
output += ` ${chalk.bgCyan.black(` ${label.padEnd(padWidth)}`)} ${chalk.white(detail)}`;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
output += ` ${chalk.cyan(` ${label.padEnd(padWidth)}`)} ${chalk.dim(detail)}`;
|
|
137
|
+
}
|
|
138
|
+
output += "\x1b[K\n";
|
|
110
139
|
}
|
|
111
|
-
output += "\x1b[K\n";
|
|
112
140
|
}
|
|
113
|
-
|
|
141
|
+
// Underline
|
|
142
|
+
output += chalk.dim("─".repeat(getBorderWidth())) + "\x1b[K\n";
|
|
143
|
+
// Status line
|
|
144
|
+
const dir = shortenDir(session.cwd);
|
|
145
|
+
output += chalk.dim(` ${dir} · Tip: ${below.tip}`) + "\x1b[K\n";
|
|
146
|
+
// Clear stale lines from previous render
|
|
147
|
+
for (let i = contentLines; i < below.renderedLines; i++) {
|
|
114
148
|
output += "\x1b[K\n";
|
|
115
149
|
}
|
|
116
|
-
|
|
117
|
-
const linesToGoUp = 1 +
|
|
150
|
+
below.renderedLines = contentLines;
|
|
151
|
+
const linesToGoUp = 1 + totalLinesToProcess;
|
|
118
152
|
output += `\x1b[${linesToGoUp}A\r`;
|
|
119
153
|
if (cursorCol > 0) {
|
|
120
154
|
output += `\x1b[${cursorCol}C`;
|
|
121
155
|
}
|
|
122
|
-
process.stdout.write(output);
|
|
156
|
+
process.stdout.write(preScroll + output);
|
|
123
157
|
}
|
|
124
|
-
function
|
|
125
|
-
if (
|
|
158
|
+
function clearBelowArea(below, cursorCol) {
|
|
159
|
+
if (below.renderedLines === 0)
|
|
126
160
|
return;
|
|
127
161
|
let output = "";
|
|
128
|
-
const totalLines =
|
|
162
|
+
const totalLines = below.renderedLines + 1;
|
|
129
163
|
for (let i = 0; i < totalLines; i++) {
|
|
130
164
|
output += "\n\x1b[K";
|
|
131
165
|
}
|
|
@@ -134,10 +168,11 @@ function clearMenu(menu, cursorCol = 0) {
|
|
|
134
168
|
output += `\x1b[${cursorCol}C`;
|
|
135
169
|
}
|
|
136
170
|
process.stdout.write(output);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
171
|
+
below.renderedLines = 0;
|
|
172
|
+
}
|
|
173
|
+
/** Show grey placeholder text when input is empty, using cursor save/restore. */
|
|
174
|
+
function showPlaceholder() {
|
|
175
|
+
process.stdout.write(`\x1b[s${chalk.dim(PLACEHOLDER)}\x1b[u`);
|
|
141
176
|
}
|
|
142
177
|
// ── Spinner ──
|
|
143
178
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -185,9 +220,18 @@ async function printExitMessage(session) {
|
|
|
185
220
|
console.log(chalk.dim(`继续最近会话: ${chalk.cyan("til --continue")}`));
|
|
186
221
|
await session.shutdown();
|
|
187
222
|
}
|
|
223
|
+
// ── Tips helper ──
|
|
224
|
+
function getRandomTip(session) {
|
|
225
|
+
const tips = [...TIPS];
|
|
226
|
+
if (session.skills.length > 0)
|
|
227
|
+
tips.push("使用 /skills 查看已加载的技能");
|
|
228
|
+
const extInfo = session.getExtensionInfo();
|
|
229
|
+
if (extInfo.length > 0)
|
|
230
|
+
tips.push("使用 /extensions 查看已加载的扩展");
|
|
231
|
+
return tips[Math.floor(Math.random() * tips.length)];
|
|
232
|
+
}
|
|
188
233
|
// ── Main interactive mode ──
|
|
189
234
|
export async function runInteractiveMode(options) {
|
|
190
|
-
// Pending permission prompts queue — resolved by the readline prompt
|
|
191
235
|
let pendingPermissionResolve = null;
|
|
192
236
|
const permissionPrompt = (question) => {
|
|
193
237
|
return new Promise((resolve) => {
|
|
@@ -210,6 +254,10 @@ export async function runInteractiveMode(options) {
|
|
|
210
254
|
let firstTokenReceived = false;
|
|
211
255
|
let insideThinkBlock = false;
|
|
212
256
|
let thinkTagBuffer = "";
|
|
257
|
+
// Multi-line input state
|
|
258
|
+
const multiLineBuffer = [];
|
|
259
|
+
// Below-area state: menu + underline + status
|
|
260
|
+
const below = { renderedLines: 0, active: false, tip: "" };
|
|
213
261
|
session.subscribe((event) => {
|
|
214
262
|
switch (event.type) {
|
|
215
263
|
case "agent_start":
|
|
@@ -365,13 +413,11 @@ export async function runInteractiveMode(options) {
|
|
|
365
413
|
spinner = null;
|
|
366
414
|
}
|
|
367
415
|
isStreaming = false;
|
|
368
|
-
// Check for errors
|
|
369
416
|
const lastMsg = event.messages?.[event.messages.length - 1];
|
|
370
417
|
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
|
|
371
418
|
const errText = lastMsg.errorMessage || "未知错误";
|
|
372
419
|
process.stdout.write(chalk.red(`\n ✗ 错误: ${errText}\n`));
|
|
373
420
|
}
|
|
374
|
-
// Show per-turn usage summary
|
|
375
421
|
const turnUsage = session.getLastTurnUsageSummary();
|
|
376
422
|
const ctxInfo = session.getContextUsagePercent();
|
|
377
423
|
if (turnUsage) {
|
|
@@ -382,7 +428,7 @@ export async function runInteractiveMode(options) {
|
|
|
382
428
|
ctxColor = chalk.yellow;
|
|
383
429
|
process.stdout.write(chalk.dim(`\n ${turnUsage} ctx:${ctxColor(ctxInfo.display)}`));
|
|
384
430
|
}
|
|
385
|
-
process.stdout.write("\n");
|
|
431
|
+
process.stdout.write("\n\n");
|
|
386
432
|
break;
|
|
387
433
|
}
|
|
388
434
|
}
|
|
@@ -391,7 +437,6 @@ export async function runInteractiveMode(options) {
|
|
|
391
437
|
visible: false,
|
|
392
438
|
items: [],
|
|
393
439
|
selectedIndex: 0,
|
|
394
|
-
renderedLines: 0,
|
|
395
440
|
type: "command",
|
|
396
441
|
};
|
|
397
442
|
const completer = (line) => {
|
|
@@ -406,7 +451,167 @@ export async function runInteractiveMode(options) {
|
|
|
406
451
|
const getCursorCol = () => PROMPT_WIDTH + (rl.cursor ?? 0);
|
|
407
452
|
const allEntries = getCommandEntries(session);
|
|
408
453
|
const originalTtyWrite = rl._ttyWrite;
|
|
454
|
+
// Helper: update menu visibility based on current line content
|
|
455
|
+
function updateMenuForLine(line, session) {
|
|
456
|
+
if (line.startsWith("/model ")) {
|
|
457
|
+
const modelFilter = line.slice("/model ".length);
|
|
458
|
+
if (!["add ", "rm ", "default "].some((sub) => modelFilter.startsWith(sub))) {
|
|
459
|
+
const entries = getModelMenuEntries(modelFilter, session);
|
|
460
|
+
if (entries.length > 0) {
|
|
461
|
+
menu.visible = true;
|
|
462
|
+
menu.items = entries;
|
|
463
|
+
menu.type = "command";
|
|
464
|
+
menu.selectedIndex = 0;
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
resetMenu(menu);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
resetMenu(menu);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else if (line.startsWith("/mcp ")) {
|
|
475
|
+
const mcpSub = line.slice("/mcp ".length);
|
|
476
|
+
if (!mcpSub.includes(" ")) {
|
|
477
|
+
const mcpSubcmds = [
|
|
478
|
+
{ label: "add", detail: "添加 MCP 服务", value: "/mcp add " },
|
|
479
|
+
{ label: "rm", detail: "删除 MCP 服务", value: "/mcp rm " },
|
|
480
|
+
{ label: "reload", detail: "重载所有 MCP 服务", value: "/mcp reload" },
|
|
481
|
+
];
|
|
482
|
+
const filtered = mcpSub ? mcpSubcmds.filter((e) => e.label.startsWith(mcpSub)) : mcpSubcmds;
|
|
483
|
+
if (filtered.length > 0) {
|
|
484
|
+
menu.visible = true;
|
|
485
|
+
menu.items = filtered;
|
|
486
|
+
menu.type = "command";
|
|
487
|
+
menu.selectedIndex = 0;
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
resetMenu(menu);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else if (mcpSub.startsWith("rm ")) {
|
|
494
|
+
const nameFilter = mcpSub.slice("rm ".length);
|
|
495
|
+
const mcpExt = session.extensionRunner?.loadedExtensions.find((e) => e.name === "mcp");
|
|
496
|
+
const serverNames = new Set();
|
|
497
|
+
if (mcpExt) {
|
|
498
|
+
for (const key of mcpExt.tools.keys()) {
|
|
499
|
+
const parts = key.split("_");
|
|
500
|
+
if (parts.length >= 3 && parts[0] === "mcp")
|
|
501
|
+
serverNames.add(parts[1]);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (session.config.mcpServers) {
|
|
505
|
+
for (const name of Object.keys(session.config.mcpServers))
|
|
506
|
+
serverNames.add(name);
|
|
507
|
+
}
|
|
508
|
+
const entries = [...serverNames]
|
|
509
|
+
.filter((n) => !nameFilter || n.startsWith(nameFilter))
|
|
510
|
+
.map((n) => ({ label: n, detail: "MCP 服务", value: `/mcp rm ${n}` }));
|
|
511
|
+
if (entries.length > 0) {
|
|
512
|
+
menu.visible = true;
|
|
513
|
+
menu.items = entries;
|
|
514
|
+
menu.type = "command";
|
|
515
|
+
menu.selectedIndex = 0;
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
resetMenu(menu);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
resetMenu(menu);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else if (line.startsWith("/skills ")) {
|
|
526
|
+
const skillsSub = line.slice("/skills ".length);
|
|
527
|
+
if (!skillsSub.includes(" ")) {
|
|
528
|
+
const skillSubcmds = [
|
|
529
|
+
{ label: "reload", detail: "重新扫描并加载技能", value: "/skills reload" },
|
|
530
|
+
{ label: "install", detail: "安装技能", value: "/skills install " },
|
|
531
|
+
{ label: "enable", detail: "启用技能", value: "/skills enable " },
|
|
532
|
+
{ label: "disable", detail: "禁用技能", value: "/skills disable " },
|
|
533
|
+
{ label: "rm", detail: "删除技能文件", value: "/skills rm " },
|
|
534
|
+
];
|
|
535
|
+
const filtered = skillsSub ? skillSubcmds.filter((e) => e.label.startsWith(skillsSub)) : skillSubcmds;
|
|
536
|
+
if (filtered.length > 0) {
|
|
537
|
+
menu.visible = true;
|
|
538
|
+
menu.items = filtered;
|
|
539
|
+
menu.type = "command";
|
|
540
|
+
menu.selectedIndex = 0;
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
resetMenu(menu);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
else if (skillsSub.startsWith("enable ")) {
|
|
547
|
+
const nameFilter = skillsSub.slice("enable ".length);
|
|
548
|
+
const disabled = session.config.disabledSkills ?? [];
|
|
549
|
+
const entries = disabled
|
|
550
|
+
.filter((n) => !nameFilter || n.startsWith(nameFilter))
|
|
551
|
+
.map((n) => ({ label: n, detail: "已禁用", value: `/skills enable ${n}` }));
|
|
552
|
+
if (entries.length > 0) {
|
|
553
|
+
menu.visible = true;
|
|
554
|
+
menu.items = entries;
|
|
555
|
+
menu.type = "command";
|
|
556
|
+
menu.selectedIndex = 0;
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
resetMenu(menu);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
else if (skillsSub.startsWith("disable ") || skillsSub.startsWith("rm ")) {
|
|
563
|
+
const cmd = skillsSub.startsWith("disable ") ? "disable" : "rm";
|
|
564
|
+
const nameFilter = skillsSub.slice(cmd.length + 1);
|
|
565
|
+
const entries = session.skills
|
|
566
|
+
.filter((s) => !nameFilter || s.name.startsWith(nameFilter))
|
|
567
|
+
.map((s) => ({ label: s.name, detail: s.source, value: `/skills ${cmd} ${s.name}` }));
|
|
568
|
+
if (entries.length > 0) {
|
|
569
|
+
menu.visible = true;
|
|
570
|
+
menu.items = entries;
|
|
571
|
+
menu.type = "command";
|
|
572
|
+
menu.selectedIndex = 0;
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
resetMenu(menu);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
resetMenu(menu);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
else if (line.startsWith("/") && !line.includes(" ")) {
|
|
583
|
+
const filtered = filterEntries(allEntries, line);
|
|
584
|
+
if (filtered.length > 0) {
|
|
585
|
+
menu.visible = true;
|
|
586
|
+
menu.items = filtered;
|
|
587
|
+
menu.type = "command";
|
|
588
|
+
menu.selectedIndex = 0;
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
resetMenu(menu);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
const atPrefix = extractAtPrefix(line);
|
|
596
|
+
if (atPrefix !== null) {
|
|
597
|
+
const entries = getFileMenuEntries(atPrefix, session.cwd);
|
|
598
|
+
if (entries.length > 0) {
|
|
599
|
+
menu.visible = true;
|
|
600
|
+
menu.items = entries;
|
|
601
|
+
menu.type = "file";
|
|
602
|
+
menu.selectedIndex = 0;
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
resetMenu(menu);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
resetMenu(menu);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
409
613
|
rl._ttyWrite = function (s, key) {
|
|
614
|
+
// ── Permission prompt passthrough ──
|
|
410
615
|
if (pendingPermissionResolve && s) {
|
|
411
616
|
const resolve = pendingPermissionResolve;
|
|
412
617
|
pendingPermissionResolve = null;
|
|
@@ -414,6 +619,7 @@ export async function runInteractiveMode(options) {
|
|
|
414
619
|
resolve(s);
|
|
415
620
|
return;
|
|
416
621
|
}
|
|
622
|
+
// ── Streaming: only Esc to abort ──
|
|
417
623
|
if (isStreaming) {
|
|
418
624
|
if (key && key.name === "escape") {
|
|
419
625
|
if (spinner) {
|
|
@@ -428,28 +634,55 @@ export async function runInteractiveMode(options) {
|
|
|
428
634
|
return originalTtyWrite.call(this, s, key);
|
|
429
635
|
}
|
|
430
636
|
const cursorCol = getCursorCol();
|
|
637
|
+
const keyName = key?.name;
|
|
638
|
+
const isReturnKey = keyName === "return" || keyName === "enter";
|
|
639
|
+
// ── Multi-line newline insertion ──
|
|
640
|
+
// 1. Shift+Enter / Alt+Enter (terminals that report modifier flags)
|
|
641
|
+
// 2. Ctrl+J: raw char is always \n (0x0A), distinct from Enter's \r (0x0D)
|
|
642
|
+
// 3. Raw \x1b\r from Kitty shift+enter mapping or legacy Alt+Enter
|
|
643
|
+
const isNewlineInsert = (key && isReturnKey && (key.shift === true || key.meta === true)) ||
|
|
644
|
+
s === "\n" ||
|
|
645
|
+
s === "\x1b\r";
|
|
646
|
+
if (isNewlineInsert) {
|
|
647
|
+
clearBelowArea(below, cursorCol);
|
|
648
|
+
multiLineBuffer.push(rl.line ?? "");
|
|
649
|
+
process.stdout.write("\n");
|
|
650
|
+
rl.line = "";
|
|
651
|
+
rl.cursor = 0;
|
|
652
|
+
rl._prompt = chalk.cyan(" … ");
|
|
653
|
+
process.stdout.write(chalk.cyan(" … "));
|
|
654
|
+
resetMenu(menu);
|
|
655
|
+
renderBelowArea(menu, below, session, PROMPT_WIDTH);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
// ── Menu visible: handle navigation & selection ──
|
|
431
659
|
if (menu.visible) {
|
|
432
|
-
if (key &&
|
|
433
|
-
menu.selectedIndex =
|
|
434
|
-
|
|
660
|
+
if (key && keyName === "up") {
|
|
661
|
+
menu.selectedIndex = menu.selectedIndex <= 0
|
|
662
|
+
? menu.items.length - 1
|
|
663
|
+
: menu.selectedIndex - 1;
|
|
664
|
+
renderBelowArea(menu, below, session, cursorCol);
|
|
435
665
|
return;
|
|
436
666
|
}
|
|
437
|
-
if (key &&
|
|
438
|
-
menu.selectedIndex =
|
|
439
|
-
|
|
667
|
+
if (key && keyName === "down") {
|
|
668
|
+
menu.selectedIndex = menu.selectedIndex >= menu.items.length - 1
|
|
669
|
+
? 0
|
|
670
|
+
: menu.selectedIndex + 1;
|
|
671
|
+
renderBelowArea(menu, below, session, cursorCol);
|
|
440
672
|
return;
|
|
441
673
|
}
|
|
442
|
-
if (key && (
|
|
674
|
+
if (key && (isReturnKey || keyName === "tab")) {
|
|
443
675
|
const selected = menu.items[menu.selectedIndex];
|
|
444
676
|
const menuType = menu.type;
|
|
445
677
|
if (selected) {
|
|
446
|
-
|
|
678
|
+
resetMenu(menu);
|
|
447
679
|
if (menuType === "command") {
|
|
448
680
|
rl.line = "";
|
|
449
681
|
rl.cursor = 0;
|
|
450
682
|
process.stdout.write("\r\x1b[K");
|
|
451
683
|
process.stdout.write(chalk.cyan(" › "));
|
|
452
|
-
if (
|
|
684
|
+
if (isReturnKey) {
|
|
685
|
+
clearBelowArea(below, PROMPT_WIDTH);
|
|
453
686
|
rl.line = selected.value;
|
|
454
687
|
rl.cursor = selected.value.length;
|
|
455
688
|
process.stdout.write(selected.value);
|
|
@@ -459,6 +692,7 @@ export async function runInteractiveMode(options) {
|
|
|
459
692
|
rl.line = selected.value;
|
|
460
693
|
rl.cursor = selected.value.length;
|
|
461
694
|
process.stdout.write(selected.value);
|
|
695
|
+
renderBelowArea(menu, below, session, getCursorCol());
|
|
462
696
|
return;
|
|
463
697
|
}
|
|
464
698
|
}
|
|
@@ -475,68 +709,87 @@ export async function runInteractiveMode(options) {
|
|
|
475
709
|
process.stdout.write(chalk.cyan(" › "));
|
|
476
710
|
process.stdout.write(newLine);
|
|
477
711
|
}
|
|
712
|
+
renderBelowArea(menu, below, session, getCursorCol());
|
|
478
713
|
return;
|
|
479
714
|
}
|
|
480
715
|
}
|
|
481
716
|
}
|
|
482
|
-
if (key &&
|
|
483
|
-
|
|
717
|
+
if (key && keyName === "escape") {
|
|
718
|
+
resetMenu(menu);
|
|
719
|
+
renderBelowArea(menu, below, session, cursorCol);
|
|
484
720
|
return;
|
|
485
721
|
}
|
|
486
722
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const entries = getModelMenuEntries(modelFilter, session);
|
|
494
|
-
if (entries.length > 0) {
|
|
495
|
-
menu.visible = true;
|
|
496
|
-
menu.items = entries;
|
|
497
|
-
menu.type = "command";
|
|
498
|
-
menu.selectedIndex = 0;
|
|
499
|
-
renderMenu(menu, newCursorCol);
|
|
723
|
+
// ── Escape: cancel multi-line or ignore ──
|
|
724
|
+
if (key && keyName === "escape") {
|
|
725
|
+
if (multiLineBuffer.length > 0) {
|
|
726
|
+
clearBelowArea(below, cursorCol);
|
|
727
|
+
for (let i = 0; i < multiLineBuffer.length; i++) {
|
|
728
|
+
process.stdout.write("\x1b[1A\x1b[2K");
|
|
500
729
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
730
|
+
process.stdout.write("\r\x1b[K");
|
|
731
|
+
multiLineBuffer.length = 0;
|
|
732
|
+
rl.line = "";
|
|
733
|
+
rl.cursor = 0;
|
|
734
|
+
rl._prompt = chalk.cyan(" › ");
|
|
735
|
+
process.stdout.write(chalk.cyan(" › "));
|
|
736
|
+
renderBelowArea(menu, below, session, PROMPT_WIDTH);
|
|
737
|
+
showPlaceholder();
|
|
738
|
+
return;
|
|
507
739
|
}
|
|
740
|
+
return;
|
|
508
741
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
742
|
+
// ── Enter: submit or backslash-newline ──
|
|
743
|
+
if (key && isReturnKey) {
|
|
744
|
+
const currentLine = rl.line ?? "";
|
|
745
|
+
const cursor = rl.cursor ?? 0;
|
|
746
|
+
// Backslash+Enter → newline (pi-mono style, works in ALL terminals)
|
|
747
|
+
if (cursor > 0 && currentLine[cursor - 1] === "\\") {
|
|
748
|
+
clearBelowArea(below, cursorCol);
|
|
749
|
+
const before = currentLine.slice(0, cursor - 1);
|
|
750
|
+
const after = currentLine.slice(cursor);
|
|
751
|
+
multiLineBuffer.push(before + after);
|
|
752
|
+
process.stdout.write("\n");
|
|
753
|
+
rl.line = "";
|
|
754
|
+
rl.cursor = 0;
|
|
755
|
+
rl._prompt = chalk.cyan(" … ");
|
|
756
|
+
process.stdout.write(chalk.cyan(" … "));
|
|
757
|
+
resetMenu(menu);
|
|
758
|
+
renderBelowArea(menu, below, session, PROMPT_WIDTH);
|
|
759
|
+
return;
|
|
517
760
|
}
|
|
518
|
-
|
|
519
|
-
|
|
761
|
+
// Normal submit
|
|
762
|
+
clearBelowArea(below, cursorCol);
|
|
763
|
+
below.active = false;
|
|
764
|
+
if (multiLineBuffer.length > 0) {
|
|
765
|
+
const fullInput = [...multiLineBuffer, currentLine].join("\n");
|
|
766
|
+
multiLineBuffer.length = 0;
|
|
767
|
+
rl.line = fullInput;
|
|
768
|
+
rl.cursor = fullInput.length;
|
|
769
|
+
rl._prompt = chalk.cyan(" › ");
|
|
520
770
|
}
|
|
771
|
+
originalTtyWrite.call(this, s, key);
|
|
772
|
+
return;
|
|
521
773
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
774
|
+
// ── Normal key processing ──
|
|
775
|
+
const prevMenuVisible = menu.visible;
|
|
776
|
+
const prevMenuItemCount = menu.items.length;
|
|
777
|
+
const prevLine = rl.line ?? "";
|
|
778
|
+
originalTtyWrite.call(this, s, key);
|
|
779
|
+
const newLine = rl.line ?? "";
|
|
780
|
+
updateMenuForLine(newLine, session);
|
|
781
|
+
// Clear placeholder remnants when first character is typed
|
|
782
|
+
if (prevLine.length === 0 && newLine.length > 0) {
|
|
783
|
+
process.stdout.write("\x1b[K");
|
|
784
|
+
}
|
|
785
|
+
const menuChanged = menu.visible !== prevMenuVisible ||
|
|
786
|
+
(menu.visible && menu.items.length !== prevMenuItemCount);
|
|
787
|
+
if (menuChanged) {
|
|
788
|
+
renderBelowArea(menu, below, session, getCursorCol());
|
|
789
|
+
}
|
|
790
|
+
// Re-show placeholder when input is empty
|
|
791
|
+
if (newLine.length === 0 && multiLineBuffer.length === 0 && !menu.visible) {
|
|
792
|
+
showPlaceholder();
|
|
540
793
|
}
|
|
541
794
|
};
|
|
542
795
|
if (process.stdin.isTTY) {
|
|
@@ -544,13 +797,22 @@ export async function runInteractiveMode(options) {
|
|
|
544
797
|
}
|
|
545
798
|
const promptUser = () => {
|
|
546
799
|
return new Promise((resolve) => {
|
|
547
|
-
|
|
800
|
+
clearBelowArea(below, 0);
|
|
801
|
+
resetMenu(menu);
|
|
802
|
+
multiLineBuffer.length = 0;
|
|
803
|
+
below.active = true;
|
|
804
|
+
below.tip = getRandomTip(session);
|
|
805
|
+
console.log();
|
|
548
806
|
printBorder();
|
|
549
807
|
rl.question(chalk.cyan(" › "), (answer) => {
|
|
550
|
-
|
|
808
|
+
clearBelowArea(below, getCursorCol());
|
|
809
|
+
below.active = false;
|
|
551
810
|
printBorder();
|
|
552
811
|
resolve(answer);
|
|
553
812
|
});
|
|
813
|
+
// Render placeholder + underline + status below input
|
|
814
|
+
showPlaceholder();
|
|
815
|
+
renderBelowArea(menu, below, session, PROMPT_WIDTH);
|
|
554
816
|
});
|
|
555
817
|
};
|
|
556
818
|
process.on("SIGINT", () => {
|
|
@@ -563,11 +825,27 @@ export async function runInteractiveMode(options) {
|
|
|
563
825
|
process.stdout.write(chalk.yellow("\n⚡ 已中止 (Ctrl+C)\n"));
|
|
564
826
|
printSessionTip(session);
|
|
565
827
|
}
|
|
828
|
+
else if (multiLineBuffer.length > 0) {
|
|
829
|
+
clearBelowArea(below, getCursorCol());
|
|
830
|
+
for (let i = 0; i < multiLineBuffer.length; i++) {
|
|
831
|
+
process.stdout.write("\x1b[1A\x1b[2K");
|
|
832
|
+
}
|
|
833
|
+
process.stdout.write("\r\x1b[K");
|
|
834
|
+
multiLineBuffer.length = 0;
|
|
835
|
+
rl.line = "";
|
|
836
|
+
rl.cursor = 0;
|
|
837
|
+
rl._prompt = chalk.cyan(" › ");
|
|
838
|
+
process.stdout.write(chalk.cyan(" › "));
|
|
839
|
+
showPlaceholder();
|
|
840
|
+
renderBelowArea(menu, below, session, PROMPT_WIDTH);
|
|
841
|
+
}
|
|
566
842
|
else {
|
|
567
843
|
console.log(chalk.dim("\n(输入 /exit 或 Ctrl+D 退出)"));
|
|
568
844
|
}
|
|
569
845
|
});
|
|
570
846
|
rl.on("close", () => {
|
|
847
|
+
clearBelowArea(below, getCursorCol());
|
|
848
|
+
below.active = false;
|
|
571
849
|
printExitMessage(session).finally(() => process.exit(0));
|
|
572
850
|
});
|
|
573
851
|
// Main REPL loop
|
|
@@ -585,9 +863,10 @@ export async function runInteractiveMode(options) {
|
|
|
585
863
|
continue;
|
|
586
864
|
}
|
|
587
865
|
try {
|
|
588
|
-
// Expand @path references into file contents
|
|
589
866
|
const { expandedText, fileContent } = extractAtReferences(trimmed, session.cwd);
|
|
590
|
-
const finalInput = fileContent
|
|
867
|
+
const finalInput = fileContent
|
|
868
|
+
? `${expandedText}\n\n<referenced_files>\n${fileContent}</referenced_files>`
|
|
869
|
+
: expandedText;
|
|
591
870
|
await session.prompt(finalInput);
|
|
592
871
|
}
|
|
593
872
|
catch (err) {
|
|
@@ -612,7 +891,6 @@ async function handleCommand(input, session) {
|
|
|
612
891
|
break;
|
|
613
892
|
case "/model": {
|
|
614
893
|
const sub = args[0];
|
|
615
|
-
// /model — show current + configured models
|
|
616
894
|
if (!sub) {
|
|
617
895
|
const cfg = await loadConfig();
|
|
618
896
|
const configuredModels = cfg.models ?? {};
|
|
@@ -639,7 +917,6 @@ async function handleCommand(input, session) {
|
|
|
639
917
|
console.log(chalk.dim(` /model rm <id> 删除模型\n`));
|
|
640
918
|
break;
|
|
641
919
|
}
|
|
642
|
-
// /model default <id> — set persistent default
|
|
643
920
|
if (sub === "default") {
|
|
644
921
|
const modelId = args[1];
|
|
645
922
|
if (!modelId) {
|
|
@@ -653,7 +930,6 @@ async function handleCommand(input, session) {
|
|
|
653
930
|
console.log(chalk.green(`\n 默认模型已设为: ${modelId} (已同步切换)\n`));
|
|
654
931
|
break;
|
|
655
932
|
}
|
|
656
|
-
// /model add <id> [baseUrl] [apiKey]
|
|
657
933
|
if (sub === "add") {
|
|
658
934
|
const modelId = args[1];
|
|
659
935
|
if (!modelId) {
|
|
@@ -675,7 +951,6 @@ async function handleCommand(input, session) {
|
|
|
675
951
|
console.log(chalk.dim(` 切换使用: /model ${modelId}\n`));
|
|
676
952
|
break;
|
|
677
953
|
}
|
|
678
|
-
// /model rm <id>
|
|
679
954
|
if (sub === "rm") {
|
|
680
955
|
const modelId = args[1];
|
|
681
956
|
if (!modelId) {
|
|
@@ -692,7 +967,6 @@ async function handleCommand(input, session) {
|
|
|
692
967
|
console.log(chalk.green(`\n 已删除: ${modelId}\n`));
|
|
693
968
|
break;
|
|
694
969
|
}
|
|
695
|
-
// /model <id> — switch model
|
|
696
970
|
try {
|
|
697
971
|
session.switchModel(sub);
|
|
698
972
|
console.log(chalk.green(`\n 已切换到: ${sub}\n`));
|
|
@@ -797,61 +1071,334 @@ async function handleCommand(input, session) {
|
|
|
797
1071
|
break;
|
|
798
1072
|
}
|
|
799
1073
|
case "/skills": {
|
|
800
|
-
const
|
|
801
|
-
if (
|
|
802
|
-
|
|
1074
|
+
const sub = args[0];
|
|
1075
|
+
if (!sub) {
|
|
1076
|
+
const skills = session.skills;
|
|
1077
|
+
const cfg = await loadConfig();
|
|
1078
|
+
const disabledSkills = cfg.disabledSkills ?? [];
|
|
1079
|
+
const totalCount = skills.length + disabledSkills.length;
|
|
1080
|
+
if (totalCount === 0) {
|
|
1081
|
+
console.log(chalk.dim("\n 暂无技能。将 SKILL.md 文件放入 ~/.til/skills/ 或 .til/skills/ 即可加载。\n"));
|
|
1082
|
+
}
|
|
1083
|
+
else {
|
|
1084
|
+
const sourceLabel = { builtin: "内置", user: "用户", project: "项目", path: "路径" };
|
|
1085
|
+
console.log(chalk.bold(`\n已加载技能 (${skills.length}):\n`));
|
|
1086
|
+
for (const s of skills) {
|
|
1087
|
+
const tag = chalk.dim(`[${sourceLabel[s.source] || s.source}]`);
|
|
1088
|
+
console.log(` ${chalk.green("●")} ${chalk.cyan(s.name.padEnd(24))} ${tag} ${chalk.dim(s.description.slice(0, 50))}`);
|
|
1089
|
+
}
|
|
1090
|
+
if (disabledSkills.length > 0) {
|
|
1091
|
+
console.log(chalk.bold(`\n已禁用技能 (${disabledSkills.length}):\n`));
|
|
1092
|
+
for (const name of disabledSkills) {
|
|
1093
|
+
console.log(` ${chalk.red("○")} ${chalk.dim(name)}`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
console.log(chalk.dim("\n /skill:<名称> 查看技能详情"));
|
|
1098
|
+
console.log(chalk.dim(" /skills reload 重新扫描并加载技能"));
|
|
1099
|
+
console.log(chalk.dim(" /skills install <source> 安装技能 (npx skills add)"));
|
|
1100
|
+
console.log(chalk.dim(" /skills enable <name> 启用已禁用的技能"));
|
|
1101
|
+
console.log(chalk.dim(" /skills disable <name> 禁用技能"));
|
|
1102
|
+
console.log(chalk.dim(" /skills rm <name> 删除技能文件\n"));
|
|
1103
|
+
break;
|
|
803
1104
|
}
|
|
804
|
-
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
|
|
1105
|
+
if (sub === "reload") {
|
|
1106
|
+
const reloadSpinner = new Spinner("正在重新加载技能...");
|
|
1107
|
+
reloadSpinner.start();
|
|
1108
|
+
try {
|
|
1109
|
+
await session.reloadSkills();
|
|
1110
|
+
reloadSpinner.stop(chalk.green(` ✓ 技能已重载 (${session.skills.length} 个)`));
|
|
1111
|
+
}
|
|
1112
|
+
catch (err) {
|
|
1113
|
+
reloadSpinner.stop(chalk.red(` ✗ 重载失败: ${err.message}`));
|
|
1114
|
+
}
|
|
1115
|
+
console.log();
|
|
1116
|
+
break;
|
|
813
1117
|
}
|
|
1118
|
+
if (sub === "install") {
|
|
1119
|
+
const source = args[1];
|
|
1120
|
+
if (!source) {
|
|
1121
|
+
console.log(chalk.red("\n 用法: /skills install <source>"));
|
|
1122
|
+
console.log(chalk.dim(" 示例: /skills install ivangdavila/excel-xlsx\n"));
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
console.log(chalk.dim(`\n 正在安装技能: ${source}...`));
|
|
1126
|
+
const { execSync } = await import("node:child_process");
|
|
1127
|
+
try {
|
|
1128
|
+
const output = execSync(`npx skills add ${source} -g -y`, {
|
|
1129
|
+
encoding: "utf-8",
|
|
1130
|
+
timeout: 60000,
|
|
1131
|
+
cwd: session.cwd,
|
|
1132
|
+
});
|
|
1133
|
+
console.log(chalk.green(` ✓ 安装完成`));
|
|
1134
|
+
if (output.trim())
|
|
1135
|
+
console.log(chalk.dim(` ${output.trim()}`));
|
|
1136
|
+
await session.reloadSkills();
|
|
1137
|
+
console.log(chalk.dim(` 已重载 (${session.skills.length} 个技能)\n`));
|
|
1138
|
+
}
|
|
1139
|
+
catch (err) {
|
|
1140
|
+
console.log(chalk.red(` ✗ 安装失败: ${err.message}\n`));
|
|
1141
|
+
}
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
if (sub === "enable") {
|
|
1145
|
+
const name = args[1];
|
|
1146
|
+
if (!name) {
|
|
1147
|
+
console.log(chalk.red("\n 用法: /skills enable <name>\n"));
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
const cfg = await loadConfig();
|
|
1151
|
+
const disabled = cfg.disabledSkills ?? [];
|
|
1152
|
+
if (!disabled.includes(name)) {
|
|
1153
|
+
console.log(chalk.yellow(`\n 技能 "${name}" 未被禁用\n`));
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
cfg.disabledSkills = disabled.filter((n) => n !== name);
|
|
1157
|
+
await saveConfig(cfg);
|
|
1158
|
+
await session.reloadSkills();
|
|
1159
|
+
console.log(chalk.green(`\n 已启用技能: ${name} (当前 ${session.skills.length} 个)\n`));
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
if (sub === "disable") {
|
|
1163
|
+
const name = args[1];
|
|
1164
|
+
if (!name) {
|
|
1165
|
+
console.log(chalk.red("\n 用法: /skills disable <name>\n"));
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
const skill = session.skills.find((s) => s.name === name);
|
|
1169
|
+
if (!skill) {
|
|
1170
|
+
console.log(chalk.yellow(`\n 技能 "${name}" 未找到或已禁用\n`));
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
const cfg = await loadConfig();
|
|
1174
|
+
if (!cfg.disabledSkills)
|
|
1175
|
+
cfg.disabledSkills = [];
|
|
1176
|
+
if (!cfg.disabledSkills.includes(name)) {
|
|
1177
|
+
cfg.disabledSkills.push(name);
|
|
1178
|
+
}
|
|
1179
|
+
await saveConfig(cfg);
|
|
1180
|
+
await session.reloadSkills();
|
|
1181
|
+
console.log(chalk.green(`\n 已禁用技能: ${name} (当前 ${session.skills.length} 个)\n`));
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
if (sub === "rm") {
|
|
1185
|
+
const name = args[1];
|
|
1186
|
+
if (!name) {
|
|
1187
|
+
console.log(chalk.red("\n 用法: /skills rm <name>\n"));
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
const { loadSkills: loadAllSkills } = await import("../core/skills.js");
|
|
1191
|
+
const allResult = loadAllSkills({ cwd: session.cwd, includeDefaults: true });
|
|
1192
|
+
const target = allResult.skills.find((s) => s.name === name);
|
|
1193
|
+
if (!target) {
|
|
1194
|
+
console.log(chalk.yellow(`\n 技能 "${name}" 未找到\n`));
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
if (target.source === "builtin") {
|
|
1198
|
+
console.log(chalk.red(`\n 内置技能不能删除,使用 /skills disable ${name} 禁用\n`));
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
const { unlink } = await import("node:fs/promises");
|
|
1202
|
+
try {
|
|
1203
|
+
await unlink(target.filePath);
|
|
1204
|
+
console.log(chalk.green(`\n 已删除: ${target.filePath}`));
|
|
1205
|
+
// Also remove from disabled list if present
|
|
1206
|
+
const cfg = await loadConfig();
|
|
1207
|
+
if (cfg.disabledSkills?.includes(name)) {
|
|
1208
|
+
cfg.disabledSkills = cfg.disabledSkills.filter((n) => n !== name);
|
|
1209
|
+
await saveConfig(cfg);
|
|
1210
|
+
}
|
|
1211
|
+
await session.reloadSkills();
|
|
1212
|
+
console.log(chalk.dim(` 已重载 (${session.skills.length} 个技能)\n`));
|
|
1213
|
+
}
|
|
1214
|
+
catch (err) {
|
|
1215
|
+
console.log(chalk.red(`\n 删除失败: ${err.message}\n`));
|
|
1216
|
+
}
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
console.log(chalk.yellow(`\n 未知子命令: ${sub}。使用 /skills 查看帮助。\n`));
|
|
814
1220
|
break;
|
|
815
1221
|
}
|
|
816
1222
|
case "/mcp": {
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1223
|
+
const sub = args[0];
|
|
1224
|
+
if (!sub) {
|
|
1225
|
+
// Display MCP status
|
|
1226
|
+
const cfg = await loadConfig();
|
|
1227
|
+
const globalMcp = cfg.mcpServers ?? {};
|
|
1228
|
+
const projectMcp = loadProjectMcpServers(session.cwd);
|
|
1229
|
+
const mergedMcp = { ...globalMcp, ...projectMcp };
|
|
1230
|
+
const serverNames = Object.keys(mergedMcp);
|
|
1231
|
+
if (serverNames.length === 0) {
|
|
1232
|
+
console.log(chalk.dim("\n 暂无 MCP 服务。\n"));
|
|
1233
|
+
console.log(chalk.dim(" 使用 /mcp add 添加服务,或在 ~/.til/config.json 中配置 mcpServers。\n"));
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
const mcpExt = session.extensionRunner?.loadedExtensions.find((e) => e.name === "mcp");
|
|
1237
|
+
const mcpTools = mcpExt ? [...mcpExt.tools.keys()] : [];
|
|
1238
|
+
console.log(chalk.bold(`\nMCP 服务 (${serverNames.length}):\n`));
|
|
1239
|
+
for (const name of serverNames) {
|
|
1240
|
+
const server = mergedMcp[name];
|
|
1241
|
+
const isProject = name in projectMcp;
|
|
1242
|
+
const rawTransport = server.transport ?? server.type;
|
|
1243
|
+
const transport = (rawTransport === "http" || rawTransport === "streamable_http" || rawTransport === "sse")
|
|
1244
|
+
? "http"
|
|
1245
|
+
: rawTransport === "stdio" ? "stdio"
|
|
1246
|
+
: server.url ? "http" : "stdio";
|
|
1247
|
+
const serverTools = mcpTools.filter((t) => t.startsWith(`mcp_${name}_`));
|
|
1248
|
+
const status = serverTools.length > 0 ? chalk.green("✓ 已连接") : chalk.red("✗ 未连接");
|
|
1249
|
+
const scope = isProject ? chalk.yellow("[项目]") : chalk.dim("[全局]");
|
|
1250
|
+
console.log(` ${chalk.cyan(name.padEnd(20))} ${status} ${chalk.dim(transport)} ${scope}`);
|
|
1251
|
+
if (server.url)
|
|
1252
|
+
console.log(` ${chalk.dim("URL:")} ${server.url}`);
|
|
1253
|
+
if (server.command)
|
|
1254
|
+
console.log(` ${chalk.dim("CMD:")} ${server.command} ${(server.args ?? []).join(" ")}`);
|
|
1255
|
+
if (serverTools.length > 0) {
|
|
1256
|
+
console.log(` ${chalk.dim(`工具 (${serverTools.length}):`)}`);
|
|
1257
|
+
for (const toolKey of serverTools) {
|
|
1258
|
+
const shortName = toolKey.replace(`mcp_${name}_`, "");
|
|
1259
|
+
const toolDef = mcpExt?.tools.get(toolKey);
|
|
1260
|
+
const desc = toolDef ? chalk.dim(` — ${(toolDef.description ?? "").slice(0, 50)}`) : "";
|
|
1261
|
+
console.log(` ${chalk.white(shortName)}${desc}`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
console.log();
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
console.log(chalk.dim(` /mcp add <name> --stdio <cmd> [args...] 添加 stdio 服务`));
|
|
1268
|
+
console.log(chalk.dim(` /mcp add <name> --url <url> [-H key:value ...] 添加 HTTP 服务`));
|
|
1269
|
+
console.log(chalk.dim(` /mcp add <name> ... --project 添加到项目级配置`));
|
|
1270
|
+
console.log(chalk.dim(` /mcp rm <name> 删除服务`));
|
|
1271
|
+
console.log(chalk.dim(` /mcp reload 重载所有服务\n`));
|
|
1272
|
+
break;
|
|
824
1273
|
}
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
if (
|
|
842
|
-
console.log(
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1274
|
+
if (sub === "add") {
|
|
1275
|
+
const name = args[1];
|
|
1276
|
+
if (!name) {
|
|
1277
|
+
console.log(chalk.red("\n 用法:"));
|
|
1278
|
+
console.log(chalk.dim(" /mcp add <name> --stdio <command> [args...]"));
|
|
1279
|
+
console.log(chalk.dim(" /mcp add <name> --url <url> [-H key:value ...]"));
|
|
1280
|
+
console.log(chalk.dim(" 添加 --project 存入项目级配置\n"));
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
const restArgs = args.slice(2);
|
|
1284
|
+
const isProject = restArgs.includes("--project");
|
|
1285
|
+
const filteredArgs = restArgs.filter((a) => a !== "--project");
|
|
1286
|
+
let serverConfig = null;
|
|
1287
|
+
if (filteredArgs.includes("--stdio")) {
|
|
1288
|
+
const stdioIdx = filteredArgs.indexOf("--stdio");
|
|
1289
|
+
const command = filteredArgs[stdioIdx + 1];
|
|
1290
|
+
if (!command) {
|
|
1291
|
+
console.log(chalk.red("\n --stdio 后需要指定命令\n"));
|
|
1292
|
+
break;
|
|
1293
|
+
}
|
|
1294
|
+
const cmdArgs = filteredArgs.slice(stdioIdx + 2).filter((a) => !a.startsWith("-H"));
|
|
1295
|
+
const env = {};
|
|
1296
|
+
// Parse -E key=value for env vars
|
|
1297
|
+
for (let i = 0; i < filteredArgs.length; i++) {
|
|
1298
|
+
if (filteredArgs[i] === "-E" && filteredArgs[i + 1]) {
|
|
1299
|
+
const [k, ...vParts] = filteredArgs[i + 1].split("=");
|
|
1300
|
+
if (k)
|
|
1301
|
+
env[k] = vParts.join("=");
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
serverConfig = {
|
|
1305
|
+
transport: "stdio",
|
|
1306
|
+
command,
|
|
1307
|
+
args: cmdArgs.length > 0 ? cmdArgs : undefined,
|
|
1308
|
+
env: Object.keys(env).length > 0 ? env : undefined,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
else if (filteredArgs.includes("--url")) {
|
|
1312
|
+
const urlIdx = filteredArgs.indexOf("--url");
|
|
1313
|
+
const url = filteredArgs[urlIdx + 1];
|
|
1314
|
+
if (!url) {
|
|
1315
|
+
console.log(chalk.red("\n --url 后需要指定 URL\n"));
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
const headers = {};
|
|
1319
|
+
for (let i = 0; i < filteredArgs.length; i++) {
|
|
1320
|
+
if (filteredArgs[i] === "-H" && filteredArgs[i + 1]) {
|
|
1321
|
+
const colonIdx = filteredArgs[i + 1].indexOf(":");
|
|
1322
|
+
if (colonIdx > 0) {
|
|
1323
|
+
const key = filteredArgs[i + 1].slice(0, colonIdx).trim();
|
|
1324
|
+
const value = filteredArgs[i + 1].slice(colonIdx + 1).trim();
|
|
1325
|
+
headers[key] = value;
|
|
1326
|
+
}
|
|
850
1327
|
}
|
|
851
1328
|
}
|
|
852
|
-
|
|
1329
|
+
serverConfig = {
|
|
1330
|
+
transport: "streamable_http",
|
|
1331
|
+
url,
|
|
1332
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
1333
|
+
};
|
|
853
1334
|
}
|
|
1335
|
+
else {
|
|
1336
|
+
console.log(chalk.red("\n 需要指定 --stdio 或 --url\n"));
|
|
1337
|
+
break;
|
|
1338
|
+
}
|
|
1339
|
+
if (serverConfig) {
|
|
1340
|
+
if (isProject) {
|
|
1341
|
+
const projectServers = loadProjectMcpServers(session.cwd);
|
|
1342
|
+
projectServers[name] = serverConfig;
|
|
1343
|
+
await saveProjectMcpServers(session.cwd, projectServers);
|
|
1344
|
+
console.log(chalk.green(`\n 已添加 MCP 服务: ${name} (项目级)`));
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
const cfg = await loadConfig();
|
|
1348
|
+
if (!cfg.mcpServers)
|
|
1349
|
+
cfg.mcpServers = {};
|
|
1350
|
+
cfg.mcpServers[name] = serverConfig;
|
|
1351
|
+
await saveConfig(cfg);
|
|
1352
|
+
console.log(chalk.green(`\n 已添加 MCP 服务: ${name} (全局)`));
|
|
1353
|
+
}
|
|
1354
|
+
console.log(chalk.dim(` 使用 /mcp reload 激活\n`));
|
|
1355
|
+
}
|
|
1356
|
+
break;
|
|
854
1357
|
}
|
|
1358
|
+
if (sub === "rm") {
|
|
1359
|
+
const name = args[1];
|
|
1360
|
+
if (!name) {
|
|
1361
|
+
console.log(chalk.red("\n 用法: /mcp rm <name>\n"));
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1364
|
+
let removed = false;
|
|
1365
|
+
const cfg = await loadConfig();
|
|
1366
|
+
if (cfg.mcpServers?.[name]) {
|
|
1367
|
+
delete cfg.mcpServers[name];
|
|
1368
|
+
await saveConfig(cfg);
|
|
1369
|
+
removed = true;
|
|
1370
|
+
}
|
|
1371
|
+
const projectServers = loadProjectMcpServers(session.cwd);
|
|
1372
|
+
if (projectServers[name]) {
|
|
1373
|
+
delete projectServers[name];
|
|
1374
|
+
await saveProjectMcpServers(session.cwd, projectServers);
|
|
1375
|
+
removed = true;
|
|
1376
|
+
}
|
|
1377
|
+
if (removed) {
|
|
1378
|
+
console.log(chalk.green(`\n 已删除 MCP 服务: ${name}`));
|
|
1379
|
+
console.log(chalk.dim(` 使用 /mcp reload 使变更生效\n`));
|
|
1380
|
+
}
|
|
1381
|
+
else {
|
|
1382
|
+
console.log(chalk.red(`\n MCP 服务 "${name}" 不存在\n`));
|
|
1383
|
+
}
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
if (sub === "reload") {
|
|
1387
|
+
const reloadSpinner = new Spinner("正在重载 MCP 服务...");
|
|
1388
|
+
reloadSpinner.start();
|
|
1389
|
+
try {
|
|
1390
|
+
await session.reloadMcpServers();
|
|
1391
|
+
const mcpExt = session.extensionRunner?.loadedExtensions.find((e) => e.name === "mcp");
|
|
1392
|
+
const toolCount = mcpExt ? mcpExt.tools.size : 0;
|
|
1393
|
+
reloadSpinner.stop(chalk.green(` ✓ MCP 服务已重载 (${toolCount} 个工具)`));
|
|
1394
|
+
}
|
|
1395
|
+
catch (err) {
|
|
1396
|
+
reloadSpinner.stop(chalk.red(` ✗ 重载失败: ${err.message}`));
|
|
1397
|
+
}
|
|
1398
|
+
console.log();
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
console.log(chalk.yellow(`\n 未知子命令: ${sub}。使用 /mcp 查看帮助。\n`));
|
|
855
1402
|
break;
|
|
856
1403
|
}
|
|
857
1404
|
case "/extensions": {
|
|
@@ -913,7 +1460,6 @@ async function handleCommand(input, session) {
|
|
|
913
1460
|
}
|
|
914
1461
|
break;
|
|
915
1462
|
}
|
|
916
|
-
// Try extension-registered commands
|
|
917
1463
|
const extCmd = cmd.slice(1);
|
|
918
1464
|
const extCommands = session.getExtensionCommands();
|
|
919
1465
|
if (extCommands.has(extCmd)) {
|
|
@@ -1014,7 +1560,7 @@ function countVisualLines(text, termWidth) {
|
|
|
1014
1560
|
}
|
|
1015
1561
|
function printBanner(session) {
|
|
1016
1562
|
const modelId = session.model.name || session.model.id;
|
|
1017
|
-
const dir = session.cwd
|
|
1563
|
+
const dir = shortenDir(session.cwd);
|
|
1018
1564
|
const sm = session.sessionManager;
|
|
1019
1565
|
console.log();
|
|
1020
1566
|
console.log(chalk.bold.cyan(">_") + " " + chalk.bold("欢迎使用千岛湖 Til-Work") + " " + chalk.dim(`v${VERSION}`));
|
|
@@ -1036,21 +1582,7 @@ function printBanner(session) {
|
|
|
1036
1582
|
const totalTools = extInfo.reduce((sum, e) => sum + e.toolCount, 0);
|
|
1037
1583
|
console.log(` ${chalk.dim("extensions")} ${chalk.white(String(extInfo.length))} 个已加载 (${totalTools} 工具)`);
|
|
1038
1584
|
}
|
|
1039
|
-
const
|
|
1040
|
-
"输入 / 查看所有可用命令,方向键选择",
|
|
1041
|
-
"使用 /model 切换模型",
|
|
1042
|
-
"按 Ctrl+C 或 Esc 中止正在执行的请求",
|
|
1043
|
-
"使用 /memory 查看记忆内容",
|
|
1044
|
-
"使用 /sessions 查看历史会话",
|
|
1045
|
-
"输入 @ 引用文件内容(支持搜索和方向键选择)",
|
|
1046
|
-
];
|
|
1047
|
-
if (session.skills.length > 0) {
|
|
1048
|
-
tips.push("使用 /skills 查看已加载的技能");
|
|
1049
|
-
}
|
|
1050
|
-
if (extInfo.length > 0) {
|
|
1051
|
-
tips.push("使用 /extensions 查看已加载的扩展");
|
|
1052
|
-
}
|
|
1053
|
-
const tip = tips[Math.floor(Math.random() * tips.length)];
|
|
1585
|
+
const tip = getRandomTip(session);
|
|
1054
1586
|
console.log(`\n ${chalk.bold("Tip:")} ${chalk.italic(tip)}\n`);
|
|
1055
1587
|
}
|
|
1056
1588
|
//# sourceMappingURL=interactive.js.map
|