claude-autorouter 0.5.1 → 0.5.2
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/docs/development.md +1 -1
- package/docs/reference.md +1 -1
- package/docs/releasing.md +2 -0
- package/package.json +1 -1
- package/src/keychain.mjs +9 -2
- package/src/redaction.mjs +74 -2
- package/src/user-config.mjs +16 -6
package/docs/development.md
CHANGED
|
@@ -94,7 +94,7 @@ For a classifier-only rubric evaluation:
|
|
|
94
94
|
npm run eval
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
The bundled evaluation makes 19 classifier calls and no Claude generations.
|
|
97
|
+
The bundled evaluation makes 19 classifier calls and no Claude generations. The evaluation uses the configured evaluator, which defaults to a local Ollama model and needs an installed one; set `AUTOROUTER_EVALUATOR=jev` to evaluate with Jev, which incurs TypeSafe usage and sends redacted excerpts to TypeSafe. It reports agreement with the starting rubric, fallback count, and p50/p95 routing latency. Edit `test/fixtures/routing.json` to represent the tasks you want to measure. Rubric agreement alone does not establish answer quality or net savings; compare completed tasks against fixed-model baselines.
|
|
98
98
|
|
|
99
99
|
For local evaluator measurements, use Ollama 0.35+ and a model compatible with `/v1/systemone`. Distinguish cold model loading from warmed classification, and record the model tag, hardware, Ollama version, context size, prompt length, and resident memory. The launcher primes the classifier with a synthetic task before opening the UI, with a separate deadline of up to 60 seconds. Runtime and benchmark share the 3,000-character/3,000-UTF-8-byte state limit, so include non-ASCII cases and excerpts that fill the budget. Also measure the first request after keep-alive expiration: its reload can hit the normal deadline even when warm requests pass. Repeat on realistic prompt distributions instead of selecting a model from a single easy request. Disk download size is not resident RAM. Keep model downloads opt-in and respect each model's license.
|
|
100
100
|
|
package/docs/reference.md
CHANGED
|
@@ -220,7 +220,7 @@ Claude Code → authenticated local gateway → Jev or local Ollama classificati
|
|
|
220
220
|
|
|
221
221
|
AutoRouter launches the user's installed official Claude Code binary without patching it and uses Claude Code's [gateway integration](https://code.claude.com/docs/en/llm-gateway-protocol), so it sees inference requests and tool continuations. It does not rely on a user-prompt hook. Each user uses their own provider credentials; AutoRouter does not provide a Claude sign-in service or a shared provider account.
|
|
222
222
|
|
|
223
|
-
The selected evaluator receives a bounded state containing the latest human request and excerpts of the original task and recent messages: up to 12,000 serialized characters sent to TypeSafe for Jev, or 3,000 UTF-8 bytes sent to the local Ollama service. Jev also receives system-text excerpts. The local path excludes Claude's top-level executor system instructions. These excerpts can include private source code and tool results. Before excerpting, recognizable sensitive values are replaced (the same filter applies to opt-in session-log prompt excerpts, including when old logs are read back) with markers such as `[REDACTED:secret]`: private keys, common provider token formats (Anthropic, OpenAI-style `sk-`, AWS, GitHub, GitLab, Slack, Google, Stripe, npm), JWTs, authorization headers, URL credentials, values assigned to password/secret/token/key-like names, email addresses, and checksum-valid IBANs and payment card numbers. Setting names stay visible. Redaction is pattern-based: unrecognized formats can remain, code resembling an assignment can be over-redacted, and it does not make arbitrary private source code safe to share. Images, document payloads, and signed thinking are omitted. Full tool schemas and full conversation history are not sent to either classifier. Anthropic receives the complete request, including its tools and attachments. Large or multimodal requests may also go to Anthropic's token-count endpoint before inference, including when classification is local.
|
|
223
|
+
The selected evaluator receives a bounded state containing the latest human request and excerpts of the original task and recent messages: up to 12,000 serialized characters sent to TypeSafe for Jev, or 3,000 UTF-8 bytes sent to the local Ollama service. Jev also receives system-text excerpts. The local path excludes Claude's top-level executor system instructions. These excerpts can include private source code and tool results. Before excerpting, recognizable sensitive values are replaced (the same filter applies to opt-in session-log prompt excerpts, including when old logs are read back) with markers such as `[REDACTED:secret]`: private keys, common provider token formats (Anthropic, OpenAI-style `sk-`, AWS, GitHub, GitLab, Slack, Google, Stripe, npm, PyPI, SendGrid, Shopify, Hugging Face, DigitalOcean, Linear, Notion, Databricks, Atlassian, Mailgun, Twilio, Telegram, Azure storage keys), JWTs, authorization and cookie headers, URL credentials (including an empty user or a password containing `@`), values assigned to password/secret/token/key-like names (including quoted values with spaces and upper-case `*_KEY` names), credentials on command lines (`curl -u`, `--password`, `--api-key`, `sshpass -p`, `mysql -p`), email addresses, international phone numbers with a leading `+`, checksum-valid Italian tax codes, US social security numbers, and checksum-valid IBANs and payment card numbers. Names, postal addresses, other national ID formats, customer identifiers and source code are not detected. Setting names stay visible. Redaction is pattern-based: unrecognized formats can remain, code resembling an assignment can be over-redacted, and it does not make arbitrary private source code safe to share. Images, document payloads, and signed thinking are omitted. Full tool schemas and full conversation history are not sent to either classifier. Anthropic receives the complete request, including its tools and attachments. Large or multimodal requests may also go to Anthropic's token-count endpoint before inference, including when classification is local.
|
|
224
224
|
|
|
225
225
|
In subscription mode, Claude Code owns login and OAuth refresh. AutoRouter forwards the current request's authorization and beta headers to Anthropic. It does not read keychain or saved login files, persist subscription tokens, or send them to Jev. A separate temporary `X-Autorouter-Token` authenticates the local connection and is stripped upstream. Subscription forwarding is restricted to `https://api.anthropic.com`. See [subscriptions and gateways](https://code.claude.com/docs/en/llm-gateway#subscriptions-and-gateways).
|
|
226
226
|
|
package/docs/releasing.md
CHANGED
|
@@ -22,6 +22,8 @@ Version `0.5.0` makes local Ollama the default evaluator and TypeSafe Jev an exp
|
|
|
22
22
|
|
|
23
23
|
Version `0.5.1` hardens defaults and adds an optional organization policy. **Behavior change:** enabling `AUTOROUTER_SESSION_LOG_DIR` now records metadata only; set `AUTOROUTER_SESSION_LOG_MODE=prompts` to keep prompt excerpts. Saved configurations that already record a mode are unchanged. A root-owned policy file can restrict the evaluator and authentication mode and lock the log mode and service URLs; see [organization policy](reference.md#organization-policy). The next launch removes status directories left by a hard-killed launcher, an Ollama `localhost` endpoint now connects to `127.0.0.1`, an oversized request body closes its connection after the 413 response, and `.env.example` selects the local evaluator to match the default.
|
|
24
24
|
|
|
25
|
+
Version `0.5.2` widens secret redaction and closes a Keychain command-injection path. Redaction now also covers URL credentials with an empty user or a password containing `@`, passwords containing `;` or spaces in quotes, `*_KEY`, `*_PASS` and `*_AUTH` names, cookies, command-line credentials (`curl -u`, `--password`, `--api-key`, `sshpass -p`, `mysql -p`), about fifteen more provider token formats, international phone numbers, checksum-validated Italian tax codes and US social security numbers. **Behavior change:** evaluator excerpts and prompt-mode logs contain more `[REDACTED:...]` markers than before, and recognizable personal identifiers now appear as `[REDACTED:phone]` or `[REDACTED:national_id]`. A configuration path containing control or line-separator characters is now rejected, and Keychain item names are validated before they reach the `security` tool. Contributor documentation no longer names Jev as the default evaluator. See the [data flow](reference.md#data-flow-and-authentication).
|
|
26
|
+
|
|
25
27
|
The GitHub repository became public on October 6, 2026, after preparation PR #1 merged. That launch created no release tag and published no new npm version. npm publication remains a separate release operation. Its tarball includes runtime source, README, configuration example, license, and shipped documentation; model weights, user configuration, credentials, transcripts, session logs, local artifacts, and test fixtures are excluded. Review each release archive, especially when the package allowlist changes. The public Git repository also exposes history, development scripts and tests; the npm archive allowlist does not govern that material.
|
|
26
28
|
|
|
27
29
|
## What runs automatically
|
package/package.json
CHANGED
package/src/keychain.mjs
CHANGED
|
@@ -16,8 +16,15 @@ function keychainError(message) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// Quote for the `security -i` command parser, which accepts backslash escapes
|
|
19
|
-
// inside double quotes
|
|
20
|
-
|
|
19
|
+
// inside double quotes and reads one command per line. The secret is validated
|
|
20
|
+
// as printable single-line ASCII; the account and label (which can carry a
|
|
21
|
+
// user-chosen path) must contain no control or line-separator characters, since
|
|
22
|
+
// a newline would start another command.
|
|
23
|
+
const UNSAFE = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
24
|
+
const quote = value => {
|
|
25
|
+
if (typeof value !== 'string' || UNSAFE.test(value)) throw keychainError('Keychain item names must not contain control characters.');
|
|
26
|
+
return `"${value.replace(/[\\"]/g, '\\$&')}"`;
|
|
27
|
+
};
|
|
21
28
|
|
|
22
29
|
export function createKeychain({ run = spawnSync, platform = process.platform } = {}) {
|
|
23
30
|
const available = platform === 'darwin';
|
package/src/redaction.mjs
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
// best-effort filter: unrecognized formats can remain, and code that merely
|
|
6
6
|
// resembles an assignment can be over-redacted. Every quantifier is bounded or
|
|
7
7
|
// excludes its delimiter, so scanning stays linear in the input length.
|
|
8
|
+
//
|
|
9
|
+
// This is deliberately not a dependency. Scanner libraries such as secretlint
|
|
10
|
+
// detect provider token formats only; they miss the assignment, URL and command
|
|
11
|
+
// forms below, and would add dozens of packages to a CLI that ships none.
|
|
8
12
|
|
|
9
13
|
const marker = kind => `[REDACTED:${kind}]`;
|
|
10
14
|
|
|
@@ -32,6 +36,25 @@ function luhnValid(value) {
|
|
|
32
36
|
return sum % 10 === 0;
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
const CF_ODD = [1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 2, 4, 18, 20, 11, 3, 6, 8, 12, 14, 16, 10, 22, 25, 24, 23];
|
|
40
|
+
function codiceFiscaleValid(value) {
|
|
41
|
+
const text = value.toUpperCase();
|
|
42
|
+
if (text.length !== 16) return false;
|
|
43
|
+
let sum = 0;
|
|
44
|
+
for (let i = 0; i < 15; i++) {
|
|
45
|
+
const code = text.charCodeAt(i);
|
|
46
|
+
const index = code >= 65 ? code - 65 : code - 48;
|
|
47
|
+
if (index < 0 || index > 25) return false;
|
|
48
|
+
sum += i % 2 === 0 ? CF_ODD[index] : index;
|
|
49
|
+
}
|
|
50
|
+
return text.charCodeAt(15) === 65 + (sum % 26);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function phoneValid(value) {
|
|
54
|
+
const digits = value.replace(/[^0-9]/g, '').length;
|
|
55
|
+
return digits >= 8 && digits <= 15;
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
const base64 = code => (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122)
|
|
36
59
|
|| code === 43 || code === 47 || code === 61;
|
|
37
60
|
|
|
@@ -58,32 +81,81 @@ function redactKeyTails(text) {
|
|
|
58
81
|
return copied ? output + text.slice(copied) : text;
|
|
59
82
|
}
|
|
60
83
|
|
|
84
|
+
// Setting names that hold a secret. "pass", "auth" and "key" are too common in
|
|
85
|
+
// ordinary names (tests_pass, AUTH_MODE, primary_key), so they are matched only
|
|
86
|
+
// as upper-case environment names or as the whole word, further below.
|
|
87
|
+
const SECRET_NAME = '(?:passw(?:or)?d|pwd|passphrase|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|signing[_-]?key|encryption[_-]?key|credentials?)';
|
|
88
|
+
// Characters that may follow an unquoted value. A comma or semicolon ends it
|
|
89
|
+
// only before whitespace, so a password such as a;b is not cut after one letter.
|
|
90
|
+
const VALUE = '(?:[^\\s"\'&,;]|[,;](?=[^\\s"\'&]))';
|
|
91
|
+
const SEPARATOR = '(["\']?[ \\t]{0,8}[:=][ \\t]{0,8})';
|
|
92
|
+
const FLAG_NAME = '(?:password|passwd|pwd|passphrase|pass|token|secret|api-?key|access-?key|private-?key|client-?secret|auth-?token|auth)';
|
|
93
|
+
|
|
61
94
|
/** @type {ReadonlyArray<readonly [RegExp, string | ((...match: string[]) => string)] | ((text: string) => string)>} */
|
|
62
95
|
const RULES = Object.freeze([
|
|
63
96
|
// A block cut off by excerpting is still redacted through the end of text.
|
|
64
97
|
[/-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]{0,40}PRIVATE KEY(?: BLOCK)?-----|$)/g, marker('private_key')],
|
|
65
98
|
redactKeyTails,
|
|
66
|
-
|
|
99
|
+
// The userinfo may have an empty user (redis://:pw@host) and a password that
|
|
100
|
+
// contains "@": the match runs to the last "@" before the host.
|
|
101
|
+
[/\b([a-z][a-z0-9+.-]{1,20}:\/\/)[^\s:@/]{0,256}:[^\s/]{1,256}@/gi, (_, scheme) => `${scheme}${marker('credentials')}@`],
|
|
102
|
+
// Provider token formats with distinctive prefixes.
|
|
67
103
|
[/\bsk-[A-Za-z0-9_-]{20,}/g, marker('secret')],
|
|
68
104
|
[/\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/g, marker('secret')],
|
|
105
|
+
[/\bwhsec_[A-Za-z0-9]{16,}/g, marker('secret')],
|
|
69
106
|
[/\b(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}\b/g, marker('secret')],
|
|
70
107
|
[/\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{22,})/g, marker('secret')],
|
|
71
108
|
[/\bglpat-[A-Za-z0-9_-]{20,}/g, marker('secret')],
|
|
72
109
|
[/\bxox[abposr]-[A-Za-z0-9-]{10,}/g, marker('secret')],
|
|
73
110
|
[/https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/]{8,}/g, marker('secret')],
|
|
74
111
|
[/\bAIza[0-9A-Za-z_-]{35}/g, marker('secret')],
|
|
112
|
+
[/\bya29\.[A-Za-z0-9_-]{20,}/g, marker('secret')],
|
|
75
113
|
[/\bnpm_[A-Za-z0-9]{36}/g, marker('secret')],
|
|
114
|
+
[/\bpypi-[A-Za-z0-9_-]{50,}/g, marker('secret')],
|
|
115
|
+
[/\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g, marker('secret')],
|
|
116
|
+
[/\bshp(?:at|ca|pa|ss)_[a-f0-9]{32}\b/g, marker('secret')],
|
|
117
|
+
[/\bhf_[A-Za-z0-9]{30,}/g, marker('secret')],
|
|
118
|
+
[/\bdo[por]_v1_[a-f0-9]{64}\b/g, marker('secret')],
|
|
119
|
+
[/\blin_api_[A-Za-z0-9]{30,}/g, marker('secret')],
|
|
120
|
+
[/\bntn_[A-Za-z0-9]{30,}/g, marker('secret')],
|
|
121
|
+
[/\bdapi[a-f0-9]{32}\b/g, marker('secret')],
|
|
122
|
+
[/\bATATT3[A-Za-z0-9_=-]{20,}/g, marker('secret')],
|
|
123
|
+
[/\bkey-[0-9a-f]{32}\b/g, marker('secret')],
|
|
124
|
+
[/\bSK[0-9a-f]{32}\b/g, marker('secret')],
|
|
125
|
+
[/\b[0-9]{8,10}:[A-Za-z0-9_-]{35}\b/g, marker('secret')],
|
|
76
126
|
[/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, marker('secret')],
|
|
127
|
+
[/\b((?:AccountKey|SharedAccessKey|SharedAccessSignature)=)[^;\s"']{8,}/g, (_, prefix) => `${prefix}${marker('secret')}`],
|
|
77
128
|
[/\b((?:proxy-)?authorization["']?[ \t]{0,8}[:=][ \t]{0,8}["']?)(?:(Bearer|Basic|Token|Digest)[ \t]+)?[^\s"',;]{4,}/gi,
|
|
78
129
|
(_, prefix, scheme) => `${prefix}${scheme ? `${scheme} ` : ''}${marker('secret')}`],
|
|
130
|
+
[/\b((?:set-)?cookie["']?[ \t]{0,8}[:=][ \t]{0,8}["']?)[^\r\n"']{8,}/gi, (_, prefix) => `${prefix}${marker('secret')}`],
|
|
79
131
|
[/\b(Bearer[ \t]+)[A-Za-z0-9._~+/-]{16,}=*/g, (_, prefix) => `${prefix}${marker('secret')}`],
|
|
132
|
+
// Command lines: curl -u user:pass, --password value, sshpass -p, mysql -pvalue.
|
|
133
|
+
[/((?:^|\s)(?:-u|--user|--proxy-user)(?:[ \t]+|=)["']?)[^\s:"']{0,64}:[^\s"']+/g, (_, prefix) => `${prefix}${marker('credentials')}`],
|
|
134
|
+
[new RegExp(`((?:^|\\s)--?[a-z0-9-]{0,31}?${FLAG_NAME})([ \\t]+|=)(?!-)(["']?)[^\\s"']{4,}`, 'gi'),
|
|
135
|
+
(_, flag, separator, quote) => `${flag}${separator}${quote}${marker('secret')}`],
|
|
136
|
+
[/\b(sshpass[ \t]+-p[ \t]*["']?)[^\s"']+/g, (_, prefix) => `${prefix}${marker('secret')}`],
|
|
137
|
+
[/\b((?:mysql|mysqldump|mysqladmin|mariadb)\b[^\n]{0,200}?[ \t]-p)\S{3,}/g, (_, prefix) => `${prefix}${marker('secret')}`],
|
|
80
138
|
// Keep the setting name: it tells the classifier what kind of work this is.
|
|
81
|
-
|
|
139
|
+
// A quoted value may contain spaces and delimiters.
|
|
140
|
+
[new RegExp(`\\b([A-Za-z0-9_.-]{0,40}${SECRET_NAME}[A-Za-z0-9_.-]{0,40})${SEPARATOR}(["'])(?:(?!\\3)[^\\r\\n]){4,512}\\3`, 'gi'),
|
|
141
|
+
(_, name, separator, quote) => `${name}${separator}${quote}${marker('secret')}${quote}`],
|
|
142
|
+
[new RegExp(`\\b([A-Za-z0-9_.-]{0,40}${SECRET_NAME}[A-Za-z0-9_.-]{0,40})${SEPARATOR}(["']?)${VALUE}{4,}`, 'gi'),
|
|
143
|
+
(_, name, separator, quote) => `${name}${separator}${quote}${marker('secret')}`],
|
|
144
|
+
// Upper-case environment names ending in _KEY, _PASS or _AUTH (STRIPE_KEY,
|
|
145
|
+
// DB_PASS), and the bare lowercase words pass and auth. Booleans stay readable.
|
|
146
|
+
[new RegExp(`\\b([A-Z][A-Z0-9_]{0,40}[_-]KEY|(?:[A-Z][A-Z0-9_]{0,40}[_-])?(?:PASS|AUTH))${SEPARATOR}(["']?)${VALUE}{4,}`, 'g'),
|
|
82
147
|
(_, name, separator, quote) => `${name}${separator}${quote}${marker('secret')}`],
|
|
148
|
+
[new RegExp(`(?<![A-Za-z0-9_-])((?:pass|auth)["']?[ \\t]{0,8}[:=][ \\t]{0,8})(["']?)(?!(?:true|false|null|none|undefined)\\b)${VALUE}{4,}`, 'gi'),
|
|
149
|
+
(_, prefix, quote) => `${prefix}${quote}${marker('secret')}`],
|
|
83
150
|
[/\b[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,24}\b/g, marker('email')],
|
|
84
151
|
[/\b[A-Z]{2}[0-9]{2}(?: ?[A-Z0-9]{4}){2,7}(?: ?[A-Z0-9]{1,4})?\b/g, match => ibanValid(match) ? marker('iban') : match],
|
|
85
152
|
// Major card networks only, so millisecond timestamps and IDs survive.
|
|
86
153
|
[/\b(?:4|5[1-5]|2[2-7]|3[47]|6)(?:[0-9][ -]?){11,17}[0-9]\b/g, match => luhnValid(match) ? marker('card') : match],
|
|
154
|
+
// Italian tax code (checksum-validated) and US social security numbers.
|
|
155
|
+
[/\b[A-Z]{6}[0-9LMNPQRSTUV]{2}[A-EHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]\b/g, match => codiceFiscaleValid(match) ? marker('national_id') : match],
|
|
156
|
+
[/\b(?!000|666|9[0-9]{2})[0-9]{3}-(?!00)[0-9]{2}-(?!0000)[0-9]{4}\b/g, marker('national_id')],
|
|
157
|
+
// International phone numbers need a leading plus sign and 8 to 15 digits.
|
|
158
|
+
[/(?<![\w+])\+[0-9][0-9 .()-]{6,20}[0-9]/g, match => phoneValid(match) ? marker('phone') : match],
|
|
87
159
|
]);
|
|
88
160
|
|
|
89
161
|
/** @param {string} text */
|
package/src/user-config.mjs
CHANGED
|
@@ -71,18 +71,28 @@ function validate(values) {
|
|
|
71
71
|
return validated;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// The path is printed, stored in Keychain item labels and sent to the macOS
|
|
75
|
+
// `security -i` command parser, which reads one command per line. Control
|
|
76
|
+
// characters (including line and paragraph separators) can therefore split a
|
|
77
|
+
// command or inject terminal escapes, and no real configuration path needs them.
|
|
78
|
+
const UNSAFE_PATH = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/;
|
|
79
|
+
|
|
74
80
|
export function getConfigPath(env = process.env) {
|
|
81
|
+
let path;
|
|
75
82
|
if (env.AUTOROUTER_CONFIG !== undefined) {
|
|
76
83
|
if (typeof env.AUTOROUTER_CONFIG !== 'string' || !env.AUTOROUTER_CONFIG.trim()) {
|
|
77
84
|
throw configError('AUTOROUTER_CONFIG must be a non-empty path.');
|
|
78
85
|
}
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
path = resolve(env.AUTOROUTER_CONFIG);
|
|
87
|
+
} else {
|
|
88
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
89
|
+
if (xdg !== undefined && (typeof xdg !== 'string' || (xdg && !isAbsolute(xdg)))) {
|
|
90
|
+
throw configError('XDG_CONFIG_HOME must be an absolute path when set.');
|
|
91
|
+
}
|
|
92
|
+
path = join(xdg || join(homedir(), '.config'), 'claude-autorouter', 'config.json');
|
|
84
93
|
}
|
|
85
|
-
|
|
94
|
+
if (UNSAFE_PATH.test(path)) throw configError('The AutoRouter configuration path must not contain control characters.');
|
|
95
|
+
return path;
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
// The saved store setting decides where saved secrets live; the environment
|