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 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
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.6",
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": [