@arela/uploader 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.claude/worktrees/agent-multi-profile/.env.template +224 -0
  2. package/.claude/worktrees/agent-multi-profile/.prettierrc +13 -0
  3. package/.claude/worktrees/agent-multi-profile/README.md +405 -0
  4. package/.claude/worktrees/agent-multi-profile/package-lock.json +7096 -0
  5. package/.claude/worktrees/agent-multi-profile/package.json +78 -0
  6. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-ds-store.js +109 -0
  7. package/.claude/worktrees/agent-multi-profile/scripts/cleanup-system-files.js +69 -0
  8. package/.claude/worktrees/agent-multi-profile/scripts/scoring-compare.js +243 -0
  9. package/.claude/worktrees/agent-multi-profile/scripts/scoring-phase4-check.js +96 -0
  10. package/.claude/worktrees/agent-multi-profile/scripts/tests/phase-7-features.test.js +415 -0
  11. package/.claude/worktrees/agent-multi-profile/scripts/tests/signal-handling.test.js +275 -0
  12. package/.claude/worktrees/agent-multi-profile/scripts/tests/smart-watch-integration.test.js +554 -0
  13. package/.claude/worktrees/agent-multi-profile/scripts/tests/watch-service-integration.test.js +584 -0
  14. package/.claude/worktrees/agent-multi-profile/src/commands/AgentCommand.js +229 -0
  15. package/.claude/worktrees/agent-multi-profile/src/commands/AgentInitCommand.js +316 -0
  16. package/.claude/worktrees/agent-multi-profile/src/commands/DatastageCommand.js +164 -0
  17. package/.claude/worktrees/agent-multi-profile/src/commands/GDriveSyncCommand.js +475 -0
  18. package/.claude/worktrees/agent-multi-profile/src/commands/IdentifyCommand.js +708 -0
  19. package/.claude/worktrees/agent-multi-profile/src/commands/PollWorkerCommand.js +169 -0
  20. package/.claude/worktrees/agent-multi-profile/src/commands/PropagateCommand.js +636 -0
  21. package/.claude/worktrees/agent-multi-profile/src/commands/PushCommand.js +743 -0
  22. package/.claude/worktrees/agent-multi-profile/src/commands/ScanCommand.js +722 -0
  23. package/.claude/worktrees/agent-multi-profile/src/commands/UploadCommand.js +587 -0
  24. package/.claude/worktrees/agent-multi-profile/src/commands/WatchCommand.js +1342 -0
  25. package/.claude/worktrees/agent-multi-profile/src/commands/WorkerCommand.js +337 -0
  26. package/.claude/worktrees/agent-multi-profile/src/config/config.js +862 -0
  27. package/.claude/worktrees/agent-multi-profile/src/document-type-shared.js +131 -0
  28. package/.claude/worktrees/agent-multi-profile/src/document-types/_pedimento-shared-extractors.js +348 -0
  29. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-pdf.js +121 -0
  30. package/.claude/worktrees/agent-multi-profile/src/document-types/doda-xml.js +118 -0
  31. package/.claude/worktrees/agent-multi-profile/src/document-types/factura-inter-agencia.js +186 -0
  32. package/.claude/worktrees/agent-multi-profile/src/document-types/facturas-comerciales.js +233 -0
  33. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo-xml.js +372 -0
  34. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-completo.js +108 -0
  35. package/.claude/worktrees/agent-multi-profile/src/document-types/pedimento-simplificado.js +76 -0
  36. package/.claude/worktrees/agent-multi-profile/src/document-types/proforma.js +29 -0
  37. package/.claude/worktrees/agent-multi-profile/src/document-types/support-document.js +200 -0
  38. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorHandler.js +278 -0
  39. package/.claude/worktrees/agent-multi-profile/src/errors/ErrorTypes.js +104 -0
  40. package/.claude/worktrees/agent-multi-profile/src/file-detection.js +338 -0
  41. package/.claude/worktrees/agent-multi-profile/src/index.js +890 -0
  42. package/.claude/worktrees/agent-multi-profile/src/scoring/db-matcher-adapter.js +98 -0
  43. package/.claude/worktrees/agent-multi-profile/src/scoring/matchers-seed.js +386 -0
  44. package/.claude/worktrees/agent-multi-profile/src/scoring/scoring-engine.js +251 -0
  45. package/.claude/worktrees/agent-multi-profile/src/services/AdvancedFilterService.js +505 -0
  46. package/.claude/worktrees/agent-multi-profile/src/services/AutoProcessingService.js +749 -0
  47. package/.claude/worktrees/agent-multi-profile/src/services/BenchmarkingService.js +381 -0
  48. package/.claude/worktrees/agent-multi-profile/src/services/DatabaseService.js +2173 -0
  49. package/.claude/worktrees/agent-multi-profile/src/services/DatastageApiService.js +240 -0
  50. package/.claude/worktrees/agent-multi-profile/src/services/ErrorMonitor.js +275 -0
  51. package/.claude/worktrees/agent-multi-profile/src/services/GoogleDriveService.js +217 -0
  52. package/.claude/worktrees/agent-multi-profile/src/services/LoggingService.js +649 -0
  53. package/.claude/worktrees/agent-multi-profile/src/services/MonitoringService.js +401 -0
  54. package/.claude/worktrees/agent-multi-profile/src/services/PerformanceOptimizer.js +511 -0
  55. package/.claude/worktrees/agent-multi-profile/src/services/PipelineApiService.js +274 -0
  56. package/.claude/worktrees/agent-multi-profile/src/services/PipelineJobRunner.js +389 -0
  57. package/.claude/worktrees/agent-multi-profile/src/services/ProfileManager.js +164 -0
  58. package/.claude/worktrees/agent-multi-profile/src/services/ReportingService.js +511 -0
  59. package/.claude/worktrees/agent-multi-profile/src/services/ScanApiService.js +775 -0
  60. package/.claude/worktrees/agent-multi-profile/src/services/SignalHandler.js +255 -0
  61. package/.claude/worktrees/agent-multi-profile/src/services/SmartWatchDatabaseService.js +527 -0
  62. package/.claude/worktrees/agent-multi-profile/src/services/WatchService.js +783 -0
  63. package/.claude/worktrees/agent-multi-profile/src/services/upload/ApiUploadService.js +676 -0
  64. package/.claude/worktrees/agent-multi-profile/src/services/upload/BaseUploadService.js +36 -0
  65. package/.claude/worktrees/agent-multi-profile/src/services/upload/MultiApiUploadService.js +233 -0
  66. package/.claude/worktrees/agent-multi-profile/src/services/upload/SupabaseUploadService.js +148 -0
  67. package/.claude/worktrees/agent-multi-profile/src/services/upload/UploadServiceFactory.js +100 -0
  68. package/.claude/worktrees/agent-multi-profile/src/utils/CleanupManager.js +262 -0
  69. package/.claude/worktrees/agent-multi-profile/src/utils/FileOperations.js +192 -0
  70. package/.claude/worktrees/agent-multi-profile/src/utils/FileSanitizer.js +99 -0
  71. package/.claude/worktrees/agent-multi-profile/src/utils/PathDetector.js +198 -0
  72. package/.claude/worktrees/agent-multi-profile/src/utils/PathNormalizer.js +274 -0
  73. package/.claude/worktrees/agent-multi-profile/src/utils/WatchEventHandler.js +522 -0
  74. package/.claude/worktrees/agent-multi-profile/supabase/migrations/001_create_initial_schema.sql +366 -0
  75. package/.claude/worktrees/agent-multi-profile/supabase/migrations/002_align_with_arela_api_schema.sql +145 -0
  76. package/.claude/worktrees/agent-multi-profile/tests/commands/IdentifyCommand.test.js +570 -0
  77. package/.claude/worktrees/agent-multi-profile/tests/commands/PropagateCommand.test.js +568 -0
  78. package/.claude/worktrees/agent-multi-profile/tests/commands/PushCommand.test.js +754 -0
  79. package/.claude/worktrees/agent-multi-profile/tests/commands/ScanCommand.test.js +382 -0
  80. package/.claude/worktrees/agent-multi-profile/tests/unit/PathAndTableNameGeneration.test.js +1211 -0
  81. package/.claude/worktrees/agent-multi-profile/tests/unit/factura-inter-agencia.test.js +218 -0
  82. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-completo-xml-matcher.test.js +271 -0
  83. package/.claude/worktrees/agent-multi-profile/tests/unit/pedimento-simplificado-matcher.test.js +185 -0
  84. package/.claude/worktrees/agent-multi-profile/tests/unit/scoring-engine.test.js +221 -0
  85. package/README.md +65 -0
  86. package/package.json +1 -1
  87. package/src/commands/AgentCommand.js +210 -0
  88. package/src/commands/AgentInitCommand.js +316 -0
  89. package/src/commands/PollWorkerCommand.js +11 -322
  90. package/src/config/config.js +44 -6
  91. package/src/document-type-shared.js +1 -1
  92. package/src/document-types/pedimento-completo.js +8 -1
  93. package/src/file-detection.js +9 -0
  94. package/src/index.js +55 -0
  95. package/src/scoring/scoring-engine.js +1 -1
  96. package/src/services/LoggingService.js +35 -0
  97. package/src/services/PipelineApiService.js +7 -1
  98. package/src/services/PipelineJobRunner.js +389 -0
  99. package/src/services/ProfileManager.js +164 -0
@@ -37,10 +37,10 @@ class Config {
37
37
  const __dirname = path.dirname(__filename);
38
38
  const packageJsonPath = path.resolve(__dirname, '../../package.json');
39
39
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
40
- return packageJson.version || '1.1.3';
40
+ return packageJson.version || '1.2.0';
41
41
  } catch (error) {
42
42
  console.warn('⚠️ Could not read package.json version, using fallback');
43
- return '1.1.3';
43
+ return '1.2.0';
44
44
  }
45
45
  }
46
46
 
@@ -126,7 +126,7 @@ class Config {
126
126
  * @param {string} target - API target: 'default', 'agencia', or 'cliente'
127
127
  */
128
128
  setApiTarget(target) {
129
- const validTargets = ['default', 'agencia', 'cliente', 'ktj'];
129
+ const validTargets = ['default', ...Object.keys(this.api.targets)];
130
130
  if (!validTargets.includes(target.toLowerCase())) {
131
131
  throw new Error(
132
132
  `Invalid API target '${target}'. Must be one of: ${validTargets.join(', ')}`,
@@ -143,7 +143,7 @@ class Config {
143
143
  * @param {string} targetTarget - Target API target (for writing data)
144
144
  */
145
145
  setCrossTenantTargets(sourceTarget, targetTarget) {
146
- const validTargets = ['default', 'agencia', 'cliente', 'ktj'];
146
+ const validTargets = ['default', ...Object.keys(this.api.targets)];
147
147
 
148
148
  if (!validTargets.includes(sourceTarget.toLowerCase())) {
149
149
  throw new Error(
@@ -171,6 +171,41 @@ class Config {
171
171
  );
172
172
  }
173
173
 
174
+ /**
175
+ * Upsert an API target in memory (does not touch process.env).
176
+ * Used by agent mode to register per-profile targets at runtime.
177
+ * @param {string} name - Target name, e.g. 'cliente', 'agencia'
178
+ * @param {{baseUrl: string, token: string}} targetConfig
179
+ */
180
+ setTargetConfig(name, { baseUrl, token }) {
181
+ this.api.targets[name.toLowerCase()] = { baseUrl, token };
182
+ }
183
+
184
+ /**
185
+ * Apply an agent profile: API targets, server id and env overrides.
186
+ * Idempotent — the agent calls it before every poll/job of the profile so the
187
+ * singleton always reflects the ACTIVE profile (jobs reference targets by
188
+ * symbolic name: 'agencia', 'cliente').
189
+ * @param {Object} profile - Normalized profile from ProfileManager
190
+ */
191
+ applyProfile(profile) {
192
+ this.api.baseUrl = profile.url;
193
+ this.api.token = profile.token;
194
+ for (const [name, targetConfig] of Object.entries(profile.targets)) {
195
+ this.setTargetConfig(name, targetConfig);
196
+ }
197
+ if (profile.serverId) {
198
+ process.env.ARELA_SERVER_ID = profile.serverId;
199
+ }
200
+ for (const [key, value] of Object.entries(profile.env || {})) {
201
+ process.env[key] = String(value);
202
+ }
203
+ // Clear cross-tenant state left over from a previous profile's job
204
+ this.api.sourceTarget = null;
205
+ this.api.targetTarget = null;
206
+ this.api.activeTarget = 'default';
207
+ }
208
+
174
209
  /**
175
210
  * Get source API config for cross-tenant operations
176
211
  * @returns {Object} Source API configuration
@@ -780,12 +815,15 @@ class Config {
780
815
  }
781
816
 
782
817
  /**
783
- * Reload upload and scan config from current process.env values.
784
- * Must be called after modifying env vars at runtime (e.g., PollWorkerCommand).
818
+ * Reload upload, scan, push and performance config from current process.env
819
+ * values. Must be called after modifying env vars at runtime (e.g.,
820
+ * PipelineJobRunner applying a job's scanConfig/tuning overrides).
785
821
  */
786
822
  reloadScanConfig() {
787
823
  this.upload = this.#loadUploadConfig();
788
824
  this.scan = this.#loadScanConfig();
825
+ this.push = this.#loadPushConfig();
826
+ this.performance = this.#loadPerformanceConfig();
789
827
  }
790
828
 
791
829
  /**
@@ -100,7 +100,7 @@ export function extractDocumentFields(source, fileExtension, filePath) {
100
100
 
101
101
  // Resolve final type if the definition supports it (e.g., pedimento_simplificado vs proforma)
102
102
  const resolvedType = docType.resolveType
103
- ? docType.resolveType(fields)
103
+ ? docType.resolveType(fields, filePath)
104
104
  : docType.type;
105
105
 
106
106
  console.log(` → Resolved type: ${resolvedType}`);
@@ -74,8 +74,15 @@ export const pedimentoCompletoDefinition = {
74
74
  * - R1 rectifications require fechaPagoRectificacion
75
75
  * - Everything else requires paymentDate
76
76
  * No payment evidence ⇒ proforma_completo.
77
+ *
78
+ * Exception: the "CoveFact" annex (…-CoveFact.pdf) doesn't always print the
79
+ * payment section, so the payment-based demotion misfires and the file gets
80
+ * stuck as proforma_completo (NON_PUSHABLE) forever — the CLI never
81
+ * re-identifies rows that already have a detected_type. The filename is the
82
+ * payment-independent signal for this variant.
77
83
  */
78
- resolveType: (fields) => {
84
+ resolveType: (fields, filePath) => {
85
+ if (/covefact/i.test(filePath ?? '')) return 'pedimento_completo';
79
86
  const clavePedimento =
80
87
  fields?.find((f) => f.name === 'clavePedimento')?.value ?? null;
81
88
  const paymentDate =
@@ -56,6 +56,15 @@ function composeArelaPath(
56
56
  return null;
57
57
  }
58
58
 
59
+ // The CoveFact annex resolves to pedimento_completo without payment
60
+ // evidence (see pedimento-completo.js resolveType), so it must never anchor
61
+ // an operation on its own: with no self-composed path it only becomes
62
+ // pushable when a PAID sibling pedimento propagates the folder's
63
+ // arela_path. Unpaid operations therefore never reach Arela through it.
64
+ if (/covefact/i.test(filePath ?? '')) {
65
+ return null;
66
+ }
67
+
59
68
  const rfc = fields?.find((f) => f.name === 'rfc')?.value;
60
69
  let patente = fields?.find((f) => f.name === 'patente')?.value;
61
70
  const aduana = fields?.find((f) => f.name === 'aduanaEntradaSalida')?.value;
package/src/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
 
4
+ import agentCommand from './commands/AgentCommand.js';
5
+ import agentInitCommand from './commands/AgentInitCommand.js';
4
6
  import datastageCommand from './commands/DatastageCommand.js';
5
7
  import gdriveSyncCommand from './commands/GDriveSyncCommand.js';
6
8
  import identifyCommand from './commands/IdentifyCommand.js';
@@ -646,6 +648,59 @@ class ArelaUploaderCLI {
646
648
  }
647
649
  });
648
650
 
651
+ // ============================================================================
652
+ // AGENT MODE - one process polling N tenant APIs (replaces N worker terminals)
653
+ // ============================================================================
654
+
655
+ const agent = this.program
656
+ .command('agent')
657
+ .description(
658
+ '🤖 Multi-profile polling agent: one process for all tenant APIs (profiles.json)',
659
+ );
660
+
661
+ agent
662
+ .command('run', { isDefault: true })
663
+ .description('Run the agent (default: `arela agent` runs this)')
664
+ .option(
665
+ '--config <path>',
666
+ 'Path to profiles.json (default: ~/.arela/profiles.json)',
667
+ )
668
+ .option('--interval <ms>', 'Base poll interval per idle round')
669
+ .option(
670
+ '--profile <names>',
671
+ 'Comma-separated subset of profiles to run (debugging)',
672
+ )
673
+ .action(async (options) => {
674
+ try {
675
+ await agentCommand.execute(options);
676
+ } catch (error) {
677
+ this.errorHandler.handleFatalError(error, { command: 'agent' });
678
+ }
679
+ });
680
+
681
+ agent
682
+ .command('init')
683
+ .description(
684
+ 'Generate profiles.json + pipeline_config seeds from legacy per-folder .env files',
685
+ )
686
+ .requiredOption(
687
+ '--from <dirs...>',
688
+ 'Folders containing the legacy .env files (one per RFC×source)',
689
+ )
690
+ .option(
691
+ '--server-id <id>',
692
+ 'Machine server id (default: ARELA_SERVER_ID from first .env, else hostname)',
693
+ )
694
+ .option('--out <dir>', 'Output directory', '~/.arela')
695
+ .option('--force', 'Overwrite an existing profiles.json')
696
+ .action(async (options) => {
697
+ try {
698
+ await agentInitCommand.execute(options.from, options);
699
+ } catch (error) {
700
+ this.errorHandler.handleFatalError(error, { command: 'agent init' });
701
+ }
702
+ });
703
+
649
704
  // Version command (already handled by program.version())
650
705
 
651
706
  // Help command
@@ -225,7 +225,7 @@ export function classifyDocument(matchers, { source, extension, filePath }) {
225
225
  }
226
226
 
227
227
  const resolvedType = def.resolveType
228
- ? def.resolveType(fields)
228
+ ? def.resolveType(fields, filePath)
229
229
  : def.documentType;
230
230
  const pedimento = def.extractNumPedimento
231
231
  ? def.extractNumPedimento(source, fields, filePath)
@@ -17,6 +17,7 @@ export class LoggingService {
17
17
  this.bufferSize = appConfig.performance.logBufferSize;
18
18
  this.flushInterval = appConfig.performance.logFlushInterval;
19
19
  this.lastFlushTime = Date.now();
20
+ this.context = null;
20
21
 
21
22
  // Event tracking (new in Fase 5)
22
23
  this.uploadEvents = [];
@@ -50,6 +51,15 @@ export class LoggingService {
50
51
  });
51
52
  }
52
53
 
54
+ /**
55
+ * Prefix a message with the active context (agent profile name), so both the
56
+ * console echo and the file entry stay grep-able per profile
57
+ * @private
58
+ */
59
+ #withContext(message) {
60
+ return this.context ? `[${this.context}] ${message}` : message;
61
+ }
62
+
53
63
  /**
54
64
  * Write a log message
55
65
  * @param {string} message - Log message
@@ -80,6 +90,7 @@ export class LoggingService {
80
90
  * @param {string} message - Message to log
81
91
  */
82
92
  info(message) {
93
+ message = this.#withContext(message);
83
94
  this.writeLog(message, 'info');
84
95
  // Echo to console: run banners, stats and the 🧩 observability lines were
85
96
  // file-only, so operators watching the terminal never saw them (the v1
@@ -92,6 +103,7 @@ export class LoggingService {
92
103
  * @param {string} message - Message to log
93
104
  */
94
105
  warn(message) {
106
+ message = this.#withContext(message);
95
107
  this.writeLog(message, 'warn');
96
108
  console.warn(`⚠️ ${message}`);
97
109
  }
@@ -101,6 +113,7 @@ export class LoggingService {
101
113
  * @param {string} message - Message to log
102
114
  */
103
115
  error(message) {
116
+ message = this.#withContext(message);
104
117
  this.writeLog(message, 'error');
105
118
  console.error(`❌ ${message}`);
106
119
  }
@@ -111,6 +124,7 @@ export class LoggingService {
111
124
  */
112
125
  debug(message) {
113
126
  if (this.isVerbose) {
127
+ message = this.#withContext(message);
114
128
  this.writeLog(message, 'debug');
115
129
  console.log(`🔍 ${message}`);
116
130
  }
@@ -122,6 +136,7 @@ export class LoggingService {
122
136
  */
123
137
  verbose(message) {
124
138
  if (this.isVerbose) {
139
+ message = this.#withContext(message);
125
140
  this.writeLog(message, 'verbose');
126
141
  console.log(message);
127
142
  }
@@ -132,6 +147,7 @@ export class LoggingService {
132
147
  * @param {string} message - Message to log
133
148
  */
134
149
  success(message) {
150
+ message = this.#withContext(message);
135
151
  this.writeLog(message, 'success');
136
152
  console.log(`✅ ${message}`);
137
153
  }
@@ -173,6 +189,25 @@ export class LoggingService {
173
189
  return this.logFilePath;
174
190
  }
175
191
 
192
+ /**
193
+ * Redirect logging to a different file (agent mode uses ~/.arela/arela-agent.log
194
+ * instead of the cwd-bound arela-upload.log). Flushes pending entries first.
195
+ * @param {string} filePath - Absolute path to the new log file
196
+ */
197
+ setLogFilePath(filePath) {
198
+ this.flush();
199
+ this.logFilePath = filePath;
200
+ }
201
+
202
+ /**
203
+ * Set a context prefix added to every file log entry (e.g. the active agent
204
+ * profile name, so one shared log stays grep-able per profile).
205
+ * @param {string|null} prefix - Context name, or null to clear
206
+ */
207
+ setContext(prefix) {
208
+ this.context = prefix || null;
209
+ }
210
+
176
211
  /**
177
212
  * Check if verbose logging is enabled
178
213
  * @returns {boolean} True if verbose logging is enabled
@@ -146,9 +146,12 @@ export class PipelineApiService {
146
146
  /**
147
147
  * Get the next available job for this server
148
148
  * @param {string} serverId - Server identifier
149
+ * @param {Object} [options]
150
+ * @param {boolean} [options.throwOnError=false] - Rethrow request failures
151
+ * instead of swallowing them (agent mode needs this to back off a dead API)
149
152
  * @returns {Promise<Object|null>} Job data or null if no jobs available
150
153
  */
151
- async getNextJob(serverId) {
154
+ async getNextJob(serverId, { throwOnError = false } = {}) {
152
155
  try {
153
156
  const result = await this.#request(
154
157
  'GET',
@@ -156,6 +159,9 @@ export class PipelineApiService {
156
159
  );
157
160
  return result;
158
161
  } catch (error) {
162
+ if (throwOnError) {
163
+ throw error;
164
+ }
159
165
  logger.error(`❌ Failed to get next job: ${error.message}`);
160
166
  return null;
161
167
  }