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.
@@ -0,0 +1,159 @@
1
+ import {
2
+ HOST_SETTING_KEYS,
3
+ cleanHostName,
4
+ isQueueMode,
5
+ isThinkingLevel,
6
+ normalizeHostName
7
+ } from "./chunk-znhn8af7.js";
8
+
9
+ // apps/host/src/host-value.ts
10
+ function constantValue(value) {
11
+ return {
12
+ value,
13
+ subscribe: () => () => {}
14
+ };
15
+ }
16
+
17
+ class Published {
18
+ #listeners = new Set;
19
+ #equal;
20
+ #value;
21
+ constructor(value, equal) {
22
+ this.#value = value;
23
+ this.#equal = equal;
24
+ }
25
+ get value() {
26
+ return this.#value;
27
+ }
28
+ subscribe(listener) {
29
+ this.#listeners.add(listener);
30
+ return () => this.#listeners.delete(listener);
31
+ }
32
+ set(next) {
33
+ if (this.#equal(this.#value, next))
34
+ return;
35
+ this.#value = next;
36
+ for (const listener of this.#listeners)
37
+ listener(next);
38
+ }
39
+ }
40
+ var sameList = (a, b, same) => a.length === b.length && a.every((entry, index) => same(entry, b[index]));
41
+
42
+ // apps/host/src/host-settings.ts
43
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
44
+ import { hostname } from "node:os";
45
+ import { dirname } from "node:path";
46
+ function settingsDefaults(settings) {
47
+ return {
48
+ ...settings.defaultModel === null ? {} : { model: settings.defaultModel },
49
+ ...settings.defaultThinkingLevel === null ? {} : { thinkingLevel: settings.defaultThinkingLevel }
50
+ };
51
+ }
52
+ var isModelRef = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.provider === "string" && value.provider !== "" && typeof value.modelId === "string" && value.modelId !== "";
53
+ var fileValueError = (key, value, models) => {
54
+ switch (key) {
55
+ case "defaultModel":
56
+ if (!isModelRef(value))
57
+ return "malformed";
58
+ return models.some((each) => each.provider === value.provider && each.modelId === value.modelId) ? undefined : "unavailable";
59
+ case "defaultThinkingLevel":
60
+ return isThinkingLevel(value) ? undefined : "malformed";
61
+ case "steeringMode":
62
+ case "followUpMode":
63
+ return isQueueMode(value) ? undefined : "malformed";
64
+ case "acceptAccountDevices":
65
+ return typeof value === "boolean" ? undefined : "malformed";
66
+ case "hostName":
67
+ return typeof value === "string" ? undefined : "malformed";
68
+ }
69
+ };
70
+ var sameModel = (a, b) => a === b || a !== null && b !== null && a.provider === b.provider && a.modelId === b.modelId;
71
+ 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) && a.hostName === b.hostName && sameList(a.custom, b.custom, (x, y) => x === y);
72
+ var parse = (path, text) => {
73
+ let parsed;
74
+ try {
75
+ parsed = JSON.parse(text);
76
+ } catch (error) {
77
+ throw new Error(`Invalid settings file ${path}: ${error instanceof Error ? error.message : String(error)}`);
78
+ }
79
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
80
+ throw new Error(`Invalid settings file ${path}: expected a JSON object`);
81
+ }
82
+ return parsed;
83
+ };
84
+ async function openHostSettings(options) {
85
+ const read = () => readFile(options.path, "utf8").then((text) => parse(options.path, text), (error) => {
86
+ if (error.code === "ENOENT")
87
+ return {};
88
+ throw new Error(`Cannot read settings file ${options.path}: ${error instanceof Error ? error.message : String(error)}`);
89
+ });
90
+ let file = await read();
91
+ const warned = new Set;
92
+ const compute = () => {
93
+ const fileValues = {};
94
+ const custom = [];
95
+ for (const key of HOST_SETTING_KEYS) {
96
+ if (!Object.hasOwn(file, key))
97
+ continue;
98
+ const value = file[key];
99
+ const invalid = fileValueError(key, value, options.models.value);
100
+ if (invalid === "malformed" && !warned.has(key)) {
101
+ warned.add(key);
102
+ console.error(`${options.path}: ignoring invalid value for settings key "${key}": ${JSON.stringify(value)}`);
103
+ }
104
+ if (invalid !== undefined)
105
+ continue;
106
+ fileValues[key] = value;
107
+ custom.push(key);
108
+ }
109
+ const { fallback } = options;
110
+ return {
111
+ defaultModel: fileValues.defaultModel ?? fallback.defaultModel(),
112
+ defaultThinkingLevel: fileValues.defaultThinkingLevel ?? fallback.defaultThinkingLevel(),
113
+ steeringMode: fileValues.steeringMode ?? fallback.steeringMode(),
114
+ followUpMode: fileValues.followUpMode ?? fallback.followUpMode(),
115
+ acceptAccountDevices: fileValues.acceptAccountDevices ?? true,
116
+ hostName: normalizeHostName(fileValues.hostName ?? hostname()),
117
+ custom
118
+ };
119
+ };
120
+ const published = new Published(compute(), sameSettings);
121
+ options.models.subscribe(() => published.set(compute()));
122
+ let tail = Promise.resolve();
123
+ const write = async (patch) => {
124
+ const model = patch.defaultModel;
125
+ if (model !== undefined && model !== null && !options.models.value.some((each) => each.provider === model.provider && each.modelId === model.modelId)) {
126
+ throw new Error(`Unknown model: ${model.provider}/${model.modelId}`);
127
+ }
128
+ const name = patch.hostName;
129
+ if (name !== undefined && name !== null && cleanHostName(name) === "") {
130
+ throw new Error(`Invalid host name: ${JSON.stringify(name)}`);
131
+ }
132
+ const next = { ...await read() };
133
+ for (const key of HOST_SETTING_KEYS) {
134
+ const value = patch[key];
135
+ if (value === undefined)
136
+ continue;
137
+ if (value === null)
138
+ delete next[key];
139
+ else
140
+ next[key] = key === "hostName" ? cleanHostName(value) : value;
141
+ }
142
+ const tmp = `${options.path}.tmp`;
143
+ await mkdir(dirname(options.path), { recursive: true });
144
+ await writeFile(tmp, `${JSON.stringify(next, null, 2)}
145
+ `, { mode: 384 });
146
+ await rename(tmp, options.path);
147
+ file = next;
148
+ published.set(compute());
149
+ };
150
+ return {
151
+ get value() {
152
+ return published.value;
153
+ },
154
+ subscribe: (listener) => published.subscribe(listener),
155
+ update: (patch) => tail = tail.then(() => write(patch), () => write(patch))
156
+ };
157
+ }
158
+
159
+ export { constantValue, Published, sameList, settingsDefaults, openHostSettings };
@@ -1,6 +1,6 @@
1
1
  // apps/host/src/distribution.ts
2
2
  import { fileURLToPath } from "node:url";
3
- var packagedVersion = "0.5.0";
3
+ var packagedVersion = "0.6.0";
4
4
  var webRoot = fileURLToPath(new URL(packagedVersion === undefined ? "../../web/dist" : "../web/", import.meta.url));
5
5
  var hostEntry = packagedVersion === undefined ? { script: fileURLToPath(new URL("./main.ts", import.meta.url)), args: [] } : { script: fileURLToPath(new URL("./pinomad.js", import.meta.url)), args: ["host"] };
6
6
  var commandHint = (name) => packagedVersion === undefined ? `bun run ${name}` : `pinomad ${name}`;
@@ -1,15 +1,19 @@
1
+ import {
2
+ isParseArgsError
3
+ } from "./chunk-kzbzacz9.js";
1
4
  import {
2
5
  commandHint,
3
6
  hostEntry,
4
7
  packagedVersion
5
- } from "./chunk-1d9sppj0.js";
8
+ } from "./chunk-c8nxym2z.js";
6
9
  import {
10
+ ProtocolMismatchError,
7
11
  connectRemoteDurable
8
- } from "./chunk-m4q9wafw.js";
12
+ } from "./chunk-d93kvxvc.js";
9
13
  import {
10
14
  DEFAULT_DATA_DIR,
11
15
  hostAddress
12
- } from "./chunk-cjfmfyjb.js";
16
+ } from "./chunk-ngf1zatm.js";
13
17
 
14
18
  // apps/host/src/cli/service.ts
15
19
  import { execFile as execFile2 } from "node:child_process";
@@ -375,7 +379,7 @@ function serviceManager(platform = process.platform) {
375
379
  var run2 = promisify2(execFile2);
376
380
  var repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
377
381
  var message = (error) => error instanceof Error ? error.message : String(error);
378
- function connect(url, token, timeoutMs = 3000) {
382
+ function connectToHost(url, token, timeoutMs = 3000) {
379
383
  const connecting = connectRemoteDurable({ url, token, reconnectDelayMs: { min: 200, max: 2000 } });
380
384
  const timedOut = Symbol("timedOut");
381
385
  return Promise.race([
@@ -416,7 +420,7 @@ async function status(argv) {
416
420
  }
417
421
  try {
418
422
  const { url, token } = await hostAddress(argv);
419
- const remote = await connect(url, token);
423
+ const remote = await connectToHost(url, token);
420
424
  if (remote.view.current().tasks === undefined)
421
425
  await remote.controller.toggleTasks();
422
426
  const tasks = remote.view.current().tasks;
@@ -424,6 +428,8 @@ async function status(argv) {
424
428
  console.log(`host: reachable at ${url}, ${count} live task${count === 1 ? "" : "s"}`);
425
429
  remote.close();
426
430
  } catch (error) {
431
+ if (isParseArgsError(error))
432
+ throw error;
427
433
  console.log(`host: unreachable (${message(error)})`);
428
434
  }
429
435
  }
@@ -431,13 +437,18 @@ async function restartService({
431
437
  force,
432
438
  argv,
433
439
  manager = serviceManager(),
434
- restart
440
+ restart,
441
+ connect = connectToHost,
442
+ pollTimeoutMs = 30000,
443
+ pollDelayMs = 500
435
444
  }) {
436
445
  if (!(await manager.state()).installed) {
437
446
  console.log(`service not installed (${commandHint("service")} install)`);
438
447
  return 1;
439
448
  }
440
- const address = await hostAddress(argv).catch(() => {
449
+ const address = await hostAddress(argv).catch((error) => {
450
+ if (isParseArgsError(error))
451
+ throw error;
441
452
  return;
442
453
  });
443
454
  if (!force && address !== undefined) {
@@ -457,7 +468,7 @@ async function restartService({
457
468
  console.log("restart requested (host address unknown; not polling)");
458
469
  return 0;
459
470
  }
460
- const deadline = Date.now() + 30000;
471
+ const deadline = Date.now() + pollTimeoutMs;
461
472
  for (;; ) {
462
473
  try {
463
474
  const remote = await connect(address.url, address.token, 2000);
@@ -465,11 +476,16 @@ async function restartService({
465
476
  console.log(`host restarted, reachable at ${address.url}`);
466
477
  return 0;
467
478
  } catch (error) {
479
+ if (error instanceof ProtocolMismatchError && error.direction === "client-older") {
480
+ const newer = error.hostVersion ?? `protocol major ${error.hostMajor}`;
481
+ console.log(`host restarted (${newer}, newer than this upgrader), reachable at ${address.url}`);
482
+ return 0;
483
+ }
468
484
  if (Date.now() > deadline) {
469
- console.error(`host did not answer within 30s after restart: ${message(error)}`);
485
+ console.error(`host did not answer within ${pollTimeoutMs / 1000}s after restart: ${message(error)}`);
470
486
  return 1;
471
487
  }
472
- await new Promise((resolve3) => setTimeout(resolve3, 500));
488
+ await new Promise((resolve3) => setTimeout(resolve3, pollDelayMs));
473
489
  }
474
490
  }
475
491
  }
@@ -512,11 +528,12 @@ async function main(argv) {
512
528
  process.exitCode = 1;
513
529
  }
514
530
  } catch (error) {
531
+ if (isParseArgsError(error))
532
+ throw error;
515
533
  console.error(message(error));
516
534
  process.exitCode = 1;
517
535
  }
518
536
  }
519
- if (false)
520
- ;
537
+ if (false) {}
521
538
 
522
539
  export { installSpec, argValue, withoutRelayArgs, serviceManager, restartService, main };
@@ -11,7 +11,7 @@ import {
11
11
  hostSupportsCall,
12
12
  hostSupportsFeature,
13
13
  organize
14
- } from "./chunk-hsp12cjt.js";
14
+ } from "./chunk-n4tjhcb7.js";
15
15
 
16
16
  // packages/protocol/src/remote-durable.ts
17
17
  import { applyImmutable } from "@earendil-works/chord/delta";
@@ -535,6 +535,9 @@ class RemoteClient {
535
535
  if (patch.acceptAccountDevices !== undefined && !hostSupportsFeature(this.#hostMinor, "acceptAccountDevices")) {
536
536
  throw new Error("The host is too old for this (needs protocol 5.8); run `pinomad upgrade` on the host");
537
537
  }
538
+ if (patch.hostName !== undefined && !hostSupportsFeature(this.#hostMinor, "hostName")) {
539
+ throw new Error("The host is too old for this (needs protocol 5.12); run `pinomad upgrade` on the host");
540
+ }
538
541
  return this.#call("updateSettings", { patch });
539
542
  }),
540
543
  createAgent: (role) => this.#request(async () => {
@@ -604,4 +607,4 @@ class RemoteClient {
604
607
  }
605
608
  }
606
609
 
607
- export { connectRemoteDurable };
610
+ export { connectRemoteDurable, ProtocolMismatchError };
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ main,
4
+ restartService
5
+ } from "./chunk-d5ypmmr7.js";
6
+ import"./chunk-kzbzacz9.js";
7
+ import"./chunk-c8nxym2z.js";
8
+ import"./chunk-d93kvxvc.js";
9
+ import"./chunk-n4tjhcb7.js";
10
+ import"./chunk-znhn8af7.js";
11
+ import"./chunk-ngf1zatm.js";
12
+ import"./chunk-df32avw6.js";
13
+ export {
14
+ restartService,
15
+ main
16
+ };
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ restartService,
4
+ serviceManager
5
+ } from "./chunk-d5ypmmr7.js";
6
+ import"./chunk-kzbzacz9.js";
7
+ import {
8
+ globalPrefixOf,
9
+ packageDir,
10
+ packagedVersion
11
+ } from "./chunk-c8nxym2z.js";
12
+ import"./chunk-d93kvxvc.js";
13
+ import"./chunk-n4tjhcb7.js";
14
+ import"./chunk-znhn8af7.js";
15
+ import {
16
+ hostAddressOptions
17
+ } from "./chunk-ngf1zatm.js";
18
+ import"./chunk-df32avw6.js";
19
+
20
+ // apps/host/src/cli/upgrade.ts
21
+ import { execFile, spawn } from "node:child_process";
22
+ import { fileURLToPath } from "node:url";
23
+ import { parseArgs, promisify } from "node:util";
24
+ var exec = promisify(execFile);
25
+ var repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
26
+ var step = (command, args) => new Promise((resolve) => {
27
+ spawn(command, [...args], { cwd: repoRoot, stdio: "inherit" }).once("exit", (code) => resolve(code ?? 1));
28
+ });
29
+ var serviceInstalled = () => Promise.resolve().then(() => serviceManager().state()).then((state) => state.installed, () => false);
30
+ async function main(argv, deps = {}) {
31
+ const { values } = parseArgs({
32
+ args: [...argv],
33
+ options: { force: { type: "boolean", default: false }, ...hostAddressOptions }
34
+ });
35
+ const run = deps.run ?? ((command, args) => exec(command, args, { cwd: repoRoot }));
36
+ const runStep = deps.step ?? step;
37
+ const restart = deps.restart ?? restartService;
38
+ if (packagedVersion === undefined) {
39
+ const { stdout } = await run("git", ["status", "--porcelain"]);
40
+ if (stdout.trim() !== "") {
41
+ console.error("upgrade: the checkout has uncommitted changes; commit or stash them first");
42
+ process.exitCode = 1;
43
+ return;
44
+ }
45
+ for (const [command, args] of [
46
+ ["git", ["pull", "--ff-only"]],
47
+ ["bun", ["install"]],
48
+ ["bun", ["run", "build"]]
49
+ ]) {
50
+ const code = await runStep(command, args);
51
+ if (code !== 0) {
52
+ process.exitCode = code;
53
+ return;
54
+ }
55
+ }
56
+ } else {
57
+ const prefix = globalPrefixOf(packageDir);
58
+ if (prefix === undefined) {
59
+ console.error(`upgrade: pinomad isn't a global npm install (${packageDir}); upgrade it the way it was installed`);
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+ const code = await runStep("npm", ["install", "-g", "--prefix", prefix, "pinomad@latest"]);
64
+ if (code !== 0) {
65
+ process.exitCode = code;
66
+ return;
67
+ }
68
+ }
69
+ if (!await (deps.serviceInstalled ?? serviceInstalled)()) {
70
+ console.log("service not installed; upgrade done, nothing restarted");
71
+ process.exitCode = 0;
72
+ return;
73
+ }
74
+ (deps.exit ?? process.exit)(await restart({ force: values.force, argv: argv.filter((arg) => arg !== "--force") }));
75
+ }
76
+ if (false) {}
77
+ export {
78
+ main
79
+ };
@@ -3,43 +3,55 @@ import {
3
3
  assertSecureServerOrigin,
4
4
  probeServerAccounts,
5
5
  runServerOperation
6
- } from "./chunk-26d04z02.js";
6
+ } from "./chunk-ywmm63ks.js";
7
+ import {
8
+ constantValue,
9
+ openHostSettings
10
+ } from "./chunk-b1h374jr.js";
7
11
  import {
8
12
  loadHostKey,
9
13
  loadRelayKey,
10
14
  writeServerConfig
11
- } from "./chunk-ebfaf3k2.js";
15
+ } from "./chunk-0xh6cy9v.js";
12
16
  import {
13
17
  relayHostId
14
- } from "./chunk-kmbh44dt.js";
18
+ } from "./chunk-7qb9tq54.js";
15
19
  import {
16
20
  argValue,
17
21
  installSpec,
18
22
  restartService,
19
23
  serviceManager,
20
24
  withoutRelayArgs
21
- } from "./chunk-aqj4wg33.js";
25
+ } from "./chunk-d5ypmmr7.js";
26
+ import"./chunk-kzbzacz9.js";
22
27
  import {
23
28
  commandHint,
24
29
  hostEntry
25
- } from "./chunk-1d9sppj0.js";
26
- import"./chunk-m4q9wafw.js";
27
- import"./chunk-hsp12cjt.js";
30
+ } from "./chunk-c8nxym2z.js";
31
+ import"./chunk-d93kvxvc.js";
32
+ import"./chunk-n4tjhcb7.js";
33
+ import {
34
+ cleanHostName,
35
+ normalizeHostName
36
+ } from "./chunk-znhn8af7.js";
28
37
  import {
29
38
  DEFAULT_DATA_DIR
30
- } from "./chunk-cjfmfyjb.js";
39
+ } from "./chunk-ngf1zatm.js";
31
40
  import"./chunk-df32avw6.js";
32
41
 
33
42
  // apps/host/src/cli/join.ts
34
43
  import { mkdir } from "node:fs/promises";
35
44
  import { homedir, hostname } from "node:os";
36
- import { resolve } from "node:path";
45
+ import { join, resolve } from "node:path";
37
46
  import { parseArgs } from "node:util";
38
47
  var message = (error) => error instanceof Error ? error.message : String(error);
39
48
  async function joinServer(args, deps) {
40
49
  const origin = args.origin.replace(/\/+$/, "");
41
50
  const dataDir = resolve(args.dataDir);
42
51
  assertSecureServerOrigin(origin);
52
+ if (args.name !== undefined && cleanHostName(args.name) === "") {
53
+ throw new Error(`Invalid host name: ${JSON.stringify(args.name)}`);
54
+ }
43
55
  await mkdir(dataDir, { recursive: true });
44
56
  const signingKey = await loadRelayKey(dataDir);
45
57
  const hostKey = await loadHostKey(dataDir);
@@ -56,13 +68,25 @@ async function joinServer(args, deps) {
56
68
  if (probe === "disabled") {
57
69
  throw new Error(`${origin} does not run server accounts — the server is too old or was started without a data dir`);
58
70
  }
71
+ const settings = await openHostSettings({
72
+ path: join(dataDir, "settings.json"),
73
+ fallback: {
74
+ defaultModel: () => null,
75
+ defaultThinkingLevel: () => null,
76
+ steeringMode: () => "one-at-a-time",
77
+ followUpMode: () => "one-at-a-time"
78
+ },
79
+ models: constantValue([])
80
+ });
81
+ const current = settings.value;
82
+ const storedName = current.custom.includes("hostName") ? current.hostName : undefined;
59
83
  const reply = await runServerOperation({
60
84
  origin,
61
85
  signingKey,
62
86
  noisePublicKey: hostKey.publicKey,
63
87
  op: "join",
64
88
  joinToken: args.token,
65
- name: args.name ?? deps?.hostname ?? hostname()
89
+ name: normalizeHostName(args.name ?? storedName ?? deps?.hostname ?? hostname())
66
90
  });
67
91
  if (reply.t === "refused") {
68
92
  throw new Error(reply.reason === "expired_token" ? "the join token expired — generate a fresh one" : reply.reason === "invalid_token" ? "the join token is invalid or already spent — generate a fresh one" : `the server refused the join: ${reply.reason}`);
@@ -73,6 +97,8 @@ async function joinServer(args, deps) {
73
97
  let service;
74
98
  try {
75
99
  await writeServerConfig(dataDir, { origin });
100
+ if (args.name !== undefined)
101
+ await settings.update({ hostName: args.name });
76
102
  const freshSpec = installSpec(["--data-dir", dataDir], {
77
103
  execPath: process.execPath,
78
104
  env: process.env,
@@ -0,0 +1,12 @@
1
+ // apps/host/src/cli/args.ts
2
+ function isParseArgsError(error) {
3
+ const code = error.code;
4
+ return typeof code === "string" && code.startsWith("ERR_PARSE_ARGS_");
5
+ }
6
+ function reportParseArgsError(command, error) {
7
+ console.error(`${command}: ${error instanceof Error ? error.message : String(error)}`);
8
+ console.error(`run "${command} --help" for usage`);
9
+ process.exitCode = 2;
10
+ }
11
+
12
+ export { isParseArgsError, reportParseArgsError };
@@ -1,16 +1,8 @@
1
- // packages/protocol/src/settings.ts
2
- var HOST_SETTING_KEY_SET = {
3
- defaultModel: true,
4
- defaultThinkingLevel: true,
5
- steeringMode: true,
6
- followUpMode: true,
7
- acceptAccountDevices: true
8
- };
9
- var HOST_SETTING_KEYS = Object.keys(HOST_SETTING_KEY_SET);
10
- var THINKING_LEVEL_KEYS = { off: true, minimal: true, low: true, medium: true, high: true, xhigh: true, max: true };
11
- var THINKING_LEVELS = Object.keys(THINKING_LEVEL_KEYS);
12
- var isThinkingLevel = (value) => typeof value === "string" && Object.prototype.hasOwnProperty.call(THINKING_LEVEL_KEYS, value);
13
- var isQueueMode = (value) => value === "all" || value === "one-at-a-time";
1
+ import {
2
+ HOST_SETTING_KEYS,
3
+ isQueueMode,
4
+ isThinkingLevel
5
+ } from "./chunk-znhn8af7.js";
14
6
 
15
7
  // packages/protocol/src/frames.ts
16
8
  var HOST_STREAMS = { conversations: 0, index: 0, devices: 0, mcp: 0, models: 1, auth: 1, settings: 2, agents: 6 };
@@ -22,7 +14,7 @@ var conversationStream = (id) => `conversation:${id}`;
22
14
  var docStream = (kind, id) => `doc:${kind}:${id}`;
23
15
  var UNAUTHORIZED_CLOSE_CODE = 4401;
24
16
  var INVALID_FRAME_CLOSE_CODE = 4400;
25
- var PROTOCOL = { major: 5, minor: 10 };
17
+ var PROTOCOL = { major: 5, minor: 12 };
26
18
  var CALL_SINCE = {
27
19
  logout: 1,
28
20
  updateSettings: 2,
@@ -37,7 +29,14 @@ var CALL_SINCE = {
37
29
  function hostSupportsCall(hostMinor, method) {
38
30
  return hostMinor >= (CALL_SINCE[method] ?? 0);
39
31
  }
40
- var FEATURE_SINCE = { images: 5, acceptAccountDevices: 8, apnsPush: 9, agentConversations: 10 };
32
+ var FEATURE_SINCE = {
33
+ images: 5,
34
+ acceptAccountDevices: 8,
35
+ apnsPush: 9,
36
+ agentConversations: 10,
37
+ liveBranch: 11,
38
+ hostName: 12
39
+ };
41
40
  function hostSupportsFeature(hostMinor, feature) {
42
41
  return hostMinor >= FEATURE_SINCE[feature];
43
42
  }
@@ -101,7 +100,8 @@ var SETTING_VALIDATORS = {
101
100
  defaultThinkingLevel: (value) => value === null || isThinkingLevel(value),
102
101
  steeringMode: (value) => value === null || isQueueMode(value),
103
102
  followUpMode: (value) => value === null || isQueueMode(value),
104
- acceptAccountDevices: (value) => value === null || typeof value === "boolean"
103
+ acceptAccountDevices: (value) => value === null || typeof value === "boolean",
104
+ hostName: (value) => value === null || typeof value === "string"
105
105
  };
106
106
  function validCallArgs(method, args) {
107
107
  const conversationId = () => typeof args.conversationId === "number" && Number.isSafeInteger(args.conversationId) && args.conversationId >= 1;
@@ -254,4 +254,4 @@ function homeOf(index, summaries, id) {
254
254
  return;
255
255
  }
256
256
 
257
- export { HOST_SETTING_KEYS, THINKING_LEVELS, isThinkingLevel, isQueueMode, HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame, organize, checkoutOf, homeOf };
257
+ export { HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame, organize, checkoutOf, homeOf };
@@ -4,18 +4,19 @@ import { homedir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
5
  import { parseArgs } from "node:util";
6
6
  var DEFAULT_DATA_DIR = process.env.PINOMAD_DATA_DIR ?? join(homedir(), ".pinomad");
7
+ var hostAddressOptions = {
8
+ "data-dir": { type: "string" },
9
+ url: { type: "string", default: "ws://127.0.0.1:7420" },
10
+ token: { type: "string" }
11
+ };
7
12
  async function hostAddress(argv) {
8
13
  const { values } = parseArgs({
9
14
  args: [...argv],
10
- options: {
11
- "data-dir": { type: "string" },
12
- url: { type: "string", default: "ws://127.0.0.1:7420" },
13
- token: { type: "string" }
14
- }
15
+ options: hostAddressOptions
15
16
  });
16
17
  const dataDir = resolve(values["data-dir"] ?? DEFAULT_DATA_DIR);
17
18
  const token = values.token ?? (await readFile(join(dataDir, "token"), "utf8")).trim();
18
19
  return { url: values.url, token, dataDir };
19
20
  }
20
21
 
21
- export { DEFAULT_DATA_DIR, hostAddress };
22
+ export { DEFAULT_DATA_DIR, hostAddressOptions, hostAddress };
@@ -9,11 +9,14 @@ import {
9
9
  relayWsBase,
10
10
  verifyRelayChallenge,
11
11
  verifyRelayChallengeV2
12
- } from "./chunk-kmbh44dt.js";
12
+ } from "./chunk-7qb9tq54.js";
13
13
  import {
14
14
  commandHint,
15
15
  webRoot
16
- } from "./chunk-1d9sppj0.js";
16
+ } from "./chunk-c8nxym2z.js";
17
+ import {
18
+ normalizeHostName
19
+ } from "./chunk-znhn8af7.js";
17
20
  import"./chunk-df32avw6.js";
18
21
 
19
22
  // apps/server/src/main.ts
@@ -325,7 +328,7 @@ async function openAccounts(options) {
325
328
  db.exec("ROLLBACK");
326
329
  return { status: "refused", reason: failure };
327
330
  }
328
- const hostName = name === undefined || name.trim() === "" ? "Unknown host" : name.trim().slice(0, 64);
331
+ const hostName = name === undefined ? "Unknown host" : normalizeHostName(name);
329
332
  db.prepare(`INSERT INTO hosts (host_id, user_id, noise_public_key, name, joined_at, last_seen_at)
330
333
  VALUES (?, ?, ?, ?, ?, ?)
331
334
  ON CONFLICT(host_id) DO UPDATE SET
@@ -358,6 +361,9 @@ async function openAccounts(options) {
358
361
  markHostSeen(hostId) {
359
362
  db.prepare("UPDATE hosts SET last_seen_at = ? WHERE host_id = ?").run(now(), hostId);
360
363
  },
364
+ renameHost(hostId, name) {
365
+ db.prepare("UPDATE hosts SET name = ? WHERE host_id = ?").run(normalizeHostName(name), hostId);
366
+ },
361
367
  async close() {
362
368
  db.close();
363
369
  await release();
@@ -1041,6 +1047,11 @@ async function startRelay(options) {
1041
1047
  handlePush(socket, hostId, answer);
1042
1048
  return;
1043
1049
  }
1050
+ if (answer.t === "name") {
1051
+ if (controls.get(hostId)?.socket === socket)
1052
+ accounts?.renameHost(hostId, answer.name);
1053
+ return;
1054
+ }
1044
1055
  if (answer.t === "pairing" || answer.t === "pairing-refused") {
1045
1056
  const request = pairingRequests.get(answer.requestId);
1046
1057
  if (request === undefined || request.control !== socket)
@@ -1097,7 +1108,9 @@ async function startRelay(options) {
1097
1108
  ...frame.features === undefined ? {} : { features: frame.features }
1098
1109
  });
1099
1110
  accounts?.markHostSeen(hostId);
1100
- socket.send(encodeRelayFrame({ t: "registered", push: pushEnvironments }));
1111
+ if (frame.name !== undefined)
1112
+ accounts?.renameHost(hostId, frame.name);
1113
+ socket.send(encodeRelayFrame({ t: "registered", push: pushEnvironments, features: ["name"] }));
1101
1114
  });
1102
1115
  socket.once("close", () => {
1103
1116
  clearTimeout(deadline);