pinomad 0.6.0 → 0.8.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.
Files changed (30) hide show
  1. package/README.md +6 -59
  2. package/dist/{chunk-wqvp1fwh.js → chunk-1r8a2434.js} +38 -15
  3. package/dist/{chunk-ywmm63ks.js → chunk-3p1eysm9.js} +4 -2
  4. package/dist/{chunk-d5ypmmr7.js → chunk-73vxwjs7.js} +2 -2
  5. package/dist/chunk-a4b61tx5.js +2984 -0
  6. package/dist/{chunk-k2s1hfq5.js → chunk-cytnycs9.js} +9 -9
  7. package/dist/{chunk-3dnnaz4j.js → chunk-ddh6wfv2.js} +8 -8
  8. package/dist/{chunk-jn7y815x.js → chunk-fes7wjw1.js} +5 -5
  9. package/dist/chunk-jgd38rna.js +622 -0
  10. package/dist/{chunk-4btj08we.js → chunk-jjtr7fr5.js} +5608 -2777
  11. package/dist/{chunk-7qb9tq54.js → chunk-khcpv54k.js} +64 -310
  12. package/dist/{chunk-7s1f4z4g.js → chunk-mv94ksvb.js} +22 -5
  13. package/dist/{chunk-367n9je8.js → chunk-p15hqqjz.js} +1 -1
  14. package/dist/{chunk-c8nxym2z.js → chunk-p56xf1r8.js} +1 -1
  15. package/dist/{chunk-ggb1taqy.js → chunk-ptjyk94f.js} +5 -5
  16. package/dist/{chunk-0xh6cy9v.js → chunk-r2ssa2ex.js} +1 -1
  17. package/dist/chunk-sygeb9qb.js +430 -0
  18. package/dist/{chunk-b1h374jr.js → chunk-v414b5pp.js} +8 -2
  19. package/dist/pinomad.js +10 -10
  20. package/package.json +2 -1
  21. package/web/assets/index-5DfEA9Zb.js +398 -0
  22. package/web/assets/index-D6qIPhPy.css +1 -0
  23. package/web/index.html +2 -2
  24. package/dist/chunk-d93kvxvc.js +0 -610
  25. package/dist/chunk-n4tjhcb7.js +0 -257
  26. package/dist/chunk-znhn8af7.js +0 -33
  27. package/web/assets/index-C_aU_ZiV.js +0 -383
  28. package/web/assets/index-DaPcXZPM.css +0 -1
  29. package/web/assets/montserrat-latin-ext-wght-normal-BsZE-iaG.woff2 +0 -0
  30. package/web/assets/montserrat-latin-wght-normal-l_AIctKy.woff2 +0 -0
package/README.md CHANGED
@@ -20,74 +20,21 @@ The host needs Node 25+ on macOS or Linux.
20
20
 
21
21
  ```sh
22
22
  npm i -g pinomad
23
- pinomad host # uses the models and credentials you set up in pi (~/.pi/agent)
23
+ pinomad host # data and credentials live in ~/.pinomad
24
24
  pinomad link # in another terminal: prints a browser link to the web client
25
25
  ```
26
26
 
27
- Haven't set up pi yet? `pinomad host --faux "hi"` runs a scripted fake model so you can click around.
28
-
29
- ## Connect your other devices
30
-
31
- Open a port for paired devices, then pair each device with its own QR code:
32
-
33
- ```sh
34
- pinomad host --remote-port 7422
35
- pinomad pair # one-time QR code and link, expires after 5 minutes
36
- ```
37
-
38
- Scan the code with the iPhone app, or paste the link into the Mac app. The remote port accepts paired devices only; revoke them under Devices in the sidebar.
39
-
40
- The pairing link uses the LAN address the host detects. On a tailnet or behind a tunnel, add `--public-url http://<tailnet-ip>:7422` so the link points there. To reach a host at home from anywhere, run the relay on a VPS: see [Remote access](docs/remote-access.md).
41
-
42
- ## Keep it running
43
-
44
- Install the host as a user service (LaunchAgent on macOS, systemd user unit on Linux) so it starts at boot and restarts on crash:
45
-
46
- ```sh
47
- pinomad service install --remote-port 7422
48
- pinomad service status
49
- pinomad upgrade # install pinomad@latest, wait for running tasks, restart
50
- ```
51
-
52
- Service flags, logs, and platform caveats: [Running as a service](docs/service.md).
53
-
54
- ## Clients
55
-
56
- | Client | How to get it |
57
- |---|---|
58
- | Web | Served by the host itself (`pinomad link`) and by the relay; any modern browser |
59
- | macOS (Apple silicon) | Signed DMG from [GitHub Releases](https://github.com/bubbleptr/pinomad/releases); updates itself |
60
- | iOS | Internal TestFlight only for now |
61
- | Linux, Windows | Use the web client |
62
-
63
- ## What the agent can do
64
-
65
- The agent has pi's four tools (read, bash, edit, write) plus:
66
-
67
- | Feature | What it does |
68
- |---|---|
69
- | Projects and worktrees | Register a directory as a project. In a Git repo each conversation works in its own worktree and branch, so your checkout stays untouched |
70
- | Forks | Branch off from a message and keep both lines going |
71
- | Subagents | Hand self-contained tasks to subagents that run in parallel |
72
- | Named agents | Reusable roles in `<data dir>/agents/<name>.md` — a `subagent` call or an `@name` mention delegates to one, optionally pinning its model and thinking level |
73
- | Questions, not approvals | Tools run without approval prompts; when the direction is unclear, the agent asks 1–4 multiple-choice questions |
74
- | codemode | The model writes JavaScript that calls other tools in a QuickJS sandbox; only the script's output enters the context |
75
- | MCP | Servers configured in `~/.agents/mcp.json`, in the usual `mcpServers` format |
76
- | Images | Paste, drop, or pick up to 4 images per message in the Web/desktop composer — resized to JPEG before sending |
77
- | AGENTS.md and skills | Reads `AGENTS.md` / `CLAUDE.md` and skills from `~/.agents` and the project, on every request |
78
-
79
- Details and the MCP config reference: [Usage](docs/usage.md).
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.
80
28
 
81
29
  ## Documentation
82
30
 
83
- These docs are written in Chinese.
31
+ 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/).
32
+
33
+ For contributors (in Chinese):
84
34
 
85
35
  | Doc | Covers |
86
36
  |---|---|
87
- | [Usage](docs/usage.md) | Projects, worktrees, tools, MCP configuration |
88
- | [Remote access](docs/remote-access.md) | Pairing, tunnels, self-hosted relay (VPS or container platforms) |
89
- | [Running as a service](docs/service.md) | `service` and `upgrade` commands, platform notes |
90
- | [Website](docs/website.md) | The pinomad.si landing page and the hosted app.pinomad.si instance |
37
+ | [Website](docs/website.md) | The pinomad.si site and the hosted app.pinomad.si instance |
91
38
  | [Architecture overview](docs/architecture.md) | How the parts and languages fit together, with send/receive diagrams |
92
39
  | [Architecture decisions](docs/adr/README.md) | Why things are the way they are |
93
40
  | [Roadmap](docs/roadmap.md) | Direction and order; items live in GitHub Issues |
@@ -9,14 +9,14 @@ import {
9
9
  relayWsBase,
10
10
  verifyRelayChallenge,
11
11
  verifyRelayChallengeV2
12
- } from "./chunk-7qb9tq54.js";
12
+ } from "./chunk-khcpv54k.js";
13
13
  import {
14
14
  commandHint,
15
15
  webRoot
16
- } from "./chunk-c8nxym2z.js";
16
+ } from "./chunk-p56xf1r8.js";
17
17
  import {
18
18
  normalizeHostName
19
- } from "./chunk-znhn8af7.js";
19
+ } from "./chunk-sygeb9qb.js";
20
20
  import"./chunk-df32avw6.js";
21
21
 
22
22
  // apps/server/src/main.ts
@@ -402,7 +402,7 @@ async function changePassword(options) {
402
402
 
403
403
  // apps/server/src/apns.ts
404
404
  import { createPrivateKey, sign } from "node:crypto";
405
- import { connect } from "node:http2";
405
+ import { connect, constants } from "node:http2";
406
406
  var AUTHORITIES = {
407
407
  production: "https://api.push.apple.com",
408
408
  development: "https://api.sandbox.push.apple.com"
@@ -471,19 +471,25 @@ function openApns(config) {
471
471
  state.token = { jwt, issuedAt: now };
472
472
  return jwt;
473
473
  };
474
+ const dropSession = (state, session) => {
475
+ if (state.session === session)
476
+ state.session = undefined;
477
+ if (!session.destroyed)
478
+ session.destroy();
479
+ };
480
+ const retireSession = (state, session) => {
481
+ if (state.session === session)
482
+ state.session = undefined;
483
+ if (!session.closed && !session.destroyed)
484
+ session.close();
485
+ };
474
486
  const sessionFor = (environment, state) => {
475
487
  const existing = state.session;
476
488
  if (existing !== undefined && !existing.destroyed && !existing.closed)
477
489
  return existing;
478
490
  const session = connect(state.authority);
479
- const drop = () => {
480
- if (state.session === session)
481
- state.session = undefined;
482
- if (!session.destroyed)
483
- session.destroy();
484
- };
485
- session.once("goaway", drop);
486
- session.once("error", drop);
491
+ session.once("goaway", () => dropSession(state, session));
492
+ session.once("error", () => dropSession(state, session));
487
493
  session.once("close", () => {
488
494
  if (state.session === session)
489
495
  state.session = undefined;
@@ -566,6 +572,7 @@ function openApns(config) {
566
572
  resolve({ result: "retry", reason: "session unavailable" });
567
573
  return;
568
574
  }
575
+ let responded = false;
569
576
  let status = 0;
570
577
  let responseHeaders = {};
571
578
  const chunks = [];
@@ -580,11 +587,16 @@ function openApns(config) {
580
587
  };
581
588
  const timer = setTimeout(() => finish({ result: "retry", reason: "apns timeout" }), REQUEST_TIMEOUT_MS);
582
589
  stream.on("response", (headers) => {
590
+ responded = true;
583
591
  status = Number(headers[":status"] ?? 0);
584
592
  responseHeaders = headers;
585
593
  });
586
594
  stream.on("data", (chunk) => chunks.push(chunk));
587
595
  stream.on("end", () => {
596
+ if (!responded) {
597
+ finish({ result: "retry", reason: "connection closed" });
598
+ return;
599
+ }
588
600
  let body2;
589
601
  try {
590
602
  body2 = JSON.parse(Buffer.concat(chunks).toString("utf8"));
@@ -593,7 +605,12 @@ function openApns(config) {
593
605
  }
594
606
  finish(classify(request.environment, status, responseHeaders, body2, state));
595
607
  });
596
- stream.on("error", () => finish({ result: "retry", reason: "connection error" }));
608
+ stream.on("error", () => {
609
+ const refused = stream.rstCode === constants.NGHTTP2_REFUSED_STREAM;
610
+ finish({ result: "retry", reason: "connection error" });
611
+ if (refused)
612
+ retireSession(state, session);
613
+ });
597
614
  stream.on("aborted", () => finish({ result: "retry", reason: "connection aborted" }));
598
615
  stream.end(body);
599
616
  });
@@ -793,7 +810,8 @@ async function handleApiRequest(request, response, ctx) {
793
810
  url: `${relayWsBase(ctx.publicOrigin)}/c/${row.hostId}`,
794
811
  hostKey: row.noisePublicKey,
795
812
  secret: result.secret,
796
- expiresAt: result.expiresAt
813
+ expiresAt: result.expiresAt,
814
+ ...result.endpointSet === undefined ? {} : { endpointSet: result.endpointSet }
797
815
  });
798
816
  return;
799
817
  case "refused":
@@ -1056,7 +1074,12 @@ async function startRelay(options) {
1056
1074
  const request = pairingRequests.get(answer.requestId);
1057
1075
  if (request === undefined || request.control !== socket)
1058
1076
  return;
1059
- request.settle(answer.t === "pairing" ? { status: "ok", secret: answer.secret, expiresAt: answer.expiresAt } : { status: "refused", reason: answer.reason });
1077
+ request.settle(answer.t === "pairing" ? {
1078
+ status: "ok",
1079
+ secret: answer.secret,
1080
+ expiresAt: answer.expiresAt,
1081
+ ...answer.endpointSet === undefined ? {} : { endpointSet: answer.endpointSet }
1082
+ } : { status: "refused", reason: answer.reason });
1060
1083
  return;
1061
1084
  }
1062
1085
  socket.close(1008, "unexpected frame");
@@ -5,9 +5,11 @@ import {
5
5
  relayHostId,
6
6
  relayWsBase,
7
7
  signRelayChallenge,
8
- signRelayChallengeV2,
8
+ signRelayChallengeV2
9
+ } from "./chunk-khcpv54k.js";
10
+ import {
9
11
  toBase64Url
10
- } from "./chunk-7qb9tq54.js";
12
+ } from "./chunk-sygeb9qb.js";
11
13
 
12
14
  // apps/host/src/cli/server-ops.ts
13
15
  import { WebSocket } from "ws";
@@ -5,11 +5,11 @@ import {
5
5
  commandHint,
6
6
  hostEntry,
7
7
  packagedVersion
8
- } from "./chunk-c8nxym2z.js";
8
+ } from "./chunk-p56xf1r8.js";
9
9
  import {
10
10
  ProtocolMismatchError,
11
11
  connectRemoteDurable
12
- } from "./chunk-d93kvxvc.js";
12
+ } from "./chunk-a4b61tx5.js";
13
13
  import {
14
14
  DEFAULT_DATA_DIR,
15
15
  hostAddress