auxilo-mcp 0.9.14 → 0.9.16

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/bin/auxilo-cli.js CHANGED
@@ -22,6 +22,7 @@ const readline = require('readline');
22
22
  const { exec } = require('child_process');
23
23
  const installer = require('../lib/installer.js');
24
24
  const review = require('../lib/review.js');
25
+ const runnerAutoupdate = require('../lib/runner-autoupdate.js');
25
26
  const providers = require('../scripts/providers/index.js');
26
27
  const byoKeyProvider = require('../scripts/providers/byo-key.js');
27
28
 
@@ -186,6 +187,10 @@ const CONSENT_TEXT = `
186
187
  other agents unlock your learnings and are not guaranteed. Earnings
187
188
  accrue now. Withdrawals open soon, and auxilo.io/status shows where
188
189
  things stand.
190
+ • UPDATES itself once a day from npm. A newer version installs only after
191
+ its signature and checksum both pass, and if either fails you keep the
192
+ copy you have. Run auxilo setup --no-autoupdate to decline. auxilo
193
+ status shows the setting and the last check.
189
194
  You can stop any time with \`auxilo disable\` (local kill-switch) and review
190
195
  every run in ~/.auxilo/extract.log. Saying No installs the MCP server only.
191
196
  No session-end capture hook is written into any client config unless you say
@@ -291,6 +296,15 @@ async function cmdSetup(flags) {
291
296
  process.exit(1);
292
297
  }
293
298
 
299
+ // RUNNER-AUTO-UPDATE (0.9.16): `--no-autoupdate` persists the opt-out so
300
+ // every future hook-fired run skips the self-update network check, not
301
+ // just this invocation of `setup` (env AUXILO_RUNNER_AUTOUPDATE=0 is the
302
+ // other, per-run-only opt-out — see lib/runner-autoupdate.js).
303
+ if (flags['no-autoupdate']) {
304
+ installer.writeRunnerConfig(HOME, { autoupdate: false });
305
+ console.log(' ✓ Runner auto-update disabled (persisted to ~/.auxilo/runner-config.json)');
306
+ }
307
+
294
308
  const claudeCode = chosen.find((c) => c.id === 'claude-code');
295
309
  if (claudeCode) {
296
310
  // LW-18: SessionStart held-count notice ("N learnings held for your
@@ -551,6 +565,13 @@ async function cmdStatus() {
551
565
  if (s.runnerInstalled) {
552
566
  const line = runnerSkewLine(installer.runnerVersionSkew(HOME));
553
567
  if (line) console.log(line);
568
+ // RUNNER-AUTO-UPDATE (0.9.16): one more line in the same section, no new
569
+ // heading — auto-update on/off/paused-by-env, last check time, and the
570
+ // last verification result (if any check has ever run).
571
+ const autoupdateLine = runnerAutoupdate.runnerAutoupdateStatusLine(
572
+ runnerAutoupdate.getRunnerAutoupdateStatus(HOME)
573
+ );
574
+ if (autoupdateLine) console.log(autoupdateLine);
554
575
  }
555
576
  console.log(extractionProviderLine(await providers.resolveProvider({})));
556
577
  // Lazy require: scripts/runner.js is a heavier module (sources, sensitivity
@@ -1435,10 +1456,12 @@ async function cmdProvider(flags) {
1435
1456
 
1436
1457
  function usage(command) {
1437
1458
  const blocks = {
1438
- setup: `Usage: auxilo setup [--re-auth] [--base-url <url>]
1459
+ setup: `Usage: auxilo setup [--re-auth] [--base-url <url>] [--no-autoupdate]
1439
1460
 
1440
1461
  Interactively detect clients, register Auxilo, sign in, install the optional
1441
- extraction runner and SessionEnd hook, and record the extraction choice.`,
1462
+ extraction runner and SessionEnd hook, and record the extraction choice.
1463
+ --no-autoupdate persists opt-out of the runner's self-update check (default:
1464
+ on — see AUXILO_RUNNER_AUTOUPDATE=0 for a per-run-only opt-out instead).`,
1442
1465
  init: `Usage: auxilo init [--scope <read|earnings-read|contribute>] [--label <name>]
1443
1466
  [--env-file <path>] [--save] [--json] [--no-browser]
1444
1467
  [--base-url <url>]
@@ -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
- const DEFAULT_INDEX_PATH = path.join(os.homedir(), '.auxilo', 'extracted-index.jsonl');
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/lib/installer.js CHANGED
@@ -24,6 +24,7 @@
24
24
 
25
25
  const fs = require('fs');
26
26
  const path = require('path');
27
+ const crypto = require('crypto');
27
28
  const { hasAuxiloSessionEndHook } = require('./hook-status.js');
28
29
 
29
30
  // ─── Constants ──────────────────────────────────────────────────────────────
@@ -118,6 +119,16 @@ const RUNNER_STACK = Object.freeze([
118
119
  ['lib/similarity.js', 'lib/similarity.js', 0o644],
119
120
  ['lib/hook-status.js', 'lib/hook-status.js', 0o644],
120
121
  ['lib/ops-alert.js', 'lib/ops-alert.js', 0o644],
122
+ // RUNNER-AUTO-UPDATE (0.9.16): runner.js requires lib/runner-autoupdate.js,
123
+ // which in turn requires lib/installer.js (installRunner/binRootFor/
124
+ // readRunnerConfig/writeRunnerConfig — the atomic-swap seam) plus its own
125
+ // two small helpers. All four must ship or the installed runner's first
126
+ // auto-update check is a MODULE_NOT_FOUND (test/runner-packaging-closure.test.js
127
+ // guards this closure automatically).
128
+ ['lib/runner-autoupdate.js', 'lib/runner-autoupdate.js', 0o644],
129
+ ['lib/tar-extract.js', 'lib/tar-extract.js', 0o644],
130
+ ['lib/semver-min.js', 'lib/semver-min.js', 0o644],
131
+ ['lib/installer.js', 'lib/installer.js', 0o644],
121
132
  ['config/near-duplicate.json', 'config/near-duplicate.json', 0o644],
122
133
  ]);
123
134
 
@@ -629,6 +640,56 @@ function writeCredentials(homeDir, creds) {
629
640
  return credPath;
630
641
  }
631
642
 
643
+ // ─── Runner config (RUNNER-AUTO-UPDATE, 0.9.16 §6) ──────────────────────────
644
+ //
645
+ // Lightweight, NON-secret sibling to credentials.json: holds the persisted
646
+ // `auxilo setup --no-autoupdate` opt-out plus the auto-update check/
647
+ // verification state `auxilo status` reports (lib/runner-autoupdate.js is
648
+ // the only other reader/writer). Deliberately a separate file rather than
649
+ // folded into credentials.json — credentials.json is chmod 0600 and its
650
+ // shape is the auth contract (api_key/base_url/email/account_id); this file
651
+ // holds neither secrets nor auth state, so it stays plain (no forced chmod)
652
+ // and its shape can grow without touching the auth surface.
653
+
654
+ function runnerConfigPath(homeDir) {
655
+ return path.join(homeDir, '.auxilo', 'runner-config.json');
656
+ }
657
+
658
+ /** Malformed/absent → {} (same tolerance as readCredentials). */
659
+ function readRunnerConfig(homeDir) {
660
+ try {
661
+ const cfg = JSON.parse(fs.readFileSync(runnerConfigPath(homeDir), 'utf-8'));
662
+ return cfg && typeof cfg === 'object' && !Array.isArray(cfg) ? cfg : {};
663
+ } catch {
664
+ return {};
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Merge `patch` into the existing config and write it back (read-modify-
670
+ * write, tmp + rename — same discipline as writeCredentials/saveLedger).
671
+ * Never throws — a failed write must not break the caller (setup, or the
672
+ * best-effort auto-update state persistence).
673
+ *
674
+ * @param {string} homeDir
675
+ * @param {object} patch Shallow-merged into the existing config.
676
+ * @returns {object} the config as written (best-effort — may be stale if
677
+ * the write itself failed).
678
+ */
679
+ function writeRunnerConfig(homeDir, patch) {
680
+ const merged = { ...readRunnerConfig(homeDir), ...(patch || {}) };
681
+ try {
682
+ const cfgPath = runnerConfigPath(homeDir);
683
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
684
+ const tmp = `${cfgPath}.tmp`;
685
+ fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\n');
686
+ fs.renameSync(tmp, cfgPath);
687
+ } catch {
688
+ /* best-effort — see doc comment above */
689
+ }
690
+ return merged;
691
+ }
692
+
632
693
  // ─── Device-code auth (spec §LW-12 step 2; server.js /auth/device) ──────────
633
694
 
634
695
  /**
@@ -843,12 +904,18 @@ exit 0
843
904
  * @param {string} homeDir
844
905
  * @param {object} [opts]
845
906
  * @param {string} [opts.packageRoot=PACKAGE_ROOT]
907
+ * @param {string} [opts.binRootOverride] RUNNER-AUTO-UPDATE (0.9.16): build
908
+ * the stack somewhere OTHER than <home>/.auxilo/bin — e.g. `<bin>.new` —
909
+ * so the auto-updater can build+verify a full new stack in a scratch dir
910
+ * and only `fs.renameSync` it into place as the single atomic swap step.
911
+ * Defaults to `binRootFor(homeDir)`, so ordinary `setup` behavior (writes
912
+ * straight into <home>/.auxilo/bin) is completely unchanged.
846
913
  * @returns {{ binRoot: string, hookPath: string, versionPath: string, version: string, installed: string[] }}
847
914
  */
848
915
  function installRunner(homeDir, opts = {}) {
849
916
  if (!homeDir) throw new Error('installRunner: homeDir is required');
850
917
  const packageRoot = opts.packageRoot || PACKAGE_ROOT;
851
- const binRoot = binRootFor(homeDir);
918
+ const binRoot = opts.binRootOverride || binRootFor(homeDir);
852
919
  const installed = [];
853
920
 
854
921
  for (const [src, dest, mode] of RUNNER_STACK) {
@@ -863,13 +930,20 @@ function installRunner(homeDir, opts = {}) {
863
930
  installed.push(destPath);
864
931
  }
865
932
 
866
- const hookPath = hookScriptPathFor(homeDir);
933
+ // NOTE: the hook script's own CONTENT always names the real final
934
+ // <home>/.auxilo/bin location (renderHookScript is homeDir-based, not
935
+ // binRoot-based) — correct even while staging into a binRootOverride
936
+ // scratch dir, since that is where this tree lands after the swap. Only
937
+ // the WRITE TARGET (hookPath/versionPath below) follows binRoot, so a
938
+ // staged build never touches the real bin root until the caller renames
939
+ // the whole staged directory into place.
940
+ const hookPath = path.join(binRoot, path.basename(hookScriptPathFor(homeDir)));
867
941
  fs.writeFileSync(hookPath, renderHookScript(homeDir));
868
942
  fs.chmodSync(hookPath, 0o755);
869
943
  installed.push(hookPath);
870
944
 
871
945
  const version = packageVersion(packageRoot);
872
- const versionPath = runnerVersionPath(homeDir);
946
+ const versionPath = path.join(binRoot, path.basename(runnerVersionPath(homeDir)));
873
947
  fs.writeFileSync(versionPath, `${version}\n`);
874
948
  fs.chmodSync(versionPath, 0o644);
875
949
  installed.push(versionPath);
@@ -877,6 +951,151 @@ function installRunner(homeDir, opts = {}) {
877
951
  return { binRoot, hookPath, versionPath, version, installed };
878
952
  }
879
953
 
954
+ /**
955
+ * RUNNER-AUTO-UPDATE B1 fix: install the runner stack into the REAL
956
+ * `<home>/.auxilo/bin` (never a scratch `binRootOverride`) with every write
957
+ * ATOMIC PER FILE — a `<dest>.tmp-<pid>-<rand>` write followed by
958
+ * `fs.renameSync` onto the final path — instead of staging a whole tree
959
+ * elsewhere and `fs.renameSync`-ing the ENTIRE directory into place.
960
+ *
961
+ * Why this exists: the old auto-update swap renamed the whole `<bin>`
962
+ * directory. `installRunner` only ever writes the files listed in
963
+ * RUNNER_STACK plus the hook script and VERSION stamp — but `<bin>` also
964
+ * holds files installRunner never touches: the per-client capture shims
965
+ * (captureShimPath), the review-notice shim (noticeShimPath), the
966
+ * sweeper/backup wrappers, and jobs/ — all referenced by ABSOLUTE PATH from
967
+ * client settings files and launchd plists. A whole-directory rename
968
+ * silently deletes every one of those on the very first auto-update. Per-
969
+ * file atomic writes touch only the files this function actually knows
970
+ * about and leave every sibling entry in `<bin>` completely untouched.
971
+ *
972
+ * Each individual file lands atomically (a reader never observes a
973
+ * partially-written file), but there is no cross-file transaction — if this
974
+ * throws partway through, some stack files may already be updated and
975
+ * others not. That is an intentional trade for correctness over the old
976
+ * design's false all-or-nothing guarantee, which in practice deleted
977
+ * everything BUT the stack on every run. A crash mid-loop still leaves a
978
+ * fully-functional `<bin>` (old files for whatever wasn't reached yet, new
979
+ * files for what was) — never a missing directory.
980
+ *
981
+ * @param {string} homeDir
982
+ * @param {object} [opts]
983
+ * @param {string} [opts.packageRoot=PACKAGE_ROOT] Root to copy FROM — the
984
+ * caller passes the already-SRI+signature-verified extracted tarball dir.
985
+ * @returns {{ binRoot: string, hookPath: string, versionPath: string, version: string, installed: string[] }}
986
+ */
987
+ /**
988
+ * MEDIUM-2 fix (0.9.16 fix pass 2). RUNNER_STACK's sources/providers/schemas
989
+ * rows are DERIVED by enumerating the package's own directories at require
990
+ * time (sourceAdapterRows/providerAdapterRows/providerSchemaRows above) — a
991
+ * file REMOVED from a newer release's package is simply absent from the
992
+ * NEW RUNNER_STACK, but installRunnerAtomic only ever WRITES what's in the
993
+ * stack; it never deleted what a PRIOR install left behind. A stale adapter
994
+ * (or provider/schema/lib module) from an old version therefore survived
995
+ * every auto-update indefinitely, still sitting in `<bin>` and still
996
+ * require()-able by anything that enumerates its directory the way
997
+ * scripts/runner.js's own loadSources() does.
998
+ *
999
+ * Scoped tightly on purpose: only the directories RUNNER_STACK actually
1000
+ * populates by enumeration are pruned — never the bin ROOT itself, and
1001
+ * never any other subdirectory. `<bin>` also holds files/dirs
1002
+ * installRunnerAtomic has never managed (capture shims, the review-notice
1003
+ * shim, sweeper/backup wrappers, jobs/ — see installRunnerAtomic's own doc
1004
+ * comment on why a whole-directory approach is unsafe here) — pruning must
1005
+ * leave every one of those completely untouched, exactly like the atomic
1006
+ * writes above already do.
1007
+ *
1008
+ * Also sweeps orphaned `<file>.tmp-<pid>-<hex>` scratch files left in a
1009
+ * pruned directory by a process that crashed between atomicWrite's write
1010
+ * and its rename (so a leftover partial write from a prior crashed install
1011
+ * doesn't accumulate forever either).
1012
+ *
1013
+ * @param {string} binRoot
1014
+ * @param {string[]} keepDestPaths Absolute dest paths this install just
1015
+ * wrote (the `installed` accumulator, stack rows only — hook/VERSION
1016
+ * live outside every prunable dir so including them would be harmless
1017
+ * but is unnecessary).
1018
+ */
1019
+ const PRUNABLE_STACK_DIRS = Object.freeze([
1020
+ 'scripts/sources',
1021
+ 'scripts/providers',
1022
+ 'scripts/providers/schemas',
1023
+ 'lib',
1024
+ ]);
1025
+
1026
+ const ORPHANED_TMP_RE = /\.tmp-\d+-[0-9a-f]+$/;
1027
+
1028
+ function pruneStaleStackFiles(binRoot, keepDestPaths) {
1029
+ const keep = new Set(keepDestPaths.map((p) => path.resolve(p)));
1030
+ for (const rel of PRUNABLE_STACK_DIRS) {
1031
+ const dir = path.join(binRoot, rel);
1032
+ let entries;
1033
+ try {
1034
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1035
+ } catch {
1036
+ continue; // directory doesn't exist (nothing shipped there) — nothing to prune
1037
+ }
1038
+ for (const entry of entries) {
1039
+ // Never descend into or remove subdirectories here — each prunable
1040
+ // subdirectory (e.g. scripts/providers/schemas) is enumerated in its
1041
+ // own PRUNABLE_STACK_DIRS row; a bare directory entry (like
1042
+ // scripts/providers/schemas itself, seen while walking
1043
+ // scripts/providers) is left alone.
1044
+ if (!entry.isFile()) continue;
1045
+ const full = path.join(dir, entry.name);
1046
+ if (ORPHANED_TMP_RE.test(entry.name)) {
1047
+ try { fs.rmSync(full, { force: true }); } catch { /* best-effort */ }
1048
+ continue;
1049
+ }
1050
+ if (!keep.has(path.resolve(full))) {
1051
+ try { fs.rmSync(full, { force: true }); } catch { /* best-effort */ }
1052
+ }
1053
+ }
1054
+ }
1055
+ }
1056
+
1057
+ function installRunnerAtomic(homeDir, opts = {}) {
1058
+ if (!homeDir) throw new Error('installRunnerAtomic: homeDir is required');
1059
+ const packageRoot = opts.packageRoot || PACKAGE_ROOT;
1060
+ const binRoot = binRootFor(homeDir);
1061
+ const installed = [];
1062
+
1063
+ function atomicWrite(destPath, writeTmp, mode) {
1064
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
1065
+ const tmp = `${destPath}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
1066
+ writeTmp(tmp);
1067
+ fs.chmodSync(tmp, mode);
1068
+ fs.renameSync(tmp, destPath); // atomic on the same filesystem (same dir)
1069
+ }
1070
+
1071
+ for (const [src, dest, mode] of RUNNER_STACK) {
1072
+ const srcPath = path.join(packageRoot, src);
1073
+ const destPath = path.join(binRoot, dest);
1074
+ if (!fs.existsSync(srcPath)) {
1075
+ throw new Error(`installRunnerAtomic: missing package file ${srcPath} — reinstall auxilo-mcp`);
1076
+ }
1077
+ atomicWrite(destPath, (tmp) => fs.copyFileSync(srcPath, tmp), mode);
1078
+ installed.push(destPath);
1079
+ }
1080
+
1081
+ // MEDIUM-2: prune stale stack files (removed adapters/providers/schemas/
1082
+ // lib modules from a prior version) now that every file the NEW stack
1083
+ // wants is safely on disk. Scoped to the enumerated dirs only — see
1084
+ // pruneStaleStackFiles's doc comment.
1085
+ pruneStaleStackFiles(binRoot, installed);
1086
+
1087
+ const hookPath = path.join(binRoot, path.basename(hookScriptPathFor(homeDir)));
1088
+ atomicWrite(hookPath, (tmp) => fs.writeFileSync(tmp, renderHookScript(homeDir)), 0o755);
1089
+ installed.push(hookPath);
1090
+
1091
+ const version = packageVersion(packageRoot);
1092
+ const versionPath = path.join(binRoot, path.basename(runnerVersionPath(homeDir)));
1093
+ atomicWrite(versionPath, (tmp) => fs.writeFileSync(tmp, `${version}\n`), 0o644);
1094
+ installed.push(versionPath);
1095
+
1096
+ return { binRoot, hookPath, versionPath, version, installed };
1097
+ }
1098
+
880
1099
  // ─── Runner-stack freshness (CLEAN-LANE-FLIP Phase B) ───────────────────────
881
1100
 
882
1101
  /** <home>/.auxilo/bin/VERSION — the package version installRunner last copied. */
@@ -1794,12 +2013,17 @@ module.exports = {
1794
2013
  credentialsPath,
1795
2014
  readCredentials,
1796
2015
  writeCredentials,
2016
+ runnerConfigPath,
2017
+ readRunnerConfig,
2018
+ writeRunnerConfig,
1797
2019
  writeEnvFile,
1798
2020
  deviceLogin,
1799
2021
  binRootFor,
1800
2022
  hookScriptPathFor,
1801
2023
  renderHookScript,
1802
2024
  installRunner,
2025
+ installRunnerAtomic,
2026
+ pruneStaleStackFiles,
1803
2027
  runnerVersionPath,
1804
2028
  packageVersion,
1805
2029
  installedRunnerVersion,