auxilo-mcp 0.9.14 → 0.9.15
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/lib/extraction-index.js +10 -1
- package/mcp-server.js +1 -1
- package/package.json +3 -2
- package/scripts/extract-local.js +116 -5
- package/scripts/providers/byo-key.js +17 -2
- package/scripts/providers/claude-code.js +155 -14
- package/scripts/providers/index.js +12 -1
- package/scripts/runner.js +5 -0
package/lib/extraction-index.js
CHANGED
|
@@ -14,7 +14,16 @@ const os = require('node:os');
|
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const { findNearDuplicate, scoreChannels } = require('./similarity.js');
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// TEST-HOME-ISOLATION incident 2: same AUXILO_HOME-over-os.homedir() fallback
|
|
18
|
+
// as scripts/providers/index.js's PROVIDERS_STATE_PATH and
|
|
19
|
+
// scripts/providers/byo-key.js's DEFAULT_PROVIDERS_STATE_PATH (duplicated,
|
|
20
|
+
// not imported — see those files' docblocks). This makes the default resolve
|
|
21
|
+
// through the same seam isolation already uses elsewhere, so a caller that
|
|
22
|
+
// forgets an explicit indexPath is still isolated by construction whenever
|
|
23
|
+
// AUXILO_HOME is set (scripts/test/run-isolated.js sets both AUXILO_HOME and
|
|
24
|
+
// HOME, so this is defense-in-depth, not the primary guard — every call site
|
|
25
|
+
// should still pass an explicit indexPath; see test/no-real-home-writes.test.js).
|
|
26
|
+
const DEFAULT_INDEX_PATH = path.join(process.env.AUXILO_HOME || os.homedir(), '.auxilo', 'extracted-index.jsonl');
|
|
18
27
|
const HYDRATION_PAGE_SIZE = 500;
|
|
19
28
|
const PROMPT_MEMORY_MAX_TOKENS = 1200;
|
|
20
29
|
const PROMPT_MEMORY_MAX_ROWS = 40;
|
package/mcp-server.js
CHANGED
|
@@ -198,7 +198,7 @@ async function postBulkChunks(headers, decisions) {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
const server = new Server(
|
|
201
|
-
{ name: 'auxilo', version: '0.9.
|
|
201
|
+
{ name: 'auxilo', version: '0.9.15' },
|
|
202
202
|
{
|
|
203
203
|
capabilities: { tools: {} },
|
|
204
204
|
instructions: `You are connected to Auxilo, a knowledge marketplace where AI agents buy and sell operational learnings.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auxilo-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.15",
|
|
4
4
|
"mcpName": "io.github.silent-architects/auxilo",
|
|
5
5
|
"description": "MCP server for Auxilo. Your agent stops solving the same problem twice: auto-extracted learnings, free self-unlocks, and earnings when other agents unlock yours.",
|
|
6
6
|
"main": "mcp-server.js",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"start": "node mcp-server.js",
|
|
35
|
-
"test": "node
|
|
35
|
+
"test": "node scripts/test/run-isolated.js",
|
|
36
|
+
"test:host": "node scripts/test/run-host.js"
|
|
36
37
|
},
|
|
37
38
|
"keywords": [
|
|
38
39
|
"mcp",
|
package/scripts/extract-local.js
CHANGED
|
@@ -458,6 +458,12 @@ async function defaultInvokeModel(transcript, invokeOpts, opts) {
|
|
|
458
458
|
authStatus: result.authStatus,
|
|
459
459
|
extractionModel: await resolveExtractionModelIdentity(result, opts),
|
|
460
460
|
...(result.authDiscrepancy !== undefined && { authDiscrepancy: result.authDiscrepancy }),
|
|
461
|
+
// EXTRACTION-RUN-LOG (0.9.15): additive passthrough for the one-line-per-run
|
|
462
|
+
// provider summary logged at the end of extractLocally() below. Only
|
|
463
|
+
// claude-code's runModel() currently sets these (argv/cliVersion); other
|
|
464
|
+
// providers simply omit them, which the log builder renders as 'n/a'.
|
|
465
|
+
...(result.argv !== undefined && { argv: result.argv }),
|
|
466
|
+
...(result.cliVersion !== undefined && { cliVersion: result.cliVersion }),
|
|
461
467
|
};
|
|
462
468
|
}
|
|
463
469
|
|
|
@@ -476,7 +482,20 @@ function defaultInvokeJudge(opts) {
|
|
|
476
482
|
log: opts.log,
|
|
477
483
|
mode: 'judge',
|
|
478
484
|
});
|
|
479
|
-
return {
|
|
485
|
+
return {
|
|
486
|
+
ok: result.ok,
|
|
487
|
+
out: result.text,
|
|
488
|
+
usage: result.usage,
|
|
489
|
+
reason: result.reason,
|
|
490
|
+
// EXTRACTION-RUN-LOG (0.9.15): threaded through so runAnchoredJudge below
|
|
491
|
+
// can report the judge call's status/argv/CLI version on the run summary
|
|
492
|
+
// log line. Additive — every existing invokeJudge stub in this repo's
|
|
493
|
+
// tests returns a subset of {ok,out,usage,reason} and these three simply
|
|
494
|
+
// come back undefined for them, changing nothing they assert.
|
|
495
|
+
reasonCode: result.reasonCode,
|
|
496
|
+
argv: result.argv,
|
|
497
|
+
cliVersion: result.cliVersion,
|
|
498
|
+
};
|
|
480
499
|
};
|
|
481
500
|
}
|
|
482
501
|
|
|
@@ -516,12 +535,26 @@ function parseJudgeDecisions(raw, candidates, rankings) {
|
|
|
516
535
|
|
|
517
536
|
async function runAnchoredJudge(candidates, indexState, opts = {}) {
|
|
518
537
|
const input = Array.isArray(candidates) ? candidates : [];
|
|
538
|
+
// judgeAttempted/judgeSucceeded (EXTRACTION-RUN-LOG, 0.9.15): additive
|
|
539
|
+
// status fields for the provider-run log line only — every field already on
|
|
540
|
+
// `empty` (kept/dropped/called/prompt_tokens/completion_tokens) keeps its
|
|
541
|
+
// exact pre-existing meaning and every existing caller's assertions on those
|
|
542
|
+
// fields are unaffected. judgeAttempted=true means invokeJudge was actually
|
|
543
|
+
// called (regardless of outcome); judgeSucceeded=true means it returned
|
|
544
|
+
// ok:true AND its output parsed into real decisions (the one path that
|
|
545
|
+
// actually judged anything, as opposed to failing open and keeping
|
|
546
|
+
// everything). This is a finer distinction than `called` — `called` is
|
|
547
|
+
// true even for a malformed-JSON response (an attempted-and-failed case) —
|
|
548
|
+
// so the log builder uses these two instead of `called` to tell
|
|
549
|
+
// "ran"/"failed"/"skipped(no-candidates)" apart accurately.
|
|
519
550
|
const empty = {
|
|
520
551
|
kept: input.slice(),
|
|
521
552
|
dropped: [],
|
|
522
553
|
called: false,
|
|
523
554
|
prompt_tokens: 0,
|
|
524
555
|
completion_tokens: 0,
|
|
556
|
+
judgeAttempted: false,
|
|
557
|
+
judgeSucceeded: false,
|
|
525
558
|
};
|
|
526
559
|
if (!input.length || !indexState || !indexState.usable ||
|
|
527
560
|
!Array.isArray(indexState.rows) || !indexState.rows.length) {
|
|
@@ -538,17 +571,29 @@ async function runAnchoredJudge(candidates, indexState, opts = {}) {
|
|
|
538
571
|
result = await invokeJudge(prompt, { candidates: input, rankings });
|
|
539
572
|
} catch (error) {
|
|
540
573
|
loudLocal(opts, `anchored judge unavailable; keeping all candidates: ${error.message}`);
|
|
541
|
-
return empty;
|
|
574
|
+
return { ...empty, judgeAttempted: true };
|
|
542
575
|
}
|
|
543
576
|
if (!result || !result.ok) {
|
|
544
577
|
loudLocal(opts, `anchored judge unavailable; keeping all candidates: ${result && result.reason ? result.reason : 'unknown error'}`);
|
|
545
|
-
return
|
|
578
|
+
return {
|
|
579
|
+
...empty,
|
|
580
|
+
judgeAttempted: true,
|
|
581
|
+
judgeReasonCode: result && result.reasonCode,
|
|
582
|
+
judgeArgv: result && result.argv,
|
|
583
|
+
judgeCliVersion: result && result.cliVersion,
|
|
584
|
+
};
|
|
546
585
|
}
|
|
547
586
|
const parsed = parseJudgeDecisions(result.out, input, rankings);
|
|
548
587
|
const usage = judgeUsage(result.usage, prompt, result.out);
|
|
588
|
+
const judgeMeta = {
|
|
589
|
+
judgeAttempted: true,
|
|
590
|
+
judgeReasonCode: result.reasonCode,
|
|
591
|
+
judgeArgv: result.argv,
|
|
592
|
+
judgeCliVersion: result.cliVersion,
|
|
593
|
+
};
|
|
549
594
|
if (!parsed.ok) {
|
|
550
595
|
loudLocal(opts, `anchored judge malformed; keeping all candidates: ${parsed.reason}`);
|
|
551
|
-
return { ...empty, called: true, ...usage };
|
|
596
|
+
return { ...empty, called: true, ...usage, ...judgeMeta, judgeSucceeded: false };
|
|
552
597
|
}
|
|
553
598
|
|
|
554
599
|
const kept = [];
|
|
@@ -575,7 +620,7 @@ async function runAnchoredJudge(candidates, indexState, opts = {}) {
|
|
|
575
620
|
kept.push(input[index]);
|
|
576
621
|
}
|
|
577
622
|
}
|
|
578
|
-
return { kept, dropped, called: true, ...usage };
|
|
623
|
+
return { kept, dropped, called: true, ...usage, ...judgeMeta, judgeSucceeded: true };
|
|
579
624
|
}
|
|
580
625
|
|
|
581
626
|
/**
|
|
@@ -630,6 +675,68 @@ function immutableSet(ids) {
|
|
|
630
675
|
|
|
631
676
|
const EXTRACTABLE_SOURCES = immutableSet(EXTRACTABLE_SOURCE_IDS);
|
|
632
677
|
|
|
678
|
+
// ─── Provider-run log line (EXTRACTION-RUN-LOG, PUNCH-LIST P3, 0.9.15) ─────
|
|
679
|
+
//
|
|
680
|
+
// One line per extractLocally() run, written to the runner's log at the point
|
|
681
|
+
// each run completes — never prompt/transcript content. reasonCode/argv/
|
|
682
|
+
// cliVersion are provider-internal diagnostics only (every provider's own
|
|
683
|
+
// `reason` string is secret-free by the same contract runModel() documents in
|
|
684
|
+
// scripts/providers/index.js), so this line carries no learning text.
|
|
685
|
+
const PRE_SPAWN_SKIP_REASON_CODES = new Set([
|
|
686
|
+
'cli-unauthenticated',
|
|
687
|
+
'cli-not-installed',
|
|
688
|
+
'cli-billing-helper-configured',
|
|
689
|
+
'cli-settings-isolation-unsupported',
|
|
690
|
+
'provider-not-configured',
|
|
691
|
+
'providers-file-mode-unsafe',
|
|
692
|
+
'provider-not-installed',
|
|
693
|
+
'no-usable-provider',
|
|
694
|
+
'no-model-provider-available',
|
|
695
|
+
]);
|
|
696
|
+
|
|
697
|
+
function formatArgvForLog(argv) {
|
|
698
|
+
if (!Array.isArray(argv) || !argv.length) return 'n/a';
|
|
699
|
+
return argv.map((arg) => (arg === '' ? "''" : arg)).join(' ');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* `finder` and `judge` report whether each call was actually attempted
|
|
704
|
+
* (spawned) this run, not whether it succeeded — a spawn that ran and then
|
|
705
|
+
* hit a model error still counts as "ran" (it happened; the failure is in
|
|
706
|
+
* `reason`, not in whether isolation applied). `hooks` is `claude-code`-
|
|
707
|
+
* specific: 'isolated' whenever a claude-code spawn this run carried
|
|
708
|
+
* --setting-sources (the only state a spawn can be in per the fail-closed
|
|
709
|
+
* gate in scripts/providers/claude-code.js — it never spawns without the
|
|
710
|
+
* flag), 'unsupported' when the CLI was found not to support the flag at
|
|
711
|
+
* all, 'n/a' for a non-claude-code provider (codex-cli/byo-key isolate by a
|
|
712
|
+
* different mechanism entirely, out of this row's scope).
|
|
713
|
+
*/
|
|
714
|
+
function logProviderRunSummary(opts, runId, modelResult, judged) {
|
|
715
|
+
try {
|
|
716
|
+
const log = typeof opts.log === 'function' ? opts.log : console.error;
|
|
717
|
+
const provider = (modelResult.extractionModel && modelResult.extractionModel.provider) || 'unknown';
|
|
718
|
+
const finderRan = Boolean(modelResult.ok) || !PRE_SPAWN_SKIP_REASON_CODES.has(modelResult.reasonCode);
|
|
719
|
+
const finder = finderRan ? 'ran' : 'skipped';
|
|
720
|
+
let judgeState;
|
|
721
|
+
if (!judged || !judged.judgeAttempted) judgeState = 'skipped(no-candidates)';
|
|
722
|
+
else if (judged.judgeSucceeded) judgeState = 'ran';
|
|
723
|
+
else judgeState = 'failed';
|
|
724
|
+
const argv = (finderRan && modelResult.argv) || (judged && judged.judgeArgv) || null;
|
|
725
|
+
const cliVersion = modelResult.cliVersion || (judged && judged.judgeCliVersion) || null;
|
|
726
|
+
const finderUnsupported = modelResult.reasonCode === 'cli-settings-isolation-unsupported';
|
|
727
|
+
const judgeUnsupported = Boolean(judged && judged.judgeReasonCode === 'cli-settings-isolation-unsupported');
|
|
728
|
+
const hooks = provider === 'claude-code'
|
|
729
|
+
? ((finderUnsupported || judgeUnsupported) ? 'unsupported' : 'isolated')
|
|
730
|
+
: 'n/a';
|
|
731
|
+
log(
|
|
732
|
+
`[providers] run=${runId || 'unknown'} provider=${provider} cli=${cliVersion || '-'} ` +
|
|
733
|
+
`finder=${finder} judge=${judgeState} flags=${formatArgvForLog(argv)} hooks=${hooks}`
|
|
734
|
+
);
|
|
735
|
+
} catch {
|
|
736
|
+
// Logging must never block or fail extraction.
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
633
740
|
async function extractLocally(transcript, sourceType, opts = {}) {
|
|
634
741
|
if (sourceType && !EXTRACTABLE_SOURCES.has(sourceType)) {
|
|
635
742
|
return { learnings: [], skipped: `local extraction not implemented for "${sourceType}" — agent contributes via auxilo_contribute` };
|
|
@@ -671,6 +778,7 @@ async function extractLocally(transcript, sourceType, opts = {}) {
|
|
|
671
778
|
const modelResult = await invokeModel(transcript, { prompt });
|
|
672
779
|
const { ok, out, reason } = modelResult;
|
|
673
780
|
if (!ok) {
|
|
781
|
+
logProviderRunSummary(opts, opts.runId, modelResult, null);
|
|
674
782
|
return {
|
|
675
783
|
learnings: [],
|
|
676
784
|
skipped: reason,
|
|
@@ -737,6 +845,7 @@ async function extractLocally(transcript, sourceType, opts = {}) {
|
|
|
737
845
|
`lexical_filter=${lexicalDropped.length}, anchored_judge=${judged.dropped.length}`
|
|
738
846
|
);
|
|
739
847
|
}
|
|
848
|
+
logProviderRunSummary(opts, opts.runId, modelResult, judged);
|
|
740
849
|
return {
|
|
741
850
|
learnings: judged.kept,
|
|
742
851
|
dedup_dropped: allDropped.length,
|
|
@@ -764,4 +873,6 @@ module.exports = {
|
|
|
764
873
|
extractWithClaudeCode: claudeCodeProvider.extractWithClaudeCode,
|
|
765
874
|
checkClaudeAuthStatus: claudeCodeProvider.checkClaudeAuthStatus,
|
|
766
875
|
resolveClaudeBin: claudeCodeProvider.resolveClaudeBin,
|
|
876
|
+
// EXTRACTION-RUN-LOG (0.9.15) — exported for direct unit coverage.
|
|
877
|
+
formatArgvForLog, logProviderRunSummary, PRE_SPAWN_SKIP_REASON_CODES,
|
|
767
878
|
};
|
|
@@ -37,8 +37,23 @@ const os = require('os');
|
|
|
37
37
|
/** Same literal value as scripts/providers/index.js's PROVIDERS_STATE_PATH —
|
|
38
38
|
* duplicated (not imported) to avoid a circular require (index.js loads this
|
|
39
39
|
* module dynamically via loadOptionalProvider). test/byo-key-provider.test.js
|
|
40
|
-
* pins the two paths equal.
|
|
41
|
-
|
|
40
|
+
* pins the two paths equal.
|
|
41
|
+
*
|
|
42
|
+
* TEST-HOME-ISOLATION: `AUXILO_HOME`, when set, wins over `os.homedir()` —
|
|
43
|
+
* a dedicated override for auxilo's own state directory, distinct from the
|
|
44
|
+
* general-purpose `HOME` every other os.homedir()-based path in this repo
|
|
45
|
+
* reads (settings.json, the VERSION stamp, credentials.json, ...). Per-call
|
|
46
|
+
* `opts.providersStatePath` (every test in this repo passes one) still wins
|
|
47
|
+
* over BOTH — this only narrows what an omitted opts falls back to, closing
|
|
48
|
+
* the gap that let a bare `bin/auxilo-cli.js provider status|clear` call (no
|
|
49
|
+
* opts seam of its own — see cmdProvider in bin/auxilo-cli.js) or any future
|
|
50
|
+
* test that forgets its own override reach the real ~/.auxilo/providers.json
|
|
51
|
+
* (TEST-HOME-ISOLATION incident, 2026-09-06). Evaluated once at module load,
|
|
52
|
+
* same as before — every entry point that cares (scripts/test/run-isolated.js
|
|
53
|
+
* for `npm test`, scripts/check-test-count.sh for the CI gate) sets both
|
|
54
|
+
* AUXILO_HOME and HOME before node starts, so this still resolves correctly
|
|
55
|
+
* even though it's a frozen constant, not a per-call lookup. */
|
|
56
|
+
const DEFAULT_PROVIDERS_STATE_PATH = path.join(process.env.AUXILO_HOME || os.homedir(), '.auxilo', 'providers.json');
|
|
42
57
|
|
|
43
58
|
const VENDOR_DEFAULT_BASE_URL = Object.freeze({
|
|
44
59
|
anthropic: 'https://api.anthropic.com/v1',
|
|
@@ -41,6 +41,93 @@ function resolveClaudeBin(opts = {}) {
|
|
|
41
41
|
return 'claude';
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// ─── Child settings/hooks isolation (EXTRACTION-CHILD-HOOKS, PUNCH-LIST P1,
|
|
45
|
+
// 0.9.15) ────────────────────────────────────────────────────────────────
|
|
46
|
+
//
|
|
47
|
+
// Every `claude -p` extraction child previously loaded the OPERATOR'S OWN
|
|
48
|
+
// ~/.claude/settings.json and therefore fired their personal SessionStart
|
|
49
|
+
// hooks (mandate.sh, session-context.sh, ...) — output that reaches the
|
|
50
|
+
// extraction/judge prompt without ever passing the package's scrubber. That
|
|
51
|
+
// is a privacy defect: content from the machine's own hook configuration
|
|
52
|
+
// (potentially personal notes, live queries, etc.) enters a transcript that
|
|
53
|
+
// gets sent to the resolved provider.
|
|
54
|
+
//
|
|
55
|
+
// `--setting-sources <sources>` ("Comma-separated list of setting sources to
|
|
56
|
+
// load (user, project, local)") is documented on both installed CLIs probed
|
|
57
|
+
// during investigation (2.1.12, 2.1.260) — scratchpad hooks-0914/
|
|
58
|
+
// EXTRACTION-CHILD-HOOKS-FINDINGS.md. This build (hooks-0915) tested LIVE
|
|
59
|
+
// whether an EMPTY list (`--setting-sources ''`) — the narrowest, fully
|
|
60
|
+
// cwd-independent value, loading none of user/project/local — is accepted:
|
|
61
|
+
// it is (2.1.12, exit 0, hook_response count 0, well-formed result). Shipping
|
|
62
|
+
// `''` means no fresh-temp-cwd workaround is needed: an empty source list
|
|
63
|
+
// loads nothing regardless of the child's cwd, unlike the `project,local`
|
|
64
|
+
// fallback (which still honors a target repo's own `.claude/settings.json`).
|
|
65
|
+
const SETTING_SOURCES_VALUE = '';
|
|
66
|
+
const SETTING_SOURCES_ARGS = Object.freeze(['--setting-sources', SETTING_SOURCES_VALUE]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fail-closed detection of "this CLI build doesn't understand
|
|
70
|
+
* --setting-sources at all" (an old install predating the flag, or a rename).
|
|
71
|
+
* Detected from the SAME spawn that already carries the flag — no extra
|
|
72
|
+
* `--help`/`--version` probe call, so the happy-path spawn count for every
|
|
73
|
+
* existing caller/test is unchanged. A CLI rejecting an unknown flag exits
|
|
74
|
+
* non-zero with a message naming the flag (commander.js-style "error:
|
|
75
|
+
* unknown option '--setting-sources'"); this pattern-matches for that
|
|
76
|
+
* specific shape rather than treating every non-zero exit as unsupported (a
|
|
77
|
+
* real model/auth error must NOT be misreported as isolation-unsupported).
|
|
78
|
+
* Cached module-wide for the lifetime of the process ("once per run"): the
|
|
79
|
+
* first spawn that hits this failure marks the CLI unsupported and every
|
|
80
|
+
* subsequent runModel() call in the same process short-circuits BEFORE
|
|
81
|
+
* spawning again — it must never spawn without the flag, and re-attempting a
|
|
82
|
+
* doomed spawn every call would be silent waste, not safety.
|
|
83
|
+
*/
|
|
84
|
+
let cachedSettingSourcesUnsupported; // undefined = not yet observed; true once detected
|
|
85
|
+
|
|
86
|
+
function looksLikeUnsupportedSettingSourcesFlag(res) {
|
|
87
|
+
if (!res || res.status === 0) return false;
|
|
88
|
+
const combined = `${String(res.stdout || '')}\n${String(res.stderr || '')}`;
|
|
89
|
+
return /--setting-sources/.test(combined) && /\b(unknown|unrecognized|invalid)\b.{0,20}\b(option|argument|flag)\b/i.test(combined);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function settingSourcesIsolationUnsupportedResult(authStatus) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
text: '',
|
|
96
|
+
usage: null,
|
|
97
|
+
reason: 'installed Claude Code CLI does not support --setting-sources; extraction declines to run a child that would load the operator\'s own settings/hooks unisolated',
|
|
98
|
+
reasonCode: 'cli-settings-isolation-unsupported',
|
|
99
|
+
authStatus: authStatus || 'unknown',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Test-only: reset the module-level isolation-support cache between fixtures. */
|
|
104
|
+
function _resetSettingSourcesCacheForTests() {
|
|
105
|
+
cachedSettingSourcesUnsupported = undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── CLI version, for diagnostics only (no subprocess spawn) ───────────────
|
|
109
|
+
//
|
|
110
|
+
// Resolves the installed package's own package.json version by following the
|
|
111
|
+
// resolved binary's real path (e.g. `/usr/local/bin/claude` -> `.../
|
|
112
|
+
// node_modules/@anthropic-ai/claude-code/cli.js`) and reading the sibling
|
|
113
|
+
// package.json — filesystem-only, so it never adds a spawn to the extraction
|
|
114
|
+
// path (verified live: realpath + package.json read, no `claude --version`
|
|
115
|
+
// call). Best-effort: any failure (bare `claude` unresolved via PATH, an
|
|
116
|
+
// install layout that doesn't carry a sibling package.json, a fixture path in
|
|
117
|
+
// tests) yields null, never throws.
|
|
118
|
+
function getClaudeCliVersion(bin, opts = {}) {
|
|
119
|
+
const realpathSyncImpl = typeof opts.realpathSyncImpl === 'function' ? opts.realpathSyncImpl : fs.realpathSync;
|
|
120
|
+
const readFileSyncImpl = typeof opts.readFileSyncImpl === 'function' ? opts.readFileSyncImpl : fs.readFileSync;
|
|
121
|
+
try {
|
|
122
|
+
const real = realpathSyncImpl(bin);
|
|
123
|
+
const pkgPath = path.join(path.dirname(real), 'package.json');
|
|
124
|
+
const pkg = JSON.parse(readFileSyncImpl(pkgPath, 'utf8'));
|
|
125
|
+
return pkg && typeof pkg.version === 'string' ? pkg.version : null;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
44
131
|
// ─── Env scrub (EXTRACT-TOOLS-LOCK, PUNCH-LIST) ────────────────────────────
|
|
45
132
|
//
|
|
46
133
|
// SME-confirmed list (claude-code-guide, verified against official docs and
|
|
@@ -79,6 +166,20 @@ const SCRUBBED_CLIENT_ENV_VARS = Object.freeze([
|
|
|
79
166
|
'CLOUD_ML_REGION',
|
|
80
167
|
]);
|
|
81
168
|
|
|
169
|
+
// ─── Spawn argv (EXTRACTION-ZERO-TOOL-CALLS control) ───────────────────────
|
|
170
|
+
//
|
|
171
|
+
// Named + frozen so a byte-pinned test (test/extraction-zero-tool-calls.test.js,
|
|
172
|
+
// test/claude-code-provider.test.js) can assert the exact argv without
|
|
173
|
+
// duplicating the literal, and so a future flag change is a conscious,
|
|
174
|
+
// greppable edit here rather than a silent literal tweak buried in the two
|
|
175
|
+
// runXMode() functions below. No behavior change: the two spawnSyncImpl()
|
|
176
|
+
// call sites below now pass these constants instead of inline array literals
|
|
177
|
+
// of the identical contents. 0.9.15 (EXTRACTION-CHILD-HOOKS) appends
|
|
178
|
+
// SETTING_SOURCES_ARGS to both — the child loads none of user/project/local
|
|
179
|
+
// settings, so the operator's own SessionStart hooks never fire.
|
|
180
|
+
const EXTRACT_MODE_ARGV = Object.freeze(['-p', '--no-session-persistence', '--tools', '', ...SETTING_SOURCES_ARGS]);
|
|
181
|
+
const JUDGE_MODE_ARGV = Object.freeze(['-p', '--output-format', 'json', '--no-session-persistence', '--tools', '', ...SETTING_SOURCES_ARGS]);
|
|
182
|
+
|
|
82
183
|
/**
|
|
83
184
|
* Build the subscription-auth-only environment shared by BOTH the extraction and
|
|
84
185
|
* judge Claude CLI children — one function, no drift between the two spawns.
|
|
@@ -297,6 +398,7 @@ function detect(opts = {}) {
|
|
|
297
398
|
function runExtractMode(opts) {
|
|
298
399
|
const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function' ? opts.spawnSyncImpl : spawnSync;
|
|
299
400
|
const bin = typeof opts.claudeBin === 'string' ? opts.claudeBin : resolveClaudeBin(opts);
|
|
401
|
+
if (cachedSettingSourcesUnsupported) return settingSourcesIsolationUnsupportedResult('unknown');
|
|
300
402
|
const authStatus = checkAuthStatus({ spawnSyncImpl, claudeBin: bin, ...opts });
|
|
301
403
|
if (authStatus === 'logged-out') {
|
|
302
404
|
return {
|
|
@@ -310,11 +412,18 @@ function runExtractMode(opts) {
|
|
|
310
412
|
}
|
|
311
413
|
const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
|
|
312
414
|
const stdin = prompt + String(opts.input || '').slice(0, 200000);
|
|
415
|
+
// --no-session-persistence (EXTRACT-PER-CLIENT W1 FIX GIVENS): matches the
|
|
416
|
+
// judge spawn below — an extraction run leaves no session file behind either.
|
|
417
|
+
// --setting-sources '' (EXTRACTION-CHILD-HOOKS, 0.9.15): the child loads none
|
|
418
|
+
// of user/project/local settings, so the operator's own SessionStart hooks
|
|
419
|
+
// never fire and their output never reaches this prompt.
|
|
420
|
+
const argv = EXTRACT_MODE_ARGV;
|
|
421
|
+
const cliVersion = getClaudeCliVersion(bin, opts);
|
|
313
422
|
let res;
|
|
314
423
|
try {
|
|
315
424
|
// --no-session-persistence (EXTRACT-PER-CLIENT W1 FIX GIVENS): matches the
|
|
316
425
|
// judge spawn below — an extraction run leaves no session file behind either.
|
|
317
|
-
res = spawnSyncImpl(bin,
|
|
426
|
+
res = spawnSyncImpl(bin, argv, {
|
|
318
427
|
input: stdin,
|
|
319
428
|
encoding: 'utf-8',
|
|
320
429
|
env: claudeChildEnv(),
|
|
@@ -322,14 +431,18 @@ function runExtractMode(opts) {
|
|
|
322
431
|
maxBuffer: 20 * 1024 * 1024,
|
|
323
432
|
});
|
|
324
433
|
} catch (error) {
|
|
325
|
-
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus };
|
|
434
|
+
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus, argv, cliVersion };
|
|
326
435
|
}
|
|
327
436
|
if (!res) {
|
|
328
|
-
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus };
|
|
437
|
+
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus, argv, cliVersion };
|
|
438
|
+
}
|
|
439
|
+
if (looksLikeUnsupportedSettingSourcesFlag(res)) {
|
|
440
|
+
cachedSettingSourcesUnsupported = true;
|
|
441
|
+
return { ...settingSourcesIsolationUnsupportedResult(authStatus), argv, cliVersion };
|
|
329
442
|
}
|
|
330
443
|
const out = String(res.stdout || '');
|
|
331
444
|
if (res.error) {
|
|
332
|
-
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus };
|
|
445
|
+
return { ok: false, text: '', usage: null, reason: `spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus, argv, cliVersion };
|
|
333
446
|
}
|
|
334
447
|
// Claude prints auth failures ("API Error: 401 ... Please run /login") to stdout.
|
|
335
448
|
if (/Please run \/login|authentication_error|401/i.test(out) || /Please run \/login|authentication_error/i.test(String(res.stderr || ''))) {
|
|
@@ -340,6 +453,8 @@ function runExtractMode(opts) {
|
|
|
340
453
|
reason: 'local model not authenticated in this context (run `claude auth login` once); skipping deterministic extraction',
|
|
341
454
|
reasonCode: 'cli-unauthenticated',
|
|
342
455
|
authStatus,
|
|
456
|
+
argv,
|
|
457
|
+
cliVersion,
|
|
343
458
|
...(authStatus === 'logged-in' && { authDiscrepancy: true }),
|
|
344
459
|
};
|
|
345
460
|
}
|
|
@@ -351,9 +466,11 @@ function runExtractMode(opts) {
|
|
|
351
466
|
reason: `local model exited ${res.status}: ${(out || String(res.stderr || '')).slice(0, 160)}`,
|
|
352
467
|
reasonCode: 'model-error',
|
|
353
468
|
authStatus,
|
|
469
|
+
argv,
|
|
470
|
+
cliVersion,
|
|
354
471
|
};
|
|
355
472
|
}
|
|
356
|
-
return { ok: true, text: out, usage: null, reason: null, authStatus };
|
|
473
|
+
return { ok: true, text: out, usage: null, reason: null, authStatus, argv, cliVersion };
|
|
357
474
|
}
|
|
358
475
|
|
|
359
476
|
/**
|
|
@@ -376,14 +493,18 @@ function normalizeJudgeUsage(rawUsage) {
|
|
|
376
493
|
return { input_tokens: inputSum, output_tokens: output };
|
|
377
494
|
}
|
|
378
495
|
|
|
379
|
-
/** mode:'judge' — binary anchored-dedup decision. Argv byte-identical to pre-move
|
|
496
|
+
/** mode:'judge' — binary anchored-dedup decision. Argv byte-identical to pre-move plus
|
|
497
|
+
* the same --setting-sources '' isolation the extraction spawn above gains (0.9.15). */
|
|
380
498
|
function runJudgeMode(opts) {
|
|
381
499
|
const spawnSyncImpl = typeof opts.spawnSyncImpl === 'function' ? opts.spawnSyncImpl : spawnSync;
|
|
382
500
|
const bin = typeof opts.claudeBin === 'string' ? opts.claudeBin : resolveClaudeBin(opts);
|
|
501
|
+
if (cachedSettingSourcesUnsupported) return settingSourcesIsolationUnsupportedResult('unknown');
|
|
383
502
|
const prompt = typeof opts.prompt === 'string' ? opts.prompt : '';
|
|
503
|
+
const argv = JUDGE_MODE_ARGV;
|
|
504
|
+
const cliVersion = getClaudeCliVersion(bin, opts);
|
|
384
505
|
let res;
|
|
385
506
|
try {
|
|
386
|
-
res = spawnSyncImpl(bin,
|
|
507
|
+
res = spawnSyncImpl(bin, argv, {
|
|
387
508
|
input: prompt,
|
|
388
509
|
encoding: 'utf8',
|
|
389
510
|
env: claudeChildEnv(),
|
|
@@ -391,17 +512,21 @@ function runJudgeMode(opts) {
|
|
|
391
512
|
maxBuffer: 20 * 1024 * 1024,
|
|
392
513
|
});
|
|
393
514
|
} catch (error) {
|
|
394
|
-
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus: 'unknown' };
|
|
515
|
+
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${error.message}`, reasonCode: 'unknown', authStatus: 'unknown', argv, cliVersion };
|
|
395
516
|
}
|
|
396
517
|
if (!res) {
|
|
397
|
-
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus: 'unknown' };
|
|
518
|
+
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): no process result`, reasonCode: 'unknown', authStatus: 'unknown', argv, cliVersion };
|
|
519
|
+
}
|
|
520
|
+
if (looksLikeUnsupportedSettingSourcesFlag(res)) {
|
|
521
|
+
cachedSettingSourcesUnsupported = true;
|
|
522
|
+
return { ...settingSourcesIsolationUnsupportedResult('unknown'), argv, cliVersion };
|
|
398
523
|
}
|
|
399
524
|
const stdout = String(res.stdout || '');
|
|
400
525
|
if (res.error) {
|
|
401
|
-
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus: 'unknown' };
|
|
526
|
+
return { ok: false, text: '', usage: null, reason: `judge spawn failed (${bin}): ${res.error.message}`, reasonCode: 'unknown', authStatus: 'unknown', argv, cliVersion };
|
|
402
527
|
}
|
|
403
528
|
if (/Please run \/login|authentication_error|401/i.test(stdout) || /Please run \/login|authentication_error/i.test(String(res.stderr || ''))) {
|
|
404
|
-
return { ok: false, text: '', usage: null, reason: 'local judge model is not authenticated', reasonCode: 'cli-unauthenticated', authStatus: 'unknown' };
|
|
529
|
+
return { ok: false, text: '', usage: null, reason: 'local judge model is not authenticated', reasonCode: 'cli-unauthenticated', authStatus: 'unknown', argv, cliVersion };
|
|
405
530
|
}
|
|
406
531
|
if (res.status !== 0) {
|
|
407
532
|
return {
|
|
@@ -411,18 +536,20 @@ function runJudgeMode(opts) {
|
|
|
411
536
|
reason: `local judge exited ${res.status}: ${(stdout || String(res.stderr || '')).slice(0, 160)}`,
|
|
412
537
|
reasonCode: 'model-error',
|
|
413
538
|
authStatus: 'unknown',
|
|
539
|
+
argv,
|
|
540
|
+
cliVersion,
|
|
414
541
|
};
|
|
415
542
|
}
|
|
416
543
|
let wrapper;
|
|
417
544
|
try {
|
|
418
545
|
wrapper = JSON.parse(stdout);
|
|
419
546
|
} catch {
|
|
420
|
-
return { ok: false, text: '', usage: null, reason: 'local judge returned malformed JSON wrapper', reasonCode: 'model-error', authStatus: 'unknown' };
|
|
547
|
+
return { ok: false, text: '', usage: null, reason: 'local judge returned malformed JSON wrapper', reasonCode: 'model-error', authStatus: 'unknown', argv, cliVersion };
|
|
421
548
|
}
|
|
422
549
|
if (!wrapper || typeof wrapper.result !== 'string' || wrapper.is_error === true) {
|
|
423
|
-
return { ok: false, text: '', usage: null, reason: 'local judge returned no successful result', reasonCode: 'model-error', authStatus: 'unknown' };
|
|
550
|
+
return { ok: false, text: '', usage: null, reason: 'local judge returned no successful result', reasonCode: 'model-error', authStatus: 'unknown', argv, cliVersion };
|
|
424
551
|
}
|
|
425
|
-
return { ok: true, text: wrapper.result, usage: normalizeJudgeUsage(wrapper.usage), reason: null, authStatus: 'unknown' };
|
|
552
|
+
return { ok: true, text: wrapper.result, usage: normalizeJudgeUsage(wrapper.usage), reason: null, authStatus: 'unknown', argv, cliVersion };
|
|
426
553
|
}
|
|
427
554
|
|
|
428
555
|
/**
|
|
@@ -497,4 +624,18 @@ module.exports = {
|
|
|
497
624
|
// Exported for direct unit coverage (test/extract-w1-fix2.test.js, GOV-3 item 6).
|
|
498
625
|
managedSettingsPathForPlatform,
|
|
499
626
|
MANAGED_SETTINGS_PATH_BY_PLATFORM,
|
|
627
|
+
// Exported for direct byte-pinned coverage (test/extraction-zero-tool-calls.test.js,
|
|
628
|
+
// test/claude-code-provider.test.js; TRUST-PAGE control — SITE-PM: put the
|
|
629
|
+
// zero-tool-call assertion in the test suite). Carries the 0.9.15 argv
|
|
630
|
+
// (SETTING_SOURCES_ARGS included).
|
|
631
|
+
EXTRACT_MODE_ARGV,
|
|
632
|
+
JUDGE_MODE_ARGV,
|
|
633
|
+
// EXTRACTION-CHILD-HOOKS (0.9.15) — exported for direct unit coverage
|
|
634
|
+
// (test/claude-code-provider.test.js) and for extract-local.js's provider-run
|
|
635
|
+
// log line (getClaudeCliVersion).
|
|
636
|
+
SETTING_SOURCES_VALUE,
|
|
637
|
+
SETTING_SOURCES_ARGS,
|
|
638
|
+
getClaudeCliVersion,
|
|
639
|
+
looksLikeUnsupportedSettingSourcesFlag,
|
|
640
|
+
_resetSettingSourcesCacheForTests,
|
|
500
641
|
};
|
|
@@ -78,7 +78,13 @@ const PROVIDERS = {
|
|
|
78
78
|
'byo-key': byoKey,
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
// TEST-HOME-ISOLATION: same AUXILO_HOME-over-os.homedir() fallback as
|
|
82
|
+
// scripts/providers/byo-key.js's DEFAULT_PROVIDERS_STATE_PATH (duplicated,
|
|
83
|
+
// not imported — see that file's docblock; test/byo-key-provider.test.js
|
|
84
|
+
// pins the two byte-equal). opts.providersStatePath, threaded through every
|
|
85
|
+
// resolveProvider()/runModel()/persistSelected() call site in this repo's
|
|
86
|
+
// tests, still wins over both.
|
|
87
|
+
const PROVIDERS_STATE_PATH = path.join(process.env.AUXILO_HOME || os.homedir(), '.auxilo', 'providers.json');
|
|
82
88
|
|
|
83
89
|
function readProvidersState(statePath) {
|
|
84
90
|
try {
|
|
@@ -232,6 +238,11 @@ const NON_RETRYABLE_FOR_THIS_PROVIDER = new Set([
|
|
|
232
238
|
'cli-billing-helper-configured',
|
|
233
239
|
'provider-not-configured',
|
|
234
240
|
'providers-file-mode-unsafe',
|
|
241
|
+
// EXTRACTION-CHILD-HOOKS (0.9.15): the resolved claude-code CLI doesn't
|
|
242
|
+
// support --setting-sources, so it can never run isolated — same
|
|
243
|
+
// "cannot run at all right now" class as the codes above, safe to try the
|
|
244
|
+
// next provider in PROVIDER_ORDER rather than reporting a hard failure.
|
|
245
|
+
'cli-settings-isolation-unsupported',
|
|
235
246
|
]);
|
|
236
247
|
|
|
237
248
|
/**
|
package/scripts/runner.js
CHANGED
|
@@ -645,6 +645,11 @@ async function postExtractDetailed(transcript, sessionId, sourceType, _scrubRepo
|
|
|
645
645
|
captureVisibility: opts.captureVisibility || CAPTURE_VISIBILITY,
|
|
646
646
|
log: runnerLog,
|
|
647
647
|
auditLog: auditDropLog,
|
|
648
|
+
// EXTRACTION-RUN-LOG (0.9.15): the identifier the provider-run summary
|
|
649
|
+
// log line reports as `run=`. sessionId is already this call's natural
|
|
650
|
+
// run identity (it's what extraction_id: `client-${sessionId}` uses
|
|
651
|
+
// below); opts.runId (if a caller supplied one) wins over it.
|
|
652
|
+
runId: opts.runId || sessionId,
|
|
648
653
|
}));
|
|
649
654
|
} catch (err) {
|
|
650
655
|
throw new Error(`Local extraction failed: ${err.message}`);
|