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
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOST_SETTING_KEYS,
|
|
3
|
+
isQueueMode,
|
|
4
|
+
isThinkingLevel
|
|
5
|
+
} from "./chunk-sygeb9qb.js";
|
|
6
|
+
|
|
7
|
+
// packages/protocol/src/auth.ts
|
|
8
|
+
var CUSTOM_PROVIDER_APIS = ["openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai"];
|
|
9
|
+
var isCustomProviderApi = (value) => CUSTOM_PROVIDER_APIS.includes(value);
|
|
10
|
+
|
|
11
|
+
// packages/protocol/src/frames.ts
|
|
12
|
+
var HOST_STREAMS = { conversations: 0, index: 0, devices: 0, mcp: 0, models: 1, auth: 1, settings: 2, agents: 6, providers: 21 };
|
|
13
|
+
var HOST_STREAM_NAMES = Object.keys(HOST_STREAMS);
|
|
14
|
+
function hostStreams(hostMinor) {
|
|
15
|
+
return HOST_STREAM_NAMES.filter((name) => HOST_STREAMS[name] <= hostMinor);
|
|
16
|
+
}
|
|
17
|
+
var conversationStream = (id) => `conversation:${id}`;
|
|
18
|
+
var docStream = (kind, id) => `doc:${kind}:${id}`;
|
|
19
|
+
var UNAUTHORIZED_CLOSE_CODE = 4401;
|
|
20
|
+
var INVALID_FRAME_CLOSE_CODE = 4400;
|
|
21
|
+
var PROTOCOL = { major: 5, minor: 25 };
|
|
22
|
+
var CALL_SINCE = {
|
|
23
|
+
addProject: 0,
|
|
24
|
+
removeProject: 0,
|
|
25
|
+
listFolders: 3,
|
|
26
|
+
listFiles: 10,
|
|
27
|
+
listSkills: 13,
|
|
28
|
+
listCommands: 17,
|
|
29
|
+
createConversation: 0,
|
|
30
|
+
archive: 0,
|
|
31
|
+
renameConversation: 4,
|
|
32
|
+
submit: 0,
|
|
33
|
+
submission: 25,
|
|
34
|
+
withdrawQueued: 14,
|
|
35
|
+
steerQueued: 14,
|
|
36
|
+
abort: 0,
|
|
37
|
+
compact: 0,
|
|
38
|
+
runCommand: 17,
|
|
39
|
+
setModel: 0,
|
|
40
|
+
setThinkingLevel: 0,
|
|
41
|
+
setAgent: 18,
|
|
42
|
+
fork: 0,
|
|
43
|
+
answer: 0,
|
|
44
|
+
changes: 0,
|
|
45
|
+
createPairing: 0,
|
|
46
|
+
revokeDevice: 0,
|
|
47
|
+
logout: 1,
|
|
48
|
+
beginLogin: 21,
|
|
49
|
+
answerLogin: 21,
|
|
50
|
+
cancelLogin: 21,
|
|
51
|
+
saveCustomProvider: 21,
|
|
52
|
+
removeCustomProvider: 21,
|
|
53
|
+
listEndpointModels: 21,
|
|
54
|
+
updateSettings: 2,
|
|
55
|
+
createAgent: 6,
|
|
56
|
+
updateAgent: 6,
|
|
57
|
+
deleteAgent: 6,
|
|
58
|
+
setPushToken: 7,
|
|
59
|
+
image: 15,
|
|
60
|
+
beginAttachment: 19,
|
|
61
|
+
appendAttachment: 19,
|
|
62
|
+
rtcOffer: 23,
|
|
63
|
+
ping: 23
|
|
64
|
+
};
|
|
65
|
+
function hostSupportsCall(hostMinor, method) {
|
|
66
|
+
return hostMinor >= CALL_SINCE[method];
|
|
67
|
+
}
|
|
68
|
+
var FEATURE_SINCE = {
|
|
69
|
+
images: 5,
|
|
70
|
+
acceptAccountDevices: 8,
|
|
71
|
+
apnsPush: 9,
|
|
72
|
+
agentConversations: 10,
|
|
73
|
+
liveBranch: 11,
|
|
74
|
+
hostName: 12,
|
|
75
|
+
imageRefs: 15,
|
|
76
|
+
boundSkills: 16,
|
|
77
|
+
attachments: 19,
|
|
78
|
+
imageVariants: 20,
|
|
79
|
+
rtc: 23,
|
|
80
|
+
submitEntry: 25
|
|
81
|
+
};
|
|
82
|
+
function hostSupportsFeature(hostMinor, feature) {
|
|
83
|
+
return hostMinor >= FEATURE_SINCE[feature];
|
|
84
|
+
}
|
|
85
|
+
function parseClientFrame(value) {
|
|
86
|
+
if (!record(value) || typeof value.type !== "string")
|
|
87
|
+
return { kind: "malformed" };
|
|
88
|
+
if (value.type === "subscribe" || value.type === "unsubscribe") {
|
|
89
|
+
const stream = value.stream;
|
|
90
|
+
if (typeof stream !== "string")
|
|
91
|
+
return { kind: "malformed" };
|
|
92
|
+
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 } };
|
|
95
|
+
}
|
|
96
|
+
const prefix = stream.slice(0, stream.indexOf(":") === -1 ? undefined : stream.indexOf(":"));
|
|
97
|
+
if (STREAM_KINDS.has(prefix))
|
|
98
|
+
return { kind: "malformed" };
|
|
99
|
+
return { kind: "unsupportedStream", type: value.type, stream };
|
|
100
|
+
}
|
|
101
|
+
if (value.type !== "call")
|
|
102
|
+
return { kind: "unknownType" };
|
|
103
|
+
const { id, method, args } = value;
|
|
104
|
+
if (typeof id !== "number" || !Number.isSafeInteger(id) || !record(args) || typeof method !== "string") {
|
|
105
|
+
return { kind: "malformed" };
|
|
106
|
+
}
|
|
107
|
+
if (!isCallMethod(method))
|
|
108
|
+
return { kind: "unsupportedCall", id, method };
|
|
109
|
+
return validCallArgs(method, args) ? { kind: "frame", frame: { type: "call", id, method, args } } : { kind: "malformed" };
|
|
110
|
+
}
|
|
111
|
+
var STREAM_KINDS = new Set(["conversation", "doc", "tasks", ...HOST_STREAM_NAMES]);
|
|
112
|
+
function isStreamName(stream) {
|
|
113
|
+
return stream === "tasks" || HOST_STREAM_NAMES.includes(stream) || /^(?:conversation:|doc:.+:)[1-9]\d*$/.test(stream);
|
|
114
|
+
}
|
|
115
|
+
var isCallMethod = (method) => CALL_METHODS.includes(method);
|
|
116
|
+
var CALL_METHODS = Object.keys(CALL_SINCE);
|
|
117
|
+
var SETTING_VALIDATORS = {
|
|
118
|
+
defaultModel: (value) => value === null || record(value) && nonempty(value.provider) && nonempty(value.modelId),
|
|
119
|
+
defaultThinkingLevel: (value) => value === null || isThinkingLevel(value),
|
|
120
|
+
steeringMode: (value) => value === null || isQueueMode(value),
|
|
121
|
+
followUpMode: (value) => value === null || isQueueMode(value),
|
|
122
|
+
acceptAccountDevices: (value) => value === null || typeof value === "boolean",
|
|
123
|
+
rtcStun: (value) => value === null || Array.isArray(value) && value.every(nonempty),
|
|
124
|
+
hostName: (value) => value === null || typeof value === "string"
|
|
125
|
+
};
|
|
126
|
+
function validCallArgs(method, args) {
|
|
127
|
+
const conversationId = () => typeof args.conversationId === "number" && Number.isSafeInteger(args.conversationId) && args.conversationId >= 1;
|
|
128
|
+
const submissionId = () => typeof args.submissionId === "number" && Number.isSafeInteger(args.submissionId) && args.submissionId >= 1;
|
|
129
|
+
const images = () => args.images === undefined || Array.isArray(args.images) && args.images.every((image) => record(image) && ("data" in image ? typeof image.data === "string" && typeof image.mimeType === "string" : nonempty(image.ref)));
|
|
130
|
+
const skills = () => args.skills === undefined || Array.isArray(args.skills) && args.skills.every(nonempty);
|
|
131
|
+
const attachments = () => args.attachments === undefined || Array.isArray(args.attachments) && args.attachments.every(nonempty);
|
|
132
|
+
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");
|
|
133
|
+
switch (method) {
|
|
134
|
+
case "submit":
|
|
135
|
+
return conversationId() && typeof args.text === "string" && (args.whenBusy === "steer" || args.whenBusy === "followUp") && nonempty(args.requestId) && images() && skills() && attachments();
|
|
136
|
+
case "submission":
|
|
137
|
+
return conversationId() && nonempty(args.requestId);
|
|
138
|
+
case "withdrawQueued":
|
|
139
|
+
case "steerQueued":
|
|
140
|
+
return conversationId() && submissionId();
|
|
141
|
+
case "abort":
|
|
142
|
+
return conversationId();
|
|
143
|
+
case "setThinkingLevel":
|
|
144
|
+
return conversationId() && nonempty(args.level);
|
|
145
|
+
case "setAgent":
|
|
146
|
+
return conversationId() && (args.agent === null || nonempty(args.agent));
|
|
147
|
+
case "compact":
|
|
148
|
+
return conversationId() && (args.instructions === undefined || typeof args.instructions === "string");
|
|
149
|
+
case "listCommands":
|
|
150
|
+
return conversationId();
|
|
151
|
+
case "runCommand":
|
|
152
|
+
return conversationId() && nonempty(args.name) && (args.args === undefined || typeof args.args === "string");
|
|
153
|
+
case "setModel":
|
|
154
|
+
return conversationId() && record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId);
|
|
155
|
+
case "fork":
|
|
156
|
+
return conversationId() && typeof args.entryId === "string" && /^[1-9]\d*$/.test(args.entryId) && (args.removeTools === undefined || Array.isArray(args.removeTools) && args.removeTools.every(nonempty));
|
|
157
|
+
case "answer":
|
|
158
|
+
return conversationId() && nonempty(args.kind) && nonempty(args.requestId) && Array.isArray(args.answers) && args.answers.every((answer) => record(answer) && Array.isArray(answer.selected) && answer.selected.every((l) => typeof l === "string") && (answer.other === undefined || typeof answer.other === "string"));
|
|
159
|
+
case "changes":
|
|
160
|
+
return conversationId();
|
|
161
|
+
case "image":
|
|
162
|
+
return nonempty(args.ref) && (args.variant === undefined || args.variant === "thumbnail" || args.variant === "preview");
|
|
163
|
+
case "beginAttachment":
|
|
164
|
+
return nonempty(args.name) && typeof args.size === "number" && Number.isSafeInteger(args.size) && args.size >= 0;
|
|
165
|
+
case "appendAttachment":
|
|
166
|
+
return nonempty(args.attachmentId) && typeof args.offset === "number" && Number.isSafeInteger(args.offset) && args.offset >= 0 && typeof args.data === "string";
|
|
167
|
+
case "addProject":
|
|
168
|
+
case "removeProject":
|
|
169
|
+
return nonempty(args.path);
|
|
170
|
+
case "listFolders":
|
|
171
|
+
return args.path === undefined || nonempty(args.path);
|
|
172
|
+
case "createConversation":
|
|
173
|
+
return record(args.home) && (args.home.kind === "chat" || args.home.kind === "project" && nonempty(args.home.path)) && typeof args.text === "string" && (nonempty(args.text) || Array.isArray(args.images) && args.images.length > 0 || Array.isArray(args.attachments) && args.attachments.length > 0) && nonempty(args.requestId) && images() && skills() && attachments() && (args.checkout === undefined || args.checkout === "worktree" || args.checkout === "project") && (args.model === undefined || record(args.model) && nonempty(args.model.provider) && nonempty(args.model.modelId)) && (args.thinkingLevel === undefined || nonempty(args.thinkingLevel)) && (args.agent === undefined || nonempty(args.agent));
|
|
174
|
+
case "listFiles":
|
|
175
|
+
return typeof args.query === "string" && record(args.scope) && (args.scope.kind === "conversation" && typeof args.scope.conversationId === "number" && Number.isSafeInteger(args.scope.conversationId) && args.scope.conversationId >= 1 || args.scope.kind === "project" && nonempty(args.scope.path) && (args.scope.checkout === "worktree" || args.scope.checkout === "project"));
|
|
176
|
+
case "listSkills":
|
|
177
|
+
return record(args.scope) && (args.scope.kind === "conversation" && typeof args.scope.conversationId === "number" && Number.isSafeInteger(args.scope.conversationId) && args.scope.conversationId >= 1 || args.scope.kind === "project" && nonempty(args.scope.path) && (args.scope.checkout === "worktree" || args.scope.checkout === "project") || args.scope.kind === "chat");
|
|
178
|
+
case "archive":
|
|
179
|
+
return conversationId() && typeof args.archived === "boolean";
|
|
180
|
+
case "renameConversation":
|
|
181
|
+
return conversationId() && typeof args.title === "string";
|
|
182
|
+
case "createPairing":
|
|
183
|
+
return Object.keys(args).length === 0;
|
|
184
|
+
case "rtcOffer":
|
|
185
|
+
return nonempty(args.attempt) && nonempty(args.sdp);
|
|
186
|
+
case "ping":
|
|
187
|
+
return nonempty(args.id);
|
|
188
|
+
case "revokeDevice":
|
|
189
|
+
return nonempty(args.publicKey) && /^[A-Za-z0-9_-]+$/.test(args.publicKey);
|
|
190
|
+
case "logout":
|
|
191
|
+
case "removeCustomProvider":
|
|
192
|
+
return nonempty(args.providerId);
|
|
193
|
+
case "beginLogin":
|
|
194
|
+
return nonempty(args.providerId) && (args.method === "oauth" || args.method === "api_key");
|
|
195
|
+
case "answerLogin":
|
|
196
|
+
return nonempty(args.loginId) && typeof args.promptId === "number" && Number.isSafeInteger(args.promptId) && typeof args.value === "string";
|
|
197
|
+
case "cancelLogin":
|
|
198
|
+
return nonempty(args.loginId);
|
|
199
|
+
case "saveCustomProvider":
|
|
200
|
+
return record(args.provider) && nonempty(args.provider.id) && typeof args.provider.name === "string" && isCustomProviderApi(args.provider.api) && typeof args.provider.baseUrl === "string" && Array.isArray(args.provider.models) && args.provider.models.every((model) => typeof model === "string") && (args.apiKey === undefined || args.apiKey === null || typeof args.apiKey === "string");
|
|
201
|
+
case "listEndpointModels":
|
|
202
|
+
return isCustomProviderApi(args.api) && typeof args.baseUrl === "string" && (args.apiKey === undefined || typeof args.apiKey === "string") && (args.providerId === undefined || nonempty(args.providerId));
|
|
203
|
+
case "createAgent":
|
|
204
|
+
case "updateAgent":
|
|
205
|
+
return role();
|
|
206
|
+
case "deleteAgent":
|
|
207
|
+
return typeof args.name === "string";
|
|
208
|
+
case "setPushToken":
|
|
209
|
+
return args.token === null || typeof args.token === "string" || record(args.token) && typeof args.token.apns === "string" && (args.token.environment === "production" || args.token.environment === "development");
|
|
210
|
+
case "updateSettings":
|
|
211
|
+
return record(args.patch) && Object.keys(args.patch).every((key) => HOST_SETTING_KEYS.includes(key)) && Object.entries(args.patch).every(([key, value]) => SETTING_VALIDATORS[key]?.(value) === true);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function record(value) {
|
|
215
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
216
|
+
}
|
|
217
|
+
function nonempty(value) {
|
|
218
|
+
return typeof value === "string" && value.length > 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
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
|
+
// packages/protocol/src/attachments.ts
|
|
391
|
+
var MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
392
|
+
var MAX_ATTACHMENTS_PER_MESSAGE = 10;
|
|
393
|
+
var ATTACHMENT_CHUNK_BYTES = 512 * 1024;
|
|
394
|
+
var MAX_PENDING_ATTACHMENTS = 20;
|
|
395
|
+
var MAX_PENDING_ATTACHMENT_BYTES = 200 * 1024 * 1024;
|
|
396
|
+
var ATTACHMENT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
397
|
+
var MAX_ATTACHMENT_NAME_LENGTH = 255;
|
|
398
|
+
var ATTACHMENT_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/;
|
|
399
|
+
function isAttachmentId(value) {
|
|
400
|
+
return ATTACHMENT_ID_PATTERN.test(value);
|
|
401
|
+
}
|
|
402
|
+
var ATTACHMENTS_OPEN = '<attachments v="1">';
|
|
403
|
+
var ATTACHMENTS_INSTRUCTION = "The user attached these files. They are read-only originals on this machine: read them with your tools when relevant, and copy one into the working directory before changing it.";
|
|
404
|
+
var ATTACHMENTS_CLOSE = "</attachments>";
|
|
405
|
+
var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
406
|
+
function base64DecodedLength(data) {
|
|
407
|
+
if (data.length % 4 !== 0 || !BASE64.test(data))
|
|
408
|
+
return;
|
|
409
|
+
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
410
|
+
return data.length * 3 / 4 - padding;
|
|
411
|
+
}
|
|
412
|
+
function attachmentBeginProblem(name, size) {
|
|
413
|
+
if (name.trim() === "")
|
|
414
|
+
return "Attachment name cannot be empty";
|
|
415
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
416
|
+
return "Attachment size must be a non-negative integer";
|
|
417
|
+
if (size > MAX_ATTACHMENT_BYTES)
|
|
418
|
+
return `Attachment is larger than ${MAX_ATTACHMENT_BYTES / (1024 * 1024)} MB`;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
function attachmentChunkProblem(data) {
|
|
422
|
+
const size = base64DecodedLength(data);
|
|
423
|
+
if (size === undefined)
|
|
424
|
+
return "Attachment chunk is not valid base64";
|
|
425
|
+
if (size > ATTACHMENT_CHUNK_BYTES)
|
|
426
|
+
return `Attachment chunk is larger than ${ATTACHMENT_CHUNK_BYTES / 1024} KiB`;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
function xmlAttribute(value) {
|
|
430
|
+
let out = "";
|
|
431
|
+
for (let i = 0;i < value.length; i++) {
|
|
432
|
+
const unit = value.charCodeAt(i);
|
|
433
|
+
if (unit === 38)
|
|
434
|
+
out += "&";
|
|
435
|
+
else if (unit === 60)
|
|
436
|
+
out += "<";
|
|
437
|
+
else if (unit === 62)
|
|
438
|
+
out += ">";
|
|
439
|
+
else if (unit === 34)
|
|
440
|
+
out += """;
|
|
441
|
+
else if (unit < 32 || unit === 127)
|
|
442
|
+
out += `&#x${unit.toString(16)};`;
|
|
443
|
+
else
|
|
444
|
+
out += value[i];
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
function attachmentBlock(files) {
|
|
449
|
+
const lines = [
|
|
450
|
+
ATTACHMENTS_OPEN,
|
|
451
|
+
ATTACHMENTS_INSTRUCTION,
|
|
452
|
+
...files.map((file) => `<file id="${xmlAttribute(file.id)}" name="${xmlAttribute(file.name)}" path="${xmlAttribute(file.path)}" size="${file.size}"/>`),
|
|
453
|
+
ATTACHMENTS_CLOSE
|
|
454
|
+
];
|
|
455
|
+
return lines.join(`
|
|
456
|
+
`);
|
|
457
|
+
}
|
|
458
|
+
var FILE_OPEN = '<file id="';
|
|
459
|
+
var FILE_NAME = '" name="';
|
|
460
|
+
var FILE_PATH = '" path="';
|
|
461
|
+
var FILE_SIZE = '" size="';
|
|
462
|
+
var FILE_END = '"/>';
|
|
463
|
+
var SIZE = /^(?:0|[1-9][0-9]*)$/;
|
|
464
|
+
function decodeXmlAttribute(value) {
|
|
465
|
+
return value.replace(/&(amp|lt|gt|quot|apos|#x[0-9A-Fa-f]+|#[0-9]+);/g, (entity, body) => {
|
|
466
|
+
if (body === "amp")
|
|
467
|
+
return "&";
|
|
468
|
+
if (body === "lt")
|
|
469
|
+
return "<";
|
|
470
|
+
if (body === "gt")
|
|
471
|
+
return ">";
|
|
472
|
+
if (body === "quot")
|
|
473
|
+
return '"';
|
|
474
|
+
if (body === "apos")
|
|
475
|
+
return "'";
|
|
476
|
+
const code = body.startsWith("#x") ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
|
|
477
|
+
return code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : entity;
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function quoted(text, at, marker) {
|
|
481
|
+
if (!text.startsWith(marker, at))
|
|
482
|
+
return;
|
|
483
|
+
const start = at + marker.length;
|
|
484
|
+
const end = text.indexOf('"', start);
|
|
485
|
+
if (end === -1)
|
|
486
|
+
return;
|
|
487
|
+
return { value: text.slice(start, end), next: end };
|
|
488
|
+
}
|
|
489
|
+
function parseFileLine(line) {
|
|
490
|
+
const id = quoted(line, 0, FILE_OPEN);
|
|
491
|
+
if (id === undefined)
|
|
492
|
+
return;
|
|
493
|
+
const name = quoted(line, id.next, FILE_NAME);
|
|
494
|
+
if (name === undefined)
|
|
495
|
+
return;
|
|
496
|
+
const path = quoted(line, name.next, FILE_PATH);
|
|
497
|
+
if (path === undefined)
|
|
498
|
+
return;
|
|
499
|
+
const size = quoted(line, path.next, FILE_SIZE);
|
|
500
|
+
if (size === undefined || !line.startsWith(FILE_END, size.next) || size.next + FILE_END.length !== line.length) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (!isAttachmentId(id.value) || !SIZE.test(size.value))
|
|
504
|
+
return;
|
|
505
|
+
const bytes = Number(size.value);
|
|
506
|
+
if (!Number.isSafeInteger(bytes))
|
|
507
|
+
return;
|
|
508
|
+
return { id: id.value, name: decodeXmlAttribute(name.value), size: bytes };
|
|
509
|
+
}
|
|
510
|
+
function parseAttachmentBlock(text) {
|
|
511
|
+
if (!text.startsWith(ATTACHMENTS_OPEN + `
|
|
512
|
+
`) || !text.endsWith(`
|
|
513
|
+
` + ATTACHMENTS_CLOSE))
|
|
514
|
+
return;
|
|
515
|
+
const lines = text.slice(ATTACHMENTS_OPEN.length + 1, -(ATTACHMENTS_CLOSE.length + 1)).split(`
|
|
516
|
+
`);
|
|
517
|
+
if (lines[0] !== ATTACHMENTS_INSTRUCTION || lines.length < 2)
|
|
518
|
+
return;
|
|
519
|
+
const files = [];
|
|
520
|
+
for (const line of lines.slice(1)) {
|
|
521
|
+
const file = parseFileLine(line);
|
|
522
|
+
if (file === undefined)
|
|
523
|
+
return;
|
|
524
|
+
files.push(file);
|
|
525
|
+
}
|
|
526
|
+
return files;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// packages/protocol/src/organization.ts
|
|
530
|
+
function organize(index, summaries) {
|
|
531
|
+
const summaryById = new Map(summaries.map((summary) => [summary.id, summary]));
|
|
532
|
+
const nodes = new Map;
|
|
533
|
+
const topLevel = new Map;
|
|
534
|
+
for (const entry of index.conversations) {
|
|
535
|
+
const home = entry.home;
|
|
536
|
+
if (home.kind === "project" && !index.projects.some((project) => project.path === home.path))
|
|
537
|
+
continue;
|
|
538
|
+
const summary = summaryById.get(entry.id);
|
|
539
|
+
if (summary === undefined)
|
|
540
|
+
continue;
|
|
541
|
+
topLevel.set(entry.id, entry);
|
|
542
|
+
nodes.set(entry.id, { summary, children: [] });
|
|
543
|
+
}
|
|
544
|
+
for (const summary of summaries) {
|
|
545
|
+
const chain = [summary];
|
|
546
|
+
let current = summary;
|
|
547
|
+
while (!topLevel.has(current.id)) {
|
|
548
|
+
const parent = current.parent === undefined ? undefined : summaryById.get(current.parent);
|
|
549
|
+
if (parent === undefined || chain.includes(parent))
|
|
550
|
+
break;
|
|
551
|
+
chain.push(parent);
|
|
552
|
+
current = parent;
|
|
553
|
+
}
|
|
554
|
+
if (!topLevel.has(current.id))
|
|
555
|
+
continue;
|
|
556
|
+
for (let i = chain.length - 1;i >= 0; i--) {
|
|
557
|
+
const member = chain[i];
|
|
558
|
+
if (!nodes.has(member.id))
|
|
559
|
+
nodes.set(member.id, { summary: member, children: [] });
|
|
560
|
+
if (i < chain.length - 1) {
|
|
561
|
+
const parent = nodes.get(chain[i + 1].id);
|
|
562
|
+
const child = nodes.get(member.id);
|
|
563
|
+
if (!parent.children.includes(child))
|
|
564
|
+
parent.children.push(child);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const byIdAscending = (a, b) => a.summary.id - b.summary.id;
|
|
569
|
+
const nodeList = (entry) => sortChildren(nodes.get(entry.id));
|
|
570
|
+
const sortChildren = (node) => ({
|
|
571
|
+
summary: node.summary,
|
|
572
|
+
children: [...node.children].sort(byIdAscending).map(sortChildren)
|
|
573
|
+
});
|
|
574
|
+
const sorted = [...topLevel.values()].sort((a, b) => b.createdAt - a.createdAt);
|
|
575
|
+
const entries = sorted.filter((entry) => entry.archived !== true);
|
|
576
|
+
return {
|
|
577
|
+
chats: entries.filter((entry) => entry.home.kind === "chat").map(nodeList),
|
|
578
|
+
projects: [...index.projects].sort((a, b) => b.addedAt - a.addedAt).map((project) => ({
|
|
579
|
+
project,
|
|
580
|
+
conversations: entries.filter((entry) => entry.home.kind === "project" && entry.home.path === project.path).map(nodeList)
|
|
581
|
+
})),
|
|
582
|
+
archived: sorted.filter((entry) => entry.archived === true).map((entry) => {
|
|
583
|
+
const home = entry.home;
|
|
584
|
+
const project = home.kind === "project" ? index.projects.find((each) => each.path === home.path) : undefined;
|
|
585
|
+
return { node: nodeList(entry), ...project === undefined ? {} : { project } };
|
|
586
|
+
})
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function checkoutOf(index, summaries, id) {
|
|
590
|
+
const record2 = index.checkouts?.find((checkout) => checkout.conversationId === id);
|
|
591
|
+
if (record2 !== undefined)
|
|
592
|
+
return record2;
|
|
593
|
+
const summaryById = new Map(summaries.map((summary) => [summary.id, summary]));
|
|
594
|
+
const seen = new Set;
|
|
595
|
+
let current = summaryById.get(id);
|
|
596
|
+
while (current !== undefined && !seen.has(current.id)) {
|
|
597
|
+
seen.add(current.id);
|
|
598
|
+
const ancestor = index.checkouts?.find((checkout) => checkout.conversationId === current.id);
|
|
599
|
+
if (ancestor !== undefined)
|
|
600
|
+
return ancestor;
|
|
601
|
+
current = current.parent === undefined ? undefined : summaryById.get(current.parent);
|
|
602
|
+
}
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
function homeOf(index, summaries, id) {
|
|
606
|
+
const entry = index.conversations.find((candidate) => candidate.id === id);
|
|
607
|
+
if (entry !== undefined)
|
|
608
|
+
return entry.home;
|
|
609
|
+
const summaryById = new Map(summaries.map((summary) => [summary.id, summary]));
|
|
610
|
+
const seen = new Set;
|
|
611
|
+
let current = summaryById.get(id);
|
|
612
|
+
while (current !== undefined && !seen.has(current.id)) {
|
|
613
|
+
seen.add(current.id);
|
|
614
|
+
const ancestor = index.conversations.find((candidate) => candidate.id === current.id);
|
|
615
|
+
if (ancestor !== undefined)
|
|
616
|
+
return ancestor.home;
|
|
617
|
+
current = current.parent === undefined ? undefined : summaryById.get(current.parent);
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
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 };
|