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.
- package/config/default.yaml +47 -0
- package/config/providers.yaml +39 -0
- package/package.json +1 -1
- package/src/cli/main.ts +173 -495
- package/src/core/llm.ts +111 -45
|
@@ -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
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.
|
|
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
|
-
|
|
24
|
+
Slash commands registry
|
|
31
25
|
═══════════════════════════════════════ */
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
62
|
+
process.stdout.write(chalk.dim(` └${"─".repeat(60)}┘\n`));
|
|
154
63
|
}
|
|
155
64
|
|
|
156
|
-
/*
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
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(
|
|
194
|
-
if (
|
|
195
|
-
if (
|
|
196
|
-
if (
|
|
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
|
-
/*
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
|
405
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (!inp) { logLine(""); continue; }
|
|
156
|
+
for await (const line of rl) {
|
|
157
|
+
const inp = line.trim();
|
|
158
|
+
if (!inp) continue;
|
|
413
159
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
|
422
|
-
const cmdLower = cmd.toLowerCase();
|
|
423
|
-
|
|
424
|
-
/* ── Slash commands ── */
|
|
165
|
+
const cmdLower = inp.toLowerCase();
|
|
425
166
|
let handled = false;
|
|
426
167
|
|
|
427
|
-
// Agent
|
|
168
|
+
// Agent switch
|
|
428
169
|
for (const n of AGENT_NAMES) {
|
|
429
170
|
if (cmdLower === `/${n}`) {
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
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") {
|
|
447
|
-
if (cmdLower === "/clear") { console.clear(); handled = true; }
|
|
448
|
-
if (cmdLower === "/version") {
|
|
449
|
-
if (cmdLower === "/status") {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
if (cmdLower === "/
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (cmdLower
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
514
|
-
|
|
515
|
-
for (const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
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
|
|
235
|
+
Task
|
|
545
236
|
═══════════════════════════════════════ */
|
|
546
|
-
async function runTask(goal: string
|
|
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
|
|
550
|
-
|
|
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
|
|
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", "
|
|
571
|
-
["Info", [["/status", "
|
|
572
|
-
["Orch.", [["/task <goal>", "Multi-agent
|
|
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
|
-
|
|
576
|
-
for (const [c, d] of cmds)
|
|
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
|
-
|
|
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
|
-
|
|
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 => {
|
|
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
|
|
665
|
+
* Complete with retry logic — real HTTP call to LLM API.
|
|
666
666
|
*/
|
|
667
667
|
private async completeWithRetry(
|
|
668
668
|
model: string,
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
702
|
-
_agentName?: string
|
|
775
|
+
messages: Record<string, unknown>[], agentName?: string
|
|
703
776
|
): AsyncGenerator<string> {
|
|
704
|
-
|
|
705
|
-
yield
|
|
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
|
-
|
|
713
|
-
|
|
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
|
-
|
|
719
|
-
yield {
|
|
720
|
-
|
|
721
|
-
|
|
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
|
}
|