@pi-unipi/unipi 2.2.7 → 2.4.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/CHANGELOG.md +61 -0
- package/package.json +31 -25
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +1 -1
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/package.json +2 -2
- package/packages/compactor/package.json +3 -3
- package/packages/compactor/src/info-screen.ts +4 -4
- package/packages/core/package.json +1 -1
- package/packages/core/utils.ts +37 -0
- package/packages/footer/package.json +2 -2
- package/packages/image/package.json +2 -2
- package/packages/image/src/generate.ts +34 -15
- package/packages/image/src/index.ts +8 -0
- package/packages/image/src/models.ts +28 -0
- package/packages/image/src/openai-images-api.ts +282 -0
- package/packages/image/src/register-providers.ts +220 -0
- package/packages/image/src/tools.ts +37 -6
- package/packages/image/src/tui/settings-dialog.ts +6 -1
- package/packages/info-screen/README.md +4 -4
- package/packages/info-screen/config.ts +28 -8
- package/packages/info-screen/core-groups.ts +5 -39
- package/packages/info-screen/index.ts +25 -10
- package/packages/info-screen/package.json +2 -2
- package/packages/info-screen/tui/info-overlay.ts +114 -38
- package/packages/info-screen/types.ts +20 -5
- package/packages/info-screen/usage-parser.ts +318 -128
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/index.ts +60 -22
- package/packages/memory/mempalace.ts +66 -1
- package/packages/memory/package.json +3 -3
- package/packages/memory/storage.ts +75 -12
- package/packages/milestone/package.json +2 -2
- package/packages/notify/package.json +2 -2
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +4 -4
- package/packages/unipi/bundled.js +37968 -0
- package/packages/updater/package.json +2 -2
- package/packages/utility/package.json +2 -2
- package/packages/utility/src/tools/env.ts +1 -22
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,67 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.4.0] — 2026-08-09
|
|
10
|
+
|
|
11
|
+
Image generation no longer requires an OpenRouter account, and `image_generate` can now edit an existing image.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`image`: image editing.** `image_generate` takes an optional `image` argument — a file path, `data:` URL, or raw base64 — and edits that image instead of generating from scratch. The result is saved and returned like any other generation.
|
|
16
|
+
|
|
17
|
+
Editing regenerates the whole image rather than masking a region, so details you did not mention can still shift. Which model you pick matters a lot here: in testing, `flux.2-pro` preserved unmentioned elements faithfully, while the `gemini-*-image` family tended to reinterpret the whole scene. Note also that image models respond to what you *describe*, not to what you negate — "no text" is as likely to add text as remove it.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **`image`: generation works with any OpenAI-compatible provider configured in pi.** Previously pi-ai shipped exactly one image provider (`openrouter`), so generating an image meant holding an OpenRouter key even when you had several other providers signed in. Every provider in pi's model registry is now bridged into pi-ai's images collection at session start, backed by a single generic adapter that posts to `{baseUrl}/images/generations`.
|
|
22
|
+
|
|
23
|
+
Credentials come from pi's existing auth — there is no separate image login. Gateways disagree on the response shape, so three known forms are normalized (`b64_json` + `media_type`, `b64_json` + `revised_prompt`, and a `data:` URL under `url`); a remote `url` is surfaced as text rather than silently dropped.
|
|
24
|
+
|
|
25
|
+
Model *discovery* remains heuristic. pi's `ProviderModelConfig` has no `output` field and the provider composer rebuilds each model from a fixed field list, so an extension cannot record "this model emits images" — hence the name-based matching, with an explicit `provider/model-id` always accepted as the escape hatch.
|
|
26
|
+
|
|
27
|
+
- **`image`: errors name the provider you are actually using.** A missing key now points at that provider's environment variable and `/login` instead of unconditionally linking OpenRouter, and an unusable provider reports "no image-generation route" rather than the misleading "cannot generate images".
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **`image`: a valid API key could be reported as missing.** pi-ai resolves credentials to an `AuthResult` — `{ auth: { apiKey } }` — but the key was read from the top level, so generation failed with "No API key for provider" while a perfectly good credential sat one level down. Both shapes are now accepted.
|
|
32
|
+
|
|
33
|
+
- **`image`: models discovered from the registry lost their endpoint.** Provider registration rebuilt each model from a field list that omitted `baseUrl`, so setup appeared to succeed and only the first real request failed with "No baseUrl for image model". The endpoint is now carried through discovery, and filled in from the registry at resolve time for hand-typed `provider/model-id` references, which never carry one.
|
|
34
|
+
|
|
35
|
+
## [2.3.0] — 2026-08-07
|
|
36
|
+
|
|
37
|
+
Startup went from **23.1s to 0.75s** — 31× faster, and within ~0.7s of bare `pi` with no extensions at all. A cold start (empty cache) is now the same speed as a warm one.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **`info-screen`: `showOnBoot` is now `bootMode`, with three states.** `"on"` keeps the dashboard up until you dismiss it, `"auto-close"` (the new default) closes it after `bootTimeoutMs`, and `"off"` never builds it at all. All three are configurable from `/unipi:info-settings` — `←/→` cycles the mode, and the row below adjusts the delay in 0.5s steps (clamped 0.5–30s). Any keypress cancels the auto-close, so the dashboard stays put if you are reading it. `/unipi:info` opened by hand never auto-closes.
|
|
42
|
+
|
|
43
|
+
Existing configs migrate automatically on read: `showOnBoot: true` → `"on"`, `false` → `"off"`. Note that `true` maps to `"on"` rather than the new `"auto-close"` default, so nobody's startup behaviour changes underneath them. An explicit `bootMode` always wins over the legacy key.
|
|
44
|
+
|
|
45
|
+
- **`info-screen`: dashboard panels load lazily.** Only the visible tab fetches at boot; the rest are prefetched 1.5s later, and any tab opened before that fetches on demand. Previously all ~17 panels were built before the first prompt was ready, including when the dashboard was disabled entirely.
|
|
46
|
+
|
|
47
|
+
- **`memory`: orphaned markdown is imported on first use rather than at startup.** `syncOrphanedFiles()` spawns a Python bridge; nothing reads its result until memory is actually used. A `.md` file dropped into a project directory is now picked up by the first memory tool call instead of at boot. Still runs exactly once per session; nothing is lost.
|
|
48
|
+
|
|
49
|
+
- **`unipi`: the all-in-one entry now ships as a prebuilt bundle.** pi loads extensions through jiti with its module cache disabled, so every startup transpiled ~577 TypeScript files from scratch — the factory calls themselves take 5ms, the cost was entirely transpilation. `pi.extensions` points at `packages/unipi/bundled.js`, built by `npm run build` and regenerated automatically by `prepublishOnly`. Only `@pi-unipi` sources are bundled; every third-party dependency stays external and resolves from `node_modules` at runtime.
|
|
50
|
+
|
|
51
|
+
### Performance
|
|
52
|
+
|
|
53
|
+
- **`info-screen`: usage stats are cached per file (8,671ms → 70ms).** `parseUsageStats()` re-read and re-parsed the entire session history on every call — 2.6GB across 600 JSONL files — and ran twice per startup. Results are now cached in `~/.unipi/cache/usage-stats.json`, keyed on each file's mtime and size. Statting 600 files costs ~1ms while only ~11 change on a busy day, so ~96% of the work was recomputing an immutable result.
|
|
54
|
+
|
|
55
|
+
This single function was the dominant startup cost. Because it never yielded, every other extension's `session_start` handler queued behind it — which is why earlier profiling rounds blamed unrelated extensions for ~9s each. They were waiting on this, not doing work.
|
|
56
|
+
|
|
57
|
+
- **`info-screen`: the usage parser no longer blocks the event loop.** A new `parseUsageStatsAsync()` yields via `setImmediate` between files, so even a cold parse cannot freeze keystrokes or the first paint. The reader also streams line-by-line instead of `readFileSync` — the largest single session file here is 213MB and was being materialized in full, along with its `split("\n")` array.
|
|
58
|
+
|
|
59
|
+
- **`memory`: MemPalace bridge calls are async (1,581ms → ~0ms of blocked loop).** The status-bar counts called `listAll()` and the cross-project listing through `spawnSync`, freezing the process for the full Python round-trip just to render `mem 32p/2262all`. Added `runBridgeAsync` plus `listAllAsync()` / `listAllProjectsCachedAsync()`; the status bar paints immediately and fills in when the bridge answers. The cross-project listing is additionally cached for 60s, invalidated on any store or delete.
|
|
60
|
+
|
|
61
|
+
- **`info-screen`/`utility`: pi's version is resolved without spawning a subprocess (710ms → ~1ms).** `getPiVersion()` probed a hardcoded path for one specific Node version and, on any other, fell through to `execSync("pi --version")` — starting a whole second pi process, synchronously, twice per startup. A shared implementation in `@pi-unipi/core` now walks up from pi's own entry point, resolving the symlink first (the binary on `PATH` is typically a shim), and caches the result.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- `info-screen`: the overview panel displayed **`Pi Version: unknown`**. The `execSync` fallback matched `/v([\d.]+)/`, requiring a literal `v` prefix that pi no longer emits — so it paid 710ms per startup *and* still failed. Now shows the real version.
|
|
66
|
+
- `info-screen`: `bootTimeoutMs` had existed in `InfoScreenSettings` and in user configs since the beginning but was never connected to anything. It now drives the auto-close timer.
|
|
67
|
+
- `info-screen`: `writeSettingsFile()` called `require()` from an ESM module, which throws under Node's module-format detection whenever the settings directory does not already exist.
|
|
68
|
+
- `unipi`: `diff` is a dependency of `@pi-unipi/utility` and lives in that package's own `node_modules`. That resolves fine under jiti (each file resolves from its own location) and fine when installed (npm hoists it), but was unresolvable from a single bundle in a local checkout. Now declared at the root.
|
|
69
|
+
|
|
9
70
|
## [2.2.7] — 2026-08-04
|
|
10
71
|
|
|
11
72
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/unipi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "All-in-one extension suite for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,9 +23,12 @@
|
|
|
23
23
|
"packages/*"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"
|
|
26
|
+
"build": "node scripts/build-bundle.mjs",
|
|
27
|
+
"prepare": "node scripts/build-bundle.mjs --if-missing",
|
|
28
|
+
"prepublishOnly": "npm run build",
|
|
29
|
+
"publish:all": "npm publish --workspaces --access public",
|
|
27
30
|
"test": "npx tsx --test tests/*.test.js && npm test --workspaces --if-present",
|
|
28
|
-
"
|
|
31
|
+
"typecheck": "npx tsc --noEmit --skipLibCheck"
|
|
29
32
|
},
|
|
30
33
|
"files": [
|
|
31
34
|
"CHANGELOG.md",
|
|
@@ -37,11 +40,12 @@
|
|
|
37
40
|
"packages/*/tui/*",
|
|
38
41
|
"packages/*/SKILL.md",
|
|
39
42
|
"packages/*/skills/**/*",
|
|
40
|
-
"packages/*/README.md"
|
|
43
|
+
"packages/*/README.md",
|
|
44
|
+
"packages/unipi/bundled.js"
|
|
41
45
|
],
|
|
42
46
|
"pi": {
|
|
43
47
|
"extensions": [
|
|
44
|
-
"packages/unipi/
|
|
48
|
+
"packages/unipi/bundled.js"
|
|
45
49
|
],
|
|
46
50
|
"skills": [
|
|
47
51
|
"packages/workflow/skills",
|
|
@@ -67,26 +71,26 @@
|
|
|
67
71
|
"typebox": "^1.1.38"
|
|
68
72
|
},
|
|
69
73
|
"dependencies": {
|
|
70
|
-
"@pi-unipi/ask-user": "2.
|
|
71
|
-
"@pi-unipi/btw": "2.
|
|
72
|
-
"@pi-unipi/compactor": "2.
|
|
73
|
-
"@pi-unipi/notify": "2.
|
|
74
|
-
"@pi-unipi/command-enchantment": "2.
|
|
75
|
-
"@pi-unipi/core": "2.
|
|
76
|
-
"@pi-unipi/info-screen": "2.
|
|
77
|
-
"@pi-unipi/mcp": "2.
|
|
78
|
-
"@pi-unipi/memory": "2.
|
|
79
|
-
"@pi-unipi/ralph": "2.
|
|
80
|
-
"@pi-unipi/subagents": "2.
|
|
81
|
-
"@pi-unipi/utility": "2.
|
|
82
|
-
"@pi-unipi/milestone": "2.
|
|
83
|
-
"@pi-unipi/kanboard": "2.
|
|
84
|
-
"@pi-unipi/web-api": "2.
|
|
85
|
-
"@pi-unipi/workflow": "2.
|
|
86
|
-
"@pi-unipi/footer": "2.
|
|
87
|
-
"@pi-unipi/updater": "2.
|
|
88
|
-
"@pi-unipi/input-shortcuts": "2.
|
|
89
|
-
"@pi-unipi/cocoindex": "2.
|
|
74
|
+
"@pi-unipi/ask-user": "2.4.0",
|
|
75
|
+
"@pi-unipi/btw": "2.4.0",
|
|
76
|
+
"@pi-unipi/compactor": "2.4.0",
|
|
77
|
+
"@pi-unipi/notify": "2.4.0",
|
|
78
|
+
"@pi-unipi/command-enchantment": "2.4.0",
|
|
79
|
+
"@pi-unipi/core": "2.4.0",
|
|
80
|
+
"@pi-unipi/info-screen": "2.4.0",
|
|
81
|
+
"@pi-unipi/mcp": "2.4.0",
|
|
82
|
+
"@pi-unipi/memory": "2.4.0",
|
|
83
|
+
"@pi-unipi/ralph": "2.4.0",
|
|
84
|
+
"@pi-unipi/subagents": "2.4.0",
|
|
85
|
+
"@pi-unipi/utility": "2.4.0",
|
|
86
|
+
"@pi-unipi/milestone": "2.4.0",
|
|
87
|
+
"@pi-unipi/kanboard": "2.4.0",
|
|
88
|
+
"@pi-unipi/web-api": "2.4.0",
|
|
89
|
+
"@pi-unipi/workflow": "2.4.0",
|
|
90
|
+
"@pi-unipi/footer": "2.4.0",
|
|
91
|
+
"@pi-unipi/updater": "2.4.0",
|
|
92
|
+
"@pi-unipi/input-shortcuts": "2.4.0",
|
|
93
|
+
"@pi-unipi/cocoindex": "2.4.0"
|
|
90
94
|
},
|
|
91
95
|
"devDependencies": {
|
|
92
96
|
"@earendil-works/pi-agent-core": "^0.80.0",
|
|
@@ -94,6 +98,8 @@
|
|
|
94
98
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
95
99
|
"@earendil-works/pi-tui": "^0.80.0",
|
|
96
100
|
"@types/node": "^25.6.0",
|
|
101
|
+
"diff": "^7.0.0",
|
|
102
|
+
"esbuild": "^0.27.7",
|
|
97
103
|
"tsx": "^4.21.0",
|
|
98
104
|
"typescript": "^6.0.0"
|
|
99
105
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ask-user",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@pi-unipi/core": "2.
|
|
43
|
+
"@pi-unipi/core": "2.4.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/btw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "A pi extension for parallel side conversations with /unipi:btw — part of the Unipi suite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "extensions/btw.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@earendil-works/pi-tui": "^0.80.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@pi-unipi/core": "2.
|
|
40
|
+
"@pi-unipi/core": "2.4.0"
|
|
41
41
|
},
|
|
42
42
|
"pi": {
|
|
43
43
|
"extensions": [],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/cocoindex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "CocoIndex integration for Pi — AST-aware content indexing, semantic vector search, and incremental pipeline management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"typebox": "^1.1.38"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@pi-unipi/core": "2.
|
|
28
|
+
"@pi-unipi/core": "2.4.0"
|
|
29
29
|
},
|
|
30
30
|
"optionalDependencies": {
|
|
31
31
|
"@lancedb/lancedb": "^0.21.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/compactor",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.
|
|
38
|
-
"@pi-unipi/info-screen": "2.
|
|
37
|
+
"@pi-unipi/core": "2.4.0",
|
|
38
|
+
"@pi-unipi/info-screen": "2.4.0",
|
|
39
39
|
"@earendil-works/pi-agent-core": "^0.80.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { SessionDB } from "./session/db.js";
|
|
14
14
|
import { getLastCompactionStats } from "./compaction/hooks.js";
|
|
15
|
-
import {
|
|
15
|
+
import { parseUsageStatsAsync } from "@pi-unipi/info-screen/usage-parser.js";
|
|
16
16
|
import type { RuntimeCounters } from "./types.js";
|
|
17
17
|
|
|
18
18
|
export interface CompactorInfoData {
|
|
@@ -42,9 +42,9 @@ function formatCost(n: number): string {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Estimate cost per token for the most-used model in the current session. */
|
|
45
|
-
function estimateCostPerToken(): number | null {
|
|
45
|
+
async function estimateCostPerToken(): Promise<number | null> {
|
|
46
46
|
try {
|
|
47
|
-
const usage =
|
|
47
|
+
const usage = await parseUsageStatsAsync();
|
|
48
48
|
// Use today's most-used model if available, otherwise all-time
|
|
49
49
|
const models = usage.byModelToday;
|
|
50
50
|
const todayKeys = Object.keys(models);
|
|
@@ -154,7 +154,7 @@ export async function getInfoScreenData(
|
|
|
154
154
|
: "No tool calls yet";
|
|
155
155
|
|
|
156
156
|
// ── Cost saved estimate ──
|
|
157
|
-
const costPerToken = estimateCostPerToken();
|
|
157
|
+
const costPerToken = await estimateCostPerToken();
|
|
158
158
|
const costSaved = costPerToken !== null ? tokensSaved * costPerToken : null;
|
|
159
159
|
|
|
160
160
|
// ── Last compaction details ──
|
package/packages/core/utils.ts
CHANGED
|
@@ -187,6 +187,43 @@ export function getInstalledPackageVersion(startDir: string, packageName: string
|
|
|
187
187
|
return getPackageVersion(root);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/** Cached pi version — resolved at most once per process. */
|
|
191
|
+
let cachedPiVersion: string | null = null;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get the running Pi agent's version.
|
|
195
|
+
*
|
|
196
|
+
* Resolves by walking up from Pi's own entry point (`process.argv[1]`), which
|
|
197
|
+
* must be `realpath`'d first: the executable on PATH is typically a symlink
|
|
198
|
+
* (e.g. mise shims `~/.local/share/mise/installs/node/lts/bin/pi`), and the
|
|
199
|
+
* package.json lives next to the *real* `dist/cli.js`, not the link.
|
|
200
|
+
*
|
|
201
|
+
* Never spawns a subprocess. A previous implementation fell back to
|
|
202
|
+
* `execSync("pi --version")`, which cost ~350ms per call and still returned
|
|
203
|
+
* "unknown" because it matched against a `v` prefix that Pi no longer emits.
|
|
204
|
+
*/
|
|
205
|
+
export function getPiVersion(): string {
|
|
206
|
+
if (cachedPiVersion !== null) return cachedPiVersion;
|
|
207
|
+
|
|
208
|
+
const PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
209
|
+
const entry = process.argv[1];
|
|
210
|
+
if (entry) {
|
|
211
|
+
try {
|
|
212
|
+
const realEntry = fs.realpathSync(entry);
|
|
213
|
+
const root = findPackageRoot(path.dirname(realEntry), PI_PACKAGE);
|
|
214
|
+
if (root) {
|
|
215
|
+
cachedPiVersion = getPackageVersion(root);
|
|
216
|
+
return cachedPiVersion;
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// Fall through to "unknown".
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
cachedPiVersion = "unknown";
|
|
224
|
+
return cachedPiVersion;
|
|
225
|
+
}
|
|
226
|
+
|
|
190
227
|
/**
|
|
191
228
|
* Check if a module is available in node_modules.
|
|
192
229
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/footer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.
|
|
35
|
+
"@pi-unipi/core": "2.4.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/image",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Image generation and image recognition tools for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.
|
|
37
|
+
"@pi-unipi/core": "2.4.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@earendil-works/pi-ai": "^0.80.0",
|
|
@@ -131,6 +131,8 @@ export interface GenerateOptions {
|
|
|
131
131
|
signal?: AbortSignal;
|
|
132
132
|
/** Absolute directory for saved images; omit to skip saving. */
|
|
133
133
|
outputDir?: string;
|
|
134
|
+
/** Source image; when set the request is an edit rather than a generation. */
|
|
135
|
+
inputImage?: { data: string; mimeType: string };
|
|
134
136
|
now?: Date;
|
|
135
137
|
/** Injected images collection, for tests. */
|
|
136
138
|
images?: ImagesModelsLike;
|
|
@@ -141,7 +143,7 @@ export interface GenerateOptions {
|
|
|
141
143
|
* @throws {Error} with an actionable message when generation fails.
|
|
142
144
|
*/
|
|
143
145
|
export async function generateImage(options: GenerateOptions): Promise<GenerateResult> {
|
|
144
|
-
const { prompt, model, signal, outputDir, now } = options;
|
|
146
|
+
const { prompt, model, signal, outputDir, now, inputImage } = options;
|
|
145
147
|
|
|
146
148
|
if (!prompt.trim()) {
|
|
147
149
|
throw new Error("A non-empty prompt is required.");
|
|
@@ -154,19 +156,19 @@ export async function generateImage(options: GenerateOptions): Promise<GenerateR
|
|
|
154
156
|
);
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
// pi-ai's images collection
|
|
158
|
-
// `
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
159
|
+
// pi-ai's images collection has its own provider set, separate from pi's
|
|
160
|
+
// chat registry. `registerRegistryImageProviders()` bridges pi's providers
|
|
161
|
+
// in, but a model may still name a provider with no image route at all —
|
|
162
|
+
// pi-ai would answer with a bare "Unknown provider: x", so say something
|
|
163
|
+
// useful instead.
|
|
162
164
|
if (!providerCanGenerate(imagesApi, model.provider)) {
|
|
163
165
|
const supported = supportedProviders(imagesApi);
|
|
164
166
|
throw new Error(
|
|
165
|
-
`Provider "${model.provider}"
|
|
166
|
-
`→
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"→ Pick
|
|
167
|
+
`Provider "${model.provider}" has no image-generation route.\n` +
|
|
168
|
+
`→ Available: ${supported.join(", ") || "openrouter"}.\n` +
|
|
169
|
+
"→ Providers are bridged from pi automatically; one without a baseUrl " +
|
|
170
|
+
"or an API key cannot be used.\n" +
|
|
171
|
+
"→ Pick another with /unipi:image-settings.",
|
|
170
172
|
);
|
|
171
173
|
}
|
|
172
174
|
|
|
@@ -174,7 +176,13 @@ export async function generateImage(options: GenerateOptions): Promise<GenerateR
|
|
|
174
176
|
// a bare OPENROUTER_API_KEY still works.
|
|
175
177
|
let apiKey: string | undefined;
|
|
176
178
|
try {
|
|
177
|
-
|
|
179
|
+
// pi-ai resolves to an `AuthResult`, i.e. `{ auth: { apiKey } }`. Older
|
|
180
|
+
// shapes put the key at the top level, so accept both — reading only one
|
|
181
|
+
// fails silently and looks like a missing credential.
|
|
182
|
+
const resolvedAuth = (await imagesApi.getAuth(model)) as
|
|
183
|
+
| { apiKey?: string; auth?: { apiKey?: string } }
|
|
184
|
+
| undefined;
|
|
185
|
+
apiKey = resolvedAuth?.auth?.apiKey ?? resolvedAuth?.apiKey;
|
|
178
186
|
} catch {
|
|
179
187
|
// Reported as a missing key below.
|
|
180
188
|
}
|
|
@@ -183,14 +191,25 @@ export async function generateImage(options: GenerateOptions): Promise<GenerateR
|
|
|
183
191
|
if (!apiKey) {
|
|
184
192
|
throw new Error(
|
|
185
193
|
`No API key for provider "${model.provider}".\n` +
|
|
186
|
-
|
|
187
|
-
`→
|
|
194
|
+
"→ Sign in with /login, or set the provider's API key environment variable.\n" +
|
|
195
|
+
`→ Expected environment variable: ` +
|
|
196
|
+
`${model.provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`,
|
|
188
197
|
);
|
|
189
198
|
}
|
|
190
199
|
|
|
200
|
+
const input: Array<{ type: string; text?: string; data?: string; mimeType?: string }> =
|
|
201
|
+
[{ type: "text", text: prompt }];
|
|
202
|
+
if (inputImage) {
|
|
203
|
+
input.push({
|
|
204
|
+
type: "image",
|
|
205
|
+
data: inputImage.data,
|
|
206
|
+
mimeType: inputImage.mimeType,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
191
210
|
const result = (await imagesApi.generateImages(
|
|
192
211
|
model,
|
|
193
|
-
{ input:
|
|
212
|
+
{ input } as { input: Array<{ type: string; text?: string }> },
|
|
194
213
|
{ apiKey, ...(signal ? { signal } : {}) },
|
|
195
214
|
)) as AssistantImagesLike;
|
|
196
215
|
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { registerImageCommands } from "./commands.js";
|
|
22
22
|
import { registerImageTools } from "./tools.js";
|
|
23
23
|
import { listImageGenModels, listVisionModels, type ChatModelRegistry } from "./models.js";
|
|
24
|
+
import { registerRegistryImageProviders } from "./register-providers.js";
|
|
24
25
|
import { loadConfig } from "./settings.js";
|
|
25
26
|
|
|
26
27
|
const VERSION = getPackageVersion(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -43,6 +44,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
43
44
|
pi.on("session_start", async (_event, ctx) => {
|
|
44
45
|
const config = loadConfig();
|
|
45
46
|
|
|
47
|
+
// Bridge pi's configured providers into pi-ai's images collection up front,
|
|
48
|
+
// so the settings picker and the info screen see them without a prior
|
|
49
|
+
// image_generate call. Best-effort: never block session start.
|
|
50
|
+
void registerRegistryImageProviders(
|
|
51
|
+
(ctx as unknown as { modelRegistry?: ChatModelRegistry }).modelRegistry,
|
|
52
|
+
).catch(() => undefined);
|
|
53
|
+
|
|
46
54
|
const tools: string[] = [];
|
|
47
55
|
if (config.generate.enabled) tools.push(IMAGE_TOOLS.GENERATE);
|
|
48
56
|
if (config.recognize.enabled) tools.push(IMAGE_TOOLS.RECOGNIZE);
|
|
@@ -211,12 +211,40 @@ export function listRegistryImageGenModels(
|
|
|
211
211
|
provider: candidate.provider,
|
|
212
212
|
name: candidate.name,
|
|
213
213
|
api: candidate.api ?? "",
|
|
214
|
+
// Carry the endpoint through. The generic images adapter POSTs to
|
|
215
|
+
// `{baseUrl}/images/generations`, and this is the only place the
|
|
216
|
+
// registry's baseUrl is available — dropping it here surfaces later as
|
|
217
|
+
// "No baseUrl for image model ..." once generation is attempted.
|
|
218
|
+
...(candidate.baseUrl ? { baseUrl: candidate.baseUrl } : {}),
|
|
214
219
|
...(candidate.output ? { output: candidate.output } : {}),
|
|
215
220
|
});
|
|
216
221
|
}
|
|
217
222
|
return out;
|
|
218
223
|
}
|
|
219
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Find a provider's API endpoint in pi's registry.
|
|
227
|
+
*
|
|
228
|
+
* Needed because a model can reach generation without one: a user-typed
|
|
229
|
+
* "provider/model-id" is accepted at face value by `asExplicitModelRef`, and
|
|
230
|
+
* carries no baseUrl of its own.
|
|
231
|
+
*/
|
|
232
|
+
export function findProviderBaseUrl(
|
|
233
|
+
registry: ChatModelRegistry | undefined,
|
|
234
|
+
provider: string,
|
|
235
|
+
): string | undefined {
|
|
236
|
+
if (!registry) return undefined;
|
|
237
|
+
try {
|
|
238
|
+
const models = (registry.getAvailable?.() ?? registry.getAll()) as Array<{
|
|
239
|
+
provider?: string;
|
|
240
|
+
baseUrl?: string;
|
|
241
|
+
}>;
|
|
242
|
+
return models.find((m) => m?.provider === provider && m.baseUrl)?.baseUrl;
|
|
243
|
+
} catch {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
220
248
|
/**
|
|
221
249
|
* Every selectable generation model: pi-ai's built-in catalog plus anything
|
|
222
250
|
* contributed by registered providers, de-duplicated by "provider/id".
|