aparavi-client 1.0.9 → 1.0.10

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.
@@ -329,11 +329,6 @@ class AparaviClient extends DAPClient_1.DAPClient {
329
329
  static _convertToWebSocketUri(uri) {
330
330
  try {
331
331
  const url = new URL(uri);
332
- // If already a WebSocket URI, preserve it
333
- if (url.protocol === 'wss:' || url.protocol === 'ws:') {
334
- return `${url.protocol}//${url.host}`;
335
- }
336
- // Convert HTTP/HTTPS to WS/WSS
337
332
  const wsScheme = url.protocol === 'https:' ? 'wss:' : 'ws:';
338
333
  return `${wsScheme}//${url.host}`;
339
334
  }
@@ -490,6 +485,7 @@ class AparaviClient extends DAPClient_1.DAPClient {
490
485
  * @param options.threads - Number of threads for execution (default: 1)
491
486
  * @param options.useExisting - Use existing pipeline instance
492
487
  * @param options.args - Command line arguments to pass to pipeline
488
+ * @param options.ttl - Time-to-live in seconds for idle pipelines (optional, server default if not provided; use 0 for no timeout)
493
489
  *
494
490
  * @returns Promise resolving to an object containing the task token and other metadata
495
491
  * @throws Error if neither pipeline nor filepath is provided
@@ -510,7 +506,7 @@ class AparaviClient extends DAPClient_1.DAPClient {
510
506
  * ```
511
507
  */
512
508
  async use(options = {}) {
513
- const { token, filepath, pipeline, source, threads, useExisting, args } = options;
509
+ const { token, filepath, pipeline, source, threads, useExisting, args, ttl } = options;
514
510
  // Validate required parameters
515
511
  if (!pipeline && !filepath) {
516
512
  throw new Error('Pipeline configuration or file path is required and must be specified');
@@ -534,8 +530,6 @@ class AparaviClient extends DAPClient_1.DAPClient {
534
530
  let processedConfig = JSON.parse(JSON.stringify(pipelineConfig));
535
531
  // Perform environment variable substitution on the pipeline configuration
536
532
  processedConfig = this.processEnvSubstitution(processedConfig);
537
- // Sanitize webhook configs for remote servers (remove port parameter)
538
- // processedConfig = this.sanitizePipelineForRemote(processedConfig);
539
533
  // Override source if specified (after substitution)
540
534
  if (source !== undefined) {
541
535
  processedConfig.pipeline.source = source;
@@ -545,6 +539,10 @@ class AparaviClient extends DAPClient_1.DAPClient {
545
539
  pipeline: processedConfig,
546
540
  args: args || [],
547
541
  };
542
+ // Add TTL if provided (server uses its default if not specified)
543
+ if (ttl !== undefined) {
544
+ arguments_.ttl = ttl;
545
+ }
548
546
  // Add optional parameters if specified
549
547
  if (token !== undefined) {
550
548
  arguments_.token = token;
@@ -574,104 +572,6 @@ class AparaviClient extends DAPClient_1.DAPClient {
574
572
  // Type assertion to ensure token is present
575
573
  return responseBody;
576
574
  }
577
- // /**
578
- // * Check if the client is connected to a remote server (not localhost).
579
- // * @returns True if connecting to a remote server, False if localhost
580
- // */
581
- // private isRemoteServer(): boolean {
582
- // try {
583
- // const uri = this._uri || '';
584
- // if (!uri) {
585
- // return true; // Assume remote if URI not available
586
- // }
587
- // // Normalize URI - remove protocol and check hostname
588
- // let uriLower = uri.toLowerCase();
589
- // // Remove ws://, wss://, http://, https://
590
- // uriLower = uriLower.replace(/^(ws|wss|http|https):\/\//, '');
591
- // // Remove port if present
592
- // uriLower = uriLower.replace(/:\d+/, '');
593
- // // Remove path
594
- // uriLower = uriLower.split('/')[0];
595
- // // Check if it's localhost or local
596
- // const localhostPatterns = ['localhost', '127.0.0.1', '::1', '0.0.0.0', 'local'];
597
- // return !localhostPatterns.some(pattern => uriLower.includes(pattern));
598
- // } catch {
599
- // // If we can't determine, assume remote to be safe
600
- // return true;
601
- // }
602
- // }
603
- // /**
604
- // * Remove port parameter from webhook config for remote servers.
605
- // * @param config Webhook component configuration object
606
- // * @returns Sanitized configuration object
607
- // */
608
- // private sanitizeWebhookConfig(config: any): any {
609
- // if (!config || typeof config !== 'object') {
610
- // return config;
611
- // }
612
- // // Check if this is a webhook component
613
- // if (config.type === 'webhook' || String(config.provider || '').toLowerCase().includes('webhook')) {
614
- // // Check parameters
615
- // const parameters = config.parameters;
616
- // if (parameters && typeof parameters === 'object' && 'port' in parameters) {
617
- // // Remove port for remote servers
618
- // if (this.isRemoteServer()) {
619
- // // Create a copy to avoid modifying original
620
- // const sanitized = JSON.parse(JSON.stringify(config));
621
- // const { port, ...restParams } = sanitized.parameters;
622
- // sanitized.parameters = restParams;
623
- // this.debugMessage('Removed port parameter from webhook config for remote server');
624
- // return sanitized;
625
- // }
626
- // }
627
- // }
628
- // return config;
629
- // }
630
- // /**
631
- // * Recursively sanitize pipeline configuration for remote servers.
632
- // * Removes port parameters from webhook components.
633
- // * @param config Pipeline configuration object
634
- // * @returns Sanitized pipeline configuration
635
- // */
636
- // private sanitizePipelineForRemote(config: any): any {
637
- // if (!config || typeof config !== 'object') {
638
- // return config;
639
- // }
640
- // // Create a deep copy to avoid modifying original
641
- // const sanitized = JSON.parse(JSON.stringify(config));
642
- // // Check if this is a pipeline config with components
643
- // if (sanitized.pipeline) {
644
- // const pipeline = sanitized.pipeline;
645
- // if (pipeline.components && Array.isArray(pipeline.components)) {
646
- // // Process each component
647
- // for (let i = 0; i < pipeline.components.length; i++) {
648
- // const component = pipeline.components[i];
649
- // if (component && typeof component === 'object') {
650
- // // Check if this is a webhook component by provider
651
- // const provider = component.provider || '';
652
- // if (provider === 'webhook' && component.config) {
653
- // // Sanitize webhook configs
654
- // pipeline.components[i].config = this.sanitizeWebhookConfig(component.config);
655
- // }
656
- // }
657
- // }
658
- // }
659
- // }
660
- // // Also check direct component arrays (for nested structures)
661
- // if (sanitized.components && Array.isArray(sanitized.components)) {
662
- // for (let i = 0; i < sanitized.components.length; i++) {
663
- // const component = sanitized.components[i];
664
- // if (component && typeof component === 'object') {
665
- // // Check if this is a webhook component by provider
666
- // const provider = component.provider || '';
667
- // if (provider === 'webhook' && component.config) {
668
- // component.config = this.sanitizeWebhookConfig(component.config);
669
- // }
670
- // }
671
- // }
672
- // }
673
- // return sanitized;
674
- // }
675
575
  /**
676
576
  * Terminate a running pipeline.
677
577
  */
@@ -1059,6 +959,638 @@ class AparaviClient extends DAPClient_1.DAPClient {
1059
959
  }
1060
960
  }
1061
961
  // ============================================================================
962
+ // PROJECT STORAGE MANAGEMENT
963
+ // ============================================================================
964
+ /**
965
+ * Save or update a project pipeline.
966
+ *
967
+ * Stores a project pipeline configuration on the server. If the project
968
+ * already exists, it will be updated. Use expectedVersion to ensure
969
+ * you're updating the version you expect (prevents conflicts).
970
+ *
971
+ * @param options - Save project options
972
+ * @param options.projectId - Unique identifier for the project
973
+ * @param options.pipeline - Pipeline configuration object
974
+ * @param options.expectedVersion - Expected current version for atomic updates (optional)
975
+ * @returns Promise resolving to save result with success status, projectId, and new version
976
+ * @throws Error if save fails due to version mismatch, storage error, or invalid input
977
+ *
978
+ * @example
979
+ * ```typescript
980
+ * // Save a new project
981
+ * const result = await client.saveProject({
982
+ * projectId: 'proj-123',
983
+ * pipeline: {
984
+ * name: 'Data Processor',
985
+ * source: 'source_1',
986
+ * components: [...]
987
+ * }
988
+ * });
989
+ * console.log(`Saved version: ${result.version}`);
990
+ *
991
+ * // Update existing project with version check
992
+ * const existing = await client.getProject({ projectId: 'proj-123' });
993
+ * existing.pipeline.name = 'Updated Name';
994
+ * const updated = await client.saveProject({
995
+ * projectId: 'proj-123',
996
+ * pipeline: existing.pipeline,
997
+ * expectedVersion: existing.version
998
+ * });
999
+ * ```
1000
+ */
1001
+ async saveProject(options) {
1002
+ const { projectId, pipeline, expectedVersion } = options;
1003
+ // Validate inputs
1004
+ if (!projectId) {
1005
+ throw new Error('projectId is required');
1006
+ }
1007
+ if (!pipeline || typeof pipeline !== 'object') {
1008
+ throw new Error('pipeline must be a non-empty object');
1009
+ }
1010
+ // Build request arguments
1011
+ const args = {
1012
+ subcommand: 'save_project',
1013
+ projectId,
1014
+ pipeline,
1015
+ };
1016
+ // Add optional version for atomic updates
1017
+ if (expectedVersion !== undefined) {
1018
+ args.expectedVersion = expectedVersion;
1019
+ }
1020
+ // Send request to server
1021
+ const request = this.buildRequest('apaext_store', { arguments: args });
1022
+ const response = await this.request(request);
1023
+ // Check for errors
1024
+ if (this.didFail(response)) {
1025
+ const errorMsg = response.message || 'Unknown error saving project';
1026
+ this.debugMessage(`Project save failed: ${errorMsg}`);
1027
+ throw new Error(errorMsg);
1028
+ }
1029
+ // Extract and return response
1030
+ this.debugMessage(`Project saved successfully: ${projectId}, version: ${response.body?.version}`);
1031
+ return response.body;
1032
+ }
1033
+ /**
1034
+ * Retrieve a project by its ID.
1035
+ *
1036
+ * Fetches the complete pipeline configuration and current version for
1037
+ * the specified project. Use this before updating to get the current
1038
+ * version for atomic updates.
1039
+ *
1040
+ * @param options - Get project options
1041
+ * @param options.projectId - Unique identifier of the project to retrieve
1042
+ * @returns Promise resolving to project data with success status, pipeline, and version
1043
+ * @throws Error if project doesn't exist or retrieval fails
1044
+ *
1045
+ * @example
1046
+ * ```typescript
1047
+ * // Get a project
1048
+ * try {
1049
+ * const project = await client.getProject({ projectId: 'proj-123' });
1050
+ * console.log(`Project: ${project.pipeline.name}`);
1051
+ * console.log(`Version: ${project.version}`);
1052
+ * } catch (error) {
1053
+ * if (error.message.includes('NOT_FOUND')) {
1054
+ * console.log("Project doesn't exist");
1055
+ * }
1056
+ * }
1057
+ *
1058
+ * // Before updating - get current version
1059
+ * const project = await client.getProject({ projectId: 'proj-123' });
1060
+ * project.pipeline.name = 'Updated';
1061
+ * await client.saveProject({
1062
+ * projectId: 'proj-123',
1063
+ * pipeline: project.pipeline,
1064
+ * expectedVersion: project.version
1065
+ * });
1066
+ * ```
1067
+ */
1068
+ async getProject(options) {
1069
+ const { projectId } = options;
1070
+ // Validate inputs
1071
+ if (!projectId) {
1072
+ throw new Error('projectId is required');
1073
+ }
1074
+ // Build request
1075
+ const args = {
1076
+ subcommand: 'get_project',
1077
+ projectId,
1078
+ };
1079
+ // Send request to server
1080
+ const request = this.buildRequest('apaext_store', { arguments: args });
1081
+ const response = await this.request(request);
1082
+ // Check for errors
1083
+ if (this.didFail(response)) {
1084
+ const errorMsg = response.message || 'Unknown error retrieving project';
1085
+ this.debugMessage(`Project retrieval failed: ${errorMsg}`);
1086
+ throw new Error(errorMsg);
1087
+ }
1088
+ // Extract and return response
1089
+ this.debugMessage(`Project retrieved successfully: ${projectId}`);
1090
+ return response.body;
1091
+ }
1092
+ /**
1093
+ * Delete a project by its ID.
1094
+ *
1095
+ * Permanently removes a project from storage. Optionally verify the
1096
+ * version before deletion to ensure you're deleting the version you
1097
+ * expect (prevents accidental deletion of modified projects).
1098
+ *
1099
+ * @param options - Delete project options
1100
+ * @param options.projectId - Unique identifier of the project to delete
1101
+ * @param options.expectedVersion - Expected current version for atomic deletion (required)
1102
+ * @returns Promise resolving to deletion result with success status and message
1103
+ * @throws Error if project doesn't exist, version mismatch, or deletion fails
1104
+ *
1105
+ * @example
1106
+ * ```typescript
1107
+ * // Safe deletion with version check
1108
+ * const project = await client.getProject({ projectId: 'proj-123' });
1109
+ * try {
1110
+ * const result = await client.deleteProject({
1111
+ * projectId: 'proj-123',
1112
+ * expectedVersion: project.version
1113
+ * });
1114
+ * console.log('Project deleted successfully');
1115
+ * } catch (error) {
1116
+ * if (error.message.includes('CONFLICT')) {
1117
+ * console.log('Project was modified, deletion cancelled');
1118
+ * }
1119
+ * }
1120
+ * ```
1121
+ */
1122
+ async deleteProject(options) {
1123
+ const { projectId, expectedVersion } = options;
1124
+ // Validate inputs
1125
+ if (!projectId) {
1126
+ throw new Error('projectId is required');
1127
+ }
1128
+ // Build request
1129
+ const args = {
1130
+ subcommand: 'delete_project',
1131
+ projectId,
1132
+ };
1133
+ // Add optional version for atomic deletion
1134
+ if (expectedVersion !== undefined) {
1135
+ args.expectedVersion = expectedVersion;
1136
+ }
1137
+ // Send request to server
1138
+ const request = this.buildRequest('apaext_store', { arguments: args });
1139
+ const response = await this.request(request);
1140
+ // Check for errors
1141
+ if (this.didFail(response)) {
1142
+ const errorMsg = response.message || 'Unknown error deleting project';
1143
+ this.debugMessage(`Project deletion failed: ${errorMsg}`);
1144
+ throw new Error(errorMsg);
1145
+ }
1146
+ // Extract and return response
1147
+ this.debugMessage(`Project deleted successfully: ${projectId}`);
1148
+ return response.body;
1149
+ }
1150
+ /**
1151
+ * List all projects for the current user.
1152
+ *
1153
+ * Retrieves a summary of all projects stored for the authenticated user.
1154
+ * Each project summary includes the ID, name, list of data sources, and total component count.
1155
+ *
1156
+ * @returns Promise resolving to list result with success status, projects array, and count
1157
+ * @throws Error if retrieval fails
1158
+ *
1159
+ * @example
1160
+ * ```typescript
1161
+ * // List all projects
1162
+ * const result = await client.getAllProjects();
1163
+ * console.log(`Found ${result.count} projects:`);
1164
+ * for (const project of result.projects) {
1165
+ * console.log(`- ${project.id}: ${project.name} (${project.totalComponents} components)`);
1166
+ * for (const source of project.sources) {
1167
+ * console.log(` * ${source.name} (${source.provider})`);
1168
+ * }
1169
+ * }
1170
+ *
1171
+ * // Find specific project
1172
+ * const result = await client.getAllProjects();
1173
+ * const myProject = result.projects.find(p => p.id === 'proj-123');
1174
+ * ```
1175
+ */
1176
+ async getAllProjects() {
1177
+ // Build request
1178
+ const args = {
1179
+ subcommand: 'get_all_projects',
1180
+ };
1181
+ // Send request to server
1182
+ const request = this.buildRequest('apaext_store', { arguments: args });
1183
+ const response = await this.request(request);
1184
+ // Check for errors
1185
+ if (this.didFail(response)) {
1186
+ const errorMsg = response.message || 'Unknown error listing projects';
1187
+ this.debugMessage(`Project list retrieval failed: ${errorMsg}`);
1188
+ throw new Error(errorMsg);
1189
+ }
1190
+ // Extract and return response
1191
+ const projectCount = response.body?.count || 0;
1192
+ this.debugMessage(`Projects retrieved successfully: ${projectCount} projects`);
1193
+ return response.body;
1194
+ }
1195
+ // ============================================================================
1196
+ // TEMPLATE STORAGE MANAGEMENT (System-wide templates)
1197
+ // ============================================================================
1198
+ /**
1199
+ * Save or update a template pipeline.
1200
+ *
1201
+ * Stores a template pipeline configuration on the server. Templates are system-wide
1202
+ * and accessible to all users. If the template already exists, it will be updated.
1203
+ * Use expectedVersion to ensure you're updating the version you expect.
1204
+ *
1205
+ * @param options - Save template options
1206
+ * @param options.templateId - Unique identifier for the template
1207
+ * @param options.pipeline - Pipeline configuration object
1208
+ * @param options.expectedVersion - Expected current version for atomic updates (optional)
1209
+ * @returns Promise resolving to save result with success status, templateId, and new version
1210
+ * @throws Error if save fails due to version mismatch, storage error, or invalid input
1211
+ *
1212
+ * @example
1213
+ * ```typescript
1214
+ * // Save a new template
1215
+ * const result = await client.saveTemplate({
1216
+ * templateId: 'tmpl-123',
1217
+ * pipeline: {
1218
+ * source: 'source_1',
1219
+ * pipeline: {
1220
+ * name: 'Data Processor Template',
1221
+ * components: [...]
1222
+ * }
1223
+ * }
1224
+ * });
1225
+ * console.log(`Saved version: ${result.version}`);
1226
+ * ```
1227
+ */
1228
+ async saveTemplate(options) {
1229
+ const { templateId, pipeline, expectedVersion } = options;
1230
+ // Validate inputs
1231
+ if (!templateId) {
1232
+ throw new Error('templateId is required');
1233
+ }
1234
+ if (!pipeline || typeof pipeline !== 'object') {
1235
+ throw new Error('pipeline must be a non-empty object');
1236
+ }
1237
+ // Build request arguments
1238
+ const args = {
1239
+ subcommand: 'save_template',
1240
+ templateId,
1241
+ pipeline,
1242
+ };
1243
+ // Add optional version for atomic updates
1244
+ if (expectedVersion !== undefined) {
1245
+ args.expectedVersion = expectedVersion;
1246
+ }
1247
+ // Send request to server
1248
+ const request = this.buildRequest('apaext_store', { arguments: args });
1249
+ const response = await this.request(request);
1250
+ // Check for errors
1251
+ if (this.didFail(response)) {
1252
+ const errorMsg = response.message || 'Unknown error saving template';
1253
+ this.debugMessage(`Template save failed: ${errorMsg}`);
1254
+ throw new Error(errorMsg);
1255
+ }
1256
+ // Extract and return response
1257
+ this.debugMessage(`Template saved successfully: ${templateId}, version: ${response.body?.version}`);
1258
+ return response.body;
1259
+ }
1260
+ /**
1261
+ * Retrieve a template by its ID.
1262
+ *
1263
+ * Fetches the complete pipeline configuration and current version for
1264
+ * the specified template.
1265
+ *
1266
+ * @param options - Get template options
1267
+ * @param options.templateId - Unique identifier of the template to retrieve
1268
+ * @returns Promise resolving to template data with success status, pipeline, and version
1269
+ * @throws Error if template doesn't exist or retrieval fails
1270
+ *
1271
+ * @example
1272
+ * ```typescript
1273
+ * try {
1274
+ * const template = await client.getTemplate({ templateId: 'tmpl-123' });
1275
+ * console.log(`Template: ${template.pipeline.pipeline.name}`);
1276
+ * console.log(`Version: ${template.version}`);
1277
+ * } catch (error) {
1278
+ * if (error.message.includes('NOT_FOUND')) {
1279
+ * console.log("Template doesn't exist");
1280
+ * }
1281
+ * }
1282
+ * ```
1283
+ */
1284
+ async getTemplate(options) {
1285
+ const { templateId } = options;
1286
+ // Validate inputs
1287
+ if (!templateId) {
1288
+ throw new Error('templateId is required');
1289
+ }
1290
+ // Build request
1291
+ const args = {
1292
+ subcommand: 'get_template',
1293
+ templateId,
1294
+ };
1295
+ // Send request to server
1296
+ const request = this.buildRequest('apaext_store', { arguments: args });
1297
+ const response = await this.request(request);
1298
+ // Check for errors
1299
+ if (this.didFail(response)) {
1300
+ const errorMsg = response.message || 'Unknown error retrieving template';
1301
+ this.debugMessage(`Template retrieval failed: ${errorMsg}`);
1302
+ throw new Error(errorMsg);
1303
+ }
1304
+ // Extract and return response
1305
+ this.debugMessage(`Template retrieved successfully: ${templateId}`);
1306
+ return response.body;
1307
+ }
1308
+ /**
1309
+ * Delete a template by its ID.
1310
+ *
1311
+ * Permanently removes a template from storage. Optionally verify the
1312
+ * version before deletion to ensure you're deleting the version you expect.
1313
+ *
1314
+ * @param options - Delete template options
1315
+ * @param options.templateId - Unique identifier of the template to delete
1316
+ * @param options.expectedVersion - Expected current version for atomic deletion (optional)
1317
+ * @returns Promise resolving to deletion result with success status and message
1318
+ * @throws Error if template doesn't exist, version mismatch, or deletion fails
1319
+ *
1320
+ * @example
1321
+ * ```typescript
1322
+ * // Safe deletion with version check
1323
+ * const template = await client.getTemplate({ templateId: 'tmpl-123' });
1324
+ * try {
1325
+ * const result = await client.deleteTemplate({
1326
+ * templateId: 'tmpl-123',
1327
+ * expectedVersion: template.version
1328
+ * });
1329
+ * console.log('Template deleted successfully');
1330
+ * } catch (error) {
1331
+ * if (error.message.includes('CONFLICT')) {
1332
+ * console.log('Template was modified, deletion cancelled');
1333
+ * }
1334
+ * }
1335
+ * ```
1336
+ */
1337
+ async deleteTemplate(options) {
1338
+ const { templateId, expectedVersion } = options;
1339
+ // Validate inputs
1340
+ if (!templateId) {
1341
+ throw new Error('templateId is required');
1342
+ }
1343
+ // Build request
1344
+ const args = {
1345
+ subcommand: 'delete_template',
1346
+ templateId,
1347
+ };
1348
+ // Add optional version for atomic deletion
1349
+ if (expectedVersion !== undefined) {
1350
+ args.expectedVersion = expectedVersion;
1351
+ }
1352
+ // Send request to server
1353
+ const request = this.buildRequest('apaext_store', { arguments: args });
1354
+ const response = await this.request(request);
1355
+ // Check for errors
1356
+ if (this.didFail(response)) {
1357
+ const errorMsg = response.message || 'Unknown error deleting template';
1358
+ this.debugMessage(`Template deletion failed: ${errorMsg}`);
1359
+ throw new Error(errorMsg);
1360
+ }
1361
+ // Extract and return response
1362
+ this.debugMessage(`Template deleted successfully: ${templateId}`);
1363
+ return response.body;
1364
+ }
1365
+ /**
1366
+ * List all templates.
1367
+ *
1368
+ * Retrieves a summary of all templates stored in the system.
1369
+ * Each template summary includes the ID, name, list of data sources, and total component count.
1370
+ *
1371
+ * @returns Promise resolving to list result with success status, templates array, and count
1372
+ * @throws Error if retrieval fails
1373
+ *
1374
+ * @example
1375
+ * ```typescript
1376
+ * // List all templates
1377
+ * const result = await client.getAllTemplates();
1378
+ * console.log(`Found ${result.count} templates:`);
1379
+ * for (const template of result.templates) {
1380
+ * console.log(`- ${template.id}: ${template.name} (${template.totalComponents} components)`);
1381
+ * for (const source of template.sources) {
1382
+ * console.log(` * ${source.name} (${source.provider})`);
1383
+ * }
1384
+ * }
1385
+ * ```
1386
+ */
1387
+ async getAllTemplates() {
1388
+ // Build request
1389
+ const args = {
1390
+ subcommand: 'get_all_templates',
1391
+ };
1392
+ // Send request to server
1393
+ const request = this.buildRequest('apaext_store', { arguments: args });
1394
+ const response = await this.request(request);
1395
+ // Check for errors
1396
+ if (this.didFail(response)) {
1397
+ const errorMsg = response.message || 'Unknown error listing templates';
1398
+ this.debugMessage(`Template list retrieval failed: ${errorMsg}`);
1399
+ throw new Error(errorMsg);
1400
+ }
1401
+ // Extract and return response
1402
+ const templateCount = response.body?.count || 0;
1403
+ this.debugMessage(`Templates retrieved successfully: ${templateCount} templates`);
1404
+ return response.body;
1405
+ }
1406
+ // ============================================================================
1407
+ // LOG STORAGE MANAGEMENT (Per-project log files for historical tracking)
1408
+ // ============================================================================
1409
+ /**
1410
+ * Save a log file for a source run.
1411
+ *
1412
+ * Creates or overwrites a log file in the project's log directory.
1413
+ * The filename is constructed as <source>-<startTime>.log where startTime
1414
+ * is extracted from contents.body.startTime.
1415
+ *
1416
+ * @param options - Save log options
1417
+ * @param options.projectId - Project ID
1418
+ * @param options.source - Name of the source
1419
+ * @param options.contents - Log contents object containing body.startTime
1420
+ * @returns Promise resolving to save result with success status and filename
1421
+ * @throws Error if save fails
1422
+ *
1423
+ * @example
1424
+ * ```typescript
1425
+ * const logContents = {
1426
+ * type: 'event',
1427
+ * event: 'apaevt_status_update',
1428
+ * body: {
1429
+ * source: 'source_1',
1430
+ * startTime: 1764337626.6564875,
1431
+ * status: 'Completed',
1432
+ * completed: true,
1433
+ * totalCount: 100,
1434
+ * completedCount: 100
1435
+ * }
1436
+ * };
1437
+ * const result = await client.saveLog({
1438
+ * projectId: 'proj-123',
1439
+ * source: 'source_1',
1440
+ * contents: logContents
1441
+ * });
1442
+ * console.log(`Saved: ${result.filename}`);
1443
+ * ```
1444
+ */
1445
+ async saveLog(options) {
1446
+ const { projectId, source, contents } = options;
1447
+ // Validate inputs
1448
+ if (!projectId) {
1449
+ throw new Error('projectId is required');
1450
+ }
1451
+ if (!source) {
1452
+ throw new Error('source is required');
1453
+ }
1454
+ if (!contents || typeof contents !== 'object') {
1455
+ throw new Error('contents must be a non-empty object');
1456
+ }
1457
+ // Build request arguments
1458
+ const args = {
1459
+ subcommand: 'save_log',
1460
+ projectId,
1461
+ source,
1462
+ contents,
1463
+ };
1464
+ // Send request to server
1465
+ const request = this.buildRequest('apaext_store', { arguments: args });
1466
+ const response = await this.request(request);
1467
+ // Check for errors
1468
+ if (this.didFail(response)) {
1469
+ const errorMsg = response.message || 'Unknown error saving log';
1470
+ this.debugMessage(`Log save failed: ${errorMsg}`);
1471
+ throw new Error(errorMsg);
1472
+ }
1473
+ // Extract and return response
1474
+ this.debugMessage(`Log saved successfully: ${response.body?.filename}`);
1475
+ return response.body;
1476
+ }
1477
+ /**
1478
+ * Get a log file by source name and start time.
1479
+ *
1480
+ * @param options - Get log options
1481
+ * @param options.projectId - Project ID
1482
+ * @param options.source - Name of the source
1483
+ * @param options.startTime - Start time of the run
1484
+ * @returns Promise resolving to log data with success status and contents
1485
+ * @throws Error if log not found or retrieval fails
1486
+ *
1487
+ * @example
1488
+ * ```typescript
1489
+ * const log = await client.getLog({
1490
+ * projectId: 'proj-123',
1491
+ * source: 'source_1',
1492
+ * startTime: 1764337626.6564875
1493
+ * });
1494
+ * console.log(`Status: ${log.contents.body.status}`);
1495
+ * ```
1496
+ */
1497
+ async getLog(options) {
1498
+ const { projectId, source, startTime } = options;
1499
+ // Validate inputs
1500
+ if (!projectId) {
1501
+ throw new Error('projectId is required');
1502
+ }
1503
+ if (!source) {
1504
+ throw new Error('source is required');
1505
+ }
1506
+ if (startTime === undefined || startTime === null) {
1507
+ throw new Error('startTime is required');
1508
+ }
1509
+ // Build request
1510
+ const args = {
1511
+ subcommand: 'get_log',
1512
+ projectId,
1513
+ source,
1514
+ startTime,
1515
+ };
1516
+ // Send request to server
1517
+ const request = this.buildRequest('apaext_store', { arguments: args });
1518
+ const response = await this.request(request);
1519
+ // Check for errors
1520
+ if (this.didFail(response)) {
1521
+ const errorMsg = response.message || 'Unknown error retrieving log';
1522
+ this.debugMessage(`Log retrieval failed: ${errorMsg}`);
1523
+ throw new Error(errorMsg);
1524
+ }
1525
+ // Extract and return response
1526
+ this.debugMessage(`Log retrieved successfully: ${projectId}/${source}`);
1527
+ return response.body;
1528
+ }
1529
+ /**
1530
+ * List log files for a project.
1531
+ *
1532
+ * @param options - List logs options
1533
+ * @param options.projectId - Project ID
1534
+ * @param options.source - Optional source name to filter logs (filters files starting with '<source>-')
1535
+ * @param options.page - Page number (0-indexed). If negative or undefined, defaults to 0. Page size is 100.
1536
+ * @returns Promise resolving to list result with success status, logs array, counts, and pagination info
1537
+ * @throws Error if retrieval fails
1538
+ *
1539
+ * @example
1540
+ * ```typescript
1541
+ * // List all logs
1542
+ * const result = await client.listLogs({ projectId: 'proj-123' });
1543
+ * console.log(`Found ${result.total_count} logs`);
1544
+ * for (const log of result.logs) {
1545
+ * console.log(` - ${log}`);
1546
+ * }
1547
+ *
1548
+ * // Filter by source
1549
+ * const filtered = await client.listLogs({
1550
+ * projectId: 'proj-123',
1551
+ * source: 'source_1'
1552
+ * });
1553
+ *
1554
+ * // With pagination
1555
+ * const page2 = await client.listLogs({
1556
+ * projectId: 'proj-123',
1557
+ * page: 1
1558
+ * });
1559
+ * ```
1560
+ */
1561
+ async listLogs(options) {
1562
+ const { projectId, source, page } = options;
1563
+ // Validate inputs
1564
+ if (!projectId) {
1565
+ throw new Error('projectId is required');
1566
+ }
1567
+ // Build request
1568
+ const args = {
1569
+ subcommand: 'list_logs',
1570
+ projectId,
1571
+ };
1572
+ // Add optional parameters
1573
+ if (source !== undefined) {
1574
+ args.source = source;
1575
+ }
1576
+ if (page !== undefined) {
1577
+ args.page = page;
1578
+ }
1579
+ // Send request to server
1580
+ const request = this.buildRequest('apaext_store', { arguments: args });
1581
+ const response = await this.request(request);
1582
+ // Check for errors
1583
+ if (this.didFail(response)) {
1584
+ const errorMsg = response.message || 'Unknown error listing logs';
1585
+ this.debugMessage(`Log list retrieval failed: ${errorMsg}`);
1586
+ throw new Error(errorMsg);
1587
+ }
1588
+ // Extract and return response
1589
+ const logCount = response.body?.total_count || 0;
1590
+ this.debugMessage(`Logs retrieved successfully: ${logCount} logs`);
1591
+ return response.body;
1592
+ }
1593
+ // ============================================================================
1062
1594
  // CONTEXT MANAGER SUPPORT - Python-style async context manager
1063
1595
  // ============================================================================
1064
1596
  /**