trackly-cli 0.13.4 → 0.14.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  [![npm](https://img.shields.io/npm/v/trackly-cli.svg)](https://www.npmjs.com/package/trackly-cli)
2
2
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3
- [![Node 20+](https://img.shields.io/badge/node-20+-brightgreen.svg)](https://nodejs.org/)
3
+ [![Node 20.20+ or 22.22+](https://img.shields.io/badge/node-20.20%2B%20%7C%2022.22%2B-brightgreen.svg)](https://nodejs.org/)
4
4
  [![MCP Server](https://img.shields.io/badge/MCP-Server-blue.svg)](https://modelcontextprotocol.io/)
5
5
 
6
6
  # trackly-cli
@@ -31,11 +31,11 @@ trackly login
31
31
  trackly jobs --function product
32
32
  ```
33
33
 
34
- > **Prerequisites:** [Node.js 20+](https://nodejs.org/) (LTS recommended). On macOS with the official `.pkg` installer, global npm installs may require `sudo`.
34
+ > **Prerequisites:** [Node.js 20.20+ or 22.22+](https://nodejs.org/) (LTS recommended). On macOS with the official `.pkg` installer, global npm installs may require `sudo`.
35
35
 
36
36
  ## At a Glance
37
37
 
38
- 3,800+ companies | 170K+ jobs | 40+ ATS types | CLI + MCP | 48 local MCP tools
38
+ 3,800+ companies | 170K+ jobs | 40+ ATS types | CLI + MCP | 55 local MCP tools
39
39
 
40
40
  ## CLI Commands
41
41
 
@@ -181,6 +181,10 @@ Then use natural language in any of these clients:
181
181
  | trackly_start_apply_execution | Start a target-counted accessible Apply execution |
182
182
  | trackly_get_active_apply_execution | Recover the active execution before legacy batch recovery |
183
183
  | trackly_get_apply_execution | Read the authoritative progress funnel and immutable child waves |
184
+ | trackly_list_recoverable_apply_executions | List bounded exact-member recovery candidates after context loss |
185
+ | trackly_recover_exact_apply_members | Recover only one explicitly confirmed candidate set without substitutes |
186
+ | trackly_list_apply_review_handoffs | Rediscover active, value-free review-handoff receipts for one execution |
187
+ | trackly_claim_apply_review_handoff | Claim and classify one exact review-handoff group before reconciliation |
184
188
  | trackly_get_apply_execution_snapshot | Fetch a compact bounded projection for current execution members and required profile keys |
185
189
  | trackly_resume_parked_apply_member | Explicitly resume one parked member for a fresh non-mutating access probe |
186
190
  | trackly_approve_apply_execution_resume | Approve one exact resume identity for an unchanged execution snapshot |
@@ -204,11 +208,14 @@ Then use natural language in any of these clients:
204
208
  | trackly_report_apply_observation | Send redacted ATS mechanics feedback |
205
209
  | trackly_lint_application_text | Locally lint application writing and return only value-free violations plus a draft hash |
206
210
  | trackly_diagnose_local_path | Locally diagnose the exact implicated filesystem path without deleting files |
211
+ | trackly_validate_apply_tab_keep_set | Locally validate an exact expected/keep tab set against complete caller-supplied inventories without controlling the browser or sending tab IDs remotely |
212
+ | trackly_validate_apply_resume_upload | Locally validate adapter capability and ordered value-free attachment proof stages |
207
213
  | trackly_report_apply_observations | Bulk-send up to 20 leased, batch-bound redacted observations |
208
214
  | trackly_record_application_outcome | Record review or confirmed submission outcome |
209
215
  | trackly_record_application_outcomes | Bulk-record up to 20 leased batch outcomes with per-member conflicts |
210
216
  | trackly_prepare_resume | Prepare a private expiring resume file for upload |
211
217
  | trackly_verify_prepared_resume | Recheck the confirmed resume immediately before attachment |
218
+ | get_more_tools | Report a missing capability so Trackly can improve its MCP surface |
212
219
 
213
220
  ## Authentication
214
221
 
@@ -274,7 +281,7 @@ trackly config --base-url http://127.0.0.1:3000 # Point at a different backend
274
281
  | Job search + filters | Yes | Yes | Yes |
275
282
  | Apply/save/dismiss | Yes | Yes | Yes |
276
283
  | AI-powered search | Yes (trackly ask) | Yes | Yes |
277
- | MCP integration | Yes (48 local tools) | -- | -- |
284
+ | MCP integration | Yes (55 local tools) | -- | -- |
278
285
  | Browser required | No | Yes | No |
279
286
  | Best for | Terminal + AI agents | Visual browsing | Custom integrations |
280
287
 
@@ -288,7 +295,18 @@ Install trackly-cli (`npm install -g trackly-cli`), authenticate with `trackly l
288
295
 
289
296
  **What MCP servers exist for job searching?**
290
297
 
291
- trackly-cli includes a built-in MCP server with 48 tools for job search, company lookup, discovery preferences, application tracking, accessible execution and frozen-batch orchestration, profile onboarding, beta evidence, and manual-submit form preparation. Run `trackly mcp` or use `trackly agent setup --client claude`.
298
+ trackly-cli includes a built-in MCP server with 55 tools: the complete Trackly
299
+ job-search and application set plus `get_more_tools`, which lets agents report
300
+ a missing capability. Run `trackly mcp` or use
301
+ `trackly agent setup --client claude`.
302
+
303
+ MCP usage analytics are on by default and relay through Trackly so the free
304
+ service can be debugged and improved. They exclude résumé text, profile answers,
305
+ demographic or work-authorization answers, and application notes. Turn off future
306
+ collection with **Share usage analytics** in Account Settings. Operators may also
307
+ disable local MCP instrumentation with `TRACKLY_MCP_ANALYTICS_DISABLED=1` (or
308
+ `TRACKLY_MCP_ANALYTICS_ENABLED=0`); the account setting remains authoritative
309
+ for authenticated backend capture.
292
310
 
293
311
  **How do I use Claude Code for job hunting?**
294
312
 
package/bin/trackly CHANGED
@@ -4,7 +4,6 @@
4
4
  const {
5
5
  DEFAULT_BASE_URL,
6
6
  apiRequest,
7
- clearConfig,
8
7
  createTracklyAccessError,
9
8
  getApiKey,
10
9
  getAuthMethod,
@@ -13,6 +12,7 @@ const {
13
12
  hasAuth,
14
13
  maintenanceOutput,
15
14
  loadConfig,
15
+ removeCredentials,
16
16
  saveConfig,
17
17
  } = require('../lib/client');
18
18
  const { color, isJSON, outputJobs, outputCompanies, outputStats, outputContacts, outputReferralCampaign, outputNetworkBrief, outputCompanyBrief, outputCompanyWorkspace, outputError } = require('../lib/formatters');
@@ -544,7 +544,7 @@ p{color:#a1a1aa;font-size:15px;line-height:1.5}
544
544
  }
545
545
 
546
546
  async function cmdLogout() {
547
- clearConfig();
547
+ await removeCredentials({ scope: 'all' });
548
548
  console.log(color('green', 'Logged out. Credentials cleared.'));
549
549
  }
550
550
 
@@ -1055,11 +1055,12 @@ async function cmdConfig(args) {
1055
1055
  failNow('Cannot use --base-url and --clear-base-url together.');
1056
1056
  }
1057
1057
 
1058
- const config = loadConfig();
1058
+ let config = loadConfig();
1059
1059
  let updated = false;
1060
1060
 
1061
1061
  if (args['clear-api-key']) {
1062
- delete config.apiKey;
1062
+ await removeCredentials({ scope: 'apiKey' });
1063
+ config = loadConfig();
1063
1064
  updated = true;
1064
1065
  console.log(color('green', 'Stored API key cleared.'));
1065
1066
  }
@@ -1123,8 +1124,10 @@ async function cmdVersion() {
1123
1124
  console.log(VERSION);
1124
1125
  }
1125
1126
 
1126
- async function cmdMcp() {
1127
- await require('../mcp/server').startMcpServer();
1127
+ async function cmdMcp(options = {}) {
1128
+ const mcpServer = options.mcpServer || require('../mcp/server');
1129
+ const server = await mcpServer.startMcpServer(options.startOptions);
1130
+ return mcpServer.installMcpSignalHandlers(server, options.signalOptions);
1128
1131
  }
1129
1132
 
1130
1133
  async function cmdAgent(args) {
@@ -1504,6 +1507,7 @@ if (require.main === module) {
1504
1507
  module.exports = {
1505
1508
  COMMAND_FLAGS,
1506
1509
  UNIVERSAL_FLAGS,
1510
+ cmdMcp,
1507
1511
  cmdLogin,
1508
1512
  formatMaintenanceForHuman,
1509
1513
  getPublicConfig,
@@ -6,9 +6,16 @@
6
6
  "contractSha256": "8c52daa8b62095a6431d5b954505079c3f7c802e5c5ae39efa8ea7e988dfbc7a"
7
7
  },
8
8
  "3.6.2": {
9
- "contractSha256": "83e107ad7ae2abdc42cdacb9848d8e3bf345951ed7b8f42d5cd9b5e203528bf2"
9
+ "contractSha256": "83e107ad7ae2abdc42cdacb9848d8e3bf345951ed7b8f42d5cd9b5e203528bf2",
10
+ "toolsSha256": "d9d24da4b79a4cc65aec589debd2b9072e41b309fd6bf6a65e95dd4b5bef3c08"
10
11
  },
11
12
  "3.6.3": {
12
13
  "contractSha256": "93a8e55196c14f04afb923711997eacdc26dbdeae50b253b906d547abd346e30"
14
+ },
15
+ "3.7.0": {
16
+ "toolsSha256": "8380e2a9fabef4da18ea731f55c8994aef5a4738bb6e7daef9be4ce6ebfec64f"
17
+ },
18
+ "3.7.1": {
19
+ "contractSha256": "0eb582fcac72804347c5da53d214a67908f657f600536db7b42167350bb3f3ab"
13
20
  }
14
21
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "contractVersion": "3.6.3",
2
+ "contractVersion": "3.7.1",
3
3
  "constants": {
4
4
  "applyExecutionMaxTarget": 20,
5
5
  "applyScenarioCodes": ["browser_reclaim", "resume_upload", "resume_parser_recheck", "semantic_boolean_commit", "custom_select_commit", "multi_step_navigation", "free_text_voice", "required_error_sweep", "final_consent", "handoff_reclaim", "critical_contact_integrity", "manual_submit_boundary", "job_identity_match"],
@@ -16,7 +16,12 @@
16
16
  "applySurfaceEvidenceTypes": ["surface_inventory_reconciled", "surface_missing", "surface_close_attempted", "surface_close_receipt", "surface_post_close_absent", "surface_close_failed"],
17
17
  "applySurfaceOwnershipStates": ["controller_owned", "user_owned", "controller_user_union", "unresolved"],
18
18
  "applySubmissionEvidenceTypes": ["request_accepted", "confirmation_detected", "provider_receipt_detected"],
19
- "applySubmissionEvidenceSources": ["submission_request", "success_page", "user_confirmation", "provider_receipt", "post_submit_receipt"]
19
+ "applySubmissionEvidenceSources": ["submission_request", "success_page", "user_confirmation", "provider_receipt", "post_submit_receipt"],
20
+ "applyPositiveSubmissionStrategyCodes": ["greenhouse_confirmation_redirect", "greenhouse_semantic_success", "ashby_semantic_success_banner", "ashby_submitted_state", "lever_confirmation_redirect", "lever_semantic_confirmation"],
21
+ "applyExecutionRecoveryEligibilityCodes": ["recoverable", "already_applied", "inactive", "revoked", "changed_requisition", "concurrently_owned", "lineage_unavailable", "retention_expired"],
22
+ "applyHandoffReconciliationClassifications": ["detected", "user_confirmed", "unresolved", "contradictory"],
23
+ "applyUploadStages": ["semantic_control_discovered", "file_chooser_armed", "file_chooser_opened", "set_files_succeeded", "user_facing_filename_committed", "parser_fields_rechecked"],
24
+ "applyUploadFailureCodes": ["upload_capability_unavailable", "upload_stage_missing", "upload_stage_out_of_order", "upload_stage_duplicate", "control_ambiguous", "chooser_timeout", "chooser_open_without_commit", "file_missing", "file_expired", "set_files_failed", "filename_uncommitted", "unexpected_file_navigation", "parser_unsettled", "parser_field_regression"]
20
25
  },
21
26
  "tools": {
22
27
  "trackly_get_apply_queue": "{limit:z.number().int().min(1).max(100).optional(),cursor:z.string().min(1).max(2048).optional()}",
@@ -26,6 +31,10 @@
26
31
  "trackly_start_apply_execution": "{mode:z.literal('complete_next_n_accessible'),target:z.number().int().min(1).max(APPLY_EXECUTION_MAX_TARGET),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
27
32
  "trackly_get_active_apply_execution": "{}",
28
33
  "trackly_get_apply_execution": "{executionId:z.number().int().min(1)}",
34
+ "trackly_list_recoverable_apply_executions": "{}",
35
+ "trackly_recover_exact_apply_members": "{sourceExecutionId:z.number().int().min(1),sourceSnapshotHash:z.string().regex(/^[a-f0-9]{64}$/),candidateIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values).size===values.length,{message:'candidateIdsmustbeunique'}),explicitExactSetConfirmation:z.literal(true),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
36
+ "trackly_list_apply_review_handoffs": "{executionId:z.number().int().min(1)}",
37
+ "trackly_claim_apply_review_handoff": "{handoffId:z.number().int().min(1),members:z.array(z.object({memberId:z.number().int().min(1),classification:z.enum(APPLY_HANDOFF_RECONCILIATION_CLASSIFICATION_CODES)}).strict()).min(1).max(APPLY_EXECUTION_MAX_TARGET).refine((values)=>newSet(values.map(({memberId})=>memberId)).size===values.length,{message:'membersmustcontainuniquememberIdvalues'}),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
29
38
  "trackly_get_apply_execution_snapshot": "{executionId:z.number().int().min(1),memberIds:z.array(z.number().int().min(1)).min(1).max(APPLY_EXECUTION_MAX_TARGET),profileKeys:z.array(z.string().min(1).max(200)).max(100).optional(),browserSurface:z.enum(APPLY_BROWSER_SURFACES)}",
30
39
  "trackly_resume_parked_apply_member": "{executionId:z.number().int().min(1),memberId:z.number().int().min(1),expectedRevision:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),explicitUserResume:z.literal(true),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
31
40
  "trackly_approve_apply_execution_resume": "{executionId:z.number().int().min(1),expectedRevision:z.number().int().min(1),originalSnapshotHash:z.string().regex(/^[a-f0-9]{64}$/),profileRevision:z.number().int().min(1),resumeId:z.number().int().min(1),resumeSha256:z.string().regex(/^[a-f0-9]{64}$/),resumeFilename:z.string().min(1).max(255),resumeSizeBytes:z.number().int().min(1),expiresAt:z.string().datetime(),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
@@ -48,11 +57,13 @@
48
57
  "trackly_get_apply_protocol": "{}",
49
58
  "trackly_report_apply_observation": "{runId:z.number().int().min(1),batchId:z.number().int().min(1).optional(),memberId:z.number().int().min(1).optional(),inspectionEpoch:z.number().int().min(0).optional(),leaseToken:z.string().min(1).max(1024).optional(),provider:z.string().regex(SAFE_OBSERVATION_CODE),fieldLabel:z.string().min(1).max(1000),observationType:z.string().regex(SAFE_OBSERVATION_CODE),resolutionCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),metadata:z.object({controlType:z.string().regex(SAFE_OBSERVATION_CODE).optional(),required:z.boolean().optional(),errorCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),committed:z.boolean(),scenarioCode:z.enum(APPLY_SCENARIO_CODES),browserSurface:z.enum(APPLY_BROWSER_SURFACES),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/).optional(),resumedAfterHandoff:z.boolean().optional()})}",
50
59
  "trackly_report_apply_observations": "{observations:z.array(z.object({runId:z.number().int().min(1),batchId:z.number().int().min(1),memberId:z.number().int().min(1),inspectionEpoch:z.number().int().min(0),leaseToken:z.string().min(1).max(1024),provider:z.string().regex(SAFE_OBSERVATION_CODE),fieldLabel:z.string().min(1).max(1000),observationType:z.string().regex(SAFE_OBSERVATION_CODE),resolutionCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),metadata:z.object({controlType:z.string().regex(SAFE_OBSERVATION_CODE).optional(),required:z.boolean().optional(),errorCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),committed:z.boolean(),scenarioCode:z.enum(APPLY_SCENARIO_CODES),browserSurface:z.enum(APPLY_BROWSER_SURFACES),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/).optional(),resumedAfterHandoff:z.boolean().optional()})})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS)}",
51
- "trackly_record_application_outcome": "{runId:z.number().int().min(1),batchId:z.number().int().min(1).optional(),memberId:z.number().int().min(1).optional(),inspectionEpoch:z.number().int().min(0).optional(),leaseToken:z.string().min(1).max(1024).optional(),outcome:z.enum(['review_ready','submitted','failed','blocked']),confirmation:z.enum(['user_confirmation','success_page']).optional()}",
52
- "trackly_record_application_outcomes": "{outcomes:z.array(z.object({runId:z.number().int().min(1),batchId:z.number().int().min(1),memberId:z.number().int().min(1),inspectionEpoch:z.number().int().min(0),leaseToken:z.string().min(1).max(1024),outcome:z.enum(['review_ready','submitted','failed','blocked']),confirmation:z.enum(['user_confirmation','success_page']).optional()})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS)}",
60
+ "trackly_record_application_outcome": "{runId:z.number().int().min(1),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),batchId:z.number().int().min(1).optional(),memberId:z.number().int().min(1).optional(),inspectionEpoch:z.number().int().min(0).optional(),leaseToken:z.string().min(1).max(1024).optional(),outcome:z.enum(['review_ready','submitted','failed','blocked']),confirmation:z.enum(['user_confirmation','success_page']).optional()}",
61
+ "trackly_record_application_outcomes": "{idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),outcomes:z.array(z.object({runId:z.number().int().min(1),batchId:z.number().int().min(1),memberId:z.number().int().min(1),inspectionEpoch:z.number().int().min(0),leaseToken:z.string().min(1).max(1024),outcome:z.enum(['review_ready','submitted','failed','blocked']),confirmation:z.enum(['user_confirmation','success_page']).optional()})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS).refine((values)=>newSet(values.map(({runId})=>runId)).size===values.length,{message:'outcomesmustcontainuniquerunIdvalues'})}",
53
62
  "trackly_prepare_resume": "{runId:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/)}",
54
63
  "trackly_lint_application_text": "{text:z.string().max(20000),emDashPolicy:z.enum(['forbid','allow_if_voice_sample','allow']).optional(),voiceSampleAllowsEmDash:z.boolean().optional(),prohibitedPhrases:z.array(z.string().min(1).max(200)).max(50).optional(),minLength:z.number().int().min(0).max(20000).optional(),maxLength:z.number().int().min(0).max(20000).optional(),claims:z.array(z.object({claimFingerprint:z.string().regex(/^[a-f0-9]{64}$/),evidenceRefs:z.array(z.string().regex(SAFE_OBSERVATION_CODE)).max(20)}).strict()).max(100),claimsComplete:z.literal(true)}",
55
64
  "trackly_diagnose_local_path": "{exactPath:z.string().min(1).max(4096),originalErrno:z.string().regex(ERRNO_PATTERN).optional()}",
65
+ "trackly_validate_apply_tab_keep_set": "{expectedTabIds:z.array(opaqueTabIdSchema).min(1).max(100),keepTabIds:z.array(opaqueTabIdSchema).max(100),controllerInventory:z.object({complete:z.boolean(),tabIds:z.array(opaqueTabIdSchema).max(1000)}).strict(),userInventory:z.object({complete:z.boolean(),tabIds:z.array(opaqueTabIdSchema).max(1000)}).strict()}",
66
+ "trackly_validate_apply_resume_upload": "{capabilities:z.object({semanticControlDiscovery:z.boolean(),chooserArming:z.boolean(),fileAttachment:z.boolean(),committedFilenameInspection:z.boolean(),parserFieldRecheck:z.boolean()}).strict(),events:z.array(z.object({stage:z.enum(APPLY_UPLOAD_STAGES),outcome:z.enum(['passed','failed']),failureCode:z.enum(APPLY_UPLOAD_FAILURE_CODES).optional()}).strict()).max(APPLY_UPLOAD_STAGES.length)}",
56
67
  "trackly_verify_prepared_resume": {
57
68
  "local": "{runId:z.number().int().min(1),resumeId:z.number().int().min(1),confirmationId:z.string().min(1).max(200),exactLocalPath:z.string().min(1).max(4096),sha256:z.string().regex(/^[a-f0-9]{64}$/i),sizeBytes:z.number().int().min(1),expiresAt:z.string().datetime()}",
58
69
  "hosted": "{runId:z.number().int().min(1),confirmationId:z.string().min(1).max(200)}"
@@ -72,6 +72,7 @@ API-key creation attempts.
72
72
 
73
73
  ### Available Tools
74
74
 
75
+ - **get_more_tools** — Report what the agent wanted to do when no Trackly tool fit, so Trackly can improve its MCP surface. The tool is always available; report delivery is best-effort and occurs only when usage analytics are enabled.
75
76
  - **trackly_search_jobs** — Search/filter jobs.
76
77
  - `function` (14 values): `product`, `engineering`, `design`, `data`, `marketing`, `sales`, `partnerships`, `finance`, `strategy`, `operations`, `people`, `legal`, `support`, `other`
77
78
  - `locationFilter`: pass ONE of
@@ -106,6 +107,10 @@ API-key creation attempts.
106
107
  - **trackly_start_apply_execution** — Start a server-owned `complete_next_n_accessible` execution with a target from 1–20, then consume the returned authoritative `progress` and `nextAction` before doing browser work.
107
108
  - **trackly_get_active_apply_execution** — Recover the active execution before legacy batch recovery.
108
109
  - **trackly_get_apply_execution** — Read an execution's latest current-wave identity and authoritative aggregate progress funnel.
110
+ - **trackly_list_recoverable_apply_executions** — List a bounded, value-free menu of exact recovery candidates after local context loss.
111
+ - **trackly_recover_exact_apply_members** — Recover only the explicitly confirmed candidates from one retained source snapshot, with no substitutions.
112
+ - **trackly_list_apply_review_handoffs** — Rediscover active, nonexpired review-handoff receipts for one execution without browser or profile values.
113
+ - **trackly_claim_apply_review_handoff** — Claim one exact handoff receipt and classify every member before grouped submission reconciliation.
109
114
  - **trackly_get_apply_execution_snapshot**: Fetch a compact bounded projection of current members, requested profile keys, mutability, allowed operations, milestones, lease timing, and the authoritative funnel.
110
115
  - **trackly_resume_parked_apply_member**: Resume one parked member only after explicit user instruction; a fresh non-mutating access probe remains required before form mutation.
111
116
  - **trackly_approve_apply_execution_resume**: Approve one exact resume identity for an execution's unchanged original snapshot while preserving immediate per-run local verification before upload.
@@ -134,9 +139,16 @@ API-key creation attempts.
134
139
  - **trackly_verify_prepared_resume** — Local MCP only: immediately before attachment, recompute the user-confirmed resume hash and size, validate the exact path/run/expiration, and lock the file read-only. Any mismatch requires a fresh preview and confirmation.
135
140
  - **trackly_lint_application_text**: Local MCP only: require an explicitly complete claim-reference packet, then return a draft hash, length, and stable value-free writing violations without sending or echoing application text to Trackly.
136
141
  - **trackly_diagnose_local_path**: Local MCP only: measure the exact implicated path's filesystem, inode, quota observability, and write-probe result without deleting user files or claiming a global disk cause.
142
+ - **trackly_validate_apply_tab_keep_set**: Local MCP only: fail closed unless a caller-supplied nonempty expected tab set exactly matches the keep set and is covered by complete controller and user inventories. This pure helper allows unrelated tabs, performs no browser action, and never sends tab IDs to Trackly.
143
+ - **trackly_validate_apply_resume_upload**: Local MCP only: validate the browser adapter's capabilities and ordered, value-free attachment proof stages without opening a chooser, reading a path, controlling a browser, or sending form data to Trackly.
137
144
 
138
145
  Apply contract v3 intentionally gives this verifier different local and hosted schemas: local MCP receives the full proof needed to inspect the private file, while hosted MCP accepts only run and confirmation identifiers and returns the manual/local-agent requirement. Local paths are never sent remotely. Resume fingerprints are sent only to authenticated Trackly resume approval and truth-certification endpoints, never observations or employer forms. Version 3.1 also records universal value-free evidence for critical-contact integrity and the manual-submit boundary. Version 3.2 authorizes the exact stored HTTPS origin for jobs Trackly ingested from employer careers sources, without granting redirect, iframe, or hostname-suffix privileges. Version 3.3.1 adds active-batch recovery, epoch-bound observations/outcomes, and truth certification for forms with no resume control. Version 3.4 adds server-owned accessible executions above immutable child batches, typed access dispositions, and an authoritative target-completion funnel.
139
146
 
147
+ Version 3.7 adds public exact-member recovery and scoped review-handoff claims,
148
+ plus local tab-finalizer and resume-upload proof validators. Recovery candidates
149
+ are bounded and value-free. The local helpers perform no browser actions and
150
+ send no tab IDs, filenames, paths, page text, or form values to Trackly.
151
+
140
152
  ### Maintenance behavior
141
153
 
142
154
  REST, refresh, download, CLI, and local/hosted MCP surfaces use canonical `code: "maintenance_mode"`; older `planned_maintenance` responses are accepted only as a compatibility alias. Structured errors retain HTTP and service status, retry time, estimated return, and request ID without clearing valid credentials. Maintenance is resumable rather than retryable: wait for the advertised window, refetch the Apply protocol and profile, then resume the existing `agent_browser` run. Never create a duplicate run or click Submit.
package/lib/agent.js CHANGED
@@ -8,13 +8,13 @@ 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
10
  const { contractVersion: MCP_CONTRACT_VERSION } = require('../contracts/trackly-apply-tools.json');
11
- const SKILL_VERSION = '4.4.2';
11
+ const SKILL_VERSION = '4.5.0';
12
12
  const CLI_USER_AGENT = `trackly-cli/${PACKAGE_VERSION}`;
13
13
  const MCP_USER_AGENT = `trackly-mcp/${PACKAGE_VERSION}`;
14
14
 
15
15
  const SKILL_NAME = 'trackly-apply';
16
16
  const SKILL_MAJOR = Number(SKILL_VERSION.split('.')[0]);
17
- const MIN_APPLY_PROTOCOL_VERSION = '3.5.0';
17
+ const MIN_APPLY_PROTOCOL_VERSION = '3.6.0';
18
18
  const CACHE_TTL_MS = 2 * 60 * 60 * 1000;
19
19
  const MANAGED_FILE = '.trackly-managed.json';
20
20
  const SKILL_MANIFEST_VERSION = 1;
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const APPLY_TAB_SET_FAILURE_CODES = Object.freeze({
4
+ CONTROLLER_INVENTORY_INCOMPLETE: 'controller_inventory_incomplete',
5
+ USER_INVENTORY_INCOMPLETE: 'user_inventory_incomplete',
6
+ EXPECTED_SET_EMPTY: 'expected_set_empty',
7
+ KEEP_SET_EMPTY_WITH_EXPECTED_TABS: 'keep_set_empty_with_expected_tabs',
8
+ DUPLICATE_EXPECTED_TAB_ID: 'duplicate_expected_tab_id',
9
+ DUPLICATE_KEEP_TAB_ID: 'duplicate_keep_tab_id',
10
+ DUPLICATE_CONTROLLER_TAB_ID: 'duplicate_controller_tab_id',
11
+ DUPLICATE_USER_TAB_ID: 'duplicate_user_tab_id',
12
+ AMBIGUOUS_CROSS_TYPE_TAB_ID: 'ambiguous_cross_type_tab_id',
13
+ EXPECTED_KEEP_CARDINALITY_MISMATCH: 'expected_keep_cardinality_mismatch',
14
+ EXPECTED_KEEP_SET_MISMATCH: 'expected_keep_set_mismatch',
15
+ EXPECTED_TAB_MISSING_FROM_INVENTORY_UNION: 'expected_tab_missing_from_inventory_union',
16
+ });
17
+
18
+ function canonicalizeTabId(tabId) {
19
+ if (typeof tabId === 'number') {
20
+ if (!Number.isSafeInteger(tabId)) throw new TypeError('Tab IDs supplied as numbers must be safe integers.');
21
+ return String(tabId);
22
+ }
23
+ if (typeof tabId !== 'string' || tabId.trim().length === 0) {
24
+ throw new TypeError('Tab IDs must be safe integers or nonblank opaque strings.');
25
+ }
26
+ if (tabId.length > 512) throw new RangeError('Opaque tab IDs must not exceed 512 characters.');
27
+ return tabId;
28
+ }
29
+
30
+ function canonicalizeTabIds(tabIds, { label, maxItems }) {
31
+ if (!Array.isArray(tabIds)) throw new TypeError('Tab ID collections must be arrays.');
32
+ if (tabIds.length > maxItems) {
33
+ throw new RangeError(`${label} must not contain more than ${maxItems} tab IDs.`);
34
+ }
35
+ return tabIds.map(canonicalizeTabId);
36
+ }
37
+
38
+ function hasDuplicates(tabIds) {
39
+ return new Set(tabIds).size !== tabIds.length;
40
+ }
41
+
42
+ function hasCrossTypeCollision(collections) {
43
+ const typesByCanonicalId = new Map();
44
+ for (const tabId of collections.flat()) {
45
+ const canonical = canonicalizeTabId(tabId);
46
+ const types = typesByCanonicalId.get(canonical) ?? new Set();
47
+ types.add(typeof tabId);
48
+ typesByCanonicalId.set(canonical, types);
49
+ }
50
+ return [...typesByCanonicalId.values()].some((types) => types.size > 1);
51
+ }
52
+
53
+ function sameSet(left, right) {
54
+ if (left.length !== right.length) return false;
55
+ const rightSet = new Set(right);
56
+ return left.every((tabId) => rightSet.has(tabId));
57
+ }
58
+
59
+ function validateApplyTabKeepSet({
60
+ expectedTabIds,
61
+ keepTabIds,
62
+ controllerInventory,
63
+ userInventory,
64
+ }) {
65
+ const expected = canonicalizeTabIds(expectedTabIds, { label: 'expectedTabIds', maxItems: 100 });
66
+ const keep = canonicalizeTabIds(keepTabIds, { label: 'keepTabIds', maxItems: 100 });
67
+ const controller = canonicalizeTabIds(controllerInventory?.tabIds, {
68
+ label: 'controllerInventory.tabIds',
69
+ maxItems: 1000,
70
+ });
71
+ const user = canonicalizeTabIds(userInventory?.tabIds, {
72
+ label: 'userInventory.tabIds',
73
+ maxItems: 1000,
74
+ });
75
+ const failureCodes = [];
76
+ const addFailure = (code) => {
77
+ if (!failureCodes.includes(code)) failureCodes.push(code);
78
+ };
79
+
80
+ if (controllerInventory?.complete !== true) {
81
+ addFailure(APPLY_TAB_SET_FAILURE_CODES.CONTROLLER_INVENTORY_INCOMPLETE);
82
+ }
83
+ if (userInventory?.complete !== true) {
84
+ addFailure(APPLY_TAB_SET_FAILURE_CODES.USER_INVENTORY_INCOMPLETE);
85
+ }
86
+ if (expected.length === 0) addFailure(APPLY_TAB_SET_FAILURE_CODES.EXPECTED_SET_EMPTY);
87
+ if (expected.length > 0 && keep.length === 0) {
88
+ addFailure(APPLY_TAB_SET_FAILURE_CODES.KEEP_SET_EMPTY_WITH_EXPECTED_TABS);
89
+ }
90
+ if (hasDuplicates(expected)) addFailure(APPLY_TAB_SET_FAILURE_CODES.DUPLICATE_EXPECTED_TAB_ID);
91
+ if (hasDuplicates(keep)) addFailure(APPLY_TAB_SET_FAILURE_CODES.DUPLICATE_KEEP_TAB_ID);
92
+ if (hasDuplicates(controller)) addFailure(APPLY_TAB_SET_FAILURE_CODES.DUPLICATE_CONTROLLER_TAB_ID);
93
+ if (hasDuplicates(user)) addFailure(APPLY_TAB_SET_FAILURE_CODES.DUPLICATE_USER_TAB_ID);
94
+ if (hasCrossTypeCollision([
95
+ expectedTabIds, keepTabIds, controllerInventory.tabIds, userInventory.tabIds,
96
+ ])) addFailure(APPLY_TAB_SET_FAILURE_CODES.AMBIGUOUS_CROSS_TYPE_TAB_ID);
97
+ if (expected.length !== keep.length) {
98
+ addFailure(APPLY_TAB_SET_FAILURE_CODES.EXPECTED_KEEP_CARDINALITY_MISMATCH);
99
+ }
100
+ if (!sameSet(expected, keep)) addFailure(APPLY_TAB_SET_FAILURE_CODES.EXPECTED_KEEP_SET_MISMATCH);
101
+
102
+ const inventoryUnion = new Set([...controller, ...user]);
103
+ if (expected.some((tabId) => !inventoryUnion.has(tabId))) {
104
+ addFailure(APPLY_TAB_SET_FAILURE_CODES.EXPECTED_TAB_MISSING_FROM_INVENTORY_UNION);
105
+ }
106
+
107
+ const safeToFinalize = failureCodes.length === 0;
108
+ return {
109
+ safeToFinalize,
110
+ failureCodes,
111
+ expectedCount: expected.length,
112
+ keepCount: keep.length,
113
+ controllerInventoryCount: controller.length,
114
+ userInventoryCount: user.length,
115
+ inventoryUnionCount: inventoryUnion.size,
116
+ canonicalKeepTabIds: safeToFinalize ? keep : [],
117
+ };
118
+ }
119
+
120
+ module.exports = {
121
+ APPLY_TAB_SET_FAILURE_CODES,
122
+ canonicalizeTabId,
123
+ validateApplyTabKeepSet,
124
+ };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const APPLY_CONTRACT = require('../contracts/trackly-apply-tools.json');
4
+
5
+ const APPLY_UPLOAD_STAGES = Object.freeze([...APPLY_CONTRACT.constants.applyUploadStages]);
6
+ const APPLY_UPLOAD_FAILURE_CODES = Object.freeze([
7
+ ...APPLY_CONTRACT.constants.applyUploadFailureCodes,
8
+ ]);
9
+ const APPLY_UPLOAD_CAPABILITIES = Object.freeze([
10
+ 'semanticControlDiscovery',
11
+ 'chooserArming',
12
+ 'fileAttachment',
13
+ 'committedFilenameInspection',
14
+ 'parserFieldRecheck',
15
+ ]);
16
+
17
+ function validateApplyResumeUpload({ capabilities, events }) {
18
+ const failureCodes = [];
19
+ const seen = new Set();
20
+ let expectedIndex = 0;
21
+ let lastPassedStage = null;
22
+
23
+ for (const capability of APPLY_UPLOAD_CAPABILITIES) {
24
+ if (capabilities[capability] !== true) failureCodes.push('upload_capability_unavailable');
25
+ }
26
+
27
+ for (const event of events) {
28
+ if (seen.has(event.stage)) {
29
+ failureCodes.push('upload_stage_duplicate');
30
+ continue;
31
+ }
32
+ seen.add(event.stage);
33
+ const index = APPLY_UPLOAD_STAGES.indexOf(event.stage);
34
+ const isExpectedStage = index === expectedIndex;
35
+ if (!isExpectedStage) failureCodes.push('upload_stage_out_of_order');
36
+
37
+ const hasFailureCode = Boolean(event.failureCode);
38
+ if (hasFailureCode) {
39
+ failureCodes.push(event.failureCode);
40
+ }
41
+
42
+ if (event.outcome === 'failed' || hasFailureCode) {
43
+ if (!event.failureCode) failureCodes.push('upload_stage_missing');
44
+ } else if (isExpectedStage) {
45
+ expectedIndex += 1;
46
+ lastPassedStage = event.stage;
47
+ }
48
+ }
49
+
50
+ if (expectedIndex !== APPLY_UPLOAD_STAGES.length) failureCodes.push('upload_stage_missing');
51
+ const uniqueFailureCodes = [...new Set(failureCodes)];
52
+ return {
53
+ safeToClaimAttachment: uniqueFailureCodes.length === 0,
54
+ failureCodes: uniqueFailureCodes,
55
+ lastPassedStage,
56
+ completedStageCount: expectedIndex,
57
+ requiredStageCount: APPLY_UPLOAD_STAGES.length,
58
+ };
59
+ }
60
+
61
+ module.exports = {
62
+ APPLY_UPLOAD_CAPABILITIES,
63
+ APPLY_UPLOAD_FAILURE_CODES,
64
+ APPLY_UPLOAD_STAGES,
65
+ validateApplyResumeUpload,
66
+ };