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.
- package/dist/{agentCommands-oHt9FQJt.mjs → agentCommands-5cqd1Sv5.mjs} +2 -2
- package/dist/{auth-Dg0s5H5y.mjs → auth-B85cMCRB.mjs} +2 -2
- package/dist/cli.mjs +51 -51
- package/dist/{commands-C_DlMpl7.mjs → commands-Bj33Q-8L.mjs} +34 -23
- package/dist/{commands-C_B8GNUT.mjs → commands-Br3TNk9B.mjs} +2 -2
- package/dist/{commands-BGE6zFa1.mjs → commands-C1UMZsmw.mjs} +28 -4
- package/dist/{commands-BXFukv2v.mjs → commands-DPC3ZNpr.mjs} +2 -2
- package/dist/{commands-BvJ_Dl1l.mjs → commands-bweAFpU9.mjs} +5 -5
- package/dist/{fleet-BlrT4zSC.mjs → fleet-B7jI4z7x.mjs} +1 -1
- package/dist/{frpc-Dn5pmk_f.mjs → frpc-CG7J02Ft.mjs} +2 -2
- package/dist/{headlessCli-D7NXB73S.mjs → headlessCli-DIupYWzT.mjs} +3 -3
- package/dist/{httpServer-D9qLS8ed.mjs → httpServer-CWn3F-0t.mjs} +2 -1
- package/dist/index.mjs +2 -2
- package/dist/{package-fu3Jsw1q.mjs → package-NWqZNy2p.mjs} +2 -2
- package/dist/{run-BmZjAEob.mjs → run-C89ZXpqq.mjs} +721 -558
- package/dist/{run-DTOSfKSH.mjs → run-CDKEGrNp.mjs} +1 -1
- package/dist/{serveCommands-zFOjNs-0.mjs → serveCommands-5TFReS1I.mjs} +5 -5
- package/dist/{serveManager-D6lGn8jh.mjs → serveManager-Drl0uy6Z.mjs} +3 -3
- package/dist/{sideband-CNyGVxRy.mjs → sideband-CHClz1Yz.mjs} +2 -2
- package/package.json +2 -2
|
@@ -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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
2027
|
+
const ownerEmail2 = newSharing.owner || context?.user?.email || "";
|
|
1667
2028
|
newSharing = {
|
|
1668
2029
|
...newSharing,
|
|
1669
2030
|
allowedUsers: (newSharing.allowedUsers || []).map(
|
|
1670
|
-
(u) => normalizeAllowedUser(u,
|
|
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-
|
|
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
|
|
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,
|
|
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
|
|
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-
|
|
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
|
|
2505
|
-
if (
|
|
2506
|
-
|
|
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.
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
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
|
-
|
|
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
|
|
3004
|
+
return process.env.SVAMP_ROUTINES_DIR || join(homedir(), ".svamp", "routines");
|
|
2661
3005
|
}
|
|
2662
|
-
const genId
|
|
2663
|
-
const genKey
|
|
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
|
|
3012
|
+
mkdirSync(dir, { recursive: true });
|
|
2669
3013
|
}
|
|
2670
3014
|
_path(id) {
|
|
2671
|
-
return join
|
|
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
|
|
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
|
|
2693
|
-
if ((r.trigger?.type === "webhook" || r.trigger?.type === "api") && !r.trigger.key) r.trigger.key = genKey
|
|
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
|
|
2698
|
-
renameSync
|
|
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
|
|
3032
|
-
this.file = join
|
|
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
|
|
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
|
|
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
|
|
3149
|
-
renameSync
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3427
|
+
const filePath = join(messagesDir, "messages.jsonl");
|
|
3300
3428
|
if (!existsSync(messagesDir)) {
|
|
3301
|
-
mkdirSync
|
|
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
|
|
4329
|
+
const filePath = join(options.messagesDir, "messages.jsonl");
|
|
4190
4330
|
try {
|
|
4191
|
-
writeFileSync
|
|
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
|
|
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
|
|
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
|
|
4377
|
+
mkdirSync(join(SVAMP_HOME$2, "inbound"), { recursive: true });
|
|
4238
4378
|
const tmp = p + ".tmp";
|
|
4239
|
-
writeFileSync
|
|
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
|
|
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
|
|
4532
|
-
const messagesPath = join
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4712
|
-
if (existsSync(join
|
|
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
|
|
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
|
|
5195
|
+
const realLocalDir = join(homedir(), ".local");
|
|
5056
5196
|
if (existsSync(realLocalDir)) {
|
|
5057
5197
|
nonoArgs.push("--read", realLocalDir);
|
|
5058
5198
|
}
|
|
5059
|
-
const realKeychainDir = join
|
|
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
|
|
5260
|
+
const stagedClaudeDir = join(config.credentialStagingPath, ".claude");
|
|
5121
5261
|
containerArgs.push("-v", `${stagedClaudeDir}:/root/.claude:ro`);
|
|
5122
|
-
const stagedGitconfig = join
|
|
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
|
|
6872
|
-
const SVAMP_BIN_DIR = join
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7009
|
-
const probeFile = join
|
|
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
|
|
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
|
|
7291
|
+
const realClaudeDir = join(realHome, ".claude");
|
|
7152
7292
|
await mkdir(STAGED_HOMES_DIR, { recursive: true });
|
|
7153
|
-
const tmpHome = join
|
|
7293
|
+
const tmpHome = join(STAGED_HOMES_DIR, sessionId);
|
|
7154
7294
|
await mkdir(tmpHome, { recursive: true });
|
|
7155
|
-
const stagedClaudeDir = join
|
|
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
|
|
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
|
|
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
|
|
7325
|
+
await copyFile(join(realHome, ".gitconfig"), join(tmpHome, ".gitconfig"));
|
|
7186
7326
|
} catch {
|
|
7187
7327
|
}
|
|
7188
7328
|
try {
|
|
7189
7329
|
await copyFile(
|
|
7190
|
-
join
|
|
7191
|
-
join
|
|
7330
|
+
join(realHome, ".gitignore_global"),
|
|
7331
|
+
join(tmpHome, ".gitignore_global")
|
|
7192
7332
|
);
|
|
7193
7333
|
} catch {
|
|
7194
7334
|
}
|
|
7195
|
-
const claudeJsonPath = join
|
|
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
|
|
7203
|
-
const stagedSkillsDir = join
|
|
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
|
|
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
|
|
7253
|
-
const destPath = join
|
|
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
|
|
7306
|
-
return join
|
|
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
|
|
7316
|
-
if (!existsSync(dir)) mkdirSync
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
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
|
|
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
|
|
8932
|
-
mkdirSync
|
|
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
|
|
8935
|
-
writeFileSync
|
|
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
|
|
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
|
|
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
|
|
8986
|
-
writeFileSync
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|