@skaile/transform-proxy 0.5.0 → 0.6.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.
- package/CHANGELOG.md +16 -0
- package/README.md +2 -0
- package/dist/cli.d.mts +6 -1
- package/dist/cli.mjs +24 -23
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +1 -1
- package/dist/omp/index.mjs +2 -2
- package/dist/{server-DdXMr4yF.d.mts → server-BWc1mocm.d.mts} +5 -2
- package/dist/{config-DHxYF3Tv.mjs → server-DTSaU_aK.mjs} +383 -258
- package/package.json +9 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 84c02a9: Less detection work behind the transform proxy, with the same protection.
|
|
8
|
+
|
|
9
|
+
forge-anon: an `AnonSession` caches each text's detection and no longer discards it when the mapping grows; earlier texts are finished again against the new mapping without a detector run. Detection of overlapping `anonymize` calls runs in parallel, placeholder allocation stays serialized. New `session.anonymize(text, { detectors })` restricts the detectors per call, and `session.remember(raw)` restores a model reply and lets its resend skip detection. The `onnx` and `llm` detectors cache model results per chunk text in memory. The proxy module restores through `remember` and takes an optional `leafDetectors` map (detectors per text leaf kind).
|
|
10
|
+
|
|
11
|
+
transform-proxy: the walkers rewrite all text leaves of a request concurrently; refusals are raised before any transform call. Transforms must accept overlapping `outbound` calls.
|
|
12
|
+
|
|
13
|
+
## 0.5.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 21bab3c: The bin installs its SIGINT, SIGTERM and `--exit-with-stdin` handlers before it prints `listening`. A parent that stopped the proxy right after that line could kill it with the default signal action (exit 143) instead of a clean shutdown (exit 0). `runCli` takes an optional `onStart` callback, called after the proxy listens and before the `listening` line.
|
|
18
|
+
|
|
3
19
|
## 0.5.0
|
|
4
20
|
|
|
5
21
|
- Library: the anon trace records what the proxy sends to the provider **after** the pipeline and what the model answers **before** restore, so a reviewer can check that no clear-text personal data reached the provider. `TransformProxyOptions.trace?: TraceSink` (synchronous `record(event: TraceEvent)`) gets one `request` line per proxied request on the wire routes and exactly one `response` or `error` line for it, linked by `requestId`. Without `trace`, nothing is recorded or hashed. A sink that throws is logged (`transform proxy: trace sink failed: <msg>`) and never reaches the client.
|
package/README.md
CHANGED
|
@@ -197,6 +197,7 @@ A module default-exports a `TransformModule`:
|
|
|
197
197
|
- It returns a `TransformFactory`, which runs once per `(session, pipeline)`: `(ctx: { sessionId, facts, log }) => Transform`.
|
|
198
198
|
- A `Transform` has `instruction?`, `outbound(text, leaf)`, `inbound(text)`, `inboundStream()`, `dispose?()`, `inspect?()` (host-only state, read through `proxy.inspect`; the walkers never call it) and `knownOriginals?()`.
|
|
199
199
|
- `knownOriginals?(): Iterable<{ kind, value }>` returns the originals the instance has already replaced in this session, each with its kind (forge-anon: its mapping). Only the [leak check](#leak-check) reads them, as source `pipeline`; the values are never sent or logged. `composeTransforms` returns the union of all stages that implement it; a stage that throws is logged and skipped.
|
|
200
|
+
- The walkers first check the whole request (refusals such as attachments or malformed slots come before any `outbound` call), then call `outbound` for every text leaf of the request at once and assemble the body in its original order. A transform must therefore accept overlapping `outbound` calls of one session; a stage that needs ordering (forge-anon allocates placeholders one call at a time) serializes internally. One rejected `outbound` fails the request.
|
|
200
201
|
|
|
201
202
|
The Anthropic walker appends the pipeline `instruction` after the host's system prompt: a string `system` gets it after a blank line, a block array gets it as one more text block at the end, and an absent `system` becomes the instruction. The host's leading blocks stay first, because Claude Code gateways (TeamClaude) refuse Sonnet requests with 429 `rate_limit_error` when the system prompt does not start with the Claude Code blocks. The OpenAI walkers still put the instruction first: a leading `system` message in chat completions, a prefix of `instructions` in responses.
|
|
202
203
|
|
|
@@ -214,3 +215,4 @@ Connectors publish facts through `proxy.session(id).publisher(source)`. Facts ar
|
|
|
214
215
|
- Fields the walkers do not rewrite (`tools`, metadata, unknown fields) are forwarded as they are. The [leak check](#leak-check) warns when one of them carries a known original; it does not block the request.
|
|
215
216
|
- Leak warnings carry kind, source and JSON path, never the value. Trace files and any other `TraceSink` get the clear text that leaked: keep them local, never commit them, delete them once reviewed.
|
|
216
217
|
- Mappings, facts, the signature cache and the trace hashes are kept in memory and are lost on restart.
|
|
218
|
+
- omp writes the request of every HTTP 400 answer to `~/.omp/logs/http-400-requests/<id>.json` (mode `0644`). That copy is what omp sent to the proxy, before anonymization, so it holds the personal data in clear text. It stays on the machine; delete it when anon is in use.
|
package/dist/cli.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as TransformProxy } from "./server-
|
|
1
|
+
import { i as TransformProxy } from "./server-BWc1mocm.mjs";
|
|
2
2
|
/** Environment variable that supplies the client token when `--token` is absent. */
|
|
3
3
|
export declare const TOKEN_ENV = "SKAILE_TRANSFORM_PROXY_TOKEN";
|
|
4
4
|
/** Environment variable with the trace directory (relative to cwd); overrides `--trace-dir` and the config's `trace.dir`. */
|
|
@@ -9,6 +9,11 @@ export interface CliEnvironment {
|
|
|
9
9
|
readonly env: Readonly<Record<string, string | undefined>>;
|
|
10
10
|
readonly stdout: (line: string) => void;
|
|
11
11
|
readonly stderr: (line: string) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Called once the proxy listens, before the `listening` line: a parent reacts to that line (with SIGTERM, or by
|
|
14
|
+
* closing stdin), so the caller's shutdown handlers must be in place by then.
|
|
15
|
+
*/
|
|
16
|
+
readonly onStart?: (run: Required<CliRun>) => void;
|
|
12
17
|
}
|
|
13
18
|
/** `proxy` is set only when the server started; the caller owns its shutdown. */
|
|
14
19
|
export interface CliRun {
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as createTransformProxy, s as createFileTraceSink, x as errorMessage, y as resolveProxyConfig } from "./server-DTSaU_aK.mjs";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -77,12 +77,14 @@ async function runCli(argv, io) {
|
|
|
77
77
|
}) }
|
|
78
78
|
});
|
|
79
79
|
if (traceDir !== void 0) io.stderr(format("info", `trace: ${traceDir}`, []));
|
|
80
|
-
|
|
81
|
-
return {
|
|
80
|
+
const run = {
|
|
82
81
|
code: 0,
|
|
83
82
|
proxy,
|
|
84
83
|
exitWithStdin: values["exit-with-stdin"] === true
|
|
85
84
|
};
|
|
85
|
+
io.onStart?.(run);
|
|
86
|
+
io.stdout(`listening ${proxy.url}`);
|
|
87
|
+
return run;
|
|
86
88
|
} catch (err) {
|
|
87
89
|
io.stderr(errorMessage(err));
|
|
88
90
|
return { code: 1 };
|
|
@@ -112,27 +114,26 @@ if (isEntryPoint()) {
|
|
|
112
114
|
home: homedir(),
|
|
113
115
|
env: process.env,
|
|
114
116
|
stdout: (line) => console.log(line),
|
|
115
|
-
stderr: (line) => console.error(line)
|
|
117
|
+
stderr: (line) => console.error(line),
|
|
118
|
+
onStart: ({ proxy, exitWithStdin }) => {
|
|
119
|
+
let stopping = false;
|
|
120
|
+
const stop = () => {
|
|
121
|
+
if (stopping) return;
|
|
122
|
+
stopping = true;
|
|
123
|
+
proxy.close().then(() => process.exit(0), (err) => {
|
|
124
|
+
console.error(errorMessage(err));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
process.once("SIGINT", stop);
|
|
129
|
+
process.once("SIGTERM", stop);
|
|
130
|
+
if (exitWithStdin) {
|
|
131
|
+
process.stdin.once("end", stop);
|
|
132
|
+
process.stdin.once("close", stop);
|
|
133
|
+
process.stdin.resume();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
116
136
|
});
|
|
117
137
|
if (run.proxy === void 0) process.exitCode = run.code;
|
|
118
|
-
else {
|
|
119
|
-
const proxy = run.proxy;
|
|
120
|
-
let stopping = false;
|
|
121
|
-
const stop = () => {
|
|
122
|
-
if (stopping) return;
|
|
123
|
-
stopping = true;
|
|
124
|
-
proxy.close().then(() => process.exit(0), (err) => {
|
|
125
|
-
console.error(errorMessage(err));
|
|
126
|
-
process.exit(1);
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
process.once("SIGINT", stop);
|
|
130
|
-
process.once("SIGTERM", stop);
|
|
131
|
-
if (run.exitWithStdin === true) {
|
|
132
|
-
process.stdin.once("end", stop);
|
|
133
|
-
process.stdin.once("close", stop);
|
|
134
|
-
process.stdin.resume();
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
138
|
}
|
|
138
139
|
export { TOKEN_ENV, TRACE_DIR_ENV, runCli };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as TraceSink, a as TransformProxyOptions, b as TransformModule, c as TransformSpec, d as SessionFactPublisher, f as SessionFacts, g as TraceLeak, h as TraceEvent, 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 Transform, x as TransformSessionContext, y as TransformFactory } from "./server-
|
|
1
|
+
import { _ as TraceSink, a as TransformProxyOptions, b as TransformModule, c as TransformSpec, d as SessionFactPublisher, f as SessionFacts, g as TraceLeak, h as TraceEvent, 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 Transform, x as TransformSessionContext, y as TransformFactory } from "./server-BWc1mocm.mjs";
|
|
2
2
|
/** One stage's streamed inbound transformer, as returned by `Transform.inboundStream()`. */
|
|
3
3
|
interface InboundStream {
|
|
4
4
|
push(chunk: string): string;
|
|
@@ -10,14 +10,6 @@ interface InboundStream {
|
|
|
10
10
|
* The empty list is the identity.
|
|
11
11
|
*/
|
|
12
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
13
|
export declare const CONFIG_FILE_NAME = "transform-proxy.yaml";
|
|
22
14
|
/** The YAML-backed subset of the proxy options. `baseDir` is the config file's directory. */
|
|
23
15
|
interface ProxyConfig {
|
|
@@ -55,6 +47,14 @@ export declare function resolveProxyConfig(lookup: ConfigLookup): ResolvedProxyC
|
|
|
55
47
|
export declare function loadProxyConfig(path: string): ProxyConfig;
|
|
56
48
|
/** Validates an already-parsed config object. Exported for hosts that load YAML themselves. */
|
|
57
49
|
export declare function parseProxyConfig(source: unknown, baseDir: string): ProxyConfig;
|
|
50
|
+
/** One session's facts: the read side for transforms and the per-source publishers for connectors. */
|
|
51
|
+
interface SessionFactStore {
|
|
52
|
+
readonly facts: SessionFacts;
|
|
53
|
+
publisher(source: string): SessionFactPublisher;
|
|
54
|
+
}
|
|
55
|
+
export declare function createSessionFacts(): SessionFactStore;
|
|
56
|
+
/** Default logger: drops everything. Hosts pass their own `Logger`. */
|
|
57
|
+
export declare const silentLogger: Logger;
|
|
58
58
|
export declare const DEFAULT_TRACE_MAX_CHARS = 262144;
|
|
59
59
|
/**
|
|
60
60
|
* One JSON line (no trailing newline). When the JSON of `content` is longer than `maxChars`,
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { m as CONFIG_FILE_NAME, o as DEFAULT_TRACE_MAX_CHARS, t as PIPELINE_HEADER, n as SESSION_HEADER, r as TOKEN_HEADER, i as UPSTREAM_HEADER, f as UnsupportedContentError, d as anthropicWalker, b as composeTransforms, h as configCandidates, s as createFileTraceSink, p as createSessionFacts, a as createTransformProxy, g as findProxyConfig, _ as loadProxyConfig, u as openaiChatWalker, l as openaiResponsesWalker, v as parseProxyConfig, y as resolveProxyConfig, c as serializeTraceEvent, S as silentLogger } from "./server-DTSaU_aK.mjs";
|
package/dist/omp/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { randomBytes, randomUUID } from "node:crypto";
|
|
1
|
+
import { _ as loadProxyConfig, g as findProxyConfig, h as configCandidates, i as UPSTREAM_HEADER, n as SESSION_HEADER, r as TOKEN_HEADER, x as errorMessage } from "../server-DTSaU_aK.mjs";
|
|
3
2
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
@@ -14,7 +14,10 @@ interface TextLeaf {
|
|
|
14
14
|
interface Transform {
|
|
15
15
|
/** Added to the system prompt by each wire walker (Anthropic: appended last; OpenAI: placed first); omit when the model needs no instruction. */
|
|
16
16
|
readonly instruction?: string;
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Host -> model, one text leaf. The leaves of one request are rewritten concurrently, so calls of one instance may
|
|
19
|
+
* overlap. A throw refuses the request (502 `transform_failed`); nothing is sent.
|
|
20
|
+
*/
|
|
18
21
|
outbound(text: string, leaf: TextLeaf): Promise<string>;
|
|
19
22
|
/** Model -> host, one complete text. */
|
|
20
23
|
inbound(text: string): string;
|
|
@@ -146,7 +149,7 @@ interface TransformProxy {
|
|
|
146
149
|
/**
|
|
147
150
|
* Host-only, in-process: each stage's `inspect()` result of the session's pipeline instance, keyed by transform id.
|
|
148
151
|
* `{}` when the session or pipeline is unknown, the instance is not built, or its build failed.
|
|
149
|
-
* Never builds an instance or creates session state; there is no HTTP route for it.
|
|
152
|
+
* Never builds an instance or creates session state; there is no HTTP route for it. Rejects when a stage's `inspect()` throws.
|
|
150
153
|
*/
|
|
151
154
|
inspect(sessionId: string, pipelineId?: string): Promise<Record<string, unknown>>;
|
|
152
155
|
close(): Promise<void>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
3
|
-
import { createServer } from "node:http";
|
|
4
2
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
5
3
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
-
import { pathToFileURL } from "node:url";
|
|
7
4
|
import { parse } from "yaml";
|
|
5
|
+
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
8
|
// src/log.ts
|
|
9
9
|
/** Default logger: drops everything. Hosts pass their own `Logger`. */
|
|
10
10
|
const silentLogger = {
|
|
@@ -92,6 +92,112 @@ function composeStreams(streams) {
|
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
+
// src/config.ts
|
|
96
|
+
const CONFIG_FILE_NAME = "transform-proxy.yaml";
|
|
97
|
+
/** The implicit lookup locations, in order: `./.skaile/` then `~/.config/skaile/`. */
|
|
98
|
+
function configCandidates(cwd, home) {
|
|
99
|
+
return [join(cwd, ".skaile", CONFIG_FILE_NAME), join(home, ".config", "skaile", CONFIG_FILE_NAME)];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Finds the config file: the explicit path when given (it must exist), otherwise the first
|
|
103
|
+
* existing implicit candidate. Returns `undefined` when no implicit candidate exists.
|
|
104
|
+
*/
|
|
105
|
+
function findProxyConfig(lookup) {
|
|
106
|
+
if (lookup.explicit !== void 0) {
|
|
107
|
+
const path = resolve(lookup.cwd, lookup.explicit);
|
|
108
|
+
if (!existsSync(path)) throw new Error(`transform proxy config not found: ${path}`);
|
|
109
|
+
return path;
|
|
110
|
+
}
|
|
111
|
+
return configCandidates(lookup.cwd, lookup.home).find((candidate) => existsSync(candidate));
|
|
112
|
+
}
|
|
113
|
+
/** Finds and loads the config. Throws, naming the searched paths, when none is found. */
|
|
114
|
+
function resolveProxyConfig(lookup) {
|
|
115
|
+
const path = findProxyConfig(lookup);
|
|
116
|
+
if (path === void 0) {
|
|
117
|
+
const searched = configCandidates(lookup.cwd, lookup.home).join(", ");
|
|
118
|
+
throw new Error(`no transform proxy config found (pass --config, or create one at ${searched})`);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
path,
|
|
122
|
+
options: loadProxyConfig(path)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const TOP_LEVEL_KEYS = {
|
|
126
|
+
transforms: true,
|
|
127
|
+
pipelines: true,
|
|
128
|
+
defaultPipeline: true,
|
|
129
|
+
allowUpstreams: true,
|
|
130
|
+
trace: true
|
|
131
|
+
};
|
|
132
|
+
const TRACE_KEYS = {
|
|
133
|
+
dir: true,
|
|
134
|
+
maxChars: true
|
|
135
|
+
};
|
|
136
|
+
/** Reads and validates a YAML config file. Unknown keys are rejected so that typos fail loudly. */
|
|
137
|
+
function loadProxyConfig(path) {
|
|
138
|
+
const source = parse(readFileSync(path, "utf8"));
|
|
139
|
+
try {
|
|
140
|
+
return parseProxyConfig(source ?? {}, dirname(path));
|
|
141
|
+
} catch (err) {
|
|
142
|
+
throw new Error(`invalid transform proxy config ${path}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Validates an already-parsed config object. Exported for hosts that load YAML themselves. */
|
|
146
|
+
function parseProxyConfig(source, baseDir) {
|
|
147
|
+
if (!isRecord$1(source)) throw new Error("the document must be a mapping");
|
|
148
|
+
for (const key of Object.keys(source)) if (!Object.hasOwn(TOP_LEVEL_KEYS, key)) throw new Error(`unknown key "${key}"`);
|
|
149
|
+
return {
|
|
150
|
+
baseDir,
|
|
151
|
+
transforms: parseTransforms(source.transforms),
|
|
152
|
+
pipelines: parsePipelines(source.pipelines),
|
|
153
|
+
...source.defaultPipeline === void 0 ? {} : { defaultPipeline: requireString(source.defaultPipeline, "defaultPipeline") },
|
|
154
|
+
...source.allowUpstreams === void 0 ? {} : { allowUpstreams: requireStringList(source.allowUpstreams, "allowUpstreams") },
|
|
155
|
+
...source.trace === void 0 ? {} : { trace: parseTrace(source.trace, baseDir) }
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function parseTransforms(value) {
|
|
159
|
+
if (value === void 0) return {};
|
|
160
|
+
if (!isRecord$1(value)) throw new Error("transforms must be a mapping of transform id to { module, config }");
|
|
161
|
+
const out = {};
|
|
162
|
+
for (const [id, spec] of Object.entries(value)) {
|
|
163
|
+
if (!isRecord$1(spec)) throw new Error(`transforms.${id} must be a mapping with a module`);
|
|
164
|
+
const module = requireString(spec.module, `transforms.${id}.module`);
|
|
165
|
+
out[id] = spec.config === void 0 ? { module } : {
|
|
166
|
+
module,
|
|
167
|
+
config: spec.config
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
function parsePipelines(value) {
|
|
173
|
+
if (value === void 0) return {};
|
|
174
|
+
if (!isRecord$1(value)) throw new Error("pipelines must be a mapping of pipeline id to a list of transform ids");
|
|
175
|
+
const out = {};
|
|
176
|
+
for (const [id, stages] of Object.entries(value)) out[id] = requireStringList(stages, `pipelines.${id}`);
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
function parseTrace(value, baseDir) {
|
|
180
|
+
if (!isRecord$1(value)) throw new Error("trace must be a mapping with a dir");
|
|
181
|
+
for (const key of Object.keys(value)) if (!Object.hasOwn(TRACE_KEYS, key)) throw new Error(`unknown key "trace.${key}"`);
|
|
182
|
+
const dir = resolve(baseDir, requireString(value.dir, "trace.dir"));
|
|
183
|
+
if (value.maxChars === void 0) return { dir };
|
|
184
|
+
if (typeof value.maxChars !== "number" || !Number.isInteger(value.maxChars) || value.maxChars <= 0) throw new Error("trace.maxChars must be a positive integer");
|
|
185
|
+
return {
|
|
186
|
+
dir,
|
|
187
|
+
maxChars: value.maxChars
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function requireString(value, field) {
|
|
191
|
+
if (typeof value !== "string" || value === "") throw new Error(`${field} must be a non-empty string`);
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
function requireStringList(value, field) {
|
|
195
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item === "")) throw new Error(`${field} must be a list of non-empty strings`);
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
function isRecord$1(value) {
|
|
199
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
200
|
+
}
|
|
95
201
|
// src/facts.ts
|
|
96
202
|
function createSessionFacts() {
|
|
97
203
|
const bySource = /* @__PURE__ */ new Map();
|
|
@@ -256,17 +362,17 @@ function resolveImportCondition(specifier, root) {
|
|
|
256
362
|
}
|
|
257
363
|
}
|
|
258
364
|
function exportTarget(exports, subpath) {
|
|
259
|
-
return conditionTarget((isRecord
|
|
365
|
+
return conditionTarget((isRecord(exports) && Object.keys(exports).some((key) => key.startsWith(".")) ? exports : { ".": exports })[subpath]);
|
|
260
366
|
}
|
|
261
367
|
function conditionTarget(entry) {
|
|
262
368
|
if (typeof entry === "string") return entry;
|
|
263
|
-
if (!isRecord
|
|
369
|
+
if (!isRecord(entry)) return void 0;
|
|
264
370
|
return conditionTarget(entry.import) ?? conditionTarget(entry.default);
|
|
265
371
|
}
|
|
266
372
|
function isTransformModule(value) {
|
|
267
|
-
return isRecord
|
|
373
|
+
return isRecord(value) && typeof value.id === "string" && typeof value.open === "function";
|
|
268
374
|
}
|
|
269
|
-
function isRecord
|
|
375
|
+
function isRecord(value) {
|
|
270
376
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
271
377
|
}
|
|
272
378
|
// src/wire/json-args.ts
|
|
@@ -410,28 +516,37 @@ function refuse$1(block) {
|
|
|
410
516
|
function malformed$2(slot) {
|
|
411
517
|
throw new UnsupportedContentError(`Anthropic ${slot} has an unexpected shape`);
|
|
412
518
|
}
|
|
413
|
-
|
|
414
|
-
|
|
519
|
+
/** Starts every queued rewrite at once; resolves when all results are written back, rejects on the first failure. */
|
|
520
|
+
async function runAll$2(pending) {
|
|
521
|
+
await Promise.all(pending.map((run) => run()));
|
|
522
|
+
}
|
|
523
|
+
/** Queues the rewrite of one text leaf; `set` later replaces the original, which stays in place until then. */
|
|
524
|
+
function queue$2(pending, transform, text, leaf, set) {
|
|
525
|
+
if (text !== "") pending.push(async () => set(await transform.outbound(text, leaf)));
|
|
415
526
|
}
|
|
416
527
|
/** A string, or an array of blocks whose `text` blocks are rewritten; anything else is refused. */
|
|
417
|
-
|
|
418
|
-
if (typeof content === "string")
|
|
419
|
-
|
|
528
|
+
function rewriteText(content, slot, rewrite, set) {
|
|
529
|
+
if (typeof content === "string") {
|
|
530
|
+
rewrite(content, set);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (!Array.isArray(content)) malformed$2(slot);
|
|
420
534
|
const out = [];
|
|
421
535
|
for (const block of content) {
|
|
422
|
-
if (!isObj$2(block))
|
|
536
|
+
if (!isObj$2(block)) malformed$2(`${slot} block`);
|
|
423
537
|
refuse$1(block);
|
|
424
538
|
if (block.type !== "text") {
|
|
425
539
|
out.push(block);
|
|
426
540
|
continue;
|
|
427
541
|
}
|
|
428
|
-
if (typeof block.text !== "string")
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
542
|
+
if (typeof block.text !== "string") malformed$2(`${slot} text block`);
|
|
543
|
+
const copy = { ...block };
|
|
544
|
+
out.push(copy);
|
|
545
|
+
rewrite(block.text, (text) => {
|
|
546
|
+
copy.text = text;
|
|
432
547
|
});
|
|
433
548
|
}
|
|
434
|
-
|
|
549
|
+
set(out);
|
|
435
550
|
}
|
|
436
551
|
/**
|
|
437
552
|
* Host-written system text that Anthropic's Claude subscription (OAuth) check reads verbatim: the
|
|
@@ -448,24 +563,37 @@ function gatePrefix(text) {
|
|
|
448
563
|
const end = text.indexOf("\n");
|
|
449
564
|
return end === -1 ? text : text.slice(0, end);
|
|
450
565
|
}
|
|
451
|
-
|
|
566
|
+
function rewriteSystemText(text, transform, pending, set) {
|
|
452
567
|
const kept = gatePrefix(text);
|
|
453
|
-
|
|
568
|
+
queue$2(pending, transform, text.slice(kept.length), { kind: "system" }, (rest) => set(kept + rest));
|
|
454
569
|
}
|
|
455
570
|
/** Every string leaf of a JSON value (tool-call input); keys stay as they are. */
|
|
456
|
-
|
|
457
|
-
if (typeof value === "string")
|
|
571
|
+
function rewriteStrings$2(value, leaf, transform, pending, set) {
|
|
572
|
+
if (typeof value === "string") {
|
|
573
|
+
queue$2(pending, transform, value, leaf, set);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
458
576
|
if (Array.isArray(value)) {
|
|
459
577
|
const out = [];
|
|
460
|
-
for (const item of value)
|
|
461
|
-
|
|
578
|
+
for (const item of value) {
|
|
579
|
+
const index = out.push(item) - 1;
|
|
580
|
+
rewriteStrings$2(item, leaf, transform, pending, (rewritten) => {
|
|
581
|
+
out[index] = rewritten;
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
set(out);
|
|
585
|
+
return;
|
|
462
586
|
}
|
|
463
587
|
if (isObj$2(value)) {
|
|
464
588
|
const out = {};
|
|
465
|
-
for (const [key, item] of Object.entries(value))
|
|
466
|
-
|
|
589
|
+
for (const [key, item] of Object.entries(value)) {
|
|
590
|
+
out[key] = item;
|
|
591
|
+
rewriteStrings$2(item, leaf, transform, pending, (rewritten) => {
|
|
592
|
+
out[key] = rewritten;
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
set(out);
|
|
467
596
|
}
|
|
468
|
-
return value;
|
|
469
597
|
}
|
|
470
598
|
/** Every string leaf of a JSON value, restored inbound. */
|
|
471
599
|
function restoreStrings$1(value, transform) {
|
|
@@ -479,7 +607,7 @@ function restoreStrings$1(value, transform) {
|
|
|
479
607
|
return value;
|
|
480
608
|
}
|
|
481
609
|
/** A thinking block: swaps in the cached model text when its signature is known, else rewrites it. */
|
|
482
|
-
|
|
610
|
+
function rewriteThinking(block, ctx, pending) {
|
|
483
611
|
if (typeof block.thinking !== "string") return malformed$2("thinking block");
|
|
484
612
|
if (typeof block.signature === "string") {
|
|
485
613
|
const cached = ctx.signatures.get(signatureKey$1(block.signature));
|
|
@@ -488,38 +616,48 @@ async function rewriteThinking(block, ctx) {
|
|
|
488
616
|
thinking: cached
|
|
489
617
|
};
|
|
490
618
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
};
|
|
619
|
+
const copy = { ...block };
|
|
620
|
+
queue$2(pending, ctx.transform, block.thinking, { kind: "thinking" }, (text) => {
|
|
621
|
+
copy.thinking = text;
|
|
622
|
+
});
|
|
623
|
+
return copy;
|
|
495
624
|
}
|
|
496
|
-
|
|
625
|
+
function rewriteContent$2(content, role, toolNames, ctx, pending, set) {
|
|
497
626
|
const textKind = role === "assistant" ? "assistant" : "user";
|
|
498
|
-
if (typeof content === "string")
|
|
499
|
-
|
|
627
|
+
if (typeof content === "string") {
|
|
628
|
+
queue$2(pending, ctx.transform, content, { kind: textKind }, set);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (!Array.isArray(content)) malformed$2("message content");
|
|
500
632
|
const out = [];
|
|
501
633
|
for (const block of content) {
|
|
502
|
-
if (!isObj$2(block))
|
|
634
|
+
if (!isObj$2(block)) malformed$2("message content block");
|
|
503
635
|
refuse$1(block);
|
|
504
636
|
switch (block.type) {
|
|
505
|
-
case "text":
|
|
506
|
-
if (typeof block.text !== "string")
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
637
|
+
case "text": {
|
|
638
|
+
if (typeof block.text !== "string") malformed$2("text block");
|
|
639
|
+
const copy = { ...block };
|
|
640
|
+
out.push(copy);
|
|
641
|
+
queue$2(pending, ctx.transform, block.text, { kind: textKind }, (text) => {
|
|
642
|
+
copy.text = text;
|
|
510
643
|
});
|
|
511
644
|
break;
|
|
645
|
+
}
|
|
512
646
|
case "thinking":
|
|
513
|
-
out.push(
|
|
647
|
+
out.push(rewriteThinking(block, ctx, pending));
|
|
514
648
|
break;
|
|
515
649
|
case "tool_use": {
|
|
516
650
|
const toolName = typeof block.name === "string" ? block.name : void 0;
|
|
517
|
-
|
|
651
|
+
const copy = {
|
|
518
652
|
...block,
|
|
519
|
-
input:
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
653
|
+
input: block.input
|
|
654
|
+
};
|
|
655
|
+
out.push(copy);
|
|
656
|
+
rewriteStrings$2(block.input, {
|
|
657
|
+
kind: "toolCall",
|
|
658
|
+
toolName
|
|
659
|
+
}, ctx.transform, pending, (input) => {
|
|
660
|
+
copy.input = input;
|
|
523
661
|
});
|
|
524
662
|
break;
|
|
525
663
|
}
|
|
@@ -529,19 +667,20 @@ async function rewriteContent$2(content, role, toolNames, ctx) {
|
|
|
529
667
|
break;
|
|
530
668
|
}
|
|
531
669
|
const toolName = typeof block.tool_use_id === "string" ? toolNames.get(block.tool_use_id) : void 0;
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
670
|
+
const copy = { ...block };
|
|
671
|
+
out.push(copy);
|
|
672
|
+
rewriteText(block.content, "tool_result content", (text, setText) => queue$2(pending, ctx.transform, text, {
|
|
673
|
+
kind: "toolResult",
|
|
674
|
+
toolName
|
|
675
|
+
}, setText), (rewritten) => {
|
|
676
|
+
copy.content = rewritten;
|
|
538
677
|
});
|
|
539
678
|
break;
|
|
540
679
|
}
|
|
541
680
|
default: out.push(block);
|
|
542
681
|
}
|
|
543
682
|
}
|
|
544
|
-
|
|
683
|
+
set(out);
|
|
545
684
|
}
|
|
546
685
|
/** Maps each `tool_use` id to its tool name, so tool results can name their tool. */
|
|
547
686
|
function toolNamesById(messages) {
|
|
@@ -752,21 +891,26 @@ const anthropicWalker = {
|
|
|
752
891
|
async request(body, ctx) {
|
|
753
892
|
if (!isObj$2(body)) return malformed$2("request body");
|
|
754
893
|
const out = { ...body };
|
|
755
|
-
|
|
756
|
-
if (
|
|
894
|
+
const pending = [];
|
|
895
|
+
if (body.system !== void 0) rewriteText(body.system, "system", (text, set) => rewriteSystemText(text, ctx.transform, pending, set), (system) => {
|
|
896
|
+
out.system = system;
|
|
897
|
+
});
|
|
757
898
|
if (body.messages !== void 0) {
|
|
758
899
|
if (!Array.isArray(body.messages)) return malformed$2("messages");
|
|
759
900
|
const toolNames = toolNamesById(body.messages);
|
|
760
901
|
const messages = [];
|
|
761
902
|
for (const message of body.messages) {
|
|
762
903
|
if (!isObj$2(message)) return malformed$2("message");
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
904
|
+
const copy = { ...message };
|
|
905
|
+
messages.push(copy);
|
|
906
|
+
rewriteContent$2(message.content, message.role, toolNames, ctx, pending, (content) => {
|
|
907
|
+
copy.content = content;
|
|
766
908
|
});
|
|
767
909
|
}
|
|
768
910
|
out.messages = messages;
|
|
769
911
|
}
|
|
912
|
+
await runAll$2(pending);
|
|
913
|
+
if (ctx.transform.instruction !== void 0) out.system = withInstruction(out.system, ctx.transform.instruction);
|
|
770
914
|
return out;
|
|
771
915
|
},
|
|
772
916
|
response(body, ctx) {
|
|
@@ -809,47 +953,69 @@ const REFUSED_PARTS$1 = {
|
|
|
809
953
|
};
|
|
810
954
|
/** Reasoning fields used by OpenAI-compatible providers. */
|
|
811
955
|
const REASONING_FIELDS = ["reasoning_content", "reasoning"];
|
|
812
|
-
|
|
813
|
-
|
|
956
|
+
/** Starts every queued rewrite at once; resolves when all results are written back, rejects on the first failure. */
|
|
957
|
+
async function runAll$1(pending) {
|
|
958
|
+
await Promise.all(pending.map((run) => run()));
|
|
959
|
+
}
|
|
960
|
+
/** Queues the rewrite of one text leaf; `set` later replaces the original, which stays in place until then. */
|
|
961
|
+
function queue$1(pending, transform, text, leaf, set) {
|
|
962
|
+
if (text !== "") pending.push(async () => set(await transform.outbound(text, leaf)));
|
|
814
963
|
}
|
|
815
964
|
/** A slot the walker reads holds a value it cannot classify: refused, never forwarded untransformed. */
|
|
816
965
|
function malformed$1(slot) {
|
|
817
966
|
throw new UnsupportedContentError(`OpenAI chat ${slot} has an unexpected shape`);
|
|
818
967
|
}
|
|
819
968
|
/** A string, or an array of parts whose `text` parts are rewritten; `null`/absent carries no text; anything else is refused. */
|
|
820
|
-
|
|
821
|
-
if (typeof content === "string")
|
|
822
|
-
|
|
823
|
-
|
|
969
|
+
function rewriteContent$1(content, leaf, transform, pending, set) {
|
|
970
|
+
if (typeof content === "string") {
|
|
971
|
+
queue$1(pending, transform, content, leaf, set);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (content === void 0 || content === null) return;
|
|
975
|
+
if (!Array.isArray(content)) malformed$1("message content");
|
|
824
976
|
const out = [];
|
|
825
977
|
for (const part of content) {
|
|
826
|
-
if (!isObj$1(part))
|
|
978
|
+
if (!isObj$1(part)) malformed$1("content part");
|
|
827
979
|
if (typeof part.type === "string" && REFUSED_PARTS$1[part.type] === true) throw new UnsupportedContentError(`OpenAI chat ${part.type} content cannot be transformed`);
|
|
828
980
|
if (part.type !== "text") {
|
|
829
981
|
out.push(part);
|
|
830
982
|
continue;
|
|
831
983
|
}
|
|
832
|
-
if (typeof part.text !== "string")
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
984
|
+
if (typeof part.text !== "string") malformed$1("text part");
|
|
985
|
+
const copy = { ...part };
|
|
986
|
+
out.push(copy);
|
|
987
|
+
queue$1(pending, transform, part.text, leaf, (text) => {
|
|
988
|
+
copy.text = text;
|
|
836
989
|
});
|
|
837
990
|
}
|
|
838
|
-
|
|
991
|
+
set(out);
|
|
839
992
|
}
|
|
840
|
-
|
|
841
|
-
if (typeof value === "string")
|
|
993
|
+
function rewriteStrings$1(value, leaf, transform, pending, set) {
|
|
994
|
+
if (typeof value === "string") {
|
|
995
|
+
queue$1(pending, transform, value, leaf, set);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
842
998
|
if (Array.isArray(value)) {
|
|
843
999
|
const out = [];
|
|
844
|
-
for (const item of value)
|
|
845
|
-
|
|
1000
|
+
for (const item of value) {
|
|
1001
|
+
const index = out.push(item) - 1;
|
|
1002
|
+
rewriteStrings$1(item, leaf, transform, pending, (rewritten) => {
|
|
1003
|
+
out[index] = rewritten;
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
set(out);
|
|
1007
|
+
return;
|
|
846
1008
|
}
|
|
847
1009
|
if (isObj$1(value)) {
|
|
848
1010
|
const out = {};
|
|
849
|
-
for (const [key, item] of Object.entries(value))
|
|
850
|
-
|
|
1011
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1012
|
+
out[key] = item;
|
|
1013
|
+
rewriteStrings$1(item, leaf, transform, pending, (rewritten) => {
|
|
1014
|
+
out[key] = rewritten;
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
set(out);
|
|
851
1018
|
}
|
|
852
|
-
return value;
|
|
853
1019
|
}
|
|
854
1020
|
function restoreStrings(value, transform) {
|
|
855
1021
|
if (typeof value === "string") return transform.inbound(value);
|
|
@@ -862,14 +1028,23 @@ function restoreStrings(value, transform) {
|
|
|
862
1028
|
return value;
|
|
863
1029
|
}
|
|
864
1030
|
/** Tool-call arguments are a JSON string: rewrite its string leaves, or the raw text when it is not JSON. */
|
|
865
|
-
|
|
1031
|
+
function rewriteArguments$1(args, leaf, transform, pending, set) {
|
|
866
1032
|
let parsed;
|
|
867
1033
|
try {
|
|
868
1034
|
parsed = JSON.parse(args);
|
|
869
1035
|
} catch {
|
|
870
|
-
|
|
1036
|
+
queue$1(pending, transform, args, leaf, set);
|
|
1037
|
+
return;
|
|
871
1038
|
}
|
|
872
|
-
|
|
1039
|
+
const leaves = [];
|
|
1040
|
+
let rewritten = parsed;
|
|
1041
|
+
rewriteStrings$1(parsed, leaf, transform, leaves, (value) => {
|
|
1042
|
+
rewritten = value;
|
|
1043
|
+
});
|
|
1044
|
+
pending.push(async () => {
|
|
1045
|
+
await runAll$1(leaves);
|
|
1046
|
+
set(JSON.stringify(rewritten));
|
|
1047
|
+
});
|
|
873
1048
|
}
|
|
874
1049
|
function restoreArguments$1(args, transform) {
|
|
875
1050
|
let parsed;
|
|
@@ -880,7 +1055,7 @@ function restoreArguments$1(args, transform) {
|
|
|
880
1055
|
}
|
|
881
1056
|
return JSON.stringify(restoreStrings(parsed, transform));
|
|
882
1057
|
}
|
|
883
|
-
|
|
1058
|
+
function rewriteToolCalls(calls, transform, names, pending) {
|
|
884
1059
|
if (calls === void 0 || calls === null) return calls;
|
|
885
1060
|
if (!Array.isArray(calls)) return malformed$1("tool_calls");
|
|
886
1061
|
const out = [];
|
|
@@ -894,53 +1069,59 @@ async function rewriteToolCalls(calls, transform, names) {
|
|
|
894
1069
|
if (!isObj$1(fn) || typeof fn.arguments !== "string") return malformed$1("function tool call");
|
|
895
1070
|
const toolName = typeof fn.name === "string" ? fn.name : void 0;
|
|
896
1071
|
if (typeof call.id === "string" && toolName !== void 0) names.set(call.id, toolName);
|
|
1072
|
+
const fnCopy = { ...fn };
|
|
897
1073
|
out.push({
|
|
898
1074
|
...call,
|
|
899
|
-
function:
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1075
|
+
function: fnCopy
|
|
1076
|
+
});
|
|
1077
|
+
rewriteArguments$1(fn.arguments, {
|
|
1078
|
+
kind: "toolCall",
|
|
1079
|
+
toolName
|
|
1080
|
+
}, transform, pending, (args) => {
|
|
1081
|
+
fnCopy.arguments = args;
|
|
906
1082
|
});
|
|
907
1083
|
}
|
|
908
1084
|
return out;
|
|
909
1085
|
}
|
|
910
|
-
|
|
1086
|
+
function rewriteMessage(message, transform, names, pending) {
|
|
911
1087
|
const out = { ...message };
|
|
1088
|
+
const setContent = (content) => {
|
|
1089
|
+
out.content = content;
|
|
1090
|
+
};
|
|
912
1091
|
switch (message.role) {
|
|
913
1092
|
case "system":
|
|
914
1093
|
case "developer":
|
|
915
|
-
|
|
1094
|
+
rewriteContent$1(message.content, { kind: "system" }, transform, pending, setContent);
|
|
916
1095
|
break;
|
|
917
1096
|
case "user":
|
|
918
|
-
|
|
1097
|
+
rewriteContent$1(message.content, { kind: "user" }, transform, pending, setContent);
|
|
919
1098
|
break;
|
|
920
1099
|
case "assistant":
|
|
921
1100
|
for (const field of REASONING_FIELDS) {
|
|
922
1101
|
const value = message[field];
|
|
923
|
-
if (typeof value === "string")
|
|
1102
|
+
if (typeof value === "string") queue$1(pending, transform, value, { kind: "thinking" }, (text) => {
|
|
1103
|
+
out[field] = text;
|
|
1104
|
+
});
|
|
924
1105
|
else if (value !== void 0 && value !== null) return malformed$1(`assistant ${field}`);
|
|
925
1106
|
}
|
|
926
|
-
|
|
927
|
-
out.tool_calls =
|
|
1107
|
+
rewriteContent$1(message.content, { kind: "assistant" }, transform, pending, setContent);
|
|
1108
|
+
out.tool_calls = rewriteToolCalls(message.tool_calls, transform, names, pending);
|
|
928
1109
|
if (message.tool_calls === void 0) delete out.tool_calls;
|
|
929
1110
|
break;
|
|
930
1111
|
case "tool": {
|
|
931
1112
|
const toolName = typeof message.tool_call_id === "string" ? names.get(message.tool_call_id) : void 0;
|
|
932
|
-
|
|
1113
|
+
rewriteContent$1(message.content, {
|
|
933
1114
|
kind: "toolResult",
|
|
934
1115
|
toolName
|
|
935
|
-
}, transform);
|
|
1116
|
+
}, transform, pending, setContent);
|
|
936
1117
|
break;
|
|
937
1118
|
}
|
|
938
1119
|
case "function": {
|
|
939
1120
|
const toolName = typeof message.name === "string" ? message.name : void 0;
|
|
940
|
-
|
|
1121
|
+
rewriteContent$1(message.content, {
|
|
941
1122
|
kind: "toolResult",
|
|
942
1123
|
toolName
|
|
943
|
-
}, transform);
|
|
1124
|
+
}, transform, pending, setContent);
|
|
944
1125
|
break;
|
|
945
1126
|
}
|
|
946
1127
|
default: return malformed$1("message role");
|
|
@@ -1192,11 +1373,13 @@ const openaiChatWalker = {
|
|
|
1192
1373
|
if (body.messages !== void 0) {
|
|
1193
1374
|
if (!Array.isArray(body.messages)) return malformed$1("messages");
|
|
1194
1375
|
const names = /* @__PURE__ */ new Map();
|
|
1376
|
+
const pending = [];
|
|
1195
1377
|
const messages = [];
|
|
1196
1378
|
for (const message of body.messages) {
|
|
1197
1379
|
if (!isObj$1(message)) return malformed$1("message");
|
|
1198
|
-
messages.push(
|
|
1380
|
+
messages.push(rewriteMessage(message, ctx.transform, names, pending));
|
|
1199
1381
|
}
|
|
1382
|
+
await runAll$1(pending);
|
|
1200
1383
|
if (ctx.transform.instruction !== void 0) messages.unshift({
|
|
1201
1384
|
role: "system",
|
|
1202
1385
|
content: ctx.transform.instruction
|
|
@@ -1251,56 +1434,87 @@ const signatureKey = (itemId) => `responses:${itemId}`;
|
|
|
1251
1434
|
function refuse(part) {
|
|
1252
1435
|
if (typeof part.type === "string" && REFUSED_PARTS[part.type] === true) throw new UnsupportedContentError(`OpenAI responses ${part.type} content cannot be transformed`);
|
|
1253
1436
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1437
|
+
/** Starts every queued rewrite at once; resolves when all results are written back, rejects on the first failure. */
|
|
1438
|
+
async function runAll(pending) {
|
|
1439
|
+
await Promise.all(pending.map((run) => run()));
|
|
1440
|
+
}
|
|
1441
|
+
/** Queues the rewrite of one text leaf; `set` later replaces the original, which stays in place until then. */
|
|
1442
|
+
function queue(pending, transform, text, leaf, set) {
|
|
1443
|
+
if (text !== "") pending.push(async () => set(await transform.outbound(text, leaf)));
|
|
1256
1444
|
}
|
|
1257
1445
|
/** A slot the walker reads holds a value it cannot classify: refused, never forwarded untransformed. */
|
|
1258
1446
|
function malformed(slot) {
|
|
1259
1447
|
throw new UnsupportedContentError(`OpenAI responses ${slot} has an unexpected shape`);
|
|
1260
1448
|
}
|
|
1261
1449
|
/** A string, or an array of parts whose `input_text`/`output_text` parts are rewritten; anything else is refused. */
|
|
1262
|
-
|
|
1263
|
-
if (typeof content === "string")
|
|
1264
|
-
|
|
1450
|
+
function rewriteContent(content, slot, leaf, transform, pending, set) {
|
|
1451
|
+
if (typeof content === "string") {
|
|
1452
|
+
queue(pending, transform, content, leaf, set);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (!Array.isArray(content)) malformed(slot);
|
|
1265
1456
|
const out = [];
|
|
1266
1457
|
for (const part of content) {
|
|
1267
|
-
if (!isObj(part))
|
|
1458
|
+
if (!isObj(part)) malformed(`${slot} part`);
|
|
1268
1459
|
refuse(part);
|
|
1269
1460
|
if (part.type !== "input_text" && part.type !== "output_text") {
|
|
1270
1461
|
out.push(part);
|
|
1271
1462
|
continue;
|
|
1272
1463
|
}
|
|
1273
|
-
if (typeof part.text !== "string")
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1464
|
+
if (typeof part.text !== "string") malformed(`${slot} ${part.type} part`);
|
|
1465
|
+
const copy = { ...part };
|
|
1466
|
+
out.push(copy);
|
|
1467
|
+
queue(pending, transform, part.text, leaf, (text) => {
|
|
1468
|
+
copy.text = text;
|
|
1277
1469
|
});
|
|
1278
1470
|
}
|
|
1279
|
-
|
|
1471
|
+
set(out);
|
|
1280
1472
|
}
|
|
1281
|
-
|
|
1282
|
-
if (typeof value === "string")
|
|
1473
|
+
function rewriteStrings(value, leaf, transform, pending, set) {
|
|
1474
|
+
if (typeof value === "string") {
|
|
1475
|
+
queue(pending, transform, value, leaf, set);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1283
1478
|
if (Array.isArray(value)) {
|
|
1284
1479
|
const out = [];
|
|
1285
|
-
for (const item of value)
|
|
1286
|
-
|
|
1480
|
+
for (const item of value) {
|
|
1481
|
+
const index = out.push(item) - 1;
|
|
1482
|
+
rewriteStrings(item, leaf, transform, pending, (rewritten) => {
|
|
1483
|
+
out[index] = rewritten;
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
set(out);
|
|
1487
|
+
return;
|
|
1287
1488
|
}
|
|
1288
1489
|
if (isObj(value)) {
|
|
1289
1490
|
const out = {};
|
|
1290
|
-
for (const [key, item] of Object.entries(value))
|
|
1291
|
-
|
|
1491
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1492
|
+
out[key] = item;
|
|
1493
|
+
rewriteStrings(item, leaf, transform, pending, (rewritten) => {
|
|
1494
|
+
out[key] = rewritten;
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
set(out);
|
|
1292
1498
|
}
|
|
1293
|
-
return value;
|
|
1294
1499
|
}
|
|
1295
1500
|
/** Tool-call arguments: a JSON string whose string leaves are rewritten; raw text when it is not JSON. */
|
|
1296
|
-
|
|
1501
|
+
function rewriteArguments(args, leaf, transform, pending, set) {
|
|
1297
1502
|
let parsed;
|
|
1298
1503
|
try {
|
|
1299
1504
|
parsed = JSON.parse(args);
|
|
1300
1505
|
} catch {
|
|
1301
|
-
|
|
1506
|
+
queue(pending, transform, args, leaf, set);
|
|
1507
|
+
return;
|
|
1302
1508
|
}
|
|
1303
|
-
|
|
1509
|
+
const leaves = [];
|
|
1510
|
+
let rewritten = parsed;
|
|
1511
|
+
rewriteStrings(parsed, leaf, transform, leaves, (value) => {
|
|
1512
|
+
rewritten = value;
|
|
1513
|
+
});
|
|
1514
|
+
pending.push(async () => {
|
|
1515
|
+
await runAll(leaves);
|
|
1516
|
+
set(JSON.stringify(rewritten));
|
|
1517
|
+
});
|
|
1304
1518
|
}
|
|
1305
1519
|
/** Restores the string values of a JSON text, keeping its formatting; restored originals are JSON-escaped. */
|
|
1306
1520
|
function restoreArguments(args, transform) {
|
|
@@ -1324,16 +1538,26 @@ function cachedSummary(item, ctx) {
|
|
|
1324
1538
|
if (!Array.isArray(texts) || !Array.isArray(item.summary) || texts.length !== item.summary.length) return void 0;
|
|
1325
1539
|
return texts;
|
|
1326
1540
|
}
|
|
1327
|
-
|
|
1541
|
+
function rewriteReasoning(item, ctx, pending) {
|
|
1328
1542
|
if (item.summary === void 0) return item;
|
|
1329
1543
|
if (!Array.isArray(item.summary)) return malformed("reasoning summary");
|
|
1330
1544
|
for (const part of item.summary) if (!isObj(part) || typeof part.text !== "string") return malformed("reasoning summary part");
|
|
1331
1545
|
const cached = cachedSummary(item, ctx);
|
|
1332
1546
|
const summary = [];
|
|
1333
|
-
for (const [i, part] of item.summary.entries())
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1547
|
+
for (const [i, part] of item.summary.entries()) {
|
|
1548
|
+
if (cached !== void 0) {
|
|
1549
|
+
summary.push({
|
|
1550
|
+
...part,
|
|
1551
|
+
text: cached[i]
|
|
1552
|
+
});
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
const copy = { ...part };
|
|
1556
|
+
summary.push(copy);
|
|
1557
|
+
queue(pending, ctx.transform, part.text, { kind: "thinking" }, (text) => {
|
|
1558
|
+
copy.text = text;
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1337
1561
|
return {
|
|
1338
1562
|
...item,
|
|
1339
1563
|
summary
|
|
@@ -1349,38 +1573,39 @@ function toolNamesByCallId(items) {
|
|
|
1349
1573
|
for (const item of items) if (isObj(item) && item.type === "function_call" && typeof item.call_id === "string" && typeof item.name === "string") names.set(item.call_id, item.name);
|
|
1350
1574
|
return names;
|
|
1351
1575
|
}
|
|
1352
|
-
|
|
1576
|
+
function rewriteItem(item, toolNames, ctx, pending) {
|
|
1577
|
+
const out = { ...item };
|
|
1353
1578
|
if (item.type === "message" || item.type === void 0 && item.role !== void 0) {
|
|
1354
1579
|
const kind = roleKind(item.role);
|
|
1355
1580
|
if (kind === void 0) return malformed("message role");
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1581
|
+
rewriteContent(item.content, "message content", { kind }, ctx.transform, pending, (content) => {
|
|
1582
|
+
out.content = content;
|
|
1583
|
+
});
|
|
1584
|
+
return out;
|
|
1360
1585
|
}
|
|
1361
1586
|
switch (item.type) {
|
|
1362
1587
|
case "function_call": {
|
|
1363
1588
|
if (typeof item.arguments !== "string") return malformed("function_call arguments");
|
|
1364
1589
|
const toolName = typeof item.name === "string" ? item.name : void 0;
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1590
|
+
rewriteArguments(item.arguments, {
|
|
1591
|
+
kind: "toolCall",
|
|
1592
|
+
toolName
|
|
1593
|
+
}, ctx.transform, pending, (args) => {
|
|
1594
|
+
out.arguments = args;
|
|
1595
|
+
});
|
|
1596
|
+
return out;
|
|
1372
1597
|
}
|
|
1373
1598
|
case "function_call_output": {
|
|
1374
1599
|
const toolName = typeof item.call_id === "string" ? toolNames.get(item.call_id) : void 0;
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1600
|
+
rewriteContent(item.output, "function_call_output output", {
|
|
1601
|
+
kind: "toolResult",
|
|
1602
|
+
toolName
|
|
1603
|
+
}, ctx.transform, pending, (output) => {
|
|
1604
|
+
out.output = output;
|
|
1605
|
+
});
|
|
1606
|
+
return out;
|
|
1382
1607
|
}
|
|
1383
|
-
case "reasoning": return rewriteReasoning(item, ctx);
|
|
1608
|
+
case "reasoning": return rewriteReasoning(item, ctx, pending);
|
|
1384
1609
|
default: return item;
|
|
1385
1610
|
}
|
|
1386
1611
|
}
|
|
@@ -1570,20 +1795,26 @@ const openaiResponsesWalker = {
|
|
|
1570
1795
|
async request(body, ctx) {
|
|
1571
1796
|
if (!isObj(body)) return malformed("request body");
|
|
1572
1797
|
const out = { ...body };
|
|
1573
|
-
|
|
1798
|
+
const pending = [];
|
|
1799
|
+
if (typeof body.instructions === "string") queue(pending, ctx.transform, body.instructions, { kind: "system" }, (text) => {
|
|
1800
|
+
out.instructions = text;
|
|
1801
|
+
});
|
|
1574
1802
|
else if (body.instructions !== void 0 && body.instructions !== null) return malformed("instructions");
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1803
|
+
if (typeof body.input === "string") queue(pending, ctx.transform, body.input, { kind: "user" }, (text) => {
|
|
1804
|
+
out.input = text;
|
|
1805
|
+
});
|
|
1578
1806
|
else if (Array.isArray(body.input)) {
|
|
1579
1807
|
const toolNames = toolNamesByCallId(body.input);
|
|
1580
1808
|
const input = [];
|
|
1581
1809
|
for (const item of body.input) {
|
|
1582
1810
|
if (!isObj(item)) return malformed("input item");
|
|
1583
|
-
input.push(
|
|
1811
|
+
input.push(rewriteItem(item, toolNames, ctx, pending));
|
|
1584
1812
|
}
|
|
1585
1813
|
out.input = input;
|
|
1586
1814
|
} else if (body.input !== void 0) return malformed("input");
|
|
1815
|
+
await runAll(pending);
|
|
1816
|
+
const instruction = ctx.transform.instruction;
|
|
1817
|
+
if (instruction !== void 0) out.instructions = typeof out.instructions === "string" && out.instructions !== "" ? `${instruction}\n\n${out.instructions}` : instruction;
|
|
1587
1818
|
return out;
|
|
1588
1819
|
},
|
|
1589
1820
|
response(body, ctx) {
|
|
@@ -2160,110 +2391,4 @@ function sendError(res, status, type, message) {
|
|
|
2160
2391
|
message
|
|
2161
2392
|
} }));
|
|
2162
2393
|
}
|
|
2163
|
-
|
|
2164
|
-
const CONFIG_FILE_NAME = "transform-proxy.yaml";
|
|
2165
|
-
/** The implicit lookup locations, in order: `./.skaile/` then `~/.config/skaile/`. */
|
|
2166
|
-
function configCandidates(cwd, home) {
|
|
2167
|
-
return [join(cwd, ".skaile", CONFIG_FILE_NAME), join(home, ".config", "skaile", CONFIG_FILE_NAME)];
|
|
2168
|
-
}
|
|
2169
|
-
/**
|
|
2170
|
-
* Finds the config file: the explicit path when given (it must exist), otherwise the first
|
|
2171
|
-
* existing implicit candidate. Returns `undefined` when no implicit candidate exists.
|
|
2172
|
-
*/
|
|
2173
|
-
function findProxyConfig(lookup) {
|
|
2174
|
-
if (lookup.explicit !== void 0) {
|
|
2175
|
-
const path = resolve(lookup.cwd, lookup.explicit);
|
|
2176
|
-
if (!existsSync(path)) throw new Error(`transform proxy config not found: ${path}`);
|
|
2177
|
-
return path;
|
|
2178
|
-
}
|
|
2179
|
-
return configCandidates(lookup.cwd, lookup.home).find((candidate) => existsSync(candidate));
|
|
2180
|
-
}
|
|
2181
|
-
/** Finds and loads the config. Throws, naming the searched paths, when none is found. */
|
|
2182
|
-
function resolveProxyConfig(lookup) {
|
|
2183
|
-
const path = findProxyConfig(lookup);
|
|
2184
|
-
if (path === void 0) {
|
|
2185
|
-
const searched = configCandidates(lookup.cwd, lookup.home).join(", ");
|
|
2186
|
-
throw new Error(`no transform proxy config found (pass --config, or create one at ${searched})`);
|
|
2187
|
-
}
|
|
2188
|
-
return {
|
|
2189
|
-
path,
|
|
2190
|
-
options: loadProxyConfig(path)
|
|
2191
|
-
};
|
|
2192
|
-
}
|
|
2193
|
-
const TOP_LEVEL_KEYS = {
|
|
2194
|
-
transforms: true,
|
|
2195
|
-
pipelines: true,
|
|
2196
|
-
defaultPipeline: true,
|
|
2197
|
-
allowUpstreams: true,
|
|
2198
|
-
trace: true
|
|
2199
|
-
};
|
|
2200
|
-
const TRACE_KEYS = {
|
|
2201
|
-
dir: true,
|
|
2202
|
-
maxChars: true
|
|
2203
|
-
};
|
|
2204
|
-
/** Reads and validates a YAML config file. Unknown keys are rejected so that typos fail loudly. */
|
|
2205
|
-
function loadProxyConfig(path) {
|
|
2206
|
-
const source = parse(readFileSync(path, "utf8"));
|
|
2207
|
-
try {
|
|
2208
|
-
return parseProxyConfig(source ?? {}, dirname(path));
|
|
2209
|
-
} catch (err) {
|
|
2210
|
-
throw new Error(`invalid transform proxy config ${path}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
/** Validates an already-parsed config object. Exported for hosts that load YAML themselves. */
|
|
2214
|
-
function parseProxyConfig(source, baseDir) {
|
|
2215
|
-
if (!isRecord(source)) throw new Error("the document must be a mapping");
|
|
2216
|
-
for (const key of Object.keys(source)) if (!Object.hasOwn(TOP_LEVEL_KEYS, key)) throw new Error(`unknown key "${key}"`);
|
|
2217
|
-
return {
|
|
2218
|
-
baseDir,
|
|
2219
|
-
transforms: parseTransforms(source.transforms),
|
|
2220
|
-
pipelines: parsePipelines(source.pipelines),
|
|
2221
|
-
...source.defaultPipeline === void 0 ? {} : { defaultPipeline: requireString(source.defaultPipeline, "defaultPipeline") },
|
|
2222
|
-
...source.allowUpstreams === void 0 ? {} : { allowUpstreams: requireStringList(source.allowUpstreams, "allowUpstreams") },
|
|
2223
|
-
...source.trace === void 0 ? {} : { trace: parseTrace(source.trace, baseDir) }
|
|
2224
|
-
};
|
|
2225
|
-
}
|
|
2226
|
-
function parseTransforms(value) {
|
|
2227
|
-
if (value === void 0) return {};
|
|
2228
|
-
if (!isRecord(value)) throw new Error("transforms must be a mapping of transform id to { module, config }");
|
|
2229
|
-
const out = {};
|
|
2230
|
-
for (const [id, spec] of Object.entries(value)) {
|
|
2231
|
-
if (!isRecord(spec)) throw new Error(`transforms.${id} must be a mapping with a module`);
|
|
2232
|
-
const module = requireString(spec.module, `transforms.${id}.module`);
|
|
2233
|
-
out[id] = spec.config === void 0 ? { module } : {
|
|
2234
|
-
module,
|
|
2235
|
-
config: spec.config
|
|
2236
|
-
};
|
|
2237
|
-
}
|
|
2238
|
-
return out;
|
|
2239
|
-
}
|
|
2240
|
-
function parsePipelines(value) {
|
|
2241
|
-
if (value === void 0) return {};
|
|
2242
|
-
if (!isRecord(value)) throw new Error("pipelines must be a mapping of pipeline id to a list of transform ids");
|
|
2243
|
-
const out = {};
|
|
2244
|
-
for (const [id, stages] of Object.entries(value)) out[id] = requireStringList(stages, `pipelines.${id}`);
|
|
2245
|
-
return out;
|
|
2246
|
-
}
|
|
2247
|
-
function parseTrace(value, baseDir) {
|
|
2248
|
-
if (!isRecord(value)) throw new Error("trace must be a mapping with a dir");
|
|
2249
|
-
for (const key of Object.keys(value)) if (!Object.hasOwn(TRACE_KEYS, key)) throw new Error(`unknown key "trace.${key}"`);
|
|
2250
|
-
const dir = resolve(baseDir, requireString(value.dir, "trace.dir"));
|
|
2251
|
-
if (value.maxChars === void 0) return { dir };
|
|
2252
|
-
if (typeof value.maxChars !== "number" || !Number.isInteger(value.maxChars) || value.maxChars <= 0) throw new Error("trace.maxChars must be a positive integer");
|
|
2253
|
-
return {
|
|
2254
|
-
dir,
|
|
2255
|
-
maxChars: value.maxChars
|
|
2256
|
-
};
|
|
2257
|
-
}
|
|
2258
|
-
function requireString(value, field) {
|
|
2259
|
-
if (typeof value !== "string" || value === "") throw new Error(`${field} must be a non-empty string`);
|
|
2260
|
-
return value;
|
|
2261
|
-
}
|
|
2262
|
-
function requireStringList(value, field) {
|
|
2263
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item === "")) throw new Error(`${field} must be a list of non-empty strings`);
|
|
2264
|
-
return value;
|
|
2265
|
-
}
|
|
2266
|
-
function isRecord(value) {
|
|
2267
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2268
|
-
}
|
|
2269
|
-
export { silentLogger as S, anthropicWalker as _, parseProxyConfig as a, composeTransforms as b, SESSION_HEADER as c, createTransformProxy as d, DEFAULT_TRACE_MAX_CHARS as f, openaiChatWalker as g, openaiResponsesWalker as h, loadProxyConfig as i, TOKEN_HEADER as l, serializeTraceEvent as m, configCandidates as n, resolveProxyConfig as o, createFileTraceSink as p, findProxyConfig as r, PIPELINE_HEADER as s, CONFIG_FILE_NAME as t, UPSTREAM_HEADER as u, UnsupportedContentError as v, errorMessage as x, createSessionFacts as y };
|
|
2394
|
+
export { silentLogger as S, loadProxyConfig as _, createTransformProxy as a, composeTransforms as b, serializeTraceEvent as c, anthropicWalker as d, UnsupportedContentError as f, findProxyConfig as g, configCandidates as h, UPSTREAM_HEADER as i, openaiResponsesWalker as l, CONFIG_FILE_NAME as m, SESSION_HEADER as n, DEFAULT_TRACE_MAX_CHARS as o, createSessionFacts as p, TOKEN_HEADER as r, createFileTraceSink as s, PIPELINE_HEADER as t, openaiChatWalker as u, parseProxyConfig as v, errorMessage as x, resolveProxyConfig as y };
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaile/transform-proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Same-protocol passthrough proxy that runs model requests and replies through a chain of text transforms (Anthropic Messages, OpenAI chat, OpenAI responses)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/skaile-ai/
|
|
9
|
+
"url": "git+https://github.com/skaile-ai/forge.git",
|
|
10
|
+
"directory": "packages/transform-proxy"
|
|
10
11
|
},
|
|
11
|
-
"homepage": "https://github.com/skaile-ai/
|
|
12
|
+
"homepage": "https://github.com/skaile-ai/forge/tree/main/packages/transform-proxy#readme",
|
|
12
13
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/skaile-ai/
|
|
14
|
+
"url": "https://github.com/skaile-ai/forge/issues"
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
16
17
|
"llm",
|
|
@@ -54,9 +55,10 @@
|
|
|
54
55
|
"scripts": {
|
|
55
56
|
"prebuild": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
56
57
|
"build": "tsdown",
|
|
57
|
-
"
|
|
58
|
-
"test": "bun
|
|
59
|
-
"typecheck": "tsc --noEmit"
|
|
58
|
+
"prepublishOnly": "npm run build",
|
|
59
|
+
"test": "bun --bun vitest run",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"check": "bun ../../scripts/check-package.ts"
|
|
60
62
|
},
|
|
61
63
|
"dependencies": {
|
|
62
64
|
"yaml": "^2.9.0"
|
|
@@ -64,7 +66,6 @@
|
|
|
64
66
|
"devDependencies": {
|
|
65
67
|
"@types/bun": "^1.3.0",
|
|
66
68
|
"@types/node": "^22.0.0",
|
|
67
|
-
"tsdown": "^0.23.1",
|
|
68
69
|
"typescript": "^5.9.3"
|
|
69
70
|
}
|
|
70
71
|
}
|