auxilo-mcp 0.9.11 → 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 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 agent's own learnings always come back at $0. When another agent unlocks what yours figured out, you earn.
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 marketplace tool; extraction stays off until you opt in.
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 to $50, set per learning; your own learnings $0 |
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` | Marketplace statistics | Free |
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: your agent's own learnings come back free, in any later session.
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 marketplace numbers: [auxilo.io/knowledge/stats](https://auxilo.io/knowledge/stats).
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 using your own claude CLI (your
123
- existing subscription). For this step your transcript is processed
124
- only by your own model provider the same way your normal sessions
125
- are, and is never sent to Auxilo, raw or scrubbed.
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'}). A draft
128
- that passes every screen publishes to the marketplace immediately
129
- under your account, and you can retract it for 7 days. A draft that
130
- any screen flags (sensitive, duplicate, uncertain quality) waits in
131
- your private queue for \`auxilo review\`. Manual mode (approve first,
132
- for everything) is available in your account settings. You earn 70%
133
- of sales.
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
- no session-end capture hook is written into any client config unless you say
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
 
@@ -493,6 +548,20 @@ async function cmdStatus() {
493
548
  console.log(`Account mode: ${s.accountMode}`);
494
549
  console.log(`Kill-switch sentinel: ${s.sentinel ? 'present (extraction enabled)' : 'absent (extraction disabled)'}`);
495
550
  console.log(`Runner installed: ${s.runnerInstalled ? 'yes (~/.auxilo/bin)' : 'no'}`);
551
+ if (s.runnerInstalled) {
552
+ const line = runnerSkewLine(installer.runnerVersionSkew(HOME));
553
+ if (line) console.log(line);
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);
496
565
  console.log(`SessionEnd hook: ${s.hookInstalled ? 'installed' : 'not installed'}${s.hookRegistered ? ', registered in Claude Code settings' : ''}`);
497
566
  for (const c of s.clients.filter((c) => c.captureHook)) {
498
567
  console.log(`Capture hooks: ${c.name} (${c.captureEvent}, ${c.captureRegistered ? 'registered' : 'not registered'})`);
@@ -501,6 +570,76 @@ async function cmdStatus() {
501
570
  console.log(`Pending upload queue: ${s.pendingCount} file(s)\n`);
502
571
  }
503
572
 
573
+ /**
574
+ * CLEAN-LANE-FLIP Phase B: ONE line when ~/.auxilo/bin/VERSION is missing or
575
+ * differs from this CLI's package version; null when the stack is current.
576
+ * `setup` is idempotent and re-copies the stack, so that is the remedy.
577
+ */
578
+ function runnerSkewLine(skew) {
579
+ if (!skew || !skew.skew) return null;
580
+ const installed = skew.installed ? `v${skew.installed}` : 'unstamped (pre-0.9.12)';
581
+ return ` ⚠ Installed runner is ${installed} (package v${skew.package}) — run: npx auxilo setup`;
582
+ }
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
+
504
643
  // ─── auxilo disable ─────────────────────────────────────────────────────────
505
644
 
506
645
  async function cmdDisable(flags) {
@@ -878,7 +1017,38 @@ async function cmdReview(flags) {
878
1017
  // byte-equal. The consent VERSION is never a client literal — it always comes
879
1018
  // from GET /account/clean-lane (consent_version_current).
880
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.';
881
1037
  const CLEAN_LANE_UNAVAILABLE = 'Auto-publish for clean learnings is not yet available on this account.';
1038
+ // CLEAN-LANE-FLIP Phase B (legal; DRAFT pending Tyler): the full text of ToS
1039
+ // §5.9.3(g) (plus its ratchet paragraph) prints ABOVE the affirmation prompt —
1040
+ // counsel condition: the enrollment surface must show what "qualifying",
1041
+ // revocation and the 7-day retraction mean, on both the dashboard and CLI
1042
+ // paths. Same package-boundary reason as the affirmation: these literals are
1043
+ // pinned byte-equal to docs/TERMS-OF-SERVICE.md and public/dashboard.html by
1044
+ // test/clean-lane-phase-b-legal.test.js. Edit the Terms first, then mirror.
1045
+ const CLEAN_LANE_TERMS_G = '(g) Standing publication consent (optional). Standing publication consent is off by default. A Builder may turn it on by an affirmative act — a dashboard setting, or a terminal command that requires typing the affirmation sentence shown on that screen. Auxilo records that act, the affirmation, and the consent-text version in a durable, hash-chained consent log, retained for the life of the account plus three (3) years under subsection (b). While it is on, a Learning submitted through Autonomous Extraction is published without separate per-item approval only if it passes every Platform screen and the quality threshold the Builder chose at activation. An account\'s first public Learning is never published this way; it is held for operator review under Section 4.1. Auxilo records each such publication in the Builder\'s dashboard and returns a notice in the response to the submission that produced it; each is retractable for seven (7) days under Section 5.9.4. If more than five percent (5%) of a Builder\'s Learnings published this way in any thirty (30) day period are retracted, Auxilo freezes the feature for that account until the Builder turns it on again. A Builder may turn it off at any time, effective immediately for later submissions; doing so does not affect Learnings already published. Subsection (c) applies in full to every Learning so published.';
1046
+ const CLEAN_LANE_TERMS_G2 = 'The quality threshold in effect for a Builder is the one that Builder selected, and Auxilo will not broaden the conditions under which a Learning qualifies for publication under this subsection without recording a new consent; Auxilo may make those conditions stricter at any time.';
1047
+ // CLEAN-LANE-FLIP Phase B (notice hardening): the no-email enrollment line —
1048
+ // GOV-2 counsel draft §6 read #2 "move 3" — printed verbatim before the
1049
+ // affirmation prompt on every enrollment surface. Byte-equal to the dashboard's
1050
+ // #clean-lane-no-email-line (test/clean-lane-phase-b-notice.test.js).
1051
+ const CLEAN_LANE_NO_EMAIL_LINE = 'You will not receive an email for these. Publications appear in your dashboard and in the response to the session that submitted them. The 7-day retraction window runs from publication.';
882
1052
  const CLEAN_LANE_MIN_QUALITY_MIN = 14;
883
1053
  const CLEAN_LANE_MIN_QUALITY_MAX = 20;
884
1054
  const CLEAN_LANE_MIN_QUALITY_DEFAULT = 16;
@@ -897,6 +1067,18 @@ and anything after an auto-freeze. Every auto-published learning can be
897
1067
  retracted for 7 days (\`npx auxilo review\` or your dashboard).
898
1068
  `;
899
1069
 
1070
+ /** Word-wrap a single paragraph at `width` columns (whitespace only; no word is altered). */
1071
+ function wrapForTerminal(paragraph, width = 78) {
1072
+ const lines = [];
1073
+ let line = '';
1074
+ for (const word of paragraph.split(' ')) {
1075
+ if (line && (line.length + 1 + word.length) > width) { lines.push(line); line = word; }
1076
+ else line = line ? `${line} ${word}` : word;
1077
+ }
1078
+ if (line) lines.push(line);
1079
+ return lines.map((l) => ` ${l}`).join('\n');
1080
+ }
1081
+
900
1082
  async function cleanLaneRequest({ apiKey, baseUrl, method, route, body }) {
901
1083
  const url = `${String(baseUrl).replace(/\/+$/, '')}${route}`;
902
1084
  const headers = { 'X-API-Key': apiKey };
@@ -927,6 +1109,12 @@ function printCleanLaneStatus(data) {
927
1109
  console.log(` a grant exists under consent version ${data.consent_version_recorded} but the current version is ${data.consent_version_current}; re-grant to re-activate.`);
928
1110
  }
929
1111
  console.log(` current consent version: ${data.consent_version_current}`);
1112
+ // CLEAN-LANE-FLIP Phase B (notice hardening): the unread count, printed only
1113
+ // when > 0. Nothing here acknowledges it — only the dashboard button does.
1114
+ const unread = data.unacknowledged_publications;
1115
+ if (Number.isInteger(unread) && unread > 0) {
1116
+ console.log(` auto-published since you last checked: ${unread} (review and acknowledge them in your dashboard)`);
1117
+ }
930
1118
  }
931
1119
 
932
1120
  async function cmdCleanLane(flags) {
@@ -1011,6 +1199,14 @@ async function cmdCleanLane(flags) {
1011
1199
  console.log(`Enter a whole number from ${CLEAN_LANE_MIN_QUALITY_MIN} to ${CLEAN_LANE_MIN_QUALITY_MAX}.`);
1012
1200
  }
1013
1201
 
1202
+ // The consent text itself, verbatim (word-wrapped for the terminal only), before the sentence.
1203
+ console.log('\nTerms of Service, Section 5.9.3(g): the consent you are giving\n');
1204
+ console.log(wrapForTerminal(CLEAN_LANE_TERMS_G));
1205
+ console.log('');
1206
+ console.log(wrapForTerminal(CLEAN_LANE_TERMS_G2));
1207
+ console.log(`\nFull Terms: ${baseUrl}/terms`);
1208
+ // The no-email line, verbatim, directly before the affirmation prompt.
1209
+ console.log(`\n${wrapForTerminal(CLEAN_LANE_NO_EMAIL_LINE)}`);
1014
1210
  console.log('\nTo turn on auto-publish, type this sentence exactly as written, then press Enter:');
1015
1211
  console.log(`\n ${CLEAN_LANE_AFFIRMATION}\n`);
1016
1212
  const typed = await ask('> ');
@@ -1052,6 +1248,182 @@ async function cmdCleanLane(flags) {
1052
1248
  console.log(' Turn it off any time: npx auxilo clean-lane revoke');
1053
1249
  }
1054
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
+
1055
1427
  // ─── Entry point ────────────────────────────────────────────────────────────
1056
1428
 
1057
1429
  function usage(command) {
@@ -1107,6 +1479,19 @@ available on your account every subcommand says so and changes nothing.
1107
1479
  this for you.
1108
1480
  revoke Turn it off (one step, no confirmation). Already-published
1109
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.`,
1110
1495
  };
1111
1496
  if (command && blocks[command]) {
1112
1497
  console.log(`\n${blocks[command]}\n`);
@@ -1158,6 +1543,10 @@ Commands:
1158
1543
  clean-lane <status|grant|revoke>
1159
1544
  Auto-publish clean learnings (standing consent). grant is
1160
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.
1161
1550
 
1162
1551
  Docs: https://auxilo.io · API: ${installer.DEFAULT_BASE_URL}
1163
1552
  `);
@@ -1166,7 +1555,7 @@ Docs: https://auxilo.io · API: ${installer.DEFAULT_BASE_URL}
1166
1555
  async function main() {
1167
1556
  const cmd = process.argv[2];
1168
1557
  const subcommandHelp = ['help', '--help', '-h'].includes(process.argv[3]);
1169
- if (['setup', 'init', 'status', 'review', 'disable', 'clean-lane'].includes(cmd) && subcommandHelp) {
1558
+ if (['setup', 'init', 'status', 'review', 'disable', 'clean-lane', 'provider'].includes(cmd) && subcommandHelp) {
1170
1559
  return usage(cmd);
1171
1560
  }
1172
1561
  const flags = parseFlags(process.argv);
@@ -1177,6 +1566,7 @@ async function main() {
1177
1566
  case 'review': return cmdReview(flags);
1178
1567
  case 'disable': return cmdDisable(flags);
1179
1568
  case 'clean-lane': return cmdCleanLane(flags);
1569
+ case 'provider': return cmdProvider(flags);
1180
1570
  case 'help': case '--help': case '-h': case undefined: return usage();
1181
1571
  default:
1182
1572
  console.error(`Unknown command: ${cmd}`);
@@ -1204,6 +1594,8 @@ if (require.main === module) {
1204
1594
 
1205
1595
  module.exports = {
1206
1596
  parseFlags,
1597
+ runnerSkewLine,
1598
+ extractionProviderLine,
1207
1599
  resolveBaseUrl,
1208
1600
  shortFlags,
1209
1601
  groupSummaryRows,
@@ -1212,4 +1604,14 @@ module.exports = {
1212
1604
  run,
1213
1605
  CLEAN_LANE_AFFIRMATION,
1214
1606
  CLEAN_LANE_UNAVAILABLE,
1607
+ CLEAN_LANE_TERMS_G,
1608
+ CLEAN_LANE_TERMS_G2,
1609
+ CLEAN_LANE_NO_EMAIL_LINE,
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,
1215
1617
  };