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 +145 -0
- package/SKILL.md +185 -0
- package/cli/jasper-recall.js +195 -0
- package/package.json +42 -0
- package/scripts/digest-sessions.sh +131 -0
- package/scripts/index-digests.py +193 -0
- package/scripts/recall.py +106 -0
- package/src/index.js +63 -0
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
|
+
};
|