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 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
+ }