pi-central 0.6.0 → 0.7.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/bin/pi-central.js +122 -12
- package/bin/pi-central.js.map +3 -3
- package/dist/ui/assets/{grammar-map-DrUpstmg.js → grammar-map-_cUmoZJI.js} +1 -1
- package/dist/ui/assets/index-B5gXmQkL.js +50 -0
- package/dist/ui/assets/{index-BA5r5TxA.css → index-DGDX197N.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +48 -6
- package/package.json +1 -1
- package/dist/ui/assets/index-CnM7j6oa.js +0 -48
package/README.md
CHANGED
|
@@ -124,12 +124,14 @@ Stated plainly, because a dashboard that overstates itself is worse than one tha
|
|
|
124
124
|
**Works:** multiple sandboxed sessions in different directories with verified isolation; live transcripts
|
|
125
125
|
reconstructed from Pi's delta-only stream; the approval dock answerable for any session; a bounded tool
|
|
126
126
|
trace with full traversal; a read-only file tree and image viewer confined to the session's directory;
|
|
127
|
+
images shown where they belong in the conversation, including the ones a tool read, and screenshots pasted or
|
|
128
|
+
dropped straight into the composer;
|
|
127
129
|
history searchable by content, with a clear refusal when a session's directory is gone; a session roster
|
|
128
130
|
that survives a restart; agent-scoped credentials with the host's own credentials denied, and commits
|
|
129
131
|
signed as the agent when the store is configured to sign.
|
|
130
132
|
|
|
131
|
-
**Not yet:** no file editing from the browser, no update checking, no export.
|
|
132
|
-
rather than a wish.
|
|
133
|
+
**Not yet:** no file editing from the browser, no update checking, no export. The image size caps are derived from the
|
|
134
|
+
transport's frame limit rather than tuned against real reading. Each is a tracked issue rather than a wish.
|
|
133
135
|
|
|
134
136
|
## Working on this project
|
|
135
137
|
|
package/bin/pi-central.js
CHANGED
|
@@ -25,7 +25,7 @@ function isBlockingDialog(record) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// packages/protocol/src/envelopes.ts
|
|
28
|
-
var PROTOCOL_VERSION =
|
|
28
|
+
var PROTOCOL_VERSION = 5;
|
|
29
29
|
|
|
30
30
|
// packages/protocol/src/own-installation.ts
|
|
31
31
|
var PIPELINE_MARKERS = ["packages/orchestrator", "packages/ui", "packages/protocol"];
|
|
@@ -1479,6 +1479,7 @@ var blockingDialogMethods = [...BLOCKING_DIALOG_METHODS];
|
|
|
1479
1479
|
var fireAndForgetUiMethods = [...FIRE_AND_FORGET_UI_METHODS];
|
|
1480
1480
|
|
|
1481
1481
|
// packages/orchestrator/src/supervisor.ts
|
|
1482
|
+
var EMPTY_PROMPT_WITH_IMAGES = "See the attached image.";
|
|
1482
1483
|
var Supervisor = class extends EventEmitter2 {
|
|
1483
1484
|
#config;
|
|
1484
1485
|
#sessions = /* @__PURE__ */ new Map();
|
|
@@ -1848,22 +1849,32 @@ ${rpc.stderrTail}` : ""}`;
|
|
|
1848
1849
|
this.#patch(id);
|
|
1849
1850
|
}
|
|
1850
1851
|
/**
|
|
1851
|
-
* Send a prompt. The command shape is closed; the message
|
|
1852
|
+
* Send a prompt. The command shape is closed; the message and its images are the only free values.
|
|
1852
1853
|
*
|
|
1853
1854
|
* `behavior` decides what a message means mid-run, and it is the caller's choice rather than
|
|
1854
1855
|
* this method's: Pi REJECTS a streaming `prompt` with no `streamingBehavior` (`"Agent is
|
|
1855
1856
|
* already processing. Specify streamingBehavior ('steer' or 'followUp')"`), so defaulting it
|
|
1856
1857
|
* silently would decide for the user something the composer is there to ask (issue #3). When
|
|
1857
1858
|
* idle there is nothing to steer into, so the flag is dropped rather than sent.
|
|
1859
|
+
*
|
|
1860
|
+
* `images` arrive ALREADY VALIDATED (`prompt-images.ts`) and are mapped to Pi's shape here — the one
|
|
1861
|
+
* place a control becomes Pi's stdin, which is the right place for the spelling to change. Pi wants
|
|
1862
|
+
* `{ type: "image", data, mimeType }`; the wire carries `{ mediaType, data }` because `type` is the
|
|
1863
|
+
* discriminant of every record on that wire and reusing it inside a payload makes a log ambiguous.
|
|
1858
1864
|
*/
|
|
1859
|
-
async prompt(id, message, behavior) {
|
|
1865
|
+
async prompt(id, message, behavior, images = []) {
|
|
1860
1866
|
const rpc = this.#processes.get(id);
|
|
1861
1867
|
const session = this.#sessions.get(id);
|
|
1862
1868
|
if (!rpc || !session) throw new Error(`no such session: ${id}`);
|
|
1869
|
+
const text = message.trim().length === 0 && images.length > 0 ? EMPTY_PROMPT_WITH_IMAGES : message;
|
|
1870
|
+
const withImages = images.length > 0 ? { images: images.map((image) => ({ type: "image", data: image.data, mimeType: image.mediaType })) } : {};
|
|
1863
1871
|
if (session.state === "streaming") {
|
|
1864
|
-
await rpc.send(
|
|
1872
|
+
await rpc.send(
|
|
1873
|
+
{ type: "prompt", message: text, streamingBehavior: behavior ?? "followUp", ...withImages },
|
|
1874
|
+
6e4
|
|
1875
|
+
);
|
|
1865
1876
|
} else {
|
|
1866
|
-
await rpc.send({ type: "prompt", message }, 6e4);
|
|
1877
|
+
await rpc.send({ type: "prompt", message: text, ...withImages }, 6e4);
|
|
1867
1878
|
}
|
|
1868
1879
|
}
|
|
1869
1880
|
/**
|
|
@@ -1916,7 +1927,20 @@ ${rpc.stderrTail}` : ""}`;
|
|
|
1916
1927
|
provider: String(model["provider"] ?? ""),
|
|
1917
1928
|
id: String(model["id"] ?? ""),
|
|
1918
1929
|
...typeof model["name"] === "string" ? { name: model["name"] } : {},
|
|
1919
|
-
...typeof model["reasoning"] === "boolean" ? { reasoning: model["reasoning"] } : {}
|
|
1930
|
+
...typeof model["reasoning"] === "boolean" ? { reasoning: model["reasoning"] } : {},
|
|
1931
|
+
/*
|
|
1932
|
+
* `input` decides whether the composer may offer a paste.
|
|
1933
|
+
*
|
|
1934
|
+
* Filtered to the two values the wire declares rather than passed through: Pi's catalogue may grow a
|
|
1935
|
+
* modality this version does not know, and an unrecognised member would widen the UI's type for a capability
|
|
1936
|
+
* nothing here can use. Absent when Pi did not report it — which is "unknown", NOT "text only", and the
|
|
1937
|
+
* difference decides whether the affordance is hidden or merely unflagged.
|
|
1938
|
+
*/
|
|
1939
|
+
...Array.isArray(model["input"]) ? {
|
|
1940
|
+
input: model["input"].filter(
|
|
1941
|
+
(value) => value === "text" || value === "image"
|
|
1942
|
+
)
|
|
1943
|
+
} : {}
|
|
1920
1944
|
})).filter((model) => model.provider !== "" && model.id !== "");
|
|
1921
1945
|
return { ok: true, models };
|
|
1922
1946
|
} catch (error) {
|
|
@@ -2124,6 +2148,90 @@ function readPackageJson(path) {
|
|
|
2124
2148
|
}
|
|
2125
2149
|
var VERIFY_INSTALL_HINT = "npm audit signatures";
|
|
2126
2150
|
|
|
2151
|
+
// packages/orchestrator/src/prompt-images.ts
|
|
2152
|
+
var PROMPT_IMAGE_TYPES = [
|
|
2153
|
+
"image/png",
|
|
2154
|
+
"image/jpeg",
|
|
2155
|
+
"image/gif",
|
|
2156
|
+
"image/webp"
|
|
2157
|
+
];
|
|
2158
|
+
var MAX_PROMPT_IMAGE_BYTES = 3 * 1024 * 1024;
|
|
2159
|
+
function refuseImage(refusal, detail) {
|
|
2160
|
+
switch (refusal) {
|
|
2161
|
+
case "unsupported-type":
|
|
2162
|
+
return `an image of type ${detail?.type ?? "unknown"} cannot be sent \u2014 prompts accept PNG, JPEG, GIF and WebP`;
|
|
2163
|
+
case "too-large":
|
|
2164
|
+
return `that image is ${formatMegabytes(detail?.bytes ?? 0)} decoded, above the ${formatMegabytes(MAX_PROMPT_IMAGE_BYTES)} limit for a prompt \u2014 downscale it and paste again`;
|
|
2165
|
+
case "not-base64":
|
|
2166
|
+
return "that image's data is not valid base64 \u2014 it may have been truncated in transit";
|
|
2167
|
+
case "missing-type":
|
|
2168
|
+
return "an image arrived without a media type";
|
|
2169
|
+
case "missing-data":
|
|
2170
|
+
return "an image arrived with no data";
|
|
2171
|
+
case "not-an-object":
|
|
2172
|
+
return "an image arrived in an unrecognised shape";
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
function formatMegabytes(bytes) {
|
|
2176
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "an unknown size";
|
|
2177
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
2178
|
+
}
|
|
2179
|
+
function base64Bytes(data) {
|
|
2180
|
+
if (data.length === 0) return 0;
|
|
2181
|
+
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
2182
|
+
return Math.floor(data.length * 3 / 4) - padding;
|
|
2183
|
+
}
|
|
2184
|
+
function isBase64(data) {
|
|
2185
|
+
if (data.length === 0 || data.length % 4 !== 0) return false;
|
|
2186
|
+
return /^[A-Za-z0-9+/]+={0,2}$/.test(data);
|
|
2187
|
+
}
|
|
2188
|
+
function acceptImage(raw) {
|
|
2189
|
+
if (typeof raw !== "object" || raw === null) return { ok: false, reason: "not-an-object" };
|
|
2190
|
+
const candidate = raw;
|
|
2191
|
+
if (typeof candidate.mediaType !== "string" || candidate.mediaType.length === 0) {
|
|
2192
|
+
return { ok: false, reason: "missing-type" };
|
|
2193
|
+
}
|
|
2194
|
+
if (typeof candidate.data !== "string" || candidate.data.length === 0) {
|
|
2195
|
+
return { ok: false, reason: "missing-data" };
|
|
2196
|
+
}
|
|
2197
|
+
const mediaType = candidate.mediaType.toLowerCase();
|
|
2198
|
+
if (!PROMPT_IMAGE_TYPES.includes(mediaType)) return { ok: false, reason: "unsupported-type" };
|
|
2199
|
+
if (!isBase64(candidate.data)) return { ok: false, reason: "not-base64" };
|
|
2200
|
+
const bytes = base64Bytes(candidate.data);
|
|
2201
|
+
if (bytes > MAX_PROMPT_IMAGE_BYTES) return { ok: false, reason: "too-large" };
|
|
2202
|
+
return { ok: true, image: { mediaType, data: candidate.data } };
|
|
2203
|
+
}
|
|
2204
|
+
var MAX_PROMPT_IMAGES_ENCODED_BYTES = 6 * 1024 * 1024;
|
|
2205
|
+
function acceptImages(raw) {
|
|
2206
|
+
if (raw === void 0 || raw === null) return { ok: true, images: [] };
|
|
2207
|
+
if (!Array.isArray(raw)) return { ok: false, message: "images must be a list" };
|
|
2208
|
+
if (raw.length === 0) return { ok: true, images: [] };
|
|
2209
|
+
const images = [];
|
|
2210
|
+
for (const entry of raw) {
|
|
2211
|
+
const result = acceptImage(entry);
|
|
2212
|
+
if (!result.ok) {
|
|
2213
|
+
const type = typeof entry === "object" && entry !== null ? entry.mediaType : void 0;
|
|
2214
|
+
const bytes = typeof entry === "object" && entry !== null && typeof entry.data === "string" ? base64Bytes(entry.data) : void 0;
|
|
2215
|
+
return {
|
|
2216
|
+
ok: false,
|
|
2217
|
+
message: refuseImage(result.reason, {
|
|
2218
|
+
...typeof type === "string" ? { type } : {},
|
|
2219
|
+
...bytes !== void 0 ? { bytes } : {}
|
|
2220
|
+
})
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
images.push(result.image);
|
|
2224
|
+
}
|
|
2225
|
+
const encoded = images.reduce((total, image) => total + image.data.length, 0);
|
|
2226
|
+
if (encoded > MAX_PROMPT_IMAGES_ENCODED_BYTES) {
|
|
2227
|
+
return {
|
|
2228
|
+
ok: false,
|
|
2229
|
+
message: `these images total ${(encoded / (1024 * 1024)).toFixed(1)} MB encoded, above the ${(MAX_PROMPT_IMAGES_ENCODED_BYTES / (1024 * 1024)).toFixed(1)} MB limit for one prompt \u2014 remove one or downscale them`
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
return { ok: true, images };
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2127
2235
|
// packages/orchestrator/src/registry.ts
|
|
2128
2236
|
import { createReadStream, readdirSync as readdirSync3, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
|
|
2129
2237
|
import { basename, dirname as dirname2, join as join10, resolve as resolve7, sep as sep4 } from "node:path";
|
|
@@ -2751,13 +2859,15 @@ var Transport = class {
|
|
|
2751
2859
|
this.#broadcast({ kind: "session", session: result.session });
|
|
2752
2860
|
return;
|
|
2753
2861
|
}
|
|
2754
|
-
case "prompt":
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
control.
|
|
2758
|
-
|
|
2759
|
-
|
|
2862
|
+
case "prompt": {
|
|
2863
|
+
const accepted = acceptImages(control.images);
|
|
2864
|
+
if (!accepted.ok) {
|
|
2865
|
+
this.#broadcast({ kind: "error", message: `image refused: ${accepted.message}`, sessionId: control.sessionId });
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
await this.#supervisor.prompt(control.sessionId, control.message, control.behavior, accepted.images);
|
|
2760
2869
|
return;
|
|
2870
|
+
}
|
|
2761
2871
|
case "clearQueue": {
|
|
2762
2872
|
await this.#supervisor.clearQueue(control.sessionId, control.andStop);
|
|
2763
2873
|
return;
|