clawborrator-cli 0.0.54 → 0.0.56

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.
Files changed (2) hide show
  1. package/dist-bundled/claw.cjs +1069 -602
  2. package/package.json +1 -1
@@ -5226,7 +5226,7 @@ var require_websocket = __commonJS({
5226
5226
  var http = require("http");
5227
5227
  var net = require("net");
5228
5228
  var tls = require("tls");
5229
- var { randomBytes: randomBytes2, createHash: createHash2 } = require("crypto");
5229
+ var { randomBytes: randomBytes3, createHash: createHash3 } = require("crypto");
5230
5230
  var { Duplex, Readable } = require("stream");
5231
5231
  var { URL: URL2 } = require("url");
5232
5232
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -5756,7 +5756,7 @@ var require_websocket = __commonJS({
5756
5756
  }
5757
5757
  }
5758
5758
  const defaultPort = isSecure ? 443 : 80;
5759
- const key = randomBytes2(16).toString("base64");
5759
+ const key = randomBytes3(16).toString("base64");
5760
5760
  const request2 = isSecure ? https.request : http.request;
5761
5761
  const protocolSet = /* @__PURE__ */ new Set();
5762
5762
  let perMessageDeflate;
@@ -5886,7 +5886,7 @@ var require_websocket = __commonJS({
5886
5886
  abortHandshake(websocket, socket, "Invalid Upgrade header");
5887
5887
  return;
5888
5888
  }
5889
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
5889
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
5890
5890
  if (res.headers["sec-websocket-accept"] !== digest) {
5891
5891
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
5892
5892
  return;
@@ -6253,7 +6253,7 @@ var require_websocket_server = __commonJS({
6253
6253
  var EventEmitter = require("events");
6254
6254
  var http = require("http");
6255
6255
  var { Duplex } = require("stream");
6256
- var { createHash: createHash2 } = require("crypto");
6256
+ var { createHash: createHash3 } = require("crypto");
6257
6257
  var extension2 = require_extension();
6258
6258
  var PerMessageDeflate2 = require_permessage_deflate();
6259
6259
  var subprotocol2 = require_subprotocol();
@@ -6554,7 +6554,7 @@ var require_websocket_server = __commonJS({
6554
6554
  );
6555
6555
  }
6556
6556
  if (this._state > RUNNING) return abortHandshake(socket, 503);
6557
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
6557
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
6558
6558
  const headers = [
6559
6559
  "HTTP/1.1 101 Switching Protocols",
6560
6560
  "Upgrade: websocket",
@@ -64056,34 +64056,38 @@ var ApiError = class extends Error {
64056
64056
  this.name = "ApiError";
64057
64057
  }
64058
64058
  };
64059
+ function buildRequestHeaders(body, token) {
64060
+ const headers = {};
64061
+ if (body !== void 0) headers["Content-Type"] = "application/json";
64062
+ if (token) headers["Authorization"] = `Bearer ${token}`;
64063
+ return headers;
64064
+ }
64065
+ function parseResponseBody(text) {
64066
+ if (!text) return null;
64067
+ try {
64068
+ return JSON.parse(text);
64069
+ } catch {
64070
+ return { error: text };
64071
+ }
64072
+ }
64073
+ function buildApiError(status, parsed) {
64074
+ const code = parsed?.error || `http_${status}`;
64075
+ const msg = typeof parsed?.message === "string" ? parsed.message : code;
64076
+ return new ApiError(status, code, msg);
64077
+ }
64059
64078
  async function request(method, path, body, opts = {}) {
64060
64079
  const cfg = loadConfig();
64061
64080
  const hubUrl = (opts.hubUrl ?? cfg.hubUrl).replace(/\/$/, "");
64062
64081
  const token = opts.token === void 0 ? cfg.sessionToken : opts.token;
64063
- const headers = {};
64064
- if (body !== void 0) headers["Content-Type"] = "application/json";
64065
- if (token) headers["Authorization"] = `Bearer ${token}`;
64066
64082
  const res = await fetch(`${hubUrl}${path}`, {
64067
64083
  method,
64068
- headers,
64084
+ headers: buildRequestHeaders(body, token),
64069
64085
  body: body !== void 0 ? JSON.stringify(body) : void 0,
64070
64086
  signal: opts.signal
64071
64087
  });
64072
64088
  if (res.status === 204) return void 0;
64073
- const text = await res.text();
64074
- let parsed = null;
64075
- if (text) {
64076
- try {
64077
- parsed = JSON.parse(text);
64078
- } catch {
64079
- parsed = { error: text };
64080
- }
64081
- }
64082
- if (!res.ok) {
64083
- const code = parsed?.error || `http_${res.status}`;
64084
- const msg = typeof parsed?.message === "string" ? parsed.message : code;
64085
- throw new ApiError(res.status, code, msg);
64086
- }
64089
+ const parsed = parseResponseBody(await res.text());
64090
+ if (!res.ok) throw buildApiError(res.status, parsed);
64087
64091
  return parsed;
64088
64092
  }
64089
64093
  var api = {
@@ -67615,36 +67619,34 @@ var AmbiguousError = class extends Error {
67615
67619
  }
67616
67620
  code = "CLW_AMBIGUOUS";
67617
67621
  };
67618
- async function pickCandidate(input, candidates, opts = {}) {
67619
- if (candidates.length === 1) return candidates[0];
67622
+ function selectPromptSet(candidates, opts) {
67620
67623
  const live = candidates.filter((c) => c.connected);
67621
- let promptSet;
67622
67624
  if (opts.destructive) {
67623
- if (candidates.length <= 1) return candidates[0];
67624
- promptSet = candidates;
67625
- } else {
67626
- if (live.length <= 1) return live[0] ?? candidates[0];
67627
- promptSet = live;
67628
- }
67629
- if (!process.stdin.isTTY || !process.stderr.isTTY) {
67630
- throw new AmbiguousError(promptSet, input);
67625
+ if (candidates.length <= 1) return { auto: candidates[0] };
67626
+ return { promptSet: candidates };
67631
67627
  }
67628
+ if (live.length <= 1) return { auto: live[0] ?? candidates[0] };
67629
+ return { promptSet: live };
67630
+ }
67631
+ function formatCandidateLine(c, idx) {
67632
+ const qualified = c.routingName ? `@${c.startedByLogin}/${c.routingName.replace(/^@/, "")}` : `(no routing name)`;
67633
+ const status = c.connected ? `${BOLD}\u25CF online${RESET}` : `${DIM}\u25CB offline${RESET}`;
67634
+ const cwd = c.cwd ? ` ${DIM}${c.cwd}${RESET}` : "";
67635
+ const host = c.host ? ` ${DIM}${c.host}${RESET}` : "";
67636
+ const seen = c.lastSeenAt ? ` ${DIM}last seen ${c.lastSeenAt}${RESET}` : "";
67637
+ return ` ${BOLD}${idx + 1}${RESET}. ${qualified} ${status}${host}${cwd}${seen}
67638
+ ${DIM}id ${c.id}${RESET}`;
67639
+ }
67640
+ function renderPromptList(input, promptSet) {
67632
67641
  process.stderr.write(`${BOLD}'${input}' is ambiguous \u2014 pick a session:${RESET}
67633
67642
  `);
67634
67643
  for (let i = 0; i < promptSet.length; i++) {
67635
- const c = promptSet[i];
67636
- const qualified = c.routingName ? `@${c.startedByLogin}/${c.routingName.replace(/^@/, "")}` : `(no routing name)`;
67637
- const status = c.connected ? `${BOLD}\u25CF online${RESET}` : `${DIM}\u25CB offline${RESET}`;
67638
- const cwd = c.cwd ? ` ${DIM}${c.cwd}${RESET}` : "";
67639
- const host = c.host ? ` ${DIM}${c.host}${RESET}` : "";
67640
- const seen = c.lastSeenAt ? ` ${DIM}last seen ${c.lastSeenAt}${RESET}` : "";
67641
- process.stderr.write(` ${BOLD}${i + 1}${RESET}. ${qualified} ${status}${host}${cwd}${seen}
67642
- `);
67643
- process.stderr.write(` ${DIM}id ${c.id}${RESET}
67644
- `);
67644
+ process.stderr.write(formatCandidateLine(promptSet[i], i) + "\n");
67645
67645
  }
67646
67646
  process.stderr.write(` ${BOLD}q${RESET}. cancel
67647
67647
  `);
67648
+ }
67649
+ async function readSelection(promptSet) {
67648
67650
  const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stderr });
67649
67651
  const answer = await new Promise((resolve3) => {
67650
67652
  rl.question(`pick [1-${promptSet.length}]: `, resolve3);
@@ -67660,6 +67662,17 @@ async function pickCandidate(input, candidates, opts = {}) {
67660
67662
  }
67661
67663
  return promptSet[idx - 1];
67662
67664
  }
67665
+ async function pickCandidate(input, candidates, opts = {}) {
67666
+ if (candidates.length === 1) return candidates[0];
67667
+ const sel = selectPromptSet(candidates, opts);
67668
+ if ("auto" in sel) return sel.auto;
67669
+ const promptSet = sel.promptSet;
67670
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
67671
+ throw new AmbiguousError(promptSet, input);
67672
+ }
67673
+ renderPromptList(input, promptSet);
67674
+ return readSelection(promptSet);
67675
+ }
67663
67676
 
67664
67677
  // src/commands/session-attach.ts
67665
67678
  var RESET2 = "\x1B[0m";
@@ -67776,161 +67789,336 @@ function emitChatLine(prefix, body) {
67776
67789
  say(` ${line}`);
67777
67790
  }
67778
67791
  }
67779
- var sessionAttach = new Command("attach").description("open a TUI on a session \u2014 see the chat stream, post op-messages").argument("<ref>", "session UUID or @routingName (e.g. @driver)").option("--limit <n>", 'history items to load before the live stream begins. 0 = none. "all" = up to 5000. default 50.', "50").option("--no-op-messages", "exclude op-messages from the history backlog (live ones still arrive once attached)").option("--no-markdown", "render assistant_text and reply payloads as raw text instead of formatted markdown").option("--debug", "after every rendered event, print its full JSON payload (truncated at 2 KB) \u2014 surfaces fields the renderer normally hides (e.g., SubagentStop's last_assistant_message, PreToolUse tool_input details)").option("--no-status", 'disable the animated "claude working" dot at the bottom of the TUI (useful for piped output or rough-ANSI terminals)').action(async (ref, opts) => {
67792
+ function applyAttachFlags(opts) {
67780
67793
  if (opts.markdown === false) markdownEnabled = false;
67781
67794
  if (opts.debug) debugMode = true;
67782
67795
  if (opts.status === false) statusEnabled = false;
67783
- const cfg = loadConfig();
67784
- if (!cfg.sessionToken) {
67785
- console.error("error: not logged in. run `claw login`.");
67796
+ }
67797
+ async function fetchMyLogin() {
67798
+ try {
67799
+ const me = await api.get("/api/v1/me");
67800
+ return me.githubLogin;
67801
+ } catch {
67802
+ return null;
67803
+ }
67804
+ }
67805
+ var ATTACH_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
67806
+ function splitRoutingRef(ref) {
67807
+ const needle = ref.startsWith("@") ? ref : "@" + ref;
67808
+ const slash = needle.indexOf("/");
67809
+ if (slash > 0) {
67810
+ return {
67811
+ ownerLogin: needle.slice(1, slash),
67812
+ slug: "@" + needle.slice(slash + 1).replace(/^@/, "")
67813
+ };
67814
+ }
67815
+ return { ownerLogin: null, slug: needle };
67816
+ }
67817
+ function filterCandidatesByName(items, parts) {
67818
+ let candidates = items.filter((s) => s.routingName === parts.slug);
67819
+ if (parts.ownerLogin !== null) {
67820
+ candidates = candidates.filter((s) => s.startedByLogin === parts.ownerLogin);
67821
+ } else {
67822
+ const mine = candidates.filter((s) => s.role === "owner");
67823
+ if (mine.length > 0) candidates = mine;
67824
+ }
67825
+ return candidates;
67826
+ }
67827
+ function reportAmbiguousAndExit(ref, e) {
67828
+ const usedQualified = ref.includes("/");
67829
+ const advice = usedQualified ? `'${ref}' is ambiguous even within owner \u2014 multiple sessions share the same routing name. Re-run with a session UUID:` : `'${ref}' is ambiguous \u2014 re-run with the qualified @owner/slug form, or with a UUID if even that collides:`;
67830
+ console.error(`error: ${advice}`);
67831
+ for (const c of e.candidates) {
67832
+ console.error(` ${c.id} @${c.startedByLogin}/${(c.routingName ?? "").replace(/^@/, "")} ${c.cwd ?? ""}`);
67833
+ }
67834
+ process.exit(2);
67835
+ }
67836
+ async function resolveAttachSessionId(ref) {
67837
+ if (ATTACH_UUID_RE.test(ref)) return ref;
67838
+ const parts = splitRoutingRef(ref);
67839
+ const data = await api.get("/api/v1/sessions");
67840
+ const candidates = filterCandidatesByName(data.items, parts);
67841
+ if (candidates.length === 0) {
67842
+ const label = parts.ownerLogin ? `@${parts.ownerLogin}/${parts.slug.slice(1)}` : parts.slug;
67843
+ console.error(`error: no session with routing name ${label} (run \`claw session list\` to see what's available)`);
67786
67844
  process.exit(2);
67787
67845
  }
67788
- let myLogin = null;
67789
67846
  try {
67790
- const me = await api.get("/api/v1/me");
67791
- myLogin = me.githubLogin;
67847
+ const picked = await pickCandidate(ref, candidates);
67848
+ return picked.id;
67849
+ } catch (e) {
67850
+ if (e instanceof AmbiguousError) reportAmbiguousAndExit(ref, e);
67851
+ console.error(`error: ${e?.message ?? String(e)}`);
67852
+ process.exit(2);
67853
+ }
67854
+ }
67855
+ function parseHistoryLimit(opts) {
67856
+ const limitArg = String(opts.limit ?? "50").toLowerCase();
67857
+ if (limitArg === "all") return 5e3;
67858
+ if (limitArg === "0") return 0;
67859
+ return Math.max(0, parseInt(limitArg, 10) || 0);
67860
+ }
67861
+ function renderTimelineItem(item, myLogin) {
67862
+ if (item.kind === "event") {
67863
+ renderEvent(
67864
+ item.event,
67865
+ myLogin,
67866
+ /* fromBacklog */
67867
+ true
67868
+ );
67869
+ return;
67870
+ }
67871
+ if (item.kind === "op-message") {
67872
+ console.log(`${DIM2}[${shortTs(item.ts)}]${RESET2} ${GREEN}@${item.authorLogin}${RESET2} ${item.text}`);
67873
+ return;
67874
+ }
67875
+ const verb = item.action === "uploaded" ? `${GREEN}\u{1F4CE} uploaded${RESET2}` : `${RED}\u2717 deleted${RESET2}`;
67876
+ console.log(`${DIM2}[${shortTs(item.ts)}]${RESET2} ${BLUE}@${item.file.uploaderLogin}${RESET2} ${verb} ${BOLD2}${item.file.filename}${RESET2} ${DIM2}(${fmtBytes(item.file.size)} \xB7 fileId=${item.file.id})${RESET2}`);
67877
+ }
67878
+ async function drainHistoryBacklog(sessionId, opts, myLogin) {
67879
+ const historyLimit = parseHistoryLimit(opts);
67880
+ if (historyLimit <= 0) return;
67881
+ const kindsParam = opts.opMessages === false ? "&kinds=event,file" : "";
67882
+ try {
67883
+ const tl = await api.get(
67884
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/timeline?limit=${historyLimit}${kindsParam}`
67885
+ );
67886
+ if (tl.items.length === 0) return;
67887
+ console.log(`${DIM2}\u2500\u2500\u2500 history (${tl.items.length} item${tl.items.length === 1 ? "" : "s"}) \u2500\u2500\u2500${RESET2}`);
67888
+ for (const item of tl.items) renderTimelineItem(item, myLogin);
67889
+ console.log(`${DIM2}\u2500\u2500\u2500 live \u2500\u2500\u2500${RESET2}`);
67890
+ } catch (e) {
67891
+ console.error(`${DIM2}(history fetch failed: ${e?.message ?? String(e)} \u2014 continuing live)${RESET2}`);
67892
+ }
67893
+ }
67894
+ var BACKOFF = [1e3, 2e3, 5e3, 15e3, 3e4, 6e4];
67895
+ function onWsOpen(state) {
67896
+ if (state.reconnectAttempt > 0) {
67897
+ say(`${DIM2}[${ts()}]${RESET2} ${AMBER}reconnected${RESET2} to ${state.hubUrl}`);
67898
+ } else {
67899
+ say(`${DIM2}[${ts()}]${RESET2} connected to ${state.hubUrl}`);
67900
+ }
67901
+ state.reconnectAttempt = 0;
67902
+ state.mySubscription = false;
67903
+ const sub = { type: "subscribe", sessionId: state.sessionId };
67904
+ state.ws.send(JSON.stringify(sub));
67905
+ }
67906
+ function trackPendingPerm(state, msg) {
67907
+ if (msg.type === "permission_request") {
67908
+ state.pendingPerms.push({ requestId: msg.requestId, tool: msg.tool, sessionId: msg.sessionId });
67909
+ return;
67910
+ }
67911
+ if (msg.type === "permission_resolved") {
67912
+ const i = state.pendingPerms.findIndex((p) => p.requestId === msg.requestId);
67913
+ if (i >= 0) state.pendingPerms.splice(i, 1);
67914
+ }
67915
+ }
67916
+ function onWsMessage(state, data) {
67917
+ let msg;
67918
+ try {
67919
+ msg = JSON.parse(data.toString("utf8"));
67792
67920
  } catch {
67921
+ return;
67793
67922
  }
67794
- const UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
67795
- let sessionId = ref;
67796
- if (!UUID_RE2.test(sessionId)) {
67797
- const needle = sessionId.startsWith("@") ? sessionId : "@" + sessionId;
67798
- const slash = needle.indexOf("/");
67799
- let ownerLogin = null;
67800
- let slug;
67801
- if (slash > 0) {
67802
- ownerLogin = needle.slice(1, slash);
67803
- slug = "@" + needle.slice(slash + 1).replace(/^@/, "");
67804
- } else {
67805
- slug = needle;
67806
- }
67923
+ trackPendingPerm(state, msg);
67924
+ printInbound(msg, state.myLogin);
67925
+ if (msg.type === "subscribed") {
67926
+ state.mySubscription = true;
67927
+ say(`${DIM2}attached as ${BOLD2}${msg.role}${RESET2}${DIM2}. type for prompt \xB7 @other <text> to route \xB7 /m <text> for op-msg \xB7 /y /n on permissions \xB7 /q to quit${RESET2}`);
67928
+ }
67929
+ if (msg.type === "error" && (msg.code === "auth_failed" || msg.code === "token_revoked")) {
67930
+ state.stopRequested = true;
67931
+ }
67932
+ }
67933
+ function onWsClose(state, reconnect, code, reason) {
67934
+ stopWorking();
67935
+ if (state.stopRequested || code === 1e3) {
67936
+ say(`${DIM2}[${ts()}] disconnected (${code}${reason && reason.length ? ": " + reason.toString() : ""})${RESET2}`);
67937
+ process.exit(0);
67938
+ }
67939
+ if (code === 1008) {
67940
+ sayErr(`${RED}disconnected (${code}): auth rejected \u2014 won't retry${RESET2}`);
67941
+ process.exit(2);
67942
+ }
67943
+ const delay = BACKOFF[Math.min(state.reconnectAttempt, BACKOFF.length - 1)];
67944
+ state.reconnectAttempt += 1;
67945
+ say(`${DIM2}[${ts()}] ${AMBER}disconnected${RESET2} (${code}${reason && reason.length ? ": " + reason.toString() : ""})${DIM2} \u2014 reconnecting in ${delay / 1e3}s (attempt ${state.reconnectAttempt})${RESET2}`);
67946
+ if (state.reconnectTimer) clearTimeout(state.reconnectTimer);
67947
+ state.reconnectTimer = setTimeout(() => {
67948
+ state.reconnectTimer = null;
67949
+ reconnect();
67950
+ }, delay);
67951
+ }
67952
+ function onWsError(state, err) {
67953
+ if (state.ws.readyState === wrapper_default.CLOSING || state.ws.readyState === wrapper_default.CLOSED) return;
67954
+ sayErr(`${RED}ws error: ${err.message}${RESET2}`);
67955
+ }
67956
+ function connectWs(state) {
67957
+ const reconnect = () => connectWs(state);
67958
+ state.ws = new wrapper_default(state.wsUrl, { headers: { Authorization: `Bearer ${state.sessionToken}` } });
67959
+ state.ws.on("open", () => onWsOpen(state));
67960
+ state.ws.on("message", (data) => onWsMessage(state, data));
67961
+ state.ws.on("close", (code, reason) => onWsClose(state, reconnect, code, reason));
67962
+ state.ws.on("error", (err) => onWsError(state, err));
67963
+ }
67964
+ function handleQuitCmd(ctx) {
67965
+ ctx.state.stopRequested = true;
67966
+ if (ctx.state.reconnectTimer) {
67967
+ clearTimeout(ctx.state.reconnectTimer);
67968
+ ctx.state.reconnectTimer = null;
67969
+ }
67970
+ ctx.state.ws.close(1e3, "user quit");
67971
+ }
67972
+ function handleDebugCmd(text) {
67973
+ const arg = text.slice("/debug".length).trim().toLowerCase();
67974
+ if (arg === "on") debugMode = true;
67975
+ else if (arg === "off") debugMode = false;
67976
+ else debugMode = !debugMode;
67977
+ say(`${DIM2}debug: ${debugMode ? `${AMBER}on${RESET2}${DIM2}` : "off"}${RESET2}`);
67978
+ }
67979
+ function handleApprovalCmd(ctx, text) {
67980
+ const pending = ctx.state.pendingPerms[ctx.state.pendingPerms.length - 1];
67981
+ if (!pending) {
67982
+ say(`${DIM2}(no pending permission to act on)${RESET2}`);
67983
+ return;
67984
+ }
67985
+ const decision = text === "/y" || text === "/yes" ? "allow" : "deny";
67986
+ const approval = {
67987
+ type: "approval",
67988
+ sessionId: pending.sessionId,
67989
+ // owner of the permission, may not be `sessionId`
67990
+ requestId: pending.requestId,
67991
+ decision
67992
+ };
67993
+ ctx.state.ws.send(JSON.stringify(approval));
67994
+ }
67995
+ function handleOpMessageCmd(ctx, text) {
67996
+ const opText = text.slice(2).trim();
67997
+ if (!opText) {
67998
+ say(`${DIM2}usage: /m <text> (sends as op-message; bare text is a prompt)${RESET2}`);
67999
+ return;
68000
+ }
68001
+ const out = { type: "op_message", sessionId: ctx.state.sessionId, text: opText };
68002
+ ctx.state.ws.send(JSON.stringify(out));
68003
+ }
68004
+ function handlePromptCmd(ctx, text) {
68005
+ const promptText = text.slice(2).trim();
68006
+ if (!promptText) {
68007
+ say(`${DIM2}usage: /p <text> (or just type \u2014 bare text is a prompt now)${RESET2}`);
68008
+ return;
68009
+ }
68010
+ const out = { type: "prompt", sessionId: ctx.state.sessionId, text: promptText };
68011
+ ctx.state.ws.send(JSON.stringify(out));
68012
+ say(`${DIM2}[${ts()}]${RESET2} ${AMBER}\u2192 prompt sent${RESET2} ${promptText}`);
68013
+ startWorking();
68014
+ }
68015
+ var REDIRECT_UUID_RE = /^@?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
68016
+ var REDIRECT_LINE_RE = /^(@?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|@[A-Za-z0-9._\-/]+)\s+([\s\S]+)$/i;
68017
+ function sendRedirectByUuid(ctx, targetRef, promptText) {
68018
+ const peerSessionId = targetRef.replace(/^@/, "");
68019
+ const out = {
68020
+ type: "prompt",
68021
+ sessionId: peerSessionId,
68022
+ text: promptText,
68023
+ sourceSessionId: ctx.state.sessionId
68024
+ };
68025
+ ctx.state.ws.send(JSON.stringify(out));
68026
+ say(`${DIM2}[${ts()}]${RESET2} ${AMBER}\u2192 prompt \u2192 ${peerSessionId.slice(0, 8)}\u2026${RESET2} ${promptText}`);
68027
+ startWorking();
68028
+ armRouteWatchdog();
68029
+ }
68030
+ function reportRedirectAmbiguous(targetRef, liveMatches) {
68031
+ const usedQualified = targetRef.includes("/");
68032
+ const advice = usedQualified ? `'${targetRef}' is ambiguous even within owner \u2014 re-issue using a session UUID:` : `'${targetRef}' is ambiguous \u2014 use the qualified form @owner/slug, or a UUID if the qualified form still collides:`;
68033
+ sayErr(`${RED}error: ${advice}${RESET2}`);
68034
+ for (const c of liveMatches) {
68035
+ sayErr(` ${c.id} @${c.startedByLogin}/${(c.routingName ?? "").replace(/^@/, "")} ${DIM2}${c.cwd ?? ""}${RESET2}`);
68036
+ }
68037
+ }
68038
+ async function resolveRedirectAndSend(ctx, targetRef, promptText) {
68039
+ const parts = splitRoutingRef(targetRef);
68040
+ try {
67807
68041
  const data = await api.get("/api/v1/sessions");
67808
- let candidates = data.items.filter((s) => s.routingName === slug);
67809
- if (ownerLogin !== null) {
67810
- candidates = candidates.filter((s) => s.startedByLogin === ownerLogin);
67811
- } else {
67812
- const mine = candidates.filter((s) => s.role === "owner");
67813
- if (mine.length > 0) candidates = mine;
67814
- }
68042
+ const candidates = filterCandidatesByName(data.items, parts);
67815
68043
  if (candidates.length === 0) {
67816
- const label = ownerLogin ? `@${ownerLogin}/${slug.slice(1)}` : slug;
67817
- console.error(`error: no session with routing name ${label} (run \`claw session list\` to see what's available)`);
67818
- process.exit(2);
68044
+ sayErr(`${RED}error: no session ${targetRef} (try \`claw session list\` in another terminal)${RESET2}`);
68045
+ return;
67819
68046
  }
67820
- try {
67821
- const picked = await pickCandidate(ref, candidates);
67822
- sessionId = picked.id;
67823
- } catch (e) {
67824
- if (e instanceof AmbiguousError) {
67825
- const usedQualified = ref.includes("/");
67826
- const advice = usedQualified ? `'${ref}' is ambiguous even within owner \u2014 multiple sessions share the same routing name. Re-run with a session UUID:` : `'${ref}' is ambiguous \u2014 re-run with the qualified @owner/slug form, or with a UUID if even that collides:`;
67827
- console.error(`error: ${advice}`);
67828
- for (const c of e.candidates) {
67829
- console.error(` ${c.id} @${c.startedByLogin}/${(c.routingName ?? "").replace(/^@/, "")} ${c.cwd ?? ""}`);
67830
- }
67831
- process.exit(2);
67832
- }
67833
- console.error(`error: ${e?.message ?? String(e)}`);
67834
- process.exit(2);
68047
+ const liveMatches = candidates.filter((c) => c.connected);
68048
+ if (liveMatches.length > 1) {
68049
+ reportRedirectAmbiguous(targetRef, liveMatches);
68050
+ return;
67835
68051
  }
68052
+ const match = candidates[0];
68053
+ const out = {
68054
+ type: "prompt",
68055
+ sessionId: match.id,
68056
+ text: promptText,
68057
+ sourceSessionId: ctx.state.sessionId
68058
+ };
68059
+ ctx.state.ws.send(JSON.stringify(out));
68060
+ say(`${DIM2}[${ts()}]${RESET2} ${AMBER}\u2192 prompt \u2192 ${targetRef}${RESET2} ${promptText}`);
68061
+ startWorking();
68062
+ armRouteWatchdog();
68063
+ } catch (e) {
68064
+ sayErr(`${RED}error: ${e?.message ?? String(e)}${RESET2}`);
67836
68065
  }
67837
- const limitArg = String(opts.limit ?? "50").toLowerCase();
67838
- const historyLimit = limitArg === "all" ? 5e3 : limitArg === "0" ? 0 : Math.max(0, parseInt(limitArg, 10) || 0);
67839
- if (historyLimit > 0) {
67840
- const kindsParam = opts.opMessages === false ? "&kinds=event,file" : "";
67841
- try {
67842
- const tl = await api.get(`/api/v1/sessions/${encodeURIComponent(sessionId)}/timeline?limit=${historyLimit}${kindsParam}`);
67843
- if (tl.items.length > 0) {
67844
- console.log(`${DIM2}\u2500\u2500\u2500 history (${tl.items.length} item${tl.items.length === 1 ? "" : "s"}) \u2500\u2500\u2500${RESET2}`);
67845
- for (const item of tl.items) {
67846
- if (item.kind === "event") {
67847
- renderEvent(
67848
- item.event,
67849
- myLogin,
67850
- /* fromBacklog */
67851
- true
67852
- );
67853
- } else if (item.kind === "op-message") {
67854
- console.log(`${DIM2}[${shortTs(item.ts)}]${RESET2} ${GREEN}@${item.authorLogin}${RESET2} ${item.text}`);
67855
- } else if (item.kind === "file") {
67856
- const verb = item.action === "uploaded" ? `${GREEN}\u{1F4CE} uploaded${RESET2}` : `${RED}\u2717 deleted${RESET2}`;
67857
- console.log(`${DIM2}[${shortTs(item.ts)}]${RESET2} ${BLUE}@${item.file.uploaderLogin}${RESET2} ${verb} ${BOLD2}${item.file.filename}${RESET2} ${DIM2}(${fmtBytes(item.file.size)} \xB7 fileId=${item.file.id})${RESET2}`);
67858
- }
67859
- }
67860
- console.log(`${DIM2}\u2500\u2500\u2500 live \u2500\u2500\u2500${RESET2}`);
67861
- }
67862
- } catch (e) {
67863
- console.error(`${DIM2}(history fetch failed: ${e?.message ?? String(e)} \u2014 continuing live)${RESET2}`);
67864
- }
67865
- }
67866
- const wsUrl = cfg.hubUrl.replace(/^http/i, "ws") + "/cli";
67867
- let mySubscription = false;
67868
- const pendingPerms = [];
67869
- const BACKOFF = [1e3, 2e3, 5e3, 15e3, 3e4, 6e4];
67870
- let ws;
67871
- let stopRequested = false;
67872
- let reconnectAttempt = 0;
67873
- let reconnectTimer = null;
67874
- function connect() {
67875
- ws = new wrapper_default(wsUrl, { headers: { Authorization: `Bearer ${cfg.sessionToken}` } });
67876
- ws.on("open", () => {
67877
- if (reconnectAttempt > 0) {
67878
- say(`${DIM2}[${ts()}]${RESET2} ${AMBER}reconnected${RESET2} to ${cfg.hubUrl}`);
67879
- } else {
67880
- say(`${DIM2}[${ts()}]${RESET2} connected to ${cfg.hubUrl}`);
67881
- }
67882
- reconnectAttempt = 0;
67883
- mySubscription = false;
67884
- const sub = { type: "subscribe", sessionId };
67885
- ws.send(JSON.stringify(sub));
67886
- });
67887
- ws.on("message", (data) => {
67888
- let msg;
67889
- try {
67890
- msg = JSON.parse(data.toString("utf8"));
67891
- } catch {
67892
- return;
67893
- }
67894
- if (msg.type === "permission_request") {
67895
- pendingPerms.push({ requestId: msg.requestId, tool: msg.tool, sessionId: msg.sessionId });
67896
- } else if (msg.type === "permission_resolved") {
67897
- const i = pendingPerms.findIndex((p) => p.requestId === msg.requestId);
67898
- if (i >= 0) pendingPerms.splice(i, 1);
67899
- }
67900
- printInbound(msg, myLogin);
67901
- if (msg.type === "subscribed") {
67902
- mySubscription = true;
67903
- say(`${DIM2}attached as ${BOLD2}${msg.role}${RESET2}${DIM2}. type for prompt \xB7 @other <text> to route \xB7 /m <text> for op-msg \xB7 /y /n on permissions \xB7 /q to quit${RESET2}`);
67904
- }
67905
- if (msg.type === "error" && (msg.code === "auth_failed" || msg.code === "token_revoked")) {
67906
- stopRequested = true;
67907
- }
67908
- });
67909
- ws.on("close", (code, reason) => {
67910
- stopWorking();
67911
- if (stopRequested || code === 1e3) {
67912
- say(`${DIM2}[${ts()}] disconnected (${code}${reason && reason.length ? ": " + reason.toString() : ""})${RESET2}`);
67913
- process.exit(0);
67914
- }
67915
- if (code === 1008) {
67916
- sayErr(`${RED}disconnected (${code}): auth rejected \u2014 won't retry${RESET2}`);
67917
- process.exit(2);
67918
- }
67919
- const delay = BACKOFF[Math.min(reconnectAttempt, BACKOFF.length - 1)];
67920
- reconnectAttempt += 1;
67921
- say(`${DIM2}[${ts()}] ${AMBER}disconnected${RESET2} (${code}${reason && reason.length ? ": " + reason.toString() : ""})${DIM2} \u2014 reconnecting in ${delay / 1e3}s (attempt ${reconnectAttempt})${RESET2}`);
67922
- if (reconnectTimer) clearTimeout(reconnectTimer);
67923
- reconnectTimer = setTimeout(() => {
67924
- reconnectTimer = null;
67925
- connect();
67926
- }, delay);
67927
- });
67928
- ws.on("error", (err) => {
67929
- if (ws.readyState === wrapper_default.CLOSING || ws.readyState === wrapper_default.CLOSED) return;
67930
- sayErr(`${RED}ws error: ${err.message}${RESET2}`);
67931
- });
68066
+ }
68067
+ function handleRedirectLine(ctx, xMatch) {
68068
+ const targetRef = xMatch[1];
68069
+ const promptText = xMatch[2].trim();
68070
+ if (!promptText) {
68071
+ say(`${DIM2}usage: ${targetRef} <prompt>${RESET2}`);
68072
+ return;
68073
+ }
68074
+ if (REDIRECT_UUID_RE.test(targetRef)) {
68075
+ sendRedirectByUuid(ctx, targetRef, promptText);
68076
+ return;
68077
+ }
68078
+ void resolveRedirectAndSend(ctx, targetRef, promptText);
68079
+ }
68080
+ function handleBarePrompt(ctx, text) {
68081
+ const out = { type: "prompt", sessionId: ctx.state.sessionId, text };
68082
+ ctx.state.ws.send(JSON.stringify(out));
68083
+ say(`${DIM2}[${ts()}]${RESET2} ${AMBER}\u2192 prompt${RESET2} ${text}`);
68084
+ startWorking();
68085
+ }
68086
+ var SLASH_HANDLERS = [
68087
+ { match: (t) => t === "/q" || t === "/quit", run: (c) => handleQuitCmd(c) },
68088
+ { match: (t) => t === "/debug" || t.startsWith("/debug "), run: (_, t) => handleDebugCmd(t) },
68089
+ { match: (t) => t === "/y" || t === "/yes" || t === "/n" || t === "/no", run: (c, t) => handleApprovalCmd(c, t) },
68090
+ { match: (t) => t === "/m" || t.startsWith("/m "), run: (c, t) => handleOpMessageCmd(c, t) },
68091
+ { match: (t) => t === "/p" || t.startsWith("/p "), run: (c, t) => handlePromptCmd(c, t) }
68092
+ ];
68093
+ function dispatchSlash(ctx, text) {
68094
+ for (const h of SLASH_HANDLERS) {
68095
+ if (h.match(text)) {
68096
+ h.run(ctx, text);
68097
+ return true;
68098
+ }
68099
+ }
68100
+ if (text.startsWith("/")) {
68101
+ say(`${DIM2}unknown slash-command: ${text} (try /m /y /n /debug /q)${RESET2}`);
68102
+ return true;
68103
+ }
68104
+ return false;
68105
+ }
68106
+ function handleInputLine(ctx, raw) {
68107
+ const text = raw.trim();
68108
+ if (!text) return;
68109
+ if (!ctx.state.mySubscription) {
68110
+ say(`${DIM2}(not subscribed yet \u2014 waiting...)${RESET2}`);
68111
+ return;
68112
+ }
68113
+ if (dispatchSlash(ctx, text)) return;
68114
+ const xMatch = REDIRECT_LINE_RE.exec(text);
68115
+ if (xMatch) {
68116
+ handleRedirectLine(ctx, xMatch);
68117
+ return;
67932
68118
  }
67933
- connect();
68119
+ handleBarePrompt(ctx, text);
68120
+ }
68121
+ function setupReadline(ctx) {
67934
68122
  const rl = (0, import_node_readline2.createInterface)({
67935
68123
  input: process.stdin,
67936
68124
  output: process.stdout,
@@ -67939,213 +68127,92 @@ var sessionAttach = new Command("attach").description("open a TUI on a session \
67939
68127
  });
67940
68128
  rlRef = rl;
67941
68129
  if (process.stdout.isTTY) rl.prompt(true);
67942
- rl.on("line", (raw) => {
67943
- const text = raw.trim();
67944
- if (!text) return;
67945
- if (!mySubscription) {
67946
- say(`${DIM2}(not subscribed yet \u2014 waiting...)${RESET2}`);
67947
- return;
67948
- }
67949
- if (text === "/q" || text === "/quit") {
67950
- stopRequested = true;
67951
- if (reconnectTimer) {
67952
- clearTimeout(reconnectTimer);
67953
- reconnectTimer = null;
67954
- }
67955
- ws.close(1e3, "user quit");
67956
- return;
67957
- }
67958
- if (text === "/debug" || text.startsWith("/debug ")) {
67959
- const arg = text.slice("/debug".length).trim().toLowerCase();
67960
- if (arg === "on") debugMode = true;
67961
- else if (arg === "off") debugMode = false;
67962
- else debugMode = !debugMode;
67963
- say(`${DIM2}debug: ${debugMode ? `${AMBER}on${RESET2}${DIM2}` : "off"}${RESET2}`);
67964
- return;
67965
- }
67966
- if (text === "/y" || text === "/yes" || text === "/n" || text === "/no") {
67967
- const pending = pendingPerms[pendingPerms.length - 1];
67968
- if (!pending) {
67969
- say(`${DIM2}(no pending permission to act on)${RESET2}`);
67970
- return;
67971
- }
67972
- const decision = text === "/y" || text === "/yes" ? "allow" : "deny";
67973
- const approval = {
67974
- type: "approval",
67975
- sessionId: pending.sessionId,
67976
- // owner of the permission, may not be `sessionId`
67977
- requestId: pending.requestId,
67978
- decision
67979
- };
67980
- ws.send(JSON.stringify(approval));
67981
- return;
67982
- }
67983
- if (text === "/m" || text.startsWith("/m ")) {
67984
- const opText = text.slice(2).trim();
67985
- if (!opText) {
67986
- say(`${DIM2}usage: /m <text> (sends as op-message; bare text is a prompt)${RESET2}`);
67987
- return;
67988
- }
67989
- const out2 = { type: "op_message", sessionId, text: opText };
67990
- ws.send(JSON.stringify(out2));
67991
- return;
67992
- }
67993
- if (text === "/p" || text.startsWith("/p ")) {
67994
- const promptText = text.slice(2).trim();
67995
- if (!promptText) {
67996
- say(`${DIM2}usage: /p <text> (or just type \u2014 bare text is a prompt now)${RESET2}`);
67997
- return;
67998
- }
67999
- const out2 = { type: "prompt", sessionId, text: promptText };
68000
- ws.send(JSON.stringify(out2));
68001
- say(`${DIM2}[${ts()}]${RESET2} ${AMBER}\u2192 prompt sent${RESET2} ${promptText}`);
68002
- startWorking();
68003
- return;
68004
- }
68005
- if (text.startsWith("/")) {
68006
- say(`${DIM2}unknown slash-command: ${text} (try /m /y /n /debug /q)${RESET2}`);
68007
- return;
68008
- }
68009
- const UUID_RE_LOOSE = /^@?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
68010
- const xMatch = /^(@?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|@[A-Za-z0-9._\-/]+)\s+([\s\S]+)$/i.exec(text);
68011
- if (xMatch) {
68012
- const targetRef = xMatch[1];
68013
- const promptText = xMatch[2].trim();
68014
- if (!promptText) {
68015
- say(`${DIM2}usage: ${targetRef} <prompt>${RESET2}`);
68016
- return;
68017
- }
68018
- if (UUID_RE_LOOSE.test(targetRef)) {
68019
- const peerSessionId = targetRef.replace(/^@/, "");
68020
- const out2 = {
68021
- type: "prompt",
68022
- sessionId: peerSessionId,
68023
- text: promptText,
68024
- sourceSessionId: sessionId
68025
- };
68026
- ws.send(JSON.stringify(out2));
68027
- say(`${DIM2}[${ts()}]${RESET2} ${AMBER}\u2192 prompt \u2192 ${peerSessionId.slice(0, 8)}\u2026${RESET2} ${promptText}`);
68028
- startWorking();
68029
- armRouteWatchdog();
68030
- return;
68031
- }
68032
- (async () => {
68033
- const needle = targetRef.startsWith("@") ? targetRef : "@" + targetRef;
68034
- const slash = needle.indexOf("/");
68035
- let ownerLogin = null;
68036
- let slug;
68037
- if (slash > 0) {
68038
- ownerLogin = needle.slice(1, slash);
68039
- slug = "@" + needle.slice(slash + 1).replace(/^@/, "");
68040
- } else {
68041
- slug = needle;
68042
- }
68043
- try {
68044
- const data = await api.get("/api/v1/sessions");
68045
- let candidates = data.items.filter((s) => s.routingName === slug);
68046
- if (ownerLogin !== null) {
68047
- candidates = candidates.filter((s) => s.startedByLogin === ownerLogin);
68048
- } else {
68049
- const mine = candidates.filter((s) => s.role === "owner");
68050
- if (mine.length > 0) candidates = mine;
68051
- }
68052
- if (candidates.length === 0) {
68053
- sayErr(`${RED}error: no session ${targetRef} (try \`claw session list\` in another terminal)${RESET2}`);
68054
- return;
68055
- }
68056
- const liveMatches = candidates.filter((c) => c.connected);
68057
- if (liveMatches.length > 1) {
68058
- const usedQualified = targetRef.includes("/");
68059
- const advice = usedQualified ? `'${targetRef}' is ambiguous even within owner \u2014 re-issue using a session UUID:` : `'${targetRef}' is ambiguous \u2014 use the qualified form @owner/slug, or a UUID if the qualified form still collides:`;
68060
- sayErr(`${RED}error: ${advice}${RESET2}`);
68061
- for (const c of liveMatches) {
68062
- sayErr(` ${c.id} @${c.startedByLogin}/${(c.routingName ?? "").replace(/^@/, "")} ${DIM2}${c.cwd ?? ""}${RESET2}`);
68063
- }
68064
- return;
68065
- }
68066
- const match = candidates[0];
68067
- const out2 = {
68068
- type: "prompt",
68069
- sessionId: match.id,
68070
- text: promptText,
68071
- sourceSessionId: sessionId
68072
- };
68073
- ws.send(JSON.stringify(out2));
68074
- say(`${DIM2}[${ts()}]${RESET2} ${AMBER}\u2192 prompt \u2192 ${targetRef}${RESET2} ${promptText}`);
68075
- startWorking();
68076
- armRouteWatchdog();
68077
- } catch (e) {
68078
- sayErr(`${RED}error: ${e?.message ?? String(e)}${RESET2}`);
68079
- }
68080
- })();
68081
- return;
68082
- }
68083
- const out = { type: "prompt", sessionId, text };
68084
- ws.send(JSON.stringify(out));
68085
- say(`${DIM2}[${ts()}]${RESET2} ${AMBER}\u2192 prompt${RESET2} ${text}`);
68086
- startWorking();
68087
- });
68130
+ rl.on("line", (raw) => handleInputLine(ctx, raw));
68088
68131
  process.on("SIGINT", () => {
68089
- stopRequested = true;
68090
- if (reconnectTimer) {
68091
- clearTimeout(reconnectTimer);
68092
- reconnectTimer = null;
68132
+ ctx.state.stopRequested = true;
68133
+ if (ctx.state.reconnectTimer) {
68134
+ clearTimeout(ctx.state.reconnectTimer);
68135
+ ctx.state.reconnectTimer = null;
68093
68136
  }
68094
- ws.close(1e3, "sigint");
68137
+ ctx.state.ws.close(1e3, "sigint");
68095
68138
  });
68139
+ }
68140
+ var sessionAttach = new Command("attach").description("open a TUI on a session \u2014 see the chat stream, post op-messages").argument("<ref>", "session UUID or @routingName (e.g. @driver)").option("--limit <n>", 'history items to load before the live stream begins. 0 = none. "all" = up to 5000. default 50.', "50").option("--no-op-messages", "exclude op-messages from the history backlog (live ones still arrive once attached)").option("--no-markdown", "render assistant_text and reply payloads as raw text instead of formatted markdown").option("--debug", "after every rendered event, print its full JSON payload (truncated at 2 KB) \u2014 surfaces fields the renderer normally hides (e.g., SubagentStop's last_assistant_message, PreToolUse tool_input details)").option("--no-status", 'disable the animated "claude working" dot at the bottom of the TUI (useful for piped output or rough-ANSI terminals)').action(async (ref, opts) => {
68141
+ applyAttachFlags(opts);
68142
+ const cfg = loadConfig();
68143
+ if (!cfg.sessionToken) {
68144
+ console.error("error: not logged in. run `claw login`.");
68145
+ process.exit(2);
68146
+ }
68147
+ const myLogin = await fetchMyLogin();
68148
+ const sessionId = await resolveAttachSessionId(ref);
68149
+ await drainHistoryBacklog(sessionId, opts, myLogin);
68150
+ const state = {
68151
+ ws: void 0,
68152
+ // assigned by connectWs()
68153
+ sessionId,
68154
+ myLogin,
68155
+ hubUrl: cfg.hubUrl,
68156
+ wsUrl: cfg.hubUrl.replace(/^http/i, "ws") + "/cli",
68157
+ sessionToken: cfg.sessionToken,
68158
+ mySubscription: false,
68159
+ pendingPerms: [],
68160
+ stopRequested: false,
68161
+ reconnectAttempt: 0,
68162
+ reconnectTimer: null
68163
+ };
68164
+ connectWs(state);
68165
+ setupReadline({ state });
68096
68166
  });
68097
- function printInbound(msg, myLogin) {
68098
- switch (msg.type) {
68099
- case "subscribed":
68100
- break;
68101
- case "event": {
68102
- renderEvent(msg.event, myLogin);
68103
- break;
68104
- }
68105
- case "op_message": {
68106
- say(`${DIM2}[${shortTs(msg.ts)}]${RESET2} ${GREEN}@${msg.authorLogin}${RESET2} ${msg.text}`);
68107
- break;
68108
- }
68109
- case "permission_request": {
68110
- say(`${RED}[!] ${shortTs(msg.ts)}${RESET2} approval needed: ${BOLD2}${msg.tool}${RESET2} \u2014 ${msg.inputPreview}`);
68111
- break;
68112
- }
68113
- case "permission_resolved": {
68114
- const dec = msg.decision === "allow" ? `${GREEN}allowed${RESET2}` : msg.decision === "deny" ? `${RED}denied${RESET2}` : `${DIM2}expired${RESET2}`;
68115
- say(`${DIM2}[${ts()}]${RESET2} permission ${msg.requestId} ${dec} by @${msg.resolverLogin ?? "?"}`);
68116
- break;
68117
- }
68118
- case "presence": {
68119
- const list3 = msg.attached.map((l) => "@" + l).join(", ") || "(empty)";
68120
- let line;
68121
- if (msg.joined) {
68122
- line = `${GREEN}+ @${msg.joined} joined${RESET2}${DIM2} (attached: ${list3})${RESET2}`;
68123
- } else if (msg.left) {
68124
- line = `${AMBER}- @${msg.left} left${RESET2}${DIM2} (attached: ${list3})${RESET2}`;
68125
- } else {
68126
- line = `${DIM2}presence: ${list3}${RESET2}`;
68127
- }
68128
- say(`${DIM2}[${ts()}]${RESET2} ${line}`);
68129
- break;
68130
- }
68131
- case "channel_status": {
68132
- const tag2 = msg.connected ? `${GREEN}\u25CF channel online${RESET2}` : `${RED}\u25CB channel offline${RESET2}`;
68133
- say(`${DIM2}[${shortTs(msg.ts)}]${RESET2} ${tag2}`);
68134
- if (!msg.connected) stopWorking();
68135
- break;
68136
- }
68137
- case "file_event": {
68138
- const verb = msg.action === "uploaded" ? `${GREEN}\u{1F4CE} uploaded${RESET2}` : `${RED}\u2717 deleted${RESET2}`;
68139
- const f = msg.file;
68140
- say(`${DIM2}[${ts()}]${RESET2} ${BLUE}@${f.uploaderLogin}${RESET2} ${verb} ${BOLD2}${f.filename}${RESET2} ${DIM2}(${fmtBytes(f.size)} \xB7 fileId=${f.id})${RESET2}`);
68141
- break;
68142
- }
68143
- case "ack":
68144
- break;
68145
- case "error":
68146
- sayErr(`${RED}error (${msg.code}): ${msg.message}${RESET2}`);
68147
- break;
68167
+ function printInboundOpMessage(msg) {
68168
+ say(`${DIM2}[${shortTs(msg.ts)}]${RESET2} ${GREEN}@${msg.authorLogin}${RESET2} ${msg.text}`);
68169
+ }
68170
+ function printInboundPermissionRequest(msg) {
68171
+ say(`${RED}[!] ${shortTs(msg.ts)}${RESET2} approval needed: ${BOLD2}${msg.tool}${RESET2} \u2014 ${msg.inputPreview}`);
68172
+ }
68173
+ function printInboundPermissionResolved(msg) {
68174
+ const dec = msg.decision === "allow" ? `${GREEN}allowed${RESET2}` : msg.decision === "deny" ? `${RED}denied${RESET2}` : `${DIM2}expired${RESET2}`;
68175
+ say(`${DIM2}[${ts()}]${RESET2} permission ${msg.requestId} ${dec} by @${msg.resolverLogin ?? "?"}`);
68176
+ }
68177
+ function printInboundPresence(msg) {
68178
+ const list3 = msg.attached.map((l) => "@" + l).join(", ") || "(empty)";
68179
+ let line;
68180
+ if (msg.joined) {
68181
+ line = `${GREEN}+ @${msg.joined} joined${RESET2}${DIM2} (attached: ${list3})${RESET2}`;
68182
+ } else if (msg.left) {
68183
+ line = `${AMBER}- @${msg.left} left${RESET2}${DIM2} (attached: ${list3})${RESET2}`;
68184
+ } else {
68185
+ line = `${DIM2}presence: ${list3}${RESET2}`;
68148
68186
  }
68187
+ say(`${DIM2}[${ts()}]${RESET2} ${line}`);
68188
+ }
68189
+ function printInboundChannelStatus(msg) {
68190
+ const tag2 = msg.connected ? `${GREEN}\u25CF channel online${RESET2}` : `${RED}\u25CB channel offline${RESET2}`;
68191
+ say(`${DIM2}[${shortTs(msg.ts)}]${RESET2} ${tag2}`);
68192
+ if (!msg.connected) stopWorking();
68193
+ }
68194
+ function printInboundFileEvent(msg) {
68195
+ const verb = msg.action === "uploaded" ? `${GREEN}\u{1F4CE} uploaded${RESET2}` : `${RED}\u2717 deleted${RESET2}`;
68196
+ const f = msg.file;
68197
+ say(`${DIM2}[${ts()}]${RESET2} ${BLUE}@${f.uploaderLogin}${RESET2} ${verb} ${BOLD2}${f.filename}${RESET2} ${DIM2}(${fmtBytes(f.size)} \xB7 fileId=${f.id})${RESET2}`);
68198
+ }
68199
+ var INBOUND_PRINTERS = {
68200
+ subscribed: () => {
68201
+ },
68202
+ event: (msg, ml) => renderEvent(msg.event, ml),
68203
+ op_message: (msg) => printInboundOpMessage(msg),
68204
+ permission_request: (msg) => printInboundPermissionRequest(msg),
68205
+ permission_resolved: (msg) => printInboundPermissionResolved(msg),
68206
+ presence: (msg) => printInboundPresence(msg),
68207
+ channel_status: (msg) => printInboundChannelStatus(msg),
68208
+ file_event: (msg) => printInboundFileEvent(msg),
68209
+ ack: () => {
68210
+ },
68211
+ error: (msg) => sayErr(`${RED}error (${msg.code}): ${msg.message}${RESET2}`)
68212
+ };
68213
+ function printInbound(msg, myLogin) {
68214
+ const fn = INBOUND_PRINTERS[msg.type];
68215
+ if (fn) fn(msg, myLogin);
68149
68216
  }
68150
68217
  function shortTs(iso) {
68151
68218
  const d = new Date(iso);
@@ -68178,133 +68245,179 @@ function maybeDebugDump(p) {
68178
68245
  say(` ${DIM2}${line}${RESET2}`);
68179
68246
  }
68180
68247
  }
68248
+ function isOwnPromptMirror(ev, p, myLogin, fromBacklog) {
68249
+ if (fromBacklog) return false;
68250
+ if (ev.kind !== "chat" || ev.type !== "prompt") return false;
68251
+ const src = String(p.source ?? "");
68252
+ if (src !== "operator" && src !== "operator-route") return false;
68253
+ return !!myLogin && p.authorLogin === myLogin;
68254
+ }
68181
68255
  function renderEvent(ev, myLogin, fromBacklog = false) {
68182
68256
  const ts2 = shortTs(ev.ts);
68183
68257
  const p = ev.payload || {};
68184
- if (!fromBacklog && ev.kind === "chat" && ev.type === "prompt") {
68185
- const src = String(p.source ?? "");
68186
- if ((src === "operator" || src === "operator-route") && myLogin && p.authorLogin === myLogin) return;
68187
- }
68258
+ if (isOwnPromptMirror(ev, p, myLogin, fromBacklog)) return;
68188
68259
  try {
68189
68260
  renderEventBody(ev, ts2, p, myLogin);
68190
68261
  } finally {
68191
68262
  maybeDebugDump(p);
68192
68263
  }
68193
68264
  }
68194
- function renderEventBody(ev, ts2, p, myLogin) {
68265
+ function applyWorkingHeartbeat(ev) {
68195
68266
  if (ev.kind === "chat" && ev.type === "prompt") startWorking();
68196
68267
  else if (ev.kind === "tail" && ev.type === "PreToolUse") startWorking();
68197
68268
  else if (ev.kind === "tail" && ev.type === "Stop") stopWorking();
68198
- if (ev.kind === "chat") {
68199
- if (ev.type === "prompt") {
68200
- const text = String(p.text ?? p.prompt ?? "").trim() || JSON.stringify(p).slice(0, 200);
68201
- const source = String(p.source ?? "cli");
68202
- if (source === "operator-route") {
68203
- const peer = String(p.peerLogin ?? p.peerSessionId ?? "?");
68204
- const peerShort = peer.length > 36 ? peer.slice(0, 8) + "\u2026" : peer;
68205
- say(`${DIM2}[${ts2}]${RESET2} ${AMBER}\u2192 prompt \u2192 ${peerShort}${RESET2} ${text}`);
68206
- return;
68207
- }
68208
- const label = source === "operator" ? `${BOLD2}@${String(p.authorLogin ?? "remote")} \u203A${RESET2}` : `${BOLD2}(cli) \u203A${RESET2}`;
68209
- say(`${DIM2}[${ts2}]${RESET2} ${label} ${text}`);
68210
- return;
68211
- }
68212
- if (ev.type === "assistant_text") {
68213
- const text = String(p.text ?? "").trim();
68214
- const isPlaceholder = !!p.placeholder;
68215
- const tag2 = isPlaceholder ? `${DIM2}claude \xB7 thinking${RESET2}` : `${AMBER}claude${RESET2}`;
68216
- const prefix = `${DIM2}[${ts2}]${RESET2} ${tag2}`;
68217
- if (isPlaceholder) {
68218
- say(`${prefix} ${DIM2}${text}${RESET2}`);
68219
- return;
68220
- }
68221
- emitChatLine(prefix, renderMarkdown(text));
68222
- return;
68223
- }
68224
- if (ev.type === "reply") {
68225
- const text = String(p.text ?? "").trim();
68226
- const tag2 = p.source === "peer-reply" && p.peerLogin ? `${AMBER}${String(p.peerLogin)} answered${RESET2}` : p.chatId ? `${AMBER}claude${RESET2} ${DIM2}(reply to ${String(p.chatId).slice(0, 8)})${RESET2}` : `${AMBER}claude${RESET2}`;
68227
- emitChatLine(`${DIM2}[${ts2}]${RESET2} ${tag2}`, renderMarkdown(text));
68228
- if (p.source === "peer-reply") disarmRouteWatchdog();
68229
- return;
68230
- }
68231
- if (ev.type === "peer-timeout") {
68232
- const peer = String(p.peerLogin ?? p.peerSessionId ?? "?");
68233
- const peerShort = peer.length > 36 ? peer.slice(0, 8) + "\u2026" : peer;
68234
- say(`${DIM2}[${ts2}]${RESET2} ${RED}\u26A0 ${peerShort} did not reply${RESET2} ${DIM2}\u2014 peer's Claude may have denied the reply tool${RESET2}`);
68235
- disarmRouteWatchdog();
68236
- stopWorking();
68237
- return;
68238
- }
68239
- say(`${DIM2}[${ts2}]${RESET2} ${AMBER}[chat:${ev.type}]${RESET2} ${previewPayload(p)}`);
68269
+ }
68270
+ function extractPromptText(p) {
68271
+ const raw = String(p.text ?? p.prompt ?? "").trim();
68272
+ return raw || JSON.stringify(p).slice(0, 200);
68273
+ }
68274
+ function shortPeer(p) {
68275
+ const peer = String(p.peerLogin ?? p.peerSessionId ?? "?");
68276
+ return peer.length > 36 ? peer.slice(0, 8) + "\u2026" : peer;
68277
+ }
68278
+ function promptLabel(p, source) {
68279
+ if (source === "operator") return `${BOLD2}@${String(p.authorLogin ?? "remote")} \u203A${RESET2}`;
68280
+ return `${BOLD2}(cli) \u203A${RESET2}`;
68281
+ }
68282
+ function renderChatPromptBody(ts2, p) {
68283
+ const text = extractPromptText(p);
68284
+ const source = String(p.source ?? "cli");
68285
+ if (source === "operator-route") {
68286
+ say(`${DIM2}[${ts2}]${RESET2} ${AMBER}\u2192 prompt \u2192 ${shortPeer(p)}${RESET2} ${text}`);
68240
68287
  return;
68241
68288
  }
68242
- switch (ev.type) {
68243
- case "PreToolUse": {
68244
- const tool = String(p.tool_name ?? p.toolName ?? p.tool ?? "?");
68245
- const input = renderToolInput(p);
68246
- say(`${DIM2}[${ts2}]${RESET2} ${BLUE}\u2192 ${tool}${RESET2} ${DIM2}${input}${RESET2}`);
68247
- return;
68248
- }
68249
- case "PostToolUse": {
68250
- const tool = String(p.tool_name ?? p.toolName ?? p.tool ?? "?");
68251
- const out = stringifyToolResponse(p.tool_response ?? p.toolResponse ?? p.outputPreview ?? p.output);
68252
- const ok = p.ok === false ? `${RED}\u2717${RESET2}` : `${BLUE}\u2713${RESET2}`;
68253
- say(`${DIM2}[${ts2}]${RESET2} ${ok} ${tool}${out ? " " + DIM2 + truncate(out, 200) + RESET2 : ""}`);
68254
- return;
68255
- }
68256
- case "PostToolUseFailure": {
68257
- const tool = String(p.tool_name ?? p.toolName ?? p.tool ?? "?");
68258
- const err = stringifyToolResponse(p.error ?? p.message ?? p.tool_response);
68259
- say(`${DIM2}[${ts2}]${RESET2} ${RED}\u2717 ${tool}${RESET2} ${RED}${truncate(err, 200)}${RESET2}`);
68260
- return;
68261
- }
68262
- case "Stop":
68263
- say(`${DIM2}[${ts2}] \u2014 turn end \u2014${RESET2}`);
68264
- return;
68265
- case "SessionStart":
68266
- say(`${DIM2}[${ts2}] \u25B8 session start${RESET2}`);
68267
- return;
68268
- case "SessionEnd":
68269
- say(`${DIM2}[${ts2}] \u25C2 session end${RESET2}`);
68270
- return;
68271
- case "TaskCreated":
68272
- case "SubagentStart":
68273
- case "SubagentStop":
68274
- case "TaskCompleted": {
68275
- const which = ev.type;
68276
- const desc = String(p.description ?? p.agentType ?? "");
68277
- say(`${DIM2}[${ts2}] ${BLUE}\u21AA ${which}${RESET2}${desc ? " " + desc : ""}`);
68278
- return;
68279
- }
68280
- case "Notification": {
68281
- const msg = String(p.message ?? p.text ?? "").trim();
68282
- say(`${DIM2}[${ts2}]${RESET2} ${BOLD2}\u{1F514}${RESET2} ${msg || JSON.stringify(p).slice(0, 200)}`);
68283
- return;
68284
- }
68285
- default:
68286
- say(`${DIM2}[${ts2}]${RESET2} ${BLUE}[${ev.type}]${RESET2} ${previewPayload(p)}`);
68289
+ say(`${DIM2}[${ts2}]${RESET2} ${promptLabel(p, source)} ${text}`);
68290
+ }
68291
+ function renderChatAssistantTextBody(ts2, p) {
68292
+ const text = String(p.text ?? "").trim();
68293
+ const isPlaceholder = !!p.placeholder;
68294
+ const tag2 = isPlaceholder ? `${DIM2}claude \xB7 thinking${RESET2}` : `${AMBER}claude${RESET2}`;
68295
+ const prefix = `${DIM2}[${ts2}]${RESET2} ${tag2}`;
68296
+ if (isPlaceholder) {
68297
+ say(`${prefix} ${DIM2}${text}${RESET2}`);
68298
+ return;
68287
68299
  }
68300
+ emitChatLine(prefix, renderMarkdown(text));
68288
68301
  }
68289
- function stringifyToolResponse(v) {
68302
+ function renderChatReplyBody(ts2, p) {
68303
+ const text = String(p.text ?? "").trim();
68304
+ const tag2 = p.source === "peer-reply" && p.peerLogin ? `${AMBER}${String(p.peerLogin)} answered${RESET2}` : p.chatId ? `${AMBER}claude${RESET2} ${DIM2}(reply to ${String(p.chatId).slice(0, 8)})${RESET2}` : `${AMBER}claude${RESET2}`;
68305
+ emitChatLine(`${DIM2}[${ts2}]${RESET2} ${tag2}`, renderMarkdown(text));
68306
+ if (p.source === "peer-reply") disarmRouteWatchdog();
68307
+ }
68308
+ function renderChatPeerTimeoutBody(ts2, p) {
68309
+ const peer = String(p.peerLogin ?? p.peerSessionId ?? "?");
68310
+ const peerShort = peer.length > 36 ? peer.slice(0, 8) + "\u2026" : peer;
68311
+ say(`${DIM2}[${ts2}]${RESET2} ${RED}\u26A0 ${peerShort} did not reply${RESET2} ${DIM2}\u2014 peer's Claude may have denied the reply tool${RESET2}`);
68312
+ disarmRouteWatchdog();
68313
+ stopWorking();
68314
+ }
68315
+ function renderChatBody(ev, ts2, p) {
68316
+ if (ev.type === "prompt") {
68317
+ renderChatPromptBody(ts2, p);
68318
+ return;
68319
+ }
68320
+ if (ev.type === "assistant_text") {
68321
+ renderChatAssistantTextBody(ts2, p);
68322
+ return;
68323
+ }
68324
+ if (ev.type === "reply") {
68325
+ renderChatReplyBody(ts2, p);
68326
+ return;
68327
+ }
68328
+ if (ev.type === "peer-timeout") {
68329
+ renderChatPeerTimeoutBody(ts2, p);
68330
+ return;
68331
+ }
68332
+ say(`${DIM2}[${ts2}]${RESET2} ${AMBER}[chat:${ev.type}]${RESET2} ${previewPayload(p)}`);
68333
+ }
68334
+ function renderTailPreToolUseBody(ts2, p) {
68335
+ const tool = String(p.tool_name ?? p.toolName ?? p.tool ?? "?");
68336
+ const input = renderToolInput(p);
68337
+ say(`${DIM2}[${ts2}]${RESET2} ${BLUE}\u2192 ${tool}${RESET2} ${DIM2}${input}${RESET2}`);
68338
+ }
68339
+ function renderTailPostToolUseBody(ts2, p) {
68340
+ const tool = String(p.tool_name ?? p.toolName ?? p.tool ?? "?");
68341
+ const out = stringifyToolResponse(p.tool_response ?? p.toolResponse ?? p.outputPreview ?? p.output);
68342
+ const ok = p.ok === false ? `${RED}\u2717${RESET2}` : `${BLUE}\u2713${RESET2}`;
68343
+ say(`${DIM2}[${ts2}]${RESET2} ${ok} ${tool}${out ? " " + DIM2 + truncate(out, 200) + RESET2 : ""}`);
68344
+ }
68345
+ function renderTailPostToolUseFailureBody(ts2, p) {
68346
+ const tool = String(p.tool_name ?? p.toolName ?? p.tool ?? "?");
68347
+ const err = stringifyToolResponse(p.error ?? p.message ?? p.tool_response);
68348
+ say(`${DIM2}[${ts2}]${RESET2} ${RED}\u2717 ${tool}${RESET2} ${RED}${truncate(err, 200)}${RESET2}`);
68349
+ }
68350
+ function renderTailTaskBody(ts2, p, which) {
68351
+ const desc = String(p.description ?? p.agentType ?? "");
68352
+ say(`${DIM2}[${ts2}] ${BLUE}\u21AA ${which}${RESET2}${desc ? " " + desc : ""}`);
68353
+ }
68354
+ function renderTailNotificationBody(ts2, p) {
68355
+ const msg = String(p.message ?? p.text ?? "").trim();
68356
+ say(`${DIM2}[${ts2}]${RESET2} ${BOLD2}\u{1F514}${RESET2} ${msg || JSON.stringify(p).slice(0, 200)}`);
68357
+ }
68358
+ var TAIL_RENDERERS = {
68359
+ PreToolUse: (ts2, p) => renderTailPreToolUseBody(ts2, p),
68360
+ PostToolUse: (ts2, p) => renderTailPostToolUseBody(ts2, p),
68361
+ PostToolUseFailure: (ts2, p) => renderTailPostToolUseFailureBody(ts2, p),
68362
+ Stop: (ts2) => say(`${DIM2}[${ts2}] \u2014 turn end \u2014${RESET2}`),
68363
+ SessionStart: (ts2) => say(`${DIM2}[${ts2}] \u25B8 session start${RESET2}`),
68364
+ SessionEnd: (ts2) => say(`${DIM2}[${ts2}] \u25C2 session end${RESET2}`),
68365
+ TaskCreated: (ts2, p, type) => renderTailTaskBody(ts2, p, type),
68366
+ SubagentStart: (ts2, p, type) => renderTailTaskBody(ts2, p, type),
68367
+ SubagentStop: (ts2, p, type) => renderTailTaskBody(ts2, p, type),
68368
+ TaskCompleted: (ts2, p, type) => renderTailTaskBody(ts2, p, type),
68369
+ Notification: (ts2, p) => renderTailNotificationBody(ts2, p)
68370
+ };
68371
+ function renderTailBody(ev, ts2, p) {
68372
+ const fn = TAIL_RENDERERS[ev.type];
68373
+ if (fn) {
68374
+ fn(ts2, p, ev.type);
68375
+ return;
68376
+ }
68377
+ say(`${DIM2}[${ts2}]${RESET2} ${BLUE}[${ev.type}]${RESET2} ${previewPayload(p)}`);
68378
+ }
68379
+ function renderEventBody(ev, ts2, p, _myLogin) {
68380
+ applyWorkingHeartbeat(ev);
68381
+ if (ev.kind === "chat") {
68382
+ renderChatBody(ev, ts2, p);
68383
+ return;
68384
+ }
68385
+ renderTailBody(ev, ts2, p);
68386
+ }
68387
+ function isTextBlock(v) {
68388
+ return typeof v === "object" && v !== null && v.type === "text" && typeof v.text === "string";
68389
+ }
68390
+ function stringifyToolResponseScalar(v) {
68290
68391
  if (v === null || v === void 0) return "";
68291
68392
  if (typeof v === "string") return v.trim();
68292
68393
  if (typeof v === "number" || typeof v === "boolean") return String(v);
68293
- if (typeof v === "object" && v !== null && v.type === "text" && typeof v.text === "string") {
68294
- return v.text.trim();
68394
+ return null;
68395
+ }
68396
+ function stringifyToolResponseArray(v) {
68397
+ const texts = [];
68398
+ for (const b of v) if (isTextBlock(b)) texts.push(b.text);
68399
+ if (texts.length === 0) return null;
68400
+ return texts.join("\n").trim();
68401
+ }
68402
+ function stringifyToolResponseFallback(v) {
68403
+ try {
68404
+ return JSON.stringify(v);
68405
+ } catch {
68406
+ return String(v);
68295
68407
  }
68408
+ }
68409
+ function stringifyToolResponse(v) {
68410
+ const scalar = stringifyToolResponseScalar(v);
68411
+ if (scalar !== null) return scalar;
68412
+ if (isTextBlock(v)) return v.text.trim();
68296
68413
  if (Array.isArray(v)) {
68297
- const texts = v.filter((b) => b && typeof b === "object" && b.type === "text" && typeof b.text === "string").map((b) => b.text);
68298
- if (texts.length) return texts.join("\n").trim();
68414
+ const out = stringifyToolResponseArray(v);
68415
+ if (out !== null) return out;
68299
68416
  }
68300
68417
  if (typeof v === "object" && v !== null && Array.isArray(v.content)) {
68301
68418
  return stringifyToolResponse(v.content);
68302
68419
  }
68303
- try {
68304
- return JSON.stringify(v);
68305
- } catch {
68306
- return String(v);
68307
- }
68420
+ return stringifyToolResponseFallback(v);
68308
68421
  }
68309
68422
  function renderToolInput(p) {
68310
68423
  const input = p.tool_input ?? p.toolInput;
@@ -68337,71 +68450,75 @@ function fmtAgo(iso) {
68337
68450
  return fmtDuration(Date.now() - new Date(iso).getTime());
68338
68451
  }
68339
68452
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
68340
- async function resolveSessionId(idOrName, opts = {}) {
68341
- if (UUID_RE.test(idOrName)) return idOrName;
68342
- const needle = idOrName.startsWith("@") ? idOrName : "@" + idOrName;
68453
+ function splitRoutingRef2(ref) {
68454
+ const needle = ref.startsWith("@") ? ref : "@" + ref;
68343
68455
  const slash = needle.indexOf("/");
68344
- let ownerLogin = null;
68345
- let slug;
68346
68456
  if (slash > 0) {
68347
- ownerLogin = needle.slice(1, slash);
68348
- slug = "@" + needle.slice(slash + 1).replace(/^@/, "");
68349
- } else {
68350
- slug = needle;
68457
+ return {
68458
+ ownerLogin: needle.slice(1, slash),
68459
+ slug: "@" + needle.slice(slash + 1).replace(/^@/, "")
68460
+ };
68351
68461
  }
68352
- const data = await api.get("/api/v1/sessions");
68353
- let candidates = data.items.filter((s) => s.routingName === slug);
68354
- if (ownerLogin !== null) {
68355
- candidates = candidates.filter((s) => s.startedByLogin === ownerLogin);
68462
+ return { ownerLogin: null, slug: needle };
68463
+ }
68464
+ function filterByRoutingParts(items, parts) {
68465
+ let candidates = items.filter((s) => s.routingName === parts.slug);
68466
+ if (parts.ownerLogin !== null) {
68467
+ candidates = candidates.filter((s) => s.startedByLogin === parts.ownerLogin);
68356
68468
  } else {
68357
68469
  const mine = candidates.filter((s) => s.role === "owner");
68358
68470
  if (mine.length > 0) candidates = mine;
68359
68471
  }
68360
- if (candidates.length === 0) {
68361
- const err = new Error(
68362
- ownerLogin ? `no session @${ownerLogin}/${slug.slice(1)} (run \`claw session list\`)` : `no session with routing name ${slug} (run \`claw session list\`)`
68363
- );
68364
- err.code = "CLW_NO_ROUTING_MATCH";
68365
- throw err;
68366
- }
68472
+ return candidates;
68473
+ }
68474
+ function noRoutingMatchError(parts) {
68475
+ const err = new Error(
68476
+ parts.ownerLogin ? `no session @${parts.ownerLogin}/${parts.slug.slice(1)} (run \`claw session list\`)` : `no session with routing name ${parts.slug} (run \`claw session list\`)`
68477
+ );
68478
+ err.code = "CLW_NO_ROUTING_MATCH";
68479
+ return err;
68480
+ }
68481
+ function ambiguousError(idOrName, e) {
68482
+ const usedQualified = idOrName.includes("/");
68483
+ const advice = usedQualified ? `'${idOrName}' is ambiguous even within owner \u2014 multiple sessions share the same routing name (different cwds with matching basenames). Re-run with a UUID:` : `'${idOrName}' is ambiguous \u2014 re-run with the qualified @owner/slug form, or with a UUID if even that collides:`;
68484
+ const err = new Error(
68485
+ advice + "\n" + e.candidates.map((c) => ` ${c.id} @${c.startedByLogin}/${(c.routingName ?? "").replace(/^@/, "")} ${c.cwd ?? ""}`).join("\n")
68486
+ );
68487
+ err.code = "CLW_AMBIGUOUS";
68488
+ return err;
68489
+ }
68490
+ async function resolveSessionId(idOrName, opts = {}) {
68491
+ if (UUID_RE.test(idOrName)) return idOrName;
68492
+ const parts = splitRoutingRef2(idOrName);
68493
+ const data = await api.get("/api/v1/sessions");
68494
+ const candidates = filterByRoutingParts(data.items, parts);
68495
+ if (candidates.length === 0) throw noRoutingMatchError(parts);
68367
68496
  try {
68368
68497
  const picked = await pickCandidate(idOrName, candidates, { destructive: !!opts.destructive });
68369
68498
  return picked.id;
68370
68499
  } catch (e) {
68371
- if (e instanceof AmbiguousError) {
68372
- const usedQualified = idOrName.includes("/");
68373
- const advice = usedQualified ? `'${idOrName}' is ambiguous even within owner \u2014 multiple sessions share the same routing name (different cwds with matching basenames). Re-run with a UUID:` : `'${idOrName}' is ambiguous \u2014 re-run with the qualified @owner/slug form, or with a UUID if even that collides:`;
68374
- const err = new Error(
68375
- advice + "\n" + e.candidates.map((c) => ` ${c.id} @${c.startedByLogin}/${(c.routingName ?? "").replace(/^@/, "")} ${c.cwd ?? ""}`).join("\n")
68376
- );
68377
- err.code = "CLW_AMBIGUOUS";
68378
- throw err;
68379
- }
68500
+ if (e instanceof AmbiguousError) throw ambiguousError(idOrName, e);
68380
68501
  throw e;
68381
68502
  }
68382
68503
  }
68383
- var sessionList = new Command("list").alias("ls").description("list sessions you can see").option("--connected", "only sessions whose channel WS is currently open").option("--all", "include archived sessions").action(async (opts) => {
68504
+ function buildSessionListQs(opts) {
68384
68505
  const qs = new URLSearchParams();
68385
68506
  if (opts.connected) qs.set("connected", "true");
68386
68507
  if (opts.all) qs.set("archived", "true");
68387
- const data = await api.get(
68388
- "/api/v1/sessions" + (qs.toString() ? "?" + qs : "")
68389
- );
68390
- if (data.items.length === 0) {
68391
- console.log("no sessions");
68392
- return;
68393
- }
68394
- for (const s of data.items) {
68395
- const dot = s.connected ? "\u25CF" : "\u25CB";
68396
- const slug = s.routingName ?? "";
68397
- const qualified = slug ? `@${s.startedByLogin}/${slug.replace(/^@/, "")}` : "(no routing name)";
68398
- const where = s.cwd ? ` ${s.cwd}` : "";
68399
- const role = s.role.padEnd(8);
68400
- const seen = s.connected ? "online" : `offline \xB7 ${fmtAgo(s.lastSeenAt)}`;
68401
- const arch = s.archivedAt ? " \xB7 ARCHIVED" : "";
68402
- console.log(`${dot} ${qualified.padEnd(28)} ${role}${where} [${seen}]${arch}`);
68403
- console.log(` id: ${s.id}`);
68404
- }
68508
+ return qs.toString() ? "?" + qs : "";
68509
+ }
68510
+ function printSessionListRow(s) {
68511
+ const dot = s.connected ? "\u25CF" : "\u25CB";
68512
+ const slug = s.routingName ?? "";
68513
+ const qualified = slug ? `@${s.startedByLogin}/${slug.replace(/^@/, "")}` : "(no routing name)";
68514
+ const where = s.cwd ? ` ${s.cwd}` : "";
68515
+ const role = s.role.padEnd(8);
68516
+ const seen = s.connected ? "online" : `offline \xB7 ${fmtAgo(s.lastSeenAt)}`;
68517
+ const arch = s.archivedAt ? " \xB7 ARCHIVED" : "";
68518
+ console.log(`${dot} ${qualified.padEnd(28)} ${role}${where} [${seen}]${arch}`);
68519
+ console.log(` id: ${s.id}`);
68520
+ }
68521
+ function printSessionListFooter() {
68405
68522
  console.log("");
68406
68523
  console.log(" attach to a session: claw session attach <ref>");
68407
68524
  console.log(" recent hook/chat: claw session events <ref>");
@@ -68409,6 +68526,17 @@ var sessionList = new Command("list").alias("ls").description("list sessions you
68409
68526
  console.log(" <ref> = UUID, @owner/slug, @slug, or bare slug");
68410
68527
  console.log(" (PowerShell tip: use the bare-slug form \u2014 `@driver` is parsed as");
68411
68528
  console.log(" a splatting operator and stripped before reaching the CLI)");
68529
+ }
68530
+ var sessionList = new Command("list").alias("ls").description("list sessions you can see").option("--connected", "only sessions whose channel WS is currently open").option("--all", "include archived sessions").action(async (opts) => {
68531
+ const data = await api.get(
68532
+ "/api/v1/sessions" + buildSessionListQs(opts)
68533
+ );
68534
+ if (data.items.length === 0) {
68535
+ console.log("no sessions");
68536
+ return;
68537
+ }
68538
+ for (const s of data.items) printSessionListRow(s);
68539
+ printSessionListFooter();
68412
68540
  });
68413
68541
  var sessionInfo = new Command("info").description("show metadata for a single session").argument("<ref>", "session UUID or @routingName").action(async (ref) => {
68414
68542
  const id = await resolveSessionId(ref);
@@ -68424,17 +68552,32 @@ var sessionInfo = new Command("info").description("show metadata for a single se
68424
68552
  console.log(`last seen: ${s.lastSeenAt}`);
68425
68553
  console.log(`status : ${s.connected ? "connected" : "offline"}${s.archivedAt ? " \xB7 ARCHIVED" : ""}`);
68426
68554
  });
68555
+ function buildEventsQs(opts) {
68556
+ const qs = new URLSearchParams({ limit: opts.limit ?? "200" });
68557
+ if (opts.after) qs.set("after", opts.after);
68558
+ if (opts.before) qs.set("before", opts.before);
68559
+ if (opts.kind) qs.set("kind", opts.kind);
68560
+ if (opts.type) qs.set("type", opts.type);
68561
+ return qs;
68562
+ }
68563
+ function printEventRow(ev) {
68564
+ const ts2 = ev.ts.slice(11, 19);
68565
+ const text = ev.payload?.text ?? ev.payload?.preview ?? "";
68566
+ const preview = typeof text === "string" && text ? (text.length > 200 ? text.slice(0, 200) + "\u2026" : text).replace(/\s+/g, " ") : "";
68567
+ console.log(`#${String(ev.id).padStart(5)} ${ts2} ${ev.kind.padEnd(4)} ${ev.type.padEnd(20)} ${preview}`);
68568
+ }
68569
+ function printPagedHasMore(data, json) {
68570
+ if (data.hasMore && !json) {
68571
+ console.log(`(more \u2014 older: --before ${data.firstId} \xB7 newer: --after ${data.lastId})`);
68572
+ }
68573
+ }
68427
68574
  var sessionEvents = new Command("events").description("dump recent events for a session (history; non-TUI)").argument("<ref>", "session UUID or @routingName").option("--limit <n>", "max events to return (default 200, max 1000)", "200").option("--after <id>", "forward pagination: events with id > given").option("--before <id>", "backward pagination: events with id < given").option("--kind <k>", "filter to chat or tail").option("--type <t>", "filter by type (e.g. PreToolUse, reply)").option("--json", "emit one JSON object per line instead of human-readable").action(async (ref, opts) => {
68428
68575
  if (opts.after && opts.before) {
68429
68576
  console.error("error: use --after OR --before, not both");
68430
68577
  process.exit(2);
68431
68578
  }
68432
68579
  const id = await resolveSessionId(ref);
68433
- const qs = new URLSearchParams({ limit: opts.limit ?? "200" });
68434
- if (opts.after) qs.set("after", opts.after);
68435
- if (opts.before) qs.set("before", opts.before);
68436
- if (opts.kind) qs.set("kind", opts.kind);
68437
- if (opts.type) qs.set("type", opts.type);
68580
+ const qs = buildEventsQs(opts);
68438
68581
  const data = await api.get(
68439
68582
  `/api/v1/sessions/${encodeURIComponent(id)}/events?${qs.toString()}`
68440
68583
  );
@@ -68447,24 +68590,28 @@ var sessionEvents = new Command("events").description("dump recent events for a
68447
68590
  console.log(JSON.stringify(ev));
68448
68591
  continue;
68449
68592
  }
68450
- const ts2 = ev.ts.slice(11, 19);
68451
- const text = ev.payload?.text ?? ev.payload?.preview ?? "";
68452
- const preview = typeof text === "string" && text ? (text.length > 200 ? text.slice(0, 200) + "\u2026" : text).replace(/\s+/g, " ") : "";
68453
- console.log(`#${String(ev.id).padStart(5)} ${ts2} ${ev.kind.padEnd(4)} ${ev.type.padEnd(20)} ${preview}`);
68454
- }
68455
- if (data.hasMore && !opts.json) {
68456
- console.log(`(more \u2014 older: --before ${data.firstId} \xB7 newer: --after ${data.lastId})`);
68593
+ printEventRow(ev);
68457
68594
  }
68595
+ printPagedHasMore(data, opts.json);
68458
68596
  });
68597
+ function buildMessagesQs(opts) {
68598
+ const qs = new URLSearchParams({ limit: opts.limit ?? "100" });
68599
+ if (opts.after) qs.set("after", opts.after);
68600
+ if (opts.before) qs.set("before", opts.before);
68601
+ return qs;
68602
+ }
68603
+ function printOpMessageRow(m) {
68604
+ const ts2 = m.ts.slice(11, 19);
68605
+ const flag = m.deletedAt ? " [deleted]" : m.editedAt ? " [edited]" : "";
68606
+ console.log(`#${String(m.id).padStart(5)} ${ts2} @${m.authorLogin.padEnd(20)} ${m.text}${flag}`);
68607
+ }
68459
68608
  var sessionMessages = new Command("messages").alias("msgs").description("dump operator-to-operator chat for a session (op-messages history)").argument("<ref>", "session UUID or @routingName").option("--limit <n>", "max messages to return (default 100, max 500)", "100").option("--after <id>", "forward pagination").option("--before <id>", "backward pagination").option("--json", "emit one JSON object per line").action(async (ref, opts) => {
68460
68609
  if (opts.after && opts.before) {
68461
68610
  console.error("error: use --after OR --before, not both");
68462
68611
  process.exit(2);
68463
68612
  }
68464
68613
  const id = await resolveSessionId(ref);
68465
- const qs = new URLSearchParams({ limit: opts.limit ?? "100" });
68466
- if (opts.after) qs.set("after", opts.after);
68467
- if (opts.before) qs.set("before", opts.before);
68614
+ const qs = buildMessagesQs(opts);
68468
68615
  const data = await api.get(
68469
68616
  `/api/v1/sessions/${encodeURIComponent(id)}/op-messages?${qs.toString()}`
68470
68617
  );
@@ -68477,13 +68624,9 @@ var sessionMessages = new Command("messages").alias("msgs").description("dump op
68477
68624
  console.log(JSON.stringify(m));
68478
68625
  continue;
68479
68626
  }
68480
- const ts2 = m.ts.slice(11, 19);
68481
- const flag = m.deletedAt ? " [deleted]" : m.editedAt ? " [edited]" : "";
68482
- console.log(`#${String(m.id).padStart(5)} ${ts2} @${m.authorLogin.padEnd(20)} ${m.text}${flag}`);
68483
- }
68484
- if (data.hasMore && !opts.json) {
68485
- console.log(`(more \u2014 older: --before ${data.firstId} \xB7 newer: --after ${data.lastId})`);
68627
+ printOpMessageRow(m);
68486
68628
  }
68629
+ printPagedHasMore(data, opts.json);
68487
68630
  });
68488
68631
  var sessionArchive = new Command("archive").description("soft-delete a session (sets archivedAt). Note: register-time reconnect unarchives, so a session whose UUID is still in a project's .claude/clawborrator/session.json will resurrect on its next start.").argument("<ref>", "session UUID or @routingName").action(async (ref) => {
68489
68632
  const id = await resolveSessionId(ref);
@@ -68497,14 +68640,12 @@ var sessionArchive = new Command("archive").description("soft-delete a session (
68497
68640
  console.log(`\u2713 archived ${r.sessionId} at ${r.archivedAt}`);
68498
68641
  }
68499
68642
  });
68500
- var sessionPrune = new Command("prune").description("hard-delete duplicate session rows that share a routing name. The live (or most-recently-seen) row is kept; the rest are removed along with their events / op-messages / shares (FK cascade). Use --dry-run first if unsure.").option("--dry-run", "show what would be deleted without writing").option("--routing <name>", "narrow to a single routing name (e.g. @driver)").action(async (opts) => {
68643
+ function buildPruneBody(opts) {
68501
68644
  const body = { dryRun: !!opts.dryRun };
68502
68645
  if (opts.routing) body.routingName = opts.routing.startsWith("@") ? opts.routing : "@" + opts.routing;
68503
- const r = await api.post(`/api/v1/sessions/prune`, body);
68504
- if (r.deleted.length === 0) {
68505
- console.log("nothing to prune (no routing-name duplicates).");
68506
- return;
68507
- }
68646
+ return body;
68647
+ }
68648
+ function printPruneResult(r) {
68508
68649
  const verb = r.dryRun ? "would delete" : "deleted";
68509
68650
  console.log(`${verb} ${r.deleted.length} duplicate${r.deleted.length === 1 ? "" : "s"}:`);
68510
68651
  for (const d of r.deleted) {
@@ -68516,6 +68657,14 @@ var sessionPrune = new Command("prune").description("hard-delete duplicate sessi
68516
68657
  console.log(` \u2713 ${k.routingName.padEnd(20)} ${k.sessionId}`);
68517
68658
  }
68518
68659
  if (r.dryRun) console.log("\n(--dry-run \u2014 re-run without it to apply)");
68660
+ }
68661
+ var sessionPrune = new Command("prune").description("hard-delete duplicate session rows that share a routing name. The live (or most-recently-seen) row is kept; the rest are removed along with their events / op-messages / shares (FK cascade). Use --dry-run first if unsure.").option("--dry-run", "show what would be deleted without writing").option("--routing <name>", "narrow to a single routing name (e.g. @driver)").action(async (opts) => {
68662
+ const r = await api.post(`/api/v1/sessions/prune`, buildPruneBody(opts));
68663
+ if (r.deleted.length === 0) {
68664
+ console.log("nothing to prune (no routing-name duplicates).");
68665
+ return;
68666
+ }
68667
+ printPruneResult(r);
68519
68668
  });
68520
68669
  var sessionDelete = new Command("delete").description("hard-delete a single session \u2014 cascades events / op-messages / shares / files (refcount-sweeps blobs). Irreversible. Use `archive` for the soft form (auto-resurrects on reconnect). Prompts if the routing name matches more than one row, even when only one is online \u2014 both are equally permanent to delete.").argument("<ref>", "session UUID or @routingName").option("--hard", "required: confirm you want a permanent delete (no soft form is offered without this flag)").action(async (ref, opts) => {
68521
68670
  if (!opts.hard) {
@@ -68632,7 +68781,28 @@ var sessionUnshareCmd = new Command("unshare").description("revoke a user's shar
68632
68781
  console.log(`\u2717 revoked @${out.login}'s access to ${out.sessionId.slice(0, 8)}\u2026`);
68633
68782
  }
68634
68783
  });
68635
- var sessionCmd = new Command("session").description("manage Claude Code sessions registered with this hub").addCommand(sessionList).addCommand(sessionInfo).addCommand(sessionAttach).addCommand(sessionEvents).addCommand(sessionMessages).addCommand(sessionArchive).addCommand(sessionPrune).addCommand(sessionPrompt).addCommand(sessionDelete).addCommand(sessionShareCmd).addCommand(sessionSharesCmd).addCommand(sessionUnshareCmd).addCommand(sessionFiles).addCommand(sessionFileRm);
68784
+ var sessionKill = new Command("kill").description("kill the CC process for a managed session (keeps the session row)").argument("<ref>", "session UUID, @routingName, or @owner/slug").action(async (ref) => {
68785
+ const id = await resolveSessionId(ref, { destructive: true });
68786
+ await api.post(`/api/v1/sessions/${encodeURIComponent(id)}/kill`, {});
68787
+ console.log(`\u2717 killed CC process for ${id.slice(0, 8)}\u2026`);
68788
+ });
68789
+ var sessionRestart = new Command("restart").description("kill + respawn the CC process for a managed session").argument("<ref>", "session UUID, @routingName, or @owner/slug").action(async (ref) => {
68790
+ const id = await resolveSessionId(ref, { destructive: true });
68791
+ const out = await api.post(
68792
+ `/api/v1/sessions/${encodeURIComponent(id)}/restart`,
68793
+ {}
68794
+ );
68795
+ console.log(`\u21BA restarted: ${out.sessionId}`);
68796
+ });
68797
+ var sessionScreenshot = new Command("screenshot").description("print the current rendered terminal frame for a managed session").argument("<ref>", "session UUID, @routingName, or @owner/slug").action(async (ref) => {
68798
+ const id = await resolveSessionId(ref);
68799
+ const out = await api.get(
68800
+ `/api/v1/sessions/${encodeURIComponent(id)}/screenshot`
68801
+ );
68802
+ console.error(`(${out.cols}\xD7${out.rows} terminal \u2014 cursor at ${out.cursor?.row ?? "?"},${out.cursor?.col ?? "?"})`);
68803
+ process.stdout.write(out.text.endsWith("\n") ? out.text : out.text + "\n");
68804
+ });
68805
+ var sessionCmd = new Command("session").description("manage Claude Code sessions registered with this hub").addCommand(sessionList).addCommand(sessionInfo).addCommand(sessionAttach).addCommand(sessionEvents).addCommand(sessionMessages).addCommand(sessionArchive).addCommand(sessionPrune).addCommand(sessionPrompt).addCommand(sessionDelete).addCommand(sessionShareCmd).addCommand(sessionSharesCmd).addCommand(sessionUnshareCmd).addCommand(sessionFiles).addCommand(sessionFileRm).addCommand(sessionKill).addCommand(sessionRestart).addCommand(sessionScreenshot);
68636
68806
 
68637
68807
  // src/commands/token.ts
68638
68808
  var import_node_fs2 = require("node:fs");
@@ -68796,27 +68966,34 @@ var webhookRm = new Command("rm").alias("delete").description("remove a webhook
68796
68966
  var webhookCmd = new Command("webhook").description("manage webhook subscriptions").addCommand(webhookAdd).addCommand(webhookList).addCommand(webhookTest).addCommand(webhookRm);
68797
68967
 
68798
68968
  // src/commands/agents.ts
68799
- var agentsList = new Command("list").alias("ls").description("list published agents (default) or your own agents (--mine)").option("--mine", "list every agent you own, including drafts").option("--owner <login>", "list a specific creator's published agents").option("--q <text>", "substring match on handle / name / tagline").action(async (opts) => {
68969
+ function buildAgentsListQs(opts) {
68800
68970
  const params = new URLSearchParams();
68801
68971
  if (opts.mine) params.set("mine", "true");
68802
68972
  if (opts.owner) params.set("owner", opts.owner);
68803
68973
  if (opts.q) params.set("q", opts.q);
68804
- const qs = params.toString() ? "?" + params.toString() : "";
68805
- const data = await api.get(`/api/v1/agents${qs}`);
68974
+ return params.toString() ? "?" + params.toString() : "";
68975
+ }
68976
+ function printAgentRow(a) {
68977
+ const dot = a.online ? "\u25CF" : "\u25CB";
68978
+ const tag2 = a.status === "draft" ? " [draft]" : "";
68979
+ const iso = a.isolated ? " [isolated]" : " [composable]";
68980
+ const stats = `${a.queriesAllTime} queries`;
68981
+ const tagln = a.tagline ? ` \u2014 ${a.tagline}` : "";
68982
+ console.log(`${dot} @${a.handle}${tag2}${iso} ${a.name} ${stats}${tagln}`);
68983
+ }
68984
+ var agentsList = new Command("list").alias("ls").description("list published agents (default) or your own agents (--mine)").option("--mine", "list every agent you own, including drafts").option("--owner <login>", "list a specific creator's published agents").option("--q <text>", "substring match on handle / name / tagline").action(async (opts) => {
68985
+ const data = await api.get(`/api/v1/agents${buildAgentsListQs(opts)}`);
68806
68986
  if (data.items.length === 0) {
68807
68987
  console.log("no agents");
68808
68988
  return;
68809
68989
  }
68810
- for (const a of data.items) {
68811
- const dot = a.online ? "\u25CF" : "\u25CB";
68812
- const tag2 = a.status === "draft" ? " [draft]" : "";
68813
- const iso = a.isolated ? " [isolated]" : " [composable]";
68814
- const stats = `${a.queriesAllTime} queries`;
68815
- const tagln = a.tagline ? ` \u2014 ${a.tagline}` : "";
68816
- console.log(`${dot} @${a.handle}${tag2}${iso} ${a.name} ${stats}${tagln}`);
68817
- }
68990
+ for (const a of data.items) printAgentRow(a);
68818
68991
  });
68819
68992
  var agentsPublish = new Command("publish").description("publish a session as a public agent").requiredOption("--session <id>", "the session UUID to back the agent").requiredOption("--name <name>", 'display name (e.g. "viper-rust-expert")').option("--tagline <text>", "one-line description (160 chars max)").option("--description <text>", "long-form description, markdown allowed (4 KB max)").option("--slug <slug>", "explicit slug (default: derived from session routingName)").option("--draft", "publish as draft (status=draft); use --published to go live immediately").option("--published", "publish as live (status=published)").option("--budget <n>", "daily budget in queries (default 1000, max 100000)", (v) => parseInt(v, 10)).option("--concurrency <n>", "concurrent in-flight queries cap (default 5, max 20)", (v) => parseInt(v, 10)).option("--isolated", "isolated mode: agent CC cannot use cross-session routing tools while answering (default true; safer)").option("--composable", "composable mode: agent CC may use cross-session routing tools (gated against the requester's own access)").action(async (opts) => {
68993
+ const r = await api.post("/api/v1/agents", buildPublishBody(opts));
68994
+ printPublishResult(r);
68995
+ });
68996
+ function buildPublishBody(opts) {
68820
68997
  const status = opts.published ? "published" : "draft";
68821
68998
  const body = {
68822
68999
  sessionId: opts.session,
@@ -68830,7 +69007,9 @@ var agentsPublish = new Command("publish").description("publish a session as a p
68830
69007
  if (typeof opts.concurrency === "number") body.concurrencyCap = opts.concurrency;
68831
69008
  if (opts.composable) body.isolated = false;
68832
69009
  else if (opts.isolated) body.isolated = true;
68833
- const r = await api.post("/api/v1/agents", body);
69010
+ return body;
69011
+ }
69012
+ function printPublishResult(r) {
68834
69013
  console.log(`\u2713 ${r.restored ? "restored" : "published"} agent: @${r.handle}`);
68835
69014
  console.log(` name: ${r.name}`);
68836
69015
  console.log(` status: ${r.status}`);
@@ -68842,19 +69021,11 @@ var agentsPublish = new Command("publish").description("publish a session as a p
68842
69021
  } else {
68843
69022
  console.log(` call as: '@${r.handle} <question>' from any session prompt`);
68844
69023
  }
68845
- });
69024
+ }
68846
69025
  var agentsUpdate = new Command("update").description("update an agent").argument("<handle>", "@owner/slug").option("--status <s>", "draft | published").option("--name <name>").option("--tagline <text>").option("--description <text>").option("--budget <n>", "daily budget in queries", (v) => parseInt(v, 10)).option("--concurrency <n>", "concurrency cap", (v) => parseInt(v, 10)).option("--isolated", "switch to isolated mode (block cross-session routing while answering)").option("--composable", "switch to composable mode (allow cross-session routing tools)").action(async (handleArg, opts) => {
68847
69026
  const handle = handleArg.replace(/^@/, "");
68848
69027
  const agent = await api.get(`/api/v1/agents/by-handle/${encodeURIComponent(handle.split("/")[0])}/${encodeURIComponent(handle.split("/")[1] ?? "")}`);
68849
- const body = {};
68850
- if (opts.status) body.status = opts.status;
68851
- if (opts.name) body.name = opts.name;
68852
- if (opts.tagline) body.tagline = opts.tagline;
68853
- if (opts.description) body.description = opts.description;
68854
- if (typeof opts.budget === "number") body.dailyBudgetQueries = opts.budget;
68855
- if (typeof opts.concurrency === "number") body.concurrencyCap = opts.concurrency;
68856
- if (opts.composable) body.isolated = false;
68857
- else if (opts.isolated) body.isolated = true;
69028
+ const body = buildUpdateBody(opts);
68858
69029
  if (Object.keys(body).length === 0) {
68859
69030
  console.error("no fields to update");
68860
69031
  process.exit(2);
@@ -68865,6 +69036,18 @@ var agentsUpdate = new Command("update").description("update an agent").argument
68865
69036
  console.log(` mode: ${r.isolated ? "isolated" : "composable"}`);
68866
69037
  console.log(` budget: ${r.dailyBudgetQueries}/day, concurrency ${r.concurrencyCap}`);
68867
69038
  });
69039
+ function buildUpdateBody(opts) {
69040
+ const body = {};
69041
+ if (opts.status) body.status = opts.status;
69042
+ if (opts.name) body.name = opts.name;
69043
+ if (opts.tagline) body.tagline = opts.tagline;
69044
+ if (opts.description) body.description = opts.description;
69045
+ if (typeof opts.budget === "number") body.dailyBudgetQueries = opts.budget;
69046
+ if (typeof opts.concurrency === "number") body.concurrencyCap = opts.concurrency;
69047
+ if (opts.composable) body.isolated = false;
69048
+ else if (opts.isolated) body.isolated = true;
69049
+ return body;
69050
+ }
68868
69051
  var agentsUnpublish = new Command("unpublish").description("soft-delete an agent (drops its handle)").argument("<handle>", "@owner/slug").action(async (handleArg) => {
68869
69052
  const handle = handleArg.replace(/^@/, "");
68870
69053
  const [owner, slug] = handle.split("/");
@@ -68885,28 +69068,309 @@ var agentsInbound = new Command("inbound").description("audit view: who has been
68885
69068
  }
68886
69069
  const agent = await api.get(`/api/v1/agents/by-handle/${encodeURIComponent(owner)}/${encodeURIComponent(slug)}`);
68887
69070
  const data = await api.get(`/api/v1/agents/${agent.id}/inbound?days=${opts.days}`);
69071
+ printInboundReport(data);
69072
+ });
69073
+ function printInboundReport(data) {
68888
69074
  console.log(`@${data.agent.handle} (${data.window.days}-day window)`);
68889
69075
  console.log(` total: ${data.summary.total} ok: ${data.summary.ok} denied: ${data.summary.denied} avg-latency: ${data.summary.avgLatencyMs ?? "\u2014"}ms askers: ${data.summary.distinctAskers}`);
68890
- if (data.topAskers.length) {
68891
- console.log("");
68892
- console.log("top askers:");
68893
- for (const t of data.topAskers) {
68894
- console.log(` @${t.login.padEnd(20)} ${String(t.count).padStart(4)} queries last: ${t.lastAt}`);
69076
+ if (data.topAskers.length) printInboundTopAskers(data.topAskers);
69077
+ if (data.recent.length) printInboundRecent(data.recent);
69078
+ }
69079
+ function printInboundTopAskers(items) {
69080
+ console.log("");
69081
+ console.log("top askers:");
69082
+ for (const t of items) {
69083
+ console.log(` @${t.login.padEnd(20)} ${String(t.count).padStart(4)} queries last: ${t.lastAt}`);
69084
+ }
69085
+ }
69086
+ function printInboundRecent(items) {
69087
+ console.log("");
69088
+ console.log("recent:");
69089
+ for (const r of items.slice(0, 20)) {
69090
+ const flag = r.ok ? "\u2713" : "\u2717";
69091
+ const lat = r.latencyMs != null ? ` ${r.latencyMs}ms` : "";
69092
+ const why = r.deniedReason ? ` [${r.deniedReason}]` : "";
69093
+ const q = r.question.length > 70 ? r.question.slice(0, 67) + "\u2026" : r.question;
69094
+ console.log(` ${flag} ${r.ts} @${r.askerLogin}${lat}${why} ${q}`);
69095
+ }
69096
+ }
69097
+ var agentsCmd = new Command("agents").description("public expert agents \u2014 list, publish, update, audit").addCommand(agentsList).addCommand(agentsPublish).addCommand(agentsUpdate).addCommand(agentsUnpublish).addCommand(agentsInbound);
69098
+
69099
+ // src/commands/apps.ts
69100
+ var import_promises = require("node:readline/promises");
69101
+ var import_node_http2 = require("node:http");
69102
+ var import_node_crypto2 = require("node:crypto");
69103
+ var import_node_child_process2 = require("node:child_process");
69104
+ function fmtAgo3(iso) {
69105
+ if (!iso) return "never";
69106
+ let s = iso;
69107
+ if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(s)) s = s.replace(" ", "T") + "Z";
69108
+ const ms = Date.now() - new Date(s).getTime();
69109
+ if (!Number.isFinite(ms)) return "\u2014";
69110
+ if (ms < 0) return "just now";
69111
+ if (ms < 6e4) return Math.max(1, Math.floor(ms / 1e3)) + "s ago";
69112
+ if (ms < 36e5) return Math.floor(ms / 6e4) + "m ago";
69113
+ if (ms < 864e5) return Math.floor(ms / 36e5) + "h ago";
69114
+ return Math.floor(ms / 864e5) + "d ago";
69115
+ }
69116
+ function base64url2(buf) {
69117
+ return buf.toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
69118
+ }
69119
+ function openBrowser2(url) {
69120
+ const platform2 = process.platform;
69121
+ try {
69122
+ if (platform2 === "win32") {
69123
+ (0, import_node_child_process2.spawn)("cmd", ["/c", "start", '""', `"${url}"`], {
69124
+ stdio: "ignore",
69125
+ detached: true,
69126
+ windowsVerbatimArguments: true
69127
+ }).unref();
69128
+ } else {
69129
+ const cmd = platform2 === "darwin" ? "open" : "xdg-open";
69130
+ (0, import_node_child_process2.spawn)(cmd, [url], { stdio: "ignore", detached: true }).unref();
68895
69131
  }
69132
+ } catch {
68896
69133
  }
68897
- if (data.recent.length) {
68898
- console.log("");
68899
- console.log("recent:");
68900
- for (const r of data.recent.slice(0, 20)) {
68901
- const flag = r.ok ? "\u2713" : "\u2717";
68902
- const lat = r.latencyMs != null ? ` ${r.latencyMs}ms` : "";
68903
- const why = r.deniedReason ? ` [${r.deniedReason}]` : "";
68904
- const q = r.question.length > 70 ? r.question.slice(0, 67) + "\u2026" : r.question;
68905
- console.log(` ${flag} ${r.ts} @${r.askerLogin}${lat}${why} ${q}`);
69134
+ }
69135
+ var appsMint = new Command("mint").description("mint a new SPA app token (cw_app_\u2026) for testing \u2014 equivalent to walking the OAuth flow but uses the CLI's already-authenticated session").argument("<name>", "human-readable label for the token, shown in `claw apps list`").action(async (name) => {
69136
+ const out = await api.post("/api/v1/auth/apps/mint", { name });
69137
+ console.log(`\u2713 minted app token "${out.tokenName}"`);
69138
+ console.log(out.token);
69139
+ console.log("");
69140
+ console.log("Stash this somewhere safe \u2014 it can't be retrieved later. Use as `Authorization: Bearer " + out.token + "` for any /api/v1/* call. Revoke with `claw apps revoke <id>` (id from `claw apps list`).");
69141
+ });
69142
+ function printAppRow(t) {
69143
+ const created = `created ${fmtAgo3(t.createdAt)}`;
69144
+ const lastUsed = `last used ${fmtAgo3(t.lastUsedAt)}`;
69145
+ const revoked = t.revokedAt ? ` REVOKED ${fmtAgo3(t.revokedAt)}` : "";
69146
+ const label = (t.appName ?? t.name ?? "").padEnd(28);
69147
+ console.log(`@${String(t.id).padStart(4)} ${label} ${created} ${lastUsed}${revoked}`);
69148
+ }
69149
+ var appsList = new Command("list").alias("ls").description("list this user's SPA app tokens (kind=app). Active only by default; --all to include revoked.").option("--all", "include revoked tokens").action(async (opts) => {
69150
+ const qs = opts.all ? "?includeRevoked=true" : "";
69151
+ const data = await api.get("/api/v1/tokens" + qs);
69152
+ const apps = data.items.filter((t) => t.kind === "app");
69153
+ if (apps.length === 0) {
69154
+ console.log("no app tokens");
69155
+ return;
69156
+ }
69157
+ for (const t of apps) printAppRow(t);
69158
+ });
69159
+ async function confirmYesNo(prompt) {
69160
+ if (!process.stdin.isTTY) {
69161
+ console.error("error: not a TTY \u2014 pass --yes to skip the confirmation prompt");
69162
+ return false;
69163
+ }
69164
+ const rl = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
69165
+ try {
69166
+ const ans = (await rl.question(prompt + " [y/N] ")).trim().toLowerCase();
69167
+ return ans === "y" || ans === "yes";
69168
+ } finally {
69169
+ rl.close();
69170
+ }
69171
+ }
69172
+ var appsRevoke = new Command("revoke").description("revoke an app token by id (from `claw apps list`). Idempotent on the server, but prompts here unless --yes.").argument("<id>", "token id (the @<num> column from `claw apps list`)").option("--yes", "skip the y/N confirmation prompt").action(async (idArg, opts) => {
69173
+ const id = Number(idArg.replace(/^@/, ""));
69174
+ if (!Number.isInteger(id) || id <= 0) {
69175
+ console.error("error: id must be a positive integer");
69176
+ process.exit(2);
69177
+ }
69178
+ if (!opts.yes) {
69179
+ const ok = await confirmYesNo(`revoke app token #${id}?`);
69180
+ if (!ok) {
69181
+ console.log("aborted");
69182
+ return;
69183
+ }
69184
+ }
69185
+ try {
69186
+ await api.delete(`/api/v1/tokens/${id}`);
69187
+ console.log(`\u2713 revoked app token #${id}`);
69188
+ } catch (e) {
69189
+ if (e instanceof ApiError) {
69190
+ console.error(`error: ${e.status} ${e.code} \u2014 ${e.message}`);
69191
+ process.exit(1);
68906
69192
  }
69193
+ throw e;
68907
69194
  }
68908
69195
  });
68909
- var agentsCmd = new Command("agents").description("public expert agents \u2014 list, publish, update, audit").addCommand(agentsList).addCommand(agentsPublish).addCommand(agentsUpdate).addCommand(agentsUnpublish).addCommand(agentsInbound);
69196
+ function awaitSpaCallback(server) {
69197
+ return new Promise((resolve3, reject) => {
69198
+ server.on("request", (req, res) => {
69199
+ res.setHeader("Connection", "close");
69200
+ const u = new URL(req.url ?? "/", "http://localhost");
69201
+ if (u.pathname !== "/" && u.pathname !== "/callback") {
69202
+ res.statusCode = 404;
69203
+ res.end("not found");
69204
+ return;
69205
+ }
69206
+ const code = u.searchParams.get("code") ?? "";
69207
+ const state = u.searchParams.get("state") ?? "";
69208
+ const error = u.searchParams.get("error");
69209
+ if (error) {
69210
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
69211
+ res.end(`<html><body><h2>oauth error: ${error}</h2><p>you can close this tab.</p></body></html>`);
69212
+ reject(new Error(`oauth error: ${error}`));
69213
+ return;
69214
+ }
69215
+ if (!code || !state) {
69216
+ res.statusCode = 400;
69217
+ res.end("missing code or state");
69218
+ reject(new Error("callback missing code or state"));
69219
+ return;
69220
+ }
69221
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
69222
+ res.end("<html><body><h2>oauth code received</h2><p>you can close this tab and return to the terminal.</p><script>setTimeout(() => window.close(), 1500);</script></body></html>");
69223
+ resolve3({ code, state });
69224
+ });
69225
+ server.on("error", reject);
69226
+ });
69227
+ }
69228
+ async function spaTestOauthFlow(args) {
69229
+ const verifier = base64url2((0, import_node_crypto2.randomBytes)(32));
69230
+ const challenge = base64url2((0, import_node_crypto2.createHash)("sha256").update(verifier).digest());
69231
+ const stateNonce = base64url2((0, import_node_crypto2.randomBytes)(16));
69232
+ const appName = "claw-cli-test-oauth";
69233
+ const server = (0, import_node_http2.createServer)();
69234
+ await new Promise((resolve3, reject) => {
69235
+ server.once("error", reject);
69236
+ server.listen(args.port, "127.0.0.1", () => resolve3());
69237
+ });
69238
+ const actualPort = server.address().port;
69239
+ const redirectUri = `http://127.0.0.1:${actualPort}`;
69240
+ const startUrl = new URL("/api/v1/auth/spa/start", args.hubUrl);
69241
+ startUrl.searchParams.set("redirect_uri", redirectUri);
69242
+ startUrl.searchParams.set("state", stateNonce);
69243
+ startUrl.searchParams.set("code_challenge", challenge);
69244
+ startUrl.searchParams.set("code_challenge_method", "S256");
69245
+ startUrl.searchParams.set("app_name", appName);
69246
+ console.log("Opening browser to authorize\u2026");
69247
+ console.log(` if it doesn't open automatically, paste this URL into a browser:`);
69248
+ console.log(` ${startUrl}`);
69249
+ console.log("");
69250
+ openBrowser2(startUrl.toString());
69251
+ let cb;
69252
+ let timeoutHandle;
69253
+ try {
69254
+ cb = await Promise.race([
69255
+ awaitSpaCallback(server),
69256
+ new Promise((_, reject) => {
69257
+ timeoutHandle = setTimeout(
69258
+ () => reject(new Error("test-oauth timed out after 5 minutes (matches app_code TTL)")),
69259
+ 5 * 60 * 1e3
69260
+ );
69261
+ })
69262
+ ]);
69263
+ } finally {
69264
+ if (timeoutHandle) clearTimeout(timeoutHandle);
69265
+ server.close();
69266
+ server.closeAllConnections?.();
69267
+ }
69268
+ if (cb.state !== stateNonce) throw new Error("state mismatch \u2014 possible CSRF; aborting");
69269
+ const res = await fetch(args.hubUrl.replace(/\/$/, "") + "/api/v1/auth/spa/exchange", {
69270
+ method: "POST",
69271
+ headers: { "Content-Type": "application/json", "User-Agent": "clawborrator-cli" },
69272
+ body: JSON.stringify({ code: cb.code, code_verifier: verifier })
69273
+ });
69274
+ if (!res.ok) {
69275
+ const text = await res.text().catch(() => "");
69276
+ let parsed = null;
69277
+ try {
69278
+ parsed = JSON.parse(text);
69279
+ } catch {
69280
+ }
69281
+ throw new ApiError(res.status, parsed?.error ?? `http_${res.status}`, parsed?.message ?? parsed?.error ?? text);
69282
+ }
69283
+ const out = await res.json();
69284
+ return { ...out, appName };
69285
+ }
69286
+ var appsTestOauth = new Command("test-oauth").description("walk the full SPA OAuth+PKCE flow end-to-end as a debug tool. Mints a real `cw_app_\u2026` token; revoke afterwards via `claw apps revoke <id>` if you don't want it lying around.").option("--port <n>", "local listener port (default 8765)", (v) => parseInt(v, 10), 8765).action(async (opts) => {
69287
+ const cfg = loadConfig();
69288
+ const hubUrl = cfg.hubUrl.replace(/\/+$/, "");
69289
+ if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {
69290
+ console.error("error: --port must be an integer in 1..65535");
69291
+ process.exit(2);
69292
+ }
69293
+ console.log(`hub: ${hubUrl}`);
69294
+ try {
69295
+ const out = await spaTestOauthFlow({ hubUrl, port: opts.port });
69296
+ console.log("");
69297
+ console.log("\u2713 flow completed");
69298
+ console.log(`token: ${out.token}`);
69299
+ console.log(`appName: ${out.appName}`);
69300
+ console.log(`curl: curl -H 'Authorization: Bearer ${out.token}' ${hubUrl}/api/v1/me`);
69301
+ console.log("");
69302
+ console.log("Note: this token is real (counted against mint quotas etc). List with `claw apps list`,");
69303
+ console.log("and revoke via `claw apps revoke <id>` once you're done debugging.");
69304
+ } catch (e) {
69305
+ console.error("error: " + (e?.message ?? String(e)));
69306
+ process.exit(1);
69307
+ }
69308
+ });
69309
+ var appsCmd = new Command("apps").description("manage SPA app tokens (kind=app, `cw_app_\u2026`) \u2014 mint, list, revoke, and end-to-end-test the SPA OAuth+PKCE flow").addCommand(appsMint).addCommand(appsList).addCommand(appsRevoke).addCommand(appsTestOauth);
69310
+
69311
+ // src/commands/desktop.ts
69312
+ var desktopList = new Command("list").alias("ls").description("list desktop daemons registered for the current user").action(async () => {
69313
+ const data = await api.get("/api/v1/desktops");
69314
+ if (data.items.length === 0) {
69315
+ console.log("no registered desktops");
69316
+ return;
69317
+ }
69318
+ for (const d of data.items) {
69319
+ const dot = d.online ? "\u25CF" : "\u25CB";
69320
+ const host = d.hostname ?? "(unknown host)";
69321
+ const ver = d.daemonVersion ?? "?";
69322
+ console.log(`${dot} ${d.machineId.slice(0, 8)} ${host.padEnd(24)} v${ver.padEnd(8)} last-seen ${fmtAgo4(d.lastSeenAt)}`);
69323
+ }
69324
+ });
69325
+ var desktopCreate = new Command("create-session").description("ask a desktop daemon to spawn a managed CC session in a folder").argument("<machineId>", "desktop machine id (from `claw desktop list`)").argument("<folder>", "absolute path on the desktop where CC should be spawned").option("--routing-name <name>", "optional routing name for the new session (e.g. @frontend)").action(async (machineId, folder, opts) => {
69326
+ const body = { folder };
69327
+ if (opts.routingName) body.routingName = opts.routingName;
69328
+ const out = await api.post(
69329
+ `/api/v1/desktops/${encodeURIComponent(machineId)}/sessions`,
69330
+ body
69331
+ );
69332
+ console.log(`\u2713 session created: ${out.sessionId}`);
69333
+ });
69334
+ var desktopCmd = new Command("desktop").description("inspect + control desktop daemons (clawborrator-supervisor)").addCommand(desktopList).addCommand(desktopCreate);
69335
+ function fmtAgo4(iso) {
69336
+ const ms = Date.now() - new Date(iso).getTime();
69337
+ if (ms < 6e4) return Math.max(1, Math.floor(ms / 1e3)) + "s ago";
69338
+ if (ms < 36e5) return Math.floor(ms / 6e4) + "m ago";
69339
+ if (ms < 864e5) return Math.floor(ms / 36e5) + "h ago";
69340
+ return Math.floor(ms / 864e5) + "d ago";
69341
+ }
69342
+
69343
+ // src/commands/prompt-memory.ts
69344
+ var promptMemoryList = new Command("list").alias("ls").description("list remembered startup-prompt answers").option("--machine-id <id>", "filter to one machine").option("--folder <path>", "filter to one folder").action(async (opts) => {
69345
+ const qs = new URLSearchParams();
69346
+ if (opts.machineId) qs.set("machineId", opts.machineId);
69347
+ if (opts.folder) qs.set("folder", opts.folder);
69348
+ const url = "/api/v1/prompt-memory" + (qs.toString() ? "?" + qs : "");
69349
+ const data = await api.get(url);
69350
+ if (data.items.length === 0) {
69351
+ console.log("no remembered answers");
69352
+ return;
69353
+ }
69354
+ for (const m of data.items) {
69355
+ const where = [m.machineId?.slice(0, 8) ?? "*", m.folder ?? "*"].join(" ");
69356
+ const printableAnswer = JSON.stringify(m.answer);
69357
+ console.log(`${m.id.toString().padStart(4)} ${m.category.padEnd(28)} ${printableAnswer.padEnd(8)} ${where}`);
69358
+ }
69359
+ });
69360
+ var promptMemorySet = new Command("set").description("set or update a remembered answer (idempotent)").requiredOption("--category <category>", "e.g. startup_trust_folder, startup_use_mcp").requiredOption("--answer <bytes>", 'literal bytes the daemon should type, e.g. "y\\n"').option("--machine-id <id>", "scope to a specific machine; omit for all-machines").option("--folder <path>", "scope to a specific folder; omit for all-folders").action(async (opts) => {
69361
+ const out = await api.post("/api/v1/prompt-memory", {
69362
+ machineId: opts.machineId ?? null,
69363
+ folder: opts.folder ?? null,
69364
+ category: opts.category,
69365
+ answer: opts.answer
69366
+ });
69367
+ console.log(`\u2713 remembered #${out.id}: ${out.category} \u2192 ${JSON.stringify(out.answer)}`);
69368
+ });
69369
+ var promptMemoryForget = new Command("forget").description("forget a remembered answer by id (from `claw prompt-memory list`)").argument("<id>", "memory id").action(async (id) => {
69370
+ await api.delete(`/api/v1/prompt-memory/${encodeURIComponent(id)}`);
69371
+ console.log(`\u2717 forgot memory ${id}`);
69372
+ });
69373
+ var promptMemoryCmd = new Command("prompt-memory").description("view and manage remembered CC startup-prompt answers").addCommand(promptMemoryList).addCommand(promptMemorySet).addCommand(promptMemoryForget);
68910
69374
 
68911
69375
  // src/index.ts
68912
69376
  var program2 = new Command();
@@ -68921,6 +69385,9 @@ program2.addCommand(routeCmd);
68921
69385
  program2.addCommand(probeCmd);
68922
69386
  program2.addCommand(webhookCmd);
68923
69387
  program2.addCommand(agentsCmd);
69388
+ program2.addCommand(appsCmd);
69389
+ program2.addCommand(desktopCmd);
69390
+ program2.addCommand(promptMemoryCmd);
68924
69391
  program2.parseAsync(process.argv).catch((err) => {
68925
69392
  console.error(err.message ?? err);
68926
69393
  process.exit(1);