Compare commits

..
252 changed files with 714 additions and 6762 deletions

No files matched your search

BIN
View File
Binary file not shown.
-366
View File
@@ -1,366 +0,0 @@
---
description: Update Clawdbot from upstream when branch has diverged (ahead/behind)
---
# Clawdbot Upstream Sync Workflow
Use this workflow when your fork has diverged from upstream (e.g., "18 commits ahead, 29 commits behind").
## Quick Reference
```bash
# Check divergence status
git fetch upstream && git rev-list --left-right --count main...upstream/main
# Full sync (rebase preferred)
git fetch upstream && git rebase upstream/main && pnpm install && pnpm build && ./scripts/restart-mac.sh
# Check for Swift 6.2 issues after sync
grep -r "FileManager\.default\|Thread\.isMainThread" src/ apps/ --include="*.swift"
```
---
## Step 1: Assess Divergence
```bash
git fetch upstream
git log --oneline --left-right main...upstream/main | head -20
```
This shows:
- `<` = your local commits (ahead)
- `>` = upstream commits you're missing (behind)
**Decision point:**
- Few local commits, many upstream → **Rebase** (cleaner history)
- Many local commits or shared branch → **Merge** (preserves history)
---
## Step 2A: Rebase Strategy (Preferred)
Replays your commits on top of upstream. Results in linear history.
```bash
# Ensure working tree is clean
git status
# Rebase onto upstream
git rebase upstream/main
```
### Handling Rebase Conflicts
```bash
# When conflicts occur:
# 1. Fix conflicts in the listed files
# 2. Stage resolved files
git add <resolved-files>
# 3. Continue rebase
git rebase --continue
# If a commit is no longer needed (already in upstream):
git rebase --skip
# To abort and return to original state:
git rebase --abort
```
### Common Conflict Patterns
| File | Resolution |
|------|------------|
| `package.json` | Take upstream deps, keep local scripts if needed |
| `pnpm-lock.yaml` | Accept upstream, regenerate with `pnpm install` |
| `*.patch` files | Usually take upstream version |
| Source files | Merge logic carefully, prefer upstream structure |
---
## Step 2B: Merge Strategy (Alternative)
Preserves all history with a merge commit.
```bash
git merge upstream/main --no-edit
```
Resolve conflicts same as rebase, then:
```bash
git add <resolved-files>
git commit
```
---
## Step 3: Rebuild Everything
After sync completes:
```bash
# Install dependencies (regenerates lock if needed)
pnpm install
# Build TypeScript
pnpm build
# Build UI assets
pnpm ui:build
# Run diagnostics
pnpm clawdbot doctor
```
---
## Step 4: Rebuild macOS App
```bash
# Full rebuild, sign, and launch
./scripts/restart-mac.sh
# Or just package without restart
pnpm mac:package
```
### Install to /Applications
```bash
# Kill running app
pkill -x "Clawdbot" || true
# Move old version
mv /Applications/Clawdbot.app /tmp/Clawdbot-backup.app
# Install new build
cp -R dist/Clawdbot.app /Applications/
# Launch
open /Applications/Clawdbot.app
```
---
## Step 4A: Verify macOS App & Agent
After rebuilding the macOS app, always verify it works correctly:
```bash
# Check gateway health
pnpm clawdbot health
# Verify no zombie processes
ps aux | grep -E "(clawdbot|gateway)" | grep -v grep
# Test agent functionality by sending a verification message
pnpm clawdbot agent --message "Verification: macOS app rebuild successful - agent is responding." --session-id YOUR_TELEGRAM_SESSION_ID
# Confirm the message was received on Telegram
# (Check your Telegram chat with the bot)
```
**Important:** Always wait for the Telegram verification message before proceeding. If the agent doesn't respond, troubleshoot the gateway or model configuration before pushing.
---
## Step 5: Handle Swift/macOS Build Issues (Common After Upstream Sync)
Upstream updates may introduce Swift 6.2 / macOS 26 SDK incompatibilities. Use analyze-mode for systematic debugging:
### Analyze-Mode Investigation
```bash
# Gather context with parallel agents
morph-mcp_warpgrep_codebase_search search_string="Find deprecated FileManager.default and Thread.isMainThread usages in Swift files" repo_path="/Volumes/Main SSD/Developer/clawdis"
morph-mcp_warpgrep_codebase_search search_string="Locate Peekaboo submodule and macOS app Swift files with concurrency issues" repo_path="/Volumes/Main SSD/Developer/clawdis"
```
### Common Swift 6.2 Fixes
**FileManager.default Deprecation:**
```bash
# Search for deprecated usage
grep -r "FileManager\.default" src/ apps/ --include="*.swift"
# Replace with proper initialization
# OLD: FileManager.default
# NEW: FileManager()
```
**Thread.isMainThread Deprecation:**
```bash
# Search for deprecated usage
grep -r "Thread\.isMainThread" src/ apps/ --include="*.swift"
# Replace with modern concurrency check
# OLD: Thread.isMainThread
# NEW: await MainActor.run { ... } or DispatchQueue.main.sync { ... }
```
### Peekaboo Submodule Fixes
```bash
# Check Peekaboo for concurrency issues
cd src/canvas-host/a2ui
grep -r "Thread\.isMainThread\|FileManager\.default" . --include="*.swift"
# Fix and rebuild submodule
cd /Volumes/Main SSD/Developer/clawdis
pnpm canvas:a2ui:bundle
```
### macOS App Concurrency Fixes
```bash
# Check macOS app for issues
grep -r "Thread\.isMainThread\|FileManager\.default" apps/macos/ --include="*.swift"
# Clean and rebuild after fixes
cd apps/macos && rm -rf .build .swiftpm
./scripts/restart-mac.sh
```
### Model Configuration Updates
If upstream introduced new model configurations:
```bash
# Check for OpenRouter API key requirements
grep -r "openrouter\|OPENROUTER" src/ --include="*.ts" --include="*.js"
# Update clawdbot.json with fallback chains
# Add model fallback configurations as needed
```
---
## Step 6: Verify & Push
```bash
# Verify everything works
pnpm clawdbot health
pnpm test
# Push (force required after rebase)
git push origin main --force-with-lease
# Or regular push after merge
git push origin main
```
---
## Troubleshooting
### Build Fails After Sync
```bash
# Clean and rebuild
rm -rf node_modules dist
pnpm install
pnpm build
```
### Type Errors (Bun/Node Incompatibility)
Common issue: `fetch.preconnect` type mismatch. Fix by using `FetchLike` type instead of `typeof fetch`.
### macOS App Crashes on Launch
Usually resource bundle mismatch. Full rebuild required:
```bash
cd apps/macos && rm -rf .build .swiftpm
./scripts/restart-mac.sh
```
### Patch Failures
```bash
# Check patch status
pnpm install 2>&1 | grep -i patch
# If patches fail, they may need updating for new dep versions
# Check patches/ directory against package.json patchedDependencies
```
### Swift 6.2 / macOS 26 SDK Build Failures
**Symptoms:** Build fails with deprecation warnings about `FileManager.default` or `Thread.isMainThread`
**Search-Mode Investigation:**
```bash
# Exhaustive search for deprecated APIs
morph-mcp_warpgrep_codebase_search search_string="Find all Swift files using deprecated FileManager.default or Thread.isMainThread" repo_path="/Volumes/Main SSD/Developer/clawdis"
```
**Quick Fix Commands:**
```bash
# Find all affected files
find . -name "*.swift" -exec grep -l "FileManager\.default\|Thread\.isMainThread" {} \;
# Replace FileManager.default with FileManager()
find . -name "*.swift" -exec sed -i '' 's/FileManager\.default/FileManager()/g' {} \;
# For Thread.isMainThread, need manual review of each usage
grep -rn "Thread\.isMainThread" --include="*.swift" .
```
**Rebuild After Fixes:**
```bash
# Clean all build artifacts
rm -rf apps/macos/.build apps/macos/.swiftpm
rm -rf src/canvas-host/a2ui/.build
# Rebuild Peekaboo bundle
pnpm canvas:a2ui:bundle
# Full macOS rebuild
./scripts/restart-mac.sh
```
---
## Automation Script
Save as `scripts/sync-upstream.sh`:
```bash
#!/usr/bin/env bash
set -euo pipefail
echo "==> Fetching upstream..."
git fetch upstream
echo "==> Current divergence:"
git rev-list --left-right --count main...upstream/main
echo "==> Rebasing onto upstream/main..."
git rebase upstream/main
echo "==> Installing dependencies..."
pnpm install
echo "==> Building..."
pnpm build
pnpm ui:build
echo "==> Running doctor..."
pnpm clawdbot doctor
echo "==> Rebuilding macOS app..."
./scripts/restart-mac.sh
echo "==> Verifying gateway health..."
pnpm clawdbot health
echo "==> Checking for Swift 6.2 compatibility issues..."
if grep -r "FileManager\.default\|Thread\.isMainThread" src/ apps/ --include="*.swift" --quiet; then
echo "⚠️ Found potential Swift 6.2 deprecated API usage"
echo " Run manual fixes or use analyze-mode investigation"
else
echo "✅ No obvious Swift deprecation issues found"
fi
echo "==> Testing agent functionality..."
# Note: Update YOUR_TELEGRAM_SESSION_ID with actual session ID
pnpm clawdbot agent --message "Verification: Upstream sync and macOS rebuild completed successfully." --session-id YOUR_TELEGRAM_SESSION_ID || echo "Warning: Agent test failed - check Telegram for verification message"
echo "==> Done! Check Telegram for verification message, then run 'git push --force-with-lease' when ready."
```
-1
View File
@@ -1 +0,0 @@
/cache
Binary file not shown.
Binary file not shown.
-87
View File
@@ -1,87 +0,0 @@
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp csharp_omnisharp
# dart elixir elm erlang fortran fsharp
# go groovy haskell java julia kotlin
# lua markdown nix pascal perl php
# powershell python python_jedi r rego ruby
# ruby_solargraph rust scala swift terraform toml
# typescript typescript_vts yaml zig
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal / Lazarus, use pascal
# Special requirements:
# - csharp: Requires the presence of a .sln file in the project folder.
# - pascal: Requires Free Pascal Compiler (fpc) and optionally Lazarus.
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- typescript
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "clawdbot"
included_optional_tools: []
-2
View File
@@ -57,8 +57,6 @@
- Changelog workflow: keep latest released version at top (no `Unreleased`); after publishing, bump version and start a new top section.
- PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags.
- PR review flow: when given a PR link, review via `gh pr view`/`gh pr diff` and do **not** change branches.
- PR review calls: prefer a single `gh pr view --json ...` to batch metadata/comments; run `gh pr diff` only when needed.
- Before starting a review when a GH Issue/PR is pasted: run `git pull`; if there are local changes or unpushed commits, stop and alert the user before reviewing.
- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless it’s truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`.
- If you review a PR and later do work on it, land via merge/squash (no direct-main commits) and always add the PR author as a co-contributor.
- When working on a PR: add a changelog entry with the PR number and thank the contributor.
+2 -16
View File
@@ -2,24 +2,11 @@
Docs: https://docs.clawd.bot
## 2026.1.20-1
### Changes
- Repo: remove the Peekaboo git submodule now that the SPM release is used.
### Fixes
- Web search: infer Perplexity base URL from API key source (direct vs OpenRouter).
- TUI: keep thinking blocks ordered before content during streaming and isolate per-run assembly. (#1202) — thanks @aaronveklabs.
- CLI: avoid duplicating --profile/--dev flags when formatting commands.
- Agents: enforce tool policy/sandbox rules for tool-dispatched skill commands and forward tool media outputs. (#1235) — thanks @dougvk.
## 2026.1.19-3
### Changes
- Android: remove legacy bridge transport code now that nodes use the gateway protocol.
- Android: send structured payloads in node events/invokes and include user-agent metadata in gateway connects.
- Gateway: expand `/v1/responses` to support file/image inputs, tool_choice, usage, and output limits. (#1229) — thanks @RyanLisse.
- Docs: surface Amazon Bedrock in provider lists and clarify Bedrock auth env vars. (#1289) — thanks @steipete.
### Fixes
- Gateway: strip inbound envelope headers from chat history messages to keep clients clean.
@@ -42,7 +29,6 @@ Docs: https://docs.clawd.bot
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety; run `clawdbot doctor --fix` to repair.
### Changes
- Gateway: add `/v1/responses` endpoint (OpenResponses API) for agentic workflows with item-based input and semantic streaming events. Enable via `gateway.http.endpoints.responses.enabled: true`.
- Usage: add `/usage cost` summaries and macOS menu cost submenu with daily charting.
- Agents: clarify node_modules read-only guidance in agent instructions.
- TUI: add syntax highlighting for code blocks. (#1200) — thanks @vignesh07.
@@ -83,8 +69,8 @@ Docs: https://docs.clawd.bot
## 2026.1.18-4
### Changes
- macOS: switch PeekabooBridge integration to the tagged Swift Package Manager release.
- macOS: stop syncing Peekaboo in postinstall.
- macOS: switch PeekabooBridge integration to the tagged Swift Package Manager release (no submodule).
- macOS: stop syncing Peekaboo as a git submodule in postinstall.
- Swabble: use the tagged Commander Swift package release.
- CLI: add `clawdbot acp client` interactive ACP harness for debugging.
- Plugins: route command detection/text chunking helpers through the plugin runtime and drop runtime exports from the SDK.
+6 -1
View File
@@ -258,7 +258,12 @@ Send these in WhatsApp/Telegram/Slack/Microsoft Teams/WebChat (group commands ar
The Gateway alone delivers a great experience. All apps are optional and add extra features.
If you plan to build/run companion apps, follow the platform runbooks below.
If you plan to build/run companion apps, initialize submodules first:
```bash
git submodule update --init --recursive
./scripts/restart-mac.sh
```
### macOS (Clawdbot.app) (optional)
+2 -2
View File
@@ -101,8 +101,8 @@ Environment variables:
- Authorization requested at first start; requires macOS 26 + new Speech.framework APIs.
## Development
- Format: `./scripts/format.sh` (uses local `.swiftformat`)
- Lint: `./scripts/lint.sh` (uses local `.swiftlint.yml`)
- Format: `./scripts/format.sh` (uses ../peekaboo/.swiftformat if present)
- Lint: `./scripts/lint.sh` (uses ../peekaboo/.swiftlint.yml if present)
- Tests: `swift test` (uses swift-testing package)
## Roadmap
+6 -1
View File
@@ -1,5 +1,10 @@
#!/bin/bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CONFIG="${ROOT}/.swiftformat"
PEEKABOO_ROOT="${ROOT}/../peekaboo"
if [ -f "${PEEKABOO_ROOT}/.swiftformat" ]; then
CONFIG="${PEEKABOO_ROOT}/.swiftformat"
else
CONFIG="${ROOT}/.swiftformat"
fi
swiftformat --config "$CONFIG" "$ROOT/Sources"
+6 -1
View File
@@ -1,7 +1,12 @@
#!/bin/bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CONFIG="${ROOT}/.swiftlint.yml"
PEEKABOO_ROOT="${ROOT}/../peekaboo"
if [ -f "${PEEKABOO_ROOT}/.swiftlint.yml" ]; then
CONFIG="${PEEKABOO_ROOT}/.swiftlint.yml"
else
CONFIG="$ROOT/.swiftlint.yml"
fi
if ! command -v swiftlint >/dev/null; then
echo "swiftlint not installed" >&2
exit 1
@@ -160,14 +160,14 @@ actor CameraController {
defer { session.stopRunning() }
await Self.warmUpCaptureSession()
let movURL = FileManager().temporaryDirectory
let movURL = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mov")
let mp4URL = FileManager().temporaryDirectory
let mp4URL = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mp4")
defer {
try? FileManager().removeItem(at: movURL)
try? FileManager().removeItem(at: mp4URL)
try? FileManager.default.removeItem(at: movURL)
try? FileManager.default.removeItem(at: mp4URL)
}
var delegate: MovieFileDelegate?
+1 -1
View File
@@ -837,7 +837,7 @@ final class NodeAppModel {
fps: params.fps,
includeAudio: params.includeAudio,
outPath: nil)
defer { try? FileManager().removeItem(atPath: path) }
defer { try? FileManager.default.removeItem(atPath: path) }
let data = try Data(contentsOf: URL(fileURLWithPath: path))
struct Payload: Codable {
var format: String
@@ -91,7 +91,7 @@ final class ScreenRecordService: @unchecked Sendable {
let includeAudio = includeAudio ?? true
let outURL = self.makeOutputURL(outPath: outPath)
try? FileManager().removeItem(at: outURL)
try? FileManager.default.removeItem(at: outURL)
return RecordConfig(
durationMs: durationMs,
@@ -104,7 +104,7 @@ final class ScreenRecordService: @unchecked Sendable {
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return URL(fileURLWithPath: outPath)
}
return FileManager().temporaryDirectory
return FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-screen-record-\(UUID().uuidString).mp4")
}
@@ -23,7 +23,7 @@ enum AgentWorkspace {
}
static func displayPath(for url: URL) -> String {
let home = FileManager().homeDirectoryForCurrentUser.path
let home = FileManager.default.homeDirectoryForCurrentUser.path
let path = url.path
if path == home { return "~" }
if path.hasPrefix(home + "/") {
@@ -44,12 +44,12 @@ enum AgentWorkspace {
}
static func workspaceEntries(workspaceURL: URL) throws -> [String] {
let contents = try FileManager().contentsOfDirectory(atPath: workspaceURL.path)
let contents = try FileManager.default.contentsOfDirectory(atPath: workspaceURL.path)
return contents.filter { !self.ignoredEntries.contains($0) }
}
static func isWorkspaceEmpty(workspaceURL: URL) -> Bool {
let fm = FileManager()
let fm = FileManager.default
var isDir: ObjCBool = false
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
return true
@@ -66,7 +66,7 @@ enum AgentWorkspace {
}
static func bootstrapSafety(for workspaceURL: URL) -> BootstrapSafety {
let fm = FileManager()
let fm = FileManager.default
var isDir: ObjCBool = false
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
return .safe
@@ -90,29 +90,29 @@ enum AgentWorkspace {
static func bootstrap(workspaceURL: URL) throws -> URL {
let shouldSeedBootstrap = self.isWorkspaceEmpty(workspaceURL: workspaceURL)
try FileManager().createDirectory(at: workspaceURL, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: workspaceURL, withIntermediateDirectories: true)
let agentsURL = self.agentsURL(workspaceURL: workspaceURL)
if !FileManager().fileExists(atPath: agentsURL.path) {
if !FileManager.default.fileExists(atPath: agentsURL.path) {
try self.defaultTemplate().write(to: agentsURL, atomically: true, encoding: .utf8)
self.logger.info("Created AGENTS.md at \(agentsURL.path, privacy: .public)")
}
let soulURL = workspaceURL.appendingPathComponent(self.soulFilename)
if !FileManager().fileExists(atPath: soulURL.path) {
if !FileManager.default.fileExists(atPath: soulURL.path) {
try self.defaultSoulTemplate().write(to: soulURL, atomically: true, encoding: .utf8)
self.logger.info("Created SOUL.md at \(soulURL.path, privacy: .public)")
}
let identityURL = workspaceURL.appendingPathComponent(self.identityFilename)
if !FileManager().fileExists(atPath: identityURL.path) {
if !FileManager.default.fileExists(atPath: identityURL.path) {
try self.defaultIdentityTemplate().write(to: identityURL, atomically: true, encoding: .utf8)
self.logger.info("Created IDENTITY.md at \(identityURL.path, privacy: .public)")
}
let userURL = workspaceURL.appendingPathComponent(self.userFilename)
if !FileManager().fileExists(atPath: userURL.path) {
if !FileManager.default.fileExists(atPath: userURL.path) {
try self.defaultUserTemplate().write(to: userURL, atomically: true, encoding: .utf8)
self.logger.info("Created USER.md at \(userURL.path, privacy: .public)")
}
let bootstrapURL = workspaceURL.appendingPathComponent(self.bootstrapFilename)
if shouldSeedBootstrap, !FileManager().fileExists(atPath: bootstrapURL.path) {
if shouldSeedBootstrap, !FileManager.default.fileExists(atPath: bootstrapURL.path) {
try self.defaultBootstrapTemplate().write(to: bootstrapURL, atomically: true, encoding: .utf8)
self.logger.info("Created BOOTSTRAP.md at \(bootstrapURL.path, privacy: .public)")
}
@@ -120,7 +120,7 @@ enum AgentWorkspace {
}
static func needsBootstrap(workspaceURL: URL) -> Bool {
let fm = FileManager()
let fm = FileManager.default
var isDir: ObjCBool = false
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
return true
@@ -305,7 +305,7 @@ enum AgentWorkspace {
if let dev = self.devTemplateURL(named: named) {
urls.append(dev)
}
let cwd = URL(fileURLWithPath: FileManager().currentDirectoryPath)
let cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
urls.append(cwd.appendingPathComponent("docs")
.appendingPathComponent(self.templateDirname)
.appendingPathComponent(named))
@@ -45,7 +45,7 @@ struct AnthropicAuthControls: View {
NSWorkspace.shared.activateFileViewerSelecting([ClawdbotOAuthStore.oauthURL()])
}
.buttonStyle(.bordered)
.disabled(!FileManager().fileExists(atPath: ClawdbotOAuthStore.oauthURL().path))
.disabled(!FileManager.default.fileExists(atPath: ClawdbotOAuthStore.oauthURL().path))
Button("Refresh") {
self.refresh()
@@ -234,7 +234,7 @@ enum ClawdbotOAuthStore {
return URL(fileURLWithPath: expanded, isDirectory: true)
}
return FileManager().homeDirectoryForCurrentUser
return FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".clawdbot", isDirectory: true)
.appendingPathComponent("credentials", isDirectory: true)
}
@@ -253,7 +253,7 @@ enum ClawdbotOAuthStore {
urls.append(URL(fileURLWithPath: expanded, isDirectory: true).appendingPathComponent(self.oauthFilename))
}
let home = FileManager().homeDirectoryForCurrentUser
let home = FileManager.default.homeDirectoryForCurrentUser
urls.append(home.appendingPathComponent(".pi/agent/\(self.oauthFilename)"))
urls.append(home.appendingPathComponent(".claude/\(self.oauthFilename)"))
urls.append(home.appendingPathComponent(".config/claude/\(self.oauthFilename)"))
@@ -270,10 +270,10 @@ enum ClawdbotOAuthStore {
static func importLegacyAnthropicOAuthIfNeeded() -> URL? {
let dest = self.oauthURL()
guard !FileManager().fileExists(atPath: dest.path) else { return nil }
guard !FileManager.default.fileExists(atPath: dest.path) else { return nil }
for url in self.legacyOAuthURLs() {
guard FileManager().fileExists(atPath: url.path) else { continue }
guard FileManager.default.fileExists(atPath: url.path) else { continue }
guard self.anthropicOAuthStatus(at: url).isConnected else { continue }
guard let storage = self.loadStorage(at: url) else { continue }
do {
@@ -296,7 +296,7 @@ enum ClawdbotOAuthStore {
}
static func anthropicOAuthStatus(at url: URL) -> AnthropicOAuthStatus {
guard FileManager().fileExists(atPath: url.path) else { return .missingFile }
guard FileManager.default.fileExists(atPath: url.path) else { return .missingFile }
guard let data = try? Data(contentsOf: url) else { return .unreadableFile }
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else { return .invalidJSON }
@@ -360,7 +360,7 @@ enum ClawdbotOAuthStore {
private static func saveStorage(_ storage: [String: Any]) throws {
let dir = self.oauthDir()
try FileManager().createDirectory(
try FileManager.default.createDirectory(
at: dir,
withIntermediateDirectories: true,
attributes: [.posixPermissions: 0o700])
@@ -370,7 +370,7 @@ enum ClawdbotOAuthStore {
withJSONObject: storage,
options: [.prettyPrinted, .sortedKeys])
try data.write(to: url, options: [.atomic])
try FileManager().setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
}
}
@@ -61,7 +61,7 @@ enum CLIInstaller {
}
private static func installPrefix() -> String {
FileManager().homeDirectoryForCurrentUser
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".clawdbot")
.path
}
@@ -167,20 +167,20 @@ actor CameraCaptureService {
defer { session.stopRunning() }
await Self.warmUpCaptureSession()
let tmpMovURL = FileManager().temporaryDirectory
let tmpMovURL = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mov")
defer { try? FileManager().removeItem(at: tmpMovURL) }
defer { try? FileManager.default.removeItem(at: tmpMovURL) }
let outputURL: URL = {
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return URL(fileURLWithPath: outPath)
}
return FileManager().temporaryDirectory
return FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mp4")
}()
// Ensure we don't fail exporting due to an existing file.
try? FileManager().removeItem(at: outputURL)
try? FileManager.default.removeItem(at: outputURL)
let logger = self.logger
var delegate: MovieFileDelegate?
@@ -25,7 +25,7 @@ final class CanvasManager {
var defaultAnchorProvider: (() -> NSRect?)?
private nonisolated static let canvasRoot: URL = {
let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
return base.appendingPathComponent("Clawdbot/canvas", isDirectory: true)
}()
@@ -83,7 +83,7 @@ final class CanvasManager {
self.panelSessionKey = nil
Self.logger.debug("showDetailed ensure canvas root dir")
try FileManager().createDirectory(at: Self.canvasRoot, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: Self.canvasRoot, withIntermediateDirectories: true)
Self.logger.debug("showDetailed init CanvasWindowController")
let controller = try CanvasWindowController(
sessionKey: session,
@@ -258,7 +258,7 @@ final class CanvasManager {
// (Avoid treating Canvas routes like "/" as filesystem paths.)
if trimmed.hasPrefix("/") {
var isDir: ObjCBool = false
if FileManager().fileExists(atPath: trimmed, isDirectory: &isDir), !isDir.boolValue {
if FileManager.default.fileExists(atPath: trimmed, isDirectory: &isDir), !isDir.boolValue {
return URL(fileURLWithPath: trimmed)
}
}
@@ -293,7 +293,7 @@ final class CanvasManager {
}
private static func localStatus(sessionDir: URL, target: String) -> CanvasShowStatus {
let fm = FileManager()
let fm = FileManager.default
let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines)
let withoutQuery = trimmed.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false).first
.map(String.init) ?? trimmed
@@ -331,7 +331,7 @@ final class CanvasManager {
}
private static func indexExists(in dir: URL) -> Bool {
let fm = FileManager()
let fm = FileManager.default
let a = dir.appendingPathComponent("index.html", isDirectory: false)
if fm.fileExists(atPath: a.path) { return true }
let b = dir.appendingPathComponent("index.htm", isDirectory: false)
@@ -69,8 +69,8 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
if path.isEmpty {
let indexA = sessionRoot.appendingPathComponent("index.html", isDirectory: false)
let indexB = sessionRoot.appendingPathComponent("index.htm", isDirectory: false)
if !FileManager().fileExists(atPath: indexA.path),
!FileManager().fileExists(atPath: indexB.path)
if !FileManager.default.fileExists(atPath: indexA.path),
!FileManager.default.fileExists(atPath: indexB.path)
{
return self.scaffoldPage(sessionRoot: sessionRoot)
}
@@ -106,7 +106,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
}
private func resolveFileURL(sessionRoot: URL, requestPath: String) -> URL? {
let fm = FileManager()
let fm = FileManager.default
var candidate = sessionRoot.appendingPathComponent(requestPath, isDirectory: false)
var isDir: ObjCBool = false
@@ -137,7 +137,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
}
private func resolveIndex(in dir: URL) -> URL? {
let fm = FileManager()
let fm = FileManager.default
let a = dir.appendingPathComponent("index.html", isDirectory: false)
if fm.fileExists(atPath: a.path) { return a }
let b = dir.appendingPathComponent("index.htm", isDirectory: false)
@@ -32,7 +32,7 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
let safeSessionKey = CanvasWindowController.sanitizeSessionKey(sessionKey)
canvasWindowLogger.debug("CanvasWindowController init sanitized session=\(safeSessionKey, privacy: .public)")
self.sessionDir = root.appendingPathComponent(safeSessionKey, isDirectory: true)
try FileManager().createDirectory(at: self.sessionDir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: self.sessionDir, withIntermediateDirectories: true)
canvasWindowLogger.debug("CanvasWindowController init session dir ready")
self.schemeHandler = CanvasSchemeHandler(root: root)
@@ -143,8 +143,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
if path == "/" || path.isEmpty {
let indexA = sessionDir.appendingPathComponent("index.html", isDirectory: false)
let indexB = sessionDir.appendingPathComponent("index.htm", isDirectory: false)
if !FileManager().fileExists(atPath: indexA.path),
!FileManager().fileExists(atPath: indexB.path)
if !FileManager.default.fileExists(atPath: indexA.path),
!FileManager.default.fileExists(atPath: indexB.path)
{
return
}
@@ -233,7 +233,7 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
// (Avoid treating Canvas routes like "/" as filesystem paths.)
if trimmed.hasPrefix("/") {
var isDir: ObjCBool = false
if FileManager().fileExists(atPath: trimmed, isDirectory: &isDir), !isDir.boolValue {
if FileManager.default.fileExists(atPath: trimmed, isDirectory: &isDir), !isDir.boolValue {
let url = URL(fileURLWithPath: trimmed)
canvasWindowLogger.debug("canvas load file \(url.absoluteString, privacy: .public)")
self.loadFile(url)
@@ -18,7 +18,7 @@ enum ClawdbotConfigFile {
static func loadDict() -> [String: Any] {
let url = self.url()
guard FileManager().fileExists(atPath: url.path) else { return [:] }
guard FileManager.default.fileExists(atPath: url.path) else { return [:] }
do {
let data = try Data(contentsOf: url)
guard let root = self.parseConfigData(data) else {
@@ -38,7 +38,7 @@ enum ClawdbotConfigFile {
do {
let data = try JSONSerialization.data(withJSONObject: dict, options: [.prettyPrinted, .sortedKeys])
let url = self.url()
try FileManager().createDirectory(
try FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true)
try data.write(to: url, options: [.atomic])
@@ -21,7 +21,7 @@ enum ClawdbotPaths {
if let override = ClawdbotEnv.path(self.stateDirEnv) {
return URL(fileURLWithPath: override, isDirectory: true)
}
return FileManager().homeDirectoryForCurrentUser
return FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".clawdbot", isDirectory: true)
}
@@ -6,9 +6,9 @@ enum CommandResolver {
static func gatewayEntrypoint(in root: URL) -> String? {
let distEntry = root.appendingPathComponent("dist/index.js").path
if FileManager().isReadableFile(atPath: distEntry) { return distEntry }
if FileManager.default.isReadableFile(atPath: distEntry) { return distEntry }
let binEntry = root.appendingPathComponent("bin/clawdbot.js").path
if FileManager().isReadableFile(atPath: binEntry) { return binEntry }
if FileManager.default.isReadableFile(atPath: binEntry) { return binEntry }
return nil
}
@@ -47,16 +47,16 @@ enum CommandResolver {
static func projectRoot() -> URL {
if let stored = UserDefaults.standard.string(forKey: self.projectRootDefaultsKey),
let url = self.expandPath(stored),
FileManager().fileExists(atPath: url.path)
FileManager.default.fileExists(atPath: url.path)
{
return url
}
let fallback = FileManager().homeDirectoryForCurrentUser
let fallback = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Projects/clawdbot")
if FileManager().fileExists(atPath: fallback.path) {
if FileManager.default.fileExists(atPath: fallback.path) {
return fallback
}
return FileManager().homeDirectoryForCurrentUser
return FileManager.default.homeDirectoryForCurrentUser
}
static func setProjectRoot(_ path: String) {
@@ -70,7 +70,7 @@ enum CommandResolver {
static func preferredPaths() -> [String] {
let current = ProcessInfo.processInfo.environment["PATH"]?
.split(separator: ":").map(String.init) ?? []
let home = FileManager().homeDirectoryForCurrentUser
let home = FileManager.default.homeDirectoryForCurrentUser
let projectRoot = self.projectRoot()
return self.preferredPaths(home: home, current: current, projectRoot: projectRoot)
}
@@ -99,10 +99,10 @@ enum CommandResolver {
let bin = base.appendingPathComponent("bin")
let nodeBin = base.appendingPathComponent("tools/node/bin")
var paths: [String] = []
if FileManager().fileExists(atPath: bin.path) {
if FileManager.default.fileExists(atPath: bin.path) {
paths.append(bin.path)
}
if FileManager().fileExists(atPath: nodeBin.path) {
if FileManager.default.fileExists(atPath: nodeBin.path) {
paths.append(nodeBin.path)
}
return paths
@@ -113,13 +113,13 @@ enum CommandResolver {
// Volta
let volta = home.appendingPathComponent(".volta/bin")
if FileManager().fileExists(atPath: volta.path) {
if FileManager.default.fileExists(atPath: volta.path) {
bins.append(volta.path)
}
// asdf
let asdf = home.appendingPathComponent(".asdf/shims")
if FileManager().fileExists(atPath: asdf.path) {
if FileManager.default.fileExists(atPath: asdf.path) {
bins.append(asdf.path)
}
@@ -137,10 +137,10 @@ enum CommandResolver {
}
private static func versionedNodeBinPaths(base: URL, suffix: String) -> [String] {
guard FileManager().fileExists(atPath: base.path) else { return [] }
guard FileManager.default.fileExists(atPath: base.path) else { return [] }
let entries: [String]
do {
entries = try FileManager().contentsOfDirectory(atPath: base.path)
entries = try FileManager.default.contentsOfDirectory(atPath: base.path)
} catch {
return []
}
@@ -167,7 +167,7 @@ enum CommandResolver {
for entry in sorted {
let binDir = base.appendingPathComponent(entry).appendingPathComponent(suffix)
let node = binDir.appendingPathComponent("node")
if FileManager().isExecutableFile(atPath: node.path) {
if FileManager.default.isExecutableFile(atPath: node.path) {
paths.append(binDir.path)
}
}
@@ -177,7 +177,7 @@ enum CommandResolver {
static func findExecutable(named name: String, searchPaths: [String]? = nil) -> String? {
for dir in searchPaths ?? self.preferredPaths() {
let candidate = (dir as NSString).appendingPathComponent(name)
if FileManager().isExecutableFile(atPath: candidate) {
if FileManager.default.isExecutableFile(atPath: candidate) {
return candidate
}
}
@@ -191,12 +191,12 @@ enum CommandResolver {
static func projectClawdbotExecutable(projectRoot: URL? = nil) -> String? {
let root = projectRoot ?? self.projectRoot()
let candidate = root.appendingPathComponent("node_modules/.bin").appendingPathComponent(self.helperName).path
return FileManager().isExecutableFile(atPath: candidate) ? candidate : nil
return FileManager.default.isExecutableFile(atPath: candidate) ? candidate : nil
}
static func nodeCliPath() -> String? {
let candidate = self.projectRoot().appendingPathComponent("bin/clawdbot.js").path
return FileManager().isReadableFile(atPath: candidate) ? candidate : nil
return FileManager.default.isReadableFile(atPath: candidate) ? candidate : nil
}
static func hasAnyClawdbotInvoker(searchPaths: [String]? = nil) -> Bool {
@@ -459,7 +459,7 @@ enum CommandResolver {
private static func expandPath(_ path: String) -> URL? {
var expanded = path
if expanded.hasPrefix("~") {
let home = FileManager().homeDirectoryForCurrentUser.path
let home = FileManager.default.homeDirectoryForCurrentUser.path
expanded.replaceSubrange(expanded.startIndex...expanded.startIndex, with: home)
}
return URL(fileURLWithPath: expanded)
@@ -26,7 +26,7 @@ enum DebugActions {
static func openLog() {
let path = self.pinoLogPath()
let url = URL(fileURLWithPath: path)
guard FileManager().fileExists(atPath: path) else {
guard FileManager.default.fileExists(atPath: path) else {
let alert = NSAlert()
alert.messageText = "Log file not found"
alert.informativeText = path
@@ -38,7 +38,7 @@ enum DebugActions {
@MainActor
static func openConfigFolder() {
let url = FileManager()
let url = FileManager.default
.homeDirectoryForCurrentUser
.appendingPathComponent(".clawdbot", isDirectory: true)
NSWorkspace.shared.activateFileViewerSelecting([url])
@@ -55,7 +55,7 @@ enum DebugActions {
}
let path = self.resolveSessionStorePath()
let url = URL(fileURLWithPath: path)
if FileManager().fileExists(atPath: path) {
if FileManager.default.fileExists(atPath: path) {
NSWorkspace.shared.activateFileViewerSelecting([url])
} else {
NSWorkspace.shared.open(url.deletingLastPathComponent())
@@ -195,7 +195,7 @@ enum DebugActions {
@MainActor
private static func resolveSessionStorePath() -> String {
let defaultPath = SessionLoader.defaultStorePath
let configURL = FileManager().homeDirectoryForCurrentUser
let configURL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".clawdbot/clawdbot.json")
guard
let data = try? Data(contentsOf: configURL),
@@ -354,7 +354,7 @@ struct DebugSettings: View {
Button("Save") { self.saveRelayRoot() }
.buttonStyle(.borderedProminent)
Button("Reset") {
let def = FileManager().homeDirectoryForCurrentUser
let def = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Projects/clawdbot").path
self.gatewayRootInput = def
self.saveRelayRoot()
@@ -743,7 +743,7 @@ struct DebugSettings: View {
do {
let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys])
try FileManager().createDirectory(
try FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true)
try data.write(to: url, options: [.atomic])
@@ -776,7 +776,7 @@ struct DebugSettings: View {
}
private func configURL() -> URL {
FileManager().homeDirectoryForCurrentUser
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".clawdbot")
.appendingPathComponent("clawdbot.json")
}
@@ -20,8 +20,8 @@ actor DiagnosticsFileLog {
}
nonisolated static func logDirectoryURL() -> URL {
let library = FileManager().urls(for: .libraryDirectory, in: .userDomainMask).first
?? FileManager().homeDirectoryForCurrentUser.appendingPathComponent("Library", isDirectory: true)
let library = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first
?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library", isDirectory: true)
return library
.appendingPathComponent("Logs", isDirectory: true)
.appendingPathComponent("Clawdbot", isDirectory: true)
@@ -43,7 +43,7 @@ actor DiagnosticsFileLog {
}
func clear() throws {
let fm = FileManager()
let fm = FileManager.default
let base = Self.logFileURL()
if fm.fileExists(atPath: base.path) {
try fm.removeItem(at: base)
@@ -67,7 +67,7 @@ actor DiagnosticsFileLog {
}
private func ensureDirectory() throws {
try FileManager().createDirectory(
try FileManager.default.createDirectory(
at: Self.logDirectoryURL(),
withIntermediateDirectories: true)
}
@@ -79,7 +79,7 @@ actor DiagnosticsFileLog {
line.append(data)
line.append(0x0A) // newline
let fm = FileManager()
let fm = FileManager.default
if !fm.fileExists(atPath: url.path) {
fm.createFile(atPath: url.path, contents: nil)
}
@@ -92,13 +92,13 @@ actor DiagnosticsFileLog {
private func rotateIfNeeded() throws {
let url = Self.logFileURL()
guard let attrs = try? FileManager().attributesOfItem(atPath: url.path),
guard let attrs = try? FileManager.default.attributesOfItem(atPath: url.path),
let size = attrs[.size] as? NSNumber
else { return }
if size.int64Value < self.maxBytes { return }
let fm = FileManager()
let fm = FileManager.default
let oldest = self.rotatedURL(index: self.maxBackups)
if fm.fileExists(atPath: oldest.path) {
@@ -176,7 +176,7 @@ enum ExecApprovalsStore {
static func readSnapshot() -> ExecApprovalsSnapshot {
let url = self.fileURL()
guard FileManager().fileExists(atPath: url.path) else {
guard FileManager.default.fileExists(atPath: url.path) else {
return ExecApprovalsSnapshot(
path: url.path,
exists: false,
@@ -216,7 +216,7 @@ enum ExecApprovalsStore {
static func loadFile() -> ExecApprovalsFile {
let url = self.fileURL()
guard FileManager().fileExists(atPath: url.path) else {
guard FileManager.default.fileExists(atPath: url.path) else {
return ExecApprovalsFile(version: 1, socket: nil, defaults: nil, agents: [:])
}
do {
@@ -238,11 +238,11 @@ enum ExecApprovalsStore {
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(file)
let url = self.fileURL()
try FileManager().createDirectory(
try FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true)
try data.write(to: url, options: [.atomic])
try? FileManager().setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
} catch {
self.logger.error("exec approvals save failed: \(error.localizedDescription, privacy: .public)")
}
@@ -442,11 +442,11 @@ enum ExecApprovalsStore {
private static func expandPath(_ raw: String) -> String {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed == "~" {
return FileManager().homeDirectoryForCurrentUser.path
return FileManager.default.homeDirectoryForCurrentUser.path
}
if trimmed.hasPrefix("~/") {
let suffix = trimmed.dropFirst(2)
return FileManager().homeDirectoryForCurrentUser
return FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(String(suffix)).path
}
return trimmed
@@ -497,7 +497,7 @@ struct ExecCommandResolution: Sendable {
return expanded
}
let base = cwd?.trimmingCharacters(in: .whitespacesAndNewlines)
let root = (base?.isEmpty == false) ? base! : FileManager().currentDirectoryPath
let root = (base?.isEmpty == false) ? base! : FileManager.default.currentDirectoryPath
return URL(fileURLWithPath: root).appendingPathComponent(expanded).path
}
let searchPaths = self.searchPaths(from: env)
@@ -155,8 +155,7 @@ actor GatewayEndpointStore {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
if let configToken = self.resolveConfigToken(isRemote: isRemote, root: root),
!configToken.isEmpty,
configToken != trimmed
!configToken.isEmpty
{
self.warnEnvOverrideOnce(
kind: .token,
@@ -165,19 +164,32 @@ actor GatewayEndpointStore {
}
return trimmed
}
if let configToken = self.resolveConfigToken(isRemote: isRemote, root: root),
!configToken.isEmpty
if isRemote {
if let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let token = remote["token"] as? String
{
let value = token.trimmingCharacters(in: .whitespacesAndNewlines)
if !value.isEmpty {
return value
}
}
return nil
}
if let gateway = root["gateway"] as? [String: Any],
let auth = gateway["auth"] as? [String: Any],
let token = auth["token"] as? String
{
return configToken
let value = token.trimmingCharacters(in: .whitespacesAndNewlines)
if !value.isEmpty {
return value
}
}
if let token = launchdSnapshot?.token?.trimmingCharacters(in: .whitespacesAndNewlines),
!token.isEmpty
{
return token
}
return nil
}
@@ -5,7 +5,7 @@ enum GatewayLaunchAgentManager {
private static let disableLaunchAgentMarker = ".clawdbot/disable-launchagent"
private static var plistURL: URL {
FileManager().homeDirectoryForCurrentUser
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/LaunchAgents/\(gatewayLaunchdLabel).plist")
}
@@ -67,9 +67,9 @@ enum GatewayLaunchAgentManager {
extension GatewayLaunchAgentManager {
private static func isLaunchAgentWriteDisabled() -> Bool {
let marker = FileManager().homeDirectoryForCurrentUser
let marker = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(self.disableLaunchAgentMarker)
return FileManager().fileExists(atPath: marker.path)
return FileManager.default.fileExists(atPath: marker.path)
}
private static func readDaemonLoaded() async -> Bool? {
@@ -365,7 +365,7 @@ final class GatewayProcessManager {
func clearLog() {
self.log = ""
try? FileManager().removeItem(atPath: GatewayLaunchAgentManager.launchdGatewayLogPath())
try? FileManager.default.removeItem(atPath: GatewayLaunchAgentManager.launchdGatewayLogPath())
self.logger.debug("gateway log cleared")
}
@@ -378,7 +378,7 @@ final class GatewayProcessManager {
}
private nonisolated static func readGatewayLog(path: String, limit: Int) -> String {
guard FileManager().fileExists(atPath: path) else { return "" }
guard FileManager.default.fileExists(atPath: path) else { return "" }
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return "" }
let text = String(data: data, encoding: .utf8) ?? ""
if text.count <= limit { return text }
@@ -3,17 +3,17 @@ import Foundation
enum LaunchAgentManager {
private static let legacyLaunchdLabel = "com.steipete.clawdbot"
private static var plistURL: URL {
FileManager().homeDirectoryForCurrentUser
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/LaunchAgents/com.clawdbot.mac.plist")
}
private static var legacyPlistURL: URL {
FileManager().homeDirectoryForCurrentUser
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/LaunchAgents/\(legacyLaunchdLabel).plist")
}
static func status() async -> Bool {
guard FileManager().fileExists(atPath: self.plistURL.path) else { return false }
guard FileManager.default.fileExists(atPath: self.plistURL.path) else { return false }
let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"])
return result == 0
}
@@ -21,7 +21,7 @@ enum LaunchAgentManager {
static func set(enabled: Bool, bundlePath: String) async {
if enabled {
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(self.legacyLaunchdLabel)"])
try? FileManager().removeItem(at: self.legacyPlistURL)
try? FileManager.default.removeItem(at: self.legacyPlistURL)
self.writePlist(bundlePath: bundlePath)
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
_ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
@@ -29,7 +29,7 @@ enum LaunchAgentManager {
} else {
// Disable autostart going forward but leave the current app running.
// bootout would terminate the launchd job immediately (and crash the app if launched via agent).
try? FileManager().removeItem(at: self.plistURL)
try? FileManager.default.removeItem(at: self.plistURL)
}
}
@@ -46,7 +46,7 @@ enum LaunchAgentManager {
<string>\(bundlePath)/Contents/MacOS/Clawdbot</string>
</array>
<key>WorkingDirectory</key>
<string>\(FileManager().homeDirectoryForCurrentUser.path)</string>
<string>\(FileManager.default.homeDirectoryForCurrentUser.path)</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
+2 -2
View File
@@ -18,7 +18,7 @@ enum LogLocator {
}
private static func ensureLogDirExists() {
try? FileManager().createDirectory(at: self.logDir, withIntermediateDirectories: true)
try? FileManager.default.createDirectory(at: self.logDir, withIntermediateDirectories: true)
}
private static func modificationDate(for url: URL) -> Date {
@@ -28,7 +28,7 @@ enum LogLocator {
/// Returns the newest log file under /tmp/clawdbot/ (rolling or stdout), or nil if none exist.
static func bestLogFile() -> URL? {
self.ensureLogDirExists()
let fm = FileManager()
let fm = FileManager.default
let files = (try? fm.contentsOfDirectory(
at: self.logDir,
includingPropertiesForKeys: [.contentModificationDateKey],
@@ -2,7 +2,7 @@ import Foundation
import JavaScriptCore
enum ModelCatalogLoader {
static let defaultPath: String = FileManager().homeDirectoryForCurrentUser
static let defaultPath: String = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Projects/pi-mono/packages/ai/src/models.generated.ts").path
private static let logger = Logger(subsystem: "com.clawdbot", category: "models")
@@ -133,7 +133,7 @@ actor MacNodeRuntime {
let sessionKey = self.mainSessionKey
let path = try await CanvasManager.shared.snapshot(sessionKey: sessionKey, outPath: nil)
defer { try? FileManager().removeItem(atPath: path) }
defer { try? FileManager.default.removeItem(atPath: path) }
let data = try Data(contentsOf: URL(fileURLWithPath: path))
guard let image = NSImage(data: data) else {
return Self.errorResponse(req, code: .unavailable, message: "canvas snapshot decode failed")
@@ -206,7 +206,7 @@ actor MacNodeRuntime {
includeAudio: params.includeAudio ?? true,
deviceId: params.deviceId,
outPath: nil)
defer { try? FileManager().removeItem(atPath: res.path) }
defer { try? FileManager.default.removeItem(atPath: res.path) }
let data = try Data(contentsOf: URL(fileURLWithPath: res.path))
struct ClipPayload: Encodable {
var format: String
@@ -312,7 +312,7 @@ actor MacNodeRuntime {
fps: params.fps,
includeAudio: params.includeAudio,
outPath: nil)
defer { try? FileManager().removeItem(atPath: res.path) }
defer { try? FileManager.default.removeItem(atPath: res.path) }
let data = try Data(contentsOf: URL(fileURLWithPath: res.path))
struct ScreenPayload: Encodable {
var format: String
@@ -24,7 +24,7 @@ actor PortGuardian {
private var records: [Record] = []
private let logger = Logger(subsystem: "com.clawdbot", category: "portguard")
private nonisolated static let appSupportDir: URL = {
let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
return base.appendingPathComponent("Clawdbot", isDirectory: true)
}()
@@ -71,7 +71,7 @@ actor PortGuardian {
}
func record(port: Int, pid: Int32, command: String, mode: AppState.ConnectionMode) async {
try? FileManager().createDirectory(at: Self.appSupportDir, withIntermediateDirectories: true)
try? FileManager.default.createDirectory(at: Self.appSupportDir, withIntermediateDirectories: true)
self.records.removeAll { $0.pid == pid }
self.records.append(
Record(
@@ -111,7 +111,7 @@ enum RuntimeLocator {
// MARK: - Internals
private static func findExecutable(named name: String, searchPaths: [String]) -> String? {
let fm = FileManager()
let fm = FileManager.default
for dir in searchPaths {
let candidate = (dir as NSString).appendingPathComponent(name)
if fm.isExecutableFile(atPath: candidate) {
@@ -42,10 +42,10 @@ final class ScreenRecordService {
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return URL(fileURLWithPath: outPath)
}
return FileManager().temporaryDirectory
return FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-screen-record-\(UUID().uuidString).mp4")
}()
try? FileManager().removeItem(at: outURL)
try? FileManager.default.removeItem(at: outURL)
let content = try await SCShareableContent.current
let displays = content.displays.sorted { $0.displayID < $1.displayID }
@@ -66,12 +66,12 @@ enum SessionActions {
let dir = URL(fileURLWithPath: storePath).deletingLastPathComponent()
urls.append(dir.appendingPathComponent("\(sessionId).jsonl"))
}
let home = FileManager().homeDirectoryForCurrentUser
let home = FileManager.default.homeDirectoryForCurrentUser
urls.append(home.appendingPathComponent(".clawdbot/sessions/\(sessionId).jsonl"))
return urls
}()
let existing = candidates.first(where: { FileManager().fileExists(atPath: $0.path) })
let existing = candidates.first(where: { FileManager.default.fileExists(atPath: $0.path) })
guard let url = existing else {
let alert = NSAlert()
alert.messageText = "Session log not found"
@@ -246,7 +246,7 @@ enum SessionLoader {
static let fallbackContextTokens = 200_000
static let defaultStorePath = standardize(
FileManager().homeDirectoryForCurrentUser
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".clawdbot/sessions/sessions.json").path)
static func loadSnapshot(
@@ -44,7 +44,7 @@ enum SoundEffectCatalog {
]
private static let searchRoots: [URL] = [
FileManager().homeDirectoryForCurrentUser.appendingPathComponent("Library/Sounds"),
FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Sounds"),
URL(fileURLWithPath: "/Library/Sounds"),
URL(fileURLWithPath: "/System/Applications/Mail.app/Contents/Resources"), // Mail “swoosh”
URL(fileURLWithPath: "/System/Library/Sounds"),
@@ -53,7 +53,7 @@ enum SoundEffectCatalog {
private static let discoveredSoundMap: [String: URL] = {
var map: [String: URL] = [:]
for root in Self.searchRoots {
guard let contents = try? FileManager().contentsOfDirectory(
guard let contents = try? FileManager.default.contentsOfDirectory(
at: root,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles])
@@ -53,7 +53,7 @@ final class TailscaleService {
#endif
func checkAppInstallation() -> Bool {
let installed = FileManager().fileExists(atPath: "/Applications/Tailscale.app")
let installed = FileManager.default.fileExists(atPath: "/Applications/Tailscale.app")
self.logger.info("Tailscale app installed: \(installed)")
return installed
}
@@ -37,7 +37,7 @@ final class VoicePushToTalkHotkey: @unchecked Sendable {
}
private func startMonitoring() {
// assert(Thread.isMainThread) - Removed for Swift 6
assert(Thread.isMainThread)
guard self.globalMonitor == nil, self.localMonitor == nil else { return }
// Listen-only global monitor; we rely on Input Monitoring permission to receive events.
self.globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
@@ -55,7 +55,7 @@ final class VoicePushToTalkHotkey: @unchecked Sendable {
}
private func stopMonitoring() {
// assert(Thread.isMainThread) - Removed for Swift 6
assert(Thread.isMainThread)
if let globalMonitor {
NSEvent.removeMonitor(globalMonitor)
self.globalMonitor = nil
@@ -75,11 +75,15 @@ final class VoicePushToTalkHotkey: @unchecked Sendable {
}
private func withMainThread(_ block: @escaping @Sendable () -> Void) {
DispatchQueue.main.async(execute: block)
if Thread.isMainThread {
block()
} else {
DispatchQueue.main.async(execute: block)
}
}
private func updateModifierState(keyCode: UInt16, modifierFlags: NSEvent.ModifierFlags) {
// assert(Thread.isMainThread) - Removed for Swift 6
assert(Thread.isMainThread)
// Right Option (keyCode 61) acts as a hold-to-talk modifier.
if keyCode == 61 {
self.optionDown = modifierFlags.contains(.option)
+1 -1
View File
@@ -409,7 +409,7 @@ extension Request: Codable {
// Shared transport settings
public let controlSocketPath =
FileManager()
FileManager.default
.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Application Support/clawdbot/control.sock")
.path
@@ -187,7 +187,7 @@ private func resolvedPassword(opts: WizardCliOptions, config: GatewayConfig) ->
}
private func loadGatewayConfig() -> GatewayConfig {
let url = FileManager().homeDirectoryForCurrentUser
let url = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".clawdbot")
.appendingPathComponent("clawdbot.json")
guard let data = try? Data(contentsOf: url) else { return GatewayConfig() }
@@ -6,7 +6,7 @@ import Testing
struct AgentWorkspaceTests {
@Test
func displayPathUsesTildeForHome() {
let home = FileManager().homeDirectoryForCurrentUser
let home = FileManager.default.homeDirectoryForCurrentUser
#expect(AgentWorkspace.displayPath(for: home) == "~")
let inside = home.appendingPathComponent("Projects", isDirectory: true)
@@ -28,12 +28,12 @@ struct AgentWorkspaceTests {
@Test
func bootstrapCreatesAgentsFileWhenMissing() throws {
let tmp = FileManager().temporaryDirectory
let tmp = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager().removeItem(at: tmp) }
defer { try? FileManager.default.removeItem(at: tmp) }
let agentsURL = try AgentWorkspace.bootstrap(workspaceURL: tmp)
#expect(FileManager().fileExists(atPath: agentsURL.path))
#expect(FileManager.default.fileExists(atPath: agentsURL.path))
let contents = try String(contentsOf: agentsURL, encoding: .utf8)
#expect(contents.contains("# AGENTS.md"))
@@ -41,9 +41,9 @@ struct AgentWorkspaceTests {
let identityURL = tmp.appendingPathComponent(AgentWorkspace.identityFilename)
let userURL = tmp.appendingPathComponent(AgentWorkspace.userFilename)
let bootstrapURL = tmp.appendingPathComponent(AgentWorkspace.bootstrapFilename)
#expect(FileManager().fileExists(atPath: identityURL.path))
#expect(FileManager().fileExists(atPath: userURL.path))
#expect(FileManager().fileExists(atPath: bootstrapURL.path))
#expect(FileManager.default.fileExists(atPath: identityURL.path))
#expect(FileManager.default.fileExists(atPath: userURL.path))
#expect(FileManager.default.fileExists(atPath: bootstrapURL.path))
let second = try AgentWorkspace.bootstrap(workspaceURL: tmp)
#expect(second == agentsURL)
@@ -51,10 +51,10 @@ struct AgentWorkspaceTests {
@Test
func bootstrapSafetyRejectsNonEmptyFolderWithoutAgents() throws {
let tmp = FileManager().temporaryDirectory
let tmp = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager().removeItem(at: tmp) }
try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tmp) }
try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
let marker = tmp.appendingPathComponent("notes.txt")
try "hello".write(to: marker, atomically: true, encoding: .utf8)
@@ -69,10 +69,10 @@ struct AgentWorkspaceTests {
@Test
func bootstrapSafetyAllowsExistingAgentsFile() throws {
let tmp = FileManager().temporaryDirectory
let tmp = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager().removeItem(at: tmp) }
try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tmp) }
try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
let agents = tmp.appendingPathComponent(AgentWorkspace.agentsFilename)
try "# AGENTS.md".write(to: agents, atomically: true, encoding: .utf8)
@@ -87,25 +87,25 @@ struct AgentWorkspaceTests {
@Test
func bootstrapSkipsBootstrapFileWhenWorkspaceHasContent() throws {
let tmp = FileManager().temporaryDirectory
let tmp = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager().removeItem(at: tmp) }
try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tmp) }
try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
let marker = tmp.appendingPathComponent("notes.txt")
try "hello".write(to: marker, atomically: true, encoding: .utf8)
_ = try AgentWorkspace.bootstrap(workspaceURL: tmp)
let bootstrapURL = tmp.appendingPathComponent(AgentWorkspace.bootstrapFilename)
#expect(!FileManager().fileExists(atPath: bootstrapURL.path))
#expect(!FileManager.default.fileExists(atPath: bootstrapURL.path))
}
@Test
func needsBootstrapFalseWhenIdentityAlreadySet() throws {
let tmp = FileManager().temporaryDirectory
let tmp = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager().removeItem(at: tmp) }
try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tmp) }
try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
let identityURL = tmp.appendingPathComponent(AgentWorkspace.identityFilename)
try """
# IDENTITY.md - Agent Identity
@@ -6,9 +6,9 @@ import Testing
struct AnthropicAuthResolverTests {
@Test
func prefersOAuthFileOverEnv() throws {
let dir = FileManager().temporaryDirectory
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)", isDirectory: true)
try FileManager().createDirectory(at: dir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let oauthFile = dir.appendingPathComponent("oauth.json")
let payload = [
"anthropic": [
@@ -6,7 +6,7 @@ import Testing
@MainActor
struct CLIInstallerTests {
@Test func installedLocationFindsExecutable() throws {
let fm = FileManager()
let fm = FileManager.default
let root = fm.temporaryDirectory.appendingPathComponent(
"clawdbot-cli-installer-\(UUID().uuidString)")
defer { try? fm.removeItem(at: root) }
@@ -7,13 +7,13 @@ import Testing
private func makeTempDir() throws -> URL {
let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let dir = base.appendingPathComponent("clawdbot-canvaswatch-\(UUID().uuidString)", isDirectory: true)
try FileManager().createDirectory(at: dir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
}
@Test func detectsInPlaceFileWrites() async throws {
let dir = try self.makeTempDir()
defer { try? FileManager().removeItem(at: dir) }
defer { try? FileManager.default.removeItem(at: dir) }
let file = dir.appendingPathComponent("index.html")
try "hello".write(to: file, atomically: false, encoding: .utf8)
@@ -8,10 +8,10 @@ import Testing
@MainActor
struct CanvasWindowSmokeTests {
@Test func panelControllerShowsAndHides() async throws {
let root = FileManager().temporaryDirectory
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-canvas-test-\(UUID().uuidString)")
try FileManager().createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager().removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let anchor = { NSRect(x: 200, y: 400, width: 40, height: 40) }
let controller = try CanvasWindowController(
@@ -31,10 +31,10 @@ struct CanvasWindowSmokeTests {
}
@Test func windowControllerShowsAndCloses() async throws {
let root = FileManager().temporaryDirectory
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-canvas-test-\(UUID().uuidString)")
try FileManager().createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager().removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let controller = try CanvasWindowController(
sessionKey: "main",
@@ -6,7 +6,7 @@ import Testing
struct ClawdbotConfigFileTests {
@Test
func configPathRespectsEnvOverride() async {
let override = FileManager().temporaryDirectory
let override = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
.appendingPathComponent("clawdbot.json")
.path
@@ -19,7 +19,7 @@ struct ClawdbotConfigFileTests {
@MainActor
@Test
func remoteGatewayPortParsesAndMatchesHost() async {
let override = FileManager().temporaryDirectory
let override = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
.appendingPathComponent("clawdbot.json")
.path
@@ -42,7 +42,7 @@ struct ClawdbotConfigFileTests {
@MainActor
@Test
func setRemoteGatewayUrlPreservesScheme() async {
let override = FileManager().temporaryDirectory
let override = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
.appendingPathComponent("clawdbot.json")
.path
@@ -64,7 +64,7 @@ struct ClawdbotConfigFileTests {
@Test
func stateDirOverrideSetsConfigPath() async {
let dir = FileManager().temporaryDirectory
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-state-\(UUID().uuidString)", isDirectory: true)
.path
@@ -6,7 +6,7 @@ import Testing
struct ClawdbotOAuthStoreTests {
@Test
func returnsMissingWhenFileAbsent() {
let url = FileManager().temporaryDirectory
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)")
.appendingPathComponent("oauth.json")
#expect(ClawdbotOAuthStore.anthropicOAuthStatus(at: url) == .missingFile)
@@ -24,7 +24,7 @@ struct ClawdbotOAuthStoreTests {
}
}
let dir = FileManager().temporaryDirectory
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)", isDirectory: true)
setenv(key, dir.path, 1)
@@ -85,9 +85,9 @@ struct ClawdbotOAuthStoreTests {
}
private func writeOAuthFile(_ json: [String: Any]) throws -> URL {
let dir = FileManager().temporaryDirectory
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)", isDirectory: true)
try FileManager().createDirectory(at: dir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let url = dir.appendingPathComponent("oauth.json")
let data = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys])
@@ -12,16 +12,16 @@ import Testing
private func makeTempDir() throws -> URL {
let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let dir = base.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager().createDirectory(at: dir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
}
private func makeExec(at path: URL) throws {
try FileManager().createDirectory(
try FileManager.default.createDirectory(
at: path.deletingLastPathComponent(),
withIntermediateDirectories: true)
FileManager().createFile(atPath: path.path, contents: Data("echo ok\n".utf8))
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path)
FileManager.default.createFile(atPath: path.path, contents: Data("echo ok\n".utf8))
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path)
}
@Test func prefersClawdbotBinary() async throws {
@@ -49,7 +49,7 @@ import Testing
let scriptPath = tmp.appendingPathComponent("bin/clawdbot.js")
try self.makeExec(at: nodePath)
try "#!/bin/sh\necho v22.0.0\n".write(to: nodePath, atomically: true, encoding: .utf8)
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path)
try self.makeExec(at: scriptPath)
let cmd = CommandResolver.clawdbotCommand(
@@ -32,7 +32,7 @@ import Testing
}
private static func swiftFiles(under root: URL) throws -> [URL] {
let fm = FileManager()
let fm = FileManager.default
guard let enumerator = fm.enumerator(at: root, includingPropertiesForKeys: [.isRegularFileKey]) else {
return []
}
@@ -4,7 +4,7 @@ import Testing
@Suite struct GatewayLaunchAgentManagerTests {
@Test func launchAgentPlistSnapshotParsesArgsAndEnv() throws {
let url = FileManager().temporaryDirectory
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-launchd-\(UUID().uuidString).plist")
let plist: [String: Any] = [
"ProgramArguments": ["clawdbot", "gateway-daemon", "--port", "18789", "--bind", "loopback"],
@@ -15,7 +15,7 @@ import Testing
]
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
try data.write(to: url, options: [.atomic])
defer { try? FileManager().removeItem(at: url) }
defer { try? FileManager.default.removeItem(at: url) }
let snapshot = try #require(LaunchAgentPlist.snapshot(url: url))
#expect(snapshot.port == 18789)
@@ -25,14 +25,14 @@ import Testing
}
@Test func launchAgentPlistSnapshotAllowsMissingBind() throws {
let url = FileManager().temporaryDirectory
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-launchd-\(UUID().uuidString).plist")
let plist: [String: Any] = [
"ProgramArguments": ["clawdbot", "gateway-daemon", "--port", "18789"],
]
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
try data.write(to: url, options: [.atomic])
defer { try? FileManager().removeItem(at: url) }
defer { try? FileManager.default.removeItem(at: url) }
let snapshot = try #require(LaunchAgentPlist.snapshot(url: url))
#expect(snapshot.port == 18789)
@@ -5,7 +5,7 @@ import Testing
@Suite struct LogLocatorTests {
@Test func launchdGatewayLogPathEnsuresTmpDirExists() throws {
let fm = FileManager()
let fm = FileManager.default
let baseDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let logDir = baseDir.appendingPathComponent("clawdbot-tests-\(UUID().uuidString)")
@@ -77,9 +77,9 @@ struct LowCoverageHelperTests {
}
@Test func pairedNodesStorePersists() async throws {
let dir = FileManager().temporaryDirectory
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent("paired-\(UUID().uuidString)", isDirectory: true)
try FileManager().createDirectory(at: dir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let url = dir.appendingPathComponent("nodes.json")
let store = PairedNodesStore(fileURL: url)
await store.load()
@@ -143,12 +143,12 @@ struct LowCoverageHelperTests {
}
@Test @MainActor func canvasSchemeHandlerResolvesFilesAndErrors() throws {
let root = FileManager().temporaryDirectory
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("canvas-\(UUID().uuidString)", isDirectory: true)
defer { try? FileManager().removeItem(at: root) }
try FileManager().createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let session = root.appendingPathComponent("main", isDirectory: true)
try FileManager().createDirectory(at: session, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: session, withIntermediateDirectories: true)
let index = session.appendingPathComponent("index.html")
try "<h1>Hello</h1>".write(to: index, atomically: true, encoding: .utf8)
@@ -59,7 +59,7 @@ struct MacNodeRuntimeTests {
includeAudio: Bool?,
outPath: String?) async throws -> (path: String, hasAudio: Bool)
{
let url = FileManager().temporaryDirectory
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-test-screen-record-\(UUID().uuidString).mp4")
try Data("ok".utf8).write(to: url)
return (path: url.path, hasAudio: false)
@@ -19,9 +19,9 @@ struct ModelCatalogLoaderTests {
};
"""
let tmp = FileManager().temporaryDirectory
let tmp = FileManager.default.temporaryDirectory
.appendingPathComponent("models-\(UUID().uuidString).ts")
defer { try? FileManager().removeItem(at: tmp) }
defer { try? FileManager.default.removeItem(at: tmp) }
try src.write(to: tmp, atomically: true, encoding: .utf8)
let choices = try await ModelCatalogLoader.load(from: tmp.path)
@@ -42,9 +42,9 @@ struct ModelCatalogLoaderTests {
@Test
func loadWithNoExportReturnsEmptyChoices() async throws {
let src = "const NOPE = 1;"
let tmp = FileManager().temporaryDirectory
let tmp = FileManager.default.temporaryDirectory
.appendingPathComponent("models-\(UUID().uuidString).ts")
defer { try? FileManager().removeItem(at: tmp) }
defer { try? FileManager.default.removeItem(at: tmp) }
try src.write(to: tmp, atomically: true, encoding: .utf8)
let choices = try await ModelCatalogLoader.load(from: tmp.path)
@@ -6,16 +6,16 @@ import Testing
private func makeTempDir() throws -> URL {
let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let dir = base.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager().createDirectory(at: dir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
}
private func makeExec(at path: URL) throws {
try FileManager().createDirectory(
try FileManager.default.createDirectory(
at: path.deletingLastPathComponent(),
withIntermediateDirectories: true)
FileManager().createFile(atPath: path.path, contents: Data("echo ok\n".utf8))
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path)
FileManager.default.createFile(atPath: path.path, contents: Data("echo ok\n".utf8))
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path)
}
@Test func fnmNodeBinsPreferNewestInstalledVersion() throws {
@@ -37,7 +37,7 @@ import Testing
let home = try self.makeTempDir()
let missingNodeBin = home
.appendingPathComponent(".local/share/fnm/node-versions/v99.0.0/installation/bin")
try FileManager().createDirectory(at: missingNodeBin, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: missingNodeBin, withIntermediateDirectories: true)
let bins = CommandResolver._testNodeManagerBinPaths(home: home)
#expect(!bins.contains(missingNodeBin.path))
@@ -6,10 +6,10 @@ import Testing
private func makeTempExecutable(contents: String) throws -> URL {
let dir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager().createDirectory(at: dir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let path = dir.appendingPathComponent("node")
try contents.write(to: path, atomically: true, encoding: .utf8)
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path)
return path
}
@@ -109,7 +109,7 @@ enum TestIsolation {
}
nonisolated static func tempConfigPath() -> String {
FileManager().temporaryDirectory
FileManager.default.temporaryDirectory
.appendingPathComponent("clawdbot-test-config-\(UUID().uuidString).json")
.path
}
@@ -47,17 +47,17 @@ import Testing
.appendingPathComponent(UUID().uuidString, isDirectory: true)
let dist = tmp.appendingPathComponent("dist/index.js")
let bin = tmp.appendingPathComponent("bin/clawdbot.js")
try FileManager().createDirectory(at: dist.deletingLastPathComponent(), withIntermediateDirectories: true)
try FileManager().createDirectory(at: bin.deletingLastPathComponent(), withIntermediateDirectories: true)
FileManager().createFile(atPath: dist.path, contents: Data())
FileManager().createFile(atPath: bin.path, contents: Data())
try FileManager.default.createDirectory(at: dist.deletingLastPathComponent(), withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: bin.deletingLastPathComponent(), withIntermediateDirectories: true)
FileManager.default.createFile(atPath: dist.path, contents: Data())
FileManager.default.createFile(atPath: bin.path, contents: Data())
let entry = CommandResolver.gatewayEntrypoint(in: tmp)
#expect(entry == dist.path)
}
@Test func logLocatorPicksNewestLogFile() throws {
let fm = FileManager()
let fm = FileManager.default
let dir = URL(fileURLWithPath: "/tmp/clawdbot", isDirectory: true)
try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
@@ -2,7 +2,7 @@ import Foundation
public enum ClawdbotNodeStorage {
public static func appSupportDir() throws -> URL {
let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
guard let base else {
throw NSError(domain: "ClawdbotNodeStorage", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Application Support directory unavailable",
@@ -19,7 +19,7 @@ public enum ClawdbotNodeStorage {
}
public static func cachesDir() throws -> URL {
let base = FileManager().urls(for: .cachesDirectory, in: .userDomainMask).first
let base = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
guard let base else {
throw NSError(domain: "ClawdbotNodeStorage", code: 2, userInfo: [
NSLocalizedDescriptionKey: "Caches directory unavailable",
+1 -7
View File
@@ -28,11 +28,9 @@ export AWS_REGION="us-east-1"
# Optional:
export AWS_SESSION_TOKEN="..."
export AWS_PROFILE="your-profile"
# Optional (Bedrock API key/bearer token):
export AWS_BEARER_TOKEN_BEDROCK="..."
```
2) Add a Bedrock provider and model to your config (no `apiKey` required):
2) Add a Bedrock provider and model to your config:
```json5
{
@@ -41,7 +39,6 @@ export AWS_BEARER_TOKEN_BEDROCK="..."
"amazon-bedrock": {
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
api: "bedrock-converse-stream",
auth: "aws-sdk",
models: [
{
id: "anthropic.claude-3-7-sonnet-20250219-v1:0",
@@ -68,9 +65,6 @@ export AWS_BEARER_TOKEN_BEDROCK="..."
- Bedrock requires **model access** enabled in your AWS account/region.
- If you use profiles, set `AWS_PROFILE` on the gateway host.
- Clawdbot surfaces the credential source in this order: `AWS_BEARER_TOKEN_BEDROCK`,
then `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, then `AWS_PROFILE`, then the
default AWS SDK chain.
- Reasoning support depends on the model; check the Bedrock model card for
current capabilities.
- If you prefer a managed key flow, you can also place an OpenAI‑compatible
-1
View File
@@ -73,7 +73,6 @@ These run inside the agent loop or gateway pipeline:
- **`agent_end`**: inspect the final message list and run metadata after completion.
- **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles.
- **`before_tool_call` / `after_tool_call`**: intercept tool params/results.
- **`tool_result_persist`**: synchronously transform tool results before they are written to the session transcript.
- **`message_received` / `message_sending` / `message_sent`**: inbound + outbound message hooks.
- **`session_start` / `session_end`**: session lifecycle boundaries.
- **`gateway_start` / `gateway_stop`**: gateway lifecycle events.
+2 -2
View File
@@ -117,8 +117,8 @@ Send these as standalone messages so they register.
```
## Inspecting
- `clawdbot status` — shows store path and recent sessions.
- `clawdbot sessions --json` — dumps every entry (filter with `--active <minutes>`).
- `pnpm clawdbot status` — shows store path and recent sessions.
- `pnpm clawdbot sessions --json` — dumps every entry (filter with `--active <minutes>`).
- `clawdbot gateway call sessions.list --params '{}'` — fetch sessions from the running gateway (use `--url`/`--token` for remote gateway access).
- Send `/status` as a standalone message in chat to see whether the agent is reachable, how much of the session context is used, current thinking/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs).
- Send `/context list` or `/context detail` to see what’s in the system prompt and injected workspace files (and the biggest context contributors).
+1 -1
View File
@@ -48,7 +48,7 @@ node --import tsx scripts/repro/tsx-name-repro.ts
## Regression history
- `2871657e` (2026-01-06): scripts changed from Bun to tsx to make Bun optional.
- Before that (Bun path), `clawdbot status` and `gateway:watch` worked.
- Before that (Bun path), `pnpm clawdbot status` and `gateway:watch` worked.
## Workarounds
- Use Bun for dev scripts (current temporary revert).
+2 -4
View File
@@ -59,11 +59,9 @@ Recommended flow (dev profile + dev bootstrap):
```bash
pnpm gateway:dev
CLAWDBOT_PROFILE=dev clawdbot tui
CLAWDBOT_PROFILE=dev pnpm clawdbot tui
```
If you don’t have a global install yet, run the CLI via `pnpm clawdbot ...`.
What this does:
1) **Profile isolation** (global `--dev`)
@@ -91,7 +89,7 @@ Note: `--dev` is a **global** profile flag and gets eaten by some runners.
If you need to spell it out, use the env var form:
```bash
CLAWDBOT_PROFILE=dev clawdbot gateway --dev --reset
CLAWDBOT_PROFILE=dev pnpm clawdbot gateway --dev --reset
```
`--reset` wipes config, credentials, sessions, and the dev workspace (using
@@ -1,122 +0,0 @@
---
summary: "Plan: Add OpenResponses /v1/responses endpoint and deprecate chat completions cleanly"
owner: "clawdbot"
status: "draft"
last_updated: "2026-01-19"
---
# OpenResponses Gateway Integration Plan
## Context
Clawdbot Gateway currently exposes a minimal OpenAI-compatible Chat Completions endpoint at
`/v1/chat/completions` (see [OpenAI Chat Completions](/gateway/openai-http-api)).
Open Responses is an open inference standard based on the OpenAI Responses API. It is designed
for agentic workflows and uses item-based inputs plus semantic streaming events. The OpenResponses
spec defines `/v1/responses`, not `/v1/chat/completions`.
## Goals
- Add a `/v1/responses` endpoint that adheres to OpenResponses semantics.
- Keep Chat Completions as a compatibility layer that is easy to disable and eventually remove.
- Standardize validation and parsing with isolated, reusable schemas.
## Non-goals
- Full OpenResponses feature parity in the first pass (images, files, hosted tools).
- Replacing internal agent execution logic or tool orchestration.
- Changing the existing `/v1/chat/completions` behavior during the first phase.
## Research Summary
Sources: OpenResponses OpenAPI, OpenResponses specification site, and the Hugging Face blog post.
Key points extracted:
- `POST /v1/responses` accepts `CreateResponseBody` fields like `model`, `input` (string or
`ItemParam[]`), `instructions`, `tools`, `tool_choice`, `stream`, `max_output_tokens`, and
`max_tool_calls`.
- `ItemParam` is a discriminated union of:
- `message` items with roles `system`, `developer`, `user`, `assistant`
- `function_call` and `function_call_output`
- `reasoning`
- `item_reference`
- Successful responses return a `ResponseResource` with `object: "response"`, `status`, and
`output` items.
- Streaming uses semantic events such as:
- `response.created`, `response.in_progress`, `response.completed`, `response.failed`
- `response.output_item.added`, `response.output_item.done`
- `response.content_part.added`, `response.content_part.done`
- `response.output_text.delta`, `response.output_text.done`
- The spec requires:
- `Content-Type: text/event-stream`
- `event:` must match the JSON `type` field
- terminal event must be literal `[DONE]`
- Reasoning items may expose `content`, `encrypted_content`, and `summary`.
- HF examples include `OpenResponses-Version: latest` in requests (optional header).
## Proposed Architecture
- Add `src/gateway/open-responses.schema.ts` containing Zod schemas only (no gateway imports).
- Add `src/gateway/openresponses-http.ts` (or `open-responses-http.ts`) for `/v1/responses`.
- Keep `src/gateway/openai-http.ts` intact as a legacy compatibility adapter.
- Add config `gateway.http.endpoints.responses.enabled` (default `false`).
- Keep `gateway.http.endpoints.chatCompletions.enabled` independent; allow both endpoints to be
toggled separately.
- Emit a startup warning when Chat Completions is enabled to signal legacy status.
## Deprecation Path for Chat Completions
- Maintain strict module boundaries: no shared schema types between responses and chat completions.
- Make Chat Completions opt-in by config so it can be disabled without code changes.
- Update docs to label Chat Completions as legacy once `/v1/responses` is stable.
- Optional future step: map Chat Completions requests to the Responses handler for a simpler
removal path.
## Phase 1 Support Subset
- Accept `input` as string or `ItemParam[]` with message roles and `function_call_output`.
- Extract system and developer messages into `extraSystemPrompt`.
- Use the most recent `user` or `function_call_output` as the current message for agent runs.
- Reject unsupported content parts (image/file) with `invalid_request_error`.
- Return a single assistant message with `output_text` content.
- Return `usage` with zeroed values until token accounting is wired.
## Validation Strategy (No SDK)
- Implement Zod schemas for the supported subset of:
- `CreateResponseBody`
- `ItemParam` + message content part unions
- `ResponseResource`
- Streaming event shapes used by the gateway
- Keep schemas in a single, isolated module to avoid drift and allow future codegen.
## Streaming Implementation (Phase 1)
- SSE lines with both `event:` and `data:`.
- Required sequence (minimum viable):
- `response.created`
- `response.output_item.added`
- `response.content_part.added`
- `response.output_text.delta` (repeat as needed)
- `response.output_text.done`
- `response.content_part.done`
- `response.completed`
- `[DONE]`
## Tests and Verification Plan
- Add e2e coverage for `/v1/responses`:
- Auth required
- Non-stream response shape
- Stream event ordering and `[DONE]`
- Session routing with headers and `user`
- Keep `src/gateway/openai-http.e2e.test.ts` unchanged.
- Manual: curl to `/v1/responses` with `stream: true` and verify event ordering and terminal
`[DONE]`.
## Doc Updates (Follow-up)
- Add a new docs page for `/v1/responses` usage and examples.
- Update `/gateway/openai-http-api` with a legacy note and pointer to `/v1/responses`.
-1
View File
@@ -2669,7 +2669,6 @@ Notes:
- `clawdbot gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
- `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).
- OpenAI Chat Completions endpoint: **disabled by default**; enable with `gateway.http.endpoints.chatCompletions.enabled: true`.
- OpenResponses endpoint: **disabled by default**; enable with `gateway.http.endpoints.responses.enabled: true`.
- Precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`.
- Non-loopback binds (`lan`/`tailnet`/`auto`) require auth. Use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
- The onboarding wizard generates a gateway token by default (even on loopback).
-1
View File
@@ -29,7 +29,6 @@ pnpm gateway:watch
- Binds WebSocket control plane to `127.0.0.1:<port>` (default 18789).
- The same port also serves HTTP (control UI, hooks, A2UI). Single-port multiplex.
- OpenAI Chat Completions (HTTP): [`/v1/chat/completions`](/gateway/openai-http-api).
- OpenResponses (HTTP): [`/v1/responses`](/gateway/openresponses-http-api).
- Starts a Canvas file server by default on `canvasHost.port` (default `18793`), serving `http://<gateway-host>:18793/__clawdbot__/canvas/` from `~/clawd/canvas`. Disable with `canvasHost.enabled=false` or `CLAWDBOT_SKIP_CANVAS_HOST=1`.
- Logs to stdout; use launchd/systemd to keep it alive and rotate logs.
- Pass `--verbose` to mirror debug logging (handshakes, req/res, events) from the log file into stdio when troubleshooting.
-298
View File
@@ -1,298 +0,0 @@
---
summary: "Expose an OpenResponses-compatible /v1/responses HTTP endpoint from the Gateway"
read_when:
- Integrating clients that speak the OpenResponses API
- You want item-based inputs, client tool calls, or SSE events
---
# OpenResponses API (HTTP)
Clawdbot’s Gateway can serve an OpenResponses-compatible `POST /v1/responses` endpoint.
This endpoint is **disabled by default**. Enable it in config first.
- `POST /v1/responses`
- Same port as the Gateway (WS + HTTP multiplex): `http://<gateway-host>:<port>/v1/responses`
Under the hood, requests are executed as a normal Gateway agent run (same codepath as
`clawdbot agent`), so routing/permissions/config match your Gateway.
## Authentication
Uses the Gateway auth configuration. Send a bearer token:
- `Authorization: Bearer <token>`
Notes:
- When `gateway.auth.mode="token"`, use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
- When `gateway.auth.mode="password"`, use `gateway.auth.password` (or `CLAWDBOT_GATEWAY_PASSWORD`).
## Choosing an agent
No custom headers required: encode the agent id in the OpenResponses `model` field:
- `model: "clawdbot:<agentId>"` (example: `"clawdbot:main"`, `"clawdbot:beta"`)
- `model: "agent:<agentId>"` (alias)
Or target a specific Clawdbot agent by header:
- `x-clawdbot-agent-id: <agentId>` (default: `main`)
Advanced:
- `x-clawdbot-session-key: <sessionKey>` to fully control session routing.
## Enabling the endpoint
Set `gateway.http.endpoints.responses.enabled` to `true`:
```json5
{
gateway: {
http: {
endpoints: {
responses: { enabled: true }
}
}
}
}
```
## Disabling the endpoint
Set `gateway.http.endpoints.responses.enabled` to `false`:
```json5
{
gateway: {
http: {
endpoints: {
responses: { enabled: false }
}
}
}
}
```
## Session behavior
By default the endpoint is **stateless per request** (a new session key is generated each call).
If the request includes an OpenResponses `user` string, the Gateway derives a stable session key
from it, so repeated calls can share an agent session.
## Request shape (supported)
The request follows the OpenResponses API with item-based input. Current support:
- `input`: string or array of item objects.
- `instructions`: merged into the system prompt.
- `tools`: client tool definitions (function tools).
- `tool_choice`: filter or require client tools.
- `stream`: enables SSE streaming.
- `max_output_tokens`: best-effort output limit (provider dependent).
- `user`: stable session routing.
Accepted but **currently ignored**:
- `max_tool_calls`
- `reasoning`
- `metadata`
- `store`
- `previous_response_id`
- `truncation`
## Items (input)
### `message`
Roles: `system`, `developer`, `user`, `assistant`.
- `system` and `developer` are appended to the system prompt.
- The most recent `user` or `function_call_output` item becomes the “current message.”
- Earlier user/assistant messages are included as history for context.
### `function_call_output` (turn-based tools)
Send tool results back to the model:
```json
{
"type": "function_call_output",
"call_id": "call_123",
"output": "{\"temperature\": \"72F\"}"
}
```
### `reasoning` and `item_reference`
Accepted for schema compatibility but ignored when building the prompt.
## Tools (client-side function tools)
Provide tools with `tools: [{ type: "function", function: { name, description?, parameters? } }]`.
If the agent decides to call a tool, the response returns a `function_call` output item.
You then send a follow-up request with `function_call_output` to continue the turn.
## Images (`input_image`)
Supports base64 or URL sources:
```json
{
"type": "input_image",
"source": { "type": "url", "url": "https://example.com/image.png" }
}
```
Allowed MIME types (current): `image/jpeg`, `image/png`, `image/gif`, `image/webp`.
Max size (current): 10MB.
## Files (`input_file`)
Supports base64 or URL sources:
```json
{
"type": "input_file",
"source": {
"type": "base64",
"media_type": "text/plain",
"data": "SGVsbG8gV29ybGQh",
"filename": "hello.txt"
}
}
```
Allowed MIME types (current): `text/plain`, `text/markdown`, `text/html`, `text/csv`,
`application/json`, `application/pdf`.
Max size (current): 5MB.
Current behavior:
- File content is decoded and added to the **system prompt**, not the user message,
so it stays ephemeral (not persisted in session history).
- PDFs are parsed for text. If little text is found, the first pages are rasterized
into images and passed to the model.
PDF parsing uses the Node-friendly `pdfjs-dist` legacy build (no worker). The modern
PDF.js build expects browser workers/DOM globals, so it is not used in the Gateway.
URL fetch defaults:
- `files.allowUrl`: `true`
- `images.allowUrl`: `true`
- Requests are guarded (DNS resolution, private IP blocking, redirect caps, timeouts).
## File + image limits (config)
Defaults can be tuned under `gateway.http.endpoints.responses`:
```json5
{
gateway: {
http: {
endpoints: {
responses: {
enabled: true,
maxBodyBytes: 20000000,
files: {
allowUrl: true,
allowedMimes: ["text/plain", "text/markdown", "text/html", "text/csv", "application/json", "application/pdf"],
maxBytes: 5242880,
maxChars: 200000,
maxRedirects: 3,
timeoutMs: 10000,
pdf: {
maxPages: 4,
maxPixels: 4000000,
minTextChars: 200
}
},
images: {
allowUrl: true,
allowedMimes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
maxBytes: 10485760,
maxRedirects: 3,
timeoutMs: 10000
}
}
}
}
}
}
```
Defaults when omitted:
- `maxBodyBytes`: 20MB
- `files.maxBytes`: 5MB
- `files.maxChars`: 200k
- `files.maxRedirects`: 3
- `files.timeoutMs`: 10s
- `files.pdf.maxPages`: 4
- `files.pdf.maxPixels`: 4,000,000
- `files.pdf.minTextChars`: 200
- `images.maxBytes`: 10MB
- `images.maxRedirects`: 3
- `images.timeoutMs`: 10s
## Streaming (SSE)
Set `stream: true` to receive Server-Sent Events (SSE):
- `Content-Type: text/event-stream`
- Each event line is `event: <type>` and `data: <json>`
- Stream ends with `data: [DONE]`
Event types currently emitted:
- `response.created`
- `response.in_progress`
- `response.output_item.added`
- `response.content_part.added`
- `response.output_text.delta`
- `response.output_text.done`
- `response.content_part.done`
- `response.output_item.done`
- `response.completed`
- `response.failed` (on error)
## Usage
`usage` is populated when the underlying provider reports token counts.
## Errors
Errors use a JSON object like:
```json
{ "error": { "message": "...", "type": "invalid_request_error" } }
```
Common cases:
- `401` missing/invalid auth
- `400` invalid request body
- `405` wrong method
## Examples
Non-streaming:
```bash
curl -sS http://127.0.0.1:18789/v1/responses \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'x-clawdbot-agent-id: main' \
-d '{
"model": "clawdbot",
"input": "hi"
}'
```
Streaming:
```bash
curl -N http://127.0.0.1:18789/v1/responses \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'x-clawdbot-agent-id: main' \
-d '{
"model": "clawdbot",
"stream": true,
"input": "hi"
}'
```
+2 -2
View File
@@ -378,7 +378,7 @@ clawdbot channels login
### Build errors on `main` — what’s the standard fix path?
1) `git pull origin main && pnpm install`
2) `clawdbot doctor`
2) `pnpm clawdbot doctor`
3) Check GitHub issues or Discord
4) Temporary workaround: check out an older commit
@@ -392,7 +392,7 @@ Typical recovery:
git status # ensure you’re in the repo root
pnpm install
pnpm build
clawdbot doctor
pnpm clawdbot doctor
clawdbot daemon restart
```
+1 -3
View File
@@ -123,11 +123,9 @@ cd clawdbot
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
clawdbot onboard --install-daemon
pnpm clawdbot onboard --install-daemon
```
If you don’t have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo.
Multi-instance quickstart (optional):
```bash
+42 -98
View File
@@ -7,43 +7,40 @@ read_when:
# Install
Use the installer unless you have a reason not to. It sets up the CLI and runs onboarding.
Runtime baseline: **Node >=22**.
## Quick install (recommended)
If the installer says it succeeded but you later see `clawdbot: command not found`, it’s usually a Node/npm PATH issue (global npm bin dir not on PATH). See the section below.
## Node.js + npm (PATH sanity)
Quick diagnosis:
```bash
node -v
npm -v
npm bin -g
echo "$PATH"
```
If the output of `npm bin -g` is **not** present inside `echo "$PATH"`, your shell can’t find global npm binaries (including `clawdbot`).
Fix: add it to your shell startup file (zsh: `~/.zshrc`, bash: `~/.bashrc`):
```bash
export PATH="/path/from/npm/bin/-g:$PATH"
```
Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
## Recommended (installer script)
```bash
curl -fsSL https://clawd.bot/install.sh | bash
```
Windows (PowerShell):
This installs the `clawdbot` CLI globally via npm and then starts onboarding.
```powershell
iwr -useb https://clawd.bot/install.ps1 | iex
```
Next step (if you skipped onboarding):
```bash
clawdbot onboard --install-daemon
```
## System requirements
- **Node >=22**
- macOS, Linux, or Windows via WSL2
- `pnpm` only if you build from source
## Choose your install path
### 1) Installer script (recommended)
Installs `clawdbot` globally via npm and runs onboarding.
```bash
curl -fsSL https://clawd.bot/install.sh | bash
```
Installer flags:
See installer flags:
```bash
curl -fsSL https://clawd.bot/install.sh | bash -s -- --help
@@ -57,60 +54,7 @@ Non-interactive (skip onboarding):
curl -fsSL https://clawd.bot/install.sh | bash -s -- --no-onboard
```
### 2) Global install (manual)
If you already have Node:
```bash
npm install -g clawdbot@latest
```
If you have libvips installed globally (common on macOS via Homebrew) and `sharp` fails to install, force prebuilt binaries:
```bash
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g clawdbot@latest
```
Or:
```bash
pnpm add -g clawdbot@latest
```
Then:
```bash
clawdbot onboard --install-daemon
```
### 3) From source (contributors/dev)
```bash
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
clawdbot onboard --install-daemon
```
Tip: if you don’t have a global install yet, run repo commands via `pnpm clawdbot ...`.
### 4) Other install options
- Docker: [Docker](/install/docker)
- Nix: [Nix](/install/nix)
- Ansible: [Ansible](/install/ansible)
- Bun (CLI only): [Bun](/install/bun)
## After install
- Run onboarding: `clawdbot onboard --install-daemon`
- Quick check: `clawdbot doctor`
- Check gateway health: `clawdbot status` + `clawdbot health`
- Open the dashboard: `clawdbot dashboard`
## Install method: npm vs git (installer)
## Install method: npm vs git
The installer supports two methods:
@@ -148,28 +92,28 @@ Equivalent env vars (useful for automation):
- `CLAWDBOT_NO_ONBOARD=1`
- `SHARP_IGNORE_GLOBAL_LIBVIPS=0|1` (default: `1`; avoids `sharp` building against system libvips)
## Troubleshooting: `clawdbot` not found (PATH)
## Global install (manual)
Quick diagnosis:
If you already have Node:
```bash
node -v
npm -v
npm bin -g
echo "$PATH"
npm install -g clawdbot@latest
```
If the output of `npm bin -g` is **not** present inside `echo "$PATH"`, your shell can’t find global npm binaries (including `clawdbot`).
Fix: add it to your shell startup file (zsh: `~/.zshrc`, bash: `~/.bashrc`):
If you have libvips installed globally (common on macOS via Homebrew) and `sharp` fails to install, force prebuilt binaries:
```bash
export PATH="/path/from/npm/bin/-g:$PATH"
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g clawdbot@latest
```
Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
Or:
## Update / uninstall
```bash
pnpm add -g clawdbot@latest
```
- Updates: [Updating](/install/updating)
- Uninstall: [Uninstall](/install/uninstall)
Then:
```bash
clawdbot onboard --install-daemon
```
+1 -1
View File
@@ -118,7 +118,7 @@ Remove it with `npm rm -g clawdbot` (or `pnpm remove -g` / `bun remove -g` if yo
### Source checkout (git clone)
If you run from a repo checkout (`git clone` + `clawdbot ...` / `bun run clawdbot ...`):
If you run from a repo checkout (`git clone` + `pnpm clawdbot ...` / `bun run clawdbot ...`):
1) Uninstall the gateway service **before** deleting the repo (use the easy path above or manual service removal).
2) Delete the repo directory.
+2 -3
View File
@@ -119,13 +119,12 @@ git pull
pnpm install
pnpm build
pnpm ui:build # auto-installs UI deps on first run
clawdbot doctor
clawdbot health
pnpm clawdbot doctor
pnpm clawdbot health
```
Notes:
- `pnpm build` matters when you run the packaged `clawdbot` binary ([`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js)) or use Node to run `dist/`.
- If you run from a repo checkout without a global install, use `pnpm clawdbot ...` for CLI commands.
- If you run directly from TypeScript (`pnpm clawdbot ...`), a rebuild is usually unnecessary, but **config migrations still apply** → run doctor.
- Switching between global and git installs is easy: install the other flavor, then run `clawdbot doctor` so the gateway service entrypoint is rewritten to the current install.
+2 -5
View File
@@ -64,11 +64,8 @@ If both `PERPLEXITY_API_KEY` and `OPENROUTER_API_KEY` are set, set
`tools.web.search.perplexity.baseUrl` (or `tools.web.search.perplexity.apiKey`)
to disambiguate.
If no base URL is set, Clawdbot chooses a default based on the API key source:
- `PERPLEXITY_API_KEY` or `pplx-...` → direct Perplexity (`https://api.perplexity.ai`)
- `OPENROUTER_API_KEY` or `sk-or-...` → OpenRouter (`https://openrouter.ai/api/v1`)
- Unknown key formats → OpenRouter (safe fallback)
If `PERPLEXITY_API_KEY` is used from the environment and no base URL is set,
Clawdbot defaults to the direct Perplexity endpoint. Set `baseUrl` to override.
## Models
+1 -1
View File
@@ -155,7 +155,7 @@ Options:
- `--timeout <ms>`: overall discovery window (default `2000`)
- `--json`: structured output for diffing
Tip: compare against `clawdbot gateway discover --json` to see whether the
Tip: compare against `pnpm clawdbot gateway discover --json` to see whether the
macOS app’s discovery pipeline (NWBrowser + tailnet DNS‑SD fallback) differs from
the Node CLI’s `dns-sd` based discovery.
+1 -1
View File
@@ -97,7 +97,7 @@ cd clawdbot
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
clawdbot onboard
pnpm clawdbot onboard
```
Full guide: [Getting Started](/start/getting-started)
-1
View File
@@ -31,7 +31,6 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/etc.)? See [Chann
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
- [OpenCode Zen](/providers/opencode)
- [Amazon Bedrock](/bedrock)
- [Z.AI](/providers/zai)
- [GLM models](/providers/glm)
- [MiniMax](/providers/minimax)
+13 -16
View File
@@ -184,25 +184,22 @@ Clawdbot is a personal AI assistant you run on your own devices. It replies on t
The repo recommends running from source and using the onboarding wizard:
```bash
curl -fsSL https://clawd.bot/install.sh | bash
clawdbot onboard --install-daemon
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
pnpm install
# Optional if you want built output / global linking:
pnpm build
# If the Control UI assets are missing or you want the dashboard:
pnpm ui:build # auto-installs UI deps on first run
pnpm clawdbot onboard
```
The wizard can also build UI assets automatically. After onboarding, you typically run the Gateway on port **18789**.
From source (contributors/dev):
```bash
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
pnpm install
pnpm build
pnpm ui:build # auto-installs UI deps on first run
clawdbot onboard
```
If you don’t have a global install yet, run it via `pnpm clawdbot onboard`.
### How do I open the dashboard after onboarding?
The wizard now opens your browser with a tokenized dashboard URL right after onboarding and also prints the full link (with token) in the summary. Keep that tab open; if it didn’t launch, copy/paste the printed URL on the same machine. Tokens stay local to your host—nothing is fetched from the browser.
@@ -333,7 +330,7 @@ git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
pnpm install
pnpm build
clawdbot doctor
pnpm clawdbot doctor
clawdbot daemon restart
```
+5 -12
View File
@@ -116,13 +116,6 @@ If a token is configured, paste it into the Control UI settings (stored as `conn
⚠️ **Bun warning (WhatsApp + Telegram):** Bun has known issues with these
channels. If you use WhatsApp or Telegram, run the Gateway with **Node**.
## 3.5) Quick verify (2 min)
```bash
clawdbot status
clawdbot health
```
## 4) Pair + connect your first chat surface
### WhatsApp (QR login)
@@ -165,11 +158,9 @@ cd clawdbot
pnpm install
pnpm ui:build # auto-installs UI deps on first run
pnpm build
clawdbot onboard --install-daemon
pnpm clawdbot onboard --install-daemon
```
If you don’t have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo.
Gateway (from this repo):
```bash
@@ -178,13 +169,15 @@ node dist/entry.js gateway --port 18789 --verbose
## 7) Verify end-to-end
In a new terminal, send a test message:
In a new terminal:
```bash
clawdbot status
clawdbot health
clawdbot message send --target +15555550123 --message "Hello from Clawdbot"
```
If `clawdbot health` shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent won’t be able to respond without it.
If `health` shows “no auth configured”, go back to the wizard and set OAuth/key auth — the agent won’t be able to respond without it.
Tip: `clawdbot status --all` is the best pasteable, read-only debug report.
Health probes: `clawdbot health` (or `clawdbot status --deep`) asks the running gateway for a health snapshot.
+2 -4
View File
@@ -35,11 +35,9 @@ clawdbot setup
From inside this repo, use the local CLI entry:
```bash
clawdbot setup
pnpm clawdbot setup
```
If you don’t have a global install yet, run it via `pnpm clawdbot setup`.
## Stable workflow (macOS app first)
1) Install + launch **Clawdbot.app** (menu bar).
@@ -94,7 +92,7 @@ The app will attach to the running gateway on the configured port.
- Or via CLI:
```bash
clawdbot health
pnpm clawdbot health
```
### Common footguns
+2 -2
View File
@@ -150,8 +150,8 @@ Live tests are split into two layers so we can isolate failures:
Tip: to see what you can test on your machine (and the exact `provider/model` ids), run:
```bash
clawdbot models list
clawdbot models list --json
pnpm clawdbot models list
pnpm clawdbot models list --json
```
## Live: Anthropic setup-token smoke
-8
View File
@@ -82,14 +82,6 @@ Notes:
- `homepage` — URL surfaced as “Website” in the macOS Skills UI (also supported via `metadata.clawdbot.homepage`).
- `user-invocable` — `true|false` (default: `true`). When `true`, the skill is exposed as a user slash command.
- `disable-model-invocation` — `true|false` (default: `false`). When `true`, the skill is excluded from the model prompt (still available via user invocation).
- `command-dispatch` — `tool` (optional). When set to `tool`, the slash command bypasses the model and dispatches directly to a tool.
- `command-tool` — tool name to invoke when `command-dispatch: tool` is set.
- `command-arg-mode` — `raw` (default). For tool dispatch, forwards the raw args string to the tool (no core parsing).
The tool is invoked with params:
`{ command: "<raw args>", commandName: "<slash command>", skillName: "<skill name>" }`.
Tool-dispatch commands still respect tool policies/sandbox rules (same as normal model tools).
Replies are derived from tool results; include `MEDIA:` tokens or media URLs in tool output to send attachments.
## Gating (load-time filters)
-3
View File
@@ -102,9 +102,6 @@ Notes:
- Currently: `/help`, `/commands`, `/status`, `/whoami` (`/id`).
- Unauthorized command-only messages are silently ignored, and inline `/...` tokens are treated as plain text.
- **Skill commands:** `user-invocable` skills are exposed as slash commands. Names are sanitized to `a-z0-9_` (max 32 chars); collisions get numeric suffixes (e.g. `_2`).
- By default, skill commands are forwarded to the model as a normal request.
- Skills may optionally declare `command-dispatch: tool` to route the command directly to a tool (deterministic, no model).
- Tool-dispatch still respects tool policies/sandbox rules; replies use the tool result text/media (including `MEDIA:` tokens).
- **Native command arguments:** Discord uses autocomplete for dynamic options (and button menus when you omit required args). Telegram and Slack show a button menu when a command supports choices and you omit the arg.
## Usage surfaces (what shows where)
+5 -8
View File
@@ -10,7 +10,7 @@ read_when:
Clawdbot ships two lightweight web tools:
- `web_search` — Search the web via Brave Search API (default) or Perplexity Sonar (direct or via OpenRouter).
- `web_search` — Search the web via Brave Search API (default) or Perplexity Sonar (via OpenRouter).
- `web_fetch` — HTTP fetch + readable extraction (HTML → markdown/text).
These are **not** browser automation. For JS-heavy sites or logins, use the
@@ -31,7 +31,7 @@ These are **not** browser automation. For JS-heavy sites or logins, use the
| Provider | Pros | Cons | API Key |
|----------|------|------|---------|
| **Brave** (default) | Fast, structured results, free tier | Traditional search results | `BRAVE_API_KEY` |
| **Perplexity** | AI-synthesized answers, citations, real-time | Requires Perplexity or OpenRouter access | `OPENROUTER_API_KEY` or `PERPLEXITY_API_KEY` |
| **Perplexity** | AI-synthesized answers, citations, real-time | Requires OpenRouter credits | `OPENROUTER_API_KEY` or `PERPLEXITY_API_KEY` |
See [Brave Search setup](/brave-search) and [Perplexity Sonar](/perplexity) for provider-specific details.
@@ -110,7 +110,7 @@ crypto/prepaid).
perplexity: {
// API key (optional if OPENROUTER_API_KEY or PERPLEXITY_API_KEY is set)
apiKey: "sk-or-v1-...",
// Base URL (key-aware default if omitted)
// Base URL (defaults to OpenRouter)
baseUrl: "https://openrouter.ai/api/v1",
// Model (defaults to perplexity/sonar-pro)
model: "perplexity/sonar-pro"
@@ -124,11 +124,8 @@ crypto/prepaid).
**Environment alternative:** set `OPENROUTER_API_KEY` or `PERPLEXITY_API_KEY` in the Gateway
environment. For a daemon install, put it in `~/.clawdbot/.env`.
If no base URL is set, Clawdbot chooses a default based on the API key source:
- `PERPLEXITY_API_KEY` or `pplx-...` → `https://api.perplexity.ai`
- `OPENROUTER_API_KEY` or `sk-or-...` → `https://openrouter.ai/api/v1`
- Unknown key formats → OpenRouter (safe fallback)
If `PERPLEXITY_API_KEY` is used from the environment and no base URL is set,
Clawdbot defaults to the direct Perplexity endpoint (`https://api.perplexity.ai`).
### Available Perplexity models
-2
View File
@@ -155,7 +155,6 @@
"@mariozechner/pi-coding-agent": "^0.46.0",
"@mariozechner/pi-tui": "^0.46.0",
"@mozilla/readability": "^0.6.0",
"@napi-rs/canvas": "^0.1.88",
"@sinclair/typebox": "0.34.47",
"@slack/bolt": "^4.6.0",
"@slack/web-api": "^7.13.0",
@@ -182,7 +181,6 @@
"long": "5.3.2",
"markdown-it": "^14.1.0",
"osc-progress": "^0.2.0",
"pdfjs-dist": "^5.4.530",
"playwright-core": "1.57.0",
"proper-lockfile": "^4.1.2",
"qrcode-terminal": "^0.12.0",
-131
View File
@@ -49,9 +49,6 @@ importers:
'@mozilla/readability':
specifier: ^0.6.0
version: 0.6.0
'@napi-rs/canvas':
specifier: ^0.1.88
version: 0.1.88
'@sinclair/typebox':
specifier: 0.34.47
version: 0.34.47
@@ -130,9 +127,6 @@ importers:
osc-progress:
specifier: ^0.2.0
version: 0.2.0
pdfjs-dist:
specifier: ^5.4.530
version: 5.4.530
playwright-core:
specifier: 1.57.0
version: 1.57.0
@@ -1211,76 +1205,6 @@ packages:
resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==}
engines: {node: '>=14.0.0'}
'@napi-rs/canvas-android-arm64@0.1.88':
resolution: {integrity: sha512-KEaClPnZuVxJ8smUWjV1wWFkByBO/D+vy4lN+Dm5DFH514oqwukxKGeck9xcKJhaWJGjfruGmYGiwRe//+/zQQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.88':
resolution: {integrity: sha512-Xgywz0dDxOKSgx3eZnK85WgGMmGrQEW7ZLA/E7raZdlEE+xXCozobgqz2ZvYigpB6DJFYkqnwHjqCOTSDGlFdg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.88':
resolution: {integrity: sha512-Yz4wSCIQOUgNucgk+8NFtQxQxZV5NO8VKRl9ePKE6XoNyNVC8JDqtvhh3b3TPqKK8W5p2EQpAr1rjjm0mfBxdg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.88':
resolution: {integrity: sha512-9gQM2SlTo76hYhxHi2XxWTAqpTOb+JtxMPEIr+H5nAhHhyEtNmTSDRtz93SP7mGd2G3Ojf2oF5tP9OdgtgXyKg==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.88':
resolution: {integrity: sha512-7qgaOBMXuVRk9Fzztzr3BchQKXDxGbY+nwsovD3I/Sx81e+sX0ReEDYHTItNb0Je4NHbAl7D0MKyd4SvUc04sg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@napi-rs/canvas-linux-arm64-musl@0.1.88':
resolution: {integrity: sha512-kYyNrUsHLkoGHBc77u4Unh067GrfiCUMbGHC2+OTxbeWfZkPt2o32UOQkhnSswKd9Fko/wSqqGkY956bIUzruA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.88':
resolution: {integrity: sha512-HVuH7QgzB0yavYdNZDRyAsn/ejoXB0hn8twwFnOqUbCCdkV+REna7RXjSR7+PdfW0qMQ2YYWsLvVBT5iL/mGpw==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
'@napi-rs/canvas-linux-x64-gnu@0.1.88':
resolution: {integrity: sha512-hvcvKIcPEQrvvJtJnwD35B3qk6umFJ8dFIr8bSymfrSMem0EQsfn1ztys8ETIFndTwdNWJKWluvxztA41ivsEw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@napi-rs/canvas-linux-x64-musl@0.1.88':
resolution: {integrity: sha512-eSMpGYY2xnZSQ6UxYJ6plDboxq4KeJ4zT5HaVkUnbObNN6DlbJe0Mclh3wifAmquXfrlgTZt6zhHsUgz++AK6g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@napi-rs/canvas-win32-arm64-msvc@0.1.88':
resolution: {integrity: sha512-qcIFfEgHrchyYqRrxsCeTQgpJZ/GqHiqPcU/Fvw/ARVlQeDX1VyFH+X+0gCR2tca6UJrq96vnW+5o7buCq+erA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@napi-rs/canvas-win32-x64-msvc@0.1.88':
resolution: {integrity: sha512-ROVqbfS4QyZxYkqmaIBBpbz/BQvAR+05FXM5PAtTYVc0uyY8Y4BHJSMdGAaMf6TdIVRsQsiq+FG/dH9XhvWCFQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas@0.1.88':
resolution: {integrity: sha512-/p08f93LEbsL5mDZFQ3DBxcPv/I4QG9EDYRRq1WNlCOXVfAHBTHMSVMwxlqG/AtnSfUr9+vgfN7MKiyDo0+Weg==}
engines: {node: '>= 10'}
'@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
@@ -3832,10 +3756,6 @@ packages:
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pdfjs-dist@5.4.530:
resolution: {integrity: sha512-r1hWsSIGGmyYUAHR26zSXkxYWLXLMd6AwqcaFYG9YUZ0GBf5GvcjJSeo512tabM4GYFhxhl5pMCmPr7Q72Rq2Q==}
engines: {node: '>=20.16.0 || >=22.3.0'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -5751,53 +5671,6 @@ snapshots:
'@mozilla/readability@0.6.0': {}
'@napi-rs/canvas-android-arm64@0.1.88':
optional: true
'@napi-rs/canvas-darwin-arm64@0.1.88':
optional: true
'@napi-rs/canvas-darwin-x64@0.1.88':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.88':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.88':
optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.88':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.88':
optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.88':
optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.88':
optional: true
'@napi-rs/canvas-win32-arm64-msvc@0.1.88':
optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.88':
optional: true
'@napi-rs/canvas@0.1.88':
optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.88
'@napi-rs/canvas-darwin-arm64': 0.1.88
'@napi-rs/canvas-darwin-x64': 0.1.88
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.88
'@napi-rs/canvas-linux-arm64-gnu': 0.1.88
'@napi-rs/canvas-linux-arm64-musl': 0.1.88
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.88
'@napi-rs/canvas-linux-x64-gnu': 0.1.88
'@napi-rs/canvas-linux-x64-musl': 0.1.88
'@napi-rs/canvas-win32-arm64-msvc': 0.1.88
'@napi-rs/canvas-win32-x64-msvc': 0.1.88
'@napi-rs/wasm-runtime@1.1.1':
dependencies:
'@emnapi/core': 1.8.1
@@ -8589,10 +8462,6 @@ snapshots:
pathe@2.0.3: {}
pdfjs-dist@5.4.530:
optionalDependencies:
'@napi-rs/canvas': 0.1.88
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -18,7 +18,7 @@ import {
ensureAuthProfileStore,
saveAuthProfileStore,
} from "./auth-profiles.js";
import { getApiKeyForModel, requireApiKey } from "./model-auth.js";
import { getApiKeyForModel } from "./model-auth.js";
import { normalizeProviderId, parseModelRef } from "./model-selection.js";
import { ensureClawdbotModelsJson } from "./models-config.js";
@@ -178,8 +178,7 @@ describeLive("live anthropic setup-token", () => {
profileId: tokenSource.profileId,
agentDir: tokenSource.agentDir,
});
const apiKey = requireApiKey(apiKeyInfo, model.provider);
const tokenError = validateAnthropicSetupToken(apiKey);
const tokenError = validateAnthropicSetupToken(apiKeyInfo.apiKey);
if (tokenError) {
throw new Error(`Resolved profile is not a setup-token: ${tokenError}`);
}
@@ -196,7 +195,7 @@ describeLive("live anthropic setup-token", () => {
],
},
{
apiKey,
apiKey: apiKeyInfo.apiKey,
maxTokens: 64,
temperature: 0,
},
+1 -2
View File
@@ -1,4 +1,3 @@
import { formatCliCommand } from "../../cli/command-format.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { normalizeProviderId } from "../model-selection.js";
import { listProfilesForProvider } from "./profiles.js";
@@ -38,6 +37,6 @@ export function formatAuthDoctorHint(params: {
}`,
`- auth store oauth profiles: ${storeOauthProfiles || "(none)"}`,
`- suggested profile: ${suggested}`,
`Fix: run "${formatCliCommand("clawdbot doctor --yes")}"`,
'Fix: run "clawdbot doctor --yes"',
].join("\n");
}
-1
View File
@@ -45,7 +45,6 @@ export async function runCliAgent(params: {
timeoutMs: number;
runId: string;
extraSystemPrompt?: string;
streamParams?: import("../commands/agent/types.js").AgentStreamParams;
ownerNumbers?: string[];
cliSessionId?: string;
images?: ImageContent[];
-183
View File
@@ -280,187 +280,4 @@ describe("getApiKeyForModel", () => {
}
}
});
it("prefers Bedrock bearer token over access keys and profile", async () => {
const previous = {
bearer: process.env.AWS_BEARER_TOKEN_BEDROCK,
access: process.env.AWS_ACCESS_KEY_ID,
secret: process.env.AWS_SECRET_ACCESS_KEY,
profile: process.env.AWS_PROFILE,
};
try {
process.env.AWS_BEARER_TOKEN_BEDROCK = "bedrock-token";
process.env.AWS_ACCESS_KEY_ID = "access-key";
process.env.AWS_SECRET_ACCESS_KEY = "secret-key";
process.env.AWS_PROFILE = "profile";
vi.resetModules();
const { resolveApiKeyForProvider } = await import("./model-auth.js");
const resolved = await resolveApiKeyForProvider({
provider: "amazon-bedrock",
store: { version: 1, profiles: {} },
cfg: {
models: {
providers: {
"amazon-bedrock": {
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
api: "bedrock-converse-stream",
auth: "aws-sdk",
models: [],
},
},
},
} as never,
});
expect(resolved.mode).toBe("aws-sdk");
expect(resolved.apiKey).toBeUndefined();
expect(resolved.source).toContain("AWS_BEARER_TOKEN_BEDROCK");
} finally {
if (previous.bearer === undefined) {
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
} else {
process.env.AWS_BEARER_TOKEN_BEDROCK = previous.bearer;
}
if (previous.access === undefined) {
delete process.env.AWS_ACCESS_KEY_ID;
} else {
process.env.AWS_ACCESS_KEY_ID = previous.access;
}
if (previous.secret === undefined) {
delete process.env.AWS_SECRET_ACCESS_KEY;
} else {
process.env.AWS_SECRET_ACCESS_KEY = previous.secret;
}
if (previous.profile === undefined) {
delete process.env.AWS_PROFILE;
} else {
process.env.AWS_PROFILE = previous.profile;
}
}
});
it("prefers Bedrock access keys over profile", async () => {
const previous = {
bearer: process.env.AWS_BEARER_TOKEN_BEDROCK,
access: process.env.AWS_ACCESS_KEY_ID,
secret: process.env.AWS_SECRET_ACCESS_KEY,
profile: process.env.AWS_PROFILE,
};
try {
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
process.env.AWS_ACCESS_KEY_ID = "access-key";
process.env.AWS_SECRET_ACCESS_KEY = "secret-key";
process.env.AWS_PROFILE = "profile";
vi.resetModules();
const { resolveApiKeyForProvider } = await import("./model-auth.js");
const resolved = await resolveApiKeyForProvider({
provider: "amazon-bedrock",
store: { version: 1, profiles: {} },
cfg: {
models: {
providers: {
"amazon-bedrock": {
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
api: "bedrock-converse-stream",
auth: "aws-sdk",
models: [],
},
},
},
} as never,
});
expect(resolved.mode).toBe("aws-sdk");
expect(resolved.apiKey).toBeUndefined();
expect(resolved.source).toContain("AWS_ACCESS_KEY_ID");
} finally {
if (previous.bearer === undefined) {
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
} else {
process.env.AWS_BEARER_TOKEN_BEDROCK = previous.bearer;
}
if (previous.access === undefined) {
delete process.env.AWS_ACCESS_KEY_ID;
} else {
process.env.AWS_ACCESS_KEY_ID = previous.access;
}
if (previous.secret === undefined) {
delete process.env.AWS_SECRET_ACCESS_KEY;
} else {
process.env.AWS_SECRET_ACCESS_KEY = previous.secret;
}
if (previous.profile === undefined) {
delete process.env.AWS_PROFILE;
} else {
process.env.AWS_PROFILE = previous.profile;
}
}
});
it("uses Bedrock profile when access keys are missing", async () => {
const previous = {
bearer: process.env.AWS_BEARER_TOKEN_BEDROCK,
access: process.env.AWS_ACCESS_KEY_ID,
secret: process.env.AWS_SECRET_ACCESS_KEY,
profile: process.env.AWS_PROFILE,
};
try {
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
delete process.env.AWS_ACCESS_KEY_ID;
delete process.env.AWS_SECRET_ACCESS_KEY;
process.env.AWS_PROFILE = "profile";
vi.resetModules();
const { resolveApiKeyForProvider } = await import("./model-auth.js");
const resolved = await resolveApiKeyForProvider({
provider: "amazon-bedrock",
store: { version: 1, profiles: {} },
cfg: {
models: {
providers: {
"amazon-bedrock": {
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
api: "bedrock-converse-stream",
auth: "aws-sdk",
models: [],
},
},
},
} as never,
});
expect(resolved.mode).toBe("aws-sdk");
expect(resolved.apiKey).toBeUndefined();
expect(resolved.source).toContain("AWS_PROFILE");
} finally {
if (previous.bearer === undefined) {
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
} else {
process.env.AWS_BEARER_TOKEN_BEDROCK = previous.bearer;
}
if (previous.access === undefined) {
delete process.env.AWS_ACCESS_KEY_ID;
} else {
process.env.AWS_ACCESS_KEY_ID = previous.access;
}
if (previous.secret === undefined) {
delete process.env.AWS_SECRET_ACCESS_KEY;
} else {
process.env.AWS_SECRET_ACCESS_KEY = previous.secret;
}
if (previous.profile === undefined) {
delete process.env.AWS_PROFILE;
} else {
process.env.AWS_PROFILE = previous.profile;
}
}
});
});
+9 -142
View File
@@ -2,9 +2,8 @@ import path from "node:path";
import { type Api, getEnvApiKey, type Model } from "@mariozechner/pi-ai";
import type { ClawdbotConfig } from "../config/config.js";
import type { ModelProviderAuthMode, ModelProviderConfig } from "../config/types.js";
import type { ModelProviderConfig } from "../config/types.js";
import { getShellEnvAppliedKeys } from "../infra/shell-env.js";
import { formatCliCommand } from "../cli/command-format.js";
import {
type AuthProfileStore,
ensureAuthProfileStore,
@@ -17,115 +16,16 @@ import { normalizeProviderId } from "./model-selection.js";
export { ensureAuthProfileStore, resolveAuthProfileOrder } from "./auth-profiles.js";
const AWS_BEARER_ENV = "AWS_BEARER_TOKEN_BEDROCK";
const AWS_ACCESS_KEY_ENV = "AWS_ACCESS_KEY_ID";
const AWS_SECRET_KEY_ENV = "AWS_SECRET_ACCESS_KEY";
const AWS_PROFILE_ENV = "AWS_PROFILE";
function resolveProviderConfig(
cfg: ClawdbotConfig | undefined,
provider: string,
): ModelProviderConfig | undefined {
const providers = cfg?.models?.providers ?? {};
const direct = providers[provider] as ModelProviderConfig | undefined;
if (direct) return direct;
const normalized = normalizeProviderId(provider);
if (normalized === provider) {
const matched = Object.entries(providers).find(
([key]) => normalizeProviderId(key) === normalized,
);
return matched?.[1] as ModelProviderConfig | undefined;
}
return (
(providers[normalized] as ModelProviderConfig | undefined) ??
(Object.entries(providers).find(
([key]) => normalizeProviderId(key) === normalized,
)?.[1] as ModelProviderConfig | undefined)
);
}
export function getCustomProviderApiKey(
cfg: ClawdbotConfig | undefined,
provider: string,
): string | undefined {
const entry = resolveProviderConfig(cfg, provider);
const providers = cfg?.models?.providers ?? {};
const entry = providers[provider] as ModelProviderConfig | undefined;
const key = entry?.apiKey?.trim();
return key || undefined;
}
function resolveProviderAuthOverride(
cfg: ClawdbotConfig | undefined,
provider: string,
): ModelProviderAuthMode | undefined {
const entry = resolveProviderConfig(cfg, provider);
const auth = entry?.auth;
if (auth === "api-key" || auth === "aws-sdk" || auth === "oauth" || auth === "token") {
return auth;
}
return undefined;
}
function resolveEnvSourceLabel(params: {
applied: Set<string>;
envVars: string[];
label: string;
}): string {
const shellApplied = params.envVars.some((envVar) => params.applied.has(envVar));
const prefix = shellApplied ? "shell env: " : "env: ";
return `${prefix}${params.label}`;
}
export function resolveAwsSdkEnvVarName(): string | undefined {
if (process.env[AWS_BEARER_ENV]?.trim()) return AWS_BEARER_ENV;
if (process.env[AWS_ACCESS_KEY_ENV]?.trim() && process.env[AWS_SECRET_KEY_ENV]?.trim()) {
return AWS_ACCESS_KEY_ENV;
}
if (process.env[AWS_PROFILE_ENV]?.trim()) return AWS_PROFILE_ENV;
return undefined;
}
function resolveAwsSdkAuthInfo(): { mode: "aws-sdk"; source: string } {
const applied = new Set(getShellEnvAppliedKeys());
if (process.env[AWS_BEARER_ENV]?.trim()) {
return {
mode: "aws-sdk",
source: resolveEnvSourceLabel({
applied,
envVars: [AWS_BEARER_ENV],
label: AWS_BEARER_ENV,
}),
};
}
if (process.env[AWS_ACCESS_KEY_ENV]?.trim() && process.env[AWS_SECRET_KEY_ENV]?.trim()) {
return {
mode: "aws-sdk",
source: resolveEnvSourceLabel({
applied,
envVars: [AWS_ACCESS_KEY_ENV, AWS_SECRET_KEY_ENV],
label: `${AWS_ACCESS_KEY_ENV} + ${AWS_SECRET_KEY_ENV}`,
}),
};
}
if (process.env[AWS_PROFILE_ENV]?.trim()) {
return {
mode: "aws-sdk",
source: resolveEnvSourceLabel({
applied,
envVars: [AWS_PROFILE_ENV],
label: AWS_PROFILE_ENV,
}),
};
}
return { mode: "aws-sdk", source: "aws-sdk default chain" };
}
export type ResolvedProviderAuth = {
apiKey?: string;
profileId?: string;
source: string;
mode: "api-key" | "oauth" | "token" | "aws-sdk";
};
export async function resolveApiKeyForProvider(params: {
provider: string;
cfg?: ClawdbotConfig;
@@ -133,7 +33,7 @@ export async function resolveApiKeyForProvider(params: {
preferredProfile?: string;
store?: AuthProfileStore;
agentDir?: string;
}): Promise<ResolvedProviderAuth> {
}): Promise<{ apiKey: string; profileId?: string; source: string }> {
const { provider, cfg, profileId, preferredProfile } = params;
const store = params.store ?? ensureAuthProfileStore(params.agentDir);
@@ -147,20 +47,13 @@ export async function resolveApiKeyForProvider(params: {
if (!resolved) {
throw new Error(`No credentials found for profile "${profileId}".`);
}
const mode = store.profiles[profileId]?.type;
return {
apiKey: resolved.apiKey,
profileId,
source: `profile:${profileId}`,
mode: mode === "oauth" ? "oauth" : mode === "token" ? "token" : "api-key",
};
}
const authOverride = resolveProviderAuthOverride(cfg, provider);
if (authOverride === "aws-sdk") {
return resolveAwsSdkAuthInfo();
}
const order = resolveAuthProfileOrder({
cfg,
store,
@@ -176,12 +69,10 @@ export async function resolveApiKeyForProvider(params: {
agentDir: params.agentDir,
});
if (resolved) {
const mode = store.profiles[candidate]?.type;
return {
apiKey: resolved.apiKey,
profileId: candidate,
source: `profile:${candidate}`,
mode: mode === "oauth" ? "oauth" : mode === "token" ? "token" : "api-key",
};
}
} catch {}
@@ -189,21 +80,12 @@ export async function resolveApiKeyForProvider(params: {
const envResolved = resolveEnvApiKey(provider);
if (envResolved) {
return {
apiKey: envResolved.apiKey,
source: envResolved.source,
mode: envResolved.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key",
};
return { apiKey: envResolved.apiKey, source: envResolved.source };
}
const customKey = getCustomProviderApiKey(cfg, provider);
if (customKey) {
return { apiKey: customKey, source: "models.json", mode: "api-key" };
}
const normalized = normalizeProviderId(provider);
if (authOverride === undefined && normalized === "amazon-bedrock") {
return resolveAwsSdkAuthInfo();
return { apiKey: customKey, source: "models.json" };
}
if (provider === "openai") {
@@ -221,13 +103,13 @@ export async function resolveApiKeyForProvider(params: {
[
`No API key found for provider "${provider}".`,
`Auth store: ${authStorePath} (agentDir: ${resolvedAgentDir}).`,
`Configure auth for this agent (${formatCliCommand("clawdbot agents add <id>")}) or copy auth-profiles.json from the main agentDir.`,
"Configure auth for this agent (clawdbot agents add <id>) or copy auth-profiles.json from the main agentDir.",
].join(" "),
);
}
export type EnvApiKeyResult = { apiKey: string; source: string };
export type ModelAuthMode = "api-key" | "oauth" | "token" | "mixed" | "aws-sdk" | "unknown";
export type ModelAuthMode = "api-key" | "oauth" | "token" | "mixed" | "unknown";
export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
const normalized = normalizeProviderId(provider);
@@ -298,9 +180,6 @@ export function resolveModelAuthMode(
const resolved = provider?.trim();
if (!resolved) return undefined;
const authOverride = resolveProviderAuthOverride(cfg, resolved);
if (authOverride === "aws-sdk") return "aws-sdk";
const authStore = store ?? ensureAuthProfileStore();
const profiles = listProfilesForProvider(authStore, resolved);
if (profiles.length > 0) {
@@ -318,10 +197,6 @@ export function resolveModelAuthMode(
if (modes.has("api_key")) return "api-key";
}
if (authOverride === undefined && normalizeProviderId(resolved) === "amazon-bedrock") {
return "aws-sdk";
}
const envKey = resolveEnvApiKey(resolved);
if (envKey?.apiKey) {
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
@@ -339,7 +214,7 @@ export async function getApiKeyForModel(params: {
preferredProfile?: string;
store?: AuthProfileStore;
agentDir?: string;
}): Promise<ResolvedProviderAuth> {
}): Promise<{ apiKey: string; profileId?: string; source: string }> {
return resolveApiKeyForProvider({
provider: params.model.provider,
cfg: params.cfg,
@@ -349,11 +224,3 @@ export async function getApiKeyForModel(params: {
agentDir: params.agentDir,
});
}
export function requireApiKey(auth: ResolvedProviderAuth, provider: string): string {
const key = auth.apiKey?.trim();
if (key) return key;
throw new Error(
`No API key resolved for provider "${provider}" (auth mode: ${auth.mode}).`,
);
}
+8 -20
View File
@@ -4,7 +4,7 @@ import {
resolveCopilotApiToken,
} from "../providers/github-copilot-token.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
import { resolveEnvApiKey } from "./model-auth.js";
import {
buildSyntheticModelDefinition,
SYNTHETIC_BASE_URL,
@@ -74,10 +74,6 @@ function resolveEnvApiKeyVarName(provider: string): string | undefined {
return match ? match[1] : undefined;
}
function resolveAwsSdkApiKeyVarName(): string {
return resolveAwsSdkEnvVarName() ?? "AWS_PROFILE";
}
function resolveApiKeyFromProfiles(params: {
provider: string;
store: ReturnType<typeof ensureAuthProfileStore>;
@@ -142,23 +138,15 @@ export function normalizeProviders(params: {
const hasModels =
Array.isArray(normalizedProvider.models) && normalizedProvider.models.length > 0;
if (hasModels && !normalizedProvider.apiKey?.trim()) {
const authMode =
normalizedProvider.auth ?? (normalizedKey === "amazon-bedrock" ? "aws-sdk" : undefined);
if (authMode === "aws-sdk") {
const apiKey = resolveAwsSdkApiKeyVarName();
const fromEnv = resolveEnvApiKeyVarName(normalizedKey);
const fromProfiles = resolveApiKeyFromProfiles({
provider: normalizedKey,
store: authStore,
});
const apiKey = fromEnv ?? fromProfiles;
if (apiKey?.trim()) {
mutated = true;
normalizedProvider = { ...normalizedProvider, apiKey };
} else {
const fromEnv = resolveEnvApiKeyVarName(normalizedKey);
const fromProfiles = resolveApiKeyFromProfiles({
provider: normalizedKey,
store: authStore,
});
const apiKey = fromEnv ?? fromProfiles;
if (apiKey?.trim()) {
mutated = true;
normalizedProvider = { ...normalizedProvider, apiKey };
}
}
}
+2 -2
View File
@@ -11,7 +11,7 @@ import {
isAnthropicRateLimitError,
} from "./live-auth-keys.js";
import { isModernModelRef } from "./live-model-filter.js";
import { getApiKeyForModel, requireApiKey } from "./model-auth.js";
import { getApiKeyForModel } from "./model-auth.js";
import { ensureClawdbotModelsJson } from "./models-config.js";
import { isRateLimitErrorMessage } from "./pi-embedded-helpers/errors.js";
@@ -226,7 +226,7 @@ describeLive("live models (profile keys)", () => {
const apiKey =
model.provider === "anthropic" && anthropicKeys.length > 0
? anthropicKeys[attempt]
: requireApiKey(apiKeyInfo, model.provider);
: apiKeyInfo.apiKey;
try {
// Special regression: OpenAI requires replayed `reasoning` items for tool-only turns.
if (
Loaded 100 of 252 files, more files were not shown because too many files have changed in this diff. Show more