@thinksoftai/cli 1.6.12 → 1.6.16

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.
@@ -126,6 +126,7 @@ async function exportBackend(options = {}) {
126
126
  }
127
127
  // Chat option (only for node-express)
128
128
  let chatOption = 'none';
129
+ let authOption = 'none';
129
130
  if (answers.format === 'node-express') {
130
131
  const chatAnswer = await inquirer_1.default.prompt([
131
132
  {
@@ -141,6 +142,21 @@ async function exportBackend(options = {}) {
141
142
  }
142
143
  ]);
143
144
  chatOption = chatAnswer.chatOption;
145
+ // Auth option
146
+ const authAnswer = await inquirer_1.default.prompt([
147
+ {
148
+ type: 'list',
149
+ name: 'authOption',
150
+ message: 'User authentication:',
151
+ choices: [
152
+ { name: 'ThinkSoft Email OTP (no setup needed)', value: 'thinksoft' },
153
+ { name: 'Self-hosted Email OTP (requires Resend/SendGrid)', value: 'self-hosted' },
154
+ { name: 'None (no authentication)', value: 'none' }
155
+ ],
156
+ default: 'thinksoft'
157
+ }
158
+ ]);
159
+ authOption = authAnswer.authOption;
144
160
  }
145
161
  // Output directory
146
162
  const appSlug = exportData.app?.name?.toLowerCase().replace(/\s+/g, '-') || appId.toLowerCase();
@@ -158,6 +174,7 @@ async function exportBackend(options = {}) {
158
174
  const fileCount = await generateExport(exportData, {
159
175
  ...answers,
160
176
  chatOption,
177
+ authOption,
161
178
  baseUrl: answers.baseUrl
162
179
  }, endpointConfig, outputDir);
163
180
  generatorSpinner.succeed(`Generated ${fileCount} files`);
@@ -193,7 +210,7 @@ async function generateExport(data, config, endpoints, outputDir) {
193
210
  // PostgreSQL only - just schema and triggers
194
211
  fs.mkdirSync(path.join(outputDir, 'database'), { recursive: true });
195
212
  // Generate schema
196
- const schema = generateSchema(data.tables);
213
+ const schema = generateSchema(data.tables, config.authOption);
197
214
  fs.writeFileSync(path.join(outputDir, 'database/schema.sql'), schema);
198
215
  fileCount++;
199
216
  // Generate triggers if rules included
@@ -226,7 +243,7 @@ async function generateExport(data, config, endpoints, outputDir) {
226
243
  fs.mkdirSync(path.join(outputDir, dir), { recursive: true });
227
244
  }
228
245
  // Database files
229
- const schema = generateSchema(data.tables);
246
+ const schema = generateSchema(data.tables, config.authOption);
230
247
  fs.writeFileSync(path.join(outputDir, 'database/schema.sql'), schema);
231
248
  fileCount++;
232
249
  if (config.includeRules && data.rules?.length > 0) {
@@ -264,7 +281,7 @@ async function generateExport(data, config, endpoints, outputDir) {
264
281
  fs.writeFileSync(path.join(outputDir, 'src/routes/agents.js'), agentRoutes);
265
282
  fileCount++;
266
283
  // Agents
267
- const agentConfig = generateAgentConfig(data.agents);
284
+ const agentConfig = generateAgentConfig(data.agents, data.rules);
268
285
  fs.writeFileSync(path.join(outputDir, 'agents/config.json'), agentConfig);
269
286
  fileCount++;
270
287
  const agentActions = generateAgentActions();
@@ -289,7 +306,7 @@ async function generateExport(data, config, endpoints, outputDir) {
289
306
  fs.writeFileSync(path.join(outputDir, 'src/events/index.js'), events);
290
307
  fileCount++;
291
308
  // Main files
292
- const indexJs = generateIndex(config.baseUrl);
309
+ const indexJs = generateIndex(config.baseUrl, config.authOption);
293
310
  fs.writeFileSync(path.join(outputDir, 'src/index.js'), indexJs);
294
311
  fileCount++;
295
312
  const dbJs = generateDb();
@@ -298,6 +315,15 @@ async function generateExport(data, config, endpoints, outputDir) {
298
315
  const authMiddleware = generateAuthMiddleware();
299
316
  fs.writeFileSync(path.join(outputDir, 'src/middleware/auth.js'), authMiddleware);
300
317
  fileCount++;
318
+ // Auth controller and routes (if auth enabled)
319
+ if (config.authOption && config.authOption !== 'none') {
320
+ const authController = generateAuthController(data.app, config.authOption);
321
+ fs.writeFileSync(path.join(outputDir, 'src/controllers/auth.js'), authController);
322
+ fileCount++;
323
+ const authRoutes = generateAuthRoutes();
324
+ fs.writeFileSync(path.join(outputDir, 'src/routes/auth.js'), authRoutes);
325
+ fileCount++;
326
+ }
301
327
  // Config files
302
328
  const apiConfig = generateApiConfig(data.tables, endpoints, config);
303
329
  fs.writeFileSync(path.join(outputDir, 'api.config.json'), apiConfig);
@@ -319,16 +345,37 @@ async function generateExport(data, config, endpoints, outputDir) {
319
345
  const readme = generateReadme(data.app, config);
320
346
  fs.writeFileSync(path.join(outputDir, 'README.md'), readme);
321
347
  fileCount++;
348
+ // Context.md - comprehensive app documentation for developers and AI
349
+ const context = generateContext(data, config, endpoints);
350
+ fs.writeFileSync(path.join(outputDir, 'context.md'), context);
351
+ fileCount++;
322
352
  }
323
353
  return fileCount;
324
354
  }
325
355
  // ============================================
326
356
  // Generator Functions
327
357
  // ============================================
328
- function generateSchema(tables) {
358
+ function generateSchema(tables, authOption = 'none') {
329
359
  let sql = '-- Auto-generated PostgreSQL schema\n';
330
360
  sql += '-- Generated by ThinkSoft CLI\n\n';
331
361
  sql += 'CREATE EXTENSION IF NOT EXISTS "pgcrypto";\n\n';
362
+ // Add app_users table for authentication
363
+ if (authOption !== 'none') {
364
+ sql += '-- Users table for authentication\n';
365
+ sql += 'CREATE TABLE app_users (\n';
366
+ sql += ' id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n';
367
+ sql += ' email VARCHAR(255) UNIQUE NOT NULL,\n';
368
+ sql += ' name VARCHAR(255),\n';
369
+ sql += ' otp_code VARCHAR(6),\n';
370
+ sql += ' otp_expires_at TIMESTAMP,\n';
371
+ sql += ' refresh_token VARCHAR(500),\n';
372
+ sql += ' last_login TIMESTAMP,\n';
373
+ sql += ' is_active BOOLEAN DEFAULT TRUE,\n';
374
+ sql += ' created_at TIMESTAMP DEFAULT NOW(),\n';
375
+ sql += ' updated_at TIMESTAMP DEFAULT NOW()\n';
376
+ sql += ');\n\n';
377
+ sql += 'CREATE INDEX idx_app_users_email ON app_users(email);\n\n';
378
+ }
332
379
  const typeMap = {
333
380
  'text': 'VARCHAR(255)',
334
381
  'textarea': 'TEXT',
@@ -596,37 +643,271 @@ module.exports = { schema, validate };
596
643
  `;
597
644
  }
598
645
  function generateHooks(table, rules) {
646
+ // Group rules by trigger
647
+ const rulesByTrigger = {
648
+ before_create: [],
649
+ after_create: [],
650
+ before_update: [],
651
+ after_update: [],
652
+ before_delete: [],
653
+ after_delete: []
654
+ };
655
+ for (const rule of rules || []) {
656
+ if (rule.trigger && rulesByTrigger[rule.trigger]) {
657
+ rulesByTrigger[rule.trigger].push(rule);
658
+ }
659
+ }
660
+ // Sort each group by priority
661
+ for (const trigger of Object.keys(rulesByTrigger)) {
662
+ rulesByTrigger[trigger].sort((a, b) => (a.priority || 100) - (b.priority || 100));
663
+ }
664
+ // Generate condition evaluation code
665
+ const generateCondition = (condition) => {
666
+ if (!condition || condition === 'true')
667
+ return 'true';
668
+ // Convert Liquid-style conditions to JavaScript
669
+ // Handle common patterns
670
+ let jsCondition = condition
671
+ // Liquid equality: {% if x == 'value' %} -> x === 'value'
672
+ .replace(/\{\%\s*if\s+([^%]+)\s*\%\}/g, '$1')
673
+ .replace(/\{\%\s*endif\s*\%\}/g, '')
674
+ // Liquid variables: {{ field }} -> data.field
675
+ .replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1')
676
+ .replace(/\{\{\s*record\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1')
677
+ .replace(/\{\{\s*_new\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1')
678
+ .replace(/\{\{\s*_old\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'oldData.$1')
679
+ // Field access without braces
680
+ .replace(/\brecord\.([a-zA-Z_][a-zA-Z0-9_]*)/g, 'data.$1')
681
+ .replace(/\b_new\.([a-zA-Z_][a-zA-Z0-9_]*)/g, 'data.$1')
682
+ .replace(/\b_old\.([a-zA-Z_][a-zA-Z0-9_]*)/g, 'oldData.$1')
683
+ // Operators
684
+ .replace(/\s+==\s+/g, ' === ')
685
+ .replace(/\s+!=\s+/g, ' !== ')
686
+ .replace(/\s+and\s+/g, ' && ')
687
+ .replace(/\s+or\s+/g, ' || ')
688
+ .replace(/\s+contains\s+/g, '.includes(')
689
+ // Liquid filters: | size -> .length
690
+ .replace(/\|\s*size/g, '.length')
691
+ // Blank check
692
+ .replace(/\s+blank/g, " === '' || data.$1 === null || data.$1 === undefined")
693
+ .replace(/\s+present/g, " !== '' && data.$1 !== null && data.$1 !== undefined")
694
+ .trim();
695
+ // If still has Liquid syntax, wrap in try-catch eval
696
+ if (jsCondition.includes('{%') || jsCondition.includes('{{')) {
697
+ return `/* Complex condition: ${condition} */ true`;
698
+ }
699
+ return jsCondition || 'true';
700
+ };
701
+ // Generate action code
702
+ const generateActionCode = (action, indent = ' ') => {
703
+ switch (action.type) {
704
+ case 'set_field': {
705
+ let value = action.value;
706
+ // Convert Liquid template to JS
707
+ if (typeof value === 'string') {
708
+ if (value.includes('{{') || value.includes('{%')) {
709
+ // Replace Liquid variables with JS
710
+ value = value
711
+ .replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, "' + data.$1 + '")
712
+ .replace(/\{\{\s*record\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, "' + data.$1 + '")
713
+ .replace(/\{\{\s*_timestamp\s*\}\}/g, "' + new Date().toISOString() + '")
714
+ .replace(/\{\{\s*_date\s*\}\}/g, "' + new Date().toISOString().split('T')[0] + '");
715
+ value = `'${value}'`;
716
+ }
717
+ else {
718
+ value = `'${value}'`;
719
+ }
720
+ }
721
+ return `${indent}data.${action.field} = ${value};`;
722
+ }
723
+ case 'calculate': {
724
+ // Convert Liquid math to JS
725
+ let formula = action.value || '';
726
+ formula = formula
727
+ .replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1')
728
+ .replace(/\{\{\s*record\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1')
729
+ .replace(/\|\s*plus:\s*/g, ' + ')
730
+ .replace(/\|\s*minus:\s*/g, ' - ')
731
+ .replace(/\|\s*times:\s*/g, ' * ')
732
+ .replace(/\|\s*divided_by:\s*/g, ' / ');
733
+ return `${indent}data.${action.field} = ${formula};`;
734
+ }
735
+ case 'reject': {
736
+ let message = action.message || 'Operation rejected by business rule';
737
+ // Convert Liquid variables in message
738
+ message = message
739
+ .replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, '${data.$1}')
740
+ .replace(/\{\{\s*record\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, '${data.$1}');
741
+ return `${indent}throw new Error(\`${message}\`);`;
742
+ }
743
+ case 'validate': {
744
+ const condition = generateCondition(action.condition || 'true');
745
+ let message = action.message || 'Validation failed';
746
+ message = message
747
+ .replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, '${data.$1}')
748
+ .replace(/\{\{\s*record\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, '${data.$1}');
749
+ return `${indent}if (!(${condition})) {\n${indent} throw new Error(\`${message}\`);\n${indent}}`;
750
+ }
751
+ case 'lookup_and_set': {
752
+ // Generate lookup query
753
+ const lookupFilter = action.lookupFilter || {};
754
+ const filterEntries = Object.entries(lookupFilter);
755
+ let filterCode = '';
756
+ if (filterEntries.length > 0) {
757
+ const conditions = filterEntries.map(([key, val]) => {
758
+ if (typeof val === 'string' && val.includes('{{')) {
759
+ const jsVal = val.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1')
760
+ .replace(/\{\{\s*record\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1');
761
+ return `${key} = $\${paramIndex++}`;
762
+ }
763
+ return `${key} = $\${paramIndex++}`;
764
+ });
765
+ filterCode = conditions.join(' AND ');
766
+ }
767
+ return `${indent}// Lookup from ${action.lookupTable}
768
+ ${indent}const lookupResult = await db.query(
769
+ ${indent} 'SELECT ${action.returnField || 'id'} FROM ${action.lookupTable} WHERE ${filterCode || 'TRUE'} LIMIT 1',
770
+ ${indent} [${filterEntries.map(([k, v]) => {
771
+ if (typeof v === 'string' && v.includes('{{')) {
772
+ return v.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1')
773
+ .replace(/\{\{\s*record\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1');
774
+ }
775
+ return `'${v}'`;
776
+ }).join(', ')}]
777
+ ${indent});
778
+ ${indent}if (lookupResult.rows.length > 0) {
779
+ ${indent} data.${action.field} = lookupResult.rows[0].${action.returnField || 'id'};
780
+ ${indent}}`;
781
+ }
782
+ case 'create_record': {
783
+ const recordData = action.data || {};
784
+ const fields = Object.keys(recordData);
785
+ const values = Object.values(recordData).map((v) => {
786
+ if (typeof v === 'string' && v.includes('{{')) {
787
+ return v.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1')
788
+ .replace(/\{\{\s*record\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1')
789
+ .replace(/\{\{\s*_new\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, 'data.$1');
790
+ }
791
+ return typeof v === 'string' ? `'${v}'` : v;
792
+ });
793
+ return `${indent}// Create record in ${action.table}
794
+ ${indent}await db.query(
795
+ ${indent} 'INSERT INTO ${action.table} (${fields.join(', ')}) VALUES (${fields.map((_, i) => '$' + (i + 1)).join(', ')})',
796
+ ${indent} [${values.join(', ')}]
797
+ ${indent});`;
798
+ }
799
+ default:
800
+ return `${indent}// Unknown action type: ${action.type}`;
801
+ }
802
+ };
803
+ // Generate hook function code
804
+ const generateHookCode = (trigger, rulesForTrigger) => {
805
+ if (rulesForTrigger.length === 0) {
806
+ if (trigger.startsWith('before')) {
807
+ return ` // No rules for this trigger
808
+ return data;`;
809
+ }
810
+ return ` // No rules for this trigger`;
811
+ }
812
+ let code = '';
813
+ for (const rule of rulesForTrigger) {
814
+ const condition = generateCondition(rule.condition || 'true');
815
+ code += ` // Rule: ${rule.name} (priority: ${rule.priority || 100})\n`;
816
+ code += ` if (${condition}) {\n`;
817
+ for (const action of rule.actions || []) {
818
+ code += generateActionCode(action, ' ') + '\n';
819
+ }
820
+ code += ` }\n\n`;
821
+ }
822
+ if (trigger.startsWith('before')) {
823
+ code += ` return data;`;
824
+ }
825
+ return code;
826
+ };
827
+ // Build the full hooks file
599
828
  return `/**
600
829
  * ${table.name} Hooks
601
- * Auto-generated by ThinkSoft CLI
830
+ * Auto-generated from business rules by ThinkSoft CLI
831
+ *
832
+ * Rules converted: ${rules?.length || 0}
602
833
  */
603
834
 
604
- module.exports = {
605
- beforeCreate: async (data, db) => {
606
- // Add custom logic here
607
- return data;
608
- },
609
-
610
- afterCreate: async (record, db) => {
611
- // Add custom logic here
612
- },
613
-
614
- beforeUpdate: async (oldRecord, data, db) => {
615
- // Add custom logic here
616
- return data;
617
- },
618
-
619
- afterUpdate: async (oldRecord, newRecord, db) => {
620
- // Add custom logic here
621
- },
622
-
623
- beforeDelete: async (record, db) => {
624
- // Add custom logic here
625
- },
835
+ /**
836
+ * Before Create Hook
837
+ * Called before a new record is inserted
838
+ * @param {object} data - The data being inserted
839
+ * @param {object} db - Database connection
840
+ * @returns {object} Modified data or throws error to reject
841
+ */
842
+ async function beforeCreate(data, db) {
843
+ ${generateHookCode('before_create', rulesByTrigger.before_create)}
844
+ }
845
+
846
+ /**
847
+ * After Create Hook
848
+ * Called after a record is successfully inserted
849
+ * @param {object} record - The created record (with id)
850
+ * @param {object} db - Database connection
851
+ */
852
+ async function afterCreate(record, db) {
853
+ const data = record; // Alias for rule compatibility
854
+ ${generateHookCode('after_create', rulesByTrigger.after_create)}
855
+ }
856
+
857
+ /**
858
+ * Before Update Hook
859
+ * Called before a record is updated
860
+ * @param {object} oldData - The current record data
861
+ * @param {object} data - The new data being applied
862
+ * @param {object} db - Database connection
863
+ * @returns {object} Modified data or throws error to reject
864
+ */
865
+ async function beforeUpdate(oldData, data, db) {
866
+ ${generateHookCode('before_update', rulesByTrigger.before_update)}
867
+ }
868
+
869
+ /**
870
+ * After Update Hook
871
+ * Called after a record is successfully updated
872
+ * @param {object} oldData - The previous record data
873
+ * @param {object} newData - The updated record
874
+ * @param {object} db - Database connection
875
+ */
876
+ async function afterUpdate(oldData, newData, db) {
877
+ const data = newData; // Alias for rule compatibility
878
+ ${generateHookCode('after_update', rulesByTrigger.after_update)}
879
+ }
880
+
881
+ /**
882
+ * Before Delete Hook
883
+ * Called before a record is deleted
884
+ * @param {object} record - The record being deleted
885
+ * @param {object} db - Database connection
886
+ * @throws {Error} To prevent deletion
887
+ */
888
+ async function beforeDelete(record, db) {
889
+ const data = record; // Alias for rule compatibility
890
+ ${generateHookCode('before_delete', rulesByTrigger.before_delete)}
891
+ }
892
+
893
+ /**
894
+ * After Delete Hook
895
+ * Called after a record is successfully deleted
896
+ * @param {object} record - The deleted record
897
+ * @param {object} db - Database connection
898
+ */
899
+ async function afterDelete(record, db) {
900
+ const data = record; // Alias for rule compatibility
901
+ ${generateHookCode('after_delete', rulesByTrigger.after_delete)}
902
+ }
626
903
 
627
- afterDelete: async (record, db) => {
628
- // Add custom logic here
629
- }
904
+ module.exports = {
905
+ beforeCreate,
906
+ afterCreate,
907
+ beforeUpdate,
908
+ afterUpdate,
909
+ beforeDelete,
910
+ afterDelete
630
911
  };
631
912
  `;
632
913
  }
@@ -663,30 +944,33 @@ function generateAgentRoutes() {
663
944
 
664
945
  const express = require('express');
665
946
  const router = express.Router();
666
- const { authenticate } = require('../middleware/auth');
667
- const agentAuth = require('../middleware/agentAuth');
668
- const { getActionChips, executeAction } = require('../../agents/actions');
947
+ const { optionalAuth } = require('../middleware/auth');
948
+ const { checkAgentAccess, checkTablePermissions } = require('../middleware/agentAuth');
949
+ const { executeAction } = require('../../agents/actions');
669
950
  const agentConfig = require('../../agents/config.json');
670
951
 
952
+ // Middleware chain: optionalAuth -> checkAgentAccess -> checkTablePermissions
953
+ // optionalAuth: tries to authenticate but doesn't fail if no token (for public agents)
954
+ // checkAgentAccess: enforces access rules (public/authenticated/table-based)
955
+ // checkTablePermissions: enforces CRUD permissions on tables
956
+
671
957
  // Get agent info
672
- router.get('/:agent', authenticate, (req, res) => {
673
- const agent = agentConfig[req.params.agent];
674
- if (!agent) {
675
- return res.status(404).json({ error: 'Agent not found' });
676
- }
958
+ router.get('/:agent', optionalAuth, checkAgentAccess, (req, res) => {
959
+ const agent = req.agent;
677
960
  res.json({
678
961
  name: agent.name,
679
962
  description: agent.description,
680
963
  welcome_message: agent.welcome_message,
681
- action_chips: agent.action_chips
964
+ action_chips: agent.action_chips,
965
+ access_type: agent.access_rules?.type || 'authenticated'
682
966
  });
683
967
  });
684
968
 
685
969
  // Execute action chip
686
- router.post('/:agent/action', authenticate, async (req, res) => {
970
+ router.post('/:agent/action', optionalAuth, checkAgentAccess, async (req, res) => {
687
971
  try {
688
972
  const { action } = req.body;
689
- const result = await executeAction(req.params.agent, action, req.user?.id);
973
+ const result = await executeAction(req.params.agent, action, req.user?.userId, req.matchedRecord?.id);
690
974
  res.json(result);
691
975
  } catch (error) {
692
976
  res.status(400).json({ error: error.message });
@@ -694,49 +978,101 @@ router.post('/:agent/action', authenticate, async (req, res) => {
694
978
  });
695
979
 
696
980
  // Agent-scoped CRUD
697
- router.get('/:agent/:table', authenticate, agentAuth, async (req, res) => {
981
+ router.get('/:agent/:table', optionalAuth, checkAgentAccess, checkTablePermissions, async (req, res) => {
698
982
  const controller = require(\`../controllers/\${req.params.table}\`);
699
983
  return controller.list(req, res);
700
984
  });
701
985
 
702
- router.get('/:agent/:table/:id', authenticate, agentAuth, async (req, res) => {
986
+ router.get('/:agent/:table/:id', optionalAuth, checkAgentAccess, checkTablePermissions, async (req, res) => {
703
987
  const controller = require(\`../controllers/\${req.params.table}\`);
704
988
  return controller.get(req, res);
705
989
  });
706
990
 
707
- router.post('/:agent/:table', authenticate, agentAuth, async (req, res) => {
991
+ router.post('/:agent/:table', optionalAuth, checkAgentAccess, checkTablePermissions, async (req, res) => {
708
992
  const controller = require(\`../controllers/\${req.params.table}\`);
709
993
  return controller.create(req, res);
710
994
  });
711
995
 
712
- router.put('/:agent/:table/:id', authenticate, agentAuth, async (req, res) => {
996
+ router.put('/:agent/:table/:id', optionalAuth, checkAgentAccess, checkTablePermissions, async (req, res) => {
713
997
  const controller = require(\`../controllers/\${req.params.table}\`);
714
998
  return controller.update(req, res);
715
999
  });
716
1000
 
1001
+ router.delete('/:agent/:table/:id', optionalAuth, checkAgentAccess, checkTablePermissions, async (req, res) => {
1002
+ const controller = require(\`../controllers/\${req.params.table}\`);
1003
+ return controller.delete(req, res);
1004
+ });
1005
+
717
1006
  module.exports = router;
718
1007
  `;
719
1008
  }
720
- function generateAgentConfig(agents) {
1009
+ function generateAgentConfig(agents, rules = []) {
721
1010
  const config = {};
722
1011
  for (const agent of agents || []) {
1012
+ // Build access_rules from auth_type
1013
+ let accessRules = { type: 'authenticated' };
1014
+ const authType = agent.auth_type || 'authenticated';
1015
+ if (authType === 'public') {
1016
+ accessRules = { type: 'public' };
1017
+ }
1018
+ else if (authType === 'table' || authType === 'table-based') {
1019
+ // Table-based auth - user must exist in specific table
1020
+ accessRules = {
1021
+ type: 'table-based',
1022
+ table: agent.auth_table || agent.audience?.toLowerCase().replace(/\s+/g, '_'),
1023
+ match_field: 'email',
1024
+ match_value: '{{user.email}}'
1025
+ };
1026
+ }
1027
+ else {
1028
+ accessRules = { type: 'authenticated' };
1029
+ }
723
1030
  config[agent.slug] = {
724
1031
  name: agent.name,
725
1032
  slug: agent.slug,
726
1033
  description: agent.description,
1034
+ audience: agent.audience || agent.name,
727
1035
  welcome_message: agent.welcome_message,
728
- auth_type: agent.auth_type || 'authenticated',
1036
+ access_rules: accessRules,
729
1037
  permissions: agent.permissions || {},
730
1038
  user_filter: agent.user_filter || {},
731
- action_chips: (agent.action_chips || []).map((chip) => ({
732
- label: chip.label,
733
- icon: chip.icon,
734
- action: chip.action,
735
- table: chip.table,
736
- display: chip.display || 'table',
737
- filter: chip.filter,
738
- fields: chip.fields
739
- }))
1039
+ action_chips: (agent.action_chips || []).map((chip) => {
1040
+ // Start with chip's existing autoFill and hiddenFields
1041
+ const autoFill = [...(chip.autoFill || [])];
1042
+ const hiddenFields = [...(chip.hiddenFields || [])];
1043
+ // For create actions, add autoFill from before_create rules
1044
+ if (chip.action === 'create' && chip.table) {
1045
+ const createRules = (rules || []).filter((r) => (r.table === chip.table || r.table_slug === chip.table) &&
1046
+ r.trigger === 'before_create' &&
1047
+ r.enabled !== false);
1048
+ for (const rule of createRules) {
1049
+ for (const action of (rule.actions || [])) {
1050
+ if (action.type === 'set_field' && action.field) {
1051
+ // Add to autoFill if not already set by chip
1052
+ if (!autoFill.find((af) => af.field === action.field)) {
1053
+ autoFill.push({ field: action.field, value: action.value });
1054
+ }
1055
+ // Add to hiddenFields if not already there
1056
+ if (!hiddenFields.includes(action.field)) {
1057
+ hiddenFields.push(action.field);
1058
+ }
1059
+ }
1060
+ }
1061
+ }
1062
+ }
1063
+ return {
1064
+ label: chip.label,
1065
+ icon: chip.icon,
1066
+ action: chip.action,
1067
+ table: chip.table,
1068
+ display: chip.display || 'table',
1069
+ filter: chip.filter,
1070
+ filters: chip.filters,
1071
+ fields: chip.fields,
1072
+ autoFill: autoFill.length > 0 ? autoFill : undefined,
1073
+ hiddenFields: hiddenFields.length > 0 ? hiddenFields : undefined
1074
+ };
1075
+ })
740
1076
  };
741
1077
  }
742
1078
  return JSON.stringify(config, null, 2);
@@ -813,18 +1149,94 @@ function generateAgentMiddleware() {
813
1149
  return `/**
814
1150
  * Agent Permission Middleware
815
1151
  * Auto-generated by ThinkSoft CLI
1152
+ *
1153
+ * Handles:
1154
+ * 1. Access Rules (public/authenticated/table-based)
1155
+ * 2. Table Permissions (CRUD)
816
1156
  */
817
1157
 
1158
+ const db = require('../db');
818
1159
  const agentConfig = require('../../agents/config.json');
819
1160
 
820
- const agentAuth = (req, res, next) => {
1161
+ /**
1162
+ * Check agent access rules
1163
+ * - public: anyone can access (no login required)
1164
+ * - authenticated: must be logged in (in app_users)
1165
+ * - table-based: must be logged in AND exist in specified table
1166
+ */
1167
+ const checkAgentAccess = async (req, res, next) => {
821
1168
  const agent = agentConfig[req.params.agent];
822
1169
  if (!agent) {
823
1170
  return res.status(404).json({ error: 'Agent not found' });
824
1171
  }
825
1172
 
1173
+ const accessRules = agent.access_rules || { type: 'authenticated' };
1174
+
1175
+ switch (accessRules.type) {
1176
+ case 'public':
1177
+ // Anyone can access, no login required
1178
+ req.agent = agent;
1179
+ return next();
1180
+
1181
+ case 'authenticated':
1182
+ // Must be logged in
1183
+ if (!req.user) {
1184
+ return res.status(401).json({ error: 'Authentication required' });
1185
+ }
1186
+ req.agent = agent;
1187
+ return next();
1188
+
1189
+ case 'table-based':
1190
+ // Must be logged in AND exist in specified table
1191
+ if (!req.user) {
1192
+ return res.status(401).json({ error: 'Authentication required' });
1193
+ }
1194
+
1195
+ if (!accessRules.table) {
1196
+ // No table specified, fall back to authenticated
1197
+ req.agent = agent;
1198
+ return next();
1199
+ }
1200
+
1201
+ try {
1202
+ const matchField = accessRules.match_field || 'email';
1203
+ const matchValue = accessRules.match_value === '{{user.id}}'
1204
+ ? req.user.userId
1205
+ : req.user.email;
1206
+
1207
+ const { rows } = await db.query(
1208
+ \`SELECT id FROM \${accessRules.table} WHERE \${matchField} = $1 LIMIT 1\`,
1209
+ [matchValue]
1210
+ );
1211
+
1212
+ if (rows.length === 0) {
1213
+ return res.status(403).json({
1214
+ error: \`Access denied. You are not registered as a \${agent.audience || 'user'}.\`
1215
+ });
1216
+ }
1217
+
1218
+ req.matchedRecord = rows[0];
1219
+ req.agent = agent;
1220
+ return next();
1221
+ } catch (error) {
1222
+ console.error('Access check error:', error);
1223
+ // If table doesn't exist, fall back to authenticated
1224
+ req.agent = agent;
1225
+ return next();
1226
+ }
1227
+
1228
+ default:
1229
+ return res.status(403).json({ error: 'Unknown access rule type' });
1230
+ }
1231
+ };
1232
+
1233
+ /**
1234
+ * Check table-level permissions (CRUD)
1235
+ */
1236
+ const checkTablePermissions = (req, res, next) => {
1237
+ const agent = req.agent;
826
1238
  const table = req.params.table;
827
- const permissions = agent.permissions[table];
1239
+ const permissions = agent.permissions?.[table];
828
1240
 
829
1241
  if (!permissions) {
830
1242
  return res.status(403).json({ error: 'No access to this table' });
@@ -838,18 +1250,20 @@ const agentAuth = (req, res, next) => {
838
1250
  (method === 'DELETE' && permissions.delete);
839
1251
 
840
1252
  if (!allowed) {
841
- return res.status(403).json({ error: 'Permission denied' });
1253
+ return res.status(403).json({ error: 'Permission denied for this action' });
842
1254
  }
843
1255
 
1256
+ // Apply user filter if configured
844
1257
  if (agent.user_filter?.[table] && req.user) {
845
- req.userFilter = agent.user_filter[table].replace(/:current_user_id/g, req.user.id);
1258
+ req.userFilter = agent.user_filter[table]
1259
+ .replace(/{{user\\.id}}/g, req.user.userId)
1260
+ .replace(/{{user\\.email}}/g, req.user.email);
846
1261
  }
847
1262
 
848
- req.agent = agent;
849
1263
  next();
850
1264
  };
851
1265
 
852
- module.exports = agentAuth;
1266
+ module.exports = { checkAgentAccess, checkTablePermissions };
853
1267
  `;
854
1268
  }
855
1269
  function generateChat(option) {
@@ -918,61 +1332,513 @@ module.exports = { chat };
918
1332
  `;
919
1333
  }
920
1334
  function generateClient(tables, endpoints, baseUrl) {
921
- let code = `/**
922
- * API Client
1335
+ // Generate table list for schema endpoint
1336
+ const tableSchemas = (tables || []).map(t => ({
1337
+ slug: t.slug,
1338
+ name: t.name,
1339
+ fields: t.fields || []
1340
+ }));
1341
+ return `/**
1342
+ * ThinkSoft Compatible Client SDK
923
1343
  * Auto-generated by ThinkSoft CLI
1344
+ *
1345
+ * This client mirrors the @thinksoftai/sdk interface.
1346
+ * Code written for ThinkSoft hosted will work with this client.
1347
+ *
1348
+ * Usage:
1349
+ * import { ThinkSoft } from './client/api';
1350
+ * const client = new ThinkSoft({ appId: 'my-app' });
1351
+ *
1352
+ * // Auth
1353
+ * await client.auth.sendOtp('user@example.com');
1354
+ * await client.auth.verifyOtp('user@example.com', '123456');
1355
+ *
1356
+ * // CRUD
1357
+ * const records = await client.from('orders').list();
1358
+ * await client.from('orders').create({ product: 'Widget' });
924
1359
  */
925
1360
 
926
- const API_BASE = process.env.REACT_APP_API_URL || 'http://localhost:3000${baseUrl}';
1361
+ const DEFAULT_BASE_URL = 'http://localhost:3000${baseUrl}';
927
1362
 
928
- const getHeaders = () => ({
929
- 'Content-Type': 'application/json',
930
- ...(localStorage.getItem('token') && {
931
- 'Authorization': \`Bearer \${localStorage.getItem('token')}\`
932
- })
933
- });
1363
+ // Storage keys
1364
+ const TOKEN_KEY = 'thinksoft_token';
1365
+ const REFRESH_TOKEN_KEY = 'thinksoft_refresh_token';
1366
+ const EXPIRES_AT_KEY = 'thinksoft_expires_at';
1367
+ const USER_KEY = 'thinksoft_user';
934
1368
 
935
- const handleResponse = async (response) => {
936
- const data = await response.json();
937
- if (!response.ok) throw new Error(data.error || 'Request failed');
938
- return data;
939
- };
1369
+ // In-memory storage fallback
1370
+ const memoryStorage = {};
940
1371
 
941
- const api = {
942
- `;
943
- for (const table of tables || []) {
944
- const path = endpoints[table.slug]?.path || `/${table.slug}`;
945
- code += ` ${table.slug}: {
946
- list: (params = {}) => fetch(\`\${API_BASE}${path}?\${new URLSearchParams(params)}\`, { headers: getHeaders() }).then(handleResponse),
947
- get: (id) => fetch(\`\${API_BASE}${path}/\${id}\`, { headers: getHeaders() }).then(handleResponse),
948
- create: (data) => fetch(\`\${API_BASE}${path}\`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(data) }).then(handleResponse),
949
- update: (id, data) => fetch(\`\${API_BASE}${path}/\${id}\`, { method: 'PUT', headers: getHeaders(), body: JSON.stringify(data) }).then(handleResponse),
950
- delete: (id) => fetch(\`\${API_BASE}${path}/\${id}\`, { method: 'DELETE', headers: getHeaders() }).then(handleResponse),
951
- },
952
- `;
953
- }
954
- code += `};
1372
+ function getStorage(type) {
1373
+ if (type === 'none') return null;
1374
+
1375
+ if (type === 'memory') {
1376
+ return {
1377
+ getItem: (key) => memoryStorage[key] || null,
1378
+ setItem: (key, value) => { memoryStorage[key] = value; },
1379
+ removeItem: (key) => { delete memoryStorage[key]; },
1380
+ clear: () => { Object.keys(memoryStorage).forEach(k => delete memoryStorage[k]); }
1381
+ };
1382
+ }
1383
+
1384
+ // Check if localStorage is available (browser)
1385
+ if (typeof window !== 'undefined' && window.localStorage) {
1386
+ return window.localStorage;
1387
+ }
1388
+
1389
+ // Fallback to memory
1390
+ return getStorage('memory');
1391
+ }
1392
+
1393
+ /**
1394
+ * Token Storage - handles storing and retrieving auth tokens
1395
+ */
1396
+ class TokenStorage {
1397
+ constructor(type = 'localStorage') {
1398
+ this.storage = getStorage(type);
1399
+ this._token = null;
1400
+ this._refreshToken = null;
1401
+ this._expiresAt = null;
1402
+ this._user = null;
1403
+ this.load();
1404
+ }
1405
+
1406
+ load() {
1407
+ if (!this.storage) return;
1408
+ this._token = this.storage.getItem(TOKEN_KEY);
1409
+ this._refreshToken = this.storage.getItem(REFRESH_TOKEN_KEY);
1410
+ const expiresAtStr = this.storage.getItem(EXPIRES_AT_KEY);
1411
+ if (expiresAtStr) this._expiresAt = parseInt(expiresAtStr, 10);
1412
+ const userJson = this.storage.getItem(USER_KEY);
1413
+ if (userJson) {
1414
+ try { this._user = JSON.parse(userJson); } catch { this._user = null; }
1415
+ }
1416
+ }
1417
+
1418
+ get token() { return this._token; }
1419
+ get refreshToken() { return this._refreshToken; }
1420
+ get expiresAt() { return this._expiresAt; }
1421
+ get user() { return this._user; }
1422
+
1423
+ isTokenExpired(bufferSeconds = 300) {
1424
+ if (!this._token || !this._expiresAt) return true;
1425
+ const now = Math.floor(Date.now() / 1000);
1426
+ return now >= (this._expiresAt - bufferSeconds);
1427
+ }
1428
+
1429
+ setToken(token, refreshToken, expiresIn) {
1430
+ this._token = token;
1431
+ this._refreshToken = refreshToken || null;
1432
+ if (expiresIn) {
1433
+ this._expiresAt = Math.floor(Date.now() / 1000) + expiresIn;
1434
+ }
1435
+ if (this.storage) {
1436
+ this.storage.setItem(TOKEN_KEY, token);
1437
+ if (refreshToken) this.storage.setItem(REFRESH_TOKEN_KEY, refreshToken);
1438
+ if (this._expiresAt) this.storage.setItem(EXPIRES_AT_KEY, this._expiresAt.toString());
1439
+ }
1440
+ }
1441
+
1442
+ setUser(user) {
1443
+ this._user = user;
1444
+ if (this.storage) {
1445
+ this.storage.setItem(USER_KEY, JSON.stringify(user));
1446
+ }
1447
+ }
1448
+
1449
+ clear() {
1450
+ this._token = null;
1451
+ this._refreshToken = null;
1452
+ this._expiresAt = null;
1453
+ this._user = null;
1454
+ if (this.storage) {
1455
+ this.storage.removeItem(TOKEN_KEY);
1456
+ this.storage.removeItem(REFRESH_TOKEN_KEY);
1457
+ this.storage.removeItem(EXPIRES_AT_KEY);
1458
+ this.storage.removeItem(USER_KEY);
1459
+ }
1460
+ }
1461
+ }
1462
+
1463
+ /**
1464
+ * HTTP Client - handles API requests
1465
+ */
1466
+ class HttpClient {
1467
+ constructor(baseUrl, storage) {
1468
+ this.baseUrl = baseUrl;
1469
+ this.storage = storage;
1470
+ }
1471
+
1472
+ async request(method, path, body = null, requiresAuth = true) {
1473
+ const headers = { 'Content-Type': 'application/json' };
1474
+
1475
+ if (requiresAuth && this.storage.token) {
1476
+ headers['Authorization'] = \`Bearer \${this.storage.token}\`;
1477
+ }
1478
+
1479
+ const options = { method, headers };
1480
+ if (body && method !== 'GET') {
1481
+ options.body = JSON.stringify(body);
1482
+ }
1483
+
1484
+ const response = await fetch(\`\${this.baseUrl}\${path}\`, options);
1485
+ const data = await response.json();
1486
+
1487
+ if (!response.ok) {
1488
+ return { error: data.error || 'Request failed' };
1489
+ }
1490
+
1491
+ return data;
1492
+ }
1493
+
1494
+ get(path, requiresAuth = true) { return this.request('GET', path, null, requiresAuth); }
1495
+ post(path, body, requiresAuth = true) { return this.request('POST', path, body, requiresAuth); }
1496
+ put(path, body, requiresAuth = true) { return this.request('PUT', path, body, requiresAuth); }
1497
+ delete(path, requiresAuth = true) { return this.request('DELETE', path, null, requiresAuth); }
1498
+ }
1499
+
1500
+ /**
1501
+ * Auth Module - handles authentication
1502
+ */
1503
+ class Auth {
1504
+ constructor(http, storage) {
1505
+ this.http = http;
1506
+ this.storage = storage;
1507
+ }
1508
+
1509
+ async sendOtp(email) {
1510
+ const response = await this.http.post('/auth/send-otp', { email }, false);
1511
+ if (response.error) return { success: false, error: response.error };
1512
+ return { success: true, message: response.message || 'OTP sent' };
1513
+ }
1514
+
1515
+ async verifyOtp(email, otp) {
1516
+ const response = await this.http.post('/auth/verify-otp', { email, otp }, false);
1517
+ if (response.error) return { success: false, error: response.error };
1518
+
1519
+ if (response.accessToken || response.token) {
1520
+ const token = response.accessToken || response.token;
1521
+ this.storage.setToken(token, response.refreshToken || response.refresh_token, response.expires_in || 3600);
1522
+ }
1523
+ if (response.user) {
1524
+ this.storage.setUser(response.user);
1525
+ }
1526
+
1527
+ return {
1528
+ success: true,
1529
+ token: response.accessToken || response.token,
1530
+ refresh_token: response.refreshToken || response.refresh_token,
1531
+ expires_in: response.expires_in,
1532
+ user: response.user
1533
+ };
1534
+ }
1535
+
1536
+ setToken(token) { this.storage.setToken(token); }
1537
+ getToken() { return this.storage.token; }
1538
+ getUser() { return this.storage.user; }
1539
+ isAuthenticated() { return !!this.storage.token; }
1540
+ isTokenExpired(bufferSeconds = 300) { return this.storage.isTokenExpired(bufferSeconds); }
1541
+ getExpiresAt() { return this.storage.expiresAt; }
1542
+
1543
+ async refreshSession() {
1544
+ const refreshToken = this.storage.refreshToken;
1545
+ if (!refreshToken) return { success: false, error: 'No refresh token available' };
1546
+
1547
+ const response = await this.http.post('/auth/refresh-token', { refreshToken }, false);
1548
+ if (response.error) {
1549
+ this.storage.clear();
1550
+ return { success: false, error: response.error };
1551
+ }
1552
+
1553
+ if (response.accessToken || response.token) {
1554
+ const token = response.accessToken || response.token;
1555
+ this.storage.setToken(token, response.refreshToken || refreshToken, response.expires_in || 3600);
1556
+ }
1557
+
1558
+ return { success: true, token: response.accessToken || response.token };
1559
+ }
955
1560
 
956
- export default api;
1561
+ async ensureValidToken() {
1562
+ if (!this.storage.token) return false;
1563
+ if (!this.isTokenExpired()) return true;
1564
+ const result = await this.refreshSession();
1565
+ return result.success;
1566
+ }
1567
+
1568
+ logout() { this.storage.clear(); }
1569
+ }
1570
+
1571
+ /**
1572
+ * Query Builder - handles CRUD operations for a table
1573
+ */
1574
+ class QueryBuilder {
1575
+ constructor(http, table, tableSchemas) {
1576
+ this.http = http;
1577
+ this.table = table;
1578
+ this.tableSchemas = tableSchemas;
1579
+ }
1580
+
1581
+ async schema() {
1582
+ // Return from local schema (self-hosted)
1583
+ const tableSchema = this.tableSchemas.find(t => t.slug === this.table);
1584
+ if (!tableSchema) throw new Error('Table not found');
1585
+ return tableSchema;
1586
+ }
1587
+
1588
+ async fields() {
1589
+ const tableSchema = await this.schema();
1590
+ return tableSchema.fields || [];
1591
+ }
1592
+
1593
+ async list(options = {}) {
1594
+ const { filter, sort, limit = 50, offset = 0 } = options;
1595
+ const params = new URLSearchParams();
1596
+ if (filter) params.set('filter', JSON.stringify(filter));
1597
+ if (sort) params.set('sort', sort);
1598
+ params.set('limit', limit.toString());
1599
+ params.set('offset', offset.toString());
1600
+
1601
+ const queryString = params.toString();
1602
+ const path = \`/\${this.table}\${queryString ? '?' + queryString : ''}\`;
1603
+ const response = await this.http.get(path);
1604
+
1605
+ if (response.error) throw new Error(response.error);
1606
+
1607
+ const data = Array.isArray(response.data) ? response.data : [];
1608
+ return { data, count: response.count ?? data.length };
1609
+ }
1610
+
1611
+ async get(id) {
1612
+ const response = await this.http.get(\`/\${this.table}/\${id}\`);
1613
+ if (response.error) throw new Error(response.error);
1614
+ return response.data;
1615
+ }
1616
+
1617
+ async create(data) {
1618
+ const response = await this.http.post(\`/\${this.table}\`, data);
1619
+ if (response.error) throw new Error(response.error);
1620
+ return response.data;
1621
+ }
1622
+
1623
+ async update(id, data) {
1624
+ const response = await this.http.put(\`/\${this.table}/\${id}\`, data);
1625
+ if (response.error) throw new Error(response.error);
1626
+ return response.data;
1627
+ }
1628
+
1629
+ async delete(id) {
1630
+ const response = await this.http.delete(\`/\${this.table}/\${id}\`);
1631
+ if (response.error) throw new Error(response.error);
1632
+ return response.success || true;
1633
+ }
1634
+
1635
+ async findFirst(filter) {
1636
+ const result = await this.list({ filter, limit: 1 });
1637
+ return result.data[0] || null;
1638
+ }
1639
+
1640
+ async count(filter) {
1641
+ const result = await this.list({ filter, limit: 0 });
1642
+ return result.count;
1643
+ }
1644
+ }
1645
+
1646
+ /**
1647
+ * ThinkSoft Client - Main SDK class
1648
+ * Compatible with @thinksoftai/sdk interface
1649
+ */
1650
+ class ThinkSoft {
1651
+ constructor(config) {
1652
+ if (!config.appId) {
1653
+ throw new Error('appId is required');
1654
+ }
1655
+
1656
+ this.config = {
1657
+ appId: config.appId,
1658
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
1659
+ token: config.token || '',
1660
+ storage: config.storage || 'localStorage'
1661
+ };
1662
+
1663
+ // Table schemas (generated from export)
1664
+ this.tableSchemas = ${JSON.stringify(tableSchemas, null, 2).split('\n').map((line, i) => i === 0 ? line : ' ' + line).join('\n')};
1665
+
1666
+ // Initialize storage
1667
+ this.storage = new TokenStorage(this.config.storage);
1668
+
1669
+ // Set initial token if provided
1670
+ if (this.config.token) {
1671
+ this.storage.setToken(this.config.token);
1672
+ }
1673
+
1674
+ // Initialize HTTP client
1675
+ this.http = new HttpClient(this.config.baseUrl, this.storage);
1676
+
1677
+ // Initialize modules
1678
+ this.auth = new Auth(this.http, this.storage);
1679
+ }
1680
+
1681
+ async tables() {
1682
+ return this.tableSchemas.map(t => ({
1683
+ id: t.slug,
1684
+ slug: t.slug,
1685
+ name: t.name,
1686
+ fieldCount: (t.fields || []).length
1687
+ }));
1688
+ }
1689
+
1690
+ from(table) {
1691
+ return new QueryBuilder(this.http, table, this.tableSchemas);
1692
+ }
1693
+
1694
+ getAppId() { return this.config.appId; }
1695
+ getBaseUrl() { return this.config.baseUrl; }
1696
+ }
1697
+
1698
+ // ES Module export
1699
+ export { ThinkSoft, Auth, QueryBuilder, TokenStorage };
1700
+ export default ThinkSoft;
1701
+
1702
+ // CommonJS compatibility
1703
+ if (typeof module !== 'undefined' && module.exports) {
1704
+ module.exports = { ThinkSoft, Auth, QueryBuilder, TokenStorage, default: ThinkSoft };
1705
+ }
957
1706
  `;
958
- return code;
959
1707
  }
960
1708
  function generateEvents(tables) {
1709
+ const tableListeners = (tables || []).map(t => `
1710
+ // ${t.name} events
1711
+ eventBus.on('${t.slug}.created', async ({ new: record }) => {
1712
+ console.log('📝 ${t.name} created:', record.id);
1713
+ await processNotifications('${t.slug}', 'created', record);
1714
+ });
1715
+
1716
+ eventBus.on('${t.slug}.updated', async ({ old: oldRecord, new: newRecord }) => {
1717
+ console.log('✏️ ${t.name} updated:', newRecord.id);
1718
+ await processNotifications('${t.slug}', 'updated', newRecord, oldRecord);
1719
+ });
1720
+
1721
+ eventBus.on('${t.slug}.deleted', async ({ old: record }) => {
1722
+ console.log('🗑️ ${t.name} deleted:', record.id);
1723
+ await processNotifications('${t.slug}', 'deleted', record);
1724
+ });
1725
+ `).join('\n');
961
1726
  return `/**
962
1727
  * Event Bus
963
1728
  * Auto-generated by ThinkSoft CLI
1729
+ *
1730
+ * Events emitted:
1731
+ * - {table}.created - { new: record }
1732
+ * - {table}.updated - { old: oldRecord, new: newRecord }
1733
+ * - {table}.deleted - { old: record }
964
1734
  */
965
1735
 
966
1736
  const EventEmitter = require('events');
967
1737
  const eventBus = new EventEmitter();
968
1738
 
969
- // Add event listeners here
970
- // eventBus.on('orders.created', async ({ new: record }) => { ... });
1739
+ // Notification queue (for external actions like emails, webhooks)
1740
+ const notificationQueue = [];
1741
+
1742
+ /**
1743
+ * Process notifications for an event
1744
+ * This handles email, webhook, and other external actions
1745
+ */
1746
+ async function processNotifications(table, action, record, oldRecord = null) {
1747
+ // Load notification handlers from config
1748
+ const handlers = getNotificationHandlers(table, action);
1749
+
1750
+ for (const handler of handlers) {
1751
+ try {
1752
+ switch (handler.type) {
1753
+ case 'email':
1754
+ await sendEmailNotification(handler, record, oldRecord);
1755
+ break;
1756
+ case 'webhook':
1757
+ await sendWebhook(handler, record, action, oldRecord);
1758
+ break;
1759
+ case 'log':
1760
+ console.log(\`[Notification] \${handler.message || table + '.' + action}\`, record.id);
1761
+ break;
1762
+ }
1763
+ } catch (error) {
1764
+ console.error(\`Notification handler error (\${handler.type}):\`, error.message);
1765
+ }
1766
+ }
1767
+ }
1768
+
1769
+ /**
1770
+ * Get notification handlers for a table/action
1771
+ * Override this to customize notification behavior
1772
+ */
1773
+ function getNotificationHandlers(table, action) {
1774
+ // Default: no notifications
1775
+ // Add your notification rules here
1776
+ return [];
1777
+ }
1778
+
1779
+ /**
1780
+ * Send email notification
1781
+ */
1782
+ async function sendEmailNotification(handler, record, oldRecord) {
1783
+ // ThinkSoft email proxy or custom implementation
1784
+ const emailEndpoint = process.env.EMAIL_ENDPOINT || 'https://developerapi-ir6zo3lfxq-uc.a.run.app/api/v1/email/send';
1785
+
1786
+ if (!handler.to) return;
1787
+
1788
+ const to = typeof handler.to === 'function' ? handler.to(record) : handler.to;
1789
+ const subject = typeof handler.subject === 'function' ? handler.subject(record) : handler.subject;
1790
+ const body = typeof handler.body === 'function' ? handler.body(record) : handler.body;
1791
+
1792
+ // Queue the email (implement your email sending logic)
1793
+ console.log(\`📧 Email queued: to=\${to}, subject=\${subject}\`);
1794
+
1795
+ // If using ThinkSoft email proxy:
1796
+ // await fetch(emailEndpoint, { method: 'POST', body: JSON.stringify({ to, subject, body }) });
1797
+ }
1798
+
1799
+ /**
1800
+ * Send webhook notification
1801
+ */
1802
+ async function sendWebhook(handler, record, action, oldRecord) {
1803
+ if (!handler.url) return;
1804
+
1805
+ try {
1806
+ const payload = {
1807
+ event: \`\${action}\`,
1808
+ timestamp: new Date().toISOString(),
1809
+ data: record,
1810
+ ...(oldRecord && { previousData: oldRecord })
1811
+ };
1812
+
1813
+ const response = await fetch(handler.url, {
1814
+ method: 'POST',
1815
+ headers: {
1816
+ 'Content-Type': 'application/json',
1817
+ ...(handler.headers || {}),
1818
+ ...(handler.secret && { 'X-Webhook-Secret': handler.secret })
1819
+ },
1820
+ body: JSON.stringify(payload)
1821
+ });
1822
+
1823
+ if (!response.ok) {
1824
+ console.error(\`Webhook failed: \${response.status}\`);
1825
+ }
1826
+ } catch (error) {
1827
+ console.error('Webhook error:', error.message);
1828
+ }
1829
+ }
1830
+
1831
+ // Table event listeners
1832
+ ${tableListeners}
971
1833
 
972
1834
  module.exports = eventBus;
1835
+ module.exports.processNotifications = processNotifications;
1836
+ module.exports.getNotificationHandlers = getNotificationHandlers;
973
1837
  `;
974
1838
  }
975
- function generateIndex(baseUrl) {
1839
+ function generateIndex(baseUrl, authOption = 'none') {
1840
+ const authImport = authOption !== 'none' ? "const authRoutes = require('./routes/auth');\n" : '';
1841
+ const authRoute = authOption !== 'none' ? `app.use('${baseUrl}/auth', authRoutes);\n` : '';
976
1842
  return `/**
977
1843
  * Server Entry Point
978
1844
  * Auto-generated by ThinkSoft CLI
@@ -983,7 +1849,7 @@ const express = require('express');
983
1849
  const cors = require('cors');
984
1850
  const routes = require('./routes');
985
1851
  const agentRoutes = require('./routes/agents');
986
-
1852
+ ${authImport}
987
1853
  const app = express();
988
1854
  const PORT = process.env.PORT || 3000;
989
1855
 
@@ -993,7 +1859,7 @@ app.use(express.json());
993
1859
  // Routes
994
1860
  app.use('${baseUrl}', routes);
995
1861
  app.use('${baseUrl}/agent', agentRoutes);
996
-
1862
+ ${authRoute}
997
1863
  // Health check
998
1864
  app.get('/health', (req, res) => res.json({ status: 'ok' }));
999
1865
 
@@ -1029,6 +1895,10 @@ function generateAuthMiddleware() {
1029
1895
 
1030
1896
  const jwt = require('jsonwebtoken');
1031
1897
 
1898
+ /**
1899
+ * Strict authentication - fails if no valid token
1900
+ * Use for protected endpoints that always require login
1901
+ */
1032
1902
  const authenticate = (req, res, next) => {
1033
1903
  const authHeader = req.headers.authorization;
1034
1904
 
@@ -1047,7 +1917,276 @@ const authenticate = (req, res, next) => {
1047
1917
  }
1048
1918
  };
1049
1919
 
1050
- module.exports = { authenticate };
1920
+ /**
1921
+ * Optional authentication - sets req.user if token present, but doesn't fail
1922
+ * Use for endpoints that may be public or authenticated based on agent config
1923
+ */
1924
+ const optionalAuth = (req, res, next) => {
1925
+ const authHeader = req.headers.authorization;
1926
+
1927
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
1928
+ // No token, but that's ok - continue without user
1929
+ req.user = null;
1930
+ return next();
1931
+ }
1932
+
1933
+ const token = authHeader.split(' ')[1];
1934
+
1935
+ try {
1936
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
1937
+ req.user = decoded;
1938
+ } catch (error) {
1939
+ // Invalid token, continue without user
1940
+ req.user = null;
1941
+ }
1942
+
1943
+ next();
1944
+ };
1945
+
1946
+ module.exports = { authenticate, optionalAuth };
1947
+ `;
1948
+ }
1949
+ function generateAuthController(app, authOption) {
1950
+ const appId = app?.id || 'APP_ID';
1951
+ const appName = app?.name || 'App';
1952
+ const emailSendCode = authOption === 'thinksoft'
1953
+ ? `// Send OTP via ThinkSoft email proxy (no setup needed)
1954
+ const response = await fetch('https://developerapi-ir6zo3lfxq-uc.a.run.app/api/v1/email/send-otp', {
1955
+ method: 'POST',
1956
+ headers: { 'Content-Type': 'application/json' },
1957
+ body: JSON.stringify({
1958
+ appId: process.env.THINKSOFT_APP_ID || '${appId}',
1959
+ email,
1960
+ otp,
1961
+ appName: process.env.APP_NAME || '${appName}',
1962
+ expiresInMinutes: 10
1963
+ })
1964
+ });
1965
+
1966
+ if (!response.ok) {
1967
+ const error = await response.json();
1968
+ throw new Error(error.error || 'Failed to send OTP');
1969
+ }`
1970
+ : `// Send OTP via self-hosted email (requires RESEND_API_KEY)
1971
+ const { Resend } = require('resend');
1972
+ const resend = new Resend(process.env.RESEND_API_KEY);
1973
+
1974
+ await resend.emails.send({
1975
+ from: process.env.EMAIL_FROM || 'noreply@example.com',
1976
+ to: email,
1977
+ subject: \`\${otp} is your verification code\`,
1978
+ html: \`<h1>Your verification code</h1><p style="font-size:36px;font-weight:bold">\${otp}</p><p>This code expires in 10 minutes.</p>\`
1979
+ });`;
1980
+ return `/**
1981
+ * Authentication Controller
1982
+ * Auto-generated by ThinkSoft CLI
1983
+ * Auth type: ${authOption}
1984
+ */
1985
+
1986
+ const db = require('../db');
1987
+ const jwt = require('jsonwebtoken');
1988
+
1989
+ // Generate 6-digit OTP
1990
+ function generateOtp() {
1991
+ return Math.floor(100000 + Math.random() * 900000).toString();
1992
+ }
1993
+
1994
+ // Send OTP to email
1995
+ async function sendOtp(req, res) {
1996
+ try {
1997
+ const { email } = req.body;
1998
+
1999
+ if (!email) {
2000
+ return res.status(400).json({ error: 'Email is required' });
2001
+ }
2002
+
2003
+ // Validate email format
2004
+ const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
2005
+ if (!emailRegex.test(email)) {
2006
+ return res.status(400).json({ error: 'Invalid email format' });
2007
+ }
2008
+
2009
+ const otp = generateOtp();
2010
+ const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
2011
+
2012
+ // Create or update user with OTP
2013
+ await db.query(\`
2014
+ INSERT INTO app_users (email, otp_code, otp_expires_at)
2015
+ VALUES ($1, $2, $3)
2016
+ ON CONFLICT (email) DO UPDATE SET
2017
+ otp_code = $2,
2018
+ otp_expires_at = $3,
2019
+ updated_at = NOW()
2020
+ \`, [email, otp, expiresAt]);
2021
+
2022
+ ${emailSendCode}
2023
+
2024
+ res.json({ success: true, message: 'OTP sent to email' });
2025
+
2026
+ } catch (error) {
2027
+ console.error('Send OTP error:', error);
2028
+ res.status(500).json({ error: 'Failed to send OTP' });
2029
+ }
2030
+ }
2031
+
2032
+ // Verify OTP and return tokens
2033
+ async function verifyOtp(req, res) {
2034
+ try {
2035
+ const { email, otp } = req.body;
2036
+
2037
+ if (!email || !otp) {
2038
+ return res.status(400).json({ error: 'Email and OTP are required' });
2039
+ }
2040
+
2041
+ // Find user with valid OTP
2042
+ const { rows } = await db.query(\`
2043
+ SELECT * FROM app_users
2044
+ WHERE email = $1 AND otp_code = $2 AND otp_expires_at > NOW()
2045
+ \`, [email, otp]);
2046
+
2047
+ if (rows.length === 0) {
2048
+ return res.status(401).json({ error: 'Invalid or expired OTP' });
2049
+ }
2050
+
2051
+ const user = rows[0];
2052
+
2053
+ // Generate tokens
2054
+ const accessToken = jwt.sign(
2055
+ { userId: user.id, email: user.email },
2056
+ process.env.JWT_SECRET,
2057
+ { expiresIn: '1h' }
2058
+ );
2059
+
2060
+ const refreshToken = jwt.sign(
2061
+ { userId: user.id, type: 'refresh' },
2062
+ process.env.JWT_SECRET,
2063
+ { expiresIn: '7d' }
2064
+ );
2065
+
2066
+ // Clear OTP and save refresh token
2067
+ await db.query(\`
2068
+ UPDATE app_users SET
2069
+ otp_code = NULL,
2070
+ otp_expires_at = NULL,
2071
+ refresh_token = $2,
2072
+ last_login = NOW(),
2073
+ updated_at = NOW()
2074
+ WHERE id = $1
2075
+ \`, [user.id, refreshToken]);
2076
+
2077
+ res.json({
2078
+ success: true,
2079
+ user: {
2080
+ id: user.id,
2081
+ email: user.email,
2082
+ name: user.name
2083
+ },
2084
+ accessToken,
2085
+ refreshToken
2086
+ });
2087
+
2088
+ } catch (error) {
2089
+ console.error('Verify OTP error:', error);
2090
+ res.status(500).json({ error: 'Authentication failed' });
2091
+ }
2092
+ }
2093
+
2094
+ // Refresh access token
2095
+ async function refreshToken(req, res) {
2096
+ try {
2097
+ const { refreshToken } = req.body;
2098
+
2099
+ if (!refreshToken) {
2100
+ return res.status(400).json({ error: 'Refresh token is required' });
2101
+ }
2102
+
2103
+ // Verify refresh token
2104
+ let decoded;
2105
+ try {
2106
+ decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);
2107
+ } catch (err) {
2108
+ return res.status(401).json({ error: 'Invalid refresh token' });
2109
+ }
2110
+
2111
+ if (decoded.type !== 'refresh') {
2112
+ return res.status(401).json({ error: 'Invalid token type' });
2113
+ }
2114
+
2115
+ // Find user with this refresh token
2116
+ const { rows } = await db.query(
2117
+ 'SELECT * FROM app_users WHERE id = $1 AND refresh_token = $2',
2118
+ [decoded.userId, refreshToken]
2119
+ );
2120
+
2121
+ if (rows.length === 0) {
2122
+ return res.status(401).json({ error: 'Token revoked or user not found' });
2123
+ }
2124
+
2125
+ const user = rows[0];
2126
+
2127
+ // Generate new access token
2128
+ const accessToken = jwt.sign(
2129
+ { userId: user.id, email: user.email },
2130
+ process.env.JWT_SECRET,
2131
+ { expiresIn: '1h' }
2132
+ );
2133
+
2134
+ res.json({ success: true, accessToken });
2135
+
2136
+ } catch (error) {
2137
+ console.error('Refresh token error:', error);
2138
+ res.status(500).json({ error: 'Token refresh failed' });
2139
+ }
2140
+ }
2141
+
2142
+ // Get current user
2143
+ async function getMe(req, res) {
2144
+ try {
2145
+ const { rows } = await db.query(
2146
+ 'SELECT id, email, name, created_at FROM app_users WHERE id = $1',
2147
+ [req.user.userId]
2148
+ );
2149
+
2150
+ if (rows.length === 0) {
2151
+ return res.status(404).json({ error: 'User not found' });
2152
+ }
2153
+
2154
+ res.json({ user: rows[0] });
2155
+
2156
+ } catch (error) {
2157
+ console.error('Get me error:', error);
2158
+ res.status(500).json({ error: 'Failed to get user' });
2159
+ }
2160
+ }
2161
+
2162
+ module.exports = {
2163
+ sendOtp,
2164
+ verifyOtp,
2165
+ refreshToken,
2166
+ getMe
2167
+ };
2168
+ `;
2169
+ }
2170
+ function generateAuthRoutes() {
2171
+ return `/**
2172
+ * Authentication Routes
2173
+ * Auto-generated by ThinkSoft CLI
2174
+ */
2175
+
2176
+ const express = require('express');
2177
+ const router = express.Router();
2178
+ const auth = require('../controllers/auth');
2179
+ const { authenticate } = require('../middleware/auth');
2180
+
2181
+ // Public routes (no auth required)
2182
+ router.post('/send-otp', auth.sendOtp);
2183
+ router.post('/verify-otp', auth.verifyOtp);
2184
+ router.post('/refresh-token', auth.refreshToken);
2185
+
2186
+ // Protected routes
2187
+ router.get('/me', authenticate, auth.getMe);
2188
+
2189
+ module.exports = router;
1051
2190
  `;
1052
2191
  }
1053
2192
  function generateApiConfig(tables, endpoints, config) {
@@ -1091,6 +2230,9 @@ function generatePackageJson(app, config) {
1091
2230
  if (config.chatOption === 'openai') {
1092
2231
  pkg.dependencies.openai = '^4.20.0';
1093
2232
  }
2233
+ if (config.authOption === 'self-hosted') {
2234
+ pkg.dependencies.resend = '^3.0.0';
2235
+ }
1094
2236
  return JSON.stringify(pkg, null, 2);
1095
2237
  }
1096
2238
  function generateDockerCompose(app) {
@@ -1152,6 +2294,20 @@ NODE_ENV=development
1152
2294
  JWT_SECRET=your-secret-key-change-in-production
1153
2295
  JWT_EXPIRES_IN=7d
1154
2296
  `;
2297
+ if (config.authOption === 'thinksoft') {
2298
+ env += `
2299
+ # ThinkSoft Email Proxy (no additional setup needed)
2300
+ THINKSOFT_APP_ID=${config.appId || 'YOUR_APP_ID'}
2301
+ APP_NAME=${config.appName || 'Your App'}
2302
+ `;
2303
+ }
2304
+ else if (config.authOption === 'self-hosted') {
2305
+ env += `
2306
+ # Self-hosted Email (requires Resend account)
2307
+ RESEND_API_KEY=re_...
2308
+ EMAIL_FROM=noreply@yourdomain.com
2309
+ `;
2310
+ }
1155
2311
  if (config.chatOption === 'openai') {
1156
2312
  env += `
1157
2313
  # OpenAI (for AI chat)
@@ -1225,4 +2381,299 @@ psql -d your_database -f database/triggers.sql
1225
2381
  \`\`\`
1226
2382
  `;
1227
2383
  }
2384
+ function generateContext(data, config, endpoints) {
2385
+ const app = data.app || {};
2386
+ const tables = data.tables || [];
2387
+ const agents = data.agents || [];
2388
+ const rules = data.rules || [];
2389
+ const exportDate = new Date().toISOString().split('T')[0];
2390
+ // Generate tables section
2391
+ const tablesSection = tables.map((t) => {
2392
+ const fields = (t.fields || []).map((f) => f.slug).join(', ');
2393
+ return `| ${t.name} | ${t.slug} | ${fields || '-'} |`;
2394
+ }).join('\n');
2395
+ // Generate relationships section
2396
+ const relationships = [];
2397
+ for (const table of tables) {
2398
+ for (const field of table.fields || []) {
2399
+ if (field.type === 'reference' && field.reference_table) {
2400
+ relationships.push(`- \`${table.slug}.${field.slug}\` → \`${field.reference_table}.id\``);
2401
+ }
2402
+ }
2403
+ }
2404
+ // Generate field details per table
2405
+ const fieldDetails = tables.map((t) => {
2406
+ const fieldsTable = (t.fields || []).map((f) => {
2407
+ const required = f.required ? '✓' : '';
2408
+ const options = f.options?.length ? f.options.join(', ') : '-';
2409
+ return `| ${f.slug} | ${f.label || f.slug} | ${f.type} | ${required} | ${options} |`;
2410
+ }).join('\n');
2411
+ return `### ${t.name} (\`${t.slug}\`)
2412
+
2413
+ | Field | Label | Type | Required | Options |
2414
+ |-------|-------|------|----------|---------|
2415
+ ${fieldsTable || '| - | - | - | - | - |'}`;
2416
+ }).join('\n\n');
2417
+ // Generate business rules section
2418
+ const rulesByTable = {};
2419
+ for (const rule of rules) {
2420
+ const table = rule.table || rule.table_slug || '*';
2421
+ if (!rulesByTable[table])
2422
+ rulesByTable[table] = [];
2423
+ rulesByTable[table].push(rule);
2424
+ }
2425
+ const rulesSection = Object.entries(rulesByTable).map(([table, tableRules]) => {
2426
+ const rulesList = tableRules.map((r) => {
2427
+ const trigger = r.trigger || 'unknown';
2428
+ const actions = (r.actions || []).map((a) => a.type).join(', ');
2429
+ return `| ${r.name} | ${trigger} | ${r.condition || 'true'} | ${actions} |`;
2430
+ }).join('\n');
2431
+ return `#### ${table === '*' ? 'All Tables' : table}
2432
+
2433
+ | Rule | Trigger | Condition | Actions |
2434
+ |------|---------|-----------|---------|
2435
+ ${rulesList}`;
2436
+ }).join('\n\n');
2437
+ // Generate agents section
2438
+ const agentsSection = agents.map((a) => {
2439
+ const accessType = a.auth_type || 'authenticated';
2440
+ const permissions = Object.entries(a.permissions || {}).map(([table, perms]) => {
2441
+ const p = [];
2442
+ if (perms.read)
2443
+ p.push('R');
2444
+ if (perms.create)
2445
+ p.push('C');
2446
+ if (perms.update)
2447
+ p.push('U');
2448
+ if (perms.delete)
2449
+ p.push('D');
2450
+ return `${table}:${p.join('')}`;
2451
+ }).join(', ');
2452
+ return `| ${a.name} | ${a.slug} | ${a.audience || '-'} | ${accessType} | ${permissions || 'all'} |`;
2453
+ }).join('\n');
2454
+ // Generate action chips section
2455
+ const actionChipsSection = agents.map((a) => {
2456
+ if (!a.action_chips?.length)
2457
+ return '';
2458
+ const chips = a.action_chips.map((c) => {
2459
+ return ` - **${c.label}**: ${c.action} on \`${c.table || '-'}\` ${c.filter ? `(filter: ${c.filter})` : ''}`;
2460
+ }).join('\n');
2461
+ return `#### ${a.name}\n${chips}`;
2462
+ }).filter(Boolean).join('\n\n');
2463
+ // Generate API endpoints section
2464
+ const apiEndpoints = tables.map((t) => {
2465
+ const path = endpoints[t.slug]?.path || `/${t.slug}`;
2466
+ return `| ${t.name} | \`GET ${config.baseUrl}${path}\` | \`POST ${config.baseUrl}${path}\` | \`PUT ${config.baseUrl}${path}/:id\` | \`DELETE ${config.baseUrl}${path}/:id\` |`;
2467
+ }).join('\n');
2468
+ // Generate env vars section
2469
+ const envVars = [
2470
+ { name: 'DATABASE_URL', required: true, desc: 'PostgreSQL connection string' },
2471
+ { name: 'JWT_SECRET', required: true, desc: 'Secret key for JWT tokens' },
2472
+ { name: 'PORT', required: false, desc: 'Server port (default: 3000)' }
2473
+ ];
2474
+ if (config.authOption === 'thinksoft') {
2475
+ envVars.push({ name: 'THINKSOFT_APP_ID', required: false, desc: 'App ID for ThinkSoft email proxy' });
2476
+ envVars.push({ name: 'APP_NAME', required: false, desc: 'App name shown in emails' });
2477
+ }
2478
+ else if (config.authOption === 'self-hosted') {
2479
+ envVars.push({ name: 'RESEND_API_KEY', required: true, desc: 'Resend API key for emails' });
2480
+ envVars.push({ name: 'EMAIL_FROM', required: true, desc: 'Sender email address' });
2481
+ }
2482
+ if (config.chatOption === 'openai') {
2483
+ envVars.push({ name: 'OPENAI_API_KEY', required: true, desc: 'OpenAI API key for chat' });
2484
+ }
2485
+ const envVarsSection = envVars.map(v => `| \`${v.name}\` | ${v.required ? 'Yes' : 'No'} | ${v.desc} |`).join('\n');
2486
+ return `# App Context: ${app.name || 'ThinkSoft App'}
2487
+
2488
+ > Auto-generated context documentation by ThinkSoft CLI
2489
+ > This file helps developers and AI assistants understand the application
2490
+
2491
+ ## Overview
2492
+
2493
+ | Property | Value |
2494
+ |----------|-------|
2495
+ | **App Name** | ${app.name || 'Unknown'} |
2496
+ | **App ID** | ${app.id || 'Unknown'} |
2497
+ | **Description** | ${app.description || '-'} |
2498
+ | **Original Platform** | ThinkSoft |
2499
+ | **Export Date** | ${exportDate} |
2500
+ | **Tables** | ${tables.length} |
2501
+ | **Agents** | ${agents.length} |
2502
+ | **Business Rules** | ${rules.length} |
2503
+
2504
+ ---
2505
+
2506
+ ## Data Model
2507
+
2508
+ ### Tables Overview
2509
+
2510
+ | Name | Slug | Key Fields |
2511
+ |------|------|------------|
2512
+ ${tablesSection || '| - | - | - |'}
2513
+
2514
+ ### Relationships
2515
+
2516
+ ${relationships.length > 0 ? relationships.join('\n') : '_No relationships defined_'}
2517
+
2518
+ ### Field Definitions
2519
+
2520
+ ${fieldDetails || '_No tables defined_'}
2521
+
2522
+ ---
2523
+
2524
+ ## Business Rules
2525
+
2526
+ Business rules are converted to application hooks in \`src/hooks/\`.
2527
+
2528
+ ${rulesSection || '_No business rules defined_'}
2529
+
2530
+ ### Rule Triggers
2531
+
2532
+ | Trigger | When it fires |
2533
+ |---------|---------------|
2534
+ | \`before_create\` | Before inserting a new record |
2535
+ | \`after_create\` | After a record is created |
2536
+ | \`before_update\` | Before updating a record |
2537
+ | \`after_update\` | After a record is updated |
2538
+ | \`before_delete\` | Before deleting a record |
2539
+ | \`after_delete\` | After a record is deleted |
2540
+
2541
+ ### Action Types
2542
+
2543
+ | Action | Description |
2544
+ |--------|-------------|
2545
+ | \`set_field\` | Set a field to a value |
2546
+ | \`calculate\` | Calculate a field from formula |
2547
+ | \`validate\` | Validate condition, reject if false |
2548
+ | \`reject\` | Reject the operation with message |
2549
+ | \`lookup_and_set\` | Query another table and set field |
2550
+ | \`create_record\` | Create a record in another table |
2551
+
2552
+ ---
2553
+
2554
+ ## Service Agents
2555
+
2556
+ Agents define access control and user interfaces.
2557
+
2558
+ | Name | Slug | Audience | Access Type | Permissions |
2559
+ |------|------|----------|-------------|-------------|
2560
+ ${agentsSection || '| - | - | - | - | - |'}
2561
+
2562
+ ### Access Types
2563
+
2564
+ | Type | Description |
2565
+ |------|-------------|
2566
+ | \`public\` | Anyone can access (no login required) |
2567
+ | \`authenticated\` | Must be logged in (exist in \`app_users\`) |
2568
+ | \`table-based\` | Must be logged in AND exist in specific role table |
2569
+
2570
+ ### Action Chips
2571
+
2572
+ ${actionChipsSection || '_No action chips defined_'}
2573
+
2574
+ ---
2575
+
2576
+ ## API Reference
2577
+
2578
+ Base URL: \`http://localhost:3000${config.baseUrl}\`
2579
+
2580
+ ### Authentication
2581
+
2582
+ | Endpoint | Method | Description |
2583
+ |----------|--------|-------------|
2584
+ | \`/auth/send-otp\` | POST | Send OTP to email |
2585
+ | \`/auth/verify-otp\` | POST | Verify OTP, get tokens |
2586
+ | \`/auth/refresh-token\` | POST | Refresh access token |
2587
+ | \`/auth/me\` | GET | Get current user |
2588
+
2589
+ ### CRUD Endpoints
2590
+
2591
+ | Table | List | Create | Update | Delete |
2592
+ |-------|------|--------|--------|--------|
2593
+ ${apiEndpoints || '| - | - | - | - | - |'}
2594
+
2595
+ ### Agent Endpoints
2596
+
2597
+ | Endpoint | Description |
2598
+ |----------|-------------|
2599
+ | \`/agent/:slug\` | Get agent info and action chips |
2600
+ | \`/agent/:slug/action\` | Execute action chip |
2601
+ | \`/agent/:slug/:table\` | Agent-scoped CRUD |
2602
+
2603
+ ---
2604
+
2605
+ ## Environment Variables
2606
+
2607
+ | Variable | Required | Description |
2608
+ |----------|----------|-------------|
2609
+ ${envVarsSection}
2610
+
2611
+ ---
2612
+
2613
+ ## File Structure
2614
+
2615
+ \`\`\`
2616
+ ├── src/
2617
+ │ ├── index.js # Server entry point
2618
+ │ ├── db.js # Database connection
2619
+ │ ├── controllers/ # CRUD logic per table
2620
+ │ ├── routes/ # API endpoints
2621
+ │ │ ├── index.js # Table routes
2622
+ │ │ ├── agents.js # Agent routes
2623
+ │ │ └── auth.js # Auth routes
2624
+ │ ├── middleware/
2625
+ │ │ ├── auth.js # JWT authentication
2626
+ │ │ └── agentAuth.js # Agent access control
2627
+ │ ├── validators/ # Request validation (Joi)
2628
+ │ ├── hooks/ # Business rules as code
2629
+ │ └── events/ # Event bus & notifications
2630
+ ├── agents/
2631
+ │ ├── config.json # Agent configurations
2632
+ │ └── actions.js # Action chip handlers
2633
+ ├── client/
2634
+ │ └── api.js # Frontend SDK (ThinkSoft compatible)
2635
+ ├── database/
2636
+ │ ├── schema.sql # PostgreSQL schema
2637
+ │ └── triggers.sql # Database triggers
2638
+ ├── docker-compose.yml # Docker setup
2639
+ ├── Dockerfile
2640
+ ├── package.json
2641
+ ├── .env.example
2642
+ └── README.md
2643
+ \`\`\`
2644
+
2645
+ ---
2646
+
2647
+ ## Migration Notes
2648
+
2649
+ ### From ThinkSoft Hosted to Self-Hosted
2650
+
2651
+ 1. **Export Data**: Use \`thinksoft export-data ${app.id || 'APP_ID'}\` to export records
2652
+ 2. **Update Frontend**: Change SDK import or set \`baseUrl\` to self-hosted server
2653
+ 3. **DNS**: Point your domain to the self-hosted server
2654
+ 4. **Email**: Configure email provider or use ThinkSoft email proxy
2655
+
2656
+ ### SDK Compatibility
2657
+
2658
+ The generated \`client/api.js\` is compatible with \`@thinksoftai/sdk\`:
2659
+
2660
+ \`\`\`javascript
2661
+ // ThinkSoft hosted
2662
+ import { ThinkSoft } from '@thinksoftai/sdk';
2663
+ const client = new ThinkSoft({ appId: '${app.id || 'APP_ID'}' });
2664
+
2665
+ // Self-hosted (same code, different import)
2666
+ import { ThinkSoft } from './client/api';
2667
+ const client = new ThinkSoft({ appId: '${app.id || 'APP_ID'}' });
2668
+
2669
+ // Same API works
2670
+ await client.auth.sendOtp('user@example.com');
2671
+ const orders = await client.from('orders').list();
2672
+ \`\`\`
2673
+
2674
+ ---
2675
+
2676
+ _Generated by ThinkSoft CLI v1.6.x_
2677
+ `;
2678
+ }
1228
2679
  //# sourceMappingURL=export.js.map