jasper-recall 0.5.5 → 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
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { execSync } from 'child_process';
14
+ import { readFileSync, existsSync } from 'fs';
14
15
  import * as path from 'path';
15
16
  import * as os from 'os';
16
17
 
@@ -131,18 +132,33 @@ export default function register(api: PluginApi) {
131
132
  try {
132
133
  let prependParts: string[] = [];
133
134
 
134
- // If fresh session, remind agent to read identity files
135
+ // If fresh session, inject identity files directly into context
135
136
  if (isFreshSession) {
136
- api.logger.info('[jasper-recall] Fresh session detected - adding identity reminder');
137
- prependParts.push(`<session-init>
138
- 🔄 **Fresh session detected.** Before responding, read your identity files:
139
- \`\`\`bash
140
- cat ~/.openclaw/workspace/IDENTITY.md
141
- cat ~/.openclaw/workspace/SOUL.md
142
- cat ~/.openclaw/workspace/USER.md
143
- \`\`\`
144
- This ensures you maintain your persona and context after session reset.
145
- </session-init>`);
137
+ api.logger.info('[jasper-recall] Fresh session detected - injecting identity context');
138
+
139
+ const workspace = path.join(os.homedir(), '.openclaw', 'workspace');
140
+ const identityFiles = ['IDENTITY.md', 'SOUL.md', 'USER.md'];
141
+ const identityParts: string[] = [];
142
+
143
+ for (const file of identityFiles) {
144
+ const filePath = path.join(workspace, file);
145
+ if (existsSync(filePath)) {
146
+ try {
147
+ const content = readFileSync(filePath, 'utf8');
148
+ identityParts.push(`### ${file}\n${content}`);
149
+ } catch (err: any) {
150
+ api.logger.warn(`[jasper-recall] Failed to read ${file}: ${err.message}`);
151
+ }
152
+ }
153
+ }
154
+
155
+ if (identityParts.length > 0) {
156
+ prependParts.push(`<session-identity>
157
+ 🔄 **Fresh session.** Your identity files:
158
+
159
+ ${identityParts.join('\n\n---\n\n')}
160
+ </session-identity>`);
161
+ }
146
162
  }
147
163
 
148
164
  const results = runRecall(event.prompt, {
@@ -180,18 +196,33 @@ ${memoryContext}
180
196
  } catch (err: any) {
181
197
  api.logger.warn(`[jasper-recall] Auto-recall failed: ${err.message}`);
182
198
 
183
- // Still inject identity reminder on fresh session even if recall fails
199
+ // Still inject identity context on fresh session even if recall fails
184
200
  if (isFreshSession) {
185
- return {
186
- prependContext: `<session-init>
187
- 🔄 **Fresh session detected.** Before responding, read your identity files:
188
- \`\`\`bash
189
- cat ~/.openclaw/workspace/IDENTITY.md
190
- cat ~/.openclaw/workspace/SOUL.md
191
- cat ~/.openclaw/workspace/USER.md
192
- \`\`\`
193
- </session-init>`,
194
- };
201
+ const workspace = path.join(os.homedir(), '.openclaw', 'workspace');
202
+ const identityFiles = ['IDENTITY.md', 'SOUL.md', 'USER.md'];
203
+ const identityParts: string[] = [];
204
+
205
+ for (const file of identityFiles) {
206
+ const filePath = path.join(workspace, file);
207
+ if (existsSync(filePath)) {
208
+ try {
209
+ const content = readFileSync(filePath, 'utf8');
210
+ identityParts.push(`### ${file}\n${content}`);
211
+ } catch {
212
+ // Skip unreadable files
213
+ }
214
+ }
215
+ }
216
+
217
+ if (identityParts.length > 0) {
218
+ return {
219
+ prependContext: `<session-identity>
220
+ 🔄 **Fresh session.** Your identity files:
221
+
222
+ ${identityParts.join('\n\n---\n\n')}
223
+ </session-identity>`,
224
+ };
225
+ }
195
226
  }
196
227
  }
197
228
  });
@@ -295,6 +326,38 @@ cat ~/.openclaw/workspace/USER.md
295
326
  },
296
327
  });
297
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
+
298
361
  // ============================================================================
299
362
  // Command: /jasper-recall setup
300
363
  // ============================================================================
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "jasper-recall",
3
- "version": "0.5.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": [