@linkedclaw/cli 0.1.6 → 0.2.0

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/bin.js CHANGED
@@ -5741,11 +5741,15 @@ var RequesterFlows = class {
5741
5741
  return this.client.discover({ capability, ...extra });
5742
5742
  }
5743
5743
  /**
5744
- * Open a session. HTTP create → WS handshake (SESSION_CREATE/ACCEPT) → done.
5744
+ * Open a session.
5745
5745
  *
5746
- * Default transport is /ws native providers register there
5747
- * (`SkillConfig.try_acp=False` upstream default). Set `tryAcp: true` to try
5748
- * /acp first; on opt-in, falls back to /ws when the recipient isn't on /acp.
5746
+ * Default: pure HTTP — `POST /sessions` then `POST /activate`. Cloud
5747
+ * drives the SESSION_CREATE handshake to the provider server-side. No
5748
+ * WebSocket opens on the requester side; no `agentId` registration
5749
+ * required.
5750
+ *
5751
+ * `tryAcp: true` opts into the legacy WS handshake path for OpenClaw
5752
+ * sub-agent / ACP-routed scenarios — see {@link HireParams.tryAcp}.
5749
5753
  */
5750
5754
  async hire(params) {
5751
5755
  const session = await this.client.createSession({
@@ -5755,9 +5759,9 @@ var RequesterFlows = class {
5755
5759
  ...params.referredBy !== void 0 ? { referred_by: params.referredBy } : {}
5756
5760
  });
5757
5761
  if (params.autoActivate === false) return { session, activated: false };
5758
- const relayUrl = params.relayUrl ?? DEFAULT_RELAY_URL;
5759
5762
  try {
5760
5763
  if (params.tryAcp) {
5764
+ const relayUrl = params.relayUrl ?? DEFAULT_RELAY_URL;
5761
5765
  const acpUrl = relayUrl.replace(/\/ws$/, "/acp");
5762
5766
  try {
5763
5767
  await this.attemptHandshake(acpUrl, session.session_id, params, ACP_CONNECT_TIMEOUT_MS);
@@ -5765,8 +5769,6 @@ var RequesterFlows = class {
5765
5769
  if (!(err instanceof TransportMissError)) throw err;
5766
5770
  await this.attemptHandshake(relayUrl, session.session_id, params, SESSION_ACCEPT_TIMEOUT_MS);
5767
5771
  }
5768
- } else {
5769
- await this.attemptHandshake(relayUrl, session.session_id, params, SESSION_ACCEPT_TIMEOUT_MS);
5770
5772
  }
5771
5773
  await this.client.activateSession(session.session_id);
5772
5774
  } catch (err) {
@@ -5779,7 +5781,14 @@ var RequesterFlows = class {
5779
5781
  }
5780
5782
  return { session, activated: true };
5781
5783
  }
5784
+ // tryAcp:true path only. Default `hire()` no longer calls this — see the
5785
+ // method comment on `hire`. Kept for OpenClaw sub-agent integration.
5782
5786
  async attemptHandshake(url, sessionId, params, connectTimeoutMs) {
5787
+ if (!params.agentId) {
5788
+ throw new Error(
5789
+ "attemptHandshake (tryAcp:true) requires `agentId` in HireParams \u2014 the WS IDENTIFY frame validates listing ownership. Use the default HTTP path (omit tryAcp) if you don't have a registered agent listing."
5790
+ );
5791
+ }
5783
5792
  const ws = new WebSocket2(url);
5784
5793
  try {
5785
5794
  await new Promise((resolve3, reject) => {
@@ -6348,19 +6357,409 @@ function registerArenaCommands(program2) {
6348
6357
  });
6349
6358
  }
6350
6359
 
6360
+ // src/auth/decide.ts
6361
+ import { existsSync as existsSync2 } from "fs";
6362
+ import { hostname, platform } from "os";
6363
+ function detectLabel(packageVersion) {
6364
+ const host = hostname();
6365
+ return truncate(
6366
+ `linkedclaw-cli/${packageVersion} on ${platform()} (host: ${host})`,
6367
+ 120
6368
+ );
6369
+ }
6370
+ function truncate(s, n) {
6371
+ return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
6372
+ }
6373
+ function isHeadless(env = process.env) {
6374
+ if (env.SSH_CLIENT || env.SSH_TTY) return true;
6375
+ if (env.CODESPACES === "true") return true;
6376
+ if (env.LINKEDCLAW_FORCE_DEVICE_FLOW === "1") return true;
6377
+ if (existsSync2("/.dockerenv") || existsSync2("/run/.containerenv")) return true;
6378
+ if (platform() === "linux") {
6379
+ if (!env.DISPLAY && !env.WAYLAND_DISPLAY) return true;
6380
+ }
6381
+ return false;
6382
+ }
6383
+
6384
+ // src/auth/loopback.ts
6385
+ import { createServer } from "http";
6386
+
6387
+ // src/auth/pkce.ts
6388
+ import { createHash as createHash2, randomBytes } from "crypto";
6389
+ function generateVerifier() {
6390
+ return randomBytes(64).toString("base64url");
6391
+ }
6392
+ function deriveChallenge(verifier) {
6393
+ return createHash2("sha256").update(verifier).digest("base64url");
6394
+ }
6395
+ function generateState() {
6396
+ return randomBytes(16).toString("base64url");
6397
+ }
6398
+
6399
+ // src/auth/loopback.ts
6400
+ var LoopbackError = class extends Error {
6401
+ constructor(message, recoverable) {
6402
+ super(message);
6403
+ this.recoverable = recoverable;
6404
+ this.name = "LoopbackError";
6405
+ }
6406
+ recoverable;
6407
+ };
6408
+ var SUCCESS_HTML = `<!doctype html>
6409
+ <html><head><meta charset="utf-8"><title>LinkedClaw \u2014 Authorized</title></head>
6410
+ <body style="font-family:system-ui,sans-serif;text-align:center;padding:48px">
6411
+ <h2>\u2705 Authorized</h2>
6412
+ <p>You can close this tab and return to your terminal.</p>
6413
+ </body></html>`;
6414
+ var ERROR_HTML = (msg) => `<!doctype html>
6415
+ <html><head><meta charset="utf-8"><title>LinkedClaw \u2014 Error</title></head>
6416
+ <body style="font-family:system-ui,sans-serif;text-align:center;padding:48px">
6417
+ <h2>\u274C Authorization failed</h2>
6418
+ <p>${msg}</p>
6419
+ <p>Return to your terminal \u2014 the CLI will retry or fall back automatically.</p>
6420
+ </body></html>`;
6421
+ async function runLoopback(opts) {
6422
+ const verifier = generateVerifier();
6423
+ const challenge = deriveChallenge(verifier);
6424
+ const state = generateState();
6425
+ const server = createServer();
6426
+ await new Promise((resolve3, reject) => {
6427
+ server.once("error", reject);
6428
+ server.listen(0, "127.0.0.1", () => {
6429
+ server.removeListener("error", reject);
6430
+ resolve3();
6431
+ });
6432
+ });
6433
+ const port = server.address().port;
6434
+ const redirectUri = `http://127.0.0.1:${port}/cb`;
6435
+ let initResp;
6436
+ try {
6437
+ initResp = await initiate(opts.cloudUrl, {
6438
+ client_label: opts.clientLabel,
6439
+ requested_scope: opts.requestedScope,
6440
+ redirect_uri: redirectUri,
6441
+ code_challenge: challenge,
6442
+ code_challenge_method: "S256",
6443
+ state
6444
+ });
6445
+ } catch (err) {
6446
+ server.close();
6447
+ throw new LoopbackError(
6448
+ `Failed to start authorization: ${err.message}`,
6449
+ // network failure shouldn't fall through to device flow — it'll fail there too.
6450
+ false
6451
+ );
6452
+ }
6453
+ try {
6454
+ await opts.openBrowser(initResp.authorize_url);
6455
+ } catch {
6456
+ server.close();
6457
+ throw new LoopbackError(
6458
+ "Could not open browser; switching to device flow.",
6459
+ true
6460
+ );
6461
+ }
6462
+ if (opts.onUserPrompt) opts.onUserPrompt(initResp.authorize_url);
6463
+ const timeoutMs = opts.timeoutMs ?? 6e4;
6464
+ const callbackParams = await waitForCallback(server, timeoutMs).catch((err) => {
6465
+ server.close();
6466
+ throw err;
6467
+ });
6468
+ server.close();
6469
+ if (callbackParams.error) {
6470
+ throw new LoopbackError(`Authorization denied: ${callbackParams.error}`, false);
6471
+ }
6472
+ if (!callbackParams.code) {
6473
+ throw new LoopbackError("Authorization callback missing code.", true);
6474
+ }
6475
+ if (callbackParams.state !== state) {
6476
+ throw new LoopbackError("Authorization callback state mismatch.", false);
6477
+ }
6478
+ const tokenResp = await exchangeToken(opts.cloudUrl, {
6479
+ grant_type: "authorization_code",
6480
+ code: callbackParams.code,
6481
+ code_verifier: verifier
6482
+ });
6483
+ return {
6484
+ apiKey: tokenResp.api_key,
6485
+ userId: tokenResp.user_id,
6486
+ handle: tokenResp.handle ?? null,
6487
+ scope: tokenResp.scope,
6488
+ keyId: tokenResp.key_id
6489
+ };
6490
+ }
6491
+ function waitForCallback(server, timeoutMs) {
6492
+ return new Promise((resolve3, reject) => {
6493
+ const timer = setTimeout(() => {
6494
+ reject(new LoopbackError("Browser callback timed out.", true));
6495
+ }, timeoutMs);
6496
+ server.on("request", (req, res) => {
6497
+ const url = new URL(req.url ?? "/", `http://127.0.0.1`);
6498
+ if (url.pathname !== "/cb") {
6499
+ res.statusCode = 404;
6500
+ res.end();
6501
+ return;
6502
+ }
6503
+ const params = {
6504
+ code: url.searchParams.get("code") ?? void 0,
6505
+ state: url.searchParams.get("state") ?? void 0,
6506
+ error: url.searchParams.get("error") ?? void 0
6507
+ };
6508
+ const html = params.error ? ERROR_HTML(params.error) : params.code ? SUCCESS_HTML : ERROR_HTML("Missing code parameter");
6509
+ res.statusCode = params.code ? 200 : 400;
6510
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
6511
+ res.end(html);
6512
+ clearTimeout(timer);
6513
+ resolve3(params);
6514
+ });
6515
+ });
6516
+ }
6517
+ async function initiate(cloudUrl, body) {
6518
+ const resp = await fetch(`${cloudUrl}/api/v1/auth/oauth/initiate`, {
6519
+ method: "POST",
6520
+ headers: { "Content-Type": "application/json" },
6521
+ body: JSON.stringify(body)
6522
+ });
6523
+ if (resp.status !== 201) {
6524
+ const detail = await safeDetail(resp);
6525
+ throw new Error(`/auth/oauth/initiate ${resp.status}: ${detail}`);
6526
+ }
6527
+ return await resp.json();
6528
+ }
6529
+ async function exchangeToken(cloudUrl, body) {
6530
+ const resp = await fetch(`${cloudUrl}/api/v1/auth/oauth/token`, {
6531
+ method: "POST",
6532
+ headers: { "Content-Type": "application/json" },
6533
+ body: JSON.stringify(body)
6534
+ });
6535
+ if (resp.status !== 200) {
6536
+ const detail = await safeDetail(resp);
6537
+ throw new LoopbackError(`Token exchange failed: ${detail}`, false);
6538
+ }
6539
+ return await resp.json();
6540
+ }
6541
+ async function safeDetail(resp) {
6542
+ try {
6543
+ const j = await resp.json();
6544
+ return j.detail ?? `${resp.status}`;
6545
+ } catch {
6546
+ return `${resp.status}`;
6547
+ }
6548
+ }
6549
+
6550
+ // src/auth/device.ts
6551
+ import { setTimeout as sleep } from "timers/promises";
6552
+ var DeviceFlowError = class extends Error {
6553
+ constructor(message, code) {
6554
+ super(message);
6555
+ this.code = code;
6556
+ this.name = "DeviceFlowError";
6557
+ }
6558
+ code;
6559
+ };
6560
+ async function runDeviceFlow(opts) {
6561
+ const init = await issueDeviceCode(opts.cloudUrl, {
6562
+ client_label: opts.clientLabel,
6563
+ requested_scope: opts.requestedScope
6564
+ });
6565
+ if (opts.openBrowser) {
6566
+ try {
6567
+ await opts.openBrowser(init.verification_uri_complete);
6568
+ } catch {
6569
+ }
6570
+ }
6571
+ opts.onUserPrompt(init);
6572
+ const deadline = Date.now() + init.expires_in * 1e3;
6573
+ let interval = init.interval;
6574
+ while (Date.now() < deadline) {
6575
+ const before = Date.now();
6576
+ const result = await pollOnce(
6577
+ opts.cloudUrl,
6578
+ init.device_code,
6579
+ opts.pollOverride
6580
+ );
6581
+ if (result.kind === "approved") {
6582
+ return {
6583
+ apiKey: result.api_key,
6584
+ userId: result.user_id,
6585
+ handle: result.handle ?? null,
6586
+ scope: result.scope,
6587
+ keyId: result.key_id
6588
+ };
6589
+ }
6590
+ if (result.kind === "slow_down") {
6591
+ interval = Math.min(interval * 2, 30);
6592
+ }
6593
+ const elapsed = Date.now() - before;
6594
+ const wait = Math.max(0, interval * 1e3 - elapsed);
6595
+ await sleep(wait);
6596
+ }
6597
+ throw new DeviceFlowError(
6598
+ "Login window expired. Run `linkedclaw login` to retry.",
6599
+ "expired_token"
6600
+ );
6601
+ }
6602
+ async function pollOnce(cloudUrl, deviceCode, override) {
6603
+ const raw = override ? await override(deviceCode) : await rawPoll(cloudUrl, deviceCode);
6604
+ return interpretPoll(raw);
6605
+ }
6606
+ async function rawPoll(cloudUrl, deviceCode) {
6607
+ const resp = await fetch(`${cloudUrl}/api/v1/auth/device/poll`, {
6608
+ method: "POST",
6609
+ headers: { "Content-Type": "application/json" },
6610
+ body: JSON.stringify({ device_code: deviceCode })
6611
+ });
6612
+ if (resp.status === 400) {
6613
+ let detail = "invalid_grant";
6614
+ try {
6615
+ const j = await resp.json();
6616
+ if (j.detail) detail = j.detail;
6617
+ } catch {
6618
+ }
6619
+ if (detail === "slow_down") return { __slow: true };
6620
+ throw new DeviceFlowError(_humanize(detail), detail);
6621
+ }
6622
+ if (resp.status !== 200) {
6623
+ throw new DeviceFlowError(`Poll failed: HTTP ${resp.status}`, "http_error");
6624
+ }
6625
+ return await resp.json();
6626
+ }
6627
+ function interpretPoll(raw) {
6628
+ if (raw && typeof raw === "object" && "__slow" in raw) {
6629
+ return { kind: "slow_down" };
6630
+ }
6631
+ const obj = raw ?? {};
6632
+ if (obj.status === "pending") return { kind: "pending" };
6633
+ if (obj.status === "approved") {
6634
+ return {
6635
+ kind: "approved",
6636
+ api_key: String(obj.api_key),
6637
+ user_id: String(obj.user_id),
6638
+ handle: obj.handle ?? null,
6639
+ scope: String(obj.scope),
6640
+ key_id: String(obj.key_id)
6641
+ };
6642
+ }
6643
+ throw new DeviceFlowError(`Unexpected poll status: ${obj.status}`, "protocol_error");
6644
+ }
6645
+ function _humanize(code) {
6646
+ switch (code) {
6647
+ case "expired_token":
6648
+ return "Login window expired. Run `linkedclaw login` to retry.";
6649
+ case "access_denied":
6650
+ return "Authorization denied by user.";
6651
+ case "invalid_grant":
6652
+ return "Authorization session is no longer valid.";
6653
+ default:
6654
+ return code;
6655
+ }
6656
+ }
6657
+ async function issueDeviceCode(cloudUrl, body) {
6658
+ const resp = await fetch(`${cloudUrl}/api/v1/auth/device/code`, {
6659
+ method: "POST",
6660
+ headers: { "Content-Type": "application/json" },
6661
+ body: JSON.stringify(body)
6662
+ });
6663
+ if (resp.status !== 201) {
6664
+ let detail = `${resp.status}`;
6665
+ try {
6666
+ const j = await resp.json();
6667
+ if (j.detail) detail = j.detail;
6668
+ } catch {
6669
+ }
6670
+ throw new DeviceFlowError(`/auth/device/code ${resp.status}: ${detail}`, "init_failed");
6671
+ }
6672
+ return await resp.json();
6673
+ }
6674
+
6351
6675
  // src/commands/auth.ts
6676
+ var CLI_VERSION = "0.2.0";
6352
6677
  function registerAuthCommands(program2) {
6353
- program2.command("login").description("Store API key in ~/.linkedclaw/config.yaml").option("--api-key <key>", "API key (otherwise read from stdin)").option("--cloud-url <url>", "Override cloud URL").action(async (opts) => {
6678
+ program2.command("login").description(
6679
+ "Authenticate via browser (loopback PKCE; falls back to device code if headless)."
6680
+ ).option("--api-key <key>", "Skip browser; store key directly").option("--paste", "Skip browser; prompt for key on stdin").option("--device", "Force device flow (skip loopback)").option("--scope <scope>", "Requested scope: full | read | invoke", "full").option("--label <label>", "Client label override (default: auto-detected)").option("--cloud-url <url>", "Override cloud URL").action(async (opts) => {
6354
6681
  await runCommand(async () => {
6355
- let apiKey = opts.apiKey;
6356
- if (!apiKey) {
6357
- apiKey = await readLine("Paste API key: ");
6358
- }
6359
- if (!apiKey) throw new Error("empty api key");
6360
6682
  const prev = readFileConfig();
6361
- const next = { ...prev, apiKey, ...opts.cloudUrl ? { cloudUrl: opts.cloudUrl } : {} };
6362
- writeFileConfig(next);
6363
- return { ok: true, path: configPath() };
6683
+ const cloudUrl = opts.cloudUrl ?? prev.cloudUrl ?? process.env.LINKEDCLAW_CLOUD_URL ?? DEFAULT_CLOUD_URL;
6684
+ if (opts.apiKey || opts.paste) {
6685
+ let apiKey = opts.apiKey;
6686
+ if (!apiKey) apiKey = await readLine("Paste API key: ");
6687
+ if (!apiKey) throw new Error("empty api key");
6688
+ writeFileConfig({ ...prev, apiKey, cloudUrl });
6689
+ return { ok: true, mode: "paste", path: configPath() };
6690
+ }
6691
+ const clientLabel = opts.label ?? detectLabel(CLI_VERSION);
6692
+ const requestedScope = opts.scope ?? "full";
6693
+ const openBrowser = async (url) => {
6694
+ const open = (await import("open")).default;
6695
+ await open(url);
6696
+ };
6697
+ const forceDevice = Boolean(opts.device) || isHeadless();
6698
+ if (!forceDevice) {
6699
+ try {
6700
+ const result2 = await runLoopback({
6701
+ cloudUrl,
6702
+ clientLabel,
6703
+ requestedScope,
6704
+ openBrowser,
6705
+ onUserPrompt: (url) => {
6706
+ process.stderr.write(
6707
+ `Opened browser to ${url}
6708
+ Approve the request to continue.
6709
+ `
6710
+ );
6711
+ }
6712
+ });
6713
+ writeFileConfig({ ...prev, apiKey: result2.apiKey, cloudUrl });
6714
+ return {
6715
+ ok: true,
6716
+ mode: "loopback",
6717
+ path: configPath(),
6718
+ user_id: result2.userId,
6719
+ handle: result2.handle,
6720
+ scope: result2.scope,
6721
+ key_id: result2.keyId
6722
+ };
6723
+ } catch (err) {
6724
+ if (err instanceof LoopbackError && err.recoverable) {
6725
+ process.stderr.write(`${err.message}
6726
+ `);
6727
+ } else {
6728
+ throw err;
6729
+ }
6730
+ }
6731
+ }
6732
+ const result = await runDeviceFlow({
6733
+ cloudUrl,
6734
+ clientLabel,
6735
+ requestedScope,
6736
+ openBrowser,
6737
+ onUserPrompt: (info) => {
6738
+ process.stderr.write(
6739
+ `
6740
+ \u{1F513} LinkedClaw login
6741
+
6742
+ Open this URL in your browser:
6743
+ ${info.verification_uri_complete}
6744
+
6745
+ Or visit ${info.verification_uri} and enter:
6746
+ ${info.user_code}
6747
+
6748
+ Waiting for approval... (${Math.floor(info.expires_in / 60)}m ${info.expires_in % 60}s remaining)
6749
+ `
6750
+ );
6751
+ }
6752
+ });
6753
+ writeFileConfig({ ...prev, apiKey: result.apiKey, cloudUrl });
6754
+ return {
6755
+ ok: true,
6756
+ mode: "device",
6757
+ path: configPath(),
6758
+ user_id: result.userId,
6759
+ handle: result.handle,
6760
+ scope: result.scope,
6761
+ key_id: result.keyId
6762
+ };
6364
6763
  });
6365
6764
  });
6366
6765
  program2.command("register").description("Open browser to create a LinkedClaw account, then paste your API key").option("--no-browser", "Print URL instead of attempting to open the browser").option("--cloud-url <url>", "Override cloud URL").action(async (opts) => {
@@ -6430,7 +6829,7 @@ function tryParseJson(v) {
6430
6829
 
6431
6830
  // src/commands/converge.ts
6432
6831
  import { spawnSync } from "child_process";
6433
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
6832
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
6434
6833
  import { isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
6435
6834
 
6436
6835
  // src/converge/api.ts
@@ -6565,7 +6964,7 @@ function makeConvergeApi(cloudUrl, apiKey) {
6565
6964
  }
6566
6965
 
6567
6966
  // src/converge/hash.ts
6568
- import { createHash as createHash2 } from "crypto";
6967
+ import { createHash as createHash3 } from "crypto";
6569
6968
  function encodeString(s) {
6570
6969
  let out = '"';
6571
6970
  for (let i = 0; i < s.length; i++) {
@@ -6591,7 +6990,7 @@ function canonicalize(value) {
6591
6990
  return "{" + keys.map((k) => encodeString(k) + ":" + canonicalize(value[k])).join(",") + "}";
6592
6991
  }
6593
6992
  function sha256OfCanonicalJson(value) {
6594
- const h = createHash2("sha256");
6993
+ const h = createHash3("sha256");
6595
6994
  h.update(canonicalize(value));
6596
6995
  return "sha256:" + h.digest("hex");
6597
6996
  }
@@ -6625,15 +7024,15 @@ function acquireLock(stagingDir) {
6625
7024
  }
6626
7025
 
6627
7026
  // src/converge/staging.ts
6628
- import { createHash as createHash3 } from "crypto";
6629
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync2 } from "fs";
7027
+ import { createHash as createHash4 } from "crypto";
7028
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync2 } from "fs";
6630
7029
  import { dirname as dirname2, join as join3 } from "path";
6631
7030
  import { load as yamlLoad2, dump as yamlDump2 } from "js-yaml";
6632
7031
  function stagingPathFor(stagingDir, cruxId) {
6633
7032
  return join3(stagingDir, `${cruxId}.md`);
6634
7033
  }
6635
7034
  function listCruxFiles(stagingDir) {
6636
- if (!existsSync2(stagingDir)) return [];
7035
+ if (!existsSync3(stagingDir)) return [];
6637
7036
  return readdirSync(stagingDir).filter(
6638
7037
  (f) => f.endsWith(".md") && !f.startsWith(".")
6639
7038
  );
@@ -6676,17 +7075,17 @@ function writeStaging(path2, doc) {
6676
7075
  writeFileSync2(path2, dumpStaging(doc), "utf8");
6677
7076
  }
6678
7077
  function computePaBodyHash(body) {
6679
- return "sha256:" + createHash3("sha256").update(Buffer.from(body, "utf8")).digest("hex");
7078
+ return "sha256:" + createHash4("sha256").update(Buffer.from(body, "utf8")).digest("hex");
6680
7079
  }
6681
7080
 
6682
7081
  // src/converge/workspace.ts
6683
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
7082
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
6684
7083
  import { dirname as dirname3, isAbsolute, join as join4, resolve } from "path";
6685
7084
  import { load as yamlLoad3, dump as yamlDump3 } from "js-yaml";
6686
7085
  var META_FILENAME = ".run-meta.yaml";
6687
7086
  function readRunMeta(stagingDir) {
6688
7087
  const metaPath = join4(stagingDir, META_FILENAME);
6689
- if (!existsSync3(metaPath)) return null;
7088
+ if (!existsSync4(metaPath)) return null;
6690
7089
  return yamlLoad3(readFileSync5(metaPath, "utf8"));
6691
7090
  }
6692
7091
  function writeRunMeta(stagingDir, meta) {
@@ -6696,7 +7095,7 @@ function writeRunMeta(stagingDir, meta) {
6696
7095
  function searchUpward(startDir, maxLevels = 5) {
6697
7096
  let dir = startDir;
6698
7097
  for (let i = 0; i < maxLevels; i++) {
6699
- if (existsSync3(join4(dir, META_FILENAME))) return dir;
7098
+ if (existsSync4(join4(dir, META_FILENAME))) return dir;
6700
7099
  const parent = dirname3(dir);
6701
7100
  if (parent === dir) break;
6702
7101
  dir = parent;
@@ -6949,7 +7348,7 @@ async function buildCruxDecisionRequest(api, ws, cruxId, action, opts = {}) {
6949
7348
  let synthesisEdited = false;
6950
7349
  if (action === "accept") {
6951
7350
  const stagingPath = safeStagingPathFor(ws.stagingDir, cruxId);
6952
- if (existsSync4(stagingPath)) {
7351
+ if (existsSync5(stagingPath)) {
6953
7352
  const doc = readStaging(stagingPath);
6954
7353
  synthesisEdited = computePaBodyHash(doc.body) !== doc.frontmatter.pa_body_hash;
6955
7354
  if (synthesisEdited) acceptedSynthesisText = doc.body;
@@ -6980,7 +7379,7 @@ async function postCruxDecision(api, ws, cruxId, action, opts = {}) {
6980
7379
  }
6981
7380
  async function materializeAcceptedCrux(ctx, api, ws, cruxId, payload, opts = {}) {
6982
7381
  const stagingPath = safeStagingPathFor(ws.stagingDir, cruxId);
6983
- if (!existsSync4(stagingPath)) return { warning: `staging_not_found: ${stagingPath}` };
7382
+ if (!existsSync5(stagingPath)) return { warning: `staging_not_found: ${stagingPath}` };
6984
7383
  const doc = readStaging(stagingPath);
6985
7384
  const fm = doc.frontmatter;
6986
7385
  const sourceDebate = await api.getDebate(ws.sourceDebateId);
@@ -7024,11 +7423,11 @@ async function materializeAcceptedCrux(ctx, api, ws, cruxId, payload, opts = {})
7024
7423
  const synthSlug = extractSynthesisSlug(body);
7025
7424
  const finalDir = join5(ws.targetCorpus, "converged", topicSlug);
7026
7425
  const finalPath = safeAcceptedPath(finalDir, cruxId, synthSlug);
7027
- if (existsSync4(finalPath) && readStaging(finalPath).body !== acceptedDoc.body) {
7426
+ if (existsSync5(finalPath) && readStaging(finalPath).body !== acceptedDoc.body) {
7028
7427
  return { warning: `sync_conflict: ${finalPath}` };
7029
7428
  }
7030
7429
  mkdirSync4(finalDir, { recursive: true });
7031
- if (!existsSync4(finalPath) || dumpStaging(readStaging(finalPath)) !== dumpStaging(acceptedDoc)) {
7430
+ if (!existsSync5(finalPath) || dumpStaging(readStaging(finalPath)) !== dumpStaging(acceptedDoc)) {
7032
7431
  writeStaging(finalPath, acceptedDoc);
7033
7432
  }
7034
7433
  unlinkSync2(stagingPath);
@@ -7092,7 +7491,7 @@ async function syncTerminalDecisions(ctx, api, ws, opts = {}) {
7092
7491
  if (result.warning) warnings.push(`${cid}: ${result.warning}`);
7093
7492
  continue;
7094
7493
  }
7095
- if (existsSync4(stagingPath)) {
7494
+ if (existsSync5(stagingPath)) {
7096
7495
  unlinkSync2(stagingPath);
7097
7496
  cleaned.push(cid);
7098
7497
  }
@@ -7208,7 +7607,7 @@ function registerConvergeCommands(program2) {
7208
7607
  const body = buildPaBody(op);
7209
7608
  const newPaBodyHash = computePaBodyHash(body);
7210
7609
  const path2 = safeStagingPathFor(ws.stagingDir, c.crux_id);
7211
- const existingDoc = existsSync4(path2) ? readStaging(path2) : null;
7610
+ const existingDoc = existsSync5(path2) ? readStaging(path2) : null;
7212
7611
  if (existingDoc && existingDoc.frontmatter.pa_body_hash === newPaBodyHash) continue;
7213
7612
  const fm = {
7214
7613
  debate_id: ws.sourceDebateId,
@@ -8759,9 +9158,9 @@ async function resolveManifestOpt(manifestOpt, manifestId, intention, defaultInt
8759
9158
 
8760
9159
  // src/bin.ts
8761
9160
  var pkgPath = join6(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
8762
- var CLI_VERSION = JSON.parse(readFileSync8(pkgPath, "utf8")).version;
9161
+ var CLI_VERSION2 = JSON.parse(readFileSync8(pkgPath, "utf8")).version;
8763
9162
  var program = new Command();
8764
- program.name("linkedclaw").description("Official LinkedClaw CLI \u2014 any agent can shell out to hire providers, invoke, or gig task").version(`cli ${CLI_VERSION}`);
9163
+ program.name("linkedclaw").description("Official LinkedClaw CLI \u2014 any agent can shell out to hire providers, invoke, or gig task").version(`cli ${CLI_VERSION2}`);
8765
9164
  registerAuthCommands(program);
8766
9165
  registerRequesterCommands(program);
8767
9166
  registerProviderCommands(program);