svamp-cli 0.2.87 → 0.2.90
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-CPV2AS4T.mjs → agentCommands-BjhBmutV.mjs} +2 -2
- package/dist/auth-LB4hwDfW.mjs +83 -0
- package/dist/cli.mjs +204 -81
- package/dist/{commands-CXEpyNro.mjs → commands-Bg7NZ6PK.mjs} +2 -1
- package/dist/{commands-CtCjHkST.mjs → commands-C-5cSioE.mjs} +5 -5
- package/dist/{commands-BPR-KpZM.mjs → commands-XrfOgVzR.mjs} +3 -2
- package/dist/{commands-Dbm3UrJS.mjs → commands-mlxYf-v-.mjs} +159 -8
- package/dist/{commands-Dc5kN_0c.mjs → commands-sQMe36dC.mjs} +2 -1
- package/dist/{fleet-CEM8CAZN.mjs → fleet-Dts-Z6LV.mjs} +2 -1
- package/dist/{frpc-CJJTmC9m.mjs → frpc-DTA0--Z7.mjs} +2 -1
- package/dist/httpServer-wwHHk1EM.mjs +101 -0
- package/dist/index.mjs +2 -1
- package/dist/{package-BiX-a9rI.mjs → package-DR--K5aO.mjs} +2 -2
- package/dist/{run-BV4hWpyA.mjs → run-1BK4-WPo.mjs} +980 -44
- package/dist/{run-3ndxhd-X.mjs → run-DrC7KnK3.mjs} +2 -1
- package/dist/{serveCommands-ClcajS1I.mjs → serveCommands-BXn2hnJ0.mjs} +5 -5
- package/dist/{serveManager-tAOF25rN.mjs → serveManager-Cd2gJI7J.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-DTA0--Z7.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,8 +1958,654 @@ 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
|
+
try {
|
|
1997
|
+
mkdirSync$1(this.dir, { recursive: true });
|
|
1998
|
+
} catch {
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
_path(id) {
|
|
2002
|
+
return join$1(this.dir, `${id}.json`);
|
|
2003
|
+
}
|
|
2004
|
+
list() {
|
|
2005
|
+
if (!existsSync(this.dir)) return [];
|
|
2006
|
+
return readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
2007
|
+
try {
|
|
2008
|
+
return JSON.parse(readFileSync(join$1(this.dir, f), "utf8"));
|
|
2009
|
+
} catch {
|
|
2010
|
+
return null;
|
|
2011
|
+
}
|
|
2012
|
+
}).filter((c) => !!c);
|
|
2013
|
+
}
|
|
2014
|
+
get(id) {
|
|
2015
|
+
try {
|
|
2016
|
+
return JSON.parse(readFileSync(this._path(id), "utf8"));
|
|
2017
|
+
} catch {
|
|
2018
|
+
return null;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
save(channel) {
|
|
2022
|
+
const c = { enabled: true, bind: "active", template: DEFAULT_TEMPLATE, last_calls: [], ...channel };
|
|
2023
|
+
if (!c.id) c.id = genId();
|
|
2024
|
+
const errs = validateChannel(c);
|
|
2025
|
+
if (errs.length) throw new Error("invalid channel: " + errs.join("; "));
|
|
2026
|
+
mkdirSync$1(this.dir, { recursive: true });
|
|
2027
|
+
const tmp = this._path(c.id) + ".tmp";
|
|
2028
|
+
writeFileSync$1(tmp, JSON.stringify(c, null, 2));
|
|
2029
|
+
renameSync$1(tmp, this._path(c.id));
|
|
2030
|
+
return c;
|
|
2031
|
+
}
|
|
2032
|
+
remove(id) {
|
|
2033
|
+
const p = this._path(id);
|
|
2034
|
+
if (existsSync(p)) {
|
|
2035
|
+
rmSync(p);
|
|
2036
|
+
return true;
|
|
2037
|
+
}
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
setEnabled(id, enabled) {
|
|
2041
|
+
const c = this.get(id);
|
|
2042
|
+
if (!c) return null;
|
|
2043
|
+
c.enabled = enabled;
|
|
2044
|
+
return this.save(c);
|
|
2045
|
+
}
|
|
2046
|
+
recordCall(id, entry) {
|
|
2047
|
+
const c = this.get(id);
|
|
2048
|
+
if (!c) return;
|
|
2049
|
+
c.last_calls = c.last_calls || [];
|
|
2050
|
+
c.last_calls.unshift({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
2051
|
+
c.last_calls = c.last_calls.slice(0, 20);
|
|
2052
|
+
this.save(c);
|
|
2053
|
+
}
|
|
2054
|
+
addCaller(id, name, kind = "agent") {
|
|
2055
|
+
const c = this.get(id);
|
|
2056
|
+
if (!c) return null;
|
|
2057
|
+
c.identity.callers = c.identity.callers || [];
|
|
2058
|
+
const caller = { name, kind, key: genKey() };
|
|
2059
|
+
c.identity.callers.push(caller);
|
|
2060
|
+
this.save(c);
|
|
2061
|
+
return caller;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
function generateSkillBody(channel, urlBase) {
|
|
2065
|
+
const url = `${"https://<svamp-tunnel>"}/channel/${channel.id}`;
|
|
2066
|
+
const isAgent = channel.action?.kind === "agent";
|
|
2067
|
+
const replyNote = isAgent ? `
|
|
2068
|
+
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\`.` : `
|
|
2069
|
+
Delivery is fire-and-forget; the message lands in the agent's inbox tagged with your identity (from/verified).`;
|
|
2070
|
+
return `---
|
|
2071
|
+
name: ${channel.skill?.name || channel.name}
|
|
2072
|
+
description: ${channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`}
|
|
2073
|
+
---
|
|
2074
|
+
# ${channel.name}
|
|
2075
|
+
${channel.description || ""}
|
|
2076
|
+
|
|
2077
|
+
Self-contained guide for messaging another agent \u2014 share this (or its URL,
|
|
2078
|
+
${url}/skill.md) with an agent so it knows how to reach me.
|
|
2079
|
+
${replyNote}
|
|
2080
|
+
|
|
2081
|
+
Hypha RPC (preferred, verified identity, no key): get_service("<ws>/<machine>:channels").send({ channel: "${channel.id}", message: "..." })
|
|
2082
|
+
HTTP: POST ${url} with header Authorization: Bearer <your-key>, body { "message": "...", "from": "..." }`;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function resolveSender(channel, input = {}) {
|
|
2086
|
+
const { key, from, hyphaUser, hyphaWorkspace, hyphaAnonymous } = input;
|
|
2087
|
+
const id = channel.identity || {};
|
|
2088
|
+
if (hyphaUser && !hyphaAnonymous && Array.isArray(id.hypha_allow) && id.hypha_allow.length) {
|
|
2089
|
+
if (id.hypha_allow.includes("*") || id.hypha_allow.includes(hyphaUser) || hyphaWorkspace && id.hypha_allow.includes(hyphaWorkspace))
|
|
2090
|
+
return { sender: { name: hyphaUser, kind: "agent", verified: true } };
|
|
2091
|
+
return { error: "caller not in hypha_allow" };
|
|
2092
|
+
}
|
|
2093
|
+
if (id.mode === "fixed") {
|
|
2094
|
+
if (!id.fixed?.name) return { error: "fixed identity not configured" };
|
|
2095
|
+
return { sender: { name: id.fixed.name, kind: id.fixed.kind, verified: true } };
|
|
2096
|
+
}
|
|
2097
|
+
if (id.mode === "per-key") {
|
|
2098
|
+
const caller = (id.callers || []).find((c) => c.key && c.key === key);
|
|
2099
|
+
if (!caller) return { error: "invalid or missing key" };
|
|
2100
|
+
return { sender: { name: caller.name, kind: caller.kind, verified: true } };
|
|
2101
|
+
}
|
|
2102
|
+
if (id.mode === "caller-supplied") {
|
|
2103
|
+
if (id.shared_key && key !== id.shared_key) return { error: "invalid key" };
|
|
2104
|
+
return { sender: { name: from || "anonymous", kind: "user", verified: false } };
|
|
2105
|
+
}
|
|
2106
|
+
return { error: "unsupported identity mode" };
|
|
2107
|
+
}
|
|
2108
|
+
const MAX_BODY = 16 * 1024;
|
|
2109
|
+
function xmlEscape(s) {
|
|
2110
|
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2111
|
+
}
|
|
2112
|
+
const stripControl = (s) => String(s ?? "").replace(/[\x00-\x1f\x7f]/g, " ");
|
|
2113
|
+
function renderMessage(channel, { sender = {}, body = {}, query = {}, callId, now }) {
|
|
2114
|
+
const obj = (v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v;
|
|
2115
|
+
const escVal = (v) => xmlEscape(obj(v));
|
|
2116
|
+
const escAttr = (v) => xmlEscape(stripControl(obj(v)));
|
|
2117
|
+
const bodyEsc = {};
|
|
2118
|
+
for (const [k, v] of Object.entries(body)) bodyEsc[k] = k === "message" ? escVal(String(v ?? "").slice(0, MAX_BODY)) : escAttr(v);
|
|
2119
|
+
const queryEsc = {};
|
|
2120
|
+
for (const [k, v] of Object.entries(query)) if (k !== "key") queryEsc[k] = escAttr(v);
|
|
2121
|
+
const ctx = {
|
|
2122
|
+
sender: { name: escAttr(sender.name), kind: escAttr(sender.kind), verified: sender.verified === true },
|
|
2123
|
+
body: bodyEsc,
|
|
2124
|
+
query: queryEsc,
|
|
2125
|
+
channel: { name: escAttr(channel.name), id: escAttr(channel.id) },
|
|
2126
|
+
call: { id: escAttr(callId) },
|
|
2127
|
+
now: escAttr(now || (/* @__PURE__ */ new Date()).toISOString())
|
|
2128
|
+
};
|
|
2129
|
+
return renderTemplate(channel.template || DEFAULT_TEMPLATE, ctx);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
const SKILL_START = "<<<WISE_SKILL ";
|
|
2133
|
+
const SKILL_END = "<<<END_WISE_SKILL>>>";
|
|
2134
|
+
function buildSkillsScanCommand() {
|
|
2135
|
+
return [
|
|
2136
|
+
'for d in "$HOME/.svamp/skills" ".svamp/skills"; do',
|
|
2137
|
+
' for f in "$d"/*/SKILL.md; do',
|
|
2138
|
+
' [ -f "$f" ] || continue;',
|
|
2139
|
+
` printf '${SKILL_START}%s>>>\\n' "$f";`,
|
|
2140
|
+
' cat "$f";',
|
|
2141
|
+
` printf '\\n${SKILL_END}\\n';`,
|
|
2142
|
+
" done;",
|
|
2143
|
+
"done"
|
|
2144
|
+
].join("\n");
|
|
2145
|
+
}
|
|
2146
|
+
function parseWiseMd(raw) {
|
|
2147
|
+
const m = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
2148
|
+
if (!m) return { body: raw.trim() };
|
|
2149
|
+
return { body: m[2].trim() };
|
|
2150
|
+
}
|
|
2151
|
+
function parseSkillFrontmatter(md) {
|
|
2152
|
+
const m = md.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
2153
|
+
if (!m) return { name: "", description: "", body: md.trim() };
|
|
2154
|
+
const fm = m[1];
|
|
2155
|
+
const body = m[2].trim();
|
|
2156
|
+
const field = (key) => {
|
|
2157
|
+
const fmatch = fm.match(new RegExp(`^${key}\\s*:\\s*(.+)$`, "m"));
|
|
2158
|
+
return fmatch ? fmatch[1].trim().replace(/^["']|["']$/g, "").trim() : "";
|
|
2159
|
+
};
|
|
2160
|
+
return { name: field("name"), description: field("description"), body };
|
|
2161
|
+
}
|
|
2162
|
+
function parseSkillsDump(dump) {
|
|
2163
|
+
if (!dump?.trim()) return [];
|
|
2164
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2165
|
+
const blocks = dump.split(SKILL_START).slice(1);
|
|
2166
|
+
for (const block of blocks) {
|
|
2167
|
+
const headerEnd = block.indexOf(">>>\n");
|
|
2168
|
+
if (headerEnd === -1) continue;
|
|
2169
|
+
const path = block.slice(0, headerEnd).trim();
|
|
2170
|
+
const rest = block.slice(headerEnd + 4);
|
|
2171
|
+
const endIdx = rest.indexOf(SKILL_END);
|
|
2172
|
+
const md = (endIdx === -1 ? rest : rest.slice(0, endIdx)).replace(/\n$/, "");
|
|
2173
|
+
const { name, description, body } = parseSkillFrontmatter(md);
|
|
2174
|
+
const skillName = name || path.split("/").slice(-2, -1)[0] || path;
|
|
2175
|
+
if (!description && !body) continue;
|
|
2176
|
+
byName.set(skillName, { name: skillName, description, path, body });
|
|
2177
|
+
}
|
|
2178
|
+
return Array.from(byName.values());
|
|
2179
|
+
}
|
|
2180
|
+
function findSkill(skills, name) {
|
|
2181
|
+
const n = (name || "").trim().toLowerCase();
|
|
2182
|
+
if (!n) return void 0;
|
|
2183
|
+
return skills.find((s) => s.name.toLowerCase() === n) ?? skills.find((s) => s.name.toLowerCase().includes(n));
|
|
2184
|
+
}
|
|
2185
|
+
function formatSkillIndex(skills, maxDescLen = 140) {
|
|
2186
|
+
if (skills.length === 0) return "";
|
|
2187
|
+
return skills.map((s) => {
|
|
2188
|
+
const d = s.description.length > maxDescLen ? s.description.slice(0, maxDescLen - 1) + "\u2026" : s.description;
|
|
2189
|
+
return `- ${s.name} \u2014 ${d || "(no description)"}`;
|
|
2190
|
+
}).join("\n");
|
|
2191
|
+
}
|
|
2192
|
+
function buildSkillsPromptSection(skills) {
|
|
2193
|
+
if (skills.length === 0) return "";
|
|
2194
|
+
return [
|
|
2195
|
+
"## Skills (load on demand \u2014 do NOT assume the steps)",
|
|
2196
|
+
"These are named procedures for this project. Only their names + descriptions are shown.",
|
|
2197
|
+
'When a request matches one, call use_skill("name") to load its full steps, then carry them',
|
|
2198
|
+
"out with run_bash. Skip skills you don't need.",
|
|
2199
|
+
"",
|
|
2200
|
+
"Available skills:",
|
|
2201
|
+
formatSkillIndex(skills)
|
|
2202
|
+
].join("\n");
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
function buildSvampApi(deps) {
|
|
2206
|
+
return {
|
|
2207
|
+
bash: (command, opts) => deps.runBash(command, opts),
|
|
2208
|
+
context: () => deps.getContext(),
|
|
2209
|
+
ask: (question) => deps.askSession(question),
|
|
2210
|
+
summarize: (question) => deps.summarizeSession(question),
|
|
2211
|
+
send: (message) => deps.sessionSend(message)
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
async function runJs(code, deps, opts = {}) {
|
|
2215
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
2216
|
+
const logs = [];
|
|
2217
|
+
const capture = (...a) => {
|
|
2218
|
+
logs.push(a.map((x) => typeof x === "string" ? x : JSON.stringify(x)).join(" "));
|
|
2219
|
+
};
|
|
2220
|
+
const sandbox = /* @__PURE__ */ Object.create(null);
|
|
2221
|
+
sandbox.svamp = buildSvampApi(deps);
|
|
2222
|
+
sandbox.console = { log: capture, error: capture, warn: capture, info: capture };
|
|
2223
|
+
const context = vm.createContext(sandbox, { name: "wise-run_js" });
|
|
2224
|
+
const wrapped = `(async () => {
|
|
2225
|
+
${code}
|
|
2226
|
+
})()`;
|
|
2227
|
+
let script;
|
|
2228
|
+
try {
|
|
2229
|
+
script = new vm.Script(wrapped, { filename: "wise-run_js.js" });
|
|
2230
|
+
} catch (e) {
|
|
2231
|
+
return { result: void 0, logs, error: "compile error: " + e.message };
|
|
2232
|
+
}
|
|
2233
|
+
let timer;
|
|
2234
|
+
try {
|
|
2235
|
+
const ran = script.runInContext(context, { timeout: timeoutMs });
|
|
2236
|
+
const result = await Promise.race([
|
|
2237
|
+
Promise.resolve(ran),
|
|
2238
|
+
new Promise((_, reject) => {
|
|
2239
|
+
timer = setTimeout(() => reject(new Error("run_js timed out")), timeoutMs);
|
|
2240
|
+
})
|
|
2241
|
+
]);
|
|
2242
|
+
return { result, logs };
|
|
2243
|
+
} catch (e) {
|
|
2244
|
+
return { result: void 0, logs, error: e.message };
|
|
2245
|
+
} finally {
|
|
2246
|
+
if (timer) clearTimeout(timer);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
const READ_ONLY_TOOLS = ["get_context", "ask_session", "summarize_session", "use_skill"];
|
|
2251
|
+
const str = (v) => v == null ? "" : String(v);
|
|
2252
|
+
function buildTools(deps, skills) {
|
|
2253
|
+
return [
|
|
2254
|
+
{
|
|
2255
|
+
name: "get_context",
|
|
2256
|
+
readOnly: true,
|
|
2257
|
+
description: 'Orient: status and recent activity of the bound session/machine. Call first for "what is happening / its status" questions.',
|
|
2258
|
+
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
2259
|
+
run: async () => JSON.stringify(await deps.getContext())
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
name: "ask_session",
|
|
2263
|
+
readOnly: true,
|
|
2264
|
+
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.",
|
|
2265
|
+
parameters: { type: "object", properties: { question: { type: "string", description: "The question to ask." } }, required: ["question"], additionalProperties: false },
|
|
2266
|
+
run: async (a) => deps.askSession(str(a?.question))
|
|
2267
|
+
},
|
|
2268
|
+
{
|
|
2269
|
+
name: "summarize_session",
|
|
2270
|
+
readOnly: true,
|
|
2271
|
+
description: "Get a short summary of the deep agent's transcript answering a specific question. Keeps long history out of your context.",
|
|
2272
|
+
parameters: { type: "object", properties: { question: { type: "string", description: "What to summarize / find out." } }, required: ["question"], additionalProperties: false },
|
|
2273
|
+
run: async (a) => deps.summarizeSession(str(a?.question))
|
|
2274
|
+
},
|
|
2275
|
+
{
|
|
2276
|
+
name: "use_skill",
|
|
2277
|
+
readOnly: true,
|
|
2278
|
+
description: "Load the full steps of a project skill from the Skills index by name (progressive disclosure), then carry them out.",
|
|
2279
|
+
parameters: { type: "object", properties: { name: { type: "string", description: "Skill name from the Skills index." } }, required: ["name"], additionalProperties: false },
|
|
2280
|
+
run: async (a) => {
|
|
2281
|
+
const s = findSkill(skills, str(a?.name));
|
|
2282
|
+
return s ? s.body || `(skill "${s.name}" has no body)` : `No skill named "${str(a?.name)}" in the index.`;
|
|
2283
|
+
}
|
|
2284
|
+
},
|
|
2285
|
+
{
|
|
2286
|
+
name: "run_bash",
|
|
2287
|
+
readOnly: false,
|
|
2288
|
+
description: "Run a shell command on the bound session's machine. Returns stdout/stderr/exit code. Use for quick reads and short tasks.",
|
|
2289
|
+
parameters: { type: "object", properties: { command: { type: "string", description: "The shell command." }, cwd: { type: "string", description: "Optional working directory." } }, required: ["command"], additionalProperties: false },
|
|
2290
|
+
run: async (a) => JSON.stringify(await deps.runBash(str(a?.command), { cwd: a?.cwd ? str(a.cwd) : void 0 }))
|
|
2291
|
+
},
|
|
2292
|
+
{
|
|
2293
|
+
name: "send_to_session",
|
|
2294
|
+
readOnly: false,
|
|
2295
|
+
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.",
|
|
2296
|
+
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 },
|
|
2297
|
+
run: async (a) => {
|
|
2298
|
+
await deps.sessionSend(str(a?.message));
|
|
2299
|
+
if (a?.wait && deps.waitForSessionTurn) {
|
|
2300
|
+
const r = await deps.waitForSessionTurn();
|
|
2301
|
+
return r.completed ? r.reply : "(sent; the agent's turn did not finish before timeout)";
|
|
2302
|
+
}
|
|
2303
|
+
return "(sent to the coding agent)";
|
|
2304
|
+
}
|
|
2305
|
+
},
|
|
2306
|
+
{
|
|
2307
|
+
name: "run_js",
|
|
2308
|
+
readOnly: false,
|
|
2309
|
+
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.",
|
|
2310
|
+
parameters: { type: "object", properties: { code: { type: "string", description: "JS body; may use await and return." } }, required: ["code"], additionalProperties: false },
|
|
2311
|
+
run: async (a) => {
|
|
2312
|
+
const r = await runJs(str(a?.code), deps, { timeoutMs: 5e3 });
|
|
2313
|
+
return JSON.stringify({ result: r.result, logs: r.logs, error: r.error });
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
];
|
|
2317
|
+
}
|
|
2318
|
+
function gateTools(all, config, senderName) {
|
|
2319
|
+
const per = config?.per_caller?.[senderName]?.tools;
|
|
2320
|
+
const allow = per ?? config?.tools ?? READ_ONLY_TOOLS;
|
|
2321
|
+
const set = new Set(allow);
|
|
2322
|
+
return all.filter((t) => set.has(t.name));
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
async function loadWiseAgentContext(deps, config) {
|
|
2326
|
+
const cwd = deps.cwd || "~";
|
|
2327
|
+
let projectInstructions = "";
|
|
2328
|
+
try {
|
|
2329
|
+
const r = await deps.runBash('cat WISE.md "$HOME/.svamp/wise.md" .svamp/wise.md 2>/dev/null || true', { cwd });
|
|
2330
|
+
projectInstructions = parseWiseMd((r.stdout || "").trim()).body;
|
|
2331
|
+
} catch {
|
|
2332
|
+
}
|
|
2333
|
+
let skills = [];
|
|
2334
|
+
try {
|
|
2335
|
+
const r = await deps.runBash(buildSkillsScanCommand(), { cwd });
|
|
2336
|
+
skills = parseSkillsDump(r.stdout || "");
|
|
2337
|
+
} catch {
|
|
2338
|
+
}
|
|
2339
|
+
if (config?.skills && config.skills.length) {
|
|
2340
|
+
const allow = new Set(config.skills.map((s) => s.toLowerCase()));
|
|
2341
|
+
skills = skills.filter((s) => allow.has(s.name.toLowerCase()));
|
|
2342
|
+
}
|
|
2343
|
+
return { projectInstructions, skills };
|
|
2344
|
+
}
|
|
2345
|
+
function buildWiseAgentInstructions(ctx, config) {
|
|
2346
|
+
const base = `# Role & Objective
|
|
2347
|
+
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.
|
|
2348
|
+
|
|
2349
|
+
# Personality & Tone
|
|
2350
|
+
- A capable, hands-on colleague \u2014 direct, concise, no preamble or narration.
|
|
2351
|
+
- Keep replies to 1\u20133 short sentences. Summarize results; never paste raw JSON, logs, or long output back.
|
|
2352
|
+
- Act now; don't promise to "check back later." No time estimates.
|
|
2353
|
+
|
|
2354
|
+
# Context
|
|
2355
|
+
- "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.
|
|
2356
|
+
- Your tools act on the bound session and its machine.
|
|
2357
|
+
- 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.
|
|
2358
|
+
|
|
2359
|
+
# Tools
|
|
2360
|
+
- get_context \u2014 status + recent activity of the session. Use first for "what's happening / its status".
|
|
2361
|
+
- ask_session \u2014 ask the deep coding agent a read-only question about its own work (slower).
|
|
2362
|
+
- summarize_session \u2014 a cheap subagent that summarizes the deep agent's transcript for a specific question.
|
|
2363
|
+
- use_skill \u2014 load a project skill's full steps by name, then carry them out with run_bash.
|
|
2364
|
+
- run_bash \u2014 run a shell command on the session's machine (when granted).
|
|
2365
|
+
- run_js \u2014 JavaScript with an async \`svamp\` API to compose several steps in one call (when granted).
|
|
2366
|
+
- send_to_session \u2014 hand a clear, reformulated instruction to the deep coding agent (when granted); pass wait=true to block for its reply.
|
|
2367
|
+
|
|
2368
|
+
# Instructions
|
|
2369
|
+
- Answer general questions and questions about yourself directly. Use tools only to act on the machine/session.
|
|
2370
|
+
- Take the cheap path: read state directly; delegate anything LONG to summarize_session \u2014 keep your own context small.
|
|
2371
|
+
- For destructive actions (deleting, stopping, killing), require a verified caller and confirm intent; for safe reads, just do it.
|
|
2372
|
+
- If a tool fails or returns nothing useful, say so plainly \u2014 never fabricate a result.
|
|
2373
|
+
- Report the outcome in one line.`;
|
|
2374
|
+
const parts = [base];
|
|
2375
|
+
const custom = ctx.projectInstructions?.trim();
|
|
2376
|
+
if (custom) parts.push(`# Project notes (WISE.md)
|
|
2377
|
+
${custom}`);
|
|
2378
|
+
const extra = config?.system?.trim();
|
|
2379
|
+
if (extra) parts.push(`# Channel brief
|
|
2380
|
+
${extra}`);
|
|
2381
|
+
const skillsSection = buildSkillsPromptSection(ctx.skills);
|
|
2382
|
+
if (skillsSection) parts.push(skillsSection);
|
|
2383
|
+
return parts.join("\n\n");
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
const DEFAULTS = {
|
|
2387
|
+
// Fast, cheap default — the snappy companion tier.
|
|
2388
|
+
"openai": { baseUrl: "https://api.openai.com", model: "gpt-5-mini" },
|
|
2389
|
+
// Quota-governed gateway. NOTE: the Hypha proxy is Anthropic-shaped today; an
|
|
2390
|
+
// OpenAI-compatible route may be required for non-Claude models (docs §6).
|
|
2391
|
+
"hypha-proxy": { baseUrl: "", model: "gpt-5-mini" },
|
|
2392
|
+
// Fallback when no OpenAI access — Claude Haiku via the same proxy path.
|
|
2393
|
+
"claude-haiku": { baseUrl: "", model: "claude-haiku-4-5" }
|
|
2394
|
+
};
|
|
2395
|
+
const DEFAULT_OPENAI_BASE = "https://api.openai.com";
|
|
2396
|
+
function resolveModel(config, env) {
|
|
2397
|
+
const provider = config?.provider || env.WISE_AGENT_PROVIDER || "openai";
|
|
2398
|
+
const d = DEFAULTS[provider] || DEFAULTS.openai;
|
|
2399
|
+
const model = config?.model || env.WISE_AGENT_MODEL || d.model;
|
|
2400
|
+
let baseUrl = env.WISE_AGENT_BASE_URL || "";
|
|
2401
|
+
let apiKey = env.WISE_AGENT_API_KEY || "";
|
|
2402
|
+
if (provider === "openai") {
|
|
2403
|
+
baseUrl = baseUrl || env.OPENAI_BASE_URL || env.OPENAI_API_BASE || d.baseUrl;
|
|
2404
|
+
apiKey = apiKey || env.OPENAI_API_KEY || "";
|
|
2405
|
+
} else if (provider === "hypha-proxy" || provider === "claude-haiku") {
|
|
2406
|
+
baseUrl = baseUrl || (env.SVAMP_HYPHA_PROXY_URL || "").replace(/\/+$/, "");
|
|
2407
|
+
apiKey = apiKey || env.HYPHA_TOKEN || "";
|
|
2408
|
+
} else {
|
|
2409
|
+
baseUrl = baseUrl || d.baseUrl;
|
|
2410
|
+
}
|
|
2411
|
+
baseUrl = baseUrl.replace(/\/v1\/?$/, "");
|
|
2412
|
+
return { provider, baseUrl, apiKey, model };
|
|
2413
|
+
}
|
|
2414
|
+
function describeMisconfiguration(resolved) {
|
|
2415
|
+
const { provider, baseUrl, apiKey } = resolved;
|
|
2416
|
+
if (!baseUrl) {
|
|
2417
|
+
if (provider === "hypha-proxy" || provider === "claude-haiku") {
|
|
2418
|
+
return `WISE Agent (${provider}) is not configured: no proxy base URL. Run \`svamp wise-agent auth use-hypha-proxy <URL>\` (or set SVAMP_HYPHA_PROXY_URL), then \`svamp daemon restart\`.`;
|
|
2419
|
+
}
|
|
2420
|
+
return "WISE Agent is not configured: no base URL. Run `svamp wise-agent auth set <URL> <KEY>` (or set WISE_AGENT_BASE_URL / OPENAI_BASE_URL), then `svamp daemon restart`.";
|
|
2421
|
+
}
|
|
2422
|
+
if (provider === "hypha-proxy" || provider === "claude-haiku") {
|
|
2423
|
+
if (!apiKey) return `WISE Agent (${provider}) is not configured: no auth token. Ensure HYPHA_TOKEN is set (\`svamp login\`), then \`svamp daemon restart\`.`;
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
if (!apiKey && baseUrl.replace(/\/v1\/?$/, "") === DEFAULT_OPENAI_BASE) {
|
|
2427
|
+
return "WISE Agent is not configured: no OpenAI API key. Run `svamp wise-agent auth use-openai <KEY>` (or set OPENAI_API_KEY / WISE_AGENT_API_KEY), then `svamp daemon restart`.";
|
|
2428
|
+
}
|
|
2429
|
+
return null;
|
|
2430
|
+
}
|
|
2431
|
+
function makeHttpTransport(resolved, fetchImpl = fetch) {
|
|
2432
|
+
return async (req) => {
|
|
2433
|
+
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)`);
|
|
2434
|
+
const url = `${resolved.baseUrl}/v1/chat/completions`;
|
|
2435
|
+
const res = await fetchImpl(url, {
|
|
2436
|
+
method: "POST",
|
|
2437
|
+
headers: {
|
|
2438
|
+
"content-type": "application/json",
|
|
2439
|
+
...resolved.apiKey ? { authorization: `Bearer ${resolved.apiKey}` } : {}
|
|
2440
|
+
},
|
|
2441
|
+
body: JSON.stringify({ ...req, model: req.model || resolved.model })
|
|
2442
|
+
});
|
|
2443
|
+
if (!res.ok) {
|
|
2444
|
+
const text = await res.text().catch(() => "");
|
|
2445
|
+
throw new Error(`WISE Agent chat ${res.status}: ${text.slice(0, 300)}`);
|
|
2446
|
+
}
|
|
2447
|
+
return await res.json();
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
const DEFAULT_TIMEOUT_SEC = 30;
|
|
2452
|
+
const DEFAULT_MAX_STEPS = 6;
|
|
2453
|
+
function toChatTools(tools) {
|
|
2454
|
+
return tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
2455
|
+
}
|
|
2456
|
+
const addUsage = (a, b) => ({
|
|
2457
|
+
prompt_tokens: (a?.prompt_tokens || 0) + (b?.prompt_tokens || 0),
|
|
2458
|
+
completion_tokens: (a?.completion_tokens || 0) + (b?.completion_tokens || 0),
|
|
2459
|
+
total_tokens: (a?.total_tokens || 0) + (b?.total_tokens || 0)
|
|
2460
|
+
});
|
|
2461
|
+
async function runWiseAgent(args) {
|
|
2462
|
+
const { message, sender, config, deps, transport, model } = args;
|
|
2463
|
+
const timeoutMs = (config?.timeout_sec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
|
|
2464
|
+
const maxSteps = config?.max_steps ?? DEFAULT_MAX_STEPS;
|
|
2465
|
+
const toolCalls = [];
|
|
2466
|
+
let usage;
|
|
2467
|
+
const turn = async () => {
|
|
2468
|
+
const ctx = await loadWiseAgentContext(deps, config);
|
|
2469
|
+
const system = buildWiseAgentInstructions(ctx, config);
|
|
2470
|
+
const allTools = buildTools(deps, ctx.skills);
|
|
2471
|
+
const granted = gateTools(allTools, config, sender.name);
|
|
2472
|
+
const byName = new Map(granted.map((t) => [t.name, t]));
|
|
2473
|
+
const chatTools = toChatTools(granted);
|
|
2474
|
+
const messages = [
|
|
2475
|
+
{ role: "system", content: system },
|
|
2476
|
+
{ role: "user", content: message }
|
|
2477
|
+
];
|
|
2478
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
2479
|
+
const res2 = await transport({
|
|
2480
|
+
model,
|
|
2481
|
+
messages,
|
|
2482
|
+
...chatTools.length ? { tools: chatTools, tool_choice: "auto" } : {}
|
|
2483
|
+
});
|
|
2484
|
+
if (res2.usage) usage = addUsage(usage, res2.usage);
|
|
2485
|
+
const choice = res2.choices?.[0]?.message;
|
|
2486
|
+
if (!choice) return { status: "error", reply: "", toolCalls, usage, error: "empty model response" };
|
|
2487
|
+
const calls = choice.tool_calls || [];
|
|
2488
|
+
if (calls.length === 0) {
|
|
2489
|
+
return { status: "completed", reply: (choice.content || "").trim(), toolCalls, usage };
|
|
2490
|
+
}
|
|
2491
|
+
messages.push({ role: "assistant", content: choice.content ?? "", tool_calls: calls });
|
|
2492
|
+
for (const call of calls) {
|
|
2493
|
+
let args2 = {};
|
|
2494
|
+
try {
|
|
2495
|
+
args2 = call.function.arguments ? JSON.parse(call.function.arguments) : {};
|
|
2496
|
+
} catch {
|
|
2497
|
+
}
|
|
2498
|
+
const tool = byName.get(call.function.name);
|
|
2499
|
+
let result;
|
|
2500
|
+
let error;
|
|
2501
|
+
if (!tool) {
|
|
2502
|
+
error = `tool "${call.function.name}" is not available to this caller`;
|
|
2503
|
+
result = error;
|
|
2504
|
+
} else {
|
|
2505
|
+
try {
|
|
2506
|
+
result = await tool.run(args2);
|
|
2507
|
+
} catch (e) {
|
|
2508
|
+
error = String(e?.message || e);
|
|
2509
|
+
result = `error: ${error}`;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
toolCalls.push({ name: call.function.name, args: args2, result: error ? void 0 : result, error });
|
|
2513
|
+
messages.push({ role: "tool", tool_call_id: call.id, name: call.function.name, content: result });
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
const res = await transport({ model, messages, tool_choice: "none" });
|
|
2517
|
+
if (res.usage) usage = addUsage(usage, res.usage);
|
|
2518
|
+
const final = res.choices?.[0]?.message?.content || "";
|
|
2519
|
+
return { status: "completed", reply: final.trim() || "(reached step limit without a final answer)", toolCalls, usage };
|
|
2520
|
+
};
|
|
2521
|
+
let timer;
|
|
2522
|
+
const timeout = new Promise((resolve) => {
|
|
2523
|
+
timer = setTimeout(() => resolve({ status: "timeout", reply: "(timed out)", toolCalls, usage }), timeoutMs);
|
|
2524
|
+
});
|
|
2525
|
+
try {
|
|
2526
|
+
return await Promise.race([turn(), timeout]);
|
|
2527
|
+
} catch (e) {
|
|
2528
|
+
return { status: "error", reply: "", toolCalls, usage, error: String(e?.message || e) };
|
|
2529
|
+
} finally {
|
|
2530
|
+
if (timer) clearTimeout(timer);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
let _testTransport;
|
|
2535
|
+
async function dispatchAgentChannel(a) {
|
|
2536
|
+
const config = a.channel.action?.kind === "agent" ? a.channel.action.agent || {} : {};
|
|
2537
|
+
const resolved = resolveModel(config, a.env);
|
|
2538
|
+
if (!a.transport && !_testTransport) {
|
|
2539
|
+
const err = describeMisconfiguration(resolved);
|
|
2540
|
+
if (err) return { status: "error", reply: "", toolCalls: [], error: err };
|
|
2541
|
+
}
|
|
2542
|
+
const transport = a.transport || _testTransport || makeHttpTransport(resolved);
|
|
2543
|
+
return runWiseAgent({
|
|
2544
|
+
message: a.message,
|
|
2545
|
+
sender: a.sender,
|
|
2546
|
+
config,
|
|
2547
|
+
deps: a.deps,
|
|
2548
|
+
transport,
|
|
2549
|
+
model: resolved.model
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
function textFrom(r) {
|
|
2554
|
+
if (r == null) return "";
|
|
2555
|
+
if (typeof r === "string") return r;
|
|
2556
|
+
if (r.success === false) return `error: ${r.error || "failed"}`;
|
|
2557
|
+
return String(r.result ?? r.answer ?? r.text ?? r.response ?? r.stdout ?? JSON.stringify(r));
|
|
2558
|
+
}
|
|
2559
|
+
function normalizeBash(r) {
|
|
2560
|
+
if (r && typeof r === "object") {
|
|
2561
|
+
return {
|
|
2562
|
+
stdout: String(r.stdout ?? ""),
|
|
2563
|
+
stderr: String(r.stderr ?? ""),
|
|
2564
|
+
exitCode: Number(r.exitCode ?? (r.success === false ? 1 : 0))
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
return { stdout: String(r ?? ""), stderr: "", exitCode: 0 };
|
|
2568
|
+
}
|
|
2569
|
+
function buildSessionDeps(rpc, opts = {}) {
|
|
2570
|
+
const ctx = opts.ownerEmail ? { user: { email: opts.ownerEmail, id: opts.ownerEmail } } : void 0;
|
|
2571
|
+
return {
|
|
2572
|
+
cwd: opts.cwd,
|
|
2573
|
+
async runBash(command, o) {
|
|
2574
|
+
return normalizeBash(await rpc.bash(command, o?.cwd, void 0, ctx));
|
|
2575
|
+
},
|
|
2576
|
+
async sessionSend(text) {
|
|
2577
|
+
const content = JSON.stringify({ role: "user", content: { type: "text", text } });
|
|
2578
|
+
await rpc.sendMessage(content, void 0, { sentFrom: "wise-agent" }, ctx);
|
|
2579
|
+
},
|
|
2580
|
+
async askSession(question) {
|
|
2581
|
+
return textFrom(await rpc.btw(question, ctx));
|
|
2582
|
+
},
|
|
2583
|
+
async summarizeSession(question) {
|
|
2584
|
+
return textFrom(await rpc.btw(`Briefly summarize the work so far, answering: ${question}`, ctx));
|
|
2585
|
+
},
|
|
2586
|
+
async getContext() {
|
|
2587
|
+
let messages = [];
|
|
2588
|
+
try {
|
|
2589
|
+
const r = await rpc.getLatestMessages(void 0, 6, ctx);
|
|
2590
|
+
messages = Array.isArray(r) ? r : r?.messages ?? [];
|
|
2591
|
+
} catch {
|
|
2592
|
+
}
|
|
2593
|
+
const latest = messages.length ? messages[messages.length - 1] : null;
|
|
2594
|
+
const latestText = latest?.content?.content?.text ?? latest?.content?.text ?? null;
|
|
2595
|
+
return {
|
|
2596
|
+
...opts.status ? opts.status() : {},
|
|
2597
|
+
recentMessageCount: messages.length,
|
|
2598
|
+
latestMessage: latestText
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
function channelPublicView(c) {
|
|
2605
|
+
return { id: c.id, name: c.name, description: c.description, identity: { mode: c.identity?.mode }, action: c.action?.kind };
|
|
2606
|
+
}
|
|
1868
2607
|
function isStructuredMessage(msg) {
|
|
1869
|
-
return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId);
|
|
2608
|
+
return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId || msg.channel);
|
|
1870
2609
|
}
|
|
1871
2610
|
function escapeXml(s) {
|
|
1872
2611
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -1875,6 +2614,8 @@ function formatInboxMessageXml(msg) {
|
|
|
1875
2614
|
if (!isStructuredMessage(msg)) return msg.body;
|
|
1876
2615
|
const attrs = [`message-id="${escapeXml(msg.messageId)}"`];
|
|
1877
2616
|
if (msg.from) attrs.push(`from="${escapeXml(msg.from)}"`);
|
|
2617
|
+
if (msg.channel) attrs.push(`channel="${escapeXml(msg.channel)}"`);
|
|
2618
|
+
if (msg.verified !== void 0) attrs.push(`verified="${msg.verified === true}"`);
|
|
1878
2619
|
if (msg.fromSession) attrs.push(`from-session="${escapeXml(msg.fromSession)}"`);
|
|
1879
2620
|
if (msg.to) attrs.push(`to="${escapeXml(msg.to)}"`);
|
|
1880
2621
|
if (msg.subject) attrs.push(`subject="${escapeXml(msg.subject)}"`);
|
|
@@ -1995,6 +2736,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1995
2736
|
replyTo: m.replyTo,
|
|
1996
2737
|
cc: m.cc,
|
|
1997
2738
|
threadId: m.threadId,
|
|
2739
|
+
// Channel provenance so the inbox UI can show who/verified.
|
|
2740
|
+
...m.verified !== void 0 ? { verified: m.verified } : {},
|
|
2741
|
+
...m.channel ? { channel: m.channel } : {},
|
|
1998
2742
|
...m.displayText ? { displayText: m.displayText } : {}
|
|
1999
2743
|
}));
|
|
2000
2744
|
metadataVersion++;
|
|
@@ -2084,6 +2828,13 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
2084
2828
|
notifyListeners({ type: "update-session", sessionId, metadata: { value: metadata, version: metadataVersion } });
|
|
2085
2829
|
callbacks.onMetadataUpdate?.(metadata);
|
|
2086
2830
|
};
|
|
2831
|
+
const channelStore = new ChannelStore(initialMetadata.path);
|
|
2832
|
+
const syncChannelsToMetadata = () => {
|
|
2833
|
+
metadata.channels = channelStore.list();
|
|
2834
|
+
metadataVersion++;
|
|
2835
|
+
notifyListeners({ type: "update-session", sessionId, metadata: { value: metadata, version: metadataVersion } });
|
|
2836
|
+
callbacks.onMetadataUpdate?.(metadata);
|
|
2837
|
+
};
|
|
2087
2838
|
const rpcHandlers = {
|
|
2088
2839
|
// ── Messages ──
|
|
2089
2840
|
getMessages: async (afterSeq, limit, context) => {
|
|
@@ -2254,6 +3005,103 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
2254
3005
|
syncRoutinesToMetadata();
|
|
2255
3006
|
return { success: true, resolved };
|
|
2256
3007
|
},
|
|
3008
|
+
// ── Channels (project-folder config; served by the channel server) ──
|
|
3009
|
+
listChannels: async (context) => {
|
|
3010
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
3011
|
+
return { channels: channelStore.list() };
|
|
3012
|
+
},
|
|
3013
|
+
saveChannel: async (channel, context) => {
|
|
3014
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
3015
|
+
try {
|
|
3016
|
+
const saved = channelStore.save(channel);
|
|
3017
|
+
syncChannelsToMetadata();
|
|
3018
|
+
return { success: true, channel: saved };
|
|
3019
|
+
} catch (e) {
|
|
3020
|
+
return { success: false, error: e?.message || String(e) };
|
|
3021
|
+
}
|
|
3022
|
+
},
|
|
3023
|
+
removeChannel: async (id, context) => {
|
|
3024
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
3025
|
+
const ok = channelStore.remove(id);
|
|
3026
|
+
syncChannelsToMetadata();
|
|
3027
|
+
return { success: ok };
|
|
3028
|
+
},
|
|
3029
|
+
setChannelEnabled: async (id, enabled, context) => {
|
|
3030
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
3031
|
+
channelStore.setEnabled(id, enabled);
|
|
3032
|
+
syncChannelsToMetadata();
|
|
3033
|
+
return { success: true };
|
|
3034
|
+
},
|
|
3035
|
+
addChannelCaller: async (id, name, kind, context) => {
|
|
3036
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
3037
|
+
const caller = channelStore.addCaller(id, name, kind);
|
|
3038
|
+
syncChannelsToMetadata();
|
|
3039
|
+
return { success: !!caller, caller };
|
|
3040
|
+
},
|
|
3041
|
+
getChannelSkill: async (id, context) => {
|
|
3042
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
3043
|
+
const c = channelStore.get(id);
|
|
3044
|
+
if (!c) return { error: "not found" };
|
|
3045
|
+
return { skill: generateSkillBody(c) };
|
|
3046
|
+
},
|
|
3047
|
+
// Public channel discovery (no session authz — channels are deliberately
|
|
3048
|
+
// published endpoints; each send() is gated by the channel's OWN identity).
|
|
3049
|
+
channelList: async () => {
|
|
3050
|
+
return { channels: channelStore.list().filter((c) => c.enabled !== false).map(channelPublicView) };
|
|
3051
|
+
},
|
|
3052
|
+
channelDescribe: async (id) => {
|
|
3053
|
+
const c = channelStore.get(id);
|
|
3054
|
+
if (!c || c.enabled === false) return { error: "not found" };
|
|
3055
|
+
return { ...channelPublicView(c), skill: { body: generateSkillBody(c) } };
|
|
3056
|
+
},
|
|
3057
|
+
channelSend: async (params, context) => {
|
|
3058
|
+
const c = channelStore.get(params.channel);
|
|
3059
|
+
if (!c || c.enabled === false) return { error: "channel not found" };
|
|
3060
|
+
const u = context?.user;
|
|
3061
|
+
const r = resolveSender(c, {
|
|
3062
|
+
key: params.key,
|
|
3063
|
+
from: params.from,
|
|
3064
|
+
hyphaUser: u && u.is_anonymous !== true ? u.email || u.id : void 0,
|
|
3065
|
+
hyphaAnonymous: u?.is_anonymous === true,
|
|
3066
|
+
hyphaWorkspace: u?.scope?.current_workspace
|
|
3067
|
+
});
|
|
3068
|
+
if (r.error || !r.sender) return { error: r.error || "unauthorized" };
|
|
3069
|
+
const callId = "call_" + randomUUID().slice(0, 10);
|
|
3070
|
+
const ownerEmail = metadata.sharing?.owner;
|
|
3071
|
+
const ownerCtx = ownerEmail ? { user: { email: ownerEmail, id: ownerEmail } } : void 0;
|
|
3072
|
+
try {
|
|
3073
|
+
if (c.action?.kind === "agent") {
|
|
3074
|
+
const rendered = renderMessage(c, { sender: r.sender, body: { message: params.message }, callId });
|
|
3075
|
+
const deps = buildSessionDeps(rpcHandlers, {
|
|
3076
|
+
cwd: metadata.path,
|
|
3077
|
+
ownerEmail,
|
|
3078
|
+
status: () => ({ thinking: lastActivity.thinking, sessionId })
|
|
3079
|
+
});
|
|
3080
|
+
const result = await dispatchAgentChannel({ channel: c, sender: r.sender, message: rendered, deps, env: process.env });
|
|
3081
|
+
channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: result.status });
|
|
3082
|
+
syncChannelsToMetadata();
|
|
3083
|
+
return { ok: result.status === "completed", call_id: callId, status: result.status, reply: result.reply, tool_calls: result.toolCalls, error: result.error };
|
|
3084
|
+
}
|
|
3085
|
+
if (c.action?.kind === "loop") return { error: "loop channels are served by the channel server, not channelSend" };
|
|
3086
|
+
const inboxMsg = {
|
|
3087
|
+
messageId: callId,
|
|
3088
|
+
body: xmlEscape(String(params.message ?? "").slice(0, 16 * 1024)),
|
|
3089
|
+
timestamp: Date.now(),
|
|
3090
|
+
read: false,
|
|
3091
|
+
from: r.sender.name,
|
|
3092
|
+
verified: r.sender.verified,
|
|
3093
|
+
channel: c.name,
|
|
3094
|
+
subject: c.name
|
|
3095
|
+
};
|
|
3096
|
+
await rpcHandlers.sendInboxMessage(inboxMsg, ownerCtx);
|
|
3097
|
+
channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "delivered" });
|
|
3098
|
+
syncChannelsToMetadata();
|
|
3099
|
+
return { ok: true, call_id: callId, status: "accepted" };
|
|
3100
|
+
} catch (e) {
|
|
3101
|
+
channelStore.recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId, outcome: "error" });
|
|
3102
|
+
return { ok: false, call_id: callId, status: "error", error: e?.message || String(e) };
|
|
3103
|
+
}
|
|
3104
|
+
},
|
|
2257
3105
|
// ── Agent State ──
|
|
2258
3106
|
getAgentState: async (context) => {
|
|
2259
3107
|
authorizeRequest(context, metadata.sharing, "view");
|
|
@@ -7241,17 +8089,17 @@ function buildBaselineSystemPrompt(sessionId) {
|
|
|
7241
8089
|
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
8090
|
|
|
7243
8091
|
**Session state:**
|
|
7244
|
-
- \`svamp session set-title "<title>"\` \u2014 set a concise 3-8 word title after the first response
|
|
8092
|
+
- \`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
8093
|
- \`svamp session set-link "<url>" "<label>"\` \u2014 surface any viewable artifact as a button
|
|
7246
8094
|
- \`svamp session notify "<msg>" [--level info|warning|error]\` \u2014 send a user notification
|
|
7247
8095
|
|
|
7248
8096
|
**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
8097
|
|
|
7250
|
-
##
|
|
8098
|
+
## Shared environment
|
|
7251
8099
|
|
|
7252
|
-
You
|
|
8100
|
+
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
8101
|
|
|
7254
|
-
**Inbox messages
|
|
8102
|
+
**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
8103
|
`;
|
|
7256
8104
|
}
|
|
7257
8105
|
|
|
@@ -7836,6 +8684,27 @@ loadDotEnv();
|
|
|
7836
8684
|
const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
|
|
7837
8685
|
const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
|
|
7838
8686
|
const DAEMON_LOCK_FILE = join(SVAMP_HOME, "daemon.lock");
|
|
8687
|
+
const DAEMON_STOP_MARKER_FILE = join(SVAMP_HOME, "daemon.stop");
|
|
8688
|
+
function writeStopMarker(reason) {
|
|
8689
|
+
try {
|
|
8690
|
+
writeFileSync(DAEMON_STOP_MARKER_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${reason}
|
|
8691
|
+
`, "utf-8");
|
|
8692
|
+
} catch {
|
|
8693
|
+
}
|
|
8694
|
+
}
|
|
8695
|
+
function clearStopMarker() {
|
|
8696
|
+
try {
|
|
8697
|
+
if (existsSync$1(DAEMON_STOP_MARKER_FILE)) unlinkSync$1(DAEMON_STOP_MARKER_FILE);
|
|
8698
|
+
} catch {
|
|
8699
|
+
}
|
|
8700
|
+
}
|
|
8701
|
+
function stopMarkerExists() {
|
|
8702
|
+
try {
|
|
8703
|
+
return existsSync$1(DAEMON_STOP_MARKER_FILE);
|
|
8704
|
+
} catch {
|
|
8705
|
+
return false;
|
|
8706
|
+
}
|
|
8707
|
+
}
|
|
7839
8708
|
const LOGS_DIR = join(SVAMP_HOME, "logs");
|
|
7840
8709
|
const SESSION_INDEX_FILE = join(SVAMP_HOME, "sessions-index.json");
|
|
7841
8710
|
function readPackageVersion() {
|
|
@@ -8711,7 +9580,7 @@ async function startDaemon(options) {
|
|
|
8711
9580
|
const list = loadExposedTunnels().filter((t) => t.name !== name);
|
|
8712
9581
|
saveExposedTunnels(list);
|
|
8713
9582
|
}
|
|
8714
|
-
const { ServeManager } = await import('./serveManager-
|
|
9583
|
+
const { ServeManager } = await import('./serveManager-Cd2gJI7J.mjs');
|
|
8715
9584
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
8716
9585
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
8717
9586
|
});
|
|
@@ -9391,6 +10260,17 @@ async function startDaemon(options) {
|
|
|
9391
10260
|
});
|
|
9392
10261
|
}
|
|
9393
10262
|
checkSvampConfig?.();
|
|
10263
|
+
try {
|
|
10264
|
+
const hasTitle = !!sessionMetadata.summary?.text?.trim() || !!sessionMetadata.customTitle?.toString().trim();
|
|
10265
|
+
if (!hasTitle) {
|
|
10266
|
+
const base = basename(directory.replace(/[/\\]+$/, "")) || "session";
|
|
10267
|
+
sessionMetadata = { ...sessionMetadata, summary: { text: base, updatedAt: Date.now() } };
|
|
10268
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
10269
|
+
sessionService.pushMessage({ type: "summary", summary: base }, "session");
|
|
10270
|
+
logger.log(`[Session ${sessionId}] Auto-title fallback \u2192 "${base}"`);
|
|
10271
|
+
}
|
|
10272
|
+
} catch {
|
|
10273
|
+
}
|
|
9394
10274
|
if (backgroundTaskCount > 0) {
|
|
9395
10275
|
const taskInfo = `Background tasks still running (${backgroundTaskCount}): ${backgroundTaskNames.join(", ")}`;
|
|
9396
10276
|
logger.log(`[Session ${sessionId}] ${taskInfo}`);
|
|
@@ -10894,6 +11774,17 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
10894
11774
|
insideOnTurnEnd = true;
|
|
10895
11775
|
try {
|
|
10896
11776
|
checkSvampConfig?.();
|
|
11777
|
+
try {
|
|
11778
|
+
const hasTitle = !!sessionMetadata.summary?.text?.trim() || !!sessionMetadata.customTitle?.toString().trim();
|
|
11779
|
+
if (!hasTitle) {
|
|
11780
|
+
const base = basename(directory.replace(/[/\\]+$/, "")) || "session";
|
|
11781
|
+
sessionMetadata = { ...sessionMetadata, summary: { text: base, updatedAt: Date.now() } };
|
|
11782
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
11783
|
+
sessionService.pushMessage({ type: "summary", summary: base }, "session");
|
|
11784
|
+
logger.log(`[Session ${sessionId}] Auto-title fallback \u2192 "${base}"`);
|
|
11785
|
+
}
|
|
11786
|
+
} catch {
|
|
11787
|
+
}
|
|
10897
11788
|
const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10898
11789
|
if (rlState) {
|
|
10899
11790
|
let promiseFulfilled = false;
|
|
@@ -11264,11 +12155,31 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11264
12155
|
}
|
|
11265
12156
|
);
|
|
11266
12157
|
logger.log(`Machine service registered: svamp-machine-${machineId}`);
|
|
12158
|
+
const channelHttpPort = Number(process.env.SVAMP_CHANNEL_HTTP_PORT) || 0;
|
|
12159
|
+
if (channelHttpPort > 0) {
|
|
12160
|
+
try {
|
|
12161
|
+
const { createChannelHttpServer } = await import('./httpServer-wwHHk1EM.mjs');
|
|
12162
|
+
const channelHttpServer = createChannelHttpServer({
|
|
12163
|
+
getSessionIds: () => {
|
|
12164
|
+
const ids = [];
|
|
12165
|
+
for (const [, s] of pidToTrackedSession) if (s.svampSessionId && !s.stopped && s.sessionRPCHandlers) ids.push(s.svampSessionId);
|
|
12166
|
+
return ids;
|
|
12167
|
+
},
|
|
12168
|
+
getSessionRPCHandlers: (sid) => {
|
|
12169
|
+
for (const [, s] of pidToTrackedSession) if (s.svampSessionId === sid && !s.stopped && s.sessionRPCHandlers) return s.sessionRPCHandlers;
|
|
12170
|
+
return void 0;
|
|
12171
|
+
}
|
|
12172
|
+
});
|
|
12173
|
+
channelHttpServer.listen(channelHttpPort, () => logger.log(`[channels] HTTP ingress on :${channelHttpPort} (POST /channel/<id>)`));
|
|
12174
|
+
} catch (e) {
|
|
12175
|
+
logger.error(`[channels] HTTP ingress failed to start: ${e.message}`);
|
|
12176
|
+
}
|
|
12177
|
+
}
|
|
11267
12178
|
(async () => {
|
|
11268
12179
|
const specs = loadExposedTunnels();
|
|
11269
12180
|
if (specs.length === 0) return;
|
|
11270
12181
|
logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
|
|
11271
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
12182
|
+
const { FrpcTunnel } = await import('./frpc-DTA0--Z7.mjs');
|
|
11272
12183
|
for (const spec of specs) {
|
|
11273
12184
|
if (tunnels.has(spec.name)) continue;
|
|
11274
12185
|
try {
|
|
@@ -11804,6 +12715,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11804
12715
|
async function stopDaemon(options) {
|
|
11805
12716
|
const signal = options?.cleanup ? "SIGUSR1" : "SIGTERM";
|
|
11806
12717
|
const mode = options?.cleanup ? "cleanup (sessions will be stopped)" : "quick (sessions preserved for auto-restore)";
|
|
12718
|
+
writeStopMarker(`stopDaemon (${options?.cleanup ? "cleanup" : "quick"})`);
|
|
11807
12719
|
const pidsToSignal = [];
|
|
11808
12720
|
const supervisorPidFile = join(SVAMP_HOME, "supervisor.pid");
|
|
11809
12721
|
try {
|
|
@@ -11847,17 +12759,32 @@ async function stopDaemon(options) {
|
|
|
11847
12759
|
} catch {
|
|
11848
12760
|
}
|
|
11849
12761
|
}
|
|
11850
|
-
|
|
12762
|
+
const { stopViaServiceManager } = await import('./serviceManager-hlOVxkhW.mjs');
|
|
12763
|
+
if (options?.cleanup && state?.pid) {
|
|
12764
|
+
try {
|
|
12765
|
+
process.kill(state.pid, "SIGUSR1");
|
|
12766
|
+
console.log(`Sent SIGUSR1 to daemon PID ${state.pid} \u2014 ${mode}`);
|
|
12767
|
+
} catch {
|
|
12768
|
+
}
|
|
12769
|
+
}
|
|
12770
|
+
const handledByService = await stopViaServiceManager();
|
|
12771
|
+
if (handledByService) {
|
|
12772
|
+
console.log("Stopped via OS service manager (KeepAlive/Restart disabled)");
|
|
12773
|
+
}
|
|
12774
|
+
if (pidsToSignal.length === 0 && !handledByService) {
|
|
11851
12775
|
console.log("No daemon running");
|
|
11852
12776
|
cleanupDaemonStateFile();
|
|
12777
|
+
clearStopMarker();
|
|
11853
12778
|
return;
|
|
11854
12779
|
}
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
12780
|
+
if (!handledByService) {
|
|
12781
|
+
for (const pid of pidsToSignal) {
|
|
12782
|
+
try {
|
|
12783
|
+
process.kill(pid, signal);
|
|
12784
|
+
console.log(`Sent ${signal} to PID ${pid} \u2014 ${mode}`);
|
|
12785
|
+
} catch {
|
|
12786
|
+
console.log(`PID ${pid} already gone`);
|
|
12787
|
+
}
|
|
11861
12788
|
}
|
|
11862
12789
|
}
|
|
11863
12790
|
pidsToSignal[0];
|
|
@@ -11914,19 +12841,24 @@ async function restartDaemon() {
|
|
|
11914
12841
|
const doFullRestart = async (reason) => {
|
|
11915
12842
|
console.log(`${reason} \u2014 doing full stop + start`);
|
|
11916
12843
|
await stopDaemon();
|
|
11917
|
-
|
|
11918
|
-
const
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
12844
|
+
clearStopMarker();
|
|
12845
|
+
const { ensureSupervisorViaServiceManager } = await import('./serviceManager-hlOVxkhW.mjs');
|
|
12846
|
+
const startedViaService = await ensureSupervisorViaServiceManager();
|
|
12847
|
+
if (!startedViaService) {
|
|
12848
|
+
const { spawn: spawn2 } = await import('child_process');
|
|
12849
|
+
const child = spawn2(process.execPath, [
|
|
12850
|
+
"--no-warnings",
|
|
12851
|
+
"--no-deprecation",
|
|
12852
|
+
...process.argv.slice(1, 2),
|
|
12853
|
+
"daemon",
|
|
12854
|
+
"start-supervised"
|
|
12855
|
+
], {
|
|
12856
|
+
detached: true,
|
|
12857
|
+
stdio: "ignore",
|
|
12858
|
+
env: process.env
|
|
12859
|
+
});
|
|
12860
|
+
child.unref();
|
|
12861
|
+
}
|
|
11930
12862
|
const stateFile2 = join(SVAMP_HOME, "daemon.state.json");
|
|
11931
12863
|
for (let i = 0; i < 100; i++) {
|
|
11932
12864
|
await new Promise((r) => setTimeout(r, 100));
|
|
@@ -12009,10 +12941,14 @@ function daemonStatus() {
|
|
|
12009
12941
|
|
|
12010
12942
|
var run = /*#__PURE__*/Object.freeze({
|
|
12011
12943
|
__proto__: null,
|
|
12944
|
+
DAEMON_STOP_MARKER_FILE: DAEMON_STOP_MARKER_FILE,
|
|
12945
|
+
clearStopMarker: clearStopMarker,
|
|
12012
12946
|
daemonStatus: daemonStatus,
|
|
12013
12947
|
restartDaemon: restartDaemon,
|
|
12014
12948
|
startDaemon: startDaemon,
|
|
12015
|
-
stopDaemon: stopDaemon
|
|
12949
|
+
stopDaemon: stopDaemon,
|
|
12950
|
+
stopMarkerExists: stopMarkerExists,
|
|
12951
|
+
writeStopMarker: writeStopMarker
|
|
12016
12952
|
});
|
|
12017
12953
|
|
|
12018
|
-
export {
|
|
12954
|
+
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 };
|