trackly-cli 0.7.2 → 0.8.1

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.
package/lib/agent.js CHANGED
@@ -7,15 +7,17 @@ const path = require('node:path');
7
7
  const { spawnSync } = require('node:child_process');
8
8
  const { apiRequest, downloadFile, getConfigPaths, hasAuth, maintenanceOutput } = require('./client');
9
9
  const { version: PACKAGE_VERSION } = require('../package.json');
10
- const SKILL_VERSION = '4.1.1';
10
+ const { contractVersion: MCP_CONTRACT_VERSION } = require('../contracts/trackly-apply-tools.json');
11
+ const SKILL_VERSION = '4.2.1';
11
12
  const CLI_USER_AGENT = `trackly-cli/${PACKAGE_VERSION}`;
12
13
  const MCP_USER_AGENT = `trackly-mcp/${PACKAGE_VERSION}`;
13
14
 
14
15
  const SKILL_NAME = 'trackly-apply';
15
16
  const SKILL_MAJOR = Number(SKILL_VERSION.split('.')[0]);
16
- const MIN_APPLY_PROTOCOL_VERSION = '3.1.0';
17
+ const MIN_APPLY_PROTOCOL_VERSION = '3.3.1';
17
18
  const CACHE_TTL_MS = 2 * 60 * 60 * 1000;
18
19
  const MANAGED_FILE = '.trackly-managed.json';
20
+ const SKILL_MANIFEST_VERSION = 1;
19
21
  const RESUME_PROOF_FILE = '.trackly-resume-proof.json';
20
22
  const RESUME_PROOF_KEY_BYTES = 32;
21
23
 
@@ -52,6 +54,43 @@ function copyDirectory(source, destination) {
52
54
  }
53
55
  }
54
56
 
57
+ function skillContentManifest(root) {
58
+ const files = [];
59
+
60
+ function collect(directory, relativeDirectory = '') {
61
+ const names = fs.readdirSync(directory).sort();
62
+ for (const name of names) {
63
+ if (!relativeDirectory && name === MANAGED_FILE) continue;
64
+ const absolutePath = path.join(directory, name);
65
+ const relativePath = relativeDirectory ? `${relativeDirectory}/${name}` : name;
66
+ const stat = fs.lstatSync(absolutePath);
67
+ if (stat.isDirectory()) {
68
+ collect(absolutePath, relativePath);
69
+ continue;
70
+ }
71
+ if (!stat.isFile()) {
72
+ throw new Error(`Trackly Apply skill contains an unsupported filesystem entry: ${relativePath}`);
73
+ }
74
+ const content = fs.readFileSync(absolutePath);
75
+ files.push({
76
+ path: relativePath,
77
+ sizeBytes: content.length,
78
+ sha256: crypto.createHash('sha256').update(content).digest('hex'),
79
+ });
80
+ }
81
+ }
82
+
83
+ collect(root);
84
+ const digest = crypto.createHash('sha256')
85
+ .update(JSON.stringify({ version: SKILL_MANIFEST_VERSION, files }))
86
+ .digest('hex');
87
+ return {
88
+ version: SKILL_MANIFEST_VERSION,
89
+ digest,
90
+ fileCount: files.length,
91
+ };
92
+ }
93
+
55
94
  function managedMetadata(extra = {}) {
56
95
  return JSON.stringify({ managedBy: 'trackly-cli', skill: SKILL_NAME, skillVersion: SKILL_VERSION, ...extra }, null, 2);
57
96
  }
@@ -80,7 +119,12 @@ function installCanonicalSkill() {
80
119
  const staging = `${destination}.staging.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
81
120
  try {
82
121
  copyDirectory(source, staging);
83
- fs.writeFileSync(path.join(staging, MANAGED_FILE), managedMetadata(), { mode: 0o600 });
122
+ const manifest = skillContentManifest(staging);
123
+ fs.writeFileSync(path.join(staging, MANAGED_FILE), managedMetadata({
124
+ skillManifestVersion: manifest.version,
125
+ skillContentDigest: manifest.digest,
126
+ skillFileCount: manifest.fileCount,
127
+ }), { mode: 0o600 });
84
128
  } catch (error) {
85
129
  if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true });
86
130
  throw error;
@@ -157,7 +201,13 @@ function linkClientSkill(client, canonical) {
157
201
  const staging = `${target}.staging.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
158
202
  try {
159
203
  copyDirectory(canonical, staging);
160
- fs.writeFileSync(path.join(staging, MANAGED_FILE), managedMetadata({ copiedFrom: canonical }), { mode: 0o600 });
204
+ const manifest = skillContentManifest(staging);
205
+ fs.writeFileSync(path.join(staging, MANAGED_FILE), managedMetadata({
206
+ copiedFrom: canonical,
207
+ skillManifestVersion: manifest.version,
208
+ skillContentDigest: manifest.digest,
209
+ skillFileCount: manifest.fileCount,
210
+ }), { mode: 0o600 });
161
211
  fs.renameSync(staging, target);
162
212
  if (backup) fs.rmSync(backup, { recursive: true, force: true });
163
213
  return { client, target, method: 'copy', status: 'installed' };
@@ -281,10 +331,47 @@ function inspectClient(client) {
281
331
  const target = clientSkillDir(client);
282
332
  const executable = commandExists(client === 'codex' ? 'codex' : 'claude');
283
333
  const metadata = managedSkillMetadata(target);
284
- const installed = fs.existsSync(path.join(target, 'SKILL.md')) && metadata?.skillVersion === SKILL_VERSION;
334
+ let expectedManifest = null;
335
+ let installedManifest = null;
336
+ try {
337
+ expectedManifest = skillContentManifest(bundledSkillDir());
338
+ installedManifest = skillContentManifest(target);
339
+ } catch (_) {}
340
+ const hasRecordedManifest = metadata?.skillManifestVersion === SKILL_MANIFEST_VERSION
341
+ && typeof metadata?.skillContentDigest === 'string'
342
+ && Number.isInteger(metadata?.skillFileCount);
343
+ const contentMatches = hasRecordedManifest
344
+ && expectedManifest !== null
345
+ && installedManifest !== null
346
+ && metadata.skillContentDigest === expectedManifest.digest
347
+ && metadata.skillContentDigest === installedManifest.digest
348
+ && metadata.skillFileCount === expectedManifest.fileCount
349
+ && metadata.skillFileCount === installedManifest.fileCount;
350
+ const skillIntegrity = contentMatches
351
+ ? 'verified'
352
+ : metadata && !hasRecordedManifest
353
+ ? 'manifest_missing'
354
+ : metadata
355
+ ? 'content_mismatch'
356
+ : 'not_managed';
357
+ const installed = fs.existsSync(path.join(target, 'SKILL.md'))
358
+ && metadata?.skillVersion === SKILL_VERSION
359
+ && skillIntegrity === 'verified';
285
360
  const mcpRegistration = mcpRegistrationState(client);
286
361
  const mcpRegistered = mcpRegistration === 'current';
287
- return { client, executable, installed, installedSkillVersion: metadata?.skillVersion || null, mcpRegistered, mcpRegistration, target };
362
+ return {
363
+ client,
364
+ executable,
365
+ installed,
366
+ installedSkillVersion: metadata?.skillVersion || null,
367
+ skillIntegrity,
368
+ expectedSkillContentDigest: expectedManifest?.digest || null,
369
+ installedSkillContentDigest: installedManifest?.digest || null,
370
+ recordedSkillContentDigest: metadata?.skillContentDigest || null,
371
+ mcpRegistered,
372
+ mcpRegistration,
373
+ target,
374
+ };
288
375
  }
289
376
 
290
377
  function cacheDir() {
@@ -368,11 +455,21 @@ function resumeCacheName(fileName, contentType, now = Date.now(), nonce = crypto
368
455
  return path.join(`${now}-${nonce}`, safeResumeName(fileName, contentType));
369
456
  }
370
457
 
371
- function resumeConfirmation(fileName, sha256, sizeBytes, exactLocalPath, runId, expiresAt, confirmationId) {
458
+ function resumeConfirmation(
459
+ fileName,
460
+ sha256,
461
+ sizeBytes,
462
+ exactLocalPath,
463
+ runId,
464
+ expiresAt,
465
+ confirmationId,
466
+ resumeId = null,
467
+ ) {
372
468
  return {
373
469
  required: true,
374
470
  source: 'trackly_default_resume',
375
471
  runId,
472
+ ...(resumeId == null ? {} : { resumeId }),
376
473
  confirmationId,
377
474
  fileName,
378
475
  sha256,
@@ -450,6 +547,10 @@ function verifyResumeProofSignature(manifest) {
450
547
  function materializeResume(download, options = {}) {
451
548
  const runId = Number(options.runId);
452
549
  if (!Number.isInteger(runId) || runId < 1) throw new Error('A valid Trackly Apply run ID is required before preparing a resume.');
550
+ const resumeId = download.resumeId == null ? null : Number(download.resumeId);
551
+ if (resumeId !== null && (!Number.isInteger(resumeId) || resumeId < 1)) {
552
+ throw new Error('Trackly returned an invalid default resume identity.');
553
+ }
453
554
  const now = options.now ?? Date.now();
454
555
  const nonce = options.nonce ?? crypto.randomBytes(4).toString('hex');
455
556
  const confirmationId = options.confirmationId ?? crypto.randomUUID();
@@ -471,11 +572,13 @@ function materializeResume(download, options = {}) {
471
572
  runId,
472
573
  expiresAt,
473
574
  confirmationId,
575
+ resumeId,
474
576
  );
475
577
  const manifestPath = path.join(path.dirname(destination), RESUME_PROOF_FILE);
476
578
  try {
477
579
  const manifest = signedResumeProof({
478
580
  runId,
581
+ resumeId,
479
582
  confirmationId,
480
583
  exactLocalPath: destination,
481
584
  fileName,
@@ -494,6 +597,7 @@ function materializeResume(download, options = {}) {
494
597
  }
495
598
  return {
496
599
  path: destination,
600
+ resumeId,
497
601
  fileName,
498
602
  contentType: validated.contentType,
499
603
  sha256: validated.digest,
@@ -542,6 +646,11 @@ function verifyPreparedResume(proof, now = Date.now()) {
542
646
  }
543
647
 
544
648
  const bindingMatches = manifest.runId === runId
649
+ && (
650
+ proof.resumeId == null
651
+ ? manifest.resumeId == null
652
+ : manifest.resumeId === Number(proof.resumeId)
653
+ )
545
654
  && manifest.confirmationId === proof.confirmationId
546
655
  && manifest.exactLocalPath === expectedPath
547
656
  && manifest.fileName === path.basename(expectedPath)
@@ -570,6 +679,7 @@ function verifyPreparedResume(proof, now = Date.now()) {
570
679
  return {
571
680
  verified: true,
572
681
  runId,
682
+ resumeId: manifest.resumeId ?? null,
573
683
  confirmationId: proof.confirmationId,
574
684
  exactLocalPath: expectedPath,
575
685
  sha256,
@@ -621,6 +731,9 @@ async function prepareResume(runId, browserSurface, browserBindingHash, userAgen
621
731
  if (Number(download.applyRunId) !== normalizedRunId) {
622
732
  throw new Error('Trackly did not confirm that the resume belongs to the active Apply run.');
623
733
  }
734
+ if (!Number.isInteger(Number(download.resumeId)) || Number(download.resumeId) < 1) {
735
+ throw new Error('Trackly did not confirm the default resume identity.');
736
+ }
624
737
  return materializeResume(download, { runId: normalizedRunId });
625
738
  } finally {
626
739
  download.buffer.fill(0);
@@ -631,7 +744,12 @@ function protocolAtLeast(version, minimum = MIN_APPLY_PROTOCOL_VERSION) {
631
744
  const parse = (value) => String(value || '').split('.').map((part) => Number(part));
632
745
  const actual = parse(version);
633
746
  const required = parse(minimum);
634
- if (actual.length < 2 || actual.some((part) => !Number.isInteger(part) || part < 0)) return false;
747
+ if (
748
+ actual.length < 2
749
+ || required.length < 2
750
+ || actual.some((part) => !Number.isInteger(part) || part < 0)
751
+ || required.some((part) => !Number.isInteger(part) || part < 0)
752
+ ) return false;
635
753
  for (let index = 0; index < Math.max(actual.length, required.length); index++) {
636
754
  const left = actual[index] || 0;
637
755
  const right = required[index] || 0;
@@ -640,6 +758,47 @@ function protocolAtLeast(version, minimum = MIN_APPLY_PROTOCOL_VERSION) {
640
758
  return true;
641
759
  }
642
760
 
761
+ function evaluateApplyCompatibility(protocol, clients) {
762
+ const compatibleSkillMajor = Number(protocol?.compatibleSkillMajor);
763
+ const minimumSkillVersion = typeof protocol?.compatibleSkillMinimumVersion === 'string'
764
+ ? protocol.compatibleSkillMinimumVersion
765
+ : null;
766
+ const minimumCliVersion = typeof protocol?.compatibleCliMinimumVersion === 'string'
767
+ ? protocol.compatibleCliMinimumVersion
768
+ : null;
769
+ const mcpContractCompatible = protocol?.mcpContractVersion === MCP_CONTRACT_VERSION;
770
+ const cliMinimumSatisfied = minimumCliVersion
771
+ ? protocolAtLeast(PACKAGE_VERSION, minimumCliVersion)
772
+ : false;
773
+ const protocolCompatible = compatibleSkillMajor === SKILL_MAJOR
774
+ && protocolAtLeast(protocol?.version, MIN_APPLY_PROTOCOL_VERSION);
775
+ const compatibleClientSkills = minimumSkillVersion
776
+ ? clients
777
+ .filter((client) => {
778
+ const installedMajor = Number(String(client.installedSkillVersion || '').split('.')[0]);
779
+ return client.installed === true
780
+ && installedMajor === compatibleSkillMajor
781
+ && protocolAtLeast(client.installedSkillVersion, minimumSkillVersion);
782
+ })
783
+ .map((client) => client.client)
784
+ : [];
785
+ const skillMinimumSatisfied = compatibleClientSkills.length > 0;
786
+ return {
787
+ protocolCompatible,
788
+ mcpContractVersion: MCP_CONTRACT_VERSION,
789
+ mcpContractCompatible,
790
+ minimumCliVersion,
791
+ cliMinimumSatisfied,
792
+ minimumSkillVersion,
793
+ compatibleClientSkills,
794
+ skillMinimumSatisfied,
795
+ compatible: protocolCompatible
796
+ && mcpContractCompatible
797
+ && cliMinimumSatisfied
798
+ && skillMinimumSatisfied,
799
+ };
800
+ }
801
+
643
802
  function liveBrowserReady(browserControl) {
644
803
  return (browserControl?.codex === true && browserControl?.codexComputerUse === true)
645
804
  || browserControl?.claude === true;
@@ -654,13 +813,38 @@ async function doctorAgent() {
654
813
  codexComputerUse,
655
814
  claude: null,
656
815
  };
816
+ const tabInventory = {
817
+ controllerOwnedTabs: 'runtime_verification_required',
818
+ userOwnedTabs: 'runtime_verification_required',
819
+ unionReconciliation: 'runtime_verification_required',
820
+ detectedSurfaces: [],
821
+ unionInventoryDetected: false,
822
+ requiredForClosedClaim: true,
823
+ note: 'Installed plugins do not prove controller-owned and user-owned tab union inventory. The active browser client must verify both inventories before claiming a tab is closed.',
824
+ };
825
+ const clients = ['codex', 'claude'].map(inspectClient);
826
+ const skillIntegrityFailures = clients
827
+ .filter((client) => (
828
+ client.installedSkillVersion === SKILL_VERSION
829
+ && client.skillIntegrity !== 'verified'
830
+ ))
831
+ .map((client) => ({ client: client.client, integrity: client.skillIntegrity }));
657
832
  const report = {
833
+ cliVersion: PACKAGE_VERSION,
834
+ mcpContractVersion: MCP_CONTRACT_VERSION,
658
835
  skillVersion: SKILL_VERSION,
659
836
  authenticated: hasAuth(),
660
- clients: ['codex', 'claude'].map(inspectClient),
837
+ clients,
838
+ skillPackIntegrity: {
839
+ ok: skillIntegrityFailures.length === 0,
840
+ expectedDigest: clients.find((client) => client.expectedSkillContentDigest)
841
+ ?.expectedSkillContentDigest || null,
842
+ failures: skillIntegrityFailures,
843
+ },
661
844
  browserControl: {
662
845
  ...browserControl,
663
846
  semanticReady: liveBrowserReady(browserControl),
847
+ tabInventory,
664
848
  note: 'A live run requires both semantic browser control and computer use. Claude capability must be verified by the active client before the run starts.',
665
849
  },
666
850
  cache,
@@ -682,8 +866,8 @@ async function doctorAgent() {
682
866
  completeness: profile.profile?.completeness,
683
867
  hasDefaultResume: Boolean(profile.profile?.defaultResume),
684
868
  };
685
- report.compatible = Number(report.protocol.compatibleSkillMajor) === SKILL_MAJOR
686
- && protocolAtLeast(report.protocol.version, MIN_APPLY_PROTOCOL_VERSION);
869
+ report.compatibility = evaluateApplyCompatibility(report.protocol, report.clients);
870
+ report.compatible = report.compatibility.compatible;
687
871
  if (report.profile.hasDefaultResume) {
688
872
  report.resume = {
689
873
  available: true,
@@ -697,6 +881,7 @@ async function doctorAgent() {
697
881
  report.ok = report.authenticated
698
882
  && report.compatible
699
883
  && report.browserControl.semanticReady
884
+ && report.skillPackIntegrity.ok
700
885
  && report.clients.some((client) => client.executable && client.installed && client.mcpRegistered)
701
886
  && Boolean(report.profile?.hasDefaultResume)
702
887
  && report.resume.available
@@ -724,6 +909,7 @@ module.exports = {
724
909
  cleanResumeCache,
725
910
  clientSkillDir,
726
911
  doctorAgent,
912
+ evaluateApplyCompatibility,
727
913
  inspectClient,
728
914
  liveBrowserReady,
729
915
  materializeResume,
package/lib/client.js CHANGED
@@ -18,6 +18,22 @@ const MAINTENANCE_APPLY_GUIDANCE =
18
18
  'Do not blindly retry an Apply mutation. Wait for the advertised maintenance window, ' +
19
19
  'refetch the Apply protocol and application profile, then resume the existing agent_browser run. ' +
20
20
  'Never create a duplicate run or click Submit.';
21
+ const TRACKLY_ACCESS_GUIDANCE =
22
+ 'Trackly is in a limited rollout. Existing members can sign in; new members need a private invite. ' +
23
+ 'Request access at https://usetrackly.app/early-access.';
24
+ const TRACKLY_ACCESS_PENDING_GUIDANCE =
25
+ 'Your Trackly access request is already pending. No new request is needed.';
26
+ const TRACKLY_ACCESS_UNAVAILABLE_GUIDANCE =
27
+ 'Trackly access could not be verified right now. Please try again.';
28
+ const TRACKLY_ACCESS_CODES = new Set([
29
+ 'INVITATION_REQUIRED',
30
+ 'INVITATION_INVALID',
31
+ 'INVITATION_REDEEMED',
32
+ 'SIGNUP_INTENT_EXPIRED',
33
+ 'ACCESS_BATCH_FULL',
34
+ 'ACCESS_CHECK_UNAVAILABLE',
35
+ 'ALREADY_REQUESTED',
36
+ ]);
21
37
 
22
38
  class TracklyConfigError extends Error {
23
39
  constructor(message) {
@@ -243,6 +259,27 @@ function nestedRecord(value) {
243
259
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
244
260
  }
245
261
 
262
+ function createTracklyAccessError(json, status) {
263
+ const record = nestedRecord(json);
264
+ const code = cleanText(record.code);
265
+ if (!code || !TRACKLY_ACCESS_CODES.has(code.toUpperCase())) return null;
266
+ const normalizedCode = code.toUpperCase();
267
+ const retryable = normalizedCode === 'ACCESS_CHECK_UNAVAILABLE';
268
+ const message = retryable
269
+ ? TRACKLY_ACCESS_UNAVAILABLE_GUIDANCE
270
+ : normalizedCode === 'ALREADY_REQUESTED'
271
+ ? TRACKLY_ACCESS_PENDING_GUIDANCE
272
+ : TRACKLY_ACCESS_GUIDANCE;
273
+ return {
274
+ ...record,
275
+ status: status ?? (retryable ? 503 : 403),
276
+ code: normalizedCode,
277
+ error: message,
278
+ message,
279
+ retryable,
280
+ };
281
+ }
282
+
246
283
  // REST maintenance responses and hosted MCP JSON-RPC errors intentionally use
247
284
  // different envelopes. Collapse both into the same source view so every CLI
248
285
  // surface preserves equivalent code/retry/ETA/request context.
@@ -472,7 +509,15 @@ async function _doRefresh() {
472
509
  return null;
473
510
  }
474
511
 
475
- function apiRequest(method, endpoint, data = null, skipAuth = false, _isRetry = false, userAgent = null) {
512
+ function apiRequest(
513
+ method,
514
+ endpoint,
515
+ data = null,
516
+ skipAuth = false,
517
+ _isRetry = false,
518
+ userAgent = null,
519
+ additionalHeaders = null
520
+ ) {
476
521
  return new Promise((resolve, reject) => {
477
522
  let url;
478
523
  try {
@@ -485,6 +530,27 @@ function apiRequest(method, endpoint, data = null, skipAuth = false, _isRetry =
485
530
  const isHttps = url.protocol === 'https:';
486
531
  const lib = isHttps ? https : http;
487
532
  const headers = getRequestHeaders(skipAuth, userAgent);
533
+ if (additionalHeaders !== null) {
534
+ const entries = (
535
+ typeof additionalHeaders === 'object'
536
+ && additionalHeaders !== null
537
+ && !Array.isArray(additionalHeaders)
538
+ && Object.getPrototypeOf(additionalHeaders) === Object.prototype
539
+ )
540
+ ? Object.entries(additionalHeaders)
541
+ : [];
542
+ const idempotencyEntry = entries.find(([name]) => name.toLowerCase() === 'idempotency-key');
543
+ if (
544
+ entries.length !== 1
545
+ || !idempotencyEntry
546
+ || typeof idempotencyEntry[1] !== 'string'
547
+ || !/^[\x20-\x7e]{16,200}$/.test(idempotencyEntry[1])
548
+ ) {
549
+ reject(new Error('Only a valid Idempotency-Key additional header is permitted.'));
550
+ return;
551
+ }
552
+ headers['Idempotency-Key'] = idempotencyEntry[1];
553
+ }
488
554
  const authMethod = getAuthMethod();
489
555
  const timeoutMs = getRequestTimeoutMs();
490
556
  const hasSensitivePayload = Boolean(data && typeof data === 'object' && 'refreshToken' in data);
@@ -567,7 +633,15 @@ function apiRequest(method, endpoint, data = null, skipAuth = false, _isRetry =
567
633
  }
568
634
  if (newToken) {
569
635
  try {
570
- const retried = await apiRequest(method, endpoint, data, false, true, userAgent);
636
+ const retried = await apiRequest(
637
+ method,
638
+ endpoint,
639
+ data,
640
+ false,
641
+ true,
642
+ userAgent,
643
+ additionalHeaders
644
+ );
571
645
  return resolve(retried);
572
646
  } catch (e) { return reject(e); }
573
647
  }
@@ -579,6 +653,8 @@ function apiRequest(method, endpoint, data = null, skipAuth = false, _isRetry =
579
653
  headers: res.headers,
580
654
  });
581
655
  if (maintenanceError) return reject(maintenanceError);
656
+ const accessError = createTracklyAccessError(json, res.statusCode);
657
+ if (accessError) return reject(accessError);
582
658
  // Spread `json` FIRST so the HTTP statusCode always wins. A backend that returns
583
659
  // `{"status": "error"}` in the body would otherwise clobber the numeric status with
584
660
  // a string, breaking downstream branches like `typeof e.status === 'number'` in
@@ -676,6 +752,11 @@ function downloadFile(endpoint, userAgent = null, _isRetry = false) {
676
752
  reject(maintenanceError);
677
753
  return;
678
754
  }
755
+ const accessError = createTracklyAccessError(body, res.statusCode);
756
+ if (accessError) {
757
+ reject(accessError);
758
+ return;
759
+ }
679
760
  reject({ ...body, status: res.statusCode, message: body.error || `HTTP ${res.statusCode}` });
680
761
  return;
681
762
  }
@@ -686,6 +767,7 @@ function downloadFile(endpoint, userAgent = null, _isRetry = false) {
686
767
  fileName: contentDispositionFileName(disposition),
687
768
  sha256: res.headers['x-trackly-file-sha256'] || null,
688
769
  applyRunId: res.headers['x-trackly-apply-run-id'] || null,
770
+ resumeId: res.headers['x-trackly-resume-id'] || null,
689
771
  });
690
772
  });
691
773
  });
@@ -720,6 +802,7 @@ module.exports = {
720
802
  TracklyConfigError,
721
803
  MAINTENANCE_APPLY_GUIDANCE,
722
804
  MAINTENANCE_CODE,
805
+ TRACKLY_ACCESS_GUIDANCE,
723
806
  apiRequest,
724
807
  downloadFile,
725
808
  clearConfig,
@@ -738,6 +821,7 @@ module.exports = {
738
821
  hasAuth,
739
822
  loadConfig,
740
823
  createMaintenanceError,
824
+ createTracklyAccessError,
741
825
  isMaintenanceError,
742
826
  maintenanceOutput,
743
827
  normalizeMaintenance,