pi-lemonade-link 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -1
- package/index.ts +533 -0
- package/lemonade.example.json +23 -1
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -282,6 +282,7 @@ and the normal menu takes over once the first instance exists.
|
|
|
282
282
|
| **Model management** | List catalog (including not-yet-downloaded registry entries), load / unload, pull (download) with live progress + esc-to-cancel, install from Hugging Face (search → pick variant → install as `user.*`), delete, change context size (unload → reload with `ctx_size`, saved), toggle chat-only filter, refresh provider |
|
|
283
283
|
| **Transcription settings** | Default Whisper model, endpoint path, smoke-test a file |
|
|
284
284
|
| **Live server logs** | WebSocket stream of the server's log: full snapshot backlog (up to 5000 entries) then live entries as they happen; Esc to close |
|
|
285
|
+
| **Spec & version check** | The three drift tiers, interactive: docs-vs-upstream status (TUI-only), live per-instance server versions beside the verified build and the latest upstream release, re-check now, one-keystroke docs re-sync (re-downloads upstream docs into the extension's docs/ folder — the code is not updated), `specCheck` / `serverVersionCheck` / `upstreamVersionCheck` toggles |
|
|
285
286
|
|
|
286
287
|
Notes on the destructive and slow paths:
|
|
287
288
|
|
|
@@ -387,7 +388,12 @@ The full shape (values shown are the built-in defaults):
|
|
|
387
388
|
"generationTimeoutMs": 600000,
|
|
388
389
|
"statusPollMs": 2000,
|
|
389
390
|
"statusBar": true,
|
|
390
|
-
"barPollMs": 5000
|
|
391
|
+
"barPollMs": 5000,
|
|
392
|
+
"specCheck": true,
|
|
393
|
+
"specCheckCooldownMs": 86400000,
|
|
394
|
+
"specCheckTimeoutMs": 5000,
|
|
395
|
+
"serverVersionCheck": true,
|
|
396
|
+
"upstreamVersionCheck": true
|
|
391
397
|
}
|
|
392
398
|
```
|
|
393
399
|
|
|
@@ -403,6 +409,9 @@ The full shape (values shown are the built-in defaults):
|
|
|
403
409
|
| `statusPollMs` | How often the setup-menu status panel re-fetches while open (live refresh). `0` = static snapshot. |
|
|
404
410
|
| `statusBar` | Show the below-editor lemonade status bar while a lemonade model is active (see *Lemonade status bar*). |
|
|
405
411
|
| `barPollMs` | Server-poll cadence for the status bar's busy/queue + CPU/GPU/NPU/VRAM segments. `0` = local metrics only (instance, tok/s, cache-hit %). |
|
|
412
|
+
| `specCheck` / `specCheckCooldownMs` / `specCheckTimeoutMs` | Docs-freshness check: the `docs/` snapshot is compared against upstream `lemonade-sdk/lemonade` by git blob SHA — **TUI-only status, no startup warning** (extension ↔ server agreement is what matters; see *Keeping the specs honest*); the verdict is durable for `specCheckCooldownMs` (default 24 h, also the upstream-release probe's throttle). |
|
|
413
|
+
| `serverVersionCheck` | **Tier 1, startup warning**: `/v1/health` self-reports the server version; any instance reporting a version other than the build this extension was verified against (`v11.7.0` — a constant in the extension code, not a setting) warns **with direction**: server newer → check for an extension update; server older → update the server. |
|
|
414
|
+
| `upstreamVersionCheck` | **Tier 2, info notice**: servers older than the latest published lemonade release (probed via GitHub, throttled) get a one-line "a server update is available" notice. Default on; separately toggleable. |
|
|
406
415
|
|
|
407
416
|
## Offline & unreachable networks
|
|
408
417
|
|
|
@@ -533,6 +542,67 @@ git clone --depth 1 --filter=blob:none --sparse https://github.com/lemonade-sdk/
|
|
|
533
542
|
&& cp docs/api/*.md ~/.pi/agent/extensions/pi-lemonade-link/docs/
|
|
534
543
|
```
|
|
535
544
|
|
|
545
|
+
### Keeping the specs honest
|
|
546
|
+
|
|
547
|
+
Drift is watched **in tiers, ordered by what actually matters at runtime**:
|
|
548
|
+
|
|
549
|
+
1. **Extension ↔ server sync (startup warning).** Every registration and
|
|
550
|
+
refresh compares each instance's live `/v1/health` version against the
|
|
551
|
+
build this extension was verified against (`v11.7.0` — a constant in the
|
|
552
|
+
extension code, **not a user setting**: it is a property of the code,
|
|
553
|
+
updated with each extension release). A mismatch is the one signal that
|
|
554
|
+
means *endpoints may have changed for this pairing*, so it warns —
|
|
555
|
+
**directionally**: server *newer* → "check for an extension update";
|
|
556
|
+
server *older* → "update the server, or expect some endpoints to be
|
|
557
|
+
missing".
|
|
558
|
+
2. **Server ↔ latest published release (info notice, one sentence, on by
|
|
559
|
+
default, separately toggleable).** The latest published lemonade release
|
|
560
|
+
is probed via GitHub (throttled, durable, silent on network failure); any
|
|
561
|
+
server older than it gets a single info line — *"instance 'x' runs
|
|
562
|
+
lemonade v11.7.0; the latest published release is v11.9.0 — a server
|
|
563
|
+
update is available."* Much lower tier: an older server isn't broken,
|
|
564
|
+
it's just behind. `upstreamVersionCheck: false` disables.
|
|
565
|
+
3. **Docs drift — TUI-only status, deliberately no startup warning.** The
|
|
566
|
+
`docs/` snapshot is the reference for the version the extension targets;
|
|
567
|
+
as long as extension and server agree on version, whether upstream HEAD
|
|
568
|
+
has moved on changes **nothing at runtime** — so it's shown only in
|
|
569
|
+
`/lemonade-setup` → Spec & version check (per-file status, re-check now,
|
|
570
|
+
one-keystroke re-sync that re-downloads the current upstream docs into
|
|
571
|
+
the extension's `docs/` folder). `specCheck: false` disables even the
|
|
572
|
+
background refresh.
|
|
573
|
+
|
|
574
|
+
All tiers share the family mechanics: fire-and-forget, never delays
|
|
575
|
+
startup, durable verdicts (at most one network pass per 24 h across
|
|
576
|
+
restarts), silent on network failure (unknown ≠ out of date). The Spec &
|
|
577
|
+
version check view manages every toggle interactively and shows live
|
|
578
|
+
per-instance versions beside the verified build and the latest upstream
|
|
579
|
+
release.
|
|
580
|
+
|
|
581
|
+
## Extension version & provider API
|
|
582
|
+
|
|
583
|
+
This extension's version (`version` in `package.json`) and the lemonade
|
|
584
|
+
server build it was verified against (`11.7.0`) are recorded together in
|
|
585
|
+
the `provider` block of `package.json` and in the
|
|
586
|
+
`EXTENSION_CONFIRMED_SERVER_VERSION` constant in `index.ts` (which drives
|
|
587
|
+
the startup version-drift warning). The server status panel and the
|
|
588
|
+
Spec & version check view both show the extension version, the live server
|
|
589
|
+
version(s), and the verified build.
|
|
590
|
+
|
|
591
|
+
**On update, keep these in lockstep** (all record the same fact):
|
|
592
|
+
|
|
593
|
+
1. `version` in `package.json` — the extension release
|
|
594
|
+
2. the `provider` block in `package.json` (`apiVerifiedAgainst`,
|
|
595
|
+
`specSnapshot`) — what that release was verified against
|
|
596
|
+
3. `EXTENSION_CONFIRMED_SERVER_VERSION` in `index.ts` — the peg the
|
|
597
|
+
version-drift warning compares your servers against
|
|
598
|
+
4. re-sync `docs/` in the extension folder from upstream (the README
|
|
599
|
+
re-sync command, or `/lemonade-setup` → Spec & version check)
|
|
600
|
+
5. the behavior-reference dates below
|
|
601
|
+
|
|
602
|
+
A release that skips any of these is lying somewhere — the spec-freshness
|
|
603
|
+
and server-version checks (see *Keeping the specs honest*) are what catch
|
|
604
|
+
#3 and #4 drifting silently.
|
|
605
|
+
|
|
536
606
|
## Lemonade behavior reference
|
|
537
607
|
|
|
538
608
|
Empirically confirmed against lemonade 11.7.0 (cross-checked with the
|
package/index.ts
CHANGED
|
@@ -46,6 +46,8 @@ import * as os from "node:os";
|
|
|
46
46
|
import * as path from "node:path";
|
|
47
47
|
import dgram from "node:dgram";
|
|
48
48
|
import { spawn } from "node:child_process";
|
|
49
|
+
import { createHash } from "node:crypto";
|
|
50
|
+
import { fileURLToPath } from "node:url";
|
|
49
51
|
import { Type } from "typebox";
|
|
50
52
|
import { DynamicBorder, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
51
53
|
import { Container, Key, matchesKey, SelectList, Text, truncateToWidth, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
@@ -89,6 +91,22 @@ interface LemonadeConfig {
|
|
|
89
91
|
statusPollMs: number; // status-panel live-refresh interval while viewing
|
|
90
92
|
statusBar: boolean; // belowEditor bar shown while a lemonade model is active
|
|
91
93
|
barPollMs: number; // bar server-poll cadence; 0 = local metrics only (tok/s, cache, instance)
|
|
94
|
+
// Spec-freshness: compare the docs/ snapshot against upstream
|
|
95
|
+
// lemonade-sdk/lemonade by git blob SHA. TUI-only status (Spec & version
|
|
96
|
+
// check view + re-sync actions) — no startup warning: as long as the
|
|
97
|
+
// extension and the server agree on version, docs matching upstream HEAD
|
|
98
|
+
// changes nothing at runtime.
|
|
99
|
+
specCheck: boolean;
|
|
100
|
+
specCheckCooldownMs: number; // durable verdicts re-checked at most this often
|
|
101
|
+
specCheckTimeoutMs: number;
|
|
102
|
+
// Live-server drift: /v1/health self-reports the server version; a version
|
|
103
|
+
// other than the build this extension was verified against (a constant in
|
|
104
|
+
// the code, not config) warns at startup, with direction.
|
|
105
|
+
serverVersionCheck: boolean;
|
|
106
|
+
// Low-tier notice: compare each server's live version against the latest
|
|
107
|
+
// published lemonade release (GitHub releases); server older → one-line
|
|
108
|
+
// info notice. Separately toggleable; default on.
|
|
109
|
+
upstreamVersionCheck: boolean;
|
|
92
110
|
discoveryTimeoutMs: number;
|
|
93
111
|
beaconTimeoutMs: number;
|
|
94
112
|
loadTimeoutMs: number;
|
|
@@ -145,6 +163,11 @@ const DEFAULT_CONFIG: LemonadeConfig = {
|
|
|
145
163
|
statusPollMs: 2000,
|
|
146
164
|
statusBar: true,
|
|
147
165
|
barPollMs: 5000,
|
|
166
|
+
specCheck: true,
|
|
167
|
+
specCheckCooldownMs: 24 * 60 * 60 * 1000,
|
|
168
|
+
specCheckTimeoutMs: 5000,
|
|
169
|
+
serverVersionCheck: true,
|
|
170
|
+
upstreamVersionCheck: true,
|
|
148
171
|
discoveryTimeoutMs: 5000,
|
|
149
172
|
beaconTimeoutMs: 3000,
|
|
150
173
|
loadTimeoutMs: 300_000,
|
|
@@ -656,6 +679,362 @@ async function discoverViaHttp(config: LemonadeConfig): Promise<DiscoveredServer
|
|
|
656
679
|
return [...seen.values()];
|
|
657
680
|
}
|
|
658
681
|
|
|
682
|
+
// ─── Spec freshness & server-version drift ─────────────────────────────────
|
|
683
|
+
|
|
684
|
+
/** The two drift signals this extension watches, so "ground truth snapshotted"
|
|
685
|
+
* stays honest:
|
|
686
|
+
*
|
|
687
|
+
* 1. Docs drift — the docs/ folder is a snapshot of lemonade-sdk/lemonade's
|
|
688
|
+
* docs/api (see the README re-sync command). Each file's git blob SHA is
|
|
689
|
+
* computed locally at runtime (nothing stored, so re-syncing docs/
|
|
690
|
+
* re-arms the comparison) and matched against the same file upstream via
|
|
691
|
+
* GitHub's contents API (a tiny JSON response; the markdown is never
|
|
692
|
+
* downloaded). Drift → TUI-only status (the Spec & version check
|
|
693
|
+
* view); deliberately NO startup warning — extension ↔ server version
|
|
694
|
+
* agreement is what matters at runtime.
|
|
695
|
+
*
|
|
696
|
+
* 2. Live-server drift — an on-prem box publishes no spec to diff, but it
|
|
697
|
+
* self-reports its version in /v1/health. A version this extension's
|
|
698
|
+
* behavior reference was never confirmed against is the honest signal
|
|
699
|
+
* that endpoints may have changed; each such instance is warned on
|
|
700
|
+
* session_start (mismatches are (re)collected on every registration and
|
|
701
|
+
* refresh, so fixing the server or the config clears the warning).
|
|
702
|
+
*
|
|
703
|
+
* Both checks are fire-and-forget, never delay startup, and are silent on
|
|
704
|
+
* network failure — a hygiene check must never cry wolf on offline
|
|
705
|
+
* startups: unknown ≠ out of date. */
|
|
706
|
+
|
|
707
|
+
const DOCS_FILES = ["README.md", "anthropic.md", "lemonade.md", "llamacpp.md", "mcp.md", "ollama.md", "openai.md"];
|
|
708
|
+
const DOCS_UPSTREAM_BASE = "https://github.com/lemonade-sdk/lemonade/blob/main/docs/api/";
|
|
709
|
+
const DOCS_GH_CONTENTS = (file: string) =>
|
|
710
|
+
`https://api.github.com/repos/lemonade-sdk/lemonade/contents/docs/api/${file}`;
|
|
711
|
+
const DOCS_RAW = (file: string) => `https://raw.githubusercontent.com/lemonade-sdk/lemonade/main/docs/api/${file}`;
|
|
712
|
+
|
|
713
|
+
/** Latest published (non-prerelease) lemonade release — the low-tier "a
|
|
714
|
+
* server update is available" notice compares each server's live version
|
|
715
|
+
* against this. Throttled by the same durable-verdict discipline. */
|
|
716
|
+
const GH_LATEST_RELEASE = "https://api.github.com/repos/lemonade-sdk/lemonade/releases/latest";
|
|
717
|
+
|
|
718
|
+
interface UpstreamVersionSnapshot {
|
|
719
|
+
checkedAt: number;
|
|
720
|
+
latest: string;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function upstreamSnapshotPath(): string {
|
|
724
|
+
return path.join(getAgentDir(), "cache", "pi-lemonade-link", "upstream-version.json");
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function loadUpstreamSnapshot(): UpstreamVersionSnapshot | undefined {
|
|
728
|
+
try {
|
|
729
|
+
const raw = JSON.parse(fs.readFileSync(upstreamSnapshotPath(), "utf8")) as UpstreamVersionSnapshot;
|
|
730
|
+
return typeof raw.checkedAt === "number" && typeof raw.latest === "string" ? raw : undefined;
|
|
731
|
+
} catch {
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function saveUpstreamSnapshot(snap: UpstreamVersionSnapshot): void {
|
|
737
|
+
try {
|
|
738
|
+
fs.mkdirSync(path.dirname(upstreamSnapshotPath()), { recursive: true });
|
|
739
|
+
fs.writeFileSync(upstreamSnapshotPath(), JSON.stringify(snap, null, 2) + "\n");
|
|
740
|
+
} catch {
|
|
741
|
+
/* cache dir unwritable → re-probe next time */
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** The latest published lemonade version, or undefined when unreachable
|
|
746
|
+
* (unknown ≠ outdated — never cry wolf). */
|
|
747
|
+
async function latestUpstreamVersion(config: LemonadeConfig): Promise<string | undefined> {
|
|
748
|
+
const fresh = loadUpstreamSnapshot();
|
|
749
|
+
if (!fresh || Date.now() - fresh.checkedAt >= Math.max(0, config.specCheckCooldownMs)) {
|
|
750
|
+
try {
|
|
751
|
+
const res = await fetch(GH_LATEST_RELEASE, {
|
|
752
|
+
headers: { Accept: "application/vnd.github+json", "User-Agent": "pi-lemonade-link" },
|
|
753
|
+
signal: AbortSignal.timeout(config.specCheckTimeoutMs),
|
|
754
|
+
});
|
|
755
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
756
|
+
const tag = ((await res.json()) as { tag_name?: string }).tag_name;
|
|
757
|
+
if (typeof tag !== "string" || !tag) throw new Error("release carried no tag_name");
|
|
758
|
+
saveUpstreamSnapshot({ checkedAt: Date.now(), latest: tag.replace(/^v/, "") });
|
|
759
|
+
} catch (error) {
|
|
760
|
+
console.error(
|
|
761
|
+
`[pi-lemonade-link] upstream-version probe skipped: ${error instanceof Error ? error.message : error}`
|
|
762
|
+
);
|
|
763
|
+
// A failed probe must still engage the cooldown ("at most one network
|
|
764
|
+
// pass per cooldown") — otherwise offline days re-probe at every
|
|
765
|
+
// session start. Keep the last known latest; a failed probe proves
|
|
766
|
+
// nothing new (unknown ≠ outdated).
|
|
767
|
+
saveUpstreamSnapshot({ checkedAt: Date.now(), latest: fresh?.latest ?? "" });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return loadUpstreamSnapshot()?.latest;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Git blob SHA of a file's bytes: sha1("blob <size>\0" + content). Pure. */
|
|
774
|
+
function gitBlobSha(data: Buffer): string {
|
|
775
|
+
return createHash("sha1").update(`blob ${data.length}\0`).update(data).digest("hex");
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Absolute path of a snapshotted doc, inside this extension's folder. */
|
|
779
|
+
function extensionDocsPath(file: string): string {
|
|
780
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "docs", file);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
interface DocsSpecSnapshot {
|
|
784
|
+
checkedAt: number; // epoch ms of the last real check
|
|
785
|
+
changed: string[]; // docs/ files whose upstream blob SHA differs (sorted)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function docsSnapshotPath(): string {
|
|
789
|
+
return path.join(getAgentDir(), "cache", "pi-lemonade-link", "spec-check.json");
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function loadDocsSnapshot(): DocsSpecSnapshot | undefined {
|
|
793
|
+
try {
|
|
794
|
+
const raw = JSON.parse(fs.readFileSync(docsSnapshotPath(), "utf8")) as DocsSpecSnapshot;
|
|
795
|
+
return typeof raw.checkedAt === "number" && Array.isArray(raw.changed) ? raw : undefined;
|
|
796
|
+
} catch {
|
|
797
|
+
return undefined;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function saveDocsSnapshot(snap: DocsSpecSnapshot): void {
|
|
802
|
+
try {
|
|
803
|
+
fs.mkdirSync(path.dirname(docsSnapshotPath()), { recursive: true });
|
|
804
|
+
fs.writeFileSync(docsSnapshotPath(), JSON.stringify(snap, null, 2) + "\n");
|
|
805
|
+
} catch {
|
|
806
|
+
/* cache dir unwritable — the check still runs, just re-checks next time */
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** The lemonade server version THIS BUILD of the extension was verified
|
|
811
|
+
* against (the README behavior reference). A constant in the code, not
|
|
812
|
+
* config: it is a property of the build — updated with each extension
|
|
813
|
+
* release after re-verification — never a user preference to move. The
|
|
814
|
+
* server's own version is probed live from /v1/health; this is the peg. */
|
|
815
|
+
const EXTENSION_CONFIRMED_SERVER_VERSION = "11.7.0";
|
|
816
|
+
|
|
817
|
+
/** This extension's own version, from package.json (kept beside the
|
|
818
|
+
* provider.apiVerifiedAgainst record — see the README versioning
|
|
819
|
+
* section; both bump together on release). */
|
|
820
|
+
const PKG_VERSION = (() => {
|
|
821
|
+
try {
|
|
822
|
+
return (
|
|
823
|
+
(JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "package.json"), "utf8")) as {
|
|
824
|
+
version?: string;
|
|
825
|
+
}).version ?? "0.0.0"
|
|
826
|
+
);
|
|
827
|
+
} catch {
|
|
828
|
+
return "0.0.0";
|
|
829
|
+
}
|
|
830
|
+
})();
|
|
831
|
+
|
|
832
|
+
/** Compare dotted numeric versions: >0 if a is newer than b, <0 if older,
|
|
833
|
+
* null when either side isn't dotted-numeric (no direction, just "differs"). Pure. */
|
|
834
|
+
function versionCompare(a: string, b: string): number | null {
|
|
835
|
+
const pa = a.split(".").map(Number);
|
|
836
|
+
const pb = b.split(".").map(Number);
|
|
837
|
+
if (pa.some(Number.isNaN) || pb.some(Number.isNaN)) return null;
|
|
838
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
839
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
840
|
+
if (d) return d;
|
|
841
|
+
}
|
|
842
|
+
return 0;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** Server-version drift collected during provider registration/refresh
|
|
846
|
+
* (registerInstanceProvider records; registerLemonadeProvider resets).
|
|
847
|
+
* direction: "ahead" = server newer than the extension was verified for
|
|
848
|
+
* (fix: extension update); "behind" = older (fix: update the server). */
|
|
849
|
+
interface ServerVersionMismatch {
|
|
850
|
+
instance: string;
|
|
851
|
+
server: string;
|
|
852
|
+
confirmed: string;
|
|
853
|
+
direction: "ahead" | "behind" | "unknown";
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const serverVersionMismatches: ServerVersionMismatch[] = [];
|
|
857
|
+
|
|
858
|
+
/** Live server versions seen at the last registration/refresh (instance →
|
|
859
|
+
* version), reused by the low-tier upstream notice without extra probes. */
|
|
860
|
+
const serverVersionsSeen = new Map<string, string>();
|
|
861
|
+
|
|
862
|
+
function recordServerVersion(config: LemonadeConfig, instance: string, version: string | undefined): void {
|
|
863
|
+
const i = serverVersionMismatches.findIndex((m) => m.instance === instance);
|
|
864
|
+
const cmp = version ? versionCompare(version, EXTENSION_CONFIRMED_SERVER_VERSION) : null;
|
|
865
|
+
const mismatched = config.serverVersionCheck && !!version && version !== EXTENSION_CONFIRMED_SERVER_VERSION;
|
|
866
|
+
if (version) serverVersionsSeen.set(instance, version);
|
|
867
|
+
else serverVersionsSeen.delete(instance);
|
|
868
|
+
if (mismatched && version) {
|
|
869
|
+
const entry: ServerVersionMismatch = {
|
|
870
|
+
instance,
|
|
871
|
+
server: version,
|
|
872
|
+
confirmed: EXTENSION_CONFIRMED_SERVER_VERSION,
|
|
873
|
+
direction: cmp !== null ? (cmp > 0 ? "ahead" : "behind") : "unknown",
|
|
874
|
+
};
|
|
875
|
+
if (i >= 0) serverVersionMismatches[i] = entry;
|
|
876
|
+
else serverVersionMismatches.push(entry);
|
|
877
|
+
} else if (i >= 0) {
|
|
878
|
+
serverVersionMismatches.splice(i, 1); // back on the verified version — clear the warning
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** One live docs comparison: per-file local blob SHA vs upstream blob SHA. Network. */
|
|
883
|
+
async function performDocsLiveCheck(
|
|
884
|
+
timeoutMs: number
|
|
885
|
+
): Promise<{ verdict: "current" | "changed"; changed: string[] } | { verdict: "unknown"; error: string }> {
|
|
886
|
+
const changed: string[] = [];
|
|
887
|
+
let reached = false;
|
|
888
|
+
let lastError = "GitHub unreachable";
|
|
889
|
+
await Promise.all(
|
|
890
|
+
DOCS_FILES.map(async (file) => {
|
|
891
|
+
try {
|
|
892
|
+
const localSha = gitBlobSha(fs.readFileSync(extensionDocsPath(file)));
|
|
893
|
+
const res = await fetch(DOCS_GH_CONTENTS(file), {
|
|
894
|
+
headers: { Accept: "application/vnd.github+json", "User-Agent": "pi-lemonade-link" },
|
|
895
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
896
|
+
});
|
|
897
|
+
if (!res.ok) {
|
|
898
|
+
lastError = `GitHub API HTTP ${res.status} (${file})`;
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
reached = true;
|
|
902
|
+
const sha = ((await res.json()) as { sha?: string }).sha;
|
|
903
|
+
if (typeof sha === "string" && sha && sha !== localSha) changed.push(file);
|
|
904
|
+
} catch (error) {
|
|
905
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
906
|
+
/* one file unreachable → not drift; the reached-gate below stays */
|
|
907
|
+
}
|
|
908
|
+
})
|
|
909
|
+
);
|
|
910
|
+
if (!reached) return { verdict: "unknown", error: lastError };
|
|
911
|
+
return changed.length ? { verdict: "changed", changed } : { verdict: "current", changed: [] };
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Re-sync the docs/ snapshot from upstream: download the current markdown
|
|
915
|
+
* into docs/ and record a "current" verdict (true by construction). This
|
|
916
|
+
* refreshes the GROUND TRUTH ONLY — the extension code itself is unchanged
|
|
917
|
+
* and may still need an update; the caller's messaging must say so. */
|
|
918
|
+
async function resyncLemonadeDocs(timeoutMs: number): Promise<{ ok: boolean; error?: string }> {
|
|
919
|
+
try {
|
|
920
|
+
await Promise.all(
|
|
921
|
+
DOCS_FILES.map(async (file) => {
|
|
922
|
+
const res = await fetch(DOCS_RAW(file), { signal: AbortSignal.timeout(timeoutMs) });
|
|
923
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} (${file})`);
|
|
924
|
+
const body = Buffer.from(await res.arrayBuffer());
|
|
925
|
+
fs.mkdirSync(path.dirname(extensionDocsPath(file)), { recursive: true });
|
|
926
|
+
fs.writeFileSync(extensionDocsPath(file), body);
|
|
927
|
+
})
|
|
928
|
+
);
|
|
929
|
+
saveDocsSnapshot({ checkedAt: Date.now(), changed: [] });
|
|
930
|
+
return { ok: true };
|
|
931
|
+
} catch (error) {
|
|
932
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** One throttled docs-freshness pass: a fresh durable verdict serves without
|
|
937
|
+
* a network call; a stale/absent one triggers the bounded GitHub probe.
|
|
938
|
+
* Drift → the caller-supplied channel. The startup caller passes a no-op
|
|
939
|
+
* (docs drift is TUI-only status — see registerSpecDriftWarnings); the
|
|
940
|
+
* throttled check still runs so the TUI's Spec & version check view has a
|
|
941
|
+
* fresh verdict. */
|
|
942
|
+
async function runDocsSpecCheck(config: LemonadeConfig, notify: (message: string) => void): Promise<void> {
|
|
943
|
+
if (!config.specCheck) return;
|
|
944
|
+
let snapshot = loadDocsSnapshot();
|
|
945
|
+
if (!snapshot || Date.now() - snapshot.checkedAt >= Math.max(0, config.specCheckCooldownMs)) {
|
|
946
|
+
const result = await performDocsLiveCheck(config.specCheckTimeoutMs);
|
|
947
|
+
if (result.verdict === "unknown") {
|
|
948
|
+
// GitHub unreachable: unknown ≠ out of date — never cry wolf. But the
|
|
949
|
+
// failed pass still engages the cooldown ("at most one network pass
|
|
950
|
+
// per cooldown") — otherwise offline days re-probe at every session
|
|
951
|
+
// start — and the last known verdict stands until it expires.
|
|
952
|
+
console.error(`[pi-lemonade-link] spec check skipped: ${result.error}`);
|
|
953
|
+
saveDocsSnapshot({ checkedAt: Date.now(), changed: snapshot?.changed ?? [] });
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
snapshot = result.verdict === "changed" ? { checkedAt: Date.now(), changed: result.changed } : { checkedAt: Date.now(), changed: [] };
|
|
957
|
+
saveDocsSnapshot(snapshot);
|
|
958
|
+
}
|
|
959
|
+
if (snapshot.changed.length) {
|
|
960
|
+
notify(
|
|
961
|
+
`pi-lemonade-link: ${snapshot.changed.length} Lemonade API doc${snapshot.changed.length > 1 ? "s" : ""} this extension keeps in its docs/ folder changed upstream ` +
|
|
962
|
+
`(${snapshot.changed.join(", ")}) — the extension may be out of date. ` +
|
|
963
|
+
`Fix: check for an extension update, or open /lemonade-setup → Spec & version check to re-download the current docs into the extension's ` +
|
|
964
|
+
`docs/ folder and compare what changed. Runs once a day; specCheck: false disables.`
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** Session-start drift signals, in tiers — ordered by what actually matters
|
|
970
|
+
* at runtime. Registered once from the extension entry.
|
|
971
|
+
*
|
|
972
|
+
* Tier 1 (warning): extension ↔ server sync — the signal that matters; a
|
|
973
|
+
* version mismatch means endpoints may have changed for this pairing.
|
|
974
|
+
* Directional (see ServerVersionMismatch).
|
|
975
|
+
*
|
|
976
|
+
* Tier 2 (info, one sentence, separately toggleable): server ↔ latest
|
|
977
|
+
* published upstream release — "a server update is available". Much lower
|
|
978
|
+
* tier: an older server is not broken, it's just behind.
|
|
979
|
+
*
|
|
980
|
+
* Docs drift: TUI-only status — as long as extension and server agree on
|
|
981
|
+
* version, the docs snapshot matching upstream HEAD changes nothing at
|
|
982
|
+
* runtime. The throttled check still runs so the Spec & version check view
|
|
983
|
+
* has a fresh verdict; it just never warns. */
|
|
984
|
+
function registerSpecDriftWarnings(pi: ExtensionAPI, configRef: { current: LemonadeConfig }): void {
|
|
985
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
986
|
+
if (!ctx.hasUI) return;
|
|
987
|
+
const config = configRef.current;
|
|
988
|
+
|
|
989
|
+
// Tier 1 — extension ↔ server sync.
|
|
990
|
+
for (const m of serverVersionMismatches) {
|
|
991
|
+
const relation =
|
|
992
|
+
m.direction === "ahead" ? "newer than" : m.direction === "behind" ? "older than" : "different from";
|
|
993
|
+
const fix =
|
|
994
|
+
m.direction === "ahead"
|
|
995
|
+
? "check for an extension update — until then, endpoints on that box may differ"
|
|
996
|
+
: m.direction === "behind"
|
|
997
|
+
? `update the lemonade server to v${m.confirmed} or newer, or expect some endpoints to be missing on that box`
|
|
998
|
+
: "update the server or the extension so both are on the verified pairing";
|
|
999
|
+
ctx.ui.notify(
|
|
1000
|
+
`pi-lemonade-link: instance "${m.instance}" runs server v${m.server} (${relation} the v${m.confirmed} this extension was verified against) — ${fix}. ` +
|
|
1001
|
+
`Details: /lemonade-setup → Spec & version check. serverVersionCheck: false disables.`,
|
|
1002
|
+
"warning"
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Tier 2 — server vs the latest published upstream release.
|
|
1007
|
+
// Fire-and-forget by design: pi's runner awaits session_start handlers
|
|
1008
|
+
// (and every UI mode awaits that emit), so awaiting the probe here would
|
|
1009
|
+
// stall startup/reload by up to ~specCheckTimeoutMs whenever the durable
|
|
1010
|
+
// verdict is stale. The notice simply arrives when the probe lands.
|
|
1011
|
+
if (config.upstreamVersionCheck) {
|
|
1012
|
+
void latestUpstreamVersion(config)
|
|
1013
|
+
.then((latest) => {
|
|
1014
|
+
if (!latest) return;
|
|
1015
|
+
for (const [instance, serverVersion] of serverVersionsSeen) {
|
|
1016
|
+
const cmp = versionCompare(serverVersion, latest);
|
|
1017
|
+
if (cmp !== null && cmp < 0) {
|
|
1018
|
+
ctx.ui.notify(
|
|
1019
|
+
`pi-lemonade-link: instance "${instance}" runs lemonade v${serverVersion}; the latest published release is v${latest} — a server update is available.`,
|
|
1020
|
+
"info"
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
})
|
|
1025
|
+
.catch((error) =>
|
|
1026
|
+
console.error(`[pi-lemonade-link] upstream notice skipped: ${error instanceof Error ? error.message : error}`)
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Docs drift — TUI-only: keep the verdict fresh, never warn. Same
|
|
1031
|
+
// fire-and-forget discipline as Tier 2.
|
|
1032
|
+
if (config.specCheck) {
|
|
1033
|
+
void runDocsSpecCheck(config, () => {});
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
|
|
659
1038
|
// ─── Provider registration ──────────────────────────────────────────────────
|
|
660
1039
|
|
|
661
1040
|
function isChatModel(m: LemonadeModel): boolean {
|
|
@@ -701,6 +1080,7 @@ async function registerInstanceProvider(pi: ExtensionAPI, view: InstanceView): P
|
|
|
701
1080
|
]);
|
|
702
1081
|
catalog = catalogRes;
|
|
703
1082
|
loaded = new Set((health?.all_models_loaded ?? []).map((m) => m.model_name));
|
|
1083
|
+
recordServerVersion(config, view.name, health?.version);
|
|
704
1084
|
} catch {
|
|
705
1085
|
// Unreachable => contribute nothing to /model. No placeholders: an
|
|
706
1086
|
// unreachable model is not listable. Sessions pinned to a lemonade model
|
|
@@ -743,6 +1123,8 @@ const LEGACY_PROVIDER_IDS = ["lemonade", "lemonade-undefined"];
|
|
|
743
1123
|
/** Register (or re-register) every configured instance. Each becomes its own
|
|
744
1124
|
* pi provider under the uniform lemonade-<name> id. */
|
|
745
1125
|
async function registerLemonadeProvider(pi: ExtensionAPI, config: LemonadeConfig): Promise<number> {
|
|
1126
|
+
serverVersionMismatches.length = 0; // (re)collected per registration pass
|
|
1127
|
+
serverVersionsSeen.clear();
|
|
746
1128
|
for (const staleId of LEGACY_PROVIDER_IDS) {
|
|
747
1129
|
try {
|
|
748
1130
|
pi.unregisterProvider(staleId);
|
|
@@ -2146,6 +2528,12 @@ function formatStatus(
|
|
|
2146
2528
|
`Status: ${health.status ?? "?"} Lemonade v${health.version ?? "?"} Telemetry: ${telemetry}` +
|
|
2147
2529
|
(updated ? ` · updated ${updated.toTimeString().slice(0, 8)}` : ""),
|
|
2148
2530
|
`Endpoint: ${config.baseUrl} WebSocket port: ${health.websocket_port ?? "?"}`,
|
|
2531
|
+
`This extension: v${PKG_VERSION}, verified against lemonade v${EXTENSION_CONFIRMED_SERVER_VERSION} · docs snapshot: ${
|
|
2532
|
+
(() => {
|
|
2533
|
+
const s = loadDocsSnapshot();
|
|
2534
|
+
return !s ? "not checked yet" : s.changed.length ? `${s.changed.length} file(s) changed upstream` : "up to date";
|
|
2535
|
+
})()
|
|
2536
|
+
}`,
|
|
2149
2537
|
""
|
|
2150
2538
|
);
|
|
2151
2539
|
|
|
@@ -2583,11 +2971,155 @@ function registerSetupCommand(pi: ExtensionAPI, configRef: { current: LemonadeCo
|
|
|
2583
2971
|
{ value: "models", label: "Model management", description: `List, load, unload, pull (progress+cancel), delete, change-ctx, HF install, filter, refresh — active instance: ${activeInstance}` },
|
|
2584
2972
|
{ value: "transcription", label: "Transcription settings", description: "Whisper model, paths, smoke test" },
|
|
2585
2973
|
{ value: "logs", label: "Live server logs", description: "Stream the server's log over websocket" },
|
|
2974
|
+
{ value: "spec", label: "Spec & version check", description: "Docs drift + live server-version drift — status, re-check, re-sync, toggles" },
|
|
2586
2975
|
{ value: "instance", label: "Switch instance", description: `Current: ${activeInstance} — pick which box status/models/logs act on` },
|
|
2587
2976
|
{ value: "exit", label: "Exit", description: "Close the setup menu" },
|
|
2588
2977
|
]);
|
|
2589
2978
|
if (choice === null || choice === "exit") break;
|
|
2590
2979
|
|
|
2980
|
+
// ── Spec & version check ───────────────────────────────────────
|
|
2981
|
+
if (choice === "spec") {
|
|
2982
|
+
for (;;) {
|
|
2983
|
+
const config = configRef.current;
|
|
2984
|
+
const docsSnap = loadDocsSnapshot();
|
|
2985
|
+
const docsState = !config.specCheck
|
|
2986
|
+
? "disabled (specCheck: false)"
|
|
2987
|
+
: docsSnap
|
|
2988
|
+
? docsSnap.changed.length
|
|
2989
|
+
? `CHANGED — ${docsSnap.changed.join(", ")}`
|
|
2990
|
+
: "up to date"
|
|
2991
|
+
: "no verdict yet this cooldown window";
|
|
2992
|
+
const serversState = !config.serverVersionCheck
|
|
2993
|
+
? "sync check disabled (serverVersionCheck: false)"
|
|
2994
|
+
: serverVersionMismatches.length
|
|
2995
|
+
? `${serverVersionMismatches.length} instance(s) at an unverified server version`
|
|
2996
|
+
: `all instances on v${EXTENSION_CONFIRMED_SERVER_VERSION} (the build this extension was verified against)`;
|
|
2997
|
+
const upstreamSnap = loadUpstreamSnapshot();
|
|
2998
|
+
const upstreamState = !config.upstreamVersionCheck
|
|
2999
|
+
? "notice disabled (upstreamVersionCheck: false)"
|
|
3000
|
+
: upstreamSnap
|
|
3001
|
+
? [...serverVersionsSeen.values()].some((v) => {
|
|
3002
|
+
const c = upstreamSnap.latest ? versionCompare(v, upstreamSnap.latest) : null;
|
|
3003
|
+
return c !== null && c < 0;
|
|
3004
|
+
})
|
|
3005
|
+
? "update available — at least one server is behind the latest published release"
|
|
3006
|
+
: `all servers current with the latest published release (v${upstreamSnap.latest})`
|
|
3007
|
+
: "not probed yet";
|
|
3008
|
+
const sub = await menu(ctx, "Spec & Version Check", [
|
|
3009
|
+
{ value: "status", label: "Status", description: `Docs: ${docsState} · Servers: ${serversState} · Upstream: ${upstreamState}` },
|
|
3010
|
+
{ value: "recheck", label: "Re-check docs now", description: "Compare the docs/ snapshot with upstream lemonade-sdk/lemonade by git blob SHA" },
|
|
3011
|
+
{ value: "resync", label: "Re-sync docs/ from upstream", description: "Download the current docs/api markdown into docs/ — ground truth only, the extension code is NOT updated" },
|
|
3012
|
+
{ value: "toggledocs", label: `Toggle docs check: ${config.specCheck ? "on → off" : "off → on"}`, description: `specCheck (currently ${config.specCheck ? "on" : "off"})` },
|
|
3013
|
+
{ value: "toggleserver", label: `Toggle server-version check: ${config.serverVersionCheck ? "on → off" : "off → on"}`, description: `serverVersionCheck (currently ${config.serverVersionCheck ? "on" : "off"}) — extension ↔ server sync, the startup warning` },
|
|
3014
|
+
{ value: "toggleupstream", label: `Toggle upstream-release notice: ${config.upstreamVersionCheck ? "on → off" : "off → on"}`, description: `upstreamVersionCheck (currently ${config.upstreamVersionCheck ? "on" : "off"}) — "a server update is available", info tier` },
|
|
3015
|
+
{ value: "back", label: "Back", description: "" },
|
|
3016
|
+
]);
|
|
3017
|
+
if (sub === null || sub === "back") break;
|
|
3018
|
+
|
|
3019
|
+
if (sub === "status") {
|
|
3020
|
+
ctx.ui.setStatus("lemonade-setup", "Probing instances…");
|
|
3021
|
+
const instanceLines: string[] = [];
|
|
3022
|
+
for (const v of listInstances(config)) {
|
|
3023
|
+
const h = await fetchHealth(v.config);
|
|
3024
|
+
const drift =
|
|
3025
|
+
h?.version && h.version !== EXTENSION_CONFIRMED_SERVER_VERSION
|
|
3026
|
+
? ` — VERSION DRIFT (extension verified against v${EXTENSION_CONFIRMED_SERVER_VERSION})`
|
|
3027
|
+
: "";
|
|
3028
|
+
instanceLines.push(
|
|
3029
|
+
` ${v.name}${v.isDefault ? " (default)" : ""} — ${h?.version ? `server v${h.version}` : "unreachable"}${drift}`
|
|
3030
|
+
);
|
|
3031
|
+
}
|
|
3032
|
+
ctx.ui.setStatus("lemonade-setup", undefined);
|
|
3033
|
+
await textView(
|
|
3034
|
+
ctx,
|
|
3035
|
+
"Spec & Version Check — Status",
|
|
3036
|
+
[
|
|
3037
|
+
"Docs (vs upstream lemonade-sdk/lemonade docs/api, by git blob SHA):",
|
|
3038
|
+
` ${docsState}`,
|
|
3039
|
+
"",
|
|
3040
|
+
"Live server versions (vs the build this extension was verified against):",
|
|
3041
|
+
...instanceLines,
|
|
3042
|
+
` verified build: v${EXTENSION_CONFIRMED_SERVER_VERSION} (recorded in the extension code — not a setting)`,
|
|
3043
|
+
"",
|
|
3044
|
+
`Latest published upstream release (probed via GitHub, ${config.upstreamVersionCheck ? "notice on" : "notice off"}):`,
|
|
3045
|
+
` latest: v${upstreamSnap?.latest ?? "(unknown — not probed or unreachable)"}`,
|
|
3046
|
+
...[...serverVersionsSeen].map(([name, v]) => {
|
|
3047
|
+
const latest = upstreamSnap?.latest;
|
|
3048
|
+
const cmp = latest ? versionCompare(v, latest) : null;
|
|
3049
|
+
const rel =
|
|
3050
|
+
cmp === null
|
|
3051
|
+
? ""
|
|
3052
|
+
: cmp < 0
|
|
3053
|
+
? " — behind (a server update is available)"
|
|
3054
|
+
: cmp > 0
|
|
3055
|
+
? " — ahead of the latest stable release"
|
|
3056
|
+
: " — current";
|
|
3057
|
+
return ` ${name}: v${v}${rel}`;
|
|
3058
|
+
}),
|
|
3059
|
+
"",
|
|
3060
|
+
"Docs drift is TUI-only status on purpose: as long as the extension and the",
|
|
3061
|
+
"server agree on version, the docs snapshot matching upstream HEAD changes",
|
|
3062
|
+
"nothing at runtime. Re-sync re-downloads the current upstream docs into",
|
|
3063
|
+
"the extension's docs/ folder; it does NOT update the extension code. The",
|
|
3064
|
+
"startup warnings are reserved for extension ↔ server sync (warning) and",
|
|
3065
|
+
"server-vs-latest-release (one-line info notice).",
|
|
3066
|
+
],
|
|
3067
|
+
true
|
|
3068
|
+
);
|
|
3069
|
+
} else if (sub === "recheck") {
|
|
3070
|
+
ctx.ui.setStatus("lemonade-setup", "Checking upstream docs…");
|
|
3071
|
+
const result = await performDocsLiveCheck(config.specCheckTimeoutMs);
|
|
3072
|
+
ctx.ui.setStatus("lemonade-setup", undefined);
|
|
3073
|
+
if (result.verdict === "unknown") {
|
|
3074
|
+
ctx.ui.notify(`Docs check failed: ${result.error}`, "error");
|
|
3075
|
+
continue;
|
|
3076
|
+
}
|
|
3077
|
+
saveDocsSnapshot({ checkedAt: Date.now(), changed: result.changed });
|
|
3078
|
+
ctx.ui.notify(
|
|
3079
|
+
result.verdict === "current"
|
|
3080
|
+
? "Docs snapshot is up to date with upstream."
|
|
3081
|
+
: `Docs differ from upstream (${result.changed.join(", ")}) — the extension may be out of date. Update the extension if one is available, or re-sync the docs for review.`,
|
|
3082
|
+
result.verdict === "current" ? "info" : "warning"
|
|
3083
|
+
);
|
|
3084
|
+
} else if (sub === "resync") {
|
|
3085
|
+
const ok = await ctx.ui.confirm(
|
|
3086
|
+
"Re-sync docs/ from upstream?",
|
|
3087
|
+
"Downloads the current docs/api markdown into docs/. This refreshes the ground truth ONLY — the extension code is unchanged and may still need an update."
|
|
3088
|
+
);
|
|
3089
|
+
if (!ok) continue;
|
|
3090
|
+
ctx.ui.setStatus("lemonade-setup", "Downloading upstream docs…");
|
|
3091
|
+
const result = await resyncLemonadeDocs(config.specCheckTimeoutMs);
|
|
3092
|
+
ctx.ui.setStatus("lemonade-setup", undefined);
|
|
3093
|
+
ctx.ui.notify(
|
|
3094
|
+
result.ok
|
|
3095
|
+
? "Docs re-synced from upstream. The extension code itself has NOT changed — re-verify it against the new docs or update the extension when available."
|
|
3096
|
+
: `Re-sync failed: ${result.error}`,
|
|
3097
|
+
result.ok ? "info" : "error"
|
|
3098
|
+
);
|
|
3099
|
+
} else if (sub === "toggledocs") {
|
|
3100
|
+
config.specCheck = !config.specCheck;
|
|
3101
|
+
saveConfig(config);
|
|
3102
|
+
ctx.ui.notify(`Docs spec check ${config.specCheck ? "enabled" : "disabled"}.`, "info");
|
|
3103
|
+
} else if (sub === "toggleupstream") {
|
|
3104
|
+
config.upstreamVersionCheck = !config.upstreamVersionCheck;
|
|
3105
|
+
saveConfig(config);
|
|
3106
|
+
ctx.ui.notify(
|
|
3107
|
+
`Upstream-release notice ${config.upstreamVersionCheck ? "enabled" : "disabled"}.`,
|
|
3108
|
+
"info"
|
|
3109
|
+
);
|
|
3110
|
+
} else if (sub === "toggleserver") {
|
|
3111
|
+
config.serverVersionCheck = !config.serverVersionCheck;
|
|
3112
|
+
saveConfig(config);
|
|
3113
|
+
ctx.ui.notify(`Server-version check ${config.serverVersionCheck ? "enabled" : "disabled"}.`, "info");
|
|
3114
|
+
const [, msg] = await withStatus(ctx, "Re-registering providers…", () =>
|
|
3115
|
+
registerLemonadeProvider(pi, config).then((n) => `${n} model(s) registered.`)
|
|
3116
|
+
);
|
|
3117
|
+
void msg;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
continue;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
2591
3123
|
// ── Switch instance ─────────────────────────────────────────────
|
|
2592
3124
|
if (choice === "instance") {
|
|
2593
3125
|
const picked = await menu(
|
|
@@ -3228,6 +3760,7 @@ export default async function piLemonadeLink(pi: ExtensionAPI): Promise<void> {
|
|
|
3228
3760
|
await registerAgentTools(pi, configRef.current);
|
|
3229
3761
|
registerLemonadeBar(pi, configRef);
|
|
3230
3762
|
registerSetupCommand(pi, configRef);
|
|
3763
|
+
registerSpecDriftWarnings(pi, configRef);
|
|
3231
3764
|
|
|
3232
3765
|
// Fresh install (or a hand-emptied config): nothing is registered yet —
|
|
3233
3766
|
// point the user at the wizard instead of leaving them wondering why
|
package/lemonade.example.json
CHANGED
|
@@ -81,5 +81,27 @@
|
|
|
81
81
|
// How often (ms) the bar polls the active instance for busy/queue +
|
|
82
82
|
// CPU/GPU/NPU/VRAM. 0 = local metrics only (instance name, tok/s,
|
|
83
83
|
// cache-hit %).
|
|
84
|
-
"barPollMs": 5000
|
|
84
|
+
"barPollMs": 5000,
|
|
85
|
+
|
|
86
|
+
// Docs-freshness check: compare the docs/ snapshot against upstream
|
|
87
|
+
// lemonade-sdk/lemonade by git blob SHA. TUI-only status (shown in
|
|
88
|
+
// /lemonade-setup → Spec & version check, with re-check/re-sync) — NO
|
|
89
|
+
// startup warning: extension ↔ server version agreement is what matters
|
|
90
|
+
// at runtime, and that has its own checks below.
|
|
91
|
+
"specCheck": true,
|
|
92
|
+
"specCheckCooldownMs": 86400000,
|
|
93
|
+
"specCheckTimeoutMs": 5000,
|
|
94
|
+
|
|
95
|
+
// Tier 1 (startup warning): /v1/health self-reports the server version.
|
|
96
|
+
// If an instance advertises a version other than the build this extension
|
|
97
|
+
// was verified against (a constant in the extension's code — see the
|
|
98
|
+
// README "Keeping the specs honest" section), a directional warning fires
|
|
99
|
+
// on startup: server newer → check for an extension update; server older →
|
|
100
|
+
// update the server.
|
|
101
|
+
"serverVersionCheck": true,
|
|
102
|
+
|
|
103
|
+
// Tier 2 (info notice, one sentence): servers older than the latest
|
|
104
|
+
// published lemonade release (probed via GitHub, throttled) get an
|
|
105
|
+
// "a server update is available" notice. Separately toggleable.
|
|
106
|
+
"upstreamVersionCheck": true
|
|
85
107
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lemonade-link",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Unified pi extension for a self-hosted Lemonade server: dynamic chat-model discovery, multimodal agent tools (transcription, image gen/edit/upscale, TTS, audio, 3D mesh), a live /lemonade-setup management TUI, and a lemonade-only below-editor status bar (tok/s, cache hits, busy/queue, VRAM).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -20,5 +20,10 @@
|
|
|
20
20
|
},
|
|
21
21
|
"pi": {
|
|
22
22
|
"extensions": ["./index.ts"]
|
|
23
|
+
},
|
|
24
|
+
"provider": {
|
|
25
|
+
"name": "lemonade",
|
|
26
|
+
"apiVerifiedAgainst": "11.7.0",
|
|
27
|
+
"specSnapshot": "docs/ from lemonade-sdk/lemonade docs/api, snapshotted 2026-09-12"
|
|
23
28
|
}
|
|
24
29
|
}
|