exempclaw 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 (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +306 -0
  3. package/dist/agent/agent.d.ts +91 -0
  4. package/dist/agent/agent.js +258 -0
  5. package/dist/agent/agent.js.map +1 -0
  6. package/dist/agent/config.d.ts +49 -0
  7. package/dist/agent/config.js +58 -0
  8. package/dist/agent/config.js.map +1 -0
  9. package/dist/agent/persona.d.ts +39 -0
  10. package/dist/agent/persona.js +81 -0
  11. package/dist/agent/persona.js.map +1 -0
  12. package/dist/agents/registry.d.ts +21 -0
  13. package/dist/agents/registry.js +51 -0
  14. package/dist/agents/registry.js.map +1 -0
  15. package/dist/cli/approve.d.ts +17 -0
  16. package/dist/cli/approve.js +50 -0
  17. package/dist/cli/approve.js.map +1 -0
  18. package/dist/cli/chat.d.ts +16 -0
  19. package/dist/cli/chat.js +148 -0
  20. package/dist/cli/chat.js.map +1 -0
  21. package/dist/cli/demo.d.ts +7 -0
  22. package/dist/cli/demo.js +82 -0
  23. package/dist/cli/demo.js.map +1 -0
  24. package/dist/cli/init.d.ts +17 -0
  25. package/dist/cli/init.js +89 -0
  26. package/dist/cli/init.js.map +1 -0
  27. package/dist/cli/live.d.ts +10 -0
  28. package/dist/cli/live.js +109 -0
  29. package/dist/cli/live.js.map +1 -0
  30. package/dist/cli/offline.d.ts +23 -0
  31. package/dist/cli/offline.js +236 -0
  32. package/dist/cli/offline.js.map +1 -0
  33. package/dist/cli/probe.d.ts +23 -0
  34. package/dist/cli/probe.js +140 -0
  35. package/dist/cli/probe.js.map +1 -0
  36. package/dist/cli/render.d.ts +15 -0
  37. package/dist/cli/render.js +50 -0
  38. package/dist/cli/render.js.map +1 -0
  39. package/dist/cli/tui.d.ts +101 -0
  40. package/dist/cli/tui.js +334 -0
  41. package/dist/cli/tui.js.map +1 -0
  42. package/dist/config/index.d.ts +33 -0
  43. package/dist/config/index.js +48 -0
  44. package/dist/config/index.js.map +1 -0
  45. package/dist/connectors/connector.d.ts +58 -0
  46. package/dist/connectors/connector.js +30 -0
  47. package/dist/connectors/connector.js.map +1 -0
  48. package/dist/connectors/email/email-connector.d.ts +43 -0
  49. package/dist/connectors/email/email-connector.js +364 -0
  50. package/dist/connectors/email/email-connector.js.map +1 -0
  51. package/dist/connectors/github/github-connector.d.ts +52 -0
  52. package/dist/connectors/github/github-connector.js +271 -0
  53. package/dist/connectors/github/github-connector.js.map +1 -0
  54. package/dist/connectors/http.d.ts +34 -0
  55. package/dist/connectors/http.js +78 -0
  56. package/dist/connectors/http.js.map +1 -0
  57. package/dist/connectors/index.d.ts +34 -0
  58. package/dist/connectors/index.js +86 -0
  59. package/dist/connectors/index.js.map +1 -0
  60. package/dist/connectors/notion/notion-connector.d.ts +45 -0
  61. package/dist/connectors/notion/notion-connector.js +222 -0
  62. package/dist/connectors/notion/notion-connector.js.map +1 -0
  63. package/dist/connectors/slack/slack-connector.d.ts +43 -0
  64. package/dist/connectors/slack/slack-connector.js +291 -0
  65. package/dist/connectors/slack/slack-connector.js.map +1 -0
  66. package/dist/core/errors.d.ts +36 -0
  67. package/dist/core/errors.js +40 -0
  68. package/dist/core/errors.js.map +1 -0
  69. package/dist/core/logger.d.ts +14 -0
  70. package/dist/core/logger.js +44 -0
  71. package/dist/core/logger.js.map +1 -0
  72. package/dist/core/run-log.d.ts +37 -0
  73. package/dist/core/run-log.js +37 -0
  74. package/dist/core/run-log.js.map +1 -0
  75. package/dist/core/usage.d.ts +22 -0
  76. package/dist/core/usage.js +58 -0
  77. package/dist/core/usage.js.map +1 -0
  78. package/dist/dashboard/data.d.ts +62 -0
  79. package/dist/dashboard/data.js +84 -0
  80. package/dist/dashboard/data.js.map +1 -0
  81. package/dist/dashboard/page.d.ts +9 -0
  82. package/dist/dashboard/page.js +421 -0
  83. package/dist/dashboard/page.js.map +1 -0
  84. package/dist/dashboard/server.d.ts +19 -0
  85. package/dist/dashboard/server.js +44 -0
  86. package/dist/dashboard/server.js.map +1 -0
  87. package/dist/demo/bootstrap.d.ts +25 -0
  88. package/dist/demo/bootstrap.js +60 -0
  89. package/dist/demo/bootstrap.js.map +1 -0
  90. package/dist/demo/claude.d.ts +31 -0
  91. package/dist/demo/claude.js +230 -0
  92. package/dist/demo/claude.js.map +1 -0
  93. package/dist/demo/demo-connector.d.ts +19 -0
  94. package/dist/demo/demo-connector.js +168 -0
  95. package/dist/demo/demo-connector.js.map +1 -0
  96. package/dist/demo/world.d.ts +60 -0
  97. package/dist/demo/world.js +117 -0
  98. package/dist/demo/world.js.map +1 -0
  99. package/dist/index.d.ts +2 -0
  100. package/dist/index.js +396 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/ingest/ingest.d.ts +63 -0
  103. package/dist/ingest/ingest.js +258 -0
  104. package/dist/ingest/ingest.js.map +1 -0
  105. package/dist/llm/claude.d.ts +97 -0
  106. package/dist/llm/claude.js +163 -0
  107. package/dist/llm/claude.js.map +1 -0
  108. package/dist/memory/compaction.d.ts +22 -0
  109. package/dist/memory/compaction.js +79 -0
  110. package/dist/memory/compaction.js.map +1 -0
  111. package/dist/memory/file-store.d.ts +28 -0
  112. package/dist/memory/file-store.js +110 -0
  113. package/dist/memory/file-store.js.map +1 -0
  114. package/dist/memory/store.d.ts +32 -0
  115. package/dist/memory/store.js +2 -0
  116. package/dist/memory/store.js.map +1 -0
  117. package/dist/orchestrator/orchestrator.d.ts +63 -0
  118. package/dist/orchestrator/orchestrator.js +181 -0
  119. package/dist/orchestrator/orchestrator.js.map +1 -0
  120. package/dist/orchestrator/scheduler.d.ts +33 -0
  121. package/dist/orchestrator/scheduler.js +67 -0
  122. package/dist/orchestrator/scheduler.js.map +1 -0
  123. package/dist/orchestrator/seen-events.d.ts +21 -0
  124. package/dist/orchestrator/seen-events.js +71 -0
  125. package/dist/orchestrator/seen-events.js.map +1 -0
  126. package/dist/plugins/apply.d.ts +9 -0
  127. package/dist/plugins/apply.js +17 -0
  128. package/dist/plugins/apply.js.map +1 -0
  129. package/dist/plugins/define.d.ts +29 -0
  130. package/dist/plugins/define.js +30 -0
  131. package/dist/plugins/define.js.map +1 -0
  132. package/dist/plugins/loader.d.ts +31 -0
  133. package/dist/plugins/loader.js +61 -0
  134. package/dist/plugins/loader.js.map +1 -0
  135. package/dist/plugins/scaffold.d.ts +5 -0
  136. package/dist/plugins/scaffold.js +72 -0
  137. package/dist/plugins/scaffold.js.map +1 -0
  138. package/dist/tools/builtin.d.ts +8 -0
  139. package/dist/tools/builtin.js +63 -0
  140. package/dist/tools/builtin.js.map +1 -0
  141. package/dist/tools/tool.d.ts +84 -0
  142. package/dist/tools/tool.js +70 -0
  143. package/dist/tools/tool.js.map +1 -0
  144. package/dist/ui/agent-view.test.d.ts +1 -0
  145. package/dist/ui/agent-view.test.js +54 -0
  146. package/dist/ui/agent-view.test.js.map +1 -0
  147. package/dist/ui/agents-data.d.ts +7 -0
  148. package/dist/ui/agents-data.js +25 -0
  149. package/dist/ui/agents-data.js.map +1 -0
  150. package/dist/ui/app.d.ts +24 -0
  151. package/dist/ui/app.js +59 -0
  152. package/dist/ui/app.js.map +1 -0
  153. package/dist/ui/app.test.d.ts +1 -0
  154. package/dist/ui/app.test.js +47 -0
  155. package/dist/ui/app.test.js.map +1 -0
  156. package/dist/ui/components/key-hints.d.ts +4 -0
  157. package/dist/ui/components/key-hints.js +6 -0
  158. package/dist/ui/components/key-hints.js.map +1 -0
  159. package/dist/ui/components/menu.d.ts +11 -0
  160. package/dist/ui/components/menu.js +20 -0
  161. package/dist/ui/components/menu.js.map +1 -0
  162. package/dist/ui/create-wizard.test.d.ts +1 -0
  163. package/dist/ui/create-wizard.test.js +58 -0
  164. package/dist/ui/create-wizard.test.js.map +1 -0
  165. package/dist/ui/doctor-data.d.ts +6 -0
  166. package/dist/ui/doctor-data.js +29 -0
  167. package/dist/ui/doctor-data.js.map +1 -0
  168. package/dist/ui/history-data.d.ts +2 -0
  169. package/dist/ui/history-data.js +18 -0
  170. package/dist/ui/history-data.js.map +1 -0
  171. package/dist/ui/screens/agent.d.ts +8 -0
  172. package/dist/ui/screens/agent.js +95 -0
  173. package/dist/ui/screens/agent.js.map +1 -0
  174. package/dist/ui/screens/agents.d.ts +7 -0
  175. package/dist/ui/screens/agents.js +47 -0
  176. package/dist/ui/screens/agents.js.map +1 -0
  177. package/dist/ui/screens/create.d.ts +7 -0
  178. package/dist/ui/screens/create.js +141 -0
  179. package/dist/ui/screens/create.js.map +1 -0
  180. package/dist/ui/screens/doctor.d.ts +5 -0
  181. package/dist/ui/screens/doctor.js +13 -0
  182. package/dist/ui/screens/doctor.js.map +1 -0
  183. package/dist/ui/screens/history.d.ts +7 -0
  184. package/dist/ui/screens/history.js +50 -0
  185. package/dist/ui/screens/history.js.map +1 -0
  186. package/dist/ui/screens/home.d.ts +8 -0
  187. package/dist/ui/screens/home.js +35 -0
  188. package/dist/ui/screens/home.js.map +1 -0
  189. package/dist/ui/screens/plugins.d.ts +7 -0
  190. package/dist/ui/screens/plugins.js +40 -0
  191. package/dist/ui/screens/plugins.js.map +1 -0
  192. package/dist/ui/services.d.ts +33 -0
  193. package/dist/ui/services.js +67 -0
  194. package/dist/ui/services.js.map +1 -0
  195. package/dist/ui/start.d.ts +1 -0
  196. package/dist/ui/start.js +16 -0
  197. package/dist/ui/start.js.map +1 -0
  198. package/dist/ui/theme.d.ts +6 -0
  199. package/dist/ui/theme.js +26 -0
  200. package/dist/ui/theme.js.map +1 -0
  201. package/package.json +69 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joseph Santana
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,306 @@
1
+ # Exempclaw
2
+
3
+ **Run helpful AI agents from your terminal — no coding required to get started.**
4
+
5
+ Exempclaw lets you create a Claude-powered assistant that can cover a role:
6
+ read and reply to email, watch a Slack channel, keep notes, and follow a
7
+ daily routine — all under a persona you define, with you approving anything it
8
+ sends. It runs in a friendly menu you drive with the arrow keys. Powered
9
+ exclusively by the **Claude API**.
10
+
11
+ ```
12
+ (\/) E X E M P C L A W fleet command
13
+
14
+ ❯ Agents 2 configured
15
+ New agent guided setup
16
+ History all runs, all agents
17
+ Plugins 1 installed
18
+ Doctor check my setup
19
+ Quit
20
+
21
+ ↑↓ move · enter select · q quit
22
+ ```
23
+
24
+ ---
25
+
26
+ ## 👋 New here? Start in 30 seconds
27
+
28
+ You need [Node.js 22 or newer](https://nodejs.org) (a free, one-click install).
29
+ Then open your terminal and run **one** of these:
30
+
31
+ ```bash
32
+ # Just try it — nothing to install:
33
+ npx exempclaw
34
+
35
+ # …or install it for keeps:
36
+ npm install -g exempclaw # then type: exempclaw
37
+ brew install Rabadakku/tap/exempclaw # (macOS, Homebrew)
38
+ ```
39
+
40
+ That's it. Type **`exempclaw`** and you'll land in the menu above. From there:
41
+
42
+ 1. **New agent** walks you through creating your first assistant — it asks
43
+ plain questions (its name, what it does, how it should sound) and explains
44
+ every choice. No files to edit.
45
+ 2. **Doctor** checks your setup and tells you, in plain English, exactly what
46
+ to fix.
47
+ 3. **Agents** lists everyone you've created. Pick one to chat with it.
48
+
49
+ **No Claude API key yet?** Run `exempclaw demo` for a full guided tour that
50
+ costs nothing and sends nothing — it's all pretend data. When you're ready for
51
+ the real thing, get a key at
52
+ [console.anthropic.com](https://console.anthropic.com) and paste it where
53
+ Doctor tells you to.
54
+
55
+ > **Why a key?** Exempclaw runs on Claude, Anthropic's AI. The key is how your
56
+ > usage is billed to *your* Anthropic account — Exempclaw never uses anyone
57
+ > else's.
58
+
59
+ ---
60
+
61
+ ## What you can do with it
62
+
63
+ - **Create an assistant in minutes** through the guided wizard — give it a
64
+ name, a job, and a tone of voice.
65
+ - **Chat with it** right in the terminal. It thinks out loud, shows you what
66
+ it's doing, and streams its answer as it types.
67
+ - **Let it watch your channels** (email, Slack, Notion, GitHub) and handle
68
+ what comes in — but it always asks before sending anything, unless you tell
69
+ it otherwise.
70
+ - **Stay in control.** Every action it takes is logged. You can review the
71
+ full history, see what it cost, and approve or deny anything that goes out.
72
+ - **Teach it the ropes.** Point it at a folder of past emails or docs and it
73
+ distills them into lasting memory, so it starts already knowing the context.
74
+ - **Add new powers with plugins** — drop a folder in, restart, done.
75
+
76
+ ---
77
+
78
+ <!-- ============================================================= -->
79
+
80
+ ## 🛠️ For developers
81
+
82
+ Everything below is the technical reference. Jump to what you need:
83
+
84
+ - [How it works](#how-it-works)
85
+ - [Installing from source](#installing-from-source)
86
+ - [The agent lifecycle](#the-agent-lifecycle)
87
+ - [Agent config reference](#agent-config-reference)
88
+ - [Command reference](#command-reference)
89
+ - [Writing a plugin](#writing-a-plugin)
90
+ - [Connectors](#connectors)
91
+ - [Outward-action safety](#outward-action-safety)
92
+ - [Memory & context](#memory--context)
93
+ - [Project layout & tests](#project-layout--tests)
94
+ - [Responsible use](#responsible-use)
95
+
96
+ ### How it works
97
+
98
+ A single process owns the fleet. The menu (an [ink](https://github.com/vadimdemedes/ink)
99
+ TUI) and every CLI subcommand drive the same orchestrator, which runs each
100
+ agent's tool-use loop against the Claude API.
101
+
102
+ ```
103
+ exempclaw ─┬─ TUI menu ───────┐
104
+ ├─ chat / run ──────┼─ Orchestrator ─ Agent (tool-use loop)
105
+ ├─ start (fleet) ───┘ │ └ { Claude · Tools · Memory · Connectors }
106
+ └─ dashboard (read-only ledger: runs, approvals, costs, memory)
107
+ ```
108
+
109
+ Bare `exempclaw` opens the TUI when stdin/stdout are a real terminal; in a
110
+ pipe or CI it prints help instead. All the classic subcommands still exist for
111
+ scripting — the TUI is a friendlier front door, not a replacement.
112
+
113
+ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full design.
114
+
115
+ ### Installing from source
116
+
117
+ ```bash
118
+ git clone https://github.com/Rabadakku/Exempclaw.git
119
+ cd exempclaw
120
+ npm install
121
+ npm run dev # runs the TUI from source (tsx)
122
+ npm run build # compiles to dist/ (the `exempclaw` bin)
123
+ ```
124
+
125
+ `npm run dev -- <subcommand>` runs any CLI command from source, e.g.
126
+ `npm run dev -- doctor`.
127
+
128
+ ### The agent lifecycle
129
+
130
+ **1. Define** — one JSON file per agent. The **New agent** wizard writes these
131
+ for you; `exempclaw init agents/sam.json` is the scriptable equivalent. Files
132
+ live in your agents directory (`./agents` in a project, otherwise
133
+ `~/.exempclaw/agents`; override with `EXEMPCLAW_AGENTS_DIR`).
134
+
135
+ **2. Ingest** *(optional, but the point)* — `exempclaw ingest <agent.json>
136
+ <dir>` reads exported artifacts (mail, docs, notes — any text), distills them
137
+ into durable role memories with Claude (people, commitments, conventions,
138
+ in-flight work — never secrets), and synthesizes a role briefing. The agent
139
+ starts already knowing the territory.
140
+
141
+ **3. Run** — `chat` for interactive work, `run` for one-shots, `start` for the
142
+ always-on fleet: inbound events (a new email, a Slack mention, a GitHub issue)
143
+ are deduplicated, routed to the owning agent, and handled in its persistent
144
+ context. Schedules (`every` / `dailyAt`) cover recurring duties.
145
+
146
+ **4. Observe** — every run lands in an append-only audit log: trigger, turns,
147
+ tokens, estimated cost, every outward action and who approved it. The TUI's
148
+ **History** screen browses it; `exempclaw costs` aggregates spend; `exempclaw
149
+ dashboard` serves the read-only **Succession Ledger** on localhost.
150
+
151
+ ### Agent config reference
152
+
153
+ ```json
154
+ {
155
+ "id": "jordan-support-lead",
156
+ "persona": {
157
+ "name": "Jordan",
158
+ "role": "Customer Support Lead",
159
+ "succeeds": "Alex Rivera",
160
+ "tone": "warm, concise, proactive",
161
+ "disclosure": "transparent"
162
+ },
163
+ "model": "claude-opus-4-8",
164
+ "effort": "high",
165
+ "connectors": ["email", "slack"],
166
+ "toolPolicies": { "slack_post_message": "ask", "email_send": "ask" },
167
+ "schedules": [{ "dailyAt": "09:00", "input": "Morning triage: …" }]
168
+ }
169
+ ```
170
+
171
+ `disclosure` is required and is one of `transparent` (always says it's an AI),
172
+ `on_request` (answers truthfully when asked), or `opaque` (doesn't volunteer
173
+ it — but still never denies being an AI when sincerely asked, and never claims
174
+ to be a specific named human). See [Responsible use](#responsible-use).
175
+
176
+ ### Command reference
177
+
178
+ The TUI covers the common path; these are the full subcommands (also useful
179
+ for scripting and automation):
180
+
181
+ | Command | What it does |
182
+ |---|---|
183
+ | *(none)* | Open the full-screen fleet menu (same as `ui`) |
184
+ | `ui` | Open the full-screen fleet menu explicitly |
185
+ | `init <path>` | Scaffold an agent config (interactive or via flags) |
186
+ | `chat <agent>` | Animated REPL — streamed text, live tool rows, slash commands, Ctrl-C interrupts the run |
187
+ | `run <agent> <input>` | One-shot run; `--json` for the full result, `--policy` to override |
188
+ | `start <agents…>` | Fleet mode: connector listeners + schedules until Ctrl-C |
189
+ | `ingest <agent> <dir>` | Distill an export directory (incl. `.eml`/`.mbox`) into role memory + briefing |
190
+ | `plugin create <name>` | Scaffold a new plugin with a working example tool |
191
+ | `plugin list` | List discovered plugins and any load errors |
192
+ | `dashboard [agents…]` | Read-only web ledger on 127.0.0.1 (`--port`, default 4177) |
193
+ | `demo` | Scripted replay of the animated TUI — no API key needed |
194
+ | `memory <agent>` | List/search durable memory; `--add`, `--rm` |
195
+ | `history <agent>` | Show the transcript; `--clear` to reset (memories kept) |
196
+ | `costs` | Tokens + estimated spend per agent from the audit log |
197
+ | `connectors` | Connector credential status |
198
+ | `doctor` | Environment check + live connector probes (`--no-probe` to skip) |
199
+
200
+ ### Writing a plugin
201
+
202
+ A plugin adds **tools** (capabilities an agent can call) and/or **connectors**
203
+ (full integrations) without touching the core. Plugins can't change the LLM —
204
+ Exempclaw is Claude-only by design.
205
+
206
+ ```bash
207
+ exempclaw plugin create weather # scaffolds ~/.exempclaw/plugins/weather
208
+ ```
209
+
210
+ That generates a working, zero-install plugin you can edit immediately:
211
+
212
+ ```js
213
+ // ~/.exempclaw/plugins/weather/index.js
214
+ export default function ({ z, defineTool, definePlugin }) {
215
+ return definePlugin({
216
+ name: "weather",
217
+ tools: [
218
+ defineTool({
219
+ name: "weather_hello",
220
+ description: "Example tool — replace with your own.",
221
+ schema: z.object({ who: z.string() }),
222
+ execute: async ({ who }) => ({ content: `Hello, ${who}!` }),
223
+ }),
224
+ ],
225
+ });
226
+ }
227
+ ```
228
+
229
+ Restart `exempclaw` and open **Plugins** — yours appears with its tools, or
230
+ with a load error to fix (a broken plugin never crashes the app). Plugins are
231
+ discovered in `~/.exempclaw/plugins` (override with `EXEMPCLAW_PLUGINS_DIR`).
232
+ Each scaffold ships a `PLUGIN.md` documenting the full interface, including how
233
+ to write connectors and how TypeScript authors can import types from
234
+ `exempclaw/plugin`.
235
+
236
+ ### Connectors
237
+
238
+ Built-in connectors: **email** (IMAP/SMTP), **Slack** (Web API + Socket Mode),
239
+ **Notion**, **GitHub**. Each contributes tools and, optionally, inbound events.
240
+ Credentials come from the environment — `exempclaw connectors` shows status,
241
+ [docs/CONNECTORS.md](docs/CONNECTORS.md) explains provisioning. Adding one
242
+ means implementing the [`Connector`](src/connectors/connector.ts) interface;
243
+ the runtime and orchestrator don't change. For third-party integrations,
244
+ prefer a [plugin](#writing-a-plugin).
245
+
246
+ ### Outward-action safety
247
+
248
+ Anything that affects the outside world (sending email, posting to Slack,
249
+ writing to Notion/GitHub) is marked `outward` and routes through an approval
250
+ policy before it executes:
251
+
252
+ - `ask` (default) — prompt to approve once, deny, or auto-approve that tool
253
+ for the session. In the TUI this is an inline yes/no dialog.
254
+ - `auto` — execute without prompting (once you trust an agent)
255
+ - `deny` — block all outward actions (dry-run / shadow mode)
256
+
257
+ Set the default with `EXEMPCLAW_ACTION_POLICY`, override per invocation with
258
+ `--policy`, and per tool in the agent config. Every decision is recorded in the
259
+ run log.
260
+
261
+ ### Memory & context
262
+
263
+ - **Durable memory** — atomic facts with source + tags, persisted per agent
264
+ (`remember`/`recall` tools, the ingest pass, or `exempclaw memory --add`).
265
+ The most recent slice is injected into the system prompt each run.
266
+ - **Conversation history** — survives restarts. When the prompt outgrows
267
+ `EXEMPCLAW_CONTEXT_BUDGET_TOKENS` (default 200k), older turns are summarized
268
+ by Claude and replaced with a compact digest; recent turns stay verbatim and
269
+ tool-call pairs are never split.
270
+ - **Prompt caching** — the persona/tools prefix and the running conversation
271
+ carry cache breakpoints, so long-lived agents mostly pay cache-read prices.
272
+
273
+ ### Project layout & tests
274
+
275
+ ```
276
+ src/ui/ ink TUI (menu, screens, components)
277
+ src/agent/ the agent tool-use loop, persona, config
278
+ src/orchestrator/ fleet supervision, routing, scheduling
279
+ src/connectors/ email · slack · notion · github
280
+ src/plugins/ plugin loader + public definePlugin API
281
+ src/tools/ built-in tools
282
+ src/memory/ durable memory + history compaction
283
+ src/core/ logger, run log, usage/cost
284
+ ```
285
+
286
+ ```bash
287
+ npm test # 160+ unit tests, no network or credentials needed
288
+ npm run typecheck
289
+ ```
290
+
291
+ ### Responsible use
292
+
293
+ Operating an agent through a real person's role and accounts implicates
294
+ consent, impersonation, and bot-disclosure rules that vary by jurisdiction.
295
+ The framework makes the choices explicit — `disclosure` is a required persona
296
+ field, and even `opaque` agents are instructed never to claim to be a specific
297
+ named human and never to deny being an AI when sincerely asked. Approval gating
298
+ and the audit log exist so a human stays accountable for outward actions.
299
+ Having authorization for each connected account, and meeting applicable
300
+ notice/disclosure requirements, is on the operator.
301
+
302
+ ---
303
+
304
+ ## License
305
+
306
+ [MIT](LICENSE) © 2026 Joseph Santana
@@ -0,0 +1,91 @@
1
+ import type { ClaudeLike, EffortLevel } from "../llm/claude.js";
2
+ import type { Logger } from "../core/logger.js";
3
+ import { type UsageTotals } from "../core/usage.js";
4
+ import type { RunLog, TriggerKind } from "../core/run-log.js";
5
+ import type { MemoryStore } from "../memory/store.js";
6
+ import { type AgentActivity, type ApprovalRequest, type ToolRegistry } from "../tools/tool.js";
7
+ import type { ActionPolicy } from "../config/index.js";
8
+ import { type Persona } from "./persona.js";
9
+ export interface AgentOptions {
10
+ id: string;
11
+ persona: Persona;
12
+ model: string;
13
+ effort?: EffortLevel;
14
+ /** Hard cap on tool-use iterations per run, to bound runaway loops. */
15
+ maxIterations?: number;
16
+ /** Per-tool approval-policy overrides; "*" matches any tool. */
17
+ toolPolicies?: Record<string, ActionPolicy>;
18
+ /** Compact history once the prompt grows past this many tokens. */
19
+ contextBudgetTokens?: number;
20
+ }
21
+ export interface AgentDeps {
22
+ claude: ClaudeLike;
23
+ tools: ToolRegistry;
24
+ memory: MemoryStore;
25
+ log: Logger;
26
+ actionPolicy: ActionPolicy;
27
+ /** Interactive approver invoked when policy is "ask" and a tool is outward. */
28
+ approve: (req: ApprovalRequest) => Promise<boolean>;
29
+ /** Audit trail; runs are recorded when provided. */
30
+ runLog?: RunLog;
31
+ }
32
+ /** Live-progress callbacks for terminal UX. All optional. */
33
+ export interface RunHooks {
34
+ /** A model turn is starting (1-based). Show a thinking indicator. */
35
+ onTurnStart?: (iteration: number) => void;
36
+ onText?: (delta: string) => void;
37
+ onToolStart?: (name: string, input: unknown) => void;
38
+ onToolEnd?: (name: string, ok: boolean, detail?: string) => void;
39
+ /** The agent called display_status — play the matching animation. */
40
+ onStatus?: (activity: AgentActivity, message: string) => void;
41
+ }
42
+ export interface RunOptions {
43
+ signal?: AbortSignal;
44
+ hooks?: RunHooks;
45
+ trigger?: {
46
+ kind: TriggerKind;
47
+ detail?: string;
48
+ };
49
+ }
50
+ export interface RunResult {
51
+ runId: string;
52
+ /** The final assistant text after the loop settled. */
53
+ text: string;
54
+ iterations: number;
55
+ stopReason: string | null;
56
+ usage: UsageTotals;
57
+ costUsd: number | null;
58
+ }
59
+ /**
60
+ * A single agent instance. Owns the Claude tool-use loop: send the conversation,
61
+ * execute any tool calls (gating outward ones through the approval policy), feed
62
+ * results back, and repeat until the model stops calling tools. History is
63
+ * persisted to the MemoryStore after every run so agents survive restarts, and
64
+ * every run is appended to the audit log.
65
+ */
66
+ export declare class Agent {
67
+ private readonly opts;
68
+ private readonly deps;
69
+ private readonly log;
70
+ private readonly maxIterations;
71
+ private readonly contextBudgetTokens;
72
+ constructor(opts: AgentOptions, deps: AgentDeps);
73
+ get id(): string;
74
+ get persona(): Persona;
75
+ /**
76
+ * Feeds one user input (a message, an incoming email, an event) into the
77
+ * agent and runs the loop to completion.
78
+ */
79
+ run(userInput: string, options?: RunOptions): Promise<RunResult>;
80
+ private composeSystemBlocks;
81
+ private tryCompact;
82
+ private extractText;
83
+ /**
84
+ * Executes the turn's tool calls. Read-only tools run concurrently; if any
85
+ * call is outward the whole batch runs sequentially so approval prompts
86
+ * arrive one at a time. Result order always matches call order.
87
+ */
88
+ private executeToolCalls;
89
+ private runSingleTool;
90
+ private toolError;
91
+ }
@@ -0,0 +1,258 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { ToolExecutionError } from "../core/errors.js";
3
+ import { addUsage, contextTokens, emptyUsage, estimateCostUsd } from "../core/usage.js";
4
+ import { compactHistory } from "../memory/compaction.js";
5
+ import { evaluatePolicy, resolvePolicy, } from "../tools/tool.js";
6
+ import { buildSystemBlocks } from "./persona.js";
7
+ /**
8
+ * A single agent instance. Owns the Claude tool-use loop: send the conversation,
9
+ * execute any tool calls (gating outward ones through the approval policy), feed
10
+ * results back, and repeat until the model stops calling tools. History is
11
+ * persisted to the MemoryStore after every run so agents survive restarts, and
12
+ * every run is appended to the audit log.
13
+ */
14
+ export class Agent {
15
+ opts;
16
+ deps;
17
+ log;
18
+ maxIterations;
19
+ contextBudgetTokens;
20
+ constructor(opts, deps) {
21
+ this.opts = opts;
22
+ this.deps = deps;
23
+ this.log = deps.log.child({ scope: "agent", agentId: opts.id });
24
+ this.maxIterations = opts.maxIterations ?? 25;
25
+ this.contextBudgetTokens = opts.contextBudgetTokens ?? 200_000;
26
+ }
27
+ get id() {
28
+ return this.opts.id;
29
+ }
30
+ get persona() {
31
+ return this.opts.persona;
32
+ }
33
+ /**
34
+ * Feeds one user input (a message, an incoming email, an event) into the
35
+ * agent and runs the loop to completion.
36
+ */
37
+ async run(userInput, options = {}) {
38
+ const runId = randomUUID();
39
+ const startedAt = new Date().toISOString();
40
+ const signal = options.signal ?? new AbortController().signal;
41
+ const hooks = options.hooks ?? {};
42
+ const outwardActions = [];
43
+ const messages = await this.deps.memory.loadHistory();
44
+ messages.push({ role: "user", content: userInput });
45
+ const system = await this.composeSystemBlocks();
46
+ const tools = this.deps.tools.toAnthropicTools();
47
+ let iterations = 0;
48
+ let stopReason = null;
49
+ let finalText = "";
50
+ let usage = emptyUsage();
51
+ let lastContextTokens = 0;
52
+ let runError;
53
+ try {
54
+ while (iterations < this.maxIterations) {
55
+ if (signal.aborted) {
56
+ stopReason = stopReason ?? "interrupted";
57
+ break;
58
+ }
59
+ iterations++;
60
+ hooks.onTurnStart?.(iterations);
61
+ let message;
62
+ try {
63
+ message = await this.deps.claude.turn({
64
+ model: this.opts.model,
65
+ system,
66
+ messages,
67
+ tools,
68
+ effort: this.opts.effort,
69
+ signal,
70
+ onText: hooks.onText,
71
+ cacheConversation: true,
72
+ });
73
+ }
74
+ catch (err) {
75
+ if (signal.aborted) {
76
+ // Operator interrupt — settle gracefully, keep what we have.
77
+ stopReason = "interrupted";
78
+ iterations--;
79
+ break;
80
+ }
81
+ throw err;
82
+ }
83
+ stopReason = message.stop_reason;
84
+ usage = addUsage(usage, message.usage);
85
+ lastContextTokens = contextTokens(message.usage);
86
+ messages.push({ role: "assistant", content: message.content });
87
+ finalText = this.extractText(message.content) || finalText;
88
+ // Server-side pause: re-send the conversation so the API resumes.
89
+ if (message.stop_reason === "pause_turn")
90
+ continue;
91
+ const toolUses = message.content.filter((b) => b.type === "tool_use");
92
+ if (message.stop_reason !== "tool_use" || toolUses.length === 0) {
93
+ break;
94
+ }
95
+ const results = await this.executeToolCalls(toolUses, signal, hooks, outwardActions);
96
+ messages.push({ role: "user", content: results });
97
+ }
98
+ }
99
+ catch (err) {
100
+ runError = err.message;
101
+ throw err;
102
+ }
103
+ finally {
104
+ let toSave = messages;
105
+ if (lastContextTokens > this.contextBudgetTokens) {
106
+ toSave = await this.tryCompact(messages, signal);
107
+ }
108
+ await this.deps.memory.saveHistory(toSave);
109
+ const costUsd = estimateCostUsd(this.opts.model, usage);
110
+ await this.deps.runLog
111
+ ?.append({
112
+ runId,
113
+ agentId: this.opts.id,
114
+ trigger: options.trigger ?? { kind: "cli" },
115
+ startedAt,
116
+ finishedAt: new Date().toISOString(),
117
+ model: this.opts.model,
118
+ iterations,
119
+ stopReason,
120
+ usage,
121
+ costUsd,
122
+ outwardActions,
123
+ ...(runError ? { error: runError } : {}),
124
+ })
125
+ .catch((err) => this.log.warn("failed to write run record", { error: err.message }));
126
+ if (!runError) {
127
+ this.log.info("run complete", {
128
+ iterations,
129
+ stopReason: stopReason ?? "none",
130
+ tokens: usage.inputTokens + usage.outputTokens,
131
+ });
132
+ }
133
+ }
134
+ return {
135
+ runId,
136
+ text: finalText,
137
+ iterations,
138
+ stopReason,
139
+ usage,
140
+ costUsd: estimateCostUsd(this.opts.model, usage),
141
+ };
142
+ }
143
+ async composeSystemBlocks() {
144
+ const memories = await this.deps.memory.allMemories();
145
+ const roleContext = memories
146
+ .slice(-50) // most recent durable knowledge; bounded to keep the prompt manageable
147
+ .map((m) => `- (${m.source}) ${m.text}`)
148
+ .join("\n");
149
+ return buildSystemBlocks(this.opts.persona, roleContext);
150
+ }
151
+ async tryCompact(messages, signal) {
152
+ try {
153
+ const compacted = await compactHistory(messages, {
154
+ summarize: (transcript) => this.deps.claude.summarize(transcript, { model: this.opts.model, signal }),
155
+ });
156
+ if (compacted !== messages) {
157
+ this.log.info("history compacted", { from: messages.length, to: compacted.length });
158
+ }
159
+ return compacted;
160
+ }
161
+ catch (err) {
162
+ this.log.warn("history compaction failed; keeping full history", { error: err.message });
163
+ return messages;
164
+ }
165
+ }
166
+ extractText(content) {
167
+ return content
168
+ .filter((b) => b.type === "text")
169
+ .map((b) => b.text)
170
+ .join("\n")
171
+ .trim();
172
+ }
173
+ /**
174
+ * Executes the turn's tool calls. Read-only tools run concurrently; if any
175
+ * call is outward the whole batch runs sequentially so approval prompts
176
+ * arrive one at a time. Result order always matches call order.
177
+ */
178
+ async executeToolCalls(toolUses, signal, hooks, outwardActions) {
179
+ const ctx = {
180
+ agentId: this.opts.id,
181
+ log: this.log,
182
+ signal,
183
+ // Tools that need ad-hoc approval resolve against the global policy.
184
+ requestApproval: (req) => evaluatePolicy(this.deps.actionPolicy, req, this.deps.approve),
185
+ emit: (event) => {
186
+ if (event.kind === "status")
187
+ hooks.onStatus?.(event.activity, event.message);
188
+ },
189
+ };
190
+ const anyOutward = toolUses.some((call) => this.deps.tools.get(call.name)?.outward);
191
+ if (anyOutward) {
192
+ const results = [];
193
+ for (const call of toolUses) {
194
+ results.push(await this.runSingleTool(call, ctx, hooks, outwardActions));
195
+ }
196
+ return results;
197
+ }
198
+ return Promise.all(toolUses.map((call) => this.runSingleTool(call, ctx, hooks, outwardActions)));
199
+ }
200
+ async runSingleTool(call, ctx, hooks, outwardActions) {
201
+ const tool = this.deps.tools.get(call.name);
202
+ if (!tool) {
203
+ return this.toolError(call.id, `unknown tool: ${call.name}`);
204
+ }
205
+ const parsed = tool.schema.safeParse(call.input);
206
+ if (!parsed.success) {
207
+ return this.toolError(call.id, `invalid input: ${parsed.error.message}`);
208
+ }
209
+ hooks.onToolStart?.(tool.name, parsed.data);
210
+ // Gate outward actions through the approval policy before executing.
211
+ if (tool.outward) {
212
+ const policy = resolvePolicy(this.deps.actionPolicy, this.opts.toolPolicies, tool.name);
213
+ const request = {
214
+ tool: tool.name,
215
+ summary: `${tool.name} requested by agent ${this.opts.id}`,
216
+ detail: JSON.stringify(parsed.data, null, 2),
217
+ };
218
+ const approved = await evaluatePolicy(policy, request, this.deps.approve);
219
+ outwardActions.push({
220
+ tool: tool.name,
221
+ approved,
222
+ summary: summarizeInput(parsed.data),
223
+ at: new Date().toISOString(),
224
+ });
225
+ if (!approved) {
226
+ this.log.warn("outward action denied", { tool: tool.name, policy });
227
+ hooks.onToolEnd?.(tool.name, false, "denied by approval policy");
228
+ return this.toolError(call.id, "action denied by approval policy");
229
+ }
230
+ }
231
+ try {
232
+ this.log.debug("executing tool", { tool: tool.name });
233
+ const result = await tool.execute(parsed.data, ctx);
234
+ hooks.onToolEnd?.(tool.name, !result.isError);
235
+ return {
236
+ type: "tool_result",
237
+ tool_use_id: call.id,
238
+ content: result.content,
239
+ ...(result.isError ? { is_error: true } : {}),
240
+ };
241
+ }
242
+ catch (err) {
243
+ const message = err instanceof ToolExecutionError ? err.message : `unexpected error: ${err.message}`;
244
+ this.log.error("tool threw", { tool: tool.name, error: message });
245
+ hooks.onToolEnd?.(tool.name, false, message);
246
+ return this.toolError(call.id, message);
247
+ }
248
+ }
249
+ toolError(toolUseId, message) {
250
+ return { type: "tool_result", tool_use_id: toolUseId, content: message, is_error: true };
251
+ }
252
+ }
253
+ /** One-line description of a tool input for the audit record. */
254
+ function summarizeInput(input) {
255
+ const json = JSON.stringify(input);
256
+ return json.length > 200 ? `${json.slice(0, 200)}…` : json;
257
+ }
258
+ //# sourceMappingURL=agent.js.map