Compare commits

..

11 Commits

Author SHA1 Message Date
github-actions[bot]
bfafc291c9 chore(release): Update version to v1.4.396 2026-01-30 06:10:15 +00:00
Kayvan Sylvan
6ad895ba12 Merge pull request #1978 from ksylvan/kayvan/no-anthropic-oauth
chore: remove OAuth support from Anthropic client
2026-01-29 22:07:53 -08:00
Kayvan Sylvan
55c6092899 chore: incoming 1978 changelog entry 2026-01-29 20:00:17 -08:00
Kayvan Sylvan
9f752f45af chore: remove OAuth support from Anthropic client
- Remove OAuth support from Anthropic client
- Delete `oauth.go` and related test files
- Simplify `IsConfigured` to check only API key
- Update configuration handling to remove OAuth references
- Clean up imports and unused variables in `anthropic.go`
- Adjust `GetConfig` and `UpdateConfig` methods in server configuration
- Remove OAuth-related environment variables from configuration
2026-01-29 19:49:50 -08:00
Kayvan Sylvan
25738f0de4 docs: fix ChangeLog snippet for PR 1975 2026-01-27 23:31:59 -08:00
Kayvan Sylvan
5330d9c173 Merge pull request #1975 from koriyoshi2041/add-suggest-clawdbot-command-pattern
feat: add suggest_moltbot_command pattern for Moltbot (formerly Clawdbot) CLI
2026-01-27 23:23:03 -08:00
Kayvan Sylvan
99b426a2af chore: incoming 1975 changelog entry 2026-01-27 23:14:10 -08:00
Kayvan Sylvan
7a5bf27bd2 refactor: rename clawdbot pattern to moltbot across the codebase
## CHANGES

- Rename `suggest_clawdbot_command` pattern to `suggest_moltbot_command`
- Replace all `clawdbot` CLI references with `moltbot` in pattern docs
- Update command examples to use new `moltbot` binary name
- Add new dictionary words for VSCode spellcheck (Moltbot, schtasks, etc.)
- Fix markdown formatting with proper table alignment
2026-01-27 23:13:33 -08:00
kigland
c0d92ff1d7 fix: resolve multi-command output format inconsistency
Address Copilot review: clarify that multiple commands should be
combined on a single line with && to preserve head -1 behavior,
with follow-up steps described in the explanation section.
2026-01-28 02:24:06 +08:00
kigland
906c648d9f chore: incoming 1975 changelog entry 2026-01-28 02:16:43 +08:00
kigland
be7af5191e feat: add suggest_clawdbot_command pattern
Add a new pattern for suggesting Clawdbot CLI commands based on
natural language intent. Clawdbot is an open-source AI agent framework
(github.com/clawdbot/clawdbot) that connects LLMs to messaging
platforms, devices, and developer tools.

This follows the same structure as suggest_gt_command: command
reference tables, intent mapping, and a pipe-friendly output format
where the first line is the executable command.

All commands verified against clawdbot help and subcommand --help
output (v2026.1.23).
2026-01-28 01:48:35 +08:00
11 changed files with 450 additions and 865 deletions

View File

@@ -30,6 +30,7 @@
"creatordate",
"curcontext",
"custompatterns",
"Daemonized",
"danielmiessler",
"davidanson",
"Debugf",
@@ -128,8 +129,11 @@
"Miessler",
"modeline",
"modelines",
"Moltbot",
"mpga",
"mvdan",
"mychat",
"mygroup",
"nicksnyder",
"nixpkgs",
"nometa",
@@ -167,6 +171,7 @@
"Sadaltager",
"samber",
"sashabaranov",
"schtasks",
"sdist",
"seaborn",
"semgrep",

View File

@@ -1,5 +1,26 @@
# Changelog
## v1.4.396 (2026-01-30)
### PR [#1975](https://github.com/danielmiessler/Fabric/pull/1975) by [koriyoshi2041](https://github.com/koriyoshi2041): feat: add suggest_moltbot_command pattern for Moltbot (formerly Clawdbot) CLI
- Added new pattern for suggesting Moltbot CLI commands based on natural language intent
- Fixed multi-command output format inconsistency to preserve pipe-friendly behavior
- Updated all CLI references and command examples to use new `moltbot` binary name
- Added new dictionary words for VSCode spellcheck and fixed markdown table formatting
### PR [#1978](https://github.com/danielmiessler/Fabric/pull/1978) by [ksylvan](https://github.com/ksylvan): chore: remove OAuth support from Anthropic client
- Remove OAuth support from Anthropic client and delete related OAuth files
- Simplify configuration handling to check only API key instead of OAuth credentials
- Clean up imports and unused variables in anthropic.go
- Update server configuration methods to remove OAuth references
- Remove OAuth-related environment variables from configuration
### Direct commits
- Docs: fix ChangeLog snippet for PR 1975
## v1.4.395 (2026-01-25)
### PR [#1972](https://github.com/danielmiessler/Fabric/pull/1972) by [ksylvan](https://github.com/ksylvan): More node package updates: remove cn, fix string and request vulnerabilities

View File

@@ -114,7 +114,6 @@ Below are the **new features and capabilities** we've added (newest first):
- [v1.4.246](https://github.com/danielmiessler/fabric/releases/tag/v1.4.246) (Jul 14, 2025) — **Automatic ChangeLog Updates**: Add AI-powered changelog generation with high-performance Go tool and comprehensive caching
- [v1.4.245](https://github.com/danielmiessler/fabric/releases/tag/v1.4.245) (Jul 11, 2025) — **Together AI**: Together AI Support with OpenAI Fallback Mechanism Added
- [v1.4.232](https://github.com/danielmiessler/fabric/releases/tag/v1.4.232) (Jul 6, 2025) — **Add Custom**: Add Custom Patterns Directory Support
- [v1.4.231](https://github.com/danielmiessler/fabric/releases/tag/v1.4.231) (Jul 5, 2025) — **OAuth Auto-Auth**: OAuth Authentication Support for Anthropic (Use your Max Subscription)
- [v1.4.230](https://github.com/danielmiessler/fabric/releases/tag/v1.4.230) (Jul 5, 2025) — **Model Management**: Add advanced image generation parameters for OpenAI models with four new CLI flags
- [v1.4.227](https://github.com/danielmiessler/fabric/releases/tag/v1.4.227) (Jul 4, 2025) — **Add Image**: Add Image Generation Support to Fabric
- [v1.4.226](https://github.com/danielmiessler/fabric/releases/tag/v1.4.226) (Jul 4, 2025) — **Web Search**: OpenAI Plugin Now Supports Web Search Functionality

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.395"
var version = "v1.4.396"

Binary file not shown.

View File

@@ -0,0 +1,387 @@
# IDENTITY
You are an expert Moltbot assistant who knows every Moltbot command intimately. Moltbot is an open-source AI agent framework that connects LLMs to messaging platforms (WhatsApp, Telegram, Discord, Slack, Signal, iMessage), devices (phones, browsers, IoT), and developer tools (cron, webhooks, skills, sandboxes). Your role is to understand what the user wants to accomplish and suggest the exact Moltbot CLI command(s) to achieve it.
You think like a patient mentor who:
1. Understands the user's intent, even when poorly expressed
2. Suggests the most direct command for the task
3. Provides context that prevents mistakes
4. Offers alternatives when multiple approaches exist
# CLAWDBOT COMMAND REFERENCE
## Setup and Configuration
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot setup` | Initialize config and workspace | First-time setup |
| `moltbot onboard` | Interactive setup wizard | Gateway, workspace, skills |
| `moltbot configure` | Interactive config wizard | Credentials, devices, defaults |
| `moltbot config get <path>` | Read a config value | `moltbot config get models.default` |
| `moltbot config set <path> <value>` | Set a config value | `moltbot config set models.default "claude-sonnet-4-20250514"` |
| `moltbot config unset <path>` | Remove a config value | Clean up old settings |
| `moltbot doctor` | Health checks and quick fixes | Diagnose problems |
| `moltbot reset` | Reset local config and state | Start fresh (keeps CLI) |
| `moltbot uninstall` | Remove gateway and local data | Full cleanup |
| `moltbot update` | Update CLI | Get latest version |
## Gateway (Core Daemon)
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot gateway` | Run the gateway (foreground) | `moltbot gateway --port 18789` |
| `moltbot gateway start` | Start as background service | Daemonized (launchd/systemd) |
| `moltbot gateway stop` | Stop the service | Graceful shutdown |
| `moltbot gateway restart` | Restart the service | Apply config changes |
| `moltbot gateway status` | Check gateway health | Quick health check |
| `moltbot gateway run` | Run in foreground | Explicit foreground mode |
| `moltbot gateway install` | Install as system service | launchd/systemd/schtasks |
| `moltbot gateway uninstall` | Remove system service | Clean up |
| `moltbot gateway probe` | Full reachability summary | Local and remote health |
| `moltbot gateway discover` | Discover gateways via Bonjour | Find gateways on network |
| `moltbot gateway usage-cost` | Usage cost summary | Token spend from session logs |
| `moltbot --dev gateway` | Dev gateway (isolated state) | Port 19001, separate config |
## Messaging
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot message send` | Send a message | `--target "+1555..." --message "Hi"` |
| `moltbot message send --channel telegram` | Send via specific channel | `--target @mychat --message "Hello"` |
| `moltbot message broadcast` | Broadcast to multiple targets | Multi-recipient |
| `moltbot message poll` | Send a poll | `--poll-question "Q?" --poll-option A --poll-option B` |
| `moltbot message react` | Add or remove a reaction | `--emoji "check"` |
| `moltbot message read` | Read recent messages | Fetch conversation history |
| `moltbot message edit` | Edit a message | Modify sent message |
| `moltbot message delete` | Delete a message | Remove message |
| `moltbot message pin` | Pin a message | Pin to channel |
| `moltbot message unpin` | Unpin a message | Remove pin |
| `moltbot message search` | Search messages | Discord message search |
## Channel Management
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot channels list` | Show configured channels | See all channel accounts |
| `moltbot channels status` | Check channel health | Connection status |
| `moltbot channels login` | Link a channel account | WhatsApp QR, Telegram bot token |
| `moltbot channels logout` | Unlink a channel | Remove session |
| `moltbot channels add` | Add new channel | Add or update account |
| `moltbot channels remove` | Remove a channel | Delete config |
| `moltbot channels logs` | Channel-specific logs | Debug channel issues |
| `moltbot channels capabilities` | Show provider capabilities | Intents, scopes, features |
## Agent and Sessions
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot agent` | Run an agent turn | `--to "+1555..." --message "Run summary" --deliver` |
| `moltbot agents list` | List isolated agents | Multi-agent setups |
| `moltbot agents add` | Create a new agent | Separate workspace and auth |
| `moltbot agents delete` | Remove an agent | Clean up |
| `moltbot sessions` | List conversation sessions | See active and recent chats |
## Models
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot models list` | Show available models | All configured providers |
| `moltbot models status` | Current model config | Default and image models |
| `moltbot models set <model>` | Set default model | `moltbot models set claude-sonnet-4-20250514` |
| `moltbot models set-image <model>` | Set image model | Vision model config |
| `moltbot models aliases list` | Show model aliases | Shorthand names |
| `moltbot models aliases add` | Add an alias | Custom model names |
| `moltbot models fallbacks list` | Show fallback chain | Backup models |
| `moltbot models fallbacks add` | Add fallback model | Redundancy |
| `moltbot models image-fallbacks list` | Show image fallback chain | Image model backups |
| `moltbot models scan` | Scan for available models | Discover provider models |
| `moltbot models auth add` | Add provider credentials | API keys |
## Scheduling (Cron)
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot cron status` | Show cron scheduler status | Is it running? |
| `moltbot cron list` | List all cron jobs | See scheduled tasks |
| `moltbot cron add` | Create a new job | Scheduled task |
| `moltbot cron edit` | Modify a job | Change schedule or text |
| `moltbot cron rm` | Remove a job | Delete task |
| `moltbot cron enable` | Enable a job | Turn on |
| `moltbot cron disable` | Disable a job | Turn off without deleting |
| `moltbot cron run` | Trigger a job now | Manual execution |
| `moltbot cron runs` | Show recent executions | Job history |
## Nodes (Remote Paired Devices)
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot nodes status` | List known nodes | Connection status and capabilities |
| `moltbot nodes describe` | Describe a node | Capabilities and supported commands |
| `moltbot nodes list` | List pending and paired nodes | All node states |
| `moltbot nodes pending` | List pending pairing requests | Awaiting approval |
| `moltbot nodes approve` | Approve a pairing request | Accept device |
| `moltbot nodes reject` | Reject a pairing request | Deny device |
| `moltbot nodes invoke` | Invoke a command on a node | Remote execution |
| `moltbot nodes run` | Run shell command on a node | Remote shell (mac only) |
| `moltbot nodes notify` | Send notification on a node | Push notification (mac only) |
| `moltbot nodes camera` | Capture camera media | Photo or video from device |
| `moltbot nodes screen` | Capture screen recording | Screen from device |
| `moltbot nodes location` | Fetch device location | GPS coordinates |
## Node Host (Local Service)
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot node run` | Run headless node host | Foreground mode |
| `moltbot node status` | Node host status | Local service health |
| `moltbot node install` | Install node host service | launchd/systemd/schtasks |
| `moltbot node uninstall` | Uninstall node host service | Clean up |
| `moltbot node stop` | Stop node host service | Shut down |
| `moltbot node restart` | Restart node host service | Restart |
## Devices and Pairing
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot devices` | Device pairing and tokens | Manage device auth |
| `moltbot pairing list` | List pairing entries | Paired and pending |
| `moltbot pairing approve` | Approve pairing | Accept device |
## Skills and Plugins
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot skills list` | Show installed skills | Available capabilities |
| `moltbot skills info <name>` | Skill details | What it does |
| `moltbot skills check` | Verify skill health | Missing deps |
| `moltbot plugins list` | Show installed plugins | Extensions |
| `moltbot plugins info <name>` | Plugin details | Configuration |
| `moltbot plugins install <name>` | Install a plugin | Add extension |
| `moltbot plugins enable <name>` | Enable a plugin | Turn on |
| `moltbot plugins disable <name>` | Disable a plugin | Turn off |
| `moltbot plugins doctor` | Plugin health check | Load errors |
## Browser Automation
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot browser status` | Browser status | Is it running? |
| `moltbot browser start` | Start managed browser | Launch Chrome/Chromium |
| `moltbot browser stop` | Stop browser | Shut down |
| `moltbot browser tabs` | List open tabs | See what is open |
| `moltbot browser open <url>` | Open a URL | New tab |
| `moltbot browser focus <id>` | Focus a tab | By target id |
| `moltbot browser close <id>` | Close a tab | By target id |
| `moltbot browser screenshot` | Capture screenshot | `--full-page` for entire page |
| `moltbot browser snapshot` | Accessibility snapshot | `--format aria` for tree |
| `moltbot browser navigate <url>` | Navigate to URL | Change page |
| `moltbot browser click <ref>` | Click element | `--double` for double-click |
| `moltbot browser type <ref> <text>` | Type into element | `--submit` to submit form |
| `moltbot browser press <key>` | Press a key | Keyboard input |
| `moltbot browser hover <ref>` | Hover element | Mouse hover |
| `moltbot browser fill` | Fill a form | `--fields '[{"ref":"1","value":"Ada"}]'` |
| `moltbot browser pdf` | Save page as PDF | Export page |
| `moltbot browser evaluate` | Run JavaScript | `--fn '(el) => el.textContent'` |
| `moltbot browser upload <path>` | Upload a file | Next file chooser |
| `moltbot browser dialog` | Handle modal dialog | `--accept` or `--dismiss` |
## System and Diagnostics
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot status` | Channel health and sessions | Quick overview |
| `moltbot health` | Gateway health check | Detailed health |
| `moltbot logs` | Gateway logs | Debug issues |
| `moltbot system event` | Enqueue system event | Custom events |
| `moltbot system heartbeat last` | Last heartbeat | Agent activity |
| `moltbot system heartbeat enable` | Enable heartbeat | Periodic agent check-ins |
| `moltbot system heartbeat disable` | Disable heartbeat | Stop check-ins |
| `moltbot system presence` | Presence info | Online and offline |
| `moltbot security audit` | Security audit | `--deep` for live probe, `--fix` to tighten |
## Other Commands
| Command | Purpose | Common Usage |
| --------- | --------- | -------------- |
| `moltbot sandbox list` | List sandboxes | Docker-based isolation |
| `moltbot sandbox recreate` | Reset sandbox | Fresh containers |
| `moltbot sandbox explain` | Explain sandbox policy | Effective config |
| `moltbot tui` | Terminal UI | Interactive interface |
| `moltbot hooks list` | List hooks | Configured hooks |
| `moltbot hooks enable` | Enable a hook | Turn on |
| `moltbot hooks disable` | Disable a hook | Turn off |
| `moltbot webhooks` | Webhook helpers | Inbound webhooks |
| `moltbot dns setup` | DNS helpers | Custom domain |
| `moltbot approvals get` | Check exec approval policy | Security settings |
| `moltbot approvals set` | Set approval policy | Restrict exec |
| `moltbot acp` | Agent Control Protocol | ACP tools |
| `moltbot dashboard` | Open Control UI | Web interface |
| `moltbot memory search <query>` | Semantic memory search | Search agent memory |
| `moltbot memory index` | Reindex memory | Refresh vector index |
| `moltbot memory status` | Memory index stats | Index health |
| `moltbot directory self` | Show current account | Who am I on this channel |
| `moltbot directory peers` | Peer directory | Contacts and users |
| `moltbot directory groups` | Group directory | Available groups |
| `moltbot docs` | Documentation helpers | Open docs |
# INTENT MAPPING
| User Intent | Best Command | Notes |
| ------------- | -------------- | ------- |
| "set up moltbot" / "first time" | `moltbot onboard` | Interactive wizard |
| "check if everything works" / "health" | `moltbot doctor` | Comprehensive checks |
| "quick status" / "what's running" | `moltbot status` | Overview |
| "start the server" / "run moltbot" | `moltbot gateway start` | Background service |
| "stop moltbot" / "shut down" | `moltbot gateway stop` | Graceful stop |
| "restart" / "apply changes" | `moltbot gateway restart` | After config changes |
| "send a message" / "text someone" | `moltbot message send --target <t> --message <m>` | Specify channel if needed |
| "send to multiple people" / "broadcast" | `moltbot message broadcast` | Multi-target |
| "create a poll" | `moltbot message poll` | Polls on supported channels |
| "connect WhatsApp" / "link WhatsApp" | `moltbot channels login` | Shows QR code |
| "connect Telegram" / "add Telegram" | `moltbot channels add` | Bot token setup |
| "connect Discord" / "add Discord" | `moltbot channels add` | Bot token setup |
| "what channels do I have" | `moltbot channels list` | All accounts |
| "is WhatsApp connected" / "channel health" | `moltbot channels status` | Connection check |
| "change the model" / "switch to GPT" | `moltbot models set <model>` | Model name |
| "what model am I using" | `moltbot models status` | Current config |
| "what models are available" | `moltbot models list` | All providers |
| "add API key" / "set up OpenAI" | `moltbot models auth add` | Provider credentials |
| "schedule a job" / "run every day" | `moltbot cron add` | Create cron job |
| "list scheduled jobs" / "what's scheduled" | `moltbot cron list` | All jobs |
| "run a job now" / "trigger job" | `moltbot cron run` | Manual trigger |
| "pair a phone" / "connect my phone" | `moltbot devices` | Device pairing |
| "run command on phone" / "remote exec" | `moltbot nodes run` | Remote shell on node |
| "take a photo" / "camera" | `moltbot nodes camera` | Capture from paired device |
| "where is my phone" / "location" | `moltbot nodes location` | GPS from paired device |
| "what skills are installed" | `moltbot skills list` | Available skills |
| "install a plugin" | `moltbot plugins install <name>` | Add extension |
| "open a website" / "browse" | `moltbot browser open <url>` | Browser automation |
| "take a screenshot" | `moltbot browser screenshot` | Current page |
| "fill out a form" | `moltbot browser fill` | Automated form filling |
| "check security" / "audit" | `moltbot security audit` | Security scan |
| "view logs" / "debug" / "what happened" | `moltbot logs` | Gateway logs |
| "update moltbot" / "get latest" | `moltbot update` | CLI update |
| "search memory" / "find in memory" | `moltbot memory search "query"` | Semantic search |
| "open the dashboard" / "web UI" | `moltbot dashboard` | Control panel |
| "dev mode" / "testing" | `moltbot --dev gateway` | Isolated dev instance |
| "how much am I spending" / "token cost" | `moltbot gateway usage-cost` | Cost summary |
| "find gateways on network" | `moltbot gateway discover` | Bonjour discovery |
| "full diagnostic" / "probe" | `moltbot gateway probe` | Reachability summary |
| "my contacts" / "who can I message" | `moltbot directory peers` | Contact list |
| "stop burning tokens" | `moltbot gateway stop` | Stop all agent activity |
# STEPS
1. **Parse Intent**: Read the user's request carefully. Identify the core action they want to perform.
2. **Match Category**: Determine which category of Moltbot commands applies:
- Setup and configuration (initial setup, config changes)
- Gateway management (starting, stopping, restarting the daemon)
- Messaging (sending messages, managing channels)
- Agent and sessions (running agents, viewing sessions)
- Models (switching models, adding providers)
- Scheduling (cron jobs, timed tasks)
- Nodes and devices (remote devices, phone pairing, camera, location)
- Skills and plugins (extending capabilities)
- Browser automation (web interaction)
- Diagnostics (health, logs, security)
3. **Select Command**: Choose the most appropriate command based on:
- Directness (simplest path to goal)
- Safety (prefer read-only when uncertain)
- Specificity (exact command for exact need)
4. **Provide Context**: Add helpful notes about:
- What the command will do
- Common gotchas or mistakes
- Alternative approaches if relevant
- Whether a gateway restart is needed
# OUTPUT FORMAT
**CRITICAL**: Your output MUST follow this exact format:
**Line 1**: The exact command to run (nothing else on this line)
**Line 2**: Empty line
**Line 3+**: Brief explanation (2-4 sentences max)
Example output:
```text
moltbot channels login
This launches the channel login flow. For WhatsApp, it will display a QR code to scan with your phone. For Telegram, it will ask for your bot token from BotFather. Run `moltbot channels list` first to see what's already configured.
```
**Why this format**: The first line being command-only allows users to pipe through `head -1` to get just the executable command:
```bash
echo "connect my whatsapp" | fabric -p suggest_moltbot_command | head -1
```
# OUTPUT INSTRUCTIONS
- Output ONLY the command on line 1 (no markdown, no backticks, no explanation)
- Keep explanations concise (2-4 sentences)
- Mention one alternative if highly relevant
- Warn about common mistakes only if critical
- Never output markdown code blocks around the command
- Never prefix the command with "Command:" or similar
- If multiple shell commands are needed, combine them into a single line (e.g., with `&&`) so that `head -1` still returns an executable command; describe any follow-up steps in the explanation
# EXAMPLES
**Input**: "I want to connect my WhatsApp to Moltbot"
**Output**:
```text
moltbot channels login
This starts the channel authentication flow. For WhatsApp, you'll see a QR code -- scan it with WhatsApp on your phone (Settings > Linked Devices > Link a Device). The session persists until you log out or unlink from your phone.
```
**Input**: "check if everything is working"
**Output**:
```text
moltbot doctor
Runs comprehensive health checks on your Moltbot installation -- config, gateway, channels, skills, and common issues. It can also auto-fix some problems. For a quick channel-only check, use `moltbot status` instead.
```
**Input**: "I want to use Claude Opus instead of Sonnet"
**Output**:
```text
moltbot models set claude-opus-4-20250514
This sets your default model to Claude Opus. The change takes effect on the next agent turn. Run `moltbot models list` to see all available models, or `moltbot models status` to confirm the current setting.
```
**Input**: "send a telegram message to my group"
**Output**:
```text
moltbot message send --channel telegram --target @mygroup --message "Hello from Moltbot"
Replace @mygroup with your actual group username or chat ID. The Telegram bot must already be added to the group. Use `moltbot channels list` to verify your Telegram bot is configured.
```
**Input**: "schedule a daily reminder at 9am"
**Output**:
```text
moltbot cron add
This opens the interactive cron job creator. You'll set a schedule (e.g., 0 9 * * * for 9 AM daily), the message text, and the delivery target. Use `moltbot cron list` to verify it was created.
```
**Input**: "stop burning tokens"
**Output**:
```text
moltbot gateway stop
This stops the gateway daemon, which handles all agent sessions and heartbeats. No more API calls will be made. To disable just the heartbeat (but keep the gateway running), use `moltbot system heartbeat disable` instead.
```

View File

@@ -3,7 +3,6 @@ package anthropic
import (
"context"
"fmt"
"net/http"
neturl "net/url"
"os"
"path"
@@ -16,7 +15,6 @@ import (
"github.com/danielmiessler/fabric/internal/domain"
debuglog "github.com/danielmiessler/fabric/internal/log"
"github.com/danielmiessler/fabric/internal/plugins"
"github.com/danielmiessler/fabric/internal/util"
)
const defaultBaseUrl = "https://api.anthropic.com/"
@@ -25,8 +23,6 @@ const webSearchToolName = "web_search"
const webSearchToolType = "web_search_20250305"
const sourcesHeader = "## Sources"
const authTokenIdentifier = "claude"
func NewClient() (ret *Client) {
vendorName := "Anthropic"
ret = &Client{}
@@ -35,7 +31,6 @@ func NewClient() (ret *Client) {
ret.ApiBaseURL = ret.AddSetupQuestion("API Base URL", false)
ret.ApiBaseURL.Value = defaultBaseUrl
ret.UseOAuth = ret.AddSetupQuestionBool("Use OAuth login", false)
ret.ApiKey = ret.PluginBase.AddSetupQuestion("API key", false)
ret.maxTokens = 4096
@@ -64,35 +59,13 @@ func NewClient() (ret *Client) {
return
}
// IsConfigured returns true if either the API key or OAuth is configured
// IsConfigured returns true if the API key is configured
func (an *Client) IsConfigured() bool {
// Check if API key is configured
if an.ApiKey.Value != "" {
return true
}
// Check if OAuth is enabled and has a valid token
if plugins.ParseBoolElseFalse(an.UseOAuth.Value) {
storage, err := util.NewOAuthStorage()
if err != nil {
return false
}
// If no valid token exists, automatically run OAuth flow
if !storage.HasValidToken(authTokenIdentifier, 5) {
fmt.Println("OAuth enabled but no valid token found. Starting authentication...")
_, err := RunOAuthFlow(authTokenIdentifier)
if err != nil {
fmt.Printf("OAuth authentication failed: %v\n", err)
return false
}
// After successful OAuth flow, check again
return storage.HasValidToken(authTokenIdentifier, 5)
}
return true
}
return false
}
@@ -100,7 +73,6 @@ type Client struct {
*plugins.PluginBase
ApiBaseURL *plugins.SetupQuestion
ApiKey *plugins.SetupQuestion
UseOAuth *plugins.SetupQuestion
maxTokens int
defaultRequiredUserMessage string
@@ -115,21 +87,6 @@ func (an *Client) Setup() (err error) {
return
}
if plugins.ParseBoolElseFalse(an.UseOAuth.Value) {
// Check if we have a valid stored token
storage, err := util.NewOAuthStorage()
if err != nil {
return err
}
if !storage.HasValidToken(authTokenIdentifier, 5) {
// No valid token, run OAuth flow
if _, err = RunOAuthFlow(authTokenIdentifier); err != nil {
return err
}
}
}
err = an.configure()
return
}
@@ -141,17 +98,7 @@ func (an *Client) configure() (err error) {
opts = append(opts, option.WithBaseURL(an.ApiBaseURL.Value))
}
if plugins.ParseBoolElseFalse(an.UseOAuth.Value) {
// For OAuth, use Bearer token with custom headers
// Create custom HTTP client that adds OAuth Bearer token and beta header
baseTransport := &http.Transport{}
httpClient := &http.Client{
Transport: NewOAuthTransport(an, baseTransport),
}
opts = append(opts, option.WithHTTPClient(httpClient))
} else {
opts = append(opts, option.WithAPIKey(an.ApiKey.Value))
}
opts = append(opts, option.WithAPIKey(an.ApiKey.Value))
an.client = anthropic.NewClient(opts...)
return
@@ -264,17 +211,6 @@ func (an *Client) buildMessageParams(msgs []anthropic.MessageParam, opts *domain
params.Temperature = anthropic.Opt(opts.Temperature)
}
// Add Claude Code spoofing system message for OAuth authentication
if plugins.ParseBoolElseFalse(an.UseOAuth.Value) {
params.System = []anthropic.TextBlockParam{
{
Type: "text",
Text: "You are Claude Code, Anthropic's official CLI for Claude.",
},
}
}
if opts.Search {
// Build the web-search tool definition:
webTool := anthropic.WebSearchTool20250305Param{

View File

@@ -1,327 +0,0 @@
package anthropic
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os/exec"
"strings"
"time"
debuglog "github.com/danielmiessler/fabric/internal/log"
"github.com/danielmiessler/fabric/internal/util"
"golang.org/x/oauth2"
)
// OAuth configuration constants
const (
oauthClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
oauthAuthURL = "https://claude.ai/oauth/authorize"
oauthTokenURL = "https://console.anthropic.com/v1/oauth/token"
oauthRedirectURL = "https://console.anthropic.com/oauth/code/callback"
)
// OAuthTransport is a custom HTTP transport that adds OAuth Bearer token and beta header
type OAuthTransport struct {
client *Client
base http.RoundTripper
}
// RoundTrip implements the http.RoundTripper interface
func (t *OAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Clone the request to avoid modifying the original
newReq := req.Clone(req.Context())
// Get current token (may refresh if needed)
token, err := t.getValidToken(authTokenIdentifier)
if err != nil {
return nil, fmt.Errorf("failed to get valid OAuth token: %w", err)
}
// Add OAuth Bearer token
newReq.Header.Set("Authorization", "Bearer "+token)
// Add the anthropic-beta header for OAuth, preserving existing betas
existing := newReq.Header.Get("anthropic-beta")
beta := "oauth-2025-04-20"
if existing != "" {
beta = existing + "," + beta
}
newReq.Header.Set("anthropic-beta", beta)
// Set User-Agent to match AI SDK exactly
newReq.Header.Set("User-Agent", "ai-sdk/anthropic")
// Remove x-api-key header if present (OAuth doesn't use it)
newReq.Header.Del("x-api-key")
return t.base.RoundTrip(newReq)
}
// getValidToken returns a valid access token, refreshing if necessary
func (t *OAuthTransport) getValidToken(tokenIdentifier string) (string, error) {
storage, err := util.NewOAuthStorage()
if err != nil {
return "", fmt.Errorf("failed to create OAuth storage: %w", err)
}
// Load stored token
token, err := storage.LoadToken(tokenIdentifier)
if err != nil {
return "", fmt.Errorf("failed to load stored token: %w", err)
}
// If no token exists, run OAuth flow
if token == nil {
debuglog.Log("No OAuth token found, initiating authentication...\n")
newAccessToken, err := RunOAuthFlow(tokenIdentifier)
if err != nil {
return "", fmt.Errorf("failed to authenticate: %w", err)
}
return newAccessToken, nil
}
// Check if token needs refresh (5 minute buffer)
if token.IsExpired(5) {
debuglog.Log("OAuth token expired, refreshing...\n")
newAccessToken, err := RefreshToken(tokenIdentifier)
if err != nil {
// If refresh fails, try re-authentication
debuglog.Log("Token refresh failed, re-authenticating...\n")
newAccessToken, err = RunOAuthFlow(tokenIdentifier)
if err != nil {
return "", fmt.Errorf("failed to refresh or re-authenticate: %w", err)
}
}
return newAccessToken, nil
}
return token.AccessToken, nil
}
// NewOAuthTransport creates a new OAuth transport for the given client
func NewOAuthTransport(client *Client, base http.RoundTripper) *OAuthTransport {
return &OAuthTransport{
client: client,
base: base,
}
}
// generatePKCE generates PKCE code verifier and challenge
func generatePKCE() (verifier, challenge string, err error) {
b := make([]byte, 32)
if _, err = rand.Read(b); err != nil {
return
}
verifier = base64.RawURLEncoding.EncodeToString(b)
sum := sha256.Sum256([]byte(verifier))
challenge = base64.RawURLEncoding.EncodeToString(sum[:])
return
}
// openBrowser attempts to open the given URL in the default browser
func openBrowser(url string) {
commands := [][]string{{"xdg-open", url}, {"open", url}, {"cmd", "/c", "start", url}}
for _, cmd := range commands {
if exec.Command(cmd[0], cmd[1:]...).Start() == nil {
return
}
}
}
// RunOAuthFlow executes the complete OAuth authorization flow
func RunOAuthFlow(tokenIdentifier string) (token string, err error) {
// First check if we have an existing token that can be refreshed
storage, err := util.NewOAuthStorage()
if err == nil {
existingToken, err := storage.LoadToken(tokenIdentifier)
if err == nil && existingToken != nil {
// If token exists but is expired, try refreshing first
if existingToken.IsExpired(5) {
debuglog.Log("Found expired OAuth token, attempting refresh...\n")
refreshedToken, refreshErr := RefreshToken(tokenIdentifier)
if refreshErr == nil {
debuglog.Log("Token refresh successful\n")
return refreshedToken, nil
}
debuglog.Log("Token refresh failed (%v), proceeding with full OAuth flow...\n", refreshErr)
} else {
// Token exists and is still valid
return existingToken.AccessToken, nil
}
}
}
verifier, challenge, err := generatePKCE()
if err != nil {
return
}
cfg := oauth2.Config{
ClientID: oauthClientID,
Endpoint: oauth2.Endpoint{AuthURL: oauthAuthURL, TokenURL: oauthTokenURL},
RedirectURL: oauthRedirectURL,
Scopes: []string{"org:create_api_key", "user:profile", "user:inference"},
}
authURL := cfg.AuthCodeURL(verifier,
oauth2.SetAuthURLParam("code_challenge", challenge),
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
oauth2.SetAuthURLParam("code", "true"),
oauth2.SetAuthURLParam("state", verifier),
)
debuglog.Log("Open the following URL in your browser. Fabric would like to authorize:\n")
debuglog.Log("%s\n", authURL)
openBrowser(authURL)
debuglog.Log("Paste the authorization code here: ")
var code string
fmt.Scanln(&code)
parts := strings.SplitN(code, "#", 2)
state := verifier
if len(parts) == 2 {
state = parts[1]
}
// Manual token exchange to match opencode implementation
tokenReq := map[string]string{
"code": parts[0],
"state": state,
"grant_type": "authorization_code",
"client_id": oauthClientID,
"redirect_uri": oauthRedirectURL,
"code_verifier": verifier,
}
token, err = exchangeToken(tokenIdentifier, tokenReq)
return
}
// exchangeToken exchanges authorization code for access token
func exchangeToken(tokenIdentifier string, params map[string]string) (token string, err error) {
reqBody, err := json.Marshal(params)
if err != nil {
return
}
resp, err := http.Post(oauthTokenURL, "application/json", bytes.NewBuffer(reqBody))
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
err = fmt.Errorf("token exchange failed: %s - %s", resp.Status, string(body))
return
}
var result struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
}
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return
}
// Save the complete token information
storage, err := util.NewOAuthStorage()
if err != nil {
return result.AccessToken, fmt.Errorf("failed to create OAuth storage: %w", err)
}
oauthToken := &util.OAuthToken{
AccessToken: result.AccessToken,
RefreshToken: result.RefreshToken,
ExpiresAt: time.Now().Unix() + int64(result.ExpiresIn),
TokenType: result.TokenType,
Scope: result.Scope,
}
if err = storage.SaveToken(tokenIdentifier, oauthToken); err != nil {
return result.AccessToken, fmt.Errorf("failed to save OAuth token: %w", err)
}
token = result.AccessToken
return
}
// RefreshToken refreshes an expired OAuth token using the refresh token
func RefreshToken(tokenIdentifier string) (string, error) {
storage, err := util.NewOAuthStorage()
if err != nil {
return "", fmt.Errorf("failed to create OAuth storage: %w", err)
}
// Load existing token
token, err := storage.LoadToken(tokenIdentifier)
if err != nil {
return "", fmt.Errorf("failed to load stored token: %w", err)
}
if token == nil || token.RefreshToken == "" {
return "", fmt.Errorf("no refresh token available")
}
// Prepare refresh request
refreshReq := map[string]string{
"grant_type": "refresh_token",
"refresh_token": token.RefreshToken,
"client_id": oauthClientID,
}
reqBody, err := json.Marshal(refreshReq)
if err != nil {
return "", fmt.Errorf("failed to marshal refresh request: %w", err)
}
// Make refresh request
resp, err := http.Post(oauthTokenURL, "application/json", bytes.NewBuffer(reqBody))
if err != nil {
return "", fmt.Errorf("refresh request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("token refresh failed: %s - %s", resp.Status, string(body))
}
var result struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
}
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", fmt.Errorf("failed to parse refresh response: %w", err)
}
// Update stored token
newToken := &util.OAuthToken{
AccessToken: result.AccessToken,
RefreshToken: result.RefreshToken,
ExpiresAt: time.Now().Unix() + int64(result.ExpiresIn),
TokenType: result.TokenType,
Scope: result.Scope,
}
// Use existing refresh token if new one not provided
if newToken.RefreshToken == "" {
newToken.RefreshToken = token.RefreshToken
}
if err = storage.SaveToken(tokenIdentifier, newToken); err != nil {
return "", fmt.Errorf("failed to save refreshed token: %w", err)
}
return result.AccessToken, nil
}

View File

@@ -1,433 +0,0 @@
package anthropic
// OAuth Testing Strategy:
//
// This test suite covers OAuth functionality while avoiding real external calls.
// Key principles:
// 1. Never trigger real OAuth flows that would open browsers or call external APIs
// 2. Use temporary directories and mock tokens for isolated testing
// 3. Skip integration tests that would require real OAuth servers
// 4. Test error paths and edge cases safely
//
// Tests are categorized as:
// - Unit tests: Test individual functions with mocked data (SAFE)
// - Integration tests: Would require real OAuth servers (SKIPPED)
// - Error path tests: Test failure scenarios safely (SAFE)
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/danielmiessler/fabric/internal/util"
)
// createTestToken creates a test OAuth token
func createTestToken(accessToken, refreshToken string, expiresIn int64) *util.OAuthToken {
return &util.OAuthToken{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: time.Now().Unix() + expiresIn,
TokenType: "Bearer",
Scope: "org:create_api_key user:profile user:inference",
}
}
// createExpiredToken creates an expired test token
func createExpiredToken(accessToken, refreshToken string) *util.OAuthToken {
return &util.OAuthToken{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: time.Now().Unix() - 3600, // Expired 1 hour ago
TokenType: "Bearer",
Scope: "org:create_api_key user:profile user:inference",
}
}
// mockTokenServer creates a mock OAuth token server for testing
func mockTokenServer(_ *testing.T, responses map[string]any) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/oauth/token" {
http.NotFound(w, r)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
var req map[string]string
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
grantType := req["grant_type"]
response, exists := responses[grantType]
if !exists {
http.Error(w, "Unsupported grant type", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
if errorResp, ok := response.(map[string]any); ok && errorResp["error"] != nil {
w.WriteHeader(http.StatusBadRequest)
}
json.NewEncoder(w).Encode(response)
}))
}
func TestGeneratePKCE(t *testing.T) {
verifier, challenge, err := generatePKCE()
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if verifier == "" {
t.Error("Expected non-empty verifier")
}
if challenge == "" {
t.Error("Expected non-empty challenge")
}
if len(verifier) < 43 { // Base64 encoded 32 bytes should be at least 43 chars
t.Errorf("Verifier too short: %d chars", len(verifier))
}
if len(challenge) < 43 { // SHA256 hash should be at least 43 chars when base64 encoded
t.Errorf("Challenge too short: %d chars", len(challenge))
}
}
func TestExchangeToken_Success(t *testing.T) {
// Create mock server
server := mockTokenServer(t, map[string]any{
"authorization_code": map[string]any{
"access_token": "test_access_token",
"refresh_token": "test_refresh_token",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "org:create_api_key user:profile user:inference",
},
})
defer server.Close()
// Create a temporary directory for token storage
tempDir := t.TempDir()
// Mock the storage creation to use our temp directory
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
// Set up a fake home directory
fakeHome := filepath.Join(tempDir, "home")
os.MkdirAll(filepath.Join(fakeHome, ".config", "fabric"), 0755)
os.Setenv("HOME", fakeHome)
// This test would need the actual exchangeToken function to be modified to accept a custom URL
// For now, we'll test the logic without the actual HTTP call
t.Skip("Skipping integration test - would need URL injection for proper testing")
}
func TestRefreshToken_Success(t *testing.T) {
// Create temporary directory and set up fake home
tempDir := t.TempDir()
fakeHome := filepath.Join(tempDir, "home")
configDir := filepath.Join(fakeHome, ".config", "fabric")
os.MkdirAll(configDir, 0755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", fakeHome)
// Create an expired token
expiredToken := createExpiredToken("old_access_token", "valid_refresh_token")
// Save the expired token
tokenPath := filepath.Join(configDir, ".test_oauth")
data, _ := json.MarshalIndent(expiredToken, "", " ")
os.WriteFile(tokenPath, data, 0600)
// Create mock server for refresh
server := mockTokenServer(t, map[string]any{
"refresh_token": map[string]any{
"access_token": "new_access_token",
"refresh_token": "new_refresh_token",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "org:create_api_key user:profile user:inference",
},
})
defer server.Close()
// This test would need the RefreshToken function to accept a custom URL
t.Skip("Skipping integration test - would need URL injection for proper testing")
}
func TestRefreshToken_NoRefreshToken(t *testing.T) {
// Create temporary directory and set up fake home
tempDir := t.TempDir()
fakeHome := filepath.Join(tempDir, "home")
configDir := filepath.Join(fakeHome, ".config", "fabric")
os.MkdirAll(configDir, 0755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", fakeHome)
// Create a token without refresh token
tokenWithoutRefresh := &util.OAuthToken{
AccessToken: "access_token",
RefreshToken: "", // No refresh token
ExpiresAt: time.Now().Unix() - 3600,
TokenType: "Bearer",
Scope: "org:create_api_key user:profile user:inference",
}
// Save the token
tokenPath := filepath.Join(configDir, ".test_oauth")
data, _ := json.MarshalIndent(tokenWithoutRefresh, "", " ")
os.WriteFile(tokenPath, data, 0600)
// Test RefreshToken
_, err := RefreshToken("test")
if err == nil {
t.Error("Expected error when no refresh token available")
}
if !strings.Contains(err.Error(), "no refresh token available") {
t.Errorf("Expected 'no refresh token available' error, got: %v", err)
}
}
func TestRefreshToken_NoStoredToken(t *testing.T) {
// Create temporary directory and set up fake home
tempDir := t.TempDir()
fakeHome := filepath.Join(tempDir, "home")
configDir := filepath.Join(fakeHome, ".config", "fabric")
os.MkdirAll(configDir, 0755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", fakeHome)
// Don't create any token file
// Test RefreshToken
_, err := RefreshToken("nonexistent")
if err == nil {
t.Error("Expected error when no token stored")
}
}
func TestOAuthTransport_RoundTrip(t *testing.T) {
// Create a mock client
client := &Client{}
// Create the transport
transport := NewOAuthTransport(client, http.DefaultTransport)
// Create a test request
req := httptest.NewRequest("GET", "https://api.anthropic.com/v1/messages", nil)
req.Header.Set("x-api-key", "should-be-removed")
// Create temporary directory and set up fake home with valid token
tempDir := t.TempDir()
fakeHome := filepath.Join(tempDir, "home")
configDir := filepath.Join(fakeHome, ".config", "fabric")
os.MkdirAll(configDir, 0755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", fakeHome)
// Create a valid token
validToken := createTestToken("valid_access_token", "refresh_token", 3600)
tokenPath := filepath.Join(configDir, fmt.Sprintf(".%s_oauth", authTokenIdentifier))
data, _ := json.MarshalIndent(validToken, "", " ")
os.WriteFile(tokenPath, data, 0600)
// Create a mock server to handle the request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check that OAuth headers are set correctly
auth := r.Header.Get("Authorization")
if auth != "Bearer valid_access_token" {
t.Errorf("Expected 'Bearer valid_access_token', got '%s'", auth)
}
beta := r.Header.Get("anthropic-beta")
if beta != "oauth-2025-04-20" {
t.Errorf("Expected 'oauth-2025-04-20', got '%s'", beta)
}
userAgent := r.Header.Get("User-Agent")
if userAgent != "ai-sdk/anthropic" {
t.Errorf("Expected 'ai-sdk/anthropic', got '%s'", userAgent)
}
// Check that x-api-key header is removed
if r.Header.Get("x-api-key") != "" {
t.Error("Expected x-api-key header to be removed")
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("success"))
}))
defer server.Close()
// Update the request URL to point to our mock server
req.URL.Host = strings.TrimPrefix(server.URL, "http://")
req.URL.Scheme = "http"
// Execute the request
resp, err := transport.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
}
func TestRunOAuthFlow_ExistingValidToken(t *testing.T) {
// Create temporary directory and set up fake home
tempDir := t.TempDir()
fakeHome := filepath.Join(tempDir, "home")
configDir := filepath.Join(fakeHome, ".config", "fabric")
os.MkdirAll(configDir, 0755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", fakeHome)
// Create a valid token
validToken := createTestToken("existing_valid_token", "refresh_token", 3600)
tokenPath := filepath.Join(configDir, ".test_oauth")
data, _ := json.MarshalIndent(validToken, "", " ")
os.WriteFile(tokenPath, data, 0600)
// Test RunOAuthFlow - should return existing token without starting OAuth flow
token, err := RunOAuthFlow("test")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if token != "existing_valid_token" {
t.Errorf("Expected 'existing_valid_token', got '%s'", token)
}
}
// Test helper functions
func TestCreateTestToken(t *testing.T) {
token := createTestToken("access", "refresh", 3600)
if token.AccessToken != "access" {
t.Errorf("Expected access token 'access', got '%s'", token.AccessToken)
}
if token.RefreshToken != "refresh" {
t.Errorf("Expected refresh token 'refresh', got '%s'", token.RefreshToken)
}
if token.IsExpired(5) {
t.Error("Expected token to not be expired")
}
}
func TestCreateExpiredToken(t *testing.T) {
token := createExpiredToken("access", "refresh")
if !token.IsExpired(5) {
t.Error("Expected token to be expired")
}
}
// TestTokenExpirationLogic tests the token expiration detection without OAuth flows
func TestTokenExpirationLogic(t *testing.T) {
// Test valid token
validToken := createTestToken("access", "refresh", 3600)
if validToken.IsExpired(5) {
t.Error("Valid token should not be expired")
}
// Test expired token
expiredToken := createExpiredToken("access", "refresh")
if !expiredToken.IsExpired(5) {
t.Error("Expired token should be expired")
}
// Test token expiring soon (within buffer)
soonExpiredToken := createTestToken("access", "refresh", 240) // 4 minutes
if !soonExpiredToken.IsExpired(5) { // 5 minute buffer
t.Error("Token expiring within buffer should be considered expired")
}
}
// TestGetValidTokenWithValidToken tests the getValidToken method with a valid token
func TestGetValidTokenWithValidToken(t *testing.T) {
// Create temporary directory and set up fake home
tempDir := t.TempDir()
fakeHome := filepath.Join(tempDir, "home")
configDir := filepath.Join(fakeHome, ".config", "fabric")
os.MkdirAll(configDir, 0755)
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", fakeHome)
// Create a valid token
validToken := createTestToken("valid_access_token", "refresh_token", 3600)
tokenPath := filepath.Join(configDir, ".test_oauth")
data, _ := json.MarshalIndent(validToken, "", " ")
os.WriteFile(tokenPath, data, 0600)
// Create transport
client := &Client{}
transport := NewOAuthTransport(client, http.DefaultTransport)
// Test getValidToken - this should return the valid token without any OAuth flow
token, err := transport.getValidToken("test")
if err != nil {
t.Fatalf("Expected no error with valid token, got: %v", err)
}
if token != "valid_access_token" {
t.Errorf("Expected 'valid_access_token', got '%s'", token)
}
}
// Benchmark tests
func BenchmarkGeneratePKCE(b *testing.B) {
for b.Loop() {
_, _, err := generatePKCE()
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkTokenIsExpired(b *testing.B) {
token := createTestToken("access", "refresh", 3600)
for b.Loop() {
token.IsExpired(5)
}
}

View File

@@ -57,18 +57,17 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) {
}
config := map[string]string{
"openai": os.Getenv("OPENAI_API_KEY"),
"anthropic": os.Getenv("ANTHROPIC_API_KEY"),
"anthropic_use_oauth_login": os.Getenv("ANTHROPIC_USE_OAUTH_LOGIN"),
"groq": os.Getenv("GROQ_API_KEY"),
"mistral": os.Getenv("MISTRAL_API_KEY"),
"gemini": os.Getenv("GEMINI_API_KEY"),
"ollama": os.Getenv("OLLAMA_URL"),
"openrouter": os.Getenv("OPENROUTER_API_KEY"),
"silicon": os.Getenv("SILICON_API_KEY"),
"deepseek": os.Getenv("DEEPSEEK_API_KEY"),
"grokai": os.Getenv("GROKAI_API_KEY"),
"lmstudio": os.Getenv("LM_STUDIO_API_BASE_URL"),
"openai": os.Getenv("OPENAI_API_KEY"),
"anthropic": os.Getenv("ANTHROPIC_API_KEY"),
"groq": os.Getenv("GROQ_API_KEY"),
"mistral": os.Getenv("MISTRAL_API_KEY"),
"gemini": os.Getenv("GEMINI_API_KEY"),
"ollama": os.Getenv("OLLAMA_URL"),
"openrouter": os.Getenv("OPENROUTER_API_KEY"),
"silicon": os.Getenv("SILICON_API_KEY"),
"deepseek": os.Getenv("DEEPSEEK_API_KEY"),
"grokai": os.Getenv("GROKAI_API_KEY"),
"lmstudio": os.Getenv("LM_STUDIO_API_BASE_URL"),
}
c.JSON(http.StatusOK, config)
@@ -81,18 +80,17 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
}
var config struct {
OpenAIApiKey string `json:"openai_api_key"`
AnthropicApiKey string `json:"anthropic_api_key"`
AnthropicUseAuthToken string `json:"anthropic_use_auth_token"`
GroqApiKey string `json:"groq_api_key"`
MistralApiKey string `json:"mistral_api_key"`
GeminiApiKey string `json:"gemini_api_key"`
OllamaURL string `json:"ollama_url"`
OpenRouterApiKey string `json:"openrouter_api_key"`
SiliconApiKey string `json:"silicon_api_key"`
DeepSeekApiKey string `json:"deepseek_api_key"`
GrokaiApiKey string `json:"grokai_api_key"`
LMStudioURL string `json:"lm_studio_base_url"`
OpenAIApiKey string `json:"openai_api_key"`
AnthropicApiKey string `json:"anthropic_api_key"`
GroqApiKey string `json:"groq_api_key"`
MistralApiKey string `json:"mistral_api_key"`
GeminiApiKey string `json:"gemini_api_key"`
OllamaURL string `json:"ollama_url"`
OpenRouterApiKey string `json:"openrouter_api_key"`
SiliconApiKey string `json:"silicon_api_key"`
DeepSeekApiKey string `json:"deepseek_api_key"`
GrokaiApiKey string `json:"grokai_api_key"`
LMStudioURL string `json:"lm_studio_base_url"`
}
if err := c.ShouldBindJSON(&config); err != nil {
@@ -101,18 +99,17 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
}
envVars := map[string]string{
"OPENAI_API_KEY": config.OpenAIApiKey,
"ANTHROPIC_API_KEY": config.AnthropicApiKey,
"ANTHROPIC_USE_OAUTH_LOGIN": config.AnthropicUseAuthToken,
"GROQ_API_KEY": config.GroqApiKey,
"MISTRAL_API_KEY": config.MistralApiKey,
"GEMINI_API_KEY": config.GeminiApiKey,
"OLLAMA_URL": config.OllamaURL,
"OPENROUTER_API_KEY": config.OpenRouterApiKey,
"SILICON_API_KEY": config.SiliconApiKey,
"DEEPSEEK_API_KEY": config.DeepSeekApiKey,
"GROKAI_API_KEY": config.GrokaiApiKey,
"LM_STUDIO_API_BASE_URL": config.LMStudioURL,
"OPENAI_API_KEY": config.OpenAIApiKey,
"ANTHROPIC_API_KEY": config.AnthropicApiKey,
"GROQ_API_KEY": config.GroqApiKey,
"MISTRAL_API_KEY": config.MistralApiKey,
"GEMINI_API_KEY": config.GeminiApiKey,
"OLLAMA_URL": config.OllamaURL,
"OPENROUTER_API_KEY": config.OpenRouterApiKey,
"SILICON_API_KEY": config.SiliconApiKey,
"DEEPSEEK_API_KEY": config.DeepSeekApiKey,
"GROKAI_API_KEY": config.GrokaiApiKey,
"LM_STUDIO_API_BASE_URL": config.LMStudioURL,
}
var envContent strings.Builder

View File

@@ -1 +1 @@
"1.4.395"
"1.4.396"