pinomad 0.5.0 → 0.6.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.
@@ -3,7 +3,7 @@ import {
3
3
  generateKeyPair,
4
4
  keyPairFromPrivate,
5
5
  toBase64Url
6
- } from "./chunk-kmbh44dt.js";
6
+ } from "./chunk-7qb9tq54.js";
7
7
 
8
8
  // apps/host/src/devices.ts
9
9
  import { link, readFile, unlink, writeFile } from "node:fs/promises";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  packagedVersion
3
- } from "./chunk-1d9sppj0.js";
3
+ } from "./chunk-c8nxym2z.js";
4
4
  import {
5
5
  hostAddress
6
- } from "./chunk-cjfmfyjb.js";
6
+ } from "./chunk-ngf1zatm.js";
7
7
  import"./chunk-df32avw6.js";
8
8
 
9
9
  // apps/host/src/cli/web-link.ts
@@ -3,28 +3,30 @@ import {
3
3
  assertSecureServerOrigin,
4
4
  probeServerAccounts,
5
5
  runServerOperation
6
- } from "./chunk-26d04z02.js";
6
+ } from "./chunk-ywmm63ks.js";
7
7
  import {
8
8
  loadHostKey,
9
9
  loadRelayKey,
10
10
  readServerConfig,
11
11
  removeServerConfig
12
- } from "./chunk-ebfaf3k2.js";
12
+ } from "./chunk-0xh6cy9v.js";
13
13
  import {
14
14
  relayHostId
15
- } from "./chunk-kmbh44dt.js";
15
+ } from "./chunk-7qb9tq54.js";
16
16
  import {
17
17
  argValue,
18
18
  restartService,
19
19
  serviceManager,
20
20
  withoutRelayArgs
21
- } from "./chunk-aqj4wg33.js";
22
- import"./chunk-1d9sppj0.js";
23
- import"./chunk-m4q9wafw.js";
24
- import"./chunk-hsp12cjt.js";
21
+ } from "./chunk-d5ypmmr7.js";
22
+ import"./chunk-kzbzacz9.js";
23
+ import"./chunk-c8nxym2z.js";
24
+ import"./chunk-d93kvxvc.js";
25
+ import"./chunk-n4tjhcb7.js";
26
+ import"./chunk-znhn8af7.js";
25
27
  import {
26
28
  DEFAULT_DATA_DIR
27
- } from "./chunk-cjfmfyjb.js";
29
+ } from "./chunk-ngf1zatm.js";
28
30
  import"./chunk-df32avw6.js";
29
31
 
30
32
  // apps/host/src/cli/leave.ts
@@ -2,6 +2,13 @@
2
2
  import {
3
3
  serveWebClient
4
4
  } from "./chunk-r6w5aqkw.js";
5
+ import {
6
+ Published,
7
+ constantValue,
8
+ openHostSettings,
9
+ sameList,
10
+ settingsDefaults
11
+ } from "./chunk-b1h374jr.js";
5
12
  import {
6
13
  DevicesDoc,
7
14
  RevocationsDoc,
@@ -14,7 +21,7 @@ import {
14
21
  readServerBinding,
15
22
  readServerConfig,
16
23
  resolveRelayOrigin
17
- } from "./chunk-ebfaf3k2.js";
24
+ } from "./chunk-0xh6cy9v.js";
18
25
  import {
19
26
  SECURE_PROLOGUE,
20
27
  SecureSession,
@@ -24,25 +31,25 @@ import {
24
31
  respondIK,
25
32
  startRelayLink,
26
33
  toBase64Url
27
- } from "./chunk-kmbh44dt.js";
34
+ } from "./chunk-7qb9tq54.js";
28
35
  import {
29
36
  packagedVersion,
30
37
  webRoot
31
- } from "./chunk-1d9sppj0.js";
38
+ } from "./chunk-c8nxym2z.js";
32
39
  import {
33
- HOST_SETTING_KEYS,
34
40
  INVALID_FRAME_CLOSE_CODE,
35
41
  PROTOCOL,
36
- THINKING_LEVELS,
37
42
  UNAUTHORIZED_CLOSE_CODE,
38
- isQueueMode,
39
- isThinkingLevel,
40
43
  organize,
41
44
  parseClientFrame
42
- } from "./chunk-hsp12cjt.js";
45
+ } from "./chunk-n4tjhcb7.js";
46
+ import {
47
+ THINKING_LEVELS,
48
+ isThinkingLevel
49
+ } from "./chunk-znhn8af7.js";
43
50
  import {
44
51
  DEFAULT_DATA_DIR
45
- } from "./chunk-cjfmfyjb.js";
52
+ } from "./chunk-ngf1zatm.js";
46
53
  import"./chunk-df32avw6.js";
47
54
 
48
55
  // apps/host/src/main.ts
@@ -181,7 +188,7 @@ function result(questions, resolution) {
181
188
  var askUserQuestion = defineTool({
182
189
  name: "ask_user_question",
183
190
  callers: ["model"],
184
- description: "Ask the user 1–4 multiple-choice questions when their decision is needed to proceed (ambiguous requirements, choosing between approaches, scope). Blocks until answered. The user can always type a custom answer, so never add an 'Other' option. Keep headers short.",
191
+ description: "Ask the user 1–4 multiple-choice questions when their decision is needed to proceed (ambiguous requirements, choosing between approaches, scope). Blocks until answered. The user can always type a custom answer, so never add an 'Other' option. Keep everything short: headers a word or two, option labels a few words. Usually leave option descriptions out; add one only when the label alone is ambiguous, as one line of at most ~60 characters.",
185
192
  parameters: Type2.Object({
186
193
  questions: Type2.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 })
187
194
  }),
@@ -522,7 +529,7 @@ var todo = {
522
529
 
523
530
  // apps/host/src/host.ts
524
531
  import { randomBytes as randomBytes3 } from "node:crypto";
525
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
532
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
526
533
  import { basename as basename2, join as join8, sep as sep5 } from "node:path";
527
534
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
528
535
  import { createRegistry, Harness } from "@earendil-works/pi-durable";
@@ -538,7 +545,7 @@ import {
538
545
  } from "@earendil-works/pi-durable";
539
546
 
540
547
  // apps/host/src/checkout.ts
541
- import { execFile } from "node:child_process";
548
+ import { execFile, spawn } from "node:child_process";
542
549
  import { randomBytes } from "node:crypto";
543
550
  import { realpath, rm, stat } from "node:fs/promises";
544
551
  import { tmpdir } from "node:os";
@@ -546,6 +553,42 @@ import { join as join2, relative, sep as sep2 } from "node:path";
546
553
  import { promisify } from "node:util";
547
554
  var run = promisify(execFile);
548
555
  var git = (dir, args, env) => run("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
556
+ function gitCapped(dir, args, maxBytes, env) {
557
+ return new Promise((resolve, reject) => {
558
+ const child = spawn("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
559
+ const chunks = [];
560
+ let size = 0;
561
+ let killed = false;
562
+ let stderr = "";
563
+ child.stdout.on("data", (chunk) => {
564
+ if (killed)
565
+ return;
566
+ chunks.push(chunk);
567
+ size += chunk.length;
568
+ if (size > maxBytes) {
569
+ killed = true;
570
+ child.kill();
571
+ child.stdout.destroy();
572
+ child.stderr.destroy();
573
+ }
574
+ });
575
+ child.stderr.on("data", (chunk) => {
576
+ if (stderr.length < 8192)
577
+ stderr += chunk.toString("utf8");
578
+ });
579
+ child.on("error", reject);
580
+ child.on("close", (code) => {
581
+ const stdout = Buffer.concat(chunks).toString("utf8");
582
+ if (killed)
583
+ resolve({ stdout, truncated: true });
584
+ else if (code === 0)
585
+ resolve({ stdout, truncated: false });
586
+ else
587
+ reject(Object.assign(new Error(`Command failed: git ${args.join(" ")}
588
+ ${stderr}`), { code, stderr }));
589
+ });
590
+ });
591
+ }
549
592
  var worktreeRoot = (dataDir) => join2(dataDir, "worktrees");
550
593
  var worktreePath = (dataDir, id) => join2(worktreeRoot(dataDir), String(id));
551
594
  var worktreeBranch = (id, suffix) => `pinomad/${id}-${suffix}`;
@@ -565,6 +608,15 @@ async function gitBase(projectPath) {
565
608
  return;
566
609
  return { repo, subdir: relative(repo, projectPath), base };
567
610
  }
611
+ async function branchOf(dir) {
612
+ const ref = await git(dir, ["symbolic-ref", "--quiet", "HEAD"]).then(({ stdout }) => stdout.trim(), () => {
613
+ return;
614
+ });
615
+ const prefix = "refs/heads/";
616
+ if (ref === undefined || !ref.startsWith(prefix))
617
+ return;
618
+ return ref.slice(prefix.length) || undefined;
619
+ }
568
620
  var repoLocks = new Map;
569
621
  function locked(repo, work) {
570
622
  const next = (repoLocks.get(repo) ?? Promise.resolve()).then(work, work);
@@ -615,12 +667,11 @@ async function changesOf(dir, base, maxBytes = 1024 * 1024) {
615
667
  try {
616
668
  await git(dir, ["read-tree", "HEAD"], env);
617
669
  await git(dir, ["add", "-A"], env);
618
- const { stdout } = await git(dir, ["diff", "--cached", "--no-color", "--find-renames", "--src-prefix=a/", "--dst-prefix=b/", base], env);
619
- if (Buffer.byteLength(stdout) <= maxBytes)
670
+ const { stdout, truncated } = await gitCapped(dir, ["diff", "--cached", "--no-color", "--find-renames", "--src-prefix=a/", "--dst-prefix=b/", base], maxBytes, env);
671
+ if (!truncated && Buffer.byteLength(stdout) <= maxBytes)
620
672
  return { base, patch: stdout, truncated: false };
621
- const cut = stdout.slice(0, maxBytes);
622
- const patch = cut.slice(0, cut.lastIndexOf(`
623
- `) + 1);
673
+ const cut = Buffer.from(stdout, "utf8").subarray(0, maxBytes);
674
+ const patch = cut.subarray(0, cut.lastIndexOf(10) + 1).toString("utf8");
624
675
  return { base, patch, truncated: true };
625
676
  } finally {
626
677
  await rm(indexFile, { force: true });
@@ -1196,39 +1247,6 @@ function agentRoleProblem(role) {
1196
1247
  return;
1197
1248
  }
1198
1249
 
1199
- // apps/host/src/host-value.ts
1200
- function constantValue(value) {
1201
- return {
1202
- value,
1203
- subscribe: () => () => {}
1204
- };
1205
- }
1206
-
1207
- class Published {
1208
- #listeners = new Set;
1209
- #equal;
1210
- #value;
1211
- constructor(value, equal) {
1212
- this.#value = value;
1213
- this.#equal = equal;
1214
- }
1215
- get value() {
1216
- return this.#value;
1217
- }
1218
- subscribe(listener) {
1219
- this.#listeners.add(listener);
1220
- return () => this.#listeners.delete(listener);
1221
- }
1222
- set(next) {
1223
- if (this.#equal(this.#value, next))
1224
- return;
1225
- this.#value = next;
1226
- for (const listener of this.#listeners)
1227
- listener(next);
1228
- }
1229
- }
1230
- var sameList = (a, b, same) => a.length === b.length && a.every((entry, index) => same(entry, b[index]));
1231
-
1232
1250
  // apps/host/src/agent-roles.ts
1233
1251
  var KNOWN_FIELDS = new Set(["description", "model", "thinkingLevel"]);
1234
1252
  async function load(dir) {
@@ -1382,114 +1400,6 @@ async function openAgentRoles(options) {
1382
1400
  };
1383
1401
  }
1384
1402
 
1385
- // apps/host/src/host-settings.ts
1386
- import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
1387
- import { dirname as dirname2 } from "node:path";
1388
- function settingsDefaults(settings) {
1389
- return {
1390
- ...settings.defaultModel === null ? {} : { model: settings.defaultModel },
1391
- ...settings.defaultThinkingLevel === null ? {} : { thinkingLevel: settings.defaultThinkingLevel }
1392
- };
1393
- }
1394
- var isModelRef = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.provider === "string" && value.provider !== "" && typeof value.modelId === "string" && value.modelId !== "";
1395
- var fileValueError = (key, value, models) => {
1396
- switch (key) {
1397
- case "defaultModel":
1398
- if (!isModelRef(value))
1399
- return "malformed";
1400
- return models.some((each) => each.provider === value.provider && each.modelId === value.modelId) ? undefined : "unavailable";
1401
- case "defaultThinkingLevel":
1402
- return isThinkingLevel(value) ? undefined : "malformed";
1403
- case "steeringMode":
1404
- case "followUpMode":
1405
- return isQueueMode(value) ? undefined : "malformed";
1406
- case "acceptAccountDevices":
1407
- return typeof value === "boolean" ? undefined : "malformed";
1408
- }
1409
- };
1410
- var sameModel = (a, b) => a === b || a !== null && b !== null && a.provider === b.provider && a.modelId === b.modelId;
1411
- var sameSettings = (a, b) => a.steeringMode === b.steeringMode && a.followUpMode === b.followUpMode && a.acceptAccountDevices === b.acceptAccountDevices && a.defaultThinkingLevel === b.defaultThinkingLevel && sameModel(a.defaultModel, b.defaultModel) && sameList(a.custom, b.custom, (x, y) => x === y);
1412
- var parse = (path, text) => {
1413
- let parsed;
1414
- try {
1415
- parsed = JSON.parse(text);
1416
- } catch (error) {
1417
- throw new Error(`Invalid settings file ${path}: ${error instanceof Error ? error.message : String(error)}`);
1418
- }
1419
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1420
- throw new Error(`Invalid settings file ${path}: expected a JSON object`);
1421
- }
1422
- return parsed;
1423
- };
1424
- async function openHostSettings(options) {
1425
- let file = await readFile3(options.path, "utf8").then((text) => parse(options.path, text), (error) => {
1426
- if (error.code === "ENOENT")
1427
- return {};
1428
- throw new Error(`Cannot read settings file ${options.path}: ${error instanceof Error ? error.message : String(error)}`);
1429
- });
1430
- const warned = new Set;
1431
- const compute = () => {
1432
- const fileValues = {};
1433
- const custom = [];
1434
- for (const key of HOST_SETTING_KEYS) {
1435
- if (!Object.hasOwn(file, key))
1436
- continue;
1437
- const value = file[key];
1438
- const invalid = fileValueError(key, value, options.models.value);
1439
- if (invalid === "malformed" && !warned.has(key)) {
1440
- warned.add(key);
1441
- console.error(`${options.path}: ignoring invalid value for settings key "${key}": ${JSON.stringify(value)}`);
1442
- }
1443
- if (invalid !== undefined)
1444
- continue;
1445
- fileValues[key] = value;
1446
- custom.push(key);
1447
- }
1448
- const { fallback } = options;
1449
- return {
1450
- defaultModel: fileValues.defaultModel ?? fallback.defaultModel(),
1451
- defaultThinkingLevel: fileValues.defaultThinkingLevel ?? fallback.defaultThinkingLevel(),
1452
- steeringMode: fileValues.steeringMode ?? fallback.steeringMode(),
1453
- followUpMode: fileValues.followUpMode ?? fallback.followUpMode(),
1454
- acceptAccountDevices: fileValues.acceptAccountDevices ?? true,
1455
- custom
1456
- };
1457
- };
1458
- const published = new Published(compute(), sameSettings);
1459
- options.models.subscribe(() => published.set(compute()));
1460
- let tail = Promise.resolve();
1461
- const write = async (patch) => {
1462
- const model = patch.defaultModel;
1463
- if (model !== undefined && model !== null && !options.models.value.some((each) => each.provider === model.provider && each.modelId === model.modelId)) {
1464
- throw new Error(`Unknown model: ${model.provider}/${model.modelId}`);
1465
- }
1466
- const next = { ...file };
1467
- for (const key of HOST_SETTING_KEYS) {
1468
- const value = patch[key];
1469
- if (value === undefined)
1470
- continue;
1471
- if (value === null)
1472
- delete next[key];
1473
- else
1474
- next[key] = value;
1475
- }
1476
- const tmp = `${options.path}.tmp`;
1477
- await mkdir3(dirname2(options.path), { recursive: true });
1478
- await writeFile2(tmp, `${JSON.stringify(next, null, 2)}
1479
- `, { mode: 384 });
1480
- await rename2(tmp, options.path);
1481
- file = next;
1482
- published.set(compute());
1483
- };
1484
- return {
1485
- get value() {
1486
- return published.value;
1487
- },
1488
- subscribe: (listener) => published.subscribe(listener),
1489
- update: (patch) => tail = tail.then(() => write(patch), () => write(patch))
1490
- };
1491
- }
1492
-
1493
1403
  // apps/host/src/host.ts
1494
1404
  import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
1495
1405
  import lockfile from "proper-lockfile";
@@ -1549,7 +1459,7 @@ async function listFolders(path, home) {
1549
1459
  }
1550
1460
 
1551
1461
  // apps/host/src/files.ts
1552
- import { spawn } from "node:child_process";
1462
+ import { spawn as spawn2 } from "node:child_process";
1553
1463
  import { opendir, realpath as realpath4, stat as stat4 } from "node:fs/promises";
1554
1464
  import { join as join6, resolve as resolve3, sep as sep3 } from "node:path";
1555
1465
  import { AgentDoc as AgentDoc3 } from "@earendil-works/pi-durable";
@@ -1608,7 +1518,7 @@ function createCandidateLister(enumerate, options = {}) {
1608
1518
  }
1609
1519
  function gitLines(cwd, args, limits) {
1610
1520
  return new Promise((resolvePromise, rejectPromise) => {
1611
- const child = spawn("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
1521
+ const child = spawn2("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
1612
1522
  const chunks = [];
1613
1523
  let bytes = 0;
1614
1524
  let truncated = false;
@@ -1656,7 +1566,7 @@ function gitLines(cwd, args, limits) {
1656
1566
  }
1657
1567
  async function inWorkTree(root) {
1658
1568
  return await new Promise((resolvePromise) => {
1659
- const child = spawn("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: ["ignore", "pipe", "ignore"] });
1569
+ const child = spawn2("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: ["ignore", "pipe", "ignore"] });
1660
1570
  let out = "";
1661
1571
  child.stdout.on("data", (chunk) => out += chunk.toString("utf8"));
1662
1572
  child.on("error", () => resolvePromise(false));
@@ -1665,7 +1575,7 @@ async function inWorkTree(root) {
1665
1575
  }
1666
1576
  async function hasHead(root) {
1667
1577
  return await new Promise((resolvePromise) => {
1668
- const child = spawn("git", ["-C", root, "rev-parse", "--verify", "-q", "HEAD"], { stdio: ["ignore", "ignore", "ignore"] });
1578
+ const child = spawn2("git", ["-C", root, "rev-parse", "--verify", "-q", "HEAD"], { stdio: ["ignore", "ignore", "ignore"] });
1669
1579
  child.on("error", () => resolvePromise(false));
1670
1580
  child.on("close", (code) => resolvePromise(code === 0));
1671
1581
  });
@@ -2479,6 +2389,12 @@ async function startGateway(options) {
2479
2389
  signingKey: secure.relay.signingKey,
2480
2390
  noisePublicKey: secure.hostKey.publicKey,
2481
2391
  accept: (socket) => secureHandshake(socket, secure, admission, register),
2392
+ hostName: {
2393
+ get value() {
2394
+ return options.settings.value.hostName;
2395
+ },
2396
+ subscribe: (listener) => options.settings.subscribe((settings) => listener(settings.hostName))
2397
+ },
2482
2398
  accountOffer: async () => {
2483
2399
  if (!options.settings.value.acceptAccountDevices)
2484
2400
  return { ok: false, reason: "disabled" };
@@ -3456,12 +3372,13 @@ class GatewayClient {
3456
3372
  if (record !== undefined) {
3457
3373
  if (!await worktreeExists(record.path))
3458
3374
  return { available: false, reason: "No changes yet" };
3459
- return { available: true, ...await changesOf(record.path, record.base) };
3375
+ const [changes, branch2] = await Promise.all([changesOf(record.path, record.base), branchOf(record.path)]);
3376
+ return { available: true, ...changes, branch: branch2 };
3460
3377
  }
3461
- const base = await gitBase(cwd);
3378
+ const [base, branch] = await Promise.all([gitBase(cwd), branchOf(cwd)]);
3462
3379
  if (base === undefined)
3463
- return { available: false, reason: "Not in a git repository" };
3464
- return { available: true, ...await changesOf(base.repo, base.base) };
3380
+ return { available: false, reason: "Not in a git repository", branch };
3381
+ return { available: true, ...await changesOf(base.repo, base.base), branch };
3465
3382
  }
3466
3383
  async#agent(id) {
3467
3384
  return await this.#options.harness.snapshot(AgentDoc4, id, context3) ?? {};
@@ -3482,16 +3399,16 @@ var context4 = BACKGROUND_CONTEXT4;
3482
3399
  async function hostToken(dataDir) {
3483
3400
  const path = join8(dataDir, "token");
3484
3401
  try {
3485
- return (await readFile4(path, "utf8")).trim();
3402
+ return (await readFile3(path, "utf8")).trim();
3486
3403
  } catch {
3487
3404
  const token = randomBytes3(24).toString("base64url");
3488
- await writeFile3(path, `${token}
3405
+ await writeFile2(path, `${token}
3489
3406
  `, { mode: 384 });
3490
3407
  return token;
3491
3408
  }
3492
3409
  }
3493
3410
  async function openHost(options) {
3494
- await mkdir4(options.dataDir, { recursive: true });
3411
+ await mkdir3(options.dataDir, { recursive: true });
3495
3412
  const stale = Math.max(2000, options.lockStaleMs ?? 1e4);
3496
3413
  const release = await lockfile.lock(options.dataDir, {
3497
3414
  realpath: false,
@@ -3551,11 +3468,11 @@ async function openHost(options) {
3551
3468
  const record = checkoutAt(index?.checkouts, cwd);
3552
3469
  if (record !== undefined) {
3553
3470
  await ensureWorktree(record);
3554
- await mkdir4(cwd, { recursive: true });
3471
+ await mkdir3(cwd, { recursive: true });
3555
3472
  }
3556
3473
  }
3557
3474
  if (cwd.startsWith(`${chatsDir}/`))
3558
- await mkdir4(cwd, { recursive: true });
3475
+ await mkdir3(cwd, { recursive: true });
3559
3476
  let env = environments.get(cwd);
3560
3477
  if (env === undefined) {
3561
3478
  env = new NodeExecutionEnv({ cwd });
@@ -3671,7 +3588,7 @@ async function openHost(options) {
3671
3588
 
3672
3589
  // apps/host/src/model-auth.ts
3673
3590
  import { existsSync as existsSync2, watch as watch2 } from "node:fs";
3674
- import { basename as basename3, dirname as dirname3 } from "node:path";
3591
+ import { basename as basename3, dirname as dirname2 } from "node:path";
3675
3592
 
3676
3593
  // apps/host/src/pi-setup.ts
3677
3594
  import { getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai";
@@ -3786,7 +3703,7 @@ function modelSummaries(modelRuntime) {
3786
3703
  }
3787
3704
 
3788
3705
  // apps/host/src/model-auth.ts
3789
- var sameModel2 = (a, b) => a.provider === b.provider && a.modelId === b.modelId && a.name === b.name && a.contextWindow === b.contextWindow && sameList(a.thinkingLevels, b.thinkingLevels, (x, y) => x === y);
3706
+ var sameModel = (a, b) => a.provider === b.provider && a.modelId === b.modelId && a.name === b.name && a.contextWindow === b.contextWindow && sameList(a.thinkingLevels, b.thinkingLevels, (x, y) => x === y);
3790
3707
  var sameLogin = (a, b) => a.providerId === b.providerId && a.name === b.name && a.kind === b.kind && a.subscription === b.subscription;
3791
3708
  async function storedLogins(modelRuntime) {
3792
3709
  const credentials = await modelRuntime.listCredentials();
@@ -3801,7 +3718,7 @@ async function storedLogins(modelRuntime) {
3801
3718
  }
3802
3719
  async function watchModelAuth(modelRuntime, options) {
3803
3720
  const debounceMs = options.debounceMs ?? 150;
3804
- const models = new Published(modelSummaries(modelRuntime), (a, b) => sameList(a, b, sameModel2));
3721
+ const models = new Published(modelSummaries(modelRuntime), (a, b) => sameList(a, b, sameModel));
3805
3722
  const auth = new Published(await storedLogins(modelRuntime), (a, b) => sameList(a.logins, b.logins, sameLogin));
3806
3723
  const fileName = basename3(options.authPath);
3807
3724
  let watcher;
@@ -3820,7 +3737,7 @@ async function watchModelAuth(modelRuntime, options) {
3820
3737
  reported = true;
3821
3738
  }
3822
3739
  };
3823
- const directory = dirname3(options.authPath);
3740
+ const directory = dirname2(options.authPath);
3824
3741
  if (existsSync2(directory)) {
3825
3742
  watcher = watch2(directory, (_event, changed) => {
3826
3743
  if (closed || changed !== null && changed !== fileName)
@@ -1,3 +1,7 @@
1
+ import {
2
+ HOST_NAME_MAX_UNITS
3
+ } from "./chunk-znhn8af7.js";
4
+
1
5
  // packages/protocol/src/relay.ts
2
6
  import { ed25519 } from "@noble/curves/ed25519.js";
3
7
 
@@ -332,14 +336,18 @@ function decodeRelayToHost(text) {
332
336
  throw new Error("unknown relay frame");
333
337
  }
334
338
  }
339
+ var isFeatureList = (value) => Array.isArray(value) && value.length <= 16 && value.every((entry) => isString2(entry) && entry.length <= 64);
335
340
  function decodeRegistered(value) {
341
+ if (value.features !== undefined && !isFeatureList(value.features))
342
+ throw new Error("malformed registered");
343
+ const declared = value.features === undefined ? {} : { features: value.features };
336
344
  if (value.push === undefined)
337
- return { t: "registered" };
345
+ return { t: "registered", ...declared };
338
346
  if (!Array.isArray(value.push) || value.push.length > 8 || !value.push.every(isString2)) {
339
347
  throw new Error("malformed registered");
340
348
  }
341
349
  const push = value.push.filter((entry) => entry === "production" || entry === "development");
342
- return { t: "registered", push };
350
+ return { t: "registered", push, ...declared };
343
351
  }
344
352
  function decodePushResult(value) {
345
353
  if (!isBoundedString(value.requestId) || !isBoundedString(value.reason, 256) || value.result !== "accepted" && value.result !== "unregistered" && value.result !== "retry" && value.result !== "rejected" && value.result !== "unavailable") {
@@ -388,6 +396,10 @@ function decodeHostToRelay(text) {
388
396
  throw new Error("malformed pairing-refused");
389
397
  }
390
398
  return { t: "pairing-refused", requestId: value.requestId, reason: value.reason };
399
+ case "name":
400
+ if (!isBoundedString(value.name, HOST_NAME_MAX_UNITS))
401
+ throw new Error("malformed name");
402
+ return { t: "name", name: value.name };
391
403
  default:
392
404
  throw new Error("unknown host frame");
393
405
  }
@@ -425,9 +437,8 @@ function decodeRegister(value) {
425
437
  throw new Error("malformed register");
426
438
  if (value.name !== undefined && !isString2(value.name))
427
439
  throw new Error("malformed register");
428
- if (value.features !== undefined && (!Array.isArray(value.features) || value.features.length > 16 || !value.features.every((feature) => isString2(feature) && feature.length <= 64))) {
440
+ if (value.features !== undefined && !isFeatureList(value.features))
429
441
  throw new Error("malformed register");
430
- }
431
442
  return {
432
443
  t: "register",
433
444
  hostId: value.hostId,
@@ -511,6 +522,8 @@ function startRelayLink(options) {
511
522
  let retryTimer;
512
523
  let pendingAccepts = 0;
513
524
  let pushEnvironments;
525
+ let reportedName;
526
+ let nameDeclared = false;
514
527
  const pushes = new Map;
515
528
  function setState(next) {
516
529
  const changed = state.status !== next.status || state.status === "rejected" && next.status === "rejected" && state.reason !== next.reason;
@@ -546,6 +559,8 @@ function startRelayLink(options) {
546
559
  const socket = new WebSocket2(`${wsBase}/host`, { handshakeTimeout: pingIntervalMs });
547
560
  control = socket;
548
561
  pushEnvironments = undefined;
562
+ nameDeclared = false;
563
+ reportedName = undefined;
549
564
  watch(socket);
550
565
  socket.on("message", (data, isBinary) => onControlMessage(socket, data, isBinary));
551
566
  socket.once("close", (code) => onControlClosed(socket, code));
@@ -572,21 +587,30 @@ function startRelayLink(options) {
572
587
  case "challenge": {
573
588
  const noisePublicKey = toBase64Url(options.noisePublicKey);
574
589
  const challenge = { relayOrigin: origin, hostId, nonce: frame.nonce };
590
+ reportedName = options.hostName?.value;
575
591
  socket.send(encodeRelayFrame({
576
592
  t: "register",
577
593
  hostId,
578
594
  signature: signRelayChallenge(options.signingKey.secretKey, challenge),
579
595
  signatureV2: signRelayChallengeV2(options.signingKey.secretKey, { ...challenge, noisePublicKey }),
580
596
  noisePublicKey,
581
- ...options.accountOffer === undefined ? {} : { features: ["pairing"] }
597
+ ...options.accountOffer === undefined ? {} : { features: ["pairing"] },
598
+ ...reportedName === undefined ? {} : { name: reportedName }
582
599
  }));
583
600
  break;
584
601
  }
585
- case "registered":
602
+ case "registered": {
586
603
  delay = minDelay;
587
604
  pushEnvironments = frame.push;
605
+ nameDeclared = frame.features?.includes("name") === true;
606
+ const current = options.hostName?.value;
607
+ if (nameDeclared && current !== undefined && current !== reportedName) {
608
+ reportedName = current;
609
+ socket.send(encodeRelayFrame({ t: "name", name: current }));
610
+ }
588
611
  setState({ status: "registered", ...frame.push === undefined ? {} : { push: frame.push } });
589
612
  break;
613
+ }
590
614
  case "push-result": {
591
615
  const request = pushes.get(frame.requestId);
592
616
  if (request === undefined || request.socket !== socket)
@@ -636,6 +660,8 @@ function startRelayLink(options) {
636
660
  function onControlClosed(socket, code) {
637
661
  control = undefined;
638
662
  pushEnvironments = undefined;
663
+ nameDeclared = false;
664
+ reportedName = undefined;
639
665
  for (const [requestId, request] of pushes) {
640
666
  if (request.socket === socket) {
641
667
  pushes.delete(requestId);
@@ -656,6 +682,15 @@ function startRelayLink(options) {
656
682
  reconnect(delay);
657
683
  delay = Math.min(maxDelay, delay * 2);
658
684
  }
685
+ const detachName = options.hostName?.subscribe((name) => {
686
+ if (name === undefined || name === reportedName)
687
+ return;
688
+ const socket = control;
689
+ if (socket === undefined || socket.readyState !== WebSocket2.OPEN || !nameDeclared)
690
+ return;
691
+ reportedName = name;
692
+ socket.send(encodeRelayFrame({ t: "name", name }));
693
+ });
659
694
  connectControl();
660
695
  return {
661
696
  hostId,
@@ -694,6 +729,7 @@ function startRelayLink(options) {
694
729
  if (retryTimer !== undefined)
695
730
  clearTimeout(retryTimer);
696
731
  clearInterval(pingTimer);
732
+ detachName?.();
697
733
  for (const socket of watched)
698
734
  socket.terminate();
699
735
  return Promise.resolve();
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  connectRemoteDurable
3
- } from "./chunk-m4q9wafw.js";
4
- import"./chunk-hsp12cjt.js";
3
+ } from "./chunk-d93kvxvc.js";
4
+ import"./chunk-n4tjhcb7.js";
5
+ import"./chunk-znhn8af7.js";
5
6
  import {
6
7
  hostAddress
7
- } from "./chunk-cjfmfyjb.js";
8
+ } from "./chunk-ngf1zatm.js";
8
9
  import"./chunk-df32avw6.js";
9
10
 
10
11
  // apps/host/src/cli/pair.ts