auxilo-mcp 0.9.19 → 0.9.21
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/lib/installer.js +14 -0
- package/mcp-server.js +1 -1
- package/package.json +2 -1
- package/scripts/extract-local.js +14 -69
- package/scripts/prompts/extraction.v1.js +99 -0
- package/scripts/prompts/index.js +30 -0
- package/scripts/runner.js +21 -0
- package/scripts/sources/copilot.js +222 -0
package/lib/installer.js
CHANGED
|
@@ -84,6 +84,18 @@ function sourceAdapterRows(packageRoot = PACKAGE_ROOT) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** Versioned extraction prompt bundle rows, derived so future bundles ship automatically. */
|
|
88
|
+
function promptBundleRows(packageRoot = PACKAGE_ROOT) {
|
|
89
|
+
try {
|
|
90
|
+
return fs.readdirSync(path.join(packageRoot, 'scripts', 'prompts'))
|
|
91
|
+
.filter((f) => f.endsWith('.js'))
|
|
92
|
+
.sort()
|
|
93
|
+
.map((f) => [`scripts/prompts/${f}`, `scripts/prompts/${f}`, 0o644]);
|
|
94
|
+
} catch {
|
|
95
|
+
return []; // missing dir surfaces as installRunner missing-file errors
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
// EXTRACT-PER-CLIENT W1 PART A: extract-local.js now requires
|
|
88
100
|
// './providers/index.js', which requires './claude-code.js' (and will require
|
|
89
101
|
// './codex-cli.js' / './byo-key.js' once PART B/C land) — every file under
|
|
@@ -137,6 +149,7 @@ const RUNNER_STACK = Object.freeze([
|
|
|
137
149
|
// at first extraction; omitting it from the installed stack is a MODULE_NOT_FOUND
|
|
138
150
|
// for every npm-installed user (test/runner-packaging-closure.test.js guards this).
|
|
139
151
|
['scripts/extract-local.js', 'scripts/extract-local.js', 0o644],
|
|
152
|
+
...promptBundleRows(),
|
|
140
153
|
// LW-18 layer 1b: SessionStart held-count notice (shim target).
|
|
141
154
|
['scripts/review-notice.js', 'scripts/review-notice.js', 0o755],
|
|
142
155
|
...sourceAdapterRows(),
|
|
@@ -2375,6 +2388,7 @@ module.exports = {
|
|
|
2375
2388
|
DEFAULT_BASE_URL,
|
|
2376
2389
|
PACKAGE_ROOT,
|
|
2377
2390
|
RUNNER_STACK,
|
|
2391
|
+
promptBundleRows,
|
|
2378
2392
|
clientRegistry,
|
|
2379
2393
|
detectClients,
|
|
2380
2394
|
clientHasCaptureHookMode,
|
package/mcp-server.js
CHANGED
|
@@ -198,7 +198,7 @@ async function postBulkChunks(headers, decisions) {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
const server = new Server(
|
|
201
|
-
{ name: 'auxilo', version: '0.9.
|
|
201
|
+
{ name: 'auxilo', version: '0.9.21' },
|
|
202
202
|
{
|
|
203
203
|
capabilities: { tools: {} },
|
|
204
204
|
instructions: `You are connected to Auxilo, a knowledge marketplace where AI agents buy and sell operational learnings.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auxilo-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.21",
|
|
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",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"scripts/capture-core.js",
|
|
28
28
|
"scripts/extract-local.js",
|
|
29
29
|
"scripts/review-notice.js",
|
|
30
|
+
"scripts/prompts/",
|
|
30
31
|
"scripts/sources/",
|
|
31
32
|
"scripts/providers/",
|
|
32
33
|
"scripts/hooks/auxilo-extract.sh",
|
package/scripts/extract-local.js
CHANGED
|
@@ -25,16 +25,24 @@ const {
|
|
|
25
25
|
} = require('../lib/extraction-index.js');
|
|
26
26
|
const providers = require('./providers/index.js');
|
|
27
27
|
const claudeCodeProvider = require('./providers/claude-code.js');
|
|
28
|
+
const {
|
|
29
|
+
CATEGORIES,
|
|
30
|
+
PRIVATE_CATEGORIES,
|
|
31
|
+
RETIRED_CATEGORIES,
|
|
32
|
+
EXTRACTION_PROMPT_BASE,
|
|
33
|
+
PUBLIC_SCOPE_BLOCK,
|
|
34
|
+
PRIVATE_SCOPE_BLOCK,
|
|
35
|
+
QUALITY_RUBRIC_ADDENDUM,
|
|
36
|
+
PROMPT_SUFFIX,
|
|
37
|
+
ANCHORED_JUDGE_PROMPT_BASE,
|
|
38
|
+
} = require('./prompts');
|
|
28
39
|
|
|
29
40
|
// CI-5 (PUNCH-LIST §30, 2026-07-19): Auxilo is TECHNICAL-ONLY. The learning
|
|
30
41
|
// taxonomy is these six tech categories; `communication` and `content-generation`
|
|
31
42
|
// are RETIRED — the server 400s them (CATEGORY_OUT_OF_SCOPE) and this extractor
|
|
32
|
-
// must never emit them.
|
|
33
|
-
//
|
|
43
|
+
// must never emit them. The governed prompt bundle carries the client-side copy
|
|
44
|
+
// from lib/category-scope-migration.js (server truth);
|
|
34
45
|
// test/ci5-scope-enforcement.test.js pins the copies equal.
|
|
35
|
-
const CATEGORIES = ['data-processing', 'web-interaction', 'code-execution', 'storage-state', 'payment-financial', 'monitoring'];
|
|
36
|
-
const PRIVATE_CATEGORIES = [...CATEGORIES, 'non-technical'];
|
|
37
|
-
const RETIRED_CATEGORIES = ['communication', 'content-generation'];
|
|
38
46
|
|
|
39
47
|
/**
|
|
40
48
|
* SPEC3 slice A1 gate — score-at-extraction, ON BY DEFAULT since 0.9.12
|
|
@@ -69,37 +77,6 @@ function scoreExtractionEnabled(env = process.env) {
|
|
|
69
77
|
return !(v === '0' || v === 'false');
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
const EXTRACTION_PROMPT_BASE = `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.
|
|
73
|
-
|
|
74
|
-
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.
|
|
75
|
-
|
|
76
|
-
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".
|
|
77
|
-
|
|
78
|
-
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.
|
|
79
|
-
|
|
80
|
-
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.
|
|
81
|
-
|
|
82
|
-
Output STRICT JSON ONLY — an object with:
|
|
83
|
-
"learnings": an array (possibly empty []) of objects with these keys:
|
|
84
|
-
"title": concise, >= 10 chars
|
|
85
|
-
"body": >= 50 chars — what was tried, what worked, what failed
|
|
86
|
-
"category": one of ${JSON.stringify(CATEGORIES)}
|
|
87
|
-
"tags": array of lowercase keyword strings
|
|
88
|
-
"task_context": one sentence describing the task
|
|
89
|
-
"outcome": one of "success","partial","failure","workaround"
|
|
90
|
-
"dedup_drops": an array (possibly empty []) used ONLY for candidates dropped
|
|
91
|
-
because they match PREVIOUSLY CAPTURED LESSONS. Each entry must be:
|
|
92
|
-
{"candidate": <the complete learning object above>,
|
|
93
|
-
"matched_index_id": "<exact id from the memory list>",
|
|
94
|
-
"matched_title": "<exact matched title>"}
|
|
95
|
-
Scope/quality/sensitivity skips are not dedup_drops.`;
|
|
96
|
-
|
|
97
|
-
const PUBLIC_SCOPE_BLOCK = `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".
|
|
98
|
-
|
|
99
|
-
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.`;
|
|
100
|
-
|
|
101
|
-
const PRIVATE_SCOPE_BLOCK = `PRIVATE CAPTURE SCOPE — OWNER-ONLY: extract reusable technical OR non-technical operational learnings. Non-technical process, workflow, communication, content, business, or creative learnings may use category "non-technical"; do not drop a genuine reusable candidate solely because it is non-technical. This private lane is never published unless the owner later sanitizes and promotes an item through public review. The mandatory sensitivity screen still applies without exception.`;
|
|
102
|
-
|
|
103
80
|
function promptBaseForVisibility(captureVisibility) {
|
|
104
81
|
if (captureVisibility !== 'private') return EXTRACTION_PROMPT_BASE;
|
|
105
82
|
return EXTRACTION_PROMPT_BASE
|
|
@@ -111,27 +88,6 @@ function promptBaseForVisibility(captureVisibility) {
|
|
|
111
88
|
.replace(JSON.stringify(CATEGORIES), JSON.stringify(PRIVATE_CATEGORIES));
|
|
112
89
|
}
|
|
113
90
|
|
|
114
|
-
/** A1: rubric addendum — appended ONLY when scoreExtractionEnabled(). */
|
|
115
|
-
const QUALITY_RUBRIC_ADDENDUM = `
|
|
116
|
-
"quality_self_assessment": an object scoring the learning honestly on four
|
|
117
|
-
dimensions, each an INTEGER 1-5: "specificity" (precise and detailed, not
|
|
118
|
-
vague), "actionability" (another agent can directly use it), "novelty"
|
|
119
|
-
(non-obvious; an LLM would likely get it wrong), "completeness" (context,
|
|
120
|
-
reproduction steps, caveats), plus "total" (the exact sum of the four).
|
|
121
|
-
A learning worth publishing scores at least 14/20 with no dimension below 3.
|
|
122
|
-
High scores REQUIRE a system+symptom anchor — a named external system and a
|
|
123
|
-
concrete error/limitation/behavior; process or workflow advice cannot score
|
|
124
|
-
high no matter how polished (CI-7 system-fact test).
|
|
125
|
-
If a learning honestly scores below that bar, DROP it from learnings rather
|
|
126
|
-
than inflating the numbers.`;
|
|
127
|
-
|
|
128
|
-
const PROMPT_SUFFIX = `
|
|
129
|
-
No prose, no explanation, no markdown code fences — just the raw JSON object
|
|
130
|
-
{"learnings":[...],"dedup_drops":[...]}.
|
|
131
|
-
|
|
132
|
-
TRANSCRIPT:
|
|
133
|
-
`;
|
|
134
|
-
|
|
135
91
|
/** Build the extraction prompt for the current (or injected) gate state. */
|
|
136
92
|
function buildExtractionPrompt(opts = {}) {
|
|
137
93
|
const withScore = opts.scoreExtraction !== undefined
|
|
@@ -361,18 +317,7 @@ function buildAnchoredJudgePrompt(candidates, indexRows, opts = {}) {
|
|
|
361
317
|
title: row.title,
|
|
362
318
|
})),
|
|
363
319
|
}));
|
|
364
|
-
const prompt =
|
|
365
|
-
For each candidate, decide whether it is a re-statement of ANY listed previously
|
|
366
|
-
captured lesson. The same operational insight in different words is YES.
|
|
367
|
-
A genuinely new fact is NO only when it would change what another agent does.
|
|
368
|
-
|
|
369
|
-
Return STRICT JSON ONLY:
|
|
370
|
-
{"decisions":[{"candidate_index":0,"duplicate":true,"matched_index_id":"..."}]}
|
|
371
|
-
Return exactly one decision for every candidate_index. When duplicate=false,
|
|
372
|
-
omit matched_index_id. When duplicate=true, matched_index_id MUST be one of that
|
|
373
|
-
candidate's listed ids. No prose and no markdown.
|
|
374
|
-
|
|
375
|
-
${JSON.stringify(payload)}`;
|
|
320
|
+
const prompt = `${ANCHORED_JUDGE_PROMPT_BASE}\n\n${JSON.stringify(payload)}`;
|
|
376
321
|
return { prompt, rankings };
|
|
377
322
|
}
|
|
378
323
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PROMPT_BUNDLE_VERSION = '1';
|
|
4
|
+
const ASSEMBLY_CONTRACT_VERSION = '1';
|
|
5
|
+
|
|
6
|
+
const CATEGORIES = Object.freeze([
|
|
7
|
+
'data-processing',
|
|
8
|
+
'web-interaction',
|
|
9
|
+
'code-execution',
|
|
10
|
+
'storage-state',
|
|
11
|
+
'payment-financial',
|
|
12
|
+
'monitoring',
|
|
13
|
+
]);
|
|
14
|
+
const PRIVATE_CATEGORIES = Object.freeze([
|
|
15
|
+
'data-processing',
|
|
16
|
+
'web-interaction',
|
|
17
|
+
'code-execution',
|
|
18
|
+
'storage-state',
|
|
19
|
+
'payment-financial',
|
|
20
|
+
'monitoring',
|
|
21
|
+
'non-technical',
|
|
22
|
+
]);
|
|
23
|
+
const RETIRED_CATEGORIES = Object.freeze(['communication', 'content-generation']);
|
|
24
|
+
|
|
25
|
+
const EXTRACTION_PROMPT_BASE = `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.
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
|
|
29
|
+
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".
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
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.
|
|
34
|
+
|
|
35
|
+
Output STRICT JSON ONLY — an object with:
|
|
36
|
+
"learnings": an array (possibly empty []) of objects with these keys:
|
|
37
|
+
"title": concise, >= 10 chars
|
|
38
|
+
"body": >= 50 chars — what was tried, what worked, what failed
|
|
39
|
+
"category": one of ["data-processing","web-interaction","code-execution","storage-state","payment-financial","monitoring"]
|
|
40
|
+
"tags": array of lowercase keyword strings
|
|
41
|
+
"task_context": one sentence describing the task
|
|
42
|
+
"outcome": one of "success","partial","failure","workaround"
|
|
43
|
+
"dedup_drops": an array (possibly empty []) used ONLY for candidates dropped
|
|
44
|
+
because they match PREVIOUSLY CAPTURED LESSONS. Each entry must be:
|
|
45
|
+
{"candidate": <the complete learning object above>,
|
|
46
|
+
"matched_index_id": "<exact id from the memory list>",
|
|
47
|
+
"matched_title": "<exact matched title>"}
|
|
48
|
+
Scope/quality/sensitivity skips are not dedup_drops.`;
|
|
49
|
+
|
|
50
|
+
const PUBLIC_SCOPE_BLOCK = `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
|
+
|
|
54
|
+
const PRIVATE_SCOPE_BLOCK = `PRIVATE CAPTURE SCOPE — OWNER-ONLY: extract reusable technical OR non-technical operational learnings. Non-technical process, workflow, communication, content, business, or creative learnings may use category "non-technical"; do not drop a genuine reusable candidate solely because it is non-technical. This private lane is never published unless the owner later sanitizes and promotes an item through public review. The mandatory sensitivity screen still applies without exception.`;
|
|
55
|
+
|
|
56
|
+
const QUALITY_RUBRIC_ADDENDUM = `
|
|
57
|
+
"quality_self_assessment": an object scoring the learning honestly on four
|
|
58
|
+
dimensions, each an INTEGER 1-5: "specificity" (precise and detailed, not
|
|
59
|
+
vague), "actionability" (another agent can directly use it), "novelty"
|
|
60
|
+
(non-obvious; an LLM would likely get it wrong), "completeness" (context,
|
|
61
|
+
reproduction steps, caveats), plus "total" (the exact sum of the four).
|
|
62
|
+
A learning worth publishing scores at least 14/20 with no dimension below 3.
|
|
63
|
+
High scores REQUIRE a system+symptom anchor — a named external system and a
|
|
64
|
+
concrete error/limitation/behavior; process or workflow advice cannot score
|
|
65
|
+
high no matter how polished (CI-7 system-fact test).
|
|
66
|
+
If a learning honestly scores below that bar, DROP it from learnings rather
|
|
67
|
+
than inflating the numbers.`;
|
|
68
|
+
|
|
69
|
+
const PROMPT_SUFFIX = `
|
|
70
|
+
No prose, no explanation, no markdown code fences — just the raw JSON object
|
|
71
|
+
{"learnings":[...],"dedup_drops":[...]}.
|
|
72
|
+
|
|
73
|
+
TRANSCRIPT:
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const ANCHORED_JUDGE_PROMPT_BASE = `You are a binary deduplication judge for operational learnings.
|
|
77
|
+
For each candidate, decide whether it is a re-statement of ANY listed previously
|
|
78
|
+
captured lesson. The same operational insight in different words is YES.
|
|
79
|
+
A genuinely new fact is NO only when it would change what another agent does.
|
|
80
|
+
|
|
81
|
+
Return STRICT JSON ONLY:
|
|
82
|
+
{"decisions":[{"candidate_index":0,"duplicate":true,"matched_index_id":"..."}]}
|
|
83
|
+
Return exactly one decision for every candidate_index. When duplicate=false,
|
|
84
|
+
omit matched_index_id. When duplicate=true, matched_index_id MUST be one of that
|
|
85
|
+
candidate's listed ids. No prose and no markdown.`;
|
|
86
|
+
|
|
87
|
+
module.exports = Object.freeze({
|
|
88
|
+
PROMPT_BUNDLE_VERSION,
|
|
89
|
+
ASSEMBLY_CONTRACT_VERSION,
|
|
90
|
+
CATEGORIES,
|
|
91
|
+
PRIVATE_CATEGORIES,
|
|
92
|
+
RETIRED_CATEGORIES,
|
|
93
|
+
EXTRACTION_PROMPT_BASE,
|
|
94
|
+
PUBLIC_SCOPE_BLOCK,
|
|
95
|
+
PRIVATE_SCOPE_BLOCK,
|
|
96
|
+
QUALITY_RUBRIC_ADDENDUM,
|
|
97
|
+
PROMPT_SUFFIX,
|
|
98
|
+
ANCHORED_JUDGE_PROMPT_BASE,
|
|
99
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const ACTIVE_BUNDLE_VERSION = '1';
|
|
8
|
+
const BUNDLE_PATHS = Object.freeze({
|
|
9
|
+
'1': path.join(__dirname, 'extraction.v1.js'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function bundlePath(version = ACTIVE_BUNDLE_VERSION) {
|
|
13
|
+
const resolved = BUNDLE_PATHS[String(version)];
|
|
14
|
+
if (!resolved) throw new Error(`Unknown extraction prompt bundle version: ${version}`);
|
|
15
|
+
return resolved;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loadPromptBundle(version = ACTIVE_BUNDLE_VERSION) {
|
|
19
|
+
return require(bundlePath(version));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bundleDigest(version = ACTIVE_BUNDLE_VERSION) {
|
|
23
|
+
return crypto.createHash('sha256').update(fs.readFileSync(bundlePath(version))).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = Object.freeze({
|
|
27
|
+
...loadPromptBundle(),
|
|
28
|
+
bundleDigest,
|
|
29
|
+
loadPromptBundle,
|
|
30
|
+
});
|
package/scripts/runner.js
CHANGED
|
@@ -850,15 +850,36 @@ const SWEEPER_LABEL = 'io.auxilo.sweeper';
|
|
|
850
850
|
*/
|
|
851
851
|
function sweeperManifest(repoRoot = path.resolve(__dirname, '..')) {
|
|
852
852
|
const sourceRows = [];
|
|
853
|
+
const promptRows = [];
|
|
854
|
+
const providerRows = [];
|
|
855
|
+
const providerSchemaRows = [];
|
|
853
856
|
try {
|
|
854
857
|
for (const f of fs.readdirSync(path.join(repoRoot, 'scripts', 'sources')).filter(f => f.endsWith('.js')).sort()) {
|
|
855
858
|
sourceRows.push([`scripts/sources/${f}`, `scripts/sources/${f}`, 0o644]);
|
|
856
859
|
}
|
|
857
860
|
} catch { /* missing dir surfaces as missing-file errors below */ }
|
|
861
|
+
try {
|
|
862
|
+
for (const f of fs.readdirSync(path.join(repoRoot, 'scripts', 'prompts')).filter(f => f.endsWith('.js')).sort()) {
|
|
863
|
+
promptRows.push([`scripts/prompts/${f}`, `scripts/prompts/${f}`, 0o644]);
|
|
864
|
+
}
|
|
865
|
+
} catch { /* missing dir surfaces as missing-file errors below */ }
|
|
866
|
+
try {
|
|
867
|
+
for (const f of fs.readdirSync(path.join(repoRoot, 'scripts', 'providers')).filter(f => f.endsWith('.js')).sort()) {
|
|
868
|
+
providerRows.push([`scripts/providers/${f}`, `scripts/providers/${f}`, 0o644]);
|
|
869
|
+
}
|
|
870
|
+
} catch { /* missing dir surfaces as missing-file errors below */ }
|
|
871
|
+
try {
|
|
872
|
+
for (const f of fs.readdirSync(path.join(repoRoot, 'scripts', 'providers', 'schemas')).filter(f => f.endsWith('.json')).sort()) {
|
|
873
|
+
providerSchemaRows.push([`scripts/providers/schemas/${f}`, `scripts/providers/schemas/${f}`, 0o644]);
|
|
874
|
+
}
|
|
875
|
+
} catch { /* missing dir surfaces as missing-file errors below */ }
|
|
858
876
|
return [
|
|
859
877
|
['scripts/auxilo-sweeper-wrapper.sh', 'auxilo-sweeper-wrapper.sh', 0o755],
|
|
860
878
|
['scripts/runner.js', 'scripts/runner.js', 0o755],
|
|
861
879
|
...sourceRows,
|
|
880
|
+
...promptRows,
|
|
881
|
+
...providerRows,
|
|
882
|
+
...providerSchemaRows,
|
|
862
883
|
['lib/sensitivity-filter.js', 'lib/sensitivity-filter.js', 0o644],
|
|
863
884
|
['lib/extraction-index.js', 'lib/extraction-index.js', 0o644],
|
|
864
885
|
['lib/similarity.js', 'lib/similarity.js', 0o644],
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/sources/copilot.js — GitHub Copilot CLI Transcript Source (BUILD-SPEC-0920)
|
|
3
|
+
*
|
|
4
|
+
* What already works (DO NOT touch — lib/installer.js registry entry
|
|
5
|
+
* 'copilot-cli' is correct as shipped): the capture Stop hook fires and
|
|
6
|
+
* hands runner.js a real `transcript_path` resolving to
|
|
7
|
+
* `~/.copilot/session-state/<sessionId>/events.jsonl`. That part was
|
|
8
|
+
* VERIFIED live (Copilot CLI 1.0.83/1.0.84, 2026-09-10). The gap this file
|
|
9
|
+
* closes: that transcript is a TYPED EVENT STREAM, not the plain
|
|
10
|
+
* role/content JSONL the generic-jsonl fallback expects, so capture fired,
|
|
11
|
+
* found the file, and silently extracted 0 turns. This is the dedicated
|
|
12
|
+
* parser so the 'copilot' entry on the client matrix is actually true.
|
|
13
|
+
*
|
|
14
|
+
* Line shape (VERIFIED live): one JSON object per line, `{"type":"<t>",
|
|
15
|
+
* "data":{...}}`. Types observed: session.start,
|
|
16
|
+
* session.permissions_changed, hook.start, hook.end, user.message,
|
|
17
|
+
* system.message, assistant.turn_start, assistant.message,
|
|
18
|
+
* tool.execution_start, tool.execution_complete, assistant.turn_end,
|
|
19
|
+
* session.usage_checkpoint, session.shutdown. Only two contribute
|
|
20
|
+
* conversation text; every other type — named above or not yet observed —
|
|
21
|
+
* is deliberately ignored rather than enumerated, so an unfamiliar future
|
|
22
|
+
* type is skipped, not a parse break.
|
|
23
|
+
*
|
|
24
|
+
* Extracted:
|
|
25
|
+
* - user.message -> [user]: data.content (a plain string). We
|
|
26
|
+
* deliberately ignore data.transformedContent — it wraps the same
|
|
27
|
+
* prompt in a <current_datetime> block and would duplicate/pollute
|
|
28
|
+
* the turn.
|
|
29
|
+
* - assistant.message -> [assistant]: data.content WHEN non-empty. An
|
|
30
|
+
* assistant.message with content:"" is a pure tool-call turn (its
|
|
31
|
+
* data.toolRequests[] carries the call instead) and contributes no
|
|
32
|
+
* text — skipped, not an empty turn.
|
|
33
|
+
*
|
|
34
|
+
* MODEL PROVENANCE (real, not 'unknown' — Copilot is the first client that
|
|
35
|
+
* records this on disk, unlike e.g. Devin which has no such field anywhere
|
|
36
|
+
* in its store): `session.start.data.selectedModel` (e.g. "gpt-5.6-terra")
|
|
37
|
+
* is authoritative; when a session lacks a readable session.start record,
|
|
38
|
+
* the last-seen `assistant.message.data.model` is used as a fallback.
|
|
39
|
+
* Never guessed beyond those two on-disk fields.
|
|
40
|
+
*
|
|
41
|
+
* SESSION ID: preferring the in-file `session.start.data.sessionId` over
|
|
42
|
+
* the caller-supplied `sessionRef.sessionId` matters for the hook path —
|
|
43
|
+
* runner.js's single-file mode derives sessionId from the transcript
|
|
44
|
+
* filename minus extension, and the filename here is always literally
|
|
45
|
+
* `events.jsonl` (the real session id is the PARENT directory, not the
|
|
46
|
+
* file), so relying on the caller's guess alone would stamp every
|
|
47
|
+
* hook-fired session as "events". The poll path below already discovers
|
|
48
|
+
* the correct id from the directory name, so this is primarily a hook-path
|
|
49
|
+
* correction, but the file's own value wins in both paths when present.
|
|
50
|
+
*
|
|
51
|
+
* @module sources/copilot
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
'use strict';
|
|
55
|
+
|
|
56
|
+
const fs = require('fs');
|
|
57
|
+
const path = require('path');
|
|
58
|
+
const os = require('os');
|
|
59
|
+
const { TranscriptSource } = require('./source.interface');
|
|
60
|
+
|
|
61
|
+
class CopilotSource extends TranscriptSource {
|
|
62
|
+
static id = 'copilot';
|
|
63
|
+
static displayName = 'GitHub Copilot CLI';
|
|
64
|
+
static version = '1.0.0';
|
|
65
|
+
|
|
66
|
+
constructor(config = {}) {
|
|
67
|
+
super(config);
|
|
68
|
+
const homeDir = config.homeDir || os.homedir();
|
|
69
|
+
this.stateDir = config.stateDir || path.join(homeDir, '.copilot', 'session-state');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async detect() {
|
|
73
|
+
try {
|
|
74
|
+
return fs.statSync(this.stateDir).isDirectory();
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hook-fired single-file capture is the primary path (see module header):
|
|
82
|
+
* the Stop hook hands runner.js the exact events.jsonl path directly, so
|
|
83
|
+
* this poll is a best-effort backfill only — a session the hook missed
|
|
84
|
+
* (e.g. capture briefly disabled) is still picked up by a later sweep.
|
|
85
|
+
* Returning [] here would be a perfectly valid contract too (hook-only
|
|
86
|
+
* clients do exactly that); this is simple enough over the fixed
|
|
87
|
+
* `<sessionId>/events.jsonl` layout to be worth the extra coverage.
|
|
88
|
+
*/
|
|
89
|
+
async discoverSessions({ since } = {}) {
|
|
90
|
+
const parsedSince = since ? Date.parse(since) : 0;
|
|
91
|
+
const sinceMs = Number.isFinite(parsedSince) ? parsedSince : 0;
|
|
92
|
+
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = fs.readdirSync(this.stateDir, { withFileTypes: true });
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const sessions = [];
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (!entry.isDirectory()) continue;
|
|
103
|
+
const filePath = path.join(this.stateDir, entry.name, 'events.jsonl');
|
|
104
|
+
let stat;
|
|
105
|
+
try {
|
|
106
|
+
stat = fs.statSync(filePath);
|
|
107
|
+
if (!stat.isFile()) continue;
|
|
108
|
+
} catch {
|
|
109
|
+
continue; // a session dir can lack/lose events.jsonl mid-sweep
|
|
110
|
+
}
|
|
111
|
+
if (stat.mtimeMs <= sinceMs) continue;
|
|
112
|
+
sessions.push({
|
|
113
|
+
sessionId: entry.name,
|
|
114
|
+
path: filePath,
|
|
115
|
+
mtime: stat.mtime.toISOString(),
|
|
116
|
+
bytes: stat.size,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return sessions.sort((a, b) =>
|
|
121
|
+
Date.parse(a.mtime) - Date.parse(b.mtime) || a.path.localeCompare(b.path)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async readSession(sessionRef) {
|
|
126
|
+
try {
|
|
127
|
+
return this._readSession(sessionRef);
|
|
128
|
+
} catch {
|
|
129
|
+
// Adapter contract is never-throw (matches codex-cli.js / devin.js):
|
|
130
|
+
// unexpected shape drift refuses the whole session, never escapes
|
|
131
|
+
// into the runner as a failed read.
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_readSession(sessionRef) {
|
|
137
|
+
const filePath = sessionRef && sessionRef.path;
|
|
138
|
+
if (!filePath) return null;
|
|
139
|
+
|
|
140
|
+
let raw;
|
|
141
|
+
try {
|
|
142
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const turns = [];
|
|
148
|
+
let sessionId = null;
|
|
149
|
+
let selectedModel = null;
|
|
150
|
+
let lastAssistantModel = null;
|
|
151
|
+
|
|
152
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
153
|
+
if (!line.trim()) continue;
|
|
154
|
+
let event;
|
|
155
|
+
try {
|
|
156
|
+
event = JSON.parse(line);
|
|
157
|
+
} catch {
|
|
158
|
+
continue; // one malformed line is skipped, not fatal to the session
|
|
159
|
+
}
|
|
160
|
+
if (!event || typeof event !== 'object') continue;
|
|
161
|
+
const data = event.data;
|
|
162
|
+
const type = event.type;
|
|
163
|
+
|
|
164
|
+
if (type === 'session.start') {
|
|
165
|
+
if (data && typeof data.selectedModel === 'string' && data.selectedModel) {
|
|
166
|
+
selectedModel = data.selectedModel;
|
|
167
|
+
}
|
|
168
|
+
if (data && typeof data.sessionId === 'string' && data.sessionId) {
|
|
169
|
+
sessionId = data.sessionId;
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (type === 'user.message') {
|
|
175
|
+
if (data && typeof data.content === 'string' && data.content.length > 0) {
|
|
176
|
+
turns.push(`[user]: ${data.content}`);
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (type === 'assistant.message') {
|
|
182
|
+
if (data && typeof data.model === 'string' && data.model) {
|
|
183
|
+
lastAssistantModel = data.model;
|
|
184
|
+
}
|
|
185
|
+
// Empty content = a pure tool-call turn (data.toolRequests[] carries
|
|
186
|
+
// the call instead) — no text to extract, not an empty turn.
|
|
187
|
+
if (data && typeof data.content === 'string' && data.content.length > 0) {
|
|
188
|
+
turns.push(`[assistant]: ${data.content}`);
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Every other type (session.*, hook.*, tool.*, system.message, the
|
|
194
|
+
// assistant.turn_start/turn_end markers, session.usage_checkpoint,
|
|
195
|
+
// session.shutdown, and anything not yet observed) is deliberately
|
|
196
|
+
// ignored — not conversation content.
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (turns.length === 0) return null;
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
transcript: turns.join('\n\n'),
|
|
203
|
+
metadata: {
|
|
204
|
+
sessionId: sessionId || (sessionRef && sessionRef.sessionId) || null,
|
|
205
|
+
source: 'copilot',
|
|
206
|
+
mtime: sessionRef.mtime,
|
|
207
|
+
bytes: sessionRef.bytes,
|
|
208
|
+
// MODEL PROVENANCE: real, on-disk — see module header. Prefer the
|
|
209
|
+
// session-level selectedModel; fall back to the last observed
|
|
210
|
+
// assistant.message.model; 'unknown' only when the file carries
|
|
211
|
+
// neither (never guessed beyond those two fields).
|
|
212
|
+
model: selectedModel || lastAssistantModel || 'unknown',
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async registerSessionEndHook(cb) {
|
|
218
|
+
return null; // hook-fired via the shipped Stop-event capture path (lib/installer.js); the poll above is best-effort backfill only
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = { CopilotSource };
|