@lawrence369/loop-cli 0.1.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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/agent/activity.d.ts +64 -0
- package/dist/agent/activity.js +265 -0
- package/dist/agent/launcher.d.ts +42 -0
- package/dist/agent/launcher.js +243 -0
- package/dist/agent/pty-session.d.ts +113 -0
- package/dist/agent/pty-session.js +490 -0
- package/dist/agent/ready-detector.d.ts +46 -0
- package/dist/agent/ready-detector.js +86 -0
- package/dist/agent/wrapper.d.ts +18 -0
- package/dist/agent/wrapper.js +110 -0
- package/dist/bin/lclaude.d.ts +3 -0
- package/dist/bin/lclaude.js +7 -0
- package/dist/bin/lcodex.d.ts +3 -0
- package/dist/bin/lcodex.js +7 -0
- package/dist/bin/lgemini.d.ts +3 -0
- package/dist/bin/lgemini.js +7 -0
- package/dist/bus/daemon.d.ts +56 -0
- package/dist/bus/daemon.js +135 -0
- package/dist/bus/event-bus.d.ts +105 -0
- package/dist/bus/event-bus.js +157 -0
- package/dist/bus/message.d.ts +48 -0
- package/dist/bus/message.js +129 -0
- package/dist/bus/queue.d.ts +50 -0
- package/dist/bus/queue.js +100 -0
- package/dist/bus/store.d.ts +88 -0
- package/dist/bus/store.js +212 -0
- package/dist/bus/subscriber.d.ts +76 -0
- package/dist/bus/subscriber.js +187 -0
- package/dist/config/index.d.ts +8 -0
- package/dist/config/index.js +72 -0
- package/dist/config/schema.d.ts +18 -0
- package/dist/config/schema.js +58 -0
- package/dist/core/conversation.d.ts +34 -0
- package/dist/core/conversation.js +289 -0
- package/dist/core/engine.d.ts +40 -0
- package/dist/core/engine.js +288 -0
- package/dist/core/loop.d.ts +33 -0
- package/dist/core/loop.js +209 -0
- package/dist/core/protocol.d.ts +60 -0
- package/dist/core/protocol.js +162 -0
- package/dist/core/scoring.d.ts +34 -0
- package/dist/core/scoring.js +69 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +408 -0
- package/dist/orchestrator/daemon.d.ts +74 -0
- package/dist/orchestrator/daemon.js +294 -0
- package/dist/orchestrator/group.d.ts +73 -0
- package/dist/orchestrator/group.js +166 -0
- package/dist/orchestrator/ipc-server.d.ts +60 -0
- package/dist/orchestrator/ipc-server.js +166 -0
- package/dist/orchestrator/scheduler.d.ts +32 -0
- package/dist/orchestrator/scheduler.js +95 -0
- package/dist/plan/context.d.ts +8 -0
- package/dist/plan/context.js +42 -0
- package/dist/plan/decisions.d.ts +18 -0
- package/dist/plan/decisions.js +143 -0
- package/dist/plan/shared-plan.d.ts +33 -0
- package/dist/plan/shared-plan.js +211 -0
- package/dist/skills/executor.d.ts +7 -0
- package/dist/skills/executor.js +11 -0
- package/dist/skills/loader.d.ts +16 -0
- package/dist/skills/loader.js +80 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +54 -0
- package/dist/terminal/adapter.d.ts +61 -0
- package/dist/terminal/adapter.js +42 -0
- package/dist/terminal/detect.d.ts +30 -0
- package/dist/terminal/detect.js +77 -0
- package/dist/terminal/iterm2-adapter.d.ts +19 -0
- package/dist/terminal/iterm2-adapter.js +120 -0
- package/dist/terminal/pty-adapter.d.ts +18 -0
- package/dist/terminal/pty-adapter.js +84 -0
- package/dist/terminal/terminal-adapter.d.ts +17 -0
- package/dist/terminal/terminal-adapter.js +94 -0
- package/dist/terminal/tmux-adapter.d.ts +18 -0
- package/dist/terminal/tmux-adapter.js +127 -0
- package/dist/ui/banner.d.ts +3 -0
- package/dist/ui/banner.js +145 -0
- package/dist/ui/colors.d.ts +41 -0
- package/dist/ui/colors.js +65 -0
- package/dist/ui/dashboard.d.ts +32 -0
- package/dist/ui/dashboard.js +138 -0
- package/dist/ui/input.d.ts +10 -0
- package/dist/ui/input.js +96 -0
- package/dist/ui/interactive.d.ts +13 -0
- package/dist/ui/interactive.js +230 -0
- package/dist/ui/renderer.d.ts +33 -0
- package/dist/ui/renderer.js +106 -0
- package/dist/utils/ansi.d.ts +11 -0
- package/dist/utils/ansi.js +16 -0
- package/dist/utils/fs.d.ts +34 -0
- package/dist/utils/fs.js +115 -0
- package/dist/utils/lock.d.ts +12 -0
- package/dist/utils/lock.js +116 -0
- package/dist/utils/process.d.ts +31 -0
- package/dist/utils/process.js +111 -0
- package/dist/utils/pty-filter.d.ts +31 -0
- package/dist/utils/pty-filter.js +187 -0
- package/package.json +71 -0
- package/skills/loop/SKILL.md +19 -0
- package/skills/plan/SKILL.md +9 -0
- package/skills/review/SKILL.md +14 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent wrapper — entry point for lclaude / lgemini / lcodex binaries.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the engine-specific CLI command, launches it through
|
|
5
|
+
* AgentLauncher, and forwards stdin/stdout for interactive use.
|
|
6
|
+
*/
|
|
7
|
+
import { AgentLauncher } from "./launcher.js";
|
|
8
|
+
const ENGINES = {
|
|
9
|
+
claude: {
|
|
10
|
+
command: "claude",
|
|
11
|
+
defaultArgs: [],
|
|
12
|
+
},
|
|
13
|
+
gemini: {
|
|
14
|
+
command: "gemini",
|
|
15
|
+
defaultArgs: [],
|
|
16
|
+
},
|
|
17
|
+
codex: {
|
|
18
|
+
command: "codex",
|
|
19
|
+
defaultArgs: [],
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Main
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/**
|
|
26
|
+
* Launch a wrapped agent CLI.
|
|
27
|
+
*
|
|
28
|
+
* This function does not return until the agent exits (or is killed).
|
|
29
|
+
* It sets up the full lifecycle: PtySession, detectors, signal handlers,
|
|
30
|
+
* and interactive stdin/stdout forwarding.
|
|
31
|
+
*
|
|
32
|
+
* @param engineName - One of "claude", "gemini", "codex"
|
|
33
|
+
* @param extraArgs - Additional CLI arguments appended after defaults
|
|
34
|
+
*/
|
|
35
|
+
export async function launchWrappedAgent(engineName, extraArgs) {
|
|
36
|
+
const spec = ENGINES[engineName];
|
|
37
|
+
if (!spec) {
|
|
38
|
+
console.error(`Unknown engine: ${engineName}`);
|
|
39
|
+
console.error(`Supported engines: ${Object.keys(ENGINES).join(", ")}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const args = [...spec.defaultArgs, ...(extraArgs ?? [])];
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
const launcher = new AgentLauncher(cwd);
|
|
45
|
+
const agent = await launcher.launch({
|
|
46
|
+
agentType: engineName,
|
|
47
|
+
command: spec.command,
|
|
48
|
+
args,
|
|
49
|
+
cwd,
|
|
50
|
+
nickname: process.env.LOOP_NICKNAME,
|
|
51
|
+
});
|
|
52
|
+
// Forward stdin to the PtySession
|
|
53
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
54
|
+
process.stdin.setRawMode(true);
|
|
55
|
+
}
|
|
56
|
+
process.stdin.resume();
|
|
57
|
+
const onStdinData = (data) => {
|
|
58
|
+
const text = typeof data === "string" ? data : data.toString("utf8");
|
|
59
|
+
agent.ptySession.write(text);
|
|
60
|
+
};
|
|
61
|
+
process.stdin.on("data", onStdinData);
|
|
62
|
+
// Forward PTY output to stdout
|
|
63
|
+
agent.ptySession.on("pty-data", (data) => {
|
|
64
|
+
process.stdout.write(data);
|
|
65
|
+
});
|
|
66
|
+
// Handle terminal resize
|
|
67
|
+
const onResize = () => {
|
|
68
|
+
if (agent.ptySession.isAlive) {
|
|
69
|
+
const cols = process.stdout.columns ?? 80;
|
|
70
|
+
const rows = process.stdout.rows ?? 24;
|
|
71
|
+
agent.ptySession.resize(cols, rows);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
if (process.stdout.isTTY) {
|
|
75
|
+
process.stdout.on("resize", onResize);
|
|
76
|
+
}
|
|
77
|
+
// Wait for exit
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
agent.ptySession.on("exit", async (code) => {
|
|
80
|
+
try {
|
|
81
|
+
// Clean up stdin
|
|
82
|
+
process.stdin.removeListener("data", onStdinData);
|
|
83
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
84
|
+
try {
|
|
85
|
+
process.stdin.setRawMode(false);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// May fail if stream is already closed
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Clean up resize handler
|
|
92
|
+
if (process.stdout.isTTY) {
|
|
93
|
+
process.stdout.removeListener("resize", onResize);
|
|
94
|
+
}
|
|
95
|
+
// Run cleanup (detectors, logger, sockets)
|
|
96
|
+
await agent.cleanup();
|
|
97
|
+
// Exit with the agent's code
|
|
98
|
+
process.exitCode = code;
|
|
99
|
+
resolve();
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
// Prevent unhandled rejection from async exit handler
|
|
103
|
+
console.error("Exit handler error:", err instanceof Error ? err.message : String(err));
|
|
104
|
+
process.exitCode = code || 1;
|
|
105
|
+
resolve();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=wrapper.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { launchWrappedAgent } from "../agent/wrapper.js";
|
|
3
|
+
launchWrappedAgent("claude", process.argv.slice(2)).catch((err) => {
|
|
4
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5
|
+
process.exit(1);
|
|
6
|
+
});
|
|
7
|
+
//# sourceMappingURL=lclaude.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { launchWrappedAgent } from "../agent/wrapper.js";
|
|
3
|
+
launchWrappedAgent("codex", process.argv.slice(2)).catch((err) => {
|
|
4
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5
|
+
process.exit(1);
|
|
6
|
+
});
|
|
7
|
+
//# sourceMappingURL=lcodex.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { launchWrappedAgent } from "../agent/wrapper.js";
|
|
3
|
+
launchWrappedAgent("gemini", process.argv.slice(2)).catch((err) => {
|
|
4
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
5
|
+
process.exit(1);
|
|
6
|
+
});
|
|
7
|
+
//# sourceMappingURL=lgemini.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { EventBus } from "./event-bus.js";
|
|
2
|
+
/**
|
|
3
|
+
* Options for the BusDaemon.
|
|
4
|
+
*/
|
|
5
|
+
export interface BusDaemonOptions {
|
|
6
|
+
/** Polling interval in milliseconds. Default: 1000 */
|
|
7
|
+
pollIntervalMs?: number;
|
|
8
|
+
/** How often (in poll cycles) to run cleanup. Default: 5 */
|
|
9
|
+
cleanupEveryNCycles?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Background daemon that polls subscriber queues and delivers messages.
|
|
13
|
+
*
|
|
14
|
+
* In the ufoo model, the daemon checks for pending messages in queue
|
|
15
|
+
* directories and delivers them to agents (via injection). In this
|
|
16
|
+
* simplified version, the daemon:
|
|
17
|
+
*
|
|
18
|
+
* 1. Periodically cleans up dead agents
|
|
19
|
+
* 2. Monitors queue directories for pending messages
|
|
20
|
+
* 3. Emits delivery notifications
|
|
21
|
+
*/
|
|
22
|
+
export declare class BusDaemon {
|
|
23
|
+
private readonly eventBus;
|
|
24
|
+
private readonly pollIntervalMs;
|
|
25
|
+
private readonly cleanupEveryNCycles;
|
|
26
|
+
private running;
|
|
27
|
+
private timer;
|
|
28
|
+
private cleanupCounter;
|
|
29
|
+
private lastCounts;
|
|
30
|
+
constructor(eventBus: EventBus, opts?: BusDaemonOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Start the daemon polling loop.
|
|
33
|
+
*/
|
|
34
|
+
start(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Stop the daemon.
|
|
37
|
+
*/
|
|
38
|
+
stop(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Check if the daemon is currently running.
|
|
41
|
+
*/
|
|
42
|
+
isRunning(): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Main polling loop.
|
|
45
|
+
*/
|
|
46
|
+
private pollLoop;
|
|
47
|
+
/**
|
|
48
|
+
* Single tick: cleanup + check queues.
|
|
49
|
+
*/
|
|
50
|
+
private tick;
|
|
51
|
+
/**
|
|
52
|
+
* Check all subscriber queues for pending messages.
|
|
53
|
+
*/
|
|
54
|
+
private checkQueues;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=daemon.d.ts.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { safeNameToSubscriber } from "./store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Background daemon that polls subscriber queues and delivers messages.
|
|
6
|
+
*
|
|
7
|
+
* In the ufoo model, the daemon checks for pending messages in queue
|
|
8
|
+
* directories and delivers them to agents (via injection). In this
|
|
9
|
+
* simplified version, the daemon:
|
|
10
|
+
*
|
|
11
|
+
* 1. Periodically cleans up dead agents
|
|
12
|
+
* 2. Monitors queue directories for pending messages
|
|
13
|
+
* 3. Emits delivery notifications
|
|
14
|
+
*/
|
|
15
|
+
export class BusDaemon {
|
|
16
|
+
eventBus;
|
|
17
|
+
pollIntervalMs;
|
|
18
|
+
cleanupEveryNCycles;
|
|
19
|
+
running = false;
|
|
20
|
+
timer = null;
|
|
21
|
+
cleanupCounter = 0;
|
|
22
|
+
lastCounts = new Map();
|
|
23
|
+
constructor(eventBus, opts) {
|
|
24
|
+
this.eventBus = eventBus;
|
|
25
|
+
this.pollIntervalMs = opts?.pollIntervalMs ?? 1000;
|
|
26
|
+
this.cleanupEveryNCycles = opts?.cleanupEveryNCycles ?? 5;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Start the daemon polling loop.
|
|
30
|
+
*/
|
|
31
|
+
async start() {
|
|
32
|
+
if (this.running)
|
|
33
|
+
return;
|
|
34
|
+
this.running = true;
|
|
35
|
+
this.pollLoop();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Stop the daemon.
|
|
39
|
+
*/
|
|
40
|
+
async stop() {
|
|
41
|
+
this.running = false;
|
|
42
|
+
if (this.timer !== null) {
|
|
43
|
+
clearTimeout(this.timer);
|
|
44
|
+
this.timer = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if the daemon is currently running.
|
|
49
|
+
*/
|
|
50
|
+
isRunning() {
|
|
51
|
+
return this.running;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Main polling loop.
|
|
55
|
+
*/
|
|
56
|
+
pollLoop() {
|
|
57
|
+
if (!this.running)
|
|
58
|
+
return;
|
|
59
|
+
this.tick()
|
|
60
|
+
.catch((err) => {
|
|
61
|
+
// Log but don't crash
|
|
62
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
63
|
+
process.stderr.write(`[bus-daemon] tick error: ${message}\n`);
|
|
64
|
+
})
|
|
65
|
+
.finally(() => {
|
|
66
|
+
if (this.running) {
|
|
67
|
+
this.timer = setTimeout(() => this.pollLoop(), this.pollIntervalMs);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Single tick: cleanup + check queues.
|
|
73
|
+
*/
|
|
74
|
+
async tick() {
|
|
75
|
+
// Periodic cleanup
|
|
76
|
+
this.cleanupCounter++;
|
|
77
|
+
if (this.cleanupCounter >= this.cleanupEveryNCycles) {
|
|
78
|
+
this.cleanupCounter = 0;
|
|
79
|
+
try {
|
|
80
|
+
const subscriberMgr = this.eventBus.getSubscriberManager();
|
|
81
|
+
await subscriberMgr.cleanupInactive();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Ignore cleanup errors
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Check all queues
|
|
88
|
+
await this.checkQueues();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check all subscriber queues for pending messages.
|
|
92
|
+
*/
|
|
93
|
+
async checkQueues() {
|
|
94
|
+
const queuesDir = join(this.eventBus.busDir, "queues");
|
|
95
|
+
let entries;
|
|
96
|
+
try {
|
|
97
|
+
entries = await readdir(queuesDir);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Queues dir may not exist yet
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const safeName of entries) {
|
|
104
|
+
const pendingPath = join(queuesDir, safeName, "pending.jsonl");
|
|
105
|
+
let fileSize;
|
|
106
|
+
try {
|
|
107
|
+
const fileStat = await stat(pendingPath);
|
|
108
|
+
fileSize = fileStat.size;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue; // File doesn't exist
|
|
112
|
+
}
|
|
113
|
+
if (fileSize === 0)
|
|
114
|
+
continue;
|
|
115
|
+
// Count current messages
|
|
116
|
+
let count = 0;
|
|
117
|
+
try {
|
|
118
|
+
const content = await readFile(pendingPath, "utf8");
|
|
119
|
+
const trimmed = content.trim();
|
|
120
|
+
count = trimmed ? trimmed.split("\n").length : 0;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const subscriberId = safeNameToSubscriber(safeName);
|
|
126
|
+
const lastCount = this.lastCounts.get(safeName) ?? 0;
|
|
127
|
+
if (count > lastCount) {
|
|
128
|
+
const now = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
|
|
129
|
+
process.stderr.write(`[bus-daemon] ${now} New messages for ${subscriberId} (${lastCount} -> ${count})\n`);
|
|
130
|
+
}
|
|
131
|
+
this.lastCounts.set(safeName, count);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=daemon.js.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { BusStore } from "./store.js";
|
|
2
|
+
import { QueueManager } from "./queue.js";
|
|
3
|
+
import { SubscriberManager, type AgentMetadata } from "./subscriber.js";
|
|
4
|
+
import { MessageManager } from "./message.js";
|
|
5
|
+
/**
|
|
6
|
+
* A single event on the bus.
|
|
7
|
+
*/
|
|
8
|
+
export interface BusEvent {
|
|
9
|
+
seq: number;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
type: string;
|
|
12
|
+
event: string;
|
|
13
|
+
publisher: string;
|
|
14
|
+
target: string;
|
|
15
|
+
data: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Summary status of the bus.
|
|
19
|
+
*/
|
|
20
|
+
export interface BusStatus {
|
|
21
|
+
id: string;
|
|
22
|
+
agents: number;
|
|
23
|
+
events: number;
|
|
24
|
+
agentList: Array<{
|
|
25
|
+
id: string;
|
|
26
|
+
type: string;
|
|
27
|
+
nickname: string;
|
|
28
|
+
status: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The main EventBus class. Orchestrates store, queue, subscriber, and
|
|
33
|
+
* message managers to provide a unified API for agent communication.
|
|
34
|
+
*/
|
|
35
|
+
export declare class EventBus {
|
|
36
|
+
readonly projectRoot: string;
|
|
37
|
+
readonly busDir: string;
|
|
38
|
+
private store;
|
|
39
|
+
private subscriberManager;
|
|
40
|
+
private messageManager;
|
|
41
|
+
private queueManager;
|
|
42
|
+
constructor(projectRoot: string);
|
|
43
|
+
/**
|
|
44
|
+
* Initialize the event bus (create directory structure, etc).
|
|
45
|
+
*/
|
|
46
|
+
init(): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Gracefully shut down the event bus.
|
|
49
|
+
*/
|
|
50
|
+
shutdown(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Send a targeted message from a publisher to a target.
|
|
53
|
+
*/
|
|
54
|
+
send(publisher: string, target: string, message: string): Promise<BusEvent>;
|
|
55
|
+
/**
|
|
56
|
+
* Broadcast a message from a publisher to all active agents.
|
|
57
|
+
*/
|
|
58
|
+
broadcast(publisher: string, message: string): Promise<BusEvent>;
|
|
59
|
+
/**
|
|
60
|
+
* Join an agent to the bus.
|
|
61
|
+
* Returns the subscriber ID.
|
|
62
|
+
*/
|
|
63
|
+
join(agentType: string, metadata?: Partial<AgentMetadata>): Promise<string>;
|
|
64
|
+
/**
|
|
65
|
+
* Remove an agent from the bus (mark as inactive).
|
|
66
|
+
*/
|
|
67
|
+
leave(subscriberId: string): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Peek at pending messages for a subscriber without consuming them.
|
|
70
|
+
*/
|
|
71
|
+
check(subscriberId: string): Promise<BusEvent[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Consume (read and clear) pending messages for a subscriber.
|
|
74
|
+
*/
|
|
75
|
+
consume(subscriberId: string): Promise<BusEvent[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Get the current status of the bus.
|
|
78
|
+
*/
|
|
79
|
+
status(): Promise<BusStatus>;
|
|
80
|
+
/**
|
|
81
|
+
* Get all registered agents.
|
|
82
|
+
*/
|
|
83
|
+
agents(): Promise<Map<string, AgentMetadata>>;
|
|
84
|
+
/**
|
|
85
|
+
* Resolve a target to subscriber IDs (exposed for daemon/orchestrator use).
|
|
86
|
+
*/
|
|
87
|
+
resolveTarget(target: string): Promise<string[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Get the subscriber manager (for orchestrator access).
|
|
90
|
+
*/
|
|
91
|
+
getSubscriberManager(): SubscriberManager;
|
|
92
|
+
/**
|
|
93
|
+
* Get the message manager (for orchestrator access).
|
|
94
|
+
*/
|
|
95
|
+
getMessageManager(): MessageManager;
|
|
96
|
+
/**
|
|
97
|
+
* Get the queue manager (for daemon access).
|
|
98
|
+
*/
|
|
99
|
+
getQueueManager(): QueueManager;
|
|
100
|
+
/**
|
|
101
|
+
* Get the store (for direct access when needed).
|
|
102
|
+
*/
|
|
103
|
+
getStore(): BusStore;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=event-bus.d.ts.map
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { join, basename } from "node:path";
|
|
2
|
+
import { BusStore } from "./store.js";
|
|
3
|
+
import { QueueManager } from "./queue.js";
|
|
4
|
+
import { SubscriberManager } from "./subscriber.js";
|
|
5
|
+
import { MessageManager } from "./message.js";
|
|
6
|
+
/**
|
|
7
|
+
* The main EventBus class. Orchestrates store, queue, subscriber, and
|
|
8
|
+
* message managers to provide a unified API for agent communication.
|
|
9
|
+
*/
|
|
10
|
+
export class EventBus {
|
|
11
|
+
projectRoot;
|
|
12
|
+
busDir;
|
|
13
|
+
store;
|
|
14
|
+
subscriberManager;
|
|
15
|
+
messageManager;
|
|
16
|
+
queueManager;
|
|
17
|
+
constructor(projectRoot) {
|
|
18
|
+
this.projectRoot = projectRoot;
|
|
19
|
+
this.busDir = join(projectRoot, ".loop", "bus");
|
|
20
|
+
this.store = new BusStore(this.busDir);
|
|
21
|
+
this.subscriberManager = new SubscriberManager(this.store);
|
|
22
|
+
this.messageManager = new MessageManager(this.busDir, this.subscriberManager);
|
|
23
|
+
this.queueManager = new QueueManager(this.busDir);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Initialize the event bus (create directory structure, etc).
|
|
27
|
+
*/
|
|
28
|
+
async init() {
|
|
29
|
+
await this.store.init();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Gracefully shut down the event bus.
|
|
33
|
+
*/
|
|
34
|
+
async shutdown() {
|
|
35
|
+
// Currently a no-op; future: close file handles, flush buffers
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Send a targeted message from a publisher to a target.
|
|
39
|
+
*/
|
|
40
|
+
async send(publisher, target, message) {
|
|
41
|
+
return this.messageManager.createEvent(publisher, target, { message }, "message/targeted");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Broadcast a message from a publisher to all active agents.
|
|
45
|
+
*/
|
|
46
|
+
async broadcast(publisher, message) {
|
|
47
|
+
return this.messageManager.createEvent(publisher, "*", { message }, "message/broadcast");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Join an agent to the bus.
|
|
51
|
+
* Returns the subscriber ID.
|
|
52
|
+
*/
|
|
53
|
+
async join(agentType, metadata) {
|
|
54
|
+
return this.subscriberManager.register(agentType, metadata);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Remove an agent from the bus (mark as inactive).
|
|
58
|
+
*/
|
|
59
|
+
async leave(subscriberId) {
|
|
60
|
+
await this.subscriberManager.unregister(subscriberId);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Peek at pending messages for a subscriber without consuming them.
|
|
64
|
+
*/
|
|
65
|
+
async check(subscriberId) {
|
|
66
|
+
// Update last_seen on check
|
|
67
|
+
try {
|
|
68
|
+
await this.subscriberManager.updateMetadata(subscriberId, {
|
|
69
|
+
last_seen: new Date().toISOString(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Ignore - subscriber may not be registered
|
|
74
|
+
}
|
|
75
|
+
return this.queueManager.peek(subscriberId);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Consume (read and clear) pending messages for a subscriber.
|
|
79
|
+
*/
|
|
80
|
+
async consume(subscriberId) {
|
|
81
|
+
// Update last_seen on consume
|
|
82
|
+
try {
|
|
83
|
+
await this.subscriberManager.updateMetadata(subscriberId, {
|
|
84
|
+
last_seen: new Date().toISOString(),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Ignore - subscriber may not be registered
|
|
89
|
+
}
|
|
90
|
+
return this.queueManager.dequeue(subscriberId);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the current status of the bus.
|
|
94
|
+
*/
|
|
95
|
+
async status() {
|
|
96
|
+
// Clean up dead agents first
|
|
97
|
+
await this.subscriberManager.cleanupInactive();
|
|
98
|
+
const agents = await this.subscriberManager.list();
|
|
99
|
+
const totalEvents = await this.store.countEvents();
|
|
100
|
+
const agentList = [];
|
|
101
|
+
let activeCount = 0;
|
|
102
|
+
for (const [id, meta] of agents) {
|
|
103
|
+
agentList.push({
|
|
104
|
+
id,
|
|
105
|
+
type: meta.agent_type,
|
|
106
|
+
nickname: meta.nickname,
|
|
107
|
+
status: meta.status,
|
|
108
|
+
});
|
|
109
|
+
if (meta.status === "active") {
|
|
110
|
+
activeCount++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
id: basename(this.projectRoot) || "loop-workspace",
|
|
115
|
+
agents: activeCount,
|
|
116
|
+
events: totalEvents,
|
|
117
|
+
agentList,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get all registered agents.
|
|
122
|
+
*/
|
|
123
|
+
async agents() {
|
|
124
|
+
return this.subscriberManager.list();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Resolve a target to subscriber IDs (exposed for daemon/orchestrator use).
|
|
128
|
+
*/
|
|
129
|
+
async resolveTarget(target) {
|
|
130
|
+
return this.messageManager.resolveTarget(target);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get the subscriber manager (for orchestrator access).
|
|
134
|
+
*/
|
|
135
|
+
getSubscriberManager() {
|
|
136
|
+
return this.subscriberManager;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get the message manager (for orchestrator access).
|
|
140
|
+
*/
|
|
141
|
+
getMessageManager() {
|
|
142
|
+
return this.messageManager;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get the queue manager (for daemon access).
|
|
146
|
+
*/
|
|
147
|
+
getQueueManager() {
|
|
148
|
+
return this.queueManager;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get the store (for direct access when needed).
|
|
152
|
+
*/
|
|
153
|
+
getStore() {
|
|
154
|
+
return this.store;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=event-bus.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SubscriberManager } from "./subscriber.js";
|
|
2
|
+
import { QueueManager } from "./queue.js";
|
|
3
|
+
import type { BusEvent } from "./event-bus.js";
|
|
4
|
+
/**
|
|
5
|
+
* Manages message creation, routing, and delivery.
|
|
6
|
+
*
|
|
7
|
+
* Target resolution order:
|
|
8
|
+
* 1. Exact subscriber ID match (e.g. "claude:abc123")
|
|
9
|
+
* 2. Nickname match (e.g. "claude-1")
|
|
10
|
+
* 3. Agent type match (e.g. "claude" -> all active claude agents)
|
|
11
|
+
* 4. Broadcast ("*" -> all active agents)
|
|
12
|
+
*/
|
|
13
|
+
export declare class MessageManager {
|
|
14
|
+
private readonly eventsDir;
|
|
15
|
+
private readonly seqFile;
|
|
16
|
+
private readonly seqLockFile;
|
|
17
|
+
private readonly subscriberManager;
|
|
18
|
+
private readonly queueManager;
|
|
19
|
+
constructor(busDir: string, subscriberManager: SubscriberManager);
|
|
20
|
+
/**
|
|
21
|
+
* Get the next monotonically increasing sequence number.
|
|
22
|
+
*/
|
|
23
|
+
nextSeq(): Promise<number>;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a target string to a list of subscriber IDs.
|
|
26
|
+
*
|
|
27
|
+
* Resolution order:
|
|
28
|
+
* 1. Exact subscriber ID match
|
|
29
|
+
* 2. Nickname match
|
|
30
|
+
* 3. Agent type match (all active agents of that type)
|
|
31
|
+
* 4. Broadcast "*" (all active agents)
|
|
32
|
+
*/
|
|
33
|
+
resolveTarget(target: string): Promise<string[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Route an event to its target subscribers' queues.
|
|
36
|
+
*/
|
|
37
|
+
route(event: BusEvent): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Create and persist a new event, then route it to targets.
|
|
40
|
+
* Returns the created event.
|
|
41
|
+
*/
|
|
42
|
+
createEvent(publisher: string, target: string, data: Record<string, unknown>, type?: string): Promise<BusEvent>;
|
|
43
|
+
/**
|
|
44
|
+
* Get the queue manager (needed by EventBus for consume operations).
|
|
45
|
+
*/
|
|
46
|
+
getQueueManager(): QueueManager;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=message.d.ts.map
|