pi2dsh 0.12.2 → 0.12.4
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 +47 -10
- package/README.zh.md +39 -8
- package/dist/{analyzer-CEjVQKXA.mjs → analyzer-CXNbeBAe.mjs} +2 -2
- package/dist/{analyzer-CEjVQKXA.mjs.map → analyzer-CXNbeBAe.mjs.map} +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/client.js +525 -0
- package/dist/compat/pi-ai.d.mts +36 -1
- package/dist/compat/pi-ai.d.mts.map +1 -1
- package/dist/compat/pi-ai.mjs +2 -2
- package/dist/compat/pi-coding-agent.d.mts +2 -1411
- package/dist/compat/pi-coding-agent.mjs +1 -1
- package/dist/compat/pi-tui.mjs +1 -1
- package/dist/{oauth-bridge-CfyImWx4.mjs → credentials-oauth-BakY-0PA.mjs} +172 -13
- package/dist/credentials-oauth-BakY-0PA.mjs.map +1 -0
- package/dist/credentials-oauth.mjs +1 -89
- package/dist/host.mjs +4 -2
- package/dist/host.mjs.map +1 -1
- package/dist/index.d.mts +16 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/mcp-config-CZepacin.mjs +766 -0
- package/dist/mcp-config-CZepacin.mjs.map +1 -0
- package/dist/{pi-ai-Dyg4zyLZ.mjs → pi-ai-BA3V_J0V.mjs} +40 -2
- package/dist/{pi-ai-Dyg4zyLZ.mjs.map → pi-ai-BA3V_J0V.mjs.map} +1 -1
- package/dist/pi-coding-agent-B0PT23gE.d.mts +1412 -0
- package/dist/pi-coding-agent-B0PT23gE.d.mts.map +1 -0
- package/dist/{pi-coding-agent-CYnmeJEx.mjs → pi-coding-agent-D9skobb6.mjs} +0 -0
- package/dist/pi-coding-agent-D9skobb6.mjs.map +1 -0
- package/dist/{pi-tui-5CYLcj-_.mjs → pi-tui-DruSeKmd.mjs} +3 -3
- package/dist/pi-tui-DruSeKmd.mjs.map +1 -0
- package/dist/{runtime-DsPaznXJ.mjs → runtime-DH0zEUpu.mjs} +1075 -67
- package/dist/runtime-DH0zEUpu.mjs.map +1 -0
- package/dist/runtime.d.mts +594 -0
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +1 -1
- package/package.json +15 -4
- package/dist/compat/pi-coding-agent.d.mts.map +0 -1
- package/dist/credentials-oauth.mjs.map +0 -1
- package/dist/mcp-config-DHkfjdvX.mjs +0 -707
- package/dist/mcp-config-DHkfjdvX.mjs.map +0 -1
- package/dist/oauth-bridge-CfyImWx4.mjs.map +0 -1
- package/dist/pi-coding-agent-CYnmeJEx.mjs.map +0 -1
- package/dist/pi-tui-5CYLcj-_.mjs.map +0 -1
- package/dist/runtime-DsPaznXJ.mjs.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
3
|
-
import { P as generateBranchSummary, Tn as PiCapabilityError, b as Theme, d as ExtensionRunner, w as __setSubagentSessionFactory, wn as CapabilityLedger } from "./pi-coding-agent-
|
|
2
|
+
import { J as AssistantMessageEventStream, Y as isContextOverflow, c as builtinProviders, i as __setPiAiLlmBridge, x as getSupportedThinkingLevels } from "./pi-ai-BA3V_J0V.mjs";
|
|
3
|
+
import { P as generateBranchSummary, Tn as PiCapabilityError, b as Theme, d as ExtensionRunner, w as __setSubagentSessionFactory, wn as CapabilityLedger } from "./pi-coding-agent-D9skobb6.mjs";
|
|
4
4
|
import { t as getAgentDir } from "./pi-config-shim-CZ1wFzqM.mjs";
|
|
5
|
-
import { a as resolvePiProviderAuth,
|
|
5
|
+
import { a as FileCredentialStore, c as providerSupportsOAuth, i as oauthCredentialRef, l as resolvePiProviderAuth, o as joinSignals, s as loginPiProvider, u as storedOAuthCredential } from "./credentials-oauth-BakY-0PA.mjs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
8
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
@@ -16,6 +16,7 @@ import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmError, QUOTA_EXCE
|
|
|
16
16
|
import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
18
|
import { foldSurface } from "@deepseek-ai/dsh-session";
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
19
20
|
import { Compile } from "typebox/compile";
|
|
20
21
|
import { Value } from "typebox/value";
|
|
21
22
|
//#region src/session-bridge.ts
|
|
@@ -102,6 +103,29 @@ var PiSessionBridge = class {
|
|
|
102
103
|
});
|
|
103
104
|
return id;
|
|
104
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The custom entries a package appended to one session, oldest first.
|
|
108
|
+
*
|
|
109
|
+
* Addressed by id alone: the browser half asks for a session it is showing,
|
|
110
|
+
* and has no DSH session object to hand over.
|
|
111
|
+
* @param sessionId - session whose sidecar to read.
|
|
112
|
+
* @returns each custom entry with its type, data and id.
|
|
113
|
+
*/
|
|
114
|
+
customEntries(sessionId) {
|
|
115
|
+
this.load(sessionId);
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const record of this.records.get(sessionId) ?? []) {
|
|
118
|
+
if (record.kind !== "custom") continue;
|
|
119
|
+
const entry = record;
|
|
120
|
+
out.push({
|
|
121
|
+
id: entry.id,
|
|
122
|
+
customType: entry.customType,
|
|
123
|
+
data: entry.data,
|
|
124
|
+
timestamp: entry.timestamp
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
105
129
|
appendBranchSummary(sessionId, summary, fromId) {
|
|
106
130
|
const id = randomUUID();
|
|
107
131
|
this.persist(sessionId, {
|
|
@@ -348,6 +372,7 @@ var PiBridgedAgentSession = class {
|
|
|
348
372
|
agent = {};
|
|
349
373
|
messages = [];
|
|
350
374
|
#seed = [];
|
|
375
|
+
#carried = /* @__PURE__ */ new WeakSet();
|
|
351
376
|
#host;
|
|
352
377
|
#handle;
|
|
353
378
|
#session;
|
|
@@ -433,7 +458,19 @@ var PiBridgedAgentSession = class {
|
|
|
433
458
|
const identity = (message) => JSON.stringify([message.role, message.content]);
|
|
434
459
|
const known = new Set(this.messages.map(identity));
|
|
435
460
|
this.#seed = next.filter((message) => !known.has(identity(message)));
|
|
436
|
-
for (const message of this.#seed)
|
|
461
|
+
for (const message of this.#seed) {
|
|
462
|
+
this.#carried.add(message);
|
|
463
|
+
this.messages.push(message);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Whether one transcript entry is carried context rather than part of the
|
|
468
|
+
* exchange in this child session.
|
|
469
|
+
* @param message - an entry of {@link messages}.
|
|
470
|
+
* @returns true for a seeded transcript entry or a host runtime snapshot.
|
|
471
|
+
*/
|
|
472
|
+
isCarriedContext(message) {
|
|
473
|
+
return this.#carried.has(message);
|
|
437
474
|
}
|
|
438
475
|
#project(event) {
|
|
439
476
|
const type = event.type;
|
|
@@ -498,6 +535,7 @@ var PiBridgedAgentSession = class {
|
|
|
498
535
|
this.#messageProjection = this.#messageProjection.then(async () => {
|
|
499
536
|
const message = await this.#host.messageFromSessionEvent(event);
|
|
500
537
|
if (message === void 0) return;
|
|
538
|
+
if ((event.data?.source)?.form === "snapshot") this.#carried.add(message);
|
|
501
539
|
this.messages.push(message);
|
|
502
540
|
emit({
|
|
503
541
|
type: "message_start",
|
|
@@ -541,7 +579,7 @@ var PiBridgedAgentSession = class {
|
|
|
541
579
|
if (rendered.length > 0) {
|
|
542
580
|
const seedBlocks = await this.#host.piContentToDsh([{
|
|
543
581
|
type: "text",
|
|
544
|
-
text:
|
|
582
|
+
text: `<${SEED_CARRIER_TAG}>\n${rendered.join("\n\n")}\n</${SEED_CARRIER_TAG}>`
|
|
545
583
|
}]);
|
|
546
584
|
this.#host.deliver(this.#handle.agent, createUserMessage({
|
|
547
585
|
content: seedBlocks,
|
|
@@ -639,6 +677,15 @@ var PiBridgedAgentSession = class {
|
|
|
639
677
|
* @param requested - a label the calling package supplied, if any.
|
|
640
678
|
* @param packageName - the Pi package the call came from.
|
|
641
679
|
*/
|
|
680
|
+
/**
|
|
681
|
+
* Envelope the bridge wraps a seeded transcript in before handing it to the
|
|
682
|
+
* child as one durable message.
|
|
683
|
+
*
|
|
684
|
+
* Exported because it is a CONTRACT, not a formatting detail: a reader of the
|
|
685
|
+
* child session (the side panel) has to recognise the carrier to tell carried
|
|
686
|
+
* context apart from the exchange, and matching on a copied string would drift.
|
|
687
|
+
*/
|
|
688
|
+
const SEED_CARRIER_TAG = "prior-conversation";
|
|
642
689
|
function childLabel(requested, packageName) {
|
|
643
690
|
if (typeof requested === "string" && requested.trim().length > 0) return requested.trim().slice(0, 80);
|
|
644
691
|
const owner = packageName?.trim();
|
|
@@ -698,6 +745,445 @@ async function createBridgedAgentSession(host, options) {
|
|
|
698
745
|
return { session: new PiBridgedAgentSession(host, handle, tools) };
|
|
699
746
|
}
|
|
700
747
|
//#endregion
|
|
748
|
+
//#region src/compat/vendor/pi-open-browser.ts
|
|
749
|
+
/**
|
|
750
|
+
* Open a URL or file in the platform browser/default handler.
|
|
751
|
+
*
|
|
752
|
+
* This intentionally never invokes a shell. On Windows, do not use
|
|
753
|
+
* `cmd /c start`: cmd.exe re-parses metacharacters (&, |, ^, ...) before
|
|
754
|
+
* `start` runs, which would make attacker-controlled URLs injectable.
|
|
755
|
+
* @param target - the URL or file to hand to the platform handler.
|
|
756
|
+
*/
|
|
757
|
+
function openBrowser(target) {
|
|
758
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [target]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", target]] : ["xdg-open", [target]];
|
|
759
|
+
spawn(cmd, args, {
|
|
760
|
+
stdio: "ignore",
|
|
761
|
+
detached: true
|
|
762
|
+
}).on("error", () => {}).unref();
|
|
763
|
+
}
|
|
764
|
+
//#endregion
|
|
765
|
+
//#region src/browser-surfaces.ts
|
|
766
|
+
/** Flatten one Pi message's content to the text a panel row shows. */
|
|
767
|
+
function messageText(message) {
|
|
768
|
+
const content = message.content;
|
|
769
|
+
if (typeof content === "string") return content;
|
|
770
|
+
if (!Array.isArray(content)) return "";
|
|
771
|
+
const parts = [];
|
|
772
|
+
for (const block of content) if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
773
|
+
else if (block.type === "toolCall" && typeof block.name === "string") parts.push(`[tool: ${block.name}]`);
|
|
774
|
+
return parts.join("\n");
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Live side threads, keyed by the parent session they belong to.
|
|
778
|
+
*
|
|
779
|
+
* Host-level (one per engine instance, like the provider directory): the panel
|
|
780
|
+
* is one surface, however many Pi packages contribute threads to it.
|
|
781
|
+
*/
|
|
782
|
+
var BrowserSurfaces = class {
|
|
783
|
+
#byParent = /* @__PURE__ */ new Map();
|
|
784
|
+
#surfaces = /* @__PURE__ */ new Map();
|
|
785
|
+
#entrySources = /* @__PURE__ */ new Map();
|
|
786
|
+
#draftRequests = /* @__PURE__ */ new Map();
|
|
787
|
+
#liveDrafts = /* @__PURE__ */ new Map();
|
|
788
|
+
#completionSources = /* @__PURE__ */ new Map();
|
|
789
|
+
/**
|
|
790
|
+
* Track one child session under its parent.
|
|
791
|
+
* @param parentSessionId - the session the panel floats over.
|
|
792
|
+
* @param thread - the child's identity and live session object.
|
|
793
|
+
* @returns a disposer that removes the thread from the panel.
|
|
794
|
+
*/
|
|
795
|
+
track(parentSessionId, thread) {
|
|
796
|
+
const threads = this.#byParent.get(parentSessionId) ?? /* @__PURE__ */ new Map();
|
|
797
|
+
threads.set(thread.id, thread);
|
|
798
|
+
this.#byParent.set(parentSessionId, threads);
|
|
799
|
+
return () => {
|
|
800
|
+
const live = this.#byParent.get(parentSessionId);
|
|
801
|
+
if (live === void 0) return;
|
|
802
|
+
live.delete(thread.id);
|
|
803
|
+
if (live.size === 0) this.#byParent.delete(parentSessionId);
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* The panel's view of one parent session.
|
|
808
|
+
* @param parentSessionId - session the browser is currently showing.
|
|
809
|
+
* @returns every side thread opened under it, oldest first.
|
|
810
|
+
*/
|
|
811
|
+
snapshot(parentSessionId) {
|
|
812
|
+
const threads = this.#byParent.get(parentSessionId);
|
|
813
|
+
if (threads === void 0) return [];
|
|
814
|
+
return [...threads.values()].map((thread) => ({
|
|
815
|
+
id: thread.id,
|
|
816
|
+
label: thread.label,
|
|
817
|
+
...thread.package === void 0 ? {} : { package: thread.package },
|
|
818
|
+
running: thread.session.isStreaming === true,
|
|
819
|
+
messages: thread.session.messages.filter((message) => !thread.session.isCarriedContext(message)).map((message) => ({
|
|
820
|
+
role: String(message.role ?? ""),
|
|
821
|
+
text: messageText(message)
|
|
822
|
+
})).filter((entry) => !entry.text.startsWith(`<${SEED_CARRIER_TAG}>`)).filter((entry) => entry.text.length > 0)
|
|
823
|
+
}));
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Pi's `setStatus(key, text)`: one keyed status entry in the status bar.
|
|
827
|
+
* The key is the entry's identity; passing undefined removes that entry,
|
|
828
|
+
* exactly like Pi clears one status.
|
|
829
|
+
* @param sessionId - session the calling agent belongs to.
|
|
830
|
+
* @param packageName - Pi package making the call, when known.
|
|
831
|
+
* @param key - the status entry's key.
|
|
832
|
+
* @param text - the text to show, or undefined to remove the entry.
|
|
833
|
+
*/
|
|
834
|
+
setStatus(sessionId, packageName, key, text) {
|
|
835
|
+
if (sessionId.length === 0) return;
|
|
836
|
+
const view = this.#view(sessionId, packageName);
|
|
837
|
+
const rendered = text === void 0 || text === null ? void 0 : String(text);
|
|
838
|
+
if (rendered === void 0 || rendered.length === 0) delete view.statuses[key];
|
|
839
|
+
else view.statuses[key] = rendered;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Pi's `setWidget(key, content)`: one keyed widget. Only string arrays are
|
|
843
|
+
* recorded — anything else is ignored exactly like Pi's own rpc mode, where
|
|
844
|
+
* widgets are transmitted to a host as lines and factory content needs TUI
|
|
845
|
+
* access the host never provides.
|
|
846
|
+
* @param sessionId - session the calling agent belongs to.
|
|
847
|
+
* @param packageName - Pi package making the call, when known.
|
|
848
|
+
* @param key - the widget's key.
|
|
849
|
+
* @param content - the widget's lines, or undefined to remove the widget.
|
|
850
|
+
*/
|
|
851
|
+
setWidget(sessionId, packageName, key, content, theme) {
|
|
852
|
+
if (sessionId.length === 0) return;
|
|
853
|
+
const view = this.#view(sessionId, packageName);
|
|
854
|
+
const text = surfaceText(content, theme);
|
|
855
|
+
if (text === void 0) delete view.widgets[key];
|
|
856
|
+
else view.widgets[key] = text;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Record one simple presentation call (working chrome, title, header,
|
|
860
|
+
* footer). The value is a string, a Pi component, a working-indicator
|
|
861
|
+
* options object, or a header/footer factory; each renders to text here so
|
|
862
|
+
* the browser half never runs Pi code.
|
|
863
|
+
* @param sessionId - session the calling agent belongs to.
|
|
864
|
+
* @param packageName - Pi package making the call, when known.
|
|
865
|
+
* @param key - which surface.
|
|
866
|
+
* @param value - the value the package passed; undefined clears it.
|
|
867
|
+
* @param theme - the bridge's headless theme, for factories that style with it.
|
|
868
|
+
*/
|
|
869
|
+
setSurface(sessionId, packageName, key, value, theme) {
|
|
870
|
+
if (sessionId.length === 0) return;
|
|
871
|
+
const view = this.#view(sessionId, packageName);
|
|
872
|
+
const text = surfaceText(value, theme);
|
|
873
|
+
if (text === void 0) delete view.values[key];
|
|
874
|
+
else view.values[key] = text;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Pi's `setWorkingVisible`: hide the working chrome without forgetting it.
|
|
878
|
+
* @param sessionId - session the calling agent belongs to.
|
|
879
|
+
* @param packageName - Pi package making the call, when known.
|
|
880
|
+
* @param visible - whether the working chrome should show.
|
|
881
|
+
*/
|
|
882
|
+
setWorkingVisible(sessionId, packageName, visible) {
|
|
883
|
+
if (sessionId.length === 0) return;
|
|
884
|
+
this.#view(sessionId, packageName).workingVisible = visible;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Register one package's custom-entry renderer.
|
|
888
|
+
*
|
|
889
|
+
* Pulled rather than pushed: a renderer's output depends on the session being
|
|
890
|
+
* looked at, and the package registers once at mount for every session it
|
|
891
|
+
* will ever be asked about.
|
|
892
|
+
* @param packageName - owning Pi package.
|
|
893
|
+
* @param source - called with the session id the browser is showing.
|
|
894
|
+
* @returns a disposer that unregisters the source.
|
|
895
|
+
*/
|
|
896
|
+
trackEntries(packageName, source) {
|
|
897
|
+
this.#entrySources.set(packageName, source);
|
|
898
|
+
return () => {
|
|
899
|
+
this.#entrySources.delete(packageName);
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Custom entries every package renders for one session, in package order.
|
|
904
|
+
* @param sessionId - session the browser is showing.
|
|
905
|
+
* @returns rendered entries; a renderer that throws contributes nothing.
|
|
906
|
+
*/
|
|
907
|
+
entries(sessionId) {
|
|
908
|
+
const out = [];
|
|
909
|
+
for (const [packageName, source] of this.#entrySources) try {
|
|
910
|
+
for (const entry of source(sessionId)) out.push({
|
|
911
|
+
...entry,
|
|
912
|
+
package: packageName
|
|
913
|
+
});
|
|
914
|
+
} catch {}
|
|
915
|
+
return out;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Ask the browser to put text in the composer.
|
|
919
|
+
*
|
|
920
|
+
* A revision rather than a flag: the seat applies a request once, and the
|
|
921
|
+
* next call — even with identical text — is a new request rather than a
|
|
922
|
+
* no-op, which is what a package retrying a paste means.
|
|
923
|
+
* @param sessionId - session whose composer to write.
|
|
924
|
+
* @param text - the full next draft.
|
|
925
|
+
*/
|
|
926
|
+
requestDraft(sessionId, text) {
|
|
927
|
+
if (sessionId.length === 0) return;
|
|
928
|
+
const current = this.#draftRequests.get(sessionId);
|
|
929
|
+
this.#draftRequests.set(sessionId, {
|
|
930
|
+
text,
|
|
931
|
+
rev: (current?.rev ?? 0) + 1
|
|
932
|
+
});
|
|
933
|
+
this.#liveDrafts.set(sessionId, text);
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* The pending composer write for one session, if any.
|
|
937
|
+
* @param sessionId - session the browser is showing.
|
|
938
|
+
* @returns the request, or undefined when nothing is pending.
|
|
939
|
+
*/
|
|
940
|
+
draftRequest(sessionId) {
|
|
941
|
+
return this.#draftRequests.get(sessionId);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Record what the composer actually holds, as reported by the browser.
|
|
945
|
+
* @param sessionId - session the report is about.
|
|
946
|
+
* @param text - the live draft.
|
|
947
|
+
*/
|
|
948
|
+
reportDraft(sessionId, text) {
|
|
949
|
+
if (sessionId.length === 0) return;
|
|
950
|
+
this.#liveDrafts.set(sessionId, text);
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* The composer text for one session.
|
|
954
|
+
* @param sessionId - session to read.
|
|
955
|
+
* @returns the browser's last report, or the last requested text.
|
|
956
|
+
*/
|
|
957
|
+
liveDraft(sessionId) {
|
|
958
|
+
return this.#liveDrafts.get(sessionId) ?? "";
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Register one package's completion provider.
|
|
962
|
+
* @param packageName - owning Pi package.
|
|
963
|
+
* @param source - asked with the trigger character and the typed query.
|
|
964
|
+
* @returns a disposer that unregisters the source.
|
|
965
|
+
*/
|
|
966
|
+
trackCompletions(packageName, source) {
|
|
967
|
+
this.#completionSources.set(packageName, source);
|
|
968
|
+
return () => {
|
|
969
|
+
this.#completionSources.delete(packageName);
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Completions every package offers for one trigger token.
|
|
974
|
+
* @param trigger - the trigger character the menu opened on.
|
|
975
|
+
* @param query - what the user has typed after it.
|
|
976
|
+
* @returns candidates in package order; a provider that throws contributes none.
|
|
977
|
+
*/
|
|
978
|
+
async completions(trigger, query) {
|
|
979
|
+
const out = [];
|
|
980
|
+
for (const source of this.#completionSources.values()) try {
|
|
981
|
+
out.push(...await source(trigger, query));
|
|
982
|
+
} catch {}
|
|
983
|
+
return out;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* What packages have put on screen for one session.
|
|
987
|
+
* @param sessionId - session the browser is showing.
|
|
988
|
+
* @returns one entry per package that has driven a surface, empties dropped.
|
|
989
|
+
*/
|
|
990
|
+
surfaces(sessionId) {
|
|
991
|
+
const bySession = this.#surfaces.get(sessionId);
|
|
992
|
+
if (bySession === void 0) return [];
|
|
993
|
+
return [...bySession.values()].filter((view) => viewHasContent(view));
|
|
994
|
+
}
|
|
995
|
+
/** Get-or-create one package's view for a session. */
|
|
996
|
+
#view(sessionId, packageName) {
|
|
997
|
+
const owner = packageName ?? "pi";
|
|
998
|
+
const bySession = this.#surfaces.get(sessionId) ?? /* @__PURE__ */ new Map();
|
|
999
|
+
let view = bySession.get(owner);
|
|
1000
|
+
if (view === void 0) {
|
|
1001
|
+
view = {
|
|
1002
|
+
...packageName === void 0 ? {} : { package: packageName },
|
|
1003
|
+
values: {},
|
|
1004
|
+
statuses: {},
|
|
1005
|
+
widgets: {},
|
|
1006
|
+
workingVisible: true
|
|
1007
|
+
};
|
|
1008
|
+
bySession.set(owner, view);
|
|
1009
|
+
this.#surfaces.set(sessionId, bySession);
|
|
1010
|
+
}
|
|
1011
|
+
return view;
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
/** Whether a view still carries something to show. */
|
|
1015
|
+
function viewHasContent(view) {
|
|
1016
|
+
return Object.keys(view.values).length > 0 || Object.keys(view.statuses).length > 0 || Object.keys(view.widgets).length > 0;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Render whatever Pi handed a presentation call into displayable text.
|
|
1020
|
+
*
|
|
1021
|
+
* Pi's UI methods accept a string, a string array (widget lines), one of its
|
|
1022
|
+
* components (`render(width): string[]`), a working-indicator options object
|
|
1023
|
+
* (`{ frames?: string[] }`), or a header/footer factory
|
|
1024
|
+
* (`(tui, theme) => Component`). Every shape is projected to text here — the
|
|
1025
|
+
* pi-tui shims honour `render(width)` headlessly, and a factory is called with
|
|
1026
|
+
* the bridge's headless theme (and no TUI, which Pi's own rpc mode never
|
|
1027
|
+
* provides either); anything that cannot render leaves the surface empty
|
|
1028
|
+
* rather than faking output.
|
|
1029
|
+
* @param value - the value the package passed.
|
|
1030
|
+
* @param theme - the bridge's headless theme, for factories that style with it.
|
|
1031
|
+
* @returns the text to show, or undefined to clear the surface.
|
|
1032
|
+
*/
|
|
1033
|
+
function surfaceText(value, theme) {
|
|
1034
|
+
if (value === void 0 || value === null) return void 0;
|
|
1035
|
+
if (typeof value === "string") return value.length === 0 ? void 0 : value;
|
|
1036
|
+
if (Array.isArray(value)) {
|
|
1037
|
+
const text = value.map(String).join("\n").trimEnd();
|
|
1038
|
+
return text.length === 0 ? void 0 : text;
|
|
1039
|
+
}
|
|
1040
|
+
const record = value;
|
|
1041
|
+
if (typeof record.render === "function") try {
|
|
1042
|
+
const lines = record.render(80);
|
|
1043
|
+
const text = (Array.isArray(lines) ? lines : [lines]).map((line) => String(line)).join("\n").trimEnd();
|
|
1044
|
+
return text.length === 0 ? void 0 : text;
|
|
1045
|
+
} catch {
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (Array.isArray(record.frames)) {
|
|
1049
|
+
const text = record.frames.map(String).join("").trimEnd();
|
|
1050
|
+
return text.length === 0 ? void 0 : text;
|
|
1051
|
+
}
|
|
1052
|
+
if (typeof value === "function") try {
|
|
1053
|
+
return surfaceText(value(void 0, theme), theme);
|
|
1054
|
+
} catch {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const text = String(value);
|
|
1058
|
+
return text.length === 0 || text === "[object Object]" ? void 0 : text;
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Serve the browser half's own read route on the host's web server.
|
|
1062
|
+
*
|
|
1063
|
+
* One prefix, one GET, one payload: the client half polls it for the session
|
|
1064
|
+
* it is showing and gets every surface at once.
|
|
1065
|
+
* A composition with no web server (the CLI profile) simply has no panel, which
|
|
1066
|
+
* is the correct outcome rather than an error — the side conversation itself
|
|
1067
|
+
* still runs, and the host's subagent catalog still lists it.
|
|
1068
|
+
* @param ctx - the mounting context.
|
|
1069
|
+
* @param registry - live thread registry to read from.
|
|
1070
|
+
* @returns whether the route was registered.
|
|
1071
|
+
*/
|
|
1072
|
+
/**
|
|
1073
|
+
* Short, clickable stand-ins for the authorization URLs a login flow announces.
|
|
1074
|
+
*
|
|
1075
|
+
* An OAuth authorize URL is 400 characters of query string. Putting it in a
|
|
1076
|
+
* dialog produces six wrapped lines with the question buried under them, and a
|
|
1077
|
+
* link that long is unusable even when the surface makes it clickable. These
|
|
1078
|
+
* are the short links shown instead; hitting one redirects to the real address.
|
|
1079
|
+
*/
|
|
1080
|
+
const pendingAuthorizations = /* @__PURE__ */ new Map();
|
|
1081
|
+
/**
|
|
1082
|
+
* Publish a short link for one authorization URL.
|
|
1083
|
+
* @param url - the address the flow wants the user to open.
|
|
1084
|
+
* @returns the path to show, or undefined when the URL is not http(s).
|
|
1085
|
+
*/
|
|
1086
|
+
function publishAuthorization(url) {
|
|
1087
|
+
if (!/^https?:\/\//u.test(url)) return void 0;
|
|
1088
|
+
const token = Math.random().toString(36).slice(2, 10);
|
|
1089
|
+
pendingAuthorizations.set(token, url);
|
|
1090
|
+
if (pendingAuthorizations.size > 8) {
|
|
1091
|
+
const oldest = pendingAuthorizations.keys().next().value;
|
|
1092
|
+
if (oldest !== void 0) pendingAuthorizations.delete(oldest);
|
|
1093
|
+
}
|
|
1094
|
+
return `/pi2dsh/authorize/${token}`;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Retire a published link when its flow is over.
|
|
1098
|
+
*
|
|
1099
|
+
* A finished (or superseded) flow's address still resolves at the provider, and
|
|
1100
|
+
* following it produces a callback the flow that is now listening cannot match
|
|
1101
|
+
* — the provider answers "State mismatch", which reads like a bug in the login
|
|
1102
|
+
* rather than a link that has expired. Saying so is the honest answer.
|
|
1103
|
+
* @param path - what {@link publishAuthorization} returned.
|
|
1104
|
+
*/
|
|
1105
|
+
function revokeAuthorization(path) {
|
|
1106
|
+
pendingAuthorizations.delete(path.slice(18));
|
|
1107
|
+
}
|
|
1108
|
+
function registerBrowserSurfaceRoute(ctx, registry) {
|
|
1109
|
+
const web = ctx.get("webServer");
|
|
1110
|
+
if (web === void 0 || typeof web.register !== "function") return false;
|
|
1111
|
+
ctx.effect(() => web.register({
|
|
1112
|
+
kind: "prefix",
|
|
1113
|
+
path: "/pi2dsh",
|
|
1114
|
+
handler: async (req, res) => {
|
|
1115
|
+
const response = res;
|
|
1116
|
+
const method = String(req.method ?? "GET");
|
|
1117
|
+
const url = new URL(String(req.url ?? "/"), "http://pi2dsh.invalid");
|
|
1118
|
+
if (method === "POST" && url.pathname === "/pi2dsh/editor-draft") {
|
|
1119
|
+
const chunks = [];
|
|
1120
|
+
const body = await new Promise((settle) => {
|
|
1121
|
+
const stream = req;
|
|
1122
|
+
stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
1123
|
+
stream.on("end", () => settle(Buffer.concat(chunks).toString("utf8")));
|
|
1124
|
+
});
|
|
1125
|
+
try {
|
|
1126
|
+
const payload = JSON.parse(body || "{}");
|
|
1127
|
+
if (typeof payload.session === "string" && typeof payload.draft === "string") registry.reportDraft(payload.session, payload.draft);
|
|
1128
|
+
} catch {}
|
|
1129
|
+
response.writeHead(204);
|
|
1130
|
+
response.end();
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
1134
|
+
response.writeHead(405);
|
|
1135
|
+
response.end();
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (url.pathname.startsWith("/pi2dsh/authorize/")) {
|
|
1139
|
+
const target = pendingAuthorizations.get(url.pathname.slice(18));
|
|
1140
|
+
if (target === void 0) {
|
|
1141
|
+
response.writeHead(404);
|
|
1142
|
+
response.end("this login link has expired");
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
response.writeHead(302, {
|
|
1146
|
+
location: target,
|
|
1147
|
+
"cache-control": "no-store"
|
|
1148
|
+
});
|
|
1149
|
+
response.end();
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (url.pathname === "/pi2dsh/completions") {
|
|
1153
|
+
const items = await registry.completions(url.searchParams.get("trigger") ?? "@", url.searchParams.get("query") ?? "");
|
|
1154
|
+
response.writeHead(200, {
|
|
1155
|
+
"content-type": "application/json; charset=utf-8",
|
|
1156
|
+
"cache-control": "no-store"
|
|
1157
|
+
});
|
|
1158
|
+
response.end(JSON.stringify({ items }));
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (url.pathname !== "/pi2dsh/browser-state") {
|
|
1162
|
+
response.writeHead(404);
|
|
1163
|
+
response.end();
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const session = url.searchParams.get("session") ?? "";
|
|
1167
|
+
const body = JSON.stringify(session === "" ? {
|
|
1168
|
+
threads: [],
|
|
1169
|
+
surfaces: [],
|
|
1170
|
+
entries: []
|
|
1171
|
+
} : {
|
|
1172
|
+
threads: registry.snapshot(session),
|
|
1173
|
+
surfaces: registry.surfaces(session),
|
|
1174
|
+
entries: registry.entries(session),
|
|
1175
|
+
...registry.draftRequest(session) === void 0 ? {} : { draft: registry.draftRequest(session) }
|
|
1176
|
+
});
|
|
1177
|
+
response.writeHead(200, {
|
|
1178
|
+
"content-type": "application/json; charset=utf-8",
|
|
1179
|
+
"cache-control": "no-store"
|
|
1180
|
+
});
|
|
1181
|
+
response.end(method === "HEAD" ? void 0 : body);
|
|
1182
|
+
}
|
|
1183
|
+
}), "pi2dsh: browser-state route");
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
//#endregion
|
|
701
1187
|
//#region src/compat/vendor/pi-tool-validation.ts
|
|
702
1188
|
const validatorCache = /* @__PURE__ */ new WeakMap();
|
|
703
1189
|
const TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
|
|
@@ -1654,12 +2140,27 @@ function providerCarriesTransport(value) {
|
|
|
1654
2140
|
}
|
|
1655
2141
|
function providerModels(provider) {
|
|
1656
2142
|
try {
|
|
1657
|
-
const models = provider.getModels
|
|
2143
|
+
const models = typeof provider.getModels === "function" ? provider.getModels() : provider.models;
|
|
1658
2144
|
return Array.isArray(models) ? models : [];
|
|
1659
2145
|
} catch {
|
|
1660
2146
|
return [];
|
|
1661
2147
|
}
|
|
1662
2148
|
}
|
|
2149
|
+
/**
|
|
2150
|
+
* The environment variable a Pi config value names, when that is all it names.
|
|
2151
|
+
*
|
|
2152
|
+
* Pi's config values (`resolve-config-value.ts`) are `$VAR` / `${VAR}`
|
|
2153
|
+
* interpolation, `!command` execution, or a literal. Only a value that is
|
|
2154
|
+
* exactly one reference can become a DSH credential reference; a command, a
|
|
2155
|
+
* template, or a literal secret must not be copied into settings, which carry
|
|
2156
|
+
* references and never values.
|
|
2157
|
+
* @param value - the package's declared config value, if it declared one.
|
|
2158
|
+
*/
|
|
2159
|
+
function envReferenceOf(value) {
|
|
2160
|
+
if (typeof value !== "string") return void 0;
|
|
2161
|
+
const match = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$|^\$([A-Za-z_][A-Za-z0-9_]*)$/u.exec(value.trim());
|
|
2162
|
+
return match?.[1] ?? match?.[2];
|
|
2163
|
+
}
|
|
1663
2164
|
const PI_CARRIED_FIELDS = [
|
|
1664
2165
|
"api",
|
|
1665
2166
|
"baseUrl",
|
|
@@ -1864,6 +2365,159 @@ function imageAdmissionCompanionAdapter(options) {
|
|
|
1864
2365
|
}
|
|
1865
2366
|
};
|
|
1866
2367
|
}
|
|
2368
|
+
/** A wire protocol DSH's official adapter can serve, or undefined. */
|
|
2369
|
+
function protocolOf(value) {
|
|
2370
|
+
return typeof value === "string" && DSH_PROTOCOLS.has(value) ? value : void 0;
|
|
2371
|
+
}
|
|
2372
|
+
const DSH_PROTOCOLS = /* @__PURE__ */ new Set([
|
|
2373
|
+
"openai-completions",
|
|
2374
|
+
"openai-responses",
|
|
2375
|
+
"anthropic-messages"
|
|
2376
|
+
]);
|
|
2377
|
+
const DSH_MODALITIES = /* @__PURE__ */ new Set(["text", "image"]);
|
|
2378
|
+
const DSH_THINKING_FORMATS = /* @__PURE__ */ new Set([
|
|
2379
|
+
"openai",
|
|
2380
|
+
"deepseek",
|
|
2381
|
+
"openrouter",
|
|
2382
|
+
"together",
|
|
2383
|
+
"zai",
|
|
2384
|
+
"qwen",
|
|
2385
|
+
"string-thinking",
|
|
2386
|
+
"ant-ling"
|
|
2387
|
+
]);
|
|
2388
|
+
/** A positive whole number, or undefined — the profile schema rejects 0 and fractions. */
|
|
2389
|
+
function wholeAbove(value) {
|
|
2390
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : void 0;
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* One Pi model as a DSH model profile.
|
|
2394
|
+
*
|
|
2395
|
+
* Field by field on purpose. The two vocabularies overlap but do not match:
|
|
2396
|
+
* Pi's `maxTokens` is routinely 0 for "unstated" while DSH requires at least 1,
|
|
2397
|
+
* Pi's `compat` carries gateway dialect flags DSH's profile has no slot for,
|
|
2398
|
+
* and Pi's `reasoning` is a boolean where DSH's `reasoningEfforts` is a map of
|
|
2399
|
+
* offered levels. Forwarding any of those verbatim rejects the whole settings
|
|
2400
|
+
* section — one bad key takes down every route in it.
|
|
2401
|
+
* @param model - the Pi model descriptor as its package declared it.
|
|
2402
|
+
* @param api - the wire protocol this route resolved to, which decides whether
|
|
2403
|
+
* the reasoning compat switches are even a legal thing to state.
|
|
2404
|
+
*/
|
|
2405
|
+
function modelProfileOf(model, api) {
|
|
2406
|
+
const compat = model.compat;
|
|
2407
|
+
const reasoningCompatAllowed = api === "openai-completions";
|
|
2408
|
+
const thinkingFormat = reasoningCompatAllowed && typeof compat?.thinkingFormat === "string" && DSH_THINKING_FORMATS.has(compat.thinkingFormat) ? { thinkingFormat: compat.thinkingFormat } : {};
|
|
2409
|
+
const supportsReasoningEffort = reasoningCompatAllowed && typeof compat?.supportsReasoningEffort === "boolean" ? { supportsReasoningEffort: compat.supportsReasoningEffort } : {};
|
|
2410
|
+
const carriedCompat = {
|
|
2411
|
+
...thinkingFormat,
|
|
2412
|
+
...supportsReasoningEffort
|
|
2413
|
+
};
|
|
2414
|
+
const input = Array.isArray(model.input) ? model.input.filter((entry) => typeof entry === "string" && DSH_MODALITIES.has(entry)) : [];
|
|
2415
|
+
const contextWindow = wholeAbove(model.contextWindow);
|
|
2416
|
+
const maxTokens = wholeAbove(model.maxTokens);
|
|
2417
|
+
return {
|
|
2418
|
+
id: String(model.id ?? ""),
|
|
2419
|
+
...typeof model.name === "string" ? { name: model.name } : {},
|
|
2420
|
+
...contextWindow === void 0 ? {} : { contextWindow },
|
|
2421
|
+
...maxTokens === void 0 ? {} : { maxTokens },
|
|
2422
|
+
...input.length === 0 ? {} : { input },
|
|
2423
|
+
...Object.keys(carriedCompat).length === 0 ? {} : { compat: carriedCompat }
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* Serve a catalog-only Pi provider through DSH's OWN llm adapter.
|
|
2428
|
+
*
|
|
2429
|
+
* A Pi package may declare a gateway without shipping any code to call it —
|
|
2430
|
+
* on Pi that is complete, because pi-ai supplies the transport for the wire
|
|
2431
|
+
* protocol the models name. The bridge used to refuse those and tell the user
|
|
2432
|
+
* to configure the gateway by hand in DSH settings, which is the translation
|
|
2433
|
+
* this package exists to do for them.
|
|
2434
|
+
*
|
|
2435
|
+
* The translation is configuration, not transport: the declaration becomes an
|
|
2436
|
+
* `llm-pi-ai` provider profile — the official adapter's own shape, which
|
|
2437
|
+
* carries `api`, `baseURL` and per-model `compat` — and that official plugin
|
|
2438
|
+
* is mounted with it. Nothing here speaks HTTP.
|
|
2439
|
+
* @returns a disposer for the mounted route, or undefined when it cannot be built.
|
|
2440
|
+
*/
|
|
2441
|
+
function registerCatalogOnlyRoute(options) {
|
|
2442
|
+
const { ctx, providerId, provider, host } = options;
|
|
2443
|
+
if (ctx === void 0) return void 0;
|
|
2444
|
+
return mountOfficialAdapter(ctx, providerId, host, () => {
|
|
2445
|
+
const models = providerModels(provider);
|
|
2446
|
+
const first = models[0] ?? {};
|
|
2447
|
+
const apiKeyEnv = envReferenceOf(provider.apiKey);
|
|
2448
|
+
const api = protocolOf(provider.api) ?? protocolOf(first.api);
|
|
2449
|
+
const baseURL = typeof provider.baseUrl === "string" ? provider.baseUrl : typeof first.baseUrl === "string" ? first.baseUrl : void 0;
|
|
2450
|
+
return {
|
|
2451
|
+
displayName: typeof provider.name === "string" ? provider.name : providerId,
|
|
2452
|
+
...apiKeyEnv === void 0 ? {} : { apiKeyEnv },
|
|
2453
|
+
...api === void 0 ? {} : { api },
|
|
2454
|
+
...baseURL === void 0 ? {} : { baseURL },
|
|
2455
|
+
...models.length === 0 ? {} : { models: models.map((model) => modelProfileOf(model, api)) }
|
|
2456
|
+
};
|
|
2457
|
+
}, `[pi2dsh] Pi provider ${JSON.stringify(providerId)} declares a catalog only; DSH's official llm-pi-ai adapter now serves it as a native route`);
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Mount DSH's official llm-pi-ai adapter for one route, rebuildable on demand.
|
|
2461
|
+
* @param buildProfile - called per mount, so a re-announce picks up new facts.
|
|
2462
|
+
* @param announcement - what to tell the user once the route is up.
|
|
2463
|
+
*/
|
|
2464
|
+
function mountOfficialAdapter(ctx, providerId, host, buildProfile, announcement) {
|
|
2465
|
+
let mounted;
|
|
2466
|
+
let disposed = false;
|
|
2467
|
+
let generation = 0;
|
|
2468
|
+
const mount = () => {
|
|
2469
|
+
const profile = buildProfile();
|
|
2470
|
+
const mine = ++generation;
|
|
2471
|
+
(async () => {
|
|
2472
|
+
try {
|
|
2473
|
+
if (await declareInSettings(ctx, providerId, profile)) {
|
|
2474
|
+
if (!disposed && mine === generation) mounted = void 0;
|
|
2475
|
+
host.warn(announcement);
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
const official = await import("@deepseek-ai/dsh-llm-pi-ai");
|
|
2479
|
+
const fiber = await ctx.plugin(official.default ?? official, { providers: { [providerId]: profile } });
|
|
2480
|
+
if (disposed || mine !== generation) fiber?.dispose?.();
|
|
2481
|
+
else mounted = fiber;
|
|
2482
|
+
host.warn(announcement);
|
|
2483
|
+
} catch (error) {
|
|
2484
|
+
host.warn(`[pi2dsh] Pi provider ${JSON.stringify(providerId)} could not be served by the official adapter (${error instanceof Error ? error.message : String(error)}); configure the gateway in the host's llm settings instead`);
|
|
2485
|
+
}
|
|
2486
|
+
})();
|
|
2487
|
+
};
|
|
2488
|
+
mount();
|
|
2489
|
+
const route = (() => {
|
|
2490
|
+
disposed = true;
|
|
2491
|
+
mounted?.dispose?.();
|
|
2492
|
+
});
|
|
2493
|
+
route.reannounce = () => {
|
|
2494
|
+
if (disposed) return;
|
|
2495
|
+
mounted?.dispose?.();
|
|
2496
|
+
mounted = void 0;
|
|
2497
|
+
mount();
|
|
2498
|
+
};
|
|
2499
|
+
return route;
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Hand one provider profile to the official adapter through DSH's settings.
|
|
2503
|
+
*
|
|
2504
|
+
* `ctx.settings.update` merges into the user layer of a namespace its OWNER
|
|
2505
|
+
* registered, so this only works where that adapter is mounted — which is the
|
|
2506
|
+
* normal composition, and exactly where mounting a second copy would collide.
|
|
2507
|
+
* A composition without it (a bare test host, an embedded runtime) rejects and
|
|
2508
|
+
* the caller falls back to mounting the plugin itself.
|
|
2509
|
+
* @returns whether the profile was stored.
|
|
2510
|
+
*/
|
|
2511
|
+
async function declareInSettings(ctx, providerId, profile) {
|
|
2512
|
+
const settings = ctx.get?.("settings");
|
|
2513
|
+
if (typeof settings?.update !== "function") return false;
|
|
2514
|
+
try {
|
|
2515
|
+
await settings.update("llm-pi-ai", { providers: { [providerId]: profile } });
|
|
2516
|
+
return true;
|
|
2517
|
+
} catch {
|
|
2518
|
+
return false;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
1867
2521
|
/**
|
|
1868
2522
|
* Register the provider as a live DSH route when an llm service is mounted.
|
|
1869
2523
|
* A provider carrying its own transport streams through it; a config-only
|
|
@@ -1876,11 +2530,16 @@ function registerPiProviderRoute(options) {
|
|
|
1876
2530
|
const { llm, providerId, provider, host } = options;
|
|
1877
2531
|
if (llm === void 0) return void 0;
|
|
1878
2532
|
if (!providerCarriesTransport(provider)) {
|
|
1879
|
-
|
|
1880
|
-
return;
|
|
2533
|
+
if (providerModels(provider).length === 0 && provider.oauth !== void 0) return void 0;
|
|
2534
|
+
return registerCatalogOnlyRoute(options);
|
|
1881
2535
|
}
|
|
1882
2536
|
try {
|
|
1883
|
-
|
|
2537
|
+
const registration = llm.registerAdapter([providerId], piProviderDshAdapter(providerId, provider, host));
|
|
2538
|
+
const route = (() => {
|
|
2539
|
+
registration();
|
|
2540
|
+
});
|
|
2541
|
+
if (typeof registration.replace === "function") route.reannounce = () => registration.replace?.([providerId]);
|
|
2542
|
+
return route;
|
|
1884
2543
|
} catch (error) {
|
|
1885
2544
|
host.warn(`[pi2dsh] Pi provider ${JSON.stringify(providerId)} was not added as a DSH llm route (${error instanceof Error ? error.message : String(error)}); the existing route keeps the name`);
|
|
1886
2545
|
return;
|
|
@@ -1888,6 +2547,8 @@ function registerPiProviderRoute(options) {
|
|
|
1888
2547
|
}
|
|
1889
2548
|
//#endregion
|
|
1890
2549
|
//#region src/runtime.ts
|
|
2550
|
+
/** Fallback thread ids when a child session reports none. */
|
|
2551
|
+
let sidePanelSerial = 0;
|
|
1891
2552
|
function logger(ctx) {
|
|
1892
2553
|
const candidate = ctx.logger;
|
|
1893
2554
|
return {
|
|
@@ -2153,6 +2814,30 @@ function currentPiModel(state, agent) {
|
|
|
2153
2814
|
reasoning: false
|
|
2154
2815
|
};
|
|
2155
2816
|
}
|
|
2817
|
+
/** A session's durable events, whether the store exposes them as value or method. */
|
|
2818
|
+
function eventsOf(session) {
|
|
2819
|
+
const events = session.events;
|
|
2820
|
+
if (typeof events === "function") return events.call(session) ?? [];
|
|
2821
|
+
return Array.isArray(events) ? events : [];
|
|
2822
|
+
}
|
|
2823
|
+
/** The session id a presentation call belongs to, or '' outside a session. */
|
|
2824
|
+
function sessionIdOf(state, agent) {
|
|
2825
|
+
const session = agentSession(agent ?? currentAgent(state));
|
|
2826
|
+
return session === void 0 ? "" : String(session.id ?? "");
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Record one Pi presentation call against the session that made it.
|
|
2830
|
+
* @param ctx - context, for the logger on an unmounted host.
|
|
2831
|
+
* @param state - runtime state carrying the shared surface registry.
|
|
2832
|
+
* @param agent - the agent whose context the call came through.
|
|
2833
|
+
* @param key - which surface.
|
|
2834
|
+
* @param value - a string or Pi component; undefined clears it.
|
|
2835
|
+
*/
|
|
2836
|
+
function putSurface(ctx, state, agent, key, value) {
|
|
2837
|
+
const surfaces = state.shared.browserSurfaces;
|
|
2838
|
+
if (surfaces === void 0) return;
|
|
2839
|
+
surfaces.setSurface(sessionIdOf(state, agent), state.packageName, key, value, state.theme);
|
|
2840
|
+
}
|
|
2156
2841
|
function thinkingLevelOf(state, agent) {
|
|
2157
2842
|
if (agent !== void 0) {
|
|
2158
2843
|
const scoped = state.thinkingLevels.get(agent);
|
|
@@ -2209,11 +2894,15 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
2209
2894
|
const level = type === "warning" || type === "error" ? "warn" : "info";
|
|
2210
2895
|
logger(ctx)[level](`[pi2dsh] ${text}`);
|
|
2211
2896
|
},
|
|
2212
|
-
select: (title, options) =>
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2897
|
+
select: async (title, options, promptSignal) => {
|
|
2898
|
+
const labels = options.map((option) => String(option));
|
|
2899
|
+
const answered = await askOne(ctx, agent, joinSignals(signal, promptSignal), {
|
|
2900
|
+
id: "pi2dsh-select",
|
|
2901
|
+
question: String(title),
|
|
2902
|
+
options: labels.map((label) => ({ label }))
|
|
2903
|
+
});
|
|
2904
|
+
return answered === void 0 ? void 0 : resolveOfferedChoice(answered, labels);
|
|
2905
|
+
},
|
|
2217
2906
|
async confirm(title, message) {
|
|
2218
2907
|
return await askOne(ctx, agent, signal, {
|
|
2219
2908
|
id: "pi2dsh-confirm",
|
|
@@ -2222,7 +2911,7 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
2222
2911
|
options: [{ label: "Yes" }, { label: "No" }]
|
|
2223
2912
|
}) === "Yes";
|
|
2224
2913
|
},
|
|
2225
|
-
input: (title, placeholder) => askOne(ctx, agent, signal, {
|
|
2914
|
+
input: (title, placeholder, promptSignal) => askOne(ctx, agent, joinSignals(signal, promptSignal), {
|
|
2226
2915
|
id: "pi2dsh-input",
|
|
2227
2916
|
question: String(title),
|
|
2228
2917
|
...placeholder === void 0 ? {} : { detail: String(placeholder) }
|
|
@@ -2232,24 +2921,45 @@ function contextFor(ctx, state, agent, signal, command = false, sessionOverride)
|
|
|
2232
2921
|
question: String(title),
|
|
2233
2922
|
...prefill === void 0 ? {} : { detail: `Current text:\n${String(prefill)}` }
|
|
2234
2923
|
}),
|
|
2235
|
-
setStatus: () =>
|
|
2236
|
-
|
|
2924
|
+
setStatus: (key, text) => {
|
|
2925
|
+
const surfaces = state.shared.browserSurfaces;
|
|
2926
|
+
if (surfaces === void 0) return;
|
|
2927
|
+
surfaces.setStatus(sessionIdOf(state, agent), state.packageName, String(key), text);
|
|
2928
|
+
},
|
|
2929
|
+
setWidget: (key, content) => {
|
|
2930
|
+
const surfaces = state.shared.browserSurfaces;
|
|
2931
|
+
if (surfaces === void 0) return;
|
|
2932
|
+
surfaces.setWidget(sessionIdOf(state, agent), state.packageName, String(key), content, state.theme);
|
|
2933
|
+
},
|
|
2237
2934
|
onTerminalInput: () => () => void 0,
|
|
2238
|
-
setWorkingMessage: () =>
|
|
2239
|
-
setWorkingVisible: () =>
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2935
|
+
setWorkingMessage: (message) => putSurface(ctx, state, agent, "workingMessage", message),
|
|
2936
|
+
setWorkingVisible: (visible) => {
|
|
2937
|
+
const surfaces = state.shared.browserSurfaces;
|
|
2938
|
+
if (surfaces === void 0) return;
|
|
2939
|
+
surfaces.setWorkingVisible(sessionIdOf(state, agent), state.packageName, visible !== false);
|
|
2940
|
+
},
|
|
2941
|
+
setWorkingIndicator: (options) => putSurface(ctx, state, agent, "workingIndicator", options),
|
|
2942
|
+
setHiddenThinkingLabel: (label) => putSurface(ctx, state, agent, "hiddenThinkingLabel", label),
|
|
2943
|
+
setFooter: (factory) => putSurface(ctx, state, agent, "footer", factory),
|
|
2944
|
+
setHeader: (factory) => putSurface(ctx, state, agent, "header", factory),
|
|
2945
|
+
setTitle: (title) => putSurface(ctx, state, agent, "title", title),
|
|
2245
2946
|
custom: async () => void 0,
|
|
2246
2947
|
pasteToEditor(text) {
|
|
2247
|
-
|
|
2948
|
+
const surfaces = state.shared.browserSurfaces;
|
|
2949
|
+
const sessionId = sessionIdOf(state, agent);
|
|
2950
|
+
const next = (surfaces?.liveDraft(sessionId) ?? (agent === void 0 ? "" : state.editorBuffers.get(agent) ?? "")) + String(text);
|
|
2951
|
+
if (agent !== void 0) state.editorBuffers.set(agent, next);
|
|
2952
|
+
surfaces?.requestDraft(sessionId, next);
|
|
2248
2953
|
},
|
|
2249
2954
|
setEditorText(text) {
|
|
2250
2955
|
if (agent !== void 0) state.editorBuffers.set(agent, String(text));
|
|
2956
|
+
state.shared.browserSurfaces?.requestDraft(sessionIdOf(state, agent), String(text));
|
|
2957
|
+
},
|
|
2958
|
+
getEditorText: () => {
|
|
2959
|
+
const live = state.shared.browserSurfaces?.liveDraft(sessionIdOf(state, agent)) ?? "";
|
|
2960
|
+
if (live.length > 0) return live;
|
|
2961
|
+
return agent === void 0 ? "" : state.editorBuffers.get(agent) ?? "";
|
|
2251
2962
|
},
|
|
2252
|
-
getEditorText: () => agent === void 0 ? "" : state.editorBuffers.get(agent) ?? "",
|
|
2253
2963
|
addAutocompleteProvider(factory) {
|
|
2254
2964
|
state.autocompleteProviders.push(factory);
|
|
2255
2965
|
},
|
|
@@ -3082,7 +3792,7 @@ function subscribeInterceptors(ctx, state) {
|
|
|
3082
3792
|
});
|
|
3083
3793
|
const systemPrompt = optionalService(ctx, "systemPrompt");
|
|
3084
3794
|
if (systemPrompt !== void 0) ctx.effect(() => systemPrompt.section({
|
|
3085
|
-
name:
|
|
3795
|
+
name: `pi2dsh:tool-guidance:${state.packageName ?? "pi"}`,
|
|
3086
3796
|
order: 150,
|
|
3087
3797
|
text: () => piToolPromptContribution(ctx, state)
|
|
3088
3798
|
}));
|
|
@@ -3299,6 +4009,7 @@ function sharedHostStateOf(ctx) {
|
|
|
3299
4009
|
shared = {
|
|
3300
4010
|
companionRoutes: /* @__PURE__ */ new Map(),
|
|
3301
4011
|
providerRouteDisposers: /* @__PURE__ */ new Map(),
|
|
4012
|
+
publishedOAuthKeys: /* @__PURE__ */ new Map(),
|
|
3302
4013
|
providers: /* @__PURE__ */ new Map()
|
|
3303
4014
|
};
|
|
3304
4015
|
SHARED_HOST_STATE.set(key, shared);
|
|
@@ -3437,6 +4148,205 @@ function ensureLoginCommand(ctx, state) {
|
|
|
3437
4148
|
logger(ctx).warn(`[pi2dsh] /login is already registered by an earlier package in this host; this package's providers use that command (${error instanceof Error ? error.message : String(error)})`);
|
|
3438
4149
|
}
|
|
3439
4150
|
}
|
|
4151
|
+
/**
|
|
4152
|
+
* Turn what the user answered into one of the offered options.
|
|
4153
|
+
* @param answer - the label, a differently-cased label, or a 1-based position.
|
|
4154
|
+
* @param offered - the options as they were shown, in order.
|
|
4155
|
+
* @returns the matching option, or undefined when nothing matches.
|
|
4156
|
+
*/
|
|
4157
|
+
function resolveOfferedChoice(answer, offered) {
|
|
4158
|
+
const trimmed = answer.trim();
|
|
4159
|
+
if (offered.includes(trimmed)) return trimmed;
|
|
4160
|
+
const insensitive = offered.find((name) => name.toLowerCase() === trimmed.toLowerCase());
|
|
4161
|
+
if (insensitive !== void 0) return insensitive;
|
|
4162
|
+
if (/^\d+$/u.test(trimmed)) {
|
|
4163
|
+
const at = Number(trimmed) - 1;
|
|
4164
|
+
if (at >= 0 && at < offered.length) return offered[at];
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
/**
|
|
4168
|
+
* Cancel the login already in flight, if any, and wait for it to let go.
|
|
4169
|
+
*
|
|
4170
|
+
* Waiting matters: the next flow binds the same fixed callback port, and Pi's
|
|
4171
|
+
* flow answers a taken port by silently degrading to a listener that never
|
|
4172
|
+
* receives a code. Starting the new flow before the old one has closed
|
|
4173
|
+
* reproduces exactly that.
|
|
4174
|
+
* @param state - the mounting package's state, holding the shared host state.
|
|
4175
|
+
* @returns the cancelled provider's name, or undefined when nothing was running.
|
|
4176
|
+
*/
|
|
4177
|
+
async function supersedeActiveLogin(state) {
|
|
4178
|
+
const previous = state.shared.activeLogin;
|
|
4179
|
+
if (previous === void 0) return void 0;
|
|
4180
|
+
state.shared.activeLogin = void 0;
|
|
4181
|
+
previous.controller.abort();
|
|
4182
|
+
for (const path of previous.published) revokeAuthorization(path);
|
|
4183
|
+
let timer;
|
|
4184
|
+
const released = await Promise.race([previous.finished.then(() => true), new Promise((resolve) => {
|
|
4185
|
+
timer = setTimeout(() => resolve(false), 5e3);
|
|
4186
|
+
})]);
|
|
4187
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4188
|
+
if (!released) throw new Error(`the ${previous.providerName} login is still shutting down; try /login again in a moment`);
|
|
4189
|
+
return previous.providerName;
|
|
4190
|
+
}
|
|
4191
|
+
/**
|
|
4192
|
+
* Give a logged-in provider a route, if nothing else already gave it one.
|
|
4193
|
+
*
|
|
4194
|
+
* Pi's built-in OAuth providers (openai-codex, anthropic, github-copilot,
|
|
4195
|
+
* kimi-coding) are seeded so `/login` can offer them, but they carry no
|
|
4196
|
+
* transport and no catalog — so the registration path above never builds them
|
|
4197
|
+
* a route, and a successful login used to leave the model picker unchanged.
|
|
4198
|
+
* A stored credential is the whole precondition: with one, the models pi-ai's
|
|
4199
|
+
* installed catalog describes become selectable through DSH's own adapter.
|
|
4200
|
+
* @param name - the Pi provider id, which is also its DSH route name.
|
|
4201
|
+
* @param config - the provider config, whose oauth block made it loginable.
|
|
4202
|
+
* @returns whether this call put a route in place.
|
|
4203
|
+
*/
|
|
4204
|
+
async function ensureLoggedInProviderRoute(ctx, state, name, config) {
|
|
4205
|
+
if (!providerSupportsOAuth(config)) return false;
|
|
4206
|
+
if (providerCarriesTransport(config) || state.providerRouteDisposers.has(name)) return false;
|
|
4207
|
+
if (llmOf(ctx)?.listProviders().some((provider) => provider.id === name) === true) return false;
|
|
4208
|
+
if (await storedOAuthCredential(oauthStoreOf(state), name).catch(() => void 0) === void 0) return false;
|
|
4209
|
+
const settings = optionalService(ctx, "settings");
|
|
4210
|
+
if (settings === void 0) {
|
|
4211
|
+
logger(ctx).warn(`[pi2dsh] logged in to ${JSON.stringify(name)}, but this composition has no settings service to declare its model route in`);
|
|
4212
|
+
return false;
|
|
4213
|
+
}
|
|
4214
|
+
const published = await publishOAuthCredential(ctx, state, name, config);
|
|
4215
|
+
if (!published.ok) return false;
|
|
4216
|
+
keepOAuthCredentialFresh(ctx, state);
|
|
4217
|
+
await settings.update("llm-pi-ai", { providers: { [name]: {
|
|
4218
|
+
...typeof config.name === "string" ? { displayName: config.name } : {},
|
|
4219
|
+
...published.baseUrl === void 0 ? {} : { baseURL: published.baseUrl },
|
|
4220
|
+
apiKeyEnv: oauthCredentialRef(name)
|
|
4221
|
+
} } });
|
|
4222
|
+
return true;
|
|
4223
|
+
}
|
|
4224
|
+
/**
|
|
4225
|
+
* Put a logged-in provider's current key where the official adapter reads it.
|
|
4226
|
+
*
|
|
4227
|
+
* Pi keeps the OAuth credential (access + refresh + expiry) in its own store;
|
|
4228
|
+
* DSH's adapter reads one opaque value through the credentials seam. This is
|
|
4229
|
+
* the join: Pi's resolution runs — including its double-checked-lock refresh
|
|
4230
|
+
* when the token is inside the five-minute window — and whatever key that
|
|
4231
|
+
* yields is stored under the reference the profile names.
|
|
4232
|
+
* @returns whether a key is now behind the reference, plus any endpoint the
|
|
4233
|
+
* credential itself decided (some providers read their host out of the token).
|
|
4234
|
+
*/
|
|
4235
|
+
async function publishOAuthCredential(ctx, state, name, config) {
|
|
4236
|
+
const credentials = optionalService(ctx, "credentials");
|
|
4237
|
+
if (credentials === void 0) {
|
|
4238
|
+
logger(ctx).warn(`[pi2dsh] logged in to ${JSON.stringify(name)}, but this composition has no credentials service to hand the token to`);
|
|
4239
|
+
return { ok: false };
|
|
4240
|
+
}
|
|
4241
|
+
const resolvedAuth = (await resolvePiProviderAuth({
|
|
4242
|
+
providerId: name,
|
|
4243
|
+
providerConfig: config,
|
|
4244
|
+
store: oauthStoreOf(state)
|
|
4245
|
+
}).catch(() => void 0))?.auth;
|
|
4246
|
+
const key = typeof resolvedAuth?.apiKey === "string" ? resolvedAuth.apiKey : void 0;
|
|
4247
|
+
if (key === void 0 || key.length === 0) {
|
|
4248
|
+
logger(ctx).warn(`[pi2dsh] logged in to ${JSON.stringify(name)}, but this provider authenticates with a request header rather than an api key — DSH routes name a credential reference, so no model route was added for it`);
|
|
4249
|
+
return { ok: false };
|
|
4250
|
+
}
|
|
4251
|
+
const ref = oauthCredentialRef(name);
|
|
4252
|
+
const endpoint = typeof resolvedAuth?.baseUrl === "string" ? { baseUrl: resolvedAuth.baseUrl } : {};
|
|
4253
|
+
if ((await credentials.describe(ref).catch(() => void 0))?.writable === false) return {
|
|
4254
|
+
ok: true,
|
|
4255
|
+
...endpoint
|
|
4256
|
+
};
|
|
4257
|
+
if (state.publishedOAuthKeys.get(name) === key) return {
|
|
4258
|
+
ok: true,
|
|
4259
|
+
...endpoint
|
|
4260
|
+
};
|
|
4261
|
+
await credentials.set(ref, key);
|
|
4262
|
+
state.publishedOAuthKeys.set(name, key);
|
|
4263
|
+
return {
|
|
4264
|
+
ok: true,
|
|
4265
|
+
...endpoint
|
|
4266
|
+
};
|
|
4267
|
+
}
|
|
4268
|
+
/**
|
|
4269
|
+
* Re-publish rotating OAuth keys at the moment a request needs them.
|
|
4270
|
+
*
|
|
4271
|
+
* An access token expires (codex's in about an hour), so a value stored once at
|
|
4272
|
+
* login goes stale and every later request fails on a credential the user
|
|
4273
|
+
* believes they supplied. DSH's own credentials contract is per-operation for
|
|
4274
|
+
* exactly this reason, and `llm/stream` is the per-operation seam.
|
|
4275
|
+
*
|
|
4276
|
+
* Whether the refresh is awaited depends on whether this request can still be
|
|
4277
|
+
* served by what is stored. Inside Pi's refresh window the stored token is
|
|
4278
|
+
* still VALID, so refreshing beside the request costs nothing and the rotated
|
|
4279
|
+
* key is there for the next one. Past expiry it is not: publishing in the
|
|
4280
|
+
* background there means the first request after an idle stretch goes out with
|
|
4281
|
+
* a dead token and fails, every time, and only the second one works. So an
|
|
4282
|
+
* expired credential blocks its own request until it has been renewed.
|
|
4283
|
+
*/
|
|
4284
|
+
function keepOAuthCredentialFresh(ctx, state) {
|
|
4285
|
+
if (state.shared.oauthRefreshHooked === true) return;
|
|
4286
|
+
state.shared.oauthRefreshHooked = true;
|
|
4287
|
+
ctx.on("llm/stream", ((options, next) => {
|
|
4288
|
+
const provider = typeof options.provider === "string" ? options.provider : void 0;
|
|
4289
|
+
const config = provider === void 0 ? void 0 : state.providers.get(provider);
|
|
4290
|
+
if (provider === void 0 || config === void 0 || !providerSupportsOAuth(config)) return next();
|
|
4291
|
+
const publish = () => publishOAuthCredential(ctx, state, provider, config).catch((error) => logger(ctx).warn(`[pi2dsh] could not refresh the stored credential for ${JSON.stringify(provider)}: ${error instanceof Error ? error.message : String(error)}`));
|
|
4292
|
+
return (async function* () {
|
|
4293
|
+
const stored = await storedOAuthCredential(oauthStoreOf(state), provider).catch(() => void 0);
|
|
4294
|
+
const expires = typeof stored?.expires === "number" ? stored.expires : void 0;
|
|
4295
|
+
if (expires === void 0 || expires <= Date.now()) await publish();
|
|
4296
|
+
else publish();
|
|
4297
|
+
yield* next();
|
|
4298
|
+
})();
|
|
4299
|
+
}));
|
|
4300
|
+
}
|
|
4301
|
+
/**
|
|
4302
|
+
* Ask a provider package to discover its models, then let the host see them.
|
|
4303
|
+
*
|
|
4304
|
+
* Gateway discovery needs a credential, and a credential is not always there
|
|
4305
|
+
* when the provider registers: OAuth providers get theirs from a `/login` the
|
|
4306
|
+
* user runs later. So this is called at registration AND after a login — the
|
|
4307
|
+
* two moments a credential can appear — and it ends by re-announcing the
|
|
4308
|
+
* route, because DSH's directory observers (the model picker among them)
|
|
4309
|
+
* re-read on the route-set notification and nothing else.
|
|
4310
|
+
* @param name - the Pi provider id, which is also its DSH route name.
|
|
4311
|
+
* @param value - the provider config the package registered.
|
|
4312
|
+
* @returns how many models the route lists afterwards, or undefined when the
|
|
4313
|
+
* package has no discovery of its own.
|
|
4314
|
+
*/
|
|
4315
|
+
async function discoverProviderModels(ctx, state, name, value) {
|
|
4316
|
+
const refreshModels = value.refreshModels;
|
|
4317
|
+
if (typeof refreshModels !== "function") return void 0;
|
|
4318
|
+
try {
|
|
4319
|
+
const apiKey = ((await resolvePiProviderAuth({
|
|
4320
|
+
providerId: name,
|
|
4321
|
+
providerConfig: value,
|
|
4322
|
+
store: oauthStoreOf(state)
|
|
4323
|
+
}).catch(() => void 0))?.auth)?.apiKey;
|
|
4324
|
+
await Promise.resolve(refreshModels.call(value, {
|
|
4325
|
+
stored: void 0,
|
|
4326
|
+
...apiKey === void 0 ? {} : { credential: {
|
|
4327
|
+
type: "api_key",
|
|
4328
|
+
key: apiKey
|
|
4329
|
+
} },
|
|
4330
|
+
store: {
|
|
4331
|
+
read: async () => void 0,
|
|
4332
|
+
write: async () => {},
|
|
4333
|
+
delete: async () => {}
|
|
4334
|
+
},
|
|
4335
|
+
allowNetwork: true,
|
|
4336
|
+
signal: new AbortController().signal,
|
|
4337
|
+
publish: async (publication) => {
|
|
4338
|
+
publication.update?.();
|
|
4339
|
+
return true;
|
|
4340
|
+
}
|
|
4341
|
+
}));
|
|
4342
|
+
} catch (error) {
|
|
4343
|
+
logger(ctx).warn(`[pi2dsh] model catalog refresh for Pi provider ${JSON.stringify(name)} failed (its registry entries stay static): ${error instanceof Error ? error.message : String(error)}`);
|
|
4344
|
+
return;
|
|
4345
|
+
}
|
|
4346
|
+
state.modelCatalog?.refresh();
|
|
4347
|
+
state.providerRouteDisposers.get(name)?.reannounce?.();
|
|
4348
|
+
return await llmOf(ctx)?.listModels(name).then((models) => models.length).catch(() => void 0);
|
|
4349
|
+
}
|
|
3440
4350
|
function registerLoginCommand(ctx, state) {
|
|
3441
4351
|
registerCommand(ctx, state, {
|
|
3442
4352
|
name: "login",
|
|
@@ -3446,22 +4356,55 @@ function registerLoginCommand(ctx, state) {
|
|
|
3446
4356
|
const oauthProviders = [...state.providers.entries()].filter(([, config]) => providerSupportsOAuth(config)).map(([name]) => name);
|
|
3447
4357
|
if (oauthProviders.length === 0) throw new Error("no registered Pi provider supports OAuth login");
|
|
3448
4358
|
const ui = commandContext.ui;
|
|
3449
|
-
let
|
|
3450
|
-
if (
|
|
3451
|
-
|
|
3452
|
-
|
|
4359
|
+
let answer = args.trim().split(/\s+/u)[0] ?? "";
|
|
4360
|
+
if (answer.length === 0) answer = oauthProviders.length === 1 ? oauthProviders[0] : String(await ui.select("Log in to which provider?", oauthProviders) ?? "");
|
|
4361
|
+
if (answer.length === 0) return "Login cancelled";
|
|
4362
|
+
const providerId = resolveOfferedChoice(answer, oauthProviders);
|
|
4363
|
+
const config = providerId === void 0 ? void 0 : state.providers.get(providerId);
|
|
4364
|
+
if (providerId === void 0 || config === void 0 || !providerSupportsOAuth(config)) throw new Error(`unknown OAuth provider ${JSON.stringify(answer)}; available: ${oauthProviders.join(", ")}`);
|
|
3453
4365
|
const oauthName = config.oauth?.name ?? providerId;
|
|
3454
4366
|
const commandSignal = commandContext.signal;
|
|
3455
|
-
await
|
|
4367
|
+
const superseded = await supersedeActiveLogin(state);
|
|
4368
|
+
if (superseded !== void 0) ui.notify(`Cancelled the ${superseded} login that was still waiting.`);
|
|
4369
|
+
const active = {
|
|
4370
|
+
providerName: oauthName,
|
|
4371
|
+
controller: new AbortController(),
|
|
4372
|
+
finished: Promise.resolve(),
|
|
4373
|
+
published: []
|
|
4374
|
+
};
|
|
4375
|
+
if (commandSignal !== void 0) {
|
|
4376
|
+
if (commandSignal.aborted) active.controller.abort();
|
|
4377
|
+
else commandSignal.addEventListener("abort", () => active.controller.abort(), { once: true });
|
|
4378
|
+
}
|
|
4379
|
+
const attempt = loginPiProvider({
|
|
4380
|
+
open: openBrowser,
|
|
4381
|
+
shorten: (url) => {
|
|
4382
|
+
const path = publishAuthorization(url);
|
|
4383
|
+
const web = ctx.get("webServer");
|
|
4384
|
+
if (path === void 0 || web?.port === void 0) return void 0;
|
|
4385
|
+
active.published.push(path);
|
|
4386
|
+
return `http://${web.host === "0.0.0.0" || web.host === void 0 ? "127.0.0.1" : web.host}:${web.port}${path}`;
|
|
4387
|
+
},
|
|
3456
4388
|
providerId,
|
|
3457
4389
|
providerName: oauthName,
|
|
3458
4390
|
providerConfig: config,
|
|
3459
4391
|
store: oauthStoreOf(state),
|
|
3460
4392
|
ui,
|
|
3461
|
-
|
|
4393
|
+
signal: active.controller.signal
|
|
3462
4394
|
});
|
|
3463
|
-
|
|
3464
|
-
|
|
4395
|
+
active.finished = attempt.then(() => void 0, () => void 0);
|
|
4396
|
+
state.shared.activeLogin = active;
|
|
4397
|
+
try {
|
|
4398
|
+
await attempt;
|
|
4399
|
+
} finally {
|
|
4400
|
+
for (const path of active.published) revokeAuthorization(path);
|
|
4401
|
+
if (state.shared.activeLogin === active) state.shared.activeLogin = void 0;
|
|
4402
|
+
}
|
|
4403
|
+
const declared = await ensureLoggedInProviderRoute(ctx, state, providerId, config);
|
|
4404
|
+
const discovered = await discoverProviderModels(ctx, state, providerId, config) ?? (declared ? await llmOf(ctx)?.listModels(providerId).then((list) => list.length).catch(() => void 0) : void 0);
|
|
4405
|
+
const models = discovered === void 0 ? "" : `; ${discovered} models available`;
|
|
4406
|
+
ui.notify(`Logged in to ${oauthName}${models}`);
|
|
4407
|
+
return `Logged in to ${oauthName}${models}`;
|
|
3465
4408
|
}
|
|
3466
4409
|
});
|
|
3467
4410
|
}
|
|
@@ -3887,6 +4830,7 @@ function createPiApi(ctx, state) {
|
|
|
3887
4830
|
}
|
|
3888
4831
|
const routeDisposer = registerPiProviderRoute({
|
|
3889
4832
|
llm: llmOf(ctx),
|
|
4833
|
+
ctx,
|
|
3890
4834
|
providerId: name,
|
|
3891
4835
|
provider: value,
|
|
3892
4836
|
host: {
|
|
@@ -3902,35 +4846,9 @@ function createPiApi(ctx, state) {
|
|
|
3902
4846
|
if (routeDisposer !== void 0) {
|
|
3903
4847
|
state.providerRouteDisposers.set(name, routeDisposer);
|
|
3904
4848
|
state.modelCatalog?.refresh();
|
|
3905
|
-
logger(ctx).info(`[pi2dsh] Pi provider ${JSON.stringify(name)} registered as a native DSH llm route`);
|
|
4849
|
+
if (providerCarriesTransport(value)) logger(ctx).info(`[pi2dsh] Pi provider ${JSON.stringify(name)} registered as a native DSH llm route`);
|
|
3906
4850
|
} else if (!providerSupportsOAuth(value)) logger(ctx).info(`[pi2dsh] recorded Pi provider ${JSON.stringify(name)}; model calls stay on DSH llm adapters`);
|
|
3907
|
-
|
|
3908
|
-
if (typeof refreshModels === "function") (async () => {
|
|
3909
|
-
const apiKey = ((await resolvePiProviderAuth({
|
|
3910
|
-
providerId: name,
|
|
3911
|
-
providerConfig: value,
|
|
3912
|
-
store: oauthStoreOf(state)
|
|
3913
|
-
}).catch(() => void 0))?.auth)?.apiKey;
|
|
3914
|
-
await Promise.resolve(refreshModels.call(value, {
|
|
3915
|
-
stored: void 0,
|
|
3916
|
-
...apiKey === void 0 ? {} : { credential: {
|
|
3917
|
-
type: "api_key",
|
|
3918
|
-
key: apiKey
|
|
3919
|
-
} },
|
|
3920
|
-
store: {
|
|
3921
|
-
read: async () => void 0,
|
|
3922
|
-
write: async () => {},
|
|
3923
|
-
delete: async () => {}
|
|
3924
|
-
},
|
|
3925
|
-
allowNetwork: true,
|
|
3926
|
-
signal: new AbortController().signal,
|
|
3927
|
-
publish: async (publication) => {
|
|
3928
|
-
publication.update?.();
|
|
3929
|
-
return true;
|
|
3930
|
-
}
|
|
3931
|
-
}));
|
|
3932
|
-
state.modelCatalog?.refresh();
|
|
3933
|
-
})().catch((error) => logger(ctx).warn(`[pi2dsh] model catalog refresh for Pi provider ${JSON.stringify(name)} failed (its registry entries stay static): ${error instanceof Error ? error.message : String(error)}`));
|
|
4851
|
+
discoverProviderModels(ctx, state, name, value);
|
|
3934
4852
|
},
|
|
3935
4853
|
unregisterProvider(name) {
|
|
3936
4854
|
state.providers.delete(name);
|
|
@@ -4141,6 +5059,20 @@ async function loadExtensions(rootDir, manifest, api, onExtensionError, onCapabi
|
|
|
4141
5059
|
aliases[`${family}/pi-ai/compat`] = aiShim;
|
|
4142
5060
|
aliases[`${family}/pi-ai/oauth`] = aiShim;
|
|
4143
5061
|
aliases[`${family}/pi-ai/providers/all`] = aiShim;
|
|
5062
|
+
aliases[`${family}/pi-ai/bedrock-provider`] = aiShim;
|
|
5063
|
+
aliases[`${family}/pi-ai/bun-oauth`] = aiShim;
|
|
5064
|
+
for (const api of [
|
|
5065
|
+
"anthropic-messages",
|
|
5066
|
+
"openai-completions",
|
|
5067
|
+
"openai-responses",
|
|
5068
|
+
"openai-codex-responses",
|
|
5069
|
+
"azure-openai-responses",
|
|
5070
|
+
"google-generative-ai",
|
|
5071
|
+
"google-vertex",
|
|
5072
|
+
"mistral-conversations",
|
|
5073
|
+
"bedrock-converse-stream",
|
|
5074
|
+
"pi-messages"
|
|
5075
|
+
]) aliases[`${family}/pi-ai/api/${api}`] = aiShim;
|
|
4144
5076
|
}
|
|
4145
5077
|
const require = createRequire(import.meta.url);
|
|
4146
5078
|
for (const entry of [
|
|
@@ -4233,7 +5165,8 @@ async function applyPiPackage(ctx, options) {
|
|
|
4233
5165
|
argMutations: /* @__PURE__ */ new WeakMap(),
|
|
4234
5166
|
streamingTexts: /* @__PURE__ */ new Map(),
|
|
4235
5167
|
lastLoggedModels: /* @__PURE__ */ new WeakMap(),
|
|
4236
|
-
providerRouteDisposers: shared.providerRouteDisposers
|
|
5168
|
+
providerRouteDisposers: shared.providerRouteDisposers,
|
|
5169
|
+
publishedOAuthKeys: shared.publishedOAuthKeys
|
|
4237
5170
|
};
|
|
4238
5171
|
subscribeLifecycle(ctx, state);
|
|
4239
5172
|
subscribeInterceptors(ctx, state);
|
|
@@ -4243,6 +5176,7 @@ async function applyPiPackage(ctx, options) {
|
|
|
4243
5176
|
oauth: provider.auth.oauth
|
|
4244
5177
|
});
|
|
4245
5178
|
ensureLoginCommand(ctx, state);
|
|
5179
|
+
for (const [name, config] of [...state.providers]) ensureLoggedInProviderRoute(ctx, state, name, config).catch((error) => logger(ctx).warn(`[pi2dsh] could not restore the route for logged-in provider ${JSON.stringify(name)}: ${error instanceof Error ? error.message : String(error)}`));
|
|
4246
5180
|
if (options.manifest.skillDirs.length > 0) {
|
|
4247
5181
|
if (ctx.get("skills") === void 0) logger(ctx).warn("[pi2dsh] migrated skills were not mounted because this DSH composition has no ctx.skills");
|
|
4248
5182
|
else {
|
|
@@ -4273,7 +5207,79 @@ async function applyPiPackage(ctx, options) {
|
|
|
4273
5207
|
}));
|
|
4274
5208
|
ctx.effect(() => () => __setPiAiLlmBridge(void 0));
|
|
4275
5209
|
}
|
|
4276
|
-
|
|
5210
|
+
const browserSurfaces = state.shared.browserSurfaces ??= new BrowserSurfaces();
|
|
5211
|
+
if (state.shared.browserSurfacesRouted !== true) state.shared.browserSurfacesRouted = registerBrowserSurfaceRoute(ctx, browserSurfaces);
|
|
5212
|
+
ctx.effect(() => browserSurfaces.trackCompletions(state.packageName ?? "pi", async (trigger, query) => {
|
|
5213
|
+
if (state.autocompleteProviders.length === 0) return [];
|
|
5214
|
+
let provider = { getSuggestions: async () => null };
|
|
5215
|
+
for (const factory of state.autocompleteProviders) {
|
|
5216
|
+
if (typeof factory !== "function") continue;
|
|
5217
|
+
provider = factory(provider) ?? provider;
|
|
5218
|
+
}
|
|
5219
|
+
const getSuggestions = provider.getSuggestions;
|
|
5220
|
+
if (typeof getSuggestions !== "function") return [];
|
|
5221
|
+
const line = `${trigger}${query}`;
|
|
5222
|
+
return ((await getSuggestions([line], 0, line.length, { signal: new AbortController().signal }))?.items ?? []).filter((item) => typeof item.value === "string").map((item) => ({
|
|
5223
|
+
value: String(item.value),
|
|
5224
|
+
label: String(item.label ?? item.value),
|
|
5225
|
+
...typeof item.description === "string" ? { description: item.description } : {}
|
|
5226
|
+
}));
|
|
5227
|
+
}));
|
|
5228
|
+
ctx.effect(() => browserSurfaces.trackEntries(state.packageName ?? "pi", (sessionId) => {
|
|
5229
|
+
const rendered = [];
|
|
5230
|
+
const draw = (renderer, subject) => typeof renderer !== "function" ? void 0 : surfaceText(renderer(subject, {}, state.theme), state.theme);
|
|
5231
|
+
for (const entry of state.bridge.customEntries(sessionId)) {
|
|
5232
|
+
const text = draw(state.entryRenderers.get(entry.customType), {
|
|
5233
|
+
type: "custom",
|
|
5234
|
+
id: entry.id,
|
|
5235
|
+
customType: entry.customType,
|
|
5236
|
+
data: entry.data,
|
|
5237
|
+
timestamp: entry.timestamp
|
|
5238
|
+
});
|
|
5239
|
+
if (text !== void 0) rendered.push({
|
|
5240
|
+
id: entry.id,
|
|
5241
|
+
customType: entry.customType,
|
|
5242
|
+
text
|
|
5243
|
+
});
|
|
5244
|
+
}
|
|
5245
|
+
if (state.messageRenderers.size > 0) {
|
|
5246
|
+
const session = optionalService(ctx, "sessions")?.get(sessionId);
|
|
5247
|
+
const events = session === void 0 ? [] : eventsOf(session);
|
|
5248
|
+
for (const event of events) {
|
|
5249
|
+
const data = event.data ?? {};
|
|
5250
|
+
const customType = data.source?.piCustomType;
|
|
5251
|
+
if (typeof customType !== "string") continue;
|
|
5252
|
+
const text = draw(state.messageRenderers.get(customType), {
|
|
5253
|
+
role: "custom",
|
|
5254
|
+
customType,
|
|
5255
|
+
content: data.content,
|
|
5256
|
+
id: `dsh-${String(event.seq ?? "")}`
|
|
5257
|
+
});
|
|
5258
|
+
if (text !== void 0) rendered.push({
|
|
5259
|
+
id: `dsh-${String(event.seq ?? "")}`,
|
|
5260
|
+
customType,
|
|
5261
|
+
text
|
|
5262
|
+
});
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
return rendered;
|
|
5266
|
+
}));
|
|
5267
|
+
__setSubagentSessionFactory(async (subagentOptions) => {
|
|
5268
|
+
const created = await createBridgedAgentSession(subagentHost(), subagentOptions);
|
|
5269
|
+
const parent = agentSession(currentAgent(state));
|
|
5270
|
+
const parentId = parent === void 0 ? "" : String(parent.id ?? "");
|
|
5271
|
+
if (parentId.length > 0) {
|
|
5272
|
+
const dispose = browserSurfaces.track(parentId, {
|
|
5273
|
+
id: String(created.session.sessionId ?? "") || `pi2dsh-thread-${parentId}-${sidePanelSerial++}`,
|
|
5274
|
+
label: childLabel(subagentOptions.label, state.packageName),
|
|
5275
|
+
package: state.packageName,
|
|
5276
|
+
session: created.session
|
|
5277
|
+
});
|
|
5278
|
+
ctx.effect(() => dispose);
|
|
5279
|
+
}
|
|
5280
|
+
return created;
|
|
5281
|
+
});
|
|
5282
|
+
const subagentHost = () => ({
|
|
4277
5283
|
cordis: ctx,
|
|
4278
5284
|
cwd: () => cwdOf(currentAgent(state)),
|
|
4279
5285
|
parentSessionId: () => {
|
|
@@ -4292,7 +5298,7 @@ async function applyPiPackage(ctx, options) {
|
|
|
4292
5298
|
messageFromSessionEvent: (event) => messageFromSessionEvent(ctx, event),
|
|
4293
5299
|
messageSource: state.messageSource,
|
|
4294
5300
|
packageName: state.packageName
|
|
4295
|
-
}
|
|
5301
|
+
});
|
|
4296
5302
|
ctx.effect(() => () => __setSubagentSessionFactory(void 0));
|
|
4297
5303
|
await registerPromptCommands(ctx, state, rootDir, options.manifest);
|
|
4298
5304
|
const onExtensionError = (failure) => logger(ctx).warn(`[pi2dsh] extension entry failed and was skipped (matching Pi's per-extension error isolation): ${failure}`);
|
|
@@ -4326,14 +5332,16 @@ async function applyPiPackage(ctx, options) {
|
|
|
4326
5332
|
}
|
|
4327
5333
|
const runtimeInternals = {
|
|
4328
5334
|
compactionReason,
|
|
5335
|
+
resolveOfferedChoice,
|
|
4329
5336
|
dshToPiContent,
|
|
4330
5337
|
expandPrompt,
|
|
4331
5338
|
isSubagentOrigin,
|
|
4332
5339
|
normalizeToolResult,
|
|
4333
5340
|
splitArguments,
|
|
5341
|
+
supersedeActiveLogin,
|
|
4334
5342
|
textBlocks
|
|
4335
5343
|
};
|
|
4336
5344
|
//#endregion
|
|
4337
5345
|
export { runtimeInternals as i, normalizeToolSchema as n, registerVisionCompanions as r, applyPiPackage as t };
|
|
4338
5346
|
|
|
4339
|
-
//# sourceMappingURL=runtime-
|
|
5347
|
+
//# sourceMappingURL=runtime-DH0zEUpu.mjs.map
|