@pi-unipi/unipi 2.17.0 → 2.17.2
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 +15 -1
- package/package.json +4 -4
- package/packages/footer/package.json +1 -1
- package/packages/footer/src/index.ts +14 -7
- package/packages/fusion/README.md +3 -0
- package/packages/fusion/package.json +1 -1
- package/packages/fusion/src/index.ts +35 -14
- package/packages/fusion/src/picker.ts +25 -13
- package/packages/fusion/src/preset.ts +18 -0
- package/packages/unipi/bundled.js +197 -99
- package/packages/updater/package.json +1 -1
- package/packages/updater/src/changelog.ts +7 -4
- package/packages/updater/src/index.ts +3 -1
- package/packages/updater/src/remote-changelog.ts +61 -0
- package/packages/updater/src/tui/update-overlay.ts +12 -10
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [2.17.
|
|
9
|
+
## [2.17.2] — 2026-09-15
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `footer`: **the startup update prompt no longer becomes unclosable after ~3.5s.** The deferred Glance editor install (`installGlanceEditor` in `src/index.ts`) calls `setEditorComponent()`, which internally re-focuses the editor and silently stole keyboard focus from whatever overlay was open — the updater prompt kept rendering while `q`/`n`/Esc typed into the input box. The install now snapshots the focused overlay (`tui.isOverlayFocused()` / `getFocusedComponent()`) and restores it after the swap, protecting the updater prompt, the boot dashboard, and any other overlay. New `tests/glance-focus.test.ts`.
|
|
14
|
+
- `updater`: **the update prompt now shows the release notes for the version being offered.** It previously parsed the `CHANGELOG.md` shipped with the *installed* package, which by definition never contains the newer version's entry, so it only ever showed an empty `Unreleased` header. New `src/remote-changelog.ts` fetches `raw.githubusercontent.com/Neuron-Mr-White/unipi/v<latest>/CHANGELOG.md` (5s timeout, falls back to `main` when the tag is not pushed yet, then to the local file when offline) and caches tag-served content under `~/.unipi/cache/updater/changelog-<version>.md`. `parseChangelog()` is split into `parseChangelogContent()` + a file wrapper; `getNewerVersions()` drops an empty `Unreleased` entry; the empty state reads `No changelog available for <version> (offline?)`.
|
|
15
|
+
|
|
16
|
+
## [2.17.1] — 2026-09-15
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- `fusion`: **the persisted Fusion pair is restored at startup.** `session_start` now re-applies the saved lead via `pi.setModel()` and restores its thinking level when pi boots on the sidekick model (previously the pair silently dropped to a single-model status); if the lead is unavailable it disables Fusion and warns instead of leaving a half-active pair.
|
|
21
|
+
- `fusion`: **leaving Fusion via `/model` persists.** Selecting a non-lead model now writes the single-model selection to the preset's `active`, so the session stays un-Fused across restarts instead of snapping back to the pair.
|
|
22
|
+
- `fusion`: **manual pricing overrides for un-priced models.** A new `prices` preset key (`{ "provider/id": { "input", "cachedInput", "output" } }`) lets you supply per-million prices for providers that report no pricing; savings estimation and the picker both consume the override. Zero/unavailable pricing now renders no slider marker and shows inline `no pricing data from provider` guidance instead of a misleading $0.
|
|
23
|
+
- `fusion`: **price-panel columns no longer run together.** Column width now reserves space for the longest header/value (e.g. `Sidekick cached input`) instead of a fixed cap, fixing truncated sidekick price headers.
|
|
10
24
|
|
|
11
25
|
### Added
|
|
12
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/unipi",
|
|
3
|
-
"version": "2.17.
|
|
3
|
+
"version": "2.17.2",
|
|
4
4
|
"description": "All-in-one extension suite for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -90,9 +90,9 @@
|
|
|
90
90
|
"@pi-unipi/command-enchantment": "2.17.0",
|
|
91
91
|
"@pi-unipi/compactor": "2.17.0",
|
|
92
92
|
"@pi-unipi/core": "2.17.0",
|
|
93
|
-
"@pi-unipi/footer": "2.17.
|
|
93
|
+
"@pi-unipi/footer": "2.17.2",
|
|
94
94
|
"@pi-unipi/image": "2.17.0",
|
|
95
|
-
"@pi-unipi/fusion": "2.17.
|
|
95
|
+
"@pi-unipi/fusion": "2.17.1",
|
|
96
96
|
"@pi-unipi/info-screen": "2.17.0",
|
|
97
97
|
"@pi-unipi/input-shortcuts": "2.17.0",
|
|
98
98
|
"@pi-unipi/kanboard": "2.17.0",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"@pi-unipi/ralph": "2.17.0",
|
|
104
104
|
"@pi-unipi/subagents": "2.17.0",
|
|
105
105
|
"@pi-unipi/trajectory": "2.17.0",
|
|
106
|
-
"@pi-unipi/updater": "2.17.
|
|
106
|
+
"@pi-unipi/updater": "2.17.2",
|
|
107
107
|
"@pi-unipi/utility": "2.17.0",
|
|
108
108
|
"@pi-unipi/web-api": "2.17.0",
|
|
109
109
|
"@pi-unipi/workflow": "2.17.0"
|
|
@@ -153,13 +153,10 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
153
153
|
// Glance-style input surface (pi-glance-inspired). Preserves all default
|
|
154
154
|
// editor behavior via CustomEditor subclassing; only paint differs.
|
|
155
155
|
//
|
|
156
|
-
// FOCUS-SAFETY DEFERRAL:
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// and strand the dashboard unclosable (q/Esc would type into the editor).
|
|
161
|
-
// The boot overlay auto-closes after ~2s; we install after a grace period
|
|
162
|
-
// longer than any sane bootTimeoutMs.
|
|
156
|
+
// FOCUS-SAFETY DEFERRAL: the timer remains a grace period for the boot
|
|
157
|
+
// dashboard, but installGlanceEditor also restores any overlay focus after
|
|
158
|
+
// setEditorComponent() calls ui.setFocus(newEditor). This covers the
|
|
159
|
+
// updater prompt too, so q/Esc cannot be stranded in the editor.
|
|
163
160
|
state.glanceInstallTimer = setTimeout(() => installGlanceEditor(state, ctx), 3500);
|
|
164
161
|
|
|
165
162
|
// Sync TPS cursor with persisted assistant messages so streaming-hook
|
|
@@ -420,6 +417,15 @@ function installGlanceEditor(
|
|
|
420
417
|
): void {
|
|
421
418
|
if (st.glanceInstalled || !st.piContext || !st.glanceMode) return;
|
|
422
419
|
try {
|
|
420
|
+
const tui = st.tuiRef as (import("@earendil-works/pi-tui").TUI & {
|
|
421
|
+
isOverlayFocused?: () => boolean;
|
|
422
|
+
getFocusedComponent?: () => import("@earendil-works/pi-tui").Component | null;
|
|
423
|
+
}) | null | undefined;
|
|
424
|
+
const overlayFocused = tui !== undefined && tui !== null
|
|
425
|
+
&& (typeof tui.isOverlayFocused === "function" ? tui.isOverlayFocused() : tui.hasOverlay());
|
|
426
|
+
const overlayOwner = overlayFocused && typeof tui?.getFocusedComponent === "function"
|
|
427
|
+
? tui.getFocusedComponent() ?? null
|
|
428
|
+
: null;
|
|
423
429
|
const piCtx = st.piContext as Record<string, unknown> | undefined;
|
|
424
430
|
const cwd = (piCtx?.sessionManager as any)?.getCwd?.() ?? (piCtx as any)?.cwd ?? process.cwd();
|
|
425
431
|
const workspace = String(cwd).split("/").filter(Boolean).pop() ?? "~";
|
|
@@ -445,6 +451,7 @@ function installGlanceEditor(
|
|
|
445
451
|
};
|
|
446
452
|
}),
|
|
447
453
|
);
|
|
454
|
+
if (overlayOwner && tui) tui.setFocus(overlayOwner);
|
|
448
455
|
st.glanceInstalled = true;
|
|
449
456
|
} catch {
|
|
450
457
|
st.glanceInstalled = false;
|
|
@@ -117,11 +117,14 @@ savings above `$0.005`.
|
|
|
117
117
|
"default": {"lead": "provider/lead", "sidekick": "provider/sidekick"},
|
|
118
118
|
"effort": {"provider/sidekick": "high"},
|
|
119
119
|
"badges": {"provider/sidekick": "new"},
|
|
120
|
+
"prices": {"provider/sidekick": {"input": 0.2, "cachedInput": 0.02, "output": 1.2}},
|
|
120
121
|
"recent": ["provider/lead"],
|
|
121
122
|
"active": {"kind": "fusion", "lead": "provider/lead", "sidekick": "provider/sidekick"}
|
|
122
123
|
}
|
|
123
124
|
```
|
|
124
125
|
|
|
126
|
+
`prices` is an optional manual override for models whose provider reports no pricing.
|
|
127
|
+
|
|
125
128
|
## Status
|
|
126
129
|
|
|
127
130
|
- [x] Preset store, project layering, curation UI, autocomplete boost
|
|
@@ -21,6 +21,7 @@ import { homedir } from "node:os";
|
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import {
|
|
23
23
|
effortLabel,
|
|
24
|
+
globalPresetPath,
|
|
24
25
|
isEffortLevel,
|
|
25
26
|
loadPreset,
|
|
26
27
|
modelKey,
|
|
@@ -60,20 +61,18 @@ function findModel(reg: Registry | undefined, key: string): Model<Api> | undefin
|
|
|
60
61
|
return modelBykey.get(key);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function costOf(m: Model<Api> | undefined): PickerModel["cost"] {
|
|
64
|
-
const cost = m?.cost;
|
|
65
|
-
return cost &&
|
|
66
|
-
? { input: cost.input, cachedInput: cost.cacheRead ?? 0, output: cost.output }
|
|
67
|
-
: undefined;
|
|
64
|
+
function costOf(m: Model<Api> | undefined, override?: PickerModel["cost"]): PickerModel["cost"] {
|
|
65
|
+
const cost = override ?? (m?.cost && typeof m.cost.input === "number" ? { input: m.cost.input, cachedInput: m.cost.cacheRead ?? 0, output: m.cost.output } : undefined);
|
|
66
|
+
return cost && (cost.input > 0 || cost.cachedInput > 0 || cost.output > 0) ? cost : undefined;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
function toPickerModel(m: Model<Api>, badge?: FusionPreset["badges"][string]): PickerModel {
|
|
69
|
+
function toPickerModel(m: Model<Api>, badge?: FusionPreset["badges"][string], override?: PickerModel["cost"]): PickerModel {
|
|
71
70
|
return {
|
|
72
71
|
key: modelKey(m),
|
|
73
72
|
name: m.name || m.id,
|
|
74
73
|
provider: m.provider,
|
|
75
74
|
badge,
|
|
76
|
-
cost: costOf(m),
|
|
75
|
+
cost: costOf(m, override),
|
|
77
76
|
reasoning: Boolean(m.reasoning),
|
|
78
77
|
};
|
|
79
78
|
}
|
|
@@ -130,9 +129,10 @@ export default function fusionExtension(pi: ExtensionAPI): void {
|
|
|
130
129
|
function statusSavings(ctx: ExtensionContext): number | undefined {
|
|
131
130
|
if (active?.kind !== "fusion" || runtime === undefined) return undefined;
|
|
132
131
|
const reg = registryOf(ctx);
|
|
132
|
+
const preset = loadPreset(ctx.cwd ?? process.cwd()).preset;
|
|
133
133
|
const lead = findModel(reg, active.lead);
|
|
134
134
|
const side = findModel(reg, active.sidekick);
|
|
135
|
-
return estimateSavings(runtime.usage, costOf(lead), costOf(side)).savedUsd;
|
|
135
|
+
return estimateSavings(runtime.usage, costOf(lead, preset.prices[active.lead]), costOf(side, preset.prices[active.sidekick])).savedUsd;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
function publishStatus(ctx: ExtensionContext): void {
|
|
@@ -180,8 +180,14 @@ export default function fusionExtension(pi: ExtensionAPI): void {
|
|
|
180
180
|
function savingsStats(ctx: ExtensionContext): string {
|
|
181
181
|
if (active?.kind !== "fusion" || runtime === undefined) return "Fusion is not active — pick a Fusion pair with /unipi:model.";
|
|
182
182
|
const reg = registryOf(ctx);
|
|
183
|
-
const
|
|
184
|
-
|
|
183
|
+
const preset = loadPreset(ctx.cwd ?? process.cwd()).preset;
|
|
184
|
+
const leadCost = costOf(findModel(reg, active.lead), preset.prices[active.lead]);
|
|
185
|
+
const sidekickCost = costOf(findModel(reg, active.sidekick), preset.prices[active.sidekick]);
|
|
186
|
+
const savings = estimateSavings(runtime.usage, leadCost, sidekickCost);
|
|
187
|
+
const pricing = leadCost === undefined && sidekickCost === undefined
|
|
188
|
+
? '\nPricing unavailable from provider — set "prices" in ~/.unipi/config/fusion/preset.json to estimate savings.'
|
|
189
|
+
: "";
|
|
190
|
+
return `Sidekick tokens: in ${String(runtime.usage.input)} · out ${String(runtime.usage.output)} · cached ${String(runtime.usage.cacheRead)} · cache write ${String(runtime.usage.cacheWrite)}\nSidekick cost: $${savings.sidekickUsd.toFixed(2)} · at lead prices: $${savings.atLeadUsd.toFixed(2)} · saved: $${savings.savedUsd.toFixed(2)}\nHandoffs: ${String(runtime.reports.size)} · runtime alive: ${String(runtime.isAlive())} · busy: ${String(runtime.isBusy())}${pricing}`;
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
registerFusionTools(pi, {
|
|
@@ -267,7 +273,7 @@ export default function fusionExtension(pi: ExtensionAPI): void {
|
|
|
267
273
|
const cwd = ctx.cwd ?? process.cwd();
|
|
268
274
|
const loaded = loadPreset(cwd);
|
|
269
275
|
const preset = loaded.preset;
|
|
270
|
-
const models = reg.getAvailable().map((m) => toPickerModel(m, preset.badges[modelKey(m)]));
|
|
276
|
+
const models = reg.getAvailable().map((m) => toPickerModel(m, preset.badges[modelKey(m)], preset.prices[modelKey(m)]));
|
|
271
277
|
if (models.length === 0) {
|
|
272
278
|
ctx.ui.notify("No models available. Use /login to add a provider.", "warning");
|
|
273
279
|
return;
|
|
@@ -347,13 +353,26 @@ export default function fusionExtension(pi: ExtensionAPI): void {
|
|
|
347
353
|
},
|
|
348
354
|
});
|
|
349
355
|
|
|
350
|
-
pi.on("session_start", (_e, ctx) => {
|
|
356
|
+
pi.on("session_start", async (_e, ctx) => {
|
|
351
357
|
stopRuntime();
|
|
352
358
|
nudged = false;
|
|
353
359
|
modelBykey.clear();
|
|
354
360
|
active = loadPreset(ctx.cwd ?? process.cwd()).preset.active;
|
|
355
|
-
|
|
356
|
-
|
|
361
|
+
if (active?.kind === "fusion" && (!ctx.model || modelKey(ctx.model) !== active.lead)) {
|
|
362
|
+
const leadKey = active.lead;
|
|
363
|
+
const lead = findModel(registryOf(ctx), leadKey);
|
|
364
|
+
const restored = lead !== undefined && await pi.setModel(lead);
|
|
365
|
+
if (restored) {
|
|
366
|
+
try {
|
|
367
|
+
pi.setThinkingLevel(active.leadEffort ?? "medium");
|
|
368
|
+
} catch {
|
|
369
|
+
/* provider may not support thinking */
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
active = undefined;
|
|
373
|
+
if (ctx.hasUI) ctx.ui.notify(`Fusion lead ${leadKey} unavailable — Fusion off`, "warning");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
357
376
|
publishStatus(ctx);
|
|
358
377
|
if (ctx.hasUI) ctx.ui.addAutocompleteProvider(createModelBoostProvider);
|
|
359
378
|
});
|
|
@@ -369,6 +388,8 @@ export default function fusionExtension(pi: ExtensionAPI): void {
|
|
|
369
388
|
if (active?.kind === "fusion" && modelKey(event.model) !== active.lead) {
|
|
370
389
|
stopRuntime();
|
|
371
390
|
active = { kind: "single", model: modelKey(event.model) };
|
|
391
|
+
const loaded = loadPreset(ctx.cwd ?? process.cwd());
|
|
392
|
+
saveRuntimeState(globalPresetPath(), { effort: loaded.preset.effort, recent: loaded.preset.recent, active });
|
|
372
393
|
publishStatus(ctx);
|
|
373
394
|
}
|
|
374
395
|
});
|
|
@@ -128,6 +128,10 @@ function money(perMillion: number): string {
|
|
|
128
128
|
return `$${rounded.replace(/\.0+$/u, "").replace(/(\.\d)0$/u, "$1")} / 1M`;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
function hasPricing(cost: PickerModel["cost"]): cost is NonNullable<PickerModel["cost"]> {
|
|
132
|
+
return cost !== undefined && (cost.input > 0 || cost.cachedInput > 0 || cost.output > 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
131
135
|
function pad(text: string, width: number): string {
|
|
132
136
|
const w = visibleWidth(text);
|
|
133
137
|
return w >= width ? text : text + " ".repeat(width - w);
|
|
@@ -161,7 +165,7 @@ export class ModelPicker {
|
|
|
161
165
|
this.onRenderRequest = options.onRenderRequest;
|
|
162
166
|
this.visibleRows = options.visibleRows ?? DEFAULT_VISIBLE_ROWS;
|
|
163
167
|
this.modelsByKey = new Map(options.state.models.map((m) => [m.key, m]));
|
|
164
|
-
const prices = options.state.models.map((m) => (m.cost ? blendedPrice(m.cost) : undefined)).filter((p): p is number => p !== undefined);
|
|
168
|
+
const prices = options.state.models.map((m) => (hasPricing(m.cost) ? blendedPrice(m.cost) : undefined)).filter((p): p is number => p !== undefined && p > 0);
|
|
165
169
|
this.priceRange = { min: prices.length > 0 ? Math.min(...prices) : 0, max: prices.length > 0 ? Math.max(...prices) : 0 };
|
|
166
170
|
this.effort = { ...options.state.effort };
|
|
167
171
|
const active = options.state.active;
|
|
@@ -477,24 +481,27 @@ export class ModelPicker {
|
|
|
477
481
|
const primaryKey = row.kind === "fusion" ? this.lead : row.key;
|
|
478
482
|
const primary = primaryKey === undefined ? undefined : this.modelsByKey.get(primaryKey);
|
|
479
483
|
const side = row.kind === "fusion" && this.sidekick !== undefined ? this.modelsByKey.get(this.sidekick) : undefined;
|
|
484
|
+
const primaryCost = primary?.cost;
|
|
485
|
+
const sideCost = side?.cost;
|
|
480
486
|
const cols: Array<[string, string]> = [];
|
|
481
|
-
if (
|
|
482
|
-
cols.push(["Input", money(
|
|
483
|
-
cols.push(["Cached input", money(
|
|
484
|
-
cols.push(["Output", money(
|
|
487
|
+
if (hasPricing(primaryCost)) {
|
|
488
|
+
cols.push(["Input", money(primaryCost.input)]);
|
|
489
|
+
cols.push(["Cached input", money(primaryCost.cachedInput)]);
|
|
490
|
+
cols.push(["Output", money(primaryCost.output)]);
|
|
485
491
|
} else {
|
|
486
492
|
cols.push(["Input", "—"], ["Cached input", "—"], ["Output", "—"]);
|
|
487
493
|
}
|
|
488
494
|
if (row.kind === "fusion") {
|
|
489
|
-
if (
|
|
490
|
-
cols.push(["Sidekick input", money(
|
|
491
|
-
cols.push(["Sidekick cached input", money(
|
|
492
|
-
cols.push(["Sidekick output", money(
|
|
495
|
+
if (hasPricing(sideCost)) {
|
|
496
|
+
cols.push(["Sidekick input", money(sideCost.input)]);
|
|
497
|
+
cols.push(["Sidekick cached input", money(sideCost.cachedInput)]);
|
|
498
|
+
cols.push(["Sidekick output", money(sideCost.output)]);
|
|
493
499
|
} else {
|
|
494
500
|
cols.push(["Sidekick input", "—"], ["Sidekick cached input", "—"], ["Sidekick output", "—"]);
|
|
495
501
|
}
|
|
496
502
|
}
|
|
497
|
-
const
|
|
503
|
+
const need = Math.max(...cols.map(([h, v]) => Math.max(visibleWidth(h), visibleWidth(v)))) + 3;
|
|
504
|
+
const colWidth = Math.max(10, Math.min(need, Math.floor((width - 4) / cols.length)));
|
|
498
505
|
const head = cols.map(([h]) => pad(t.fg("dim", h), colWidth)).join("");
|
|
499
506
|
const vals = cols.map(([, v]) => pad(t.fg("text", v), colWidth)).join("");
|
|
500
507
|
const desc =
|
|
@@ -506,7 +513,11 @@ export class ModelPicker {
|
|
|
506
513
|
const badges = this.state.models.some((m) => m.badge !== undefined)
|
|
507
514
|
? `${t.fg("success", "✱")} ${t.fg("dim", "New")} ${t.fg("accent", "✱")} ${t.fg("dim", "Promotion")} ${t.fg("warning", "✱")} ${t.fg("dim", "Beta")} ${t.fg("dim", "·")}`
|
|
508
515
|
: "";
|
|
509
|
-
const
|
|
516
|
+
const noPricing = row.kind === "fusion"
|
|
517
|
+
? !hasPricing(primaryCost) || !hasPricing(sideCost)
|
|
518
|
+
: !hasPricing(primaryCost);
|
|
519
|
+
const pricing = noPricing ? t.fg("dim", " · no pricing data from provider") : "";
|
|
520
|
+
const description = `${badges}${badges.length > 0 ? " " : ""}${desc}${pricing}`;
|
|
510
521
|
return [truncateToWidth(` ${head}`, width - 1), truncateToWidth(` ${vals}`, width - 1), truncateToWidth(` ${description}`, width - 1)];
|
|
511
522
|
}
|
|
512
523
|
|
|
@@ -561,8 +572,9 @@ export class ModelPicker {
|
|
|
561
572
|
const sliderCells = Math.min(48, Math.max(1, width - 6));
|
|
562
573
|
const sliderKey = row?.kind === "fusion" ? this.lead : row?.key;
|
|
563
574
|
const sliderModel = sliderKey === undefined ? undefined : this.modelsByKey.get(sliderKey);
|
|
564
|
-
const
|
|
565
|
-
const
|
|
575
|
+
const sliderCost = sliderModel?.cost;
|
|
576
|
+
const sliderPrice = hasPricing(sliderCost) ? blendedPrice(sliderCost) : undefined;
|
|
577
|
+
const marker = this.priceRange.max <= 0 || sliderPrice === undefined ? undefined : sliderPosition(sliderPrice, this.priceRange.min, this.priceRange.max, sliderCells);
|
|
566
578
|
lines.push(truncateToWidth(` ${renderSlider(sliderCells, marker)}`, width - 1));
|
|
567
579
|
lines.push(...this.renderPricePanel(row, width));
|
|
568
580
|
lines.push("");
|
|
@@ -61,6 +61,8 @@ export interface FusionPreset {
|
|
|
61
61
|
recent: ModelKey[];
|
|
62
62
|
/** Optional hand-curated model badge metadata. */
|
|
63
63
|
badges: Record<ModelKey, FusionBadge>;
|
|
64
|
+
/** Manual pricing overrides for providers that report no pricing. */
|
|
65
|
+
prices: Record<ModelKey, { input: number; cachedInput: number; output: number }>;
|
|
64
66
|
/** What the user last confirmed in the picker. */
|
|
65
67
|
active?: ActiveSelection | undefined;
|
|
66
68
|
}
|
|
@@ -74,6 +76,7 @@ export function emptyPreset(): FusionPreset {
|
|
|
74
76
|
effort: {},
|
|
75
77
|
recent: [],
|
|
76
78
|
badges: {},
|
|
79
|
+
prices: {},
|
|
77
80
|
};
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -136,6 +139,20 @@ export function parsePreset(raw: unknown): Partial<FusionPreset> {
|
|
|
136
139
|
}
|
|
137
140
|
out.badges = badges;
|
|
138
141
|
}
|
|
142
|
+
if (typeof r["prices"] === "object" && r["prices"] !== null) {
|
|
143
|
+
const prices: FusionPreset["prices"] = {};
|
|
144
|
+
for (const [k, v] of Object.entries(r["prices"] as Record<string, unknown>)) {
|
|
145
|
+
if (typeof v !== "object" || v === null) continue;
|
|
146
|
+
const price = v as Record<string, unknown>;
|
|
147
|
+
const input = price["input"];
|
|
148
|
+
const cachedInput = price["cachedInput"];
|
|
149
|
+
const output = price["output"];
|
|
150
|
+
if ([input, cachedInput, output].every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0)) {
|
|
151
|
+
prices[k] = { input: input as number, cachedInput: cachedInput as number, output: output as number };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
out.prices = prices;
|
|
155
|
+
}
|
|
139
156
|
const active = r["active"];
|
|
140
157
|
if (typeof active === "object" && active !== null) {
|
|
141
158
|
const a = active as Record<string, unknown>;
|
|
@@ -167,6 +184,7 @@ export function mergePresets(base: FusionPreset, over: Partial<FusionPreset>): F
|
|
|
167
184
|
effort: { ...base.effort, ...(over.effort ?? {}) },
|
|
168
185
|
recent: over.recent ?? base.recent,
|
|
169
186
|
badges: { ...base.badges, ...(over.badges ?? {}) },
|
|
187
|
+
prices: { ...base.prices, ...(over.prices ?? {}) },
|
|
170
188
|
active: over.active ?? base.active,
|
|
171
189
|
};
|
|
172
190
|
}
|
|
@@ -6978,7 +6978,7 @@ var init_hooks = __esm({
|
|
|
6978
6978
|
if (!debug) return;
|
|
6979
6979
|
try {
|
|
6980
6980
|
import("node:fs").then(
|
|
6981
|
-
({ writeFileSync:
|
|
6981
|
+
({ writeFileSync: writeFileSync49 }) => writeFileSync49("/tmp/compactor-debug.json", JSON.stringify(data, null, 2))
|
|
6982
6982
|
).catch(() => {
|
|
6983
6983
|
});
|
|
6984
6984
|
} catch {
|
|
@@ -15246,8 +15246,8 @@ function resolvePiPackageRoot() {
|
|
|
15246
15246
|
return void 0;
|
|
15247
15247
|
}
|
|
15248
15248
|
}
|
|
15249
|
-
function isRunnableNodeScript(filePath,
|
|
15250
|
-
if (!
|
|
15249
|
+
function isRunnableNodeScript(filePath, existsSync65) {
|
|
15250
|
+
if (!existsSync65(filePath)) return false;
|
|
15251
15251
|
return /\.(?:mjs|cjs|js)$/i.test(filePath);
|
|
15252
15252
|
}
|
|
15253
15253
|
function normalizePath(filePath) {
|
|
@@ -15258,16 +15258,16 @@ function isStandalonePiExecutable(execPath) {
|
|
|
15258
15258
|
return /^pi(?:\.exe)?$/i.test(executableName ?? "");
|
|
15259
15259
|
}
|
|
15260
15260
|
function resolvePiCliScript(deps = {}) {
|
|
15261
|
-
const
|
|
15261
|
+
const existsSync65 = deps.existsSync ?? fs9.existsSync;
|
|
15262
15262
|
const realpathSync9 = deps.realpathSync ?? fs9.realpathSync;
|
|
15263
|
-
const
|
|
15263
|
+
const readFileSync56 = deps.readFileSync ?? ((filePath, encoding) => fs9.readFileSync(filePath, encoding));
|
|
15264
15264
|
const argv1 = deps.argv1 ?? process.argv[1];
|
|
15265
15265
|
if (argv1) {
|
|
15266
15266
|
const argvPath = normalizePath(argv1);
|
|
15267
|
-
if (isRunnableNodeScript(argvPath,
|
|
15267
|
+
if (isRunnableNodeScript(argvPath, existsSync65)) {
|
|
15268
15268
|
try {
|
|
15269
15269
|
const canonicalArgvPath = realpathSync9(argvPath);
|
|
15270
|
-
if (isRunnableNodeScript(canonicalArgvPath,
|
|
15270
|
+
if (isRunnableNodeScript(canonicalArgvPath, existsSync65) && findPiPackageRootFromEntry(canonicalArgvPath)) {
|
|
15271
15271
|
return canonicalArgvPath;
|
|
15272
15272
|
}
|
|
15273
15273
|
} catch {
|
|
@@ -15285,12 +15285,12 @@ function resolvePiCliScript(deps = {}) {
|
|
|
15285
15285
|
return path9.join(packageRoot, "package.json");
|
|
15286
15286
|
});
|
|
15287
15287
|
const packageJsonPath = resolvePackageJson();
|
|
15288
|
-
const packageJson = JSON.parse(
|
|
15288
|
+
const packageJson = JSON.parse(readFileSync56(packageJsonPath, "utf8"));
|
|
15289
15289
|
const binField = packageJson.bin;
|
|
15290
15290
|
const binPath = typeof binField === "string" ? binField : binField?.pi ?? Object.values(binField ?? {})[0];
|
|
15291
15291
|
if (!binPath) return void 0;
|
|
15292
15292
|
const candidate = path9.resolve(path9.dirname(packageJsonPath), binPath);
|
|
15293
|
-
if (isRunnableNodeScript(candidate,
|
|
15293
|
+
if (isRunnableNodeScript(candidate, existsSync65)) {
|
|
15294
15294
|
return candidate;
|
|
15295
15295
|
}
|
|
15296
15296
|
} catch {
|
|
@@ -42422,15 +42422,15 @@ function compactorExtension(pi) {
|
|
|
42422
42422
|
config = loadConfig4(cwd);
|
|
42423
42423
|
currentSessionId = `${ctx.sessionId ?? "default"}${getWorktreeSuffix()}`;
|
|
42424
42424
|
try {
|
|
42425
|
-
const { existsSync:
|
|
42426
|
-
const { join:
|
|
42425
|
+
const { existsSync: existsSync65 } = await import("node:fs");
|
|
42426
|
+
const { join: join78 } = await import("node:path");
|
|
42427
42427
|
const strategies = [
|
|
42428
42428
|
{ key: "commits", config: config.commits }
|
|
42429
42429
|
];
|
|
42430
42430
|
for (const { key, config: strat } of strategies) {
|
|
42431
42431
|
if (strat.autoDetect === "git") {
|
|
42432
|
-
const gitDir =
|
|
42433
|
-
if (!
|
|
42432
|
+
const gitDir = join78(cwd, ".git");
|
|
42433
|
+
if (!existsSync65(gitDir)) {
|
|
42434
42434
|
strat.enabled = false;
|
|
42435
42435
|
}
|
|
42436
42436
|
}
|
|
@@ -46449,11 +46449,14 @@ function applyGlanceMode(st, cmdCtx) {
|
|
|
46449
46449
|
function installGlanceEditor(st, uiHost) {
|
|
46450
46450
|
if (st.glanceInstalled || !st.piContext || !st.glanceMode) return;
|
|
46451
46451
|
try {
|
|
46452
|
+
const tui = st.tuiRef;
|
|
46453
|
+
const overlayFocused = tui !== void 0 && tui !== null && (typeof tui.isOverlayFocused === "function" ? tui.isOverlayFocused() : tui.hasOverlay());
|
|
46454
|
+
const overlayOwner = overlayFocused && typeof tui?.getFocusedComponent === "function" ? tui.getFocusedComponent() ?? null : null;
|
|
46452
46455
|
const piCtx = st.piContext;
|
|
46453
46456
|
const cwd = piCtx?.sessionManager?.getCwd?.() ?? piCtx?.cwd ?? process.cwd();
|
|
46454
46457
|
const workspace = String(cwd).split("/").filter(Boolean).pop() ?? "~";
|
|
46455
46458
|
uiHost.ui.setEditorComponent(
|
|
46456
|
-
(
|
|
46459
|
+
(tui2, theme, keybindings) => new GlanceEditor(tui2, theme, keybindings, () => {
|
|
46457
46460
|
const p = st.piContext;
|
|
46458
46461
|
const usage2 = typeof p?.getContextUsage === "function" ? p.getContextUsage() : void 0;
|
|
46459
46462
|
const model = p?.model;
|
|
@@ -46472,6 +46475,7 @@ function installGlanceEditor(st, uiHost) {
|
|
|
46472
46475
|
};
|
|
46473
46476
|
})
|
|
46474
46477
|
);
|
|
46478
|
+
if (overlayOwner && tui) tui.setFocus(overlayOwner);
|
|
46475
46479
|
st.glanceInstalled = true;
|
|
46476
46480
|
} catch {
|
|
46477
46481
|
st.glanceInstalled = false;
|
|
@@ -46968,9 +46972,12 @@ var VERSION_HEADER_RE = /^## \[(.+?)\](?:\s*[-—–]\s*(.+))?$/;
|
|
|
46968
46972
|
var SECTION_HEADER_RE = /^### (.+)$/;
|
|
46969
46973
|
function parseChangelog(filePath) {
|
|
46970
46974
|
if (!existsSync56(filePath)) return [];
|
|
46971
|
-
|
|
46972
|
-
|
|
46973
|
-
|
|
46975
|
+
return parseChangelogContent(readFileSync47(filePath, "utf-8"));
|
|
46976
|
+
}
|
|
46977
|
+
function parseChangelogContent(content) {
|
|
46978
|
+
const trimmed = content.trim();
|
|
46979
|
+
if (!trimmed) return [];
|
|
46980
|
+
const lines = trimmed.split("\n");
|
|
46974
46981
|
const entries = [];
|
|
46975
46982
|
let currentEntry = null;
|
|
46976
46983
|
let currentSection = null;
|
|
@@ -47006,12 +47013,12 @@ function parseChangelog(filePath) {
|
|
|
47006
47013
|
currentItems = [];
|
|
47007
47014
|
continue;
|
|
47008
47015
|
}
|
|
47009
|
-
const
|
|
47010
|
-
if (currentSection && (
|
|
47011
|
-
currentItems.push(
|
|
47012
|
-
} else if (
|
|
47016
|
+
const trimmed2 = line.trim();
|
|
47017
|
+
if (currentSection && (trimmed2.startsWith("- ") || trimmed2.startsWith("* "))) {
|
|
47018
|
+
currentItems.push(trimmed2.slice(2).trim());
|
|
47019
|
+
} else if (trimmed2 && currentSection) {
|
|
47013
47020
|
if (currentItems.length > 0) {
|
|
47014
|
-
currentItems[currentItems.length - 1] += " " +
|
|
47021
|
+
currentItems[currentItems.length - 1] += " " + trimmed2;
|
|
47015
47022
|
}
|
|
47016
47023
|
}
|
|
47017
47024
|
}
|
|
@@ -47038,7 +47045,7 @@ function getNewerVersions(entries, installedVersion) {
|
|
|
47038
47045
|
const result2 = [];
|
|
47039
47046
|
for (const entry of entries) {
|
|
47040
47047
|
if (entry.version === "Unreleased") {
|
|
47041
|
-
result2.push(entry);
|
|
47048
|
+
if (Object.keys(entry.sections).length > 0) result2.push(entry);
|
|
47042
47049
|
continue;
|
|
47043
47050
|
}
|
|
47044
47051
|
if (!isNewerVersion(entry.version, installedVersion)) break;
|
|
@@ -47475,15 +47482,17 @@ function padVisible3(content, targetWidth) {
|
|
|
47475
47482
|
const pad2 = Math.max(0, targetWidth - vw);
|
|
47476
47483
|
return content + " ".repeat(pad2);
|
|
47477
47484
|
}
|
|
47478
|
-
function renderUpdateOverlay(checkResult) {
|
|
47485
|
+
function renderUpdateOverlay(checkResult, providedNewerVersions) {
|
|
47479
47486
|
return (tui, theme, _kb, done) => {
|
|
47480
47487
|
const config = loadConfig5();
|
|
47481
|
-
let newerVersions = [];
|
|
47482
|
-
|
|
47483
|
-
|
|
47484
|
-
|
|
47485
|
-
|
|
47486
|
-
|
|
47488
|
+
let newerVersions = providedNewerVersions ?? [];
|
|
47489
|
+
if (providedNewerVersions === void 0) {
|
|
47490
|
+
const changelogPath = resolveChangelogPath();
|
|
47491
|
+
try {
|
|
47492
|
+
const entries = parseChangelog(changelogPath);
|
|
47493
|
+
newerVersions = getNewerVersions(entries, checkResult.currentVersion);
|
|
47494
|
+
} catch (_err) {
|
|
47495
|
+
}
|
|
47487
47496
|
}
|
|
47488
47497
|
const contentLines = [];
|
|
47489
47498
|
for (const entry of newerVersions) {
|
|
@@ -47496,7 +47505,7 @@ function renderUpdateOverlay(checkResult) {
|
|
|
47496
47505
|
contentLines.push("");
|
|
47497
47506
|
}
|
|
47498
47507
|
if (contentLines.length === 0) {
|
|
47499
|
-
contentLines.push(` ${theme.fg("muted",
|
|
47508
|
+
contentLines.push(` ${theme.fg("muted", `No changelog available for ${checkResult.latestVersion} (offline?).`)}`);
|
|
47500
47509
|
}
|
|
47501
47510
|
const state2 = {
|
|
47502
47511
|
result: checkResult,
|
|
@@ -47633,6 +47642,49 @@ function renderUpdateOverlay(checkResult) {
|
|
|
47633
47642
|
};
|
|
47634
47643
|
}
|
|
47635
47644
|
|
|
47645
|
+
// packages/updater/src/remote-changelog.ts
|
|
47646
|
+
init_core();
|
|
47647
|
+
import { existsSync as existsSync60, mkdirSync as mkdirSync38, readFileSync as readFileSync50, writeFileSync as writeFileSync42 } from "node:fs";
|
|
47648
|
+
import { homedir as homedir34 } from "node:os";
|
|
47649
|
+
import { join as join69 } from "node:path";
|
|
47650
|
+
var CHANGELOG_RAW_BASE = "https://raw.githubusercontent.com/Neuron-Mr-White/unipi";
|
|
47651
|
+
function cacheDirectory(opts) {
|
|
47652
|
+
return (opts?.cacheDir ?? UPDATER_DIRS.CACHE).replace("~", homedir34());
|
|
47653
|
+
}
|
|
47654
|
+
async function fetchRemoteChangelog(version, opts = {}) {
|
|
47655
|
+
const cacheDir2 = cacheDirectory(opts);
|
|
47656
|
+
const cachePath = join69(cacheDir2, `changelog-${version}.md`);
|
|
47657
|
+
try {
|
|
47658
|
+
if (existsSync60(cachePath)) return readFileSync50(cachePath, "utf8");
|
|
47659
|
+
} catch {
|
|
47660
|
+
}
|
|
47661
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
47662
|
+
const signal = AbortSignal.timeout(opts.timeoutMs ?? 5e3);
|
|
47663
|
+
try {
|
|
47664
|
+
let response = await fetchImpl(`${CHANGELOG_RAW_BASE}/v${version}/CHANGELOG.md`, { signal });
|
|
47665
|
+
const cacheable = response.ok;
|
|
47666
|
+
if (response.status === 404) {
|
|
47667
|
+
response = await fetchImpl(`${CHANGELOG_RAW_BASE}/main/CHANGELOG.md`, { signal });
|
|
47668
|
+
}
|
|
47669
|
+
if (!response.ok) return null;
|
|
47670
|
+
const content = await response.text();
|
|
47671
|
+
if (cacheable) {
|
|
47672
|
+
mkdirSync38(cacheDir2, { recursive: true });
|
|
47673
|
+
writeFileSync42(cachePath, content, "utf8");
|
|
47674
|
+
}
|
|
47675
|
+
return content;
|
|
47676
|
+
} catch {
|
|
47677
|
+
return null;
|
|
47678
|
+
}
|
|
47679
|
+
}
|
|
47680
|
+
async function loadUpdateChangelog(currentVersion, latestVersion, opts) {
|
|
47681
|
+
const remote = await fetchRemoteChangelog(latestVersion, opts);
|
|
47682
|
+
const entries = remote === null ? [] : parseChangelogContent(remote);
|
|
47683
|
+
const newer = remote === null ? [] : getNewerVersions(entries, currentVersion);
|
|
47684
|
+
if (newer.length > 0) return newer;
|
|
47685
|
+
return getNewerVersions(parseChangelog(resolveChangelogPath()), currentVersion);
|
|
47686
|
+
}
|
|
47687
|
+
|
|
47636
47688
|
// packages/updater/src/index.ts
|
|
47637
47689
|
var VERSION10 = getPackageVersion(new URL("..", import.meta.url).pathname);
|
|
47638
47690
|
function updaterExtension(pi) {
|
|
@@ -47726,8 +47778,9 @@ function updaterExtension(pi) {
|
|
|
47726
47778
|
latestVersion: result2.latestVersion
|
|
47727
47779
|
});
|
|
47728
47780
|
if (ctx.hasUI) {
|
|
47781
|
+
const entries = await loadUpdateChangelog(result2.currentVersion, result2.latestVersion);
|
|
47729
47782
|
const updateResult = await ctx.ui.custom(
|
|
47730
|
-
renderUpdateOverlay(result2),
|
|
47783
|
+
renderUpdateOverlay(result2, entries),
|
|
47731
47784
|
{
|
|
47732
47785
|
overlay: true,
|
|
47733
47786
|
overlayOptions: {
|
|
@@ -47755,8 +47808,8 @@ init_core();
|
|
|
47755
47808
|
import { Key as Key19, matchesKey as matchesKey25 } from "@earendil-works/pi-tui";
|
|
47756
47809
|
|
|
47757
47810
|
// packages/input-shortcuts/src/registers.ts
|
|
47758
|
-
import { existsSync as
|
|
47759
|
-
import { dirname as dirname45, join as
|
|
47811
|
+
import { existsSync as existsSync61, mkdirSync as mkdirSync39, readFileSync as readFileSync51, renameSync as renameSync13, writeFileSync as writeFileSync43 } from "node:fs";
|
|
47812
|
+
import { dirname as dirname45, join as join70 } from "node:path";
|
|
47760
47813
|
|
|
47761
47814
|
// packages/input-shortcuts/src/types.ts
|
|
47762
47815
|
var DEFAULT_CONFIG8 = {
|
|
@@ -47779,7 +47832,7 @@ var RegisterStore = class {
|
|
|
47779
47832
|
filePath;
|
|
47780
47833
|
loaded = false;
|
|
47781
47834
|
constructor(baseDir) {
|
|
47782
|
-
this.filePath = baseDir ?
|
|
47835
|
+
this.filePath = baseDir ? join70(baseDir, REGISTERS_FILE) : REGISTERS_FILE;
|
|
47783
47836
|
}
|
|
47784
47837
|
/** Get the stash register contents. */
|
|
47785
47838
|
getStash() {
|
|
@@ -47803,8 +47856,8 @@ var RegisterStore = class {
|
|
|
47803
47856
|
if (this.loaded) return;
|
|
47804
47857
|
this.loaded = true;
|
|
47805
47858
|
try {
|
|
47806
|
-
if (
|
|
47807
|
-
const raw =
|
|
47859
|
+
if (existsSync61(this.filePath)) {
|
|
47860
|
+
const raw = readFileSync51(this.filePath, "utf-8");
|
|
47808
47861
|
const parsed = JSON.parse(raw);
|
|
47809
47862
|
this.data = {
|
|
47810
47863
|
stash: typeof parsed.stash === "string" ? parsed.stash : "",
|
|
@@ -47821,11 +47874,11 @@ var RegisterStore = class {
|
|
|
47821
47874
|
save() {
|
|
47822
47875
|
try {
|
|
47823
47876
|
const dir = dirname45(this.filePath);
|
|
47824
|
-
if (!
|
|
47825
|
-
|
|
47877
|
+
if (!existsSync61(dir)) {
|
|
47878
|
+
mkdirSync39(dir, { recursive: true });
|
|
47826
47879
|
}
|
|
47827
47880
|
const tmpPath = this.filePath + ".tmp";
|
|
47828
|
-
|
|
47881
|
+
writeFileSync43(tmpPath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
47829
47882
|
renameSync13(tmpPath, this.filePath);
|
|
47830
47883
|
} catch {
|
|
47831
47884
|
}
|
|
@@ -48037,8 +48090,8 @@ var ChordOverlay = class extends Container2 {
|
|
|
48037
48090
|
|
|
48038
48091
|
// packages/input-shortcuts/src/settings-overlay.ts
|
|
48039
48092
|
import { SettingsList as SettingsList3 } from "@earendil-works/pi-tui";
|
|
48040
|
-
import { existsSync as
|
|
48041
|
-
import { dirname as dirname46, join as
|
|
48093
|
+
import { existsSync as existsSync62, mkdirSync as mkdirSync40, readFileSync as readFileSync52, renameSync as renameSync14, writeFileSync as writeFileSync44 } from "node:fs";
|
|
48094
|
+
import { dirname as dirname46, join as join71 } from "node:path";
|
|
48042
48095
|
var ALT_KEY_OPTIONS = [
|
|
48043
48096
|
"alt+a",
|
|
48044
48097
|
"alt+b",
|
|
@@ -48070,10 +48123,10 @@ var ALT_KEY_OPTIONS = [
|
|
|
48070
48123
|
var CONFLICTS = /* @__PURE__ */ new Set(["alt+e"]);
|
|
48071
48124
|
var FREE_ALT_KEYS = ALT_KEY_OPTIONS.filter((k) => !CONFLICTS.has(k));
|
|
48072
48125
|
function loadConfig6(baseDir) {
|
|
48073
|
-
const filePath = baseDir ?
|
|
48126
|
+
const filePath = baseDir ? join71(baseDir, CONFIG_FILE) : CONFIG_FILE;
|
|
48074
48127
|
try {
|
|
48075
|
-
if (
|
|
48076
|
-
const raw =
|
|
48128
|
+
if (existsSync62(filePath)) {
|
|
48129
|
+
const raw = readFileSync52(filePath, "utf-8");
|
|
48077
48130
|
const parsed = JSON.parse(raw);
|
|
48078
48131
|
return {
|
|
48079
48132
|
chordKey: typeof parsed.chordKey === "string" ? parsed.chordKey : DEFAULT_CONFIG8.chordKey,
|
|
@@ -48085,14 +48138,14 @@ function loadConfig6(baseDir) {
|
|
|
48085
48138
|
return { ...DEFAULT_CONFIG8 };
|
|
48086
48139
|
}
|
|
48087
48140
|
function saveConfig5(config, baseDir) {
|
|
48088
|
-
const filePath = baseDir ?
|
|
48141
|
+
const filePath = baseDir ? join71(baseDir, CONFIG_FILE) : CONFIG_FILE;
|
|
48089
48142
|
try {
|
|
48090
48143
|
const dir = dirname46(filePath);
|
|
48091
|
-
if (!
|
|
48092
|
-
|
|
48144
|
+
if (!existsSync62(dir)) {
|
|
48145
|
+
mkdirSync40(dir, { recursive: true });
|
|
48093
48146
|
}
|
|
48094
48147
|
const tmpPath = filePath + ".tmp";
|
|
48095
|
-
|
|
48148
|
+
writeFileSync44(tmpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
48096
48149
|
renameSync14(tmpPath, filePath);
|
|
48097
48150
|
} catch {
|
|
48098
48151
|
}
|
|
@@ -50304,13 +50357,13 @@ function src_default5(pi) {
|
|
|
50304
50357
|
|
|
50305
50358
|
// packages/fusion/src/index.ts
|
|
50306
50359
|
init_core();
|
|
50307
|
-
import { homedir as
|
|
50308
|
-
import { join as
|
|
50360
|
+
import { homedir as homedir37 } from "node:os";
|
|
50361
|
+
import { join as join76 } from "node:path";
|
|
50309
50362
|
|
|
50310
50363
|
// packages/fusion/src/preset.ts
|
|
50311
|
-
import { existsSync as
|
|
50312
|
-
import { homedir as
|
|
50313
|
-
import { dirname as dirname48, join as
|
|
50364
|
+
import { existsSync as existsSync64, mkdirSync as mkdirSync43, readFileSync as readFileSync55, writeFileSync as writeFileSync47, renameSync as renameSync15 } from "node:fs";
|
|
50365
|
+
import { homedir as homedir36 } from "node:os";
|
|
50366
|
+
import { dirname as dirname48, join as join74 } from "node:path";
|
|
50314
50367
|
var PRESET_SCHEMA_VERSION = 1;
|
|
50315
50368
|
var EFFORT_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
50316
50369
|
var RECENT_LIMIT = 5;
|
|
@@ -50322,14 +50375,15 @@ function emptyPreset() {
|
|
|
50322
50375
|
default: {},
|
|
50323
50376
|
effort: {},
|
|
50324
50377
|
recent: [],
|
|
50325
|
-
badges: {}
|
|
50378
|
+
badges: {},
|
|
50379
|
+
prices: {}
|
|
50326
50380
|
};
|
|
50327
50381
|
}
|
|
50328
|
-
function globalPresetPath(home =
|
|
50329
|
-
return
|
|
50382
|
+
function globalPresetPath(home = homedir36()) {
|
|
50383
|
+
return join74(home, ".unipi", "config", "fusion", "preset.json");
|
|
50330
50384
|
}
|
|
50331
50385
|
function projectPresetPath(cwd) {
|
|
50332
|
-
return
|
|
50386
|
+
return join74(cwd, ".unipi", "fusion-preset.json");
|
|
50333
50387
|
}
|
|
50334
50388
|
function modelKey(model) {
|
|
50335
50389
|
return `${model.provider}/${model.id}`;
|
|
@@ -50377,6 +50431,20 @@ function parsePreset2(raw) {
|
|
|
50377
50431
|
}
|
|
50378
50432
|
out.badges = badges;
|
|
50379
50433
|
}
|
|
50434
|
+
if (typeof r["prices"] === "object" && r["prices"] !== null) {
|
|
50435
|
+
const prices = {};
|
|
50436
|
+
for (const [k, v] of Object.entries(r["prices"])) {
|
|
50437
|
+
if (typeof v !== "object" || v === null) continue;
|
|
50438
|
+
const price = v;
|
|
50439
|
+
const input = price["input"];
|
|
50440
|
+
const cachedInput = price["cachedInput"];
|
|
50441
|
+
const output = price["output"];
|
|
50442
|
+
if ([input, cachedInput, output].every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0)) {
|
|
50443
|
+
prices[k] = { input, cachedInput, output };
|
|
50444
|
+
}
|
|
50445
|
+
}
|
|
50446
|
+
out.prices = prices;
|
|
50447
|
+
}
|
|
50380
50448
|
const active = r["active"];
|
|
50381
50449
|
if (typeof active === "object" && active !== null) {
|
|
50382
50450
|
const a = active;
|
|
@@ -50403,18 +50471,19 @@ function mergePresets(base, over) {
|
|
|
50403
50471
|
effort: { ...base.effort, ...over.effort ?? {} },
|
|
50404
50472
|
recent: over.recent ?? base.recent,
|
|
50405
50473
|
badges: { ...base.badges, ...over.badges ?? {} },
|
|
50474
|
+
prices: { ...base.prices, ...over.prices ?? {} },
|
|
50406
50475
|
active: over.active ?? base.active
|
|
50407
50476
|
};
|
|
50408
50477
|
}
|
|
50409
50478
|
function readJson4(path41) {
|
|
50410
50479
|
try {
|
|
50411
|
-
if (!
|
|
50412
|
-
return JSON.parse(
|
|
50480
|
+
if (!existsSync64(path41)) return void 0;
|
|
50481
|
+
return JSON.parse(readFileSync55(path41, "utf8"));
|
|
50413
50482
|
} catch {
|
|
50414
50483
|
return void 0;
|
|
50415
50484
|
}
|
|
50416
50485
|
}
|
|
50417
|
-
function loadPreset(cwd, home =
|
|
50486
|
+
function loadPreset(cwd, home = homedir36()) {
|
|
50418
50487
|
const globalPath = globalPresetPath(home);
|
|
50419
50488
|
const projectPath = projectPresetPath(cwd);
|
|
50420
50489
|
const globalRaw = readJson4(globalPath);
|
|
@@ -50425,9 +50494,9 @@ function loadPreset(cwd, home = homedir35()) {
|
|
|
50425
50494
|
return { preset: preset2, globalPath, projectPath, hasProjectLayer };
|
|
50426
50495
|
}
|
|
50427
50496
|
function writeJsonAtomic2(path41, value) {
|
|
50428
|
-
|
|
50497
|
+
mkdirSync43(dirname48(path41), { recursive: true });
|
|
50429
50498
|
const tmp = `${path41}.${String(process.pid)}.tmp`;
|
|
50430
|
-
|
|
50499
|
+
writeFileSync47(tmp, `${JSON.stringify(value, null, 2)}
|
|
50431
50500
|
`, "utf8");
|
|
50432
50501
|
renameSync15(tmp, path41);
|
|
50433
50502
|
}
|
|
@@ -50522,6 +50591,9 @@ function money(perMillion) {
|
|
|
50522
50591
|
const rounded = perMillion >= 10 ? perMillion.toFixed(0) : perMillion >= 1 ? perMillion.toFixed(1) : perMillion.toFixed(2);
|
|
50523
50592
|
return `$${rounded.replace(/\.0+$/u, "").replace(/(\.\d)0$/u, "$1")} / 1M`;
|
|
50524
50593
|
}
|
|
50594
|
+
function hasPricing(cost) {
|
|
50595
|
+
return cost !== void 0 && (cost.input > 0 || cost.cachedInput > 0 || cost.output > 0);
|
|
50596
|
+
}
|
|
50525
50597
|
function pad(text, width) {
|
|
50526
50598
|
const w = visibleWidth23(text);
|
|
50527
50599
|
return w >= width ? text : text + " ".repeat(width - w);
|
|
@@ -50552,7 +50624,7 @@ var ModelPicker = class {
|
|
|
50552
50624
|
this.onRenderRequest = options.onRenderRequest;
|
|
50553
50625
|
this.visibleRows = options.visibleRows ?? DEFAULT_VISIBLE_ROWS;
|
|
50554
50626
|
this.modelsByKey = new Map(options.state.models.map((m) => [m.key, m]));
|
|
50555
|
-
const prices = options.state.models.map((m) => m.cost ? blendedPrice(m.cost) : void 0).filter((p) => p !== void 0);
|
|
50627
|
+
const prices = options.state.models.map((m) => hasPricing(m.cost) ? blendedPrice(m.cost) : void 0).filter((p) => p !== void 0 && p > 0);
|
|
50556
50628
|
this.priceRange = { min: prices.length > 0 ? Math.min(...prices) : 0, max: prices.length > 0 ? Math.max(...prices) : 0 };
|
|
50557
50629
|
this.effort = { ...options.state.effort };
|
|
50558
50630
|
const active = options.state.active;
|
|
@@ -50802,29 +50874,34 @@ var ModelPicker = class {
|
|
|
50802
50874
|
const primaryKey = row.kind === "fusion" ? this.lead : row.key;
|
|
50803
50875
|
const primary = primaryKey === void 0 ? void 0 : this.modelsByKey.get(primaryKey);
|
|
50804
50876
|
const side = row.kind === "fusion" && this.sidekick !== void 0 ? this.modelsByKey.get(this.sidekick) : void 0;
|
|
50877
|
+
const primaryCost = primary?.cost;
|
|
50878
|
+
const sideCost = side?.cost;
|
|
50805
50879
|
const cols = [];
|
|
50806
|
-
if (
|
|
50807
|
-
cols.push(["Input", money(
|
|
50808
|
-
cols.push(["Cached input", money(
|
|
50809
|
-
cols.push(["Output", money(
|
|
50880
|
+
if (hasPricing(primaryCost)) {
|
|
50881
|
+
cols.push(["Input", money(primaryCost.input)]);
|
|
50882
|
+
cols.push(["Cached input", money(primaryCost.cachedInput)]);
|
|
50883
|
+
cols.push(["Output", money(primaryCost.output)]);
|
|
50810
50884
|
} else {
|
|
50811
50885
|
cols.push(["Input", "\u2014"], ["Cached input", "\u2014"], ["Output", "\u2014"]);
|
|
50812
50886
|
}
|
|
50813
50887
|
if (row.kind === "fusion") {
|
|
50814
|
-
if (
|
|
50815
|
-
cols.push(["Sidekick input", money(
|
|
50816
|
-
cols.push(["Sidekick cached input", money(
|
|
50817
|
-
cols.push(["Sidekick output", money(
|
|
50888
|
+
if (hasPricing(sideCost)) {
|
|
50889
|
+
cols.push(["Sidekick input", money(sideCost.input)]);
|
|
50890
|
+
cols.push(["Sidekick cached input", money(sideCost.cachedInput)]);
|
|
50891
|
+
cols.push(["Sidekick output", money(sideCost.output)]);
|
|
50818
50892
|
} else {
|
|
50819
50893
|
cols.push(["Sidekick input", "\u2014"], ["Sidekick cached input", "\u2014"], ["Sidekick output", "\u2014"]);
|
|
50820
50894
|
}
|
|
50821
50895
|
}
|
|
50822
|
-
const
|
|
50896
|
+
const need = Math.max(...cols.map(([h, v]) => Math.max(visibleWidth23(h), visibleWidth23(v)))) + 3;
|
|
50897
|
+
const colWidth = Math.max(10, Math.min(need, Math.floor((width - 4) / cols.length)));
|
|
50823
50898
|
const head = cols.map(([h]) => pad(t.fg("dim", h), colWidth)).join("");
|
|
50824
50899
|
const vals = cols.map(([, v]) => pad(t.fg("text", v), colWidth)).join("");
|
|
50825
50900
|
const desc = row.kind === "fusion" ? t.fg("dim", "Pairs frontier intelligence with cost-efficient execution") : primary?.reasoning ? t.fg("dim", "Reasoning model \xB7 \u2190/\u2192 adjusts thinking effort") : t.fg("dim", "Non-reasoning model \xB7 effort is ignored by the provider");
|
|
50826
50901
|
const badges = this.state.models.some((m) => m.badge !== void 0) ? `${t.fg("success", "\u2731")} ${t.fg("dim", "New")} ${t.fg("accent", "\u2731")} ${t.fg("dim", "Promotion")} ${t.fg("warning", "\u2731")} ${t.fg("dim", "Beta")} ${t.fg("dim", "\xB7")}` : "";
|
|
50827
|
-
const
|
|
50902
|
+
const noPricing = row.kind === "fusion" ? !hasPricing(primaryCost) || !hasPricing(sideCost) : !hasPricing(primaryCost);
|
|
50903
|
+
const pricing = noPricing ? t.fg("dim", " \xB7 no pricing data from provider") : "";
|
|
50904
|
+
const description = `${badges}${badges.length > 0 ? " " : ""}${desc}${pricing}`;
|
|
50828
50905
|
return [truncateToWidth26(` ${head}`, width - 1), truncateToWidth26(` ${vals}`, width - 1), truncateToWidth26(` ${description}`, width - 1)];
|
|
50829
50906
|
}
|
|
50830
50907
|
hintLine(row) {
|
|
@@ -50873,8 +50950,9 @@ var ModelPicker = class {
|
|
|
50873
50950
|
const sliderCells = Math.min(48, Math.max(1, width - 6));
|
|
50874
50951
|
const sliderKey = row?.kind === "fusion" ? this.lead : row?.key;
|
|
50875
50952
|
const sliderModel = sliderKey === void 0 ? void 0 : this.modelsByKey.get(sliderKey);
|
|
50876
|
-
const
|
|
50877
|
-
const
|
|
50953
|
+
const sliderCost = sliderModel?.cost;
|
|
50954
|
+
const sliderPrice = hasPricing(sliderCost) ? blendedPrice(sliderCost) : void 0;
|
|
50955
|
+
const marker = this.priceRange.max <= 0 || sliderPrice === void 0 ? void 0 : sliderPosition(sliderPrice, this.priceRange.min, this.priceRange.max, sliderCells);
|
|
50878
50956
|
lines.push(truncateToWidth26(` ${renderSlider(sliderCells, marker)}`, width - 1));
|
|
50879
50957
|
lines.push(...this.renderPricePanel(row, width));
|
|
50880
50958
|
lines.push("");
|
|
@@ -51013,9 +51091,9 @@ var PresetEditor = class {
|
|
|
51013
51091
|
|
|
51014
51092
|
// packages/fusion/src/sidekick-runtime.ts
|
|
51015
51093
|
import { spawn as defaultSpawn2 } from "node:child_process";
|
|
51016
|
-
import { mkdirSync as
|
|
51094
|
+
import { mkdirSync as mkdirSync44, unlinkSync as unlinkSync12, writeFileSync as writeFileSync48 } from "node:fs";
|
|
51017
51095
|
import { tmpdir as tmpdir7 } from "node:os";
|
|
51018
|
-
import { dirname as dirname49, join as
|
|
51096
|
+
import { dirname as dirname49, join as join75 } from "node:path";
|
|
51019
51097
|
import { randomUUID as randomUUID11 } from "node:crypto";
|
|
51020
51098
|
var emptyUsage = () => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
|
|
51021
51099
|
var SidekickRuntime = class {
|
|
@@ -51055,9 +51133,9 @@ var SidekickRuntime = class {
|
|
|
51055
51133
|
this.promptPath = void 0;
|
|
51056
51134
|
}
|
|
51057
51135
|
spawn() {
|
|
51058
|
-
|
|
51059
|
-
this.promptPath =
|
|
51060
|
-
|
|
51136
|
+
mkdirSync44(dirname49(this.cfg.sessionFile), { recursive: true });
|
|
51137
|
+
this.promptPath = join75(tmpdir7(), `unipi-fusion-${randomUUID11()}.txt`);
|
|
51138
|
+
writeFileSync48(this.promptPath, this.cfg.systemPrompt, "utf8");
|
|
51061
51139
|
const command = this.cfg.command ?? getPiSpawnCommand([
|
|
51062
51140
|
"--mode",
|
|
51063
51141
|
"rpc",
|
|
@@ -51476,7 +51554,7 @@ ${progressText(runtime, id)}`);
|
|
|
51476
51554
|
var MODEL_COMMAND = `${UNIPI_PREFIX}model`;
|
|
51477
51555
|
var PRESET_COMMAND = `${UNIPI_PREFIX}fusion-preset`;
|
|
51478
51556
|
function sidekickSessionPath(leadSessionId) {
|
|
51479
|
-
return
|
|
51557
|
+
return join76(homedir37(), ".unipi", "state", "fusion", "sidekick", `${leadSessionId ?? "default"}.jsonl`);
|
|
51480
51558
|
}
|
|
51481
51559
|
function registryOf(ctx) {
|
|
51482
51560
|
const r = ctx.modelRegistry;
|
|
@@ -51487,17 +51565,17 @@ function findModel(reg, key) {
|
|
|
51487
51565
|
if (modelBykey.size === 0 && reg) for (const m of reg.getAvailable()) modelBykey.set(modelKey(m), m);
|
|
51488
51566
|
return modelBykey.get(key);
|
|
51489
51567
|
}
|
|
51490
|
-
function costOf(m) {
|
|
51491
|
-
const cost = m?.cost;
|
|
51492
|
-
return cost &&
|
|
51568
|
+
function costOf(m, override) {
|
|
51569
|
+
const cost = override ?? (m?.cost && typeof m.cost.input === "number" ? { input: m.cost.input, cachedInput: m.cost.cacheRead ?? 0, output: m.cost.output } : void 0);
|
|
51570
|
+
return cost && (cost.input > 0 || cost.cachedInput > 0 || cost.output > 0) ? cost : void 0;
|
|
51493
51571
|
}
|
|
51494
|
-
function toPickerModel(m, badge) {
|
|
51572
|
+
function toPickerModel(m, badge, override) {
|
|
51495
51573
|
return {
|
|
51496
51574
|
key: modelKey(m),
|
|
51497
51575
|
name: m.name || m.id,
|
|
51498
51576
|
provider: m.provider,
|
|
51499
51577
|
badge,
|
|
51500
|
-
cost: costOf(m),
|
|
51578
|
+
cost: costOf(m, override),
|
|
51501
51579
|
reasoning: Boolean(m.reasoning)
|
|
51502
51580
|
};
|
|
51503
51581
|
}
|
|
@@ -51546,9 +51624,10 @@ function fusionExtension(pi) {
|
|
|
51546
51624
|
function statusSavings(ctx) {
|
|
51547
51625
|
if (active?.kind !== "fusion" || runtime === void 0) return void 0;
|
|
51548
51626
|
const reg = registryOf(ctx);
|
|
51627
|
+
const preset2 = loadPreset(ctx.cwd ?? process.cwd()).preset;
|
|
51549
51628
|
const lead = findModel(reg, active.lead);
|
|
51550
51629
|
const side = findModel(reg, active.sidekick);
|
|
51551
|
-
return estimateSavings(runtime.usage, costOf(lead), costOf(side)).savedUsd;
|
|
51630
|
+
return estimateSavings(runtime.usage, costOf(lead, preset2.prices[active.lead]), costOf(side, preset2.prices[active.sidekick])).savedUsd;
|
|
51552
51631
|
}
|
|
51553
51632
|
function publishStatus(ctx) {
|
|
51554
51633
|
const reg = registryOf(ctx);
|
|
@@ -51589,10 +51668,14 @@ function fusionExtension(pi) {
|
|
|
51589
51668
|
function savingsStats(ctx) {
|
|
51590
51669
|
if (active?.kind !== "fusion" || runtime === void 0) return "Fusion is not active \u2014 pick a Fusion pair with /unipi:model.";
|
|
51591
51670
|
const reg = registryOf(ctx);
|
|
51592
|
-
const
|
|
51671
|
+
const preset2 = loadPreset(ctx.cwd ?? process.cwd()).preset;
|
|
51672
|
+
const leadCost = costOf(findModel(reg, active.lead), preset2.prices[active.lead]);
|
|
51673
|
+
const sidekickCost = costOf(findModel(reg, active.sidekick), preset2.prices[active.sidekick]);
|
|
51674
|
+
const savings = estimateSavings(runtime.usage, leadCost, sidekickCost);
|
|
51675
|
+
const pricing = leadCost === void 0 && sidekickCost === void 0 ? '\nPricing unavailable from provider \u2014 set "prices" in ~/.unipi/config/fusion/preset.json to estimate savings.' : "";
|
|
51593
51676
|
return `Sidekick tokens: in ${String(runtime.usage.input)} \xB7 out ${String(runtime.usage.output)} \xB7 cached ${String(runtime.usage.cacheRead)} \xB7 cache write ${String(runtime.usage.cacheWrite)}
|
|
51594
51677
|
Sidekick cost: $${savings.sidekickUsd.toFixed(2)} \xB7 at lead prices: $${savings.atLeadUsd.toFixed(2)} \xB7 saved: $${savings.savedUsd.toFixed(2)}
|
|
51595
|
-
Handoffs: ${String(runtime.reports.size)} \xB7 runtime alive: ${String(runtime.isAlive())} \xB7 busy: ${String(runtime.isBusy())}`;
|
|
51678
|
+
Handoffs: ${String(runtime.reports.size)} \xB7 runtime alive: ${String(runtime.isAlive())} \xB7 busy: ${String(runtime.isBusy())}${pricing}`;
|
|
51596
51679
|
}
|
|
51597
51680
|
registerFusionTools(pi, {
|
|
51598
51681
|
getRuntime,
|
|
@@ -51664,7 +51747,7 @@ ${leadPolicy(identity(ctx))}` } : void 0);
|
|
|
51664
51747
|
const cwd = ctx.cwd ?? process.cwd();
|
|
51665
51748
|
const loaded = loadPreset(cwd);
|
|
51666
51749
|
const preset2 = loaded.preset;
|
|
51667
|
-
const models = reg.getAvailable().map((m) => toPickerModel(m, preset2.badges[modelKey(m)]));
|
|
51750
|
+
const models = reg.getAvailable().map((m) => toPickerModel(m, preset2.badges[modelKey(m)], preset2.prices[modelKey(m)]));
|
|
51668
51751
|
if (models.length === 0) {
|
|
51669
51752
|
ctx.ui.notify("No models available. Use /login to add a provider.", "warning");
|
|
51670
51753
|
return;
|
|
@@ -51739,12 +51822,25 @@ ${String(result2.curation.lead.length)} lead \xB7 ${String(result2.curation.side
|
|
|
51739
51822
|
);
|
|
51740
51823
|
}
|
|
51741
51824
|
});
|
|
51742
|
-
pi.on("session_start", (_e, ctx) => {
|
|
51825
|
+
pi.on("session_start", async (_e, ctx) => {
|
|
51743
51826
|
stopRuntime();
|
|
51744
51827
|
nudged = false;
|
|
51745
51828
|
modelBykey.clear();
|
|
51746
51829
|
active = loadPreset(ctx.cwd ?? process.cwd()).preset.active;
|
|
51747
|
-
if (active?.kind === "fusion" && ctx.model
|
|
51830
|
+
if (active?.kind === "fusion" && (!ctx.model || modelKey(ctx.model) !== active.lead)) {
|
|
51831
|
+
const leadKey = active.lead;
|
|
51832
|
+
const lead = findModel(registryOf(ctx), leadKey);
|
|
51833
|
+
const restored = lead !== void 0 && await pi.setModel(lead);
|
|
51834
|
+
if (restored) {
|
|
51835
|
+
try {
|
|
51836
|
+
pi.setThinkingLevel(active.leadEffort ?? "medium");
|
|
51837
|
+
} catch {
|
|
51838
|
+
}
|
|
51839
|
+
} else {
|
|
51840
|
+
active = void 0;
|
|
51841
|
+
if (ctx.hasUI) ctx.ui.notify(`Fusion lead ${leadKey} unavailable \u2014 Fusion off`, "warning");
|
|
51842
|
+
}
|
|
51843
|
+
}
|
|
51748
51844
|
publishStatus(ctx);
|
|
51749
51845
|
if (ctx.hasUI) ctx.ui.addAutocompleteProvider(createModelBoostProvider);
|
|
51750
51846
|
});
|
|
@@ -51756,6 +51852,8 @@ ${String(result2.curation.lead.length)} lead \xB7 ${String(result2.curation.side
|
|
|
51756
51852
|
if (active?.kind === "fusion" && modelKey(event.model) !== active.lead) {
|
|
51757
51853
|
stopRuntime();
|
|
51758
51854
|
active = { kind: "single", model: modelKey(event.model) };
|
|
51855
|
+
const loaded = loadPreset(ctx.cwd ?? process.cwd());
|
|
51856
|
+
saveRuntimeState(globalPresetPath(), { effort: loaded.preset.effort, recent: loaded.preset.recent, active });
|
|
51759
51857
|
publishStatus(ctx);
|
|
51760
51858
|
}
|
|
51761
51859
|
});
|
|
@@ -51919,9 +52017,9 @@ var PrefixIntegrityTracker = class {
|
|
|
51919
52017
|
};
|
|
51920
52018
|
|
|
51921
52019
|
// packages/trajectory/src/telemetry.ts
|
|
51922
|
-
import { appendFileSync, closeSync as closeSync4, mkdirSync as
|
|
51923
|
-
import { homedir as
|
|
51924
|
-
import { join as
|
|
52020
|
+
import { appendFileSync, closeSync as closeSync4, mkdirSync as mkdirSync45, openSync as openSync4, readSync as readSync3, statSync as statSync18 } from "node:fs";
|
|
52021
|
+
import { homedir as homedir38 } from "node:os";
|
|
52022
|
+
import { join as join77 } from "node:path";
|
|
51925
52023
|
var SECRET_KEY = /authorization|api[-_]?key|token|cookie|secret|password|credential/i;
|
|
51926
52024
|
var SECRET_VALUE = /^(?:bearer\s+|sk-[a-z0-9_-]{12,}|gh[pousr]_[a-z0-9]{12,}|AIza[a-z0-9_-]{20,})/i;
|
|
51927
52025
|
var MAX_STRING = 2e5;
|
|
@@ -51985,9 +52083,9 @@ function redactTelemetry(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
|
51985
52083
|
var TelemetrySidecar = class {
|
|
51986
52084
|
file;
|
|
51987
52085
|
state;
|
|
51988
|
-
constructor(sessionId, root =
|
|
51989
|
-
|
|
51990
|
-
this.file =
|
|
52086
|
+
constructor(sessionId, root = join77(homedir38(), ".unipi", "trajectory")) {
|
|
52087
|
+
mkdirSync45(root, { recursive: true, mode: 448 });
|
|
52088
|
+
this.file = join77(root, `${sessionId.replace(/[^a-zA-Z0-9._-]/g, "_")}.jsonl`);
|
|
51991
52089
|
this.state = stateFor(this.file);
|
|
51992
52090
|
}
|
|
51993
52091
|
append(event) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/updater",
|
|
3
|
-
"version": "2.17.
|
|
3
|
+
"version": "2.17.2",
|
|
4
4
|
"description": "Auto-updater, changelog browser, and readme browser for Unipi — checks npm registry, renders CHANGELOG.md and README.md files in TUI overlays",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -47,11 +47,14 @@ const SECTION_HEADER_RE = /^### (.+)$/;
|
|
|
47
47
|
*/
|
|
48
48
|
export function parseChangelog(filePath: string): ChangelogEntry[] {
|
|
49
49
|
if (!existsSync(filePath)) return [];
|
|
50
|
+
return parseChangelogContent(readFileSync(filePath, "utf-8"));
|
|
51
|
+
}
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
export function parseChangelogContent(content: string): ChangelogEntry[] {
|
|
54
|
+
const trimmed = content.trim();
|
|
55
|
+
if (!trimmed) return [];
|
|
53
56
|
|
|
54
|
-
const lines =
|
|
57
|
+
const lines = trimmed.split("\n");
|
|
55
58
|
const entries: ChangelogEntry[] = [];
|
|
56
59
|
|
|
57
60
|
let currentEntry: ChangelogEntry | null = null;
|
|
@@ -143,7 +146,7 @@ export function getNewerVersions(
|
|
|
143
146
|
const result: ChangelogEntry[] = [];
|
|
144
147
|
for (const entry of entries) {
|
|
145
148
|
if (entry.version === "Unreleased") {
|
|
146
|
-
result.push(entry);
|
|
149
|
+
if (Object.keys(entry.sections).length > 0) result.push(entry);
|
|
147
150
|
continue;
|
|
148
151
|
}
|
|
149
152
|
// Compare rather than test for equality. Stopping only on an exact match
|
|
@@ -23,6 +23,7 @@ import { loadConfig } from "./settings.js";
|
|
|
23
23
|
import { checkForUpdates } from "./checker.js";
|
|
24
24
|
import { isVersionSkipped } from "./cache.js";
|
|
25
25
|
import { renderUpdateOverlay } from "./tui/update-overlay.js";
|
|
26
|
+
import { loadUpdateChangelog } from "./remote-changelog.js";
|
|
26
27
|
|
|
27
28
|
/** Package version */
|
|
28
29
|
const VERSION = getPackageVersion(new URL("..", import.meta.url).pathname);
|
|
@@ -142,8 +143,9 @@ export default function updaterExtension(pi: ExtensionAPI): void {
|
|
|
142
143
|
|
|
143
144
|
// Show update overlay if UI is available
|
|
144
145
|
if (ctx.hasUI) {
|
|
146
|
+
const entries = await loadUpdateChangelog(result.currentVersion, result.latestVersion);
|
|
145
147
|
const updateResult = await ctx.ui.custom(
|
|
146
|
-
renderUpdateOverlay(result),
|
|
148
|
+
renderUpdateOverlay(result, entries),
|
|
147
149
|
{
|
|
148
150
|
overlay: true,
|
|
149
151
|
overlayOptions: {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { UPDATER_DIRS } from "@pi-unipi/core";
|
|
5
|
+
import type { ChangelogEntry } from "../types.js";
|
|
6
|
+
import { getNewerVersions, parseChangelogContent, parseChangelog, resolveChangelogPath } from "./changelog.js";
|
|
7
|
+
|
|
8
|
+
export const CHANGELOG_RAW_BASE = "https://raw.githubusercontent.com/Neuron-Mr-White/unipi";
|
|
9
|
+
|
|
10
|
+
export interface RemoteChangelogOptions {
|
|
11
|
+
fetchImpl?: typeof fetch;
|
|
12
|
+
cacheDir?: string;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cacheDirectory(opts?: RemoteChangelogOptions): string {
|
|
17
|
+
return (opts?.cacheDir ?? UPDATER_DIRS.CACHE).replace("~", homedir());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function fetchRemoteChangelog(version: string, opts: RemoteChangelogOptions = {}): Promise<string | null> {
|
|
21
|
+
const cacheDir = cacheDirectory(opts);
|
|
22
|
+
const cachePath = join(cacheDir, `changelog-${version}.md`);
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(cachePath)) return readFileSync(cachePath, "utf8");
|
|
25
|
+
} catch {
|
|
26
|
+
// Continue with the network request.
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
30
|
+
const signal = AbortSignal.timeout(opts.timeoutMs ?? 5000);
|
|
31
|
+
try {
|
|
32
|
+
let response = await fetchImpl(`${CHANGELOG_RAW_BASE}/v${version}/CHANGELOG.md`, { signal });
|
|
33
|
+
// The release tag is immutable, so its changelog is safe to cache forever.
|
|
34
|
+
// The `main` fallback (tag not pushed yet) is not: never cache it.
|
|
35
|
+
const cacheable = response.ok;
|
|
36
|
+
if (response.status === 404) {
|
|
37
|
+
response = await fetchImpl(`${CHANGELOG_RAW_BASE}/main/CHANGELOG.md`, { signal });
|
|
38
|
+
}
|
|
39
|
+
if (!response.ok) return null;
|
|
40
|
+
const content = await response.text();
|
|
41
|
+
if (cacheable) {
|
|
42
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
43
|
+
writeFileSync(cachePath, content, "utf8");
|
|
44
|
+
}
|
|
45
|
+
return content;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function loadUpdateChangelog(
|
|
52
|
+
currentVersion: string,
|
|
53
|
+
latestVersion: string,
|
|
54
|
+
opts?: RemoteChangelogOptions,
|
|
55
|
+
): Promise<ChangelogEntry[]> {
|
|
56
|
+
const remote = await fetchRemoteChangelog(latestVersion, opts);
|
|
57
|
+
const entries = remote === null ? [] : parseChangelogContent(remote);
|
|
58
|
+
const newer = remote === null ? [] : getNewerVersions(entries, currentVersion);
|
|
59
|
+
if (newer.length > 0) return newer;
|
|
60
|
+
return getNewerVersions(parseChangelog(resolveChangelogPath()), currentVersion);
|
|
61
|
+
}
|
|
@@ -40,7 +40,7 @@ interface UpdateState {
|
|
|
40
40
|
/**
|
|
41
41
|
* Render the update available overlay.
|
|
42
42
|
*/
|
|
43
|
-
export function renderUpdateOverlay(checkResult: UpdateCheckResult) {
|
|
43
|
+
export function renderUpdateOverlay(checkResult: UpdateCheckResult, providedNewerVersions?: ChangelogEntry[]) {
|
|
44
44
|
return (
|
|
45
45
|
tui: import("@earendil-works/pi-tui").TUI,
|
|
46
46
|
theme: Theme,
|
|
@@ -49,14 +49,16 @@ export function renderUpdateOverlay(checkResult: UpdateCheckResult) {
|
|
|
49
49
|
) => {
|
|
50
50
|
const config = loadConfig();
|
|
51
51
|
|
|
52
|
-
// Load changelog for newer versions
|
|
53
|
-
let newerVersions: ChangelogEntry[] = [];
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
// Load changelog for newer versions unless the caller already fetched it.
|
|
53
|
+
let newerVersions: ChangelogEntry[] = providedNewerVersions ?? [];
|
|
54
|
+
if (providedNewerVersions === undefined) {
|
|
55
|
+
const changelogPath = resolveChangelogPath();
|
|
56
|
+
try {
|
|
57
|
+
const entries = parseChangelog(changelogPath);
|
|
58
|
+
newerVersions = getNewerVersions(entries, checkResult.currentVersion);
|
|
59
|
+
} catch (_err) {
|
|
60
|
+
// No changelog
|
|
61
|
+
}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
// Build content lines from changelog using markdown renderer
|
|
@@ -74,7 +76,7 @@ export function renderUpdateOverlay(checkResult: UpdateCheckResult) {
|
|
|
74
76
|
contentLines.push("");
|
|
75
77
|
}
|
|
76
78
|
if (contentLines.length === 0) {
|
|
77
|
-
contentLines.push(` ${theme.fg("muted",
|
|
79
|
+
contentLines.push(` ${theme.fg("muted", `No changelog available for ${checkResult.latestVersion} (offline?).`)}`);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
const state: UpdateState = {
|