@skaile/transform-proxy 0.3.0 → 0.4.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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ - Library: `TransformProxy.inspect(sessionId, pipelineId?)` returns host-only transform state, `{ <transformId>: state }` for each stage of the session's pipeline instance (default `defaultPipeline`) that implements the new optional `Transform.inspect?()`. A host such as anon-ai reads forge-anon's mapping through it to show what was anonymized. It is in-process only (no HTTP route, never sent), returns `{}` for an unknown session, an unbuilt pipeline or a failed build, and never builds an instance or creates session state.
6
+
7
+ ## 0.3.1
8
+
9
+ - Anthropic walker: the system text that Anthropic's Claude subscription (OAuth) check reads verbatim passes untransformed: the leading Claude Code identity sentence and the leading `x-anthropic-billing-header:` line. The rest of each block is still transformed. forge-anon read `Claude` as a name and the `cc_version` as IP addresses, so every request of a subscription session with anon on got 429 `rate_limit_error` "Error".
10
+
3
11
  ## 0.3.0
4
12
 
5
13
  - omp extension, **behaviour change**: `/anon on` and `/anon off` switch only the omp process they run in (its subagents and its `/new`, `/resume`, `/fork` sessions included) and no longer save the default. Before, every `/anon on|off` wrote `~/.omp/agent/skaile-transform-proxy.json`, so turning anon off in one window turned it off for every omp started later. `/anon on --global` and `/anon off --global` also save the default. A saved `on` is applied at session start only until the first successful `/anon on|off` in that process, so a plain `/anon off` stays off across `/new`. An existing saved `enabled: true` still turns anon on in new omp processes; run `/anon off --global` to clear it.
package/README.md CHANGED
@@ -85,6 +85,8 @@ await proxy.dropSession('s1')
85
85
  await proxy.close()
86
86
  ```
87
87
 
88
+ `await proxy.inspect('s1')` lets the host read what the session's transforms hold, e.g. forge-anon's mapping for highlights: `{ <transformId>: state }`, one entry per stage of the pipeline (`inspect('s1', 'anon')`; default `defaultPipeline`) whose `Transform` implements `inspect()`. It is in-process only: there is no HTTP route, and the result is never sent anywhere. An unknown session, a pipeline that has not been built for the session yet, or a failed build gives `{}`; `inspect` never builds an instance or creates session state.
89
+
88
90
  ## omp extension
89
91
 
90
92
  The package ships an [omp](https://omp.sh) extension (subpath `@skaile/transform-proxy/omp`, declared in the package manifest under `omp.extensions`):
@@ -128,10 +130,12 @@ A module default-exports a `TransformModule`:
128
130
 
129
131
  - `open(config, { baseDir, log })` runs once per proxy. It validates the config and builds shared state. Throwing rejects startup.
130
132
  - It returns a `TransformFactory`, which runs once per `(session, pipeline)`: `(ctx: { sessionId, facts, log }) => Transform`.
131
- - A `Transform` has `instruction?`, `outbound(text, leaf)`, `inbound(text)`, `inboundStream()` and `dispose?()`.
133
+ - A `Transform` has `instruction?`, `outbound(text, leaf)`, `inbound(text)`, `inboundStream()`, `dispose?()` and `inspect?()` (host-only state, read through `proxy.inspect`; the walkers never call it).
132
134
 
133
135
  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.
134
136
 
137
+ Those leading blocks also pass untransformed: a system text that starts with the Claude Code identity sentence (`You are Claude Code, Anthropic's official CLI for Claude.`) keeps that sentence, and one that starts with `x-anthropic-billing-header:` keeps that first line; the rest of the text is transformed. Anthropic's Claude subscription (OAuth) check reads them verbatim and answers 429 `rate_limit_error` with the message `Error` when they differ, and a German NER reads `Claude` as a name and a `cc_version` as an IP address. The host writes these blocks; they carry no user data.
138
+
135
139
  `inboundStream().push(c1) + … + flush()` must equal `inbound(c1 + … + cn)` for every chunking.
136
140
 
137
141
  Connectors publish facts through `proxy.session(id).publisher(source)`. Facts are in-process only.
package/dist/cli.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as TransformProxy } from "./server-C_eKyCTV.mjs";
1
+ import { i as TransformProxy } from "./server-CicvlQ7q.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
  export interface CliEnvironment {
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { d as createTransformProxy, o as resolveProxyConfig, v as errorMessage } from "./config-PKHxejC8.mjs";
2
+ import { d as createTransformProxy, o as resolveProxyConfig, v as errorMessage } from "./config-Ceb5AF_a.mjs";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { homedir } from "node:os";
@@ -323,8 +323,8 @@ async function outbound$2(transform, text, leaf) {
323
323
  return text === "" ? text : transform.outbound(text, leaf);
324
324
  }
325
325
  /** A string, or an array of blocks whose `text` blocks are rewritten; anything else is refused. */
326
- async function rewriteText(content, slot, leaf, transform) {
327
- if (typeof content === "string") return outbound$2(transform, content, leaf);
326
+ async function rewriteText(content, slot, rewrite) {
327
+ if (typeof content === "string") return rewrite(content);
328
328
  if (!Array.isArray(content)) return malformed$2(slot);
329
329
  const out = [];
330
330
  for (const block of content) {
@@ -337,11 +337,30 @@ async function rewriteText(content, slot, leaf, transform) {
337
337
  if (typeof block.text !== "string") return malformed$2(`${slot} text block`);
338
338
  out.push({
339
339
  ...block,
340
- text: await outbound$2(transform, block.text, leaf)
340
+ text: await rewrite(block.text)
341
341
  });
342
342
  }
343
343
  return out;
344
344
  }
345
+ /**
346
+ * Host-written system text that Anthropic's Claude subscription (OAuth) check reads verbatim: the
347
+ * billing header line and the Claude Code identity sentence. Rewritten ("CLI for <NAME_…>",
348
+ * "cc_version=<CONTACT_IP_…>"), the request is answered 429 `rate_limit_error` "Error". Neither
349
+ * carries user data.
350
+ */
351
+ const BILLING_HEADER = "x-anthropic-billing-header:";
352
+ const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
353
+ /** The leading part of a system text that passes untransformed; `''` when there is none. */
354
+ function gatePrefix(text) {
355
+ if (text.startsWith(CLAUDE_CODE_IDENTITY)) return CLAUDE_CODE_IDENTITY;
356
+ if (!text.startsWith(BILLING_HEADER)) return "";
357
+ const end = text.indexOf("\n");
358
+ return end === -1 ? text : text.slice(0, end);
359
+ }
360
+ async function rewriteSystemText(text, transform) {
361
+ const kept = gatePrefix(text);
362
+ return kept + await outbound$2(transform, text.slice(kept.length), { kind: "system" });
363
+ }
345
364
  /** Every string leaf of a JSON value (tool-call input); keys stay as they are. */
346
365
  async function rewriteStrings$2(value, leaf, transform) {
347
366
  if (typeof value === "string") return outbound$2(transform, value, leaf);
@@ -421,10 +440,10 @@ async function rewriteContent$2(content, role, toolNames, ctx) {
421
440
  const toolName = typeof block.tool_use_id === "string" ? toolNames.get(block.tool_use_id) : void 0;
422
441
  out.push({
423
442
  ...block,
424
- content: await rewriteText(block.content, "tool_result content", {
443
+ content: await rewriteText(block.content, "tool_result content", (text) => outbound$2(ctx.transform, text, {
425
444
  kind: "toolResult",
426
445
  toolName
427
- }, ctx.transform)
446
+ }))
428
447
  });
429
448
  break;
430
449
  }
@@ -567,7 +586,7 @@ const anthropicWalker = {
567
586
  async request(body, ctx) {
568
587
  if (!isObj$2(body)) return malformed$2("request body");
569
588
  const out = { ...body };
570
- if (body.system !== void 0) out.system = await rewriteText(body.system, "system", { kind: "system" }, ctx.transform);
589
+ if (body.system !== void 0) out.system = await rewriteText(body.system, "system", (text) => rewriteSystemText(text, ctx.transform));
571
590
  if (ctx.transform.instruction !== void 0) out.system = withInstruction(out.system, ctx.transform.instruction);
572
591
  if (body.messages !== void 0) {
573
592
  if (!Array.isArray(body.messages)) return malformed$2("messages");
@@ -1405,6 +1424,8 @@ async function createTransformProxy(options) {
1405
1424
  const factories = await openTransforms(options.transforms, options.baseDir, log);
1406
1425
  const pipelines = options.pipelines;
1407
1426
  const sessions = /* @__PURE__ */ new Map();
1427
+ /** Composed instance -> its stages with their transform ids, for `inspect`. */
1428
+ const stagesOf = /* @__PURE__ */ new WeakMap();
1408
1429
  function stateFor(id) {
1409
1430
  const existing = sessions.get(id);
1410
1431
  if (existing) return existing;
@@ -1437,7 +1458,12 @@ async function createTransformProxy(options) {
1437
1458
  }
1438
1459
  throw err;
1439
1460
  }
1440
- return composeTransforms(stages, log);
1461
+ const composed = composeTransforms(stages, log);
1462
+ stagesOf.set(composed, ids.map((id, i) => ({
1463
+ id,
1464
+ transform: stages[i]
1465
+ })));
1466
+ return composed;
1441
1467
  }
1442
1468
  async function handle(req, res) {
1443
1469
  const url = new URL(req.url ?? "/", "http://proxy.invalid");
@@ -1554,6 +1580,22 @@ async function createTransformProxy(options) {
1554
1580
  sessions.delete(id);
1555
1581
  await disposeInstances(state, log);
1556
1582
  },
1583
+ async inspect(sessionId, pipelineId) {
1584
+ const state = sessions.get(sessionId);
1585
+ const pipeline = pipelineId ?? options.defaultPipeline;
1586
+ if (state === void 0 || pipeline === void 0) return {};
1587
+ const pending = state.instances.get(pipeline);
1588
+ if (pending === void 0) return {};
1589
+ let instance;
1590
+ try {
1591
+ instance = await pending;
1592
+ } catch {
1593
+ return {};
1594
+ }
1595
+ const result = {};
1596
+ for (const stage of stagesOf.get(instance) ?? []) if (typeof stage.transform.inspect === "function") result[stage.id] = stage.transform.inspect();
1597
+ return result;
1598
+ },
1557
1599
  async close() {
1558
1600
  const closed = Promise.withResolvers();
1559
1601
  server.close((err) => err ? closed.reject(err) : closed.resolve());
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
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-C_eKyCTV.mjs";
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-CicvlQ7q.mjs";
2
2
  /** One stage's streamed inbound transformer, as returned by `Transform.inboundStream()`. */
3
3
  interface InboundStream {
4
4
  push(chunk: string): string;
package/dist/index.mjs CHANGED
@@ -1 +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-PKHxejC8.mjs";
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-Ceb5AF_a.mjs";
@@ -1,4 +1,4 @@
1
- import { c as SESSION_HEADER, i as loadProxyConfig, l as TOKEN_HEADER, n as configCandidates, r as findProxyConfig, u as UPSTREAM_HEADER, v as errorMessage } from "../config-PKHxejC8.mjs";
1
+ import { c as SESSION_HEADER, i as loadProxyConfig, l as TOKEN_HEADER, n as configCandidates, r as findProxyConfig, u as UPSTREAM_HEADER, v as errorMessage } from "../config-Ceb5AF_a.mjs";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
@@ -25,6 +25,8 @@ interface Transform {
25
25
  };
26
26
  /** Called once when the proxy drops the session. */
27
27
  dispose?(): void | Promise<void>;
28
+ /** Host-only state of this instance (e.g. the anon mapping), read through TransformProxy.inspect. Never called by the walkers, never sent anywhere. */
29
+ inspect?(): unknown;
28
30
  }
29
31
  interface SessionFact {
30
32
  readonly kind: string;
@@ -86,6 +88,12 @@ interface TransformProxy {
86
88
  };
87
89
  /** Disposes the session's transform instances and forgets its facts and caches. */
88
90
  dropSession(id: string): Promise<void>;
91
+ /**
92
+ * Host-only, in-process: each stage's `inspect()` result of the session's pipeline instance, keyed by transform id.
93
+ * `{}` when the session or pipeline is unknown, the instance is not built, or its build failed.
94
+ * Never builds an instance or creates session state; there is no HTTP route for it.
95
+ */
96
+ inspect(sessionId: string, pipelineId?: string): Promise<Record<string, unknown>>;
89
97
  close(): Promise<void>;
90
98
  }
91
99
  declare const UPSTREAM_HEADER = "x-skaile-transform-upstream";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaile/transform-proxy",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",