trackly-cli 0.15.0 → 0.16.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
@@ -46,6 +46,7 @@ trackly jobs --region us # Filter by region (us, non_us, all, or a
46
46
  trackly jobs --job-type internship # Filter by employment type (full_time, internship, all)
47
47
  trackly jobs --work-arrangement hybrid,remote # Filter by work arrangement independently
48
48
  trackly jobs --function product # Filter by function
49
+ trackly jobs --sponsorship exclude_no # Visa sponsorship filter (all, exclude_no, only_yes)
49
50
  trackly jobs --company 243 # Filter by company ID
50
51
  trackly job 1234 # Get job details
51
52
  trackly jobs 1234 # Alias for job details
@@ -161,7 +162,7 @@ Then use natural language in any of these clients:
161
162
 
162
163
  | Tool | Description |
163
164
  |------|-------------|
164
- | trackly_search_jobs | Search and filter jobs by function, company, region, employment type, work arrangement, and status |
165
+ | trackly_search_jobs | Search and filter jobs by function, company, region, employment type, work arrangement, visa sponsorship, and status |
165
166
  | trackly_get_job | Get full details for a specific job |
166
167
  | trackly_search_companies | Semantic company search |
167
168
  | trackly_list_companies | List all tracked companies |
package/bin/trackly CHANGED
@@ -39,6 +39,7 @@ const VALUE_FLAGS = new Set([
39
39
  'region',
40
40
  'search',
41
41
  'sort',
42
+ 'sponsorship',
42
43
  'status',
43
44
  'url',
44
45
  'window-days',
@@ -62,7 +63,7 @@ const COMMAND_FLAGS = {
62
63
  login: [],
63
64
  logout: [],
64
65
  whoami: [],
65
- jobs: ['function', 'company', 'region', 'job-type', 'work-arrangement', 'remote', 'status', 'sort', 'limit', 'offset', 'keywords', 'location', 'modality'],
66
+ jobs: ['function', 'company', 'region', 'job-type', 'work-arrangement', 'remote', 'status', 'sort', 'sponsorship', 'limit', 'offset', 'keywords', 'location', 'modality'],
66
67
  job: [],
67
68
  companies: ['limit', 'offset'],
68
69
  search: [],
@@ -94,6 +95,15 @@ const STATUS_LABELS = {
94
95
  dismissed: 'dismissed',
95
96
  };
96
97
  const WORK_ARRANGEMENT_VALUES = ['remote', 'hybrid', 'in_person', 'unspecified'];
98
+ // Discovery-only visa sponsorship filter (close-ai PR #1578). Mirrors the
99
+ // hosted MCP enum exactly. Absent or 'all' applies no filter.
100
+ const SPONSORSHIP_VALUES = ['all', 'exclude_no', 'only_yes'];
101
+ // Job-detail `sponsorshipStatus` values → user-facing copy. Anything else
102
+ // (including null/'unknown') reads "Not mentioned" — never "unknown".
103
+ const SPONSORSHIP_STATUS_LABELS = {
104
+ yes: 'Sponsors visa',
105
+ no: 'No sponsorship',
106
+ };
97
107
  const JOB_FUNCTIONS = [
98
108
  'product', 'engineering', 'design', 'data', 'marketing', 'sales', 'partnerships',
99
109
  'finance', 'strategy', 'operations', 'people', 'legal', 'support', 'other',
@@ -324,7 +334,7 @@ function hasJobListFilters(args) {
324
334
  // (which then raises the migration error) instead of cmdJob(42) silently ignoring
325
335
  // the filter. See PR #13 Cursor Bugbot finding.
326
336
  const deprecated = Object.keys(DEPRECATED_JOB_FLAGS);
327
- const active = ['function', 'company', 'keywords', 'limit', 'region', 'job-type', 'work-arrangement', 'remote', 'offset', 'sort', 'status'];
337
+ const active = ['function', 'company', 'keywords', 'limit', 'region', 'job-type', 'work-arrangement', 'remote', 'offset', 'sort', 'sponsorship', 'status'];
328
338
  return [...active, ...deprecated].some((key) => args[key] !== undefined);
329
339
  }
330
340
 
@@ -598,6 +608,17 @@ async function cmdJobs(args) {
598
608
  }
599
609
  }
600
610
 
611
+ let sponsorship;
612
+ if (args.sponsorship !== undefined) {
613
+ if (typeof args.sponsorship !== 'string' || !args.sponsorship.trim()) {
614
+ failNow(`--sponsorship requires a value. Use: ${SPONSORSHIP_VALUES.join(', ')}.`);
615
+ }
616
+ sponsorship = args.sponsorship.trim();
617
+ if (!SPONSORSHIP_VALUES.includes(sponsorship)) {
618
+ failNow(`Invalid --sponsorship value "${sponsorship}". Use: ${SPONSORSHIP_VALUES.join(', ')}.`);
619
+ }
620
+ }
621
+
601
622
  const params = new URLSearchParams();
602
623
  if (args.function) params.set('jobFunction', args.function);
603
624
  if (args.company) params.set('companyId', args.company);
@@ -607,6 +628,13 @@ async function cmdJobs(args) {
607
628
  if (args.remote) params.set('usStates', 'REMOTE');
608
629
  if (args.status) params.set('status', args.status);
609
630
  if (args.sort) params.set('sort', args.sort);
631
+ if (sponsorship) {
632
+ params.set('sponsorship', sponsorship);
633
+ // The backend honors the filter only on the literal status=new discovery
634
+ // view — without this default, `--sponsorship exclude_no` alone would be
635
+ // a silent no-op. Mirrors the MCP server's behavior.
636
+ if (sponsorship !== 'all' && !args.status) params.set('status', 'new');
637
+ }
610
638
  if (args.limit) params.set('limit', args.limit);
611
639
  if (args.offset) params.set('offset', args.offset);
612
640
  if (args.keywords) params.set('search', args.keywords);
@@ -638,6 +666,12 @@ async function cmdJob(args) {
638
666
  if (job.location) console.log(`Location: ${job.location}`);
639
667
  if (job.jobUrl) console.log(`URL: ${color('dim', job.jobUrl)}`);
640
668
  if (job.postedAt) console.log(`Posted: ${job.postedAt}`);
669
+ // Field presence, not truthiness: a returned null/'unknown' status is a real
670
+ // "Not mentioned" answer from the backend. Only an older backend that omits
671
+ // the key entirely should render no Sponsorship line at all.
672
+ if (job && typeof job === 'object' && 'sponsorshipStatus' in job) {
673
+ console.log(`Sponsorship: ${SPONSORSHIP_STATUS_LABELS[job.sponsorshipStatus] || 'Not mentioned'}`);
674
+ }
641
675
  if (typeof job.description === 'string' && job.description.length > 0) {
642
676
  console.log();
643
677
  console.log(color('bold', 'Description:'));
@@ -1277,7 +1311,7 @@ ${color('bold', 'Commands:')}
1277
1311
  ${color('cyan', 'login')} Authenticate via Google OAuth
1278
1312
  ${color('cyan', 'logout')} Clear credentials
1279
1313
  ${color('cyan', 'whoami')} Show current user
1280
- ${color('cyan', 'jobs')} List jobs (--function, --company <id>, --region <tag>, --job-type <type>, --work-arrangement <values>, --remote, --status, --sort, --limit, --offset, --keywords)
1314
+ ${color('cyan', 'jobs')} List jobs (--function, --company <id>, --region <tag>, --job-type <type>, --work-arrangement <values>, --remote, --status, --sort, --sponsorship, --limit, --offset, --keywords)
1281
1315
  ${color('cyan', 'job <id>')} Get full job detail
1282
1316
  ${color('cyan', 'jobs <id>')} Alias for job <id>
1283
1317
  ${color('cyan', 'companies')} List companies
@@ -1333,6 +1367,10 @@ ${color('bold', 'Filter values:')}
1333
1367
  --status new, applied_confirmed, check_later, not_interested, all
1334
1368
  (filters by YOUR application pipeline state)
1335
1369
  --sort newest (default), match (highest match score first, requires resume)
1370
+ --sponsorship all (default), exclude_no, only_yes
1371
+ exclude_no hides jobs that explicitly state they do not sponsor
1372
+ (~13% of active US jobs); only_yes keeps only jobs that explicitly
1373
+ state they sponsor (~2%). Most postings never state a policy.
1336
1374
 
1337
1375
  ${color('bold', 'MCP Setup:')}
1338
1376
  Claude Code: claude mcp add --scope user trackly -- trackly mcp
@@ -83,6 +83,7 @@ API-key creation attempts.
83
83
  - `remote` (boolean): filter to remote jobs only. Maps to `usStates=REMOTE`.
84
84
  - `status`: your application pipeline state. Values: `new`, `applied_confirmed`, `check_later`, `not_interested`, `all`.
85
85
  - `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.
86
87
  - `keywords`, `companyId`, `limit`, `offset`.
87
88
  - **trackly_get_job** — Get full job details by ID
88
89
  - **trackly_search_companies** — Semantic company search
package/mcp/analytics.js CHANGED
@@ -111,11 +111,11 @@ const SAFE_ENUM_VALUES = new Set([
111
111
  'unspecified', 'us', 'non_us', 'europe', 'latam', 'middle_east', 'asia', 'africa',
112
112
  'canada', 'oceania', 'unknown', 'product', 'engineering', 'design', 'data',
113
113
  'marketing', 'sales', 'partnerships', 'finance', 'strategy', 'operations', 'people',
114
- 'legal', 'support', 'other', 'newest', 'match',
114
+ 'legal', 'support', 'other', 'newest', 'match', 'exclude_no', 'only_yes',
115
115
  ]);
116
116
  const SAFE_ENUM_KEYS = new Set([
117
117
  'status', 'stage', 'function', 'jobfunction', 'jobmodality', 'workarrangement',
118
- 'region', 'regiontag', 'locationfilter', 'sort',
118
+ 'region', 'regiontag', 'locationfilter', 'sort', 'sponsorship',
119
119
  ]);
120
120
  const SDK_FAILURE_WARNING = /Warning: (?:Failed to instrument server|Failed to setup tool call instrumentation|No PostHog client passed)/i;
121
121
 
@@ -474,7 +474,7 @@ function projectMcpParameters(toolName, parameters) {
474
474
  const allowed = {
475
475
  trackly_search_jobs: new Set([
476
476
  'function', 'companyId', 'locationFilter', 'jobModality', 'workArrangements',
477
- 'remote', 'status', 'sort', 'limit', 'offset', 'keywords',
477
+ 'remote', 'status', 'sort', 'sponsorship', 'limit', 'offset', 'keywords',
478
478
  ]),
479
479
  trackly_get_job: new Set(['id']),
480
480
  trackly_search_companies: new Set(['query', 'limit']),
package/mcp/server.js CHANGED
@@ -67,6 +67,12 @@ const MCP_MISSING_DESTINATION_VALUES = [
67
67
  'trackly', 'external_site', 'file', 'email', 'calendar', 'crm', 'browser', 'other',
68
68
  ];
69
69
 
70
+ // `sponsorship` enum matches the backend's discovery-only filter (close-ai
71
+ // PR #1578). Absent or 'all' applies no filter; only ~2% of active US jobs
72
+ // carry an explicit "sponsors" statement and ~13% an explicit "does not
73
+ // sponsor" one, so the remaining ~85% never state a policy either way.
74
+ const SPONSORSHIP_VALUES = ['all', 'exclude_no', 'only_yes'];
75
+
70
76
  // `sort` enum matches backend handler at `jobscout.ts:3053` — NOT the pre-fix
71
77
  // `newest|oldest|company` (backend rejects oldest/company with HTTP 400).
72
78
  const SORT_VALUES = ['newest', 'match'];
@@ -254,6 +260,9 @@ function createServer() {
254
260
  'Filter by YOUR application pipeline state. Not a generic job-posting status. Values: ' + STATUS_VALUES.join(', ')
255
261
  ),
256
262
  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.'),
263
+ sponsorship: z.enum(SPONSORSHIP_VALUES).optional().describe(
264
+ "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)."
265
+ ),
257
266
  limit: z.number().max(50).optional().describe('Max results (default 20, max 50)'),
258
267
  offset: z.number().min(0).optional().describe('Pagination offset'),
259
268
  keywords: z.string().max(500).optional().describe('Keyword search in title, company, or description'),
@@ -281,6 +290,16 @@ function createServer() {
281
290
  if (params.remote === true) qs.set('usStates', 'REMOTE');
282
291
  if (params.status !== undefined) qs.set('status', params.status);
283
292
  if (params.sort !== undefined) qs.set('sort', params.sort);
293
+ if (params.sponsorship !== undefined) {
294
+ qs.set('sponsorship', String(params.sponsorship));
295
+ // The backend applies the filter only on the literal status=new
296
+ // discovery view; without this default an agent passing a filtering
297
+ // mode alone would get silently unfiltered results. Mirrors
298
+ // close-ai src/mcp/server.ts.
299
+ if (params.sponsorship !== 'all' && params.status === undefined) {
300
+ qs.set('status', 'new');
301
+ }
302
+ }
284
303
  if (params.limit !== undefined) qs.set('limit', String(params.limit));
285
304
  if (params.offset !== undefined) qs.set('offset', String(params.offset));
286
305
  if (params.keywords !== undefined) qs.set('search', params.keywords);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "trackly-cli",
9
- "version": "0.15.0",
9
+ "version": "0.16.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "1.30.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "mcpName": "io.github.trackly-app/trackly",
5
5
  "description": "AI job search CLI + hosted MCP server with OAuth. 170K+ jobs, 3,800+ companies, 40+ ATS. Works with Claude, ChatGPT, Cursor, Windsurf, Codex via hosted streamable-http or local stdio.",
6
6
  "bin": {
package/server.json CHANGED
@@ -17,12 +17,12 @@
17
17
  ]
18
18
  }
19
19
  ],
20
- "version": "0.15.0",
20
+ "version": "0.16.0",
21
21
  "packages": [
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "trackly-cli",
25
- "version": "0.15.0",
25
+ "version": "0.16.0",
26
26
  "runtimeHint": "npx",
27
27
  "runtimeArguments": [
28
28
  {