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.
- package/README.md +6 -59
- package/dist/{chunk-wqvp1fwh.js → chunk-1r8a2434.js} +38 -15
- package/dist/{chunk-ywmm63ks.js → chunk-3p1eysm9.js} +4 -2
- package/dist/{chunk-d5ypmmr7.js → chunk-73vxwjs7.js} +2 -2
- package/dist/chunk-a4b61tx5.js +2984 -0
- package/dist/{chunk-k2s1hfq5.js → chunk-cytnycs9.js} +9 -9
- package/dist/{chunk-3dnnaz4j.js → chunk-ddh6wfv2.js} +8 -8
- package/dist/{chunk-jn7y815x.js → chunk-fes7wjw1.js} +5 -5
- package/dist/chunk-jgd38rna.js +622 -0
- package/dist/{chunk-4btj08we.js → chunk-jjtr7fr5.js} +5608 -2777
- package/dist/{chunk-7qb9tq54.js → chunk-khcpv54k.js} +64 -310
- package/dist/{chunk-7s1f4z4g.js → chunk-mv94ksvb.js} +22 -5
- package/dist/{chunk-367n9je8.js → chunk-p15hqqjz.js} +1 -1
- package/dist/{chunk-c8nxym2z.js → chunk-p56xf1r8.js} +1 -1
- package/dist/{chunk-ggb1taqy.js → chunk-ptjyk94f.js} +5 -5
- package/dist/{chunk-0xh6cy9v.js → chunk-r2ssa2ex.js} +1 -1
- package/dist/chunk-sygeb9qb.js +430 -0
- package/dist/{chunk-b1h374jr.js → chunk-v414b5pp.js} +8 -2
- package/dist/pinomad.js +10 -10
- package/package.json +2 -1
- package/web/assets/index-5DfEA9Zb.js +398 -0
- package/web/assets/index-D6qIPhPy.css +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-d93kvxvc.js +0 -610
- package/dist/chunk-n4tjhcb7.js +0 -257
- package/dist/chunk-znhn8af7.js +0 -33
- package/web/assets/index-C_aU_ZiV.js +0 -383
- package/web/assets/index-DaPcXZPM.css +0 -1
- package/web/assets/montserrat-latin-ext-wght-normal-BsZE-iaG.woff2 +0 -0
- 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 #
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| [
|
|
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-
|
|
12
|
+
} from "./chunk-khcpv54k.js";
|
|
13
13
|
import {
|
|
14
14
|
commandHint,
|
|
15
15
|
webRoot
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-p56xf1r8.js";
|
|
17
17
|
import {
|
|
18
18
|
normalizeHostName
|
|
19
|
-
} from "./chunk-
|
|
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
|
-
|
|
480
|
-
|
|
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", () =>
|
|
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" ? {
|
|
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-
|
|
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-
|
|
8
|
+
} from "./chunk-p56xf1r8.js";
|
|
9
9
|
import {
|
|
10
10
|
ProtocolMismatchError,
|
|
11
11
|
connectRemoteDurable
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-a4b61tx5.js";
|
|
13
13
|
import {
|
|
14
14
|
DEFAULT_DATA_DIR,
|
|
15
15
|
hostAddress
|