@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -0
  3. package/package.json +25 -0
  4. 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
+ };