@tonbo/yield 0.3.10 → 0.3.11
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/README.md +2 -2
- package/dist/bin/yield.js +855 -603
- package/package.json +1 -1
package/dist/bin/yield.js
CHANGED
|
@@ -1253,6 +1253,12 @@ async function assertPrivateRegularFile(filename) {
|
|
|
1253
1253
|
var exec = promisify(execFile);
|
|
1254
1254
|
var DEFAULT_LOGIN_HINT = credentialsContract.reader.login_hint;
|
|
1255
1255
|
var LOOPBACK_HOST = "127.0.0.1";
|
|
1256
|
+
var BrowserUnavailableError = class extends Error {
|
|
1257
|
+
};
|
|
1258
|
+
function prefersDeviceLogin(env = process.env) {
|
|
1259
|
+
const ssh = Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
|
|
1260
|
+
return ssh && !env.DISPLAY && !env.WAYLAND_DISPLAY;
|
|
1261
|
+
}
|
|
1256
1262
|
function newLoginSecret() {
|
|
1257
1263
|
return randomBytes(32).toString("base64url");
|
|
1258
1264
|
}
|
|
@@ -1268,7 +1274,8 @@ var AuthClient = class {
|
|
|
1268
1274
|
hostname;
|
|
1269
1275
|
loginHint;
|
|
1270
1276
|
entryLine;
|
|
1271
|
-
|
|
1277
|
+
env;
|
|
1278
|
+
constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname(), loginHint = DEFAULT_LOGIN_HINT, entryLine = "cli", env = process.env) {
|
|
1272
1279
|
this.credentials = credentials;
|
|
1273
1280
|
this.fetcher = fetcher;
|
|
1274
1281
|
this.loginOrigin = loginOrigin2;
|
|
@@ -1276,6 +1283,7 @@ var AuthClient = class {
|
|
|
1276
1283
|
this.hostname = hostname;
|
|
1277
1284
|
this.loginHint = loginHint;
|
|
1278
1285
|
this.entryLine = entryLine;
|
|
1286
|
+
this.env = env;
|
|
1279
1287
|
}
|
|
1280
1288
|
/** The personal key requests are made with: TONBO_API_KEY, or the saved
|
|
1281
1289
|
* login while it has not expired. */
|
|
@@ -1295,12 +1303,16 @@ var AuthClient = class {
|
|
|
1295
1303
|
* a free port, then the one-time code is redeemed, with this device's login
|
|
1296
1304
|
* secret, for the account's login key, which replaces this device's earlier
|
|
1297
1305
|
* login key there ("CLI on <hostname>" in a personal account, `Yield CLI ·
|
|
1298
|
-
* <8 hex>` in any other).
|
|
1299
|
-
*
|
|
1300
|
-
*
|
|
1306
|
+
* <8 hex>` in any other). With `device`, in an SSH session without a
|
|
1307
|
+
* display, or when no browser can be opened, the same PKCE challenge goes
|
|
1308
|
+
* to a device-code login instead: the person enters a short code at
|
|
1309
|
+
* /cli/device in any browser and the CLI polls for the key. The key joins
|
|
1310
|
+
* the file's `accounts`; it becomes the top level too unless `current` is
|
|
1311
|
+
* false (a login made only to publish into an organization). Answers that
|
|
1312
|
+
* account's login.
|
|
1301
1313
|
*/
|
|
1302
1314
|
async login(progress2 = () => {
|
|
1303
|
-
}, account, { current: current2 = true } = {}) {
|
|
1315
|
+
}, account, { current: current2 = true, device } = {}) {
|
|
1304
1316
|
const existing = await this.credentials.load().catch((error) => {
|
|
1305
1317
|
if (error instanceof UnusableLoginError)
|
|
1306
1318
|
return null;
|
|
@@ -1309,6 +1321,30 @@ var AuthClient = class {
|
|
|
1309
1321
|
const loginSecret = existing?.login_secret ?? newLoginSecret();
|
|
1310
1322
|
const verifier = randomBytes(48).toString("base64url");
|
|
1311
1323
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
1324
|
+
const redeemed = device ?? prefersDeviceLogin(this.env) ? await this.deviceLogin(challenge, verifier, loginSecret, account, progress2) : await this.browserLogin(challenge, verifier, loginSecret, account, progress2).catch((error) => {
|
|
1325
|
+
if (!(error instanceof BrowserUnavailableError))
|
|
1326
|
+
throw error;
|
|
1327
|
+
progress2({ status: "failed", step: "browser", reason: error.message });
|
|
1328
|
+
return this.deviceLogin(challenge, verifier, loginSecret, account, progress2);
|
|
1329
|
+
});
|
|
1330
|
+
const login = {
|
|
1331
|
+
account_id: redeemed.account_id,
|
|
1332
|
+
key_id: redeemed.api_key.id,
|
|
1333
|
+
key: redeemed.key,
|
|
1334
|
+
expires_at: redeemed.api_key.expires_at,
|
|
1335
|
+
kind: redeemed.account_kind ?? null,
|
|
1336
|
+
name: redeemed.account_name ?? null
|
|
1337
|
+
};
|
|
1338
|
+
progress2({ status: "started", step: "credential-store" });
|
|
1339
|
+
const latest = await this.credentials.load().catch(() => existing);
|
|
1340
|
+
await this.credentials.save(withAccountLogin(latest, login, {
|
|
1341
|
+
current: current2,
|
|
1342
|
+
loginSecret: latest?.login_secret ?? loginSecret
|
|
1343
|
+
}));
|
|
1344
|
+
progress2({ status: "completed", step: "credential-store" });
|
|
1345
|
+
return login;
|
|
1346
|
+
}
|
|
1347
|
+
async browserLogin(challenge, verifier, loginSecret, account, progress2) {
|
|
1312
1348
|
const state = randomBytes(24).toString("base64url");
|
|
1313
1349
|
const code = await this.listenForCode(state, (redirectUri) => {
|
|
1314
1350
|
const authorization = new URL("/cli/login", this.loginOrigin);
|
|
@@ -1331,34 +1367,85 @@ var AuthClient = class {
|
|
|
1331
1367
|
body: JSON.stringify({ code, code_verifier: verifier, login_secret: loginSecret })
|
|
1332
1368
|
});
|
|
1333
1369
|
progress2({ status: "completed", step: "key-redemption" });
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1370
|
+
return redeemed;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* The device-code login (RFC 8628's shape): the PKCE challenge and the
|
|
1374
|
+
* hostname buy a device code and a user code; the person approves the user
|
|
1375
|
+
* code at /cli/device, seeing the hostname; the CLI polls with the device
|
|
1376
|
+
* code, its verifier and the login secret, all in POST bodies, and the
|
|
1377
|
+
* approved poll answers the key once. A denied or expired code ends it.
|
|
1378
|
+
*/
|
|
1379
|
+
async deviceLogin(challenge, verifier, loginSecret, account, progress2) {
|
|
1380
|
+
progress2({ status: "started", step: "device-code" });
|
|
1381
|
+
const started = await requestJson(this.fetcher, new URL("/api/cli/login/device", this.loginOrigin).toString(), {
|
|
1382
|
+
method: "POST",
|
|
1383
|
+
headers: { "content-type": "application/json" },
|
|
1384
|
+
body: JSON.stringify({
|
|
1385
|
+
code_challenge: challenge,
|
|
1386
|
+
code_challenge_method: "S256",
|
|
1387
|
+
hostname: this.hostname
|
|
1388
|
+
})
|
|
1389
|
+
});
|
|
1390
|
+
progress2({ status: "completed", step: "device-code" });
|
|
1391
|
+
const page = new URL("/cli/device", this.loginOrigin);
|
|
1392
|
+
const complete = new URL(page);
|
|
1393
|
+
complete.search = new URLSearchParams({
|
|
1394
|
+
code: started.user_code,
|
|
1395
|
+
from: this.entryLine,
|
|
1396
|
+
...account ? { account } : {}
|
|
1397
|
+
}).toString();
|
|
1398
|
+
progress2({
|
|
1399
|
+
status: "device-code",
|
|
1400
|
+
step: "device-authorization",
|
|
1401
|
+
userCode: started.user_code,
|
|
1402
|
+
url: page.toString(),
|
|
1403
|
+
completeUrl: complete.toString(),
|
|
1404
|
+
expiresIn: started.expires_in
|
|
1405
|
+
});
|
|
1406
|
+
progress2({ status: "started", step: "device-authorization" });
|
|
1407
|
+
const expired = new Error(`The login code expired. Run \`${this.loginHint}\` again.`);
|
|
1408
|
+
const deadline = Date.now() + started.expires_in * 1e3;
|
|
1409
|
+
let interval = started.interval;
|
|
1410
|
+
for (; ; ) {
|
|
1411
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
|
|
1412
|
+
try {
|
|
1413
|
+
const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/device/token", this.loginOrigin).toString(), {
|
|
1414
|
+
method: "POST",
|
|
1415
|
+
headers: { "content-type": "application/json" },
|
|
1416
|
+
body: JSON.stringify({
|
|
1417
|
+
device_code: started.device_code,
|
|
1418
|
+
code_verifier: verifier,
|
|
1419
|
+
login_secret: loginSecret
|
|
1420
|
+
})
|
|
1421
|
+
});
|
|
1422
|
+
progress2({ status: "completed", step: "device-authorization" });
|
|
1423
|
+
return redeemed;
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
const code = error instanceof HttpError ? error.code : null;
|
|
1426
|
+
if (code === "slow_down")
|
|
1427
|
+
interval += 5;
|
|
1428
|
+
else if (code === "access_denied")
|
|
1429
|
+
throw new Error("The login was not approved (access_denied).");
|
|
1430
|
+
else if (code === "expired_token")
|
|
1431
|
+
throw expired;
|
|
1432
|
+
else if (code !== "authorization_pending" && !transient(error))
|
|
1433
|
+
throw error;
|
|
1434
|
+
if (Date.now() >= deadline)
|
|
1435
|
+
throw expired;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1350
1438
|
}
|
|
1351
1439
|
listenForCode(expectedState, authorizationUrl, progress2) {
|
|
1352
1440
|
return new Promise((resolve, reject) => {
|
|
1353
1441
|
let settled = false;
|
|
1354
1442
|
const sockets = /* @__PURE__ */ new Set();
|
|
1355
1443
|
let authorizationStarted = false;
|
|
1356
|
-
const startAuthorization = (
|
|
1444
|
+
const startAuthorization = () => {
|
|
1357
1445
|
if (authorizationStarted)
|
|
1358
1446
|
return;
|
|
1359
1447
|
authorizationStarted = true;
|
|
1360
|
-
|
|
1361
|
-
progress2({ status: "completed", step: "browser" });
|
|
1448
|
+
progress2({ status: "completed", step: "browser" });
|
|
1362
1449
|
progress2({ status: "started", step: "browser-authorization" });
|
|
1363
1450
|
};
|
|
1364
1451
|
const finish = (result, completedResponseSocket) => {
|
|
@@ -1428,18 +1515,17 @@ var AuthClient = class {
|
|
|
1428
1515
|
void Promise.resolve().then(() => this.browserOpener(url)).then(() => startAuthorization(), (error) => {
|
|
1429
1516
|
if (settled || authorizationStarted)
|
|
1430
1517
|
return;
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
step: "browser",
|
|
1434
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
1435
|
-
callbackPort: port
|
|
1518
|
+
finish({
|
|
1519
|
+
error: new BrowserUnavailableError(error instanceof Error ? error.message : String(error))
|
|
1436
1520
|
});
|
|
1437
|
-
startAuthorization(false);
|
|
1438
1521
|
});
|
|
1439
1522
|
});
|
|
1440
1523
|
});
|
|
1441
1524
|
}
|
|
1442
1525
|
};
|
|
1526
|
+
function transient(error) {
|
|
1527
|
+
return error instanceof HttpError ? error.status === 429 || error.status >= 500 : error instanceof TypeError;
|
|
1528
|
+
}
|
|
1443
1529
|
function respond(response, outcome, finished) {
|
|
1444
1530
|
const page = callbackResponse(outcome);
|
|
1445
1531
|
if (finished)
|
|
@@ -1918,7 +2004,7 @@ var USD = new Intl.NumberFormat("en-US", {
|
|
|
1918
2004
|
// ../../packages/tonbo-cli-core/dist/src/login.js
|
|
1919
2005
|
async function loginCommand(deps, options = {}) {
|
|
1920
2006
|
try {
|
|
1921
|
-
const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account, { current: options.current });
|
|
2007
|
+
const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account, { current: options.current, device: options.device });
|
|
1922
2008
|
deps.output(`Logged in to account ${login.name ?? login.account_id}.${login.expires_at ? ` The login expires ${login.expires_at}.` : ""}`);
|
|
1923
2009
|
return login;
|
|
1924
2010
|
} catch (error) {
|
|
@@ -1947,6 +2033,14 @@ var LOGIN_PROGRESS_MESSAGES = {
|
|
|
1947
2033
|
started: "Receiving the CLI key",
|
|
1948
2034
|
completed: "CLI key received"
|
|
1949
2035
|
},
|
|
2036
|
+
"device-code": {
|
|
2037
|
+
started: "Requesting a login code",
|
|
2038
|
+
completed: "Login code received"
|
|
2039
|
+
},
|
|
2040
|
+
"device-authorization": {
|
|
2041
|
+
started: "Waiting for approval in a browser",
|
|
2042
|
+
completed: "Login approved"
|
|
2043
|
+
},
|
|
1950
2044
|
"credential-store": {
|
|
1951
2045
|
started: "Saving login",
|
|
1952
2046
|
completed: "Login saved"
|
|
@@ -1958,9 +2052,13 @@ function reportLoginProgress(progress2, event) {
|
|
|
1958
2052
|
return;
|
|
1959
2053
|
}
|
|
1960
2054
|
if (event.status === "failed") {
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2055
|
+
progress2.fail(`Could not open a browser: ${event.reason.replace(/\.$/, "")}. Approve with a code instead.`);
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (event.status === "device-code") {
|
|
2059
|
+
const minutes = Math.max(1, Math.round(event.expiresIn / 60));
|
|
2060
|
+
progress2.note(`To approve this login, open ${event.url} in any browser and enter the code ${event.userCode}. It expires in ${minutes} minutes.`);
|
|
2061
|
+
progress2.note(`Or open ${event.completeUrl}`);
|
|
1964
2062
|
return;
|
|
1965
2063
|
}
|
|
1966
2064
|
const messages = LOGIN_PROGRESS_MESSAGES[event.step];
|
|
@@ -3397,8 +3495,8 @@ var recallContract = {
|
|
|
3397
3495
|
"GET /files?repo=<name>&path=<path>&limit=<n>": "the turns that changed the file, newest first: {repo, path, change, direct, session, turn, src, device, sub, commit, base, at_ms, cwd, git_branch, stream_offset_from, stream_offset_to}; a row of another author's turn answers no src, device, cwd, git_branch or stream offsets (team_metadata)",
|
|
3398
3496
|
"GET /sessions/<session id>/files?limit=<n>": "the files the session changed, newest first: {repo, path, change, direct, turn, commit, at_ms}",
|
|
3399
3497
|
"GET /sessions?repo=<name>&limit=<n>": "sessions seen in the repository, most recent activity first: {session, src, device, sub, cwd, git_branch, started_ms, last_turn, last_ms}; a row of another author's session answers no src, device, cwd or git_branch (team_metadata)",
|
|
3400
|
-
"GET /sessions?scope=team&limit=<n>": "the team catalog: the catalog's rows for every author in the workspace's account, each with its author (the stamped sub), under the catalog's credential rule; in an organization's workspace that is every member's team sessions (ADR-0017 D-1), in a personal one the person's own; the caller's own rows are the catalog's, and another author's row answers team_metadata's fields only: {session, author, last_ms, state, state_ms, paths_written, turns}",
|
|
3401
|
-
"GET /sessions?limit=<n>": "the catalog (docs/design/yield-session-state.md): the sessions whose records the caller appended (their stamped author is the token's sub), most recent activity first, for a person's own key only (a token bound by git_workspace with no agent_id; an Agent's key and a token minted before ADR-0016 PR 3 answer 403) holding git.repos.read on the whole workspace (git:<ws_hex>:*) and denied it on none of it: {session, src, device, device_handle, cwd, git_root, git_branch, started_ms, last_turn, last_ms, state, state_ms, paths_written, paths_inferred}; a row of the organization-published form answers device, cwd and git_root null and names its device_handle, null otherwise; state is the session's latest session_state in stream order, null when none was reported, and paths_written is at most caps.catalog_paths paths, the turn in progress's first, then those of its newest turns; paths_inferred is the same of capture-v1 inbox.paths_inferred, kept apart and never exact"
|
|
3498
|
+
"GET /sessions?scope=team&limit=<n>": "the team catalog: the catalog's rows for every author in the workspace's account, each with its author (the stamped sub), under the catalog's credential rule; in an organization's workspace that is every member's team sessions (ADR-0017 D-1), in a personal one the person's own; the caller's own rows are the catalog's, and another author's row answers team_metadata's fields only: {session, author, project_id, last_ms, state, state_ms, paths_written, turns}",
|
|
3499
|
+
"GET /sessions?limit=<n>": "the catalog (docs/design/yield-session-state.md): the sessions whose records the caller appended (their stamped author is the token's sub), most recent activity first, for a person's own key only (a token bound by git_workspace with no agent_id; an Agent's key and a token minted before ADR-0016 PR 3 answer 403) holding git.repos.read on the whole workspace (git:<ws_hex>:*) and denied it on none of it: {session, src, device, device_handle, project_id, cwd, git_root, git_branch, started_ms, last_turn, last_ms, state, state_ms, paths_written, paths_inferred, turns}; a row of the organization-published form answers device, cwd and git_root null and names its device_handle, null otherwise; state is the session's latest session_state in stream order, null when none was reported, and paths_written is at most caps.catalog_paths paths, the turn in progress's first, then those of its newest turns; paths_inferred is the same of capture-v1 inbox.paths_inferred, kept apart and never exact; project_id is the Project the session's latest record named (capture-v1 inbox.project), null when none did, which the person's sessions of one Project on their other devices share; turns is team_metadata's turns, so the person's daemons compare their own sessions across devices (conflict alerts)"
|
|
3402
3500
|
},
|
|
3403
3501
|
"organization": "rows indexed from an organization's proc.inbox, whose records take the published form (contracts/sessions/capture-v1.json inbox.organization_published), answer cwd null, name the device only by its device_handle and never by a device id, and name paths relative to the bound folder; their provenance (session, stream_offset_from, stream_offset_to) points into the author's own session.<session> in the author's personal store (capture-v1 team_session_stream), or for a session captured before ADR-0017's metadata-only amendment into session.t.<session> in the organization's store, and only the author's rows carry it",
|
|
3404
3502
|
"provenance": "every row the caller may read raw names its session, turn, commit and time; a reader reads the turn's records from session.<session> between stream_offset_from and stream_offset_to with its own Streams token. A team session's author reads them from their own personal store, and from the records they wrote to session.t.<session> before the metadata-only amendment; a teammate never reads them (team_metadata.raw)",
|
|
@@ -6558,105 +6656,609 @@ var LineFilter = class {
|
|
|
6558
6656
|
|
|
6559
6657
|
// src/conflicts.ts
|
|
6560
6658
|
import { execFile as execFile5 } from "node:child_process";
|
|
6561
|
-
import { createHash as
|
|
6659
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
6562
6660
|
import path16 from "node:path";
|
|
6563
6661
|
import { promisify as promisify5 } from "node:util";
|
|
6564
6662
|
|
|
6565
|
-
// src/
|
|
6566
|
-
import {
|
|
6567
|
-
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
6568
|
-
import { appendFile, mkdir as mkdir5, open as open8, readFile as readFile9, rename as rename7, rm as rm7 } from "node:fs/promises";
|
|
6569
|
-
import path15 from "node:path";
|
|
6570
|
-
import { promisify as promisify4 } from "node:util";
|
|
6571
|
-
import { setTimeout as delay4 } from "node:timers/promises";
|
|
6572
|
-
|
|
6573
|
-
// src/handoffs.ts
|
|
6574
|
-
async function listHandoffs(request) {
|
|
6575
|
-
const data = await request("listHandoffs", {});
|
|
6576
|
-
if (!Array.isArray(data)) throw new Error("The handoff list answered an invalid body.");
|
|
6577
|
-
return data;
|
|
6578
|
-
}
|
|
6579
|
-
async function readHandoffEvents(request, after, wait) {
|
|
6580
|
-
const answer = await request("listHandoffEvents", {
|
|
6581
|
-
query: { after: String(after), wait: String(wait) }
|
|
6582
|
-
});
|
|
6583
|
-
if (!Array.isArray(answer?.events) || !Number.isSafeInteger(answer.next_after))
|
|
6584
|
-
throw new Error("The handoff events answered an invalid body.");
|
|
6585
|
-
return answer;
|
|
6586
|
-
}
|
|
6587
|
-
async function cancelHandoff(request, link3) {
|
|
6588
|
-
const id = handoffId(link3);
|
|
6589
|
-
const answer = await request(
|
|
6590
|
-
"cancelPersonalHandoff",
|
|
6591
|
-
{ params: { id } }
|
|
6592
|
-
);
|
|
6593
|
-
if (answer.id !== id || answer.state !== "cancelled")
|
|
6594
|
-
throw new Error("The handoff answered an invalid cancellation.");
|
|
6595
|
-
return answer;
|
|
6596
|
-
}
|
|
6597
|
-
function handoffListLine(item, me, name) {
|
|
6598
|
-
const who = (sub) => sub === me ? "you" : name(item.account_id, sub);
|
|
6599
|
-
const parts = [`by ${who(item.author_sub)}`];
|
|
6600
|
-
if (item.target_kind === "person" && item.target_sub) parts.push(`for ${who(item.target_sub)}`);
|
|
6601
|
-
if (item.target_kind === "open") parts.push("for any member");
|
|
6602
|
-
if (item.target_kind === "agent") parts.push(`for ${item.target_harness ?? "another harness"}`);
|
|
6603
|
-
if (item.target_kind === "machine")
|
|
6604
|
-
parts.push(`dispatched to your device on ${item.target_harness ?? "a harness"}`);
|
|
6605
|
-
if (item.claimed_by_sub) parts.push(`taken by ${who(item.claimed_by_sub)}`);
|
|
6606
|
-
return `${item.state.padEnd(10)} ${item.link} ${item.task} (${parts.join(", ")})`;
|
|
6607
|
-
}
|
|
6608
|
-
var VERBS2 = {
|
|
6609
|
-
created: "created",
|
|
6610
|
-
claimed: "took",
|
|
6611
|
-
running: "started",
|
|
6612
|
-
done: "finished",
|
|
6613
|
-
need_human: "needs a human on",
|
|
6614
|
-
failed: "failed",
|
|
6615
|
-
returned: "returned",
|
|
6616
|
-
cancelled: "cancelled"
|
|
6617
|
-
};
|
|
6618
|
-
function handoffEventLine(event, me, name) {
|
|
6619
|
-
const actor = !event.actor_sub ? "someone" : event.actor_sub === me ? "you" : name(event.account_id, event.actor_sub);
|
|
6620
|
-
return `#${event.seq} ${event.at} ${actor} ${VERBS2[event.kind] ?? event.kind} ${event.link} ${event.task}`;
|
|
6621
|
-
}
|
|
6663
|
+
// src/device-conflicts.ts
|
|
6664
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
6622
6665
|
|
|
6623
|
-
// src/
|
|
6624
|
-
var
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6666
|
+
// src/generated/machine.ts
|
|
6667
|
+
var machineRegisterDefs = {
|
|
6668
|
+
"byoc_arch": {
|
|
6669
|
+
"description": "A `byoc` device's architecture; grows only by a contract change. The regional machine_byoc_arches() equals it.",
|
|
6670
|
+
"enum": [
|
|
6671
|
+
"arm64",
|
|
6672
|
+
"amd64"
|
|
6673
|
+
]
|
|
6674
|
+
},
|
|
6675
|
+
"byoc_client": {
|
|
6676
|
+
"description": "The client that registered a `byoc` device; grows only by a contract change. The regional machine_byoc_clients() equals it.",
|
|
6677
|
+
"enum": [
|
|
6678
|
+
"yield"
|
|
6679
|
+
]
|
|
6680
|
+
},
|
|
6681
|
+
"byoc_client_version": {
|
|
6682
|
+
"description": "The client's version, a semantic version of at most 64 octets.",
|
|
6683
|
+
"type": "string",
|
|
6684
|
+
"maxLength": 64,
|
|
6685
|
+
"pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
|
|
6686
|
+
},
|
|
6687
|
+
"byoc_harness": {
|
|
6688
|
+
"description": "An agent harness `yield` captures on a `byoc` device: a key of contracts/sessions/capture-v1.json `sources`, its one authority, which automation/tests/test_machines_machine_v2_contract.py holds this enum equal to. The regional machine_byoc_harnesses() equals it.",
|
|
6689
|
+
"enum": [
|
|
6690
|
+
"claude-code",
|
|
6691
|
+
"codex"
|
|
6692
|
+
]
|
|
6693
|
+
},
|
|
6694
|
+
"byoc_harnesses": {
|
|
6695
|
+
"description": "The harnesses installed on the device that its client captures, without duplicates.",
|
|
6696
|
+
"type": "array",
|
|
6697
|
+
"uniqueItems": true,
|
|
6698
|
+
"items": {
|
|
6699
|
+
"$ref": "#/$defs/byoc_harness"
|
|
6700
|
+
}
|
|
6701
|
+
},
|
|
6702
|
+
"byoc_hostname": {
|
|
6703
|
+
"description": "The device's hostname as the client reports it, at most 253 octets (the database counts octets). Personal data, answered to the device's owner only (machine-v2.json `byoc.visibility`).",
|
|
6704
|
+
"type": "string",
|
|
6705
|
+
"minLength": 1,
|
|
6706
|
+
"maxLength": 253
|
|
6707
|
+
},
|
|
6708
|
+
"byoc_os": {
|
|
6709
|
+
"description": "A `byoc` device's operating system family; grows only by a contract change. The regional machine_byoc_oses() equals it.",
|
|
6710
|
+
"enum": [
|
|
6711
|
+
"darwin",
|
|
6712
|
+
"linux"
|
|
6713
|
+
]
|
|
6714
|
+
},
|
|
6715
|
+
"name": {
|
|
6716
|
+
"description": "A Machine name: the Agent name grammar (contracts/agents/agent-name-v1.json `$defs.name`, the same pattern and lengths, which the contract test holds equal) without its reserved-name list, which a Machine label cannot collide with (machine-v2.json `names`); 1-40 characters, no `--` and no `.`. Unique within its account across both kinds (ADR-0015 Q5); `<name>-<account>` must also pass the label rule of machine-v2.json `hostnames` for a managed Machine. `self` is reserved, so `/v1/machines/self` never names a Machine; global machine_name_valid follows this definition verbatim.",
|
|
6717
|
+
"type": "string",
|
|
6718
|
+
"minLength": 1,
|
|
6719
|
+
"maxLength": 40,
|
|
6720
|
+
"pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
|
|
6721
|
+
"not": {
|
|
6722
|
+
"const": "self"
|
|
6723
|
+
}
|
|
6724
|
+
},
|
|
6725
|
+
"register": {
|
|
6726
|
+
"description": "The body of `PUT /v1/machines/self` (machine-v2.json `actions` `register`, ADR-0015 `Registration`), signed by the CLI login key of the computer it registers. It has no `kind`: the server derives `byoc`, and a `kind` is refused as an unknown field. `name` is read only by the registration that creates the device; a device that exists keeps its name.",
|
|
6727
|
+
"type": "object",
|
|
6728
|
+
"additionalProperties": false,
|
|
6729
|
+
"required": [
|
|
6730
|
+
"device_id",
|
|
6731
|
+
"hostname",
|
|
6732
|
+
"os",
|
|
6733
|
+
"arch",
|
|
6734
|
+
"client",
|
|
6735
|
+
"client_version",
|
|
6736
|
+
"harnesses"
|
|
6737
|
+
],
|
|
6738
|
+
"properties": {
|
|
6739
|
+
"device_id": {
|
|
6740
|
+
"$ref": "#/$defs/uuid"
|
|
6741
|
+
},
|
|
6742
|
+
"hostname": {
|
|
6743
|
+
"$ref": "#/$defs/byoc_hostname"
|
|
6744
|
+
},
|
|
6745
|
+
"os": {
|
|
6746
|
+
"$ref": "#/$defs/byoc_os"
|
|
6747
|
+
},
|
|
6748
|
+
"arch": {
|
|
6749
|
+
"$ref": "#/$defs/byoc_arch"
|
|
6750
|
+
},
|
|
6751
|
+
"client": {
|
|
6752
|
+
"$ref": "#/$defs/byoc_client"
|
|
6753
|
+
},
|
|
6754
|
+
"client_version": {
|
|
6755
|
+
"$ref": "#/$defs/byoc_client_version"
|
|
6756
|
+
},
|
|
6757
|
+
"harnesses": {
|
|
6758
|
+
"$ref": "#/$defs/byoc_harnesses"
|
|
6759
|
+
},
|
|
6760
|
+
"name": {
|
|
6761
|
+
"$ref": "#/$defs/name"
|
|
6652
6762
|
}
|
|
6653
6763
|
}
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6764
|
+
},
|
|
6765
|
+
"uuid": {
|
|
6766
|
+
"description": "A lowercase canonical UUID.",
|
|
6767
|
+
"type": "string",
|
|
6768
|
+
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
|
6658
6769
|
}
|
|
6659
|
-
|
|
6770
|
+
};
|
|
6771
|
+
var byocPresence = {
|
|
6772
|
+
"heartbeat": {
|
|
6773
|
+
"interval_seconds": 300,
|
|
6774
|
+
"jitter_seconds": 30
|
|
6775
|
+
},
|
|
6776
|
+
"online_window_seconds": 900
|
|
6777
|
+
};
|
|
6778
|
+
var byocRetryable = [
|
|
6779
|
+
"device_registration_conflict",
|
|
6780
|
+
"device_report_throttled"
|
|
6781
|
+
];
|
|
6782
|
+
|
|
6783
|
+
// src/devices.ts
|
|
6784
|
+
async function listDevices(list2) {
|
|
6785
|
+
const devices = [];
|
|
6786
|
+
const unavailable = /* @__PURE__ */ new Set();
|
|
6787
|
+
let cursor;
|
|
6788
|
+
for (; ; ) {
|
|
6789
|
+
const page = await list2(cursor ? { cursor } : {});
|
|
6790
|
+
devices.push(
|
|
6791
|
+
...page.data.filter((machine) => machine.kind === "byoc")
|
|
6792
|
+
);
|
|
6793
|
+
for (const region of page.unavailable_regions ?? []) unavailable.add(region);
|
|
6794
|
+
if (!page.next_cursor || page.next_cursor === cursor) break;
|
|
6795
|
+
cursor = page.next_cursor;
|
|
6796
|
+
}
|
|
6797
|
+
return { devices, unavailableRegions: [...unavailable] };
|
|
6798
|
+
}
|
|
6799
|
+
var DISPATCHABLE = /* @__PURE__ */ new Set(["claude-code", "codex"]);
|
|
6800
|
+
function isOnline(lastSeen, now) {
|
|
6801
|
+
const seen = Date.parse(lastSeen);
|
|
6802
|
+
return Number.isFinite(seen) && now - seen <= byocPresence.online_window_seconds * 1e3;
|
|
6803
|
+
}
|
|
6804
|
+
function deviceRows(devices, device, now) {
|
|
6805
|
+
return devices.map((machine) => ({
|
|
6806
|
+
dispatch_target: machine.byoc.device_id !== device && machine.byoc.key_state === "active" && isOnline(machine.byoc.last_seen, now) && machine.byoc.harnesses.some((harness) => DISPATCHABLE.has(harness)),
|
|
6807
|
+
name: machine.name,
|
|
6808
|
+
os: machine.byoc.os,
|
|
6809
|
+
arch: machine.byoc.arch,
|
|
6810
|
+
harnesses: machine.byoc.harnesses,
|
|
6811
|
+
client_version: machine.byoc.client_version,
|
|
6812
|
+
last_seen: machine.byoc.last_seen,
|
|
6813
|
+
online: isOnline(machine.byoc.last_seen, now),
|
|
6814
|
+
key_state: machine.byoc.key_state,
|
|
6815
|
+
this_device: machine.byoc.device_id === device,
|
|
6816
|
+
device_id: machine.byoc.device_id ?? null,
|
|
6817
|
+
hostname: machine.byoc.hostname ?? null
|
|
6818
|
+
})).sort((a, b) => Number(b.this_device) - Number(a.this_device) || a.name.localeCompare(b.name));
|
|
6819
|
+
}
|
|
6820
|
+
|
|
6821
|
+
// src/recall.ts
|
|
6822
|
+
var str = (value) => typeof value === "string" ? value : null;
|
|
6823
|
+
var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
6824
|
+
var flag = (value) => value === true || value === 1;
|
|
6825
|
+
function rowsOf(body) {
|
|
6826
|
+
const record3 = body && typeof body === "object" ? body : null;
|
|
6827
|
+
const rows = Array.isArray(body) ? body : record3?.data ?? record3?.rows ?? record3?.files;
|
|
6828
|
+
if (!Array.isArray(rows)) throw new Error("The recall answer holds no rows.");
|
|
6829
|
+
return rows.filter((row) => !!row && typeof row === "object");
|
|
6830
|
+
}
|
|
6831
|
+
function parseFileTurns(body) {
|
|
6832
|
+
return rowsOf(body).flatMap((row) => {
|
|
6833
|
+
const session = str(row.session);
|
|
6834
|
+
const turn = num(row.turn);
|
|
6835
|
+
const commit = str(row.commit);
|
|
6836
|
+
const at = num(row.at_ms);
|
|
6837
|
+
if (!session || turn === null || !commit || at === null) return [];
|
|
6838
|
+
return [
|
|
6839
|
+
{
|
|
6840
|
+
repo: str(row.repo) ?? "",
|
|
6841
|
+
path: str(row.path) ?? "",
|
|
6842
|
+
change: str(row.change) ?? "?",
|
|
6843
|
+
direct: flag(row.direct),
|
|
6844
|
+
session,
|
|
6845
|
+
turn,
|
|
6846
|
+
src: str(row.src),
|
|
6847
|
+
device: str(row.device),
|
|
6848
|
+
sub: str(row.sub),
|
|
6849
|
+
commit,
|
|
6850
|
+
base: str(row.base),
|
|
6851
|
+
at_ms: at,
|
|
6852
|
+
cwd: str(row.cwd),
|
|
6853
|
+
git_branch: str(row.git_branch),
|
|
6854
|
+
stream_offset_from: str(row.stream_offset_from),
|
|
6855
|
+
stream_offset_to: str(row.stream_offset_to)
|
|
6856
|
+
}
|
|
6857
|
+
];
|
|
6858
|
+
});
|
|
6859
|
+
}
|
|
6860
|
+
function parseSessionFiles(body) {
|
|
6861
|
+
return rowsOf(body).flatMap((row) => {
|
|
6862
|
+
const turn = num(row.turn);
|
|
6863
|
+
const path34 = str(row.path);
|
|
6864
|
+
const at = num(row.at_ms);
|
|
6865
|
+
if (turn === null || !path34 || at === null) return [];
|
|
6866
|
+
return [
|
|
6867
|
+
{
|
|
6868
|
+
repo: str(row.repo) ?? "",
|
|
6869
|
+
path: path34,
|
|
6870
|
+
change: str(row.change) ?? "?",
|
|
6871
|
+
direct: flag(row.direct),
|
|
6872
|
+
turn,
|
|
6873
|
+
commit: str(row.commit) ?? "",
|
|
6874
|
+
at_ms: at
|
|
6875
|
+
}
|
|
6876
|
+
];
|
|
6877
|
+
});
|
|
6878
|
+
}
|
|
6879
|
+
function parseRepoSessions(body) {
|
|
6880
|
+
return rowsOf(body).flatMap((row) => {
|
|
6881
|
+
const session = str(row.session);
|
|
6882
|
+
if (!session) return [];
|
|
6883
|
+
return [
|
|
6884
|
+
{
|
|
6885
|
+
session,
|
|
6886
|
+
src: str(row.src),
|
|
6887
|
+
device: str(row.device),
|
|
6888
|
+
sub: str(row.sub),
|
|
6889
|
+
cwd: str(row.cwd),
|
|
6890
|
+
git_branch: str(row.git_branch),
|
|
6891
|
+
started_ms: num(row.started_ms),
|
|
6892
|
+
last_turn: num(row.last_turn),
|
|
6893
|
+
last_ms: num(row.last_ms)
|
|
6894
|
+
}
|
|
6895
|
+
];
|
|
6896
|
+
});
|
|
6897
|
+
}
|
|
6898
|
+
function parseCatalog(body) {
|
|
6899
|
+
return rowsOf(body).flatMap((row) => {
|
|
6900
|
+
const session = str(row.session);
|
|
6901
|
+
if (!session) return [];
|
|
6902
|
+
const strings = (value) => (Array.isArray(value) ? value : []).filter(
|
|
6903
|
+
(path34) => typeof path34 === "string"
|
|
6904
|
+
);
|
|
6905
|
+
return [
|
|
6906
|
+
{
|
|
6907
|
+
session,
|
|
6908
|
+
src: str(row.src),
|
|
6909
|
+
device: str(row.device),
|
|
6910
|
+
cwd: str(row.cwd),
|
|
6911
|
+
git_root: str(row.git_root),
|
|
6912
|
+
git_branch: str(row.git_branch),
|
|
6913
|
+
started_ms: num(row.started_ms),
|
|
6914
|
+
last_turn: num(row.last_turn),
|
|
6915
|
+
last_ms: num(row.last_ms),
|
|
6916
|
+
state: str(row.state),
|
|
6917
|
+
state_ms: num(row.state_ms),
|
|
6918
|
+
paths_written: strings(row.paths_written),
|
|
6919
|
+
paths_inferred: strings(row.paths_inferred),
|
|
6920
|
+
device_handle: str(row.device_handle),
|
|
6921
|
+
author: str(row.author),
|
|
6922
|
+
project_id: str(row.project_id),
|
|
6923
|
+
turns: (Array.isArray(row.turns) ? row.turns : []).flatMap((value) => {
|
|
6924
|
+
const entry = value && typeof value === "object" ? value : {};
|
|
6925
|
+
const turn = num(entry.turn);
|
|
6926
|
+
const at = num(entry.at_ms);
|
|
6927
|
+
return turn === null || at === null ? [] : [{ turn, at_ms: at, paths_written: strings(entry.paths_written) }];
|
|
6928
|
+
})
|
|
6929
|
+
}
|
|
6930
|
+
];
|
|
6931
|
+
});
|
|
6932
|
+
}
|
|
6933
|
+
function indexUnavailableReason(status, body) {
|
|
6934
|
+
const record3 = body && typeof body === "object" ? body : {};
|
|
6935
|
+
if (status !== 503 || record3.code !== "index_unavailable") return null;
|
|
6936
|
+
const index = record3.index && typeof record3.index === "object" ? record3.index : {};
|
|
6937
|
+
const phase = str(index.phase);
|
|
6938
|
+
const kind = str(index.kind) ?? (num(index.kind) === null ? null : String(index.kind));
|
|
6939
|
+
const since = str(index.since);
|
|
6940
|
+
const time2 = since && Number.isFinite(Date.parse(since)) ? since.slice(11, 16) : null;
|
|
6941
|
+
const where = [phase && kind ? `${phase}: ${kind}` : phase, time2 && `since ${time2} UTC`].filter(Boolean).join(" ");
|
|
6942
|
+
return `the index is not open yet${where ? ` (${where})` : ""}`;
|
|
6943
|
+
}
|
|
6944
|
+
function recallLimit(limit = RECALL_DEFAULT_LIMIT) {
|
|
6945
|
+
return Math.max(1, Math.min(RECALL_MAX_LIMIT, Math.floor(limit)));
|
|
6946
|
+
}
|
|
6947
|
+
var RecallClient = class {
|
|
6948
|
+
constructor(options) {
|
|
6949
|
+
this.options = options;
|
|
6950
|
+
}
|
|
6951
|
+
async files(repo, path34, limit) {
|
|
6952
|
+
return parseFileTurns(
|
|
6953
|
+
await this.get("/files", { repo, path: path34, limit: String(recallLimit(limit)) })
|
|
6954
|
+
);
|
|
6955
|
+
}
|
|
6956
|
+
async sessionFiles(session, limit) {
|
|
6957
|
+
return parseSessionFiles(
|
|
6958
|
+
await this.get(`/sessions/${encodeURIComponent(session)}/files`, {
|
|
6959
|
+
limit: String(recallLimit(limit))
|
|
6960
|
+
})
|
|
6961
|
+
);
|
|
6962
|
+
}
|
|
6963
|
+
async sessions(repo, limit) {
|
|
6964
|
+
return parseRepoSessions(
|
|
6965
|
+
await this.get("/sessions", { repo, limit: String(recallLimit(limit)) })
|
|
6966
|
+
);
|
|
6967
|
+
}
|
|
6968
|
+
/** The catalog: the caller's sessions of the index, most recent activity
|
|
6969
|
+
* first; `team`, every author's (recall-v1 `GET /sessions?scope=team`). */
|
|
6970
|
+
async catalog(limit, options = {}) {
|
|
6971
|
+
return parseCatalog(
|
|
6972
|
+
await this.get("/sessions", {
|
|
6973
|
+
...options.team ? { scope: "team" } : {},
|
|
6974
|
+
limit: String(recallLimit(limit))
|
|
6975
|
+
})
|
|
6976
|
+
);
|
|
6977
|
+
}
|
|
6978
|
+
async get(path34, query) {
|
|
6979
|
+
const { fetch: fetcher, tokens: tokens2, key, origin, workspace } = this.options;
|
|
6980
|
+
const url = `${recallBase(origin, workspace)}${path34}?${new URLSearchParams(query)}`;
|
|
6981
|
+
const response = await tokens2.send(
|
|
6982
|
+
key,
|
|
6983
|
+
(token) => fetcher(url, {
|
|
6984
|
+
redirect: "manual",
|
|
6985
|
+
signal: AbortSignal.timeout(3e4),
|
|
6986
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json" }
|
|
6987
|
+
})
|
|
6988
|
+
);
|
|
6989
|
+
const body = await response.json().catch(() => null);
|
|
6990
|
+
if (response.ok) return body;
|
|
6991
|
+
const record3 = body && typeof body === "object" ? body : {};
|
|
6992
|
+
const detail = indexUnavailableReason(response.status, body) ?? str(record3.detail) ?? str(record3.title) ?? str(record3.error);
|
|
6993
|
+
throw new HttpError(
|
|
6994
|
+
response.status === 404 ? "The recall API answered 404: it is not available for this workspace yet." : `The recall API answered ${response.status}${detail ? `: ${detail}` : "."}`,
|
|
6995
|
+
response.status,
|
|
6996
|
+
body,
|
|
6997
|
+
response.headers.get("x-request-id")
|
|
6998
|
+
);
|
|
6999
|
+
}
|
|
7000
|
+
};
|
|
7001
|
+
|
|
7002
|
+
// src/team-conflicts.ts
|
|
7003
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
7004
|
+
var TEAM_POLL_MS = 6e4;
|
|
7005
|
+
var NAMES_KEPT_MS = 10 * 6e4;
|
|
7006
|
+
function liveRow(row, now) {
|
|
7007
|
+
const [shown] = psRows([row], { device: "", now, all: true });
|
|
7008
|
+
return ["running", "waiting", "idle"].includes(shown?.shown_state ?? "");
|
|
7009
|
+
}
|
|
7010
|
+
function teamConflicts(mine, catalog, options) {
|
|
7011
|
+
const found = [];
|
|
7012
|
+
for (const row of catalog.rows) {
|
|
7013
|
+
if (!row.author || row.author === catalog.me || !liveRow(row, options.now)) continue;
|
|
7014
|
+
const theirs = /* @__PURE__ */ new Set();
|
|
7015
|
+
for (const turn of row.turns)
|
|
7016
|
+
if (options.now - turn.at_ms < CONFLICT_WINDOW_MS) {
|
|
7017
|
+
for (const file of turn.paths_written) if (!ignoredPath(file)) theirs.add(file);
|
|
7018
|
+
}
|
|
7019
|
+
if (!theirs.size) continue;
|
|
7020
|
+
for (const own of mine) {
|
|
7021
|
+
const pair = `t ${own.session} ${row.session}`;
|
|
7022
|
+
if (options.alerted(pair)) continue;
|
|
7023
|
+
const files = Object.entries(own.paths).filter(([file, ms]) => options.now - ms < CONFLICT_WINDOW_MS && theirs.has(file)).map(([file]) => file).sort();
|
|
7024
|
+
if (files.length)
|
|
7025
|
+
found.push({
|
|
7026
|
+
pair,
|
|
7027
|
+
session: own.session,
|
|
7028
|
+
teammate: row.session,
|
|
7029
|
+
author: row.author,
|
|
7030
|
+
files
|
|
7031
|
+
});
|
|
7032
|
+
}
|
|
7033
|
+
}
|
|
7034
|
+
return found;
|
|
7035
|
+
}
|
|
7036
|
+
function teamConflictMessage(conflict, name, now) {
|
|
7037
|
+
const shown = conflict.files.slice(0, 3).join(", ") + (conflict.files.length > 3 ? `, +${conflict.files.length - 3}` : "");
|
|
7038
|
+
const who = name ?? `teammate ${conflict.author.slice(0, 8)}`;
|
|
7039
|
+
return {
|
|
7040
|
+
id: `c${createHash6("sha256").update(conflict.pair).digest("hex").slice(0, 10)}`,
|
|
7041
|
+
kind: "conflict",
|
|
7042
|
+
at: new Date(now).toISOString(),
|
|
7043
|
+
recipient: { kind: "session", id: conflict.session },
|
|
7044
|
+
reason: "a teammate's live session wrote the same file",
|
|
7045
|
+
source: { kind: "local", detail: conflict.teammate },
|
|
7046
|
+
content: `${who}'s session ${conflict.teammate.slice(0, 8)} in this team Project also wrote ${shown} in the last 2 h`,
|
|
7047
|
+
notify: true
|
|
7048
|
+
};
|
|
7049
|
+
}
|
|
7050
|
+
function teamCatalogReader(options) {
|
|
7051
|
+
const people = /* @__PURE__ */ new Map();
|
|
7052
|
+
return async (accountId) => {
|
|
7053
|
+
const file = await options.credentials.load();
|
|
7054
|
+
const login = file ? accountLogins(file).find(
|
|
7055
|
+
(entry) => entry.account_id === accountId && entry.kind !== "user" && !isExpiredLogin(entry)
|
|
7056
|
+
) : void 0;
|
|
7057
|
+
if (!login) throw new Error(`no current login of organization ${accountId}`);
|
|
7058
|
+
let known = people.get(accountId);
|
|
7059
|
+
if (!known || known.key !== login.key || Date.now() - known.at > NAMES_KEPT_MS) {
|
|
7060
|
+
const [account, members, store] = await Promise.all([
|
|
7061
|
+
options.api.getAccount(login.key),
|
|
7062
|
+
options.api.page(
|
|
7063
|
+
"listMembers",
|
|
7064
|
+
login.key,
|
|
7065
|
+
{ limit: 100 }
|
|
7066
|
+
),
|
|
7067
|
+
getSessionsStore(options.fetch, options.apiOrigin, login.key, { team: true })
|
|
7068
|
+
]);
|
|
7069
|
+
known = {
|
|
7070
|
+
at: Date.now(),
|
|
7071
|
+
key: login.key,
|
|
7072
|
+
me: account.member.subject,
|
|
7073
|
+
workspace: workspaceHex(store.workspace_id),
|
|
7074
|
+
names: new Map(
|
|
7075
|
+
members.data.map((member2) => [
|
|
7076
|
+
member2.subject,
|
|
7077
|
+
member2.display_name ?? member2.name ?? member2.subject
|
|
7078
|
+
])
|
|
7079
|
+
)
|
|
7080
|
+
};
|
|
7081
|
+
people.set(accountId, known);
|
|
7082
|
+
}
|
|
7083
|
+
const recall = new RecallClient({
|
|
7084
|
+
fetch: options.fetch,
|
|
7085
|
+
tokens: options.gitTokens,
|
|
7086
|
+
key: login.key,
|
|
7087
|
+
origin: gitOrigin(options.apiOrigin),
|
|
7088
|
+
workspace: known.workspace
|
|
7089
|
+
});
|
|
7090
|
+
return { me: known.me, names: known.names, rows: await recall.catalog(100, { team: true }) };
|
|
7091
|
+
};
|
|
7092
|
+
}
|
|
7093
|
+
|
|
7094
|
+
// src/device-conflicts.ts
|
|
7095
|
+
function deviceConflicts(mine, rows, options) {
|
|
7096
|
+
const found = [];
|
|
7097
|
+
for (const row of rows) {
|
|
7098
|
+
if (!row.device || row.device === options.device || !row.project_id || !row.git_root) continue;
|
|
7099
|
+
if (!liveRow(row, options.now)) continue;
|
|
7100
|
+
const theirs = /* @__PURE__ */ new Set();
|
|
7101
|
+
for (const turn of row.turns)
|
|
7102
|
+
if (options.now - turn.at_ms < CONFLICT_WINDOW_MS) {
|
|
7103
|
+
for (const file of turn.paths_written) if (!ignoredPath(file)) theirs.add(file);
|
|
7104
|
+
}
|
|
7105
|
+
if (!theirs.size) continue;
|
|
7106
|
+
for (const own of mine) {
|
|
7107
|
+
if (own.project !== row.project_id) continue;
|
|
7108
|
+
const pair = `d ${own.session} ${row.session}`;
|
|
7109
|
+
if (options.alerted(pair)) continue;
|
|
7110
|
+
const files = Object.entries(own.paths).filter(([file, ms]) => options.now - ms < CONFLICT_WINDOW_MS && theirs.has(file)).map(([file]) => file).sort();
|
|
7111
|
+
if (files.length) found.push({ pair, session: own.session, other: row, files });
|
|
7112
|
+
}
|
|
7113
|
+
}
|
|
7114
|
+
return found;
|
|
7115
|
+
}
|
|
7116
|
+
function deviceConflictMessage(conflict, names, now, home) {
|
|
7117
|
+
const shown = conflict.files.slice(0, 3).join(", ") + (conflict.files.length > 3 ? `, +${conflict.files.length - 3}` : "");
|
|
7118
|
+
const { other } = conflict;
|
|
7119
|
+
const device = other.device;
|
|
7120
|
+
const on = names.get(device) ?? `device ${device.slice(0, 8)}`;
|
|
7121
|
+
const where = other.git_root ? ` in ${tilde(other.git_root, home)}` : "";
|
|
7122
|
+
return {
|
|
7123
|
+
id: `c${createHash7("sha256").update(conflict.pair).digest("hex").slice(0, 10)}`,
|
|
7124
|
+
kind: "conflict",
|
|
7125
|
+
at: new Date(now).toISOString(),
|
|
7126
|
+
recipient: { kind: "session", id: conflict.session },
|
|
7127
|
+
reason: "your session on another device wrote the same file",
|
|
7128
|
+
source: { kind: "local", detail: other.session },
|
|
7129
|
+
content: `your ${other.src ?? "agent"} session ${other.session.slice(0, 8)} on ${on}${where} also wrote ${shown} in the last 2 h`,
|
|
7130
|
+
notify: true
|
|
7131
|
+
};
|
|
7132
|
+
}
|
|
7133
|
+
var KEPT_MS = 10 * 6e4;
|
|
7134
|
+
function ownCatalogReader(options) {
|
|
7135
|
+
const now = options.now ?? (() => Date.now());
|
|
7136
|
+
let known = null;
|
|
7137
|
+
return async () => {
|
|
7138
|
+
const key = await options.key();
|
|
7139
|
+
if (!key) throw new Error("no current personal login");
|
|
7140
|
+
if (!known || known.key !== key || now() - known.at > KEPT_MS) {
|
|
7141
|
+
const [store, { devices }] = await Promise.all([
|
|
7142
|
+
getSessionsStore(options.fetch, options.apiOrigin, key),
|
|
7143
|
+
listDevices((page) => options.api.listMachines(key, page))
|
|
7144
|
+
]);
|
|
7145
|
+
known = {
|
|
7146
|
+
at: now(),
|
|
7147
|
+
key,
|
|
7148
|
+
workspace: workspaceHex(store.workspace_id),
|
|
7149
|
+
names: new Map(
|
|
7150
|
+
devices.flatMap(
|
|
7151
|
+
(machine) => machine.byoc.device_id ? [[machine.byoc.device_id, machine.name]] : []
|
|
7152
|
+
)
|
|
7153
|
+
)
|
|
7154
|
+
};
|
|
7155
|
+
}
|
|
7156
|
+
const recall = new RecallClient({
|
|
7157
|
+
fetch: options.fetch,
|
|
7158
|
+
tokens: options.gitTokens,
|
|
7159
|
+
key,
|
|
7160
|
+
origin: gitOrigin(options.apiOrigin),
|
|
7161
|
+
workspace: known.workspace
|
|
7162
|
+
});
|
|
7163
|
+
return { rows: await recall.catalog(100), names: known.names };
|
|
7164
|
+
};
|
|
7165
|
+
}
|
|
7166
|
+
|
|
7167
|
+
// src/inbox.ts
|
|
7168
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
7169
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
7170
|
+
import { appendFile, mkdir as mkdir5, open as open8, readFile as readFile9, rename as rename7, rm as rm7 } from "node:fs/promises";
|
|
7171
|
+
import path15 from "node:path";
|
|
7172
|
+
import { promisify as promisify4 } from "node:util";
|
|
7173
|
+
import { setTimeout as delay4 } from "node:timers/promises";
|
|
7174
|
+
|
|
7175
|
+
// src/handoffs.ts
|
|
7176
|
+
async function listHandoffs(request) {
|
|
7177
|
+
const data = await request("listHandoffs", {});
|
|
7178
|
+
if (!Array.isArray(data)) throw new Error("The handoff list answered an invalid body.");
|
|
7179
|
+
return data;
|
|
7180
|
+
}
|
|
7181
|
+
async function readHandoffEvents(request, after, wait) {
|
|
7182
|
+
const answer = await request("listHandoffEvents", {
|
|
7183
|
+
query: { after: String(after), wait: String(wait) }
|
|
7184
|
+
});
|
|
7185
|
+
if (!Array.isArray(answer?.events) || !Number.isSafeInteger(answer.next_after))
|
|
7186
|
+
throw new Error("The handoff events answered an invalid body.");
|
|
7187
|
+
return answer;
|
|
7188
|
+
}
|
|
7189
|
+
async function cancelHandoff(request, link3) {
|
|
7190
|
+
const id = handoffId(link3);
|
|
7191
|
+
const answer = await request(
|
|
7192
|
+
"cancelPersonalHandoff",
|
|
7193
|
+
{ params: { id } }
|
|
7194
|
+
);
|
|
7195
|
+
if (answer.id !== id || answer.state !== "cancelled")
|
|
7196
|
+
throw new Error("The handoff answered an invalid cancellation.");
|
|
7197
|
+
return answer;
|
|
7198
|
+
}
|
|
7199
|
+
function handoffListLine(item, me, name) {
|
|
7200
|
+
const who = (sub) => sub === me ? "you" : name(item.account_id, sub);
|
|
7201
|
+
const parts = [`by ${who(item.author_sub)}`];
|
|
7202
|
+
if (item.target_kind === "person" && item.target_sub) parts.push(`for ${who(item.target_sub)}`);
|
|
7203
|
+
if (item.target_kind === "open") parts.push("for any member");
|
|
7204
|
+
if (item.target_kind === "agent") parts.push(`for ${item.target_harness ?? "another harness"}`);
|
|
7205
|
+
if (item.target_kind === "machine")
|
|
7206
|
+
parts.push(`dispatched to your device on ${item.target_harness ?? "a harness"}`);
|
|
7207
|
+
if (item.claimed_by_sub) parts.push(`taken by ${who(item.claimed_by_sub)}`);
|
|
7208
|
+
return `${item.state.padEnd(10)} ${item.link} ${item.task} (${parts.join(", ")})`;
|
|
7209
|
+
}
|
|
7210
|
+
var VERBS2 = {
|
|
7211
|
+
created: "created",
|
|
7212
|
+
claimed: "took",
|
|
7213
|
+
running: "started",
|
|
7214
|
+
done: "finished",
|
|
7215
|
+
need_human: "needs a human on",
|
|
7216
|
+
failed: "failed",
|
|
7217
|
+
returned: "returned",
|
|
7218
|
+
cancelled: "cancelled"
|
|
7219
|
+
};
|
|
7220
|
+
function handoffEventLine(event, me, name) {
|
|
7221
|
+
const actor = !event.actor_sub ? "someone" : event.actor_sub === me ? "you" : name(event.account_id, event.actor_sub);
|
|
7222
|
+
return `#${event.seq} ${event.at} ${actor} ${VERBS2[event.kind] ?? event.kind} ${event.link} ${event.task}`;
|
|
7223
|
+
}
|
|
7224
|
+
|
|
7225
|
+
// src/inbox.ts
|
|
7226
|
+
var InboxStore = class {
|
|
7227
|
+
constructor(directory2 = stateDirectory()) {
|
|
7228
|
+
this.directory = directory2;
|
|
7229
|
+
}
|
|
7230
|
+
file(name) {
|
|
7231
|
+
return path15.join(this.directory, name);
|
|
7232
|
+
}
|
|
7233
|
+
/** Appends messages to the spool (the daemon). */
|
|
7234
|
+
async append(messages) {
|
|
7235
|
+
if (!messages.length) return;
|
|
7236
|
+
await mkdir5(this.directory, { recursive: true, mode: 448 });
|
|
7237
|
+
await appendFile(
|
|
7238
|
+
this.file("inbox.jsonl"),
|
|
7239
|
+
messages.map((message3) => `${JSON.stringify(message3)}
|
|
7240
|
+
`).join(""),
|
|
7241
|
+
{ mode: 384 }
|
|
7242
|
+
);
|
|
7243
|
+
}
|
|
7244
|
+
/** Every message, oldest first, each once; a torn last line is skipped. */
|
|
7245
|
+
async messages() {
|
|
7246
|
+
const text3 = await readFile9(this.file("inbox.jsonl"), "utf8").catch(() => "");
|
|
7247
|
+
const seen = /* @__PURE__ */ new Map();
|
|
7248
|
+
for (const line of text3.split("\n")) {
|
|
7249
|
+
if (!line.trim()) continue;
|
|
7250
|
+
try {
|
|
7251
|
+
const message3 = JSON.parse(line);
|
|
7252
|
+
if (typeof message3?.id === "string" && !seen.has(message3.id)) seen.set(message3.id, message3);
|
|
7253
|
+
} catch {
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
7256
|
+
return [...seen.values()];
|
|
7257
|
+
}
|
|
7258
|
+
async cursor() {
|
|
7259
|
+
return readJson(this.file("inbox-cursor.json"));
|
|
7260
|
+
}
|
|
7261
|
+
async saveCursor(cursor) {
|
|
6660
7262
|
await writeJson(this.directory, this.file("inbox-cursor.json"), cursor);
|
|
6661
7263
|
}
|
|
6662
7264
|
async marks() {
|
|
@@ -6879,305 +7481,31 @@ function inboxPoll(options) {
|
|
|
6879
7481
|
names: new Map(
|
|
6880
7482
|
data.map((member2) => [
|
|
6881
7483
|
member2.subject,
|
|
6882
|
-
member2.display_name ?? member2.name ?? member2.subject
|
|
6883
|
-
])
|
|
6884
|
-
)
|
|
6885
|
-
};
|
|
6886
|
-
names.set(accountId, entry);
|
|
6887
|
-
}
|
|
6888
|
-
return entry.names.get(sub) ?? null;
|
|
6889
|
-
}
|
|
6890
|
-
};
|
|
6891
|
-
}
|
|
6892
|
-
function inboxLine(message3, status) {
|
|
6893
|
-
return `${status.padEnd(11)} ${message3.id} ${message3.at} ${message3.content} (${message3.reason})`;
|
|
6894
|
-
}
|
|
6895
|
-
async function ackHandoff(store, handoffId2) {
|
|
6896
|
-
try {
|
|
6897
|
-
const marks = await store.marks();
|
|
6898
|
-
const ids = (await store.messages()).filter(
|
|
6899
|
-
(message3) => message3.source.kind === "handoff" && message3.source.handoff_id === handoffId2 && unread(statusOf(marks, message3.id))
|
|
6900
|
-
).map((message3) => message3.id);
|
|
6901
|
-
if (!ids.length) return;
|
|
6902
|
-
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
6903
|
-
for (const id of ids) marks.marks[id] = { ...marks.marks[id], status: "read", at };
|
|
6904
|
-
await store.saveMarks(marks);
|
|
6905
|
-
} catch {
|
|
6906
|
-
}
|
|
6907
|
-
}
|
|
6908
|
-
|
|
6909
|
-
// src/team-conflicts.ts
|
|
6910
|
-
import { createHash as createHash6 } from "node:crypto";
|
|
6911
|
-
|
|
6912
|
-
// src/recall.ts
|
|
6913
|
-
var str = (value) => typeof value === "string" ? value : null;
|
|
6914
|
-
var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
6915
|
-
var flag = (value) => value === true || value === 1;
|
|
6916
|
-
function rowsOf(body) {
|
|
6917
|
-
const record3 = body && typeof body === "object" ? body : null;
|
|
6918
|
-
const rows = Array.isArray(body) ? body : record3?.data ?? record3?.rows ?? record3?.files;
|
|
6919
|
-
if (!Array.isArray(rows)) throw new Error("The recall answer holds no rows.");
|
|
6920
|
-
return rows.filter((row) => !!row && typeof row === "object");
|
|
6921
|
-
}
|
|
6922
|
-
function parseFileTurns(body) {
|
|
6923
|
-
return rowsOf(body).flatMap((row) => {
|
|
6924
|
-
const session = str(row.session);
|
|
6925
|
-
const turn = num(row.turn);
|
|
6926
|
-
const commit = str(row.commit);
|
|
6927
|
-
const at = num(row.at_ms);
|
|
6928
|
-
if (!session || turn === null || !commit || at === null) return [];
|
|
6929
|
-
return [
|
|
6930
|
-
{
|
|
6931
|
-
repo: str(row.repo) ?? "",
|
|
6932
|
-
path: str(row.path) ?? "",
|
|
6933
|
-
change: str(row.change) ?? "?",
|
|
6934
|
-
direct: flag(row.direct),
|
|
6935
|
-
session,
|
|
6936
|
-
turn,
|
|
6937
|
-
src: str(row.src),
|
|
6938
|
-
device: str(row.device),
|
|
6939
|
-
sub: str(row.sub),
|
|
6940
|
-
commit,
|
|
6941
|
-
base: str(row.base),
|
|
6942
|
-
at_ms: at,
|
|
6943
|
-
cwd: str(row.cwd),
|
|
6944
|
-
git_branch: str(row.git_branch),
|
|
6945
|
-
stream_offset_from: str(row.stream_offset_from),
|
|
6946
|
-
stream_offset_to: str(row.stream_offset_to)
|
|
6947
|
-
}
|
|
6948
|
-
];
|
|
6949
|
-
});
|
|
6950
|
-
}
|
|
6951
|
-
function parseSessionFiles(body) {
|
|
6952
|
-
return rowsOf(body).flatMap((row) => {
|
|
6953
|
-
const turn = num(row.turn);
|
|
6954
|
-
const path34 = str(row.path);
|
|
6955
|
-
const at = num(row.at_ms);
|
|
6956
|
-
if (turn === null || !path34 || at === null) return [];
|
|
6957
|
-
return [
|
|
6958
|
-
{
|
|
6959
|
-
repo: str(row.repo) ?? "",
|
|
6960
|
-
path: path34,
|
|
6961
|
-
change: str(row.change) ?? "?",
|
|
6962
|
-
direct: flag(row.direct),
|
|
6963
|
-
turn,
|
|
6964
|
-
commit: str(row.commit) ?? "",
|
|
6965
|
-
at_ms: at
|
|
6966
|
-
}
|
|
6967
|
-
];
|
|
6968
|
-
});
|
|
6969
|
-
}
|
|
6970
|
-
function parseRepoSessions(body) {
|
|
6971
|
-
return rowsOf(body).flatMap((row) => {
|
|
6972
|
-
const session = str(row.session);
|
|
6973
|
-
if (!session) return [];
|
|
6974
|
-
return [
|
|
6975
|
-
{
|
|
6976
|
-
session,
|
|
6977
|
-
src: str(row.src),
|
|
6978
|
-
device: str(row.device),
|
|
6979
|
-
sub: str(row.sub),
|
|
6980
|
-
cwd: str(row.cwd),
|
|
6981
|
-
git_branch: str(row.git_branch),
|
|
6982
|
-
started_ms: num(row.started_ms),
|
|
6983
|
-
last_turn: num(row.last_turn),
|
|
6984
|
-
last_ms: num(row.last_ms)
|
|
6985
|
-
}
|
|
6986
|
-
];
|
|
6987
|
-
});
|
|
6988
|
-
}
|
|
6989
|
-
function parseCatalog(body) {
|
|
6990
|
-
return rowsOf(body).flatMap((row) => {
|
|
6991
|
-
const session = str(row.session);
|
|
6992
|
-
if (!session) return [];
|
|
6993
|
-
const strings = (value) => (Array.isArray(value) ? value : []).filter(
|
|
6994
|
-
(path34) => typeof path34 === "string"
|
|
6995
|
-
);
|
|
6996
|
-
return [
|
|
6997
|
-
{
|
|
6998
|
-
session,
|
|
6999
|
-
src: str(row.src),
|
|
7000
|
-
device: str(row.device),
|
|
7001
|
-
cwd: str(row.cwd),
|
|
7002
|
-
git_root: str(row.git_root),
|
|
7003
|
-
git_branch: str(row.git_branch),
|
|
7004
|
-
started_ms: num(row.started_ms),
|
|
7005
|
-
last_turn: num(row.last_turn),
|
|
7006
|
-
last_ms: num(row.last_ms),
|
|
7007
|
-
state: str(row.state),
|
|
7008
|
-
state_ms: num(row.state_ms),
|
|
7009
|
-
paths_written: strings(row.paths_written),
|
|
7010
|
-
paths_inferred: strings(row.paths_inferred),
|
|
7011
|
-
device_handle: str(row.device_handle),
|
|
7012
|
-
author: str(row.author),
|
|
7013
|
-
turns: (Array.isArray(row.turns) ? row.turns : []).flatMap((value) => {
|
|
7014
|
-
const entry = value && typeof value === "object" ? value : {};
|
|
7015
|
-
const turn = num(entry.turn);
|
|
7016
|
-
const at = num(entry.at_ms);
|
|
7017
|
-
return turn === null || at === null ? [] : [{ turn, at_ms: at, paths_written: strings(entry.paths_written) }];
|
|
7018
|
-
})
|
|
7019
|
-
}
|
|
7020
|
-
];
|
|
7021
|
-
});
|
|
7022
|
-
}
|
|
7023
|
-
function indexUnavailableReason(status, body) {
|
|
7024
|
-
const record3 = body && typeof body === "object" ? body : {};
|
|
7025
|
-
if (status !== 503 || record3.code !== "index_unavailable") return null;
|
|
7026
|
-
const index = record3.index && typeof record3.index === "object" ? record3.index : {};
|
|
7027
|
-
const phase = str(index.phase);
|
|
7028
|
-
const kind = str(index.kind) ?? (num(index.kind) === null ? null : String(index.kind));
|
|
7029
|
-
const since = str(index.since);
|
|
7030
|
-
const time2 = since && Number.isFinite(Date.parse(since)) ? since.slice(11, 16) : null;
|
|
7031
|
-
const where = [phase && kind ? `${phase}: ${kind}` : phase, time2 && `since ${time2} UTC`].filter(Boolean).join(" ");
|
|
7032
|
-
return `the index is not open yet${where ? ` (${where})` : ""}`;
|
|
7033
|
-
}
|
|
7034
|
-
function recallLimit(limit = RECALL_DEFAULT_LIMIT) {
|
|
7035
|
-
return Math.max(1, Math.min(RECALL_MAX_LIMIT, Math.floor(limit)));
|
|
7036
|
-
}
|
|
7037
|
-
var RecallClient = class {
|
|
7038
|
-
constructor(options) {
|
|
7039
|
-
this.options = options;
|
|
7040
|
-
}
|
|
7041
|
-
async files(repo, path34, limit) {
|
|
7042
|
-
return parseFileTurns(
|
|
7043
|
-
await this.get("/files", { repo, path: path34, limit: String(recallLimit(limit)) })
|
|
7044
|
-
);
|
|
7045
|
-
}
|
|
7046
|
-
async sessionFiles(session, limit) {
|
|
7047
|
-
return parseSessionFiles(
|
|
7048
|
-
await this.get(`/sessions/${encodeURIComponent(session)}/files`, {
|
|
7049
|
-
limit: String(recallLimit(limit))
|
|
7050
|
-
})
|
|
7051
|
-
);
|
|
7052
|
-
}
|
|
7053
|
-
async sessions(repo, limit) {
|
|
7054
|
-
return parseRepoSessions(
|
|
7055
|
-
await this.get("/sessions", { repo, limit: String(recallLimit(limit)) })
|
|
7056
|
-
);
|
|
7057
|
-
}
|
|
7058
|
-
/** The catalog: the caller's sessions of the index, most recent activity
|
|
7059
|
-
* first; `team`, every author's (recall-v1 `GET /sessions?scope=team`). */
|
|
7060
|
-
async catalog(limit, options = {}) {
|
|
7061
|
-
return parseCatalog(
|
|
7062
|
-
await this.get("/sessions", {
|
|
7063
|
-
...options.team ? { scope: "team" } : {},
|
|
7064
|
-
limit: String(recallLimit(limit))
|
|
7065
|
-
})
|
|
7066
|
-
);
|
|
7067
|
-
}
|
|
7068
|
-
async get(path34, query) {
|
|
7069
|
-
const { fetch: fetcher, tokens: tokens2, key, origin, workspace } = this.options;
|
|
7070
|
-
const url = `${recallBase(origin, workspace)}${path34}?${new URLSearchParams(query)}`;
|
|
7071
|
-
const response = await tokens2.send(
|
|
7072
|
-
key,
|
|
7073
|
-
(token) => fetcher(url, {
|
|
7074
|
-
redirect: "manual",
|
|
7075
|
-
signal: AbortSignal.timeout(3e4),
|
|
7076
|
-
headers: { authorization: `Bearer ${token}`, accept: "application/json" }
|
|
7077
|
-
})
|
|
7078
|
-
);
|
|
7079
|
-
const body = await response.json().catch(() => null);
|
|
7080
|
-
if (response.ok) return body;
|
|
7081
|
-
const record3 = body && typeof body === "object" ? body : {};
|
|
7082
|
-
const detail = indexUnavailableReason(response.status, body) ?? str(record3.detail) ?? str(record3.title) ?? str(record3.error);
|
|
7083
|
-
throw new HttpError(
|
|
7084
|
-
response.status === 404 ? "The recall API answered 404: it is not available for this workspace yet." : `The recall API answered ${response.status}${detail ? `: ${detail}` : "."}`,
|
|
7085
|
-
response.status,
|
|
7086
|
-
body,
|
|
7087
|
-
response.headers.get("x-request-id")
|
|
7088
|
-
);
|
|
7089
|
-
}
|
|
7090
|
-
};
|
|
7091
|
-
|
|
7092
|
-
// src/team-conflicts.ts
|
|
7093
|
-
var TEAM_POLL_MS = 6e4;
|
|
7094
|
-
var NAMES_KEPT_MS = 10 * 6e4;
|
|
7095
|
-
function liveTeammate(row, now) {
|
|
7096
|
-
const [shown] = psRows([row], { device: "", now, all: true });
|
|
7097
|
-
return ["running", "waiting", "idle"].includes(shown?.shown_state ?? "");
|
|
7098
|
-
}
|
|
7099
|
-
function teamConflicts(mine, catalog, options) {
|
|
7100
|
-
const found = [];
|
|
7101
|
-
for (const row of catalog.rows) {
|
|
7102
|
-
if (!row.author || row.author === catalog.me || !liveTeammate(row, options.now)) continue;
|
|
7103
|
-
const theirs = /* @__PURE__ */ new Set();
|
|
7104
|
-
for (const turn of row.turns)
|
|
7105
|
-
if (options.now - turn.at_ms < CONFLICT_WINDOW_MS) {
|
|
7106
|
-
for (const file of turn.paths_written) if (!ignoredPath(file)) theirs.add(file);
|
|
7107
|
-
}
|
|
7108
|
-
if (!theirs.size) continue;
|
|
7109
|
-
for (const own of mine) {
|
|
7110
|
-
const pair = `t ${own.session} ${row.session}`;
|
|
7111
|
-
if (options.alerted(pair)) continue;
|
|
7112
|
-
const files = Object.entries(own.paths).filter(([file, ms]) => options.now - ms < CONFLICT_WINDOW_MS && theirs.has(file)).map(([file]) => file).sort();
|
|
7113
|
-
if (files.length)
|
|
7114
|
-
found.push({
|
|
7115
|
-
pair,
|
|
7116
|
-
session: own.session,
|
|
7117
|
-
teammate: row.session,
|
|
7118
|
-
author: row.author,
|
|
7119
|
-
files
|
|
7120
|
-
});
|
|
7484
|
+
member2.display_name ?? member2.name ?? member2.subject
|
|
7485
|
+
])
|
|
7486
|
+
)
|
|
7487
|
+
};
|
|
7488
|
+
names.set(accountId, entry);
|
|
7489
|
+
}
|
|
7490
|
+
return entry.names.get(sub) ?? null;
|
|
7121
7491
|
}
|
|
7122
|
-
}
|
|
7123
|
-
return found;
|
|
7124
|
-
}
|
|
7125
|
-
function teamConflictMessage(conflict, name, now) {
|
|
7126
|
-
const shown = conflict.files.slice(0, 3).join(", ") + (conflict.files.length > 3 ? `, +${conflict.files.length - 3}` : "");
|
|
7127
|
-
const who = name ?? `teammate ${conflict.author.slice(0, 8)}`;
|
|
7128
|
-
return {
|
|
7129
|
-
id: `c${createHash6("sha256").update(conflict.pair).digest("hex").slice(0, 10)}`,
|
|
7130
|
-
kind: "conflict",
|
|
7131
|
-
at: new Date(now).toISOString(),
|
|
7132
|
-
recipient: { kind: "session", id: conflict.session },
|
|
7133
|
-
reason: "a teammate's live session wrote the same file",
|
|
7134
|
-
source: { kind: "local", detail: conflict.teammate },
|
|
7135
|
-
content: `${who}'s session ${conflict.teammate.slice(0, 8)} in this team Project also wrote ${shown} in the last 2 h`,
|
|
7136
|
-
notify: true
|
|
7137
7492
|
};
|
|
7138
7493
|
}
|
|
7139
|
-
function
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
if (!
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
{ limit: 100 }
|
|
7155
|
-
),
|
|
7156
|
-
getSessionsStore(options.fetch, options.apiOrigin, login.key, { team: true })
|
|
7157
|
-
]);
|
|
7158
|
-
known = {
|
|
7159
|
-
at: Date.now(),
|
|
7160
|
-
key: login.key,
|
|
7161
|
-
me: account.member.subject,
|
|
7162
|
-
workspace: workspaceHex(store.workspace_id),
|
|
7163
|
-
names: new Map(
|
|
7164
|
-
members.data.map((member2) => [
|
|
7165
|
-
member2.subject,
|
|
7166
|
-
member2.display_name ?? member2.name ?? member2.subject
|
|
7167
|
-
])
|
|
7168
|
-
)
|
|
7169
|
-
};
|
|
7170
|
-
people.set(accountId, known);
|
|
7171
|
-
}
|
|
7172
|
-
const recall = new RecallClient({
|
|
7173
|
-
fetch: options.fetch,
|
|
7174
|
-
tokens: options.gitTokens,
|
|
7175
|
-
key: login.key,
|
|
7176
|
-
origin: gitOrigin(options.apiOrigin),
|
|
7177
|
-
workspace: known.workspace
|
|
7178
|
-
});
|
|
7179
|
-
return { me: known.me, names: known.names, rows: await recall.catalog(100, { team: true }) };
|
|
7180
|
-
};
|
|
7494
|
+
function inboxLine(message3, status) {
|
|
7495
|
+
return `${status.padEnd(11)} ${message3.id} ${message3.at} ${message3.content} (${message3.reason})`;
|
|
7496
|
+
}
|
|
7497
|
+
async function ackHandoff(store, handoffId2) {
|
|
7498
|
+
try {
|
|
7499
|
+
const marks = await store.marks();
|
|
7500
|
+
const ids = (await store.messages()).filter(
|
|
7501
|
+
(message3) => message3.source.kind === "handoff" && message3.source.handoff_id === handoffId2 && unread(statusOf(marks, message3.id))
|
|
7502
|
+
).map((message3) => message3.id);
|
|
7503
|
+
if (!ids.length) return;
|
|
7504
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
7505
|
+
for (const id of ids) marks.marks[id] = { ...marks.marks[id], status: "read", at };
|
|
7506
|
+
await store.saveMarks(marks);
|
|
7507
|
+
} catch {
|
|
7508
|
+
}
|
|
7181
7509
|
}
|
|
7182
7510
|
|
|
7183
7511
|
// src/conflicts.ts
|
|
@@ -7301,7 +7629,7 @@ function conflictMessages(state, conflict, now, home) {
|
|
|
7301
7629
|
const theirs = state.sessions[other];
|
|
7302
7630
|
const where = theirs.cwd ? ` in ${tilde(theirs.cwd, home)}` : "";
|
|
7303
7631
|
return {
|
|
7304
|
-
id: `c${
|
|
7632
|
+
id: `c${createHash8("sha256").update(`${conflict.pair} ${me}`).digest("hex").slice(0, 10)}`,
|
|
7305
7633
|
kind: "conflict",
|
|
7306
7634
|
at: new Date(now).toISOString(),
|
|
7307
7635
|
recipient: { kind: "session", id: me },
|
|
@@ -7338,6 +7666,8 @@ var ConflictWatch = class {
|
|
|
7338
7666
|
capped = "";
|
|
7339
7667
|
polled = /* @__PURE__ */ new Map();
|
|
7340
7668
|
failing = /* @__PURE__ */ new Map();
|
|
7669
|
+
projects = /* @__PURE__ */ new Map();
|
|
7670
|
+
ownPolled = 0;
|
|
7341
7671
|
/** A turn's writes, which its end clears from the session's state before
|
|
7342
7672
|
* the next pass sees them. */
|
|
7343
7673
|
turnEnded(writes) {
|
|
@@ -7387,7 +7717,10 @@ var ConflictWatch = class {
|
|
|
7387
7717
|
}
|
|
7388
7718
|
}
|
|
7389
7719
|
const repository = await this.repository(write.cwd);
|
|
7390
|
-
if (repository)
|
|
7720
|
+
if (!repository) continue;
|
|
7721
|
+
remember(state, { ...write, repository });
|
|
7722
|
+
const project = place ? null : await this.project(write.cwd);
|
|
7723
|
+
if (project) state.sessions[write.session].project = project;
|
|
7391
7724
|
}
|
|
7392
7725
|
prune(state, now);
|
|
7393
7726
|
const live = (session) => liveEntry(entries.get(session));
|
|
@@ -7402,10 +7735,23 @@ var ConflictWatch = class {
|
|
|
7402
7735
|
now
|
|
7403
7736
|
);
|
|
7404
7737
|
await this.teammates(state, live, now);
|
|
7738
|
+
await this.devices(state, live, now);
|
|
7405
7739
|
} catch (error) {
|
|
7406
7740
|
log("warn", `conflicts: ${error instanceof Error ? error.message : String(error)}`);
|
|
7407
7741
|
}
|
|
7408
7742
|
}
|
|
7743
|
+
/** A personal cwd's Project, looked up again after TEAM_POLL_MS. */
|
|
7744
|
+
project(cwd) {
|
|
7745
|
+
const lookup = this.options.devices?.project;
|
|
7746
|
+
if (!lookup) return Promise.resolve(null);
|
|
7747
|
+
const now = this.options.now();
|
|
7748
|
+
let cached = this.projects.get(cwd);
|
|
7749
|
+
if (!cached || now - cached.at >= TEAM_POLL_MS) {
|
|
7750
|
+
cached = { at: now, found: lookup(cwd).catch(() => null) };
|
|
7751
|
+
this.projects.set(cwd, cached);
|
|
7752
|
+
}
|
|
7753
|
+
return cached.found;
|
|
7754
|
+
}
|
|
7409
7755
|
/** How many of `found` pairs today's cap still lets alert. */
|
|
7410
7756
|
room(state, found) {
|
|
7411
7757
|
const room = Math.max(0, CONFLICT_DAILY_CAP - state.day.count);
|
|
@@ -7464,124 +7810,49 @@ var ConflictWatch = class {
|
|
|
7464
7810
|
);
|
|
7465
7811
|
}
|
|
7466
7812
|
}
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
"yield"
|
|
7482
|
-
]
|
|
7483
|
-
},
|
|
7484
|
-
"byoc_client_version": {
|
|
7485
|
-
"description": "The client's version, a semantic version of at most 64 octets.",
|
|
7486
|
-
"type": "string",
|
|
7487
|
-
"maxLength": 64,
|
|
7488
|
-
"pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
|
|
7489
|
-
},
|
|
7490
|
-
"byoc_harness": {
|
|
7491
|
-
"description": "An agent harness `yield` captures on a `byoc` device: a key of contracts/sessions/capture-v1.json `sources`, its one authority, which automation/tests/test_machines_machine_v2_contract.py holds this enum equal to. The regional machine_byoc_harnesses() equals it.",
|
|
7492
|
-
"enum": [
|
|
7493
|
-
"claude-code",
|
|
7494
|
-
"codex"
|
|
7495
|
-
]
|
|
7496
|
-
},
|
|
7497
|
-
"byoc_harnesses": {
|
|
7498
|
-
"description": "The harnesses installed on the device that its client captures, without duplicates.",
|
|
7499
|
-
"type": "array",
|
|
7500
|
-
"uniqueItems": true,
|
|
7501
|
-
"items": {
|
|
7502
|
-
"$ref": "#/$defs/byoc_harness"
|
|
7503
|
-
}
|
|
7504
|
-
},
|
|
7505
|
-
"byoc_hostname": {
|
|
7506
|
-
"description": "The device's hostname as the client reports it, at most 253 octets (the database counts octets). Personal data, answered to the device's owner only (machine-v2.json `byoc.visibility`).",
|
|
7507
|
-
"type": "string",
|
|
7508
|
-
"minLength": 1,
|
|
7509
|
-
"maxLength": 253
|
|
7510
|
-
},
|
|
7511
|
-
"byoc_os": {
|
|
7512
|
-
"description": "A `byoc` device's operating system family; grows only by a contract change. The regional machine_byoc_oses() equals it.",
|
|
7513
|
-
"enum": [
|
|
7514
|
-
"darwin",
|
|
7515
|
-
"linux"
|
|
7516
|
-
]
|
|
7517
|
-
},
|
|
7518
|
-
"name": {
|
|
7519
|
-
"description": "A Machine name: the Agent name grammar (contracts/agents/agent-name-v1.json `$defs.name`, the same pattern and lengths, which the contract test holds equal) without its reserved-name list, which a Machine label cannot collide with (machine-v2.json `names`); 1-40 characters, no `--` and no `.`. Unique within its account across both kinds (ADR-0015 Q5); `<name>-<account>` must also pass the label rule of machine-v2.json `hostnames` for a managed Machine. `self` is reserved, so `/v1/machines/self` never names a Machine; global machine_name_valid follows this definition verbatim.",
|
|
7520
|
-
"type": "string",
|
|
7521
|
-
"minLength": 1,
|
|
7522
|
-
"maxLength": 40,
|
|
7523
|
-
"pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
|
|
7524
|
-
"not": {
|
|
7525
|
-
"const": "self"
|
|
7526
|
-
}
|
|
7527
|
-
},
|
|
7528
|
-
"register": {
|
|
7529
|
-
"description": "The body of `PUT /v1/machines/self` (machine-v2.json `actions` `register`, ADR-0015 `Registration`), signed by the CLI login key of the computer it registers. It has no `kind`: the server derives `byoc`, and a `kind` is refused as an unknown field. `name` is read only by the registration that creates the device; a device that exists keeps its name.",
|
|
7530
|
-
"type": "object",
|
|
7531
|
-
"additionalProperties": false,
|
|
7532
|
-
"required": [
|
|
7533
|
-
"device_id",
|
|
7534
|
-
"hostname",
|
|
7535
|
-
"os",
|
|
7536
|
-
"arch",
|
|
7537
|
-
"client",
|
|
7538
|
-
"client_version",
|
|
7539
|
-
"harnesses"
|
|
7540
|
-
],
|
|
7541
|
-
"properties": {
|
|
7542
|
-
"device_id": {
|
|
7543
|
-
"$ref": "#/$defs/uuid"
|
|
7544
|
-
},
|
|
7545
|
-
"hostname": {
|
|
7546
|
-
"$ref": "#/$defs/byoc_hostname"
|
|
7547
|
-
},
|
|
7548
|
-
"os": {
|
|
7549
|
-
"$ref": "#/$defs/byoc_os"
|
|
7550
|
-
},
|
|
7551
|
-
"arch": {
|
|
7552
|
-
"$ref": "#/$defs/byoc_arch"
|
|
7553
|
-
},
|
|
7554
|
-
"client": {
|
|
7555
|
-
"$ref": "#/$defs/byoc_client"
|
|
7556
|
-
},
|
|
7557
|
-
"client_version": {
|
|
7558
|
-
"$ref": "#/$defs/byoc_client_version"
|
|
7559
|
-
},
|
|
7560
|
-
"harnesses": {
|
|
7561
|
-
"$ref": "#/$defs/byoc_harnesses"
|
|
7562
|
-
},
|
|
7563
|
-
"name": {
|
|
7564
|
-
"$ref": "#/$defs/name"
|
|
7813
|
+
/** Compares my live personal sessions' writes here with my live sessions
|
|
7814
|
+
* of the same Project on my other devices, from my personal catalog, read
|
|
7815
|
+
* at most each TEAM_POLL_MS and only while one of them has writes in the
|
|
7816
|
+
* window. */
|
|
7817
|
+
async devices(state, live, now) {
|
|
7818
|
+
const own = this.options.devices;
|
|
7819
|
+
if (!own) return;
|
|
7820
|
+
const mine = [];
|
|
7821
|
+
for (const [session, entry] of Object.entries(state.sessions)) {
|
|
7822
|
+
if (!entry.project || !live(session)) continue;
|
|
7823
|
+
const paths = {};
|
|
7824
|
+
for (const [key, ms] of Object.entries(entry.paths)) {
|
|
7825
|
+
const file = key.slice(key.indexOf("\n") + 1);
|
|
7826
|
+
paths[file] = Math.max(paths[file] ?? 0, ms);
|
|
7565
7827
|
}
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7828
|
+
mine.push({ session, project: entry.project, paths });
|
|
7829
|
+
}
|
|
7830
|
+
if (!mine.length || now - this.ownPolled < TEAM_POLL_MS) return;
|
|
7831
|
+
this.ownPolled = now;
|
|
7832
|
+
const catalog = await own.read().catch((error) => {
|
|
7833
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
7834
|
+
if (this.failing.get("") !== reason)
|
|
7835
|
+
this.options.log("warn", `conflicts across devices: ${reason}`);
|
|
7836
|
+
this.failing.set("", reason);
|
|
7837
|
+
return null;
|
|
7838
|
+
});
|
|
7839
|
+
if (!catalog) return;
|
|
7840
|
+
this.failing.delete("");
|
|
7841
|
+
const found = deviceConflicts(mine, catalog.rows, {
|
|
7842
|
+
device: own.device,
|
|
7843
|
+
now,
|
|
7844
|
+
alerted: (pair) => pair in state.pairs
|
|
7845
|
+
});
|
|
7846
|
+
await this.alert(
|
|
7847
|
+
state,
|
|
7848
|
+
found.slice(0, this.room(state, found.length)).map((conflict) => ({
|
|
7849
|
+
pair: conflict.pair,
|
|
7850
|
+
messages: [deviceConflictMessage(conflict, catalog.names, now, this.options.home)]
|
|
7851
|
+
})),
|
|
7852
|
+
now
|
|
7853
|
+
);
|
|
7572
7854
|
}
|
|
7573
7855
|
};
|
|
7574
|
-
var byocPresence = {
|
|
7575
|
-
"heartbeat": {
|
|
7576
|
-
"interval_seconds": 300,
|
|
7577
|
-
"jitter_seconds": 30
|
|
7578
|
-
},
|
|
7579
|
-
"online_window_seconds": 900
|
|
7580
|
-
};
|
|
7581
|
-
var byocRetryable = [
|
|
7582
|
-
"device_registration_conflict",
|
|
7583
|
-
"device_report_throttled"
|
|
7584
|
-
];
|
|
7585
7856
|
|
|
7586
7857
|
// src/register.ts
|
|
7587
7858
|
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
@@ -7873,7 +8144,7 @@ function message(error) {
|
|
|
7873
8144
|
}
|
|
7874
8145
|
|
|
7875
8146
|
// src/snapshot.ts
|
|
7876
|
-
import { createHash as
|
|
8147
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
7877
8148
|
import { lstat as lstat4, mkdir as mkdir6, realpath as realpath5 } from "node:fs/promises";
|
|
7878
8149
|
import path19 from "node:path";
|
|
7879
8150
|
var MAX_SNAPSHOT_FILE_BYTES = 100 * 1024 * 1024;
|
|
@@ -8135,7 +8406,7 @@ var Snapshots = class {
|
|
|
8135
8406
|
const index = path19.join(
|
|
8136
8407
|
this.deps.directory,
|
|
8137
8408
|
"index",
|
|
8138
|
-
|
|
8409
|
+
createHash9("sha256").update(folder).digest("hex").slice(0, 32)
|
|
8139
8410
|
);
|
|
8140
8411
|
await mkdir6(path19.dirname(index), { recursive: true, mode: 448 });
|
|
8141
8412
|
const env = { GIT_INDEX_FILE: index };
|
|
@@ -8595,7 +8866,18 @@ async function runDaemon(options, signal) {
|
|
|
8595
8866
|
fetch: options.fetch ?? fetch,
|
|
8596
8867
|
apiOrigin: options.apiOrigin,
|
|
8597
8868
|
gitTokens: options.gitTokens
|
|
8598
|
-
}) : void 0
|
|
8869
|
+
}) : void 0,
|
|
8870
|
+
devices: options.apiOrigin && router ? {
|
|
8871
|
+
device,
|
|
8872
|
+
project: async (cwd) => (await router.project(cwd))?.project_id ?? null,
|
|
8873
|
+
read: ownCatalogReader({
|
|
8874
|
+
key: async () => (await captureLogin(options.credentials).catch(() => null))?.key ?? null,
|
|
8875
|
+
api: new ManagementApi(options.fetch ?? fetch, options.apiOrigin),
|
|
8876
|
+
fetch: options.fetch ?? fetch,
|
|
8877
|
+
apiOrigin: options.apiOrigin,
|
|
8878
|
+
gitTokens: options.gitTokens
|
|
8879
|
+
})
|
|
8880
|
+
} : void 0
|
|
8599
8881
|
}) : void 0;
|
|
8600
8882
|
const registration = registerOnHeartbeat(options, device, signal);
|
|
8601
8883
|
const inbox = options.inbox && options.apiOrigin ? pollInbox(
|
|
@@ -9136,7 +9418,7 @@ async function acquireLock(filename) {
|
|
|
9136
9418
|
|
|
9137
9419
|
// src/device.ts
|
|
9138
9420
|
import { execFile as execFile6 } from "node:child_process";
|
|
9139
|
-
import { createHash as
|
|
9421
|
+
import { createHash as createHash10, randomUUID as randomUUID9 } from "node:crypto";
|
|
9140
9422
|
import { link, mkdir as mkdir8, open as open10, readFile as readFile12, rename as rename8, rm as rm9 } from "node:fs/promises";
|
|
9141
9423
|
import path23 from "node:path";
|
|
9142
9424
|
import { promisify as promisify6 } from "node:util";
|
|
@@ -9159,7 +9441,7 @@ async function readMachineId(platform = process.platform) {
|
|
|
9159
9441
|
return null;
|
|
9160
9442
|
}
|
|
9161
9443
|
function machineIdHash(machineId) {
|
|
9162
|
-
return
|
|
9444
|
+
return createHash10("sha256").update(machineId).digest("hex");
|
|
9163
9445
|
}
|
|
9164
9446
|
var DeviceStore = class {
|
|
9165
9447
|
constructor(filename = path23.join(stateDirectory(), "device.json"), machineId = readMachineId) {
|
|
@@ -9712,44 +9994,6 @@ async function directHandoff(deps, target, options = {}) {
|
|
|
9712
9994
|
}
|
|
9713
9995
|
}
|
|
9714
9996
|
|
|
9715
|
-
// src/devices.ts
|
|
9716
|
-
async function listDevices(list2) {
|
|
9717
|
-
const devices = [];
|
|
9718
|
-
const unavailable = /* @__PURE__ */ new Set();
|
|
9719
|
-
let cursor;
|
|
9720
|
-
for (; ; ) {
|
|
9721
|
-
const page = await list2(cursor ? { cursor } : {});
|
|
9722
|
-
devices.push(
|
|
9723
|
-
...page.data.filter((machine) => machine.kind === "byoc")
|
|
9724
|
-
);
|
|
9725
|
-
for (const region of page.unavailable_regions ?? []) unavailable.add(region);
|
|
9726
|
-
if (!page.next_cursor || page.next_cursor === cursor) break;
|
|
9727
|
-
cursor = page.next_cursor;
|
|
9728
|
-
}
|
|
9729
|
-
return { devices, unavailableRegions: [...unavailable] };
|
|
9730
|
-
}
|
|
9731
|
-
var DISPATCHABLE = /* @__PURE__ */ new Set(["claude-code", "codex"]);
|
|
9732
|
-
function isOnline(lastSeen, now) {
|
|
9733
|
-
const seen = Date.parse(lastSeen);
|
|
9734
|
-
return Number.isFinite(seen) && now - seen <= byocPresence.online_window_seconds * 1e3;
|
|
9735
|
-
}
|
|
9736
|
-
function deviceRows(devices, device, now) {
|
|
9737
|
-
return devices.map((machine) => ({
|
|
9738
|
-
dispatch_target: machine.byoc.device_id !== device && machine.byoc.key_state === "active" && isOnline(machine.byoc.last_seen, now) && machine.byoc.harnesses.some((harness) => DISPATCHABLE.has(harness)),
|
|
9739
|
-
name: machine.name,
|
|
9740
|
-
os: machine.byoc.os,
|
|
9741
|
-
arch: machine.byoc.arch,
|
|
9742
|
-
harnesses: machine.byoc.harnesses,
|
|
9743
|
-
client_version: machine.byoc.client_version,
|
|
9744
|
-
last_seen: machine.byoc.last_seen,
|
|
9745
|
-
online: isOnline(machine.byoc.last_seen, now),
|
|
9746
|
-
key_state: machine.byoc.key_state,
|
|
9747
|
-
this_device: machine.byoc.device_id === device,
|
|
9748
|
-
device_id: machine.byoc.device_id ?? null,
|
|
9749
|
-
hostname: machine.byoc.hostname ?? null
|
|
9750
|
-
})).sort((a, b) => Number(b.this_device) - Number(a.this_device) || a.name.localeCompare(b.name));
|
|
9751
|
-
}
|
|
9752
|
-
|
|
9753
9997
|
// src/dispatch.ts
|
|
9754
9998
|
import { spawn } from "node:child_process";
|
|
9755
9999
|
import { realpathSync } from "node:fs";
|
|
@@ -10720,7 +10964,7 @@ async function thisMachine(options, key) {
|
|
|
10720
10964
|
}
|
|
10721
10965
|
|
|
10722
10966
|
// src/pull.ts
|
|
10723
|
-
import { createHash as
|
|
10967
|
+
import { createHash as createHash11, randomUUID as randomUUID12 } from "node:crypto";
|
|
10724
10968
|
import { open as open13, readFile as readFile16 } from "node:fs/promises";
|
|
10725
10969
|
import path28 from "node:path";
|
|
10726
10970
|
var SHA15 = /^[0-9a-f]{40}$/;
|
|
@@ -10759,7 +11003,7 @@ async function pullHandoff(deps, link3, cwd = process.cwd(), asPatch = false) {
|
|
|
10759
11003
|
if (marker2.patch_file !== path28.basename(patchFile()) || !/^[0-9a-f]{64}$/.test(marker2.patch_sha256 ?? ""))
|
|
10760
11004
|
throw new Error("The local patch marker is corrupt; no release was acknowledged.");
|
|
10761
11005
|
const content = await readFile16(patchFile());
|
|
10762
|
-
if (
|
|
11006
|
+
if (createHash11("sha256").update(content).digest("hex") !== marker2.patch_sha256)
|
|
10763
11007
|
throw new Error("The pulled patch changed; no release was acknowledged.");
|
|
10764
11008
|
} else {
|
|
10765
11009
|
if (marker2.branch !== branch())
|
|
@@ -10867,7 +11111,7 @@ async function pullHandoff(deps, link3, cwd = process.cwd(), asPatch = false) {
|
|
|
10867
11111
|
handoff_id: id,
|
|
10868
11112
|
commit: fetched,
|
|
10869
11113
|
patch_file: path28.basename(patchFile()),
|
|
10870
|
-
patch_sha256:
|
|
11114
|
+
patch_sha256: createHash11("sha256").update(await readFile16(patchFile())).digest("hex")
|
|
10871
11115
|
} : { handoff_id: id, commit: fetched, branch: branch() };
|
|
10872
11116
|
const file = await open13(markerFile(), "wx", 384);
|
|
10873
11117
|
try {
|
|
@@ -10903,7 +11147,7 @@ async function pullHandoff(deps, link3, cwd = process.cwd(), asPatch = false) {
|
|
|
10903
11147
|
}
|
|
10904
11148
|
|
|
10905
11149
|
// src/give.ts
|
|
10906
|
-
import { createHash as
|
|
11150
|
+
import { createHash as createHash12, randomUUID as randomUUID13 } from "node:crypto";
|
|
10907
11151
|
import { mkdir as mkdir11, open as open14, readFile as readFile17 } from "node:fs/promises";
|
|
10908
11152
|
import path29 from "node:path";
|
|
10909
11153
|
var UUID11 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
@@ -10990,7 +11234,7 @@ async function giveHandoff(deps, target, session, task, assignment = OPEN) {
|
|
|
10990
11234
|
target.folder,
|
|
10991
11235
|
pushEnv
|
|
10992
11236
|
);
|
|
10993
|
-
const requestHash =
|
|
11237
|
+
const requestHash = createHash12("sha256").update(
|
|
10994
11238
|
JSON.stringify(
|
|
10995
11239
|
assignment.kind === "open" ? { task, target_kind: "open" } : {
|
|
10996
11240
|
task,
|
|
@@ -11510,7 +11754,7 @@ function handoffLines(view, names = /* @__PURE__ */ new Map()) {
|
|
|
11510
11754
|
}
|
|
11511
11755
|
|
|
11512
11756
|
// src/skills.ts
|
|
11513
|
-
import { createHash as
|
|
11757
|
+
import { createHash as createHash13 } from "node:crypto";
|
|
11514
11758
|
import { mkdir as mkdir13, readFile as readFile18, rm as rm13, rmdir, writeFile as writeFile5 } from "node:fs/promises";
|
|
11515
11759
|
import os10 from "node:os";
|
|
11516
11760
|
import path31 from "node:path";
|
|
@@ -11520,7 +11764,7 @@ var tonboSkill = "---\nname: tonbo\ndescription: Recall the captured agent sessi
|
|
|
11520
11764
|
|
|
11521
11765
|
// src/skills.ts
|
|
11522
11766
|
var SKILL_NAME = "tonbo";
|
|
11523
|
-
var sha256 = (value) =>
|
|
11767
|
+
var sha256 = (value) => createHash13("sha256").update(value).digest("hex");
|
|
11524
11768
|
function skillPaths(home = os10.homedir(), env = process.env) {
|
|
11525
11769
|
const claude = env.CLAUDE_CONFIG_DIR || path31.join(home, ".claude");
|
|
11526
11770
|
const codex = env.CODEX_HOME || path31.join(home, ".codex");
|
|
@@ -12374,19 +12618,27 @@ function createProgram(dependencies = createDependencies) {
|
|
|
12374
12618
|
program.command("login").description("sign in through the browser; shared with the tonbo CLI").option("--account <name>", "the account to log in to; defaults to your personal account").option(
|
|
12375
12619
|
"--name <name>",
|
|
12376
12620
|
"the name this computer registers under the first time; <os>-<n> by default"
|
|
12377
|
-
).option("--no-daemon", "only sign in; do not install and start capture on this computer").
|
|
12378
|
-
|
|
12379
|
-
|
|
12380
|
-
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12621
|
+
).option("--no-daemon", "only sign in; do not install and start capture on this computer").option(
|
|
12622
|
+
"--device",
|
|
12623
|
+
"approve with a code in a browser on any computer, as over SSH without a display"
|
|
12624
|
+
).action(
|
|
12625
|
+
async (options) => {
|
|
12626
|
+
const deps = dependencies();
|
|
12627
|
+
const login = await loginCommand(deps, {
|
|
12628
|
+
account: options.account,
|
|
12629
|
+
device: options.device
|
|
12630
|
+
});
|
|
12631
|
+
if (login.kind === "organization") return;
|
|
12632
|
+
await registerThisComputer(deps, options.name);
|
|
12633
|
+
if (!options.daemon) return;
|
|
12634
|
+
try {
|
|
12635
|
+
deps.output((await installCapture()).join("\n"));
|
|
12636
|
+
deps.output("Capture is on. `yield status` checks it; `yield ps` lists your sessions.");
|
|
12637
|
+
} catch (error) {
|
|
12638
|
+
deps.output(error instanceof Error ? error.message : String(error));
|
|
12639
|
+
}
|
|
12388
12640
|
}
|
|
12389
|
-
|
|
12641
|
+
);
|
|
12390
12642
|
program.command("give").description("push the latest captured snapshot of a session and create a handoff").argument("<session>", "full captured session id").requiredOption("--task <text>", "what the next person or harness should do").option(
|
|
12391
12643
|
"--to <who>",
|
|
12392
12644
|
"a teammate in a team Project (name or subject), claude-code or codex for yourself, or <device>:<harness> for another of your computers"
|