skyloom 1.4.4 → 1.5.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.
@@ -0,0 +1,47 @@
1
+ # Skyloom default configuration
2
+ llm:
3
+ default_model: gpt-4o
4
+ language: zh
5
+ max_retries: 2
6
+ temperature: 0.7
7
+ max_tokens: 4096
8
+
9
+ agents:
10
+ fog:
11
+ model: gpt-4o
12
+ temperature: 0.7
13
+ rain:
14
+ model: gpt-4o
15
+ temperature: 0.7
16
+ frost:
17
+ model: gpt-4o
18
+ temperature: 0.3
19
+ snow:
20
+ model: gpt-4o
21
+ temperature: 0.5
22
+ dew:
23
+ model: gpt-4o
24
+ temperature: 0.3
25
+ fair:
26
+ model: gpt-4o
27
+ temperature: 0.9
28
+
29
+ memory:
30
+ db_path: ~/.skyloom/memory.db
31
+ short_term_limit: 100
32
+ max_persisted_messages: 2000
33
+
34
+ workspace:
35
+ path: auto
36
+
37
+ cli:
38
+ default_agent: fog
39
+ approval_mode: interactive
40
+
41
+ plugins:
42
+ enabled: true
43
+ directories:
44
+ - ~/.skyloom/plugins
45
+
46
+ mcp:
47
+ servers: []
@@ -0,0 +1,39 @@
1
+ # Provider catalog — API key env vars, base URLs, docs
2
+ openai:
3
+ env_var: OPENAI_API_KEY
4
+ base_url: https://api.openai.com/v1
5
+ docs_url: https://platform.openai.com/api-keys
6
+
7
+ anthropic:
8
+ env_var: ANTHROPIC_API_KEY
9
+ base_url: https://api.anthropic.com/v1
10
+ docs_url: https://console.anthropic.com/settings/keys
11
+
12
+ deepseek:
13
+ env_var: DEEPSEEK_API_KEY
14
+ base_url: https://api.deepseek.com/v1
15
+ docs_url: https://platform.deepseek.com/api_keys
16
+
17
+ groq:
18
+ env_var: GROQ_API_KEY
19
+ base_url: https://api.groq.com/openai/v1
20
+
21
+ mistral:
22
+ env_var: MISTRAL_API_KEY
23
+ base_url: https://api.mistral.ai/v1
24
+
25
+ cohere:
26
+ env_var: COHERE_API_KEY
27
+ base_url: https://api.cohere.ai/v1
28
+
29
+ openrouter:
30
+ env_var: OPENROUTER_API_KEY
31
+ base_url: https://openrouter.ai/api/v1
32
+
33
+ gemini:
34
+ env_var: GEMINI_API_KEY
35
+ base_url: https://generativelanguage.googleapis.com/v1beta
36
+
37
+ ollama:
38
+ base_url: http://localhost:11434/v1
39
+ env_var: OLLAMA_HOST
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skyloom",
3
- "version": "1.4.4",
3
+ "version": "1.5.0",
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,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * 天空织机 CLI — Skyloom Terminal Interface
4
- * Raw-mode input + slash command popup + streaming display
5
4
  */
6
5
  import { Command } from "commander";
7
6
  import * as fs from "fs";
@@ -13,13 +12,8 @@ import { classify } from "../core/router";
13
12
  import { InteractiveMode, ModeController } from "./mode";
14
13
 
15
14
  const MODE = new ModeController();
16
- const VERSION = "1.4.2";
15
+ const VERSION = "1.4.4";
17
16
 
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
17
  const AGENT_DISPLAY: Record<string, string> = {
24
18
  fog: "≋ 雾 Fog", rain: "⸽ 雨 Rain", frost: "✱ 霜 Frost",
25
19
  snow: "❉ 雪 Snow", dew: "∘ 露 Dew", fair: "☼ 晴 Fair",
@@ -27,555 +21,242 @@ const AGENT_DISPLAY: Record<string, string> = {
27
21
  const AGENT_NAMES = ["fog", "rain", "frost", "snow", "dew", "fair"] as const;
28
22
 
29
23
  /* ═══════════════════════════════════════
30
- Commander program
24
+ Slash commands registry
31
25
  ═══════════════════════════════════════ */
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, ""],
26
+ const SLASH_CMDS: [string, string][] = [
27
+ ["/help", "Show all commands"],
28
+ ["/clear", "Clear screen"],
29
+ ["/status", "Agent overview"],
30
+ ["/cost", "Usage & cost"],
31
+ ["/cost reset", "Reset usage stats"],
32
+ ["/compact", "Compress context"],
33
+ ["/retry", "Resend last msg"],
34
+ ["/memory", "Memory stats"],
35
+ ["/memory clear", "Clear short-term memory"],
36
+ ["/sessions", "Session list"],
37
+ ["/workspace", "Workspace info"],
38
+ ["/model", "Model info"],
39
+ ["/mcp", "MCP server status"],
40
+ ["/version", "Version info"],
41
+ ["/task <goal>", "Multi-agent orchestrate"],
42
+ ["/fog", "≋ Fog — research insight"],
43
+ ["/rain", " Rain — creation codegen"],
44
+ ["/frost", "✱ Frost review quality"],
45
+ ["/snow", "❉ Snow — planning architect"],
46
+ ["/dew", " Dew — devops reliability"],
47
+ ["/fair", " Fair companion warmth"],
48
+ ["/quit", "Exit chat"],
49
+ ["/exit", "Exit chat"],
88
50
  ];
89
51
 
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
- }
115
-
116
- /* ── Render response ── */
117
- function renderResponse(text: string): string[] {
118
- const lines: string[] = [];
52
+ function showPopup(cmds: [string, string][], selIdx: number) {
119
53
  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
- }
54
+ const start = Math.max(0, Math.min(selIdx - 4, cmds.length - 8));
55
+ const end = Math.min(cmds.length, start + 8);
56
+ process.stdout.write(chalk.dim(" ┌─ commands (↑↓ pick · type letter to filter · tab/enter select) ─┐\n"));
57
+ for (let i = start; i < end; i++) {
58
+ const [cmd, desc] = cmds[i];
59
+ const marker = i === selIdx ? chalk.cyan("") : " ";
60
+ process.stdout.write(` │${marker}${chalk.cyan(cmd.padEnd(24))}${chalk.dim(desc)}${" ".repeat(Math.max(0, 50 - desc.length))}│\n`);
152
61
  }
153
- return lines;
62
+ process.stdout.write(chalk.dim(` └${"─".repeat(60)}┘\n`));
154
63
  }
155
64
 
156
- /* ── Welcome banner ── */
157
- function printWelcome(agent: any, model: string) {
65
+ /* ═══════════════════════════════════════
66
+ Commander
67
+ ═══════════════════════════════════════ */
68
+ const program = new Command()
69
+ .name("sky").description("天空织机 Skyloom").version(VERSION);
70
+
71
+ program.command("chat").argument("[agent]", "agent name", "fog")
72
+ .option("-m,--model <m>", "model").action(async (a: string, o: { model?: string }) => { await chat(a, o.model); });
73
+ program.command("task").argument("[goal]", "task goal")
74
+ .action(async (g?: string) => { if (g) await runTask(g); });
75
+ program.command("web").option("-p,--port <p>", "port", "3000")
76
+ .action((o: { port?: string }) => { import("../web/server").then(m => m.startWebServer(parseInt(o.port || "3000"))); });
77
+ program.command("mcp").action(() => { import("../core/mcp_server").then(m => m.startMCPServer()); });
78
+ 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`); });
79
+ 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"); });
80
+ program.command("version").action(() => { process.stdout.write(`Skyloom v${VERSION}\n`); });
81
+
82
+ /* ═══════════════════════════════════════
83
+ Welcome
84
+ ═══════════════════════════════════════ */
85
+ function welcome(agent: any) {
158
86
  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);
87
+ const pad = " ".repeat(Math.max(0, Math.floor((w - 34) / 2)));
88
+ process.stdout.write("\n" + pad + chalk.cyan("✦ 天 空 织 机 ✦\n"));
89
+ process.stdout.write(pad + chalk.dim("S K Y L O O M\n\n"));
90
+ const parts: string[] = [];
91
+ for (const n of AGENT_NAMES) {
92
+ const a = n === agent.name;
93
+ const s = `${AGENT_DISPLAY[n].split(" ")[0]} ${AGENT_DISPLAY[n].split(" ")[1]}`;
94
+ parts.push(a ? chalk.bold.cyan(s) : chalk.dim(s));
168
95
  }
169
- logLine(" " + agentLine.join(chalk.dim(" · ")));
170
- logLine("");
171
- logLine(chalk.dim(` Model: ${model} · /help for commands · /quit to exit`));
172
- logLine("");
96
+ process.stdout.write(" " + parts.join(chalk.dim(" · ")) + "\n\n");
97
+ process.stdout.write(chalk.dim(" /help for commands · /quit to exit\n\n"));
173
98
  }
174
99
 
175
- /* ── Status bar ── */
176
100
  function statusBar(agent: any, ctx: any): string {
177
- let ctxStr = "";
178
- let costStr = "$0";
179
- let modelStr = "default";
180
101
  try {
181
102
  const cu = agent.contextUsage();
182
- modelStr = cu.model || "?";
183
103
  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))}`);
104
+ const bar = pct < 50 ? chalk.green : pct < 80 ? chalk.yellow : chalk.red;
105
+ const filled = Math.round(pct / 10);
106
+ const ctxBar = `${bar("█".repeat(filled) + "░".repeat(10 - filled))} ${pct}%`;
107
+ const cost = formatCost(ctx.llm.getTotalCost());
108
+ return chalk.dim(`${ctxBar} · ${cost} · ${cu.model || "?"}`);
109
+ } catch { return ""; }
191
110
  }
192
111
 
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)}¢`);
112
+ function formatCost(c: number): string {
113
+ if (c >= 1) return chalk.yellow(`$${c.toFixed(2)}`);
114
+ if (c >= 0.01) return chalk.yellow(`$${c.toFixed(4)}`);
115
+ if (c > 0) return chalk.green(`${(c * 100).toFixed(2)}¢`);
197
116
  return "$0";
198
117
  }
199
118
 
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
- }
208
-
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
- }
241
-
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`));
119
+ /* ═══════════════════════════════════════
120
+ Response render
121
+ ═══════════════════════════════════════ */
122
+ function render(text: string): string[] {
123
+ const out: string[] = [];
124
+ for (const para of text.split("\n\n")) {
125
+ const t = para.trim();
126
+ if (!t) continue;
127
+ if (t.startsWith("```")) {
128
+ const lines = t.split("\n");
129
+ out.push(chalk.dim(" ╭─ code ──"));
130
+ for (let i = 1; i < lines.length - 1; i++) out.push(` ${chalk.dim("│")} ${chalk.gray(lines[i].slice(0, 72))}`);
131
+ out.push(chalk.dim(" ╰────────"));
132
+ } else {
133
+ for (const line of t.split("\n")) {
134
+ if (line.startsWith("# ")) out.push(" " + chalk.bold(line));
135
+ else if (line.startsWith("- ") || line.startsWith("* ")) out.push(" " + chalk.dim("• ") + line.slice(2));
136
+ else out.push(" " + line);
262
137
  }
263
-
264
- // Status bar
265
- process.stdout.write(statusBar(agent, ctx) + (popup ? "" : "\n"));
266
- }
267
-
268
- function accept(line: string) {
269
- try { stdin.setRawMode?.(false); } catch { }
270
- stdin.pause();
271
- resolve(line);
272
138
  }
273
-
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
- }
358
-
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
- });
390
-
391
- render();
392
- });
139
+ }
140
+ return out;
393
141
  }
394
142
 
395
143
  /* ═══════════════════════════════════════
396
- Main chat loop
144
+ Chat loop
397
145
  ═══════════════════════════════════════ */
398
146
  async function chat(agentName: string, modelOverride?: string): Promise<void> {
399
147
  const ctx = createSystemContext();
400
148
  let agent = ctx.agentMap.get(agentName);
401
- if (!agent) { logLine(chalk.red(`Unknown agent: ${agentName}`)); return; }
149
+ if (!agent) { process.stdout.write(chalk.red(`Unknown agent: ${agentName}\n`)); return; }
402
150
  await agent.init();
151
+ welcome(agent);
403
152
 
404
- const model = modelOverride || "default";
405
- printWelcome(agent, model);
406
-
407
- const inputHistory: string[] = [];
153
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
154
+ const history: string[] = [];
408
155
 
409
- while (true) {
410
- /* ── Read input ── */
411
- const inp = await readWithPopup(agent, ctx);
412
- if (!inp) { logLine(""); continue; }
156
+ for await (const line of rl) {
157
+ const inp = line.trim();
158
+ if (!inp) continue;
413
159
 
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();
160
+ if (inp[0] !== "/" && !history.includes(inp)) {
161
+ history.push(inp);
162
+ if (history.length > 50) history.shift();
418
163
  }
419
- (readWithPopup as any)._popupHistory = inputHistory;
420
164
 
421
- const cmd = inp.trim();
422
- const cmdLower = cmd.toLowerCase();
423
-
424
- /* ── Slash commands ── */
165
+ const cmdLower = inp.toLowerCase();
425
166
  let handled = false;
426
167
 
427
- // Agent switching
168
+ // Agent switch
428
169
  for (const n of AGENT_NAMES) {
429
170
  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
- }
171
+ const a = ctx.agentMap.get(n);
172
+ if (a) { await a.init(); agent = a; process.stdout.write(chalk.dim(` ⟳ ${AGENT_DISPLAY[n]}\n`)); }
441
173
  handled = true; break;
442
174
  }
443
175
  }
444
176
 
177
+ if (handled) continue;
445
178
  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;
454
- }
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;
460
- }
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;
467
- }
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;
472
- }
473
- if (cmdLower === "/workspace") {
474
- logLine(chalk.dim(`\n Workspace: ${ctx.workspacePath || "default"}`));
475
- handled = true;
476
- }
477
- if (cmdLower === "/mcp") {
478
- logLine(chalk.dim(`\n MCP servers: ${ctx.mcpStatus?.length ? ctx.mcpStatus.join(", ") : "none configured"}`));
479
- handled = true;
480
- }
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;
490
- }
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;
496
- }
497
-
498
- if (handled) { logLine(""); continue; }
499
-
500
- /* ── Route message ── */
501
- const mode = MODE.current;
502
- if (mode === InteractiveMode.PLAN) {
503
- await runTask(cmd); logLine(""); continue;
504
- }
179
+ if (cmdLower === "/help") { process.stdout.write(helpText()); handled = true; }
180
+ if (cmdLower === "/clear") { console.clear(); welcome(agent); handled = true; }
181
+ if (cmdLower === "/version") { process.stdout.write(` Skyloom v${VERSION}\n`); handled = true; }
182
+ if (cmdLower === "/status") { process.stdout.write(chalk.bold(`\n ${agent.displayName} (${agent.name})\n`) + chalk.dim(` State: ${agent.state} · Memory: ${agent.memory.shortTerm.length} msgs\n\n`)); handled = true; }
183
+ if (cmdLower === "/cost") { process.stdout.write(chalk.bold(`\n Total: ${formatCost(ctx.llm.getTotalCost())}\n\n`)); handled = true; }
184
+ if (cmdLower === "/cost reset") { (ctx.llm as any).resetUsageStats?.(); process.stdout.write(chalk.dim(" Reset\n\n")); handled = true; }
185
+ if (cmdLower === "/compact") { const r = await agent.compact(); process.stdout.write(chalk.green(` ✓ ${r}\n\n`)); handled = true; }
186
+ if (cmdLower === "/memory") { process.stdout.write(chalk.dim(` Short-term: ${agent.memory.shortTerm.length} msgs · Working: ${Object.keys(agent.memory.working).length} keys\n\n`)); handled = true; }
187
+ if (cmdLower === "/workspace") { process.stdout.write(chalk.dim(` ${ctx.workspacePath || "default"}\n\n`)); handled = true; }
188
+ if (cmdLower === "/mcp") { process.stdout.write(chalk.dim(` ${ctx.mcpStatus?.join(", ") || "none"}\n\n`)); handled = true; }
189
+ if (cmdLower === "/sessions") { const ss = await agent.memory.listSessions(); if (ss.length) { for (const s of ss.slice(0, 10)) process.stdout.write(chalk.dim(` ${s.id?.slice(0, 10)}... ${s.preview || ""} (${s.messageCount || 0} msgs)\n`)); } else process.stdout.write(chalk.dim(" No saved sessions\n")); process.stdout.write("\n"); handled = true; }
190
+ if (cmdLower.startsWith("/model")) { process.stdout.write(chalk.dim(" Configure in ~/.skyloom/config.yaml\n\n")); handled = true; }
191
+
192
+ if (handled) continue;
193
+
194
+ // Task orchestration
195
+ if (cmdLower.startsWith("/task ")) { const g = inp.slice(6).trim(); if (g) { process.stdout.write(chalk.cyan(`\n ✦ ${g}\n\n`)); await runTask(g); } continue; }
196
+
197
+ // Unknown slash help
198
+ if (inp.startsWith("/")) { process.stdout.write(helpText()); continue; }
199
+
200
+ // ── Chat ──
201
+ process.stdout.write(chalk.dim(` ${agent.displayName} thinking...\r`));
505
202
 
506
- const cls = classify(cmd);
507
- if (cls === "orchestrate" && mode !== InteractiveMode.AUTO) {
508
- await runTask(cmd); logLine(""); continue;
509
- }
510
-
511
- /* ── Chat ── */
512
203
  try {
513
- const response = await chatWithSpinner(agent, ctx, cmd);
514
- logLine("");
515
- for (const line of renderResponse(response)) {
516
- logLine(line);
517
- }
518
- logLine("");
204
+ const response = await agent.chat(inp);
205
+ process.stdout.write("\r" + " ".repeat(40) + "\r");
206
+ for (const l of render(response)) process.stdout.write(l + "\n");
207
+ process.stdout.write("\n");
208
+ // Status bar
209
+ process.stdout.write(" " + statusBar(agent, ctx) + "\n");
210
+ } catch (e) {
211
+ process.stdout.write(chalk.red(`\n ✗ ${(e as Error).message || e}\n\n`));
212
+ }
519
213
 
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 */ }
531
- }
214
+ // Auto-continue
215
+ if (MODE.current === InteractiveMode.AUTO && agent.memory.shortTerm.length) {
216
+ const last = agent.memory.shortTerm[agent.memory.shortTerm.length - 1];
217
+ if (last?.content && /(?:接下来|下一步|继续|next|let me|I'[vl]l)/i.test(last.content.split("\n").slice(-4).join("\n"))) {
218
+ process.stdout.write(chalk.yellow(" [auto]\n"));
219
+ try {
220
+ const r2 = await agent.chat("请继续完成");
221
+ process.stdout.write("\n");
222
+ for (const l of render(r2)) process.stdout.write(l + "\n");
223
+ process.stdout.write("\n");
224
+ } catch { /* ignore */ }
532
225
  }
533
- } catch (e) {
534
- logLine(chalk.red(`\n ✗ Error: ${(e as Error).message || e}\n`));
535
226
  }
536
227
  }
537
228
 
538
- logLine(chalk.dim("\n Session ended"));
229
+ process.stdout.write(chalk.dim("\n Session ended\n"));
539
230
  await ctx.closeAll();
540
231
  process.exit(0);
541
232
  }
542
233
 
543
234
  /* ═══════════════════════════════════════
544
- Task execution
235
+ Task
545
236
  ═══════════════════════════════════════ */
546
- async function runTask(goal: string, resume?: boolean): Promise<void> {
237
+ async function runTask(goal: string): Promise<void> {
547
238
  const ctx = createSystemContext();
548
239
  await ctx.initAll();
549
- const [, results, summary] = await orchestrateTask(goal, ctx.agentMap, null, {
550
- resultTruncate: 500, maxTaskRetries: 3, maxReplanRounds: 1, resume,
551
- });
552
-
553
- logLine(chalk.bold("\n Task Results"));
554
- logLine(chalk.dim(" ─".repeat(30)));
555
- for (const r of results) {
556
- logLine(` ${r.success ? chalk.green("✓") : chalk.red("✗")} ${chalk.cyan(r.agent)}: ${r.description.slice(0, 60)}`);
557
- }
558
- logLine(chalk.bold("\n Summary"));
559
- logLine(chalk.dim(" ─".repeat(30)));
560
- logLine(` ${summary.slice(0, 1000)}`);
561
- logLine("");
240
+ const [, results, summary] = await orchestrateTask(goal, ctx.agentMap);
241
+ for (const r of results) process.stdout.write(` ${r.success ? chalk.green("✓") : chalk.red("✗")} ${chalk.cyan(r.agent)}: ${r.description.slice(0, 60)}\n`);
242
+ process.stdout.write(chalk.bold("\n " + summary.slice(0, 800) + "\n\n"));
562
243
  await ctx.closeAll();
563
244
  }
564
245
 
565
- function printHelp() {
566
- logLine(chalk.bold("\n Slash Commands"));
567
- logLine(chalk.dim(" ─".repeat(40)));
246
+ function helpText(): string {
568
247
  const groups: [string, [string, string][]][] = [
569
248
  ["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"]]],
249
+ ["Chat", [["/help", "Commands"], ["/clear", "Clear"], ["/compact", "Compress"], ["/retry", "Resend"]]],
250
+ ["Info", [["/status", "Status"], ["/cost", "Cost"], ["/memory", "Memory"], ["/sessions", "Sessions"], ["/workspace", "Workspace"], ["/version", "Version"]]],
251
+ ["Orch.", [["/task <goal>", "Multi-agent"]]],
573
252
  ];
253
+ let s = "";
574
254
  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)}`);
255
+ s += chalk.cyan(` ${title}\n`);
256
+ for (const [c, d] of cmds) s += ` ${chalk.cyan(c.padEnd(18))}${chalk.dim(d)}\n`;
577
257
  }
578
- logLine("");
258
+ s += "\n";
259
+ return s;
579
260
  }
580
261
 
581
262
  /* ═══════════════════════════════════════
@@ -586,14 +267,11 @@ async function main() {
586
267
  if (args.length === 0) { await chat("fog"); return; }
587
268
  if ((AGENT_NAMES as readonly string[]).includes(args[0])) {
588
269
  let m: string | undefined;
589
- for (let i = 1; i < args.length; i++) {
590
- if ((args[i] === "-m" || args[i] === "--model") && i + 1 < args.length) m = args[++i];
591
- }
270
+ for (let i = 1; i < args.length; i++) if ((args[i] === "-m" || args[i] === "--model") && i + 1 < args.length) m = args[++i];
592
271
  await chat(args[0], m); return;
593
272
  }
594
- const subCmds = ["chat", "task", "web", "config", "init", "version", "mcp", "help"];
595
- if (!subCmds.includes(args[0]) && !args[0].startsWith("-")) { await chat("fog"); return; }
273
+ if (!["chat", "task", "web", "config", "init", "version", "mcp", "help"].includes(args[0]) && !args[0].startsWith("-")) { await chat("fog"); return; }
596
274
  program.parse(process.argv);
597
275
  }
598
276
 
599
- main().catch(e => { logLine(chalk.red(`Fatal: ${(e as Error).message}`)); process.exit(1); });
277
+ main().catch(e => { process.stderr.write(chalk.red(`Fatal: ${(e as Error).message}\n`)); process.exit(1); });
package/src/core/llm.ts CHANGED
@@ -662,63 +662,129 @@ export class LLMClient {
662
662
  }
663
663
 
664
664
  /**
665
- * Complete with retry logic (placeholder).
665
+ * Complete with retry logic — real HTTP call to LLM API.
666
666
  */
667
667
  private async completeWithRetry(
668
668
  model: string,
669
- _messages: Record<string, unknown>[],
670
- _agentName?: string,
671
- _tools?: string[],
672
- _stream: boolean = false,
669
+ messages: Record<string, unknown>[],
670
+ agentName?: string,
671
+ tools?: string[],
672
+ stream: boolean = false,
673
673
  overrides?: Record<string, unknown>
674
674
  ): Promise<LLMResponse> {
675
- // This is a placeholder. Real implementation would:
676
- // 1. Validate cache
677
- // 2. Call actual LLM API (OpenAI, Anthropic, etc.)
678
- // 3. Apply Anthropic cache control if needed
679
- // 4. Handle retry logic with exponential backoff
680
- // 5. Track usage and cost
681
- // 6. Cache results if appropriate
682
-
683
- const _temperature = (overrides?.temperature as number) ?? 0.7;
684
- const _maxTokens = (overrides?.maxTokens as number) ?? 2000;
685
-
686
- // For now, return a dummy response
687
- return {
688
- content: "Placeholder response from LLM",
689
- toolCalls: [],
690
- model,
691
- usage: { promptTokens: 100, completionTokens: 50 },
692
- cost: estimateCost(model, 100, 50),
693
- truncated: false,
694
- };
675
+ const temperature = (overrides?.temperature as number) ?? 0.7;
676
+ const maxTokens = (overrides?.maxTokens as number) ?? 4096;
677
+ const maxRetries = (this.config.llm as any)?.maxRetries ?? 2;
678
+ const isAnthropic = model.includes("claude") || model.startsWith("anthropic/");
679
+
680
+ let lastError: Error | null = null;
681
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
682
+ try {
683
+ if (attempt > 0) await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
684
+
685
+ let content: string;
686
+ let toolCalls: ToolCall[] = [];
687
+ let usage: UsageStats = { promptTokens: 0, completionTokens: 0 };
688
+
689
+ if (isAnthropic) {
690
+ const r = await this.callAnthropic(model, messages, tools, temperature, maxTokens);
691
+ content = r.content; toolCalls = r.toolCalls; usage = r.usage;
692
+ } else {
693
+ const r = await this.callOpenAI(model, messages, tools, temperature, maxTokens);
694
+ content = r.content; toolCalls = r.toolCalls; usage = r.usage;
695
+ }
696
+
697
+ const name = agentName || "default";
698
+ if (!this.usageStats.has(name)) this.usageStats.set(name, { prompt_tokens: 0, completion_tokens: 0, calls: 0, cost: 0 });
699
+ const s = this.usageStats.get(name)!;
700
+ s.prompt_tokens += usage.promptTokens; s.completion_tokens += usage.completionTokens; s.calls += 1;
701
+ const cost = estimateCost(model, usage.promptTokens, usage.completionTokens);
702
+ s.cost += cost; this.totalCost += cost;
703
+
704
+ return { content, toolCalls, model, usage, cost, truncated: false };
705
+ } catch (e: any) {
706
+ lastError = e;
707
+ if (attempt >= maxRetries) throw e;
708
+ }
709
+ }
710
+ throw lastError || new Error("Unknown error");
711
+ }
712
+
713
+ private async callOpenAI(
714
+ m: string, messages: Record<string, unknown>[], tools?: string[], temp?: number, maxTok?: number
715
+ ): Promise<{ content: string; toolCalls: ToolCall[]; usage: UsageStats }> {
716
+ const apiKey = this.getApiKey(m);
717
+ const baseUrl = this.getBaseUrl(m);
718
+ const body: Record<string, unknown> = { model: m, messages, temperature: temp ?? 0.7, max_tokens: maxTok ?? 4096 };
719
+ if (tools?.length) {
720
+ const defs = tools.map(t => this._toolRegistry.get(t)).filter(Boolean) as any[];
721
+ if (defs.length) body.tools = defs.map(t => ({ type: "function", function: { name: t.name, description: t.description, parameters: this.paramsToSchema(t.parameters || []) } }));
722
+ }
723
+ const resp = await fetch(baseUrl + "/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKey }, body: JSON.stringify(body) });
724
+ if (!resp.ok) { const e: any = new Error("API " + resp.status + ": " + ((await resp.text()).slice(0, 200))); e.status_code = resp.status; throw e; }
725
+ const data: any = await resp.json();
726
+ const msg = data.choices?.[0]?.message || {};
727
+ return { content: msg.content || "", toolCalls: (msg.tool_calls || []).map((tc: any) => ({ id: tc.id, type: "function", function: { name: tc.function?.name || "", arguments: tc.function?.arguments || "{}" } })), usage: { promptTokens: data.usage?.prompt_tokens || 0, completionTokens: data.usage?.completion_tokens || 0 } };
728
+ }
729
+
730
+ private async callAnthropic(
731
+ m: string, messages: Record<string, unknown>[], tools?: string[], temp?: number, maxTok?: number
732
+ ): Promise<{ content: string; toolCalls: ToolCall[]; usage: UsageStats }> {
733
+ const apiKey = this.getApiKey("anthropic");
734
+ const body: Record<string, unknown> = { model: m, max_tokens: maxTok ?? 4096, messages: messages.filter(msg => msg.role !== "system"), temperature: temp ?? 0.7 };
735
+ const sys = messages.find(msg => msg.role === "system"); if (sys) body.system = sys.content;
736
+ if (tools?.length) {
737
+ const defs = tools.map(t => this._toolRegistry.get(t)).filter(Boolean) as any[];
738
+ if (defs.length) body.tools = defs.map(t => ({ name: t.name, description: t.description, input_schema: this.paramsToSchema(t.parameters || []) }));
739
+ }
740
+ const resp = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" }, body: JSON.stringify(body) });
741
+ if (!resp.ok) { const e: any = new Error("API " + resp.status + ": " + ((await resp.text()).slice(0, 200))); e.status_code = resp.status; throw e; }
742
+ const data: any = await resp.json(); let content = ""; const toolCalls: ToolCall[] = [];
743
+ for (const b of data.content || []) { if (b.type === "text") content += b.text; if (b.type === "tool_use") toolCalls.push({ id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input) } }); }
744
+ return { content, toolCalls, usage: { promptTokens: data.usage?.input_tokens || 0, completionTokens: data.usage?.output_tokens || 0 } };
745
+ }
746
+
747
+ private paramsToSchema(params: any[]): Record<string, any> {
748
+ const props: Record<string, any> = {};
749
+ for (const p of params) props[p.name] = { type: p.type === "integer" ? "integer" : p.type === "number" ? "number" : p.type === "boolean" ? "boolean" : "string", description: p.description };
750
+ const required = params.filter(p => p.required).map(p => p.name);
751
+ return { type: "object", properties: props, ...(required.length > 0 ? { required } : {}) };
752
+ }
753
+
754
+ private getApiKey(model: string): string {
755
+ let provider = "openai"; const [pr] = splitProvider(model); if (pr) provider = pr;
756
+ else { const l = model.toLowerCase(); if (l.includes("claude")) provider = "anthropic"; else if (l.includes("deepseek")) provider = "deepseek"; else if (l.includes("groq")) provider = "groq"; else if (l.includes("openrouter")) provider = "openrouter"; else if (l.includes("gemini")) provider = "gemini"; }
757
+ const envMap = getProviderEnvMap();
758
+ const envVar = envMap.get(provider) || (provider.toUpperCase() + "_API_KEY");
759
+ const key = process.env[envVar];
760
+ if (!key) throw new Error("Missing " + envVar + ". Set environment variable or configure in ~/.skyloom/config.yaml");
761
+ return key;
762
+ }
763
+
764
+ private getBaseUrl(model: string): string {
765
+ let provider = "openai"; const [pr] = splitProvider(model); if (pr) provider = pr;
766
+ else { const l = model.toLowerCase(); if (l.includes("claude")) return "https://api.anthropic.com/v1"; else if (l.includes("deepseek")) return "https://api.deepseek.com/v1"; else if (l.includes("groq")) return "https://api.groq.com/openai/v1"; else if (l.includes("openrouter")) return "https://openrouter.ai/api/v1"; else if (l.includes("ollama")) return ((process.env.OLLAMA_HOST || "http://localhost:11434") + "/v1"); }
767
+ if (provider === "deepseek") return "https://api.deepseek.com/v1";
768
+ if (provider === "groq") return "https://api.groq.com/openai/v1";
769
+ if (provider === "openrouter") return "https://openrouter.ai/api/v1";
770
+ if (provider === "ollama") return ((process.env.OLLAMA_HOST || "http://localhost:11434") + "/v1");
771
+ return "https://api.openai.com/v1";
695
772
  }
696
773
 
697
- /**
698
- * Stream a completion (placeholder).
699
- */
700
774
  async *stream(
701
- _messages: Record<string, unknown>[],
702
- _agentName?: string
775
+ messages: Record<string, unknown>[], agentName?: string
703
776
  ): AsyncGenerator<string> {
704
- // Placeholder implementation
705
- yield "Streaming response...";
777
+ const response = await this.complete(messages, agentName);
778
+ yield response.content;
706
779
  }
707
780
 
708
- /**
709
- * Stream completion with tool awareness (placeholder).
710
- */
711
781
  async *streamWithTools(
712
- _messages: Record<string, unknown>[],
713
- _agentName?: string,
714
- _tools?: string[],
715
- _toolRegistry?: ToolRegistry,
716
- _overrides?: Record<string, unknown>
782
+ messages: Record<string, unknown>[], agentName?: string, tools?: string[],
783
+ _toolRegistry?: ToolRegistry, overrides?: Record<string, unknown>
717
784
  ): AsyncGenerator<StreamEvent> {
718
- // Placeholder implementation
719
- yield {
720
- type: "content",
721
- text: "Tool-aware streaming response...",
722
- };
785
+ const response = await this.complete(messages, agentName, tools, false, overrides);
786
+ if (response.content) yield { type: "content", text: response.content };
787
+ for (const tc of response.toolCalls || []) yield { type: "tool_call", toolCall: tc };
788
+ yield { type: "done", usage: response.usage, reasoningContent: response.reasoningContent };
723
789
  }
724
790
  }