svamp-cli 0.2.91 → 0.2.93

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.
@@ -6,9 +6,7 @@ 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';
8
8
  import { existsSync, readFileSync, mkdirSync as mkdirSync$1, readdirSync, writeFileSync as writeFileSync$1, renameSync as renameSync$1, rmSync, appendFileSync, unlinkSync } from 'node:fs';
9
- import vm from 'node:vm';
10
9
  import { exec, spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
11
- import { WebSocket } from 'ws';
12
10
  import { promisify } from 'util';
13
11
  import { randomBytes, randomUUID, createHash } from 'node:crypto';
14
12
  import { join as join$1 } from 'node:path';
@@ -380,54 +378,6 @@ function buildSkillsPromptSection(skills) {
380
378
  ].join("\n");
381
379
  }
382
380
 
383
- function buildSvampApi(deps) {
384
- return {
385
- bash: (command, opts) => deps.runBash(command, opts),
386
- context: () => deps.getContext(),
387
- ask: (question) => deps.askSession(question),
388
- summarize: (question) => deps.summarizeSession(question),
389
- send: (message) => deps.sessionSend(message)
390
- };
391
- }
392
- async function runJs(code, deps, opts = {}) {
393
- return runJsWithApi(code, buildSvampApi(deps), opts);
394
- }
395
- async function runJsWithApi(code, api, opts = {}) {
396
- const timeoutMs = opts.timeoutMs ?? 5e3;
397
- const logs = [];
398
- const capture = (...a) => {
399
- logs.push(a.map((x) => typeof x === "string" ? x : JSON.stringify(x)).join(" "));
400
- };
401
- const sandbox = /* @__PURE__ */ Object.create(null);
402
- sandbox.svamp = api;
403
- sandbox.console = { log: capture, error: capture, warn: capture, info: capture };
404
- const context = vm.createContext(sandbox, { name: "wise-run_js" });
405
- const wrapped = `(async () => {
406
- ${code}
407
- })()`;
408
- let script;
409
- try {
410
- script = new vm.Script(wrapped, { filename: "wise-run_js.js" });
411
- } catch (e) {
412
- return { result: void 0, logs, error: "compile error: " + e.message };
413
- }
414
- let timer;
415
- try {
416
- const ran = script.runInContext(context, { timeout: timeoutMs });
417
- const result = await Promise.race([
418
- Promise.resolve(ran),
419
- new Promise((_, reject) => {
420
- timer = setTimeout(() => reject(new Error("run_js timed out")), timeoutMs);
421
- })
422
- ]);
423
- return { result, logs };
424
- } catch (e) {
425
- return { result: void 0, logs, error: e.message };
426
- } finally {
427
- if (timer) clearTimeout(timer);
428
- }
429
- }
430
-
431
381
  const READ_ONLY_TOOLS = ["get_context", "ask_session", "summarize_session", "use_skill"];
432
382
  const str$1 = (v) => v == null ? "" : String(v);
433
383
  function buildTools(deps, skills) {
@@ -483,16 +433,6 @@ function buildTools(deps, skills) {
483
433
  }
484
434
  return "(sent to the coding agent)";
485
435
  }
486
- },
487
- {
488
- name: "run_js",
489
- readOnly: false,
490
- 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.",
491
- parameters: { type: "object", properties: { code: { type: "string", description: "JS body; may use await and return." } }, required: ["code"], additionalProperties: false },
492
- run: async (a) => {
493
- const r = await runJs(str$1(a?.code), deps, { timeoutMs: 5e3 });
494
- return JSON.stringify({ result: r.result, logs: r.logs, error: r.error });
495
- }
496
436
  }
497
437
  ];
498
438
  }
@@ -619,7 +559,6 @@ You are WISE Agent, a fast, text-mode companion to the deep coding agent (Claude
619
559
  - summarize_session \u2014 a cheap subagent that summarizes the deep agent's transcript for a specific question.
620
560
  - use_skill \u2014 load a project skill's full steps by name, then carry them out with run_bash.
621
561
  - run_bash \u2014 run a shell command on the session's machine (when granted).
622
- - run_js \u2014 JavaScript with an async \`svamp\` API to compose several steps in one call (when granted).
623
562
  - send_to_session \u2014 hand a clear, reformulated instruction to the deep coding agent (when granted); pass wait=true to block for its reply.
624
563
 
625
564
  # Instructions
@@ -733,7 +672,7 @@ async function runWiseAgent(args) {
733
672
  }
734
673
 
735
674
  const MACHINE_READ_ONLY = ["daemon_status", "list_sessions", "read_session", "use_skill"];
736
- const MACHINE_MUTATING = ["run_bash", "run_js", "send_to_session", "spawn_session"];
675
+ const MACHINE_MUTATING = ["run_bash", "send_to_session", "spawn_session"];
737
676
  const ALL_MACHINE_TOOLS = [...MACHINE_READ_ONLY, ...MACHINE_MUTATING];
738
677
  const str = (v) => v == null ? "" : String(v);
739
678
  function machineToolsForRole(role) {
@@ -741,16 +680,6 @@ function machineToolsForRole(role) {
741
680
  if (role === "interact") return [...MACHINE_READ_ONLY, "send_to_session"];
742
681
  return [...MACHINE_READ_ONLY];
743
682
  }
744
- function machineSvampApi(deps) {
745
- return {
746
- bash: (command, opts) => deps.runBash(command, opts),
747
- status: () => deps.daemonStatus(),
748
- listSessions: () => deps.listSessions(),
749
- readSession: (id, opts) => deps.readSession(id, opts),
750
- sendToSession: (id, message) => deps.sendToSession(id, message),
751
- spawnSession: (directory) => deps.spawnSession(directory)
752
- };
753
- }
754
683
  function buildMachineTools(deps, skills) {
755
684
  return [
756
685
  {
@@ -804,16 +733,6 @@ function buildMachineTools(deps, skills) {
804
733
  description: "Start a new agent session in a directory on this machine.",
805
734
  parameters: { type: "object", properties: { directory: { type: "string" } }, required: ["directory"], additionalProperties: false },
806
735
  run: async (a) => JSON.stringify(await deps.spawnSession(str(a?.directory)))
807
- },
808
- {
809
- name: "run_js",
810
- readOnly: false,
811
- description: "Run JavaScript with an async `svamp` machine API to compose steps: svamp.bash(cmd), svamp.status(), svamp.listSessions(), svamp.readSession(id), svamp.sendToSession(id,msg), svamp.spawnSession(dir). Use await + return; console.log captured. Sandboxed, ~5s.",
812
- parameters: { type: "object", properties: { code: { type: "string" } }, required: ["code"], additionalProperties: false },
813
- run: async (a) => {
814
- const r = await runJsWithApi(str(a?.code), machineSvampApi(deps), { timeoutMs: 5e3 });
815
- return JSON.stringify({ result: r.result, logs: r.logs, error: r.error });
816
- }
817
736
  }
818
737
  ];
819
738
  }
@@ -848,7 +767,7 @@ You are WISE in machine-manager (global) mode \u2014 a fast cockpit over the sva
848
767
  # Tools
849
768
  - daemon_status \u2014 daemon health / version / session count. Use first for machine status.
850
769
  - list_sessions / read_session \u2014 see all sessions and inspect any one.
851
- - run_bash / run_js \u2014 run quick commands / scripts on the machine.
770
+ - run_bash \u2014 run quick commands / scripts on the machine.
852
771
  - send_to_session(id, message) \u2014 delegate a task to a specific session's Claude agent.
853
772
  - spawn_session(directory) \u2014 start a new session.
854
773
  - use_skill \u2014 load a machine-level procedure.
@@ -1021,126 +940,6 @@ function buildSessionDeps(rpc, opts = {}) {
1021
940
  };
1022
941
  }
1023
942
 
1024
- function toolsForRole(role) {
1025
- if (role === "admin") return [...READ_ONLY_TOOLS, "run_bash", "send_to_session", "run_js"];
1026
- if (role === "interact") return [...READ_ONLY_TOOLS, "send_to_session"];
1027
- return [...READ_ONLY_TOOLS];
1028
- }
1029
- function toRealtimeTools(tools) {
1030
- return tools.map((t) => ({ type: "function", name: t.name, description: t.description, parameters: t.parameters }));
1031
- }
1032
- function buildVoiceSessionUpdate(instructions, tools, opts) {
1033
- return {
1034
- type: "session.update",
1035
- session: {
1036
- instructions,
1037
- tools: toRealtimeTools(tools),
1038
- tool_choice: "auto",
1039
- ...opts?.voice ? { audio: { output: { voice: opts.voice } } } : {}
1040
- }
1041
- };
1042
- }
1043
- async function handleRealtimeEvent(ev, tools, send, nameByCallId) {
1044
- if (ev?.type === "response.output_item.added" && ev.item?.type === "function_call") {
1045
- if (nameByCallId && ev.item.call_id) nameByCallId.set(ev.item.call_id, ev.item.name);
1046
- return null;
1047
- }
1048
- if (!ev || ev.type !== "response.function_call_arguments.done") return null;
1049
- const callId = ev.call_id;
1050
- const name = ev.name || nameByCallId && nameByCallId.get(callId) || "";
1051
- let args = {};
1052
- try {
1053
- args = ev.arguments ? JSON.parse(ev.arguments) : {};
1054
- } catch {
1055
- }
1056
- const tool = tools.find((t) => t.name === name);
1057
- let output;
1058
- if (!tool) {
1059
- output = `error: tool "${name}" is not available to this caller`;
1060
- } else {
1061
- try {
1062
- output = await tool.run(args);
1063
- } catch (e) {
1064
- output = `error: ${e?.message || e}`;
1065
- }
1066
- }
1067
- send({ type: "conversation.item.create", item: { type: "function_call_output", call_id: callId, output } });
1068
- send({ type: "response.create" });
1069
- return { name, output };
1070
- }
1071
- async function initVoiceSession(init) {
1072
- const ctx = await loadWiseAgentContext(init.deps, init.config);
1073
- const instructions = buildWiseAgentInstructions(ctx, init.config);
1074
- const granted = gateTools(buildTools(init.deps, ctx.skills), init.config, init.sender.name);
1075
- return { tools: granted, sessionUpdate: buildVoiceSessionUpdate(instructions, granted, { voice: init.voice }) };
1076
- }
1077
- async function initMachineVoiceSession(init) {
1078
- const ctx = await loadMachineContext(init.deps);
1079
- const instructions = buildMachineInstructions(ctx);
1080
- const allow = new Set(machineToolsForRole(init.role ?? "admin"));
1081
- const granted = buildMachineTools(init.deps, ctx.skills).filter((t) => allow.has(t.name));
1082
- return { tools: granted, sessionUpdate: buildVoiceSessionUpdate(instructions, granted, { voice: init.voice }) };
1083
- }
1084
-
1085
- var sideband = /*#__PURE__*/Object.freeze({
1086
- __proto__: null,
1087
- buildVoiceSessionUpdate: buildVoiceSessionUpdate,
1088
- handleRealtimeEvent: handleRealtimeEvent,
1089
- initMachineVoiceSession: initMachineVoiceSession,
1090
- initVoiceSession: initVoiceSession,
1091
- toRealtimeTools: toRealtimeTools,
1092
- toolsForRole: toolsForRole
1093
- });
1094
-
1095
- const realFactory = (url, headers) => new WebSocket(url, { headers });
1096
- let _testFactory;
1097
- async function openSideband(opts) {
1098
- const base = (opts.baseUrl || "https://api.openai.com").replace(/^http/, "ws").replace(/\/+$/, "");
1099
- const url = `${base}/v1/realtime?call_id=${encodeURIComponent(opts.callId)}`;
1100
- const { tools, sessionUpdate } = opts.prepared ?? await initVoiceSession(opts.init);
1101
- const ws = (opts.wsFactory || _testFactory || realFactory)(url, { Authorization: `Bearer ${opts.apiKey}` });
1102
- const send = (e) => {
1103
- try {
1104
- ws.send(JSON.stringify(e));
1105
- } catch {
1106
- }
1107
- };
1108
- const nameByCallId = /* @__PURE__ */ new Map();
1109
- ws.on("open", () => send(sessionUpdate));
1110
- const wantTools = new Set(tools.map((t) => t.name));
1111
- let reasserts = 0;
1112
- ws.on("message", async (data) => {
1113
- let ev;
1114
- try {
1115
- ev = JSON.parse(typeof data === "string" ? data : data?.toString?.() ?? "");
1116
- } catch {
1117
- return;
1118
- }
1119
- if (ev?.type === "session.updated" && wantTools.size) {
1120
- const have = new Set((ev.session?.tools || []).map((t) => t?.name));
1121
- const missing = [...wantTools].some((n) => !have.has(n));
1122
- if (missing && reasserts < 10) {
1123
- reasserts++;
1124
- send(sessionUpdate);
1125
- return;
1126
- }
1127
- }
1128
- try {
1129
- await handleRealtimeEvent(ev, tools, send, nameByCallId);
1130
- } catch (e) {
1131
- opts.onError?.(e instanceof Error ? e : new Error(String(e)));
1132
- }
1133
- });
1134
- ws.on("close", () => opts.onClose?.());
1135
- ws.on("error", (e) => opts.onError?.(e instanceof Error ? e : new Error(String(e))));
1136
- return { callId: opts.callId, close: () => {
1137
- try {
1138
- ws.close();
1139
- } catch {
1140
- }
1141
- } };
1142
- }
1143
-
1144
943
  const execFileAsync$1 = promisify(execFile);
1145
944
  function parseEtime(s) {
1146
945
  if (!s) return 0;
@@ -1265,6 +1064,31 @@ function filterTerminalResponses(data) {
1265
1064
  function getMachineMetadataPath(svampHomeDir) {
1266
1065
  return join(svampHomeDir, "machine-metadata.json");
1267
1066
  }
1067
+ async function mintRealtimeEphemeralKey(baseUrl, apiKey, opts) {
1068
+ const realtimeBase = baseUrl || "https://api.openai.com";
1069
+ const ctrl = new AbortController();
1070
+ const timer = setTimeout(() => ctrl.abort(), 15e3);
1071
+ try {
1072
+ const response = await fetch(`${realtimeBase}/v1/realtime/client_secrets`, {
1073
+ method: "POST",
1074
+ headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
1075
+ body: JSON.stringify({
1076
+ session: {
1077
+ type: "realtime",
1078
+ model: opts.model || "gpt-realtime-mini",
1079
+ ...opts.voice ? { audio: { output: { voice: opts.voice } } } : {}
1080
+ }
1081
+ }),
1082
+ signal: ctrl.signal
1083
+ });
1084
+ if (!response.ok) throw new Error(`OpenAI client_secrets error: ${response.status}`);
1085
+ const result = await response.json();
1086
+ if (!result.value) throw new Error("client_secrets returned no value");
1087
+ return result.value;
1088
+ } finally {
1089
+ clearTimeout(timer);
1090
+ }
1091
+ }
1268
1092
  function loadPersistedMachineMetadata(svampHomeDir) {
1269
1093
  try {
1270
1094
  const data = readFileSync$1(getMachineMetadataPath(svampHomeDir), "utf-8");
@@ -1294,7 +1118,6 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1294
1118
  lastInboundRpcAt = Date.now();
1295
1119
  };
1296
1120
  const listeners = [];
1297
- const voiceSidebands = /* @__PURE__ */ new Map();
1298
1121
  const removeListener = (listener, reason) => {
1299
1122
  const idx = listeners.indexOf(listener);
1300
1123
  if (idx >= 0) {
@@ -2230,7 +2053,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2230
2053
  const tunnels = handlers.tunnels;
2231
2054
  if (!tunnels) throw new Error("Tunnel management not available");
2232
2055
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2233
- const { FrpcTunnel } = await import('./frpc-BBNWJChC.mjs');
2056
+ const { FrpcTunnel } = await import('./frpc-glQKHUBu.mjs');
2234
2057
  const tunnel = new FrpcTunnel({
2235
2058
  name: params.name,
2236
2059
  ports: params.ports,
@@ -2380,123 +2203,13 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2380
2203
  if (misconfig || !resolved.apiKey) {
2381
2204
  return { success: false, error: misconfig || "WISE voice is not configured: no OpenAI API key on this machine. Run `svamp wise-agent auth use-openai <KEY>` (or set OPENAI_API_KEY), then `svamp daemon restart`." };
2382
2205
  }
2383
- const realtimeBase = resolved.baseUrl || "https://api.openai.com";
2384
2206
  try {
2385
- const wisCtrl = new AbortController();
2386
- const wisTimer = setTimeout(() => wisCtrl.abort(), 15e3);
2387
- let response;
2388
- try {
2389
- response = await fetch(`${realtimeBase}/v1/realtime/client_secrets`, {
2390
- method: "POST",
2391
- headers: {
2392
- "Authorization": `Bearer ${resolved.apiKey}`,
2393
- "Content-Type": "application/json"
2394
- },
2395
- body: JSON.stringify({
2396
- session: {
2397
- type: "realtime",
2398
- model: params.model || "gpt-realtime-mini",
2399
- ...params.voice ? { audio: { output: { voice: params.voice } } } : {}
2400
- }
2401
- }),
2402
- signal: wisCtrl.signal
2403
- });
2404
- } finally {
2405
- clearTimeout(wisTimer);
2406
- }
2407
- if (!response.ok) {
2408
- return { success: false, error: `OpenAI API error: ${response.status}` };
2409
- }
2410
- const result = await response.json();
2411
- return { success: true, clientSecret: result.value };
2207
+ const clientSecret = await mintRealtimeEphemeralKey(resolved.baseUrl, resolved.apiKey, { model: params.model, voice: params.voice });
2208
+ return { success: true, clientSecret };
2412
2209
  } catch (error) {
2413
2210
  return { success: false, error: error instanceof Error ? error.message : "Failed to create token" };
2414
2211
  }
2415
2212
  },
2416
- // Attach the daemon's server-side control to an in-flight Realtime voice
2417
- // session (browser holds the WebRTC audio leg). The browser passes the
2418
- // `call_id` it got from OpenAI's SDP answer; the daemon opens the sideband
2419
- // WS and runs WISE tools server-side, gated by the caller's session role.
2420
- wiseAttachSideband: async (params, context) => {
2421
- trackInbound();
2422
- if (context && (!context.user || context.user.is_anonymous)) {
2423
- return { success: false, error: "Sign in to use WISE voice." };
2424
- }
2425
- if (!params?.callId) return { success: false, error: "callId is required" };
2426
- const resolved = resolveModel({ provider: "openai" }, process.env);
2427
- const misconfig = describeMisconfiguration(resolved);
2428
- if (misconfig || !resolved.apiKey) {
2429
- return { success: false, error: misconfig || "WISE voice is not configured on this machine." };
2430
- }
2431
- const userKey = (context?.user?.email || context?.user?.id || "anon").toLowerCase();
2432
- const senderName = context?.user?.email || context?.user?.id || "user";
2433
- let prepared;
2434
- let role;
2435
- if (params.sessionId) {
2436
- const rpc = handlers.getSessionRPCHandlers?.(params.sessionId);
2437
- if (!rpc) return { success: false, error: "Session not found on this machine" };
2438
- let cwd;
2439
- let owner;
2440
- try {
2441
- role = (await rpc.getEffectiveRole(context))?.role ?? null;
2442
- if (!role) return { success: false, error: "Not authorized for this session" };
2443
- const metaRes = await rpc.getMetadata(context);
2444
- cwd = metaRes?.metadata?.path;
2445
- owner = metaRes?.metadata?.sharing?.owner;
2446
- } catch {
2447
- return { success: false, error: "Not authorized for this session" };
2448
- }
2449
- try {
2450
- const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
2451
- prepared = await initVoiceSession({ deps, config: { tools: toolsForRole(role) }, sender: { name: senderName, kind: "user", verified: true }, voice: params.voice });
2452
- } catch (e) {
2453
- return { success: false, error: e?.message || "Failed to prepare voice session" };
2454
- }
2455
- } else {
2456
- role = getEffectiveRole(context, currentMetadata.sharing);
2457
- if (!role) return { success: false, error: "Not authorized on this machine" };
2458
- try {
2459
- const machineDeps = buildMachineDeps(
2460
- {
2461
- getSessionIds: handlers.getSessionIds,
2462
- getSessionRPCHandlers: handlers.getSessionRPCHandlers,
2463
- getTrackedSessions: handlers.getTrackedSessions,
2464
- spawnSession: handlers.spawnSession,
2465
- getDaemonStatus: () => ({ status: currentDaemonState.status, machineId, pid: currentDaemonState.pid })
2466
- },
2467
- { cwd: process.env.HOME, ownerEmail: currentMetadata.sharing?.owner }
2468
- );
2469
- prepared = await initMachineVoiceSession({ deps: machineDeps, role, voice: params.voice });
2470
- } catch (e) {
2471
- return { success: false, error: e?.message || "Failed to prepare voice session" };
2472
- }
2473
- }
2474
- voiceSidebands.get(userKey)?.close();
2475
- voiceSidebands.delete(userKey);
2476
- try {
2477
- const handle = await openSideband({
2478
- callId: params.callId,
2479
- apiKey: resolved.apiKey,
2480
- baseUrl: resolved.baseUrl,
2481
- prepared,
2482
- onClose: () => {
2483
- if (voiceSidebands.get(userKey)?.callId === params.callId) voiceSidebands.delete(userKey);
2484
- }
2485
- });
2486
- voiceSidebands.set(userKey, handle);
2487
- return { success: true, role, scope: params.sessionId ? "session" : "machine" };
2488
- } catch (e) {
2489
- return { success: false, error: e?.message || "Failed to attach voice sideband" };
2490
- }
2491
- },
2492
- // Explicit teardown of the caller's active voice sideband (e.g. on hang-up).
2493
- wiseReleaseVoice: async (_params, context) => {
2494
- trackInbound();
2495
- const userKey = (context?.user?.email || context?.user?.id || "anon").toLowerCase();
2496
- voiceSidebands.get(userKey)?.close();
2497
- voiceSidebands.delete(userKey);
2498
- return { success: true };
2499
- },
2500
2213
  // Text WISE turn. GLOBAL (machine-manager) by default; pass sessionId to
2501
2214
  // scope to a session. Runs server-side and returns the reply synchronously.
2502
2215
  wiseAsk: async (params, context) => {
@@ -2528,8 +2241,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2528
2241
  }
2529
2242
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
2530
2243
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
2531
- const { toolsForRole: toolsForRole2 } = await Promise.resolve().then(function () { return sideband; });
2532
- const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole2(role2) }, deps, transport, model: resolved.model });
2244
+ const { toolsForRole } = await import('./sideband-7glSvpTW.mjs');
2245
+ const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
2533
2246
  return fmt(r2);
2534
2247
  }
2535
2248
  const role = getEffectiveRole(context, currentMetadata.sharing);
@@ -2587,6 +2300,19 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2587
2300
  if (!rpc?.channelDescribe) return { error: "channel not found" };
2588
2301
  return rpc.channelDescribe(kwargs.channel);
2589
2302
  },
2303
+ // Clean GET endpoint for the self-contained skill markdown. Param is
2304
+ // named `channel` (not `kwargs`) so the Hypha HTTP gateway maps a plain
2305
+ // `?channel=<id>` query → a tidy, shareable SKILL.md URL.
2306
+ skill: async (channel) => {
2307
+ const id = channel;
2308
+ if (!id) return "# error\nMissing ?channel=<id>";
2309
+ const rpc = await findChannelOwner(id);
2310
+ if (!rpc?.channelDescribe) return `# error
2311
+ channel "${id}" not found`;
2312
+ const d = await rpc.channelDescribe(id);
2313
+ return d?.skill?.body || `# error
2314
+ ${d?.error || "not found"}`;
2315
+ },
2590
2316
  send: async (kwargs = {}, context) => {
2591
2317
  trackInbound();
2592
2318
  const rpc = await findChannelOwner(kwargs.channel);
@@ -2624,13 +2350,6 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2624
2350
  for (const listener of toRemove) {
2625
2351
  removeListener(listener, "disconnect");
2626
2352
  }
2627
- for (const h of voiceSidebands.values()) {
2628
- try {
2629
- h.close();
2630
- } catch {
2631
- }
2632
- }
2633
- voiceSidebands.clear();
2634
2353
  await server.unregisterService(serviceInfo.id);
2635
2354
  await server.unregisterService(channelsServiceInfo.id).catch(() => {
2636
2355
  });
@@ -2955,7 +2674,7 @@ function validateChannel(c) {
2955
2674
  if (c.action?.kind === "loop" && m === "caller-supplied" && !c.identity?.shared_key)
2956
2675
  errs.push("a caller-supplied channel without a shared_key may not use a loop action (unauthenticated task injection)");
2957
2676
  if (c.action?.kind === "agent" && m === "caller-supplied" && !c.identity?.shared_key) {
2958
- const MUTATING = ["run_bash", "send_to_session", "run_js"];
2677
+ const MUTATING = ["run_bash", "send_to_session"];
2959
2678
  const ag = c.action.agent || {};
2960
2679
  const grantsMutating = (ag.tools || []).some((t) => MUTATING.includes(t)) || Object.values(ag.per_caller || {}).some((p) => (p?.tools || []).some((t) => MUTATING.includes(t)));
2961
2680
  if (grantsMutating) errs.push("a caller-supplied agent channel without a shared_key may not grant run_bash/send_to_session");
@@ -3041,25 +2760,53 @@ class ChannelStore {
3041
2760
  return caller;
3042
2761
  }
3043
2762
  }
3044
- function generateSkillBody(channel, urlBase) {
3045
- const url = `${"https://<svamp-tunnel>"}/channel/${channel.id}`;
2763
+ function gatewayBase(channelsServiceId, baseUrl) {
2764
+ const slash = channelsServiceId.indexOf("/");
2765
+ if (slash < 0) return `${baseUrl.replace(/\/$/, "")}/services/${channelsServiceId}`;
2766
+ const ws = channelsServiceId.slice(0, slash);
2767
+ const clientSvc = channelsServiceId.slice(slash + 1);
2768
+ return `${baseUrl.replace(/\/$/, "")}/${ws}/services/${clientSvc}`;
2769
+ }
2770
+ function generateSkillBody(channel, ctx) {
2771
+ const svc = ctx?.channelsServiceId || "<workspace>/<machine>:channels";
2772
+ const base = ctx?.baseUrl || "https://hypha.aicell.io";
2773
+ const gw = ctx?.channelsServiceId ? gatewayBase(svc, base) : `${base}/<workspace>/services/<machine>:channels`;
2774
+ const skillUrl = `${gw}/skill?channel=${channel.id}`;
2775
+ const sendUrl = `${gw}/send`;
2776
+ const key = ctx?.key || "<your-key>";
3046
2777
  const isAgent = channel.action?.kind === "agent";
3047
- const replyNote = isAgent ? `
3048
- 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\`.` : `
3049
- Delivery is fire-and-forget; the message lands in the agent's inbox tagged with your identity (from/verified).`;
2778
+ const hyphaOpen = (channel.identity?.hypha_allow || []).length > 0;
2779
+ const name = channel.skill?.name || channel.name;
2780
+ const desc = channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`;
2781
+ const replyNote = isAgent ? `This is a **WISE Agent** channel: \`send()\` runs a fast assistant against the session's tools/skills and returns its answer synchronously in the result \`reply\`.` : `Delivery is fire-and-forget \u2014 the message lands in the agent's inbox, tagged with your verified identity (\`from\`).`;
2782
+ const rpcLine = hyphaOpen ? `**Hypha RPC** \u2014 preferred. Your verified Hypha identity is accepted, no key needed:` : `**Hypha RPC** \u2014 verified identity:`;
3050
2783
  return `---
3051
- name: ${channel.skill?.name || channel.name}
3052
- description: ${channel.skill?.description || channel.description || `Send a message to the "${channel.name}" channel.`}
2784
+ name: ${name}
2785
+ description: ${desc}
3053
2786
  ---
3054
- # ${channel.name}
2787
+ # ${name}
3055
2788
  ${channel.description || ""}
3056
2789
 
3057
- Self-contained guide for messaging another agent \u2014 share this (or its URL,
3058
- ${url}/skill.md) with an agent so it knows how to reach me.
3059
- ${replyNote}
2790
+ Self-contained guide for messaging the **${channel.name}** channel. ${replyNote}
2791
+ This skill (with every value below already filled in) is served at:
2792
+ ${skillUrl}
2793
+
2794
+ ${rpcLine}
2795
+ \`\`\`js
2796
+ await get_service("${svc}").send({
2797
+ channel: "${channel.id}",
2798
+ message: "your message here",
2799
+ from: "your-name",
2800
+ });
2801
+ \`\`\`
2802
+
2803
+ **HTTP** \u2014 any client, no Hypha SDK needed (Hypha gateway wraps args under \`kwargs\`):
2804
+ \`\`\`
2805
+ POST ${sendUrl}
2806
+ Content-Type: application/json
3060
2807
 
3061
- Hypha RPC (preferred, verified identity, no key): get_service("<ws>/<machine>:channels").send({ channel: "${channel.id}", message: "..." })
3062
- HTTP: POST ${url} with header Authorization: Bearer <your-key>, body { "message": "...", "from": "..." }`;
2808
+ {"kwargs": {"channel": "${channel.id}", "message": "your message here", "from": "your-name", "key": "${key}"}}
2809
+ \`\`\``;
3063
2810
  }
3064
2811
 
3065
2812
  function resolveSender(channel, input = {}) {
@@ -3337,6 +3084,19 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3337
3084
  callbacks.onMetadataUpdate?.(metadata);
3338
3085
  };
3339
3086
  const channelStore = new ChannelStore(initialMetadata.path);
3087
+ const cfg = server?.config || {};
3088
+ const channelsServiceId = cfg.workspace && cfg.client_id ? `${cfg.workspace}/${cfg.client_id}:channels` : void 0;
3089
+ const channelsBaseUrl = cfg.public_base_url || process.env.HYPHA_SERVER_URL || "https://hypha.aicell.io";
3090
+ const skillCtxFor = (c) => ({
3091
+ channelsServiceId,
3092
+ baseUrl: channelsBaseUrl,
3093
+ key: c.identity?.shared_key || c.identity?.callers?.[0]?.key
3094
+ });
3095
+ const skillUrlsFor = (c) => {
3096
+ if (!channelsServiceId) return { skillUrl: void 0, sendUrl: void 0 };
3097
+ const gw = gatewayBase(channelsServiceId, channelsBaseUrl);
3098
+ return { skillUrl: `${gw}/describe?channel=${c.id}`, sendUrl: `${gw}/send` };
3099
+ };
3340
3100
  const syncChannelsToMetadata = () => {
3341
3101
  metadata.channels = channelStore.list();
3342
3102
  metadataVersion++;
@@ -3550,7 +3310,17 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3550
3310
  authorizeRequest(context, metadata.sharing, "view");
3551
3311
  const c = channelStore.get(id);
3552
3312
  if (!c) return { error: "not found" };
3553
- return { skill: generateSkillBody(c) };
3313
+ const { skillUrl, sendUrl } = skillUrlsFor(c);
3314
+ return {
3315
+ skill: generateSkillBody(c, skillCtxFor(c)),
3316
+ channelsServiceId,
3317
+ channelId: c.id,
3318
+ name: c.name,
3319
+ skillUrl,
3320
+ sendUrl,
3321
+ key: c.identity?.shared_key || c.identity?.callers?.[0]?.key,
3322
+ hyphaKeyless: (c.identity?.hypha_allow || []).length > 0
3323
+ };
3554
3324
  },
3555
3325
  // Public channel discovery (no session authz — channels are deliberately
3556
3326
  // published endpoints; each send() is gated by the channel's OWN identity).
@@ -3560,7 +3330,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3560
3330
  channelDescribe: async (id) => {
3561
3331
  const c = channelStore.get(id);
3562
3332
  if (!c || c.enabled === false) return { error: "not found" };
3563
- return { ...channelPublicView(c), skill: { body: generateSkillBody(c) } };
3333
+ return { ...channelPublicView(c), skill: { body: generateSkillBody(c, skillCtxFor(c)) } };
3564
3334
  },
3565
3335
  channelSend: async (params, context) => {
3566
3336
  const c = channelStore.get(params.channel);
@@ -10207,7 +9977,7 @@ async function startDaemon(options) {
10207
9977
  const list = loadExposedTunnels().filter((t) => t.name !== name);
10208
9978
  saveExposedTunnels(list);
10209
9979
  }
10210
- const { ServeManager } = await import('./serveManager-Dn9GyzAG.mjs');
9980
+ const { ServeManager } = await import('./serveManager-CoVwgYS_.mjs');
10211
9981
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
10212
9982
  ensureAutoInstalledSkills(logger).catch(() => {
10213
9983
  });
@@ -12835,7 +12605,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12835
12605
  const specs = loadExposedTunnels();
12836
12606
  if (specs.length === 0) return;
12837
12607
  logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
12838
- const { FrpcTunnel } = await import('./frpc-BBNWJChC.mjs');
12608
+ const { FrpcTunnel } = await import('./frpc-glQKHUBu.mjs');
12839
12609
  for (const spec of specs) {
12840
12610
  if (tunnels.has(spec.name)) continue;
12841
12611
  try {
@@ -13607,4 +13377,4 @@ var run = /*#__PURE__*/Object.freeze({
13607
13377
  writeStopMarker: writeStopMarker
13608
13378
  });
13609
13379
 
13610
- export { normalizeAllowedUser as A, loadSecurityContextConfig as B, resolveSecurityContext as C, buildSecurityContextFromFlags as D, mergeSecurityContexts as E, buildSessionShareUrl as F, computeOutboundHop as G, buildMachineShareUrl as H, generateHookSettings as I, DefaultTransport$1 as J, acpBackend as K, acpAgentConfig as L, codexMcpBackend as M, GeminiTransport$1 as N, claudeAuth as O, instanceConfig as P, api as Q, RoutineStore as R, ServeAuth as S, run as T, 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 };
13380
+ export { loadMachineContext as A, buildMachineInstructions as B, machineToolsForRole as C, buildMachineTools as D, resolveModel as E, normalizeAllowedUser as F, loadSecurityContextConfig as G, resolveSecurityContext as H, buildSecurityContextFromFlags as I, mergeSecurityContexts as J, buildSessionShareUrl as K, computeOutboundHop as L, buildMachineShareUrl as M, describeMisconfiguration as N, buildMachineDeps as O, generateHookSettings as P, DefaultTransport$1 as Q, RoutineStore as R, ServeAuth as S, acpBackend as T, acpAgentConfig as U, codexMcpBackend as V, GeminiTransport$1 as W, claudeAuth as X, instanceConfig as Y, api as Z, run as _, 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, READ_ONLY_TOOLS as z };