@agentbus/agentbus-client 0.11.2
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 +77 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +296 -0
- package/dist/locator.d.ts +5 -0
- package/dist/locator.js +22 -0
- package/dist/stdio.d.ts +23 -0
- package/dist/stdio.js +70 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +2 -0
- package/dist/watcher.d.ts +18 -0
- package/dist/watcher.js +72 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @agentbus/agentbus-client
|
|
2
|
+
|
|
3
|
+
TypeScript / Node.js EventEmitter client for [AgentBus](https://github.com/onicarps/agentbus).
|
|
4
|
+
|
|
5
|
+
- Resolves `events.db` from `AGENTBUS_WORKSPACE`
|
|
6
|
+
- Debounced `fs.watch` + slow fallback timer to trigger polls
|
|
7
|
+
- Spawns `agentbus mcp-serve` over stdio (or accepts an injected MCP client)
|
|
8
|
+
- `publish` / `poll` map to `agentbus_publish` / `agentbus_poll`
|
|
9
|
+
- Emits `event` and per-topic events (`bus.on('okf/handoff', ...)`)
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
1. Python AgentBus installed and on `PATH` (or set `AGENTBUS_BIN`):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install 'okf-agentbus[devex]'
|
|
17
|
+
# or use a repo venv:
|
|
18
|
+
export AGENTBUS_BIN=/path/to/agentbus/.venv/bin/agentbus
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. Workspace initialized:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export AGENTBUS_WORKSPACE=/path/to/project
|
|
25
|
+
agentbus token ensure --workspace "$AGENTBUS_WORKSPACE"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd packages/js/agentbus-client
|
|
32
|
+
npm install
|
|
33
|
+
npm test
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Auto stdio (recommended)
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { AgentBus } from "@agentbus/agentbus-client";
|
|
42
|
+
|
|
43
|
+
const bus = new AgentBus({
|
|
44
|
+
workspace: process.env.AGENTBUS_WORKSPACE,
|
|
45
|
+
topics: ["okf/handoff"], // or ["*"] to expand via agentbus_status
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
bus.on("okf/handoff", (payload, meta) => {
|
|
49
|
+
console.log("handoff", payload, meta.event_id);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await bus.connect(); // spawns `agentbus mcp-serve`
|
|
53
|
+
await bus.publish("okf/handoff", {
|
|
54
|
+
from: "node-agent",
|
|
55
|
+
to: "swarm",
|
|
56
|
+
summary: "hello from TS",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// later
|
|
60
|
+
await bus.disconnect();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Injected MCP client (tests / custom transports)
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { AgentBus, createStdioMcpClient } from "@agentbus/agentbus-client";
|
|
67
|
+
|
|
68
|
+
const mcp = await createStdioMcpClient({
|
|
69
|
+
workspace: process.env.AGENTBUS_WORKSPACE!,
|
|
70
|
+
command: process.env.AGENTBUS_BIN, // optional
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const bus = new AgentBus({ mcp, workspace: process.env.AGENTBUS_WORKSPACE });
|
|
74
|
+
await bus.connect();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Set `AGENTBUS_WORKSPACE` to the project root that contains `.agentbus/events.db`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import type { AgentBusOptions, BusEvent } from "./types";
|
|
3
|
+
export { getDatabasePath } from "./locator";
|
|
4
|
+
export { DatabaseWatcher } from "./watcher";
|
|
5
|
+
export { createStdioMcpClient } from "./stdio";
|
|
6
|
+
export type { AgentBusOptions, BusEvent, McpToolClient, } from "./types";
|
|
7
|
+
export type { StdioMcpOptions } from "./stdio";
|
|
8
|
+
/**
|
|
9
|
+
* Idiomatic Node client: EventEmitter + publish/poll over AgentBus MCP tools,
|
|
10
|
+
* with fs.watch-triggered polls and a slow timer fallback.
|
|
11
|
+
*
|
|
12
|
+
* When `options.mcp` is omitted, `connect()` spawns `agentbus mcp-serve` over
|
|
13
|
+
* stdio (requires `agentbus` on PATH or `AGENTBUS_BIN`).
|
|
14
|
+
*/
|
|
15
|
+
export declare class AgentBus extends EventEmitter {
|
|
16
|
+
private watcher;
|
|
17
|
+
private options;
|
|
18
|
+
/** Global high-water (min of per-topic cursors after last poll). */
|
|
19
|
+
private lastId;
|
|
20
|
+
/** Exclusive since_id per topic — avoids skipping pages across topics. */
|
|
21
|
+
private topicCursors;
|
|
22
|
+
private initialSince;
|
|
23
|
+
private connected;
|
|
24
|
+
private polling;
|
|
25
|
+
private ownsMcp;
|
|
26
|
+
private mcp;
|
|
27
|
+
constructor(options?: AgentBusOptions);
|
|
28
|
+
connect(): Promise<void>;
|
|
29
|
+
publish(topic: string, payload: unknown, opts?: {
|
|
30
|
+
producerId?: string;
|
|
31
|
+
}): Promise<unknown>;
|
|
32
|
+
/** Force a poll (also used by watcher). Drains each topic fully. */
|
|
33
|
+
poll(): Promise<BusEvent[]>;
|
|
34
|
+
get cursor(): number;
|
|
35
|
+
/** Per-topic exclusive cursors (for diagnostics / multi-topic correctness). */
|
|
36
|
+
getTopicCursor(topic: string): number;
|
|
37
|
+
disconnect(): Promise<void>;
|
|
38
|
+
private reportBackgroundError;
|
|
39
|
+
private rollbackConnect;
|
|
40
|
+
private requireMcp;
|
|
41
|
+
private resolveTopics;
|
|
42
|
+
}
|
|
43
|
+
/** Pull JSON out of MCP CallToolResult envelopes or raw values. */
|
|
44
|
+
export declare function unwrapToolResult(raw: unknown): unknown;
|
|
45
|
+
export declare function parsePollPage(raw: unknown): {
|
|
46
|
+
events: BusEvent[];
|
|
47
|
+
hasMore: boolean;
|
|
48
|
+
};
|
|
49
|
+
export declare function normalizePollResult(raw: unknown): BusEvent[];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentBus = exports.createStdioMcpClient = exports.DatabaseWatcher = exports.getDatabasePath = void 0;
|
|
4
|
+
exports.unwrapToolResult = unwrapToolResult;
|
|
5
|
+
exports.parsePollPage = parsePollPage;
|
|
6
|
+
exports.normalizePollResult = normalizePollResult;
|
|
7
|
+
const events_1 = require("events");
|
|
8
|
+
const watcher_1 = require("./watcher");
|
|
9
|
+
const locator_1 = require("./locator");
|
|
10
|
+
const stdio_1 = require("./stdio");
|
|
11
|
+
var locator_2 = require("./locator");
|
|
12
|
+
Object.defineProperty(exports, "getDatabasePath", { enumerable: true, get: function () { return locator_2.getDatabasePath; } });
|
|
13
|
+
var watcher_2 = require("./watcher");
|
|
14
|
+
Object.defineProperty(exports, "DatabaseWatcher", { enumerable: true, get: function () { return watcher_2.DatabaseWatcher; } });
|
|
15
|
+
var stdio_2 = require("./stdio");
|
|
16
|
+
Object.defineProperty(exports, "createStdioMcpClient", { enumerable: true, get: function () { return stdio_2.createStdioMcpClient; } });
|
|
17
|
+
const DEFAULT_POLL_LIMIT = 50;
|
|
18
|
+
/**
|
|
19
|
+
* Idiomatic Node client: EventEmitter + publish/poll over AgentBus MCP tools,
|
|
20
|
+
* with fs.watch-triggered polls and a slow timer fallback.
|
|
21
|
+
*
|
|
22
|
+
* When `options.mcp` is omitted, `connect()` spawns `agentbus mcp-serve` over
|
|
23
|
+
* stdio (requires `agentbus` on PATH or `AGENTBUS_BIN`).
|
|
24
|
+
*/
|
|
25
|
+
class AgentBus extends events_1.EventEmitter {
|
|
26
|
+
watcher = null;
|
|
27
|
+
options;
|
|
28
|
+
/** Global high-water (min of per-topic cursors after last poll). */
|
|
29
|
+
lastId;
|
|
30
|
+
/** Exclusive since_id per topic — avoids skipping pages across topics. */
|
|
31
|
+
topicCursors = new Map();
|
|
32
|
+
initialSince;
|
|
33
|
+
connected = false;
|
|
34
|
+
polling = false;
|
|
35
|
+
ownsMcp = false;
|
|
36
|
+
mcp;
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
super();
|
|
39
|
+
this.options = options;
|
|
40
|
+
this.initialSince = options.sinceId ?? 0;
|
|
41
|
+
this.lastId = this.initialSince;
|
|
42
|
+
this.mcp = options.mcp;
|
|
43
|
+
}
|
|
44
|
+
async connect() {
|
|
45
|
+
if (this.connected)
|
|
46
|
+
return;
|
|
47
|
+
const spawnedMcpHere = !this.mcp;
|
|
48
|
+
try {
|
|
49
|
+
if (!this.mcp) {
|
|
50
|
+
const producerId = this.options.producerId ?? process.env.AGENTBUS_PRODUCER_ID;
|
|
51
|
+
const env = {
|
|
52
|
+
...(this.options.stdio?.env ?? {}),
|
|
53
|
+
};
|
|
54
|
+
if (producerId) {
|
|
55
|
+
env.AGENTBUS_PRODUCER_ID = producerId;
|
|
56
|
+
}
|
|
57
|
+
this.mcp = await (0, stdio_1.createStdioMcpClient)({
|
|
58
|
+
workspace: this.options.workspace,
|
|
59
|
+
command: this.options.stdio?.command,
|
|
60
|
+
args: this.options.stdio?.args,
|
|
61
|
+
cwd: this.options.stdio?.cwd,
|
|
62
|
+
env,
|
|
63
|
+
});
|
|
64
|
+
this.ownsMcp = true;
|
|
65
|
+
}
|
|
66
|
+
const dbPath = (0, locator_1.getDatabasePath)(this.options.workspace);
|
|
67
|
+
this.watcher = new watcher_1.DatabaseWatcher(dbPath, () => {
|
|
68
|
+
void this.poll().catch((err) => {
|
|
69
|
+
this.reportBackgroundError(err);
|
|
70
|
+
});
|
|
71
|
+
}, this.options.fallbackMs ?? 5000);
|
|
72
|
+
this.watcher.start();
|
|
73
|
+
// Initial catch-up poll — must succeed before we mark connected.
|
|
74
|
+
await this.poll();
|
|
75
|
+
this.connected = true;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
await this.rollbackConnect(spawnedMcpHere);
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async publish(topic, payload, opts) {
|
|
83
|
+
const mcp = this.requireMcp();
|
|
84
|
+
// Server expects payload as object (dict), not a JSON string.
|
|
85
|
+
const body = typeof payload === "string"
|
|
86
|
+
? safeParseJson(payload) ?? { text: payload }
|
|
87
|
+
: payload;
|
|
88
|
+
const producerId = opts?.producerId ??
|
|
89
|
+
this.options.producerId ??
|
|
90
|
+
process.env.AGENTBUS_PRODUCER_ID;
|
|
91
|
+
const args = {
|
|
92
|
+
topic,
|
|
93
|
+
payload: body,
|
|
94
|
+
};
|
|
95
|
+
if (producerId) {
|
|
96
|
+
args.producer_id = producerId;
|
|
97
|
+
}
|
|
98
|
+
const result = await mcp.callTool("agentbus_publish", args);
|
|
99
|
+
return unwrapToolResult(result);
|
|
100
|
+
}
|
|
101
|
+
/** Force a poll (also used by watcher). Drains each topic fully. */
|
|
102
|
+
async poll() {
|
|
103
|
+
if (this.polling)
|
|
104
|
+
return [];
|
|
105
|
+
this.polling = true;
|
|
106
|
+
try {
|
|
107
|
+
const mcp = this.mcp;
|
|
108
|
+
if (!mcp) {
|
|
109
|
+
// Without MCP, watcher still runs; no events to emit.
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const topics = await this.resolveTopics(mcp);
|
|
113
|
+
const merged = [];
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
// Commit + emit per topic after that topic drains. If a later topic fails,
|
|
116
|
+
// earlier topics keep their events and cursors (no silent drops).
|
|
117
|
+
for (const topic of topics) {
|
|
118
|
+
let since = this.topicCursors.get(topic) ?? this.initialSince;
|
|
119
|
+
let hasMore = true;
|
|
120
|
+
const topicEvents = [];
|
|
121
|
+
while (hasMore) {
|
|
122
|
+
const raw = await mcp.callTool("agentbus_poll", {
|
|
123
|
+
topic,
|
|
124
|
+
since_id: since,
|
|
125
|
+
limit: DEFAULT_POLL_LIMIT,
|
|
126
|
+
});
|
|
127
|
+
const page = parsePollPage(raw);
|
|
128
|
+
hasMore = page.hasMore;
|
|
129
|
+
for (const ev of page.events) {
|
|
130
|
+
if (ev.event_id > since) {
|
|
131
|
+
since = ev.event_id;
|
|
132
|
+
}
|
|
133
|
+
if (seen.has(ev.event_id))
|
|
134
|
+
continue;
|
|
135
|
+
seen.add(ev.event_id);
|
|
136
|
+
topicEvents.push(ev);
|
|
137
|
+
}
|
|
138
|
+
// Safety: empty page ends the loop even if has_more is stale.
|
|
139
|
+
if (page.events.length === 0) {
|
|
140
|
+
hasMore = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
this.topicCursors.set(topic, since);
|
|
144
|
+
// Preserve arrival order within a topic (server returns ASC by event_id).
|
|
145
|
+
for (const ev of topicEvents) {
|
|
146
|
+
this.emit("event", ev);
|
|
147
|
+
this.emit(ev.topic, ev.payload, ev);
|
|
148
|
+
}
|
|
149
|
+
merged.push(...topicEvents);
|
|
150
|
+
}
|
|
151
|
+
// Global cursor = min of topic cursors (never claims past unfetched pages).
|
|
152
|
+
if (topics.length > 0) {
|
|
153
|
+
let minCursor = Number.POSITIVE_INFINITY;
|
|
154
|
+
for (const topic of topics) {
|
|
155
|
+
const c = this.topicCursors.get(topic) ?? this.initialSince;
|
|
156
|
+
if (c < minCursor)
|
|
157
|
+
minCursor = c;
|
|
158
|
+
}
|
|
159
|
+
if (Number.isFinite(minCursor)) {
|
|
160
|
+
this.lastId = minCursor;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return merged;
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
this.polling = false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
get cursor() {
|
|
170
|
+
return this.lastId;
|
|
171
|
+
}
|
|
172
|
+
/** Per-topic exclusive cursors (for diagnostics / multi-topic correctness). */
|
|
173
|
+
getTopicCursor(topic) {
|
|
174
|
+
return this.topicCursors.get(topic) ?? this.initialSince;
|
|
175
|
+
}
|
|
176
|
+
async disconnect() {
|
|
177
|
+
if (this.watcher) {
|
|
178
|
+
this.watcher.stop();
|
|
179
|
+
this.watcher = null;
|
|
180
|
+
}
|
|
181
|
+
this.connected = false;
|
|
182
|
+
// Only close MCP clients we spawned; injected clients are caller-owned.
|
|
183
|
+
if (this.ownsMcp && this.mcp?.close) {
|
|
184
|
+
await this.mcp.close();
|
|
185
|
+
this.mcp = undefined;
|
|
186
|
+
this.ownsMcp = false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
reportBackgroundError(err) {
|
|
190
|
+
// EventEmitter "error" throws if no listeners — guard that path.
|
|
191
|
+
if (this.listenerCount("error") > 0) {
|
|
192
|
+
this.emit("error", err);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
this.emit("pollError", err);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async rollbackConnect(spawnedMcpHere) {
|
|
199
|
+
if (this.watcher) {
|
|
200
|
+
this.watcher.stop();
|
|
201
|
+
this.watcher = null;
|
|
202
|
+
}
|
|
203
|
+
this.connected = false;
|
|
204
|
+
if (spawnedMcpHere && this.ownsMcp && this.mcp?.close) {
|
|
205
|
+
try {
|
|
206
|
+
await this.mcp.close();
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
/* ignore close errors during rollback */
|
|
210
|
+
}
|
|
211
|
+
this.mcp = undefined;
|
|
212
|
+
this.ownsMcp = false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
requireMcp() {
|
|
216
|
+
if (!this.mcp) {
|
|
217
|
+
throw new Error("AgentBus.publish requires connect() first (or pass options.mcp)");
|
|
218
|
+
}
|
|
219
|
+
return this.mcp;
|
|
220
|
+
}
|
|
221
|
+
async resolveTopics(mcp) {
|
|
222
|
+
const configured = this.options.topics ?? ["okf/handoff"];
|
|
223
|
+
if (!configured.includes("*")) {
|
|
224
|
+
return configured;
|
|
225
|
+
}
|
|
226
|
+
const raw = await mcp.callTool("agentbus_status", {});
|
|
227
|
+
const status = unwrapToolResult(raw);
|
|
228
|
+
const listed = Array.isArray(status?.topics) ? status.topics : [];
|
|
229
|
+
// Keep any explicit topics alongside *, drop the star marker.
|
|
230
|
+
const explicit = configured.filter((t) => t !== "*");
|
|
231
|
+
const set = new Set([...explicit, ...listed]);
|
|
232
|
+
return set.size > 0 ? [...set] : ["okf/handoff"];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
exports.AgentBus = AgentBus;
|
|
236
|
+
function safeParseJson(text) {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(text);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Pull JSON out of MCP CallToolResult envelopes or raw values. */
|
|
245
|
+
function unwrapToolResult(raw) {
|
|
246
|
+
if (raw == null)
|
|
247
|
+
return null;
|
|
248
|
+
if (typeof raw === "string") {
|
|
249
|
+
return safeParseJson(raw) ?? raw;
|
|
250
|
+
}
|
|
251
|
+
if (typeof raw === "object") {
|
|
252
|
+
const obj = raw;
|
|
253
|
+
if (Array.isArray(obj.content)) {
|
|
254
|
+
for (const part of obj.content) {
|
|
255
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
256
|
+
return safeParseJson(part.text) ?? part.text;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return raw;
|
|
262
|
+
}
|
|
263
|
+
function parsePollPage(raw) {
|
|
264
|
+
const data = unwrapToolResult(raw);
|
|
265
|
+
if (data == null)
|
|
266
|
+
return { events: [], hasMore: false };
|
|
267
|
+
if (Array.isArray(data)) {
|
|
268
|
+
return {
|
|
269
|
+
events: data.map(normalizeEvent),
|
|
270
|
+
hasMore: data.length >= DEFAULT_POLL_LIMIT,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (typeof data === "object") {
|
|
274
|
+
const obj = data;
|
|
275
|
+
if (Array.isArray(obj.events)) {
|
|
276
|
+
const events = obj.events.map(normalizeEvent);
|
|
277
|
+
const hasMore = typeof obj.has_more === "boolean"
|
|
278
|
+
? obj.has_more
|
|
279
|
+
: events.length >= DEFAULT_POLL_LIMIT;
|
|
280
|
+
return { events, hasMore };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { events: [], hasMore: false };
|
|
284
|
+
}
|
|
285
|
+
function normalizePollResult(raw) {
|
|
286
|
+
return parsePollPage(raw).events;
|
|
287
|
+
}
|
|
288
|
+
function normalizeEvent(ev) {
|
|
289
|
+
// Payload may still be a JSON string from older projections.
|
|
290
|
+
if (typeof ev.payload === "string") {
|
|
291
|
+
const parsed = safeParseJson(ev.payload);
|
|
292
|
+
if (parsed != null)
|
|
293
|
+
return { ...ev, payload: parsed };
|
|
294
|
+
}
|
|
295
|
+
return ev;
|
|
296
|
+
}
|
package/dist/locator.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getDatabasePath = getDatabasePath;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the AgentBus SQLite events.db path from AGENTBUS_WORKSPACE
|
|
11
|
+
* (or an explicit workspace override).
|
|
12
|
+
*/
|
|
13
|
+
function getDatabasePath(workspace) {
|
|
14
|
+
const ws = workspace ?? process.env.AGENTBUS_WORKSPACE;
|
|
15
|
+
if (!ws) {
|
|
16
|
+
throw new Error("AGENTBUS_WORKSPACE environment variable must be set");
|
|
17
|
+
}
|
|
18
|
+
if (!fs_1.default.existsSync(ws)) {
|
|
19
|
+
throw new Error("Workspace directory not found: " + ws);
|
|
20
|
+
}
|
|
21
|
+
return path_1.default.join(ws, ".agentbus", "events.db");
|
|
22
|
+
}
|
package/dist/stdio.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { McpToolClient } from "./types";
|
|
2
|
+
export type StdioMcpOptions = {
|
|
3
|
+
/** Workspace root containing `.agentbus/` (defaults to AGENTBUS_WORKSPACE). */
|
|
4
|
+
workspace?: string;
|
|
5
|
+
/** Binary to spawn (default: AGENTBUS_BIN or `agentbus`). */
|
|
6
|
+
command?: string;
|
|
7
|
+
/** Args after command (default: `mcp-serve --workspace <ws>`). */
|
|
8
|
+
args?: string[];
|
|
9
|
+
cwd?: string;
|
|
10
|
+
/** Extra env merged on top of MCP-safe defaults. */
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
/** Client name reported to the MCP server. */
|
|
13
|
+
clientName?: string;
|
|
14
|
+
/** Client version reported to the MCP server. */
|
|
15
|
+
clientVersion?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Spawn `agentbus mcp-serve` over stdio and return a minimal tool client.
|
|
19
|
+
*
|
|
20
|
+
* Requires `agentbus` on PATH (or `AGENTBUS_BIN` / `options.command`) and a
|
|
21
|
+
* workspace with a valid token (`agentbus init` / `token ensure`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function createStdioMcpClient(options?: StdioMcpOptions): Promise<McpToolClient>;
|
package/dist/stdio.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createStdioMcpClient = createStdioMcpClient;
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
6
|
+
/**
|
|
7
|
+
* Spawn `agentbus mcp-serve` over stdio and return a minimal tool client.
|
|
8
|
+
*
|
|
9
|
+
* Requires `agentbus` on PATH (or `AGENTBUS_BIN` / `options.command`) and a
|
|
10
|
+
* workspace with a valid token (`agentbus init` / `token ensure`).
|
|
11
|
+
*/
|
|
12
|
+
async function createStdioMcpClient(options = {}) {
|
|
13
|
+
const workspace = options.workspace ?? process.env.AGENTBUS_WORKSPACE;
|
|
14
|
+
if (!workspace) {
|
|
15
|
+
throw new Error("createStdioMcpClient: workspace or AGENTBUS_WORKSPACE is required");
|
|
16
|
+
}
|
|
17
|
+
const command = options.command ?? process.env.AGENTBUS_BIN ?? "agentbus";
|
|
18
|
+
const args = options.args ?? ["mcp-serve", "--workspace", workspace];
|
|
19
|
+
const env = {
|
|
20
|
+
...(0, stdio_js_1.getDefaultEnvironment)(),
|
|
21
|
+
...options.env,
|
|
22
|
+
AGENTBUS_WORKSPACE: workspace,
|
|
23
|
+
};
|
|
24
|
+
// Preserve PATH so the child can resolve `agentbus` / python when needed.
|
|
25
|
+
if (process.env.PATH && !env.PATH) {
|
|
26
|
+
env.PATH = process.env.PATH;
|
|
27
|
+
}
|
|
28
|
+
const transport = new stdio_js_1.StdioClientTransport({
|
|
29
|
+
command,
|
|
30
|
+
args,
|
|
31
|
+
env,
|
|
32
|
+
cwd: options.cwd,
|
|
33
|
+
stderr: "pipe",
|
|
34
|
+
});
|
|
35
|
+
const client = new index_js_1.Client({
|
|
36
|
+
name: options.clientName ?? "@agentbus/agentbus-client",
|
|
37
|
+
version: options.clientVersion ?? "0.1.0",
|
|
38
|
+
});
|
|
39
|
+
await client.connect(transport);
|
|
40
|
+
return {
|
|
41
|
+
async callTool(name, toolArgs) {
|
|
42
|
+
const result = await client.callTool({
|
|
43
|
+
name,
|
|
44
|
+
arguments: toolArgs,
|
|
45
|
+
});
|
|
46
|
+
if (result.isError) {
|
|
47
|
+
const text = extractText(result);
|
|
48
|
+
throw new Error(text || `MCP tool ${name} returned isError without detail`);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
},
|
|
52
|
+
async close() {
|
|
53
|
+
await client.close();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function extractText(result) {
|
|
58
|
+
if (result == null || typeof result !== "object")
|
|
59
|
+
return "";
|
|
60
|
+
const content = result.content;
|
|
61
|
+
if (!Array.isArray(content))
|
|
62
|
+
return "";
|
|
63
|
+
return content
|
|
64
|
+
.filter((p) => !!p &&
|
|
65
|
+
typeof p === "object" &&
|
|
66
|
+
p.type === "text" &&
|
|
67
|
+
typeof p.text === "string")
|
|
68
|
+
.map((p) => p.text)
|
|
69
|
+
.join("\n");
|
|
70
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Minimal MCP tool surface used by AgentBus (stdio or mock). */
|
|
2
|
+
export type McpToolClient = {
|
|
3
|
+
callTool(name: string, args: Record<string, unknown>): Promise<unknown>;
|
|
4
|
+
close?(): Promise<void> | void;
|
|
5
|
+
};
|
|
6
|
+
export type BusEvent = {
|
|
7
|
+
event_id: number;
|
|
8
|
+
topic: string;
|
|
9
|
+
producer_id?: string;
|
|
10
|
+
timestamp?: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
payload: unknown;
|
|
13
|
+
};
|
|
14
|
+
export type AgentBusOptions = {
|
|
15
|
+
/** Override workspace (defaults to AGENTBUS_WORKSPACE). */
|
|
16
|
+
workspace?: string;
|
|
17
|
+
/** Fallback poll interval when fs.watch is quiet (ms). */
|
|
18
|
+
fallbackMs?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Injected MCP tool caller for tests / custom transports.
|
|
21
|
+
* If omitted, `connect()` spawns stdio via `createStdioMcpClient`.
|
|
22
|
+
*/
|
|
23
|
+
mcp?: McpToolClient;
|
|
24
|
+
/** Initial poll cursor (exclusive). */
|
|
25
|
+
sinceId?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Default producer id for publish (also set as AGENTBUS_PRODUCER_ID on stdio).
|
|
28
|
+
* Falls back to process.env.AGENTBUS_PRODUCER_ID.
|
|
29
|
+
*/
|
|
30
|
+
producerId?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Topics to poll each cycle.
|
|
33
|
+
* Default: `["okf/handoff"]`.
|
|
34
|
+
* Pass `["*"]` to expand via `agentbus_status` topics list each poll.
|
|
35
|
+
*/
|
|
36
|
+
topics?: string[];
|
|
37
|
+
/** Options for auto stdio spawn when `mcp` is not injected. */
|
|
38
|
+
stdio?: {
|
|
39
|
+
command?: string;
|
|
40
|
+
args?: string[];
|
|
41
|
+
cwd?: string;
|
|
42
|
+
env?: Record<string, string>;
|
|
43
|
+
};
|
|
44
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debounced fs.watch on the events.db (or parent dir) plus a slow fallback poll.
|
|
3
|
+
* WAL renames can make watching the file alone flaky — we prefer the .agentbus dir
|
|
4
|
+
* when the db path does not exist yet.
|
|
5
|
+
*/
|
|
6
|
+
export declare class DatabaseWatcher {
|
|
7
|
+
private dbPath;
|
|
8
|
+
private onTrigger;
|
|
9
|
+
private fallbackMs;
|
|
10
|
+
private debounceMs;
|
|
11
|
+
private timer;
|
|
12
|
+
private watcher;
|
|
13
|
+
private debounceTimer;
|
|
14
|
+
constructor(dbPath: string, onTrigger: () => void, fallbackMs?: number, debounceMs?: number);
|
|
15
|
+
start(): void;
|
|
16
|
+
private trigger;
|
|
17
|
+
stop(): void;
|
|
18
|
+
}
|
package/dist/watcher.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DatabaseWatcher = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/**
|
|
10
|
+
* Debounced fs.watch on the events.db (or parent dir) plus a slow fallback poll.
|
|
11
|
+
* WAL renames can make watching the file alone flaky — we prefer the .agentbus dir
|
|
12
|
+
* when the db path does not exist yet.
|
|
13
|
+
*/
|
|
14
|
+
class DatabaseWatcher {
|
|
15
|
+
dbPath;
|
|
16
|
+
onTrigger;
|
|
17
|
+
fallbackMs;
|
|
18
|
+
debounceMs;
|
|
19
|
+
timer = null;
|
|
20
|
+
watcher = null;
|
|
21
|
+
debounceTimer = null;
|
|
22
|
+
constructor(dbPath, onTrigger, fallbackMs = 5000, debounceMs = 50) {
|
|
23
|
+
this.dbPath = dbPath;
|
|
24
|
+
this.onTrigger = onTrigger;
|
|
25
|
+
this.fallbackMs = fallbackMs;
|
|
26
|
+
this.debounceMs = debounceMs;
|
|
27
|
+
}
|
|
28
|
+
start() {
|
|
29
|
+
this.timer = setInterval(() => this.trigger(), this.fallbackMs);
|
|
30
|
+
const watchTarget = fs_1.default.existsSync(this.dbPath)
|
|
31
|
+
? this.dbPath
|
|
32
|
+
: path_1.default.dirname(this.dbPath);
|
|
33
|
+
try {
|
|
34
|
+
this.watcher = fs_1.default.watch(watchTarget, () => {
|
|
35
|
+
if (this.debounceTimer)
|
|
36
|
+
clearTimeout(this.debounceTimer);
|
|
37
|
+
this.debounceTimer = setTimeout(() => this.trigger(), this.debounceMs);
|
|
38
|
+
});
|
|
39
|
+
// Async watch failures (deleted path, etc.) must not crash the process;
|
|
40
|
+
// fallback timer continues to drive polls.
|
|
41
|
+
this.watcher.on("error", () => {
|
|
42
|
+
if (this.watcher) {
|
|
43
|
+
try {
|
|
44
|
+
this.watcher.close();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* ignore */
|
|
48
|
+
}
|
|
49
|
+
this.watcher = null;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Ignore if path is not watchable yet; fallback timer still runs.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
trigger() {
|
|
58
|
+
this.onTrigger();
|
|
59
|
+
}
|
|
60
|
+
stop() {
|
|
61
|
+
if (this.timer)
|
|
62
|
+
clearInterval(this.timer);
|
|
63
|
+
if (this.debounceTimer)
|
|
64
|
+
clearTimeout(this.debounceTimer);
|
|
65
|
+
if (this.watcher)
|
|
66
|
+
this.watcher.close();
|
|
67
|
+
this.timer = null;
|
|
68
|
+
this.debounceTimer = null;
|
|
69
|
+
this.watcher = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.DatabaseWatcher = DatabaseWatcher;
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentbus/agentbus-client",
|
|
3
|
+
"version": "0.11.2",
|
|
4
|
+
"description": "TypeScript/Node EventEmitter client for AgentBus (MCP + fs.watch poll)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"agentbus",
|
|
19
|
+
"okf",
|
|
20
|
+
"mcp",
|
|
21
|
+
"agents"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/onicarps/agentbus.git",
|
|
30
|
+
"directory": "packages/js/agentbus-client"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/onicarps/agentbus/tree/main/packages/js/agentbus-client",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/onicarps/agentbus/issues"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"provenance": true
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.15.0",
|
|
45
|
+
"typescript": "^5.8.3",
|
|
46
|
+
"vitest": "^3.1.2"
|
|
47
|
+
}
|
|
48
|
+
}
|