skyloom 1.12.0 → 1.13.1

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.
Files changed (137) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +137 -46
  3. package/config/default.yaml +43 -47
  4. package/config/models.yaml +155 -155
  5. package/config/providers.yaml +39 -39
  6. package/config/skills/api_integrator/SKILL.md +15 -15
  7. package/config/skills/arch_designer/SKILL.md +13 -13
  8. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  9. package/config/skills/code_analysis/SKILL.md +13 -13
  10. package/config/skills/code_generator/SKILL.md +12 -12
  11. package/config/skills/code_reviewer/SKILL.md +13 -13
  12. package/config/skills/content_writer/SKILL.md +14 -14
  13. package/config/skills/data_transformer/SKILL.md +15 -15
  14. package/config/skills/document_analysis/SKILL.md +13 -13
  15. package/config/skills/emotional_companion/SKILL.md +15 -15
  16. package/config/skills/performance_checker/SKILL.md +14 -14
  17. package/config/skills/security_auditor/SKILL.md +14 -14
  18. package/config/skills/self_evolve/SKILL.md +13 -13
  19. package/config/skills/sys_operator/SKILL.md +15 -15
  20. package/config/skills/task_planner/SKILL.md +14 -14
  21. package/config/skills/web_research/SKILL.md +14 -14
  22. package/config/skills/workflow_designer/SKILL.md +13 -13
  23. package/dist/agents/dew.js +52 -52
  24. package/dist/agents/fair.js +84 -84
  25. package/dist/agents/fog.js +30 -30
  26. package/dist/agents/frost.js +32 -32
  27. package/dist/agents/rain.js +32 -32
  28. package/dist/agents/snow.js +68 -68
  29. package/dist/cli/main.js +127 -74
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/cli/tui.d.ts +52 -19
  32. package/dist/cli/tui.d.ts.map +1 -1
  33. package/dist/cli/tui.js +198 -265
  34. package/dist/cli/tui.js.map +1 -1
  35. package/dist/core/agent/task.d.ts +58 -0
  36. package/dist/core/agent/task.d.ts.map +1 -0
  37. package/dist/core/agent/task.js +83 -0
  38. package/dist/core/agent/task.js.map +1 -0
  39. package/dist/core/agent.d.ts +2 -45
  40. package/dist/core/agent.d.ts.map +1 -1
  41. package/dist/core/agent.js +61 -145
  42. package/dist/core/agent.js.map +1 -1
  43. package/dist/core/agent_helpers.d.ts +10 -0
  44. package/dist/core/agent_helpers.d.ts.map +1 -1
  45. package/dist/core/agent_helpers.js +39 -0
  46. package/dist/core/agent_helpers.js.map +1 -1
  47. package/dist/core/catalog.d.ts +71 -0
  48. package/dist/core/catalog.d.ts.map +1 -0
  49. package/dist/core/catalog.js +176 -0
  50. package/dist/core/catalog.js.map +1 -0
  51. package/dist/core/config.d.ts +8 -0
  52. package/dist/core/config.d.ts.map +1 -1
  53. package/dist/core/config.js +12 -4
  54. package/dist/core/config.js.map +1 -1
  55. package/dist/core/factory.js +16 -16
  56. package/dist/core/llm.d.ts +7 -0
  57. package/dist/core/llm.d.ts.map +1 -1
  58. package/dist/core/llm.js +139 -7
  59. package/dist/core/llm.js.map +1 -1
  60. package/dist/core/longdoc.js +5 -5
  61. package/dist/core/memory.d.ts.map +1 -1
  62. package/dist/core/memory.js +69 -62
  63. package/dist/core/memory.js.map +1 -1
  64. package/dist/core/theme.d.ts +46 -0
  65. package/dist/core/theme.d.ts.map +1 -0
  66. package/dist/core/theme.js +42 -0
  67. package/dist/core/theme.js.map +1 -0
  68. package/dist/web/server.js +542 -519
  69. package/dist/web/server.js.map +1 -1
  70. package/docs/AESTHETIC_DESIGN.md +144 -0
  71. package/docs/OPTIMIZATION_PLAN.md +178 -0
  72. package/package.json +60 -60
  73. package/scripts/install.js +48 -48
  74. package/scripts/link.js +10 -10
  75. package/setup.bat +79 -79
  76. package/skill-test-ty2fOA/test.md +10 -10
  77. package/src/agents/dew.ts +70 -70
  78. package/src/agents/fair.ts +102 -102
  79. package/src/agents/fog.ts +48 -48
  80. package/src/agents/frost.ts +50 -50
  81. package/src/agents/rain.ts +50 -50
  82. package/src/agents/snow.ts +239 -239
  83. package/src/cli/main.ts +417 -372
  84. package/src/cli/mode.ts +58 -58
  85. package/src/cli/tui.ts +174 -223
  86. package/src/core/agent/task.ts +100 -0
  87. package/src/core/agent.ts +1446 -1549
  88. package/src/core/agent_helpers.ts +496 -461
  89. package/src/core/arbitrate.ts +162 -162
  90. package/src/core/catalog.ts +178 -0
  91. package/src/core/checkpoint.ts +94 -94
  92. package/src/core/config.ts +20 -4
  93. package/src/core/estimate.ts +104 -104
  94. package/src/core/evolve.ts +191 -191
  95. package/src/core/factory.ts +627 -627
  96. package/src/core/filter.ts +103 -103
  97. package/src/core/graph.ts +156 -156
  98. package/src/core/icons.ts +53 -53
  99. package/src/core/index.ts +37 -37
  100. package/src/core/learn.ts +146 -146
  101. package/src/core/llm.ts +108 -5
  102. package/src/core/longdoc.ts +155 -155
  103. package/src/core/mcp_server.ts +176 -176
  104. package/src/core/memory.ts +1178 -1171
  105. package/src/core/profile.ts +255 -255
  106. package/src/core/router.ts +124 -124
  107. package/src/core/sandbox.ts +142 -142
  108. package/src/core/security.ts +243 -243
  109. package/src/core/skill.ts +342 -342
  110. package/src/core/theme.ts +65 -0
  111. package/src/core/tool_router.ts +193 -193
  112. package/src/core/vector.ts +152 -152
  113. package/src/core/workspace.ts +150 -150
  114. package/src/plugins/loader.ts +66 -66
  115. package/src/skills/loader.ts +46 -46
  116. package/src/sql.js.d.ts +29 -29
  117. package/src/tools/builtin.ts +380 -380
  118. package/src/tools/computer.ts +269 -269
  119. package/src/tools/delegate.ts +49 -49
  120. package/src/web/server.ts +660 -634
  121. package/src/web/tts.ts +93 -93
  122. package/tests/agent_helpers.test.ts +48 -0
  123. package/tests/bus.test.ts +121 -121
  124. package/tests/catalog.test.ts +86 -0
  125. package/tests/config.test.ts +41 -0
  126. package/tests/icons.test.ts +45 -45
  127. package/tests/memory.test.ts +147 -0
  128. package/tests/router.test.ts +86 -86
  129. package/tests/schemas.test.ts +51 -51
  130. package/tests/semantic.test.ts +83 -83
  131. package/tests/setup.ts +10 -10
  132. package/tests/skill.test.ts +172 -172
  133. package/tests/task.test.ts +60 -0
  134. package/tests/tool.test.ts +108 -108
  135. package/tests/tool_router.test.ts +71 -71
  136. package/tests/tui.test.ts +67 -0
  137. package/vitest.config.ts +17 -17
package/src/cli/main.ts CHANGED
@@ -1,372 +1,417 @@
1
- #!/usr/bin/env node
2
- /**
3
- * 天空织机 CLI — Skyloom Terminal Interface
4
- */
5
- import { Command } from "commander";
6
- import * as fs from "fs";
7
- import * as readline from "readline";
8
- import chalk from "chalk";
9
- import { createSystemContext, orchestrateTask } from "../core/factory";
10
- import { loadConfig, USER_CONFIG_DIR } from "../core/config";
11
- import { classify } from "../core/router";
12
- import { InteractiveMode, ModeController } from "./mode";
13
- import { readInput, type TUIContext } from "./tui";
14
-
15
- const MODE = new ModeController();
16
- const VERSION = (() => { try { return require("../../package.json").version; } catch { return "1.5.2"; } })();
17
-
18
- const AGENT_DISPLAY: Record<string, string> = {
19
- fog: "≋ 雾 Fog", rain: "⸽ 雨 Rain", frost: "✱ 霜 Frost",
20
- snow: "❉ Snow", dew: " Dew", fair: " Fair",
21
- };
22
- const AGENT_NAMES = ["fog", "rain", "frost", "snow", "dew", "fair"] as const;
23
-
24
- /* ═══════════════════════════════════════
25
- Slash commands registry
26
- ═══════════════════════════════════════ */
27
- const SLASH_CMDS: [string, string][] = [
28
- ["/help", "Show all commands"],
29
- ["/clear", "Clear screen"],
30
- ["/status", "Agent overview"],
31
- ["/cost", "Usage & cost"],
32
- ["/cost reset", "Reset usage stats"],
33
- ["/compact", "Compress context"],
34
- ["/retry", "Resend last msg"],
35
- ["/memory", "Memory stats"],
36
- ["/memory clear", "Clear short-term memory"],
37
- ["/sessions", "Session list"],
38
- ["/workspace", "Workspace info"],
39
- ["/model", "Model info"],
40
- ["/mcp", "MCP server status"],
41
- ["/version", "Version info"],
42
- ["/task <goal>", "Multi-agent orchestrate"],
43
- ["/fog", " Fogresearch insight"],
44
- ["/rain", " Raincreation codegen"],
45
- ["/frost", " Frostreview quality"],
46
- ["/snow", " Snowplanning architect"],
47
- ["/dew", " Dew — devops reliability"],
48
- ["/fair", " Fair — companion warmth"],
49
- ["/quit", "Exit chat"],
50
- ["/exit", "Exit chat"],
51
- ];
52
-
53
- function showPopup(cmds: [string, string][], selIdx: number) {
54
- const w = process.stdout.columns || 80;
55
- const start = Math.max(0, Math.min(selIdx - 4, cmds.length - 8));
56
- const end = Math.min(cmds.length, start + 8);
57
- process.stdout.write(chalk.dim(" ┌─ commands (↑↓ pick · type letter to filter · tab/enter select) ─┐\n"));
58
- for (let i = start; i < end; i++) {
59
- const [cmd, desc] = cmds[i];
60
- const marker = i === selIdx ? chalk.cyan(" ▶ ") : " ";
61
- process.stdout.write(` │${marker}${chalk.cyan(cmd.padEnd(24))}${chalk.dim(desc)}${" ".repeat(Math.max(0, 50 - desc.length))}│\n`);
62
- }
63
- process.stdout.write(chalk.dim(` └${"─".repeat(60)}┘\n`));
64
- }
65
-
66
- /* ═══════════════════════════════════════
67
- Commander
68
- ═══════════════════════════════════════ */
69
- const program = new Command()
70
- .name("sky").description("天空织机 Skyloom").version(VERSION);
71
-
72
- program.command("chat").argument("[agent]", "agent name", "fog")
73
- .option("-m,--model <m>", "model").action(async (a: string, o: { model?: string }) => { await chat(a, o.model); });
74
- program.command("task").argument("[goal]", "task goal")
75
- .action(async (g?: string) => { if (g) await runTask(g); });
76
- program.command("web").option("-p,--port <p>", "port", "3000")
77
- .action((o: { port?: string }) => { import("../web/server").then(m => m.startWebServer(parseInt(o.port || "3000"))); });
78
- program.command("mcp").action(() => { import("../core/mcp_server").then(m => m.startMCPServer()); });
79
- program.command("config").action(() => { const c = loadConfig(); process.stdout.write(chalk.cyan("\nConfig: ") + USER_CONFIG_DIR + "\n"); for (const [n, a] of Object.entries(c.agents || {})) process.stdout.write(` ${chalk.bold(n)}: ${(a as any).model || "default"}\n`); });
80
- program.command("init").action(() => { if (!fs.existsSync(USER_CONFIG_DIR)) fs.mkdirSync(USER_CONFIG_DIR, { recursive: true }); process.stdout.write(chalk.green(" ") + USER_CONFIG_DIR + "\n"); });
81
- program.command("apikey").description("Manage API keys (persisted to ~/.skyloom/config.yaml)")
82
- .argument("[action]", "set|list").argument("[provider]", "e.g. deepseek").argument("[key]", "API key")
83
- .action((action?: string, provider?: string, key?: string) => {
84
- if (action === "set" && provider && key) { saveApiKey(provider, key); process.stdout.write(chalk.green("✓ Saved " + provider + " API key\n")); }
85
- else { process.stdout.write(chalk.dim("Usage: sky apikey set deepseek YOUR_KEY\n")); }
86
- });
87
- program.command("version").action(() => { process.stdout.write(`Skyloom v${VERSION}\n`); });
88
-
89
- /* ═══════════════════════════════════════
90
- Welcome
91
- ═══════════════════════════════════════ */
92
- function welcome(agent: any) {
93
- const w = process.stdout.columns || 80;
94
- const pad = " ".repeat(Math.max(0, Math.floor((w - 34) / 2)));
95
- process.stdout.write("\n" + pad + chalk.cyan("✦ 天 空 织 机 ✦\n"));
96
- process.stdout.write(pad + chalk.dim("S K Y L O O M\n\n"));
97
- const parts: string[] = [];
98
- for (const n of AGENT_NAMES) {
99
- const a = n === agent.name;
100
- const s = `${AGENT_DISPLAY[n].split(" ")[0]} ${AGENT_DISPLAY[n].split(" ")[1]}`;
101
- parts.push(a ? chalk.bold.cyan(s) : chalk.dim(s));
102
- }
103
- process.stdout.write(" " + parts.join(chalk.dim(" · ")) + "\n\n");
104
- process.stdout.write(chalk.dim(" /help for commands · /quit to exit\n\n"));
105
- }
106
-
107
- function statusBar(agent: any, ctx: any): string {
108
- try {
109
- const cu = agent.contextUsage();
110
- const pct = cu.pct || 0;
111
- const bar = pct < 50 ? chalk.green : pct < 80 ? chalk.yellow : chalk.red;
112
- const filled = Math.round(pct / 10);
113
- const ctxBar = `${bar("█".repeat(filled) + "░".repeat(10 - filled))} ${pct}%`;
114
- const cost = formatCost(ctx.llm.getTotalCost());
115
- return chalk.dim(`${ctxBar} · ${cost} · ${cu.model || "?"}`);
116
- } catch { return ""; }
117
- }
118
-
119
- function formatCost(c: number): string {
120
- if (c >= 1) return chalk.yellow(`$${c.toFixed(2)}`);
121
- if (c >= 0.01) return chalk.yellow(`$${c.toFixed(4)}`);
122
- if (c > 0) return chalk.green(`${(c * 100).toFixed(2)}¢`);
123
- return "$0";
124
- }
125
-
126
- /* ═══════════════════════════════════════
127
- Response render
128
- ═══════════════════════════════════════ */
129
- function render(text: string): string[] {
130
- const out: string[] = [];
131
- for (const para of text.split("\n\n")) {
132
- const t = para.trim();
133
- if (!t) continue;
134
- if (t.startsWith("```")) {
135
- const lines = t.split("\n");
136
- out.push(chalk.dim(" ╭─ code ──"));
137
- for (let i = 1; i < lines.length - 1; i++) out.push(` ${chalk.dim("│")} ${chalk.gray(lines[i].slice(0, 72))}`);
138
- out.push(chalk.dim(" ╰────────"));
139
- } else {
140
- for (const line of t.split("\n")) {
141
- if (line.startsWith("# ")) out.push(" " + chalk.bold(line));
142
- else if (line.startsWith("- ") || line.startsWith("* ")) out.push(" " + chalk.dim("• ") + line.slice(2));
143
- else out.push(" " + line);
144
- }
145
- }
146
- }
147
- return out;
148
- }
149
-
150
- /* ═══════════════════════════════════════
151
- Chat loop
152
- ═══════════════════════════════════════ */
153
- /* API key persistence read from config file too */
154
- function checkApiKeys(): string | null {
155
- // Check env vars
156
- const envKeys = ["DEEPSEEK_API_KEY","OPENAI_API_KEY","ANTHROPIC_API_KEY","GROQ_API_KEY","OPENROUTER_API_KEY"];
157
- for (const k of envKeys) { if (process.env[k]) return "env:" + k; }
158
- // Check config file
159
- try {
160
- const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
161
- const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
162
- if (fs.existsSync(cfgPath)) {
163
- const cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {};
164
- const keys = cfg.api_keys || {};
165
- for (const [p, k] of Object.entries(keys)) { if (k) return "cfg:" + p; }
166
- }
167
- } catch { /* ignore */ }
168
- return null;
169
- }
170
-
171
- /** Save API key to config file */
172
- function saveApiKey(provider: string, key: string): void {
173
- const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
174
- const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
175
- const dir = path.dirname(cfgPath);
176
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
177
- let cfg: any = {};
178
- if (fs.existsSync(cfgPath)) { try { cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {}; } catch { } }
179
- if (!cfg.api_keys) cfg.api_keys = {};
180
- cfg.api_keys[provider] = key;
181
- fs.writeFileSync(cfgPath, yaml.stringify(cfg), "utf-8");
182
- }
183
-
184
- /* ═══════════════════════════════════════
185
- Interactive setup wizard
186
- ═══════════════════════════════════════ */
187
- async function setupWizard(): Promise<{ provider: string; key: string; model: string } | null> {
188
- const providers = [
189
- { id: "deepseek", name: "DeepSeek", models: ["deepseek-chat","deepseek-v4-flash","deepseek-v4-pro","deepseek-reasoner"] },
190
- { id: "openai", name: "OpenAI", models: ["gpt-4.1","gpt-4o","gpt-4o-mini","o4-mini"] },
191
- { id: "anthropic", name: "Anthropic", models: ["claude-sonnet-4-6","claude-opus-4-7","claude-haiku-4-5"] },
192
- { id: "google", name: "Google Gemini", models: ["gemini-2.5-pro","gemini-2.5-flash"] },
193
- { id: "groq", name: "Groq", models: ["llama-3.3-70b","mixtral-8x7b"] },
194
- { id: "openrouter", name: "OpenRouter (多模型)", models: ["openai/gpt-4.1","anthropic/claude-sonnet-4-6","google/gemini-2.5-flash","meta-llama/llama-4-maverick"] },
195
- { id: "mistral", name: "Mistral", models: ["mistral-large","mistral-small"] },
196
- { id: "xai", name: "xAI (Grok)", models: ["grok-4"] },
197
- { id: "ollama", name: "Ollama 本地", models: ["llama3","qwen2.5","deepseek-r1"] },
198
- ];
199
-
200
- process.stdout.write("\n" + chalk.cyan(" ✦ API Key 设置向导 ✦\n\n"));
201
- process.stdout.write(chalk.dim(" 选择 Provider(Key 保存在 ~/.skyloom/config.yaml):\n\n"));
202
-
203
- for (let i = 0; i < providers.length; i++) {
204
- process.stdout.write(chalk.dim(` ${String(i+1).padStart(2)}. ${providers[i].name.padEnd(22)} ${providers[i].models.slice(0,3).join(", ")}\n`));
205
- }
206
-
207
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
208
- const ask = (q: string): Promise<string> => new Promise(r => rl.question(q, r));
209
-
210
- const choice = await ask(chalk.cyan("\n 编号 (1-"+providers.length+", q退出): "));
211
- if (choice === "q") { rl.close(); return null; }
212
- const idx = parseInt(choice) - 1;
213
- if (isNaN(idx) || idx < 0 || idx >= providers.length) { rl.close(); process.stdout.write(chalk.dim(" 已取消\n")); return null; }
214
-
215
- const prov = providers[idx];
216
- const key = await ask(chalk.cyan(` ${prov.name} API Key: `));
217
- if (!key.trim()) { rl.close(); return null; }
218
-
219
- saveApiKey(prov.id, key.trim());
220
-
221
- process.stdout.write(chalk.dim("\n 可用模型:\n"));
222
- for (let i = 0; i < prov.models.length; i++) process.stdout.write(chalk.dim(` ${i+1}. ${prov.models[i]}\n`));
223
-
224
- const mc = await ask(chalk.cyan("\n 选择模型 (1-"+prov.models.length+", 默认1): ")) || "1";
225
- const mi = (parseInt(mc) || 1) - 1;
226
- const model = prov.models[Math.max(0, Math.min(mi, prov.models.length - 1))];
227
-
228
- // Save to config
229
- const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
230
- const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
231
- let cfg: any = {}; if (fs.existsSync(cfgPath)) { try { cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {}; } catch { } }
232
- cfg.default_model = model; cfg.default_provider = prov.id;
233
- fs.writeFileSync(cfgPath, yaml.stringify(cfg), "utf-8");
234
-
235
- rl.close();
236
- process.stdout.write(chalk.green(`\n ✓ ${prov.name} · ${model} · 就绪!\n\n`));
237
- return { provider: prov.id, key: key.trim(), model };
238
- }
239
-
240
- async function chat(agentName: string, modelOverride?: string): Promise<void> {
241
- const haveKey = checkApiKeys();
242
- if (!haveKey) {
243
- process.stdout.write("\n" + chalk.cyan(" ✦ 天空织机 Skyloom ✦\n"));
244
- process.stdout.write(chalk.dim(" 检测到未配置 API Key,进入设置向导...\n\n"));
245
- const result = await setupWizard();
246
- if (!result) { process.stdout.write(chalk.red(" 设置未完成,请重新运行 sky 配置。\n")); process.exit(0); }
247
- process.stdout.write(chalk.green(` ✓ ${result.provider} 已就绪 · 模型: ${result.model}\n\n`));
248
- }
249
-
250
- const ctx = createSystemContext();
251
- let agent = ctx.agentMap.get(agentName);
252
- if (!agent) { process.stdout.write(chalk.red("Unknown agent: " + agentName) + "\n"); return; }
253
- await agent.init();
254
-
255
- // Wire up security approval — prompt user for HIGH/CRITICAL operations
256
- try {
257
- const { getSecurity, DangerLevel } = require("../core/security");
258
- const sec = getSecurity();
259
- sec.setApprovalCallback(async (tool: string, args: Record<string, any>, level: number) => {
260
- process.stdout.write(chalk.yellow(`\n ⚠ ${tool} ( danger level ${level} )\n`));
261
- process.stdout.write(chalk.dim(` args: ${JSON.stringify(args).slice(0, 80)}\n`));
262
- const answer = await new Promise<string>(resolve => {
263
- const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
264
- rl2.question(chalk.red(" Approve? [y/N] "), (a: string) => { rl2.close(); resolve(a.trim().toLowerCase()); });
265
- });
266
- return answer === "y" || answer === "yes";
267
- });
268
- } catch { /* security module optional */ }
269
-
270
- // eslint-disable-next-line prefer-const
271
- let currentAgent = agent; // mutable for agent switching
272
- welcome(agent);
273
-
274
- process.stdout.write(chalk.dim(" Key: " + haveKey + "\n\n"));
275
-
276
- // ── TUI loop ──
277
- const ctx_: TUIContext = { agent: currentAgent, agents: ctx.agentMap, model: "default", cost: "$0", width: 80, height: 24 };
278
-
279
- while (true) {
280
- const inp = await readInput(process.stdin, process.stdout, ctx_);
281
- if (!inp) continue;
282
-
283
- const cmdL = inp.toLowerCase();
284
-
285
- // Agent switch
286
- let switched = false;
287
- for (const n of AGENT_NAMES) {
288
- if (cmdL === "/" + n) { const a = ctx.agentMap.get(n); if (a) { await a.init(); currentAgent = a; ctx_.agent = a; } switched = true; break; }
289
- }
290
- if (switched) continue;
291
- if (cmdL === "/quit" || cmdL === "/exit") break;
292
- if (cmdL === "/clear") { console.clear(); continue; }
293
- if (cmdL === "/help") { process.stdout.write(helpText()); continue; }
294
- if (cmdL === "/version") { process.stdout.write(" Skyloom v" + VERSION + "\n"); continue; }
295
- if (cmdL === "/status") { process.stdout.write(chalk.bold("\n " + currentAgent.displayName + " (" + currentAgent.name + ")\n") + chalk.dim(" State: " + currentAgent.state + " · Memory: " + currentAgent.memory.shortTerm.length + " msgs\n\n")); continue; }
296
- if (cmdL === "/cost") { process.stdout.write(chalk.bold("\n Total: " + formatCost(ctx.llm.getTotalCost()) + "\n\n")); continue; }
297
- if (cmdL === "/cost reset") { (ctx.llm as any).resetUsageStats?.(); process.stdout.write(chalk.dim(" Stats reset\n")); continue; }
298
- if (cmdL === "/compact") { const r = await currentAgent.compact(); process.stdout.write(chalk.green(" ✓ " + r + "\n\n")); continue; }
299
- if (cmdL === "/memory") { process.stdout.write(chalk.dim(" Short-term: " + currentAgent.memory.shortTerm.length + " msgs · Working: " + Object.keys(currentAgent.memory.working).length + " keys\n")); continue; }
300
- if (cmdL === "/memory clear") { await currentAgent.memory.clearShortTerm(); process.stdout.write(chalk.dim(" Memory cleared\n")); continue; }
301
- if (cmdL === "/workspace") { process.stdout.write(chalk.dim(" " + (ctx.workspacePath || "default") + "\n")); continue; }
302
- if (cmdL === "/sessions") { const ss = await currentAgent.memory.listSessions(); process.stdout.write(chalk.bold("\n Sessions:\n")); for (const s of ss.slice(0, 10)) process.stdout.write(chalk.dim(" " + s.id?.slice(0, 10) + "... " + s.preview + " (" + s.messageCount + " msgs)\n")); continue; }
303
- if (cmdL === "/mcp") { process.stdout.write(chalk.dim(" " + (ctx.mcpStatus?.join(", ") || "none") + "\n")); continue; }
304
- if (cmdL.startsWith("/apikey set ")) { const p = inp.split(/\s+/); if (p.length >= 4) { saveApiKey(p[2], p[3]); process.stdout.write(chalk.green(" ✓ Saved " + p[2] + " API key\n")); } else { process.stdout.write(chalk.yellow(" Usage: /apikey set <provider> <key>\n")); } continue; }
305
- if (cmdL === "/apikey") { process.stdout.write(chalk.bold("\n API Keys:\n")); for (const p of ["openai","deepseek","anthropic","groq","openrouter"]) { process.stdout.write(chalk.dim(" " + p.padEnd(14) + (!!process.env[p.toUpperCase() + "_API_KEY"] ? chalk.green("env") : chalk.dim("—")) + "\n")); } process.stdout.write("\n"); continue; }
306
- if (cmdL.startsWith("/task ")) { const g = inp.slice(6); process.stdout.write(chalk.cyan("\n ✦ " + g + "\n\n")); await runTask(g); continue; }
307
- if (cmdL === "/setup") { const r = await setupWizard(); if (r) process.stdout.write(chalk.green(` ${r.provider} · ${r.model} — Ready!\n`)); continue; }
308
- if (cmdL.startsWith("/model")) { process.stdout.write(chalk.dim(" Run /setup to reconfigure models\n")); continue; }
309
- if (inp.startsWith("/")) { process.stdout.write(helpText()); continue; }
310
-
311
- // ── Chat ──
312
- process.stdout.write(chalk.dim(" " + currentAgent.displayName + " thinking...\r"));
313
- try {
314
- const response = await currentAgent.chat(inp);
315
- process.stdout.write("\r" + " ".repeat(40) + "\r\n");
316
- for (const l of render(response)) process.stdout.write(l + "\n");
317
- process.stdout.write("\n");
318
- } catch (e: any) {
319
- process.stdout.write("\r" + " ".repeat(40) + "\r");
320
- process.stdout.write(chalk.red(" ✗ " + (e.message || e) + "\n\n"));
321
- }
322
- }
323
-
324
- process.stdout.write(chalk.dim("\n Session ended\n"));
325
- await ctx.closeAll();
326
- process.exit(0);
327
- }
328
-
329
- /* ═══════════════════════════════════════
330
- Task
331
- ═══════════════════════════════════════ */
332
- async function runTask(goal: string): Promise<void> {
333
- const ctx = createSystemContext();
334
- await ctx.initAll();
335
- const [, results, summary] = await orchestrateTask(goal, ctx.agentMap);
336
- for (const r of results) process.stdout.write(` ${r.success ? chalk.green("") : chalk.red("")} ${chalk.cyan(r.agent)}: ${r.description.slice(0, 60)}\n`);
337
- process.stdout.write(chalk.bold("\n " + summary.slice(0, 800) + "\n\n"));
338
- await ctx.closeAll();
339
- }
340
-
341
- function helpText(): string {
342
- const groups: [string, [string, string][]][] = [
343
- ["Agent", [["/fog /rain /frost", "Switch agents"], ["/snow /dew /fair", "Switch agents"]]],
344
- ["Chat", [["/help", "Commands"], ["/clear", "Clear"], ["/compact", "Compress"], ["/retry", "Resend"]]],
345
- ["Info", [["/status", "Status"], ["/cost", "Cost"], ["/memory", "Memory"], ["/sessions", "Sessions"], ["/workspace", "Workspace"], ["/version", "Version"]]],
346
- ["Orch.", [["/task <goal>", "Multi-agent"]]],
347
- ];
348
- let s = "";
349
- for (const [title, cmds] of groups) {
350
- s += chalk.cyan(` ${title}\n`);
351
- for (const [c, d] of cmds) s += ` ${chalk.cyan(c.padEnd(18))}${chalk.dim(d)}\n`;
352
- }
353
- s += "\n";
354
- return s;
355
- }
356
-
357
- /* ═══════════════════════════════════════
358
- Entry
359
- ═══════════════════════════════════════ */
360
- async function main() {
361
- const args = process.argv.slice(2);
362
- if (args.length === 0) { await chat("fog"); return; }
363
- if ((AGENT_NAMES as readonly string[]).includes(args[0])) {
364
- let m: string | undefined;
365
- for (let i = 1; i < args.length; i++) if ((args[i] === "-m" || args[i] === "--model") && i + 1 < args.length) m = args[++i];
366
- await chat(args[0], m); return;
367
- }
368
- if (!["chat", "task", "web", "config", "init", "version", "mcp", "help"].includes(args[0]) && !args[0].startsWith("-")) { await chat("fog"); return; }
369
- program.parse(process.argv);
370
- }
371
-
372
- main().catch(e => { process.stderr.write(chalk.red(`Fatal: ${(e as Error).message}\n`)); process.exit(1); });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 天空织机 CLI — Skyloom Terminal Interface
4
+ */
5
+ import { Command } from "commander";
6
+ import * as fs from "fs";
7
+ import * as readline from "readline";
8
+ import chalk from "chalk";
9
+ import { createSystemContext, orchestrateTask } from "../core/factory";
10
+ import { loadConfig, USER_CONFIG_DIR } from "../core/config";
11
+ import { listProviders, modelsFor, providerLabel, validateModel } from "../core/catalog";
12
+ import { agentTheme } from "../core/theme";
13
+ import { classify } from "../core/router";
14
+ import { InteractiveMode, ModeController } from "./mode";
15
+ import { readLine, renderPalette, StreamRenderer } from "./tui";
16
+
17
+ const MODE = new ModeController();
18
+ const VERSION = (() => { try { return require("../../package.json").version; } catch { return "1.5.2"; } })();
19
+
20
+ const AGENT_NAMES = ["fog", "rain", "frost", "snow", "dew", "fair"] as const;
21
+
22
+ /* ═══════════════════════════════════════
23
+ Slash commands registry
24
+ ═══════════════════════════════════════ */
25
+ const SLASH_CMDS: [string, string][] = [
26
+ ["/help", "Show all commands"],
27
+ ["/clear", "Clear screen"],
28
+ ["/status", "Agent overview"],
29
+ ["/cost", "Usage & cost"],
30
+ ["/cost reset", "Reset usage stats"],
31
+ ["/compact", "Compress context"],
32
+ ["/retry", "Resend last msg"],
33
+ ["/memory", "Memory stats"],
34
+ ["/memory clear", "Clear short-term memory"],
35
+ ["/sessions", "Session list"],
36
+ ["/workspace", "Workspace info"],
37
+ ["/model", "Model info"],
38
+ ["/mcp", "MCP server status"],
39
+ ["/version", "Version info"],
40
+ ["/task <goal>", "Multi-agent orchestrate"],
41
+ ["/fog", " Fog — research insight"],
42
+ ["/rain", " Rain — creation codegen"],
43
+ ["/frost", " Frostreview quality"],
44
+ ["/snow", " Snowplanning architect"],
45
+ ["/dew", " Dewdevops reliability"],
46
+ ["/fair", " Faircompanion warmth"],
47
+ ["/quit", "Exit chat"],
48
+ ["/exit", "Exit chat"],
49
+ ];
50
+
51
+ function showPopup(cmds: [string, string][], selIdx: number) {
52
+ const w = process.stdout.columns || 80;
53
+ const start = Math.max(0, Math.min(selIdx - 4, cmds.length - 8));
54
+ const end = Math.min(cmds.length, start + 8);
55
+ process.stdout.write(chalk.dim(" ┌─ commands (↑↓ pick · type letter to filter · tab/enter select) ─┐\n"));
56
+ for (let i = start; i < end; i++) {
57
+ const [cmd, desc] = cmds[i];
58
+ const marker = i === selIdx ? chalk.cyan(" ") : " ";
59
+ process.stdout.write(` │${marker}${chalk.cyan(cmd.padEnd(24))}${chalk.dim(desc)}${" ".repeat(Math.max(0, 50 - desc.length))}│\n`);
60
+ }
61
+ process.stdout.write(chalk.dim(` └${"".repeat(60)}┘\n`));
62
+ }
63
+
64
+ /* ═══════════════════════════════════════
65
+ Commander
66
+ ═══════════════════════════════════════ */
67
+ const program = new Command()
68
+ .name("sky").description("天空织机 Skyloom").version(VERSION);
69
+
70
+ program.command("chat").argument("[agent]", "agent name", "fog")
71
+ .option("-m,--model <m>", "model").action(async (a: string, o: { model?: string }) => { await chat(a, o.model); });
72
+ program.command("task").argument("[goal]", "task goal")
73
+ .action(async (g?: string) => { if (g) await runTask(g); });
74
+ program.command("web").option("-p,--port <p>", "port", "3000")
75
+ .action((o: { port?: string }) => { import("../web/server").then(m => m.startWebServer(parseInt(o.port || "3000"))); });
76
+ program.command("mcp").action(() => { import("../core/mcp_server").then(m => m.startMCPServer()); });
77
+ program.command("config").action(() => { const c = loadConfig(); process.stdout.write(chalk.cyan("\nConfig: ") + USER_CONFIG_DIR + "\n"); for (const [n, a] of Object.entries(c.agents || {})) process.stdout.write(` ${chalk.bold(n)}: ${(a as any).model || "default"}\n`); });
78
+ program.command("init").action(() => { if (!fs.existsSync(USER_CONFIG_DIR)) fs.mkdirSync(USER_CONFIG_DIR, { recursive: true }); process.stdout.write(chalk.green("✓ ") + USER_CONFIG_DIR + "\n"); });
79
+ program.command("apikey").description("Manage API keys (persisted to ~/.skyloom/config.yaml)")
80
+ .argument("[action]", "set|list").argument("[provider]", "e.g. deepseek").argument("[key]", "API key")
81
+ .action((action?: string, provider?: string, key?: string) => {
82
+ if (action === "set" && provider && key) { saveApiKey(provider, key); process.stdout.write(chalk.green("✓ Saved " + provider + " API key\n")); }
83
+ else { process.stdout.write(chalk.dim("Usage: sky apikey set deepseek YOUR_KEY\n")); }
84
+ });
85
+ program.command("version").action(() => { process.stdout.write(`Skyloom v${VERSION}\n`); });
86
+
87
+ /* ═══════════════════════════════════════
88
+ Welcome
89
+ ═══════════════════════════════════════ */
90
+ function welcome(agent: any) {
91
+ const w = process.stdout.columns || 80;
92
+ const active = agentTheme(agent.name);
93
+ const seal = chalk.hex(active.hex);
94
+ const pad = " ".repeat(Math.max(0, Math.floor((w - 34) / 2)));
95
+ process.stdout.write("\n" + pad + seal("✦ 天 空 织 机 ✦\n"));
96
+ process.stdout.write(pad + chalk.dim("S K Y L O O M\n\n"));
97
+ // Six shuttles, each in its own mineral pigment; active one bolded with a seal.
98
+ const parts: string[] = [];
99
+ for (const n of AGENT_NAMES) {
100
+ const t = agentTheme(n);
101
+ const isActive = n === agent.name;
102
+ const label = `${t.symbol} ${t.kanji}`;
103
+ parts.push(isActive ? chalk.bold.hex(t.hex)(`▣ ${label}`) : chalk.hex(t.hex).dim(label));
104
+ }
105
+ process.stdout.write(" " + parts.join(chalk.dim(" · ")) + "\n");
106
+ process.stdout.write(" " + chalk.dim.italic(active.poem) + "\n\n");
107
+ process.stdout.write(chalk.dim(" /help for commands · /quit to exit\n\n"));
108
+ }
109
+
110
+ function statusBar(agent: any, ctx: any): string {
111
+ try {
112
+ const cu = agent.contextUsage();
113
+ const pct = cu.pct || 0;
114
+ const bar = pct < 50 ? chalk.green : pct < 80 ? chalk.yellow : chalk.red;
115
+ const filled = Math.round(pct / 10);
116
+ const ctxBar = `${bar("█".repeat(filled) + "".repeat(10 - filled))} ${pct}%`;
117
+ const cost = formatCost(ctx.llm.getTotalCost());
118
+ return chalk.dim(`${ctxBar} · ${cost} · ${cu.model || "?"}`);
119
+ } catch { return ""; }
120
+ }
121
+
122
+ function formatCost(c: number): string {
123
+ if (c >= 1) return chalk.yellow(`$${c.toFixed(2)}`);
124
+ if (c >= 0.01) return chalk.yellow(`$${c.toFixed(4)}`);
125
+ if (c > 0) return chalk.green(`${(c * 100).toFixed(2)}¢`);
126
+ return "$0";
127
+ }
128
+
129
+ /* ═══════════════════════════════════════
130
+ Streaming renderer consumes agent.chatStream()
131
+ ═══════════════════════════════════════ */
132
+ /**
133
+ * Render a streamed turn live: reasoning in faint ink, content in mineral
134
+ * pigment, tool calls as pulsing weather events. Replaces the old blocking
135
+ * chat() + fake render. Tokens appear as they arrive.
136
+ */
137
+ async function streamResponse(agent: any, input: string): Promise<void> {
138
+ const theme = agentTheme(agent.name);
139
+ const pigment = chalk.hex(theme.hex);
140
+ const out = process.stdout;
141
+
142
+ // ── Thinking spinner (animates until the first token lands; TTY only) ──
143
+ const isTTY = !!out.isTTY;
144
+ const frames = ["· ", "·· ", " ··", " ·"];
145
+ let fi = 0, spinning = true;
146
+ const draw = () => { if (spinning && isTTY) out.write(`\r ${pigment(theme.symbol)} ${chalk.dim("思忖 " + frames[fi++ % frames.length])}`); };
147
+ const timer = isTTY ? setInterval(draw, 140) : null; draw();
148
+ const stopSpinner = () => { if (spinning) { spinning = false; if (timer) clearInterval(timer); if (isTTY) out.write("\r" + " ".repeat(20) + "\r"); } };
149
+
150
+ let headerShown = false;
151
+ let mode: "none" | "reasoning" | "content" = "none";
152
+ let renderer: StreamRenderer | null = null;
153
+ const header = () => { if (!headerShown) { out.write("\n " + chalk.bold.hex(theme.hex)(`${theme.symbol} ${theme.kanji}`) + chalk.hex(theme.hex)(` ${theme.name}`) + "\n\n"); headerShown = true; } };
154
+ const endBlock = () => { if (renderer) { renderer.flush(); renderer = null; out.write("\n"); } };
155
+
156
+ try {
157
+ for await (const ev of agent.chatStream(input)) {
158
+ switch (ev.type) {
159
+ case "reasoning":
160
+ stopSpinner();
161
+ if (mode !== "reasoning") { out.write(chalk.dim(" ◦ 思考 ")); mode = "reasoning"; }
162
+ out.write(chalk.dim.italic(String(ev.text).replace(/\s+/g, " ")));
163
+ break;
164
+ case "content":
165
+ stopSpinner();
166
+ if (mode === "reasoning") out.write("\n");
167
+ if (mode !== "content") { header(); renderer = new StreamRenderer(out, { gutter: " " }); mode = "content"; }
168
+ renderer!.write(String(ev.text));
169
+ break;
170
+ case "tool_status":
171
+ stopSpinner();
172
+ endBlock();
173
+ out.write("\n " + pigment(`${theme.symbol} ${ev.tool_name}`) + (ev.label ? chalk.dim(` ${ev.label}`) : "") + chalk.dim(" …") + "\n");
174
+ mode = "none";
175
+ break;
176
+ case "tool_done":
177
+ out.write(" " + (ev.success ? chalk.hex("#3a7a6e")("✓") : chalk.hex("#b3342d")("✗")) + " " + chalk.dim(String(ev.tool_name)) + "\n");
178
+ mode = "none";
179
+ break;
180
+ case "truncated":
181
+ endBlock();
182
+ out.write(chalk.yellow(`\n ⚠ ${ev.reason}\n`));
183
+ break;
184
+ case "done":
185
+ break;
186
+ }
187
+ }
188
+ } finally {
189
+ stopSpinner();
190
+ endBlock();
191
+ }
192
+ out.write("\n");
193
+ }
194
+
195
+ /* ═══════════════════════════════════════
196
+ Chat loop
197
+ ═══════════════════════════════════════ */
198
+ /* API key persistence — read from config file too */
199
+ function checkApiKeys(): string | null {
200
+ // Check env vars
201
+ const envKeys = ["DEEPSEEK_API_KEY","OPENAI_API_KEY","ANTHROPIC_API_KEY","GROQ_API_KEY","OPENROUTER_API_KEY"];
202
+ for (const k of envKeys) { if (process.env[k]) return "env:" + k; }
203
+ // Check config file
204
+ try {
205
+ const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
206
+ const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
207
+ if (fs.existsSync(cfgPath)) {
208
+ const cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {};
209
+ const keys = cfg.api_keys || {};
210
+ for (const [p, k] of Object.entries(keys)) { if (k) return "cfg:" + p; }
211
+ }
212
+ } catch { /* ignore */ }
213
+ return null;
214
+ }
215
+
216
+ /** Save API key to config file */
217
+ function saveApiKey(provider: string, key: string): void {
218
+ const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
219
+ const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
220
+ const dir = path.dirname(cfgPath);
221
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
222
+ let cfg: any = {};
223
+ if (fs.existsSync(cfgPath)) { try { cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {}; } catch { } }
224
+ if (!cfg.api_keys) cfg.api_keys = {};
225
+ cfg.api_keys[provider] = key;
226
+ fs.writeFileSync(cfgPath, yaml.stringify(cfg), "utf-8");
227
+ }
228
+
229
+ /* ═══════════════════════════════════════
230
+ Interactive setup wizard
231
+ ═══════════════════════════════════════ */
232
+ async function setupWizard(): Promise<{ provider: string; key: string; model: string } | null> {
233
+ // Derived from the single-source model catalog (config/models.yaml).
234
+ // Every listed model is callable — no hardcoded/fictional entries.
235
+ const providers = listProviders().map((id) => ({
236
+ id,
237
+ name: providerLabel(id),
238
+ models: modelsFor(id).map((m) => m.id),
239
+ }));
240
+
241
+ process.stdout.write("\n" + chalk.cyan(" ✦ API Key 设置向导 ✦\n\n"));
242
+ process.stdout.write(chalk.dim(" 选择 Provider(Key 保存在 ~/.skyloom/config.yaml):\n\n"));
243
+
244
+ for (let i = 0; i < providers.length; i++) {
245
+ process.stdout.write(chalk.dim(` ${String(i+1).padStart(2)}. ${providers[i].name.padEnd(22)} ${providers[i].models.slice(0,3).join(", ")}\n`));
246
+ }
247
+
248
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
249
+ const ask = (q: string): Promise<string> => new Promise(r => rl.question(q, r));
250
+
251
+ const choice = await ask(chalk.cyan("\n 编号 (1-"+providers.length+", q退出): "));
252
+ if (choice === "q") { rl.close(); return null; }
253
+ const idx = parseInt(choice) - 1;
254
+ if (isNaN(idx) || idx < 0 || idx >= providers.length) { rl.close(); process.stdout.write(chalk.dim(" 已取消\n")); return null; }
255
+
256
+ const prov = providers[idx];
257
+ const key = await ask(chalk.cyan(` ${prov.name} API Key: `));
258
+ if (!key.trim()) { rl.close(); return null; }
259
+
260
+ saveApiKey(prov.id, key.trim());
261
+
262
+ process.stdout.write(chalk.dim("\n 可用模型:\n"));
263
+ for (let i = 0; i < prov.models.length; i++) process.stdout.write(chalk.dim(` ${i+1}. ${prov.models[i]}\n`));
264
+
265
+ const mc = await ask(chalk.cyan("\n 选择模型 (1-"+prov.models.length+", 默认1): ")) || "1";
266
+ const mi = (parseInt(mc) || 1) - 1;
267
+ const model = prov.models[Math.max(0, Math.min(mi, prov.models.length - 1))];
268
+
269
+ // Save to config
270
+ const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
271
+ const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
272
+ let cfg: any = {}; if (fs.existsSync(cfgPath)) { try { cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {}; } catch { } }
273
+ cfg.default_model = model; cfg.default_provider = prov.id;
274
+ fs.writeFileSync(cfgPath, yaml.stringify(cfg), "utf-8");
275
+
276
+ rl.close();
277
+ process.stdout.write(chalk.green(`\n ✓ ${prov.name} · ${model} · 就绪!\n\n`));
278
+ return { provider: prov.id, key: key.trim(), model };
279
+ }
280
+
281
+ async function chat(agentName: string, modelOverride?: string): Promise<void> {
282
+ const haveKey = checkApiKeys();
283
+ if (!haveKey) {
284
+ process.stdout.write("\n" + chalk.cyan(" ✦ 天空织机 Skyloom ✦\n"));
285
+ process.stdout.write(chalk.dim(" 检测到未配置 API Key,进入设置向导...\n\n"));
286
+ const result = await setupWizard();
287
+ if (!result) { process.stdout.write(chalk.red(" 设置未完成,请重新运行 sky 配置。\n")); process.exit(0); }
288
+ process.stdout.write(chalk.green(` ✓ ${result.provider} 已就绪 · 模型: ${result.model}\n\n`));
289
+ }
290
+
291
+ const ctx = createSystemContext();
292
+ let agent = ctx.agentMap.get(agentName);
293
+ if (!agent) { process.stdout.write(chalk.red("Unknown agent: " + agentName) + "\n"); return; }
294
+
295
+ // Validate the active model is real catches stale/fictional configs
296
+ // before they 404 mid-request.
297
+ try {
298
+ const cfg = loadConfig();
299
+ const activeModel = cfg.agents?.[agentName]?.model || (cfg as any).llm?.default_model;
300
+ const v = validateModel(activeModel);
301
+ if (!v.ok) {
302
+ process.stdout.write(chalk.yellow(`\n 配置的模型 "${activeModel || "(未设置)"}" 不在可用目录中。\n`));
303
+ process.stdout.write(chalk.dim(` 可选: ${v.suggestions.join(", ")}\n`));
304
+ process.stdout.write(chalk.dim(` 运行 /setup 重新选择,或编辑 ~/.skyloom/config.yaml。\n\n`));
305
+ }
306
+ } catch { /* validation is best-effort */ }
307
+
308
+ await agent.init();
309
+
310
+ // Wire up security approval — prompt user for HIGH/CRITICAL operations
311
+ try {
312
+ const { getSecurity, DangerLevel } = require("../core/security");
313
+ const sec = getSecurity();
314
+ sec.setApprovalCallback(async (tool: string, args: Record<string, any>, level: number) => {
315
+ process.stdout.write(chalk.yellow(`\n ⚠ ${tool} ( danger level ${level} )\n`));
316
+ process.stdout.write(chalk.dim(` args: ${JSON.stringify(args).slice(0, 80)}\n`));
317
+ const answer = await new Promise<string>(resolve => {
318
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
319
+ rl2.question(chalk.red(" Approve? [y/N] "), (a: string) => { rl2.close(); resolve(a.trim().toLowerCase()); });
320
+ });
321
+ return answer === "y" || answer === "yes";
322
+ });
323
+ } catch { /* security module optional */ }
324
+
325
+ // eslint-disable-next-line prefer-const
326
+ let currentAgent = agent; // mutable for agent switching
327
+ welcome(agent);
328
+
329
+ process.stdout.write(chalk.dim(" · 输入 / 看命令(Tab 补全)· ↑↓ 翻历史 · Ctrl-C 退出\n\n"));
330
+
331
+ while (true) {
332
+ const inp = await readLine(currentAgent.name);
333
+ if (!inp) continue;
334
+
335
+ // Bare "/" show the inline command palette
336
+ if (inp === "/") { process.stdout.write("\n" + renderPalette("") + "\n"); continue; }
337
+
338
+ const cmdL = inp.toLowerCase();
339
+
340
+ // Agent switch — stamp a mineral seal on change
341
+ let switched = false;
342
+ for (const n of AGENT_NAMES) {
343
+ if (cmdL === "/" + n) {
344
+ const a = ctx.agentMap.get(n);
345
+ if (a) {
346
+ await a.init(); currentAgent = a;
347
+ const t = agentTheme(n);
348
+ process.stdout.write("\n " + chalk.bold.hex(t.hex)(`▣ ${t.kanji} ${t.pigment}`) + chalk.dim(` · ${t.specialty}`) + "\n");
349
+ process.stdout.write(" " + chalk.dim.italic(t.poem) + "\n\n");
350
+ }
351
+ switched = true; break;
352
+ }
353
+ }
354
+ if (switched) continue;
355
+ if (cmdL === "/quit" || cmdL === "/exit") break;
356
+ if (cmdL === "/clear") { console.clear(); continue; }
357
+ if (cmdL === "/help") { process.stdout.write("\n" + renderPalette("") + "\n"); continue; }
358
+ if (cmdL === "/version") { process.stdout.write(" Skyloom v" + VERSION + "\n"); continue; }
359
+ if (cmdL === "/status") { process.stdout.write(chalk.bold("\n " + currentAgent.displayName + " (" + currentAgent.name + ")\n") + chalk.dim(" State: " + currentAgent.state + " · Memory: " + currentAgent.memory.shortTerm.length + " msgs\n\n")); continue; }
360
+ if (cmdL === "/cost") { process.stdout.write(chalk.bold("\n Total: " + formatCost(ctx.llm.getTotalCost()) + "\n\n")); continue; }
361
+ if (cmdL === "/cost reset") { (ctx.llm as any).resetUsageStats?.(); process.stdout.write(chalk.dim(" Stats reset\n")); continue; }
362
+ if (cmdL === "/compact") { const r = await currentAgent.compact(); process.stdout.write(chalk.green("" + r + "\n\n")); continue; }
363
+ if (cmdL === "/memory") { process.stdout.write(chalk.dim(" Short-term: " + currentAgent.memory.shortTerm.length + " msgs · Working: " + Object.keys(currentAgent.memory.working).length + " keys\n")); continue; }
364
+ if (cmdL === "/memory clear") { await currentAgent.memory.clearShortTerm(); process.stdout.write(chalk.dim(" Memory cleared\n")); continue; }
365
+ if (cmdL === "/workspace") { process.stdout.write(chalk.dim(" " + (ctx.workspacePath || "default") + "\n")); continue; }
366
+ if (cmdL === "/sessions") { const ss = await currentAgent.memory.listSessions(); process.stdout.write(chalk.bold("\n Sessions:\n")); for (const s of ss.slice(0, 10)) process.stdout.write(chalk.dim(" " + s.id?.slice(0, 10) + "... " + s.preview + " (" + s.messageCount + " msgs)\n")); continue; }
367
+ if (cmdL === "/mcp") { process.stdout.write(chalk.dim(" " + (ctx.mcpStatus?.join(", ") || "none") + "\n")); continue; }
368
+ if (cmdL.startsWith("/apikey set ")) { const p = inp.split(/\s+/); if (p.length >= 4) { saveApiKey(p[2], p[3]); process.stdout.write(chalk.green(" ✓ Saved " + p[2] + " API key\n")); } else { process.stdout.write(chalk.yellow(" Usage: /apikey set <provider> <key>\n")); } continue; }
369
+ if (cmdL === "/apikey") { process.stdout.write(chalk.bold("\n API Keys:\n")); for (const p of ["openai","deepseek","anthropic","groq","openrouter"]) { process.stdout.write(chalk.dim(" " + p.padEnd(14) + (!!process.env[p.toUpperCase() + "_API_KEY"] ? chalk.green("env") : chalk.dim("—")) + "\n")); } process.stdout.write("\n"); continue; }
370
+ if (cmdL.startsWith("/task ")) { const g = inp.slice(6); process.stdout.write(chalk.cyan("\n ✦ " + g + "\n\n")); await runTask(g); continue; }
371
+ if (cmdL === "/setup") { const r = await setupWizard(); if (r) process.stdout.write(chalk.green(` ${r.provider} · ${r.model} — Ready!\n`)); continue; }
372
+ if (cmdL.startsWith("/model")) { process.stdout.write(chalk.dim(" Run /setup to reconfigure models\n")); continue; }
373
+ if (inp.startsWith("/")) { process.stdout.write("\n" + chalk.dim(` 未知命令 ${inp.split(" ")[0]}\n`) + renderPalette(cmdL.split(" ")[0]) + "\n"); continue; }
374
+
375
+ // ── Chat (real streaming) ──
376
+ try {
377
+ await streamResponse(currentAgent, inp);
378
+ } catch (e: any) {
379
+ process.stdout.write("\r" + " ".repeat(40) + "\r");
380
+ process.stdout.write(chalk.red(" ✗ " + (e.message || e) + "\n\n"));
381
+ }
382
+ }
383
+
384
+ process.stdout.write(chalk.dim("\n Session ended\n"));
385
+ await ctx.closeAll();
386
+ process.exit(0);
387
+ }
388
+
389
+ /* ═══════════════════════════════════════
390
+ Task
391
+ ═══════════════════════════════════════ */
392
+ async function runTask(goal: string): Promise<void> {
393
+ const ctx = createSystemContext();
394
+ await ctx.initAll();
395
+ const [, results, summary] = await orchestrateTask(goal, ctx.agentMap);
396
+ for (const r of results) process.stdout.write(` ${r.success ? chalk.green("✓") : chalk.red("✗")} ${chalk.cyan(r.agent)}: ${r.description.slice(0, 60)}\n`);
397
+ process.stdout.write(chalk.bold("\n " + summary.slice(0, 800) + "\n\n"));
398
+ await ctx.closeAll();
399
+ }
400
+
401
+
402
+ /* ═══════════════════════════════════════
403
+ Entry
404
+ ═══════════════════════════════════════ */
405
+ async function main() {
406
+ const args = process.argv.slice(2);
407
+ if (args.length === 0) { await chat("fog"); return; }
408
+ if ((AGENT_NAMES as readonly string[]).includes(args[0])) {
409
+ let m: string | undefined;
410
+ for (let i = 1; i < args.length; i++) if ((args[i] === "-m" || args[i] === "--model") && i + 1 < args.length) m = args[++i];
411
+ await chat(args[0], m); return;
412
+ }
413
+ if (!["chat", "task", "web", "config", "init", "version", "mcp", "help"].includes(args[0]) && !args[0].startsWith("-")) { await chat("fog"); return; }
414
+ program.parse(process.argv);
415
+ }
416
+
417
+ main().catch(e => { process.stderr.write(chalk.red(`Fatal: ${(e as Error).message}\n`)); process.exit(1); });