@orgloop/agentctl 1.0.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.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/adapters/claude-code.d.ts +83 -0
- package/dist/adapters/claude-code.js +783 -0
- package/dist/adapters/openclaw.d.ts +88 -0
- package/dist/adapters/openclaw.js +297 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +808 -0
- package/dist/client/daemon-client.d.ts +6 -0
- package/dist/client/daemon-client.js +81 -0
- package/dist/compat-shim.d.ts +2 -0
- package/dist/compat-shim.js +15 -0
- package/dist/core/types.d.ts +68 -0
- package/dist/core/types.js +2 -0
- package/dist/daemon/fuse-engine.d.ts +30 -0
- package/dist/daemon/fuse-engine.js +118 -0
- package/dist/daemon/launchagent.d.ts +7 -0
- package/dist/daemon/launchagent.js +49 -0
- package/dist/daemon/lock-manager.d.ts +16 -0
- package/dist/daemon/lock-manager.js +71 -0
- package/dist/daemon/metrics.d.ts +20 -0
- package/dist/daemon/metrics.js +72 -0
- package/dist/daemon/server.d.ts +33 -0
- package/dist/daemon/server.js +283 -0
- package/dist/daemon/session-tracker.d.ts +28 -0
- package/dist/daemon/session-tracker.js +121 -0
- package/dist/daemon/state.d.ts +61 -0
- package/dist/daemon/state.js +126 -0
- package/dist/daemon/supervisor.d.ts +24 -0
- package/dist/daemon/supervisor.js +79 -0
- package/dist/hooks.d.ts +19 -0
- package/dist/hooks.js +39 -0
- package/dist/merge.d.ts +24 -0
- package/dist/merge.js +65 -0
- package/dist/migration/migrate-locks.d.ts +5 -0
- package/dist/migration/migrate-locks.js +41 -0
- package/dist/worktree.d.ts +24 -0
- package/dist/worktree.js +65 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Charlie Hulcher
|
|
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,228 @@
|
|
|
1
|
+
# agentctl
|
|
2
|
+
|
|
3
|
+
Universal agent supervision interface. Monitor and control AI coding agents from a single CLI.
|
|
4
|
+
|
|
5
|
+
agentctl reads from native sources (Claude Code's `~/.claude/` directory, running processes) and provides a standard interface to list, inspect, stop, launch, and resume agent sessions. It never replicates state — it reads what's actually happening.
|
|
6
|
+
|
|
7
|
+
## Layer Model
|
|
8
|
+
|
|
9
|
+
| Layer | Role |
|
|
10
|
+
|-------|------|
|
|
11
|
+
| **agentctl** | Read/control interface. Discovers sessions, emits lifecycle events. |
|
|
12
|
+
| **OrgLoop** | Routes lifecycle events to mechanical reactions (cluster fuse, notifications). |
|
|
13
|
+
| **OpenClaw** | Reasoning layer. Makes judgment calls about what to do. |
|
|
14
|
+
|
|
15
|
+
agentctl intentionally does **not**:
|
|
16
|
+
- Maintain its own session registry (reads native sources)
|
|
17
|
+
- Have a hook/reaction system (that's OrgLoop)
|
|
18
|
+
- Make judgment calls about what to do (that's OpenClaw)
|
|
19
|
+
|
|
20
|
+
## Why agentctl?
|
|
21
|
+
|
|
22
|
+
You can use `claude code` (or any agent CLI) directly — agentctl is not a replacement. It's a supervisory layer for people and agents managing multiple concurrent coding sessions.
|
|
23
|
+
|
|
24
|
+
What it adds: session discovery across all running Claude Code instances, lifecycle tracking that persists session info even after processes exit, a daemon with directory locks to prevent duplicate launches on the same working directory, fuse timers for automated resource cleanup, and a standard interface that works the same regardless of which coding agent is underneath. The adapter model means support for additional agent runtimes (Codex, Aider, etc.) can be added without changing the CLI or daemon interface.
|
|
25
|
+
|
|
26
|
+
Over time, agentctl can extend to handle more concerns of headless coding — automating worktree bootstrap/teardown, running N parallel implementations across different adapters and models and judging who did it best, and other patterns that emerge as AI-assisted development matures.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g agentctl
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Node.js >= 20.
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# List running sessions
|
|
40
|
+
agentctl list
|
|
41
|
+
|
|
42
|
+
# List all sessions (including stopped, last 7 days)
|
|
43
|
+
agentctl list -a
|
|
44
|
+
|
|
45
|
+
# Peek at recent output from a session
|
|
46
|
+
agentctl peek <session-id>
|
|
47
|
+
|
|
48
|
+
# Launch a new Claude Code session
|
|
49
|
+
agentctl launch -p "Read the spec and implement phase 2"
|
|
50
|
+
|
|
51
|
+
# Stop a session
|
|
52
|
+
agentctl stop <session-id>
|
|
53
|
+
|
|
54
|
+
# Resume a stopped session with a new message
|
|
55
|
+
agentctl resume <session-id> "fix the failing tests"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Session IDs support prefix matching — `agentctl peek abc123` matches any session starting with `abc123`.
|
|
59
|
+
|
|
60
|
+
## CLI Reference
|
|
61
|
+
|
|
62
|
+
### Session Management
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
agentctl list [options]
|
|
66
|
+
--adapter <name> Filter by adapter (claude-code, openclaw)
|
|
67
|
+
--status <status> Filter by status (running|stopped|idle|error)
|
|
68
|
+
-a, --all Include stopped sessions (last 7 days)
|
|
69
|
+
--json Output as JSON
|
|
70
|
+
|
|
71
|
+
agentctl status <id> [options]
|
|
72
|
+
--adapter <name> Adapter to use
|
|
73
|
+
--json Output as JSON
|
|
74
|
+
|
|
75
|
+
agentctl peek <id> [options]
|
|
76
|
+
-n, --lines <n> Number of recent messages (default: 20)
|
|
77
|
+
--adapter <name> Adapter to use
|
|
78
|
+
|
|
79
|
+
agentctl launch [adapter] [options]
|
|
80
|
+
-p, --prompt <text> Prompt to send (required)
|
|
81
|
+
--spec <path> Spec file path
|
|
82
|
+
--cwd <dir> Working directory
|
|
83
|
+
--model <model> Model to use (e.g. sonnet, opus)
|
|
84
|
+
--force Override directory locks
|
|
85
|
+
|
|
86
|
+
agentctl stop <id> [options]
|
|
87
|
+
--force Force kill (SIGINT then SIGKILL)
|
|
88
|
+
--adapter <name> Adapter to use
|
|
89
|
+
|
|
90
|
+
agentctl resume <id> <message> [options]
|
|
91
|
+
--adapter <name> Adapter to use
|
|
92
|
+
|
|
93
|
+
agentctl events [options]
|
|
94
|
+
--json Output as NDJSON (default)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Directory Locks
|
|
98
|
+
|
|
99
|
+
agentctl tracks which directories have active sessions to prevent conflicting launches.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
agentctl lock <directory> [options]
|
|
103
|
+
--by <name> Who is locking
|
|
104
|
+
--reason <reason> Why
|
|
105
|
+
|
|
106
|
+
agentctl unlock <directory>
|
|
107
|
+
|
|
108
|
+
agentctl locks [options]
|
|
109
|
+
--json Output as JSON
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Fuse Timers
|
|
113
|
+
|
|
114
|
+
For Kind cluster management — automatically shuts down clusters when sessions end.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
agentctl fuses [options]
|
|
118
|
+
--json Output as JSON
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Daemon
|
|
122
|
+
|
|
123
|
+
The daemon provides session tracking, directory locks, fuse timers, and Prometheus metrics. It auto-starts on first `agentctl` command.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
agentctl daemon start [options]
|
|
127
|
+
--foreground Run in foreground (don't daemonize)
|
|
128
|
+
--metrics-port Prometheus metrics port (default: 9200)
|
|
129
|
+
|
|
130
|
+
agentctl daemon stop
|
|
131
|
+
agentctl daemon status
|
|
132
|
+
agentctl daemon restart
|
|
133
|
+
|
|
134
|
+
agentctl daemon install # Install macOS LaunchAgent (auto-start on login)
|
|
135
|
+
agentctl daemon uninstall # Remove LaunchAgent
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Metrics are exposed at `http://localhost:9200/metrics` in Prometheus text format.
|
|
139
|
+
|
|
140
|
+
## Architecture
|
|
141
|
+
|
|
142
|
+
agentctl is structured in three layers: the **CLI** parses commands and formats output, the **daemon** provides persistent state (session tracking, directory locks, fuse timers, Prometheus metrics), and **adapters** bridge to specific agent runtimes. The CLI communicates with the daemon over a Unix socket at `~/.agentctl/agentctl.sock`.
|
|
143
|
+
|
|
144
|
+
All session state is derived from native sources — agentctl never maintains its own session registry. The Claude Code adapter reads `~/.claude/projects/` and cross-references running processes; other adapters connect to their respective APIs. This means agentctl always reflects ground truth.
|
|
145
|
+
|
|
146
|
+
## Adapters
|
|
147
|
+
|
|
148
|
+
agentctl uses an adapter model to support different agent runtimes.
|
|
149
|
+
|
|
150
|
+
### Claude Code (default)
|
|
151
|
+
|
|
152
|
+
Reads session data from `~/.claude/projects/` and cross-references with running `claude` processes. Detects PID recycling via process start time verification. Tracks detached processes that survive wrapper exit.
|
|
153
|
+
|
|
154
|
+
### OpenClaw
|
|
155
|
+
|
|
156
|
+
Connects to the OpenClaw gateway via WebSocket RPC. Read-only — sessions are managed through the gateway.
|
|
157
|
+
|
|
158
|
+
### Writing an Adapter
|
|
159
|
+
|
|
160
|
+
Implement the `AgentAdapter` interface:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
interface AgentAdapter {
|
|
164
|
+
id: string;
|
|
165
|
+
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
166
|
+
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
167
|
+
status(sessionId: string): Promise<AgentSession>;
|
|
168
|
+
launch(opts: LaunchOpts): Promise<AgentSession>;
|
|
169
|
+
stop(sessionId: string, opts?: StopOpts): Promise<void>;
|
|
170
|
+
resume(sessionId: string, message: string): Promise<void>;
|
|
171
|
+
events(): AsyncIterable<LifecycleEvent>;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Configuration
|
|
176
|
+
|
|
177
|
+
agentctl stores daemon state in `~/.agentctl/`:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
~/.agentctl/
|
|
181
|
+
agentctl.sock # Unix socket for CLI ↔ daemon communication
|
|
182
|
+
agentctl.pid # Daemon PID file
|
|
183
|
+
state.json # Session tracking state
|
|
184
|
+
locks.json # Directory locks
|
|
185
|
+
fuses.json # Fuse timers
|
|
186
|
+
daemon.stdout.log # Daemon stdout
|
|
187
|
+
daemon.stderr.log # Daemon stderr
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Development
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
git clone https://github.com/orgloop/agentctl.git
|
|
194
|
+
cd agentctl
|
|
195
|
+
npm install
|
|
196
|
+
npm run build
|
|
197
|
+
npm link # makes agentctl available globally
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npm run dev # run CLI via tsx (no build needed)
|
|
202
|
+
npm test # vitest
|
|
203
|
+
npm run typecheck # tsc --noEmit
|
|
204
|
+
npm run lint # biome check
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Project Structure
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
src/
|
|
211
|
+
cli.ts # CLI entry point (commander)
|
|
212
|
+
core/types.ts # Core interfaces
|
|
213
|
+
adapters/claude-code.ts # Claude Code adapter
|
|
214
|
+
adapters/openclaw.ts # OpenClaw gateway adapter
|
|
215
|
+
daemon/server.ts # Daemon: Unix socket server + HTTP metrics
|
|
216
|
+
daemon/session-tracker.ts # Session lifecycle tracking
|
|
217
|
+
daemon/lock-manager.ts # Directory locks
|
|
218
|
+
daemon/fuse-engine.ts # Kind cluster fuse timers
|
|
219
|
+
daemon/metrics.ts # Prometheus metrics registry
|
|
220
|
+
daemon/state.ts # State persistence
|
|
221
|
+
daemon/launchagent.ts # macOS LaunchAgent plist generator
|
|
222
|
+
client/daemon-client.ts # Unix socket client
|
|
223
|
+
migration/migrate-locks.ts # Migration from legacy locks
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
2
|
+
export interface PidInfo {
|
|
3
|
+
pid: number;
|
|
4
|
+
cwd: string;
|
|
5
|
+
args: string;
|
|
6
|
+
/** Process start time from `ps -p <pid> -o lstart=`, used to detect PID recycling */
|
|
7
|
+
startTime?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Metadata persisted by launch() so status checks survive wrapper exit */
|
|
10
|
+
export interface LaunchedSessionMeta {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
pid: number;
|
|
13
|
+
/** Process start time from `ps -p <pid> -o lstart=` for PID recycling detection */
|
|
14
|
+
startTime?: string;
|
|
15
|
+
/** The PID of the wrapper (agentctl launch) — may differ from `pid` (Claude Code process) */
|
|
16
|
+
wrapperPid?: number;
|
|
17
|
+
cwd: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
prompt?: string;
|
|
20
|
+
launchedAt: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ClaudeCodeAdapterOpts {
|
|
23
|
+
claudeDir?: string;
|
|
24
|
+
sessionsMetaDir?: string;
|
|
25
|
+
getPids?: () => Promise<Map<number, PidInfo>>;
|
|
26
|
+
/** Override PID liveness check for testing (default: process.kill(pid, 0)) */
|
|
27
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Claude Code adapter — reads session data directly from ~/.claude/
|
|
31
|
+
* and cross-references with running PIDs. NEVER maintains its own registry.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
34
|
+
readonly id = "claude-code";
|
|
35
|
+
private readonly claudeDir;
|
|
36
|
+
private readonly projectsDir;
|
|
37
|
+
private readonly sessionsMetaDir;
|
|
38
|
+
private readonly getPids;
|
|
39
|
+
private readonly isProcessAlive;
|
|
40
|
+
constructor(opts?: ClaudeCodeAdapterOpts);
|
|
41
|
+
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
42
|
+
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
43
|
+
status(sessionId: string): Promise<AgentSession>;
|
|
44
|
+
launch(opts: LaunchOpts): Promise<AgentSession>;
|
|
45
|
+
/**
|
|
46
|
+
* Poll the launch log file for up to `timeoutMs` to extract the real session ID.
|
|
47
|
+
* Claude Code's stream-json output includes sessionId in early messages.
|
|
48
|
+
*/
|
|
49
|
+
private pollForSessionId;
|
|
50
|
+
stop(sessionId: string, opts?: StopOpts): Promise<void>;
|
|
51
|
+
resume(sessionId: string, message: string): Promise<void>;
|
|
52
|
+
events(): AsyncIterable<LifecycleEvent>;
|
|
53
|
+
/**
|
|
54
|
+
* Get session entries for a project — uses sessions-index.json when available,
|
|
55
|
+
* falls back to scanning .jsonl files for projects without an index
|
|
56
|
+
* (e.g. currently running sessions that haven't been indexed yet).
|
|
57
|
+
*/
|
|
58
|
+
private getEntriesForProject;
|
|
59
|
+
private buildSessionFromIndex;
|
|
60
|
+
private isSessionRunning;
|
|
61
|
+
/**
|
|
62
|
+
* Check whether a process plausibly belongs to a session by verifying
|
|
63
|
+
* the process started at or after the session's creation time.
|
|
64
|
+
* This detects PID recycling: if a process started before the session
|
|
65
|
+
* was created, it can't be the process that's running this session.
|
|
66
|
+
*
|
|
67
|
+
* When start time is unavailable, defaults to false (assume no match).
|
|
68
|
+
* This prevents old sessions from appearing as 'running' due to
|
|
69
|
+
* recycled PIDs when start time verification is impossible.
|
|
70
|
+
*/
|
|
71
|
+
private processStartedAfterSession;
|
|
72
|
+
private findMatchingPid;
|
|
73
|
+
private parseSessionTail;
|
|
74
|
+
private findSessionFile;
|
|
75
|
+
private findIndexEntry;
|
|
76
|
+
private findPidForSession;
|
|
77
|
+
/** Write session metadata to disk so status checks survive wrapper exit */
|
|
78
|
+
writeSessionMeta(meta: Omit<LaunchedSessionMeta, "startTime">): Promise<void>;
|
|
79
|
+
/** Read persisted session metadata */
|
|
80
|
+
readSessionMeta(sessionId: string): Promise<LaunchedSessionMeta | null>;
|
|
81
|
+
/** Delete stale session metadata */
|
|
82
|
+
private deleteSessionMeta;
|
|
83
|
+
}
|