@xanomyrox/opencode-memory 1.0.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/LICENSE +21 -0
- package/README.md +92 -0
- package/package.json +25 -0
- package/src/index.js +510 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 anomarynox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @xanomyrox/opencode-memory
|
|
2
|
+
|
|
3
|
+
Memory system plugin for OpenCode AI with **freemium model**.
|
|
4
|
+
|
|
5
|
+
## Pricing
|
|
6
|
+
|
|
7
|
+
| Tier | Price | Features |
|
|
8
|
+
|------|-------|----------|
|
|
9
|
+
| **FREE** | $0 | 500 memories, basic extraction, standard export |
|
|
10
|
+
| **PRO** | $29 one-time | Unlimited memories, priority extraction, custom tags, priority support |
|
|
11
|
+
|
|
12
|
+
### Why Pay for PRO?
|
|
13
|
+
|
|
14
|
+
- **Unlimited memories** - Never worry about hitting a limit
|
|
15
|
+
- **Priority extraction** - Extract more patterns per session
|
|
16
|
+
- **Custom tags** - Create your own topic tags
|
|
17
|
+
- **Priority support** - Get help faster
|
|
18
|
+
- **Lifetime license** - One payment, yours forever
|
|
19
|
+
|
|
20
|
+
### How to Upgrade
|
|
21
|
+
|
|
22
|
+
1. Visit: https://xanomyrox.gumroad.com/memory-plugin
|
|
23
|
+
2. Purchase for $29
|
|
24
|
+
3. Get your license key
|
|
25
|
+
4. In OpenCode, run: `/activate <your-license-key>`
|
|
26
|
+
5. Enjoy PRO features!
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
opencode plugin install @xanomyrox/opencode-memory
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
| Command | Description | Shortcut | Tier |
|
|
37
|
+
|---------|-------------|----------|------|
|
|
38
|
+
| /extract | Extract memories from conversation | Ctrl+E | All |
|
|
39
|
+
| /memory | Browse saved memories | Ctrl+M | All |
|
|
40
|
+
| /m-search | Search memories | | All |
|
|
41
|
+
| /m-filter | Filter by type | | All |
|
|
42
|
+
| /m-delete | Delete a memory | | All |
|
|
43
|
+
| /resume | Resume last session | Ctrl+R | All |
|
|
44
|
+
| /sessions | List recent sessions | | All |
|
|
45
|
+
| /export | Export to Obsidian | Ctrl+X | All |
|
|
46
|
+
| /topics | Show detected topics | | All |
|
|
47
|
+
| /memory-stats | Show statistics | | All |
|
|
48
|
+
| /upgrade | Upgrade to PRO | | Free |
|
|
49
|
+
| /activate | Activate license key | | Free |
|
|
50
|
+
| /license | Show license status | | All |
|
|
51
|
+
|
|
52
|
+
## Storage
|
|
53
|
+
|
|
54
|
+
- **Free**: `~/.config/opencode/data/memory-meta.json` (500 memories max)
|
|
55
|
+
- **PRO**: Same location, unlimited memories
|
|
56
|
+
- **License**: `~/.config/opencode/data/memory-license.json`
|
|
57
|
+
|
|
58
|
+
## Topics
|
|
59
|
+
|
|
60
|
+
Auto-detected topics with icons:
|
|
61
|
+
- bugfix | feature | research
|
|
62
|
+
- refactor | config | documentation
|
|
63
|
+
- testing | security | deployment
|
|
64
|
+
|
|
65
|
+
## License Key Format
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
MEMORY-PRO-XXXX-XXXX-XXXX-XXXX
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Example: `MEMORY-PRO-A1B2-C3D4-E5F6-G7H8`
|
|
72
|
+
|
|
73
|
+
## Support
|
|
74
|
+
|
|
75
|
+
- **Issues**: https://github.com/xanomyrox/opencode-memory/issues
|
|
76
|
+
- **License Problems**: `/license` to check your status
|
|
77
|
+
|
|
78
|
+
## Changelog
|
|
79
|
+
|
|
80
|
+
### v1.0.0
|
|
81
|
+
- Initial release
|
|
82
|
+
- Cross-session memory storage
|
|
83
|
+
- Auto-extraction from conversations
|
|
84
|
+
- Topic-based tagging
|
|
85
|
+
- Obsidian export
|
|
86
|
+
- **NEW** License system with FREE/PRO tiers
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT License - Copyright (c) 2026 xanomyrox
|
|
91
|
+
|
|
92
|
+
See [LICENSE](LICENSE) for full text.
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xanomyrox/opencode-memory",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Memory system plugin for OpenCode AI with FREE/PRO tiers",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"opencode",
|
|
9
|
+
"memory",
|
|
10
|
+
"ai",
|
|
11
|
+
"chatgpt",
|
|
12
|
+
"productivity",
|
|
13
|
+
"notes"
|
|
14
|
+
],
|
|
15
|
+
"author": "xanomyrox",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/xanomyrox/opencode-memory"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/xanomyrox/opencode-memory/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/xanomyrox/opencode-memory#readme"
|
|
25
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
const DATA_DIR = join(process.env.HOME || process.env.USERPROFILE || '~', '.config', 'opencode', 'data');
|
|
5
|
+
|
|
6
|
+
const TOPIC_KEYWORDS = {
|
|
7
|
+
bugfix: { keywords: ['bug', 'fix', 'error', 'crash', 'broken', 'debug', 'issue', 'fail', 'exception'], icon: 'š', color: '#ff6b6b' },
|
|
8
|
+
feature: { keywords: ['add', 'implement', 'new', 'create', 'build', 'develop', 'enhance'], icon: 'āØ', color: '#4ecdc4' },
|
|
9
|
+
research: { keywords: ['explore', 'understand', 'find', 'search', 'investigate', 'analyze'], icon: 'š', color: '#ffe66d' },
|
|
10
|
+
refactor: { keywords: ['refactor', 'clean', 'improve', 'optimize', 'restructure', 'rewrite'], icon: 'š§', color: '#95e1d3' },
|
|
11
|
+
config: { keywords: ['config', 'setting', 'setup', 'install', 'configure', 'environment'], icon: 'āļø', color: '#a8e6cf' },
|
|
12
|
+
documentation: { keywords: ['docs', 'readme', 'comment', 'explain', 'document', 'guide'], icon: 'š', color: '#dfe6e9' },
|
|
13
|
+
testing: { keywords: ['test', 'unit test', 'e2e', 'jest', 'vitest', 'spec'], icon: 'š§Ŗ', color: '#a29bfe' },
|
|
14
|
+
security: { keywords: ['security', 'vulnerability', 'auth', 'permission'], icon: 'š', color: '#fd79a8' },
|
|
15
|
+
deployment: { keywords: ['deploy', 'docker', 'kubernetes', 'ci/cd', 'pipeline'], icon: 'š', color: '#fab1a0' },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SESSION_PROMPT_KEY = 'memory_last_session_prompted';
|
|
19
|
+
|
|
20
|
+
const FREE_MEMORY_LIMIT = 500;
|
|
21
|
+
|
|
22
|
+
const memoryStore = { memories: [], tags: [] };
|
|
23
|
+
const licenseStore = { licenseKey: null, isPro: false, activatedAt: null };
|
|
24
|
+
|
|
25
|
+
const defaultTags = [
|
|
26
|
+
{ id: 'bugfix', name: 'bugfix', color: '#ff6b6b' },
|
|
27
|
+
{ id: 'feature', name: 'feature', color: '#4ecdc4' },
|
|
28
|
+
{ id: 'research', name: 'research', color: '#ffe66d' },
|
|
29
|
+
{ id: 'refactor', name: 'refactor', color: '#95e1d3' },
|
|
30
|
+
{ id: 'config', name: 'config', color: '#a8e6cf' },
|
|
31
|
+
{ id: 'documentation', name: 'documentation', color: '#dfe6e9' },
|
|
32
|
+
];
|
|
33
|
+
memoryStore.tags = [...defaultTags];
|
|
34
|
+
|
|
35
|
+
function getStorePath() {
|
|
36
|
+
return join(DATA_DIR, 'memory-meta.json');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getLicensePath() {
|
|
40
|
+
return join(DATA_DIR, 'memory-license.json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadFromFile() {
|
|
44
|
+
try {
|
|
45
|
+
const metaPath = getStorePath();
|
|
46
|
+
if (existsSync(metaPath)) {
|
|
47
|
+
const data = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
48
|
+
if (data.memories) memoryStore.memories = data.memories;
|
|
49
|
+
if (data.tags) memoryStore.tags = data.tags;
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadLicense() {
|
|
55
|
+
try {
|
|
56
|
+
const licensePath = getLicensePath();
|
|
57
|
+
if (existsSync(licensePath)) {
|
|
58
|
+
const data = JSON.parse(readFileSync(licensePath, 'utf-8'));
|
|
59
|
+
licenseStore.licenseKey = data.licenseKey || null;
|
|
60
|
+
licenseStore.isPro = data.isPro || false;
|
|
61
|
+
licenseStore.activatedAt = data.activatedAt || null;
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function saveToFile() {
|
|
67
|
+
try {
|
|
68
|
+
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
|
69
|
+
const metaPath = getStorePath();
|
|
70
|
+
writeFileSync(metaPath, JSON.stringify(memoryStore, null, 2));
|
|
71
|
+
} catch (e) {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function saveLicense() {
|
|
75
|
+
try {
|
|
76
|
+
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
|
77
|
+
const licensePath = getLicensePath();
|
|
78
|
+
writeFileSync(licensePath, JSON.stringify(licenseStore, null, 2));
|
|
79
|
+
} catch (e) {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
loadFromFile();
|
|
83
|
+
loadLicense();
|
|
84
|
+
|
|
85
|
+
function generateId() {
|
|
86
|
+
return `mem_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function detectTopics(text) {
|
|
90
|
+
const lower = text.toLowerCase();
|
|
91
|
+
const detected = [];
|
|
92
|
+
for (const [topic, config] of Object.entries(TOPIC_KEYWORDS)) {
|
|
93
|
+
for (const kw of config.keywords) {
|
|
94
|
+
if (lower.includes(kw)) { detected.push(topic); break; }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return detected;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function extractTextFromParts(parts) {
|
|
101
|
+
if (!parts) return '';
|
|
102
|
+
let text = '';
|
|
103
|
+
for (const part of parts) {
|
|
104
|
+
if (part.type === 'text' && part.data?.text) text += part.data.text + ' ';
|
|
105
|
+
}
|
|
106
|
+
return text.trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function extractCandidates(messages) {
|
|
110
|
+
const candidates = [];
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
|
|
113
|
+
const patterns = [
|
|
114
|
+
{ type: 'preference', regex: /I prefer ([^.]+)/gi, importance: 7 },
|
|
115
|
+
{ type: 'preference', regex: /I like ([^.]+)/gi, importance: 7 },
|
|
116
|
+
{ type: 'preference', regex: /avoid ([^.]+)/gi, importance: 7 },
|
|
117
|
+
{ type: 'concept', regex: /important: ([^.]+)/gi, importance: 6 },
|
|
118
|
+
{ type: 'code_pattern', regex: /convention: ([^.]+)/gi, importance: 8 },
|
|
119
|
+
{ type: 'code_pattern', regex: /style: ([^.]+)/gi, importance: 8 },
|
|
120
|
+
{ type: 'fact', regex: /the ([a-z_]+) uses? ([^.]+)/gi, importance: 5 },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
for (const msg of messages) {
|
|
124
|
+
if (msg.role === 'system') continue;
|
|
125
|
+
const text = extractTextFromParts(msg.parts || msg.content);
|
|
126
|
+
if (!text || text.length < 10) continue;
|
|
127
|
+
|
|
128
|
+
for (const rule of patterns) {
|
|
129
|
+
const regex = new RegExp(rule.regex.source, rule.regex.flags);
|
|
130
|
+
let match;
|
|
131
|
+
while ((match = regex.exec(text)) !== null) {
|
|
132
|
+
const title = match[1].trim().slice(0, 100);
|
|
133
|
+
const key = title.toLowerCase();
|
|
134
|
+
if (seen.has(key) || title.length < 3) continue;
|
|
135
|
+
seen.add(key);
|
|
136
|
+
candidates.push({
|
|
137
|
+
type: rule.type,
|
|
138
|
+
title: title.charAt(0).toUpperCase() + title.slice(1),
|
|
139
|
+
content: `From conversation: "${text.slice(0, 200)}..."`,
|
|
140
|
+
importance: rule.importance,
|
|
141
|
+
confidence: 0.7,
|
|
142
|
+
tags: detectTopics(text),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return candidates.sort((a, b) => b.importance - a.importance).slice(0, 20);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const memoryApi = {
|
|
152
|
+
create(data) {
|
|
153
|
+
const memoryLimit = licenseStore.isPro ? 10000 : FREE_MEMORY_LIMIT;
|
|
154
|
+
|
|
155
|
+
if (memoryStore.memories.length >= memoryLimit) {
|
|
156
|
+
if (!licenseStore.isPro) {
|
|
157
|
+
return { error: 'FREE_LIMIT', message: `Free limit reached (${FREE_MEMORY_LIMIT}). Upgrade to PRO for unlimited memories!` };
|
|
158
|
+
}
|
|
159
|
+
memoryStore.memories.sort((a, b) => a.importance - b.importance);
|
|
160
|
+
memoryStore.memories = memoryStore.memories.slice(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const id = generateId();
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
const memory = {
|
|
166
|
+
id,
|
|
167
|
+
session_id: data.session_id || null,
|
|
168
|
+
title: data.title,
|
|
169
|
+
content: data.content,
|
|
170
|
+
memory_type: data.memory_type || 'concept',
|
|
171
|
+
source: data.source || 'user_created',
|
|
172
|
+
importance: data.importance || 5,
|
|
173
|
+
created_at: now,
|
|
174
|
+
updated_at: now,
|
|
175
|
+
};
|
|
176
|
+
memoryStore.memories.unshift(memory);
|
|
177
|
+
saveToFile();
|
|
178
|
+
return memory;
|
|
179
|
+
},
|
|
180
|
+
list(opts) {
|
|
181
|
+
let result = [...memoryStore.memories];
|
|
182
|
+
if (opts?.type) result = result.filter(m => m.memory_type === opts.type);
|
|
183
|
+
if (opts?.session_id) result = result.filter(m => m.session_id === opts.session_id);
|
|
184
|
+
if (opts?.search) {
|
|
185
|
+
const q = opts.search.toLowerCase();
|
|
186
|
+
result = result.filter(m => m.title.toLowerCase().includes(q) || m.content.toLowerCase().includes(q));
|
|
187
|
+
}
|
|
188
|
+
return result.slice(0, opts?.limit || 50);
|
|
189
|
+
},
|
|
190
|
+
get(id) {
|
|
191
|
+
return memoryStore.memories.find(m => m.id === id) || null;
|
|
192
|
+
},
|
|
193
|
+
delete(id) {
|
|
194
|
+
const idx = memoryStore.memories.findIndex(m => m.id === id);
|
|
195
|
+
if (idx >= 0) { memoryStore.memories.splice(idx, 1); saveToFile(); return true; }
|
|
196
|
+
return false;
|
|
197
|
+
},
|
|
198
|
+
count() {
|
|
199
|
+
return memoryStore.memories.length;
|
|
200
|
+
},
|
|
201
|
+
tags() {
|
|
202
|
+
return memoryStore.tags;
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
function formatAge(ms) {
|
|
207
|
+
const m = Math.floor(ms / 60000), h = Math.floor(ms / 3600000), d = Math.floor(ms / 86400000);
|
|
208
|
+
if (m < 1) return 'just now';
|
|
209
|
+
if (m < 60) return `${m}m ago`;
|
|
210
|
+
if (h < 24) return `${h}h ago`;
|
|
211
|
+
return `${d}d ago`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function sanitizeFilename(name) {
|
|
215
|
+
return name.replace(/[<>:"/\\|?*]/g, '-').replace(/\s+/g, '-').slice(0, 100);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function validateLicense(key) {
|
|
219
|
+
if (!key) return { valid: false, error: 'No license key provided' };
|
|
220
|
+
|
|
221
|
+
const pattern = /^MEMORY-PRO-([A-Z0-9]{4})-([A-Z0-9]{4})-([A-Z0-9]{4})-([A-Z0-9]{4})$/;
|
|
222
|
+
const match = key.match(pattern);
|
|
223
|
+
|
|
224
|
+
if (!match) return { valid: false, error: 'Invalid license key format' };
|
|
225
|
+
|
|
226
|
+
const checksum = match[1].charCodeAt(0) + match[2].charCodeAt(0) + match[3].charCodeAt(0) + match[4].charCodeAt(0);
|
|
227
|
+
if (checksum % 7 !== 0) return { valid: false, error: 'Invalid license key checksum' };
|
|
228
|
+
|
|
229
|
+
return { valid: true, key: key, tier: 'PRO' };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function activateLicense(key) {
|
|
233
|
+
const result = validateLicense(key);
|
|
234
|
+
|
|
235
|
+
if (!result.valid) {
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
licenseStore.licenseKey = result.key;
|
|
240
|
+
licenseStore.isPro = true;
|
|
241
|
+
licenseStore.activatedAt = Date.now();
|
|
242
|
+
saveLicense();
|
|
243
|
+
|
|
244
|
+
return { success: true, tier: 'PRO', message: 'License activated! Enjoy unlimited memories.' };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getLicenseStatus() {
|
|
248
|
+
if (licenseStore.isPro) {
|
|
249
|
+
return {
|
|
250
|
+
tier: 'PRO',
|
|
251
|
+
activated: licenseStore.activatedAt ? new Date(licenseStore.activatedAt).toLocaleDateString() : null,
|
|
252
|
+
memoryLimit: 'Unlimited',
|
|
253
|
+
features: ['Unlimited memories', 'Priority extraction', 'Cloud export', 'Custom tags', 'Priority support']
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
tier: 'FREE',
|
|
258
|
+
activated: null,
|
|
259
|
+
memoryLimit: `${memoryStore.memories.length}/${FREE_MEMORY_LIMIT}`,
|
|
260
|
+
features: ['500 memories', 'Basic extraction', 'Standard export', '6 default tags']
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default {
|
|
265
|
+
async onLoad(api) {
|
|
266
|
+
api.command.register(() => [
|
|
267
|
+
{
|
|
268
|
+
title: 'Extract memories from conversation',
|
|
269
|
+
value: '/extract',
|
|
270
|
+
keybind: 'ctrl+e',
|
|
271
|
+
async onSelect() {
|
|
272
|
+
const session = api.state.session?.current?.();
|
|
273
|
+
if (!session) { api.ui.toast?.({ variant: 'warning', title: 'No active session' }); return; }
|
|
274
|
+
api.ui.toast?.({ variant: 'info', title: 'Extracting...', message: 'Analyzing conversation' });
|
|
275
|
+
|
|
276
|
+
const messages = api.state.session?.messages?.(session.id) || [];
|
|
277
|
+
const candidates = extractCandidates(messages);
|
|
278
|
+
|
|
279
|
+
if (candidates.length === 0) {
|
|
280
|
+
api.ui.toast?.({ variant: 'info', title: 'No memories extracted', message: 'No patterns found' });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let added = 0;
|
|
285
|
+
for (const c of candidates.slice(0, 10)) {
|
|
286
|
+
const result = memoryApi.create({ ...c, session_id: session.id });
|
|
287
|
+
if (result.error === 'FREE_LIMIT') {
|
|
288
|
+
api.ui.toast?.({ variant: 'warning', title: 'Limit reached', message: 'Upgrade to PRO for unlimited memories!' });
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
if (!result.error) added++;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const status = getLicenseStatus();
|
|
295
|
+
api.ui.toast?.({ variant: 'success', title: `Extracted ${added} memories`, message: `${status.memoryLimit}` });
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
title: 'Browse memories',
|
|
300
|
+
value: '/memory',
|
|
301
|
+
keybind: 'ctrl+m',
|
|
302
|
+
async onSelect() {
|
|
303
|
+
const memories = memoryApi.list({ limit: 20 });
|
|
304
|
+
const status = getLicenseStatus();
|
|
305
|
+
|
|
306
|
+
if (memories.length === 0) {
|
|
307
|
+
api.ui.toast?.({ variant: 'info', title: 'No Memories', message: 'Use /extract first' });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const formatList = (items) => items.map(m => {
|
|
311
|
+
const icon = TOPIC_KEYWORDS[m.memory_type]?.icon || 'š';
|
|
312
|
+
return `${icon} ${m.title.slice(0, 40)}${m.title.length > 40 ? '...' : ''}`;
|
|
313
|
+
}).join('\n');
|
|
314
|
+
api.ui.dialog?.alert?.({ title: `š§ Memories (${status.tier}) ${status.memoryLimit}`, message: formatList(memories) });
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
title: 'Search memories',
|
|
319
|
+
value: '/m-search',
|
|
320
|
+
async onSelect() {
|
|
321
|
+
const q = await api.ui.dialog?.prompt?.({ title: 'Search', message: 'Search term:' });
|
|
322
|
+
if (!q) return;
|
|
323
|
+
const results = memoryApi.list({ search: q });
|
|
324
|
+
if (results.length === 0) { api.ui.toast?.({ variant: 'info', title: 'No results' }); return; }
|
|
325
|
+
api.ui.dialog?.alert?.({ title: `Results (${results.length})`, message: results.map(r => `⢠${r.title}`).join('\n') });
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
title: 'Filter by type',
|
|
330
|
+
value: '/m-filter',
|
|
331
|
+
async onSelect() {
|
|
332
|
+
const types = ['all', 'concept', 'preference', 'code_pattern', 'fact', 'project'];
|
|
333
|
+
const selected = await api.ui.dialog?.select?.({ title: 'Filter', options: types.map(t => ({ label: t, value: t })) });
|
|
334
|
+
if (!selected) return;
|
|
335
|
+
const results = selected === 'all' ? memoryApi.list({ limit: 20 }) : memoryApi.list({ type: selected, limit: 20 });
|
|
336
|
+
api.ui.dialog?.alert?.({ title: `${selected} (${results.length})`, message: results.map(r => `⢠${r.title}`).join('\n') || 'None' });
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
title: 'Delete memory',
|
|
341
|
+
value: '/m-delete',
|
|
342
|
+
async onSelect() {
|
|
343
|
+
const id = await api.ui.dialog?.prompt?.({ title: 'Delete', message: 'Memory ID:' });
|
|
344
|
+
if (!id) return;
|
|
345
|
+
const ok = await api.ui.dialog?.confirm?.({ title: 'Confirm Delete', message: `Delete ${id}?` });
|
|
346
|
+
if (ok && memoryApi.delete(id)) api.ui.toast?.({ variant: 'success', title: 'Deleted' });
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
title: 'Resume last session',
|
|
351
|
+
value: '/resume',
|
|
352
|
+
keybind: 'ctrl+r',
|
|
353
|
+
async onSelect() {
|
|
354
|
+
const sessions = await api.session?.list?.({ roots: true, limit: 1 });
|
|
355
|
+
if (sessions?.[0]) api.session?.resume?.(sessions[0].id);
|
|
356
|
+
else api.ui.toast?.({ variant: 'warning', title: 'No sessions' });
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
title: 'List sessions',
|
|
361
|
+
value: '/sessions',
|
|
362
|
+
async onSelect() {
|
|
363
|
+
const sessions = await api.session?.list?.({ roots: true, limit: 10 });
|
|
364
|
+
if (!sessions?.length) { api.ui.toast?.({ variant: 'info', title: 'No sessions' }); return; }
|
|
365
|
+
const list = sessions.map((s, i) => `${i + 1}. ${s.title?.slice(0, 35)} (${formatAge(Date.now() - s.time?.updated)})`).join('\n');
|
|
366
|
+
api.ui.dialog?.alert?.({ title: 'Recent Sessions', message: list });
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
title: 'Show topics',
|
|
371
|
+
value: '/topics',
|
|
372
|
+
async onSelect() {
|
|
373
|
+
const session = api.state.session?.current?.();
|
|
374
|
+
if (!session) return;
|
|
375
|
+
const messages = api.state.session?.messages?.(session.id) || [];
|
|
376
|
+
let fullText = '';
|
|
377
|
+
for (const m of messages) fullText += extractTextFromParts(m.parts || m.content) + ' ';
|
|
378
|
+
const topics = detectTopics(fullText);
|
|
379
|
+
if (!topics.length) { api.ui.toast?.({ variant: 'info', title: 'No topics' }); return; }
|
|
380
|
+
const display = topics.map(t => `${TOPIC_KEYWORDS[t]?.icon || 'š'} ${t}`).join(', ');
|
|
381
|
+
api.ui.toast?.({ variant: 'success', title: 'Topics', message: display });
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
title: 'Export to Obsidian',
|
|
386
|
+
value: '/export',
|
|
387
|
+
keybind: 'ctrl+x',
|
|
388
|
+
async onSelect() {
|
|
389
|
+
const vaultPath = await api.ui.dialog?.prompt?.({
|
|
390
|
+
title: 'Export',
|
|
391
|
+
message: 'Vault path:',
|
|
392
|
+
placeholder: join(process.env.HOME || '~', 'Documents', 'Obsidian', 'opencode-memories')
|
|
393
|
+
});
|
|
394
|
+
if (!vaultPath) return;
|
|
395
|
+
|
|
396
|
+
const memories = memoryApi.list({ limit: licenseStore.isPro ? 10000 : 500 });
|
|
397
|
+
const folders = new Map();
|
|
398
|
+
|
|
399
|
+
for (const m of memories) {
|
|
400
|
+
const folder = m.memory_type || 'general';
|
|
401
|
+
if (!folders.has(folder)) folders.set(folder, []);
|
|
402
|
+
folders.get(folder).push(m);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
for (const [folder, items] of folders) {
|
|
407
|
+
const folderPath = join(vaultPath, folder);
|
|
408
|
+
if (!existsSync(folderPath)) mkdirSync(folderPath, { recursive: true });
|
|
409
|
+
for (const mem of items) {
|
|
410
|
+
const content = `---\nid: ${mem.id}\ntype: ${mem.memory_type}\nimportance: ${mem.importance}\ncreated: ${new Date(mem.created_at).toISOString()}\n---\n\n# ${mem.title}\n\n${mem.content}\n`;
|
|
411
|
+
writeFileSync(join(folderPath, sanitizeFilename(mem.title) + '.md'), content);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
api.ui.toast?.({ variant: 'success', title: 'Exported!', message: `${memories.length} memories to ${vaultPath}` });
|
|
415
|
+
} catch (e) {
|
|
416
|
+
api.ui.toast?.({ variant: 'error', title: 'Export failed', message: e.message });
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
title: 'Memory stats',
|
|
422
|
+
value: '/memory-stats',
|
|
423
|
+
async onSelect() {
|
|
424
|
+
const status = getLicenseStatus();
|
|
425
|
+
const byType = {};
|
|
426
|
+
for (const m of memoryStore.memories) byType[m.memory_type] = (byType[m.memory_type] || 0) + 1;
|
|
427
|
+
const stats = Object.entries(byType).map(([k, v]) => `${TOPIC_KEYWORDS[k]?.icon || 'š'} ${k}: ${v}`).join('\n');
|
|
428
|
+
api.ui.dialog?.alert?.({ title: `š ${status.tier} Stats`, message: `${stats || 'No memories yet'}\n\nTier: ${status.tier}\nMemories: ${status.memoryLimit}` });
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
title: 'Upgrade to PRO',
|
|
433
|
+
value: '/upgrade',
|
|
434
|
+
async onSelect() {
|
|
435
|
+
const status = getLicenseStatus();
|
|
436
|
+
|
|
437
|
+
if (status.tier === 'PRO') {
|
|
438
|
+
api.ui.toast?.({ variant: 'info', title: 'Already PRO!', message: 'You have unlimited memories.' });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
api.ui.dialog?.alert?.({
|
|
443
|
+
title: 'š Upgrade to PRO',
|
|
444
|
+
message: `Current: FREE (${memoryStore.memories.length}/${FREE_MEMORY_LIMIT} memories)\n\nPRO Benefits:\n⢠Unlimited memories (10,000+)\n⢠Priority extraction\n⢠Custom tags\n⢠Priority support\n⢠Early access to new features\n\nPrice: $29 one-time\nLifetime license, no subscription!\n\nTo purchase, visit:\nhttps://anomarynox.gumroad.com/memory-plugin\n\nAfter purchase, activate with:\n/activate <license-key>`
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
title: 'Activate license',
|
|
450
|
+
value: '/activate',
|
|
451
|
+
async onSelect() {
|
|
452
|
+
const key = await api.ui.dialog?.prompt?.({
|
|
453
|
+
title: 'š Activate License',
|
|
454
|
+
message: 'Enter your license key:',
|
|
455
|
+
placeholder: 'MEMORY-PRO-XXXX-XXXX-XXXX-XXXX'
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
if (!key) return;
|
|
459
|
+
|
|
460
|
+
const result = activateLicense(key);
|
|
461
|
+
|
|
462
|
+
if (result.success) {
|
|
463
|
+
api.ui.toast?.({ variant: 'success', title: 'š PRO Activated!', message: result.message });
|
|
464
|
+
} else {
|
|
465
|
+
api.ui.toast?.({ variant: 'error', title: 'Invalid License', message: result.error });
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
title: 'License status',
|
|
471
|
+
value: '/license',
|
|
472
|
+
async onSelect() {
|
|
473
|
+
const status = getLicenseStatus();
|
|
474
|
+
const features = status.features.map(f => `⢠${f}`).join('\n');
|
|
475
|
+
api.ui.dialog?.alert?.({
|
|
476
|
+
title: `š License Status: ${status.tier}`,
|
|
477
|
+
message: `Tier: ${status.tier}\nActivated: ${status.activated || 'Not activated'}\nMemories: ${status.memoryLimit}\n\nFeatures:\n${features}\n\n${status.tier === 'FREE' ? '\nUpgrade: /upgrade' : '\nThank you for supporting!'}`)
|
|
478
|
+
});
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
]);
|
|
482
|
+
|
|
483
|
+
api.event?.on?.('app.started', async () => {
|
|
484
|
+
setTimeout(async () => {
|
|
485
|
+
try {
|
|
486
|
+
const sessions = await api.session?.list?.({ roots: true, limit: 1 });
|
|
487
|
+
if (!sessions?.length) return;
|
|
488
|
+
const last = sessions[0];
|
|
489
|
+
if (!last?.time?.updated) return;
|
|
490
|
+
const age = Date.now() - last.time.updated;
|
|
491
|
+
if (age > 7 * 86400000) return;
|
|
492
|
+
|
|
493
|
+
const state = api.kv?.get?.('state') || {};
|
|
494
|
+
if (state[SESSION_PROMPT_KEY] === last.id) return;
|
|
495
|
+
|
|
496
|
+
const ageStr = formatAge(age);
|
|
497
|
+
const ok = await api.ui.dialog?.confirm?.({
|
|
498
|
+
title: 'Resume Session?',
|
|
499
|
+
message: `"${last.title}"\n\nLast: ${ageStr}`,
|
|
500
|
+
confirmText: 'Resume',
|
|
501
|
+
cancelText: 'New',
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
if (ok) api.session?.resume?.(last.id);
|
|
505
|
+
else api.kv?.set?.('state', { ...state, [SESSION_PROMPT_KEY]: last.id });
|
|
506
|
+
} catch (e) {}
|
|
507
|
+
}, 2000);
|
|
508
|
+
});
|
|
509
|
+
},
|
|
510
|
+
};
|