skyloom 1.4.5 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.5",
3
+ "version": "1.5.1",
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
@@ -143,92 +143,73 @@ function render(text: string): string[] {
143
143
  /* ═══════════════════════════════════════
144
144
  Chat loop
145
145
  ═══════════════════════════════════════ */
146
+ /* Check for API key availability */
147
+ function checkApiKeys(): string | null {
148
+ const keys = ["DEEPSEEK_API_KEY","OPENAI_API_KEY","ANTHROPIC_API_KEY","GROQ_API_KEY","OPENROUTER_API_KEY"];
149
+ for (const k of keys) { if (process.env[k]) return k; }
150
+ return null;
151
+ }
152
+
146
153
  async function chat(agentName: string, modelOverride?: string): Promise<void> {
154
+ const haveKey = checkApiKeys();
155
+ if (!haveKey) {
156
+ process.stdout.write("\n" + chalk.yellow(" ⚠ No API key configured.\n"));
157
+ process.stdout.write(chalk.dim(" Set one: $env:DEEPSEEK_API_KEY = \"sk-your-key\" (PowerShell)\n"));
158
+ process.stdout.write(chalk.dim(" export DEEPSEEK_API_KEY=sk-your-key (Bash)\n\n"));
159
+ process.stdout.write(chalk.dim(" Then run: sky\n\n"));
160
+ process.exit(1);
161
+ }
162
+
147
163
  const ctx = createSystemContext();
148
164
  let agent = ctx.agentMap.get(agentName);
149
- if (!agent) { process.stdout.write(chalk.red(`Unknown agent: ${agentName}\n`)); return; }
165
+ if (!agent) { process.stdout.write(chalk.red("Unknown agent: " + agentName) + "\n"); return; }
150
166
  await agent.init();
167
+ // eslint-disable-next-line prefer-const
168
+ let currentAgent = agent; // mutable for agent switching
151
169
  welcome(agent);
152
170
 
153
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
154
- const history: string[] = [];
171
+ process.stdout.write(chalk.dim(" Key: " + haveKey + "\n\n"));
155
172
 
156
- for await (const line of rl) {
157
- const inp = line.trim();
158
- if (!inp) continue;
173
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
159
174
 
160
- if (inp[0] !== "/" && !history.includes(inp)) {
161
- history.push(inp);
162
- if (history.length > 50) history.shift();
163
- }
175
+ function ask() { rl.question(chalk.cyan(" " + currentAgent.displayName + " ❯ "), handler); }
176
+ async function handler(inp: string) {
177
+ inp = inp.trim();
178
+ if (!inp) { ask(); return; }
164
179
 
165
- const cmdLower = inp.toLowerCase();
166
- let handled = false;
180
+ const cmdL = inp.toLowerCase();
167
181
 
168
182
  // Agent switch
169
183
  for (const n of AGENT_NAMES) {
170
- if (cmdLower === `/${n}`) {
171
- const a = ctx.agentMap.get(n);
172
- if (a) { await a.init(); agent = a; process.stdout.write(chalk.dim(` ⟳ ${AGENT_DISPLAY[n]}\n`)); }
173
- handled = true; break;
174
- }
184
+ if (cmdL === "/" + n) { const a = ctx.agentMap.get(n); if (a) { await a.init(); currentAgent = a; } process.stdout.write(chalk.dim(" ⟳ " + AGENT_DISPLAY[n] + "\n")); ask(); return; }
175
185
  }
176
186
 
177
- if (handled) continue;
178
- if (cmdLower === "/quit" || cmdLower === "/exit") break;
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; }
187
+ if (cmdL === "/quit" || cmdL === "/exit") { process.stdout.write(chalk.dim("\n Session ended\n")); rl.close(); await ctx.closeAll(); process.exit(0); return; }
188
+ if (cmdL === "/help") { process.stdout.write(helpText()); ask(); return; }
189
+ if (cmdL === "/clear") { console.clear(); welcome(agent); process.stdout.write(chalk.dim(" Key: " + haveKey + "\n\n")); ask(); return; }
190
+ if (cmdL === "/status") { process.stdout.write(chalk.bold("\n " + currentAgent.displayName + " (" + currentAgent.name + ")\n") + chalk.dim(" State: " + currentAgent.state + " · Memory: " + currentAgent.memory.shortTerm.length + " msgs\n\n")); ask(); return; }
191
+ if (cmdL === "/cost") { process.stdout.write(chalk.bold("\n Total: " + formatCost(ctx.llm.getTotalCost()) + "\n\n")); ask(); return; }
192
+ if (cmdL === "/compact") { const r = await currentAgent.compact(); process.stdout.write(chalk.green(" " + r + "\n\n")); ask(); return; }
193
+ if (cmdL === "/version") { process.stdout.write(" Skyloom v" + VERSION + "\n"); ask(); return; }
194
+ if (cmdL.startsWith("/task ")) { const g = inp.slice(6); process.stdout.write(chalk.cyan("\n ✦ " + g + "\n\n")); await runTask(g); ask(); return; }
195
+ if (inp.startsWith("/")) { process.stdout.write(helpText()); ask(); return; }
199
196
 
200
197
  // ── Chat ──
201
- process.stdout.write(chalk.dim(` ${agent.displayName} thinking...\r`));
202
-
198
+ process.stdout.write(chalk.dim(" " + currentAgent.displayName + " thinking...\r"));
203
199
  try {
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");
200
+ const response = await currentAgent.chat(inp);
201
+ process.stdout.write("\r" + " ".repeat(40) + "\r\n");
202
+ const lines = render(response);
203
+ for (const l of lines) process.stdout.write(l + "\n");
207
204
  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
- }
213
-
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 */ }
225
- }
205
+ } catch (e: any) {
206
+ process.stdout.write("\r" + " ".repeat(40) + "\r");
207
+ process.stdout.write(chalk.red(" ✗ " + (e.message || e) + "\n\n"));
226
208
  }
209
+ ask();
227
210
  }
228
211
 
229
- process.stdout.write(chalk.dim("\n Session ended\n"));
230
- await ctx.closeAll();
231
- process.exit(0);
212
+ ask();
232
213
  }
233
214
 
234
215
  /* ═══════════════════════════════════════
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
  }