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.
Files changed (98) hide show
  1. package/README.md +11 -9
  2. package/dist/chunk-41q0saz0.js +73 -0
  3. package/dist/chunk-42nqtx5r.js +22 -0
  4. package/dist/{chunk-wb10efyq.js → chunk-445cevpm.js} +1 -1
  5. package/dist/{chunk-jgd38rna.js → chunk-47ff1d1t.js} +206 -178
  6. package/dist/chunk-5qf1grkk.js +220 -0
  7. package/dist/{chunk-4h7pkkzr.js → chunk-7mn38n4f.js} +3 -3
  8. package/dist/{chunk-zehwx009.js → chunk-7wj0jh3t.js} +1 -1
  9. package/dist/{chunk-5791mb0b.js → chunk-agq4tn9r.js} +47 -23
  10. package/dist/{chunk-c5p9xkd8.js → chunk-aqj9wfmb.js} +3 -3
  11. package/dist/{chunk-cx8k63by.js → chunk-g3ahp7b9.js} +12 -1
  12. package/dist/chunk-gg2dcqbg.js +458 -0
  13. package/dist/{chunk-73efzaf1.js → chunk-jeqsdcj1.js} +30 -13
  14. package/dist/{chunk-czj9nfrz.js → chunk-ksfxqd8s.js} +500 -41
  15. package/dist/{chunk-p978qj9m.js → chunk-mnmqyq7d.js} +565 -39
  16. package/dist/{chunk-4sepcfpy.js → chunk-np4jd070.js} +155 -3
  17. package/dist/{chunk-v414b5pp.js → chunk-nppgxh6c.js} +2 -2
  18. package/dist/chunk-nq57fdnq.js +106 -0
  19. package/dist/{chunk-sygeb9qb.js → chunk-pqe7q1s7.js} +1 -1
  20. package/dist/{chunk-w2m9xkks.js → chunk-t5abqsva.js} +199 -72
  21. package/dist/{chunk-8gywfw5n.js → chunk-t91zt5f0.js} +2 -51
  22. package/dist/{chunk-vax4zspw.js → chunk-vqkb70d7.js} +3 -3
  23. package/dist/chunk-vx7dy07y.js +26 -0
  24. package/dist/pinomad.js +19 -12
  25. package/package.json +2 -1
  26. package/web/assets/astro-DOly5024.js +1 -0
  27. package/web/assets/c-BIGW1oBm.js +1 -0
  28. package/web/assets/core-DnpzcQzs.js +4 -0
  29. package/web/assets/cpp-DddhVWhK.js +1 -0
  30. package/web/assets/csharp-B_TYUkTI.js +1 -0
  31. package/web/assets/css-Z72CF80r.js +1 -0
  32. package/web/assets/dart-CtKP3W-j.js +1 -0
  33. package/web/assets/diff-D97Zzqfu.js +1 -0
  34. package/web/assets/dockerfile-BcOcwvcX.js +1 -0
  35. package/web/assets/elixir-DkJLSxtU.js +1 -0
  36. package/web/assets/engine-javascript-BHPwbUfN.js +141 -0
  37. package/web/assets/engine-oniguruma-CQjQPVHy.js +1 -0
  38. package/web/assets/fish-BvzEVeQv.js +1 -0
  39. package/web/assets/go-C27-OAKa.js +1 -0
  40. package/web/assets/graphql-ChdNCCLP.js +1 -0
  41. package/web/assets/haskell-Df6bDoY_.js +1 -0
  42. package/web/assets/hcl-BWvSN4gD.js +1 -0
  43. package/web/assets/highlighted-diff-Dn5ifsvK.js +175 -0
  44. package/web/assets/highlights-DnHv6NBM.js +4 -0
  45. package/web/assets/highlights-themes-CUdrlZk3.js +1 -0
  46. package/web/assets/html-BlS82ilI.js +1 -0
  47. package/web/assets/{index-DoyHMPwf.css → index-BV0ecolo.css} +1 -1
  48. package/web/assets/index-DNsTTgPE.js +396 -0
  49. package/web/assets/ini-BEwlwnbL.js +1 -0
  50. package/web/assets/java-CylS5w8V.js +1 -0
  51. package/web/assets/javascript-wDzz0qaB.js +1 -0
  52. package/web/assets/json-Cp-IABpG.js +1 -0
  53. package/web/assets/jsonc-Des-eS-w.js +1 -0
  54. package/web/assets/jsx-g9-lgVsj.js +1 -0
  55. package/web/assets/kotlin-BdnUsdx6.js +1 -0
  56. package/web/assets/less-B1dDrJ26.js +1 -0
  57. package/web/assets/lua-BaeVxFsk.js +1 -0
  58. package/web/assets/make-CHLpvVh8.js +1 -0
  59. package/web/assets/markdown-Cvjx9yec.js +1 -0
  60. package/web/assets/nix-CwoSXNpI.js +1 -0
  61. package/web/assets/objective-c-DXmwc3jG.js +1 -0
  62. package/web/assets/php-DoruYMF0.js +1 -0
  63. package/web/assets/pierre-dark-CyvmCCZW.js +1 -0
  64. package/web/assets/pierre-dark-VKo95ZgA.js +1 -0
  65. package/web/assets/pierre-light-480U9XYS.js +1 -0
  66. package/web/assets/pierre-light-BVSW-Xev.js +1 -0
  67. package/web/assets/postcss-DnzrXTVv.js +1 -0
  68. package/web/assets/powershell-Dd0kTxKP.js +1 -0
  69. package/web/assets/prisma-Vru482bI.js +1 -0
  70. package/web/assets/proto-C7zT0LnQ.js +1 -0
  71. package/web/assets/python-B6aJPvgy.js +1 -0
  72. package/web/assets/ruby-B43yHlf9.js +1 -0
  73. package/web/assets/rust-DPvrY3IQ.js +1 -0
  74. package/web/assets/scala-CqE71os6.js +1 -0
  75. package/web/assets/scss-Cw6XMfVs.js +1 -0
  76. package/web/assets/shellscript-Yzrsuije.js +1 -0
  77. package/web/assets/shiki-BuPP5HCg.js +2 -0
  78. package/web/assets/shiki-langs-FJk0v0Ga.js +2 -0
  79. package/web/assets/shiki-wasm-CgFRq91U.js +1 -0
  80. package/web/assets/sql-DkvoVjww.js +1 -0
  81. package/web/assets/stream-tokenizer-Bl6bisIa.js +4 -0
  82. package/web/assets/svelte-DoIXUgJc.js +1 -0
  83. package/web/assets/swift-C2oV4EkX.js +1 -0
  84. package/web/assets/terraform-BETggiCN.js +1 -0
  85. package/web/assets/theming-themes-BL_CC-Mm.js +1 -0
  86. package/web/assets/toml-vGWfd6FD.js +1 -0
  87. package/web/assets/tsx-COt5Ahok.js +1 -0
  88. package/web/assets/typescript-BPQ3VLAy.js +1 -0
  89. package/web/assets/vue-CwSeJimo.js +1 -0
  90. package/web/assets/xml-sdJ4AIDG.js +1 -0
  91. package/web/assets/xterm-view-6GBZ9nXN.css +32 -0
  92. package/web/assets/xterm-view-ZsuzJb2C.js +38 -0
  93. package/web/assets/yaml-Buea-lGh.js +1 -0
  94. package/web/assets/zig-VOosw3JB.js +1 -0
  95. package/web/index.html +2 -2
  96. package/dist/chunk-bhtx2c8a.js +0 -16
  97. package/dist/chunk-c05g401k.js +0 -170
  98. 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
- 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` 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
- 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
 
@@ -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-sygeb9qb.js";
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: 25 };
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
- const refs = value.type === "subscribe" && value.images === "ref";
94
- return { kind: "frame", frame: refs ? { type: "subscribe", stream, images: "ref" } : { type: value.type, stream } };
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:.+:)[1-9]\d*$/.test(stream);
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 { isCustomProviderApi, HOST_STREAM_NAMES, hostStreams, conversationStream, docStream, UNAUTHORIZED_CLOSE_CODE, INVALID_FRAME_CLOSE_CODE, PROTOCOL, hostSupportsCall, hostSupportsFeature, parseClientFrame, rtcSelectedPair, RTC_DEFAULT_STUN, RTC_DATA_CHANNEL_LABEL, RTC_MAX_RECORD_BYTES, isRtcHello, rtcCandidateType, sdpCandidates, withRtcCandidates, parseRtcMaxMessageSize, 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 };
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 };