@xanomyrox/opencode-memory 1.0.0 → 1.0.1
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/GUMROAD-SETUP.md +151 -0
- package/license-gen.cjs +147 -0
- package/package.json +1 -1
- package/sold-keys.json +6 -0
- package/src/index.js +22 -19
package/GUMROAD-SETUP.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Gumroad Setup Guide for @xanomyrox/opencode-memory
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This guide explains how to sell PRO licenses for your OpenCode Memory plugin using Gumroad.
|
|
6
|
+
|
|
7
|
+
## Workflow
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Buyer purchases on Gumroad
|
|
11
|
+
↓
|
|
12
|
+
Gumroad sends purchase email with instructions
|
|
13
|
+
↓
|
|
14
|
+
You generate license key (using license-gen.cjs)
|
|
15
|
+
↓
|
|
16
|
+
You email the license key to buyer
|
|
17
|
+
↓
|
|
18
|
+
Buyer activates with /activate <key>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Step 1: Create Gumroad Account
|
|
22
|
+
|
|
23
|
+
1. Go to: https://gumroad.com
|
|
24
|
+
2. Sign up / Login
|
|
25
|
+
3. Verify your email
|
|
26
|
+
|
|
27
|
+
## Step 2: Create Product
|
|
28
|
+
|
|
29
|
+
1. Click **"New Product"**
|
|
30
|
+
|
|
31
|
+
2. **Product Details:**
|
|
32
|
+
- **Name**: `OpenCode Memory PRO`
|
|
33
|
+
- **Description**:
|
|
34
|
+
```
|
|
35
|
+
Lifetime PRO license for @xanomyrox/opencode-memory
|
|
36
|
+
|
|
37
|
+
Includes:
|
|
38
|
+
- Unlimited memories (10,000+)
|
|
39
|
+
- Priority extraction
|
|
40
|
+
- Custom tags
|
|
41
|
+
- Priority support
|
|
42
|
+
- Lifetime updates
|
|
43
|
+
|
|
44
|
+
After purchase, contact the seller to receive your license key.
|
|
45
|
+
```
|
|
46
|
+
- **Price**: `$29` (one-time)
|
|
47
|
+
|
|
48
|
+
3. **Product Type**: Select **"Single product"**
|
|
49
|
+
|
|
50
|
+
4. **Customization**:
|
|
51
|
+
- Add a nice cover image
|
|
52
|
+
- Add screenshots of the plugin
|
|
53
|
+
|
|
54
|
+
5. Click **"Publish"**
|
|
55
|
+
|
|
56
|
+
## Step 3: Update README
|
|
57
|
+
|
|
58
|
+
Update your Gumroad URL in the plugin. The URL should be like:
|
|
59
|
+
```
|
|
60
|
+
https://xanomyrox.gumroad.com/l/opencode-memory-pro
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Step 4: Generate & Deliver License Keys
|
|
64
|
+
|
|
65
|
+
### Generate a Key:
|
|
66
|
+
```bash
|
|
67
|
+
cd ~/.config/opencode/plugins/@xanomyrox/opencode-memory
|
|
68
|
+
node license-gen.cjs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Example Output:
|
|
72
|
+
```
|
|
73
|
+
========================================
|
|
74
|
+
NEW LICENSE KEY GENERATED!
|
|
75
|
+
========================================
|
|
76
|
+
|
|
77
|
+
MEMORY-PRO-PG7N-2ZAT-GNE5-9K3O
|
|
78
|
+
|
|
79
|
+
========================================
|
|
80
|
+
|
|
81
|
+
Order #1
|
|
82
|
+
Generated: 2026-04-01T23:51:57.480Z
|
|
83
|
+
|
|
84
|
+
Send this key to the buyer!
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Deliver to Buyer:
|
|
88
|
+
1. Check your Gumroad sales dashboard
|
|
89
|
+
2. Find the buyer's email
|
|
90
|
+
3. Email them their license key
|
|
91
|
+
|
|
92
|
+
## Step 5: Buyer Activates
|
|
93
|
+
|
|
94
|
+
Buyer runs in OpenCode:
|
|
95
|
+
```
|
|
96
|
+
/activate MEMORY-PRO-PG7N-2ZAT-GNE5-9K3O
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Automating Delivery (Optional)
|
|
100
|
+
|
|
101
|
+
For automatic key delivery, you can:
|
|
102
|
+
|
|
103
|
+
1. Use Gumroad's **Overlay** feature
|
|
104
|
+
2. Set up an email auto-responder
|
|
105
|
+
3. Or use Gumroad's **API** with a custom backend
|
|
106
|
+
|
|
107
|
+
## Pricing Options
|
|
108
|
+
|
|
109
|
+
| Option | Price | Recommended |
|
|
110
|
+
|--------|-------|-------------|
|
|
111
|
+
| Lifetime | $29 | ✅ Best for you |
|
|
112
|
+
| Monthly | $5/month | Better for income |
|
|
113
|
+
| Freemium | $0/$9 | Complex setup |
|
|
114
|
+
|
|
115
|
+
## Gumroad Fees
|
|
116
|
+
|
|
117
|
+
| Sales | Gumroad Fee |
|
|
118
|
+
|-------|-------------|
|
|
119
|
+
| $0-500 | 10% |
|
|
120
|
+
| $500+ | 5% |
|
|
121
|
+
| With Gumroad Pro | 0% + $10/month |
|
|
122
|
+
|
|
123
|
+
## Support Email Template
|
|
124
|
+
|
|
125
|
+
When a buyer purchases, email them:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
Subject: Your OpenCode Memory PRO License Key
|
|
129
|
+
|
|
130
|
+
Hi,
|
|
131
|
+
|
|
132
|
+
Thank you for purchasing OpenCode Memory PRO!
|
|
133
|
+
|
|
134
|
+
Your license key:
|
|
135
|
+
MEMORY-PRO-XXXX-XXXX-XXXX-XXXX
|
|
136
|
+
|
|
137
|
+
To activate:
|
|
138
|
+
1. Install the plugin: opencode plugin install @xanomyrox/opencode-memory
|
|
139
|
+
2. Open OpenCode
|
|
140
|
+
3. Type: /activate MEMORY-PRO-XXXX-XXXX-XXXX-XXXX
|
|
141
|
+
|
|
142
|
+
Need help? Reply to this email!
|
|
143
|
+
|
|
144
|
+
Best regards,
|
|
145
|
+
xanomyrox
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Files
|
|
149
|
+
|
|
150
|
+
- `license-gen.cjs` - Generate license keys
|
|
151
|
+
- `sold-keys.json` - Records of all keys generated
|
package/license-gen.cjs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const KEYS_FILE = path.join(__dirname, 'sold-keys.json');
|
|
7
|
+
|
|
8
|
+
function generateLicenseKey() {
|
|
9
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
10
|
+
const segments = [];
|
|
11
|
+
|
|
12
|
+
for (let s = 0; s < 4; s++) {
|
|
13
|
+
let segment = '';
|
|
14
|
+
for (let i = 0; i < 4; i++) {
|
|
15
|
+
segment += chars[Math.floor(Math.random() * chars.length)];
|
|
16
|
+
}
|
|
17
|
+
segments.push(segment);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const checksum = segments.join('').split('').reduce((a, c) => a + c.charCodeAt(0), 0);
|
|
21
|
+
const validChecksum = checksum % 7 === 0;
|
|
22
|
+
|
|
23
|
+
if (!validChecksum) {
|
|
24
|
+
return generateLicenseKey();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return `MEMORY-PRO-${segments.join('-')}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadSoldKeys() {
|
|
31
|
+
try {
|
|
32
|
+
if (fs.existsSync(KEYS_FILE)) {
|
|
33
|
+
return JSON.parse(fs.readFileSync(KEYS_FILE, 'utf-8'));
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {}
|
|
36
|
+
return { keys: [], sold: 0 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function saveSoldKeys(data) {
|
|
40
|
+
fs.writeFileSync(KEYS_FILE, JSON.stringify(data, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function generateNewKey() {
|
|
44
|
+
const key = generateLicenseKey();
|
|
45
|
+
const data = loadSoldKeys();
|
|
46
|
+
|
|
47
|
+
if (data.keys.includes(key)) {
|
|
48
|
+
return generateNewKey();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
data.keys.push(key);
|
|
52
|
+
data.sold++;
|
|
53
|
+
saveSoldKeys(data);
|
|
54
|
+
|
|
55
|
+
console.log('\n========================================');
|
|
56
|
+
console.log(' NEW LICENSE KEY GENERATED!');
|
|
57
|
+
console.log('========================================\n');
|
|
58
|
+
console.log(` ${key}`);
|
|
59
|
+
console.log('\n========================================\n');
|
|
60
|
+
console.log(` Order #${data.sold}`);
|
|
61
|
+
console.log(` Generated: ${new Date().toISOString()}\n`);
|
|
62
|
+
console.log(' Send this key to the buyer!\n');
|
|
63
|
+
|
|
64
|
+
return { key, order: data.sold };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function listKeys() {
|
|
68
|
+
const data = loadSoldKeys();
|
|
69
|
+
console.log(`\n Total Keys Generated: ${data.sold}`);
|
|
70
|
+
console.log(` Total Keys in File: ${data.keys.length}\n`);
|
|
71
|
+
|
|
72
|
+
if (data.keys.length > 0) {
|
|
73
|
+
console.log(' Recent Keys:');
|
|
74
|
+
data.keys.slice(-10).forEach((key, i) => {
|
|
75
|
+
console.log(` ${i + 1}. ${key}`);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
console.log('');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function showHelp() {
|
|
82
|
+
console.log(`
|
|
83
|
+
License Key Generator for @xanomyrox/opencode-memory
|
|
84
|
+
|
|
85
|
+
Usage:
|
|
86
|
+
node license-gen.js Generate new key
|
|
87
|
+
node license-gen.js list List all keys
|
|
88
|
+
node license-gen.js check <key> Check if key is valid
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
node license-gen.js
|
|
92
|
+
node license-gen.js list
|
|
93
|
+
node license-gen.js check MEMORY-PRO-XXXX-XXXX-XXXX-XXXX
|
|
94
|
+
`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function checkKey(key) {
|
|
98
|
+
const data = loadSoldKeys();
|
|
99
|
+
const isSold = data.keys.includes(key);
|
|
100
|
+
|
|
101
|
+
const pattern = /^MEMORY-PRO-([A-Z0-9]{4})-([A-Z0-9]{4})-([A-Z0-9]{4})-([A-Z0-9]{4})$/;
|
|
102
|
+
const match = key.match(pattern);
|
|
103
|
+
|
|
104
|
+
if (!match) {
|
|
105
|
+
console.log('\n Invalid format\n');
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const checksum = key.replace('MEMORY-PRO-', '').replace(/-/g, '').split('').reduce((a, c) => a + c.charCodeAt(0), 0);
|
|
110
|
+
const validChecksum = checksum % 7 === 0;
|
|
111
|
+
|
|
112
|
+
console.log('\n========================================');
|
|
113
|
+
console.log(' LICENSE KEY CHECK');
|
|
114
|
+
console.log('========================================\n');
|
|
115
|
+
console.log(` Key: ${key}`);
|
|
116
|
+
console.log(` Format: ${match ? 'Valid' : 'Invalid'}`);
|
|
117
|
+
console.log(` Checksum: ${validChecksum ? 'Valid' : 'Invalid'}`);
|
|
118
|
+
console.log(` Sold: ${isSold ? 'Yes ✅' : 'No ❌'}`);
|
|
119
|
+
console.log('\n========================================\n');
|
|
120
|
+
|
|
121
|
+
return isSold && validChecksum;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const cmd = process.argv[2];
|
|
125
|
+
|
|
126
|
+
switch (cmd) {
|
|
127
|
+
case 'list':
|
|
128
|
+
listKeys();
|
|
129
|
+
break;
|
|
130
|
+
case 'check':
|
|
131
|
+
const keyToCheck = process.argv[3];
|
|
132
|
+
if (keyToCheck) {
|
|
133
|
+
checkKey(keyToCheck);
|
|
134
|
+
} else {
|
|
135
|
+
console.log('\n Please provide a key to check\n');
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
case 'help':
|
|
139
|
+
showHelp();
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
if (cmd) {
|
|
143
|
+
showHelp();
|
|
144
|
+
} else {
|
|
145
|
+
generateNewKey();
|
|
146
|
+
}
|
|
147
|
+
}
|
package/package.json
CHANGED
package/sold-keys.json
ADDED
package/src/index.js
CHANGED
|
@@ -262,8 +262,9 @@ function getLicenseStatus() {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
export default {
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
id: 'opencode-memory',
|
|
266
|
+
tui: async (api, options, meta) => {
|
|
267
|
+
const unregister = api.command.register(() => [
|
|
267
268
|
{
|
|
268
269
|
title: 'Extract memories from conversation',
|
|
269
270
|
value: '/extract',
|
|
@@ -311,18 +312,18 @@ export default {
|
|
|
311
312
|
const icon = TOPIC_KEYWORDS[m.memory_type]?.icon || '📋';
|
|
312
313
|
return `${icon} ${m.title.slice(0, 40)}${m.title.length > 40 ? '...' : ''}`;
|
|
313
314
|
}).join('\n');
|
|
314
|
-
api.ui.dialog
|
|
315
|
+
api.ui.dialog.alert?.({ title: `🧠 Memories (${status.tier}) ${status.memoryLimit}`, message: formatList(memories) });
|
|
315
316
|
},
|
|
316
317
|
},
|
|
317
318
|
{
|
|
318
319
|
title: 'Search memories',
|
|
319
320
|
value: '/m-search',
|
|
320
321
|
async onSelect() {
|
|
321
|
-
const q = await api.ui.dialog
|
|
322
|
+
const q = await api.ui.dialog.prompt?.({ title: 'Search', message: 'Search term:' });
|
|
322
323
|
if (!q) return;
|
|
323
324
|
const results = memoryApi.list({ search: q });
|
|
324
325
|
if (results.length === 0) { api.ui.toast?.({ variant: 'info', title: 'No results' }); return; }
|
|
325
|
-
api.ui.dialog
|
|
326
|
+
api.ui.dialog.alert?.({ title: `Results (${results.length})`, message: results.map(r => `• ${r.title}`).join('\n') });
|
|
326
327
|
},
|
|
327
328
|
},
|
|
328
329
|
{
|
|
@@ -330,19 +331,19 @@ export default {
|
|
|
330
331
|
value: '/m-filter',
|
|
331
332
|
async onSelect() {
|
|
332
333
|
const types = ['all', 'concept', 'preference', 'code_pattern', 'fact', 'project'];
|
|
333
|
-
const selected = await api.ui.dialog
|
|
334
|
+
const selected = await api.ui.dialog.select?.({ title: 'Filter', options: types.map(t => ({ label: t, value: t })) });
|
|
334
335
|
if (!selected) return;
|
|
335
336
|
const results = selected === 'all' ? memoryApi.list({ limit: 20 }) : memoryApi.list({ type: selected, limit: 20 });
|
|
336
|
-
api.ui.dialog
|
|
337
|
+
api.ui.dialog.alert?.({ title: `${selected} (${results.length})`, message: results.map(r => `• ${r.title}`).join('\n') || 'None' });
|
|
337
338
|
},
|
|
338
339
|
},
|
|
339
340
|
{
|
|
340
341
|
title: 'Delete memory',
|
|
341
342
|
value: '/m-delete',
|
|
342
343
|
async onSelect() {
|
|
343
|
-
const id = await api.ui.dialog
|
|
344
|
+
const id = await api.ui.dialog.prompt?.({ title: 'Delete', message: 'Memory ID:' });
|
|
344
345
|
if (!id) return;
|
|
345
|
-
const ok = await api.ui.dialog
|
|
346
|
+
const ok = await api.ui.dialog.confirm?.({ title: 'Confirm Delete', message: `Delete ${id}?` });
|
|
346
347
|
if (ok && memoryApi.delete(id)) api.ui.toast?.({ variant: 'success', title: 'Deleted' });
|
|
347
348
|
},
|
|
348
349
|
},
|
|
@@ -363,7 +364,7 @@ export default {
|
|
|
363
364
|
const sessions = await api.session?.list?.({ roots: true, limit: 10 });
|
|
364
365
|
if (!sessions?.length) { api.ui.toast?.({ variant: 'info', title: 'No sessions' }); return; }
|
|
365
366
|
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
|
|
367
|
+
api.ui.dialog.alert?.({ title: 'Recent Sessions', message: list });
|
|
367
368
|
},
|
|
368
369
|
},
|
|
369
370
|
{
|
|
@@ -386,7 +387,7 @@ export default {
|
|
|
386
387
|
value: '/export',
|
|
387
388
|
keybind: 'ctrl+x',
|
|
388
389
|
async onSelect() {
|
|
389
|
-
const vaultPath = await api.ui.dialog
|
|
390
|
+
const vaultPath = await api.ui.dialog.prompt?.({
|
|
390
391
|
title: 'Export',
|
|
391
392
|
message: 'Vault path:',
|
|
392
393
|
placeholder: join(process.env.HOME || '~', 'Documents', 'Obsidian', 'opencode-memories')
|
|
@@ -425,7 +426,7 @@ export default {
|
|
|
425
426
|
const byType = {};
|
|
426
427
|
for (const m of memoryStore.memories) byType[m.memory_type] = (byType[m.memory_type] || 0) + 1;
|
|
427
428
|
const stats = Object.entries(byType).map(([k, v]) => `${TOPIC_KEYWORDS[k]?.icon || '📋'} ${k}: ${v}`).join('\n');
|
|
428
|
-
api.ui.dialog
|
|
429
|
+
api.ui.dialog.alert?.({ title: `📊 ${status.tier} Stats`, message: `${stats || 'No memories yet'}\n\nTier: ${status.tier}\nMemories: ${status.memoryLimit}` });
|
|
429
430
|
},
|
|
430
431
|
},
|
|
431
432
|
{
|
|
@@ -439,9 +440,9 @@ export default {
|
|
|
439
440
|
return;
|
|
440
441
|
}
|
|
441
442
|
|
|
442
|
-
api.ui.dialog
|
|
443
|
+
api.ui.dialog.alert?.({
|
|
443
444
|
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://
|
|
445
|
+
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://xanomyrox.gumroad.com/l/xhfmla\n\nAfter purchase, activate with:\n/activate <license-key>`
|
|
445
446
|
});
|
|
446
447
|
},
|
|
447
448
|
},
|
|
@@ -449,7 +450,7 @@ export default {
|
|
|
449
450
|
title: 'Activate license',
|
|
450
451
|
value: '/activate',
|
|
451
452
|
async onSelect() {
|
|
452
|
-
const key = await api.ui.dialog
|
|
453
|
+
const key = await api.ui.dialog.prompt?.({
|
|
453
454
|
title: '🔑 Activate License',
|
|
454
455
|
message: 'Enter your license key:',
|
|
455
456
|
placeholder: 'MEMORY-PRO-XXXX-XXXX-XXXX-XXXX'
|
|
@@ -472,7 +473,7 @@ export default {
|
|
|
472
473
|
async onSelect() {
|
|
473
474
|
const status = getLicenseStatus();
|
|
474
475
|
const features = status.features.map(f => `• ${f}`).join('\n');
|
|
475
|
-
api.ui.dialog
|
|
476
|
+
api.ui.dialog.alert?.({
|
|
476
477
|
title: `📋 License Status: ${status.tier}`,
|
|
477
478
|
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,7 +481,7 @@ export default {
|
|
|
480
481
|
},
|
|
481
482
|
]);
|
|
482
483
|
|
|
483
|
-
api.event
|
|
484
|
+
api.event.on('app.started', async () => {
|
|
484
485
|
setTimeout(async () => {
|
|
485
486
|
try {
|
|
486
487
|
const sessions = await api.session?.list?.({ roots: true, limit: 1 });
|
|
@@ -494,7 +495,7 @@ export default {
|
|
|
494
495
|
if (state[SESSION_PROMPT_KEY] === last.id) return;
|
|
495
496
|
|
|
496
497
|
const ageStr = formatAge(age);
|
|
497
|
-
const ok = await api.ui.dialog
|
|
498
|
+
const ok = await api.ui.dialog.confirm({
|
|
498
499
|
title: 'Resume Session?',
|
|
499
500
|
message: `"${last.title}"\n\nLast: ${ageStr}`,
|
|
500
501
|
confirmText: 'Resume',
|
|
@@ -506,5 +507,7 @@ export default {
|
|
|
506
507
|
} catch (e) {}
|
|
507
508
|
}, 2000);
|
|
508
509
|
});
|
|
509
|
-
|
|
510
|
+
|
|
511
|
+
return () => unregister();
|
|
512
|
+
}
|
|
510
513
|
};
|