auxilo-mcp 0.9.2 → 0.9.3
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 +205 -31
- package/lib/installer.js +392 -12
- package/mcp-server.js +5 -5
- package/package.json +5 -2
- package/scripts/extract-local.js +99 -14
- package/scripts/review-notice.js +145 -0
- package/scripts/runner.js +161 -44
- package/scripts/sources/cline.js +160 -0
- package/scripts/sources/continue.js +140 -0
- package/scripts/sources/roo-code.js +35 -0
package/lib/installer.js
CHANGED
|
@@ -36,6 +36,24 @@ const DEFAULT_BASE_URL = 'https://auxilo.io';
|
|
|
36
36
|
/** Root of the installed npm package (or the repo, when run from a clone). */
|
|
37
37
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Source-adapter rows for the runner stack, DERIVED by enumerating
|
|
41
|
+
* scripts/sources/*.js (UC-3): a new adapter file is automatically shipped —
|
|
42
|
+
* the d8c7099 bug class (adapter added, manifest not) is structurally
|
|
43
|
+
* impossible for the sources dir. Mirrored by runner.js sweeperManifest();
|
|
44
|
+
* the manifest-closure test asserts both stay complete.
|
|
45
|
+
*/
|
|
46
|
+
function sourceAdapterRows(packageRoot = PACKAGE_ROOT) {
|
|
47
|
+
try {
|
|
48
|
+
return fs.readdirSync(path.join(packageRoot, 'scripts', 'sources'))
|
|
49
|
+
.filter((f) => f.endsWith('.js'))
|
|
50
|
+
.sort()
|
|
51
|
+
.map((f) => [`scripts/sources/${f}`, `scripts/sources/${f}`, 0o644]);
|
|
52
|
+
} catch {
|
|
53
|
+
return []; // missing dir surfaces as installRunner missing-file errors
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
/**
|
|
40
58
|
* Runner stack shipped in the npm tarball and copied into <home>/.auxilo/bin,
|
|
41
59
|
* preserving relative layout so runner.js's requires resolve
|
|
@@ -51,12 +69,9 @@ const RUNNER_STACK = Object.freeze([
|
|
|
51
69
|
// at first extraction; omitting it from the installed stack is a MODULE_NOT_FOUND
|
|
52
70
|
// for every npm-installed user (test/runner-packaging-closure.test.js guards this).
|
|
53
71
|
['scripts/extract-local.js', 'scripts/extract-local.js', 0o644],
|
|
54
|
-
|
|
55
|
-
['scripts/
|
|
56
|
-
|
|
57
|
-
['scripts/sources/gemini-cli.js', 'scripts/sources/gemini-cli.js', 0o644],
|
|
58
|
-
['scripts/sources/antigravity.js', 'scripts/sources/antigravity.js', 0o644],
|
|
59
|
-
['scripts/sources/generic-jsonl.js', 'scripts/sources/generic-jsonl.js', 0o644],
|
|
72
|
+
// LW-18 layer 1b: SessionStart held-count notice (shim target).
|
|
73
|
+
['scripts/review-notice.js', 'scripts/review-notice.js', 0o755],
|
|
74
|
+
...sourceAdapterRows(),
|
|
60
75
|
['lib/sensitivity-filter.js', 'lib/sensitivity-filter.js', 0o644],
|
|
61
76
|
]);
|
|
62
77
|
|
|
@@ -89,6 +104,21 @@ const RUNNER_STACK = Object.freeze([
|
|
|
89
104
|
* @param {object} [opts.env=process.env] For %APPDATA% on win32.
|
|
90
105
|
* @returns {Array<object>} registry entries (detected or not)
|
|
91
106
|
*/
|
|
107
|
+
/**
|
|
108
|
+
* VS Code-family globalStorage dirs for an extension publisher id, per
|
|
109
|
+
* platform (UC-3 poll-source detection: Cline / Roo Code). Any-of probing —
|
|
110
|
+
* Code, Code - Insiders, VSCodium.
|
|
111
|
+
*/
|
|
112
|
+
function vscodeGlobalStorageDirs(homeDir, platform, env, publisherDirs) {
|
|
113
|
+
const apps = ['Code', 'Code - Insiders', 'VSCodium'];
|
|
114
|
+
const userDirs = platform === 'darwin'
|
|
115
|
+
? apps.map((a) => path.join(homeDir, 'Library', 'Application Support', a, 'User'))
|
|
116
|
+
: platform === 'win32'
|
|
117
|
+
? apps.map((a) => path.join(env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), a, 'User'))
|
|
118
|
+
: apps.map((a) => path.join(homeDir, '.config', a, 'User'));
|
|
119
|
+
return userDirs.flatMap((u) => publisherDirs.map((p) => path.join(u, 'globalStorage', p)));
|
|
120
|
+
}
|
|
121
|
+
|
|
92
122
|
function clientRegistry(homeDir, opts = {}) {
|
|
93
123
|
if (!homeDir) throw new Error('clientRegistry: homeDir is required');
|
|
94
124
|
const platform = opts.platform || process.platform;
|
|
@@ -138,6 +168,24 @@ function clientRegistry(homeDir, opts = {}) {
|
|
|
138
168
|
mcp: false,
|
|
139
169
|
hooks: false,
|
|
140
170
|
},
|
|
171
|
+
// ── UC-3 poll-based sources (no MCP config; sweeper adapters) ───────────
|
|
172
|
+
{
|
|
173
|
+
id: 'cline',
|
|
174
|
+
name: 'Cline (VS Code)',
|
|
175
|
+
detectDirs: vscodeGlobalStorageDirs(homeDir, platform, env, ['saoudrizwan.claude-dev']),
|
|
176
|
+
configPath: null, // poll-based source adapter (scripts/sources/cline.js)
|
|
177
|
+
mcp: false,
|
|
178
|
+
hooks: false,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 'roo-code',
|
|
182
|
+
name: 'Roo Code (VS Code)',
|
|
183
|
+
detectDirs: vscodeGlobalStorageDirs(homeDir, platform, env,
|
|
184
|
+
['rooveterinaryinc.roo-cline', 'rooveterinaryinc.roo-code']),
|
|
185
|
+
configPath: null, // poll-based source adapter (scripts/sources/roo-code.js)
|
|
186
|
+
mcp: false,
|
|
187
|
+
hooks: false,
|
|
188
|
+
},
|
|
141
189
|
// ── UC-0 additions (config paths web-verified June 2026, BUILD-SPEC-UNIVERSAL-CLIENTS §5) ──
|
|
142
190
|
{
|
|
143
191
|
id: 'windsurf',
|
|
@@ -544,11 +592,19 @@ function writeCredentials(homeDir, creds) {
|
|
|
544
592
|
*
|
|
545
593
|
* @param {object} opts
|
|
546
594
|
* @param {string} [opts.baseUrl=DEFAULT_BASE_URL]
|
|
595
|
+
* @param {string} [opts.scope] Wave 3.4 (D2/NF-3): requested key scope
|
|
596
|
+
* (read | earnings-read | contribute). Omitted →
|
|
597
|
+
* no request body at all (exact pre-3.4 wire
|
|
598
|
+
* shape; server mints 'contribute').
|
|
599
|
+
* @param {string} [opts.label] Wave 3.4: requested key label (`auxilo init`).
|
|
547
600
|
* @param {Function} [opts.fetchImpl=fetch] Injectable for tests.
|
|
548
601
|
* @param {Function} [opts.onCode] (userCode, verificationUrl) → void; print/open browser.
|
|
549
602
|
* @param {Function} [opts.sleep] (ms) → Promise; injectable for tests.
|
|
550
603
|
* @param {number} [opts.maxWaitMs=600000] 10 min TTL (server DEVICE_CODE_TTL).
|
|
551
|
-
* @returns {Promise<{ api_key: string, account_id: string, email: string, base_url: string
|
|
604
|
+
* @returns {Promise<{ api_key: string, account_id: string, email: string, base_url: string,
|
|
605
|
+
* scope?: string, label?: string }>}
|
|
606
|
+
* scope/label are the server's echo of what was ACTUALLY minted (absent on
|
|
607
|
+
* pre-Wave-3.4 servers — callers requesting a scope should compare and warn).
|
|
552
608
|
* @throws Error on expired code, HTTP failure, or timeout.
|
|
553
609
|
*/
|
|
554
610
|
async function deviceLogin(opts = {}) {
|
|
@@ -557,10 +613,17 @@ async function deviceLogin(opts = {}) {
|
|
|
557
613
|
const sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
558
614
|
const maxWaitMs = opts.maxWaitMs !== undefined ? opts.maxWaitMs : 600000;
|
|
559
615
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
616
|
+
// Wave 3.4: send a body ONLY when a scope/label was requested — an omitted
|
|
617
|
+
// body keeps the exact pre-3.4 request shape (and old servers ignore the
|
|
618
|
+
// body entirely, minting the historical contribute-scoped Device Login Key).
|
|
619
|
+
const init = { method: 'POST', headers: { 'Content-Type': 'application/json' } };
|
|
620
|
+
if (opts.scope !== undefined || opts.label !== undefined) {
|
|
621
|
+
const body = {};
|
|
622
|
+
if (opts.scope !== undefined) body.scope = opts.scope;
|
|
623
|
+
if (opts.label !== undefined) body.label = opts.label;
|
|
624
|
+
init.body = JSON.stringify(body);
|
|
625
|
+
}
|
|
626
|
+
const res = await fetchImpl(`${baseUrl}/auth/device`, init);
|
|
564
627
|
if (!res.ok) throw new Error(`Device code request failed (HTTP ${res.status})`);
|
|
565
628
|
const device = await res.json();
|
|
566
629
|
// A-1: device_code is the secret polling credential; user_code is the human
|
|
@@ -590,6 +653,10 @@ async function deviceLogin(opts = {}) {
|
|
|
590
653
|
account_id: status.account_id,
|
|
591
654
|
email: status.email,
|
|
592
655
|
base_url: baseUrl,
|
|
656
|
+
// Wave 3.4: server echo of the minted scope/label (undefined on
|
|
657
|
+
// pre-3.4 servers — callers compare against the request and warn).
|
|
658
|
+
scope: status.scope,
|
|
659
|
+
label: status.label,
|
|
593
660
|
};
|
|
594
661
|
}
|
|
595
662
|
if (status.status === 'expired') {
|
|
@@ -600,6 +667,50 @@ async function deviceLogin(opts = {}) {
|
|
|
600
667
|
throw new Error('Timed out waiting for device authorization (10 minutes).');
|
|
601
668
|
}
|
|
602
669
|
|
|
670
|
+
// ─── Env-file writer (Wave 3.4 / NF-3 `auxilo init --env-file`) ─────────────
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Write (or update in place) an env file with the minted key. Idempotent:
|
|
674
|
+
* existing AUXILO_API_KEY / AUXILO_BASE_URL lines are REPLACED, never
|
|
675
|
+
* duplicated; every other line is preserved byte-for-byte. New files are
|
|
676
|
+
* created 0600; existing files are chmod'd 0600 before the secret lands
|
|
677
|
+
* (same tmp(0600)+rename discipline as writeCredentials).
|
|
678
|
+
*
|
|
679
|
+
* @param {string} filePath Absolute or cwd-relative env file path.
|
|
680
|
+
* @param {{ api_key: string, base_url?: string }} values
|
|
681
|
+
* @returns {{ path: string, created: boolean }}
|
|
682
|
+
*/
|
|
683
|
+
function writeEnvFile(filePath, values) {
|
|
684
|
+
if (!filePath) throw new Error('writeEnvFile: filePath is required');
|
|
685
|
+
if (!values || !values.api_key) throw new Error('writeEnvFile: values.api_key is required');
|
|
686
|
+
|
|
687
|
+
const existed = fs.existsSync(filePath);
|
|
688
|
+
let lines = [];
|
|
689
|
+
if (existed) {
|
|
690
|
+
fs.chmodSync(filePath, 0o600);
|
|
691
|
+
lines = fs.readFileSync(filePath, 'utf-8').split('\n');
|
|
692
|
+
// Drop a single trailing empty line so we can re-append cleanly.
|
|
693
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const upsert = (name, value) => {
|
|
697
|
+
const rendered = `${name}=${value}`;
|
|
698
|
+
const idx = lines.findIndex((l) => l.startsWith(`${name}=`) || l.startsWith(`export ${name}=`));
|
|
699
|
+
if (idx === -1) lines.push(rendered);
|
|
700
|
+
else lines[idx] = lines[idx].startsWith('export ') ? `export ${rendered}` : rendered;
|
|
701
|
+
};
|
|
702
|
+
upsert('AUXILO_API_KEY', values.api_key);
|
|
703
|
+
if (values.base_url) upsert('AUXILO_BASE_URL', values.base_url);
|
|
704
|
+
|
|
705
|
+
const dir = path.dirname(filePath);
|
|
706
|
+
if (dir && dir !== '.') fs.mkdirSync(dir, { recursive: true });
|
|
707
|
+
const tmp = `${filePath}.tmp`;
|
|
708
|
+
fs.writeFileSync(tmp, lines.join('\n') + '\n', { mode: 0o600 });
|
|
709
|
+
fs.renameSync(tmp, filePath);
|
|
710
|
+
fs.chmodSync(filePath, 0o600);
|
|
711
|
+
return { path: filePath, created: !existed };
|
|
712
|
+
}
|
|
713
|
+
|
|
603
714
|
// ─── Runner install (spec §LW-12 step 3; layout per P1-13) ──────────────────
|
|
604
715
|
|
|
605
716
|
function binRootFor(homeDir) {
|
|
@@ -782,6 +893,52 @@ function registerClaudeCodeHook(homeDir) {
|
|
|
782
893
|
return { changed: true, hookCmd, removedLegacy: removedLegacy.filter((c) => c !== hookCmd) };
|
|
783
894
|
}
|
|
784
895
|
|
|
896
|
+
/**
|
|
897
|
+
* UC-1a: remove every Auxilo extraction entry from Claude Code's SessionEnd
|
|
898
|
+
* hooks — bare-string legacy entries AND structured command entries, while
|
|
899
|
+
* preserving non-Auxilo commands sharing a matcher group. Used by the setup
|
|
900
|
+
* consent=No path so no capture artifact remains (including ones left by
|
|
901
|
+
* earlier installs that wrote hooks pre-consent).
|
|
902
|
+
*
|
|
903
|
+
* Idempotent; missing settings file is a no-op; malformed JSON throws (B15).
|
|
904
|
+
*
|
|
905
|
+
* @returns {{ changed: boolean, removed: string[] }}
|
|
906
|
+
*/
|
|
907
|
+
function removeClaudeCodeHook(homeDir) {
|
|
908
|
+
if (!homeDir) throw new Error('removeClaudeCodeHook: homeDir is required');
|
|
909
|
+
const settingsPath = path.join(homeDir, '.claude', 'settings.json');
|
|
910
|
+
if (!fs.existsSync(settingsPath)) return { changed: false, removed: [] };
|
|
911
|
+
|
|
912
|
+
const settings = readClientConfig(settingsPath);
|
|
913
|
+
if (!settings.hooks || !Array.isArray(settings.hooks.SessionEnd)) {
|
|
914
|
+
return { changed: false, removed: [] };
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const removed = [];
|
|
918
|
+
const kept = [];
|
|
919
|
+
for (const entry of settings.hooks.SessionEnd) {
|
|
920
|
+
if (typeof entry === 'string' && entry.includes('auxilo-extract')) {
|
|
921
|
+
removed.push(entry);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (entry && typeof entry === 'object' && Array.isArray(entry.hooks)) {
|
|
925
|
+
const auxilo = entry.hooks.filter(isAuxiloCommandHook);
|
|
926
|
+
if (auxilo.length > 0) {
|
|
927
|
+
removed.push(...auxilo.map((h) => h.command));
|
|
928
|
+
const rest = entry.hooks.filter((h) => !isAuxiloCommandHook(h));
|
|
929
|
+
if (rest.length > 0) kept.push({ ...entry, hooks: rest });
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
kept.push(entry);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (removed.length === 0) return { changed: false, removed: [] };
|
|
937
|
+
settings.hooks.SessionEnd = kept;
|
|
938
|
+
writeJsonAtomic(settingsPath, settings);
|
|
939
|
+
return { changed: true, removed };
|
|
940
|
+
}
|
|
941
|
+
|
|
785
942
|
// ─── UC-1 capture-hook registration ─────────────────────────────────────────
|
|
786
943
|
//
|
|
787
944
|
// Per-client session-end hooks that pipe the client's hook JSON into the
|
|
@@ -800,6 +957,16 @@ function captureShimPath(homeDir, sourceId) {
|
|
|
800
957
|
return path.join(binRootFor(homeDir), `auxilo-capture-${sourceId}.sh`);
|
|
801
958
|
}
|
|
802
959
|
|
|
960
|
+
/**
|
|
961
|
+
* Shell-quote a literal for generated bash: single-quoted, embedded single
|
|
962
|
+
* quotes escaped as '\''. UC-1a L2: paths were previously interpolated inside
|
|
963
|
+
* double quotes, so a homeDir containing `"`, `$`, or backticks broke (or
|
|
964
|
+
* worse, executed inside) the generated script.
|
|
965
|
+
*/
|
|
966
|
+
function shellQuote(s) {
|
|
967
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
968
|
+
}
|
|
969
|
+
|
|
803
970
|
/**
|
|
804
971
|
* Generate the capture shim body. stdin (the client's hook JSON) passes
|
|
805
972
|
* straight through `exec` to capture-core.js; the PATH export covers GUI-
|
|
@@ -810,7 +977,7 @@ function renderCaptureShim(homeDir, sourceId) {
|
|
|
810
977
|
return `#!/bin/bash
|
|
811
978
|
# Auxilo capture shim (${sourceId}) — generated by \`auxilo setup\` (UC-1).
|
|
812
979
|
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
813
|
-
exec /usr/bin/env node
|
|
980
|
+
exec /usr/bin/env node ${shellQuote(corePath)} --source ${shellQuote(sourceId)}
|
|
814
981
|
`;
|
|
815
982
|
}
|
|
816
983
|
|
|
@@ -1073,6 +1240,208 @@ function installCaptureHooks(homeDir, clients, opts = {}) {
|
|
|
1073
1240
|
return results;
|
|
1074
1241
|
}
|
|
1075
1242
|
|
|
1243
|
+
// ─── UC-1a capture-hook removal (consent=No cleanup) ────────────────────────
|
|
1244
|
+
//
|
|
1245
|
+
// Setup previously wrote capture hooks into third-party client configs BEFORE
|
|
1246
|
+
// the consent prompt (boundary smell + uninstall surprise — GOV-3 L1). The
|
|
1247
|
+
// fix is two-sided: bin/auxilo-cli.js now writes hooks only AFTER consent=yes,
|
|
1248
|
+
// and the No path calls these removers so no hook artifact remains — including
|
|
1249
|
+
// artifacts left by earlier installs that wrote pre-consent.
|
|
1250
|
+
|
|
1251
|
+
/** Strip every auxilo-capture entry from a flat hook array; drop empty result. */
|
|
1252
|
+
function stripFlatHookArray(arr) {
|
|
1253
|
+
if (!Array.isArray(arr)) return arr;
|
|
1254
|
+
return arr.filter((e) => !(e && typeof e === 'object' && isAuxiloCaptureCommand(e.command)));
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/** Strip auxilo-capture inner hooks from matcher groups; drop groups left empty. */
|
|
1258
|
+
function stripGroupHookArray(arr) {
|
|
1259
|
+
if (!Array.isArray(arr)) return arr;
|
|
1260
|
+
const kept = [];
|
|
1261
|
+
for (const group of arr) {
|
|
1262
|
+
if (group && typeof group === 'object' && Array.isArray(group.hooks)) {
|
|
1263
|
+
const rest = group.hooks.filter((h) => !(h && isAuxiloCaptureCommand(h.command)));
|
|
1264
|
+
if (rest.length === 0 && group.hooks.length > 0) continue; // group was all ours
|
|
1265
|
+
kept.push(rest.length === group.hooks.length ? group : { ...group, hooks: rest });
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
kept.push(group);
|
|
1269
|
+
}
|
|
1270
|
+
return kept;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Remove the UC-1 capture hook for one client: strip the config entry (per
|
|
1275
|
+
* the client's dialect) and delete the per-source shim. Idempotent — absent
|
|
1276
|
+
* artifacts report changed:false. Malformed config JSON throws (B15 — caller
|
|
1277
|
+
* skips loudly).
|
|
1278
|
+
*
|
|
1279
|
+
* @returns {{ changed: boolean, hookPath: string, configPath: string }}
|
|
1280
|
+
*/
|
|
1281
|
+
function removeCaptureHook(client, homeDir) {
|
|
1282
|
+
if (!homeDir) throw new Error('removeCaptureHook: homeDir is required');
|
|
1283
|
+
if (!client || !client.captureHook || !client.captureConfigPath) {
|
|
1284
|
+
throw new Error(`removeCaptureHook: client ${client && client.id} has no capture-hook support`);
|
|
1285
|
+
}
|
|
1286
|
+
const sourceId = client.sourceId || client.id;
|
|
1287
|
+
const shimPath = captureShimPath(homeDir, sourceId);
|
|
1288
|
+
let changed = false;
|
|
1289
|
+
|
|
1290
|
+
if (fs.existsSync(client.captureConfigPath)) {
|
|
1291
|
+
if (client.id === 'copilot-cli') {
|
|
1292
|
+
// Drop-in file we own entirely — delete it.
|
|
1293
|
+
fs.unlinkSync(client.captureConfigPath);
|
|
1294
|
+
changed = true;
|
|
1295
|
+
} else {
|
|
1296
|
+
changed = patchJsonHookConfig(client.captureConfigPath, (config) => {
|
|
1297
|
+
if (client.id === 'antigravity') {
|
|
1298
|
+
delete config['auxilo-capture']; // we own only this top-level key
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (!config.hooks || typeof config.hooks !== 'object') return;
|
|
1302
|
+
for (const event of Object.keys(config.hooks)) {
|
|
1303
|
+
const arr = config.hooks[event];
|
|
1304
|
+
if (!Array.isArray(arr)) continue;
|
|
1305
|
+
const flat = stripFlatHookArray(arr);
|
|
1306
|
+
config.hooks[event] = stripGroupHookArray(flat);
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (fs.existsSync(shimPath)) {
|
|
1313
|
+
fs.unlinkSync(shimPath);
|
|
1314
|
+
changed = true;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
return { changed, hookPath: shimPath, configPath: client.captureConfigPath };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Remove capture hooks for every captureHook-capable client in `clients`.
|
|
1322
|
+
* Per-client try/catch (B15 skip-loudly), mirroring installCaptureHooks.
|
|
1323
|
+
*
|
|
1324
|
+
* @returns {Array<{ id, name, changed?, hookPath?, configPath?, error? }>}
|
|
1325
|
+
*/
|
|
1326
|
+
function removeCaptureHooks(homeDir, clients) {
|
|
1327
|
+
if (!homeDir) throw new Error('removeCaptureHooks: homeDir is required');
|
|
1328
|
+
const results = [];
|
|
1329
|
+
for (const client of (clients || []).filter((c) => c && c.captureHook)) {
|
|
1330
|
+
try {
|
|
1331
|
+
results.push({ id: client.id, name: client.name, ...removeCaptureHook(client, homeDir) });
|
|
1332
|
+
} catch (err) {
|
|
1333
|
+
results.push({ id: client.id, name: client.name, error: err.message });
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return results;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// ─── SessionStart held-count notice (LW-18 layer 1b) ────────────────────────
|
|
1340
|
+
//
|
|
1341
|
+
// A structured Claude Code SessionStart hook that runs
|
|
1342
|
+
// scripts/review-notice.js (count-only, fail-silent, ≤1 notice per 4h — the
|
|
1343
|
+
// script owns all of that; see its header for the threat-model contract).
|
|
1344
|
+
// NOT a capture hook: it reads the account's pending count from the server
|
|
1345
|
+
// and never touches transcripts, so it installs at setup step 4 independent
|
|
1346
|
+
// of extraction consent (the review queue also holds MCP contributions).
|
|
1347
|
+
|
|
1348
|
+
/** Absolute path of the SessionStart notice shim. */
|
|
1349
|
+
function noticeShimPath(homeDir) {
|
|
1350
|
+
return path.join(binRootFor(homeDir), 'auxilo-review-notice.sh');
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/** Marker every notice entry carries (registration + removal + status probe). */
|
|
1354
|
+
function isAuxiloNoticeCommand(cmd) {
|
|
1355
|
+
return typeof cmd === 'string' && cmd.includes('auxilo-review-notice');
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/** Generate the notice shim body (fail-silent: always exits 0). */
|
|
1359
|
+
function renderNoticeShim(homeDir) {
|
|
1360
|
+
const noticePath = path.join(binRootFor(homeDir), 'scripts', 'review-notice.js');
|
|
1361
|
+
return `#!/bin/bash
|
|
1362
|
+
# Auxilo SessionStart review notice — generated by \`auxilo setup\` (LW-18).
|
|
1363
|
+
# Count-only pending-review notice; fail-silent (never blocks session start).
|
|
1364
|
+
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
1365
|
+
/usr/bin/env node ${shellQuote(noticePath)} 2>/dev/null
|
|
1366
|
+
exit 0
|
|
1367
|
+
`;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Register the SessionStart notice: write the shim (0755) and patch
|
|
1372
|
+
* <home>/.claude/settings.json hooks.SessionStart[] to contain exactly one
|
|
1373
|
+
* STRUCTURED Auxilo entry ({hooks:[{type:'command',command}]}) — bare-string
|
|
1374
|
+
* entries are silently ignored by Claude Code (the LW-17 0.8.1 lesson).
|
|
1375
|
+
* Idempotent; stale auxilo-review-notice references replaced; non-Auxilo
|
|
1376
|
+
* SessionStart hooks preserved; malformed settings throws (B15).
|
|
1377
|
+
*
|
|
1378
|
+
* @returns {{ changed: boolean, hookCmd: string }}
|
|
1379
|
+
*/
|
|
1380
|
+
function registerClaudeCodeSessionStartNotice(homeDir) {
|
|
1381
|
+
if (!homeDir) throw new Error('registerClaudeCodeSessionStartNotice: homeDir is required');
|
|
1382
|
+
const settingsPath = path.join(homeDir, '.claude', 'settings.json');
|
|
1383
|
+
const hookCmd = noticeShimPath(homeDir);
|
|
1384
|
+
|
|
1385
|
+
// Shim first (its own idempotency: rewritten only when stale).
|
|
1386
|
+
const body = renderNoticeShim(homeDir);
|
|
1387
|
+
let shimChanged = false;
|
|
1388
|
+
if (!fs.existsSync(hookCmd) || fs.readFileSync(hookCmd, 'utf-8') !== body) {
|
|
1389
|
+
fs.mkdirSync(path.dirname(hookCmd), { recursive: true });
|
|
1390
|
+
fs.writeFileSync(hookCmd, body);
|
|
1391
|
+
fs.chmodSync(hookCmd, 0o755);
|
|
1392
|
+
shimChanged = true;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const settings = readClientConfig(settingsPath);
|
|
1396
|
+
if (!settings.hooks) settings.hooks = {};
|
|
1397
|
+
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
1398
|
+
|
|
1399
|
+
const kept = [];
|
|
1400
|
+
let alreadyExact = false;
|
|
1401
|
+
for (const entry of settings.hooks.SessionStart) {
|
|
1402
|
+
if (typeof entry === 'string' && isAuxiloNoticeCommand(entry)) continue; // dead bare-string form
|
|
1403
|
+
if (entry && typeof entry === 'object' && Array.isArray(entry.hooks)) {
|
|
1404
|
+
const ours = entry.hooks.filter((h) => h && isAuxiloNoticeCommand(h.command));
|
|
1405
|
+
if (ours.length > 0) {
|
|
1406
|
+
if (entry.hooks.length === 1 && ours[0].command === hookCmd && ours[0].type === 'command') {
|
|
1407
|
+
alreadyExact = true;
|
|
1408
|
+
}
|
|
1409
|
+
const rest = entry.hooks.filter((h) => !(h && isAuxiloNoticeCommand(h.command)));
|
|
1410
|
+
if (rest.length > 0) kept.push({ ...entry, hooks: rest });
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
kept.push(entry);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (alreadyExact && kept.length === settings.hooks.SessionStart.length - 1) {
|
|
1418
|
+
return { changed: shimChanged, hookCmd };
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
settings.hooks.SessionStart = [...kept, { hooks: [{ type: 'command', command: hookCmd }] }];
|
|
1422
|
+
writeJsonAtomic(settingsPath, settings);
|
|
1423
|
+
return { changed: true, hookCmd };
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Read-only probe for `auxilo status`: notice counts as registered when the
|
|
1428
|
+
* shim exists AND a STRUCTURED SessionStart entry references it. Never throws.
|
|
1429
|
+
*/
|
|
1430
|
+
function sessionStartNoticeRegistered(homeDir) {
|
|
1431
|
+
try {
|
|
1432
|
+
if (!fs.existsSync(noticeShimPath(homeDir))) return false;
|
|
1433
|
+
const settings = JSON.parse(
|
|
1434
|
+
fs.readFileSync(path.join(homeDir, '.claude', 'settings.json'), 'utf-8')
|
|
1435
|
+
);
|
|
1436
|
+
return Array.isArray(settings.hooks && settings.hooks.SessionStart) &&
|
|
1437
|
+
settings.hooks.SessionStart.some((e) =>
|
|
1438
|
+
e && typeof e === 'object' && Array.isArray(e.hooks) &&
|
|
1439
|
+
e.hooks.some((h) => h && isAuxiloNoticeCommand(h.command)));
|
|
1440
|
+
} catch {
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1076
1445
|
// ─── Consent (spec §LW-12 step 5; server.js POST /extract/consent) ──────────
|
|
1077
1446
|
|
|
1078
1447
|
/**
|
|
@@ -1222,6 +1591,8 @@ async function getStatus(homeDir, opts = {}) {
|
|
|
1222
1591
|
runnerInstalled,
|
|
1223
1592
|
hookInstalled: fs.existsSync(hookPath),
|
|
1224
1593
|
hookRegistered,
|
|
1594
|
+
// LW-18 layer 1b: SessionStart held-count notice
|
|
1595
|
+
noticeRegistered: sessionStartNoticeRegistered(homeDir),
|
|
1225
1596
|
lastSweep,
|
|
1226
1597
|
pendingCount,
|
|
1227
1598
|
};
|
|
@@ -1315,17 +1686,26 @@ module.exports = {
|
|
|
1315
1686
|
credentialsPath,
|
|
1316
1687
|
readCredentials,
|
|
1317
1688
|
writeCredentials,
|
|
1689
|
+
writeEnvFile,
|
|
1318
1690
|
deviceLogin,
|
|
1319
1691
|
binRootFor,
|
|
1320
1692
|
hookScriptPathFor,
|
|
1321
1693
|
renderHookScript,
|
|
1322
1694
|
installRunner,
|
|
1323
1695
|
registerClaudeCodeHook,
|
|
1696
|
+
removeClaudeCodeHook,
|
|
1324
1697
|
captureShimPath,
|
|
1325
1698
|
renderCaptureShim,
|
|
1699
|
+
shellQuote,
|
|
1326
1700
|
registerCaptureHook,
|
|
1327
1701
|
captureHookRegistered,
|
|
1328
1702
|
installCaptureHooks,
|
|
1703
|
+
removeCaptureHook,
|
|
1704
|
+
removeCaptureHooks,
|
|
1705
|
+
noticeShimPath,
|
|
1706
|
+
renderNoticeShim,
|
|
1707
|
+
registerClaudeCodeSessionStartNotice,
|
|
1708
|
+
sessionStartNoticeRegistered,
|
|
1329
1709
|
recordConsent,
|
|
1330
1710
|
sentinelPath,
|
|
1331
1711
|
sentinelPresent,
|
package/mcp-server.js
CHANGED
|
@@ -160,7 +160,7 @@ async function postBulkChunks(headers, decisions) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
const server = new Server(
|
|
163
|
-
{ name: 'auxilo', version: '0.9.
|
|
163
|
+
{ name: 'auxilo', version: '0.9.3' },
|
|
164
164
|
{
|
|
165
165
|
capabilities: { tools: {} },
|
|
166
166
|
instructions: `You are connected to Auxilo, a knowledge marketplace where AI agents buy and sell operational learnings.
|
|
@@ -295,7 +295,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
295
295
|
},
|
|
296
296
|
{
|
|
297
297
|
name: 'auxilo_rate',
|
|
298
|
-
description: 'Rate a learning 1-5 after using it. WHEN TO USE: After you unlock and apply knowledge from auxilo_unlock, always come back and rate it. Your rating helps other agents find the best knowledge and deprioritizes low-quality submissions. This is how the marketplace stays useful. Free.',
|
|
298
|
+
description: 'Rate a learning 1-5 after using it. WHEN TO USE: After you unlock and apply knowledge from auxilo_unlock, always come back and rate it. Your rating helps other agents find the best knowledge and deprioritizes low-quality submissions. This is how the marketplace stays useful. Free. REQUIRES: your API key (run `npx auxilo setup` if unset) and a prior unlock of this learning by your account — only verified purchasers can rate (LW-7).',
|
|
299
299
|
inputSchema: {
|
|
300
300
|
type: 'object',
|
|
301
301
|
properties: {
|
|
@@ -736,12 +736,12 @@ if (require.main !== module) {
|
|
|
736
736
|
return;
|
|
737
737
|
}
|
|
738
738
|
|
|
739
|
-
// ─── CLI delegation (LW-17)
|
|
740
|
-
// `npx auxilo-mcp setup|status|review|disable` runs the full turnkey CLI.
|
|
739
|
+
// ─── CLI delegation (LW-17; init added Wave 3.4/NF-3) ─────────────────────────
|
|
740
|
+
// `npx auxilo-mcp setup|init|status|review|disable` runs the full turnkey CLI.
|
|
741
741
|
// npx resolves PACKAGE names, not bin aliases — the documented `npx auxilo
|
|
742
742
|
// setup` 404s until the `auxilo` npm package name is claimed, so the package
|
|
743
743
|
// bin must handle these commands itself.
|
|
744
|
-
if (['setup', 'status', 'review', 'disable'].includes(process.argv[2])) {
|
|
744
|
+
if (['setup', 'init', 'status', 'review', 'disable'].includes(process.argv[2])) {
|
|
745
745
|
require('./bin/auxilo-cli.js').run();
|
|
746
746
|
return; // module-level return in CommonJS stops the MCP server from starting
|
|
747
747
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auxilo-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"mcpName": "io.github.silent-architects/auxilo",
|
|
5
5
|
"description": "MCP server for Auxilo. Your agent stops solving the same problem twice: auto-extracted learnings, free self-unlocks, and earnings when other agents unlock yours.",
|
|
6
6
|
"main": "mcp-server.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"scripts/runner.js",
|
|
18
18
|
"scripts/capture-core.js",
|
|
19
19
|
"scripts/extract-local.js",
|
|
20
|
+
"scripts/review-notice.js",
|
|
20
21
|
"scripts/sources/",
|
|
21
22
|
"scripts/hooks/auxilo-extract.sh",
|
|
22
23
|
"README.md",
|
|
@@ -49,6 +50,8 @@
|
|
|
49
50
|
"homepage": "https://auxilo.io",
|
|
50
51
|
"license": "MIT",
|
|
51
52
|
"dependencies": {
|
|
53
|
+
"@aws-sdk/client-s3": "^3.1090.0",
|
|
54
|
+
"@aws-sdk/lib-storage": "^3.1090.0",
|
|
52
55
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
53
56
|
"stripe": "^17.0.0",
|
|
54
57
|
"viem": "^2.46.3"
|
|
@@ -56,4 +59,4 @@
|
|
|
56
59
|
"devDependencies": {
|
|
57
60
|
"proxyquire": "^2.1.3"
|
|
58
61
|
}
|
|
59
|
-
}
|
|
62
|
+
}
|