auxilo-mcp 0.9.1 → 0.9.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.
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+ /*
3
+ * scripts/extract-local.js — CLIENT-SIDE learning extraction using the user's OWN model.
4
+ *
5
+ * Auxilo does not pay to extract. At session end, the agent that just did the work
6
+ * (itself an LLM) extracts reusable learnings AND self-screens them for sensitivity,
7
+ * using the local client's model (`claude -p` for Claude Code). Finished learnings are
8
+ * then submitted to POST /learn by the runner. Zero cost to Auxilo.
9
+ *
10
+ * RECURSION SAFETY: `claude -p` starts a headless Claude Code turn, which will itself
11
+ * fire the SessionEnd hook. We set AUXILO_EXTRACTING=1 on the child env so that child's
12
+ * runner no-ops immediately (runner.js checks it). runner.js also sets it in-process
13
+ * before we're called; we set it on the child explicitly as belt-and-suspenders.
14
+ */
15
+ const { spawnSync } = require('child_process');
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+
20
+ const CATEGORIES = ['data-processing', 'web-interaction', 'code-execution', 'communication', 'storage-state', 'content-generation', 'payment-financial', 'monitoring'];
21
+
22
+ const EXTRACTION_PROMPT = `You are extracting reusable OPERATIONAL LEARNINGS from an AI agent's session transcript, to publish to a PUBLIC knowledge marketplace read by other AI agents.
23
+
24
+ Extract 0 to 5 GENUINE learnings: non-obvious solutions, workarounds, API quirks, error root-causes, integration gotchas — the kind of thing that cost real debugging or combined multiple sources. SKIP trivial lookups, well-documented standard approaches, opinions, and conversation.
25
+
26
+ MANDATORY SENSITIVITY SELF-SCREEN (the marketplace is PUBLIC): NEVER include secrets, credentials, API keys, tokens, private keys, or seed phrases; personal data (real people's names, emails, phone numbers, wallet addresses); private filesystem paths, internal hostnames, or infrastructure identifiers; proprietary, confidential, or client-specific business content. Rewrite specifics into generic placeholders (/Users/USER/..., API_KEY, "a client") or omit them. If a learning cannot be generalized without leaking private material, DROP it entirely.
27
+
28
+ Output STRICT JSON ONLY — an array (possibly empty []) of objects with these keys:
29
+ "title": concise, >= 10 chars
30
+ "body": >= 50 chars — what was tried, what worked, what failed
31
+ "category": one of ${JSON.stringify(CATEGORIES)}
32
+ "tags": array of lowercase keyword strings
33
+ "task_context": one sentence describing the task
34
+ "outcome": one of "success","partial","failure","workaround"
35
+ No prose, no explanation, no markdown code fences — just the raw JSON array.
36
+
37
+ TRANSCRIPT:
38
+ `;
39
+
40
+ /** Resolve the `claude` binary — hook/launchd env may have a minimal PATH. */
41
+ function resolveClaudeBin() {
42
+ const candidates = [
43
+ 'claude',
44
+ path.join(os.homedir(), '.claude', 'local', 'claude'),
45
+ '/usr/local/bin/claude',
46
+ '/opt/homebrew/bin/claude',
47
+ path.join(os.homedir(), '.local', 'bin', 'claude'),
48
+ ];
49
+ for (const c of candidates) {
50
+ try {
51
+ if (c === 'claude') return c; // let PATH resolve it; spawn will ENOENT if absent
52
+ if (fs.existsSync(c)) return c;
53
+ } catch (_) { /* ignore */ }
54
+ }
55
+ return 'claude';
56
+ }
57
+
58
+ /**
59
+ * Invoke the local Claude Code model headlessly (uses the USER's subscription auth).
60
+ * Prompt+transcript go via stdin. Returns { ok, out, reason } — never throws, so the
61
+ * SessionEnd hook degrades gracefully (the proactive auxilo_contribute path is the
62
+ * reliable primary; this deterministic hook is best-effort).
63
+ */
64
+ function extractWithClaudeCode(transcript) {
65
+ const bin = resolveClaudeBin();
66
+ const input = EXTRACTION_PROMPT + String(transcript).slice(0, 200000);
67
+ // Do NOT pass ANTHROPIC_API_KEY through — we want the user's logged-in Claude
68
+ // subscription (OAuth), not an API key (which would bill someone). Delete it.
69
+ const childEnv = { ...process.env, AUXILO_EXTRACTING: '1' };
70
+ delete childEnv.ANTHROPIC_API_KEY;
71
+ const res = spawnSync(bin, ['-p'], { input, encoding: 'utf-8', env: childEnv, timeout: 120000, maxBuffer: 20 * 1024 * 1024 });
72
+ const out = String(res.stdout || '');
73
+ if (res.error) return { ok: false, out: '', reason: `spawn failed (${bin}): ${res.error.message}` };
74
+ // Claude prints auth failures ("API Error: 401 ... Please run /login") to stdout.
75
+ if (/Please run \/login|authentication_error|401/i.test(out) || /Please run \/login|authentication_error/i.test(String(res.stderr || ''))) {
76
+ return { ok: false, out, reason: 'local model not authenticated in this context (run `claude` and /login once); skipping deterministic extraction' };
77
+ }
78
+ if (res.status !== 0) return { ok: false, out, reason: `local model exited ${res.status}: ${(out || String(res.stderr || '')).slice(0, 160)}` };
79
+ return { ok: true, out, reason: null };
80
+ }
81
+
82
+ /** Defensively parse a JSON array of learnings out of model output. */
83
+ function parseLearnings(raw) {
84
+ let s = String(raw || '').trim();
85
+ const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/i);
86
+ if (fence) s = fence[1].trim();
87
+ const start = s.indexOf('[');
88
+ const end = s.lastIndexOf(']');
89
+ if (start === -1 || end === -1 || end < start) return [];
90
+ let arr;
91
+ try { arr = JSON.parse(s.slice(start, end + 1)); } catch (_) { return []; }
92
+ if (!Array.isArray(arr)) return [];
93
+ return arr
94
+ .filter(l => l && typeof l.title === 'string' && typeof l.body === 'string' && l.title.length >= 10 && l.body.length >= 50)
95
+ .map(l => ({
96
+ title: l.title,
97
+ body: l.body,
98
+ category: CATEGORIES.includes(l.category) ? l.category : 'code-execution',
99
+ tags: Array.isArray(l.tags) ? l.tags.slice(0, 8).map(String) : [],
100
+ task_context: typeof l.task_context === 'string' ? l.task_context : '',
101
+ outcome: ['success', 'partial', 'failure', 'workaround'].includes(l.outcome) ? l.outcome : 'success',
102
+ }));
103
+ }
104
+
105
+ /**
106
+ * Extract learnings locally. Returns { learnings: [...] } or { learnings: [], skipped }.
107
+ * Only claude-code has a local extractor today; other clients rely on the agent's
108
+ * proactive auxilo_contribute (MCP) call.
109
+ */
110
+ async function extractLocally(transcript, sourceType) {
111
+ if (sourceType && sourceType !== 'claude-code') {
112
+ return { learnings: [], skipped: `local extraction not implemented for "${sourceType}" — agent contributes via auxilo_contribute` };
113
+ }
114
+ const { ok, out, reason } = extractWithClaudeCode(transcript);
115
+ if (!ok) return { learnings: [], skipped: reason };
116
+ return { learnings: parseLearnings(out) };
117
+ }
118
+
119
+ module.exports = { extractLocally, parseLearnings, resolveClaudeBin, EXTRACTION_PROMPT, CATEGORIES };
package/scripts/runner.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * Transport layer for the autonomous extraction pipeline:
7
7
  * 1. Check kill-switch sentinel + recursion guard (A5.2)
8
8
  * 2. Enumerate active sources via TranscriptSource interface (A5.1)
9
- * 3. For each new session: readSession() → client-side scrub → write queue file → POST /extract
9
+ * 3. For each new session: readSession() → client-side scrub → write queue file → local extraction → POST /learn
10
10
  * 4. On success: update ledger, delete queue file. On failure: leave queue file.
11
11
  *
12
12
  * Usage:
@@ -23,7 +23,7 @@
23
23
  * node scripts/runner.js --force # ignore ledger high-water
24
24
  *
25
25
  * Environment:
26
- * AUXILO_API_KEY — API key for authenticated /extract calls (REQUIRED unless --dry-run)
26
+ * AUXILO_API_KEY — API key for authenticated /learn submissions (REQUIRED unless --dry-run)
27
27
  * AUXILO_BASE_URL — Server URL (default: http://localhost:49152)
28
28
  * AUXILO_EXTRACTING — Recursion guard (A5.2); set to "1" by this runner
29
29
  *
@@ -186,7 +186,7 @@ function ledgerMark(ledger, sourceId, sessionId, sha, mtime) {
186
186
  let queueCounter = Date.now();
187
187
 
188
188
  /**
189
- * Write a queue file BEFORE POSTing to /extract.
189
+ * Write a queue file BEFORE local extraction and the /learn submissions.
190
190
  * B6: Uses O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW with mode 0o600 to prevent
191
191
  * symlink attacks on the pending-learnings directory.
192
192
  *
@@ -359,7 +359,8 @@ function installHooks() {
359
359
  // ~/Documents would still be TCC-blocked) — and point the LaunchAgent plist
360
360
  // at the installed copy. Re-run after changing any of these files.
361
361
 
362
- const SWEEPER_LABEL = 'tech.conway.auxilo-sweeper';
362
+ // Renamed 2026-07-15: the original label carried a dead pre-Fly host prefix (legacy-label purge).
363
+ const SWEEPER_LABEL = 'io.auxilo.sweeper';
363
364
 
364
365
  function installSweeper() {
365
366
  const repoRoot = path.resolve(__dirname, '..');
@@ -441,7 +442,7 @@ function installSweeper() {
441
442
 
442
443
  // ─── Install Daily Digest (P1-13 follow-up) ─────────────────────────────────
443
444
  //
444
- // Same TCC root cause as the sweeper: the original tech.conway.auxilo-digest
445
+ // Same TCC root cause as the sweeper: the original digest
445
446
  // plist ran node against jobs/daily-digest.js under ~/Documents, which launchd
446
447
  // cannot read ("Unknown system error -11" in daily-digest.stderr.log). The
447
448
  // digest is a purely local job — it summarizes ~/.auxilo/extract.log — so the
@@ -449,7 +450,8 @@ function installSweeper() {
449
450
  // daily-digest.js is self-contained (fs/path/os only), so it is the only file
450
451
  // to copy. Re-run after changing it.
451
452
 
452
- const DIGEST_LABEL = 'tech.conway.auxilo-digest';
453
+ // Renamed 2026-07-15: the original label carried a dead pre-Fly host prefix (legacy-label purge).
454
+ const DIGEST_LABEL = 'io.auxilo.digest';
453
455
 
454
456
  function installDigest() {
455
457
  const repoRoot = path.resolve(__dirname, '..');
@@ -726,7 +728,7 @@ async function main() {
726
728
  }
727
729
 
728
730
  if (args.dryRun) {
729
- log(`[runner] [DRY RUN] Would upload ${cleaned.length} chars to ${BASE_URL}/extract`);
731
+ log(`[runner] [DRY RUN] Would extract locally from ${cleaned.length} scrubbed chars, then submit finished learnings to ${BASE_URL}/learn`);
730
732
  process.exit(0);
731
733
  }
732
734
 
@@ -849,7 +851,7 @@ async function main() {
849
851
  }
850
852
 
851
853
  if (args.dryRun) {
852
- log(`[runner] [DRY RUN] Would upload ${cleaned.length} chars to ${BASE_URL}/extract`);
854
+ log(`[runner] [DRY RUN] Would extract locally from ${cleaned.length} scrubbed chars, then submit finished learnings to ${BASE_URL}/learn`);
853
855
  totalProcessed++;
854
856
  continue;
855
857
  }