@makerstufferupper/flypi 0.0.0-stage → 0.1.3
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/LICENSE +21 -0
- package/README.md +64 -2
- package/assets/screenshot.png +0 -0
- package/bin/flypi.mjs +28 -0
- package/dist/ui/assets/index-CG1CuGxu.js +105 -0
- package/dist/ui/assets/index-qaKjH5fH.css +1 -0
- package/dist/ui/flypi-logo-dark.jpeg +0 -0
- package/dist/ui/flypi-logo-light.jpeg +0 -0
- package/dist/ui/index.html +17 -0
- package/package.json +71 -5
- package/scripts/benchmark-nli.ts +273 -0
- package/scripts/flypi.mjs +310 -0
- package/scripts/probe-turn-boundary.ts +97 -0
- package/scripts/probe.ts +119 -0
- package/scripts/record-transcript.ts +63 -0
- package/scripts/reembed-memory.ts +41 -0
- package/scripts/release.mjs +38 -0
- package/scripts/secret.ts +24 -0
- package/scripts/serve-daemon.ts +101 -0
- package/scripts/serve-memory.ts +78 -0
- package/scripts/serve.ts +89 -0
- package/scripts/smoke-memory.mjs +239 -0
- package/scripts/smoke-secrets.mjs +153 -0
- package/scripts/smoke.mjs +59 -0
- package/scripts/start.mjs +294 -0
- package/scripts/turn-boundary.ts +52 -0
- package/scripts/update.ts +92 -0
- package/scripts/warm-models.ts +41 -0
- package/src/agent/config-files.ts +529 -0
- package/src/agent/mcp-signin.ts +137 -0
- package/src/agent/mcp-status.ts +168 -0
- package/src/agent/onboarding.ts +74 -0
- package/src/agent/packages.ts +150 -0
- package/src/agent/paths.ts +80 -0
- package/src/agent/permission-gate.ts +70 -0
- package/src/agent/pi-mcp.ts +97 -0
- package/src/agent/runtime.ts +347 -0
- package/src/daemon/index.ts +442 -0
- package/src/daemon/jobs.ts +202 -0
- package/src/daemon/update.ts +248 -0
- package/src/memory/capture.ts +109 -0
- package/src/memory/config.ts +90 -0
- package/src/memory/consolidate.ts +218 -0
- package/src/memory/db.ts +393 -0
- package/src/memory/embed.ts +154 -0
- package/src/memory/judge.ts +167 -0
- package/src/memory/mcp.ts +496 -0
- package/src/memory/media.ts +135 -0
- package/src/memory/progress.ts +39 -0
- package/src/memory/recall.ts +528 -0
- package/src/memory/review.ts +86 -0
- package/src/memory/store.ts +1034 -0
- package/src/memory/wiki.ts +140 -0
- package/src/secrets/cli.ts +248 -0
- package/src/secrets/config.ts +133 -0
- package/src/secrets/crypto.ts +180 -0
- package/src/secrets/extension.ts +253 -0
- package/src/secrets/redact.ts +132 -0
- package/src/secrets/vault.ts +396 -0
- package/src/server/event-log.ts +49 -0
- package/src/server/extension-ui.ts +118 -0
- package/src/server/index.ts +398 -0
- package/src/server/memory-api.ts +158 -0
- package/src/server/protocol.ts +947 -0
- package/src/server/run-store.ts +136 -0
- package/src/server/secrets-api.ts +88 -0
- package/src/server/session-registry.ts +593 -0
- package/src/shared/attachments.ts +40 -0
- package/src/shared/cache-probe.ts +47 -0
- package/src/shared/markdown.ts +17 -0
- package/src/shared/metrics.ts +212 -0
- package/src/shared/sqlite.ts +74 -0
- package/src/shared/transcript.ts +404 -0
- package/src/shared/turn-boundary.ts +98 -0
- package/src/shared/views.ts +31 -0
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WS wire protocol (docs/protocol.md). One JSON object per frame, in both directions.
|
|
3
|
+
*
|
|
4
|
+
* Commands are validated on the way in — this is the trust boundary. The pi event payloads
|
|
5
|
+
* carried by `event` frames are passed through unchanged, so a reassembled transcript is
|
|
6
|
+
* identical to what the session emitted (that equality is Phase 1's feature, not a
|
|
7
|
+
* nice-to-have). The frame around them is what this file owns: `seq`, `conversationId`,
|
|
8
|
+
* `requestId`.
|
|
9
|
+
*/
|
|
10
|
+
import { Type, type Static } from "typebox";
|
|
11
|
+
import { Check, Errors } from "typebox/value";
|
|
12
|
+
import { MAX_ATTACHMENTS, MAX_ATTACHMENT_BYTES } from "../shared/attachments.ts";
|
|
13
|
+
|
|
14
|
+
export const PROTOCOL_VERSION = 1;
|
|
15
|
+
|
|
16
|
+
const Id = Type.String({ minLength: 1, maxLength: 512 });
|
|
17
|
+
const RequestId = Type.String({ minLength: 1, maxLength: 100 });
|
|
18
|
+
|
|
19
|
+
/** `steer` interrupts the running turn, `followUp` waits for it. Required while streaming. */
|
|
20
|
+
const QueueMode = Type.Union([Type.Literal("steer"), Type.Literal("followUp")]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A pasted file (src/shared/attachments.ts). Bytes as base64, so the size limit is checked here as a
|
|
24
|
+
* character count — the trust boundary cannot know what the bytes decode to.
|
|
25
|
+
*/
|
|
26
|
+
const Attachment = Type.Object({
|
|
27
|
+
name: Type.String({ minLength: 1, maxLength: 200 }),
|
|
28
|
+
mimeType: Type.String({ minLength: 1, maxLength: 100 }),
|
|
29
|
+
data: Type.String({ minLength: 1, maxLength: Math.ceil(MAX_ATTACHMENT_BYTES * 4 / 3) }),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const PromptCommand = Type.Object({
|
|
33
|
+
type: Type.Literal("prompt"),
|
|
34
|
+
conversationId: Id,
|
|
35
|
+
/** Empty is allowed only with an attachment; the registry refuses a prompt that has neither. */
|
|
36
|
+
text: Type.String(),
|
|
37
|
+
attachments: Type.Optional(Type.Array(Attachment, { maxItems: MAX_ATTACHMENTS })),
|
|
38
|
+
mode: Type.Optional(QueueMode),
|
|
39
|
+
requestId: Type.Optional(RequestId),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const AbortCommand = Type.Object({
|
|
43
|
+
type: Type.Literal("abort"),
|
|
44
|
+
conversationId: Id,
|
|
45
|
+
requestId: Type.Optional(RequestId),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const NewSessionCommand = Type.Object({
|
|
49
|
+
type: Type.Literal("newSession"),
|
|
50
|
+
cwd: Type.Optional(Id),
|
|
51
|
+
requestId: Type.Optional(RequestId),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** Rebind this conversation to an existing session file (the sidebar's "open"). */
|
|
55
|
+
export const SwitchSessionCommand = Type.Object({
|
|
56
|
+
type: Type.Literal("switchSession"),
|
|
57
|
+
conversationId: Id,
|
|
58
|
+
sessionPath: Id,
|
|
59
|
+
requestId: Type.Optional(RequestId),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Branch the conversation at one of its answers, into a new conversation.
|
|
64
|
+
*
|
|
65
|
+
* The message is named by the `seq` of the `message_end` frame that carried it — the same number the
|
|
66
|
+
* UI already holds for the message it is rendering. A position we each counted separately (nth answer,
|
|
67
|
+
* nth rendered message) would have to agree across the wire by coincidence; the seq is the identity the
|
|
68
|
+
* stream gave that message.
|
|
69
|
+
*/
|
|
70
|
+
export const ForkCommand = Type.Object({
|
|
71
|
+
type: Type.Literal("fork"),
|
|
72
|
+
conversationId: Id,
|
|
73
|
+
messageSeq: Type.Integer({ minimum: 1 }),
|
|
74
|
+
requestId: Type.Optional(RequestId),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const SetModelCommand = Type.Object({
|
|
78
|
+
type: Type.Literal("setModel"),
|
|
79
|
+
conversationId: Id,
|
|
80
|
+
provider: Id,
|
|
81
|
+
modelId: Id,
|
|
82
|
+
requestId: Type.Optional(RequestId),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const SetThinkingLevelCommand = Type.Object({
|
|
86
|
+
type: Type.Literal("setThinkingLevel"),
|
|
87
|
+
conversationId: Id,
|
|
88
|
+
level: Type.Union([
|
|
89
|
+
Type.Literal("off"),
|
|
90
|
+
Type.Literal("minimal"),
|
|
91
|
+
Type.Literal("low"),
|
|
92
|
+
Type.Literal("medium"),
|
|
93
|
+
Type.Literal("high"),
|
|
94
|
+
Type.Literal("xhigh"),
|
|
95
|
+
Type.Literal("max"),
|
|
96
|
+
]),
|
|
97
|
+
requestId: Type.Optional(RequestId),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The answer to an extension's question (`ctx.ui.select` / `confirm` / `input`, pushed as an
|
|
102
|
+
* `extensionUi` frame). `value` is the option or the text; null is "cancelled", which is what pi's
|
|
103
|
+
* own dialogs report when the user dismisses them.
|
|
104
|
+
*/
|
|
105
|
+
export const ExtensionReplyCommand = Type.Object({
|
|
106
|
+
type: Type.Literal("extensionReply"),
|
|
107
|
+
conversationId: Id,
|
|
108
|
+
dialogId: Id,
|
|
109
|
+
value: Type.Union([Type.String({ maxLength: 8000 }), Type.Null()]),
|
|
110
|
+
requestId: Type.Optional(RequestId),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/** Reconnect: "send me everything after seq N". */
|
|
114
|
+
export const ResumeCommand = Type.Object({
|
|
115
|
+
type: Type.Literal("resume"),
|
|
116
|
+
conversationId: Id,
|
|
117
|
+
afterSeq: Type.Integer({ minimum: 0 }),
|
|
118
|
+
requestId: Type.Optional(RequestId),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/** The sidebar's "open": the session files flypi owns. No conversation needed. */
|
|
122
|
+
export const ListSessionsCommand = Type.Object({
|
|
123
|
+
type: Type.Literal("listSessions"),
|
|
124
|
+
requestId: Type.Optional(RequestId),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/** Rename the current conversation's session (pi's own session name, not a flypi side-store). */
|
|
128
|
+
export const RenameSessionCommand = Type.Object({
|
|
129
|
+
type: Type.Literal("renameSession"),
|
|
130
|
+
conversationId: Id,
|
|
131
|
+
name: Type.String({ minLength: 1, maxLength: 200 }),
|
|
132
|
+
requestId: Type.Optional(RequestId),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Delete one session file, permanently. Path-scoped like `listSessions`: the sidebar's rows are
|
|
137
|
+
* keyed by path, and deleting is not something you can undo. The `result` carries the refreshed
|
|
138
|
+
* `sessions` list, so the row that was deleted is gone from the screen as soon as it is gone from disk.
|
|
139
|
+
*/
|
|
140
|
+
export const DeleteSessionCommand = Type.Object({
|
|
141
|
+
type: Type.Literal("deleteSession"),
|
|
142
|
+
sessionPath: Id,
|
|
143
|
+
requestId: Type.Optional(RequestId),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* What the debug panel shows, and what the sidebar's model/thinking selectors list.
|
|
148
|
+
* `memoryBlock` is the block the last turn actually injected, or null when none was.
|
|
149
|
+
*/
|
|
150
|
+
export const DebugCommand = Type.Object({
|
|
151
|
+
type: Type.Literal("debug"),
|
|
152
|
+
conversationId: Id,
|
|
153
|
+
requestId: Type.Optional(RequestId),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The scheduler: what is queued, when it next runs, and what the last runs did
|
|
158
|
+
* (docs/architecture.md Phase 4.5). Not conversation-scoped — the queue belongs to the daemon.
|
|
159
|
+
*/
|
|
160
|
+
export const SchedulerCommand = Type.Object({
|
|
161
|
+
type: Type.Literal("scheduler"),
|
|
162
|
+
requestId: Type.Optional(RequestId),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* What the settings panel reads and edits: provider endpoints in `.flypi/agent/models.json` and MCP
|
|
167
|
+
* servers in `.flypi/agent/mcp.json`. Neither is conversation-scoped — they are the agent dir's config,
|
|
168
|
+
* like `listSessions` is the session files. `getConfig` answers with the current files; `saveConfig`
|
|
169
|
+
* applies edits and answers with the refreshed files, so the panel shows what is on disk after the write.
|
|
170
|
+
*
|
|
171
|
+
* An edit carries only the fields the user touched, and `models` only *adds* ids: the files hold far
|
|
172
|
+
* more than the panel shows (`compat`, `cost`, extra headers), and flypi never rewrites what it does not
|
|
173
|
+
* show. `remove: true` deletes one entry. `files` is the exception, and the way to reach what the forms do
|
|
174
|
+
* not hold: it replaces `models.json` or `settings.json` with the text typed in, checked against the text
|
|
175
|
+
* the editor was shown. See `src/agent/config-files.ts`.
|
|
176
|
+
*/
|
|
177
|
+
export const GetConfigCommand = Type.Object({
|
|
178
|
+
type: Type.Literal("getConfig"),
|
|
179
|
+
requestId: Type.Optional(RequestId),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
/** Names become provider keys and `mcp__<server>__<tool>` tool names, so the server's own rule is checked here. */
|
|
183
|
+
const ConfigName = Type.String({ minLength: 1, maxLength: 100, pattern: "^[A-Za-z0-9][A-Za-z0-9_-]*$" });
|
|
184
|
+
const Text = Type.String({ maxLength: 500 });
|
|
185
|
+
/**
|
|
186
|
+
* The OAuth fields the panel edits. pi's others (`clientName`, `clientRegistration`, `callbackPort`,
|
|
187
|
+
* `authServerMetadataUrl`) are read field by field as well, so an entry that has them keeps them.
|
|
188
|
+
*/
|
|
189
|
+
const OAuthFields = Type.Object({
|
|
190
|
+
clientId: Type.Optional(Text),
|
|
191
|
+
clientSecret: Type.Optional(Text),
|
|
192
|
+
callbackUrl: Type.Optional(Text),
|
|
193
|
+
scope: Type.Optional(Text),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/** pi's own tool-reach settings for a server (docs/mcp.md "Control tool exposure"). */
|
|
197
|
+
const McpExposure = Type.Union([
|
|
198
|
+
Type.Literal("direct"),
|
|
199
|
+
Type.Literal("deferred"),
|
|
200
|
+
Type.Literal("codemode"),
|
|
201
|
+
Type.Literal("hidden"),
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
export const EndpointEdit = Type.Object({
|
|
205
|
+
name: ConfigName,
|
|
206
|
+
baseUrl: Type.Optional(Text),
|
|
207
|
+
api: Type.Optional(Type.String({ maxLength: 100 })),
|
|
208
|
+
/** May be a literal, `${ENV_VAR}` or `!command` — passed through exactly as pi reads it. */
|
|
209
|
+
apiKey: Type.Optional(Text),
|
|
210
|
+
models: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 200 }), { maxItems: 100 })),
|
|
211
|
+
remove: Type.Optional(Type.Literal(true)),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
export const McpEdit = Type.Object({
|
|
215
|
+
name: ConfigName,
|
|
216
|
+
url: Type.Optional(Text),
|
|
217
|
+
command: Type.Optional(Text),
|
|
218
|
+
args: Type.Optional(Type.Array(Text, { maxItems: 100 })),
|
|
219
|
+
/** Written as `headers.Authorization`; the other headers of an entry are never touched. */
|
|
220
|
+
authorization: Type.Optional(Text),
|
|
221
|
+
/** Written as the whole `env` map (a row carries every entry the file has); an empty value removes that key. */
|
|
222
|
+
env: Type.Optional(Type.Record(Type.String(), Text)),
|
|
223
|
+
cwd: Type.Optional(Text),
|
|
224
|
+
description: Type.Optional(Text),
|
|
225
|
+
exposure: Type.Optional(McpExposure),
|
|
226
|
+
/** Per-tool overrides of `exposure`, keyed as pi reads them: an offered tool name or a `*` pattern. */
|
|
227
|
+
toolExposure: Type.Optional(Type.Record(Type.String(), McpExposure)),
|
|
228
|
+
oauth: Type.Optional(OAuthFields),
|
|
229
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
230
|
+
remove: Type.Optional(Type.Literal(true)),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Which endpoint a new conversation starts on — `defaultProvider` and `defaultModel` in
|
|
235
|
+
* `.flypi/agent/settings.json`, the pair `resolveConfiguredModel()` reads (src/agent/runtime.ts).
|
|
236
|
+
* The model is named as well as the provider because one endpoint can define several.
|
|
237
|
+
*/
|
|
238
|
+
export const DefaultsEdit = Type.Object({
|
|
239
|
+
provider: ConfigName,
|
|
240
|
+
model: Type.String({ minLength: 1, maxLength: 200 }),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/** The same pair as read from the file: plain strings, because settings.json may not name one yet. */
|
|
244
|
+
export const DefaultsRow = Type.Object({
|
|
245
|
+
provider: Type.String(),
|
|
246
|
+
model: Type.String(),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The two files the settings view offers as text — everything its forms do not hold (`compat`, cost,
|
|
251
|
+
* `contextWindow`, a model's `input`, settings.json's thinking level, tool list and `permissions`).
|
|
252
|
+
* `mcp.json` is not one of them: its form already covers every field the daemon reads.
|
|
253
|
+
*/
|
|
254
|
+
const ConfigFileName = Type.Union([Type.Literal("models"), Type.Literal("settings")]);
|
|
255
|
+
|
|
256
|
+
/** One of those files, exactly as it is on disk — `""` when it does not exist yet. */
|
|
257
|
+
export const ConfigFileRow = Type.Object({
|
|
258
|
+
name: ConfigFileName,
|
|
259
|
+
text: Type.String(),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Replace one of those files wholesale. `base` is the text the editor was loaded with: a file that has
|
|
264
|
+
* moved since then is refused rather than overwritten, because these files hold credentials and the
|
|
265
|
+
* panel's own lists write them too. The text has to parse as a JSON object — see `applyConfigEdits`.
|
|
266
|
+
*/
|
|
267
|
+
export const ConfigFileEdit = Type.Object({
|
|
268
|
+
name: ConfigFileName,
|
|
269
|
+
text: Type.String({ maxLength: 200_000 }),
|
|
270
|
+
base: Type.Optional(Type.String({ maxLength: 200_000 })),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
export const SaveConfigCommand = Type.Object({
|
|
274
|
+
type: Type.Literal("saveConfig"),
|
|
275
|
+
endpoints: Type.Optional(Type.Array(EndpointEdit, { maxItems: 50 })),
|
|
276
|
+
mcpServers: Type.Optional(Type.Array(McpEdit, { maxItems: 50 })),
|
|
277
|
+
/** Sets the default. Omitted means settings.json is not touched at all. */
|
|
278
|
+
defaults: Type.Optional(DefaultsEdit),
|
|
279
|
+
/** Replaces one of the two files with the text typed in the raw editor. */
|
|
280
|
+
files: Type.Optional(Type.Array(ConfigFileEdit, { maxItems: 2 })),
|
|
281
|
+
requestId: Type.Optional(RequestId),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Are the configured MCP servers reachable? Asked for when the settings view is opened, never polled: a
|
|
286
|
+
* probe spawns an stdio server and opens an HTTP connection, which is not something to do on a timer.
|
|
287
|
+
* Answers with `mcpStatus` — flypi's own connections to those servers, not the ones a running conversation
|
|
288
|
+
* happens to hold (src/agent/mcp-status.ts).
|
|
289
|
+
*/
|
|
290
|
+
export const McpStatusCommand = Type.Object({
|
|
291
|
+
type: Type.Literal("mcpStatus"),
|
|
292
|
+
requestId: Type.Optional(RequestId),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Sign one configured MCP server in through the browser (docs/architecture.md Phase 5, risk 5). The request
|
|
297
|
+
* stays open until the sign-in ends: the URL to open arrives as an `mcpAuth` frame, and the ack is the
|
|
298
|
+
* refreshed `mcpStatus` — the row the user was looking at, now answering. A server this cannot sign in to
|
|
299
|
+
* (a stdio entry, one with its own `Authorization` header) is an `error` frame for the `requestId`.
|
|
300
|
+
*/
|
|
301
|
+
export const McpSignInCommand = Type.Object({
|
|
302
|
+
type: Type.Literal("mcpSignIn"),
|
|
303
|
+
name: ConfigName,
|
|
304
|
+
requestId: Type.Optional(RequestId),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The address the browser was redirected to, pasted back for the sign-in named `name` — the answer to its
|
|
309
|
+
* `mcpAuth` frame, so no `requestId` of its own. Empty cancels. Nothing else answers a sign-in: when the
|
|
310
|
+
* browser can reach this machine, the loopback callback finishes it and no frame is needed.
|
|
311
|
+
*/
|
|
312
|
+
export const McpRedirectCommand = Type.Object({
|
|
313
|
+
type: Type.Literal("mcpRedirect"),
|
|
314
|
+
name: ConfigName,
|
|
315
|
+
url: Type.String({ maxLength: 4096 }),
|
|
316
|
+
requestId: Type.Optional(RequestId),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* One extension package: pi's own source string, kept exactly as `.flypi/agent/settings.json` holds it — an
|
|
321
|
+
* npm spec, a git URL or a path (pi's docs/packages.md). No name pattern: a URL and a path are both legal.
|
|
322
|
+
*/
|
|
323
|
+
const PackageSource = Type.String({ minLength: 1, maxLength: 500 });
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* What the settings view lists for `packages`. `installed` is false when the entry names something the code
|
|
327
|
+
* is not on disk for yet; `update` is the daemon's daily answer (src/agent/packages.ts), never a live check —
|
|
328
|
+
* asking the registries and every git remote is not something to do when a panel opens.
|
|
329
|
+
*/
|
|
330
|
+
export const PackageRow = Type.Object({
|
|
331
|
+
source: Type.String({ minLength: 1 }),
|
|
332
|
+
scope: Type.Union([Type.Literal("user"), Type.Literal("project")]),
|
|
333
|
+
installed: Type.Boolean(),
|
|
334
|
+
path: Type.String(),
|
|
335
|
+
update: Type.Optional(Type.Boolean()),
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
export const PackagesSnapshot = Type.Object({
|
|
339
|
+
packages: Type.Array(PackageRow),
|
|
340
|
+
/** When the daemon last asked the registries; absent until it has. */
|
|
341
|
+
checkedAt: Type.Optional(Type.String()),
|
|
342
|
+
/** A `settings.json` pi could not read — said, rather than shown as "no packages". */
|
|
343
|
+
errors: Type.Array(Type.String()),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The extension packages the agent loads, as `.flypi/agent/settings.json` declares them. Asked when the
|
|
348
|
+
* settings view opens; reaches no network (src/agent/packages.ts).
|
|
349
|
+
*/
|
|
350
|
+
export const PackagesCommand = Type.Object({
|
|
351
|
+
type: Type.Literal("packages"),
|
|
352
|
+
requestId: Type.Optional(RequestId),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Install, remove or update packages. pi's own package manager does it, so its source forms and its rules
|
|
357
|
+
* apply: a source naming a version or a git ref is pinned and `update` leaves it where it is. The install
|
|
358
|
+
* runs npm or git, which takes seconds — the page shows a busy state rather than polling. The `result` carries
|
|
359
|
+
* the refreshed list; a source pi could not install is an `error` frame, and edits earlier in the same request
|
|
360
|
+
* have already been applied.
|
|
361
|
+
*/
|
|
362
|
+
export const SavePackagesCommand = Type.Object({
|
|
363
|
+
type: Type.Literal("savePackages"),
|
|
364
|
+
install: Type.Optional(Type.Array(PackageSource, { maxItems: 10 })),
|
|
365
|
+
remove: Type.Optional(Type.Array(PackageSource, { maxItems: 10 })),
|
|
366
|
+
update: Type.Optional(Type.Array(PackageSource, { maxItems: 10 })),
|
|
367
|
+
requestId: Type.Optional(RequestId),
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
/** What kind of secret a row claims to be — the same list the vault's schema allows (src/secrets/crypto.ts). */
|
|
371
|
+
export const SecretKind = Type.Union([
|
|
372
|
+
Type.Literal("password"),
|
|
373
|
+
Type.Literal("ssh_private_key"),
|
|
374
|
+
Type.Literal("ssh_public_key"),
|
|
375
|
+
Type.Literal("yaml"),
|
|
376
|
+
Type.Literal("generic"),
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The vault (docs/secrets.md Phase 5.5). Asked for when the Secrets tab is opened, like the config files are:
|
|
381
|
+
* nothing here is polled, and a snapshot never carries a value — only masks, counters and key state. The
|
|
382
|
+
* plaintext comes from `revealSecret`, one name at a time, and the page forgets it again after a few seconds.
|
|
383
|
+
*/
|
|
384
|
+
export const SecretsCommand = Type.Object({
|
|
385
|
+
type: Type.Literal("secrets"),
|
|
386
|
+
requestId: Type.Optional(RequestId),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Install the update the daemon has already downloaded, now rather than at the idle moment
|
|
391
|
+
* (docs/architecture.md Phase 7). The install replaces the files the daemon runs from, so it restarts and
|
|
392
|
+
* this socket drops with it — the page re-reads its token and reconnects (ui/lib/client.ts). A process that
|
|
393
|
+
* does not manage its own updates (a checkout, a headless server, nothing downloaded) answers an error frame.
|
|
394
|
+
*/
|
|
395
|
+
export const InstallUpdateCommand = Type.Object({
|
|
396
|
+
type: Type.Literal("installUpdate"),
|
|
397
|
+
requestId: Type.Optional(RequestId),
|
|
398
|
+
});
|
|
399
|
+
/**
|
|
400
|
+
* One edit to one secret. `value` means put (or rotate, when the name is already there); `remove` is a
|
|
401
|
+
* tombstone; `restore` is the undo that tombstone makes possible without the page ever holding the value.
|
|
402
|
+
*/
|
|
403
|
+
export const SecretEdit = Type.Object({
|
|
404
|
+
name: Type.String({ minLength: 1, maxLength: 120 }),
|
|
405
|
+
kind: Type.Optional(SecretKind),
|
|
406
|
+
value: Type.Optional(Type.String({ maxLength: 65_536 })),
|
|
407
|
+
/** Non-secret by convention (host, username, the path of an SSH key). Omitted means "leave what is stored". */
|
|
408
|
+
meta: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
409
|
+
remove: Type.Optional(Type.Boolean()),
|
|
410
|
+
restore: Type.Optional(Type.Boolean()),
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
export const SaveSecretsCommand = Type.Object({
|
|
414
|
+
type: Type.Literal("saveSecrets"),
|
|
415
|
+
secrets: Type.Array(SecretEdit, { maxItems: 20 }),
|
|
416
|
+
requestId: Type.Optional(RequestId),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
/** Ask for one value, once. This is the only frame in the protocol that carries a secret in plaintext. */
|
|
420
|
+
export const RevealSecretCommand = Type.Object({
|
|
421
|
+
type: Type.Literal("revealSecret"),
|
|
422
|
+
name: Type.String({ minLength: 1, maxLength: 120 }),
|
|
423
|
+
requestId: Type.Optional(RequestId),
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The memory browser (docs/memory.md Phase 6). Asked when the Memory view opens, like the vault is: nothing
|
|
428
|
+
* here is polled. The snapshot is the store as it stands — every row in every status (so a superseded chain
|
|
429
|
+
* and a tombstone are both visible), the turns that say what recall was shown and what an answer cited, the
|
|
430
|
+
* wiki as it was last rendered, and the hit-rate numbers.
|
|
431
|
+
*
|
|
432
|
+
* It is a **read of the store**, not a search: the browser filters what it was given, so a row that a search
|
|
433
|
+
* would rank last is still listed. And it is answered only by the process that owns the memory database —
|
|
434
|
+
* `flypi start` writes it, the dev server reads it, and a process with neither says so in an error frame
|
|
435
|
+
* rather than showing an empty store.
|
|
436
|
+
*/
|
|
437
|
+
export const MemoryCommand = Type.Object({
|
|
438
|
+
type: Type.Literal("memory"),
|
|
439
|
+
requestId: Type.Optional(RequestId),
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
/** One change to one memory. Editing is the browser's, and it is the only place a memory is edited at all —
|
|
443
|
+
* no automatic pass edits a body, and nothing here can set provenance. */
|
|
444
|
+
export const MemoryEdit = Type.Object({
|
|
445
|
+
id: Type.Integer({ minimum: 1 }),
|
|
446
|
+
body: Type.Optional(Type.String({ minLength: 1, maxLength: 8000 })),
|
|
447
|
+
kind: Type.Optional(Type.String({ minLength: 1, maxLength: 60 })),
|
|
448
|
+
tags: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 60 }), { maxItems: 20 })),
|
|
449
|
+
/** A tombstone, never a removal: `status='deleted'` takes a user action and stays undoable. */
|
|
450
|
+
remove: Type.Optional(Type.Boolean()),
|
|
451
|
+
/** Reverse one audited change — the undo for anything a sweep did, and for a delete. */
|
|
452
|
+
undoAudit: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
export const SaveMemoryCommand = Type.Object({
|
|
456
|
+
type: Type.Literal("saveMemory"),
|
|
457
|
+
memories: Type.Array(MemoryEdit, { maxItems: 20 }),
|
|
458
|
+
requestId: Type.Optional(RequestId),
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
/** One audited change to a memory, oldest first. `preImage` is the stored JSON text, shown rather than
|
|
462
|
+
* re-rendered: it is what the row looked like before, and the page has no business guessing at columns. */
|
|
463
|
+
export const MemoryAuditRow = Type.Object({
|
|
464
|
+
id: Type.Integer(),
|
|
465
|
+
action: Type.String(),
|
|
466
|
+
actor: Type.String(),
|
|
467
|
+
reason: Type.String(),
|
|
468
|
+
createdAt: Type.String(),
|
|
469
|
+
preImage: Type.String(),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
export const MemoryEntryRow = Type.Object({
|
|
473
|
+
id: Type.Integer(),
|
|
474
|
+
kind: Type.String(),
|
|
475
|
+
body: Type.String(),
|
|
476
|
+
tags: Type.Array(Type.String()),
|
|
477
|
+
status: Type.Union([Type.Literal("active"), Type.Literal("superseded"), Type.Literal("deleted")]),
|
|
478
|
+
/** Authority, set by code at write time and never by the body: the browser shows it because a reader
|
|
479
|
+
* cannot use authority it cannot see. */
|
|
480
|
+
originClass: Type.Union([Type.Literal("owner"), Type.Literal("agent"), Type.Literal("untrusted"), Type.Literal("system")]),
|
|
481
|
+
sessionKind: Type.String(),
|
|
482
|
+
strength: Type.Number(),
|
|
483
|
+
useCount: Type.Integer(),
|
|
484
|
+
citedCount: Type.Integer(),
|
|
485
|
+
createdAt: Type.String(),
|
|
486
|
+
lastVerifiedAt: Type.Union([Type.String(), Type.Null()]),
|
|
487
|
+
/** The supersede chain, in both directions, so a merged row can be traced to what replaced it. */
|
|
488
|
+
supersedes: Type.Union([Type.Integer(), Type.Null()]),
|
|
489
|
+
supersededBy: Type.Union([Type.Integer(), Type.Null()]),
|
|
490
|
+
mergedFrom: Type.Array(Type.Integer()),
|
|
491
|
+
history: Type.Array(MemoryAuditRow),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* One recorded turn: the question, which rows recall fetched for it, which ones the answer cited, and what
|
|
496
|
+
* the fetched rows would have cost in prompt tokens. That last number is what makes "why was this injected"
|
|
497
|
+
* answerable in cost rather than in feelings, and it is an estimate (`estimateTokens`) because the store
|
|
498
|
+
* keeps text, not a tokenizer.
|
|
499
|
+
*/
|
|
500
|
+
export const MemoryTurnRow = Type.Object({
|
|
501
|
+
id: Type.Integer(),
|
|
502
|
+
conversationId: Type.String(),
|
|
503
|
+
createdAt: Type.String(),
|
|
504
|
+
userText: Type.String(),
|
|
505
|
+
retrieved: Type.Array(Type.Integer()),
|
|
506
|
+
cited: Type.Array(Type.Integer()),
|
|
507
|
+
injectedTokens: Type.Integer(),
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
export const WikiSectionRow = Type.Object({
|
|
511
|
+
heading: Type.String(),
|
|
512
|
+
body: Type.String(),
|
|
513
|
+
claimIds: Type.Array(Type.Integer()),
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
export const WikiPageRow = Type.Object({
|
|
517
|
+
slug: Type.String(),
|
|
518
|
+
title: Type.String(),
|
|
519
|
+
topicKey: Type.String(),
|
|
520
|
+
body: Type.String(),
|
|
521
|
+
claimIds: Type.Array(Type.Integer()),
|
|
522
|
+
/** Null while the prose is the template's rather than a model's (src/memory/wiki.ts). */
|
|
523
|
+
model: Type.Union([Type.String(), Type.Null()]),
|
|
524
|
+
generatedAt: Type.String(),
|
|
525
|
+
sections: Type.Array(WikiSectionRow),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
export const MemoryStats = Type.Object({
|
|
529
|
+
turns: Type.Integer(),
|
|
530
|
+
turnsShownMemories: Type.Integer(),
|
|
531
|
+
turnsCiting: Type.Integer(),
|
|
532
|
+
memoriesRetrieved: Type.Integer(),
|
|
533
|
+
memoriesCited: Type.Integer(),
|
|
534
|
+
rate: Type.Number(),
|
|
535
|
+
/** The embedding index: how many active rows have a vector, and how many of them are from another model. */
|
|
536
|
+
indexRows: Type.Integer(),
|
|
537
|
+
indexStale: Type.Integer(),
|
|
538
|
+
embedModel: Type.Union([Type.String(), Type.Null()]),
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
export const MemorySnapshot = Type.Object({
|
|
542
|
+
memories: Type.Array(MemoryEntryRow),
|
|
543
|
+
turns: Type.Array(MemoryTurnRow),
|
|
544
|
+
wiki: Type.Array(WikiPageRow),
|
|
545
|
+
stats: MemoryStats,
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
export const ClientCommandSchema = Type.Union([
|
|
549
|
+
PromptCommand,
|
|
550
|
+
AbortCommand,
|
|
551
|
+
NewSessionCommand,
|
|
552
|
+
SwitchSessionCommand,
|
|
553
|
+
ForkCommand,
|
|
554
|
+
SetModelCommand,
|
|
555
|
+
SetThinkingLevelCommand,
|
|
556
|
+
ResumeCommand,
|
|
557
|
+
ExtensionReplyCommand,
|
|
558
|
+
ListSessionsCommand,
|
|
559
|
+
RenameSessionCommand,
|
|
560
|
+
DeleteSessionCommand,
|
|
561
|
+
DebugCommand,
|
|
562
|
+
SchedulerCommand,
|
|
563
|
+
GetConfigCommand,
|
|
564
|
+
SaveConfigCommand,
|
|
565
|
+
McpStatusCommand,
|
|
566
|
+
McpSignInCommand,
|
|
567
|
+
McpRedirectCommand,
|
|
568
|
+
PackagesCommand,
|
|
569
|
+
SavePackagesCommand,
|
|
570
|
+
SecretsCommand,
|
|
571
|
+
SaveSecretsCommand,
|
|
572
|
+
RevealSecretCommand,
|
|
573
|
+
MemoryCommand,
|
|
574
|
+
SaveMemoryCommand,
|
|
575
|
+
InstallUpdateCommand,
|
|
576
|
+
]);
|
|
577
|
+
export type ClientCommand = Static<typeof ClientCommandSchema>;
|
|
578
|
+
|
|
579
|
+
/** A pi session event, passed through verbatim. Only the discriminator is checked here. */
|
|
580
|
+
export const SessionEventFrameBody = Type.Object({ type: Type.String() }, { additionalProperties: true });
|
|
581
|
+
|
|
582
|
+
export const HelloFrame = Type.Object({
|
|
583
|
+
type: Type.Literal("hello"),
|
|
584
|
+
protocolVersion: Type.Integer(),
|
|
585
|
+
serverVersion: Type.String(),
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
export const EventFrame = Type.Object({
|
|
589
|
+
type: Type.Literal("event"),
|
|
590
|
+
conversationId: Id,
|
|
591
|
+
seq: Type.Integer({ minimum: 1 }),
|
|
592
|
+
event: SessionEventFrameBody,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
/** A row in the sidebar. `firstMessage` is pi's own excerpt, so flypi stores nothing extra. */
|
|
596
|
+
export const SessionSummary = Type.Object({
|
|
597
|
+
path: Id,
|
|
598
|
+
name: Type.Optional(Type.String()),
|
|
599
|
+
modified: Type.String(),
|
|
600
|
+
messageCount: Type.Integer(),
|
|
601
|
+
firstMessage: Type.String(),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* pi's own context estimate for the session (`AgentSession.getContextUsage()`): what the next request
|
|
606
|
+
* would carry, against the model's window. `tokens`/`percent` are null when pi cannot know yet — right
|
|
607
|
+
* after a compaction, before the next LLM response.
|
|
608
|
+
*/
|
|
609
|
+
export const ContextUsage = Type.Object({
|
|
610
|
+
tokens: Type.Union([Type.Number(), Type.Null()]),
|
|
611
|
+
contextWindow: Type.Number(),
|
|
612
|
+
percent: Type.Union([Type.Number(), Type.Null()]),
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
export const ModelEntry = Type.Object({
|
|
616
|
+
provider: Id,
|
|
617
|
+
modelId: Type.String(),
|
|
618
|
+
/**
|
|
619
|
+
* pi's own capability list (`["text"]`, `["text", "image"]`, …), passed through rather than translated:
|
|
620
|
+
* it is the field pi-ai itself uses to decide whether an image reaches the provider, so it is the only
|
|
621
|
+
* honest way to say "this model cannot see what you pasted". Empty means the model declares nothing.
|
|
622
|
+
*/
|
|
623
|
+
input: Type.Array(Type.String()),
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* One slash command the session answers to, as the chat's `/` list shows it. `name` is what has to be
|
|
628
|
+
* typed: pi's own invocation name, so a command whose name collides with a built-in one arrives as
|
|
629
|
+
* `name:2` rather than being unreachable.
|
|
630
|
+
*/
|
|
631
|
+
export const CommandRow = Type.Object({
|
|
632
|
+
name: Type.String({ minLength: 1 }),
|
|
633
|
+
description: Type.String(),
|
|
634
|
+
source: Type.Union([Type.Literal("extension"), Type.Literal("prompt")]),
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
export const DebugInfo = Type.Object({
|
|
638
|
+
systemPrompt: Type.String(),
|
|
639
|
+
activeTools: Type.Array(Type.String()),
|
|
640
|
+
/** Every slash command this conversation has: extensions first, then prompt templates. */
|
|
641
|
+
commands: Type.Array(CommandRow),
|
|
642
|
+
models: Type.Array(ModelEntry),
|
|
643
|
+
model: Type.Optional(ModelEntry),
|
|
644
|
+
thinkingLevel: Type.String(),
|
|
645
|
+
sessionName: Type.Optional(Type.String()),
|
|
646
|
+
/**
|
|
647
|
+
* The `<relevant_memories>` block the last turn actually injected, or null when none was — the
|
|
648
|
+
* provenance for automatic recall (docs/memory.md Phase 4). It is not what a recall *would* return
|
|
649
|
+
* now, and it is cleared when a new turn starts. The block is never in the session file or an export,
|
|
650
|
+
* so this snapshot is the only place it is visible.
|
|
651
|
+
*/
|
|
652
|
+
memoryBlock: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
653
|
+
/** The chat's context gauge. Absent only when the session has no usable model window. */
|
|
654
|
+
context: Type.Optional(ContextUsage),
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* One row of a config file, as the panel shows it. Names are plain strings here: a file the user edited
|
|
659
|
+
* by hand may hold something the panel did not write, and showing it beats refusing the whole snapshot.
|
|
660
|
+
*/
|
|
661
|
+
export const EndpointRow = Type.Object({
|
|
662
|
+
name: Type.String({ minLength: 1 }),
|
|
663
|
+
baseUrl: Type.String(),
|
|
664
|
+
api: Type.String(),
|
|
665
|
+
apiKey: Type.String(),
|
|
666
|
+
models: Type.Array(Type.String()),
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
export const McpRow = Type.Object({
|
|
670
|
+
name: Type.String({ minLength: 1 }),
|
|
671
|
+
url: Type.String(),
|
|
672
|
+
command: Type.String(),
|
|
673
|
+
args: Type.Array(Type.String()),
|
|
674
|
+
authorization: Type.String(),
|
|
675
|
+
env: Type.Record(Type.String(), Type.String()),
|
|
676
|
+
cwd: Type.String(),
|
|
677
|
+
description: Type.String(),
|
|
678
|
+
exposure: McpExposure,
|
|
679
|
+
toolExposure: Type.Record(Type.String(), McpExposure),
|
|
680
|
+
oauth: Type.Object({ clientId: Type.String(), clientSecret: Type.String(), callbackUrl: Type.String(), scope: Type.String() }),
|
|
681
|
+
enabled: Type.Boolean(),
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
export const ConfigSnapshot = Type.Object({
|
|
685
|
+
endpoints: Type.Array(EndpointRow),
|
|
686
|
+
mcpServers: Type.Array(McpRow),
|
|
687
|
+
/** What a new conversation would start on; empty strings when settings.json does not say. */
|
|
688
|
+
defaults: DefaultsRow,
|
|
689
|
+
/** `models.json` and `settings.json` as their exact bytes, for the raw editor. */
|
|
690
|
+
files: Type.Array(ConfigFileRow),
|
|
691
|
+
/** A config file that will not parse — the panel says so instead of listing an empty config. */
|
|
692
|
+
errors: Type.Array(Type.String()),
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* One server as the status panel shows it. `tools` is what the server announced when it answered — the two
|
|
697
|
+
* flags are its own annotations (`destructiveHint`, `openWorldHint`), never flypi's guess, and a tool that
|
|
698
|
+
* announced nothing reads as `false`/`false`. This is what a user reads to decide which names belong on
|
|
699
|
+
* `permissions.askBeforeUse` (src/agent/permission-gate.ts), because that gate only ever sees a tool name.
|
|
700
|
+
*/
|
|
701
|
+
export const McpToolRow = Type.Object({
|
|
702
|
+
name: Type.String({ minLength: 1 }),
|
|
703
|
+
destructive: Type.Boolean(),
|
|
704
|
+
openWorld: Type.Boolean(),
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
export const McpStatusRow = Type.Object({
|
|
708
|
+
name: Type.String({ minLength: 1 }),
|
|
709
|
+
/** "not answering" is a state of its own: a server that hangs is not the same as one that refused. */
|
|
710
|
+
state: Type.Union([Type.Literal("connected"), Type.Literal("disabled"), Type.Literal("failed"), Type.Literal("needs-auth")]),
|
|
711
|
+
transport: Type.Union([Type.Literal("http"), Type.Literal("stdio")]),
|
|
712
|
+
/** The server's own words, for `failed` and `needs-auth`. */
|
|
713
|
+
error: Type.Optional(Type.String()),
|
|
714
|
+
tools: Type.Array(McpToolRow),
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
export const McpStatusSnapshot = Type.Object({
|
|
718
|
+
servers: Type.Array(McpStatusRow),
|
|
719
|
+
/** The tail of what pi's MCP extension logged for the connections a session really made. Empty when nothing was logged. */
|
|
720
|
+
log: Type.String(),
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* One secret as the vault reports it. There is no value field, by construction: the mask is what a list, a
|
|
725
|
+
* debug frame and this page are allowed to hold. `fingerprint` is a hash, so it identifies a value without
|
|
726
|
+
* showing it — which is how a user tells two rows with the same mask apart.
|
|
727
|
+
*/
|
|
728
|
+
export const SecretRow = Type.Object({
|
|
729
|
+
id: Type.Integer(),
|
|
730
|
+
name: Type.String(),
|
|
731
|
+
kind: SecretKind,
|
|
732
|
+
mask: Type.String(),
|
|
733
|
+
meta: Type.Record(Type.String(), Type.String()),
|
|
734
|
+
status: Type.Union([Type.Literal("active"), Type.Literal("deleted")]),
|
|
735
|
+
createdAt: Type.String(),
|
|
736
|
+
updatedAt: Type.Optional(Type.String()),
|
|
737
|
+
lastUsedAt: Type.Optional(Type.String()),
|
|
738
|
+
useCount: Type.Integer(),
|
|
739
|
+
keyVersion: Type.Integer(),
|
|
740
|
+
fingerprint: Type.String(),
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* A config entry that points at a vault name (`!flypi secret print NAME`). This is the "used by" half: a
|
|
745
|
+
* secret cannot be deleted safely without knowing which auth.json or mcp.json entry would stop working, and
|
|
746
|
+
* the vault itself cannot know — the files are the only place that relationship is written down.
|
|
747
|
+
*/
|
|
748
|
+
export const SecretReferenceRow = Type.Object({
|
|
749
|
+
file: Type.String(),
|
|
750
|
+
path: Type.String(),
|
|
751
|
+
name: Type.String(),
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
export const KeyState = Type.Object({
|
|
755
|
+
provider: Type.String(),
|
|
756
|
+
currentVersion: Type.Integer(),
|
|
757
|
+
rows: Type.Integer(),
|
|
758
|
+
/** Rows still sealed under an older key: the drift the status panel surfaces (docs/secrets.md). */
|
|
759
|
+
stale: Type.Integer(),
|
|
760
|
+
reencryptQueued: Type.Boolean(),
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
export const SecretsSnapshot = Type.Object({
|
|
764
|
+
secrets: Type.Array(SecretRow),
|
|
765
|
+
references: Type.Array(SecretReferenceRow),
|
|
766
|
+
key: KeyState,
|
|
767
|
+
/** A secret too short to redact from a memory or a transcript (src/secrets/redact.ts). Shown, not hidden. */
|
|
768
|
+
tooShortToRedact: Type.Array(Type.String()),
|
|
769
|
+
/** A vault this process could not open, in the panel's words rather than an empty list. */
|
|
770
|
+
error: Type.Optional(Type.String()),
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
/** One revealed value. Transient on both sides: the page drops it after a few seconds. */
|
|
774
|
+
export const RevealedSecret = Type.Object({
|
|
775
|
+
name: Type.String(),
|
|
776
|
+
value: Type.String(),
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* One row of the daemon's job queue, and one recorded attempt. `detail` is what the job reported —
|
|
781
|
+
* for `memory.reembed`, which ids it re-embedded — and the reason it failed when it failed.
|
|
782
|
+
*/
|
|
783
|
+
export const JobRow = Type.Object({
|
|
784
|
+
kind: Type.String(),
|
|
785
|
+
nextRunAt: Type.String(),
|
|
786
|
+
running: Type.Boolean(),
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
export const JobRunRow = Type.Object({
|
|
790
|
+
kind: Type.String(),
|
|
791
|
+
status: Type.Union([Type.Literal("running"), Type.Literal("ok"), Type.Literal("failed")]),
|
|
792
|
+
startedAt: Type.String(),
|
|
793
|
+
finishedAt: Type.Optional(Type.String()),
|
|
794
|
+
detail: Type.Optional(Type.String()),
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
export const SchedulerSnapshot = Type.Object({
|
|
798
|
+
jobs: Type.Array(JobRow),
|
|
799
|
+
jobRuns: Type.Array(JobRunRow),
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* A turn the daemon found half-finished when it started (its row was still `running`). The chat says
|
|
804
|
+
* so when it opens that session rather than letting the missing answer look like a bug (Phase 4.5).
|
|
805
|
+
*/
|
|
806
|
+
export const InterruptedRun = Type.Object({
|
|
807
|
+
at: Type.String(),
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
export const ResultFrame = Type.Object({
|
|
811
|
+
type: Type.Literal("result"),
|
|
812
|
+
requestId: RequestId,
|
|
813
|
+
conversationId: Type.Optional(Id),
|
|
814
|
+
sessionPath: Type.Optional(Id),
|
|
815
|
+
sessions: Type.Optional(Type.Array(SessionSummary)),
|
|
816
|
+
debug: Type.Optional(DebugInfo),
|
|
817
|
+
mcpStatus: Type.Optional(McpStatusSnapshot),
|
|
818
|
+
config: Type.Optional(ConfigSnapshot),
|
|
819
|
+
packages: Type.Optional(PackagesSnapshot),
|
|
820
|
+
scheduler: Type.Optional(SchedulerSnapshot),
|
|
821
|
+
secrets: Type.Optional(SecretsSnapshot),
|
|
822
|
+
revealed: Type.Optional(RevealedSecret),
|
|
823
|
+
memory: Type.Optional(MemorySnapshot),
|
|
824
|
+
interrupted: Type.Optional(InterruptedRun),
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* The URL a sign-in needs opened (src/agent/mcp-signin.ts). Pushed while the `mcpSignIn` request is still
|
|
829
|
+
* open, and the only frame of its kind: it belongs to a sign-in the page started, not to a request it is
|
|
830
|
+
* waiting on. The page shows it as a link and a paste box.
|
|
831
|
+
*/
|
|
832
|
+
export const McpAuthFrame = Type.Object({
|
|
833
|
+
type: Type.Literal("mcpAuth"),
|
|
834
|
+
name: ConfigName,
|
|
835
|
+
url: Type.String({ minLength: 1, maxLength: 4096 }),
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* An extension spoke to the user (`src/server/extension-ui.ts`). `notify` needs no answer; `select`,
|
|
840
|
+
* `confirm` and `input` carry a `dialogId` the page answers with an `extensionReply` command, and the
|
|
841
|
+
* extension's handler waits for it. Nothing else on pi's extension UI surface has a browser equivalent,
|
|
842
|
+
* so those calls stay no-ops.
|
|
843
|
+
*/
|
|
844
|
+
export const ExtensionUiFrame = Type.Object({
|
|
845
|
+
type: Type.Literal("extensionUi"),
|
|
846
|
+
conversationId: Id,
|
|
847
|
+
kind: Type.Union([
|
|
848
|
+
Type.Literal("notify"),
|
|
849
|
+
Type.Literal("select"),
|
|
850
|
+
Type.Literal("confirm"),
|
|
851
|
+
Type.Literal("input"),
|
|
852
|
+
]),
|
|
853
|
+
message: Type.String(),
|
|
854
|
+
title: Type.Optional(Type.String()),
|
|
855
|
+
/** The choices for a `select`. */
|
|
856
|
+
options: Type.Optional(Type.Array(Type.String())),
|
|
857
|
+
level: Type.Optional(Type.Union([Type.Literal("info"), Type.Literal("warning"), Type.Literal("error")])),
|
|
858
|
+
dialogId: Type.Optional(Id),
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
export const ErrorFrame = Type.Object({
|
|
862
|
+
type: Type.Literal("error"),
|
|
863
|
+
message: Type.String(),
|
|
864
|
+
/** Stable reason for clients: `replay_gap` means some events were already evicted. */
|
|
865
|
+
code: Type.Optional(Type.String()),
|
|
866
|
+
conversationId: Type.Optional(Id),
|
|
867
|
+
requestId: Type.Optional(RequestId),
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
export const ServerFrameSchema = Type.Union([HelloFrame, EventFrame, ResultFrame, McpAuthFrame, ExtensionUiFrame, ErrorFrame]);
|
|
871
|
+
export type HelloFrame = Static<typeof HelloFrame>;
|
|
872
|
+
export type EventFrame = Static<typeof EventFrame>;
|
|
873
|
+
export type ResultFrame = Static<typeof ResultFrame>;
|
|
874
|
+
export type ErrorFrame = Static<typeof ErrorFrame>;
|
|
875
|
+
export type McpAuthFrame = Static<typeof McpAuthFrame>;
|
|
876
|
+
export type ExtensionUiFrame = Static<typeof ExtensionUiFrame>;
|
|
877
|
+
export type CommandRow = Static<typeof CommandRow>;
|
|
878
|
+
export type ServerFrame = Static<typeof ServerFrameSchema>;
|
|
879
|
+
export type SessionSummary = Static<typeof SessionSummary>;
|
|
880
|
+
export type DebugInfo = Static<typeof DebugInfo>;
|
|
881
|
+
export type ConfigSnapshot = Static<typeof ConfigSnapshot>;
|
|
882
|
+
export type SchedulerSnapshot = Static<typeof SchedulerSnapshot>;
|
|
883
|
+
export type JobRow = Static<typeof JobRow>;
|
|
884
|
+
export type JobRunRow = Static<typeof JobRunRow>;
|
|
885
|
+
export type InterruptedRun = Static<typeof InterruptedRun>;
|
|
886
|
+
export type DefaultsEdit = Static<typeof DefaultsEdit>;
|
|
887
|
+
export type DefaultsRow = Static<typeof DefaultsRow>;
|
|
888
|
+
export type ConfigFileRow = Static<typeof ConfigFileRow>;
|
|
889
|
+
export type ConfigFileEdit = Static<typeof ConfigFileEdit>;
|
|
890
|
+
export type EndpointRow = Static<typeof EndpointRow>;
|
|
891
|
+
export type McpRow = Static<typeof McpRow>;
|
|
892
|
+
export type McpStatusRow = Static<typeof McpStatusRow>;
|
|
893
|
+
export type McpToolRow = Static<typeof McpToolRow>;
|
|
894
|
+
export type McpStatusSnapshot = Static<typeof McpStatusSnapshot>;
|
|
895
|
+
export type EndpointEdit = Static<typeof EndpointEdit>;
|
|
896
|
+
export type McpEdit = Static<typeof McpEdit>;
|
|
897
|
+
export type McpExposure = Static<typeof McpExposure>;
|
|
898
|
+
export type McpOAuth = Static<typeof OAuthFields>;
|
|
899
|
+
export type PackageRow = Static<typeof PackageRow>;
|
|
900
|
+
export type PackagesSnapshot = Static<typeof PackagesSnapshot>;
|
|
901
|
+
export type SecretKind = Static<typeof SecretKind>;
|
|
902
|
+
export type SecretRow = Static<typeof SecretRow>;
|
|
903
|
+
export type SecretReferenceRow = Static<typeof SecretReferenceRow>;
|
|
904
|
+
export type SecretsSnapshot = Static<typeof SecretsSnapshot>;
|
|
905
|
+
export type RevealedSecret = Static<typeof RevealedSecret>;
|
|
906
|
+
export type KeyState = Static<typeof KeyState>;
|
|
907
|
+
export type SecretEdit = Static<typeof SecretEdit>;
|
|
908
|
+
export type MemoryEdit = Static<typeof MemoryEdit>;
|
|
909
|
+
export type MemoryEntryRow = Static<typeof MemoryEntryRow>;
|
|
910
|
+
export type MemoryAuditRow = Static<typeof MemoryAuditRow>;
|
|
911
|
+
export type MemoryTurnRow = Static<typeof MemoryTurnRow>;
|
|
912
|
+
export type MemoryStats = Static<typeof MemoryStats>;
|
|
913
|
+
export type MemorySnapshot = Static<typeof MemorySnapshot>;
|
|
914
|
+
export type WikiPageRow = Static<typeof WikiPageRow>;
|
|
915
|
+
export type WikiSectionRow = Static<typeof WikiSectionRow>;
|
|
916
|
+
export type ModelEntry = Static<typeof ModelEntry>;
|
|
917
|
+
export type ContextUsage = Static<typeof ContextUsage>;
|
|
918
|
+
|
|
919
|
+
/** Parse a client frame. Never throws: a bad frame is an error message, not a stack trace. */
|
|
920
|
+
export function parseClientCommand(raw: string): { ok: true; command: ClientCommand } | { ok: false; message: string } {
|
|
921
|
+
let value: unknown;
|
|
922
|
+
try {
|
|
923
|
+
value = JSON.parse(raw);
|
|
924
|
+
} catch (error) {
|
|
925
|
+
return { ok: false, message: `not JSON: ${(error as Error).message}` };
|
|
926
|
+
}
|
|
927
|
+
if (Check(ClientCommandSchema, value)) return { ok: true, command: value };
|
|
928
|
+
return { ok: false, message: `not a flypi command: ${describe(ClientCommandSchema, value)}` };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export function encodeFrame(frame: ServerFrame): string {
|
|
932
|
+
return JSON.stringify(frame);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** Validate a frame before sending it. Used by the tests; a malformed frame is our bug. */
|
|
936
|
+
export function checkServerFrame(frame: unknown): { ok: true } | { ok: false; message: string } {
|
|
937
|
+
if (Check(ServerFrameSchema, frame)) return { ok: true };
|
|
938
|
+
return { ok: false, message: describe(ServerFrameSchema, frame) };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// `Errors` reports a reason for every `Check` failure, so the joined list is never empty.
|
|
942
|
+
function describe(schema: unknown, value: unknown): string {
|
|
943
|
+
return Errors(schema as never, value)
|
|
944
|
+
.slice(0, 3)
|
|
945
|
+
.map((error) => `${error.instancePath} ${error.keyword}`)
|
|
946
|
+
.join("; ");
|
|
947
|
+
}
|