@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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/dist/adapters/claude-code.d.ts +83 -0
  4. package/dist/adapters/claude-code.js +783 -0
  5. package/dist/adapters/openclaw.d.ts +88 -0
  6. package/dist/adapters/openclaw.js +297 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +808 -0
  9. package/dist/client/daemon-client.d.ts +6 -0
  10. package/dist/client/daemon-client.js +81 -0
  11. package/dist/compat-shim.d.ts +2 -0
  12. package/dist/compat-shim.js +15 -0
  13. package/dist/core/types.d.ts +68 -0
  14. package/dist/core/types.js +2 -0
  15. package/dist/daemon/fuse-engine.d.ts +30 -0
  16. package/dist/daemon/fuse-engine.js +118 -0
  17. package/dist/daemon/launchagent.d.ts +7 -0
  18. package/dist/daemon/launchagent.js +49 -0
  19. package/dist/daemon/lock-manager.d.ts +16 -0
  20. package/dist/daemon/lock-manager.js +71 -0
  21. package/dist/daemon/metrics.d.ts +20 -0
  22. package/dist/daemon/metrics.js +72 -0
  23. package/dist/daemon/server.d.ts +33 -0
  24. package/dist/daemon/server.js +283 -0
  25. package/dist/daemon/session-tracker.d.ts +28 -0
  26. package/dist/daemon/session-tracker.js +121 -0
  27. package/dist/daemon/state.d.ts +61 -0
  28. package/dist/daemon/state.js +126 -0
  29. package/dist/daemon/supervisor.d.ts +24 -0
  30. package/dist/daemon/supervisor.js +79 -0
  31. package/dist/hooks.d.ts +19 -0
  32. package/dist/hooks.js +39 -0
  33. package/dist/merge.d.ts +24 -0
  34. package/dist/merge.js +65 -0
  35. package/dist/migration/migrate-locks.d.ts +5 -0
  36. package/dist/migration/migrate-locks.js +41 -0
  37. package/dist/worktree.d.ts +24 -0
  38. package/dist/worktree.js +65 -0
  39. 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
+ }