trackly-cli 0.20.1 → 0.21.0

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
@@ -175,6 +175,30 @@ Then use natural language in any of these clients:
175
175
  - "What remote engineering roles are available?"
176
176
  - "Mark job 1234 as applied"
177
177
 
178
+ ### Search filters and updates
179
+
180
+ `trackly_search_jobs` supports multiple roles and regions, US states and Canadian
181
+ provinces, internship education levels, favorite companies, posting windows,
182
+ annual USD salary floors, incremental sync, work arrangement, sponsorship and
183
+ pipeline status. [Exact values and examples](docs/trackly-tools.md#available-tools).
184
+ Search returns 20 jobs by default; request up to 100 and use `offset` to page.
185
+
186
+ For sponsorship, “exclude no” keeps yes **and not mentioned**. Use `exclude_no`;
187
+ `only_yes` excludes jobs with unknown sponsorship. Ask the user when intent is
188
+ ambiguous. Salary floors also keep unknown and non-comparable salaries visible.
189
+ Unsupported industry/specialization/literal-search controls are rejected.
190
+
191
+ For a local server without a global package install, run `npx -y trackly-cli mcp`.
192
+ For Codex, use `codex mcp add trackly -- npx -y trackly-cli mcp`; Cursor and
193
+ Windsurf can use `"command": "npx", "args": ["-y", "trackly-cli", "mcp"]`.
194
+ Authenticate with `npx -y trackly-cli login` first, or configure `TRACKLY_API_KEY`.
195
+
196
+ Existing global installs require `npm install --global trackly-cli@latest`.
197
+ Restart the MCP process/client and refresh tool discovery after updating.
198
+ For native skills and marketplace plugin updates, follow the
199
+ [release and update guide](docs/release-verification.md#existing-public-users).
200
+ Hosted and local releases are separate: the current client schema is authoritative.
201
+
178
202
  ## MCP Tools Reference
179
203
 
180
204
  The local stdio server (`trackly mcp`) registers the 58 tools below. Four are
@@ -185,7 +209,7 @@ The hosted server omits those four and adds `trackly_chat`, for 55 hosted tools.
185
209
 
186
210
  | Tool | Description |
187
211
  |------|-------------|
188
- | trackly_search_jobs | Search and filter jobs by function, company, region, employment type, work arrangement, visa sponsorship, and status |
212
+ | trackly_search_jobs | Search by roles, regions, states/provinces, internship level, favorites, recency, annual USD salary floor, sponsorship, work arrangement, and pipeline status |
189
213
  | trackly_get_job | Get full details for a specific job |
190
214
  | trackly_search_companies | Semantic company search |
191
215
  | trackly_list_companies | List all tracked companies |
package/bin/trackly CHANGED
@@ -885,7 +885,7 @@ async function cmdAsk(args) {
885
885
  if (result.jobsUrl) {
886
886
  // Path allowlist: /ask can return any string for jobsUrl. normalizeEndpoint already
887
887
  // blocks cross-origin fetches (baseUrl mismatch), but a compromised backend could
888
- // still emit a same-origin path like `/api/admin/secret-dump`. Restrict to the two
888
+ // still emit an unrelated privileged same-origin path. Restrict to the two
889
889
  // legitimate handlers that /ask is designed to route to.
890
890
  const JOBS_URL_ALLOWLIST = /^\/api\/(v1|jobscout)\/jobs(\?|$)/;
891
891
  if (!JOBS_URL_ALLOWLIST.test(result.jobsUrl)) {
@@ -16,7 +16,7 @@ execution: code
16
16
 
17
17
  - **Objective:** Make Trackly Apply recover the exact interrupted jobs, preserve cumulative completion, reconcile every member in a submitted handoff, and fail closed around browser cleanup and resume upload.
18
18
  - **Authority:** The backend owns membership, lifecycle, completion credit, idempotency, and current state. The browser harness owns live DOM evidence and local tab identity. The user owns Submit, credentials, verification challenges, and ambiguous answers.
19
- - **Execution profile:** Deep, cross-repository reliability work in `close-ai` and `trackly-cli`. Use fresh worktrees from current `origin/main`; keep base checkouts read-only.
19
+ - **Execution profile:** Deep, cross-repository reliability work in the private backend and `trackly-cli`. Use fresh worktrees from current `origin/main`; keep base checkouts read-only.
20
20
  - **Stop conditions:** Stop browser mutation on unexpected recovery membership, stale inspection authority, ambiguous submission state, incomplete tab inventories, unproven attachment state, or incompatible protocol versions.
21
21
  - **Tail ownership:** Merge and deploy the backend first. Then publish and install the compatible CLI and skill. Finish with no-submit fixtures and a manually submitted dogfood recovery flow.
22
22
 
@@ -168,7 +168,7 @@ None of these receipts implies another.
168
168
 
169
169
  ### Success Criteria
170
170
 
171
- - The faithful Postgres sequence from the August 5 incident passes without projection contradictions.
171
+ - The faithful database sequence from the August 5 incident passes without projection contradictions.
172
172
  - Exact recovery never mutates `saved_at`, never substitutes newer jobs, and never uses old-epoch evidence.
173
173
  - Every grouped submission report produces one result per handoff member.
174
174
  - No finalizer call receives an empty or partial keep set when expected tabs exist.
@@ -208,7 +208,7 @@ None of these receipts implies another.
208
208
  ### Key Technical Decisions
209
209
 
210
210
  - KTD1. **Persist completion credit in an append-only achievement ledger.** (session-settled: user-approved — chosen over concurrent unsubmitted inventory: once a job is ready or submitted it remains part of the requested total.) Create one value-free credit per `(execution_id, user_id, frozen_job_id)`, constrained to reviewed sources such as `review_ready` and `submitted_repair`. Insert the credit in the same transaction as the durable outcome. Expose `achievementCount` as the raw unique effective-credit count and compute target-facing `completed = LEAST(achievementCount, target)`. Credits are the sole target-achievement input; current ready/submitted counts remain operator projections. Do not add mutable columns to immutable execution candidates. A credit later proven to originate from invalid evidence may be neutralized only by an append-only compensating revocation through an audited internal repair operation; later submission or lifecycle change is never a revocation reason. Governs R1-R3.
211
- - KTD2. **Characterize the live state sequence before changing accounting.** Current unit tests already cover `ready + submitted`, so implementation begins with a Postgres-backed checkpoint, certification, bulk-outcome, submission, and progress-read reproduction. A passing unit calculator test is not sufficient evidence. Governs R1-R3, R8.
211
+ - KTD2. **Characterize the live state sequence before changing accounting.** Current unit tests already cover `ready + submitted`, so implementation begins with a database-backed checkpoint, certification, bulk-outcome, submission, and progress-read reproduction. A passing unit calculator test is not sufficient evidence. Governs R1-R3, R8.
212
212
  - KTD3. **Model exact recovery as a new execution mode over stable source candidates.** The original August 5 decision used `recover_exact_members` with `sourceExecutionId`, exact execution-candidate IDs proven inside the source snapshot hash, optimistic revision, and idempotency key. It persisted `source_execution_id`, `source_candidate_id`, and the attempt-specific `source_member_id` when present; identity assertion was atomic, eligibility could be partial (superseded—see the shipped-contract addendum immediately below), and the server created no substitutes. Governs R4-R7.
213
213
 
214
214
  **Shipped-contract addendum (2026-08-13):** Backend implementation and adversarial review superseded partial eligibility with an atomic all-or-nothing eligibility check. If any confirmed candidate is no longer eligible, the server creates no recovery execution and no substitutes. AE4 and U4 below reflect this shipped contract while the original decision remains recorded here.
@@ -221,7 +221,7 @@ None of these receipts implies another.
221
221
  - KTD10. **Reuse resume approval only with exact lineage.** A recovery may reuse execution-scoped content approval only when the backend proves the same resume hash, profile revision, snapshot lineage, and unexpired approval. Every run still performs immediate local file verification. Otherwise the user approves the resume again. Governs R16, R22.
222
222
  - KTD11. **Ship additive coordinated versions.** Expected releases are Apply protocol 3.6.0, MCP contract 3.7.0, skill 4.5.0, and CLI 0.14.0. Recompute versions and migration maximum immediately before implementation. Older active work keeps its published recovery path. Governs R20-R22.
223
223
  - KTD12. **Persist request-key idempotency for outcomes.** Single and bulk outcomes require an idempotency key. Atomically store operation type, canonical payload hash, and exact item results; exact retries return those saved results. Current-state inference remains a bounded repair fallback, not the primary replay mechanism. Governs R8-R12.
224
- - KTD13. **Retain recovery identity beyond operational expiry.** Add `recoverable_until`, computed by PostgreSQL from `expires_at` plus 30 calendar days. The stored value is immutable and cannot be extended. Daily purge may delete source execution data only after that boundary. Record only value-free interruption-to-resume duration metrics. Purge for the new mode remains feature-flagged until dogfood/private-beta evidence is reviewed and the product owner approves the 30-day recovery SLO; until then, retain the additive recovery manifest under normal account-retention rules. Governs R4-R7, R23.
224
+ - KTD13. **Retain recovery identity beyond operational expiry.** Add `recoverable_until`, computed by the database from `expires_at` plus 30 calendar days. The stored value is immutable and cannot be extended. Daily purge may delete source execution data only after that boundary. Record only value-free interruption-to-resume duration metrics. Purge for the new mode remains feature-flagged until dogfood/private-beta evidence is reviewed and the product owner approves the 30-day recovery SLO; until then, retain the additive recovery manifest under normal account-retention rules. Governs R4-R7, R23.
225
225
 
226
226
  ### High-Level Technical Design
227
227
 
@@ -285,7 +285,7 @@ The backend owns durable identity, membership, milestones, and reconciliation. T
285
285
  | Recovery reuses stale browser evidence | Increment inspection epoch and require fresh binding, identity, contact, and review evidence. |
286
286
  | A group phrase marks the wrong jobs submitted | Resolve it to an exact handoff receipt; inspect every staged member; ask only about unresolved members. |
287
287
  | Provider strategy becomes remote executable browser code | Publish bounded declarative strategy enums and semantic evidence requirements only. |
288
- | Completion credit and current ready counts drift | Update/read them under the same owner lock and assert invariants in Postgres integration tests. |
288
+ | Completion credit and current ready counts drift | Update/read them under the same owner lock and assert invariants in database integration tests. |
289
289
  | Operational expiry destroys exact recovery identity | Separate execution expiry from bounded recovery retention and purge only after `recoverable_until`. |
290
290
  | Old clients misread the new execution mode | Roll out compatible readers first; they must recognize or safely reject `recover_exact_members` before writes are enabled. |
291
291
  | Local tab helper creates a false sense of visibility | Return set-safety only; keep user-visible presentation as a separate receipt. |
@@ -300,8 +300,8 @@ The backend owns durable identity, membership, milestones, and reconciliation. T
300
300
 
301
301
  - **Goal:** Reproduce the production accounting and review-replay failure before changing persistent state.
302
302
  - **Requirements:** R1-R3, R8, AE1-AE2.
303
- - **Repositories:** `close-ai`.
304
- - **Files:** `src/services/application-profile/__tests__/apply-execution-service.test.ts`, `src/services/application-profile/__tests__/service.test.ts`, and a Postgres integration test beside the existing application-profile integration fixtures.
303
+ - **Repositories:** the private backend.
304
+ - **Files:** backend execution-service tests, backend application-profile tests, and a database integration test beside the existing application-profile integration fixtures.
305
305
  - **Approach:** Exercise real execution, batch, member, run, evidence, and attestation rows through checkpoint, truth certification, bulk review outcome, one submission, and progress read. Assert both current counts and cumulative status. Preserve the existing pure calculator test as a smaller unit check.
306
306
  - **Test scenarios:** Exact duplicate review intent; stale epoch; one of two submitted; both submitted; ambiguous transport followed by refetch; concurrent submit and advance.
307
307
  - **Verification:** At least one characterization fails on pre-fix code for the same durable state mismatch observed in the incident.
@@ -311,19 +311,19 @@ The backend owns durable identity, membership, milestones, and reconciliation. T
311
311
  - **Goal:** Make target achievement durable and independent from later lifecycle or attestation projection changes.
312
312
  - **Requirements:** R1-R3, R13, AE1.
313
313
  - **Dependencies:** U1.
314
- - **Repositories:** `close-ai`.
315
- - **Files:** next sequential migration, migration runner/test if required by index strategy, `src/services/application-profile/batch-service.ts`, `src/services/application-profile/service.ts`, application-profile service tests.
314
+ - **Repositories:** the private backend.
315
+ - **Files:** next sequential migration, migration runner/test if required by index strategy, backend batch service, backend application-profile service, application-profile service tests.
316
316
  - **Approach:** Create a separate append-only achievement ledger rather than weakening immutable candidate rows. Insert one owner-bound credit with `ON CONFLICT DO NOTHING` only after durable review outcome or authorized submission repair, in the same transaction as execution status. Expose raw `achievementCount`, compute target-facing `completed = LEAST(achievementCount, target)`, and retain current `durablyReviewReady` and `submitted` for operator detail. Make credit the sole target-achievement input. Before migration, produce a dry-run report grouped by provable source predicate and ambiguous exclusion reason. Backfill every provable submitted row with authorized evidence, or review-ready row with an `awaiting_manual_submit` run and matching active truth attestation for the certified epoch/set; do not discard valid historical overflow above target.
317
317
  - **Test scenarios:** Credit set once; submission preserves credit; sibling submission does not reduce total; profile/attestation changes do not erase credit; concurrent writers cannot double-credit; account/execution deletion cascades correctly; ambiguous legacy rows remain uncredited; valid historical overflow remains visible; invalid-evidence revocation is append-only, audited, and cannot be triggered by ordinary lifecycle change.
318
- - **Verification:** AE1 passes in pure and Postgres-backed tests, and an advance after target achievement produces no child wave. Before historical writes, a migration-owner review must approve per-predicate counts, sampled source evidence, and ambiguous-exclusion totals; any sample mismatch ships prospective crediting only and defers backfill.
318
+ - **Verification:** AE1 passes in pure and database-backed tests, and an advance after target achievement produces no child wave. Before historical writes, a migration-owner review must approve per-predicate counts, sampled source evidence, and ambiguous-exclusion totals; any sample mismatch ships prospective crediting only and defers backfill.
319
319
 
320
320
  ### U3. Make review outcomes replay-safe and projections explicit
321
321
 
322
322
  - **Goal:** Return existing durable milestones for exact replay and make historical/current state legible.
323
323
  - **Requirements:** R8-R9, R13, AE2.
324
324
  - **Dependencies:** U1-U2.
325
- - **Repositories:** `close-ai`.
326
- - **Files:** `src/services/application-profile/service.ts`, `src/routes/trackly-apply.ts`, route/runtime tests, service tests, batch projection tests.
325
+ - **Repositories:** the private backend.
326
+ - **Files:** backend application-profile service, backend Apply HTTP routes, route/runtime tests, service tests, batch projection tests.
327
327
  - **Approach:** Require an idempotency key for single and bulk outcomes. Extract a transaction-aware outcome writer, acquire the orchestration-owner lock, process bulk items in canonical member order with per-item savepoints, and persist the canonical request hash plus every item result in the same outer transaction. Return results in original request order; exact retries return `transition=replayed` from the stored result journal. Preserve partial bulk successes. State inference remains a bounded fallback for repairs created before request idempotency. Create a first-class execution-owned handoff receipt only after the ordered ready set is durable. Add explicit historical and current projection fields without mutating old rows.
328
328
  - **Test scenarios:** Exact replay; mismatched epoch; mismatched member; partial bulk replay/commit/conflict; old batch with current submitted run; repeated request after lost transport response.
329
329
  - **Verification:** Identical replay performs no version/event mutation and returns `awaiting_manual_submit`; historical views cannot be mistaken for actionable current state.
@@ -333,8 +333,8 @@ The backend owns durable identity, membership, milestones, and reconciliation. T
333
333
  - **Goal:** Reauthorize only explicitly requested eligible jobs after an expired or terminal execution.
334
334
  - **Requirements:** R4-R7, R13, R23, AE3-AE4, AE8.
335
335
  - **Dependencies:** U2-U3.
336
- - **Repositories:** `close-ai`.
337
- - **Files:** `src/services/application-profile/apply-execution-contract.ts`, `src/services/application-profile/batch-service.ts`, `src/services/application-profile/service.ts`, `src/routes/trackly-apply.ts`, `src/mcp/server.ts`, execution service/route/MCP contract tests.
336
+ - **Repositories:** the private backend.
337
+ - **Files:** backend execution contract, backend batch service, backend application-profile service, backend Apply HTTP routes, backend hosted MCP registry, execution service/route/MCP contract tests.
338
338
  - **Approach:** Add an authenticated discovery read returning only the user's recoverable execution IDs, snapshot hashes, stable candidate IDs, bounded lifecycle/eligibility codes, and expiry. It must not return profile values, browser data, or local paths. After explicit exact-set confirmation, create recovery with source execution, stable execution-candidate list, source snapshot hash, and idempotency key. Validate ownership and no competing mutable execution under the documented lock order. Evaluate eligibility for the entire asserted source set before creation; if any confirmed candidate is ineligible, fail atomically without creating an execution or substituting another candidate. When all are eligible, preserve source execution/candidate/member lineage, create one immutable non-replenishing recovery execution for the exact confirmed set, and require fresh epochs and browser bindings. Separate operational expiry from a bounded recovery-retention boundary so the nightly purge cannot erase eligible identity mid-window.
339
339
  - **Test scenarios:** Complete local context loss followed by discovery; cross-user discovery rejection; explicit exact-set confirmation; newer queue candidates present; saved timestamps unchanged; wholly eligible set; mixed eligible/applied/inactive set rejected with no execution; duplicate IDs; changed requisition; foreign user; concurrent identical recovery; concurrent different recovery; old evidence rejected; second interruption; recovery immediately before and after operational expiry; recovery after daily purge but before `recoverable_until`; rejection after retention expiry.
340
340
  - **Verification:** AE3-AE4 pass; AE4 explicitly proves that an ineligible confirmed member yields zero recovery executions, and every execution created by an eligible case has a response membership hash that exactly matches the full asserted source set.
@@ -344,7 +344,7 @@ The backend owns durable identity, membership, milestones, and reconciliation. T
344
344
  - **Goal:** Make exact recovery and handoff receipts safely callable from hosted and local agents.
345
345
  - **Requirements:** R7, R9-R10, R19-R22.
346
346
  - **Dependencies:** U3-U4.
347
- - **Repositories:** `close-ai`, then `trackly-cli`.
347
+ - **Repositories:** the private backend, then `trackly-cli`.
348
348
  - **Files:** backend protocol route and hosted MCP schemas; `mcp/apply-tools.js`, contract history/fixture, `docs/trackly-tools.md`, MCP schema and hosted-contract tests, package changelog/version files.
349
349
  - **Approach:** First ship compatible readers that recognize or safely reject `recover_exact_members`; only then enable writes behind a feature flag. Advertise protocol capability and additive versions. Add public recovery-discovery and exact-recovery tools and result schemas. Include requested-set hash, stable candidate eligibility, fresh-epoch requirements, cumulative `completed`, handoff receipt, and persisted transition result. Keep local-only helpers out of hosted parity expectations through the existing documented exception pattern.
350
350
  - **Test scenarios:** Compatible and incompatible clients; hosted/local public-schema equality; cross-user rejection; replay response; missing capability; older active execution recovery.
@@ -355,7 +355,7 @@ The backend owns durable identity, membership, milestones, and reconciliation. T
355
355
  - **Goal:** Reconcile every job in the exact staged handoff after the user reports submission.
356
356
  - **Requirements:** R9-R13, R19, R21-R22, AE5.
357
357
  - **Dependencies:** U3, U5.
358
- - **Repositories:** `close-ai`, `trackly-cli`.
358
+ - **Repositories:** the private backend, `trackly-cli`.
359
359
  - **Files:** backend ATS capability/protocol definitions; `skills/trackly-apply/SKILL.md`; `skills/trackly-apply/references/ats-playbook.md`, `review-handoff.md`, `batch-orchestration.md`, `form-integrity.md`; prompt-resource and contract tests.
360
360
  - **Approach:** Publish bounded provider strategy enums. Define the local `DetectedSubmission` boundary and require the skill to resolve group language to a handoff receipt, inspect each member's live DOM, classify each as `detected`, `user_confirmed`, `unresolved`, or `contradictory`, record positive evidence through the existing backend writer, preserve unmatched members, and ask once only for unresolved submission authority. URL/title remain identity and navigation signals only.
361
361
  - **Test scenarios:** Greenhouse redirect; Ashby unchanged route/banner; Lever redirect and same-route confirmation; one of two succeeds; both succeed quickly; explicit user confirmation preserves stronger stored evidence; contradictory “already applied” state.
@@ -377,7 +377,7 @@ The backend owns durable identity, membership, milestones, and reconciliation. T
377
377
  - **Goal:** Make recovery state and the next human/agent action obvious while completing cross-repository release gates.
378
378
  - **Requirements:** R7, R19-R22.
379
379
  - **Dependencies:** U1-U7.
380
- - **Repositories:** `close-ai`, `trackly-cli`.
380
+ - **Repositories:** the private backend, `trackly-cli`.
381
381
  - **Files:** `skills/trackly-apply/references/review-handoff.md`, browser lifecycle/recovery references, MCP prompt resources, changelogs, release notes, doctor/consumer tests.
382
382
  - **Approach:** Add deterministic normal-review, recovery, visibility-unverified, upload-manual, and post-submission templates. Each recovery template shows tab, form, and authority receipts separately. Deploy/migrate backend, verify live protocol, publish/install CLI and canonical skill, run doctor, then dogfood exact recovery and grouped reconciliation without agent submission.
383
383
  - **Test scenarios:** Restart with blank forms; restored tab with expired authority; unresolved auth member; upload unavailable; one staged member unresolved; no active work; older compatible active execution.
@@ -389,11 +389,11 @@ The backend owns durable identity, membership, milestones, and reconciliation. T
389
389
 
390
390
  | Gate | Repository | Command or check | Required result |
391
391
  |---|---|---|---|
392
- | Type safety | `close-ai` | `npm run typecheck` | No TypeScript errors. |
393
- | Build | `close-ai` | `npm run build` | Production bundle succeeds. |
394
- | Lint | `close-ai` | `npm run lint` | No lint errors in changed code. |
395
- | Targeted Apply tests | `close-ai` | Run the application-profile service, execution, route, MCP-contract, migration, and new Postgres integration test files with Vitest | Every lifecycle and concurrency scenario passes. |
396
- | Full backend suite | `close-ai` | `npm test` | Full suite passes. |
392
+ | Type safety | the private backend | `npm run typecheck` | No TypeScript errors. |
393
+ | Build | the private backend | `npm run build` | Production bundle succeeds. |
394
+ | Lint | the private backend | `npm run lint` | No lint errors in changed code. |
395
+ | Targeted Apply tests | the private backend | Run the application-profile service, execution, route, MCP-contract, migration, and new database integration test files with Vitest | Every lifecycle and concurrency scenario passes. |
396
+ | Full backend suite | the private backend | `npm test` | Full suite passes. |
397
397
  | CLI tests | `trackly-cli` | `npm test` | Full Node test suite passes. |
398
398
  | CLI contract fixture | `trackly-cli` | `npm run test:contract-fixture` | Skill and MCP contract assertions pass. |
399
399
  | Hosted/local parity | `trackly-cli` | `npm run test:hosted-contract` | Public tool schemas match the deployed backend; only documented local helpers differ. |
@@ -449,10 +449,10 @@ The backend owns durable identity, membership, milestones, and reconciliation. T
449
449
 
450
450
  ### Current implementation anchors
451
451
 
452
- - `close-ai:src/services/application-profile/batch-service.ts` owns parent executions, immutable child waves, access dispositions, and the pure progress calculator.
453
- - `close-ai:src/services/application-profile/service.ts` reconstructs progress, records outcomes, applies evidence repair, and updates current job state.
454
- - `close-ai:src/routes/trackly-apply.ts` owns authenticated HTTP contracts.
455
- - `close-ai:src/mcp/server.ts` owns hosted MCP schemas and prompt resources.
452
+ - backend batch service owns parent executions, immutable child waves, access dispositions, and the pure progress calculator.
453
+ - backend application-profile service reconstructs progress, records outcomes, applies evidence repair, and updates current job state.
454
+ - backend Apply HTTP routes own authenticated HTTP contracts.
455
+ - backend hosted MCP registry owns hosted MCP schemas and prompt resources.
456
456
  - `trackly-cli:mcp/apply-tools.js` owns local/hosted HTTP wrappers and local-only helpers.
457
457
  - `trackly-cli:skills/trackly-apply/` owns reusable browser mechanics.
458
458
 
@@ -0,0 +1,107 @@
1
+ # Trackly release verification
2
+
3
+ This checklist is mandatory for every Trackly CLI upgrade. A release makes the
4
+ current Trackly tools available in its public packages and deployed MCP surfaces;
5
+ installation verification proves that the named environments are using it.
6
+ Release closeout requires both. Keep publication, installation and authenticated
7
+ browser readiness as separate states.
8
+
9
+ ## Before publication
10
+
11
+ - Record the intended npm CLI version, MCP contract version, native Apply skill
12
+ version, plugin version, plugin adaptation lock and any required harness pins.
13
+ These identifiers belong to different packages and need not share a number.
14
+ - Run the repository CI checks: tests, dependency audit and audit policy, checked-in
15
+ hosted contract, plugin submission preflight and package smoke test.
16
+ - When tools or schemas change, compare the final backend candidate with
17
+ the test:hosted-contract npm script and TRACKLY_BACKEND_DIR set to its checkout.
18
+ Verify complete tool names and input schemas, including the reviewed differences
19
+ between local MCP, hosted MCP and the plugin facade. Equal tool counts alone
20
+ do not establish parity; distinct facade allowlists must remain intact.
21
+ - Coordinate the CLI and backend owners and update cloud installer pins and their
22
+ captured tool-contract fixtures through the normal reviewed PR workflow.
23
+
24
+ ## Published artifacts and live tools
25
+
26
+ - Verify the exact merged commit, GitHub release, npm latest version, published
27
+ tarball integrity and provenance attestations. Check successful npm and MCP
28
+ Registry publication steps rather than relying only on a release tag.
29
+ - Inspect the published tarball, rather than only the source checkout. Verify
30
+ CLI/server versions, dependency declarations against the source lock, MCP code
31
+ and native skill contents. Boot
32
+ the published MCP server and obtain the full paginated tools/list inventory.
33
+ Compare tool names and input schemas against the intended release contract.
34
+ - Check the production hosted MCP and plugin facade with approved existing
35
+ authentication. Verify their complete discoverable tools and schemas and the
36
+ deployed backend contract. Source fixtures and unauthenticated HTTP challenges
37
+ do not prove authenticated live discovery. Do not run application mutations or
38
+ paid AI calls as release probes.
39
+ - Verify the plugin manifest and adaptation lock against the installed package.
40
+ A newer Git marketplace package does not prove that a separately reviewed
41
+ plugin catalog or connector listing has been updated. Record its actual
42
+ submission, approval and publication state independently.
43
+
44
+ ## Installation matrix
45
+
46
+ Record a fresh receipt for each target below. Include its account and environment
47
+ identity, timestamp with time zone, npm package/CLI version, supported Node
48
+ runtime, actual MCP server version, complete tool names and schema digest, native
49
+ skill version and file hashes, installed plugin version and lock hashes, and any
50
+ applicable harness version. Keep credentials and profile answers out of receipts.
51
+
52
+ | Target | Required evidence |
53
+ | --- | --- |
54
+ | M1 | Verify the actual application account and its installed package, native skills, plugin and MCP discovery; an SSH login to another account is insufficient. |
55
+ | M4 | Verify the installed package, native skills, plugin manager result and MCP discovery. |
56
+ | Conductor Cloud image | Verify the successful active build, exact merged installer source, pinned package/plugin versions and MCP schema checks. |
57
+ | Conductor retained homes | Verify each in-scope retained home separately; rebuilding the shared image does not update them. Enumerate any dormant or archived homes left unverified. |
58
+ | Current Codex Cloud | Verify the signed-in account, actual helper outputs, saved and published environment revision, then installation and MCP discovery in a restored workspace. A setup-chat success alone is insufficient. |
59
+
60
+ Use the client plugin manager to update installed plugins; never patch versioned
61
+ plugin caches. Verify all applicable clients that consume the package. Where a
62
+ client supports native skills instead of this plugin format, record that
63
+ difference and verify the native skill. A genuinely unsupported plugin manager,
64
+ missing authentication or unreachable environment is an explicit verification
65
+ gap, not a passing result. Preserve unrelated settings and report intentional
66
+ configuration changes separately from configuration preservation checks.
67
+
68
+ ## Existing public users
69
+
70
+ An existing global npm install remains at its installed version until updated.
71
+ The supported local update sequence is:
72
+
73
+ ~~~sh
74
+ npm install --global trackly-cli@latest
75
+ trackly --version
76
+ trackly agent setup --client both --skills-only
77
+ trackly agent doctor
78
+ ~~~
79
+
80
+ The skills-only setup preserves existing MCP registration and authentication.
81
+ Users with one client can select codex or claude. Restart the configured local
82
+ MCP server or its client after updating, refresh discovery, and verify the new
83
+ server version and tools. A running old process keeps the old tool implementation.
84
+ Check the actual Node runtime against the package engines requirement.
85
+
86
+ For the official Git marketplace plugin, use the documented Codex plugin manager:
87
+
88
+ ~~~sh
89
+ # Register the official marketplace once, if it is not already configured.
90
+ codex plugin marketplace add trackly-app/trackly-cli --ref main
91
+ codex plugin marketplace upgrade trackly-cli
92
+ codex plugin add trackly@trackly-cli --json
93
+ ~~~
94
+
95
+ Verify the returned installed version and package hashes, then reload the client.
96
+ Other plugin catalogs follow their own supported update and approval lifecycle.
97
+ Hosted MCP users receive the deployed server implementation; reconnect or refresh
98
+ client discovery after a tool change and verify the intended authenticated surface.
99
+
100
+ ## Closeout
101
+
102
+ Attach the release version set, published artifact evidence, live tool-contract
103
+ checks, installation matrix, changed-configuration checks and remaining gaps to
104
+ the release record. Every applicable row must pass before claiming global parity.
105
+ When only package publication is verified, report publication; when installations
106
+ are verified but live discovery is unavailable, report that narrower result.
107
+ An upgrade remains incomplete until required verification gaps are resolved.
@@ -0,0 +1,39 @@
1
+ ---
2
+ title: Analytics instrumentation must preserve strict MCP schemas
3
+ category: integration-issues
4
+ module: MCP analytics
5
+ component: observability
6
+ date: 2026-10-19
7
+ problem_type: integration_issue
8
+ severity: medium
9
+ symptoms:
10
+ - Unsupported search arguments reached the handler without an MCP validation error
11
+ - Unknown controls disappeared when optional analytics context was installed
12
+ root_cause: missing_validation
13
+ resolution_type: code_fix
14
+ applies_when:
15
+ - Adding fields to registered MCP tool schemas
16
+ - Wrapping tools with optional analytics context
17
+ tags: [mcp, analytics, zod, strict-schema, sdk]
18
+ ---
19
+
20
+ The MCP SDK's registered-tool `update({ paramsSchema })` shape API rebuilds an
21
+ object schema. With SDK 1.32.1 this loses the strict/catchall policy of a real Zod
22
+ object. An unsupported search control can therefore disappear before the
23
+ handler, leaving a successful response to a different request.
24
+
25
+ Extend an existing object schema with its own `extend` method and update the
26
+ callback separately. Preserve the shape-based path only for SDK-created empty
27
+ Mini schemas that do not expose `extend`. Strip the optional analytics context
28
+ before calling the business handler, while retaining all original validation.
29
+
30
+ The regression must use a real MCP client and linked server transport with
31
+ analytics enabled. A schema-only assertion before instrumentation cannot expose
32
+ this defect. Send an unknown argument and assert an MCP error plus zero handler
33
+ calls. Also check that valid calls, optional context, strict/catchall policies,
34
+ and tools without input fields remain compatible.
35
+
36
+ Verification: `node --test test/mcp-analytics.test.js test/mcp-schema.test.js`
37
+ and `npm test`. The real transport regression failed with the old instrumentation
38
+ and passed after the fix. Keyword telemetry continues to retain length only;
39
+ this change does not permit raw query or context storage.
@@ -40,7 +40,7 @@ Adding more checks to every caller would not close the version-skew hole. Compat
40
40
 
41
41
  ## Solution
42
42
 
43
- Move the mandatory challenge-and-confirm invariant to the backend operation that performs the deletion, then keep MCP callers as thin protocol adapters. The backend implementation is outside this repository; internal/private [close-ai PR #1296](https://github.com/trackly-app/close-ai/pull/1296) and public [trackly-cli PR #90](https://github.com/trackly-app/trackly-cli/pull/90) record the service-authoritative design.
43
+ Move the mandatory challenge-and-confirm invariant to the backend operation that performs the deletion, then keep MCP callers as thin protocol adapters. The backend implementation is recorded in private repositories; public [trackly-cli PR #90](https://github.com/trackly-app/trackly-cli/pull/90) documents the service-authoritative design.
44
44
 
45
45
  The current CLI publishes `sensitiveRevocationConfirmToken` in the tool schema and forwards the complete request to the application-profile endpoint:
46
46
 
@@ -67,7 +67,7 @@ Every backend-classified agent caller, including stale CLI and MCP versions, mus
67
67
 
68
68
  The separation also removes semantic duplication. The backend owns the authoritative deletion decision, confirmation validity, revision binding, archive behavior, and mutation. MCP owns discoverability and transport: publish the token field, explain the two-step interaction, preserve the full request, and relay the service response.
69
69
 
70
- Internal/private [close-ai issue #1314](https://github.com/trackly-app/close-ai/issues/1314) remains administratively open, but current backend main appears to satisfy its scoped-inventory acceptance criteria through merged internal/private [close-ai PR #1369](https://github.com/trackly-app/close-ai/pull/1369): the service inventories sensitive and restricted rows across scopes, serializes scope identity into `affectedKeys`, and uses the same user-and-sensitivity predicate for archival and deletion. Close the issue only after confirming the fix is deployed and behaviorally verified; an open tracker alone is not evidence of a current functional gap.
70
+ Private backend tracking records indicate that the scoped-inventory work remains administratively open, but current backend main appears to satisfy its acceptance criteria: the service inventories sensitive and restricted rows across scopes, serializes scope identity into `affectedKeys`, and uses the same user-and-sensitivity predicate for archival and deletion. Close the issue only after confirming the fix is deployed and behaviorally verified; an open tracker alone is not evidence of a current functional gap.
71
71
 
72
72
  ## Prevention
73
73
 
@@ -83,6 +83,6 @@ Internal/private [close-ai issue #1314](https://github.com/trackly-app/close-ai/
83
83
 
84
84
  - [trackly-cli PR #89](https://github.com/trackly-app/trackly-cli/pull/89) — initial caller-owned guard, retained as the superseded attempt
85
85
  - [trackly-cli PR #90](https://github.com/trackly-app/trackly-cli/pull/90) — thin caller relay and service-authoritative correction
86
- - Internal/private [close-ai PR #1296](https://github.com/trackly-app/close-ai/pull/1296) — backend enforcement recorded by the coordinated fix
87
- - Internal/private [close-ai issue #1314](https://github.com/trackly-app/close-ai/issues/1314) and [close-ai PR #1369](https://github.com/trackly-app/close-ai/pull/1369) — tracker still open; current main contains the scoped-inventory fix pending deployment/behavior confirmation
86
+ - Private backend records document enforcement in the coordinated fix
87
+ - Private scoped-inventory tracking remains open pending deployment and behavior confirmation
88
88
  - [Durable exact recovery after browser state loss](durable-exact-recovery-after-browser-state-loss.md) — related Trackly Apply authority-boundary learning
@@ -78,7 +78,7 @@ Verification: all 19 skill files matched the release manifest; the CLI suite
78
78
  passed 721 tests with 20 skipped; the installer test proved no MCP client command
79
79
  or configuration write; the Cloud fixture rejected failed skill installation.
80
80
 
81
- The coordinated durable recovery work shipped through [trackly-cli PR #100](https://github.com/trackly-app/trackly-cli/pull/100), which enforces exact-set discovery and response validation in the CLI/MCP/skill layer, and [close-ai PR #1391](https://github.com/trackly-app/close-ai/pull/1391), whose backend implementation owns atomic all-or-nothing recovery creation.
81
+ The coordinated durable recovery work shipped through [trackly-cli PR #100](https://github.com/trackly-app/trackly-cli/pull/100), which enforces exact-set discovery and response validation in the CLI/MCP/skill layer, and a coordinated private backend change, which owns atomic all-or-nothing recovery creation.
82
82
 
83
83
  ### 1. Discover a bounded, value-free recovery menu
84
84
 
@@ -143,5 +143,5 @@ Inspection epochs close stale-evidence reuse. Field provenance protects user cor
143
143
  ## Related Issues
144
144
 
145
145
  - [Trackly CLI PR #100](https://github.com/trackly-app/trackly-cli/pull/100) — CLI, MCP, and skill-side durable exact recovery contract.
146
- - [Close AI PR #1391](https://github.com/trackly-app/close-ai/pull/1391) — backend durable recovery lineage and exact-member control plane.
146
+ - Private backend release records document durable recovery lineage and the exact-member control plane.
147
147
  - [Durable recovery implementation plan](../../plans/2026-08-05-001-trackly-apply-durable-recovery-reconciliation-plan.md) — historical design record aligned with the released all-or-nothing recovery contract.
@@ -37,7 +37,7 @@ The OpenAI Platform could authenticate to Trackly but could not complete its man
37
37
 
38
38
  OpenAI's current MCP client probes legacy servers with the exact JSON-RPC method `server/discover` before falling back to the initialization-based protocol. Trackly's strict scope gateway treated every unknown method as a scoped operation and returned HTTP 403, so the client interpreted a compatibility probe as an OAuth authorization failure.
39
39
 
40
- The backend fix in `trackly-app/close-ai#1515` added only `server/discover` to the immutable scope-free protocol-method set. It did not add a tool or grant access to user data. The existing legacy SDK then returns JSON-RPC `-32601 Method not found`, which is the compatibility signal the OpenAI client needs to fall back to `initialize` and `tools/list`.
40
+ The coordinated backend compatibility fix added only `server/discover` to the immutable scope-free protocol-method set. It did not add a tool or grant access to user data. The existing legacy SDK then returns JSON-RPC `-32601 Method not found`, which is the compatibility signal the OpenAI client needs to fall back to `initialize` and `tools/list`.
41
41
 
42
42
  The CLI contract alignment in `trackly-app/trackly-cli#106` records the exact backend source and merge provenance, locks the scope-free set and middleware behavior, and verifies the hosted catalog without broadening tool scopes. The coordinated verifier also checks the published Apply schemas, including `truthCertificationInputSchema`, in `scripts/verify-hosted-contract.js`.
43
43
 
@@ -55,5 +55,5 @@ HTTP 401/403 communicates an authentication or authorization failure, so a dual-
55
55
 
56
56
  ## Related Issues
57
57
 
58
- - `trackly-app/close-ai#1515` — backend compatibility fix.
58
+ - Private backend release records document the compatibility fix.
59
59
  - `trackly-app/trackly-cli#106` — packaged contract and verifier alignment.
@@ -74,17 +74,32 @@ API-key creation attempts.
74
74
 
75
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.
76
76
  - **trackly_search_jobs** — Search/filter jobs.
77
- - `function` (14 values): `product`, `engineering`, `design`, `data`, `marketing`, `sales`, `partnerships`, `finance`, `strategy`, `operations`, `people`, `legal`, `support`, `other`
78
- - `locationFilter`: pass ONE of
77
+ - `function` (one value) or `jobFunctions` (nonempty array, OR; use one spelling only). The 14 values are: `product`, `engineering`, `design`, `data`, `marketing`, `sales`, `partnerships`, `finance`, `strategy`, `operations`, `people`, `legal`, `support`, `other`
78
+ - `region`: nonempty array of `us`, `europe`, `latam`, `middle_east`, `asia`, `africa`, `canada`, `oceania`, `remote`, `unknown`, combined with OR. Use instead of `locationFilter`; unlike its legacy list form, this supports US plus other regions.
79
+ - `usStates`: nonempty array of US codes `AL`, `AK`, `AZ`, `AR`, `CA`, `CO`, `CT`, `DC`, `DE`, `FL`, `GA`, `HI`, `ID`, `IL`, `IN`, `IA`, `KS`, `KY`, `LA`, `ME`, `MD`, `MA`, `MI`, `MN`, `MS`, `MO`, `MT`, `NE`, `NV`, `NH`, `NJ`, `NM`, `NY`, `NC`, `ND`, `OH`, `OK`, `OR`, `PA`, `RI`, `SC`, `SD`, `TN`, `TX`, `UT`, `VT`, `VA`, `WA`, `WV`, `WI`, `WY`, `PR`, `REMOTE`.
80
+ - `caProvinces`: nonempty array of `AB`, `BC`, `MB`, `NB`, `NL`, `NS`, `NT`, `NU`, `ON`, `PE`, `QC`, `SK`, `YT`, `REMOTE`. Geographic children follow the backend location rollout: parent-scoped OR applies under canonical enforcement; legacy compatibility remains before that. `REMOTE` alongside codes also includes remote jobs.
81
+ - `internshipLevels`: nonempty array of `undergraduate`, `graduate`, `mba`, `not_specified`, combined with OR. Applies to internships when `jobModality=internship` or `all`; full-time jobs remain unaffected. Set `jobModality` explicitly when you want internships; education levels do not change your saved/default modality.
82
+ - `favoriteCompaniesOnly` (boolean): restrict to your favorited companies; `false` explicitly disables it.
83
+ - `postedWithinDays`: `0`, `1`, `3`, `7`, `14`, `30`; `0` disables the saved posting window. Uses the backend discovery-date policy; omitted uses saved/default preferences.
84
+ - `salaryFloorUsd`: integer `0`–`1000000`. Excludes comparable listed annual-USD minimums below this floor. Unknown, hourly and non-USD salaries remain visible; this is not a guaranteed salary threshold. `0` disables the floor; omitted uses saved/default preferences.
85
+ - `since`: ISO-8601 date or timestamp within the past 90 days, never in the future. Filters when Trackly first saw a job, rather than the employer posting date. Prefer a timestamp with timezone for incremental sync. Other saved filters still apply: use `postedWithinDays:0` and `salaryFloorUsd:0` to disable those saved constraints.
86
+ - `locationFilter`: compatibility alias; use instead of `region`. Pass ONE of
79
87
  - a single scalar: `us`, `non_us`, `all`, or a region tag (`europe`, `latam`, `middle_east`, `asia`, `africa`, `canada`, `oceania`, `remote`, `unknown`), OR
80
88
  - an array of region tags for multi-region (e.g. `["europe", "canada"]`). The array form excludes `us` — combining `us` with other tags in an array causes the backend to silently drop the others. To get "not US" use the scalar `non_us` alone.
81
89
  - `jobModality`: `full_time`, `internship`, `all` — employment type, NOT work arrangement.
82
90
  - `workArrangements`: one or more of `remote`, `hybrid`, `in_person`, `unspecified`. This is an independent axis and combines with region, employment type, and function filters.
83
- - `remote` (boolean): filter to remote jobs only. Maps to `usStates=REMOTE`.
91
+ - `remote` (boolean): filter to remote jobs only. Maps to `usStates=REMOTE`. Do not combine `remote=true` with geographic child arrays; use their `REMOTE` token for alternatives. Do not combine a parent region with `remote=true` or only `REMOTE` children: that backend form overrides the parent. Use `workArrangements:["remote"]` plus your region for remote work within that geography.
84
92
  - `status`: your application pipeline state. Values: `new`, `applied_confirmed`, `check_later`, `not_interested`, `all`.
85
93
  - `sort`: `newest` (default) or `match` (highest match score first; requires a resume on file). Backend rejects the deprecated values `oldest` and `company` with HTTP 400.
86
- - `sponsorship`: `all` (default), `exclude_no`, `only_yes`. `exclude_no` hides jobs with a reliable explicit "does not sponsor" statement (~13% of active US jobs); `only_yes` keeps only jobs with a reliable explicit "sponsors" statement (~2%). Most postings never state a policy either way, so `only_yes` is a very narrow slice.
87
- - `keywords`, `companyId`, `limit`, `offset`.
94
+ - `sponsorship`: `all` (no restriction), `exclude_no` (keep yes **and unknown/not mentioned**, exclude reliable explicit no), `only_yes` (reliable explicit yes only). “Not no” means `exclude_no`, never `only_yes`; ask when intent is ambiguous. Filtering requires `status=new`, which is defaulted when omitted; other pipeline views ignore this filter.
95
+ - `keywords`: legacy keyword search, maximum 500 characters. Title/company/location matching is supported; description recall depends on the backend provider. This is not guaranteed full-catalog literal, phrase or exclusion search.
96
+ - `companyId`: positive integer from company search.
97
+ - `limit`: integer `1`–`100`, MCP default `20`; `offset`: non-negative safe integer, default `0`. Keep all filters and sort unchanged while paging. The live catalog may change between calls.
98
+ - Unsupported keys are rejected before HTTP, including when usage analytics is enabled. Industry, specialization and literal-search controls are not exposed until their backend contract is deployed.
99
+
100
+ Example: `{"jobFunctions":["finance","partnerships"],"region":["us","europe"],"sponsorship":"exclude_no","postedWithinDays":7,"limit":20}`.
101
+ The local package and hosted service have separate release lifecycles. Refresh the client's tool discovery after upgrading or reconnecting and use only parameters its current schema advertises.
102
+
88
103
  - **trackly_get_job** — Get full job details by ID
89
104
  - **trackly_search_companies** — Semantic company search
90
105
  - **trackly_list_companies** — List all tracked companies with job counts
package/mcp/analytics.js CHANGED
@@ -615,11 +615,14 @@ function addOptionalContextToTools(server) {
615
615
  if (Object.hasOwn(shape, 'context')) continue;
616
616
 
617
617
  const originalHandler = tool.handler;
618
+ const schema = tool.inputSchema;
619
+ const context = z.string().optional().describe(CONTEXT_DESCRIPTION);
620
+ // SDK update(paramsSchema) rebuilds objects and drops strict/catchall rules.
621
+ // Retain real object schemas; SDK-created empty Mini schemas need the shape API.
622
+ const paramsSchema = typeof schema.extend === 'function' ? null : { ...shape, context };
623
+ if (paramsSchema === null) tool.inputSchema = schema.extend({ context });
618
624
  tool.update({
619
- paramsSchema: {
620
- ...shape,
621
- context: z.string().optional().describe(CONTEXT_DESCRIPTION),
622
- },
625
+ ...(paramsSchema === null ? {} : { paramsSchema }),
623
626
  callback: (args, extra) => {
624
627
  if (!args || typeof args !== 'object' || !Object.hasOwn(args, 'context')) {
625
628
  return originalHandler(args, extra);
package/mcp/server.js CHANGED
@@ -52,6 +52,14 @@ const JOB_MODALITIES = ['full_time', 'internship', 'all'];
52
52
  // Independent from geography and employment type. Matches the backend's
53
53
  // workArrangements query contract and job_postings constraint.
54
54
  const WORK_ARRANGEMENTS = ['remote', 'hybrid', 'in_person', 'unspecified'];
55
+ const US_STATES = [
56
+ 'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DC', 'DE', 'FL', 'GA', 'HI', 'ID',
57
+ 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO',
58
+ 'MT', 'NE', 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA',
59
+ 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY', 'PR', 'REMOTE',
60
+ ];
61
+ const CA_PROVINCES = ['AB', 'BC', 'MB', 'NB', 'NL', 'NS', 'NT', 'NU', 'ON', 'PE', 'QC', 'SK', 'YT', 'REMOTE'];
62
+ const INTERNSHIP_LEVELS = ['undergraduate', 'graduate', 'mba', 'not_specified'];
55
63
 
56
64
  const MCP_MISSING_CAPABILITY_VALUES = [
57
65
  'search', 'inspect', 'compare', 'rank', 'recommend', 'track', 'update', 'apply',
@@ -254,12 +262,21 @@ function createServer() {
254
262
  version: PACKAGE_VERSION,
255
263
  });
256
264
 
257
- server.tool(
265
+ server.registerTool(
258
266
  'trackly_search_jobs',
259
- 'Search and filter job postings. Returns matching jobs with title, company, location, and structured fields. Use companyId to filter jobs at a specific company (get companyId from trackly_search_companies first). Work arrangement is independent from region and employment type: use workArrangements for remote, hybrid, in-person, or unspecified classifications.',
260
- {
267
+ { description: 'Search and filter job postings. Returns one matching page with title, company, location, structured fields and backend pagination. Use jobFunctions for multiple roles, region for multiple regions, and usStates/caProvinces for geographic detail. Work arrangement is independent from geography and employment type. Unknown sponsorship and non-comparable salary data stay visible with exclude_no and salaryFloorUsd. Never invent industry, specialization, exclusion or literal-search controls; ask when sponsorship intent is ambiguous.',
268
+ inputSchema: z.object({
261
269
  function: z.enum(JOB_FUNCTIONS).optional().describe('Job function filter. One of: ' + JOB_FUNCTIONS.join(', ')),
262
- companyId: z.number().optional().describe('Filter jobs by company ID (get from trackly_search_companies)'),
270
+ jobFunctions: z.array(z.enum(JOB_FUNCTIONS)).min(1).max(14).optional().describe('Multiple job functions, combined with OR. Use instead of function. Omit both to request all 14 functions.'),
271
+ companyId: z.number().int().positive().optional().describe('Filter jobs by company ID (get from trackly_search_companies)'),
272
+ region: z.array(z.enum(REGION_TAGS)).min(1).max(10).optional().describe('Region tags combined with OR, including us. Use instead of locationFilter; this avoids its legacy mixed-US list limitation. Geographic child behavior follows the backend location rollout.'),
273
+ usStates: z.array(z.enum(US_STATES)).min(1).max(53).optional().describe('US state codes, DC, PR, or REMOTE. Multiple values select alternatives. REMOTE alongside codes also includes remote jobs. Under canonical location enforcement children select their parent; legacy rollout keeps compatibility behavior. Do not combine with remote=true.'),
274
+ caProvinces: z.array(z.enum(CA_PROVINCES)).min(1).max(14).optional().describe('Canadian province/territory codes or REMOTE. Multiple values select alternatives. REMOTE alongside codes also includes remote jobs. Parent-scoped OR requires canonical location enforcement. Do not combine with remote=true.'),
275
+ internshipLevels: z.array(z.enum(INTERNSHIP_LEVELS)).min(1).max(4).optional().describe('Internship education levels, combined with OR: undergraduate, graduate, mba, not_specified. Applies to internships in jobModality=internship or all; full-time jobs remain unaffected. This does not change the saved/default modality: set jobModality explicitly when you want internships.'),
276
+ favoriteCompaniesOnly: z.boolean().optional().describe('true limits results to your favorited companies; false explicitly disables this restriction.'),
277
+ postedWithinDays: z.union([z.literal(0), z.literal(1), z.literal(3), z.literal(7), z.literal(14), z.literal(30)]).optional().describe('Posting-recency window in days: 1, 3, 7, 14, or 30; 0 disables the saved window. Uses the backend discovery-date policy; not an incremental-sync cursor. Omit to use the saved/default window.'),
278
+ salaryFloorUsd: z.number().int().min(0).max(1_000_000).optional().describe('Annual USD minimum salary floor. Excludes comparable listed annual-USD minimums below this amount; unknown, hourly and non-USD salaries remain visible. 0 disables the floor; omitted uses saved/default preferences. This is not a guaranteed salary threshold.'),
279
+ since: z.string().regex(/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/).optional().describe('Incremental sync: jobs first seen by Trackly since this ISO-8601 date/timestamp, not employer posting dates. Must be within the past 90 days and not in the future; backend validates the range. Prefer a timestamp with timezone. Other saved filters still apply; use postedWithinDays=0 and salaryFloorUsd=0 to disable those saved constraints.'),
263
280
  locationFilter: z.union([
264
281
  z.enum(['us', 'non_us', 'all']),
265
282
  z.enum(REGION_TAGS),
@@ -273,19 +290,34 @@ function createServer() {
273
290
  workArrangements: z.array(z.enum(WORK_ARRANGEMENTS)).min(1).max(4).optional().describe(
274
291
  'Work arrangement filter, independent from geography and employment type. Values: remote, hybrid, in_person, unspecified. Multiple values use OR semantics.'
275
292
  ),
276
- remote: z.boolean().optional().describe('Filter to remote jobs only (maps to usStates=REMOTE).'),
293
+ remote: z.boolean().optional().describe('Filter to global remote-only jobs (maps to usStates=REMOTE). Do not combine with region/locationFilter or geographic child arrays. For remote work within a region, use workArrangements=["remote"] with region instead.'),
277
294
  status: z.enum(STATUS_VALUES).optional().describe(
278
295
  'Filter by YOUR application pipeline state. Not a generic job-posting status. Values: ' + STATUS_VALUES.join(', ')
279
296
  ),
280
297
  sort: z.enum(SORT_VALUES).optional().describe('Sort order: newest (default) or match (highest match score first; requires resume). Backend rejects legacy oldest/company with HTTP 400.'),
281
298
  sponsorship: z.enum(SPONSORSHIP_VALUES).optional().describe(
282
- "Visa sponsorship filter. 'exclude_no' hides jobs with a reliable explicit \"does not sponsor\" statement (~13% of active US jobs); 'only_yes' keeps only jobs with a reliable explicit \"sponsors\" statement (~2% — most postings never state a policy either way). Default 'all' shows every job. Applies only to the discovery view: when you pass a filtering mode without `status`, status defaults to 'new' (the backend ignores the filter on tracked/pipeline views)."
299
+ "Visa sponsorship: all applies no filter; exclude_no removes reliable explicit no statements and keeps yes plus unknown/not mentioned; only_yes requires reliable explicit yes and excludes unknown. 'Not no' means exclude_no, never only_yes. Ask when intent is ambiguous. Filtering requires status=new (defaulted when omitted); the backend ignores sponsorship on other pipeline views."
283
300
  ),
284
- limit: z.number().max(50).optional().describe('Max results (default 20, max 50)'),
285
- offset: z.number().min(0).optional().describe('Pagination offset'),
286
- keywords: z.string().max(500).optional().describe('Keyword search in title, company, or description'),
287
- },
301
+ limit: z.number().int().min(1).max(100).optional().describe('Page size, integer 1-100; MCP default 20 for concise agent results. Request up to the full API limit of 100 when needed.'),
302
+ offset: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional().describe('Pagination offset, non-negative integer. Keep filters and sort unchanged when paging; live catalog changes can move rows.'),
303
+ keywords: z.string().max(500).optional().describe('Legacy keyword search. Title/company/location matches are supported; description recall depends on the backend provider. Not a guaranteed full-catalog literal/phrase/exclusion search.'),
304
+ }).strict() },
288
305
  wrapTool(async (params) => {
306
+ if (params.function !== undefined && params.jobFunctions !== undefined) {
307
+ throw new Error('Use either function or jobFunctions, not both.');
308
+ }
309
+ if (params.region !== undefined && params.locationFilter !== undefined) {
310
+ throw new Error('Use either region or locationFilter, not both.');
311
+ }
312
+ if (params.remote === true && (params.usStates !== undefined || params.caProvinces !== undefined)) {
313
+ throw new Error('Use remote=true alone for remote-only jobs, or include REMOTE in the geographic arrays for alternatives.');
314
+ }
315
+ const hasParentLocation = params.region !== undefined || params.locationFilter !== undefined;
316
+ const childLocations = [...(params.usStates ?? []), ...(params.caProvinces ?? [])];
317
+ const remoteOnlyChildren = childLocations.length > 0 && childLocations.every((value) => value === 'REMOTE');
318
+ if (hasParentLocation && (params.remote === true || remoteOnlyChildren)) {
319
+ throw new Error('REMOTE-only location overrides the parent region. Use workArrangements=["remote"] with region or locationFilter for remote work within that geography.');
320
+ }
289
321
  const qs = new URLSearchParams();
290
322
  // When `function` isn't specified, request ALL canonical functions so the
291
323
  // backend takes the all-roles short-circuit (granola-followup-app
@@ -296,6 +328,13 @@ function createServer() {
296
328
  // Health (id=3350) — both had non-PM-only job sets and search_jobs
297
329
  // returned total=0 without an explicit function filter.
298
330
  qs.set('jobFunction', params.function !== undefined ? params.function : JOB_FUNCTIONS.join(','));
331
+ if (params.jobFunctions !== undefined) qs.set('jobFunction', params.jobFunctions.join(','));
332
+ for (const key of ['region', 'usStates', 'caProvinces', 'internshipLevels']) {
333
+ if (params[key] !== undefined) qs.set(key, params[key].join(','));
334
+ }
335
+ for (const key of ['favoriteCompaniesOnly', 'postedWithinDays', 'salaryFloorUsd', 'since']) {
336
+ if (params[key] !== undefined) qs.set(key, String(params[key]));
337
+ }
299
338
  if (params.companyId !== undefined) qs.set('companyId', String(params.companyId));
300
339
  if (params.locationFilter !== undefined) {
301
340
  const value = Array.isArray(params.locationFilter)
@@ -318,7 +357,7 @@ function createServer() {
318
357
  qs.set('status', 'new');
319
358
  }
320
359
  }
321
- if (params.limit !== undefined) qs.set('limit', String(params.limit));
360
+ qs.set('limit', String(params.limit ?? 20));
322
361
  if (params.offset !== undefined) qs.set('offset', String(params.offset));
323
362
  if (params.keywords !== undefined) qs.set('search', params.keywords);
324
363
  return apiRequest('GET', `/api/jobscout/jobs?${qs.toString()}`, null, false, false, MCP_USER_AGENT);
@@ -462,7 +501,7 @@ function createServer() {
462
501
  if (askResult.jobsUrl) {
463
502
  // Path allowlist: /ask returns a jobsUrl string. normalizeEndpoint already blocks
464
503
  // cross-origin fetches, but a compromised backend could emit a same-origin path
465
- // like `/api/admin/secret-dump`. Only follow the two handlers /ask is designed to
504
+ // outside the job API. Only follow the two handlers /ask is designed to
466
505
  // route to. Mirrors the same guard in bin/trackly:cmdAsk.
467
506
  const JOBS_URL_ALLOWLIST = /^\/api\/(v1|jobscout)\/jobs(\?|$)/;
468
507
  if (JOBS_URL_ALLOWLIST.test(askResult.jobsUrl)) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "trackly-cli",
9
- "version": "0.20.1",
9
+ "version": "0.21.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "1.32.1",
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "mcpName": "io.github.trackly-app/trackly",
5
- "description": "AI job search CLI + hosted MCP server with OAuth. 250K+ jobs, 5,400+ companies, 40+ ATS. Works with Claude, ChatGPT, Cursor, Windsurf, Codex via hosted streamable-http or local stdio.",
5
+ "description": "Search 250K+ jobs across 5,400+ companies with the Trackly CLI and MCP server for Claude, ChatGPT, Codex and Cursor. Filter by role, location, salary and sponsorship; track jobs and prepare applications.",
6
6
  "bin": {
7
7
  "trackly": "./bin/trackly"
8
8
  },
@@ -52,7 +52,12 @@
52
52
  "job-application",
53
53
  "job-hunting",
54
54
  "career",
55
- "terminal"
55
+ "terminal",
56
+ "codex",
57
+ "job-search-mcp",
58
+ "visa-sponsorship",
59
+ "internships",
60
+ "remote-jobs"
56
61
  ],
57
62
  "author": "Kevin Astuhuaman <hello@usetrackly.app>",
58
63
  "license": "MIT",
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.trackly-app/trackly",
4
4
  "title": "Trackly CLI",
5
- "description": "Job search for Claude, ChatGPT, Cursor. 250K+ jobs, 5,400+ companies. OAuth or stdio.",
5
+ "description": "Search 250K+ jobs, 5,400+ companies. MCP for Claude, ChatGPT, Codex and Cursor.",
6
6
  "repository": {
7
7
  "url": "https://github.com/trackly-app/trackly-cli",
8
8
  "source": "github"
@@ -17,12 +17,12 @@
17
17
  ]
18
18
  }
19
19
  ],
20
- "version": "0.20.1",
20
+ "version": "0.21.0",
21
21
  "packages": [
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "trackly-cli",
25
- "version": "0.20.1",
25
+ "version": "0.21.0",
26
26
  "runtimeHint": "npx",
27
27
  "runtimeArguments": [
28
28
  {