auxilo-mcp 0.8.1 → 0.8.2

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
@@ -26,13 +26,25 @@ const HOME = os.homedir();
26
26
 
27
27
  // ─── Prompt helpers ─────────────────────────────────────────────────────────
28
28
 
29
+ // LW-17: ONE shared readline interface for the whole run. Creating a fresh
30
+ // interface per question loses any input readline already buffered when the
31
+ // previous interface closed — with piped/scripted stdin the answer to
32
+ // question N+1 arrives with question N's buffer and the next prompt hangs
33
+ // on EOF (the 0.8.1 consent step silently never completed).
34
+ let sharedRl = null;
35
+ function getRl() {
36
+ if (!sharedRl) {
37
+ sharedRl = readline.createInterface({ input: process.stdin, output: process.stdout });
38
+ }
39
+ return sharedRl;
40
+ }
41
+ function closeRl() {
42
+ if (sharedRl) { sharedRl.close(); sharedRl = null; }
43
+ }
44
+
29
45
  function ask(question) {
30
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
31
46
  return new Promise((resolve) => {
32
- rl.question(question, (answer) => {
33
- rl.close();
34
- resolve(answer.trim());
35
- });
47
+ getRl().question(question, (answer) => resolve(answer.trim()));
36
48
  });
37
49
  }
38
50
 
@@ -415,11 +427,21 @@ async function main() {
415
427
  }
416
428
  }
417
429
 
418
- if (require.main === module) {
419
- main().catch((err) => {
430
+ /** Run the CLI: used by require.main below AND by mcp-server.js delegation
431
+ * (LW-17: `npx auxilo-mcp setup` — the `auxilo` bin alias is unreachable via
432
+ * npx until the `auxilo` npm package name is claimed). */
433
+ function run() {
434
+ main().then(() => {
435
+ closeRl(); // an open readline interface keeps the process alive
436
+ }).catch((err) => {
437
+ closeRl();
420
438
  console.error(`auxilo: ${err.message}`);
421
439
  process.exit(1);
422
440
  });
423
441
  }
424
442
 
425
- module.exports = { parseFlags, resolveBaseUrl };
443
+ if (require.main === module) {
444
+ run();
445
+ }
446
+
447
+ module.exports = { parseFlags, resolveBaseUrl, run };
package/lib/installer.js CHANGED
@@ -246,8 +246,11 @@ async function deviceLogin(opts = {}) {
246
246
  });
247
247
  if (!res.ok) throw new Error(`Device code request failed (HTTP ${res.status})`);
248
248
  const device = await res.json();
249
- const { user_code: userCode, verification_url: verificationUrl } = device;
249
+ // A-1: device_code is the secret polling credential; user_code is the human
250
+ // code shown on the verification page only.
251
+ const { user_code: userCode, device_code: deviceCode, verification_url: verificationUrl } = device;
250
252
  if (!userCode) throw new Error('Device code request failed: no user_code in response');
253
+ if (!deviceCode) throw new Error('Device code request failed: no device_code in response');
251
254
 
252
255
  // Server returns interval in seconds (default 5 per server.js /auth/device).
253
256
  const intervalMs = (device.interval || 5) * 1000;
@@ -259,7 +262,7 @@ async function deviceLogin(opts = {}) {
259
262
  await sleep(intervalMs);
260
263
  let status;
261
264
  try {
262
- const poll = await fetchImpl(`${baseUrl}/auth/device/status?code=${encodeURIComponent(userCode)}`);
265
+ const poll = await fetchImpl(`${baseUrl}/auth/device/status?device_code=${encodeURIComponent(deviceCode)}`);
263
266
  status = await poll.json();
264
267
  } catch {
265
268
  continue; // transient network error during poll — keep polling
@@ -338,7 +341,10 @@ LOG="${auxiloDir}/extract.log"
338
341
  # P1-13 lesson: do NOT set AUXILO_EXTRACTING here — runner.js's own recursion
339
342
  # guard would trip and silently no-op every run. The runner sets it itself
340
343
  # for child processes.
341
- nohup /usr/bin/env node "$RUNNER" --transcript "$transcript_path" >> "$LOG" 2>&1 &
344
+ # LW-17: stdout goes to /dev/null runner.js log() already appends every
345
+ # line to extract.log itself; redirecting stdout there too double-wrote each
346
+ # line and the daily digest double-counted. stderr still captured for crashes.
347
+ nohup /usr/bin/env node "$RUNNER" --transcript "$transcript_path" > /dev/null 2>> "$LOG" &
342
348
 
343
349
  exit 0
344
350
  `;
@@ -385,14 +391,27 @@ function installRunner(homeDir, opts = {}) {
385
391
 
386
392
  // ─── Claude Code hook registration (spec §LW-12 step 4) ─────────────────────
387
393
 
394
+ /** True when an inner hook object is an Auxilo extraction command. */
395
+ function isAuxiloCommandHook(h) {
396
+ return h && typeof h === 'object' && typeof h.command === 'string' &&
397
+ h.command.includes('auxilo-extract');
398
+ }
399
+
388
400
  /**
389
401
  * Patch <home>/.claude/settings.json hooks.SessionEnd[] to contain exactly one
390
- * Auxilo entry: <home>/.auxilo/bin/auxilo-extract.sh.
402
+ * Auxilo entry: <home>/.auxilo/bin/auxilo-extract.sh, in Claude Code's
403
+ * STRUCTURED form ({ hooks: [{ type: 'command', command }] }).
404
+ *
405
+ * LW-17: 0.8.1 appended the command as a bare string — Claude Code silently
406
+ * ignores string entries, so the hook NEVER fired. It also only removed
407
+ * legacy entries that were strings, missing the pre-LW-12 structured entry
408
+ * (~/.claude/hooks/auxilo-extract.sh nested inside a matcher group).
391
409
  *
392
- * Idempotent. Replaces any legacy string entry referencing auxilo-extract.sh
393
- * (e.g. the pre-LW-12 ~/.claude/hooks/auxilo-extract.sh location). Non-Auxilo
394
- * entries including non-string structured entries are preserved untouched.
395
- * Malformed settings.json throws (B15) — never overwritten.
410
+ * Idempotent. Removes every Auxilo reference (bare strings, and command
411
+ * objects inside matcher groups preserving any non-Auxilo commands sharing
412
+ * the group), then appends one canonical structured entry. Non-Auxilo entries
413
+ * are preserved untouched. Malformed settings.json throws (B15) — never
414
+ * overwritten.
396
415
  *
397
416
  * @returns {{ changed: boolean, hookCmd: string, removedLegacy: string[] }}
398
417
  */
@@ -406,21 +425,44 @@ function registerClaudeCodeHook(homeDir) {
406
425
  if (!Array.isArray(settings.hooks.SessionEnd)) settings.hooks.SessionEnd = [];
407
426
 
408
427
  const before = settings.hooks.SessionEnd;
409
- const removedLegacy = before.filter(
410
- (h) => typeof h === 'string' && h.includes('auxilo-extract') && h !== hookCmd
411
- );
412
- const kept = before.filter(
413
- (h) => !(typeof h === 'string' && h.includes('auxilo-extract'))
414
- );
415
-
416
- const alreadyExact = before.includes(hookCmd) && removedLegacy.length === 0;
428
+ const removedLegacy = [];
429
+ const kept = [];
430
+
431
+ for (const entry of before) {
432
+ if (typeof entry === 'string' && entry.includes('auxilo-extract')) {
433
+ removedLegacy.push(entry); // includes 0.8.1's dead bare-string entry
434
+ continue;
435
+ }
436
+ if (entry && typeof entry === 'object' && Array.isArray(entry.hooks)) {
437
+ const auxilo = entry.hooks.filter(isAuxiloCommandHook);
438
+ if (auxilo.length > 0) {
439
+ removedLegacy.push(...auxilo.map((h) => h.command));
440
+ const rest = entry.hooks.filter((h) => !isAuxiloCommandHook(h));
441
+ if (rest.length > 0) kept.push({ ...entry, hooks: rest });
442
+ continue;
443
+ }
444
+ }
445
+ kept.push(entry);
446
+ }
447
+
448
+ const canonical = { hooks: [{ type: 'command', command: hookCmd }] };
449
+
450
+ // Idempotency: unchanged iff the only Auxilo reference found was already
451
+ // the canonical command in a structured single-command entry.
452
+ const alreadyExact =
453
+ removedLegacy.length === 1 && removedLegacy[0] === hookCmd &&
454
+ before.some((e) =>
455
+ e && typeof e === 'object' && Array.isArray(e.hooks) &&
456
+ e.hooks.length === 1 && isAuxiloCommandHook(e.hooks[0]) &&
457
+ e.hooks[0].command === hookCmd && e.hooks[0].type === 'command'
458
+ );
417
459
  if (alreadyExact) {
418
460
  return { changed: false, hookCmd, removedLegacy: [] };
419
461
  }
420
462
 
421
- settings.hooks.SessionEnd = [...kept, hookCmd];
463
+ settings.hooks.SessionEnd = [...kept, canonical];
422
464
  writeJsonAtomic(settingsPath, settings);
423
- return { changed: true, hookCmd, removedLegacy };
465
+ return { changed: true, hookCmd, removedLegacy: removedLegacy.filter((c) => c !== hookCmd) };
424
466
  }
425
467
 
426
468
  // ─── Consent (spec §LW-12 step 5; server.js POST /extract/consent) ──────────
@@ -541,8 +583,12 @@ async function getStatus(homeDir, opts = {}) {
541
583
  const settings = JSON.parse(
542
584
  fs.readFileSync(path.join(homeDir, '.claude', 'settings.json'), 'utf-8')
543
585
  );
586
+ // LW-17: only STRUCTURED entries count — Claude Code silently ignores
587
+ // bare-string entries (the 0.8.1 dead-hook bug), so reporting one as
588
+ // "registered" would mask exactly the failure this status exists to catch.
544
589
  hookRegistered = Array.isArray(settings.hooks && settings.hooks.SessionEnd) &&
545
- settings.hooks.SessionEnd.some((h) => typeof h === 'string' && h.includes('auxilo-extract'));
590
+ settings.hooks.SessionEnd.some((h) =>
591
+ h && typeof h === 'object' && Array.isArray(h.hooks) && h.hooks.some(isAuxiloCommandHook));
546
592
  } catch { /* no settings */ }
547
593
 
548
594
  // 5. Last extraction + pending queue (ledger conventions from scripts/runner.js)
@@ -108,7 +108,13 @@ const PATTERNS = [
108
108
  // ── P2.1a new patterns (§7.6 — 10 additions) ─────────────────────────────
109
109
  {
110
110
  name: 'email_address',
111
- regex: /[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}/gi,
111
+ // Linearly-bounded form (D-1 ReDoS fix). The previous
112
+ // /[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}/ backtracked O(n^2) on a long run
113
+ // of dash/digit chars with no valid TLD, pinning the single-process event
114
+ // loop for minutes. Each quantifier here is explicitly bounded and the
115
+ // domain labels do not overlap with the dot separators, so there is no
116
+ // catastrophic-backtracking path.
117
+ regex: /[a-z0-9._%+\-]{1,64}@[a-z0-9](?:[a-z0-9\-]{0,253}[a-z0-9])?(?:\.[a-z]{2,24})+/gi,
112
118
  description: 'Email address',
113
119
  },
114
120
  {
package/mcp-server.js CHANGED
@@ -16,7 +16,9 @@ let credentials = {};
16
16
  try {
17
17
  credentials = JSON.parse(fs.readFileSync(CRED_PATH, 'utf8'));
18
18
  } catch { /* no credentials file — unauthenticated mode */ }
19
- const AUXILO_BASE = credentials.base_url || 'https://3000-725fa3fea775ba39db5a2e3703fa4557.life.conway.tech';
19
+ // LW-17: default was a long-dead Conway sandbox URL — fresh installs without
20
+ // credentials.json pointed every tool call at it.
21
+ const AUXILO_BASE = credentials.base_url || 'https://auxilo.io';
20
22
 
21
23
  function baseHeaders(extra = {}) {
22
24
  const headers = { 'Content-Type': 'application/json', ...extra };
@@ -27,7 +29,7 @@ function baseHeaders(extra = {}) {
27
29
  }
28
30
 
29
31
  const server = new Server(
30
- { name: 'auxilo', version: '0.7.0' },
32
+ { name: 'auxilo', version: '0.8.2' },
31
33
  {
32
34
  capabilities: { tools: {} },
33
35
  instructions: `You are connected to Auxilo, a knowledge marketplace where AI agents buy and sell operational learnings.
@@ -542,9 +544,18 @@ function text(obj) {
542
544
  return { content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }] };
543
545
  }
544
546
 
545
- // ─── CLI: setup command (Change 4) ─────────────────────────────────────────────
546
- // LW-12: superseded by the turnkey installer — kept for backward compatibility.
547
- if (process.argv[2] === 'setup') {
547
+ // ─── CLI delegation (LW-17) ────────────────────────────────────────────────────
548
+ // `npx auxilo-mcp setup|status|review|disable` runs the full turnkey CLI.
549
+ // npx resolves PACKAGE names, not bin aliases — the documented `npx auxilo
550
+ // setup` 404s until the `auxilo` npm package name is claimed, so the package
551
+ // bin must handle these commands itself.
552
+ if (['setup', 'status', 'review', 'disable'].includes(process.argv[2])) {
553
+ require('./bin/auxilo-cli.js').run();
554
+ return; // module-level return in CommonJS stops the MCP server from starting
555
+ }
556
+
557
+ // ─── CLI: legacy setup (Change 4, pre-LW-12) — now handled by delegation above ──
558
+ if (process.argv[2] === '__legacy_setup_unreachable__') {
548
559
  console.log('Note: `auxilo-mcp setup` is superseded by `npx auxilo setup` (interactive');
549
560
  console.log('install: MCP registration + login + background extraction). Continuing with');
550
561
  console.log('legacy MCP-registration-only setup...\n');
@@ -622,7 +633,9 @@ if (process.argv[2] === 'login') {
622
633
  process.exit(1);
623
634
  }
624
635
  const deviceData = await deviceResp.json();
625
- const { user_code, verification_url } = deviceData;
636
+ // A-1: device_code is the secret polling credential; user_code is only the
637
+ // human code shown on the verification page.
638
+ const { user_code, device_code, verification_url } = deviceData;
626
639
 
627
640
  // 2. Display code and URL
628
641
  console.log(`\nYour device code: ${user_code}`);
@@ -639,7 +652,7 @@ if (process.argv[2] === 'login') {
639
652
  for (let i = 0; i < maxAttempts; i++) {
640
653
  await new Promise(resolve => setTimeout(resolve, 5000));
641
654
  try {
642
- const statusResp = await fetch(`${AUXILO_BASE}/auth/device/status?code=${user_code}`);
655
+ const statusResp = await fetch(`${AUXILO_BASE}/auth/device/status?device_code=${encodeURIComponent(device_code)}`);
643
656
  const statusData = await statusResp.json();
644
657
 
645
658
  if (statusData.status === 'authorized') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auxilo-mcp",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "MCP server for Auxilo — Agent Capability Discovery & Knowledge Marketplace",
5
5
  "main": "mcp-server.js",
6
6
  "bin": {