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.
- package/README.md +130 -149
- package/bin/auxilo-cli.js +230 -68
- package/lib/installer.js +5 -1
- package/lib/review.js +170 -3
- package/mcp-server.js +243 -146
- package/package.json +11 -2
- package/scripts/extract-local.js +119 -0
- package/scripts/runner.js +10 -8
|
@@ -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 /
|
|
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 /
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|