@orgloop/agentctl 1.0.0 → 1.0.1
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.
- package/README.md +132 -3
- package/dist/adapters/openclaw.js +23 -4
- package/dist/cli.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Over time, agentctl can extend to handle more concerns of headless coding — au
|
|
|
28
28
|
## Installation
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
npm install -g agentctl
|
|
31
|
+
npm install -g @orgloop/agentctl
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Requires Node.js >= 20.
|
|
@@ -90,6 +90,11 @@ agentctl stop <id> [options]
|
|
|
90
90
|
agentctl resume <id> <message> [options]
|
|
91
91
|
--adapter <name> Adapter to use
|
|
92
92
|
|
|
93
|
+
# <message> is a continuation prompt sent to the agent.
|
|
94
|
+
# The agent receives it as new user input and resumes work.
|
|
95
|
+
# Example: resume a stopped session with a follow-up instruction
|
|
96
|
+
agentctl resume abc123 "fix the failing tests and re-run the suite"
|
|
97
|
+
|
|
93
98
|
agentctl events [options]
|
|
94
99
|
--json Output as NDJSON (default)
|
|
95
100
|
```
|
|
@@ -109,15 +114,68 @@ agentctl locks [options]
|
|
|
109
114
|
--json Output as JSON
|
|
110
115
|
```
|
|
111
116
|
|
|
117
|
+
### Lifecycle Hooks
|
|
118
|
+
|
|
119
|
+
Hooks are shell commands that run at specific points in a session's lifecycle. Pass them as flags to `launch` or `merge`:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
agentctl launch -p "implement feature X" \
|
|
123
|
+
--on-create "echo 'Session $AGENTCTL_SESSION_ID started'" \
|
|
124
|
+
--on-complete "npm test"
|
|
125
|
+
|
|
126
|
+
agentctl merge <id> \
|
|
127
|
+
--pre-merge "npm run lint && npm test" \
|
|
128
|
+
--post-merge "curl -X POST https://slack.example.com/webhook -d '{\"text\": \"PR merged\"}'"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Available hooks:
|
|
132
|
+
|
|
133
|
+
| Hook | Trigger | Typical use |
|
|
134
|
+
|------|---------|-------------|
|
|
135
|
+
| `--on-create <script>` | After a session is created | Notify, set up environment |
|
|
136
|
+
| `--on-complete <script>` | After a session completes | Run tests, send alerts |
|
|
137
|
+
| `--pre-merge <script>` | Before `agentctl merge` commits | Lint, test, validate |
|
|
138
|
+
| `--post-merge <script>` | After `agentctl merge` pushes/opens PR | Notify, trigger CI |
|
|
139
|
+
|
|
140
|
+
Hook scripts receive context via environment variables:
|
|
141
|
+
|
|
142
|
+
| Variable | Description |
|
|
143
|
+
|----------|-------------|
|
|
144
|
+
| `AGENTCTL_SESSION_ID` | Session UUID |
|
|
145
|
+
| `AGENTCTL_CWD` | Working directory of the session |
|
|
146
|
+
| `AGENTCTL_ADAPTER` | Adapter name (e.g. `claude-code`) |
|
|
147
|
+
| `AGENTCTL_BRANCH` | Git branch (when using `--worktree`) |
|
|
148
|
+
| `AGENTCTL_EXIT_CODE` | Process exit code (in `--on-complete`) |
|
|
149
|
+
|
|
150
|
+
Hooks run with a 60-second timeout. If a hook fails, its stderr is printed but execution continues.
|
|
151
|
+
|
|
112
152
|
### Fuse Timers
|
|
113
153
|
|
|
114
|
-
|
|
154
|
+
Fuse timers provide automatic cleanup of [Kind](https://kind.sigs.k8s.io/) Kubernetes clusters tied to coding sessions. When a session exits, agentctl starts a countdown timer. If no new session starts in the same worktree directory before the timer expires, the associated Kind cluster is deleted to free resources.
|
|
155
|
+
|
|
156
|
+
This is useful when running agents in worktree-per-branch workflows where each branch has its own Kind cluster (e.g. `kindo-charlie-<branch>`). Without fuse timers, forgotten clusters accumulate and waste resources.
|
|
157
|
+
|
|
158
|
+
**How it works:**
|
|
159
|
+
|
|
160
|
+
1. Agent session exits in a worktree directory (e.g. `~/code/mono-my-feature`)
|
|
161
|
+
2. agentctl derives the cluster name (`kindo-charlie-my-feature`) and starts a fuse timer
|
|
162
|
+
3. If the timer expires (default: configured at daemon startup), the cluster is deleted via `kind delete cluster`
|
|
163
|
+
4. If a new session starts in the same directory before expiry, the fuse is cancelled
|
|
115
164
|
|
|
116
165
|
```bash
|
|
166
|
+
# List active fuse timers
|
|
117
167
|
agentctl fuses [options]
|
|
118
168
|
--json Output as JSON
|
|
119
169
|
```
|
|
120
170
|
|
|
171
|
+
Example output:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
Directory Cluster Expires In
|
|
175
|
+
~/code/mono-feat-x kindo-charlie-feat-x 12m
|
|
176
|
+
~/code/mono-hotfix kindo-charlie-hotfix 45m
|
|
177
|
+
```
|
|
178
|
+
|
|
121
179
|
### Daemon
|
|
122
180
|
|
|
123
181
|
The daemon provides session tracking, directory locks, fuse timers, and Prometheus metrics. It auto-starts on first `agentctl` command.
|
|
@@ -135,7 +193,76 @@ agentctl daemon install # Install macOS LaunchAgent (auto-start on login)
|
|
|
135
193
|
agentctl daemon uninstall # Remove LaunchAgent
|
|
136
194
|
```
|
|
137
195
|
|
|
138
|
-
Metrics are exposed at `http://localhost:9200/metrics` in Prometheus text format.
|
|
196
|
+
Metrics are exposed at `http://localhost:9200/metrics` in Prometheus text format. See [Prometheus Metrics](#prometheus-metrics) for the full list.
|
|
197
|
+
|
|
198
|
+
### Events
|
|
199
|
+
|
|
200
|
+
`agentctl events` streams session lifecycle events as NDJSON (newline-delimited JSON). Each line is a self-contained JSON object:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{"type":"session.started","adapter":"claude-code","sessionId":"abc123...","timestamp":"2025-06-15T10:30:00.000Z","session":{...}}
|
|
204
|
+
{"type":"session.stopped","adapter":"claude-code","sessionId":"abc123...","timestamp":"2025-06-15T11:00:00.000Z","session":{...}}
|
|
205
|
+
{"type":"session.idle","adapter":"claude-code","sessionId":"def456...","timestamp":"2025-06-15T11:05:00.000Z","session":{...}}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Event types: `session.started`, `session.stopped`, `session.idle`, `session.error`.
|
|
209
|
+
|
|
210
|
+
The `session` field contains the full session object (id, adapter, status, cwd, model, prompt, tokens, etc.).
|
|
211
|
+
|
|
212
|
+
**Piping events into OrgLoop (or similar event routers):**
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# Pipe lifecycle events to OrgLoop's webhook endpoint
|
|
216
|
+
agentctl events | while IFS= read -r event; do
|
|
217
|
+
curl -s -X POST https://orgloop.example.com/hooks/agentctl \
|
|
218
|
+
-H "Content-Type: application/json" \
|
|
219
|
+
-d "$event"
|
|
220
|
+
done
|
|
221
|
+
|
|
222
|
+
# Or use a persistent pipe with jq filtering
|
|
223
|
+
agentctl events | jq -c 'select(.type == "session.stopped")' | while IFS= read -r event; do
|
|
224
|
+
curl -s -X POST https://orgloop.example.com/hooks/session-ended \
|
|
225
|
+
-H "Content-Type: application/json" \
|
|
226
|
+
-d "$event"
|
|
227
|
+
done
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
This pattern works with any webhook-based system — OrgLoop, Zapier, n8n, or a custom event router. The NDJSON format is compatible with standard Unix tools (`jq`, `grep`, `awk`) for filtering and transformation.
|
|
231
|
+
|
|
232
|
+
## Prometheus Metrics
|
|
233
|
+
|
|
234
|
+
The daemon exposes metrics at `http://localhost:9200/metrics` in Prometheus text format. Default port is 9200, configurable via `--metrics-port`.
|
|
235
|
+
|
|
236
|
+
### Gauges
|
|
237
|
+
|
|
238
|
+
| Metric | Labels | Description |
|
|
239
|
+
|--------|--------|-------------|
|
|
240
|
+
| `agentctl_sessions_active` | — | Number of currently active sessions |
|
|
241
|
+
| `agentctl_locks_active` | `type="auto"\|"manual"` | Number of active directory locks by type |
|
|
242
|
+
| `agentctl_fuses_active` | — | Number of active fuse timers |
|
|
243
|
+
|
|
244
|
+
### Counters
|
|
245
|
+
|
|
246
|
+
| Metric | Labels | Description |
|
|
247
|
+
|--------|--------|-------------|
|
|
248
|
+
| `agentctl_sessions_total` | `status="completed"\|"failed"\|"stopped"` | Total sessions by final status |
|
|
249
|
+
| `agentctl_fuses_fired_total` | — | Total fuse timers that fired (clusters deleted) |
|
|
250
|
+
| `agentctl_kind_clusters_deleted_total` | — | Total Kind clusters deleted by fuse timers |
|
|
251
|
+
|
|
252
|
+
### Histogram
|
|
253
|
+
|
|
254
|
+
| Metric | Buckets (seconds) | Description |
|
|
255
|
+
|--------|-------------------|-------------|
|
|
256
|
+
| `agentctl_session_duration_seconds` | 60, 300, 600, 1800, 3600, 7200, +Inf | Session duration distribution |
|
|
257
|
+
|
|
258
|
+
Example scrape config for Prometheus:
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
scrape_configs:
|
|
262
|
+
- job_name: agentctl
|
|
263
|
+
static_configs:
|
|
264
|
+
- targets: ["localhost:9200"]
|
|
265
|
+
```
|
|
139
266
|
|
|
140
267
|
## Architecture
|
|
141
268
|
|
|
@@ -155,6 +282,8 @@ Reads session data from `~/.claude/projects/` and cross-references with running
|
|
|
155
282
|
|
|
156
283
|
Connects to the OpenClaw gateway via WebSocket RPC. Read-only — sessions are managed through the gateway.
|
|
157
284
|
|
|
285
|
+
Requires the `OPENCLAW_WEBHOOK_TOKEN` environment variable. The adapter warns clearly if the token is missing or authentication fails.
|
|
286
|
+
|
|
158
287
|
### Writing an Adapter
|
|
159
288
|
|
|
160
289
|
Implement the `AgentAdapter` interface:
|
|
@@ -17,6 +17,11 @@ export class OpenClawAdapter {
|
|
|
17
17
|
this.rpcCall = opts?.rpcCall || this.defaultRpcCall.bind(this);
|
|
18
18
|
}
|
|
19
19
|
async list(opts) {
|
|
20
|
+
if (!this.authToken) {
|
|
21
|
+
console.warn("Warning: OPENCLAW_WEBHOOK_TOKEN is not set — OpenClaw adapter cannot authenticate. " +
|
|
22
|
+
"Set this environment variable to connect to the gateway.");
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
20
25
|
let result;
|
|
21
26
|
try {
|
|
22
27
|
result = (await this.rpcCall("sessions.list", {
|
|
@@ -24,8 +29,16 @@ export class OpenClawAdapter {
|
|
|
24
29
|
includeLastMessage: true,
|
|
25
30
|
}));
|
|
26
31
|
}
|
|
27
|
-
catch {
|
|
28
|
-
|
|
32
|
+
catch (err) {
|
|
33
|
+
const msg = err?.message || "unknown error";
|
|
34
|
+
if (msg.includes("auth") || msg.includes("Auth")) {
|
|
35
|
+
console.warn(`Warning: OpenClaw gateway authentication failed: ${msg}. ` +
|
|
36
|
+
"Check that OPENCLAW_WEBHOOK_TOKEN is valid.");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.warn(`Warning: OpenClaw gateway unreachable (${msg}). ` +
|
|
40
|
+
`Is the gateway running at ${this.baseUrl}?`);
|
|
41
|
+
}
|
|
29
42
|
return [];
|
|
30
43
|
}
|
|
31
44
|
let sessions = result.sessions.map((row) => this.mapRowToSession(row, result.defaults));
|
|
@@ -67,6 +80,9 @@ export class OpenClawAdapter {
|
|
|
67
80
|
return assistantMessages.slice(-limit).join("\n---\n");
|
|
68
81
|
}
|
|
69
82
|
async status(sessionId) {
|
|
83
|
+
if (!this.authToken) {
|
|
84
|
+
throw new Error("OPENCLAW_WEBHOOK_TOKEN is not set — cannot connect to OpenClaw gateway");
|
|
85
|
+
}
|
|
70
86
|
let result;
|
|
71
87
|
try {
|
|
72
88
|
result = (await this.rpcCall("sessions.list", {
|
|
@@ -184,14 +200,17 @@ export class OpenClawAdapter {
|
|
|
184
200
|
* Resolve a sessionId (or prefix) to a gateway session key.
|
|
185
201
|
*/
|
|
186
202
|
async resolveKey(sessionId) {
|
|
203
|
+
if (!this.authToken) {
|
|
204
|
+
throw new Error("OPENCLAW_WEBHOOK_TOKEN is not set — cannot connect to OpenClaw gateway");
|
|
205
|
+
}
|
|
187
206
|
let result;
|
|
188
207
|
try {
|
|
189
208
|
result = (await this.rpcCall("sessions.list", {
|
|
190
209
|
search: sessionId,
|
|
191
210
|
}));
|
|
192
211
|
}
|
|
193
|
-
catch {
|
|
194
|
-
|
|
212
|
+
catch (err) {
|
|
213
|
+
throw new Error(`Failed to resolve session ${sessionId}: ${err.message}`);
|
|
195
214
|
}
|
|
196
215
|
const row = result.sessions.find((s) => s.sessionId === sessionId ||
|
|
197
216
|
s.key === sessionId ||
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import os from "node:os";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const { version: PKG_VERSION } = require("../package.json");
|
|
5
8
|
import path from "node:path";
|
|
6
9
|
import { fileURLToPath } from "node:url";
|
|
7
10
|
import { Command } from "commander";
|
|
@@ -158,7 +161,7 @@ const program = new Command();
|
|
|
158
161
|
program
|
|
159
162
|
.name("agentctl")
|
|
160
163
|
.description("Universal agent supervision interface")
|
|
161
|
-
.version(
|
|
164
|
+
.version(PKG_VERSION);
|
|
162
165
|
// list
|
|
163
166
|
program
|
|
164
167
|
.command("list")
|