jasper-recall 0.5.6 → 0.5.7
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/cli/config.js +0 -0
- package/cli/digest-sessions.js +233 -0
- package/cli/doctor.js +0 -0
- package/cli/index-digests.js +47 -0
- package/cli/recall.js +47 -0
- package/cli/server.js +0 -0
- package/cli/update-check.js +0 -0
- package/extensions/jasper-recall/index.ts +32 -0
- package/package.json +5 -2
package/cli/config.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* digest-sessions — Extract summaries from OpenClaw session logs
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx jasper-recall digest-sessions [--all] [--recent N] [--dry-run]
|
|
7
|
+
* digest-sessions [--all] [--recent N] [--dry-run]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const readline = require('readline');
|
|
14
|
+
|
|
15
|
+
// Config with environment overrides
|
|
16
|
+
const WORKSPACE = process.env.RECALL_WORKSPACE || path.join(os.homedir(), '.openclaw', 'workspace');
|
|
17
|
+
const SESSIONS_DIR = process.env.RECALL_SESSIONS_DIR || path.join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions');
|
|
18
|
+
const MEMORY_DIR = path.join(WORKSPACE, 'memory');
|
|
19
|
+
const DIGEST_DIR = path.join(MEMORY_DIR, 'session-digests');
|
|
20
|
+
const STATE_FILE = path.join(MEMORY_DIR, '.digest-state.json');
|
|
21
|
+
|
|
22
|
+
// Parse args
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const DRY_RUN = args.includes('--dry-run');
|
|
25
|
+
const ALL = args.includes('--all');
|
|
26
|
+
const recentIdx = args.indexOf('--recent');
|
|
27
|
+
const RECENT = recentIdx !== -1 ? parseInt(args[recentIdx + 1], 10) : null;
|
|
28
|
+
|
|
29
|
+
// Patterns to filter out from topics
|
|
30
|
+
const SKIP_PATTERNS = [
|
|
31
|
+
/^\[message_id:/,
|
|
32
|
+
/^System:/,
|
|
33
|
+
/^\{/,
|
|
34
|
+
/^<session-init>/,
|
|
35
|
+
/^<session-identity>/,
|
|
36
|
+
/^<relevant-memories>/,
|
|
37
|
+
/^🔄 \*\*Fresh session/,
|
|
38
|
+
/^Read HEARTBEAT\.md/,
|
|
39
|
+
/^HEARTBEAT_OK/,
|
|
40
|
+
/^NO_REPLY/,
|
|
41
|
+
/^ANNOUNCE_SKIP/,
|
|
42
|
+
/^Agent-to-agent/,
|
|
43
|
+
/^📋 \*\*PR Review/,
|
|
44
|
+
/^🤖 Codex/,
|
|
45
|
+
/^✅ \*\*Hourly/,
|
|
46
|
+
/^The following memories/,
|
|
47
|
+
/^- \[memory\//,
|
|
48
|
+
/^###\s+(IDENTITY|SOUL|USER)\.md/,
|
|
49
|
+
/^cat ~/,
|
|
50
|
+
/^```/,
|
|
51
|
+
/^---$/,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function shouldSkip(line) {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
if (!trimmed || trimmed.length < 5) return true;
|
|
57
|
+
return SKIP_PATTERNS.some(p => p.test(trimmed));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readState() {
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
63
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
return { processed: [], lastRun: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function saveState(state) {
|
|
70
|
+
state.lastRun = Date.now();
|
|
71
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function parseSession(sessionFile) {
|
|
75
|
+
const topics = [];
|
|
76
|
+
const toolCounts = {};
|
|
77
|
+
let messageCount = 0;
|
|
78
|
+
|
|
79
|
+
const rl = readline.createInterface({
|
|
80
|
+
input: fs.createReadStream(sessionFile),
|
|
81
|
+
crlfDelay: Infinity
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
for await (const line of rl) {
|
|
85
|
+
try {
|
|
86
|
+
const entry = JSON.parse(line);
|
|
87
|
+
messageCount++;
|
|
88
|
+
|
|
89
|
+
// Extract user messages for topics
|
|
90
|
+
if (entry.message?.role === 'user') {
|
|
91
|
+
let content = entry.message.content;
|
|
92
|
+
|
|
93
|
+
// Handle array content (multi-part messages)
|
|
94
|
+
if (Array.isArray(content)) {
|
|
95
|
+
content = content
|
|
96
|
+
.filter(p => p.type === 'text')
|
|
97
|
+
.map(p => p.text)
|
|
98
|
+
.join(' ');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof content === 'string') {
|
|
102
|
+
// Split into lines and filter
|
|
103
|
+
const lines = content.split('\n');
|
|
104
|
+
for (const l of lines) {
|
|
105
|
+
if (!shouldSkip(l) && topics.length < 20) {
|
|
106
|
+
topics.push(l.trim().slice(0, 200));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Count tool usage
|
|
113
|
+
if (entry.message?.role === 'assistant' && Array.isArray(entry.message.content)) {
|
|
114
|
+
for (const part of entry.message.content) {
|
|
115
|
+
if (part.type === 'toolCall' || part.type === 'tool_use') {
|
|
116
|
+
const name = part.name || part.toolName || 'unknown';
|
|
117
|
+
toolCounts[name] = (toolCounts[name] || 0) + 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// Skip malformed lines
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Sort tools by usage
|
|
127
|
+
const tools = Object.entries(toolCounts)
|
|
128
|
+
.sort((a, b) => b[1] - a[1])
|
|
129
|
+
.slice(0, 5)
|
|
130
|
+
.map(([name, count]) => `${name} (${count}x)`)
|
|
131
|
+
.join(', ');
|
|
132
|
+
|
|
133
|
+
return { topics: topics.slice(0, 10), tools: tools || 'none', messageCount };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function main() {
|
|
137
|
+
// Ensure directories exist
|
|
138
|
+
fs.mkdirSync(DIGEST_DIR, { recursive: true });
|
|
139
|
+
|
|
140
|
+
// Check sessions dir
|
|
141
|
+
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
142
|
+
console.log(`⚠ Sessions directory not found: ${SESSIONS_DIR}`);
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Get session files
|
|
147
|
+
const sessionFiles = fs.readdirSync(SESSIONS_DIR)
|
|
148
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
149
|
+
.map(f => f.replace('.jsonl', ''));
|
|
150
|
+
|
|
151
|
+
if (sessionFiles.length === 0) {
|
|
152
|
+
console.log('No session files found.');
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Load state
|
|
157
|
+
const state = await readState();
|
|
158
|
+
const processed = new Set(state.processed);
|
|
159
|
+
|
|
160
|
+
// Filter to new sessions (unless --all)
|
|
161
|
+
let toProcess = ALL
|
|
162
|
+
? sessionFiles
|
|
163
|
+
: sessionFiles.filter(s => !processed.has(s));
|
|
164
|
+
|
|
165
|
+
// Apply --recent limit
|
|
166
|
+
if (RECENT && RECENT > 0) {
|
|
167
|
+
toProcess = toProcess.slice(-RECENT);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (toProcess.length === 0) {
|
|
171
|
+
console.log('✓ No new sessions to digest.');
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log('🦊 Jasper Recall — Session Digester');
|
|
176
|
+
console.log('='.repeat(40));
|
|
177
|
+
console.log(`Sessions to process: ${toProcess.length}\n`);
|
|
178
|
+
|
|
179
|
+
for (const sessionId of toProcess) {
|
|
180
|
+
const sessionFile = path.join(SESSIONS_DIR, `${sessionId}.jsonl`);
|
|
181
|
+
if (!fs.existsSync(sessionFile)) continue;
|
|
182
|
+
|
|
183
|
+
const stats = fs.statSync(sessionFile);
|
|
184
|
+
const size = (stats.size / 1024).toFixed(0) + 'K';
|
|
185
|
+
const date = stats.mtime.toISOString().split('T')[0];
|
|
186
|
+
|
|
187
|
+
console.log(`Processing: ${sessionId.slice(0, 8)}... (${size})`);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const { topics, tools, messageCount } = await parseSession(sessionFile);
|
|
191
|
+
|
|
192
|
+
const digestFile = path.join(DIGEST_DIR, `${sessionId.slice(0, 8)}-${date}.md`);
|
|
193
|
+
|
|
194
|
+
const topicsFormatted = topics.length > 0
|
|
195
|
+
? topics.map(t => `- ${t}`).join('\n')
|
|
196
|
+
: '- (no topics extracted)';
|
|
197
|
+
|
|
198
|
+
const content = `# Session ${sessionId.slice(0, 8)} — ${date}
|
|
199
|
+
|
|
200
|
+
**Size:** ${size} | **Messages:** ${messageCount}
|
|
201
|
+
**Tools:** ${tools}
|
|
202
|
+
|
|
203
|
+
## Topics
|
|
204
|
+
|
|
205
|
+
${topicsFormatted}
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
*Full session: ${sessionFile}*
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
if (!DRY_RUN) {
|
|
212
|
+
fs.writeFileSync(digestFile, content);
|
|
213
|
+
state.processed.push(sessionId);
|
|
214
|
+
console.log(` ✓ Created: ${path.basename(digestFile)}`);
|
|
215
|
+
} else {
|
|
216
|
+
console.log(` [dry-run] Would create: ${path.basename(digestFile)}`);
|
|
217
|
+
}
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!DRY_RUN) {
|
|
224
|
+
saveState(state);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(`\n✓ Digests saved to: ${DIGEST_DIR}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
main().catch(err => {
|
|
231
|
+
console.error('Error:', err.message);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
});
|
package/cli/doctor.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* index-digests — Index memory files into ChromaDB
|
|
4
|
+
* Wrapper for the Python script
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { execSync, spawn } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
const VENV_PATH = path.join(os.homedir(), '.openclaw', 'rag-env');
|
|
13
|
+
const PYTHON = path.join(VENV_PATH, 'bin', 'python');
|
|
14
|
+
|
|
15
|
+
// Find the Python script - check multiple locations
|
|
16
|
+
const SCRIPT_LOCATIONS = [
|
|
17
|
+
path.join(__dirname, '..', 'scripts', 'index-digests.py'),
|
|
18
|
+
path.join(os.homedir(), '.local', 'share', 'jasper-recall', 'scripts', 'index-digests.py'),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
let scriptPath = null;
|
|
22
|
+
for (const loc of SCRIPT_LOCATIONS) {
|
|
23
|
+
if (fs.existsSync(loc)) {
|
|
24
|
+
scriptPath = loc;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!scriptPath) {
|
|
30
|
+
console.error('❌ index-digests.py not found. Run: npx jasper-recall setup');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(PYTHON)) {
|
|
35
|
+
console.error('❌ Python venv not found. Run: npx jasper-recall setup');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Run the Python script
|
|
40
|
+
const child = spawn(PYTHON, [scriptPath, ...process.argv.slice(2)], {
|
|
41
|
+
stdio: 'inherit',
|
|
42
|
+
env: { ...process.env }
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
child.on('exit', (code) => {
|
|
46
|
+
process.exit(code || 0);
|
|
47
|
+
});
|
package/cli/recall.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* recall — Semantic search over indexed memory
|
|
4
|
+
* Wrapper for the Python script
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
const VENV_PATH = path.join(os.homedir(), '.openclaw', 'rag-env');
|
|
13
|
+
const PYTHON = path.join(VENV_PATH, 'bin', 'python');
|
|
14
|
+
|
|
15
|
+
// Find the Python script - check multiple locations
|
|
16
|
+
const SCRIPT_LOCATIONS = [
|
|
17
|
+
path.join(__dirname, '..', 'scripts', 'recall.py'),
|
|
18
|
+
path.join(os.homedir(), '.local', 'share', 'jasper-recall', 'scripts', 'recall.py'),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
let scriptPath = null;
|
|
22
|
+
for (const loc of SCRIPT_LOCATIONS) {
|
|
23
|
+
if (fs.existsSync(loc)) {
|
|
24
|
+
scriptPath = loc;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!scriptPath) {
|
|
30
|
+
console.error('❌ recall.py not found. Run: npx jasper-recall setup');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(PYTHON)) {
|
|
35
|
+
console.error('❌ Python venv not found. Run: npx jasper-recall setup');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Run the Python script
|
|
40
|
+
const child = spawn(PYTHON, [scriptPath, ...process.argv.slice(2)], {
|
|
41
|
+
stdio: 'inherit',
|
|
42
|
+
env: { ...process.env }
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
child.on('exit', (code) => {
|
|
46
|
+
process.exit(code || 0);
|
|
47
|
+
});
|
package/cli/server.js
CHANGED
|
File without changes
|
package/cli/update-check.js
CHANGED
|
File without changes
|
|
@@ -326,6 +326,38 @@ ${identityParts.join('\n\n---\n\n')}
|
|
|
326
326
|
},
|
|
327
327
|
});
|
|
328
328
|
|
|
329
|
+
// ============================================================================
|
|
330
|
+
// Command: /digest-sessions
|
|
331
|
+
// ============================================================================
|
|
332
|
+
|
|
333
|
+
api.registerCommand({
|
|
334
|
+
name: 'digest-sessions',
|
|
335
|
+
description: 'Extract summaries from session logs into memory',
|
|
336
|
+
acceptsArgs: true,
|
|
337
|
+
requireAuth: true,
|
|
338
|
+
handler: async (ctx: { args?: string }) => {
|
|
339
|
+
try {
|
|
340
|
+
const args = ctx.args?.trim() || '';
|
|
341
|
+
const digestPath = path.join(BIN_PATH, 'digest-sessions');
|
|
342
|
+
|
|
343
|
+
// Check if digest-sessions exists in PATH, otherwise use npx
|
|
344
|
+
let cmd: string;
|
|
345
|
+
try {
|
|
346
|
+
execSync(`which ${digestPath}`, { encoding: 'utf8' });
|
|
347
|
+
cmd = `${digestPath} ${args}`;
|
|
348
|
+
} catch {
|
|
349
|
+
// Fall back to npx
|
|
350
|
+
cmd = `npx jasper-recall digest-sessions ${args}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const output = execSync(cmd, { encoding: 'utf8', timeout: 300000 });
|
|
354
|
+
return { text: `🗂️ **Session Digests**\n\n${output}` };
|
|
355
|
+
} catch (err: any) {
|
|
356
|
+
return { text: `❌ Digest failed: ${err.message}` };
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
329
361
|
// ============================================================================
|
|
330
362
|
// Command: /jasper-recall setup
|
|
331
363
|
// ============================================================================
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jasper-recall",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Local RAG system for AI agent memory using ChromaDB and sentence-transformers",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"jasper-recall": "./cli/jasper-recall.js"
|
|
7
|
+
"jasper-recall": "./cli/jasper-recall.js",
|
|
8
|
+
"digest-sessions": "./cli/digest-sessions.js",
|
|
9
|
+
"index-digests": "./cli/index-digests.js",
|
|
10
|
+
"recall": "./cli/recall.js"
|
|
8
11
|
},
|
|
9
12
|
"openclaw": {
|
|
10
13
|
"extensions": [
|