pinomad 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,21 +16,23 @@ A self-hosted coding agent built on [Pi Durable](https://www.npmjs.com/package/@
16
16
 
17
17
  ## Quick start
18
18
 
19
- The host needs Node 25+ on macOS or Linux.
19
+ 1. **Create an account** at [app.pinomad.si](https://app.pinomad.si/) (sign-up is by invite).
20
+ 2. **Add your host.** On the machine that should run the agent (macOS or Linux), run the two lines below. `pinomad up` prints a code; enter it on the page it opens, signed in, to add the host to your account. It then runs as a service.
20
21
 
21
- ```sh
22
- npm i -g pinomad
23
- pinomad host # data and credentials live in ~/.pinomad
24
- pinomad link # in another terminal: prints a browser link to the web client
25
- ```
22
+ ```sh
23
+ curl -fsSL https://pinomad.si/install.sh | sh
24
+ pinomad up
25
+ ```
26
26
 
27
- Sign in to a model provider under Settings → Providers: a subscription you already pay for (Claude, ChatGPT, Copilot, …), an API key, or your own OpenAI- or Anthropic-compatible endpoint. To click around without one, `pinomad host --faux "hi"` runs a scripted fake model.
27
+ Already installed with npm? The script leaves that install alone; run `pinomad upgrade`, then `pinomad up`.
28
28
 
29
- To reach the host from your Mac and iPhone anywhere without running a server, [use the official server](https://pinomad.si/docs/official-server/) at app.pinomad.si.
29
+ 3. **Sign in on your other devices** (web, Mac app, iPhone). They pair with your hosts on their own; then connect a model provider under Settings → Providers.
30
+
31
+ To run without an account (one machine, LAN or tailnet, or your own server), see [Without an account](https://pinomad.si/docs/without-account/).
30
32
 
31
33
  ## Documentation
32
34
 
33
- User docs live on **[pinomad.si/docs](https://pinomad.si/docs/)**: [getting started](https://pinomad.si/docs/) (connecting your other devices, keeping the host running, clients) and [what the agent can do](https://pinomad.si/docs/features/). Usage, remote access, and the service reference are in Chinese for now: [中文文档](https://pinomad.si/zh/docs/).
35
+ User docs live on **[pinomad.si/docs](https://pinomad.si/docs/)**: [getting started](https://pinomad.si/docs/) (account, adding hosts, devices, push), [your account](https://pinomad.si/docs/official-server/), [without an account](https://pinomad.si/docs/without-account/), and [what the agent can do](https://pinomad.si/docs/features/). Usage, remote access, and the service reference are in Chinese for now: [中文文档](https://pinomad.si/zh/docs/).
34
36
 
35
37
  For contributors (in Chinese):
36
38
 
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  RTC_DEFAULT_STUN
3
- } from "./chunk-jgd38rna.js";
3
+ } from "./chunk-xqycs43g.js";
4
4
  import {
5
5
  HOST_SETTING_KEYS,
6
6
  cleanHostName,
7
7
  isQueueMode,
8
8
  isThinkingLevel,
9
9
  normalizeHostName
10
- } from "./chunk-sygeb9qb.js";
10
+ } from "./chunk-pqe7q1s7.js";
11
11
 
12
12
  // apps/host/src/host-value.ts
13
13
  function constantValue(value) {
@@ -3,30 +3,32 @@ import {
3
3
  assertSecureServerOrigin,
4
4
  probeServerAccounts,
5
5
  runServerOperation
6
- } from "./chunk-3p1eysm9.js";
6
+ } from "./chunk-ncbqa8jk.js";
7
7
  import {
8
8
  loadHostKey,
9
- loadRelayKey,
9
+ loadRelayKey
10
+ } from "./chunk-t91zt5f0.js";
11
+ import {
10
12
  readServerConfig,
11
13
  removeServerConfig
12
- } from "./chunk-5gp9svzs.js";
14
+ } from "./chunk-etf7ef97.js";
13
15
  import {
14
16
  relayHostId
15
- } from "./chunk-khcpv54k.js";
17
+ } from "./chunk-y127fe1a.js";
16
18
  import {
17
19
  argValue,
18
20
  restartService,
19
21
  serviceManager,
20
22
  withoutRelayArgs
21
- } from "./chunk-32g3m2h0.js";
23
+ } from "./chunk-vvj9b1gc.js";
24
+ import"./chunk-jt34g8st.js";
22
25
  import"./chunk-kzbzacz9.js";
23
- import"./chunk-fqgvf8v0.js";
24
- import"./chunk-4sepcfpy.js";
25
- import"./chunk-jgd38rna.js";
26
- import"./chunk-sygeb9qb.js";
26
+ import"./chunk-xqycs43g.js";
27
+ import"./chunk-pqe7q1s7.js";
27
28
  import {
28
29
  DEFAULT_DATA_DIR
29
30
  } from "./chunk-ngf1zatm.js";
31
+ import"./chunk-sd5fkxjj.js";
30
32
  import"./chunk-df32avw6.js";
31
33
 
32
34
  // apps/host/src/cli/leave.ts
@@ -1,14 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- serveWebClient
4
- } from "./chunk-wb10efyq.js";
5
2
  import {
6
3
  Published,
7
4
  constantValue,
8
5
  openHostSettings,
9
6
  sameList,
10
7
  settingsDefaults
11
- } from "./chunk-v414b5pp.js";
8
+ } from "./chunk-44pnts98.js";
12
9
  import {
13
10
  DevicesDoc,
14
11
  RevocationsDoc,
@@ -16,21 +13,22 @@ import {
16
13
  ensureDevices,
17
14
  loadHostKey,
18
15
  loadRelayKey,
16
+ pairingOffers
17
+ } from "./chunk-t91zt5f0.js";
18
+ import {
19
19
  normalizeOrigin,
20
- pairingOffers,
21
20
  readServerBinding,
22
21
  readServerConfig,
23
22
  resolveRelayOrigin
24
- } from "./chunk-5gp9svzs.js";
23
+ } from "./chunk-etf7ef97.js";
24
+ import {
25
+ serveWebClient
26
+ } from "./chunk-wb10efyq.js";
25
27
  import {
26
28
  relayHostId,
27
29
  relayWsBase,
28
30
  startRelayLink
29
- } from "./chunk-khcpv54k.js";
30
- import {
31
- packagedVersion,
32
- webRoot
33
- } from "./chunk-fqgvf8v0.js";
31
+ } from "./chunk-y127fe1a.js";
34
32
  import {
35
33
  ATTACHMENT_TTL_MS,
36
34
  INVALID_FRAME_CLOSE_CODE,
@@ -56,7 +54,7 @@ import {
56
54
  rtcCandidateType,
57
55
  sdpCandidates,
58
56
  withRtcCandidates
59
- } from "./chunk-jgd38rna.js";
57
+ } from "./chunk-xqycs43g.js";
60
58
  import {
61
59
  ENDPOINT_SET_SCHEMA_VERSION,
62
60
  MAX_HOST_ENDPOINT_URL_LENGTH,
@@ -72,10 +70,14 @@ import {
72
70
  parseEndpointSet,
73
71
  respondIK,
74
72
  toBase64Url
75
- } from "./chunk-sygeb9qb.js";
73
+ } from "./chunk-pqe7q1s7.js";
76
74
  import {
77
75
  DEFAULT_DATA_DIR
78
76
  } from "./chunk-ngf1zatm.js";
77
+ import {
78
+ packagedVersion,
79
+ webRoot
80
+ } from "./chunk-sd5fkxjj.js";
79
81
  import {
80
82
  __require
81
83
  } from "./chunk-df32avw6.js";
@@ -3355,6 +3357,14 @@ async function startGateway(options) {
3355
3357
  };
3356
3358
  let remote;
3357
3359
  let relay;
3360
+ const closeDevice = (publicKey) => {
3361
+ for (const connection of [...connections]) {
3362
+ if (connection.devicePublicKey === publicKey) {
3363
+ connection.closeConnection(UNAUTHORIZED_CLOSE_CODE, "device revoked");
3364
+ }
3365
+ }
3366
+ rtc?.closeDevice(publicKey);
3367
+ };
3358
3368
  const currentAdvertisement = () => {
3359
3369
  const advertiseUrl = remote?.advertiseUrl;
3360
3370
  const endpoints = remoteEndpointCandidates({
@@ -3381,14 +3391,7 @@ async function startGateway(options) {
3381
3391
  rtc?.closeOwner(self);
3382
3392
  self.dispose();
3383
3393
  },
3384
- closeDevice: (publicKey) => {
3385
- for (const connection2 of [...connections]) {
3386
- if (connection2.devicePublicKey === publicKey) {
3387
- connection2.closeConnection(UNAUTHORIZED_CLOSE_CODE, "device revoked");
3388
- }
3389
- }
3390
- rtc?.closeDevice(publicKey);
3391
- },
3394
+ closeDevice,
3392
3395
  rtcOffer: (owner, key, attempt, sdp) => {
3393
3396
  if (rtc === undefined)
3394
3397
  throw new Error("WebRTC is not available on this host");
@@ -3462,7 +3465,8 @@ async function startGateway(options) {
3462
3465
  acceptAccountDevices: () => options.settings.value.acceptAccountDevices,
3463
3466
  serverBinding: async () => await readServerBinding(options.dataDir).catch(() => {
3464
3467
  return;
3465
- })
3468
+ }),
3469
+ accountAdmitted: (requestId, publicKey) => relay?.reportAdmitted(requestId, publicKey)
3466
3470
  };
3467
3471
  const secure = options.secure;
3468
3472
  const handshakeHandlers = {
@@ -3515,6 +3519,16 @@ async function startGateway(options) {
3515
3519
  });
3516
3520
  if (secure?.relay !== undefined) {
3517
3521
  const relayOrigin = secure.relay.origin;
3522
+ const currentBinding = async () => {
3523
+ const config = await readServerConfig(options.dataDir).catch(() => {
3524
+ return;
3525
+ });
3526
+ if (config?.binding === undefined)
3527
+ return { refused: "not_joined" };
3528
+ if (normalizeOrigin(config.origin) !== normalizeOrigin(relayOrigin))
3529
+ return { refused: "rebinding" };
3530
+ return { binding: config.binding };
3531
+ };
3518
3532
  let link;
3519
3533
  link = startRelayLink({
3520
3534
  origin: relayOrigin,
@@ -3527,17 +3541,12 @@ async function startGateway(options) {
3527
3541
  },
3528
3542
  subscribe: (listener) => options.settings.subscribe((settings) => listener(settings.hostName))
3529
3543
  },
3530
- accountOffer: async () => {
3544
+ accountOffer: async (requestId) => {
3531
3545
  if (!options.settings.value.acceptAccountDevices)
3532
3546
  return { ok: false, reason: "disabled" };
3533
- const config = await readServerConfig(options.dataDir).catch(() => {
3534
- return;
3535
- });
3536
- if (config?.binding === undefined)
3537
- return { ok: false, reason: "not_joined" };
3538
- if (normalizeOrigin(config.origin) !== normalizeOrigin(relayOrigin)) {
3539
- return { ok: false, reason: "rebinding" };
3540
- }
3547
+ const bound = await currentBinding();
3548
+ if ("refused" in bound)
3549
+ return { ok: false, reason: bound.refused };
3541
3550
  let endpointSet;
3542
3551
  try {
3543
3552
  const current = checkedEndpointSet(currentAdvertisement().endpoints);
@@ -3545,9 +3554,18 @@ async function startGateway(options) {
3545
3554
  } catch {
3546
3555
  return { ok: false, reason: "invalid_endpoint" };
3547
3556
  }
3548
- const { secret, expiresAt } = secure.offers.createAccount(config.binding);
3557
+ const { secret, expiresAt } = secure.offers.createAccount(bound.binding, requestId);
3549
3558
  return { ok: true, secret, expiresAt, ...endpointSet === undefined ? {} : { endpointSet } };
3550
3559
  },
3560
+ deviceReports: {
3561
+ revoke: async (publicKey) => {
3562
+ const bound = await currentBinding();
3563
+ if ("refused" in bound)
3564
+ throw new Error(`revocation refused: ${bound.refused}`);
3565
+ await revokeDevice(options.harness, publicKey, context3);
3566
+ closeDevice(publicKey);
3567
+ }
3568
+ },
3551
3569
  onStateChange: (state) => {
3552
3570
  if (state.status === "registered") {
3553
3571
  const push = state.push === undefined ? "undeclared" : state.push.length === 0 ? "none" : state.push.join(",");
@@ -3817,6 +3835,9 @@ function secureHandshake(carrier, secure, policy, handlers) {
3817
3835
  carrier.close(UNAUTHORIZED_CLOSE_CODE, "unauthorized");
3818
3836
  return;
3819
3837
  }
3838
+ if (decision.outcome === "admitted" && decision.requestId !== undefined) {
3839
+ policy.accountAdmitted(decision.requestId, deviceKey);
3840
+ }
3820
3841
  const { message, result: result2 } = responder.writeMessage2(encodeHostHello({ v: 1 }));
3821
3842
  return {
3822
3843
  result: result2,
@@ -2,30 +2,38 @@
2
2
  import {
3
3
  restartService,
4
4
  serviceManager
5
- } from "./chunk-32g3m2h0.js";
5
+ } from "./chunk-vvj9b1gc.js";
6
+ import"./chunk-jt34g8st.js";
6
7
  import"./chunk-kzbzacz9.js";
8
+ import"./chunk-xqycs43g.js";
9
+ import"./chunk-pqe7q1s7.js";
10
+ import {
11
+ hostAddressOptions
12
+ } from "./chunk-ngf1zatm.js";
7
13
  import {
8
14
  globalPrefixOf,
9
15
  packageDir,
10
16
  packagedVersion
11
- } from "./chunk-fqgvf8v0.js";
12
- import"./chunk-4sepcfpy.js";
13
- import"./chunk-jgd38rna.js";
14
- import"./chunk-sygeb9qb.js";
15
- import {
16
- hostAddressOptions
17
- } from "./chunk-ngf1zatm.js";
17
+ } from "./chunk-sd5fkxjj.js";
18
18
  import"./chunk-df32avw6.js";
19
19
 
20
20
  // apps/host/src/cli/upgrade.ts
21
21
  import { execFile, spawn } from "node:child_process";
22
+ import { existsSync } from "node:fs";
23
+ import { dirname, join } from "node:path";
22
24
  import { fileURLToPath } from "node:url";
23
25
  import { parseArgs, promisify } from "node:util";
24
26
  var exec = promisify(execFile);
25
27
  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
+ var step = (command, args, binDir) => new Promise((resolve) => {
29
+ const env = binDir === undefined ? process.env : { ...process.env, PATH: `${binDir}:${process.env.PATH ?? ""}` };
30
+ spawn(command, [...args], { cwd: repoRoot, env, stdio: "inherit" }).once("exit", (code) => resolve(code ?? 1));
28
31
  });
32
+ function npmBeside(execPath) {
33
+ const binDir = dirname(execPath);
34
+ const cli = join(binDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
35
+ return existsSync(cli) ? { command: execPath, args: [cli], binDir } : { command: "npm", args: [] };
36
+ }
29
37
  var serviceInstalled = () => Promise.resolve().then(() => serviceManager().state()).then((state) => state.installed, () => false);
30
38
  async function main(argv, deps = {}) {
31
39
  const { values } = parseArgs({
@@ -60,7 +68,8 @@ async function main(argv, deps = {}) {
60
68
  process.exitCode = 1;
61
69
  return;
62
70
  }
63
- const code = await runStep("npm", ["install", "-g", "--prefix", prefix, "pinomad@latest"]);
71
+ const npm = npmBeside(process.execPath);
72
+ const code = await runStep(npm.command, [...npm.args, "install", "-g", "--prefix", prefix, "pinomad@latest"], npm.binDir);
64
73
  if (code !== 0) {
65
74
  process.exitCode = code;
66
75
  return;
@@ -75,5 +84,6 @@ async function main(argv, deps = {}) {
75
84
  }
76
85
  if (false) {}
77
86
  export {
87
+ npmBeside,
78
88
  main
79
89
  };