pinomad 0.10.0 → 0.12.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 +11 -9
- package/dist/chunk-41q0saz0.js +73 -0
- package/dist/chunk-42nqtx5r.js +22 -0
- package/dist/{chunk-wb10efyq.js → chunk-445cevpm.js} +1 -1
- package/dist/{chunk-jgd38rna.js → chunk-47ff1d1t.js} +206 -178
- package/dist/chunk-5qf1grkk.js +220 -0
- package/dist/{chunk-4h7pkkzr.js → chunk-7mn38n4f.js} +3 -3
- package/dist/{chunk-zehwx009.js → chunk-7wj0jh3t.js} +1 -1
- package/dist/{chunk-5791mb0b.js → chunk-agq4tn9r.js} +47 -23
- package/dist/{chunk-c5p9xkd8.js → chunk-aqj9wfmb.js} +3 -3
- package/dist/{chunk-cx8k63by.js → chunk-g3ahp7b9.js} +12 -1
- package/dist/chunk-gg2dcqbg.js +458 -0
- package/dist/{chunk-73efzaf1.js → chunk-jeqsdcj1.js} +30 -13
- package/dist/{chunk-czj9nfrz.js → chunk-ksfxqd8s.js} +500 -41
- package/dist/{chunk-p978qj9m.js → chunk-mnmqyq7d.js} +565 -39
- package/dist/{chunk-4sepcfpy.js → chunk-np4jd070.js} +155 -3
- package/dist/{chunk-v414b5pp.js → chunk-nppgxh6c.js} +2 -2
- package/dist/chunk-nq57fdnq.js +106 -0
- package/dist/{chunk-sygeb9qb.js → chunk-pqe7q1s7.js} +1 -1
- package/dist/{chunk-w2m9xkks.js → chunk-t5abqsva.js} +199 -72
- package/dist/{chunk-8gywfw5n.js → chunk-t91zt5f0.js} +2 -51
- package/dist/{chunk-vax4zspw.js → chunk-vqkb70d7.js} +3 -3
- package/dist/chunk-vx7dy07y.js +26 -0
- package/dist/pinomad.js +19 -12
- package/package.json +2 -1
- package/web/assets/astro-DOly5024.js +1 -0
- package/web/assets/c-BIGW1oBm.js +1 -0
- package/web/assets/core-DnpzcQzs.js +4 -0
- package/web/assets/cpp-DddhVWhK.js +1 -0
- package/web/assets/csharp-B_TYUkTI.js +1 -0
- package/web/assets/css-Z72CF80r.js +1 -0
- package/web/assets/dart-CtKP3W-j.js +1 -0
- package/web/assets/diff-D97Zzqfu.js +1 -0
- package/web/assets/dockerfile-BcOcwvcX.js +1 -0
- package/web/assets/elixir-DkJLSxtU.js +1 -0
- package/web/assets/engine-javascript-BHPwbUfN.js +141 -0
- package/web/assets/engine-oniguruma-CQjQPVHy.js +1 -0
- package/web/assets/fish-BvzEVeQv.js +1 -0
- package/web/assets/go-C27-OAKa.js +1 -0
- package/web/assets/graphql-ChdNCCLP.js +1 -0
- package/web/assets/haskell-Df6bDoY_.js +1 -0
- package/web/assets/hcl-BWvSN4gD.js +1 -0
- package/web/assets/highlighted-diff-Dn5ifsvK.js +175 -0
- package/web/assets/highlights-DnHv6NBM.js +4 -0
- package/web/assets/highlights-themes-CUdrlZk3.js +1 -0
- package/web/assets/html-BlS82ilI.js +1 -0
- package/web/assets/{index-DoyHMPwf.css → index-BV0ecolo.css} +1 -1
- package/web/assets/index-DNsTTgPE.js +396 -0
- package/web/assets/ini-BEwlwnbL.js +1 -0
- package/web/assets/java-CylS5w8V.js +1 -0
- package/web/assets/javascript-wDzz0qaB.js +1 -0
- package/web/assets/json-Cp-IABpG.js +1 -0
- package/web/assets/jsonc-Des-eS-w.js +1 -0
- package/web/assets/jsx-g9-lgVsj.js +1 -0
- package/web/assets/kotlin-BdnUsdx6.js +1 -0
- package/web/assets/less-B1dDrJ26.js +1 -0
- package/web/assets/lua-BaeVxFsk.js +1 -0
- package/web/assets/make-CHLpvVh8.js +1 -0
- package/web/assets/markdown-Cvjx9yec.js +1 -0
- package/web/assets/nix-CwoSXNpI.js +1 -0
- package/web/assets/objective-c-DXmwc3jG.js +1 -0
- package/web/assets/php-DoruYMF0.js +1 -0
- package/web/assets/pierre-dark-CyvmCCZW.js +1 -0
- package/web/assets/pierre-dark-VKo95ZgA.js +1 -0
- package/web/assets/pierre-light-480U9XYS.js +1 -0
- package/web/assets/pierre-light-BVSW-Xev.js +1 -0
- package/web/assets/postcss-DnzrXTVv.js +1 -0
- package/web/assets/powershell-Dd0kTxKP.js +1 -0
- package/web/assets/prisma-Vru482bI.js +1 -0
- package/web/assets/proto-C7zT0LnQ.js +1 -0
- package/web/assets/python-B6aJPvgy.js +1 -0
- package/web/assets/ruby-B43yHlf9.js +1 -0
- package/web/assets/rust-DPvrY3IQ.js +1 -0
- package/web/assets/scala-CqE71os6.js +1 -0
- package/web/assets/scss-Cw6XMfVs.js +1 -0
- package/web/assets/shellscript-Yzrsuije.js +1 -0
- package/web/assets/shiki-BuPP5HCg.js +2 -0
- package/web/assets/shiki-langs-FJk0v0Ga.js +2 -0
- package/web/assets/shiki-wasm-CgFRq91U.js +1 -0
- package/web/assets/sql-DkvoVjww.js +1 -0
- package/web/assets/stream-tokenizer-Bl6bisIa.js +4 -0
- package/web/assets/svelte-DoIXUgJc.js +1 -0
- package/web/assets/swift-C2oV4EkX.js +1 -0
- package/web/assets/terraform-BETggiCN.js +1 -0
- package/web/assets/theming-themes-BL_CC-Mm.js +1 -0
- package/web/assets/toml-vGWfd6FD.js +1 -0
- package/web/assets/tsx-COt5Ahok.js +1 -0
- package/web/assets/typescript-BPQ3VLAy.js +1 -0
- package/web/assets/vue-CwSeJimo.js +1 -0
- package/web/assets/xml-sdJ4AIDG.js +1 -0
- package/web/assets/xterm-view-6GBZ9nXN.css +32 -0
- package/web/assets/xterm-view-ZsuzJb2C.js +38 -0
- package/web/assets/yaml-Buea-lGh.js +1 -0
- package/web/assets/zig-VOosw3JB.js +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-bhtx2c8a.js +0 -16
- package/dist/chunk-c05g401k.js +0 -170
- package/web/assets/index-BdBjP1qJ.js +0 -395
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
|
-
|
|
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` opens a page in your browser; sign in and confirm to add the host to your account. It then runs as a service. Without a browser on that machine (over SSH, say), it prints a code to enter on any signed-in device instead.
|
|
20
21
|
|
|
21
|
-
```sh
|
|
22
|
-
|
|
23
|
-
pinomad
|
|
24
|
-
|
|
25
|
-
```
|
|
22
|
+
```sh
|
|
23
|
+
curl -fsSL https://pinomad.si/install.sh | sh
|
|
24
|
+
pinomad up
|
|
25
|
+
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Already installed with npm? The script leaves that install alone; run `pinomad upgrade`, then `pinomad up`.
|
|
28
28
|
|
|
29
|
-
|
|
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/) (
|
|
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
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
readRemovalNotice,
|
|
4
|
+
readServerConfig
|
|
5
|
+
} from "./chunk-nq57fdnq.js";
|
|
6
|
+
import {
|
|
7
|
+
argValue,
|
|
8
|
+
reachabilityLine,
|
|
9
|
+
serviceManager,
|
|
10
|
+
versionLine
|
|
11
|
+
} from "./chunk-t5abqsva.js";
|
|
12
|
+
import"./chunk-np4jd070.js";
|
|
13
|
+
import"./chunk-kzbzacz9.js";
|
|
14
|
+
import"./chunk-47ff1d1t.js";
|
|
15
|
+
import"./chunk-pqe7q1s7.js";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_DATA_DIR
|
|
18
|
+
} from "./chunk-ngf1zatm.js";
|
|
19
|
+
import {
|
|
20
|
+
commandHint
|
|
21
|
+
} from "./chunk-7wj0jh3t.js";
|
|
22
|
+
import"./chunk-df32avw6.js";
|
|
23
|
+
|
|
24
|
+
// apps/host/src/cli/status.ts
|
|
25
|
+
import { resolve } from "node:path";
|
|
26
|
+
import { parseArgs } from "node:util";
|
|
27
|
+
async function statusReport(args, deps) {
|
|
28
|
+
const dataDir = resolve(args.dataDir);
|
|
29
|
+
const manager = deps?.manager ?? serviceManager();
|
|
30
|
+
const server = await readServerConfig(dataDir);
|
|
31
|
+
const removed = server === undefined ? await readRemovalNotice(dataDir) : undefined;
|
|
32
|
+
const service = await manager.state();
|
|
33
|
+
const spec = service.installed ? await manager.read() : undefined;
|
|
34
|
+
const own = spec !== undefined && resolve(spec.dataDir) === dataDir;
|
|
35
|
+
const lanPort = own ? argValue(spec.hostArgs, "--remote-port") : undefined;
|
|
36
|
+
const port = own ? argValue(spec.hostArgs, "--port") : undefined;
|
|
37
|
+
return {
|
|
38
|
+
...server === undefined ? {} : { server },
|
|
39
|
+
...removed === undefined ? {} : { removed },
|
|
40
|
+
...lanPort === undefined ? {} : { lanPort },
|
|
41
|
+
service,
|
|
42
|
+
...port === undefined ? {} : { port },
|
|
43
|
+
...spec === undefined || own ? {} : { otherDataDir: spec.dataDir }
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function main(argv) {
|
|
47
|
+
const { values } = parseArgs({ args: [...argv], options: { "data-dir": { type: "string" } } });
|
|
48
|
+
const dataDir = resolve(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
49
|
+
try {
|
|
50
|
+
const report = await statusReport({ dataDir });
|
|
51
|
+
const { server, removed } = report;
|
|
52
|
+
console.log(server !== undefined ? `server: ${server.username === undefined ? "joined" : server.username} at ${server.origin}` : removed !== undefined ? `server: removed from ${removed.username === undefined ? "its account" : `${removed.username}'s account`} at ${removed.origin} (\`${commandHint("up")}\` to join again)` : `server: not joined (\`${commandHint("up")}\` to join)`);
|
|
53
|
+
console.log(`LAN and tailnet: ${report.lanPort === undefined ? "off" : `port ${report.lanPort}`}`);
|
|
54
|
+
console.log(`service: ${!report.service.installed ? "not installed" : report.service.stopped ? `stopped (\`${commandHint("service")} start\` to turn it back on)` : report.service.detail}`);
|
|
55
|
+
if (report.otherDataDir !== undefined)
|
|
56
|
+
console.log(`note: the installed service runs ${report.otherDataDir}, not ${dataDir}`);
|
|
57
|
+
console.log(await versionLine());
|
|
58
|
+
console.log(await reachabilityLine([
|
|
59
|
+
"--data-dir",
|
|
60
|
+
dataDir,
|
|
61
|
+
...report.port === undefined ? [] : ["--url", `ws://127.0.0.1:${report.port}`]
|
|
62
|
+
]));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (false)
|
|
69
|
+
;
|
|
70
|
+
export {
|
|
71
|
+
statusReport,
|
|
72
|
+
main
|
|
73
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
joinServer,
|
|
4
|
+
main
|
|
5
|
+
} from "./chunk-gg2dcqbg.js";
|
|
6
|
+
import"./chunk-7mn38n4f.js";
|
|
7
|
+
import"./chunk-nppgxh6c.js";
|
|
8
|
+
import"./chunk-t91zt5f0.js";
|
|
9
|
+
import"./chunk-nq57fdnq.js";
|
|
10
|
+
import"./chunk-g3ahp7b9.js";
|
|
11
|
+
import"./chunk-t5abqsva.js";
|
|
12
|
+
import"./chunk-np4jd070.js";
|
|
13
|
+
import"./chunk-kzbzacz9.js";
|
|
14
|
+
import"./chunk-47ff1d1t.js";
|
|
15
|
+
import"./chunk-pqe7q1s7.js";
|
|
16
|
+
import"./chunk-ngf1zatm.js";
|
|
17
|
+
import"./chunk-7wj0jh3t.js";
|
|
18
|
+
import"./chunk-df32avw6.js";
|
|
19
|
+
export {
|
|
20
|
+
main,
|
|
21
|
+
joinServer
|
|
22
|
+
};
|
|
@@ -16,7 +16,7 @@ var NOT_BUILT = "Web client not built: run bun run build";
|
|
|
16
16
|
var SECURITY_HEADERS = {
|
|
17
17
|
"content-security-policy": [
|
|
18
18
|
"default-src 'self'",
|
|
19
|
-
"script-src 'self'",
|
|
19
|
+
"script-src 'self' 'wasm-unsafe-eval'",
|
|
20
20
|
"style-src 'self' 'unsafe-inline'",
|
|
21
21
|
"img-src 'self' data:",
|
|
22
22
|
"connect-src 'self' ws: wss:",
|
|
@@ -2,7 +2,176 @@ import {
|
|
|
2
2
|
HOST_SETTING_KEYS,
|
|
3
3
|
isQueueMode,
|
|
4
4
|
isThinkingLevel
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-pqe7q1s7.js";
|
|
6
|
+
|
|
7
|
+
// packages/protocol/src/rtc.ts
|
|
8
|
+
function rtcAddressScope(address) {
|
|
9
|
+
const ip = parseRtcAddress(address);
|
|
10
|
+
if (ip === undefined)
|
|
11
|
+
return "unknown";
|
|
12
|
+
if (ip.family === 4)
|
|
13
|
+
return ipv4Scope(ip.octets);
|
|
14
|
+
const octets = mappedIpv4(ip.segments);
|
|
15
|
+
return octets === undefined ? ipv6Scope(ip.segments) : ipv4Scope(octets);
|
|
16
|
+
}
|
|
17
|
+
function rtcCandidateEnd(info) {
|
|
18
|
+
return {
|
|
19
|
+
...info.type === undefined ? {} : { type: info.type },
|
|
20
|
+
...info.address === undefined ? {} : { address: info.address },
|
|
21
|
+
scope: rtcAddressScope(info.address)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function rtcSelectedPair(local, remote) {
|
|
25
|
+
return { local: rtcCandidateEnd(local), remote: rtcCandidateEnd(remote) };
|
|
26
|
+
}
|
|
27
|
+
function parseRtcAddress(address) {
|
|
28
|
+
if (address === undefined)
|
|
29
|
+
return;
|
|
30
|
+
const text = address.trim();
|
|
31
|
+
if (text === "")
|
|
32
|
+
return;
|
|
33
|
+
let host = text;
|
|
34
|
+
if (text.startsWith("[")) {
|
|
35
|
+
const close = text.indexOf("]");
|
|
36
|
+
if (close === -1)
|
|
37
|
+
return;
|
|
38
|
+
const tail = text.slice(close + 1);
|
|
39
|
+
if (tail !== "" && !/^:\d+$/.test(tail))
|
|
40
|
+
return;
|
|
41
|
+
host = text.slice(1, close);
|
|
42
|
+
} else if (text.split(":").length === 2) {
|
|
43
|
+
const [candidate, port] = text.split(":");
|
|
44
|
+
if (/^\d+$/.test(port))
|
|
45
|
+
host = candidate;
|
|
46
|
+
}
|
|
47
|
+
const zone = host.indexOf("%");
|
|
48
|
+
if (zone !== -1)
|
|
49
|
+
host = host.slice(0, zone);
|
|
50
|
+
const ipv4 = parseIpv4(host);
|
|
51
|
+
if (ipv4 !== undefined)
|
|
52
|
+
return { family: 4, octets: ipv4 };
|
|
53
|
+
const ipv6 = parseIpv6(host);
|
|
54
|
+
return ipv6 === undefined ? undefined : { family: 6, segments: ipv6 };
|
|
55
|
+
}
|
|
56
|
+
function parseIpv4(text) {
|
|
57
|
+
const parts = text.split(".");
|
|
58
|
+
if (parts.length !== 4)
|
|
59
|
+
return;
|
|
60
|
+
const octets = [];
|
|
61
|
+
for (const part of parts) {
|
|
62
|
+
if (!/^(?:0|[1-9]\d{0,2})$/.test(part))
|
|
63
|
+
return;
|
|
64
|
+
const value = Number(part);
|
|
65
|
+
if (value > 255)
|
|
66
|
+
return;
|
|
67
|
+
octets.push(value);
|
|
68
|
+
}
|
|
69
|
+
return octets;
|
|
70
|
+
}
|
|
71
|
+
function parseIpv6(text) {
|
|
72
|
+
const halves = text.split("::");
|
|
73
|
+
if (halves.length > 2)
|
|
74
|
+
return;
|
|
75
|
+
const parsePart = (part, allowIpv4) => {
|
|
76
|
+
if (part === "")
|
|
77
|
+
return [];
|
|
78
|
+
const tokens = part.split(":");
|
|
79
|
+
const groups = [];
|
|
80
|
+
for (let index = 0;index < tokens.length; index++) {
|
|
81
|
+
const token = tokens[index];
|
|
82
|
+
if (allowIpv4 && index === tokens.length - 1 && token.includes(".")) {
|
|
83
|
+
const v4 = parseIpv4(token);
|
|
84
|
+
if (v4 === undefined)
|
|
85
|
+
return;
|
|
86
|
+
groups.push(v4[0] << 8 | v4[1], v4[2] << 8 | v4[3]);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(token))
|
|
90
|
+
return;
|
|
91
|
+
groups.push(Number.parseInt(token, 16));
|
|
92
|
+
}
|
|
93
|
+
return groups;
|
|
94
|
+
};
|
|
95
|
+
if (halves.length === 1) {
|
|
96
|
+
const groups = parsePart(text, true);
|
|
97
|
+
return groups?.length === 8 ? groups : undefined;
|
|
98
|
+
}
|
|
99
|
+
const left = parsePart(halves[0], false);
|
|
100
|
+
const right = parsePart(halves[1], true);
|
|
101
|
+
if (left === undefined || right === undefined)
|
|
102
|
+
return;
|
|
103
|
+
const zeros = 8 - left.length - right.length;
|
|
104
|
+
if (zeros < 1)
|
|
105
|
+
return;
|
|
106
|
+
return [...left, ...new Array(zeros).fill(0), ...right];
|
|
107
|
+
}
|
|
108
|
+
function mappedIpv4(segments) {
|
|
109
|
+
if (!segments.slice(0, 5).every((group) => group === 0) || segments[5] !== 65535)
|
|
110
|
+
return;
|
|
111
|
+
return [segments[6] >> 8, segments[6] & 255, segments[7] >> 8, segments[7] & 255];
|
|
112
|
+
}
|
|
113
|
+
function ipv4Scope(octets) {
|
|
114
|
+
const [a, b] = octets;
|
|
115
|
+
if (a === 127)
|
|
116
|
+
return "loopback";
|
|
117
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
118
|
+
return "tailnet";
|
|
119
|
+
if (a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254) {
|
|
120
|
+
return "lan";
|
|
121
|
+
}
|
|
122
|
+
return "public";
|
|
123
|
+
}
|
|
124
|
+
function ipv6Scope(segments) {
|
|
125
|
+
if (segments[0] === 64890 && segments[1] === 4444 && segments[2] === 41440)
|
|
126
|
+
return "tailnet";
|
|
127
|
+
if ((segments[0] & 65024) === 64512 || (segments[0] & 65472) === 65152)
|
|
128
|
+
return "lan";
|
|
129
|
+
if (segments.slice(0, 7).every((group) => group === 0) && segments[7] === 1)
|
|
130
|
+
return "loopback";
|
|
131
|
+
return "public";
|
|
132
|
+
}
|
|
133
|
+
var RTC_DEFAULT_STUN = ["stun:stun.cloudflare.com:3478"];
|
|
134
|
+
var RTC_DATA_CHANNEL_LABEL = "pinomad";
|
|
135
|
+
var RTC_MAX_RECORD_BYTES = 65535;
|
|
136
|
+
var RTC_DEFAULT_MAX_MESSAGE_SIZE = 65536;
|
|
137
|
+
var RTC_MIN_MAX_MESSAGE_SIZE = RTC_MAX_RECORD_BYTES;
|
|
138
|
+
function isRtcHello(value) {
|
|
139
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
140
|
+
return false;
|
|
141
|
+
const hello = value;
|
|
142
|
+
return Array.isArray(hello.stun) && hello.stun.every((server) => typeof server === "string" && server.length > 0) && hello.signalOnly === true;
|
|
143
|
+
}
|
|
144
|
+
function rtcCandidateType(candidate) {
|
|
145
|
+
const parts = candidate.trim().split(/\s+/);
|
|
146
|
+
const at = parts.indexOf("typ");
|
|
147
|
+
return at === -1 ? undefined : parts[at + 1];
|
|
148
|
+
}
|
|
149
|
+
function sdpCandidates(sdp) {
|
|
150
|
+
return sdp.split(/\r?\n/).filter((line) => line.startsWith("a=candidate:")).map((line) => line.slice("a=".length));
|
|
151
|
+
}
|
|
152
|
+
function withRtcCandidates(sdp, candidates) {
|
|
153
|
+
const lines = sdp.split(/\r?\n/).filter((line) => line !== "" && !line.startsWith("a=candidate:") && line !== "a=end-of-candidates");
|
|
154
|
+
const unique = [...new Set(candidates.map((candidate) => candidate.replace(/^a=/, "")))];
|
|
155
|
+
return [...lines, ...unique.map((candidate) => `a=${candidate}`), "a=end-of-candidates", ""].join(`\r
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
158
|
+
function parseRtcMaxMessageSize(sdp) {
|
|
159
|
+
const line = sdp.split(/\r?\n/).find((entry) => entry.startsWith("a=max-message-size:"));
|
|
160
|
+
if (line === undefined)
|
|
161
|
+
return { kind: "bounded", bytes: RTC_DEFAULT_MAX_MESSAGE_SIZE };
|
|
162
|
+
const raw = line.slice("a=max-message-size:".length).trim();
|
|
163
|
+
if (!/^\d+$/.test(raw))
|
|
164
|
+
return { kind: "invalid", reason: `malformed a=max-message-size: ${raw}` };
|
|
165
|
+
const bytes = Number(raw);
|
|
166
|
+
if (!Number.isSafeInteger(bytes))
|
|
167
|
+
return { kind: "invalid", reason: `a=max-message-size is not a safe integer: ${raw}` };
|
|
168
|
+
if (bytes === 0)
|
|
169
|
+
return { kind: "unlimited" };
|
|
170
|
+
if (bytes < RTC_MIN_MAX_MESSAGE_SIZE) {
|
|
171
|
+
return { kind: "invalid", reason: `a=max-message-size ${bytes} cannot carry a ${RTC_MIN_MAX_MESSAGE_SIZE}-byte record` };
|
|
172
|
+
}
|
|
173
|
+
return { kind: "bounded", bytes };
|
|
174
|
+
}
|
|
6
175
|
|
|
7
176
|
// packages/protocol/src/auth.ts
|
|
8
177
|
var CUSTOM_PROVIDER_APIS = ["openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai"];
|
|
@@ -18,7 +187,7 @@ var conversationStream = (id) => `conversation:${id}`;
|
|
|
18
187
|
var docStream = (kind, id) => `doc:${kind}:${id}`;
|
|
19
188
|
var UNAUTHORIZED_CLOSE_CODE = 4401;
|
|
20
189
|
var INVALID_FRAME_CLOSE_CODE = 4400;
|
|
21
|
-
var PROTOCOL = { major: 5, minor:
|
|
190
|
+
var PROTOCOL = { major: 5, minor: 27 };
|
|
22
191
|
var CALL_SINCE = {
|
|
23
192
|
addProject: 0,
|
|
24
193
|
removeProject: 0,
|
|
@@ -60,7 +229,13 @@ var CALL_SINCE = {
|
|
|
60
229
|
beginAttachment: 19,
|
|
61
230
|
appendAttachment: 19,
|
|
62
231
|
rtcOffer: 23,
|
|
63
|
-
ping: 23
|
|
232
|
+
ping: 23,
|
|
233
|
+
openTerminal: 27,
|
|
234
|
+
terminalInput: 27,
|
|
235
|
+
resizeTerminal: 27,
|
|
236
|
+
renameTerminal: 27,
|
|
237
|
+
restartTerminal: 27,
|
|
238
|
+
closeTerminal: 27
|
|
64
239
|
};
|
|
65
240
|
function hostSupportsCall(hostMinor, method) {
|
|
66
241
|
return hostMinor >= CALL_SINCE[method];
|
|
@@ -77,7 +252,8 @@ var FEATURE_SINCE = {
|
|
|
77
252
|
attachments: 19,
|
|
78
253
|
imageVariants: 20,
|
|
79
254
|
rtc: 23,
|
|
80
|
-
submitEntry: 25
|
|
255
|
+
submitEntry: 25,
|
|
256
|
+
terminals: 27
|
|
81
257
|
};
|
|
82
258
|
function hostSupportsFeature(hostMinor, feature) {
|
|
83
259
|
return hostMinor >= FEATURE_SINCE[feature];
|
|
@@ -90,8 +266,16 @@ function parseClientFrame(value) {
|
|
|
90
266
|
if (typeof stream !== "string")
|
|
91
267
|
return { kind: "malformed" };
|
|
92
268
|
if (isStreamName(stream)) {
|
|
93
|
-
|
|
94
|
-
|
|
269
|
+
if (value.type === "unsubscribe")
|
|
270
|
+
return { kind: "frame", frame: { type: "unsubscribe", stream } };
|
|
271
|
+
if (stream.startsWith("terminal:") && value.since !== undefined) {
|
|
272
|
+
const since = value.since;
|
|
273
|
+
if (typeof since !== "number" || !Number.isSafeInteger(since) || since < 0)
|
|
274
|
+
return { kind: "malformed" };
|
|
275
|
+
return { kind: "frame", frame: { type: "subscribe", stream, since } };
|
|
276
|
+
}
|
|
277
|
+
const refs = value.images === "ref";
|
|
278
|
+
return { kind: "frame", frame: refs ? { type: "subscribe", stream, images: "ref" } : { type: "subscribe", stream } };
|
|
95
279
|
}
|
|
96
280
|
const prefix = stream.slice(0, stream.indexOf(":") === -1 ? undefined : stream.indexOf(":"));
|
|
97
281
|
if (STREAM_KINDS.has(prefix))
|
|
@@ -108,9 +292,9 @@ function parseClientFrame(value) {
|
|
|
108
292
|
return { kind: "unsupportedCall", id, method };
|
|
109
293
|
return validCallArgs(method, args) ? { kind: "frame", frame: { type: "call", id, method, args } } : { kind: "malformed" };
|
|
110
294
|
}
|
|
111
|
-
var STREAM_KINDS = new Set(["conversation", "doc", "tasks", ...HOST_STREAM_NAMES]);
|
|
295
|
+
var STREAM_KINDS = new Set(["conversation", "doc", "tasks", "terminals", "terminal", ...HOST_STREAM_NAMES]);
|
|
112
296
|
function isStreamName(stream) {
|
|
113
|
-
return stream === "tasks" || HOST_STREAM_NAMES.includes(stream) || /^(?:conversation:|doc
|
|
297
|
+
return stream === "tasks" || HOST_STREAM_NAMES.includes(stream) || /^(?:conversation:|doc:.+:|terminals:)[1-9]\d*$/.test(stream) || /^terminal:[\w-]+$/.test(stream);
|
|
114
298
|
}
|
|
115
299
|
var isCallMethod = (method) => CALL_METHODS.includes(method);
|
|
116
300
|
var CALL_METHODS = Object.keys(CALL_SINCE);
|
|
@@ -130,7 +314,20 @@ function validCallArgs(method, args) {
|
|
|
130
314
|
const skills = () => args.skills === undefined || Array.isArray(args.skills) && args.skills.every(nonempty);
|
|
131
315
|
const attachments = () => args.attachments === undefined || Array.isArray(args.attachments) && args.attachments.every(nonempty);
|
|
132
316
|
const role = () => record(args.role) && typeof args.role.name === "string" && typeof args.role.description === "string" && typeof args.role.prompt === "string" && (args.role.model === undefined || typeof args.role.model === "string") && (args.role.thinkingLevel === undefined || typeof args.role.thinkingLevel === "string");
|
|
317
|
+
const terminalId = () => nonempty(args.terminalId);
|
|
318
|
+
const size = () => [args.cols, args.rows].every((n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 1);
|
|
133
319
|
switch (method) {
|
|
320
|
+
case "openTerminal":
|
|
321
|
+
return conversationId() && size();
|
|
322
|
+
case "terminalInput":
|
|
323
|
+
return terminalId() && typeof args.data === "string";
|
|
324
|
+
case "resizeTerminal":
|
|
325
|
+
case "restartTerminal":
|
|
326
|
+
return terminalId() && size();
|
|
327
|
+
case "renameTerminal":
|
|
328
|
+
return terminalId() && (args.name === null || typeof args.name === "string");
|
|
329
|
+
case "closeTerminal":
|
|
330
|
+
return terminalId();
|
|
134
331
|
case "submit":
|
|
135
332
|
return conversationId() && typeof args.text === "string" && (args.whenBusy === "steer" || args.whenBusy === "followUp") && nonempty(args.requestId) && images() && skills() && attachments();
|
|
136
333
|
case "submission":
|
|
@@ -218,175 +415,6 @@ function nonempty(value) {
|
|
|
218
415
|
return typeof value === "string" && value.length > 0;
|
|
219
416
|
}
|
|
220
417
|
|
|
221
|
-
// packages/protocol/src/rtc.ts
|
|
222
|
-
function rtcAddressScope(address) {
|
|
223
|
-
const ip = parseRtcAddress(address);
|
|
224
|
-
if (ip === undefined)
|
|
225
|
-
return "unknown";
|
|
226
|
-
if (ip.family === 4)
|
|
227
|
-
return ipv4Scope(ip.octets);
|
|
228
|
-
const octets = mappedIpv4(ip.segments);
|
|
229
|
-
return octets === undefined ? ipv6Scope(ip.segments) : ipv4Scope(octets);
|
|
230
|
-
}
|
|
231
|
-
function rtcCandidateEnd(info) {
|
|
232
|
-
return {
|
|
233
|
-
...info.type === undefined ? {} : { type: info.type },
|
|
234
|
-
...info.address === undefined ? {} : { address: info.address },
|
|
235
|
-
scope: rtcAddressScope(info.address)
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
function rtcSelectedPair(local, remote) {
|
|
239
|
-
return { local: rtcCandidateEnd(local), remote: rtcCandidateEnd(remote) };
|
|
240
|
-
}
|
|
241
|
-
function parseRtcAddress(address) {
|
|
242
|
-
if (address === undefined)
|
|
243
|
-
return;
|
|
244
|
-
const text = address.trim();
|
|
245
|
-
if (text === "")
|
|
246
|
-
return;
|
|
247
|
-
let host = text;
|
|
248
|
-
if (text.startsWith("[")) {
|
|
249
|
-
const close = text.indexOf("]");
|
|
250
|
-
if (close === -1)
|
|
251
|
-
return;
|
|
252
|
-
const tail = text.slice(close + 1);
|
|
253
|
-
if (tail !== "" && !/^:\d+$/.test(tail))
|
|
254
|
-
return;
|
|
255
|
-
host = text.slice(1, close);
|
|
256
|
-
} else if (text.split(":").length === 2) {
|
|
257
|
-
const [candidate, port] = text.split(":");
|
|
258
|
-
if (/^\d+$/.test(port))
|
|
259
|
-
host = candidate;
|
|
260
|
-
}
|
|
261
|
-
const zone = host.indexOf("%");
|
|
262
|
-
if (zone !== -1)
|
|
263
|
-
host = host.slice(0, zone);
|
|
264
|
-
const ipv4 = parseIpv4(host);
|
|
265
|
-
if (ipv4 !== undefined)
|
|
266
|
-
return { family: 4, octets: ipv4 };
|
|
267
|
-
const ipv6 = parseIpv6(host);
|
|
268
|
-
return ipv6 === undefined ? undefined : { family: 6, segments: ipv6 };
|
|
269
|
-
}
|
|
270
|
-
function parseIpv4(text) {
|
|
271
|
-
const parts = text.split(".");
|
|
272
|
-
if (parts.length !== 4)
|
|
273
|
-
return;
|
|
274
|
-
const octets = [];
|
|
275
|
-
for (const part of parts) {
|
|
276
|
-
if (!/^(?:0|[1-9]\d{0,2})$/.test(part))
|
|
277
|
-
return;
|
|
278
|
-
const value = Number(part);
|
|
279
|
-
if (value > 255)
|
|
280
|
-
return;
|
|
281
|
-
octets.push(value);
|
|
282
|
-
}
|
|
283
|
-
return octets;
|
|
284
|
-
}
|
|
285
|
-
function parseIpv6(text) {
|
|
286
|
-
const halves = text.split("::");
|
|
287
|
-
if (halves.length > 2)
|
|
288
|
-
return;
|
|
289
|
-
const parsePart = (part, allowIpv4) => {
|
|
290
|
-
if (part === "")
|
|
291
|
-
return [];
|
|
292
|
-
const tokens = part.split(":");
|
|
293
|
-
const groups = [];
|
|
294
|
-
for (let index = 0;index < tokens.length; index++) {
|
|
295
|
-
const token = tokens[index];
|
|
296
|
-
if (allowIpv4 && index === tokens.length - 1 && token.includes(".")) {
|
|
297
|
-
const v4 = parseIpv4(token);
|
|
298
|
-
if (v4 === undefined)
|
|
299
|
-
return;
|
|
300
|
-
groups.push(v4[0] << 8 | v4[1], v4[2] << 8 | v4[3]);
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
if (!/^[0-9a-fA-F]{1,4}$/.test(token))
|
|
304
|
-
return;
|
|
305
|
-
groups.push(Number.parseInt(token, 16));
|
|
306
|
-
}
|
|
307
|
-
return groups;
|
|
308
|
-
};
|
|
309
|
-
if (halves.length === 1) {
|
|
310
|
-
const groups = parsePart(text, true);
|
|
311
|
-
return groups?.length === 8 ? groups : undefined;
|
|
312
|
-
}
|
|
313
|
-
const left = parsePart(halves[0], false);
|
|
314
|
-
const right = parsePart(halves[1], true);
|
|
315
|
-
if (left === undefined || right === undefined)
|
|
316
|
-
return;
|
|
317
|
-
const zeros = 8 - left.length - right.length;
|
|
318
|
-
if (zeros < 1)
|
|
319
|
-
return;
|
|
320
|
-
return [...left, ...new Array(zeros).fill(0), ...right];
|
|
321
|
-
}
|
|
322
|
-
function mappedIpv4(segments) {
|
|
323
|
-
if (!segments.slice(0, 5).every((group) => group === 0) || segments[5] !== 65535)
|
|
324
|
-
return;
|
|
325
|
-
return [segments[6] >> 8, segments[6] & 255, segments[7] >> 8, segments[7] & 255];
|
|
326
|
-
}
|
|
327
|
-
function ipv4Scope(octets) {
|
|
328
|
-
const [a, b] = octets;
|
|
329
|
-
if (a === 127)
|
|
330
|
-
return "loopback";
|
|
331
|
-
if (a === 100 && b >= 64 && b <= 127)
|
|
332
|
-
return "tailnet";
|
|
333
|
-
if (a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254) {
|
|
334
|
-
return "lan";
|
|
335
|
-
}
|
|
336
|
-
return "public";
|
|
337
|
-
}
|
|
338
|
-
function ipv6Scope(segments) {
|
|
339
|
-
if (segments[0] === 64890 && segments[1] === 4444 && segments[2] === 41440)
|
|
340
|
-
return "tailnet";
|
|
341
|
-
if ((segments[0] & 65024) === 64512 || (segments[0] & 65472) === 65152)
|
|
342
|
-
return "lan";
|
|
343
|
-
if (segments.slice(0, 7).every((group) => group === 0) && segments[7] === 1)
|
|
344
|
-
return "loopback";
|
|
345
|
-
return "public";
|
|
346
|
-
}
|
|
347
|
-
var RTC_DEFAULT_STUN = ["stun:stun.cloudflare.com:3478"];
|
|
348
|
-
var RTC_DATA_CHANNEL_LABEL = "pinomad";
|
|
349
|
-
var RTC_MAX_RECORD_BYTES = 65535;
|
|
350
|
-
var RTC_DEFAULT_MAX_MESSAGE_SIZE = 65536;
|
|
351
|
-
var RTC_MIN_MAX_MESSAGE_SIZE = RTC_MAX_RECORD_BYTES;
|
|
352
|
-
function isRtcHello(value) {
|
|
353
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
354
|
-
return false;
|
|
355
|
-
const hello = value;
|
|
356
|
-
return Array.isArray(hello.stun) && hello.stun.every((server) => typeof server === "string" && server.length > 0) && hello.signalOnly === true;
|
|
357
|
-
}
|
|
358
|
-
function rtcCandidateType(candidate) {
|
|
359
|
-
const parts = candidate.trim().split(/\s+/);
|
|
360
|
-
const at = parts.indexOf("typ");
|
|
361
|
-
return at === -1 ? undefined : parts[at + 1];
|
|
362
|
-
}
|
|
363
|
-
function sdpCandidates(sdp) {
|
|
364
|
-
return sdp.split(/\r?\n/).filter((line) => line.startsWith("a=candidate:")).map((line) => line.slice("a=".length));
|
|
365
|
-
}
|
|
366
|
-
function withRtcCandidates(sdp, candidates) {
|
|
367
|
-
const lines = sdp.split(/\r?\n/).filter((line) => line !== "" && !line.startsWith("a=candidate:") && line !== "a=end-of-candidates");
|
|
368
|
-
const unique = [...new Set(candidates.map((candidate) => candidate.replace(/^a=/, "")))];
|
|
369
|
-
return [...lines, ...unique.map((candidate) => `a=${candidate}`), "a=end-of-candidates", ""].join(`\r
|
|
370
|
-
`);
|
|
371
|
-
}
|
|
372
|
-
function parseRtcMaxMessageSize(sdp) {
|
|
373
|
-
const line = sdp.split(/\r?\n/).find((entry) => entry.startsWith("a=max-message-size:"));
|
|
374
|
-
if (line === undefined)
|
|
375
|
-
return { kind: "bounded", bytes: RTC_DEFAULT_MAX_MESSAGE_SIZE };
|
|
376
|
-
const raw = line.slice("a=max-message-size:".length).trim();
|
|
377
|
-
if (!/^\d+$/.test(raw))
|
|
378
|
-
return { kind: "invalid", reason: `malformed a=max-message-size: ${raw}` };
|
|
379
|
-
const bytes = Number(raw);
|
|
380
|
-
if (!Number.isSafeInteger(bytes))
|
|
381
|
-
return { kind: "invalid", reason: `a=max-message-size is not a safe integer: ${raw}` };
|
|
382
|
-
if (bytes === 0)
|
|
383
|
-
return { kind: "unlimited" };
|
|
384
|
-
if (bytes < RTC_MIN_MAX_MESSAGE_SIZE) {
|
|
385
|
-
return { kind: "invalid", reason: `a=max-message-size ${bytes} cannot carry a ${RTC_MIN_MAX_MESSAGE_SIZE}-byte record` };
|
|
386
|
-
}
|
|
387
|
-
return { kind: "bounded", bytes };
|
|
388
|
-
}
|
|
389
|
-
|
|
390
418
|
// packages/protocol/src/attachments.ts
|
|
391
419
|
var MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
392
420
|
var MAX_ATTACHMENTS_PER_MESSAGE = 10;
|
|
@@ -619,4 +647,4 @@ function homeOf(index, summaries, id) {
|
|
|
619
647
|
return;
|
|
620
648
|
}
|
|
621
649
|
|
|
622
|
-
export {
|
|
650
|
+
export { rtcSelectedPair, RTC_DEFAULT_STUN, RTC_DATA_CHANNEL_LABEL, RTC_MAX_RECORD_BYTES, isRtcHello, rtcCandidateType, sdpCandidates, withRtcCandidates, parseRtcMaxMessageSize, isCustomProviderApi, HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame, MAX_ATTACHMENTS_PER_MESSAGE, MAX_PENDING_ATTACHMENTS, MAX_PENDING_ATTACHMENT_BYTES, ATTACHMENT_TTL_MS, MAX_ATTACHMENT_NAME_LENGTH, isAttachmentId, base64DecodedLength, attachmentBeginProblem, attachmentChunkProblem, attachmentBlock, parseAttachmentBlock, organize, checkoutOf, homeOf };
|