svamp-cli 0.2.102 → 0.2.104
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/bin/skills/loop/SKILL.md +1 -1
- package/bin/skills/loop/bin/loop-init.mjs +5 -0
- package/dist/{agentCommands-Cm-Cu_9Z.mjs → agentCommands-DhcQMAe-.mjs} +3 -6
- package/dist/{auth-BwHB9Upd.mjs → auth-D4G47YjL.mjs} +2 -2
- package/dist/cli.mjs +51 -51
- package/dist/{commands-h6yGJuJw.mjs → commands-AkN7uDYW.mjs} +5 -5
- package/dist/{commands-DNxxgy4V.mjs → commands-C1dsiSL5.mjs} +15 -2
- package/dist/{commands-x1wznXLf.mjs → commands-DsSzRtJu.mjs} +2 -2
- package/dist/{commands-DpRXzSr9.mjs → commands-o0MBJocy.mjs} +44 -24
- package/dist/{commands-Clj_0eV6.mjs → commands-u519ohSC.mjs} +2 -2
- package/dist/{fleet-Cilkj57K.mjs → fleet-CjAI4rxS.mjs} +1 -1
- package/dist/{frpc-cJUGFtWY.mjs → frpc-B8ORdlOO.mjs} +2 -2
- package/dist/{headlessCli-CZEbwp47.mjs → headlessCli-CIMYmqci.mjs} +3 -3
- package/dist/{httpServer-D9qLS8ed.mjs → httpServer-CWn3F-0t.mjs} +2 -1
- package/dist/index.mjs +2 -2
- package/dist/{package-BsbKsOb_.mjs → package-IQYoSW7o.mjs} +2 -2
- package/dist/{run-B_FyvS11.mjs → run-CoaYIdw1.mjs} +741 -551
- package/dist/{run-CW6bkzDX.mjs → run-aFkkky75.mjs} +1 -1
- package/dist/{serveCommands-O2px1za0.mjs → serveCommands-DSn_4Auj.mjs} +5 -5
- package/dist/{serveManager-B757hHGd.mjs → serveManager-Bq33kB5r.mjs} +3 -3
- package/dist/{sideband-DYhbiCEA.mjs → sideband-CO0bdYO_.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,362 @@ 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 ((t?.type === "webhook" || t?.type === "api") && t.public && a?.kind === "loop")
|
|
1225
|
+
errs.push("a public webhook/api may not use a loop action (unauthenticated task injection) \u2014 use a message action or require a key");
|
|
1226
|
+
return errs;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const genId$1 = () => "c_" + randomBytes(5).toString("hex");
|
|
1230
|
+
const genKey$1 = () => "ck_" + randomBytes(18).toString("base64url");
|
|
1231
|
+
const DEFAULT_TEMPLATE = `<inbound-message from="\${sender.name}" sender-type="\${sender.kind}" verified="\${sender.verified}" channel="\${channel.name}" call-id="\${call.id}" at="\${now}">
|
|
1232
|
+
\${body.message}
|
|
1233
|
+
</inbound-message>`;
|
|
1234
|
+
function validateChannel(c) {
|
|
1235
|
+
const errs = [];
|
|
1236
|
+
if (!c || typeof c !== "object") return ["channel must be an object"];
|
|
1237
|
+
if (!c.name) errs.push("name required");
|
|
1238
|
+
const m = c.identity?.mode;
|
|
1239
|
+
if (!["per-key", "caller-supplied", "fixed"].includes(m)) errs.push("identity.mode must be per-key|caller-supplied|fixed");
|
|
1240
|
+
if (m === "fixed" && !c.identity.fixed?.name) errs.push("identity.fixed.name required for fixed mode");
|
|
1241
|
+
if (!["message", "loop", "agent"].includes(c.action?.kind)) errs.push("action.kind must be message|loop|agent");
|
|
1242
|
+
const b = c.bind;
|
|
1243
|
+
const bindOk = b === "dynamic" || b === "stateless" || b && typeof b.session === "string" && !!b.session;
|
|
1244
|
+
if (!bindOk) errs.push('bind must be "dynamic", "stateless", or { session }');
|
|
1245
|
+
if (b === "stateless" && c.reply?.mode === "queue")
|
|
1246
|
+
errs.push('a stateless channel cannot use reply.mode "queue" (no persistent session to answer later)');
|
|
1247
|
+
if (b === "stateless" && (c.action?.kind === "loop" || c.action?.kind === "agent"))
|
|
1248
|
+
errs.push(`a stateless channel cannot use a ${c.action?.kind} action (needs a live session); use dynamic or { session }`);
|
|
1249
|
+
if (c.action?.kind === "loop" && m === "caller-supplied" && !c.identity?.shared_key)
|
|
1250
|
+
errs.push("a caller-supplied channel without a shared_key may not use a loop action (unauthenticated task injection)");
|
|
1251
|
+
if (c.action?.kind === "agent" && m === "caller-supplied" && !c.identity?.shared_key) {
|
|
1252
|
+
const MUTATING = ["run_bash", "send_to_session"];
|
|
1253
|
+
const ag = c.action.agent || {};
|
|
1254
|
+
const grantsMutating = (ag.tools || []).some((t) => MUTATING.includes(t)) || Object.values(ag.per_caller || {}).some((p) => (p?.tools || []).some((t) => MUTATING.includes(t)));
|
|
1255
|
+
if (grantsMutating) errs.push("a caller-supplied agent channel without a shared_key may not grant run_bash/send_to_session");
|
|
1256
|
+
}
|
|
1257
|
+
const unsafe = /[<>"'&\r\n]/;
|
|
1258
|
+
if (unsafe.test(c.name || "")) errs.push(`name must be single-line and not contain < > " ' &`);
|
|
1259
|
+
if (c.description && unsafe.test(c.description)) errs.push(`description must be single-line and not contain < > " ' &`);
|
|
1260
|
+
if (c.skill?.name && unsafe.test(c.skill.name)) errs.push(`skill.name must be single-line and not contain < > " ' &`);
|
|
1261
|
+
if (c.skill?.description && unsafe.test(c.skill.description)) errs.push(`skill.description must be single-line and not contain < > " ' &`);
|
|
1262
|
+
if (m === "fixed" && c.identity.fixed?.name && unsafe.test(c.identity.fixed.name)) errs.push(`identity.fixed.name must not contain < > " ' & or newlines`);
|
|
1263
|
+
for (const cl of c.identity?.callers || []) if (unsafe.test(cl.name || "")) errs.push(`caller name "${cl.name}" must not contain < > " ' & or newlines`);
|
|
1264
|
+
return errs;
|
|
1265
|
+
}
|
|
1266
|
+
function normalizeBind(c) {
|
|
1267
|
+
const b = c.bind;
|
|
1268
|
+
if (b === "dynamic" || b === "stateless" || b && typeof b.session === "string" && b.session) return c;
|
|
1269
|
+
c.bind = "dynamic";
|
|
1270
|
+
return c;
|
|
1271
|
+
}
|
|
1272
|
+
function bindMode(c) {
|
|
1273
|
+
const b = normalizeBind(c).bind;
|
|
1274
|
+
if (b === "stateless") return "stateless";
|
|
1275
|
+
if (b && typeof b.session === "string") return "fixed";
|
|
1276
|
+
return "dynamic";
|
|
1277
|
+
}
|
|
1278
|
+
function fixedSessionId(c) {
|
|
1279
|
+
const b = c.bind;
|
|
1280
|
+
return b && typeof b.session === "string" ? b.session : void 0;
|
|
1281
|
+
}
|
|
1282
|
+
class ChannelStore {
|
|
1283
|
+
dir;
|
|
1284
|
+
constructor(projectDir) {
|
|
1285
|
+
this.dir = join(projectDir, ".svamp", "channels");
|
|
1286
|
+
try {
|
|
1287
|
+
mkdirSync(this.dir, { recursive: true });
|
|
1288
|
+
} catch {
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
_path(id) {
|
|
1292
|
+
return join(this.dir, `${id}.json`);
|
|
1293
|
+
}
|
|
1294
|
+
list() {
|
|
1295
|
+
if (!existsSync(this.dir)) return [];
|
|
1296
|
+
return readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
1297
|
+
try {
|
|
1298
|
+
return normalizeBind(JSON.parse(readFileSync(join(this.dir, f), "utf8")));
|
|
1299
|
+
} catch {
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
}).filter((c) => !!c);
|
|
1303
|
+
}
|
|
1304
|
+
get(id) {
|
|
1305
|
+
try {
|
|
1306
|
+
return normalizeBind(JSON.parse(readFileSync(this._path(id), "utf8")));
|
|
1307
|
+
} catch {
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
save(channel) {
|
|
1312
|
+
const c = { enabled: true, bind: "dynamic", template: DEFAULT_TEMPLATE, last_calls: [], ...channel };
|
|
1313
|
+
if (!c.id) c.id = genId$1();
|
|
1314
|
+
const errs = validateChannel(c);
|
|
1315
|
+
if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
|
|
1316
|
+
mkdirSync(this.dir, { recursive: true });
|
|
1317
|
+
const tmp = this._path(c.id) + ".tmp";
|
|
1318
|
+
writeFileSync(tmp, JSON.stringify(c, null, 2));
|
|
1319
|
+
renameSync(tmp, this._path(c.id));
|
|
1320
|
+
return c;
|
|
1321
|
+
}
|
|
1322
|
+
remove(id) {
|
|
1323
|
+
const p = this._path(id);
|
|
1324
|
+
if (existsSync(p)) {
|
|
1325
|
+
rmSync(p);
|
|
1326
|
+
return true;
|
|
1327
|
+
}
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
setEnabled(id, enabled) {
|
|
1331
|
+
const c = this.get(id);
|
|
1332
|
+
if (!c) return null;
|
|
1333
|
+
c.enabled = enabled;
|
|
1334
|
+
return this.save(c);
|
|
1335
|
+
}
|
|
1336
|
+
recordCall(id, entry) {
|
|
1337
|
+
const c = this.get(id);
|
|
1338
|
+
if (!c) return;
|
|
1339
|
+
c.last_calls = c.last_calls || [];
|
|
1340
|
+
c.last_calls.unshift({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
1341
|
+
c.last_calls = c.last_calls.slice(0, 20);
|
|
1342
|
+
this.save(c);
|
|
1343
|
+
}
|
|
1344
|
+
addCaller(id, name, kind = "agent") {
|
|
1345
|
+
const c = this.get(id);
|
|
1346
|
+
if (!c) return null;
|
|
1347
|
+
c.identity.callers = c.identity.callers || [];
|
|
1348
|
+
const caller = { name, kind, key: genKey$1() };
|
|
1349
|
+
c.identity.callers.push(caller);
|
|
1350
|
+
this.save(c);
|
|
1351
|
+
return caller;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
function routingSession(channel, ctx) {
|
|
1355
|
+
const mode = bindMode(channel);
|
|
1356
|
+
if (mode === "fixed") return fixedSessionId(channel);
|
|
1357
|
+
if (mode === "dynamic") return ctx?.session;
|
|
1358
|
+
return void 0;
|
|
1359
|
+
}
|
|
1360
|
+
function gatewayBase(channelsServiceId, baseUrl) {
|
|
1361
|
+
const slash = channelsServiceId.indexOf("/");
|
|
1362
|
+
if (slash < 0) return `${baseUrl.replace(/\/$/, "")}/services/${channelsServiceId}`;
|
|
1363
|
+
const ws = channelsServiceId.slice(0, slash);
|
|
1364
|
+
const clientSvc = channelsServiceId.slice(slash + 1);
|
|
1365
|
+
return `${baseUrl.replace(/\/$/, "")}/${ws}/services/${clientSvc}`;
|
|
1366
|
+
}
|
|
1367
|
+
function generateSkillBody(channel, ctx) {
|
|
1368
|
+
const svc = ctx?.channelsServiceId || "<workspace>/<machine>:channels";
|
|
1369
|
+
const base = ctx?.baseUrl || "https://hypha.aicell.io";
|
|
1370
|
+
const gw = ctx?.channelsServiceId ? gatewayBase(svc, base) : `${base}/<workspace>/services/<machine>:channels`;
|
|
1371
|
+
const skillUrl = `${gw}/skill?channel=${channel.id}`;
|
|
1372
|
+
const sendUrl = `${gw}/send`;
|
|
1373
|
+
const key = ctx?.key || "<your-key>";
|
|
1374
|
+
const isAgent = channel.action?.kind === "agent";
|
|
1375
|
+
const isQueue = channel.reply?.mode === "queue";
|
|
1376
|
+
const recvUrl = `${gw}/receive`;
|
|
1377
|
+
const hyphaOpen = (channel.identity?.hypha_allow || []).length > 0;
|
|
1378
|
+
const mode = bindMode(channel);
|
|
1379
|
+
const rSession = routingSession(channel, ctx);
|
|
1380
|
+
const sessionLineJs = rSession ? `
|
|
1381
|
+
session: "${rSession}",` : "";
|
|
1382
|
+
const sessionKv = rSession ? `, "session": "${rSession}"` : "";
|
|
1383
|
+
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.`;
|
|
1384
|
+
const name = channel.skill?.name || channel.name;
|
|
1385
|
+
const desc = channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`;
|
|
1386
|
+
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\`).`;
|
|
1387
|
+
const queueSection = isQueue ? `
|
|
1388
|
+
|
|
1389
|
+
## Getting the reply (async)
|
|
1390
|
+
\`send()\` returns \`{ correlationId }\`. The agent answers later; retrieve replies addressed
|
|
1391
|
+
to you by **long-polling** \`receive\` with a cursor you advance each call:
|
|
1392
|
+
\`\`\`js
|
|
1393
|
+
const { correlationId } = await get_service("${svc}").send({ channel: "${channel.id}", message: "\u2026", from: "your-name" });
|
|
1394
|
+
let cursor = 0;
|
|
1395
|
+
while (true) {
|
|
1396
|
+
const r = await get_service("${svc}").receive({ channel: "${channel.id}", key: "${key}", cursor, wait: 25 });
|
|
1397
|
+
cursor = r.cursor;
|
|
1398
|
+
for (const reply of r.replies) if (reply.correlationId === correlationId) return reply.body;
|
|
1399
|
+
}
|
|
1400
|
+
\`\`\`
|
|
1401
|
+
**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).` : "";
|
|
1402
|
+
const rpcLine = hyphaOpen ? `**Hypha RPC** \u2014 preferred. Your verified Hypha identity is accepted, no key needed:` : `**Hypha RPC** \u2014 verified identity:`;
|
|
1403
|
+
return `---
|
|
1404
|
+
name: ${name}
|
|
1405
|
+
description: ${desc}
|
|
1406
|
+
---
|
|
1407
|
+
# ${name}
|
|
1408
|
+
${channel.description || ""}
|
|
1409
|
+
|
|
1410
|
+
Self-contained guide for messaging the **${channel.name}** channel. ${replyNote}
|
|
1411
|
+
${bindNote}
|
|
1412
|
+
This skill (with every value below already filled in) is served at:
|
|
1413
|
+
${skillUrl}
|
|
1414
|
+
|
|
1415
|
+
${rpcLine}
|
|
1416
|
+
\`\`\`js
|
|
1417
|
+
await get_service("${svc}").send({
|
|
1418
|
+
channel: "${channel.id}",
|
|
1419
|
+
message: "your message here",
|
|
1420
|
+
from: "your-name",${sessionLineJs}
|
|
1421
|
+
});
|
|
1422
|
+
\`\`\`
|
|
1423
|
+
|
|
1424
|
+
**HTTP** \u2014 any client, no Hypha SDK needed (Hypha gateway wraps args under \`kwargs\`):
|
|
1425
|
+
\`\`\`
|
|
1426
|
+
POST ${sendUrl}
|
|
1427
|
+
Content-Type: application/json
|
|
1428
|
+
|
|
1429
|
+
{"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"${sessionKv}}}
|
|
1430
|
+
\`\`\`${queueSection}`;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
function resolveSender(channel, input = {}) {
|
|
1434
|
+
const { key, from, hyphaUser, hyphaWorkspace, hyphaAnonymous } = input;
|
|
1435
|
+
const id = channel.identity || {};
|
|
1436
|
+
if (hyphaUser && !hyphaAnonymous && Array.isArray(id.hypha_allow) && id.hypha_allow.length) {
|
|
1437
|
+
if (id.hypha_allow.includes("*") || id.hypha_allow.includes(hyphaUser) || hyphaWorkspace && id.hypha_allow.includes(hyphaWorkspace))
|
|
1438
|
+
return { sender: { name: hyphaUser, kind: "agent", verified: true } };
|
|
1439
|
+
return { error: "caller not in hypha_allow" };
|
|
1440
|
+
}
|
|
1441
|
+
if (id.mode === "fixed") {
|
|
1442
|
+
if (!id.fixed?.name) return { error: "fixed identity not configured" };
|
|
1443
|
+
return { sender: { name: id.fixed.name, kind: id.fixed.kind, verified: true } };
|
|
1444
|
+
}
|
|
1445
|
+
if (id.mode === "per-key") {
|
|
1446
|
+
const caller = (id.callers || []).find((c) => c.key && c.key === key);
|
|
1447
|
+
if (!caller) return { error: "invalid or missing key" };
|
|
1448
|
+
return { sender: { name: caller.name, kind: caller.kind, verified: true } };
|
|
1449
|
+
}
|
|
1450
|
+
if (id.mode === "caller-supplied") {
|
|
1451
|
+
if (id.shared_key && key !== id.shared_key) return { error: "invalid key" };
|
|
1452
|
+
return { sender: { name: from || "anonymous", kind: "user", verified: false } };
|
|
1453
|
+
}
|
|
1454
|
+
return { error: "unsupported identity mode" };
|
|
1455
|
+
}
|
|
1456
|
+
const MAX_BODY = 16 * 1024;
|
|
1457
|
+
function xmlEscape(s) {
|
|
1458
|
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1459
|
+
}
|
|
1460
|
+
const stripControl = (s) => String(s ?? "").replace(/[\x00-\x1f\x7f]/g, " ");
|
|
1461
|
+
function renderMessage(channel, { sender = {}, body = {}, query = {}, callId, now }) {
|
|
1462
|
+
const obj = (v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v;
|
|
1463
|
+
const escVal = (v) => xmlEscape(obj(v));
|
|
1464
|
+
const escAttr = (v) => xmlEscape(stripControl(obj(v)));
|
|
1465
|
+
const bodyEsc = {};
|
|
1466
|
+
for (const [k, v] of Object.entries(body)) bodyEsc[k] = k === "message" ? escVal(String(v ?? "").slice(0, MAX_BODY)) : escAttr(v);
|
|
1467
|
+
const queryEsc = {};
|
|
1468
|
+
for (const [k, v] of Object.entries(query)) if (k !== "key") queryEsc[k] = escAttr(v);
|
|
1469
|
+
const ctx = {
|
|
1470
|
+
sender: { name: escAttr(sender.name), kind: escAttr(sender.kind), verified: sender.verified === true },
|
|
1471
|
+
body: bodyEsc,
|
|
1472
|
+
query: queryEsc,
|
|
1473
|
+
channel: { name: escAttr(channel.name), id: escAttr(channel.id) },
|
|
1474
|
+
call: { id: escAttr(callId) },
|
|
1475
|
+
now: escAttr(now || (/* @__PURE__ */ new Date()).toISOString())
|
|
1476
|
+
};
|
|
1477
|
+
return renderTemplate(channel.template || DEFAULT_TEMPLATE, ctx);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1124
1480
|
function getParamNames(fn) {
|
|
1125
1481
|
const src = fn.toString();
|
|
1126
1482
|
const match = src.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
|
|
@@ -1151,7 +1507,7 @@ function filterTerminalResponses(data) {
|
|
|
1151
1507
|
return filtered;
|
|
1152
1508
|
}
|
|
1153
1509
|
function getMachineMetadataPath(svampHomeDir) {
|
|
1154
|
-
return join(svampHomeDir, "machine-metadata.json");
|
|
1510
|
+
return join$1(svampHomeDir, "machine-metadata.json");
|
|
1155
1511
|
}
|
|
1156
1512
|
async function mintRealtimeEphemeralKey(baseUrl, apiKey, opts) {
|
|
1157
1513
|
const realtimeBase = baseUrl || "https://api.openai.com";
|
|
@@ -1212,11 +1568,11 @@ function loadPersistedMachineMetadata(svampHomeDir) {
|
|
|
1212
1568
|
}
|
|
1213
1569
|
function savePersistedMachineMetadata(svampHomeDir, data) {
|
|
1214
1570
|
try {
|
|
1215
|
-
mkdirSync(svampHomeDir, { recursive: true });
|
|
1571
|
+
mkdirSync$1(svampHomeDir, { recursive: true });
|
|
1216
1572
|
const filePath = getMachineMetadataPath(svampHomeDir);
|
|
1217
1573
|
const tmpPath = filePath + ".tmp";
|
|
1218
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
1219
|
-
renameSync(tmpPath, filePath);
|
|
1574
|
+
writeFileSync$1(tmpPath, JSON.stringify(data, null, 2));
|
|
1575
|
+
renameSync$1(tmpPath, filePath);
|
|
1220
1576
|
} catch (err) {
|
|
1221
1577
|
console.error("[HYPHA MACHINE] Failed to persist machine metadata:", err);
|
|
1222
1578
|
}
|
|
@@ -1663,11 +2019,11 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
1663
2019
|
if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
|
|
1664
2020
|
newSharing = { ...newSharing, owner: context.user.email };
|
|
1665
2021
|
}
|
|
1666
|
-
const
|
|
2022
|
+
const ownerEmail2 = newSharing.owner || context?.user?.email || "";
|
|
1667
2023
|
newSharing = {
|
|
1668
2024
|
...newSharing,
|
|
1669
2025
|
allowedUsers: (newSharing.allowedUsers || []).map(
|
|
1670
|
-
(u) => normalizeAllowedUser(u,
|
|
2026
|
+
(u) => normalizeAllowedUser(u, ownerEmail2)
|
|
1671
2027
|
),
|
|
1672
2028
|
publicAccess: null
|
|
1673
2029
|
};
|
|
@@ -2166,7 +2522,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
2166
2522
|
const tunnels = handlers.tunnels;
|
|
2167
2523
|
if (!tunnels) throw new Error("Tunnel management not available");
|
|
2168
2524
|
if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
|
|
2169
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
2525
|
+
const { FrpcTunnel } = await import('./frpc-B8ORdlOO.mjs');
|
|
2170
2526
|
const tunnel = new FrpcTunnel({
|
|
2171
2527
|
name: params.name,
|
|
2172
2528
|
ports: params.ports,
|
|
@@ -2222,9 +2578,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
2222
2578
|
authorizeRequest(context, currentMetadata.sharing, "interact");
|
|
2223
2579
|
const sm = handlers.serveManager;
|
|
2224
2580
|
if (!sm) throw new Error("Serve manager not available");
|
|
2225
|
-
const
|
|
2581
|
+
const ownerEmail2 = params.ownerEmail || context?.user?.email || void 0;
|
|
2226
2582
|
const access = params.access || "owner";
|
|
2227
|
-
return sm.addMount(params.name, params.directory, params.sessionId, access,
|
|
2583
|
+
return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail2);
|
|
2228
2584
|
},
|
|
2229
2585
|
/**
|
|
2230
2586
|
* Apply a mount declaratively. Replaces existing mount with same name.
|
|
@@ -2235,7 +2591,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
2235
2591
|
authorizeRequest(context, currentMetadata.sharing, "interact");
|
|
2236
2592
|
const sm = handlers.serveManager;
|
|
2237
2593
|
if (!sm) throw new Error("Serve manager not available");
|
|
2238
|
-
const
|
|
2594
|
+
const ownerEmail2 = params.ownerEmail || context?.user?.email || void 0;
|
|
2239
2595
|
const access = params.access ?? "owner";
|
|
2240
2596
|
return sm.applyMount({
|
|
2241
2597
|
name: params.name,
|
|
@@ -2243,7 +2599,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
2243
2599
|
process: params.process,
|
|
2244
2600
|
sessionId: params.sessionId,
|
|
2245
2601
|
access,
|
|
2246
|
-
ownerEmail
|
|
2602
|
+
ownerEmail: ownerEmail2
|
|
2247
2603
|
});
|
|
2248
2604
|
},
|
|
2249
2605
|
/** Remove a mount from the shared static file server. */
|
|
@@ -2427,7 +2783,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
2427
2783
|
}
|
|
2428
2784
|
const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
|
|
2429
2785
|
const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
|
|
2430
|
-
const { toolsForRole } = await import('./sideband-
|
|
2786
|
+
const { toolsForRole } = await import('./sideband-CO0bdYO_.mjs');
|
|
2431
2787
|
const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
|
|
2432
2788
|
return fmt(r2);
|
|
2433
2789
|
}
|
|
@@ -2462,6 +2818,89 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
2462
2818
|
}
|
|
2463
2819
|
return void 0;
|
|
2464
2820
|
};
|
|
2821
|
+
const readChannelFromDisk = (channelId) => {
|
|
2822
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2823
|
+
for (const t of handlers.getTrackedSessions?.() || []) {
|
|
2824
|
+
const dir = t.directory;
|
|
2825
|
+
if (!dir || seen.has(dir)) continue;
|
|
2826
|
+
seen.add(dir);
|
|
2827
|
+
try {
|
|
2828
|
+
const c = new ChannelStore(dir).get(channelId);
|
|
2829
|
+
if (c && c.enabled !== false) return { c, dir };
|
|
2830
|
+
} catch {
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
return void 0;
|
|
2834
|
+
};
|
|
2835
|
+
const liveSessionsInDir = (dir) => (handlers.getTrackedSessions?.() || []).filter((t) => t.directory === dir && handlers.getSessionRPCHandlers?.(t.sessionId)).map((t) => t.sessionId);
|
|
2836
|
+
const resolveChannel = (channelId, targetSession) => {
|
|
2837
|
+
const found = readChannelFromDisk(channelId);
|
|
2838
|
+
if (!found) return { error: "channel not found" };
|
|
2839
|
+
const { c, dir } = found;
|
|
2840
|
+
const mode = bindMode(c);
|
|
2841
|
+
const kind = c.action?.kind;
|
|
2842
|
+
if (kind === "agent" || kind === "loop") {
|
|
2843
|
+
const prefer = mode === "fixed" ? fixedSessionId(c) : mode === "dynamic" ? targetSession : void 0;
|
|
2844
|
+
const sid = prefer && handlers.getSessionRPCHandlers?.(prefer) ? prefer : liveSessionsInDir(dir)[0];
|
|
2845
|
+
if (!sid) {
|
|
2846
|
+
if (mode === "fixed") return { tier: "session", sessionId: fixedSessionId(c) || "?", stopped: true, c, dir };
|
|
2847
|
+
return { error: `no live session to host this ${kind} channel \u2014 start a session in ${dir}` };
|
|
2848
|
+
}
|
|
2849
|
+
return { tier: "session", sessionId: sid, rpc: handlers.getSessionRPCHandlers(sid), c, dir };
|
|
2850
|
+
}
|
|
2851
|
+
const target = mode === "fixed" ? fixedSessionId(c) : mode === "dynamic" ? targetSession : void 0;
|
|
2852
|
+
if (target) {
|
|
2853
|
+
const rpc = handlers.getSessionRPCHandlers?.(target);
|
|
2854
|
+
if (rpc) return { tier: "session", sessionId: target, rpc, c, dir };
|
|
2855
|
+
if (mode === "fixed") return { tier: "session", sessionId: target, stopped: true, c, dir };
|
|
2856
|
+
}
|
|
2857
|
+
return { tier: "stateless", c, dir };
|
|
2858
|
+
};
|
|
2859
|
+
const ownerEmail = currentMetadata.sharing?.owner;
|
|
2860
|
+
const ownerCtx = ownerEmail ? { user: { email: ownerEmail, id: ownerEmail } } : void 0;
|
|
2861
|
+
const selfMachine = {
|
|
2862
|
+
spawnSession: (opts) => handlers.spawnSession(opts),
|
|
2863
|
+
sessionRPC: async (sessionId, method, kwargs) => {
|
|
2864
|
+
const rpc = handlers.getSessionRPCHandlers?.(sessionId);
|
|
2865
|
+
if (!rpc) throw new Error(`Session ${sessionId} not found on this machine`);
|
|
2866
|
+
const handler = rpc[method];
|
|
2867
|
+
if (typeof handler !== "function") throw new Error(`Unknown session method: ${method}`);
|
|
2868
|
+
const paramNames = getParamNames(handler);
|
|
2869
|
+
const callArgs = paramNames.map((n) => n === "context" ? ownerCtx : kwargs?.[n] ?? void 0);
|
|
2870
|
+
return handler(...callArgs);
|
|
2871
|
+
}
|
|
2872
|
+
};
|
|
2873
|
+
const dispatchStateless = async (c, dir, kwargs, context) => {
|
|
2874
|
+
const u = context?.user;
|
|
2875
|
+
const r = resolveSender(c, {
|
|
2876
|
+
key: kwargs.key,
|
|
2877
|
+
from: kwargs.from,
|
|
2878
|
+
hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
|
|
2879
|
+
hyphaAnonymous: u?.is_anonymous === true,
|
|
2880
|
+
hyphaWorkspace: u?.scope?.current_workspace
|
|
2881
|
+
});
|
|
2882
|
+
if (r.error || !r.sender) return { error: r.error || "unauthorized" };
|
|
2883
|
+
const callId = "call_" + Math.random().toString(16).slice(2, 12);
|
|
2884
|
+
const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
|
|
2885
|
+
const { queryCore } = await import('./commands-o0MBJocy.mjs');
|
|
2886
|
+
const timeout = c.reply?.timeout_sec || 120;
|
|
2887
|
+
let result;
|
|
2888
|
+
try {
|
|
2889
|
+
result = await queryCore(selfMachine, dir, rendered, { permissionMode: "bypassPermissions", tag: "svamp-channel", timeout });
|
|
2890
|
+
} catch (e) {
|
|
2891
|
+
return { ok: false, call_id: callId, status: "error", error: e?.message || String(e) };
|
|
2892
|
+
} finally {
|
|
2893
|
+
if (result?.sessionId) {
|
|
2894
|
+
try {
|
|
2895
|
+
handlers.deleteSession?.(result.sessionId);
|
|
2896
|
+
} catch {
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
if (result.status === "error") return { ok: false, call_id: callId, status: "error", error: result.error };
|
|
2901
|
+
if (result.status === "permission-pending") return { ok: false, call_id: callId, status: "permission-pending", error: result.error };
|
|
2902
|
+
return { ok: true, call_id: callId, status: "completed", reply: result.response };
|
|
2903
|
+
};
|
|
2465
2904
|
const channelsServiceInfo = await server.registerService(
|
|
2466
2905
|
{
|
|
2467
2906
|
id: "channels",
|
|
@@ -2501,17 +2940,22 @@ ${d?.error || "not found"}`;
|
|
|
2501
2940
|
},
|
|
2502
2941
|
send: async (kwargs = {}, context) => {
|
|
2503
2942
|
trackInbound();
|
|
2504
|
-
const
|
|
2505
|
-
if (
|
|
2506
|
-
|
|
2943
|
+
const res = resolveChannel(kwargs.channel, kwargs.session);
|
|
2944
|
+
if ("error" in res) return { error: res.error };
|
|
2945
|
+
if (res.tier === "stateless") return dispatchStateless(res.c, res.dir, kwargs, context);
|
|
2946
|
+
if ("stopped" in res) return { error: `session ${res.sessionId.slice(0, 8)} is stopped \u2014 resume it to reach this channel` };
|
|
2947
|
+
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
2948
|
},
|
|
2508
2949
|
// Async reply retrieval for queue-mode channels — long-poll the channel outbox
|
|
2509
|
-
// for replies addressed to the caller.
|
|
2950
|
+
// for replies addressed to the caller. Routed to the session the message landed
|
|
2951
|
+
// in (the `session` target); stateless channels can't queue. Channel-identity auth.
|
|
2510
2952
|
receive: async (kwargs = {}, context) => {
|
|
2511
2953
|
trackInbound();
|
|
2512
|
-
const
|
|
2513
|
-
if (
|
|
2514
|
-
|
|
2954
|
+
const res = resolveChannel(kwargs.channel, kwargs.session);
|
|
2955
|
+
if ("error" in res) return { error: res.error };
|
|
2956
|
+
if (res.tier === "stateless") return { error: "stateless channels have no async replies (no persistent session)" };
|
|
2957
|
+
if ("stopped" in res) return { error: `session ${res.sessionId.slice(0, 8)} is stopped` };
|
|
2958
|
+
return res.rpc.channelReceive({ channel: kwargs.channel, key: kwargs.key, from: kwargs.from, cursor: kwargs.cursor, correlationId: kwargs.correlationId, wait: kwargs.wait }, context);
|
|
2515
2959
|
}
|
|
2516
2960
|
},
|
|
2517
2961
|
{ overwrite: true }
|
|
@@ -2522,158 +2966,53 @@ ${d?.error || "not found"}`;
|
|
|
2522
2966
|
notifySessionEvent: notifyListeners,
|
|
2523
2967
|
updateMetadata: (newMetadata) => {
|
|
2524
2968
|
currentMetadata = newMetadata;
|
|
2525
|
-
metadataVersion++;
|
|
2526
|
-
notifyListeners({
|
|
2527
|
-
type: "update-machine",
|
|
2528
|
-
machineId,
|
|
2529
|
-
metadata: { value: currentMetadata, version: metadataVersion }
|
|
2530
|
-
});
|
|
2531
|
-
},
|
|
2532
|
-
updateDaemonState: (newState) => {
|
|
2533
|
-
currentDaemonState = newState;
|
|
2534
|
-
daemonStateVersion++;
|
|
2535
|
-
notifyListeners({
|
|
2536
|
-
type: "update-machine",
|
|
2537
|
-
machineId,
|
|
2538
|
-
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
2539
|
-
});
|
|
2540
|
-
},
|
|
2541
|
-
getLastInboundRpcAt: () => lastInboundRpcAt,
|
|
2542
|
-
disconnect: async () => {
|
|
2543
|
-
const toRemove = [...listeners];
|
|
2544
|
-
for (const listener of toRemove) {
|
|
2545
|
-
removeListener(listener, "disconnect");
|
|
2546
|
-
}
|
|
2547
|
-
await server.unregisterService(serviceInfo.id);
|
|
2548
|
-
await server.unregisterService(channelsServiceInfo.id).catch(() => {
|
|
2549
|
-
});
|
|
2550
|
-
}
|
|
2551
|
-
};
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
const FIELD_RANGES = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
|
|
2555
|
-
function parseField(token, [min, max]) {
|
|
2556
|
-
const set = /* @__PURE__ */ new Set();
|
|
2557
|
-
for (const part of token.split(",")) {
|
|
2558
|
-
let m;
|
|
2559
|
-
if (part === "*") {
|
|
2560
|
-
for (let i = min; i <= max; i++) set.add(i);
|
|
2561
|
-
} else if (m = part.match(/^\*\/(\d+)$/)) {
|
|
2562
|
-
const s = +m[1];
|
|
2563
|
-
for (let i = min; i <= max; i += s) set.add(i);
|
|
2564
|
-
} else if (m = part.match(/^(\d+)-(\d+)\/(\d+)$/)) {
|
|
2565
|
-
for (let i = +m[1]; i <= +m[2]; i += +m[3]) set.add(i);
|
|
2566
|
-
} else if (m = part.match(/^(\d+)-(\d+)$/)) {
|
|
2567
|
-
for (let i = +m[1]; i <= +m[2]; i++) set.add(i);
|
|
2568
|
-
} else if (m = part.match(/^(\d+)$/)) {
|
|
2569
|
-
set.add(+m[1]);
|
|
2570
|
-
} else throw new Error(`invalid cron field: "${token}"`);
|
|
2571
|
-
}
|
|
2572
|
-
for (const v of set) if (v < min || v > max) throw new Error(`cron value ${v} out of range [${min},${max}]`);
|
|
2573
|
-
return set;
|
|
2574
|
-
}
|
|
2575
|
-
function parseCron(expr) {
|
|
2576
|
-
const fields = String(expr).trim().split(/\s+/);
|
|
2577
|
-
if (fields.length !== 5) throw new Error(`cron must have 5 fields, got ${fields.length}: "${expr}"`);
|
|
2578
|
-
const [minute, hour, dom, month, dow] = fields.map((f, i) => parseField(f, FIELD_RANGES[i]));
|
|
2579
|
-
return { minute, hour, dom, month, dow, domRestricted: fields[2] !== "*", dowRestricted: fields[4] !== "*" };
|
|
2580
|
-
}
|
|
2581
|
-
function cronMatches(expr, date) {
|
|
2582
|
-
const c = typeof expr === "string" ? parseCron(expr) : expr;
|
|
2583
|
-
if (!c.minute.has(date.getMinutes())) return false;
|
|
2584
|
-
if (!c.hour.has(date.getHours())) return false;
|
|
2585
|
-
if (!c.month.has(date.getMonth() + 1)) return false;
|
|
2586
|
-
const domOk = c.dom.has(date.getDate());
|
|
2587
|
-
const dowOk = c.dow.has(date.getDay());
|
|
2588
|
-
if (c.domRestricted && c.dowRestricted) return domOk || dowOk;
|
|
2589
|
-
return domOk && dowOk;
|
|
2590
|
-
}
|
|
2591
|
-
function inZone(date, tz) {
|
|
2592
|
-
if (!tz) return date;
|
|
2593
|
-
try {
|
|
2594
|
-
const p = new Intl.DateTimeFormat("en-US", {
|
|
2595
|
-
timeZone: tz,
|
|
2596
|
-
hour12: false,
|
|
2597
|
-
year: "numeric",
|
|
2598
|
-
month: "2-digit",
|
|
2599
|
-
day: "2-digit",
|
|
2600
|
-
hour: "2-digit",
|
|
2601
|
-
minute: "2-digit"
|
|
2602
|
-
}).formatToParts(date).reduce((o, x) => {
|
|
2603
|
-
o[x.type] = x.value;
|
|
2604
|
-
return o;
|
|
2605
|
-
}, {});
|
|
2606
|
-
return new Date(+p.year, +p.month - 1, +p.day, +(p.hour === "24" ? 0 : p.hour), +p.minute);
|
|
2607
|
-
} catch {
|
|
2608
|
-
return date;
|
|
2609
|
-
}
|
|
2610
|
-
}
|
|
2611
|
-
function nextFire(expr, from, tz) {
|
|
2612
|
-
const c = parseCron(expr);
|
|
2613
|
-
const d = new Date(from.getTime());
|
|
2614
|
-
d.setSeconds(0, 0);
|
|
2615
|
-
d.setMinutes(d.getMinutes() + 1);
|
|
2616
|
-
for (let i = 0; i < 366 * 24 * 60; i++) {
|
|
2617
|
-
if (cronMatches(c, tz ? inZone(d, tz) : d)) return new Date(d.getTime());
|
|
2618
|
-
d.setMinutes(d.getMinutes() + 1);
|
|
2619
|
-
}
|
|
2620
|
-
return null;
|
|
2621
|
-
}
|
|
2622
|
-
function resolvePath(ctx, path) {
|
|
2623
|
-
return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], ctx);
|
|
2624
|
-
}
|
|
2625
|
-
function renderTemplate(template, ctx) {
|
|
2626
|
-
return String(template).replace(/\$\{([\w.$]+)\}/g, (_m, p) => {
|
|
2627
|
-
const v = resolvePath(ctx, p);
|
|
2628
|
-
return v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
2629
|
-
});
|
|
2630
|
-
}
|
|
2631
|
-
const TRIGGER_TYPES = ["manual", "schedule", "webhook", "api"];
|
|
2632
|
-
const ACTION_KINDS = ["message", "loop"];
|
|
2633
|
-
const OVERLAP = ["queue", "skip", "replace"];
|
|
2634
|
-
function validateRoutine(r) {
|
|
2635
|
-
const errs = [];
|
|
2636
|
-
if (!r || typeof r !== "object") return ["routine must be an object"];
|
|
2637
|
-
if (!r.session_id) errs.push("session_id required");
|
|
2638
|
-
if (!r.name) errs.push("name required");
|
|
2639
|
-
const t = r.trigger;
|
|
2640
|
-
if (!t || !TRIGGER_TYPES.includes(t.type)) errs.push(`trigger.type must be one of ${TRIGGER_TYPES.join("|")}`);
|
|
2641
|
-
if (t?.type === "schedule") {
|
|
2642
|
-
try {
|
|
2643
|
-
parseCron(t.cron);
|
|
2644
|
-
} catch (e) {
|
|
2645
|
-
errs.push(`trigger.cron: ${e.message}`);
|
|
2969
|
+
metadataVersion++;
|
|
2970
|
+
notifyListeners({
|
|
2971
|
+
type: "update-machine",
|
|
2972
|
+
machineId,
|
|
2973
|
+
metadata: { value: currentMetadata, version: metadataVersion }
|
|
2974
|
+
});
|
|
2975
|
+
},
|
|
2976
|
+
updateDaemonState: (newState) => {
|
|
2977
|
+
currentDaemonState = newState;
|
|
2978
|
+
daemonStateVersion++;
|
|
2979
|
+
notifyListeners({
|
|
2980
|
+
type: "update-machine",
|
|
2981
|
+
machineId,
|
|
2982
|
+
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
2983
|
+
});
|
|
2984
|
+
},
|
|
2985
|
+
getLastInboundRpcAt: () => lastInboundRpcAt,
|
|
2986
|
+
disconnect: async () => {
|
|
2987
|
+
const toRemove = [...listeners];
|
|
2988
|
+
for (const listener of toRemove) {
|
|
2989
|
+
removeListener(listener, "disconnect");
|
|
2990
|
+
}
|
|
2991
|
+
await server.unregisterService(serviceInfo.id);
|
|
2992
|
+
await server.unregisterService(channelsServiceInfo.id).catch(() => {
|
|
2993
|
+
});
|
|
2646
2994
|
}
|
|
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;
|
|
2995
|
+
};
|
|
2657
2996
|
}
|
|
2658
2997
|
|
|
2659
2998
|
function defaultRoutinesDir() {
|
|
2660
|
-
return process.env.SVAMP_ROUTINES_DIR || join
|
|
2999
|
+
return process.env.SVAMP_ROUTINES_DIR || join(homedir(), ".svamp", "routines");
|
|
2661
3000
|
}
|
|
2662
|
-
const genId
|
|
2663
|
-
const genKey
|
|
3001
|
+
const genId = () => "rt_" + randomBytes(5).toString("hex");
|
|
3002
|
+
const genKey = () => randomBytes(18).toString("base64url");
|
|
2664
3003
|
class RoutineStore {
|
|
2665
3004
|
dir;
|
|
2666
3005
|
constructor(dir = defaultRoutinesDir()) {
|
|
2667
3006
|
this.dir = dir;
|
|
2668
|
-
mkdirSync
|
|
3007
|
+
mkdirSync(dir, { recursive: true });
|
|
2669
3008
|
}
|
|
2670
3009
|
_path(id) {
|
|
2671
|
-
return join
|
|
3010
|
+
return join(this.dir, `${id}.json`);
|
|
2672
3011
|
}
|
|
2673
3012
|
list(sessionId) {
|
|
2674
3013
|
const all = readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
2675
3014
|
try {
|
|
2676
|
-
return JSON.parse(readFileSync(join
|
|
3015
|
+
return JSON.parse(readFileSync(join(this.dir, f), "utf8"));
|
|
2677
3016
|
} catch {
|
|
2678
3017
|
return null;
|
|
2679
3018
|
}
|
|
@@ -2689,13 +3028,13 @@ class RoutineStore {
|
|
|
2689
3028
|
}
|
|
2690
3029
|
save(routine) {
|
|
2691
3030
|
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
|
|
3031
|
+
if (!r.id) r.id = genId();
|
|
3032
|
+
if ((r.trigger?.type === "webhook" || r.trigger?.type === "api") && !r.trigger.key) r.trigger.key = genKey();
|
|
2694
3033
|
const errs = validateRoutine(r);
|
|
2695
3034
|
if (errs.length) throw new Error("invalid routine: " + errs.join("; "));
|
|
2696
3035
|
const tmp = this._path(r.id) + ".tmp";
|
|
2697
|
-
writeFileSync
|
|
2698
|
-
renameSync
|
|
3036
|
+
writeFileSync(tmp, JSON.stringify(r, null, 2));
|
|
3037
|
+
renameSync(tmp, this._path(r.id));
|
|
2699
3038
|
return r;
|
|
2700
3039
|
}
|
|
2701
3040
|
remove(id) {
|
|
@@ -2851,175 +3190,6 @@ class RoutineRunner {
|
|
|
2851
3190
|
}
|
|
2852
3191
|
}
|
|
2853
3192
|
|
|
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
3193
|
const MAX_PER_CHANNEL = 200;
|
|
3024
3194
|
const TTL_MS = 60 * 60 * 1e3;
|
|
3025
3195
|
class ChannelOutbox {
|
|
@@ -3028,11 +3198,11 @@ class ChannelOutbox {
|
|
|
3028
3198
|
seqByChannel = /* @__PURE__ */ new Map();
|
|
3029
3199
|
emitter = new EventEmitter();
|
|
3030
3200
|
constructor(projectDir) {
|
|
3031
|
-
const dir = join
|
|
3032
|
-
this.file = join
|
|
3201
|
+
const dir = join(projectDir, ".svamp", "channels");
|
|
3202
|
+
this.file = join(dir, "_outbox.jsonl");
|
|
3033
3203
|
this.emitter.setMaxListeners(0);
|
|
3034
3204
|
try {
|
|
3035
|
-
mkdirSync
|
|
3205
|
+
mkdirSync(dir, { recursive: true });
|
|
3036
3206
|
} catch {
|
|
3037
3207
|
}
|
|
3038
3208
|
this._load();
|
|
@@ -3081,7 +3251,7 @@ class ChannelOutbox {
|
|
|
3081
3251
|
appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
|
|
3082
3252
|
} catch {
|
|
3083
3253
|
try {
|
|
3084
|
-
mkdirSync
|
|
3254
|
+
mkdirSync(join(this.file, ".."), { recursive: true });
|
|
3085
3255
|
appendFileSync(this.file, JSON.stringify({ channelId, ...reply }) + "\n");
|
|
3086
3256
|
} catch {
|
|
3087
3257
|
}
|
|
@@ -3145,62 +3315,15 @@ class ChannelOutbox {
|
|
|
3145
3315
|
}
|
|
3146
3316
|
});
|
|
3147
3317
|
const tmp = this.file + ".tmp";
|
|
3148
|
-
writeFileSync
|
|
3149
|
-
renameSync
|
|
3318
|
+
writeFileSync(tmp, kept.join("\n") + (kept.length ? "\n" : ""));
|
|
3319
|
+
renameSync(tmp, this.file);
|
|
3150
3320
|
} catch {
|
|
3151
3321
|
}
|
|
3152
3322
|
}
|
|
3153
3323
|
}
|
|
3154
3324
|
|
|
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
3325
|
function channelPublicView(c) {
|
|
3203
|
-
return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind };
|
|
3326
|
+
return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind, bind: c.bind };
|
|
3204
3327
|
}
|
|
3205
3328
|
function isStructuredMessage(msg) {
|
|
3206
3329
|
return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId || msg.channel);
|
|
@@ -3227,7 +3350,7 @@ ${msg.body}
|
|
|
3227
3350
|
</svamp-message>`;
|
|
3228
3351
|
}
|
|
3229
3352
|
function loadMessages(messagesDir, sessionId) {
|
|
3230
|
-
const filePath = join
|
|
3353
|
+
const filePath = join(messagesDir, "messages.jsonl");
|
|
3231
3354
|
if (!existsSync(filePath)) return [];
|
|
3232
3355
|
try {
|
|
3233
3356
|
const lines = readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
|
|
@@ -3244,7 +3367,7 @@ function loadMessages(messagesDir, sessionId) {
|
|
|
3244
3367
|
}
|
|
3245
3368
|
}
|
|
3246
3369
|
function loadMessagesFromDisk(messagesDir, afterSeq, limit) {
|
|
3247
|
-
const filePath = join
|
|
3370
|
+
const filePath = join(messagesDir, "messages.jsonl");
|
|
3248
3371
|
if (!existsSync(filePath)) return { messages: [], hasMore: false };
|
|
3249
3372
|
try {
|
|
3250
3373
|
const lines = readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
|
|
@@ -3263,7 +3386,7 @@ function loadMessagesFromDisk(messagesDir, afterSeq, limit) {
|
|
|
3263
3386
|
}
|
|
3264
3387
|
}
|
|
3265
3388
|
function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
|
|
3266
|
-
const filePath = join
|
|
3389
|
+
const filePath = join(messagesDir, "messages.jsonl");
|
|
3267
3390
|
if (!existsSync(filePath)) return { messages: [], hasMore: false };
|
|
3268
3391
|
try {
|
|
3269
3392
|
const lines = readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
|
|
@@ -3282,7 +3405,7 @@ function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
|
|
|
3282
3405
|
}
|
|
3283
3406
|
}
|
|
3284
3407
|
function countMessagesOnDisk(messagesDir, fallbackInMemory) {
|
|
3285
|
-
const filePath = join
|
|
3408
|
+
const filePath = join(messagesDir, "messages.jsonl");
|
|
3286
3409
|
if (!existsSync(filePath)) return fallbackInMemory;
|
|
3287
3410
|
try {
|
|
3288
3411
|
const data = readFileSync(filePath, "utf-8");
|
|
@@ -3296,9 +3419,9 @@ function countMessagesOnDisk(messagesDir, fallbackInMemory) {
|
|
|
3296
3419
|
}
|
|
3297
3420
|
function appendMessage(messagesDir, sessionId, msg) {
|
|
3298
3421
|
try {
|
|
3299
|
-
const filePath = join
|
|
3422
|
+
const filePath = join(messagesDir, "messages.jsonl");
|
|
3300
3423
|
if (!existsSync(messagesDir)) {
|
|
3301
|
-
mkdirSync
|
|
3424
|
+
mkdirSync(messagesDir, { recursive: true });
|
|
3302
3425
|
}
|
|
3303
3426
|
appendFileSync(filePath, JSON.stringify(msg) + "\n");
|
|
3304
3427
|
} catch (err) {
|
|
@@ -3434,7 +3557,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
3434
3557
|
const skillCtxFor = (c) => ({
|
|
3435
3558
|
channelsServiceId,
|
|
3436
3559
|
baseUrl: channelsBaseUrl,
|
|
3437
|
-
key: c.identity?.shared_key || c.identity?.callers?.[0]?.key
|
|
3560
|
+
key: c.identity?.shared_key || c.identity?.callers?.[0]?.key,
|
|
3561
|
+
// The session this skill is being copied FROM — baked into `dynamic` channel
|
|
3562
|
+
// instructions as the routing target so the copied link lands in THIS session.
|
|
3563
|
+
session: sessionId
|
|
3438
3564
|
});
|
|
3439
3565
|
const skillUrlsFor = (c) => {
|
|
3440
3566
|
if (!channelsServiceId) return { skillUrl: void 0, sendUrl: void 0 };
|
|
@@ -3625,6 +3751,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
3625
3751
|
saveChannel: async (channel, context) => {
|
|
3626
3752
|
authorizeRequest(context, metadata.sharing, "admin");
|
|
3627
3753
|
try {
|
|
3754
|
+
const b = channel.bind;
|
|
3755
|
+
if (b && typeof b === "object" && (b.session === "current" || b.session === "" || b.session == null)) {
|
|
3756
|
+
channel = { ...channel, bind: { session: sessionId } };
|
|
3757
|
+
}
|
|
3628
3758
|
const saved = channelStore.save(channel);
|
|
3629
3759
|
syncChannelsToMetadata();
|
|
3630
3760
|
return { success: true, channel: saved };
|
|
@@ -3656,6 +3786,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
3656
3786
|
const c = channelStore.get(id);
|
|
3657
3787
|
if (!c) return { error: "not found" };
|
|
3658
3788
|
const { skillUrl, sendUrl } = skillUrlsFor(c);
|
|
3789
|
+
const mode = bindMode(c);
|
|
3790
|
+
const routeSession = mode === "fixed" ? fixedSessionId(c) : mode === "dynamic" ? sessionId : void 0;
|
|
3659
3791
|
return {
|
|
3660
3792
|
skill: generateSkillBody(c, skillCtxFor(c)),
|
|
3661
3793
|
channelsServiceId,
|
|
@@ -3664,7 +3796,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
3664
3796
|
skillUrl,
|
|
3665
3797
|
sendUrl,
|
|
3666
3798
|
key: c.identity?.shared_key || c.identity?.callers?.[0]?.key,
|
|
3667
|
-
hyphaKeyless: (c.identity?.hypha_allow || []).length > 0
|
|
3799
|
+
hyphaKeyless: (c.identity?.hypha_allow || []).length > 0,
|
|
3800
|
+
bind: c.bind,
|
|
3801
|
+
bindMode: mode,
|
|
3802
|
+
session: routeSession
|
|
3668
3803
|
};
|
|
3669
3804
|
},
|
|
3670
3805
|
// Public channel discovery (no session authz — channels are deliberately
|
|
@@ -4186,9 +4321,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
4186
4321
|
messages.length = 0;
|
|
4187
4322
|
nextSeq = 1;
|
|
4188
4323
|
if (options?.messagesDir) {
|
|
4189
|
-
const filePath = join
|
|
4324
|
+
const filePath = join(options.messagesDir, "messages.jsonl");
|
|
4190
4325
|
try {
|
|
4191
|
-
writeFileSync
|
|
4326
|
+
writeFileSync(filePath, "");
|
|
4192
4327
|
} catch {
|
|
4193
4328
|
}
|
|
4194
4329
|
}
|
|
@@ -4216,7 +4351,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
4216
4351
|
return { store, rpcHandlers };
|
|
4217
4352
|
}
|
|
4218
4353
|
|
|
4219
|
-
const SVAMP_HOME$2 = process.env.SVAMP_HOME || join
|
|
4354
|
+
const SVAMP_HOME$2 = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
|
|
4220
4355
|
const num = (key, def) => {
|
|
4221
4356
|
const v = Number(process.env[key]);
|
|
4222
4357
|
return Number.isFinite(v) && v > 0 ? v : def;
|
|
@@ -4229,19 +4364,19 @@ const BREAKER_MAX_WAKES = num("SVAMP_INBOX_BREAKER_MAX_WAKES", 30);
|
|
|
4229
4364
|
const BREAKER_COOLDOWN_MS = num("SVAMP_INBOX_BREAKER_COOLDOWN_MS", 12e4);
|
|
4230
4365
|
const CTX_STALE_MS = num("SVAMP_INBOX_CTX_STALE_MS", 30 * 6e4);
|
|
4231
4366
|
function ctxPath(sessionId) {
|
|
4232
|
-
return join
|
|
4367
|
+
return join(SVAMP_HOME$2, "inbound", `${sessionId}.json`);
|
|
4233
4368
|
}
|
|
4234
4369
|
function writeInboundContext(sessionId, ctx) {
|
|
4235
4370
|
try {
|
|
4236
4371
|
const p = ctxPath(sessionId);
|
|
4237
|
-
mkdirSync
|
|
4372
|
+
mkdirSync(join(SVAMP_HOME$2, "inbound"), { recursive: true });
|
|
4238
4373
|
const tmp = p + ".tmp";
|
|
4239
|
-
writeFileSync
|
|
4374
|
+
writeFileSync(tmp, JSON.stringify({ ...ctx, deliveredAt: Date.now() }));
|
|
4240
4375
|
try {
|
|
4241
4376
|
rmSync(p, { force: true });
|
|
4242
4377
|
} catch {
|
|
4243
4378
|
}
|
|
4244
|
-
writeFileSync
|
|
4379
|
+
writeFileSync(p, readFileSync(tmp));
|
|
4245
4380
|
try {
|
|
4246
4381
|
rmSync(tmp, { force: true });
|
|
4247
4382
|
} catch {
|
|
@@ -4528,8 +4663,8 @@ class SessionArtifactSync {
|
|
|
4528
4663
|
this.syncing = true;
|
|
4529
4664
|
try {
|
|
4530
4665
|
const artifactAlias = `session-${sessionId}`;
|
|
4531
|
-
const sessionJsonPath = join
|
|
4532
|
-
const messagesPath = join
|
|
4666
|
+
const sessionJsonPath = join(sessionsDir, "session.json");
|
|
4667
|
+
const messagesPath = join(sessionsDir, "messages.jsonl");
|
|
4533
4668
|
let sessionData = null;
|
|
4534
4669
|
if (existsSync(sessionJsonPath)) {
|
|
4535
4670
|
try {
|
|
@@ -4639,18 +4774,18 @@ class SessionArtifactSync {
|
|
|
4639
4774
|
artifact_id: artifactAlias,
|
|
4640
4775
|
_rkwargs: true
|
|
4641
4776
|
});
|
|
4642
|
-
if (!existsSync(targetDir)) mkdirSync
|
|
4777
|
+
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
4643
4778
|
try {
|
|
4644
4779
|
const data = await this.downloadFile(artifact.id, "session.json");
|
|
4645
4780
|
if (data) {
|
|
4646
|
-
writeFileSync
|
|
4781
|
+
writeFileSync(join(targetDir, "session.json"), data);
|
|
4647
4782
|
}
|
|
4648
4783
|
} catch {
|
|
4649
4784
|
}
|
|
4650
4785
|
try {
|
|
4651
4786
|
const data = await this.downloadFile(artifact.id, "messages.jsonl");
|
|
4652
4787
|
if (data) {
|
|
4653
|
-
writeFileSync
|
|
4788
|
+
writeFileSync(join(targetDir, "messages.jsonl"), data);
|
|
4654
4789
|
}
|
|
4655
4790
|
} catch {
|
|
4656
4791
|
}
|
|
@@ -4703,13 +4838,13 @@ class SessionArtifactSync {
|
|
|
4703
4838
|
*/
|
|
4704
4839
|
async syncAll(svampHome, machineId) {
|
|
4705
4840
|
if (!this.initialized) return;
|
|
4706
|
-
const indexFile = join
|
|
4841
|
+
const indexFile = join(svampHome, "sessions-index.json");
|
|
4707
4842
|
if (!existsSync(indexFile)) return;
|
|
4708
4843
|
try {
|
|
4709
4844
|
const index = JSON.parse(readFileSync(indexFile, "utf-8"));
|
|
4710
4845
|
for (const [sessionId, entry] of Object.entries(index)) {
|
|
4711
|
-
const sessionDir = join
|
|
4712
|
-
if (existsSync(join
|
|
4846
|
+
const sessionDir = join(entry.directory, ".svamp", sessionId);
|
|
4847
|
+
if (existsSync(join(sessionDir, "session.json"))) {
|
|
4713
4848
|
await this.syncSession(sessionId, sessionDir, void 0, machineId);
|
|
4714
4849
|
}
|
|
4715
4850
|
}
|
|
@@ -5025,7 +5160,7 @@ var DefaultTransport$1 = /*#__PURE__*/Object.freeze({
|
|
|
5025
5160
|
|
|
5026
5161
|
function expandHome(p) {
|
|
5027
5162
|
if (p === "~") return homedir();
|
|
5028
|
-
if (p.startsWith("~/")) return join
|
|
5163
|
+
if (p.startsWith("~/")) return join(homedir(), p.slice(2));
|
|
5029
5164
|
return p;
|
|
5030
5165
|
}
|
|
5031
5166
|
function wrapWithIsolation(originalCommand, originalArgs, config) {
|
|
@@ -5052,11 +5187,11 @@ function wrapWithNono(command, args, config) {
|
|
|
5052
5187
|
nonoArgs.push("--allow-cwd");
|
|
5053
5188
|
if (config.credentialStagingPath) {
|
|
5054
5189
|
env.HOME = config.credentialStagingPath;
|
|
5055
|
-
const realLocalDir = join
|
|
5190
|
+
const realLocalDir = join(homedir(), ".local");
|
|
5056
5191
|
if (existsSync(realLocalDir)) {
|
|
5057
5192
|
nonoArgs.push("--read", realLocalDir);
|
|
5058
5193
|
}
|
|
5059
|
-
const realKeychainDir = join
|
|
5194
|
+
const realKeychainDir = join(homedir(), "Library", "Keychains");
|
|
5060
5195
|
if (existsSync(realKeychainDir)) {
|
|
5061
5196
|
nonoArgs.push("--read", realKeychainDir);
|
|
5062
5197
|
}
|
|
@@ -5117,9 +5252,9 @@ function wrapWithContainer(runtime, command, args, config) {
|
|
|
5117
5252
|
config.workspacePath
|
|
5118
5253
|
];
|
|
5119
5254
|
if (config.credentialStagingPath) {
|
|
5120
|
-
const stagedClaudeDir = join
|
|
5255
|
+
const stagedClaudeDir = join(config.credentialStagingPath, ".claude");
|
|
5121
5256
|
containerArgs.push("-v", `${stagedClaudeDir}:/root/.claude:ro`);
|
|
5122
|
-
const stagedGitconfig = join
|
|
5257
|
+
const stagedGitconfig = join(config.credentialStagingPath, ".gitconfig");
|
|
5123
5258
|
containerArgs.push("-v", `${stagedGitconfig}:/root/.gitconfig:ro`);
|
|
5124
5259
|
containerArgs.push("-e", "HOME=/root");
|
|
5125
5260
|
}
|
|
@@ -6868,8 +7003,8 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
|
|
|
6868
7003
|
});
|
|
6869
7004
|
|
|
6870
7005
|
const execFileAsync = promisify$1(execFile$1);
|
|
6871
|
-
const SVAMP_TOOLS_DIR = join
|
|
6872
|
-
const SVAMP_BIN_DIR = join
|
|
7006
|
+
const SVAMP_TOOLS_DIR = join(homedir(), ".svamp", "tools");
|
|
7007
|
+
const SVAMP_BIN_DIR = join(SVAMP_TOOLS_DIR, "bin");
|
|
6873
7008
|
async function checkCommand(command, versionArgs) {
|
|
6874
7009
|
try {
|
|
6875
7010
|
const { stdout } = await execFileAsync(command, versionArgs, {
|
|
@@ -6879,7 +7014,7 @@ async function checkCommand(command, versionArgs) {
|
|
|
6879
7014
|
return { found: true, version, path: command };
|
|
6880
7015
|
} catch {
|
|
6881
7016
|
}
|
|
6882
|
-
const localPath = join
|
|
7017
|
+
const localPath = join(SVAMP_BIN_DIR, command);
|
|
6883
7018
|
try {
|
|
6884
7019
|
const { stdout } = await execFileAsync(localPath, versionArgs, {
|
|
6885
7020
|
timeout: 5e3
|
|
@@ -6946,7 +7081,7 @@ async function installNono() {
|
|
|
6946
7081
|
const downloadUrl = asset.browser_download_url;
|
|
6947
7082
|
console.log(`[isolation] Downloading nono ${version} from ${downloadUrl}...`);
|
|
6948
7083
|
await mkdir(SVAMP_BIN_DIR, { recursive: true });
|
|
6949
|
-
const tarball = join
|
|
7084
|
+
const tarball = join(SVAMP_BIN_DIR, assetName);
|
|
6950
7085
|
await execFileAsync("curl", [
|
|
6951
7086
|
"-fsSL",
|
|
6952
7087
|
"-o",
|
|
@@ -6963,7 +7098,7 @@ async function installNono() {
|
|
|
6963
7098
|
], { timeout: 15e3 });
|
|
6964
7099
|
await rm(tarball, { force: true }).catch(() => {
|
|
6965
7100
|
});
|
|
6966
|
-
const nonoPath = join
|
|
7101
|
+
const nonoPath = join(SVAMP_BIN_DIR, "nono");
|
|
6967
7102
|
await chmod(nonoPath, 493);
|
|
6968
7103
|
try {
|
|
6969
7104
|
await access(nonoPath);
|
|
@@ -7005,8 +7140,8 @@ async function parseIsolationTestOutput(stdout, probeFile) {
|
|
|
7005
7140
|
}
|
|
7006
7141
|
async function verifyNonoIsolation(binaryPath) {
|
|
7007
7142
|
const testBase = "/tmp";
|
|
7008
|
-
const workDir = await mkdtemp(join
|
|
7009
|
-
const probeFile = join
|
|
7143
|
+
const workDir = await mkdtemp(join(testBase, "svamp-iso-work-"));
|
|
7144
|
+
const probeFile = join(homedir(), `.svamp-iso-probe-${process.pid}`);
|
|
7010
7145
|
try {
|
|
7011
7146
|
const testScript = [
|
|
7012
7147
|
`echo ok > "${workDir}/test" 2>/dev/null; W=$?`,
|
|
@@ -7132,7 +7267,7 @@ async function detectIsolationCapabilities() {
|
|
|
7132
7267
|
return { available, preferred, details };
|
|
7133
7268
|
}
|
|
7134
7269
|
|
|
7135
|
-
const STAGED_HOMES_DIR = join
|
|
7270
|
+
const STAGED_HOMES_DIR = join(homedir(), ".svamp", "staged-homes");
|
|
7136
7271
|
const SENSITIVE_ENV_VARS = [
|
|
7137
7272
|
"HYPHA_TOKEN",
|
|
7138
7273
|
"HYPHA_CLIENT_ID",
|
|
@@ -7148,23 +7283,23 @@ const SENSITIVE_ENV_VARS = [
|
|
|
7148
7283
|
];
|
|
7149
7284
|
async function stageCredentialsForSharing(sessionId) {
|
|
7150
7285
|
const realHome = homedir();
|
|
7151
|
-
const realClaudeDir = join
|
|
7286
|
+
const realClaudeDir = join(realHome, ".claude");
|
|
7152
7287
|
await mkdir(STAGED_HOMES_DIR, { recursive: true });
|
|
7153
|
-
const tmpHome = join
|
|
7288
|
+
const tmpHome = join(STAGED_HOMES_DIR, sessionId);
|
|
7154
7289
|
await mkdir(tmpHome, { recursive: true });
|
|
7155
|
-
const stagedClaudeDir = join
|
|
7290
|
+
const stagedClaudeDir = join(tmpHome, ".claude");
|
|
7156
7291
|
await mkdir(stagedClaudeDir, { recursive: true });
|
|
7157
7292
|
const credentialFiles = ["credentials.json", ".credentials.json"];
|
|
7158
7293
|
let credentialsCopied = false;
|
|
7159
7294
|
for (const file of credentialFiles) {
|
|
7160
7295
|
try {
|
|
7161
|
-
await copyFile(join
|
|
7296
|
+
await copyFile(join(realClaudeDir, file), join(stagedClaudeDir, file));
|
|
7162
7297
|
credentialsCopied = true;
|
|
7163
7298
|
} catch {
|
|
7164
7299
|
}
|
|
7165
7300
|
}
|
|
7166
7301
|
if (!credentialsCopied && platform() === "darwin") {
|
|
7167
|
-
const stagedCredFile = join
|
|
7302
|
+
const stagedCredFile = join(stagedClaudeDir, ".credentials.json");
|
|
7168
7303
|
const hasExistingCredentials = existsSync(stagedCredFile);
|
|
7169
7304
|
if (!hasExistingCredentials) {
|
|
7170
7305
|
try {
|
|
@@ -7182,25 +7317,25 @@ async function stageCredentialsForSharing(sessionId) {
|
|
|
7182
7317
|
}
|
|
7183
7318
|
}
|
|
7184
7319
|
try {
|
|
7185
|
-
await copyFile(join
|
|
7320
|
+
await copyFile(join(realHome, ".gitconfig"), join(tmpHome, ".gitconfig"));
|
|
7186
7321
|
} catch {
|
|
7187
7322
|
}
|
|
7188
7323
|
try {
|
|
7189
7324
|
await copyFile(
|
|
7190
|
-
join
|
|
7191
|
-
join
|
|
7325
|
+
join(realHome, ".gitignore_global"),
|
|
7326
|
+
join(tmpHome, ".gitignore_global")
|
|
7192
7327
|
);
|
|
7193
7328
|
} catch {
|
|
7194
7329
|
}
|
|
7195
|
-
const claudeJsonPath = join
|
|
7330
|
+
const claudeJsonPath = join(tmpHome, ".claude.json");
|
|
7196
7331
|
if (!existsSync(claudeJsonPath)) {
|
|
7197
7332
|
try {
|
|
7198
7333
|
await writeFile(claudeJsonPath, "{}");
|
|
7199
7334
|
} catch {
|
|
7200
7335
|
}
|
|
7201
7336
|
}
|
|
7202
|
-
const realSkillsDir = join
|
|
7203
|
-
const stagedSkillsDir = join
|
|
7337
|
+
const realSkillsDir = join(realClaudeDir, "skills");
|
|
7338
|
+
const stagedSkillsDir = join(stagedClaudeDir, "skills");
|
|
7204
7339
|
try {
|
|
7205
7340
|
await copyDirRecursive(realSkillsDir, stagedSkillsDir);
|
|
7206
7341
|
} catch {
|
|
@@ -7234,7 +7369,7 @@ async function sweepOrphanedStagedHomes(activeSessionIds) {
|
|
|
7234
7369
|
for (const entry of entries) {
|
|
7235
7370
|
if (!entry.isDirectory()) continue;
|
|
7236
7371
|
if (active.has(entry.name)) continue;
|
|
7237
|
-
const path = join
|
|
7372
|
+
const path = join(STAGED_HOMES_DIR, entry.name);
|
|
7238
7373
|
try {
|
|
7239
7374
|
await rm(path, { recursive: true, force: true });
|
|
7240
7375
|
removed.push(entry.name);
|
|
@@ -7249,8 +7384,8 @@ async function copyDirRecursive(src, dest) {
|
|
|
7249
7384
|
await mkdir(dest, { recursive: true });
|
|
7250
7385
|
const entries = await readdir(src, { withFileTypes: true });
|
|
7251
7386
|
for (const entry of entries) {
|
|
7252
|
-
const srcPath = join
|
|
7253
|
-
const destPath = join
|
|
7387
|
+
const srcPath = join(src, entry.name);
|
|
7388
|
+
const destPath = join(dest, entry.name);
|
|
7254
7389
|
if (entry.isDirectory()) {
|
|
7255
7390
|
await copyDirRecursive(srcPath, destPath);
|
|
7256
7391
|
} else if (entry.isFile()) {
|
|
@@ -7302,8 +7437,8 @@ function resolveHyphaProxyUrl() {
|
|
|
7302
7437
|
}
|
|
7303
7438
|
}
|
|
7304
7439
|
function envFilePath() {
|
|
7305
|
-
const svampHome = process.env.SVAMP_HOME || join
|
|
7306
|
-
return join
|
|
7440
|
+
const svampHome = process.env.SVAMP_HOME || join(homedir(), ".svamp");
|
|
7441
|
+
return join(svampHome, ".env");
|
|
7307
7442
|
}
|
|
7308
7443
|
function readEnvLines() {
|
|
7309
7444
|
const file = envFilePath();
|
|
@@ -7312,12 +7447,12 @@ function readEnvLines() {
|
|
|
7312
7447
|
}
|
|
7313
7448
|
function writeEnvLines(lines) {
|
|
7314
7449
|
const file = envFilePath();
|
|
7315
|
-
const dir = join
|
|
7316
|
-
if (!existsSync(dir)) mkdirSync
|
|
7450
|
+
const dir = join(file, "..");
|
|
7451
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
7317
7452
|
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
7318
7453
|
lines.pop();
|
|
7319
7454
|
}
|
|
7320
|
-
writeFileSync
|
|
7455
|
+
writeFileSync(file, lines.join("\n") + "\n", "utf-8");
|
|
7321
7456
|
}
|
|
7322
7457
|
function updateEnvFile(updates) {
|
|
7323
7458
|
const lines = readEnvLines();
|
|
@@ -7468,10 +7603,10 @@ var claudeAuth = /*#__PURE__*/Object.freeze({
|
|
|
7468
7603
|
});
|
|
7469
7604
|
|
|
7470
7605
|
function svampHome() {
|
|
7471
|
-
return process.env.SVAMP_HOME || join(homedir$1(), ".svamp");
|
|
7606
|
+
return process.env.SVAMP_HOME || join$1(homedir$1(), ".svamp");
|
|
7472
7607
|
}
|
|
7473
7608
|
function cacheFile() {
|
|
7474
|
-
return join(svampHome(), "instance-config.json");
|
|
7609
|
+
return join$1(svampHome(), "instance-config.json");
|
|
7475
7610
|
}
|
|
7476
7611
|
const CONFIG_FILENAME = "svamp.json";
|
|
7477
7612
|
let _config = null;
|
|
@@ -7514,8 +7649,8 @@ function readCache() {
|
|
|
7514
7649
|
}
|
|
7515
7650
|
function writeCache(cfg) {
|
|
7516
7651
|
try {
|
|
7517
|
-
mkdirSync(svampHome(), { recursive: true });
|
|
7518
|
-
writeFileSync(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
7652
|
+
mkdirSync$1(svampHome(), { recursive: true });
|
|
7653
|
+
writeFileSync$1(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
7519
7654
|
} catch {
|
|
7520
7655
|
}
|
|
7521
7656
|
}
|
|
@@ -8033,7 +8168,7 @@ function escapeHtml(s) {
|
|
|
8033
8168
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
8034
8169
|
}
|
|
8035
8170
|
|
|
8036
|
-
const SKILLS_DIR = join(os$1.homedir(), ".claude", "skills");
|
|
8171
|
+
const SKILLS_DIR = join$1(os$1.homedir(), ".claude", "skills");
|
|
8037
8172
|
function getSkillsWorkspaceName() {
|
|
8038
8173
|
return getSkillsWorkspace();
|
|
8039
8174
|
}
|
|
@@ -8764,7 +8899,7 @@ const REFRESH_BUFFER_MS = 60 * 60 * 1e3;
|
|
|
8764
8899
|
const OAUTH_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
8765
8900
|
const REFRESH_TIMEOUT_MS = 15e3;
|
|
8766
8901
|
function getCredentialsPath() {
|
|
8767
|
-
return join
|
|
8902
|
+
return join(homedir(), ".claude", ".credentials.json");
|
|
8768
8903
|
}
|
|
8769
8904
|
async function readCredentials() {
|
|
8770
8905
|
const path = getCredentialsPath();
|
|
@@ -8925,14 +9060,14 @@ function resolveContextWindow(opts) {
|
|
|
8925
9060
|
return candidate;
|
|
8926
9061
|
}
|
|
8927
9062
|
|
|
8928
|
-
const SVAMP_HOME$1 = process.env.SVAMP_HOME || join
|
|
9063
|
+
const SVAMP_HOME$1 = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
|
|
8929
9064
|
function generateHookSettings(portOrOptions = {}) {
|
|
8930
9065
|
const opts = typeof portOrOptions === "number" ? { sessionStartPort: portOrOptions } : portOrOptions;
|
|
8931
|
-
const hooksDir = join
|
|
8932
|
-
mkdirSync
|
|
9066
|
+
const hooksDir = join(SVAMP_HOME$1, "tmp", "hooks");
|
|
9067
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
8933
9068
|
const id = opts.id || String(process.pid);
|
|
8934
|
-
const validatorPath = join
|
|
8935
|
-
writeFileSync
|
|
9069
|
+
const validatorPath = join(hooksDir, `image-validator-${id}.cjs`);
|
|
9070
|
+
writeFileSync(validatorPath, IMAGE_VALIDATOR_SCRIPT, { mode: 493 });
|
|
8936
9071
|
const cleanupPaths = [validatorPath];
|
|
8937
9072
|
const hooks = {
|
|
8938
9073
|
PreToolUse: [
|
|
@@ -8949,7 +9084,7 @@ function generateHookSettings(portOrOptions = {}) {
|
|
|
8949
9084
|
]
|
|
8950
9085
|
};
|
|
8951
9086
|
if (typeof opts.sessionStartPort === "number" && opts.sessionStartPort > 0) {
|
|
8952
|
-
const forwarderPath = join
|
|
9087
|
+
const forwarderPath = join(hooksDir, `forwarder-${id}.cjs`);
|
|
8953
9088
|
const forwarderCode = `#!/usr/bin/env node
|
|
8954
9089
|
const http = require('http');
|
|
8955
9090
|
const port = parseInt(process.argv[2], 10);
|
|
@@ -8968,7 +9103,7 @@ process.stdin.on('end', () => {
|
|
|
8968
9103
|
});
|
|
8969
9104
|
process.stdin.resume();
|
|
8970
9105
|
`;
|
|
8971
|
-
writeFileSync
|
|
9106
|
+
writeFileSync(forwarderPath, forwarderCode, { mode: 493 });
|
|
8972
9107
|
cleanupPaths.push(forwarderPath);
|
|
8973
9108
|
hooks.SessionStart = [
|
|
8974
9109
|
{
|
|
@@ -8982,8 +9117,8 @@ process.stdin.resume();
|
|
|
8982
9117
|
}
|
|
8983
9118
|
];
|
|
8984
9119
|
}
|
|
8985
|
-
const settingsPath = join
|
|
8986
|
-
writeFileSync
|
|
9120
|
+
const settingsPath = join(hooksDir, `session-hook-${id}.json`);
|
|
9121
|
+
writeFileSync(settingsPath, JSON.stringify({ hooks }, null, 2));
|
|
8987
9122
|
cleanupPaths.push(settingsPath);
|
|
8988
9123
|
const cleanup = () => {
|
|
8989
9124
|
for (const p of cleanupPaths) {
|
|
@@ -9145,7 +9280,7 @@ async function readSessionFileBase64(resolvedPath) {
|
|
|
9145
9280
|
|
|
9146
9281
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
9147
9282
|
const __dirname$1 = dirname(__filename$1);
|
|
9148
|
-
const CLAUDE_SKILLS_DIR = join(os$1.homedir(), ".claude", "skills");
|
|
9283
|
+
const CLAUDE_SKILLS_DIR = join$1(os$1.homedir(), ".claude", "skills");
|
|
9149
9284
|
function looksLikeClaudeError(line) {
|
|
9150
9285
|
const l = line.toLowerCase();
|
|
9151
9286
|
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 +9320,7 @@ function buildClaudeErrorHint(text, apiErrorStatus) {
|
|
|
9185
9320
|
}
|
|
9186
9321
|
function readSkillVersion(skillDir) {
|
|
9187
9322
|
try {
|
|
9188
|
-
const md = readFileSync$1(join(skillDir, "SKILL.md"), "utf-8");
|
|
9323
|
+
const md = readFileSync$1(join$1(skillDir, "SKILL.md"), "utf-8");
|
|
9189
9324
|
const m = md.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
9190
9325
|
if (!m) return null;
|
|
9191
9326
|
const versionLine = m[1].split("\n").find((l) => /^\s*version\s*:/.test(l));
|
|
@@ -9211,18 +9346,18 @@ async function installSkillFromEndpoint(name, baseUrl) {
|
|
|
9211
9346
|
const index = await resp.json();
|
|
9212
9347
|
const files = index.files || [];
|
|
9213
9348
|
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 });
|
|
9349
|
+
const targetDir = join$1(CLAUDE_SKILLS_DIR, name);
|
|
9350
|
+
mkdirSync$1(targetDir, { recursive: true });
|
|
9216
9351
|
for (const filePath of files) {
|
|
9217
9352
|
if (!filePath) continue;
|
|
9218
9353
|
const url = `${baseUrl}${filePath}`;
|
|
9219
9354
|
const fileResp = await fetch(url, { signal: AbortSignal.timeout(3e4) });
|
|
9220
9355
|
if (!fileResp.ok) throw new Error(`Failed to download ${filePath}: HTTP ${fileResp.status}`);
|
|
9221
9356
|
const content = await fileResp.text();
|
|
9222
|
-
const localPath = join(targetDir, filePath);
|
|
9357
|
+
const localPath = join$1(targetDir, filePath);
|
|
9223
9358
|
if (!localPath.startsWith(targetDir + "/")) continue;
|
|
9224
|
-
mkdirSync(dirname(localPath), { recursive: true });
|
|
9225
|
-
writeFileSync(localPath, content, "utf-8");
|
|
9359
|
+
mkdirSync$1(dirname(localPath), { recursive: true });
|
|
9360
|
+
writeFileSync$1(localPath, content, "utf-8");
|
|
9226
9361
|
}
|
|
9227
9362
|
}
|
|
9228
9363
|
async function installSkillFromMarketplace(name) {
|
|
@@ -9246,26 +9381,26 @@ async function installSkillFromMarketplace(name) {
|
|
|
9246
9381
|
}
|
|
9247
9382
|
const files = await collectFiles();
|
|
9248
9383
|
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 });
|
|
9384
|
+
const targetDir = join$1(CLAUDE_SKILLS_DIR, name);
|
|
9385
|
+
mkdirSync$1(targetDir, { recursive: true });
|
|
9251
9386
|
for (const filePath of files) {
|
|
9252
9387
|
const url = `${BASE}/files/${filePath}`;
|
|
9253
9388
|
const resp = await fetch(url, { signal: AbortSignal.timeout(3e4) });
|
|
9254
9389
|
if (!resp.ok) throw new Error(`Failed to download ${filePath}: HTTP ${resp.status}`);
|
|
9255
9390
|
const content = await resp.text();
|
|
9256
|
-
const localPath = join(targetDir, filePath);
|
|
9391
|
+
const localPath = join$1(targetDir, filePath);
|
|
9257
9392
|
if (!localPath.startsWith(targetDir + "/")) continue;
|
|
9258
|
-
mkdirSync(dirname(localPath), { recursive: true });
|
|
9259
|
-
writeFileSync(localPath, content, "utf-8");
|
|
9393
|
+
mkdirSync$1(dirname(localPath), { recursive: true });
|
|
9394
|
+
writeFileSync$1(localPath, content, "utf-8");
|
|
9260
9395
|
}
|
|
9261
9396
|
}
|
|
9262
9397
|
function getBundledSkillsDir() {
|
|
9263
9398
|
try {
|
|
9264
9399
|
const here = fileURLToPath(import.meta.url);
|
|
9265
9400
|
const candidates = [
|
|
9266
|
-
join(dirname(here), "..", "bin", "skills"),
|
|
9401
|
+
join$1(dirname(here), "..", "bin", "skills"),
|
|
9267
9402
|
// built dist/ layout
|
|
9268
|
-
join(dirname(here), "..", "..", "bin", "skills")
|
|
9403
|
+
join$1(dirname(here), "..", "..", "bin", "skills")
|
|
9269
9404
|
// src/daemon → bin layout via tsx
|
|
9270
9405
|
];
|
|
9271
9406
|
for (const c of candidates) {
|
|
@@ -9278,17 +9413,17 @@ function getBundledSkillsDir() {
|
|
|
9278
9413
|
function installBundledSkill(name) {
|
|
9279
9414
|
const bundledDir = getBundledSkillsDir();
|
|
9280
9415
|
if (!bundledDir) throw new Error(`Bundled skills directory not found`);
|
|
9281
|
-
const src = join(bundledDir, name);
|
|
9416
|
+
const src = join$1(bundledDir, name);
|
|
9282
9417
|
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 });
|
|
9418
|
+
const dst = join$1(CLAUDE_SKILLS_DIR, name);
|
|
9419
|
+
mkdirSync$1(dst, { recursive: true });
|
|
9285
9420
|
function copyDir(s, d) {
|
|
9286
|
-
mkdirSync(d, { recursive: true });
|
|
9421
|
+
mkdirSync$1(d, { recursive: true });
|
|
9287
9422
|
for (const entry of readdirSync$1(s, { withFileTypes: true })) {
|
|
9288
|
-
const sp = join(s, entry.name);
|
|
9289
|
-
const dp = join(d, entry.name);
|
|
9423
|
+
const sp = join$1(s, entry.name);
|
|
9424
|
+
const dp = join$1(d, entry.name);
|
|
9290
9425
|
if (entry.isDirectory()) copyDir(sp, dp);
|
|
9291
|
-
else if (entry.isFile()) writeFileSync(dp, readFileSync$1(sp));
|
|
9426
|
+
else if (entry.isFile()) writeFileSync$1(dp, readFileSync$1(sp));
|
|
9292
9427
|
}
|
|
9293
9428
|
}
|
|
9294
9429
|
copyDir(src, dst);
|
|
@@ -9296,7 +9431,7 @@ function installBundledSkill(name) {
|
|
|
9296
9431
|
function readBundledSkillVersion(name) {
|
|
9297
9432
|
const bundledDir = getBundledSkillsDir();
|
|
9298
9433
|
if (!bundledDir) return null;
|
|
9299
|
-
return readSkillVersion(join(bundledDir, name));
|
|
9434
|
+
return readSkillVersion(join$1(bundledDir, name));
|
|
9300
9435
|
}
|
|
9301
9436
|
function preventMachineSleep(logger) {
|
|
9302
9437
|
if (process.platform === "darwin") {
|
|
@@ -9404,7 +9539,7 @@ async function ensureAutoInstalledSkills(logger) {
|
|
|
9404
9539
|
}
|
|
9405
9540
|
];
|
|
9406
9541
|
for (const task of tasks) {
|
|
9407
|
-
const targetDir = join(CLAUDE_SKILLS_DIR, task.name);
|
|
9542
|
+
const targetDir = join$1(CLAUDE_SKILLS_DIR, task.name);
|
|
9408
9543
|
const installed = existsSync$1(targetDir);
|
|
9409
9544
|
if (!installed) {
|
|
9410
9545
|
try {
|
|
@@ -9445,20 +9580,20 @@ function loadEnvFile(path) {
|
|
|
9445
9580
|
return true;
|
|
9446
9581
|
}
|
|
9447
9582
|
function loadDotEnv() {
|
|
9448
|
-
const svampEnv = join(process.env.SVAMP_HOME || os$1.homedir() + "/.svamp", ".env");
|
|
9583
|
+
const svampEnv = join$1(process.env.SVAMP_HOME || os$1.homedir() + "/.svamp", ".env");
|
|
9449
9584
|
if (!loadEnvFile(svampEnv)) {
|
|
9450
|
-
const hyphaEnv = join(process.env.HYPHA_HOME || os$1.homedir() + "/.hypha", ".env");
|
|
9585
|
+
const hyphaEnv = join$1(process.env.HYPHA_HOME || os$1.homedir() + "/.hypha", ".env");
|
|
9451
9586
|
loadEnvFile(hyphaEnv);
|
|
9452
9587
|
}
|
|
9453
9588
|
}
|
|
9454
9589
|
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");
|
|
9590
|
+
const SVAMP_HOME = process.env.SVAMP_HOME || join$1(os$1.homedir(), ".svamp");
|
|
9591
|
+
const DAEMON_STATE_FILE = join$1(SVAMP_HOME, "daemon.state.json");
|
|
9592
|
+
const DAEMON_LOCK_FILE = join$1(SVAMP_HOME, "daemon.lock");
|
|
9593
|
+
const DAEMON_STOP_MARKER_FILE = join$1(SVAMP_HOME, "daemon.stop");
|
|
9459
9594
|
function writeStopMarker(reason) {
|
|
9460
9595
|
try {
|
|
9461
|
-
writeFileSync(DAEMON_STOP_MARKER_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${reason}
|
|
9596
|
+
writeFileSync$1(DAEMON_STOP_MARKER_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${reason}
|
|
9462
9597
|
`, "utf-8");
|
|
9463
9598
|
} catch {
|
|
9464
9599
|
}
|
|
@@ -9476,11 +9611,11 @@ function stopMarkerExists() {
|
|
|
9476
9611
|
return false;
|
|
9477
9612
|
}
|
|
9478
9613
|
}
|
|
9479
|
-
const LOGS_DIR = join(SVAMP_HOME, "logs");
|
|
9480
|
-
const SESSION_INDEX_FILE = join(SVAMP_HOME, "sessions-index.json");
|
|
9614
|
+
const LOGS_DIR = join$1(SVAMP_HOME, "logs");
|
|
9615
|
+
const SESSION_INDEX_FILE = join$1(SVAMP_HOME, "sessions-index.json");
|
|
9481
9616
|
function readPackageVersion() {
|
|
9482
9617
|
try {
|
|
9483
|
-
const pkgPath = join(__dirname$1, "../package.json");
|
|
9618
|
+
const pkgPath = join$1(__dirname$1, "../package.json");
|
|
9484
9619
|
if (existsSync$1(pkgPath)) {
|
|
9485
9620
|
return JSON.parse(readFileSync$1(pkgPath, "utf-8")).version || "unknown";
|
|
9486
9621
|
}
|
|
@@ -9490,7 +9625,7 @@ function readPackageVersion() {
|
|
|
9490
9625
|
}
|
|
9491
9626
|
const DAEMON_VERSION = readPackageVersion();
|
|
9492
9627
|
function loadAgentConfig() {
|
|
9493
|
-
const configPath = join(SVAMP_HOME, "agent-config.json");
|
|
9628
|
+
const configPath = join$1(SVAMP_HOME, "agent-config.json");
|
|
9494
9629
|
if (existsSync$1(configPath)) {
|
|
9495
9630
|
try {
|
|
9496
9631
|
return JSON.parse(readFileSync$1(configPath, "utf-8"));
|
|
@@ -9501,19 +9636,19 @@ function loadAgentConfig() {
|
|
|
9501
9636
|
return {};
|
|
9502
9637
|
}
|
|
9503
9638
|
function getSessionSvampDir(directory) {
|
|
9504
|
-
return join(directory, ".svamp");
|
|
9639
|
+
return join$1(directory, ".svamp");
|
|
9505
9640
|
}
|
|
9506
9641
|
function getSessionDir(directory, sessionId) {
|
|
9507
|
-
return join(getSessionSvampDir(directory), sessionId);
|
|
9642
|
+
return join$1(getSessionSvampDir(directory), sessionId);
|
|
9508
9643
|
}
|
|
9509
9644
|
function getSessionFilePath(directory, sessionId) {
|
|
9510
|
-
return join(getSessionDir(directory, sessionId), "session.json");
|
|
9645
|
+
return join$1(getSessionDir(directory, sessionId), "session.json");
|
|
9511
9646
|
}
|
|
9512
9647
|
function getSessionMessagesPath(directory, sessionId) {
|
|
9513
|
-
return join(getSessionDir(directory, sessionId), "messages.jsonl");
|
|
9648
|
+
return join$1(getSessionDir(directory, sessionId), "messages.jsonl");
|
|
9514
9649
|
}
|
|
9515
9650
|
function getSvampConfigPath(directory, sessionId) {
|
|
9516
|
-
return join(getSessionDir(directory, sessionId), "config.json");
|
|
9651
|
+
return join$1(getSessionDir(directory, sessionId), "config.json");
|
|
9517
9652
|
}
|
|
9518
9653
|
function readSvampConfig(configPath) {
|
|
9519
9654
|
try {
|
|
@@ -9523,19 +9658,19 @@ function readSvampConfig(configPath) {
|
|
|
9523
9658
|
return {};
|
|
9524
9659
|
}
|
|
9525
9660
|
function writeSvampConfig(configPath, config) {
|
|
9526
|
-
mkdirSync(dirname(configPath), { recursive: true });
|
|
9661
|
+
mkdirSync$1(dirname(configPath), { recursive: true });
|
|
9527
9662
|
const content = JSON.stringify(config, null, 2);
|
|
9528
9663
|
const tmpPath = configPath + ".tmp";
|
|
9529
|
-
writeFileSync(tmpPath, content);
|
|
9530
|
-
renameSync(tmpPath, configPath);
|
|
9664
|
+
writeFileSync$1(tmpPath, content);
|
|
9665
|
+
renameSync$1(tmpPath, configPath);
|
|
9531
9666
|
return content;
|
|
9532
9667
|
}
|
|
9533
9668
|
function getLoopDir(directory) {
|
|
9534
|
-
return join(directory, ".claude", "loop");
|
|
9669
|
+
return join$1(directory, ".claude", "loop");
|
|
9535
9670
|
}
|
|
9536
9671
|
function readLoopState(directory) {
|
|
9537
9672
|
try {
|
|
9538
|
-
const p = join(getLoopDir(directory), "loop-state.json");
|
|
9673
|
+
const p = join$1(getLoopDir(directory), "loop-state.json");
|
|
9539
9674
|
if (!existsSync$1(p)) return null;
|
|
9540
9675
|
return JSON.parse(readFileSync$1(p, "utf-8"));
|
|
9541
9676
|
} catch {
|
|
@@ -9546,10 +9681,21 @@ function isLoopActive(directory) {
|
|
|
9546
9681
|
const s = readLoopState(directory);
|
|
9547
9682
|
return !!s && s.active !== false && s.phase !== "done" && s.phase !== "gave_up" && s.phase !== "cancelled";
|
|
9548
9683
|
}
|
|
9684
|
+
function loopOwnerSession(directory) {
|
|
9685
|
+
const s = readLoopState(directory);
|
|
9686
|
+
if (!s || s.active === false || s.phase === "done" || s.phase === "gave_up" || s.phase === "cancelled") return null;
|
|
9687
|
+
return typeof s.session_id === "string" ? s.session_id : null;
|
|
9688
|
+
}
|
|
9689
|
+
function isLoopActiveForSession(directory, sessionId) {
|
|
9690
|
+
const s = readLoopState(directory);
|
|
9691
|
+
if (!s || s.active === false || s.phase === "done" || s.phase === "gave_up" || s.phase === "cancelled") return false;
|
|
9692
|
+
if (typeof s.session_id !== "string") return true;
|
|
9693
|
+
return s.session_id === sessionId;
|
|
9694
|
+
}
|
|
9549
9695
|
function resolveLoopInit() {
|
|
9550
9696
|
const candidates = [
|
|
9551
|
-
join(CLAUDE_SKILLS_DIR, "loop", "bin", "loop-init.mjs"),
|
|
9552
|
-
...getBundledSkillsDir() ? [join(getBundledSkillsDir(), "loop", "bin", "loop-init.mjs")] : []
|
|
9697
|
+
join$1(CLAUDE_SKILLS_DIR, "loop", "bin", "loop-init.mjs"),
|
|
9698
|
+
...getBundledSkillsDir() ? [join$1(getBundledSkillsDir(), "loop", "bin", "loop-init.mjs")] : []
|
|
9553
9699
|
];
|
|
9554
9700
|
for (const c of candidates) if (existsSync$1(c)) return c;
|
|
9555
9701
|
return null;
|
|
@@ -9563,19 +9709,20 @@ function initLoop(directory, cfg) {
|
|
|
9563
9709
|
if (typeof cfg.maxIterations === "number") args.push("--max", String(cfg.maxIterations));
|
|
9564
9710
|
args.push("--evaluator", cfg.evaluator === false ? "off" : "on");
|
|
9565
9711
|
if (cfg.model) args.push("--model", cfg.model);
|
|
9712
|
+
if (cfg.sessionId) args.push("--session", cfg.sessionId);
|
|
9566
9713
|
const res = spawnSync(process.execPath, args, { encoding: "utf-8", timeout: 3e4 });
|
|
9567
9714
|
return res.status === 0;
|
|
9568
9715
|
}
|
|
9569
9716
|
function deactivateLoop(directory) {
|
|
9570
9717
|
try {
|
|
9571
|
-
const p = join(getLoopDir(directory), "loop-state.json");
|
|
9718
|
+
const p = join$1(getLoopDir(directory), "loop-state.json");
|
|
9572
9719
|
if (!existsSync$1(p)) return;
|
|
9573
9720
|
const s = JSON.parse(readFileSync$1(p, "utf-8"));
|
|
9574
9721
|
s.active = false;
|
|
9575
9722
|
s.phase = "cancelled";
|
|
9576
9723
|
const tmp = p + ".tmp";
|
|
9577
|
-
writeFileSync(tmp, JSON.stringify(s, null, 2));
|
|
9578
|
-
renameSync(tmp, p);
|
|
9724
|
+
writeFileSync$1(tmp, JSON.stringify(s, null, 2));
|
|
9725
|
+
renameSync$1(tmp, p);
|
|
9579
9726
|
} catch {
|
|
9580
9727
|
}
|
|
9581
9728
|
}
|
|
@@ -9664,7 +9811,8 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9664
9811
|
criteria: typeof lp.criteria === "string" && lp.criteria.trim() ? lp.criteria.trim() : void 0,
|
|
9665
9812
|
oracle,
|
|
9666
9813
|
maxIterations,
|
|
9667
|
-
evaluator
|
|
9814
|
+
evaluator,
|
|
9815
|
+
sessionId
|
|
9668
9816
|
});
|
|
9669
9817
|
if (ok) {
|
|
9670
9818
|
const existingQueue = getMetadata().messageQueue || [];
|
|
@@ -9712,7 +9860,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9712
9860
|
let watcher = null;
|
|
9713
9861
|
try {
|
|
9714
9862
|
const configDir = dirname(configPath);
|
|
9715
|
-
mkdirSync(configDir, { recursive: true });
|
|
9863
|
+
mkdirSync$1(configDir, { recursive: true });
|
|
9716
9864
|
watcher = watch(configDir, (eventType, filename) => {
|
|
9717
9865
|
if (filename === "config.json") configChecker();
|
|
9718
9866
|
});
|
|
@@ -9738,18 +9886,18 @@ function loadSessionIndex() {
|
|
|
9738
9886
|
}
|
|
9739
9887
|
function saveSessionIndex(index) {
|
|
9740
9888
|
const tmp = SESSION_INDEX_FILE + ".tmp";
|
|
9741
|
-
writeFileSync(tmp, JSON.stringify(index, null, 2), "utf-8");
|
|
9742
|
-
renameSync(tmp, SESSION_INDEX_FILE);
|
|
9889
|
+
writeFileSync$1(tmp, JSON.stringify(index, null, 2), "utf-8");
|
|
9890
|
+
renameSync$1(tmp, SESSION_INDEX_FILE);
|
|
9743
9891
|
}
|
|
9744
9892
|
function saveSession(session) {
|
|
9745
9893
|
const sessionDir = getSessionDir(session.directory, session.sessionId);
|
|
9746
9894
|
if (!existsSync$1(sessionDir)) {
|
|
9747
|
-
mkdirSync(sessionDir, { recursive: true });
|
|
9895
|
+
mkdirSync$1(sessionDir, { recursive: true });
|
|
9748
9896
|
}
|
|
9749
9897
|
const filePath = getSessionFilePath(session.directory, session.sessionId);
|
|
9750
9898
|
const tmpPath = filePath + ".tmp";
|
|
9751
|
-
writeFileSync(tmpPath, JSON.stringify(session, null, 2), "utf-8");
|
|
9752
|
-
renameSync(tmpPath, filePath);
|
|
9899
|
+
writeFileSync$1(tmpPath, JSON.stringify(session, null, 2), "utf-8");
|
|
9900
|
+
renameSync$1(tmpPath, filePath);
|
|
9753
9901
|
const index = loadSessionIndex();
|
|
9754
9902
|
index[session.sessionId] = { directory: session.directory, createdAt: session.createdAt };
|
|
9755
9903
|
saveSessionIndex(index);
|
|
@@ -9793,8 +9941,8 @@ function markSessionAsArchived(sessionId) {
|
|
|
9793
9941
|
if (data.stopped === true) return true;
|
|
9794
9942
|
data.stopped = true;
|
|
9795
9943
|
const tmpPath = filePath + ".tmp";
|
|
9796
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
9797
|
-
renameSync(tmpPath, filePath);
|
|
9944
|
+
writeFileSync$1(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
9945
|
+
renameSync$1(tmpPath, filePath);
|
|
9798
9946
|
return true;
|
|
9799
9947
|
} catch {
|
|
9800
9948
|
return false;
|
|
@@ -9811,8 +9959,8 @@ function clearSessionArchivedFlag(sessionId) {
|
|
|
9811
9959
|
if (data.stopped) {
|
|
9812
9960
|
delete data.stopped;
|
|
9813
9961
|
const tmpPath = filePath + ".tmp";
|
|
9814
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
9815
|
-
renameSync(tmpPath, filePath);
|
|
9962
|
+
writeFileSync$1(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
9963
|
+
renameSync$1(tmpPath, filePath);
|
|
9816
9964
|
}
|
|
9817
9965
|
return data;
|
|
9818
9966
|
} catch {
|
|
@@ -9844,15 +9992,15 @@ function loadPersistedSessions() {
|
|
|
9844
9992
|
}
|
|
9845
9993
|
function ensureHomeDir() {
|
|
9846
9994
|
if (!existsSync$1(SVAMP_HOME)) {
|
|
9847
|
-
mkdirSync(SVAMP_HOME, { recursive: true });
|
|
9995
|
+
mkdirSync$1(SVAMP_HOME, { recursive: true });
|
|
9848
9996
|
}
|
|
9849
9997
|
if (!existsSync$1(LOGS_DIR)) {
|
|
9850
|
-
mkdirSync(LOGS_DIR, { recursive: true });
|
|
9998
|
+
mkdirSync$1(LOGS_DIR, { recursive: true });
|
|
9851
9999
|
}
|
|
9852
10000
|
}
|
|
9853
10001
|
function createLogger() {
|
|
9854
10002
|
ensureHomeDir();
|
|
9855
|
-
const logFile = join(LOGS_DIR, `daemon-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.log`);
|
|
10003
|
+
const logFile = join$1(LOGS_DIR, `daemon-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.log`);
|
|
9856
10004
|
return {
|
|
9857
10005
|
logFilePath: logFile,
|
|
9858
10006
|
log: (...args) => {
|
|
@@ -9876,8 +10024,8 @@ function createLogger() {
|
|
|
9876
10024
|
function writeDaemonStateFile(state) {
|
|
9877
10025
|
ensureHomeDir();
|
|
9878
10026
|
const tmpPath = DAEMON_STATE_FILE + ".tmp";
|
|
9879
|
-
writeFileSync(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
9880
|
-
renameSync(tmpPath, DAEMON_STATE_FILE);
|
|
10027
|
+
writeFileSync$1(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
10028
|
+
renameSync$1(tmpPath, DAEMON_STATE_FILE);
|
|
9881
10029
|
}
|
|
9882
10030
|
function readDaemonStateFile() {
|
|
9883
10031
|
try {
|
|
@@ -10076,7 +10224,7 @@ async function startDaemon(options) {
|
|
|
10076
10224
|
logger.log('Warning: No HYPHA_TOKEN set. Run "svamp login" to authenticate.');
|
|
10077
10225
|
logger.log("Connecting anonymously...");
|
|
10078
10226
|
}
|
|
10079
|
-
const machineIdFile = join(SVAMP_HOME, "machine-id");
|
|
10227
|
+
const machineIdFile = join$1(SVAMP_HOME, "machine-id");
|
|
10080
10228
|
let machineId = process.env.SVAMP_MACHINE_ID;
|
|
10081
10229
|
if (!machineId) {
|
|
10082
10230
|
if (existsSync$1(machineIdFile)) {
|
|
@@ -10085,7 +10233,7 @@ async function startDaemon(options) {
|
|
|
10085
10233
|
if (!machineId) {
|
|
10086
10234
|
machineId = `machine-${os$1.hostname()}-${randomUUID$1().slice(0, 8)}`;
|
|
10087
10235
|
try {
|
|
10088
|
-
writeFileSync(machineIdFile, machineId, "utf-8");
|
|
10236
|
+
writeFileSync$1(machineIdFile, machineId, "utf-8");
|
|
10089
10237
|
} catch {
|
|
10090
10238
|
}
|
|
10091
10239
|
}
|
|
@@ -10095,10 +10243,10 @@ async function startDaemon(options) {
|
|
|
10095
10243
|
logger.log(` Workspace: ${hyphaWorkspace || "(default)"}`);
|
|
10096
10244
|
logger.log(` Machine ID: ${machineId}`);
|
|
10097
10245
|
let server = null;
|
|
10098
|
-
const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
|
|
10246
|
+
const supervisor = new ProcessSupervisor(join$1(SVAMP_HOME, "processes"));
|
|
10099
10247
|
await supervisor.init();
|
|
10100
10248
|
const tunnels = /* @__PURE__ */ new Map();
|
|
10101
|
-
const EXPOSED_TUNNELS_FILE = join(SVAMP_HOME, "exposed-tunnels.json");
|
|
10249
|
+
const EXPOSED_TUNNELS_FILE = join$1(SVAMP_HOME, "exposed-tunnels.json");
|
|
10102
10250
|
function loadExposedTunnels() {
|
|
10103
10251
|
try {
|
|
10104
10252
|
if (!existsSync$1(EXPOSED_TUNNELS_FILE)) return [];
|
|
@@ -10111,8 +10259,8 @@ async function startDaemon(options) {
|
|
|
10111
10259
|
}
|
|
10112
10260
|
function saveExposedTunnels(specs) {
|
|
10113
10261
|
try {
|
|
10114
|
-
mkdirSync(SVAMP_HOME, { recursive: true });
|
|
10115
|
-
writeFileSync(EXPOSED_TUNNELS_FILE, JSON.stringify({ tunnels: specs }, null, 2));
|
|
10262
|
+
mkdirSync$1(SVAMP_HOME, { recursive: true });
|
|
10263
|
+
writeFileSync$1(EXPOSED_TUNNELS_FILE, JSON.stringify({ tunnels: specs }, null, 2));
|
|
10116
10264
|
} catch (err) {
|
|
10117
10265
|
logger.log(`[exposed-tunnels] Persist failed: ${err.message}`);
|
|
10118
10266
|
}
|
|
@@ -10126,7 +10274,7 @@ async function startDaemon(options) {
|
|
|
10126
10274
|
const list = loadExposedTunnels().filter((t) => t.name !== name);
|
|
10127
10275
|
saveExposedTunnels(list);
|
|
10128
10276
|
}
|
|
10129
|
-
const { ServeManager } = await import('./serveManager-
|
|
10277
|
+
const { ServeManager } = await import('./serveManager-Bq33kB5r.mjs');
|
|
10130
10278
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
10131
10279
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
10132
10280
|
});
|
|
@@ -10238,7 +10386,7 @@ async function startDaemon(options) {
|
|
|
10238
10386
|
}
|
|
10239
10387
|
}, shouldAutoAllow2 = function(toolName, toolInput) {
|
|
10240
10388
|
if (toolName === "AskUserQuestion") {
|
|
10241
|
-
return
|
|
10389
|
+
return isLoopActiveForSession(directory, sessionId);
|
|
10242
10390
|
}
|
|
10243
10391
|
if (toolName === "Bash") {
|
|
10244
10392
|
const inputObj = toolInput;
|
|
@@ -10251,7 +10399,7 @@ async function startDaemon(options) {
|
|
|
10251
10399
|
} else if (allowedTools.has(toolName)) {
|
|
10252
10400
|
return true;
|
|
10253
10401
|
}
|
|
10254
|
-
if (
|
|
10402
|
+
if (isLoopActiveForSession(directory, sessionId)) return true;
|
|
10255
10403
|
if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
|
|
10256
10404
|
if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
|
|
10257
10405
|
return false;
|
|
@@ -10317,8 +10465,8 @@ async function startDaemon(options) {
|
|
|
10317
10465
|
machineId,
|
|
10318
10466
|
homeDir: os$1.homedir(),
|
|
10319
10467
|
svampHomeDir: SVAMP_HOME,
|
|
10320
|
-
svampLibDir: join(__dirname$1, ".."),
|
|
10321
|
-
svampToolsDir: join(__dirname$1, "..", "tools"),
|
|
10468
|
+
svampLibDir: join$1(__dirname$1, ".."),
|
|
10469
|
+
svampToolsDir: join$1(__dirname$1, "..", "tools"),
|
|
10322
10470
|
startedFromDaemon: true,
|
|
10323
10471
|
startedBy: "daemon",
|
|
10324
10472
|
lifecycleState: resumeSessionId ? "idle" : "starting",
|
|
@@ -10425,7 +10573,7 @@ async function startDaemon(options) {
|
|
|
10425
10573
|
if (persisted && persisted.sessionId !== sessionId) {
|
|
10426
10574
|
const oldDir = persisted.directory || directory;
|
|
10427
10575
|
const newSessionDir = getSessionDir(directory, sessionId);
|
|
10428
|
-
if (!existsSync$1(newSessionDir)) mkdirSync(newSessionDir, { recursive: true });
|
|
10576
|
+
if (!existsSync$1(newSessionDir)) mkdirSync$1(newSessionDir, { recursive: true });
|
|
10429
10577
|
const oldMsgFile = getSessionMessagesPath(oldDir, persisted.sessionId);
|
|
10430
10578
|
const newMsgFile = getSessionMessagesPath(directory, sessionId);
|
|
10431
10579
|
try {
|
|
@@ -10616,6 +10764,12 @@ async function startDaemon(options) {
|
|
|
10616
10764
|
logger.log(`[Session ${sessionId}] Permission request: ${requestId} (corr=${correlationId}) tool=${toolName}`);
|
|
10617
10765
|
if (shouldAutoAllow2(toolName, toolInput)) {
|
|
10618
10766
|
logger.log(`[Session ${sessionId}] Auto-allowing ${toolName} (mode=${currentPermissionMode})`);
|
|
10767
|
+
if (toolName === "AskUserQuestion") {
|
|
10768
|
+
sessionService.pushMessage(
|
|
10769
|
+
{ type: "message", message: "\u{1F501} Question auto-dismissed \u2014 a loop is running, so there is no human to prompt. The agent will proceed as if the questions were skipped.", level: "warning" },
|
|
10770
|
+
"event"
|
|
10771
|
+
);
|
|
10772
|
+
}
|
|
10619
10773
|
if (claudeProcess && !claudeProcess.killed && claudeProcess.stdin) {
|
|
10620
10774
|
const controlResponse = JSON.stringify({
|
|
10621
10775
|
type: "control_response",
|
|
@@ -11543,7 +11697,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11543
11697
|
const children = [];
|
|
11544
11698
|
await Promise.all(entries.map(async (entry) => {
|
|
11545
11699
|
if (entry.isSymbolicLink()) return;
|
|
11546
|
-
const childPath = join(p, entry.name);
|
|
11700
|
+
const childPath = join$1(p, entry.name);
|
|
11547
11701
|
const childNode = await buildTree(childPath, entry.name, depth + 1);
|
|
11548
11702
|
if (childNode) children.push(childNode);
|
|
11549
11703
|
}));
|
|
@@ -11693,7 +11847,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11693
11847
|
} else if (allowedTools.has(toolName)) {
|
|
11694
11848
|
return true;
|
|
11695
11849
|
}
|
|
11696
|
-
if (
|
|
11850
|
+
if (isLoopActiveForSession(directory, sessionId)) return true;
|
|
11697
11851
|
if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
|
|
11698
11852
|
if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
|
|
11699
11853
|
return false;
|
|
@@ -11706,8 +11860,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11706
11860
|
machineId,
|
|
11707
11861
|
homeDir: os$1.homedir(),
|
|
11708
11862
|
svampHomeDir: SVAMP_HOME,
|
|
11709
|
-
svampLibDir: join(__dirname$1, ".."),
|
|
11710
|
-
svampToolsDir: join(__dirname$1, "..", "tools"),
|
|
11863
|
+
svampLibDir: join$1(__dirname$1, ".."),
|
|
11864
|
+
svampToolsDir: join$1(__dirname$1, "..", "tools"),
|
|
11711
11865
|
startedFromDaemon: true,
|
|
11712
11866
|
startedBy: "daemon",
|
|
11713
11867
|
lifecycleState: "starting",
|
|
@@ -12012,7 +12166,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12012
12166
|
const children = [];
|
|
12013
12167
|
await Promise.all(entries.map(async (entry) => {
|
|
12014
12168
|
if (entry.isSymbolicLink()) return;
|
|
12015
|
-
const childPath = join(p, entry.name);
|
|
12169
|
+
const childPath = join$1(p, entry.name);
|
|
12016
12170
|
const childNode = await buildTree(childPath, entry.name, depth + 1);
|
|
12017
12171
|
if (childNode) children.push(childNode);
|
|
12018
12172
|
}));
|
|
@@ -12248,8 +12402,20 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12248
12402
|
};
|
|
12249
12403
|
const archiveSession = (sessionId) => {
|
|
12250
12404
|
logger.log(`Archiving session: ${sessionId}`);
|
|
12405
|
+
let loopDir;
|
|
12406
|
+
for (const s of pidToTrackedSession.values()) {
|
|
12407
|
+
if (s.svampSessionId === sessionId) {
|
|
12408
|
+
loopDir = s.directory;
|
|
12409
|
+
break;
|
|
12410
|
+
}
|
|
12411
|
+
}
|
|
12412
|
+
if (!loopDir) loopDir = loadSessionIndex()[sessionId]?.directory;
|
|
12251
12413
|
const wasInMemory = teardownTrackedSession(sessionId);
|
|
12252
12414
|
const markedArchived = markSessionAsArchived(sessionId);
|
|
12415
|
+
if (loopDir && isLoopActiveForSession(loopDir, sessionId)) {
|
|
12416
|
+
deactivateLoop(loopDir);
|
|
12417
|
+
logger.log(`Deactivated loop for archived session ${sessionId}`);
|
|
12418
|
+
}
|
|
12253
12419
|
if (wasInMemory || markedArchived) {
|
|
12254
12420
|
logger.log(`Session ${sessionId} archived (inMemory=${wasInMemory}, persisted=${markedArchived})`);
|
|
12255
12421
|
return true;
|
|
@@ -12293,8 +12459,19 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12293
12459
|
};
|
|
12294
12460
|
const deleteSession = (sessionId) => {
|
|
12295
12461
|
logger.log(`Deleting session: ${sessionId}`);
|
|
12462
|
+
let loopDir;
|
|
12463
|
+
for (const s of pidToTrackedSession.values()) {
|
|
12464
|
+
if (s.svampSessionId === sessionId) {
|
|
12465
|
+
loopDir = s.directory;
|
|
12466
|
+
break;
|
|
12467
|
+
}
|
|
12468
|
+
}
|
|
12469
|
+
if (!loopDir) loopDir = loadSessionIndex()[sessionId]?.directory;
|
|
12296
12470
|
teardownTrackedSession(sessionId);
|
|
12297
12471
|
deletePersistedSession(sessionId);
|
|
12472
|
+
if (loopDir && isLoopActiveForSession(loopDir, sessionId)) {
|
|
12473
|
+
deactivateLoop(loopDir);
|
|
12474
|
+
}
|
|
12298
12475
|
logger.log(`Session ${sessionId} deleted`);
|
|
12299
12476
|
return true;
|
|
12300
12477
|
};
|
|
@@ -12351,7 +12528,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12351
12528
|
svampVersion: "0.1.0 (hypha)",
|
|
12352
12529
|
homeDir: defaultHomeDir,
|
|
12353
12530
|
svampHomeDir: SVAMP_HOME,
|
|
12354
|
-
svampLibDir: join(__dirname$1, ".."),
|
|
12531
|
+
svampLibDir: join$1(__dirname$1, ".."),
|
|
12355
12532
|
displayName: process.env.SVAMP_DISPLAY_NAME || void 0,
|
|
12356
12533
|
isolationCapabilities,
|
|
12357
12534
|
// Restore persisted sharing (possibly augmented with --share seed above),
|
|
@@ -12420,7 +12597,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12420
12597
|
const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
|
|
12421
12598
|
if (channelHttpPort > 0) {
|
|
12422
12599
|
try {
|
|
12423
|
-
const { createChannelHttpServer } = await import('./httpServer-
|
|
12600
|
+
const { createChannelHttpServer } = await import('./httpServer-CWn3F-0t.mjs');
|
|
12424
12601
|
const channelHttpServer = createChannelHttpServer({
|
|
12425
12602
|
getSessionIds: () => {
|
|
12426
12603
|
const ids = [];
|
|
@@ -12441,7 +12618,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12441
12618
|
const specs = loadExposedTunnels();
|
|
12442
12619
|
if (specs.length === 0) return;
|
|
12443
12620
|
logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
|
|
12444
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
12621
|
+
const { FrpcTunnel } = await import('./frpc-B8ORdlOO.mjs');
|
|
12445
12622
|
for (const spec of specs) {
|
|
12446
12623
|
if (tunnels.has(spec.name)) continue;
|
|
12447
12624
|
try {
|
|
@@ -12523,6 +12700,19 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12523
12700
|
}
|
|
12524
12701
|
const sessionsToAutoContinue = [];
|
|
12525
12702
|
const sessionsToLoopResume = [];
|
|
12703
|
+
{
|
|
12704
|
+
const knownSessionIds = new Set(persistedSessions.map((p) => p.sessionId));
|
|
12705
|
+
const sweptDirs = /* @__PURE__ */ new Set();
|
|
12706
|
+
for (const p of persistedSessions) {
|
|
12707
|
+
if (sweptDirs.has(p.directory)) continue;
|
|
12708
|
+
sweptDirs.add(p.directory);
|
|
12709
|
+
const owner = loopOwnerSession(p.directory);
|
|
12710
|
+
if (owner && !knownSessionIds.has(owner)) {
|
|
12711
|
+
deactivateLoop(p.directory);
|
|
12712
|
+
logger.log(`[loop] Deactivated stale loop-state in ${p.directory} (owner session ${owner} no longer known)`);
|
|
12713
|
+
}
|
|
12714
|
+
}
|
|
12715
|
+
}
|
|
12526
12716
|
if (persistedSessions.length > 0) {
|
|
12527
12717
|
logger.log(`Restoring ${persistedSessions.length} persisted session(s)...`);
|
|
12528
12718
|
for (const persisted of persistedSessions) {
|
|
@@ -12567,7 +12757,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12567
12757
|
if (persisted.wasProcessing && persisted.claudeResumeId && !isOrphaned) {
|
|
12568
12758
|
sessionsToAutoContinue.push(persisted.sessionId);
|
|
12569
12759
|
}
|
|
12570
|
-
if (!isOrphaned && !persisted.wasProcessing &&
|
|
12760
|
+
if (!isOrphaned && !persisted.wasProcessing && isLoopActiveForSession(persisted.directory, persisted.sessionId)) {
|
|
12571
12761
|
sessionsToLoopResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
|
|
12572
12762
|
}
|
|
12573
12763
|
} else {
|
|
@@ -12614,7 +12804,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12614
12804
|
}
|
|
12615
12805
|
setTimeout(async () => {
|
|
12616
12806
|
try {
|
|
12617
|
-
if (!
|
|
12807
|
+
if (!isLoopActiveForSession(sessDir, sessionId)) return;
|
|
12618
12808
|
const prompt = "Continue the loop. Read LOOP.md and keep working toward the exit conditions until the Stop gate confirms completion.";
|
|
12619
12809
|
await rpc.sendMessage(
|
|
12620
12810
|
JSON.stringify({
|
|
@@ -12892,8 +13082,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12892
13082
|
if (existsSync$1(filePath)) {
|
|
12893
13083
|
const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
|
|
12894
13084
|
const tmpPath = filePath + ".tmp";
|
|
12895
|
-
writeFileSync(tmpPath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
|
|
12896
|
-
renameSync(tmpPath, filePath);
|
|
13085
|
+
writeFileSync$1(tmpPath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
|
|
13086
|
+
renameSync$1(tmpPath, filePath);
|
|
12897
13087
|
markedCount++;
|
|
12898
13088
|
}
|
|
12899
13089
|
} catch {
|
|
@@ -12956,7 +13146,7 @@ async function stopDaemon(options) {
|
|
|
12956
13146
|
const mode = options?.cleanup ? "cleanup (sessions will be stopped)" : "quick (sessions preserved for auto-restore)";
|
|
12957
13147
|
writeStopMarker(`stopDaemon (${options?.cleanup ? "cleanup" : "quick"})`);
|
|
12958
13148
|
const pidsToSignal = [];
|
|
12959
|
-
const supervisorPidFile = join(SVAMP_HOME, "supervisor.pid");
|
|
13149
|
+
const supervisorPidFile = join$1(SVAMP_HOME, "supervisor.pid");
|
|
12960
13150
|
try {
|
|
12961
13151
|
if (existsSync$1(supervisorPidFile)) {
|
|
12962
13152
|
const supervisorPid = parseInt(readFileSync$1(supervisorPidFile, "utf-8").trim(), 10);
|
|
@@ -13061,7 +13251,7 @@ async function stopDaemon(options) {
|
|
|
13061
13251
|
}
|
|
13062
13252
|
}
|
|
13063
13253
|
async function restartDaemon() {
|
|
13064
|
-
const supervisorPidFile = join(SVAMP_HOME, "supervisor.pid");
|
|
13254
|
+
const supervisorPidFile = join$1(SVAMP_HOME, "supervisor.pid");
|
|
13065
13255
|
let supervisorPid = null;
|
|
13066
13256
|
try {
|
|
13067
13257
|
if (existsSync$1(supervisorPidFile)) {
|
|
@@ -13098,7 +13288,7 @@ async function restartDaemon() {
|
|
|
13098
13288
|
});
|
|
13099
13289
|
child.unref();
|
|
13100
13290
|
}
|
|
13101
|
-
const stateFile2 = join(SVAMP_HOME, "daemon.state.json");
|
|
13291
|
+
const stateFile2 = join$1(SVAMP_HOME, "daemon.state.json");
|
|
13102
13292
|
for (let i = 0; i < 100; i++) {
|
|
13103
13293
|
await new Promise((r) => setTimeout(r, 100));
|
|
13104
13294
|
if (existsSync$1(stateFile2)) {
|
|
@@ -13122,7 +13312,7 @@ async function restartDaemon() {
|
|
|
13122
13312
|
await doFullRestart("Failed to signal supervisor");
|
|
13123
13313
|
return;
|
|
13124
13314
|
}
|
|
13125
|
-
const stateFile = join(SVAMP_HOME, "daemon.state.json");
|
|
13315
|
+
const stateFile = join$1(SVAMP_HOME, "daemon.state.json");
|
|
13126
13316
|
for (let i = 0; i < 300; i++) {
|
|
13127
13317
|
await new Promise((r) => setTimeout(r, 100));
|
|
13128
13318
|
try {
|
|
@@ -13147,7 +13337,7 @@ async function restartDaemon() {
|
|
|
13147
13337
|
function daemonStatus() {
|
|
13148
13338
|
const state = readDaemonStateFile();
|
|
13149
13339
|
if (!state) {
|
|
13150
|
-
const plistPath = join(os$1.homedir(), "Library", "LaunchAgents", "io.hypha.svamp.daemon.plist");
|
|
13340
|
+
const plistPath = join$1(os$1.homedir(), "Library", "LaunchAgents", "io.hypha.svamp.daemon.plist");
|
|
13151
13341
|
if (existsSync$1(plistPath)) {
|
|
13152
13342
|
console.log("Status: Not running (launchd service installed \u2014 may be starting)");
|
|
13153
13343
|
} else {
|