auxilo-mcp 0.9.19 → 0.9.20
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/mcp-server.js +1 -1
- package/package.json +1 -1
- package/scripts/sources/copilot.js +222 -0
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.20' },
|
|
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.20",
|
|
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",
|
|
@@ -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 };
|