clawborrator-cli 0.0.37 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist-bundled/claw.cjs +191 -164
  2. package/package.json +1 -1
@@ -5226,7 +5226,7 @@ var require_websocket = __commonJS({
5226
5226
  var http = require("http");
5227
5227
  var net = require("net");
5228
5228
  var tls = require("tls");
5229
- var { randomBytes, createHash } = require("crypto");
5229
+ var { randomBytes: randomBytes2, createHash: createHash2 } = 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 = randomBytes(16).toString("base64");
5759
+ const key = randomBytes2(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 = createHash("sha1").update(key + GUID).digest("base64");
5889
+ const digest = createHash2("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 } = require("crypto");
6256
+ var { createHash: createHash2 } = 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 = createHash("sha1").update(key + GUID).digest("base64");
6557
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
6558
6558
  const headers = [
6559
6559
  "HTTP/1.1 101 Switching Protocols",
6560
6560
  "Upgrade: websocket",
@@ -63997,6 +63997,11 @@ var {
63997
63997
  Help
63998
63998
  } = import_index.default;
63999
63999
 
64000
+ // src/commands/login.ts
64001
+ var import_node_http = require("node:http");
64002
+ var import_node_crypto = require("node:crypto");
64003
+ var import_node_child_process = require("node:child_process");
64004
+
64000
64005
  // src/config.ts
64001
64006
  var import_node_os = require("node:os");
64002
64007
  var import_node_path = require("node:path");
@@ -64005,7 +64010,7 @@ var CONFIG_DIR = (0, import_node_path.resolve)((0, import_node_os.homedir)(), ".
64005
64010
  var CONFIG_PATH = (0, import_node_path.resolve)(CONFIG_DIR, "config.json");
64006
64011
  var DEFAULTS = {
64007
64012
  hubUrl: process.env.CLAWBORRATOR_HUB ?? "http://localhost:8787",
64008
- pat: null
64013
+ sessionToken: null
64009
64014
  };
64010
64015
  function loadConfig() {
64011
64016
  if (!(0, import_node_fs.existsSync)(CONFIG_PATH)) return { ...DEFAULTS };
@@ -64014,7 +64019,11 @@ function loadConfig() {
64014
64019
  const parsed = JSON.parse(raw);
64015
64020
  return {
64016
64021
  hubUrl: parsed.hubUrl?.trim() || DEFAULTS.hubUrl,
64017
- pat: parsed.pat ?? null
64022
+ // Forward-migrate a legacy `pat` field. The token itself is now
64023
+ // dead on the server side, but carrying it forward avoids loud
64024
+ // "config corrupt" errors — the next API call will 401 cleanly
64025
+ // and prompt re-login.
64026
+ sessionToken: parsed.sessionToken ?? parsed.pat ?? null
64018
64027
  };
64019
64028
  } catch {
64020
64029
  return { ...DEFAULTS };
@@ -64030,9 +64039,9 @@ function saveConfig(cfg) {
64030
64039
  } catch {
64031
64040
  }
64032
64041
  }
64033
- function clearPat() {
64042
+ function clearSession() {
64034
64043
  const cfg = loadConfig();
64035
- saveConfig({ ...cfg, pat: null });
64044
+ saveConfig({ ...cfg, sessionToken: null });
64036
64045
  }
64037
64046
 
64038
64047
  // src/client/api.ts
@@ -64047,10 +64056,10 @@ var ApiError = class extends Error {
64047
64056
  async function request(method, path, body, opts = {}) {
64048
64057
  const cfg = loadConfig();
64049
64058
  const hubUrl = (opts.hubUrl ?? cfg.hubUrl).replace(/\/$/, "");
64050
- const pat = opts.pat === void 0 ? cfg.pat : opts.pat;
64059
+ const token = opts.token === void 0 ? cfg.sessionToken : opts.token;
64051
64060
  const headers = {};
64052
64061
  if (body !== void 0) headers["Content-Type"] = "application/json";
64053
- if (pat) headers["Authorization"] = `Bearer ${pat}`;
64062
+ if (token) headers["Authorization"] = `Bearer ${token}`;
64054
64063
  const res = await fetch(`${hubUrl}${path}`, {
64055
64064
  method,
64056
64065
  headers,
@@ -64082,61 +64091,113 @@ var api = {
64082
64091
  };
64083
64092
 
64084
64093
  // src/commands/login.ts
64085
- async function deviceFlowLogin(hubUrl) {
64086
- const start = await api.post("/api/v1/auth/device/start", {}, { hubUrl, pat: null });
64087
- console.log("To continue, open:");
64088
- console.log(` ${start.verificationUri}`);
64089
- console.log(`and enter the code: ${start.userCode}`);
64094
+ function base64url(buf) {
64095
+ return buf.toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
64096
+ }
64097
+ function openBrowser(url) {
64098
+ const platform2 = process.platform;
64099
+ const cmd = platform2 === "win32" ? "cmd" : platform2 === "darwin" ? "open" : "xdg-open";
64100
+ const args = platform2 === "win32" ? ["/c", "start", '""', url] : [url];
64101
+ try {
64102
+ (0, import_node_child_process.spawn)(cmd, args, { stdio: "ignore", detached: true }).unref();
64103
+ } catch {
64104
+ }
64105
+ }
64106
+ function awaitCallback(server) {
64107
+ return new Promise((resolve3, reject) => {
64108
+ server.on("request", (req, res) => {
64109
+ const u = new URL(req.url ?? "/", "http://localhost");
64110
+ if (u.pathname !== "/callback") {
64111
+ res.statusCode = 404;
64112
+ res.end("not found");
64113
+ return;
64114
+ }
64115
+ const code = u.searchParams.get("code") ?? "";
64116
+ const state = u.searchParams.get("state") ?? "";
64117
+ const error = u.searchParams.get("error");
64118
+ if (error) {
64119
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
64120
+ res.end(`<html><body><h2>login failed: ${error}</h2><p>you can close this tab.</p></body></html>`);
64121
+ reject(new Error(`oauth error: ${error}`));
64122
+ return;
64123
+ }
64124
+ if (!code || !state) {
64125
+ res.statusCode = 400;
64126
+ res.end("missing code or state");
64127
+ reject(new Error("callback missing code or state"));
64128
+ return;
64129
+ }
64130
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
64131
+ res.end("<html><body><h2>logged in</h2><p>you can close this tab and return to the terminal.</p><script>setTimeout(() => window.close(), 1500);</script></body></html>");
64132
+ resolve3({ code, state });
64133
+ });
64134
+ server.on("error", reject);
64135
+ });
64136
+ }
64137
+ async function browserOAuthFlow(hubUrl) {
64138
+ const verifier = base64url((0, import_node_crypto.randomBytes)(32));
64139
+ const challenge = base64url((0, import_node_crypto.createHash)("sha256").update(verifier).digest());
64140
+ const state = base64url((0, import_node_crypto.randomBytes)(16));
64141
+ const server = (0, import_node_http.createServer)();
64142
+ await new Promise((resolve3, reject) => {
64143
+ server.once("error", reject);
64144
+ server.listen(0, "127.0.0.1", () => resolve3());
64145
+ });
64146
+ const port = server.address().port;
64147
+ const redirectUri = `http://localhost:${port}/callback`;
64148
+ const startUrl = new URL("/api/v1/auth/oauth/start", hubUrl);
64149
+ startUrl.searchParams.set("redirect_uri", redirectUri);
64150
+ startUrl.searchParams.set("state", state);
64151
+ startUrl.searchParams.set("code_challenge", challenge);
64152
+ startUrl.searchParams.set("code_challenge_method", "S256");
64153
+ console.log("opening browser to authenticate with GitHub\u2026");
64154
+ console.log(` if it doesn't open automatically, paste this URL into a browser:`);
64155
+ console.log(` ${startUrl}`);
64090
64156
  console.log("");
64091
- process.stdout.write("waiting for authorization");
64092
- let intervalSec = start.intervalSeconds || 5;
64093
- const deadline = new Date(start.expiresAt).getTime();
64094
- for (; ; ) {
64095
- if (Date.now() > deadline) {
64096
- console.log("");
64097
- throw new Error("device code expired before authorization completed");
64098
- }
64099
- await new Promise((r) => setTimeout(r, intervalSec * 1e3));
64100
- const poll = await api.get(
64101
- `/api/v1/auth/device/poll?id=${encodeURIComponent(start.pollingId)}`,
64102
- { hubUrl, pat: null }
64103
- );
64104
- if (poll.status === "pending") {
64105
- process.stdout.write(".");
64106
- if (poll.intervalSeconds) intervalSec = poll.intervalSeconds;
64107
- continue;
64108
- }
64109
- if (poll.status === "expired") {
64110
- console.log("");
64111
- throw new Error("device code expired");
64112
- }
64113
- if (poll.status === "denied") {
64114
- console.log("");
64115
- throw new Error("authorization denied");
64116
- }
64117
- if (poll.status === "granted" && poll.user && poll.pat) {
64118
- console.log("");
64119
- return { user: poll.user, pat: poll.pat };
64157
+ openBrowser(startUrl.toString());
64158
+ let cb;
64159
+ try {
64160
+ cb = await Promise.race([
64161
+ awaitCallback(server),
64162
+ new Promise(
64163
+ (_, reject) => setTimeout(() => reject(new Error("login timed out after 5 minutes")), 5 * 60 * 1e3)
64164
+ )
64165
+ ]);
64166
+ } finally {
64167
+ server.close();
64168
+ }
64169
+ if (cb.state !== state) throw new Error("state mismatch \u2014 possible CSRF; aborting");
64170
+ const res = await fetch(hubUrl.replace(/\/$/, "") + "/api/v1/auth/oauth/token", {
64171
+ method: "POST",
64172
+ headers: { "Content-Type": "application/json", "User-Agent": "clawborrator-cli" },
64173
+ body: JSON.stringify({ code: cb.code, code_verifier: verifier })
64174
+ });
64175
+ if (!res.ok) {
64176
+ const text = await res.text().catch(() => "");
64177
+ let parsed = null;
64178
+ try {
64179
+ parsed = JSON.parse(text);
64180
+ } catch {
64120
64181
  }
64121
- console.log("");
64122
- throw new Error(`unexpected poll status: ${poll.status}`);
64182
+ throw new ApiError(res.status, parsed?.error ?? `http_${res.status}`, parsed?.message ?? parsed?.error ?? text);
64123
64183
  }
64184
+ return res.json();
64124
64185
  }
64125
- var loginCmd = new Command("login").description("authenticate against a hub_v1 instance via GitHub device-flow OAuth").option("--hub <url>", "hub URL to use (defaults to config or http://localhost:8787)").action(async (opts) => {
64186
+ var loginCmd = new Command("login").description("authenticate against a hub_v1 instance via GitHub OAuth (browser callback)").option("--hub <url>", "hub URL to use (defaults to config or http://localhost:8787)").action(async (opts) => {
64126
64187
  const cfg = loadConfig();
64127
64188
  const hubUrl = opts.hub ?? cfg.hubUrl;
64128
64189
  try {
64129
- const { user, pat } = await deviceFlowLogin(hubUrl);
64130
- saveConfig({ hubUrl, pat: pat.token });
64190
+ const { user, session } = await browserOAuthFlow(hubUrl);
64191
+ saveConfig({ hubUrl, sessionToken: session.token });
64131
64192
  console.log(`logged in as @${user.githubLogin}`);
64132
- console.log(`hub: ${hubUrl}`);
64133
- console.log(`PAT: ${pat.prefix}\u2026 (stored in ~/.clawborrator/config.json)`);
64193
+ console.log(`hub: ${hubUrl}`);
64194
+ console.log(`session: ${session.token.slice(0, 16)}\u2026 (stored in ~/.clawborrator/config.json)`);
64195
+ console.log(`expires at: ${session.expiresAt}`);
64134
64196
  } catch (e) {
64135
64197
  if (e instanceof ApiError && e.status === 503) {
64136
64198
  console.error("error: this hub does not have GitHub OAuth configured");
64137
- console.error(" hub operator must register a GitHub OAuth App with device flow enabled");
64138
- console.error(" and start the hub with GITHUB_CLIENT_ID=Iv1.xxxxxx");
64139
- console.error(" see hub_v1/docs/SETUP.md for step-by-step");
64199
+ console.error(" hub operator must register a GitHub OAuth App, set GITHUB_CLIENT_ID +");
64200
+ console.error(" GITHUB_CLIENT_SECRET, and add the callback URL.");
64140
64201
  process.exit(2);
64141
64202
  }
64142
64203
  throw e;
@@ -64144,34 +64205,27 @@ var loginCmd = new Command("login").description("authenticate against a hub_v1 i
64144
64205
  });
64145
64206
 
64146
64207
  // src/commands/logout.ts
64147
- var logoutCmd = new Command("logout").description("forget the locally-stored PAT").option("--keep-server", "do not revoke the PAT server-side (default: revoke)").action(async (opts) => {
64208
+ var logoutCmd = new Command("logout").description("forget the locally-stored session token").option("--keep-server", "do not revoke the session server-side (default: revoke)").action(async (opts) => {
64148
64209
  const cfg = loadConfig();
64149
- if (!cfg.pat) {
64210
+ if (!cfg.sessionToken) {
64150
64211
  console.log("not logged in; nothing to do");
64151
64212
  return;
64152
64213
  }
64153
64214
  if (!opts.keepServer) {
64154
64215
  try {
64155
- const me = await api.get("/api/v1/me");
64156
- const list3 = await api.get(
64157
- "/api/v1/tokens?kind=pat"
64158
- );
64159
- const target = list3.items.find((t) => cfg.pat.startsWith(t.prefix) && !t.revokedAt);
64160
- if (target) {
64161
- await api.delete(`/api/v1/tokens/${target.id}`);
64162
- }
64216
+ await api.post("/api/v1/auth/logout");
64163
64217
  } catch (e) {
64164
64218
  console.warn(`warning: server-side revoke failed: ${e?.message ?? e}`);
64165
64219
  }
64166
64220
  }
64167
- clearPat();
64168
- console.log("logged out (PAT cleared from local config)");
64221
+ clearSession();
64222
+ console.log("logged out (session cleared from local config)");
64169
64223
  });
64170
64224
 
64171
64225
  // src/commands/whoami.ts
64172
64226
  var whoamiCmd = new Command("whoami").description("show the currently authenticated user").action(async () => {
64173
64227
  const cfg = loadConfig();
64174
- if (!cfg.pat) {
64228
+ if (!cfg.sessionToken) {
64175
64229
  console.log("not logged in \u2014 run: claw login");
64176
64230
  return;
64177
64231
  }
@@ -64181,7 +64235,7 @@ var whoamiCmd = new Command("whoami").description("show the currently authentica
64181
64235
  console.log(`hub: ${cfg.hubUrl}`);
64182
64236
  } catch (e) {
64183
64237
  if (e instanceof ApiError && e.status === 401) {
64184
- console.error("error: PAT rejected by hub. Run: claw login");
64238
+ console.error("error: session rejected by hub. Run: claw login");
64185
64239
  process.exit(2);
64186
64240
  }
64187
64241
  throw e;
@@ -67636,6 +67690,23 @@ function stopWorking() {
67636
67690
  }
67637
67691
  refreshPrompt();
67638
67692
  }
67693
+ var routeWatchdog = null;
67694
+ var ROUTE_WATCHDOG_MS = 6e4;
67695
+ function armRouteWatchdog() {
67696
+ if (routeWatchdog) clearTimeout(routeWatchdog);
67697
+ routeWatchdog = setTimeout(() => {
67698
+ routeWatchdog = null;
67699
+ say(`${DIM2}[${ts()}]${RESET2} ${RED}\u26A0 no peer reply within 60s${RESET2} ${DIM2}\u2014 peer's Claude may have denied the reply tool${RESET2}`);
67700
+ stopWorking();
67701
+ }, ROUTE_WATCHDOG_MS);
67702
+ routeWatchdog.unref();
67703
+ }
67704
+ function disarmRouteWatchdog() {
67705
+ if (routeWatchdog) {
67706
+ clearTimeout(routeWatchdog);
67707
+ routeWatchdog = null;
67708
+ }
67709
+ }
67639
67710
  function say(line) {
67640
67711
  if (!process.stdout.isTTY || !rlRef) {
67641
67712
  console.log(line);
@@ -67685,7 +67756,7 @@ var sessionAttach = new Command("attach").description("open a TUI on a session \
67685
67756
  if (opts.debug) debugMode = true;
67686
67757
  if (opts.status === false) statusEnabled = false;
67687
67758
  const cfg = loadConfig();
67688
- if (!cfg.pat) {
67759
+ if (!cfg.sessionToken) {
67689
67760
  console.error("error: not logged in. run `claw login`.");
67690
67761
  process.exit(2);
67691
67762
  }
@@ -67773,7 +67844,7 @@ var sessionAttach = new Command("attach").description("open a TUI on a session \
67773
67844
  let reconnectAttempt = 0;
67774
67845
  let reconnectTimer = null;
67775
67846
  function connect() {
67776
- ws = new wrapper_default(wsUrl, { headers: { Authorization: `Bearer ${cfg.pat}` } });
67847
+ ws = new wrapper_default(wsUrl, { headers: { Authorization: `Bearer ${cfg.sessionToken}` } });
67777
67848
  ws.on("open", () => {
67778
67849
  if (reconnectAttempt > 0) {
67779
67850
  say(`${DIM2}[${ts()}]${RESET2} ${AMBER}reconnected${RESET2} to ${cfg.hubUrl}`);
@@ -67927,6 +67998,7 @@ var sessionAttach = new Command("attach").description("open a TUI on a session \
67927
67998
  ws.send(JSON.stringify(out2));
67928
67999
  say(`${DIM2}[${ts()}]${RESET2} ${AMBER}\u2192 prompt \u2192 ${peerSessionId.slice(0, 8)}\u2026${RESET2} ${promptText}`);
67929
68000
  startWorking();
68001
+ armRouteWatchdog();
67930
68002
  return;
67931
68003
  }
67932
68004
  (async () => {
@@ -67973,6 +68045,7 @@ var sessionAttach = new Command("attach").description("open a TUI on a session \
67973
68045
  ws.send(JSON.stringify(out2));
67974
68046
  say(`${DIM2}[${ts()}]${RESET2} ${AMBER}\u2192 prompt \u2192 ${targetRef}${RESET2} ${promptText}`);
67975
68047
  startWorking();
68048
+ armRouteWatchdog();
67976
68049
  } catch (e) {
67977
68050
  sayErr(`${RED}error: ${e?.message ?? String(e)}${RESET2}`);
67978
68051
  }
@@ -68112,6 +68185,15 @@ function renderEventBody(ev, ts2, p, myLogin) {
68112
68185
  const text = String(p.text ?? "").trim();
68113
68186
  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}`;
68114
68187
  emitChatLine(`${DIM2}[${ts2}]${RESET2} ${tag2}`, renderMarkdown(text));
68188
+ if (p.source === "peer-reply") disarmRouteWatchdog();
68189
+ return;
68190
+ }
68191
+ if (ev.type === "peer-timeout") {
68192
+ const peer = String(p.peerLogin ?? p.peerSessionId ?? "?");
68193
+ const peerShort = peer.length > 36 ? peer.slice(0, 8) + "\u2026" : peer;
68194
+ say(`${DIM2}[${ts2}]${RESET2} ${RED}\u26A0 ${peerShort} did not reply${RESET2} ${DIM2}\u2014 peer's Claude may have denied the reply tool${RESET2}`);
68195
+ disarmRouteWatchdog();
68196
+ stopWorking();
68115
68197
  return;
68116
68198
  }
68117
68199
  say(`${DIM2}[${ts2}]${RESET2} ${AMBER}[chat:${ev.type}]${RESET2} ${previewPayload(p)}`);
@@ -68460,112 +68542,57 @@ var sessionCmd = new Command("session").description("manage Claude Code sessions
68460
68542
  // src/commands/token.ts
68461
68543
  var import_node_fs2 = require("node:fs");
68462
68544
  var import_node_path2 = require("node:path");
68463
-
68464
- // ../shared/dist/scopes.js
68465
- var ALL_SCOPES = [
68466
- "me:read",
68467
- "sessions:read",
68468
- "sessions:read.transcript",
68469
- "sessions:write",
68470
- // archive/purge/share/unshare
68471
- "sessions:approve",
68472
- // permission decisions
68473
- "sessions:share",
68474
- "op-messages:read",
68475
- "op-messages:write",
68476
- "prompts:write",
68477
- "webhooks:manage",
68478
- "tokens:manage"
68479
- ];
68480
- var CLI_DEFAULT_SCOPES = [
68481
- "me:read",
68482
- "sessions:read",
68483
- "sessions:read.transcript",
68484
- "sessions:write",
68485
- "sessions:approve",
68486
- "sessions:share",
68487
- "op-messages:read",
68488
- "op-messages:write",
68489
- "prompts:write",
68490
- "webhooks:manage",
68491
- "tokens:manage"
68492
- ];
68493
-
68494
- // src/commands/token.ts
68495
- var tokenMint = new Command("mint").description("create a new token").requiredOption("--kind <kind>", "channel | pat").requiredOption("--name <name>", 'human-readable label (e.g. "alice-laptop")').option("--scopes <csv>", "comma-separated scopes (PAT only); default = sensible CLI set").option("--app <name>", "pin the PAT to a specific app (display only)").option("--mcp-snippet", "after minting a channel token, also produce a ready-to-use .mcp.json block. By default writes to stdout (prose to stderr); pair with --out=<path> to write the file directly (recommended on Windows \u2014 `>` redirection in PowerShell encodes as UTF-16 w/ BOM, which CC rejects).").option("--out <path>", "when used with --mcp-snippet, write the JSON to <path> (UTF-8, no BOM) instead of stdout. Pass `.mcp.json` for the canonical project location.").action(async (opts) => {
68496
- if (opts.kind === "channel") {
68497
- const out = await api.post("/api/v1/tokens/channel", { name: opts.name });
68498
- const proseToStderr = opts.mcpSnippet && !opts.out;
68499
- const prose = proseToStderr ? console.error : console.log;
68500
- prose(`\u2713 channel token minted: ${out.name}`);
68501
- prose(` ${out.token}`);
68502
- prose(" (shown ONCE \u2014 store it now)");
68503
- if (opts.mcpSnippet) {
68504
- const cfg = loadConfig();
68505
- const wsUrl = cfg.hubUrl.replace(/^http(s?):\/\//, "ws$1://");
68506
- const json = JSON.stringify({
68507
- mcpServers: {
68508
- clawborrator: {
68509
- command: "npx",
68510
- args: ["-y", "clawborrator-mcp"],
68511
- env: {
68512
- CLAWBORRATOR_HUB_URL: wsUrl,
68513
- CLAWBORRATOR_TOKEN: out.token
68514
- }
68515
- }
68516
- }
68517
- }, null, 2) + "\n";
68518
- if (opts.out) {
68519
- const target = (0, import_node_path2.resolve)(opts.out);
68520
- (0, import_node_fs2.writeFileSync)(target, json, "utf8");
68521
- prose("");
68522
- prose(`\u2713 wrote ${target}`);
68523
- } else {
68524
- process.stdout.write(json);
68525
- }
68545
+ var tokenMint = new Command("mint").description("create a new channel token").requiredOption("--name <name>", 'human-readable label (e.g. "alice-laptop")').option("--mcp-snippet", "after minting, also produce a ready-to-use .mcp.json block. By default writes to stdout (prose to stderr); pair with --out=<path> to write the file directly (recommended on Windows \u2014 `>` redirection in PowerShell encodes as UTF-16 w/ BOM, which CC rejects).").option("--out <path>", "when used with --mcp-snippet, write the JSON to <path> (UTF-8, no BOM) instead of stdout. Pass `.mcp.json` for the canonical project location.").action(async (opts) => {
68546
+ const out = await api.post("/api/v1/tokens/channel", { name: opts.name });
68547
+ const proseToStderr = opts.mcpSnippet && !opts.out;
68548
+ const prose = proseToStderr ? console.error : console.log;
68549
+ prose(`\u2713 channel token minted: ${out.name}`);
68550
+ prose(` ${out.token}`);
68551
+ prose(" (shown ONCE \u2014 store it now)");
68552
+ if (opts.mcpSnippet) {
68553
+ const cfg = loadConfig();
68554
+ const wsUrl = cfg.hubUrl.replace(/^http(s?):\/\//, "ws$1://");
68555
+ const json = JSON.stringify({
68556
+ mcpServers: {
68557
+ clawborrator: {
68558
+ command: "npx",
68559
+ args: ["-y", "clawborrator-mcp"],
68560
+ env: {
68561
+ CLAWBORRATOR_HUB_URL: wsUrl,
68562
+ CLAWBORRATOR_TOKEN: out.token
68563
+ }
68564
+ }
68565
+ }
68566
+ }, null, 2) + "\n";
68567
+ if (opts.out) {
68568
+ const target = (0, import_node_path2.resolve)(opts.out);
68569
+ (0, import_node_fs2.writeFileSync)(target, json, "utf8");
68526
68570
  prose("");
68527
- prose(" next: launch CC with the clawborrator channel enabled \u2014");
68528
- prose(" claude --dangerously-load-development-channels server:clawborrator");
68571
+ prose(`\u2713 wrote ${target}`);
68572
+ } else {
68573
+ process.stdout.write(json);
68529
68574
  }
68530
- return;
68575
+ prose("");
68576
+ prose(" next: launch CC with the clawborrator channel enabled \u2014");
68577
+ prose(" claude --dangerously-load-development-channels server:clawborrator");
68531
68578
  }
68532
- if (opts.kind === "pat") {
68533
- const scopes = opts.scopes ? opts.scopes.split(",").map((s) => s.trim()).filter(Boolean) : CLI_DEFAULT_SCOPES;
68534
- const out = await api.post("/api/v1/tokens/pat", {
68535
- name: opts.name,
68536
- scopes,
68537
- appName: opts.app ?? null
68538
- });
68539
- console.log(`\u2713 PAT minted: ${out.name}`);
68540
- console.log(` ${out.token}`);
68541
- console.log(` scopes: ${out.scopes.join(", ")}`);
68542
- console.log(" (shown ONCE \u2014 store it now)");
68543
- return;
68544
- }
68545
- console.error(`error: --kind must be channel or pat (got ${opts.kind})`);
68546
- process.exit(2);
68547
68579
  });
68548
- var tokenList = new Command("list").alias("ls").description("list tokens (channel + PAT) for the current user").option("--kind <kind>", "narrow to one kind").action(async (opts) => {
68549
- const qs = opts.kind ? `?kind=${encodeURIComponent(opts.kind)}` : "";
68550
- const data = await api.get(`/api/v1/tokens${qs}`);
68580
+ var tokenList = new Command("list").alias("ls").description("list channel tokens for the current user").action(async () => {
68581
+ const data = await api.get("/api/v1/tokens");
68551
68582
  if (data.items.length === 0) {
68552
- console.log("no active tokens");
68583
+ console.log("no active channel tokens");
68553
68584
  return;
68554
68585
  }
68555
68586
  for (const t of data.items) {
68556
68587
  const used = t.lastUsedAt ? `last used ${fmtAgo2(t.lastUsedAt)}` : "never used";
68557
- const meta = t.kind === "pat" ? ` scopes=${t.scopes.length}${t.appName ? ` app=${t.appName}` : ""}` : "";
68558
- console.log(`${t.id.toString().padStart(3)} ${t.kind.padEnd(7)} ${t.prefix}\u2026 ${t.name.padEnd(28)} ${used}${meta}`);
68588
+ console.log(`${t.id.toString().padStart(3)} ${t.prefix}\u2026 ${t.name.padEnd(28)} ${used}`);
68559
68589
  }
68560
68590
  });
68561
- var tokenRevoke = new Command("revoke").description("revoke a token by id").argument("<id>", "token id (from `claw token list`)").action(async (id) => {
68591
+ var tokenRevoke = new Command("revoke").description("revoke a channel token by id").argument("<id>", "token id (from `claw token list`)").action(async (id) => {
68562
68592
  await api.delete(`/api/v1/tokens/${encodeURIComponent(id)}`);
68563
68593
  console.log(`\u2713 token ${id} revoked`);
68564
68594
  });
68565
- var tokenScopes = new Command("scopes").description("print every valid scope name").action(() => {
68566
- for (const s of ALL_SCOPES) console.log(s);
68567
- });
68568
- var tokenCmd = new Command("token").description("mint, list, and revoke channel tokens + PATs").addCommand(tokenMint).addCommand(tokenList).addCommand(tokenRevoke).addCommand(tokenScopes);
68595
+ var tokenCmd = new Command("token").description("mint, list, and revoke channel tokens").addCommand(tokenMint).addCommand(tokenList).addCommand(tokenRevoke);
68569
68596
  function fmtAgo2(iso) {
68570
68597
  const ms = Date.now() - new Date(iso).getTime();
68571
68598
  if (ms < 6e4) return Math.max(1, Math.floor(ms / 1e3)) + "s ago";
@@ -68675,7 +68702,7 @@ var webhookCmd = new Command("webhook").description("manage webhook subscription
68675
68702
 
68676
68703
  // src/index.ts
68677
68704
  var program2 = new Command();
68678
- program2.name("claw").description("clawborrator CLI \u2014 control your Claude Code sessions from the terminal").version("0.0.37");
68705
+ program2.name("claw").description("clawborrator CLI \u2014 control your Claude Code sessions from the terminal").version("0.0.39");
68679
68706
  program2.addCommand(loginCmd);
68680
68707
  program2.addCommand(logoutCmd);
68681
68708
  program2.addCommand(whoamiCmd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawborrator-cli",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "type": "module",
5
5
  "description": "claw — command-line client for clawborrator hub_v1. Manages PATs, channel tokens, sessions, cross-session routing, and webhooks; ships an inline TUI for live multi-operator session attach.",
6
6
  "license": "MIT",