@adapt-toolkit/a2adapt 0.5.1 → 0.7.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 +163 -33
- package/dist/hooks/runner.js +43 -4
- package/dist/index.js +163 -77
- package/dist/mufl_code/BA1E7E4B9D350E98D474F122789C2E5B6A187C7CFE493318E109A469CE2E2D62.muflo +0 -0
- package/dist/mufl_code/actor.mu +94 -39
- package/package.json +1 -1
- package/dist/mufl_code/CBC04E71F18E1EC2829E7645BB7F22BBBEEB775C359C062703B7A6FCFB60CFE2.muflo +0 -0
package/dist/cli.js
CHANGED
|
@@ -4,15 +4,80 @@ import { createRequire } from 'node:module'; const require = createRequire(impor
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { connect } from "node:net";
|
|
7
|
-
import { homedir, userInfo } from "node:os";
|
|
8
|
-
import { resolve, join, dirname } from "node:path";
|
|
7
|
+
import { homedir as homedir2, userInfo } from "node:os";
|
|
8
|
+
import { resolve as resolve2, join as join2, dirname as dirname2 } from "node:path";
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
import { createInterface } from "node:readline/promises";
|
|
11
|
+
import * as fs2 from "node:fs";
|
|
12
|
+
|
|
13
|
+
// src/config.ts
|
|
10
14
|
import * as fs from "node:fs";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { resolve, join, dirname } from "node:path";
|
|
17
|
+
var DEFAULT_CONFIG = {
|
|
18
|
+
brokerUrl: "ws://a2adapt.adaptframework.solutions/broker",
|
|
19
|
+
port: 3030,
|
|
20
|
+
stateDir: resolve(homedir(), ".a2adapt"),
|
|
21
|
+
gcIntervalMs: 36e5
|
|
22
|
+
};
|
|
23
|
+
function configPath() {
|
|
24
|
+
return process.env.A2ADAPT_CONFIG ?? join(homedir(), ".a2adapt", "config.json");
|
|
25
|
+
}
|
|
26
|
+
function readFileConfig() {
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = fs.readFileSync(configPath(), "utf8");
|
|
30
|
+
} catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(raw);
|
|
36
|
+
} catch {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
const out2 = {};
|
|
40
|
+
if (typeof parsed.brokerUrl === "string") out2.brokerUrl = parsed.brokerUrl;
|
|
41
|
+
if (typeof parsed.port === "number" && Number.isFinite(parsed.port)) out2.port = parsed.port;
|
|
42
|
+
if (typeof parsed.stateDir === "string") out2.stateDir = resolve(parsed.stateDir);
|
|
43
|
+
if (typeof parsed.gcIntervalMs === "number" && Number.isFinite(parsed.gcIntervalMs)) {
|
|
44
|
+
out2.gcIntervalMs = parsed.gcIntervalMs;
|
|
45
|
+
}
|
|
46
|
+
return out2;
|
|
47
|
+
}
|
|
48
|
+
function envInt(name) {
|
|
49
|
+
const v = process.env[name];
|
|
50
|
+
if (v === void 0) return void 0;
|
|
51
|
+
const n = parseInt(v, 10);
|
|
52
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
53
|
+
}
|
|
54
|
+
function loadConfig() {
|
|
55
|
+
const file = readFileConfig();
|
|
56
|
+
return {
|
|
57
|
+
brokerUrl: process.env.A2ADAPT_BROKER_URL ?? file.brokerUrl ?? DEFAULT_CONFIG.brokerUrl,
|
|
58
|
+
port: envInt("A2ADAPT_PORT") ?? file.port ?? DEFAULT_CONFIG.port,
|
|
59
|
+
stateDir: resolve(process.env.A2ADAPT_STATE_DIR ?? file.stateDir ?? DEFAULT_CONFIG.stateDir),
|
|
60
|
+
gcIntervalMs: envInt("A2ADAPT_GC_INTERVAL_MS") ?? file.gcIntervalMs ?? DEFAULT_CONFIG.gcIntervalMs
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function writeConfig(cfg) {
|
|
64
|
+
const path = configPath();
|
|
65
|
+
fs.mkdirSync(dirname(path), { recursive: true });
|
|
66
|
+
fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
67
|
+
try {
|
|
68
|
+
fs.chmodSync(path, 384);
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
return path;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/cli.ts
|
|
75
|
+
var CONFIG = loadConfig();
|
|
76
|
+
var STATE_DIR = CONFIG.stateDir;
|
|
77
|
+
var PORT = CONFIG.port;
|
|
78
|
+
var BROKER_URL = CONFIG.brokerUrl;
|
|
79
|
+
var PID_PATH = join2(STATE_DIR, "daemon.pid");
|
|
80
|
+
var LOG_PATH = join2(STATE_DIR, "daemon.log");
|
|
16
81
|
var SELF = fileURLToPath(import.meta.url);
|
|
17
82
|
var out = (...p) => process.stdout.write(`${p.join(" ")}
|
|
18
83
|
`);
|
|
@@ -21,7 +86,7 @@ var err = (...p) => process.stderr.write(`${p.join(" ")}
|
|
|
21
86
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
22
87
|
function readPid() {
|
|
23
88
|
try {
|
|
24
|
-
const n = parseInt(
|
|
89
|
+
const n = parseInt(fs2.readFileSync(PID_PATH, "utf8").trim(), 10);
|
|
25
90
|
return Number.isFinite(n) ? n : null;
|
|
26
91
|
} catch {
|
|
27
92
|
return null;
|
|
@@ -40,7 +105,7 @@ function runningPid() {
|
|
|
40
105
|
if (pid && isAlive(pid)) return pid;
|
|
41
106
|
if (pid) {
|
|
42
107
|
try {
|
|
43
|
-
|
|
108
|
+
fs2.rmSync(PID_PATH, { force: true });
|
|
44
109
|
} catch {
|
|
45
110
|
}
|
|
46
111
|
}
|
|
@@ -73,8 +138,8 @@ async function cmdStart() {
|
|
|
73
138
|
out(`a2adapt-mcp is already running (pid ${existing}, port ${PORT}).`);
|
|
74
139
|
return;
|
|
75
140
|
}
|
|
76
|
-
|
|
77
|
-
const logFd =
|
|
141
|
+
fs2.mkdirSync(STATE_DIR, { recursive: true });
|
|
142
|
+
const logFd = fs2.openSync(LOG_PATH, "a");
|
|
78
143
|
const child = spawn(process.execPath, [SELF, "serve"], {
|
|
79
144
|
detached: true,
|
|
80
145
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -91,7 +156,7 @@ async function cmdStart() {
|
|
|
91
156
|
err("failed to spawn the daemon.");
|
|
92
157
|
process.exit(1);
|
|
93
158
|
}
|
|
94
|
-
|
|
159
|
+
fs2.writeFileSync(PID_PATH, String(child.pid));
|
|
95
160
|
out(`starting a2adapt-mcp (pid ${child.pid})\u2026`);
|
|
96
161
|
const ready = await waitForPort(PORT);
|
|
97
162
|
if (ready) {
|
|
@@ -125,7 +190,7 @@ async function cmdStop() {
|
|
|
125
190
|
}
|
|
126
191
|
}
|
|
127
192
|
try {
|
|
128
|
-
|
|
193
|
+
fs2.rmSync(PID_PATH, { force: true });
|
|
129
194
|
} catch {
|
|
130
195
|
}
|
|
131
196
|
out("stopped.");
|
|
@@ -150,21 +215,80 @@ async function cmdStatus() {
|
|
|
150
215
|
out(` state: ${STATE_DIR}`);
|
|
151
216
|
out(` logs: ${LOG_PATH}`);
|
|
152
217
|
}
|
|
218
|
+
async function cmdSetup() {
|
|
219
|
+
const current = loadConfig();
|
|
220
|
+
const path = configPath();
|
|
221
|
+
out(`a2adapt-mcp setup \u2014 ${path}`);
|
|
222
|
+
out("Enter a value, or press Enter to keep the current [bracketed] one.");
|
|
223
|
+
out("");
|
|
224
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
225
|
+
let next;
|
|
226
|
+
try {
|
|
227
|
+
const ask = async (label, cur) => {
|
|
228
|
+
const ans = (await rl.question(` ${label} [${cur}]: `)).trim();
|
|
229
|
+
return ans === "" ? cur : ans;
|
|
230
|
+
};
|
|
231
|
+
const brokerUrl = await ask("broker URL", current.brokerUrl);
|
|
232
|
+
const portStr = await ask("HTTP port", String(current.port));
|
|
233
|
+
const stateDir = await ask("state dir", current.stateDir);
|
|
234
|
+
const gcStr = await ask("GC interval (ms)", String(current.gcIntervalMs));
|
|
235
|
+
const port = parseInt(portStr, 10);
|
|
236
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
237
|
+
err(`invalid port: ${portStr}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
const gcIntervalMs = parseInt(gcStr, 10);
|
|
241
|
+
if (!Number.isFinite(gcIntervalMs) || gcIntervalMs <= 0) {
|
|
242
|
+
err(`invalid GC interval: ${gcStr}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
next = { brokerUrl, port, stateDir: resolve2(stateDir), gcIntervalMs };
|
|
246
|
+
} finally {
|
|
247
|
+
rl.close();
|
|
248
|
+
}
|
|
249
|
+
writeConfig(next);
|
|
250
|
+
out("");
|
|
251
|
+
out(`wrote ${path} (mode 0600):`);
|
|
252
|
+
out(JSON.stringify(next, null, 2));
|
|
253
|
+
const shadowed = ["A2ADAPT_BROKER_URL", "A2ADAPT_PORT", "A2ADAPT_STATE_DIR", "A2ADAPT_GC_INTERVAL_MS"].filter((k) => process.env[k] !== void 0);
|
|
254
|
+
if (shadowed.length) {
|
|
255
|
+
out("");
|
|
256
|
+
out(`note: these env vars are set and OVERRIDE the file at runtime: ${shadowed.join(", ")}`);
|
|
257
|
+
}
|
|
258
|
+
const pid = runningPid();
|
|
259
|
+
if (!pid) return;
|
|
260
|
+
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
261
|
+
let restart = false;
|
|
262
|
+
try {
|
|
263
|
+
const ans = (await rl2.question(`
|
|
264
|
+
daemon is running (pid ${pid}); restart now to apply? [y/N]: `)).trim().toLowerCase();
|
|
265
|
+
restart = ans === "y" || ans === "yes";
|
|
266
|
+
} finally {
|
|
267
|
+
rl2.close();
|
|
268
|
+
}
|
|
269
|
+
if (!restart) {
|
|
270
|
+
out("not restarting \u2014 changes apply on the next `a2adapt-mcp restart`.");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
await cmdStop();
|
|
274
|
+
const r = spawnSync(process.execPath, [SELF, "start"], { stdio: "inherit" });
|
|
275
|
+
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
276
|
+
}
|
|
153
277
|
function cmdWatch(which) {
|
|
154
278
|
const offsets = /* @__PURE__ */ new Map();
|
|
155
279
|
const scan = (initial) => {
|
|
156
280
|
let names;
|
|
157
281
|
try {
|
|
158
|
-
names =
|
|
282
|
+
names = fs2.readdirSync(STATE_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
159
283
|
} catch {
|
|
160
284
|
return;
|
|
161
285
|
}
|
|
162
286
|
for (const name of names) {
|
|
163
287
|
if (which && name !== which) continue;
|
|
164
|
-
const logPath =
|
|
288
|
+
const logPath = join2(STATE_DIR, name, "notifications.log");
|
|
165
289
|
let size;
|
|
166
290
|
try {
|
|
167
|
-
size =
|
|
291
|
+
size = fs2.statSync(logPath).size;
|
|
168
292
|
} catch {
|
|
169
293
|
continue;
|
|
170
294
|
}
|
|
@@ -180,10 +304,10 @@ function cmdWatch(which) {
|
|
|
180
304
|
}
|
|
181
305
|
let chunk;
|
|
182
306
|
try {
|
|
183
|
-
const fd =
|
|
307
|
+
const fd = fs2.openSync(logPath, "r");
|
|
184
308
|
const buf = Buffer.alloc(size - seen);
|
|
185
|
-
|
|
186
|
-
|
|
309
|
+
fs2.readSync(fd, buf, 0, buf.length, seen);
|
|
310
|
+
fs2.closeSync(fd);
|
|
187
311
|
chunk = buf.toString("utf8");
|
|
188
312
|
} catch {
|
|
189
313
|
continue;
|
|
@@ -218,10 +342,10 @@ function cmdWatch(which) {
|
|
|
218
342
|
var SYSTEMD_UNIT = "a2adapt.service";
|
|
219
343
|
var LAUNCHD_LABEL = "solutions.adaptframework.a2adapt";
|
|
220
344
|
function systemdUnitPath() {
|
|
221
|
-
return
|
|
345
|
+
return join2(homedir2(), ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
222
346
|
}
|
|
223
347
|
function launchdPlistPath() {
|
|
224
|
-
return
|
|
348
|
+
return join2(homedir2(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
225
349
|
}
|
|
226
350
|
function run(cmd, args) {
|
|
227
351
|
const r = spawnSync(cmd, args, { stdio: "inherit" });
|
|
@@ -229,7 +353,7 @@ function run(cmd, args) {
|
|
|
229
353
|
}
|
|
230
354
|
function installSystemd() {
|
|
231
355
|
const unitPath = systemdUnitPath();
|
|
232
|
-
|
|
356
|
+
fs2.mkdirSync(dirname2(unitPath), { recursive: true });
|
|
233
357
|
const unit = `[Unit]
|
|
234
358
|
Description=a2adapt MCP daemon (secure agent-to-agent messaging over ADAPT)
|
|
235
359
|
After=network-online.target
|
|
@@ -248,7 +372,7 @@ RestartSec=2
|
|
|
248
372
|
[Install]
|
|
249
373
|
WantedBy=default.target
|
|
250
374
|
`;
|
|
251
|
-
|
|
375
|
+
fs2.writeFileSync(unitPath, unit);
|
|
252
376
|
out(`wrote ${unitPath}`);
|
|
253
377
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
254
378
|
if (!run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT])) {
|
|
@@ -269,7 +393,7 @@ function uninstallSystemd() {
|
|
|
269
393
|
run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
|
|
270
394
|
const unitPath = systemdUnitPath();
|
|
271
395
|
try {
|
|
272
|
-
|
|
396
|
+
fs2.rmSync(unitPath, { force: true });
|
|
273
397
|
out(`removed ${unitPath}`);
|
|
274
398
|
} catch (e) {
|
|
275
399
|
err(`failed to remove ${unitPath}: ${String(e)}`);
|
|
@@ -279,7 +403,7 @@ function uninstallSystemd() {
|
|
|
279
403
|
}
|
|
280
404
|
function installLaunchd() {
|
|
281
405
|
const plistPath = launchdPlistPath();
|
|
282
|
-
|
|
406
|
+
fs2.mkdirSync(dirname2(plistPath), { recursive: true });
|
|
283
407
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
284
408
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
285
409
|
<plist version="1.0">
|
|
@@ -305,7 +429,7 @@ function installLaunchd() {
|
|
|
305
429
|
</dict>
|
|
306
430
|
</plist>
|
|
307
431
|
`;
|
|
308
|
-
|
|
432
|
+
fs2.writeFileSync(plistPath, plist);
|
|
309
433
|
out(`wrote ${plistPath}`);
|
|
310
434
|
run("launchctl", ["unload", plistPath]);
|
|
311
435
|
if (!run("launchctl", ["load", "-w", plistPath])) {
|
|
@@ -320,7 +444,7 @@ function uninstallLaunchd() {
|
|
|
320
444
|
const plistPath = launchdPlistPath();
|
|
321
445
|
run("launchctl", ["unload", plistPath]);
|
|
322
446
|
try {
|
|
323
|
-
|
|
447
|
+
fs2.rmSync(plistPath, { force: true });
|
|
324
448
|
out(`removed ${plistPath}`);
|
|
325
449
|
} catch (e) {
|
|
326
450
|
err(`failed to remove ${plistPath}: ${String(e)}`);
|
|
@@ -348,14 +472,17 @@ function usage() {
|
|
|
348
472
|
out(" stop stop the running daemon");
|
|
349
473
|
out(" restart stop then start");
|
|
350
474
|
out(" status show whether the daemon is running");
|
|
475
|
+
out(" setup interactively edit the config file (broker / port / state dir / gc)");
|
|
351
476
|
out(" serve run in the foreground (used by start; handy for debugging)");
|
|
352
477
|
out(" watch [identity] stream one line per new inbound message (wake source for a Monitor)");
|
|
353
478
|
out("");
|
|
354
479
|
out(" install-service install + start a boot-persistent service (systemd/launchd)");
|
|
355
480
|
out(" uninstall-service stop + remove that service");
|
|
356
481
|
out("");
|
|
357
|
-
out("Config (
|
|
358
|
-
out("
|
|
482
|
+
out("Config precedence (per field): env var > config.json > default.");
|
|
483
|
+
out(" config.json: A2ADAPT_CONFIG, else ~/.a2adapt/config.json \u2014 edit with `setup`.");
|
|
484
|
+
out(" env: A2ADAPT_BROKER_URL, A2ADAPT_PORT (3030), A2ADAPT_STATE_DIR (~/.a2adapt), A2ADAPT_GC_INTERVAL_MS (3600000)");
|
|
485
|
+
out("(install-service bakes the resolved config values into the service definition.)");
|
|
359
486
|
}
|
|
360
487
|
async function main() {
|
|
361
488
|
const cmd = process.argv[2] ?? "help";
|
|
@@ -363,12 +490,12 @@ async function main() {
|
|
|
363
490
|
case "serve":
|
|
364
491
|
case "run":
|
|
365
492
|
if (!process.env.A2ADAPT_TRANSPORT) process.env.A2ADAPT_TRANSPORT = "http";
|
|
366
|
-
|
|
367
|
-
|
|
493
|
+
fs2.mkdirSync(STATE_DIR, { recursive: true });
|
|
494
|
+
fs2.writeFileSync(PID_PATH, String(process.pid));
|
|
368
495
|
{
|
|
369
496
|
const cleanup = () => {
|
|
370
497
|
try {
|
|
371
|
-
|
|
498
|
+
fs2.rmSync(PID_PATH, { force: true });
|
|
372
499
|
} catch {
|
|
373
500
|
}
|
|
374
501
|
};
|
|
@@ -380,7 +507,7 @@ async function main() {
|
|
|
380
507
|
});
|
|
381
508
|
}
|
|
382
509
|
}
|
|
383
|
-
await import(pathToFileURL(
|
|
510
|
+
await import(pathToFileURL(join2(dirname2(SELF), "index.js")).href);
|
|
384
511
|
break;
|
|
385
512
|
case "start":
|
|
386
513
|
await cmdStart();
|
|
@@ -395,6 +522,9 @@ async function main() {
|
|
|
395
522
|
case "status":
|
|
396
523
|
await cmdStatus();
|
|
397
524
|
break;
|
|
525
|
+
case "setup":
|
|
526
|
+
await cmdSetup();
|
|
527
|
+
break;
|
|
398
528
|
case "watch":
|
|
399
529
|
cmdWatch(process.argv[3]);
|
|
400
530
|
break;
|
package/dist/hooks/runner.js
CHANGED
|
@@ -4,10 +4,11 @@ import { createRequire } from 'node:module'; const require = createRequire(impor
|
|
|
4
4
|
// src/hooks/runner.ts
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { resolve, join } from "node:path";
|
|
7
|
+
import { resolve, join, dirname } from "node:path";
|
|
8
8
|
var STATE_DIR = resolve(
|
|
9
9
|
process.env.A2ADAPT_STATE_DIR ?? resolve(homedir(), ".a2adapt")
|
|
10
10
|
);
|
|
11
|
+
var IDENTITY_FILE = ".a2adapt-identity";
|
|
11
12
|
function readStdin() {
|
|
12
13
|
try {
|
|
13
14
|
return fs.readFileSync(0, "utf8");
|
|
@@ -72,23 +73,61 @@ ${lines.join("\n")}
|
|
|
72
73
|
|
|
73
74
|
To read: choose_identity({ name }) then get_messages() (returns the bodies and marks them read). To wait for live replies, arm a Monitor on the per-identity wake source \`a2adapt-mcp watch <name>\` (each new-mail line wakes you).`;
|
|
74
75
|
}
|
|
76
|
+
function findPinnedIdentity(start) {
|
|
77
|
+
let dir = resolve(start);
|
|
78
|
+
for (; ; ) {
|
|
79
|
+
let raw;
|
|
80
|
+
try {
|
|
81
|
+
raw = fs.readFileSync(join(dir, IDENTITY_FILE), "utf8");
|
|
82
|
+
} catch {
|
|
83
|
+
const parent = dirname(dir);
|
|
84
|
+
if (parent === dir) return null;
|
|
85
|
+
dir = parent;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const name = String(JSON.parse(raw).identity ?? "").trim();
|
|
90
|
+
return name || null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function identityExists(name) {
|
|
97
|
+
try {
|
|
98
|
+
return fs.statSync(join(STATE_DIR, name)).isDirectory();
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function renderIdentityDirective(name, exists) {
|
|
104
|
+
const bind = exists ? `call \`choose_identity({ name: "${name}" })\` to bind it to this session` : `it does not exist yet \u2014 call \`create_identity({ name: "${name}" })\` to create and bind it`;
|
|
105
|
+
return `a2adapt \u2014 this workspace is pinned to identity "${name}" (via ${IDENTITY_FILE}). Before other a2adapt work, ${bind}. Then arm a Monitor on the wake source \`a2adapt-mcp watch ${name}\` so new mail wakes you. Do this once, up front; if a different identity is already bound, prefer this pinned one.`;
|
|
106
|
+
}
|
|
75
107
|
function sessionStart() {
|
|
76
108
|
const raw = readStdin();
|
|
77
109
|
let source = "";
|
|
110
|
+
let cwd = process.cwd();
|
|
78
111
|
if (raw) {
|
|
79
112
|
try {
|
|
80
|
-
|
|
113
|
+
const payload = JSON.parse(raw);
|
|
114
|
+
source = payload.source ?? "";
|
|
115
|
+
if (typeof payload.cwd === "string" && payload.cwd) cwd = payload.cwd;
|
|
81
116
|
} catch {
|
|
82
117
|
}
|
|
83
118
|
}
|
|
84
119
|
if (source === "compact") return noop();
|
|
120
|
+
const pinned = findPinnedIdentity(cwd);
|
|
85
121
|
const unread = collectUnread();
|
|
86
|
-
|
|
122
|
+
const blocks = [];
|
|
123
|
+
if (pinned) blocks.push(renderIdentityDirective(pinned, identityExists(pinned)));
|
|
124
|
+
if (unread.length > 0) blocks.push(renderContext(unread));
|
|
125
|
+
if (blocks.length === 0) return noop();
|
|
87
126
|
emit({
|
|
88
127
|
continue: true,
|
|
89
128
|
hookSpecificOutput: {
|
|
90
129
|
hookEventName: "SessionStart",
|
|
91
|
-
additionalContext:
|
|
130
|
+
additionalContext: blocks.join("\n\n")
|
|
92
131
|
}
|
|
93
132
|
});
|
|
94
133
|
}
|
package/dist/index.js
CHANGED
|
@@ -2981,7 +2981,7 @@ var require_compile = __commonJS({
|
|
|
2981
2981
|
const schOrFunc = root.refs[ref];
|
|
2982
2982
|
if (schOrFunc)
|
|
2983
2983
|
return schOrFunc;
|
|
2984
|
-
let _sch =
|
|
2984
|
+
let _sch = resolve3.call(this, root, ref);
|
|
2985
2985
|
if (_sch === void 0) {
|
|
2986
2986
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
2987
2987
|
const { schemaId } = this.opts;
|
|
@@ -3008,7 +3008,7 @@ var require_compile = __commonJS({
|
|
|
3008
3008
|
function sameSchemaEnv(s1, s2) {
|
|
3009
3009
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3010
3010
|
}
|
|
3011
|
-
function
|
|
3011
|
+
function resolve3(root, ref) {
|
|
3012
3012
|
let sch;
|
|
3013
3013
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3014
3014
|
ref = sch;
|
|
@@ -3639,7 +3639,7 @@ var require_fast_uri = __commonJS({
|
|
|
3639
3639
|
}
|
|
3640
3640
|
return uri;
|
|
3641
3641
|
}
|
|
3642
|
-
function
|
|
3642
|
+
function resolve3(baseURI, relativeURI, options) {
|
|
3643
3643
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3644
3644
|
const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
3645
3645
|
schemelessOptions.skipEscape = true;
|
|
@@ -3897,7 +3897,7 @@ var require_fast_uri = __commonJS({
|
|
|
3897
3897
|
var fastUri = {
|
|
3898
3898
|
SCHEMES,
|
|
3899
3899
|
normalize,
|
|
3900
|
-
resolve:
|
|
3900
|
+
resolve: resolve3,
|
|
3901
3901
|
resolveComponent,
|
|
3902
3902
|
equal,
|
|
3903
3903
|
serialize,
|
|
@@ -6873,12 +6873,12 @@ var require_dist = __commonJS({
|
|
|
6873
6873
|
throw new Error(`Unknown format "${name}"`);
|
|
6874
6874
|
return f;
|
|
6875
6875
|
};
|
|
6876
|
-
function addFormats(ajv, list,
|
|
6876
|
+
function addFormats(ajv, list, fs3, exportName) {
|
|
6877
6877
|
var _a;
|
|
6878
6878
|
var _b;
|
|
6879
6879
|
(_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
6880
6880
|
for (const f of list)
|
|
6881
|
-
ajv.addFormat(f,
|
|
6881
|
+
ajv.addFormat(f, fs3[f]);
|
|
6882
6882
|
}
|
|
6883
6883
|
module.exports = exports = formatsPlugin;
|
|
6884
6884
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -18980,7 +18980,7 @@ var Protocol = class {
|
|
|
18980
18980
|
return;
|
|
18981
18981
|
}
|
|
18982
18982
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
18983
|
-
await new Promise((
|
|
18983
|
+
await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
|
|
18984
18984
|
options?.signal?.throwIfAborted();
|
|
18985
18985
|
}
|
|
18986
18986
|
} catch (error2) {
|
|
@@ -18997,7 +18997,7 @@ var Protocol = class {
|
|
|
18997
18997
|
*/
|
|
18998
18998
|
request(request, resultSchema, options) {
|
|
18999
18999
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
19000
|
-
return new Promise((
|
|
19000
|
+
return new Promise((resolve3, reject) => {
|
|
19001
19001
|
const earlyReject = (error2) => {
|
|
19002
19002
|
reject(error2);
|
|
19003
19003
|
};
|
|
@@ -19075,7 +19075,7 @@ var Protocol = class {
|
|
|
19075
19075
|
if (!parseResult.success) {
|
|
19076
19076
|
reject(parseResult.error);
|
|
19077
19077
|
} else {
|
|
19078
|
-
|
|
19078
|
+
resolve3(parseResult.data);
|
|
19079
19079
|
}
|
|
19080
19080
|
} catch (error2) {
|
|
19081
19081
|
reject(error2);
|
|
@@ -19336,12 +19336,12 @@ var Protocol = class {
|
|
|
19336
19336
|
}
|
|
19337
19337
|
} catch {
|
|
19338
19338
|
}
|
|
19339
|
-
return new Promise((
|
|
19339
|
+
return new Promise((resolve3, reject) => {
|
|
19340
19340
|
if (signal.aborted) {
|
|
19341
19341
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
19342
19342
|
return;
|
|
19343
19343
|
}
|
|
19344
|
-
const timeoutId = setTimeout(
|
|
19344
|
+
const timeoutId = setTimeout(resolve3, interval);
|
|
19345
19345
|
signal.addEventListener("abort", () => {
|
|
19346
19346
|
clearTimeout(timeoutId);
|
|
19347
19347
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -20441,7 +20441,7 @@ var McpServer = class {
|
|
|
20441
20441
|
let task = createTaskResult.task;
|
|
20442
20442
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
20443
20443
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
20444
|
-
await new Promise((
|
|
20444
|
+
await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
|
|
20445
20445
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
20446
20446
|
if (!updatedTask) {
|
|
20447
20447
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -21090,12 +21090,12 @@ var StdioServerTransport = class {
|
|
|
21090
21090
|
this.onclose?.();
|
|
21091
21091
|
}
|
|
21092
21092
|
send(message) {
|
|
21093
|
-
return new Promise((
|
|
21093
|
+
return new Promise((resolve3) => {
|
|
21094
21094
|
const json = serializeMessage(message);
|
|
21095
21095
|
if (this._stdout.write(json)) {
|
|
21096
|
-
|
|
21096
|
+
resolve3();
|
|
21097
21097
|
} else {
|
|
21098
|
-
this._stdout.once("drain",
|
|
21098
|
+
this._stdout.once("drain", resolve3);
|
|
21099
21099
|
}
|
|
21100
21100
|
});
|
|
21101
21101
|
}
|
|
@@ -21594,7 +21594,7 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
|
|
|
21594
21594
|
});
|
|
21595
21595
|
if (!chunk) {
|
|
21596
21596
|
if (i === 1) {
|
|
21597
|
-
await new Promise((
|
|
21597
|
+
await new Promise((resolve3) => setTimeout(resolve3));
|
|
21598
21598
|
maxReadCount = 3;
|
|
21599
21599
|
continue;
|
|
21600
21600
|
}
|
|
@@ -22094,9 +22094,9 @@ data:
|
|
|
22094
22094
|
const initRequest = messages.find((m) => isInitializeRequest(m));
|
|
22095
22095
|
const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
|
|
22096
22096
|
if (this._enableJsonResponse) {
|
|
22097
|
-
return new Promise((
|
|
22097
|
+
return new Promise((resolve3) => {
|
|
22098
22098
|
this._streamMapping.set(streamId, {
|
|
22099
|
-
resolveJson:
|
|
22099
|
+
resolveJson: resolve3,
|
|
22100
22100
|
cleanup: () => {
|
|
22101
22101
|
this._streamMapping.delete(streamId);
|
|
22102
22102
|
}
|
|
@@ -22427,23 +22427,75 @@ var StreamableHTTPServerTransport = class {
|
|
|
22427
22427
|
};
|
|
22428
22428
|
|
|
22429
22429
|
// src/index.ts
|
|
22430
|
-
import {
|
|
22431
|
-
import { resolve, join, dirname } from "node:path";
|
|
22430
|
+
import { resolve as resolve2, join as join2, dirname as dirname2 } from "node:path";
|
|
22432
22431
|
import { fileURLToPath } from "node:url";
|
|
22433
22432
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
22434
22433
|
import { createServer as createHttpServer } from "node:http";
|
|
22435
|
-
import * as
|
|
22434
|
+
import * as fs2 from "node:fs";
|
|
22436
22435
|
import { brotliCompressSync, brotliDecompressSync, constants as zlibConstants } from "node:zlib";
|
|
22437
22436
|
import { adapt_wrapper } from "@adapt-toolkit/sdk/executables";
|
|
22438
22437
|
import { PacketWrapperConfigurator } from "@adapt-toolkit/sdk/wrappers";
|
|
22439
22438
|
import { object_to_adapt_value } from "@adapt-toolkit/sdk/wrapper";
|
|
22440
|
-
|
|
22441
|
-
|
|
22442
|
-
|
|
22443
|
-
|
|
22444
|
-
|
|
22439
|
+
|
|
22440
|
+
// src/config.ts
|
|
22441
|
+
import * as fs from "node:fs";
|
|
22442
|
+
import { homedir } from "node:os";
|
|
22443
|
+
import { resolve, join, dirname } from "node:path";
|
|
22444
|
+
var DEFAULT_CONFIG = {
|
|
22445
|
+
brokerUrl: "ws://a2adapt.adaptframework.solutions/broker",
|
|
22446
|
+
port: 3030,
|
|
22447
|
+
stateDir: resolve(homedir(), ".a2adapt"),
|
|
22448
|
+
gcIntervalMs: 36e5
|
|
22449
|
+
};
|
|
22450
|
+
function configPath() {
|
|
22451
|
+
return process.env.A2ADAPT_CONFIG ?? join(homedir(), ".a2adapt", "config.json");
|
|
22452
|
+
}
|
|
22453
|
+
function readFileConfig() {
|
|
22454
|
+
let raw;
|
|
22455
|
+
try {
|
|
22456
|
+
raw = fs.readFileSync(configPath(), "utf8");
|
|
22457
|
+
} catch {
|
|
22458
|
+
return {};
|
|
22459
|
+
}
|
|
22460
|
+
let parsed;
|
|
22461
|
+
try {
|
|
22462
|
+
parsed = JSON.parse(raw);
|
|
22463
|
+
} catch {
|
|
22464
|
+
return {};
|
|
22465
|
+
}
|
|
22466
|
+
const out = {};
|
|
22467
|
+
if (typeof parsed.brokerUrl === "string") out.brokerUrl = parsed.brokerUrl;
|
|
22468
|
+
if (typeof parsed.port === "number" && Number.isFinite(parsed.port)) out.port = parsed.port;
|
|
22469
|
+
if (typeof parsed.stateDir === "string") out.stateDir = resolve(parsed.stateDir);
|
|
22470
|
+
if (typeof parsed.gcIntervalMs === "number" && Number.isFinite(parsed.gcIntervalMs)) {
|
|
22471
|
+
out.gcIntervalMs = parsed.gcIntervalMs;
|
|
22472
|
+
}
|
|
22473
|
+
return out;
|
|
22474
|
+
}
|
|
22475
|
+
function envInt(name) {
|
|
22476
|
+
const v = process.env[name];
|
|
22477
|
+
if (v === void 0) return void 0;
|
|
22478
|
+
const n = parseInt(v, 10);
|
|
22479
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
22480
|
+
}
|
|
22481
|
+
function loadConfig() {
|
|
22482
|
+
const file = readFileConfig();
|
|
22483
|
+
return {
|
|
22484
|
+
brokerUrl: process.env.A2ADAPT_BROKER_URL ?? file.brokerUrl ?? DEFAULT_CONFIG.brokerUrl,
|
|
22485
|
+
port: envInt("A2ADAPT_PORT") ?? file.port ?? DEFAULT_CONFIG.port,
|
|
22486
|
+
stateDir: resolve(process.env.A2ADAPT_STATE_DIR ?? file.stateDir ?? DEFAULT_CONFIG.stateDir),
|
|
22487
|
+
gcIntervalMs: envInt("A2ADAPT_GC_INTERVAL_MS") ?? file.gcIntervalMs ?? DEFAULT_CONFIG.gcIntervalMs
|
|
22488
|
+
};
|
|
22489
|
+
}
|
|
22490
|
+
|
|
22491
|
+
// src/index.ts
|
|
22492
|
+
var VERSION = true ? "0.7.0" : "0.0.0-dev";
|
|
22493
|
+
var CONFIG = loadConfig();
|
|
22494
|
+
var STATE_DIR = CONFIG.stateDir;
|
|
22495
|
+
var BROKER_URL = CONFIG.brokerUrl;
|
|
22445
22496
|
var TRANSPORT = process.env.A2ADAPT_TRANSPORT ?? "http";
|
|
22446
|
-
var PORT =
|
|
22497
|
+
var PORT = CONFIG.port;
|
|
22498
|
+
var GC_INTERVAL_MS = CONFIG.gcIntervalMs;
|
|
22447
22499
|
var log = (...parts) => process.stderr.write(`a2adapt: ${parts.join(" ")}
|
|
22448
22500
|
`);
|
|
22449
22501
|
var NAME_RE = /^[A-Za-z0-9 _.-]{1,64}$/;
|
|
@@ -22457,15 +22509,15 @@ function validateName(name) {
|
|
|
22457
22509
|
return null;
|
|
22458
22510
|
}
|
|
22459
22511
|
function locateUnit() {
|
|
22460
|
-
const here =
|
|
22512
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
22461
22513
|
const override = process.env.A2ADAPT_UNIT_DIR;
|
|
22462
|
-
const candidates = override ? [
|
|
22514
|
+
const candidates = override ? [resolve2(override)] : [join2(here, "mufl_code"), join2(here, "..", "mufl_code")];
|
|
22463
22515
|
for (const dir of candidates) {
|
|
22464
|
-
if (!
|
|
22465
|
-
const muflo =
|
|
22516
|
+
if (!fs2.existsSync(dir)) continue;
|
|
22517
|
+
const muflo = fs2.readdirSync(dir).find((f) => f.endsWith(".muflo"));
|
|
22466
22518
|
if (muflo) {
|
|
22467
22519
|
const hash = muflo.slice(0, -".muflo".length);
|
|
22468
|
-
const contents = new Uint8Array(
|
|
22520
|
+
const contents = new Uint8Array(fs2.readFileSync(join2(dir, muflo)));
|
|
22469
22521
|
return { dir, hash, contents };
|
|
22470
22522
|
}
|
|
22471
22523
|
}
|
|
@@ -22479,18 +22531,18 @@ var identities = /* @__PURE__ */ new Map();
|
|
|
22479
22531
|
var sessionBinding = /* @__PURE__ */ new Map();
|
|
22480
22532
|
var bindingOwner = /* @__PURE__ */ new Map();
|
|
22481
22533
|
var evictedSessions = /* @__PURE__ */ new Set();
|
|
22482
|
-
var identityDir = (name) =>
|
|
22483
|
-
var seedPath = (dir) =>
|
|
22484
|
-
var dataPath = (dir) =>
|
|
22485
|
-
var notifyLogPath = (dir) =>
|
|
22486
|
-
var unreadPath = (dir) =>
|
|
22534
|
+
var identityDir = (name) => join2(STATE_DIR, name);
|
|
22535
|
+
var seedPath = (dir) => join2(dir, "identity.seed");
|
|
22536
|
+
var dataPath = (dir) => join2(dir, "state_data.bin");
|
|
22537
|
+
var notifyLogPath = (dir) => join2(dir, "notifications.log");
|
|
22538
|
+
var unreadPath = (dir) => join2(dir, "unread.json");
|
|
22487
22539
|
function listPersistedNames() {
|
|
22488
|
-
if (!
|
|
22489
|
-
return
|
|
22540
|
+
if (!fs2.existsSync(STATE_DIR)) return [];
|
|
22541
|
+
return fs2.readdirSync(STATE_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && fs2.existsSync(seedPath(join2(STATE_DIR, d.name)))).map((d) => d.name);
|
|
22490
22542
|
}
|
|
22491
22543
|
function hasSavedState(dir) {
|
|
22492
22544
|
try {
|
|
22493
|
-
return
|
|
22545
|
+
return fs2.existsSync(dataPath(dir)) && fs2.statSync(dataPath(dir)).size > 0;
|
|
22494
22546
|
} catch {
|
|
22495
22547
|
return false;
|
|
22496
22548
|
}
|
|
@@ -22501,19 +22553,19 @@ function saveState(id) {
|
|
|
22501
22553
|
object_to_adapt_value({ name: "::actor::export_state", targ: void 0 })
|
|
22502
22554
|
);
|
|
22503
22555
|
const bytes = Buffer.from(exported.Serialize());
|
|
22504
|
-
|
|
22556
|
+
fs2.mkdirSync(id.dir, { recursive: true });
|
|
22505
22557
|
const tmp = `${dataPath(id.dir)}.tmp`;
|
|
22506
|
-
|
|
22507
|
-
|
|
22558
|
+
fs2.writeFileSync(tmp, bytes);
|
|
22559
|
+
fs2.renameSync(tmp, dataPath(id.dir));
|
|
22508
22560
|
} catch (err) {
|
|
22509
22561
|
log(`[${id.name}] failed to save state:`, String(err));
|
|
22510
22562
|
}
|
|
22511
22563
|
}
|
|
22512
22564
|
function appendNotifyLog(id, from, msgId, date3) {
|
|
22513
22565
|
try {
|
|
22514
|
-
|
|
22566
|
+
fs2.mkdirSync(id.dir, { recursive: true });
|
|
22515
22567
|
const line = JSON.stringify({ event: "message_received", from, msg_id: msgId, date: date3 }) + "\n";
|
|
22516
|
-
|
|
22568
|
+
fs2.appendFileSync(notifyLogPath(id.dir), line);
|
|
22517
22569
|
} catch (err) {
|
|
22518
22570
|
log(`[${id.name}] failed to append notifications.log:`, String(err));
|
|
22519
22571
|
}
|
|
@@ -22526,10 +22578,10 @@ function refreshUnread(id) {
|
|
|
22526
22578
|
count: unread.length,
|
|
22527
22579
|
recent: unread.slice(-10).map((m) => ({ from: m.sender_name, msg_id: m.msg_id, date: m.date }))
|
|
22528
22580
|
};
|
|
22529
|
-
|
|
22581
|
+
fs2.mkdirSync(id.dir, { recursive: true });
|
|
22530
22582
|
const tmp = `${unreadPath(id.dir)}.tmp`;
|
|
22531
|
-
|
|
22532
|
-
|
|
22583
|
+
fs2.writeFileSync(tmp, JSON.stringify(snapshot));
|
|
22584
|
+
fs2.renameSync(tmp, unreadPath(id.dir));
|
|
22533
22585
|
} catch (err) {
|
|
22534
22586
|
log(`[${id.name}] failed to refresh unread snapshot:`, String(err));
|
|
22535
22587
|
}
|
|
@@ -22665,9 +22717,9 @@ function createPacket(name, seed, dir) {
|
|
|
22665
22717
|
}
|
|
22666
22718
|
async function provisionIdentity(name) {
|
|
22667
22719
|
const dir = identityDir(name);
|
|
22668
|
-
|
|
22720
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
22669
22721
|
const seed = randomBytes(24).toString("hex");
|
|
22670
|
-
|
|
22722
|
+
fs2.writeFileSync(seedPath(dir), seed, { mode: 384 });
|
|
22671
22723
|
const id = await createPacket(name, seed, dir);
|
|
22672
22724
|
await mutatingTx(id, "::actor::set_my_name", { name });
|
|
22673
22725
|
saveState(id);
|
|
@@ -22675,11 +22727,11 @@ async function provisionIdentity(name) {
|
|
|
22675
22727
|
}
|
|
22676
22728
|
async function restoreIdentity(name) {
|
|
22677
22729
|
const dir = identityDir(name);
|
|
22678
|
-
const seed =
|
|
22730
|
+
const seed = fs2.readFileSync(seedPath(dir), "utf8").trim();
|
|
22679
22731
|
const id = await createPacket(name, seed, dir);
|
|
22680
22732
|
if (hasSavedState(dir)) {
|
|
22681
22733
|
try {
|
|
22682
|
-
const buf =
|
|
22734
|
+
const buf = fs2.readFileSync(dataPath(dir));
|
|
22683
22735
|
const adaptData = id.pw.packet.ParseValue(new Uint8Array(buf));
|
|
22684
22736
|
await mutatingTx(id, "::actor::import_state", adaptData);
|
|
22685
22737
|
} catch (err) {
|
|
@@ -22786,10 +22838,10 @@ function packInvite(raw) {
|
|
|
22786
22838
|
[zlibConstants.BROTLI_PARAM_SIZE_HINT]: raw.length
|
|
22787
22839
|
}
|
|
22788
22840
|
});
|
|
22789
|
-
return Buffer.concat([Buffer.from([INVITE_BROTLI_V1]), compressed]).toString("
|
|
22841
|
+
return Buffer.concat([Buffer.from([INVITE_BROTLI_V1]), compressed]).toString("base64url");
|
|
22790
22842
|
}
|
|
22791
22843
|
function unpackInvite(b64) {
|
|
22792
|
-
const outer = Buffer.from(b64.
|
|
22844
|
+
const outer = Buffer.from(b64.replace(/\s+/g, ""), "base64url");
|
|
22793
22845
|
if (outer.length < 2) throw new Error("the invite blob is too short or not valid base64");
|
|
22794
22846
|
const version2 = outer[0];
|
|
22795
22847
|
if (version2 === INVITE_BROTLI_V1) return Buffer.from(brotliDecompressSync(outer.subarray(1)));
|
|
@@ -22897,7 +22949,7 @@ ${lines.join("\n")}`);
|
|
|
22897
22949
|
sessionBinding.delete(holder);
|
|
22898
22950
|
}
|
|
22899
22951
|
try {
|
|
22900
|
-
|
|
22952
|
+
fs2.rmSync(id.dir, { recursive: true, force: true });
|
|
22901
22953
|
} catch (err) {
|
|
22902
22954
|
return textResult(`Identity "${name}" removed from memory, but deleting ${id.dir} failed: ${String(err)}`, true);
|
|
22903
22955
|
}
|
|
@@ -23003,6 +23055,23 @@ ${contacts.map((c) => `\u2022 ${c.name} \u2014 ${c.container_id}`).join("\n")}`)
|
|
|
23003
23055
|
}
|
|
23004
23056
|
}
|
|
23005
23057
|
);
|
|
23058
|
+
server.tool(
|
|
23059
|
+
"remove_contact",
|
|
23060
|
+
"Forget a contact (by name or container id) \u2014 drops it from the bound identity's contacts, so you can no longer message them and inbound messages from them are rejected. This is a contacts-layer forget, NOT a key wipe: the per-peer channel key material persists, so re-adding the same peer reuses the existing encrypted channel rather than re-handshaking. Requires a bound identity.",
|
|
23061
|
+
{ contact: external_exports.string().min(1).describe("Contact name or container id to remove.") },
|
|
23062
|
+
async ({ contact }) => {
|
|
23063
|
+
const { id, err } = boundOr();
|
|
23064
|
+
if (err) return err;
|
|
23065
|
+
try {
|
|
23066
|
+
const data = await mutatingTx(id, "::actor::remove_contact", { contact });
|
|
23067
|
+
const name = data.Reduce("removed").Visualize();
|
|
23068
|
+
const cid = data.Reduce("container_id").Visualize();
|
|
23069
|
+
return textResult(`Removed contact "${name}" (${cid}).`);
|
|
23070
|
+
} catch (e) {
|
|
23071
|
+
return textResult(`remove_contact failed: ${String(e)}`, true);
|
|
23072
|
+
}
|
|
23073
|
+
}
|
|
23074
|
+
);
|
|
23006
23075
|
server.tool(
|
|
23007
23076
|
"list_incoming_messages",
|
|
23008
23077
|
"List ALL messages in the bound identity's inbox (decrypted), each with its id and status (unread/read). A read-only history view \u2014 it does not change any status. To consume new mail use get_messages instead.",
|
|
@@ -23025,7 +23094,7 @@ ${inbox.map((m) => fmtMsg(m)).join("\n")}`
|
|
|
23025
23094
|
);
|
|
23026
23095
|
server.tool(
|
|
23027
23096
|
"get_messages",
|
|
23028
|
-
'Fetch the messages the bound identity has not seen yet (status "unread") and mark them "
|
|
23097
|
+
'Fetch the messages the bound identity has not seen yet (status "unread") and mark them "processed". This is the ONLY call that returns message bodies, and each message is delivered exactly once, so reading and acting on it immediately never double-processes \u2014 no acknowledgement call is needed. If you read a message but crash or want to hand it to another session before acting, call defer_messages to put it back to "unread"; otherwise handled messages are garbage-collected automatically.',
|
|
23029
23098
|
{},
|
|
23030
23099
|
async () => {
|
|
23031
23100
|
const { id, err } = boundOr();
|
|
@@ -23039,33 +23108,16 @@ ${inbox.map((m) => fmtMsg(m)).join("\n")}`
|
|
|
23039
23108
|
`${fresh.length} new message(s):
|
|
23040
23109
|
${fresh.map((m) => fmtMsg(m, false)).join("\n")}
|
|
23041
23110
|
|
|
23042
|
-
|
|
23111
|
+
These are now marked processed (auto-GC'd later). To hand any back to another session: defer_messages({ msg_ids: [${fresh.map((m) => m.msg_id).join(", ")}] }).`
|
|
23043
23112
|
);
|
|
23044
23113
|
} catch (e) {
|
|
23045
23114
|
return textResult(`get_messages failed: ${String(e)}`, true);
|
|
23046
23115
|
}
|
|
23047
23116
|
}
|
|
23048
23117
|
);
|
|
23049
|
-
server.tool(
|
|
23050
|
-
"mark_processed",
|
|
23051
|
-
"Mark the given messages handled \u2014 they are removed from the inbox permanently. Idempotent: ids already gone are ignored. Optional for correctness (get_messages already prevents re-delivery); use it to keep the inbox tidy.",
|
|
23052
|
-
{ msg_ids: external_exports.array(external_exports.number().int()).min(1).describe("Message ids (from get_messages) to mark processed.") },
|
|
23053
|
-
async ({ msg_ids }) => {
|
|
23054
|
-
const { id, err } = boundOr();
|
|
23055
|
-
if (err) return err;
|
|
23056
|
-
try {
|
|
23057
|
-
const data = await mutatingTx(id, "::actor::mark_processed", { msg_ids });
|
|
23058
|
-
const n = data.Reduce("processed").Visualize();
|
|
23059
|
-
refreshUnread(id);
|
|
23060
|
-
return textResult(`Marked ${n} message(s) processed.`);
|
|
23061
|
-
} catch (e) {
|
|
23062
|
-
return textResult(`mark_processed failed: ${String(e)}`, true);
|
|
23063
|
-
}
|
|
23064
|
-
}
|
|
23065
|
-
);
|
|
23066
23118
|
server.tool(
|
|
23067
23119
|
"defer_messages",
|
|
23068
|
-
`Put
|
|
23120
|
+
`Put handled messages back into the queue (status "unread") so another session's get_messages picks them up. Works on messages you have read (status "processed") and even ones already queued for GC ("ready_to_delete"), so a message stays recoverable across a full GC cycle.`,
|
|
23069
23121
|
{ msg_ids: external_exports.array(external_exports.number().int()).min(1).describe("Message ids (from get_messages) to defer back to unread.") },
|
|
23070
23122
|
async ({ msg_ids }) => {
|
|
23071
23123
|
const { id, err } = boundOr();
|
|
@@ -23083,12 +23135,12 @@ When handled: mark_processed({ msg_ids: [${fresh.map((m) => m.msg_id).join(", ")
|
|
|
23083
23135
|
return server;
|
|
23084
23136
|
}
|
|
23085
23137
|
function readBody(req) {
|
|
23086
|
-
return new Promise((
|
|
23138
|
+
return new Promise((resolve3, reject) => {
|
|
23087
23139
|
let data = "";
|
|
23088
23140
|
req.on("data", (chunk) => data += chunk);
|
|
23089
23141
|
req.on("end", () => {
|
|
23090
23142
|
try {
|
|
23091
|
-
|
|
23143
|
+
resolve3(JSON.parse(data));
|
|
23092
23144
|
} catch (e) {
|
|
23093
23145
|
reject(e);
|
|
23094
23146
|
}
|
|
@@ -23096,6 +23148,36 @@ function readBody(req) {
|
|
|
23096
23148
|
req.on("error", reject);
|
|
23097
23149
|
});
|
|
23098
23150
|
}
|
|
23151
|
+
var gcTimer = null;
|
|
23152
|
+
var gcRunning = false;
|
|
23153
|
+
function startGcTimer() {
|
|
23154
|
+
if (gcTimer) return;
|
|
23155
|
+
gcTimer = setInterval(() => {
|
|
23156
|
+
if (gcRunning) return;
|
|
23157
|
+
gcRunning = true;
|
|
23158
|
+
void (async () => {
|
|
23159
|
+
try {
|
|
23160
|
+
for (const id of identities.values()) {
|
|
23161
|
+
try {
|
|
23162
|
+
await mutatingTx(id, "::actor::gc", {});
|
|
23163
|
+
} catch (e) {
|
|
23164
|
+
log(`gc(${id.name}) failed:`, String(e));
|
|
23165
|
+
}
|
|
23166
|
+
}
|
|
23167
|
+
} finally {
|
|
23168
|
+
gcRunning = false;
|
|
23169
|
+
}
|
|
23170
|
+
})();
|
|
23171
|
+
}, GC_INTERVAL_MS);
|
|
23172
|
+
gcTimer.unref?.();
|
|
23173
|
+
log(`message GC timer armed (every ${GC_INTERVAL_MS}ms)`);
|
|
23174
|
+
}
|
|
23175
|
+
function stopGcTimer() {
|
|
23176
|
+
if (gcTimer) {
|
|
23177
|
+
clearInterval(gcTimer);
|
|
23178
|
+
gcTimer = null;
|
|
23179
|
+
}
|
|
23180
|
+
}
|
|
23099
23181
|
async function main() {
|
|
23100
23182
|
if (TRANSPORT === "stdio") {
|
|
23101
23183
|
const server = createMcpServer(() => "stdio");
|
|
@@ -23104,8 +23186,10 @@ async function main() {
|
|
|
23104
23186
|
await server.connect(transport);
|
|
23105
23187
|
log("MCP stdio transport connected, booting wrapper\u2026");
|
|
23106
23188
|
await bootWrapper();
|
|
23189
|
+
startGcTimer();
|
|
23107
23190
|
log(`MCP server v${VERSION} ready (transport=stdio, identities=${identities.size}, state=${STATE_DIR}, broker=${BROKER_URL})`);
|
|
23108
23191
|
const flush = () => {
|
|
23192
|
+
stopGcTimer();
|
|
23109
23193
|
for (const id of identities.values()) saveState(id);
|
|
23110
23194
|
process.exit(0);
|
|
23111
23195
|
};
|
|
@@ -23115,6 +23199,7 @@ async function main() {
|
|
|
23115
23199
|
}
|
|
23116
23200
|
log("booting wrapper\u2026");
|
|
23117
23201
|
await bootWrapper();
|
|
23202
|
+
startGcTimer();
|
|
23118
23203
|
log(`wrapper ready (identities=${identities.size}), starting HTTP server\u2026`);
|
|
23119
23204
|
const transports = {};
|
|
23120
23205
|
const httpServer = createHttpServer(async (req, res) => {
|
|
@@ -23191,6 +23276,7 @@ async function main() {
|
|
|
23191
23276
|
});
|
|
23192
23277
|
const shutdown = async () => {
|
|
23193
23278
|
log("shutting down\u2026");
|
|
23279
|
+
stopGcTimer();
|
|
23194
23280
|
for (const sid of Object.keys(transports)) {
|
|
23195
23281
|
try {
|
|
23196
23282
|
await transports[sid].close();
|
|
Binary file
|
package/dist/mufl_code/actor.mu
CHANGED
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
// encrypted; the key exchange is handled for us by the stdlib `encrypted_channel`
|
|
6
6
|
// library — we only ever address peers by their container id.
|
|
7
7
|
//
|
|
8
|
-
// User transactions (each backs one MCP tool):
|
|
8
|
+
// User transactions (each backs one MCP tool, except gc which the host fires):
|
|
9
9
|
// set_my_name — set the display name peers see for me
|
|
10
10
|
// generate_invite — make a slim personal invite blob for a named peer
|
|
11
11
|
// add_contact — join via an invite blob, reply to the inviter
|
|
12
12
|
// send_message — send an e2e-encrypted message to a contact
|
|
13
|
+
// remove_contact — forget a contact (drops it from my contacts)
|
|
13
14
|
// list_contacts — (readonly) my contacts
|
|
14
15
|
// list_incoming_messages — (readonly) my inbox, with per-message id + status
|
|
15
|
-
// get_messages — return unread messages + mark them
|
|
16
|
-
//
|
|
17
|
-
//
|
|
16
|
+
// get_messages — return unread messages + mark them processed (sole body egress)
|
|
17
|
+
// defer_messages — flip processed/ready_to_delete messages back to unread
|
|
18
|
+
// gc — two-generation GC of handled messages (host-fired, not a tool)
|
|
18
19
|
//
|
|
19
20
|
// External transactions (inbound, not exposed as tools):
|
|
20
21
|
// accept_contact — inviter learns the joiner's identity + name
|
|
@@ -55,10 +56,12 @@ application actor loads libraries
|
|
|
55
56
|
// (each key knows its own function + id), so the reconstructed identity is
|
|
56
57
|
// byte-identical to the signed one and the self-signatures still verify.
|
|
57
58
|
metadef invite_t: ($d -> global_id, $n -> str, $c -> global_id, $k -> key_utils::t_publickey(,), $a -> crypto_signature(,)).
|
|
58
|
-
// A received message carries a stable per-packet id and a lifecycle
|
|
59
|
-
//
|
|
60
|
-
// get_messages)
|
|
61
|
-
//
|
|
59
|
+
// A received message carries a stable per-packet id and a lifecycle status:
|
|
60
|
+
// "unread" (just arrived) -> "processed" (handed to the agent via
|
|
61
|
+
// get_messages) -> "ready_to_delete" (first gc tick) -> deleted (next gc
|
|
62
|
+
// tick). defer_messages flips "processed"/"ready_to_delete" back to "unread"
|
|
63
|
+
// so another session can pick it up — restorable from either generation,
|
|
64
|
+
// which is what makes the two-generation gc window race-free.
|
|
62
65
|
metadef message_t: ($msg_id -> int, $sender_id -> global_id, $sender_name -> str, $text -> str, $date -> time, $status -> str).
|
|
63
66
|
// The pre-lifecycle inbox shape (no per-message id/status). import_state
|
|
64
67
|
// migrates blobs in this shape forward — see below.
|
|
@@ -236,6 +239,23 @@ application actor loads libraries
|
|
|
236
239
|
}).
|
|
237
240
|
}
|
|
238
241
|
|
|
242
|
+
trn remove_contact _:($contact -> contact_ref: str)
|
|
243
|
+
{
|
|
244
|
+
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
245
|
+
|
|
246
|
+
target_id = resolve_contact contact_ref.
|
|
247
|
+
removed = contacts target_id.
|
|
248
|
+
removed_name = removed? $name.
|
|
249
|
+
|
|
250
|
+
delete contacts target_id.
|
|
251
|
+
if peer_ads target_id != NIL { delete peer_ads target_id. }
|
|
252
|
+
|
|
253
|
+
return transaction::success [
|
|
254
|
+
_return_data ($removed -> removed_name, $container_id -> target_id),
|
|
255
|
+
_save_state NIL
|
|
256
|
+
].
|
|
257
|
+
}
|
|
258
|
+
|
|
239
259
|
trn readonly list_contacts _
|
|
240
260
|
{
|
|
241
261
|
return contacts.
|
|
@@ -246,13 +266,13 @@ application actor loads libraries
|
|
|
246
266
|
return inbox.
|
|
247
267
|
}
|
|
248
268
|
|
|
249
|
-
// Hand the agent every message it has not seen yet (status "unread") and
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
269
|
+
// Hand the agent every message it has not seen yet (status "unread") and flip
|
|
270
|
+
// those to "processed" — the ONLY place message bodies leave the packet, and
|
|
271
|
+
// the sole dedup point: a message is returned exactly once, so an agent that
|
|
272
|
+
// reads and acts immediately never double-processes. CRASH WINDOW: a
|
|
273
|
+
// "processed" body has already left; an agent that crashes after get_messages
|
|
274
|
+
// but before acting must defer_messages to recover it (-> "unread") before gc
|
|
275
|
+
// promotes it through "ready_to_delete" and deletes it (>= 1 gc cycle).
|
|
256
276
|
trn get_messages _
|
|
257
277
|
{
|
|
258
278
|
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
@@ -263,16 +283,16 @@ application actor loads libraries
|
|
|
263
283
|
{
|
|
264
284
|
if (m $status) == "unread"
|
|
265
285
|
{
|
|
266
|
-
|
|
286
|
+
processed_m is message_t = (
|
|
267
287
|
$msg_id -> m $msg_id,
|
|
268
288
|
$sender_id -> m $sender_id,
|
|
269
289
|
$sender_name -> m $sender_name,
|
|
270
290
|
$text -> m $text,
|
|
271
291
|
$date -> m $date,
|
|
272
|
-
$status -> "
|
|
292
|
+
$status -> "processed"
|
|
273
293
|
).
|
|
274
294
|
fresh (_count fresh|) -> m.
|
|
275
|
-
new_inbox (_count new_inbox|) ->
|
|
295
|
+
new_inbox (_count new_inbox|) -> processed_m.
|
|
276
296
|
}
|
|
277
297
|
else
|
|
278
298
|
{
|
|
@@ -287,40 +307,57 @@ application actor loads libraries
|
|
|
287
307
|
].
|
|
288
308
|
}
|
|
289
309
|
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
310
|
+
// Two-generation garbage collection of handled messages, fired by the host on
|
|
311
|
+
// a timer (NOT piggybacked on other transactions — that would collapse the
|
|
312
|
+
// window under traffic). Order matters: (A) delete everything already marked
|
|
313
|
+
// "ready_to_delete", THEN (B) promote "processed" -> "ready_to_delete". A
|
|
314
|
+
// single pass keyed on the current status gives exactly that — a message is in
|
|
315
|
+
// one status, so a freshly-processed message is promoted (not dropped) this
|
|
316
|
+
// tick and only deleted on the NEXT tick, guaranteeing it survives a full
|
|
317
|
+
// cycle (>= 1 interval) as a deferrable "ready_to_delete".
|
|
318
|
+
trn gc _
|
|
293
319
|
{
|
|
294
320
|
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
295
321
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
new_inbox is message_t[] = [].
|
|
300
|
-
removed is int = 0.
|
|
322
|
+
kept is message_t[] = [].
|
|
323
|
+
deleted is int = 0.
|
|
324
|
+
promoted is int = 0.
|
|
301
325
|
sc inbox -- ( -> m)
|
|
302
326
|
{
|
|
303
|
-
if
|
|
327
|
+
if (m $status) == "ready_to_delete"
|
|
304
328
|
{
|
|
305
|
-
|
|
329
|
+
deleted -> deleted + 1.
|
|
330
|
+
}
|
|
331
|
+
elif (m $status) == "processed"
|
|
332
|
+
{
|
|
333
|
+
kept (_count kept|) -> (
|
|
334
|
+
$msg_id -> m $msg_id,
|
|
335
|
+
$sender_id -> m $sender_id,
|
|
336
|
+
$sender_name -> m $sender_name,
|
|
337
|
+
$text -> m $text,
|
|
338
|
+
$date -> m $date,
|
|
339
|
+
$status -> "ready_to_delete"
|
|
340
|
+
).
|
|
341
|
+
promoted -> promoted + 1.
|
|
306
342
|
}
|
|
307
343
|
else
|
|
308
344
|
{
|
|
309
|
-
|
|
345
|
+
kept (_count kept|) -> m.
|
|
310
346
|
}
|
|
311
347
|
}
|
|
312
|
-
inbox ->
|
|
348
|
+
inbox -> kept.
|
|
313
349
|
|
|
314
350
|
return transaction::success [
|
|
315
|
-
_return_data ($
|
|
351
|
+
_return_data ($deleted -> deleted, $promoted -> promoted),
|
|
316
352
|
_save_state NIL
|
|
317
353
|
].
|
|
318
354
|
}
|
|
319
355
|
|
|
320
|
-
// Put
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
356
|
+
// Put handled messages back into the queue (status -> "unread") so a different
|
|
357
|
+
// session picks them up on its next get_messages. Restores from EITHER post-
|
|
358
|
+
// read generation — "processed" or "ready_to_delete" — so a message stays
|
|
359
|
+
// recoverable across a full gc cycle. The opt-in counterpart to the safe
|
|
360
|
+
// default: forgetting to defer just means the message stays handled by you.
|
|
324
361
|
trn defer_messages _:($msg_ids -> ids: int[])
|
|
325
362
|
{
|
|
326
363
|
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
@@ -332,7 +369,7 @@ application actor loads libraries
|
|
|
332
369
|
deferred is int = 0.
|
|
333
370
|
sc inbox -- ( -> m)
|
|
334
371
|
{
|
|
335
|
-
if (wanted (m $msg_id)) && ((m $status) == "
|
|
372
|
+
if (wanted (m $msg_id)) && (((m $status) == "processed") || ((m $status) == "ready_to_delete"))
|
|
336
373
|
{
|
|
337
374
|
new_inbox (_count new_inbox|) -> (
|
|
338
375
|
$msg_id -> m $msg_id,
|
|
@@ -389,12 +426,13 @@ application actor loads libraries
|
|
|
389
426
|
pending_invites -> (data $pending_invites) safe (global_id ->> str).
|
|
390
427
|
peer_ads -> (data $peer_ads) safe (global_id ->> address_document_types::t_address_document).
|
|
391
428
|
|
|
392
|
-
// The inbox + next_msg_seq are the only parts the message-lifecycle
|
|
429
|
+
// The inbox + next_msg_seq are the only parts the message-lifecycle changes
|
|
393
430
|
// touched. A pre-lifecycle blob has no $next_msg_seq and inbox entries with
|
|
394
431
|
// no id/status — MIGRATE it forward (the whole point of code-independent
|
|
395
432
|
// state is that an old export upgrades, never resets): assign each legacy
|
|
396
433
|
// message a sequential id and status "unread", and seed next_msg_seq past
|
|
397
|
-
// them. A current blob is
|
|
434
|
+
// them. A current-shape blob is loaded as-is except the gc vocabulary bump
|
|
435
|
+
// ("read" -> "processed", see below).
|
|
398
436
|
if (data $next_msg_seq) == NIL
|
|
399
437
|
{
|
|
400
438
|
legacy_inbox = (data $inbox) safe (legacy_message_t[]).
|
|
@@ -417,7 +455,24 @@ application actor loads libraries
|
|
|
417
455
|
}
|
|
418
456
|
else
|
|
419
457
|
{
|
|
420
|
-
|
|
458
|
+
// Current-shape blob. One vocabulary change: a blob written before the
|
|
459
|
+
// gc change may carry the old "read" status — migrate it to "processed"
|
|
460
|
+
// (else such a message is stuck: never returned, gc'd, or deferred).
|
|
461
|
+
loaded = (data $inbox) safe (message_t[]).
|
|
462
|
+
upgraded is message_t[] = [].
|
|
463
|
+
sc loaded -- ( -> m)
|
|
464
|
+
{
|
|
465
|
+
mstatus = ((m $status) == "read" ?? "processed" ; m $status).
|
|
466
|
+
upgraded (_count upgraded|) -> (
|
|
467
|
+
$msg_id -> m $msg_id,
|
|
468
|
+
$sender_id -> m $sender_id,
|
|
469
|
+
$sender_name -> m $sender_name,
|
|
470
|
+
$text -> m $text,
|
|
471
|
+
$date -> m $date,
|
|
472
|
+
$status -> mstatus
|
|
473
|
+
).
|
|
474
|
+
}
|
|
475
|
+
inbox -> upgraded.
|
|
421
476
|
next_msg_seq -> (data $next_msg_seq) safe int.
|
|
422
477
|
}
|
|
423
478
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adapt-toolkit/a2adapt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "MCP server daemon for a2adapt — one native ADAPT wrapper hosting N self-sovereign identities, exposing secure agent-to-agent messaging tools over HTTP (Streamable HTTP). Run `a2adapt-mcp start`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|