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.
@@ -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 };