skyloom 1.12.0 → 1.13.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.
Files changed (135) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +142 -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 +103 -51
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/cli/tui.d.ts.map +1 -1
  32. package/dist/cli/tui.js +8 -1
  33. package/dist/cli/tui.js.map +1 -1
  34. package/dist/core/agent/task.d.ts +58 -0
  35. package/dist/core/agent/task.d.ts.map +1 -0
  36. package/dist/core/agent/task.js +83 -0
  37. package/dist/core/agent/task.js.map +1 -0
  38. package/dist/core/agent.d.ts +2 -45
  39. package/dist/core/agent.d.ts.map +1 -1
  40. package/dist/core/agent.js +61 -145
  41. package/dist/core/agent.js.map +1 -1
  42. package/dist/core/agent_helpers.d.ts +10 -0
  43. package/dist/core/agent_helpers.d.ts.map +1 -1
  44. package/dist/core/agent_helpers.js +39 -0
  45. package/dist/core/agent_helpers.js.map +1 -1
  46. package/dist/core/catalog.d.ts +71 -0
  47. package/dist/core/catalog.d.ts.map +1 -0
  48. package/dist/core/catalog.js +176 -0
  49. package/dist/core/catalog.js.map +1 -0
  50. package/dist/core/config.d.ts +8 -0
  51. package/dist/core/config.d.ts.map +1 -1
  52. package/dist/core/config.js +12 -4
  53. package/dist/core/config.js.map +1 -1
  54. package/dist/core/factory.js +16 -16
  55. package/dist/core/llm.d.ts +7 -0
  56. package/dist/core/llm.d.ts.map +1 -1
  57. package/dist/core/llm.js +139 -7
  58. package/dist/core/llm.js.map +1 -1
  59. package/dist/core/longdoc.js +5 -5
  60. package/dist/core/memory.d.ts.map +1 -1
  61. package/dist/core/memory.js +69 -62
  62. package/dist/core/memory.js.map +1 -1
  63. package/dist/core/theme.d.ts +46 -0
  64. package/dist/core/theme.d.ts.map +1 -0
  65. package/dist/core/theme.js +42 -0
  66. package/dist/core/theme.js.map +1 -0
  67. package/dist/web/server.js +542 -519
  68. package/dist/web/server.js.map +1 -1
  69. package/docs/AESTHETIC_DESIGN.md +144 -0
  70. package/docs/OPTIMIZATION_PLAN.md +178 -0
  71. package/package.json +60 -60
  72. package/scripts/install.js +48 -48
  73. package/scripts/link.js +10 -10
  74. package/setup.bat +79 -79
  75. package/skill-test-ty2fOA/test.md +10 -10
  76. package/src/agents/dew.ts +70 -70
  77. package/src/agents/fair.ts +102 -102
  78. package/src/agents/fog.ts +48 -48
  79. package/src/agents/frost.ts +50 -50
  80. package/src/agents/rain.ts +50 -50
  81. package/src/agents/snow.ts +239 -239
  82. package/src/cli/main.ts +425 -372
  83. package/src/cli/mode.ts +58 -58
  84. package/src/cli/tui.ts +272 -269
  85. package/src/core/agent/task.ts +100 -0
  86. package/src/core/agent.ts +1446 -1549
  87. package/src/core/agent_helpers.ts +496 -461
  88. package/src/core/arbitrate.ts +162 -162
  89. package/src/core/catalog.ts +178 -0
  90. package/src/core/checkpoint.ts +94 -94
  91. package/src/core/config.ts +20 -4
  92. package/src/core/estimate.ts +104 -104
  93. package/src/core/evolve.ts +191 -191
  94. package/src/core/factory.ts +627 -627
  95. package/src/core/filter.ts +103 -103
  96. package/src/core/graph.ts +156 -156
  97. package/src/core/icons.ts +53 -53
  98. package/src/core/index.ts +37 -37
  99. package/src/core/learn.ts +146 -146
  100. package/src/core/llm.ts +108 -5
  101. package/src/core/longdoc.ts +155 -155
  102. package/src/core/mcp_server.ts +176 -176
  103. package/src/core/memory.ts +1178 -1171
  104. package/src/core/profile.ts +255 -255
  105. package/src/core/router.ts +124 -124
  106. package/src/core/sandbox.ts +142 -142
  107. package/src/core/security.ts +243 -243
  108. package/src/core/skill.ts +342 -342
  109. package/src/core/theme.ts +65 -0
  110. package/src/core/tool_router.ts +193 -193
  111. package/src/core/vector.ts +152 -152
  112. package/src/core/workspace.ts +150 -150
  113. package/src/plugins/loader.ts +66 -66
  114. package/src/skills/loader.ts +46 -46
  115. package/src/sql.js.d.ts +29 -29
  116. package/src/tools/builtin.ts +380 -380
  117. package/src/tools/computer.ts +269 -269
  118. package/src/tools/delegate.ts +49 -49
  119. package/src/web/server.ts +660 -634
  120. package/src/web/tts.ts +93 -93
  121. package/tests/agent_helpers.test.ts +48 -0
  122. package/tests/bus.test.ts +121 -121
  123. package/tests/catalog.test.ts +86 -0
  124. package/tests/config.test.ts +41 -0
  125. package/tests/icons.test.ts +45 -45
  126. package/tests/memory.test.ts +147 -0
  127. package/tests/router.test.ts +86 -86
  128. package/tests/schemas.test.ts +51 -51
  129. package/tests/semantic.test.ts +83 -83
  130. package/tests/setup.ts +10 -10
  131. package/tests/skill.test.ts +172 -172
  132. package/tests/task.test.ts +60 -0
  133. package/tests/tool.test.ts +108 -108
  134. package/tests/tool_router.test.ts +71 -71
  135. package/vitest.config.ts +17 -17
package/src/cli/main.ts CHANGED
@@ -1,372 +1,425 @@
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 { readInput, type TUIContext } 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
+ let mode: "none" | "reasoning" | "content" = "none";
141
+ let atLineStart = true;
142
+
143
+ const writeContent = (text: string) => {
144
+ for (const ch of text) {
145
+ if (atLineStart) { process.stdout.write(" "); atLineStart = false; }
146
+ process.stdout.write(ch);
147
+ if (ch === "\n") atLineStart = true;
148
+ }
149
+ };
150
+
151
+ // Thinking indicator until the first event lands
152
+ process.stdout.write(chalk.dim(` ${theme.symbol} ${theme.pigment} …\r`));
153
+ let cleared = false;
154
+ const clearThinking = () => { if (!cleared) { process.stdout.write("\r" + " ".repeat(40) + "\r"); cleared = true; } };
155
+
156
+ for await (const ev of agent.chatStream(input)) {
157
+ switch (ev.type) {
158
+ case "reasoning":
159
+ clearThinking();
160
+ if (mode !== "reasoning") { process.stdout.write(chalk.dim("\n ◦ ")); mode = "reasoning"; }
161
+ process.stdout.write(chalk.dim.italic(String(ev.text).replace(/\n/g, " ")));
162
+ break;
163
+ case "content":
164
+ clearThinking();
165
+ if (mode !== "content") { process.stdout.write(mode === "reasoning" ? "\n\n" : "\n"); atLineStart = true; mode = "content"; }
166
+ writeContent(String(ev.text));
167
+ break;
168
+ case "tool_status":
169
+ clearThinking();
170
+ process.stdout.write("\n" + pigment(` ${theme.symbol} ${ev.tool_name}`) + chalk.dim(` ${ev.label || ""} …`) + "\n");
171
+ atLineStart = true; mode = "none";
172
+ break;
173
+ case "tool_done":
174
+ process.stdout.write((ev.success ? chalk.green(" ") : chalk.red("")) + chalk.dim(String(ev.tool_name)) + "\n");
175
+ atLineStart = true; mode = "none";
176
+ break;
177
+ case "truncated":
178
+ process.stdout.write(chalk.yellow(`\n ⚠ 截断: ${ev.reason}\n`));
179
+ break;
180
+ case "done":
181
+ break;
182
+ }
183
+ }
184
+ clearThinking();
185
+ process.stdout.write("\n\n");
186
+ }
187
+
188
+ /* ═══════════════════════════════════════
189
+ Chat loop
190
+ ═══════════════════════════════════════ */
191
+ /* API key persistence read from config file too */
192
+ function checkApiKeys(): string | null {
193
+ // Check env vars
194
+ const envKeys = ["DEEPSEEK_API_KEY","OPENAI_API_KEY","ANTHROPIC_API_KEY","GROQ_API_KEY","OPENROUTER_API_KEY"];
195
+ for (const k of envKeys) { if (process.env[k]) return "env:" + k; }
196
+ // Check config file
197
+ try {
198
+ const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
199
+ const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
200
+ if (fs.existsSync(cfgPath)) {
201
+ const cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {};
202
+ const keys = cfg.api_keys || {};
203
+ for (const [p, k] of Object.entries(keys)) { if (k) return "cfg:" + p; }
204
+ }
205
+ } catch { /* ignore */ }
206
+ return null;
207
+ }
208
+
209
+ /** Save API key to config file */
210
+ function saveApiKey(provider: string, key: string): void {
211
+ const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
212
+ const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
213
+ const dir = path.dirname(cfgPath);
214
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
215
+ let cfg: any = {};
216
+ if (fs.existsSync(cfgPath)) { try { cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {}; } catch { } }
217
+ if (!cfg.api_keys) cfg.api_keys = {};
218
+ cfg.api_keys[provider] = key;
219
+ fs.writeFileSync(cfgPath, yaml.stringify(cfg), "utf-8");
220
+ }
221
+
222
+ /* ═══════════════════════════════════════
223
+ Interactive setup wizard
224
+ ═══════════════════════════════════════ */
225
+ async function setupWizard(): Promise<{ provider: string; key: string; model: string } | null> {
226
+ // Derived from the single-source model catalog (config/models.yaml).
227
+ // Every listed model is callable — no hardcoded/fictional entries.
228
+ const providers = listProviders().map((id) => ({
229
+ id,
230
+ name: providerLabel(id),
231
+ models: modelsFor(id).map((m) => m.id),
232
+ }));
233
+
234
+ process.stdout.write("\n" + chalk.cyan(" ✦ API Key 设置向导 ✦\n\n"));
235
+ process.stdout.write(chalk.dim(" 选择 Provider(Key 保存在 ~/.skyloom/config.yaml):\n\n"));
236
+
237
+ for (let i = 0; i < providers.length; i++) {
238
+ process.stdout.write(chalk.dim(` ${String(i+1).padStart(2)}. ${providers[i].name.padEnd(22)} ${providers[i].models.slice(0,3).join(", ")}\n`));
239
+ }
240
+
241
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
242
+ const ask = (q: string): Promise<string> => new Promise(r => rl.question(q, r));
243
+
244
+ const choice = await ask(chalk.cyan("\n 编号 (1-"+providers.length+", q退出): "));
245
+ if (choice === "q") { rl.close(); return null; }
246
+ const idx = parseInt(choice) - 1;
247
+ if (isNaN(idx) || idx < 0 || idx >= providers.length) { rl.close(); process.stdout.write(chalk.dim(" 已取消\n")); return null; }
248
+
249
+ const prov = providers[idx];
250
+ const key = await ask(chalk.cyan(` ${prov.name} API Key: `));
251
+ if (!key.trim()) { rl.close(); return null; }
252
+
253
+ saveApiKey(prov.id, key.trim());
254
+
255
+ process.stdout.write(chalk.dim("\n 可用模型:\n"));
256
+ for (let i = 0; i < prov.models.length; i++) process.stdout.write(chalk.dim(` ${i+1}. ${prov.models[i]}\n`));
257
+
258
+ const mc = await ask(chalk.cyan("\n 选择模型 (1-"+prov.models.length+", 默认1): ")) || "1";
259
+ const mi = (parseInt(mc) || 1) - 1;
260
+ const model = prov.models[Math.max(0, Math.min(mi, prov.models.length - 1))];
261
+
262
+ // Save to config
263
+ const path = require("path"); const fs = require("fs"); const yaml = require("yaml");
264
+ const cfgPath = path.join(require("os").homedir(), ".skyloom", "config.yaml");
265
+ let cfg: any = {}; if (fs.existsSync(cfgPath)) { try { cfg = yaml.parse(fs.readFileSync(cfgPath, "utf-8")) || {}; } catch { } }
266
+ cfg.default_model = model; cfg.default_provider = prov.id;
267
+ fs.writeFileSync(cfgPath, yaml.stringify(cfg), "utf-8");
268
+
269
+ rl.close();
270
+ process.stdout.write(chalk.green(`\n ✓ ${prov.name} · ${model} · 就绪!\n\n`));
271
+ return { provider: prov.id, key: key.trim(), model };
272
+ }
273
+
274
+ async function chat(agentName: string, modelOverride?: string): Promise<void> {
275
+ const haveKey = checkApiKeys();
276
+ if (!haveKey) {
277
+ process.stdout.write("\n" + chalk.cyan(" 天空织机 Skyloom ✦\n"));
278
+ process.stdout.write(chalk.dim(" 检测到未配置 API Key,进入设置向导...\n\n"));
279
+ const result = await setupWizard();
280
+ if (!result) { process.stdout.write(chalk.red(" 设置未完成,请重新运行 sky 配置。\n")); process.exit(0); }
281
+ process.stdout.write(chalk.green(` ✓ ${result.provider} 已就绪 · 模型: ${result.model}\n\n`));
282
+ }
283
+
284
+ const ctx = createSystemContext();
285
+ let agent = ctx.agentMap.get(agentName);
286
+ if (!agent) { process.stdout.write(chalk.red("Unknown agent: " + agentName) + "\n"); return; }
287
+
288
+ // Validate the active model is real catches stale/fictional configs
289
+ // before they 404 mid-request.
290
+ try {
291
+ const cfg = loadConfig();
292
+ const activeModel = cfg.agents?.[agentName]?.model || (cfg as any).llm?.default_model;
293
+ const v = validateModel(activeModel);
294
+ if (!v.ok) {
295
+ process.stdout.write(chalk.yellow(`\n 配置的模型 "${activeModel || "(未设置)"}" 不在可用目录中。\n`));
296
+ process.stdout.write(chalk.dim(` 可选: ${v.suggestions.join(", ")}\n`));
297
+ process.stdout.write(chalk.dim(` 运行 /setup 重新选择,或编辑 ~/.skyloom/config.yaml。\n\n`));
298
+ }
299
+ } catch { /* validation is best-effort */ }
300
+
301
+ await agent.init();
302
+
303
+ // Wire up security approval prompt user for HIGH/CRITICAL operations
304
+ try {
305
+ const { getSecurity, DangerLevel } = require("../core/security");
306
+ const sec = getSecurity();
307
+ sec.setApprovalCallback(async (tool: string, args: Record<string, any>, level: number) => {
308
+ process.stdout.write(chalk.yellow(`\n ${tool} ( danger level ${level} )\n`));
309
+ process.stdout.write(chalk.dim(` args: ${JSON.stringify(args).slice(0, 80)}\n`));
310
+ const answer = await new Promise<string>(resolve => {
311
+ const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
312
+ rl2.question(chalk.red(" Approve? [y/N] "), (a: string) => { rl2.close(); resolve(a.trim().toLowerCase()); });
313
+ });
314
+ return answer === "y" || answer === "yes";
315
+ });
316
+ } catch { /* security module optional */ }
317
+
318
+ // eslint-disable-next-line prefer-const
319
+ let currentAgent = agent; // mutable for agent switching
320
+ welcome(agent);
321
+
322
+ process.stdout.write(chalk.dim(" Key: " + haveKey + "\n\n"));
323
+
324
+ // ── TUI loop ──
325
+ const ctx_: TUIContext = { agent: currentAgent, agents: ctx.agentMap, model: "default", cost: "$0", width: 80, height: 24 };
326
+
327
+ while (true) {
328
+ const inp = await readInput(process.stdin, process.stdout, ctx_);
329
+ if (!inp) continue;
330
+
331
+ const cmdL = inp.toLowerCase();
332
+
333
+ // Agent switch — stamp a mineral seal on change
334
+ let switched = false;
335
+ for (const n of AGENT_NAMES) {
336
+ if (cmdL === "/" + n) {
337
+ const a = ctx.agentMap.get(n);
338
+ if (a) {
339
+ await a.init(); currentAgent = a; ctx_.agent = a;
340
+ const t = agentTheme(n);
341
+ process.stdout.write("\n " + chalk.bold.hex(t.hex)(`▣ ${t.kanji} ${t.pigment}`) + chalk.dim(` · ${t.specialty}`) + "\n");
342
+ process.stdout.write(" " + chalk.dim.italic(t.poem) + "\n\n");
343
+ }
344
+ switched = true; break;
345
+ }
346
+ }
347
+ if (switched) continue;
348
+ if (cmdL === "/quit" || cmdL === "/exit") break;
349
+ if (cmdL === "/clear") { console.clear(); continue; }
350
+ if (cmdL === "/help") { process.stdout.write(helpText()); continue; }
351
+ if (cmdL === "/version") { process.stdout.write(" Skyloom v" + VERSION + "\n"); continue; }
352
+ 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; }
353
+ if (cmdL === "/cost") { process.stdout.write(chalk.bold("\n Total: " + formatCost(ctx.llm.getTotalCost()) + "\n\n")); continue; }
354
+ if (cmdL === "/cost reset") { (ctx.llm as any).resetUsageStats?.(); process.stdout.write(chalk.dim(" Stats reset\n")); continue; }
355
+ if (cmdL === "/compact") { const r = await currentAgent.compact(); process.stdout.write(chalk.green(" ✓ " + r + "\n\n")); continue; }
356
+ 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; }
357
+ if (cmdL === "/memory clear") { await currentAgent.memory.clearShortTerm(); process.stdout.write(chalk.dim(" Memory cleared\n")); continue; }
358
+ if (cmdL === "/workspace") { process.stdout.write(chalk.dim(" " + (ctx.workspacePath || "default") + "\n")); continue; }
359
+ 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; }
360
+ if (cmdL === "/mcp") { process.stdout.write(chalk.dim(" " + (ctx.mcpStatus?.join(", ") || "none") + "\n")); continue; }
361
+ 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; }
362
+ 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; }
363
+ if (cmdL.startsWith("/task ")) { const g = inp.slice(6); process.stdout.write(chalk.cyan("\n ✦ " + g + "\n\n")); await runTask(g); continue; }
364
+ if (cmdL === "/setup") { const r = await setupWizard(); if (r) process.stdout.write(chalk.green(` ${r.provider} · ${r.model} — Ready!\n`)); continue; }
365
+ if (cmdL.startsWith("/model")) { process.stdout.write(chalk.dim(" Run /setup to reconfigure models\n")); continue; }
366
+ if (inp.startsWith("/")) { process.stdout.write(helpText()); continue; }
367
+
368
+ // ── Chat (real streaming) ──
369
+ try {
370
+ await streamResponse(currentAgent, inp);
371
+ } catch (e: any) {
372
+ process.stdout.write("\r" + " ".repeat(40) + "\r");
373
+ process.stdout.write(chalk.red(" ✗ " + (e.message || e) + "\n\n"));
374
+ }
375
+ }
376
+
377
+ process.stdout.write(chalk.dim("\n Session ended\n"));
378
+ await ctx.closeAll();
379
+ process.exit(0);
380
+ }
381
+
382
+ /* ═══════════════════════════════════════
383
+ Task
384
+ ═══════════════════════════════════════ */
385
+ async function runTask(goal: string): Promise<void> {
386
+ const ctx = createSystemContext();
387
+ await ctx.initAll();
388
+ const [, results, summary] = await orchestrateTask(goal, ctx.agentMap);
389
+ for (const r of results) process.stdout.write(` ${r.success ? chalk.green("✓") : chalk.red("✗")} ${chalk.cyan(r.agent)}: ${r.description.slice(0, 60)}\n`);
390
+ process.stdout.write(chalk.bold("\n " + summary.slice(0, 800) + "\n\n"));
391
+ await ctx.closeAll();
392
+ }
393
+
394
+ function helpText(): string {
395
+ const groups: [string, [string, string][]][] = [
396
+ ["Agent", [["/fog /rain /frost", "Switch agents"], ["/snow /dew /fair", "Switch agents"]]],
397
+ ["Chat", [["/help", "Commands"], ["/clear", "Clear"], ["/compact", "Compress"], ["/retry", "Resend"]]],
398
+ ["Info", [["/status", "Status"], ["/cost", "Cost"], ["/memory", "Memory"], ["/sessions", "Sessions"], ["/workspace", "Workspace"], ["/version", "Version"]]],
399
+ ["Orch.", [["/task <goal>", "Multi-agent"]]],
400
+ ];
401
+ let s = "";
402
+ for (const [title, cmds] of groups) {
403
+ s += chalk.cyan(` ${title}\n`);
404
+ for (const [c, d] of cmds) s += ` ${chalk.cyan(c.padEnd(18))}${chalk.dim(d)}\n`;
405
+ }
406
+ s += "\n";
407
+ return s;
408
+ }
409
+
410
+ /* ═══════════════════════════════════════
411
+ Entry
412
+ ═══════════════════════════════════════ */
413
+ async function main() {
414
+ const args = process.argv.slice(2);
415
+ if (args.length === 0) { await chat("fog"); return; }
416
+ if ((AGENT_NAMES as readonly string[]).includes(args[0])) {
417
+ let m: string | undefined;
418
+ for (let i = 1; i < args.length; i++) if ((args[i] === "-m" || args[i] === "--model") && i + 1 < args.length) m = args[++i];
419
+ await chat(args[0], m); return;
420
+ }
421
+ if (!["chat", "task", "web", "config", "init", "version", "mcp", "help"].includes(args[0]) && !args[0].startsWith("-")) { await chat("fog"); return; }
422
+ program.parse(process.argv);
423
+ }
424
+
425
+ main().catch(e => { process.stderr.write(chalk.red(`Fatal: ${(e as Error).message}\n`)); process.exit(1); });