Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4e30d559b |
No files matched your search
Vendored
BIN
Binary file not shown.
@@ -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 +0,0 @@
|
|||||||
/cache
|
|
||||||
BIN
Binary file not shown.
Binary file not shown.
@@ -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: []
|
|
||||||
@@ -57,8 +57,6 @@
|
|||||||
- Changelog workflow: keep latest released version at top (no `Unreleased`); after publishing, bump version and start a new top section.
|
- 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.
|
- 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 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`.
|
- 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.
|
- 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.
|
- When working on a PR: add a changelog entry with the PR number and thank the contributor.
|
||||||
|
|||||||
+2
-18
@@ -2,26 +2,11 @@
|
|||||||
|
|
||||||
Docs: https://docs.clawd.bot
|
Docs: https://docs.clawd.bot
|
||||||
|
|
||||||
## 2026.1.20-1
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- Repo: remove the Peekaboo git submodule now that the SPM release is used.
|
|
||||||
- Plugins: require manifest-embedded config schemas, validate configs without loading plugin code, and surface plugin config warnings. (#1272) — thanks @thewilloftheshadow.
|
|
||||||
- Plugins: move channel catalog metadata into plugin manifests; align Nextcloud Talk policy helpers with core patterns. (#1290) — thanks @NicholaiVogel.
|
|
||||||
### 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.
|
|
||||||
- Exec: prefer bash when fish is default shell, falling back to sh if bash is missing. (#1297) — thanks @ysqander.
|
|
||||||
- Plugins: add Nextcloud Talk manifest for plugin config validation. (#1297) — thanks @ysqander.
|
|
||||||
|
|
||||||
## 2026.1.19-3
|
## 2026.1.19-3
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Android: remove legacy bridge transport code now that nodes use the gateway protocol.
|
- 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.
|
- 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
|
### Fixes
|
||||||
- Gateway: strip inbound envelope headers from chat history messages to keep clients clean.
|
- Gateway: strip inbound envelope headers from chat history messages to keep clients clean.
|
||||||
@@ -44,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.
|
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety; run `clawdbot doctor --fix` to repair.
|
||||||
|
|
||||||
### Changes
|
### 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.
|
- Usage: add `/usage cost` summaries and macOS menu cost submenu with daily charting.
|
||||||
- Agents: clarify node_modules read-only guidance in agent instructions.
|
- Agents: clarify node_modules read-only guidance in agent instructions.
|
||||||
- TUI: add syntax highlighting for code blocks. (#1200) — thanks @vignesh07.
|
- TUI: add syntax highlighting for code blocks. (#1200) — thanks @vignesh07.
|
||||||
@@ -85,8 +69,8 @@ Docs: https://docs.clawd.bot
|
|||||||
## 2026.1.18-4
|
## 2026.1.18-4
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- macOS: switch PeekabooBridge integration to the tagged Swift Package Manager release.
|
- macOS: switch PeekabooBridge integration to the tagged Swift Package Manager release (no submodule).
|
||||||
- macOS: stop syncing Peekaboo in postinstall.
|
- macOS: stop syncing Peekaboo as a git submodule in postinstall.
|
||||||
- Swabble: use the tagged Commander Swift package release.
|
- Swabble: use the tagged Commander Swift package release.
|
||||||
- CLI: add `clawdbot acp client` interactive ACP harness for debugging.
|
- 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.
|
- Plugins: route command detection/text chunking helpers through the plugin runtime and drop runtime exports from the SDK.
|
||||||
|
|||||||
@@ -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.
|
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)
|
### macOS (Clawdbot.app) (optional)
|
||||||
|
|
||||||
@@ -471,26 +476,24 @@ Thanks to all clawtributors:
|
|||||||
|
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a>
|
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a>
|
||||||
<a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a>
|
<a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a>
|
||||||
<a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a>
|
<a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a>
|
||||||
<a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a>
|
<a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a>
|
||||||
<a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a>
|
<a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a>
|
||||||
<a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a>
|
<a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a>
|
||||||
<a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a>
|
<a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a>
|
||||||
<a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a>
|
<a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a>
|
||||||
<a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a>
|
<a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a>
|
||||||
<a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a>
|
<a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a>
|
||||||
<a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a>
|
<a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a>
|
||||||
<a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a>
|
<a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a>
|
||||||
<a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a>
|
<a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a>
|
||||||
<a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a>
|
<a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a>
|
||||||
<a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a>
|
<a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a>
|
||||||
<a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a>
|
<a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a>
|
||||||
<a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a>
|
<a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a>
|
||||||
<a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a>
|
<a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a>
|
||||||
<a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a>
|
<a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a>
|
||||||
<a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a>
|
<a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a>
|
||||||
<a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a>
|
<a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
||||||
<a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a>
|
|
||||||
<a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
|
||||||
</p>
|
</p>
|
||||||
+2
-2
@@ -101,8 +101,8 @@ Environment variables:
|
|||||||
- Authorization requested at first start; requires macOS 26 + new Speech.framework APIs.
|
- Authorization requested at first start; requires macOS 26 + new Speech.framework APIs.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
- Format: `./scripts/format.sh` (uses local `.swiftformat`)
|
- Format: `./scripts/format.sh` (uses ../peekaboo/.swiftformat if present)
|
||||||
- Lint: `./scripts/lint.sh` (uses local `.swiftlint.yml`)
|
- Lint: `./scripts/lint.sh` (uses ../peekaboo/.swiftlint.yml if present)
|
||||||
- Tests: `swift test` (uses swift-testing package)
|
- Tests: `swift test` (uses swift-testing package)
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
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"
|
swiftformat --config "$CONFIG" "$ROOT/Sources"
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
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
|
if ! command -v swiftlint >/dev/null; then
|
||||||
echo "swiftlint not installed" >&2
|
echo "swiftlint not installed" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -160,14 +160,14 @@ actor CameraController {
|
|||||||
defer { session.stopRunning() }
|
defer { session.stopRunning() }
|
||||||
await Self.warmUpCaptureSession()
|
await Self.warmUpCaptureSession()
|
||||||
|
|
||||||
let movURL = FileManager().temporaryDirectory
|
let movURL = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mov")
|
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mov")
|
||||||
let mp4URL = FileManager().temporaryDirectory
|
let mp4URL = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mp4")
|
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mp4")
|
||||||
|
|
||||||
defer {
|
defer {
|
||||||
try? FileManager().removeItem(at: movURL)
|
try? FileManager.default.removeItem(at: movURL)
|
||||||
try? FileManager().removeItem(at: mp4URL)
|
try? FileManager.default.removeItem(at: mp4URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
var delegate: MovieFileDelegate?
|
var delegate: MovieFileDelegate?
|
||||||
|
|||||||
@@ -837,7 +837,7 @@ final class NodeAppModel {
|
|||||||
fps: params.fps,
|
fps: params.fps,
|
||||||
includeAudio: params.includeAudio,
|
includeAudio: params.includeAudio,
|
||||||
outPath: nil)
|
outPath: nil)
|
||||||
defer { try? FileManager().removeItem(atPath: path) }
|
defer { try? FileManager.default.removeItem(atPath: path) }
|
||||||
let data = try Data(contentsOf: URL(fileURLWithPath: path))
|
let data = try Data(contentsOf: URL(fileURLWithPath: path))
|
||||||
struct Payload: Codable {
|
struct Payload: Codable {
|
||||||
var format: String
|
var format: String
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ final class ScreenRecordService: @unchecked Sendable {
|
|||||||
let includeAudio = includeAudio ?? true
|
let includeAudio = includeAudio ?? true
|
||||||
|
|
||||||
let outURL = self.makeOutputURL(outPath: outPath)
|
let outURL = self.makeOutputURL(outPath: outPath)
|
||||||
try? FileManager().removeItem(at: outURL)
|
try? FileManager.default.removeItem(at: outURL)
|
||||||
|
|
||||||
return RecordConfig(
|
return RecordConfig(
|
||||||
durationMs: durationMs,
|
durationMs: durationMs,
|
||||||
@@ -104,7 +104,7 @@ final class ScreenRecordService: @unchecked Sendable {
|
|||||||
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
return URL(fileURLWithPath: outPath)
|
return URL(fileURLWithPath: outPath)
|
||||||
}
|
}
|
||||||
return FileManager().temporaryDirectory
|
return FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-screen-record-\(UUID().uuidString).mp4")
|
.appendingPathComponent("clawdbot-screen-record-\(UUID().uuidString).mp4")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ enum AgentWorkspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func displayPath(for url: URL) -> String {
|
static func displayPath(for url: URL) -> String {
|
||||||
let home = FileManager().homeDirectoryForCurrentUser.path
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
let path = url.path
|
let path = url.path
|
||||||
if path == home { return "~" }
|
if path == home { return "~" }
|
||||||
if path.hasPrefix(home + "/") {
|
if path.hasPrefix(home + "/") {
|
||||||
@@ -44,12 +44,12 @@ enum AgentWorkspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func workspaceEntries(workspaceURL: URL) throws -> [String] {
|
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) }
|
return contents.filter { !self.ignoredEntries.contains($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
static func isWorkspaceEmpty(workspaceURL: URL) -> Bool {
|
static func isWorkspaceEmpty(workspaceURL: URL) -> Bool {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
var isDir: ObjCBool = false
|
var isDir: ObjCBool = false
|
||||||
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
|
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
|
||||||
return true
|
return true
|
||||||
@@ -66,7 +66,7 @@ enum AgentWorkspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func bootstrapSafety(for workspaceURL: URL) -> BootstrapSafety {
|
static func bootstrapSafety(for workspaceURL: URL) -> BootstrapSafety {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
var isDir: ObjCBool = false
|
var isDir: ObjCBool = false
|
||||||
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
|
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
|
||||||
return .safe
|
return .safe
|
||||||
@@ -90,29 +90,29 @@ enum AgentWorkspace {
|
|||||||
|
|
||||||
static func bootstrap(workspaceURL: URL) throws -> URL {
|
static func bootstrap(workspaceURL: URL) throws -> URL {
|
||||||
let shouldSeedBootstrap = self.isWorkspaceEmpty(workspaceURL: workspaceURL)
|
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)
|
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)
|
try self.defaultTemplate().write(to: agentsURL, atomically: true, encoding: .utf8)
|
||||||
self.logger.info("Created AGENTS.md at \(agentsURL.path, privacy: .public)")
|
self.logger.info("Created AGENTS.md at \(agentsURL.path, privacy: .public)")
|
||||||
}
|
}
|
||||||
let soulURL = workspaceURL.appendingPathComponent(self.soulFilename)
|
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)
|
try self.defaultSoulTemplate().write(to: soulURL, atomically: true, encoding: .utf8)
|
||||||
self.logger.info("Created SOUL.md at \(soulURL.path, privacy: .public)")
|
self.logger.info("Created SOUL.md at \(soulURL.path, privacy: .public)")
|
||||||
}
|
}
|
||||||
let identityURL = workspaceURL.appendingPathComponent(self.identityFilename)
|
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)
|
try self.defaultIdentityTemplate().write(to: identityURL, atomically: true, encoding: .utf8)
|
||||||
self.logger.info("Created IDENTITY.md at \(identityURL.path, privacy: .public)")
|
self.logger.info("Created IDENTITY.md at \(identityURL.path, privacy: .public)")
|
||||||
}
|
}
|
||||||
let userURL = workspaceURL.appendingPathComponent(self.userFilename)
|
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)
|
try self.defaultUserTemplate().write(to: userURL, atomically: true, encoding: .utf8)
|
||||||
self.logger.info("Created USER.md at \(userURL.path, privacy: .public)")
|
self.logger.info("Created USER.md at \(userURL.path, privacy: .public)")
|
||||||
}
|
}
|
||||||
let bootstrapURL = workspaceURL.appendingPathComponent(self.bootstrapFilename)
|
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)
|
try self.defaultBootstrapTemplate().write(to: bootstrapURL, atomically: true, encoding: .utf8)
|
||||||
self.logger.info("Created BOOTSTRAP.md at \(bootstrapURL.path, privacy: .public)")
|
self.logger.info("Created BOOTSTRAP.md at \(bootstrapURL.path, privacy: .public)")
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ enum AgentWorkspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func needsBootstrap(workspaceURL: URL) -> Bool {
|
static func needsBootstrap(workspaceURL: URL) -> Bool {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
var isDir: ObjCBool = false
|
var isDir: ObjCBool = false
|
||||||
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
|
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
|
||||||
return true
|
return true
|
||||||
@@ -305,7 +305,7 @@ enum AgentWorkspace {
|
|||||||
if let dev = self.devTemplateURL(named: named) {
|
if let dev = self.devTemplateURL(named: named) {
|
||||||
urls.append(dev)
|
urls.append(dev)
|
||||||
}
|
}
|
||||||
let cwd = URL(fileURLWithPath: FileManager().currentDirectoryPath)
|
let cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
|
||||||
urls.append(cwd.appendingPathComponent("docs")
|
urls.append(cwd.appendingPathComponent("docs")
|
||||||
.appendingPathComponent(self.templateDirname)
|
.appendingPathComponent(self.templateDirname)
|
||||||
.appendingPathComponent(named))
|
.appendingPathComponent(named))
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ struct AnthropicAuthControls: View {
|
|||||||
NSWorkspace.shared.activateFileViewerSelecting([ClawdbotOAuthStore.oauthURL()])
|
NSWorkspace.shared.activateFileViewerSelecting([ClawdbotOAuthStore.oauthURL()])
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.disabled(!FileManager().fileExists(atPath: ClawdbotOAuthStore.oauthURL().path))
|
.disabled(!FileManager.default.fileExists(atPath: ClawdbotOAuthStore.oauthURL().path))
|
||||||
|
|
||||||
Button("Refresh") {
|
Button("Refresh") {
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ enum ClawdbotOAuthStore {
|
|||||||
return URL(fileURLWithPath: expanded, isDirectory: true)
|
return URL(fileURLWithPath: expanded, isDirectory: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return FileManager().homeDirectoryForCurrentUser
|
return FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".clawdbot", isDirectory: true)
|
.appendingPathComponent(".clawdbot", isDirectory: true)
|
||||||
.appendingPathComponent("credentials", isDirectory: true)
|
.appendingPathComponent("credentials", isDirectory: true)
|
||||||
}
|
}
|
||||||
@@ -253,7 +253,7 @@ enum ClawdbotOAuthStore {
|
|||||||
urls.append(URL(fileURLWithPath: expanded, isDirectory: true).appendingPathComponent(self.oauthFilename))
|
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(".pi/agent/\(self.oauthFilename)"))
|
||||||
urls.append(home.appendingPathComponent(".claude/\(self.oauthFilename)"))
|
urls.append(home.appendingPathComponent(".claude/\(self.oauthFilename)"))
|
||||||
urls.append(home.appendingPathComponent(".config/claude/\(self.oauthFilename)"))
|
urls.append(home.appendingPathComponent(".config/claude/\(self.oauthFilename)"))
|
||||||
@@ -270,10 +270,10 @@ enum ClawdbotOAuthStore {
|
|||||||
|
|
||||||
static func importLegacyAnthropicOAuthIfNeeded() -> URL? {
|
static func importLegacyAnthropicOAuthIfNeeded() -> URL? {
|
||||||
let dest = self.oauthURL()
|
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() {
|
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 self.anthropicOAuthStatus(at: url).isConnected else { continue }
|
||||||
guard let storage = self.loadStorage(at: url) else { continue }
|
guard let storage = self.loadStorage(at: url) else { continue }
|
||||||
do {
|
do {
|
||||||
@@ -296,7 +296,7 @@ enum ClawdbotOAuthStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func anthropicOAuthStatus(at url: URL) -> AnthropicOAuthStatus {
|
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 data = try? Data(contentsOf: url) else { return .unreadableFile }
|
||||||
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else { return .invalidJSON }
|
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 {
|
private static func saveStorage(_ storage: [String: Any]) throws {
|
||||||
let dir = self.oauthDir()
|
let dir = self.oauthDir()
|
||||||
try FileManager().createDirectory(
|
try FileManager.default.createDirectory(
|
||||||
at: dir,
|
at: dir,
|
||||||
withIntermediateDirectories: true,
|
withIntermediateDirectories: true,
|
||||||
attributes: [.posixPermissions: 0o700])
|
attributes: [.posixPermissions: 0o700])
|
||||||
@@ -370,7 +370,7 @@ enum ClawdbotOAuthStore {
|
|||||||
withJSONObject: storage,
|
withJSONObject: storage,
|
||||||
options: [.prettyPrinted, .sortedKeys])
|
options: [.prettyPrinted, .sortedKeys])
|
||||||
try data.write(to: url, options: [.atomic])
|
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 {
|
private static func installPrefix() -> String {
|
||||||
FileManager().homeDirectoryForCurrentUser
|
FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".clawdbot")
|
.appendingPathComponent(".clawdbot")
|
||||||
.path
|
.path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,20 +167,20 @@ actor CameraCaptureService {
|
|||||||
defer { session.stopRunning() }
|
defer { session.stopRunning() }
|
||||||
await Self.warmUpCaptureSession()
|
await Self.warmUpCaptureSession()
|
||||||
|
|
||||||
let tmpMovURL = FileManager().temporaryDirectory
|
let tmpMovURL = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mov")
|
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mov")
|
||||||
defer { try? FileManager().removeItem(at: tmpMovURL) }
|
defer { try? FileManager.default.removeItem(at: tmpMovURL) }
|
||||||
|
|
||||||
let outputURL: URL = {
|
let outputURL: URL = {
|
||||||
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
return URL(fileURLWithPath: outPath)
|
return URL(fileURLWithPath: outPath)
|
||||||
}
|
}
|
||||||
return FileManager().temporaryDirectory
|
return FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mp4")
|
.appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mp4")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Ensure we don't fail exporting due to an existing file.
|
// 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
|
let logger = self.logger
|
||||||
var delegate: MovieFileDelegate?
|
var delegate: MovieFileDelegate?
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ final class CanvasManager {
|
|||||||
var defaultAnchorProvider: (() -> NSRect?)?
|
var defaultAnchorProvider: (() -> NSRect?)?
|
||||||
|
|
||||||
private nonisolated static let canvasRoot: URL = {
|
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)
|
return base.appendingPathComponent("Clawdbot/canvas", isDirectory: true)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ final class CanvasManager {
|
|||||||
self.panelSessionKey = nil
|
self.panelSessionKey = nil
|
||||||
|
|
||||||
Self.logger.debug("showDetailed ensure canvas root dir")
|
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")
|
Self.logger.debug("showDetailed init CanvasWindowController")
|
||||||
let controller = try CanvasWindowController(
|
let controller = try CanvasWindowController(
|
||||||
sessionKey: session,
|
sessionKey: session,
|
||||||
@@ -258,7 +258,7 @@ final class CanvasManager {
|
|||||||
// (Avoid treating Canvas routes like "/" as filesystem paths.)
|
// (Avoid treating Canvas routes like "/" as filesystem paths.)
|
||||||
if trimmed.hasPrefix("/") {
|
if trimmed.hasPrefix("/") {
|
||||||
var isDir: ObjCBool = false
|
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)
|
return URL(fileURLWithPath: trimmed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,7 +293,7 @@ final class CanvasManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func localStatus(sessionDir: URL, target: String) -> CanvasShowStatus {
|
private static func localStatus(sessionDir: URL, target: String) -> CanvasShowStatus {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let withoutQuery = trimmed.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false).first
|
let withoutQuery = trimmed.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false).first
|
||||||
.map(String.init) ?? trimmed
|
.map(String.init) ?? trimmed
|
||||||
@@ -331,7 +331,7 @@ final class CanvasManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func indexExists(in dir: URL) -> Bool {
|
private static func indexExists(in dir: URL) -> Bool {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
let a = dir.appendingPathComponent("index.html", isDirectory: false)
|
let a = dir.appendingPathComponent("index.html", isDirectory: false)
|
||||||
if fm.fileExists(atPath: a.path) { return true }
|
if fm.fileExists(atPath: a.path) { return true }
|
||||||
let b = dir.appendingPathComponent("index.htm", isDirectory: false)
|
let b = dir.appendingPathComponent("index.htm", isDirectory: false)
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
|||||||
if path.isEmpty {
|
if path.isEmpty {
|
||||||
let indexA = sessionRoot.appendingPathComponent("index.html", isDirectory: false)
|
let indexA = sessionRoot.appendingPathComponent("index.html", isDirectory: false)
|
||||||
let indexB = sessionRoot.appendingPathComponent("index.htm", isDirectory: false)
|
let indexB = sessionRoot.appendingPathComponent("index.htm", isDirectory: false)
|
||||||
if !FileManager().fileExists(atPath: indexA.path),
|
if !FileManager.default.fileExists(atPath: indexA.path),
|
||||||
!FileManager().fileExists(atPath: indexB.path)
|
!FileManager.default.fileExists(atPath: indexB.path)
|
||||||
{
|
{
|
||||||
return self.scaffoldPage(sessionRoot: sessionRoot)
|
return self.scaffoldPage(sessionRoot: sessionRoot)
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func resolveFileURL(sessionRoot: URL, requestPath: String) -> URL? {
|
private func resolveFileURL(sessionRoot: URL, requestPath: String) -> URL? {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
var candidate = sessionRoot.appendingPathComponent(requestPath, isDirectory: false)
|
var candidate = sessionRoot.appendingPathComponent(requestPath, isDirectory: false)
|
||||||
|
|
||||||
var isDir: ObjCBool = false
|
var isDir: ObjCBool = false
|
||||||
@@ -137,7 +137,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func resolveIndex(in dir: URL) -> URL? {
|
private func resolveIndex(in dir: URL) -> URL? {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
let a = dir.appendingPathComponent("index.html", isDirectory: false)
|
let a = dir.appendingPathComponent("index.html", isDirectory: false)
|
||||||
if fm.fileExists(atPath: a.path) { return a }
|
if fm.fileExists(atPath: a.path) { return a }
|
||||||
let b = dir.appendingPathComponent("index.htm", isDirectory: false)
|
let b = dir.appendingPathComponent("index.htm", isDirectory: false)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
|
|||||||
let safeSessionKey = CanvasWindowController.sanitizeSessionKey(sessionKey)
|
let safeSessionKey = CanvasWindowController.sanitizeSessionKey(sessionKey)
|
||||||
canvasWindowLogger.debug("CanvasWindowController init sanitized session=\(safeSessionKey, privacy: .public)")
|
canvasWindowLogger.debug("CanvasWindowController init sanitized session=\(safeSessionKey, privacy: .public)")
|
||||||
self.sessionDir = root.appendingPathComponent(safeSessionKey, isDirectory: true)
|
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")
|
canvasWindowLogger.debug("CanvasWindowController init session dir ready")
|
||||||
|
|
||||||
self.schemeHandler = CanvasSchemeHandler(root: root)
|
self.schemeHandler = CanvasSchemeHandler(root: root)
|
||||||
@@ -143,8 +143,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
|
|||||||
if path == "/" || path.isEmpty {
|
if path == "/" || path.isEmpty {
|
||||||
let indexA = sessionDir.appendingPathComponent("index.html", isDirectory: false)
|
let indexA = sessionDir.appendingPathComponent("index.html", isDirectory: false)
|
||||||
let indexB = sessionDir.appendingPathComponent("index.htm", isDirectory: false)
|
let indexB = sessionDir.appendingPathComponent("index.htm", isDirectory: false)
|
||||||
if !FileManager().fileExists(atPath: indexA.path),
|
if !FileManager.default.fileExists(atPath: indexA.path),
|
||||||
!FileManager().fileExists(atPath: indexB.path)
|
!FileManager.default.fileExists(atPath: indexB.path)
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -233,7 +233,7 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
|
|||||||
// (Avoid treating Canvas routes like "/" as filesystem paths.)
|
// (Avoid treating Canvas routes like "/" as filesystem paths.)
|
||||||
if trimmed.hasPrefix("/") {
|
if trimmed.hasPrefix("/") {
|
||||||
var isDir: ObjCBool = false
|
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)
|
let url = URL(fileURLWithPath: trimmed)
|
||||||
canvasWindowLogger.debug("canvas load file \(url.absoluteString, privacy: .public)")
|
canvasWindowLogger.debug("canvas load file \(url.absoluteString, privacy: .public)")
|
||||||
self.loadFile(url)
|
self.loadFile(url)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ enum ClawdbotConfigFile {
|
|||||||
|
|
||||||
static func loadDict() -> [String: Any] {
|
static func loadDict() -> [String: Any] {
|
||||||
let url = self.url()
|
let url = self.url()
|
||||||
guard FileManager().fileExists(atPath: url.path) else { return [:] }
|
guard FileManager.default.fileExists(atPath: url.path) else { return [:] }
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: url)
|
let data = try Data(contentsOf: url)
|
||||||
guard let root = self.parseConfigData(data) else {
|
guard let root = self.parseConfigData(data) else {
|
||||||
@@ -38,7 +38,7 @@ enum ClawdbotConfigFile {
|
|||||||
do {
|
do {
|
||||||
let data = try JSONSerialization.data(withJSONObject: dict, options: [.prettyPrinted, .sortedKeys])
|
let data = try JSONSerialization.data(withJSONObject: dict, options: [.prettyPrinted, .sortedKeys])
|
||||||
let url = self.url()
|
let url = self.url()
|
||||||
try FileManager().createDirectory(
|
try FileManager.default.createDirectory(
|
||||||
at: url.deletingLastPathComponent(),
|
at: url.deletingLastPathComponent(),
|
||||||
withIntermediateDirectories: true)
|
withIntermediateDirectories: true)
|
||||||
try data.write(to: url, options: [.atomic])
|
try data.write(to: url, options: [.atomic])
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ enum ClawdbotPaths {
|
|||||||
if let override = ClawdbotEnv.path(self.stateDirEnv) {
|
if let override = ClawdbotEnv.path(self.stateDirEnv) {
|
||||||
return URL(fileURLWithPath: override, isDirectory: true)
|
return URL(fileURLWithPath: override, isDirectory: true)
|
||||||
}
|
}
|
||||||
return FileManager().homeDirectoryForCurrentUser
|
return FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".clawdbot", isDirectory: true)
|
.appendingPathComponent(".clawdbot", isDirectory: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ enum CommandResolver {
|
|||||||
|
|
||||||
static func gatewayEntrypoint(in root: URL) -> String? {
|
static func gatewayEntrypoint(in root: URL) -> String? {
|
||||||
let distEntry = root.appendingPathComponent("dist/index.js").path
|
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
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,16 +47,16 @@ enum CommandResolver {
|
|||||||
static func projectRoot() -> URL {
|
static func projectRoot() -> URL {
|
||||||
if let stored = UserDefaults.standard.string(forKey: self.projectRootDefaultsKey),
|
if let stored = UserDefaults.standard.string(forKey: self.projectRootDefaultsKey),
|
||||||
let url = self.expandPath(stored),
|
let url = self.expandPath(stored),
|
||||||
FileManager().fileExists(atPath: url.path)
|
FileManager.default.fileExists(atPath: url.path)
|
||||||
{
|
{
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
let fallback = FileManager().homeDirectoryForCurrentUser
|
let fallback = FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent("Projects/clawdbot")
|
.appendingPathComponent("Projects/clawdbot")
|
||||||
if FileManager().fileExists(atPath: fallback.path) {
|
if FileManager.default.fileExists(atPath: fallback.path) {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
return FileManager().homeDirectoryForCurrentUser
|
return FileManager.default.homeDirectoryForCurrentUser
|
||||||
}
|
}
|
||||||
|
|
||||||
static func setProjectRoot(_ path: String) {
|
static func setProjectRoot(_ path: String) {
|
||||||
@@ -70,7 +70,7 @@ enum CommandResolver {
|
|||||||
static func preferredPaths() -> [String] {
|
static func preferredPaths() -> [String] {
|
||||||
let current = ProcessInfo.processInfo.environment["PATH"]?
|
let current = ProcessInfo.processInfo.environment["PATH"]?
|
||||||
.split(separator: ":").map(String.init) ?? []
|
.split(separator: ":").map(String.init) ?? []
|
||||||
let home = FileManager().homeDirectoryForCurrentUser
|
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||||
let projectRoot = self.projectRoot()
|
let projectRoot = self.projectRoot()
|
||||||
return self.preferredPaths(home: home, current: current, projectRoot: projectRoot)
|
return self.preferredPaths(home: home, current: current, projectRoot: projectRoot)
|
||||||
}
|
}
|
||||||
@@ -99,10 +99,10 @@ enum CommandResolver {
|
|||||||
let bin = base.appendingPathComponent("bin")
|
let bin = base.appendingPathComponent("bin")
|
||||||
let nodeBin = base.appendingPathComponent("tools/node/bin")
|
let nodeBin = base.appendingPathComponent("tools/node/bin")
|
||||||
var paths: [String] = []
|
var paths: [String] = []
|
||||||
if FileManager().fileExists(atPath: bin.path) {
|
if FileManager.default.fileExists(atPath: bin.path) {
|
||||||
paths.append(bin.path)
|
paths.append(bin.path)
|
||||||
}
|
}
|
||||||
if FileManager().fileExists(atPath: nodeBin.path) {
|
if FileManager.default.fileExists(atPath: nodeBin.path) {
|
||||||
paths.append(nodeBin.path)
|
paths.append(nodeBin.path)
|
||||||
}
|
}
|
||||||
return paths
|
return paths
|
||||||
@@ -113,13 +113,13 @@ enum CommandResolver {
|
|||||||
|
|
||||||
// Volta
|
// Volta
|
||||||
let volta = home.appendingPathComponent(".volta/bin")
|
let volta = home.appendingPathComponent(".volta/bin")
|
||||||
if FileManager().fileExists(atPath: volta.path) {
|
if FileManager.default.fileExists(atPath: volta.path) {
|
||||||
bins.append(volta.path)
|
bins.append(volta.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// asdf
|
// asdf
|
||||||
let asdf = home.appendingPathComponent(".asdf/shims")
|
let asdf = home.appendingPathComponent(".asdf/shims")
|
||||||
if FileManager().fileExists(atPath: asdf.path) {
|
if FileManager.default.fileExists(atPath: asdf.path) {
|
||||||
bins.append(asdf.path)
|
bins.append(asdf.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,10 +137,10 @@ enum CommandResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func versionedNodeBinPaths(base: URL, suffix: String) -> [String] {
|
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]
|
let entries: [String]
|
||||||
do {
|
do {
|
||||||
entries = try FileManager().contentsOfDirectory(atPath: base.path)
|
entries = try FileManager.default.contentsOfDirectory(atPath: base.path)
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ enum CommandResolver {
|
|||||||
for entry in sorted {
|
for entry in sorted {
|
||||||
let binDir = base.appendingPathComponent(entry).appendingPathComponent(suffix)
|
let binDir = base.appendingPathComponent(entry).appendingPathComponent(suffix)
|
||||||
let node = binDir.appendingPathComponent("node")
|
let node = binDir.appendingPathComponent("node")
|
||||||
if FileManager().isExecutableFile(atPath: node.path) {
|
if FileManager.default.isExecutableFile(atPath: node.path) {
|
||||||
paths.append(binDir.path)
|
paths.append(binDir.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,7 +177,7 @@ enum CommandResolver {
|
|||||||
static func findExecutable(named name: String, searchPaths: [String]? = nil) -> String? {
|
static func findExecutable(named name: String, searchPaths: [String]? = nil) -> String? {
|
||||||
for dir in searchPaths ?? self.preferredPaths() {
|
for dir in searchPaths ?? self.preferredPaths() {
|
||||||
let candidate = (dir as NSString).appendingPathComponent(name)
|
let candidate = (dir as NSString).appendingPathComponent(name)
|
||||||
if FileManager().isExecutableFile(atPath: candidate) {
|
if FileManager.default.isExecutableFile(atPath: candidate) {
|
||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,12 +191,12 @@ enum CommandResolver {
|
|||||||
static func projectClawdbotExecutable(projectRoot: URL? = nil) -> String? {
|
static func projectClawdbotExecutable(projectRoot: URL? = nil) -> String? {
|
||||||
let root = projectRoot ?? self.projectRoot()
|
let root = projectRoot ?? self.projectRoot()
|
||||||
let candidate = root.appendingPathComponent("node_modules/.bin").appendingPathComponent(self.helperName).path
|
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? {
|
static func nodeCliPath() -> String? {
|
||||||
let candidate = self.projectRoot().appendingPathComponent("bin/clawdbot.js").path
|
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 {
|
static func hasAnyClawdbotInvoker(searchPaths: [String]? = nil) -> Bool {
|
||||||
@@ -459,7 +459,7 @@ enum CommandResolver {
|
|||||||
private static func expandPath(_ path: String) -> URL? {
|
private static func expandPath(_ path: String) -> URL? {
|
||||||
var expanded = path
|
var expanded = path
|
||||||
if expanded.hasPrefix("~") {
|
if expanded.hasPrefix("~") {
|
||||||
let home = FileManager().homeDirectoryForCurrentUser.path
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
expanded.replaceSubrange(expanded.startIndex...expanded.startIndex, with: home)
|
expanded.replaceSubrange(expanded.startIndex...expanded.startIndex, with: home)
|
||||||
}
|
}
|
||||||
return URL(fileURLWithPath: expanded)
|
return URL(fileURLWithPath: expanded)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ enum DebugActions {
|
|||||||
static func openLog() {
|
static func openLog() {
|
||||||
let path = self.pinoLogPath()
|
let path = self.pinoLogPath()
|
||||||
let url = URL(fileURLWithPath: path)
|
let url = URL(fileURLWithPath: path)
|
||||||
guard FileManager().fileExists(atPath: path) else {
|
guard FileManager.default.fileExists(atPath: path) else {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = "Log file not found"
|
alert.messageText = "Log file not found"
|
||||||
alert.informativeText = path
|
alert.informativeText = path
|
||||||
@@ -38,7 +38,7 @@ enum DebugActions {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static func openConfigFolder() {
|
static func openConfigFolder() {
|
||||||
let url = FileManager()
|
let url = FileManager.default
|
||||||
.homeDirectoryForCurrentUser
|
.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".clawdbot", isDirectory: true)
|
.appendingPathComponent(".clawdbot", isDirectory: true)
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([url])
|
NSWorkspace.shared.activateFileViewerSelecting([url])
|
||||||
@@ -55,7 +55,7 @@ enum DebugActions {
|
|||||||
}
|
}
|
||||||
let path = self.resolveSessionStorePath()
|
let path = self.resolveSessionStorePath()
|
||||||
let url = URL(fileURLWithPath: path)
|
let url = URL(fileURLWithPath: path)
|
||||||
if FileManager().fileExists(atPath: path) {
|
if FileManager.default.fileExists(atPath: path) {
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([url])
|
NSWorkspace.shared.activateFileViewerSelecting([url])
|
||||||
} else {
|
} else {
|
||||||
NSWorkspace.shared.open(url.deletingLastPathComponent())
|
NSWorkspace.shared.open(url.deletingLastPathComponent())
|
||||||
@@ -195,7 +195,7 @@ enum DebugActions {
|
|||||||
@MainActor
|
@MainActor
|
||||||
private static func resolveSessionStorePath() -> String {
|
private static func resolveSessionStorePath() -> String {
|
||||||
let defaultPath = SessionLoader.defaultStorePath
|
let defaultPath = SessionLoader.defaultStorePath
|
||||||
let configURL = FileManager().homeDirectoryForCurrentUser
|
let configURL = FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".clawdbot/clawdbot.json")
|
.appendingPathComponent(".clawdbot/clawdbot.json")
|
||||||
guard
|
guard
|
||||||
let data = try? Data(contentsOf: configURL),
|
let data = try? Data(contentsOf: configURL),
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ struct DebugSettings: View {
|
|||||||
Button("Save") { self.saveRelayRoot() }
|
Button("Save") { self.saveRelayRoot() }
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
Button("Reset") {
|
Button("Reset") {
|
||||||
let def = FileManager().homeDirectoryForCurrentUser
|
let def = FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent("Projects/clawdbot").path
|
.appendingPathComponent("Projects/clawdbot").path
|
||||||
self.gatewayRootInput = def
|
self.gatewayRootInput = def
|
||||||
self.saveRelayRoot()
|
self.saveRelayRoot()
|
||||||
@@ -743,7 +743,7 @@ struct DebugSettings: View {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys])
|
let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys])
|
||||||
try FileManager().createDirectory(
|
try FileManager.default.createDirectory(
|
||||||
at: url.deletingLastPathComponent(),
|
at: url.deletingLastPathComponent(),
|
||||||
withIntermediateDirectories: true)
|
withIntermediateDirectories: true)
|
||||||
try data.write(to: url, options: [.atomic])
|
try data.write(to: url, options: [.atomic])
|
||||||
@@ -776,7 +776,7 @@ struct DebugSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func configURL() -> URL {
|
private func configURL() -> URL {
|
||||||
FileManager().homeDirectoryForCurrentUser
|
FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".clawdbot")
|
.appendingPathComponent(".clawdbot")
|
||||||
.appendingPathComponent("clawdbot.json")
|
.appendingPathComponent("clawdbot.json")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ actor DiagnosticsFileLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nonisolated static func logDirectoryURL() -> URL {
|
nonisolated static func logDirectoryURL() -> URL {
|
||||||
let library = FileManager().urls(for: .libraryDirectory, in: .userDomainMask).first
|
let library = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first
|
||||||
?? FileManager().homeDirectoryForCurrentUser.appendingPathComponent("Library", isDirectory: true)
|
?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library", isDirectory: true)
|
||||||
return library
|
return library
|
||||||
.appendingPathComponent("Logs", isDirectory: true)
|
.appendingPathComponent("Logs", isDirectory: true)
|
||||||
.appendingPathComponent("Clawdbot", isDirectory: true)
|
.appendingPathComponent("Clawdbot", isDirectory: true)
|
||||||
@@ -43,7 +43,7 @@ actor DiagnosticsFileLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func clear() throws {
|
func clear() throws {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
let base = Self.logFileURL()
|
let base = Self.logFileURL()
|
||||||
if fm.fileExists(atPath: base.path) {
|
if fm.fileExists(atPath: base.path) {
|
||||||
try fm.removeItem(at: base)
|
try fm.removeItem(at: base)
|
||||||
@@ -67,7 +67,7 @@ actor DiagnosticsFileLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func ensureDirectory() throws {
|
private func ensureDirectory() throws {
|
||||||
try FileManager().createDirectory(
|
try FileManager.default.createDirectory(
|
||||||
at: Self.logDirectoryURL(),
|
at: Self.logDirectoryURL(),
|
||||||
withIntermediateDirectories: true)
|
withIntermediateDirectories: true)
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ actor DiagnosticsFileLog {
|
|||||||
line.append(data)
|
line.append(data)
|
||||||
line.append(0x0A) // newline
|
line.append(0x0A) // newline
|
||||||
|
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
if !fm.fileExists(atPath: url.path) {
|
if !fm.fileExists(atPath: url.path) {
|
||||||
fm.createFile(atPath: url.path, contents: nil)
|
fm.createFile(atPath: url.path, contents: nil)
|
||||||
}
|
}
|
||||||
@@ -92,13 +92,13 @@ actor DiagnosticsFileLog {
|
|||||||
|
|
||||||
private func rotateIfNeeded() throws {
|
private func rotateIfNeeded() throws {
|
||||||
let url = Self.logFileURL()
|
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
|
let size = attrs[.size] as? NSNumber
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
if size.int64Value < self.maxBytes { return }
|
if size.int64Value < self.maxBytes { return }
|
||||||
|
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
|
|
||||||
let oldest = self.rotatedURL(index: self.maxBackups)
|
let oldest = self.rotatedURL(index: self.maxBackups)
|
||||||
if fm.fileExists(atPath: oldest.path) {
|
if fm.fileExists(atPath: oldest.path) {
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ enum ExecApprovalsStore {
|
|||||||
|
|
||||||
static func readSnapshot() -> ExecApprovalsSnapshot {
|
static func readSnapshot() -> ExecApprovalsSnapshot {
|
||||||
let url = self.fileURL()
|
let url = self.fileURL()
|
||||||
guard FileManager().fileExists(atPath: url.path) else {
|
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||||
return ExecApprovalsSnapshot(
|
return ExecApprovalsSnapshot(
|
||||||
path: url.path,
|
path: url.path,
|
||||||
exists: false,
|
exists: false,
|
||||||
@@ -216,7 +216,7 @@ enum ExecApprovalsStore {
|
|||||||
|
|
||||||
static func loadFile() -> ExecApprovalsFile {
|
static func loadFile() -> ExecApprovalsFile {
|
||||||
let url = self.fileURL()
|
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: [:])
|
return ExecApprovalsFile(version: 1, socket: nil, defaults: nil, agents: [:])
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
@@ -238,11 +238,11 @@ enum ExecApprovalsStore {
|
|||||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||||
let data = try encoder.encode(file)
|
let data = try encoder.encode(file)
|
||||||
let url = self.fileURL()
|
let url = self.fileURL()
|
||||||
try FileManager().createDirectory(
|
try FileManager.default.createDirectory(
|
||||||
at: url.deletingLastPathComponent(),
|
at: url.deletingLastPathComponent(),
|
||||||
withIntermediateDirectories: true)
|
withIntermediateDirectories: true)
|
||||||
try data.write(to: url, options: [.atomic])
|
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 {
|
} catch {
|
||||||
self.logger.error("exec approvals save failed: \(error.localizedDescription, privacy: .public)")
|
self.logger.error("exec approvals save failed: \(error.localizedDescription, privacy: .public)")
|
||||||
}
|
}
|
||||||
@@ -442,11 +442,11 @@ enum ExecApprovalsStore {
|
|||||||
private static func expandPath(_ raw: String) -> String {
|
private static func expandPath(_ raw: String) -> String {
|
||||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if trimmed == "~" {
|
if trimmed == "~" {
|
||||||
return FileManager().homeDirectoryForCurrentUser.path
|
return FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
}
|
}
|
||||||
if trimmed.hasPrefix("~/") {
|
if trimmed.hasPrefix("~/") {
|
||||||
let suffix = trimmed.dropFirst(2)
|
let suffix = trimmed.dropFirst(2)
|
||||||
return FileManager().homeDirectoryForCurrentUser
|
return FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(String(suffix)).path
|
.appendingPathComponent(String(suffix)).path
|
||||||
}
|
}
|
||||||
return trimmed
|
return trimmed
|
||||||
@@ -497,7 +497,7 @@ struct ExecCommandResolution: Sendable {
|
|||||||
return expanded
|
return expanded
|
||||||
}
|
}
|
||||||
let base = cwd?.trimmingCharacters(in: .whitespacesAndNewlines)
|
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
|
return URL(fileURLWithPath: root).appendingPathComponent(expanded).path
|
||||||
}
|
}
|
||||||
let searchPaths = self.searchPaths(from: env)
|
let searchPaths = self.searchPaths(from: env)
|
||||||
|
|||||||
@@ -155,8 +155,7 @@ actor GatewayEndpointStore {
|
|||||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if !trimmed.isEmpty {
|
if !trimmed.isEmpty {
|
||||||
if let configToken = self.resolveConfigToken(isRemote: isRemote, root: root),
|
if let configToken = self.resolveConfigToken(isRemote: isRemote, root: root),
|
||||||
!configToken.isEmpty,
|
!configToken.isEmpty
|
||||||
configToken != trimmed
|
|
||||||
{
|
{
|
||||||
self.warnEnvOverrideOnce(
|
self.warnEnvOverrideOnce(
|
||||||
kind: .token,
|
kind: .token,
|
||||||
@@ -165,19 +164,32 @@ actor GatewayEndpointStore {
|
|||||||
}
|
}
|
||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
|
if isRemote {
|
||||||
if let configToken = self.resolveConfigToken(isRemote: isRemote, root: root),
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
!configToken.isEmpty
|
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),
|
if let token = launchdSnapshot?.token?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
!token.isEmpty
|
!token.isEmpty
|
||||||
{
|
{
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ enum GatewayLaunchAgentManager {
|
|||||||
private static let disableLaunchAgentMarker = ".clawdbot/disable-launchagent"
|
private static let disableLaunchAgentMarker = ".clawdbot/disable-launchagent"
|
||||||
|
|
||||||
private static var plistURL: URL {
|
private static var plistURL: URL {
|
||||||
FileManager().homeDirectoryForCurrentUser
|
FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent("Library/LaunchAgents/\(gatewayLaunchdLabel).plist")
|
.appendingPathComponent("Library/LaunchAgents/\(gatewayLaunchdLabel).plist")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +67,9 @@ enum GatewayLaunchAgentManager {
|
|||||||
|
|
||||||
extension GatewayLaunchAgentManager {
|
extension GatewayLaunchAgentManager {
|
||||||
private static func isLaunchAgentWriteDisabled() -> Bool {
|
private static func isLaunchAgentWriteDisabled() -> Bool {
|
||||||
let marker = FileManager().homeDirectoryForCurrentUser
|
let marker = FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(self.disableLaunchAgentMarker)
|
.appendingPathComponent(self.disableLaunchAgentMarker)
|
||||||
return FileManager().fileExists(atPath: marker.path)
|
return FileManager.default.fileExists(atPath: marker.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func readDaemonLoaded() async -> Bool? {
|
private static func readDaemonLoaded() async -> Bool? {
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ final class GatewayProcessManager {
|
|||||||
|
|
||||||
func clearLog() {
|
func clearLog() {
|
||||||
self.log = ""
|
self.log = ""
|
||||||
try? FileManager().removeItem(atPath: GatewayLaunchAgentManager.launchdGatewayLogPath())
|
try? FileManager.default.removeItem(atPath: GatewayLaunchAgentManager.launchdGatewayLogPath())
|
||||||
self.logger.debug("gateway log cleared")
|
self.logger.debug("gateway log cleared")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +378,7 @@ final class GatewayProcessManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private nonisolated static func readGatewayLog(path: String, limit: Int) -> String {
|
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 "" }
|
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return "" }
|
||||||
let text = String(data: data, encoding: .utf8) ?? ""
|
let text = String(data: data, encoding: .utf8) ?? ""
|
||||||
if text.count <= limit { return text }
|
if text.count <= limit { return text }
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import Foundation
|
|||||||
enum LaunchAgentManager {
|
enum LaunchAgentManager {
|
||||||
private static let legacyLaunchdLabel = "com.steipete.clawdbot"
|
private static let legacyLaunchdLabel = "com.steipete.clawdbot"
|
||||||
private static var plistURL: URL {
|
private static var plistURL: URL {
|
||||||
FileManager().homeDirectoryForCurrentUser
|
FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent("Library/LaunchAgents/com.clawdbot.mac.plist")
|
.appendingPathComponent("Library/LaunchAgents/com.clawdbot.mac.plist")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var legacyPlistURL: URL {
|
private static var legacyPlistURL: URL {
|
||||||
FileManager().homeDirectoryForCurrentUser
|
FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent("Library/LaunchAgents/\(legacyLaunchdLabel).plist")
|
.appendingPathComponent("Library/LaunchAgents/\(legacyLaunchdLabel).plist")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func status() async -> Bool {
|
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)"])
|
let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"])
|
||||||
return result == 0
|
return result == 0
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ enum LaunchAgentManager {
|
|||||||
static func set(enabled: Bool, bundlePath: String) async {
|
static func set(enabled: Bool, bundlePath: String) async {
|
||||||
if enabled {
|
if enabled {
|
||||||
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(self.legacyLaunchdLabel)"])
|
_ = 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)
|
self.writePlist(bundlePath: bundlePath)
|
||||||
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
|
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
|
||||||
_ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
|
_ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
|
||||||
@@ -29,7 +29,7 @@ enum LaunchAgentManager {
|
|||||||
} else {
|
} else {
|
||||||
// Disable autostart going forward but leave the current app running.
|
// 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).
|
// 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>
|
<string>\(bundlePath)/Contents/MacOS/Clawdbot</string>
|
||||||
</array>
|
</array>
|
||||||
<key>WorkingDirectory</key>
|
<key>WorkingDirectory</key>
|
||||||
<string>\(FileManager().homeDirectoryForCurrentUser.path)</string>
|
<string>\(FileManager.default.homeDirectoryForCurrentUser.path)</string>
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>KeepAlive</key>
|
<key>KeepAlive</key>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ enum LogLocator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func ensureLogDirExists() {
|
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 {
|
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.
|
/// Returns the newest log file under /tmp/clawdbot/ (rolling or stdout), or nil if none exist.
|
||||||
static func bestLogFile() -> URL? {
|
static func bestLogFile() -> URL? {
|
||||||
self.ensureLogDirExists()
|
self.ensureLogDirExists()
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
let files = (try? fm.contentsOfDirectory(
|
let files = (try? fm.contentsOfDirectory(
|
||||||
at: self.logDir,
|
at: self.logDir,
|
||||||
includingPropertiesForKeys: [.contentModificationDateKey],
|
includingPropertiesForKeys: [.contentModificationDateKey],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Foundation
|
|||||||
import JavaScriptCore
|
import JavaScriptCore
|
||||||
|
|
||||||
enum ModelCatalogLoader {
|
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
|
.appendingPathComponent("Projects/pi-mono/packages/ai/src/models.generated.ts").path
|
||||||
private static let logger = Logger(subsystem: "com.clawdbot", category: "models")
|
private static let logger = Logger(subsystem: "com.clawdbot", category: "models")
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ actor MacNodeRuntime {
|
|||||||
|
|
||||||
let sessionKey = self.mainSessionKey
|
let sessionKey = self.mainSessionKey
|
||||||
let path = try await CanvasManager.shared.snapshot(sessionKey: sessionKey, outPath: nil)
|
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))
|
let data = try Data(contentsOf: URL(fileURLWithPath: path))
|
||||||
guard let image = NSImage(data: data) else {
|
guard let image = NSImage(data: data) else {
|
||||||
return Self.errorResponse(req, code: .unavailable, message: "canvas snapshot decode failed")
|
return Self.errorResponse(req, code: .unavailable, message: "canvas snapshot decode failed")
|
||||||
@@ -206,7 +206,7 @@ actor MacNodeRuntime {
|
|||||||
includeAudio: params.includeAudio ?? true,
|
includeAudio: params.includeAudio ?? true,
|
||||||
deviceId: params.deviceId,
|
deviceId: params.deviceId,
|
||||||
outPath: nil)
|
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))
|
let data = try Data(contentsOf: URL(fileURLWithPath: res.path))
|
||||||
struct ClipPayload: Encodable {
|
struct ClipPayload: Encodable {
|
||||||
var format: String
|
var format: String
|
||||||
@@ -312,7 +312,7 @@ actor MacNodeRuntime {
|
|||||||
fps: params.fps,
|
fps: params.fps,
|
||||||
includeAudio: params.includeAudio,
|
includeAudio: params.includeAudio,
|
||||||
outPath: nil)
|
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))
|
let data = try Data(contentsOf: URL(fileURLWithPath: res.path))
|
||||||
struct ScreenPayload: Encodable {
|
struct ScreenPayload: Encodable {
|
||||||
var format: String
|
var format: String
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ actor PortGuardian {
|
|||||||
private var records: [Record] = []
|
private var records: [Record] = []
|
||||||
private let logger = Logger(subsystem: "com.clawdbot", category: "portguard")
|
private let logger = Logger(subsystem: "com.clawdbot", category: "portguard")
|
||||||
private nonisolated static let appSupportDir: URL = {
|
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)
|
return base.appendingPathComponent("Clawdbot", isDirectory: true)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ actor PortGuardian {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func record(port: Int, pid: Int32, command: String, mode: AppState.ConnectionMode) async {
|
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.removeAll { $0.pid == pid }
|
||||||
self.records.append(
|
self.records.append(
|
||||||
Record(
|
Record(
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ enum RuntimeLocator {
|
|||||||
// MARK: - Internals
|
// MARK: - Internals
|
||||||
|
|
||||||
private static func findExecutable(named name: String, searchPaths: [String]) -> String? {
|
private static func findExecutable(named name: String, searchPaths: [String]) -> String? {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
for dir in searchPaths {
|
for dir in searchPaths {
|
||||||
let candidate = (dir as NSString).appendingPathComponent(name)
|
let candidate = (dir as NSString).appendingPathComponent(name)
|
||||||
if fm.isExecutableFile(atPath: candidate) {
|
if fm.isExecutableFile(atPath: candidate) {
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ final class ScreenRecordService {
|
|||||||
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
return URL(fileURLWithPath: outPath)
|
return URL(fileURLWithPath: outPath)
|
||||||
}
|
}
|
||||||
return FileManager().temporaryDirectory
|
return FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-screen-record-\(UUID().uuidString).mp4")
|
.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 content = try await SCShareableContent.current
|
||||||
let displays = content.displays.sorted { $0.displayID < $1.displayID }
|
let displays = content.displays.sorted { $0.displayID < $1.displayID }
|
||||||
|
|||||||
@@ -66,12 +66,12 @@ enum SessionActions {
|
|||||||
let dir = URL(fileURLWithPath: storePath).deletingLastPathComponent()
|
let dir = URL(fileURLWithPath: storePath).deletingLastPathComponent()
|
||||||
urls.append(dir.appendingPathComponent("\(sessionId).jsonl"))
|
urls.append(dir.appendingPathComponent("\(sessionId).jsonl"))
|
||||||
}
|
}
|
||||||
let home = FileManager().homeDirectoryForCurrentUser
|
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||||
urls.append(home.appendingPathComponent(".clawdbot/sessions/\(sessionId).jsonl"))
|
urls.append(home.appendingPathComponent(".clawdbot/sessions/\(sessionId).jsonl"))
|
||||||
return urls
|
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 {
|
guard let url = existing else {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = "Session log not found"
|
alert.messageText = "Session log not found"
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ enum SessionLoader {
|
|||||||
static let fallbackContextTokens = 200_000
|
static let fallbackContextTokens = 200_000
|
||||||
|
|
||||||
static let defaultStorePath = standardize(
|
static let defaultStorePath = standardize(
|
||||||
FileManager().homeDirectoryForCurrentUser
|
FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".clawdbot/sessions/sessions.json").path)
|
.appendingPathComponent(".clawdbot/sessions/sessions.json").path)
|
||||||
|
|
||||||
static func loadSnapshot(
|
static func loadSnapshot(
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ enum SoundEffectCatalog {
|
|||||||
]
|
]
|
||||||
|
|
||||||
private static let searchRoots: [URL] = [
|
private static let searchRoots: [URL] = [
|
||||||
FileManager().homeDirectoryForCurrentUser.appendingPathComponent("Library/Sounds"),
|
FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Sounds"),
|
||||||
URL(fileURLWithPath: "/Library/Sounds"),
|
URL(fileURLWithPath: "/Library/Sounds"),
|
||||||
URL(fileURLWithPath: "/System/Applications/Mail.app/Contents/Resources"), // Mail “swoosh”
|
URL(fileURLWithPath: "/System/Applications/Mail.app/Contents/Resources"), // Mail “swoosh”
|
||||||
URL(fileURLWithPath: "/System/Library/Sounds"),
|
URL(fileURLWithPath: "/System/Library/Sounds"),
|
||||||
@@ -53,7 +53,7 @@ enum SoundEffectCatalog {
|
|||||||
private static let discoveredSoundMap: [String: URL] = {
|
private static let discoveredSoundMap: [String: URL] = {
|
||||||
var map: [String: URL] = [:]
|
var map: [String: URL] = [:]
|
||||||
for root in Self.searchRoots {
|
for root in Self.searchRoots {
|
||||||
guard let contents = try? FileManager().contentsOfDirectory(
|
guard let contents = try? FileManager.default.contentsOfDirectory(
|
||||||
at: root,
|
at: root,
|
||||||
includingPropertiesForKeys: nil,
|
includingPropertiesForKeys: nil,
|
||||||
options: [.skipsHiddenFiles])
|
options: [.skipsHiddenFiles])
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ final class TailscaleService {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
func checkAppInstallation() -> Bool {
|
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)")
|
self.logger.info("Tailscale app installed: \(installed)")
|
||||||
return installed
|
return installed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ final class VoicePushToTalkHotkey: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func startMonitoring() {
|
private func startMonitoring() {
|
||||||
// assert(Thread.isMainThread) - Removed for Swift 6
|
assert(Thread.isMainThread)
|
||||||
guard self.globalMonitor == nil, self.localMonitor == nil else { return }
|
guard self.globalMonitor == nil, self.localMonitor == nil else { return }
|
||||||
// Listen-only global monitor; we rely on Input Monitoring permission to receive events.
|
// Listen-only global monitor; we rely on Input Monitoring permission to receive events.
|
||||||
self.globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
|
self.globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
|
||||||
@@ -55,7 +55,7 @@ final class VoicePushToTalkHotkey: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func stopMonitoring() {
|
private func stopMonitoring() {
|
||||||
// assert(Thread.isMainThread) - Removed for Swift 6
|
assert(Thread.isMainThread)
|
||||||
if let globalMonitor {
|
if let globalMonitor {
|
||||||
NSEvent.removeMonitor(globalMonitor)
|
NSEvent.removeMonitor(globalMonitor)
|
||||||
self.globalMonitor = nil
|
self.globalMonitor = nil
|
||||||
@@ -75,11 +75,15 @@ final class VoicePushToTalkHotkey: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func withMainThread(_ block: @escaping @Sendable () -> Void) {
|
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) {
|
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.
|
// Right Option (keyCode 61) acts as a hold-to-talk modifier.
|
||||||
if keyCode == 61 {
|
if keyCode == 61 {
|
||||||
self.optionDown = modifierFlags.contains(.option)
|
self.optionDown = modifierFlags.contains(.option)
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ extension Request: Codable {
|
|||||||
|
|
||||||
// Shared transport settings
|
// Shared transport settings
|
||||||
public let controlSocketPath =
|
public let controlSocketPath =
|
||||||
FileManager()
|
FileManager.default
|
||||||
.homeDirectoryForCurrentUser
|
.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent("Library/Application Support/clawdbot/control.sock")
|
.appendingPathComponent("Library/Application Support/clawdbot/control.sock")
|
||||||
.path
|
.path
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -187,7 +187,7 @@ private func resolvedPassword(opts: WizardCliOptions, config: GatewayConfig) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func loadGatewayConfig() -> GatewayConfig {
|
private func loadGatewayConfig() -> GatewayConfig {
|
||||||
let url = FileManager().homeDirectoryForCurrentUser
|
let url = FileManager.default.homeDirectoryForCurrentUser
|
||||||
.appendingPathComponent(".clawdbot")
|
.appendingPathComponent(".clawdbot")
|
||||||
.appendingPathComponent("clawdbot.json")
|
.appendingPathComponent("clawdbot.json")
|
||||||
guard let data = try? Data(contentsOf: url) else { return GatewayConfig() }
|
guard let data = try? Data(contentsOf: url) else { return GatewayConfig() }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Testing
|
|||||||
struct AgentWorkspaceTests {
|
struct AgentWorkspaceTests {
|
||||||
@Test
|
@Test
|
||||||
func displayPathUsesTildeForHome() {
|
func displayPathUsesTildeForHome() {
|
||||||
let home = FileManager().homeDirectoryForCurrentUser
|
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||||
#expect(AgentWorkspace.displayPath(for: home) == "~")
|
#expect(AgentWorkspace.displayPath(for: home) == "~")
|
||||||
|
|
||||||
let inside = home.appendingPathComponent("Projects", isDirectory: true)
|
let inside = home.appendingPathComponent("Projects", isDirectory: true)
|
||||||
@@ -28,12 +28,12 @@ struct AgentWorkspaceTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
func bootstrapCreatesAgentsFileWhenMissing() throws {
|
func bootstrapCreatesAgentsFileWhenMissing() throws {
|
||||||
let tmp = FileManager().temporaryDirectory
|
let tmp = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
|
.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)
|
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)
|
let contents = try String(contentsOf: agentsURL, encoding: .utf8)
|
||||||
#expect(contents.contains("# AGENTS.md"))
|
#expect(contents.contains("# AGENTS.md"))
|
||||||
@@ -41,9 +41,9 @@ struct AgentWorkspaceTests {
|
|||||||
let identityURL = tmp.appendingPathComponent(AgentWorkspace.identityFilename)
|
let identityURL = tmp.appendingPathComponent(AgentWorkspace.identityFilename)
|
||||||
let userURL = tmp.appendingPathComponent(AgentWorkspace.userFilename)
|
let userURL = tmp.appendingPathComponent(AgentWorkspace.userFilename)
|
||||||
let bootstrapURL = tmp.appendingPathComponent(AgentWorkspace.bootstrapFilename)
|
let bootstrapURL = tmp.appendingPathComponent(AgentWorkspace.bootstrapFilename)
|
||||||
#expect(FileManager().fileExists(atPath: identityURL.path))
|
#expect(FileManager.default.fileExists(atPath: identityURL.path))
|
||||||
#expect(FileManager().fileExists(atPath: userURL.path))
|
#expect(FileManager.default.fileExists(atPath: userURL.path))
|
||||||
#expect(FileManager().fileExists(atPath: bootstrapURL.path))
|
#expect(FileManager.default.fileExists(atPath: bootstrapURL.path))
|
||||||
|
|
||||||
let second = try AgentWorkspace.bootstrap(workspaceURL: tmp)
|
let second = try AgentWorkspace.bootstrap(workspaceURL: tmp)
|
||||||
#expect(second == agentsURL)
|
#expect(second == agentsURL)
|
||||||
@@ -51,10 +51,10 @@ struct AgentWorkspaceTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
func bootstrapSafetyRejectsNonEmptyFolderWithoutAgents() throws {
|
func bootstrapSafetyRejectsNonEmptyFolderWithoutAgents() throws {
|
||||||
let tmp = FileManager().temporaryDirectory
|
let tmp = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
|
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
|
||||||
defer { try? FileManager().removeItem(at: tmp) }
|
defer { try? FileManager.default.removeItem(at: tmp) }
|
||||||
try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
|
||||||
let marker = tmp.appendingPathComponent("notes.txt")
|
let marker = tmp.appendingPathComponent("notes.txt")
|
||||||
try "hello".write(to: marker, atomically: true, encoding: .utf8)
|
try "hello".write(to: marker, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
@@ -69,10 +69,10 @@ struct AgentWorkspaceTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
func bootstrapSafetyAllowsExistingAgentsFile() throws {
|
func bootstrapSafetyAllowsExistingAgentsFile() throws {
|
||||||
let tmp = FileManager().temporaryDirectory
|
let tmp = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
|
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
|
||||||
defer { try? FileManager().removeItem(at: tmp) }
|
defer { try? FileManager.default.removeItem(at: tmp) }
|
||||||
try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
|
||||||
let agents = tmp.appendingPathComponent(AgentWorkspace.agentsFilename)
|
let agents = tmp.appendingPathComponent(AgentWorkspace.agentsFilename)
|
||||||
try "# AGENTS.md".write(to: agents, atomically: true, encoding: .utf8)
|
try "# AGENTS.md".write(to: agents, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
@@ -87,25 +87,25 @@ struct AgentWorkspaceTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
func bootstrapSkipsBootstrapFileWhenWorkspaceHasContent() throws {
|
func bootstrapSkipsBootstrapFileWhenWorkspaceHasContent() throws {
|
||||||
let tmp = FileManager().temporaryDirectory
|
let tmp = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
|
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
|
||||||
defer { try? FileManager().removeItem(at: tmp) }
|
defer { try? FileManager.default.removeItem(at: tmp) }
|
||||||
try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
|
||||||
let marker = tmp.appendingPathComponent("notes.txt")
|
let marker = tmp.appendingPathComponent("notes.txt")
|
||||||
try "hello".write(to: marker, atomically: true, encoding: .utf8)
|
try "hello".write(to: marker, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
_ = try AgentWorkspace.bootstrap(workspaceURL: tmp)
|
_ = try AgentWorkspace.bootstrap(workspaceURL: tmp)
|
||||||
|
|
||||||
let bootstrapURL = tmp.appendingPathComponent(AgentWorkspace.bootstrapFilename)
|
let bootstrapURL = tmp.appendingPathComponent(AgentWorkspace.bootstrapFilename)
|
||||||
#expect(!FileManager().fileExists(atPath: bootstrapURL.path))
|
#expect(!FileManager.default.fileExists(atPath: bootstrapURL.path))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func needsBootstrapFalseWhenIdentityAlreadySet() throws {
|
func needsBootstrapFalseWhenIdentityAlreadySet() throws {
|
||||||
let tmp = FileManager().temporaryDirectory
|
let tmp = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
|
.appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true)
|
||||||
defer { try? FileManager().removeItem(at: tmp) }
|
defer { try? FileManager.default.removeItem(at: tmp) }
|
||||||
try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
|
||||||
let identityURL = tmp.appendingPathComponent(AgentWorkspace.identityFilename)
|
let identityURL = tmp.appendingPathComponent(AgentWorkspace.identityFilename)
|
||||||
try """
|
try """
|
||||||
# IDENTITY.md - Agent Identity
|
# IDENTITY.md - Agent Identity
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import Testing
|
|||||||
struct AnthropicAuthResolverTests {
|
struct AnthropicAuthResolverTests {
|
||||||
@Test
|
@Test
|
||||||
func prefersOAuthFileOverEnv() throws {
|
func prefersOAuthFileOverEnv() throws {
|
||||||
let dir = FileManager().temporaryDirectory
|
let dir = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)", isDirectory: true)
|
.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 oauthFile = dir.appendingPathComponent("oauth.json")
|
||||||
let payload = [
|
let payload = [
|
||||||
"anthropic": [
|
"anthropic": [
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Testing
|
|||||||
@MainActor
|
@MainActor
|
||||||
struct CLIInstallerTests {
|
struct CLIInstallerTests {
|
||||||
@Test func installedLocationFindsExecutable() throws {
|
@Test func installedLocationFindsExecutable() throws {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
let root = fm.temporaryDirectory.appendingPathComponent(
|
let root = fm.temporaryDirectory.appendingPathComponent(
|
||||||
"clawdbot-cli-installer-\(UUID().uuidString)")
|
"clawdbot-cli-installer-\(UUID().uuidString)")
|
||||||
defer { try? fm.removeItem(at: root) }
|
defer { try? fm.removeItem(at: root) }
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import Testing
|
|||||||
private func makeTempDir() throws -> URL {
|
private func makeTempDir() throws -> URL {
|
||||||
let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||||
let dir = base.appendingPathComponent("clawdbot-canvaswatch-\(UUID().uuidString)", 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
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func detectsInPlaceFileWrites() async throws {
|
@Test func detectsInPlaceFileWrites() async throws {
|
||||||
let dir = try self.makeTempDir()
|
let dir = try self.makeTempDir()
|
||||||
defer { try? FileManager().removeItem(at: dir) }
|
defer { try? FileManager.default.removeItem(at: dir) }
|
||||||
|
|
||||||
let file = dir.appendingPathComponent("index.html")
|
let file = dir.appendingPathComponent("index.html")
|
||||||
try "hello".write(to: file, atomically: false, encoding: .utf8)
|
try "hello".write(to: file, atomically: false, encoding: .utf8)
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import Testing
|
|||||||
@MainActor
|
@MainActor
|
||||||
struct CanvasWindowSmokeTests {
|
struct CanvasWindowSmokeTests {
|
||||||
@Test func panelControllerShowsAndHides() async throws {
|
@Test func panelControllerShowsAndHides() async throws {
|
||||||
let root = FileManager().temporaryDirectory
|
let root = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-canvas-test-\(UUID().uuidString)")
|
.appendingPathComponent("clawdbot-canvas-test-\(UUID().uuidString)")
|
||||||
try FileManager().createDirectory(at: root, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||||
defer { try? FileManager().removeItem(at: root) }
|
defer { try? FileManager.default.removeItem(at: root) }
|
||||||
|
|
||||||
let anchor = { NSRect(x: 200, y: 400, width: 40, height: 40) }
|
let anchor = { NSRect(x: 200, y: 400, width: 40, height: 40) }
|
||||||
let controller = try CanvasWindowController(
|
let controller = try CanvasWindowController(
|
||||||
@@ -31,10 +31,10 @@ struct CanvasWindowSmokeTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test func windowControllerShowsAndCloses() async throws {
|
@Test func windowControllerShowsAndCloses() async throws {
|
||||||
let root = FileManager().temporaryDirectory
|
let root = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-canvas-test-\(UUID().uuidString)")
|
.appendingPathComponent("clawdbot-canvas-test-\(UUID().uuidString)")
|
||||||
try FileManager().createDirectory(at: root, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||||
defer { try? FileManager().removeItem(at: root) }
|
defer { try? FileManager.default.removeItem(at: root) }
|
||||||
|
|
||||||
let controller = try CanvasWindowController(
|
let controller = try CanvasWindowController(
|
||||||
sessionKey: "main",
|
sessionKey: "main",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Testing
|
|||||||
struct ClawdbotConfigFileTests {
|
struct ClawdbotConfigFileTests {
|
||||||
@Test
|
@Test
|
||||||
func configPathRespectsEnvOverride() async {
|
func configPathRespectsEnvOverride() async {
|
||||||
let override = FileManager().temporaryDirectory
|
let override = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
|
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
|
||||||
.appendingPathComponent("clawdbot.json")
|
.appendingPathComponent("clawdbot.json")
|
||||||
.path
|
.path
|
||||||
@@ -19,7 +19,7 @@ struct ClawdbotConfigFileTests {
|
|||||||
@MainActor
|
@MainActor
|
||||||
@Test
|
@Test
|
||||||
func remoteGatewayPortParsesAndMatchesHost() async {
|
func remoteGatewayPortParsesAndMatchesHost() async {
|
||||||
let override = FileManager().temporaryDirectory
|
let override = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
|
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
|
||||||
.appendingPathComponent("clawdbot.json")
|
.appendingPathComponent("clawdbot.json")
|
||||||
.path
|
.path
|
||||||
@@ -42,7 +42,7 @@ struct ClawdbotConfigFileTests {
|
|||||||
@MainActor
|
@MainActor
|
||||||
@Test
|
@Test
|
||||||
func setRemoteGatewayUrlPreservesScheme() async {
|
func setRemoteGatewayUrlPreservesScheme() async {
|
||||||
let override = FileManager().temporaryDirectory
|
let override = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
|
.appendingPathComponent("clawdbot-config-\(UUID().uuidString)")
|
||||||
.appendingPathComponent("clawdbot.json")
|
.appendingPathComponent("clawdbot.json")
|
||||||
.path
|
.path
|
||||||
@@ -64,7 +64,7 @@ struct ClawdbotConfigFileTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
func stateDirOverrideSetsConfigPath() async {
|
func stateDirOverrideSetsConfigPath() async {
|
||||||
let dir = FileManager().temporaryDirectory
|
let dir = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-state-\(UUID().uuidString)", isDirectory: true)
|
.appendingPathComponent("clawdbot-state-\(UUID().uuidString)", isDirectory: true)
|
||||||
.path
|
.path
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Testing
|
|||||||
struct ClawdbotOAuthStoreTests {
|
struct ClawdbotOAuthStoreTests {
|
||||||
@Test
|
@Test
|
||||||
func returnsMissingWhenFileAbsent() {
|
func returnsMissingWhenFileAbsent() {
|
||||||
let url = FileManager().temporaryDirectory
|
let url = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)")
|
.appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)")
|
||||||
.appendingPathComponent("oauth.json")
|
.appendingPathComponent("oauth.json")
|
||||||
#expect(ClawdbotOAuthStore.anthropicOAuthStatus(at: url) == .missingFile)
|
#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)
|
.appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)", isDirectory: true)
|
||||||
setenv(key, dir.path, 1)
|
setenv(key, dir.path, 1)
|
||||||
|
|
||||||
@@ -85,9 +85,9 @@ struct ClawdbotOAuthStoreTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func writeOAuthFile(_ json: [String: Any]) throws -> URL {
|
private func writeOAuthFile(_ json: [String: Any]) throws -> URL {
|
||||||
let dir = FileManager().temporaryDirectory
|
let dir = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)", isDirectory: true)
|
.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 url = dir.appendingPathComponent("oauth.json")
|
||||||
let data = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys])
|
let data = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys])
|
||||||
|
|||||||
@@ -12,16 +12,16 @@ import Testing
|
|||||||
private func makeTempDir() throws -> URL {
|
private func makeTempDir() throws -> URL {
|
||||||
let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||||
let dir = base.appendingPathComponent(UUID().uuidString, 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
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeExec(at path: URL) throws {
|
private func makeExec(at path: URL) throws {
|
||||||
try FileManager().createDirectory(
|
try FileManager.default.createDirectory(
|
||||||
at: path.deletingLastPathComponent(),
|
at: path.deletingLastPathComponent(),
|
||||||
withIntermediateDirectories: true)
|
withIntermediateDirectories: true)
|
||||||
FileManager().createFile(atPath: path.path, contents: Data("echo ok\n".utf8))
|
FileManager.default.createFile(atPath: path.path, contents: Data("echo ok\n".utf8))
|
||||||
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path)
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func prefersClawdbotBinary() async throws {
|
@Test func prefersClawdbotBinary() async throws {
|
||||||
@@ -49,7 +49,7 @@ import Testing
|
|||||||
let scriptPath = tmp.appendingPathComponent("bin/clawdbot.js")
|
let scriptPath = tmp.appendingPathComponent("bin/clawdbot.js")
|
||||||
try self.makeExec(at: nodePath)
|
try self.makeExec(at: nodePath)
|
||||||
try "#!/bin/sh\necho v22.0.0\n".write(to: nodePath, atomically: true, encoding: .utf8)
|
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)
|
try self.makeExec(at: scriptPath)
|
||||||
|
|
||||||
let cmd = CommandResolver.clawdbotCommand(
|
let cmd = CommandResolver.clawdbotCommand(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import Testing
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func swiftFiles(under root: URL) throws -> [URL] {
|
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 {
|
guard let enumerator = fm.enumerator(at: root, includingPropertiesForKeys: [.isRegularFileKey]) else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Testing
|
|||||||
|
|
||||||
@Suite struct GatewayLaunchAgentManagerTests {
|
@Suite struct GatewayLaunchAgentManagerTests {
|
||||||
@Test func launchAgentPlistSnapshotParsesArgsAndEnv() throws {
|
@Test func launchAgentPlistSnapshotParsesArgsAndEnv() throws {
|
||||||
let url = FileManager().temporaryDirectory
|
let url = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-launchd-\(UUID().uuidString).plist")
|
.appendingPathComponent("clawdbot-launchd-\(UUID().uuidString).plist")
|
||||||
let plist: [String: Any] = [
|
let plist: [String: Any] = [
|
||||||
"ProgramArguments": ["clawdbot", "gateway-daemon", "--port", "18789", "--bind", "loopback"],
|
"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)
|
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
|
||||||
try data.write(to: url, options: [.atomic])
|
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))
|
let snapshot = try #require(LaunchAgentPlist.snapshot(url: url))
|
||||||
#expect(snapshot.port == 18789)
|
#expect(snapshot.port == 18789)
|
||||||
@@ -25,14 +25,14 @@ import Testing
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test func launchAgentPlistSnapshotAllowsMissingBind() throws {
|
@Test func launchAgentPlistSnapshotAllowsMissingBind() throws {
|
||||||
let url = FileManager().temporaryDirectory
|
let url = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-launchd-\(UUID().uuidString).plist")
|
.appendingPathComponent("clawdbot-launchd-\(UUID().uuidString).plist")
|
||||||
let plist: [String: Any] = [
|
let plist: [String: Any] = [
|
||||||
"ProgramArguments": ["clawdbot", "gateway-daemon", "--port", "18789"],
|
"ProgramArguments": ["clawdbot", "gateway-daemon", "--port", "18789"],
|
||||||
]
|
]
|
||||||
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
|
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
|
||||||
try data.write(to: url, options: [.atomic])
|
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))
|
let snapshot = try #require(LaunchAgentPlist.snapshot(url: url))
|
||||||
#expect(snapshot.port == 18789)
|
#expect(snapshot.port == 18789)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Testing
|
|||||||
|
|
||||||
@Suite struct LogLocatorTests {
|
@Suite struct LogLocatorTests {
|
||||||
@Test func launchdGatewayLogPathEnsuresTmpDirExists() throws {
|
@Test func launchdGatewayLogPathEnsuresTmpDirExists() throws {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
let baseDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
let baseDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||||
let logDir = baseDir.appendingPathComponent("clawdbot-tests-\(UUID().uuidString)")
|
let logDir = baseDir.appendingPathComponent("clawdbot-tests-\(UUID().uuidString)")
|
||||||
|
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ struct LowCoverageHelperTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test func pairedNodesStorePersists() async throws {
|
@Test func pairedNodesStorePersists() async throws {
|
||||||
let dir = FileManager().temporaryDirectory
|
let dir = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("paired-\(UUID().uuidString)", isDirectory: true)
|
.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 url = dir.appendingPathComponent("nodes.json")
|
||||||
let store = PairedNodesStore(fileURL: url)
|
let store = PairedNodesStore(fileURL: url)
|
||||||
await store.load()
|
await store.load()
|
||||||
@@ -143,12 +143,12 @@ struct LowCoverageHelperTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test @MainActor func canvasSchemeHandlerResolvesFilesAndErrors() throws {
|
@Test @MainActor func canvasSchemeHandlerResolvesFilesAndErrors() throws {
|
||||||
let root = FileManager().temporaryDirectory
|
let root = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("canvas-\(UUID().uuidString)", isDirectory: true)
|
.appendingPathComponent("canvas-\(UUID().uuidString)", isDirectory: true)
|
||||||
defer { try? FileManager().removeItem(at: root) }
|
defer { try? FileManager.default.removeItem(at: root) }
|
||||||
try FileManager().createDirectory(at: root, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||||
let session = root.appendingPathComponent("main", isDirectory: 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")
|
let index = session.appendingPathComponent("index.html")
|
||||||
try "<h1>Hello</h1>".write(to: index, atomically: true, encoding: .utf8)
|
try "<h1>Hello</h1>".write(to: index, atomically: true, encoding: .utf8)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ struct MacNodeRuntimeTests {
|
|||||||
includeAudio: Bool?,
|
includeAudio: Bool?,
|
||||||
outPath: String?) async throws -> (path: String, hasAudio: 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")
|
.appendingPathComponent("clawdbot-test-screen-record-\(UUID().uuidString).mp4")
|
||||||
try Data("ok".utf8).write(to: url)
|
try Data("ok".utf8).write(to: url)
|
||||||
return (path: url.path, hasAudio: false)
|
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")
|
.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)
|
try src.write(to: tmp, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
let choices = try await ModelCatalogLoader.load(from: tmp.path)
|
let choices = try await ModelCatalogLoader.load(from: tmp.path)
|
||||||
@@ -42,9 +42,9 @@ struct ModelCatalogLoaderTests {
|
|||||||
@Test
|
@Test
|
||||||
func loadWithNoExportReturnsEmptyChoices() async throws {
|
func loadWithNoExportReturnsEmptyChoices() async throws {
|
||||||
let src = "const NOPE = 1;"
|
let src = "const NOPE = 1;"
|
||||||
let tmp = FileManager().temporaryDirectory
|
let tmp = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("models-\(UUID().uuidString).ts")
|
.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)
|
try src.write(to: tmp, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
let choices = try await ModelCatalogLoader.load(from: tmp.path)
|
let choices = try await ModelCatalogLoader.load(from: tmp.path)
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ import Testing
|
|||||||
private func makeTempDir() throws -> URL {
|
private func makeTempDir() throws -> URL {
|
||||||
let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||||
let dir = base.appendingPathComponent(UUID().uuidString, 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
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeExec(at path: URL) throws {
|
private func makeExec(at path: URL) throws {
|
||||||
try FileManager().createDirectory(
|
try FileManager.default.createDirectory(
|
||||||
at: path.deletingLastPathComponent(),
|
at: path.deletingLastPathComponent(),
|
||||||
withIntermediateDirectories: true)
|
withIntermediateDirectories: true)
|
||||||
FileManager().createFile(atPath: path.path, contents: Data("echo ok\n".utf8))
|
FileManager.default.createFile(atPath: path.path, contents: Data("echo ok\n".utf8))
|
||||||
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path)
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func fnmNodeBinsPreferNewestInstalledVersion() throws {
|
@Test func fnmNodeBinsPreferNewestInstalledVersion() throws {
|
||||||
@@ -37,7 +37,7 @@ import Testing
|
|||||||
let home = try self.makeTempDir()
|
let home = try self.makeTempDir()
|
||||||
let missingNodeBin = home
|
let missingNodeBin = home
|
||||||
.appendingPathComponent(".local/share/fnm/node-versions/v99.0.0/installation/bin")
|
.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)
|
let bins = CommandResolver._testNodeManagerBinPaths(home: home)
|
||||||
#expect(!bins.contains(missingNodeBin.path))
|
#expect(!bins.contains(missingNodeBin.path))
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import Testing
|
|||||||
private func makeTempExecutable(contents: String) throws -> URL {
|
private func makeTempExecutable(contents: String) throws -> URL {
|
||||||
let dir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
let dir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||||
.appendingPathComponent(UUID().uuidString, 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")
|
let path = dir.appendingPathComponent("node")
|
||||||
try contents.write(to: path, atomically: true, encoding: .utf8)
|
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
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ enum TestIsolation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nonisolated static func tempConfigPath() -> String {
|
nonisolated static func tempConfigPath() -> String {
|
||||||
FileManager().temporaryDirectory
|
FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdbot-test-config-\(UUID().uuidString).json")
|
.appendingPathComponent("clawdbot-test-config-\(UUID().uuidString).json")
|
||||||
.path
|
.path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,17 +47,17 @@ import Testing
|
|||||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||||
let dist = tmp.appendingPathComponent("dist/index.js")
|
let dist = tmp.appendingPathComponent("dist/index.js")
|
||||||
let bin = tmp.appendingPathComponent("bin/clawdbot.js")
|
let bin = tmp.appendingPathComponent("bin/clawdbot.js")
|
||||||
try FileManager().createDirectory(at: dist.deletingLastPathComponent(), withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: dist.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||||
try FileManager().createDirectory(at: bin.deletingLastPathComponent(), withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: bin.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||||
FileManager().createFile(atPath: dist.path, contents: Data())
|
FileManager.default.createFile(atPath: dist.path, contents: Data())
|
||||||
FileManager().createFile(atPath: bin.path, contents: Data())
|
FileManager.default.createFile(atPath: bin.path, contents: Data())
|
||||||
|
|
||||||
let entry = CommandResolver.gatewayEntrypoint(in: tmp)
|
let entry = CommandResolver.gatewayEntrypoint(in: tmp)
|
||||||
#expect(entry == dist.path)
|
#expect(entry == dist.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func logLocatorPicksNewestLogFile() throws {
|
@Test func logLocatorPicksNewestLogFile() throws {
|
||||||
let fm = FileManager()
|
let fm = FileManager.default
|
||||||
let dir = URL(fileURLWithPath: "/tmp/clawdbot", isDirectory: true)
|
let dir = URL(fileURLWithPath: "/tmp/clawdbot", isDirectory: true)
|
||||||
try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
|
try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Foundation
|
|||||||
|
|
||||||
public enum ClawdbotNodeStorage {
|
public enum ClawdbotNodeStorage {
|
||||||
public static func appSupportDir() throws -> URL {
|
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 {
|
guard let base else {
|
||||||
throw NSError(domain: "ClawdbotNodeStorage", code: 1, userInfo: [
|
throw NSError(domain: "ClawdbotNodeStorage", code: 1, userInfo: [
|
||||||
NSLocalizedDescriptionKey: "Application Support directory unavailable",
|
NSLocalizedDescriptionKey: "Application Support directory unavailable",
|
||||||
@@ -19,7 +19,7 @@ public enum ClawdbotNodeStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func cachesDir() throws -> URL {
|
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 {
|
guard let base else {
|
||||||
throw NSError(domain: "ClawdbotNodeStorage", code: 2, userInfo: [
|
throw NSError(domain: "ClawdbotNodeStorage", code: 2, userInfo: [
|
||||||
NSLocalizedDescriptionKey: "Caches directory unavailable",
|
NSLocalizedDescriptionKey: "Caches directory unavailable",
|
||||||
|
|||||||
+1
-7
@@ -28,11 +28,9 @@ export AWS_REGION="us-east-1"
|
|||||||
# Optional:
|
# Optional:
|
||||||
export AWS_SESSION_TOKEN="..."
|
export AWS_SESSION_TOKEN="..."
|
||||||
export AWS_PROFILE="your-profile"
|
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
|
```json5
|
||||||
{
|
{
|
||||||
@@ -41,7 +39,6 @@ export AWS_BEARER_TOKEN_BEDROCK="..."
|
|||||||
"amazon-bedrock": {
|
"amazon-bedrock": {
|
||||||
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||||
api: "bedrock-converse-stream",
|
api: "bedrock-converse-stream",
|
||||||
auth: "aws-sdk",
|
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
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.
|
- Bedrock requires **model access** enabled in your AWS account/region.
|
||||||
- If you use profiles, set `AWS_PROFILE` on the gateway host.
|
- 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
|
- Reasoning support depends on the model; check the Bedrock model card for
|
||||||
current capabilities.
|
current capabilities.
|
||||||
- If you prefer a managed key flow, you can also place an OpenAI‑compatible
|
- If you prefer a managed key flow, you can also place an OpenAI‑compatible
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ Text is supported everywhere; media and reactions vary by channel.
|
|||||||
- [iMessage](/channels/imessage) — macOS only; native integration.
|
- [iMessage](/channels/imessage) — macOS only; native integration.
|
||||||
- [BlueBubbles](/channels/bluebubbles) — iMessage via BlueBubbles macOS server (bundled plugin, disabled by default).
|
- [BlueBubbles](/channels/bluebubbles) — iMessage via BlueBubbles macOS server (bundled plugin, disabled by default).
|
||||||
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
|
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
|
||||||
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
|
|
||||||
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
||||||
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
|
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
|
||||||
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
|
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "Nextcloud Talk support status, capabilities, and configuration"
|
|
||||||
read_when:
|
|
||||||
- Working on Nextcloud Talk channel features
|
|
||||||
---
|
|
||||||
# Nextcloud Talk (plugin)
|
|
||||||
|
|
||||||
Status: supported via plugin (webhook bot). Direct messages, rooms, reactions, and markdown messages are supported.
|
|
||||||
|
|
||||||
## Plugin required
|
|
||||||
Nextcloud Talk ships as a plugin and is not bundled with the core install.
|
|
||||||
|
|
||||||
Install via CLI (npm registry):
|
|
||||||
```bash
|
|
||||||
clawdbot plugins install @clawdbot/nextcloud-talk
|
|
||||||
```
|
|
||||||
|
|
||||||
Local checkout (when running from a git repo):
|
|
||||||
```bash
|
|
||||||
clawdbot plugins install ./extensions/nextcloud-talk
|
|
||||||
```
|
|
||||||
|
|
||||||
If you choose Nextcloud Talk during configure/onboarding and a git checkout is detected,
|
|
||||||
Clawdbot will offer the local install path automatically.
|
|
||||||
|
|
||||||
Details: [Plugins](/plugin)
|
|
||||||
|
|
||||||
## Quick setup (beginner)
|
|
||||||
1) Install the Nextcloud Talk plugin.
|
|
||||||
2) On your Nextcloud server, create a bot:
|
|
||||||
```bash
|
|
||||||
./occ talk:bot:install "Clawdbot" "<shared-secret>" "<webhook-url>" --feature reaction
|
|
||||||
```
|
|
||||||
3) Enable the bot in the target room settings.
|
|
||||||
4) Configure Clawdbot:
|
|
||||||
- Config: `channels.nextcloud-talk.baseUrl` + `channels.nextcloud-talk.botSecret`
|
|
||||||
- Or env: `NEXTCLOUD_TALK_BOT_SECRET` (default account only)
|
|
||||||
5) Restart the gateway (or finish onboarding).
|
|
||||||
|
|
||||||
Minimal config:
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
channels: {
|
|
||||||
"nextcloud-talk": {
|
|
||||||
enabled: true,
|
|
||||||
baseUrl: "https://cloud.example.com",
|
|
||||||
botSecret: "shared-secret",
|
|
||||||
dmPolicy: "pairing"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- Bots cannot initiate DMs. The user must message the bot first.
|
|
||||||
- Webhook URL must be reachable by the Gateway; set `webhookPublicUrl` if behind a proxy.
|
|
||||||
- Media uploads are not supported by the bot API; media is sent as URLs.
|
|
||||||
- The webhook payload does not distinguish DMs vs rooms; set `apiUser` + `apiPassword` to enable room-type lookups (otherwise DMs are treated as rooms).
|
|
||||||
|
|
||||||
## Access control (DMs)
|
|
||||||
- Default: `channels.nextcloud-talk.dmPolicy = "pairing"`. Unknown senders get a pairing code.
|
|
||||||
- Approve via:
|
|
||||||
- `clawdbot pairing list nextcloud-talk`
|
|
||||||
- `clawdbot pairing approve nextcloud-talk <CODE>`
|
|
||||||
- Public DMs: `channels.nextcloud-talk.dmPolicy="open"` plus `channels.nextcloud-talk.allowFrom=["*"]`.
|
|
||||||
|
|
||||||
## Rooms (groups)
|
|
||||||
- Default: `channels.nextcloud-talk.groupPolicy = "allowlist"` (mention-gated).
|
|
||||||
- Allowlist rooms with `channels.nextcloud-talk.rooms`:
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
channels: {
|
|
||||||
"nextcloud-talk": {
|
|
||||||
rooms: {
|
|
||||||
"room-token": { requireMention: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- To allow no rooms, keep the allowlist empty or set `channels.nextcloud-talk.groupPolicy="disabled"`.
|
|
||||||
|
|
||||||
## Capabilities
|
|
||||||
| Feature | Status |
|
|
||||||
|---------|--------|
|
|
||||||
| Direct messages | Supported |
|
|
||||||
| Rooms | Supported |
|
|
||||||
| Threads | Not supported |
|
|
||||||
| Media | URL-only |
|
|
||||||
| Reactions | Supported |
|
|
||||||
| Native commands | Not supported |
|
|
||||||
|
|
||||||
## Configuration reference (Nextcloud Talk)
|
|
||||||
Full configuration: [Configuration](/gateway/configuration)
|
|
||||||
|
|
||||||
Provider options:
|
|
||||||
- `channels.nextcloud-talk.enabled`: enable/disable channel startup.
|
|
||||||
- `channels.nextcloud-talk.baseUrl`: Nextcloud instance URL.
|
|
||||||
- `channels.nextcloud-talk.botSecret`: bot shared secret.
|
|
||||||
- `channels.nextcloud-talk.botSecretFile`: secret file path.
|
|
||||||
- `channels.nextcloud-talk.apiUser`: API user for room lookups (DM detection).
|
|
||||||
- `channels.nextcloud-talk.apiPassword`: API/app password for room lookups.
|
|
||||||
- `channels.nextcloud-talk.apiPasswordFile`: API password file path.
|
|
||||||
- `channels.nextcloud-talk.webhookPort`: webhook listener port (default: 8788).
|
|
||||||
- `channels.nextcloud-talk.webhookHost`: webhook host (default: 0.0.0.0).
|
|
||||||
- `channels.nextcloud-talk.webhookPath`: webhook path (default: /nextcloud-talk-webhook).
|
|
||||||
- `channels.nextcloud-talk.webhookPublicUrl`: externally reachable webhook URL.
|
|
||||||
- `channels.nextcloud-talk.dmPolicy`: `pairing | allowlist | open | disabled`.
|
|
||||||
- `channels.nextcloud-talk.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`.
|
|
||||||
- `channels.nextcloud-talk.groupPolicy`: `allowlist | open | disabled`.
|
|
||||||
- `channels.nextcloud-talk.groupAllowFrom`: group allowlist (user IDs).
|
|
||||||
- `channels.nextcloud-talk.rooms`: per-room settings and allowlist.
|
|
||||||
- `channels.nextcloud-talk.historyLimit`: group history limit (0 disables).
|
|
||||||
- `channels.nextcloud-talk.dmHistoryLimit`: DM history limit (0 disables).
|
|
||||||
- `channels.nextcloud-talk.dms`: per-DM overrides (historyLimit).
|
|
||||||
- `channels.nextcloud-talk.textChunkLimit`: outbound text chunk size (chars).
|
|
||||||
- `channels.nextcloud-talk.blockStreaming`: disable block streaming for this channel.
|
|
||||||
- `channels.nextcloud-talk.blockStreamingCoalesce`: block streaming coalesce tuning.
|
|
||||||
- `channels.nextcloud-talk.mediaMaxMb`: inbound media cap (MB).
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "CLI reference for `clawdbot devices` (device pairing + token rotation/revocation)"
|
|
||||||
read_when:
|
|
||||||
- You are approving device pairing requests
|
|
||||||
- You need to rotate or revoke device tokens
|
|
||||||
---
|
|
||||||
|
|
||||||
# `clawdbot devices`
|
|
||||||
|
|
||||||
Manage device pairing requests and device-scoped tokens.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### `clawdbot devices list`
|
|
||||||
|
|
||||||
List pending pairing requests and paired devices.
|
|
||||||
|
|
||||||
```
|
|
||||||
clawdbot devices list
|
|
||||||
clawdbot devices list --json
|
|
||||||
```
|
|
||||||
|
|
||||||
### `clawdbot devices approve <requestId>`
|
|
||||||
|
|
||||||
Approve a pending device pairing request.
|
|
||||||
|
|
||||||
```
|
|
||||||
clawdbot devices approve <requestId>
|
|
||||||
```
|
|
||||||
|
|
||||||
### `clawdbot devices reject <requestId>`
|
|
||||||
|
|
||||||
Reject a pending device pairing request.
|
|
||||||
|
|
||||||
```
|
|
||||||
clawdbot devices reject <requestId>
|
|
||||||
```
|
|
||||||
|
|
||||||
### `clawdbot devices rotate --device <id> --role <role> [--scope <scope...>]`
|
|
||||||
|
|
||||||
Rotate a device token for a specific role (optionally updating scopes).
|
|
||||||
|
|
||||||
```
|
|
||||||
clawdbot devices rotate --device <deviceId> --role operator --scope operator.read --scope operator.write
|
|
||||||
```
|
|
||||||
|
|
||||||
### `clawdbot devices revoke --device <id> --role <role>`
|
|
||||||
|
|
||||||
Revoke a device token for a specific role.
|
|
||||||
|
|
||||||
```
|
|
||||||
clawdbot devices revoke --device <deviceId> --role node
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common options
|
|
||||||
|
|
||||||
- `--url <url>`: Gateway WebSocket URL (defaults to `gateway.remote.url` when configured).
|
|
||||||
- `--token <token>`: Gateway token (if required).
|
|
||||||
- `--password <password>`: Gateway password (password auth).
|
|
||||||
- `--timeout <ms>`: RPC timeout.
|
|
||||||
- `--json`: JSON output (recommended for scripting).
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Token rotation returns a new token (sensitive). Treat it like a secret.
|
|
||||||
- These commands require `operator.pairing` (or `operator.admin`) scope.
|
|
||||||
+2
-3
@@ -116,18 +116,17 @@ clawdbot gateway call logs.tail --params '{"sinceMs": 60000}'
|
|||||||
|
|
||||||
## Discover gateways (Bonjour)
|
## Discover gateways (Bonjour)
|
||||||
|
|
||||||
`gateway discover` scans for Gateway beacons (`_clawdbot-gateway._tcp`).
|
`gateway discover` scans for Gateway bridge beacons (`_clawdbot-bridge._tcp`).
|
||||||
|
|
||||||
- Multicast DNS-SD: `local.`
|
- Multicast DNS-SD: `local.`
|
||||||
- Unicast DNS-SD (Wide-Area Bonjour): `clawdbot.internal.` (requires split DNS + DNS server; see [/gateway/bonjour](/gateway/bonjour))
|
- Unicast DNS-SD (Wide-Area Bonjour): `clawdbot.internal.` (requires split DNS + DNS server; see [/gateway/bonjour](/gateway/bonjour))
|
||||||
|
|
||||||
Only gateways with Bonjour discovery enabled (default) advertise the beacon.
|
Only gateways with the **bridge enabled** will advertise the discovery beacon.
|
||||||
|
|
||||||
Wide-Area discovery records include (TXT):
|
Wide-Area discovery records include (TXT):
|
||||||
- `gatewayPort` (WebSocket port, usually `18789`)
|
- `gatewayPort` (WebSocket port, usually `18789`)
|
||||||
- `sshPort` (SSH port; defaults to `22` if not present)
|
- `sshPort` (SSH port; defaults to `22` if not present)
|
||||||
- `tailnetDns` (MagicDNS hostname, when available)
|
- `tailnetDns` (MagicDNS hostname, when available)
|
||||||
- `gatewayTls` / `gatewayTlsSha256` (TLS enabled + cert fingerprint)
|
|
||||||
- `cliPath` (optional hint for remote installs)
|
- `cliPath` (optional hint for remote installs)
|
||||||
|
|
||||||
### `gateway discover`
|
### `gateway discover`
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ This page describes the current CLI behavior. If commands change, update this do
|
|||||||
- [`models`](/cli/models)
|
- [`models`](/cli/models)
|
||||||
- [`memory`](/cli/memory)
|
- [`memory`](/cli/memory)
|
||||||
- [`nodes`](/cli/nodes)
|
- [`nodes`](/cli/nodes)
|
||||||
- [`devices`](/cli/devices)
|
|
||||||
- [`node`](/cli/node)
|
- [`node`](/cli/node)
|
||||||
- [`approvals`](/cli/approvals)
|
- [`approvals`](/cli/approvals)
|
||||||
- [`sandbox`](/cli/sandbox)
|
- [`sandbox`](/cli/sandbox)
|
||||||
@@ -189,7 +188,6 @@ clawdbot [--dev] [--profile <name>] <command>
|
|||||||
runs
|
runs
|
||||||
run
|
run
|
||||||
nodes
|
nodes
|
||||||
devices
|
|
||||||
node
|
node
|
||||||
start
|
start
|
||||||
daemon
|
daemon
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ Manage Gateway plugins/extensions (loaded in-process).
|
|||||||
|
|
||||||
Related:
|
Related:
|
||||||
- Plugin system: [Plugins](/plugin)
|
- Plugin system: [Plugins](/plugin)
|
||||||
- Plugin manifest + schema: [Plugin manifest](/plugins/manifest)
|
|
||||||
- Security hardening: [Security](/gateway/security)
|
- Security hardening: [Security](/gateway/security)
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
@@ -29,10 +28,6 @@ clawdbot plugins update --all
|
|||||||
Bundled plugins ship with Clawdbot but start disabled. Use `plugins enable` to
|
Bundled plugins ship with Clawdbot but start disabled. Use `plugins enable` to
|
||||||
activate them.
|
activate them.
|
||||||
|
|
||||||
All plugins must ship a `clawdbot.plugin.json` file with an inline JSON Schema
|
|
||||||
(`configSchema`, even if empty). Missing/invalid manifests or schemas prevent
|
|
||||||
the plugin from loading and fail config validation.
|
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -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.
|
- **`agent_end`**: inspect the final message list and run metadata after completion.
|
||||||
- **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles.
|
- **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles.
|
||||||
- **`before_tool_call` / `after_tool_call`**: intercept tool params/results.
|
- **`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.
|
- **`message_received` / `message_sending` / `message_sent`**: inbound + outbound message hooks.
|
||||||
- **`session_start` / `session_end`**: session lifecycle boundaries.
|
- **`session_start` / `session_end`**: session lifecycle boundaries.
|
||||||
- **`gateway_start` / `gateway_stop`**: gateway lifecycle events.
|
- **`gateway_start` / `gateway_stop`**: gateway lifecycle events.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ We serialize inbound auto-reply runs (all channels) through a tiny in-process qu
|
|||||||
- Serializing avoids competing for shared resources (session files, logs, CLI stdin) and reduces the chance of upstream rate limits.
|
- Serializing avoids competing for shared resources (session files, logs, CLI stdin) and reduces the chance of upstream rate limits.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
- A lane-aware FIFO queue drains each lane with a configurable concurrency cap (default 1 for unconfigured lanes; main defaults to 4, subagent to 8).
|
- A lane-aware FIFO queue drains each lane with a configurable concurrency cap (default 1).
|
||||||
- `runEmbeddedPiAgent` enqueues by **session key** (lane `session:<key>`) to guarantee only one active run per session.
|
- `runEmbeddedPiAgent` enqueues by **session key** (lane `session:<key>`) to guarantee only one active run per session.
|
||||||
- Each session run is then queued into a **global lane** (`main` by default) so overall parallelism is capped by `agents.defaults.maxConcurrent`.
|
- Each session run is then queued into a **global lane** (`main` by default) so overall parallelism is capped by `agents.defaults.maxConcurrent`.
|
||||||
- When verbose logging is enabled, queued runs emit a short notice if they waited more than ~2s before starting.
|
- When verbose logging is enabled, queued runs emit a short notice if they waited more than ~2s before starting.
|
||||||
|
|||||||
@@ -117,8 +117,8 @@ Send these as standalone messages so they register.
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Inspecting
|
## Inspecting
|
||||||
- `clawdbot status` — shows store path and recent sessions.
|
- `pnpm clawdbot status` — shows store path and recent sessions.
|
||||||
- `clawdbot sessions --json` — dumps every entry (filter with `--active <minutes>`).
|
- `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).
|
- `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 `/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).
|
- Send `/context list` or `/context detail` to see what’s in the system prompt and injected workspace files (and the biggest context contributors).
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ node --import tsx scripts/repro/tsx-name-repro.ts
|
|||||||
|
|
||||||
## Regression history
|
## Regression history
|
||||||
- `2871657e` (2026-01-06): scripts changed from Bun to tsx to make Bun optional.
|
- `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
|
## Workarounds
|
||||||
- Use Bun for dev scripts (current temporary revert).
|
- Use Bun for dev scripts (current temporary revert).
|
||||||
|
|||||||
+2
-4
@@ -59,11 +59,9 @@ Recommended flow (dev profile + dev bootstrap):
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm gateway:dev
|
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:
|
What this does:
|
||||||
|
|
||||||
1) **Profile isolation** (global `--dev`)
|
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:
|
If you need to spell it out, use the env var form:
|
||||||
|
|
||||||
```bash
|
```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
|
`--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`.
|
|
||||||
@@ -1840,7 +1840,7 @@ Example:
|
|||||||
|
|
||||||
`agents.defaults.subagents` configures sub-agent defaults:
|
`agents.defaults.subagents` configures sub-agent defaults:
|
||||||
- `model`: default model for spawned sub-agents (string or `{ primary, fallbacks }`). If omitted, sub-agents inherit the caller’s model unless overridden per agent or per call.
|
- `model`: default model for spawned sub-agents (string or `{ primary, fallbacks }`). If omitted, sub-agents inherit the caller’s model unless overridden per agent or per call.
|
||||||
- `maxConcurrent`: max concurrent sub-agent runs (default 8)
|
- `maxConcurrent`: max concurrent sub-agent runs (default 1)
|
||||||
- `archiveAfterMinutes`: auto-archive sub-agent sessions after N minutes (default 60; set `0` to disable)
|
- `archiveAfterMinutes`: auto-archive sub-agent sessions after N minutes (default 60; set `0` to disable)
|
||||||
- Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny` (deny wins)
|
- Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny` (deny wins)
|
||||||
|
|
||||||
@@ -1974,7 +1974,7 @@ Notes:
|
|||||||
|
|
||||||
`agents.defaults.maxConcurrent` sets the maximum number of embedded agent runs that can
|
`agents.defaults.maxConcurrent` sets the maximum number of embedded agent runs that can
|
||||||
execute in parallel across sessions. Each session is still serialized (one run
|
execute in parallel across sessions. Each session is still serialized (one run
|
||||||
per session key at a time). Default: 4.
|
per session key at a time). Default: 1.
|
||||||
|
|
||||||
### `agents.defaults.sandbox`
|
### `agents.defaults.sandbox`
|
||||||
|
|
||||||
@@ -2669,7 +2669,6 @@ Notes:
|
|||||||
- `clawdbot gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
|
- `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).
|
- `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`.
|
- 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`.
|
- 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`).
|
- 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).
|
- The onboarding wizard generates a gateway token by default (even on loopback).
|
||||||
@@ -2677,7 +2676,7 @@ Notes:
|
|||||||
|
|
||||||
Auth and Tailscale:
|
Auth and Tailscale:
|
||||||
- `gateway.auth.mode` sets the handshake requirements (`token` or `password`).
|
- `gateway.auth.mode` sets the handshake requirements (`token` or `password`).
|
||||||
- `gateway.auth.token` stores the shared token for token auth (used by the CLI on the same machine and as the bootstrap credential for device pairing).
|
- `gateway.auth.token` stores the shared token for token auth (used by the CLI on the same machine).
|
||||||
- When `gateway.auth.mode` is set, only that method is accepted (plus optional Tailscale headers).
|
- When `gateway.auth.mode` is set, only that method is accepted (plus optional Tailscale headers).
|
||||||
- `gateway.auth.password` can be set here, or via `CLAWDBOT_GATEWAY_PASSWORD` (recommended).
|
- `gateway.auth.password` can be set here, or via `CLAWDBOT_GATEWAY_PASSWORD` (recommended).
|
||||||
- `gateway.auth.allowTailscale` allows Tailscale Serve identity headers
|
- `gateway.auth.allowTailscale` allows Tailscale Serve identity headers
|
||||||
@@ -2686,9 +2685,6 @@ Auth and Tailscale:
|
|||||||
`true`, Serve requests do not need a token/password; set `false` to require
|
`true`, Serve requests do not need a token/password; set `false` to require
|
||||||
explicit credentials. Defaults to `true` when `tailscale.mode = "serve"` and
|
explicit credentials. Defaults to `true` when `tailscale.mode = "serve"` and
|
||||||
auth mode is not `password`.
|
auth mode is not `password`.
|
||||||
- After pairing, the Gateway issues **device tokens** scoped to the device role + scopes.
|
|
||||||
These are returned in `hello-ok.auth.deviceToken`; clients should persist and reuse them
|
|
||||||
instead of the shared token. Rotate/revoke via `device.token.rotate`/`device.token.revoke`.
|
|
||||||
- `gateway.tailscale.mode: "serve"` uses Tailscale Serve (tailnet only, loopback bind).
|
- `gateway.tailscale.mode: "serve"` uses Tailscale Serve (tailnet only, loopback bind).
|
||||||
- `gateway.tailscale.mode: "funnel"` exposes the dashboard publicly; requires auth.
|
- `gateway.tailscale.mode: "funnel"` exposes the dashboard publicly; requires auth.
|
||||||
- `gateway.tailscale.resetOnExit` resets Serve/Funnel config on shutdown.
|
- `gateway.tailscale.resetOnExit` resets Serve/Funnel config on shutdown.
|
||||||
@@ -2716,29 +2712,6 @@ macOS app behavior:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `gateway.nodes` (Node command allowlist)
|
|
||||||
|
|
||||||
The Gateway enforces a per-platform command allowlist for `node.invoke`. Nodes must both
|
|
||||||
**declare** a command and have it **allowed** by the Gateway to run it.
|
|
||||||
|
|
||||||
Use this section to extend or deny commands:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
gateway: {
|
|
||||||
nodes: {
|
|
||||||
allowCommands: ["custom.vendor.command"], // extra commands beyond defaults
|
|
||||||
denyCommands: ["sms.send"] // block a command even if declared
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- `allowCommands` extends the built-in per-platform defaults.
|
|
||||||
- `denyCommands` always wins (even if the node claims the command).
|
|
||||||
- `node.invoke` rejects commands that are not declared by the node.
|
|
||||||
|
|
||||||
### `gateway.reload` (Config hot reload)
|
### `gateway.reload` (Config hot reload)
|
||||||
|
|
||||||
The Gateway watches `~/.clawdbot/clawdbot.json` (or `CLAWDBOT_CONFIG_PATH`) and applies changes automatically.
|
The Gateway watches `~/.clawdbot/clawdbot.json` (or `CLAWDBOT_CONFIG_PATH`) and applies changes automatically.
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ pnpm gateway:watch
|
|||||||
- Binds WebSocket control plane to `127.0.0.1:<port>` (default 18789).
|
- 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.
|
- The same port also serves HTTP (control UI, hooks, A2UI). Single-port multiplex.
|
||||||
- OpenAI Chat Completions (HTTP): [`/v1/chat/completions`](/gateway/openai-http-api).
|
- 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`.
|
- 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.
|
- 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.
|
- Pass `--verbose` to mirror debug logging (handshakes, req/res, events) from the log file into stdio when troubleshooting.
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
@@ -59,18 +59,6 @@ Gateway → Client:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
When a device token is issued, `hello-ok` also includes:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"auth": {
|
|
||||||
"deviceToken": "…",
|
|
||||||
"role": "operator",
|
|
||||||
"scopes": ["operator.read", "operator.write"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Node example
|
### Node example
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -135,11 +123,6 @@ Nodes declare capability claims at connect time:
|
|||||||
|
|
||||||
The Gateway treats these as **claims** and enforces server-side allowlists.
|
The Gateway treats these as **claims** and enforces server-side allowlists.
|
||||||
|
|
||||||
### Node helper methods
|
|
||||||
|
|
||||||
- Nodes may call `skills.bins` to fetch the current list of skill executables
|
|
||||||
for auto-allow checks.
|
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema.ts`.
|
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema.ts`.
|
||||||
@@ -153,11 +136,6 @@ The Gateway treats these as **claims** and enforces server-side allowlists.
|
|||||||
|
|
||||||
- If `CLAWDBOT_GATEWAY_TOKEN` (or `--token`) is set, `connect.params.auth.token`
|
- If `CLAWDBOT_GATEWAY_TOKEN` (or `--token`) is set, `connect.params.auth.token`
|
||||||
must match or the socket is closed.
|
must match or the socket is closed.
|
||||||
- After pairing, the Gateway issues a **device token** scoped to the connection
|
|
||||||
role + scopes. It is returned in `hello-ok.auth.deviceToken` and should be
|
|
||||||
persisted by the client for future connects.
|
|
||||||
- Device tokens can be rotated/revoked via `device.token.rotate` and
|
|
||||||
`device.token.revoke` (requires `operator.pairing` scope).
|
|
||||||
|
|
||||||
## Device identity + pairing
|
## Device identity + pairing
|
||||||
|
|
||||||
@@ -166,7 +144,6 @@ The Gateway treats these as **claims** and enforces server-side allowlists.
|
|||||||
- Gateways issue tokens per device + role.
|
- Gateways issue tokens per device + role.
|
||||||
- Pairing approvals are required for new device IDs unless local auto-approval
|
- Pairing approvals are required for new device IDs unless local auto-approval
|
||||||
is enabled.
|
is enabled.
|
||||||
- All WS clients must include `device` identity during `connect` (operator + node).
|
|
||||||
|
|
||||||
## TLS + pinning
|
## TLS + pinning
|
||||||
|
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ clawdbot channels login
|
|||||||
### Build errors on `main` — what’s the standard fix path?
|
### Build errors on `main` — what’s the standard fix path?
|
||||||
|
|
||||||
1) `git pull origin main && pnpm install`
|
1) `git pull origin main && pnpm install`
|
||||||
2) `clawdbot doctor`
|
2) `pnpm clawdbot doctor`
|
||||||
3) Check GitHub issues or Discord
|
3) Check GitHub issues or Discord
|
||||||
4) Temporary workaround: check out an older commit
|
4) Temporary workaround: check out an older commit
|
||||||
|
|
||||||
@@ -392,7 +392,7 @@ Typical recovery:
|
|||||||
git status # ensure you’re in the repo root
|
git status # ensure you’re in the repo root
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm build
|
pnpm build
|
||||||
clawdbot doctor
|
pnpm clawdbot doctor
|
||||||
clawdbot daemon restart
|
clawdbot daemon restart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -235,12 +235,6 @@ Triggered when the gateway starts:
|
|||||||
|
|
||||||
- **`gateway:startup`**: After channels start and hooks are loaded
|
- **`gateway:startup`**: After channels start and hooks are loaded
|
||||||
|
|
||||||
### Tool Result Hooks (Plugin API)
|
|
||||||
|
|
||||||
These hooks are not event-stream listeners; they let plugins synchronously adjust tool results before Clawdbot persists them.
|
|
||||||
|
|
||||||
- **`tool_result_persist`**: transform tool results before they are written to the session transcript. Must be synchronous; return the updated tool result payload or `undefined` to keep it as-is. See [Agent Loop](/concepts/agent-loop).
|
|
||||||
|
|
||||||
### Future Events
|
### Future Events
|
||||||
|
|
||||||
Planned event types:
|
Planned event types:
|
||||||
|
|||||||
+1
-3
@@ -123,11 +123,9 @@ cd clawdbot
|
|||||||
pnpm install
|
pnpm install
|
||||||
pnpm ui:build # auto-installs UI deps on first run
|
pnpm ui:build # auto-installs UI deps on first run
|
||||||
pnpm build
|
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):
|
Multi-instance quickstart (optional):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+42
-98
@@ -7,43 +7,40 @@ read_when:
|
|||||||
|
|
||||||
# Install
|
# 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
|
```bash
|
||||||
curl -fsSL https://clawd.bot/install.sh | bash
|
curl -fsSL https://clawd.bot/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows (PowerShell):
|
This installs the `clawdbot` CLI globally via npm and then starts onboarding.
|
||||||
|
|
||||||
```powershell
|
See installer flags:
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://clawd.bot/install.sh | bash -s -- --help
|
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
|
curl -fsSL https://clawd.bot/install.sh | bash -s -- --no-onboard
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2) Global install (manual)
|
## Install method: npm vs git
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
The installer supports two methods:
|
The installer supports two methods:
|
||||||
|
|
||||||
@@ -148,28 +92,28 @@ Equivalent env vars (useful for automation):
|
|||||||
- `CLAWDBOT_NO_ONBOARD=1`
|
- `CLAWDBOT_NO_ONBOARD=1`
|
||||||
- `SHARP_IGNORE_GLOBAL_LIBVIPS=0|1` (default: `1`; avoids `sharp` building against system libvips)
|
- `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
|
```bash
|
||||||
node -v
|
npm install -g clawdbot@latest
|
||||||
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`).
|
If you have libvips installed globally (common on macOS via Homebrew) and `sharp` fails to install, force prebuilt binaries:
|
||||||
|
|
||||||
Fix: add it to your shell startup file (zsh: `~/.zshrc`, bash: `~/.bashrc`):
|
|
||||||
|
|
||||||
```bash
|
```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)
|
Then:
|
||||||
- Uninstall: [Uninstall](/install/uninstall)
|
|
||||||
|
```bash
|
||||||
|
clawdbot onboard --install-daemon
|
||||||
|
```
|
||||||
@@ -118,7 +118,7 @@ Remove it with `npm rm -g clawdbot` (or `pnpm remove -g` / `bun remove -g` if yo
|
|||||||
|
|
||||||
### Source checkout (git clone)
|
### 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).
|
1) Uninstall the gateway service **before** deleting the repo (use the easy path above or manual service removal).
|
||||||
2) Delete the repo directory.
|
2) Delete the repo directory.
|
||||||
|
|||||||
@@ -119,13 +119,12 @@ git pull
|
|||||||
pnpm install
|
pnpm install
|
||||||
pnpm build
|
pnpm build
|
||||||
pnpm ui:build # auto-installs UI deps on first run
|
pnpm ui:build # auto-installs UI deps on first run
|
||||||
clawdbot doctor
|
pnpm clawdbot doctor
|
||||||
clawdbot health
|
pnpm clawdbot health
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
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/`.
|
- `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.
|
- 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.
|
- 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
@@ -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`)
|
`tools.web.search.perplexity.baseUrl` (or `tools.web.search.perplexity.apiKey`)
|
||||||
to disambiguate.
|
to disambiguate.
|
||||||
|
|
||||||
If no base URL is set, Clawdbot chooses a default based on the API key source:
|
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.
|
||||||
- `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)
|
|
||||||
|
|
||||||
## Models
|
## Models
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ Options:
|
|||||||
- `--timeout <ms>`: overall discovery window (default `2000`)
|
- `--timeout <ms>`: overall discovery window (default `2000`)
|
||||||
- `--json`: structured output for diffing
|
- `--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
|
macOS app’s discovery pipeline (NWBrowser + tailnet DNS‑SD fallback) differs from
|
||||||
the Node CLI’s `dns-sd` based discovery.
|
the Node CLI’s `dns-sd` based discovery.
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ cd clawdbot
|
|||||||
pnpm install
|
pnpm install
|
||||||
pnpm ui:build # auto-installs UI deps on first run
|
pnpm ui:build # auto-installs UI deps on first run
|
||||||
pnpm build
|
pnpm build
|
||||||
clawdbot onboard
|
pnpm clawdbot onboard
|
||||||
```
|
```
|
||||||
|
|
||||||
Full guide: [Getting Started](/start/getting-started)
|
Full guide: [Getting Started](/start/getting-started)
|
||||||
|
|||||||
+14
-64
@@ -48,11 +48,8 @@ See [Voice Call](/plugins/voice-call) for a concrete example plugin.
|
|||||||
- Qwen OAuth (provider auth) — bundled as `qwen-portal-auth` (disabled by default)
|
- Qwen OAuth (provider auth) — bundled as `qwen-portal-auth` (disabled by default)
|
||||||
- Copilot Proxy (provider auth) — local VS Code Copilot Proxy bridge; distinct from built-in `github-copilot` device login (bundled, disabled by default)
|
- Copilot Proxy (provider auth) — local VS Code Copilot Proxy bridge; distinct from built-in `github-copilot` device login (bundled, disabled by default)
|
||||||
|
|
||||||
Clawdbot plugins are **TypeScript modules** loaded at runtime via jiti. **Config
|
Clawdbot plugins are **TypeScript modules** loaded at runtime via jiti. They can
|
||||||
validation does not execute plugin code**; it uses the plugin manifest and JSON
|
register:
|
||||||
Schema instead. See [Plugin manifest](/plugins/manifest).
|
|
||||||
|
|
||||||
Plugins can register:
|
|
||||||
|
|
||||||
- Gateway RPC methods
|
- Gateway RPC methods
|
||||||
- Gateway HTTP handlers
|
- Gateway HTTP handlers
|
||||||
@@ -86,10 +83,6 @@ Bundled plugins must be enabled explicitly via `plugins.entries.<id>.enabled`
|
|||||||
or `clawdbot plugins enable <id>`. Installed plugins are enabled by default,
|
or `clawdbot plugins enable <id>`. Installed plugins are enabled by default,
|
||||||
but can be disabled the same way.
|
but can be disabled the same way.
|
||||||
|
|
||||||
Each plugin must include a `clawdbot.plugin.json` file in its root. If a path
|
|
||||||
points at a file, the plugin root is the file's directory and must contain the
|
|
||||||
manifest.
|
|
||||||
|
|
||||||
If multiple plugins resolve to the same id, the first match in the order above
|
If multiple plugins resolve to the same id, the first match in the order above
|
||||||
wins and lower-precedence copies are ignored.
|
wins and lower-precedence copies are ignored.
|
||||||
|
|
||||||
@@ -112,37 +105,6 @@ becomes `name/<fileBase>`.
|
|||||||
If your plugin imports npm deps, install them in that directory so
|
If your plugin imports npm deps, install them in that directory so
|
||||||
`node_modules` is available (`npm install` / `pnpm install`).
|
`node_modules` is available (`npm install` / `pnpm install`).
|
||||||
|
|
||||||
### Channel catalog metadata
|
|
||||||
|
|
||||||
Channel plugins can advertise onboarding metadata via `clawdbot.channel` and
|
|
||||||
install hints via `clawdbot.install`. This keeps the core catalog data-free.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "@clawdbot/nextcloud-talk",
|
|
||||||
"clawdbot": {
|
|
||||||
"extensions": ["./index.ts"],
|
|
||||||
"channel": {
|
|
||||||
"id": "nextcloud-talk",
|
|
||||||
"label": "Nextcloud Talk",
|
|
||||||
"selectionLabel": "Nextcloud Talk (self-hosted)",
|
|
||||||
"docsPath": "/channels/nextcloud-talk",
|
|
||||||
"docsLabel": "nextcloud-talk",
|
|
||||||
"blurb": "Self-hosted chat via Nextcloud Talk webhook bots.",
|
|
||||||
"order": 65,
|
|
||||||
"aliases": ["nc-talk", "nc"]
|
|
||||||
},
|
|
||||||
"install": {
|
|
||||||
"npmSpec": "@clawdbot/nextcloud-talk",
|
|
||||||
"localPath": "extensions/nextcloud-talk",
|
|
||||||
"defaultChoice": "npm"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Plugin IDs
|
## Plugin IDs
|
||||||
|
|
||||||
Default plugin ids:
|
Default plugin ids:
|
||||||
@@ -178,14 +140,6 @@ Fields:
|
|||||||
|
|
||||||
Config changes **require a gateway restart**.
|
Config changes **require a gateway restart**.
|
||||||
|
|
||||||
Validation rules (strict):
|
|
||||||
- Unknown plugin ids in `entries`, `allow`, `deny`, or `slots` are **errors**.
|
|
||||||
- Unknown `channels.<id>` keys are **errors** unless a plugin manifest declares
|
|
||||||
the channel id.
|
|
||||||
- Plugin config is validated using the JSON Schema embedded in
|
|
||||||
`clawdbot.plugin.json` (`configSchema`).
|
|
||||||
- If a plugin is disabled, its config is preserved and a **warning** is emitted.
|
|
||||||
|
|
||||||
## Plugin slots (exclusive categories)
|
## Plugin slots (exclusive categories)
|
||||||
|
|
||||||
Some plugin categories are **exclusive** (only one active at a time). Use
|
Some plugin categories are **exclusive** (only one active at a time). Use
|
||||||
@@ -215,26 +169,22 @@ Clawdbot augments `uiHints` at runtime based on discovered plugins:
|
|||||||
`plugins.entries.<id>.config.<field>`
|
`plugins.entries.<id>.config.<field>`
|
||||||
|
|
||||||
If you want your plugin config fields to show good labels/placeholders (and mark secrets as sensitive),
|
If you want your plugin config fields to show good labels/placeholders (and mark secrets as sensitive),
|
||||||
provide `uiHints` alongside your JSON Schema in the plugin manifest.
|
provide `configSchema.uiHints`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```json
|
```ts
|
||||||
{
|
export default {
|
||||||
"id": "my-plugin",
|
id: "my-plugin",
|
||||||
"configSchema": {
|
configSchema: {
|
||||||
"type": "object",
|
parse: (v) => v,
|
||||||
"additionalProperties": false,
|
uiHints: {
|
||||||
"properties": {
|
"apiKey": { label: "API Key", sensitive: true },
|
||||||
"apiKey": { "type": "string" },
|
"region": { label: "Region", placeholder: "us-east-1" },
|
||||||
"region": { "type": "string" }
|
},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"uiHints": {
|
register(api) {},
|
||||||
"apiKey": { "label": "API Key", "sensitive": true },
|
};
|
||||||
"region": { "label": "Region", "placeholder": "us-east-1" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "Plugin manifest + JSON schema requirements (strict config validation)"
|
|
||||||
read_when:
|
|
||||||
- You are building a Clawdbot plugin
|
|
||||||
- You need to ship a plugin config schema or debug plugin validation errors
|
|
||||||
---
|
|
||||||
# Plugin manifest (clawdbot.plugin.json)
|
|
||||||
|
|
||||||
Every plugin **must** ship a `clawdbot.plugin.json` file in the **plugin root**.
|
|
||||||
Clawdbot uses this manifest to validate configuration **without executing plugin
|
|
||||||
code**. Missing or invalid manifests are treated as plugin errors and block
|
|
||||||
config validation.
|
|
||||||
|
|
||||||
See the full plugin system guide: [Plugins](/plugin).
|
|
||||||
|
|
||||||
## Required fields
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "voice-call",
|
|
||||||
"configSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Required keys:
|
|
||||||
- `id` (string): canonical plugin id.
|
|
||||||
- `configSchema` (object): JSON Schema for plugin config (inline).
|
|
||||||
|
|
||||||
Optional keys:
|
|
||||||
- `kind` (string): plugin kind (example: `"memory"`).
|
|
||||||
- `channels` (array): channel ids registered by this plugin (example: `["matrix"]`).
|
|
||||||
- `providers` (array): provider ids registered by this plugin.
|
|
||||||
- `name` (string): display name for the plugin.
|
|
||||||
- `description` (string): short plugin summary.
|
|
||||||
- `uiHints` (object): config field labels/placeholders/sensitive flags for UI rendering.
|
|
||||||
- `version` (string): plugin version (informational).
|
|
||||||
|
|
||||||
## JSON Schema requirements
|
|
||||||
|
|
||||||
- **Every plugin must ship a JSON Schema**, even if it accepts no config.
|
|
||||||
- An empty schema is acceptable (for example, `{ "type": "object", "additionalProperties": false }`).
|
|
||||||
- Schemas are validated at config read/write time, not at runtime.
|
|
||||||
|
|
||||||
## Validation behavior
|
|
||||||
|
|
||||||
- Unknown `channels.*` keys are **errors**, unless the channel id is declared by
|
|
||||||
a plugin manifest.
|
|
||||||
- `plugins.entries.<id>`, `plugins.allow`, `plugins.deny`, and `plugins.slots.*`
|
|
||||||
must reference **discoverable** plugin ids. Unknown ids are **errors**.
|
|
||||||
- If a plugin is installed but has a broken or missing manifest or schema,
|
|
||||||
validation fails and Doctor reports the plugin error.
|
|
||||||
- If plugin config exists but the plugin is **disabled**, the config is kept and
|
|
||||||
a **warning** is surfaced in Doctor + logs.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- The manifest is **required for all plugins**, including local filesystem loads.
|
|
||||||
- Runtime still loads the plugin module separately; the manifest is only for
|
|
||||||
discovery + validation.
|
|
||||||
@@ -31,7 +31,6 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/etc.)? See [Chann
|
|||||||
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||||
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
|
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
|
||||||
- [OpenCode Zen](/providers/opencode)
|
- [OpenCode Zen](/providers/opencode)
|
||||||
- [Amazon Bedrock](/bedrock)
|
|
||||||
- [Z.AI](/providers/zai)
|
- [Z.AI](/providers/zai)
|
||||||
- [GLM models](/providers/glm)
|
- [GLM models](/providers/glm)
|
||||||
- [MiniMax](/providers/minimax)
|
- [MiniMax](/providers/minimax)
|
||||||
|
|||||||
@@ -22,20 +22,17 @@ read_when:
|
|||||||
- Unknown keys are validation errors (no passthrough at root or nested).
|
- Unknown keys are validation errors (no passthrough at root or nested).
|
||||||
- `plugins.entries.<id>.config` must be validated by the plugin’s schema.
|
- `plugins.entries.<id>.config` must be validated by the plugin’s schema.
|
||||||
- If a plugin lacks a schema, **reject plugin load** and surface a clear error.
|
- If a plugin lacks a schema, **reject plugin load** and surface a clear error.
|
||||||
- Unknown `channels.<id>` keys are errors unless a plugin manifest declares the channel id.
|
|
||||||
- Plugin manifests (`clawdbot.plugin.json`) are required for all plugins.
|
|
||||||
|
|
||||||
## Plugin schema enforcement
|
## Plugin schema enforcement
|
||||||
- Each plugin provides a strict JSON Schema for its config (inline in the manifest).
|
- Each plugin provides a strict schema for its config (no passthrough).
|
||||||
- Plugin load flow:
|
- Plugin load flow:
|
||||||
1) Resolve plugin manifest + schema (`clawdbot.plugin.json`).
|
1) Resolve plugin schema by plugin id.
|
||||||
2) Validate config against the schema.
|
2) Validate config against the schema.
|
||||||
3) If missing schema or invalid config: block plugin load, record error.
|
3) If missing schema or invalid config: block plugin load, record error.
|
||||||
- Error message includes:
|
- Error message includes:
|
||||||
- Plugin id
|
- Plugin id
|
||||||
- Reason (missing schema / invalid config)
|
- Reason (missing schema / invalid config)
|
||||||
- Path(s) that failed validation
|
- Path(s) that failed validation
|
||||||
- Disabled plugins keep their config, but Doctor + logs surface a warning.
|
|
||||||
|
|
||||||
## Doctor flow
|
## Doctor flow
|
||||||
- Doctor runs **every time** config is loaded (dry-run by default).
|
- Doctor runs **every time** config is loaded (dry-run by default).
|
||||||
|
|||||||
+13
-16
@@ -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:
|
The repo recommends running from source and using the onboarding wizard:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://clawd.bot/install.sh | bash
|
git clone https://github.com/clawdbot/clawdbot.git
|
||||||
clawdbot onboard --install-daemon
|
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**.
|
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?
|
### 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.
|
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
|
cd clawdbot
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm build
|
pnpm build
|
||||||
clawdbot doctor
|
pnpm clawdbot doctor
|
||||||
clawdbot daemon restart
|
clawdbot daemon restart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
⚠️ **Bun warning (WhatsApp + Telegram):** Bun has known issues with these
|
||||||
channels. If you use WhatsApp or Telegram, run the Gateway with **Node**.
|
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
|
## 4) Pair + connect your first chat surface
|
||||||
|
|
||||||
### WhatsApp (QR login)
|
### WhatsApp (QR login)
|
||||||
@@ -165,11 +158,9 @@ cd clawdbot
|
|||||||
pnpm install
|
pnpm install
|
||||||
pnpm ui:build # auto-installs UI deps on first run
|
pnpm ui:build # auto-installs UI deps on first run
|
||||||
pnpm build
|
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):
|
Gateway (from this repo):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -178,13 +169,15 @@ node dist/entry.js gateway --port 18789 --verbose
|
|||||||
|
|
||||||
## 7) Verify end-to-end
|
## 7) Verify end-to-end
|
||||||
|
|
||||||
In a new terminal, send a test message:
|
In a new terminal:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
clawdbot status
|
||||||
|
clawdbot health
|
||||||
clawdbot message send --target +15555550123 --message "Hello from Clawdbot"
|
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.
|
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.
|
Health probes: `clawdbot health` (or `clawdbot status --deep`) asks the running gateway for a health snapshot.
|
||||||
|
|||||||
+2
-4
@@ -35,11 +35,9 @@ clawdbot setup
|
|||||||
From inside this repo, use the local CLI entry:
|
From inside this repo, use the local CLI entry:
|
||||||
|
|
||||||
```bash
|
```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)
|
## Stable workflow (macOS app first)
|
||||||
|
|
||||||
1) Install + launch **Clawdbot.app** (menu bar).
|
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:
|
- Or via CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot health
|
pnpm clawdbot health
|
||||||
```
|
```
|
||||||
|
|
||||||
### Common footguns
|
### Common footguns
|
||||||
|
|||||||
Loaded 100 of 408 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user