@agentproto/cli 0.1.0-alpha.8 → 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/README.md +4 -4
- package/dist/cli.mjs +1848 -256
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +391 -41
- package/dist/index.mjs.map +1 -1
- package/dist/registry/builtins.d.ts +14 -0
- package/dist/registry/builtins.mjs +329 -0
- package/dist/registry/builtins.mjs.map +1 -0
- package/dist/registry/manifest.d.ts +88 -0
- package/dist/registry/manifest.mjs +42 -0
- package/dist/registry/manifest.mjs.map +1 -0
- package/dist/registry/plugins.d.ts +23 -0
- package/dist/registry/plugins.mjs +201 -0
- package/dist/registry/plugins.mjs.map +1 -0
- package/dist/registry/runtime.d.ts +70 -0
- package/dist/registry/runtime.mjs +52 -0
- package/dist/registry/runtime.mjs.map +1 -0
- package/dist/util/credentials.d.ts +71 -0
- package/dist/util/credentials.mjs +88 -0
- package/dist/util/credentials.mjs.map +1 -0
- package/package.json +35 -7
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { AdapterContext } from './runtime.js';
|
|
2
|
+
import '@agentproto/agent-runtime';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Built-in MultiAgentRuntime adapters. Registers reference adapters
|
|
6
|
+
* shipped by `@agentproto/agent-runtime` against the runtime registry
|
|
7
|
+
* so a vanilla `agentproto run-swarm` works without any plugins.
|
|
8
|
+
*
|
|
9
|
+
* Built-in `kind` strings: `file`, `mention`, `fs`, `agent-cli`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
declare function registerBuiltins(): void;
|
|
13
|
+
|
|
14
|
+
export { registerBuiltins };
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { resolve, dirname } from 'path';
|
|
2
|
+
import { appendFile, readFile, writeFile, stat, mkdir } from 'fs/promises';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import matter from 'gray-matter';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @agentproto/cli v0.1.0-alpha
|
|
9
|
+
* The `agentproto` binary — install / run / serve AIP-45 agent CLIs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
var HEADER_RE = /^=== TURN id=([^\s]+) participant=([^\s]+) ts=([^\s]+) ===$/;
|
|
13
|
+
var FileSubstrate = class {
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.opts = opts;
|
|
16
|
+
}
|
|
17
|
+
kind = "file";
|
|
18
|
+
async append(input) {
|
|
19
|
+
const timestamp = input.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
20
|
+
const id = hashTurnId(input.participantId, timestamp, input.content);
|
|
21
|
+
const turn = {
|
|
22
|
+
id,
|
|
23
|
+
participantId: input.participantId,
|
|
24
|
+
timestamp,
|
|
25
|
+
content: input.content,
|
|
26
|
+
meta: input.meta
|
|
27
|
+
};
|
|
28
|
+
await ensureDir(this.opts.path);
|
|
29
|
+
const block = formatTurnBlock(turn);
|
|
30
|
+
await appendFile(this.opts.path, block, "utf8");
|
|
31
|
+
return turn;
|
|
32
|
+
}
|
|
33
|
+
async read(since) {
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = await readFile(this.opts.path, "utf8");
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (isNotFound(err)) return [];
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
const turns = parseJournal(raw);
|
|
42
|
+
if (since === void 0) return turns;
|
|
43
|
+
const cutoff = turns.findIndex((t) => t.id === since);
|
|
44
|
+
if (cutoff === -1) return turns;
|
|
45
|
+
return turns.slice(cutoff + 1);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function formatTurnBlock(turn) {
|
|
49
|
+
const header = `=== TURN id=${turn.id} participant=${turn.participantId} ts=${turn.timestamp} ===
|
|
50
|
+
`;
|
|
51
|
+
const body = turn.content.endsWith("\n") ? turn.content : `${turn.content}
|
|
52
|
+
`;
|
|
53
|
+
return `${header}${body}`;
|
|
54
|
+
}
|
|
55
|
+
function parseJournal(raw) {
|
|
56
|
+
const lines = raw.split("\n");
|
|
57
|
+
const turns = [];
|
|
58
|
+
let current = null;
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
const match = HEADER_RE.exec(line);
|
|
61
|
+
if (match && match[1] && match[2] && match[3]) {
|
|
62
|
+
if (current) turns.push(materialise(current));
|
|
63
|
+
current = { id: match[1], pid: match[2], ts: match[3], buf: [] };
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (current) current.buf.push(line);
|
|
67
|
+
}
|
|
68
|
+
if (current) turns.push(materialise(current));
|
|
69
|
+
return turns;
|
|
70
|
+
}
|
|
71
|
+
function materialise(c) {
|
|
72
|
+
let body = c.buf.join("\n");
|
|
73
|
+
while (body.endsWith("\n")) body = body.slice(0, -1);
|
|
74
|
+
return {
|
|
75
|
+
id: c.id,
|
|
76
|
+
participantId: c.pid,
|
|
77
|
+
timestamp: c.ts,
|
|
78
|
+
content: body
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function hashTurnId(pid, ts, content) {
|
|
82
|
+
const h = createHash("sha256");
|
|
83
|
+
h.update(pid);
|
|
84
|
+
h.update("\0");
|
|
85
|
+
h.update(ts);
|
|
86
|
+
h.update("\0");
|
|
87
|
+
h.update(content);
|
|
88
|
+
return `t_${h.digest("hex").slice(0, 12)}`;
|
|
89
|
+
}
|
|
90
|
+
async function ensureDir(path) {
|
|
91
|
+
const dir = dirname(path);
|
|
92
|
+
try {
|
|
93
|
+
await stat(dir);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (isNotFound(err)) {
|
|
96
|
+
await mkdir(dir, { recursive: true });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function isNotFound(err) {
|
|
103
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ../agent-runtime/dist/adapters/dispatcher-mention.mjs
|
|
107
|
+
function textContainsMention(text, name) {
|
|
108
|
+
if (text.includes(`@${name}`)) return true;
|
|
109
|
+
if (name.includes(" ")) {
|
|
110
|
+
const firstName = name.split(" ")[0]?.trim();
|
|
111
|
+
if (!firstName) return false;
|
|
112
|
+
const regex = new RegExp(`@${firstName}(?!\\w)`, "i");
|
|
113
|
+
return regex.test(text);
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
var MentionDispatcher = class {
|
|
118
|
+
kind = "mention";
|
|
119
|
+
lastProcessedTriggerId;
|
|
120
|
+
async selectNext(input) {
|
|
121
|
+
const trigger = input.recentTurns[input.recentTurns.length - 1];
|
|
122
|
+
if (!trigger) return [];
|
|
123
|
+
if (trigger.id === this.lastProcessedTriggerId) return [];
|
|
124
|
+
const text = trigger.content;
|
|
125
|
+
const selected = [];
|
|
126
|
+
for (const p of input.participants) {
|
|
127
|
+
if (p.id === trigger.participantId) continue;
|
|
128
|
+
if (textContainsMention(text, p.displayName)) {
|
|
129
|
+
selected.push(p.id);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (selected.length > 0) {
|
|
133
|
+
this.lastProcessedTriggerId = trigger.id;
|
|
134
|
+
}
|
|
135
|
+
return selected;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var FileStateStore = class {
|
|
139
|
+
constructor(opts) {
|
|
140
|
+
this.opts = opts;
|
|
141
|
+
}
|
|
142
|
+
kind = "fs";
|
|
143
|
+
async read(participantId) {
|
|
144
|
+
const path = this.pathFor(participantId);
|
|
145
|
+
try {
|
|
146
|
+
const raw = await readFile(path, "utf8");
|
|
147
|
+
return JSON.parse(raw);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (isNotFound2(err)) return Object.freeze({});
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async write(participantId, state) {
|
|
154
|
+
await ensureDir2(this.opts.dir);
|
|
155
|
+
const path = this.pathFor(participantId);
|
|
156
|
+
await writeFile(path, JSON.stringify(state, null, 2), "utf8");
|
|
157
|
+
}
|
|
158
|
+
pathFor(participantId) {
|
|
159
|
+
const safe = participantId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
160
|
+
return resolve(this.opts.dir, `${safe}.json`);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
async function ensureDir2(dir) {
|
|
164
|
+
try {
|
|
165
|
+
await stat(dir);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (isNotFound2(err)) {
|
|
168
|
+
await mkdir(dir, { recursive: true });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function isNotFound2(err) {
|
|
175
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
176
|
+
}
|
|
177
|
+
var AgentCliParticipant = class {
|
|
178
|
+
constructor(opts) {
|
|
179
|
+
this.opts = opts;
|
|
180
|
+
}
|
|
181
|
+
kind = "agent-cli";
|
|
182
|
+
async executeTurn(input) {
|
|
183
|
+
const prompt = await assemblePrompt(input);
|
|
184
|
+
const stdout = await spawnWithStdin({
|
|
185
|
+
command: this.opts.command,
|
|
186
|
+
args: this.opts.args ?? [],
|
|
187
|
+
cwd: this.opts.cwd ?? process.cwd(),
|
|
188
|
+
timeoutMs: this.opts.timeoutMs ?? 9e4,
|
|
189
|
+
stdin: prompt,
|
|
190
|
+
signal: input.signal
|
|
191
|
+
});
|
|
192
|
+
const parsed = this.opts.parseOutput?.(stdout) ?? null;
|
|
193
|
+
const content = parsed ?? stdout.trimEnd();
|
|
194
|
+
return { content };
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
async function assemblePrompt(input) {
|
|
198
|
+
const roleText = input.participant.role ? await loadRole(input.participant.role) : "";
|
|
199
|
+
const transcript = input.recentTurns.map((t) => `[${t.participantId}] ${t.content}`).join("\n\n");
|
|
200
|
+
return [
|
|
201
|
+
roleText && `# Your role
|
|
202
|
+
|
|
203
|
+
${roleText}`,
|
|
204
|
+
`# Recent conversation
|
|
205
|
+
|
|
206
|
+
${transcript}`,
|
|
207
|
+
`# Your turn
|
|
208
|
+
|
|
209
|
+
You are ${input.participant.displayName}. The latest message in the conversation triggered you (most likely because it mentions you). Read the transcript above and reply in character. Keep it conversational unless the trigger asks for detailed work. Output only your reply \u2014 no preamble, no role labels, no quotes around the response.`
|
|
210
|
+
].filter(Boolean).join("\n\n");
|
|
211
|
+
}
|
|
212
|
+
var ROLE_FILE_EXTENSIONS = [".md", ".markdown", ".txt"];
|
|
213
|
+
async function loadRole(roleField) {
|
|
214
|
+
if (!looksLikeRoleFile(roleField)) return roleField;
|
|
215
|
+
try {
|
|
216
|
+
const raw = await readFile(roleField, "utf8");
|
|
217
|
+
const parsed = matter(raw);
|
|
218
|
+
return parsed.content.trim();
|
|
219
|
+
} catch (err) {
|
|
220
|
+
const code = err.code;
|
|
221
|
+
process.stderr.write(
|
|
222
|
+
`agent-cli participant: role path '${roleField}' not readable (${code ?? "unknown"}); using the literal string as inline role text.
|
|
223
|
+
`
|
|
224
|
+
);
|
|
225
|
+
return roleField;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function looksLikeRoleFile(s) {
|
|
229
|
+
const lower = s.toLowerCase();
|
|
230
|
+
return ROLE_FILE_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
231
|
+
}
|
|
232
|
+
async function spawnWithStdin(args) {
|
|
233
|
+
return new Promise((resolveP, rejectP) => {
|
|
234
|
+
const proc = spawn(args.command, [...args.args], {
|
|
235
|
+
cwd: args.cwd,
|
|
236
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
237
|
+
});
|
|
238
|
+
let stdout = "";
|
|
239
|
+
let stderr = "";
|
|
240
|
+
const onAbort = () => proc.kill("SIGTERM");
|
|
241
|
+
args.signal?.addEventListener("abort", onAbort);
|
|
242
|
+
const timer = setTimeout(() => {
|
|
243
|
+
proc.kill("SIGTERM");
|
|
244
|
+
}, args.timeoutMs);
|
|
245
|
+
proc.stdout.on("data", (chunk) => {
|
|
246
|
+
stdout += chunk.toString("utf8");
|
|
247
|
+
});
|
|
248
|
+
proc.stderr.on("data", (chunk) => {
|
|
249
|
+
stderr += chunk.toString("utf8");
|
|
250
|
+
});
|
|
251
|
+
proc.on("error", (err) => {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
args.signal?.removeEventListener("abort", onAbort);
|
|
254
|
+
rejectP(err);
|
|
255
|
+
});
|
|
256
|
+
proc.on("close", (code) => {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
args.signal?.removeEventListener("abort", onAbort);
|
|
259
|
+
if (code === 0) {
|
|
260
|
+
resolveP(stdout);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
rejectP(
|
|
264
|
+
new Error(
|
|
265
|
+
`agent-cli participant "${args.command}" exited with code ${code}: ${stderr.trim() || "(no stderr)"}`
|
|
266
|
+
)
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
proc.stdin.write(args.stdin);
|
|
270
|
+
proc.stdin.end();
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function parseClaudeJsonOutput(stdout) {
|
|
274
|
+
try {
|
|
275
|
+
const parsed = JSON.parse(stdout);
|
|
276
|
+
if (typeof parsed === "object" && parsed !== null && "result" in parsed && typeof parsed.result === "string") {
|
|
277
|
+
return parsed.result;
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/registry/runtime.ts
|
|
286
|
+
var substrates = /* @__PURE__ */ new Map();
|
|
287
|
+
var dispatchers = /* @__PURE__ */ new Map();
|
|
288
|
+
var executors = /* @__PURE__ */ new Map();
|
|
289
|
+
var stateStores = /* @__PURE__ */ new Map();
|
|
290
|
+
function registerSubstrate(kind, factory) {
|
|
291
|
+
substrates.set(kind, factory);
|
|
292
|
+
}
|
|
293
|
+
function registerDispatcher(kind, factory) {
|
|
294
|
+
dispatchers.set(kind, factory);
|
|
295
|
+
}
|
|
296
|
+
function registerExecutor(kind, factory) {
|
|
297
|
+
executors.set(kind, factory);
|
|
298
|
+
}
|
|
299
|
+
function registerStateStore(kind, factory) {
|
|
300
|
+
stateStores.set(kind, factory);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/registry/builtins.ts
|
|
304
|
+
function registerBuiltins() {
|
|
305
|
+
registerSubstrate("file", (cfg, ctx) => {
|
|
306
|
+
const path = typeof cfg.path === "string" ? cfg.path : ".runtime/conversation.md";
|
|
307
|
+
return new FileSubstrate({ path: resolve(ctx.baseDir, path) });
|
|
308
|
+
});
|
|
309
|
+
registerDispatcher("mention", () => new MentionDispatcher());
|
|
310
|
+
registerStateStore("fs", (cfg, ctx) => {
|
|
311
|
+
const dir = typeof cfg.dir === "string" ? cfg.dir : ".runtime/state";
|
|
312
|
+
return new FileStateStore({ dir: resolve(ctx.baseDir, dir) });
|
|
313
|
+
});
|
|
314
|
+
registerExecutor("agent-cli", (cfg) => buildAgentCli(cfg));
|
|
315
|
+
}
|
|
316
|
+
function buildAgentCli(cfg) {
|
|
317
|
+
const command = typeof cfg.command === "string" ? cfg.command : "claude";
|
|
318
|
+
const args = Array.isArray(cfg.args) ? cfg.args.filter((a) => typeof a === "string") : ["--print", "--output-format=json"];
|
|
319
|
+
const useClaudeJson = cfg.parseJson !== false && command === "claude";
|
|
320
|
+
return new AgentCliParticipant({
|
|
321
|
+
command,
|
|
322
|
+
args,
|
|
323
|
+
parseOutput: useClaudeJson ? parseClaudeJsonOutput : void 0
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export { registerBuiltins };
|
|
328
|
+
//# sourceMappingURL=builtins.mjs.map
|
|
329
|
+
//# sourceMappingURL=builtins.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../agent-runtime/src/adapters/substrate-file.ts","../../../agent-runtime/src/util/mention-parser.mjs","../../../agent-runtime/src/adapters/dispatcher-mention.ts","../../../agent-runtime/src/adapters/state-fs.ts","../../../agent-runtime/src/adapters/participant-agent-cli.ts","../../src/registry/runtime.ts","../../src/registry/builtins.ts"],"names":["readFile","isNotFound","ensureDir","writeFile","resolvePath","stat","mkdir"],"mappings":"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","file":"builtins.mjs","sourcesContent":["/**\n * File substrate — append-only markdown journal.\n *\n * Each turn is delimited by a header line:\n * === TURN id=<id> participant=<pid> ts=<iso> ===\n * <body lines…>\n *\n * Ids are content-hashed, so re-running an append with the same body\n * is idempotent at the row level.\n *\n * Caveats:\n * - `appendFile` does not fsync. A power-loss between buffered write\n * and disk flush can lose the most recent turn(s); callers that\n * need durability across crashes should layer their own fsync.\n * - The parser splits on lines matching `=== TURN ... ===`. Turn\n * content containing such a line will corrupt subsequent parses.\n * Treat turn content as untrusted? Sanitize before append.\n */\n\nimport { appendFile, readFile, writeFile, mkdir, stat } from \"node:fs/promises\"\nimport { dirname } from \"node:path\"\nimport { createHash } from \"node:crypto\"\nimport type { Substrate, Turn, TurnInput, TurnId } from \"../ports.js\"\n\nexport type FileSubstrateOptions = {\n /** Absolute path to the journal file. */\n readonly path: string\n}\n\nconst HEADER_RE =\n /^=== TURN id=([^\\s]+) participant=([^\\s]+) ts=([^\\s]+) ===$/\n\nexport class FileSubstrate implements Substrate {\n readonly kind = \"file\"\n\n constructor(private readonly opts: FileSubstrateOptions) {}\n\n async append(input: TurnInput): Promise<Turn> {\n const timestamp = input.timestamp ?? new Date().toISOString()\n const id = hashTurnId(input.participantId, timestamp, input.content)\n const turn: Turn = {\n id,\n participantId: input.participantId,\n timestamp,\n content: input.content,\n meta: input.meta,\n }\n await ensureDir(this.opts.path)\n const block = formatTurnBlock(turn)\n await appendFile(this.opts.path, block, \"utf8\")\n return turn\n }\n\n async read(since?: TurnId): Promise<readonly Turn[]> {\n let raw: string\n try {\n raw = await readFile(this.opts.path, \"utf8\")\n } catch (err) {\n if (isNotFound(err)) return []\n throw err\n }\n const turns = parseJournal(raw)\n if (since === undefined) return turns\n const cutoff = turns.findIndex((t) => t.id === since)\n if (cutoff === -1) return turns\n return turns.slice(cutoff + 1)\n }\n}\n\n/**\n * Reset the journal by truncating it. Useful for tests. Not part of the\n * Substrate interface — callers must reach for the adapter directly.\n */\nexport async function resetFileSubstrate(path: string): Promise<void> {\n await ensureDir(path)\n await writeFile(path, \"\", \"utf8\")\n}\n\nfunction formatTurnBlock(turn: Turn): string {\n const header = `=== TURN id=${turn.id} participant=${turn.participantId} ts=${turn.timestamp} ===\\n`\n const body = turn.content.endsWith(\"\\n\") ? turn.content : `${turn.content}\\n`\n return `${header}${body}`\n}\n\nfunction parseJournal(raw: string): Turn[] {\n const lines = raw.split(\"\\n\")\n const turns: Turn[] = []\n let current: { id: string; pid: string; ts: string; buf: string[] } | null = null\n\n for (const line of lines) {\n const match = HEADER_RE.exec(line)\n if (match && match[1] && match[2] && match[3]) {\n if (current) turns.push(materialise(current))\n current = { id: match[1], pid: match[2], ts: match[3], buf: [] }\n continue\n }\n if (current) current.buf.push(line)\n }\n if (current) turns.push(materialise(current))\n return turns\n}\n\nfunction materialise(c: {\n id: string\n pid: string\n ts: string\n buf: string[]\n}): Turn {\n // Drop the trailing empty line that appendFile leaves between blocks.\n let body = c.buf.join(\"\\n\")\n while (body.endsWith(\"\\n\")) body = body.slice(0, -1)\n return {\n id: c.id,\n participantId: c.pid,\n timestamp: c.ts,\n content: body,\n }\n}\n\nfunction hashTurnId(pid: string, ts: string, content: string): string {\n const h = createHash(\"sha256\")\n h.update(pid)\n h.update(\"\\x00\")\n h.update(ts)\n h.update(\"\\x00\")\n h.update(content)\n return `t_${h.digest(\"hex\").slice(0, 12)}`\n}\n\nasync function ensureDir(path: string): Promise<void> {\n const dir = dirname(path)\n try {\n await stat(dir)\n } catch (err) {\n if (isNotFound(err)) {\n await mkdir(dir, { recursive: true })\n return\n }\n throw err\n }\n}\n\nfunction isNotFound(err: unknown): boolean {\n return (\n typeof err === \"object\" &&\n err !== null &&\n \"code\" in err &&\n (err as { code: string }).code === \"ENOENT\"\n )\n}\n","/**\n * Canonical @-mention parser.\n *\n * Lives as raw JavaScript (no types) because it's consumed in two\n * places that don't share a build:\n *\n * 1. The kernel imports `textContainsMention` from this file as a\n * normal ESM module (the dispatcher in `adapters/dispatcher-mention.ts`).\n *\n * 2. Runtime profiles inline this file's source into Claude Code\n * hooks at profile-build time, because `.claude/` has no\n * module-resolution at hook-execution time. The build script\n * reads this file as text and stamps it into the hook template.\n *\n * Two cases:\n * 1. Literal `@<name>` substring (case-sensitive, full name).\n * 2. For multi-word names, also `@<firstName>` with non-word\n * boundary (case-insensitive).\n *\n * Edit here only — both consumers stay in sync via build pipelines.\n */\n\n/** @param {string} text @param {string} name @returns {boolean} */\nexport function textContainsMention(text, name) {\n if (text.includes(`@${name}`)) return true\n if (name.includes(\" \")) {\n const firstName = name.split(\" \")[0]?.trim()\n if (!firstName) return false\n const regex = new RegExp(`@${firstName}(?!\\\\w)`, \"i\")\n return regex.test(text)\n }\n return false\n}\n","/**\n * Mention dispatcher — selects participants whose displayName is\n * @-mentioned in the most recent turn.\n *\n * Skip rules:\n * - never re-select the participant who authored the trigger turn\n * (otherwise they'd reply to themselves indefinitely)\n * - never dispatch on a trigger we already processed (in-memory cursor —\n * prevents the loop from re-firing on the same @mention every poll\n * when the kernel's own reply isn't yet visible in the next read).\n * Per-dispatcher-instance state; a fresh swarm process starts with\n * no cursor and naturally responds to the latest unhandled mention\n * exactly once.\n * - if no mentions match any known participant, return [] (idle)\n *\n * The parser itself lives in `../util/mention-parser.mjs` (vanilla JS)\n * so runtime profiles can stamp the same implementation into Claude\n * Code hooks at build time without depending on the TS build pipeline.\n */\n\nimport type {\n Dispatcher,\n DispatcherInput,\n ParticipantId,\n TurnId,\n} from \"../ports.js\"\nimport { textContainsMention } from \"../util/mention-parser.mjs\"\n\nexport class MentionDispatcher implements Dispatcher {\n readonly kind = \"mention\"\n private lastProcessedTriggerId: TurnId | undefined\n\n async selectNext(input: DispatcherInput): Promise<readonly ParticipantId[]> {\n const trigger = input.recentTurns[input.recentTurns.length - 1]\n if (!trigger) return []\n if (trigger.id === this.lastProcessedTriggerId) return []\n const text = trigger.content\n\n const selected: ParticipantId[] = []\n for (const p of input.participants) {\n if (p.id === trigger.participantId) continue\n if (textContainsMention(text, p.displayName)) {\n selected.push(p.id)\n }\n }\n\n if (selected.length > 0) {\n this.lastProcessedTriggerId = trigger.id\n }\n return selected\n }\n}\n\n// Re-exported so callers writing their own adapters can detect\n// mentions with identical semantics.\nexport { textContainsMention }\n","/**\n * Filesystem state store — one JSON file per participant.\n *\n * <stateDir>/<participantId>.json\n *\n * Missing files read as {}. Writes are full overwrites; callers\n * pass the merged state, the adapter doesn't merge on its own.\n */\n\nimport { readFile, writeFile, mkdir, stat } from \"node:fs/promises\"\nimport { resolve as resolvePath } from \"node:path\"\nimport type { ParticipantId, StateStore } from \"../ports.js\"\n\nexport type FileStateStoreOptions = {\n /** Directory to hold the per-participant JSON files. */\n readonly dir: string\n}\n\nexport class FileStateStore implements StateStore {\n readonly kind = \"fs\"\n\n constructor(private readonly opts: FileStateStoreOptions) {}\n\n async read(participantId: ParticipantId): Promise<Readonly<Record<string, unknown>>> {\n const path = this.pathFor(participantId)\n try {\n const raw = await readFile(path, \"utf8\")\n return JSON.parse(raw) as Record<string, unknown>\n } catch (err) {\n if (isNotFound(err)) return Object.freeze({})\n throw err\n }\n }\n\n async write(\n participantId: ParticipantId,\n state: Readonly<Record<string, unknown>>\n ): Promise<void> {\n await ensureDir(this.opts.dir)\n const path = this.pathFor(participantId)\n await writeFile(path, JSON.stringify(state, null, 2), \"utf8\")\n }\n\n private pathFor(participantId: ParticipantId): string {\n // Sanitize: participant ids are user-controlled. Disallow path traversal.\n const safe = participantId.replace(/[^a-zA-Z0-9._-]/g, \"_\")\n return resolvePath(this.opts.dir, `${safe}.json`)\n }\n}\n\nasync function ensureDir(dir: string): Promise<void> {\n try {\n await stat(dir)\n } catch (err) {\n if (isNotFound(err)) {\n await mkdir(dir, { recursive: true })\n return\n }\n throw err\n }\n}\n\nfunction isNotFound(err: unknown): boolean {\n return (\n typeof err === \"object\" &&\n err !== null &&\n \"code\" in err &&\n (err as { code: string }).code === \"ENOENT\"\n )\n}\n","/**\n * Agent-CLI participant — spawns a CLI binary, pipes the assembled prompt\n * over stdin, and uses captured stdout as the turn content.\n *\n * Works for any CLI that supports a one-shot \"read prompt from stdin,\n * print response to stdout\" mode (claude-code's `--print`, hermes' `-p`,\n * etc.).\n */\n\nimport { spawn } from \"node:child_process\"\nimport { readFile } from \"node:fs/promises\"\nimport matter from \"gray-matter\"\nimport type {\n ParticipantExecuteInput,\n ParticipantExecuteOutput,\n ParticipantExecutor,\n} from \"../ports.js\"\n\nexport type AgentCliParticipantOptions = {\n /** Executable to invoke. Examples: \"claude\", \"hermes\", \"goose\". */\n readonly command: string\n /** Static args. The prompt is fed over stdin, not as an argument. */\n readonly args?: readonly string[]\n /** Working directory. Defaults to process.cwd(). */\n readonly cwd?: string\n /** Hard timeout in ms. Default 90000. */\n readonly timeoutMs?: number\n /** Optional output parser — if returns null, content falls back to raw stdout. */\n readonly parseOutput?: (stdout: string) => string | null\n}\n\nexport class AgentCliParticipant implements ParticipantExecutor {\n readonly kind = \"agent-cli\"\n\n constructor(private readonly opts: AgentCliParticipantOptions) {}\n\n async executeTurn(\n input: ParticipantExecuteInput\n ): Promise<ParticipantExecuteOutput> {\n const prompt = await assemblePrompt(input)\n const stdout = await spawnWithStdin({\n command: this.opts.command,\n args: this.opts.args ?? [],\n cwd: this.opts.cwd ?? process.cwd(),\n timeoutMs: this.opts.timeoutMs ?? 90000,\n stdin: prompt,\n signal: input.signal,\n })\n const parsed = this.opts.parseOutput?.(stdout) ?? null\n const content = parsed ?? stdout.trimEnd()\n return { content }\n }\n}\n\nasync function assemblePrompt(\n input: ParticipantExecuteInput\n): Promise<string> {\n const roleText = input.participant.role\n ? await loadRole(input.participant.role)\n : \"\"\n\n const transcript = input.recentTurns\n .map((t) => `[${t.participantId}] ${t.content}`)\n .join(\"\\n\\n\")\n\n return [\n roleText && `# Your role\\n\\n${roleText}`,\n `# Recent conversation\\n\\n${transcript}`,\n `# Your turn\\n\\nYou are ${input.participant.displayName}. The latest message in the conversation triggered you (most likely because it mentions you). Read the transcript above and reply in character. Keep it conversational unless the trigger asks for detailed work. Output only your reply — no preamble, no role labels, no quotes around the response.`,\n ]\n .filter(Boolean)\n .join(\"\\n\\n\")\n}\n\n// File extensions we treat as a path-to-role-file. Anything else is\n// inline role text — even strings that contain `/`, which are common\n// in normal sentences (\"I am an AI/ML reviewer\").\nconst ROLE_FILE_EXTENSIONS = [\".md\", \".markdown\", \".txt\"]\n\nasync function loadRole(roleField: string): Promise<string> {\n if (!looksLikeRoleFile(roleField)) return roleField\n try {\n const raw = await readFile(roleField, \"utf8\")\n // Strip optional YAML frontmatter — lets a Claude Code agent\n // definition file (.claude/agents/*.md) double as a swarm role\n // without ferrying the agent's metadata into the prompt.\n const parsed = matter(raw)\n return parsed.content.trim()\n } catch (err) {\n // The string had a role-file extension but the file isn't readable\n // — surface a hint on stderr so authors don't silently get the\n // literal path as a prompt. Still fall back to inline so the swarm\n // doesn't crash on a typo.\n const code = (err as NodeJS.ErrnoException).code\n process.stderr.write(\n `agent-cli participant: role path '${roleField}' not readable (${code ?? \"unknown\"}); using the literal string as inline role text.\\n`\n )\n return roleField\n }\n}\n\nfunction looksLikeRoleFile(s: string): boolean {\n const lower = s.toLowerCase()\n return ROLE_FILE_EXTENSIONS.some((ext) => lower.endsWith(ext))\n}\n\ntype SpawnArgs = {\n command: string\n args: readonly string[]\n cwd: string\n timeoutMs: number\n stdin: string\n signal?: AbortSignal\n}\n\nasync function spawnWithStdin(args: SpawnArgs): Promise<string> {\n return new Promise((resolveP, rejectP) => {\n const proc = spawn(args.command, [...args.args], {\n cwd: args.cwd,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n })\n\n let stdout = \"\"\n let stderr = \"\"\n const onAbort = () => proc.kill(\"SIGTERM\")\n args.signal?.addEventListener(\"abort\", onAbort)\n\n const timer = setTimeout(() => {\n proc.kill(\"SIGTERM\")\n }, args.timeoutMs)\n\n proc.stdout.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString(\"utf8\")\n })\n proc.stderr.on(\"data\", (chunk: Buffer) => {\n stderr += chunk.toString(\"utf8\")\n })\n proc.on(\"error\", (err) => {\n clearTimeout(timer)\n args.signal?.removeEventListener(\"abort\", onAbort)\n rejectP(err)\n })\n proc.on(\"close\", (code) => {\n clearTimeout(timer)\n args.signal?.removeEventListener(\"abort\", onAbort)\n if (code === 0) {\n resolveP(stdout)\n return\n }\n rejectP(\n new Error(\n `agent-cli participant \"${args.command}\" exited with code ${code}: ${stderr.trim() || \"(no stderr)\"}`\n )\n )\n })\n\n proc.stdin.write(args.stdin)\n proc.stdin.end()\n })\n}\n\n/**\n * Parser for `claude --output-format=json` responses. Returns the\n * `result` field as the turn content, or null if the JSON doesn't have\n * the expected shape (caller falls back to raw stdout).\n */\nexport function parseClaudeJsonOutput(stdout: string): string | null {\n try {\n const parsed = JSON.parse(stdout) as unknown\n if (\n typeof parsed === \"object\" &&\n parsed !== null &&\n \"result\" in parsed &&\n typeof (parsed as { result: unknown }).result === \"string\"\n ) {\n return (parsed as { result: string }).result\n }\n return null\n } catch {\n return null\n }\n}\n","/**\n * MultiAgentRuntime adapter registry — the seam third-party packages\n * use to plug substrates, dispatchers, participant executors, and\n * state stores into `agentproto run-swarm` without forking the CLI.\n *\n * A plugin module exports nothing required by name — it just imports\n * `register*` helpers from `@agentproto/cli/registry/runtime` and\n * calls them at module load. The CLI discovers plugins via:\n *\n * 1. `--plugin <module-id>` flags on `run-swarm`\n * 2. The `plugins[]` array in `~/.agentproto/config.json`\n * 3. Auto-registered built-ins (file substrate, mention dispatcher,\n * fs state, agent-cli participant) — registered by\n * `registerBuiltins()` at startup.\n *\n * Manifest `kind` strings are resolved through the registry: the kind\n * is the lookup key; the factory builds the concrete adapter from the\n * (loose) manifest config + shared context.\n */\n\nimport type {\n Dispatcher,\n ParticipantExecutor,\n StateStore,\n Substrate,\n} from \"@agentproto/agent-runtime\"\n\n// ── Context passed to every factory ──\n\n/**\n * Shared context every factory receives. Carries the manifest's base\n * directory (for resolving relative paths declared in adapter configs)\n * and a `cleanup` collector so adapters that hold disposable resources\n * (MCP clients, sockets, child processes) can register teardown callbacks\n * that the CLI runs on shutdown.\n */\nexport interface AdapterContext {\n /** Absolute directory of the manifest file. */\n readonly baseDir: string\n /** Register a teardown callback to run when the swarm shuts down. */\n registerCleanup(fn: () => Promise<void> | void): void\n}\n\n// ── Loose config shape ──\n\n/**\n * Manifest adapter blocks are loose: `{ kind: string, ...host-extension }`.\n * Each factory pulls its own typed fields off the config and validates\n * them inline.\n */\nexport interface AdapterConfig {\n readonly kind: string\n readonly [extension: string]: unknown\n}\n\n// ── Factory signatures ──\n\nexport type SubstrateFactory = (\n config: AdapterConfig,\n ctx: AdapterContext\n) => Promise<Substrate> | Substrate\n\nexport type DispatcherFactory = (\n config: AdapterConfig,\n ctx: AdapterContext\n) => Promise<Dispatcher> | Dispatcher\n\nexport type ExecutorFactory = (\n config: AdapterConfig,\n ctx: AdapterContext\n) => Promise<ParticipantExecutor> | ParticipantExecutor\n\nexport type StateStoreFactory = (\n config: AdapterConfig,\n ctx: AdapterContext\n) => Promise<StateStore> | StateStore\n\n// ── Registry state ──\n\nconst substrates = new Map<string, SubstrateFactory>()\nconst dispatchers = new Map<string, DispatcherFactory>()\nconst executors = new Map<string, ExecutorFactory>()\nconst stateStores = new Map<string, StateStoreFactory>()\n\n// ── Public registration API ──\n\nexport function registerSubstrate(kind: string, factory: SubstrateFactory): void {\n substrates.set(kind, factory)\n}\n\nexport function registerDispatcher(\n kind: string,\n factory: DispatcherFactory\n): void {\n dispatchers.set(kind, factory)\n}\n\nexport function registerExecutor(kind: string, factory: ExecutorFactory): void {\n executors.set(kind, factory)\n}\n\nexport function registerStateStore(\n kind: string,\n factory: StateStoreFactory\n): void {\n stateStores.set(kind, factory)\n}\n\n// ── Lookup API (used by run-swarm wiring) ──\n\nexport function getSubstrateFactory(kind: string): SubstrateFactory | undefined {\n return substrates.get(kind)\n}\n\nexport function getDispatcherFactory(\n kind: string\n): DispatcherFactory | undefined {\n return dispatchers.get(kind)\n}\n\nexport function getExecutorFactory(kind: string): ExecutorFactory | undefined {\n return executors.get(kind)\n}\n\nexport function getStateStoreFactory(\n kind: string\n): StateStoreFactory | undefined {\n return stateStores.get(kind)\n}\n\nexport function listRegisteredKinds(): {\n substrates: readonly string[]\n dispatchers: readonly string[]\n executors: readonly string[]\n stateStores: readonly string[]\n} {\n return {\n substrates: [...substrates.keys()],\n dispatchers: [...dispatchers.keys()],\n executors: [...executors.keys()],\n stateStores: [...stateStores.keys()],\n }\n}\n\n/**\n * Test-only: drop every registration. Plugin registrations persist for\n * the lifetime of the process, so tests that load + unload plugins use\n * this to start from a clean slate.\n */\nexport function _resetRegistryForTests(): void {\n substrates.clear()\n dispatchers.clear()\n executors.clear()\n stateStores.clear()\n}\n","/**\n * Built-in MultiAgentRuntime adapters. Registers reference adapters\n * shipped by `@agentproto/agent-runtime` against the runtime registry\n * so a vanilla `agentproto run-swarm` works without any plugins.\n *\n * Built-in `kind` strings: `file`, `mention`, `fs`, `agent-cli`.\n */\n\nimport { resolve as resolvePath } from \"node:path\"\nimport { FileSubstrate } from \"@agentproto/agent-runtime/adapters/substrate-file\"\nimport { MentionDispatcher } from \"@agentproto/agent-runtime/adapters/dispatcher-mention\"\nimport { FileStateStore } from \"@agentproto/agent-runtime/adapters/state-fs\"\nimport {\n AgentCliParticipant,\n parseClaudeJsonOutput,\n} from \"@agentproto/agent-runtime/adapters/participant-agent-cli\"\nimport {\n registerDispatcher,\n registerExecutor,\n registerStateStore,\n registerSubstrate,\n type AdapterConfig,\n type AdapterContext,\n} from \"./runtime.js\"\n\nexport function registerBuiltins(): void {\n registerSubstrate(\"file\", (cfg, ctx) => {\n const path = typeof cfg.path === \"string\" ? cfg.path : \".runtime/conversation.md\"\n return new FileSubstrate({ path: resolvePath(ctx.baseDir, path) })\n })\n\n registerDispatcher(\"mention\", () => new MentionDispatcher())\n\n registerStateStore(\"fs\", (cfg, ctx) => {\n const dir = typeof cfg.dir === \"string\" ? cfg.dir : \".runtime/state\"\n return new FileStateStore({ dir: resolvePath(ctx.baseDir, dir) })\n })\n\n registerExecutor(\"agent-cli\", (cfg) => buildAgentCli(cfg))\n}\n\nfunction buildAgentCli(cfg: AdapterConfig): AgentCliParticipant {\n const command = typeof cfg.command === \"string\" ? cfg.command : \"claude\"\n const args = Array.isArray(cfg.args)\n ? cfg.args.filter((a): a is string => typeof a === \"string\")\n : [\"--print\", \"--output-format=json\"]\n const useClaudeJson = cfg.parseJson !== false && command === \"claude\"\n return new AgentCliParticipant({\n command,\n args,\n parseOutput: useClaudeJson ? parseClaudeJsonOutput : undefined,\n })\n}\n\n// Suppress unused-context warning on registrations that don't read it.\nexport type { AdapterContext }\n"]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin manifest — `agentproto/plugin/v1`.
|
|
5
|
+
*
|
|
6
|
+
* Plugins declare what they provide in either:
|
|
7
|
+
* - their `package.json` under the `agentproto` key, OR
|
|
8
|
+
* - a standalone `agentproto.json` next to their `package.json`.
|
|
9
|
+
*
|
|
10
|
+
* The CLI reads this manifest, dynamic-imports each adapter entry,
|
|
11
|
+
* and registers it with the runtime registry. Plugins don't need
|
|
12
|
+
* side-effect imports any more — they just export their factory
|
|
13
|
+
* functions and let the manifest do the wiring.
|
|
14
|
+
*
|
|
15
|
+
* Example (in @guilde/agentproto-bridge's package.json):
|
|
16
|
+
*
|
|
17
|
+
* {
|
|
18
|
+
* "name": "@guilde/agentproto-bridge",
|
|
19
|
+
* "agentproto": {
|
|
20
|
+
* "schema": "agentproto/plugin/v1",
|
|
21
|
+
* "substrates": [
|
|
22
|
+
* {
|
|
23
|
+
* "kind": "guilde-mcp",
|
|
24
|
+
* "entry": "./dist/index.mjs",
|
|
25
|
+
* "export": "guildeMcpSubstrateFactory",
|
|
26
|
+
* "capabilities": ["mentions", "reactions", "identity"],
|
|
27
|
+
* "description": "Reads/writes turns through Guilde's MCP server."
|
|
28
|
+
* }
|
|
29
|
+
* ],
|
|
30
|
+
* "executors": [
|
|
31
|
+
* {
|
|
32
|
+
* "kind": "db-operator",
|
|
33
|
+
* "entry": "./dist/index.mjs",
|
|
34
|
+
* "export": "dbOperatorExecutorFactory",
|
|
35
|
+
* "description": "Delegates to Mastra operators via run_operator."
|
|
36
|
+
* }
|
|
37
|
+
* ]
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
declare const PLUGIN_MANIFEST_SCHEMA: "agentproto/plugin/v1";
|
|
43
|
+
declare const AdapterEntrySchema: z.ZodObject<{
|
|
44
|
+
kind: z.ZodString;
|
|
45
|
+
entry: z.ZodString;
|
|
46
|
+
export: z.ZodString;
|
|
47
|
+
description: z.ZodOptional<z.ZodString>;
|
|
48
|
+
}, z.core.$loose>;
|
|
49
|
+
declare const SubstrateEntrySchema: z.ZodObject<{
|
|
50
|
+
kind: z.ZodString;
|
|
51
|
+
entry: z.ZodString;
|
|
52
|
+
export: z.ZodString;
|
|
53
|
+
description: z.ZodOptional<z.ZodString>;
|
|
54
|
+
capabilities: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
55
|
+
}, z.core.$loose>;
|
|
56
|
+
declare const PluginManifestSchema: z.ZodObject<{
|
|
57
|
+
schema: z.ZodLiteral<"agentproto/plugin/v1">;
|
|
58
|
+
substrates: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
59
|
+
kind: z.ZodString;
|
|
60
|
+
entry: z.ZodString;
|
|
61
|
+
export: z.ZodString;
|
|
62
|
+
description: z.ZodOptional<z.ZodString>;
|
|
63
|
+
capabilities: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
64
|
+
}, z.core.$loose>>>;
|
|
65
|
+
dispatchers: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
66
|
+
kind: z.ZodString;
|
|
67
|
+
entry: z.ZodString;
|
|
68
|
+
export: z.ZodString;
|
|
69
|
+
description: z.ZodOptional<z.ZodString>;
|
|
70
|
+
}, z.core.$loose>>>;
|
|
71
|
+
executors: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
72
|
+
kind: z.ZodString;
|
|
73
|
+
entry: z.ZodString;
|
|
74
|
+
export: z.ZodString;
|
|
75
|
+
description: z.ZodOptional<z.ZodString>;
|
|
76
|
+
}, z.core.$loose>>>;
|
|
77
|
+
stateStores: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
78
|
+
kind: z.ZodString;
|
|
79
|
+
entry: z.ZodString;
|
|
80
|
+
export: z.ZodString;
|
|
81
|
+
description: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, z.core.$loose>>>;
|
|
83
|
+
}, z.core.$loose>;
|
|
84
|
+
type PluginManifest = z.infer<typeof PluginManifestSchema>;
|
|
85
|
+
type AdapterEntry = z.infer<typeof AdapterEntrySchema>;
|
|
86
|
+
type SubstrateEntry = z.infer<typeof SubstrateEntrySchema>;
|
|
87
|
+
|
|
88
|
+
export { type AdapterEntry, PLUGIN_MANIFEST_SCHEMA, type PluginManifest, PluginManifestSchema, type SubstrateEntry };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @agentproto/cli v0.1.0-alpha
|
|
5
|
+
* The `agentproto` binary — install / run / serve AIP-45 agent CLIs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
var PLUGIN_MANIFEST_SCHEMA = "agentproto/plugin/v1";
|
|
9
|
+
var AdapterEntrySchema = z.object({
|
|
10
|
+
/** The `kind` string the manifest's substrate/dispatcher/etc. block uses. */
|
|
11
|
+
kind: z.string().min(1),
|
|
12
|
+
/**
|
|
13
|
+
* Path to the entry module, relative to the plugin package root.
|
|
14
|
+
* Resolved via the plugin's `package.json#main`/`exports` (i.e. you
|
|
15
|
+
* can use a subpath like `./dist/substrates.mjs` or an export name
|
|
16
|
+
* like `.` if the plugin re-exports everything from its root).
|
|
17
|
+
*/
|
|
18
|
+
entry: z.string().min(1),
|
|
19
|
+
/** Named export inside `entry` — the factory function. */
|
|
20
|
+
export: z.string().min(1),
|
|
21
|
+
/** Free-form description; shown by `agentproto plugins show`. */
|
|
22
|
+
description: z.string().optional()
|
|
23
|
+
}).loose();
|
|
24
|
+
var SubstrateEntrySchema = AdapterEntrySchema.extend({
|
|
25
|
+
/**
|
|
26
|
+
* Free-form capability tags. Not gated by the kernel yet, but
|
|
27
|
+
* surfaced by `agentproto plugins show` so users can see what the
|
|
28
|
+
* substrate claims to support (mentions, reactions, visibility, …).
|
|
29
|
+
*/
|
|
30
|
+
capabilities: z.array(z.string()).optional()
|
|
31
|
+
});
|
|
32
|
+
var PluginManifestSchema = z.object({
|
|
33
|
+
schema: z.literal(PLUGIN_MANIFEST_SCHEMA),
|
|
34
|
+
substrates: z.array(SubstrateEntrySchema).default([]),
|
|
35
|
+
dispatchers: z.array(AdapterEntrySchema).default([]),
|
|
36
|
+
executors: z.array(AdapterEntrySchema).default([]),
|
|
37
|
+
stateStores: z.array(AdapterEntrySchema).default([])
|
|
38
|
+
}).loose();
|
|
39
|
+
|
|
40
|
+
export { PLUGIN_MANIFEST_SCHEMA, PluginManifestSchema };
|
|
41
|
+
//# sourceMappingURL=manifest.mjs.map
|
|
42
|
+
//# sourceMappingURL=manifest.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/registry/manifest.ts"],"names":[],"mappings":";;;;;;;AAyCO,IAAM,sBAAA,GAAyB;AAEtC,IAAM,kBAAA,GAAqB,EACxB,MAAA,CAAO;AAAA;AAAA,EAEN,IAAA,EAAM,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtB,KAAA,EAAO,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAEvB,MAAA,EAAQ,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAExB,WAAA,EAAa,CAAA,CAAE,MAAA,EAAO,CAAE,QAAA;AAC1B,CAAC,EACA,KAAA,EAAM;AAET,IAAM,oBAAA,GAAuB,mBAAmB,MAAA,CAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMrD,cAAc,CAAA,CAAE,KAAA,CAAM,EAAE,MAAA,EAAQ,EAAE,QAAA;AACpC,CAAC,CAAA;AAEM,IAAM,oBAAA,GAAuB,EACjC,MAAA,CAAO;AAAA,EACN,MAAA,EAAQ,CAAA,CAAE,OAAA,CAAQ,sBAAsB,CAAA;AAAA,EACxC,YAAY,CAAA,CAAE,KAAA,CAAM,oBAAoB,CAAA,CAAE,OAAA,CAAQ,EAAE,CAAA;AAAA,EACpD,aAAa,CAAA,CAAE,KAAA,CAAM,kBAAkB,CAAA,CAAE,OAAA,CAAQ,EAAE,CAAA;AAAA,EACnD,WAAW,CAAA,CAAE,KAAA,CAAM,kBAAkB,CAAA,CAAE,OAAA,CAAQ,EAAE,CAAA;AAAA,EACjD,aAAa,CAAA,CAAE,KAAA,CAAM,kBAAkB,CAAA,CAAE,OAAA,CAAQ,EAAE;AACrD,CAAC,EACA,KAAA","file":"manifest.mjs","sourcesContent":["/**\n * Plugin manifest — `agentproto/plugin/v1`.\n *\n * Plugins declare what they provide in either:\n * - their `package.json` under the `agentproto` key, OR\n * - a standalone `agentproto.json` next to their `package.json`.\n *\n * The CLI reads this manifest, dynamic-imports each adapter entry,\n * and registers it with the runtime registry. Plugins don't need\n * side-effect imports any more — they just export their factory\n * functions and let the manifest do the wiring.\n *\n * Example (in @guilde/agentproto-bridge's package.json):\n *\n * {\n * \"name\": \"@guilde/agentproto-bridge\",\n * \"agentproto\": {\n * \"schema\": \"agentproto/plugin/v1\",\n * \"substrates\": [\n * {\n * \"kind\": \"guilde-mcp\",\n * \"entry\": \"./dist/index.mjs\",\n * \"export\": \"guildeMcpSubstrateFactory\",\n * \"capabilities\": [\"mentions\", \"reactions\", \"identity\"],\n * \"description\": \"Reads/writes turns through Guilde's MCP server.\"\n * }\n * ],\n * \"executors\": [\n * {\n * \"kind\": \"db-operator\",\n * \"entry\": \"./dist/index.mjs\",\n * \"export\": \"dbOperatorExecutorFactory\",\n * \"description\": \"Delegates to Mastra operators via run_operator.\"\n * }\n * ]\n * }\n * }\n */\n\nimport { z } from \"zod\"\n\nexport const PLUGIN_MANIFEST_SCHEMA = \"agentproto/plugin/v1\" as const\n\nconst AdapterEntrySchema = z\n .object({\n /** The `kind` string the manifest's substrate/dispatcher/etc. block uses. */\n kind: z.string().min(1),\n /**\n * Path to the entry module, relative to the plugin package root.\n * Resolved via the plugin's `package.json#main`/`exports` (i.e. you\n * can use a subpath like `./dist/substrates.mjs` or an export name\n * like `.` if the plugin re-exports everything from its root).\n */\n entry: z.string().min(1),\n /** Named export inside `entry` — the factory function. */\n export: z.string().min(1),\n /** Free-form description; shown by `agentproto plugins show`. */\n description: z.string().optional(),\n })\n .loose()\n\nconst SubstrateEntrySchema = AdapterEntrySchema.extend({\n /**\n * Free-form capability tags. Not gated by the kernel yet, but\n * surfaced by `agentproto plugins show` so users can see what the\n * substrate claims to support (mentions, reactions, visibility, …).\n */\n capabilities: z.array(z.string()).optional(),\n})\n\nexport const PluginManifestSchema = z\n .object({\n schema: z.literal(PLUGIN_MANIFEST_SCHEMA),\n substrates: z.array(SubstrateEntrySchema).default([]),\n dispatchers: z.array(AdapterEntrySchema).default([]),\n executors: z.array(AdapterEntrySchema).default([]),\n stateStores: z.array(AdapterEntrySchema).default([]),\n })\n .loose()\n\nexport type PluginManifest = z.infer<typeof PluginManifestSchema>\nexport type AdapterEntry = z.infer<typeof AdapterEntrySchema>\nexport type SubstrateEntry = z.infer<typeof SubstrateEntrySchema>\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin loader. For each plugin id:
|
|
3
|
+
*
|
|
4
|
+
* 1. Try manifest-based wiring — read the plugin's
|
|
5
|
+
* `package.json#agentproto` (or standalone `agentproto.json`),
|
|
6
|
+
* validate the `agentproto/plugin/v1` schema, dynamic-import
|
|
7
|
+
* each declared adapter entry, register with the runtime
|
|
8
|
+
* registry.
|
|
9
|
+
* 2. Fall back to side-effect import — older plugins that just
|
|
10
|
+
* `registerSubstrate(...)` at module load are still supported.
|
|
11
|
+
*
|
|
12
|
+
* Discovery sources for plugin ids:
|
|
13
|
+
* - `--plugin <module-id>` flag(s) on the verb
|
|
14
|
+
* - `plugins[]` array in `~/.agentproto/config.json`
|
|
15
|
+
*
|
|
16
|
+
* Failures: a plugin that throws on load is reported on stderr and
|
|
17
|
+
* the load skips it. The CLI continues — a single bad plugin
|
|
18
|
+
* shouldn't take down the swarm.
|
|
19
|
+
*/
|
|
20
|
+
declare function loadPluginsFromConfig(): Promise<readonly string[]>;
|
|
21
|
+
declare function loadPlugins(moduleIds: readonly string[]): Promise<void>;
|
|
22
|
+
|
|
23
|
+
export { loadPlugins, loadPluginsFromConfig };
|