@skaile/transform-proxy 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ import { _ as TransformModule, a as TransformProxyOptions, c as TransformSpec, d as SessionFactPublisher, f as SessionFacts, g as TransformFactory, h as Transform, i as TransformProxy, l as Logger, m as TextLeafKind, n as SESSION_HEADER, o as UPSTREAM_HEADER, p as TextLeaf, r as TOKEN_HEADER, s as createTransformProxy, t as PIPELINE_HEADER, u as SessionFact, v as TransformSessionContext } from "./server-D5nBnO7T.mjs";
2
+ /** One stage's streamed inbound transformer, as returned by `Transform.inboundStream()`. */
3
+ interface InboundStream {
4
+ push(chunk: string): string;
5
+ flush(): string;
6
+ }
7
+ /**
8
+ * Composes the stages of one pipeline into a single transform.
9
+ * Outbound runs stage 1 to n, inbound and streamed inbound run stage n to 1.
10
+ * The empty list is the identity.
11
+ */
12
+ export declare function composeTransforms(stages: readonly Transform[], log?: Logger): Transform;
13
+ /** Default logger: drops everything. Hosts pass their own `Logger`. */
14
+ export declare const silentLogger: Logger;
15
+ /** One session's facts: the read side for transforms and the per-source publishers for connectors. */
16
+ interface SessionFactStore {
17
+ readonly facts: SessionFacts;
18
+ publisher(source: string): SessionFactPublisher;
19
+ }
20
+ export declare function createSessionFacts(): SessionFactStore;
21
+ export declare const CONFIG_FILE_NAME = "transform-proxy.yaml";
22
+ /** The YAML-backed subset of the proxy options. `baseDir` is the config file's directory. */
23
+ interface ProxyConfig {
24
+ readonly baseDir: string;
25
+ readonly transforms: Record<string, TransformSpec>;
26
+ readonly pipelines: Record<string, readonly string[]>;
27
+ readonly defaultPipeline?: string;
28
+ readonly allowUpstreams?: readonly string[];
29
+ }
30
+ interface ResolvedProxyConfig {
31
+ readonly path: string;
32
+ readonly options: ProxyConfig;
33
+ }
34
+ interface ConfigLookup {
35
+ /** The `--config` value, relative to `cwd`. When set, it is the only candidate. */
36
+ readonly explicit?: string;
37
+ readonly cwd: string;
38
+ readonly home: string;
39
+ }
40
+ /** The implicit lookup locations, in order: `./.skaile/` then `~/.config/skaile/`. */
41
+ export declare function configCandidates(cwd: string, home: string): readonly string[];
42
+ /**
43
+ * Finds the config file: the explicit path when given (it must exist), otherwise the first
44
+ * existing implicit candidate. Returns `undefined` when no implicit candidate exists.
45
+ */
46
+ export declare function findProxyConfig(lookup: ConfigLookup): string | undefined;
47
+ /** Finds and loads the config. Throws, naming the searched paths, when none is found. */
48
+ export declare function resolveProxyConfig(lookup: ConfigLookup): ResolvedProxyConfig;
49
+ /** Reads and validates a YAML config file. Unknown keys are rejected so that typos fail loudly. */
50
+ export declare function loadProxyConfig(path: string): ProxyConfig;
51
+ /** Validates an already-parsed config object. Exported for hosts that load YAML themselves. */
52
+ export declare function parseProxyConfig(source: unknown, baseDir: string): ProxyConfig;
53
+ interface WireContext {
54
+ /** The session's composed pipeline. */
55
+ readonly transform: Transform;
56
+ /**
57
+ * Session-scoped cache of signed reasoning: key -> text exactly as the model produced it.
58
+ * Walkers namespace their keys (`anthropic:<signature>`, `responses:<itemId>`).
59
+ */
60
+ readonly signatures: Map<string, string>;
61
+ }
62
+ /** Thrown by `request` for attachments (image, document, file parts); the server answers 400 `unsupported_content`. */
63
+ export declare class UnsupportedContentError extends Error {
64
+ readonly code = "unsupported_content";
65
+ constructor(message: string);
66
+ }
67
+ /** A text transformer over raw SSE text; safe at any chunk boundary. */
68
+ interface SseTransformer {
69
+ push(chunk: string): string;
70
+ flush(): string;
71
+ }
72
+ /** Pure functions over one wire protocol's parsed JSON. */
73
+ interface WireWalker {
74
+ /**
75
+ * Returns the request body with every text leaf passed through `outbound` and the
76
+ * pipeline `instruction` added to the system prompt. Throws `UnsupportedContentError`
77
+ * for attachments; `outbound` errors propagate unchanged.
78
+ */
79
+ request(body: unknown, ctx: WireContext): Promise<unknown>;
80
+ /** Restores a non-streamed JSON response. */
81
+ response(body: unknown, ctx: WireContext): unknown;
82
+ /** SSE event transformer for a streamed response. */
83
+ stream(ctx: WireContext): SseTransformer;
84
+ }
85
+ export declare const anthropicWalker: WireWalker;
86
+ export declare const openaiChatWalker: WireWalker;
87
+ export declare const openaiResponsesWalker: WireWalker;
88
+ export { type ConfigLookup, type InboundStream, type Logger, PIPELINE_HEADER, type ProxyConfig, type ResolvedProxyConfig, SESSION_HEADER, type SessionFact, type SessionFactPublisher, type SessionFactStore, type SessionFacts, type SseTransformer, TOKEN_HEADER, type TextLeaf, type TextLeafKind, type Transform, type TransformFactory, type TransformModule, type TransformProxy, type TransformProxyOptions, type TransformSessionContext, type TransformSpec, UPSTREAM_HEADER, type WireContext, type WireWalker, createTransformProxy };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export { t as CONFIG_FILE_NAME, s as PIPELINE_HEADER, c as SESSION_HEADER, l as TOKEN_HEADER, u as UPSTREAM_HEADER, h as UnsupportedContentError, m as anthropicWalker, _ as composeTransforms, n as configCandidates, g as createSessionFacts, d as createTransformProxy, r as findProxyConfig, i as loadProxyConfig, p as openaiChatWalker, f as openaiResponsesWalker, a as parseProxyConfig, o as resolveProxyConfig, y as silentLogger } from "./config-DQPsbYLi.mjs";
@@ -0,0 +1,89 @@
1
+ import { a as TransformProxyOptions, i as TransformProxy } from "../server-D5nBnO7T.mjs";
2
+ interface OmpModel {
3
+ readonly provider: string;
4
+ readonly id: string;
5
+ readonly api: string;
6
+ readonly baseUrl?: string;
7
+ }
8
+ /** A runtime provider override: omp merges `headers` into the provider's existing headers. */
9
+ interface OmpProviderOverride {
10
+ baseUrl?: string;
11
+ headers?: Record<string, string>;
12
+ }
13
+ interface OmpModelRegistry {
14
+ getAll(): readonly OmpModel[];
15
+ find(provider: string, modelId: string): OmpModel | undefined;
16
+ getProviderBaseUrl(provider: string): string | undefined;
17
+ getProviderHeaders(provider: string): Record<string, string> | undefined;
18
+ registerProvider(name: string, config: OmpProviderOverride, sourceId?: string): void;
19
+ /**
20
+ * Clears the runtime overrides of every provider registered under `sourceId` and reloads the
21
+ * static (built-in / models.yml) config once. `unregisterProvider` reloads once per provider:
22
+ * about half a second each, with ~80 built-in providers.
23
+ */
24
+ clearSourceRegistrations(sourceId: string): void;
25
+ }
26
+ type OmpNotifyType = 'info' | 'warning' | 'error';
27
+ interface OmpUi {
28
+ notify(message: string, type?: OmpNotifyType): void;
29
+ setStatus(key: string, text: string | undefined): void;
30
+ }
31
+ interface OmpContext {
32
+ readonly ui: OmpUi;
33
+ readonly cwd: string;
34
+ readonly modelRegistry: OmpModelRegistry;
35
+ readonly sessionManager: {
36
+ getSessionId(): string;
37
+ };
38
+ /** The session's active model: a snapshot that a provider override does not update. */
39
+ readonly model: OmpModel | undefined;
40
+ }
41
+ interface OmpAutocompleteItem {
42
+ value: string;
43
+ label: string;
44
+ description?: string;
45
+ }
46
+ type OmpEventName = 'session_start' | 'before_agent_start' | 'turn_start';
47
+ /** Per-session actions of the extension API. */
48
+ interface OmpSessionActions {
49
+ /** Rebinds the session's active model (without persisting it as the default). */
50
+ setModel(model: OmpModel): Promise<boolean>;
51
+ }
52
+ interface OmpExtensionApi extends OmpSessionActions {
53
+ registerCommand(name: string, options: {
54
+ description?: string;
55
+ getArgumentCompletions?: (argumentPrefix: string) => OmpAutocompleteItem[] | null;
56
+ handler: (args: string, ctx: OmpContext) => Promise<void>;
57
+ }): void;
58
+ on(event: OmpEventName, handler: (event: unknown, ctx: OmpContext) => void | Promise<void>): void;
59
+ }
60
+ interface AnonControllerDeps {
61
+ readonly env: Readonly<Record<string, string | undefined>>;
62
+ readonly home: string;
63
+ /** Default: `$SKAILE_TRANSFORM_PROXY_OMP_STATE`, else `<omp agent dir>/skaile-transform-proxy.json`. */
64
+ readonly statePath?: string;
65
+ /** Default: `createTransformProxy`. */
66
+ readonly startProxy?: (options: TransformProxyOptions) => Promise<TransformProxy>;
67
+ }
68
+ interface AnonController {
69
+ readonly required: boolean;
70
+ readonly statePath: string;
71
+ /** Handles `/anon <args>`. `session` rebinds the session's active model after a switch. */
72
+ command(args: string, ctx: OmpContext, session: OmpSessionActions): Promise<void>;
73
+ completions(prefix: string): OmpAutocompleteItem[] | null;
74
+ /** Applies the persisted flag (or required mode) and refreshes the status bar. */
75
+ sessionStart(ctx: OmpContext, session: OmpSessionActions): Promise<void>;
76
+ /** While on: routes providers that appeared, or were reset, since the last pass. */
77
+ reapply(ctx: OmpContext, session: OmpSessionActions): Promise<void>;
78
+ }
79
+ /**
80
+ * The process-wide `/anon` state machine. omp builds one extension instance per session
81
+ * (subagents included) while they share one model registry, so one controller serves them all.
82
+ */
83
+ export declare function createAnonController(deps: AnonControllerDeps): AnonController;
84
+ /** `$SKAILE_TRANSFORM_PROXY_OMP_STATE`, else `skaile-transform-proxy.json` in omp's agent dir (`$PI_CODING_AGENT_DIR` or `~/.omp/agent`). */
85
+ export declare function defaultStatePath(env: Readonly<Record<string, string | undefined>>, home: string): string;
86
+ /** Registers the `/anon` command and the event handlers on one session's extension API. */
87
+ export declare function registerAnonExtension(pi: OmpExtensionApi, controller: AnonController): void;
88
+ export default function anonExtension(pi: OmpExtensionApi): void;
89
+ export type { AnonController, AnonControllerDeps, OmpAutocompleteItem, OmpContext, OmpEventName, OmpExtensionApi, OmpModel, OmpModelRegistry, OmpNotifyType, OmpProviderOverride, OmpSessionActions, OmpUi };
@@ -0,0 +1,345 @@
1
+ import { c as SESSION_HEADER, d as createTransformProxy, i as loadProxyConfig, l as TOKEN_HEADER, n as configCandidates, r as findProxyConfig, u as UPSTREAM_HEADER, v as errorMessage, y as silentLogger } from "../config-DQPsbYLi.mjs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ // src/omp/anon.ts
7
+ /** A running proxy the extension routes to. */
8
+ const URL_ENV = "SKAILE_TRANSFORM_PROXY_URL";
9
+ /** The client token of that proxy. */
10
+ const TOKEN_ENV = "SKAILE_TRANSFORM_PROXY_TOKEN";
11
+ /** `required` (set by a skaile session in required mode): routing is forced on and `/anon off` is refused. */
12
+ const MODE_ENV = "SKAILE_TRANSFORM_PROXY_MODE";
13
+ /** Overrides the path of the persisted enabled flag. */
14
+ const STATE_ENV = "SKAILE_TRANSFORM_PROXY_OMP_STATE";
15
+ /** The footer status key. */
16
+ const STATUS_KEY = "anon";
17
+ /** The model-registry source the proxy overrides are registered under; `/anon off` clears it in one reload. */
18
+ const SOURCE_ID = "skaile-transform-proxy";
19
+ /** The wire APIs the proxy rewrites. Providers on any other API are routed too, and the proxy refuses their requests. */
20
+ const SERVED_APIS = {
21
+ "anthropic-messages": true,
22
+ "openai-completions": true,
23
+ "openai-responses": true
24
+ };
25
+ const USAGE = "usage: /anon on|off|status";
26
+ const VERBS = [
27
+ {
28
+ value: "on",
29
+ label: "on",
30
+ description: "route every provider through the transform proxy"
31
+ },
32
+ {
33
+ value: "off",
34
+ label: "off",
35
+ description: "restore the original provider base URLs"
36
+ },
37
+ {
38
+ value: "status",
39
+ label: "status",
40
+ description: "show the proxy, pipeline and routed providers"
41
+ }
42
+ ];
43
+ /**
44
+ * The process-wide `/anon` state machine. omp builds one extension instance per session
45
+ * (subagents included) while they share one model registry, so one controller serves them all.
46
+ */
47
+ function createAnonController(deps) {
48
+ const env = deps.env;
49
+ const required = env[MODE_ENV] === "required";
50
+ const statePath = deps.statePath ?? defaultStatePath(env, deps.home);
51
+ const startProxy = deps.startProxy ?? createTransformProxy;
52
+ const registries = /* @__PURE__ */ new WeakMap();
53
+ let enabled = false;
54
+ let endpoint;
55
+ let starting;
56
+ let sessionId;
57
+ function stateOf(registry) {
58
+ let state = registries.get(registry);
59
+ if (state === void 0) {
60
+ state = {
61
+ originals: /* @__PURE__ */ new Map(),
62
+ routed: /* @__PURE__ */ new Set()
63
+ };
64
+ registries.set(registry, state);
65
+ }
66
+ return state;
67
+ }
68
+ async function resolveEndpoint(ctx) {
69
+ if (endpoint !== void 0) return endpoint;
70
+ const url = env[URL_ENV];
71
+ if (url !== void 0 && url !== "") {
72
+ const token = env[TOKEN_ENV];
73
+ endpoint = {
74
+ url,
75
+ ...token ? { token } : {},
76
+ source: URL_ENV
77
+ };
78
+ return endpoint;
79
+ }
80
+ starting ??= startInProcess(ctx.cwd).finally(() => {
81
+ starting = void 0;
82
+ });
83
+ endpoint = await starting;
84
+ return endpoint;
85
+ }
86
+ async function startInProcess(cwd) {
87
+ const path = findProxyConfig({
88
+ cwd,
89
+ home: deps.home
90
+ });
91
+ if (path === void 0) {
92
+ const searched = configCandidates(cwd, deps.home).join(" or ");
93
+ throw new Error(`no transform proxy config found: create ${searched}, or set ${URL_ENV} to a running proxy`);
94
+ }
95
+ const options = loadProxyConfig(path);
96
+ if (options.defaultPipeline === void 0) throw new Error(`${path} sets no defaultPipeline: the omp extension sends no pipeline header`);
97
+ return {
98
+ url: (await startProxy({
99
+ ...options,
100
+ log: silentLogger
101
+ })).url,
102
+ pipeline: options.defaultPipeline,
103
+ source: `in-process, config ${path}`
104
+ };
105
+ }
106
+ function currentSessionId(ctx) {
107
+ if (sessionId === void 0) {
108
+ let id;
109
+ try {
110
+ id = ctx.sessionManager.getSessionId();
111
+ } catch {
112
+ id = void 0;
113
+ }
114
+ sessionId = typeof id === "string" && id !== "" ? id : randomUUID();
115
+ }
116
+ return sessionId;
117
+ }
118
+ function route(ctx, target) {
119
+ const registry = ctx.modelRegistry;
120
+ const state = stateOf(registry);
121
+ const session = currentSessionId(ctx);
122
+ for (const provider of providerApis(registry).keys()) {
123
+ let original = state.originals.get(provider);
124
+ if (original === void 0) {
125
+ original = snapshot(registry, provider);
126
+ state.originals.set(provider, original);
127
+ }
128
+ if (original.preRouted) continue;
129
+ if (state.routed.has(provider) && registry.getProviderBaseUrl(provider) === target.url) continue;
130
+ const headers = { [SESSION_HEADER]: session };
131
+ if (original.baseUrl !== void 0) headers[UPSTREAM_HEADER] = original.baseUrl;
132
+ if (target.token !== void 0) headers[TOKEN_HEADER] = target.token;
133
+ registry.registerProvider(provider, {
134
+ baseUrl: target.url,
135
+ headers
136
+ }, SOURCE_ID);
137
+ state.routed.add(provider);
138
+ }
139
+ }
140
+ /** Clears the proxy overrides; re-applies the recorded original where another override was layered underneath. */
141
+ function unroute(ctx) {
142
+ const registry = ctx.modelRegistry;
143
+ const state = stateOf(registry);
144
+ registry.clearSourceRegistrations(SOURCE_ID);
145
+ const lost = [];
146
+ for (const provider of state.routed) {
147
+ const original = state.originals.get(provider);
148
+ const restored = registry.getProviderBaseUrl(provider);
149
+ if (original === void 0 || restored === original.baseUrl) continue;
150
+ if (restored === void 0) {
151
+ lost.push(provider);
152
+ continue;
153
+ }
154
+ if (original.baseUrl !== void 0) registry.registerProvider(provider, {
155
+ baseUrl: original.baseUrl,
156
+ ...original.headers === void 0 ? {} : { headers: original.headers }
157
+ });
158
+ }
159
+ state.routed.clear();
160
+ return lost;
161
+ }
162
+ async function turnOn(ctx, session) {
163
+ let target;
164
+ try {
165
+ target = await resolveEndpoint(ctx);
166
+ } catch (err) {
167
+ ctx.ui.notify(`anon: ${errorMessage(err)}`, "error");
168
+ return false;
169
+ }
170
+ route(ctx, target);
171
+ enabled = true;
172
+ await rebind(ctx, session);
173
+ return true;
174
+ }
175
+ /** The session holds its active model as a snapshot: swap in the registry's re-projected one. */
176
+ async function rebind(ctx, session) {
177
+ const active = ctx.model;
178
+ if (active === void 0) return;
179
+ const live = ctx.modelRegistry.find(active.provider, active.id);
180
+ if (live === void 0 || live.baseUrl === active.baseUrl) return;
181
+ if (!await session.setModel(live)) ctx.ui.notify(`anon: could not rebind the active model ${active.provider}/${active.id}; select it again`, "warning");
182
+ }
183
+ function persist(ctx, value) {
184
+ try {
185
+ mkdirSync(dirname(statePath), { recursive: true });
186
+ writeFileSync(statePath, `${JSON.stringify({ enabled: value }, null, 2)}\n`);
187
+ } catch (err) {
188
+ ctx.ui.notify(`anon: could not save ${statePath}: ${errorMessage(err)}`, "warning");
189
+ }
190
+ }
191
+ function showStatus(ctx) {
192
+ ctx.ui.setStatus(STATUS_KEY, enabled ? "anon on" : "anon off");
193
+ }
194
+ function statusText(ctx) {
195
+ const registry = ctx.modelRegistry;
196
+ const state = stateOf(registry);
197
+ const apis = providerApis(registry);
198
+ const lines = [`anon ${enabled ? "on" : "off"}${required ? " (required)" : ""}`];
199
+ if (endpoint !== void 0) {
200
+ lines.push(`proxy: ${endpoint.url} (${endpoint.source})`);
201
+ lines.push(`pipeline: ${endpoint.pipeline ?? "the proxy default"}`);
202
+ }
203
+ if (sessionId !== void 0) lines.push(`session: ${sessionId}`);
204
+ const routed = [...state.routed].sort();
205
+ const refused = routed.flatMap((provider) => {
206
+ const reason = refusalReason(apis.get(provider), state.originals.get(provider));
207
+ return reason === void 0 ? [] : [`${provider} (${reason})`];
208
+ });
209
+ const preRouted = [...state.originals].filter(([, original]) => original.preRouted).map(([provider]) => provider);
210
+ lines.push(`routed (${routed.length}): ${routed.length === 0 ? "none" : routed.join(", ")}`);
211
+ if (refused.length > 0) lines.push(`refused by the proxy (${refused.length}): ${refused.join(", ")}`);
212
+ if (preRouted.length > 0) lines.push(`routed by their own config (${preRouted.length}): ${preRouted.sort().join(", ")}`);
213
+ return lines.join("\n");
214
+ }
215
+ return {
216
+ required,
217
+ statePath,
218
+ async command(args, ctx, session) {
219
+ switch (args.trim().toLowerCase()) {
220
+ case "on":
221
+ if (await turnOn(ctx, session)) {
222
+ persist(ctx, true);
223
+ const count = stateOf(ctx.modelRegistry).routed.size;
224
+ ctx.ui.notify(`anon on: ${count} provider(s) routed through ${endpoint?.url}`, "info");
225
+ }
226
+ break;
227
+ case "off": {
228
+ if (required) {
229
+ ctx.ui.notify(`anon: off refused: this session requires the transform proxy (${MODE_ENV}=required)`, "warning");
230
+ break;
231
+ }
232
+ const lost = unroute(ctx);
233
+ enabled = false;
234
+ await rebind(ctx, session);
235
+ persist(ctx, false);
236
+ ctx.ui.notify("anon off: original provider base URLs restored", "info");
237
+ if (lost.length > 0) ctx.ui.notify(`anon: ${lost.join(", ")} lost their runtime registration (registered by another extension); restart omp to restore them`, "warning");
238
+ break;
239
+ }
240
+ case "":
241
+ case "status":
242
+ ctx.ui.notify(statusText(ctx), "info");
243
+ break;
244
+ default: ctx.ui.notify(USAGE, "error");
245
+ }
246
+ showStatus(ctx);
247
+ },
248
+ completions(prefix) {
249
+ const needle = prefix.trim().toLowerCase();
250
+ const items = VERBS.filter((item) => item.value.startsWith(needle));
251
+ return items.length > 0 ? items : null;
252
+ },
253
+ async sessionStart(ctx, session) {
254
+ currentSessionId(ctx);
255
+ if (enabled && endpoint !== void 0) {
256
+ route(ctx, endpoint);
257
+ await rebind(ctx, session);
258
+ } else if (required || readEnabled(statePath)) {
259
+ if (!await turnOn(ctx, session) && required) ctx.ui.notify("anon: required mode, but no provider is routed through the transform proxy", "error");
260
+ }
261
+ showStatus(ctx);
262
+ },
263
+ async reapply(ctx, session) {
264
+ if (!enabled || endpoint === void 0) return;
265
+ route(ctx, endpoint);
266
+ await rebind(ctx, session);
267
+ }
268
+ };
269
+ }
270
+ /** `$SKAILE_TRANSFORM_PROXY_OMP_STATE`, else `skaile-transform-proxy.json` in omp's agent dir (`$PI_CODING_AGENT_DIR` or `~/.omp/agent`). */
271
+ function defaultStatePath(env, home) {
272
+ const explicit = env[STATE_ENV];
273
+ if (explicit !== void 0 && explicit !== "") return explicit;
274
+ const agentDir = env.PI_CODING_AGENT_DIR || join(home, ".omp", "agent");
275
+ return join(agentDir, "skaile-transform-proxy.json");
276
+ }
277
+ function readEnabled(path) {
278
+ try {
279
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
280
+ return typeof parsed === "object" && parsed !== null && "enabled" in parsed && parsed.enabled === true;
281
+ } catch {
282
+ return false;
283
+ }
284
+ }
285
+ /** Every provider of the registry with the wire APIs of its models. */
286
+ function providerApis(registry) {
287
+ const out = /* @__PURE__ */ new Map();
288
+ for (const model of registry.getAll()) {
289
+ let apis = out.get(model.provider);
290
+ if (apis === void 0) {
291
+ apis = /* @__PURE__ */ new Set();
292
+ out.set(model.provider, apis);
293
+ }
294
+ apis.add(model.api);
295
+ }
296
+ return out;
297
+ }
298
+ function snapshot(registry, provider) {
299
+ const live = registry.getProviderHeaders(provider);
300
+ const headers = live === void 0 ? void 0 : { ...live };
301
+ const preRouted = headers !== void 0 && Object.keys(headers).some((name) => name.toLowerCase() === "x-skaile-transform-upstream");
302
+ const baseUrl = registry.getProviderBaseUrl(provider);
303
+ return {
304
+ ...baseUrl === void 0 ? {} : { baseUrl },
305
+ ...headers === void 0 || Object.keys(headers).length === 0 ? {} : { headers },
306
+ preRouted
307
+ };
308
+ }
309
+ function refusalReason(apis, original) {
310
+ if (original?.baseUrl === void 0) return "no base URL";
311
+ const unserved = [...apis ?? []].filter((api) => !Object.hasOwn(SERVED_APIS, api));
312
+ return unserved.length === 0 ? void 0 : unserved.join(", ");
313
+ }
314
+ // src/omp/index.ts
315
+ /** omp re-imports the extension per session; the controller (proxy, recorded originals) is process-wide. */
316
+ const CONTROLLER_KEY = Symbol.for("@skaile/transform-proxy/omp");
317
+ /** Registers the `/anon` command and the event handlers on one session's extension API. */
318
+ function registerAnonExtension(pi, controller) {
319
+ pi.registerCommand("anon", {
320
+ description: "Route model requests through the skaile transform proxy: on | off | status",
321
+ getArgumentCompletions: (prefix) => controller.completions(prefix),
322
+ handler: (args, ctx) => guarded(ctx, () => controller.command(args, ctx, pi))
323
+ });
324
+ on(pi, "session_start", (ctx) => controller.sessionStart(ctx, pi));
325
+ on(pi, "before_agent_start", (ctx) => controller.reapply(ctx, pi));
326
+ on(pi, "turn_start", (ctx) => controller.reapply(ctx, pi));
327
+ }
328
+ function anonExtension(pi) {
329
+ const scope = globalThis;
330
+ registerAnonExtension(pi, scope[CONTROLLER_KEY] ??= createAnonController({
331
+ env: process.env,
332
+ home: homedir()
333
+ }));
334
+ }
335
+ function on(pi, event, run) {
336
+ pi.on(event, (_event, ctx) => guarded(ctx, () => run(ctx)));
337
+ }
338
+ async function guarded(ctx, run) {
339
+ try {
340
+ await run();
341
+ } catch (err) {
342
+ ctx.ui.notify(`anon: ${errorMessage(err)}`, "error");
343
+ }
344
+ }
345
+ export { createAnonController, anonExtension as default, defaultStatePath, registerAnonExtension };
@@ -0,0 +1,96 @@
1
+ /** Minimal logger; structurally compatible with the workspaces `Logger`. */
2
+ interface Logger {
3
+ debug(msg: string, ...args: unknown[]): void;
4
+ info(msg: string, ...args: unknown[]): void;
5
+ warn(msg: string, ...args: unknown[]): void;
6
+ error(msg: string, ...args: unknown[]): void;
7
+ }
8
+ type TextLeafKind = 'system' | 'user' | 'assistant' | 'thinking' | 'toolCall' | 'toolResult';
9
+ interface TextLeaf {
10
+ readonly kind: TextLeafKind;
11
+ readonly toolName?: string;
12
+ }
13
+ /** One session's instance of one pipeline stage. */
14
+ interface Transform {
15
+ /** Prepended to the system prompt; omit when the model needs no instruction. */
16
+ readonly instruction?: string;
17
+ /** Host -> model, one text leaf. A throw refuses the request (502 `transform_failed`); nothing is sent. */
18
+ outbound(text: string, leaf: TextLeaf): Promise<string>;
19
+ /** Model -> host, one complete text. */
20
+ inbound(text: string): string;
21
+ /** Model -> host, one streamed text block. Invariant: push(c1)+...+push(cn)+flush() === inbound(c1+...+cn). */
22
+ inboundStream(): {
23
+ push(chunk: string): string;
24
+ flush(): string;
25
+ };
26
+ /** Called once when the proxy drops the session. */
27
+ dispose?(): void | Promise<void>;
28
+ }
29
+ interface SessionFact {
30
+ readonly kind: string;
31
+ readonly value: string;
32
+ readonly source: string;
33
+ }
34
+ interface SessionFacts {
35
+ /** Bumped on every publish/clear; readers re-read `list()` only when it changed. */
36
+ readonly version: number;
37
+ list(kind?: string): readonly SessionFact[];
38
+ }
39
+ interface SessionFactPublisher {
40
+ /** Replaces this publisher's facts (source = publisher id). */
41
+ publish(facts: readonly Omit<SessionFact, 'source'>[]): void;
42
+ clear(): void;
43
+ }
44
+ interface TransformSessionContext {
45
+ readonly sessionId: string;
46
+ readonly facts: SessionFacts;
47
+ readonly log: Logger;
48
+ }
49
+ type TransformFactory = (ctx: TransformSessionContext) => Promise<Transform>;
50
+ /** Default export of a transform module, e.g. `@skaile/forge-anon/proxy`. */
51
+ interface TransformModule {
52
+ readonly id: string;
53
+ /** Once per proxy and config: validate config, build shared heavy state (detectors). Throws on a bad config. */
54
+ open(config: unknown, ctx: {
55
+ readonly baseDir: string;
56
+ readonly log: Logger;
57
+ }): Promise<TransformFactory>;
58
+ }
59
+ /** A configured transform: a module (object, npm specifier or relative path) and its config. */
60
+ interface TransformSpec {
61
+ readonly module: string | TransformModule;
62
+ readonly config?: unknown;
63
+ }
64
+ interface TransformProxyOptions {
65
+ /** Default 127.0.0.1. A non-loopback host requires `token`. */
66
+ host?: string;
67
+ /** Default 0 (ephemeral). */
68
+ port?: number;
69
+ /** Required `x-skaile-transform-token` when set. */
70
+ token?: string;
71
+ /** Where transform module specifiers and configs resolve. */
72
+ baseDir: string;
73
+ transforms: Readonly<Record<string, TransformSpec>>;
74
+ pipelines: Readonly<Record<string, readonly string[]>>;
75
+ /** Used when the request names no pipeline. Absent = such requests are refused. */
76
+ defaultPipeline?: string;
77
+ /** Origins. Absent = any https origin plus loopback http. */
78
+ allowUpstreams?: readonly string[];
79
+ log?: Logger;
80
+ }
81
+ interface TransformProxy {
82
+ readonly url: string;
83
+ session(id: string): {
84
+ readonly facts: SessionFacts;
85
+ publisher(source: string): SessionFactPublisher;
86
+ };
87
+ /** Disposes the session's transform instances and forgets its facts and caches. */
88
+ dropSession(id: string): Promise<void>;
89
+ close(): Promise<void>;
90
+ }
91
+ declare const UPSTREAM_HEADER = "x-skaile-transform-upstream";
92
+ declare const SESSION_HEADER = "x-skaile-transform-session";
93
+ declare const PIPELINE_HEADER = "x-skaile-transform-pipeline";
94
+ declare const TOKEN_HEADER = "x-skaile-transform-token";
95
+ declare function createTransformProxy(options: TransformProxyOptions): Promise<TransformProxy>;
96
+ export { TransformModule as _, TransformProxyOptions as a, TransformSpec as c, SessionFactPublisher as d, SessionFacts as f, TransformFactory as g, Transform as h, TransformProxy as i, Logger as l, TextLeafKind as m, SESSION_HEADER as n, UPSTREAM_HEADER as o, TextLeaf as p, TOKEN_HEADER as r, createTransformProxy as s, PIPELINE_HEADER as t, SessionFact as u, TransformSessionContext as v };