auxilo-mcp 0.9.3 → 0.9.4

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 CHANGED
@@ -4,9 +4,9 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/auxilo-mcp)](https://www.npmjs.com/package/auxilo-mcp)
5
5
  [![license](https://img.shields.io/npm/l/auxilo-mcp)](LICENSE)
6
6
 
7
- Auxilo is an MCP server that auto-extracts operational learnings from your coding agent's sessions, gives your agent its own learnings back free in every later session, and lists them in a marketplace where other agents pay to unlock them.
7
+ Your agent already solved this. It found the fix, shipped, and lost it when the session ended. Next run it hits the same wall and burns the time and tokens you already paid for, while you sit and watch. Auxilo stops that. Your agent stops solving the same problem twice.
8
8
 
9
- Your agent stops solving the same problem twice. When another agent unlocks what yours figured out, you earn.
9
+ Auxilo is an MCP server that auto-extracts operational learnings from your coding agent's sessions, gives your agent its own learnings back in every later session, and lists them in a marketplace where other agents pay to unlock them. Your agent's own learnings always come back at $0. When another agent unlocks what yours figured out, you earn.
10
10
 
11
11
  ## The problem
12
12
 
@@ -92,7 +92,7 @@ The same block works anywhere MCP configs are read. The installer also detects C
92
92
 
93
93
  ## Tools
94
94
 
95
- 18 tools:
95
+ 17 tools:
96
96
 
97
97
  | Tool | What it does | Cost |
98
98
  |---|---|---|
@@ -105,7 +105,6 @@ The same block works anywhere MCP configs are read. The installer also detects C
105
105
  | `auxilo_skill` | Connection details, auth, and pricing for one skill | Free |
106
106
  | `auxilo_categories` | List categories with counts | Free |
107
107
  | `auxilo_stats` | Registry statistics | Free |
108
- | `get_stats` | Registry statistics, alias | Free |
109
108
  | `get_knowledge_stats` | Marketplace statistics | Free |
110
109
  | `auxilo_contributor` | Earnings for a contributor wallet | Free |
111
110
  | `auxilo_account_earnings` | Earnings and pending balance for your account | Free |
@@ -153,7 +152,7 @@ Unlocks (`GET /knowledge/:id`, minimum $0.05) are paid with [x402](https://www.x
153
152
 
154
153
  - OpenAPI spec: [auxilo.io/openapi.json](https://auxilo.io/openapi.json)
155
154
  - Agent discovery card: `https://auxilo.io/.well-known/agent.json`
156
- - Categories: data-processing, web-interaction, code-execution, communication, storage-state, content-generation, payment-financial, monitoring
155
+ - Categories: data-processing, web-interaction, code-execution, storage-state, payment-financial, monitoring. Learnings are technical-only — `communication` and `content-generation` are retired labels the server refuses (`CATEGORY_OUT_OF_SCOPE`); technical email/messaging-API learnings belong under web-interaction or code-execution.
157
156
 
158
157
  ## Privacy
159
158
 
@@ -18,7 +18,9 @@
18
18
  // ─── Version ─────────────────────────────────────────────────────────────────
19
19
  // Bumped when patterns are added or behavior changes. Server rejects extractions
20
20
  // from clients older than N-1 (§7.6).
21
- const SENSITIVITY_FILTER_VERSION = '0.4.0';
21
+ // 0.5.0 (task-#19, 2026-07-19): Google Drive/Docs/Sheets/Slides file-ID
22
+ // patterns — a private Drive doc ID survived the scrubber in the wild.
23
+ const SENSITIVITY_FILTER_VERSION = '0.5.0';
22
24
 
23
25
  // ─── Patterns ────────────────────────────────────────────────────────────────
24
26
  // Each pattern has a name, regex, and description for the rejection message.
@@ -165,6 +167,40 @@ const PATTERNS = [
165
167
  regex: /[MN][A-Za-z\d]{23}\.[\w-]{6}\.[\w-]{27}/g,
166
168
  description: 'Discord bot token',
167
169
  },
170
+ // ── task-#19 (2026-07-19): Google Drive file IDs ──────────────────────────
171
+ // Found live: a private Google Drive doc ID survived the scrubber. A Drive
172
+ // ID is a capability reference — a link-shared doc is readable by ANYONE
173
+ // holding the ID — so it is credentials-class, not merely PII-class.
174
+ {
175
+ // The /d/<id>/ URL shapes across the editors + Drive, plus the id= and
176
+ // folders/ forms. Precise (requires the google.com host shape), so it
177
+ // fires unconditionally — a Drive URL in a public learning is never a
178
+ // false positive worth waving through.
179
+ name: 'google_drive_url',
180
+ regex: /(?:docs|drive|sheets|slides)\.google\.com\/(?:(?:document|spreadsheets|presentation|forms|drawings|file)\/(?:u\/\d+\/)?d\/[A-Za-z0-9_-]{20,}|open\?[^\s]*\bid=[A-Za-z0-9_-]{20,}|uc\?[^\s]*\bid=[A-Za-z0-9_-]{20,}|drive\/(?:u\/\d+\/)?folders\/[A-Za-z0-9_-]{20,}|folderview\?[^\s]*\bid=[A-Za-z0-9_-]{20,})/g,
181
+ description: 'Google Drive/Docs/Sheets/Slides URL exposing a file ID',
182
+ },
183
+ {
184
+ // Bare Drive-ID heuristic, GUARDED (this filter is a hard 422 at /learn,
185
+ // so a loose rule bounces legit content). Shape: modern IDs start with
186
+ // '1' (33/44 chars), legacy folder IDs with '0B'; charset base64url.
187
+ // Gate-A F3: the validate hook requires MIXED CASE + a digit beyond the
188
+ // prefix char. The original uppercase/-/_ guard still 422'd six real FP
189
+ // classes the reviewer probed: issue-numbered branch names
190
+ // (1234-fix-the-thing…), numeric-leading URL slugs (10-ways-to-…),
191
+ // digit-1-leading UUIDs (~6% of v4s), numeric-separator literals
192
+ // (1_000_000_…), kebab date-ranges (19-07-2026-to-…), and UPPERCASE
193
+ // 40-hex SHAs. Mixed-case+digit excludes every one (branch/slug/UUID/
194
+ // date-range have no uppercase; separator literals no lowercase;
195
+ // uppercase SHAs no lowercase) while keeping real Drive IDs: for 32
196
+ // random base64url chars P(no lowercase)≈P(no uppercase)≈6e-8 and
197
+ // P(no digit)≈(54/64)^32≈0.4% — the URL rule above still catches every
198
+ // linked form regardless.
199
+ name: 'google_drive_id',
200
+ regex: /(?<![A-Za-z0-9_-])(?:1|0B)[A-Za-z0-9_-]{24,63}(?![A-Za-z0-9_-])/g,
201
+ description: 'Bare Google Drive file ID (25+ char base64url, 1/0B prefix)',
202
+ validate: (m) => /[A-Z]/.test(m) && /[a-z]/.test(m) && /\d/.test(m.slice(1)),
203
+ },
168
204
  ];
169
205
 
170
206
  // ─── M-2: /g flag invariant assertion at module load ────────────────────────
@@ -220,6 +256,11 @@ function scanLearning(learning) {
220
256
  if (m[0].length <= 42) continue;
221
257
  }
222
258
 
259
+ // task-#19: optional per-pattern validate hook — a match the hook
260
+ // refuses is skipped (used to guard heuristic patterns whose raw
261
+ // regex would over-fire, e.g. the bare Drive-ID rule).
262
+ if (pattern.validate && !pattern.validate(m[0])) continue;
263
+
223
264
  matches.push({
224
265
  pattern: pattern.name,
225
266
  field: fieldName,
@@ -280,6 +321,9 @@ function getRedactionHint(patternName) {
280
321
  openai_project_key: '{OPENAI_KEY}',
281
322
  anthropic_key: '{ANTHROPIC_KEY}',
282
323
  discord_bot_token: '{DISCORD_TOKEN}',
324
+ // task-#19 patterns
325
+ google_drive_url: 'https://docs.google.com/document/d/{DRIVE_FILE_ID}/',
326
+ google_drive_id: '{DRIVE_FILE_ID}',
283
327
  };
284
328
  return hints[patternName] || '{REDACTED}';
285
329
  }
@@ -345,6 +389,9 @@ function scanText(text) {
345
389
  // Private key length check (same as scanLearning)
346
390
  if (pattern.name === 'private_key' && m[0].length <= 42) continue;
347
391
 
392
+ // task-#19: per-pattern validate hook (same semantics as scanLearning).
393
+ if (pattern.validate && !pattern.validate(m[0])) continue;
394
+
348
395
  matchList.push({
349
396
  pattern: pattern.name,
350
397
  match: redactMatch(m[0]),
package/mcp-server.js CHANGED
@@ -160,14 +160,16 @@ async function postBulkChunks(headers, decisions) {
160
160
  }
161
161
 
162
162
  const server = new Server(
163
- { name: 'auxilo', version: '0.9.3' },
163
+ { name: 'auxilo', version: '0.9.4' },
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.
167
167
 
168
168
  SEARCH BEFORE YOU DEBUG: When you encounter a non-trivial problem — an unfamiliar API, an unexpected error, an integration challenge — search Auxilo FIRST using auxilo_knowledge before spending tokens on trial-and-error. A free search that returns one verified answer saves minutes of debugging and dollars in API calls. Rule: if your first attempt fails, search Auxilo before trying a second approach.
169
169
 
170
- CONTRIBUTE AFTER YOU SOLVE: When you solve a non-trivial problem through debugging, experimentation, or combining information from multiple sources, submit the learning to Auxilo using auxilo_contribute. This includes: workarounds for undocumented behavior, edge cases, subtle bugs, API quirks, and solutions that required multiple attempts to find. Do NOT submit trivial lookups or well-documented standard approaches.
170
+ CONTRIBUTE AFTER YOU SOLVE: When you solve a non-trivial TECHNICAL problem through debugging, experimentation, or combining information from multiple sources, submit the learning to Auxilo using auxilo_contribute. This includes: workarounds for undocumented behavior, edge cases, subtle bugs, API quirks, and solutions that required multiple attempts to find. Do NOT submit trivial lookups or well-documented standard approaches.
171
+
172
+ TECHNICAL SCOPE (hard rule — the server refuses anything else): Auxilo accepts TECHNICAL learnings only — APIs, developer tools, code, infrastructure, data pipelines, monitoring, payment/crypto technology, debugging. Never submit interpersonal or communication strategy, copywriting/content/marketing insights, business or negotiation strategy, personal matters, or creative-writing technique. Categories: data-processing, web-interaction, code-execution, storage-state, payment-financial, monitoring. A technical learning about a messaging/email/notification API belongs under web-interaction or code-execution; content/data pipeline tech belongs under data-processing. SYSTEM-FACT TEST: submit ONLY when a system and a symptom are at the core — an error, an undocumented limitation, a reproducible behavior of an external tool/API/OS. Advice about how to work (process, workflow, methodology, decision practice) is not a learning and is held for review, regardless of quality score.
171
173
 
172
174
  QUALITY GATE: Before submitting, self-assess on four dimensions (1-5 each): Specificity, Actionability, Novelty, Completeness. Only submit if total >= 14/20 and no single dimension is below 3.
173
175
 
@@ -234,19 +236,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
234
236
  },
235
237
  {
236
238
  name: 'auxilo_contribute',
237
- description: 'Submit operational knowledge you discovered during this session to the Auxilo marketplace. WHEN TO USE: Call this AFTER you solve a non-trivial problem that required debugging, experimentation, or combining information from multiple sources. Call it when you discover undocumented behavior, a workaround, or a subtle edge case. Do NOT call it for trivial lookups or standard documentation answers. QUALITY GATE: Self-assess on Specificity, Actionability, Novelty, Completeness (1-5 each) and ALWAYS include your scores in quality_self_assessment — a submission WITHOUT it is held for manual review instead of publishing seamlessly. Only submit if total >= 14/20, no dimension below 3 (the server quarantines below-floor submissions for review). DEDUP: Search auxilo_knowledge first to avoid duplicates. SENSITIVITY (mandatory self-screen): never include secrets, credentials, API keys, PII, private filesystem paths, or proprietary/client business content — generalize to placeholders or omit; this is a PUBLIC marketplace. PRICING: Leave unlock_price unset to let the dynamic pricing engine calculate automatically (recommended). If setting manually: $0.05-$0.10 common techniques, $0.10-$1.00 specific solutions, $1.00-$10.00 novel discoveries, $10.00-$50.00 breakthroughs. Minimum $0.05, maximum $50.00. Free to submit — you earn 70% when others unlock. If the result is status pending_review, follow its how_to_review instructions (self-approval via `auxilo review`, the dashboard queue, or GET /account/pending).',
239
+ description: 'Submit TECHNICAL operational knowledge you discovered during this session to the Auxilo marketplace. WHEN TO USE: Call this AFTER you solve a non-trivial TECHNICAL problem that required debugging, experimentation, or combining information from multiple sources. Call it when you discover undocumented behavior, a workaround, or a subtle edge case. Do NOT call it for trivial lookups or standard documentation answers. SCOPE (hard rule — the server refuses out-of-scope submissions with CATEGORY_OUT_OF_SCOPE): technical learnings ONLY — APIs, developer tools, code, infrastructure, data pipelines, monitoring, payment/crypto technology, debugging. NEVER submit interpersonal/communication strategy, copywriting/content/marketing insights, business or negotiation strategy, personal matters, or creative-writing technique. A technical learning about a messaging/email/notification API belongs under web-interaction or code-execution; content/data pipeline tech belongs under data-processing. QUALITY GATE: Self-assess on Specificity, Actionability, Novelty, Completeness (1-5 each) and ALWAYS include your scores in quality_self_assessment — a submission WITHOUT it is held for manual review instead of publishing seamlessly. Only submit if total >= 14/20, no dimension below 3 (the server quarantines below-floor submissions for review). DEDUP: Search auxilo_knowledge first to avoid duplicates. SENSITIVITY (mandatory self-screen): never include secrets, credentials, API keys, PII, private filesystem paths, or proprietary/client business content — generalize to placeholders or omit; this is a PUBLIC marketplace. PRICING: Leave unlock_price unset to let the dynamic pricing engine calculate automatically (recommended). If setting manually: $0.05-$0.10 common techniques, $0.10-$1.00 specific solutions, $1.00-$10.00 novel discoveries, $10.00-$50.00 breakthroughs. Minimum $0.05, maximum $50.00. Free to submit — you earn 70% when others unlock. If the result is status pending_review, follow its how_to_review instructions (self-approval via `auxilo review`, the dashboard queue, or GET /account/pending).',
238
240
  inputSchema: {
239
241
  type: 'object',
240
242
  properties: {
241
243
  title: { type: 'string', description: 'Concise title (min 10 chars)' },
242
244
  body: { type: 'string', description: 'Detailed explanation — what you tried, what worked, what failed (min 50 chars)' },
243
- category: { type: 'string', enum: ['data-processing', 'web-interaction', 'code-execution', 'communication', 'storage-state', 'content-generation', 'payment-financial', 'monitoring'] },
245
+ // CI-5: technical-only taxonomy 'communication' and 'content-generation' retired.
246
+ category: { type: 'string', enum: ['data-processing', 'web-interaction', 'code-execution', 'storage-state', 'payment-financial', 'monitoring'] },
244
247
  tags: { type: 'array', items: { type: 'string' }, description: 'Relevant keywords' },
245
248
  task_context: { type: 'string', description: 'What task were you performing?' },
246
249
  outcome: { type: 'string', enum: ['success', 'partial', 'failure', 'workaround'] },
247
250
  quality_self_assessment: {
248
251
  type: 'object',
249
- description: 'Your quality self-assessment. ALWAYS include this — without it the submission is held for manual review. Score each dimension 1-5; total MUST equal their sum (server-verified). Submissions below the floor (total < 14 or any dimension < 3) are quarantined for review rather than published.',
252
+ description: 'Your quality self-assessment. ALWAYS include this — without it the submission is held for manual review. Score each dimension 1-5; total MUST equal their sum (server-verified). Submissions below the floor (total < 14 or any dimension < 3) are quarantined for review rather than published. High scores REQUIRE a system+symptom anchor — a named external system and a concrete error/limitation/reproducible behavior an agent would search mid-task; process or workflow advice cannot score high no matter how polished (the server holds it for review as process_advice).',
250
253
  properties: {
251
254
  specificity: { type: 'integer', minimum: 1, maximum: 5, description: 'How precise and detailed? (1-5)' },
252
255
  actionability: { type: 'integer', minimum: 1, maximum: 5, description: 'Can another agent directly use this? (1-5)' },
@@ -272,7 +275,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
272
275
  type: 'object',
273
276
  properties: {
274
277
  query: { type: 'string', description: 'What you need help with' },
275
- category: { type: 'string', enum: ['data-processing', 'web-interaction', 'code-execution', 'communication', 'storage-state', 'content-generation', 'payment-financial', 'monitoring'] },
278
+ // CI-5: learnings are technical-only; retired labels match nothing post-migration.
279
+ category: { type: 'string', enum: ['data-processing', 'web-interaction', 'code-execution', 'storage-state', 'payment-financial', 'monitoring'] },
276
280
  outcome: { type: 'string', enum: ['success', 'partial', 'failure', 'workaround'] },
277
281
  related_skill: { type: 'string', description: 'Filter by Auxilo skill ID' },
278
282
  limit: { type: 'number', description: 'Max results (default 5, max 15)' },
@@ -283,7 +287,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
283
287
  },
284
288
  {
285
289
  name: 'auxilo_unlock',
286
- description: 'Unlock full learning content by ID. Price is set by the contributor (min $0.05 USDC). 70% of the amount you pay goes to the contributor who shared this knowledge. Check unlock_price_usd in search results to see the cost before unlocking.',
290
+ description: 'Unlock full learning content by ID. Price is set by the contributor (min $0.05 USDC). 70% of the amount you pay goes to the contributor who shared this knowledge. Check unlock_price_usd in search results to see the cost before unlocking. YOUR OWN learnings are $0: anything this account contributed (or that was contributed under a wallet linked to this account) comes back free, with no credit deducted and no earnings movement.',
287
291
  inputSchema: {
288
292
  type: 'object',
289
293
  properties: {
@@ -295,7 +299,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
295
299
  },
296
300
  {
297
301
  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. 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).',
302
+ 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. One rating slot per account per learning: re-rating REPLACES your prior score, it never counts twice (CH-6). 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
303
  inputSchema: {
300
304
  type: 'object',
301
305
  properties: {
@@ -390,27 +394,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
390
394
  },
391
395
  {
392
396
  name: 'auxilo_review',
393
- description: 'Review YOUR OWN pending-review learnings (from background extraction) so they can be approved to the public marketplace or rejected to stay private. Account-scoped: only the authenticated account\'s own pending items are ever visible or affected. ACTIONS: "list" returns the triage summary (counts + compact rows with quality score and platform screen verdicts: injection, content sensitivity, near-duplicate). "approve" / "reject" apply explicit decisions to the ids you pass (the operator must have named or confirmed these items). "approve_clean" selects every item that passed ALL platform screens AND has quality >= min_quality (default 14/20); it is DRY-RUN BY DEFAULT and returns exactly what WOULD be approved. CONSENT CONTRACT: nothing goes public without the contributor\'s explicit approval. So before executing approve_clean you MUST show the operator the dry-run list and count and get their confirmation, then call again with dry_run:false, confirm:true, and expected_count set to the dry-run count. The server also enforces a counted-confirmation gate on every bulk call. Requires your configured API key (or session_token).',
397
+ description: 'Review YOUR OWN pending-review learnings (from background extraction) so they can be approved to the public marketplace or rejected to stay private. Account-scoped: only the authenticated account\'s own pending items are ever visible or affected. ACTIONS: "list" returns the triage summary (counts incl. by_signal + compact rows with quality score, lane, a one-sentence why for flagged items, and platform screen verdicts: injection, content sensitivity, near-duplicate). "approve" / "reject" apply explicit decisions to the ids you pass (the operator must have named or confirmed these items). "approve_clean" selects every item that passed ALL platform screens AND has quality >= min_quality (default 14/20); it is DRY-RUN BY DEFAULT and returns exactly what WOULD be approved. "reject_by_signal" bulk-rejects every pending item carrying one flag signal (e.g. social_handle) — REJECT ONLY (items stay private; there is deliberately no bulk approve by class): the operator must confirm the signal AND its by_signal count from "list", and you pass that count as expected_count — the server refuses if the live selection differs. "sanitize" resubmits ONE operator-corrected item through EVERY screen with lineage (the original is retired to private, reason sanitize-resubmit; the replacement is ALWAYS held for the operator\'s explicit approval — never auto-published): only call it with a correction the operator reviewed. CONSENT CONTRACT: nothing goes public without the contributor\'s explicit approval. So before executing approve_clean you MUST show the operator the dry-run list and count and get their confirmation, then call again with dry_run:false, confirm:true, and expected_count set to the dry-run count. The server also enforces a counted-confirmation gate on every bulk call. Requires your configured API key (or session_token).',
394
398
  inputSchema: {
395
399
  type: 'object',
396
400
  properties: {
397
- action: { type: 'string', enum: ['list', 'approve', 'reject', 'approve_clean'], description: 'What to do. Start with "list".' },
401
+ action: { type: 'string', enum: ['list', 'approve', 'reject', 'approve_clean', 'reject_by_signal', 'sanitize'], description: 'What to do. Start with "list".' },
398
402
  ids: { type: 'array', items: { type: 'string' }, description: 'Learning ids for action approve/reject. These must be items the operator explicitly chose.' },
399
- reason: { type: 'string', description: 'Optional rejection reason (action reject; max 500 chars).' },
403
+ reason: { type: 'string', description: 'Optional rejection reason (actions reject / reject_by_signal; max 500 chars).' },
404
+ signal: { type: 'string', description: 'reject_by_signal only. The flag signal to reject by (a name from counts.by_signal, e.g. social_handle, person_name, injection).' },
405
+ id: { type: 'string', description: 'sanitize only. The pending/rejected learning being corrected (operator-chosen).' },
406
+ title: { type: 'string', description: 'sanitize only. Corrected title (omit to keep the original).' },
407
+ body: { type: 'string', description: 'sanitize only. Corrected body (omit to keep the original). At least one of title/body is required.' },
408
+ tags: { type: 'array', items: { type: 'string' }, description: 'sanitize only. Corrected tags (omit to keep the original).' },
400
409
  dry_run: { type: 'boolean', description: 'approve_clean only. Default TRUE: report what would be approved without changing anything. Set false only together with confirm:true and expected_count after the operator confirmed the dry-run list.' },
401
410
  confirm: { type: 'boolean', description: 'approve_clean only. Must be exactly true to execute. Never set this without the operator\'s explicit go-ahead on the dry-run output.' },
402
- expected_count: { type: 'number', description: 'approve_clean execute only. The count from the dry run, echoed back. If the live selection differs (queue changed), nothing is approved and a fresh dry run is returned.' },
411
+ expected_count: { type: 'number', description: 'approve_clean execute + reject_by_signal. The count the operator confirmed (dry-run count / by_signal count), echoed back. If the live selection differs (queue changed), nothing is mutated.' },
403
412
  min_quality: { type: 'number', description: 'approve_clean quality threshold 0-20 (default 14). 0 includes unscored items.' },
404
413
  session_token: { type: 'string', description: 'Optional JWT session token from /auth/verify. If omitted, your configured API key authenticates the account.' },
405
414
  },
406
415
  required: ['action'],
407
416
  },
408
417
  },
409
- {
410
- name: 'get_stats',
411
- description: 'Get Auxilo registry statistics — catalog size, skill types, and query volume. Free.',
412
- inputSchema: { type: 'object', properties: {} },
413
- },
414
418
  {
415
419
  name: 'get_knowledge_stats',
416
420
  description: 'Get knowledge marketplace statistics — total learnings, unlocks, contributors, and top categories. Free.',
@@ -703,12 +707,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
703
707
  });
704
708
  }
705
709
 
706
- return text({ error: `Unknown action: ${args.action}. Use list, approve, reject, or approve_clean.` });
707
- }
710
+ if (args.action === 'reject_by_signal') {
711
+ // SPEC3 B2 (§5.2): server-side counted bulk reject by flag class.
712
+ // The operator confirms signal + by_signal count from {action:"list"};
713
+ // the server refuses (409, nothing mutated) if the live selection
714
+ // differs. Reject-only — the safe direction (items stay private).
715
+ if (typeof args.signal !== 'string' || !args.signal) {
716
+ return text({ error: 'reject_by_signal requires "signal" (a name from counts.by_signal — run {action:"list"} first).' });
717
+ }
718
+ if (!Number.isInteger(args.expected_count)) {
719
+ return text({ error: 'reject_by_signal requires expected_count: the by_signal count the operator confirmed. This is the counted-confirmation gate.' });
720
+ }
721
+ const resp = await fetch(`${AUXILO_BASE}/account/pending/reject-by-signal`, {
722
+ method: 'POST',
723
+ headers,
724
+ body: JSON.stringify({
725
+ signal: args.signal,
726
+ expected_count: args.expected_count,
727
+ ...(args.reason && { reason: args.reason }),
728
+ }),
729
+ });
730
+ return text(await resp.json());
731
+ }
708
732
 
709
- case 'get_stats': {
710
- const resp = await fetch(`${AUXILO_BASE}/stats`, { headers: baseHeaders() });
711
- return text(await resp.json());
733
+ if (args.action === 'sanitize') {
734
+ // SPEC3 B3 (§5.3): operator-corrected resubmission through EVERY
735
+ // screen, with lineage. The replacement is ALWAYS held for the
736
+ // operator's explicit approve — this action can never publish.
737
+ if (typeof args.id !== 'string' || !args.id) {
738
+ return text({ error: 'sanitize requires "id" (the pending/rejected learning the operator chose to correct).' });
739
+ }
740
+ if (args.title === undefined && args.body === undefined) {
741
+ return text({ error: 'sanitize requires at least one of "title" / "body" (the operator-reviewed correction).' });
742
+ }
743
+ const resp = await fetch(`${AUXILO_BASE}/account/pending/${encodeURIComponent(args.id)}/sanitize`, {
744
+ method: 'POST',
745
+ headers,
746
+ body: JSON.stringify({
747
+ ...(args.title !== undefined && { title: args.title }),
748
+ ...(args.body !== undefined && { body: args.body }),
749
+ ...(args.tags !== undefined && { tags: args.tags }),
750
+ }),
751
+ });
752
+ return text(await resp.json());
753
+ }
754
+
755
+ return text({ error: `Unknown action: ${args.action}. Use list, approve, reject, approve_clean, reject_by_signal, or sanitize.` });
712
756
  }
713
757
 
714
758
  case 'get_knowledge_stats': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auxilo-mcp",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
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",
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "start": "node mcp-server.js",
28
- "test": "node --test --test-force-exit test/*.test.js"
28
+ "test": "node --test test/*.test.js && node tests/test-mobile-nav-overlay.js"
29
29
  },
30
30
  "keywords": [
31
31
  "mcp",
@@ -57,6 +57,7 @@
57
57
  "viem": "^2.46.3"
58
58
  },
59
59
  "devDependencies": {
60
+ "playwright": "1.61.1",
60
61
  "proxyquire": "^2.1.3"
61
62
  }
62
63
  }
@@ -17,7 +17,14 @@ const fs = require('fs');
17
17
  const path = require('path');
18
18
  const os = require('os');
19
19
 
20
- const CATEGORIES = ['data-processing', 'web-interaction', 'code-execution', 'communication', 'storage-state', 'content-generation', 'payment-financial', 'monitoring'];
20
+ // CI-5 (PUNCH-LIST §30, 2026-07-19): Auxilo is TECHNICAL-ONLY. The learning
21
+ // taxonomy is these six tech categories; `communication` and `content-generation`
22
+ // are RETIRED — the server 400s them (CATEGORY_OUT_OF_SCOPE) and this extractor
23
+ // must never emit them. This file ships standalone in the npm package, so the
24
+ // lists are duplicated here from lib/category-scope-migration.js (server truth);
25
+ // test/ci5-scope-enforcement.test.js pins the copies equal.
26
+ const CATEGORIES = ['data-processing', 'web-interaction', 'code-execution', 'storage-state', 'payment-financial', 'monitoring'];
27
+ const RETIRED_CATEGORIES = ['communication', 'content-generation'];
21
28
 
22
29
  /**
23
30
  * SPEC3 slice A1 gate — score-at-extraction, BUILT BUT DARK by default.
@@ -40,6 +47,10 @@ const EXTRACTION_PROMPT_BASE = `You are extracting reusable OPERATIONAL LEARNING
40
47
 
41
48
  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.
42
49
 
50
+ HARD SCOPE RULE — TECHNICAL LEARNINGS ONLY (the marketplace accepts nothing else): extract ONLY technical/operational learnings — APIs, developer tools, code, infrastructure, data pipelines, monitoring/observability, payment/crypto TECHNOLOGY, debugging. NEVER extract interpersonal or communication strategy, copywriting/content/marketing insights, business or negotiation strategy, personal matters, or creative-writing technique — DROP such candidates entirely, do not relabel them. A technical learning about a messaging/email/notification API belongs under "web-interaction" or "code-execution"; content/data pipeline TECH belongs under "data-processing".
51
+
52
+ SYSTEM-FACT TEST (CI-7): Extract ONLY when a system and a symptom are at the core — an error, an undocumented limitation, a reproducible behavior of an external tool/API/OS. If the candidate is advice about how to work (process, workflow, methodology, decision practice), do NOT extract it. "Odesli cannot resolve Tidal artist URLs" is a learning; "use a two-phase consultation workflow" is not, no matter how well it would score.
53
+
43
54
  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.
44
55
 
45
56
  Output STRICT JSON ONLY — an array (possibly empty []) of objects with these keys:
@@ -58,6 +69,9 @@ const QUALITY_RUBRIC_ADDENDUM = `
58
69
  (non-obvious; an LLM would likely get it wrong), "completeness" (context,
59
70
  reproduction steps, caveats), plus "total" (the exact sum of the four).
60
71
  A learning worth publishing scores at least 14/20 with no dimension below 3.
72
+ High scores REQUIRE a system+symptom anchor — a named external system and a
73
+ concrete error/limitation/behavior; process or workflow advice cannot score
74
+ high no matter how polished (CI-7 system-fact test).
61
75
  If a learning honestly scores below that bar, DROP it from the array rather
62
76
  than inflating the numbers.`;
63
77
 
@@ -164,13 +178,25 @@ function parseLearnings(raw, opts = {}) {
164
178
  let arr;
165
179
  try { arr = JSON.parse(s.slice(start, end + 1)); } catch (_) { return []; }
166
180
  if (!Array.isArray(arr)) return [];
167
- return arr
168
- .filter(l => l && typeof l.title === 'string' && typeof l.body === 'string' && l.title.length >= 10 && l.body.length >= 50)
181
+ const shaped = arr
182
+ .filter(l => l && typeof l.title === 'string' && typeof l.body === 'string' && l.title.length >= 10 && l.body.length >= 50);
183
+ // CI-5 post-parse scope validation (defense-in-depth against prompt drift):
184
+ // a candidate whose category is outside the tech set is DROPPED entirely.
185
+ // The old coerce-unknown-to-'code-execution' fallback is gone — coercion
186
+ // would launder a non-tech candidate (e.g. one the model labeled
187
+ // 'communication') into the catalog wearing a tech label. Category-based,
188
+ // so it applies identically in BOTH score-gate states.
189
+ const inScope = shaped.filter(l => CATEGORIES.includes(l.category));
190
+ // Gate-A F5: make the drop observable — count to stderr (never stdout; the
191
+ // hook log captures it) so a silently over-dropping prompt is diagnosable.
192
+ const dropped = shaped.length - inScope.length;
193
+ if (dropped > 0) console.error(`[extract-local] dropped ${dropped} candidate(s) outside the technical category set (CI-5 scope)`);
194
+ return inScope
169
195
  .map(l => {
170
196
  const out = {
171
197
  title: l.title,
172
198
  body: l.body,
173
- category: CATEGORIES.includes(l.category) ? l.category : 'code-execution',
199
+ category: l.category,
174
200
  tags: Array.isArray(l.tags) ? l.tags.slice(0, 8).map(String) : [],
175
201
  task_context: typeof l.task_context === 'string' ? l.task_context : '',
176
202
  outcome: ['success', 'partial', 'failure', 'workaround'].includes(l.outcome) ? l.outcome : 'success',
@@ -198,7 +224,7 @@ async function extractLocally(transcript, sourceType) {
198
224
  }
199
225
 
200
226
  module.exports = {
201
- extractLocally, parseLearnings, resolveClaudeBin, CATEGORIES,
227
+ extractLocally, parseLearnings, resolveClaudeBin, CATEGORIES, RETIRED_CATEGORIES,
202
228
  EXTRACTION_PROMPT, buildExtractionPrompt, scoreExtractionEnabled,
203
229
  validateQualityAssessment, QUALITY_DIMENSIONS,
204
230
  };
package/scripts/runner.js CHANGED
@@ -781,9 +781,19 @@ async function main() {
781
781
 
782
782
  let transcriptData;
783
783
  try {
784
- transcriptData = await source.readSession(sessionRef);
784
+ // N1: capped read — the base-adapter path enforces AUXILO_MAX_SESSION_BYTES.
785
+ transcriptData = await source.readSessionCapped(sessionRef);
785
786
  } catch (err) {
786
- // Fallback: treat the file as a plain pre-formatted transcript
787
+ if (err && err.code === 'SESSION_TOO_LARGE') {
788
+ // Oversize is a counted SKIP, and it must short-circuit BEFORE the raw
789
+ // readFileSync fallback below — that fallback is the exact unbounded
790
+ // read the cap exists to prevent.
791
+ console.error(`[runner] SKIPPED oversize session ${sessionRef.sessionId} (${err.bytes} bytes > cap ${err.maxBytes}; oversize_skipped=1)`);
792
+ log(`[runner] Skipped oversize session ${sessionRef.sessionId} (${err.bytes} bytes > cap ${err.maxBytes})`);
793
+ process.exit(0);
794
+ }
795
+ // Fallback: treat the file as a plain pre-formatted transcript. Safe to
796
+ // read raw here: the cap check above already passed (under-cap file).
787
797
  try {
788
798
  transcriptData = { transcript: fs.readFileSync(transcriptPath, 'utf-8') };
789
799
  } catch (e2) {
@@ -892,6 +902,7 @@ async function main() {
892
902
  let totalDiscovered = 0;
893
903
  let totalProcessed = 0;
894
904
  let totalSkipped = 0;
905
+ let totalOversize = 0; // N1: oversize-cap skips (subset of totalSkipped)
895
906
  let totalFailed = 0;
896
907
  let totalHeld = 0;
897
908
 
@@ -918,11 +929,23 @@ async function main() {
918
929
  for (const sessionRef of sessions) {
919
930
  log(`[runner] Processing ${sessionRef.sessionId} (${sessionRef.bytes} bytes)...`);
920
931
 
921
- // Read transcript
932
+ // Read transcript (N1: capped — base-adapter path enforces
933
+ // AUXILO_MAX_SESSION_BYTES before any byte is read)
922
934
  let transcriptData;
923
935
  try {
924
- transcriptData = await source.readSession(sessionRef);
936
+ transcriptData = await source.readSessionCapped(sessionRef);
925
937
  } catch (err) {
938
+ if (err && err.code === 'SESSION_TOO_LARGE') {
939
+ // Counted skip, never a failure. stderr so constrained-sweeper logs
940
+ // surface it; ledger-marked (probe-refused idiom) so an immutable
941
+ // oversize file is not re-announced every sweep.
942
+ totalOversize++;
943
+ totalSkipped++;
944
+ console.error(`[runner] SKIPPED oversize session ${sessionRef.sessionId} (${err.bytes} bytes > cap ${err.maxBytes}; oversize_skipped=${totalOversize})`);
945
+ log(`[runner] Skipped oversize session ${sessionRef.sessionId} (${err.bytes} bytes > cap ${err.maxBytes})`);
946
+ ledgerMark(ledger, source.type, sessionRef.sessionId, 'oversize', sessionRef.mtime);
947
+ continue;
948
+ }
926
949
  log(`[runner] Read failed: ${err.message}`);
927
950
  totalFailed++;
928
951
  continue;
@@ -1007,7 +1030,10 @@ async function main() {
1007
1030
  }
1008
1031
 
1009
1032
  saveLedger(ledger);
1010
- log(`[runner] Summary: ${totalDiscovered} discovered, ${totalProcessed} processed, ${totalSkipped} skipped, ${totalFailed} failed`);
1033
+ log(`[runner] Summary: ${totalDiscovered} discovered, ${totalProcessed} processed, ${totalSkipped} skipped (${totalOversize} oversize), ${totalFailed} failed`);
1034
+ if (totalOversize > 0) {
1035
+ console.error(`[runner] oversize_skipped=${totalOversize} (sessions above AUXILO_MAX_SESSION_BYTES were skipped, not read)`);
1036
+ }
1011
1037
  notifyHeld(totalHeld); // LW-18 layer 2: one notification per sweep, count-only
1012
1038
  process.exit(totalFailed > 0 ? 1 : 0);
1013
1039
  }
@@ -13,6 +13,50 @@
13
13
 
14
14
  'use strict';
15
15
 
16
+ const fs = require('fs');
17
+
18
+ // ─── Session read-size cap (Wave 5C N1) ─────────────────────────────────────
19
+ //
20
+ // discoverSessions() returns `bytes` per the contract above, but nothing ever
21
+ // enforced a ceiling: every adapter's readSession() did an unbounded
22
+ // fs.readFileSync (54MB observed in the wild; a multi-GB transcript OOMs a
23
+ // constrained sweeper node) — all to feed a pipeline that truncates to 30k
24
+ // chars anyway (runner.js MAX_CHARS).
25
+ //
26
+ // Why 64MB: the ceiling's job is OOM-protection, not thrift. 54MB was proven
27
+ // in the wild AND extracted successfully — a cap below that silently regresses
28
+ // real coverage (the exact silent-loss class Wave 5C closes). 64MB = proven
29
+ // max + ~18% headroom; worst-case read peaks at low-hundreds-MB RSS
30
+ // (survivable on a 512MB node) while multi-GB is structurally refused.
31
+ // Constrained sweepers tighten via AUXILO_MAX_SESSION_BYTES.
32
+
33
+ const DEFAULT_MAX_SESSION_BYTES = 64 * 1024 * 1024;
34
+
35
+ /**
36
+ * Resolve the effective cap. Env override AUXILO_MAX_SESSION_BYTES must be a
37
+ * positive integer; anything else (absent, garbage, zero, negative) falls back
38
+ * to the default. Read at CALL time, not module load, so sweepers and tests
39
+ * can tune per-invocation.
40
+ */
41
+ function resolveMaxSessionBytes(env = process.env) {
42
+ const raw = env.AUXILO_MAX_SESSION_BYTES;
43
+ if (raw === undefined || raw === null || raw === '') return DEFAULT_MAX_SESSION_BYTES;
44
+ const n = Number(raw);
45
+ if (!Number.isInteger(n) || n <= 0) return DEFAULT_MAX_SESSION_BYTES;
46
+ return n;
47
+ }
48
+
49
+ /** Thrown by readSessionCapped BEFORE any read when a session exceeds the cap. */
50
+ class SessionTooLargeError extends Error {
51
+ constructor(sessionRef, bytes, maxBytes) {
52
+ super(`Session ${sessionRef && sessionRef.sessionId ? sessionRef.sessionId : '(unknown)'} is ${bytes} bytes — exceeds the ${maxBytes}-byte cap (AUXILO_MAX_SESSION_BYTES)`);
53
+ this.name = 'SessionTooLargeError';
54
+ this.code = 'SESSION_TOO_LARGE';
55
+ this.bytes = bytes;
56
+ this.maxBytes = maxBytes;
57
+ }
58
+ }
59
+
16
60
  /**
17
61
  * Abstract base class for transcript source adapters.
18
62
  *
@@ -67,6 +111,40 @@ class TranscriptSource {
67
111
  throw new Error('TranscriptSource.readSession() must be implemented by subclass');
68
112
  }
69
113
 
114
+ /**
115
+ * Size-capped session read — the ONLY entry point the runner uses (N1).
116
+ *
117
+ * CONCRETE on the base class so every adapter (including the generic-jsonl
118
+ * fallback and any future self-registered UC-3 adapter) inherits the cap
119
+ * through the shared path — no per-adapter copies. Size resolution: a fresh
120
+ * fs.statSync of sessionRef.path when the file stats (authoritative, costs
121
+ * nothing), else the discoverSessions-supplied sessionRef.bytes. Over-cap
122
+ * throws SessionTooLargeError BEFORE any byte is read; callers treat
123
+ * code === 'SESSION_TOO_LARGE' as a counted skip, never a failure.
124
+ *
125
+ * Subclasses must NOT override this — override readSession() only.
126
+ *
127
+ * @param {object} sessionRef - A session reference from discoverSessions()
128
+ * @returns {Promise<{transcript: string, metadata: object}>}
129
+ * @throws {SessionTooLargeError} when the session exceeds the cap
130
+ */
131
+ async readSessionCapped(sessionRef) {
132
+ const maxBytes = resolveMaxSessionBytes();
133
+ let bytes = null;
134
+ if (sessionRef && sessionRef.path) {
135
+ try { bytes = fs.statSync(sessionRef.path).size; } catch { /* fall through */ }
136
+ }
137
+ if (bytes === null && sessionRef && Number.isFinite(sessionRef.bytes)) {
138
+ bytes = sessionRef.bytes;
139
+ }
140
+ // Gate-A 5C F1: unknown size fails CLOSED — a future adapter without byte
141
+ // metadata must not silently reopen the unbounded-read OOM class this cap exists for.
142
+ if (bytes === null || bytes > maxBytes) {
143
+ throw new SessionTooLargeError(sessionRef, bytes === null ? -1 : bytes, maxBytes);
144
+ }
145
+ return this.readSession(sessionRef);
146
+ }
147
+
70
148
  /**
71
149
  * Register a callback for session-end events (live hook mode).
72
150
  *
@@ -82,4 +160,9 @@ class TranscriptSource {
82
160
  }
83
161
  }
84
162
 
85
- module.exports = { TranscriptSource };
163
+ module.exports = {
164
+ TranscriptSource,
165
+ SessionTooLargeError,
166
+ DEFAULT_MAX_SESSION_BYTES,
167
+ resolveMaxSessionBytes,
168
+ };