@yuaone/cli 0.3.4 → 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/dist/cli.js +9 -6
- package/dist/cli.js.map +1 -1
- package/dist/commands/index.d.ts +73 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +872 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -17
- package/dist/config.js.map +1 -1
- package/dist/interactive.d.ts +0 -15
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +42 -134
- package/dist/interactive.js.map +1 -1
- package/dist/oneshot.js.map +1 -1
- package/dist/renderer.d.ts +1 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +24 -3
- package/dist/renderer.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +40 -72
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/agent-bridge.d.ts +6 -0
- package/dist/tui/agent-bridge.d.ts.map +1 -1
- package/dist/tui/agent-bridge.js +100 -3
- package/dist/tui/agent-bridge.js.map +1 -1
- package/dist/tui/hooks/useSlashCommands.d.ts.map +1 -1
- package/dist/tui/hooks/useSlashCommands.js +7 -15
- package/dist/tui/hooks/useSlashCommands.js.map +1 -1
- package/dist/tui/lib/ansi.d.ts +1 -1
- package/dist/tui/lib/ansi.d.ts.map +1 -1
- package/dist/tui/lib/ansi.js +5 -1
- package/dist/tui/lib/ansi.js.map +1 -1
- package/dist/tui/lib/update-checker.d.ts +1 -1
- package/dist/tui/lib/update-checker.d.ts.map +1 -1
- package/dist/tui/lib/update-checker.js +12 -1
- package/dist/tui/lib/update-checker.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YUAN CLI — Unified Command Dispatcher
|
|
3
|
+
*
|
|
4
|
+
* 통합 커맨드 핸들러. TUI/Classic 양쪽에서 동일 로직 사용.
|
|
5
|
+
* 한 번 구현 → 양쪽 동작.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { loadSettings, saveSettings } from "../tui/lib/update-checker.js";
|
|
12
|
+
/* ──────────────────────────────────────────
|
|
13
|
+
Core Commands (P1)
|
|
14
|
+
────────────────────────────────────────── */
|
|
15
|
+
const help = (ctx) => {
|
|
16
|
+
const lines = [
|
|
17
|
+
"Available commands:",
|
|
18
|
+
"",
|
|
19
|
+
" Core",
|
|
20
|
+
" /help — Show this help",
|
|
21
|
+
" /status — Provider, model, tokens, session info",
|
|
22
|
+
" /clear — Clear conversation",
|
|
23
|
+
" /config — Show current configuration",
|
|
24
|
+
" /session — Session info",
|
|
25
|
+
" /diff [N] — Show file changes (N = context lines, default 5)",
|
|
26
|
+
" /undo — Undo last file change",
|
|
27
|
+
" /model [m] — Show or change model",
|
|
28
|
+
" /mode [m] — Show or change agent mode",
|
|
29
|
+
" /settings — Auto-update preferences",
|
|
30
|
+
" /exit — Exit YUAN",
|
|
31
|
+
"",
|
|
32
|
+
" Extended",
|
|
33
|
+
" /cost — Token usage & estimated cost",
|
|
34
|
+
" /compact — Compress context history",
|
|
35
|
+
" /approve — Approve pending action",
|
|
36
|
+
" /reject — Reject pending action",
|
|
37
|
+
"",
|
|
38
|
+
" Advanced",
|
|
39
|
+
" /tools — List available tools",
|
|
40
|
+
" /memory — Show YUAN.md learnings",
|
|
41
|
+
" /retry — Retry last failed action",
|
|
42
|
+
"",
|
|
43
|
+
" Plugin System",
|
|
44
|
+
" /plugins — Plugin management (install/remove/search)",
|
|
45
|
+
" /skills — Available skills (tree view)",
|
|
46
|
+
"",
|
|
47
|
+
" yuaone.com",
|
|
48
|
+
];
|
|
49
|
+
return { output: lines.join("\n") };
|
|
50
|
+
};
|
|
51
|
+
const status = (ctx) => {
|
|
52
|
+
const { agentInfo, filesChanged } = ctx;
|
|
53
|
+
const lines = [
|
|
54
|
+
`YUAN v${ctx.version}`,
|
|
55
|
+
` Provider : ${ctx.provider}`,
|
|
56
|
+
` Model : ${ctx.model}`,
|
|
57
|
+
` Status : ${agentInfo.status}`,
|
|
58
|
+
` Messages : ${agentInfo.messageCount}`,
|
|
59
|
+
` Tokens : ${agentInfo.totalTokens.toLocaleString()} total`,
|
|
60
|
+
agentInfo.tokensPerSecond > 0 ? ` Speed : ${agentInfo.tokensPerSecond} tok/s` : "",
|
|
61
|
+
` Files : ${filesChanged.length} changed`,
|
|
62
|
+
"",
|
|
63
|
+
" yuaone.com",
|
|
64
|
+
];
|
|
65
|
+
return { output: lines.filter(Boolean).join("\n") };
|
|
66
|
+
};
|
|
67
|
+
const config = (ctx) => {
|
|
68
|
+
return { output: ctx.config.show() };
|
|
69
|
+
};
|
|
70
|
+
const session = (ctx) => {
|
|
71
|
+
const { sessionInfo, agentInfo } = ctx;
|
|
72
|
+
const lines = [
|
|
73
|
+
"Session Info",
|
|
74
|
+
" " + "-".repeat(40),
|
|
75
|
+
` ID : ${sessionInfo.id}`,
|
|
76
|
+
` Created : ${new Date(sessionInfo.createdAt).toLocaleString()}`,
|
|
77
|
+
` Messages : ${agentInfo.messageCount}`,
|
|
78
|
+
` Work Dir : ${ctx.workDir}`,
|
|
79
|
+
];
|
|
80
|
+
return { output: lines.join("\n") };
|
|
81
|
+
};
|
|
82
|
+
const diff = (ctx, args) => {
|
|
83
|
+
// /diff [context_lines] — default 5, Claude Code style
|
|
84
|
+
const contextLines = args.length > 0 ? parseInt(args[0], 10) || 5 : 5;
|
|
85
|
+
try {
|
|
86
|
+
const diffOutput = execFileSync("git", ["diff", `-U${contextLines}`], {
|
|
87
|
+
cwd: ctx.workDir,
|
|
88
|
+
stdio: "pipe",
|
|
89
|
+
encoding: "utf-8",
|
|
90
|
+
maxBuffer: 1024 * 1024,
|
|
91
|
+
});
|
|
92
|
+
if (!diffOutput.trim()) {
|
|
93
|
+
const stagedOutput = execFileSync("git", ["diff", "--cached", `-U${contextLines}`], {
|
|
94
|
+
cwd: ctx.workDir,
|
|
95
|
+
stdio: "pipe",
|
|
96
|
+
encoding: "utf-8",
|
|
97
|
+
maxBuffer: 1024 * 1024,
|
|
98
|
+
});
|
|
99
|
+
if (!stagedOutput.trim()) {
|
|
100
|
+
// Fall back to session file list
|
|
101
|
+
if (ctx.filesChanged.length > 0) {
|
|
102
|
+
return { output: `Changed files (this session):\n${ctx.filesChanged.map(f => ` ${f}`).join("\n")}` };
|
|
103
|
+
}
|
|
104
|
+
return { output: "No file changes detected (working tree clean)." };
|
|
105
|
+
}
|
|
106
|
+
return { output: `Staged Changes:\n${stagedOutput}` };
|
|
107
|
+
}
|
|
108
|
+
return { output: `Working Directory Changes:\n${diffOutput}` };
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Fallback: show session-tracked files
|
|
112
|
+
if (ctx.filesChanged.length > 0) {
|
|
113
|
+
return { output: `Changed files (this session):\n${ctx.filesChanged.map(f => ` ${f}`).join("\n")}` };
|
|
114
|
+
}
|
|
115
|
+
return { output: "Not a git repository or git not available." };
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const undo = (ctx) => {
|
|
119
|
+
if (ctx.filesChanged.length === 0) {
|
|
120
|
+
return { output: "No file changes to undo in this session." };
|
|
121
|
+
}
|
|
122
|
+
const lastFile = ctx.filesChanged[ctx.filesChanged.length - 1];
|
|
123
|
+
try {
|
|
124
|
+
execFileSync("git", ["checkout", "--", lastFile], {
|
|
125
|
+
cwd: ctx.workDir,
|
|
126
|
+
stdio: "pipe",
|
|
127
|
+
});
|
|
128
|
+
ctx.filesChanged.pop();
|
|
129
|
+
return { output: `Reverted: ${lastFile}` };
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
try {
|
|
133
|
+
const backupPath = `${lastFile}.yuan-backup`;
|
|
134
|
+
fs.renameSync(backupPath, lastFile);
|
|
135
|
+
ctx.filesChanged.pop();
|
|
136
|
+
return { output: `Restored from backup: ${lastFile}` };
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return { output: `Cannot undo: ${lastFile} — not in git and no backup found.` };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const model = (ctx, args) => {
|
|
144
|
+
if (args.length === 0) {
|
|
145
|
+
const validModels = [
|
|
146
|
+
"YUA: yua-basic, yua-normal, yua-pro, yua-research",
|
|
147
|
+
"OpenAI: gpt-4o-mini, gpt-4o, gpt-4.1-mini",
|
|
148
|
+
"Claude: claude-sonnet-4-20250514, claude-haiku-4-5-20251001",
|
|
149
|
+
];
|
|
150
|
+
return {
|
|
151
|
+
output: [
|
|
152
|
+
`Current model: ${ctx.model}`,
|
|
153
|
+
"",
|
|
154
|
+
"Available models:",
|
|
155
|
+
...validModels.map(m => ` ${m}`),
|
|
156
|
+
"",
|
|
157
|
+
"Usage: /model <name>",
|
|
158
|
+
].join("\n"),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const newModel = args[0];
|
|
162
|
+
if (ctx.onModelChange) {
|
|
163
|
+
ctx.onModelChange(newModel);
|
|
164
|
+
return { output: `Model changed to: ${newModel}` };
|
|
165
|
+
}
|
|
166
|
+
return { output: "Model change not supported in this mode." };
|
|
167
|
+
};
|
|
168
|
+
const VALID_MODES = [
|
|
169
|
+
"code", "review", "security", "debug", "refactor",
|
|
170
|
+
"test", "plan", "architect", "report",
|
|
171
|
+
];
|
|
172
|
+
const mode = (ctx, args) => {
|
|
173
|
+
if (args.length === 0) {
|
|
174
|
+
return {
|
|
175
|
+
output: [
|
|
176
|
+
"Agent Modes:",
|
|
177
|
+
"",
|
|
178
|
+
" code — Autonomous coding (default)",
|
|
179
|
+
" review — Code review (read-only)",
|
|
180
|
+
" security — Security audit (OWASP)",
|
|
181
|
+
" debug — Systematic debugging",
|
|
182
|
+
" refactor — Code refactoring",
|
|
183
|
+
" test — Test generation/execution",
|
|
184
|
+
" plan — Task planning (read-only)",
|
|
185
|
+
" architect — Architecture analysis",
|
|
186
|
+
" report — Analysis report",
|
|
187
|
+
"",
|
|
188
|
+
"Usage: /mode <name>",
|
|
189
|
+
].join("\n"),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const newMode = args[0].toLowerCase();
|
|
193
|
+
if (!VALID_MODES.includes(newMode)) {
|
|
194
|
+
return { output: `Unknown mode: ${newMode}. Valid: ${VALID_MODES.join(", ")}` };
|
|
195
|
+
}
|
|
196
|
+
if (ctx.onModeChange) {
|
|
197
|
+
ctx.onModeChange(newMode);
|
|
198
|
+
return { output: `Mode changed to: ${newMode}` };
|
|
199
|
+
}
|
|
200
|
+
return { output: "Mode change not supported in this mode." };
|
|
201
|
+
};
|
|
202
|
+
const settings = () => {
|
|
203
|
+
const s = loadSettings();
|
|
204
|
+
const current = s.autoUpdate;
|
|
205
|
+
const options = ["prompt", "auto", "never"];
|
|
206
|
+
const currentIdx = options.indexOf(current);
|
|
207
|
+
const nextIdx = (currentIdx + 1) % options.length;
|
|
208
|
+
const next = options[nextIdx];
|
|
209
|
+
s.autoUpdate = next;
|
|
210
|
+
saveSettings(s);
|
|
211
|
+
const labels = {
|
|
212
|
+
prompt: "Ask before updating",
|
|
213
|
+
auto: "Auto-update on launch",
|
|
214
|
+
never: "Never check for updates",
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
output: [
|
|
218
|
+
`Auto-update: ${labels[next]}`,
|
|
219
|
+
` 1. prompt — ${next === "prompt" ? "●" : "○"} Ask before updating`,
|
|
220
|
+
` 2. auto — ${next === "auto" ? "●" : "○"} Auto-update on launch`,
|
|
221
|
+
` 3. never — ${next === "never" ? "●" : "○"} Never check`,
|
|
222
|
+
"",
|
|
223
|
+
` Changed to: ${labels[next]} (run /settings again to cycle)`,
|
|
224
|
+
].join("\n"),
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
/* ──────────────────────────────────────────
|
|
228
|
+
Extended Commands (P2)
|
|
229
|
+
────────────────────────────────────────── */
|
|
230
|
+
const cost = (ctx) => {
|
|
231
|
+
const tokens = ctx.agentInfo.totalTokens;
|
|
232
|
+
// Rough estimate: $0.15/1M input, $0.60/1M output (gpt-4o-mini prices)
|
|
233
|
+
const estimatedCost = (tokens / 1_000_000) * 0.375;
|
|
234
|
+
return {
|
|
235
|
+
output: [
|
|
236
|
+
"Token Usage",
|
|
237
|
+
` Total tokens : ${tokens.toLocaleString()}`,
|
|
238
|
+
` Est. cost : $${estimatedCost.toFixed(4)}`,
|
|
239
|
+
` Model : ${ctx.model}`,
|
|
240
|
+
"",
|
|
241
|
+
" (Cost estimate based on average input/output ratio)",
|
|
242
|
+
].join("\n"),
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
const compact = () => {
|
|
246
|
+
return { output: "Context compression triggered. (Handled by ContextManager at next iteration)" };
|
|
247
|
+
};
|
|
248
|
+
const approve = () => {
|
|
249
|
+
return { output: "No pending approval requests." };
|
|
250
|
+
};
|
|
251
|
+
const reject = () => {
|
|
252
|
+
return { output: "No pending approval requests." };
|
|
253
|
+
};
|
|
254
|
+
/* ──────────────────────────────────────────
|
|
255
|
+
Advanced Commands (P3)
|
|
256
|
+
────────────────────────────────────────── */
|
|
257
|
+
const tools = () => {
|
|
258
|
+
const toolList = [
|
|
259
|
+
"Built-in Tools (10 core + 6 design):",
|
|
260
|
+
"",
|
|
261
|
+
" Core:",
|
|
262
|
+
" file_read — Read files with offset/limit",
|
|
263
|
+
" file_write — Write files with auto-backup",
|
|
264
|
+
" file_edit — String replacement with diff preview",
|
|
265
|
+
" grep — Regex file search",
|
|
266
|
+
" glob — Pattern-based file finding",
|
|
267
|
+
" code_search — Symbol search (definition/reference)",
|
|
268
|
+
" git_ops — Git operations (status/diff/log/commit...)",
|
|
269
|
+
" shell_exec — Command execution (sandboxed)",
|
|
270
|
+
" test_run — Test auto-detection & execution",
|
|
271
|
+
" security_scan — OWASP audit + secrets detection",
|
|
272
|
+
"",
|
|
273
|
+
" Design (design mode only):",
|
|
274
|
+
" design_snapshot, design_screenshot, design_navigate,",
|
|
275
|
+
" design_resize, design_inspect, design_scroll",
|
|
276
|
+
];
|
|
277
|
+
return { output: toolList.join("\n") };
|
|
278
|
+
};
|
|
279
|
+
const memory = (ctx) => {
|
|
280
|
+
// Try to read YUAN.md from project
|
|
281
|
+
const paths = ["YUAN.md", ".yuan/YUAN.md", ".yuan/config.md", "docs/YUAN.md"];
|
|
282
|
+
for (const p of paths) {
|
|
283
|
+
const fullPath = `${ctx.workDir}/${p}`;
|
|
284
|
+
try {
|
|
285
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
286
|
+
const preview = content.slice(0, 2000);
|
|
287
|
+
return { output: `YUAN.md (${p}):\n\n${preview}${content.length > 2000 ? "\n\n... (truncated)" : ""}` };
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return { output: "No YUAN.md found in project. Agent learnings will be stored here." };
|
|
294
|
+
};
|
|
295
|
+
const retry = () => {
|
|
296
|
+
return { output: "No failed actions to retry." };
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* Minimal YAML parser — extracts simple key: value pairs and basic arrays.
|
|
300
|
+
* Only handles the subset needed for plugin.yaml files (flat fields + skills/tools arrays).
|
|
301
|
+
*/
|
|
302
|
+
function parseSimpleYaml(content) {
|
|
303
|
+
const result = {};
|
|
304
|
+
const lines = content.split("\n");
|
|
305
|
+
let currentArray = null;
|
|
306
|
+
let currentArrayKey = null;
|
|
307
|
+
for (const rawLine of lines) {
|
|
308
|
+
const line = rawLine.replace(/\r$/, "");
|
|
309
|
+
// Array item: " - key: value" or " - value"
|
|
310
|
+
if (currentArrayKey && /^\s+-\s/.test(line)) {
|
|
311
|
+
const itemStr = line.replace(/^\s+-\s*/, "");
|
|
312
|
+
if (itemStr.includes(":")) {
|
|
313
|
+
const obj = {};
|
|
314
|
+
// Parse "id: foo" or "name: bar" inline
|
|
315
|
+
const pairs = itemStr.split(/,\s*/);
|
|
316
|
+
for (const pair of pairs) {
|
|
317
|
+
const colonIdx = pair.indexOf(":");
|
|
318
|
+
if (colonIdx > 0) {
|
|
319
|
+
const k = pair.slice(0, colonIdx).trim();
|
|
320
|
+
const v = pair.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
321
|
+
obj[k] = v;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
currentArray.push(obj);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
const val = itemStr.trim().replace(/^["']|["']$/g, "");
|
|
328
|
+
currentArray.push({ name: val, id: val });
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
// Top-level "key: value"
|
|
333
|
+
const topMatch = line.match(/^(\w[\w_-]*):\s*(.*)/);
|
|
334
|
+
if (topMatch) {
|
|
335
|
+
const key = topMatch[1];
|
|
336
|
+
const val = topMatch[2].trim().replace(/^["']|["']$/g, "");
|
|
337
|
+
if (val === "" || val === "[]") {
|
|
338
|
+
// Start of an array section or empty
|
|
339
|
+
currentArrayKey = key;
|
|
340
|
+
currentArray = [];
|
|
341
|
+
result[key] = currentArray;
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
currentArrayKey = null;
|
|
345
|
+
currentArray = null;
|
|
346
|
+
result[key] = val;
|
|
347
|
+
}
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
// Non-matching line ends current array
|
|
351
|
+
if (!/^\s/.test(line) && line.trim() !== "") {
|
|
352
|
+
currentArrayKey = null;
|
|
353
|
+
currentArray = null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Try to read and parse a plugin manifest (plugin.yaml, plugin.yml, or plugin.json).
|
|
360
|
+
* Returns parsed data or null.
|
|
361
|
+
*/
|
|
362
|
+
function readPluginManifest(dir) {
|
|
363
|
+
const candidates = ["plugin.yaml", "plugin.yml", "plugin.json"];
|
|
364
|
+
for (const fname of candidates) {
|
|
365
|
+
const fpath = path.join(dir, fname);
|
|
366
|
+
try {
|
|
367
|
+
const content = fs.readFileSync(fpath, "utf-8");
|
|
368
|
+
if (fname.endsWith(".json")) {
|
|
369
|
+
return JSON.parse(content);
|
|
370
|
+
}
|
|
371
|
+
return parseSimpleYaml(content);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Extract PluginInfo from a parsed manifest + metadata.
|
|
381
|
+
*/
|
|
382
|
+
function manifestToPluginInfo(data, source, pluginPath) {
|
|
383
|
+
const rawSkills = Array.isArray(data["skills"]) ? data["skills"] : [];
|
|
384
|
+
const rawTools = Array.isArray(data["tools"]) ? data["tools"] : [];
|
|
385
|
+
return {
|
|
386
|
+
id: String(data["id"] || data["name"] || path.basename(pluginPath)),
|
|
387
|
+
name: String(data["name"] || data["id"] || path.basename(pluginPath)),
|
|
388
|
+
version: String(data["version"] || "0.0.0"),
|
|
389
|
+
category: String(data["category"] || "general"),
|
|
390
|
+
trustLevel: String(data["trust_level"] || data["trustLevel"] || "community"),
|
|
391
|
+
skills: rawSkills.map((s) => typeof s === "string" ? { id: s, name: s } : { id: s.id || s.name || "", name: s.name || s.id || "" }),
|
|
392
|
+
tools: rawTools.map((t) => typeof t === "string" ? { name: t } : { name: t.name || "" }),
|
|
393
|
+
source,
|
|
394
|
+
path: pluginPath,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Scan all known plugin locations and return discovered plugins.
|
|
399
|
+
*/
|
|
400
|
+
function scanInstalledPlugins(workDir) {
|
|
401
|
+
const plugins = [];
|
|
402
|
+
const scanDir = (dir, source) => {
|
|
403
|
+
try {
|
|
404
|
+
if (!fs.existsSync(dir))
|
|
405
|
+
return;
|
|
406
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
407
|
+
for (const entry of entries) {
|
|
408
|
+
if (!entry.isDirectory())
|
|
409
|
+
continue;
|
|
410
|
+
const pluginDir = path.join(dir, entry.name);
|
|
411
|
+
const manifest = readPluginManifest(pluginDir);
|
|
412
|
+
if (manifest) {
|
|
413
|
+
plugins.push(manifestToPluginInfo(manifest, source, pluginDir));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// Directory not readable — skip
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
// 1. Project-local plugins
|
|
422
|
+
scanDir(path.join(workDir, ".yuan", "plugins"), "local");
|
|
423
|
+
// 2. Global user plugins
|
|
424
|
+
scanDir(path.join(os.homedir(), ".yuan", "plugins"), "global");
|
|
425
|
+
// 3. npm-installed @yuaone/plugin-* and yuan-plugin-*
|
|
426
|
+
const nodeModulesDir = path.join(workDir, "node_modules");
|
|
427
|
+
try {
|
|
428
|
+
if (fs.existsSync(nodeModulesDir)) {
|
|
429
|
+
// Scoped @yuaone/plugin-*
|
|
430
|
+
const scopeDir = path.join(nodeModulesDir, "@yuaone");
|
|
431
|
+
if (fs.existsSync(scopeDir)) {
|
|
432
|
+
try {
|
|
433
|
+
const scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
|
|
434
|
+
for (const entry of scopeEntries) {
|
|
435
|
+
if (entry.isDirectory() && entry.name.startsWith("plugin-")) {
|
|
436
|
+
const pluginDir = path.join(scopeDir, entry.name);
|
|
437
|
+
const manifest = readPluginManifest(pluginDir);
|
|
438
|
+
if (manifest) {
|
|
439
|
+
plugins.push(manifestToPluginInfo(manifest, "npm", pluginDir));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// skip
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Community yuan-plugin-*
|
|
449
|
+
try {
|
|
450
|
+
const nmEntries = fs.readdirSync(nodeModulesDir, { withFileTypes: true });
|
|
451
|
+
for (const entry of nmEntries) {
|
|
452
|
+
if (entry.isDirectory() && entry.name.startsWith("yuan-plugin-")) {
|
|
453
|
+
const pluginDir = path.join(nodeModulesDir, entry.name);
|
|
454
|
+
const manifest = readPluginManifest(pluginDir);
|
|
455
|
+
if (manifest) {
|
|
456
|
+
plugins.push(manifestToPluginInfo(manifest, "npm", pluginDir));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// skip
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// skip
|
|
468
|
+
}
|
|
469
|
+
return plugins;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Read the disabled skills config from .yuan/config.json
|
|
473
|
+
*/
|
|
474
|
+
function readSkillsConfig(workDir) {
|
|
475
|
+
const configPath = path.join(workDir, ".yuan", "config.json");
|
|
476
|
+
try {
|
|
477
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
478
|
+
const parsed = JSON.parse(raw);
|
|
479
|
+
return { disabledSkills: Array.isArray(parsed.disabledSkills) ? parsed.disabledSkills : [] };
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return { disabledSkills: [] };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Write the disabled skills config to .yuan/config.json
|
|
487
|
+
*/
|
|
488
|
+
function writeSkillsConfig(workDir, config) {
|
|
489
|
+
const yuanDir = path.join(workDir, ".yuan");
|
|
490
|
+
const configPath = path.join(yuanDir, "config.json");
|
|
491
|
+
try {
|
|
492
|
+
// Read existing config to preserve other fields
|
|
493
|
+
let existing = {};
|
|
494
|
+
try {
|
|
495
|
+
existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
// No existing config — start fresh
|
|
499
|
+
}
|
|
500
|
+
if (!fs.existsSync(yuanDir)) {
|
|
501
|
+
fs.mkdirSync(yuanDir, { recursive: true });
|
|
502
|
+
}
|
|
503
|
+
existing["disabledSkills"] = config.disabledSkills;
|
|
504
|
+
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// Silently fail — will be reported by caller
|
|
508
|
+
throw new Error("Failed to write .yuan/config.json");
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/* ──────────────────────────────────────────
|
|
512
|
+
Plugin System Commands (P4)
|
|
513
|
+
────────────────────────────────────────── */
|
|
514
|
+
const plugins = (ctx, args) => {
|
|
515
|
+
if (args.length === 0) {
|
|
516
|
+
// List installed plugins by scanning real directories
|
|
517
|
+
const installed = scanInstalledPlugins(ctx.workDir);
|
|
518
|
+
if (installed.length === 0) {
|
|
519
|
+
return {
|
|
520
|
+
output: [
|
|
521
|
+
"Installed Plugins",
|
|
522
|
+
" (none)",
|
|
523
|
+
"",
|
|
524
|
+
" No plugins installed. Use /plugins search <query> to find plugins.",
|
|
525
|
+
"",
|
|
526
|
+
" Use: /plugins search <query> — Search npm registry",
|
|
527
|
+
" /plugins install <name> — Install plugin",
|
|
528
|
+
].join("\n"),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
const lines = ["Installed Plugins", ""];
|
|
532
|
+
for (let i = 0; i < installed.length; i++) {
|
|
533
|
+
const p = installed[i];
|
|
534
|
+
const isLast = i === installed.length - 1;
|
|
535
|
+
const prefix = isLast ? "└── " : "├── ";
|
|
536
|
+
const childPrefix = isLast ? " " : "│ ";
|
|
537
|
+
const trustLabel = p.trustLevel === "official" ? "[official]" : `[${p.trustLevel}]`;
|
|
538
|
+
lines.push(`${prefix}${p.id} v${p.version} ${trustLabel} (${p.source})`);
|
|
539
|
+
if (p.skills.length > 0) {
|
|
540
|
+
lines.push(`${childPrefix}├── Skills: ${p.skills.map(s => s.name).join(", ")}`);
|
|
541
|
+
}
|
|
542
|
+
if (p.tools.length > 0) {
|
|
543
|
+
const toolConnector = p.skills.length > 0 ? "└── " : "└── ";
|
|
544
|
+
lines.push(`${childPrefix}${toolConnector}Tools: ${p.tools.map(t => t.name).join(", ")}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
lines.push("");
|
|
548
|
+
lines.push(" Use: /plugins search <query> — Search npm registry");
|
|
549
|
+
lines.push(" /plugins install <name> — Install plugin");
|
|
550
|
+
lines.push(" /plugins remove <name> — Remove plugin");
|
|
551
|
+
lines.push(" /plugins info <name> — Plugin details");
|
|
552
|
+
lines.push(" /plugins update — Update all plugins");
|
|
553
|
+
return { output: lines.join("\n") };
|
|
554
|
+
}
|
|
555
|
+
const subCommand = args[0];
|
|
556
|
+
if (subCommand === "search") {
|
|
557
|
+
const query = args.slice(1).join(" ") || "";
|
|
558
|
+
if (!query) {
|
|
559
|
+
return { output: "Usage: /plugins search <query>\nExample: /plugins search typescript" };
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
const raw = execFileSync("npm", ["search", `@yuaone/plugin-${query}`, "yuan-plugin-${query}", "--json"], {
|
|
563
|
+
encoding: "utf-8",
|
|
564
|
+
stdio: "pipe",
|
|
565
|
+
timeout: 5000,
|
|
566
|
+
maxBuffer: 512 * 1024,
|
|
567
|
+
});
|
|
568
|
+
const results = JSON.parse(raw);
|
|
569
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
570
|
+
return { output: `No results found for "${query}".` };
|
|
571
|
+
}
|
|
572
|
+
const lines = [`Search results for "${query}"`, ""];
|
|
573
|
+
for (const r of results.slice(0, 10)) {
|
|
574
|
+
const name = r.name || "unknown";
|
|
575
|
+
const desc = r.description || "";
|
|
576
|
+
const ver = r.version || "";
|
|
577
|
+
lines.push(` ${name} v${ver}`);
|
|
578
|
+
if (desc)
|
|
579
|
+
lines.push(` ${desc}`);
|
|
580
|
+
}
|
|
581
|
+
lines.push("");
|
|
582
|
+
lines.push(" /plugins install <name> to install");
|
|
583
|
+
return { output: lines.join("\n") };
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return { output: `No results found for "${query}" (npm search unavailable or timed out).` };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (subCommand === "install") {
|
|
590
|
+
const pluginName = args[1];
|
|
591
|
+
if (!pluginName) {
|
|
592
|
+
return { output: "Usage: /plugins install <plugin-name>" };
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
const output = execFileSync("pnpm", ["add", pluginName, "--save-dev"], {
|
|
596
|
+
cwd: ctx.workDir,
|
|
597
|
+
encoding: "utf-8",
|
|
598
|
+
stdio: "pipe",
|
|
599
|
+
timeout: 30000,
|
|
600
|
+
maxBuffer: 1024 * 1024,
|
|
601
|
+
});
|
|
602
|
+
// Verify it's a valid YUAN plugin by checking for plugin manifest
|
|
603
|
+
const pkgDir = path.join(ctx.workDir, "node_modules", pluginName);
|
|
604
|
+
const manifest = readPluginManifest(pkgDir);
|
|
605
|
+
if (manifest) {
|
|
606
|
+
const info = manifestToPluginInfo(manifest, "npm", pkgDir);
|
|
607
|
+
return {
|
|
608
|
+
output: [
|
|
609
|
+
`Installed ${pluginName} v${info.version}`,
|
|
610
|
+
` Category : ${info.category}`,
|
|
611
|
+
` Skills : ${info.skills.length > 0 ? info.skills.map(s => s.name).join(", ") : "(none)"}`,
|
|
612
|
+
` Tools : ${info.tools.length > 0 ? info.tools.map(t => t.name).join(", ") : "(none)"}`,
|
|
613
|
+
].join("\n"),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
output: [
|
|
618
|
+
`Installed ${pluginName} (no plugin.yaml found — may not be a YUAN plugin)`,
|
|
619
|
+
output.trim() ? `\n${output.trim()}` : "",
|
|
620
|
+
].join(""),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
625
|
+
return { output: `Failed to install ${pluginName}: ${msg}` };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (subCommand === "remove") {
|
|
629
|
+
const pluginName = args[1];
|
|
630
|
+
if (!pluginName) {
|
|
631
|
+
return { output: "Usage: /plugins remove <plugin-name>" };
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
execFileSync("pnpm", ["remove", pluginName], {
|
|
635
|
+
cwd: ctx.workDir,
|
|
636
|
+
encoding: "utf-8",
|
|
637
|
+
stdio: "pipe",
|
|
638
|
+
timeout: 30000,
|
|
639
|
+
maxBuffer: 1024 * 1024,
|
|
640
|
+
});
|
|
641
|
+
return { output: `Removed ${pluginName}` };
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
645
|
+
return { output: `Failed to remove ${pluginName}: ${msg}` };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (subCommand === "info") {
|
|
649
|
+
const pluginName = args[1];
|
|
650
|
+
if (!pluginName) {
|
|
651
|
+
return { output: "Usage: /plugins info <plugin-name>" };
|
|
652
|
+
}
|
|
653
|
+
// Search installed plugins for a match
|
|
654
|
+
const installed = scanInstalledPlugins(ctx.workDir);
|
|
655
|
+
const found = installed.find((p) => p.id === pluginName || p.name === pluginName || p.path.endsWith(pluginName));
|
|
656
|
+
if (!found) {
|
|
657
|
+
// Try node_modules directly
|
|
658
|
+
const tryPaths = [
|
|
659
|
+
path.join(ctx.workDir, "node_modules", pluginName),
|
|
660
|
+
path.join(ctx.workDir, ".yuan", "plugins", pluginName),
|
|
661
|
+
path.join(os.homedir(), ".yuan", "plugins", pluginName),
|
|
662
|
+
];
|
|
663
|
+
for (const tryPath of tryPaths) {
|
|
664
|
+
const manifest = readPluginManifest(tryPath);
|
|
665
|
+
if (manifest) {
|
|
666
|
+
const info = manifestToPluginInfo(manifest, "npm", tryPath);
|
|
667
|
+
const lines = [
|
|
668
|
+
`Plugin: ${info.name}`,
|
|
669
|
+
` ID : ${info.id}`,
|
|
670
|
+
` Version : ${info.version}`,
|
|
671
|
+
` Category : ${info.category}`,
|
|
672
|
+
` Trust Level: ${info.trustLevel}`,
|
|
673
|
+
` Path : ${tryPath}`,
|
|
674
|
+
` Skills (${info.skills.length}): ${info.skills.length > 0 ? info.skills.map(s => s.name).join(", ") : "(none)"}`,
|
|
675
|
+
` Tools (${info.tools.length}): ${info.tools.length > 0 ? info.tools.map(t => t.name).join(", ") : "(none)"}`,
|
|
676
|
+
];
|
|
677
|
+
return { output: lines.join("\n") };
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return { output: `Plugin "${pluginName}" not found. Is it installed?` };
|
|
681
|
+
}
|
|
682
|
+
const lines = [
|
|
683
|
+
`Plugin: ${found.name}`,
|
|
684
|
+
` ID : ${found.id}`,
|
|
685
|
+
` Version : ${found.version}`,
|
|
686
|
+
` Category : ${found.category}`,
|
|
687
|
+
` Trust Level: ${found.trustLevel}`,
|
|
688
|
+
` Source : ${found.source}`,
|
|
689
|
+
` Path : ${found.path}`,
|
|
690
|
+
` Skills (${found.skills.length}): ${found.skills.length > 0 ? found.skills.map(s => s.name).join(", ") : "(none)"}`,
|
|
691
|
+
` Tools (${found.tools.length}): ${found.tools.length > 0 ? found.tools.map(t => t.name).join(", ") : "(none)"}`,
|
|
692
|
+
];
|
|
693
|
+
return { output: lines.join("\n") };
|
|
694
|
+
}
|
|
695
|
+
if (subCommand === "update") {
|
|
696
|
+
try {
|
|
697
|
+
const output = execFileSync("pnpm", ["update", "@yuaone/plugin-*", "yuan-plugin-*"], {
|
|
698
|
+
cwd: ctx.workDir,
|
|
699
|
+
encoding: "utf-8",
|
|
700
|
+
stdio: "pipe",
|
|
701
|
+
timeout: 30000,
|
|
702
|
+
maxBuffer: 1024 * 1024,
|
|
703
|
+
});
|
|
704
|
+
const trimmed = output.trim();
|
|
705
|
+
return { output: trimmed ? `Plugin updates:\n${trimmed}` : "All plugins are up to date." };
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
709
|
+
return { output: `Failed to update plugins: ${msg}` };
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return { output: `Unknown subcommand: ${subCommand}. Use /plugins for help.` };
|
|
713
|
+
};
|
|
714
|
+
const skills = (ctx, args) => {
|
|
715
|
+
if (args.length === 0) {
|
|
716
|
+
// List built-in skills + skills from installed plugins
|
|
717
|
+
const installed = scanInstalledPlugins(ctx.workDir);
|
|
718
|
+
const skillsConfig = readSkillsConfig(ctx.workDir);
|
|
719
|
+
const disabled = new Set(skillsConfig.disabledSkills);
|
|
720
|
+
const builtinSkills = [
|
|
721
|
+
{ id: "code-review", name: "Code review (read-only analysis)" },
|
|
722
|
+
{ id: "security-scan", name: "OWASP security audit" },
|
|
723
|
+
{ id: "test-gen", name: "Auto-generate tests" },
|
|
724
|
+
{ id: "refactor", name: "Intelligent refactoring" },
|
|
725
|
+
{ id: "debug", name: "Systematic debugging" },
|
|
726
|
+
{ id: "plan", name: "Task planning & architecture" },
|
|
727
|
+
];
|
|
728
|
+
const lines = ["Available Skills", ""];
|
|
729
|
+
// Built-in section
|
|
730
|
+
lines.push(" Built-in:");
|
|
731
|
+
for (let i = 0; i < builtinSkills.length; i++) {
|
|
732
|
+
const s = builtinSkills[i];
|
|
733
|
+
const isLast = i === builtinSkills.length - 1;
|
|
734
|
+
const prefix = isLast ? " └── " : " ├── ";
|
|
735
|
+
const statusMark = disabled.has(s.id) ? " [disabled]" : "";
|
|
736
|
+
lines.push(`${prefix}${s.id.padEnd(16)} — ${s.name}${statusMark}`);
|
|
737
|
+
}
|
|
738
|
+
// Plugin skills section
|
|
739
|
+
const pluginsWithSkills = installed.filter(p => p.skills.length > 0);
|
|
740
|
+
if (pluginsWithSkills.length > 0) {
|
|
741
|
+
lines.push("");
|
|
742
|
+
lines.push(" Plugin Skills:");
|
|
743
|
+
for (let pi = 0; pi < pluginsWithSkills.length; pi++) {
|
|
744
|
+
const p = pluginsWithSkills[pi];
|
|
745
|
+
const isLastPlugin = pi === pluginsWithSkills.length - 1;
|
|
746
|
+
const pluginPrefix = isLastPlugin ? " └── " : " ├── ";
|
|
747
|
+
const childPrefix = isLastPlugin ? " " : " │ ";
|
|
748
|
+
lines.push(`${pluginPrefix}${p.id}/`);
|
|
749
|
+
for (let si = 0; si < p.skills.length; si++) {
|
|
750
|
+
const s = p.skills[si];
|
|
751
|
+
const isLastSkill = si === p.skills.length - 1;
|
|
752
|
+
const skillPrefix = isLastSkill ? "└── " : "├── ";
|
|
753
|
+
const statusMark = disabled.has(s.id) ? " [disabled]" : "";
|
|
754
|
+
lines.push(`${childPrefix}${skillPrefix}${s.id || s.name}${statusMark}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
else if (installed.length === 0) {
|
|
759
|
+
lines.push("");
|
|
760
|
+
lines.push(" Plugin Skills:");
|
|
761
|
+
lines.push(" (no plugins installed — use /plugins search <query>)");
|
|
762
|
+
}
|
|
763
|
+
lines.push("");
|
|
764
|
+
lines.push(" Use: /skills enable <name> — Enable skill");
|
|
765
|
+
lines.push(" /skills disable <name> — Disable skill");
|
|
766
|
+
return { output: lines.join("\n") };
|
|
767
|
+
}
|
|
768
|
+
const subCommand = args[0];
|
|
769
|
+
if (subCommand === "enable") {
|
|
770
|
+
const skillName = args[1];
|
|
771
|
+
if (!skillName)
|
|
772
|
+
return { output: "Usage: /skills enable <skill-name>" };
|
|
773
|
+
try {
|
|
774
|
+
const config = readSkillsConfig(ctx.workDir);
|
|
775
|
+
const idx = config.disabledSkills.indexOf(skillName);
|
|
776
|
+
if (idx === -1) {
|
|
777
|
+
return { output: `Skill "${skillName}" is already enabled.` };
|
|
778
|
+
}
|
|
779
|
+
config.disabledSkills.splice(idx, 1);
|
|
780
|
+
writeSkillsConfig(ctx.workDir, config);
|
|
781
|
+
return { output: `Enabled skill: ${skillName}` };
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
return { output: `Failed to enable skill "${skillName}" (could not write .yuan/config.json).` };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (subCommand === "disable") {
|
|
788
|
+
const skillName = args[1];
|
|
789
|
+
if (!skillName)
|
|
790
|
+
return { output: "Usage: /skills disable <skill-name>" };
|
|
791
|
+
try {
|
|
792
|
+
const config = readSkillsConfig(ctx.workDir);
|
|
793
|
+
if (config.disabledSkills.includes(skillName)) {
|
|
794
|
+
return { output: `Skill "${skillName}" is already disabled.` };
|
|
795
|
+
}
|
|
796
|
+
config.disabledSkills.push(skillName);
|
|
797
|
+
writeSkillsConfig(ctx.workDir, config);
|
|
798
|
+
return { output: `Disabled skill: ${skillName}` };
|
|
799
|
+
}
|
|
800
|
+
catch {
|
|
801
|
+
return { output: `Failed to disable skill "${skillName}" (could not write .yuan/config.json).` };
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return { output: `Unknown subcommand: ${subCommand}. Use /skills for help.` };
|
|
805
|
+
};
|
|
806
|
+
/* ──────────────────────────────────────────
|
|
807
|
+
Registry
|
|
808
|
+
────────────────────────────────────────── */
|
|
809
|
+
/** All command definitions with metadata */
|
|
810
|
+
export const COMMAND_DEFS = [
|
|
811
|
+
// Core (P1)
|
|
812
|
+
{ name: "/help", description: "Show available commands", aliases: ["/h"], handler: help },
|
|
813
|
+
{ name: "/status", description: "Provider, model, tokens, session info", handler: status },
|
|
814
|
+
{ name: "/clear", description: "Clear conversation history", handler: (_ctx) => ({ clear: true }) },
|
|
815
|
+
{ name: "/config", description: "Show current configuration", handler: config },
|
|
816
|
+
{ name: "/session", description: "Session management", handler: session },
|
|
817
|
+
{ name: "/diff", description: "Show file changes (git diff)", handler: diff },
|
|
818
|
+
{ name: "/undo", description: "Undo last file change", handler: undo },
|
|
819
|
+
{ name: "/model", description: "Show or change model", handler: model },
|
|
820
|
+
{ name: "/mode", description: "Show or change agent mode", handler: mode },
|
|
821
|
+
{ name: "/settings", description: "Auto-update preferences", handler: settings },
|
|
822
|
+
{ name: "/exit", description: "Exit YUAN", aliases: ["/quit", "/q"], handler: () => ({ exit: true }) },
|
|
823
|
+
// Extended (P2)
|
|
824
|
+
{ name: "/cost", description: "Token usage & estimated cost", handler: cost },
|
|
825
|
+
{ name: "/compact", description: "Compress context history", handler: compact },
|
|
826
|
+
{ name: "/approve", description: "Approve pending action", handler: approve },
|
|
827
|
+
{ name: "/reject", description: "Reject pending action", handler: reject },
|
|
828
|
+
// Advanced (P3)
|
|
829
|
+
{ name: "/tools", description: "List available tools", handler: tools },
|
|
830
|
+
{ name: "/memory", description: "Show YUAN.md learnings", handler: memory },
|
|
831
|
+
{ name: "/retry", description: "Retry last failed action", handler: retry },
|
|
832
|
+
// Plugin System (P4)
|
|
833
|
+
{ name: "/plugins", description: "Plugin management (install/remove/search)", handler: plugins },
|
|
834
|
+
{ name: "/skills", description: "Available skills (tree view)", aliases: ["/skill"], handler: skills },
|
|
835
|
+
];
|
|
836
|
+
/** Flat handler map for quick lookup */
|
|
837
|
+
const HANDLER_MAP = new Map();
|
|
838
|
+
for (const def of COMMAND_DEFS) {
|
|
839
|
+
HANDLER_MAP.set(def.name, def.handler);
|
|
840
|
+
if (def.aliases) {
|
|
841
|
+
for (const alias of def.aliases) {
|
|
842
|
+
HANDLER_MAP.set(alias, def.handler);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Execute a slash command.
|
|
848
|
+
* @returns CommandResult, or null if command not found
|
|
849
|
+
*/
|
|
850
|
+
export function executeCommand(ctx, input) {
|
|
851
|
+
const parts = input.trim().toLowerCase().split(/\s+/);
|
|
852
|
+
const cmd = parts[0];
|
|
853
|
+
const args = parts.slice(1);
|
|
854
|
+
const handler = HANDLER_MAP.get(cmd);
|
|
855
|
+
if (!handler)
|
|
856
|
+
return null;
|
|
857
|
+
return handler(ctx, args);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Check if a string is a known command (for slash menu validation).
|
|
861
|
+
*/
|
|
862
|
+
export function isKnownCommand(input) {
|
|
863
|
+
const cmd = input.trim().toLowerCase().split(/\s+/)[0];
|
|
864
|
+
return HANDLER_MAP.has(cmd);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Get all command definitions for the slash menu.
|
|
868
|
+
*/
|
|
869
|
+
export function getCommandDefs() {
|
|
870
|
+
return COMMAND_DEFS;
|
|
871
|
+
}
|
|
872
|
+
//# sourceMappingURL=index.js.map
|