auxilo-mcp 0.7.0 → 0.8.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 +9 -9
- package/bin/auxilo-cli.js +447 -0
- package/lib/installer.js +647 -0
- package/lib/review.js +124 -0
- package/lib/sensitivity-filter.js +388 -0
- package/mcp-server.js +230 -64
- package/package.json +16 -4
- package/prompts/auxilo-learning-claude-code.md +250 -0
- package/prompts/auxilo-learning-generic.md +327 -0
- package/prompts/auxilo-learning-schema.json +98 -0
- package/scripts/hooks/auxilo-extract.sh +49 -0
- package/scripts/runner.js +843 -0
- package/scripts/sources/claude-code.js +182 -0
- package/scripts/sources/openclaw.js +171 -0
- package/scripts/sources/source.interface.js +85 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/sources/claude-code.js — Claude Code Transcript Source (P2.1a §4.2)
|
|
3
|
+
*
|
|
4
|
+
* Discovers and reads conversation transcripts from the Claude Code JSONL format.
|
|
5
|
+
* Default location: ~/.claude/projects/{name}/conversations/{id}.jsonl
|
|
6
|
+
*
|
|
7
|
+
* @module sources/claude-code
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const { TranscriptSource } = require('./source.interface');
|
|
16
|
+
|
|
17
|
+
class ClaudeCodeSource extends TranscriptSource {
|
|
18
|
+
static id = 'claude-code';
|
|
19
|
+
static displayName = 'Claude Code (Anthropic)';
|
|
20
|
+
static version = '1.0.0';
|
|
21
|
+
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
super(config);
|
|
24
|
+
/** @type {string} Root dir for Claude Code projects */
|
|
25
|
+
this.dataDir = config.dataDir || path.join(os.homedir(), '.claude', 'projects');
|
|
26
|
+
/** @type {string} Path to settings.json for hook registration */
|
|
27
|
+
this.settingsPath = config.settingsPath || path.join(os.homedir(), '.claude', 'settings.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect if Claude Code is installed: ~/.claude/projects exists AND
|
|
32
|
+
* ~/.claude/settings.json is readable.
|
|
33
|
+
*/
|
|
34
|
+
async detect() {
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.existsSync(this.dataDir)) return false;
|
|
37
|
+
fs.accessSync(this.settingsPath, fs.constants.R_OK);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Discover sessions modified after `since`.
|
|
46
|
+
* Walks ~/.claude/projects/*\/conversations/*.jsonl
|
|
47
|
+
*/
|
|
48
|
+
async discoverSessions({ since } = {}) {
|
|
49
|
+
const results = [];
|
|
50
|
+
if (!fs.existsSync(this.dataDir)) return results;
|
|
51
|
+
|
|
52
|
+
const sinceMs = since ? new Date(since).getTime() : 0;
|
|
53
|
+
|
|
54
|
+
let projects;
|
|
55
|
+
try {
|
|
56
|
+
projects = fs.readdirSync(this.dataDir).filter(p => {
|
|
57
|
+
const full = path.join(this.dataDir, p);
|
|
58
|
+
try { return fs.statSync(full).isDirectory(); } catch { return false; }
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Real Claude Code layout: ~/.claude/projects/<project>/<session-id>.jsonl
|
|
65
|
+
// The jsonl files sit directly under the project directory. Historical
|
|
66
|
+
// versions put them under a `conversations/` subdir — we still check
|
|
67
|
+
// that as a fallback for safety, but the modern layout is the default.
|
|
68
|
+
for (const project of projects) {
|
|
69
|
+
const projectDir = path.join(this.dataDir, project);
|
|
70
|
+
const candidateDirs = [
|
|
71
|
+
projectDir,
|
|
72
|
+
path.join(projectDir, 'conversations'), // legacy fallback
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
for (const dir of candidateDirs) {
|
|
76
|
+
if (!fs.existsSync(dir)) continue;
|
|
77
|
+
|
|
78
|
+
let files;
|
|
79
|
+
try { files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')); }
|
|
80
|
+
catch { continue; }
|
|
81
|
+
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const filePath = path.join(dir, file);
|
|
84
|
+
try {
|
|
85
|
+
const stat = fs.statSync(filePath);
|
|
86
|
+
// Skip directories masquerading via .jsonl suffix (shouldn't happen,
|
|
87
|
+
// but be defensive); the readdirSync already excludes non-files in
|
|
88
|
+
// practice, but stat check is cheap.
|
|
89
|
+
if (!stat.isFile()) continue;
|
|
90
|
+
if (stat.mtimeMs <= sinceMs) continue;
|
|
91
|
+
results.push({
|
|
92
|
+
sessionId: path.basename(file, '.jsonl'),
|
|
93
|
+
path: filePath,
|
|
94
|
+
mtime: stat.mtime.toISOString(),
|
|
95
|
+
bytes: stat.size,
|
|
96
|
+
});
|
|
97
|
+
} catch { /* skip unreadable files */ }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return results;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Read a session transcript. JSONL → plain "ROLE: text" blocks.
|
|
107
|
+
*/
|
|
108
|
+
async readSession(sessionRef) {
|
|
109
|
+
const filePath = sessionRef.path;
|
|
110
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
111
|
+
throw new Error(`Transcript file not found: ${filePath}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
115
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
116
|
+
|
|
117
|
+
// Real Claude Code JSONL shape:
|
|
118
|
+
// { type: 'user'|'assistant'|'system'|'queue-operation'|'attachment'|...,
|
|
119
|
+
// message: { role, content } }
|
|
120
|
+
// For user: message.content is a string OR [{type:'text', text:'...'}]
|
|
121
|
+
// For assistant: message.content is [{type:'text'|'thinking'|'tool_use'|'tool_result', ...}]
|
|
122
|
+
// We want user + assistant text content only, concatenated in order.
|
|
123
|
+
const turns = [];
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
try {
|
|
126
|
+
const entry = JSON.parse(line);
|
|
127
|
+
// Skip meta types — they don't contribute to the conversation
|
|
128
|
+
if (entry.type && !['user', 'assistant', 'system'].includes(entry.type)) continue;
|
|
129
|
+
|
|
130
|
+
const role = entry.message?.role || entry.role || entry.type || 'unknown';
|
|
131
|
+
const raw = entry.message?.content ?? entry.content;
|
|
132
|
+
if (raw == null) continue;
|
|
133
|
+
|
|
134
|
+
let text = '';
|
|
135
|
+
if (typeof raw === 'string') {
|
|
136
|
+
text = raw;
|
|
137
|
+
} else if (Array.isArray(raw)) {
|
|
138
|
+
// Concatenate text-like blocks. Skip tool_use/tool_result payloads
|
|
139
|
+
// and thinking blocks (internal reasoning, not conversation).
|
|
140
|
+
text = raw
|
|
141
|
+
.filter(b => b && (b.type === 'text' || b.type === 'input_text' || typeof b.text === 'string'))
|
|
142
|
+
.map(b => b.text || '')
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.join('\n');
|
|
145
|
+
} else if (typeof raw === 'object' && typeof raw.text === 'string') {
|
|
146
|
+
text = raw.text;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (text.trim().length > 0) {
|
|
150
|
+
turns.push(`[${role}]: ${text}`);
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Skip malformed lines
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
transcript: turns.join('\n\n'),
|
|
159
|
+
metadata: {
|
|
160
|
+
sessionId: sessionRef.sessionId,
|
|
161
|
+
source: 'claude-code',
|
|
162
|
+
mtime: sessionRef.mtime,
|
|
163
|
+
bytes: sessionRef.bytes,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Register SessionEnd hook via ~/.claude/settings.json.
|
|
170
|
+
* Returns an unregister function.
|
|
171
|
+
*/
|
|
172
|
+
async registerSessionEndHook(cb) {
|
|
173
|
+
// Hook registration is handled by installHooks() in runner.js.
|
|
174
|
+
// This method returns null to indicate that the runner should
|
|
175
|
+
// use the installHooks() path for setup, and poll for discovery.
|
|
176
|
+
// Live hook support via settings.json SessionEnd array is wired
|
|
177
|
+
// through the bash hook script, not through this JS method.
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { ClaudeCodeSource };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/sources/openclaw.js — OpenClaw Transcript Source (P2.1a §4.3)
|
|
3
|
+
*
|
|
4
|
+
* Discovers and reads session transcripts from the OpenClaw runtime directory.
|
|
5
|
+
* A5.4 FIX: Correct path is ~/.openclaw/agents/{agentId}/sessions/{sessionId}.jsonl
|
|
6
|
+
* (NOT data/openclaw/ inside the repo)
|
|
7
|
+
*
|
|
8
|
+
* Storage layout (from BUILD-1 research, spec §4.3):
|
|
9
|
+
* ~/.openclaw/agents/{agentId}/sessions/{sessionId}.jsonl
|
|
10
|
+
* with sessions.json index in that same directory.
|
|
11
|
+
* First line of each JSONL = session metadata; subsequent lines = messages.
|
|
12
|
+
*
|
|
13
|
+
* @module sources/openclaw
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const { TranscriptSource } = require('./source.interface');
|
|
22
|
+
|
|
23
|
+
class OpenClawSource extends TranscriptSource {
|
|
24
|
+
static id = 'openclaw';
|
|
25
|
+
static displayName = 'OpenClaw Memory System';
|
|
26
|
+
static version = '1.0.0';
|
|
27
|
+
|
|
28
|
+
constructor(config = {}) {
|
|
29
|
+
super(config);
|
|
30
|
+
// A5.4: Correct path — ~/.openclaw/agents (NOT data/openclaw/ inside repo)
|
|
31
|
+
this.dataDir = config.dataDir || path.join(os.homedir(), '.openclaw', 'agents');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect if OpenClaw is installed: ~/.openclaw/agents exists AND
|
|
36
|
+
* contains at least one agent directory.
|
|
37
|
+
*/
|
|
38
|
+
async detect() {
|
|
39
|
+
try {
|
|
40
|
+
if (!fs.existsSync(this.dataDir)) return false;
|
|
41
|
+
const agents = fs.readdirSync(this.dataDir).filter(d => {
|
|
42
|
+
try { return fs.statSync(path.join(this.dataDir, d)).isDirectory(); }
|
|
43
|
+
catch { return false; }
|
|
44
|
+
});
|
|
45
|
+
return agents.length > 0;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Discover sessions modified after `since`.
|
|
53
|
+
* Reads sessions.json index in each agent's sessions dir, filters by updatedAt.
|
|
54
|
+
*/
|
|
55
|
+
async discoverSessions({ since } = {}) {
|
|
56
|
+
const results = [];
|
|
57
|
+
if (!fs.existsSync(this.dataDir)) return results;
|
|
58
|
+
|
|
59
|
+
const sinceMs = since ? new Date(since).getTime() : 0;
|
|
60
|
+
|
|
61
|
+
let agents;
|
|
62
|
+
try {
|
|
63
|
+
agents = fs.readdirSync(this.dataDir).filter(d => {
|
|
64
|
+
try { return fs.statSync(path.join(this.dataDir, d)).isDirectory(); }
|
|
65
|
+
catch { return false; }
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
return results;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const agentId of agents) {
|
|
72
|
+
const sessionsDir = path.join(this.dataDir, agentId, 'sessions');
|
|
73
|
+
if (!fs.existsSync(sessionsDir)) continue;
|
|
74
|
+
|
|
75
|
+
// Try sessions.json index first
|
|
76
|
+
const indexPath = path.join(sessionsDir, 'sessions.json');
|
|
77
|
+
if (fs.existsSync(indexPath)) {
|
|
78
|
+
try {
|
|
79
|
+
const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
80
|
+
const sessions = Array.isArray(index) ? index : (index.sessions || []);
|
|
81
|
+
for (const entry of sessions) {
|
|
82
|
+
const updatedAt = entry.updatedAt || entry.updated_at || entry.mtime;
|
|
83
|
+
if (updatedAt && new Date(updatedAt).getTime() <= sinceMs) continue;
|
|
84
|
+
const sessionFile = path.join(sessionsDir, `${entry.sessionId || entry.id}.jsonl`);
|
|
85
|
+
if (!fs.existsSync(sessionFile)) continue;
|
|
86
|
+
const stat = fs.statSync(sessionFile);
|
|
87
|
+
results.push({
|
|
88
|
+
sessionId: entry.sessionId || entry.id,
|
|
89
|
+
path: sessionFile,
|
|
90
|
+
mtime: stat.mtime.toISOString(),
|
|
91
|
+
bytes: stat.size,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
continue; // Used index, skip file scan for this agent
|
|
95
|
+
} catch {
|
|
96
|
+
// Fall through to direct file scan
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fallback: direct .jsonl scan
|
|
101
|
+
try {
|
|
102
|
+
const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl'));
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
const filePath = path.join(sessionsDir, file);
|
|
105
|
+
try {
|
|
106
|
+
const stat = fs.statSync(filePath);
|
|
107
|
+
if (stat.mtimeMs <= sinceMs) continue;
|
|
108
|
+
results.push({
|
|
109
|
+
sessionId: path.basename(file, '.jsonl'),
|
|
110
|
+
path: filePath,
|
|
111
|
+
mtime: stat.mtime.toISOString(),
|
|
112
|
+
bytes: stat.size,
|
|
113
|
+
});
|
|
114
|
+
} catch { /* skip unreadable */ }
|
|
115
|
+
}
|
|
116
|
+
} catch { /* skip unreadable dir */ }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read a session transcript. Skip first (metadata) line per spec §4.3.
|
|
124
|
+
* Convert messages into "ROLE: text" format.
|
|
125
|
+
*/
|
|
126
|
+
async readSession(sessionRef) {
|
|
127
|
+
const filePath = sessionRef.path;
|
|
128
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
129
|
+
throw new Error(`Session file not found: ${filePath}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
133
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
134
|
+
|
|
135
|
+
// First line is session metadata per spec §4.3 — skip it
|
|
136
|
+
const turns = [];
|
|
137
|
+
for (let i = 1; i < lines.length; i++) {
|
|
138
|
+
try {
|
|
139
|
+
const msg = JSON.parse(lines[i]);
|
|
140
|
+
const role = msg.role || msg.type || 'unknown';
|
|
141
|
+
const text = typeof msg.content === 'string'
|
|
142
|
+
? msg.content
|
|
143
|
+
: (msg.content && msg.content[0]?.text) || JSON.stringify(msg.content || '');
|
|
144
|
+
turns.push(`[${role}]: ${text}`);
|
|
145
|
+
} catch {
|
|
146
|
+
// Skip malformed lines
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
transcript: turns.join('\n\n'),
|
|
152
|
+
metadata: {
|
|
153
|
+
sessionId: sessionRef.sessionId,
|
|
154
|
+
source: 'openclaw',
|
|
155
|
+
mtime: sessionRef.mtime,
|
|
156
|
+
bytes: sessionRef.bytes,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Register session-end hook.
|
|
163
|
+
* Returns null — OpenClaw is poll-only at launch per spec §4.3.
|
|
164
|
+
* Live hooks deferred pending SPEC-2 confirmation of plugin manifest.
|
|
165
|
+
*/
|
|
166
|
+
async registerSessionEndHook(cb) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { OpenClawSource };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/sources/source.interface.js — Transcript Source Interface (P2.1a §4.1)
|
|
3
|
+
*
|
|
4
|
+
* Base class for all transcript source adapters.
|
|
5
|
+
* Spec contract from BUILD-SPEC-P2.1a §4.1:
|
|
6
|
+
* - detect() → boolean (installed on host?)
|
|
7
|
+
* - discoverSessions({ since }) → [{ sessionId, path, mtime, bytes }]
|
|
8
|
+
* - readSession(sessionRef) → { transcript: string, metadata: {...} }
|
|
9
|
+
* - registerSessionEndHook(cb) → unregister fn | null (null = poll-only)
|
|
10
|
+
*
|
|
11
|
+
* @module sources/source.interface
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Abstract base class for transcript source adapters.
|
|
18
|
+
*
|
|
19
|
+
* Subclasses MUST set the static fields and implement all four methods.
|
|
20
|
+
*
|
|
21
|
+
* @abstract
|
|
22
|
+
*/
|
|
23
|
+
class TranscriptSource {
|
|
24
|
+
/** @type {string} Source type identifier, e.g. "claude-code" */
|
|
25
|
+
static id = 'abstract';
|
|
26
|
+
|
|
27
|
+
/** @type {string} Human-readable display name */
|
|
28
|
+
static displayName = 'Abstract Source';
|
|
29
|
+
|
|
30
|
+
/** @type {string} Semantic version of this adapter */
|
|
31
|
+
static version = '0.0.0';
|
|
32
|
+
|
|
33
|
+
constructor(config = {}) {
|
|
34
|
+
/** @type {string} Instance-level reference to the source type */
|
|
35
|
+
this.type = this.constructor.id || config.type || 'abstract';
|
|
36
|
+
/** @type {string} Instance-level display name */
|
|
37
|
+
this.label = this.constructor.displayName || config.label || '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detect whether this source is installed on the host.
|
|
42
|
+
*
|
|
43
|
+
* @returns {Promise<boolean>} true if the source data directory exists and is readable
|
|
44
|
+
*/
|
|
45
|
+
async detect() {
|
|
46
|
+
throw new Error('TranscriptSource.detect() must be implemented by subclass');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Discover new sessions since a high-water mark.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} opts
|
|
53
|
+
* @param {string} [opts.since] - ISO timestamp; only return sessions modified after this time
|
|
54
|
+
* @returns {Promise<Array<{sessionId: string, path: string, mtime: string, bytes: number}>>}
|
|
55
|
+
*/
|
|
56
|
+
async discoverSessions({ since } = {}) {
|
|
57
|
+
throw new Error('TranscriptSource.discoverSessions() must be implemented by subclass');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read a session's transcript.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} sessionRef - A session reference object from discoverSessions()
|
|
64
|
+
* @returns {Promise<{transcript: string, metadata: {sessionId: string, source: string, mtime: string, bytes: number}}>}
|
|
65
|
+
*/
|
|
66
|
+
async readSession(sessionRef) {
|
|
67
|
+
throw new Error('TranscriptSource.readSession() must be implemented by subclass');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Register a callback for session-end events (live hook mode).
|
|
72
|
+
*
|
|
73
|
+
* Returns null if the source only supports poll-based discovery (no live hooks).
|
|
74
|
+
* Returns an unregister function if hooks are supported.
|
|
75
|
+
*
|
|
76
|
+
* @param {Function} cb - Callback invoked with session path on session end
|
|
77
|
+
* @returns {Promise<Function|null>} Unregister function, or null for poll-only sources
|
|
78
|
+
*/
|
|
79
|
+
async registerSessionEndHook(cb) {
|
|
80
|
+
// Default: poll-only source — no live hooks available
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { TranscriptSource };
|