tx-ai-db-pro 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/README.md +104 -0
- package/package.json +43 -0
- package/src/github-key-server.js +99 -0
- package/src/index.js +301 -0
- package/src/license.js +136 -0
- package/src/postinstall.js +17 -0
- package/src/setup-wizard.js +144 -0
- package/src/unlock.js +156 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# tx-ai-db-pro
|
|
2
|
+
|
|
3
|
+
Premium SQLite wrapper for AI conversations with auto-migration, search, and formatted responses.
|
|
4
|
+
|
|
5
|
+
**OpenAI API Compatible Format only.**
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install tx-ai-db-pro
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**This package is FREE to install and use for basic operations (retrieve, update, delete).**
|
|
14
|
+
|
|
15
|
+
## Unlock Pro Features ($40)
|
|
16
|
+
|
|
17
|
+
**Pro Features:**
|
|
18
|
+
- Auto-migration: Point to conversation folder, auto-discovers and imports ALL conversations (2,000+)
|
|
19
|
+
- Search: Drop JSON with search term, get formatted results
|
|
20
|
+
- Response formatting: Specify title, summary format, etc.
|
|
21
|
+
- File cleanup: Auto-remove or backup after processing
|
|
22
|
+
|
|
23
|
+
**How to Unlock:**
|
|
24
|
+
|
|
25
|
+
1. **Install the package:**
|
|
26
|
+
```bash
|
|
27
|
+
npm install tx-ai-db-pro
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
2. **Sponsor at $40 tier** on [GitHub Sponsors](https://github.com/sponsors/TX-AI-Series)
|
|
31
|
+
|
|
32
|
+
3. **Wait for key activation** (up to 48 hours - we process keys as quickly as possible, but weekends/holidays may cause delays)
|
|
33
|
+
|
|
34
|
+
4. **Run the unlock wizard:**
|
|
35
|
+
```bash
|
|
36
|
+
npm run unlock
|
|
37
|
+
```
|
|
38
|
+
Enter your GitHub username when prompted
|
|
39
|
+
|
|
40
|
+
5. **Pro features unlocked!**
|
|
41
|
+
|
|
42
|
+
**Return a key (uninstall):**
|
|
43
|
+
```bash
|
|
44
|
+
npm run return
|
|
45
|
+
```
|
|
46
|
+
This returns your key so you can activate it on a different machine.
|
|
47
|
+
|
|
48
|
+
## Setup
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm run setup
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The setup wizard will:
|
|
55
|
+
1. Ask for your SDK/API endpoint
|
|
56
|
+
2. Fetch the schema
|
|
57
|
+
3. List all fields containing "id"
|
|
58
|
+
4. You select which field is your conversation ID
|
|
59
|
+
5. Point to your conversation folder
|
|
60
|
+
6. Auto-import ALL conversations (requires pro license)
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
### Basic Operations (FREE)
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
const TXAIDbPro = require('tx-ai-db-pro');
|
|
68
|
+
const db = new TXAIDbPro();
|
|
69
|
+
|
|
70
|
+
// Retrieve
|
|
71
|
+
const conv = db.retrieve('conv_123');
|
|
72
|
+
|
|
73
|
+
// Update/Insert
|
|
74
|
+
db.update({ id: 'conv_123', messages: [...] });
|
|
75
|
+
|
|
76
|
+
// Delete
|
|
77
|
+
db.delete('conv_123');
|
|
78
|
+
|
|
79
|
+
// List
|
|
80
|
+
const all = db.listConversations();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Pro Features (Requires License)
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
// Search with formatting
|
|
87
|
+
const results = db.search({
|
|
88
|
+
query: "hello world",
|
|
89
|
+
format: { title: true, summary: 'first_last' }
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Auto-migrate folder
|
|
93
|
+
db.migrateFolder('./my-conversations-folder');
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Pricing
|
|
97
|
+
|
|
98
|
+
$40 one-time payment via GitHub Sponsors. 4 installations per license. No subscriptions.
|
|
99
|
+
|
|
100
|
+
$5 per extra key (contact us).
|
|
101
|
+
|
|
102
|
+
## Funding
|
|
103
|
+
|
|
104
|
+
If you find this useful, consider sponsoring: https://github.com/sponsors/TX-AI-Series
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tx-ai-db-pro",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Premium SQLite wrapper for AI conversations with auto-migration, search, and formatted responses. OpenAI API Compatible Format only.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node tests/test.js",
|
|
8
|
+
"setup": "node src/setup-wizard.js",
|
|
9
|
+
"unlock": "node src/unlock.js",
|
|
10
|
+
"return": "node src/unlock.js return",
|
|
11
|
+
"postinstall": "node src/postinstall.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"sqlite",
|
|
15
|
+
"ai",
|
|
16
|
+
"chat",
|
|
17
|
+
"conversation",
|
|
18
|
+
"openai",
|
|
19
|
+
"database",
|
|
20
|
+
"file-driven",
|
|
21
|
+
"pro",
|
|
22
|
+
"search",
|
|
23
|
+
"migration"
|
|
24
|
+
],
|
|
25
|
+
"author": "TX-AI-Series",
|
|
26
|
+
"license": "COMMERCIAL",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/TX-AI-Series/tx-ai-db-pro"
|
|
30
|
+
},
|
|
31
|
+
"funding": {
|
|
32
|
+
"type": "github",
|
|
33
|
+
"url": "https://github.com/sponsors/TX-AI-Series"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"better-sqlite3": "^9.4.3"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"src/",
|
|
40
|
+
"README.md",
|
|
41
|
+
"sqlite-source/"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GitHub Pages Key Server
|
|
6
|
+
* Fetches license keys from GitHub Pages hosted keys.json
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class GitHubKeyServer {
|
|
10
|
+
constructor(config = {}) {
|
|
11
|
+
this.keysUrl = config.keysUrl || 'https://raw.githubusercontent.com/TX-AI-Series/tx-ai-db-pro-keys/main/keys.json';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fetch keys from GitHub Pages
|
|
16
|
+
*/
|
|
17
|
+
fetchKeys() {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const url = new URL(this.keysUrl);
|
|
20
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
21
|
+
|
|
22
|
+
const req = lib.request(this.keysUrl, { method: 'GET' }, (res) => {
|
|
23
|
+
if (res.statusCode === 404) {
|
|
24
|
+
reject(new Error('Keys file not found on GitHub Pages'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let data = '';
|
|
29
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
30
|
+
res.on('end', () => {
|
|
31
|
+
try {
|
|
32
|
+
const keysData = JSON.parse(data);
|
|
33
|
+
resolve(keysData);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
reject(new Error('Failed to parse keys JSON'));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
req.on('error', reject);
|
|
41
|
+
req.end();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get keys for a specific GitHub username
|
|
47
|
+
*/
|
|
48
|
+
async getKeysForUser(username) {
|
|
49
|
+
const keysData = await this.fetchKeys();
|
|
50
|
+
|
|
51
|
+
if (!keysData.sponsors || !keysData.sponsors[username]) {
|
|
52
|
+
return {
|
|
53
|
+
found: false,
|
|
54
|
+
message: `No keys found for GitHub user: ${username}. If you recently sponsored, please wait up to 48 hours for key activation.`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sponsor = keysData.sponsors[username];
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
found: true,
|
|
62
|
+
username: username,
|
|
63
|
+
keys: sponsor.keys,
|
|
64
|
+
active: sponsor.active,
|
|
65
|
+
sponsoredAt: sponsor.sponsoredAt,
|
|
66
|
+
tier: sponsor.tier
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get an available (inactive) key for a user
|
|
72
|
+
*/
|
|
73
|
+
async getAvailableKey(username) {
|
|
74
|
+
const userData = await this.getKeysForUser(username);
|
|
75
|
+
|
|
76
|
+
if (!userData.found) {
|
|
77
|
+
return userData;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Find first inactive key
|
|
81
|
+
for (let i = 0; i < userData.keys.length; i++) {
|
|
82
|
+
if (!userData.active[i]) {
|
|
83
|
+
return {
|
|
84
|
+
found: true,
|
|
85
|
+
key: userData.keys[i],
|
|
86
|
+
installNumber: i + 1,
|
|
87
|
+
username: username
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
found: false,
|
|
94
|
+
message: 'All 4 keys are already active for this user. Use "npm run return" to free up a key.'
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = GitHubKeyServer;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
const Database = require('better-sqlite3');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const LicenseValidator = require('./license');
|
|
5
|
+
|
|
6
|
+
class TXAIDbPro {
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.dbPath = config.dbPath || path.join(process.cwd(), 'pro.db');
|
|
9
|
+
this.conversationIdField = config.conversationIdField || 'id';
|
|
10
|
+
this.db = null;
|
|
11
|
+
this.license = new LicenseValidator();
|
|
12
|
+
this.init();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
init() {
|
|
16
|
+
this.db = new Database(this.dbPath);
|
|
17
|
+
this.db.pragma('journal_mode = WAL');
|
|
18
|
+
this.db.pragma('foreign_keys = ON');
|
|
19
|
+
this.createTables();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
createTables() {
|
|
23
|
+
this.db.exec(`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
raw_data JSON NOT NULL,
|
|
27
|
+
title TEXT,
|
|
28
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
29
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
conversation_id TEXT NOT NULL,
|
|
35
|
+
message_index INTEGER NOT NULL,
|
|
36
|
+
role TEXT NOT NULL,
|
|
37
|
+
content TEXT,
|
|
38
|
+
raw_data JSON NOT NULL,
|
|
39
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_messages_role ON messages(role);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_title ON conversations(title);
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if pro features are unlocked
|
|
50
|
+
*/
|
|
51
|
+
_requirePro(featureName) {
|
|
52
|
+
if (!this.license.isProUnlocked()) {
|
|
53
|
+
throw new Error(`Pro feature "${featureName}" is locked. Add your license keys to .license file or run "npm run unlock".`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Retrieve a conversation by ID (FREE - works without license)
|
|
59
|
+
*/
|
|
60
|
+
retrieve(conversationId) {
|
|
61
|
+
const conversation = this.db.prepare(
|
|
62
|
+
'SELECT * FROM conversations WHERE id = ?'
|
|
63
|
+
).get(conversationId);
|
|
64
|
+
|
|
65
|
+
if (!conversation) return null;
|
|
66
|
+
|
|
67
|
+
const messages = this.db.prepare(
|
|
68
|
+
'SELECT * FROM messages WHERE conversation_id = ? ORDER BY message_index'
|
|
69
|
+
).all(conversationId);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...conversation,
|
|
73
|
+
messages: messages.map(m => JSON.parse(m.raw_data))
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Update or insert a conversation (FREE - works without license)
|
|
79
|
+
*/
|
|
80
|
+
update(conversationData) {
|
|
81
|
+
const conversationId = conversationData[this.conversationIdField];
|
|
82
|
+
if (!conversationId) {
|
|
83
|
+
throw new Error(`Missing conversation ID field: ${this.conversationIdField}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const existing = this.db.prepare(
|
|
87
|
+
'SELECT id FROM conversations WHERE id = ?'
|
|
88
|
+
).get(conversationId);
|
|
89
|
+
|
|
90
|
+
// Extract title from metadata or first message
|
|
91
|
+
let title = conversationData.metadata?.title ||
|
|
92
|
+
conversationData.title ||
|
|
93
|
+
(conversationData.messages?.[0]?.content || '').substring(0, 50) ||
|
|
94
|
+
'Untitled';
|
|
95
|
+
|
|
96
|
+
const tx = this.db.transaction(() => {
|
|
97
|
+
if (existing) {
|
|
98
|
+
this.db.prepare(
|
|
99
|
+
'UPDATE conversations SET raw_data = ?, title = ?, updated_at = datetime(\'now\') WHERE id = ?'
|
|
100
|
+
).run(JSON.stringify(conversationData), title, conversationId);
|
|
101
|
+
|
|
102
|
+
this.db.prepare(
|
|
103
|
+
'DELETE FROM messages WHERE conversation_id = ?'
|
|
104
|
+
).run(conversationId);
|
|
105
|
+
} else {
|
|
106
|
+
this.db.prepare(
|
|
107
|
+
'INSERT INTO conversations (id, raw_data, title) VALUES (?, ?, ?)'
|
|
108
|
+
).run(conversationId, JSON.stringify(conversationData), title);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Insert messages
|
|
112
|
+
const messages = conversationData.messages || [];
|
|
113
|
+
const insertMsg = this.db.prepare(
|
|
114
|
+
'INSERT INTO messages (conversation_id, message_index, role, content, raw_data) VALUES (?, ?, ?, ?, ?)'
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < messages.length; i++) {
|
|
118
|
+
const msg = messages[i];
|
|
119
|
+
insertMsg.run(
|
|
120
|
+
conversationId,
|
|
121
|
+
i,
|
|
122
|
+
msg.role || 'unknown',
|
|
123
|
+
typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
|
|
124
|
+
JSON.stringify(msg)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
tx();
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Delete a conversation by ID (FREE - works without license)
|
|
135
|
+
*/
|
|
136
|
+
delete(conversationId) {
|
|
137
|
+
const result = this.db.prepare(
|
|
138
|
+
'DELETE FROM conversations WHERE id = ?'
|
|
139
|
+
).run(conversationId);
|
|
140
|
+
|
|
141
|
+
return result.changes > 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Search conversations with customizable response formatting (PRO - requires license)
|
|
146
|
+
*/
|
|
147
|
+
search(options = {}) {
|
|
148
|
+
this._requirePro('search');
|
|
149
|
+
|
|
150
|
+
const {
|
|
151
|
+
query = '',
|
|
152
|
+
format = { title: true, summary: 'first_last' },
|
|
153
|
+
limit = 20,
|
|
154
|
+
offset = 0
|
|
155
|
+
} = options;
|
|
156
|
+
|
|
157
|
+
let results;
|
|
158
|
+
|
|
159
|
+
if (query) {
|
|
160
|
+
results = this.db.prepare(`
|
|
161
|
+
SELECT DISTINCT c.id, c.title, c.created_at, c.updated_at, c.raw_data
|
|
162
|
+
FROM conversations c
|
|
163
|
+
JOIN messages m ON c.id = m.conversation_id
|
|
164
|
+
WHERE m.content LIKE ? OR c.title LIKE ?
|
|
165
|
+
ORDER BY c.updated_at DESC
|
|
166
|
+
LIMIT ? OFFSET ?
|
|
167
|
+
`).all(`%${query}%`, `%${query}%`, limit, offset);
|
|
168
|
+
} else {
|
|
169
|
+
results = this.db.prepare(`
|
|
170
|
+
SELECT id, title, created_at, updated_at, raw_data
|
|
171
|
+
FROM conversations
|
|
172
|
+
ORDER BY updated_at DESC
|
|
173
|
+
LIMIT ? OFFSET ?
|
|
174
|
+
`).all(limit, offset);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Format results
|
|
178
|
+
return results.map(conv => {
|
|
179
|
+
const formatted = {
|
|
180
|
+
id: conv.id,
|
|
181
|
+
created_at: conv.created_at,
|
|
182
|
+
updated_at: conv.updated_at
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (format.title) {
|
|
186
|
+
formatted.title = conv.title;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (format.summary === 'first_last') {
|
|
190
|
+
const messages = this.db.prepare(
|
|
191
|
+
'SELECT raw_data FROM messages WHERE conversation_id = ? ORDER BY message_index'
|
|
192
|
+
).all(conv.id);
|
|
193
|
+
|
|
194
|
+
if (messages.length > 0) {
|
|
195
|
+
const firstMsg = JSON.parse(messages[0].raw_data);
|
|
196
|
+
const lastMsg = JSON.parse(messages[messages.length - 1].raw_data);
|
|
197
|
+
|
|
198
|
+
formatted.summary = {
|
|
199
|
+
first_message: firstMsg.content?.substring(0, 100) || '',
|
|
200
|
+
last_message: lastMsg.content?.substring(0, 100) || '',
|
|
201
|
+
message_count: messages.length
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return formatted;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Auto-migrate conversations from a folder (PRO - requires license)
|
|
212
|
+
*/
|
|
213
|
+
migrateFolder(folderPath, options = {}) {
|
|
214
|
+
this._requirePro('migrateFolder');
|
|
215
|
+
|
|
216
|
+
const {
|
|
217
|
+
backupAfterImport = true,
|
|
218
|
+
backupPath = null
|
|
219
|
+
} = options;
|
|
220
|
+
|
|
221
|
+
if (!fs.existsSync(folderPath)) {
|
|
222
|
+
throw new Error(`Folder not found: ${folderPath}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const files = fs.readdirSync(folderPath);
|
|
226
|
+
let successCount = 0;
|
|
227
|
+
let errorCount = 0;
|
|
228
|
+
|
|
229
|
+
console.log(`Found ${files.length} files in ${folderPath}\n`);
|
|
230
|
+
|
|
231
|
+
for (const file of files) {
|
|
232
|
+
if (!file.endsWith('.json')) continue;
|
|
233
|
+
|
|
234
|
+
const filePath = path.join(folderPath, file);
|
|
235
|
+
const conversationId = path.basename(file, '.json');
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const conversationData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
239
|
+
|
|
240
|
+
// Ensure the conversation ID field is set
|
|
241
|
+
if (!conversationData[this.conversationIdField]) {
|
|
242
|
+
conversationData[this.conversationIdField] = conversationId;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Save to database
|
|
246
|
+
this.update(conversationData);
|
|
247
|
+
console.log(`✅ Imported: ${conversationId}`);
|
|
248
|
+
successCount++;
|
|
249
|
+
|
|
250
|
+
// Backup if enabled
|
|
251
|
+
if (backupAfterImport) {
|
|
252
|
+
const destPath = backupPath
|
|
253
|
+
? path.join(backupPath, file)
|
|
254
|
+
: path.join(folderPath, 'imported', file);
|
|
255
|
+
|
|
256
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
257
|
+
fs.renameSync(filePath, destPath);
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.log(`❌ Error importing ${conversationId}: ${err.message}`);
|
|
261
|
+
errorCount++;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(`\n========================================`);
|
|
266
|
+
console.log(` Migration Complete`);
|
|
267
|
+
console.log(`========================================`);
|
|
268
|
+
console.log(` Success: ${successCount}`);
|
|
269
|
+
console.log(` Errors: ${errorCount}`);
|
|
270
|
+
console.log(`========================================\n`);
|
|
271
|
+
|
|
272
|
+
return { successCount, errorCount };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get list of all conversation IDs (FREE - works without license)
|
|
277
|
+
*/
|
|
278
|
+
listConversations() {
|
|
279
|
+
return this.db.prepare(
|
|
280
|
+
'SELECT id, title, created_at, updated_at FROM conversations ORDER BY updated_at DESC'
|
|
281
|
+
).all();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get license info
|
|
286
|
+
*/
|
|
287
|
+
getLicenseInfo() {
|
|
288
|
+
return this.license.getLicenseInfo();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Close the database connection
|
|
293
|
+
*/
|
|
294
|
+
close() {
|
|
295
|
+
if (this.db) {
|
|
296
|
+
this.db.close();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = TXAIDbPro;
|
package/src/license.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* License validation system for tx-ai-db-pro
|
|
6
|
+
*
|
|
7
|
+
* Token format: XXXX-XXXX-XXXX-XXXX
|
|
8
|
+
* Block 1: Last 4 digits of payment ID
|
|
9
|
+
* Block 2: MMYY (payment date)
|
|
10
|
+
* Block 3: First 4 digits of payment ID
|
|
11
|
+
* Block 4: Last digit of Block1 + Last digit of Block2 + Last digit of Block3 + Install number (1-4)
|
|
12
|
+
*
|
|
13
|
+
* .license file must contain exactly 4 keys, one per row.
|
|
14
|
+
* The first key is the active license for this install.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
class LicenseValidator {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.licensePath = path.join(process.cwd(), '.license');
|
|
20
|
+
this.isPro = false;
|
|
21
|
+
this.licenseData = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate a single license token format
|
|
26
|
+
*/
|
|
27
|
+
validateTokenFormat(token) {
|
|
28
|
+
// Check format: XXXX-XXXX-XXXX-XXXX
|
|
29
|
+
const regex = /^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/;
|
|
30
|
+
if (!regex.test(token)) {
|
|
31
|
+
return { valid: false, error: 'Invalid token format. Expected: XXXX-XXXX-XXXX-XXXX' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parts = token.split('-');
|
|
35
|
+
const block1 = parts[0];
|
|
36
|
+
const block2 = parts[1];
|
|
37
|
+
const block3 = parts[2];
|
|
38
|
+
const block4 = parts[3];
|
|
39
|
+
|
|
40
|
+
// Validate block 4 structure
|
|
41
|
+
const expectedBlock4 = block1[3] + block2[3] + block3[3];
|
|
42
|
+
const installNumber = block4[3];
|
|
43
|
+
|
|
44
|
+
// Check first 3 characters of block 4
|
|
45
|
+
if (block4.substring(0, 3) !== expectedBlock4) {
|
|
46
|
+
return { valid: false, error: 'Invalid token checksum' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check install number (1-4)
|
|
50
|
+
if (!['1', '2', '3', '4'].includes(installNumber)) {
|
|
51
|
+
return { valid: false, error: 'Invalid install number. Must be 1-4' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
valid: true,
|
|
56
|
+
installNumber: parseInt(installNumber),
|
|
57
|
+
paymentIdLast4: block1,
|
|
58
|
+
paymentDate: block2,
|
|
59
|
+
paymentIdFirst4: block3
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Load and validate .license file
|
|
65
|
+
*/
|
|
66
|
+
loadLicense() {
|
|
67
|
+
if (!fs.existsSync(this.licensePath)) {
|
|
68
|
+
return {
|
|
69
|
+
isPro: false,
|
|
70
|
+
error: 'No .license file found. Run "npm run unlock" to activate pro features.'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const content = fs.readFileSync(this.licensePath, 'utf8');
|
|
75
|
+
const keys = content.split('\n').map(k => k.trim()).filter(k => k && !k.startsWith('#'));
|
|
76
|
+
|
|
77
|
+
if (keys.length !== 4) {
|
|
78
|
+
return {
|
|
79
|
+
isPro: false,
|
|
80
|
+
error: `Invalid license file. Must contain exactly 4 keys (found ${keys.length}). Add all 4 keys you received after payment.`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Validate all keys
|
|
85
|
+
const validations = keys.map(key => this.validateTokenFormat(key));
|
|
86
|
+
const invalidKey = validations.find(v => !v.valid);
|
|
87
|
+
|
|
88
|
+
if (invalidKey) {
|
|
89
|
+
return { isPro: false, error: invalidKey.error };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// First key is the active license
|
|
93
|
+
const activeKey = keys[0];
|
|
94
|
+
const activeValidation = validations[0];
|
|
95
|
+
|
|
96
|
+
this.isPro = true;
|
|
97
|
+
this.licenseData = {
|
|
98
|
+
keys: keys,
|
|
99
|
+
activeKey: activeKey,
|
|
100
|
+
installNumber: activeValidation.installNumber,
|
|
101
|
+
paymentInfo: {
|
|
102
|
+
last4: activeValidation.paymentIdLast4,
|
|
103
|
+
date: activeValidation.paymentDate,
|
|
104
|
+
first4: activeValidation.paymentIdFirst4
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return { isPro: true, licenseData: this.licenseData };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if pro features are unlocked
|
|
113
|
+
*/
|
|
114
|
+
isProUnlocked() {
|
|
115
|
+
const result = this.loadLicense();
|
|
116
|
+
return result.isPro;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get license info (for debugging/display)
|
|
121
|
+
*/
|
|
122
|
+
getLicenseInfo() {
|
|
123
|
+
const result = this.loadLicense();
|
|
124
|
+
if (!result.isPro) {
|
|
125
|
+
return { isPro: false, message: result.error };
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
isPro: true,
|
|
129
|
+
installNumber: result.licenseData.installNumber,
|
|
130
|
+
paymentDate: result.licenseData.paymentInfo.date,
|
|
131
|
+
keysLoaded: result.licenseData.keys.length
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = LicenseValidator;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
console.log(`
|
|
2
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
3
|
+
║ ║
|
|
4
|
+
║ tx-ai-db-pro installed successfully! ║
|
|
5
|
+
║ ║
|
|
6
|
+
║ Next steps: ║
|
|
7
|
+
║ 1. Run setup: npm run setup ║
|
|
8
|
+
║ 2. Point to your conversation folder ║
|
|
9
|
+
║ 3. Auto-import ALL conversations ║
|
|
10
|
+
║ ║
|
|
11
|
+
║ ───────────────────────────────────────────────────── ║
|
|
12
|
+
║ ║
|
|
13
|
+
║ 💚 Love this project? Consider sponsoring us! ║
|
|
14
|
+
║ https://github.com/sponsors/TX-AI-Series ║
|
|
15
|
+
║ ║
|
|
16
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
17
|
+
`);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function askQuestion(question) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
resolve(answer.trim());
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function fetchSchema(endpoint) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const url = new URL(endpoint);
|
|
23
|
+
const lib = url.protocol === 'https:' ? https : http;
|
|
24
|
+
const req = lib.request(url.toString(), { method: 'GET', headers: { 'Accept': 'application/json' } }, (res) => {
|
|
25
|
+
let data = '';
|
|
26
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
27
|
+
res.on('end', () => {
|
|
28
|
+
try { resolve(JSON.parse(data)); }
|
|
29
|
+
catch (e) { reject(new Error('Failed to parse JSON response')); }
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
req.on('error', reject);
|
|
33
|
+
req.end();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function findIdFields(schema, prefix = '') {
|
|
38
|
+
let idFields = [];
|
|
39
|
+
if (typeof schema === 'object' && schema !== null) {
|
|
40
|
+
if (Array.isArray(schema)) {
|
|
41
|
+
schema.forEach((item, index) => {
|
|
42
|
+
idFields = idFields.concat(findIdFields(item, `${prefix}[${index}]`));
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
46
|
+
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
47
|
+
if (key.toLowerCase().includes('id')) {
|
|
48
|
+
idFields.push({ field: fullPath, type: typeof value, value: value });
|
|
49
|
+
}
|
|
50
|
+
idFields = idFields.concat(findIdFields(value, fullPath));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return idFields;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function runSetupWizard() {
|
|
58
|
+
console.log('========================================');
|
|
59
|
+
console.log(' tx-ai-db-pro Setup Wizard');
|
|
60
|
+
console.log('========================================\n');
|
|
61
|
+
|
|
62
|
+
// Step 1: Get SDK/API endpoint
|
|
63
|
+
console.log('Step 1: SDK/API Configuration');
|
|
64
|
+
const endpoint = await askQuestion('API Endpoint (or press Enter to skip): ');
|
|
65
|
+
|
|
66
|
+
let idFields = [];
|
|
67
|
+
if (endpoint) {
|
|
68
|
+
try {
|
|
69
|
+
console.log('\nFetching schema from API...');
|
|
70
|
+
const schema = await fetchSchema(endpoint);
|
|
71
|
+
idFields = findIdFields(schema);
|
|
72
|
+
console.log(`Found ${idFields.length} fields containing "id":\n`);
|
|
73
|
+
idFields.forEach((field, index) => {
|
|
74
|
+
console.log(` ${index + 1}. ${field.field} (${field.type}) - Example: ${JSON.stringify(field.value).substring(0, 50)}`);
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.log(`\nFailed to fetch schema: ${err.message}\nUsing default schema...\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (idFields.length === 0) {
|
|
82
|
+
idFields = [
|
|
83
|
+
{ field: 'id', type: 'string', value: 'thread_abc123' },
|
|
84
|
+
{ field: 'thread_id', type: 'string', value: 'thread_abc123' },
|
|
85
|
+
{ field: 'conversation_id', type: 'string', value: 'conv_123' },
|
|
86
|
+
{ field: 'session_id', type: 'string', value: 'sess_456' }
|
|
87
|
+
];
|
|
88
|
+
console.log('Default conversation ID fields:');
|
|
89
|
+
idFields.forEach((field, index) => {
|
|
90
|
+
console.log(` ${index + 1}. ${field.field}`);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Step 2: Select conversation ID field
|
|
95
|
+
console.log('\nStep 2: Select Conversation ID Field');
|
|
96
|
+
const selection = await askQuestion(`Enter number (1-${idFields.length}): `);
|
|
97
|
+
const selectedIndex = parseInt(selection) - 1;
|
|
98
|
+
const conversationIdField = (selectedIndex >= 0 && selectedIndex < idFields.length) ? idFields[selectedIndex].field : 'id';
|
|
99
|
+
console.log(`\nSelected: ${conversationIdField}\n`);
|
|
100
|
+
|
|
101
|
+
// Step 3: Get conversation folder path
|
|
102
|
+
console.log('Step 3: Conversation Folder Path');
|
|
103
|
+
const folderPath = await askQuestion('Path to conversation folder (or press Enter for default ./conversations): ');
|
|
104
|
+
const finalFolderPath = folderPath || path.join(process.cwd(), 'conversations');
|
|
105
|
+
|
|
106
|
+
// Step 4: Create config
|
|
107
|
+
console.log('\nStep 4: Creating configuration...');
|
|
108
|
+
const config = {
|
|
109
|
+
conversationIdField: conversationIdField,
|
|
110
|
+
dbPath: path.join(process.cwd(), 'pro.db'),
|
|
111
|
+
folderPath: finalFolderPath,
|
|
112
|
+
backupAfterImport: true,
|
|
113
|
+
version: '1.0.0-pro'
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const configPath = path.join(process.cwd(), 'pro-config.json');
|
|
117
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
118
|
+
console.log(`Config saved to: ${configPath}\n`);
|
|
119
|
+
|
|
120
|
+
// Step 5: Auto-migrate
|
|
121
|
+
console.log('Step 5: Auto-Migration');
|
|
122
|
+
const migrate = await askQuestion('Import all conversations now? (y/n): ');
|
|
123
|
+
|
|
124
|
+
if (migrate.toLowerCase() === 'y') {
|
|
125
|
+
const TXAIDbPro = require('./index');
|
|
126
|
+
const db = new TXAIDbPro(config);
|
|
127
|
+
try {
|
|
128
|
+
db.migrateFolder(finalFolderPath, { backupAfterImport: true });
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.log(`Migration error: ${err.message}`);
|
|
131
|
+
}
|
|
132
|
+
db.close();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log('\n========================================');
|
|
136
|
+
console.log(' Setup Complete!');
|
|
137
|
+
console.log('========================================\n');
|
|
138
|
+
console.log('Your conversations are now in the database!');
|
|
139
|
+
console.log('Use db.search(), db.retrieve(), db.update(), db.delete() in your code.\n');
|
|
140
|
+
|
|
141
|
+
rl.close();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
runSetupWizard().catch(console.error);
|
package/src/unlock.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const GitHubKeyServer = require('./github-key-server');
|
|
5
|
+
|
|
6
|
+
const rl = readline.createInterface({
|
|
7
|
+
input: process.stdin,
|
|
8
|
+
output: process.stdout
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function askQuestion(question) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
rl.question(question, (answer) => {
|
|
14
|
+
resolve(answer.trim());
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function unlock() {
|
|
20
|
+
console.log('========================================');
|
|
21
|
+
console.log(' tx-ai-db-pro License Unlock');
|
|
22
|
+
console.log('========================================\n');
|
|
23
|
+
|
|
24
|
+
const licensePath = path.join(process.cwd(), '.license');
|
|
25
|
+
const configPath = path.join(process.cwd(), 'pro-config.json');
|
|
26
|
+
|
|
27
|
+
// Load config for keys URL
|
|
28
|
+
let config = {};
|
|
29
|
+
if (fs.existsSync(configPath)) {
|
|
30
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const keyServer = new GitHubKeyServer(config);
|
|
34
|
+
|
|
35
|
+
// Check if license file already exists with 4 valid keys
|
|
36
|
+
if (fs.existsSync(licensePath)) {
|
|
37
|
+
const existing = fs.readFileSync(licensePath, 'utf8');
|
|
38
|
+
const keys = existing.split('\n').map(k => k.trim()).filter(k => k && !k.startsWith('#'));
|
|
39
|
+
|
|
40
|
+
if (keys.length === 4 && !keys.some(k => k.startsWith('PENDING'))) {
|
|
41
|
+
console.log('License file already exists with 4 keys.');
|
|
42
|
+
console.log('Pro features are already unlocked!\n');
|
|
43
|
+
rl.close();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log('Enter your GitHub username to fetch your license key.');
|
|
49
|
+
console.log('Keys are activated within 48 hours of payment.\n');
|
|
50
|
+
|
|
51
|
+
const githubUsername = await askQuestion('GitHub username: ');
|
|
52
|
+
|
|
53
|
+
console.log('\nFetching key from GitHub Pages...');
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = await keyServer.getAvailableKey(githubUsername);
|
|
57
|
+
|
|
58
|
+
if (!result.found) {
|
|
59
|
+
console.log(`\n❌ ${result.message}`);
|
|
60
|
+
rl.close();
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const key = result.key;
|
|
65
|
+
console.log(`✅ Key received for install #${result.installNumber}`);
|
|
66
|
+
|
|
67
|
+
// Load existing license or create new
|
|
68
|
+
let existingKeys = ['PENDING', 'PENDING', 'PENDING', 'PENDING'];
|
|
69
|
+
if (fs.existsSync(licensePath)) {
|
|
70
|
+
const existing = fs.readFileSync(licensePath, 'utf8');
|
|
71
|
+
existingKeys = existing.split('\n').map(k => k.trim()).filter(k => k && !k.startsWith('#'));
|
|
72
|
+
while (existingKeys.length < 4) existingKeys.push('PENDING');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Replace the first PENDING key with the new one
|
|
76
|
+
const pendingIndex = existingKeys.findIndex(k => k.startsWith('PENDING'));
|
|
77
|
+
if (pendingIndex >= 0) {
|
|
78
|
+
existingKeys[pendingIndex] = key;
|
|
79
|
+
} else {
|
|
80
|
+
existingKeys[result.installNumber - 1] = key;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Write license file
|
|
84
|
+
fs.writeFileSync(licensePath, existingKeys.join('\n') + '\n');
|
|
85
|
+
console.log(`\n✅ License saved to: ${licensePath}`);
|
|
86
|
+
console.log(`Key #${result.installNumber} activated for this install.\n`);
|
|
87
|
+
|
|
88
|
+
if (existingKeys.some(k => k.startsWith('PENDING'))) {
|
|
89
|
+
console.log('To activate remaining keys, run: npm run unlock');
|
|
90
|
+
console.log('(for each remaining install number)\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log('========================================');
|
|
94
|
+
console.log(' Unlock Complete!');
|
|
95
|
+
console.log('========================================\n');
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.log(`\nError fetching key: ${error.message}`);
|
|
99
|
+
console.log('Make sure you have a stable internet connection.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
rl.close();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function returnLicense() {
|
|
106
|
+
console.log('========================================');
|
|
107
|
+
console.log(' tx-ai-db-pro License Return (Uninstall)');
|
|
108
|
+
console.log('========================================\n');
|
|
109
|
+
|
|
110
|
+
const licensePath = path.join(process.cwd(), '.license');
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(licensePath)) {
|
|
113
|
+
console.log('No license file found.');
|
|
114
|
+
rl.close();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const existing = fs.readFileSync(licensePath, 'utf8');
|
|
119
|
+
const keys = existing.split('\n').map(k => k.trim()).filter(k => k && !k.startsWith('#'));
|
|
120
|
+
|
|
121
|
+
if (keys.length === 0) {
|
|
122
|
+
console.log('No keys found in license file.');
|
|
123
|
+
rl.close();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('Current keys:');
|
|
128
|
+
keys.forEach((key, i) => console.log(` ${i + 1}. ${key}`));
|
|
129
|
+
console.log('');
|
|
130
|
+
|
|
131
|
+
const keyIndex = await askQuestion('Which key to return? (1-4): ');
|
|
132
|
+
const index = parseInt(keyIndex) - 1;
|
|
133
|
+
|
|
134
|
+
if (index < 0 || index >= keys.length) {
|
|
135
|
+
console.log('\nInvalid key number.');
|
|
136
|
+
rl.close();
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Mark key as pending (returned)
|
|
141
|
+
keys[index] = 'PENDING-RETURNED';
|
|
142
|
+
fs.writeFileSync(licensePath, keys.join('\n') + '\n');
|
|
143
|
+
|
|
144
|
+
console.log(`\n✅ Key #${keyIndex} has been returned.`);
|
|
145
|
+
console.log('You can now activate it on a different machine.\n');
|
|
146
|
+
|
|
147
|
+
rl.close();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check command line args
|
|
151
|
+
const action = process.argv[2];
|
|
152
|
+
if (action === 'return') {
|
|
153
|
+
returnLicense().catch(console.error);
|
|
154
|
+
} else {
|
|
155
|
+
unlock().catch(console.error);
|
|
156
|
+
}
|