fixo-cli 1.0.3 → 2.0.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.
Potentially problematic release.
This version of fixo-cli might be problematic. Click here for more details.
- package/CHANGELOG.md +62 -0
- package/README.md +18 -14
- package/dist/agent/agent-client.d.ts +28 -6
- package/dist/agent/agent-client.d.ts.map +1 -1
- package/dist/agent/agent-client.js +118 -39
- package/dist/agent/agent-client.js.map +1 -1
- package/dist/agent/agent-pool.d.ts +55 -6
- package/dist/agent/agent-pool.d.ts.map +1 -1
- package/dist/agent/agent-pool.js +120 -20
- package/dist/agent/agent-pool.js.map +1 -1
- package/dist/agent/auto-verifier.d.ts +55 -0
- package/dist/agent/auto-verifier.d.ts.map +1 -0
- package/dist/agent/auto-verifier.js +50 -0
- package/dist/agent/auto-verifier.js.map +1 -0
- package/dist/agent/command-parser.d.ts +37 -0
- package/dist/agent/command-parser.d.ts.map +1 -1
- package/dist/agent/command-parser.js +473 -1
- package/dist/agent/command-parser.js.map +1 -1
- package/dist/agent/context-builder.d.ts +24 -0
- package/dist/agent/context-builder.d.ts.map +1 -0
- package/dist/agent/context-builder.js +197 -0
- package/dist/agent/context-builder.js.map +1 -0
- package/dist/agent/conversation.d.ts +32 -2
- package/dist/agent/conversation.d.ts.map +1 -1
- package/dist/agent/conversation.js +84 -9
- package/dist/agent/conversation.js.map +1 -1
- package/dist/agent/duration.d.ts +24 -0
- package/dist/agent/duration.d.ts.map +1 -0
- package/dist/agent/duration.js +42 -0
- package/dist/agent/duration.js.map +1 -0
- package/dist/agent/file-writing-rules.d.ts +19 -0
- package/dist/agent/file-writing-rules.d.ts.map +1 -0
- package/dist/agent/file-writing-rules.js +31 -0
- package/dist/agent/file-writing-rules.js.map +1 -0
- package/dist/agent/mcp-bridge.js +1 -1
- package/dist/agent/mcp-bridge.js.map +1 -1
- package/dist/agent/orchestrator.d.ts +45 -0
- package/dist/agent/orchestrator.d.ts.map +1 -1
- package/dist/agent/orchestrator.js +140 -3
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/parser-adapter.d.ts +17 -0
- package/dist/agent/parser-adapter.d.ts.map +1 -1
- package/dist/agent/parser-adapter.js +311 -7
- package/dist/agent/parser-adapter.js.map +1 -1
- package/dist/agent/predictive-gate.d.ts.map +1 -1
- package/dist/agent/predictive-gate.js +4 -1
- package/dist/agent/predictive-gate.js.map +1 -1
- package/dist/agent/provider-cooldown.d.ts.map +1 -1
- package/dist/agent/provider-cooldown.js +3 -2
- package/dist/agent/provider-cooldown.js.map +1 -1
- package/dist/agent/providers-manager.d.ts +5 -0
- package/dist/agent/providers-manager.d.ts.map +1 -1
- package/dist/agent/providers-manager.js +119 -8
- package/dist/agent/providers-manager.js.map +1 -1
- package/dist/agent/repo-map.d.ts +18 -1
- package/dist/agent/repo-map.d.ts.map +1 -1
- package/dist/agent/repo-map.js +144 -54
- package/dist/agent/repo-map.js.map +1 -1
- package/dist/agent/retry.js +1 -2
- package/dist/agent/retry.js.map +1 -1
- package/dist/agent/single-agent.d.ts +13 -0
- package/dist/agent/single-agent.d.ts.map +1 -1
- package/dist/agent/single-agent.js +225 -37
- package/dist/agent/single-agent.js.map +1 -1
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +2 -1
- package/dist/agent/skills.js.map +1 -1
- package/dist/agent/subagent.js +2 -2
- package/dist/agent/subagent.js.map +1 -1
- package/dist/agent/task-router.d.ts +46 -0
- package/dist/agent/task-router.d.ts.map +1 -0
- package/dist/agent/task-router.js +352 -0
- package/dist/agent/task-router.js.map +1 -0
- package/dist/agent/telemetry.d.ts +29 -1
- package/dist/agent/telemetry.d.ts.map +1 -1
- package/dist/agent/telemetry.js +29 -11
- package/dist/agent/telemetry.js.map +1 -1
- package/dist/agent/tool-definitions.d.ts +3 -0
- package/dist/agent/tool-definitions.d.ts.map +1 -0
- package/dist/agent/tool-definitions.js +519 -0
- package/dist/agent/tool-definitions.js.map +1 -0
- package/dist/agent/tool-executor.d.ts +6 -1
- package/dist/agent/tool-executor.d.ts.map +1 -1
- package/dist/agent/tool-executor.js +99 -553
- package/dist/agent/tool-executor.js.map +1 -1
- package/dist/agent/tools/command-tools.d.ts +6 -0
- package/dist/agent/tools/command-tools.d.ts.map +1 -0
- package/dist/agent/tools/command-tools.js +104 -0
- package/dist/agent/tools/command-tools.js.map +1 -0
- package/dist/agent/tools/file-tools.d.ts +15 -0
- package/dist/agent/tools/file-tools.d.ts.map +1 -0
- package/dist/agent/tools/file-tools.js +551 -0
- package/dist/agent/tools/file-tools.js.map +1 -0
- package/dist/agent/tools/todo-tools.d.ts +3 -0
- package/dist/agent/tools/todo-tools.d.ts.map +1 -0
- package/dist/agent/tools/todo-tools.js +70 -0
- package/dist/agent/tools/todo-tools.js.map +1 -0
- package/dist/agent/web-impl.d.ts.map +1 -1
- package/dist/agent/web-impl.js +45 -0
- package/dist/agent/web-impl.js.map +1 -1
- package/dist/agent/worker-agent.d.ts +3 -1
- package/dist/agent/worker-agent.d.ts.map +1 -1
- package/dist/agent/worker-agent.js +56 -16
- package/dist/agent/worker-agent.js.map +1 -1
- package/dist/config.d.ts +253 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +81 -1
- package/dist/config.js.map +1 -1
- package/dist/git/git-manager.d.ts +33 -2
- package/dist/git/git-manager.d.ts.map +1 -1
- package/dist/git/git-manager.js +111 -15
- package/dist/git/git-manager.js.map +1 -1
- package/dist/git/git-ops.d.ts.map +1 -1
- package/dist/git/git-ops.js +2 -1
- package/dist/git/git-ops.js.map +1 -1
- package/dist/index.js +89 -8
- package/dist/index.js.map +1 -1
- package/dist/lsp/lsp-manager.js +1 -1
- package/dist/lsp/lsp-manager.js.map +1 -1
- package/dist/model-outcomes.d.ts.map +1 -1
- package/dist/model-outcomes.js +2 -1
- package/dist/model-outcomes.js.map +1 -1
- package/dist/planner.d.ts +0 -9
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +0 -9
- package/dist/planner.js.map +1 -1
- package/dist/project-memory.d.ts +12 -1
- package/dist/project-memory.d.ts.map +1 -1
- package/dist/project-memory.js +8 -6
- package/dist/project-memory.js.map +1 -1
- package/dist/runtime/loop-mitigation.d.ts +119 -0
- package/dist/runtime/loop-mitigation.d.ts.map +1 -0
- package/dist/runtime/loop-mitigation.js +192 -0
- package/dist/runtime/loop-mitigation.js.map +1 -0
- package/dist/runtime/os-sandbox.d.ts +100 -0
- package/dist/runtime/os-sandbox.d.ts.map +1 -0
- package/dist/runtime/os-sandbox.js +246 -0
- package/dist/runtime/os-sandbox.js.map +1 -0
- package/dist/runtime/run-inventory.d.ts +17 -0
- package/dist/runtime/run-inventory.d.ts.map +1 -0
- package/dist/runtime/run-inventory.js +49 -0
- package/dist/runtime/run-inventory.js.map +1 -0
- package/dist/runtime/session-snapshots.d.ts +52 -2
- package/dist/runtime/session-snapshots.d.ts.map +1 -1
- package/dist/runtime/session-snapshots.js +76 -1
- package/dist/runtime/session-snapshots.js.map +1 -1
- package/dist/runtime/staging.d.ts.map +1 -1
- package/dist/runtime/staging.js +4 -1
- package/dist/runtime/staging.js.map +1 -1
- package/dist/runtime/task-session.d.ts +14 -0
- package/dist/runtime/task-session.d.ts.map +1 -1
- package/dist/runtime/task-session.js +26 -0
- package/dist/runtime/task-session.js.map +1 -1
- package/dist/setup-wizard.d.ts +11 -3
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +113 -15
- package/dist/setup-wizard.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/commands/context-commands.d.ts +7 -0
- package/dist/ui/commands/context-commands.d.ts.map +1 -0
- package/dist/ui/commands/context-commands.js +241 -0
- package/dist/ui/commands/context-commands.js.map +1 -0
- package/dist/ui/commands/index.d.ts +3 -0
- package/dist/ui/commands/index.d.ts.map +1 -0
- package/dist/ui/commands/index.js +46 -0
- package/dist/ui/commands/index.js.map +1 -0
- package/dist/ui/commands/info-commands.d.ts +15 -0
- package/dist/ui/commands/info-commands.d.ts.map +1 -0
- package/dist/ui/commands/info-commands.js +122 -0
- package/dist/ui/commands/info-commands.js.map +1 -0
- package/dist/ui/commands/model-commands.d.ts +5 -0
- package/dist/ui/commands/model-commands.d.ts.map +1 -0
- package/dist/ui/commands/model-commands.js +417 -0
- package/dist/ui/commands/model-commands.js.map +1 -0
- package/dist/ui/commands/session-commands.d.ts +5 -0
- package/dist/ui/commands/session-commands.d.ts.map +1 -0
- package/dist/ui/commands/session-commands.js +154 -0
- package/dist/ui/commands/session-commands.js.map +1 -0
- package/dist/ui/commands/task-commands.d.ts +8 -0
- package/dist/ui/commands/task-commands.d.ts.map +1 -0
- package/dist/ui/commands/task-commands.js +152 -0
- package/dist/ui/commands/task-commands.js.map +1 -0
- package/dist/ui/commands/types.d.ts +46 -0
- package/dist/ui/commands/types.d.ts.map +1 -0
- package/dist/ui/commands/types.js +2 -0
- package/dist/ui/commands/types.js.map +1 -0
- package/dist/ui/commands/workspace-commands.d.ts +8 -0
- package/dist/ui/commands/workspace-commands.d.ts.map +1 -0
- package/dist/ui/commands/workspace-commands.js +131 -0
- package/dist/ui/commands/workspace-commands.js.map +1 -0
- package/dist/ui/loading-animation.d.ts +24 -0
- package/dist/ui/loading-animation.d.ts.map +1 -0
- package/dist/ui/loading-animation.js +123 -0
- package/dist/ui/loading-animation.js.map +1 -0
- package/dist/ui/markdown-stream.js +2 -2
- package/dist/ui/markdown-stream.js.map +1 -1
- package/dist/ui/prompt.d.ts +7 -0
- package/dist/ui/prompt.d.ts.map +1 -1
- package/dist/ui/prompt.js +461 -1143
- package/dist/ui/prompt.js.map +1 -1
- package/dist/ui/render-primitives.d.ts +6 -0
- package/dist/ui/render-primitives.d.ts.map +1 -1
- package/dist/ui/render-primitives.js +30 -13
- package/dist/ui/render-primitives.js.map +1 -1
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +2 -0
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/session-header.d.ts +13 -0
- package/dist/ui/session-header.d.ts.map +1 -1
- package/dist/ui/session-header.js +6 -0
- package/dist/ui/session-header.js.map +1 -1
- package/package.json +22 -4
- package/scripts/check-vendor-wasm.js +55 -0
- package/vendor/tree-sitter-bash.wasm +0 -0
- package/vendor/tree-sitter-go.wasm +0 -0
- package/vendor/tree-sitter-javascript.wasm +0 -0
- package/vendor/tree-sitter-python.wasm +0 -0
- package/vendor/tree-sitter-rust.wasm +0 -0
- package/vendor/tree-sitter-tsx.wasm +0 -0
- package/vendor/tree-sitter-typescript.wasm +0 -0
- package/vendor/tree-sitter.wasm +0 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to FixO CLI will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [1.0.4] – 2025-06-26
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
- **decryptKey** now throws on AES-256-GCM decryption failure instead of silently returning ciphertext, preventing corrupted keys from being used as live credentials.
|
|
13
|
+
- `getOrCreateRunId()` switched from `Math.random()` to `crypto.randomBytes(6)` for cryptographically secure staging-directory namespace IDs.
|
|
14
|
+
- `RETRYABLE_STATUS_CODES` in `agent-client.ts` now includes `504` (Gateway Timeout), matching the canonical set in `retry.ts`.
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
- Fixed a duplicate `name === 'AbortError'` condition in `defaultIsRetryable` (dead-code bug in `retry.ts`).
|
|
18
|
+
- `SIGINT` handler is now deduplicated when both the readline interface and the process fire simultaneously.
|
|
19
|
+
- `buildLavaStatusState()` now derives the `transport` field from the actual `provider_mode` config instead of always displaying `'freellmapi'`.
|
|
20
|
+
- `getOrCreateRunId()` uses canonical `MUTATION_TOOL_NAMES` set instead of a fragile string-heuristic for mutating action detection.
|
|
21
|
+
|
|
22
|
+
### Improvements
|
|
23
|
+
- Non-null assertions (`!`) in setup-wizard provider registry lookups replaced with proper runtime guards.
|
|
24
|
+
- Removed dead empty section headers from `src/ui/prompt.ts`.
|
|
25
|
+
- Simplified `buildLavaStatusState()` ternary chain (removed unreachable `else` branch).
|
|
26
|
+
- Removed unused `width` variable from `drawSuggestions()`.
|
|
27
|
+
- Silent `catch {}` in `exitCleanup` now logs in debug/verbose mode.
|
|
28
|
+
- Trailing whitespace removed from `retry.ts`.
|
|
29
|
+
|
|
30
|
+
### Packaging
|
|
31
|
+
- Added `"exports"` field to `package.json` for proper ESM resolution.
|
|
32
|
+
- Added `postinstall` script to enforce Node.js >= 20.0.0 at install time.
|
|
33
|
+
- `CHANGELOG.md` added to published `files` list.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## [1.0.3] – 2025-06-20
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- Atomic staging pipeline with rollback (`AtomicStagingManager`).
|
|
41
|
+
- LSP pre-save gate (Pillar 3) for syntax validation before disk writes.
|
|
42
|
+
- Semantic loop detector (`SemanticLoopDetector`) to complement hash-based loop trap.
|
|
43
|
+
- `run_command_async` / `poll_command_status` / `kill_command` tools for long-running tasks.
|
|
44
|
+
- `glob_files` tool using Node.js 22+ native `fs.promises.glob`.
|
|
45
|
+
|
|
46
|
+
### Security
|
|
47
|
+
- AES-256-GCM encryption for API keys at rest in `providers.json`.
|
|
48
|
+
- `WorkspaceGuard.assertNotPlatformPath()` prevents agent from modifying its own source files.
|
|
49
|
+
- `SCRUB_PATTERNS` expanded to cover OpenAI, Anthropic, OpenRouter, GitHub, Google, AWS, and JWT tokens.
|
|
50
|
+
- Provider credential vault (`ProviderKeyVault`) with scoped `withApiKey` callbacks.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## [1.0.0] – 2025-05-01
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- Initial release of FixO CLI.
|
|
58
|
+
- Multi-provider support: OpenAI, Anthropic, Groq, Google, Mistral, Together, Perplexity, DeepSeek, Cohere, OpenRouter, NVIDIA, xAI, GitHub Models, Ollama, Zen.
|
|
59
|
+
- FreeLLMAPI proxy mode with load-balanced failover.
|
|
60
|
+
- Interactive setup wizard (`/setup`).
|
|
61
|
+
- Loop-trap detection, atomic writes, LSP integration.
|
|
62
|
+
- REPL with slash commands, autocomplete, paste attachments, and session history.
|
package/README.md
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
[](https://www.typescriptlang.org/)
|
|
5
5
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
6
6
|
[](https://tree-sitter.github.io/tree-sitter/)
|
|
7
|
-
[]()
|
|
8
8
|
|
|
9
|
-
Fixo CLI is a terminal-based autonomous coding assistant designed to execute complex programming tasks directly in your workspace.
|
|
9
|
+
Fixo CLI is a terminal-based autonomous coding assistant designed to execute complex programming tasks directly in your workspace. It writes implementation plans, edits code files, runs test suites, and iterates toward the goal. Tree-sitter is used today for shell-command parsing and LSP-fallback syntax checks; expanding it to the workspace symbol map is on the roadmap.
|
|
10
10
|
|
|
11
|
-
Fixo CLI
|
|
11
|
+
Fixo CLI ships with **13 direct providers built-in** (OpenAI, Anthropic, Google, Groq, Mistral, Cohere, OpenRouter, NVIDIA, Cerebras, SambaNova, GitHub Models, xAI, Zen) — paste your own key for any of them. The optional **FreeLLMAPI** proxy backend is available as an opt-in convenience for users who want load-balanced failover across free-tier providers without managing individual keys.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -18,33 +18,35 @@ Here is how Fixo CLI compares against other prominent terminal and editor-based
|
|
|
18
18
|
|
|
19
19
|
| Feature / Metric | **Fixo CLI** | **Claude Code** | **Aider** | **Cline** |
|
|
20
20
|
| :--- | :--- | :--- | :--- | :--- |
|
|
21
|
-
| **API Cost** | 💰 **
|
|
22
|
-
| **Multi-Provider Fallback**| 🔄 **Automatic
|
|
23
|
-
| **Workspace Indexing** |
|
|
24
|
-
| **Autonomy Loops** | 🤖 **Multi-agent / Planning Mode** | 🤖 Agent loops | 💬 Interactive / chat-driven | 💬 Prompt-to-action loops |
|
|
25
|
-
| **Self-Correction** | 🧪
|
|
26
|
-
| **No-Card Verification** | ✅ **Yes** (
|
|
21
|
+
| **API Cost** | 💰 **BYOK or free via optional FreeLLMAPI proxy** | 💸 **Paid** (Anthropic API charges) | 💸 **Paid** (Requires personal keys) | 💸 **Paid** (Requires personal keys) |
|
|
22
|
+
| **Multi-Provider Fallback**| 🔄 **Automatic failover (FreeLLMAPI proxy mode)** | ❌ None (Locked to Anthropic) | ❌ Manual (Requires editing configs) | ❌ Manual (Drops request on 429) |
|
|
23
|
+
| **Workspace Indexing** | 🗂️ Depth-capped regex scan (tree-sitter symbol map planned) | 🔍 Regex / basic grep | 🗺️ Git/ctags-based map | 🔍 Basic file search |
|
|
24
|
+
| **Autonomy Loops** | 🤖 **Multi-agent / Planning Mode** ¹ | 🤖 Agent loops | 💬 Interactive / chat-driven | 💬 Prompt-to-action loops |
|
|
25
|
+
| **Self-Correction** | 🧪 Opt-in via `/fix-tests` (automatic post-edit verification on the roadmap) | ❌ Manual trigger | ❌ Requires manual input | ❌ Requires manual input |
|
|
26
|
+
| **No-Card Verification** | ✅ **Yes** (BYOK or free proxy — no card required either way) | ❌ No (Requires credit card) | ❌ No (Requires paid API keys) | ❌ No (Requires paid API keys) |
|
|
27
|
+
|
|
28
|
+
¹ The multi-agent path is currently triggered by a keyword heuristic. An LLM-based complexity classifier is wired-but-dead in `src/planner.ts` and is being moved onto the live code path.
|
|
27
29
|
|
|
28
30
|
---
|
|
29
31
|
|
|
30
32
|
## ⚙️ Architecture & Lifecycle Flow
|
|
31
33
|
|
|
32
|
-
Fixo CLI separates concerns between code understanding (
|
|
34
|
+
Fixo CLI separates concerns between code understanding (workspace indexer), task coordination (Planner), and execution (Agent).
|
|
33
35
|
|
|
34
36
|
```mermaid
|
|
35
37
|
sequenceDiagram
|
|
36
38
|
autonumber
|
|
37
39
|
actor User as Developer
|
|
38
40
|
participant CLI as Fixo CLI
|
|
39
|
-
participant Indexer as
|
|
41
|
+
participant Indexer as Workspace Indexer
|
|
40
42
|
participant Planner as Plan Engine
|
|
41
43
|
participant Agent as Autonomous Agent
|
|
42
44
|
participant Proxy as FreeLLMAPI Proxy
|
|
43
45
|
participant LLM as Provider (Groq/Gemini/NIM)
|
|
44
46
|
|
|
45
47
|
User->>CLI: Request task (e.g. "Fix auth bug")
|
|
46
|
-
CLI->>Indexer: Scan repository &
|
|
47
|
-
Indexer-->>CLI: Return
|
|
48
|
+
CLI->>Indexer: Scan repository & build export map
|
|
49
|
+
Indexer-->>CLI: Return repository structure summary
|
|
48
50
|
CLI->>Planner: Propose implementation plan
|
|
49
51
|
Planner->>Proxy: Fetch reasoning (smart routing)
|
|
50
52
|
Proxy->>LLM: Try highest ranked provider
|
|
@@ -61,12 +63,14 @@ sequenceDiagram
|
|
|
61
63
|
Agent-->>User: Task completed successfully!
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
> **Note on routing:** Smart routing across models (cheaper for planning, stronger for execution) is a feature of the optional FreeLLMAPI proxy backend. In direct-provider mode, requests use the model you selected at setup — local fast/heavy-tier substitution via `preferences.modelRouting` is on the roadmap.
|
|
67
|
+
|
|
64
68
|
---
|
|
65
69
|
|
|
66
70
|
## 🌟 Key Features
|
|
67
71
|
|
|
68
72
|
* **Autonomous Agent Loop:** Fixo CLI runs an agent loop that defines planning sub-agents, writes files, runs shell commands, reads compiler output, and self-corrects until tests pass.
|
|
69
|
-
* **Workspace
|
|
73
|
+
* **Workspace Indexer:** Today, a depth-capped (4 levels, 200 files) directory scanner extracts exports via regex for a quick repository map. A tree-sitter-backed symbol map for TS/JS/Python/Go/Rust is on the roadmap; the WASM runtime is already vendored and used elsewhere in the codebase.
|
|
70
74
|
* **Free Multi-Provider Routing:** Connects to your FreeLLMAPI server to query models like Llama 3.3, Qwen 3, and Gemini 2.5/3.1 without incurring high API costs.
|
|
71
75
|
* **Smart Cooldown & Failover:** The CLI automatically tracks rate-limited providers (429/402/404) and switches to working alternatives in the fallback chain mid-request.
|
|
72
76
|
* **Resilience Stack:** Stream recovery, provider cooldown, context-budget enforcement, and a local telemetry sink work together so the agent stays productive on flaky networks and large codebases. See [Resilience](#-resilience) below.
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Includes retry with exponential backoff for transient errors.
|
|
5
5
|
*/
|
|
6
6
|
import type { ChatMessage, ChatToolDefinition, ChatToolChoice, TokenUsage } from '../shared/types.js';
|
|
7
|
+
import { type ModelRoutingConfig } from '../config.js';
|
|
7
8
|
export interface ChatOptions {
|
|
8
9
|
tools?: ChatToolDefinition[];
|
|
9
10
|
tool_choice?: ChatToolChoice;
|
|
@@ -68,19 +69,40 @@ export declare class HttpError extends Error {
|
|
|
68
69
|
status: number;
|
|
69
70
|
constructor(status: number, message: string);
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Thrown when the client is running in direct-provider mode but the
|
|
74
|
+
* model the caller asked for did not resolve to any direct provider
|
|
75
|
+
* via {@link AgentClient.resolveDirectConfig}. Catching this gives
|
|
76
|
+
* the UI a chance to suggest `/model` or `/providers add` instead of
|
|
77
|
+
* silently leaking the request to the FreeLLMAPI proxy.
|
|
78
|
+
*/
|
|
79
|
+
export declare class DirectModelUnresolvedError extends Error {
|
|
80
|
+
model: string;
|
|
81
|
+
constructor(model: string);
|
|
82
|
+
}
|
|
71
83
|
export declare class AgentClient {
|
|
72
84
|
private baseUrl;
|
|
73
85
|
private apiKey;
|
|
74
86
|
private verbose;
|
|
75
|
-
|
|
87
|
+
private providerMode;
|
|
88
|
+
private modelRouting;
|
|
89
|
+
constructor(apiKey: string, apiUrl?: string, verbose?: boolean, providerMode?: 'direct' | 'proxy', modelRouting?: ModelRoutingConfig);
|
|
90
|
+
/**
|
|
91
|
+
* Phase 2.4 — substitute the caller-supplied model with a
|
|
92
|
+
* configured tier when `required_capabilities` asks for one.
|
|
93
|
+
* Returns the caller's model unchanged when no matching tier is
|
|
94
|
+
* configured, so the call is a no-op for users who haven't set
|
|
95
|
+
* up routing.
|
|
96
|
+
*/
|
|
97
|
+
private applyCapabilityRouting;
|
|
76
98
|
private resolveDirectConfig;
|
|
77
99
|
/**
|
|
78
|
-
* Maps a model id to the
|
|
79
|
-
*
|
|
80
|
-
* `
|
|
81
|
-
*
|
|
100
|
+
* Maps a model id to the tracking key for `providerCooldown`.
|
|
101
|
+
* Model-specific isolation ensures a timeout on one model (e.g.
|
|
102
|
+
* `openrouter:claude-3`) does not poison other models on the
|
|
103
|
+
* same provider gateway.
|
|
82
104
|
*/
|
|
83
|
-
private
|
|
105
|
+
private getCooldownKey;
|
|
84
106
|
chat(messages: ChatMessage[], model: string, options?: ChatOptions): Promise<ChatResult>;
|
|
85
107
|
private executeSingleChatStreamAttempt;
|
|
86
108
|
chatStream(messages: ChatMessage[], model: string, options?: ChatOptions): AsyncGenerator<StreamChunk>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-client.d.ts","sourceRoot":"","sources":["../../src/agent/agent-client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EACV,WAAW,EAGX,kBAAkB,EAClB,cAAc,EACd,UAAU,EACX,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-client.d.ts","sourceRoot":"","sources":["../../src/agent/agent-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,8 +9,23 @@ import { extractTextFromContent } from '../shared/content.js';
|
|
|
9
9
|
/* ──────────────────────── Constants ──────────────────────── */
|
|
10
10
|
const MAX_RETRIES = 5;
|
|
11
11
|
const BASE_DELAY_MS = 1500;
|
|
12
|
-
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503]);
|
|
13
|
-
|
|
12
|
+
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
13
|
+
function getValidatedApiUrl(urlStr) {
|
|
14
|
+
if (!urlStr)
|
|
15
|
+
return undefined;
|
|
16
|
+
try {
|
|
17
|
+
const parsed = new URL(urlStr);
|
|
18
|
+
if (parsed.protocol === 'http:' && parsed.hostname !== 'localhost' && parsed.hostname !== '127.0.0.1') {
|
|
19
|
+
console.warn(`[Security Warning] API URL is using an insecure HTTP protocol (${urlStr}). HTTPS is required for remote URLs. Falling back to default.`);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return urlStr;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const BASE_URL = getValidatedApiUrl(process.env.FIXO_API_URL) || DEFAULT_API_URL;
|
|
14
29
|
/** Wrapper around `providerCooldown.recordFailure` that also emits a
|
|
15
30
|
* telemetry event. Keeps the 6 callsites terse. */
|
|
16
31
|
function trackProviderError(providerId, status, message) {
|
|
@@ -152,14 +167,55 @@ export class HttpError extends Error {
|
|
|
152
167
|
}
|
|
153
168
|
}
|
|
154
169
|
/* ──────────────────────── AgentClient ──────────────────────── */
|
|
170
|
+
/**
|
|
171
|
+
* Thrown when the client is running in direct-provider mode but the
|
|
172
|
+
* model the caller asked for did not resolve to any direct provider
|
|
173
|
+
* via {@link AgentClient.resolveDirectConfig}. Catching this gives
|
|
174
|
+
* the UI a chance to suggest `/model` or `/providers add` instead of
|
|
175
|
+
* silently leaking the request to the FreeLLMAPI proxy.
|
|
176
|
+
*/
|
|
177
|
+
export class DirectModelUnresolvedError extends Error {
|
|
178
|
+
model;
|
|
179
|
+
constructor(model) {
|
|
180
|
+
super(`Model "${model}" did not match any direct provider configured in your vault. ` +
|
|
181
|
+
`Run /providers to add a key, /model to pick a recognized model, or run setup again to switch to FreeLLMAPI proxy mode.`);
|
|
182
|
+
this.model = model;
|
|
183
|
+
this.name = 'DirectModelUnresolvedError';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
155
186
|
export class AgentClient {
|
|
156
187
|
baseUrl;
|
|
157
188
|
apiKey;
|
|
158
189
|
verbose;
|
|
159
|
-
|
|
160
|
-
|
|
190
|
+
providerMode;
|
|
191
|
+
modelRouting;
|
|
192
|
+
constructor(apiKey, apiUrl, verbose = false, providerMode = 'proxy', modelRouting) {
|
|
193
|
+
this.baseUrl = getValidatedApiUrl(process.env.FIXO_API_URL) || getValidatedApiUrl(apiUrl) || BASE_URL;
|
|
161
194
|
this.apiKey = apiKey;
|
|
162
195
|
this.verbose = verbose;
|
|
196
|
+
this.providerMode = providerMode;
|
|
197
|
+
this.modelRouting = modelRouting ?? {};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Phase 2.4 — substitute the caller-supplied model with a
|
|
201
|
+
* configured tier when `required_capabilities` asks for one.
|
|
202
|
+
* Returns the caller's model unchanged when no matching tier is
|
|
203
|
+
* configured, so the call is a no-op for users who haven't set
|
|
204
|
+
* up routing.
|
|
205
|
+
*/
|
|
206
|
+
applyCapabilityRouting(model, capabilities) {
|
|
207
|
+
if (!capabilities || capabilities.length === 0)
|
|
208
|
+
return model;
|
|
209
|
+
if (capabilities.includes('fast') && this.modelRouting.fast) {
|
|
210
|
+
return this.modelRouting.fast;
|
|
211
|
+
}
|
|
212
|
+
if (capabilities.includes('heavy') && this.modelRouting.heavy) {
|
|
213
|
+
return this.modelRouting.heavy;
|
|
214
|
+
}
|
|
215
|
+
if (this.modelRouting.default) {
|
|
216
|
+
return this.modelRouting.default;
|
|
217
|
+
}
|
|
218
|
+
return model;
|
|
163
219
|
}
|
|
164
220
|
resolveDirectConfig(model) {
|
|
165
221
|
const modelLower = model.toLowerCase();
|
|
@@ -241,29 +297,39 @@ export class AgentClient {
|
|
|
241
297
|
return null;
|
|
242
298
|
}
|
|
243
299
|
/**
|
|
244
|
-
* Maps a model id to the
|
|
245
|
-
*
|
|
246
|
-
* `
|
|
247
|
-
*
|
|
300
|
+
* Maps a model id to the tracking key for `providerCooldown`.
|
|
301
|
+
* Model-specific isolation ensures a timeout on one model (e.g.
|
|
302
|
+
* `openrouter:claude-3`) does not poison other models on the
|
|
303
|
+
* same provider gateway.
|
|
248
304
|
*/
|
|
249
|
-
|
|
305
|
+
getCooldownKey(model) {
|
|
250
306
|
const direct = this.resolveDirectConfig(model);
|
|
251
307
|
if (direct)
|
|
252
|
-
return direct.providerName
|
|
253
|
-
return
|
|
308
|
+
return `${direct.providerName}:${model}`;
|
|
309
|
+
return `freellmapi:${model}`;
|
|
254
310
|
}
|
|
255
311
|
/* ─── Non-streaming chat ─── */
|
|
256
312
|
async chat(messages, model, options = {}) {
|
|
257
313
|
const { signal: externalSignal, ...restOptions } = options;
|
|
258
|
-
|
|
259
|
-
|
|
314
|
+
// Phase 2.4 — substitute the model BEFORE provider resolution so
|
|
315
|
+
// both the routing decision and the eventual request body see
|
|
316
|
+
// the same name. No-op when no capabilities are tagged or no
|
|
317
|
+
// tier is configured.
|
|
318
|
+
model = this.applyCapabilityRouting(model, options.required_capabilities);
|
|
319
|
+
const cooldownKey = this.getCooldownKey(model);
|
|
320
|
+
providerCooldown.assertAvailable(cooldownKey);
|
|
260
321
|
const direct = this.resolveDirectConfig(model);
|
|
261
322
|
const isAnthropicDirect = direct && direct.providerName === 'anthropic';
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
323
|
+
// Direct-mode safety: refuse to silently fall through to the
|
|
324
|
+
// FreeLLMAPI proxy when the user explicitly chose direct mode at
|
|
325
|
+
// setup. A user who picked direct deserves a loud error, not a
|
|
326
|
+
// request that surprises them by transiting a third-party SaaS.
|
|
327
|
+
if (this.providerMode === 'direct' && !direct) {
|
|
328
|
+
throw new DirectModelUnresolvedError(model);
|
|
329
|
+
}
|
|
330
|
+
// The timeout was removed to allow slow reasoning models to take as long as they need.
|
|
331
|
+
// The request will only abort if the user explicitly cancels it via `externalSignal`.
|
|
332
|
+
const combinedSignal = externalSignal;
|
|
267
333
|
let requestUrl = `${this.baseUrl}/chat/completions`;
|
|
268
334
|
let headers = {
|
|
269
335
|
'Content-Type': 'application/json',
|
|
@@ -335,7 +401,7 @@ export class AgentClient {
|
|
|
335
401
|
body = JSON.stringify(bodyObj);
|
|
336
402
|
}
|
|
337
403
|
// Check for pre-flight cancellation
|
|
338
|
-
if (combinedSignal
|
|
404
|
+
if (combinedSignal?.aborted) {
|
|
339
405
|
throw new Error('Task cancelled by user.');
|
|
340
406
|
}
|
|
341
407
|
let lastError = null;
|
|
@@ -356,7 +422,7 @@ export class AgentClient {
|
|
|
356
422
|
}
|
|
357
423
|
// Retryable errors
|
|
358
424
|
if (RETRYABLE_STATUS_CODES.has(response.status)) {
|
|
359
|
-
trackProviderError(
|
|
425
|
+
trackProviderError(cooldownKey, response.status, `HTTP ${response.status}`);
|
|
360
426
|
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
361
427
|
if (attempt < MAX_RETRIES) {
|
|
362
428
|
console.log(`${colors.yellow}⚠ [API] Error ${response.status}. Retrying in ${(delayMs / 1000).toFixed(1)}s (${attempt + 1}/${MAX_RETRIES})${colors.reset}`);
|
|
@@ -369,9 +435,11 @@ export class AgentClient {
|
|
|
369
435
|
throw new Error(`API error (${response.status}): ${errorText}`);
|
|
370
436
|
}
|
|
371
437
|
const rawData = await response.json();
|
|
372
|
-
const data = isAnthropicDirect
|
|
438
|
+
const data = isAnthropicDirect
|
|
439
|
+
? translateAnthropicToOpenAI(rawData)
|
|
440
|
+
: rawData;
|
|
373
441
|
const choice = data.choices[0];
|
|
374
|
-
providerCooldown.recordSuccess(
|
|
442
|
+
providerCooldown.recordSuccess(cooldownKey);
|
|
375
443
|
// ChatResult.content is `string | null`. The widened
|
|
376
444
|
// ChatMessage.content union allows blocks on input, but
|
|
377
445
|
// every provider we ship returns text-only assistant
|
|
@@ -427,7 +495,7 @@ export class AgentClient {
|
|
|
427
495
|
}
|
|
428
496
|
}
|
|
429
497
|
if (isNetworkError && attempt < MAX_RETRIES) {
|
|
430
|
-
trackProviderError(
|
|
498
|
+
trackProviderError(cooldownKey, 0, lastError.message.slice(0, 200));
|
|
431
499
|
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
432
500
|
console.log(`${colors.yellow}⚠ [Network] ${lastError.message.slice(0, 60)}. Retrying in ${(delayMs / 1000).toFixed(1)}s (${attempt + 1}/${MAX_RETRIES})${colors.reset}`);
|
|
433
501
|
await sleep(delayMs);
|
|
@@ -442,9 +510,9 @@ export class AgentClient {
|
|
|
442
510
|
throw lastError ?? new Error('All retry attempts exhausted.');
|
|
443
511
|
}
|
|
444
512
|
/* ─── Streaming chat (SSE) ─── */
|
|
445
|
-
async *executeSingleChatStreamAttempt(requestUrl, headers, body, model, isAnthropicDirect, signal
|
|
513
|
+
async *executeSingleChatStreamAttempt(requestUrl, headers, body, model, isAnthropicDirect, signal) {
|
|
446
514
|
// Pre-flight cancellation check
|
|
447
|
-
if (signal
|
|
515
|
+
if (signal?.aborted) {
|
|
448
516
|
throw new Error('Task cancelled by user.');
|
|
449
517
|
}
|
|
450
518
|
const response = await fetch(requestUrl, {
|
|
@@ -722,13 +790,18 @@ export class AgentClient {
|
|
|
722
790
|
}
|
|
723
791
|
async *chatStream(messages, model, options = {}) {
|
|
724
792
|
const { signal: externalSignal, ...restOptions } = options;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
|
|
793
|
+
// Phase 2.4 — capability-tier substitution (see chat() comment).
|
|
794
|
+
model = this.applyCapabilityRouting(model, options.required_capabilities);
|
|
795
|
+
// The timeout was removed to allow slow reasoning models to take as long as they need.
|
|
796
|
+
const combinedSignal = externalSignal;
|
|
797
|
+
const cooldownKey = this.getCooldownKey(model);
|
|
798
|
+
providerCooldown.assertAvailable(cooldownKey);
|
|
730
799
|
const direct = this.resolveDirectConfig(model);
|
|
731
800
|
const isAnthropicDirect = !!(direct && direct.providerName === 'anthropic');
|
|
801
|
+
// Same direct-mode safety as `chat()` — refuse to leak to proxy.
|
|
802
|
+
if (this.providerMode === 'direct' && !direct) {
|
|
803
|
+
throw new DirectModelUnresolvedError(model);
|
|
804
|
+
}
|
|
732
805
|
let requestUrl = `${this.baseUrl}/chat/completions`;
|
|
733
806
|
let headers = {
|
|
734
807
|
'Content-Type': 'application/json',
|
|
@@ -807,7 +880,7 @@ export class AgentClient {
|
|
|
807
880
|
hasYielded = true;
|
|
808
881
|
yield chunk;
|
|
809
882
|
}
|
|
810
|
-
providerCooldown.recordSuccess(
|
|
883
|
+
providerCooldown.recordSuccess(cooldownKey);
|
|
811
884
|
return; // Success — don't retry
|
|
812
885
|
}
|
|
813
886
|
catch (error) {
|
|
@@ -850,7 +923,7 @@ export class AgentClient {
|
|
|
850
923
|
}
|
|
851
924
|
}
|
|
852
925
|
if (lastError instanceof HttpError && RETRYABLE_STATUS_CODES.has(lastError.status)) {
|
|
853
|
-
trackProviderError(
|
|
926
|
+
trackProviderError(cooldownKey, lastError.status, `HTTP ${lastError.status}`);
|
|
854
927
|
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
855
928
|
if (attempt < MAX_RETRIES) {
|
|
856
929
|
console.log(`${colors.yellow}⚠ [API] Error ${lastError.status}. Retrying in ${(delayMs / 1000).toFixed(1)}s (${attempt + 1}/${MAX_RETRIES})${colors.reset}`);
|
|
@@ -859,7 +932,7 @@ export class AgentClient {
|
|
|
859
932
|
}
|
|
860
933
|
}
|
|
861
934
|
if (isNetworkError && attempt < MAX_RETRIES) {
|
|
862
|
-
trackProviderError(
|
|
935
|
+
trackProviderError(cooldownKey, 0, lastError.message.slice(0, 200));
|
|
863
936
|
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
864
937
|
console.log(`${colors.yellow}⚠ [Network] ${lastError.message.slice(0, 60)}. Retrying in ${(delayMs / 1000).toFixed(1)}s (${attempt + 1}/${MAX_RETRIES})${colors.reset}`);
|
|
865
938
|
await sleep(delayMs);
|
|
@@ -979,8 +1052,8 @@ export class AgentClient {
|
|
|
979
1052
|
}
|
|
980
1053
|
}
|
|
981
1054
|
async getEmbedding(text, model = 'text-embedding-3-small') {
|
|
982
|
-
const
|
|
983
|
-
providerCooldown.assertAvailable(
|
|
1055
|
+
const cooldownKey = this.getCooldownKey(model);
|
|
1056
|
+
providerCooldown.assertAvailable(cooldownKey);
|
|
984
1057
|
const direct = this.resolveDirectConfig(model);
|
|
985
1058
|
let requestUrl = `${this.baseUrl}/embeddings`;
|
|
986
1059
|
let headers = {
|
|
@@ -1008,7 +1081,7 @@ export class AgentClient {
|
|
|
1008
1081
|
body,
|
|
1009
1082
|
});
|
|
1010
1083
|
if (RETRYABLE_STATUS_CODES.has(response.status)) {
|
|
1011
|
-
trackProviderError(
|
|
1084
|
+
trackProviderError(cooldownKey, response.status, `HTTP ${response.status}`);
|
|
1012
1085
|
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
1013
1086
|
if (attempt < MAX_RETRIES) {
|
|
1014
1087
|
if (this.verbose) {
|
|
@@ -1024,7 +1097,7 @@ export class AgentClient {
|
|
|
1024
1097
|
}
|
|
1025
1098
|
const data = await response.json();
|
|
1026
1099
|
if (data.data && data.data[0] && data.data[0].embedding) {
|
|
1027
|
-
providerCooldown.recordSuccess(
|
|
1100
|
+
providerCooldown.recordSuccess(cooldownKey);
|
|
1028
1101
|
return data.data[0].embedding;
|
|
1029
1102
|
}
|
|
1030
1103
|
throw new Error('Malformed embedding response structure');
|
|
@@ -1037,7 +1110,7 @@ export class AgentClient {
|
|
|
1037
1110
|
error.message.includes('fetch failed') ||
|
|
1038
1111
|
error.message.includes('ETIMEDOUT'));
|
|
1039
1112
|
if (isNetworkError) {
|
|
1040
|
-
trackProviderError(
|
|
1113
|
+
trackProviderError(cooldownKey, 0, error.message.slice(0, 200));
|
|
1041
1114
|
}
|
|
1042
1115
|
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
1043
1116
|
await sleep(delayMs);
|
|
@@ -1153,6 +1226,11 @@ function messagesForOpenAIWire(messages) {
|
|
|
1153
1226
|
}
|
|
1154
1227
|
function translateOpenAIToAnthropic(messages, model, options) {
|
|
1155
1228
|
let system = '';
|
|
1229
|
+
// Phase 4.6 — `any[]` paydown. The shape here is the Anthropic
|
|
1230
|
+
// wire-format messages array. The narrower type doesn't capture
|
|
1231
|
+
// every field the SDK accepts (tool_result, document blocks),
|
|
1232
|
+
// but `unknown[]` lets us keep type-safety at this construction
|
|
1233
|
+
// site without inventing a half-typed interface that drifts.
|
|
1156
1234
|
const anthropicMessages = [];
|
|
1157
1235
|
for (const msg of messages) {
|
|
1158
1236
|
if (msg.role === 'system') {
|
|
@@ -1173,6 +1251,7 @@ function translateOpenAIToAnthropic(messages, model, options) {
|
|
|
1173
1251
|
else if (msg.role === 'assistant') {
|
|
1174
1252
|
const assistantText = extractTextFromContent(msg.content);
|
|
1175
1253
|
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
1254
|
+
// Phase 4.6 — see `anthropicMessages` comment for rationale.
|
|
1176
1255
|
const contentBlocks = [];
|
|
1177
1256
|
if (assistantText.length > 0) {
|
|
1178
1257
|
contentBlocks.push({ type: 'text', text: assistantText });
|
|
@@ -1279,7 +1358,7 @@ function translateAnthropicToOpenAI(anthropicRes) {
|
|
|
1279
1358
|
role: 'assistant',
|
|
1280
1359
|
content: text || null,
|
|
1281
1360
|
},
|
|
1282
|
-
finish_reason: finishReasonMap[anthropicRes.stop_reason] || 'stop',
|
|
1361
|
+
finish_reason: finishReasonMap[anthropicRes.stop_reason ?? ''] || 'stop',
|
|
1283
1362
|
};
|
|
1284
1363
|
if (toolCalls.length > 0) {
|
|
1285
1364
|
choice.message.tool_calls = toolCalls;
|