orcareplay 0.1.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.
Files changed (109) hide show
  1. package/dist/api.d.ts +127 -0
  2. package/dist/api.d.ts.map +1 -0
  3. package/dist/api.js +159 -0
  4. package/dist/api.js.map +1 -0
  5. package/dist/args.d.ts +24 -0
  6. package/dist/args.d.ts.map +1 -0
  7. package/dist/args.js +124 -0
  8. package/dist/args.js.map +1 -0
  9. package/dist/cli.d.ts +3 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +4 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/commands/compare.d.ts +27 -0
  14. package/dist/commands/compare.d.ts.map +1 -0
  15. package/dist/commands/compare.js +182 -0
  16. package/dist/commands/compare.js.map +1 -0
  17. package/dist/commands/doctor.d.ts +24 -0
  18. package/dist/commands/doctor.d.ts.map +1 -0
  19. package/dist/commands/doctor.js +358 -0
  20. package/dist/commands/doctor.js.map +1 -0
  21. package/dist/commands/gc.d.ts +24 -0
  22. package/dist/commands/gc.d.ts.map +1 -0
  23. package/dist/commands/gc.js +326 -0
  24. package/dist/commands/gc.js.map +1 -0
  25. package/dist/commands/inspect.d.ts +24 -0
  26. package/dist/commands/inspect.d.ts.map +1 -0
  27. package/dist/commands/inspect.js +262 -0
  28. package/dist/commands/inspect.js.map +1 -0
  29. package/dist/commands/record.d.ts +17 -0
  30. package/dist/commands/record.d.ts.map +1 -0
  31. package/dist/commands/record.js +497 -0
  32. package/dist/commands/record.js.map +1 -0
  33. package/dist/commands/replay.d.ts +43 -0
  34. package/dist/commands/replay.d.ts.map +1 -0
  35. package/dist/commands/replay.js +800 -0
  36. package/dist/commands/replay.js.map +1 -0
  37. package/dist/commands/scrub.d.ts +34 -0
  38. package/dist/commands/scrub.d.ts.map +1 -0
  39. package/dist/commands/scrub.js +452 -0
  40. package/dist/commands/scrub.js.map +1 -0
  41. package/dist/commands/setup.d.ts +23 -0
  42. package/dist/commands/setup.d.ts.map +1 -0
  43. package/dist/commands/setup.js +177 -0
  44. package/dist/commands/setup.js.map +1 -0
  45. package/dist/config.d.ts +80 -0
  46. package/dist/config.d.ts.map +1 -0
  47. package/dist/config.js +117 -0
  48. package/dist/config.js.map +1 -0
  49. package/dist/exchange-events.d.ts +72 -0
  50. package/dist/exchange-events.d.ts.map +1 -0
  51. package/dist/exchange-events.js +170 -0
  52. package/dist/exchange-events.js.map +1 -0
  53. package/dist/flags.d.ts +10 -0
  54. package/dist/flags.d.ts.map +1 -0
  55. package/dist/flags.js +83 -0
  56. package/dist/flags.js.map +1 -0
  57. package/dist/fs-events.d.ts +19 -0
  58. package/dist/fs-events.d.ts.map +1 -0
  59. package/dist/fs-events.js +53 -0
  60. package/dist/fs-events.js.map +1 -0
  61. package/dist/index.d.ts +18 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +18 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/main.d.ts +2 -0
  66. package/dist/main.d.ts.map +1 -0
  67. package/dist/main.js +282 -0
  68. package/dist/main.js.map +1 -0
  69. package/dist/mcp-server.d.ts +44 -0
  70. package/dist/mcp-server.d.ts.map +1 -0
  71. package/dist/mcp-server.js +245 -0
  72. package/dist/mcp-server.js.map +1 -0
  73. package/dist/mcp.d.ts +111 -0
  74. package/dist/mcp.d.ts.map +1 -0
  75. package/dist/mcp.js +221 -0
  76. package/dist/mcp.js.map +1 -0
  77. package/dist/out.d.ts +69 -0
  78. package/dist/out.d.ts.map +1 -0
  79. package/dist/out.js +138 -0
  80. package/dist/out.js.map +1 -0
  81. package/dist/rasterize.d.ts +62 -0
  82. package/dist/rasterize.d.ts.map +1 -0
  83. package/dist/rasterize.js +154 -0
  84. package/dist/rasterize.js.map +1 -0
  85. package/dist/serial.d.ts +19 -0
  86. package/dist/serial.d.ts.map +1 -0
  87. package/dist/serial.js +34 -0
  88. package/dist/serial.js.map +1 -0
  89. package/dist/share-card.d.ts +73 -0
  90. package/dist/share-card.d.ts.map +1 -0
  91. package/dist/share-card.js +413 -0
  92. package/dist/share-card.js.map +1 -0
  93. package/dist/snapshot.d.ts +30 -0
  94. package/dist/snapshot.d.ts.map +1 -0
  95. package/dist/snapshot.js +43 -0
  96. package/dist/snapshot.js.map +1 -0
  97. package/dist/tls-capture.d.ts +57 -0
  98. package/dist/tls-capture.d.ts.map +1 -0
  99. package/dist/tls-capture.js +198 -0
  100. package/dist/tls-capture.js.map +1 -0
  101. package/dist/upstream.d.ts +20 -0
  102. package/dist/upstream.d.ts.map +1 -0
  103. package/dist/upstream.js +31 -0
  104. package/dist/upstream.js.map +1 -0
  105. package/dist/version.d.ts +2 -0
  106. package/dist/version.d.ts.map +1 -0
  107. package/dist/version.js +2 -0
  108. package/dist/version.js.map +1 -0
  109. package/package.json +49 -0
@@ -0,0 +1,177 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { modelInfoFor } from '@orcareplay/providers';
3
+ import { configPath, gatewayHeaders, ORCAROUTER_CONSOLE, ORCAROUTER_URL, readConfig, writeConfig, } from '../config.js';
4
+ /** OpenAI-compatible model listing, which every gateway worth pointing orca at implements. */
5
+ async function probeModels(gateway, headers) {
6
+ const res = await fetch(`${gateway.replace(/\/+$/, '')}/v1/models`, { headers });
7
+ if (!res.ok)
8
+ throw new Error(`${res.status} ${res.statusText}`);
9
+ const body = (await res.json());
10
+ return (body.data ?? [])
11
+ .map((m) => m.id)
12
+ .filter((id) => typeof id === 'string')
13
+ .sort();
14
+ }
15
+ /** Prompts on a real terminal, and refuses to be one when nothing is attached. */
16
+ function terminalAsk() {
17
+ if (!process.stdin.isTTY)
18
+ return undefined;
19
+ return async (question) => {
20
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
21
+ try {
22
+ return (await rl.question(question)).trim();
23
+ }
24
+ finally {
25
+ rl.close();
26
+ }
27
+ };
28
+ }
29
+ export async function setupCommand(args, out, deps = {}) {
30
+ const env = deps.env ?? process.env;
31
+ const probe = deps.probe ?? probeModels;
32
+ const ask = deps.ask ?? terminalAsk();
33
+ let url = args.str('gateway');
34
+ let key = args.str('key');
35
+ const keyEnv = args.str('key-env');
36
+ // OrcaRouter fills the blank, and Enter accepts it. Offered rather than imposed: the whole point
37
+ // of a gateway is that one origin serves several models, and most people asking for that do not
38
+ // have one already — but anyone who does types over it, and `--gateway` skips the question.
39
+ if (!url && ask)
40
+ url = await ask(`Gateway URL (serves the model APIs) [${ORCAROUTER_URL}]: `);
41
+ if (!url)
42
+ url = ORCAROUTER_URL;
43
+ if (!key && !keyEnv && ask) {
44
+ if (sameOrigin(url, ORCAROUTER_URL)) {
45
+ out.plain(` get a key at ${ORCAROUTER_CONSOLE} — OrcaRouter keys start sk-orca-`);
46
+ }
47
+ key = await ask('API key (stored 0600; leave blank for none): ');
48
+ }
49
+ const gateway = { url };
50
+ // Stored key wins if both are given, and only one is ever written: keeping both would leave a
51
+ // credential on disk for someone who explicitly asked not to have one.
52
+ if (key)
53
+ gateway.api_key = key;
54
+ else if (keyEnv)
55
+ gateway.api_key_env = keyEnv;
56
+ const existing = await readConfig(env);
57
+ const config = { ...existing, gateway };
58
+ // Ask before saving is tempting, but saving first means an unreachable gateway still leaves you
59
+ // configured — being offline should not stop you setting up the thing you will use online.
60
+ const path = await writeConfig(config, env);
61
+ // `auth:`, not `key:` — the terminal guard redacts any field named `key`, which is right in
62
+ // general and would hide the one thing this line exists to tell you: whether a key was stored at
63
+ // all, and where it came from. The value is a description, never the credential.
64
+ out.info('config.saved', { path, mode: '0600', gateway: url, auth: describeKey(gateway) });
65
+ let available = [];
66
+ try {
67
+ available = await probe(url, gatewayHeaders(config, env));
68
+ if (available.length === 0) {
69
+ out.warn('gateway.no_models', { note: 'reachable, but it listed no models' });
70
+ }
71
+ else {
72
+ out.plain('');
73
+ out.plain(` ${available.length} models available:`);
74
+ for (const m of available.slice(0, 20))
75
+ out.plain(` ${m}`);
76
+ if (available.length > 20)
77
+ out.plain(` … and ${available.length - 20} more — orca models`);
78
+ }
79
+ // Ask which of them to compare by default, so `orca compare` needs no flags afterwards. This
80
+ // is the half that makes setup worth running: a gateway with nothing chosen still leaves you
81
+ // typing a model list on every invocation, which is what the command exists to remove.
82
+ const chosen = args.str('models') ?? (ask ? await askModels(ask, available) : undefined);
83
+ if (chosen) {
84
+ config.models = chosen
85
+ .split(',')
86
+ .map((m) => m.trim())
87
+ .filter((m) => m !== '');
88
+ if (config.models.length > 0) {
89
+ await writeConfig(config, env);
90
+ out.info('config.models', { models: config.models.join(',') });
91
+ }
92
+ }
93
+ }
94
+ catch (err) {
95
+ out.warn('gateway.unreachable', {
96
+ why: String(err instanceof Error ? err.message : err),
97
+ note: 'the config was saved; fix the URL or key and run orca setup again',
98
+ });
99
+ }
100
+ out.plain('');
101
+ // Built from what the gateway actually serves, never from two model names picked here. Model ids
102
+ // are gateway-specific — OrcaRouter namespaces them by provider, a direct provider does not — so a
103
+ // hardcoded pair is a copyable line that fails against the gateway orca just configured.
104
+ out.plain(nextStep(config, available));
105
+ return config;
106
+ }
107
+ /** The line worth copying next, using real model ids wherever we have them. */
108
+ function nextStep(config, available) {
109
+ if (config.models && config.models.length > 0)
110
+ return ' orca compare last --verify "npm test"';
111
+ if (available.length > 0) {
112
+ return ` orca compare last --models ${available.slice(0, 2).join(',')} --verify "npm test"`;
113
+ }
114
+ return ' orca models # what this gateway serves, then compare two of them';
115
+ }
116
+ /** Offer the gateway's own list, so the answer is a choice rather than a spelling test. */
117
+ async function askModels(ask, available) {
118
+ const suggestion = available.slice(0, 3).join(',');
119
+ const answer = await ask(`Models to compare by default${suggestion ? ` [${suggestion}]` : ''} (comma-separated, blank to skip): `);
120
+ return answer === '' ? (suggestion === '' ? undefined : suggestion) : answer;
121
+ }
122
+ export async function modelsCommand(args, out, deps = {}) {
123
+ const env = deps.env ?? process.env;
124
+ const probe = deps.probe ?? probeModels;
125
+ const config = await readConfig(env);
126
+ if (!config.gateway?.url) {
127
+ out.plain('no gateway configured');
128
+ out.plain('');
129
+ out.plain(` orca setup # ${ORCAROUTER_URL}, or any gateway you name`);
130
+ out.plain(` orca setup --gateway <url> --key <key>`);
131
+ out.plain('');
132
+ out.plain(` a key for the default gateway: ${ORCAROUTER_CONSOLE}`);
133
+ return [];
134
+ }
135
+ let models;
136
+ try {
137
+ models = await probe(config.gateway.url, gatewayHeaders(config, env));
138
+ }
139
+ catch (err) {
140
+ out.failure({
141
+ event: 'gateway.unreachable',
142
+ what: `could not reach ${config.gateway.url}`,
143
+ why: String(err instanceof Error ? err.message : err),
144
+ next: `check the URL and key in ${configPath(env)}, or run orca setup again`,
145
+ });
146
+ return [];
147
+ }
148
+ // Price where we know it, a dash where we do not. Inventing a number for an unknown model is
149
+ // how a comparison table ends up quoting a cost that was never real.
150
+ out.table(['MODEL', '$/MTOK IN', '$/MTOK OUT'], models.map((id) => {
151
+ const info = modelInfoFor(id);
152
+ return [
153
+ id,
154
+ info ? String(info.input_price_per_mtok) : '—',
155
+ info ? String(info.output_price_per_mtok) : '—',
156
+ ];
157
+ }));
158
+ return models;
159
+ }
160
+ /** Same origin, tolerating a trailing slash — used only to decide whether to print the key hint. */
161
+ function sameOrigin(a, b) {
162
+ try {
163
+ return new URL(a).origin === new URL(b).origin;
164
+ }
165
+ catch {
166
+ return a.replace(/\/+$/, '') === b.replace(/\/+$/, '');
167
+ }
168
+ }
169
+ /** Says whether a key is set and where it came from, never what it is. */
170
+ function describeKey(gateway) {
171
+ if (gateway.api_key)
172
+ return 'stored';
173
+ if (gateway.api_key_env)
174
+ return `from $${gateway.api_key_env}`;
175
+ return 'none';
176
+ }
177
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ import type { ParsedArgs } from './args.js';
2
+ /**
3
+ * User-level configuration, which exists for exactly one job: let
4
+ * `orca compare --models a,b,c` reach several models without re-typing a gateway URL and a key on
5
+ * every invocation.
6
+ *
7
+ * That means this file holds a credential, and everything here follows from it. The file is
8
+ * `0600` inside a `0700` directory, the key can live in an environment variable instead of on
9
+ * disk, and nothing ever prints it back. The key itself never enters a trace either — the proxy
10
+ * adds it to the outbound request only, while what gets recorded is derived from the *incoming*
11
+ * request with auth stripped, so a gateway key orca injects is invisible to the recording by
12
+ * construction rather than by a rule someone has to remember.
13
+ */
14
+ /**
15
+ * OrcaRouter, the gateway orca suggests when you do not name one.
16
+ *
17
+ * A default, never a redirect. It fills in the blank when you ask for a gateway — `orca setup` with
18
+ * no `--gateway` — and nothing more: a run with no gateway configured still proxies the agent's own
19
+ * traffic straight to the provider the agent was already talking to, on the agent's own key. Sending
20
+ * that somewhere the user never named would mean posting their source code to a third party as a
21
+ * side effect of pressing record, and their existing provider key would not authenticate there
22
+ * anyway. Naming a default is a recommendation; rerouting unconfigured traffic would be a decision
23
+ * taken on someone's behalf.
24
+ *
25
+ * The **origin**, deliberately without the `/v1` an OpenAI SDK wants. That SDK is configured with
26
+ * `base_url=https://api.orcarouter.ai/v1` because it appends only `/chat/completions`; orca appends
27
+ * the whole dialect path (`/v1/messages` or `/v1/chat/completions`) and probes `/v1/models`, so a
28
+ * `/v1` here would produce `/v1/v1/chat/completions`. Confirmed against the maintainers' own
29
+ * published action, whose `orcarouter-url` input defaults to
30
+ * `https://api.orcarouter.ai/v1/chat/completions`.
31
+ *
32
+ * Model ids there are namespaced by provider — `anthropic/claude-sonnet-4.6`,
33
+ * `openai/gpt-4o-mini`. Both places that read a model id already cope: dialect selection matches
34
+ * `(?:.*\/)?claude[-.]`, and `resolveModelId` strips the namespace before pricing.
35
+ */
36
+ export declare const ORCAROUTER_URL = "https://api.orcarouter.ai";
37
+ /** Where a person gets a key for the default gateway. Printed, never fetched. */
38
+ export declare const ORCAROUTER_CONSOLE = "https://www.orcarouter.ai/console/token";
39
+ export interface GatewayConfig {
40
+ /** Origin that serves the model APIs. One gateway usually serves both wire formats. */
41
+ url: string;
42
+ /** Stored key. Prefer `api_key_env` if you would rather not keep a credential on disk. */
43
+ api_key?: string;
44
+ /** Name of an environment variable to read the key from at call time. */
45
+ api_key_env?: string;
46
+ }
47
+ export interface OrcaConfig {
48
+ gateway?: GatewayConfig;
49
+ /** Default model list for `orca compare` when `--models` is not given. */
50
+ models?: string[];
51
+ }
52
+ /** XDG, with the documented fallback. Honours `XDG_CONFIG_HOME` so tests need no real home. */
53
+ export declare function configPath(env?: NodeJS.ProcessEnv): string;
54
+ /**
55
+ * Read the config, or an empty one.
56
+ *
57
+ * Never throws. A hand-edited file with a stray comma should cost you the gateway setting, not
58
+ * every orca command — and a tool people reach for when something is already broken is the worst
59
+ * possible thing to have its own unrecoverable failure mode.
60
+ */
61
+ export declare function readConfig(env?: NodeJS.ProcessEnv): Promise<OrcaConfig>;
62
+ export declare function writeConfig(config: OrcaConfig, env?: NodeJS.ProcessEnv): Promise<string>;
63
+ /**
64
+ * The auth header for the configured gateway, or nothing.
65
+ *
66
+ * Returns `{}` rather than an empty bearer when no key is available. An `Authorization: Bearer `
67
+ * with nothing after it is worse than sending none: a gateway may accept it as an anonymous
68
+ * session, and the user never learns their key was not applied.
69
+ */
70
+ export declare function gatewayHeaders(config: OrcaConfig, env?: NodeJS.ProcessEnv): Record<string, string>;
71
+ /**
72
+ * Where live model calls go: flag, then environment, then the configured gateway.
73
+ *
74
+ * Needed by record *and* by replay — `--loose` and any fork continue live, and a fork that ignored
75
+ * the override would quietly talk to the real provider instead of the gateway the user pointed it
76
+ * at. Same precedence, whichever command is running: the more specific and more recent the
77
+ * instruction, the more it wins.
78
+ */
79
+ export declare function resolveUpstream(args: ParsedArgs, env?: NodeJS.ProcessEnv): Promise<Record<string, string> | undefined>;
80
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C;;;;;;;;;;;GAWG;AACH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,cAAc,8BAA8B,CAAC;AAE1D,iFAAiF;AACjF,eAAO,MAAM,kBAAkB,4CAA4C,CAAC;AAE5E,MAAM,WAAW,aAAa;IAC5B,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC;IACZ,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,+FAA+F;AAC/F,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAGvE;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ1F;AAED,wBAAsB,WAAW,CAC/B,MAAM,EAAE,UAAU,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,MAAM,CAAC,CASjB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,UAAU,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,UAAU,EAChB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAgB7C"}
package/dist/config.js ADDED
@@ -0,0 +1,117 @@
1
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ /**
5
+ * User-level configuration, which exists for exactly one job: let
6
+ * `orca compare --models a,b,c` reach several models without re-typing a gateway URL and a key on
7
+ * every invocation.
8
+ *
9
+ * That means this file holds a credential, and everything here follows from it. The file is
10
+ * `0600` inside a `0700` directory, the key can live in an environment variable instead of on
11
+ * disk, and nothing ever prints it back. The key itself never enters a trace either — the proxy
12
+ * adds it to the outbound request only, while what gets recorded is derived from the *incoming*
13
+ * request with auth stripped, so a gateway key orca injects is invisible to the recording by
14
+ * construction rather than by a rule someone has to remember.
15
+ */
16
+ /**
17
+ * OrcaRouter, the gateway orca suggests when you do not name one.
18
+ *
19
+ * A default, never a redirect. It fills in the blank when you ask for a gateway — `orca setup` with
20
+ * no `--gateway` — and nothing more: a run with no gateway configured still proxies the agent's own
21
+ * traffic straight to the provider the agent was already talking to, on the agent's own key. Sending
22
+ * that somewhere the user never named would mean posting their source code to a third party as a
23
+ * side effect of pressing record, and their existing provider key would not authenticate there
24
+ * anyway. Naming a default is a recommendation; rerouting unconfigured traffic would be a decision
25
+ * taken on someone's behalf.
26
+ *
27
+ * The **origin**, deliberately without the `/v1` an OpenAI SDK wants. That SDK is configured with
28
+ * `base_url=https://api.orcarouter.ai/v1` because it appends only `/chat/completions`; orca appends
29
+ * the whole dialect path (`/v1/messages` or `/v1/chat/completions`) and probes `/v1/models`, so a
30
+ * `/v1` here would produce `/v1/v1/chat/completions`. Confirmed against the maintainers' own
31
+ * published action, whose `orcarouter-url` input defaults to
32
+ * `https://api.orcarouter.ai/v1/chat/completions`.
33
+ *
34
+ * Model ids there are namespaced by provider — `anthropic/claude-sonnet-4.6`,
35
+ * `openai/gpt-4o-mini`. Both places that read a model id already cope: dialect selection matches
36
+ * `(?:.*\/)?claude[-.]`, and `resolveModelId` strips the namespace before pricing.
37
+ */
38
+ export const ORCAROUTER_URL = 'https://api.orcarouter.ai';
39
+ /** Where a person gets a key for the default gateway. Printed, never fetched. */
40
+ export const ORCAROUTER_CONSOLE = 'https://www.orcarouter.ai/console/token';
41
+ /** XDG, with the documented fallback. Honours `XDG_CONFIG_HOME` so tests need no real home. */
42
+ export function configPath(env = process.env) {
43
+ const base = env.XDG_CONFIG_HOME ?? join(env.HOME ?? homedir(), '.config');
44
+ return join(base, 'orca', 'config.json');
45
+ }
46
+ /**
47
+ * Read the config, or an empty one.
48
+ *
49
+ * Never throws. A hand-edited file with a stray comma should cost you the gateway setting, not
50
+ * every orca command — and a tool people reach for when something is already broken is the worst
51
+ * possible thing to have its own unrecoverable failure mode.
52
+ */
53
+ export async function readConfig(env = process.env) {
54
+ try {
55
+ const raw = await readFile(configPath(env), 'utf8');
56
+ const parsed = JSON.parse(raw);
57
+ return parsed !== null && typeof parsed === 'object' ? parsed : {};
58
+ }
59
+ catch {
60
+ return {};
61
+ }
62
+ }
63
+ export async function writeConfig(config, env = process.env) {
64
+ const path = configPath(env);
65
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
66
+ // Set explicitly as well as passed to mkdir: an existing directory keeps whatever mode it had,
67
+ // and a 0755 directory makes the file's 0600 decoration.
68
+ await chmod(dirname(path), 0o700).catch(() => { });
69
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
70
+ await chmod(path, 0o600);
71
+ return path;
72
+ }
73
+ /**
74
+ * The auth header for the configured gateway, or nothing.
75
+ *
76
+ * Returns `{}` rather than an empty bearer when no key is available. An `Authorization: Bearer `
77
+ * with nothing after it is worse than sending none: a gateway may accept it as an anonymous
78
+ * session, and the user never learns their key was not applied.
79
+ */
80
+ export function gatewayHeaders(config, env = process.env) {
81
+ const gateway = config.gateway;
82
+ if (!gateway)
83
+ return {};
84
+ const key = gateway.api_key ?? (gateway.api_key_env ? env[gateway.api_key_env] : undefined) ?? '';
85
+ if (key === '')
86
+ return {};
87
+ // Both header names, because a gateway fronting both wire formats reads whichever its callers
88
+ // send — and sending an extra header costs nothing while guessing wrong costs a confusing 401.
89
+ return { authorization: `Bearer ${key}`, 'x-api-key': key };
90
+ }
91
+ /**
92
+ * Where live model calls go: flag, then environment, then the configured gateway.
93
+ *
94
+ * Needed by record *and* by replay — `--loose` and any fork continue live, and a fork that ignored
95
+ * the override would quietly talk to the real provider instead of the gateway the user pointed it
96
+ * at. Same precedence, whichever command is running: the more specific and more recent the
97
+ * instruction, the more it wins.
98
+ */
99
+ export async function resolveUpstream(args, env = process.env) {
100
+ const config = await readConfig(env);
101
+ const gateway = config.gateway?.url;
102
+ const out = {};
103
+ const anthropic = args.str('upstream-anthropic') ?? env.ORCA_UPSTREAM_ANTHROPIC ?? gateway;
104
+ const openai = args.str('upstream-openai') ?? env.ORCA_UPSTREAM_OPENAI ?? gateway;
105
+ if (anthropic)
106
+ out.anthropic = anthropic;
107
+ if (openai) {
108
+ out.openai = openai;
109
+ // The proxy resolves an origin by dialect id, and chat completions and the Responses API are
110
+ // two dialects sharing one provider — so a map holding only `openai` sends every Codex and
111
+ // Agents-SDK call straight past a gateway the user configured, on their own key, silently.
112
+ // `--upstream-openai` names a provider, not a wire format.
113
+ out['openai-responses'] = openai;
114
+ }
115
+ return Object.keys(out).length > 0 ? out : undefined;
116
+ }
117
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import type { RecordedExchange } from '@orcareplay/proxy';
2
+ import type { TraceWriter } from '@orcareplay/core';
3
+ /**
4
+ * Turning intercepted model exchanges into trace events.
5
+ *
6
+ * This is where the project's central observation is cashed in. A tool result never appears in the
7
+ * response that requested it — the harness runs the tool and hands the result back on the *next*
8
+ * request. So we hold each `tool_use` open and close it when a later request carries the matching
9
+ * `tool_result`, which is how a proxy that only sees model traffic still reconstructs the whole
10
+ * tool loop without touching the agent.
11
+ *
12
+ * The consequence to keep in mind: a tool call whose result never comes back (the run ended, the
13
+ * agent crashed) stays unresolved, and `unresolved()` reports it rather than silently dropping it.
14
+ */
15
+ export interface PendingToolCall {
16
+ id: string;
17
+ name: string;
18
+ input: unknown;
19
+ /** seq of the tool.call event, so the tool.result can point back at it. */
20
+ seq: number;
21
+ turn: number;
22
+ }
23
+ export interface DerivedEvent {
24
+ type: 'model.request' | 'model.response' | 'tool.call' | 'tool.result';
25
+ actor: 'agent' | 'model' | 'harness';
26
+ attrs: Record<string, unknown>;
27
+ payload?: unknown;
28
+ /** Index into the pending map, resolved to a real seq by the caller. */
29
+ causesToolId?: string;
30
+ /**
31
+ * Positions of earlier events *in this same batch* that caused this one.
32
+ *
33
+ * Seqs do not exist until the writer has appended, so the deriver cannot name them. It names
34
+ * positions in the array it is about to return instead, and the recorder swaps each one for the
35
+ * seq it wrote. Both edges this carries are observed rather than inferred: a `tool_use` block is
36
+ * inside the response that emitted it, and a `tool_result` block is inside the request that
37
+ * carried it back.
38
+ */
39
+ causesIndex?: number[];
40
+ }
41
+ export declare class ExchangeEventDeriver {
42
+ #private;
43
+ /** Tool calls that were issued but whose result never came back. */
44
+ unresolved(): PendingToolCall[];
45
+ /**
46
+ * Derive the events for one exchange. Returns them in the order they should be appended;
47
+ * `tool.result` events come first because they describe work that happened *before* this
48
+ * request was made.
49
+ */
50
+ derive(exchange: RecordedExchange, turn: number): DerivedEvent[];
51
+ markPending(toolUseId: string, seq: number): void;
52
+ seqOf(toolUseId: string): number | undefined;
53
+ }
54
+ /**
55
+ * Derive one exchange's events, resolve their causal references, and write them.
56
+ *
57
+ * Shared because it has two callers — `orca record` and the live half of a fork — and they drifted:
58
+ * the fork wrote every model exchange with no `causes` at all, so a forked run could be replayed
59
+ * and forked again but not explained. The seq resolution is the part that is easy to leave out,
60
+ * since it cannot happen until the writer has appended, so it lives here rather than in either
61
+ * caller.
62
+ */
63
+ export declare function appendDerivedEvents(writer: TraceWriter, deriver: ExchangeEventDeriver, exchange: RecordedExchange, turn: number,
64
+ /**
65
+ * Each derived event as it is written.
66
+ *
67
+ * The recorder needs to know whether the agent ran any commands, so it can say when the shell
68
+ * layer was on and saw none of them. That is only knowable from what the model asked for, and
69
+ * this is the one place the derived events exist.
70
+ */
71
+ onDerived?: (derived: DerivedEvent) => void): Promise<void>;
72
+ //# sourceMappingURL=exchange-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exchange-events.d.ts","sourceRoot":"","sources":["../src/exchange-events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,eAAe,GAAG,gBAAgB,GAAG,WAAW,GAAG,aAAa,CAAC;IACvE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,qBAAa,oBAAoB;;IAS/B,oEAAoE;IACpE,UAAU,IAAI,eAAe,EAAE;IAO/B;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE;IA+EhE,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAKjD,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAI7C;AAyBD;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,MAAM;AACZ;;;;;;GAMG;AACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,GAC1C,OAAO,CAAC,IAAI,CAAC,CA2Bf"}
@@ -0,0 +1,170 @@
1
+ export class ExchangeEventDeriver {
2
+ #pending = new Map();
3
+ /**
4
+ * Tool results already turned into events. Necessary because the conversation is resent in
5
+ * full every turn: without this, a result would be re-emitted on every subsequent request and
6
+ * a long run would accumulate one duplicate per turn per tool call.
7
+ */
8
+ #closed = new Set();
9
+ /** Tool calls that were issued but whose result never came back. */
10
+ unresolved() {
11
+ // A pending entry now outlives the result that answered it, so that the recorder can still
12
+ // resolve the call's seq for `causes` after derive has returned. `#closed` is the authority on
13
+ // whether a call was actually answered; "still in #pending" no longer means "unanswered".
14
+ return [...this.#pending.values()].filter((p) => !this.#closed.has(p.id));
15
+ }
16
+ /**
17
+ * Derive the events for one exchange. Returns them in the order they should be appended;
18
+ * `tool.result` events come first because they describe work that happened *before* this
19
+ * request was made.
20
+ */
21
+ derive(exchange, turn) {
22
+ const events = [];
23
+ /** Where the results this batch produced ended up, so the request can name them. */
24
+ const resultsAt = [];
25
+ for (const result of collectToolResults(exchange.canonicalRequest)) {
26
+ // Already accounted for on an earlier turn — the resent conversation is not new information.
27
+ if (this.#closed.has(result.tool_use_id))
28
+ continue;
29
+ // No pending call either: recording began mid-conversation, so this result is the first
30
+ // thing we know about that tool use. Record it rather than lose it.
31
+ const pending = this.#pending.get(result.tool_use_id);
32
+ events.push({
33
+ type: 'tool.result',
34
+ actor: 'harness',
35
+ attrs: {
36
+ tool_use_id: result.tool_use_id,
37
+ name: pending?.name ?? 'unknown',
38
+ is_error: result.is_error ?? false,
39
+ bytes: result.content.length,
40
+ },
41
+ payload: result.content,
42
+ causesToolId: result.tool_use_id,
43
+ });
44
+ resultsAt.push(events.length - 1);
45
+ // Deliberately not deleted here. The recorder resolves the call's seq *after* derive
46
+ // returns — it has to, since the seq only exists once the event is written — so dropping the
47
+ // entry in this loop meant `causes` came back empty on every tool result ever recorded, with
48
+ // no error to notice. `#closed` is what stops a resent result being re-emitted, so the
49
+ // pending entry can outlive the result it answered without any duplication.
50
+ this.#closed.add(result.tool_use_id);
51
+ }
52
+ events.push({
53
+ type: 'model.request',
54
+ actor: 'agent',
55
+ attrs: {
56
+ model: exchange.canonicalRequest.model,
57
+ dialect: exchange.dialect,
58
+ path: exchange.path,
59
+ messages: exchange.canonicalRequest.messages.length,
60
+ tools: exchange.canonicalRequest.tools?.length ?? 0,
61
+ },
62
+ payload: exchange.rawRequest,
63
+ // Absent rather than empty when nothing came back: an edge list naming nothing is noise a
64
+ // reader has to interpret, and `causes` is optional precisely so it can be left off.
65
+ ...(resultsAt.length > 0 ? { causesIndex: resultsAt } : {}),
66
+ });
67
+ const response = exchange.canonicalResponse;
68
+ events.push({
69
+ type: 'model.response',
70
+ actor: 'model',
71
+ attrs: {
72
+ model: response?.model ?? exchange.canonicalRequest.model,
73
+ stop_reason: response?.stop_reason ?? 'unknown',
74
+ input_tokens: response?.usage.input_tokens ?? 0,
75
+ output_tokens: response?.usage.output_tokens ?? 0,
76
+ status: exchange.status,
77
+ duration_ms: exchange.durationMs ?? 0,
78
+ streamed: exchange.streamed,
79
+ },
80
+ payload: exchange.rawResponse,
81
+ });
82
+ const responseAt = events.length - 1;
83
+ for (const use of collectToolUses(response)) {
84
+ events.push({
85
+ type: 'tool.call',
86
+ actor: 'model',
87
+ attrs: { tool_use_id: use.id, name: use.name, input: use.input },
88
+ causesIndex: [responseAt],
89
+ });
90
+ // seq is assigned by the writer; the caller patches it back in via markPending.
91
+ this.#pending.set(use.id, { id: use.id, name: use.name, input: use.input, seq: -1, turn });
92
+ }
93
+ return events;
94
+ }
95
+ markPending(toolUseId, seq) {
96
+ const p = this.#pending.get(toolUseId);
97
+ if (p)
98
+ p.seq = seq;
99
+ }
100
+ seqOf(toolUseId) {
101
+ const seq = this.#pending.get(toolUseId)?.seq;
102
+ return seq === undefined || seq < 0 ? undefined : seq;
103
+ }
104
+ }
105
+ function collectToolResults(req) {
106
+ const out = [];
107
+ // Only the trailing user message can carry results for the calls we are still holding open;
108
+ // scanning the whole conversation would re-emit every earlier result on every turn.
109
+ const last = req.messages[req.messages.length - 1];
110
+ if (!last || last.role !== 'user')
111
+ return out;
112
+ for (const block of last.content) {
113
+ if (block.type === 'tool_result')
114
+ out.push(block);
115
+ }
116
+ return out;
117
+ }
118
+ function collectToolUses(res) {
119
+ if (!res)
120
+ return [];
121
+ return res.content.filter((b) => b.type === 'tool_use');
122
+ }
123
+ /**
124
+ * Derive one exchange's events, resolve their causal references, and write them.
125
+ *
126
+ * Shared because it has two callers — `orca record` and the live half of a fork — and they drifted:
127
+ * the fork wrote every model exchange with no `causes` at all, so a forked run could be replayed
128
+ * and forked again but not explained. The seq resolution is the part that is easy to leave out,
129
+ * since it cannot happen until the writer has appended, so it lives here rather than in either
130
+ * caller.
131
+ */
132
+ export async function appendDerivedEvents(writer, deriver, exchange, turn,
133
+ /**
134
+ * Each derived event as it is written.
135
+ *
136
+ * The recorder needs to know whether the agent ran any commands, so it can say when the shell
137
+ * layer was on and saw none of them. That is only knowable from what the model asked for, and
138
+ * this is the one place the derived events exist.
139
+ */
140
+ onDerived) {
141
+ /** `written[i]` is the seq of the i-th derived event, which is what `causesIndex` refers to. */
142
+ const written = [];
143
+ for (const derived of deriver.derive(exchange, turn)) {
144
+ const causes = [];
145
+ if (derived.causesToolId) {
146
+ const seq = deriver.seqOf(derived.causesToolId);
147
+ if (seq !== undefined)
148
+ causes.push(seq);
149
+ }
150
+ for (const index of derived.causesIndex ?? []) {
151
+ const seq = written[index];
152
+ if (seq !== undefined)
153
+ causes.push(seq);
154
+ }
155
+ const event = await writer.append({
156
+ type: derived.type,
157
+ actor: derived.actor,
158
+ turn,
159
+ attrs: derived.attrs,
160
+ payload: derived.payload,
161
+ ...(causes.length > 0 ? { causes } : {}),
162
+ });
163
+ written.push(event.seq);
164
+ onDerived?.(derived);
165
+ if (derived.type === 'tool.call') {
166
+ deriver.markPending(String(derived.attrs['tool_use_id']), event.seq);
167
+ }
168
+ }
169
+ }
170
+ //# sourceMappingURL=exchange-events.js.map