svamp-cli 0.2.103 → 0.2.105

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.
@@ -1,15 +1,15 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os$1, { homedir as homedir$1 } from 'os';
2
2
  import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writeFile$1, rename, unlink } from 'fs/promises';
3
- import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, rmSync as rmSync$1, unlinkSync as unlinkSync$1, copyFileSync, watch, rmdirSync, readdirSync as readdirSync$1 } from 'fs';
4
- import path__default, { join, dirname, basename, resolve } from 'path';
3
+ import { readFileSync as readFileSync$1, mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1, renameSync as renameSync$1, existsSync as existsSync$1, rmSync as rmSync$1, unlinkSync as unlinkSync$1, copyFileSync, watch, rmdirSync, readdirSync as readdirSync$1 } from 'fs';
4
+ import path__default, { join as join$1, dirname, basename, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { execFile, spawn as spawn$1, execSync as execSync$1, spawnSync } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
- import { existsSync, readFileSync, mkdirSync as mkdirSync$1, readdirSync, writeFileSync as writeFileSync$1, renameSync as renameSync$1, rmSync, appendFileSync, unlinkSync } from 'node:fs';
8
+ import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync, renameSync, rmSync, appendFileSync, unlinkSync } from 'node:fs';
9
9
  import { exec, spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
10
10
  import { promisify } from 'util';
11
+ import { join } from 'node:path';
11
12
  import { randomBytes, randomUUID, createHash } from 'node:crypto';
12
- import { join as join$1 } from 'node:path';
13
13
  import os, { homedir, platform } from 'node:os';
14
14
  import { EventEmitter } from 'node:events';
15
15
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
@@ -1121,6 +1121,367 @@ async function killDescendant(rootPid, targetPid, signal = "SIGTERM") {
1121
1121
  }
1122
1122
  }
1123
1123
 
1124
+ const FIELD_RANGES = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
1125
+ function parseField(token, [min, max]) {
1126
+ const set = /* @__PURE__ */ new Set();
1127
+ for (const part of token.split(",")) {
1128
+ let m;
1129
+ if (part === "*") {
1130
+ for (let i = min; i <= max; i++) set.add(i);
1131
+ } else if (m = part.match(/^\*\/(\d+)$/)) {
1132
+ const s = +m[1];
1133
+ for (let i = min; i <= max; i += s) set.add(i);
1134
+ } else if (m = part.match(/^(\d+)-(\d+)\/(\d+)$/)) {
1135
+ for (let i = +m[1]; i <= +m[2]; i += +m[3]) set.add(i);
1136
+ } else if (m = part.match(/^(\d+)-(\d+)$/)) {
1137
+ for (let i = +m[1]; i <= +m[2]; i++) set.add(i);
1138
+ } else if (m = part.match(/^(\d+)$/)) {
1139
+ set.add(+m[1]);
1140
+ } else throw new Error(`invalid cron field: "${token}"`);
1141
+ }
1142
+ for (const v of set) if (v < min || v > max) throw new Error(`cron value ${v} out of range [${min},${max}]`);
1143
+ return set;
1144
+ }
1145
+ function parseCron(expr) {
1146
+ const fields = String(expr).trim().split(/\s+/);
1147
+ if (fields.length !== 5) throw new Error(`cron must have 5 fields, got ${fields.length}: "${expr}"`);
1148
+ const [minute, hour, dom, month, dow] = fields.map((f, i) => parseField(f, FIELD_RANGES[i]));
1149
+ return { minute, hour, dom, month, dow, domRestricted: fields[2] !== "*", dowRestricted: fields[4] !== "*" };
1150
+ }
1151
+ function cronMatches(expr, date) {
1152
+ const c = typeof expr === "string" ? parseCron(expr) : expr;
1153
+ if (!c.minute.has(date.getMinutes())) return false;
1154
+ if (!c.hour.has(date.getHours())) return false;
1155
+ if (!c.month.has(date.getMonth() + 1)) return false;
1156
+ const domOk = c.dom.has(date.getDate());
1157
+ const dowOk = c.dow.has(date.getDay());
1158
+ if (c.domRestricted && c.dowRestricted) return domOk || dowOk;
1159
+ return domOk && dowOk;
1160
+ }
1161
+ function inZone(date, tz) {
1162
+ if (!tz) return date;
1163
+ try {
1164
+ const p = new Intl.DateTimeFormat("en-US", {
1165
+ timeZone: tz,
1166
+ hour12: false,
1167
+ year: "numeric",
1168
+ month: "2-digit",
1169
+ day: "2-digit",
1170
+ hour: "2-digit",
1171
+ minute: "2-digit"
1172
+ }).formatToParts(date).reduce((o, x) => {
1173
+ o[x.type] = x.value;
1174
+ return o;
1175
+ }, {});
1176
+ return new Date(+p.year, +p.month - 1, +p.day, +(p.hour === "24" ? 0 : p.hour), +p.minute);
1177
+ } catch {
1178
+ return date;
1179
+ }
1180
+ }
1181
+ function nextFire(expr, from, tz) {
1182
+ const c = parseCron(expr);
1183
+ const d = new Date(from.getTime());
1184
+ d.setSeconds(0, 0);
1185
+ d.setMinutes(d.getMinutes() + 1);
1186
+ for (let i = 0; i < 366 * 24 * 60; i++) {
1187
+ if (cronMatches(c, tz ? inZone(d, tz) : d)) return new Date(d.getTime());
1188
+ d.setMinutes(d.getMinutes() + 1);
1189
+ }
1190
+ return null;
1191
+ }
1192
+ function resolvePath(ctx, path) {
1193
+ return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], ctx);
1194
+ }
1195
+ function renderTemplate(template, ctx) {
1196
+ return String(template).replace(/\$\{([\w.$]+)\}/g, (_m, p) => {
1197
+ const v = resolvePath(ctx, p);
1198
+ return v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
1199
+ });
1200
+ }
1201
+ const TRIGGER_TYPES = ["manual", "schedule", "webhook", "api"];
1202
+ const ACTION_KINDS = ["message", "loop"];
1203
+ const OVERLAP = ["queue", "skip", "replace"];
1204
+ function validateRoutine(r) {
1205
+ const errs = [];
1206
+ if (!r || typeof r !== "object") return ["routine must be an object"];
1207
+ if (!r.session_id) errs.push("session_id required");
1208
+ if (!r.name) errs.push("name required");
1209
+ const t = r.trigger;
1210
+ if (!t || !TRIGGER_TYPES.includes(t.type)) errs.push(`trigger.type must be one of ${TRIGGER_TYPES.join("|")}`);
1211
+ if (t?.type === "schedule") {
1212
+ try {
1213
+ parseCron(t.cron);
1214
+ } catch (e) {
1215
+ errs.push(`trigger.cron: ${e.message}`);
1216
+ }
1217
+ if (t.missed && !["catchup", "skip"].includes(t.missed)) errs.push("trigger.missed must be catchup|skip");
1218
+ }
1219
+ const a = r.action;
1220
+ if (!a || !ACTION_KINDS.includes(a.kind)) errs.push(`action.kind must be one of ${ACTION_KINDS.join("|")}`);
1221
+ if (a?.kind === "message" && !a.template) errs.push("action.template required for message action");
1222
+ if (a?.kind === "loop" && !a.loop && !a.task_template) errs.push("action.loop or action.task_template required for loop action");
1223
+ if (r.overlap && !OVERLAP.includes(r.overlap)) errs.push(`overlap must be one of ${OVERLAP.join("|")}`);
1224
+ if (r.bind && r.bind !== "stateful" && r.bind !== "stateless") errs.push('bind must be "stateful" or "stateless"');
1225
+ if (r.bind === "stateless") {
1226
+ if (!r.dir) errs.push("a stateless routine requires a dir (the project folder to spawn the one-shot in)");
1227
+ if (a?.kind === "loop") errs.push('a stateless routine cannot use a loop action (needs a persistent session); use a message action or bind "stateful"');
1228
+ }
1229
+ if ((t?.type === "webhook" || t?.type === "api") && t.public && a?.kind === "loop")
1230
+ errs.push("a public webhook/api may not use a loop action (unauthenticated task injection) \u2014 use a message action or require a key");
1231
+ return errs;
1232
+ }
1233
+
1234
+ const genId$1 = () => "c_" + randomBytes(5).toString("hex");
1235
+ const genKey$1 = () => "ck_" + randomBytes(18).toString("base64url");
1236
+ const DEFAULT_TEMPLATE = `<inbound-message from="\${sender.name}" sender-type="\${sender.kind}" verified="\${sender.verified}" channel="\${channel.name}" call-id="\${call.id}" at="\${now}">
1237
+ \${body.message}
1238
+ </inbound-message>`;
1239
+ function validateChannel(c) {
1240
+ const errs = [];
1241
+ if (!c || typeof c !== "object") return ["channel must be an object"];
1242
+ if (!c.name) errs.push("name required");
1243
+ const m = c.identity?.mode;
1244
+ if (!["per-key", "caller-supplied", "fixed"].includes(m)) errs.push("identity.mode must be per-key|caller-supplied|fixed");
1245
+ if (m === "fixed" && !c.identity.fixed?.name) errs.push("identity.fixed.name required for fixed mode");
1246
+ if (!["message", "loop", "agent"].includes(c.action?.kind)) errs.push("action.kind must be message|loop|agent");
1247
+ const b = c.bind;
1248
+ const bindOk = b === "dynamic" || b === "stateless" || b && typeof b.session === "string" && !!b.session;
1249
+ if (!bindOk) errs.push('bind must be "dynamic", "stateless", or { session }');
1250
+ if (b === "stateless" && c.reply?.mode === "queue")
1251
+ errs.push('a stateless channel cannot use reply.mode "queue" (no persistent session to answer later)');
1252
+ if (b === "stateless" && (c.action?.kind === "loop" || c.action?.kind === "agent"))
1253
+ errs.push(`a stateless channel cannot use a ${c.action?.kind} action (needs a live session); use dynamic or { session }`);
1254
+ if (c.action?.kind === "loop" && m === "caller-supplied" && !c.identity?.shared_key)
1255
+ errs.push("a caller-supplied channel without a shared_key may not use a loop action (unauthenticated task injection)");
1256
+ if (c.action?.kind === "agent" && m === "caller-supplied" && !c.identity?.shared_key) {
1257
+ const MUTATING = ["run_bash", "send_to_session"];
1258
+ const ag = c.action.agent || {};
1259
+ const grantsMutating = (ag.tools || []).some((t) => MUTATING.includes(t)) || Object.values(ag.per_caller || {}).some((p) => (p?.tools || []).some((t) => MUTATING.includes(t)));
1260
+ if (grantsMutating) errs.push("a caller-supplied agent channel without a shared_key may not grant run_bash/send_to_session");
1261
+ }
1262
+ const unsafe = /[<>"'&\r\n]/;
1263
+ if (unsafe.test(c.name || "")) errs.push(`name must be single-line and not contain < > " ' &`);
1264
+ if (c.description && unsafe.test(c.description)) errs.push(`description must be single-line and not contain < > " ' &`);
1265
+ if (c.skill?.name && unsafe.test(c.skill.name)) errs.push(`skill.name must be single-line and not contain < > " ' &`);
1266
+ if (c.skill?.description && unsafe.test(c.skill.description)) errs.push(`skill.description must be single-line and not contain < > " ' &`);
1267
+ if (m === "fixed" && c.identity.fixed?.name && unsafe.test(c.identity.fixed.name)) errs.push(`identity.fixed.name must not contain < > " ' & or newlines`);
1268
+ for (const cl of c.identity?.callers || []) if (unsafe.test(cl.name || "")) errs.push(`caller name "${cl.name}" must not contain < > " ' & or newlines`);
1269
+ return errs;
1270
+ }
1271
+ function normalizeBind(c) {
1272
+ const b = c.bind;
1273
+ if (b === "dynamic" || b === "stateless" || b && typeof b.session === "string" && b.session) return c;
1274
+ c.bind = "dynamic";
1275
+ return c;
1276
+ }
1277
+ function bindMode(c) {
1278
+ const b = normalizeBind(c).bind;
1279
+ if (b === "stateless") return "stateless";
1280
+ if (b && typeof b.session === "string") return "fixed";
1281
+ return "dynamic";
1282
+ }
1283
+ function fixedSessionId(c) {
1284
+ const b = c.bind;
1285
+ return b && typeof b.session === "string" ? b.session : void 0;
1286
+ }
1287
+ class ChannelStore {
1288
+ dir;
1289
+ constructor(projectDir) {
1290
+ this.dir = join(projectDir, ".svamp", "channels");
1291
+ try {
1292
+ mkdirSync(this.dir, { recursive: true });
1293
+ } catch {
1294
+ }
1295
+ }
1296
+ _path(id) {
1297
+ return join(this.dir, `${id}.json`);
1298
+ }
1299
+ list() {
1300
+ if (!existsSync(this.dir)) return [];
1301
+ return readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
1302
+ try {
1303
+ return normalizeBind(JSON.parse(readFileSync(join(this.dir, f), "utf8")));
1304
+ } catch {
1305
+ return null;
1306
+ }
1307
+ }).filter((c) => !!c);
1308
+ }
1309
+ get(id) {
1310
+ try {
1311
+ return normalizeBind(JSON.parse(readFileSync(this._path(id), "utf8")));
1312
+ } catch {
1313
+ return null;
1314
+ }
1315
+ }
1316
+ save(channel) {
1317
+ const c = { enabled: true, bind: "dynamic", template: DEFAULT_TEMPLATE, last_calls: [], ...channel };
1318
+ if (!c.id) c.id = genId$1();
1319
+ const errs = validateChannel(c);
1320
+ if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
1321
+ mkdirSync(this.dir, { recursive: true });
1322
+ const tmp = this._path(c.id) + ".tmp";
1323
+ writeFileSync(tmp, JSON.stringify(c, null, 2));
1324
+ renameSync(tmp, this._path(c.id));
1325
+ return c;
1326
+ }
1327
+ remove(id) {
1328
+ const p = this._path(id);
1329
+ if (existsSync(p)) {
1330
+ rmSync(p);
1331
+ return true;
1332
+ }
1333
+ return false;
1334
+ }
1335
+ setEnabled(id, enabled) {
1336
+ const c = this.get(id);
1337
+ if (!c) return null;
1338
+ c.enabled = enabled;
1339
+ return this.save(c);
1340
+ }
1341
+ recordCall(id, entry) {
1342
+ const c = this.get(id);
1343
+ if (!c) return;
1344
+ c.last_calls = c.last_calls || [];
1345
+ c.last_calls.unshift({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
1346
+ c.last_calls = c.last_calls.slice(0, 20);
1347
+ this.save(c);
1348
+ }
1349
+ addCaller(id, name, kind = "agent") {
1350
+ const c = this.get(id);
1351
+ if (!c) return null;
1352
+ c.identity.callers = c.identity.callers || [];
1353
+ const caller = { name, kind, key: genKey$1() };
1354
+ c.identity.callers.push(caller);
1355
+ this.save(c);
1356
+ return caller;
1357
+ }
1358
+ }
1359
+ function routingSession(channel, ctx) {
1360
+ const mode = bindMode(channel);
1361
+ if (mode === "fixed") return fixedSessionId(channel);
1362
+ if (mode === "dynamic") return ctx?.session;
1363
+ return void 0;
1364
+ }
1365
+ function gatewayBase(channelsServiceId, baseUrl) {
1366
+ const slash = channelsServiceId.indexOf("/");
1367
+ if (slash < 0) return `${baseUrl.replace(/\/$/, "")}/services/${channelsServiceId}`;
1368
+ const ws = channelsServiceId.slice(0, slash);
1369
+ const clientSvc = channelsServiceId.slice(slash + 1);
1370
+ return `${baseUrl.replace(/\/$/, "")}/${ws}/services/${clientSvc}`;
1371
+ }
1372
+ function generateSkillBody(channel, ctx) {
1373
+ const svc = ctx?.channelsServiceId || "<workspace>/<machine>:channels";
1374
+ const base = ctx?.baseUrl || "https://hypha.aicell.io";
1375
+ const gw = ctx?.channelsServiceId ? gatewayBase(svc, base) : `${base}/<workspace>/services/<machine>:channels`;
1376
+ const skillUrl = `${gw}/skill?channel=${channel.id}`;
1377
+ const sendUrl = `${gw}/send`;
1378
+ const key = ctx?.key || "<your-key>";
1379
+ const isAgent = channel.action?.kind === "agent";
1380
+ const isQueue = channel.reply?.mode === "queue";
1381
+ const recvUrl = `${gw}/receive`;
1382
+ const hyphaOpen = (channel.identity?.hypha_allow || []).length > 0;
1383
+ const mode = bindMode(channel);
1384
+ const rSession = routingSession(channel, ctx);
1385
+ const sessionLineJs = rSession ? `
1386
+ session: "${rSession}",` : "";
1387
+ const sessionKv = rSession ? `, "session": "${rSession}"` : "";
1388
+ const bindNote = mode === "stateless" ? `**Stateless channel** \u2014 each call spawns a fresh, isolated one-shot session (cold-start: a few seconds), runs, replies, and closes. No shared memory between calls.` : mode === "fixed" ? `Routes to a **fixed session** (\`${rSession}\`); reachable only while that session is live.` : rSession ? `Routes to **session \`${rSession}\`** (the one this instruction was copied from); reachable only while it is live, else a fresh stateless run answers.` : `**Dynamic channel** \u2014 pass \`session: "<id>"\` to target a specific live session, else a fresh stateless run answers.`;
1389
+ const name = channel.skill?.name || channel.name;
1390
+ const desc = channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`;
1391
+ const replyNote = isAgent ? `This is a **WISE Agent** channel: \`send()\` runs a fast assistant against the session's tools/skills and returns its answer synchronously in the result \`reply\`.` : isQueue ? `This is an **async** channel: \`send()\` returns a \`correlationId\` and the agent replies later \u2014 poll \`receive()\` (or GET /receive \xB7 /events) for replies addressed to you.` : `Delivery is fire-and-forget \u2014 the message lands in the agent's inbox, tagged with your verified identity (\`from\`).`;
1392
+ const queueSection = isQueue ? `
1393
+
1394
+ ## Getting the reply (async)
1395
+ \`send()\` returns \`{ correlationId }\`. The agent answers later; retrieve replies addressed
1396
+ to you by **long-polling** \`receive\` with a cursor you advance each call:
1397
+ \`\`\`js
1398
+ const { correlationId } = await get_service("${svc}").send({ channel: "${channel.id}", message: "\u2026", from: "your-name" });
1399
+ let cursor = 0;
1400
+ while (true) {
1401
+ const r = await get_service("${svc}").receive({ channel: "${channel.id}", key: "${key}", cursor, wait: 25 });
1402
+ cursor = r.cursor;
1403
+ for (const reply of r.replies) if (reply.correlationId === correlationId) return reply.body;
1404
+ }
1405
+ \`\`\`
1406
+ **HTTP:** \`POST ${recvUrl}\` with \`{"kwargs": {"channel": "${channel.id}", "key": "${key}", "cursor": 0, "wait": 25}}\` (long-poll), or stream \`GET <channel-http>/channel/${channel.id}/events?key=${key}\` (SSE).` : "";
1407
+ const rpcLine = hyphaOpen ? `**Hypha RPC** \u2014 preferred. Your verified Hypha identity is accepted, no key needed:` : `**Hypha RPC** \u2014 verified identity:`;
1408
+ return `---
1409
+ name: ${name}
1410
+ description: ${desc}
1411
+ ---
1412
+ # ${name}
1413
+ ${channel.description || ""}
1414
+
1415
+ Self-contained guide for messaging the **${channel.name}** channel. ${replyNote}
1416
+ ${bindNote}
1417
+ This skill (with every value below already filled in) is served at:
1418
+ ${skillUrl}
1419
+
1420
+ ${rpcLine}
1421
+ \`\`\`js
1422
+ await get_service("${svc}").send({
1423
+ channel: "${channel.id}",
1424
+ message: "your message here",
1425
+ from: "your-name",${sessionLineJs}
1426
+ });
1427
+ \`\`\`
1428
+
1429
+ **HTTP** \u2014 any client, no Hypha SDK needed (Hypha gateway wraps args under \`kwargs\`):
1430
+ \`\`\`
1431
+ POST ${sendUrl}
1432
+ Content-Type: application/json
1433
+
1434
+ {"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"${sessionKv}}}
1435
+ \`\`\`${queueSection}`;
1436
+ }
1437
+
1438
+ function resolveSender(channel, input = {}) {
1439
+ const { key, from, hyphaUser, hyphaWorkspace, hyphaAnonymous } = input;
1440
+ const id = channel.identity || {};
1441
+ if (hyphaUser && !hyphaAnonymous && Array.isArray(id.hypha_allow) && id.hypha_allow.length) {
1442
+ if (id.hypha_allow.includes("*") || id.hypha_allow.includes(hyphaUser) || hyphaWorkspace && id.hypha_allow.includes(hyphaWorkspace))
1443
+ return { sender: { name: hyphaUser, kind: "agent", verified: true } };
1444
+ return { error: "caller not in hypha_allow" };
1445
+ }
1446
+ if (id.mode === "fixed") {
1447
+ if (!id.fixed?.name) return { error: "fixed identity not configured" };
1448
+ return { sender: { name: id.fixed.name, kind: id.fixed.kind, verified: true } };
1449
+ }
1450
+ if (id.mode === "per-key") {
1451
+ const caller = (id.callers || []).find((c) => c.key && c.key === key);
1452
+ if (!caller) return { error: "invalid or missing key" };
1453
+ return { sender: { name: caller.name, kind: caller.kind, verified: true } };
1454
+ }
1455
+ if (id.mode === "caller-supplied") {
1456
+ if (id.shared_key && key !== id.shared_key) return { error: "invalid key" };
1457
+ return { sender: { name: from || "anonymous", kind: "user", verified: false } };
1458
+ }
1459
+ return { error: "unsupported identity mode" };
1460
+ }
1461
+ const MAX_BODY = 16 * 1024;
1462
+ function xmlEscape(s) {
1463
+ return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1464
+ }
1465
+ const stripControl = (s) => String(s ?? "").replace(/[\x00-\x1f\x7f]/g, " ");
1466
+ function renderMessage(channel, { sender = {}, body = {}, query = {}, callId, now }) {
1467
+ const obj = (v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v;
1468
+ const escVal = (v) => xmlEscape(obj(v));
1469
+ const escAttr = (v) => xmlEscape(stripControl(obj(v)));
1470
+ const bodyEsc = {};
1471
+ for (const [k, v] of Object.entries(body)) bodyEsc[k] = k === "message" ? escVal(String(v ?? "").slice(0, MAX_BODY)) : escAttr(v);
1472
+ const queryEsc = {};
1473
+ for (const [k, v] of Object.entries(query)) if (k !== "key") queryEsc[k] = escAttr(v);
1474
+ const ctx = {
1475
+ sender: { name: escAttr(sender.name), kind: escAttr(sender.kind), verified: sender.verified === true },
1476
+ body: bodyEsc,
1477
+ query: queryEsc,
1478
+ channel: { name: escAttr(channel.name), id: escAttr(channel.id) },
1479
+ call: { id: escAttr(callId) },
1480
+ now: escAttr(now || (/* @__PURE__ */ new Date()).toISOString())
1481
+ };
1482
+ return renderTemplate(channel.template || DEFAULT_TEMPLATE, ctx);
1483
+ }
1484
+
1124
1485
  function getParamNames(fn) {
1125
1486
  const src = fn.toString();
1126
1487
  const match = src.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
@@ -1151,7 +1512,7 @@ function filterTerminalResponses(data) {
1151
1512
  return filtered;
1152
1513
  }
1153
1514
  function getMachineMetadataPath(svampHomeDir) {
1154
- return join(svampHomeDir, "machine-metadata.json");
1515
+ return join$1(svampHomeDir, "machine-metadata.json");
1155
1516
  }
1156
1517
  async function mintRealtimeEphemeralKey(baseUrl, apiKey, opts) {
1157
1518
  const realtimeBase = baseUrl || "https://api.openai.com";
@@ -1212,11 +1573,11 @@ function loadPersistedMachineMetadata(svampHomeDir) {
1212
1573
  }
1213
1574
  function savePersistedMachineMetadata(svampHomeDir, data) {
1214
1575
  try {
1215
- mkdirSync(svampHomeDir, { recursive: true });
1576
+ mkdirSync$1(svampHomeDir, { recursive: true });
1216
1577
  const filePath = getMachineMetadataPath(svampHomeDir);
1217
1578
  const tmpPath = filePath + ".tmp";
1218
- writeFileSync(tmpPath, JSON.stringify(data, null, 2));
1219
- renameSync(tmpPath, filePath);
1579
+ writeFileSync$1(tmpPath, JSON.stringify(data, null, 2));
1580
+ renameSync$1(tmpPath, filePath);
1220
1581
  } catch (err) {
1221
1582
  console.error("[HYPHA MACHINE] Failed to persist machine metadata:", err);
1222
1583
  }
@@ -1663,11 +2024,11 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1663
2024
  if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
1664
2025
  newSharing = { ...newSharing, owner: context.user.email };
1665
2026
  }
1666
- const ownerEmail = newSharing.owner || context?.user?.email || "";
2027
+ const ownerEmail2 = newSharing.owner || context?.user?.email || "";
1667
2028
  newSharing = {
1668
2029
  ...newSharing,
1669
2030
  allowedUsers: (newSharing.allowedUsers || []).map(
1670
- (u) => normalizeAllowedUser(u, ownerEmail)
2031
+ (u) => normalizeAllowedUser(u, ownerEmail2)
1671
2032
  ),
1672
2033
  publicAccess: null
1673
2034
  };
@@ -2166,7 +2527,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2166
2527
  const tunnels = handlers.tunnels;
2167
2528
  if (!tunnels) throw new Error("Tunnel management not available");
2168
2529
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2169
- const { FrpcTunnel } = await import('./frpc-Dn5pmk_f.mjs');
2530
+ const { FrpcTunnel } = await import('./frpc-CG7J02Ft.mjs');
2170
2531
  const tunnel = new FrpcTunnel({
2171
2532
  name: params.name,
2172
2533
  ports: params.ports,
@@ -2222,9 +2583,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2222
2583
  authorizeRequest(context, currentMetadata.sharing, "interact");
2223
2584
  const sm = handlers.serveManager;
2224
2585
  if (!sm) throw new Error("Serve manager not available");
2225
- const ownerEmail = params.ownerEmail || context?.user?.email || void 0;
2586
+ const ownerEmail2 = params.ownerEmail || context?.user?.email || void 0;
2226
2587
  const access = params.access || "owner";
2227
- return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail);
2588
+ return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail2);
2228
2589
  },
2229
2590
  /**
2230
2591
  * Apply a mount declaratively. Replaces existing mount with same name.
@@ -2235,7 +2596,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2235
2596
  authorizeRequest(context, currentMetadata.sharing, "interact");
2236
2597
  const sm = handlers.serveManager;
2237
2598
  if (!sm) throw new Error("Serve manager not available");
2238
- const ownerEmail = params.ownerEmail || context?.user?.email || void 0;
2599
+ const ownerEmail2 = params.ownerEmail || context?.user?.email || void 0;
2239
2600
  const access = params.access ?? "owner";
2240
2601
  return sm.applyMount({
2241
2602
  name: params.name,
@@ -2243,7 +2604,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2243
2604
  process: params.process,
2244
2605
  sessionId: params.sessionId,
2245
2606
  access,
2246
- ownerEmail
2607
+ ownerEmail: ownerEmail2
2247
2608
  });
2248
2609
  },
2249
2610
  /** Remove a mount from the shared static file server. */
@@ -2427,7 +2788,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2427
2788
  }
2428
2789
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
2429
2790
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
2430
- const { toolsForRole } = await import('./sideband-CNyGVxRy.mjs');
2791
+ const { toolsForRole } = await import('./sideband-CHClz1Yz.mjs');
2431
2792
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
2432
2793
  return fmt(r2);
2433
2794
  }
@@ -2462,6 +2823,89 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2462
2823
  }
2463
2824
  return void 0;
2464
2825
  };
2826
+ const readChannelFromDisk = (channelId) => {
2827
+ const seen = /* @__PURE__ */ new Set();
2828
+ for (const t of handlers.getTrackedSessions?.() || []) {
2829
+ const dir = t.directory;
2830
+ if (!dir || seen.has(dir)) continue;
2831
+ seen.add(dir);
2832
+ try {
2833
+ const c = new ChannelStore(dir).get(channelId);
2834
+ if (c && c.enabled !== false) return { c, dir };
2835
+ } catch {
2836
+ }
2837
+ }
2838
+ return void 0;
2839
+ };
2840
+ const liveSessionsInDir = (dir) => (handlers.getTrackedSessions?.() || []).filter((t) => t.directory === dir && handlers.getSessionRPCHandlers?.(t.sessionId)).map((t) => t.sessionId);
2841
+ const resolveChannel = (channelId, targetSession) => {
2842
+ const found = readChannelFromDisk(channelId);
2843
+ if (!found) return { error: "channel not found" };
2844
+ const { c, dir } = found;
2845
+ const mode = bindMode(c);
2846
+ const kind = c.action?.kind;
2847
+ if (kind === "agent" || kind === "loop") {
2848
+ const prefer = mode === "fixed" ? fixedSessionId(c) : mode === "dynamic" ? targetSession : void 0;
2849
+ const sid = prefer && handlers.getSessionRPCHandlers?.(prefer) ? prefer : liveSessionsInDir(dir)[0];
2850
+ if (!sid) {
2851
+ if (mode === "fixed") return { tier: "session", sessionId: fixedSessionId(c) || "?", stopped: true, c, dir };
2852
+ return { error: `no live session to host this ${kind} channel \u2014 start a session in ${dir}` };
2853
+ }
2854
+ return { tier: "session", sessionId: sid, rpc: handlers.getSessionRPCHandlers(sid), c, dir };
2855
+ }
2856
+ const target = mode === "fixed" ? fixedSessionId(c) : mode === "dynamic" ? targetSession : void 0;
2857
+ if (target) {
2858
+ const rpc = handlers.getSessionRPCHandlers?.(target);
2859
+ if (rpc) return { tier: "session", sessionId: target, rpc, c, dir };
2860
+ if (mode === "fixed") return { tier: "session", sessionId: target, stopped: true, c, dir };
2861
+ }
2862
+ return { tier: "stateless", c, dir };
2863
+ };
2864
+ const ownerEmail = currentMetadata.sharing?.owner;
2865
+ const ownerCtx = ownerEmail ? { user: { email: ownerEmail, id: ownerEmail } } : void 0;
2866
+ const selfMachine = {
2867
+ spawnSession: (opts) => handlers.spawnSession(opts),
2868
+ sessionRPC: async (sessionId, method, kwargs) => {
2869
+ const rpc = handlers.getSessionRPCHandlers?.(sessionId);
2870
+ if (!rpc) throw new Error(`Session ${sessionId} not found on this machine`);
2871
+ const handler = rpc[method];
2872
+ if (typeof handler !== "function") throw new Error(`Unknown session method: ${method}`);
2873
+ const paramNames = getParamNames(handler);
2874
+ const callArgs = paramNames.map((n) => n === "context" ? ownerCtx : kwargs?.[n] ?? void 0);
2875
+ return handler(...callArgs);
2876
+ }
2877
+ };
2878
+ const dispatchStateless = async (c, dir, kwargs, context) => {
2879
+ const u = context?.user;
2880
+ const r = resolveSender(c, {
2881
+ key: kwargs.key,
2882
+ from: kwargs.from,
2883
+ hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
2884
+ hyphaAnonymous: u?.is_anonymous === true,
2885
+ hyphaWorkspace: u?.scope?.current_workspace
2886
+ });
2887
+ if (r.error || !r.sender) return { error: r.error || "unauthorized" };
2888
+ const callId = "call_" + Math.random().toString(16).slice(2, 12);
2889
+ const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
2890
+ const { queryCore } = await import('./commands-Bj33Q-8L.mjs');
2891
+ const timeout = c.reply?.timeout_sec || 120;
2892
+ let result;
2893
+ try {
2894
+ result = await queryCore(selfMachine, dir, rendered, { permissionMode: "bypassPermissions", tag: "svamp-channel", timeout });
2895
+ } catch (e) {
2896
+ return { ok: false, call_id: callId, status: "error", error: e?.message || String(e) };
2897
+ } finally {
2898
+ if (result?.sessionId) {
2899
+ try {
2900
+ handlers.deleteSession?.(result.sessionId);
2901
+ } catch {
2902
+ }
2903
+ }
2904
+ }
2905
+ if (result.status === "error") return { ok: false, call_id: callId, status: "error", error: result.error };
2906
+ if (result.status === "permission-pending") return { ok: false, call_id: callId, status: "permission-pending", error: result.error };
2907
+ return { ok: true, call_id: callId, status: "completed", reply: result.response };
2908
+ };
2465
2909
  const channelsServiceInfo = await server.registerService(
2466
2910
  {
2467
2911
  id: "channels",
@@ -2501,179 +2945,79 @@ ${d?.error || "not found"}`;
2501
2945
  },
2502
2946
  send: async (kwargs = {}, context) => {
2503
2947
  trackInbound();
2504
- const rpc = await findChannelOwner(kwargs.channel);
2505
- if (!rpc?.channelSend) return { error: "channel not found" };
2506
- return rpc.channelSend({ channel: kwargs.channel, message: kwargs.message, from: kwargs.from, key: kwargs.key, reply_to: kwargs.reply_to }, context);
2948
+ const res = resolveChannel(kwargs.channel, kwargs.session);
2949
+ if ("error" in res) return { error: res.error };
2950
+ if (res.tier === "stateless") return dispatchStateless(res.c, res.dir, kwargs, context);
2951
+ if ("stopped" in res) return { error: `session ${res.sessionId.slice(0, 8)} is stopped \u2014 resume it to reach this channel` };
2952
+ return res.rpc.channelSend({ channel: kwargs.channel, message: kwargs.message, from: kwargs.from, key: kwargs.key, session: kwargs.session, reply_to: kwargs.reply_to }, context);
2507
2953
  },
2508
2954
  // Async reply retrieval for queue-mode channels — long-poll the channel outbox
2509
- // for replies addressed to the caller. Channel-identity auth (key/from).
2510
- receive: async (kwargs = {}, context) => {
2511
- trackInbound();
2512
- const rpc = await findChannelOwner(kwargs.channel);
2513
- if (!rpc?.channelReceive) return { error: "channel not found" };
2514
- return rpc.channelReceive({ channel: kwargs.channel, key: kwargs.key, from: kwargs.from, cursor: kwargs.cursor, correlationId: kwargs.correlationId, wait: kwargs.wait }, context);
2515
- }
2516
- },
2517
- { overwrite: true }
2518
- );
2519
- console.log(`[HYPHA MACHINE] Channels service registered: ${channelsServiceInfo.id} (type svamp-channels)`);
2520
- return {
2521
- serviceInfo,
2522
- notifySessionEvent: notifyListeners,
2523
- updateMetadata: (newMetadata) => {
2524
- currentMetadata = newMetadata;
2525
- metadataVersion++;
2526
- notifyListeners({
2527
- type: "update-machine",
2528
- machineId,
2529
- metadata: { value: currentMetadata, version: metadataVersion }
2530
- });
2531
- },
2532
- updateDaemonState: (newState) => {
2533
- currentDaemonState = newState;
2534
- daemonStateVersion++;
2535
- notifyListeners({
2536
- type: "update-machine",
2537
- machineId,
2538
- daemonState: { value: currentDaemonState, version: daemonStateVersion }
2539
- });
2540
- },
2541
- getLastInboundRpcAt: () => lastInboundRpcAt,
2542
- disconnect: async () => {
2543
- const toRemove = [...listeners];
2544
- for (const listener of toRemove) {
2545
- removeListener(listener, "disconnect");
2546
- }
2547
- await server.unregisterService(serviceInfo.id);
2548
- await server.unregisterService(channelsServiceInfo.id).catch(() => {
2549
- });
2550
- }
2551
- };
2552
- }
2553
-
2554
- const FIELD_RANGES = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
2555
- function parseField(token, [min, max]) {
2556
- const set = /* @__PURE__ */ new Set();
2557
- for (const part of token.split(",")) {
2558
- let m;
2559
- if (part === "*") {
2560
- for (let i = min; i <= max; i++) set.add(i);
2561
- } else if (m = part.match(/^\*\/(\d+)$/)) {
2562
- const s = +m[1];
2563
- for (let i = min; i <= max; i += s) set.add(i);
2564
- } else if (m = part.match(/^(\d+)-(\d+)\/(\d+)$/)) {
2565
- for (let i = +m[1]; i <= +m[2]; i += +m[3]) set.add(i);
2566
- } else if (m = part.match(/^(\d+)-(\d+)$/)) {
2567
- for (let i = +m[1]; i <= +m[2]; i++) set.add(i);
2568
- } else if (m = part.match(/^(\d+)$/)) {
2569
- set.add(+m[1]);
2570
- } else throw new Error(`invalid cron field: "${token}"`);
2571
- }
2572
- for (const v of set) if (v < min || v > max) throw new Error(`cron value ${v} out of range [${min},${max}]`);
2573
- return set;
2574
- }
2575
- function parseCron(expr) {
2576
- const fields = String(expr).trim().split(/\s+/);
2577
- if (fields.length !== 5) throw new Error(`cron must have 5 fields, got ${fields.length}: "${expr}"`);
2578
- const [minute, hour, dom, month, dow] = fields.map((f, i) => parseField(f, FIELD_RANGES[i]));
2579
- return { minute, hour, dom, month, dow, domRestricted: fields[2] !== "*", dowRestricted: fields[4] !== "*" };
2580
- }
2581
- function cronMatches(expr, date) {
2582
- const c = typeof expr === "string" ? parseCron(expr) : expr;
2583
- if (!c.minute.has(date.getMinutes())) return false;
2584
- if (!c.hour.has(date.getHours())) return false;
2585
- if (!c.month.has(date.getMonth() + 1)) return false;
2586
- const domOk = c.dom.has(date.getDate());
2587
- const dowOk = c.dow.has(date.getDay());
2588
- if (c.domRestricted && c.dowRestricted) return domOk || dowOk;
2589
- return domOk && dowOk;
2590
- }
2591
- function inZone(date, tz) {
2592
- if (!tz) return date;
2593
- try {
2594
- const p = new Intl.DateTimeFormat("en-US", {
2595
- timeZone: tz,
2596
- hour12: false,
2597
- year: "numeric",
2598
- month: "2-digit",
2599
- day: "2-digit",
2600
- hour: "2-digit",
2601
- minute: "2-digit"
2602
- }).formatToParts(date).reduce((o, x) => {
2603
- o[x.type] = x.value;
2604
- return o;
2605
- }, {});
2606
- return new Date(+p.year, +p.month - 1, +p.day, +(p.hour === "24" ? 0 : p.hour), +p.minute);
2607
- } catch {
2608
- return date;
2609
- }
2610
- }
2611
- function nextFire(expr, from, tz) {
2612
- const c = parseCron(expr);
2613
- const d = new Date(from.getTime());
2614
- d.setSeconds(0, 0);
2615
- d.setMinutes(d.getMinutes() + 1);
2616
- for (let i = 0; i < 366 * 24 * 60; i++) {
2617
- if (cronMatches(c, tz ? inZone(d, tz) : d)) return new Date(d.getTime());
2618
- d.setMinutes(d.getMinutes() + 1);
2619
- }
2620
- return null;
2621
- }
2622
- function resolvePath(ctx, path) {
2623
- return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], ctx);
2624
- }
2625
- function renderTemplate(template, ctx) {
2626
- return String(template).replace(/\$\{([\w.$]+)\}/g, (_m, p) => {
2627
- const v = resolvePath(ctx, p);
2628
- return v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
2629
- });
2630
- }
2631
- const TRIGGER_TYPES = ["manual", "schedule", "webhook", "api"];
2632
- const ACTION_KINDS = ["message", "loop"];
2633
- const OVERLAP = ["queue", "skip", "replace"];
2634
- function validateRoutine(r) {
2635
- const errs = [];
2636
- if (!r || typeof r !== "object") return ["routine must be an object"];
2637
- if (!r.session_id) errs.push("session_id required");
2638
- if (!r.name) errs.push("name required");
2639
- const t = r.trigger;
2640
- if (!t || !TRIGGER_TYPES.includes(t.type)) errs.push(`trigger.type must be one of ${TRIGGER_TYPES.join("|")}`);
2641
- if (t?.type === "schedule") {
2642
- try {
2643
- parseCron(t.cron);
2644
- } catch (e) {
2645
- errs.push(`trigger.cron: ${e.message}`);
2955
+ // for replies addressed to the caller. Routed to the session the message landed
2956
+ // in (the `session` target); stateless channels can't queue. Channel-identity auth.
2957
+ receive: async (kwargs = {}, context) => {
2958
+ trackInbound();
2959
+ const res = resolveChannel(kwargs.channel, kwargs.session);
2960
+ if ("error" in res) return { error: res.error };
2961
+ if (res.tier === "stateless") return { error: "stateless channels have no async replies (no persistent session)" };
2962
+ if ("stopped" in res) return { error: `session ${res.sessionId.slice(0, 8)} is stopped` };
2963
+ return res.rpc.channelReceive({ channel: kwargs.channel, key: kwargs.key, from: kwargs.from, cursor: kwargs.cursor, correlationId: kwargs.correlationId, wait: kwargs.wait }, context);
2964
+ }
2965
+ },
2966
+ { overwrite: true }
2967
+ );
2968
+ console.log(`[HYPHA MACHINE] Channels service registered: ${channelsServiceInfo.id} (type svamp-channels)`);
2969
+ return {
2970
+ serviceInfo,
2971
+ notifySessionEvent: notifyListeners,
2972
+ updateMetadata: (newMetadata) => {
2973
+ currentMetadata = newMetadata;
2974
+ metadataVersion++;
2975
+ notifyListeners({
2976
+ type: "update-machine",
2977
+ machineId,
2978
+ metadata: { value: currentMetadata, version: metadataVersion }
2979
+ });
2980
+ },
2981
+ updateDaemonState: (newState) => {
2982
+ currentDaemonState = newState;
2983
+ daemonStateVersion++;
2984
+ notifyListeners({
2985
+ type: "update-machine",
2986
+ machineId,
2987
+ daemonState: { value: currentDaemonState, version: daemonStateVersion }
2988
+ });
2989
+ },
2990
+ getLastInboundRpcAt: () => lastInboundRpcAt,
2991
+ disconnect: async () => {
2992
+ const toRemove = [...listeners];
2993
+ for (const listener of toRemove) {
2994
+ removeListener(listener, "disconnect");
2995
+ }
2996
+ await server.unregisterService(serviceInfo.id);
2997
+ await server.unregisterService(channelsServiceInfo.id).catch(() => {
2998
+ });
2646
2999
  }
2647
- if (t.missed && !["catchup", "skip"].includes(t.missed)) errs.push("trigger.missed must be catchup|skip");
2648
- }
2649
- const a = r.action;
2650
- if (!a || !ACTION_KINDS.includes(a.kind)) errs.push(`action.kind must be one of ${ACTION_KINDS.join("|")}`);
2651
- if (a?.kind === "message" && !a.template) errs.push("action.template required for message action");
2652
- if (a?.kind === "loop" && !a.loop && !a.task_template) errs.push("action.loop or action.task_template required for loop action");
2653
- if (r.overlap && !OVERLAP.includes(r.overlap)) errs.push(`overlap must be one of ${OVERLAP.join("|")}`);
2654
- if ((t?.type === "webhook" || t?.type === "api") && t.public && a?.kind === "loop")
2655
- errs.push("a public webhook/api may not use a loop action (unauthenticated task injection) \u2014 use a message action or require a key");
2656
- return errs;
3000
+ };
2657
3001
  }
2658
3002
 
2659
3003
  function defaultRoutinesDir() {
2660
- return process.env.SVAMP_ROUTINES_DIR || join$1(homedir(), ".svamp", "routines");
3004
+ return process.env.SVAMP_ROUTINES_DIR || join(homedir(), ".svamp", "routines");
2661
3005
  }
2662
- const genId$1 = () => "rt_" + randomBytes(5).toString("hex");
2663
- const genKey$1 = () => randomBytes(18).toString("base64url");
3006
+ const genId = () => "rt_" + randomBytes(5).toString("hex");
3007
+ const genKey = () => randomBytes(18).toString("base64url");
2664
3008
  class RoutineStore {
2665
3009
  dir;
2666
3010
  constructor(dir = defaultRoutinesDir()) {
2667
3011
  this.dir = dir;
2668
- mkdirSync$1(dir, { recursive: true });
3012
+ mkdirSync(dir, { recursive: true });
2669
3013
  }
2670
3014
  _path(id) {
2671
- return join$1(this.dir, `${id}.json`);
3015
+ return join(this.dir, `${id}.json`);
2672
3016
  }
2673
3017
  list(sessionId) {
2674
3018
  const all = readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
2675
3019
  try {
2676
- return JSON.parse(readFileSync(join$1(this.dir, f), "utf8"));
3020
+ return JSON.parse(readFileSync(join(this.dir, f), "utf8"));
2677
3021
  } catch {
2678
3022
  return null;
2679
3023
  }
@@ -2689,13 +3033,13 @@ class RoutineStore {
2689
3033
  }
2690
3034
  save(routine) {
2691
3035
  const r = { overlap: "queue", enabled: true, last_runs: [], ...routine };
2692
- if (!r.id) r.id = genId$1();
2693
- if ((r.trigger?.type === "webhook" || r.trigger?.type === "api") && !r.trigger.key) r.trigger.key = genKey$1();
3036
+ if (!r.id) r.id = genId();
3037
+ if ((r.trigger?.type === "webhook" || r.trigger?.type === "api") && !r.trigger.key) r.trigger.key = genKey();
2694
3038
  const errs = validateRoutine(r);
2695
3039
  if (errs.length) throw new Error("invalid routine: " + errs.join("; "));
2696
3040
  const tmp = this._path(r.id) + ".tmp";
2697
- writeFileSync$1(tmp, JSON.stringify(r, null, 2));
2698
- renameSync$1(tmp, this._path(r.id));
3041
+ writeFileSync(tmp, JSON.stringify(r, null, 2));
3042
+ renameSync(tmp, this._path(r.id));
2699
3043
  return r;
2700
3044
  }
2701
3045
  remove(id) {
@@ -2851,175 +3195,6 @@ class RoutineRunner {
2851
3195
  }
2852
3196
  }
2853
3197
 
2854
- const genId = () => "c_" + randomBytes(5).toString("hex");
2855
- const genKey = () => "ck_" + randomBytes(18).toString("base64url");
2856
- const DEFAULT_TEMPLATE = `<inbound-message from="\${sender.name}" sender-type="\${sender.kind}" verified="\${sender.verified}" channel="\${channel.name}" call-id="\${call.id}" at="\${now}">
2857
- \${body.message}
2858
- </inbound-message>`;
2859
- function validateChannel(c) {
2860
- const errs = [];
2861
- if (!c || typeof c !== "object") return ["channel must be an object"];
2862
- if (!c.name) errs.push("name required");
2863
- const m = c.identity?.mode;
2864
- if (!["per-key", "caller-supplied", "fixed"].includes(m)) errs.push("identity.mode must be per-key|caller-supplied|fixed");
2865
- if (m === "fixed" && !c.identity.fixed?.name) errs.push("identity.fixed.name required for fixed mode");
2866
- if (!["message", "loop", "agent"].includes(c.action?.kind)) errs.push("action.kind must be message|loop|agent");
2867
- if (c.bind !== "active" && !(c.bind && (c.bind.tag || c.bind.session))) errs.push('bind must be "active", {tag}, or {session}');
2868
- if (c.action?.kind === "loop" && m === "caller-supplied" && !c.identity?.shared_key)
2869
- errs.push("a caller-supplied channel without a shared_key may not use a loop action (unauthenticated task injection)");
2870
- if (c.action?.kind === "agent" && m === "caller-supplied" && !c.identity?.shared_key) {
2871
- const MUTATING = ["run_bash", "send_to_session"];
2872
- const ag = c.action.agent || {};
2873
- const grantsMutating = (ag.tools || []).some((t) => MUTATING.includes(t)) || Object.values(ag.per_caller || {}).some((p) => (p?.tools || []).some((t) => MUTATING.includes(t)));
2874
- if (grantsMutating) errs.push("a caller-supplied agent channel without a shared_key may not grant run_bash/send_to_session");
2875
- }
2876
- const unsafe = /[<>"'&\r\n]/;
2877
- if (unsafe.test(c.name || "")) errs.push(`name must be single-line and not contain < > " ' &`);
2878
- if (c.description && unsafe.test(c.description)) errs.push(`description must be single-line and not contain < > " ' &`);
2879
- if (c.skill?.name && unsafe.test(c.skill.name)) errs.push(`skill.name must be single-line and not contain < > " ' &`);
2880
- if (c.skill?.description && unsafe.test(c.skill.description)) errs.push(`skill.description must be single-line and not contain < > " ' &`);
2881
- if (m === "fixed" && c.identity.fixed?.name && unsafe.test(c.identity.fixed.name)) errs.push(`identity.fixed.name must not contain < > " ' & or newlines`);
2882
- for (const cl of c.identity?.callers || []) if (unsafe.test(cl.name || "")) errs.push(`caller name "${cl.name}" must not contain < > " ' & or newlines`);
2883
- return errs;
2884
- }
2885
- class ChannelStore {
2886
- dir;
2887
- constructor(projectDir) {
2888
- this.dir = join$1(projectDir, ".svamp", "channels");
2889
- try {
2890
- mkdirSync$1(this.dir, { recursive: true });
2891
- } catch {
2892
- }
2893
- }
2894
- _path(id) {
2895
- return join$1(this.dir, `${id}.json`);
2896
- }
2897
- list() {
2898
- if (!existsSync(this.dir)) return [];
2899
- return readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
2900
- try {
2901
- return JSON.parse(readFileSync(join$1(this.dir, f), "utf8"));
2902
- } catch {
2903
- return null;
2904
- }
2905
- }).filter((c) => !!c);
2906
- }
2907
- get(id) {
2908
- try {
2909
- return JSON.parse(readFileSync(this._path(id), "utf8"));
2910
- } catch {
2911
- return null;
2912
- }
2913
- }
2914
- save(channel) {
2915
- const c = { enabled: true, bind: "active", template: DEFAULT_TEMPLATE, last_calls: [], ...channel };
2916
- if (!c.id) c.id = genId();
2917
- const errs = validateChannel(c);
2918
- if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
2919
- mkdirSync$1(this.dir, { recursive: true });
2920
- const tmp = this._path(c.id) + ".tmp";
2921
- writeFileSync$1(tmp, JSON.stringify(c, null, 2));
2922
- renameSync$1(tmp, this._path(c.id));
2923
- return c;
2924
- }
2925
- remove(id) {
2926
- const p = this._path(id);
2927
- if (existsSync(p)) {
2928
- rmSync(p);
2929
- return true;
2930
- }
2931
- return false;
2932
- }
2933
- setEnabled(id, enabled) {
2934
- const c = this.get(id);
2935
- if (!c) return null;
2936
- c.enabled = enabled;
2937
- return this.save(c);
2938
- }
2939
- recordCall(id, entry) {
2940
- const c = this.get(id);
2941
- if (!c) return;
2942
- c.last_calls = c.last_calls || [];
2943
- c.last_calls.unshift({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
2944
- c.last_calls = c.last_calls.slice(0, 20);
2945
- this.save(c);
2946
- }
2947
- addCaller(id, name, kind = "agent") {
2948
- const c = this.get(id);
2949
- if (!c) return null;
2950
- c.identity.callers = c.identity.callers || [];
2951
- const caller = { name, kind, key: genKey() };
2952
- c.identity.callers.push(caller);
2953
- this.save(c);
2954
- return caller;
2955
- }
2956
- }
2957
- function gatewayBase(channelsServiceId, baseUrl) {
2958
- const slash = channelsServiceId.indexOf("/");
2959
- if (slash < 0) return `${baseUrl.replace(/\/$/, "")}/services/${channelsServiceId}`;
2960
- const ws = channelsServiceId.slice(0, slash);
2961
- const clientSvc = channelsServiceId.slice(slash + 1);
2962
- return `${baseUrl.replace(/\/$/, "")}/${ws}/services/${clientSvc}`;
2963
- }
2964
- function generateSkillBody(channel, ctx) {
2965
- const svc = ctx?.channelsServiceId || "<workspace>/<machine>:channels";
2966
- const base = ctx?.baseUrl || "https://hypha.aicell.io";
2967
- const gw = ctx?.channelsServiceId ? gatewayBase(svc, base) : `${base}/<workspace>/services/<machine>:channels`;
2968
- const skillUrl = `${gw}/skill?channel=${channel.id}`;
2969
- const sendUrl = `${gw}/send`;
2970
- const key = ctx?.key || "<your-key>";
2971
- const isAgent = channel.action?.kind === "agent";
2972
- const isQueue = channel.reply?.mode === "queue";
2973
- const recvUrl = `${gw}/receive`;
2974
- const hyphaOpen = (channel.identity?.hypha_allow || []).length > 0;
2975
- const name = channel.skill?.name || channel.name;
2976
- const desc = channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`;
2977
- const replyNote = isAgent ? `This is a **WISE Agent** channel: \`send()\` runs a fast assistant against the session's tools/skills and returns its answer synchronously in the result \`reply\`.` : isQueue ? `This is an **async** channel: \`send()\` returns a \`correlationId\` and the agent replies later \u2014 poll \`receive()\` (or GET /receive \xB7 /events) for replies addressed to you.` : `Delivery is fire-and-forget \u2014 the message lands in the agent's inbox, tagged with your verified identity (\`from\`).`;
2978
- const queueSection = isQueue ? `
2979
-
2980
- ## Getting the reply (async)
2981
- \`send()\` returns \`{ correlationId }\`. The agent answers later; retrieve replies addressed
2982
- to you by **long-polling** \`receive\` with a cursor you advance each call:
2983
- \`\`\`js
2984
- const { correlationId } = await get_service("${svc}").send({ channel: "${channel.id}", message: "\u2026", from: "your-name" });
2985
- let cursor = 0;
2986
- while (true) {
2987
- const r = await get_service("${svc}").receive({ channel: "${channel.id}", key: "${key}", cursor, wait: 25 });
2988
- cursor = r.cursor;
2989
- for (const reply of r.replies) if (reply.correlationId === correlationId) return reply.body;
2990
- }
2991
- \`\`\`
2992
- **HTTP:** \`POST ${recvUrl}\` with \`{"kwargs": {"channel": "${channel.id}", "key": "${key}", "cursor": 0, "wait": 25}}\` (long-poll), or stream \`GET <channel-http>/channel/${channel.id}/events?key=${key}\` (SSE).` : "";
2993
- const rpcLine = hyphaOpen ? `**Hypha RPC** \u2014 preferred. Your verified Hypha identity is accepted, no key needed:` : `**Hypha RPC** \u2014 verified identity:`;
2994
- return `---
2995
- name: ${name}
2996
- description: ${desc}
2997
- ---
2998
- # ${name}
2999
- ${channel.description || ""}
3000
-
3001
- Self-contained guide for messaging the **${channel.name}** channel. ${replyNote}
3002
- This skill (with every value below already filled in) is served at:
3003
- ${skillUrl}
3004
-
3005
- ${rpcLine}
3006
- \`\`\`js
3007
- await get_service("${svc}").send({
3008
- channel: "${channel.id}",
3009
- message: "your message here",
3010
- from: "your-name",
3011
- });
3012
- \`\`\`
3013
-
3014
- **HTTP** \u2014 any client, no Hypha SDK needed (Hypha gateway wraps args under \`kwargs\`):
3015
- \`\`\`
3016
- POST ${sendUrl}
3017
- Content-Type: application/json
3018
-
3019
- {"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"}}
3020
- \`\`\`${queueSection}`;
3021
- }
3022
-
3023
3198
  const MAX_PER_CHANNEL = 200;
3024
3199
  const TTL_MS = 60 * 60 * 1e3;
3025
3200
  class ChannelOutbox {
@@ -3028,11 +3203,11 @@ class ChannelOutbox {
3028
3203
  seqByChannel = /* @__PURE__ */ new Map();
3029
3204
  emitter = new EventEmitter();
3030
3205
  constructor(projectDir) {
3031
- const dir = join$1(projectDir, ".svamp", "channels");
3032
- this.file = join$1(dir, "_outbox.jsonl");
3206
+ const dir = join(projectDir, ".svamp", "channels");
3207
+ this.file = join(dir, "_outbox.jsonl");
3033
3208
  this.emitter.setMaxListeners(0);
3034
3209
  try {
3035
- mkdirSync$1(dir, { recursive: true });
3210
+ mkdirSync(dir, { recursive: true });
3036
3211
  } catch {
3037
3212
  }
3038
3213
  this._load();
@@ -3081,7 +3256,7 @@ class ChannelOutbox {
3081
3256
  appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
3082
3257
  } catch {
3083
3258
  try {
3084
- mkdirSync$1(join$1(this.file, ".."), { recursive: true });
3259
+ mkdirSync(join(this.file, ".."), { recursive: true });
3085
3260
  appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
3086
3261
  } catch {
3087
3262
  }
@@ -3145,62 +3320,15 @@ class ChannelOutbox {
3145
3320
  }
3146
3321
  });
3147
3322
  const tmp = this.file + ".tmp";
3148
- writeFileSync$1(tmp, kept.join("\n") + (kept.length ? "\n" : ""));
3149
- renameSync$1(tmp, this.file);
3323
+ writeFileSync(tmp, kept.join("\n") + (kept.length ? "\n" : ""));
3324
+ renameSync(tmp, this.file);
3150
3325
  } catch {
3151
3326
  }
3152
3327
  }
3153
3328
  }
3154
3329
 
3155
- function resolveSender(channel, input = {}) {
3156
- const { key, from, hyphaUser, hyphaWorkspace, hyphaAnonymous } = input;
3157
- const id = channel.identity || {};
3158
- if (hyphaUser && !hyphaAnonymous && Array.isArray(id.hypha_allow) && id.hypha_allow.length) {
3159
- if (id.hypha_allow.includes("*") || id.hypha_allow.includes(hyphaUser) || hyphaWorkspace && id.hypha_allow.includes(hyphaWorkspace))
3160
- return { sender: { name: hyphaUser, kind: "agent", verified: true } };
3161
- return { error: "caller not in hypha_allow" };
3162
- }
3163
- if (id.mode === "fixed") {
3164
- if (!id.fixed?.name) return { error: "fixed identity not configured" };
3165
- return { sender: { name: id.fixed.name, kind: id.fixed.kind, verified: true } };
3166
- }
3167
- if (id.mode === "per-key") {
3168
- const caller = (id.callers || []).find((c) => c.key && c.key === key);
3169
- if (!caller) return { error: "invalid or missing key" };
3170
- return { sender: { name: caller.name, kind: caller.kind, verified: true } };
3171
- }
3172
- if (id.mode === "caller-supplied") {
3173
- if (id.shared_key && key !== id.shared_key) return { error: "invalid key" };
3174
- return { sender: { name: from || "anonymous", kind: "user", verified: false } };
3175
- }
3176
- return { error: "unsupported identity mode" };
3177
- }
3178
- const MAX_BODY = 16 * 1024;
3179
- function xmlEscape(s) {
3180
- return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3181
- }
3182
- const stripControl = (s) => String(s ?? "").replace(/[\x00-\x1f\x7f]/g, " ");
3183
- function renderMessage(channel, { sender = {}, body = {}, query = {}, callId, now }) {
3184
- const obj = (v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v;
3185
- const escVal = (v) => xmlEscape(obj(v));
3186
- const escAttr = (v) => xmlEscape(stripControl(obj(v)));
3187
- const bodyEsc = {};
3188
- for (const [k, v] of Object.entries(body)) bodyEsc[k] = k === "message" ? escVal(String(v ?? "").slice(0, MAX_BODY)) : escAttr(v);
3189
- const queryEsc = {};
3190
- for (const [k, v] of Object.entries(query)) if (k !== "key") queryEsc[k] = escAttr(v);
3191
- const ctx = {
3192
- sender: { name: escAttr(sender.name), kind: escAttr(sender.kind), verified: sender.verified === true },
3193
- body: bodyEsc,
3194
- query: queryEsc,
3195
- channel: { name: escAttr(channel.name), id: escAttr(channel.id) },
3196
- call: { id: escAttr(callId) },
3197
- now: escAttr(now || (/* @__PURE__ */ new Date()).toISOString())
3198
- };
3199
- return renderTemplate(channel.template || DEFAULT_TEMPLATE, ctx);
3200
- }
3201
-
3202
3330
  function channelPublicView(c) {
3203
- return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind };
3331
+ return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind, bind: c.bind };
3204
3332
  }
3205
3333
  function isStructuredMessage(msg) {
3206
3334
  return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId || msg.channel);
@@ -3227,7 +3355,7 @@ ${msg.body}
3227
3355
  </svamp-message>`;
3228
3356
  }
3229
3357
  function loadMessages(messagesDir, sessionId) {
3230
- const filePath = join$1(messagesDir, "messages.jsonl");
3358
+ const filePath = join(messagesDir, "messages.jsonl");
3231
3359
  if (!existsSync(filePath)) return [];
3232
3360
  try {
3233
3361
  const lines = readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
@@ -3244,7 +3372,7 @@ function loadMessages(messagesDir, sessionId) {
3244
3372
  }
3245
3373
  }
3246
3374
  function loadMessagesFromDisk(messagesDir, afterSeq, limit) {
3247
- const filePath = join$1(messagesDir, "messages.jsonl");
3375
+ const filePath = join(messagesDir, "messages.jsonl");
3248
3376
  if (!existsSync(filePath)) return { messages: [], hasMore: false };
3249
3377
  try {
3250
3378
  const lines = readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
@@ -3263,7 +3391,7 @@ function loadMessagesFromDisk(messagesDir, afterSeq, limit) {
3263
3391
  }
3264
3392
  }
3265
3393
  function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
3266
- const filePath = join$1(messagesDir, "messages.jsonl");
3394
+ const filePath = join(messagesDir, "messages.jsonl");
3267
3395
  if (!existsSync(filePath)) return { messages: [], hasMore: false };
3268
3396
  try {
3269
3397
  const lines = readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
@@ -3282,7 +3410,7 @@ function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
3282
3410
  }
3283
3411
  }
3284
3412
  function countMessagesOnDisk(messagesDir, fallbackInMemory) {
3285
- const filePath = join$1(messagesDir, "messages.jsonl");
3413
+ const filePath = join(messagesDir, "messages.jsonl");
3286
3414
  if (!existsSync(filePath)) return fallbackInMemory;
3287
3415
  try {
3288
3416
  const data = readFileSync(filePath, "utf-8");
@@ -3296,9 +3424,9 @@ function countMessagesOnDisk(messagesDir, fallbackInMemory) {
3296
3424
  }
3297
3425
  function appendMessage(messagesDir, sessionId, msg) {
3298
3426
  try {
3299
- const filePath = join$1(messagesDir, "messages.jsonl");
3427
+ const filePath = join(messagesDir, "messages.jsonl");
3300
3428
  if (!existsSync(messagesDir)) {
3301
- mkdirSync$1(messagesDir, { recursive: true });
3429
+ mkdirSync(messagesDir, { recursive: true });
3302
3430
  }
3303
3431
  appendFileSync(filePath, JSON.stringify(msg) + "\n");
3304
3432
  } catch (err) {
@@ -3434,7 +3562,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3434
3562
  const skillCtxFor = (c) => ({
3435
3563
  channelsServiceId,
3436
3564
  baseUrl: channelsBaseUrl,
3437
- key: c.identity?.shared_key || c.identity?.callers?.[0]?.key
3565
+ key: c.identity?.shared_key || c.identity?.callers?.[0]?.key,
3566
+ // The session this skill is being copied FROM — baked into `dynamic` channel
3567
+ // instructions as the routing target so the copied link lands in THIS session.
3568
+ session: sessionId
3438
3569
  });
3439
3570
  const skillUrlsFor = (c) => {
3440
3571
  if (!channelsServiceId) return { skillUrl: void 0, sendUrl: void 0 };
@@ -3584,7 +3715,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3584
3715
  saveRoutine: async (routine, context) => {
3585
3716
  authorizeRequest(context, metadata.sharing, "admin");
3586
3717
  try {
3587
- const saved = routineStore.save({ ...routine, session_id: sessionId });
3718
+ const saved = routineStore.save({ ...routine, session_id: sessionId, dir: routine.dir || metadata.path });
3588
3719
  syncRoutinesToMetadata();
3589
3720
  return { success: true, routine: saved };
3590
3721
  } catch (e) {
@@ -3625,6 +3756,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3625
3756
  saveChannel: async (channel, context) => {
3626
3757
  authorizeRequest(context, metadata.sharing, "admin");
3627
3758
  try {
3759
+ const b = channel.bind;
3760
+ if (b && typeof b === "object" && (b.session === "current" || b.session === "" || b.session == null)) {
3761
+ channel = { ...channel, bind: { session: sessionId } };
3762
+ }
3628
3763
  const saved = channelStore.save(channel);
3629
3764
  syncChannelsToMetadata();
3630
3765
  return { success: true, channel: saved };
@@ -3656,6 +3791,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3656
3791
  const c = channelStore.get(id);
3657
3792
  if (!c) return { error: "not found" };
3658
3793
  const { skillUrl, sendUrl } = skillUrlsFor(c);
3794
+ const mode = bindMode(c);
3795
+ const routeSession = mode === "fixed" ? fixedSessionId(c) : mode === "dynamic" ? sessionId : void 0;
3659
3796
  return {
3660
3797
  skill: generateSkillBody(c, skillCtxFor(c)),
3661
3798
  channelsServiceId,
@@ -3664,7 +3801,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3664
3801
  skillUrl,
3665
3802
  sendUrl,
3666
3803
  key: c.identity?.shared_key || c.identity?.callers?.[0]?.key,
3667
- hyphaKeyless: (c.identity?.hypha_allow || []).length > 0
3804
+ hyphaKeyless: (c.identity?.hypha_allow || []).length > 0,
3805
+ bind: c.bind,
3806
+ bindMode: mode,
3807
+ session: routeSession
3668
3808
  };
3669
3809
  },
3670
3810
  // Public channel discovery (no session authz — channels are deliberately
@@ -4186,9 +4326,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
4186
4326
  messages.length = 0;
4187
4327
  nextSeq = 1;
4188
4328
  if (options?.messagesDir) {
4189
- const filePath = join$1(options.messagesDir, "messages.jsonl");
4329
+ const filePath = join(options.messagesDir, "messages.jsonl");
4190
4330
  try {
4191
- writeFileSync$1(filePath, "");
4331
+ writeFileSync(filePath, "");
4192
4332
  } catch {
4193
4333
  }
4194
4334
  }
@@ -4216,7 +4356,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
4216
4356
  return { store, rpcHandlers };
4217
4357
  }
4218
4358
 
4219
- const SVAMP_HOME$2 = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
4359
+ const SVAMP_HOME$2 = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
4220
4360
  const num = (key, def) => {
4221
4361
  const v = Number(process.env[key]);
4222
4362
  return Number.isFinite(v) && v > 0 ? v : def;
@@ -4229,19 +4369,19 @@ const BREAKER_MAX_WAKES = num("SVAMP_INBOX_BREAKER_MAX_WAKES", 30);
4229
4369
  const BREAKER_COOLDOWN_MS = num("SVAMP_INBOX_BREAKER_COOLDOWN_MS", 12e4);
4230
4370
  const CTX_STALE_MS = num("SVAMP_INBOX_CTX_STALE_MS", 30 * 6e4);
4231
4371
  function ctxPath(sessionId) {
4232
- return join$1(SVAMP_HOME$2, "inbound", `${sessionId}.json`);
4372
+ return join(SVAMP_HOME$2, "inbound", `${sessionId}.json`);
4233
4373
  }
4234
4374
  function writeInboundContext(sessionId, ctx) {
4235
4375
  try {
4236
4376
  const p = ctxPath(sessionId);
4237
- mkdirSync$1(join$1(SVAMP_HOME$2, "inbound"), { recursive: true });
4377
+ mkdirSync(join(SVAMP_HOME$2, "inbound"), { recursive: true });
4238
4378
  const tmp = p + ".tmp";
4239
- writeFileSync$1(tmp, JSON.stringify({ ...ctx, deliveredAt: Date.now() }));
4379
+ writeFileSync(tmp, JSON.stringify({ ...ctx, deliveredAt: Date.now() }));
4240
4380
  try {
4241
4381
  rmSync(p, { force: true });
4242
4382
  } catch {
4243
4383
  }
4244
- writeFileSync$1(p, readFileSync(tmp));
4384
+ writeFileSync(p, readFileSync(tmp));
4245
4385
  try {
4246
4386
  rmSync(tmp, { force: true });
4247
4387
  } catch {
@@ -4528,8 +4668,8 @@ class SessionArtifactSync {
4528
4668
  this.syncing = true;
4529
4669
  try {
4530
4670
  const artifactAlias = `session-${sessionId}`;
4531
- const sessionJsonPath = join$1(sessionsDir, "session.json");
4532
- const messagesPath = join$1(sessionsDir, "messages.jsonl");
4671
+ const sessionJsonPath = join(sessionsDir, "session.json");
4672
+ const messagesPath = join(sessionsDir, "messages.jsonl");
4533
4673
  let sessionData = null;
4534
4674
  if (existsSync(sessionJsonPath)) {
4535
4675
  try {
@@ -4639,18 +4779,18 @@ class SessionArtifactSync {
4639
4779
  artifact_id: artifactAlias,
4640
4780
  _rkwargs: true
4641
4781
  });
4642
- if (!existsSync(targetDir)) mkdirSync$1(targetDir, { recursive: true });
4782
+ if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
4643
4783
  try {
4644
4784
  const data = await this.downloadFile(artifact.id, "session.json");
4645
4785
  if (data) {
4646
- writeFileSync$1(join$1(targetDir, "session.json"), data);
4786
+ writeFileSync(join(targetDir, "session.json"), data);
4647
4787
  }
4648
4788
  } catch {
4649
4789
  }
4650
4790
  try {
4651
4791
  const data = await this.downloadFile(artifact.id, "messages.jsonl");
4652
4792
  if (data) {
4653
- writeFileSync$1(join$1(targetDir, "messages.jsonl"), data);
4793
+ writeFileSync(join(targetDir, "messages.jsonl"), data);
4654
4794
  }
4655
4795
  } catch {
4656
4796
  }
@@ -4703,13 +4843,13 @@ class SessionArtifactSync {
4703
4843
  */
4704
4844
  async syncAll(svampHome, machineId) {
4705
4845
  if (!this.initialized) return;
4706
- const indexFile = join$1(svampHome, "sessions-index.json");
4846
+ const indexFile = join(svampHome, "sessions-index.json");
4707
4847
  if (!existsSync(indexFile)) return;
4708
4848
  try {
4709
4849
  const index = JSON.parse(readFileSync(indexFile, "utf-8"));
4710
4850
  for (const [sessionId, entry] of Object.entries(index)) {
4711
- const sessionDir = join$1(entry.directory, ".svamp", sessionId);
4712
- if (existsSync(join$1(sessionDir, "session.json"))) {
4851
+ const sessionDir = join(entry.directory, ".svamp", sessionId);
4852
+ if (existsSync(join(sessionDir, "session.json"))) {
4713
4853
  await this.syncSession(sessionId, sessionDir, void 0, machineId);
4714
4854
  }
4715
4855
  }
@@ -5025,7 +5165,7 @@ var DefaultTransport$1 = /*#__PURE__*/Object.freeze({
5025
5165
 
5026
5166
  function expandHome(p) {
5027
5167
  if (p === "~") return homedir();
5028
- if (p.startsWith("~/")) return join$1(homedir(), p.slice(2));
5168
+ if (p.startsWith("~/")) return join(homedir(), p.slice(2));
5029
5169
  return p;
5030
5170
  }
5031
5171
  function wrapWithIsolation(originalCommand, originalArgs, config) {
@@ -5052,11 +5192,11 @@ function wrapWithNono(command, args, config) {
5052
5192
  nonoArgs.push("--allow-cwd");
5053
5193
  if (config.credentialStagingPath) {
5054
5194
  env.HOME = config.credentialStagingPath;
5055
- const realLocalDir = join$1(homedir(), ".local");
5195
+ const realLocalDir = join(homedir(), ".local");
5056
5196
  if (existsSync(realLocalDir)) {
5057
5197
  nonoArgs.push("--read", realLocalDir);
5058
5198
  }
5059
- const realKeychainDir = join$1(homedir(), "Library", "Keychains");
5199
+ const realKeychainDir = join(homedir(), "Library", "Keychains");
5060
5200
  if (existsSync(realKeychainDir)) {
5061
5201
  nonoArgs.push("--read", realKeychainDir);
5062
5202
  }
@@ -5117,9 +5257,9 @@ function wrapWithContainer(runtime, command, args, config) {
5117
5257
  config.workspacePath
5118
5258
  ];
5119
5259
  if (config.credentialStagingPath) {
5120
- const stagedClaudeDir = join$1(config.credentialStagingPath, ".claude");
5260
+ const stagedClaudeDir = join(config.credentialStagingPath, ".claude");
5121
5261
  containerArgs.push("-v", `${stagedClaudeDir}:/root/.claude:ro`);
5122
- const stagedGitconfig = join$1(config.credentialStagingPath, ".gitconfig");
5262
+ const stagedGitconfig = join(config.credentialStagingPath, ".gitconfig");
5123
5263
  containerArgs.push("-v", `${stagedGitconfig}:/root/.gitconfig:ro`);
5124
5264
  containerArgs.push("-e", "HOME=/root");
5125
5265
  }
@@ -6868,8 +7008,8 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
6868
7008
  });
6869
7009
 
6870
7010
  const execFileAsync = promisify$1(execFile$1);
6871
- const SVAMP_TOOLS_DIR = join$1(homedir(), ".svamp", "tools");
6872
- const SVAMP_BIN_DIR = join$1(SVAMP_TOOLS_DIR, "bin");
7011
+ const SVAMP_TOOLS_DIR = join(homedir(), ".svamp", "tools");
7012
+ const SVAMP_BIN_DIR = join(SVAMP_TOOLS_DIR, "bin");
6873
7013
  async function checkCommand(command, versionArgs) {
6874
7014
  try {
6875
7015
  const { stdout } = await execFileAsync(command, versionArgs, {
@@ -6879,7 +7019,7 @@ async function checkCommand(command, versionArgs) {
6879
7019
  return { found: true, version, path: command };
6880
7020
  } catch {
6881
7021
  }
6882
- const localPath = join$1(SVAMP_BIN_DIR, command);
7022
+ const localPath = join(SVAMP_BIN_DIR, command);
6883
7023
  try {
6884
7024
  const { stdout } = await execFileAsync(localPath, versionArgs, {
6885
7025
  timeout: 5e3
@@ -6946,7 +7086,7 @@ async function installNono() {
6946
7086
  const downloadUrl = asset.browser_download_url;
6947
7087
  console.log(`[isolation] Downloading nono ${version} from ${downloadUrl}...`);
6948
7088
  await mkdir(SVAMP_BIN_DIR, { recursive: true });
6949
- const tarball = join$1(SVAMP_BIN_DIR, assetName);
7089
+ const tarball = join(SVAMP_BIN_DIR, assetName);
6950
7090
  await execFileAsync("curl", [
6951
7091
  "-fsSL",
6952
7092
  "-o",
@@ -6963,7 +7103,7 @@ async function installNono() {
6963
7103
  ], { timeout: 15e3 });
6964
7104
  await rm(tarball, { force: true }).catch(() => {
6965
7105
  });
6966
- const nonoPath = join$1(SVAMP_BIN_DIR, "nono");
7106
+ const nonoPath = join(SVAMP_BIN_DIR, "nono");
6967
7107
  await chmod(nonoPath, 493);
6968
7108
  try {
6969
7109
  await access(nonoPath);
@@ -7005,8 +7145,8 @@ async function parseIsolationTestOutput(stdout, probeFile) {
7005
7145
  }
7006
7146
  async function verifyNonoIsolation(binaryPath) {
7007
7147
  const testBase = "/tmp";
7008
- const workDir = await mkdtemp(join$1(testBase, "svamp-iso-work-"));
7009
- const probeFile = join$1(homedir(), `.svamp-iso-probe-${process.pid}`);
7148
+ const workDir = await mkdtemp(join(testBase, "svamp-iso-work-"));
7149
+ const probeFile = join(homedir(), `.svamp-iso-probe-${process.pid}`);
7010
7150
  try {
7011
7151
  const testScript = [
7012
7152
  `echo ok > "${workDir}/test" 2>/dev/null; W=$?`,
@@ -7132,7 +7272,7 @@ async function detectIsolationCapabilities() {
7132
7272
  return { available, preferred, details };
7133
7273
  }
7134
7274
 
7135
- const STAGED_HOMES_DIR = join$1(homedir(), ".svamp", "staged-homes");
7275
+ const STAGED_HOMES_DIR = join(homedir(), ".svamp", "staged-homes");
7136
7276
  const SENSITIVE_ENV_VARS = [
7137
7277
  "HYPHA_TOKEN",
7138
7278
  "HYPHA_CLIENT_ID",
@@ -7148,23 +7288,23 @@ const SENSITIVE_ENV_VARS = [
7148
7288
  ];
7149
7289
  async function stageCredentialsForSharing(sessionId) {
7150
7290
  const realHome = homedir();
7151
- const realClaudeDir = join$1(realHome, ".claude");
7291
+ const realClaudeDir = join(realHome, ".claude");
7152
7292
  await mkdir(STAGED_HOMES_DIR, { recursive: true });
7153
- const tmpHome = join$1(STAGED_HOMES_DIR, sessionId);
7293
+ const tmpHome = join(STAGED_HOMES_DIR, sessionId);
7154
7294
  await mkdir(tmpHome, { recursive: true });
7155
- const stagedClaudeDir = join$1(tmpHome, ".claude");
7295
+ const stagedClaudeDir = join(tmpHome, ".claude");
7156
7296
  await mkdir(stagedClaudeDir, { recursive: true });
7157
7297
  const credentialFiles = ["credentials.json", ".credentials.json"];
7158
7298
  let credentialsCopied = false;
7159
7299
  for (const file of credentialFiles) {
7160
7300
  try {
7161
- await copyFile(join$1(realClaudeDir, file), join$1(stagedClaudeDir, file));
7301
+ await copyFile(join(realClaudeDir, file), join(stagedClaudeDir, file));
7162
7302
  credentialsCopied = true;
7163
7303
  } catch {
7164
7304
  }
7165
7305
  }
7166
7306
  if (!credentialsCopied && platform() === "darwin") {
7167
- const stagedCredFile = join$1(stagedClaudeDir, ".credentials.json");
7307
+ const stagedCredFile = join(stagedClaudeDir, ".credentials.json");
7168
7308
  const hasExistingCredentials = existsSync(stagedCredFile);
7169
7309
  if (!hasExistingCredentials) {
7170
7310
  try {
@@ -7182,25 +7322,25 @@ async function stageCredentialsForSharing(sessionId) {
7182
7322
  }
7183
7323
  }
7184
7324
  try {
7185
- await copyFile(join$1(realHome, ".gitconfig"), join$1(tmpHome, ".gitconfig"));
7325
+ await copyFile(join(realHome, ".gitconfig"), join(tmpHome, ".gitconfig"));
7186
7326
  } catch {
7187
7327
  }
7188
7328
  try {
7189
7329
  await copyFile(
7190
- join$1(realHome, ".gitignore_global"),
7191
- join$1(tmpHome, ".gitignore_global")
7330
+ join(realHome, ".gitignore_global"),
7331
+ join(tmpHome, ".gitignore_global")
7192
7332
  );
7193
7333
  } catch {
7194
7334
  }
7195
- const claudeJsonPath = join$1(tmpHome, ".claude.json");
7335
+ const claudeJsonPath = join(tmpHome, ".claude.json");
7196
7336
  if (!existsSync(claudeJsonPath)) {
7197
7337
  try {
7198
7338
  await writeFile(claudeJsonPath, "{}");
7199
7339
  } catch {
7200
7340
  }
7201
7341
  }
7202
- const realSkillsDir = join$1(realClaudeDir, "skills");
7203
- const stagedSkillsDir = join$1(stagedClaudeDir, "skills");
7342
+ const realSkillsDir = join(realClaudeDir, "skills");
7343
+ const stagedSkillsDir = join(stagedClaudeDir, "skills");
7204
7344
  try {
7205
7345
  await copyDirRecursive(realSkillsDir, stagedSkillsDir);
7206
7346
  } catch {
@@ -7234,7 +7374,7 @@ async function sweepOrphanedStagedHomes(activeSessionIds) {
7234
7374
  for (const entry of entries) {
7235
7375
  if (!entry.isDirectory()) continue;
7236
7376
  if (active.has(entry.name)) continue;
7237
- const path = join$1(STAGED_HOMES_DIR, entry.name);
7377
+ const path = join(STAGED_HOMES_DIR, entry.name);
7238
7378
  try {
7239
7379
  await rm(path, { recursive: true, force: true });
7240
7380
  removed.push(entry.name);
@@ -7249,8 +7389,8 @@ async function copyDirRecursive(src, dest) {
7249
7389
  await mkdir(dest, { recursive: true });
7250
7390
  const entries = await readdir(src, { withFileTypes: true });
7251
7391
  for (const entry of entries) {
7252
- const srcPath = join$1(src, entry.name);
7253
- const destPath = join$1(dest, entry.name);
7392
+ const srcPath = join(src, entry.name);
7393
+ const destPath = join(dest, entry.name);
7254
7394
  if (entry.isDirectory()) {
7255
7395
  await copyDirRecursive(srcPath, destPath);
7256
7396
  } else if (entry.isFile()) {
@@ -7302,8 +7442,8 @@ function resolveHyphaProxyUrl() {
7302
7442
  }
7303
7443
  }
7304
7444
  function envFilePath() {
7305
- const svampHome = process.env.SVAMP_HOME || join$1(homedir(), ".svamp");
7306
- return join$1(svampHome, ".env");
7445
+ const svampHome = process.env.SVAMP_HOME || join(homedir(), ".svamp");
7446
+ return join(svampHome, ".env");
7307
7447
  }
7308
7448
  function readEnvLines() {
7309
7449
  const file = envFilePath();
@@ -7312,12 +7452,12 @@ function readEnvLines() {
7312
7452
  }
7313
7453
  function writeEnvLines(lines) {
7314
7454
  const file = envFilePath();
7315
- const dir = join$1(file, "..");
7316
- if (!existsSync(dir)) mkdirSync$1(dir, { recursive: true });
7455
+ const dir = join(file, "..");
7456
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
7317
7457
  while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
7318
7458
  lines.pop();
7319
7459
  }
7320
- writeFileSync$1(file, lines.join("\n") + "\n", "utf-8");
7460
+ writeFileSync(file, lines.join("\n") + "\n", "utf-8");
7321
7461
  }
7322
7462
  function updateEnvFile(updates) {
7323
7463
  const lines = readEnvLines();
@@ -7468,10 +7608,10 @@ var claudeAuth = /*#__PURE__*/Object.freeze({
7468
7608
  });
7469
7609
 
7470
7610
  function svampHome() {
7471
- return process.env.SVAMP_HOME || join(homedir$1(), ".svamp");
7611
+ return process.env.SVAMP_HOME || join$1(homedir$1(), ".svamp");
7472
7612
  }
7473
7613
  function cacheFile() {
7474
- return join(svampHome(), "instance-config.json");
7614
+ return join$1(svampHome(), "instance-config.json");
7475
7615
  }
7476
7616
  const CONFIG_FILENAME = "svamp.json";
7477
7617
  let _config = null;
@@ -7514,8 +7654,8 @@ function readCache() {
7514
7654
  }
7515
7655
  function writeCache(cfg) {
7516
7656
  try {
7517
- mkdirSync(svampHome(), { recursive: true });
7518
- writeFileSync(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
7657
+ mkdirSync$1(svampHome(), { recursive: true });
7658
+ writeFileSync$1(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
7519
7659
  } catch {
7520
7660
  }
7521
7661
  }
@@ -8033,7 +8173,7 @@ function escapeHtml(s) {
8033
8173
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
8034
8174
  }
8035
8175
 
8036
- const SKILLS_DIR = join(os$1.homedir(), ".claude", "skills");
8176
+ const SKILLS_DIR = join$1(os$1.homedir(), ".claude", "skills");
8037
8177
  function getSkillsWorkspaceName() {
8038
8178
  return getSkillsWorkspace();
8039
8179
  }
@@ -8764,7 +8904,7 @@ const REFRESH_BUFFER_MS = 60 * 60 * 1e3;
8764
8904
  const OAUTH_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
8765
8905
  const REFRESH_TIMEOUT_MS = 15e3;
8766
8906
  function getCredentialsPath() {
8767
- return join$1(homedir(), ".claude", ".credentials.json");
8907
+ return join(homedir(), ".claude", ".credentials.json");
8768
8908
  }
8769
8909
  async function readCredentials() {
8770
8910
  const path = getCredentialsPath();
@@ -8925,14 +9065,14 @@ function resolveContextWindow(opts) {
8925
9065
  return candidate;
8926
9066
  }
8927
9067
 
8928
- const SVAMP_HOME$1 = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
9068
+ const SVAMP_HOME$1 = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
8929
9069
  function generateHookSettings(portOrOptions = {}) {
8930
9070
  const opts = typeof portOrOptions === "number" ? { sessionStartPort: portOrOptions } : portOrOptions;
8931
- const hooksDir = join$1(SVAMP_HOME$1, "tmp", "hooks");
8932
- mkdirSync$1(hooksDir, { recursive: true });
9071
+ const hooksDir = join(SVAMP_HOME$1, "tmp", "hooks");
9072
+ mkdirSync(hooksDir, { recursive: true });
8933
9073
  const id = opts.id || String(process.pid);
8934
- const validatorPath = join$1(hooksDir, `image-validator-${id}.cjs`);
8935
- writeFileSync$1(validatorPath, IMAGE_VALIDATOR_SCRIPT, { mode: 493 });
9074
+ const validatorPath = join(hooksDir, `image-validator-${id}.cjs`);
9075
+ writeFileSync(validatorPath, IMAGE_VALIDATOR_SCRIPT, { mode: 493 });
8936
9076
  const cleanupPaths = [validatorPath];
8937
9077
  const hooks = {
8938
9078
  PreToolUse: [
@@ -8949,7 +9089,7 @@ function generateHookSettings(portOrOptions = {}) {
8949
9089
  ]
8950
9090
  };
8951
9091
  if (typeof opts.sessionStartPort === "number" && opts.sessionStartPort > 0) {
8952
- const forwarderPath = join$1(hooksDir, `forwarder-${id}.cjs`);
9092
+ const forwarderPath = join(hooksDir, `forwarder-${id}.cjs`);
8953
9093
  const forwarderCode = `#!/usr/bin/env node
8954
9094
  const http = require('http');
8955
9095
  const port = parseInt(process.argv[2], 10);
@@ -8968,7 +9108,7 @@ process.stdin.on('end', () => {
8968
9108
  });
8969
9109
  process.stdin.resume();
8970
9110
  `;
8971
- writeFileSync$1(forwarderPath, forwarderCode, { mode: 493 });
9111
+ writeFileSync(forwarderPath, forwarderCode, { mode: 493 });
8972
9112
  cleanupPaths.push(forwarderPath);
8973
9113
  hooks.SessionStart = [
8974
9114
  {
@@ -8982,8 +9122,8 @@ process.stdin.resume();
8982
9122
  }
8983
9123
  ];
8984
9124
  }
8985
- const settingsPath = join$1(hooksDir, `session-hook-${id}.json`);
8986
- writeFileSync$1(settingsPath, JSON.stringify({ hooks }, null, 2));
9125
+ const settingsPath = join(hooksDir, `session-hook-${id}.json`);
9126
+ writeFileSync(settingsPath, JSON.stringify({ hooks }, null, 2));
8987
9127
  cleanupPaths.push(settingsPath);
8988
9128
  const cleanup = () => {
8989
9129
  for (const p of cleanupPaths) {
@@ -9145,7 +9285,7 @@ async function readSessionFileBase64(resolvedPath) {
9145
9285
 
9146
9286
  const __filename$1 = fileURLToPath(import.meta.url);
9147
9287
  const __dirname$1 = dirname(__filename$1);
9148
- const CLAUDE_SKILLS_DIR = join(os$1.homedir(), ".claude", "skills");
9288
+ const CLAUDE_SKILLS_DIR = join$1(os$1.homedir(), ".claude", "skills");
9149
9289
  function looksLikeClaudeError(line) {
9150
9290
  const l = line.toLowerCase();
9151
9291
  return l.includes("api error") || l.includes("request rejected") || l.includes("error:") || l.includes("overloaded") || l.includes("rate limit") || l.includes("unauthorized") || l.includes("forbidden") || /\b(4\d\d|5\d\d)\b/.test(l) && (l.includes("status") || l.includes("error") || l.includes("rejected"));
@@ -9185,7 +9325,7 @@ function buildClaudeErrorHint(text, apiErrorStatus) {
9185
9325
  }
9186
9326
  function readSkillVersion(skillDir) {
9187
9327
  try {
9188
- const md = readFileSync$1(join(skillDir, "SKILL.md"), "utf-8");
9328
+ const md = readFileSync$1(join$1(skillDir, "SKILL.md"), "utf-8");
9189
9329
  const m = md.match(/^---\s*\n([\s\S]*?)\n---/);
9190
9330
  if (!m) return null;
9191
9331
  const versionLine = m[1].split("\n").find((l) => /^\s*version\s*:/.test(l));
@@ -9211,18 +9351,18 @@ async function installSkillFromEndpoint(name, baseUrl) {
9211
9351
  const index = await resp.json();
9212
9352
  const files = index.files || [];
9213
9353
  if (files.length === 0) throw new Error(`Skill index at ${baseUrl} has no files`);
9214
- const targetDir = join(CLAUDE_SKILLS_DIR, name);
9215
- mkdirSync(targetDir, { recursive: true });
9354
+ const targetDir = join$1(CLAUDE_SKILLS_DIR, name);
9355
+ mkdirSync$1(targetDir, { recursive: true });
9216
9356
  for (const filePath of files) {
9217
9357
  if (!filePath) continue;
9218
9358
  const url = `${baseUrl}${filePath}`;
9219
9359
  const fileResp = await fetch(url, { signal: AbortSignal.timeout(3e4) });
9220
9360
  if (!fileResp.ok) throw new Error(`Failed to download ${filePath}: HTTP ${fileResp.status}`);
9221
9361
  const content = await fileResp.text();
9222
- const localPath = join(targetDir, filePath);
9362
+ const localPath = join$1(targetDir, filePath);
9223
9363
  if (!localPath.startsWith(targetDir + "/")) continue;
9224
- mkdirSync(dirname(localPath), { recursive: true });
9225
- writeFileSync(localPath, content, "utf-8");
9364
+ mkdirSync$1(dirname(localPath), { recursive: true });
9365
+ writeFileSync$1(localPath, content, "utf-8");
9226
9366
  }
9227
9367
  }
9228
9368
  async function installSkillFromMarketplace(name) {
@@ -9246,26 +9386,26 @@ async function installSkillFromMarketplace(name) {
9246
9386
  }
9247
9387
  const files = await collectFiles();
9248
9388
  if (files.length === 0) throw new Error(`Skill ${name} has no files in marketplace`);
9249
- const targetDir = join(CLAUDE_SKILLS_DIR, name);
9250
- mkdirSync(targetDir, { recursive: true });
9389
+ const targetDir = join$1(CLAUDE_SKILLS_DIR, name);
9390
+ mkdirSync$1(targetDir, { recursive: true });
9251
9391
  for (const filePath of files) {
9252
9392
  const url = `${BASE}/files/${filePath}`;
9253
9393
  const resp = await fetch(url, { signal: AbortSignal.timeout(3e4) });
9254
9394
  if (!resp.ok) throw new Error(`Failed to download ${filePath}: HTTP ${resp.status}`);
9255
9395
  const content = await resp.text();
9256
- const localPath = join(targetDir, filePath);
9396
+ const localPath = join$1(targetDir, filePath);
9257
9397
  if (!localPath.startsWith(targetDir + "/")) continue;
9258
- mkdirSync(dirname(localPath), { recursive: true });
9259
- writeFileSync(localPath, content, "utf-8");
9398
+ mkdirSync$1(dirname(localPath), { recursive: true });
9399
+ writeFileSync$1(localPath, content, "utf-8");
9260
9400
  }
9261
9401
  }
9262
9402
  function getBundledSkillsDir() {
9263
9403
  try {
9264
9404
  const here = fileURLToPath(import.meta.url);
9265
9405
  const candidates = [
9266
- join(dirname(here), "..", "bin", "skills"),
9406
+ join$1(dirname(here), "..", "bin", "skills"),
9267
9407
  // built dist/ layout
9268
- join(dirname(here), "..", "..", "bin", "skills")
9408
+ join$1(dirname(here), "..", "..", "bin", "skills")
9269
9409
  // src/daemon → bin layout via tsx
9270
9410
  ];
9271
9411
  for (const c of candidates) {
@@ -9278,17 +9418,17 @@ function getBundledSkillsDir() {
9278
9418
  function installBundledSkill(name) {
9279
9419
  const bundledDir = getBundledSkillsDir();
9280
9420
  if (!bundledDir) throw new Error(`Bundled skills directory not found`);
9281
- const src = join(bundledDir, name);
9421
+ const src = join$1(bundledDir, name);
9282
9422
  if (!existsSync$1(src)) throw new Error(`Bundled skill not found: ${src}`);
9283
- const dst = join(CLAUDE_SKILLS_DIR, name);
9284
- mkdirSync(dst, { recursive: true });
9423
+ const dst = join$1(CLAUDE_SKILLS_DIR, name);
9424
+ mkdirSync$1(dst, { recursive: true });
9285
9425
  function copyDir(s, d) {
9286
- mkdirSync(d, { recursive: true });
9426
+ mkdirSync$1(d, { recursive: true });
9287
9427
  for (const entry of readdirSync$1(s, { withFileTypes: true })) {
9288
- const sp = join(s, entry.name);
9289
- const dp = join(d, entry.name);
9428
+ const sp = join$1(s, entry.name);
9429
+ const dp = join$1(d, entry.name);
9290
9430
  if (entry.isDirectory()) copyDir(sp, dp);
9291
- else if (entry.isFile()) writeFileSync(dp, readFileSync$1(sp));
9431
+ else if (entry.isFile()) writeFileSync$1(dp, readFileSync$1(sp));
9292
9432
  }
9293
9433
  }
9294
9434
  copyDir(src, dst);
@@ -9296,7 +9436,7 @@ function installBundledSkill(name) {
9296
9436
  function readBundledSkillVersion(name) {
9297
9437
  const bundledDir = getBundledSkillsDir();
9298
9438
  if (!bundledDir) return null;
9299
- return readSkillVersion(join(bundledDir, name));
9439
+ return readSkillVersion(join$1(bundledDir, name));
9300
9440
  }
9301
9441
  function preventMachineSleep(logger) {
9302
9442
  if (process.platform === "darwin") {
@@ -9404,7 +9544,7 @@ async function ensureAutoInstalledSkills(logger) {
9404
9544
  }
9405
9545
  ];
9406
9546
  for (const task of tasks) {
9407
- const targetDir = join(CLAUDE_SKILLS_DIR, task.name);
9547
+ const targetDir = join$1(CLAUDE_SKILLS_DIR, task.name);
9408
9548
  const installed = existsSync$1(targetDir);
9409
9549
  if (!installed) {
9410
9550
  try {
@@ -9445,20 +9585,20 @@ function loadEnvFile(path) {
9445
9585
  return true;
9446
9586
  }
9447
9587
  function loadDotEnv() {
9448
- const svampEnv = join(process.env.SVAMP_HOME || os$1.homedir() + "/.svamp", ".env");
9588
+ const svampEnv = join$1(process.env.SVAMP_HOME || os$1.homedir() + "/.svamp", ".env");
9449
9589
  if (!loadEnvFile(svampEnv)) {
9450
- const hyphaEnv = join(process.env.HYPHA_HOME || os$1.homedir() + "/.hypha", ".env");
9590
+ const hyphaEnv = join$1(process.env.HYPHA_HOME || os$1.homedir() + "/.hypha", ".env");
9451
9591
  loadEnvFile(hyphaEnv);
9452
9592
  }
9453
9593
  }
9454
9594
  loadDotEnv();
9455
- const SVAMP_HOME = process.env.SVAMP_HOME || join(os$1.homedir(), ".svamp");
9456
- const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
9457
- const DAEMON_LOCK_FILE = join(SVAMP_HOME, "daemon.lock");
9458
- const DAEMON_STOP_MARKER_FILE = join(SVAMP_HOME, "daemon.stop");
9595
+ const SVAMP_HOME = process.env.SVAMP_HOME || join$1(os$1.homedir(), ".svamp");
9596
+ const DAEMON_STATE_FILE = join$1(SVAMP_HOME, "daemon.state.json");
9597
+ const DAEMON_LOCK_FILE = join$1(SVAMP_HOME, "daemon.lock");
9598
+ const DAEMON_STOP_MARKER_FILE = join$1(SVAMP_HOME, "daemon.stop");
9459
9599
  function writeStopMarker(reason) {
9460
9600
  try {
9461
- writeFileSync(DAEMON_STOP_MARKER_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${reason}
9601
+ writeFileSync$1(DAEMON_STOP_MARKER_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${reason}
9462
9602
  `, "utf-8");
9463
9603
  } catch {
9464
9604
  }
@@ -9476,11 +9616,11 @@ function stopMarkerExists() {
9476
9616
  return false;
9477
9617
  }
9478
9618
  }
9479
- const LOGS_DIR = join(SVAMP_HOME, "logs");
9480
- const SESSION_INDEX_FILE = join(SVAMP_HOME, "sessions-index.json");
9619
+ const LOGS_DIR = join$1(SVAMP_HOME, "logs");
9620
+ const SESSION_INDEX_FILE = join$1(SVAMP_HOME, "sessions-index.json");
9481
9621
  function readPackageVersion() {
9482
9622
  try {
9483
- const pkgPath = join(__dirname$1, "../package.json");
9623
+ const pkgPath = join$1(__dirname$1, "../package.json");
9484
9624
  if (existsSync$1(pkgPath)) {
9485
9625
  return JSON.parse(readFileSync$1(pkgPath, "utf-8")).version || "unknown";
9486
9626
  }
@@ -9490,7 +9630,7 @@ function readPackageVersion() {
9490
9630
  }
9491
9631
  const DAEMON_VERSION = readPackageVersion();
9492
9632
  function loadAgentConfig() {
9493
- const configPath = join(SVAMP_HOME, "agent-config.json");
9633
+ const configPath = join$1(SVAMP_HOME, "agent-config.json");
9494
9634
  if (existsSync$1(configPath)) {
9495
9635
  try {
9496
9636
  return JSON.parse(readFileSync$1(configPath, "utf-8"));
@@ -9501,19 +9641,19 @@ function loadAgentConfig() {
9501
9641
  return {};
9502
9642
  }
9503
9643
  function getSessionSvampDir(directory) {
9504
- return join(directory, ".svamp");
9644
+ return join$1(directory, ".svamp");
9505
9645
  }
9506
9646
  function getSessionDir(directory, sessionId) {
9507
- return join(getSessionSvampDir(directory), sessionId);
9647
+ return join$1(getSessionSvampDir(directory), sessionId);
9508
9648
  }
9509
9649
  function getSessionFilePath(directory, sessionId) {
9510
- return join(getSessionDir(directory, sessionId), "session.json");
9650
+ return join$1(getSessionDir(directory, sessionId), "session.json");
9511
9651
  }
9512
9652
  function getSessionMessagesPath(directory, sessionId) {
9513
- return join(getSessionDir(directory, sessionId), "messages.jsonl");
9653
+ return join$1(getSessionDir(directory, sessionId), "messages.jsonl");
9514
9654
  }
9515
9655
  function getSvampConfigPath(directory, sessionId) {
9516
- return join(getSessionDir(directory, sessionId), "config.json");
9656
+ return join$1(getSessionDir(directory, sessionId), "config.json");
9517
9657
  }
9518
9658
  function readSvampConfig(configPath) {
9519
9659
  try {
@@ -9523,19 +9663,19 @@ function readSvampConfig(configPath) {
9523
9663
  return {};
9524
9664
  }
9525
9665
  function writeSvampConfig(configPath, config) {
9526
- mkdirSync(dirname(configPath), { recursive: true });
9666
+ mkdirSync$1(dirname(configPath), { recursive: true });
9527
9667
  const content = JSON.stringify(config, null, 2);
9528
9668
  const tmpPath = configPath + ".tmp";
9529
- writeFileSync(tmpPath, content);
9530
- renameSync(tmpPath, configPath);
9669
+ writeFileSync$1(tmpPath, content);
9670
+ renameSync$1(tmpPath, configPath);
9531
9671
  return content;
9532
9672
  }
9533
9673
  function getLoopDir(directory) {
9534
- return join(directory, ".claude", "loop");
9674
+ return join$1(directory, ".claude", "loop");
9535
9675
  }
9536
9676
  function readLoopState(directory) {
9537
9677
  try {
9538
- const p = join(getLoopDir(directory), "loop-state.json");
9678
+ const p = join$1(getLoopDir(directory), "loop-state.json");
9539
9679
  if (!existsSync$1(p)) return null;
9540
9680
  return JSON.parse(readFileSync$1(p, "utf-8"));
9541
9681
  } catch {
@@ -9559,8 +9699,8 @@ function isLoopActiveForSession(directory, sessionId) {
9559
9699
  }
9560
9700
  function resolveLoopInit() {
9561
9701
  const candidates = [
9562
- join(CLAUDE_SKILLS_DIR, "loop", "bin", "loop-init.mjs"),
9563
- ...getBundledSkillsDir() ? [join(getBundledSkillsDir(), "loop", "bin", "loop-init.mjs")] : []
9702
+ join$1(CLAUDE_SKILLS_DIR, "loop", "bin", "loop-init.mjs"),
9703
+ ...getBundledSkillsDir() ? [join$1(getBundledSkillsDir(), "loop", "bin", "loop-init.mjs")] : []
9564
9704
  ];
9565
9705
  for (const c of candidates) if (existsSync$1(c)) return c;
9566
9706
  return null;
@@ -9580,14 +9720,14 @@ function initLoop(directory, cfg) {
9580
9720
  }
9581
9721
  function deactivateLoop(directory) {
9582
9722
  try {
9583
- const p = join(getLoopDir(directory), "loop-state.json");
9723
+ const p = join$1(getLoopDir(directory), "loop-state.json");
9584
9724
  if (!existsSync$1(p)) return;
9585
9725
  const s = JSON.parse(readFileSync$1(p, "utf-8"));
9586
9726
  s.active = false;
9587
9727
  s.phase = "cancelled";
9588
9728
  const tmp = p + ".tmp";
9589
- writeFileSync(tmp, JSON.stringify(s, null, 2));
9590
- renameSync(tmp, p);
9729
+ writeFileSync$1(tmp, JSON.stringify(s, null, 2));
9730
+ renameSync$1(tmp, p);
9591
9731
  } catch {
9592
9732
  }
9593
9733
  }
@@ -9725,7 +9865,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9725
9865
  let watcher = null;
9726
9866
  try {
9727
9867
  const configDir = dirname(configPath);
9728
- mkdirSync(configDir, { recursive: true });
9868
+ mkdirSync$1(configDir, { recursive: true });
9729
9869
  watcher = watch(configDir, (eventType, filename) => {
9730
9870
  if (filename === "config.json") configChecker();
9731
9871
  });
@@ -9751,18 +9891,18 @@ function loadSessionIndex() {
9751
9891
  }
9752
9892
  function saveSessionIndex(index) {
9753
9893
  const tmp = SESSION_INDEX_FILE + ".tmp";
9754
- writeFileSync(tmp, JSON.stringify(index, null, 2), "utf-8");
9755
- renameSync(tmp, SESSION_INDEX_FILE);
9894
+ writeFileSync$1(tmp, JSON.stringify(index, null, 2), "utf-8");
9895
+ renameSync$1(tmp, SESSION_INDEX_FILE);
9756
9896
  }
9757
9897
  function saveSession(session) {
9758
9898
  const sessionDir = getSessionDir(session.directory, session.sessionId);
9759
9899
  if (!existsSync$1(sessionDir)) {
9760
- mkdirSync(sessionDir, { recursive: true });
9900
+ mkdirSync$1(sessionDir, { recursive: true });
9761
9901
  }
9762
9902
  const filePath = getSessionFilePath(session.directory, session.sessionId);
9763
9903
  const tmpPath = filePath + ".tmp";
9764
- writeFileSync(tmpPath, JSON.stringify(session, null, 2), "utf-8");
9765
- renameSync(tmpPath, filePath);
9904
+ writeFileSync$1(tmpPath, JSON.stringify(session, null, 2), "utf-8");
9905
+ renameSync$1(tmpPath, filePath);
9766
9906
  const index = loadSessionIndex();
9767
9907
  index[session.sessionId] = { directory: session.directory, createdAt: session.createdAt };
9768
9908
  saveSessionIndex(index);
@@ -9806,8 +9946,8 @@ function markSessionAsArchived(sessionId) {
9806
9946
  if (data.stopped === true) return true;
9807
9947
  data.stopped = true;
9808
9948
  const tmpPath = filePath + ".tmp";
9809
- writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
9810
- renameSync(tmpPath, filePath);
9949
+ writeFileSync$1(tmpPath, JSON.stringify(data, null, 2), "utf-8");
9950
+ renameSync$1(tmpPath, filePath);
9811
9951
  return true;
9812
9952
  } catch {
9813
9953
  return false;
@@ -9824,8 +9964,8 @@ function clearSessionArchivedFlag(sessionId) {
9824
9964
  if (data.stopped) {
9825
9965
  delete data.stopped;
9826
9966
  const tmpPath = filePath + ".tmp";
9827
- writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
9828
- renameSync(tmpPath, filePath);
9967
+ writeFileSync$1(tmpPath, JSON.stringify(data, null, 2), "utf-8");
9968
+ renameSync$1(tmpPath, filePath);
9829
9969
  }
9830
9970
  return data;
9831
9971
  } catch {
@@ -9857,15 +9997,15 @@ function loadPersistedSessions() {
9857
9997
  }
9858
9998
  function ensureHomeDir() {
9859
9999
  if (!existsSync$1(SVAMP_HOME)) {
9860
- mkdirSync(SVAMP_HOME, { recursive: true });
10000
+ mkdirSync$1(SVAMP_HOME, { recursive: true });
9861
10001
  }
9862
10002
  if (!existsSync$1(LOGS_DIR)) {
9863
- mkdirSync(LOGS_DIR, { recursive: true });
10003
+ mkdirSync$1(LOGS_DIR, { recursive: true });
9864
10004
  }
9865
10005
  }
9866
10006
  function createLogger() {
9867
10007
  ensureHomeDir();
9868
- const logFile = join(LOGS_DIR, `daemon-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.log`);
10008
+ const logFile = join$1(LOGS_DIR, `daemon-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.log`);
9869
10009
  return {
9870
10010
  logFilePath: logFile,
9871
10011
  log: (...args) => {
@@ -9889,8 +10029,8 @@ function createLogger() {
9889
10029
  function writeDaemonStateFile(state) {
9890
10030
  ensureHomeDir();
9891
10031
  const tmpPath = DAEMON_STATE_FILE + ".tmp";
9892
- writeFileSync(tmpPath, JSON.stringify(state, null, 2), "utf-8");
9893
- renameSync(tmpPath, DAEMON_STATE_FILE);
10032
+ writeFileSync$1(tmpPath, JSON.stringify(state, null, 2), "utf-8");
10033
+ renameSync$1(tmpPath, DAEMON_STATE_FILE);
9894
10034
  }
9895
10035
  function readDaemonStateFile() {
9896
10036
  try {
@@ -10089,7 +10229,7 @@ async function startDaemon(options) {
10089
10229
  logger.log('Warning: No HYPHA_TOKEN set. Run "svamp login" to authenticate.');
10090
10230
  logger.log("Connecting anonymously...");
10091
10231
  }
10092
- const machineIdFile = join(SVAMP_HOME, "machine-id");
10232
+ const machineIdFile = join$1(SVAMP_HOME, "machine-id");
10093
10233
  let machineId = process.env.SVAMP_MACHINE_ID;
10094
10234
  if (!machineId) {
10095
10235
  if (existsSync$1(machineIdFile)) {
@@ -10098,7 +10238,7 @@ async function startDaemon(options) {
10098
10238
  if (!machineId) {
10099
10239
  machineId = `machine-${os$1.hostname()}-${randomUUID$1().slice(0, 8)}`;
10100
10240
  try {
10101
- writeFileSync(machineIdFile, machineId, "utf-8");
10241
+ writeFileSync$1(machineIdFile, machineId, "utf-8");
10102
10242
  } catch {
10103
10243
  }
10104
10244
  }
@@ -10108,10 +10248,10 @@ async function startDaemon(options) {
10108
10248
  logger.log(` Workspace: ${hyphaWorkspace || "(default)"}`);
10109
10249
  logger.log(` Machine ID: ${machineId}`);
10110
10250
  let server = null;
10111
- const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
10251
+ const supervisor = new ProcessSupervisor(join$1(SVAMP_HOME, "processes"));
10112
10252
  await supervisor.init();
10113
10253
  const tunnels = /* @__PURE__ */ new Map();
10114
- const EXPOSED_TUNNELS_FILE = join(SVAMP_HOME, "exposed-tunnels.json");
10254
+ const EXPOSED_TUNNELS_FILE = join$1(SVAMP_HOME, "exposed-tunnels.json");
10115
10255
  function loadExposedTunnels() {
10116
10256
  try {
10117
10257
  if (!existsSync$1(EXPOSED_TUNNELS_FILE)) return [];
@@ -10124,8 +10264,8 @@ async function startDaemon(options) {
10124
10264
  }
10125
10265
  function saveExposedTunnels(specs) {
10126
10266
  try {
10127
- mkdirSync(SVAMP_HOME, { recursive: true });
10128
- writeFileSync(EXPOSED_TUNNELS_FILE, JSON.stringify({ tunnels: specs }, null, 2));
10267
+ mkdirSync$1(SVAMP_HOME, { recursive: true });
10268
+ writeFileSync$1(EXPOSED_TUNNELS_FILE, JSON.stringify({ tunnels: specs }, null, 2));
10129
10269
  } catch (err) {
10130
10270
  logger.log(`[exposed-tunnels] Persist failed: ${err.message}`);
10131
10271
  }
@@ -10139,7 +10279,7 @@ async function startDaemon(options) {
10139
10279
  const list = loadExposedTunnels().filter((t) => t.name !== name);
10140
10280
  saveExposedTunnels(list);
10141
10281
  }
10142
- const { ServeManager } = await import('./serveManager-D6lGn8jh.mjs');
10282
+ const { ServeManager } = await import('./serveManager-Drl0uy6Z.mjs');
10143
10283
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
10144
10284
  ensureAutoInstalledSkills(logger).catch(() => {
10145
10285
  });
@@ -10330,8 +10470,8 @@ async function startDaemon(options) {
10330
10470
  machineId,
10331
10471
  homeDir: os$1.homedir(),
10332
10472
  svampHomeDir: SVAMP_HOME,
10333
- svampLibDir: join(__dirname$1, ".."),
10334
- svampToolsDir: join(__dirname$1, "..", "tools"),
10473
+ svampLibDir: join$1(__dirname$1, ".."),
10474
+ svampToolsDir: join$1(__dirname$1, "..", "tools"),
10335
10475
  startedFromDaemon: true,
10336
10476
  startedBy: "daemon",
10337
10477
  lifecycleState: resumeSessionId ? "idle" : "starting",
@@ -10438,7 +10578,7 @@ async function startDaemon(options) {
10438
10578
  if (persisted && persisted.sessionId !== sessionId) {
10439
10579
  const oldDir = persisted.directory || directory;
10440
10580
  const newSessionDir = getSessionDir(directory, sessionId);
10441
- if (!existsSync$1(newSessionDir)) mkdirSync(newSessionDir, { recursive: true });
10581
+ if (!existsSync$1(newSessionDir)) mkdirSync$1(newSessionDir, { recursive: true });
10442
10582
  const oldMsgFile = getSessionMessagesPath(oldDir, persisted.sessionId);
10443
10583
  const newMsgFile = getSessionMessagesPath(directory, sessionId);
10444
10584
  try {
@@ -11562,7 +11702,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11562
11702
  const children = [];
11563
11703
  await Promise.all(entries.map(async (entry) => {
11564
11704
  if (entry.isSymbolicLink()) return;
11565
- const childPath = join(p, entry.name);
11705
+ const childPath = join$1(p, entry.name);
11566
11706
  const childNode = await buildTree(childPath, entry.name, depth + 1);
11567
11707
  if (childNode) children.push(childNode);
11568
11708
  }));
@@ -11725,8 +11865,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11725
11865
  machineId,
11726
11866
  homeDir: os$1.homedir(),
11727
11867
  svampHomeDir: SVAMP_HOME,
11728
- svampLibDir: join(__dirname$1, ".."),
11729
- svampToolsDir: join(__dirname$1, "..", "tools"),
11868
+ svampLibDir: join$1(__dirname$1, ".."),
11869
+ svampToolsDir: join$1(__dirname$1, "..", "tools"),
11730
11870
  startedFromDaemon: true,
11731
11871
  startedBy: "daemon",
11732
11872
  lifecycleState: "starting",
@@ -12031,7 +12171,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12031
12171
  const children = [];
12032
12172
  await Promise.all(entries.map(async (entry) => {
12033
12173
  if (entry.isSymbolicLink()) return;
12034
- const childPath = join(p, entry.name);
12174
+ const childPath = join$1(p, entry.name);
12035
12175
  const childNode = await buildTree(childPath, entry.name, depth + 1);
12036
12176
  if (childNode) children.push(childNode);
12037
12177
  }));
@@ -12267,8 +12407,20 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12267
12407
  };
12268
12408
  const archiveSession = (sessionId) => {
12269
12409
  logger.log(`Archiving session: ${sessionId}`);
12410
+ let loopDir;
12411
+ for (const s of pidToTrackedSession.values()) {
12412
+ if (s.svampSessionId === sessionId) {
12413
+ loopDir = s.directory;
12414
+ break;
12415
+ }
12416
+ }
12417
+ if (!loopDir) loopDir = loadSessionIndex()[sessionId]?.directory;
12270
12418
  const wasInMemory = teardownTrackedSession(sessionId);
12271
12419
  const markedArchived = markSessionAsArchived(sessionId);
12420
+ if (loopDir && isLoopActiveForSession(loopDir, sessionId)) {
12421
+ deactivateLoop(loopDir);
12422
+ logger.log(`Deactivated loop for archived session ${sessionId}`);
12423
+ }
12272
12424
  if (wasInMemory || markedArchived) {
12273
12425
  logger.log(`Session ${sessionId} archived (inMemory=${wasInMemory}, persisted=${markedArchived})`);
12274
12426
  return true;
@@ -12312,8 +12464,19 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12312
12464
  };
12313
12465
  const deleteSession = (sessionId) => {
12314
12466
  logger.log(`Deleting session: ${sessionId}`);
12467
+ let loopDir;
12468
+ for (const s of pidToTrackedSession.values()) {
12469
+ if (s.svampSessionId === sessionId) {
12470
+ loopDir = s.directory;
12471
+ break;
12472
+ }
12473
+ }
12474
+ if (!loopDir) loopDir = loadSessionIndex()[sessionId]?.directory;
12315
12475
  teardownTrackedSession(sessionId);
12316
12476
  deletePersistedSession(sessionId);
12477
+ if (loopDir && isLoopActiveForSession(loopDir, sessionId)) {
12478
+ deactivateLoop(loopDir);
12479
+ }
12317
12480
  logger.log(`Session ${sessionId} deleted`);
12318
12481
  return true;
12319
12482
  };
@@ -12370,7 +12533,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12370
12533
  svampVersion: "0.1.0 (hypha)",
12371
12534
  homeDir: defaultHomeDir,
12372
12535
  svampHomeDir: SVAMP_HOME,
12373
- svampLibDir: join(__dirname$1, ".."),
12536
+ svampLibDir: join$1(__dirname$1, ".."),
12374
12537
  displayName: process.env.SVAMP_DISPLAY_NAME || void 0,
12375
12538
  isolationCapabilities,
12376
12539
  // Restore persisted sharing (possibly augmented with --share seed above),
@@ -12439,7 +12602,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12439
12602
  const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
12440
12603
  if (channelHttpPort > 0) {
12441
12604
  try {
12442
- const { createChannelHttpServer } = await import('./httpServer-D9qLS8ed.mjs');
12605
+ const { createChannelHttpServer } = await import('./httpServer-CWn3F-0t.mjs');
12443
12606
  const channelHttpServer = createChannelHttpServer({
12444
12607
  getSessionIds: () => {
12445
12608
  const ids = [];
@@ -12460,7 +12623,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12460
12623
  const specs = loadExposedTunnels();
12461
12624
  if (specs.length === 0) return;
12462
12625
  logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
12463
- const { FrpcTunnel } = await import('./frpc-Dn5pmk_f.mjs');
12626
+ const { FrpcTunnel } = await import('./frpc-CG7J02Ft.mjs');
12464
12627
  for (const spec of specs) {
12465
12628
  if (tunnels.has(spec.name)) continue;
12466
12629
  try {
@@ -12924,8 +13087,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12924
13087
  if (existsSync$1(filePath)) {
12925
13088
  const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
12926
13089
  const tmpPath = filePath + ".tmp";
12927
- writeFileSync(tmpPath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
12928
- renameSync(tmpPath, filePath);
13090
+ writeFileSync$1(tmpPath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
13091
+ renameSync$1(tmpPath, filePath);
12929
13092
  markedCount++;
12930
13093
  }
12931
13094
  } catch {
@@ -12988,7 +13151,7 @@ async function stopDaemon(options) {
12988
13151
  const mode = options?.cleanup ? "cleanup (sessions will be stopped)" : "quick (sessions preserved for auto-restore)";
12989
13152
  writeStopMarker(`stopDaemon (${options?.cleanup ? "cleanup" : "quick"})`);
12990
13153
  const pidsToSignal = [];
12991
- const supervisorPidFile = join(SVAMP_HOME, "supervisor.pid");
13154
+ const supervisorPidFile = join$1(SVAMP_HOME, "supervisor.pid");
12992
13155
  try {
12993
13156
  if (existsSync$1(supervisorPidFile)) {
12994
13157
  const supervisorPid = parseInt(readFileSync$1(supervisorPidFile, "utf-8").trim(), 10);
@@ -13093,7 +13256,7 @@ async function stopDaemon(options) {
13093
13256
  }
13094
13257
  }
13095
13258
  async function restartDaemon() {
13096
- const supervisorPidFile = join(SVAMP_HOME, "supervisor.pid");
13259
+ const supervisorPidFile = join$1(SVAMP_HOME, "supervisor.pid");
13097
13260
  let supervisorPid = null;
13098
13261
  try {
13099
13262
  if (existsSync$1(supervisorPidFile)) {
@@ -13130,7 +13293,7 @@ async function restartDaemon() {
13130
13293
  });
13131
13294
  child.unref();
13132
13295
  }
13133
- const stateFile2 = join(SVAMP_HOME, "daemon.state.json");
13296
+ const stateFile2 = join$1(SVAMP_HOME, "daemon.state.json");
13134
13297
  for (let i = 0; i < 100; i++) {
13135
13298
  await new Promise((r) => setTimeout(r, 100));
13136
13299
  if (existsSync$1(stateFile2)) {
@@ -13154,7 +13317,7 @@ async function restartDaemon() {
13154
13317
  await doFullRestart("Failed to signal supervisor");
13155
13318
  return;
13156
13319
  }
13157
- const stateFile = join(SVAMP_HOME, "daemon.state.json");
13320
+ const stateFile = join$1(SVAMP_HOME, "daemon.state.json");
13158
13321
  for (let i = 0; i < 300; i++) {
13159
13322
  await new Promise((r) => setTimeout(r, 100));
13160
13323
  try {
@@ -13179,7 +13342,7 @@ async function restartDaemon() {
13179
13342
  function daemonStatus() {
13180
13343
  const state = readDaemonStateFile();
13181
13344
  if (!state) {
13182
- const plistPath = join(os$1.homedir(), "Library", "LaunchAgents", "io.hypha.svamp.daemon.plist");
13345
+ const plistPath = join$1(os$1.homedir(), "Library", "LaunchAgents", "io.hypha.svamp.daemon.plist");
13183
13346
  if (existsSync$1(plistPath)) {
13184
13347
  console.log("Status: Not running (launchd service installed \u2014 may be starting)");
13185
13348
  } else {