@makerstufferupper/flypi 0.1.3 → 0.1.5
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 +9 -4
- package/dist/ui/assets/{index-qaKjH5fH.css → index-CBuNmrDT.css} +1 -1
- package/dist/ui/assets/index-DmMmjg_M.js +102 -0
- package/dist/ui/index.html +2 -2
- package/package.json +15 -5
- package/scripts/dev-container.mjs +131 -0
- package/scripts/flypi.mjs +20 -2
- package/scripts/pi.ts +150 -0
- package/scripts/probe-pi.ts +18 -0
- package/scripts/release-check.mjs +239 -0
- package/scripts/serve.ts +2 -0
- package/src/agent/packages.ts +104 -11
- package/src/agent/pi-compat.ts +204 -0
- package/src/daemon/index.ts +58 -4
- package/src/daemon/pi.ts +373 -0
- package/src/memory/recall.ts +11 -2
- package/src/server/index.ts +44 -4
- package/src/server/protocol.ts +79 -0
- package/src/server/session-registry.ts +26 -2
- package/dist/ui/assets/index-CG1CuGxu.js +0 -105
package/src/server/protocol.ts
CHANGED
|
@@ -332,7 +332,17 @@ export const PackageRow = Type.Object({
|
|
|
332
332
|
scope: Type.Union([Type.Literal("user"), Type.Literal("project")]),
|
|
333
333
|
installed: Type.Boolean(),
|
|
334
334
|
path: Type.String(),
|
|
335
|
+
/** The version the installed code's own `package.json` says. A source that names a version is pinned,
|
|
336
|
+
* and this is what tells the reader which version that is (docs/pi-upgrades.md). */
|
|
337
|
+
version: Type.Optional(Type.String()),
|
|
335
338
|
update: Type.Optional(Type.Boolean()),
|
|
339
|
+
/** The npm package an `npm:` source names, version stripped. Absent for a git URL or a path, and its
|
|
340
|
+
* presence next to a source that is not `npm:<name>` is what reads as pinned. */
|
|
341
|
+
name: Type.Optional(Type.String()),
|
|
342
|
+
/** Published releases, oldest first, from the same daily stamp — what "install this version" offers. */
|
|
343
|
+
versions: Type.Optional(Type.Array(Type.String())),
|
|
344
|
+
/** The newest published release newer than what is installed. */
|
|
345
|
+
available: Type.Optional(Type.String()),
|
|
336
346
|
});
|
|
337
347
|
|
|
338
348
|
export const PackagesSnapshot = Type.Object({
|
|
@@ -367,6 +377,60 @@ export const SavePackagesCommand = Type.Object({
|
|
|
367
377
|
requestId: Type.Optional(RequestId),
|
|
368
378
|
});
|
|
369
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Which pi answers, what is on disk to move to, and what the daemon's daily registry answer said (Phase 8.4,
|
|
382
|
+
* docs/pi-upgrades.md). `latest`/`next` are absent until the daemon has asked — the request itself reaches no
|
|
383
|
+
* network, the same rule `packages` follows.
|
|
384
|
+
*/
|
|
385
|
+
export const PiVersions = Type.Object({
|
|
386
|
+
/** The version this process loaded. */
|
|
387
|
+
running: Type.String(),
|
|
388
|
+
pinned: Type.Boolean(),
|
|
389
|
+
/** What the tree flypi was installed with holds, when that is not what is running. */
|
|
390
|
+
bundled: Type.Optional(Type.String()),
|
|
391
|
+
/** The pi versions with a tree on disk — what "switch back" can offer. */
|
|
392
|
+
trees: Type.Array(Type.String()),
|
|
393
|
+
next: Type.Optional(Type.String()),
|
|
394
|
+
latest: Type.Optional(Type.String()),
|
|
395
|
+
checkedAt: Type.Optional(Type.String()),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
export type PiVersions = Static<typeof PiVersions>;
|
|
399
|
+
|
|
400
|
+
/** Ask what pi this flypi runs and what it could move to. Reaches no network. */
|
|
401
|
+
export const PiVersionsCommand = Type.Object({
|
|
402
|
+
type: Type.Literal("piVersions"),
|
|
403
|
+
requestId: Type.Optional(RequestId),
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Ask the registry for pi now instead of on the daemon's daily rhythm. The reply is a fresh `piVersions`, so
|
|
408
|
+
* the page needs no new frame; nothing moves by itself either way.
|
|
409
|
+
*/
|
|
410
|
+
export const PiCheckCommand = Type.Object({
|
|
411
|
+
type: Type.Literal("piCheck"),
|
|
412
|
+
requestId: Type.Optional(RequestId),
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Move to a published pi version. The daemon flips the pointer and hands the restart to the CLI — the same
|
|
417
|
+
* path `installUpdate` takes — and the ack goes out before the restart closes this socket. The switch is
|
|
418
|
+
* provisional: if the new process does not answer reporting that version, the pointer goes back on its own
|
|
419
|
+
* (docs/risks.md risk 13), so the page comes back and says which version it is on.
|
|
420
|
+
*/
|
|
421
|
+
export const UsePiCommand = Type.Object({
|
|
422
|
+
type: Type.Literal("usePi"),
|
|
423
|
+
version: Type.String({ minLength: 1 }),
|
|
424
|
+
requestId: Type.Optional(RequestId),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
/** The same call with an older version — only offered for the trees still on disk. */
|
|
428
|
+
export const RollbackPiCommand = Type.Object({
|
|
429
|
+
type: Type.Literal("rollbackPi"),
|
|
430
|
+
version: Type.String({ minLength: 1 }),
|
|
431
|
+
requestId: Type.Optional(RequestId),
|
|
432
|
+
});
|
|
433
|
+
|
|
370
434
|
/** What kind of secret a row claims to be — the same list the vault's schema allows (src/secrets/crypto.ts). */
|
|
371
435
|
export const SecretKind = Type.Union([
|
|
372
436
|
Type.Literal("password"),
|
|
@@ -567,6 +631,10 @@ export const ClientCommandSchema = Type.Union([
|
|
|
567
631
|
McpRedirectCommand,
|
|
568
632
|
PackagesCommand,
|
|
569
633
|
SavePackagesCommand,
|
|
634
|
+
PiVersionsCommand,
|
|
635
|
+
PiCheckCommand,
|
|
636
|
+
UsePiCommand,
|
|
637
|
+
RollbackPiCommand,
|
|
570
638
|
SecretsCommand,
|
|
571
639
|
SaveSecretsCommand,
|
|
572
640
|
RevealSecretCommand,
|
|
@@ -579,10 +647,20 @@ export type ClientCommand = Static<typeof ClientCommandSchema>;
|
|
|
579
647
|
/** A pi session event, passed through verbatim. Only the discriminator is checked here. */
|
|
580
648
|
export const SessionEventFrameBody = Type.Object({ type: Type.String() }, { additionalProperties: true });
|
|
581
649
|
|
|
650
|
+
/** Which pi answers (docs/pi-upgrades.md Phase 8.1): the version loaded, and what chose it. */
|
|
651
|
+
export const PiInfoFrame = Type.Object({
|
|
652
|
+
version: Type.String(),
|
|
653
|
+
pinned: Type.Boolean(),
|
|
654
|
+
/** What the tree flypi was installed with holds, when that is not what is running. */
|
|
655
|
+
bundled: Type.Optional(Type.String()),
|
|
656
|
+
});
|
|
657
|
+
|
|
582
658
|
export const HelloFrame = Type.Object({
|
|
583
659
|
type: Type.Literal("hello"),
|
|
584
660
|
protocolVersion: Type.Integer(),
|
|
585
661
|
serverVersion: Type.String(),
|
|
662
|
+
/** Optional so an older server still parses; every flypi server from here on sends it. */
|
|
663
|
+
pi: Type.Optional(PiInfoFrame),
|
|
586
664
|
});
|
|
587
665
|
|
|
588
666
|
export const EventFrame = Type.Object({
|
|
@@ -817,6 +895,7 @@ export const ResultFrame = Type.Object({
|
|
|
817
895
|
mcpStatus: Type.Optional(McpStatusSnapshot),
|
|
818
896
|
config: Type.Optional(ConfigSnapshot),
|
|
819
897
|
packages: Type.Optional(PackagesSnapshot),
|
|
898
|
+
pi: Type.Optional(PiVersions),
|
|
820
899
|
scheduler: Type.Optional(SchedulerSnapshot),
|
|
821
900
|
secrets: Type.Optional(SecretsSnapshot),
|
|
822
901
|
revealed: Type.Optional(RevealedSecret),
|
|
@@ -126,11 +126,33 @@ export class SessionRegistry {
|
|
|
126
126
|
);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Open an existing session for a client: the conversation already answering it when there is one, and
|
|
131
|
+
* otherwise this conversation moved onto it — or a new one, if this conversation is mid-turn.
|
|
132
|
+
*/
|
|
130
133
|
async switchSession(conversationId: string, sessionPath: string): Promise<Conversation> {
|
|
131
134
|
const conversation = this.get(conversationId);
|
|
132
135
|
const target = this.sessionFileIn(sessionPath);
|
|
136
|
+
// Already answering it: opening the file a second time would put a second writer on it, so the
|
|
137
|
+
// conversation the page is already on stays where it is. An idle one is reopened as asked — that is
|
|
138
|
+
// how a client reloads a session's history from disk.
|
|
139
|
+
if (conversation.session.sessionFile === target && !conversation.session.isIdle) return conversation;
|
|
140
|
+
// A session another conversation is already answering **is** that conversation, not a copy of it. Opening
|
|
141
|
+
// the file a second time would put a second writer on it and freeze the transcript at the moment of the
|
|
142
|
+
// switch, so the answer on its way would never reach the page that just asked for that session.
|
|
143
|
+
const live = [...this.conversations.values()].find((other) => other.id !== conversationId && other.session.sessionFile === target);
|
|
144
|
+
if (live) return live;
|
|
145
|
+
// Rebinding disposes the session this conversation is answering in, and a turn still running would be cut
|
|
146
|
+
// short: its answer would reach neither the page nor the session file. So a session with no conversation
|
|
147
|
+
// of its own is opened as one, and this conversation is left alone to finish (it stays in the map, and
|
|
148
|
+
// switching back to it returns it from the lookup above, log and all).
|
|
149
|
+
if (!conversation.session.isIdle) return this.bind(SessionManager.open(target, this.paths.sessionsDir));
|
|
133
150
|
this.teardown(conversation);
|
|
151
|
+
// The conversation lives on with a new session, so it is not disposed any more. Without this the
|
|
152
|
+
// exactly-once guard below stays set and the *new* session is never unsubscribed or disposed — it
|
|
153
|
+
// keeps streaming into this conversation's log (and its listeners) after the page moved on, keeps
|
|
154
|
+
// the conversation unevictable, and writes back any session file it is deleted out from under.
|
|
155
|
+
conversation.disposed = false;
|
|
134
156
|
conversation.session = await this.createSession({
|
|
135
157
|
sessionManager: SessionManager.open(target, this.paths.sessionsDir),
|
|
136
158
|
conversationId,
|
|
@@ -171,7 +193,9 @@ export class SessionRegistry {
|
|
|
171
193
|
|
|
172
194
|
get(conversationId: string): Conversation {
|
|
173
195
|
const conversation = this.conversations.get(conversationId);
|
|
174
|
-
|
|
196
|
+
// The code is what a page acts on: it holds a conversation id across reconnects, and after a restart
|
|
197
|
+
// every command it sends names a conversation this process never had (ui/lib/app-state.svelte.ts).
|
|
198
|
+
if (!conversation) throw Object.assign(new Error(`unknown conversation ${conversationId}`), { code: "unknownConversation" });
|
|
175
199
|
return conversation;
|
|
176
200
|
}
|
|
177
201
|
|