openfox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +68 -0
  2. package/dist/branch.api-WDHKZJS6.js +48 -0
  3. package/dist/branch.api-WDHKZJS6.js.map +1 -0
  4. package/dist/chunk-26NRL3HX.js +116 -0
  5. package/dist/chunk-26NRL3HX.js.map +1 -0
  6. package/dist/chunk-43SWNGYW.js +273 -0
  7. package/dist/chunk-43SWNGYW.js.map +1 -0
  8. package/dist/chunk-4Z2OZ6NX.js +132 -0
  9. package/dist/chunk-4Z2OZ6NX.js.map +1 -0
  10. package/dist/chunk-6MOGZMGX.js +218 -0
  11. package/dist/chunk-6MOGZMGX.js.map +1 -0
  12. package/dist/chunk-AF7XS4OI.js +55 -0
  13. package/dist/chunk-AF7XS4OI.js.map +1 -0
  14. package/dist/chunk-EI46G5WO.js +138 -0
  15. package/dist/chunk-EI46G5WO.js.map +1 -0
  16. package/dist/chunk-FF6Z2MC3.js +844 -0
  17. package/dist/chunk-FF6Z2MC3.js.map +1 -0
  18. package/dist/chunk-IKAWIOXM.js +5817 -0
  19. package/dist/chunk-IKAWIOXM.js.map +1 -0
  20. package/dist/chunk-IVHCPQO2.js +29 -0
  21. package/dist/chunk-IVHCPQO2.js.map +1 -0
  22. package/dist/chunk-MTX5Y3V3.js +46 -0
  23. package/dist/chunk-MTX5Y3V3.js.map +1 -0
  24. package/dist/chunk-N2XYX4ZA.js +83 -0
  25. package/dist/chunk-N2XYX4ZA.js.map +1 -0
  26. package/dist/chunk-QY7BMXWT.js +124 -0
  27. package/dist/chunk-QY7BMXWT.js.map +1 -0
  28. package/dist/chunk-U62ZECSQ.js +54 -0
  29. package/dist/chunk-U62ZECSQ.js.map +1 -0
  30. package/dist/chunk-YQ36AK5A.js +5570 -0
  31. package/dist/chunk-YQ36AK5A.js.map +1 -0
  32. package/dist/cli/dev.d.ts +1 -0
  33. package/dist/cli/dev.js +14 -0
  34. package/dist/cli/dev.js.map +1 -0
  35. package/dist/cli/index.d.ts +1 -0
  36. package/dist/cli/index.js +14 -0
  37. package/dist/cli/index.js.map +1 -0
  38. package/dist/command-defaults/defaults/commit-push.command.md +13 -0
  39. package/dist/command-defaults/defaults/init.command.md +18 -0
  40. package/dist/command-defaults/defaults/test-ui.command.md +16 -0
  41. package/dist/config-6GJ4SVFH.js +33 -0
  42. package/dist/config-6GJ4SVFH.js.map +1 -0
  43. package/dist/init-UVZDOHMX.js +185 -0
  44. package/dist/init-UVZDOHMX.js.map +1 -0
  45. package/dist/paths-4BPXWB2W.js +17 -0
  46. package/dist/paths-4BPXWB2W.js.map +1 -0
  47. package/dist/project-creator-CWPKRIKI.js +69 -0
  48. package/dist/project-creator-CWPKRIKI.js.map +1 -0
  49. package/dist/protocol-BJsm88OW.d.ts +760 -0
  50. package/dist/provider-LCE6WZA6.js +343 -0
  51. package/dist/provider-LCE6WZA6.js.map +1 -0
  52. package/dist/serve-27DZK34K.js +177 -0
  53. package/dist/serve-27DZK34K.js.map +1 -0
  54. package/dist/server/index.d.ts +479 -0
  55. package/dist/server/index.js +20 -0
  56. package/dist/server/index.js.map +1 -0
  57. package/dist/settings-D5O3TMLD.js +17 -0
  58. package/dist/settings-D5O3TMLD.js.map +1 -0
  59. package/dist/shared/index.d.ts +19 -0
  60. package/dist/shared/index.js +17 -0
  61. package/dist/shared/index.js.map +1 -0
  62. package/dist/skill-defaults/defaults/playwright-cli.skill.md +38 -0
  63. package/dist/tools-R4YQAYUF.js +46 -0
  64. package/dist/tools-R4YQAYUF.js.map +1 -0
  65. package/dist/web/assets/html2canvas.esm-QH1iLAAe.js +22 -0
  66. package/dist/web/assets/index-BMNDCsbJ.js +120 -0
  67. package/dist/web/assets/index-BSlSWZrM.css +1 -0
  68. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  69. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  70. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  71. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  72. package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
  73. package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
  74. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  75. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  76. package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  77. package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  78. package/dist/web/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
  79. package/dist/web/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
  80. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  81. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  82. package/dist/web/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  83. package/dist/web/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  84. package/dist/web/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
  85. package/dist/web/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
  86. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  87. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  88. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  89. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  90. package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
  91. package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
  92. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  93. package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  94. package/dist/web/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
  95. package/dist/web/assets/openfox.png +0 -0
  96. package/dist/web/fox.svg +25 -0
  97. package/dist/web/index.html +14 -0
  98. package/dist/web/sounds/achievement.mp3 +0 -0
  99. package/dist/web/sounds/intervention.mp3 +0 -0
  100. package/dist/web/sounds/notification.mp3 +0 -0
  101. package/dist/web/sounds/waiting-for-user.mp3 +0 -0
  102. package/package.json +101 -0
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # OpenFox
2
+
3
+ **Local-LLM-first agentic coding assistant**
4
+
5
+ Autonomous coding agent for local LLMs with contract-driven execution.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx openfox
11
+ ```
12
+
13
+ On first run, OpenFox automatically detects your local LLM backend (vLLM, sglang, ollama, llamacpp) and configures itself.
14
+
15
+ ## CLI Commands
16
+
17
+ ```bash
18
+ # Start server for current project
19
+ npx openfox
20
+
21
+ # Start on custom port
22
+ npx openfox --port 8080
23
+
24
+ # Start without opening browser
25
+ npx openfox --no-browser
26
+
27
+ # Interactive configuration setup
28
+ npx openfox init
29
+
30
+ # Show current configuration
31
+ npx openfox config
32
+
33
+ # Manage LLM providers
34
+ npx openfox provider add # Add new provider
35
+ npx openfox provider list # List configured providers
36
+ npx openfox provider use # Switch active provider
37
+ npx openfox provider remove # Remove provider
38
+ ```
39
+
40
+ ## CLI Options
41
+
42
+ | Option | Description | Default |
43
+ |--------|-------------|---------|
44
+ | `-p, --port <number>` | Specify server port | 10369 |
45
+ | `--no-browser` | Don't open browser on start | Opens browser |
46
+ | `-h, --help` | Show help message | - |
47
+ | `-v, --version` | Show version number | - |
48
+
49
+ ## Requirements
50
+
51
+ - Node.js >= 24.0.0
52
+ - Local LLM backend with OpenAI-compatible API:
53
+ - vLLM
54
+ - sglang
55
+ - ollama
56
+ - llamacpp
57
+
58
+ ## Features
59
+
60
+ - **Plan → Builder Workflow**: Interactive task breakdown followed by autonomous implementation
61
+ - **Contract-Driven Execution**: Acceptance criteria serve as immutable contract
62
+ - **Iterative Verification**: Agent loops until all criteria pass
63
+ - **LSP Integration**: Immediate feedback on code validity
64
+ - **Real-Time Metrics**: Prefill time, generation speed, context usage
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,48 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-U62ZECSQ.js";
4
+
5
+ // src/server/branch.api.ts
6
+ import { spawn } from "child_process";
7
+ import { resolve } from "path";
8
+ async function getCurrentBranch(req, res) {
9
+ try {
10
+ const workdir = req.query["workdir"] || process.cwd();
11
+ const resolvedWorkdir = resolve(workdir);
12
+ const branch = await getGitBranch(resolvedWorkdir);
13
+ if (branch) {
14
+ res.json({ branch, workdir: resolvedWorkdir });
15
+ } else {
16
+ res.json({ branch: null, workdir: resolvedWorkdir, error: "Not a git repository" });
17
+ }
18
+ } catch (error) {
19
+ logger.error("Error getting current branch", { error: error instanceof Error ? error.message : String(error) });
20
+ res.status(500).json({ error: "Failed to get branch", branch: null });
21
+ }
22
+ }
23
+ function getGitBranch(cwd) {
24
+ return new Promise((resolve2) => {
25
+ const proc = spawn("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
26
+ cwd,
27
+ stdio: ["ignore", "pipe", "ignore"]
28
+ });
29
+ let stdout = "";
30
+ proc.stdout.on("data", (data) => {
31
+ stdout += data.toString();
32
+ });
33
+ proc.on("close", (code) => {
34
+ if (code === 0 && stdout.trim()) {
35
+ resolve2(stdout.trim());
36
+ } else {
37
+ resolve2(null);
38
+ }
39
+ });
40
+ proc.on("error", () => {
41
+ resolve2(null);
42
+ });
43
+ });
44
+ }
45
+ export {
46
+ getCurrentBranch
47
+ };
48
+ //# sourceMappingURL=branch.api-WDHKZJS6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/branch.api.ts"],"sourcesContent":["import type { Request, Response } from 'express'\nimport { spawn } from 'node:child_process'\nimport { resolve } from 'node:path'\nimport { logger } from './utils/logger.js'\n\n/**\n * GET /api/branch\n * Get the current git branch name for a workdir\n */\nexport async function getCurrentBranch(req: Request, res: Response): Promise<void> {\n try {\n const workdir = (req.query['workdir'] as string) || process.cwd()\n const resolvedWorkdir = resolve(workdir)\n\n const branch = await getGitBranch(resolvedWorkdir)\n\n if (branch) {\n res.json({ branch, workdir: resolvedWorkdir })\n } else {\n res.json({ branch: null, workdir: resolvedWorkdir, error: 'Not a git repository' })\n }\n } catch (error) {\n logger.error('Error getting current branch', { error: error instanceof Error ? error.message : String(error) })\n res.status(500).json({ error: 'Failed to get branch', branch: null })\n }\n}\n\n/**\n * Execute git rev-parse --abbrev-ref HEAD to get the current branch name\n */\nfunction getGitBranch(cwd: string): Promise<string | null> {\n return new Promise((resolve) => {\n const proc = spawn('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {\n cwd,\n stdio: ['ignore', 'pipe', 'ignore'],\n })\n\n let stdout = ''\n\n proc.stdout.on('data', (data: Buffer) => {\n stdout += data.toString()\n })\n\n proc.on('close', (code) => {\n if (code === 0 && stdout.trim()) {\n resolve(stdout.trim())\n } else {\n resolve(null)\n }\n })\n\n proc.on('error', () => {\n resolve(null)\n })\n })\n}\n"],"mappings":";;;;;AACA,SAAS,aAAa;AACtB,SAAS,eAAe;AAOxB,eAAsB,iBAAiB,KAAc,KAA8B;AACjF,MAAI;AACF,UAAM,UAAW,IAAI,MAAM,SAAS,KAAgB,QAAQ,IAAI;AAChE,UAAM,kBAAkB,QAAQ,OAAO;AAEvC,UAAM,SAAS,MAAM,aAAa,eAAe;AAEjD,QAAI,QAAQ;AACV,UAAI,KAAK,EAAE,QAAQ,SAAS,gBAAgB,CAAC;AAAA,IAC/C,OAAO;AACL,UAAI,KAAK,EAAE,QAAQ,MAAM,SAAS,iBAAiB,OAAO,uBAAuB,CAAC;AAAA,IACpF;AAAA,EACF,SAAS,OAAO;AACd,WAAO,MAAM,gCAAgC,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,CAAC;AAC9G,QAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,wBAAwB,QAAQ,KAAK,CAAC;AAAA,EACtE;AACF;AAKA,SAAS,aAAa,KAAqC;AACzD,SAAO,IAAI,QAAQ,CAACA,aAAY;AAC9B,UAAM,OAAO,MAAM,OAAO,CAAC,aAAa,gBAAgB,MAAM,GAAG;AAAA,MAC/D;AAAA,MACA,OAAO,CAAC,UAAU,QAAQ,QAAQ;AAAA,IACpC,CAAC;AAED,QAAI,SAAS;AAEb,SAAK,OAAO,GAAG,QAAQ,CAAC,SAAiB;AACvC,gBAAU,KAAK,SAAS;AAAA,IAC1B,CAAC;AAED,SAAK,GAAG,SAAS,CAAC,SAAS;AACzB,UAAI,SAAS,KAAK,OAAO,KAAK,GAAG;AAC/B,QAAAA,SAAQ,OAAO,KAAK,CAAC;AAAA,MACvB,OAAO;AACL,QAAAA,SAAQ,IAAI;AAAA,MACd;AAAA,IACF,CAAC;AAED,SAAK,GAAG,SAAS,MAAM;AACrB,MAAAA,SAAQ,IAAI;AAAA,IACd,CAAC;AAAA,EACH,CAAC;AACH;","names":["resolve"]}
@@ -0,0 +1,116 @@
1
+ // src/server/llm/streaming.ts
2
+ var XML_TOOL_PATTERNS = ["<tool_call>", "<function=", "</tool_call>", "<parameter="];
3
+ function hasXmlToolPattern(text) {
4
+ return XML_TOOL_PATTERNS.some((p) => text.includes(p));
5
+ }
6
+ async function* streamWithSegments(client, request) {
7
+ const xmlAbortController = new AbortController();
8
+ const combinedSignal = request.signal ? AbortSignal.any([request.signal, xmlAbortController.signal]) : xmlAbortController.signal;
9
+ let content = "";
10
+ let thinkingContent = "";
11
+ let response = null;
12
+ const segments = [];
13
+ let currentTextSegment = "";
14
+ let currentThinkingSegment = "";
15
+ const startTime = performance.now();
16
+ let firstTokenTime = null;
17
+ const flushText = () => {
18
+ if (currentTextSegment.trim()) {
19
+ segments.push({ type: "text", content: currentTextSegment });
20
+ }
21
+ currentTextSegment = "";
22
+ };
23
+ const flushThinking = () => {
24
+ if (currentThinkingSegment.trim()) {
25
+ segments.push({ type: "thinking", content: currentThinkingSegment });
26
+ }
27
+ currentThinkingSegment = "";
28
+ };
29
+ try {
30
+ for await (const event of client.stream({ ...request, signal: combinedSignal })) {
31
+ switch (event.type) {
32
+ case "text_delta":
33
+ if (firstTokenTime === null) {
34
+ firstTokenTime = performance.now();
35
+ }
36
+ flushThinking();
37
+ content += event.content;
38
+ currentTextSegment += event.content;
39
+ if (hasXmlToolPattern(content)) {
40
+ xmlAbortController.abort();
41
+ yield { type: "xml_tool_abort" };
42
+ return null;
43
+ }
44
+ yield { type: "text_delta", content: event.content };
45
+ break;
46
+ case "thinking_delta":
47
+ if (firstTokenTime === null) {
48
+ firstTokenTime = performance.now();
49
+ }
50
+ flushText();
51
+ thinkingContent += event.content;
52
+ currentThinkingSegment += event.content;
53
+ if (hasXmlToolPattern(thinkingContent)) {
54
+ xmlAbortController.abort();
55
+ yield { type: "xml_tool_abort" };
56
+ return null;
57
+ }
58
+ yield { type: "thinking_delta", content: event.content };
59
+ break;
60
+ case "tool_call_delta":
61
+ yield {
62
+ type: "tool_call_delta",
63
+ index: event.index,
64
+ ...event.id !== void 0 ? { id: event.id } : {},
65
+ ...event.name !== void 0 ? { name: event.name } : {},
66
+ ...event.arguments !== void 0 ? { arguments: event.arguments } : {}
67
+ };
68
+ break;
69
+ case "done":
70
+ flushThinking();
71
+ flushText();
72
+ response = event.response;
73
+ yield { type: "done", response: event.response };
74
+ break;
75
+ case "error":
76
+ yield { type: "error", error: event.error };
77
+ return null;
78
+ }
79
+ }
80
+ } catch (error) {
81
+ if (error instanceof Error && error.name === "AbortError") {
82
+ return null;
83
+ }
84
+ yield { type: "error", error: error instanceof Error ? error.message : "Unknown error" };
85
+ return null;
86
+ }
87
+ if (!response) {
88
+ return null;
89
+ }
90
+ const toolCalls = response.toolCalls ?? [];
91
+ for (const tc of toolCalls) {
92
+ segments.push({ type: "tool_call", toolCallId: tc.id });
93
+ }
94
+ const endTime = performance.now();
95
+ const ttft = ((firstTokenTime ?? endTime) - startTime) / 1e3;
96
+ const completionTime = (endTime - (firstTokenTime ?? startTime)) / 1e3;
97
+ const { promptTokens, completionTokens } = response.usage;
98
+ return {
99
+ content,
100
+ thinkingContent,
101
+ toolCalls,
102
+ response,
103
+ segments,
104
+ timing: {
105
+ ttft,
106
+ completionTime,
107
+ tps: completionTime > 0 ? completionTokens / completionTime : 0,
108
+ prefillTps: ttft > 0 ? promptTokens / ttft : 0
109
+ }
110
+ };
111
+ }
112
+
113
+ export {
114
+ streamWithSegments
115
+ };
116
+ //# sourceMappingURL=chunk-26NRL3HX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/llm/streaming.ts"],"sourcesContent":["import type { MessageSegment, ToolCall } from '../../shared/types.js'\nimport type { LLMClient, LLMCompletionRequest, LLMStreamEvent, LLMCompletionResponse } from './types.js'\n\n// XML patterns that indicate wrong tool format (model outputting XML instead of JSON function calls)\nconst XML_TOOL_PATTERNS = ['<tool_call>', '<function=', '</tool_call>', '<parameter=']\n\nfunction hasXmlToolPattern(text: string): boolean {\n return XML_TOOL_PATTERNS.some(p => text.includes(p))\n}\n\n/**\n * Streaming event with segment tracking.\n * Same as LLMStreamEvent but adds segment context.\n */\nexport type StreamEvent =\n | { type: 'text_delta'; content: string }\n | { type: 'thinking_delta'; content: string }\n | { type: 'tool_call_delta'; index: number; id?: string; name?: string; arguments?: string }\n | { type: 'done'; response: LLMCompletionResponse }\n | { type: 'error'; error: string }\n | { type: 'xml_tool_abort' }\n\n/**\n * Timing metrics for a streaming LLM call.\n */\nexport interface StreamTiming {\n ttft: number // time to first token (seconds)\n completionTime: number // generation time (seconds)\n tps: number // tokens per second (generation)\n prefillTps: number // prompt tokens / ttft\n}\n\n/**\n * Result from a single streaming LLM call.\n * Contains accumulated content and segments for that call.\n */\nexport interface StreamResult {\n content: string\n thinkingContent: string\n toolCalls: ToolCall[]\n response: LLMCompletionResponse\n segments: MessageSegment[]\n timing: StreamTiming\n}\n\n/**\n * Streams an LLM completion and accumulates content with segment tracking.\n * Yields events for real-time streaming, returns accumulated result.\n * \n * Merges consecutive text/thinking deltas into single segments.\n */\nexport async function* streamWithSegments(\n client: LLMClient,\n request: LLMCompletionRequest\n): AsyncGenerator<StreamEvent, StreamResult | null> {\n // Create internal abort controller for XML detection\n const xmlAbortController = new AbortController()\n \n // Combine with any external signal (e.g., user abort)\n const combinedSignal = request.signal\n ? AbortSignal.any([request.signal, xmlAbortController.signal])\n : xmlAbortController.signal\n \n let content = ''\n let thinkingContent = ''\n let response: LLMCompletionResponse | null = null\n \n const segments: MessageSegment[] = []\n let currentTextSegment = ''\n let currentThinkingSegment = ''\n \n // Timing tracking\n const startTime = performance.now()\n let firstTokenTime: number | null = null\n \n // Flush accumulated text to segments (skip whitespace-only)\n const flushText = () => {\n if (currentTextSegment.trim()) {\n segments.push({ type: 'text', content: currentTextSegment })\n }\n currentTextSegment = ''\n }\n \n // Flush accumulated thinking to segments (skip whitespace-only)\n const flushThinking = () => {\n if (currentThinkingSegment.trim()) {\n segments.push({ type: 'thinking', content: currentThinkingSegment })\n }\n currentThinkingSegment = ''\n }\n \n try {\n for await (const event of client.stream({ ...request, signal: combinedSignal })) {\n switch (event.type) {\n case 'text_delta':\n // Track first token time\n if (firstTokenTime === null) {\n firstTokenTime = performance.now()\n }\n // If we were accumulating thinking, flush it first\n flushThinking()\n content += event.content\n currentTextSegment += event.content\n \n // Check for XML tool syntax - abort immediately to save tokens\n if (hasXmlToolPattern(content)) {\n xmlAbortController.abort()\n yield { type: 'xml_tool_abort' }\n return null\n }\n \n yield { type: 'text_delta', content: event.content }\n break\n \n case 'thinking_delta':\n // Track first token time\n if (firstTokenTime === null) {\n firstTokenTime = performance.now()\n }\n // If we were accumulating text, flush it first\n flushText()\n thinkingContent += event.content\n currentThinkingSegment += event.content\n \n // Check for XML tool syntax in thinking too\n if (hasXmlToolPattern(thinkingContent)) {\n xmlAbortController.abort()\n yield { type: 'xml_tool_abort' }\n return null\n }\n \n yield { type: 'thinking_delta', content: event.content }\n break\n \n case 'tool_call_delta':\n // Forward tool call delta events for early UI feedback\n yield {\n type: 'tool_call_delta',\n index: event.index,\n ...(event.id !== undefined ? { id: event.id } : {}),\n ...(event.name !== undefined ? { name: event.name } : {}),\n ...(event.arguments !== undefined ? { arguments: event.arguments } : {}),\n }\n break\n \n case 'done':\n // Flush any remaining content\n flushThinking()\n flushText()\n response = event.response\n yield { type: 'done', response: event.response }\n break\n \n case 'error':\n yield { type: 'error', error: event.error }\n return null\n }\n }\n } catch (error) {\n // AbortError is expected when we abort for XML detection\n if (error instanceof Error && error.name === 'AbortError') {\n return null\n }\n yield { type: 'error', error: error instanceof Error ? error.message : 'Unknown error' }\n return null\n }\n \n if (!response) {\n return null\n }\n \n // Add tool call segments (references to the toolCalls array)\n const toolCalls = response.toolCalls ?? []\n for (const tc of toolCalls) {\n segments.push({ type: 'tool_call', toolCallId: tc.id })\n }\n \n // Calculate timing\n const endTime = performance.now()\n const ttft = ((firstTokenTime ?? endTime) - startTime) / 1000\n const completionTime = (endTime - (firstTokenTime ?? startTime)) / 1000\n const { promptTokens, completionTokens } = response.usage\n \n return {\n content,\n thinkingContent,\n toolCalls,\n response,\n segments,\n timing: {\n ttft,\n completionTime,\n tps: completionTime > 0 ? completionTokens / completionTime : 0,\n prefillTps: ttft > 0 ? promptTokens / ttft : 0,\n },\n }\n}\n\n/**\n * Builder for accumulating segments across multiple LLM calls.\n * Use this when you have a loop with multiple LLM calls + tool executions.\n */\nexport class SegmentBuilder {\n private segments: MessageSegment[] = []\n \n /**\n * Add segments from a streaming result.\n */\n addFromResult(result: StreamResult): void {\n this.segments.push(...result.segments)\n }\n \n /**\n * Manually add a tool call segment (for tool calls executed between LLM calls).\n */\n addToolCall(toolCallId: string): void {\n this.segments.push({ type: 'tool_call', toolCallId })\n }\n \n /**\n * Get all accumulated segments.\n */\n build(): MessageSegment[] {\n return [...this.segments]\n }\n \n /**\n * Clear all segments (for reuse).\n */\n clear(): void {\n this.segments = []\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,273 @@
1
+ import {
2
+ detectBackend,
3
+ detectModel
4
+ } from "./chunk-FF6Z2MC3.js";
5
+ import {
6
+ getGlobalConfigPath
7
+ } from "./chunk-AF7XS4OI.js";
8
+
9
+ // src/cli/config.ts
10
+ import { z } from "zod";
11
+ import { randomUUID } from "crypto";
12
+ import { readFile, writeFile, mkdir, access } from "fs/promises";
13
+ import { dirname } from "path";
14
+ var SMART_DEFAULTS = [
15
+ "http://localhost:8000",
16
+ "http://localhost:11434",
17
+ "http://localhost:8080"
18
+ ];
19
+ async function trySmartDefaults(mode) {
20
+ const results = await Promise.all(
21
+ SMART_DEFAULTS.map(async (url) => {
22
+ try {
23
+ const [backend, model] = await Promise.all([
24
+ detectBackend(url, void 0, true),
25
+ detectModel(url, 1, true)
26
+ // Only 1 retry attempt
27
+ ]);
28
+ if (backend !== "unknown" && model) {
29
+ return { url, backend, model };
30
+ }
31
+ } catch {
32
+ }
33
+ return null;
34
+ })
35
+ );
36
+ return results.find((r) => r !== null) || null;
37
+ }
38
+ async function configFileExists(mode) {
39
+ const configPath = getGlobalConfigPath(mode);
40
+ try {
41
+ await access(configPath);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ var backendSchema = z.enum(["auto", "vllm", "sglang", "ollama", "llamacpp", "openai", "anthropic", "unknown"]);
48
+ var providerSchema = z.object({
49
+ id: z.string(),
50
+ name: z.string(),
51
+ url: z.string(),
52
+ backend: backendSchema,
53
+ apiKey: z.string().optional(),
54
+ maxContext: z.number().optional(),
55
+ isActive: z.boolean(),
56
+ createdAt: z.string(),
57
+ // Deprecated: model field kept for migration, will be removed after migration
58
+ model: z.string().optional()
59
+ });
60
+ var serverSchema = z.object({
61
+ port: z.number().default(10369),
62
+ host: z.string().default("127.0.0.1"),
63
+ openBrowser: z.boolean().default(true)
64
+ });
65
+ var loggingSchema = z.object({
66
+ level: z.enum(["debug", "info", "warn", "error"]).default("info")
67
+ });
68
+ var databaseSchema = z.object({
69
+ path: z.string().default("")
70
+ });
71
+ var workspaceSchema = z.object({
72
+ workdir: z.string().default(process.cwd())
73
+ });
74
+ var configSchema = z.object({
75
+ providers: z.array(providerSchema).default([]),
76
+ defaultModelSelection: z.string().optional(),
77
+ activeProviderId: z.string().optional(),
78
+ activeWorkflowId: z.string().optional(),
79
+ server: serverSchema.default({}),
80
+ logging: loggingSchema.default({}),
81
+ database: databaseSchema.default({}),
82
+ workspace: workspaceSchema.default({})
83
+ });
84
+ var oldLlmSchema = z.object({
85
+ url: z.string().url().default("http://localhost:8000/v1"),
86
+ model: z.string().default("auto"),
87
+ backend: z.enum(["auto", "vllm", "sglang", "ollama", "llamacpp"]).default("auto"),
88
+ maxContext: z.number().default(2e5),
89
+ disableThinking: z.boolean().default(false),
90
+ apiKey: z.string().optional()
91
+ });
92
+ var oldConfigSchema = z.object({
93
+ llm: oldLlmSchema,
94
+ server: serverSchema.default({}),
95
+ logging: loggingSchema.default({}),
96
+ database: databaseSchema.default({})
97
+ });
98
+ function migrateConfig(raw) {
99
+ if (typeof raw === "object" && raw !== null && "providers" in raw) {
100
+ const obj = raw;
101
+ if (obj.defaultModelSelection) {
102
+ const providers2 = obj.providers.map((p) => {
103
+ const { model, ...rest } = p;
104
+ return rest;
105
+ });
106
+ return configSchema.parse({
107
+ ...obj,
108
+ providers: providers2
109
+ });
110
+ }
111
+ const providers = obj.providers.map((p) => {
112
+ const { model, ...rest } = p;
113
+ return rest;
114
+ });
115
+ let defaultModelSelection;
116
+ if (obj.activeProviderId) {
117
+ const activeProvider = obj.providers.find((p) => p.id === obj.activeProviderId);
118
+ if (activeProvider?.model) {
119
+ defaultModelSelection = `${obj.activeProviderId}/${activeProvider.model}`;
120
+ } else {
121
+ defaultModelSelection = `${obj.activeProviderId}/auto`;
122
+ }
123
+ }
124
+ return configSchema.parse({
125
+ ...obj,
126
+ providers,
127
+ defaultModelSelection
128
+ });
129
+ }
130
+ if (typeof raw === "object" && raw !== null && "llm" in raw) {
131
+ const oldConfig = oldConfigSchema.parse(raw);
132
+ const providerId = randomUUID();
133
+ const provider = {
134
+ id: providerId,
135
+ name: "Default",
136
+ url: oldConfig.llm.url,
137
+ backend: oldConfig.llm.backend,
138
+ apiKey: oldConfig.llm.apiKey,
139
+ maxContext: oldConfig.llm.maxContext,
140
+ isActive: true,
141
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
142
+ };
143
+ const model = oldConfig.llm.model || "auto";
144
+ return {
145
+ providers: [provider],
146
+ defaultModelSelection: `${providerId}/${model}`,
147
+ server: oldConfig.server,
148
+ logging: oldConfig.logging,
149
+ database: oldConfig.database,
150
+ workspace: { workdir: process.cwd() }
151
+ };
152
+ }
153
+ return configSchema.parse(raw);
154
+ }
155
+ async function loadGlobalConfig(mode) {
156
+ const configPath = getGlobalConfigPath(mode);
157
+ try {
158
+ const content = await readFile(configPath, "utf-8");
159
+ const parsed = JSON.parse(content);
160
+ return migrateConfig(parsed);
161
+ } catch {
162
+ return configSchema.parse({});
163
+ }
164
+ }
165
+ async function saveGlobalConfig(mode, config) {
166
+ const configPath = getGlobalConfigPath(mode);
167
+ await mkdir(dirname(configPath), { recursive: true });
168
+ await writeFile(configPath, JSON.stringify(config, null, 2));
169
+ }
170
+ function getActiveProvider(config) {
171
+ if (config.defaultModelSelection) {
172
+ const [providerId] = config.defaultModelSelection.split("/");
173
+ return config.providers?.find((p) => p.id === providerId);
174
+ }
175
+ if (!config.activeProviderId) return void 0;
176
+ return config.providers?.find((p) => p.id === config.activeProviderId);
177
+ }
178
+ function getDefaultModel(config) {
179
+ if (!config.defaultModelSelection) return void 0;
180
+ const parts = config.defaultModelSelection.split("/");
181
+ return parts[1] ?? "auto";
182
+ }
183
+ function setDefaultModelSelection(config, providerId, model) {
184
+ const defaultModelSelection = `${providerId}/${model}`;
185
+ return {
186
+ ...config,
187
+ defaultModelSelection,
188
+ activeProviderId: providerId,
189
+ // Keep for backwards compatibility
190
+ providers: config.providers?.map((p) => ({ ...p, isActive: p.id === providerId })) ?? []
191
+ };
192
+ }
193
+ function addProvider(config, provider) {
194
+ const newProvider = {
195
+ ...provider,
196
+ id: randomUUID(),
197
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
198
+ };
199
+ const shouldActivate = provider.isActive || config.providers?.length === 0;
200
+ return {
201
+ ...config,
202
+ providers: [
203
+ ...(config.providers ?? []).map((p) => shouldActivate ? { ...p, isActive: false } : p),
204
+ { ...newProvider, isActive: shouldActivate }
205
+ ],
206
+ defaultModelSelection: shouldActivate ? `${newProvider.id}/auto` : config.defaultModelSelection,
207
+ activeProviderId: shouldActivate ? newProvider.id : config.activeProviderId,
208
+ workspace: config.workspace ?? { workdir: process.cwd() }
209
+ };
210
+ }
211
+ function removeProvider(config, providerId) {
212
+ const currentProviders = config.providers ?? [];
213
+ const filtered = currentProviders.filter((p) => p.id !== providerId);
214
+ let newDefaultModelSelection = config.defaultModelSelection;
215
+ if (config.defaultModelSelection) {
216
+ const [selectedProviderId] = config.defaultModelSelection.split("/");
217
+ if (selectedProviderId === providerId) {
218
+ newDefaultModelSelection = filtered.length > 0 ? `${filtered[0].id}/auto` : void 0;
219
+ }
220
+ }
221
+ const wasActive = config.activeProviderId === providerId;
222
+ const newActiveId = wasActive && filtered.length > 0 ? filtered[0].id : wasActive ? void 0 : config.activeProviderId;
223
+ return {
224
+ ...config,
225
+ providers: filtered.map((p) => ({ ...p, isActive: p.id === newActiveId })),
226
+ activeProviderId: newActiveId,
227
+ defaultModelSelection: newDefaultModelSelection
228
+ };
229
+ }
230
+ function activateProvider(config, providerId) {
231
+ const provider = config.providers?.find((p) => p.id === providerId);
232
+ if (!provider) return config;
233
+ return {
234
+ ...config,
235
+ providers: (config.providers ?? []).map((p) => ({ ...p, isActive: p.id === providerId })),
236
+ activeProviderId: providerId
237
+ };
238
+ }
239
+ function mergeConfigs(...configs) {
240
+ const result = configs.reduce((acc, curr) => {
241
+ if (curr.llm) {
242
+ acc.llm = { ...acc.llm, ...curr.llm };
243
+ }
244
+ if (curr.server) {
245
+ acc.server = { ...acc.server, ...curr.server };
246
+ }
247
+ if (curr.logging) {
248
+ acc.logging = { ...acc.logging, ...curr.logging };
249
+ }
250
+ return acc;
251
+ }, {
252
+ llm: { url: "http://localhost:8000/v1", model: "auto", backend: "auto", maxContext: 2e5, disableThinking: false },
253
+ server: { port: 10369, host: "127.0.0.1", openBrowser: true },
254
+ logging: { level: "info" }
255
+ });
256
+ return oldConfigSchema.parse(result);
257
+ }
258
+
259
+ export {
260
+ trySmartDefaults,
261
+ configFileExists,
262
+ migrateConfig,
263
+ loadGlobalConfig,
264
+ saveGlobalConfig,
265
+ getActiveProvider,
266
+ getDefaultModel,
267
+ setDefaultModelSelection,
268
+ addProvider,
269
+ removeProvider,
270
+ activateProvider,
271
+ mergeConfigs
272
+ };
273
+ //# sourceMappingURL=chunk-43SWNGYW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/config.ts"],"sourcesContent":["import { z } from 'zod'\nimport { randomUUID } from 'node:crypto'\nimport { readFile, writeFile, mkdir, access } from 'node:fs/promises'\nimport { dirname } from 'node:path'\nimport type { Mode } from './main.js'\nimport { getGlobalConfigPath } from './paths.js'\nimport { detectBackend, detectModel } from '../server/llm/index.js'\nimport type { Provider, ProviderBackend } from '../shared/types.js'\n\nconst SMART_DEFAULTS = [\n 'http://localhost:8000',\n 'http://localhost:11434',\n 'http://localhost:8080',\n]\n\nexport async function trySmartDefaults(mode: Mode): Promise<{ url: string; backend: string; model: string } | null> {\n // Try all URLs in parallel, no retries\n const results = await Promise.all(\n SMART_DEFAULTS.map(async (url) => {\n try {\n const [backend, model] = await Promise.all([\n detectBackend(url, undefined, true),\n detectModel(url, 1, true), // Only 1 retry attempt\n ])\n if (backend !== 'unknown' && model) {\n return { url, backend, model }\n }\n } catch {\n // Silent fail\n }\n return null\n })\n )\n \n // Return first successful detection\n return results.find(r => r !== null) || null\n}\n\nexport async function configFileExists(mode: Mode): Promise<boolean> {\n const configPath = getGlobalConfigPath(mode)\n try {\n await access(configPath)\n return true\n } catch {\n return false\n }\n}\n\n// ============================================================================\n// Schema Definitions\n// ============================================================================\n\nconst backendSchema = z.enum(['auto', 'vllm', 'sglang', 'ollama', 'llamacpp', 'openai', 'anthropic', 'unknown'])\n\nconst providerSchema = z.object({\n id: z.string(),\n name: z.string(),\n url: z.string(),\n backend: backendSchema,\n apiKey: z.string().optional(),\n maxContext: z.number().optional(),\n isActive: z.boolean(),\n createdAt: z.string(),\n // Deprecated: model field kept for migration, will be removed after migration\n model: z.string().optional(),\n})\n\nconst serverSchema = z.object({\n port: z.number().default(10369),\n host: z.string().default('127.0.0.1'),\n openBrowser: z.boolean().default(true),\n})\n\nconst loggingSchema = z.object({\n level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),\n})\n\nconst databaseSchema = z.object({\n path: z.string().default(''),\n})\n\nconst workspaceSchema = z.object({\n workdir: z.string().default(process.cwd()),\n})\n\n// New config schema with providers array\nconst configSchema = z.object({\n providers: z.array(providerSchema).default([]),\n defaultModelSelection: z.string().optional(),\n activeProviderId: z.string().optional(),\n activeWorkflowId: z.string().optional(),\n server: serverSchema.default({}),\n logging: loggingSchema.default({}),\n database: databaseSchema.default({}),\n workspace: workspaceSchema.default({}),\n})\n\n// Old config schema (for migration detection)\nconst oldLlmSchema = z.object({\n url: z.string().url().default('http://localhost:8000/v1'),\n model: z.string().default('auto'),\n backend: z.enum(['auto', 'vllm', 'sglang', 'ollama', 'llamacpp']).default('auto'),\n maxContext: z.number().default(200000),\n disableThinking: z.boolean().default(false),\n apiKey: z.string().optional(),\n})\n\nconst oldConfigSchema = z.object({\n llm: oldLlmSchema,\n server: serverSchema.default({}),\n logging: loggingSchema.default({}),\n database: databaseSchema.default({}),\n})\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type GlobalConfig = z.infer<typeof configSchema>\nexport type OldGlobalConfig = z.infer<typeof oldConfigSchema>\n\n// ============================================================================\n// Migration\n// ============================================================================\n\n/**\n * Migrate old config format (single llm object) to new format (providers array).\n * Also migrates provider.model to global defaultModelSelection.\n * If already in new format with defaultModelSelection, returns as-is.\n */\nexport function migrateConfig(raw: unknown): GlobalConfig {\n type RawProvider = { id: string; name: string; url: string; model?: string; backend: string; apiKey?: string; maxContext?: number; isActive: boolean; createdAt: string }\n type RawConfig = { providers: RawProvider[]; activeProviderId?: string; defaultModelSelection?: string; [key: string]: unknown }\n \n // Check if it's already the new format (has providers array)\n if (typeof raw === 'object' && raw !== null && 'providers' in raw) {\n const obj = raw as RawConfig\n \n // If already has defaultModelSelection, just parse and return\n if (obj.defaultModelSelection) {\n // Strip model from providers before parsing\n const providers = obj.providers.map(p => {\n const { model, ...rest } = p\n return rest\n })\n return configSchema.parse({\n ...obj,\n providers,\n })\n }\n \n // Migrate from activeProviderId + provider.model to defaultModelSelection\n const providers = obj.providers.map(p => {\n const { model, ...rest } = p\n return rest\n })\n \n let defaultModelSelection: string | undefined\n if (obj.activeProviderId) {\n const activeProvider = obj.providers.find(p => p.id === obj.activeProviderId)\n if (activeProvider?.model) {\n defaultModelSelection = `${obj.activeProviderId}/${activeProvider.model}`\n } else {\n defaultModelSelection = `${obj.activeProviderId}/auto`\n }\n }\n \n return configSchema.parse({\n ...obj,\n providers,\n defaultModelSelection,\n })\n }\n \n // Check if it's the old format (has llm object)\n if (typeof raw === 'object' && raw !== null && 'llm' in raw) {\n const oldConfig = oldConfigSchema.parse(raw)\n const providerId = randomUUID()\n \n const provider: Provider = {\n id: providerId,\n name: 'Default',\n url: oldConfig.llm.url,\n backend: oldConfig.llm.backend as ProviderBackend,\n apiKey: oldConfig.llm.apiKey,\n maxContext: oldConfig.llm.maxContext,\n isActive: true,\n createdAt: new Date().toISOString(),\n }\n \n const model = oldConfig.llm.model || 'auto'\n \n return {\n providers: [provider],\n defaultModelSelection: `${providerId}/${model}`,\n server: oldConfig.server,\n logging: oldConfig.logging,\n database: oldConfig.database,\n workspace: { workdir: process.cwd() },\n }\n }\n \n // Empty or minimal config - return defaults\n return configSchema.parse(raw)\n}\n\n// ============================================================================\n// Load / Save\n// ============================================================================\n\nexport async function loadGlobalConfig(mode: Mode): Promise<GlobalConfig> {\n const configPath = getGlobalConfigPath(mode)\n \n try {\n const content = await readFile(configPath, 'utf-8')\n const parsed = JSON.parse(content)\n return migrateConfig(parsed)\n } catch {\n return configSchema.parse({})\n }\n}\n\nexport async function saveGlobalConfig(mode: Mode, config: GlobalConfig): Promise<void> {\n const configPath = getGlobalConfigPath(mode)\n await mkdir(dirname(configPath), { recursive: true })\n await writeFile(configPath, JSON.stringify(config, null, 2))\n}\n\n// ============================================================================\n// Provider Helpers\n// ============================================================================\n\nexport function getActiveProvider(config: GlobalConfig): Provider | undefined {\n // Use defaultModelSelection if available\n if (config.defaultModelSelection) {\n const [providerId] = config.defaultModelSelection.split('/')\n return config.providers?.find(p => p.id === providerId)\n }\n // Fallback to activeProviderId for backwards compatibility\n if (!config.activeProviderId) return undefined\n return config.providers?.find(p => p.id === config.activeProviderId)\n}\n\nexport function getDefaultModel(config: GlobalConfig): string | undefined {\n if (!config.defaultModelSelection) return undefined\n const parts = config.defaultModelSelection.split('/')\n return parts[1] ?? 'auto'\n}\n\nexport function setDefaultModelSelection(config: GlobalConfig, providerId: string, model: string): GlobalConfig {\n const defaultModelSelection = `${providerId}/${model}`\n return {\n ...config,\n defaultModelSelection,\n activeProviderId: providerId, // Keep for backwards compatibility\n providers: config.providers?.map(p => ({ ...p, isActive: p.id === providerId })) ?? [],\n }\n}\n\nexport function addProvider(config: GlobalConfig, provider: Omit<Provider, 'id' | 'createdAt'>): GlobalConfig {\n const newProvider: Provider = {\n ...provider,\n id: randomUUID(),\n createdAt: new Date().toISOString(),\n }\n \n // If this is the first provider or marked active, update defaultModelSelection\n const shouldActivate = provider.isActive || config.providers?.length === 0\n \n return {\n ...config,\n providers: [\n ...(config.providers ?? []).map(p => shouldActivate ? { ...p, isActive: false } : p),\n { ...newProvider, isActive: shouldActivate },\n ],\n defaultModelSelection: shouldActivate ? `${newProvider.id}/auto` : config.defaultModelSelection,\n activeProviderId: shouldActivate ? newProvider.id : config.activeProviderId,\n workspace: config.workspace ?? { workdir: process.cwd() },\n }\n}\n\nexport function removeProvider(config: GlobalConfig, providerId: string): GlobalConfig {\n const currentProviders = config.providers ?? []\n const filtered = currentProviders.filter(p => p.id !== providerId)\n \n // Check if we're removing the default model selection's provider\n let newDefaultModelSelection = config.defaultModelSelection\n if (config.defaultModelSelection) {\n const [selectedProviderId] = config.defaultModelSelection.split('/')\n if (selectedProviderId === providerId) {\n // Reset to first available provider with auto\n newDefaultModelSelection = filtered.length > 0 ? `${filtered[0]!.id}/auto` : undefined\n }\n }\n \n const wasActive = config.activeProviderId === providerId\n \n // If we removed the active provider, activate the first remaining one\n const newActiveId = wasActive && filtered.length > 0 \n ? filtered[0]!.id \n : (wasActive ? undefined : config.activeProviderId)\n \n return {\n ...config,\n providers: filtered.map(p => ({ ...p, isActive: p.id === newActiveId })),\n activeProviderId: newActiveId,\n defaultModelSelection: newDefaultModelSelection,\n }\n}\n\nexport function activateProvider(config: GlobalConfig, providerId: string): GlobalConfig {\n const provider = config.providers?.find(p => p.id === providerId)\n if (!provider) return config\n \n return {\n ...config,\n providers: (config.providers ?? []).map(p => ({ ...p, isActive: p.id === providerId })),\n activeProviderId: providerId,\n }\n}\n\n// ============================================================================\n// Legacy mergeConfigs (for backwards compatibility during transition)\n// ============================================================================\n\n/**\n * @deprecated Use provider-based config instead\n */\nexport function mergeConfigs(...configs: Array<Partial<OldGlobalConfig>>): OldGlobalConfig {\n const result = configs.reduce((acc, curr) => {\n if (curr.llm) {\n acc.llm = { ...acc.llm, ...curr.llm }\n }\n if (curr.server) {\n acc.server = { ...acc.server, ...curr.server }\n }\n if (curr.logging) {\n acc.logging = { ...acc.logging, ...curr.logging }\n }\n return acc\n }, {\n llm: { url: 'http://localhost:8000/v1', model: 'auto', backend: 'auto' as const, maxContext: 200000, disableThinking: false },\n server: { port: 10369, host: '127.0.0.1', openBrowser: true },\n logging: { level: 'info' as const },\n })\n return oldConfigSchema.parse(result)\n}\n"],"mappings":"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","names":["providers"]}