skyloom 1.4.2 → 1.4.3

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli/main.ts +525 -338
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skyloom",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "天空织机 Skyloom — 6 weather-themed AI agents: Fog, Rain, Frost, Snow, Dew, Fair",
5
5
  "preferGlobal": true,
6
6
  "type": "commonjs",
package/src/cli/main.ts CHANGED
@@ -1,412 +1,599 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  /**
4
- * CLI interface for Skyloom terminal agent product.
5
- * Uses Commander.js for command routing.
3
+ * 天空织机 CLI Skyloom Terminal Interface
4
+ * Raw-mode input + slash command popup + streaming display
6
5
  */
7
-
8
- import { Command } from 'commander';
9
- import * as fs from 'fs';
10
- import * as readline from 'readline';
11
- import chalk from 'chalk';
12
- import { createSystemContext, orchestrateTask } from '../core/factory';
13
- import { loadConfig, USER_CONFIG_DIR } from '../core/config';
14
- import { classify } from '../core/router';
15
- import { InteractiveMode, ModeController } from './mode';
6
+ import { Command } from "commander";
7
+ import * as fs from "fs";
8
+ import * as readline from "readline";
9
+ import chalk from "chalk";
10
+ import { createSystemContext, orchestrateTask } from "../core/factory";
11
+ import { loadConfig, USER_CONFIG_DIR } from "../core/config";
12
+ import { classify } from "../core/router";
13
+ import { InteractiveMode, ModeController } from "./mode";
16
14
 
17
15
  const MODE = new ModeController();
18
- const VERSION = '1.4.0';
19
-
20
- const program = new Command();
16
+ const VERSION = "1.4.2";
17
+
18
+ /* ── Agent colors ── */
19
+ const AGENT_COLORS: Record<string, string> = {
20
+ fog: "#b8c6db", rain: "#4a90d9", frost: "#2cd4d4",
21
+ snow: "#e8ecf1", dew: "#7bed9f", fair: "#f7b733",
22
+ };
23
+ const AGENT_DISPLAY: Record<string, string> = {
24
+ fog: "≋ 雾 Fog", rain: "⸽ 雨 Rain", frost: "✱ 霜 Frost",
25
+ snow: "❉ 雪 Snow", dew: "∘ 露 Dew", fair: "☼ 晴 Fair",
26
+ };
27
+ const AGENT_NAMES = ["fog", "rain", "frost", "snow", "dew", "fair"] as const;
28
+
29
+ /* ═══════════════════════════════════════
30
+ Commander program
31
+ ═══════════════════════════════════════ */
32
+ const program = new Command()
33
+ .name("sky").description("天空织机 Skyloom — 6 weather-themed AI agents").version(VERSION);
34
+
35
+ program.command("chat").description("Start interactive chat")
36
+ .argument("[agent]", "agent name", "fog")
37
+ .option("-m,--model <model>", "Model override")
38
+ .action(async (a: string, o: { model?: string }) => { await chat(a, o.model); });
39
+
40
+ program.command("task").description("Multi-agent orchestration")
41
+ .argument("[goal]", "task goal")
42
+ .option("-r,--resume", "resume from checkpoint")
43
+ .action(async (g?: string, o?: { resume?: boolean }) => { if (g) await runTask(g, o?.resume); });
44
+
45
+ program.command("web").description("Start web server")
46
+ .option("-p,--port <port>", "port", "3000")
47
+ .action(async (o: { port?: string }) => { const { startWebServer } = await import("../web/server"); await startWebServer(parseInt(o.port || "3000", 10)); });
48
+
49
+ program.command("mcp").description("Start MCP server")
50
+ .action(async () => { const { startMCPServer } = await import("../core/mcp_server"); await startMCPServer(); });
51
+
52
+ program.command("config").description("Show configuration")
53
+ .action(() => { const c = loadConfig(); logLine(chalk.cyan("Config dir: ") + USER_CONFIG_DIR); logLine(chalk.cyan("Agent models:")); for (const [n, a] of Object.entries(c.agents || {})) logLine(` ${chalk.bold(n)}: ${(a as any).model || "default"}`); });
54
+
55
+ program.command("init").description("Initialize config directory")
56
+ .action(() => { const d = USER_CONFIG_DIR; if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); logLine(chalk.green("✓ ") + d); });
57
+
58
+ program.command("version").description("Show version")
59
+ .action(() => logLine(`Skyloom v${VERSION}`));
60
+
61
+ /* ═══════════════════════════════════════
62
+ Interactive Chat — raw-mode input + popup
63
+ ═══════════════════════════════════════ */
64
+ const SLASH_CMDS: [string, string, boolean, string][] = [
65
+ ["/help", "Show all commands", false, ""],
66
+ ["/clear", "Clear screen", false, ""],
67
+ ["/status", "Agent overview", false, ""],
68
+ ["/cost", "Usage & cost", false, ""],
69
+ ["/cost reset", "Reset usage stats", false, ""],
70
+ ["/compact", "Compress context", false, ""],
71
+ ["/retry", "Resend last message", false, ""],
72
+ ["/mcp", "MCP server status", false, ""],
73
+ ["/memory", "Memory stats", false, ""],
74
+ ["/sessions", "Session list", false, ""],
75
+ ["/workspace", "Workspace info", false, ""],
76
+ ["/model", "Model info", false, ""],
77
+ ["/version", "Version info", false, ""],
78
+ ["/task <goal>", "Multi-agent orchestrate", true, ""],
79
+ ["/quiz", "Export chat as quiz", false, ""],
80
+ ["/fog", "≋ Fog — research", false, "fog"],
81
+ ["/rain", "⸽ Rain — codegen", false, "rain"],
82
+ ["/frost", "✱ Frost — review", false, "frost"],
83
+ ["/snow", "❉ Snow — planning", false, "snow"],
84
+ ["/dew", "∘ Dew — devops", false, "dew"],
85
+ ["/fair", "☼ Fair — companion", false, "fair"],
86
+ ["/quit", "Exit chat", false, ""],
87
+ ["/exit", "Exit chat", false, ""],
88
+ ];
89
+
90
+ function logLine(s: string) { process.stdout.write(s + "\n"); }
91
+
92
+ /* ── Stream response with spinner ── */
93
+ async function chatWithSpinner(
94
+ agent: any, ctx: any, message: string
95
+ ): Promise<string> {
96
+ let frame = 0;
97
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
98
+ const spinner = setInterval(() => {
99
+ readline.cursorTo(process.stdout, 0);
100
+ process.stdout.write(chalk.cyan(` ${frames[frame % frames.length]} ${agent.displayName} thinking...`));
101
+ frame++;
102
+ }, 80);
103
+
104
+ try {
105
+ const response = await agent.chat(message);
106
+ clearInterval(spinner);
107
+ readline.cursorTo(process.stdout, 0);
108
+ process.stdout.write(" ".repeat(50) + "\r");
109
+ return response;
110
+ } catch (e) {
111
+ clearInterval(spinner);
112
+ throw e;
113
+ }
114
+ }
21
115
 
22
- program
23
- .name('sky')
24
- .description('Skyloom CLI multi-agent orchestration framework')
25
- .version(VERSION);
116
+ /* ── Render response ── */
117
+ function renderResponse(text: string): string[] {
118
+ const lines: string[] = [];
119
+ const w = process.stdout.columns || 80;
120
+ const maxW = Math.min(w - 6, 76);
121
+ for (const block of text.split("\n\n")) {
122
+ const trimmed = block.trim();
123
+ if (!trimmed) continue;
124
+ // Code blocks
125
+ if (trimmed.startsWith("```")) {
126
+ const codeLines = trimmed.split("\n");
127
+ lines.push(chalk.dim(" ┌─ code ──────────────"));
128
+ for (let i = 1; i < codeLines.length - 1; i++) {
129
+ const cl = codeLines[i];
130
+ lines.push(` ${chalk.dim("│")} ${chalk.white(cl.slice(0, maxW - 4))}`);
131
+ }
132
+ lines.push(chalk.dim(" └────────────────────"));
133
+ continue;
134
+ }
135
+ // Wrap long lines
136
+ for (const line of trimmed.split("\n")) {
137
+ if (line.startsWith("#")) {
138
+ lines.push(" " + chalk.bold(line));
139
+ } else if (line.startsWith("- ") || line.startsWith("* ")) {
140
+ lines.push(" " + chalk.dim("• ") + line.slice(2));
141
+ } else {
142
+ let remaining = line;
143
+ while (remaining.length > maxW) {
144
+ const cut = remaining.lastIndexOf(" ", maxW);
145
+ const idx = cut > 0 ? cut : maxW;
146
+ lines.push(" " + remaining.slice(0, idx));
147
+ remaining = remaining.slice(idx).trimStart();
148
+ }
149
+ if (remaining) lines.push(" " + remaining);
150
+ }
151
+ }
152
+ }
153
+ return lines;
154
+ }
26
155
 
27
- // ── Chat command ──
156
+ /* ── Welcome banner ── */
157
+ function printWelcome(agent: any, model: string) {
158
+ const w = process.stdout.columns || 80;
159
+ logLine("");
160
+ logLine(" ".repeat(Math.max(0, Math.floor((w - 40) / 2))) + chalk.cyan("✦ 天 空 织 机 ✦"));
161
+ logLine(" ".repeat(Math.max(0, Math.floor((w - 36) / 2))) + chalk.dim("S K Y L O O M"));
162
+ logLine("");
163
+ const agentLine: string[] = [];
164
+ for (const name of AGENT_NAMES) {
165
+ const active = name === agent.name;
166
+ const prefix = active ? chalk.bold(AGENT_DISPLAY[name].split(" ").slice(0, 2).join(" ")) : chalk.dim(AGENT_DISPLAY[name].split(" ")[0] + " " + AGENT_DISPLAY[name].split(" ")[1]);
167
+ agentLine.push(prefix);
168
+ }
169
+ logLine(" " + agentLine.join(chalk.dim(" · ")));
170
+ logLine("");
171
+ logLine(chalk.dim(` Model: ${model} · /help for commands · /quit to exit`));
172
+ logLine("");
173
+ }
28
174
 
29
- program
30
- .command('chat')
31
- .description('Start interactive chat with an agent')
32
- .argument('[agent]', 'Agent name (fog, rain, frost, snow, dew, fair)', 'fog')
33
- .option('-m, --model <model>', 'Model to use')
34
- .action(async (agentName: string, options: { model?: string }) => {
35
- try {
36
- await interactiveChat(agentName, options.model);
37
- } catch (e) {
38
- console.error(chalk.red(`Error: ${e}`));
39
- process.exit(1);
40
- }
41
- });
175
+ /* ── Status bar ── */
176
+ function statusBar(agent: any, ctx: any): string {
177
+ let ctxStr = "";
178
+ let costStr = "$0";
179
+ let modelStr = "default";
180
+ try {
181
+ const cu = agent.contextUsage();
182
+ modelStr = cu.model || "?";
183
+ const pct = cu.pct || 0;
184
+ const barColor = pct < 50 ? chalk.green : pct < 80 ? chalk.yellow : chalk.red;
185
+ const barLen = Math.round(pct / 10);
186
+ ctxStr = `${barColor("█".repeat(barLen) + "░".repeat(10 - barLen))} ${pct}%`;
187
+ costStr = formatCost(ctx.llm.getTotalCost());
188
+ } catch { /* ignore */ }
189
+ const w = process.stdout.columns || 80;
190
+ return chalk.dim(`┤ ${ctxStr} · ${costStr} · ${modelStr} ├${"─".repeat(Math.max(0, w - 60))}`);
191
+ }
42
192
 
43
- // ── Task command ──
44
-
45
- program
46
- .command('task')
47
- .description('Execute a multi-agent orchestration task')
48
- .argument('[goal]', 'Task goal description')
49
- .option('-r, --resume', 'Resume from checkpoint')
50
- .action(async (goal?: string, options?: { resume?: boolean }) => {
51
- if (!goal) {
52
- console.log(chalk.yellow('Please provide a task goal. Usage: sky task "<goal>"'));
53
- return;
54
- }
55
- try {
56
- await runTask(goal, options?.resume);
57
- } catch (e) {
58
- console.error(chalk.red(`Error: ${e}`));
59
- }
60
- });
193
+ function formatCost(cost: number): string {
194
+ if (cost >= 1) return chalk.yellow(`$${cost.toFixed(2)}`);
195
+ if (cost >= 0.01) return chalk.yellow(`$${cost.toFixed(4)}`);
196
+ if (cost > 0) return chalk.green(`${(cost * 100).toFixed(2)}¢`);
197
+ return "$0";
198
+ }
61
199
 
62
- // ── Web command ──
200
+ /* ── Slash-command popup ── */
201
+ async function readWithPopup(agent: any, ctx: any): Promise<string> {
202
+ if (!process.stdin.isTTY) {
203
+ return new Promise<string>(resolve => {
204
+ const rl = readline.createInterface({ input: process.stdin });
205
+ rl.on("line", (line) => { rl.close(); resolve(line.trim()); });
206
+ });
207
+ }
63
208
 
64
- program
65
- .command('web')
66
- .description('Start web server')
67
- .option('-p, --port <port>', 'Port to listen on', '3000')
68
- .action(async (options: { port?: string }) => {
69
- try {
70
- // Dynamic import to avoid loading express when not needed
71
- const { startWebServer } = await import('../web/server');
72
- const port = parseInt(options.port || '3000', 10);
73
- await startWebServer(port);
74
- } catch (e) {
75
- console.error(chalk.red(`Web server error: ${e}`));
76
- }
77
- });
209
+ return new Promise<string>(resolve => {
210
+ const stdin = process.stdin;
211
+ try { stdin.setRawMode?.(true); } catch { /* non-TTY */ }
212
+ stdin.resume();
213
+
214
+ let buf = "";
215
+ let cursor = 0;
216
+ let popup = false;
217
+ let selIdx = 0;
218
+ const _history: string[] = (readWithPopup as any)._popupHistory || [];
219
+ let histIdx = _history.length;
220
+
221
+ function render() {
222
+ const w = process.stdout.columns || 80;
223
+ readline.cursorTo(process.stdout, 0);
224
+
225
+ // Clear current line area
226
+ const promptLine = ` ${chalk.cyan(agent.displayName)} ${chalk.dim("❯")} `;
227
+
228
+ // Build filtered commands
229
+ const filtered = popup ? SLASH_CMDS.filter(c => c[0].toLowerCase().includes(buf.toLowerCase())) : [];
230
+ if (filtered.length && selIdx >= filtered.length) selIdx = filtered.length - 1;
231
+
232
+ const popupH = Math.min(filtered.length, 10);
233
+ const totalH = popup ? popupH + 3 : 1;
234
+
235
+ // Move cursor up
236
+ if (popup) {
237
+ for (let i = 0; i < popupH + 2; i++) process.stdout.write("\x1b[1A\x1b[2K");
238
+ } else {
239
+ process.stdout.write("\x1b[2K\r");
240
+ }
78
241
 
79
- // ── MCP command ──
242
+ // Input line
243
+ const before = buf.slice(0, cursor);
244
+ const after = buf.slice(cursor);
245
+ const cursorChar = after[0] || " ";
246
+ process.stdout.write(promptLine + before + chalk.inverse(cursorChar) + after.slice(1) + "\n");
247
+
248
+ // Popup
249
+ if (popup && filtered.length) {
250
+ const maxW = Math.min(w - 4, 60);
251
+ const start = Math.max(0, Math.min(selIdx - 4, filtered.length - 8));
252
+ const end = Math.min(filtered.length, start + 8);
253
+ process.stdout.write(chalk.dim(` ┌─ commands (↑↓ pick · type to filter · tab/enter select · esc close)${"─".repeat(Math.max(0, maxW - 58))}┐\n`));
254
+ for (let i = start; i < end; i++) {
255
+ const [cmd, desc] = filtered[i];
256
+ const marker = i === selIdx ? chalk.cyan(" ▶ ") : " ";
257
+ const cmdColored = i === selIdx ? chalk.bold(cmd) : chalk.cyan(cmd);
258
+ const line = ` │${marker}${cmdColored.padEnd(24)}${chalk.dim(desc)}`;
259
+ process.stdout.write(line + " ".repeat(Math.max(0, maxW - line.length + 6)) + "│\n");
260
+ }
261
+ process.stdout.write(chalk.dim(` └${"─".repeat(maxW + 1)}┘\n`));
262
+ }
80
263
 
81
- program
82
- .command('mcp')
83
- .description('Start MCP server (stdio JSON-RPC for Claude Desktop etc.)')
84
- .action(async () => {
85
- try {
86
- console.error(chalk.cyan('Starting MCP server on stdio...'));
87
- const { startMCPServer } = await import('../core/mcp_server');
88
- await startMCPServer();
89
- } catch (e) {
90
- console.error(chalk.red(`MCP server error: ${e}`));
91
- process.exit(1);
264
+ // Status bar
265
+ process.stdout.write(statusBar(agent, ctx) + (popup ? "" : "\n"));
92
266
  }
93
- });
94
267
 
95
- // ── Config command ──
96
-
97
- program
98
- .command('config')
99
- .description('Show current configuration')
100
- .action(() => {
101
- const config = loadConfig();
102
- console.log(chalk.bold('\nSkyloom Configuration'));
103
- console.log(chalk.dim('─'.repeat(40)));
104
- console.log(chalk.cyan('Config dir:'), USER_CONFIG_DIR);
105
- console.log(chalk.cyan('Agents:'));
106
- for (const [name, cfg] of Object.entries(config.agents || {})) {
107
- console.log(` ${chalk.bold(name)}: ${cfg.model || 'default'}`);
268
+ function accept(line: string) {
269
+ try { stdin.setRawMode?.(false); } catch { }
270
+ stdin.pause();
271
+ resolve(line);
108
272
  }
109
- });
110
-
111
- // ── Init / Setup command ──
112
273
 
113
- program
114
- .command('init')
115
- .description('Initialize Skyloom configuration')
116
- .action(() => {
117
- const configDir = USER_CONFIG_DIR;
118
- if (!fs.existsSync(configDir)) {
119
- fs.mkdirSync(configDir, { recursive: true });
120
- }
121
- console.log(chalk.green(`✓ Initialized config at ${configDir}`));
122
- console.log(chalk.dim('Edit config.yaml in that directory to configure agents and models.'));
123
- });
274
+ stdin.on("data", (data: Buffer) => {
275
+ const seq = data.toString();
276
+ for (const ch of seq) {
277
+ // Esc
278
+ if (ch === "\x1b") {
279
+ if (popup) { popup = false; render(); return; }
280
+ accept(""); return;
281
+ }
282
+ // Enter
283
+ if (ch === "\r" || ch === "\n") {
284
+ if (popup && SLASH_CMDS.filter(c => c[0].toLowerCase().includes(buf.toLowerCase())).length) {
285
+ const filtered = SLASH_CMDS.filter(c => c[0].toLowerCase().includes(buf.toLowerCase()));
286
+ buf = filtered[selIdx]?.[0] || buf;
287
+ popup = false;
288
+ }
289
+ accept(buf.trim()); return;
290
+ }
291
+ // Tab
292
+ if (ch === "\t") {
293
+ if (popup && SLASH_CMDS.filter(c => c[0].toLowerCase().includes(buf.toLowerCase())).length) {
294
+ const filtered = SLASH_CMDS.filter(c => c[0].toLowerCase().includes(buf.toLowerCase()));
295
+ buf = filtered[selIdx]?.[0] || buf;
296
+ cursor = buf.length;
297
+ popup = false;
298
+ render(); return;
299
+ }
300
+ // Insert 2 spaces
301
+ buf = buf.slice(0, cursor) + " " + buf.slice(cursor);
302
+ cursor += 2;
303
+ render(); return;
304
+ }
305
+ // Backspace
306
+ if (ch === "\x7f" || ch === "\b") {
307
+ if (cursor > 0) { buf = buf.slice(0, cursor - 1) + buf.slice(cursor); cursor--; }
308
+ if (!buf) { popup = false; }
309
+ render(); return;
310
+ }
311
+ // Ctrl+C
312
+ if (ch === "\x03") { accept("/quit"); return; }
313
+ // Printable
314
+ if (ch >= " ") {
315
+ buf = buf.slice(0, cursor) + ch + buf.slice(cursor);
316
+ cursor++;
317
+ if (buf === "/") { popup = true; selIdx = 0; }
318
+ else if (popup) { selIdx = 0; }
319
+ render(); return;
320
+ }
321
+ }
322
+ });
323
+
324
+ // Arrow keys come as escape sequences — handle via process.stdin
325
+ // For simplicity, arrow keys in raw mode are: \x1b[A (up), \x1b[B (down), \x1b[C (right), \x1b[D (left)
326
+ // We handle them in the data handler above by checking for escape sequences
327
+ // Actually the raw mode data comes byte by byte — let me use a state machine approach
328
+ // For now, let me handle the common case: the full escape sequence arrives in one data event
329
+
330
+ // Note: Arrow keys are 3 bytes: \x1b [ A/B/C/D. They may arrive in one or multiple data events.
331
+ // In practice on Windows they arrive as one event. Let me add a simple buffered approach.
332
+
333
+ stdin.removeAllListeners("data");
334
+
335
+ let escBuf = "";
336
+ stdin.on("data", (data: Buffer) => {
337
+ const str = data.toString();
338
+ escBuf += str;
339
+
340
+ // If we have an escape sequence, process it
341
+ if (escBuf.startsWith("\x1b[")) {
342
+ if (escBuf.length >= 3) {
343
+ const code = escBuf[2];
344
+ escBuf = "";
345
+ if (code === "A") {
346
+ if (popup) { selIdx = Math.max(0, selIdx - 1); } else if (_history.length) { histIdx = Math.max(0, histIdx - 1); buf = _history[histIdx] || ""; cursor = buf.length; }
347
+ render(); return;
348
+ }
349
+ if (code === "B") {
350
+ if (popup) { const f = SLASH_CMDS.filter(c => c[0].toLowerCase().includes(buf.toLowerCase())); selIdx = Math.min(f.length - 1, selIdx + 1); }
351
+ else if (_history.length && histIdx < _history.length) { histIdx++; buf = _history[histIdx] || ""; cursor = buf.length; }
352
+ render(); return;
353
+ }
354
+ if (code === "C") { if (cursor < buf.length) cursor++; if (popup) popup = false; render(); return; }
355
+ if (code === "D") { if (cursor > 0) cursor--; if (popup) popup = false; render(); return; }
356
+ } else { return; /* wait for more bytes */ }
357
+ }
124
358
 
125
- // ── Version command ──
359
+ // Not an escape sequence — process normally
360
+ for (const ch of escBuf) {
361
+ escBuf = "";
362
+ if (ch === "\x1b") { if (popup) { popup = false; render(); } return; }
363
+ if (ch === "\r" || ch === "\n") {
364
+ if (popup && SLASH_CMDS.filter(c => c[0].toLowerCase().includes(buf.toLowerCase())).length) {
365
+ buf = SLASH_CMDS.filter(c => c[0].toLowerCase().includes(buf.toLowerCase()))[selIdx]?.[0] || buf;
366
+ popup = false;
367
+ }
368
+ accept(buf.trim()); return;
369
+ }
370
+ if (ch === "\t") {
371
+ if (popup && SLASH_CMDS.filter(c => c[0].toLowerCase().includes(buf.toLowerCase())).length) {
372
+ const filtered = SLASH_CMDS.filter(c => c[0].toLowerCase().includes(buf.toLowerCase()));
373
+ buf = filtered[selIdx]?.[0] || buf;
374
+ cursor = buf.length; popup = false;
375
+ render(); return;
376
+ }
377
+ buf = buf.slice(0, cursor) + " " + buf.slice(cursor); cursor += 2;
378
+ render(); return;
379
+ }
380
+ if (ch === "\x7f" || ch === "\b") { if (cursor > 0) { buf = buf.slice(0, cursor - 1) + buf.slice(cursor); cursor--; } if (!buf) popup = false; render(); return; }
381
+ if (ch === "\x03") { accept("/quit"); return; }
382
+ if (ch >= " ") {
383
+ buf = buf.slice(0, cursor) + ch + buf.slice(cursor); cursor++;
384
+ if (buf === "/") { popup = true; selIdx = 0; }
385
+ else if (popup) selIdx = 0;
386
+ render(); return;
387
+ }
388
+ }
389
+ });
126
390
 
127
- program
128
- .command('version')
129
- .description('Show version')
130
- .action(() => {
131
- console.log(`Skyloom v${VERSION}`);
391
+ render();
132
392
  });
393
+ }
133
394
 
134
- // ── Interactive chat ──
135
-
136
- async function interactiveChat(agentName: string, modelOverride?: string): Promise<void> {
395
+ /* ═══════════════════════════════════════
396
+ Main chat loop
397
+ ═══════════════════════════════════════ */
398
+ async function chat(agentName: string, modelOverride?: string): Promise<void> {
137
399
  const ctx = createSystemContext();
138
- const agent = ctx.agentMap.get(agentName);
139
- if (!agent) {
140
- console.error(chalk.red(`Unknown agent: ${agentName}. Available: ${[...ctx.agentMap.keys()].join(', ')}`));
141
- return;
142
- }
143
-
400
+ let agent = ctx.agentMap.get(agentName);
401
+ if (!agent) { logLine(chalk.red(`Unknown agent: ${agentName}`)); return; }
144
402
  await agent.init();
145
- const color = getAgentColor(agentName);
146
-
147
- console.log();
148
- console.log(chalk.cyan('≈ S K Y L O O M ≈'));
149
- console.log(chalk.dim(`Agent: ${chalk.bold(agent.displayName)} · Model: ${modelOverride || 'default'}`));
150
- console.log(chalk.dim('Type /help for commands, /quit to exit'));
151
- console.log();
152
-
153
- const rl = readline.createInterface({
154
- input: process.stdin,
155
- output: process.stdout,
156
- prompt: '',
157
- });
403
+
404
+ const model = modelOverride || "default";
405
+ printWelcome(agent, model);
158
406
 
159
407
  const inputHistory: string[] = [];
160
408
 
161
- const processInput = async (input: string): Promise<void> => {
162
- const cmd = input.trim();
163
- if (!cmd) return;
409
+ while (true) {
410
+ /* ── Read input ── */
411
+ const inp = await readWithPopup(agent, ctx);
412
+ if (!inp) { logLine(""); continue; }
164
413
 
165
- // Slash commands
166
- if (cmd === '/quit' || cmd === '/exit') {
167
- rl.close();
168
- return;
414
+ // Save to history
415
+ if (!inputHistory[inputHistory.length - 1] || inputHistory[inputHistory.length - 1] !== inp) {
416
+ inputHistory.push(inp);
417
+ if (inputHistory.length > 50) inputHistory.shift();
169
418
  }
170
- if (cmd === '/help') {
171
- printHelp();
172
- return;
419
+ (readWithPopup as any)._popupHistory = inputHistory;
420
+
421
+ const cmd = inp.trim();
422
+ const cmdLower = cmd.toLowerCase();
423
+
424
+ /* ── Slash commands ── */
425
+ let handled = false;
426
+
427
+ // Agent switching
428
+ for (const n of AGENT_NAMES) {
429
+ if (cmdLower === `/${n}`) {
430
+ const newAgent = ctx.agentMap.get(n);
431
+ if (newAgent) {
432
+ await newAgent.init();
433
+ // Switch agent reference (mutate closure)
434
+ logLine(chalk.dim(`\n ⟳ ${AGENT_DISPLAY[n]}\n`));
435
+ // We can't reassign the outer `agent` const, so use a workaround
436
+ (chat as any)._currentAgent = newAgent;
437
+ (chat as any)._currentCtx = ctx;
438
+ // Actually, let me handle this differently — replace the agent in the closure
439
+ agent = newAgent;
440
+ }
441
+ handled = true; break;
442
+ }
173
443
  }
174
- if (cmd === '/clear') {
175
- console.clear();
176
- return;
444
+
445
+ if (cmdLower === "/quit" || cmdLower === "/exit") break;
446
+ if (cmdLower === "/help") { printHelp(); handled = true; }
447
+ if (cmdLower === "/clear") { console.clear(); handled = true; }
448
+ if (cmdLower === "/version") { logLine(` Skyloom v${VERSION}`); handled = true; }
449
+ if (cmdLower === "/status") {
450
+ logLine(chalk.bold(`\n ${agent.displayName} (${agent.name})`));
451
+ logLine(chalk.dim(` State: ${agent.state} · Specialty: ${agent.specialty}`));
452
+ logLine(chalk.dim(` Memory: ${agent.memory.shortTerm.length} messages · ${Object.keys(agent.memory.working).length} working keys`));
453
+ handled = true;
177
454
  }
178
- if (cmd === '/status') {
179
- const status = agent.getStatus();
180
- console.log(chalk.bold('\nAgent Status'));
181
- console.log(chalk.dim('─'.repeat(40)));
182
- console.log(` ${chalk.cyan(status.displayName)} (${status.name})`);
183
- console.log(` State: ${status.state}`);
184
- console.log(` Specialty: ${status.specialty}`);
185
- if (status.skills?.length) {
186
- const active = status.skills.filter((s: any) => s.active).map((s: any) => s.name);
187
- if (active.length) console.log(` Active skills: ${chalk.green(active.join(', '))}`);
188
- }
189
- return;
455
+ if (cmdLower === "/cost") {
456
+ logLine(chalk.bold("\n Usage & Cost"));
457
+ logLine(chalk.dim(" ─".repeat(24)));
458
+ logLine(` Total: ${formatCost(ctx.llm.getTotalCost())}`);
459
+ handled = true;
190
460
  }
191
- if (cmd === '/cost') {
192
- const totalCost = ctx.llm.getTotalCost();
193
- console.log(` Total cost: ${chalk.green(formatCost(totalCost))}`);
194
- return;
461
+ if (cmdLower === "/cost reset") { (ctx.llm as any).resetUsageStats?.(); logLine(chalk.dim(" Stats reset")); handled = true; }
462
+ if (cmdLower === "/compact") {
463
+ logLine(chalk.dim(" Compacting..."));
464
+ const r = await agent.compact();
465
+ logLine(chalk.green(` ✓ ${r}`));
466
+ handled = true;
195
467
  }
196
- if (cmd === '/compact') {
197
- const result = await agent.compact();
198
- console.log(chalk.green(` ${result}`));
199
- return;
468
+ if (cmdLower === "/memory") {
469
+ logLine(chalk.bold("\n Memory"));
470
+ logLine(chalk.dim(` Short-term: ${agent.memory.shortTerm.length} msgs · Working: ${Object.keys(agent.memory.working).length} keys`));
471
+ handled = true;
200
472
  }
201
- if (cmd === '/version') {
202
- console.log(` Skyloom v${VERSION}`);
203
- return;
473
+ if (cmdLower === "/workspace") {
474
+ logLine(chalk.dim(`\n Workspace: ${ctx.workspacePath || "default"}`));
475
+ handled = true;
204
476
  }
205
- if (cmd.startsWith('/model')) {
206
- console.log(chalk.dim(' Model management: use config.yaml to change models'));
207
- return;
477
+ if (cmdLower === "/mcp") {
478
+ logLine(chalk.dim(`\n MCP servers: ${ctx.mcpStatus?.length ? ctx.mcpStatus.join(", ") : "none configured"}`));
479
+ handled = true;
208
480
  }
209
- if (cmd.startsWith('/task ')) {
210
- const goal = cmd.slice(6).trim();
211
- if (goal) {
212
- console.log(chalk.cyan(`\n Orchestrating: ${goal}\n`));
213
- await runTask(goal);
214
- }
215
- return;
481
+ if (cmdLower === "/sessions") {
482
+ const sessions = await agent.memory.listSessions();
483
+ if (sessions.length) {
484
+ logLine(chalk.bold("\n Sessions"));
485
+ for (const s of sessions.slice(0, 10)) {
486
+ logLine(chalk.dim(` ${s.id?.slice(0, 10)}... ${s.preview || ""} (${s.messageCount || 0} msgs)`));
487
+ }
488
+ } else { logLine(chalk.dim(" No saved sessions")); }
489
+ handled = true;
216
490
  }
217
- if (cmd.startsWith('/')) {
218
- printHelp();
219
- return;
491
+ if (cmdLower.startsWith("/model")) { logLine(chalk.dim(` Model: ${modelOverride || "default"}. Configure in ~/.skyloom/config.yaml`)); handled = true; }
492
+ if (cmdLower.startsWith("/task ")) {
493
+ const goal = cmd.slice(6).trim();
494
+ if (goal) { logLine(chalk.cyan(`\n ✦ Orchestrating: ${goal}\n`)); await runTask(goal); }
495
+ handled = true;
220
496
  }
221
497
 
222
- // Save to history
223
- if (!inputHistory.includes(cmd)) {
224
- inputHistory.push(cmd);
225
- if (inputHistory.length > 50) inputHistory.shift();
226
- }
498
+ if (handled) { logLine(""); continue; }
227
499
 
228
- // Classify and route
500
+ /* ── Route message ── */
229
501
  const mode = MODE.current;
230
502
  if (mode === InteractiveMode.PLAN) {
231
- console.log(chalk.magenta('\n [PLAN mode] Routing to orchestrator...\n'));
232
- await runTask(cmd);
233
- return;
503
+ await runTask(cmd); logLine(""); continue;
234
504
  }
235
505
 
236
506
  const cls = classify(cmd);
237
- if (cls === 'orchestrate' && mode !== InteractiveMode.AUTO) {
238
- await runTask(cmd);
239
- return;
507
+ if (cls === "orchestrate" && mode !== InteractiveMode.AUTO) {
508
+ await runTask(cmd); logLine(""); continue;
240
509
  }
241
510
 
242
- // Single-agent chat
243
- process.stdout.write(`\n ${chalk.cyan(agent.displayName)} ${chalk.dim('thinking...')}\n`);
244
-
511
+ /* ── Chat ── */
245
512
  try {
246
- const response = await agent.chat(cmd);
247
- process.stdout.write('\n');
248
- console.log(chalk.white(response));
249
- } catch (e) {
250
- console.error(chalk.red(`\n Error: ${e}`));
251
- }
252
-
253
- // AUTO mode: continue if model signals more work
254
- if (mode === InteractiveMode.AUTO) {
255
- // Simple auto-continue check
256
- const lastMsg = agent.memory.shortTerm[agent.memory.shortTerm.length - 1];
257
- if (lastMsg && lastMsg.content && shouldAutoContinue(lastMsg.content)) {
258
- process.stdout.write(chalk.yellow('\n [auto-continue]\n'));
259
- // Re-trigger
260
- try {
261
- const response = await agent.chat('请继续完成');
262
- process.stdout.write('\n');
263
- console.log(chalk.white(response));
264
- } catch (e) {
265
- console.error(chalk.red(`\n Error: ${e}`));
513
+ const response = await chatWithSpinner(agent, ctx, cmd);
514
+ logLine("");
515
+ for (const line of renderResponse(response)) {
516
+ logLine(line);
517
+ }
518
+ logLine("");
519
+
520
+ // Auto-continue
521
+ if (mode === InteractiveMode.AUTO) {
522
+ const tail = response.split("\n").slice(-6).join("\n");
523
+ if (/(?:接下来|下一步|继续|next|let me|I'[vl]l)/i.test(tail) && !/(?:完成了|全部完成|all done)/i.test(tail)) {
524
+ logLine(chalk.yellow(" [auto-continue]\n"));
525
+ try {
526
+ const r2 = await chatWithSpinner(agent, ctx, "请继续完成");
527
+ logLine("");
528
+ for (const line of renderResponse(r2)) logLine(line);
529
+ logLine("");
530
+ } catch { /* ignore */ }
266
531
  }
267
532
  }
268
- }
269
- };
270
-
271
- rl.on('line', async (line) => {
272
- try {
273
- await processInput(line);
274
533
  } catch (e) {
275
- console.error(chalk.red(`Error: ${e}`));
534
+ logLine(chalk.red(`\n ✗ Error: ${(e as Error).message || e}\n`));
276
535
  }
277
- rl.prompt();
278
- });
279
-
280
- rl.on('close', () => {
281
- console.log(chalk.dim('\n Session ended'));
282
- ctx.closeAll().catch(() => {});
283
- process.exit(0);
284
- });
536
+ }
285
537
 
286
- rl.prompt();
538
+ logLine(chalk.dim("\n Session ended"));
539
+ await ctx.closeAll();
540
+ process.exit(0);
287
541
  }
288
542
 
543
+ /* ═══════════════════════════════════════
544
+ Task execution
545
+ ═══════════════════════════════════════ */
289
546
  async function runTask(goal: string, resume?: boolean): Promise<void> {
290
547
  const ctx = createSystemContext();
291
548
  await ctx.initAll();
549
+ const [, results, summary] = await orchestrateTask(goal, ctx.agentMap, null, {
550
+ resultTruncate: 500, maxTaskRetries: 3, maxReplanRounds: 1, resume,
551
+ });
292
552
 
293
- const [_tasks, results, summary] = await orchestrateTask(
294
- goal,
295
- ctx.agentMap,
296
- null,
297
- {
298
- resultTruncate: 500,
299
- maxTaskRetries: 3,
300
- maxReplanRounds: 1,
301
- resume,
302
- }
303
- );
304
-
305
- console.log(chalk.bold('\n Task Results'));
306
- console.log(chalk.dim(' ─'.repeat(30)));
307
-
553
+ logLine(chalk.bold("\n Task Results"));
554
+ logLine(chalk.dim(" ─".repeat(30)));
308
555
  for (const r of results) {
309
- const status = r.success ? chalk.green('') : chalk.red('');
310
- console.log(` ${status} ${chalk.cyan(r.agent)}: ${r.description.slice(0, 60)}...`);
556
+ logLine(` ${r.success ? chalk.green("") : chalk.red("")} ${chalk.cyan(r.agent)}: ${r.description.slice(0, 60)}`);
311
557
  }
312
-
313
- console.log(chalk.bold('\n Summary'));
314
- console.log(chalk.dim(' ─'.repeat(30)));
315
- console.log(` ${summary.slice(0, 1000)}`);
316
- console.log();
317
-
558
+ logLine(chalk.bold("\n Summary"));
559
+ logLine(chalk.dim(" ─".repeat(30)));
560
+ logLine(` ${summary.slice(0, 1000)}`);
561
+ logLine("");
318
562
  await ctx.closeAll();
319
563
  }
320
564
 
321
- function printHelp(): void {
322
- console.log(chalk.bold('\n Commands'));
323
- console.log(chalk.dim(''.repeat(30)));
324
- const cmds = [
325
- ['/help', 'Show this help'],
326
- ['/clear', 'Clear screen'],
327
- ['/status', 'Agent status'],
328
- ['/cost', 'Usage & cost'],
329
- ['/compact', 'Compress context'],
330
- ['/version', 'Version info'],
331
- ['/task <goal>', 'Multi-agent task'],
332
- ['/quit', 'Exit chat'],
333
- ['', ''],
334
- ['Switch agents:', ''],
335
- ['/fog', 'Fog — research'],
336
- ['/rain', 'Rain — codegen'],
337
- ['/frost', 'Frost — review'],
338
- ['/snow', 'Snow — planning'],
339
- ['/dew', 'Dew — devops'],
340
- ['/fair', 'Fair — companion'],
565
+ function printHelp() {
566
+ logLine(chalk.bold("\n Slash Commands"));
567
+ logLine(chalk.dim("".repeat(40)));
568
+ const groups: [string, [string, string][]][] = [
569
+ ["Agent", [["/fog /rain /frost", "Switch agents"], ["/snow /dew /fair", "Switch agents"]]],
570
+ ["Chat", [["/help", "Show commands"], ["/clear", "Clear screen"], ["/compact", "Compress context"], ["/retry", "Resend last msg"], ["/quit", "Exit"]]],
571
+ ["Info", [["/status", "Agent status"], ["/cost", "Usage & cost"], ["/memory", "Memory stats"], ["/sessions", "Session list"], ["/workspace", "Workspace info"], ["/version", "Version"]]],
572
+ ["Orch.", [["/task <goal>", "Multi-agent task"]]],
341
573
  ];
342
- for (const [cmd, desc] of cmds) {
343
- if (cmd) {
344
- console.log(` ${chalk.cyan(cmd.padEnd(20))}${chalk.dim(desc)}`);
345
- } else {
346
- console.log();
347
- }
574
+ for (const [title, cmds] of groups) {
575
+ logLine(chalk.cyan(` ${title}`));
576
+ for (const [c, d] of cmds) logLine(` ${chalk.cyan(c.padEnd(18))}${chalk.dim(d)}`);
348
577
  }
349
- console.log();
350
- }
351
-
352
- function getAgentColor(name: string): string {
353
- const colors: Record<string, string> = {
354
- fog: 'bright_white', rain: 'blue', frost: 'cyan',
355
- snow: 'bright_white', dew: 'green', fair: '#FFD700',
356
- };
357
- return colors[name] || 'white';
578
+ logLine("");
358
579
  }
359
580
 
360
- function formatCost(cost: number): string {
361
- if (cost >= 1.0) return `$${cost.toFixed(2)}`;
362
- if (cost >= 0.01) return `$${cost.toFixed(4)}`;
363
- if (cost > 0.0) return `${(cost * 100).toFixed(2)}¢`;
364
- return '$0';
365
- }
366
-
367
- function shouldAutoContinue(text: string): boolean {
368
- const autoContinuePattern = /(?:接下来|下一步|下面我|然后我|接着|继续|next|let me\s|I'[vl]l\s)/i;
369
- const autoStopPattern = /(?:完成了|全部完成|以上就|all done|task complete)/i;
370
- const tail = text.split('\n').slice(-6).join('\n');
371
- if (autoStopPattern.test(tail)) return false;
372
- return autoContinuePattern.test(tail);
373
- }
374
-
375
- // ── Parse CLI args and run ──
376
-
377
- async function main(): Promise<void> {
581
+ /* ═══════════════════════════════════════
582
+ Entry
583
+ ═══════════════════════════════════════ */
584
+ async function main() {
378
585
  const args = process.argv.slice(2);
379
-
380
- // `sky` with no args = start chat directly (fastest path)
381
- if (args.length === 0) {
382
- await interactiveChat('fog');
383
- return;
384
- }
385
-
386
- // `sky <agent>` or `sky <agent> -m <model>` — chat with specific agent
387
- const knownAgents = new Set(['fog', 'rain', 'frost', 'snow', 'dew', 'fair']);
388
- if (knownAgents.has(args[0])) {
389
- let model: string | undefined;
586
+ if (args.length === 0) { await chat("fog"); return; }
587
+ if ((AGENT_NAMES as readonly string[]).includes(args[0])) {
588
+ let m: string | undefined;
390
589
  for (let i = 1; i < args.length; i++) {
391
- if ((args[i] === '-m' || args[i] === '--model') && i + 1 < args.length) {
392
- model = args[++i];
393
- }
590
+ if ((args[i] === "-m" || args[i] === "--model") && i + 1 < args.length) m = args[++i];
394
591
  }
395
- await interactiveChat(args[0], model);
396
- return;
592
+ await chat(args[0], m); return;
397
593
  }
398
-
399
- // `sky <message>` anything not a known subcommand → fast chat
400
- const knownCommands = ['chat', 'task', 'web', 'config', 'init', 'version', 'mcp', 'help'];
401
- if (!knownCommands.includes(args[0]) && !args[0].startsWith('-')) {
402
- await interactiveChat('fog');
403
- return;
404
- }
405
-
594
+ const subCmds = ["chat", "task", "web", "config", "init", "version", "mcp", "help"];
595
+ if (!subCmds.includes(args[0]) && !args[0].startsWith("-")) { await chat("fog"); return; }
406
596
  program.parse(process.argv);
407
597
  }
408
598
 
409
- main().catch((e) => {
410
- console.error(chalk.red(`Fatal error: ${e}`));
411
- process.exit(1);
412
- });
599
+ main().catch(e => { logLine(chalk.red(`Fatal: ${(e as Error).message}`)); process.exit(1); });