@snutils/snu 0.2.8 → 0.2.11

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.
@@ -460,6 +460,187 @@ class StandaloneDispatcher {
460
460
  return [];
461
461
  }
462
462
  }
463
+ // One page of one table into canonical workspace files. Shared by
464
+ // pull_records (a single page the caller sized) and pull_scope (which walks
465
+ // every code table of an application page by page).
466
+ async pullTablePage(inst, reqCommand, opts) {
467
+ const { table, codeFields, limit } = opts;
468
+ const combinedQuery = opts.query;
469
+ const offset = opts.offset || 0;
470
+ const displayFields = ['sys_id', 'name', 'sys_name', 'short_description', 'sys_scope', 'sys_scope.scope'];
471
+ const allRequestedFields = Array.from(new Set([...displayFields, ...codeFields])).join(',');
472
+ const queryParams = {
473
+ sysparm_fields: allRequestedFields,
474
+ sysparm_limit: String(limit),
475
+ sysparm_offset: String(offset),
476
+ sysparm_display_value: 'false',
477
+ sysparm_exclude_reference_link: 'true',
478
+ sysparm_no_count: 'true',
479
+ };
480
+ if (combinedQuery) {
481
+ queryParams.sysparm_query = combinedQuery;
482
+ }
483
+ const correlationId = crypto.randomUUID();
484
+ const pendingPromise = this.pending.register({ id: correlationId, command: reqCommand, timeoutMs: 70_000 });
485
+ this.ws.sendToBrowser({
486
+ action: 'agentRestApi',
487
+ agentRequestId: correlationId,
488
+ endpoint: `/api/now/table/${table}`,
489
+ method: 'GET',
490
+ queryParams,
491
+ instance: inst.settings,
492
+ appName: 'SN Utils CLI',
493
+ });
494
+ const res = await pendingPromise;
495
+ if (res.success === false) {
496
+ throw Object.assign(new Error(res.error || 'Failed to pull records'), { code: 'E_COMMAND_FAILED' });
497
+ }
498
+ const matchedRecords = Array.isArray(res.data?.result) ? res.data.result : (res.data?.result ? [res.data.result] : []);
499
+ const isFolderRecordTable = FOLDERRECORDTABLES.includes(table);
500
+ let filesWritten = 0;
501
+ let skippedEmpty = 0;
502
+ const warnings = [];
503
+ const pulledRecordsList = [];
504
+ for (const rec of matchedRecords) {
505
+ const sysId = typeof rec.sys_id === 'object' ? rec.sys_id.value : String(rec.sys_id || '');
506
+ if (!sysId)
507
+ continue;
508
+ let scope = 'global';
509
+ if (rec['sys_scope.scope']) {
510
+ scope = String(rec['sys_scope.scope']);
511
+ }
512
+ else if (rec.sys_scope) {
513
+ scope = typeof rec.sys_scope === 'object' ? String(rec.sys_scope.value || rec.sys_scope.display_value || 'global') : String(rec.sys_scope);
514
+ }
515
+ if (!scope || scope === 'null' || scope === 'undefined')
516
+ scope = 'global';
517
+ const rawName = rec.name || rec.sys_name || rec.short_description || sysId;
518
+ const name = String(rawName).trim();
519
+ let mapPath;
520
+ try {
521
+ mapPath = safeJoinUnderRoot(this.cwd, inst.name, scope, table, '_map.json');
522
+ }
523
+ catch (e) {
524
+ warnings.push(`Could not resolve map path for ${scope}/${table}: ${e?.message || e}`);
525
+ continue;
526
+ }
527
+ const { cleanName, renamedTo, map: nameToSysId } = resolveMappedFileName(mapPath, name, sysId);
528
+ try {
529
+ writeMapFile(mapPath, nameToSysId);
530
+ }
531
+ catch (e) {
532
+ warnings.push(`Failed to write _map.json at ${mapPath}: ${e?.message || e}`);
533
+ }
534
+ if (renamedTo) {
535
+ warnings.push(`Record ${sysId} is named '${renamedTo}' on the instance but keeps local file name '${cleanName}' (rename tracked in _map.json).`);
536
+ }
537
+ if (table === 'sp_widget') {
538
+ try {
539
+ const testUrlsPath = safeJoinUnderRoot(this.cwd, inst.name, scope, table, cleanName, '_test_urls.txt');
540
+ if (!fs.existsSync(testUrlsPath)) {
541
+ const dispVal = name.toLowerCase().replace(/\s+/g, '_');
542
+ const testUrls = [
543
+ `${inst.settings.url}/$sp.do?id=sp-preview&sys_id=${sysId}`,
544
+ `${inst.settings.url}/sp_config?id=${dispVal}`,
545
+ `${inst.settings.url}/sp?id=${dispVal}`,
546
+ `${inst.settings.url}/esc?id=${dispVal}`,
547
+ ].join('\n');
548
+ fs.mkdirSync(path.dirname(testUrlsPath), { recursive: true });
549
+ fs.writeFileSync(testUrlsPath, testUrls, 'utf8');
550
+ }
551
+ }
552
+ catch { }
553
+ }
554
+ const recordFiles = [];
555
+ for (const field of codeFields) {
556
+ const ext = resolveFieldExtension(table, field, this.cwd);
557
+ let targetPath;
558
+ try {
559
+ targetPath = isFolderRecordTable
560
+ ? safeJoinUnderRoot(this.cwd, inst.name, scope, table, cleanName, `${field}${ext}`)
561
+ : safeJoinUnderRoot(this.cwd, inst.name, scope, table, `${cleanName}.${field}${ext}`);
562
+ }
563
+ catch (e) {
564
+ warnings.push(`Unsafe path for ${scope}/${table}/${cleanName}.${field}: ${e?.message || e}`);
565
+ continue;
566
+ }
567
+ const relPath = path.relative(this.cwd, targetPath).replace(/\\/g, '/');
568
+ const rawVal = rec[field];
569
+ const content = rawVal !== null && rawVal !== undefined ? String(rawVal) : '';
570
+ const fileExisted = fs.existsSync(targetPath);
571
+ if (content.length > 0) {
572
+ try {
573
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
574
+ fs.writeFileSync(targetPath, content, 'utf8');
575
+ filesWritten++;
576
+ const action = fileExisted ? 'updated' : 'created';
577
+ recordFiles.push({ field, path: relPath, bytes: Buffer.byteLength(content, 'utf8'), action });
578
+ }
579
+ catch (e) {
580
+ warnings.push(`Failed to write ${relPath}: ${e?.message || e}`);
581
+ }
582
+ }
583
+ else if (fileExisted) {
584
+ try {
585
+ fs.writeFileSync(targetPath, '', 'utf8');
586
+ filesWritten++;
587
+ recordFiles.push({ field, path: relPath, bytes: 0, action: 'cleared' });
588
+ }
589
+ catch (e) {
590
+ warnings.push(`Failed to clear ${relPath}: ${e?.message || e}`);
591
+ }
592
+ }
593
+ else {
594
+ skippedEmpty++;
595
+ recordFiles.push({ field, path: relPath, bytes: 0, action: 'skipped_empty' });
596
+ }
597
+ }
598
+ pulledRecordsList.push({
599
+ sys_id: sysId,
600
+ name,
601
+ scope,
602
+ files: recordFiles,
603
+ });
604
+ }
605
+ return {
606
+ table,
607
+ matchedRecords: matchedRecords.length,
608
+ pulledRecords: pulledRecordsList.length,
609
+ filesWritten,
610
+ skippedEmpty,
611
+ warnings,
612
+ records: pulledRecordsList,
613
+ };
614
+ }
615
+ // Which artifact tables snu knows how to write to disk: the ones in
616
+ // resources/metaDataRelations.json that declare code fields.
617
+ allCodeTables() {
618
+ const meta = getMetaDataRelations(this.cwd);
619
+ const out = new Set();
620
+ for (const [table, def] of Object.entries(meta?.tableFields || {})) {
621
+ if (def?.codeFields && typeof def.codeFields === 'object')
622
+ out.add(table);
623
+ }
624
+ return out;
625
+ }
626
+ async restGet(inst, reqCommand, endpoint, queryParams) {
627
+ const correlationId = crypto.randomUUID();
628
+ const pendingPromise = this.pending.register({ id: correlationId, command: reqCommand, timeoutMs: 70_000 });
629
+ this.ws.sendToBrowser({
630
+ action: 'agentRestApi',
631
+ agentRequestId: correlationId,
632
+ endpoint,
633
+ method: 'GET',
634
+ queryParams,
635
+ instance: inst.settings,
636
+ appName: 'SN Utils CLI',
637
+ });
638
+ const res = await pendingPromise;
639
+ if (res?.success === false) {
640
+ throw Object.assign(new Error(res.error || `REST request failed: ${endpoint}`), { code: res.code || 'E_COMMAND_FAILED' });
641
+ }
642
+ return res?.data;
643
+ }
463
644
  resolveInstance(requestInstance) {
464
645
  const folders = this.listInstanceFolders();
465
646
  const liveInstances = this.ws.getLiveInstances();
@@ -1173,6 +1354,156 @@ class StandaloneDispatcher {
1173
1354
  result: res.data?.result || res.data,
1174
1355
  };
1175
1356
  }
1357
+ // Pull Scope: every scriptable artifact of one application, paged
1358
+ if (req.command === 'pull_scope') {
1359
+ const rawScope = req.params?.scope;
1360
+ if (!rawScope || typeof rawScope !== 'string' || !rawScope.trim()) {
1361
+ throw Object.assign(new Error('Missing required param "scope" (application scope name such as x_acme_app, or its sys_scope sys_id)'), { code: 'E_INVALID_PARAMS' });
1362
+ }
1363
+ const scopeParam = rawScope.trim();
1364
+ if (scopeParam === 'global') {
1365
+ throw Object.assign(new Error('pull_scope targets one application; the global scope is too large to pull whole. Use pull_records with a query instead.'), { code: 'E_INVALID_PARAMS' });
1366
+ }
1367
+ if (!/^[A-Za-z0-9_]+$/.test(scopeParam)) {
1368
+ throw Object.assign(new Error('Parameter "scope" must be a scope name (letters, digits, underscore) or a 32-character sys_id.'), { code: 'E_INVALID_PARAMS' });
1369
+ }
1370
+ const PAGE = 100, DEFAULT_LIMIT = 2000, MAX_LIMIT = 10000;
1371
+ let limit = DEFAULT_LIMIT;
1372
+ if (req.params?.limit !== undefined) {
1373
+ if (typeof req.params.limit !== 'number' || !Number.isInteger(req.params.limit) || req.params.limit < 1 || req.params.limit > MAX_LIMIT) {
1374
+ throw Object.assign(new Error(`Parameter "limit" must be an integer between 1 and ${MAX_LIMIT} (records per table).`), { code: 'E_INVALID_PARAMS' });
1375
+ }
1376
+ limit = req.params.limit;
1377
+ }
1378
+ let tableFilter = null;
1379
+ if (req.params?.tables !== undefined) {
1380
+ const raw = Array.isArray(req.params.tables) ? req.params.tables : (typeof req.params.tables === 'string' ? req.params.tables.split(',') : null);
1381
+ if (!raw)
1382
+ throw Object.assign(new Error('Parameter "tables" must be an array of table names.'), { code: 'E_INVALID_PARAMS' });
1383
+ tableFilter = new Set();
1384
+ for (const t of raw) {
1385
+ if (typeof t !== 'string' || !/^[A-Za-z0-9_]+$/.test(t.trim())) {
1386
+ throw Object.assign(new Error(`Invalid table name in "tables": ${String(t)}`), { code: 'E_INVALID_PARAMS' });
1387
+ }
1388
+ tableFilter.add(t.trim());
1389
+ }
1390
+ }
1391
+ const includeRecords = req.params?.includeRecords === true;
1392
+ // Resolve the scope: sys_id as given, else scopes.json, else the instance.
1393
+ let scopeSysId;
1394
+ let scopeName;
1395
+ const scopesPath = path.join(this.cwd, inst.name, 'scopes.json');
1396
+ let scopes = {};
1397
+ try {
1398
+ scopes = JSON.parse(fs.readFileSync(scopesPath, 'utf8')) || {};
1399
+ }
1400
+ catch { }
1401
+ if (/^[0-9a-fA-F]{32}$/.test(scopeParam)) {
1402
+ scopeSysId = scopeParam.toLowerCase();
1403
+ scopeName = Object.keys(scopes).find((k) => String(scopes[k]).toLowerCase() === scopeSysId);
1404
+ }
1405
+ else {
1406
+ scopeName = scopeParam;
1407
+ scopeSysId = scopes[scopeName] ? String(scopes[scopeName]).toLowerCase() : undefined;
1408
+ }
1409
+ if (!scopeSysId || !scopeName) {
1410
+ const data = await this.restGet(inst, req.command, '/api/now/table/sys_scope', {
1411
+ sysparm_query: scopeSysId ? `sys_id=${scopeSysId}` : `scope=${scopeName}`,
1412
+ sysparm_fields: 'sys_id,scope,name',
1413
+ sysparm_limit: '1',
1414
+ sysparm_exclude_reference_link: 'true',
1415
+ sysparm_no_count: 'true',
1416
+ });
1417
+ const row = Array.isArray(data?.result) ? data.result[0] : data?.result;
1418
+ if (!row?.sys_id) {
1419
+ throw Object.assign(new Error(`Application scope "${scopeParam}" was not found on ${inst.name}.`), { code: 'E_NOT_FOUND' });
1420
+ }
1421
+ scopeSysId = String(typeof row.sys_id === 'object' ? row.sys_id.value : row.sys_id).toLowerCase();
1422
+ scopeName = String(typeof row.scope === 'object' ? row.scope.value : row.scope);
1423
+ try {
1424
+ scopes[scopeName] = scopeSysId;
1425
+ fs.mkdirSync(path.dirname(scopesPath), { recursive: true });
1426
+ fs.writeFileSync(scopesPath, JSON.stringify(scopes, null, 2));
1427
+ }
1428
+ catch { }
1429
+ }
1430
+ // Which artifact tables does this application actually use?
1431
+ const classCounts = new Map();
1432
+ const metaPage = 1000;
1433
+ for (let offset = 0;; offset += metaPage) {
1434
+ const data = await this.restGet(inst, req.command, '/api/now/table/sys_metadata', {
1435
+ sysparm_query: `sys_scope=${scopeSysId}^sys_class_name!=sys_metadata_delete^sys_update_name!=NULL^ORDERBYsys_id`,
1436
+ sysparm_fields: 'sys_class_name',
1437
+ sysparm_limit: String(metaPage),
1438
+ sysparm_offset: String(offset),
1439
+ sysparm_exclude_reference_link: 'true',
1440
+ sysparm_no_count: 'true',
1441
+ });
1442
+ const rows = Array.isArray(data?.result) ? data.result : [];
1443
+ for (const r of rows) {
1444
+ const cls = String(typeof r.sys_class_name === 'object' ? r.sys_class_name.value : r.sys_class_name || '');
1445
+ if (cls)
1446
+ classCounts.set(cls, (classCounts.get(cls) || 0) + 1);
1447
+ }
1448
+ if (rows.length < metaPage)
1449
+ break;
1450
+ }
1451
+ const codeTables = this.allCodeTables();
1452
+ const tables = [...classCounts.keys()].filter((t) => codeTables.has(t) && (!tableFilter || tableFilter.has(t))).sort();
1453
+ const skippedTables = [...classCounts.keys()].filter((t) => !codeTables.has(t)).sort()
1454
+ .map((t) => ({ table: t, records: classCounts.get(t) || 0 }));
1455
+ const missingTables = tableFilter ? [...tableFilter].filter((t) => !classCounts.has(t)).sort() : [];
1456
+ const perTable = [];
1457
+ const warnings = [];
1458
+ let totalRecords = 0, totalFiles = 0, totalSkippedEmpty = 0;
1459
+ for (const table of tables) {
1460
+ const codeFields = resolveTableCodeFields(table, this.cwd);
1461
+ const query = `sys_scope=${scopeSysId}^sys_class_name=${table}^ORDERBYsys_id`;
1462
+ const entry = { table, matchedRecords: 0, pulledRecords: 0, filesWritten: 0, skippedEmpty: 0, truncated: false };
1463
+ if (includeRecords)
1464
+ entry.records = [];
1465
+ for (let offset = 0;; offset += PAGE) {
1466
+ const pageLimit = Math.min(PAGE, limit - entry.matchedRecords);
1467
+ if (pageLimit <= 0) {
1468
+ entry.truncated = true;
1469
+ break;
1470
+ }
1471
+ const page = await this.pullTablePage(inst, req.command, { table, query, codeFields, limit: pageLimit, offset });
1472
+ entry.matchedRecords += page.matchedRecords;
1473
+ entry.pulledRecords += page.pulledRecords;
1474
+ entry.filesWritten += page.filesWritten;
1475
+ entry.skippedEmpty += page.skippedEmpty;
1476
+ if (includeRecords)
1477
+ entry.records.push(...page.records);
1478
+ for (const w of page.warnings)
1479
+ warnings.push(`${table}: ${w}`);
1480
+ if (page.matchedRecords < pageLimit)
1481
+ break;
1482
+ }
1483
+ if (entry.truncated)
1484
+ warnings.push(`${table}: stopped at the per-table limit of ${limit} records; raise "limit" or pull the rest with pull_records.`);
1485
+ totalRecords += entry.pulledRecords;
1486
+ totalFiles += entry.filesWritten;
1487
+ totalSkippedEmpty += entry.skippedEmpty;
1488
+ perTable.push(entry);
1489
+ }
1490
+ for (const t of missingTables)
1491
+ warnings.push(`${t}: no records of this table in scope ${scopeName}.`);
1492
+ return {
1493
+ id: req.id,
1494
+ command: req.command,
1495
+ status: 'success',
1496
+ timestamp: Date.now(),
1497
+ result: {
1498
+ scope: { name: scopeName, sys_id: scopeSysId },
1499
+ folder: path.join(inst.name, scopeName).replace(/\\/g, '/'),
1500
+ tables: perTable,
1501
+ totals: { tables: perTable.length, records: totalRecords, filesWritten: totalFiles, skippedEmpty: totalSkippedEmpty },
1502
+ skippedTables,
1503
+ warnings,
1504
+ },
1505
+ };
1506
+ }
1176
1507
  // Pull Records / Pull Artifacts
1177
1508
  if (req.command === 'pull_records' || req.command === 'pull_artifacts') {
1178
1509
  const rawTable = req.params?.table;
@@ -1228,154 +1559,8 @@ class StandaloneDispatcher {
1228
1559
  if (codeFields.length === 0) {
1229
1560
  codeFields = resolveTableCodeFields(table, this.cwd);
1230
1561
  }
1231
- const displayFields = ['sys_id', 'name', 'sys_name', 'short_description', 'sys_scope', 'sys_scope.scope'];
1232
- const allRequestedFields = Array.from(new Set([...displayFields, ...codeFields])).join(',');
1233
- const queryParams = {
1234
- sysparm_fields: allRequestedFields,
1235
- sysparm_limit: String(limit),
1236
- sysparm_display_value: 'false',
1237
- sysparm_exclude_reference_link: 'true',
1238
- sysparm_no_count: 'true',
1239
- };
1240
- if (combinedQuery) {
1241
- queryParams.sysparm_query = combinedQuery;
1242
- }
1243
- const pendingPromise = this.pending.register({ id: correlationId, command: req.command, timeoutMs: 70_000 });
1244
- this.ws.sendToBrowser({
1245
- action: 'agentRestApi',
1246
- agentRequestId: correlationId,
1247
- endpoint: `/api/now/table/${table}`,
1248
- method: 'GET',
1249
- queryParams,
1250
- instance: inst.settings,
1251
- appName: 'SN Utils CLI',
1252
- });
1253
- const res = await pendingPromise;
1254
- if (res.success === false) {
1255
- throw Object.assign(new Error(res.error || 'Failed to pull records'), { code: 'E_COMMAND_FAILED' });
1256
- }
1257
- const matchedRecords = Array.isArray(res.data?.result) ? res.data.result : (res.data?.result ? [res.data.result] : []);
1258
- const isFolderRecordTable = FOLDERRECORDTABLES.includes(table);
1259
- let filesWritten = 0;
1260
- let skippedEmpty = 0;
1261
- const warnings = [];
1262
- const pulledRecordsList = [];
1263
- for (const rec of matchedRecords) {
1264
- const sysId = typeof rec.sys_id === 'object' ? rec.sys_id.value : String(rec.sys_id || '');
1265
- if (!sysId)
1266
- continue;
1267
- let scope = 'global';
1268
- if (rec['sys_scope.scope']) {
1269
- scope = String(rec['sys_scope.scope']);
1270
- }
1271
- else if (rec.sys_scope) {
1272
- scope = typeof rec.sys_scope === 'object' ? String(rec.sys_scope.value || rec.sys_scope.display_value || 'global') : String(rec.sys_scope);
1273
- }
1274
- if (!scope || scope === 'null' || scope === 'undefined')
1275
- scope = 'global';
1276
- const rawName = rec.name || rec.sys_name || rec.short_description || sysId;
1277
- const name = String(rawName).trim();
1278
- let mapPath;
1279
- try {
1280
- mapPath = safeJoinUnderRoot(this.cwd, inst.name, scope, table, '_map.json');
1281
- }
1282
- catch (e) {
1283
- warnings.push(`Could not resolve map path for ${scope}/${table}: ${e?.message || e}`);
1284
- continue;
1285
- }
1286
- const { cleanName, renamedTo, map: nameToSysId } = resolveMappedFileName(mapPath, name, sysId);
1287
- try {
1288
- writeMapFile(mapPath, nameToSysId);
1289
- }
1290
- catch (e) {
1291
- warnings.push(`Failed to write _map.json at ${mapPath}: ${e?.message || e}`);
1292
- }
1293
- if (renamedTo) {
1294
- warnings.push(`Record ${sysId} is named '${renamedTo}' on the instance but keeps local file name '${cleanName}' (rename tracked in _map.json).`);
1295
- }
1296
- if (table === 'sp_widget') {
1297
- try {
1298
- const testUrlsPath = safeJoinUnderRoot(this.cwd, inst.name, scope, table, cleanName, '_test_urls.txt');
1299
- if (!fs.existsSync(testUrlsPath)) {
1300
- const dispVal = name.toLowerCase().replace(/\s+/g, '_');
1301
- const testUrls = [
1302
- `${inst.settings.url}/$sp.do?id=sp-preview&sys_id=${sysId}`,
1303
- `${inst.settings.url}/sp_config?id=${dispVal}`,
1304
- `${inst.settings.url}/sp?id=${dispVal}`,
1305
- `${inst.settings.url}/esc?id=${dispVal}`,
1306
- ].join('\n');
1307
- fs.mkdirSync(path.dirname(testUrlsPath), { recursive: true });
1308
- fs.writeFileSync(testUrlsPath, testUrls, 'utf8');
1309
- }
1310
- }
1311
- catch { }
1312
- }
1313
- const recordFiles = [];
1314
- for (const field of codeFields) {
1315
- const ext = resolveFieldExtension(table, field, this.cwd);
1316
- let targetPath;
1317
- try {
1318
- targetPath = isFolderRecordTable
1319
- ? safeJoinUnderRoot(this.cwd, inst.name, scope, table, cleanName, `${field}${ext}`)
1320
- : safeJoinUnderRoot(this.cwd, inst.name, scope, table, `${cleanName}.${field}${ext}`);
1321
- }
1322
- catch (e) {
1323
- warnings.push(`Unsafe path for ${scope}/${table}/${cleanName}.${field}: ${e?.message || e}`);
1324
- continue;
1325
- }
1326
- const relPath = path.relative(this.cwd, targetPath).replace(/\\/g, '/');
1327
- const rawVal = rec[field];
1328
- const content = rawVal !== null && rawVal !== undefined ? String(rawVal) : '';
1329
- const fileExisted = fs.existsSync(targetPath);
1330
- if (content.length > 0) {
1331
- try {
1332
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
1333
- fs.writeFileSync(targetPath, content, 'utf8');
1334
- filesWritten++;
1335
- const action = fileExisted ? 'updated' : 'created';
1336
- recordFiles.push({ field, path: relPath, bytes: Buffer.byteLength(content, 'utf8'), action });
1337
- }
1338
- catch (e) {
1339
- warnings.push(`Failed to write ${relPath}: ${e?.message || e}`);
1340
- }
1341
- }
1342
- else if (fileExisted) {
1343
- try {
1344
- fs.writeFileSync(targetPath, '', 'utf8');
1345
- filesWritten++;
1346
- recordFiles.push({ field, path: relPath, bytes: 0, action: 'cleared' });
1347
- }
1348
- catch (e) {
1349
- warnings.push(`Failed to clear ${relPath}: ${e?.message || e}`);
1350
- }
1351
- }
1352
- else {
1353
- skippedEmpty++;
1354
- recordFiles.push({ field, path: relPath, bytes: 0, action: 'skipped_empty' });
1355
- }
1356
- }
1357
- pulledRecordsList.push({
1358
- sys_id: sysId,
1359
- name,
1360
- scope,
1361
- files: recordFiles,
1362
- });
1363
- }
1364
- return {
1365
- id: req.id,
1366
- command: req.command,
1367
- status: 'success',
1368
- timestamp: Date.now(),
1369
- result: {
1370
- table,
1371
- matchedRecords: matchedRecords.length,
1372
- pulledRecords: pulledRecordsList.length,
1373
- filesWritten,
1374
- skippedEmpty,
1375
- warnings,
1376
- records: pulledRecordsList,
1377
- },
1378
- };
1562
+ const result = await this.pullTablePage(inst, req.command, { table, query: combinedQuery, codeFields, limit, offset: 0 });
1563
+ return { id: req.id, command: req.command, status: 'success', timestamp: Date.now(), result };
1379
1564
  }
1380
1565
  // Generic REST passthrough. The escape hatch the typed commands are built
1381
1566
  // on: the browser helper's agentRestApi action has always accepted every