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