pinomad 0.3.0 → 0.5.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 +4 -2
- package/dist/{chunk-3tf23vjq.js → chunk-1d9sppj0.js} +1 -1
- package/dist/chunk-26d04z02.js +126 -0
- package/dist/chunk-4by4x5c4.js +1376 -0
- package/dist/{chunk-kxws8b83.js → chunk-5ng86fmy.js} +4 -4
- package/dist/chunk-9d59jjmt.js +144 -0
- package/dist/{chunk-t9y2wfk5.js → chunk-aqj4wg33.js} +98 -6
- package/dist/{chunk-n60kkf1m.js → chunk-crv3dqzp.js} +4 -4
- package/dist/{chunk-tfqc1djy.js → chunk-d4wdt5qs.js} +976 -306
- package/dist/chunk-ebfaf3k2.js +190 -0
- package/dist/chunk-f0v3sht0.js +136 -0
- package/dist/{chunk-3qn8gbjb.js → chunk-gqr7ahv4.js} +1 -1
- package/dist/{chunk-c155p0t0.js → chunk-hsp12cjt.js} +111 -8
- package/dist/chunk-kmbh44dt.js +704 -0
- package/dist/{chunk-bggesvnj.js → chunk-m4q9wafw.js} +28 -92
- package/dist/{chunk-syf0z68f.js → chunk-zttf1hvh.js} +2 -2
- package/dist/pinomad.js +16 -11
- package/package.json +1 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/assets/index-BkF0-im5.css +1 -0
- package/web/assets/index-D4lgm3Y4.js +383 -0
- package/web/favicon.svg +1 -0
- package/web/index.html +4 -2
- package/dist/chunk-4dj2vb8t.js +0 -284
- package/dist/chunk-7fqn69ys.js +0 -33
- package/dist/chunk-fxaewke2.js +0 -113
- package/dist/chunk-z4x88s0q.js +0 -347
- package/web/assets/index-CBXdQLua.js +0 -383
- package/web/assets/index-CG33GBxM.css +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PiNomad
|
|
2
2
|
|
|
3
|
-
English · [中文](README.zh-CN.md)
|
|
3
|
+
English · [中文](README.zh-CN.md) · [pinomad.si](https://pinomad.si)
|
|
4
4
|
|
|
5
5
|
A self-hosted coding agent built on [Pi Durable](https://www.npmjs.com/package/@earendil-works/pi-durable). The agent runs on your own machine; the Mac app, the web client, and the iPhone app all attach to the same runs.
|
|
6
6
|
|
|
@@ -87,8 +87,10 @@ These docs are written in Chinese.
|
|
|
87
87
|
| [Usage](docs/usage.md) | Projects, worktrees, tools, MCP configuration |
|
|
88
88
|
| [Remote access](docs/remote-access.md) | Pairing, tunnels, self-hosted relay (VPS or container platforms) |
|
|
89
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 |
|
|
91
|
+
| [Architecture overview](docs/architecture.md) | How the parts and languages fit together, with send/receive diagrams |
|
|
90
92
|
| [Architecture decisions](docs/adr/README.md) | Why things are the way they are |
|
|
91
|
-
| [Roadmap](docs/roadmap.md) |
|
|
93
|
+
| [Roadmap](docs/roadmap.md) | Direction and order; items live in GitHub Issues |
|
|
92
94
|
| [Glossary](GLOSSARY.md) | Domain terms |
|
|
93
95
|
|
|
94
96
|
## Contributing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// apps/host/src/distribution.ts
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
var packagedVersion = "0.
|
|
3
|
+
var packagedVersion = "0.5.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}`;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RELAY_CLOSE,
|
|
3
|
+
decodeRelayToHost,
|
|
4
|
+
encodeRelayFrame,
|
|
5
|
+
relayHostId,
|
|
6
|
+
relayWsBase,
|
|
7
|
+
signRelayChallenge,
|
|
8
|
+
signRelayChallengeV2,
|
|
9
|
+
toBase64Url
|
|
10
|
+
} from "./chunk-kmbh44dt.js";
|
|
11
|
+
|
|
12
|
+
// apps/host/src/cli/server-ops.ts
|
|
13
|
+
import { WebSocket } from "ws";
|
|
14
|
+
|
|
15
|
+
// packages/client/src/account.ts
|
|
16
|
+
var fallbackFetch = (url, init) => fetch(url, init);
|
|
17
|
+
async function probeAccounts(origin, fetchImpl = fallbackFetch) {
|
|
18
|
+
const timeout = new AbortController;
|
|
19
|
+
const timer = setTimeout(() => timeout.abort(), 5000);
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetchImpl(`${origin.replace(/\/+$/, "")}/api/me`, { signal: timeout.signal });
|
|
22
|
+
if (response.status === 401)
|
|
23
|
+
return "enabled";
|
|
24
|
+
if (response.status === 200) {
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
let body;
|
|
27
|
+
try {
|
|
28
|
+
body = JSON.parse(text);
|
|
29
|
+
} catch {
|
|
30
|
+
return "disabled";
|
|
31
|
+
}
|
|
32
|
+
return typeof body?.user === "object" && body.user !== null ? "enabled" : "disabled";
|
|
33
|
+
}
|
|
34
|
+
return response.status === 404 ? "disabled" : "unreachable";
|
|
35
|
+
} catch {
|
|
36
|
+
return "unreachable";
|
|
37
|
+
} finally {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isSecureServerOrigin(origin) {
|
|
42
|
+
let url;
|
|
43
|
+
try {
|
|
44
|
+
url = new URL(origin);
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const host = url.hostname;
|
|
49
|
+
const loopback = host === "localhost" || host.endsWith(".localhost") || host.startsWith("127.") || host === "::1" || host === "[::1]";
|
|
50
|
+
return url.protocol === "https:" || url.protocol === "http:" && loopback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// apps/host/src/cli/server-ops.ts
|
|
54
|
+
function assertSecureServerOrigin(origin) {
|
|
55
|
+
try {
|
|
56
|
+
new URL(origin);
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(`not a URL: ${origin}`);
|
|
59
|
+
}
|
|
60
|
+
if (isSecureServerOrigin(origin))
|
|
61
|
+
return;
|
|
62
|
+
throw new Error(`account operations need an https origin (loopback http allowed for development) — got ${origin}`);
|
|
63
|
+
}
|
|
64
|
+
var probeServerAccounts = (origin, fetchImpl = fetch) => probeAccounts(origin, fetchImpl);
|
|
65
|
+
var closeReason = (code, origin) => {
|
|
66
|
+
switch (code) {
|
|
67
|
+
case RELAY_CLOSE.badSignature:
|
|
68
|
+
return `the server rejected this host's registration signature (${code})`;
|
|
69
|
+
case RELAY_CLOSE.hostNotAllowed:
|
|
70
|
+
return `the server does not know this host (${code}) — a pre-accounts server can't do join/leave; upgrade it first`;
|
|
71
|
+
default:
|
|
72
|
+
return `the connection to ${origin} closed (${code})`;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
async function runServerOperation(options) {
|
|
76
|
+
const origin = options.origin.replace(/\/+$/, "");
|
|
77
|
+
assertSecureServerOrigin(origin);
|
|
78
|
+
const hostId = relayHostId(options.signingKey.publicKey);
|
|
79
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
80
|
+
const socket = new WebSocket(`${relayWsBase(origin)}/host`, { handshakeTimeout: timeoutMs });
|
|
81
|
+
return await new Promise((resolve, reject) => {
|
|
82
|
+
const finish = (done) => {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
socket.close();
|
|
85
|
+
done();
|
|
86
|
+
};
|
|
87
|
+
const timer = setTimeout(() => finish(() => reject(new Error(`no answer from ${origin} within ${timeoutMs / 1000}s`))), timeoutMs);
|
|
88
|
+
socket.once("error", () => {});
|
|
89
|
+
socket.once("close", (code) => finish(() => reject(new Error(closeReason(code, origin)))));
|
|
90
|
+
socket.on("message", (data, isBinary) => {
|
|
91
|
+
let frame;
|
|
92
|
+
try {
|
|
93
|
+
if (isBinary)
|
|
94
|
+
throw new Error("binary frame");
|
|
95
|
+
frame = decodeRelayToHost(data.toString());
|
|
96
|
+
} catch {
|
|
97
|
+
finish(() => reject(new Error(`${origin} is not speaking the pinomad relay protocol`)));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (frame.t === "challenge") {
|
|
101
|
+
const noisePublicKey = toBase64Url(options.noisePublicKey);
|
|
102
|
+
const challenge = { relayOrigin: origin, hostId, nonce: frame.nonce };
|
|
103
|
+
socket.send(encodeRelayFrame({
|
|
104
|
+
t: "register",
|
|
105
|
+
hostId,
|
|
106
|
+
signature: signRelayChallenge(options.signingKey.secretKey, challenge),
|
|
107
|
+
signatureV2: signRelayChallengeV2(options.signingKey.secretKey, { ...challenge, noisePublicKey }),
|
|
108
|
+
noisePublicKey,
|
|
109
|
+
op: options.op,
|
|
110
|
+
...options.joinToken === undefined ? {} : { joinToken: options.joinToken },
|
|
111
|
+
...options.name === undefined ? {} : { name: options.name }
|
|
112
|
+
}));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (frame.t === "registered") {
|
|
116
|
+
finish(() => reject(new Error(`${origin} ignored the operation — the server is too old for accounts; upgrade it first`)));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (frame.t === "incoming" || frame.t === "pairing-request" || frame.t === "push-result")
|
|
120
|
+
return;
|
|
121
|
+
finish(() => resolve(frame));
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { assertSecureServerOrigin, probeServerAccounts, runServerOperation };
|