svamp-cli 0.2.86 → 0.2.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agentCommands-BiIVrk52.mjs → agentCommands-7zzJYIzN.mjs} +2 -2
- package/dist/auth-BRm_dfqc.mjs +73 -0
- package/dist/cli.mjs +197 -81
- package/dist/{commands-BkUoyPy-.mjs → commands-25hlQVK5.mjs} +5 -5
- package/dist/{commands-BKYz39Hl.mjs → commands-BIsNPVCT.mjs} +2 -1
- package/dist/{commands-DQaKE9SC.mjs → commands-CA-A0G2y.mjs} +2 -1
- package/dist/{commands-Db9HLZR5.mjs → commands-CVPCcCqU.mjs} +3 -2
- package/dist/{commands-Dj7Be8dw.mjs → commands-DY1ciMPa.mjs} +159 -8
- package/dist/{fleet-DHV-NyW1.mjs → fleet-B_HW0j1E.mjs} +2 -1
- package/dist/{frpc-BfBqHN33.mjs → frpc-BiazjxzU.mjs} +2 -1
- package/dist/index.mjs +2 -1
- package/dist/{package-C93hgi-7.mjs → package-jHkQchZL.mjs} +2 -2
- package/dist/{run-rWXfNd7e.mjs → run-C4i9N-F6.mjs} +942 -61
- package/dist/{run-CEs0etJC.mjs → run-DLtX5Yom.mjs} +2 -1
- package/dist/{serveCommands-Cw5hnh0B.mjs → serveCommands-B2brwRfz.mjs} +5 -5
- package/dist/{serveManager-BgnooFJZ.mjs → serveManager-DHJy5C0z.mjs} +3 -2
- package/dist/serviceManager-hlOVxkhW.mjs +78 -0
- package/package.json +45 -45
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os, { 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,
|
|
4
|
-
import path__default, { join, dirname,
|
|
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';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { execFile, spawn as spawn$1, execSync as execSync$1 } from 'child_process';
|
|
7
7
|
import { randomUUID as randomUUID$1 } from 'crypto';
|
|
@@ -10,6 +10,7 @@ import { promisify } from 'util';
|
|
|
10
10
|
import { randomBytes, randomUUID, createHash } from 'node:crypto';
|
|
11
11
|
import { join as join$1 } from 'node:path';
|
|
12
12
|
import os$1, { homedir, platform } from 'node:os';
|
|
13
|
+
import vm from 'node:vm';
|
|
13
14
|
import { spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
|
|
14
15
|
import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
15
16
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
@@ -1393,7 +1394,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
1393
1394
|
const tunnels = handlers.tunnels;
|
|
1394
1395
|
if (!tunnels) throw new Error("Tunnel management not available");
|
|
1395
1396
|
if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
|
|
1396
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
1397
|
+
const { FrpcTunnel } = await import('./frpc-BiazjxzU.mjs');
|
|
1397
1398
|
const tunnel = new FrpcTunnel({
|
|
1398
1399
|
name: params.name,
|
|
1399
1400
|
ports: params.ports,
|
|
@@ -1577,6 +1578,52 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
1577
1578
|
{ overwrite: true }
|
|
1578
1579
|
);
|
|
1579
1580
|
console.log(`[HYPHA MACHINE] Machine service registered: ${serviceInfo.id}`);
|
|
1581
|
+
const findChannelOwner = async (channelId) => {
|
|
1582
|
+
for (const sid of handlers.getSessionIds?.() || []) {
|
|
1583
|
+
const rpc = handlers.getSessionRPCHandlers?.(sid);
|
|
1584
|
+
if (!rpc?.channelList) continue;
|
|
1585
|
+
try {
|
|
1586
|
+
const r = await rpc.channelList();
|
|
1587
|
+
if ((r?.channels || []).some((c) => c.id === channelId)) return rpc;
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return void 0;
|
|
1592
|
+
};
|
|
1593
|
+
const channelsServiceInfo = await server.registerService(
|
|
1594
|
+
{
|
|
1595
|
+
id: "channels",
|
|
1596
|
+
name: `Svamp Channels (${currentMetadata.displayName || machineId})`,
|
|
1597
|
+
type: "svamp-channels",
|
|
1598
|
+
config: { visibility: "public", require_context: true },
|
|
1599
|
+
list: async () => {
|
|
1600
|
+
const out = [];
|
|
1601
|
+
for (const sid of handlers.getSessionIds?.() || []) {
|
|
1602
|
+
const rpc = handlers.getSessionRPCHandlers?.(sid);
|
|
1603
|
+
if (!rpc?.channelList) continue;
|
|
1604
|
+
try {
|
|
1605
|
+
const r = await rpc.channelList();
|
|
1606
|
+
for (const c of r?.channels || []) out.push({ ...c, session: sid });
|
|
1607
|
+
} catch {
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return out;
|
|
1611
|
+
},
|
|
1612
|
+
describe: async (kwargs = {}) => {
|
|
1613
|
+
const rpc = await findChannelOwner(kwargs.channel);
|
|
1614
|
+
if (!rpc?.channelDescribe) return { error: "channel not found" };
|
|
1615
|
+
return rpc.channelDescribe(kwargs.channel);
|
|
1616
|
+
},
|
|
1617
|
+
send: async (kwargs = {}, context) => {
|
|
1618
|
+
trackInbound();
|
|
1619
|
+
const rpc = await findChannelOwner(kwargs.channel);
|
|
1620
|
+
if (!rpc?.channelSend) return { error: "channel not found" };
|
|
1621
|
+
return rpc.channelSend({ channel: kwargs.channel, message: kwargs.message, from: kwargs.from, key: kwargs.key }, context);
|
|
1622
|
+
}
|
|
1623
|
+
},
|
|
1624
|
+
{ overwrite: true }
|
|
1625
|
+
);
|
|
1626
|
+
console.log(`[HYPHA MACHINE] Channels service registered: ${channelsServiceInfo.id} (type svamp-channels)`);
|
|
1580
1627
|
return {
|
|
1581
1628
|
serviceInfo,
|
|
1582
1629
|
notifySessionEvent: notifyListeners,
|
|
@@ -1605,6 +1652,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
1605
1652
|
removeListener(listener, "disconnect");
|
|
1606
1653
|
}
|
|
1607
1654
|
await server.unregisterService(serviceInfo.id);
|
|
1655
|
+
await server.unregisterService(channelsServiceInfo.id).catch(() => {
|
|
1656
|
+
});
|
|
1608
1657
|
}
|
|
1609
1658
|
};
|
|
1610
1659
|
}
|
|
@@ -1646,13 +1695,33 @@ function cronMatches(expr, date) {
|
|
|
1646
1695
|
if (c.domRestricted && c.dowRestricted) return domOk || dowOk;
|
|
1647
1696
|
return domOk && dowOk;
|
|
1648
1697
|
}
|
|
1649
|
-
function
|
|
1698
|
+
function inZone(date, tz) {
|
|
1699
|
+
if (!tz) return date;
|
|
1700
|
+
try {
|
|
1701
|
+
const p = new Intl.DateTimeFormat("en-US", {
|
|
1702
|
+
timeZone: tz,
|
|
1703
|
+
hour12: false,
|
|
1704
|
+
year: "numeric",
|
|
1705
|
+
month: "2-digit",
|
|
1706
|
+
day: "2-digit",
|
|
1707
|
+
hour: "2-digit",
|
|
1708
|
+
minute: "2-digit"
|
|
1709
|
+
}).formatToParts(date).reduce((o, x) => {
|
|
1710
|
+
o[x.type] = x.value;
|
|
1711
|
+
return o;
|
|
1712
|
+
}, {});
|
|
1713
|
+
return new Date(+p.year, +p.month - 1, +p.day, +(p.hour === "24" ? 0 : p.hour), +p.minute);
|
|
1714
|
+
} catch {
|
|
1715
|
+
return date;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
function nextFire(expr, from, tz) {
|
|
1650
1719
|
const c = parseCron(expr);
|
|
1651
1720
|
const d = new Date(from.getTime());
|
|
1652
1721
|
d.setSeconds(0, 0);
|
|
1653
1722
|
d.setMinutes(d.getMinutes() + 1);
|
|
1654
1723
|
for (let i = 0; i < 366 * 24 * 60; i++) {
|
|
1655
|
-
if (cronMatches(c, d)) return new Date(d.getTime());
|
|
1724
|
+
if (cronMatches(c, tz ? inZone(d, tz) : d)) return new Date(d.getTime());
|
|
1656
1725
|
d.setMinutes(d.getMinutes() + 1);
|
|
1657
1726
|
}
|
|
1658
1727
|
return null;
|
|
@@ -1689,14 +1758,16 @@ function validateRoutine(r) {
|
|
|
1689
1758
|
if (a?.kind === "message" && !a.template) errs.push("action.template required for message action");
|
|
1690
1759
|
if (a?.kind === "loop" && !a.loop && !a.task_template) errs.push("action.loop or action.task_template required for loop action");
|
|
1691
1760
|
if (r.overlap && !OVERLAP.includes(r.overlap)) errs.push(`overlap must be one of ${OVERLAP.join("|")}`);
|
|
1761
|
+
if ((t?.type === "webhook" || t?.type === "api") && t.public && a?.kind === "loop")
|
|
1762
|
+
errs.push("a public webhook/api may not use a loop action (unauthenticated task injection) \u2014 use a message action or require a key");
|
|
1692
1763
|
return errs;
|
|
1693
1764
|
}
|
|
1694
1765
|
|
|
1695
1766
|
function defaultRoutinesDir() {
|
|
1696
1767
|
return process.env.SVAMP_ROUTINES_DIR || join$1(homedir(), ".svamp", "routines");
|
|
1697
1768
|
}
|
|
1698
|
-
const genId = () => "rt_" + randomBytes(5).toString("hex");
|
|
1699
|
-
const genKey = () => randomBytes(18).toString("base64url");
|
|
1769
|
+
const genId$1 = () => "rt_" + randomBytes(5).toString("hex");
|
|
1770
|
+
const genKey$1 = () => randomBytes(18).toString("base64url");
|
|
1700
1771
|
class RoutineStore {
|
|
1701
1772
|
dir;
|
|
1702
1773
|
constructor(dir = defaultRoutinesDir()) {
|
|
@@ -1725,8 +1796,8 @@ class RoutineStore {
|
|
|
1725
1796
|
}
|
|
1726
1797
|
save(routine) {
|
|
1727
1798
|
const r = { overlap: "queue", enabled: true, last_runs: [], ...routine };
|
|
1728
|
-
if (!r.id) r.id = genId();
|
|
1729
|
-
if ((r.trigger?.type === "webhook" || r.trigger?.type === "api") && !r.trigger.key) r.trigger.key = genKey();
|
|
1799
|
+
if (!r.id) r.id = genId$1();
|
|
1800
|
+
if ((r.trigger?.type === "webhook" || r.trigger?.type === "api") && !r.trigger.key) r.trigger.key = genKey$1();
|
|
1730
1801
|
const errs = validateRoutine(r);
|
|
1731
1802
|
if (errs.length) throw new Error("invalid routine: " + errs.join("; "));
|
|
1732
1803
|
const tmp = this._path(r.id) + ".tmp";
|
|
@@ -1783,13 +1854,23 @@ class RoutineRunner {
|
|
|
1783
1854
|
markDone(id) {
|
|
1784
1855
|
this.active.delete(id);
|
|
1785
1856
|
}
|
|
1857
|
+
// Deliveries counted today via a dedicated counter (NOT derived from the
|
|
1858
|
+
// capped last_runs audit, which would undercount past 20 runs/day).
|
|
1786
1859
|
_deliveredToday(routine) {
|
|
1787
1860
|
const today = this.now().toDateString();
|
|
1788
|
-
return
|
|
1861
|
+
return routine.daily && routine.daily.date === today ? routine.daily.n : 0;
|
|
1789
1862
|
}
|
|
1863
|
+
// `active` guards genuinely CONCURRENT in-flight deliveries only — the runner
|
|
1864
|
+
// cannot observe a spawned loop's full duration (fire-and-forget), so it is
|
|
1865
|
+
// cleared once delivery returns. `replace` calls onReplace (best-effort) then proceeds.
|
|
1790
1866
|
async fire(routine, payload = {}, via = "manual") {
|
|
1791
1867
|
if (!routine.enabled) return { skipped: "disabled" };
|
|
1792
|
-
if (routine.
|
|
1868
|
+
if (routine.action?.kind === "loop" && (routine.trigger?.type === "webhook" || routine.trigger?.type === "api") && routine.trigger?.public)
|
|
1869
|
+
return { skipped: "forbidden (public webhook + loop)" };
|
|
1870
|
+
const today = this.now().toDateString();
|
|
1871
|
+
const r0 = this.store.get(routine.id) || routine;
|
|
1872
|
+
const usedToday = r0.daily && r0.daily.date === today ? r0.daily.n : 0;
|
|
1873
|
+
if (routine.daily_cap && usedToday >= routine.daily_cap) {
|
|
1793
1874
|
this.store.recordRun(routine.id, { via, delivered: routine.action.kind, outcome: "skipped (daily cap)" });
|
|
1794
1875
|
return { skipped: "daily_cap" };
|
|
1795
1876
|
}
|
|
@@ -1803,16 +1884,28 @@ class RoutineRunner {
|
|
|
1803
1884
|
this.onReplace?.(routine.id);
|
|
1804
1885
|
} catch {
|
|
1805
1886
|
}
|
|
1806
|
-
this.active.delete(routine.id);
|
|
1807
1887
|
}
|
|
1808
1888
|
}
|
|
1889
|
+
this.active.add(routine.id);
|
|
1890
|
+
if (routine.daily_cap) {
|
|
1891
|
+
r0.daily = { date: today, n: usedToday + 1 };
|
|
1892
|
+
this.store.save(r0);
|
|
1893
|
+
}
|
|
1809
1894
|
const resolved = this.resolveAction(routine, payload);
|
|
1810
1895
|
let outcome = "delivered";
|
|
1811
1896
|
try {
|
|
1812
1897
|
await this.deliver({ routine, action: routine.action, resolved, payload, via });
|
|
1813
|
-
if (resolved.kind === "loop") this.active.add(routine.id);
|
|
1814
1898
|
} catch (e) {
|
|
1815
1899
|
outcome = "error: " + (e?.message || e);
|
|
1900
|
+
if (routine.daily_cap) {
|
|
1901
|
+
const rb = this.store.get(routine.id);
|
|
1902
|
+
if (rb?.daily?.date === today && rb.daily.n > 0) {
|
|
1903
|
+
rb.daily.n -= 1;
|
|
1904
|
+
this.store.save(rb);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
} finally {
|
|
1908
|
+
this.active.delete(routine.id);
|
|
1816
1909
|
}
|
|
1817
1910
|
const r = this.store.get(routine.id) || routine;
|
|
1818
1911
|
r.last_fired_at = this.now().toISOString();
|
|
@@ -1824,7 +1917,7 @@ class RoutineRunner {
|
|
|
1824
1917
|
const results = [];
|
|
1825
1918
|
for (const r of this.store.list()) {
|
|
1826
1919
|
if (!r.enabled || r.trigger?.type !== "schedule") continue;
|
|
1827
|
-
if (!cronMatches(r.trigger.cron, date)) continue;
|
|
1920
|
+
if (!cronMatches(r.trigger.cron, inZone(date, r.trigger.tz))) continue;
|
|
1828
1921
|
const mk = minuteKey(date);
|
|
1829
1922
|
if (this._firedMinute.get(r.id) === mk) continue;
|
|
1830
1923
|
this._firedMinute.set(r.id, mk);
|
|
@@ -1855,7 +1948,7 @@ class RoutineRunner {
|
|
|
1855
1948
|
if (!r.enabled || r.trigger?.type !== "schedule" || r.trigger.missed !== "catchup") continue;
|
|
1856
1949
|
const deadlineMs = (r.trigger.deadline_sec || 3600) * 1e3;
|
|
1857
1950
|
const since = new Date(Math.max(sinceDate.getTime(), nowDate.getTime() - deadlineMs));
|
|
1858
|
-
const due = nextFire(r.trigger.cron, since);
|
|
1951
|
+
const due = nextFire(r.trigger.cron, since, r.trigger.tz);
|
|
1859
1952
|
if (due && due <= nowDate) {
|
|
1860
1953
|
const last = r.last_fired_at ? new Date(r.last_fired_at) : /* @__PURE__ */ new Date(0);
|
|
1861
1954
|
if (last < due) results.push({ id: r.id, ...await this.fire(r, { catchup: due.toISOString() }, "schedule") });
|
|
@@ -1865,6 +1958,622 @@ class RoutineRunner {
|
|
|
1865
1958
|
}
|
|
1866
1959
|
}
|
|
1867
1960
|
|
|
1961
|
+
const genId = () => "c_" + randomBytes(5).toString("hex");
|
|
1962
|
+
const genKey = () => "ck_" + randomBytes(18).toString("base64url");
|
|
1963
|
+
const DEFAULT_TEMPLATE = `<inbound-message from="\${sender.name}" sender-type="\${sender.kind}" verified="\${sender.verified}" channel="\${channel.name}" call-id="\${call.id}" at="\${now}">
|
|
1964
|
+
\${body.message}
|
|
1965
|
+
</inbound-message>`;
|
|
1966
|
+
function validateChannel(c) {
|
|
1967
|
+
const errs = [];
|
|
1968
|
+
if (!c || typeof c !== "object") return ["channel must be an object"];
|
|
1969
|
+
if (!c.name) errs.push("name required");
|
|
1970
|
+
const m = c.identity?.mode;
|
|
1971
|
+
if (!["per-key", "caller-supplied", "fixed"].includes(m)) errs.push("identity.mode must be per-key|caller-supplied|fixed");
|
|
1972
|
+
if (m === "fixed" && !c.identity.fixed?.name) errs.push("identity.fixed.name required for fixed mode");
|
|
1973
|
+
if (!["message", "loop", "agent"].includes(c.action?.kind)) errs.push("action.kind must be message|loop|agent");
|
|
1974
|
+
if (c.bind !== "active" && !(c.bind && (c.bind.tag || c.bind.session))) errs.push('bind must be "active", {tag}, or {session}');
|
|
1975
|
+
if (c.action?.kind === "loop" && m === "caller-supplied" && !c.identity?.shared_key)
|
|
1976
|
+
errs.push("a caller-supplied channel without a shared_key may not use a loop action (unauthenticated task injection)");
|
|
1977
|
+
if (c.action?.kind === "agent" && m === "caller-supplied" && !c.identity?.shared_key) {
|
|
1978
|
+
const MUTATING = ["run_bash", "send_to_session", "run_js"];
|
|
1979
|
+
const ag = c.action.agent || {};
|
|
1980
|
+
const grantsMutating = (ag.tools || []).some((t) => MUTATING.includes(t)) || Object.values(ag.per_caller || {}).some((p) => (p?.tools || []).some((t) => MUTATING.includes(t)));
|
|
1981
|
+
if (grantsMutating) errs.push("a caller-supplied agent channel without a shared_key may not grant run_bash/send_to_session");
|
|
1982
|
+
}
|
|
1983
|
+
const unsafe = /[<>"'&\r\n]/;
|
|
1984
|
+
if (unsafe.test(c.name || "")) errs.push(`name must be single-line and not contain < > " ' &`);
|
|
1985
|
+
if (c.description && unsafe.test(c.description)) errs.push(`description must be single-line and not contain < > " ' &`);
|
|
1986
|
+
if (c.skill?.name && unsafe.test(c.skill.name)) errs.push(`skill.name must be single-line and not contain < > " ' &`);
|
|
1987
|
+
if (c.skill?.description && unsafe.test(c.skill.description)) errs.push(`skill.description must be single-line and not contain < > " ' &`);
|
|
1988
|
+
if (m === "fixed" && c.identity.fixed?.name && unsafe.test(c.identity.fixed.name)) errs.push(`identity.fixed.name must not contain < > " ' & or newlines`);
|
|
1989
|
+
for (const cl of c.identity?.callers || []) if (unsafe.test(cl.name || "")) errs.push(`caller name "${cl.name}" must not contain < > " ' & or newlines`);
|
|
1990
|
+
return errs;
|
|
1991
|
+
}
|
|
1992
|
+
class ChannelStore {
|
|
1993
|
+
dir;
|
|
1994
|
+
constructor(projectDir) {
|
|
1995
|
+
this.dir = join$1(projectDir, ".svamp", "channels");
|
|
1996
|
+
mkdirSync$1(this.dir, { recursive: true });
|
|
1997
|
+
}
|
|
1998
|
+
_path(id) {
|
|
1999
|
+
return join$1(this.dir, `${id}.json`);
|
|
2000
|
+
}
|
|
2001
|
+
list() {
|
|
2002
|
+
return readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
2003
|
+
try {
|
|
2004
|
+
return JSON.parse(readFileSync(join$1(this.dir, f), "utf8"));
|
|
2005
|
+
} catch {
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
}).filter((c) => !!c);
|
|
2009
|
+
}
|
|
2010
|
+
get(id) {
|
|
2011
|
+
try {
|
|
2012
|
+
return JSON.parse(readFileSync(this._path(id), "utf8"));
|
|
2013
|
+
} catch {
|
|
2014
|
+
return null;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
save(channel) {
|
|
2018
|
+
const c = { enabled: true, bind: "active", template: DEFAULT_TEMPLATE, last_calls: [], ...channel };
|
|
2019
|
+
if (!c.id) c.id = genId();
|
|
2020
|
+
const errs = validateChannel(c);
|
|
2021
|
+
if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
|
|
2022
|
+
const tmp = this._path(c.id) + ".tmp";
|
|
2023
|
+
writeFileSync$1(tmp, JSON.stringify(c, null, 2));
|
|
2024
|
+
renameSync$1(tmp, this._path(c.id));
|
|
2025
|
+
return c;
|
|
2026
|
+
}
|
|
2027
|
+
remove(id) {
|
|
2028
|
+
const p = this._path(id);
|
|
2029
|
+
if (existsSync(p)) {
|
|
2030
|
+
rmSync(p);
|
|
2031
|
+
return true;
|
|
2032
|
+
}
|
|
2033
|
+
return false;
|
|
2034
|
+
}
|
|
2035
|
+
setEnabled(id, enabled) {
|
|
2036
|
+
const c = this.get(id);
|
|
2037
|
+
if (!c) return null;
|
|
2038
|
+
c.enabled = enabled;
|
|
2039
|
+
return this.save(c);
|
|
2040
|
+
}
|
|
2041
|
+
recordCall(id, entry) {
|
|
2042
|
+
const c = this.get(id);
|
|
2043
|
+
if (!c) return;
|
|
2044
|
+
c.last_calls = c.last_calls || [];
|
|
2045
|
+
c.last_calls.unshift({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
2046
|
+
c.last_calls = c.last_calls.slice(0, 20);
|
|
2047
|
+
this.save(c);
|
|
2048
|
+
}
|
|
2049
|
+
addCaller(id, name, kind = "agent") {
|
|
2050
|
+
const c = this.get(id);
|
|
2051
|
+
if (!c) return null;
|
|
2052
|
+
c.identity.callers = c.identity.callers || [];
|
|
2053
|
+
const caller = { name, kind, key: genKey() };
|
|
2054
|
+
c.identity.callers.push(caller);
|
|
2055
|
+
this.save(c);
|
|
2056
|
+
return caller;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
function generateSkillBody(channel, urlBase) {
|
|
2060
|
+
const url = `${"https://<svamp-tunnel>"}/channel/${channel.id}`;
|
|
2061
|
+
const isAgent = channel.action?.kind === "agent";
|
|
2062
|
+
const replyNote = isAgent ? `
|
|
2063
|
+
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\`.` : `
|
|
2064
|
+
Delivery is fire-and-forget; the receiving agent sees an <inbound-message> tag.`;
|
|
2065
|
+
return `---
|
|
2066
|
+
name: ${channel.skill?.name || channel.name}
|
|
2067
|
+
description: ${channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`}
|
|
2068
|
+
---
|
|
2069
|
+
# ${channel.name}
|
|
2070
|
+
${channel.description || ""}
|
|
2071
|
+
|
|
2072
|
+
Self-contained guide for messaging another agent \u2014 share this (or its URL,
|
|
2073
|
+
${url}/skill.md) with an agent so it knows how to reach me.
|
|
2074
|
+
${replyNote}
|
|
2075
|
+
|
|
2076
|
+
Hypha RPC (preferred, verified identity, no key): get_service("<ws>/<machine>:channels").send({ channel: "${channel.id}", message: "..." })
|
|
2077
|
+
HTTP: POST ${url} with header Authorization: Bearer <your-key>, body { "message": "...", "from": "..." }`;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
function resolveSender(channel, input = {}) {
|
|
2081
|
+
const { key, from, hyphaUser, hyphaWorkspace, hyphaAnonymous } = input;
|
|
2082
|
+
const id = channel.identity || {};
|
|
2083
|
+
if (hyphaUser && !hyphaAnonymous && Array.isArray(id.hypha_allow) && id.hypha_allow.length) {
|
|
2084
|
+
if (id.hypha_allow.includes("*") || id.hypha_allow.includes(hyphaUser) || hyphaWorkspace && id.hypha_allow.includes(hyphaWorkspace))
|
|
2085
|
+
return { sender: { name: hyphaUser, kind: "agent", verified: true } };
|
|
2086
|
+
return { error: "caller not in hypha_allow" };
|
|
2087
|
+
}
|
|
2088
|
+
if (id.mode === "fixed") {
|
|
2089
|
+
if (!id.fixed?.name) return { error: "fixed identity not configured" };
|
|
2090
|
+
return { sender: { name: id.fixed.name, kind: id.fixed.kind, verified: true } };
|
|
2091
|
+
}
|
|
2092
|
+
if (id.mode === "per-key") {
|
|
2093
|
+
const caller = (id.callers || []).find((c) => c.key && c.key === key);
|
|
2094
|
+
if (!caller) return { error: "invalid or missing key" };
|
|
2095
|
+
return { sender: { name: caller.name, kind: caller.kind, verified: true } };
|
|
2096
|
+
}
|
|
2097
|
+
if (id.mode === "caller-supplied") {
|
|
2098
|
+
if (id.shared_key && key !== id.shared_key) return { error: "invalid key" };
|
|
2099
|
+
return { sender: { name: from || "anonymous", kind: "user", verified: false } };
|
|
2100
|
+
}
|
|
2101
|
+
return { error: "unsupported identity mode" };
|
|
2102
|
+
}
|
|
2103
|
+
const MAX_BODY = 16 * 1024;
|
|
2104
|
+
function xmlEscape(s) {
|
|
2105
|
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2106
|
+
}
|
|
2107
|
+
const stripControl = (s) => String(s ?? "").replace(/[\x00-\x1f\x7f]/g, " ");
|
|
2108
|
+
function renderMessage(channel, { sender = {}, body = {}, query = {}, callId, now }) {
|
|
2109
|
+
const obj = (v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v;
|
|
2110
|
+
const escVal = (v) => xmlEscape(obj(v));
|
|
2111
|
+
const escAttr = (v) => xmlEscape(stripControl(obj(v)));
|
|
2112
|
+
const bodyEsc = {};
|
|
2113
|
+
for (const [k, v] of Object.entries(body)) bodyEsc[k] = k === "message" ? escVal(String(v ?? "").slice(0, MAX_BODY)) : escAttr(v);
|
|
2114
|
+
const queryEsc = {};
|
|
2115
|
+
for (const [k, v] of Object.entries(query)) if (k !== "key") queryEsc[k] = escAttr(v);
|
|
2116
|
+
const ctx = {
|
|
2117
|
+
sender: { name: escAttr(sender.name), kind: escAttr(sender.kind), verified: sender.verified === true },
|
|
2118
|
+
body: bodyEsc,
|
|
2119
|
+
query: queryEsc,
|
|
2120
|
+
channel: { name: escAttr(channel.name), id: escAttr(channel.id) },
|
|
2121
|
+
call: { id: escAttr(callId) },
|
|
2122
|
+
now: escAttr(now || (/* @__PURE__ */ new Date()).toISOString())
|
|
2123
|
+
};
|
|
2124
|
+
return renderTemplate(channel.template || DEFAULT_TEMPLATE, ctx);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
const SKILL_START = "<<<WISE_SKILL ";
|
|
2128
|
+
const SKILL_END = "<<<END_WISE_SKILL>>>";
|
|
2129
|
+
function buildSkillsScanCommand() {
|
|
2130
|
+
return [
|
|
2131
|
+
'for d in "$HOME/.svamp/skills" ".svamp/skills"; do',
|
|
2132
|
+
' for f in "$d"/*/SKILL.md; do',
|
|
2133
|
+
' [ -f "$f" ] || continue;',
|
|
2134
|
+
` printf '${SKILL_START}%s>>>\\n' "$f";`,
|
|
2135
|
+
' cat "$f";',
|
|
2136
|
+
` printf '\\n${SKILL_END}\\n';`,
|
|
2137
|
+
" done;",
|
|
2138
|
+
"done"
|
|
2139
|
+
].join("\n");
|
|
2140
|
+
}
|
|
2141
|
+
function parseWiseMd(raw) {
|
|
2142
|
+
const m = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
2143
|
+
if (!m) return { body: raw.trim() };
|
|
2144
|
+
return { body: m[2].trim() };
|
|
2145
|
+
}
|
|
2146
|
+
function parseSkillFrontmatter(md) {
|
|
2147
|
+
const m = md.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
2148
|
+
if (!m) return { name: "", description: "", body: md.trim() };
|
|
2149
|
+
const fm = m[1];
|
|
2150
|
+
const body = m[2].trim();
|
|
2151
|
+
const field = (key) => {
|
|
2152
|
+
const fmatch = fm.match(new RegExp(`^${key}\\s*:\\s*(.+)$`, "m"));
|
|
2153
|
+
return fmatch ? fmatch[1].trim().replace(/^["']|["']$/g, "").trim() : "";
|
|
2154
|
+
};
|
|
2155
|
+
return { name: field("name"), description: field("description"), body };
|
|
2156
|
+
}
|
|
2157
|
+
function parseSkillsDump(dump) {
|
|
2158
|
+
if (!dump?.trim()) return [];
|
|
2159
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2160
|
+
const blocks = dump.split(SKILL_START).slice(1);
|
|
2161
|
+
for (const block of blocks) {
|
|
2162
|
+
const headerEnd = block.indexOf(">>>\n");
|
|
2163
|
+
if (headerEnd === -1) continue;
|
|
2164
|
+
const path = block.slice(0, headerEnd).trim();
|
|
2165
|
+
const rest = block.slice(headerEnd + 4);
|
|
2166
|
+
const endIdx = rest.indexOf(SKILL_END);
|
|
2167
|
+
const md = (endIdx === -1 ? rest : rest.slice(0, endIdx)).replace(/\n$/, "");
|
|
2168
|
+
const { name, description, body } = parseSkillFrontmatter(md);
|
|
2169
|
+
const skillName = name || path.split("/").slice(-2, -1)[0] || path;
|
|
2170
|
+
if (!description && !body) continue;
|
|
2171
|
+
byName.set(skillName, { name: skillName, description, path, body });
|
|
2172
|
+
}
|
|
2173
|
+
return Array.from(byName.values());
|
|
2174
|
+
}
|
|
2175
|
+
function findSkill(skills, name) {
|
|
2176
|
+
const n = (name || "").trim().toLowerCase();
|
|
2177
|
+
if (!n) return void 0;
|
|
2178
|
+
return skills.find((s) => s.name.toLowerCase() === n) ?? skills.find((s) => s.name.toLowerCase().includes(n));
|
|
2179
|
+
}
|
|
2180
|
+
function formatSkillIndex(skills, maxDescLen = 140) {
|
|
2181
|
+
if (skills.length === 0) return "";
|
|
2182
|
+
return skills.map((s) => {
|
|
2183
|
+
const d = s.description.length > maxDescLen ? s.description.slice(0, maxDescLen - 1) + "\u2026" : s.description;
|
|
2184
|
+
return `- ${s.name} \u2014 ${d || "(no description)"}`;
|
|
2185
|
+
}).join("\n");
|
|
2186
|
+
}
|
|
2187
|
+
function buildSkillsPromptSection(skills) {
|
|
2188
|
+
if (skills.length === 0) return "";
|
|
2189
|
+
return [
|
|
2190
|
+
"## Skills (load on demand \u2014 do NOT assume the steps)",
|
|
2191
|
+
"These are named procedures for this project. Only their names + descriptions are shown.",
|
|
2192
|
+
'When a request matches one, call use_skill("name") to load its full steps, then carry them',
|
|
2193
|
+
"out with run_bash. Skip skills you don't need.",
|
|
2194
|
+
"",
|
|
2195
|
+
"Available skills:",
|
|
2196
|
+
formatSkillIndex(skills)
|
|
2197
|
+
].join("\n");
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
function buildSvampApi(deps) {
|
|
2201
|
+
return {
|
|
2202
|
+
bash: (command, opts) => deps.runBash(command, opts),
|
|
2203
|
+
context: () => deps.getContext(),
|
|
2204
|
+
ask: (question) => deps.askSession(question),
|
|
2205
|
+
summarize: (question) => deps.summarizeSession(question),
|
|
2206
|
+
send: (message) => deps.sessionSend(message)
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
async function runJs(code, deps, opts = {}) {
|
|
2210
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
2211
|
+
const logs = [];
|
|
2212
|
+
const capture = (...a) => {
|
|
2213
|
+
logs.push(a.map((x) => typeof x === "string" ? x : JSON.stringify(x)).join(" "));
|
|
2214
|
+
};
|
|
2215
|
+
const sandbox = /* @__PURE__ */ Object.create(null);
|
|
2216
|
+
sandbox.svamp = buildSvampApi(deps);
|
|
2217
|
+
sandbox.console = { log: capture, error: capture, warn: capture, info: capture };
|
|
2218
|
+
const context = vm.createContext(sandbox, { name: "wise-run_js" });
|
|
2219
|
+
const wrapped = `(async () => {
|
|
2220
|
+
${code}
|
|
2221
|
+
})()`;
|
|
2222
|
+
let script;
|
|
2223
|
+
try {
|
|
2224
|
+
script = new vm.Script(wrapped, { filename: "wise-run_js.js" });
|
|
2225
|
+
} catch (e) {
|
|
2226
|
+
return { result: void 0, logs, error: "compile error: " + e.message };
|
|
2227
|
+
}
|
|
2228
|
+
let timer;
|
|
2229
|
+
try {
|
|
2230
|
+
const ran = script.runInContext(context, { timeout: timeoutMs });
|
|
2231
|
+
const result = await Promise.race([
|
|
2232
|
+
Promise.resolve(ran),
|
|
2233
|
+
new Promise((_, reject) => {
|
|
2234
|
+
timer = setTimeout(() => reject(new Error("run_js timed out")), timeoutMs);
|
|
2235
|
+
})
|
|
2236
|
+
]);
|
|
2237
|
+
return { result, logs };
|
|
2238
|
+
} catch (e) {
|
|
2239
|
+
return { result: void 0, logs, error: e.message };
|
|
2240
|
+
} finally {
|
|
2241
|
+
if (timer) clearTimeout(timer);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
const READ_ONLY_TOOLS = ["get_context", "ask_session", "summarize_session", "use_skill"];
|
|
2246
|
+
const str = (v) => v == null ? "" : String(v);
|
|
2247
|
+
function buildTools(deps, skills) {
|
|
2248
|
+
return [
|
|
2249
|
+
{
|
|
2250
|
+
name: "get_context",
|
|
2251
|
+
readOnly: true,
|
|
2252
|
+
description: 'Orient: status and recent activity of the bound session/machine. Call first for "what is happening / its status" questions.',
|
|
2253
|
+
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
2254
|
+
run: async () => JSON.stringify(await deps.getContext())
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
name: "ask_session",
|
|
2258
|
+
readOnly: true,
|
|
2259
|
+
description: "Ask the deep Claude session agent a read-only question about its current state/work. Does not modify its history. Slower than get_context.",
|
|
2260
|
+
parameters: { type: "object", properties: { question: { type: "string", description: "The question to ask." } }, required: ["question"], additionalProperties: false },
|
|
2261
|
+
run: async (a) => deps.askSession(str(a?.question))
|
|
2262
|
+
},
|
|
2263
|
+
{
|
|
2264
|
+
name: "summarize_session",
|
|
2265
|
+
readOnly: true,
|
|
2266
|
+
description: "Get a short summary of the deep agent's transcript answering a specific question. Keeps long history out of your context.",
|
|
2267
|
+
parameters: { type: "object", properties: { question: { type: "string", description: "What to summarize / find out." } }, required: ["question"], additionalProperties: false },
|
|
2268
|
+
run: async (a) => deps.summarizeSession(str(a?.question))
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
name: "use_skill",
|
|
2272
|
+
readOnly: true,
|
|
2273
|
+
description: "Load the full steps of a project skill from the Skills index by name (progressive disclosure), then carry them out.",
|
|
2274
|
+
parameters: { type: "object", properties: { name: { type: "string", description: "Skill name from the Skills index." } }, required: ["name"], additionalProperties: false },
|
|
2275
|
+
run: async (a) => {
|
|
2276
|
+
const s = findSkill(skills, str(a?.name));
|
|
2277
|
+
return s ? s.body || `(skill "${s.name}" has no body)` : `No skill named "${str(a?.name)}" in the index.`;
|
|
2278
|
+
}
|
|
2279
|
+
},
|
|
2280
|
+
{
|
|
2281
|
+
name: "run_bash",
|
|
2282
|
+
readOnly: false,
|
|
2283
|
+
description: "Run a shell command on the bound session's machine. Returns stdout/stderr/exit code. Use for quick reads and short tasks.",
|
|
2284
|
+
parameters: { type: "object", properties: { command: { type: "string", description: "The shell command." }, cwd: { type: "string", description: "Optional working directory." } }, required: ["command"], additionalProperties: false },
|
|
2285
|
+
run: async (a) => JSON.stringify(await deps.runBash(str(a?.command), { cwd: a?.cwd ? str(a.cwd) : void 0 }))
|
|
2286
|
+
},
|
|
2287
|
+
{
|
|
2288
|
+
name: "send_to_session",
|
|
2289
|
+
readOnly: false,
|
|
2290
|
+
description: "Hand a clear, reformulated instruction to the deep Claude session agent (when the caller wants it to DO something). Fire-and-forget; pass wait=true to block for its reply.",
|
|
2291
|
+
parameters: { type: "object", properties: { message: { type: "string", description: "The instruction for the coding agent." }, wait: { type: "boolean", description: "Block for the agent's reply." } }, required: ["message"], additionalProperties: false },
|
|
2292
|
+
run: async (a) => {
|
|
2293
|
+
await deps.sessionSend(str(a?.message));
|
|
2294
|
+
if (a?.wait && deps.waitForSessionTurn) {
|
|
2295
|
+
const r = await deps.waitForSessionTurn();
|
|
2296
|
+
return r.completed ? r.reply : "(sent; the agent's turn did not finish before timeout)";
|
|
2297
|
+
}
|
|
2298
|
+
return "(sent to the coding agent)";
|
|
2299
|
+
}
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
name: "run_js",
|
|
2303
|
+
readOnly: false,
|
|
2304
|
+
description: "Run JavaScript with an async `svamp` API to read state and compose several steps in one call: svamp.bash(cmd), svamp.context(), svamp.ask(q), svamp.summarize(q), svamp.send(msg). Use `await` and `return` a value; console.log is captured. Sandboxed, ~5s limit.",
|
|
2305
|
+
parameters: { type: "object", properties: { code: { type: "string", description: "JS body; may use await and return." } }, required: ["code"], additionalProperties: false },
|
|
2306
|
+
run: async (a) => {
|
|
2307
|
+
const r = await runJs(str(a?.code), deps, { timeoutMs: 5e3 });
|
|
2308
|
+
return JSON.stringify({ result: r.result, logs: r.logs, error: r.error });
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
];
|
|
2312
|
+
}
|
|
2313
|
+
function gateTools(all, config, senderName) {
|
|
2314
|
+
const per = config?.per_caller?.[senderName]?.tools;
|
|
2315
|
+
const allow = per ?? config?.tools ?? READ_ONLY_TOOLS;
|
|
2316
|
+
const set = new Set(allow);
|
|
2317
|
+
return all.filter((t) => set.has(t.name));
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
async function loadWiseAgentContext(deps, config) {
|
|
2321
|
+
const cwd = deps.cwd || "~";
|
|
2322
|
+
let projectInstructions = "";
|
|
2323
|
+
try {
|
|
2324
|
+
const r = await deps.runBash('cat WISE.md "$HOME/.svamp/wise.md" .svamp/wise.md 2>/dev/null || true', { cwd });
|
|
2325
|
+
projectInstructions = parseWiseMd((r.stdout || "").trim()).body;
|
|
2326
|
+
} catch {
|
|
2327
|
+
}
|
|
2328
|
+
let skills = [];
|
|
2329
|
+
try {
|
|
2330
|
+
const r = await deps.runBash(buildSkillsScanCommand(), { cwd });
|
|
2331
|
+
skills = parseSkillsDump(r.stdout || "");
|
|
2332
|
+
} catch {
|
|
2333
|
+
}
|
|
2334
|
+
if (config?.skills && config.skills.length) {
|
|
2335
|
+
const allow = new Set(config.skills.map((s) => s.toLowerCase()));
|
|
2336
|
+
skills = skills.filter((s) => allow.has(s.name.toLowerCase()));
|
|
2337
|
+
}
|
|
2338
|
+
return { projectInstructions, skills };
|
|
2339
|
+
}
|
|
2340
|
+
function buildWiseAgentInstructions(ctx, config) {
|
|
2341
|
+
const base = `# Role & Objective
|
|
2342
|
+
You are WISE Agent, a fast, text-mode companion to the deep coding agent (Claude) working in this session. You answer quickly and run short tasks against the session's machine, CLIs, skills, and services on a caller's behalf. Success = answering or doing what was asked, then reporting back briefly.
|
|
2343
|
+
|
|
2344
|
+
# Personality & Tone
|
|
2345
|
+
- A capable, hands-on colleague \u2014 direct, concise, no preamble or narration.
|
|
2346
|
+
- Keep replies to 1\u20133 short sentences. Summarize results; never paste raw JSON, logs, or long output back.
|
|
2347
|
+
- Act now; don't promise to "check back later." No time estimates.
|
|
2348
|
+
|
|
2349
|
+
# Context
|
|
2350
|
+
- "the agent" / "the coding agent" = the separate, deep Claude agent working in this session. You are WISE Agent, not that agent \u2014 you are the fast lane beside it.
|
|
2351
|
+
- Your tools act on the bound session and its machine.
|
|
2352
|
+
- Callers reach you over HTTP/RPC and may be other agents, CI, or people. Each message carries provenance (who sent it, and whether it is verified) \u2014 weigh it before doing anything destructive.
|
|
2353
|
+
|
|
2354
|
+
# Tools
|
|
2355
|
+
- get_context \u2014 status + recent activity of the session. Use first for "what's happening / its status".
|
|
2356
|
+
- ask_session \u2014 ask the deep coding agent a read-only question about its own work (slower).
|
|
2357
|
+
- summarize_session \u2014 a cheap subagent that summarizes the deep agent's transcript for a specific question.
|
|
2358
|
+
- use_skill \u2014 load a project skill's full steps by name, then carry them out with run_bash.
|
|
2359
|
+
- run_bash \u2014 run a shell command on the session's machine (when granted).
|
|
2360
|
+
- run_js \u2014 JavaScript with an async \`svamp\` API to compose several steps in one call (when granted).
|
|
2361
|
+
- send_to_session \u2014 hand a clear, reformulated instruction to the deep coding agent (when granted); pass wait=true to block for its reply.
|
|
2362
|
+
|
|
2363
|
+
# Instructions
|
|
2364
|
+
- Answer general questions and questions about yourself directly. Use tools only to act on the machine/session.
|
|
2365
|
+
- Take the cheap path: read state directly; delegate anything LONG to summarize_session \u2014 keep your own context small.
|
|
2366
|
+
- For destructive actions (deleting, stopping, killing), require a verified caller and confirm intent; for safe reads, just do it.
|
|
2367
|
+
- If a tool fails or returns nothing useful, say so plainly \u2014 never fabricate a result.
|
|
2368
|
+
- Report the outcome in one line.`;
|
|
2369
|
+
const parts = [base];
|
|
2370
|
+
const custom = ctx.projectInstructions?.trim();
|
|
2371
|
+
if (custom) parts.push(`# Project notes (WISE.md)
|
|
2372
|
+
${custom}`);
|
|
2373
|
+
const extra = config?.system?.trim();
|
|
2374
|
+
if (extra) parts.push(`# Channel brief
|
|
2375
|
+
${extra}`);
|
|
2376
|
+
const skillsSection = buildSkillsPromptSection(ctx.skills);
|
|
2377
|
+
if (skillsSection) parts.push(skillsSection);
|
|
2378
|
+
return parts.join("\n\n");
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
const DEFAULTS = {
|
|
2382
|
+
// Fast, cheap default — the snappy companion tier.
|
|
2383
|
+
"openai": { baseUrl: "https://api.openai.com", model: "gpt-5-mini" },
|
|
2384
|
+
// Quota-governed gateway. NOTE: the Hypha proxy is Anthropic-shaped today; an
|
|
2385
|
+
// OpenAI-compatible route may be required for non-Claude models (docs §6).
|
|
2386
|
+
"hypha-proxy": { baseUrl: "", model: "gpt-5-mini" },
|
|
2387
|
+
// Fallback when no OpenAI access — Claude Haiku via the same proxy path.
|
|
2388
|
+
"claude-haiku": { baseUrl: "", model: "claude-haiku-4-5" }
|
|
2389
|
+
};
|
|
2390
|
+
function resolveModel(config, env) {
|
|
2391
|
+
const provider = config?.provider || env.WISE_AGENT_PROVIDER || "openai";
|
|
2392
|
+
const d = DEFAULTS[provider] || DEFAULTS.openai;
|
|
2393
|
+
const model = config?.model || env.WISE_AGENT_MODEL || d.model;
|
|
2394
|
+
let baseUrl = env.WISE_AGENT_BASE_URL || d.baseUrl;
|
|
2395
|
+
let apiKey = env.WISE_AGENT_API_KEY || "";
|
|
2396
|
+
if (provider === "openai") {
|
|
2397
|
+
apiKey = apiKey || env.OPENAI_API_KEY || "";
|
|
2398
|
+
} else if (provider === "hypha-proxy" || provider === "claude-haiku") {
|
|
2399
|
+
baseUrl = baseUrl || (env.SVAMP_HYPHA_PROXY_URL || "").replace(/\/+$/, "");
|
|
2400
|
+
apiKey = apiKey || env.HYPHA_TOKEN || "";
|
|
2401
|
+
}
|
|
2402
|
+
baseUrl = baseUrl.replace(/\/v1\/?$/, "");
|
|
2403
|
+
return { provider, baseUrl, apiKey, model };
|
|
2404
|
+
}
|
|
2405
|
+
function makeHttpTransport(resolved, fetchImpl = fetch) {
|
|
2406
|
+
return async (req) => {
|
|
2407
|
+
if (!resolved.baseUrl) throw new Error(`WISE Agent: no base URL for provider "${resolved.provider}" (set WISE_AGENT_BASE_URL or SVAMP_HYPHA_PROXY_URL)`);
|
|
2408
|
+
const url = `${resolved.baseUrl}/v1/chat/completions`;
|
|
2409
|
+
const res = await fetchImpl(url, {
|
|
2410
|
+
method: "POST",
|
|
2411
|
+
headers: {
|
|
2412
|
+
"content-type": "application/json",
|
|
2413
|
+
...resolved.apiKey ? { authorization: `Bearer ${resolved.apiKey}` } : {}
|
|
2414
|
+
},
|
|
2415
|
+
body: JSON.stringify({ ...req, model: req.model || resolved.model })
|
|
2416
|
+
});
|
|
2417
|
+
if (!res.ok) {
|
|
2418
|
+
const text = await res.text().catch(() => "");
|
|
2419
|
+
throw new Error(`WISE Agent chat ${res.status}: ${text.slice(0, 300)}`);
|
|
2420
|
+
}
|
|
2421
|
+
return await res.json();
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
const DEFAULT_TIMEOUT_SEC = 30;
|
|
2426
|
+
const DEFAULT_MAX_STEPS = 6;
|
|
2427
|
+
function toChatTools(tools) {
|
|
2428
|
+
return tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
2429
|
+
}
|
|
2430
|
+
const addUsage = (a, b) => ({
|
|
2431
|
+
prompt_tokens: (a?.prompt_tokens || 0) + (b?.prompt_tokens || 0),
|
|
2432
|
+
completion_tokens: (a?.completion_tokens || 0) + (b?.completion_tokens || 0),
|
|
2433
|
+
total_tokens: (a?.total_tokens || 0) + (b?.total_tokens || 0)
|
|
2434
|
+
});
|
|
2435
|
+
async function runWiseAgent(args) {
|
|
2436
|
+
const { message, sender, config, deps, transport, model } = args;
|
|
2437
|
+
const timeoutMs = (config?.timeout_sec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
|
|
2438
|
+
const maxSteps = config?.max_steps ?? DEFAULT_MAX_STEPS;
|
|
2439
|
+
const toolCalls = [];
|
|
2440
|
+
let usage;
|
|
2441
|
+
const turn = async () => {
|
|
2442
|
+
const ctx = await loadWiseAgentContext(deps, config);
|
|
2443
|
+
const system = buildWiseAgentInstructions(ctx, config);
|
|
2444
|
+
const allTools = buildTools(deps, ctx.skills);
|
|
2445
|
+
const granted = gateTools(allTools, config, sender.name);
|
|
2446
|
+
const byName = new Map(granted.map((t) => [t.name, t]));
|
|
2447
|
+
const chatTools = toChatTools(granted);
|
|
2448
|
+
const messages = [
|
|
2449
|
+
{ role: "system", content: system },
|
|
2450
|
+
{ role: "user", content: message }
|
|
2451
|
+
];
|
|
2452
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
2453
|
+
const res2 = await transport({
|
|
2454
|
+
model,
|
|
2455
|
+
messages,
|
|
2456
|
+
...chatTools.length ? { tools: chatTools, tool_choice: "auto" } : {}
|
|
2457
|
+
});
|
|
2458
|
+
if (res2.usage) usage = addUsage(usage, res2.usage);
|
|
2459
|
+
const choice = res2.choices?.[0]?.message;
|
|
2460
|
+
if (!choice) return { status: "error", reply: "", toolCalls, usage, error: "empty model response" };
|
|
2461
|
+
const calls = choice.tool_calls || [];
|
|
2462
|
+
if (calls.length === 0) {
|
|
2463
|
+
return { status: "completed", reply: (choice.content || "").trim(), toolCalls, usage };
|
|
2464
|
+
}
|
|
2465
|
+
messages.push({ role: "assistant", content: choice.content ?? "", tool_calls: calls });
|
|
2466
|
+
for (const call of calls) {
|
|
2467
|
+
let args2 = {};
|
|
2468
|
+
try {
|
|
2469
|
+
args2 = call.function.arguments ? JSON.parse(call.function.arguments) : {};
|
|
2470
|
+
} catch {
|
|
2471
|
+
}
|
|
2472
|
+
const tool = byName.get(call.function.name);
|
|
2473
|
+
let result;
|
|
2474
|
+
let error;
|
|
2475
|
+
if (!tool) {
|
|
2476
|
+
error = `tool "${call.function.name}" is not available to this caller`;
|
|
2477
|
+
result = error;
|
|
2478
|
+
} else {
|
|
2479
|
+
try {
|
|
2480
|
+
result = await tool.run(args2);
|
|
2481
|
+
} catch (e) {
|
|
2482
|
+
error = String(e?.message || e);
|
|
2483
|
+
result = `error: ${error}`;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
toolCalls.push({ name: call.function.name, args: args2, result: error ? void 0 : result, error });
|
|
2487
|
+
messages.push({ role: "tool", tool_call_id: call.id, name: call.function.name, content: result });
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
const res = await transport({ model, messages, tool_choice: "none" });
|
|
2491
|
+
if (res.usage) usage = addUsage(usage, res.usage);
|
|
2492
|
+
const final = res.choices?.[0]?.message?.content || "";
|
|
2493
|
+
return { status: "completed", reply: final.trim() || "(reached step limit without a final answer)", toolCalls, usage };
|
|
2494
|
+
};
|
|
2495
|
+
let timer;
|
|
2496
|
+
const timeout = new Promise((resolve) => {
|
|
2497
|
+
timer = setTimeout(() => resolve({ status: "timeout", reply: "(timed out)", toolCalls, usage }), timeoutMs);
|
|
2498
|
+
});
|
|
2499
|
+
try {
|
|
2500
|
+
return await Promise.race([turn(), timeout]);
|
|
2501
|
+
} catch (e) {
|
|
2502
|
+
return { status: "error", reply: "", toolCalls, usage, error: String(e?.message || e) };
|
|
2503
|
+
} finally {
|
|
2504
|
+
if (timer) clearTimeout(timer);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
let _testTransport;
|
|
2509
|
+
async function dispatchAgentChannel(a) {
|
|
2510
|
+
const config = a.channel.action?.kind === "agent" ? a.channel.action.agent || {} : {};
|
|
2511
|
+
const resolved = resolveModel(config, a.env);
|
|
2512
|
+
const transport = a.transport || _testTransport || makeHttpTransport(resolved);
|
|
2513
|
+
return runWiseAgent({
|
|
2514
|
+
message: a.message,
|
|
2515
|
+
sender: a.sender,
|
|
2516
|
+
config,
|
|
2517
|
+
deps: a.deps,
|
|
2518
|
+
transport,
|
|
2519
|
+
model: resolved.model
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
function textFrom(r) {
|
|
2524
|
+
if (r == null) return "";
|
|
2525
|
+
if (typeof r === "string") return r;
|
|
2526
|
+
if (r.success === false) return `error: ${r.error || "failed"}`;
|
|
2527
|
+
return String(r.result ?? r.answer ?? r.text ?? r.response ?? r.stdout ?? JSON.stringify(r));
|
|
2528
|
+
}
|
|
2529
|
+
function normalizeBash(r) {
|
|
2530
|
+
if (r && typeof r === "object") {
|
|
2531
|
+
return {
|
|
2532
|
+
stdout: String(r.stdout ?? ""),
|
|
2533
|
+
stderr: String(r.stderr ?? ""),
|
|
2534
|
+
exitCode: Number(r.exitCode ?? (r.success === false ? 1 : 0))
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
return { stdout: String(r ?? ""), stderr: "", exitCode: 0 };
|
|
2538
|
+
}
|
|
2539
|
+
function buildSessionDeps(rpc, opts = {}) {
|
|
2540
|
+
const ctx = opts.ownerEmail ? { user: { email: opts.ownerEmail, id: opts.ownerEmail } } : void 0;
|
|
2541
|
+
return {
|
|
2542
|
+
cwd: opts.cwd,
|
|
2543
|
+
async runBash(command, o) {
|
|
2544
|
+
return normalizeBash(await rpc.bash(command, o?.cwd, void 0, ctx));
|
|
2545
|
+
},
|
|
2546
|
+
async sessionSend(text) {
|
|
2547
|
+
const content = JSON.stringify({ role: "user", content: { type: "text", text } });
|
|
2548
|
+
await rpc.sendMessage(content, void 0, { sentFrom: "wise-agent" }, ctx);
|
|
2549
|
+
},
|
|
2550
|
+
async askSession(question) {
|
|
2551
|
+
return textFrom(await rpc.btw(question, ctx));
|
|
2552
|
+
},
|
|
2553
|
+
async summarizeSession(question) {
|
|
2554
|
+
return textFrom(await rpc.btw(`Briefly summarize the work so far, answering: ${question}`, ctx));
|
|
2555
|
+
},
|
|
2556
|
+
async getContext() {
|
|
2557
|
+
let messages = [];
|
|
2558
|
+
try {
|
|
2559
|
+
const r = await rpc.getLatestMessages(void 0, 6, ctx);
|
|
2560
|
+
messages = Array.isArray(r) ? r : r?.messages ?? [];
|
|
2561
|
+
} catch {
|
|
2562
|
+
}
|
|
2563
|
+
const latest = messages.length ? messages[messages.length - 1] : null;
|
|
2564
|
+
const latestText = latest?.content?.content?.text ?? latest?.content?.text ?? null;
|
|
2565
|
+
return {
|
|
2566
|
+
...opts.status ? opts.status() : {},
|
|
2567
|
+
recentMessageCount: messages.length,
|
|
2568
|
+
latestMessage: latestText
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function channelPublicView(c) {
|
|
2575
|
+
return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind };
|
|
2576
|
+
}
|
|
1868
2577
|
function isStructuredMessage(msg) {
|
|
1869
2578
|
return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId);
|
|
1870
2579
|
}
|
|
@@ -2084,6 +2793,13 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
2084
2793
|
notifyListeners({ type: "update-session", sessionId, metadata: { value: metadata, version: metadataVersion } });
|
|
2085
2794
|
callbacks.onMetadataUpdate?.(metadata);
|
|
2086
2795
|
};
|
|
2796
|
+
const channelStore = new ChannelStore(initialMetadata.path);
|
|
2797
|
+
const syncChannelsToMetadata = () => {
|
|
2798
|
+
metadata.channels = channelStore.list();
|
|
2799
|
+
metadataVersion++;
|
|
2800
|
+
notifyListeners({ type: "update-session", sessionId, metadata: { value: metadata, version: metadataVersion } });
|
|
2801
|
+
callbacks.onMetadataUpdate?.(metadata);
|
|
2802
|
+
};
|
|
2087
2803
|
const rpcHandlers = {
|
|
2088
2804
|
// ── Messages ──
|
|
2089
2805
|
getMessages: async (afterSeq, limit, context) => {
|
|
@@ -2254,6 +2970,93 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
2254
2970
|
syncRoutinesToMetadata();
|
|
2255
2971
|
return { success: true, resolved };
|
|
2256
2972
|
},
|
|
2973
|
+
// ── Channels (project-folder config; served by the channel server) ──
|
|
2974
|
+
listChannels: async (context) => {
|
|
2975
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
2976
|
+
return { channels: channelStore.list() };
|
|
2977
|
+
},
|
|
2978
|
+
saveChannel: async (channel, context) => {
|
|
2979
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
2980
|
+
try {
|
|
2981
|
+
const saved = channelStore.save(channel);
|
|
2982
|
+
syncChannelsToMetadata();
|
|
2983
|
+
return { success: true, channel: saved };
|
|
2984
|
+
} catch (e) {
|
|
2985
|
+
return { success: false, error: e?.message || String(e) };
|
|
2986
|
+
}
|
|
2987
|
+
},
|
|
2988
|
+
removeChannel: async (id, context) => {
|
|
2989
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
2990
|
+
const ok = channelStore.remove(id);
|
|
2991
|
+
syncChannelsToMetadata();
|
|
2992
|
+
return { success: ok };
|
|
2993
|
+
},
|
|
2994
|
+
setChannelEnabled: async (id, enabled, context) => {
|
|
2995
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
2996
|
+
channelStore.setEnabled(id, enabled);
|
|
2997
|
+
syncChannelsToMetadata();
|
|
2998
|
+
return { success: true };
|
|
2999
|
+
},
|
|
3000
|
+
addChannelCaller: async (id, name, kind, context) => {
|
|
3001
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
3002
|
+
const caller = channelStore.addCaller(id, name, kind);
|
|
3003
|
+
syncChannelsToMetadata();
|
|
3004
|
+
return { success: !!caller, caller };
|
|
3005
|
+
},
|
|
3006
|
+
getChannelSkill: async (id, context) => {
|
|
3007
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
3008
|
+
const c = channelStore.get(id);
|
|
3009
|
+
if (!c) return { error: "not found" };
|
|
3010
|
+
return { skill: generateSkillBody(c) };
|
|
3011
|
+
},
|
|
3012
|
+
// Public channel discovery (no session authz — channels are deliberately
|
|
3013
|
+
// published endpoints; each send() is gated by the channel's OWN identity).
|
|
3014
|
+
channelList: async () => {
|
|
3015
|
+
return { channels: channelStore.list().filter((c) => c.enabled !== false).map(channelPublicView) };
|
|
3016
|
+
},
|
|
3017
|
+
channelDescribe: async (id) => {
|
|
3018
|
+
const c = channelStore.get(id);
|
|
3019
|
+
if (!c || c.enabled === false) return { error: "not found" };
|
|
3020
|
+
return { ...channelPublicView(c), skill: { body: generateSkillBody(c) } };
|
|
3021
|
+
},
|
|
3022
|
+
channelSend: async (params, context) => {
|
|
3023
|
+
const c = channelStore.get(params.channel);
|
|
3024
|
+
if (!c || c.enabled === false) return { error: "channel not found" };
|
|
3025
|
+
const u = context?.user;
|
|
3026
|
+
const r = resolveSender(c, {
|
|
3027
|
+
key: params.key,
|
|
3028
|
+
from: params.from,
|
|
3029
|
+
hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
|
|
3030
|
+
hyphaAnonymous: u?.is_anonymous === true,
|
|
3031
|
+
hyphaWorkspace: u?.scope?.current_workspace
|
|
3032
|
+
});
|
|
3033
|
+
if (r.error || !r.sender) return { error: r.error || "unauthorized" };
|
|
3034
|
+
const callId = "call_" + randomUUID().slice(0, 10);
|
|
3035
|
+
const rendered = renderMessage(c, { sender: r.sender, body: { message: params.message }, callId });
|
|
3036
|
+
const ownerEmail = metadata.sharing?.owner;
|
|
3037
|
+
const ownerCtx = ownerEmail ? { user: { email: ownerEmail, id: ownerEmail } } : void 0;
|
|
3038
|
+
try {
|
|
3039
|
+
if (c.action?.kind === "agent") {
|
|
3040
|
+
const deps = buildSessionDeps(rpcHandlers, {
|
|
3041
|
+
cwd: metadata.path,
|
|
3042
|
+
ownerEmail,
|
|
3043
|
+
status: () => ({ thinking: lastActivity.thinking, sessionId })
|
|
3044
|
+
});
|
|
3045
|
+
const result = await dispatchAgentChannel({ channel: c, sender: r.sender, message: rendered, deps, env: process.env });
|
|
3046
|
+
channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: result.status });
|
|
3047
|
+
syncChannelsToMetadata();
|
|
3048
|
+
return { ok: result.status === "completed", call_id: callId, status: result.status, reply: result.reply, tool_calls: result.toolCalls, error: result.error };
|
|
3049
|
+
}
|
|
3050
|
+
if (c.action?.kind === "loop") return { error: "loop channels are served by the channel server, not channelSend" };
|
|
3051
|
+
await rpcHandlers.sendMessage(rendered, void 0, { sentFrom: "channel" }, ownerCtx);
|
|
3052
|
+
channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "delivered" });
|
|
3053
|
+
syncChannelsToMetadata();
|
|
3054
|
+
return { ok: true, call_id: callId, status: "accepted" };
|
|
3055
|
+
} catch (e) {
|
|
3056
|
+
channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "error" });
|
|
3057
|
+
return { ok: false, call_id: callId, status: "error", error: e?.message || String(e) };
|
|
3058
|
+
}
|
|
3059
|
+
},
|
|
2257
3060
|
// ── Agent State ──
|
|
2258
3061
|
getAgentState: async (context) => {
|
|
2259
3062
|
authorizeRequest(context, metadata.sharing, "view");
|
|
@@ -7241,17 +8044,17 @@ function buildBaselineSystemPrompt(sessionId) {
|
|
|
7241
8044
|
You are running inside a Svamp session (id: ${sessionId}) on Hypha Cloud. Use the \`svamp\` CLI to manage session state (title, link, notify) and the \`hypha\` CLI for artifacts, tokens, and RPC services. Installed skills live in \`~/.claude/skills/\` \u2014 read them for full references.
|
|
7242
8045
|
|
|
7243
8046
|
**Session state:**
|
|
7244
|
-
- \`svamp session set-title "<title>"\` \u2014 set a concise 3-8 word title after the first response
|
|
8047
|
+
- \`svamp session set-title "<title>"\` \u2014 set a concise 3-8 word title after the first response, and update it whenever the topic shifts. This is how the user and other agents recognize you in lists \u2014 keep it current.
|
|
7245
8048
|
- \`svamp session set-link "<url>" "<label>"\` \u2014 surface any viewable artifact as a button
|
|
7246
8049
|
- \`svamp session notify "<msg>" [--level info|warning|error]\` \u2014 send a user notification
|
|
7247
8050
|
|
|
7248
8051
|
**Artifacts (rich inline output):** Write HTML to a file (\`.svamp/<sessionId>/outputs/\` for disposable, \`./outputs/\` for persistent) and emit \`<artifact src="./outputs/viz.html" title="..." />\` \u2014 the Svamp client renders it inline as a sandboxed iframe with theme CSS vars, auto-resize, file inlining, and a header with Reload/Fullscreen/\u22EE menu. Modes: \`default\`, \`bare\` (controls on hover), \`immersive\` (no chrome), \`card\` (click-to-open preview with \`description\`+\`poster\`). Use \`<artifact src="https://..." height="540" />\` to embed a live server. Run \`svamp serve <name> <dir>\` to share. Use a real backend server (\`svamp service expose\`) when you need persistence/auth. See the \`artifact\` skill.
|
|
7249
8052
|
|
|
7250
|
-
##
|
|
8053
|
+
## Shared environment
|
|
7251
8054
|
|
|
7252
|
-
You
|
|
8055
|
+
You share this machine and project folder with other agent sessions (same user, possibly other machines too). When collaboration, coordination, or avoiding edit conflicts becomes relevant, run \`svamp session whoami\` \u2014 it shows who's around and how to reach them. Keep cross-agent contact purposeful: only initiate with a concrete reason, avoid ping-pong loops.
|
|
7253
8056
|
|
|
7254
|
-
**Inbox messages
|
|
8057
|
+
**Inbox messages from other agents** arrive wrapped as \`<svamp-message message-id="\u2026" from="agent:\u2026" from-session="\u2026" \u2026>BODY</svamp-message>\` (a plain user turn has no wrapper). Reply only when useful: \`svamp session inbox reply <message-id> "<body>"\`.
|
|
7255
8058
|
`;
|
|
7256
8059
|
}
|
|
7257
8060
|
|
|
@@ -7512,6 +8315,32 @@ function checkTruncation(format, tail, fileSize, head) {
|
|
|
7512
8315
|
})().catch(() => { /* never block on validator errors */ });
|
|
7513
8316
|
`;
|
|
7514
8317
|
|
|
8318
|
+
const MAX_SESSION_READFILE_BYTES = 10 * 1024 * 1024;
|
|
8319
|
+
async function readSessionFileBase64(resolvedPath) {
|
|
8320
|
+
let st;
|
|
8321
|
+
try {
|
|
8322
|
+
st = await fs.stat(resolvedPath);
|
|
8323
|
+
} catch (err) {
|
|
8324
|
+
if (err?.code === "ENOENT") {
|
|
8325
|
+
return { success: false, code: "ENOENT", error: "File not found", path: resolvedPath };
|
|
8326
|
+
}
|
|
8327
|
+
throw err;
|
|
8328
|
+
}
|
|
8329
|
+
if (st.isDirectory()) {
|
|
8330
|
+
return { success: false, code: "EISDIR", error: "Path is a directory, not a file", path: resolvedPath };
|
|
8331
|
+
}
|
|
8332
|
+
if (!st.isFile()) {
|
|
8333
|
+
return { success: false, code: "ENOTREG", error: "Not a regular file (pipes, devices, and sockets cannot be previewed)", path: resolvedPath };
|
|
8334
|
+
}
|
|
8335
|
+
if (st.size > MAX_SESSION_READFILE_BYTES) {
|
|
8336
|
+
const mb = (st.size / (1024 * 1024)).toFixed(1);
|
|
8337
|
+
const limitMb = (MAX_SESSION_READFILE_BYTES / (1024 * 1024)).toFixed(0);
|
|
8338
|
+
return { success: false, code: "EFBIG", error: `File too large to preview (${mb} MB, limit ${limitMb} MB). Download it instead.`, path: resolvedPath };
|
|
8339
|
+
}
|
|
8340
|
+
const buffer = await fs.readFile(resolvedPath);
|
|
8341
|
+
return buffer.toString("base64");
|
|
8342
|
+
}
|
|
8343
|
+
|
|
7515
8344
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
7516
8345
|
const __dirname$1 = dirname(__filename$1);
|
|
7517
8346
|
const CLAUDE_SKILLS_DIR = join(os.homedir(), ".claude", "skills");
|
|
@@ -7810,6 +8639,27 @@ loadDotEnv();
|
|
|
7810
8639
|
const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
|
|
7811
8640
|
const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
|
|
7812
8641
|
const DAEMON_LOCK_FILE = join(SVAMP_HOME, "daemon.lock");
|
|
8642
|
+
const DAEMON_STOP_MARKER_FILE = join(SVAMP_HOME, "daemon.stop");
|
|
8643
|
+
function writeStopMarker(reason) {
|
|
8644
|
+
try {
|
|
8645
|
+
writeFileSync(DAEMON_STOP_MARKER_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${reason}
|
|
8646
|
+
`, "utf-8");
|
|
8647
|
+
} catch {
|
|
8648
|
+
}
|
|
8649
|
+
}
|
|
8650
|
+
function clearStopMarker() {
|
|
8651
|
+
try {
|
|
8652
|
+
if (existsSync$1(DAEMON_STOP_MARKER_FILE)) unlinkSync$1(DAEMON_STOP_MARKER_FILE);
|
|
8653
|
+
} catch {
|
|
8654
|
+
}
|
|
8655
|
+
}
|
|
8656
|
+
function stopMarkerExists() {
|
|
8657
|
+
try {
|
|
8658
|
+
return existsSync$1(DAEMON_STOP_MARKER_FILE);
|
|
8659
|
+
} catch {
|
|
8660
|
+
return false;
|
|
8661
|
+
}
|
|
8662
|
+
}
|
|
7813
8663
|
const LOGS_DIR = join(SVAMP_HOME, "logs");
|
|
7814
8664
|
const SESSION_INDEX_FILE = join(SVAMP_HOME, "sessions-index.json");
|
|
7815
8665
|
function readPackageVersion() {
|
|
@@ -8685,7 +9535,7 @@ async function startDaemon(options) {
|
|
|
8685
9535
|
const list = loadExposedTunnels().filter((t) => t.name !== name);
|
|
8686
9536
|
saveExposedTunnels(list);
|
|
8687
9537
|
}
|
|
8688
|
-
const { ServeManager } = await import('./serveManager-
|
|
9538
|
+
const { ServeManager } = await import('./serveManager-DHJy5C0z.mjs');
|
|
8689
9539
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
8690
9540
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
8691
9541
|
});
|
|
@@ -9365,6 +10215,17 @@ async function startDaemon(options) {
|
|
|
9365
10215
|
});
|
|
9366
10216
|
}
|
|
9367
10217
|
checkSvampConfig?.();
|
|
10218
|
+
try {
|
|
10219
|
+
const hasTitle = !!sessionMetadata.summary?.text?.trim() || !!sessionMetadata.customTitle?.toString().trim();
|
|
10220
|
+
if (!hasTitle) {
|
|
10221
|
+
const base = basename(directory.replace(/[/\\]+$/, "")) || "session";
|
|
10222
|
+
sessionMetadata = { ...sessionMetadata, summary: { text: base, updatedAt: Date.now() } };
|
|
10223
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
10224
|
+
sessionService.pushMessage({ type: "summary", summary: base }, "session");
|
|
10225
|
+
logger.log(`[Session ${sessionId}] Auto-title fallback \u2192 "${base}"`);
|
|
10226
|
+
}
|
|
10227
|
+
} catch {
|
|
10228
|
+
}
|
|
9368
10229
|
if (backgroundTaskCount > 0) {
|
|
9369
10230
|
const taskInfo = `Background tasks still running (${backgroundTaskCount}): ${backgroundTaskNames.join(", ")}`;
|
|
9370
10231
|
logger.log(`[Session ${sessionId}] ${taskInfo}`);
|
|
@@ -10230,15 +11091,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
10230
11091
|
if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
10231
11092
|
throw new Error("Path outside working directory");
|
|
10232
11093
|
}
|
|
10233
|
-
|
|
10234
|
-
const buffer = await fs.readFile(resolvedPath);
|
|
10235
|
-
return buffer.toString("base64");
|
|
10236
|
-
} catch (err) {
|
|
10237
|
-
if (err?.code === "ENOENT") {
|
|
10238
|
-
return { success: false, code: "ENOENT", error: "File not found", path: resolvedPath };
|
|
10239
|
-
}
|
|
10240
|
-
throw err;
|
|
10241
|
-
}
|
|
11094
|
+
return await readSessionFileBase64(resolvedPath);
|
|
10242
11095
|
},
|
|
10243
11096
|
onWriteFile: async (path, content) => {
|
|
10244
11097
|
const resolvedPath = resolve(directory, path);
|
|
@@ -10726,15 +11579,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
10726
11579
|
if (sessionMetadata.securityContext && resolvedPath !== resolve(directory) && !resolvedPath.startsWith(resolve(directory) + "/")) {
|
|
10727
11580
|
throw new Error("Path outside working directory");
|
|
10728
11581
|
}
|
|
10729
|
-
|
|
10730
|
-
const buffer = await fs.readFile(resolvedPath);
|
|
10731
|
-
return buffer.toString("base64");
|
|
10732
|
-
} catch (err) {
|
|
10733
|
-
if (err?.code === "ENOENT") {
|
|
10734
|
-
return { success: false, code: "ENOENT", error: "File not found", path: resolvedPath };
|
|
10735
|
-
}
|
|
10736
|
-
throw err;
|
|
10737
|
-
}
|
|
11582
|
+
return await readSessionFileBase64(resolvedPath);
|
|
10738
11583
|
},
|
|
10739
11584
|
onWriteFile: async (path, content) => {
|
|
10740
11585
|
const resolvedPath = resolve(directory, path);
|
|
@@ -10884,6 +11729,17 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
10884
11729
|
insideOnTurnEnd = true;
|
|
10885
11730
|
try {
|
|
10886
11731
|
checkSvampConfig?.();
|
|
11732
|
+
try {
|
|
11733
|
+
const hasTitle = !!sessionMetadata.summary?.text?.trim() || !!sessionMetadata.customTitle?.toString().trim();
|
|
11734
|
+
if (!hasTitle) {
|
|
11735
|
+
const base = basename(directory.replace(/[/\\]+$/, "")) || "session";
|
|
11736
|
+
sessionMetadata = { ...sessionMetadata, summary: { text: base, updatedAt: Date.now() } };
|
|
11737
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
11738
|
+
sessionService.pushMessage({ type: "summary", summary: base }, "session");
|
|
11739
|
+
logger.log(`[Session ${sessionId}] Auto-title fallback \u2192 "${base}"`);
|
|
11740
|
+
}
|
|
11741
|
+
} catch {
|
|
11742
|
+
}
|
|
10887
11743
|
const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10888
11744
|
if (rlState) {
|
|
10889
11745
|
let promiseFulfilled = false;
|
|
@@ -11258,7 +12114,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11258
12114
|
const specs = loadExposedTunnels();
|
|
11259
12115
|
if (specs.length === 0) return;
|
|
11260
12116
|
logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
|
|
11261
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
12117
|
+
const { FrpcTunnel } = await import('./frpc-BiazjxzU.mjs');
|
|
11262
12118
|
for (const spec of specs) {
|
|
11263
12119
|
if (tunnels.has(spec.name)) continue;
|
|
11264
12120
|
try {
|
|
@@ -11794,6 +12650,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11794
12650
|
async function stopDaemon(options) {
|
|
11795
12651
|
const signal = options?.cleanup ? "SIGUSR1" : "SIGTERM";
|
|
11796
12652
|
const mode = options?.cleanup ? "cleanup (sessions will be stopped)" : "quick (sessions preserved for auto-restore)";
|
|
12653
|
+
writeStopMarker(`stopDaemon (${options?.cleanup ? "cleanup" : "quick"})`);
|
|
11797
12654
|
const pidsToSignal = [];
|
|
11798
12655
|
const supervisorPidFile = join(SVAMP_HOME, "supervisor.pid");
|
|
11799
12656
|
try {
|
|
@@ -11837,17 +12694,32 @@ async function stopDaemon(options) {
|
|
|
11837
12694
|
} catch {
|
|
11838
12695
|
}
|
|
11839
12696
|
}
|
|
11840
|
-
|
|
12697
|
+
const { stopViaServiceManager } = await import('./serviceManager-hlOVxkhW.mjs');
|
|
12698
|
+
if (options?.cleanup && state?.pid) {
|
|
12699
|
+
try {
|
|
12700
|
+
process.kill(state.pid, "SIGUSR1");
|
|
12701
|
+
console.log(`Sent SIGUSR1 to daemon PID ${state.pid} \u2014 ${mode}`);
|
|
12702
|
+
} catch {
|
|
12703
|
+
}
|
|
12704
|
+
}
|
|
12705
|
+
const handledByService = await stopViaServiceManager();
|
|
12706
|
+
if (handledByService) {
|
|
12707
|
+
console.log("Stopped via OS service manager (KeepAlive/Restart disabled)");
|
|
12708
|
+
}
|
|
12709
|
+
if (pidsToSignal.length === 0 && !handledByService) {
|
|
11841
12710
|
console.log("No daemon running");
|
|
11842
12711
|
cleanupDaemonStateFile();
|
|
12712
|
+
clearStopMarker();
|
|
11843
12713
|
return;
|
|
11844
12714
|
}
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
12715
|
+
if (!handledByService) {
|
|
12716
|
+
for (const pid of pidsToSignal) {
|
|
12717
|
+
try {
|
|
12718
|
+
process.kill(pid, signal);
|
|
12719
|
+
console.log(`Sent ${signal} to PID ${pid} \u2014 ${mode}`);
|
|
12720
|
+
} catch {
|
|
12721
|
+
console.log(`PID ${pid} already gone`);
|
|
12722
|
+
}
|
|
11851
12723
|
}
|
|
11852
12724
|
}
|
|
11853
12725
|
pidsToSignal[0];
|
|
@@ -11904,19 +12776,24 @@ async function restartDaemon() {
|
|
|
11904
12776
|
const doFullRestart = async (reason) => {
|
|
11905
12777
|
console.log(`${reason} \u2014 doing full stop + start`);
|
|
11906
12778
|
await stopDaemon();
|
|
11907
|
-
|
|
11908
|
-
const
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
12779
|
+
clearStopMarker();
|
|
12780
|
+
const { ensureSupervisorViaServiceManager } = await import('./serviceManager-hlOVxkhW.mjs');
|
|
12781
|
+
const startedViaService = await ensureSupervisorViaServiceManager();
|
|
12782
|
+
if (!startedViaService) {
|
|
12783
|
+
const { spawn: spawn2 } = await import('child_process');
|
|
12784
|
+
const child = spawn2(process.execPath, [
|
|
12785
|
+
"--no-warnings",
|
|
12786
|
+
"--no-deprecation",
|
|
12787
|
+
...process.argv.slice(1, 2),
|
|
12788
|
+
"daemon",
|
|
12789
|
+
"start-supervised"
|
|
12790
|
+
], {
|
|
12791
|
+
detached: true,
|
|
12792
|
+
stdio: "ignore",
|
|
12793
|
+
env: process.env
|
|
12794
|
+
});
|
|
12795
|
+
child.unref();
|
|
12796
|
+
}
|
|
11920
12797
|
const stateFile2 = join(SVAMP_HOME, "daemon.state.json");
|
|
11921
12798
|
for (let i = 0; i < 100; i++) {
|
|
11922
12799
|
await new Promise((r) => setTimeout(r, 100));
|
|
@@ -11999,10 +12876,14 @@ function daemonStatus() {
|
|
|
11999
12876
|
|
|
12000
12877
|
var run = /*#__PURE__*/Object.freeze({
|
|
12001
12878
|
__proto__: null,
|
|
12879
|
+
DAEMON_STOP_MARKER_FILE: DAEMON_STOP_MARKER_FILE,
|
|
12880
|
+
clearStopMarker: clearStopMarker,
|
|
12002
12881
|
daemonStatus: daemonStatus,
|
|
12003
12882
|
restartDaemon: restartDaemon,
|
|
12004
12883
|
startDaemon: startDaemon,
|
|
12005
|
-
stopDaemon: stopDaemon
|
|
12884
|
+
stopDaemon: stopDaemon,
|
|
12885
|
+
stopMarkerExists: stopMarkerExists,
|
|
12886
|
+
writeStopMarker: writeStopMarker
|
|
12006
12887
|
});
|
|
12007
12888
|
|
|
12008
|
-
export {
|
|
12889
|
+
export { normalizeAllowedUser as A, loadSecurityContextConfig as B, resolveSecurityContext as C, buildSecurityContextFromFlags as D, mergeSecurityContexts as E, buildSessionShareUrl as F, buildMachineShareUrl as G, generateHookSettings as H, DefaultTransport$1 as I, acpBackend as J, acpAgentConfig as K, codexMcpBackend as L, GeminiTransport$1 as M, claudeAuth as N, instanceConfig as O, api as P, run as Q, RoutineStore as R, ServeAuth as S, registerSessionService as a, stopDaemon as b, connectToHypha as c, daemonStatus as d, clearStopMarker as e, stopMarkerExists as f, getHyphaServerUrl$1 as g, getFrpsSubdomainHost as h, getFrpsServerPort as i, getFrpsServerAddr as j, getHyphaServerUrl as k, hasCookieToken as l, RoutineRunner as m, getSkillsServer as n, getSkillsWorkspaceName as o, parseFrontmatter as p, getSkillsCollectionName as q, registerMachineService as r, startDaemon as s, fetchWithTimeout as t, searchSkills as u, SKILLS_DIR as v, getSkillInfo as w, downloadSkillFile as x, listSkillFiles as y, resolveModel as z };
|