trackly-cli 0.21.0 → 0.21.2
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 +3 -1
- package/docs/solutions/integration-issues/analytics-must-preserve-strict-mcp-schemas.md +14 -0
- package/docs/trackly-tools.md +3 -3
- package/mcp/analytics.js +63 -2
- package/mcp/server.js +8 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -353,7 +353,9 @@ a missing capability. Run `trackly mcp` or use
|
|
|
353
353
|
|
|
354
354
|
MCP usage analytics are on by default and relay through Trackly so the free
|
|
355
355
|
service can be debugged and improved. They exclude résumé text, profile answers,
|
|
356
|
-
demographic or work-authorization answers, and application notes.
|
|
356
|
+
demographic or work-authorization answers, and application notes. Client attribution
|
|
357
|
+
uses a fixed family label; errors use categories, and search text is retained only
|
|
358
|
+
as length. Raw queries and conversation text are excluded. Turn off future
|
|
357
359
|
collection with **Share usage analytics** in Account Settings. Operators may also
|
|
358
360
|
disable local MCP instrumentation with `TRACKLY_MCP_ANALYTICS_DISABLED=1` (or
|
|
359
361
|
`TRACKLY_MCP_ANALYTICS_ENABLED=0`); the account setting remains authoritative
|
|
@@ -37,3 +37,17 @@ Verification: `node --test test/mcp-analytics.test.js test/mcp-schema.test.js`
|
|
|
37
37
|
and `npm test`. The real transport regression failed with the old instrumentation
|
|
38
38
|
and passed after the fix. Keyword telemetry continues to retain length only;
|
|
39
39
|
this change does not permit raw query or context storage.
|
|
40
|
+
|
|
41
|
+
Error and client attribution also need real SDK coverage. The SDK's error type
|
|
42
|
+
may be a JavaScript constructor name, and its vendor field comes from HTTP
|
|
43
|
+
headers that stdio does not have. Normalize both at the privacy boundary:
|
|
44
|
+
derive a closed error category from the failure and a closed client family from
|
|
45
|
+
available metadata, with explicit `none`, `other` and `unknown` states.
|
|
46
|
+
|
|
47
|
+
The HTTP client rejects response records containing numeric status and an
|
|
48
|
+
`error` field. Capturing only `message` loses that status and substitutes a
|
|
49
|
+
generic fallback. Preserve the status in the telemetry-only exception and give
|
|
50
|
+
it precedence over endpoint words such as "network brief". Verify job detail
|
|
51
|
+
and brief calls through a local HTTP fixture with 404, 429 and 500 responses;
|
|
52
|
+
assert unchanged tool results and classified, content-free telemetry. Sanitize
|
|
53
|
+
the result twice because the SDK hook and backend relay both project events.
|
package/docs/trackly-tools.md
CHANGED
|
@@ -80,9 +80,9 @@ API-key creation attempts.
|
|
|
80
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
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
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.
|
|
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 only with status omitted, `new`, or `all`. Explicit pipeline views (`applied_confirmed`, `check_later`, `not_interested`) skip the saved window/floor; explicit request values still apply.
|
|
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 only with status omitted, `new`, or `all`. Explicit pipeline views (`applied_confirmed`, `check_later`, `not_interested`) skip the saved window/floor; explicit request values still apply.
|
|
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. Since does not reset filters. Saved posting and salary constraints apply only with status omitted, `new`, or `all`; explicit pipeline views skip those saved constraints while retaining explicit request filters: use `postedWithinDays:0` and `salaryFloorUsd:0` to disable those saved constraints.
|
|
86
86
|
- `locationFilter`: compatibility alias; use instead of `region`. Pass ONE of
|
|
87
87
|
- a single scalar: `us`, `non_us`, `all`, or a region tag (`europe`, `latam`, `middle_east`, `asia`, `africa`, `canada`, `oceania`, `remote`, `unknown`), OR
|
|
88
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.
|
package/mcp/analytics.js
CHANGED
|
@@ -118,6 +118,28 @@ const SAFE_ENUM_KEYS = new Set([
|
|
|
118
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
|
+
const ERROR_CATEGORIES = new Set([
|
|
122
|
+
'authentication', 'authorization', 'rate_limit', 'timeout', 'not_found',
|
|
123
|
+
'validation', 'conflict', 'network', 'server', 'unknown',
|
|
124
|
+
]);
|
|
125
|
+
const HTTP_ERROR_CATEGORIES = {
|
|
126
|
+
400: 'validation', 401: 'authentication', 403: 'authorization',
|
|
127
|
+
404: 'not_found', 408: 'timeout', 409: 'conflict', 410: 'not_found',
|
|
128
|
+
413: 'validation', 422: 'validation', 429: 'rate_limit',
|
|
129
|
+
};
|
|
130
|
+
const CLIENT_FAMILIES = [
|
|
131
|
+
['chatgpt', /\bchatgpt\b/],
|
|
132
|
+
['claude', /\bclaude(?: desktop)?\b/],
|
|
133
|
+
['codex', /\bcodex\b/],
|
|
134
|
+
['cursor', /\bcursor\b/],
|
|
135
|
+
['windsurf', /\bwindsurf\b/],
|
|
136
|
+
['vscode', /\b(?:vscode|visual studio code)\b/],
|
|
137
|
+
['continue', /\bcontinue\b/],
|
|
138
|
+
['cline', /\bcline\b/],
|
|
139
|
+
['roo', /\broo(?: code)?\b/],
|
|
140
|
+
['zed', /\bzed\b/],
|
|
141
|
+
['mcp-inspector', /\bmcp inspector\b/],
|
|
142
|
+
];
|
|
121
143
|
|
|
122
144
|
const ALLOWED_EVENT_PROPERTY_KEYS = new Set([
|
|
123
145
|
'$exception_list', '$lib', '$lib_version', '$mcp_client_name',
|
|
@@ -250,6 +272,13 @@ function classifyIntent(toolName, value) {
|
|
|
250
272
|
|
|
251
273
|
function classifyError(value) {
|
|
252
274
|
const text = typeof value === 'string' ? value.toLowerCase() : '';
|
|
275
|
+
if (ERROR_CATEGORIES.has(text)) return text;
|
|
276
|
+
// HTTP status is authoritative; endpoint words such as "network brief"
|
|
277
|
+
// must not turn an upstream 500 into a transport/network classification.
|
|
278
|
+
const status = /\bhttp\s+([45]\d\d)\b/.exec(text)?.[1];
|
|
279
|
+
if (status && HTTP_ERROR_CATEGORIES[status]) return HTTP_ERROR_CATEGORIES[status];
|
|
280
|
+
if (status?.startsWith('5')) return 'server';
|
|
281
|
+
if (status) return 'unknown';
|
|
253
282
|
if (/\b(?:401|unauthenticated|authentication|invalid token|expired token)\b/.test(text)) {
|
|
254
283
|
return 'authentication';
|
|
255
284
|
}
|
|
@@ -264,6 +293,38 @@ function classifyError(value) {
|
|
|
264
293
|
return 'unknown';
|
|
265
294
|
}
|
|
266
295
|
|
|
296
|
+
function canonicalClientFamily(value) {
|
|
297
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 240) return undefined;
|
|
298
|
+
const normalized = value.toLowerCase().replace(/[_-]/g, ' ');
|
|
299
|
+
return CLIENT_FAMILIES.find(([, pattern]) => pattern.test(normalized))?.[0];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function projectEventAttribution(properties, eventName) {
|
|
303
|
+
const clientValues = [
|
|
304
|
+
properties.$mcp_vendor_client,
|
|
305
|
+
properties.$mcp_client_name,
|
|
306
|
+
properties.$mcp_client_user_agent,
|
|
307
|
+
];
|
|
308
|
+
const clientFamily = clientValues.map(canonicalClientFamily).find(Boolean)
|
|
309
|
+
|| (clientValues.some((value) => (
|
|
310
|
+
typeof value === 'string' && value.trim() && value !== 'unknown'
|
|
311
|
+
)) ? 'other' : 'unknown');
|
|
312
|
+
properties.$mcp_vendor_client = clientFamily;
|
|
313
|
+
properties.$mcp_client_name = clientFamily;
|
|
314
|
+
if (properties.$mcp_client_user_agent !== undefined) {
|
|
315
|
+
properties.$mcp_client_user_agent = canonicalClientFamily(properties.$mcp_client_user_agent)
|
|
316
|
+
|| 'other';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const errorMessage = properties.$mcp_error_message;
|
|
320
|
+
const exceptionMessage = properties.$exception_list?.[0]?.value;
|
|
321
|
+
const isError = eventName === '$exception' || properties.$mcp_is_error === true;
|
|
322
|
+
properties.$mcp_error_type = isError
|
|
323
|
+
? classifyError(errorMessage ?? exceptionMessage ?? properties.$mcp_error_type)
|
|
324
|
+
: 'none';
|
|
325
|
+
return errorMessage;
|
|
326
|
+
}
|
|
327
|
+
|
|
267
328
|
function safeFrameName(value, maxLength = 120) {
|
|
268
329
|
if (typeof value !== 'string' || value === '<local-path>') return undefined;
|
|
269
330
|
const basename = value.replace(/\\/g, '/').split('/').pop() || '';
|
|
@@ -311,7 +372,7 @@ function projectExceptionList(value, errorValue) {
|
|
|
311
372
|
}).filter((frame) => Object.keys(frame).length > 0);
|
|
312
373
|
return {
|
|
313
374
|
type: safeFrameName(exception.type, 80) || 'Error',
|
|
314
|
-
category: classifyError(exception.value ?? errorValue),
|
|
375
|
+
category: classifyError(exception.category ?? exception.value ?? errorValue),
|
|
315
376
|
stacktrace: { frames: projectedFrames },
|
|
316
377
|
};
|
|
317
378
|
});
|
|
@@ -521,6 +582,7 @@ function sanitizeMcpAnalyticsEvent(event) {
|
|
|
521
582
|
}
|
|
522
583
|
const properties = sanitized.properties;
|
|
523
584
|
if (!properties || typeof properties !== 'object') return sanitized;
|
|
585
|
+
const sourceErrorMessage = projectEventAttribution(properties, event.event);
|
|
524
586
|
|
|
525
587
|
// Identity is backend-owned. The CLI may carry only its ephemeral anonymous
|
|
526
588
|
// session identifier; verified user/person properties never cross this relay.
|
|
@@ -582,7 +644,6 @@ function sanitizeMcpAnalyticsEvent(event) {
|
|
|
582
644
|
delete properties.$mcp_intent_source;
|
|
583
645
|
}
|
|
584
646
|
|
|
585
|
-
const sourceErrorMessage = properties.$mcp_error_message;
|
|
586
647
|
if (typeof sourceErrorMessage === 'string') {
|
|
587
648
|
properties.$mcp_error_message = classifyError(sourceErrorMessage);
|
|
588
649
|
}
|
package/mcp/server.js
CHANGED
|
@@ -232,9 +232,13 @@ function wrapTool(handler, fallbackMessage) {
|
|
|
232
232
|
};
|
|
233
233
|
} catch (error) {
|
|
234
234
|
if (extra && typeof extra === 'object') {
|
|
235
|
+
const statusPrefix = Number.isInteger(error?.status) ? `HTTP ${error.status}: ` : '';
|
|
235
236
|
extra.__mcp_analytics_error = error instanceof Error
|
|
236
237
|
? error
|
|
237
|
-
: new Error(
|
|
238
|
+
: new Error(
|
|
239
|
+
`${statusPrefix}${error?.error || error?.message || fallbackMessage}`,
|
|
240
|
+
{ cause: error },
|
|
241
|
+
);
|
|
238
242
|
}
|
|
239
243
|
return createErrorResult(
|
|
240
244
|
error,
|
|
@@ -274,9 +278,9 @@ function createServer() {
|
|
|
274
278
|
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
279
|
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
280
|
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.
|
|
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
|
|
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.
|
|
281
|
+
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. Omitted uses the saved/default window only when status is omitted, new, or all; explicit pipeline statuses skip that saved window. Explicit values apply in every view.'),
|
|
282
|
+
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 only when status is omitted, new, or all; explicit pipeline statuses skip the saved floor. Explicit values apply in every view. This is not a guaranteed salary threshold.'),
|
|
283
|
+
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. Since does not reset filters. Saved posting and salary constraints apply only when status is omitted, new, or all; use postedWithinDays=0 and salaryFloorUsd=0 to disable them. Explicit pipeline views skip saved filters; explicit request filters still apply.'),
|
|
280
284
|
locationFilter: z.union([
|
|
281
285
|
z.enum(['us', 'non_us', 'all']),
|
|
282
286
|
z.enum(REGION_TAGS),
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackly-cli",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "trackly-cli",
|
|
9
|
-
"version": "0.21.
|
|
9
|
+
"version": "0.21.2",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "1.32.1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackly-cli",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"mcpName": "io.github.trackly-app/trackly",
|
|
5
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": {
|
package/server.json
CHANGED
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
]
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
|
-
"version": "0.21.
|
|
20
|
+
"version": "0.21.2",
|
|
21
21
|
"packages": [
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "trackly-cli",
|
|
25
|
-
"version": "0.21.
|
|
25
|
+
"version": "0.21.2",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"runtimeArguments": [
|
|
28
28
|
{
|