pinomad 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{chunk-9z7echn7.js → chunk-5712fvyp.js} +4 -4
- package/dist/{chunk-y73t2jmc.js → chunk-5h97zxag.js} +119 -41
- package/dist/{chunk-zx77w1nm.js → chunk-d9q0dz8s.js} +2 -2
- package/dist/{chunk-mqbg2cbw.js → chunk-fhtkkcgt.js} +390 -158
- package/dist/{chunk-wgn3cx5q.js → chunk-h35fermn.js} +48 -12
- package/dist/{chunk-tpd3trvd.js → chunk-haycqqp1.js} +1 -1
- package/dist/{chunk-8h3wrn84.js → chunk-n4bvmcy8.js} +4 -4
- package/dist/{chunk-hsdzb73r.js → chunk-q26n39mf.js} +10 -4
- package/dist/{chunk-ne6a5q2j.js → chunk-qdxhqva8.js} +1 -1
- package/dist/{chunk-fpb6f9n5.js → chunk-rkfdsacq.js} +1 -1
- package/dist/pinomad.js +7 -7
- package/package.json +1 -1
- package/web/assets/index-4p91UVoc.js +383 -0
- package/web/assets/index-KwICnYux.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-B9yvK0AN.js +0 -383
- package/web/assets/index-t5dAiohJ.css +0 -1
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ bun run pair # 打印配对二维码(一次性,5 分钟过期)
|
|
|
89
89
|
bun run host -- --remote-port 7422 --public-url https://pinomad.example.com
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
远程端口不接受 token
|
|
92
|
+
远程端口不接受 token:只有完成配对、公钥已登记在设备表里的客户端能连。不开 `--remote-port` / `--relay` 时 `pair` 打印的是回环链接(回环端口 `/secure` 路径上的安全通道),只能给本机上的客户端用,比如桌面端 App。桌面端 App 可以配对多台宿主,在侧栏底部切换(一次只连一台)。App 会自动检查更新(GitHub Release 上的 `latest-mac.yml`,自动下载、不自动安装),下载完成后侧栏底部出现 "Restart to update",也可以在菜单里手动 Check for Updates…。
|
|
93
93
|
|
|
94
94
|
</details>
|
|
95
95
|
|
|
@@ -209,7 +209,9 @@ bun run build
|
|
|
209
209
|
|
|
210
210
|
## 发布(ADR-0016)
|
|
211
211
|
|
|
212
|
-
推 `v*` tag 触发 `.github/workflows/release.yml
|
|
212
|
+
推 `v*` tag 触发 `.github/workflows/release.yml`,三个 job:`verify` 跑 typecheck / test / build、打包、`npm pack`、装包冒烟;`macos`(macos-15)签名 + 公证出 arm64 的 dmg/zip,挂镜像验证签名和公证票据,再跑一遍 `desktop:e2e`;`publish` 先把两者做成 **draft** GitHub Release(`PiNomad <version>`,自动生成 release notes,预发布 tag 打 `--prerelease`),上传 dmg / zip / blockmap / `latest-mac.yml` / `SHA256SUMS.txt`,然后 `npm publish`,全成了才解除 draft。`latest-mac.yml` 就是桌面端自动更新的更新源。带 `-` 的预发布 tag(`v0.2.0-beta.1`)发到 `next` 且不是 latest release,正式版发到 `latest`。发布用 npm trusted publishing(OIDC),仓库里没有 npm token。
|
|
213
|
+
|
|
214
|
+
macOS 签名和公证需要这些仓库 secrets:`MAC_CSC_LINK`(开发者证书的 base64 .p12)、`MAC_CSC_KEY_PASSWORD`(p12 密码)、`APPLE_API_KEY`(App Store Connect 的 .p8 内容)、`APPLE_API_KEY_ID`、`APPLE_API_ISSUER`、`APPLE_TEAM_ID`。`workflow_dispatch` 手动触发也跑完整签名公证(版本号 `0.0.0-dryrun`),只是不会走 `publish` job,方便干跑验证发布链。
|
|
213
215
|
|
|
214
216
|
```sh
|
|
215
217
|
git tag v0.1.1
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
main,
|
|
4
4
|
restartService
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import"./chunk-
|
|
7
|
-
import"./chunk-
|
|
8
|
-
import"./chunk-
|
|
5
|
+
} from "./chunk-d9q0dz8s.js";
|
|
6
|
+
import"./chunk-rkfdsacq.js";
|
|
7
|
+
import"./chunk-5h97zxag.js";
|
|
8
|
+
import"./chunk-h35fermn.js";
|
|
9
9
|
import"./chunk-cjfmfyjb.js";
|
|
10
10
|
import"./chunk-df32avw6.js";
|
|
11
11
|
export {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
INVALID_FRAME_CLOSE_CODE,
|
|
3
|
+
PROTOCOL,
|
|
3
4
|
UNAUTHORIZED_CLOSE_CODE,
|
|
4
5
|
conversationStream,
|
|
5
6
|
docStream
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-h35fermn.js";
|
|
7
8
|
|
|
8
9
|
// packages/protocol/src/remote-durable.ts
|
|
9
10
|
import { applyImmutable } from "@earendil-works/chord/delta";
|
|
@@ -125,13 +126,19 @@ class UnauthorizedError extends Error {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
class ProtocolMismatchError extends Error {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
hostMajor;
|
|
130
|
+
clientMajor;
|
|
131
|
+
hostVersion;
|
|
132
|
+
direction;
|
|
133
|
+
constructor(hostMajor, clientMajor, hostVersion) {
|
|
134
|
+
const direction = hostMajor > clientMajor ? "client-older" : "host-older";
|
|
135
|
+
super(direction === "client-older" ? `Client out of date: the host speaks protocol major ${hostMajor}, this client speaks ${clientMajor}` : `Host out of date: this client speaks protocol major ${clientMajor}, the host speaks ${hostMajor}`);
|
|
132
136
|
this.name = "ProtocolMismatchError";
|
|
133
|
-
this.
|
|
134
|
-
this.
|
|
137
|
+
this.hostMajor = hostMajor;
|
|
138
|
+
this.clientMajor = clientMajor;
|
|
139
|
+
if (hostVersion !== undefined)
|
|
140
|
+
this.hostVersion = hostVersion;
|
|
141
|
+
this.direction = direction;
|
|
135
142
|
}
|
|
136
143
|
}
|
|
137
144
|
|
|
@@ -141,6 +148,7 @@ class RemoteClient {
|
|
|
141
148
|
#listeners = new Set;
|
|
142
149
|
#values = new Map;
|
|
143
150
|
#wanted = new Set(["conversations", "index", "devices", "mcp"]);
|
|
151
|
+
#fresh = new Set;
|
|
144
152
|
#docs = [];
|
|
145
153
|
#toolPresentations = {};
|
|
146
154
|
#snapshotWaiters = new Map;
|
|
@@ -149,6 +157,7 @@ class RemoteClient {
|
|
|
149
157
|
#connection;
|
|
150
158
|
#state;
|
|
151
159
|
#current;
|
|
160
|
+
#side;
|
|
152
161
|
#nextCall = 1;
|
|
153
162
|
#nextNotice = 1;
|
|
154
163
|
#attempt = 0;
|
|
@@ -192,10 +201,16 @@ class RemoteClient {
|
|
|
192
201
|
return;
|
|
193
202
|
if (code === UNAUTHORIZED_CLOSE_CODE) {
|
|
194
203
|
this.#closed = true;
|
|
195
|
-
this.#update({ connection: "closed" });
|
|
204
|
+
this.#update({ connection: "closed", unauthorized: true });
|
|
196
205
|
this.#notice("error", new UnauthorizedError().message);
|
|
197
206
|
return;
|
|
198
207
|
}
|
|
208
|
+
if (code === INVALID_FRAME_CLOSE_CODE) {
|
|
209
|
+
this.#closed = true;
|
|
210
|
+
this.#update({ connection: "closed" });
|
|
211
|
+
this.#notice("error", `The host rejected a malformed frame from this client${reason === undefined ? "" : `: ${reason}`}`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
199
214
|
this.#update({ connection: "reconnecting" });
|
|
200
215
|
const { min, max } = this.#options.reconnectDelayMs ?? { min: 200, max: 2000 };
|
|
201
216
|
const delay = Math.min(max, min * 2 ** this.#attempt++);
|
|
@@ -208,15 +223,27 @@ class RemoteClient {
|
|
|
208
223
|
this.#connection?.send(JSON.stringify(frame));
|
|
209
224
|
}
|
|
210
225
|
#receive(frame) {
|
|
226
|
+
if (frame === null || typeof frame !== "object")
|
|
227
|
+
return;
|
|
211
228
|
switch (frame.type) {
|
|
212
229
|
case "hello": {
|
|
213
|
-
|
|
214
|
-
|
|
230
|
+
const announced = frame.protocol;
|
|
231
|
+
const hostMajor = typeof announced === "number" ? announced : announced !== null && typeof announced === "object" ? announced.major : undefined;
|
|
232
|
+
if (hostMajor !== PROTOCOL.major) {
|
|
233
|
+
const mismatch = new ProtocolMismatchError(typeof hostMajor === "number" ? hostMajor : Number.NaN, PROTOCOL.major, frame.hostVersion);
|
|
215
234
|
this.#closed = true;
|
|
216
235
|
if (this.#state === undefined) {
|
|
217
236
|
this.#rejectReady(mismatch);
|
|
218
237
|
} else {
|
|
219
|
-
this.#update({
|
|
238
|
+
this.#update({
|
|
239
|
+
connection: "outdated",
|
|
240
|
+
protocolMismatch: {
|
|
241
|
+
hostMajor: mismatch.hostMajor,
|
|
242
|
+
clientMajor: mismatch.clientMajor,
|
|
243
|
+
...mismatch.hostVersion === undefined ? {} : { hostVersion: mismatch.hostVersion },
|
|
244
|
+
direction: mismatch.direction
|
|
245
|
+
}
|
|
246
|
+
});
|
|
220
247
|
this.#notice("error", mismatch.message);
|
|
221
248
|
}
|
|
222
249
|
this.#connection?.close();
|
|
@@ -225,9 +252,13 @@ class RemoteClient {
|
|
|
225
252
|
this.#attempt = 0;
|
|
226
253
|
this.#docs = frame.docs;
|
|
227
254
|
this.#toolPresentations = frame.toolPresentations;
|
|
255
|
+
this.#fresh.clear();
|
|
228
256
|
if (this.#current !== undefined)
|
|
229
257
|
for (const stream of this.#conversationStreams(this.#current))
|
|
230
258
|
this.#wanted.add(stream);
|
|
259
|
+
if (this.#side !== undefined)
|
|
260
|
+
for (const stream of this.#conversationStreams(this.#side))
|
|
261
|
+
this.#wanted.add(stream);
|
|
231
262
|
for (const stream of this.#wanted)
|
|
232
263
|
this.#send({ type: "subscribe", stream });
|
|
233
264
|
if (this.#state !== undefined) {
|
|
@@ -246,6 +277,7 @@ class RemoteClient {
|
|
|
246
277
|
if (!this.#wanted.has(frame.stream))
|
|
247
278
|
return;
|
|
248
279
|
this.#values.set(frame.stream, frame.value);
|
|
280
|
+
this.#fresh.add(frame.stream);
|
|
249
281
|
this.#refresh();
|
|
250
282
|
for (const waiter of this.#snapshotWaiters.get(frame.stream)?.splice(0) ?? [])
|
|
251
283
|
waiter.resolve();
|
|
@@ -259,6 +291,7 @@ class RemoteClient {
|
|
|
259
291
|
case "ended":
|
|
260
292
|
this.#wanted.delete(frame.stream);
|
|
261
293
|
this.#values.delete(frame.stream);
|
|
294
|
+
this.#fresh.delete(frame.stream);
|
|
262
295
|
for (const waiter of this.#snapshotWaiters.get(frame.stream)?.splice(0) ?? []) {
|
|
263
296
|
waiter.reject(new Error(`Stream ${frame.stream} ended: ${frame.reason}`));
|
|
264
297
|
}
|
|
@@ -306,26 +339,66 @@ class RemoteClient {
|
|
|
306
339
|
#conversationStreams(id) {
|
|
307
340
|
return [conversationStream(id), ...this.#docs.map((doc) => docStream(doc.kind, id))];
|
|
308
341
|
}
|
|
342
|
+
async#acquire(id) {
|
|
343
|
+
const missing = this.#conversationStreams(id).filter((stream) => !this.#wanted.has(stream));
|
|
344
|
+
const stale = this.#conversationStreams(id).filter((stream) => this.#wanted.has(stream) && !this.#fresh.has(stream));
|
|
345
|
+
const shown = Promise.all([...missing, ...stale].map((stream) => this.#snapshot(stream)));
|
|
346
|
+
for (const stream of missing) {
|
|
347
|
+
this.#wanted.add(stream);
|
|
348
|
+
this.#send({ type: "subscribe", stream });
|
|
349
|
+
}
|
|
350
|
+
await shown;
|
|
351
|
+
}
|
|
352
|
+
#release(id, other) {
|
|
353
|
+
const keep = new Set(other === undefined ? [] : this.#conversationStreams(other));
|
|
354
|
+
for (const stream of this.#conversationStreams(id)) {
|
|
355
|
+
if (keep.has(stream))
|
|
356
|
+
continue;
|
|
357
|
+
this.#wanted.delete(stream);
|
|
358
|
+
this.#values.delete(stream);
|
|
359
|
+
this.#fresh.delete(stream);
|
|
360
|
+
this.#send({ type: "unsubscribe", stream });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
#descendsFrom(id, ancestor) {
|
|
364
|
+
const summaries = this.#values.get("conversations") ?? [];
|
|
365
|
+
const parentOf = new Map(summaries.map((summary) => [summary.id, summary.parent]));
|
|
366
|
+
let cursor = id;
|
|
367
|
+
for (let depth = 0;cursor !== undefined && depth < 8; depth++) {
|
|
368
|
+
if (cursor === ancestor)
|
|
369
|
+
return true;
|
|
370
|
+
cursor = parentOf.get(cursor);
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
#docsOf(id) {
|
|
375
|
+
return this.#docs.flatMap((doc) => {
|
|
376
|
+
const stream = docStream(doc.kind, id);
|
|
377
|
+
return this.#values.has(stream) ? [{ kind: doc.kind, presentation: doc.presentation, value: this.#values.get(stream) }] : [];
|
|
378
|
+
});
|
|
379
|
+
}
|
|
309
380
|
#refresh() {
|
|
310
381
|
if (this.#state === undefined)
|
|
311
382
|
return;
|
|
312
383
|
const current = this.#current;
|
|
384
|
+
const side = this.#side;
|
|
313
385
|
const conversation = current === undefined ? undefined : this.#values.get(conversationStream(current));
|
|
314
386
|
const summaries = this.#values.get("conversations") ?? [];
|
|
315
387
|
const index = this.#values.get("index") ?? { projects: [], conversations: [] };
|
|
316
388
|
const devices = this.#values.get("devices")?.devices ?? [];
|
|
317
389
|
const tasks = this.#wanted.has("tasks") ? this.#values.get("tasks") : undefined;
|
|
318
|
-
const docs = current === undefined ? [] : this.#docs.flatMap((doc) => {
|
|
319
|
-
const stream = docStream(doc.kind, current);
|
|
320
|
-
return this.#values.has(stream) ? [{ kind: doc.kind, presentation: doc.presentation, value: this.#values.get(stream) }] : [];
|
|
321
|
-
});
|
|
322
390
|
this.#update({
|
|
323
391
|
conversation,
|
|
324
392
|
organized: organize(index, summaries),
|
|
325
393
|
home: current === undefined ? undefined : homeOf(index, summaries, current),
|
|
326
394
|
checkout: current === undefined ? undefined : checkoutOf(index, summaries, current),
|
|
327
395
|
tasks,
|
|
328
|
-
docs,
|
|
396
|
+
docs: current === undefined ? [] : this.#docsOf(current),
|
|
397
|
+
side: side === undefined ? undefined : {
|
|
398
|
+
id: side,
|
|
399
|
+
conversation: this.#values.get(conversationStream(side)),
|
|
400
|
+
docs: this.#docsOf(side)
|
|
401
|
+
},
|
|
329
402
|
devices,
|
|
330
403
|
mcp: this.#values.get("mcp") ?? null
|
|
331
404
|
});
|
|
@@ -396,15 +469,18 @@ class RemoteClient {
|
|
|
396
469
|
await this.#call("archive", { conversationId: id, archived });
|
|
397
470
|
if (archived && id === this.#current)
|
|
398
471
|
this.#unshow();
|
|
472
|
+
if (archived && this.#side !== undefined && this.#descendsFrom(this.#side, id))
|
|
473
|
+
await this.#showSide(undefined);
|
|
399
474
|
}),
|
|
400
|
-
submit: (text, whenBusy) => this.#command(() => this.#call("submit", { conversationId: conversationId(), text, whenBusy, requestId: newRequestId() })),
|
|
475
|
+
submit: (text, whenBusy, target) => this.#command(() => this.#call("submit", { conversationId: target ?? conversationId(), text, whenBusy, requestId: newRequestId() })),
|
|
401
476
|
compact: (instructions) => this.#command(() => this.#call("compact", { conversationId: conversationId(), ...instructions === undefined ? {} : { instructions } })),
|
|
402
|
-
abort: () => {
|
|
403
|
-
|
|
477
|
+
abort: (target) => {
|
|
478
|
+
const targetConversation = target ?? this.#current;
|
|
479
|
+
if (targetConversation === undefined) {
|
|
404
480
|
this.#notice("error", "No conversation selected");
|
|
405
481
|
return Promise.resolve();
|
|
406
482
|
}
|
|
407
|
-
return this.#call("abort", { conversationId:
|
|
483
|
+
return this.#call("abort", { conversationId: targetConversation }).then(() => {}, (error) => this.#notice("error", error instanceof Error ? error.message : String(error)));
|
|
408
484
|
},
|
|
409
485
|
setThinkingLevel: (level) => this.#command(() => this.#call("setThinkingLevel", { conversationId: conversationId(), level })),
|
|
410
486
|
setModel: (model) => this.#command(() => this.#call("setModel", { conversationId: conversationId(), model })),
|
|
@@ -421,16 +497,20 @@ class RemoteClient {
|
|
|
421
497
|
await shown;
|
|
422
498
|
}),
|
|
423
499
|
switchConversation: (id) => this.#command(() => this.#switch(id)),
|
|
424
|
-
|
|
500
|
+
showSide: (id) => this.#command(() => this.#showSide(id)),
|
|
501
|
+
fork: (entryId, prompt, removeTools, options) => this.#command(async () => {
|
|
425
502
|
const { conversationId: forked } = await this.#call("fork", {
|
|
426
503
|
conversationId: conversationId(),
|
|
427
504
|
entryId,
|
|
428
505
|
...removeTools === undefined ? {} : { removeTools }
|
|
429
506
|
});
|
|
430
|
-
|
|
507
|
+
if (options?.show === "side")
|
|
508
|
+
await this.#showSide(forked);
|
|
509
|
+
else
|
|
510
|
+
await this.#switch(forked);
|
|
431
511
|
await this.#call("submit", { conversationId: forked, text: prompt, whenBusy: "followUp", requestId: newRequestId() });
|
|
432
512
|
}),
|
|
433
|
-
answer: (kind, requestId, answers) => this.#command(() => this.#call("answer", { conversationId: conversationId(), kind, requestId, answers })),
|
|
513
|
+
answer: (kind, requestId, answers, target) => this.#command(() => this.#call("answer", { conversationId: target ?? conversationId(), kind, requestId, answers })),
|
|
434
514
|
changes: () => {
|
|
435
515
|
const call = this.#commands.then(() => this.#call("changes", { conversationId: conversationId() }));
|
|
436
516
|
this.#commands = call.then(() => {}, () => {});
|
|
@@ -448,19 +528,10 @@ class RemoteClient {
|
|
|
448
528
|
const previous = this.#current;
|
|
449
529
|
if (id === previous)
|
|
450
530
|
return;
|
|
451
|
-
|
|
452
|
-
for (const stream of next)
|
|
453
|
-
this.#wanted.add(stream);
|
|
454
|
-
const shown = Promise.all(next.map((stream) => this.#snapshot(stream)));
|
|
455
|
-
for (const stream of next)
|
|
456
|
-
this.#send({ type: "subscribe", stream });
|
|
457
|
-
await shown;
|
|
531
|
+
await this.#acquire(id);
|
|
458
532
|
this.#current = id;
|
|
459
|
-
|
|
460
|
-
this.#
|
|
461
|
-
this.#values.delete(stream);
|
|
462
|
-
this.#send({ type: "unsubscribe", stream });
|
|
463
|
-
}
|
|
533
|
+
if (previous !== undefined)
|
|
534
|
+
this.#release(previous, this.#side);
|
|
464
535
|
this.#refresh();
|
|
465
536
|
}
|
|
466
537
|
#unshow() {
|
|
@@ -468,11 +539,18 @@ class RemoteClient {
|
|
|
468
539
|
if (previous === undefined)
|
|
469
540
|
return;
|
|
470
541
|
this.#current = undefined;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
542
|
+
this.#release(previous, this.#side);
|
|
543
|
+
this.#refresh();
|
|
544
|
+
}
|
|
545
|
+
async#showSide(id) {
|
|
546
|
+
const previous = this.#side;
|
|
547
|
+
if (id === previous)
|
|
548
|
+
return;
|
|
549
|
+
if (id !== undefined)
|
|
550
|
+
await this.#acquire(id);
|
|
551
|
+
this.#side = id;
|
|
552
|
+
if (previous !== undefined)
|
|
553
|
+
this.#release(previous, this.#current);
|
|
476
554
|
this.#refresh();
|
|
477
555
|
}
|
|
478
556
|
#close() {
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
commandHint,
|
|
3
3
|
hostEntry,
|
|
4
4
|
packagedVersion
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-rkfdsacq.js";
|
|
6
6
|
import {
|
|
7
7
|
connectRemoteDurable
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-5h97zxag.js";
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_DATA_DIR,
|
|
11
11
|
hostAddress
|