skimpyclaw 0.3.15 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +100 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +348 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # SkimpyClaw 👙🦞
2
2
 
3
- Lightweight personal AI assistant (~23k LOC). Runs locally. Telegram/Discord chat, scheduled routines, a web dashboard, and a tool-enabled agent — all in one tiny service.
3
+ Lightweight personal AI assistant (~21k LOC). Runs locally. Telegram/Discord chat, scheduled routines, a web dashboard, and a tool-enabled agent — all in one tiny service.
4
4
 
5
5
  ## Why SkimpyClaw vs OpenClaw
6
6
 
7
7
  Both are personal AI assistants you run yourself. The difference is scope.
8
8
 
9
- | | SkimpyClaw (~23k LOC) | OpenClaw (~700k LOC) |
9
+ | | SkimpyClaw (~21k LOC) | OpenClaw (~700k LOC) |
10
10
  | ------------------- | --------------------------------------- | -------------------------------------------------------- |
11
11
  | **Channels** | Telegram, Discord | WhatsApp, Signal, iMessage, Slack, Teams, Matrix, + more |
12
12
  | **Setup** | `skimpyclaw onboard` → done | Daemon + wizard + per-channel pairing |
13
13
  | **Codebase** | Read it in an afternoon | Full platform with extensions, packages, native UI |
14
- | **Model support** | Anthropic, OpenAI, Kimi, MiniMax, Codex | Same + more |
14
+ | **Model support** | Anthropic, Codex | Same + more |
15
15
  | **Release cadence** | Move fast, no stability guarantees | Stable / beta / dev channels |
16
16
 
17
17
  Use SkimpyClaw if you live in Telegram or Discord, want to read and own every line, and don't need 13 channels. Use OpenClaw if you need to support multiple channels like WhatsApp, Signal, iMessage, or Slack — or want a more polished, maintained platform.
@@ -19,10 +19,10 @@ Use SkimpyClaw if you live in Telegram or Discord, want to read and own every li
19
19
  ## Features
20
20
 
21
21
  - **Chat interface** — Telegram and Discord bots with persistent conversation history
22
- - **Tool-enabled agent** — file read/write, bash, browser (Playwright), MCP tools via mcporter
22
+ - **Tool-enabled agent** — file read/write, bash, fetch, MCP tools via mcporter
23
23
  - **Multi-modal support** — voice messages (STT/TTS), image analysis
24
- - **Coding agents** — delegate to Claude Code / Codex CLI; `code_with_team` for parallel multi-agent work
25
- - **Code agents** — delegate coding tasks to Claude Code, Codex, or Kimi CLI with `code_with_agent` and `code_with_team`
24
+ - **Agents** — configure multiple local agents with their own identity, prompts, model, effort, and memory; Discord adds reusable `/agent` profiles and `@alias <message>` shortcuts
25
+ - **Coding agents** — delegate coding tasks to Claude Code or Codex with `code_with_agent`; Discord threads can also run bidirectional interactive Claude sessions that `--resume` the same process
26
26
  - **Cron scheduler** — run agent prompts or shell scripts on a schedule
27
27
  - **Web dashboard** — Preact/Vite SPA with status, cron, audit log, memory, templates, config editor, skills, approvals
28
28
  - **Heartbeat** — periodic keep-alive with Telegram/Discord alerts
@@ -31,15 +31,21 @@ Use SkimpyClaw if you live in Telegram or Discord, want to read and own every li
31
31
  - **Voice** — optional TTS/STT support (ElevenLabs, OpenAI, macOS `say`, local Whisper)
32
32
  - **Observability** — optional Langfuse tracing per agent turn
33
33
  - **Audit logging** — JSONL-based audit traces for all agent interactions
34
- - **Multiple model providers** — Anthropic, OpenAI, Kimi, MiniMax, Codex (ChatGPT backend), any OpenAI-compatible API
34
+ - **Model providers** — Anthropic and Codex (ChatGPT backend)
35
35
 
36
36
  ## Architecture
37
37
 
38
+ SkimpyClaw routes all model calls through a provider adapter registry in `src/providers/index.ts`.
39
+
40
+ - `chat()` and `chatWithTools()` both resolve provider/model and dispatch to a `ProviderAdapter`.
41
+ - Adapters implement `isAvailable()`, `chat()`, and `chatWithTools()` behavior via a shared interface.
42
+ - Tool-enabled turns use one common execution path: `runToolLoop()` in `src/providers/tool-loop.ts`.
43
+ - Context compaction is unified through shared context-management utilities used by all adapters.
44
+
38
45
  ```mermaid
39
46
  flowchart LR
40
47
  subgraph Channels
41
48
  user["Telegram / Discord"]
42
- browser["Browser"]
43
49
  end
44
50
 
45
51
  subgraph Gateway["Fastify :18790"]
@@ -49,6 +55,9 @@ flowchart LR
49
55
 
50
56
  subgraph Core
51
57
  agent["Agent Runtime"]
58
+ registry["Provider Registry"]
59
+ adapters["Provider Adapters"]
60
+ loop["runToolLoop()"]
52
61
  codeAgents["Coding Agents"]
53
62
  cron["Cron"]
54
63
  hb["Heartbeat"]
@@ -58,19 +67,21 @@ flowchart LR
58
67
  end
59
68
 
60
69
  user --> agent
61
- browser --> dash --> api --> agent
70
+ dash --> api --> agent
62
71
  cron --> agent
63
72
  hb --> agent
64
73
  agent --> codeAgents
65
74
  agent --> audit
66
75
  agent --> skills
67
76
  agent --> approvals
68
- agent --> models["Anthropic / OpenAI / Codex"]
77
+ agent --> registry --> adapters
78
+ adapters --> loop
79
+ adapters --> models["Anthropic / Codex"]
69
80
  agent --> mcp["MCP Servers"]
70
81
  agent --> fs["~/.skimpyclaw"]
71
82
  ```
72
83
 
73
- See [docs/architecture.md](docs/architecture.md) for runtime flow and startup sequence diagrams.
84
+ See [docs/guide/architecture.md](docs/guide/architecture.md) for runtime flow and startup sequence diagrams.
74
85
 
75
86
  ## Quick Start
76
87
 
@@ -85,16 +96,6 @@ Onboarding validates your Telegram token, provider auth, and creates:
85
96
  - `~/.skimpyclaw/config.json`
86
97
  - `~/.skimpyclaw/agents/main/*.md` (from templates)
87
98
 
88
- **Sandbox setup (optional — isolates Bash commands in a container):**
89
-
90
- ```bash
91
- skimpyclaw sandbox init # detect runtime, build image, update config
92
- skimpyclaw sandbox doctor # verify everything works
93
- skimpyclaw restart # pick up sandbox config
94
- ```
95
-
96
- Requires Apple Containers (macOS 26+) or Docker. See [docs/guide/sandbox.md](docs/guide/sandbox.md) for options.
97
-
98
99
  **Stop/Restart daemon (macOS):**
99
100
 
100
101
  ```bash
@@ -124,13 +125,18 @@ http://127.0.0.1:18790/dashboard
124
125
 
125
126
  Bearer token is shown in startup logs.
126
127
 
128
+ ## Security
129
+
130
+ SkimpyClaw uses defense-in-depth for local execution: strict config/secrets handling, bearer-authenticated control endpoints, constrained tool paths, SSRF protections, and safer command execution defaults.
131
+
132
+ For the full security model and controls, see [docs/guide/security.md](docs/guide/security.md).
133
+
127
134
  ## Tech Stack
128
135
 
129
136
  - **Backend:** TypeScript (ESM), Fastify, Vitest
130
137
  - **Frontend:** Preact, Vite, TypeScript
131
138
  - **Chat:** grammy (Telegram), discord.js (Discord)
132
139
  - **Scheduling:** Croner
133
- - **Browser:** Playwright
134
140
  - **AI SDKs:** Anthropic SDK, OpenAI SDK
135
141
  - **Observability:** Langfuse (optional)
136
142
 
@@ -140,12 +146,12 @@ Bearer token is shown in startup logs.
140
146
  src/
141
147
  index.ts # App entrypoint with logging setup
142
148
  gateway.ts # Fastify server + top-level routes
143
- agent.ts # Prompt assembly, model routing, tool loop, memory writes
149
+ agent.ts # Prompt assembly, runAgentTurn orchestration, memory writes
144
150
  tools.ts # Tool registry + dispatch
145
- tools/ # Tool executors (bash, browser, file tools, path utils, execute context)
146
- providers/ # Provider routing + provider implementations (anthropic/openai/codex)
147
- code-agents/ # Background coding-agent runtime (executor/parser/orchestrator/registry)
148
- channels/ # Channel adapters/utilities (telegram/discord)
151
+ tools/ # Tool executors (bash, fetch, file tools, path utils, execute context)
152
+ providers/ # Provider registry, adapters, unified tool loop, provider implementations
153
+ code-agents/ # Background coding-agent runtime (executor/parser/registry + interactive sessions)
154
+ channels/ # Channel adapters/utilities (telegram/discord, Discord agent profile routing)
149
155
  file-lock.ts # In-memory file lock for concurrent writes
150
156
  audit.ts # Append-only audit log (trace/event model, JSONL storage)
151
157
  cron.ts # Job scheduling + execution + cron logging
@@ -202,17 +208,21 @@ dist/ # Compiled output + built dashboard assets
202
208
 
203
209
  | Doc | Contents |
204
210
  | ---------------------------------------------- | ---------------------------------------------------------------- |
205
- | [docs/architecture.md](docs/architecture.md) | Component diagram, runtime flow, startup sequence, source layout |
206
- | [docs/configuration.md](docs/configuration.md) | Full config reference, all sections with examples |
207
- | [docs/tools.md](docs/tools.md) | Built-in tools, browser tool, MCP integration, code agents |
208
- | [docs/dashboard.md](docs/dashboard.md) | Web dashboard, all HTTP endpoints + API routes |
209
- | [docs/coding-agents.md](docs/coding-agents.md) | Coding-agent execution model and CLI backends |
210
- | [docs/cli.md](docs/cli.md) | CLI commands, service management |
211
- | [docs/chat-commands.md](docs/chat-commands.md) | Telegram/Discord bot commands |
212
- | [docs/skills.md](docs/skills.md) | Skills system, built-in skills, creating custom skills |
213
- | [docs/data-storage.md](docs/data-storage.md) | File layout, audit log format, security notes |
211
+ | [docs/guide/architecture.md](docs/guide/architecture.md) | Component diagram, runtime flow, startup sequence, source layout |
212
+ | [docs/guide/configuration.md](docs/guide/configuration.md) | Full config reference, all sections with examples |
213
+ | [docs/guide/security.md](docs/guide/security.md) | Security model and runtime safeguards |
214
+ | [docs/guide/tools.md](docs/guide/tools.md) | Built-in tools, fetch, MCP integration, code agents |
215
+ | [docs/guide/dashboard.md](docs/guide/dashboard.md) | Web dashboard, all HTTP endpoints + API routes |
216
+ | [docs/guide/agents.md](docs/guide/agents.md) | Core agents, prompt directories, Discord profile aliases |
217
+ | [docs/guide/coding-agents.md](docs/guide/coding-agents.md) | Coding-agent execution model and CLI backends |
218
+ | [docs/guide/cli.md](docs/guide/cli.md) | CLI commands, service management |
219
+ | [docs/guide/chat-commands.md](docs/guide/chat-commands.md) | Telegram/Discord bot commands |
220
+ | [docs/guide/discord-updates.md](docs/guide/discord-updates.md) | Discord workflow documentation maintenance process |
221
+ | [docs/guide/changelog.md](docs/guide/changelog.md) | Documentation and behavior changelog |
222
+ | [docs/guide/skills.md](docs/guide/skills.md) | Skills system, built-in skills, creating custom skills |
223
+ | [docs/guide/data-storage.md](docs/guide/data-storage.md) | File layout, audit log format, security notes |
214
224
  | [Setup Guide](https://docs.skimpyclaw.xyz/guide/setup-guide.html) | Step-by-step installation and setup guide |
215
- | [docs/troubleshooting.md](docs/troubleshooting.md) | Common issues and solutions |
225
+ | [docs/guide/troubleshooting.md](docs/guide/troubleshooting.md) | Common issues and solutions |
216
226
 
217
227
  ## Development
218
228
 
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Compile-time validation tests for adapter interfaces.
3
+ */
4
+ export {};
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Compile-time validation tests for adapter interfaces.
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { buildToolLogEntry, logIteration } from '../providers/loop-utils.js';
6
+ describe('ProviderAdapter interface', () => {
7
+ it('should define correct structure for NormalizedResponse', () => {
8
+ const response = {
9
+ hasToolCalls: false,
10
+ toolCalls: [],
11
+ textContent: 'test response',
12
+ usage: { inputTokens: 100, outputTokens: 50 },
13
+ rawResponse: {},
14
+ };
15
+ expect(response.hasToolCalls).toBe(false);
16
+ expect(response.textContent).toBe('test response');
17
+ });
18
+ it('should define correct structure for NormalizedToolCall', () => {
19
+ const toolCall = {
20
+ id: 'call_123',
21
+ name: 'testTool',
22
+ args: { param: 'value' },
23
+ rawArgs: '{"param":"value"}',
24
+ };
25
+ expect(toolCall.name).toBe('testTool');
26
+ expect(toolCall.args.param).toBe('value');
27
+ });
28
+ it('should define correct structure for ProviderMessages', () => {
29
+ const messages = {
30
+ messages: [{ role: 'user', content: 'test' }],
31
+ systemParam: 'system prompt',
32
+ };
33
+ expect(messages.messages).toHaveLength(1);
34
+ expect(messages.systemParam).toBe('system prompt');
35
+ });
36
+ it('should define correct structure for CompactionResult', () => {
37
+ const result = {
38
+ messages: [],
39
+ compacted: true,
40
+ method: 'llm',
41
+ };
42
+ expect(result.compacted).toBe(true);
43
+ expect(result.method).toBe('llm');
44
+ });
45
+ });
46
+ describe('loop-utils', () => {
47
+ it('buildToolLogEntry should format tool calls correctly', () => {
48
+ const log = buildToolLogEntry('Read', 'file.txt', 'file contents here');
49
+ expect(log).toContain('Read');
50
+ expect(log).toContain('file.txt');
51
+ expect(log).toContain('file contents here');
52
+ });
53
+ it('buildToolLogEntry should truncate long inputs and outputs', () => {
54
+ const longInput = 'a'.repeat(200);
55
+ const longOutput = 'b'.repeat(300);
56
+ const log = buildToolLogEntry('Write', longInput, longOutput);
57
+ expect(log.length).toBeLessThan(320);
58
+ expect(log).toContain('...');
59
+ });
60
+ it('logIteration should not throw', () => {
61
+ expect(() => logIteration('test', 0, 10, 'model-id')).not.toThrow();
62
+ });
63
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tests for the Anthropic provider adapter.
3
+ */
4
+ export {};
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Tests for the Anthropic provider adapter.
3
+ */
4
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
5
+ import { AnthropicAdapter } from '../providers/adapters/anthropic-adapter.js';
6
+ // Mock the Anthropic client - create a single shared mock
7
+ const mockMessagesCreate = vi.fn();
8
+ const mockMessagesStream = vi.fn();
9
+ vi.mock('../providers/anthropic.js', () => ({
10
+ getAnthropicClient: vi.fn(() => ({
11
+ messages: {
12
+ create: mockMessagesCreate,
13
+ stream: mockMessagesStream,
14
+ },
15
+ })),
16
+ }));
17
+ // Mock utils
18
+ vi.mock('../providers/utils.js', () => ({
19
+ buildSystemParam: vi.fn((content, cache) => ({ content, cache })),
20
+ addToolCacheBreakpoint: vi.fn((defs) => defs),
21
+ contentToText: vi.fn((content) => String(content)),
22
+ stripProvider: vi.fn((model) => model.replace(/^[^/]+\//, '')),
23
+ buildThinkingConfig: vi.fn(() => null),
24
+ }));
25
+ // Mock context manager
26
+ vi.mock('../providers/context-manager.js', () => ({
27
+ compactMessages: vi.fn(async (messages) => ({
28
+ messages,
29
+ compacted: false,
30
+ })),
31
+ anthropicFormatHelper: {
32
+ isToolResult: () => false,
33
+ truncateToolResult: (item) => item,
34
+ serialize: () => '',
35
+ buildSummaryMessage: (s) => ({ role: 'user', content: [{ type: 'text', text: s }] }),
36
+ },
37
+ }));
38
+ // Mock observability
39
+ vi.mock('../providers/observability.js', () => ({
40
+ toCostDetails: vi.fn(() => ({ input: 0.001, output: 0.002, total: 0.003 })),
41
+ }));
42
+ // Mock usage tracking
43
+ vi.mock('../usage.js', () => ({
44
+ buildUsageRecord: vi.fn((opts) => opts),
45
+ recordUsage: vi.fn(),
46
+ }));
47
+ describe('AnthropicAdapter', () => {
48
+ let adapter;
49
+ let config;
50
+ let options;
51
+ beforeEach(async () => {
52
+ adapter = new AnthropicAdapter();
53
+ config = {
54
+ gateway: { port: 18790, mode: 'local' },
55
+ agents: { default: 'main', list: {} },
56
+ models: { providers: {}, aliases: {}, promptCaching: true },
57
+ channels: {
58
+ telegram: {
59
+ enabled: false,
60
+ token: 'test-token',
61
+ allowFrom: [],
62
+ }
63
+ },
64
+ cron: { jobs: [] },
65
+ heartbeat: { intervalMs: 300000, prompt: 'HEARTBEAT' },
66
+ };
67
+ options = {
68
+ model: 'anthropic/claude-opus-4',
69
+ maxTokens: 1000,
70
+ };
71
+ // Clear mock call history
72
+ mockMessagesCreate.mockClear();
73
+ mockMessagesStream.mockClear();
74
+ const { recordUsage } = await import('../usage.js');
75
+ vi.mocked(recordUsage).mockClear();
76
+ });
77
+ describe('buildMessages', () => {
78
+ it('should extract system message and build API messages', () => {
79
+ const messages = [
80
+ { role: 'system', content: 'You are helpful' },
81
+ { role: 'user', content: 'Hello' },
82
+ { role: 'assistant', content: 'Hi!' },
83
+ ];
84
+ const result = adapter.buildMessages(messages, options, config);
85
+ expect(result.messages).toHaveLength(2); // system excluded
86
+ expect(result.messages[0].role).toBe('user');
87
+ expect(result.messages[1].role).toBe('assistant');
88
+ expect(result.systemParam).toBeDefined();
89
+ });
90
+ it('should handle missing system message', () => {
91
+ const messages = [
92
+ { role: 'user', content: 'Hello' },
93
+ ];
94
+ const result = adapter.buildMessages(messages, options, config);
95
+ expect(result.messages).toHaveLength(1);
96
+ });
97
+ });
98
+ describe('buildToolDefs', () => {
99
+ it('should return tool definitions with cache breakpoints when caching enabled', async () => {
100
+ const { addToolCacheBreakpoint } = await import('../providers/utils.js');
101
+ vi.mocked(addToolCacheBreakpoint).mockClear();
102
+ const toolDefs = [
103
+ { name: 'Read', description: 'Read file' },
104
+ { name: 'Write', description: 'Write file' },
105
+ ];
106
+ const result = adapter.buildToolDefs(toolDefs, config);
107
+ expect(result).toHaveLength(2);
108
+ expect(result[0].name).toBe('Read');
109
+ expect(addToolCacheBreakpoint).toHaveBeenCalled();
110
+ });
111
+ it('should skip cache breakpoints when promptCaching is false', async () => {
112
+ const { addToolCacheBreakpoint } = await import('../providers/utils.js');
113
+ vi.mocked(addToolCacheBreakpoint).mockClear();
114
+ const noCacheConfig = {
115
+ ...config,
116
+ models: { ...config.models, promptCaching: false },
117
+ };
118
+ const toolDefs = [{ name: 'Read', description: 'Read file' }];
119
+ adapter.buildToolDefs(toolDefs, noCacheConfig);
120
+ expect(addToolCacheBreakpoint).not.toHaveBeenCalled();
121
+ });
122
+ });
123
+ describe('call', () => {
124
+ it('should normalize Anthropic response without tool calls', async () => {
125
+ mockMessagesCreate.mockResolvedValue({
126
+ content: [{ type: 'text', text: 'Hello!' }],
127
+ stop_reason: 'end_turn',
128
+ usage: { input_tokens: 100, output_tokens: 50 },
129
+ });
130
+ const providerMessages = {
131
+ messages: [{ role: 'user', content: 'Hi' }],
132
+ systemParam: { content: 'system' },
133
+ };
134
+ const result = await adapter.call(providerMessages, [], options, config);
135
+ expect(result.hasToolCalls).toBe(false);
136
+ expect(result.textContent).toBe('Hello!');
137
+ expect(result.usage?.inputTokens).toBe(100);
138
+ expect(result.usage?.outputTokens).toBe(50);
139
+ });
140
+ it('should normalize Anthropic response with tool calls', async () => {
141
+ mockMessagesCreate.mockResolvedValue({
142
+ content: [
143
+ { type: 'text', text: 'Let me read that file' },
144
+ {
145
+ type: 'tool_use',
146
+ id: 'tool_123',
147
+ name: 'Read',
148
+ input: { file_path: 'test.txt' },
149
+ },
150
+ ],
151
+ stop_reason: 'tool_use',
152
+ usage: { input_tokens: 150, output_tokens: 80 },
153
+ });
154
+ const providerMessages = {
155
+ messages: [{ role: 'user', content: 'Read test.txt' }],
156
+ systemParam: { content: 'system' },
157
+ };
158
+ const result = await adapter.call(providerMessages, [], options, config);
159
+ expect(result.hasToolCalls).toBe(true);
160
+ expect(result.toolCalls).toHaveLength(1);
161
+ expect(result.toolCalls[0].id).toBe('tool_123');
162
+ expect(result.toolCalls[0].name).toBe('Read');
163
+ expect(result.toolCalls[0].args).toEqual({ file_path: 'test.txt' });
164
+ expect(result.textContent).toBe('Let me read that file');
165
+ });
166
+ it('should handle cache metrics logging', async () => {
167
+ const consoleSpy = vi.spyOn(console, 'log');
168
+ mockMessagesCreate.mockResolvedValue({
169
+ content: [{ type: 'text', text: 'Cached response' }],
170
+ stop_reason: 'end_turn',
171
+ usage: {
172
+ input_tokens: 100,
173
+ output_tokens: 50,
174
+ cache_read_input_tokens: 500,
175
+ cache_creation_input_tokens: 100,
176
+ },
177
+ });
178
+ const providerMessages = {
179
+ messages: [{ role: 'user', content: 'Hi' }],
180
+ };
181
+ await adapter.call(providerMessages, [], options, config);
182
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[cache]'));
183
+ });
184
+ it('should use streaming for large xhigh thinking requests', async () => {
185
+ const { buildThinkingConfig } = await import('../providers/utils.js');
186
+ vi.mocked(buildThinkingConfig).mockReturnValueOnce({ budget: 32768, maxTokens: 36864 });
187
+ const finalMessage = vi.fn().mockResolvedValue({
188
+ content: [{ type: 'text', text: 'Streamed response' }],
189
+ stop_reason: 'end_turn',
190
+ usage: { input_tokens: 100, output_tokens: 50 },
191
+ });
192
+ mockMessagesStream.mockReturnValue({ finalMessage });
193
+ const providerMessages = {
194
+ messages: [{ role: 'user', content: 'Hi' }],
195
+ };
196
+ const result = await adapter.call(providerMessages, [], { ...options, thinking: 'xhigh' }, config);
197
+ expect(mockMessagesCreate).not.toHaveBeenCalled();
198
+ expect(mockMessagesStream).toHaveBeenCalledWith(expect.objectContaining({
199
+ max_tokens: 36864,
200
+ thinking: { type: 'enabled', budget_tokens: 32768 },
201
+ }));
202
+ expect(result.textContent).toBe('Streamed response');
203
+ });
204
+ });
205
+ describe('appendAssistantResponse', () => {
206
+ it('should append raw response to messages', () => {
207
+ const providerMessages = {
208
+ messages: [{ role: 'user', content: 'Hi' }],
209
+ };
210
+ const rawResponse = {
211
+ content: [{ type: 'text', text: 'Hello' }],
212
+ };
213
+ adapter.appendAssistantResponse(providerMessages, rawResponse);
214
+ expect(providerMessages.messages).toHaveLength(2);
215
+ expect(providerMessages.messages[1].role).toBe('assistant');
216
+ expect(providerMessages.messages[1].content).toBe(rawResponse.content);
217
+ });
218
+ });
219
+ describe('appendToolResult', () => {
220
+ it('should append tool result as user message', () => {
221
+ const providerMessages = {
222
+ messages: [{ role: 'user', content: 'Hi' }],
223
+ };
224
+ adapter.appendToolResult(providerMessages, 'tool_123', 'file contents', false);
225
+ expect(providerMessages.messages).toHaveLength(2);
226
+ expect(providerMessages.messages[1].role).toBe('user');
227
+ expect(providerMessages.messages[1].content).toHaveLength(1);
228
+ expect(providerMessages.messages[1].content[0].type).toBe('tool_result');
229
+ expect(providerMessages.messages[1].content[0].tool_use_id).toBe('tool_123');
230
+ });
231
+ it('should mark error results', () => {
232
+ const providerMessages = {
233
+ messages: [],
234
+ };
235
+ adapter.appendToolResult(providerMessages, 'tool_123', 'error!', true);
236
+ expect(providerMessages.messages[0].content[0].is_error).toBe(true);
237
+ });
238
+ });
239
+ describe('compactMessages', () => {
240
+ it('should delegate to generic compactMessages with anthropicFormatHelper', async () => {
241
+ const { compactMessages } = await import('../providers/context-manager.js');
242
+ const providerMessages = {
243
+ messages: [{ role: 'user', content: 'Hi' }],
244
+ };
245
+ const result = await adapter.compactMessages(providerMessages, {}, 1, config);
246
+ expect(compactMessages).toHaveBeenCalled();
247
+ expect(result.compacted).toBe(false);
248
+ });
249
+ });
250
+ describe('recordUsage', () => {
251
+ it('should record usage with costs', async () => {
252
+ const { recordUsage } = await import('../usage.js');
253
+ adapter.recordUsage('claude-opus-4', { inputTokens: 100, outputTokens: 50 }, 'api', 'agent-1');
254
+ expect(recordUsage).toHaveBeenCalled();
255
+ });
256
+ it('should skip recording when tokens are zero', async () => {
257
+ const { recordUsage } = await import('../usage.js');
258
+ vi.mocked(recordUsage).mockClear();
259
+ adapter.recordUsage('claude-opus-4', { inputTokens: 0, outputTokens: 0 }, 'api');
260
+ // recordUsage should not be called when tokens are zero
261
+ expect(recordUsage).not.toHaveBeenCalled();
262
+ });
263
+ });
264
+ });
@@ -889,7 +889,6 @@ describe('Health endpoint', () => {
889
889
  expect(body).toHaveProperty('features');
890
890
  expect(body.features).toHaveProperty('telegram');
891
891
  expect(body.features).toHaveProperty('discord');
892
- expect(body.features).toHaveProperty('browser');
893
892
  expect(body.features).toHaveProperty('voice');
894
893
  });
895
894
  it('GET /api/dashboard/health returns env var status', async () => {
@@ -1,10 +1,8 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { spawnSync } from 'child_process';
3
- import { join } from 'path';
4
3
  describe('cli integration', () => {
5
- it('onboard --dry-run exits successfully without prompting', () => {
6
- const tsxCli = join(process.cwd(), 'node_modules', 'tsx', 'dist', 'cli.mjs');
7
- const result = spawnSync(process.execPath, [tsxCli, 'src/cli.ts', 'onboard', '--dry-run'], {
4
+ it('onboard --dry-run exits successfully without prompting', { timeout: 30000 }, () => {
5
+ const result = spawnSync(process.execPath, ['--import', 'tsx', 'src/cli.ts', 'onboard', '--dry-run'], {
8
6
  cwd: process.cwd(),
9
7
  encoding: 'utf-8',
10
8
  });
@@ -175,7 +175,6 @@ describe('runCli', () => {
175
175
  codex: { authPath: '~/.codex/auth.json' },
176
176
  },
177
177
  aliases: {
178
- 'claude-fast': 'anthropic/claude-haiku-4-5',
179
178
  'codex5.1': 'codex/gpt-5.1-codex',
180
179
  'codex5.3': 'codex/gpt-5.3-codex',
181
180
  },