@valni/cli-darwin-x64 0.0.0-stage → 0.1.3
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/LICENSE +52 -0
- package/package.json +18 -5
- package/runtime/LICENSE +52 -0
- package/runtime/assets/clankolas.png +0 -0
- package/runtime/examples/extensions/auto-commit-on-exit.ts +49 -0
- package/runtime/examples/extensions/bash-spawn-hook.ts +30 -0
- package/runtime/examples/extensions/bookmark.ts +50 -0
- package/runtime/examples/extensions/border-status-editor.ts +145 -0
- package/runtime/examples/extensions/built-in-tool-renderer.ts +249 -0
- package/runtime/examples/extensions/claude-rules.ts +86 -0
- package/runtime/examples/extensions/commands.ts +72 -0
- package/runtime/examples/extensions/confirm-destructive.ts +59 -0
- package/runtime/examples/extensions/custom-footer.ts +64 -0
- package/runtime/examples/extensions/custom-header.ts +73 -0
- package/runtime/examples/extensions/custom-provider-anthropic/index.ts +604 -0
- package/runtime/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
- package/runtime/examples/extensions/custom-provider-anthropic/package.json +19 -0
- package/runtime/examples/extensions/dirty-repo-guard.ts +56 -0
- package/runtime/examples/extensions/doom-overlay/doom/build/doom.js +21 -0
- package/runtime/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
- package/runtime/examples/extensions/doom-overlay/doom/build.sh +152 -0
- package/runtime/examples/extensions/doom-overlay/doom/doomgeneric_valni.c +72 -0
- package/runtime/examples/extensions/doom-overlay/doom-component.ts +132 -0
- package/runtime/examples/extensions/doom-overlay/doom-engine.ts +173 -0
- package/runtime/examples/extensions/doom-overlay/doom-keys.ts +104 -0
- package/runtime/examples/extensions/doom-overlay/index.ts +74 -0
- package/runtime/examples/extensions/doom-overlay/wad-finder.ts +51 -0
- package/runtime/examples/extensions/dynamic-resources/dynamic.json +78 -0
- package/runtime/examples/extensions/dynamic-resources/index.ts +13 -0
- package/runtime/examples/extensions/dynamic-tools.ts +74 -0
- package/runtime/examples/extensions/entry-renderer.ts +41 -0
- package/runtime/examples/extensions/event-bus.ts +43 -0
- package/runtime/examples/extensions/file-trigger.ts +41 -0
- package/runtime/examples/extensions/git-checkpoint.ts +53 -0
- package/runtime/examples/extensions/git-merge-and-resolve.ts +115 -0
- package/runtime/examples/extensions/github-issue-autocomplete.ts +185 -0
- package/runtime/examples/extensions/hello.ts +26 -0
- package/runtime/examples/extensions/hidden-thinking-label.ts +53 -0
- package/runtime/examples/extensions/inline-bash.ts +94 -0
- package/runtime/examples/extensions/input-transform-streaming.ts +39 -0
- package/runtime/examples/extensions/input-transform.ts +43 -0
- package/runtime/examples/extensions/interactive-shell.ts +196 -0
- package/runtime/examples/extensions/mac-system-theme.ts +47 -0
- package/runtime/examples/extensions/message-renderer.ts +59 -0
- package/runtime/examples/extensions/minimal-mode.ts +426 -0
- package/runtime/examples/extensions/modal-editor.ts +85 -0
- package/runtime/examples/extensions/model-status.ts +31 -0
- package/runtime/examples/extensions/notify.ts +55 -0
- package/runtime/examples/extensions/overlay-qa-tests.ts +1450 -0
- package/runtime/examples/extensions/overlay-test.ts +153 -0
- package/runtime/examples/extensions/permission-gate.ts +34 -0
- package/runtime/examples/extensions/pirate.ts +47 -0
- package/runtime/examples/extensions/plan-mode/index.ts +390 -0
- package/runtime/examples/extensions/plan-mode/utils.ts +168 -0
- package/runtime/examples/extensions/preset.ts +436 -0
- package/runtime/examples/extensions/project-trust.ts +64 -0
- package/runtime/examples/extensions/prompt-customizer.ts +97 -0
- package/runtime/examples/extensions/protected-paths.ts +30 -0
- package/runtime/examples/extensions/provider-payload.ts +18 -0
- package/runtime/examples/extensions/question.ts +278 -0
- package/runtime/examples/extensions/questionnaire.ts +440 -0
- package/runtime/examples/extensions/rainbow-editor.ts +88 -0
- package/runtime/examples/extensions/reload-runtime.ts +37 -0
- package/runtime/examples/extensions/rpc-demo.ts +118 -0
- package/runtime/examples/extensions/sandbox/index.ts +321 -0
- package/runtime/examples/extensions/sandbox/package-lock.json +92 -0
- package/runtime/examples/extensions/sandbox/package.json +19 -0
- package/runtime/examples/extensions/send-user-message.ts +97 -0
- package/runtime/examples/extensions/session-name.ts +27 -0
- package/runtime/examples/extensions/shutdown-command.ts +63 -0
- package/runtime/examples/extensions/snake.ts +343 -0
- package/runtime/examples/extensions/space-invaders.ts +560 -0
- package/runtime/examples/extensions/ssh.ts +220 -0
- package/runtime/examples/extensions/status-line.ts +32 -0
- package/runtime/examples/extensions/structured-output.ts +65 -0
- package/runtime/examples/extensions/subagent/agents.ts +126 -0
- package/runtime/examples/extensions/subagent/index.ts +1009 -0
- package/runtime/examples/extensions/system-prompt-header.ts +17 -0
- package/runtime/examples/extensions/tic-tac-toe.ts +1008 -0
- package/runtime/examples/extensions/timed-confirm.ts +70 -0
- package/runtime/examples/extensions/titlebar-spinner.ts +58 -0
- package/runtime/examples/extensions/todo.ts +297 -0
- package/runtime/examples/extensions/tool-override.ts +144 -0
- package/runtime/examples/extensions/tools.ts +146 -0
- package/runtime/examples/extensions/trigger-compact.ts +50 -0
- package/runtime/examples/extensions/truncated-tool.ts +195 -0
- package/runtime/examples/extensions/widget-placement.ts +9 -0
- package/runtime/examples/extensions/with-deps/index.ts +32 -0
- package/runtime/examples/extensions/with-deps/package-lock.json +31 -0
- package/runtime/examples/extensions/with-deps/package.json +22 -0
- package/runtime/examples/extensions/working-indicator.ts +123 -0
- package/runtime/examples/extensions/working-message-test.ts +25 -0
- package/runtime/examples/rpc-extension-ui.ts +632 -0
- package/runtime/examples/sdk/01-minimal.ts +26 -0
- package/runtime/examples/sdk/02-custom-model.ts +43 -0
- package/runtime/examples/sdk/03-custom-prompt.ts +70 -0
- package/runtime/examples/sdk/04-skills.ts +55 -0
- package/runtime/examples/sdk/05-tools.ts +48 -0
- package/runtime/examples/sdk/06-extensions.ts +94 -0
- package/runtime/examples/sdk/07-context-files.ts +42 -0
- package/runtime/examples/sdk/08-prompt-templates.ts +51 -0
- package/runtime/examples/sdk/09-api-keys-and-oauth.ts +34 -0
- package/runtime/examples/sdk/10-settings.ts +53 -0
- package/runtime/examples/sdk/11-sessions.ts +52 -0
- package/runtime/examples/sdk/12-full-control.ts +71 -0
- package/runtime/examples/sdk/13-session-runtime.ts +67 -0
- package/runtime/export-html/ansi-to-html.js +249 -0
- package/runtime/export-html/index.js +226 -0
- package/runtime/export-html/template.css +1066 -0
- package/runtime/export-html/template.html +55 -0
- package/runtime/export-html/template.js +1864 -0
- package/runtime/export-html/tool-renderer.js +108 -0
- package/runtime/export-html/vendor/highlight.min.js +1213 -0
- package/runtime/export-html/vendor/marked.min.js +78 -0
- package/runtime/extensions/pi-switchboard/.github/workflows/ci.yml +51 -0
- package/runtime/extensions/pi-switchboard/LICENSE +22 -0
- package/runtime/extensions/pi-switchboard/README.md +85 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/catalog.ts +183 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/config.ts +97 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/connections.ts +386 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/constants.ts +12 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/device-auth.ts +127 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/envelope.ts +199 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/errors.ts +180 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/index.ts +125 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/participant.ts +17 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/sessionStream.ts +131 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/toolProxy.ts +136 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/tools.ts +16 -0
- package/runtime/extensions/pi-switchboard/extensions/switchboard/types.ts +37 -0
- package/runtime/extensions/pi-switchboard/package.json +30 -0
- package/runtime/extensions/pi-switchboard/tsconfig.json +13 -0
- package/runtime/extensions/switchboard-local/.github/workflows/ci.yml +80 -0
- package/runtime/extensions/switchboard-local/.github/workflows/release.yml +46 -0
- package/runtime/extensions/switchboard-local/README.md +98 -0
- package/runtime/extensions/switchboard-local/extensions/local/engine.ts +28 -0
- package/runtime/extensions/switchboard-local/extensions/local/engines/gguf.ts +324 -0
- package/runtime/extensions/switchboard-local/extensions/local/engines/llamaBuild.ts +62 -0
- package/runtime/extensions/switchboard-local/extensions/local/engines/mlx.ts +245 -0
- package/runtime/extensions/switchboard-local/extensions/local/engines/select.ts +38 -0
- package/runtime/extensions/switchboard-local/extensions/local/engines/support.ts +151 -0
- package/runtime/extensions/switchboard-local/extensions/local/hf.ts +106 -0
- package/runtime/extensions/switchboard-local/extensions/local/index.ts +67 -0
- package/runtime/extensions/switchboard-local/extensions/local/nativeStream.ts +602 -0
- package/runtime/extensions/switchboard-local/extensions/local/paths.ts +44 -0
- package/runtime/extensions/switchboard-local/extensions/local/provider.ts +57 -0
- package/runtime/extensions/switchboard-local/extensions/local/store.ts +82 -0
- package/runtime/extensions/switchboard-local/extensions/local/ui.ts +60 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/apiTypes.js +74 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/client.js +292 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/deviceAuth.js +155 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/errors.js +95 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/index.js +8 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/messages.js +253 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/models.js +19 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/native.js +886 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/dist/wire.js +85 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard/package.json +39 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/package.json +29 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/anthropic.ts +127 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/google.ts +111 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/index.ts +8 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/modelRecord.test.ts +25 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/modelRecord.ts +57 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/openaiGeneric.ts +103 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/openaiPro.ts +89 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/profile.ts +120 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/routing.test.ts +82 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/routing.ts +74 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.google.test.ts +137 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.openai.test.ts +241 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.test.ts +174 -0
- package/runtime/extensions/switchboard-local/node_modules/@valni/switchboard-native/src/validateConfig.ts +438 -0
- package/runtime/extensions/switchboard-local/package.json +34 -0
- package/runtime/extensions/switchboard-local/tsconfig.json +13 -0
- package/runtime/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/runtime/package.json +98 -0
- package/runtime/photon_rs_bg.wasm +0 -0
- package/runtime/theme/dark.json +86 -0
- package/runtime/theme/light.json +85 -0
- package/runtime/theme/theme-schema.json +340 -0
- package/runtime/valni +4 -0
- package/README.md +0 -3
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { EVENT_STREAM_CONTENT_TYPE, SENTINEL_SEGMENT } from "./constants.ts";
|
|
2
|
+
import { resolveBaseUrl, resolveEndUserId, resolveSessionId } from "./config.ts";
|
|
3
|
+
import { describeNetworkFailure, isAbortError, streamErrorResponse, translatedErrorResponse } from "./errors.ts";
|
|
4
|
+
import { KIND_TO_API, type SwitchboardKind } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
const INFERENCE_PATH = "/v1/switchboard/inference";
|
|
7
|
+
const SESSION_HEADER = "X-Switchboard-Session";
|
|
8
|
+
const TOOL_ASK_HEADER = "X-Switchboard-Tool-Ask";
|
|
9
|
+
const TOOL_ASK_STREAM_MARKER = ":switchboard.tool_ask ";
|
|
10
|
+
const STEER_HEADER = "X-Switchboard-Steer";
|
|
11
|
+
const STEER_STREAM_MARKER = ":switchboard.steer ";
|
|
12
|
+
|
|
13
|
+
export type SteerDelivery = "steer" | "followUp";
|
|
14
|
+
|
|
15
|
+
export type SteerSink = (steer: string, deliverAs: SteerDelivery) => void;
|
|
16
|
+
|
|
17
|
+
let steerSink: SteerSink | null = null;
|
|
18
|
+
|
|
19
|
+
export function onSteer(sink: SteerSink): void {
|
|
20
|
+
steerSink = sink;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function deliverSteers(entries: unknown): void {
|
|
24
|
+
if (!Array.isArray(entries)) return;
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
27
|
+
const steer = (entry as { steer?: unknown }).steer;
|
|
28
|
+
if (typeof steer !== "string" || steer.length === 0) continue;
|
|
29
|
+
const deliverAs = (entry as { deliverAs?: unknown }).deliverAs === "steer" ? "steer" : "followUp";
|
|
30
|
+
steerSink?.(steer, deliverAs);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function recordSteerHeader(response: Response): void {
|
|
35
|
+
const header = response.headers.get(STEER_HEADER);
|
|
36
|
+
if (header === null) return;
|
|
37
|
+
try {
|
|
38
|
+
deliverSteers(JSON.parse(Buffer.from(header, "base64").toString("utf8")));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error("pi-switchboard: failed to parse the steer header", error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function recordSteerMarkerLine(line: string): void {
|
|
45
|
+
if (!line.startsWith(STEER_STREAM_MARKER)) return;
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(line.slice(STEER_STREAM_MARKER.length).trim()) as { steers?: unknown };
|
|
48
|
+
deliverSteers(parsed.steers);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("pi-switchboard: failed to parse a steer stream marker", error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AskTag {
|
|
55
|
+
tool: string;
|
|
56
|
+
rule: string;
|
|
57
|
+
layer: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface AskEntry {
|
|
61
|
+
id?: string;
|
|
62
|
+
tool: string;
|
|
63
|
+
rule: string;
|
|
64
|
+
layer: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const pendingAsks = new Map<string, AskTag>();
|
|
68
|
+
|
|
69
|
+
export function consumePendingAsk(id: string): AskTag | undefined {
|
|
70
|
+
const ask = pendingAsks.get(id);
|
|
71
|
+
if (ask !== undefined) pendingAsks.delete(id);
|
|
72
|
+
return ask;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function clearPendingAsks(): void {
|
|
76
|
+
pendingAsks.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function recordAskEntries(entries: AskEntry[]): void {
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (typeof entry.id !== "string") {
|
|
82
|
+
console.error("pi-switchboard: ignoring a tool-ask tag without a tool-call id; cannot correlate it to a tool call");
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
pendingAsks.set(entry.id, { tool: entry.tool, rule: entry.rule, layer: entry.layer });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function recordAskHeader(response: Response): void {
|
|
90
|
+
const header = response.headers.get(TOOL_ASK_HEADER);
|
|
91
|
+
if (header === null) return;
|
|
92
|
+
try {
|
|
93
|
+
const entries = JSON.parse(Buffer.from(header, "base64").toString("utf8")) as AskEntry[];
|
|
94
|
+
if (Array.isArray(entries)) recordAskEntries(entries);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error("pi-switchboard: failed to parse the tool-ask header", error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function recordAskMarkerLine(line: string): void {
|
|
101
|
+
if (!line.startsWith(TOOL_ASK_STREAM_MARKER)) return;
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(line.slice(TOOL_ASK_STREAM_MARKER.length).trim()) as { asks?: AskEntry[] };
|
|
104
|
+
if (Array.isArray(parsed.asks)) recordAskEntries(parsed.asks);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error("pi-switchboard: failed to parse a tool-ask stream marker", error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function scanMarkerLine(line: string): void {
|
|
111
|
+
recordAskMarkerLine(line);
|
|
112
|
+
recordSteerMarkerLine(line);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function scanResponseStream(response: Response): Response {
|
|
116
|
+
if (response.body === null) return response;
|
|
117
|
+
const decoder = new TextDecoder();
|
|
118
|
+
let buffer = "";
|
|
119
|
+
const scanner = new TransformStream<Uint8Array, Uint8Array>({
|
|
120
|
+
transform(chunk, controller) {
|
|
121
|
+
controller.enqueue(chunk);
|
|
122
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
123
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
124
|
+
while (newlineIndex !== -1) {
|
|
125
|
+
scanMarkerLine(buffer.slice(0, newlineIndex));
|
|
126
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
127
|
+
newlineIndex = buffer.indexOf("\n");
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
flush() {
|
|
131
|
+
buffer += decoder.decode();
|
|
132
|
+
scanMarkerLine(buffer);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return new Response(response.body.pipeThrough(scanner), {
|
|
136
|
+
status: response.status,
|
|
137
|
+
statusText: response.statusText,
|
|
138
|
+
headers: response.headers,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function captureResponseSignals(response: Response): Response {
|
|
143
|
+
recordAskHeader(response);
|
|
144
|
+
recordSteerHeader(response);
|
|
145
|
+
if ((response.headers.get("content-type") ?? "").includes(EVENT_STREAM_CONTENT_TYPE)) {
|
|
146
|
+
return scanResponseStream(response);
|
|
147
|
+
}
|
|
148
|
+
return response;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isEnvelopeTarget(url: string): SwitchboardKind | null {
|
|
152
|
+
const sentinelIndex = url.indexOf(SENTINEL_SEGMENT);
|
|
153
|
+
if (sentinelIndex === -1) return null;
|
|
154
|
+
const kindTag = url.slice(sentinelIndex + SENTINEL_SEGMENT.length).split("/")[0];
|
|
155
|
+
return kindTag in KIND_TO_API ? (kindTag as SwitchboardKind) : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let envelopeFetchInstalled = false;
|
|
159
|
+
|
|
160
|
+
export function installEnvelopeFetch(): void {
|
|
161
|
+
if (envelopeFetchInstalled) return;
|
|
162
|
+
envelopeFetchInstalled = true;
|
|
163
|
+
const baseFetch = globalThis.fetch.bind(globalThis);
|
|
164
|
+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
165
|
+
const url = input instanceof Request ? input.url : input.toString();
|
|
166
|
+
const kindTag = isEnvelopeTarget(url);
|
|
167
|
+
if (kindTag === null) return baseFetch(input, init);
|
|
168
|
+
const request = new Request(input, init);
|
|
169
|
+
const nativeBody = (await request.json()) as Record<string, unknown>;
|
|
170
|
+
const headers = new Headers(request.headers);
|
|
171
|
+
headers.set("Content-Type", "application/json");
|
|
172
|
+
const anthropicStyleKey = headers.get("x-api-key");
|
|
173
|
+
if (!headers.has("Authorization") && anthropicStyleKey) {
|
|
174
|
+
headers.set("Authorization", `Bearer ${anthropicStyleKey}`);
|
|
175
|
+
headers.delete("x-api-key");
|
|
176
|
+
}
|
|
177
|
+
const sessionId = resolveSessionId();
|
|
178
|
+
if (sessionId !== null) headers.set(SESSION_HEADER, sessionId);
|
|
179
|
+
let response: Response;
|
|
180
|
+
try {
|
|
181
|
+
response = await baseFetch(`${resolveBaseUrl()}${INFERENCE_PATH}`, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers,
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
user_id: resolveEndUserId(),
|
|
186
|
+
time: new Date().toISOString(),
|
|
187
|
+
idempotency_key: crypto.randomUUID(),
|
|
188
|
+
kind: { [kindTag]: nativeBody },
|
|
189
|
+
}),
|
|
190
|
+
signal: request.signal,
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (isAbortError(error)) throw error;
|
|
194
|
+
return streamErrorResponse(kindTag, describeNetworkFailure(error));
|
|
195
|
+
}
|
|
196
|
+
if (response.ok) return captureResponseSignals(response);
|
|
197
|
+
return translatedErrorResponse(kindTag, response);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { EVENT_STREAM_CONTENT_TYPE } from "./constants.ts";
|
|
2
|
+
import { resolveBaseUrl } from "./config.ts";
|
|
3
|
+
import type { SwitchboardKind } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
const PORTAL_URL = "https://valni.app/platform";
|
|
6
|
+
const ERROR_DETAIL_LIMIT = 300;
|
|
7
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]+/g;
|
|
8
|
+
const PROVIDER_TROUBLE_MESSAGE = "The model provider is having trouble. Retry shortly, or switch to a different model.";
|
|
9
|
+
const TRANSIENT_PROVIDER_CODES = new Set(["SWB-5105", "SWB-5204", "SWB-5205", "SWB-5206"]);
|
|
10
|
+
|
|
11
|
+
export interface SwitchboardErrorEnvelope {
|
|
12
|
+
code?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
fault?: string;
|
|
15
|
+
requestId?: string;
|
|
16
|
+
detail?: string;
|
|
17
|
+
upstreamStatus?: number;
|
|
18
|
+
requestedMaxTokens?: number;
|
|
19
|
+
maxOutputTokens?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function displayableDetail(text: string): string {
|
|
23
|
+
return singleLine(text.replace(CONTROL_CHARACTERS, " ")).slice(0, ERROR_DETAIL_LIMIT);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function upstreamDetail(envelope: SwitchboardErrorEnvelope): string {
|
|
27
|
+
if (typeof envelope.detail !== "string") return "";
|
|
28
|
+
const trimmed = envelope.detail.trim();
|
|
29
|
+
if (!trimmed) return "";
|
|
30
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(trimmed) as { error?: { message?: unknown } };
|
|
33
|
+
const message = parsed.error?.message;
|
|
34
|
+
if (typeof message === "string" && message.trim()) return displayableDetail(message);
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return displayableDetail(trimmed);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function switchboardGuidance(envelope: SwitchboardErrorEnvelope & { code: string; error: string }): string {
|
|
42
|
+
switch (envelope.code) {
|
|
43
|
+
case "SWB-1001":
|
|
44
|
+
return `Wrong application key. Log in to Switchboard at ${PORTAL_URL} and get a key, then set your application key and restart pi.`;
|
|
45
|
+
case "SWB-1013":
|
|
46
|
+
return "Your Switchboard session expired. Run /login to sign in again.";
|
|
47
|
+
case "SWB-1014":
|
|
48
|
+
return "Your Switchboard session is invalid. Run /login to sign in again.";
|
|
49
|
+
case "SWB-1007":
|
|
50
|
+
return `Out of Switchboard credit. Top up at ${PORTAL_URL} and retry.`;
|
|
51
|
+
case "SWB-1003":
|
|
52
|
+
case "SWB-1004":
|
|
53
|
+
case "SWB-1008":
|
|
54
|
+
return "Over your rate limit. Wait a moment and retry, or raise the limit in the portal.";
|
|
55
|
+
case "SWB-1005":
|
|
56
|
+
case "SWB-1009":
|
|
57
|
+
case "SWB-1011":
|
|
58
|
+
return "A spend limit on this account was reached. Raise it in the portal to continue.";
|
|
59
|
+
case "SWB-1010":
|
|
60
|
+
case "SWB-1012":
|
|
61
|
+
return "This model is not enabled for this account. Switch models, or enable it in the portal.";
|
|
62
|
+
case "SWB-1025":
|
|
63
|
+
return `This model runs on your own provider subscription, which is not connected to Switchboard yet. Connect it at ${PORTAL_URL}/switchboard#connections, then retry.`;
|
|
64
|
+
case "SWB-1026":
|
|
65
|
+
return `Your provider subscription connection to Switchboard expired. Reconnect it at ${PORTAL_URL}/switchboard#connections, then retry.`;
|
|
66
|
+
case "SWB-1301":
|
|
67
|
+
return "Company policy denied this tool call. Change the policy in the portal, or ask the agent for a different approach.";
|
|
68
|
+
case "SWB-2005":
|
|
69
|
+
case "SWB-2006":
|
|
70
|
+
case "SWB-2007":
|
|
71
|
+
return "Your end user id is not registered on this account. Fix the end user id or register it in the portal, then restart pi.";
|
|
72
|
+
case "SWB-2008":
|
|
73
|
+
return `This end user is disabled on the account. Re-enable it at ${PORTAL_URL} to continue.`;
|
|
74
|
+
case "SWB-2048":
|
|
75
|
+
return typeof envelope.maxOutputTokens === "number" && typeof envelope.requestedMaxTokens === "number"
|
|
76
|
+
? `This model accepts at most ${envelope.maxOutputTokens} output tokens and the request asked for ${envelope.requestedMaxTokens}. Lower max_tokens and retry.`
|
|
77
|
+
: "The request asked for more output tokens than this model allows. Lower max_tokens and retry.";
|
|
78
|
+
case "SWB-3001":
|
|
79
|
+
case "SWB-3005":
|
|
80
|
+
return "This model is no longer available in the catalog. Pick a different model.";
|
|
81
|
+
case "SWB-5207":
|
|
82
|
+
return "The provider account behind Switchboard is out of credit. Retrying will not help — the account needs topping up.";
|
|
83
|
+
case "SWB-5208": {
|
|
84
|
+
const rejection = upstreamDetail(envelope);
|
|
85
|
+
return rejection
|
|
86
|
+
? `The provider rejected the request Switchboard sent: ${rejection}. Retrying the same request will not help.`
|
|
87
|
+
: "The provider rejected the request Switchboard sent. Retrying the same request will not help; report the reference.";
|
|
88
|
+
}
|
|
89
|
+
case "SWB-5002":
|
|
90
|
+
case "SWB-5003":
|
|
91
|
+
case "SWB-5101":
|
|
92
|
+
case "SWB-5102":
|
|
93
|
+
case "SWB-5103":
|
|
94
|
+
case "SWB-5108":
|
|
95
|
+
case "SWB-5202":
|
|
96
|
+
return "This model is not available on Switchboard right now. Pick a different model, and report the reference if it keeps happening.";
|
|
97
|
+
}
|
|
98
|
+
if (envelope.fault === "provider") {
|
|
99
|
+
const detail = upstreamDetail(envelope);
|
|
100
|
+
if (!detail) return PROVIDER_TROUBLE_MESSAGE;
|
|
101
|
+
return TRANSIENT_PROVIDER_CODES.has(envelope.code)
|
|
102
|
+
? `The model provider is having trouble: ${detail}. Retry shortly, or switch to a different model.`
|
|
103
|
+
: `The model provider returned an error: ${detail}`;
|
|
104
|
+
}
|
|
105
|
+
if (envelope.fault === "client") {
|
|
106
|
+
return `Switchboard rejected the request: ${envelope.error}.`;
|
|
107
|
+
}
|
|
108
|
+
return "Switchboard hit an internal problem. Not your fault. Retry shortly, and report the request id if it keeps happening.";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function referenceCode(envelope: SwitchboardErrorEnvelope & { code: string }): string {
|
|
112
|
+
return typeof envelope.upstreamStatus === "number" ? `${envelope.code}(${envelope.upstreamStatus})` : envelope.code;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function userFacingMessage(envelope: SwitchboardErrorEnvelope & { code: string; error: string }, status: number): string {
|
|
116
|
+
const code = referenceCode(envelope);
|
|
117
|
+
const reference = envelope.requestId ? `${code}, request ${envelope.requestId}` : `${code}, HTTP ${status}`;
|
|
118
|
+
return `${switchboardGuidance(envelope)} [${reference}]`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function unexpectedResponseMessage(response: Response, text: string): string {
|
|
122
|
+
console.error(`pi-switchboard: unexpected Switchboard response (HTTP ${response.status}): ${text.slice(0, ERROR_DETAIL_LIMIT)}`);
|
|
123
|
+
return `Switchboard returned an unexpected response (HTTP ${response.status}). Retry shortly, and report it if it keeps happening.`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function describeFailure(response: Response): Promise<string> {
|
|
127
|
+
const text = await response.text();
|
|
128
|
+
let envelope: SwitchboardErrorEnvelope;
|
|
129
|
+
try {
|
|
130
|
+
envelope = JSON.parse(text) as SwitchboardErrorEnvelope;
|
|
131
|
+
} catch {
|
|
132
|
+
return unexpectedResponseMessage(response, text);
|
|
133
|
+
}
|
|
134
|
+
if (!envelope.code || !envelope.error) {
|
|
135
|
+
return unexpectedResponseMessage(response, text);
|
|
136
|
+
}
|
|
137
|
+
return userFacingMessage({ ...envelope, code: envelope.code, error: envelope.error }, response.status);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function isAbortError(error: unknown): boolean {
|
|
141
|
+
return error instanceof Error && error.name === "AbortError";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function singleLine(message: string): string {
|
|
145
|
+
return message.replace(/\s+/g, " ").trim();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function streamErrorResponse(kindTag: SwitchboardKind, message: string): Response {
|
|
149
|
+
const clean = singleLine(message);
|
|
150
|
+
const stream =
|
|
151
|
+
kindTag === "anthropic"
|
|
152
|
+
? `event: error\ndata: ${clean}\n\n`
|
|
153
|
+
: `data: ${JSON.stringify({ error: { message: clean, type: "api_error" } })}\n\n`;
|
|
154
|
+
return new Response(stream, {
|
|
155
|
+
status: 200,
|
|
156
|
+
headers: { "Content-Type": EVENT_STREAM_CONTENT_TYPE },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function translatedErrorResponse(kindTag: SwitchboardKind, response: Response): Promise<Response> {
|
|
161
|
+
return streamErrorResponse(kindTag, await describeFailure(response));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function describeNetworkFailure(error: unknown): string {
|
|
165
|
+
let location = "";
|
|
166
|
+
try {
|
|
167
|
+
const host = new URL(resolveBaseUrl()).host;
|
|
168
|
+
if (host) location = ` at ${host}`;
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
172
|
+
return `Can't reach Switchboard${location}: it took too long to respond. Check your connection and try again.`;
|
|
173
|
+
}
|
|
174
|
+
const cause = (error as { cause?: { code?: string } }).cause;
|
|
175
|
+
const code = cause?.code ?? (error as { code?: string }).code;
|
|
176
|
+
if (code === "ECONNREFUSED") {
|
|
177
|
+
return `Can't reach Switchboard${location}: the connection was refused. It may be down. Try again in a moment.`;
|
|
178
|
+
}
|
|
179
|
+
return `Can't reach Switchboard${location}. Check your internet connection and try again.`;
|
|
180
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { ModelsStoreEntry, OAuthCredentials, RefreshModelsContext } from "@earendil-works/pi-ai";
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { buildModelConfigs, credentialBearer, discoverCatalog, loadRegistryModels } from "./catalog.ts";
|
|
7
|
+
import {
|
|
8
|
+
clearSessionCredentials,
|
|
9
|
+
clearSessionId,
|
|
10
|
+
rememberSessionCredentials,
|
|
11
|
+
resolveBaseUrl,
|
|
12
|
+
setSessionEndUserId,
|
|
13
|
+
setSessionId,
|
|
14
|
+
} from "./config.ts";
|
|
15
|
+
import { MILLISECONDS_PER_SECOND, PROVIDER_ID, spawnedConnectionId } from "./constants.ts";
|
|
16
|
+
import { switchboardLogin } from "./connections.ts";
|
|
17
|
+
import { deviceRefresh } from "./device-auth.ts";
|
|
18
|
+
import { clearPendingAsks, consumePendingAsk, installEnvelopeFetch, onSteer } from "./envelope.ts";
|
|
19
|
+
import { startSessionStream, stopSessionStream } from "./sessionStream.ts";
|
|
20
|
+
import { registerParticipantFraming } from "./participant.ts";
|
|
21
|
+
import { discoverTools } from "./toolProxy.ts";
|
|
22
|
+
import { registerServerTools } from "./tools.ts";
|
|
23
|
+
|
|
24
|
+
const PROVIDER_NAME = "Switchboard";
|
|
25
|
+
const APPROVAL_DIALOG_TITLE = "Switchboard approval required";
|
|
26
|
+
|
|
27
|
+
function extensionVersion(): string {
|
|
28
|
+
try {
|
|
29
|
+
const packagePath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
30
|
+
return (JSON.parse(readFileSync(packagePath, "utf8")) as { version: string }).version;
|
|
31
|
+
} catch {
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default async function (pi: ExtensionAPI) {
|
|
37
|
+
console.error(`pi-switchboard v${extensionVersion()}`);
|
|
38
|
+
const baseUrl = resolveBaseUrl();
|
|
39
|
+
const registry = await loadRegistryModels();
|
|
40
|
+
const environmentKey = process.env.SWITCHBOARD_API_KEY;
|
|
41
|
+
let startupModels: ReturnType<typeof buildModelConfigs> = [];
|
|
42
|
+
if (environmentKey) {
|
|
43
|
+
const [catalog, tools] = await Promise.allSettled([
|
|
44
|
+
discoverCatalog(baseUrl, environmentKey),
|
|
45
|
+
discoverTools(baseUrl, environmentKey),
|
|
46
|
+
]);
|
|
47
|
+
if (catalog.status === "rejected") throw catalog.reason;
|
|
48
|
+
startupModels = buildModelConfigs(catalog.value, registry);
|
|
49
|
+
if (tools.status === "fulfilled") {
|
|
50
|
+
registerServerTools(pi, tools.value);
|
|
51
|
+
} else {
|
|
52
|
+
console.error("pi-switchboard: server tool discovery failed for key mode", tools.reason);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
installEnvelopeFetch();
|
|
56
|
+
registerParticipantFraming(pi);
|
|
57
|
+
onSteer((steer, deliverAs) => {
|
|
58
|
+
pi.sendUserMessage(steer, { deliverAs });
|
|
59
|
+
});
|
|
60
|
+
pi.on("session_start", (_event, ctx) => {
|
|
61
|
+
setSessionId(spawnedConnectionId() ?? ctx.sessionManager.getSessionId());
|
|
62
|
+
startSessionStream(ctx);
|
|
63
|
+
});
|
|
64
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
65
|
+
const ask = consumePendingAsk(event.toolCallId);
|
|
66
|
+
if (ask === undefined) return;
|
|
67
|
+
const policy = `Switchboard ${ask.layer} policy requires approval for ${ask.rule}`;
|
|
68
|
+
if (!ctx.hasUI) {
|
|
69
|
+
return { block: true, reason: `${policy}; denied in a non-interactive session (no human to approve)` };
|
|
70
|
+
}
|
|
71
|
+
const approved = await ctx.ui.confirm(
|
|
72
|
+
APPROVAL_DIALOG_TITLE,
|
|
73
|
+
`The agent wants to run ${event.toolName}.\n\n${policy}. Allow this call?`,
|
|
74
|
+
);
|
|
75
|
+
if (!approved) return { block: true, reason: `Declined: ${policy}` };
|
|
76
|
+
return;
|
|
77
|
+
});
|
|
78
|
+
pi.on("session_shutdown", () => {
|
|
79
|
+
clearPendingAsks();
|
|
80
|
+
stopSessionStream();
|
|
81
|
+
clearSessionId();
|
|
82
|
+
clearSessionCredentials();
|
|
83
|
+
});
|
|
84
|
+
pi.registerProvider(PROVIDER_ID, {
|
|
85
|
+
name: PROVIDER_NAME,
|
|
86
|
+
baseUrl,
|
|
87
|
+
apiKey: "$SWITCHBOARD_API_KEY",
|
|
88
|
+
oauth: {
|
|
89
|
+
name: "Switchboard",
|
|
90
|
+
login: switchboardLogin,
|
|
91
|
+
refreshToken: deviceRefresh,
|
|
92
|
+
getApiKey: (credentials: OAuthCredentials) => {
|
|
93
|
+
if (typeof credentials.endUserId === "string") setSessionEndUserId(credentials.endUserId);
|
|
94
|
+
rememberSessionCredentials(credentials);
|
|
95
|
+
return credentials.access;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
models: startupModels,
|
|
99
|
+
refreshModels: async (context: RefreshModelsContext) => {
|
|
100
|
+
const bearer = credentialBearer(context.credential);
|
|
101
|
+
if (!bearer) {
|
|
102
|
+
await context.store.delete();
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const stored = await context.store.read();
|
|
106
|
+
if (!context.allowNetwork) {
|
|
107
|
+
return stored ? ([...stored.models] as ReturnType<typeof buildModelConfigs>) : startupModels;
|
|
108
|
+
}
|
|
109
|
+
const catalog = await discoverCatalog(baseUrl, bearer);
|
|
110
|
+
const configs = buildModelConfigs(catalog, registry);
|
|
111
|
+
await context.store.write({
|
|
112
|
+
models: configs as unknown as ModelsStoreEntry["models"],
|
|
113
|
+
checkedAt: Math.floor(Date.now() / MILLISECONDS_PER_SECOND),
|
|
114
|
+
});
|
|
115
|
+
if (context.credential?.type === "oauth") {
|
|
116
|
+
try {
|
|
117
|
+
registerServerTools(pi, await discoverTools(baseUrl, bearer));
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error("pi-switchboard: server tool discovery failed", error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return configs;
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { spawnedConnectionId } from "./constants.ts";
|
|
3
|
+
|
|
4
|
+
export const PARTICIPANT_FRAMING =
|
|
5
|
+
"You are a participant on a live automation connection, not a solo task. Your text output is not delivered to anyone — the only way to reach the other participant is the send_message tool: set expects_response true if you are waiting on a reply, false if you have nothing pending. The connection stays open and you stay available between messages — after you reply, wait for the other participant's next message. When the work is finished, call close_automation_connection to end it; nothing more is expected of you. Do your own work and reply here — do not open_automation_connection unless the task explicitly asks you to bring in a separate automation. The connection_id is filled in for you — you never type it.";
|
|
6
|
+
|
|
7
|
+
export function composeParticipantSystemPrompt(existing: string | undefined): string {
|
|
8
|
+
if (existing === undefined || existing.trim() === "") return PARTICIPANT_FRAMING;
|
|
9
|
+
return `${existing}\n\n${PARTICIPANT_FRAMING}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function registerParticipantFraming(pi: ExtensionAPI): void {
|
|
13
|
+
if (spawnedConnectionId() === null) return;
|
|
14
|
+
pi.on("before_agent_start", event => ({
|
|
15
|
+
systemPrompt: composeParticipantSystemPrompt(event.systemPrompt),
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { resolveBaseUrl, resolveSessionId } from "./config.ts";
|
|
3
|
+
import { PROVIDER_ID, spawnedConnectionId } from "./constants.ts";
|
|
4
|
+
import { deliverSteers } from "./envelope.ts";
|
|
5
|
+
|
|
6
|
+
const KEY_ENV = "SWITCHBOARD_API_KEY";
|
|
7
|
+
|
|
8
|
+
const STREAM_PATH = "/v1/switchboard/session-stream";
|
|
9
|
+
const SESSION_QUERY_PARAMETER = "session";
|
|
10
|
+
const RECONNECT_BASE_MS = 1_000;
|
|
11
|
+
const RECONNECT_CAP_MS = 60_000;
|
|
12
|
+
const STREAMING_MODES = new Set(["tui", "rpc"]);
|
|
13
|
+
|
|
14
|
+
let activeSocket: WebSocket | null = null;
|
|
15
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
16
|
+
let reconnectAttempt = 0;
|
|
17
|
+
let stopped = true;
|
|
18
|
+
|
|
19
|
+
function streamUrl(sessionId: string): string {
|
|
20
|
+
const url = new URL(`${resolveBaseUrl()}${STREAM_PATH}`);
|
|
21
|
+
if (url.protocol === "https:") url.protocol = "wss:";
|
|
22
|
+
else if (url.protocol === "http:") url.protocol = "ws:";
|
|
23
|
+
url.searchParams.set(SESSION_QUERY_PARAMETER, sessionId);
|
|
24
|
+
return url.toString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function spawnedStreamKey(connectionId: string | null, envKey: string | undefined): string | null {
|
|
28
|
+
if (connectionId === null) return null;
|
|
29
|
+
const key = envKey?.trim();
|
|
30
|
+
return key ? key : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function resolveStreamToken(context: ExtensionContext): Promise<string | null> {
|
|
34
|
+
const connectionId = spawnedConnectionId();
|
|
35
|
+
if (connectionId !== null) return spawnedStreamKey(connectionId, process.env[KEY_ENV]);
|
|
36
|
+
let resolution: Awaited<ReturnType<ExtensionContext["modelRegistry"]["getProviderAuth"]>>;
|
|
37
|
+
try {
|
|
38
|
+
resolution = await context.modelRegistry.getProviderAuth(PROVIDER_ID);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (resolution?.source !== "OAuth") return null;
|
|
43
|
+
return resolution.auth.apiKey ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function scheduleReconnect(context: ExtensionContext): void {
|
|
47
|
+
if (stopped || reconnectTimer !== null) return;
|
|
48
|
+
const backoff = Math.min(RECONNECT_BASE_MS * 2 ** reconnectAttempt, RECONNECT_CAP_MS);
|
|
49
|
+
const delay = backoff / 2 + Math.random() * (backoff / 2);
|
|
50
|
+
reconnectAttempt += 1;
|
|
51
|
+
reconnectTimer = setTimeout(() => {
|
|
52
|
+
reconnectTimer = null;
|
|
53
|
+
void connect(context);
|
|
54
|
+
}, delay);
|
|
55
|
+
reconnectTimer.unref?.();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleFrame(raw: unknown): void {
|
|
59
|
+
if (typeof raw !== "string") return;
|
|
60
|
+
let parsed: unknown;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(raw);
|
|
63
|
+
} catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (parsed === null || typeof parsed !== "object") return;
|
|
67
|
+
if ((parsed as { type?: unknown }).type !== "steer") return;
|
|
68
|
+
deliverSteers([parsed]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function connect(context: ExtensionContext): Promise<void> {
|
|
72
|
+
if (stopped || activeSocket !== null) return;
|
|
73
|
+
const sessionId = resolveSessionId();
|
|
74
|
+
if (sessionId === null) return scheduleReconnect(context);
|
|
75
|
+
const token = await resolveStreamToken(context);
|
|
76
|
+
if (token === null) return scheduleReconnect(context);
|
|
77
|
+
|
|
78
|
+
let socket: WebSocket;
|
|
79
|
+
try {
|
|
80
|
+
socket = new WebSocket(streamUrl(sessionId));
|
|
81
|
+
} catch {
|
|
82
|
+
return scheduleReconnect(context);
|
|
83
|
+
}
|
|
84
|
+
activeSocket = socket;
|
|
85
|
+
|
|
86
|
+
socket.addEventListener("open", () => {
|
|
87
|
+
socket.send(JSON.stringify({ type: "auth", token, session: sessionId }));
|
|
88
|
+
reconnectAttempt = 0;
|
|
89
|
+
});
|
|
90
|
+
socket.addEventListener("message", (event: MessageEvent) => {
|
|
91
|
+
handleFrame(event.data);
|
|
92
|
+
});
|
|
93
|
+
socket.addEventListener("close", () => {
|
|
94
|
+
if (activeSocket === socket) activeSocket = null;
|
|
95
|
+
scheduleReconnect(context);
|
|
96
|
+
});
|
|
97
|
+
socket.addEventListener("error", () => {
|
|
98
|
+
if (activeSocket === socket) activeSocket = null;
|
|
99
|
+
try {
|
|
100
|
+
socket.close();
|
|
101
|
+
} catch {
|
|
102
|
+
void 0;
|
|
103
|
+
}
|
|
104
|
+
scheduleReconnect(context);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function startSessionStream(context: ExtensionContext): void {
|
|
109
|
+
if (!STREAMING_MODES.has(context.mode)) return;
|
|
110
|
+
stopSessionStream();
|
|
111
|
+
stopped = false;
|
|
112
|
+
void connect(context);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function stopSessionStream(): void {
|
|
116
|
+
stopped = true;
|
|
117
|
+
reconnectAttempt = 0;
|
|
118
|
+
if (reconnectTimer !== null) {
|
|
119
|
+
clearTimeout(reconnectTimer);
|
|
120
|
+
reconnectTimer = null;
|
|
121
|
+
}
|
|
122
|
+
const socket = activeSocket;
|
|
123
|
+
activeSocket = null;
|
|
124
|
+
if (socket !== null) {
|
|
125
|
+
try {
|
|
126
|
+
socket.close();
|
|
127
|
+
} catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|