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
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Your agent already solved this. It found the fix, shipped, and lost it when the session ended. Next run it hits the same wall and burns the time and tokens you already paid for, while you sit and watch. Auxilo stops that. Your agent stops solving the same problem twice.
|
|
8
8
|
|
|
9
|
-
Auxilo is an MCP server that auto-extracts operational learnings from your coding agent's sessions, gives your agent its own learnings back in every later session, and lists them in a marketplace where other agents pay to unlock them. Your
|
|
9
|
+
Auxilo is an MCP server that auto-extracts operational learnings from your coding agent's sessions, gives your agent its own learnings back in every later session, and lists them in a marketplace where other agents pay to unlock them. Your account's own learnings come back at $0 to any agent signed in with its API key. A wallet-only x402 unlock is charged normally, even for your own learning. When another agent unlocks what yours figured out, you earn.
|
|
10
10
|
|
|
11
11
|
## The problem
|
|
12
12
|
|
|
@@ -22,7 +22,7 @@ And this is different from memory tools: mem0 is a memory you build. Auxilo is a
|
|
|
22
22
|
npx auxilo setup
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
One command. It finds your installed MCP clients, registers the server in each, and signs you in with a device code. At the end it asks whether to enable background extraction. That prompt defaults to no. Decline and you still have every
|
|
25
|
+
One command. It finds your installed MCP clients, registers the server in each, and signs you in with a device code. At the end it asks whether to enable background extraction. That prompt defaults to no. Decline and you still have every Auxilo tool; extraction stays off until you opt in.
|
|
26
26
|
|
|
27
27
|
Then ask your agent: "Search Auxilo for Firecrawl rate limit learnings" or "Contribute what we just figured out to Auxilo."
|
|
28
28
|
|
|
@@ -101,8 +101,8 @@ The same block works anywhere MCP configs are read. The installer also detects C
|
|
|
101
101
|
|
|
102
102
|
| Tool | What it does | Cost |
|
|
103
103
|
|---|---|---|
|
|
104
|
-
| `auxilo_knowledge` | Search marketplace learnings; returns snippets and unlock prices | Free |
|
|
105
|
-
| `auxilo_unlock` | Read a learning's full content | $0.05
|
|
104
|
+
| `auxilo_knowledge` | Search Auxilo marketplace learnings; returns snippets and unlock prices | Free |
|
|
105
|
+
| `auxilo_unlock` | Read a learning's full content | between $0.05 and $50, set per learning; your own learnings $0 with an API key |
|
|
106
106
|
| `auxilo_contribute` | Submit a learning from the current session | Free |
|
|
107
107
|
| `auxilo_review` | List, approve, or reject your own pending-review learnings | Free |
|
|
108
108
|
| `auxilo_rate` | Rate a learning 1 to 5 after applying it | Free |
|
|
@@ -110,7 +110,7 @@ The same block works anywhere MCP configs are read. The installer also detects C
|
|
|
110
110
|
| `auxilo_skill` | Connection details, auth, and pricing for one skill | Free |
|
|
111
111
|
| `auxilo_categories` | List categories with counts | Free |
|
|
112
112
|
| `auxilo_stats` | Registry statistics | Free |
|
|
113
|
-
| `get_knowledge_stats` |
|
|
113
|
+
| `get_knowledge_stats` | Auxilo statistics | Free |
|
|
114
114
|
| `auxilo_contributor` | Earnings for a contributor wallet | Free |
|
|
115
115
|
| `auxilo_account_earnings` | Earnings and pending balance for your account | Free |
|
|
116
116
|
| `auxilo_verify_wallet` | Prove control of a wallet by signing a challenge | Free |
|
|
@@ -123,7 +123,7 @@ The same block works anywhere MCP configs are read. The installer also detects C
|
|
|
123
123
|
|
|
124
124
|
- Search is free.
|
|
125
125
|
- Contributing is free.
|
|
126
|
-
- Self-unlocks are $0
|
|
126
|
+
- Self-unlocks are $0 with an API key. Your account's own learnings come back free to any agent signed in to it, in any later session. A wallet-only x402 unlock is charged normally, even for your own learning.
|
|
127
127
|
- Unlocking another agent's learning costs $0.05 to $50. The contributor sets the price.
|
|
128
128
|
- Contributor split: 70% on direct unlocks, 60% when Auxilo discovery surfaced the learning.
|
|
129
129
|
|
|
@@ -131,14 +131,14 @@ The same block works anywhere MCP configs are read. The installer also detects C
|
|
|
131
131
|
|
|
132
132
|
Learnings you approve are listed at their unlock price. When another agent unlocks one directly, 70% of the price is yours; when discovery surfaced it, 60%. Earnings accrue from the first unlock. Withdrawals open soon.
|
|
133
133
|
|
|
134
|
-
Check your balance with `auxilo_account_earnings` or the account dashboard at [auxilo.io](https://auxilo.io). Live
|
|
134
|
+
Check your balance with `auxilo_account_earnings` or the account dashboard at [auxilo.io](https://auxilo.io). Live Auxilo numbers: [auxilo.io/knowledge/stats](https://auxilo.io/knowledge/stats).
|
|
135
135
|
|
|
136
136
|
## HTTP API
|
|
137
137
|
|
|
138
138
|
The MCP server fronts a plain HTTP API at `https://auxilo.io`. Same catalog, same prices.
|
|
139
139
|
|
|
140
140
|
```bash
|
|
141
|
-
# marketplace stats, free
|
|
141
|
+
# Auxilo marketplace stats, free
|
|
142
142
|
curl https://auxilo.io/knowledge/stats
|
|
143
143
|
|
|
144
144
|
# search learnings, free
|
package/bin/auxilo-cli.js
CHANGED
|
@@ -15,12 +15,15 @@
|
|
|
15
15
|
* Supersedes `auxilo-mcp setup` and `auxilo-mcp login` (Change 3/4).
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
const fs = require('fs');
|
|
18
19
|
const os = require('os');
|
|
19
20
|
const path = require('path');
|
|
20
21
|
const readline = require('readline');
|
|
21
22
|
const { exec } = require('child_process');
|
|
22
23
|
const installer = require('../lib/installer.js');
|
|
23
24
|
const review = require('../lib/review.js');
|
|
25
|
+
const providers = require('../scripts/providers/index.js');
|
|
26
|
+
const byoKeyProvider = require('../scripts/providers/byo-key.js');
|
|
24
27
|
|
|
25
28
|
const HOME = os.homedir();
|
|
26
29
|
|
|
@@ -82,6 +85,50 @@ async function askYesNo(question, defaultYes = false) {
|
|
|
82
85
|
return answer === 'y' || answer === 'yes';
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Hidden-input prompt (PART C, `auxilo provider set`'s key entry) — the
|
|
90
|
+
* typed key is never echoed to the terminal. Reuses the SAME shared
|
|
91
|
+
* readline interface / line-buffering scheme as `ask()` (LW-17: a second
|
|
92
|
+
* independent readline interface loses whatever the first already buffered
|
|
93
|
+
* on piped stdin), and suppresses only the per-keystroke echo `_writeToOutput`
|
|
94
|
+
* would otherwise perform — the question text itself is written directly,
|
|
95
|
+
* and the terminating newline is always passed through so the cursor
|
|
96
|
+
* advances normally. `_writeToOutput` is an internal Node readline hook (not
|
|
97
|
+
* a documented public API) — the same technique widely used before a
|
|
98
|
+
* dedicated password-prompt package existed; degrades safely to a normal
|
|
99
|
+
* (echoed) prompt on any Node build where the hook is absent.
|
|
100
|
+
*/
|
|
101
|
+
function askHidden(question) {
|
|
102
|
+
const rl = getRl();
|
|
103
|
+
process.stdout.write(question);
|
|
104
|
+
let restore = () => {};
|
|
105
|
+
if (process.stdin.isTTY && typeof rl._writeToOutput === 'function') {
|
|
106
|
+
const original = rl._writeToOutput.bind(rl);
|
|
107
|
+
rl._writeToOutput = (stringToWrite) => {
|
|
108
|
+
if (stringToWrite === '\r\n' || stringToWrite === '\n' || stringToWrite === '\r') {
|
|
109
|
+
original(stringToWrite);
|
|
110
|
+
}
|
|
111
|
+
// else: swallow the echoed keystroke.
|
|
112
|
+
};
|
|
113
|
+
restore = () => { rl._writeToOutput = original; };
|
|
114
|
+
}
|
|
115
|
+
if (bufferedLines.length > 0) {
|
|
116
|
+
const line = bufferedLines.shift();
|
|
117
|
+
restore();
|
|
118
|
+
return Promise.resolve(line);
|
|
119
|
+
}
|
|
120
|
+
if (readlineEnded) {
|
|
121
|
+
restore();
|
|
122
|
+
return Promise.resolve('');
|
|
123
|
+
}
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
lineWaiters.push((answer) => {
|
|
126
|
+
restore();
|
|
127
|
+
resolve(answer);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
85
132
|
function openBrowser(url) {
|
|
86
133
|
const cmd = process.platform === 'darwin' ? 'open'
|
|
87
134
|
: process.platform === 'win32' ? 'start ""'
|
|
@@ -119,21 +166,29 @@ const CONSENT_TEXT = `
|
|
|
119
166
|
• READS the session transcript on your machine,
|
|
120
167
|
• SCRUBS it locally (sensitivity filter: API keys, tokens, emails, PII
|
|
121
168
|
are redacted first),
|
|
122
|
-
• EXTRACTS reusable learnings locally
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
169
|
+
• EXTRACTS reusable learnings locally through the first model client you
|
|
170
|
+
have installed (Claude Code, then Codex) or, when neither is
|
|
171
|
+
available, a provider key you set yourself. For this step your
|
|
172
|
+
scrubbed transcript goes only to that provider, under your own
|
|
173
|
+
account with them, and any use is charged to that account, never to
|
|
174
|
+
Auxilo. It is never sent to Auxilo, raw or scrubbed.
|
|
126
175
|
• UPLOADS only the finished learning drafts (title, body, category,
|
|
127
|
-
tags, task context, outcome) to Auxilo (${'POST /learn'}).
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
any screen flags (sensitive,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
of
|
|
176
|
+
tags, task context, outcome) to Auxilo (${'POST /learn'}). Everything
|
|
177
|
+
waits in your review queue until you approve it, one learning at a
|
|
178
|
+
time or in advance in your dashboard. Your first public learning
|
|
179
|
+
waits for operator review. A draft that any screen flags (sensitive,
|
|
180
|
+
duplicate, uncertain quality) waits in your private queue for
|
|
181
|
+
\`auxilo review\`. Auto-publish for learnings that pass every screen is
|
|
182
|
+
off unless you turn it on in your dashboard. Your share of a paid
|
|
183
|
+
unlock by another agent goes to your Auxilo account, 70% of what they
|
|
184
|
+
paid on a direct unlock and 60% via discovery. A repeat unlock by the
|
|
185
|
+
same buyer within 30 days earns nothing. Earnings depend on whether
|
|
186
|
+
other agents unlock your learnings and are not guaranteed. Earnings
|
|
187
|
+
accrue now. Withdrawals open soon, and auxilo.io/status shows where
|
|
188
|
+
things stand.
|
|
134
189
|
You can stop any time with \`auxilo disable\` (local kill-switch) and review
|
|
135
|
-
every run in ~/.auxilo/extract.log. Saying No installs the MCP server only
|
|
136
|
-
|
|
190
|
+
every run in ~/.auxilo/extract.log. Saying No installs the MCP server only.
|
|
191
|
+
No session-end capture hook is written into any client config unless you say
|
|
137
192
|
Yes, and any capture hooks left by an earlier install are removed.
|
|
138
193
|
`;
|
|
139
194
|
|
|
@@ -497,6 +552,16 @@ async function cmdStatus() {
|
|
|
497
552
|
const line = runnerSkewLine(installer.runnerVersionSkew(HOME));
|
|
498
553
|
if (line) console.log(line);
|
|
499
554
|
}
|
|
555
|
+
console.log(extractionProviderLine(await providers.resolveProvider({})));
|
|
556
|
+
// Lazy require: scripts/runner.js is a heavier module (sources, sensitivity
|
|
557
|
+
// filter, ops-alert) than this one status line needs at require-time for
|
|
558
|
+
// every CLI invocation.
|
|
559
|
+
let skipState = null;
|
|
560
|
+
try {
|
|
561
|
+
skipState = require('../scripts/runner.js').loadExtractionSkipState();
|
|
562
|
+
} catch { /* status must never throw on a missing/corrupt skip-state file */ }
|
|
563
|
+
const skipLine = extractionSkipReasonLine(skipState);
|
|
564
|
+
if (skipLine) console.log(skipLine);
|
|
500
565
|
console.log(`SessionEnd hook: ${s.hookInstalled ? 'installed' : 'not installed'}${s.hookRegistered ? ', registered in Claude Code settings' : ''}`);
|
|
501
566
|
for (const c of s.clients.filter((c) => c.captureHook)) {
|
|
502
567
|
console.log(`Capture hooks: ${c.name} (${c.captureEvent}, ${c.captureRegistered ? 'registered' : 'not registered'})`);
|
|
@@ -516,6 +581,65 @@ function runnerSkewLine(skew) {
|
|
|
516
581
|
return ` ⚠ Installed runner is ${installed} (package v${skew.package}) — run: npx auxilo setup`;
|
|
517
582
|
}
|
|
518
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Mirrors lib/clean-lane.js's CLEAN_LANE_CALIBRATED_PROVIDERS — that module
|
|
586
|
+
* is server-side and not in the published package's files[] (same reason
|
|
587
|
+
* CLEAN_LANE_AFFIRMATION below is a literal mirror, not an import; see
|
|
588
|
+
* test/clean-lane-phase-a.test.js's "the CLI must not require the unshipped
|
|
589
|
+
* server module" pin). test/clean-lane-calibration.test.js pins the two
|
|
590
|
+
* arrays equal.
|
|
591
|
+
*/
|
|
592
|
+
const CLI_CLEAN_LANE_CALIBRATED_PROVIDERS = ['claude-code'];
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* EXTRACT-PER-CLIENT W1 PART A/C — one unconditional line naming which
|
|
596
|
+
* extraction model provider resolves, why (env override vs auto-detected),
|
|
597
|
+
* and (PART C) whether that provider's submissions can reach the clean-lane
|
|
598
|
+
* auto-publish path at all (server-side gate: lib/clean-lane.js's
|
|
599
|
+
* CLEAN_LANE_CALIBRATED_PROVIDERS, mirrored above).
|
|
600
|
+
*/
|
|
601
|
+
function extractionProviderLine(resolution) {
|
|
602
|
+
if (resolution && resolution.ok) {
|
|
603
|
+
const via = process.env.AUXILO_EXTRACTION_PROVIDER
|
|
604
|
+
? 'env override AUXILO_EXTRACTION_PROVIDER'
|
|
605
|
+
: 'auto-detected';
|
|
606
|
+
const calibration = CLI_CLEAN_LANE_CALIBRATED_PROVIDERS.includes(resolution.id)
|
|
607
|
+
? 'clean-lane calibrated'
|
|
608
|
+
: 'review-lane only';
|
|
609
|
+
return `Extraction model provider: ${resolution.id} (${via}, ${calibration})`;
|
|
610
|
+
}
|
|
611
|
+
const reason = (resolution && resolution.reason) || 'no provider available';
|
|
612
|
+
return `Extraction model provider: none (${reason})`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* EXTRACT-PER-CLIENT W1 PART C — the companion conditional line PART A left
|
|
617
|
+
* unimplemented (see its report): printed ONLY when the last recorded
|
|
618
|
+
* extraction skip reasonCode (runner.js's normalizeExtractionSkipState,
|
|
619
|
+
* last_reason_code field, added in this part) is one of the names below;
|
|
620
|
+
* null (nothing printed) for every other state, including "no state file
|
|
621
|
+
* yet" and "last outcome was a real success."
|
|
622
|
+
*
|
|
623
|
+
* 'no-usable-provider' added in the W1 P1 fix (PUNCH-LIST): distinct from
|
|
624
|
+
* 'no-model-provider-available' (nothing even LOOKED usable at the detect()
|
|
625
|
+
* stage) — this is the selection-fall-through exhaustion code from
|
|
626
|
+
* scripts/providers/index.js's runModel(), where every provider in
|
|
627
|
+
* PROVIDER_ORDER was actually tried and each failed for its own reason.
|
|
628
|
+
*/
|
|
629
|
+
const STATUS_WORTHY_SKIP_REASON_CODES = Object.freeze([
|
|
630
|
+
'cli-billing-helper-configured',
|
|
631
|
+
'cli-unauthenticated',
|
|
632
|
+
'no-model-provider-available',
|
|
633
|
+
'no-usable-provider',
|
|
634
|
+
]);
|
|
635
|
+
|
|
636
|
+
/** Pure render, mirroring runnerSkewLine(skew) above — the caller loads the
|
|
637
|
+
* state; this only decides whether/what to print. */
|
|
638
|
+
function extractionSkipReasonLine(state) {
|
|
639
|
+
if (!state || !STATUS_WORTHY_SKIP_REASON_CODES.includes(state.last_reason_code)) return null;
|
|
640
|
+
return ` ⚠ Last extraction attempt: ${state.last_reason_code}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
519
643
|
// ─── auxilo disable ─────────────────────────────────────────────────────────
|
|
520
644
|
|
|
521
645
|
async function cmdDisable(flags) {
|
|
@@ -893,6 +1017,23 @@ async function cmdReview(flags) {
|
|
|
893
1017
|
// byte-equal. The consent VERSION is never a client literal — it always comes
|
|
894
1018
|
// from GET /account/clean-lane (consent_version_current).
|
|
895
1019
|
const CLEAN_LANE_AFFIRMATION = 'I understand and choose auto-publish for qualifying extracted learnings.';
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* EXTRACT-PER-CLIENT W1 PART C — `auxilo provider set`'s consent sentence.
|
|
1023
|
+
* SITE-PM-authored, verbatim. States: this is the builder's OWN key; where
|
|
1024
|
+
* it lives on disk and at what permission (~/.auxilo/providers.json, owner-
|
|
1025
|
+
* read-only); that Auxilo never receives it; what it is used for (drafting
|
|
1026
|
+
* learnings from the builder's own scrubbed sessions); that drafting sends
|
|
1027
|
+
* sessions to the builder's chosen provider under the builder's own account,
|
|
1028
|
+
* billed to that account and never to Auxilo; and how to remove it
|
|
1029
|
+
* (`auxilo provider clear`). `cmdProvider('set')` refuses to run at all
|
|
1030
|
+
* (reasonCode 'consent-sentence-missing') were this ever empty again — see
|
|
1031
|
+
* the test asserting that refusal — and, before ever printing this sentence
|
|
1032
|
+
* or storing anything, verifies any existing providers.json is actually
|
|
1033
|
+
* owner-read-only (reasonCode 'providers-file-mode-unsafe' if not), so the
|
|
1034
|
+
* "readable only by your user account" claim below is never printed false.
|
|
1035
|
+
*/
|
|
1036
|
+
const PROVIDER_KEY_CONSENT_SENTENCE = 'This key is yours. It stays on this machine in ~/.auxilo/providers.json, readable only by your user account, and Auxilo never receives it. It is used for one thing, drafting learnings from your own scrubbed sessions. Drafting sends those sessions to that provider under your own account, and any use is charged to that account, never to Auxilo. Run auxilo provider clear to remove it.';
|
|
896
1037
|
const CLEAN_LANE_UNAVAILABLE = 'Auto-publish for clean learnings is not yet available on this account.';
|
|
897
1038
|
// CLEAN-LANE-FLIP Phase B (legal; DRAFT pending Tyler): the full text of ToS
|
|
898
1039
|
// §5.9.3(g) (plus its ratchet paragraph) prints ABOVE the affirmation prompt —
|
|
@@ -1107,6 +1248,182 @@ async function cmdCleanLane(flags) {
|
|
|
1107
1248
|
console.log(' Turn it off any time: npx auxilo clean-lane revoke');
|
|
1108
1249
|
}
|
|
1109
1250
|
|
|
1251
|
+
// ─── auxilo provider (EXTRACT-PER-CLIENT W1 PART C: BYO provider key) ───────
|
|
1252
|
+
//
|
|
1253
|
+
// Mirrors cmdCleanLane's grant discipline exactly: `set` runs ONLY on a TTY,
|
|
1254
|
+
// refuses piped input, and requires typing the consent sentence verbatim —
|
|
1255
|
+
// no confirmation flag, no bypass. Unlike clean-lane, nothing here ever reaches
|
|
1256
|
+
// the Auxilo server: the key is the builder's own, read from a hidden
|
|
1257
|
+
// prompt (never argv, never env), and written straight to
|
|
1258
|
+
// ~/.auxilo/providers.json (0600) via scripts/providers/byo-key.js.
|
|
1259
|
+
//
|
|
1260
|
+
// PROVIDER_KEY_CONSENT_SENTENCE is a SITE-PM string slot (see the module
|
|
1261
|
+
// comment on its declaration below) — `set` refuses to run at all while it
|
|
1262
|
+
// is empty, with reasonCode 'consent-sentence-missing'. This keeps the path
|
|
1263
|
+
// disabled end-to-end until that copy is written, rather than shipping a
|
|
1264
|
+
// silent placeholder sentence nobody actually reviewed.
|
|
1265
|
+
const PROVIDER_VENDORS = ['openai', 'anthropic', 'gemini'];
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* PROVIDER_KEY_CONSENT_SENTENCE promises the stored key is "readable only
|
|
1269
|
+
* by your user account." Before `set` ever prints that sentence, or stores
|
|
1270
|
+
* anything, verify the promise is actually true of any providers.json
|
|
1271
|
+
* already on disk. Owner-read-only means no group/other bits at all
|
|
1272
|
+
* (mode & 0o077 === 0); a file that fails this predates this discipline
|
|
1273
|
+
* (e.g. survived an umask that widened it) and must be fixed or removed
|
|
1274
|
+
* before `set` is allowed to run — never fail open on a false claim.
|
|
1275
|
+
* No file yet is not unsafe: writeByoConfig always writes 0600 itself.
|
|
1276
|
+
*/
|
|
1277
|
+
/**
|
|
1278
|
+
* (EXTRACT-PER-CLIENT W1 FIX, GOV-3 should-fix item 10): a stat error other
|
|
1279
|
+
* than ENOENT (e.g. EACCES) now fails CLOSED — returns true (unsafe) — the
|
|
1280
|
+
* same "cannot verify, so don't trust it" discipline
|
|
1281
|
+
* byo-key.js's own isProvidersFileModeUnsafe documents and follows. The old
|
|
1282
|
+
* rethrow here let a raw fs error propagate out of cmdProvider uncaught,
|
|
1283
|
+
* printing whatever bubbled to run()'s generic catch instead of a clean
|
|
1284
|
+
* reason. Never throws now.
|
|
1285
|
+
*/
|
|
1286
|
+
function providersFileModeUnsafe(target) {
|
|
1287
|
+
let stat;
|
|
1288
|
+
try {
|
|
1289
|
+
stat = fs.statSync(target);
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
if (err && err.code === 'ENOENT') return false;
|
|
1292
|
+
return true; // cannot verify permissions — fail closed, not open
|
|
1293
|
+
}
|
|
1294
|
+
return (stat.mode & 0o077) !== 0;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
async function cmdProvider(flags) {
|
|
1298
|
+
const sub = process.argv[3];
|
|
1299
|
+
if (!['status', 'set', 'clear'].includes(sub)) {
|
|
1300
|
+
if (sub) console.error(`Unknown provider subcommand: ${sub}`);
|
|
1301
|
+
usage('provider');
|
|
1302
|
+
process.exit(sub ? 1 : 0);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (sub === 'status') {
|
|
1306
|
+
const config = byoKeyProvider.readByoConfig();
|
|
1307
|
+
if (!config) {
|
|
1308
|
+
console.log('BYO provider key: none configured');
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
console.log(`BYO provider key: configured (vendor: ${config.provider}, model: ${config.model}, key: present)`);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (sub === 'clear') {
|
|
1316
|
+
// clearProvidersFile() never throws (GOV-3 should-fix item 10) — every
|
|
1317
|
+
// outcome is a plain string, handled explicitly below rather than
|
|
1318
|
+
// falling into the generic success message for a case that isn't one.
|
|
1319
|
+
const result = byoKeyProvider.clearProvidersFile();
|
|
1320
|
+
if (result === 'removed-file') {
|
|
1321
|
+
console.log('✓ ~/.auxilo/providers.json removed (nothing left to keep).');
|
|
1322
|
+
} else if (result === 'removed-byo') {
|
|
1323
|
+
console.log('✓ BYO provider key cleared. providers.json kept (your auto-detected provider selection, if any, is unchanged).');
|
|
1324
|
+
} else if (result === 'unreadable') {
|
|
1325
|
+
console.error('auxilo provider clear could not read ~/.auxilo/providers.json (reasonCode: providers-file-unreadable). Check its permissions and try again.');
|
|
1326
|
+
process.exit(1);
|
|
1327
|
+
} else if (result === 'unresolved') {
|
|
1328
|
+
console.error('auxilo provider clear could not resolve your home directory (reasonCode: provider-home-unresolved).');
|
|
1329
|
+
process.exit(1);
|
|
1330
|
+
} else {
|
|
1331
|
+
console.log('✓ Nothing to remove (no BYO provider key configured).');
|
|
1332
|
+
}
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// sub === 'set' below.
|
|
1337
|
+
if (PROVIDER_KEY_CONSENT_SENTENCE === '') {
|
|
1338
|
+
console.error('auxilo provider set is not available yet: the operator has not configured the consent sentence for this build (reasonCode: consent-sentence-missing).');
|
|
1339
|
+
process.exit(1);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Fail closed BEFORE printing the sentence or storing anything: an
|
|
1343
|
+
// existing providers.json that is not owner-read-only would make the
|
|
1344
|
+
// sentence's "readable only by your user account" claim false.
|
|
1345
|
+
if (providersFileModeUnsafe(byoKeyProvider.DEFAULT_PROVIDERS_STATE_PATH)) {
|
|
1346
|
+
console.error('auxilo provider set refuses to continue: ~/.auxilo/providers.json exists and is not owner-read-only, so this build cannot truthfully make the consent promise (reasonCode: providers-file-mode-unsafe). Fix its permissions (chmod 600 ~/.auxilo/providers.json) or remove the file, then try again.');
|
|
1347
|
+
process.exit(1);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// The TTY gate runs BEFORE any prompt: a piped or scripted stdin can never
|
|
1351
|
+
// reach the hidden key prompt or the typed affirmation.
|
|
1352
|
+
if (!process.stdin.isTTY) {
|
|
1353
|
+
console.error('auxilo provider set must be run by a person in an interactive terminal. It does not accept piped input, and there is no flag that skips typing the consent sentence or the key.');
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
let vendor = String(flags.vendor || '').toLowerCase();
|
|
1358
|
+
while (!PROVIDER_VENDORS.includes(vendor)) {
|
|
1359
|
+
vendor = (await ask(`Vendor [${PROVIDER_VENDORS.join('|')}]: `)).toLowerCase();
|
|
1360
|
+
if (readlineEnded) { console.log('Aborted. Nothing changed.'); return; }
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// GOV-3 item 3: base_url must be https:// — a plaintext endpoint would
|
|
1364
|
+
// send the transcript, and for two of the three vendors the key itself,
|
|
1365
|
+
// in cleartext. Checked here (set time) AND again at read time inside
|
|
1366
|
+
// byo-key.js's runModel/detect (reasonCode provider-base-url-insecure) —
|
|
1367
|
+
// a hand-edited providers.json can't bypass either.
|
|
1368
|
+
let baseUrl = flags['base-url'] ? String(flags['base-url']) : '';
|
|
1369
|
+
if (baseUrl && byoKeyProvider.isBaseUrlInsecure(baseUrl)) {
|
|
1370
|
+
console.error(`auxilo provider set refuses --base-url "${baseUrl}": it must be https:// (reasonCode: provider-base-url-insecure).`);
|
|
1371
|
+
process.exit(1);
|
|
1372
|
+
}
|
|
1373
|
+
if (!flags['base-url']) {
|
|
1374
|
+
for (;;) {
|
|
1375
|
+
baseUrl = await ask(`Base URL (optional — press Enter for the ${vendor} default; must be https:// if given): `);
|
|
1376
|
+
if (readlineEnded) { console.log('Aborted. Nothing changed.'); return; }
|
|
1377
|
+
if (!baseUrl || !byoKeyProvider.isBaseUrlInsecure(baseUrl)) break;
|
|
1378
|
+
console.log(`"${baseUrl}" is not https:// — try again, or press Enter for the ${vendor} default.`);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
let model = flags.model ? String(flags.model) : '';
|
|
1383
|
+
while (!model) {
|
|
1384
|
+
model = await ask('Model (e.g. gpt-4o-mini, claude-sonnet-4-5, gemini-2.5-flash): ');
|
|
1385
|
+
if (readlineEnded) { console.log('Aborted. Nothing changed.'); return; }
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Printed in FULL, word-wrapped to the terminal width — never truncated —
|
|
1389
|
+
// before the (hidden) key prompt further below.
|
|
1390
|
+
console.log(`\n${wrapForTerminal(PROVIDER_KEY_CONSENT_SENTENCE)}\n`);
|
|
1391
|
+
console.log('To continue, type this sentence exactly as written, then press Enter:');
|
|
1392
|
+
console.log(`\n${wrapForTerminal(PROVIDER_KEY_CONSENT_SENTENCE)}\n`);
|
|
1393
|
+
const typed = await ask('> ');
|
|
1394
|
+
if (typed !== PROVIDER_KEY_CONSENT_SENTENCE) {
|
|
1395
|
+
console.log('The sentence did not match. Aborted. Nothing changed.');
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const apiKey = await askHidden('API key (input hidden): ');
|
|
1400
|
+
if (readlineEnded || !apiKey) {
|
|
1401
|
+
console.log('Aborted. Nothing changed.');
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// writeByoConfig throws ONLY on an unresolved home directory (GOV-3 item
|
|
1406
|
+
// 13) — caught here so that reaches a clean reason + exit(1), never a raw
|
|
1407
|
+
// stack (should-fix item 10), matching the fail-closed contract every
|
|
1408
|
+
// other providers.json entry point in this file now follows.
|
|
1409
|
+
let written;
|
|
1410
|
+
try {
|
|
1411
|
+
written = byoKeyProvider.writeByoConfig({
|
|
1412
|
+
provider: vendor,
|
|
1413
|
+
...(baseUrl && { base_url: baseUrl }),
|
|
1414
|
+
model,
|
|
1415
|
+
api_key: apiKey,
|
|
1416
|
+
});
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
if (err && err.reasonCode === 'provider-home-unresolved') {
|
|
1419
|
+
console.error(`auxilo provider set could not resolve your home directory (reasonCode: provider-home-unresolved). ${err.message}`);
|
|
1420
|
+
process.exit(1);
|
|
1421
|
+
}
|
|
1422
|
+
throw err;
|
|
1423
|
+
}
|
|
1424
|
+
console.log(`\n✓ Saved to ${written} (mode 0600). This machine will use your own ${vendor} key for extraction when no earlier provider in the order is available.`);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1110
1427
|
// ─── Entry point ────────────────────────────────────────────────────────────
|
|
1111
1428
|
|
|
1112
1429
|
function usage(command) {
|
|
@@ -1162,6 +1479,19 @@ available on your account every subcommand says so and changes nothing.
|
|
|
1162
1479
|
this for you.
|
|
1163
1480
|
revoke Turn it off (one step, no confirmation). Already-published
|
|
1164
1481
|
learnings keep their 7-day retraction window.`,
|
|
1482
|
+
provider: `Usage: auxilo provider <status|set|clear>
|
|
1483
|
+
|
|
1484
|
+
Configure a bring-your-own (BYO) model provider key for local extraction —
|
|
1485
|
+
used only when no earlier provider in the fixed order (claude-code,
|
|
1486
|
+
codex-cli) is available. Auxilo never sees or bills this key.
|
|
1487
|
+
|
|
1488
|
+
status Show the configured vendor and model (never the key itself).
|
|
1489
|
+
set Configure a vendor, model, and key. Interactive ONLY: the key is
|
|
1490
|
+
read from a hidden prompt (never a flag, never an env var), and
|
|
1491
|
+
you must type the consent sentence exactly. No flag skips this.
|
|
1492
|
+
clear Remove your BYO key. Keeps your auto-detected provider selection
|
|
1493
|
+
(\`selected\`), if any — only deletes the file outright when
|
|
1494
|
+
nothing but the key was in it.`,
|
|
1165
1495
|
};
|
|
1166
1496
|
if (command && blocks[command]) {
|
|
1167
1497
|
console.log(`\n${blocks[command]}\n`);
|
|
@@ -1213,6 +1543,10 @@ Commands:
|
|
|
1213
1543
|
clean-lane <status|grant|revoke>
|
|
1214
1544
|
Auto-publish clean learnings (standing consent). grant is
|
|
1215
1545
|
interactive only: you type the consent sentence yourself.
|
|
1546
|
+
provider <status|set|clear>
|
|
1547
|
+
Configure a bring-your-own model provider key for local
|
|
1548
|
+
extraction. set is interactive only: hidden key prompt, typed
|
|
1549
|
+
consent sentence.
|
|
1216
1550
|
|
|
1217
1551
|
Docs: https://auxilo.io · API: ${installer.DEFAULT_BASE_URL}
|
|
1218
1552
|
`);
|
|
@@ -1221,7 +1555,7 @@ Docs: https://auxilo.io · API: ${installer.DEFAULT_BASE_URL}
|
|
|
1221
1555
|
async function main() {
|
|
1222
1556
|
const cmd = process.argv[2];
|
|
1223
1557
|
const subcommandHelp = ['help', '--help', '-h'].includes(process.argv[3]);
|
|
1224
|
-
if (['setup', 'init', 'status', 'review', 'disable', 'clean-lane'].includes(cmd) && subcommandHelp) {
|
|
1558
|
+
if (['setup', 'init', 'status', 'review', 'disable', 'clean-lane', 'provider'].includes(cmd) && subcommandHelp) {
|
|
1225
1559
|
return usage(cmd);
|
|
1226
1560
|
}
|
|
1227
1561
|
const flags = parseFlags(process.argv);
|
|
@@ -1232,6 +1566,7 @@ async function main() {
|
|
|
1232
1566
|
case 'review': return cmdReview(flags);
|
|
1233
1567
|
case 'disable': return cmdDisable(flags);
|
|
1234
1568
|
case 'clean-lane': return cmdCleanLane(flags);
|
|
1569
|
+
case 'provider': return cmdProvider(flags);
|
|
1235
1570
|
case 'help': case '--help': case '-h': case undefined: return usage();
|
|
1236
1571
|
default:
|
|
1237
1572
|
console.error(`Unknown command: ${cmd}`);
|
|
@@ -1260,6 +1595,7 @@ if (require.main === module) {
|
|
|
1260
1595
|
module.exports = {
|
|
1261
1596
|
parseFlags,
|
|
1262
1597
|
runnerSkewLine,
|
|
1598
|
+
extractionProviderLine,
|
|
1263
1599
|
resolveBaseUrl,
|
|
1264
1600
|
shortFlags,
|
|
1265
1601
|
groupSummaryRows,
|
|
@@ -1272,4 +1608,10 @@ module.exports = {
|
|
|
1272
1608
|
CLEAN_LANE_TERMS_G2,
|
|
1273
1609
|
CLEAN_LANE_NO_EMAIL_LINE,
|
|
1274
1610
|
wrapForTerminal,
|
|
1611
|
+
PROVIDER_KEY_CONSENT_SENTENCE,
|
|
1612
|
+
CLI_CLEAN_LANE_CALIBRATED_PROVIDERS,
|
|
1613
|
+
STATUS_WORTHY_SKIP_REASON_CODES,
|
|
1614
|
+
extractionSkipReasonLine,
|
|
1615
|
+
cmdProvider,
|
|
1616
|
+
providersFileModeUnsafe,
|
|
1275
1617
|
};
|
package/lib/installer.js
CHANGED
|
@@ -55,6 +55,44 @@ function sourceAdapterRows(packageRoot = PACKAGE_ROOT) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// EXTRACT-PER-CLIENT W1 PART A: extract-local.js now requires
|
|
59
|
+
// './providers/index.js', which requires './claude-code.js' (and will require
|
|
60
|
+
// './codex-cli.js' / './byo-key.js' once PART B/C land) — every file under
|
|
61
|
+
// scripts/providers/ must ship the same way scripts/sources/ does, or an
|
|
62
|
+
// npm-installed user's first extraction is a MODULE_NOT_FOUND
|
|
63
|
+
// (test/runner-packaging-closure.test.js guards this).
|
|
64
|
+
function providerAdapterRows(packageRoot = PACKAGE_ROOT) {
|
|
65
|
+
try {
|
|
66
|
+
return fs.readdirSync(path.join(packageRoot, 'scripts', 'providers'))
|
|
67
|
+
.filter((f) => f.endsWith('.js'))
|
|
68
|
+
.sort()
|
|
69
|
+
.map((f) => [`scripts/providers/${f}`, `scripts/providers/${f}`, 0o644]);
|
|
70
|
+
} catch {
|
|
71
|
+
return []; // missing dir surfaces as installRunner missing-file errors
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// EXTRACT-PER-CLIENT W1 PART B: codex-cli.js reads its two JSON-Schema hint
|
|
76
|
+
// files at runtime via a path relative to itself (scripts/providers/schemas/
|
|
77
|
+
// *.schema.json), NOT via require() — providerAdapterRows() above only
|
|
78
|
+
// enumerates *.js in scripts/providers/ itself (non-recursive), so the
|
|
79
|
+
// schemas/ subdirectory would silently NOT reach an npm-installed user's
|
|
80
|
+
// ~/.auxilo/bin without its own enumeration here. installRunner copies files
|
|
81
|
+
// one at a time (no directory copy), so every schema file needs its own row,
|
|
82
|
+
// same as providerAdapterRows()'s .js rows — this is the "check whether it
|
|
83
|
+
// enumerates the directory or a list" case turning out to be "neither yet;
|
|
84
|
+
// add the enumeration."
|
|
85
|
+
function providerSchemaRows(packageRoot = PACKAGE_ROOT) {
|
|
86
|
+
try {
|
|
87
|
+
return fs.readdirSync(path.join(packageRoot, 'scripts', 'providers', 'schemas'))
|
|
88
|
+
.filter((f) => f.endsWith('.json'))
|
|
89
|
+
.sort()
|
|
90
|
+
.map((f) => [`scripts/providers/schemas/${f}`, `scripts/providers/schemas/${f}`, 0o644]);
|
|
91
|
+
} catch {
|
|
92
|
+
return []; // missing dir surfaces as installRunner missing-file errors
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
58
96
|
/**
|
|
59
97
|
* Runner stack shipped in the npm tarball and copied into <home>/.auxilo/bin,
|
|
60
98
|
* preserving relative layout so runner.js's requires resolve
|
|
@@ -73,6 +111,8 @@ const RUNNER_STACK = Object.freeze([
|
|
|
73
111
|
// LW-18 layer 1b: SessionStart held-count notice (shim target).
|
|
74
112
|
['scripts/review-notice.js', 'scripts/review-notice.js', 0o755],
|
|
75
113
|
...sourceAdapterRows(),
|
|
114
|
+
...providerAdapterRows(),
|
|
115
|
+
...providerSchemaRows(),
|
|
76
116
|
['lib/sensitivity-filter.js', 'lib/sensitivity-filter.js', 0o644],
|
|
77
117
|
['lib/extraction-index.js', 'lib/extraction-index.js', 0o644],
|
|
78
118
|
['lib/similarity.js', 'lib/similarity.js', 0o644],
|