@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 +433 -34
- package/dist/bin.js.map +1 -1
- package/package.json +4 -4
- package/src/auth/decide.ts +48 -0
- package/src/auth/device.ts +203 -0
- package/src/auth/loopback.ts +239 -0
- package/src/auth/pkce.ts +22 -0
- package/src/commands/auth.ts +96 -10
- package/test/auth-decide.test.ts +38 -0
- package/test/auth-device.test.ts +126 -0
- package/test/auth-loopback.test.ts +190 -0
- package/test/auth-pkce.test.ts +32 -0
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.
|
|
5744
|
+
* Open a session.
|
|
5745
5745
|
*
|
|
5746
|
-
* Default
|
|
5747
|
-
*
|
|
5748
|
-
*
|
|
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(
|
|
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
|
|
6362
|
-
|
|
6363
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
6629
|
-
import { existsSync as
|
|
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 (!
|
|
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:" +
|
|
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
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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 =
|
|
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
|
|
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 ${
|
|
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);
|