@adapt-toolkit/a2adapt 0.2.0 → 0.4.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/dist/cli.js +422 -0
- package/dist/hooks/runner.js +115 -17
- package/dist/index.js +2050 -89
- package/dist/mufl_code/A93F52302D67D1F28269D3F35657610A0FA140AABC521D667A262C101A7AC090.muflo +0 -0
- package/dist/mufl_code/actor.mu +287 -20
- package/dist/mufl_code/config.mufl +20 -17
- package/package.json +25 -6
package/dist/cli.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { connect } from "node:net";
|
|
7
|
+
import { homedir, userInfo } from "node:os";
|
|
8
|
+
import { resolve, join, dirname } from "node:path";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
var STATE_DIR = resolve(process.env.A2ADAPT_STATE_DIR ?? resolve(homedir(), ".a2adapt"));
|
|
12
|
+
var PORT = parseInt(process.env.A2ADAPT_PORT ?? "3030", 10);
|
|
13
|
+
var BROKER_URL = process.env.A2ADAPT_BROKER_URL ?? "wss://a2adapt.adaptframework.solutions/broker";
|
|
14
|
+
var PID_PATH = join(STATE_DIR, "daemon.pid");
|
|
15
|
+
var LOG_PATH = join(STATE_DIR, "daemon.log");
|
|
16
|
+
var SELF = fileURLToPath(import.meta.url);
|
|
17
|
+
var out = (...p) => process.stdout.write(`${p.join(" ")}
|
|
18
|
+
`);
|
|
19
|
+
var err = (...p) => process.stderr.write(`${p.join(" ")}
|
|
20
|
+
`);
|
|
21
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
22
|
+
function readPid() {
|
|
23
|
+
try {
|
|
24
|
+
const n = parseInt(fs.readFileSync(PID_PATH, "utf8").trim(), 10);
|
|
25
|
+
return Number.isFinite(n) ? n : null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isAlive(pid) {
|
|
31
|
+
try {
|
|
32
|
+
process.kill(pid, 0);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function runningPid() {
|
|
39
|
+
const pid = readPid();
|
|
40
|
+
if (pid && isAlive(pid)) return pid;
|
|
41
|
+
if (pid) {
|
|
42
|
+
try {
|
|
43
|
+
fs.rmSync(PID_PATH, { force: true });
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function portOpen(port, timeoutMs = 1e3) {
|
|
50
|
+
return new Promise((res) => {
|
|
51
|
+
const sock = connect({ host: "127.0.0.1", port });
|
|
52
|
+
const done = (ok) => {
|
|
53
|
+
sock.destroy();
|
|
54
|
+
res(ok);
|
|
55
|
+
};
|
|
56
|
+
sock.setTimeout(timeoutMs);
|
|
57
|
+
sock.once("connect", () => done(true));
|
|
58
|
+
sock.once("timeout", () => done(false));
|
|
59
|
+
sock.once("error", () => done(false));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function waitForPort(port, totalMs = 3e4) {
|
|
63
|
+
const deadline = Date.now() + totalMs;
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
if (await portOpen(port)) return true;
|
|
66
|
+
await sleep(400);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
async function cmdStart() {
|
|
71
|
+
const existing = runningPid();
|
|
72
|
+
if (existing) {
|
|
73
|
+
out(`a2adapt-mcp is already running (pid ${existing}, port ${PORT}).`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
77
|
+
const logFd = fs.openSync(LOG_PATH, "a");
|
|
78
|
+
const child = spawn(process.execPath, [SELF, "serve"], {
|
|
79
|
+
detached: true,
|
|
80
|
+
stdio: ["ignore", logFd, logFd],
|
|
81
|
+
env: {
|
|
82
|
+
...process.env,
|
|
83
|
+
A2ADAPT_TRANSPORT: "http",
|
|
84
|
+
A2ADAPT_PORT: String(PORT),
|
|
85
|
+
A2ADAPT_BROKER_URL: BROKER_URL,
|
|
86
|
+
A2ADAPT_STATE_DIR: STATE_DIR
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
child.unref();
|
|
90
|
+
if (!child.pid) {
|
|
91
|
+
err("failed to spawn the daemon.");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
fs.writeFileSync(PID_PATH, String(child.pid));
|
|
95
|
+
out(`starting a2adapt-mcp (pid ${child.pid})\u2026`);
|
|
96
|
+
const ready = await waitForPort(PORT);
|
|
97
|
+
if (ready) {
|
|
98
|
+
out(`a2adapt-mcp is up on http://localhost:${PORT}/mcp`);
|
|
99
|
+
out(` broker: ${BROKER_URL}`);
|
|
100
|
+
out(` state: ${STATE_DIR}`);
|
|
101
|
+
out(` logs: ${LOG_PATH}`);
|
|
102
|
+
} else {
|
|
103
|
+
err(`daemon started (pid ${child.pid}) but port ${PORT} did not open within 30s \u2014 check ${LOG_PATH}.`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function cmdStop() {
|
|
108
|
+
const pid = runningPid();
|
|
109
|
+
if (!pid) {
|
|
110
|
+
out("a2adapt-mcp is not running.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
out(`stopping a2adapt-mcp (pid ${pid})\u2026`);
|
|
114
|
+
try {
|
|
115
|
+
process.kill(pid, "SIGTERM");
|
|
116
|
+
} catch (e) {
|
|
117
|
+
err(`failed to signal pid ${pid}: ${String(e)}`);
|
|
118
|
+
}
|
|
119
|
+
for (let i = 0; i < 25 && isAlive(pid); i++) await sleep(200);
|
|
120
|
+
if (isAlive(pid)) {
|
|
121
|
+
err(`pid ${pid} did not exit; sending SIGKILL.`);
|
|
122
|
+
try {
|
|
123
|
+
process.kill(pid, "SIGKILL");
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
fs.rmSync(PID_PATH, { force: true });
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
out("stopped.");
|
|
132
|
+
}
|
|
133
|
+
async function cmdStatus() {
|
|
134
|
+
const pid = runningPid();
|
|
135
|
+
if (!pid) {
|
|
136
|
+
if (await portOpen(PORT)) {
|
|
137
|
+
out("a2adapt-mcp: running (no pidfile \u2014 likely a stale process or external launcher)");
|
|
138
|
+
out(` url: http://localhost:${PORT}/mcp (reachable)`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
out("a2adapt-mcp: stopped");
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const up = await portOpen(PORT);
|
|
146
|
+
out("a2adapt-mcp: running");
|
|
147
|
+
out(` pid: ${pid}`);
|
|
148
|
+
out(` url: http://localhost:${PORT}/mcp ${up ? "(reachable)" : "(port not answering!)"}`);
|
|
149
|
+
out(` broker: ${BROKER_URL}`);
|
|
150
|
+
out(` state: ${STATE_DIR}`);
|
|
151
|
+
out(` logs: ${LOG_PATH}`);
|
|
152
|
+
}
|
|
153
|
+
function cmdWatch(which) {
|
|
154
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
155
|
+
const scan = (initial) => {
|
|
156
|
+
let names;
|
|
157
|
+
try {
|
|
158
|
+
names = fs.readdirSync(STATE_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
159
|
+
} catch {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
for (const name of names) {
|
|
163
|
+
if (which && name !== which) continue;
|
|
164
|
+
const logPath = join(STATE_DIR, name, "inbox.log");
|
|
165
|
+
let size;
|
|
166
|
+
try {
|
|
167
|
+
size = fs.statSync(logPath).size;
|
|
168
|
+
} catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
let seen = offsets.get(logPath);
|
|
172
|
+
if (seen === void 0) {
|
|
173
|
+
seen = initial ? size : 0;
|
|
174
|
+
offsets.set(logPath, seen);
|
|
175
|
+
if (initial) continue;
|
|
176
|
+
}
|
|
177
|
+
if (size <= seen) {
|
|
178
|
+
if (size < seen) offsets.set(logPath, size);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
let chunk;
|
|
182
|
+
try {
|
|
183
|
+
const fd = fs.openSync(logPath, "r");
|
|
184
|
+
const buf = Buffer.alloc(size - seen);
|
|
185
|
+
fs.readSync(fd, buf, 0, buf.length, seen);
|
|
186
|
+
fs.closeSync(fd);
|
|
187
|
+
chunk = buf.toString("utf8");
|
|
188
|
+
} catch {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
offsets.set(logPath, size);
|
|
192
|
+
for (const line of chunk.split("\n")) {
|
|
193
|
+
if (!line.trim()) continue;
|
|
194
|
+
let msg;
|
|
195
|
+
try {
|
|
196
|
+
msg = JSON.parse(line);
|
|
197
|
+
} catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
out(
|
|
201
|
+
`[${name}] new message from ${msg.sender ?? "?"}: ${msg.text ?? ""}` + (msg.date ? ` (${msg.date})` : "")
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
err(
|
|
207
|
+
`a2adapt-mcp watch: watching ${which ? `identity "${which}"` : "all identities"} under ${STATE_DIR} (Ctrl-C to stop)`
|
|
208
|
+
);
|
|
209
|
+
scan(true);
|
|
210
|
+
const timer = setInterval(() => scan(false), 1e3);
|
|
211
|
+
const stop = () => {
|
|
212
|
+
clearInterval(timer);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
};
|
|
215
|
+
process.on("SIGINT", stop);
|
|
216
|
+
process.on("SIGTERM", stop);
|
|
217
|
+
}
|
|
218
|
+
var SYSTEMD_UNIT = "a2adapt.service";
|
|
219
|
+
var LAUNCHD_LABEL = "solutions.adaptframework.a2adapt";
|
|
220
|
+
function systemdUnitPath() {
|
|
221
|
+
return join(homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
222
|
+
}
|
|
223
|
+
function launchdPlistPath() {
|
|
224
|
+
return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
225
|
+
}
|
|
226
|
+
function run(cmd, args) {
|
|
227
|
+
const r = spawnSync(cmd, args, { stdio: "inherit" });
|
|
228
|
+
return r.status === 0;
|
|
229
|
+
}
|
|
230
|
+
function installSystemd() {
|
|
231
|
+
const unitPath = systemdUnitPath();
|
|
232
|
+
fs.mkdirSync(dirname(unitPath), { recursive: true });
|
|
233
|
+
const unit = `[Unit]
|
|
234
|
+
Description=a2adapt MCP daemon (secure agent-to-agent messaging over ADAPT)
|
|
235
|
+
After=network-online.target
|
|
236
|
+
Wants=network-online.target
|
|
237
|
+
|
|
238
|
+
[Service]
|
|
239
|
+
Type=simple
|
|
240
|
+
ExecStart=${process.execPath} ${SELF} serve
|
|
241
|
+
Environment=A2ADAPT_TRANSPORT=http
|
|
242
|
+
Environment=A2ADAPT_PORT=${PORT}
|
|
243
|
+
Environment=A2ADAPT_BROKER_URL=${BROKER_URL}
|
|
244
|
+
Environment=A2ADAPT_STATE_DIR=${STATE_DIR}
|
|
245
|
+
Restart=on-failure
|
|
246
|
+
RestartSec=2
|
|
247
|
+
|
|
248
|
+
[Install]
|
|
249
|
+
WantedBy=default.target
|
|
250
|
+
`;
|
|
251
|
+
fs.writeFileSync(unitPath, unit);
|
|
252
|
+
out(`wrote ${unitPath}`);
|
|
253
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
254
|
+
if (!run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT])) {
|
|
255
|
+
err("failed to enable/start the service via systemctl --user.");
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
if (!run("loginctl", ["enable-linger", userInfo().username])) {
|
|
259
|
+
err("warning: could not enable linger \u2014 the daemon may not start until you log in.");
|
|
260
|
+
err(` run manually: loginctl enable-linger ${userInfo().username}`);
|
|
261
|
+
}
|
|
262
|
+
out("");
|
|
263
|
+
out(`a2adapt-mcp installed as a systemd user service and started.`);
|
|
264
|
+
out(` status: systemctl --user status ${SYSTEMD_UNIT}`);
|
|
265
|
+
out(` logs: journalctl --user -u ${SYSTEMD_UNIT} -f`);
|
|
266
|
+
out(` remove: a2adapt-mcp uninstall-service`);
|
|
267
|
+
}
|
|
268
|
+
function uninstallSystemd() {
|
|
269
|
+
run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
|
|
270
|
+
const unitPath = systemdUnitPath();
|
|
271
|
+
try {
|
|
272
|
+
fs.rmSync(unitPath, { force: true });
|
|
273
|
+
out(`removed ${unitPath}`);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
err(`failed to remove ${unitPath}: ${String(e)}`);
|
|
276
|
+
}
|
|
277
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
278
|
+
out("a2adapt-mcp service uninstalled.");
|
|
279
|
+
}
|
|
280
|
+
function installLaunchd() {
|
|
281
|
+
const plistPath = launchdPlistPath();
|
|
282
|
+
fs.mkdirSync(dirname(plistPath), { recursive: true });
|
|
283
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
284
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
285
|
+
<plist version="1.0">
|
|
286
|
+
<dict>
|
|
287
|
+
<key>Label</key><string>${LAUNCHD_LABEL}</string>
|
|
288
|
+
<key>ProgramArguments</key>
|
|
289
|
+
<array>
|
|
290
|
+
<string>${process.execPath}</string>
|
|
291
|
+
<string>${SELF}</string>
|
|
292
|
+
<string>serve</string>
|
|
293
|
+
</array>
|
|
294
|
+
<key>EnvironmentVariables</key>
|
|
295
|
+
<dict>
|
|
296
|
+
<key>A2ADAPT_TRANSPORT</key><string>http</string>
|
|
297
|
+
<key>A2ADAPT_PORT</key><string>${PORT}</string>
|
|
298
|
+
<key>A2ADAPT_BROKER_URL</key><string>${BROKER_URL}</string>
|
|
299
|
+
<key>A2ADAPT_STATE_DIR</key><string>${STATE_DIR}</string>
|
|
300
|
+
</dict>
|
|
301
|
+
<key>RunAtLoad</key><true/>
|
|
302
|
+
<key>KeepAlive</key><true/>
|
|
303
|
+
<key>StandardOutPath</key><string>${LOG_PATH}</string>
|
|
304
|
+
<key>StandardErrorPath</key><string>${LOG_PATH}</string>
|
|
305
|
+
</dict>
|
|
306
|
+
</plist>
|
|
307
|
+
`;
|
|
308
|
+
fs.writeFileSync(plistPath, plist);
|
|
309
|
+
out(`wrote ${plistPath}`);
|
|
310
|
+
run("launchctl", ["unload", plistPath]);
|
|
311
|
+
if (!run("launchctl", ["load", "-w", plistPath])) {
|
|
312
|
+
err("failed to load the launchd agent.");
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
out("");
|
|
316
|
+
out("a2adapt-mcp installed as a launchd agent and started.");
|
|
317
|
+
out(` remove: a2adapt-mcp uninstall-service`);
|
|
318
|
+
}
|
|
319
|
+
function uninstallLaunchd() {
|
|
320
|
+
const plistPath = launchdPlistPath();
|
|
321
|
+
run("launchctl", ["unload", plistPath]);
|
|
322
|
+
try {
|
|
323
|
+
fs.rmSync(plistPath, { force: true });
|
|
324
|
+
out(`removed ${plistPath}`);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
err(`failed to remove ${plistPath}: ${String(e)}`);
|
|
327
|
+
}
|
|
328
|
+
out("a2adapt-mcp service uninstalled.");
|
|
329
|
+
}
|
|
330
|
+
async function cmdInstallService() {
|
|
331
|
+
await cmdStop();
|
|
332
|
+
if (process.platform === "linux") return installSystemd();
|
|
333
|
+
if (process.platform === "darwin") return installLaunchd();
|
|
334
|
+
err(`install-service: unsupported platform "${process.platform}" (only linux/systemd and macOS/launchd).`);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
function cmdUninstallService() {
|
|
338
|
+
if (process.platform === "linux") return uninstallSystemd();
|
|
339
|
+
if (process.platform === "darwin") return uninstallLaunchd();
|
|
340
|
+
err(`uninstall-service: unsupported platform "${process.platform}".`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
function usage() {
|
|
344
|
+
out("a2adapt-mcp \u2014 daemon for the a2adapt MCP server");
|
|
345
|
+
out("");
|
|
346
|
+
out("Usage: a2adapt-mcp <command>");
|
|
347
|
+
out(" start start the daemon in the background");
|
|
348
|
+
out(" stop stop the running daemon");
|
|
349
|
+
out(" restart stop then start");
|
|
350
|
+
out(" status show whether the daemon is running");
|
|
351
|
+
out(" serve run in the foreground (used by start; handy for debugging)");
|
|
352
|
+
out(" watch [identity] stream one line per new inbound message (wake source for a Monitor)");
|
|
353
|
+
out("");
|
|
354
|
+
out(" install-service install + start a boot-persistent service (systemd/launchd)");
|
|
355
|
+
out(" uninstall-service stop + remove that service");
|
|
356
|
+
out("");
|
|
357
|
+
out("Config (env): A2ADAPT_BROKER_URL, A2ADAPT_PORT (3030), A2ADAPT_STATE_DIR (~/.a2adapt)");
|
|
358
|
+
out("(install-service bakes the current config values into the service definition.)");
|
|
359
|
+
}
|
|
360
|
+
async function main() {
|
|
361
|
+
const cmd = process.argv[2] ?? "help";
|
|
362
|
+
switch (cmd) {
|
|
363
|
+
case "serve":
|
|
364
|
+
case "run":
|
|
365
|
+
if (!process.env.A2ADAPT_TRANSPORT) process.env.A2ADAPT_TRANSPORT = "http";
|
|
366
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
367
|
+
fs.writeFileSync(PID_PATH, String(process.pid));
|
|
368
|
+
{
|
|
369
|
+
const cleanup = () => {
|
|
370
|
+
try {
|
|
371
|
+
fs.rmSync(PID_PATH, { force: true });
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
process.on("exit", cleanup);
|
|
376
|
+
for (const sig of ["SIGTERM", "SIGINT"]) {
|
|
377
|
+
process.on(sig, () => {
|
|
378
|
+
cleanup();
|
|
379
|
+
process.exit(0);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
await import(pathToFileURL(join(dirname(SELF), "index.js")).href);
|
|
384
|
+
break;
|
|
385
|
+
case "start":
|
|
386
|
+
await cmdStart();
|
|
387
|
+
break;
|
|
388
|
+
case "stop":
|
|
389
|
+
await cmdStop();
|
|
390
|
+
break;
|
|
391
|
+
case "restart":
|
|
392
|
+
await cmdStop();
|
|
393
|
+
await cmdStart();
|
|
394
|
+
break;
|
|
395
|
+
case "status":
|
|
396
|
+
await cmdStatus();
|
|
397
|
+
break;
|
|
398
|
+
case "watch":
|
|
399
|
+
cmdWatch(process.argv[3]);
|
|
400
|
+
break;
|
|
401
|
+
case "install-service":
|
|
402
|
+
await cmdInstallService();
|
|
403
|
+
break;
|
|
404
|
+
case "uninstall-service":
|
|
405
|
+
cmdUninstallService();
|
|
406
|
+
break;
|
|
407
|
+
case "help":
|
|
408
|
+
case "--help":
|
|
409
|
+
case "-h":
|
|
410
|
+
usage();
|
|
411
|
+
break;
|
|
412
|
+
default:
|
|
413
|
+
err(`unknown command: ${cmd}
|
|
414
|
+
`);
|
|
415
|
+
usage();
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
main().catch((e) => {
|
|
420
|
+
err(`a2adapt-mcp error: ${e?.stack ?? e}`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
});
|
package/dist/hooks/runner.js
CHANGED
|
@@ -2,24 +2,122 @@
|
|
|
2
2
|
import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/hooks/runner.ts
|
|
5
|
-
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { resolve, join } from "node:path";
|
|
8
|
+
var STATE_DIR = resolve(
|
|
9
|
+
process.env.A2ADAPT_STATE_DIR ?? resolve(homedir(), ".a2adapt")
|
|
10
|
+
);
|
|
11
|
+
function readStdin() {
|
|
12
|
+
try {
|
|
13
|
+
return fs.readFileSync(0, "utf8");
|
|
14
|
+
} catch {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function emit(payload) {
|
|
19
|
+
process.stdout.write(JSON.stringify(payload));
|
|
6
20
|
}
|
|
7
|
-
|
|
21
|
+
function noop() {
|
|
22
|
+
emit({ continue: true });
|
|
8
23
|
}
|
|
9
|
-
|
|
24
|
+
function readCursor(dir) {
|
|
25
|
+
try {
|
|
26
|
+
return parseInt(fs.readFileSync(join(dir, "inbox_cursor"), "utf8").trim(), 10) || 0;
|
|
27
|
+
} catch {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function readInboxLog(dir) {
|
|
32
|
+
let raw;
|
|
33
|
+
try {
|
|
34
|
+
raw = fs.readFileSync(join(dir, "inbox.log"), "utf8");
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const msgs = [];
|
|
39
|
+
for (const line of raw.split("\n")) {
|
|
40
|
+
if (!line.trim()) continue;
|
|
41
|
+
try {
|
|
42
|
+
const m = JSON.parse(line);
|
|
43
|
+
msgs.push({
|
|
44
|
+
sender: String(m.sender ?? "?"),
|
|
45
|
+
text: String(m.text ?? ""),
|
|
46
|
+
date: String(m.date ?? "")
|
|
47
|
+
});
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return msgs;
|
|
52
|
+
}
|
|
53
|
+
function collectUnread() {
|
|
54
|
+
let names;
|
|
55
|
+
try {
|
|
56
|
+
names = fs.readdirSync(STATE_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const name of names) {
|
|
62
|
+
const dir = join(STATE_DIR, name);
|
|
63
|
+
const all = readInboxLog(dir);
|
|
64
|
+
if (all.length === 0) continue;
|
|
65
|
+
const unread = all.slice(readCursor(dir));
|
|
66
|
+
if (unread.length === 0) continue;
|
|
67
|
+
out.push({ name, count: unread.length, messages: unread });
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function renderContext(unread) {
|
|
72
|
+
const total = unread.reduce((n, u) => n + u.count, 0);
|
|
73
|
+
const lines = [];
|
|
74
|
+
for (const u of unread) {
|
|
75
|
+
lines.push(`\u2022 ${u.name} \u2014 ${u.count} unread:`);
|
|
76
|
+
for (const m of u.messages.slice(-5)) {
|
|
77
|
+
lines.push(` [${m.sender}] ${m.text}${m.date ? ` (${m.date})` : ""}`);
|
|
78
|
+
}
|
|
79
|
+
if (u.count > 5) lines.push(` \u2026and ${u.count - 5} earlier`);
|
|
80
|
+
}
|
|
81
|
+
return `a2adapt \u2014 ${total} unread message(s) across ${unread.length} identit${unread.length === 1 ? "y" : "ies"} (arrived while you were away):
|
|
82
|
+
${lines.join("\n")}
|
|
83
|
+
|
|
84
|
+
To read & clear: choose_identity({ name }) then process_incoming_message(). To wait for live replies, arm a Monitor on the wake source \`a2adapt-mcp watch\` (each new-mail line wakes you).`;
|
|
85
|
+
}
|
|
86
|
+
function sessionStart() {
|
|
87
|
+
const raw = readStdin();
|
|
88
|
+
let source = "";
|
|
89
|
+
if (raw) {
|
|
90
|
+
try {
|
|
91
|
+
source = JSON.parse(raw).source ?? "";
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (source === "compact") return noop();
|
|
96
|
+
const unread = collectUnread();
|
|
97
|
+
if (unread.length === 0) return noop();
|
|
98
|
+
emit({
|
|
99
|
+
continue: true,
|
|
100
|
+
hookSpecificOutput: {
|
|
101
|
+
hookEventName: "SessionStart",
|
|
102
|
+
additionalContext: renderContext(unread)
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function main() {
|
|
10
107
|
const kind = process.argv[2] ?? "";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
main().catch((err) => {
|
|
23
|
-
process.stderr.write(`a2adapt hook: ${err?.stack ?? err}
|
|
108
|
+
try {
|
|
109
|
+
switch (kind) {
|
|
110
|
+
case "session-start":
|
|
111
|
+
sessionStart();
|
|
112
|
+
return;
|
|
113
|
+
default:
|
|
114
|
+
noop();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
process.stderr.write(`a2adapt hook: ${err?.stack ?? err}
|
|
24
119
|
`);
|
|
25
|
-
|
|
120
|
+
noop();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
main();
|