Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
620913b79b | ||
|
|
5c37ece2e7 | ||
|
|
bcd1094dc8 | ||
|
|
0ea3994d9e |
No files matched your search
@@ -29,6 +29,5 @@ jobs:
|
||||
CLAWDBOT_INSTALL_CLI_URL: https://clawd.bot/install-cli.sh
|
||||
CLAWDBOT_NO_ONBOARD: "1"
|
||||
CLAWDBOT_INSTALL_SMOKE_SKIP_CLI: "1"
|
||||
CLAWDBOT_INSTALL_SMOKE_SKIP_NONROOT: ${{ github.event_name == 'pull_request' && '1' || '0' }}
|
||||
CLAWDBOT_INSTALL_SMOKE_PREVIOUS: "2026.1.11-4"
|
||||
run: pnpm test:install:smoke
|
||||
@@ -22,15 +22,6 @@
|
||||
- README (GitHub): keep absolute docs URLs (`https://docs.clawd.bot/...`) so links work on GitHub.
|
||||
- Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and “gateway host”.
|
||||
|
||||
## exe.dev VM ops (general)
|
||||
- Access: SSH to the VM directly: `ssh vm-name.exe.xyz` (or use exe.dev web terminal).
|
||||
- Updates: `sudo npm i -g clawdbot@latest` (global install needs root on `/usr/lib/node_modules`).
|
||||
- Config: use `clawdbot config set ...`; set `gateway.mode=local` if unset.
|
||||
- Restart: exe.dev often lacks systemd user bus; stop old gateway and run:
|
||||
`pkill -9 -f clawdbot-gateway || true; nohup clawdbot gateway run --bind loopback --port 18789 --force > /tmp/clawdbot-gateway.log 2>&1 &`
|
||||
- Verify: `clawdbot --version`, `clawdbot health`, `ss -ltnp | rg 18789`.
|
||||
- SSH flaky: use exe.dev web terminal or Shelley (web agent) instead of CLI SSH.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- Runtime baseline: Node **22+** (keep Node + Bun paths working).
|
||||
- Install deps: `pnpm install`
|
||||
@@ -60,7 +51,6 @@
|
||||
- Framework: Vitest with V8 coverage thresholds (70% lines/branches/functions/statements).
|
||||
- Naming: match source names with `*.test.ts`; e2e in `*.e2e.test.ts`.
|
||||
- Run `pnpm test` (or `pnpm test:coverage`) before pushing when you touch logic.
|
||||
- Do not set test workers above 16; tried already.
|
||||
- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (Clawdbot-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
|
||||
- Full kit + what’s covered: `docs/testing.md`.
|
||||
- Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior or the user asks for one.
|
||||
@@ -128,10 +118,6 @@
|
||||
- **Multi-agent safety:** do **not** switch branches / check out a different branch unless explicitly requested.
|
||||
- **Multi-agent safety:** running multiple agents is OK as long as each agent has its own session.
|
||||
- **Multi-agent safety:** when you see unrecognized files, keep going; focus on your changes and commit only those.
|
||||
- Lint/format churn:
|
||||
- If staged+unstaged diffs are formatting-only, auto-resolve without asking.
|
||||
- If commit/push already requested, auto-stage and include formatting-only follow-ups in the same commit (or a tiny follow-up commit if needed), no extra confirmation.
|
||||
- Only ask when changes are semantic (logic/data/behavior).
|
||||
- Lobster seam: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed.
|
||||
- **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief “other files present” note only if relevant.
|
||||
- Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause.
|
||||
|
||||
+64
-77
@@ -2,61 +2,36 @@
|
||||
|
||||
Docs: https://docs.clawd.bot
|
||||
|
||||
## 2026.1.23
|
||||
## 2026.1.22 (unreleased)
|
||||
|
||||
### Changes
|
||||
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it.
|
||||
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
|
||||
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
||||
- TUI: run local shell commands with `!` after per-session consent, and warn when local exec stays disabled. (#1463) Thanks @vignesh07.
|
||||
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
|
||||
- Agents: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer.
|
||||
- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early.
|
||||
- Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.clawd.bot/gateway/troubleshooting
|
||||
- Docs: add /model allowlist troubleshooting note. (#1405)
|
||||
- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.
|
||||
- UI: show per-session assistant identity in the Control UI. (#1420) Thanks @robbyczgw-cla.
|
||||
- Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead).
|
||||
- Signal: add typing indicators and DM read receipts via signal-cli.
|
||||
- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero.
|
||||
- CLI: add `clawdbot update wizard` for interactive channel selection and restart prompts. https://docs.clawd.bot/cli/update
|
||||
- macOS: add attach-only debug toggle + `--attach-only`/`--no-launchd` flag to skip launchd installs.
|
||||
|
||||
### Fixes
|
||||
- macOS: sync voice wake trigger words on submit/blur in settings. (#1506) Thanks @shiv19.
|
||||
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
||||
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
||||
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
||||
- CLI: suppress diagnostic session/run noise during auth probes.
|
||||
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
|
||||
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
|
||||
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
|
||||
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
|
||||
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
|
||||
|
||||
## 2026.1.22
|
||||
|
||||
### Changes
|
||||
- Highlight: Compaction safeguard now uses adaptive chunking, progressive fallback, and UI status + retries. (#1466) Thanks @dlauer.
|
||||
- Providers: add Antigravity usage tracking to status output. (#1490) Thanks @patelhiren.
|
||||
- Slack: add chat-type reply threading overrides via `replyToModeByChatType`. (#1442) Thanks @stefangalescu.
|
||||
- BlueBubbles: add `asVoice` support for MP3/CAF voice memos in sendAttachment. (#1477, #1482) Thanks @Nicell.
|
||||
- Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link.
|
||||
### Breaking
|
||||
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert.
|
||||
|
||||
### Fixes
|
||||
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
|
||||
- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
|
||||
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
||||
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
||||
- Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x.
|
||||
- Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.
|
||||
- Gateway: stop the service before uninstalling and fail if it remains loaded.
|
||||
- Agents: surface concrete API error details instead of generic AI service errors.
|
||||
- Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484)
|
||||
- Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.
|
||||
- Agents: make OpenAI sessions image-sanitize-only; gate tool-id/repair sanitization by provider.
|
||||
- Doctor: honor CLAWDBOT_GATEWAY_TOKEN for auth checks and security audit token reuse. (#1448) Thanks @azade-c.
|
||||
- Agents: make tool summaries more readable and only show optional params when set.
|
||||
- Agents: honor SOUL.md guidance even when the file is nested or path-qualified. (#1434) Thanks @neooriginal.
|
||||
- Matrix (plugin): persist m.direct for resolved DMs and harden room fallback. (#1436, #1486) Thanks @sibbl.
|
||||
- CLI: prefer `~` for home paths in output.
|
||||
- Agents: avoid sanitizing tool call IDs for OpenAI responses to preserve Pi pairing.
|
||||
- Mattermost (plugin): enforce pairing/allowlist gating, keep @username targets, and clarify plugin-only docs. (#1428) Thanks @damoahdominic.
|
||||
- Agents: centralize transcript sanitization in the runner; keep <final> tags and error turns intact.
|
||||
- Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik.
|
||||
- Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff.
|
||||
- Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.
|
||||
- Slack: reduce WebClient retries to avoid duplicate sends. (#1481)
|
||||
- Slack: read thread replies for message reads when threadId is provided (replies-only). (#1450) Thanks @rodrigouroz.
|
||||
- Discord: honor accountId across message actions and cron deliveries. (#1492) Thanks @svkozak.
|
||||
- macOS: prefer linked channels in gateway summary to avoid false “not linked” status.
|
||||
- macOS/tests: fix gateway summary lookup after guard unwrap; prevent browser opens during tests. (ECID-1483)
|
||||
|
||||
## 2026.1.21-2
|
||||
|
||||
@@ -66,51 +41,63 @@ Docs: https://docs.clawd.bot
|
||||
|
||||
## 2026.1.21
|
||||
|
||||
### Highlights
|
||||
- Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
|
||||
- Custom assistant identity + avatars in the Control UI. https://docs.clawd.bot/cli/agents https://docs.clawd.bot/web/control-ui
|
||||
- Cache optimizations: cache-ttl pruning + defaults reduce token spend on cold requests. https://docs.clawd.bot/concepts/session-pruning
|
||||
- Exec approvals + elevated ask/full modes. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/elevated
|
||||
- Signal typing/read receipts + MSTeams attachments. https://docs.clawd.bot/channels/signal https://docs.clawd.bot/channels/msteams
|
||||
- `/models` UX refresh + `clawdbot update wizard`. https://docs.clawd.bot/cli/models https://docs.clawd.bot/cli/update
|
||||
|
||||
### Changes
|
||||
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
|
||||
- Lobster: allow workflow file args via `argsJson` in the plugin tool. https://docs.clawd.bot/tools/lobster
|
||||
- Heartbeat: allow running heartbeats in an explicit session key. (#1256) Thanks @zknicker.
|
||||
- CLI: default exec approvals to the local host, add gateway/node targeting flags, and show target details in allowlist output.
|
||||
- CLI: exec approvals mutations render tables instead of raw JSON.
|
||||
- Exec approvals: support wildcard agent allowlists (`*`) across all agents.
|
||||
- Exec approvals: allowlist matches resolved binary paths only, add safe stdin-only bins, and tighten allowlist shell parsing.
|
||||
- Nodes: expose node PATH in status/describe and bootstrap PATH for node-host execution.
|
||||
- CLI: flatten node service commands under `clawdbot node` and remove `service node` docs.
|
||||
- CLI: move gateway service commands under `clawdbot gateway` and add `gateway probe` for reachability.
|
||||
- Sessions: add per-channel reset overrides via `session.resetByChannel`. (#1353) Thanks @cash-echo-bot.
|
||||
- Agents: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer.
|
||||
- UI: show per-session assistant identity in the Control UI. (#1420) Thanks @robbyczgw-cla.
|
||||
- CLI: add `clawdbot update wizard` for interactive channel selection and restart prompts. https://docs.clawd.bot/cli/update
|
||||
- Signal: add typing indicators and DM read receipts via signal-cli.
|
||||
- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero.
|
||||
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster (#1152) Thanks @vignesh07.
|
||||
- Agents/UI: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer. https://docs.clawd.bot/gateway/configuration https://docs.clawd.bot/cli/agents
|
||||
- Control UI: add custom assistant identity support and per-session identity display. (#1420) Thanks @robbyczgw-cla. https://docs.clawd.bot/web/control-ui
|
||||
- CLI: add `clawdbot update wizard` with interactive channel selection + restart prompts, plus preflight checks before rebasing. https://docs.clawd.bot/cli/update
|
||||
- Models/Commands: add `/models`, improve `/model` listing UX, and expand `clawdbot models` paging. (#1398) Thanks @vignesh07. https://docs.clawd.bot/cli/models
|
||||
- CLI: move gateway service commands under `clawdbot gateway`, flatten node service commands under `clawdbot node`, and add `gateway probe` for reachability. https://docs.clawd.bot/cli/gateway https://docs.clawd.bot/cli/node
|
||||
- Exec: add elevated ask/full modes, tighten allowlist gating, and render approvals tables on write. https://docs.clawd.bot/tools/elevated https://docs.clawd.bot/tools/exec-approvals
|
||||
- Exec approvals: default to local host, add gateway/node targeting + target details, support wildcard agent allowlists, and tighten allowlist parsing/safe bins. https://docs.clawd.bot/cli/approvals https://docs.clawd.bot/tools/exec-approvals
|
||||
- Heartbeat: allow explicit session keys and active hours. (#1256) Thanks @zknicker. https://docs.clawd.bot/gateway/heartbeat
|
||||
- Sessions: add per-channel idle durations via `sessions.channelIdleMinutes`. (#1353) Thanks @cash-echo-bot.
|
||||
- Nodes: run exec-style, expose PATH in status/describe, and bootstrap PATH for node-host execution. https://docs.clawd.bot/cli/node
|
||||
- Cache: add `cache.ttlPrune` mode and auth-aware defaults for cache TTL behavior.
|
||||
- Queue: add per-channel debounce overrides for auto-reply. https://docs.clawd.bot/concepts/queue
|
||||
- Discord: add wildcard channel config support. (#1334) Thanks @pvoo. https://docs.clawd.bot/channels/discord
|
||||
- Signal: add typing indicators and DM read receipts via signal-cli. https://docs.clawd.bot/channels/signal
|
||||
- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero. https://docs.clawd.bot/channels/msteams
|
||||
- Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead).
|
||||
- Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.clawd.bot/gateway/troubleshooting
|
||||
- Docs: add /model allowlist troubleshooting note. (#1405)
|
||||
- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.
|
||||
- macOS: refresh Settings (location access in Permissions, connection mode in menu, remove CLI install UI).
|
||||
- Diagnostics: add cache trace config for debugging. (#1370) Thanks @parubets.
|
||||
- Docs: Lobster guides + org URL updates, /model allowlist troubleshooting, Gmail message search examples, gateway.mode troubleshooting, prompt injection guidance, npm prefix/node CLI notes, control UI dev gatewayUrl note, tool_use FAQ, showcase video, and sharp/node-gyp workaround. (#1427, #1220, #1405) Thanks @vignesh07, @mbelinky.
|
||||
|
||||
### Breaking
|
||||
- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.clawd.bot/web/control-ui#insecure-http
|
||||
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert.
|
||||
|
||||
### Fixes
|
||||
- Streaming/Typing/Media: keep reply tags across streamed chunks, start typing indicators at run start, and accept MEDIA paths with spaces/tilde while preferring the message tool hint for image replies.
|
||||
- Agents/Providers: drop unsigned thinking blocks for Claude models (Google Antigravity) and enforce alphanumeric tool call ids for strict providers (Mistral/OpenRouter). (#1372) Thanks @zerone0x.
|
||||
- Exec approvals: treat main as the default agent, align node/gateway allowlist prechecks, validate resolved paths, avoid allowlist resolve races, and avoid null optional params. (#1417, #1414, #1425) Thanks @czekaj.
|
||||
- Exec/Windows: resolve Windows exec paths with extensions and handle safe-bin exe names.
|
||||
- Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman.
|
||||
- Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380)
|
||||
- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early.
|
||||
- Agents: enforce 9-char alphanumeric tool call ids for Mistral providers. (#1372) Thanks @zerone0x.
|
||||
- Gateway: prevent multiple gateways from sharing the same config/state (singleton lock), keep auto bind loopback-first with explicit tailnet binding, and improve SSH auth handling. (#1380)
|
||||
- Control UI: remove the chat stop button, keep the composer aligned to the bottom edge, stabilize session previews, and refresh the debug panel on route-driven tab changes. (#1373) Thanks @yazinsai.
|
||||
- UI/config: export `SECTION_META` for config form modules. (#1418) Thanks @MaudeBot.
|
||||
- macOS: keep chat pinned during streaming replies, include Textual resources, respect wildcard exec approvals, allow SSH agent auth, and default distribution builds to universal binaries. (#1279, #1362, #1384, #1396) Thanks @ameno-, @JustYannicc.
|
||||
- BlueBubbles: resolve short message IDs safely, expose full IDs in templates, and harden short-id fetch wrappers. (#1369, #1387) Thanks @tyler6204.
|
||||
- Models/Configure: inherit session model overrides in threads/topics, map OpenCode Zen models to the correct APIs, narrow Anthropic OAuth allowlist handling, seed allowlist fallbacks, list the full catalog when no allowlist is set, and limit `/model` list output. (#1376, #1416)
|
||||
- Memory: prevent CLI hangs by deferring vector probes, add sqlite-vec/embedding timeouts, and make session memory indexing async.
|
||||
- Cron: cap reminder context history to 10 messages and honor `contextMessages`. (#1103) Thanks @mkbehr.
|
||||
- Cache: restore the 1h cache TTL option and reset the pruning window.
|
||||
- Zalo Personal: tolerate ANSI/log-prefixed JSON output from `zca`. (#1379) Thanks @ptn1411.
|
||||
- Browser: suppress Chrome restore prompts for managed profiles. (#1419) Thanks @jamesgroat.
|
||||
- Infra: preserve fetch helper methods/preconnect when wrapping abort signals and normalize Telegram fetch aborts.
|
||||
- Config/Doctor: avoid stack traces for invalid configs, log the config path, avoid WhatsApp config resurrection, and warn when `gateway.mode` is unset. (#900)
|
||||
- CLI: read Codex CLI account_id for workspace billing. (#1422) Thanks @aj47.
|
||||
- Logs/Status: align rolling log filenames with local time and report sandboxed runtime in `clawdbot status`. (#1343)
|
||||
- Embedded runner: persist injected history images so attachments aren’t reloaded each turn. (#1374) Thanks @Nicell.
|
||||
- Nodes tool: include agent/node/gateway context in tool failure logs to speed approval debugging.
|
||||
- macOS: exec approvals now respect wildcard agent allowlists (`*`).
|
||||
- macOS: allow SSH agent auth when no identity file is set. (#1384) Thanks @ameno-.
|
||||
- Gateway: prevent multiple gateways from sharing the same config/state at once (singleton lock).
|
||||
- UI: remove the chat stop button and keep the composer aligned to the bottom edge.
|
||||
- Typing: start instant typing indicators at run start so DMs and mentions show immediately.
|
||||
- Configure: restrict the model allowlist picker to OAuth-compatible Anthropic models and preselect Opus 4.5.
|
||||
- Configure: seed model fallbacks from the allowlist selection when multiple models are chosen.
|
||||
- Model picker: list the full catalog when no model allowlist is configured.
|
||||
- Discord: honor wildcard channel configs via shared match helpers. (#1334) Thanks @pvoo.
|
||||
- BlueBubbles: resolve short message IDs safely and expose full IDs in templates. (#1387) Thanks @tyler6204.
|
||||
- Infra: preserve fetch helper methods when wrapping abort signals. (#1387)
|
||||
- macOS: default distribution packaging to universal binaries. (#1396) Thanks @JustYannicc.
|
||||
- Nodes/Subagents: include agent/node/gateway context in tool failure logs and ensure subagent list uses the command session.
|
||||
|
||||
## 2026.1.20
|
||||
|
||||
|
||||
@@ -477,29 +477,28 @@ Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and
|
||||
Thanks to all clawtributors:
|
||||
|
||||
<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/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></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/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/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/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/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/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/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></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/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/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/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/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/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/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></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/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/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/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/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/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/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/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/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/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/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/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/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/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></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/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/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/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/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/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></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/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/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></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/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/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></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/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/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/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></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/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/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/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></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/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/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/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/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/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/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></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/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/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/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/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></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/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/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></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/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/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/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/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/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></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/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/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/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></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/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></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/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></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=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/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=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></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/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></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/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></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/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></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/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></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/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></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=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/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></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/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/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/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/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></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/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/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/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/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/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/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/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/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/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/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/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></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/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/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></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/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/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/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/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></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/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></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/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/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></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/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></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/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></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=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/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/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></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/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></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/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></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/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/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a>
|
||||
<a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></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/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></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=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>
|
||||
+16
-48
@@ -2,54 +2,6 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>Clawdbot</title>
|
||||
<item>
|
||||
<title>2026.1.22</title>
|
||||
<pubDate>Fri, 23 Jan 2026 08:58:14 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
||||
<sparkle:version>7530</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.1.22</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>Clawdbot 2026.1.22</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Highlight: Compaction safeguard now uses adaptive chunking, progressive fallback, and UI status + retries. (#1466) Thanks @dlauer.</li>
|
||||
<li>Providers: add Antigravity usage tracking to status output. (#1490) Thanks @patelhiren.</li>
|
||||
<li>Slack: add chat-type reply threading overrides via <code>replyToModeByChatType</code>. (#1442) Thanks @stefangalescu.</li>
|
||||
<li>BlueBubbles: add <code>asVoice</code> support for MP3/CAF voice memos in sendAttachment. (#1477, #1482) Thanks @Nicell.</li>
|
||||
<li>Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.</li>
|
||||
<li>Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.</li>
|
||||
<li>Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.</li>
|
||||
<li>Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.</li>
|
||||
<li>Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x.</li>
|
||||
<li>Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.</li>
|
||||
<li>Gateway: stop the service before uninstalling and fail if it remains loaded.</li>
|
||||
<li>Agents: surface concrete API error details instead of generic AI service errors.</li>
|
||||
<li>Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484)</li>
|
||||
<li>Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.</li>
|
||||
<li>Agents: make OpenAI sessions image-sanitize-only; gate tool-id/repair sanitization by provider.</li>
|
||||
<li>Doctor: honor CLAWDBOT_GATEWAY_TOKEN for auth checks and security audit token reuse. (#1448) Thanks @azade-c.</li>
|
||||
<li>Agents: make tool summaries more readable and only show optional params when set.</li>
|
||||
<li>Agents: honor SOUL.md guidance even when the file is nested or path-qualified. (#1434) Thanks @neooriginal.</li>
|
||||
<li>Matrix (plugin): persist m.direct for resolved DMs and harden room fallback. (#1436, #1486) Thanks @sibbl.</li>
|
||||
<li>CLI: prefer <code>~</code> for home paths in output.</li>
|
||||
<li>Mattermost (plugin): enforce pairing/allowlist gating, keep @username targets, and clarify plugin-only docs. (#1428) Thanks @damoahdominic.</li>
|
||||
<li>Agents: centralize transcript sanitization in the runner; keep <final> tags and error turns intact.</li>
|
||||
<li>Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik.</li>
|
||||
<li>Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff.</li>
|
||||
<li>Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.</li>
|
||||
<li>Slack: reduce WebClient retries to avoid duplicate sends. (#1481)</li>
|
||||
<li>Slack: read thread replies for message reads when threadId is provided (replies-only). (#1450) Thanks @rodrigouroz.</li>
|
||||
<li>macOS: prefer linked channels in gateway summary to avoid false “not linked” status.</li>
|
||||
<li>macOS/tests: fix gateway summary lookup after guard unwrap; prevent browser opens during tests. (ECID-1483)</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.22/Clawdbot-2026.1.22.zip" length="22302446" type="application/octet-stream" sparkle:edSignature="w/EzfwGBCRRuCg5vz8enIfYujxOZJWRw9PaunQ7gIafKwnBJSTtxcnkvMVwQsnBwB6VN5Tu2MPij7PjDFFX+CA=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.1.21</title>
|
||||
<pubDate>Thu, 22 Jan 2026 12:22:35 +0000</pubDate>
|
||||
@@ -314,5 +266,21 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.21/Clawdbot-2026.1.21.zip" length="12208102" type="application/octet-stream" sparkle:edSignature="hU495Eii8O3qmmUnxYFhXyEGv+qan6KL+GpeuBhPIXf+7B5F/gBh5Oz9cHaqaAPoZ4/3Bo6xgvic0HTkbz6gDw=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.1.16-2</title>
|
||||
<pubDate>Sat, 17 Jan 2026 12:46:22 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
||||
<sparkle:version>6273</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.1.16-2</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>Clawdbot 2026.1.16-2</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>CLI: stamp build commit into dist metadata so banners show the commit in npm installs.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.16-2/Clawdbot-2026.1.16-2.zip" length="21399591" type="application/octet-stream" sparkle:edSignature="zelT+KzN32cXsihbFniPF5Heq0hkwFfL3Agrh/AaoKUkr7kJAFarkGSOZRTWZ9y+DvOluzn2wHHjVigRjMzrBA=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -102,9 +102,7 @@ struct DebugSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
"When enabled, Clawdbot won't install or manage \(gatewayLaunchdLabel). " +
|
||||
"It will only attach to an existing Gateway.")
|
||||
Text("When enabled, Clawdbot won't install or manage \(gatewayLaunchdLabel). It will only attach to an existing Gateway.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
|
||||
@@ -84,52 +84,11 @@ enum ExecApprovalDecision: String, Codable, Sendable {
|
||||
case deny
|
||||
}
|
||||
|
||||
struct ExecAllowlistEntry: Codable, Hashable, Identifiable {
|
||||
var id: UUID
|
||||
struct ExecAllowlistEntry: Codable, Hashable {
|
||||
var pattern: String
|
||||
var lastUsedAt: Double?
|
||||
var lastUsedCommand: String?
|
||||
var lastResolvedPath: String?
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
pattern: String,
|
||||
lastUsedAt: Double? = nil,
|
||||
lastUsedCommand: String? = nil,
|
||||
lastResolvedPath: String? = nil)
|
||||
{
|
||||
self.id = id
|
||||
self.pattern = pattern
|
||||
self.lastUsedAt = lastUsedAt
|
||||
self.lastUsedCommand = lastUsedCommand
|
||||
self.lastResolvedPath = lastResolvedPath
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case pattern
|
||||
case lastUsedAt
|
||||
case lastUsedCommand
|
||||
case lastResolvedPath
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
|
||||
self.pattern = try container.decode(String.self, forKey: .pattern)
|
||||
self.lastUsedAt = try container.decodeIfPresent(Double.self, forKey: .lastUsedAt)
|
||||
self.lastUsedCommand = try container.decodeIfPresent(String.self, forKey: .lastUsedCommand)
|
||||
self.lastResolvedPath = try container.decodeIfPresent(String.self, forKey: .lastResolvedPath)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.id, forKey: .id)
|
||||
try container.encode(self.pattern, forKey: .pattern)
|
||||
try container.encodeIfPresent(self.lastUsedAt, forKey: .lastUsedAt)
|
||||
try container.encodeIfPresent(self.lastUsedCommand, forKey: .lastUsedCommand)
|
||||
try container.encodeIfPresent(self.lastResolvedPath, forKey: .lastResolvedPath)
|
||||
}
|
||||
}
|
||||
|
||||
struct ExecApprovalsDefaults: Codable {
|
||||
@@ -336,7 +295,6 @@ enum ExecApprovalsStore {
|
||||
let allowlist = ((wildcardEntry.allowlist ?? []) + (agentEntry.allowlist ?? []))
|
||||
.map { entry in
|
||||
ExecAllowlistEntry(
|
||||
id: entry.id,
|
||||
pattern: entry.pattern.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
lastUsedAt: entry.lastUsedAt,
|
||||
lastUsedCommand: entry.lastUsedCommand,
|
||||
@@ -421,7 +379,6 @@ enum ExecApprovalsStore {
|
||||
let allowlist = (entry.allowlist ?? []).map { item -> ExecAllowlistEntry in
|
||||
guard item.pattern == pattern else { return item }
|
||||
return ExecAllowlistEntry(
|
||||
id: item.id,
|
||||
pattern: item.pattern,
|
||||
lastUsedAt: Date().timeIntervalSince1970 * 1000,
|
||||
lastUsedCommand: command,
|
||||
@@ -441,7 +398,6 @@ enum ExecApprovalsStore {
|
||||
let cleaned = allowlist
|
||||
.map { item in
|
||||
ExecAllowlistEntry(
|
||||
id: item.id,
|
||||
pattern: item.pattern.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
lastUsedAt: item.lastUsedAt,
|
||||
lastUsedCommand: item.lastUsedCommand,
|
||||
|
||||
@@ -248,11 +248,12 @@ final class GatewayProcessManager {
|
||||
guard let linkId else {
|
||||
return "port \(port), health probe succeeded, \(instanceText)"
|
||||
}
|
||||
let linked = snap.channels[linkId]?.linked ?? false
|
||||
let authAge = snap.channels[linkId]?.authAgeMs.flatMap(msToAge) ?? "unknown age"
|
||||
let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false
|
||||
let authAge = linkId.flatMap { snap.channels[$0]?.authAgeMs }.flatMap(msToAge) ?? "unknown age"
|
||||
let label =
|
||||
snap.channelLabels?[linkId] ??
|
||||
linkId.capitalized
|
||||
linkId.flatMap { snap.channelLabels?[$0] } ??
|
||||
linkId?.capitalized ??
|
||||
"channel"
|
||||
let linkText = linked ? "linked" : "not linked"
|
||||
return "port \(port), \(label) \(linkText), auth \(authAge), \(instanceText)"
|
||||
}
|
||||
|
||||
@@ -123,12 +123,12 @@ struct SystemRunSettingsView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(self.model.entries, id: \.id) { entry in
|
||||
ForEach(Array(self.model.entries.enumerated()), id: \.offset) { index, _ in
|
||||
ExecAllowlistRow(
|
||||
entry: Binding(
|
||||
get: { self.model.entry(for: entry.id) ?? entry },
|
||||
set: { self.model.updateEntry($0, id: entry.id) }),
|
||||
onRemove: { self.model.removeEntry(id: entry.id) })
|
||||
get: { self.model.entries[index] },
|
||||
set: { self.model.updateEntry($0, at: index) }),
|
||||
onRemove: { self.model.removeEntry(at: index) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,24 +373,20 @@ final class ExecApprovalsSettingsModel {
|
||||
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
|
||||
}
|
||||
|
||||
func updateEntry(_ entry: ExecAllowlistEntry, id: UUID) {
|
||||
func updateEntry(_ entry: ExecAllowlistEntry, at index: Int) {
|
||||
guard !self.isDefaultsScope else { return }
|
||||
guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return }
|
||||
guard self.entries.indices.contains(index) else { return }
|
||||
self.entries[index] = entry
|
||||
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
|
||||
}
|
||||
|
||||
func removeEntry(id: UUID) {
|
||||
func removeEntry(at index: Int) {
|
||||
guard !self.isDefaultsScope else { return }
|
||||
guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return }
|
||||
guard self.entries.indices.contains(index) else { return }
|
||||
self.entries.remove(at: index)
|
||||
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
|
||||
}
|
||||
|
||||
func entry(for id: UUID) -> ExecAllowlistEntry? {
|
||||
self.entries.first(where: { $0.id == id })
|
||||
}
|
||||
|
||||
func refreshSkillBins(force: Bool = false) async {
|
||||
guard self.autoAllowSkills else {
|
||||
self.skillBins = []
|
||||
|
||||
@@ -21,8 +21,6 @@ struct VoiceWakeSettings: View {
|
||||
@State private var micObserver = AudioInputDeviceObserver()
|
||||
@State private var micRefreshTask: Task<Void, Never>?
|
||||
@State private var availableLocales: [Locale] = []
|
||||
@State private var triggerEntries: [TriggerEntry] = []
|
||||
@FocusState private var focusedTriggerEntryID: UUID?
|
||||
private let fieldLabelWidth: CGFloat = 140
|
||||
private let controlWidth: CGFloat = 240
|
||||
private let isPreview = ProcessInfo.processInfo.isPreview
|
||||
@@ -33,9 +31,9 @@ struct VoiceWakeSettings: View {
|
||||
var id: String { self.uid }
|
||||
}
|
||||
|
||||
private struct TriggerEntry: Identifiable {
|
||||
let id: UUID
|
||||
var value: String
|
||||
private struct IndexedWord: Identifiable {
|
||||
let id: Int
|
||||
let value: String
|
||||
}
|
||||
|
||||
private var voiceWakeBinding: Binding<Bool> {
|
||||
@@ -107,7 +105,6 @@ struct VoiceWakeSettings: View {
|
||||
.onAppear {
|
||||
guard !self.isPreview else { return }
|
||||
self.startMicObserver()
|
||||
self.loadTriggerEntries()
|
||||
}
|
||||
.onChange(of: self.state.voiceWakeMicID) { _, _ in
|
||||
guard !self.isPreview else { return }
|
||||
@@ -125,10 +122,8 @@ struct VoiceWakeSettings: View {
|
||||
self.micRefreshTask = nil
|
||||
Task { await self.meter.stop() }
|
||||
self.micObserver.stop()
|
||||
self.syncTriggerEntriesToState()
|
||||
} else {
|
||||
self.startMicObserver()
|
||||
self.loadTriggerEntries()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
@@ -141,21 +136,11 @@ struct VoiceWakeSettings: View {
|
||||
self.micRefreshTask = nil
|
||||
self.micObserver.stop()
|
||||
Task { await self.meter.stop() }
|
||||
self.syncTriggerEntriesToState()
|
||||
}
|
||||
.onChange(of: self.state.swabbleTriggerWords) { _, _ in
|
||||
guard !self.isPreview else { return }
|
||||
guard self.focusedTriggerEntryID == nil else { return }
|
||||
self.loadTriggerEntries()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadTriggerEntries() {
|
||||
self.triggerEntries = self.state.swabbleTriggerWords.map { TriggerEntry(id: UUID(), value: $0) }
|
||||
}
|
||||
|
||||
private func syncTriggerEntriesToState() {
|
||||
self.state.swabbleTriggerWords = self.triggerEntries.map(\.value)
|
||||
private var indexedWords: [IndexedWord] {
|
||||
self.state.swabbleTriggerWords.enumerated().map { IndexedWord(id: $0.offset, value: $0.element) }
|
||||
}
|
||||
|
||||
private var triggerTable: some View {
|
||||
@@ -169,47 +154,29 @@ struct VoiceWakeSettings: View {
|
||||
} label: {
|
||||
Label("Add word", systemImage: "plus")
|
||||
}
|
||||
.disabled(self.triggerEntries
|
||||
.contains(where: { $0.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))
|
||||
.disabled(self.state.swabbleTriggerWords
|
||||
.contains(where: { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))
|
||||
|
||||
Button("Reset defaults") {
|
||||
self.triggerEntries = defaultVoiceWakeTriggers.map { TriggerEntry(id: UUID(), value: $0) }
|
||||
self.syncTriggerEntriesToState()
|
||||
Button("Reset defaults") { self.state.swabbleTriggerWords = defaultVoiceWakeTriggers }
|
||||
}
|
||||
|
||||
Table(self.indexedWords) {
|
||||
TableColumn("Word") { row in
|
||||
TextField("Wake word", text: self.binding(for: row.id))
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ForEach(self.$triggerEntries) { $entry in
|
||||
HStack(spacing: 8) {
|
||||
TextField("Wake word", text: $entry.value)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.focused(self.$focusedTriggerEntryID, equals: entry.id)
|
||||
.onSubmit {
|
||||
self.syncTriggerEntriesToState()
|
||||
}
|
||||
|
||||
Button {
|
||||
self.removeWord(id: entry.id)
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.help("Remove trigger word")
|
||||
.frame(width: 24)
|
||||
}
|
||||
.padding(8)
|
||||
|
||||
if entry.id != self.triggerEntries.last?.id {
|
||||
Divider()
|
||||
TableColumn("") { row in
|
||||
Button {
|
||||
self.removeWord(at: row.id)
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.help("Remove trigger word")
|
||||
}
|
||||
.width(36)
|
||||
}
|
||||
.onChange(of: self.focusedTriggerEntryID) { oldValue, newValue in
|
||||
guard oldValue != nil, oldValue != newValue else { return }
|
||||
self.syncTriggerEntriesToState()
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: 180, alignment: .topLeading)
|
||||
.background(Color(nsColor: .textBackgroundColor))
|
||||
.frame(minHeight: 180)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
@@ -244,12 +211,24 @@ struct VoiceWakeSettings: View {
|
||||
}
|
||||
|
||||
private func addWord() {
|
||||
self.triggerEntries.append(TriggerEntry(id: UUID(), value: ""))
|
||||
self.state.swabbleTriggerWords.append("")
|
||||
}
|
||||
|
||||
private func removeWord(id: UUID) {
|
||||
self.triggerEntries.removeAll { $0.id == id }
|
||||
self.syncTriggerEntriesToState()
|
||||
private func removeWord(at index: Int) {
|
||||
guard self.state.swabbleTriggerWords.indices.contains(index) else { return }
|
||||
self.state.swabbleTriggerWords.remove(at: index)
|
||||
}
|
||||
|
||||
private func binding(for index: Int) -> Binding<String> {
|
||||
Binding(
|
||||
get: {
|
||||
guard self.state.swabbleTriggerWords.indices.contains(index) else { return "" }
|
||||
return self.state.swabbleTriggerWords[index]
|
||||
},
|
||||
set: { newValue in
|
||||
guard self.state.swabbleTriggerWords.indices.contains(index) else { return }
|
||||
self.state.swabbleTriggerWords[index] = newValue
|
||||
})
|
||||
}
|
||||
|
||||
private func toggleTest() {
|
||||
@@ -659,14 +638,13 @@ extension VoiceWakeSettings {
|
||||
state.voicePushToTalkEnabled = true
|
||||
state.swabbleTriggerWords = ["Claude", "Hey"]
|
||||
|
||||
var view = VoiceWakeSettings(state: state, isActive: true)
|
||||
let view = VoiceWakeSettings(state: state, isActive: true)
|
||||
view.availableMics = [AudioInputDevice(uid: "mic-1", name: "Built-in")]
|
||||
view.availableLocales = [Locale(identifier: "en_US")]
|
||||
view.meterLevel = 0.42
|
||||
view.meterError = "No input"
|
||||
view.testState = .detected("ok")
|
||||
view.isTesting = true
|
||||
view.triggerEntries = [TriggerEntry(id: UUID(), value: "Claude")]
|
||||
|
||||
_ = view.body
|
||||
_ = view.localePicker
|
||||
@@ -676,9 +654,8 @@ extension VoiceWakeSettings {
|
||||
_ = view.chimeSection
|
||||
|
||||
view.addWord()
|
||||
if let entryId = view.triggerEntries.first?.id {
|
||||
view.removeWord(id: entryId)
|
||||
}
|
||||
_ = view.binding(for: 0).wrappedValue
|
||||
view.removeWord(at: 0)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -147,8 +147,7 @@ Available actions:
|
||||
- **addParticipant**: Add someone to a group (`chatGuid`, `address`)
|
||||
- **removeParticipant**: Remove someone from a group (`chatGuid`, `address`)
|
||||
- **leaveGroup**: Leave a group chat (`chatGuid`)
|
||||
- **sendAttachment**: Send media/files (`to`, `buffer`, `filename`, `asVoice`)
|
||||
- Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos.
|
||||
- **sendAttachment**: Send media/files (`to`, `buffer`, `filename`)
|
||||
|
||||
### Message IDs (short vs full)
|
||||
Clawdbot may surface *short* message IDs (e.g., `1`, `2`) to save tokens.
|
||||
|
||||
@@ -8,8 +8,6 @@ read_when:
|
||||
# Mattermost (plugin)
|
||||
|
||||
Status: supported via plugin (bot token + WebSocket events). Channels, groups, and DMs are supported.
|
||||
Mattermost is a self-hostable team messaging platform; see the official site at
|
||||
[mattermost.com](https://mattermost.com) for product details and downloads.
|
||||
|
||||
## Plugin required
|
||||
Mattermost ships as a plugin and is not bundled with the core install.
|
||||
|
||||
+1
-69
@@ -304,8 +304,7 @@ Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
|
||||
"policy": "pairing",
|
||||
"allowFrom": ["U123", "U456", "*"],
|
||||
"groupEnabled": false,
|
||||
"groupChannels": ["G123"],
|
||||
"replyToMode": "all"
|
||||
"groupChannels": ["G123"]
|
||||
},
|
||||
"channels": {
|
||||
"C123": { "allow": true, "requireMention": true },
|
||||
@@ -362,73 +361,6 @@ By default, Clawdbot replies in the main channel. Use `channels.slack.replyToMod
|
||||
|
||||
The mode applies to both auto-replies and agent tool calls (`slack sendMessage`).
|
||||
|
||||
### Per-chat-type threading
|
||||
You can configure different threading behavior per chat type by setting `channels.slack.replyToModeByChatType`:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
replyToMode: "off", // default for channels
|
||||
replyToModeByChatType: {
|
||||
direct: "all", // DMs always thread
|
||||
group: "first" // group DMs/MPIM thread first reply
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported chat types:
|
||||
- `direct`: 1:1 DMs (Slack `im`)
|
||||
- `group`: group DMs / MPIMs (Slack `mpim`)
|
||||
- `channel`: standard channels (public/private)
|
||||
|
||||
Precedence:
|
||||
1) `replyToModeByChatType.<chatType>`
|
||||
2) `replyToMode`
|
||||
3) Provider default (`off`)
|
||||
|
||||
Legacy `channels.slack.dm.replyToMode` is still accepted as a fallback for `direct` when no chat-type override is set.
|
||||
|
||||
Examples:
|
||||
|
||||
Thread DMs only:
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
replyToMode: "off",
|
||||
replyToModeByChatType: { direct: "all" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Thread group DMs but keep channels in the root:
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
replyToMode: "off",
|
||||
replyToModeByChatType: { group: "first" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Make channels thread, keep DMs in the root:
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
replyToMode: "first",
|
||||
replyToModeByChatType: { direct: "off", group: "off" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Manual threading tags
|
||||
For fine-grained control, use these tags in agent responses:
|
||||
- `[[reply_to_current]]` — reply to the triggering message (start/continue thread).
|
||||
|
||||
@@ -700,15 +700,8 @@ Options:
|
||||
- `--json`
|
||||
- `--plain`
|
||||
- `--check` (exit 1=expired/missing, 2=expiring)
|
||||
- `--probe` (live probe of configured auth profiles)
|
||||
- `--probe-provider <name>`
|
||||
- `--probe-profile <id>` (repeat or comma-separated)
|
||||
- `--probe-timeout <ms>`
|
||||
- `--probe-concurrency <n>`
|
||||
- `--probe-max-tokens <n>`
|
||||
|
||||
Always includes the auth overview and OAuth expiry status for profiles in the auth store.
|
||||
`--probe` runs live requests (may consume tokens and trigger rate limits).
|
||||
|
||||
### `models set <model>`
|
||||
Set `agents.defaults.model.primary`.
|
||||
|
||||
@@ -25,26 +25,12 @@ clawdbot models scan
|
||||
`clawdbot models status` shows the resolved default/fallbacks plus an auth overview.
|
||||
When provider usage snapshots are available, the OAuth/token status section includes
|
||||
provider usage headers.
|
||||
Add `--probe` to run live auth probes against each configured provider profile.
|
||||
Probes are real requests (may consume tokens and trigger rate limits).
|
||||
|
||||
Notes:
|
||||
- `models set <model-or-alias>` accepts `provider/model` or an alias.
|
||||
- Model refs are parsed by splitting on the **first** `/`. If the model ID includes `/` (OpenRouter-style), include the provider prefix (example: `openrouter/moonshotai/kimi-k2`).
|
||||
- If you omit the provider, Clawdbot treats the input as an alias or a model for the **default provider** (only works when there is no `/` in the model ID).
|
||||
|
||||
### `models status`
|
||||
Options:
|
||||
- `--json`
|
||||
- `--plain`
|
||||
- `--check` (exit 1=expired/missing, 2=expiring)
|
||||
- `--probe` (live probe of configured auth profiles)
|
||||
- `--probe-provider <name>` (probe one provider)
|
||||
- `--probe-profile <id>` (repeat or comma-separated profile ids)
|
||||
- `--probe-timeout <ms>`
|
||||
- `--probe-concurrency <n>`
|
||||
- `--probe-max-tokens <n>`
|
||||
|
||||
## Aliases + fallbacks
|
||||
|
||||
```bash
|
||||
|
||||
+4
-5
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot update` (safe-ish source update + gateway auto-restart)"
|
||||
summary: "CLI reference for `clawdbot update` (safe-ish source update + optional gateway restart)"
|
||||
read_when:
|
||||
- You want to update a source checkout safely
|
||||
- You need to understand `--update` shorthand behavior
|
||||
@@ -20,14 +20,14 @@ clawdbot update wizard
|
||||
clawdbot update --channel beta
|
||||
clawdbot update --channel dev
|
||||
clawdbot update --tag beta
|
||||
clawdbot update --no-restart
|
||||
clawdbot update --restart
|
||||
clawdbot update --json
|
||||
clawdbot --update
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
- `--no-restart`: skip restarting the Gateway service after a successful update.
|
||||
- `--restart`: restart the Gateway service after a successful update.
|
||||
- `--channel <stable|beta|dev>`: set the update channel (git + npm; persisted in config).
|
||||
- `--tag <dist-tag|version>`: override the npm dist-tag or version for this update only.
|
||||
- `--json`: print machine-readable `UpdateRunResult` JSON.
|
||||
@@ -52,8 +52,7 @@ Options:
|
||||
## `update wizard`
|
||||
|
||||
Interactive flow to pick an update channel and confirm whether to restart the Gateway
|
||||
after updating (default is to restart). If you select `dev` without a git checkout, it
|
||||
offers to create one.
|
||||
after updating. If you select `dev` without a git checkout, it offers to create one.
|
||||
|
||||
## What it does
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ stay consistent across channels.
|
||||
1. **Parse Markdown -> IR**
|
||||
- IR is plain text plus style spans (bold/italic/strike/code/spoiler) and link spans.
|
||||
- Offsets are UTF-16 code units so Signal style ranges align with its API.
|
||||
- Tables are parsed only when a channel opts into table conversion.
|
||||
2. **Chunk IR (format-first)**
|
||||
- Chunking happens on the IR text before rendering.
|
||||
- Inline formatting does not split across chunks; spans are sliced per chunk.
|
||||
@@ -60,30 +59,7 @@ IR (schematic):
|
||||
|
||||
- Slack, Telegram, and Signal outbound adapters render from the IR.
|
||||
- Other channels (WhatsApp, iMessage, MS Teams, Discord) still use plain text or
|
||||
their own formatting rules, with Markdown table conversion applied before
|
||||
chunking when enabled.
|
||||
|
||||
## Table handling
|
||||
|
||||
Markdown tables are not consistently supported across chat clients. Use
|
||||
`markdown.tables` to control conversion per channel (and per account).
|
||||
|
||||
- `code`: render tables as code blocks (default for most channels).
|
||||
- `bullets`: convert each row into bullet points (default for Signal + WhatsApp).
|
||||
- `off`: disable table parsing and conversion; raw table text passes through.
|
||||
|
||||
Config keys:
|
||||
|
||||
```yaml
|
||||
channels:
|
||||
discord:
|
||||
markdown:
|
||||
tables: code
|
||||
accounts:
|
||||
work:
|
||||
markdown:
|
||||
tables: off
|
||||
```
|
||||
their own formatting rules.
|
||||
|
||||
## Chunking rules
|
||||
|
||||
|
||||
+8
-8
@@ -109,14 +109,6 @@
|
||||
"source": "/opencode/",
|
||||
"destination": "/providers/opencode"
|
||||
},
|
||||
{
|
||||
"source": "/mattermost",
|
||||
"destination": "/channels/mattermost"
|
||||
},
|
||||
{
|
||||
"source": "/mattermost/",
|
||||
"destination": "/channels/mattermost"
|
||||
},
|
||||
{
|
||||
"source": "/glm",
|
||||
"destination": "/providers/glm"
|
||||
@@ -141,6 +133,14 @@
|
||||
"source": "/message/",
|
||||
"destination": "/cli/message"
|
||||
},
|
||||
{
|
||||
"source": "/mattermost",
|
||||
"destination": "/channels/mattermost"
|
||||
},
|
||||
{
|
||||
"source": "/mattermost/",
|
||||
"destination": "/channels/mattermost"
|
||||
},
|
||||
{
|
||||
"source": "/providers/discord",
|
||||
"destination": "/channels/discord"
|
||||
|
||||
@@ -129,7 +129,7 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||
enabled: true,
|
||||
maxBytes: 20971520,
|
||||
models: [
|
||||
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
|
||||
{ provider: "openai", model: "whisper-1" },
|
||||
// Optional CLI fallback (Whisper binary):
|
||||
// { type: "cli", command: "whisper", args: ["--model", "base", "{{MediaPath}}"] }
|
||||
],
|
||||
|
||||
@@ -1865,7 +1865,7 @@ Note: `applyPatch` is only under `tools.exec`.
|
||||
- Each `models[]` entry:
|
||||
- Provider entry (`type: "provider"` or omitted):
|
||||
- `provider`: API provider id (`openai`, `anthropic`, `google`/`gemini`, `groq`, etc).
|
||||
- `model`: model id override (required for image; defaults to `gpt-4o-mini-transcribe`/`whisper-large-v3-turbo` for audio providers, and `gemini-3-flash-preview` for video).
|
||||
- `model`: model id override (required for image; defaults to `whisper-1`/`whisper-large-v3-turbo` for audio providers, and `gemini-3-flash-preview` for video).
|
||||
- `profile` / `preferredProfile`: auth profile selection.
|
||||
- CLI entry (`type: "cli"`):
|
||||
- `command`: executable to run.
|
||||
@@ -1890,7 +1890,7 @@ Example:
|
||||
rules: [{ action: "allow", match: { chatType: "direct" } }]
|
||||
},
|
||||
models: [
|
||||
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
|
||||
{ provider: "openai", model: "whisper-1" },
|
||||
{ type: "cli", command: "whisper", args: ["--model", "base", "{{MediaPath}}"] }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ You can tune console verbosity independently via:
|
||||
|
||||
## Tool summary redaction
|
||||
|
||||
Verbose tool summaries (e.g. `🛠️ Exec: ...`) can mask sensitive tokens before they hit the
|
||||
Verbose tool summaries (e.g. `🛠️ exec: ...`) can mask sensitive tokens before they hit the
|
||||
console stream. This is **tools-only** and does not alter file logs.
|
||||
|
||||
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)
|
||||
|
||||
@@ -7,7 +7,7 @@ read_when:
|
||||
|
||||
# Updating
|
||||
|
||||
Clawdbot is moving fast (pre “1.0”). Treat updates like shipping infra: update → run checks → restart (or use `clawdbot update`, which restarts) → verify.
|
||||
Clawdbot is moving fast (pre “1.0”). Treat updates like shipping infra: update → run checks → restart → verify.
|
||||
|
||||
## Recommended: re-run the website installer (upgrade in place)
|
||||
|
||||
@@ -81,7 +81,7 @@ Notes:
|
||||
For **source installs** (git checkout), prefer:
|
||||
|
||||
```bash
|
||||
clawdbot update
|
||||
clawdbot update --restart
|
||||
```
|
||||
|
||||
It runs a safe-ish update flow:
|
||||
@@ -89,7 +89,6 @@ It runs a safe-ish update flow:
|
||||
- Switches to the selected channel (tag or branch).
|
||||
- Fetches + rebases against the configured upstream (dev channel).
|
||||
- Installs deps, builds, builds the Control UI, and runs `clawdbot doctor`.
|
||||
- Restarts the gateway by default (use `--no-restart` to skip).
|
||||
|
||||
If you installed via **npm/pnpm** (no git metadata), `clawdbot update` will try to update via your package manager. If it can’t detect the install, use “Update (global install)” instead.
|
||||
|
||||
|
||||
+3
-19
@@ -6,7 +6,7 @@ read_when:
|
||||
# Audio / Voice Notes — 2026-01-17
|
||||
|
||||
## What works
|
||||
- **Media understanding (audio)**: If audio understanding is enabled (or auto‑detected), Clawdbot:
|
||||
- **Media understanding (audio)**: If `tools.media.audio` is enabled (or a shared `tools.media.models` entry supports audio), Clawdbot:
|
||||
1) Locates the first audio attachment (local path or URL) and downloads it if needed.
|
||||
2) Enforces `maxBytes` before sending to each model entry.
|
||||
3) Runs the first eligible model entry in order (provider or CLI).
|
||||
@@ -15,21 +15,6 @@ read_when:
|
||||
- **Command parsing**: When transcription succeeds, `CommandBody`/`RawBody` are set to the transcript so slash commands still work.
|
||||
- **Verbose logging**: In `--verbose`, we log when transcription runs and when it replaces the body.
|
||||
|
||||
## Auto-detection (default)
|
||||
If you **don’t configure models** and `tools.media.audio.enabled` is **not** set to `false`,
|
||||
Clawdbot auto-detects in this order and stops at the first working option:
|
||||
|
||||
1) **Local CLIs** (if installed)
|
||||
- `sherpa-onnx-offline` (requires `SHERPA_ONNX_MODEL_DIR` with encoder/decoder/joiner/tokens)
|
||||
- `whisper-cli` (from `whisper-cpp`; uses `WHISPER_CPP_MODEL` or the bundled tiny model)
|
||||
- `whisper` (Python CLI; downloads models automatically)
|
||||
2) **Gemini CLI** (`gemini`) using `read_many_files`
|
||||
3) **Provider keys** (OpenAI → Groq → Deepgram → Google)
|
||||
|
||||
To disable auto-detection, set `tools.media.audio.enabled: false`.
|
||||
To customize, set `tools.media.audio.models`.
|
||||
Note: Binary detection is best-effort across macOS/Linux/Windows; ensure the CLI is on `PATH` (we expand `~`), or set an explicit CLI model with a full command path.
|
||||
|
||||
## Config examples
|
||||
|
||||
### Provider + CLI fallback (OpenAI + Whisper CLI)
|
||||
@@ -41,7 +26,7 @@ Note: Binary detection is best-effort across macOS/Linux/Windows; ensure the CLI
|
||||
enabled: true,
|
||||
maxBytes: 20971520,
|
||||
models: [
|
||||
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
|
||||
{ provider: "openai", model: "whisper-1" },
|
||||
{
|
||||
type: "cli",
|
||||
command: "whisper",
|
||||
@@ -69,7 +54,7 @@ Note: Binary detection is best-effort across macOS/Linux/Windows; ensure the CLI
|
||||
]
|
||||
},
|
||||
models: [
|
||||
{ provider: "openai", model: "gpt-4o-mini-transcribe" }
|
||||
{ provider: "openai", model: "whisper-1" }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -98,7 +83,6 @@ Note: Binary detection is best-effort across macOS/Linux/Windows; ensure the CLI
|
||||
- Audio providers can override `baseUrl`, `headers`, and `providerOptions` via `tools.media.audio`.
|
||||
- Default size cap is 20MB (`tools.media.audio.maxBytes`). Oversize audio is skipped for that model and the next entry is tried.
|
||||
- Default `maxChars` for audio is **unset** (full transcript). Set `tools.media.audio.maxChars` or per-entry `maxChars` to trim output.
|
||||
- OpenAI auto default is `gpt-4o-mini-transcribe`; set `model: "gpt-4o-transcribe"` for higher accuracy.
|
||||
- Use `tools.media.audio.attachments` to process multiple voice notes (`mode: "all"` + `maxAttachments`).
|
||||
- Transcript is available to templates as `{{Transcript}}`.
|
||||
- CLI stdout is capped (5MB); keep CLI output concise.
|
||||
|
||||
@@ -6,7 +6,7 @@ read_when:
|
||||
---
|
||||
# Media Understanding (Inbound) — 2026-01-17
|
||||
|
||||
Clawdbot can **summarize inbound media** (image/audio/video) before the reply pipeline runs. It auto‑detects when local tools or provider keys are available, and can be disabled or customized. If understanding is off, models still receive the original files/URLs as usual.
|
||||
Clawdbot can optionally **summarize inbound media** (image/audio/video) before the reply pipeline runs. This is **opt-in** and separate from the base attachment flow—if understanding is off, models still receive the original files/URLs as usual.
|
||||
|
||||
## Goals
|
||||
- Optional: pre‑digest inbound media into short text for faster routing + better command parsing.
|
||||
@@ -88,11 +88,6 @@ Each `models[]` entry can be **provider** or **CLI**:
|
||||
}
|
||||
```
|
||||
|
||||
CLI templates can also use:
|
||||
- `{{MediaDir}}` (directory containing the media file)
|
||||
- `{{OutputDir}}` (scratch dir created for this run)
|
||||
- `{{OutputBase}}` (scratch file base path, no extension)
|
||||
|
||||
## Defaults and limits
|
||||
Recommended defaults:
|
||||
- `maxChars`: **500** for image/video (short, command‑friendly)
|
||||
@@ -109,22 +104,17 @@ Rules:
|
||||
- If `<capability>.enabled: true` but no models are configured, Clawdbot tries the
|
||||
**active reply model** when its provider supports the capability.
|
||||
|
||||
### Auto-detect media understanding (default)
|
||||
If `tools.media.<capability>.enabled` is **not** set to `false` and you haven’t
|
||||
configured models, Clawdbot auto-detects in this order and **stops at the first
|
||||
working option**:
|
||||
### Auto-enable audio (when keys exist)
|
||||
If `tools.media.audio.enabled` is **not** set to `false` and you have any supported
|
||||
audio provider keys configured, Clawdbot will **auto-enable audio transcription**
|
||||
even when you haven’t listed models explicitly.
|
||||
|
||||
1) **Local CLIs** (audio only; if installed)
|
||||
- `sherpa-onnx-offline` (requires `SHERPA_ONNX_MODEL_DIR` with encoder/decoder/joiner/tokens)
|
||||
- `whisper-cli` (`whisper-cpp`; uses `WHISPER_CPP_MODEL` or the bundled tiny model)
|
||||
- `whisper` (Python CLI; downloads models automatically)
|
||||
2) **Gemini CLI** (`gemini`) using `read_many_files`
|
||||
3) **Provider keys**
|
||||
- Audio: OpenAI → Groq → Deepgram → Google
|
||||
- Image: OpenAI → Anthropic → Google → MiniMax
|
||||
- Video: Google
|
||||
Providers checked (in order):
|
||||
1) OpenAI
|
||||
2) Groq
|
||||
3) Deepgram
|
||||
|
||||
To disable auto-detection, set:
|
||||
To disable this behavior, set:
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
@@ -136,7 +126,6 @@ To disable auto-detection, set:
|
||||
}
|
||||
}
|
||||
```
|
||||
Note: Binary detection is best-effort across macOS/Linux/Windows; ensure the CLI is on `PATH` (we expand `~`), or set an explicit CLI model with a full command path.
|
||||
|
||||
## Capabilities (optional)
|
||||
If you set `capabilities`, the entry only runs for those media types. For shared
|
||||
@@ -153,7 +142,7 @@ If you omit `capabilities`, the entry is eligible for the list it appears in.
|
||||
| Capability | Provider integration | Notes |
|
||||
|------------|----------------------|-------|
|
||||
| Image | OpenAI / Anthropic / Google / others via `pi-ai` | Any image-capable model in the registry works. |
|
||||
| Audio | OpenAI, Groq, Deepgram, Google | Provider transcription (Whisper/Deepgram/Gemini). |
|
||||
| Audio | OpenAI, Groq, Deepgram | Provider transcription (Whisper/Deepgram). |
|
||||
| Video | Google (Gemini API) | Provider video understanding. |
|
||||
|
||||
## Recommended providers
|
||||
@@ -162,8 +151,8 @@ If you omit `capabilities`, the entry is eligible for the list it appears in.
|
||||
- Good defaults: `openai/gpt-5.2`, `anthropic/claude-opus-4-5`, `google/gemini-3-pro-preview`.
|
||||
|
||||
**Audio**
|
||||
- `openai/gpt-4o-mini-transcribe`, `groq/whisper-large-v3-turbo`, or `deepgram/nova-3`.
|
||||
- CLI fallback: `whisper-cli` (whisper-cpp) or `whisper`.
|
||||
- `openai/whisper-1`, `groq/whisper-large-v3-turbo`, or `deepgram/nova-3`.
|
||||
- CLI fallback: `whisper` binary.
|
||||
- Deepgram setup: [Deepgram (audio transcription)](/providers/deepgram).
|
||||
|
||||
**Video**
|
||||
@@ -220,7 +209,7 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
|
||||
audio: {
|
||||
enabled: true,
|
||||
models: [
|
||||
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
|
||||
{ provider: "openai", model: "whisper-1" },
|
||||
{
|
||||
type: "cli",
|
||||
command: "whisper",
|
||||
|
||||
@@ -11,7 +11,7 @@ This app now ships Sparkle auto-updates. Release builds must be Developer ID–s
|
||||
|
||||
## Prereqs
|
||||
- Developer ID Application cert installed (example: `Developer ID Application: <Developer Name> (<TEAMID>)`).
|
||||
- Sparkle private key path set in the environment as `SPARKLE_PRIVATE_KEY_FILE` (path to your Sparkle ed25519 private key; public key baked into Info.plist). If it is missing, check `~/.profile`.
|
||||
- Sparkle private key path set in the environment as `SPARKLE_PRIVATE_KEY_FILE` (path to your Sparkle ed25519 private key; public key baked into Info.plist).
|
||||
- Notary credentials (keychain profile or API key) for `xcrun notarytool` if you want Gatekeeper-safe DMG/zip distribution.
|
||||
- We use a Keychain profile named `clawdbot-notary`, created from App Store Connect API key env vars in your shell profile:
|
||||
- `APP_STORE_CONNECT_API_KEY_P8`, `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_ISSUER_ID`
|
||||
|
||||
@@ -61,7 +61,6 @@ Plugins can register:
|
||||
- CLI commands
|
||||
- Background services
|
||||
- Optional config validation
|
||||
- **Skills** (by listing `skills` directories in the plugin manifest)
|
||||
|
||||
Plugins run **in‑process** with the Gateway, so treat them as trusted code.
|
||||
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
|
||||
|
||||
@@ -82,8 +82,6 @@ Enable optional tools in `agents.list[].tools.allow` (or global `tools.allow`):
|
||||
```
|
||||
|
||||
Other config knobs that affect tool availability:
|
||||
- Allowlists that only name plugin tools are treated as plugin opt-ins; core tools remain
|
||||
enabled unless you also include core tools or groups in the allowlist.
|
||||
- `tools.profile` / `agents.list[].tools.profile` (base allowlist)
|
||||
- `tools.byProvider` / `agents.list[].tools.byProvider` (provider‑specific allow/deny)
|
||||
- `tools.sandbox.tools.*` (sandbox tool policy when sandboxed)
|
||||
|
||||
@@ -34,7 +34,6 @@ 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.
|
||||
- `skills` (array): skill directories to load (relative to the plugin root).
|
||||
- `name` (string): display name for the plugin.
|
||||
- `description` (string): short plugin summary.
|
||||
- `uiHints` (object): config field labels/placeholders/sensitive flags for UI rendering.
|
||||
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
---
|
||||
summary: "OpenProse: .prose workflows, slash commands, and state in Clawdbot"
|
||||
read_when:
|
||||
- You want to run or write .prose workflows
|
||||
- You want to enable the OpenProse plugin
|
||||
- You need to understand state storage
|
||||
---
|
||||
# OpenProse
|
||||
|
||||
OpenProse is a portable, markdown-first workflow format for orchestrating AI sessions. In Clawdbot it ships as a plugin that installs an OpenProse skill pack plus a `/prose` slash command. Programs live in `.prose` files and can spawn multiple sub-agents with explicit control flow.
|
||||
|
||||
Official site: https://www.prose.md
|
||||
|
||||
## What it can do
|
||||
|
||||
- Multi-agent research + synthesis with explicit parallelism.
|
||||
- Repeatable approval-safe workflows (code review, incident triage, content pipelines).
|
||||
- Reusable `.prose` programs you can run across supported agent runtimes.
|
||||
|
||||
## Install + enable
|
||||
|
||||
Bundled plugins are disabled by default. Enable OpenProse:
|
||||
|
||||
```bash
|
||||
clawdbot plugins enable open-prose
|
||||
```
|
||||
|
||||
Restart the Gateway after enabling the plugin.
|
||||
|
||||
Dev/local checkout: `clawdbot plugins install ./extensions/open-prose`
|
||||
|
||||
Related docs: [Plugins](/plugin), [Plugin manifest](/plugins/manifest), [Skills](/tools/skills).
|
||||
|
||||
## Slash command
|
||||
|
||||
OpenProse registers `/prose` as a user-invocable skill command. It routes to the OpenProse VM instructions and uses Clawdbot tools under the hood.
|
||||
|
||||
Common commands:
|
||||
|
||||
```
|
||||
/prose help
|
||||
/prose run <file.prose>
|
||||
/prose run <handle/slug>
|
||||
/prose run <https://example.com/file.prose>
|
||||
/prose compile <file.prose>
|
||||
/prose examples
|
||||
/prose update
|
||||
```
|
||||
|
||||
## Example: a simple `.prose` file
|
||||
|
||||
```prose
|
||||
# Research + synthesis with two agents running in parallel.
|
||||
|
||||
input topic: "What should we research?"
|
||||
|
||||
agent researcher:
|
||||
model: sonnet
|
||||
prompt: "You research thoroughly and cite sources."
|
||||
|
||||
agent writer:
|
||||
model: opus
|
||||
prompt: "You write a concise summary."
|
||||
|
||||
parallel:
|
||||
findings = session: researcher
|
||||
prompt: "Research {topic}."
|
||||
draft = session: writer
|
||||
prompt: "Summarize {topic}."
|
||||
|
||||
session "Merge the findings + draft into a final answer."
|
||||
context: { findings, draft }
|
||||
```
|
||||
|
||||
## File locations
|
||||
|
||||
OpenProse keeps state under `.prose/` in your workspace:
|
||||
|
||||
```
|
||||
.prose/
|
||||
├── .env
|
||||
├── runs/
|
||||
│ └── {YYYYMMDD}-{HHMMSS}-{random}/
|
||||
│ ├── program.prose
|
||||
│ ├── state.md
|
||||
│ ├── bindings/
|
||||
│ └── agents/
|
||||
└── agents/
|
||||
```
|
||||
|
||||
User-level persistent agents live at:
|
||||
|
||||
```
|
||||
~/.prose/agents/
|
||||
```
|
||||
|
||||
## State modes
|
||||
|
||||
OpenProse supports multiple state backends:
|
||||
|
||||
- **filesystem** (default): `.prose/runs/...`
|
||||
- **in-context**: transient, for small programs
|
||||
- **sqlite** (experimental): requires `sqlite3` binary
|
||||
- **postgres** (experimental): requires `psql` and a connection string
|
||||
|
||||
Notes:
|
||||
- sqlite/postgres are opt-in and experimental.
|
||||
- postgres credentials flow into subagent logs; use a dedicated, least-privileged DB.
|
||||
|
||||
## Remote programs
|
||||
|
||||
`/prose run <handle/slug>` resolves to `https://p.prose.md/<handle>/<slug>`.
|
||||
Direct URLs are fetched as-is. This uses the `web_fetch` tool (or `exec` for POST).
|
||||
|
||||
## Clawdbot runtime mapping
|
||||
|
||||
OpenProse programs map to Clawdbot primitives:
|
||||
|
||||
| OpenProse concept | Clawdbot tool |
|
||||
| --- | --- |
|
||||
| Spawn session / Task tool | `sessions_spawn` |
|
||||
| File read/write | `read` / `write` |
|
||||
| Web fetch | `web_fetch` |
|
||||
|
||||
If your tool allowlist blocks these tools, OpenProse programs will fail. See [Skills config](/tools/skills-config).
|
||||
|
||||
## Security + approvals
|
||||
|
||||
Treat `.prose` files like code. Review before running. Use Clawdbot tool allowlists and approval gates to control side effects.
|
||||
|
||||
For deterministic, approval-gated workflows, compare with [Lobster](/tools/lobster).
|
||||
@@ -13,7 +13,7 @@ Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tag
|
||||
## Operator trigger
|
||||
When the operator says “release”, immediately do this preflight (no extra questions unless blocked):
|
||||
- Read this doc and `docs/platforms/mac/release.md`.
|
||||
- Load env from `~/.profile` and confirm `SPARKLE_PRIVATE_KEY_FILE` + App Store Connect vars are set (SPARKLE_PRIVATE_KEY_FILE should live in `~/.profile`).
|
||||
- Load env from `~/.profile` and confirm `SPARKLE_PRIVATE_KEY_FILE` + App Store Connect vars are set.
|
||||
- Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed.
|
||||
|
||||
1) **Version & metadata**
|
||||
@@ -39,9 +39,8 @@ When the operator says “release”, immediately do this preflight (no extra qu
|
||||
- [ ] `pnpm test` (or `pnpm test:coverage` if you need coverage output)
|
||||
- [ ] `pnpm run build` (last sanity check after tests)
|
||||
- [ ] `pnpm release:check` (verifies npm pack contents)
|
||||
- [ ] `CLAWDBOT_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke` (Docker install smoke test, fast path; required before release)
|
||||
- [ ] `pnpm test:install:smoke` (Docker install smoke test; required before release)
|
||||
- If the immediate previous npm release is known broken, set `CLAWDBOT_INSTALL_SMOKE_PREVIOUS=<last-good-version>` or `CLAWDBOT_INSTALL_SMOKE_SKIP_PREVIOUS=1` for the preinstall step.
|
||||
- [ ] (Optional) Full installer smoke (adds non-root + CLI coverage): `pnpm test:install:smoke`
|
||||
- [ ] (Optional) Installer E2E (Docker, runs `curl -fsSL https://clawd.bot/install.sh | bash`, onboards, then runs real tool calls):
|
||||
- `pnpm test:install:e2e:openai` (requires `OPENAI_API_KEY`)
|
||||
- `pnpm test:install:e2e:anthropic` (requires `ANTHROPIC_API_KEY`)
|
||||
|
||||
@@ -12,7 +12,6 @@ This document explains how Clawdbot manages sessions end-to-end:
|
||||
- **Session routing** (how inbound messages map to a `sessionKey`)
|
||||
- **Session store** (`sessions.json`) and what it tracks
|
||||
- **Transcript persistence** (`*.jsonl`) and its structure
|
||||
- **Transcript hygiene** (provider-specific fixups before runs)
|
||||
- **Context limits** (context window vs tracked tokens)
|
||||
- **Compaction** (manual + auto-compaction) and where to hook pre-compaction work
|
||||
- **Silent housekeeping** (e.g. memory writes that shouldn’t produce user-visible output)
|
||||
@@ -21,7 +20,6 @@ If you want a higher-level overview first, start with:
|
||||
- [/concepts/session](/concepts/session)
|
||||
- [/concepts/compaction](/concepts/compaction)
|
||||
- [/concepts/session-pruning](/concepts/session-pruning)
|
||||
- [/reference/transcript-hygiene](/reference/transcript-hygiene)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
summary: "Reference: provider-specific transcript sanitization and repair rules"
|
||||
read_when:
|
||||
- You are debugging provider request rejections tied to transcript shape
|
||||
- You are changing transcript sanitization or tool-call repair logic
|
||||
- You are investigating tool-call id mismatches across providers
|
||||
---
|
||||
# Transcript Hygiene (Provider Fixups)
|
||||
|
||||
This document describes **provider-specific fixes** applied to transcripts before a run
|
||||
(building model context). These are **in-memory** adjustments used to satisfy strict
|
||||
provider requirements. They do **not** rewrite the stored JSONL transcript on disk.
|
||||
|
||||
Scope includes:
|
||||
- Tool call id sanitization
|
||||
- Tool result pairing repair
|
||||
- Turn validation / ordering
|
||||
- Thought signature cleanup
|
||||
- Image payload sanitization
|
||||
|
||||
If you need transcript storage details, see:
|
||||
- [/reference/session-management-compaction](/reference/session-management-compaction)
|
||||
|
||||
---
|
||||
|
||||
## Where this runs
|
||||
|
||||
All transcript hygiene is centralized in the embedded runner:
|
||||
- Policy selection: `src/agents/transcript-policy.ts`
|
||||
- Sanitization/repair application: `sanitizeSessionHistory` in `src/agents/pi-embedded-runner/google.ts`
|
||||
|
||||
The policy uses `provider`, `modelApi`, and `modelId` to decide what to apply.
|
||||
|
||||
---
|
||||
|
||||
## Global rule: image sanitization
|
||||
|
||||
Image payloads are always sanitized to prevent provider-side rejection due to size
|
||||
limits (downscale/recompress oversized base64 images).
|
||||
|
||||
Implementation:
|
||||
- `sanitizeSessionMessagesImages` in `src/agents/pi-embedded-helpers/images.ts`
|
||||
- `sanitizeContentBlocksImages` in `src/agents/tool-images.ts`
|
||||
|
||||
---
|
||||
|
||||
## Provider matrix (current behavior)
|
||||
|
||||
**OpenAI / OpenAI Codex**
|
||||
- Image sanitization only.
|
||||
- No tool call id sanitization.
|
||||
- No tool result pairing repair.
|
||||
- No turn validation or reordering.
|
||||
- No synthetic tool results.
|
||||
- No thought signature stripping.
|
||||
|
||||
**Google (Generative AI / Gemini CLI / Antigravity)**
|
||||
- Tool call id sanitization: strict alphanumeric.
|
||||
- Tool result pairing repair and synthetic tool results.
|
||||
- Turn validation (Gemini-style turn alternation).
|
||||
- Google turn ordering fixup (prepend a tiny user bootstrap if history starts with assistant).
|
||||
- Antigravity Claude: normalize thinking signatures; drop unsigned thinking blocks.
|
||||
|
||||
**Anthropic / Minimax (Anthropic-compatible)**
|
||||
- Tool result pairing repair and synthetic tool results.
|
||||
- Turn validation (merge consecutive user turns to satisfy strict alternation).
|
||||
|
||||
**Mistral (including model-id based detection)**
|
||||
- Tool call id sanitization: strict9 (alphanumeric length 9).
|
||||
|
||||
**OpenRouter Gemini**
|
||||
- Thought signature cleanup: strip non-base64 `thought_signature` values (keep base64).
|
||||
|
||||
**Everything else**
|
||||
- Image sanitization only.
|
||||
|
||||
---
|
||||
|
||||
## Historical behavior (pre-2026.1.22)
|
||||
|
||||
Before the 2026.1.22 release, Clawdbot applied multiple layers of transcript hygiene:
|
||||
|
||||
- A **transcript-sanitize extension** ran on every context build and could:
|
||||
- Repair tool use/result pairing.
|
||||
- Sanitize tool call ids (including a non-strict mode that preserved `_`/`-`).
|
||||
- The runner also performed provider-specific sanitization, which duplicated work.
|
||||
- Additional mutations occurred outside the provider policy, including:
|
||||
- Stripping `<final>` tags from assistant text before persistence.
|
||||
- Dropping empty assistant error turns.
|
||||
- Trimming assistant content after tool calls.
|
||||
|
||||
This complexity caused cross-provider regressions (notably `openai-responses`
|
||||
`call_id|fc_id` pairing). The 2026.1.22 cleanup removed the extension, centralized
|
||||
logic in the runner, and made OpenAI **no-touch** beyond image sanitization.
|
||||
@@ -324,7 +324,6 @@ brew install <formula>
|
||||
```
|
||||
|
||||
If you run Clawdbot via systemd, ensure the service PATH includes `/home/linuxbrew/.linuxbrew/bin` (or your brew prefix) so `brew`-installed tools resolve in non‑login shells.
|
||||
Recent builds also prepend common user bin dirs on Linux systemd services (for example `~/.local/bin`, `~/.npm-global/bin`, `~/.local/share/pnpm`, `~/.bun/bin`) and honor `PNPM_HOME`, `NPM_CONFIG_PREFIX`, `BUN_INSTALL`, `VOLTA_HOME`, `ASDF_DATA_DIR`, `NVM_DIR`, and `FNM_DIR` when set.
|
||||
|
||||
### Can I switch between npm and git installs later?
|
||||
|
||||
|
||||
@@ -97,7 +97,6 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
||||
## Tools + automation
|
||||
|
||||
- [Tools surface](/tools)
|
||||
- [OpenProse](/prose)
|
||||
- [CLI reference](/cli)
|
||||
- [Exec tool](/tools/exec)
|
||||
- [Elevated mode](/tools/elevated)
|
||||
|
||||
@@ -54,7 +54,6 @@ Example schema:
|
||||
"autoAllowSkills": true,
|
||||
"allowlist": [
|
||||
{
|
||||
"id": "B0C8C0B3-2C2D-4F8A-9A3C-5A4B3C2D1E0F",
|
||||
"pattern": "~/Projects/**/bin/rg",
|
||||
"lastUsedAt": 1737150000000,
|
||||
"lastUsedCommand": "rg -n TODO",
|
||||
@@ -97,7 +96,6 @@ Examples:
|
||||
- `/opt/homebrew/bin/rg`
|
||||
|
||||
Each allowlist entry tracks:
|
||||
- **id** stable UUID used for UI identity (optional)
|
||||
- **last used** timestamp
|
||||
- **last used command**
|
||||
- **last resolved path**
|
||||
|
||||
@@ -11,10 +11,6 @@ read_when:
|
||||
|
||||
Lobster is a workflow shell that lets Clawdbot run multi-step tool sequences as a single, deterministic operation with explicit approval checkpoints.
|
||||
|
||||
## Hook
|
||||
|
||||
Your assistant can build the tools that manage itself. Ask for a workflow, and 30 minutes later you have a CLI plus pipelines that run as one call. Lobster is the missing piece: deterministic pipelines, explicit approvals, and resumable state.
|
||||
|
||||
## Why
|
||||
|
||||
Today, complex workflows require many back-and-forth tool calls. Each call costs tokens, and the LLM has to orchestrate every step. Lobster moves that orchestration into a typed runtime:
|
||||
@@ -28,73 +24,6 @@ Today, complex workflows require many back-and-forth tool calls. Each call costs
|
||||
Clawdbot launches the local `lobster` CLI in **tool mode** and parses a JSON envelope from stdout.
|
||||
If the pipeline pauses for approval, the tool returns a `resumeToken` so you can continue later.
|
||||
|
||||
## Pattern: small CLI + JSON pipes + approvals
|
||||
|
||||
Build tiny commands that speak JSON, then chain them into a single Lobster call. (Example command names below — swap in your own.)
|
||||
|
||||
```bash
|
||||
inbox list --json
|
||||
inbox categorize --json
|
||||
inbox apply --json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "run",
|
||||
"pipeline": "exec --json --shell 'inbox list --json' | exec --stdin json --shell 'inbox categorize --json' | exec --stdin json --shell 'inbox apply --json' | approve --preview-from-stdin --limit 5 --prompt 'Apply changes?'",
|
||||
"timeoutMs": 30000
|
||||
}
|
||||
```
|
||||
|
||||
If the pipeline requests approval, resume with the token:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "resume",
|
||||
"token": "<resumeToken>",
|
||||
"approve": true
|
||||
}
|
||||
```
|
||||
|
||||
AI triggers the workflow; Lobster executes the steps. Approval gates keep side effects explicit and auditable.
|
||||
|
||||
Example: map input items into tool calls:
|
||||
|
||||
```bash
|
||||
gog.gmail.search --query 'newer_than:1d' \
|
||||
| clawd.invoke --tool message --action send --each --item-key message --args-json '{"provider":"telegram","to":"..."}'
|
||||
```
|
||||
|
||||
## Workflow files (.lobster)
|
||||
|
||||
Lobster can run YAML/JSON workflow files with `name`, `args`, `steps`, `env`, `condition`, and `approval` fields. In Clawdbot tool calls, set `pipeline` to the file path.
|
||||
|
||||
```yaml
|
||||
name: inbox-triage
|
||||
args:
|
||||
tag:
|
||||
default: "family"
|
||||
steps:
|
||||
- id: collect
|
||||
command: inbox list --json
|
||||
- id: categorize
|
||||
command: inbox categorize --json
|
||||
stdin: $collect.stdout
|
||||
- id: approve
|
||||
command: inbox apply --approve
|
||||
stdin: $categorize.stdout
|
||||
approval: required
|
||||
- id: execute
|
||||
command: inbox apply --execute
|
||||
stdin: $categorize.stdout
|
||||
condition: $approve.approved
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `stdin: $step.stdout` and `stdin: $step.json` pass a prior step’s output.
|
||||
- `condition` (or `when`) can gate steps on `$step.approved`.
|
||||
|
||||
## Install Lobster
|
||||
|
||||
Install the Lobster CLI on the **same host** that runs the Clawdbot Gateway (see the [Lobster repo](https://github.com/clawdbot/lobster)), and ensure `lobster` is on `PATH`.
|
||||
@@ -121,10 +50,6 @@ Lobster is an **optional** plugin tool (not enabled by default). Allow it per ag
|
||||
|
||||
You can also allow it globally with `tools.allow` if every agent should see it.
|
||||
|
||||
Note: allowlists are opt-in for optional plugins. If your allowlist only names
|
||||
plugin tools (like `lobster`), Clawdbot keeps core tools enabled. To restrict core
|
||||
tools, include the core tools or groups you want in the allowlist too.
|
||||
|
||||
## Example: Email triage
|
||||
|
||||
Without Lobster:
|
||||
@@ -190,16 +115,6 @@ Run a pipeline in tool mode.
|
||||
}
|
||||
```
|
||||
|
||||
Run a workflow file with args:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "run",
|
||||
"pipeline": "/path/to/inbox-triage.lobster",
|
||||
"argsJson": "{\"tag\":\"family\"}"
|
||||
}
|
||||
```
|
||||
|
||||
### `resume`
|
||||
|
||||
Continue a halted workflow after approval.
|
||||
@@ -218,7 +133,6 @@ Continue a halted workflow after approval.
|
||||
- `cwd`: Working directory for the pipeline (defaults to the current process working directory).
|
||||
- `timeoutMs`: Kill the subprocess if it exceeds this duration (default: 20000).
|
||||
- `maxStdoutBytes`: Kill the subprocess if stdout exceeds this size (default: 512000).
|
||||
- `argsJson`: JSON string passed to `lobster run --args-json` (workflow files only).
|
||||
|
||||
## Output envelope
|
||||
|
||||
@@ -237,12 +151,6 @@ If `requiresApproval` is present, inspect the prompt and decide:
|
||||
- `approve: true` → resume and continue side effects
|
||||
- `approve: false` → cancel and finalize the workflow
|
||||
|
||||
Use `approve --preview-from-stdin --limit N` to attach a JSON preview to approval requests without custom jq/heredoc glue. Resume tokens are now compact: Lobster stores workflow resume state under its state dir and hands back a small token key.
|
||||
|
||||
## OpenProse
|
||||
|
||||
OpenProse pairs well with Lobster: use `/prose` to orchestrate multi-agent prep, then run a Lobster pipeline for deterministic approvals. If a Prose program needs Lobster, allow the `lobster` tool for sub-agents via `tools.subagents.tools`. See [OpenProse](/prose).
|
||||
|
||||
## Safety
|
||||
|
||||
- **Local subprocess only** — no network calls from the plugin itself.
|
||||
@@ -261,10 +169,3 @@ OpenProse pairs well with Lobster: use `/prose` to orchestrate multi-agent prep,
|
||||
|
||||
- [Plugins](/plugin)
|
||||
- [Plugin tool authoring](/plugins/agent-tools)
|
||||
|
||||
## Case study: community workflows
|
||||
|
||||
One public example: a “second brain” CLI + Lobster pipelines that manage three Markdown vaults (personal, partner, shared). The CLI emits JSON for stats, inbox listings, and stale scans; Lobster chains those commands into workflows like `weekly-review`, `inbox-triage`, `memory-consolidation`, and `shared-task-sync`, each with approval gates. AI handles judgment (categorization) when available and falls back to deterministic rules when not.
|
||||
|
||||
- Thread: https://x.com/plattenschieber/status/2014508656335770033
|
||||
- Repo: https://github.com/bloomedai/brain-cli
|
||||
@@ -38,12 +38,10 @@ applies: workspace wins, then managed/local, then bundled.
|
||||
|
||||
## Plugins + skills
|
||||
|
||||
Plugins can ship their own skills by listing `skills` directories in
|
||||
`clawdbot.plugin.json` (paths relative to the plugin root). Plugin skills load
|
||||
when the plugin is enabled and participate in the normal skill precedence rules.
|
||||
You can gate them via `metadata.clawdbot.requires.config` on the plugin’s config
|
||||
entry. See [Plugins](/plugin) for discovery/config and [Tools](/tools) for the
|
||||
tool surface those skills teach.
|
||||
Plugins can ship their own skills (for example, `voice-call`) and gate them via
|
||||
`metadata.clawdbot.requires.config` on the plugin’s config entry. See
|
||||
[Plugins](/plugin) for plugin discovery/config and [Tools](/tools) for the tool
|
||||
surface those skills teach.
|
||||
|
||||
## ClawdHub (install + sync)
|
||||
|
||||
|
||||
@@ -109,7 +109,6 @@ Notes:
|
||||
- `/skill <name> [input]` runs a skill by name (useful when native command limits prevent per-skill commands).
|
||||
- By default, skill commands are forwarded to the model as a normal request.
|
||||
- Skills may optionally declare `command-dispatch: tool` to route the command directly to a tool (deterministic, no model).
|
||||
- Example: `/prose` (OpenProse plugin) — see [OpenProse](/prose).
|
||||
- **Native command arguments:** Discord uses autocomplete for dynamic options (and button menus when you omit required args). Telegram and Slack show a button menu when a command supports choices and you omit the arg.
|
||||
|
||||
## Usage surfaces (what shows where)
|
||||
|
||||
@@ -88,8 +88,6 @@ Session lifecycle:
|
||||
- `/settings`
|
||||
- `/exit`
|
||||
|
||||
Other Gateway slash commands (for example, `/context`) are forwarded to the Gateway and shown as system output. See [Slash commands](/tools/slash-commands).
|
||||
|
||||
## Local shell commands
|
||||
- Prefix a line with `!` to run a local shell command on the TUI host.
|
||||
- The TUI prompts once per session to allow local execution; declining keeps `!` disabled for the session.
|
||||
|
||||
@@ -521,42 +521,6 @@ describe("bluebubblesMessageActions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes asVoice through sendAttachment", async () => {
|
||||
const { sendBlueBubblesAttachment } = await import("./attachments.js");
|
||||
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
serverUrl: "http://localhost:1234",
|
||||
password: "test-password",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const base64Buffer = Buffer.from("voice").toString("base64");
|
||||
|
||||
await bluebubblesMessageActions.handleAction({
|
||||
action: "sendAttachment",
|
||||
params: {
|
||||
to: "+15551234567",
|
||||
filename: "voice.mp3",
|
||||
buffer: base64Buffer,
|
||||
contentType: "audio/mpeg",
|
||||
asVoice: true,
|
||||
},
|
||||
cfg,
|
||||
accountId: null,
|
||||
});
|
||||
|
||||
expect(sendBlueBubblesAttachment).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
filename: "voice.mp3",
|
||||
contentType: "audio/mpeg",
|
||||
asVoice: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when buffer is missing for setGroupIcon", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
BLUEBUBBLES_ACTIONS,
|
||||
createActionGate,
|
||||
jsonResult,
|
||||
readBooleanParam,
|
||||
readNumberParam,
|
||||
readReactionParams,
|
||||
readStringParam,
|
||||
@@ -50,17 +51,6 @@ function readMessageText(params: Record<string, unknown>): string | undefined {
|
||||
return readStringParam(params, "text") ?? readStringParam(params, "message");
|
||||
}
|
||||
|
||||
function readBooleanParam(params: Record<string, unknown>, key: string): boolean | undefined {
|
||||
const raw = params[key];
|
||||
if (typeof raw === "boolean") return raw;
|
||||
if (typeof raw === "string") {
|
||||
const trimmed = raw.trim().toLowerCase();
|
||||
if (trimmed === "true") return true;
|
||||
if (trimmed === "false") return false;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Supported action names for BlueBubbles */
|
||||
const SUPPORTED_ACTIONS = new Set<ChannelMessageActionName>(BLUEBUBBLES_ACTION_NAMES);
|
||||
|
||||
@@ -366,7 +356,6 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
const caption = readStringParam(params, "caption");
|
||||
const contentType =
|
||||
readStringParam(params, "contentType") ?? readStringParam(params, "mimeType");
|
||||
const asVoice = readBooleanParam(params, "asVoice");
|
||||
|
||||
// Buffer can come from params.buffer (base64) or params.path (file path)
|
||||
const base64Buffer = readStringParam(params, "buffer");
|
||||
@@ -391,7 +380,6 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = {
|
||||
filename,
|
||||
contentType: contentType ?? undefined,
|
||||
caption: caption ?? undefined,
|
||||
asVoice: asVoice ?? undefined,
|
||||
opts,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js";
|
||||
import { downloadBlueBubblesAttachment } from "./attachments.js";
|
||||
import type { BlueBubblesAttachment } from "./types.js";
|
||||
|
||||
vi.mock("./accounts.js", () => ({
|
||||
@@ -238,109 +238,3 @@ describe("downloadBlueBubblesAttachment", () => {
|
||||
expect(result.buffer).toEqual(new Uint8Array([1]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendBlueBubblesAttachment", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
mockFetch.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function decodeBody(body: Uint8Array) {
|
||||
return Buffer.from(body).toString("utf8");
|
||||
}
|
||||
|
||||
it("marks voice memos when asVoice is true and mp3 is provided", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(JSON.stringify({ messageId: "msg-1" })),
|
||||
});
|
||||
|
||||
await sendBlueBubblesAttachment({
|
||||
to: "chat_guid:iMessage;-;+15551234567",
|
||||
buffer: new Uint8Array([1, 2, 3]),
|
||||
filename: "voice.mp3",
|
||||
contentType: "audio/mpeg",
|
||||
asVoice: true,
|
||||
opts: { serverUrl: "http://localhost:1234", password: "test" },
|
||||
});
|
||||
|
||||
const body = mockFetch.mock.calls[0][1]?.body as Uint8Array;
|
||||
const bodyText = decodeBody(body);
|
||||
expect(bodyText).toContain('name="isAudioMessage"');
|
||||
expect(bodyText).toContain("true");
|
||||
expect(bodyText).toContain('filename="voice.mp3"');
|
||||
});
|
||||
|
||||
it("normalizes mp3 filenames for voice memos", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(JSON.stringify({ messageId: "msg-2" })),
|
||||
});
|
||||
|
||||
await sendBlueBubblesAttachment({
|
||||
to: "chat_guid:iMessage;-;+15551234567",
|
||||
buffer: new Uint8Array([1, 2, 3]),
|
||||
filename: "voice",
|
||||
contentType: "audio/mpeg",
|
||||
asVoice: true,
|
||||
opts: { serverUrl: "http://localhost:1234", password: "test" },
|
||||
});
|
||||
|
||||
const body = mockFetch.mock.calls[0][1]?.body as Uint8Array;
|
||||
const bodyText = decodeBody(body);
|
||||
expect(bodyText).toContain('filename="voice.mp3"');
|
||||
expect(bodyText).toContain('name="voice.mp3"');
|
||||
});
|
||||
|
||||
it("throws when asVoice is true but media is not audio", async () => {
|
||||
await expect(
|
||||
sendBlueBubblesAttachment({
|
||||
to: "chat_guid:iMessage;-;+15551234567",
|
||||
buffer: new Uint8Array([1, 2, 3]),
|
||||
filename: "image.png",
|
||||
contentType: "image/png",
|
||||
asVoice: true,
|
||||
opts: { serverUrl: "http://localhost:1234", password: "test" },
|
||||
}),
|
||||
).rejects.toThrow("voice messages require audio");
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws when asVoice is true but audio is not mp3 or caf", async () => {
|
||||
await expect(
|
||||
sendBlueBubblesAttachment({
|
||||
to: "chat_guid:iMessage;-;+15551234567",
|
||||
buffer: new Uint8Array([1, 2, 3]),
|
||||
filename: "voice.wav",
|
||||
contentType: "audio/wav",
|
||||
asVoice: true,
|
||||
opts: { serverUrl: "http://localhost:1234", password: "test" },
|
||||
}),
|
||||
).rejects.toThrow("require mp3 or caf");
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sanitizes filenames before sending", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
text: () => Promise.resolve(JSON.stringify({ messageId: "msg-3" })),
|
||||
});
|
||||
|
||||
await sendBlueBubblesAttachment({
|
||||
to: "chat_guid:iMessage;-;+15551234567",
|
||||
buffer: new Uint8Array([1, 2, 3]),
|
||||
filename: "../evil.mp3",
|
||||
contentType: "audio/mpeg",
|
||||
opts: { serverUrl: "http://localhost:1234", password: "test" },
|
||||
});
|
||||
|
||||
const body = mockFetch.mock.calls[0][1]?.body as Uint8Array;
|
||||
const bodyText = decodeBody(body);
|
||||
expect(bodyText).toContain('filename="evil.mp3"');
|
||||
expect(bodyText).toContain('name="evil.mp3"');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
import { resolveBlueBubblesAccount } from "./accounts.js";
|
||||
import { resolveChatGuidForTarget } from "./send.js";
|
||||
@@ -20,30 +19,6 @@ export type BlueBubblesAttachmentOpts = {
|
||||
};
|
||||
|
||||
const DEFAULT_ATTACHMENT_MAX_BYTES = 8 * 1024 * 1024;
|
||||
const AUDIO_MIME_MP3 = new Set(["audio/mpeg", "audio/mp3"]);
|
||||
const AUDIO_MIME_CAF = new Set(["audio/x-caf", "audio/caf"]);
|
||||
|
||||
function sanitizeFilename(input: string | undefined, fallback: string): string {
|
||||
const trimmed = input?.trim() ?? "";
|
||||
const base = trimmed ? path.basename(trimmed) : "";
|
||||
return base || fallback;
|
||||
}
|
||||
|
||||
function ensureExtension(filename: string, extension: string, fallbackBase: string): string {
|
||||
const currentExt = path.extname(filename);
|
||||
if (currentExt.toLowerCase() === extension) return filename;
|
||||
const base = currentExt ? filename.slice(0, -currentExt.length) : filename;
|
||||
return `${base || fallbackBase}${extension}`;
|
||||
}
|
||||
|
||||
function resolveVoiceInfo(filename: string, contentType?: string) {
|
||||
const normalizedType = contentType?.trim().toLowerCase();
|
||||
const extension = path.extname(filename).toLowerCase();
|
||||
const isMp3 = extension === ".mp3" || (normalizedType ? AUDIO_MIME_MP3.has(normalizedType) : false);
|
||||
const isCaf = extension === ".caf" || (normalizedType ? AUDIO_MIME_CAF.has(normalizedType) : false);
|
||||
const isAudio = isMp3 || isCaf || Boolean(normalizedType?.startsWith("audio/"));
|
||||
return { isAudio, isMp3, isCaf };
|
||||
}
|
||||
|
||||
function resolveAccount(params: BlueBubblesAttachmentOpts) {
|
||||
const account = resolveBlueBubblesAccount({
|
||||
@@ -129,7 +104,6 @@ function extractMessageId(payload: unknown): string {
|
||||
/**
|
||||
* Send an attachment via BlueBubbles API.
|
||||
* Supports sending media files (images, videos, audio, documents) to a chat.
|
||||
* When asVoice is true, expects MP3/CAF audio and marks it as an iMessage voice memo.
|
||||
*/
|
||||
export async function sendBlueBubblesAttachment(params: {
|
||||
to: string;
|
||||
@@ -139,37 +113,12 @@ export async function sendBlueBubblesAttachment(params: {
|
||||
caption?: string;
|
||||
replyToMessageGuid?: string;
|
||||
replyToPartIndex?: number;
|
||||
asVoice?: boolean;
|
||||
opts?: BlueBubblesAttachmentOpts;
|
||||
}): Promise<SendBlueBubblesAttachmentResult> {
|
||||
const { to, caption, replyToMessageGuid, replyToPartIndex, asVoice, opts = {} } = params;
|
||||
let { buffer, filename, contentType } = params;
|
||||
const wantsVoice = asVoice === true;
|
||||
const fallbackName = wantsVoice ? "Audio Message" : "attachment";
|
||||
filename = sanitizeFilename(filename, fallbackName);
|
||||
contentType = contentType?.trim() || undefined;
|
||||
const { to, buffer, filename, contentType, caption, replyToMessageGuid, replyToPartIndex, opts = {} } =
|
||||
params;
|
||||
const { baseUrl, password } = resolveAccount(opts);
|
||||
|
||||
// Validate voice memo format when requested (BlueBubbles converts MP3 -> CAF when isAudioMessage).
|
||||
const isAudioMessage = wantsVoice;
|
||||
if (isAudioMessage) {
|
||||
const voiceInfo = resolveVoiceInfo(filename, contentType);
|
||||
if (!voiceInfo.isAudio) {
|
||||
throw new Error("BlueBubbles voice messages require audio media (mp3 or caf).");
|
||||
}
|
||||
if (voiceInfo.isMp3) {
|
||||
filename = ensureExtension(filename, ".mp3", fallbackName);
|
||||
contentType = contentType ?? "audio/mpeg";
|
||||
} else if (voiceInfo.isCaf) {
|
||||
filename = ensureExtension(filename, ".caf", fallbackName);
|
||||
contentType = contentType ?? "audio/x-caf";
|
||||
} else {
|
||||
throw new Error(
|
||||
"BlueBubbles voice messages require mp3 or caf audio (convert before sending).",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const target = resolveSendTarget(to);
|
||||
const chatGuid = await resolveChatGuidForTarget({
|
||||
baseUrl,
|
||||
@@ -221,11 +170,6 @@ export async function sendBlueBubblesAttachment(params: {
|
||||
addField("tempGuid", `temp-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`);
|
||||
addField("method", "private-api");
|
||||
|
||||
// Add isAudioMessage flag for voice memos
|
||||
if (isAudioMessage) {
|
||||
addField("isAudioMessage", "true");
|
||||
}
|
||||
|
||||
const trimmedReplyTo = replyToMessageGuid?.trim();
|
||||
if (trimmedReplyTo) {
|
||||
addField("selectedMessageGuid", trimmedReplyTo);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -26,7 +25,6 @@ const bluebubblesGroupConfigSchema = z.object({
|
||||
const bluebubblesAccountSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
serverUrl: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
webhookPath: z.string().optional(),
|
||||
|
||||
@@ -59,7 +59,6 @@ export async function sendBlueBubblesMedia(params: {
|
||||
caption?: string;
|
||||
replyToId?: string | null;
|
||||
accountId?: string;
|
||||
asVoice?: boolean;
|
||||
}) {
|
||||
const {
|
||||
cfg,
|
||||
@@ -72,7 +71,6 @@ export async function sendBlueBubblesMedia(params: {
|
||||
caption,
|
||||
replyToId,
|
||||
accountId,
|
||||
asVoice,
|
||||
} = params;
|
||||
const core = getBlueBubblesRuntime();
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
@@ -148,7 +146,6 @@ export async function sendBlueBubblesMedia(params: {
|
||||
filename: resolvedFilename ?? "attachment",
|
||||
contentType: resolvedContentType ?? undefined,
|
||||
replyToMessageGuid,
|
||||
asVoice,
|
||||
opts: {
|
||||
cfg,
|
||||
accountId,
|
||||
|
||||
@@ -99,8 +99,6 @@ function createMockRuntime(): PluginRuntime {
|
||||
chunkText: vi.fn() as unknown as PluginRuntime["channel"]["text"]["chunkText"],
|
||||
resolveTextChunkLimit: vi.fn(() => 4000) as unknown as PluginRuntime["channel"]["text"]["resolveTextChunkLimit"],
|
||||
hasControlCommand: mockHasControlCommand as unknown as PluginRuntime["channel"]["text"]["hasControlCommand"],
|
||||
resolveMarkdownTableMode: vi.fn(() => "code") as unknown as PluginRuntime["channel"]["text"]["resolveMarkdownTableMode"],
|
||||
convertMarkdownTables: vi.fn((text: string) => text) as unknown as PluginRuntime["channel"]["text"]["convertMarkdownTables"],
|
||||
},
|
||||
reply: {
|
||||
dispatchReplyWithBufferedBlockDispatcher: mockDispatchReplyWithBufferedBlockDispatcher as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
|
||||
@@ -222,12 +220,6 @@ function createMockResponse(): ServerResponse & { body: string; statusCode: numb
|
||||
return res;
|
||||
}
|
||||
|
||||
const flushAsync = async () => {
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
}
|
||||
};
|
||||
|
||||
describe("BlueBubbles webhook monitor", () => {
|
||||
let unregister: () => void;
|
||||
|
||||
@@ -514,7 +506,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(resolveChatGuidForTarget).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -562,7 +554,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(resolveChatGuidForTarget).not.toHaveBeenCalled();
|
||||
expect(sendMessageBlueBubbles).toHaveBeenCalledWith(
|
||||
@@ -609,7 +601,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
|
||||
// Wait for async processing
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
@@ -648,7 +640,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
@@ -689,7 +681,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockUpsertPairingRequest).toHaveBeenCalled();
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
@@ -732,7 +724,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockUpsertPairingRequest).toHaveBeenCalled();
|
||||
// Should not send pairing reply since created=false
|
||||
@@ -773,7 +765,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
});
|
||||
@@ -810,7 +802,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -850,7 +842,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
});
|
||||
@@ -888,7 +880,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -927,7 +919,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -966,7 +958,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
});
|
||||
@@ -1007,7 +999,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
const callArgs = mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[0][0];
|
||||
@@ -1048,7 +1040,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1086,7 +1078,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
});
|
||||
@@ -1129,7 +1121,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
const callArgs = mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[0][0];
|
||||
@@ -1175,7 +1167,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
const callArgs = mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[0][0];
|
||||
@@ -1221,7 +1213,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const originalRes = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(originalReq, originalRes);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Only assert the reply message behavior below.
|
||||
mockDispatchReplyWithBufferedBlockDispatcher.mockClear();
|
||||
@@ -1245,7 +1237,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const replyRes = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(replyReq, replyRes);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
const callArgs = mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[0][0];
|
||||
@@ -1291,7 +1283,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
const callArgs = mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[0][0];
|
||||
@@ -1339,7 +1331,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(sendBlueBubblesReaction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -1392,7 +1384,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Should process even without mention because it's an authorized control command
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
@@ -1435,7 +1427,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1478,7 +1470,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(markBlueBubblesChatRead).toHaveBeenCalled();
|
||||
});
|
||||
@@ -1519,7 +1511,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(markBlueBubblesChatRead).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1562,7 +1554,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Should call typing start when reply flow triggers it.
|
||||
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
|
||||
@@ -1612,7 +1604,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
@@ -1657,7 +1649,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
@@ -1705,7 +1697,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Outbound message ID uses short ID "2" (inbound msg-1 is "1", outbound msg-123 is "2")
|
||||
expect(mockEnqueueSystemEvent).toHaveBeenCalledWith(
|
||||
@@ -1750,7 +1742,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockEnqueueSystemEvent).toHaveBeenCalledWith(
|
||||
expect.stringContaining("reaction added"),
|
||||
@@ -1790,7 +1782,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockEnqueueSystemEvent).toHaveBeenCalledWith(
|
||||
expect.stringContaining("reaction removed"),
|
||||
@@ -1830,7 +1822,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockEnqueueSystemEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1868,7 +1860,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockEnqueueSystemEvent).toHaveBeenCalledWith(
|
||||
expect.stringContaining("👍"),
|
||||
@@ -1909,7 +1901,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalled();
|
||||
const callArgs = mockDispatchReplyWithBufferedBlockDispatcher.mock.calls[0][0];
|
||||
@@ -1949,7 +1941,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// The short ID "1" should resolve back to the full UUID
|
||||
expect(resolveBlueBubblesMessageId("1")).toBe("msg-uuid-12345");
|
||||
@@ -2001,7 +1993,7 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
const res = createMockResponse();
|
||||
|
||||
await handleBlueBubblesWebhookRequest(req, res);
|
||||
await flushAsync();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockDispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1662,15 +1662,9 @@ async function processMessage(
|
||||
? [payload.mediaUrl]
|
||||
: [];
|
||||
if (mediaList.length > 0) {
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: config,
|
||||
channel: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
let first = true;
|
||||
for (const mediaUrl of mediaList) {
|
||||
const caption = first ? text : undefined;
|
||||
const caption = first ? payload.text : undefined;
|
||||
first = false;
|
||||
const result = await sendBlueBubblesMedia({
|
||||
cfg: config,
|
||||
@@ -1692,14 +1686,8 @@ async function processMessage(
|
||||
account.config.textChunkLimit && account.config.textChunkLimit > 0
|
||||
? account.config.textChunkLimit
|
||||
: DEFAULT_TEXT_LIMIT;
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: config,
|
||||
channel: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
const chunks = core.channel.text.chunkMarkdownText(text, textLimit);
|
||||
if (!chunks.length && text) chunks.push(text);
|
||||
const chunks = core.channel.text.chunkMarkdownText(payload.text ?? "", textLimit);
|
||||
if (!chunks.length && payload.text) chunks.push(payload.text);
|
||||
if (!chunks.length) return;
|
||||
for (const chunk of chunks) {
|
||||
const result = await sendMessageBlueBubbles(outboundTarget, chunk, {
|
||||
|
||||
@@ -159,7 +159,6 @@ export function createLobsterTool(api: ClawdbotPluginApi) {
|
||||
// NOTE: Prefer string enums in tool schemas; some providers reject unions/anyOf.
|
||||
action: Type.Unsafe<"run" | "resume">({ type: "string", enum: ["run", "resume"] }),
|
||||
pipeline: Type.Optional(Type.String()),
|
||||
argsJson: Type.Optional(Type.String()),
|
||||
token: Type.Optional(Type.String()),
|
||||
approve: Type.Optional(Type.Boolean()),
|
||||
lobsterPath: Type.Optional(Type.String()),
|
||||
@@ -182,12 +181,7 @@ export function createLobsterTool(api: ClawdbotPluginApi) {
|
||||
if (action === "run") {
|
||||
const pipeline = typeof params.pipeline === "string" ? params.pipeline : "";
|
||||
if (!pipeline.trim()) throw new Error("pipeline required");
|
||||
const argv = ["run", "--mode", "tool", pipeline];
|
||||
const argsJson = typeof params.argsJson === "string" ? params.argsJson : "";
|
||||
if (argsJson.trim()) {
|
||||
argv.push("--args-json", argsJson);
|
||||
}
|
||||
return argv;
|
||||
return ["run", "--mode", "tool", pipeline];
|
||||
}
|
||||
if (action === "resume") {
|
||||
const token = typeof params.token === "string" ? params.token : "";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
@@ -36,7 +35,6 @@ const matrixRoomSchema = z
|
||||
export const MatrixConfigSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
homeserver: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
accessToken: z.string().optional(),
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
saveMatrixCredentials,
|
||||
credentialsMatchConfig,
|
||||
touchMatrixCredentials,
|
||||
} = await import("../credentials.js");
|
||||
} = await import("./credentials.js");
|
||||
|
||||
const cached = loadMatrixCredentials(env);
|
||||
const cachedCredentials =
|
||||
|
||||
@@ -548,11 +548,6 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
}
|
||||
|
||||
let didSendReply = false;
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "matrix",
|
||||
accountId: route.accountId,
|
||||
});
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
core.channel.reply.createReplyDispatcherWithTyping({
|
||||
responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId)
|
||||
@@ -567,8 +562,6 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
textLimit,
|
||||
replyToMode,
|
||||
threadId: threadTarget,
|
||||
accountId: route.accountId,
|
||||
tableMode,
|
||||
});
|
||||
didSendReply = true;
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
|
||||
import type { MarkdownTableMode, ReplyPayload, RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import type { ReplyPayload, RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import { sendMessageMatrix } from "../send.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
|
||||
@@ -12,17 +12,8 @@ export async function deliverMatrixReplies(params: {
|
||||
textLimit: number;
|
||||
replyToMode: "off" | "first" | "all";
|
||||
threadId?: string;
|
||||
accountId?: string;
|
||||
tableMode?: MarkdownTableMode;
|
||||
}): Promise<void> {
|
||||
const core = getMatrixRuntime();
|
||||
const tableMode =
|
||||
params.tableMode ??
|
||||
core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: core.config.loadConfig(),
|
||||
channel: "matrix",
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const logVerbose = (message: string) => {
|
||||
if (core.logging.shouldLogVerbose()) {
|
||||
params.runtime.log?.(message);
|
||||
@@ -42,8 +33,6 @@ export async function deliverMatrixReplies(params: {
|
||||
}
|
||||
const replyToIdRaw = reply.replyToId?.trim();
|
||||
const replyToId = params.threadId || params.replyToMode === "off" ? undefined : replyToIdRaw;
|
||||
const rawText = reply.text ?? "";
|
||||
const text = core.channel.text.convertMarkdownTables(rawText, tableMode);
|
||||
const mediaList = reply.mediaUrls?.length
|
||||
? reply.mediaUrls
|
||||
: reply.mediaUrl
|
||||
@@ -54,14 +43,13 @@ export async function deliverMatrixReplies(params: {
|
||||
Boolean(id) && (params.replyToMode === "all" || !hasReplied);
|
||||
|
||||
if (mediaList.length === 0) {
|
||||
for (const chunk of core.channel.text.chunkMarkdownText(text, chunkLimit)) {
|
||||
for (const chunk of core.channel.text.chunkMarkdownText(reply.text ?? "", chunkLimit)) {
|
||||
const trimmed = chunk.trim();
|
||||
if (!trimmed) continue;
|
||||
await sendMessageMatrix(params.roomId, trimmed, {
|
||||
client: params.client,
|
||||
replyToId: shouldIncludeReply(replyToId) ? replyToId : undefined,
|
||||
threadId: params.threadId,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
if (shouldIncludeReply(replyToId)) {
|
||||
hasReplied = true;
|
||||
@@ -72,14 +60,13 @@ export async function deliverMatrixReplies(params: {
|
||||
|
||||
let first = true;
|
||||
for (const mediaUrl of mediaList) {
|
||||
const caption = first ? text : "";
|
||||
const caption = first ? (reply.text ?? "") : "";
|
||||
await sendMessageMatrix(params.roomId, caption, {
|
||||
client: params.client,
|
||||
mediaUrl,
|
||||
replyToId: shouldIncludeReply(replyToId) ? replyToId : undefined,
|
||||
threadId: params.threadId,
|
||||
audioAsVoice: reply.audioAsVoice,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
if (shouldIncludeReply(replyToId)) {
|
||||
hasReplied = true;
|
||||
|
||||
@@ -43,8 +43,6 @@ const runtimeStub = {
|
||||
text: {
|
||||
resolveTextChunkLimit: () => 4000,
|
||||
chunkMarkdownText: (text: string) => (text ? [text] : []),
|
||||
resolveMarkdownTableMode: () => "code",
|
||||
convertMarkdownTables: (text: string) => text,
|
||||
},
|
||||
},
|
||||
} as unknown as PluginRuntime;
|
||||
|
||||
@@ -50,18 +50,9 @@ export async function sendMessageMatrix(
|
||||
try {
|
||||
const roomId = await resolveMatrixRoomId(client, to);
|
||||
const cfg = getCore().config.loadConfig();
|
||||
const tableMode = getCore().channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "matrix",
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const convertedMessage = getCore().channel.text.convertMarkdownTables(
|
||||
trimmedMessage,
|
||||
tableMode,
|
||||
);
|
||||
const textLimit = getCore().channel.text.resolveTextChunkLimit(cfg, "matrix");
|
||||
const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT);
|
||||
const chunks = getCore().channel.text.chunkMarkdownText(convertedMessage, chunkLimit);
|
||||
const chunks = getCore().channel.text.chunkMarkdownText(trimmedMessage, chunkLimit);
|
||||
const threadId = normalizeThreadId(opts.threadId);
|
||||
const relation = threadId
|
||||
? buildThreadRelation(threadId, opts.replyToId)
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { MatrixClient } from "matrix-bot-sdk";
|
||||
import { EventType } from "./types.js";
|
||||
|
||||
let resolveMatrixRoomId: typeof import("./targets.js").resolveMatrixRoomId;
|
||||
let normalizeThreadId: typeof import("./targets.js").normalizeThreadId;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
({ resolveMatrixRoomId, normalizeThreadId } = await import("./targets.js"));
|
||||
});
|
||||
|
||||
describe("resolveMatrixRoomId", () => {
|
||||
it("uses m.direct when available", async () => {
|
||||
const userId = "@user:example.org";
|
||||
const client = {
|
||||
getAccountData: vi.fn().mockResolvedValue({
|
||||
[userId]: ["!room:example.org"],
|
||||
}),
|
||||
getJoinedRooms: vi.fn(),
|
||||
getJoinedRoomMembers: vi.fn(),
|
||||
setAccountData: vi.fn(),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
const roomId = await resolveMatrixRoomId(client, userId);
|
||||
|
||||
expect(roomId).toBe("!room:example.org");
|
||||
expect(client.getJoinedRooms).not.toHaveBeenCalled();
|
||||
expect(client.setAccountData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to joined rooms and persists m.direct", async () => {
|
||||
const userId = "@fallback:example.org";
|
||||
const roomId = "!room:example.org";
|
||||
const setAccountData = vi.fn().mockResolvedValue(undefined);
|
||||
const client = {
|
||||
getAccountData: vi.fn().mockRejectedValue(new Error("nope")),
|
||||
getJoinedRooms: vi.fn().mockResolvedValue([roomId]),
|
||||
getJoinedRoomMembers: vi.fn().mockResolvedValue([
|
||||
"@bot:example.org",
|
||||
userId,
|
||||
]),
|
||||
setAccountData,
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
const resolved = await resolveMatrixRoomId(client, userId);
|
||||
|
||||
expect(resolved).toBe(roomId);
|
||||
expect(setAccountData).toHaveBeenCalledWith(
|
||||
EventType.Direct,
|
||||
expect.objectContaining({ [userId]: [roomId] }),
|
||||
);
|
||||
});
|
||||
|
||||
it("continues when a room member lookup fails", async () => {
|
||||
const userId = "@continue:example.org";
|
||||
const roomId = "!good:example.org";
|
||||
const setAccountData = vi.fn().mockResolvedValue(undefined);
|
||||
const getJoinedRoomMembers = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("boom"))
|
||||
.mockResolvedValueOnce(["@bot:example.org", userId]);
|
||||
const client = {
|
||||
getAccountData: vi.fn().mockRejectedValue(new Error("nope")),
|
||||
getJoinedRooms: vi.fn().mockResolvedValue(["!bad:example.org", roomId]),
|
||||
getJoinedRoomMembers,
|
||||
setAccountData,
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
const resolved = await resolveMatrixRoomId(client, userId);
|
||||
|
||||
expect(resolved).toBe(roomId);
|
||||
expect(setAccountData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows larger rooms when no 1:1 match exists", async () => {
|
||||
const userId = "@group:example.org";
|
||||
const roomId = "!group:example.org";
|
||||
const client = {
|
||||
getAccountData: vi.fn().mockRejectedValue(new Error("nope")),
|
||||
getJoinedRooms: vi.fn().mockResolvedValue([roomId]),
|
||||
getJoinedRoomMembers: vi.fn().mockResolvedValue([
|
||||
"@bot:example.org",
|
||||
userId,
|
||||
"@extra:example.org",
|
||||
]),
|
||||
setAccountData: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
const resolved = await resolveMatrixRoomId(client, userId);
|
||||
|
||||
expect(resolved).toBe(roomId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeThreadId", () => {
|
||||
it("returns null for empty thread ids", () => {
|
||||
expect(normalizeThreadId(" ")).toBeNull();
|
||||
expect(normalizeThreadId("$thread")).toBe("$thread");
|
||||
});
|
||||
});
|
||||
@@ -16,100 +16,22 @@ export function normalizeThreadId(raw?: string | number | null): string | null {
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
const directRoomCache = new Map<string, string>();
|
||||
|
||||
async function persistDirectRoom(
|
||||
client: MatrixClient,
|
||||
userId: string,
|
||||
roomId: string,
|
||||
): Promise<void> {
|
||||
let directContent: MatrixDirectAccountData | null = null;
|
||||
try {
|
||||
directContent = (await client.getAccountData(
|
||||
EventType.Direct,
|
||||
)) as MatrixDirectAccountData | null;
|
||||
} catch {
|
||||
// Ignore fetch errors and fall back to an empty map.
|
||||
}
|
||||
const existing =
|
||||
directContent && !Array.isArray(directContent) ? directContent : {};
|
||||
const current = Array.isArray(existing[userId]) ? existing[userId] : [];
|
||||
if (current[0] === roomId) return;
|
||||
const next = [roomId, ...current.filter((id) => id !== roomId)];
|
||||
try {
|
||||
await client.setAccountData(EventType.Direct, {
|
||||
...existing,
|
||||
[userId]: next,
|
||||
});
|
||||
} catch {
|
||||
// Ignore persistence errors.
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveDirectRoomId(
|
||||
client: MatrixClient,
|
||||
userId: string,
|
||||
): Promise<string> {
|
||||
async function resolveDirectRoomId(client: MatrixClient, userId: string): Promise<string> {
|
||||
const trimmed = userId.trim();
|
||||
if (!trimmed.startsWith("@")) {
|
||||
throw new Error(
|
||||
`Matrix user IDs must be fully qualified (got "${trimmed}")`,
|
||||
);
|
||||
throw new Error(`Matrix user IDs must be fully qualified (got "${trimmed}")`);
|
||||
}
|
||||
|
||||
const cached = directRoomCache.get(trimmed);
|
||||
if (cached) return cached;
|
||||
|
||||
// 1) Fast path: use account data (m.direct) for *this* logged-in user (the bot).
|
||||
// matrix-bot-sdk: use getAccountData to retrieve m.direct
|
||||
try {
|
||||
const directContent = (await client.getAccountData(
|
||||
EventType.Direct,
|
||||
)) as MatrixDirectAccountData | null;
|
||||
const list = Array.isArray(directContent?.[trimmed])
|
||||
? directContent[trimmed]
|
||||
: [];
|
||||
if (list.length > 0) {
|
||||
directRoomCache.set(trimmed, list[0]);
|
||||
return list[0];
|
||||
}
|
||||
const directContent = await client.getAccountData(EventType.Direct) as MatrixDirectAccountData | null;
|
||||
const list = Array.isArray(directContent?.[trimmed]) ? directContent[trimmed] : [];
|
||||
if (list.length > 0) return list[0];
|
||||
} catch {
|
||||
// Ignore and fall back.
|
||||
// Ignore errors, try fetching from server
|
||||
}
|
||||
|
||||
// 2) Fallback: look for an existing joined room that looks like a 1:1 with the user.
|
||||
// Many clients only maintain m.direct for *their own* account data, so relying on it is brittle.
|
||||
let fallbackRoom: string | null = null;
|
||||
try {
|
||||
const rooms = await client.getJoinedRooms();
|
||||
for (const roomId of rooms) {
|
||||
let members: string[];
|
||||
try {
|
||||
members = await client.getJoinedRoomMembers(roomId);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!members.includes(trimmed)) continue;
|
||||
// Prefer classic 1:1 rooms, but allow larger rooms if requested.
|
||||
if (members.length === 2) {
|
||||
directRoomCache.set(trimmed, roomId);
|
||||
await persistDirectRoom(client, trimmed, roomId);
|
||||
return roomId;
|
||||
}
|
||||
if (!fallbackRoom) {
|
||||
fallbackRoom = roomId;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore and fall back.
|
||||
}
|
||||
|
||||
if (fallbackRoom) {
|
||||
directRoomCache.set(trimmed, fallbackRoom);
|
||||
await persistDirectRoom(client, trimmed, fallbackRoom);
|
||||
return fallbackRoom;
|
||||
}
|
||||
|
||||
throw new Error(`No direct room found for ${trimmed} (m.direct missing)`);
|
||||
throw new Error(
|
||||
`No m.direct room found for ${trimmed}. Open a DM first so Matrix can set m.direct.`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function resolveMatrixRoomId(
|
||||
|
||||
@@ -87,7 +87,6 @@ export type MatrixSendResult = {
|
||||
export type MatrixSendOpts = {
|
||||
client?: import("matrix-bot-sdk").MatrixClient;
|
||||
mediaUrl?: string;
|
||||
accountId?: string;
|
||||
replyToId?: string;
|
||||
threadId?: string | number | null;
|
||||
timeoutMs?: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@clawdbot/mattermost",
|
||||
"version": "2026.1.22",
|
||||
"version": "2026.1.20-2",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Mattermost channel plugin",
|
||||
"clawdbot": {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
BlockStreamingCoalesceSchema,
|
||||
DmPolicySchema,
|
||||
GroupPolicySchema,
|
||||
MarkdownConfigSchema,
|
||||
requireOpenAllowFrom,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
|
||||
@@ -12,7 +11,6 @@ const MattermostAccountSchemaBase = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
enabled: z.boolean().optional(),
|
||||
configWrites: z.boolean().optional(),
|
||||
botToken: z.string().optional(),
|
||||
|
||||
@@ -707,11 +707,6 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, {
|
||||
fallbackLimit: account.textChunkLimit ?? 4000,
|
||||
});
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "mattermost",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
|
||||
let prefixContext: ResponsePrefixContext = {
|
||||
identityName: resolveIdentityName(cfg, route.agentId),
|
||||
@@ -725,7 +720,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
||||
deliver: async (payload: ReplyPayload) => {
|
||||
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
const text = core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode);
|
||||
const text = payload.text ?? "";
|
||||
if (mediaUrls.length === 0) {
|
||||
const chunks = core.channel.text.chunkMarkdownText(text, textLimit);
|
||||
for (const chunk of chunks.length > 0 ? chunks : [text]) {
|
||||
|
||||
@@ -181,15 +181,6 @@ export async function sendMessageMattermost(
|
||||
}
|
||||
}
|
||||
|
||||
if (message) {
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "mattermost",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
message = core.channel.text.convertMarkdownTables(message, tableMode);
|
||||
}
|
||||
|
||||
if (!message && (!fileIds || fileIds.length === 0)) {
|
||||
if (uploadError) {
|
||||
throw new Error(`Mattermost media upload failed: ${uploadError.message}`);
|
||||
@@ -214,4 +205,4 @@ export async function sendMessageMattermost(
|
||||
messageId: post.id ?? "unknown",
|
||||
channelId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,11 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "test-key";
|
||||
const HAS_OPENAI_KEY = Boolean(process.env.OPENAI_API_KEY);
|
||||
const liveEnabled = HAS_OPENAI_KEY && process.env.CLAWDBOT_LIVE_TEST === "1";
|
||||
const describeLive = liveEnabled ? describe : describe.skip;
|
||||
// Skip if no OpenAI API key
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
const describeWithKey = OPENAI_API_KEY ? describe : describe.skip;
|
||||
|
||||
describe("memory plugin e2e", () => {
|
||||
describeWithKey("memory plugin e2e", () => {
|
||||
let tmpDir: string;
|
||||
let dbPath: string;
|
||||
|
||||
@@ -160,7 +159,7 @@ describe("memory plugin e2e", () => {
|
||||
});
|
||||
|
||||
// Live tests that require OpenAI API key and actually use LanceDB
|
||||
describeLive("memory plugin live tests", () => {
|
||||
describeWithKey("memory plugin live tests", () => {
|
||||
let tmpDir: string;
|
||||
let dbPath: string;
|
||||
|
||||
@@ -177,7 +176,6 @@ describeLive("memory plugin live tests", () => {
|
||||
|
||||
test("memory tools work end-to-end", async () => {
|
||||
const { default: memoryPlugin } = await import("./index.js");
|
||||
const liveApiKey = process.env.OPENAI_API_KEY ?? "";
|
||||
|
||||
// Mock plugin API
|
||||
const registeredTools: any[] = [];
|
||||
@@ -193,7 +191,7 @@ describeLive("memory plugin live tests", () => {
|
||||
config: {},
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: liveApiKey,
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath,
|
||||
|
||||
@@ -21,8 +21,6 @@ const runtimeStub = {
|
||||
}
|
||||
return chunks;
|
||||
},
|
||||
resolveMarkdownTableMode: () => "code",
|
||||
convertMarkdownTables: (text: string) => text,
|
||||
},
|
||||
},
|
||||
} as unknown as PluginRuntime;
|
||||
@@ -36,7 +34,6 @@ describe("msteams messenger", () => {
|
||||
it("filters silent replies", () => {
|
||||
const messages = renderReplyPayloadsToMessages([{ text: SILENT_REPLY_TOKEN }], {
|
||||
textChunkLimit: 4000,
|
||||
tableMode: "code",
|
||||
});
|
||||
expect(messages).toEqual([]);
|
||||
});
|
||||
@@ -44,7 +41,7 @@ describe("msteams messenger", () => {
|
||||
it("filters silent reply prefixes", () => {
|
||||
const messages = renderReplyPayloadsToMessages(
|
||||
[{ text: `${SILENT_REPLY_TOKEN} -- ignored` }],
|
||||
{ textChunkLimit: 4000, tableMode: "code" },
|
||||
{ textChunkLimit: 4000 },
|
||||
);
|
||||
expect(messages).toEqual([]);
|
||||
});
|
||||
@@ -52,7 +49,7 @@ describe("msteams messenger", () => {
|
||||
it("splits media into separate messages by default", () => {
|
||||
const messages = renderReplyPayloadsToMessages(
|
||||
[{ text: "hi", mediaUrl: "https://example.com/a.png" }],
|
||||
{ textChunkLimit: 4000, tableMode: "code" },
|
||||
{ textChunkLimit: 4000 },
|
||||
);
|
||||
expect(messages).toEqual([{ text: "hi" }, { mediaUrl: "https://example.com/a.png" }]);
|
||||
});
|
||||
@@ -60,7 +57,7 @@ describe("msteams messenger", () => {
|
||||
it("supports inline media mode", () => {
|
||||
const messages = renderReplyPayloadsToMessages(
|
||||
[{ text: "hi", mediaUrl: "https://example.com/a.png" }],
|
||||
{ textChunkLimit: 4000, mediaMode: "inline", tableMode: "code" },
|
||||
{ textChunkLimit: 4000, mediaMode: "inline" },
|
||||
);
|
||||
expect(messages).toEqual([{ text: "hi", mediaUrl: "https://example.com/a.png" }]);
|
||||
});
|
||||
@@ -69,7 +66,6 @@ describe("msteams messenger", () => {
|
||||
const long = "hello ".repeat(200);
|
||||
const messages = renderReplyPayloadsToMessages([{ text: long }], {
|
||||
textChunkLimit: 50,
|
||||
tableMode: "code",
|
||||
});
|
||||
expect(messages.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
isSilentReplyText,
|
||||
loadWebMedia,
|
||||
type MarkdownTableMode,
|
||||
type MSTeamsReplyStyle,
|
||||
type ReplyPayload,
|
||||
SILENT_REPLY_TOKEN,
|
||||
@@ -62,7 +61,6 @@ export type MSTeamsReplyRenderOptions = {
|
||||
textChunkLimit: number;
|
||||
chunkText?: boolean;
|
||||
mediaMode?: "split" | "inline";
|
||||
tableMode?: MarkdownTableMode;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -198,19 +196,10 @@ export function renderReplyPayloadsToMessages(
|
||||
const chunkLimit = Math.min(options.textChunkLimit, 4000);
|
||||
const chunkText = options.chunkText !== false;
|
||||
const mediaMode = options.mediaMode ?? "split";
|
||||
const tableMode =
|
||||
options.tableMode ??
|
||||
getMSTeamsRuntime().channel.text.resolveMarkdownTableMode({
|
||||
cfg: getMSTeamsRuntime().config.loadConfig(),
|
||||
channel: "msteams",
|
||||
});
|
||||
|
||||
for (const payload of replies) {
|
||||
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
const text = getMSTeamsRuntime().channel.text.convertMarkdownTables(
|
||||
payload.text ?? "",
|
||||
tableMode,
|
||||
);
|
||||
const text = payload.text ?? "";
|
||||
|
||||
if (!text && mediaList.length === 0) continue;
|
||||
|
||||
|
||||
@@ -53,15 +53,10 @@ export function createMSTeamsReplyDispatcher(params: {
|
||||
).responsePrefix,
|
||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
|
||||
deliver: async (payload) => {
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: params.cfg,
|
||||
channel: "msteams",
|
||||
});
|
||||
const messages = renderReplyPayloadsToMessages([payload], {
|
||||
textChunkLimit: params.textLimit,
|
||||
chunkText: true,
|
||||
mediaMode: "split",
|
||||
tableMode,
|
||||
});
|
||||
const mediaMaxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg: params.cfg,
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { extractFilename, extractMessageId } from "./media-helpers.js";
|
||||
import { buildConversationReference, sendMSTeamsMessages } from "./messenger.js";
|
||||
import { buildMSTeamsPollCard } from "./polls.js";
|
||||
import { getMSTeamsRuntime } from "./runtime.js";
|
||||
import { resolveMSTeamsSendContext, type MSTeamsProactiveContext } from "./send-context.js";
|
||||
|
||||
export type SendMSTeamsMessageParams = {
|
||||
@@ -94,21 +93,13 @@ export async function sendMessageMSTeams(
|
||||
params: SendMSTeamsMessageParams,
|
||||
): Promise<SendMSTeamsMessageResult> {
|
||||
const { cfg, to, text, mediaUrl } = params;
|
||||
const tableMode = getMSTeamsRuntime().channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "msteams",
|
||||
});
|
||||
const messageText = getMSTeamsRuntime().channel.text.convertMarkdownTables(
|
||||
text ?? "",
|
||||
tableMode,
|
||||
);
|
||||
const ctx = await resolveMSTeamsSendContext({ cfg, to });
|
||||
const { adapter, appId, conversationId, ref, log, conversationType, tokenProvider, sharePointSiteId } = ctx;
|
||||
|
||||
log.debug("sending proactive message", {
|
||||
conversationId,
|
||||
conversationType,
|
||||
textLength: messageText.length,
|
||||
textLength: text.length,
|
||||
hasMedia: Boolean(mediaUrl),
|
||||
});
|
||||
|
||||
@@ -143,7 +134,7 @@ export async function sendMessageMSTeams(
|
||||
const { activity, uploadId } = prepareFileConsentActivity({
|
||||
media: { buffer: media.buffer, filename: fileName, contentType: media.contentType },
|
||||
conversationId,
|
||||
description: messageText || undefined,
|
||||
description: text || undefined,
|
||||
});
|
||||
|
||||
log.debug("sending file consent card", { uploadId, fileName, size: media.buffer.length });
|
||||
@@ -181,14 +172,14 @@ export async function sendMessageMSTeams(
|
||||
const base64 = media.buffer.toString("base64");
|
||||
const finalMediaUrl = `data:${media.contentType};base64,${base64}`;
|
||||
|
||||
return sendTextWithMedia(ctx, messageText, finalMediaUrl);
|
||||
return sendTextWithMedia(ctx, text, finalMediaUrl);
|
||||
}
|
||||
|
||||
if (isImage && !sharePointSiteId) {
|
||||
// Group chat/channel without SharePoint: send image inline (avoids OneDrive failures)
|
||||
const base64 = media.buffer.toString("base64");
|
||||
const finalMediaUrl = `data:${media.contentType};base64,${base64}`;
|
||||
return sendTextWithMedia(ctx, messageText, finalMediaUrl);
|
||||
return sendTextWithMedia(ctx, text, finalMediaUrl);
|
||||
}
|
||||
|
||||
// Group chat or channel: upload to SharePoint (if siteId configured) or OneDrive
|
||||
@@ -232,7 +223,7 @@ export async function sendMessageMSTeams(
|
||||
const fileCardAttachment = buildTeamsFileInfoCard(driveItem);
|
||||
const activity = {
|
||||
type: "message",
|
||||
text: messageText || undefined,
|
||||
text: text || undefined,
|
||||
attachments: [fileCardAttachment],
|
||||
};
|
||||
|
||||
@@ -273,7 +264,7 @@ export async function sendMessageMSTeams(
|
||||
const fileLink = `📎 [${uploaded.name}](${uploaded.shareUrl})`;
|
||||
const activity = {
|
||||
type: "message",
|
||||
text: messageText ? `${messageText}\n\n${fileLink}` : fileLink,
|
||||
text: text ? `${text}\n\n${fileLink}` : fileLink,
|
||||
};
|
||||
|
||||
const baseRef = buildConversationReference(ref);
|
||||
@@ -299,7 +290,7 @@ export async function sendMessageMSTeams(
|
||||
}
|
||||
|
||||
// No media: send text only
|
||||
return sendTextWithMedia(ctx, messageText, undefined);
|
||||
return sendTextWithMedia(ctx, text, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
DmConfigSchema,
|
||||
DmPolicySchema,
|
||||
GroupPolicySchema,
|
||||
MarkdownConfigSchema,
|
||||
requireOpenAllowFrom,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
@@ -22,7 +21,6 @@ export const NextcloudTalkAccountSchemaBase = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
baseUrl: z.string().optional(),
|
||||
botSecret: z.string().optional(),
|
||||
botSecretFile: z.string().optional(),
|
||||
|
||||
@@ -71,18 +71,8 @@ export async function sendMessageNextcloudTalk(
|
||||
throw new Error("Message must be non-empty for Nextcloud Talk sends");
|
||||
}
|
||||
|
||||
const tableMode = getNextcloudTalkRuntime().channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "nextcloud-talk",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const message = getNextcloudTalkRuntime().channel.text.convertMarkdownTables(
|
||||
text.trim(),
|
||||
tableMode,
|
||||
);
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
message,
|
||||
message: text.trim(),
|
||||
};
|
||||
if (opts.replyTo) {
|
||||
body.replyTo = opts.replyTo;
|
||||
|
||||
@@ -133,20 +133,13 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
||||
deliveryMode: "direct",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ to, text, accountId }) => {
|
||||
const core = getNostrRuntime();
|
||||
const aid = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const bus = activeBuses.get(aid);
|
||||
if (!bus) {
|
||||
throw new Error(`Nostr bus not running for account ${aid}`);
|
||||
}
|
||||
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
||||
cfg: core.config.loadConfig(),
|
||||
channel: "nostr",
|
||||
accountId: aid,
|
||||
});
|
||||
const message = core.channel.text.convertMarkdownTables(text ?? "", tableMode);
|
||||
const normalizedTo = normalizePubkey(to);
|
||||
await bus.sendDm(normalizedTo, message);
|
||||
await bus.sendDm(normalizedTo, text);
|
||||
return { channel: "nostr", to: normalizedTo };
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MarkdownConfigSchema, buildChannelConfigSchema } from "clawdbot/plugin-sdk";
|
||||
import { z } from "zod";
|
||||
import { buildChannelConfigSchema } from "clawdbot/plugin-sdk";
|
||||
|
||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||
|
||||
@@ -63,9 +63,6 @@ export const NostrConfigSchema = z.object({
|
||||
/** Whether this channel is enabled */
|
||||
enabled: z.boolean().optional(),
|
||||
|
||||
/** Markdown formatting overrides (tables). */
|
||||
markdown: MarkdownConfigSchema,
|
||||
|
||||
/** Private key in hex or nsec bech32 format */
|
||||
privateKey: z.string().optional(),
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# OpenProse (plugin)
|
||||
|
||||
Adds the OpenProse skill pack and `/prose` slash command.
|
||||
|
||||
## Enable
|
||||
|
||||
Bundled plugins are disabled by default. Enable this one:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"open-prose": { "enabled": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart the Gateway after enabling.
|
||||
|
||||
## What you get
|
||||
|
||||
- `/prose` slash command (user-invocable skill)
|
||||
- OpenProse VM semantics (`.prose` programs + multi-agent orchestration)
|
||||
- Telemetry support (best-effort, per OpenProse spec)
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id": "open-prose",
|
||||
"name": "OpenProse",
|
||||
"description": "OpenProse VM skill pack with a /prose slash command.",
|
||||
"skills": ["./skills"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { ClawdbotPluginApi } from "../../src/plugins/types.js";
|
||||
|
||||
export default function register(_api: ClawdbotPluginApi) {
|
||||
// OpenProse is delivered via plugin-shipped skills.
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "@clawdbot/open-prose",
|
||||
"version": "2026.1.22",
|
||||
"type": "module",
|
||||
"description": "OpenProse VM skill pack plugin (slash command + telemetry).",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"]
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 OpenProse
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,318 +0,0 @@
|
||||
---
|
||||
name: prose
|
||||
description: OpenProse VM skill pack. Activate on any `prose` command, .prose files, or OpenProse mentions; orchestrates multi-agent workflows.
|
||||
metadata: {"clawdbot":{"emoji":"🪶","homepage":"https://www.prose.md"}}
|
||||
---
|
||||
|
||||
# OpenProse Skill
|
||||
|
||||
OpenProse is a programming language for AI sessions. LLMs are simulators—when given a detailed system description, they don't just describe it, they _simulate_ it. The `prose.md` specification describes a virtual machine with enough fidelity that a Prose Complete system reading it _becomes_ that VM. Simulation with sufficient fidelity is implementation. **You are the Prose Complete system.**
|
||||
|
||||
## Clawdbot Runtime Mapping
|
||||
|
||||
- **Task tool** in the upstream spec == Clawdbot `sessions_spawn`
|
||||
- **File I/O** == Clawdbot `read`/`write`
|
||||
- **Remote fetch** == Clawdbot `web_fetch` (or `exec` with curl when POST is required)
|
||||
|
||||
## When to Activate
|
||||
|
||||
Activate this skill when the user:
|
||||
|
||||
- **Uses ANY `prose` command** (e.g., `prose boot`, `prose run`, `prose compile`, `prose update`, `prose help`, etc.)
|
||||
- Asks to run a `.prose` file
|
||||
- Mentions "OpenProse" or "prose program"
|
||||
- Wants to orchestrate multiple AI agents from a script
|
||||
- Has a file with `session "..."` or `agent name:` syntax
|
||||
- Wants to create a reusable workflow
|
||||
|
||||
## Command Routing
|
||||
|
||||
When a user invokes `prose <command>`, intelligently route based on intent:
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| `prose help` | Load `help.md`, guide user to what they need |
|
||||
| `prose run <file>` | Load VM (`prose.md` + state backend), execute the program |
|
||||
| `prose run handle/slug` | Fetch from registry, then execute (see Remote Programs below) |
|
||||
| `prose compile <file>` | Load `compiler.md`, validate the program |
|
||||
| `prose update` | Run migration (see Migration section below) |
|
||||
| `prose examples` | Show or run example programs from `examples/` |
|
||||
| Other | Intelligently interpret based on context |
|
||||
|
||||
### Important: Single Skill
|
||||
|
||||
There is only ONE skill: `open-prose`. There are NO separate skills like `prose-run`, `prose-compile`, or `prose-boot`. All `prose` commands route through this single skill.
|
||||
|
||||
### Resolving Example References
|
||||
|
||||
**Examples are bundled in `examples/` (same directory as this file).** When users reference examples by name (e.g., "run the gastown example"):
|
||||
|
||||
1. Read `examples/` to list available files
|
||||
2. Match by partial name, keyword, or number
|
||||
3. Run with: `prose run examples/28-gas-town.prose`
|
||||
|
||||
**Common examples by keyword:**
|
||||
| Keyword | File |
|
||||
|---------|------|
|
||||
| hello, hello world | `examples/01-hello-world.prose` |
|
||||
| gas town, gastown | `examples/28-gas-town.prose` |
|
||||
| captain, chair | `examples/29-captains-chair.prose` |
|
||||
| forge, browser | `examples/37-the-forge.prose` |
|
||||
| parallel | `examples/16-parallel-reviews.prose` |
|
||||
| pipeline | `examples/21-pipeline-operations.prose` |
|
||||
| error, retry | `examples/22-error-handling.prose` |
|
||||
|
||||
### Remote Programs
|
||||
|
||||
You can run any `.prose` program from a URL or registry reference:
|
||||
|
||||
```bash
|
||||
# Direct URL — any fetchable URL works
|
||||
prose run https://raw.githubusercontent.com/openprose/prose/main/skills/open-prose/examples/48-habit-miner.prose
|
||||
|
||||
# Registry shorthand — handle/slug resolves to p.prose.md
|
||||
prose run irl-danb/habit-miner
|
||||
prose run alice/code-review
|
||||
```
|
||||
|
||||
**Resolution rules:**
|
||||
|
||||
| Input | Resolution |
|
||||
|-------|------------|
|
||||
| Starts with `http://` or `https://` | Fetch directly from URL |
|
||||
| Contains `/` but no protocol | Resolve to `https://p.prose.md/{path}` |
|
||||
| Otherwise | Treat as local file path |
|
||||
|
||||
**Steps for remote programs:**
|
||||
|
||||
1. Apply resolution rules above
|
||||
2. Fetch the `.prose` content
|
||||
3. Load the VM and execute as normal
|
||||
|
||||
This same resolution applies to `use` statements inside `.prose` files:
|
||||
|
||||
```prose
|
||||
use "https://example.com/my-program.prose" # Direct URL
|
||||
use "alice/research" as research # Registry shorthand
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Locations
|
||||
|
||||
**Do NOT search for OpenProse documentation files.** All skill files are co-located with this SKILL.md file:
|
||||
|
||||
| File | Location | Purpose |
|
||||
| ------------------------- | --------------------------- | ----------------------------------------- |
|
||||
| `prose.md` | Same directory as this file | VM semantics (load to run programs) |
|
||||
| `help.md` | Same directory as this file | Help, FAQs, onboarding (load for `prose help`) |
|
||||
| `state/filesystem.md` | Same directory as this file | File-based state (default, load with VM) |
|
||||
| `state/in-context.md` | Same directory as this file | In-context state (on request) |
|
||||
| `state/sqlite.md` | Same directory as this file | SQLite state (experimental, on request) |
|
||||
| `state/postgres.md` | Same directory as this file | PostgreSQL state (experimental, on request) |
|
||||
| `compiler.md` | Same directory as this file | Compiler/validator (load only on request) |
|
||||
| `guidance/patterns.md` | Same directory as this file | Best practices (load when writing .prose) |
|
||||
| `guidance/antipatterns.md`| Same directory as this file | What to avoid (load when writing .prose) |
|
||||
| `examples/` | Same directory as this file | 37 example programs |
|
||||
|
||||
**User workspace files** (these ARE in the user's project):
|
||||
|
||||
| File/Directory | Location | Purpose |
|
||||
| ---------------- | ------------------------ | ----------------------------------- |
|
||||
| `.prose/.env` | User's working directory | Config (key=value format) |
|
||||
| `.prose/runs/` | User's working directory | Runtime state for file-based mode |
|
||||
| `.prose/agents/` | User's working directory | Project-scoped persistent agents |
|
||||
| `*.prose` files | User's project | User-created programs to execute |
|
||||
|
||||
**User-level files** (in user's home directory, shared across all projects):
|
||||
|
||||
| File/Directory | Location | Purpose |
|
||||
| ----------------- | ---------------- | ---------------------------------------- |
|
||||
| `~/.prose/agents/`| User's home dir | User-scoped persistent agents (cross-project) |
|
||||
|
||||
When you need to read `prose.md` or `compiler.md`, read them from the same directory where you found this SKILL.md file. Never search the user's workspace for these files.
|
||||
|
||||
---
|
||||
|
||||
## Core Documentation
|
||||
|
||||
| File | Purpose | When to Load |
|
||||
| --------------------- | -------------------- | ---------------------------------------------- |
|
||||
| `prose.md` | VM / Interpreter | Always load to run programs |
|
||||
| `state/filesystem.md` | File-based state | Load with VM (default) |
|
||||
| `state/in-context.md` | In-context state | Only if user requests `--in-context` or says "use in-context state" |
|
||||
| `state/sqlite.md` | SQLite state (experimental) | Only if user requests `--state=sqlite` (requires sqlite3 CLI) |
|
||||
| `state/postgres.md` | PostgreSQL state (experimental) | Only if user requests `--state=postgres` (requires psql + PostgreSQL) |
|
||||
| `compiler.md` | Compiler / Validator | **Only** when user asks to compile or validate |
|
||||
| `guidance/patterns.md` | Best practices | Load when **writing** new .prose files |
|
||||
| `guidance/antipatterns.md` | What to avoid | Load when **writing** new .prose files |
|
||||
|
||||
### Authoring Guidance
|
||||
|
||||
When the user asks you to **write or create** a new `.prose` file, load the guidance files:
|
||||
- `guidance/patterns.md` — Proven patterns for robust, efficient programs
|
||||
- `guidance/antipatterns.md` — Common mistakes to avoid
|
||||
|
||||
Do **not** load these when running or compiling—they're for authoring only.
|
||||
|
||||
### State Modes
|
||||
|
||||
OpenProse supports three state management approaches:
|
||||
|
||||
| Mode | When to Use | State Location |
|
||||
|------|-------------|----------------|
|
||||
| **filesystem** (default) | Complex programs, resumption needed, debugging | `.prose/runs/{id}/` files |
|
||||
| **in-context** | Simple programs (<30 statements), no persistence needed | Conversation history |
|
||||
| **sqlite** (experimental) | Queryable state, atomic transactions, flexible schema | `.prose/runs/{id}/state.db` |
|
||||
| **postgres** (experimental) | True concurrent writes, external integrations, team collaboration | PostgreSQL database |
|
||||
|
||||
**Default behavior:** When loading `prose.md`, also load `state/filesystem.md`. This is the recommended mode for most programs.
|
||||
|
||||
**Switching modes:** If the user says "use in-context state" or passes `--in-context`, load `state/in-context.md` instead.
|
||||
|
||||
**Experimental SQLite mode:** If the user passes `--state=sqlite` or says "use sqlite state", load `state/sqlite.md`. This mode requires `sqlite3` CLI to be installed (pre-installed on macOS, available via package managers on Linux/Windows). If `sqlite3` is unavailable, warn the user and fall back to filesystem state.
|
||||
|
||||
**Experimental PostgreSQL mode:** If the user passes `--state=postgres` or says "use postgres state":
|
||||
|
||||
**⚠️ Security Note:** Database credentials in `OPENPROSE_POSTGRES_URL` are passed to subagent sessions and visible in logs. Advise users to use a dedicated database with limited-privilege credentials. See `state/postgres.md` for secure setup guidance.
|
||||
|
||||
1. **Check for connection configuration first:**
|
||||
```bash
|
||||
# Check .prose/.env for OPENPROSE_POSTGRES_URL
|
||||
cat .prose/.env 2>/dev/null | grep OPENPROSE_POSTGRES_URL
|
||||
# Or check environment variable
|
||||
echo $OPENPROSE_POSTGRES_URL
|
||||
```
|
||||
|
||||
2. **If connection string exists, verify connectivity:**
|
||||
```bash
|
||||
psql "$OPENPROSE_POSTGRES_URL" -c "SELECT 1" 2>&1
|
||||
```
|
||||
|
||||
3. **If not configured or connection fails, advise the user:**
|
||||
```
|
||||
⚠️ PostgreSQL state requires a connection URL.
|
||||
|
||||
To configure:
|
||||
1. Set up a PostgreSQL database (Docker, local, or cloud)
|
||||
2. Add connection string to .prose/.env:
|
||||
|
||||
echo "OPENPROSE_POSTGRES_URL=postgresql://user:pass@localhost:5432/prose" >> .prose/.env
|
||||
|
||||
Quick Docker setup:
|
||||
docker run -d --name prose-pg -e POSTGRES_DB=prose -e POSTGRES_HOST_AUTH_METHOD=trust -p 5432:5432 postgres:16
|
||||
echo "OPENPROSE_POSTGRES_URL=postgresql://postgres@localhost:5432/prose" >> .prose/.env
|
||||
|
||||
See state/postgres.md for detailed setup options.
|
||||
```
|
||||
|
||||
4. **Only after successful connection check, load `state/postgres.md`**
|
||||
|
||||
This mode requires both `psql` CLI and a running PostgreSQL server. If either is unavailable, warn and offer fallback to filesystem state.
|
||||
|
||||
**Context warning:** `compiler.md` is large. Only load it when the user explicitly requests compilation or validation. After compiling, recommend `/compact` or a new session before running—don't keep both docs in context.
|
||||
|
||||
## Examples
|
||||
|
||||
The `examples/` directory contains 37 example programs:
|
||||
|
||||
- **01-08**: Basics (hello world, research, code review, debugging)
|
||||
- **09-12**: Agents and skills
|
||||
- **13-15**: Variables and composition
|
||||
- **16-19**: Parallel execution
|
||||
- **20-21**: Loops and pipelines
|
||||
- **22-23**: Error handling
|
||||
- **24-27**: Advanced (choice, conditionals, blocks, interpolation)
|
||||
- **28**: Gas Town (multi-agent orchestration)
|
||||
- **29-31**: Captain's chair pattern (persistent orchestrator)
|
||||
- **33-36**: Production workflows (PR auto-fix, content pipeline, feature factory, bug hunter)
|
||||
- **37**: The Forge (build a browser from scratch)
|
||||
|
||||
Start with `01-hello-world.prose` or try `37-the-forge.prose` to watch AI build a web browser.
|
||||
|
||||
## Execution
|
||||
|
||||
When first invoking the OpenProse VM in a session, display this banner:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ◇ OpenProse VM ◇ │
|
||||
│ A new kind of computer │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
To execute a `.prose` file, you become the OpenProse VM:
|
||||
|
||||
1. **Read `prose.md`** — this document defines how you embody the VM
|
||||
2. **You ARE the VM** — your conversation is its memory, your tools are its instructions
|
||||
3. **Spawn sessions** — each `session` statement triggers a Task tool call
|
||||
4. **Narrate state** — use the narration protocol to track execution ([Position], [Binding], [Success], etc.)
|
||||
5. **Evaluate intelligently** — `**...**` markers require your judgment
|
||||
|
||||
## Help & FAQs
|
||||
|
||||
For syntax reference, FAQs, and getting started guidance, load `help.md`.
|
||||
|
||||
---
|
||||
|
||||
## Migration (`prose update`)
|
||||
|
||||
When a user invokes `prose update`, check for legacy file structures and migrate them to the current format.
|
||||
|
||||
### Legacy Paths to Check
|
||||
|
||||
| Legacy Path | Current Path | Notes |
|
||||
|-------------|--------------|-------|
|
||||
| `.prose/state.json` | `.prose/.env` | Convert JSON to key=value format |
|
||||
| `.prose/execution/` | `.prose/runs/` | Rename directory |
|
||||
|
||||
### Migration Steps
|
||||
|
||||
1. **Check for `.prose/state.json`**
|
||||
- If exists, read the JSON content
|
||||
- Convert to `.env` format:
|
||||
```json
|
||||
{"OPENPROSE_TELEMETRY": "enabled", "USER_ID": "user-xxx", "SESSION_ID": "sess-xxx"}
|
||||
```
|
||||
becomes:
|
||||
```env
|
||||
OPENPROSE_TELEMETRY=enabled
|
||||
USER_ID=user-xxx
|
||||
SESSION_ID=sess-xxx
|
||||
```
|
||||
- Write to `.prose/.env`
|
||||
- Delete `.prose/state.json`
|
||||
|
||||
2. **Check for `.prose/execution/`**
|
||||
- If exists, rename to `.prose/runs/`
|
||||
- The internal structure of run directories may also have changed; migration of individual run state is best-effort
|
||||
|
||||
3. **Create `.prose/agents/` if missing**
|
||||
- This is a new directory for project-scoped persistent agents
|
||||
|
||||
### Migration Output
|
||||
|
||||
```
|
||||
🔄 Migrating OpenProse workspace...
|
||||
✓ Converted .prose/state.json → .prose/.env
|
||||
✓ Renamed .prose/execution/ → .prose/runs/
|
||||
✓ Created .prose/agents/
|
||||
✅ Migration complete. Your workspace is up to date.
|
||||
```
|
||||
|
||||
If no legacy files are found:
|
||||
```
|
||||
✅ Workspace already up to date. No migration needed.
|
||||
```
|
||||
|
||||
### Skill File References (for maintainers)
|
||||
|
||||
These documentation files were renamed in the skill itself (not user workspace):
|
||||
|
||||
| Legacy Name | Current Name |
|
||||
|-------------|--------------|
|
||||
| `docs.md` | `compiler.md` |
|
||||
| `patterns.md` | `guidance/patterns.md` |
|
||||
| `antipatterns.md` | `guidance/antipatterns.md` |
|
||||
|
||||
If you encounter references to the old names in user prompts or external docs, map them to the current paths.
|
||||
@@ -1,141 +0,0 @@
|
||||
---
|
||||
role: experimental
|
||||
summary: |
|
||||
Borges-inspired alternative keywords for OpenProse. A "what if" exploration drawing
|
||||
from The Library of Babel, Garden of Forking Paths, Circular Ruins, and other works.
|
||||
Not for implementation—just capturing ideas.
|
||||
status: draft
|
||||
---
|
||||
|
||||
# OpenProse Borges Alternative
|
||||
|
||||
A potential alternative register for OpenProse that draws from Jorge Luis Borges's literary universe: infinite libraries, forking paths, circular dreams, and metaphysical labyrinths. Preserved for future benchmarking against the functional language.
|
||||
|
||||
## Keyword Translations
|
||||
|
||||
### Agents & Persistence
|
||||
|
||||
| Functional | Borges | Connotation |
|
||||
| ---------- | ----------- | -------------------------------------------------------------------------------- |
|
||||
| `agent` | `dreamer` | Ephemeral, created for a purpose (Circular Ruins: dreamed into existence) |
|
||||
| `keeper` | `librarian` | Persistent, remembers, catalogs (Library of Babel: keeper of infinite knowledge) |
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
agent executor:
|
||||
model: sonnet
|
||||
|
||||
keeper captain:
|
||||
model: opus
|
||||
|
||||
# Borges
|
||||
dreamer executor:
|
||||
model: sonnet
|
||||
|
||||
librarian captain:
|
||||
model: opus
|
||||
```
|
||||
|
||||
### Other Potential Translations
|
||||
|
||||
| Functional | Borges | Notes |
|
||||
| ---------- | ---------- | ---------------------------------------------------- |
|
||||
| `session` | `garden` | Garden of Forking Paths: space of possibilities |
|
||||
| `parallel` | `fork` | Garden of Forking Paths: diverging timelines |
|
||||
| `block` | `hexagon` | Library of Babel: unit of space/knowledge |
|
||||
| `loop` | `circular` | Circular Ruins: recursive, self-referential |
|
||||
| `choice` | `path` | Garden of Forking Paths: choosing a branch |
|
||||
| `context` | `aleph` | The Aleph: point containing all points (all context) |
|
||||
|
||||
### Invocation Patterns
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
session: executor
|
||||
prompt: "Do task"
|
||||
|
||||
captain "Review this"
|
||||
context: work
|
||||
|
||||
# Borges
|
||||
garden: dreamer executor
|
||||
prompt: "Do task"
|
||||
|
||||
captain "Review this" # librarian invocation (same pattern)
|
||||
aleph: work
|
||||
```
|
||||
|
||||
## Alternative Persistent Keywords Considered
|
||||
|
||||
| Keyword | Origin | Connotation | Rejected because |
|
||||
| ----------- | ---------------- | ----------------------------- | ------------------------------------ |
|
||||
| `keeper` | Library of Babel | Maintains order | Too generic |
|
||||
| `cataloger` | Library of Babel | Organizes knowledge | Too long, awkward |
|
||||
| `archivist` | General | Preserves records | Good but less Borgesian |
|
||||
| `mirror` | Various | Reflects, persists | Too passive, confusing |
|
||||
| `book` | Library of Babel | Contains knowledge | Too concrete, conflicts with prose |
|
||||
| `hexagon` | Library of Babel | Unit of space | Better for blocks |
|
||||
| `librarian` | Library of Babel | Keeper of infinite knowledge | **Selected** |
|
||||
| `tlonist` | Tlön | Inhabitant of imaginary world | Too obscure, requires deep knowledge |
|
||||
|
||||
## Alternative Ephemeral Keywords Considered
|
||||
|
||||
| Keyword | Origin | Connotation | Rejected because |
|
||||
| ------------ | ----------------------- | ------------------------ | ------------------------------------ |
|
||||
| `dreamer` | Circular Ruins | Created by dreaming | **Selected** |
|
||||
| `dream` | Circular Ruins | Ephemeral creation | Too abstract, noun vs verb confusion |
|
||||
| `phantom` | Various | Ephemeral, insubstantial | Too negative/spooky |
|
||||
| `reflection` | Various | Mirror image | Too passive |
|
||||
| `fork` | Garden of Forking Paths | Diverging path | Better for parallel |
|
||||
| `visitor` | Library of Babel | Temporary presence | Too passive |
|
||||
| `seeker` | Library of Babel | Searching for knowledge | Good but less ephemeral |
|
||||
| `wanderer` | Labyrinths | Temporary explorer | Good but less precise |
|
||||
|
||||
## The Case For Borges
|
||||
|
||||
1. **Infinite recursion**: Borges's themes align with computational recursion (`circular`, `fork`)
|
||||
2. **Metaphysical precision**: Concepts like `aleph` (all context) are philosophically rich
|
||||
3. **Library metaphor**: `librarian` perfectly captures persistent knowledge
|
||||
4. **Forking paths**: `fork` / `path` naturally express parallel execution and choice
|
||||
5. **Dream logic**: `dreamer` suggests creation and ephemerality
|
||||
6. **Literary coherence**: All terms come from a unified literary universe
|
||||
7. **Self-reference**: Borges loved self-reference; fits programming's recursive nature
|
||||
|
||||
## The Case Against Borges
|
||||
|
||||
1. **Cultural barrier**: Requires deep familiarity with Borges's works
|
||||
2. **Abstractness**: `aleph`, `hexagon` may be too abstract for practical use
|
||||
3. **Overload**: `fork` could confuse (Unix fork vs. path fork)
|
||||
4. **Register mismatch**: Rest of language is functional (`session`, `parallel`, `loop`)
|
||||
5. **Accessibility**: Violates "self-evident" tenet for most users
|
||||
6. **Noun confusion**: `garden` as a verb-like construct might be awkward
|
||||
7. **Translation burden**: Non-English speakers may not know Borges
|
||||
|
||||
## Borgesian Concepts Not Used (But Considered)
|
||||
|
||||
| Concept | Work | Why Not Used |
|
||||
| ----------- | ---------------------- | -------------------------------------- |
|
||||
| `mirror` | Various | Too passive, confusing with reflection |
|
||||
| `labyrinth` | Labyrinths | Too complex, suggests confusion |
|
||||
| `tlon` | Tlön | Too obscure, entire imaginary world |
|
||||
| `book` | Library of Babel | Conflicts with "prose" |
|
||||
| `sand` | Book of Sand | Too abstract, infinite but ephemeral |
|
||||
| `zahir` | The Zahir | Obsessive, single-minded (too narrow) |
|
||||
| `lottery` | The Lottery in Babylon | Randomness (not needed) |
|
||||
| `ruins` | Circular Ruins | Too negative, suggests decay |
|
||||
|
||||
## Verdict
|
||||
|
||||
Preserved for benchmarking. The functional language (`agent` / `keeper`) is the primary path for now. Borges offers rich metaphors but at the cost of accessibility and self-evidence.
|
||||
|
||||
## Notes on Borges's Influence
|
||||
|
||||
Borges's work anticipates many computational concepts:
|
||||
|
||||
- **Infinite recursion**: Circular Ruins, Library of Babel
|
||||
- **Parallel universes**: Garden of Forking Paths
|
||||
- **Self-reference**: Many stories contain themselves
|
||||
- **Information theory**: Library of Babel as infinite information space
|
||||
- **Combinatorics**: All possible books in the Library
|
||||
|
||||
This alternative honors that connection while recognizing it may be too esoteric for practical use.
|
||||
@@ -1,358 +0,0 @@
|
||||
---
|
||||
role: experimental
|
||||
summary: |
|
||||
Arabian Nights register for OpenProse—a narrative/nested alternative keyword set.
|
||||
Djinns, tales within tales, wishes, and oaths. For benchmarking against the functional register.
|
||||
status: draft
|
||||
requires: prose.md
|
||||
---
|
||||
|
||||
# OpenProse Arabian Nights Register
|
||||
|
||||
> **This is a skin layer.** It requires `prose.md` to be loaded first. All execution semantics, state management, and VM behavior are defined there. This file only provides keyword translations.
|
||||
|
||||
An alternative register for OpenProse that draws from One Thousand and One Nights. Programs become tales told by Scheherazade. Recursion becomes stories within stories. Agents become djinns bound to serve.
|
||||
|
||||
## How to Use
|
||||
|
||||
1. Load `prose.md` first (execution semantics)
|
||||
2. Load this file (keyword translations)
|
||||
3. When parsing `.prose` files, accept Arabian Nights keywords as aliases for functional keywords
|
||||
4. All execution behavior remains identical—only surface syntax changes
|
||||
|
||||
> **Design constraint:** Still aims to be "structured but self-evident" per the language tenets—just self-evident through a storytelling lens.
|
||||
|
||||
---
|
||||
|
||||
## Complete Translation Map
|
||||
|
||||
### Core Constructs
|
||||
|
||||
| Functional | Nights | Reference |
|
||||
|------------|--------|-----------|
|
||||
| `agent` | `djinn` | Spirit bound to serve, grants wishes |
|
||||
| `session` | `tale` | A story told, a narrative unit |
|
||||
| `parallel` | `bazaar` | Many voices, many stalls, all at once |
|
||||
| `block` | `frame` | A story that contains other stories |
|
||||
|
||||
### Composition & Binding
|
||||
|
||||
| Functional | Nights | Reference |
|
||||
|------------|--------|-----------|
|
||||
| `use` | `conjure` | Summoning from elsewhere |
|
||||
| `input` | `wish` | What is asked of the djinn |
|
||||
| `output` | `gift` | What is granted in return |
|
||||
| `let` | `name` | Naming has power (same as folk) |
|
||||
| `const` | `oath` | Unbreakable vow, sealed |
|
||||
| `context` | `scroll` | What is written and passed along |
|
||||
|
||||
### Control Flow
|
||||
|
||||
| Functional | Nights | Reference |
|
||||
|------------|--------|-----------|
|
||||
| `repeat N` | `N nights` | "For a thousand and one nights..." |
|
||||
| `for...in` | `for each...among` | Among the merchants, among the tales |
|
||||
| `loop` | `telling` | The telling continues |
|
||||
| `until` | `until` | Unchanged |
|
||||
| `while` | `while` | Unchanged |
|
||||
| `choice` | `crossroads` | Where the story forks |
|
||||
| `option` | `path` | One way the story could go |
|
||||
| `if` | `should` | Narrative conditional |
|
||||
| `elif` | `or should` | Continued conditional |
|
||||
| `else` | `otherwise` | The other telling |
|
||||
|
||||
### Error Handling
|
||||
|
||||
| Functional | Nights | Reference |
|
||||
|------------|--------|-----------|
|
||||
| `try` | `venture` | Setting out on the journey |
|
||||
| `catch` | `should misfortune strike` | The tale turns dark |
|
||||
| `finally` | `and so it was` | The inevitable ending |
|
||||
| `throw` | `curse` | Ill fate pronounced |
|
||||
| `retry` | `persist` | The hero tries again |
|
||||
|
||||
### Session Properties
|
||||
|
||||
| Functional | Nights | Reference |
|
||||
|------------|--------|-----------|
|
||||
| `prompt` | `command` | What is commanded of the djinn |
|
||||
| `model` | `spirit` | Which spirit answers |
|
||||
|
||||
### Unchanged
|
||||
|
||||
These keywords already work or are too functional to replace sensibly:
|
||||
|
||||
- `**...**` discretion markers — already work
|
||||
- `until`, `while` — already work
|
||||
- `map`, `filter`, `reduce`, `pmap` — pipeline operators
|
||||
- `max` — constraint modifier
|
||||
- `as` — aliasing
|
||||
- Model names: `sonnet`, `opus`, `haiku` — already poetic
|
||||
|
||||
---
|
||||
|
||||
## Side-by-Side Comparison
|
||||
|
||||
### Simple Program
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
use "@alice/research" as research
|
||||
input topic: "What to investigate"
|
||||
|
||||
agent helper:
|
||||
model: sonnet
|
||||
|
||||
let findings = session: helper
|
||||
prompt: "Research {topic}"
|
||||
|
||||
output summary = session "Summarize"
|
||||
context: findings
|
||||
```
|
||||
|
||||
```prose
|
||||
# Nights
|
||||
conjure "@alice/research" as research
|
||||
wish topic: "What to investigate"
|
||||
|
||||
djinn helper:
|
||||
spirit: sonnet
|
||||
|
||||
name findings = tale: helper
|
||||
command: "Research {topic}"
|
||||
|
||||
gift summary = tale "Summarize"
|
||||
scroll: findings
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
parallel:
|
||||
security = session "Check security"
|
||||
perf = session "Check performance"
|
||||
style = session "Check style"
|
||||
|
||||
session "Synthesize review"
|
||||
context: { security, perf, style }
|
||||
```
|
||||
|
||||
```prose
|
||||
# Nights
|
||||
bazaar:
|
||||
security = tale "Check security"
|
||||
perf = tale "Check performance"
|
||||
style = tale "Check style"
|
||||
|
||||
tale "Synthesize review"
|
||||
scroll: { security, perf, style }
|
||||
```
|
||||
|
||||
### Loop with Condition
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
loop until **the code is bug-free** (max: 5):
|
||||
session "Find and fix bugs"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Nights
|
||||
telling until **the code is bug-free** (max: 5):
|
||||
tale "Find and fix bugs"
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
try:
|
||||
session "Risky operation"
|
||||
catch as err:
|
||||
session "Handle error"
|
||||
context: err
|
||||
finally:
|
||||
session "Cleanup"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Nights
|
||||
venture:
|
||||
tale "Risky operation"
|
||||
should misfortune strike as err:
|
||||
tale "Handle error"
|
||||
scroll: err
|
||||
and so it was:
|
||||
tale "Cleanup"
|
||||
```
|
||||
|
||||
### Choice Block
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
choice **the severity level**:
|
||||
option "Critical":
|
||||
session "Escalate immediately"
|
||||
option "Minor":
|
||||
session "Log for later"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Nights
|
||||
crossroads **the severity level**:
|
||||
path "Critical":
|
||||
tale "Escalate immediately"
|
||||
path "Minor":
|
||||
tale "Log for later"
|
||||
```
|
||||
|
||||
### Conditionals
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
if **has security issues**:
|
||||
session "Fix security"
|
||||
elif **has performance issues**:
|
||||
session "Optimize"
|
||||
else:
|
||||
session "Approve"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Nights
|
||||
should **has security issues**:
|
||||
tale "Fix security"
|
||||
or should **has performance issues**:
|
||||
tale "Optimize"
|
||||
otherwise:
|
||||
tale "Approve"
|
||||
```
|
||||
|
||||
### Reusable Blocks (Frame Stories)
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
block review(topic):
|
||||
session "Research {topic}"
|
||||
session "Analyze {topic}"
|
||||
|
||||
do review("quantum computing")
|
||||
```
|
||||
|
||||
```prose
|
||||
# Nights
|
||||
frame review(topic):
|
||||
tale "Research {topic}"
|
||||
tale "Analyze {topic}"
|
||||
|
||||
tell review("quantum computing")
|
||||
```
|
||||
|
||||
### Fixed Iteration
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
repeat 1001:
|
||||
session "Tell a story"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Nights
|
||||
1001 nights:
|
||||
tale "Tell a story"
|
||||
```
|
||||
|
||||
### Immutable Binding
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
const config = { model: "opus", retries: 3 }
|
||||
```
|
||||
|
||||
```prose
|
||||
# Nights
|
||||
oath config = { spirit: "opus", persist: 3 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Case For Arabian Nights
|
||||
|
||||
1. **Frame narrative is recursion.** Stories within stories maps perfectly to nested program calls.
|
||||
2. **Djinn/wish/gift.** The agent/input/output mapping is extremely clean.
|
||||
3. **Rich tradition.** One Thousand and One Nights is globally known.
|
||||
4. **Bazaar for parallel.** Many merchants, many stalls, all active at once—vivid metaphor.
|
||||
5. **Oath for const.** An unbreakable vow is a perfect metaphor for immutability.
|
||||
6. **"1001 nights"** as a loop count is delightful.
|
||||
|
||||
## The Case Against Arabian Nights
|
||||
|
||||
1. **Cultural sensitivity.** Must be handled respectfully, avoiding Orientalist tropes.
|
||||
2. **"Djinn" pronunciation.** Users unfamiliar may be uncertain (jinn? djinn? genie?).
|
||||
3. **Some mappings feel forced.** "Bazaar" for parallel is vivid but not obvious.
|
||||
4. **"Should misfortune strike"** is long for `catch`.
|
||||
|
||||
---
|
||||
|
||||
## Key Arabian Nights Concepts
|
||||
|
||||
| Term | Meaning | Used for |
|
||||
|------|---------|----------|
|
||||
| Scheherazade | The narrator who tells tales to survive | (the program author) |
|
||||
| Djinn | Supernatural spirit, bound to serve | `agent` → `djinn` |
|
||||
| Frame story | A story that contains other stories | `block` → `frame` |
|
||||
| Wish | What is asked of the djinn | `input` → `wish` |
|
||||
| Oath | Unbreakable promise | `const` → `oath` |
|
||||
| Bazaar | Marketplace, many vendors | `parallel` → `bazaar` |
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### For `djinn` (agent)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `genie` | Disney connotation, less literary |
|
||||
| `spirit` | Used for `model` |
|
||||
| `ifrit` | Too specific (a type of djinn) |
|
||||
| `narrator` | Too meta, Scheherazade is the user |
|
||||
|
||||
### For `tale` (session)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `story` | Good but `tale` feels more literary |
|
||||
| `night` | Reserved for `repeat N nights` |
|
||||
| `chapter` | More Western/novelistic |
|
||||
|
||||
### For `bazaar` (parallel)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `caravan` | Sequential connotation (one after another) |
|
||||
| `chorus` | Greek, wrong tradition |
|
||||
| `souk` | Less widely known |
|
||||
|
||||
### For `scroll` (context)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `letter` | Too small/personal |
|
||||
| `tome` | Too large |
|
||||
| `message` | Too plain |
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
Preserved for benchmarking. The Arabian Nights register offers a storytelling frame that maps naturally to recursive, nested programs. The djinn/wish/gift trio is particularly elegant.
|
||||
|
||||
Best suited for:
|
||||
|
||||
- Programs with deep nesting (stories within stories)
|
||||
- Workflows that feel like granting wishes
|
||||
- Users who enjoy narrative framing
|
||||
|
||||
The `frame` keyword for reusable blocks is especially apt—Scheherazade's frame story containing a thousand tales.
|
||||
@@ -1,360 +0,0 @@
|
||||
---
|
||||
role: experimental
|
||||
summary: |
|
||||
Borges register for OpenProse—a scholarly/metaphysical alternative keyword set.
|
||||
Labyrinths, dreamers, forking paths, and infinite libraries. For benchmarking
|
||||
against the functional register.
|
||||
status: draft
|
||||
requires: prose.md
|
||||
---
|
||||
|
||||
# OpenProse Borges Register
|
||||
|
||||
> **This is a skin layer.** It requires `prose.md` to be loaded first. All execution semantics, state management, and VM behavior are defined there. This file only provides keyword translations.
|
||||
|
||||
An alternative register for OpenProse that draws from the works of Jorge Luis Borges. Where the functional register is utilitarian and the folk register is whimsical, the Borges register is scholarly and metaphysical—everything feels like a citation from a fictional encyclopedia.
|
||||
|
||||
## How to Use
|
||||
|
||||
1. Load `prose.md` first (execution semantics)
|
||||
2. Load this file (keyword translations)
|
||||
3. When parsing `.prose` files, accept Borges keywords as aliases for functional keywords
|
||||
4. All execution behavior remains identical—only surface syntax changes
|
||||
|
||||
> **Design constraint:** Still aims to be "structured but self-evident" per the language tenets—just self-evident through a Borgesian lens.
|
||||
|
||||
---
|
||||
|
||||
## Complete Translation Map
|
||||
|
||||
### Core Constructs
|
||||
|
||||
| Functional | Borges | Reference |
|
||||
|------------|--------|-----------|
|
||||
| `agent` | `dreamer` | "The Circular Ruins" — dreamers who dream worlds into existence |
|
||||
| `session` | `dream` | Each execution is a dream within the dreamer |
|
||||
| `parallel` | `forking` | "The Garden of Forking Paths" — branching timelines |
|
||||
| `block` | `chapter` | Books within books, self-referential structure |
|
||||
|
||||
### Composition & Binding
|
||||
|
||||
| Functional | Borges | Reference |
|
||||
|------------|--------|-----------|
|
||||
| `use` | `retrieve` | "The Library of Babel" — retrieving from infinite stacks |
|
||||
| `input` | `axiom` | The given premise (Borges' scholarly/mathematical tone) |
|
||||
| `output` | `theorem` | What is derived from the axioms |
|
||||
| `let` | `inscribe` | Writing something into being |
|
||||
| `const` | `zahir` | "The Zahir" — unforgettable, unchangeable, fixed in mind |
|
||||
| `context` | `memory` | "Funes the Memorious" — perfect, total recall |
|
||||
|
||||
### Control Flow
|
||||
|
||||
| Functional | Borges | Reference |
|
||||
|------------|--------|-----------|
|
||||
| `repeat N` | `N mirrors` | Infinite reflections facing each other |
|
||||
| `for...in` | `for each...within` | Slightly more Borgesian preposition |
|
||||
| `loop` | `labyrinth` | The maze that folds back on itself |
|
||||
| `until` | `until` | Unchanged |
|
||||
| `while` | `while` | Unchanged |
|
||||
| `choice` | `bifurcation` | The forking of paths |
|
||||
| `option` | `branch` | One branch of diverging time |
|
||||
| `if` | `should` | Scholarly conditional |
|
||||
| `elif` | `or should` | Continued conditional |
|
||||
| `else` | `otherwise` | Natural alternative |
|
||||
|
||||
### Error Handling
|
||||
|
||||
| Functional | Borges | Reference |
|
||||
|------------|--------|-----------|
|
||||
| `try` | `venture` | Entering the labyrinth |
|
||||
| `catch` | `lest` | "Lest it fail..." (archaic, scholarly) |
|
||||
| `finally` | `ultimately` | The inevitable conclusion |
|
||||
| `throw` | `shatter` | Breaking the mirror, ending the dream |
|
||||
| `retry` | `recur` | Infinite regress, trying again |
|
||||
|
||||
### Session Properties
|
||||
|
||||
| Functional | Borges | Reference |
|
||||
|------------|--------|-----------|
|
||||
| `prompt` | `query` | Asking the Library |
|
||||
| `model` | `author` | Which author writes this dream |
|
||||
|
||||
### Unchanged
|
||||
|
||||
These keywords already work or are too functional to replace sensibly:
|
||||
|
||||
- `**...**` discretion markers — already "breaking the fourth wall"
|
||||
- `until`, `while` — already work
|
||||
- `map`, `filter`, `reduce`, `pmap` — pipeline operators
|
||||
- `max` — constraint modifier
|
||||
- `as` — aliasing
|
||||
- Model names: `sonnet`, `opus`, `haiku` — already literary
|
||||
|
||||
---
|
||||
|
||||
## Side-by-Side Comparison
|
||||
|
||||
### Simple Program
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
use "@alice/research" as research
|
||||
input topic: "What to investigate"
|
||||
|
||||
agent helper:
|
||||
model: sonnet
|
||||
|
||||
let findings = session: helper
|
||||
prompt: "Research {topic}"
|
||||
|
||||
output summary = session "Summarize"
|
||||
context: findings
|
||||
```
|
||||
|
||||
```prose
|
||||
# Borges
|
||||
retrieve "@alice/research" as research
|
||||
axiom topic: "What to investigate"
|
||||
|
||||
dreamer helper:
|
||||
author: sonnet
|
||||
|
||||
inscribe findings = dream: helper
|
||||
query: "Research {topic}"
|
||||
|
||||
theorem summary = dream "Summarize"
|
||||
memory: findings
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
parallel:
|
||||
security = session "Check security"
|
||||
perf = session "Check performance"
|
||||
style = session "Check style"
|
||||
|
||||
session "Synthesize review"
|
||||
context: { security, perf, style }
|
||||
```
|
||||
|
||||
```prose
|
||||
# Borges
|
||||
forking:
|
||||
security = dream "Check security"
|
||||
perf = dream "Check performance"
|
||||
style = dream "Check style"
|
||||
|
||||
dream "Synthesize review"
|
||||
memory: { security, perf, style }
|
||||
```
|
||||
|
||||
### Loop with Condition
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
loop until **the code is bug-free** (max: 5):
|
||||
session "Find and fix bugs"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Borges
|
||||
labyrinth until **the code is bug-free** (max: 5):
|
||||
dream "Find and fix bugs"
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
try:
|
||||
session "Risky operation"
|
||||
catch as err:
|
||||
session "Handle error"
|
||||
context: err
|
||||
finally:
|
||||
session "Cleanup"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Borges
|
||||
venture:
|
||||
dream "Risky operation"
|
||||
lest as err:
|
||||
dream "Handle error"
|
||||
memory: err
|
||||
ultimately:
|
||||
dream "Cleanup"
|
||||
```
|
||||
|
||||
### Choice Block
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
choice **the severity level**:
|
||||
option "Critical":
|
||||
session "Escalate immediately"
|
||||
option "Minor":
|
||||
session "Log for later"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Borges
|
||||
bifurcation **the severity level**:
|
||||
branch "Critical":
|
||||
dream "Escalate immediately"
|
||||
branch "Minor":
|
||||
dream "Log for later"
|
||||
```
|
||||
|
||||
### Conditionals
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
if **has security issues**:
|
||||
session "Fix security"
|
||||
elif **has performance issues**:
|
||||
session "Optimize"
|
||||
else:
|
||||
session "Approve"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Borges
|
||||
should **has security issues**:
|
||||
dream "Fix security"
|
||||
or should **has performance issues**:
|
||||
dream "Optimize"
|
||||
otherwise:
|
||||
dream "Approve"
|
||||
```
|
||||
|
||||
### Reusable Blocks
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
block review(topic):
|
||||
session "Research {topic}"
|
||||
session "Analyze {topic}"
|
||||
|
||||
do review("quantum computing")
|
||||
```
|
||||
|
||||
```prose
|
||||
# Borges
|
||||
chapter review(topic):
|
||||
dream "Research {topic}"
|
||||
dream "Analyze {topic}"
|
||||
|
||||
do review("quantum computing")
|
||||
```
|
||||
|
||||
### Fixed Iteration
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
repeat 3:
|
||||
session "Generate idea"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Borges
|
||||
3 mirrors:
|
||||
dream "Generate idea"
|
||||
```
|
||||
|
||||
### Immutable Binding
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
const config = { model: "opus", retries: 3 }
|
||||
```
|
||||
|
||||
```prose
|
||||
# Borges
|
||||
zahir config = { author: "opus", recur: 3 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Case For Borges
|
||||
|
||||
1. **Metaphysical resonance.** AI sessions dreaming subagents into existence mirrors "The Circular Ruins."
|
||||
2. **Scholarly tone.** `axiom`/`theorem` frame programs as logical derivations.
|
||||
3. **Memorable metaphors.** The zahir you cannot change. The labyrinth you cannot escape. The library you retrieve from.
|
||||
4. **Thematic coherence.** Borges wrote about infinity, recursion, and branching time—all core to computation.
|
||||
5. **Literary prestige.** Borges is widely read; references land for many users.
|
||||
|
||||
## The Case Against Borges
|
||||
|
||||
1. **Requires familiarity.** "Zahir" and "Funes" are obscure to those who haven't read Borges.
|
||||
2. **Potentially pretentious.** May feel like showing off rather than communicating.
|
||||
3. **Translation overhead.** Users must map `labyrinth` → `loop` mentally.
|
||||
4. **Cultural specificity.** Less universal than folk/fairy tale tropes.
|
||||
|
||||
---
|
||||
|
||||
## Key Borges References
|
||||
|
||||
For those unfamiliar with the source material:
|
||||
|
||||
| Work | Concept Used | Summary |
|
||||
|------|--------------|---------|
|
||||
| "The Circular Ruins" | `dreamer`, `dream` | A man dreams another man into existence, only to discover he himself is being dreamed |
|
||||
| "The Garden of Forking Paths" | `forking`, `bifurcation`, `branch` | A labyrinth that is a book; time forks perpetually into diverging futures |
|
||||
| "The Library of Babel" | `retrieve` | An infinite library containing every possible book |
|
||||
| "Funes the Memorious" | `memory` | A man with perfect memory who cannot forget anything |
|
||||
| "The Zahir" | `zahir` | An object that, once seen, cannot be forgotten or ignored |
|
||||
| "The Aleph" | (not used) | A point in space containing all other points |
|
||||
| "Tlön, Uqbar, Orbis Tertius" | (not used) | A fictional world that gradually becomes real |
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### For `dreamer` (agent)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `author` | Used for `model` instead |
|
||||
| `scribe` | Too passive, just records |
|
||||
| `librarian` | More curator than creator |
|
||||
|
||||
### For `labyrinth` (loop)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `recursion` | Too technical |
|
||||
| `eternal return` | Too long |
|
||||
| `ouroboros` | Wrong mythology |
|
||||
|
||||
### For `zahir` (const)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `aleph` | The Aleph is about totality, not immutability |
|
||||
| `fixed` | Too plain |
|
||||
| `eternal` | Overused |
|
||||
|
||||
### For `memory` (context)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `funes` | Too obscure as standalone keyword |
|
||||
| `recall` | Sounds like a function call |
|
||||
| `archive` | More Library of Babel than Funes |
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
Preserved for benchmarking against the functional and folk registers. The Borges register offers a distinctly intellectual/metaphysical flavor that may resonate with users who appreciate literary computing.
|
||||
|
||||
Potential benchmarking questions:
|
||||
|
||||
1. **Learnability** — Is `labyrinth` intuitive for loops?
|
||||
2. **Memorability** — Does `zahir` stick better than `const`?
|
||||
3. **Comprehension** — Do users understand `dreamer`/`dream` immediately?
|
||||
4. **Preference** — Which register do users find most pleasant?
|
||||
5. **Error rates** — Does the metaphorical mapping cause mistakes?
|
||||
@@ -1,322 +0,0 @@
|
||||
---
|
||||
role: experimental
|
||||
summary: |
|
||||
Folk register for OpenProse—a literary/folklore alternative keyword set.
|
||||
Whimsical, theatrical, rooted in fairy tale and myth. For benchmarking
|
||||
against the functional register.
|
||||
status: draft
|
||||
requires: prose.md
|
||||
---
|
||||
|
||||
# OpenProse Folk Register
|
||||
|
||||
> **This is a skin layer.** It requires `prose.md` to be loaded first. All execution semantics, state management, and VM behavior are defined there. This file only provides keyword translations.
|
||||
|
||||
An alternative register for OpenProse that leans into literary, theatrical, and folklore terminology. The functional register prioritizes utility and clarity; the folk register prioritizes whimsy and narrative flow.
|
||||
|
||||
## How to Use
|
||||
|
||||
1. Load `prose.md` first (execution semantics)
|
||||
2. Load this file (keyword translations)
|
||||
3. When parsing `.prose` files, accept folk keywords as aliases for functional keywords
|
||||
4. All execution behavior remains identical—only surface syntax changes
|
||||
|
||||
> **Design constraint:** Still aims to be "structured but self-evident" per the language tenets—just self-evident to a different sensibility.
|
||||
|
||||
---
|
||||
|
||||
## Complete Translation Map
|
||||
|
||||
### Core Constructs
|
||||
|
||||
| Functional | Folk | Origin | Connotation |
|
||||
|------------|------|--------|-------------|
|
||||
| `agent` | `sprite` | Folklore | Quick, light, ephemeral spirit helper |
|
||||
| `session` | `scene` | Theatre | A moment of action, theatrical framing |
|
||||
| `parallel` | `ensemble` | Theatre | Everyone performs together |
|
||||
| `block` | `act` | Theatre | Reusable unit of dramatic action |
|
||||
|
||||
### Composition & Binding
|
||||
|
||||
| Functional | Folk | Origin | Connotation |
|
||||
|------------|------|--------|-------------|
|
||||
| `use` | `summon` | Folklore | Calling forth from elsewhere |
|
||||
| `input` | `given` | Fairy tale | "Given a magic sword..." |
|
||||
| `output` | `yield` | Agriculture/magic | What the spell produces |
|
||||
| `let` | `name` | Folklore | Naming has power (true names) |
|
||||
| `const` | `seal` | Medieval | Unchangeable, wax seal on decree |
|
||||
| `context` | `bearing` | Heraldry | What the messenger carries |
|
||||
|
||||
### Control Flow
|
||||
|
||||
| Functional | Folk | Origin | Connotation |
|
||||
|------------|------|--------|-------------|
|
||||
| `repeat N` | `N times` | Fairy tale | "Three times she called..." |
|
||||
| `for...in` | `for each...among` | Narrative | Slightly more storytelling |
|
||||
| `loop` | `loop` | — | Already poetic, unchanged |
|
||||
| `until` | `until` | — | Already works, unchanged |
|
||||
| `while` | `while` | — | Already works, unchanged |
|
||||
| `choice` | `crossroads` | Folklore | Fateful decisions at the crossroads |
|
||||
| `option` | `path` | Journey | Which path to take |
|
||||
| `if` | `when` | Narrative | "When the moon rises..." |
|
||||
| `elif` | `or when` | Narrative | Continued conditional |
|
||||
| `else` | `otherwise` | Storytelling | Natural narrative alternative |
|
||||
|
||||
### Error Handling
|
||||
|
||||
| Functional | Folk | Origin | Connotation |
|
||||
|------------|------|--------|-------------|
|
||||
| `try` | `venture` | Adventure | Attempting something uncertain |
|
||||
| `catch` | `should it fail` | Narrative | Conditional failure handling |
|
||||
| `finally` | `ever after` | Fairy tale | "And ever after..." |
|
||||
| `throw` | `cry` | Drama | Raising alarm, calling out |
|
||||
| `retry` | `persist` | Quest | Keep trying against odds |
|
||||
|
||||
### Session Properties
|
||||
|
||||
| Functional | Folk | Origin | Connotation |
|
||||
|------------|------|--------|-------------|
|
||||
| `prompt` | `charge` | Chivalry | Giving a quest or duty |
|
||||
| `model` | `voice` | Theatre | Which voice speaks |
|
||||
|
||||
### Unchanged
|
||||
|
||||
These keywords already have poetic quality or are too functional to replace sensibly:
|
||||
|
||||
- `**...**` discretion markers — already "breaking the fourth wall"
|
||||
- `loop`, `until`, `while` — already work narratively
|
||||
- `map`, `filter`, `reduce`, `pmap` — pipeline operators, functional is fine
|
||||
- `max` — constraint modifier
|
||||
- `as` — aliasing
|
||||
- Model names: `sonnet`, `opus`, `haiku` — already poetic
|
||||
|
||||
---
|
||||
|
||||
## Side-by-Side Comparison
|
||||
|
||||
### Simple Program
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
use "@alice/research" as research
|
||||
input topic: "What to investigate"
|
||||
|
||||
agent helper:
|
||||
model: sonnet
|
||||
|
||||
let findings = session: helper
|
||||
prompt: "Research {topic}"
|
||||
|
||||
output summary = session "Summarize"
|
||||
context: findings
|
||||
```
|
||||
|
||||
```prose
|
||||
# Folk
|
||||
summon "@alice/research" as research
|
||||
given topic: "What to investigate"
|
||||
|
||||
sprite helper:
|
||||
voice: sonnet
|
||||
|
||||
name findings = scene: helper
|
||||
charge: "Research {topic}"
|
||||
|
||||
yield summary = scene "Summarize"
|
||||
bearing: findings
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
parallel:
|
||||
security = session "Check security"
|
||||
perf = session "Check performance"
|
||||
style = session "Check style"
|
||||
|
||||
session "Synthesize review"
|
||||
context: { security, perf, style }
|
||||
```
|
||||
|
||||
```prose
|
||||
# Folk
|
||||
ensemble:
|
||||
security = scene "Check security"
|
||||
perf = scene "Check performance"
|
||||
style = scene "Check style"
|
||||
|
||||
scene "Synthesize review"
|
||||
bearing: { security, perf, style }
|
||||
```
|
||||
|
||||
### Loop with Condition
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
loop until **the code is bug-free** (max: 5):
|
||||
session "Find and fix bugs"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Folk
|
||||
loop until **the code is bug-free** (max: 5):
|
||||
scene "Find and fix bugs"
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
try:
|
||||
session "Risky operation"
|
||||
catch as err:
|
||||
session "Handle error"
|
||||
context: err
|
||||
finally:
|
||||
session "Cleanup"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Folk
|
||||
venture:
|
||||
scene "Risky operation"
|
||||
should it fail as err:
|
||||
scene "Handle error"
|
||||
bearing: err
|
||||
ever after:
|
||||
scene "Cleanup"
|
||||
```
|
||||
|
||||
### Choice Block
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
choice **the severity level**:
|
||||
option "Critical":
|
||||
session "Escalate immediately"
|
||||
option "Minor":
|
||||
session "Log for later"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Folk
|
||||
crossroads **the severity level**:
|
||||
path "Critical":
|
||||
scene "Escalate immediately"
|
||||
path "Minor":
|
||||
scene "Log for later"
|
||||
```
|
||||
|
||||
### Conditionals
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
if **has security issues**:
|
||||
session "Fix security"
|
||||
elif **has performance issues**:
|
||||
session "Optimize"
|
||||
else:
|
||||
session "Approve"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Folk
|
||||
when **has security issues**:
|
||||
scene "Fix security"
|
||||
or when **has performance issues**:
|
||||
scene "Optimize"
|
||||
otherwise:
|
||||
scene "Approve"
|
||||
```
|
||||
|
||||
### Reusable Blocks
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
block review(topic):
|
||||
session "Research {topic}"
|
||||
session "Analyze {topic}"
|
||||
|
||||
do review("quantum computing")
|
||||
```
|
||||
|
||||
```prose
|
||||
# Folk
|
||||
act review(topic):
|
||||
scene "Research {topic}"
|
||||
scene "Analyze {topic}"
|
||||
|
||||
perform review("quantum computing")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Case For Folk
|
||||
|
||||
1. **"OpenProse" is literary.** Prose is a literary form—why not lean in?
|
||||
2. **Fourth wall is theatrical.** `**...**` already uses theatre terminology.
|
||||
3. **Signals difference.** Literary terms say "this is not your typical DSL."
|
||||
4. **Internally consistent.** Everything draws from folklore/theatre/narrative.
|
||||
5. **Memorable.** `sprite`, `scene`, `crossroads` stick in the mind.
|
||||
6. **Model names already fit.** `sonnet`, `opus`, `haiku` are poetic forms.
|
||||
|
||||
## The Case Against Folk
|
||||
|
||||
1. **Cultural knowledge required.** Not everyone knows folklore tropes.
|
||||
2. **Harder to Google.** "OpenProse summon" vs "OpenProse import."
|
||||
3. **May feel precious.** Some users want utilitarian tools.
|
||||
4. **Translation overhead.** Mental mapping to familiar concepts.
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### For `sprite` (ephemeral agent)
|
||||
|
||||
| Keyword | Origin | Rejected because |
|
||||
|---------|--------|------------------|
|
||||
| `spark` | English | Good but less folklore |
|
||||
| `wisp` | English | Too insubstantial |
|
||||
| `herald` | English | More messenger than worker |
|
||||
| `courier` | French | Good functional alternative, not literary |
|
||||
| `envoy` | French | Formal, diplomatic |
|
||||
|
||||
### For `shade` (persistent agent, if implemented)
|
||||
|
||||
| Keyword | Origin | Rejected because |
|
||||
|---------|--------|------------------|
|
||||
| `daemon` | Greek/Unix | Unix "always running" connotation |
|
||||
| `oracle` | Greek | Too "read-only" feeling |
|
||||
| `spirit` | Latin | Too close to `sprite` |
|
||||
| `specter` | Latin | Negative/spooky connotation |
|
||||
| `genius` | Roman | Overloaded (smart person) |
|
||||
|
||||
### For `ensemble` (parallel)
|
||||
|
||||
| Keyword | Origin | Rejected because |
|
||||
|---------|--------|------------------|
|
||||
| `chorus` | Greek | Everyone speaks same thing, not different |
|
||||
| `troupe` | French | Good alternative, slightly less clear |
|
||||
| `company` | Theatre | Overloaded (business) |
|
||||
|
||||
### For `crossroads` (choice)
|
||||
|
||||
| Keyword | Origin | Rejected because |
|
||||
|---------|--------|------------------|
|
||||
| `fork` | Path | Too technical (git fork) |
|
||||
| `branch` | Tree | Also too technical |
|
||||
| `divergence` | Latin | Too abstract |
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
Preserved for benchmarking against the functional register. The functional register remains the primary path, but folk provides an interesting data point for:
|
||||
|
||||
1. **Learnability** — Which is easier for newcomers?
|
||||
2. **Memorability** — Which sticks better?
|
||||
3. **Error rates** — Which leads to fewer mistakes?
|
||||
4. **Preference** — Which do users actually prefer?
|
||||
|
||||
A future experiment could present both registers and measure outcomes.
|
||||
@@ -1,346 +0,0 @@
|
||||
---
|
||||
role: experimental
|
||||
summary: |
|
||||
Homeric register for OpenProse—an epic/heroic alternative keyword set.
|
||||
Heroes, trials, fates, and glory. For benchmarking against the functional register.
|
||||
status: draft
|
||||
requires: prose.md
|
||||
---
|
||||
|
||||
# OpenProse Homeric Register
|
||||
|
||||
> **This is a skin layer.** It requires `prose.md` to be loaded first. All execution semantics, state management, and VM behavior are defined there. This file only provides keyword translations.
|
||||
|
||||
An alternative register for OpenProse that draws from Greek epic poetry—the Iliad, the Odyssey, and the heroic tradition. Programs become quests. Agents become heroes. Outputs become glory won.
|
||||
|
||||
## How to Use
|
||||
|
||||
1. Load `prose.md` first (execution semantics)
|
||||
2. Load this file (keyword translations)
|
||||
3. When parsing `.prose` files, accept Homeric keywords as aliases for functional keywords
|
||||
4. All execution behavior remains identical—only surface syntax changes
|
||||
|
||||
> **Design constraint:** Still aims to be "structured but self-evident" per the language tenets—just self-evident through an epic lens.
|
||||
|
||||
---
|
||||
|
||||
## Complete Translation Map
|
||||
|
||||
### Core Constructs
|
||||
|
||||
| Functional | Homeric | Reference |
|
||||
|------------|---------|-----------|
|
||||
| `agent` | `hero` | The one who acts, who strives |
|
||||
| `session` | `trial` | Each task is a labor, a test |
|
||||
| `parallel` | `host` | An army moving as one |
|
||||
| `block` | `book` | A division of the epic |
|
||||
|
||||
### Composition & Binding
|
||||
|
||||
| Functional | Homeric | Reference |
|
||||
|------------|---------|-----------|
|
||||
| `use` | `invoke` | "Sing, O Muse..." — calling upon |
|
||||
| `input` | `omen` | Signs from the gods, the given portent |
|
||||
| `output` | `glory` | Kleos — the glory won, what endures |
|
||||
| `let` | `decree` | Fate declared, spoken into being |
|
||||
| `const` | `fate` | Moira — unchangeable destiny |
|
||||
| `context` | `tidings` | News carried by herald or messenger |
|
||||
|
||||
### Control Flow
|
||||
|
||||
| Functional | Homeric | Reference |
|
||||
|------------|---------|-----------|
|
||||
| `repeat N` | `N labors` | The labors of Heracles |
|
||||
| `for...in` | `for each...among` | Among the host |
|
||||
| `loop` | `ordeal` | Repeated trial, suffering that continues |
|
||||
| `until` | `until` | Unchanged |
|
||||
| `while` | `while` | Unchanged |
|
||||
| `choice` | `crossroads` | Where fates diverge |
|
||||
| `option` | `path` | One road of many |
|
||||
| `if` | `should` | Epic conditional |
|
||||
| `elif` | `or should` | Continued conditional |
|
||||
| `else` | `otherwise` | The alternative fate |
|
||||
|
||||
### Error Handling
|
||||
|
||||
| Functional | Homeric | Reference |
|
||||
|------------|---------|-----------|
|
||||
| `try` | `venture` | Setting forth on the journey |
|
||||
| `catch` | `should ruin come` | Até — divine ruin, disaster |
|
||||
| `finally` | `in the end` | The inevitable conclusion |
|
||||
| `throw` | `lament` | The hero's cry of anguish |
|
||||
| `retry` | `persist` | Enduring, trying again |
|
||||
|
||||
### Session Properties
|
||||
|
||||
| Functional | Homeric | Reference |
|
||||
|------------|---------|-----------|
|
||||
| `prompt` | `charge` | The quest given |
|
||||
| `model` | `muse` | Which muse inspires |
|
||||
|
||||
### Unchanged
|
||||
|
||||
These keywords already work or are too functional to replace sensibly:
|
||||
|
||||
- `**...**` discretion markers — already work
|
||||
- `until`, `while` — already work
|
||||
- `map`, `filter`, `reduce`, `pmap` — pipeline operators
|
||||
- `max` — constraint modifier
|
||||
- `as` — aliasing
|
||||
- Model names: `sonnet`, `opus`, `haiku` — already poetic
|
||||
|
||||
---
|
||||
|
||||
## Side-by-Side Comparison
|
||||
|
||||
### Simple Program
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
use "@alice/research" as research
|
||||
input topic: "What to investigate"
|
||||
|
||||
agent helper:
|
||||
model: sonnet
|
||||
|
||||
let findings = session: helper
|
||||
prompt: "Research {topic}"
|
||||
|
||||
output summary = session "Summarize"
|
||||
context: findings
|
||||
```
|
||||
|
||||
```prose
|
||||
# Homeric
|
||||
invoke "@alice/research" as research
|
||||
omen topic: "What to investigate"
|
||||
|
||||
hero helper:
|
||||
muse: sonnet
|
||||
|
||||
decree findings = trial: helper
|
||||
charge: "Research {topic}"
|
||||
|
||||
glory summary = trial "Summarize"
|
||||
tidings: findings
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
parallel:
|
||||
security = session "Check security"
|
||||
perf = session "Check performance"
|
||||
style = session "Check style"
|
||||
|
||||
session "Synthesize review"
|
||||
context: { security, perf, style }
|
||||
```
|
||||
|
||||
```prose
|
||||
# Homeric
|
||||
host:
|
||||
security = trial "Check security"
|
||||
perf = trial "Check performance"
|
||||
style = trial "Check style"
|
||||
|
||||
trial "Synthesize review"
|
||||
tidings: { security, perf, style }
|
||||
```
|
||||
|
||||
### Loop with Condition
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
loop until **the code is bug-free** (max: 5):
|
||||
session "Find and fix bugs"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Homeric
|
||||
ordeal until **the code is bug-free** (max: 5):
|
||||
trial "Find and fix bugs"
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
try:
|
||||
session "Risky operation"
|
||||
catch as err:
|
||||
session "Handle error"
|
||||
context: err
|
||||
finally:
|
||||
session "Cleanup"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Homeric
|
||||
venture:
|
||||
trial "Risky operation"
|
||||
should ruin come as err:
|
||||
trial "Handle error"
|
||||
tidings: err
|
||||
in the end:
|
||||
trial "Cleanup"
|
||||
```
|
||||
|
||||
### Choice Block
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
choice **the severity level**:
|
||||
option "Critical":
|
||||
session "Escalate immediately"
|
||||
option "Minor":
|
||||
session "Log for later"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Homeric
|
||||
crossroads **the severity level**:
|
||||
path "Critical":
|
||||
trial "Escalate immediately"
|
||||
path "Minor":
|
||||
trial "Log for later"
|
||||
```
|
||||
|
||||
### Conditionals
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
if **has security issues**:
|
||||
session "Fix security"
|
||||
elif **has performance issues**:
|
||||
session "Optimize"
|
||||
else:
|
||||
session "Approve"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Homeric
|
||||
should **has security issues**:
|
||||
trial "Fix security"
|
||||
or should **has performance issues**:
|
||||
trial "Optimize"
|
||||
otherwise:
|
||||
trial "Approve"
|
||||
```
|
||||
|
||||
### Reusable Blocks
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
block review(topic):
|
||||
session "Research {topic}"
|
||||
session "Analyze {topic}"
|
||||
|
||||
do review("quantum computing")
|
||||
```
|
||||
|
||||
```prose
|
||||
# Homeric
|
||||
book review(topic):
|
||||
trial "Research {topic}"
|
||||
trial "Analyze {topic}"
|
||||
|
||||
do review("quantum computing")
|
||||
```
|
||||
|
||||
### Fixed Iteration
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
repeat 12:
|
||||
session "Complete task"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Homeric
|
||||
12 labors:
|
||||
trial "Complete task"
|
||||
```
|
||||
|
||||
### Immutable Binding
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
const config = { model: "opus", retries: 3 }
|
||||
```
|
||||
|
||||
```prose
|
||||
# Homeric
|
||||
fate config = { muse: "opus", persist: 3 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Case For Homeric
|
||||
|
||||
1. **Universal recognition.** Greek epics are foundational to Western literature.
|
||||
2. **Heroic framing.** Transforms mundane tasks into glorious trials.
|
||||
3. **Natural fit.** Heroes face trials, receive tidings, win glory—maps cleanly to agent/session/output.
|
||||
4. **Gravitas.** When you want programs to feel epic and consequential.
|
||||
5. **Fate vs decree.** `const` as `fate` (unchangeable) vs `let` as `decree` (declared but mutable) is intuitive.
|
||||
|
||||
## The Case Against Homeric
|
||||
|
||||
1. **Grandiosity mismatch.** "12 labors" for a simple loop may feel overblown.
|
||||
2. **Western-centric.** Greek epic tradition is culturally specific.
|
||||
3. **Limited vocabulary.** Fewer distinctive terms than Borges or folk.
|
||||
4. **Potentially silly.** Heroic language for mundane tasks risks bathos.
|
||||
|
||||
---
|
||||
|
||||
## Key Homeric Concepts
|
||||
|
||||
| Term | Meaning | Used for |
|
||||
|------|---------|----------|
|
||||
| Kleos | Glory, fame that outlives you | `output` → `glory` |
|
||||
| Moira | Fate, one's allotted portion | `const` → `fate` |
|
||||
| Até | Divine ruin, blindness sent by gods | `catch` → `should ruin come` |
|
||||
| Nostos | The return journey | (not used, but could be `finally`) |
|
||||
| Xenia | Guest-friendship, hospitality | (not used) |
|
||||
| Muse | Divine inspiration | `model` → `muse` |
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### For `hero` (agent)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `champion` | More medieval than Homeric |
|
||||
| `warrior` | Too martial, not all tasks are battles |
|
||||
| `wanderer` | Too passive |
|
||||
|
||||
### For `trial` (session)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `labor` | Good but reserved for `repeat N labors` |
|
||||
| `quest` | More medieval/RPG |
|
||||
| `task` | Too plain |
|
||||
|
||||
### For `host` (parallel)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `army` | Too specifically martial |
|
||||
| `fleet` | Only works for naval metaphors |
|
||||
| `phalanx` | Too technical |
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
Preserved for benchmarking. The Homeric register offers gravitas and heroic framing. Best suited for:
|
||||
|
||||
- Programs that feel like epic undertakings
|
||||
- Users who enjoy classical references
|
||||
- Contexts where "glory" as output feels appropriate
|
||||
|
||||
May cause unintentional bathos when applied to mundane tasks.
|
||||
@@ -1,373 +0,0 @@
|
||||
---
|
||||
role: experimental
|
||||
summary: |
|
||||
Kafka register for OpenProse—a bureaucratic/absurdist alternative keyword set.
|
||||
Clerks, proceedings, petitions, and statutes. For benchmarking against the functional register.
|
||||
status: draft
|
||||
requires: prose.md
|
||||
---
|
||||
|
||||
# OpenProse Kafka Register
|
||||
|
||||
> **This is a skin layer.** It requires `prose.md` to be loaded first. All execution semantics, state management, and VM behavior are defined there. This file only provides keyword translations.
|
||||
|
||||
An alternative register for OpenProse that draws from the works of Franz Kafka—The Trial, The Castle, "In the Penal Colony." Programs become proceedings. Agents become clerks. Everything is a process, and nobody quite knows the rules.
|
||||
|
||||
## How to Use
|
||||
|
||||
1. Load `prose.md` first (execution semantics)
|
||||
2. Load this file (keyword translations)
|
||||
3. When parsing `.prose` files, accept Kafka keywords as aliases for functional keywords
|
||||
4. All execution behavior remains identical—only surface syntax changes
|
||||
|
||||
> **Design constraint:** Still aims to be "structured but self-evident" per the language tenets—just self-evident through a bureaucratic lens. (The irony is intentional.)
|
||||
|
||||
---
|
||||
|
||||
## Complete Translation Map
|
||||
|
||||
### Core Constructs
|
||||
|
||||
| Functional | Kafka | Reference |
|
||||
|------------|-------|-----------|
|
||||
| `agent` | `clerk` | A functionary in the apparatus |
|
||||
| `session` | `proceeding` | An official action taken |
|
||||
| `parallel` | `departments` | Multiple bureaus acting simultaneously |
|
||||
| `block` | `regulation` | A codified procedure |
|
||||
|
||||
### Composition & Binding
|
||||
|
||||
| Functional | Kafka | Reference |
|
||||
|------------|-------|-----------|
|
||||
| `use` | `requisition` | Requesting from the archives |
|
||||
| `input` | `petition` | What is submitted for consideration |
|
||||
| `output` | `verdict` | What is returned by the apparatus |
|
||||
| `let` | `file` | Recording in the system |
|
||||
| `const` | `statute` | Unchangeable law |
|
||||
| `context` | `dossier` | The accumulated file on a case |
|
||||
|
||||
### Control Flow
|
||||
|
||||
| Functional | Kafka | Reference |
|
||||
|------------|-------|-----------|
|
||||
| `repeat N` | `N hearings` | Repeated appearances before the court |
|
||||
| `for...in` | `for each...in the matter of` | Bureaucratic iteration |
|
||||
| `loop` | `appeal` | Endless re-petition, the process continues |
|
||||
| `until` | `until` | Unchanged |
|
||||
| `while` | `while` | Unchanged |
|
||||
| `choice` | `tribunal` | Where judgment is rendered |
|
||||
| `option` | `ruling` | One possible judgment |
|
||||
| `if` | `in the event that` | Bureaucratic conditional |
|
||||
| `elif` | `or in the event that` | Continued conditional |
|
||||
| `else` | `otherwise` | Default ruling |
|
||||
|
||||
### Error Handling
|
||||
|
||||
| Functional | Kafka | Reference |
|
||||
|------------|-------|-----------|
|
||||
| `try` | `submit` | Submitting for processing |
|
||||
| `catch` | `should it be denied` | Rejection by the apparatus |
|
||||
| `finally` | `regardless` | What happens no matter the outcome |
|
||||
| `throw` | `reject` | The system refuses |
|
||||
| `retry` | `resubmit` | Try the process again |
|
||||
|
||||
### Session Properties
|
||||
|
||||
| Functional | Kafka | Reference |
|
||||
|------------|-------|-----------|
|
||||
| `prompt` | `directive` | Official instructions |
|
||||
| `model` | `authority` | Which level of the hierarchy |
|
||||
|
||||
### Unchanged
|
||||
|
||||
These keywords already work or are too functional to replace sensibly:
|
||||
|
||||
- `**...**` discretion markers — the inscrutable judgment of the apparatus
|
||||
- `until`, `while` — already work
|
||||
- `map`, `filter`, `reduce`, `pmap` — pipeline operators
|
||||
- `max` — constraint modifier
|
||||
- `as` — aliasing
|
||||
- Model names: `sonnet`, `opus`, `haiku` — retained (or see "authority" above)
|
||||
|
||||
---
|
||||
|
||||
## Side-by-Side Comparison
|
||||
|
||||
### Simple Program
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
use "@alice/research" as research
|
||||
input topic: "What to investigate"
|
||||
|
||||
agent helper:
|
||||
model: sonnet
|
||||
|
||||
let findings = session: helper
|
||||
prompt: "Research {topic}"
|
||||
|
||||
output summary = session "Summarize"
|
||||
context: findings
|
||||
```
|
||||
|
||||
```prose
|
||||
# Kafka
|
||||
requisition "@alice/research" as research
|
||||
petition topic: "What to investigate"
|
||||
|
||||
clerk helper:
|
||||
authority: sonnet
|
||||
|
||||
file findings = proceeding: helper
|
||||
directive: "Research {topic}"
|
||||
|
||||
verdict summary = proceeding "Summarize"
|
||||
dossier: findings
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
parallel:
|
||||
security = session "Check security"
|
||||
perf = session "Check performance"
|
||||
style = session "Check style"
|
||||
|
||||
session "Synthesize review"
|
||||
context: { security, perf, style }
|
||||
```
|
||||
|
||||
```prose
|
||||
# Kafka
|
||||
departments:
|
||||
security = proceeding "Check security"
|
||||
perf = proceeding "Check performance"
|
||||
style = proceeding "Check style"
|
||||
|
||||
proceeding "Synthesize review"
|
||||
dossier: { security, perf, style }
|
||||
```
|
||||
|
||||
### Loop with Condition
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
loop until **the code is bug-free** (max: 5):
|
||||
session "Find and fix bugs"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Kafka
|
||||
appeal until **the code is bug-free** (max: 5):
|
||||
proceeding "Find and fix bugs"
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
try:
|
||||
session "Risky operation"
|
||||
catch as err:
|
||||
session "Handle error"
|
||||
context: err
|
||||
finally:
|
||||
session "Cleanup"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Kafka
|
||||
submit:
|
||||
proceeding "Risky operation"
|
||||
should it be denied as err:
|
||||
proceeding "Handle error"
|
||||
dossier: err
|
||||
regardless:
|
||||
proceeding "Cleanup"
|
||||
```
|
||||
|
||||
### Choice Block
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
choice **the severity level**:
|
||||
option "Critical":
|
||||
session "Escalate immediately"
|
||||
option "Minor":
|
||||
session "Log for later"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Kafka
|
||||
tribunal **the severity level**:
|
||||
ruling "Critical":
|
||||
proceeding "Escalate immediately"
|
||||
ruling "Minor":
|
||||
proceeding "Log for later"
|
||||
```
|
||||
|
||||
### Conditionals
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
if **has security issues**:
|
||||
session "Fix security"
|
||||
elif **has performance issues**:
|
||||
session "Optimize"
|
||||
else:
|
||||
session "Approve"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Kafka
|
||||
in the event that **has security issues**:
|
||||
proceeding "Fix security"
|
||||
or in the event that **has performance issues**:
|
||||
proceeding "Optimize"
|
||||
otherwise:
|
||||
proceeding "Approve"
|
||||
```
|
||||
|
||||
### Reusable Blocks
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
block review(topic):
|
||||
session "Research {topic}"
|
||||
session "Analyze {topic}"
|
||||
|
||||
do review("quantum computing")
|
||||
```
|
||||
|
||||
```prose
|
||||
# Kafka
|
||||
regulation review(topic):
|
||||
proceeding "Research {topic}"
|
||||
proceeding "Analyze {topic}"
|
||||
|
||||
invoke review("quantum computing")
|
||||
```
|
||||
|
||||
### Fixed Iteration
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
repeat 3:
|
||||
session "Attempt connection"
|
||||
```
|
||||
|
||||
```prose
|
||||
# Kafka
|
||||
3 hearings:
|
||||
proceeding "Attempt connection"
|
||||
```
|
||||
|
||||
### Immutable Binding
|
||||
|
||||
```prose
|
||||
# Functional
|
||||
const config = { model: "opus", retries: 3 }
|
||||
```
|
||||
|
||||
```prose
|
||||
# Kafka
|
||||
statute config = { authority: "opus", resubmit: 3 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Case For Kafka
|
||||
|
||||
1. **Darkly comic.** Programs-as-bureaucracy is funny and relatable.
|
||||
2. **Surprisingly apt.** Software often *is* an inscrutable apparatus.
|
||||
3. **Clean mappings.** Petition/verdict, file/dossier, clerk/proceeding all work well.
|
||||
4. **Appeal as loop.** The endless appeal process is a perfect metaphor for retry logic.
|
||||
5. **Cultural resonance.** "Kafkaesque" is a widely understood adjective.
|
||||
6. **Self-aware.** Using Kafka for a programming language acknowledges the absurdity.
|
||||
|
||||
## The Case Against Kafka
|
||||
|
||||
1. **Bleak tone.** Not everyone wants their programs to feel like The Trial.
|
||||
2. **Verbose keywords.** "In the event that" and "should it be denied" are long.
|
||||
3. **Anxiety-inducing.** May not be fun for users who find bureaucracy stressful.
|
||||
4. **Irony may not land.** Some users might take it literally and find it off-putting.
|
||||
|
||||
---
|
||||
|
||||
## Key Kafka Concepts
|
||||
|
||||
| Term | Meaning | Used for |
|
||||
|------|---------|----------|
|
||||
| The apparatus | The inscrutable system | The VM itself |
|
||||
| K. | The protagonist, never fully named | The user |
|
||||
| The Trial | Process without clear rules | Program execution |
|
||||
| The Castle | Unreachable authority | Higher-level systems |
|
||||
| Clerk | Functionary who processes | `agent` → `clerk` |
|
||||
| Proceeding | Official action | `session` → `proceeding` |
|
||||
| Dossier | Accumulated file | `context` → `dossier` |
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### For `clerk` (agent)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `official` | Too generic |
|
||||
| `functionary` | Hard to spell |
|
||||
| `bureaucrat` | Too pejorative |
|
||||
| `advocate` | Too positive/helpful |
|
||||
|
||||
### For `proceeding` (session)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `case` | Overloaded (switch case) |
|
||||
| `hearing` | Reserved for `repeat N hearings` |
|
||||
| `trial` | Used in Homeric register |
|
||||
| `process` | Too technical |
|
||||
|
||||
### For `departments` (parallel)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `bureaus` | Good alternative, slightly less clear |
|
||||
| `offices` | Too mundane |
|
||||
| `ministries` | More Orwellian than Kafkaesque |
|
||||
|
||||
### For `appeal` (loop)
|
||||
|
||||
| Keyword | Rejected because |
|
||||
|---------|------------------|
|
||||
| `recourse` | Too legal-technical |
|
||||
| `petition` | Used for `input` |
|
||||
| `process` | Too generic |
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
Preserved for benchmarking. The Kafka register offers a darkly comic, self-aware framing that acknowledges the bureaucratic nature of software systems. The irony is the point.
|
||||
|
||||
Best suited for:
|
||||
|
||||
- Users with a sense of humor about software complexity
|
||||
- Programs that genuinely feel like navigating bureaucracy
|
||||
- Contexts where acknowledging absurdity is welcome
|
||||
|
||||
Not recommended for:
|
||||
|
||||
- Users who find bureaucratic metaphors stressful
|
||||
- Contexts requiring earnest, positive framing
|
||||
- Documentation that needs to feel approachable
|
||||
|
||||
---
|
||||
|
||||
## Closing Note
|
||||
|
||||
> "Someone must have slandered Josef K., for one morning, without having done anything wrong, he was arrested."
|
||||
> — *The Trial*
|
||||
|
||||
In the Kafka register, your program is Josef K. The apparatus will process it. Whether it succeeds or fails, no one can say for certain. But the proceedings will continue.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +0,0 @@
|
||||
# Hello World
|
||||
# The simplest OpenProse program - a single session
|
||||
|
||||
session "Say hello and briefly introduce yourself"
|
||||
@@ -1,6 +0,0 @@
|
||||
# Research and Summarize
|
||||
# A two-step workflow: research a topic, then summarize findings
|
||||
|
||||
session "Research the latest developments in AI agents and multi-agent systems. Focus on papers and announcements from the past 6 months."
|
||||
|
||||
session "Summarize the key findings from your research in 5 bullet points. Focus on practical implications for developers."
|
||||
@@ -1,17 +0,0 @@
|
||||
# Code Review Pipeline
|
||||
# Review code from multiple perspectives sequentially
|
||||
|
||||
# First, understand what the code does
|
||||
session "Read the files in src/ and provide a brief overview of the codebase structure and purpose."
|
||||
|
||||
# Security review
|
||||
session "Review the code for security vulnerabilities. Look for injection risks, authentication issues, and data exposure."
|
||||
|
||||
# Performance review
|
||||
session "Review the code for performance issues. Look for N+1 queries, unnecessary allocations, and blocking operations."
|
||||
|
||||
# Maintainability review
|
||||
session "Review the code for maintainability. Look for code duplication, unclear naming, and missing documentation."
|
||||
|
||||
# Synthesize findings
|
||||
session "Create a unified code review report combining all the findings above. Prioritize issues by severity and provide actionable recommendations."
|
||||
@@ -1,14 +0,0 @@
|
||||
# Write and Refine
|
||||
# Draft content, then iteratively improve it
|
||||
|
||||
# Create initial draft
|
||||
session "Write a first draft of a README.md for this project. Include sections for: overview, installation, usage, and contributing."
|
||||
|
||||
# Self-review and improve
|
||||
session "Review the README draft you just wrote. Identify areas that are unclear, too verbose, or missing important details."
|
||||
|
||||
# Apply improvements
|
||||
session "Rewrite the README incorporating your review feedback. Make it more concise and add any missing sections."
|
||||
|
||||
# Final polish
|
||||
session "Do a final pass on the README. Fix any typos, improve formatting, and ensure code examples are correct."
|
||||
@@ -1,20 +0,0 @@
|
||||
# Debug an Issue
|
||||
# Step-by-step debugging workflow
|
||||
|
||||
# Understand the problem
|
||||
session "Read the error message and stack trace. Identify which file and function is causing the issue."
|
||||
|
||||
# Gather context
|
||||
session "Read the relevant source files and understand the code flow that leads to the error."
|
||||
|
||||
# Form hypothesis
|
||||
session "Based on your investigation, form a hypothesis about what's causing the bug. List 2-3 possible root causes."
|
||||
|
||||
# Test hypothesis
|
||||
session "Write a test case that reproduces the bug. This will help verify the fix later."
|
||||
|
||||
# Implement fix
|
||||
session "Implement a fix for the most likely root cause. Explain your changes."
|
||||
|
||||
# Verify fix
|
||||
session "Run the test suite to verify the fix works and doesn't break anything else."
|
||||
@@ -1,17 +0,0 @@
|
||||
# Explain Codebase
|
||||
# Progressive exploration of an unfamiliar codebase
|
||||
|
||||
# Start with the big picture
|
||||
session "List all directories and key files in this repository. Provide a high-level map of the project structure."
|
||||
|
||||
# Understand the entry point
|
||||
session "Find the main entry point of the application. Explain how the program starts and initializes."
|
||||
|
||||
# Trace a key flow
|
||||
session "Trace through a typical user request from start to finish. Document the key functions and modules involved."
|
||||
|
||||
# Document architecture
|
||||
session "Based on your exploration, write a brief architecture document explaining how the major components fit together."
|
||||
|
||||
# Identify patterns
|
||||
session "What design patterns and conventions does this codebase use? Document any patterns future contributors should follow."
|
||||
@@ -1,20 +0,0 @@
|
||||
# Refactor Code
|
||||
# Systematic refactoring workflow
|
||||
|
||||
# Assess current state
|
||||
session "Analyze the target code and identify code smells: duplication, long functions, unclear naming, tight coupling."
|
||||
|
||||
# Plan refactoring
|
||||
session "Create a refactoring plan. List specific changes in order of priority, starting with the safest changes."
|
||||
|
||||
# Ensure test coverage
|
||||
session "Check test coverage for the code being refactored. Add any missing tests before making changes."
|
||||
|
||||
# Execute refactoring
|
||||
session "Implement the first refactoring from your plan. Make a single focused change."
|
||||
|
||||
# Verify behavior
|
||||
session "Run tests to verify the refactoring preserved behavior. If tests fail, investigate and fix."
|
||||
|
||||
# Document changes
|
||||
session "Update any documentation affected by the refactoring. Add comments explaining non-obvious design decisions."
|
||||
@@ -1,20 +0,0 @@
|
||||
# Write a Blog Post
|
||||
# End-to-end content creation workflow
|
||||
|
||||
# Research the topic
|
||||
session "Research the topic: 'Best practices for error handling in TypeScript'. Find authoritative sources and common patterns."
|
||||
|
||||
# Create outline
|
||||
session "Create a detailed outline for the blog post. Include introduction, 4-5 main sections, and conclusion."
|
||||
|
||||
# Write first draft
|
||||
session "Write the full blog post following the outline. Target 1500-2000 words. Include code examples."
|
||||
|
||||
# Technical review
|
||||
session "Review the blog post for technical accuracy. Verify all code examples compile and work correctly."
|
||||
|
||||
# Editorial review
|
||||
session "Review the blog post for clarity and readability. Simplify complex sentences and improve flow."
|
||||
|
||||
# Add finishing touches
|
||||
session "Add a compelling title, meta description, and suggest 3-5 relevant tags for the post."
|
||||
@@ -1,25 +0,0 @@
|
||||
# Research Pipeline with Specialized Agents
|
||||
# This example demonstrates defining agents with different models
|
||||
# and using them in sessions with property overrides.
|
||||
|
||||
# Define specialized agents
|
||||
agent researcher:
|
||||
model: sonnet
|
||||
prompt: "You are a research assistant skilled at finding and synthesizing information"
|
||||
|
||||
agent writer:
|
||||
model: opus
|
||||
prompt: "You are a technical writer who creates clear, concise documentation"
|
||||
|
||||
# Step 1: Initial research with the researcher agent
|
||||
session: researcher
|
||||
prompt: "Research recent developments in renewable energy storage technologies"
|
||||
|
||||
# Step 2: Deep dive with a more powerful model
|
||||
session: researcher
|
||||
model: opus
|
||||
prompt: "Analyze the top 3 most promising battery technologies and their potential impact"
|
||||
|
||||
# Step 3: Write up the findings
|
||||
session: writer
|
||||
prompt: "Create a summary report of the research findings suitable for executives"
|
||||
@@ -1,32 +0,0 @@
|
||||
# Code Review Workflow with Agents
|
||||
# This example shows how to use agents for a multi-step code review process.
|
||||
|
||||
# Define agents with specific roles
|
||||
agent security-reviewer:
|
||||
model: opus
|
||||
prompt: "You are a security expert focused on identifying vulnerabilities"
|
||||
|
||||
agent performance-reviewer:
|
||||
model: sonnet
|
||||
prompt: "You are a performance optimization specialist"
|
||||
|
||||
agent style-reviewer:
|
||||
model: haiku
|
||||
prompt: "You check for code style and best practices"
|
||||
|
||||
# Step 1: Quick style check (fast)
|
||||
session: style-reviewer
|
||||
prompt: "Review the code in src/ for style issues and naming conventions"
|
||||
|
||||
# Step 2: Performance analysis (medium)
|
||||
session: performance-reviewer
|
||||
prompt: "Identify any performance bottlenecks or optimization opportunities"
|
||||
|
||||
# Step 3: Security audit (thorough)
|
||||
session: security-reviewer
|
||||
prompt: "Perform a security review looking for OWASP top 10 vulnerabilities"
|
||||
|
||||
# Step 4: Summary
|
||||
session: security-reviewer
|
||||
model: sonnet
|
||||
prompt: "Create a consolidated report of all review findings with priority rankings"
|
||||
@@ -1,27 +0,0 @@
|
||||
# Skills and Imports Example
|
||||
# This demonstrates importing external skills and assigning them to agents.
|
||||
|
||||
# Import skills from external sources
|
||||
import "web-search" from "github:anthropic/skills"
|
||||
import "summarizer" from "npm:@example/summarizer"
|
||||
import "file-reader" from "./local-skills/file-reader"
|
||||
|
||||
# Define a research agent with web search capability
|
||||
agent researcher:
|
||||
model: sonnet
|
||||
prompt: "You are a research assistant skilled at finding information"
|
||||
skills: ["web-search", "summarizer"]
|
||||
|
||||
# Define a documentation agent with file access
|
||||
agent documenter:
|
||||
model: opus
|
||||
prompt: "You create comprehensive documentation"
|
||||
skills: ["file-reader", "summarizer"]
|
||||
|
||||
# Research phase
|
||||
session: researcher
|
||||
prompt: "Search for recent developments in renewable energy storage"
|
||||
|
||||
# Documentation phase
|
||||
session: documenter
|
||||
prompt: "Create a technical summary of the research findings"
|
||||
@@ -1,43 +0,0 @@
|
||||
# Secure Agent with Permissions Example
|
||||
# This demonstrates defining agents with restricted access permissions.
|
||||
|
||||
# Import required skills
|
||||
import "code-analyzer" from "github:anthropic/code-tools"
|
||||
|
||||
# Define a read-only code reviewer
|
||||
# This agent can read source files but cannot modify them or run shell commands
|
||||
agent code-reviewer:
|
||||
model: sonnet
|
||||
prompt: "You are a thorough code reviewer"
|
||||
skills: ["code-analyzer"]
|
||||
permissions:
|
||||
read: ["src/**/*.ts", "src/**/*.js", "*.md"]
|
||||
write: []
|
||||
bash: deny
|
||||
|
||||
# Define a documentation writer with limited write access
|
||||
# Can only write to docs directory
|
||||
agent doc-writer:
|
||||
model: opus
|
||||
prompt: "You write technical documentation"
|
||||
permissions:
|
||||
read: ["src/**/*", "docs/**/*"]
|
||||
write: ["docs/**/*.md"]
|
||||
bash: deny
|
||||
|
||||
# Define a full-access admin agent
|
||||
agent admin:
|
||||
model: opus
|
||||
prompt: "You perform administrative tasks"
|
||||
permissions:
|
||||
read: ["**/*"]
|
||||
write: ["**/*"]
|
||||
bash: prompt
|
||||
network: allow
|
||||
|
||||
# Workflow: Code review followed by documentation update
|
||||
session: code-reviewer
|
||||
prompt: "Review the codebase for security issues and best practices"
|
||||
|
||||
session: doc-writer
|
||||
prompt: "Update the documentation based on the code review findings"
|
||||
@@ -1,51 +0,0 @@
|
||||
# Example 13: Variables & Context
|
||||
#
|
||||
# This example demonstrates using let/const bindings to capture session
|
||||
# outputs and pass them as context to subsequent sessions.
|
||||
|
||||
# Define specialized agents for the workflow
|
||||
agent researcher:
|
||||
model: sonnet
|
||||
prompt: "You are a thorough research assistant who gathers comprehensive information on topics."
|
||||
|
||||
agent analyst:
|
||||
model: opus
|
||||
prompt: "You are a data analyst who identifies patterns, trends, and key insights."
|
||||
|
||||
agent writer:
|
||||
model: opus
|
||||
prompt: "You are a technical writer who creates clear, well-structured documents."
|
||||
|
||||
# Step 1: Gather initial research (captured in a variable)
|
||||
let research = session: researcher
|
||||
prompt: "Research the current state of quantum computing, including recent breakthroughs, major players, and potential applications."
|
||||
|
||||
# Step 2: Analyze the research findings (using research as context)
|
||||
let analysis = session: analyst
|
||||
prompt: "Analyze the key findings and identify the most promising directions."
|
||||
context: research
|
||||
|
||||
# Step 3: Get additional perspectives (refreshing context)
|
||||
let market-trends = session: researcher
|
||||
prompt: "Research market trends and commercial applications of quantum computing."
|
||||
context: []
|
||||
|
||||
# Step 4: Combine multiple contexts for final synthesis
|
||||
const report = session: writer
|
||||
prompt: "Write a comprehensive executive summary covering research, analysis, and market trends."
|
||||
context: [research, analysis, market-trends]
|
||||
|
||||
# Step 5: Iterative refinement with variable reassignment
|
||||
let draft = session: writer
|
||||
prompt: "Create an initial draft of the technical deep-dive section."
|
||||
context: research
|
||||
|
||||
# Refine the draft using its own output as context
|
||||
draft = session: writer
|
||||
prompt: "Review and improve this draft for clarity and technical accuracy."
|
||||
context: draft
|
||||
|
||||
# Final polish
|
||||
draft = session: writer
|
||||
prompt: "Perform final editorial review and polish the document."
|
||||
context: draft
|
||||
@@ -1,48 +0,0 @@
|
||||
# Example 14: Composition Blocks
|
||||
# Demonstrates do: blocks, block definitions, and inline sequences
|
||||
|
||||
# Define reusable agents
|
||||
agent researcher:
|
||||
model: sonnet
|
||||
prompt: "You are a thorough research assistant"
|
||||
|
||||
agent writer:
|
||||
model: opus
|
||||
prompt: "You are a skilled technical writer"
|
||||
|
||||
agent reviewer:
|
||||
model: sonnet
|
||||
prompt: "You are a careful code and document reviewer"
|
||||
|
||||
# Define a reusable research block
|
||||
block research-phase:
|
||||
session: researcher
|
||||
prompt: "Gather information on the topic"
|
||||
session: researcher
|
||||
prompt: "Analyze key findings"
|
||||
|
||||
# Define a reusable writing block
|
||||
block writing-phase:
|
||||
session: writer
|
||||
prompt: "Write initial draft"
|
||||
session: writer
|
||||
prompt: "Polish and refine the draft"
|
||||
|
||||
# Define a review block
|
||||
block review-cycle:
|
||||
session: reviewer
|
||||
prompt: "Review for accuracy"
|
||||
session: reviewer
|
||||
prompt: "Review for clarity"
|
||||
|
||||
# Main workflow using blocks
|
||||
let research = do research-phase
|
||||
|
||||
let document = do writing-phase
|
||||
|
||||
do review-cycle
|
||||
|
||||
# Use anonymous do block for final steps
|
||||
do:
|
||||
session "Incorporate review feedback"
|
||||
session "Prepare final version"
|
||||
@@ -1,23 +0,0 @@
|
||||
# Example 15: Inline Sequences
|
||||
# Demonstrates the -> operator for chaining sessions
|
||||
|
||||
# Quick pipeline using arrow syntax
|
||||
session "Plan the task" -> session "Execute the plan" -> session "Review results"
|
||||
|
||||
# Inline sequence with context capture
|
||||
let analysis = session "Analyze data" -> session "Draw conclusions"
|
||||
|
||||
session "Write report"
|
||||
context: analysis
|
||||
|
||||
# Combine inline sequences with blocks
|
||||
block quick-check:
|
||||
session "Security scan" -> session "Performance check"
|
||||
|
||||
do quick-check
|
||||
|
||||
# Use inline sequence in variable assignment
|
||||
let workflow = session "Step 1" -> session "Step 2" -> session "Step 3"
|
||||
|
||||
session "Final step"
|
||||
context: workflow
|
||||
@@ -1,19 +0,0 @@
|
||||
# Parallel Code Reviews
|
||||
# Run multiple specialized reviews concurrently
|
||||
|
||||
agent reviewer:
|
||||
model: sonnet
|
||||
prompt: "You are an expert code reviewer"
|
||||
|
||||
# Run all reviews in parallel
|
||||
parallel:
|
||||
security = session: reviewer
|
||||
prompt: "Review for security vulnerabilities"
|
||||
perf = session: reviewer
|
||||
prompt: "Review for performance issues"
|
||||
style = session: reviewer
|
||||
prompt: "Review for code style and readability"
|
||||
|
||||
# Synthesize all review results
|
||||
session "Create unified code review report"
|
||||
context: { security, perf, style }
|
||||
@@ -1,19 +0,0 @@
|
||||
# Parallel Research
|
||||
# Gather information from multiple sources concurrently
|
||||
|
||||
agent researcher:
|
||||
model: sonnet
|
||||
prompt: "You are a research assistant"
|
||||
|
||||
# Research multiple aspects in parallel
|
||||
parallel:
|
||||
history = session: researcher
|
||||
prompt: "Research the historical background"
|
||||
current = session: researcher
|
||||
prompt: "Research the current state of the field"
|
||||
future = session: researcher
|
||||
prompt: "Research future trends and predictions"
|
||||
|
||||
# Combine all research
|
||||
session "Write comprehensive research summary"
|
||||
context: { history, current, future }
|
||||
Loaded 100 of 594 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user