auxilo-mcp 0.9.12 → 0.9.13
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/README.md +8 -8
- package/bin/auxilo-cli.js +356 -14
- package/lib/installer.js +40 -0
- package/mcp-server.js +8 -8
- package/package.json +2 -1
- package/scripts/extract-local.js +107 -201
- package/scripts/providers/byo-key.js +631 -0
- package/scripts/providers/claude-code.js +477 -0
- package/scripts/providers/codex-cli.js +358 -0
- package/scripts/providers/index.js +316 -0
- package/scripts/providers/provider.interface.js +98 -0
- package/scripts/providers/schemas/extraction-envelope.schema.json +42 -0
- package/scripts/providers/schemas/judge-decisions.schema.json +20 -0
- package/scripts/runner.js +32 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* scripts/providers/claude-code.js — Claude Code provider adapter
|
|
4
|
+
* (EXTRACT-PER-CLIENT W1 PART A, absorbs 0913 PART A / EXTRACT-TOOLS-LOCK).
|
|
5
|
+
*
|
|
6
|
+
* Moved out of scripts/extract-local.js: resolveClaudeBin, checkClaudeAuthStatus,
|
|
7
|
+
* the extraction spawn, and the dedup-judge spawn. Behavior preserved except two
|
|
8
|
+
* deliberate hardenings (PUNCH-LIST EXTRACT-TOOLS-LOCK):
|
|
9
|
+
* 1. The extraction spawn gains `--tools ''` (already true of the judge spawn) —
|
|
10
|
+
* the model receives prompt+transcript on stdin and needs no tool access.
|
|
11
|
+
* 2. BOTH spawns now share ONE claudeChildEnv() that scrubs every gateway/cloud
|
|
12
|
+
* billing var (not just ANTHROPIC_API_KEY) — closing the drift where the judge
|
|
13
|
+
* built its own inline (single-var) env copy.
|
|
14
|
+
* Plus one new gate: a foreign-billing CLI helper (settings.json apiKeyHelper /
|
|
15
|
+
* awsAuthRefresh / awsCredentialExport / gcpAuthRefresh) short-circuits BEFORE any
|
|
16
|
+
* spawn, with reasonCode 'cli-billing-helper-configured' — extraction declines to
|
|
17
|
+
* run under a billing path it cannot audit.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { spawnSync } = require('child_process');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
|
|
25
|
+
/** Resolve the `claude` binary — hook/launchd env may have a minimal PATH. */
|
|
26
|
+
function resolveClaudeBin(opts = {}) {
|
|
27
|
+
const homeDir = typeof opts.homeDir === 'string' ? opts.homeDir : os.homedir();
|
|
28
|
+
const existsSync = typeof opts.existsSync === 'function' ? opts.existsSync : fs.existsSync;
|
|
29
|
+
const candidates = [
|
|
30
|
+
path.join(homeDir, '.claude', 'local', 'claude'),
|
|
31
|
+
'/usr/local/bin/claude',
|
|
32
|
+
'/opt/homebrew/bin/claude',
|
|
33
|
+
path.join(homeDir, '.local', 'bin', 'claude'),
|
|
34
|
+
];
|
|
35
|
+
for (const c of candidates) {
|
|
36
|
+
try {
|
|
37
|
+
if (existsSync(c)) return c;
|
|
38
|
+
} catch (_) { /* ignore */ }
|
|
39
|
+
}
|
|
40
|
+
// Absolute launchd fallbacks are absent; let PATH resolve the final option.
|
|
41
|
+
return 'claude';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Env scrub (EXTRACT-TOOLS-LOCK, PUNCH-LIST) ────────────────────────────
|
|
45
|
+
//
|
|
46
|
+
// SME-confirmed list (claude-code-guide, verified against official docs and
|
|
47
|
+
// `claude --help` on 2.1.251). Precedence per docs: cloud switches > AUTH_TOKEN >
|
|
48
|
+
// API_KEY > apiKeyHelper > OAUTH_TOKEN > profiles > subscription OAuth — every one
|
|
49
|
+
// of these wins over the user's login if present, so every one must be scrubbed
|
|
50
|
+
// for the child to run on the user's own subscription auth, never a billed path.
|
|
51
|
+
const SCRUBBED_CLIENT_ENV_VARS = Object.freeze([
|
|
52
|
+
'ANTHROPIC_API_KEY',
|
|
53
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
54
|
+
'ANTHROPIC_BASE_URL',
|
|
55
|
+
'ANTHROPIC_CUSTOM_HEADERS',
|
|
56
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
57
|
+
'ANTHROPIC_PROFILE',
|
|
58
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
59
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
60
|
+
'CLAUDE_CODE_USE_FOUNDRY',
|
|
61
|
+
'CLAUDE_CODE_USE_MANTLE',
|
|
62
|
+
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
|
|
63
|
+
'CLAUDE_CODE_SKIP_MANTLE_AUTH',
|
|
64
|
+
'ANTHROPIC_BEDROCK_BASE_URL',
|
|
65
|
+
'ANTHROPIC_BEDROCK_MANTLE_BASE_URL',
|
|
66
|
+
'ANTHROPIC_VERTEX_BASE_URL',
|
|
67
|
+
'ANTHROPIC_VERTEX_PROJECT_ID',
|
|
68
|
+
'ANTHROPIC_FOUNDRY_RESOURCE',
|
|
69
|
+
'ANTHROPIC_FOUNDRY_API_KEY',
|
|
70
|
+
'ANTHROPIC_FOUNDRY_AUTH_TOKEN',
|
|
71
|
+
'ANTHROPIC_FOUNDRY_BASE_URL',
|
|
72
|
+
'AWS_ACCESS_KEY_ID',
|
|
73
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
74
|
+
'AWS_SESSION_TOKEN',
|
|
75
|
+
'AWS_PROFILE',
|
|
76
|
+
'AWS_REGION',
|
|
77
|
+
'AWS_BEARER_TOKEN_BEDROCK',
|
|
78
|
+
'GOOGLE_APPLICATION_CREDENTIALS',
|
|
79
|
+
'CLOUD_ML_REGION',
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build the subscription-auth-only environment shared by BOTH the extraction and
|
|
84
|
+
* judge Claude CLI children — one function, no drift between the two spawns.
|
|
85
|
+
*/
|
|
86
|
+
function claudeChildEnv() {
|
|
87
|
+
const childEnv = { ...process.env, AUXILO_EXTRACTING: '1' };
|
|
88
|
+
for (const key of SCRUBBED_CLIENT_ENV_VARS) delete childEnv[key];
|
|
89
|
+
return childEnv;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Billing-helper detector ────────────────────────────────────────────────
|
|
93
|
+
//
|
|
94
|
+
// NOT scrubbable via env: settings.json `apiKeyHelper`, `awsAuthRefresh`,
|
|
95
|
+
// `awsCredentialExport`, `gcpAuthRefresh` let the CLI shell out for credentials
|
|
96
|
+
// at runtime, bypassing the env scrub entirely. We detect and decline rather than
|
|
97
|
+
// silently letting an audited-looking run bill through a helper we can't see.
|
|
98
|
+
const BILLING_HELPER_KEYS = ['apiKeyHelper', 'awsAuthRefresh', 'awsCredentialExport', 'gcpAuthRefresh'];
|
|
99
|
+
|
|
100
|
+
function readJsonSafe(filePath, readFileSyncImpl) {
|
|
101
|
+
try {
|
|
102
|
+
const raw = readFileSyncImpl(filePath, 'utf8');
|
|
103
|
+
const parsed = JSON.parse(raw);
|
|
104
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function settingsHasBillingHelper(settings) {
|
|
111
|
+
if (!settings || typeof settings !== 'object') return false;
|
|
112
|
+
return BILLING_HELPER_KEYS.some((key) => Boolean(settings[key]));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Walk upward from `startDir` for the nearest `.claude` directory. Never throws. */
|
|
116
|
+
function findNearestProjectClaudeDir(startDir, existsSyncImpl) {
|
|
117
|
+
try {
|
|
118
|
+
let dir = startDir;
|
|
119
|
+
// Bounded by the filesystem root — dirname(dir) === dir terminates the walk.
|
|
120
|
+
for (let i = 0; i < 1024; i += 1) {
|
|
121
|
+
const candidate = path.join(dir, '.claude');
|
|
122
|
+
if (existsSyncImpl(candidate)) return candidate;
|
|
123
|
+
const parent = path.dirname(dir);
|
|
124
|
+
if (parent === dir) return null;
|
|
125
|
+
dir = parent;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Enterprise/organization "managed settings" (EXTRACT-PER-CLIENT W1 FIX
|
|
134
|
+
// GOV-3 item 6). These outrank every one of ~/.claude/settings.json and the
|
|
135
|
+
// project settings.json/settings.local.json above, AND are loaded by
|
|
136
|
+
// headless (`-p`) sessions — the exact case this detector exists for. A
|
|
137
|
+
// managed apiKeyHelper would bill a foreign account while the detector
|
|
138
|
+
// reported "no helper" if these paths were never checked. Paths verified
|
|
139
|
+
// live against the Claude Code SME + official docs:
|
|
140
|
+
// https://code.claude.com/docs/en/managed-settings#choose-a-delivery-mechanism
|
|
141
|
+
// ("File-based: managed-settings.json ... in the system directory:
|
|
142
|
+
// /Library/Application Support/ClaudeCode/ on macOS, /etc/claude-code/ on
|
|
143
|
+
// Linux and WSL, and C:\Program Files\ClaudeCode\ on Windows.")
|
|
144
|
+
const MANAGED_SETTINGS_PATH_BY_PLATFORM = Object.freeze({
|
|
145
|
+
darwin: '/Library/Application Support/ClaudeCode/managed-settings.json',
|
|
146
|
+
win32: 'C:\\Program Files\\ClaudeCode\\managed-settings.json',
|
|
147
|
+
// linux, and everything else this repo runs tests on (WSL is a Linux
|
|
148
|
+
// filesystem for this purpose per the doc above) — the Linux path.
|
|
149
|
+
linux: '/etc/claude-code/managed-settings.json',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
function managedSettingsPathForPlatform(opts) {
|
|
153
|
+
// opts.managedSettingsPath is a direct test-injection seam (this path
|
|
154
|
+
// otherwise names a fixed, real, OS-level location no test should touch).
|
|
155
|
+
if (typeof opts.managedSettingsPath === 'string') return opts.managedSettingsPath;
|
|
156
|
+
const platform = typeof opts.platform === 'string' ? opts.platform : process.platform;
|
|
157
|
+
return MANAGED_SETTINGS_PATH_BY_PLATFORM[platform] || MANAGED_SETTINGS_PATH_BY_PLATFORM.linux;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Managed settings get a STRICTER read than the user/project files below:
|
|
162
|
+
* present-but-unreadable (EACCES, a policy file this account can't open) or
|
|
163
|
+
* present-but-unparseable is NOT treated as "no helper" — it fails CLOSED
|
|
164
|
+
* (helper-configured = true), because we cannot verify a policy file we know
|
|
165
|
+
* exists. (User/project settings.json stay fail-open on malformed content —
|
|
166
|
+
* see the module's "malformed settings.json is treated as no helper" note;
|
|
167
|
+
* that matches Claude Code's own behavior for those files, which this
|
|
168
|
+
* managed path does not share.)
|
|
169
|
+
*/
|
|
170
|
+
function managedSettingsBlocksOrUnverifiable(filePath, existsSyncImpl, readFileSyncImpl) {
|
|
171
|
+
let exists;
|
|
172
|
+
try {
|
|
173
|
+
exists = existsSyncImpl(filePath);
|
|
174
|
+
} catch {
|
|
175
|
+
exists = false;
|
|
176
|
+
}
|
|
177
|
+
if (!exists) return false;
|
|
178
|
+
const parsed = readJsonSafe(filePath, readFileSyncImpl);
|
|
179
|
+
if (parsed === null) return true; // present but unreadable/unparseable — fail closed
|
|
180
|
+
return settingsHasBillingHelper(parsed);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* true iff `~/.claude/settings.json`, the nearest project
|
|
185
|
+
* `.claude/settings.json`/`.claude/settings.local.json` upward from cwd, OR
|
|
186
|
+
* this platform's managed-settings.json (see above) has a truthy
|
|
187
|
+
* apiKeyHelper/awsAuthRefresh/awsCredentialExport/gcpAuthRefresh — or the
|
|
188
|
+
* managed file exists but could not be verified. Never throws —
|
|
189
|
+
* missing/malformed USER/PROJECT files are treated as "no helper configured"
|
|
190
|
+
* (matches the CLI's own fail-open behavior there); an unreadable/malformed
|
|
191
|
+
* MANAGED file is NOT — see managedSettingsBlocksOrUnverifiable above.
|
|
192
|
+
*/
|
|
193
|
+
function detectBillingHelperConfigured(opts = {}) {
|
|
194
|
+
const readFileSyncImpl = typeof opts.readFileSyncImpl === 'function' ? opts.readFileSyncImpl : fs.readFileSync;
|
|
195
|
+
const existsSyncImpl = typeof opts.existsSyncImpl === 'function' ? opts.existsSyncImpl : fs.existsSync;
|
|
196
|
+
const homeDir = typeof opts.homeDir === 'string' ? opts.homeDir : os.homedir();
|
|
197
|
+
const cwd = typeof opts.cwd === 'string' ? opts.cwd : process.cwd();
|
|
198
|
+
|
|
199
|
+
if (managedSettingsBlocksOrUnverifiable(managedSettingsPathForPlatform(opts), existsSyncImpl, readFileSyncImpl)) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const filesToCheck = [path.join(homeDir, '.claude', 'settings.json')];
|
|
204
|
+
const projectClaudeDir = findNearestProjectClaudeDir(cwd, existsSyncImpl);
|
|
205
|
+
if (projectClaudeDir) {
|
|
206
|
+
filesToCheck.push(path.join(projectClaudeDir, 'settings.json'));
|
|
207
|
+
filesToCheck.push(path.join(projectClaudeDir, 'settings.local.json'));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const filePath of filesToCheck) {
|
|
211
|
+
if (settingsHasBillingHelper(readJsonSafe(filePath, readFileSyncImpl))) return true;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Ask Claude Code for its authoritative local auth state. Only the boolean
|
|
218
|
+
* `loggedIn` field is classified; every other outcome is 'unknown' so callers can
|
|
219
|
+
* fall through to the real model invocation as the classifier of record.
|
|
220
|
+
*/
|
|
221
|
+
function checkAuthStatus(opts = {}) {
|
|
222
|
+
const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function' ? opts.spawnSyncImpl : spawnSync;
|
|
223
|
+
const bin = typeof opts.claudeBin === 'string' ? opts.claudeBin : resolveClaudeBin(opts);
|
|
224
|
+
let res;
|
|
225
|
+
try {
|
|
226
|
+
res = spawnSyncImpl(bin, ['auth', 'status'], {
|
|
227
|
+
encoding: 'utf-8',
|
|
228
|
+
env: claudeChildEnv(),
|
|
229
|
+
timeout: 5000,
|
|
230
|
+
maxBuffer: 1024 * 1024,
|
|
231
|
+
});
|
|
232
|
+
} catch {
|
|
233
|
+
return 'unknown';
|
|
234
|
+
}
|
|
235
|
+
if (!res || res.error || res.status !== 0) return 'unknown';
|
|
236
|
+
try {
|
|
237
|
+
const status = JSON.parse(String(res.stdout || ''));
|
|
238
|
+
if (!status || typeof status.loggedIn !== 'boolean') return 'unknown';
|
|
239
|
+
return status.loggedIn ? 'logged-in' : 'logged-out';
|
|
240
|
+
} catch {
|
|
241
|
+
return 'unknown';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* detect(): "usable now", not merely "installed" (EXTRACT-PER-CLIENT W1 FIX,
|
|
247
|
+
* PUNCH-LIST P1). Two bugs fixed here:
|
|
248
|
+
* 1. A resolved filesystem candidate used to short-circuit straight to
|
|
249
|
+
* `true` with NO auth check at all — a stale, logged-out install still
|
|
250
|
+
* "detected". Now the auth check ALWAYS runs, regardless of how the
|
|
251
|
+
* binary was found.
|
|
252
|
+
* 2. The PATH-fallback branch returned `status !== 'unknown'`, which is
|
|
253
|
+
* true for BOTH 'logged-in' AND 'logged-out' — only 'unknown' (auth
|
|
254
|
+
* state could not be determined) read as unusable. That is backwards:
|
|
255
|
+
* 'unknown' cannot PROVE the builder is logged out, so the real call is
|
|
256
|
+
* the classifier of record (see runExtractMode's own pre-spawn check);
|
|
257
|
+
* 'logged-out' is the one status detect() can act on with confidence.
|
|
258
|
+
* true iff: the billing-helper detector does NOT fire (a foreign-billing
|
|
259
|
+
* helper is a skip, not a usable provider — see detectBillingHelperConfigured
|
|
260
|
+
* above) AND auth status is 'logged-in' or 'unknown' (never 'logged-out').
|
|
261
|
+
*/
|
|
262
|
+
function detect(opts = {}) {
|
|
263
|
+
if (detectBillingHelperConfigured(opts)) return false;
|
|
264
|
+
const bin = resolveClaudeBin(opts);
|
|
265
|
+
const status = checkAuthStatus({ ...opts, claudeBin: bin });
|
|
266
|
+
return status === 'logged-in' || status === 'unknown';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* mode:'extract' — draft learnings from a transcript. Sync (no I/O the caller
|
|
271
|
+
* needs to await beyond the spawn itself); `runModel` wraps it in a resolved
|
|
272
|
+
* Promise, per the provider.interface.js contract.
|
|
273
|
+
*/
|
|
274
|
+
function runExtractMode(opts) {
|
|
275
|
+
const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function' ? opts.spawnSyncImpl : spawnSync;
|
|
276
|
+
const bin = typeof opts.claudeBin === 'string' ? opts.claudeBin : resolveClaudeBin(opts);
|
|
277
|
+
const authStatus = checkAuthStatus({ spawnSyncImpl, claudeBin: bin, ...opts });
|
|
278
|
+
if (authStatus === 'logged-out') {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
text: '',
|
|
282
|
+
usage: null,
|
|
283
|
+
reason: 'local model not authenticated in this context (run `claude auth login` once); skipping deterministic extraction',
|
|
284
|
+
reasonCode: 'cli-unauthenticated',
|
|
285
|
+
authStatus,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
|
|
289
|
+
const stdin = prompt + String(opts.input || '').slice(0, 200000);
|
|
290
|
+
let res;
|
|
291
|
+
try {
|
|
292
|
+
// --no-session-persistence (EXTRACT-PER-CLIENT W1 FIX GIVENS): matches the
|
|
293
|
+
// judge spawn below — an extraction run leaves no session file behind either.
|
|
294
|
+
res = spawnSyncImpl(bin, ['-p', '--no-session-persistence', '--tools', ''], {
|
|
295
|
+
input: stdin,
|
|
296
|
+
encoding: 'utf-8',
|
|
297
|
+
env: claudeChildEnv(),
|
|
298
|
+
timeout: opts.timeoutMs || 120000,
|
|
299
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
300
|
+
});
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus };
|
|
303
|
+
}
|
|
304
|
+
if (!res) {
|
|
305
|
+
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus };
|
|
306
|
+
}
|
|
307
|
+
const out = String(res.stdout || '');
|
|
308
|
+
if (res.error) {
|
|
309
|
+
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus };
|
|
310
|
+
}
|
|
311
|
+
// Claude prints auth failures ("API Error: 401 ... Please run /login") to stdout.
|
|
312
|
+
if (/Please run \/login|authentication_error|401/i.test(out) || /Please run \/login|authentication_error/i.test(String(res.stderr || ''))) {
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
text: out,
|
|
316
|
+
usage: null,
|
|
317
|
+
reason: 'local model not authenticated in this context (run `claude auth login` once); skipping deterministic extraction',
|
|
318
|
+
reasonCode: 'cli-unauthenticated',
|
|
319
|
+
authStatus,
|
|
320
|
+
...(authStatus === 'logged-in' && { authDiscrepancy: true }),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (res.status !== 0) {
|
|
324
|
+
return {
|
|
325
|
+
ok: false,
|
|
326
|
+
text: out,
|
|
327
|
+
usage: null,
|
|
328
|
+
reason: `local model exited ${res.status}: ${(out || String(res.stderr || '')).slice(0, 160)}`,
|
|
329
|
+
reasonCode: 'model-error',
|
|
330
|
+
authStatus,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return { ok: true, text: out, usage: null, reason: null, authStatus };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Normalize Claude/Anthropic's raw wrapper.usage (input_tokens, output_tokens,
|
|
338
|
+
* cache_creation_input_tokens, cache_read_input_tokens) into the provider
|
|
339
|
+
* contract's {input_tokens, output_tokens} shape. Cache tokens fold into
|
|
340
|
+
* input_tokens (unchanged summing logic from the pre-move judgeUsage). Returns
|
|
341
|
+
* null when no usable numbers are present — the caller (extract-local.js) does
|
|
342
|
+
* its own text-length estimate fallback in that case, generically, for whichever
|
|
343
|
+
* provider ran.
|
|
344
|
+
*/
|
|
345
|
+
function normalizeJudgeUsage(rawUsage) {
|
|
346
|
+
if (!rawUsage || typeof rawUsage !== 'object') return null;
|
|
347
|
+
const directInput = Number(rawUsage.input_tokens) || 0;
|
|
348
|
+
const cacheCreation = Number(rawUsage.cache_creation_input_tokens) || 0;
|
|
349
|
+
const cacheRead = Number(rawUsage.cache_read_input_tokens) || 0;
|
|
350
|
+
const output = Number(rawUsage.output_tokens) || 0;
|
|
351
|
+
const inputSum = directInput + cacheCreation + cacheRead;
|
|
352
|
+
if (!inputSum && !output) return null;
|
|
353
|
+
return { input_tokens: inputSum, output_tokens: output };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** mode:'judge' — binary anchored-dedup decision. Argv byte-identical to pre-move. */
|
|
357
|
+
function runJudgeMode(opts) {
|
|
358
|
+
const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function' ? opts.spawnSyncImpl : spawnSync;
|
|
359
|
+
const bin = typeof opts.claudeBin === 'string' ? opts.claudeBin : resolveClaudeBin(opts);
|
|
360
|
+
const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
|
|
361
|
+
let res;
|
|
362
|
+
try {
|
|
363
|
+
res = spawnSyncImpl(bin, ['-p', '--output-format', 'json', '--no-session-persistence', '--tools', ''], {
|
|
364
|
+
input: prompt,
|
|
365
|
+
encoding: 'utf8',
|
|
366
|
+
env: claudeChildEnv(),
|
|
367
|
+
timeout: opts.timeoutMs || 120000,
|
|
368
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus: 'unknown' };
|
|
372
|
+
}
|
|
373
|
+
if (!res) {
|
|
374
|
+
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus: 'unknown' };
|
|
375
|
+
}
|
|
376
|
+
const stdout = String(res.stdout || '');
|
|
377
|
+
if (res.error) {
|
|
378
|
+
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus: 'unknown' };
|
|
379
|
+
}
|
|
380
|
+
if (/Please run \/login|authentication_error|401/i.test(stdout) || /Please run \/login|authentication_error/i.test(String(res.stderr || ''))) {
|
|
381
|
+
return { ok: false, text: '', usage: null, reason: 'local judge model is not authenticated', reasonCode: 'cli-unauthenticated', authStatus: 'unknown' };
|
|
382
|
+
}
|
|
383
|
+
if (res.status !== 0) {
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
text: '',
|
|
387
|
+
usage: null,
|
|
388
|
+
reason: `local judge exited ${res.status}: ${(stdout || String(res.stderr || '')).slice(0, 160)}`,
|
|
389
|
+
reasonCode: 'model-error',
|
|
390
|
+
authStatus: 'unknown',
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
let wrapper;
|
|
394
|
+
try {
|
|
395
|
+
wrapper = JSON.parse(stdout);
|
|
396
|
+
} catch {
|
|
397
|
+
return { ok: false, text: '', usage: null, reason: 'local judge returned malformed JSON wrapper', reasonCode: 'model-error', authStatus: 'unknown' };
|
|
398
|
+
}
|
|
399
|
+
if (!wrapper || typeof wrapper.result !== 'string' || wrapper.is_error === true) {
|
|
400
|
+
return { ok: false, text: '', usage: null, reason: 'local judge returned no successful result', reasonCode: 'model-error', authStatus: 'unknown' };
|
|
401
|
+
}
|
|
402
|
+
return { ok: true, text: wrapper.result, usage: normalizeJudgeUsage(wrapper.usage), reason: null, authStatus: 'unknown' };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* runModel(opts) — the provider.interface.js contract. Checks the billing-helper
|
|
407
|
+
* detector BEFORE any auth check or spawn (both modes); mode:'extract' then does
|
|
408
|
+
* its existing pre-spawn auth short-circuit, mode:'judge' spawns directly as it
|
|
409
|
+
* always has (auth failure there is only knowable post-hoc, from the output).
|
|
410
|
+
*/
|
|
411
|
+
async function runModel(opts = {}) {
|
|
412
|
+
if (detectBillingHelperConfigured(opts)) {
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
text: '',
|
|
416
|
+
usage: null,
|
|
417
|
+
reason: 'a foreign-billing CLI helper is configured — extraction declines to run under it',
|
|
418
|
+
reasonCode: 'cli-billing-helper-configured',
|
|
419
|
+
authStatus: 'unknown',
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const mode = opts.mode === 'judge' ? 'judge' : 'extract';
|
|
423
|
+
return mode === 'judge' ? runJudgeMode(opts) : runExtractMode(opts);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Legacy-shaped synchronous entry point — extractWithClaudeCode(transcript, opts).
|
|
428
|
+
* Kept (not deleted) because test/ext-0806b-silent-skip.test.js imports it
|
|
429
|
+
* directly from scripts/extract-local.js (which re-exports it from here) and
|
|
430
|
+
* exercises its exact auth short-circuit / reason-code behavior. Implemented as a
|
|
431
|
+
* direct call into the same runExtractMode() runModel() itself uses — no drift
|
|
432
|
+
* between the two entry points, including the billing-helper gate.
|
|
433
|
+
*/
|
|
434
|
+
function extractWithClaudeCode(transcript, opts = {}) {
|
|
435
|
+
if (detectBillingHelperConfigured(opts)) {
|
|
436
|
+
return {
|
|
437
|
+
ok: false,
|
|
438
|
+
out: '',
|
|
439
|
+
reason: 'a foreign-billing CLI helper is configured — extraction declines to run under it',
|
|
440
|
+
reasonCode: 'cli-billing-helper-configured',
|
|
441
|
+
authStatus: 'unknown',
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const result = runExtractMode({ ...opts, input: transcript });
|
|
445
|
+
return {
|
|
446
|
+
ok: result.ok,
|
|
447
|
+
out: result.text,
|
|
448
|
+
reason: result.reason,
|
|
449
|
+
reasonCode: result.reasonCode,
|
|
450
|
+
authStatus: result.authStatus,
|
|
451
|
+
...(result.authDiscrepancy !== undefined && { authDiscrepancy: result.authDiscrepancy }),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Legacy name preserved for direct re-export (test/ext-0806b-silent-skip.test.js). */
|
|
456
|
+
const checkClaudeAuthStatus = checkAuthStatus;
|
|
457
|
+
|
|
458
|
+
module.exports = {
|
|
459
|
+
runModel,
|
|
460
|
+
detect,
|
|
461
|
+
checkAuthStatus,
|
|
462
|
+
// Legacy-shaped re-exports (extract-local.js forwards these; tests import them
|
|
463
|
+
// directly from extract-local.js's module.exports, per source discipline: grep
|
|
464
|
+
// confirmed no other consumer needs invokeJudgeWithClaudeCode/judgeUsage moved
|
|
465
|
+
// this way, so those two are NOT re-exported — no dead surface carried).
|
|
466
|
+
extractWithClaudeCode,
|
|
467
|
+
checkClaudeAuthStatus,
|
|
468
|
+
resolveClaudeBin,
|
|
469
|
+
// Exported for direct unit coverage (test/claude-code-provider.test.js) and for
|
|
470
|
+
// bin/auxilo-cli.js's cmdStatus provider line.
|
|
471
|
+
claudeChildEnv,
|
|
472
|
+
SCRUBBED_CLIENT_ENV_VARS,
|
|
473
|
+
detectBillingHelperConfigured,
|
|
474
|
+
// Exported for direct unit coverage (test/extract-w1-fix2.test.js, GOV-3 item 6).
|
|
475
|
+
managedSettingsPathForPlatform,
|
|
476
|
+
MANAGED_SETTINGS_PATH_BY_PLATFORM,
|
|
477
|
+
};
|