@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
@@ -0,0 +1,6 @@
1
+ export declare class DaemonClient {
2
+ private sockPath;
3
+ constructor(sockPath?: string);
4
+ call<T>(method: string, params?: unknown): Promise<T>;
5
+ isRunning(): Promise<boolean>;
6
+ }
@@ -0,0 +1,81 @@
1
+ import crypto from "node:crypto";
2
+ import net from "node:net";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ const DEFAULT_SOCK_PATH = path.join(os.homedir(), ".agentctl", "agentctl.sock");
6
+ export class DaemonClient {
7
+ sockPath;
8
+ constructor(sockPath) {
9
+ this.sockPath = sockPath || DEFAULT_SOCK_PATH;
10
+ }
11
+ async call(method, params) {
12
+ return new Promise((resolve, reject) => {
13
+ const socket = net.createConnection(this.sockPath);
14
+ const id = crypto.randomUUID();
15
+ let buffer = "";
16
+ let settled = false;
17
+ const timeout = setTimeout(() => {
18
+ if (!settled) {
19
+ settled = true;
20
+ socket.destroy();
21
+ reject(new Error("Daemon request timed out"));
22
+ }
23
+ }, 30_000);
24
+ socket.on("connect", () => {
25
+ socket.write(`${JSON.stringify({ id, method, params })}\n`);
26
+ });
27
+ socket.on("data", (chunk) => {
28
+ buffer += chunk.toString();
29
+ const lines = buffer.split("\n");
30
+ buffer = lines.pop() ?? "";
31
+ for (const line of lines) {
32
+ if (!line)
33
+ continue;
34
+ try {
35
+ const msg = JSON.parse(line);
36
+ if (msg.id === id && !msg.stream) {
37
+ settled = true;
38
+ clearTimeout(timeout);
39
+ if (msg.error)
40
+ reject(new Error(msg.error.message));
41
+ else
42
+ resolve(msg.result);
43
+ socket.end();
44
+ }
45
+ }
46
+ catch {
47
+ // Malformed response
48
+ }
49
+ }
50
+ });
51
+ socket.on("error", (err) => {
52
+ if (settled)
53
+ return;
54
+ settled = true;
55
+ clearTimeout(timeout);
56
+ if (err.code === "ENOENT" || err.code === "ECONNREFUSED") {
57
+ reject(new Error("Daemon not running. Start with: agentctl daemon start"));
58
+ }
59
+ else {
60
+ reject(err);
61
+ }
62
+ });
63
+ socket.on("close", () => {
64
+ if (!settled) {
65
+ settled = true;
66
+ clearTimeout(timeout);
67
+ reject(new Error("Connection closed before response received"));
68
+ }
69
+ });
70
+ });
71
+ }
72
+ async isRunning() {
73
+ try {
74
+ await this.call("daemon.status");
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ console.error("\u26a0\ufe0f agent-ctl is renamed to agentctl. Please update your scripts.");
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const agentctlBin = path.join(path.dirname(__filename), "cli.js");
8
+ try {
9
+ execFileSync(process.execPath, [agentctlBin, ...process.argv.slice(2)], {
10
+ stdio: "inherit",
11
+ });
12
+ }
13
+ catch (err) {
14
+ process.exit(err.status ?? 1);
15
+ }
@@ -0,0 +1,68 @@
1
+ export interface AgentAdapter {
2
+ id: string;
3
+ list(opts?: ListOpts): Promise<AgentSession[]>;
4
+ peek(sessionId: string, opts?: PeekOpts): Promise<string>;
5
+ status(sessionId: string): Promise<AgentSession>;
6
+ launch(opts: LaunchOpts): Promise<AgentSession>;
7
+ stop(sessionId: string, opts?: StopOpts): Promise<void>;
8
+ resume(sessionId: string, message: string): Promise<void>;
9
+ events(): AsyncIterable<LifecycleEvent>;
10
+ }
11
+ export interface AgentSession {
12
+ id: string;
13
+ adapter: string;
14
+ status: "running" | "idle" | "stopped" | "error";
15
+ startedAt: Date;
16
+ stoppedAt?: Date;
17
+ cwd?: string;
18
+ spec?: string;
19
+ model?: string;
20
+ prompt?: string;
21
+ tokens?: {
22
+ in: number;
23
+ out: number;
24
+ };
25
+ cost?: number;
26
+ pid?: number;
27
+ meta: Record<string, unknown>;
28
+ }
29
+ export interface LifecycleEvent {
30
+ type: "session.started" | "session.stopped" | "session.idle" | "session.error";
31
+ adapter: string;
32
+ sessionId: string;
33
+ session: AgentSession;
34
+ timestamp: Date;
35
+ meta?: Record<string, unknown>;
36
+ }
37
+ export interface ListOpts {
38
+ status?: "running" | "idle" | "stopped" | "error";
39
+ all?: boolean;
40
+ }
41
+ export interface PeekOpts {
42
+ lines?: number;
43
+ }
44
+ export interface StopOpts {
45
+ force?: boolean;
46
+ }
47
+ export interface LaunchOpts {
48
+ adapter: string;
49
+ prompt: string;
50
+ spec?: string;
51
+ cwd?: string;
52
+ model?: string;
53
+ env?: Record<string, string>;
54
+ adapterOpts?: Record<string, unknown>;
55
+ /** Git worktree options — auto-create worktree before launch */
56
+ worktree?: {
57
+ repo: string;
58
+ branch: string;
59
+ };
60
+ /** Lifecycle hooks — shell commands to run at various points */
61
+ hooks?: LifecycleHooks;
62
+ }
63
+ export interface LifecycleHooks {
64
+ onCreate?: string;
65
+ onComplete?: string;
66
+ preMerge?: string;
67
+ postMerge?: string;
68
+ }
@@ -0,0 +1,2 @@
1
+ // agentctl core types — Universal Agent Supervision Interface
2
+ export {};
@@ -0,0 +1,30 @@
1
+ import type { EventEmitter } from "node:events";
2
+ import type { FuseTimer, SessionRecord, StateManager } from "./state.js";
3
+ export interface FuseEngineOpts {
4
+ defaultDurationMs: number;
5
+ emitter?: EventEmitter;
6
+ }
7
+ export declare class FuseEngine {
8
+ private timers;
9
+ private state;
10
+ private defaultDurationMs;
11
+ private emitter?;
12
+ constructor(state: StateManager, opts: FuseEngineOpts);
13
+ /** Derive cluster name from worktree directory. Returns null if not a mono worktree. */
14
+ static deriveClusterName(directory: string): {
15
+ clusterName: string;
16
+ branch: string;
17
+ } | null;
18
+ /** Called when a session exits. Starts fuse if applicable. */
19
+ onSessionExit(session: SessionRecord): void;
20
+ private startFuse;
21
+ /** Cancel fuse for a directory. */
22
+ cancelFuse(directory: string, persist?: boolean): boolean;
23
+ /** Resume fuses from persisted state after daemon restart. */
24
+ resumeTimers(): void;
25
+ /** Fire a fuse — delete the Kind cluster. */
26
+ private fireFuse;
27
+ listActive(): FuseTimer[];
28
+ /** Clear all timers (for clean shutdown) */
29
+ shutdown(): void;
30
+ }
@@ -0,0 +1,118 @@
1
+ import { exec } from "node:child_process";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ const execAsync = promisify(exec);
6
+ export class FuseEngine {
7
+ timers = new Map();
8
+ state;
9
+ defaultDurationMs;
10
+ emitter;
11
+ constructor(state, opts) {
12
+ this.state = state;
13
+ this.defaultDurationMs = opts.defaultDurationMs;
14
+ this.emitter = opts.emitter;
15
+ }
16
+ /** Derive cluster name from worktree directory. Returns null if not a mono worktree. */
17
+ static deriveClusterName(directory) {
18
+ const home = os.homedir();
19
+ const monoPrefix = path.join(home, "code", "mono-");
20
+ if (!directory.startsWith(monoPrefix))
21
+ return null;
22
+ const branch = directory.slice(monoPrefix.length);
23
+ if (!branch)
24
+ return null;
25
+ return {
26
+ clusterName: `kindo-charlie-${branch}`,
27
+ branch,
28
+ };
29
+ }
30
+ /** Called when a session exits. Starts fuse if applicable. */
31
+ onSessionExit(session) {
32
+ if (!session.cwd)
33
+ return;
34
+ const derived = FuseEngine.deriveClusterName(session.cwd);
35
+ if (!derived)
36
+ return;
37
+ this.startFuse(session.cwd, derived.clusterName, derived.branch, session.id);
38
+ }
39
+ startFuse(directory, clusterName, branch, sessionId) {
40
+ // Cancel existing fuse for same directory
41
+ this.cancelFuse(directory, false);
42
+ const expiresAt = new Date(Date.now() + this.defaultDurationMs);
43
+ const fuse = {
44
+ directory,
45
+ clusterName,
46
+ branch,
47
+ expiresAt: expiresAt.toISOString(),
48
+ sessionId,
49
+ };
50
+ this.state.addFuse(fuse);
51
+ const timeout = setTimeout(() => this.fireFuse(fuse), this.defaultDurationMs);
52
+ this.timers.set(directory, timeout);
53
+ }
54
+ /** Cancel fuse for a directory. */
55
+ cancelFuse(directory, persist = true) {
56
+ const timer = this.timers.get(directory);
57
+ if (timer) {
58
+ clearTimeout(timer);
59
+ this.timers.delete(directory);
60
+ }
61
+ if (persist) {
62
+ this.state.removeFuse(directory);
63
+ }
64
+ return !!timer;
65
+ }
66
+ /** Resume fuses from persisted state after daemon restart. */
67
+ resumeTimers() {
68
+ const fuses = this.state.getFuses();
69
+ const now = Date.now();
70
+ for (const fuse of fuses) {
71
+ const remaining = new Date(fuse.expiresAt).getTime() - now;
72
+ if (remaining <= 0) {
73
+ // Expired while daemon was down — fire immediately
74
+ this.fireFuse(fuse);
75
+ }
76
+ else {
77
+ const timeout = setTimeout(() => this.fireFuse(fuse), remaining);
78
+ this.timers.set(fuse.directory, timeout);
79
+ }
80
+ }
81
+ }
82
+ /** Fire a fuse — delete the Kind cluster. */
83
+ async fireFuse(fuse) {
84
+ this.timers.delete(fuse.directory);
85
+ this.state.removeFuse(fuse.directory);
86
+ console.log(`Fuse fired: deleting cluster ${fuse.clusterName}`);
87
+ try {
88
+ // Best effort: yarn local:down first
89
+ try {
90
+ await execAsync("yarn local:down", {
91
+ cwd: fuse.directory,
92
+ timeout: 60_000,
93
+ });
94
+ }
95
+ catch {
96
+ // Ignore
97
+ }
98
+ await execAsync(`kind delete cluster --name ${fuse.clusterName}`, {
99
+ timeout: 120_000,
100
+ });
101
+ console.log(`Cluster ${fuse.clusterName} deleted`);
102
+ this.emitter?.emit("fuse.fired", fuse);
103
+ }
104
+ catch (err) {
105
+ console.error(`Failed to delete cluster ${fuse.clusterName}:`, err);
106
+ }
107
+ }
108
+ listActive() {
109
+ return this.state.getFuses();
110
+ }
111
+ /** Clear all timers (for clean shutdown) */
112
+ shutdown() {
113
+ for (const timer of this.timers.values()) {
114
+ clearTimeout(timer);
115
+ }
116
+ this.timers.clear();
117
+ }
118
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Generate a LaunchAgent plist with dynamically resolved paths.
3
+ */
4
+ export declare function generatePlist(opts?: {
5
+ nodePath?: string;
6
+ cliPath?: string;
7
+ }): string;
@@ -0,0 +1,49 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /**
5
+ * Generate a LaunchAgent plist with dynamically resolved paths.
6
+ */
7
+ export function generatePlist(opts) {
8
+ const home = os.homedir();
9
+ const nodePath = opts?.nodePath || process.execPath;
10
+ const cliPath = opts?.cliPath ||
11
+ path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "cli.js");
12
+ // Normalize to absolute
13
+ const resolvedCliPath = path.resolve(cliPath);
14
+ // Get PATH including node's bin dir
15
+ const nodeBinDir = path.dirname(nodePath);
16
+ const envPath = `/usr/local/bin:/usr/bin:/bin:${nodeBinDir}`;
17
+ return `<?xml version="1.0" encoding="UTF-8"?>
18
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
19
+ <plist version="1.0">
20
+ <dict>
21
+ <key>Label</key>
22
+ <string>com.agentctl.daemon</string>
23
+ <key>ProgramArguments</key>
24
+ <array>
25
+ <string>${nodePath}</string>
26
+ <string>${resolvedCliPath}</string>
27
+ <string>daemon</string>
28
+ <string>start</string>
29
+ <string>--foreground</string>
30
+ </array>
31
+ <key>RunAtLoad</key>
32
+ <true/>
33
+ <key>KeepAlive</key>
34
+ <true/>
35
+ <key>StandardOutPath</key>
36
+ <string>${home}/.agentctl/daemon.stdout.log</string>
37
+ <key>StandardErrorPath</key>
38
+ <string>${home}/.agentctl/daemon.stderr.log</string>
39
+ <key>EnvironmentVariables</key>
40
+ <dict>
41
+ <key>PATH</key>
42
+ <string>${envPath}</string>
43
+ <key>HOME</key>
44
+ <string>${home}</string>
45
+ </dict>
46
+ </dict>
47
+ </plist>
48
+ `;
49
+ }
@@ -0,0 +1,16 @@
1
+ import type { Lock, StateManager } from "./state.js";
2
+ export declare class LockManager {
3
+ private state;
4
+ constructor(state: StateManager);
5
+ /** Check if directory is locked. Returns the lock if so, null if free. */
6
+ check(directory: string): Lock | null;
7
+ /** Auto-lock a directory for a session. Idempotent if same session. */
8
+ autoLock(directory: string, sessionId: string): Lock;
9
+ /** Remove auto-lock for a session. */
10
+ autoUnlock(sessionId: string): void;
11
+ /** Manual lock. Fails if already manually locked. */
12
+ manualLock(directory: string, by?: string, reason?: string): Lock;
13
+ /** Manual unlock. Only removes manual locks. */
14
+ manualUnlock(directory: string): void;
15
+ listAll(): Lock[];
16
+ }
@@ -0,0 +1,71 @@
1
+ import path from "node:path";
2
+ export class LockManager {
3
+ state;
4
+ constructor(state) {
5
+ this.state = state;
6
+ }
7
+ /** Check if directory is locked. Returns the lock if so, null if free. */
8
+ check(directory) {
9
+ const resolved = path.resolve(directory);
10
+ const locks = this.state.getLocks();
11
+ // Manual locks take precedence
12
+ const manual = locks.find((l) => l.directory === resolved && l.type === "manual");
13
+ if (manual)
14
+ return manual;
15
+ const auto = locks.find((l) => l.directory === resolved && l.type === "auto");
16
+ return auto || null;
17
+ }
18
+ /** Auto-lock a directory for a session. Idempotent if same session. */
19
+ autoLock(directory, sessionId) {
20
+ const resolved = path.resolve(directory);
21
+ const existing = this.state
22
+ .getLocks()
23
+ .find((l) => l.directory === resolved &&
24
+ l.type === "auto" &&
25
+ l.sessionId === sessionId);
26
+ if (existing)
27
+ return existing;
28
+ const lock = {
29
+ directory: resolved,
30
+ type: "auto",
31
+ sessionId,
32
+ lockedAt: new Date().toISOString(),
33
+ };
34
+ this.state.addLock(lock);
35
+ return lock;
36
+ }
37
+ /** Remove auto-lock for a session. */
38
+ autoUnlock(sessionId) {
39
+ this.state.removeLocks((l) => l.type === "auto" && l.sessionId === sessionId);
40
+ }
41
+ /** Manual lock. Fails if already manually locked. */
42
+ manualLock(directory, by, reason) {
43
+ const resolved = path.resolve(directory);
44
+ const existing = this.check(resolved);
45
+ if (existing?.type === "manual") {
46
+ throw new Error(`Already manually locked by ${existing.lockedBy}: ${existing.reason}`);
47
+ }
48
+ const lock = {
49
+ directory: resolved,
50
+ type: "manual",
51
+ lockedBy: by,
52
+ reason,
53
+ lockedAt: new Date().toISOString(),
54
+ };
55
+ this.state.addLock(lock);
56
+ return lock;
57
+ }
58
+ /** Manual unlock. Only removes manual locks. */
59
+ manualUnlock(directory) {
60
+ const resolved = path.resolve(directory);
61
+ const existing = this.state
62
+ .getLocks()
63
+ .find((l) => l.directory === resolved && l.type === "manual");
64
+ if (!existing)
65
+ throw new Error(`No manual lock on ${resolved}`);
66
+ this.state.removeLocks((l) => l.directory === resolved && l.type === "manual");
67
+ }
68
+ listAll() {
69
+ return this.state.getLocks();
70
+ }
71
+ }
@@ -0,0 +1,20 @@
1
+ import type { FuseEngine } from "./fuse-engine.js";
2
+ import type { LockManager } from "./lock-manager.js";
3
+ import type { SessionTracker } from "./session-tracker.js";
4
+ export declare class MetricsRegistry {
5
+ private sessionTracker;
6
+ private lockManager;
7
+ private fuseEngine;
8
+ sessionsTotalCompleted: number;
9
+ sessionsTotalFailed: number;
10
+ sessionsTotalStopped: number;
11
+ fusesFiredTotal: number;
12
+ clustersDeletedTotal: number;
13
+ sessionDurations: number[];
14
+ constructor(sessionTracker: SessionTracker, lockManager: LockManager, fuseEngine: FuseEngine);
15
+ recordSessionCompleted(durationSeconds?: number): void;
16
+ recordSessionFailed(durationSeconds?: number): void;
17
+ recordSessionStopped(durationSeconds?: number): void;
18
+ recordFuseFired(): void;
19
+ generateMetrics(): string;
20
+ }
@@ -0,0 +1,72 @@
1
+ export class MetricsRegistry {
2
+ sessionTracker;
3
+ lockManager;
4
+ fuseEngine;
5
+ sessionsTotalCompleted = 0;
6
+ sessionsTotalFailed = 0;
7
+ sessionsTotalStopped = 0;
8
+ fusesFiredTotal = 0;
9
+ clustersDeletedTotal = 0;
10
+ sessionDurations = []; // seconds
11
+ constructor(sessionTracker, lockManager, fuseEngine) {
12
+ this.sessionTracker = sessionTracker;
13
+ this.lockManager = lockManager;
14
+ this.fuseEngine = fuseEngine;
15
+ }
16
+ recordSessionCompleted(durationSeconds) {
17
+ this.sessionsTotalCompleted++;
18
+ if (durationSeconds != null)
19
+ this.sessionDurations.push(durationSeconds);
20
+ }
21
+ recordSessionFailed(durationSeconds) {
22
+ this.sessionsTotalFailed++;
23
+ if (durationSeconds != null)
24
+ this.sessionDurations.push(durationSeconds);
25
+ }
26
+ recordSessionStopped(durationSeconds) {
27
+ this.sessionsTotalStopped++;
28
+ if (durationSeconds != null)
29
+ this.sessionDurations.push(durationSeconds);
30
+ }
31
+ recordFuseFired() {
32
+ this.fusesFiredTotal++;
33
+ this.clustersDeletedTotal++;
34
+ }
35
+ generateMetrics() {
36
+ const lines = [];
37
+ const g = (name, help, value, labels) => {
38
+ lines.push(`# HELP ${name} ${help}`);
39
+ lines.push(`# TYPE ${name} gauge`);
40
+ lines.push(labels ? `${name}{${labels}} ${value}` : `${name} ${value}`);
41
+ };
42
+ const c = (name, help, value, labels) => {
43
+ lines.push(`# HELP ${name} ${help}`);
44
+ lines.push(`# TYPE ${name} counter`);
45
+ lines.push(labels ? `${name}{${labels}} ${value}` : `${name} ${value}`);
46
+ };
47
+ // Gauges
48
+ g("agentctl_sessions_active", "Number of active sessions", this.sessionTracker.activeCount());
49
+ const locks = this.lockManager.listAll();
50
+ g("agentctl_locks_active", "Number of active locks", locks.filter((l) => l.type === "auto").length, 'type="auto"');
51
+ g("agentctl_locks_active", "Number of active locks", locks.filter((l) => l.type === "manual").length, 'type="manual"');
52
+ g("agentctl_fuses_active", "Number of active fuse timers", this.fuseEngine.listActive().length);
53
+ // Counters
54
+ c("agentctl_sessions_total", "Total sessions by status", this.sessionsTotalCompleted, 'status="completed"');
55
+ c("agentctl_sessions_total", "Total sessions by status", this.sessionsTotalFailed, 'status="failed"');
56
+ c("agentctl_sessions_total", "Total sessions by status", this.sessionsTotalStopped, 'status="stopped"');
57
+ c("agentctl_fuses_fired_total", "Total fuses fired", this.fusesFiredTotal);
58
+ c("agentctl_kind_clusters_deleted_total", "Total Kind clusters deleted", this.clustersDeletedTotal);
59
+ // Histogram (session duration)
60
+ lines.push("# HELP agentctl_session_duration_seconds Session duration histogram");
61
+ lines.push("# TYPE agentctl_session_duration_seconds histogram");
62
+ const buckets = [60, 300, 600, 1800, 3600, 7200, Number.POSITIVE_INFINITY];
63
+ for (const b of buckets) {
64
+ const count = this.sessionDurations.filter((d) => d <= b).length;
65
+ const label = b === Number.POSITIVE_INFINITY ? "+Inf" : String(b);
66
+ lines.push(`agentctl_session_duration_seconds_bucket{le="${label}"} ${count}`);
67
+ }
68
+ lines.push(`agentctl_session_duration_seconds_sum ${this.sessionDurations.reduce((a, b) => a + b, 0)}`);
69
+ lines.push(`agentctl_session_duration_seconds_count ${this.sessionDurations.length}`);
70
+ return `${lines.join("\n")}\n`;
71
+ }
72
+ }
@@ -0,0 +1,33 @@
1
+ import http from "node:http";
2
+ import net from "node:net";
3
+ import type { AgentAdapter } from "../core/types.js";
4
+ export interface DaemonRequest {
5
+ id: string;
6
+ method: string;
7
+ params?: unknown;
8
+ }
9
+ export interface DaemonResponse {
10
+ id: string;
11
+ result?: unknown;
12
+ error?: {
13
+ code: string;
14
+ message: string;
15
+ };
16
+ }
17
+ export interface DaemonStatus {
18
+ pid: number;
19
+ uptime: number;
20
+ sessions: number;
21
+ locks: number;
22
+ fuses: number;
23
+ }
24
+ export interface DaemonStartOpts {
25
+ metricsPort?: number;
26
+ configDir?: string;
27
+ adapters?: Record<string, AgentAdapter>;
28
+ }
29
+ export declare function startDaemon(opts?: DaemonStartOpts): Promise<{
30
+ socketServer: net.Server;
31
+ httpServer: http.Server;
32
+ shutdown: () => Promise<void>;
33
+ }>;