jasper-recall 0.1.0

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 ADDED
@@ -0,0 +1,145 @@
1
+ # Jasper Recall 🦊
2
+
3
+ Local RAG (Retrieval-Augmented Generation) system for AI agent memory. Gives your agent the ability to remember and search past conversations using ChromaDB and sentence-transformers.
4
+
5
+ ## Features
6
+
7
+ - **Semantic search** over session logs and memory files
8
+ - **Local embeddings** β€” no API keys needed
9
+ - **Incremental indexing** β€” only processes changed files
10
+ - **Session digests** β€” automatically extracts key info from chat logs
11
+ - **OpenClaw integration** β€” works seamlessly with OpenClaw agents
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # One-command setup
17
+ npx jasper-recall setup
18
+
19
+ # Search your memory
20
+ recall "what did we decide about the API"
21
+
22
+ # Index your files
23
+ index-digests
24
+
25
+ # Process new session logs
26
+ digest-sessions
27
+ ```
28
+
29
+ ## What Gets Indexed
30
+
31
+ By default, indexes markdown files from `~/.openclaw/workspace/memory/`:
32
+
33
+ - Daily notes (`*.md`)
34
+ - Session digests (`session-digests/*.md`)
35
+ - Project docs (`repos/*.md`)
36
+ - SOPs (`sops/*.md`)
37
+
38
+ ## How It Works
39
+
40
+ ```
41
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
42
+ β”‚ Session Logs │────▢│ digest- │────▢│ Markdown β”‚
43
+ β”‚ (.jsonl) β”‚ β”‚ sessions β”‚ β”‚ Digests β”‚
44
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
45
+ β”‚
46
+ β–Ό
47
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
48
+ β”‚ Memory Files │────▢│ index- │────▢│ ChromaDB β”‚
49
+ β”‚ (*.md) β”‚ β”‚ digests β”‚ β”‚ Vectors β”‚
50
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
51
+ β”‚
52
+ β–Ό
53
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
54
+ β”‚ recall │◀────│ Query β”‚
55
+ β”‚ "query" β”‚ β”‚ β”‚
56
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
57
+ ```
58
+
59
+ ## CLI Reference
60
+
61
+ ### recall
62
+
63
+ Search your indexed memory:
64
+
65
+ ```bash
66
+ recall "query" # Basic search
67
+ recall "query" -n 10 # More results
68
+ recall "query" --json # JSON output
69
+ recall "query" -v # Show similarity scores
70
+ ```
71
+
72
+ ### index-digests
73
+
74
+ Index markdown files into ChromaDB:
75
+
76
+ ```bash
77
+ index-digests # Index all files
78
+ ```
79
+
80
+ ### digest-sessions
81
+
82
+ Extract summaries from session logs:
83
+
84
+ ```bash
85
+ digest-sessions # Process new sessions only
86
+ digest-sessions --all # Reprocess everything
87
+ digest-sessions --dry-run # Preview without writing
88
+ ```
89
+
90
+ ## Configuration
91
+
92
+ Set environment variables to customize paths:
93
+
94
+ ```bash
95
+ export RECALL_WORKSPACE=~/.openclaw/workspace
96
+ export RECALL_CHROMA_DB=~/.openclaw/chroma-db
97
+ export RECALL_SESSIONS_DIR=~/.openclaw/agents/main/sessions
98
+ export RECALL_VENV=~/.openclaw/rag-env
99
+ ```
100
+
101
+ ## OpenClaw Integration
102
+
103
+ Add to your agent's HEARTBEAT.md for automatic memory maintenance:
104
+
105
+ ```markdown
106
+ ## Memory Maintenance
107
+ - [ ] New sessions? β†’ `digest-sessions`
108
+ - [ ] Files updated? β†’ `index-digests`
109
+ ```
110
+
111
+ Or schedule via cron:
112
+
113
+ ```json
114
+ {
115
+ "schedule": { "kind": "cron", "expr": "0 */6 * * *" },
116
+ "payload": {
117
+ "kind": "agentTurn",
118
+ "message": "Run index-digests to update memory index"
119
+ },
120
+ "sessionTarget": "isolated"
121
+ }
122
+ ```
123
+
124
+ ## Technical Details
125
+
126
+ - **Embedding model**: `sentence-transformers/all-MiniLM-L6-v2` (384 dimensions, ~80MB)
127
+ - **Vector store**: ChromaDB (persistent, local)
128
+ - **Chunking**: 500 chars with 100 char overlap
129
+ - **Deduplication**: Content hash check skips unchanged files
130
+
131
+ ## Requirements
132
+
133
+ - Python 3.10+
134
+ - Node.js 18+ (for setup CLI)
135
+ - ~500MB disk space (model + dependencies)
136
+
137
+ ## License
138
+
139
+ MIT
140
+
141
+ ## Links
142
+
143
+ - [GitHub](https://github.com/E-x-O-Entertainment-Studios-Inc/jasper-recall)
144
+ - [ClawHub](https://clawhub.ai/skills/jasper-recall)
145
+ - [Documentation](https://exohaven.online/products/jasper-recall)
package/SKILL.md ADDED
@@ -0,0 +1,185 @@
1
+ ---
2
+ name: jasper-recall
3
+ description: Local RAG system for agent memory using ChromaDB and sentence-transformers. Provides semantic search over session logs, daily notes, and memory files. Use when you need persistent memory across sessions, want to search past conversations, or build agents that remember context. Commands: recall "query", index-digests, digest-sessions.
4
+ ---
5
+
6
+ # Jasper Recall
7
+
8
+ Local RAG (Retrieval-Augmented Generation) system for AI agent memory. Gives your agent the ability to remember and search past conversations.
9
+
10
+ ## When to Use
11
+
12
+ - **Memory recall**: Search past sessions for context before answering
13
+ - **Continuous learning**: Index daily notes and decisions for future reference
14
+ - **Session continuity**: Remember what happened across restarts
15
+ - **Knowledge base**: Build searchable documentation from your agent's experience
16
+
17
+ ## Quick Start
18
+
19
+ ### Setup
20
+
21
+ One command installs everything:
22
+
23
+ ```bash
24
+ npx jasper-recall setup
25
+ ```
26
+
27
+ This creates:
28
+ - Python venv at `~/.openclaw/rag-env`
29
+ - ChromaDB database at `~/.openclaw/chroma-db`
30
+ - CLI scripts in `~/.local/bin/`
31
+
32
+ ### Basic Usage
33
+
34
+ **Search your memory:**
35
+ ```bash
36
+ recall "what did we decide about the API design"
37
+ recall "hopeIDS patterns" --limit 10
38
+ recall "meeting notes" --json
39
+ ```
40
+
41
+ **Index your files:**
42
+ ```bash
43
+ index-digests # Index memory files into ChromaDB
44
+ ```
45
+
46
+ **Create session digests:**
47
+ ```bash
48
+ digest-sessions # Process new sessions
49
+ digest-sessions --dry-run # Preview what would be processed
50
+ ```
51
+
52
+ ## How It Works
53
+
54
+ ### Three Components
55
+
56
+ 1. **digest-sessions** β€” Extracts key info from session logs (topics, tools used)
57
+ 2. **index-digests** β€” Chunks and embeds markdown files into ChromaDB
58
+ 3. **recall** β€” Semantic search across your indexed memory
59
+
60
+ ### What Gets Indexed
61
+
62
+ By default, indexes files from `~/.openclaw/workspace/memory/`:
63
+
64
+ - `*.md` β€” Daily notes, MEMORY.md
65
+ - `session-digests/*.md` β€” Session summaries
66
+ - `repos/*.md` β€” Project documentation
67
+ - `founder-logs/*.md` β€” Development logs (if present)
68
+
69
+ ### Embedding Model
70
+
71
+ Uses `sentence-transformers/all-MiniLM-L6-v2`:
72
+ - 384-dimensional embeddings
73
+ - ~80MB download on first run
74
+ - Runs locally, no API needed
75
+
76
+ ## Agent Integration
77
+
78
+ ### Memory-Augmented Responses
79
+
80
+ ```python
81
+ # Before answering questions about past work
82
+ results = exec("recall 'project setup decisions' --json")
83
+ # Include relevant context in your response
84
+ ```
85
+
86
+ ### Automated Indexing (Heartbeat)
87
+
88
+ Add to HEARTBEAT.md:
89
+ ```markdown
90
+ ## Memory Maintenance
91
+ - [ ] New session logs? β†’ `digest-sessions`
92
+ - [ ] Memory files updated? β†’ `index-digests`
93
+ ```
94
+
95
+ ### Cron Job
96
+
97
+ Schedule regular indexing:
98
+ ```json
99
+ {
100
+ "schedule": { "kind": "cron", "expr": "0 */6 * * *" },
101
+ "payload": {
102
+ "kind": "agentTurn",
103
+ "message": "Run index-digests to update the memory index"
104
+ },
105
+ "sessionTarget": "isolated"
106
+ }
107
+ ```
108
+
109
+ ## CLI Reference
110
+
111
+ ### recall
112
+
113
+ ```
114
+ recall "query" [OPTIONS]
115
+
116
+ Options:
117
+ -n, --limit N Number of results (default: 5)
118
+ --json Output as JSON
119
+ -v, --verbose Show similarity scores
120
+ ```
121
+
122
+ ### index-digests
123
+
124
+ ```
125
+ index-digests
126
+
127
+ Indexes markdown files from:
128
+ ~/.openclaw/workspace/memory/*.md
129
+ ~/.openclaw/workspace/memory/session-digests/*.md
130
+ ~/.openclaw/workspace/memory/repos/*.md
131
+ ~/.openclaw/workspace/memory/founder-logs/*.md
132
+
133
+ Skips files that haven't changed (content hash check).
134
+ ```
135
+
136
+ ### digest-sessions
137
+
138
+ ```
139
+ digest-sessions [OPTIONS]
140
+
141
+ Options:
142
+ --dry-run Preview without writing
143
+ --all Process all sessions (not just new)
144
+ --recent N Process only N most recent sessions
145
+ ```
146
+
147
+ ## Configuration
148
+
149
+ ### Custom Paths
150
+
151
+ Set environment variables:
152
+
153
+ ```bash
154
+ export RECALL_WORKSPACE=~/.openclaw/workspace
155
+ export RECALL_CHROMA_DB=~/.openclaw/chroma-db
156
+ export RECALL_SESSIONS_DIR=~/.openclaw/agents/main/sessions
157
+ ```
158
+
159
+ ### Chunking
160
+
161
+ Default settings in index-digests:
162
+ - Chunk size: 500 characters
163
+ - Overlap: 100 characters
164
+
165
+ ## Troubleshooting
166
+
167
+ **"No index found"**
168
+ ```bash
169
+ index-digests # Create the index first
170
+ ```
171
+
172
+ **"Collection not found"**
173
+ ```bash
174
+ rm -rf ~/.openclaw/chroma-db # Clear and rebuild
175
+ index-digests
176
+ ```
177
+
178
+ **Model download slow**
179
+ First run downloads ~80MB model. Subsequent runs are instant.
180
+
181
+ ## Links
182
+
183
+ - **GitHub**: https://github.com/E-x-O-Entertainment-Studios-Inc/jasper-recall
184
+ - **npm**: https://www.npmjs.com/package/jasper-recall
185
+ - **ClawHub**: https://clawhub.ai/skills/jasper-recall
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Jasper Recall CLI
4
+ * Local RAG system for AI agent memory
5
+ *
6
+ * Usage:
7
+ * npx jasper-recall setup # Install dependencies and create scripts
8
+ * npx jasper-recall recall # Run a query (alias)
9
+ * npx jasper-recall index # Index files (alias)
10
+ * npx jasper-recall digest # Digest sessions (alias)
11
+ */
12
+
13
+ const { execSync, spawn } = require('child_process');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ const VERSION = '0.1.0';
19
+ const VENV_PATH = path.join(os.homedir(), '.openclaw', 'rag-env');
20
+ const CHROMA_PATH = path.join(os.homedir(), '.openclaw', 'chroma-db');
21
+ const BIN_PATH = path.join(os.homedir(), '.local', 'bin');
22
+ const SCRIPTS_DIR = path.join(__dirname, '..', 'scripts');
23
+
24
+ function log(msg) {
25
+ console.log(`🦊 ${msg}`);
26
+ }
27
+
28
+ function error(msg) {
29
+ console.error(`❌ ${msg}`);
30
+ }
31
+
32
+ function run(cmd, opts = {}) {
33
+ try {
34
+ return execSync(cmd, { stdio: opts.silent ? 'pipe' : 'inherit', ...opts });
35
+ } catch (e) {
36
+ if (!opts.ignoreError) {
37
+ error(`Command failed: ${cmd}`);
38
+ process.exit(1);
39
+ }
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function setup() {
45
+ log('Jasper Recall β€” Setup');
46
+ console.log('=' .repeat(40));
47
+
48
+ // Check Python
49
+ log('Checking Python...');
50
+ let python = 'python3';
51
+ try {
52
+ const version = execSync(`${python} --version`, { encoding: 'utf8' });
53
+ console.log(` βœ“ ${version.trim()}`);
54
+ } catch {
55
+ error('Python 3 is required. Install it first.');
56
+ process.exit(1);
57
+ }
58
+
59
+ // Create venv
60
+ log('Creating Python virtual environment...');
61
+ fs.mkdirSync(path.dirname(VENV_PATH), { recursive: true });
62
+ if (!fs.existsSync(VENV_PATH)) {
63
+ run(`${python} -m venv ${VENV_PATH}`);
64
+ console.log(` βœ“ Created: ${VENV_PATH}`);
65
+ } else {
66
+ console.log(` βœ“ Already exists: ${VENV_PATH}`);
67
+ }
68
+
69
+ // Install Python dependencies
70
+ log('Installing Python dependencies (this may take a minute)...');
71
+ const pip = path.join(VENV_PATH, 'bin', 'pip');
72
+ run(`${pip} install --quiet chromadb sentence-transformers`);
73
+ console.log(' βœ“ Installed: chromadb, sentence-transformers');
74
+
75
+ // Create bin directory
76
+ fs.mkdirSync(BIN_PATH, { recursive: true });
77
+
78
+ // Copy scripts
79
+ log('Installing CLI scripts...');
80
+
81
+ const scripts = [
82
+ { src: 'recall.py', dest: 'recall', shebang: `#!${path.join(VENV_PATH, 'bin', 'python3')}` },
83
+ { src: 'index-digests.py', dest: 'index-digests', shebang: `#!${path.join(VENV_PATH, 'bin', 'python3')}` },
84
+ { src: 'digest-sessions.sh', dest: 'digest-sessions', shebang: '#!/bin/bash' }
85
+ ];
86
+
87
+ for (const script of scripts) {
88
+ const srcPath = path.join(SCRIPTS_DIR, script.src);
89
+ const destPath = path.join(BIN_PATH, script.dest);
90
+
91
+ let content = fs.readFileSync(srcPath, 'utf8');
92
+
93
+ // Replace generic shebang with specific one for Python scripts
94
+ if (script.src.endsWith('.py')) {
95
+ content = content.replace(/^#!.*python3?\n/, script.shebang + '\n');
96
+ }
97
+
98
+ fs.writeFileSync(destPath, content);
99
+ fs.chmodSync(destPath, 0o755);
100
+ console.log(` βœ“ Installed: ${destPath}`);
101
+ }
102
+
103
+ // Create chroma directory
104
+ fs.mkdirSync(CHROMA_PATH, { recursive: true });
105
+
106
+ // Verify PATH
107
+ const pathEnv = process.env.PATH || '';
108
+ if (!pathEnv.includes(BIN_PATH)) {
109
+ console.log('');
110
+ log('Add to your PATH (add to ~/.bashrc or ~/.zshrc):');
111
+ console.log(` export PATH="$HOME/.local/bin:$PATH"`);
112
+ }
113
+
114
+ console.log('');
115
+ console.log('=' .repeat(40));
116
+ log('Setup complete!');
117
+ console.log('');
118
+ console.log('Next steps:');
119
+ console.log(' 1. index-digests # Index your memory files');
120
+ console.log(' 2. recall "query" # Search your memory');
121
+ console.log(' 3. digest-sessions # Process session logs');
122
+ }
123
+
124
+ function showHelp() {
125
+ console.log(`
126
+ Jasper Recall v${VERSION}
127
+ Local RAG system for AI agent memory
128
+
129
+ USAGE:
130
+ npx jasper-recall <command>
131
+
132
+ COMMANDS:
133
+ setup Install dependencies and CLI scripts
134
+ recall Search your memory (alias for the recall command)
135
+ index Index memory files (alias for index-digests)
136
+ digest Process session logs (alias for digest-sessions)
137
+ help Show this help message
138
+
139
+ EXAMPLES:
140
+ npx jasper-recall setup
141
+ recall "what did we discuss yesterday"
142
+ index-digests
143
+ digest-sessions --dry-run
144
+ `);
145
+ }
146
+
147
+ // Main
148
+ const command = process.argv[2];
149
+
150
+ switch (command) {
151
+ case 'setup':
152
+ setup();
153
+ break;
154
+ case 'recall':
155
+ // Pass through to recall script
156
+ const recallScript = path.join(BIN_PATH, 'recall');
157
+ if (fs.existsSync(recallScript)) {
158
+ const args = process.argv.slice(3);
159
+ spawn(recallScript, args, { stdio: 'inherit' });
160
+ } else {
161
+ error('Run "npx jasper-recall setup" first');
162
+ }
163
+ break;
164
+ case 'index':
165
+ const indexScript = path.join(BIN_PATH, 'index-digests');
166
+ if (fs.existsSync(indexScript)) {
167
+ spawn(indexScript, [], { stdio: 'inherit' });
168
+ } else {
169
+ error('Run "npx jasper-recall setup" first');
170
+ }
171
+ break;
172
+ case 'digest':
173
+ const digestScript = path.join(BIN_PATH, 'digest-sessions');
174
+ if (fs.existsSync(digestScript)) {
175
+ const args = process.argv.slice(3);
176
+ spawn(digestScript, args, { stdio: 'inherit' });
177
+ } else {
178
+ error('Run "npx jasper-recall setup" first');
179
+ }
180
+ break;
181
+ case '--version':
182
+ case '-v':
183
+ console.log(VERSION);
184
+ break;
185
+ case 'help':
186
+ case '--help':
187
+ case '-h':
188
+ case undefined:
189
+ showHelp();
190
+ break;
191
+ default:
192
+ error(`Unknown command: ${command}`);
193
+ showHelp();
194
+ process.exit(1);
195
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "jasper-recall",
3
+ "version": "0.1.0",
4
+ "description": "Local RAG system for AI agent memory using ChromaDB and sentence-transformers",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "jasper-recall": "./cli/jasper-recall.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node cli/jasper-recall.js --version"
11
+ },
12
+ "keywords": [
13
+ "rag",
14
+ "chromadb",
15
+ "embeddings",
16
+ "memory",
17
+ "ai-agent",
18
+ "openclaw",
19
+ "semantic-search",
20
+ "vector-database"
21
+ ],
22
+ "author": "E.x.O. Entertainment Studios Inc.",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/E-x-O-Entertainment-Studios-Inc/jasper-recall.git"
27
+ },
28
+ "homepage": "https://exohaven.online/products/jasper-recall",
29
+ "bugs": {
30
+ "url": "https://github.com/E-x-O-Entertainment-Studios-Inc/jasper-recall/issues"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "files": [
36
+ "cli/",
37
+ "scripts/",
38
+ "src/",
39
+ "SKILL.md",
40
+ "README.md"
41
+ ]
42
+ }
@@ -0,0 +1,131 @@
1
+ #!/bin/bash
2
+ # digest-sessions β€” Extract learnings from session logs
3
+ # Usage: digest-sessions [--all | --recent N | --dry-run]
4
+
5
+ set -e
6
+
7
+ # Support custom paths via environment
8
+ WORKSPACE="${RECALL_WORKSPACE:-$HOME/.openclaw/workspace}"
9
+ SESSIONS_DIR="${RECALL_SESSIONS_DIR:-$HOME/.openclaw/agents/main/sessions}"
10
+ MEMORY_DIR="$WORKSPACE/memory"
11
+ DIGEST_DIR="$MEMORY_DIR/session-digests"
12
+ STATE_FILE="$MEMORY_DIR/.digest-state.json"
13
+
14
+ DRY_RUN=false
15
+ RECENT=""
16
+ ALL=false
17
+
18
+ # Parse args
19
+ while [[ $# -gt 0 ]]; do
20
+ case $1 in
21
+ --dry-run) DRY_RUN=true; shift ;;
22
+ --all) ALL=true; shift ;;
23
+ --recent) RECENT="$2"; shift 2 ;;
24
+ *) echo "Unknown option: $1"; exit 1 ;;
25
+ esac
26
+ done
27
+
28
+ # Create directories
29
+ mkdir -p "$DIGEST_DIR"
30
+
31
+ # Initialize state file if missing
32
+ if [[ ! -f "$STATE_FILE" ]]; then
33
+ echo '{"processed":[],"lastRun":0}' > "$STATE_FILE"
34
+ fi
35
+
36
+ # Check if sessions dir exists
37
+ if [[ ! -d "$SESSIONS_DIR" ]]; then
38
+ echo "⚠ Sessions directory not found: $SESSIONS_DIR"
39
+ exit 0
40
+ fi
41
+
42
+ # Get already processed sessions
43
+ processed=$(jq -r '.processed[]' "$STATE_FILE" 2>/dev/null || echo "")
44
+
45
+ # Find sessions to process
46
+ if [[ -n "$(ls -A "$SESSIONS_DIR"/*.jsonl 2>/dev/null)" ]]; then
47
+ all_sessions=$(ls -1 "$SESSIONS_DIR"/*.jsonl 2>/dev/null | xargs -I{} basename {} .jsonl)
48
+ else
49
+ echo "No session files found in $SESSIONS_DIR"
50
+ exit 0
51
+ fi
52
+
53
+ new_sessions=""
54
+
55
+ if [[ "$ALL" == "true" ]]; then
56
+ new_sessions="$all_sessions"
57
+ else
58
+ for s in $all_sessions; do
59
+ if ! echo "$processed" | grep -q "^${s}$"; then
60
+ new_sessions="$new_sessions $s"
61
+ fi
62
+ done
63
+ fi
64
+
65
+ # Apply --recent limit
66
+ if [[ -n "$RECENT" ]]; then
67
+ new_sessions=$(echo "$new_sessions" | tr ' ' '\n' | tail -n "$RECENT" | tr '\n' ' ')
68
+ fi
69
+
70
+ if [[ -z "$(echo $new_sessions | tr -d ' ')" ]]; then
71
+ echo "βœ“ No new sessions to digest."
72
+ exit 0
73
+ fi
74
+
75
+ echo "🦊 Jasper Recall β€” Session Digester"
76
+ echo "=" * 40
77
+ echo "Sessions to process: $(echo $new_sessions | wc -w)"
78
+ echo ""
79
+
80
+ # Process each session
81
+ for session_id in $new_sessions; do
82
+ session_file="$SESSIONS_DIR/${session_id}.jsonl"
83
+ [[ ! -f "$session_file" ]] && continue
84
+
85
+ size=$(du -h "$session_file" | cut -f1)
86
+ msgs=$(wc -l < "$session_file")
87
+ date=$(stat -c %y "$session_file" 2>/dev/null | cut -d' ' -f1 || stat -f %Sm -t %Y-%m-%d "$session_file" 2>/dev/null || echo "unknown")
88
+
89
+ echo "Processing: ${session_id:0:8}... ($size, $msgs messages)"
90
+
91
+ # Extract key info using jq
92
+ topics=$(jq -r 'select(.message.role == "user") | .message.content |
93
+ if type == "array" then
94
+ map(select(.type == "text") | .text) | join(" ")
95
+ else . end' "$session_file" 2>/dev/null | \
96
+ grep -v "^\[message_id:" | \
97
+ grep -v "^System:" | \
98
+ grep -v "^{" | \
99
+ head -10 || echo "")
100
+
101
+ tools=$(jq -r '.message.content[]? | select(.type == "toolCall") | .name' "$session_file" 2>/dev/null | \
102
+ sort | uniq -c | sort -rn | head -5 | awk '{print $2 " (" $1 "x)"}' | tr '\n' ', ' | sed 's/, $//' || echo "")
103
+
104
+ # Create digest file for this session
105
+ digest_file="$DIGEST_DIR/${session_id:0:8}-$date.md"
106
+
107
+ if [[ "$DRY_RUN" == "false" ]]; then
108
+ cat > "$digest_file" << EOF
109
+ # Session ${session_id:0:8} β€” $date
110
+
111
+ **Size:** $size | **Messages:** $msgs
112
+ **Tools:** ${tools:-none}
113
+
114
+ ## Topics
115
+
116
+ $(echo "$topics" | head -5 | sed 's/^/- /' | grep -v "^- $" || echo "- (no topics extracted)")
117
+
118
+ ---
119
+ *Full session: $session_file*
120
+ EOF
121
+
122
+ # Update state
123
+ jq --arg s "$session_id" '.processed += [$s] | .lastRun = now' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
124
+ echo " βœ“ Created: $(basename $digest_file)"
125
+ else
126
+ echo " [dry-run] Would create: $(basename $digest_file)"
127
+ fi
128
+ done
129
+
130
+ echo ""
131
+ echo "βœ“ Digests saved to: $DIGEST_DIR"
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Index markdown files into ChromaDB for RAG retrieval.
4
+ Reads from memory/, session-digests/, repos/, and founder-logs/.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import glob
10
+ import hashlib
11
+ from pathlib import Path
12
+
13
+ # Support custom paths via environment
14
+ WORKSPACE = os.environ.get("RECALL_WORKSPACE", os.path.expanduser("~/.openclaw/workspace"))
15
+ CHROMA_DIR = os.environ.get("RECALL_CHROMA_DB", os.path.expanduser("~/.openclaw/chroma-db"))
16
+ VENV_PATH = os.environ.get("RECALL_VENV", os.path.expanduser("~/.openclaw/rag-env"))
17
+
18
+ MEMORY_DIR = os.path.join(WORKSPACE, "memory")
19
+ DIGESTS_DIR = os.path.join(MEMORY_DIR, "session-digests")
20
+
21
+ # Chunking config
22
+ CHUNK_SIZE = 500 # characters
23
+ CHUNK_OVERLAP = 100
24
+
25
+ # Activate the venv
26
+ sys.path.insert(0, os.path.join(VENV_PATH, "lib/python3.12/site-packages"))
27
+ for pyver in ["python3.11", "python3.10"]:
28
+ alt_path = os.path.join(VENV_PATH, f"lib/{pyver}/site-packages")
29
+ if os.path.exists(alt_path):
30
+ sys.path.insert(0, alt_path)
31
+
32
+ try:
33
+ import chromadb
34
+ from sentence_transformers import SentenceTransformer
35
+ except ImportError as e:
36
+ print(f"❌ Missing dependency: {e}", file=sys.stderr)
37
+ print("Run 'npx jasper-recall setup' to install dependencies.", file=sys.stderr)
38
+ sys.exit(1)
39
+
40
+
41
+ def chunk_text(text: str, chunk_size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -> list:
42
+ """Split text into overlapping chunks."""
43
+ chunks = []
44
+ start = 0
45
+ while start < len(text):
46
+ end = start + chunk_size
47
+ chunk = text[start:end]
48
+ if chunk.strip():
49
+ chunks.append(chunk.strip())
50
+ start = end - overlap
51
+ return chunks
52
+
53
+
54
+ def get_file_hash(content: str) -> str:
55
+ """Get MD5 hash of content."""
56
+ return hashlib.md5(content.encode()).hexdigest()
57
+
58
+
59
+ def main():
60
+ print("🦊 Jasper Recall β€” RAG Indexer")
61
+ print("=" * 40)
62
+
63
+ # Check if memory dir exists
64
+ if not os.path.exists(MEMORY_DIR):
65
+ print(f"⚠ Memory directory not found: {MEMORY_DIR}")
66
+ print("Create some markdown files there first.")
67
+ sys.exit(1)
68
+
69
+ # Initialize embedding model (will download on first run)
70
+ print("Loading embedding model...")
71
+ model = SentenceTransformer('all-MiniLM-L6-v2')
72
+ print("βœ“ Model loaded")
73
+
74
+ # Initialize ChromaDB
75
+ os.makedirs(CHROMA_DIR, exist_ok=True)
76
+ client = chromadb.PersistentClient(path=CHROMA_DIR)
77
+
78
+ # Get or create collection
79
+ collection = client.get_or_create_collection(
80
+ name="jasper_memory",
81
+ metadata={"description": "Agent session digests and memory files"}
82
+ )
83
+
84
+ # Gather files to index
85
+ files_to_index = []
86
+
87
+ # Session digests
88
+ if os.path.exists(DIGESTS_DIR):
89
+ files_to_index.extend(glob.glob(os.path.join(DIGESTS_DIR, "*.md")))
90
+
91
+ # Daily notes and other memory files (but not subdirs)
92
+ files_to_index.extend(glob.glob(os.path.join(MEMORY_DIR, "*.md")))
93
+
94
+ # Repos documentation
95
+ repos_dir = os.path.join(MEMORY_DIR, "repos")
96
+ if os.path.exists(repos_dir):
97
+ files_to_index.extend(glob.glob(os.path.join(repos_dir, "*.md")))
98
+
99
+ # Founder Logs
100
+ for logs_dir_name in ["founder-logs", "founderLogs"]:
101
+ logs_dir = os.path.join(MEMORY_DIR, logs_dir_name)
102
+ if os.path.exists(logs_dir):
103
+ files_to_index.extend(glob.glob(os.path.join(logs_dir, "*.md")))
104
+
105
+ # SOPs
106
+ sops_dir = os.path.join(MEMORY_DIR, "sops")
107
+ if os.path.exists(sops_dir):
108
+ files_to_index.extend(glob.glob(os.path.join(sops_dir, "*.md")))
109
+
110
+ print(f"Found {len(files_to_index)} files to index")
111
+
112
+ # Track stats
113
+ total_chunks = 0
114
+ indexed_files = 0
115
+ skipped_files = 0
116
+
117
+ for filepath in files_to_index:
118
+ filename = os.path.basename(filepath)
119
+ rel_path = os.path.relpath(filepath, WORKSPACE)
120
+
121
+ try:
122
+ with open(filepath, 'r', encoding='utf-8') as f:
123
+ content = f.read()
124
+ except Exception as e:
125
+ print(f" ⚠ Error reading {filename}: {e}")
126
+ continue
127
+
128
+ if not content.strip():
129
+ continue
130
+
131
+ # Check if already indexed with same hash
132
+ file_hash = get_file_hash(content)
133
+
134
+ # Check for existing chunks from this file
135
+ existing = collection.get(
136
+ where={"source": rel_path},
137
+ include=[]
138
+ )
139
+
140
+ if existing['ids']:
141
+ # Check if hash matches (stored in first chunk's metadata)
142
+ existing_meta = collection.get(
143
+ ids=[existing['ids'][0]],
144
+ include=["metadatas"]
145
+ )
146
+ if existing_meta['metadatas'] and existing_meta['metadatas'][0].get('file_hash') == file_hash:
147
+ skipped_files += 1
148
+ continue
149
+
150
+ # File changed, delete old chunks
151
+ collection.delete(ids=existing['ids'])
152
+
153
+ # Chunk the content
154
+ chunks = chunk_text(content)
155
+
156
+ if not chunks:
157
+ continue
158
+
159
+ # Generate embeddings
160
+ embeddings = model.encode(chunks).tolist()
161
+
162
+ # Create IDs and metadata
163
+ ids = [f"{rel_path}::{i}" for i in range(len(chunks))]
164
+ metadatas = [
165
+ {
166
+ "source": rel_path,
167
+ "chunk_index": i,
168
+ "file_hash": file_hash,
169
+ "filename": filename
170
+ }
171
+ for i in range(len(chunks))
172
+ ]
173
+
174
+ # Add to collection
175
+ collection.add(
176
+ ids=ids,
177
+ embeddings=embeddings,
178
+ documents=chunks,
179
+ metadatas=metadatas
180
+ )
181
+
182
+ total_chunks += len(chunks)
183
+ indexed_files += 1
184
+ print(f" βœ“ {filename}: {len(chunks)} chunks")
185
+
186
+ print("=" * 40)
187
+ print(f"βœ“ Indexed {indexed_files} files ({total_chunks} chunks)")
188
+ print(f" Skipped {skipped_files} unchanged files")
189
+ print(f" Database: {CHROMA_DIR}")
190
+
191
+
192
+ if __name__ == "__main__":
193
+ main()
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ RAG recall: Search agent memory for relevant context.
4
+ Usage: recall "query" [--limit N] [--json] [--verbose]
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import argparse
10
+ import json
11
+
12
+ # Support custom paths via environment
13
+ CHROMA_DIR = os.environ.get("RECALL_CHROMA_DB", os.path.expanduser("~/.openclaw/chroma-db"))
14
+ VENV_PATH = os.environ.get("RECALL_VENV", os.path.expanduser("~/.openclaw/rag-env"))
15
+
16
+ # Activate the venv
17
+ sys.path.insert(0, os.path.join(VENV_PATH, "lib/python3.12/site-packages"))
18
+ # Also try python3.11, 3.10 for compatibility
19
+ for pyver in ["python3.11", "python3.10"]:
20
+ alt_path = os.path.join(VENV_PATH, f"lib/{pyver}/site-packages")
21
+ if os.path.exists(alt_path):
22
+ sys.path.insert(0, alt_path)
23
+
24
+ try:
25
+ import chromadb
26
+ from sentence_transformers import SentenceTransformer
27
+ except ImportError as e:
28
+ print(f"❌ Missing dependency: {e}", file=sys.stderr)
29
+ print("Run 'npx jasper-recall setup' to install dependencies.", file=sys.stderr)
30
+ sys.exit(1)
31
+
32
+
33
+ def main():
34
+ parser = argparse.ArgumentParser(description="Search agent memory")
35
+ parser.add_argument("query", help="Search query")
36
+ parser.add_argument("-n", "--limit", type=int, default=5, help="Number of results (default: 5)")
37
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
38
+ parser.add_argument("-v", "--verbose", action="store_true", help="Show similarity scores")
39
+ args = parser.parse_args()
40
+
41
+ if not os.path.exists(CHROMA_DIR):
42
+ print("❌ No index found. Run 'index-digests' first.", file=sys.stderr)
43
+ sys.exit(1)
44
+
45
+ # Load model and database
46
+ model = SentenceTransformer('all-MiniLM-L6-v2')
47
+ client = chromadb.PersistentClient(path=CHROMA_DIR)
48
+
49
+ try:
50
+ collection = client.get_collection("jasper_memory")
51
+ except Exception:
52
+ print("❌ Collection not found. Run 'index-digests' first.", file=sys.stderr)
53
+ sys.exit(1)
54
+
55
+ # Embed query
56
+ query_embedding = model.encode([args.query])[0].tolist()
57
+
58
+ # Search
59
+ results = collection.query(
60
+ query_embeddings=[query_embedding],
61
+ n_results=args.limit,
62
+ include=["documents", "metadatas", "distances"]
63
+ )
64
+
65
+ if not results['documents'][0]:
66
+ if args.json:
67
+ print("[]")
68
+ else:
69
+ print(f"πŸ” No results for: \"{args.query}\"")
70
+ return
71
+
72
+ if args.json:
73
+ output = []
74
+ for i, (doc, meta, dist) in enumerate(zip(
75
+ results['documents'][0],
76
+ results['metadatas'][0],
77
+ results['distances'][0]
78
+ )):
79
+ output.append({
80
+ "rank": i + 1,
81
+ "source": meta.get('source', 'unknown'),
82
+ "similarity": round(1 - dist, 3), # Convert distance to similarity
83
+ "content": doc
84
+ })
85
+ print(json.dumps(output, indent=2))
86
+ else:
87
+ print(f"πŸ” Results for: \"{args.query}\"\n")
88
+
89
+ for i, (doc, meta, dist) in enumerate(zip(
90
+ results['documents'][0],
91
+ results['metadatas'][0],
92
+ results['distances'][0]
93
+ )):
94
+ similarity = 1 - dist
95
+ score_str = f" ({similarity:.1%})" if args.verbose else ""
96
+ source = meta.get('source', 'unknown')
97
+
98
+ print(f"━━━ [{i+1}] {source}{score_str} ━━━")
99
+ # Truncate long content
100
+ content = doc[:500] + "..." if len(doc) > 500 else doc
101
+ print(content)
102
+ print()
103
+
104
+
105
+ if __name__ == "__main__":
106
+ main()
package/src/index.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Jasper Recall
3
+ * Local RAG system for AI agent memory
4
+ *
5
+ * This module exports utilities for programmatic access.
6
+ * For CLI usage, use the `jasper-recall` command.
7
+ */
8
+
9
+ const { execSync } = require('child_process');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ const BIN_PATH = path.join(os.homedir(), '.local', 'bin');
14
+
15
+ /**
16
+ * Search the memory index
17
+ * @param {string} query - Search query
18
+ * @param {Object} options - Options { limit, json, verbose }
19
+ * @returns {Array|string} - Search results
20
+ */
21
+ function recall(query, options = {}) {
22
+ const args = [query];
23
+ if (options.limit) args.push('-n', options.limit);
24
+ if (options.json) args.push('--json');
25
+ if (options.verbose) args.push('-v');
26
+
27
+ const recallPath = path.join(BIN_PATH, 'recall');
28
+ const result = execSync(`${recallPath} ${args.map(a => `"${a}"`).join(' ')}`, {
29
+ encoding: 'utf8'
30
+ });
31
+
32
+ return options.json ? JSON.parse(result) : result;
33
+ }
34
+
35
+ /**
36
+ * Index memory files
37
+ * @returns {string} - Index output
38
+ */
39
+ function indexDigests() {
40
+ const scriptPath = path.join(BIN_PATH, 'index-digests');
41
+ return execSync(scriptPath, { encoding: 'utf8' });
42
+ }
43
+
44
+ /**
45
+ * Process session logs into digests
46
+ * @param {Object} options - Options { dryRun, all, recent }
47
+ * @returns {string} - Digest output
48
+ */
49
+ function digestSessions(options = {}) {
50
+ const args = [];
51
+ if (options.dryRun) args.push('--dry-run');
52
+ if (options.all) args.push('--all');
53
+ if (options.recent) args.push('--recent', options.recent);
54
+
55
+ const scriptPath = path.join(BIN_PATH, 'digest-sessions');
56
+ return execSync(`${scriptPath} ${args.join(' ')}`, { encoding: 'utf8' });
57
+ }
58
+
59
+ module.exports = {
60
+ recall,
61
+ indexDigests,
62
+ digestSessions
63
+ };