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.
- package/dist-bundled/claw.cjs +1069 -602
- package/package.json +1 -1
package/dist-bundled/claw.cjs
CHANGED
|
@@ -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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
64074
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67784
|
-
|
|
67785
|
-
|
|
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
|
|
67791
|
-
|
|
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
|
-
|
|
67795
|
-
|
|
67796
|
-
if (
|
|
67797
|
-
|
|
67798
|
-
|
|
67799
|
-
|
|
67800
|
-
|
|
67801
|
-
|
|
67802
|
-
|
|
67803
|
-
|
|
67804
|
-
|
|
67805
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67817
|
-
|
|
67818
|
-
process.exit(2);
|
|
68044
|
+
sayErr(`${RED}error: no session ${targetRef} (try \`claw session list\` in another terminal)${RESET2}`);
|
|
68045
|
+
return;
|
|
67819
68046
|
}
|
|
67820
|
-
|
|
67821
|
-
|
|
67822
|
-
|
|
67823
|
-
|
|
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
|
-
|
|
67838
|
-
|
|
67839
|
-
|
|
67840
|
-
|
|
67841
|
-
|
|
67842
|
-
|
|
67843
|
-
|
|
67844
|
-
|
|
67845
|
-
|
|
67846
|
-
|
|
67847
|
-
|
|
67848
|
-
|
|
67849
|
-
|
|
67850
|
-
|
|
67851
|
-
|
|
67852
|
-
|
|
67853
|
-
|
|
67854
|
-
|
|
67855
|
-
|
|
67856
|
-
|
|
67857
|
-
|
|
67858
|
-
|
|
67859
|
-
|
|
67860
|
-
|
|
67861
|
-
|
|
67862
|
-
|
|
67863
|
-
|
|
67864
|
-
|
|
67865
|
-
|
|
67866
|
-
|
|
67867
|
-
|
|
67868
|
-
|
|
67869
|
-
|
|
67870
|
-
|
|
67871
|
-
|
|
67872
|
-
|
|
67873
|
-
|
|
67874
|
-
|
|
67875
|
-
|
|
67876
|
-
|
|
67877
|
-
|
|
67878
|
-
|
|
67879
|
-
|
|
67880
|
-
|
|
67881
|
-
|
|
67882
|
-
|
|
67883
|
-
|
|
67884
|
-
|
|
67885
|
-
|
|
67886
|
-
|
|
67887
|
-
|
|
67888
|
-
|
|
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
|
-
|
|
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
|
|
68098
|
-
|
|
68099
|
-
|
|
68100
|
-
|
|
68101
|
-
|
|
68102
|
-
|
|
68103
|
-
|
|
68104
|
-
|
|
68105
|
-
|
|
68106
|
-
|
|
68107
|
-
|
|
68108
|
-
|
|
68109
|
-
|
|
68110
|
-
|
|
68111
|
-
|
|
68112
|
-
|
|
68113
|
-
|
|
68114
|
-
|
|
68115
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
68199
|
-
|
|
68200
|
-
|
|
68201
|
-
|
|
68202
|
-
|
|
68203
|
-
|
|
68204
|
-
|
|
68205
|
-
|
|
68206
|
-
|
|
68207
|
-
|
|
68208
|
-
|
|
68209
|
-
|
|
68210
|
-
|
|
68211
|
-
|
|
68212
|
-
|
|
68213
|
-
|
|
68214
|
-
|
|
68215
|
-
|
|
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
|
-
|
|
68243
|
-
|
|
68244
|
-
|
|
68245
|
-
|
|
68246
|
-
|
|
68247
|
-
|
|
68248
|
-
|
|
68249
|
-
|
|
68250
|
-
|
|
68251
|
-
|
|
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
|
|
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
|
-
|
|
68294
|
-
|
|
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
|
|
68298
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
68341
|
-
|
|
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
|
-
|
|
68348
|
-
|
|
68349
|
-
|
|
68350
|
-
|
|
68457
|
+
return {
|
|
68458
|
+
ownerLogin: needle.slice(1, slash),
|
|
68459
|
+
slug: "@" + needle.slice(slash + 1).replace(/^@/, "")
|
|
68460
|
+
};
|
|
68351
68461
|
}
|
|
68352
|
-
|
|
68353
|
-
|
|
68354
|
-
|
|
68355
|
-
|
|
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
|
-
|
|
68361
|
-
|
|
68362
|
-
|
|
68363
|
-
|
|
68364
|
-
|
|
68365
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68388
|
-
|
|
68389
|
-
|
|
68390
|
-
|
|
68391
|
-
|
|
68392
|
-
|
|
68393
|
-
}
|
|
68394
|
-
|
|
68395
|
-
|
|
68396
|
-
|
|
68397
|
-
|
|
68398
|
-
|
|
68399
|
-
|
|
68400
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68504
|
-
|
|
68505
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
68805
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68892
|
-
|
|
68893
|
-
|
|
68894
|
-
|
|
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
|
-
|
|
68898
|
-
|
|
68899
|
-
|
|
68900
|
-
|
|
68901
|
-
|
|
68902
|
-
|
|
68903
|
-
|
|
68904
|
-
|
|
68905
|
-
|
|
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
|
-
|
|
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);
|