trackly-cli 0.22.1 → 0.22.3
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 +9 -12
- package/docs/trackly-tools.md +1 -1
- package/lib/agent.js +1 -1
- package/lib/client.js +25 -136
- package/mcp/analytics.js +3 -38
- package/mcp/apply-tools.js +4 -3
- package/mcp/server.js +1 -1
- package/npm-shrinkwrap.json +33 -33
- package/package.json +10 -9
- package/scripts/verify-audit-exceptions.js +10 -2
- package/server.json +3 -3
- package/skills/trackly-apply/SKILL.md +6 -4
- package/skills/trackly-apply/references/onboarding.md +9 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
The only job tracking CLI built for AI agents.
|
|
9
9
|
|
|
10
|
-
Search
|
|
10
|
+
Search Thousands of jobs across Thousands of companies and 40+ ATS types. Track applications, get AI-powered recommendations, and manage your job search -- from the terminal or through Claude, ChatGPT, Cursor, and other MCP-compatible AI agents.
|
|
11
11
|
|
|
12
12
|
## Two ways to connect
|
|
13
13
|
|
|
@@ -35,7 +35,7 @@ trackly jobs --function product
|
|
|
35
35
|
|
|
36
36
|
## At a Glance
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Thousands of companies | Thousands of jobs | 40+ ATS types | CLI + MCP | 58 local + 55 hosted MCP tools
|
|
39
39
|
|
|
40
40
|
## CLI Commands
|
|
41
41
|
|
|
@@ -351,15 +351,12 @@ job-search and application set plus `get_more_tools`, which lets agents report
|
|
|
351
351
|
a missing capability. Run `trackly mcp` or use
|
|
352
352
|
`trackly agent setup --client claude`.
|
|
353
353
|
|
|
354
|
-
MCP usage analytics are
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
uses a fixed family label;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
disable local MCP instrumentation with `TRACKLY_MCP_ANALYTICS_DISABLED=1` (or
|
|
361
|
-
`TRACKLY_MCP_ANALYTICS_ENABLED=0`); the account setting remains authoritative
|
|
362
|
-
for authenticated backend capture.
|
|
354
|
+
MCP usage analytics are part of the free Trackly service and relay through
|
|
355
|
+
Trackly so it can be debugged and improved; there is no account-level opt-out.
|
|
356
|
+
They exclude résumé text, profile answers, demographic or work-authorization
|
|
357
|
+
answers, and application notes. Client attribution uses a fixed family label;
|
|
358
|
+
errors use categories, and search text is retained only as length. Raw queries
|
|
359
|
+
and conversation text are excluded.
|
|
363
360
|
|
|
364
361
|
**How do I use Claude Code for job hunting?**
|
|
365
362
|
|
|
@@ -367,7 +364,7 @@ Add trackly as an MCP server in Claude Code. Then ask questions naturally: "Find
|
|
|
367
364
|
|
|
368
365
|
**What are the best CLI tools for job search?**
|
|
369
366
|
|
|
370
|
-
trackly-cli is the first dedicated job tracking CLI. It provides direct terminal access to
|
|
367
|
+
trackly-cli is the first dedicated job tracking CLI. It provides direct terminal access to Thousands of jobs across Thousands of companies, with filters for job function, location, and work modality. It also integrates with AI agents via the Model Context Protocol (MCP).
|
|
371
368
|
|
|
372
369
|
## Security
|
|
373
370
|
|
package/docs/trackly-tools.md
CHANGED
|
@@ -72,7 +72,7 @@ API-key creation attempts.
|
|
|
72
72
|
|
|
73
73
|
### Available Tools
|
|
74
74
|
|
|
75
|
-
- **get_more_tools** — Report what the agent wanted to do when no Trackly tool fit, so Trackly can improve its MCP surface. The tool is always available; report delivery is best-effort
|
|
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.
|
|
76
76
|
- **trackly_search_jobs** — Search/filter jobs. A successful page may have `total: null`, `pagination.total: null` and `countStatus: "unavailable"` when its total cannot be computed. The returned jobs remain valid: explain `countGuidance`, use `hasMore` and pagination to continue, and avoid retrying just to obtain a total. Zero is a known count; null is unknown. Genuine page failures remain tool errors.
|
|
77
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
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.
|
package/lib/agent.js
CHANGED
|
@@ -8,7 +8,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
8
8
|
const { apiRequest, downloadFile, getConfigPaths, hasAuth, maintenanceOutput } = require('./client');
|
|
9
9
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
10
10
|
const { contractVersion: MCP_CONTRACT_VERSION } = require('../contracts/trackly-apply-tools.json');
|
|
11
|
-
const SKILL_VERSION = '4.9.
|
|
11
|
+
const SKILL_VERSION = '4.9.2';
|
|
12
12
|
const SKILL_POLICY_REVISION = SKILL_VERSION;
|
|
13
13
|
const CLI_USER_AGENT = `trackly-cli/${PACKAGE_VERSION}`;
|
|
14
14
|
const MCP_USER_AGENT = `trackly-mcp/${PACKAGE_VERSION}`;
|
package/lib/client.js
CHANGED
|
@@ -11,7 +11,11 @@ const APPLY_CONTRACT = require('../contracts/trackly-apply-tools.json');
|
|
|
11
11
|
const DEFAULT_CONFIG_DIR = path.join(os.homedir(), '.trackly');
|
|
12
12
|
const DEFAULT_BASE_URL = 'https://closeai.mba';
|
|
13
13
|
const DEFAULT_HTTP_TIMEOUT_MS = 30000;
|
|
14
|
-
|
|
14
|
+
// Retired local analytics opt-out marker. Usage analytics are part of the free
|
|
15
|
+
// product and cannot be turned off from the account, so older installs that
|
|
16
|
+
// persisted this key must resume capture: it is ignored on load and dropped on
|
|
17
|
+
// every save.
|
|
18
|
+
const RETIRED_ANALYTICS_OPT_OUT_KEY = 'mcpAnalyticsOptOut';
|
|
15
19
|
const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']);
|
|
16
20
|
const MAINTENANCE_CODE = 'maintenance_mode';
|
|
17
21
|
const LEGACY_MAINTENANCE_CODE = 'planned_maintenance';
|
|
@@ -68,14 +72,23 @@ function loadConfig() {
|
|
|
68
72
|
}
|
|
69
73
|
throw error;
|
|
70
74
|
}
|
|
75
|
+
let parsed;
|
|
71
76
|
try {
|
|
72
|
-
|
|
77
|
+
parsed = JSON.parse(raw);
|
|
73
78
|
} catch (error) {
|
|
74
79
|
if (error instanceof SyntaxError) {
|
|
75
80
|
throw new TracklyConfigError(`Trackly config is invalid JSON. Fix or delete ${file}.`);
|
|
76
81
|
}
|
|
77
82
|
throw error;
|
|
78
83
|
}
|
|
84
|
+
return withoutRetiredKeys(parsed);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function withoutRetiredKeys(config) {
|
|
88
|
+
if (!isRecord(config) || !Object.hasOwn(config, RETIRED_ANALYTICS_OPT_OUT_KEY)) return config;
|
|
89
|
+
const next = { ...config };
|
|
90
|
+
delete next[RETIRED_ANALYTICS_OPT_OUT_KEY];
|
|
91
|
+
return next;
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
function ensureConfigDir() {
|
|
@@ -95,7 +108,7 @@ function saveConfig(config) {
|
|
|
95
108
|
// a zero-byte or partial config.json, and the next CLI invocation silently loses all auth.
|
|
96
109
|
// Temp file is created mode 0600 so tokens are never world-readable, even briefly.
|
|
97
110
|
const tmp = `${file}.tmp.${process.pid}`;
|
|
98
|
-
fs.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
111
|
+
fs.writeFileSync(tmp, JSON.stringify(withoutRetiredKeys(config), null, 2), { mode: 0o600 });
|
|
99
112
|
try { fs.chmodSync(tmp, 0o600); } catch (e) {}
|
|
100
113
|
try {
|
|
101
114
|
fs.renameSync(tmp, file);
|
|
@@ -108,25 +121,6 @@ function saveConfig(config) {
|
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
function clearConfig() {
|
|
111
|
-
let config = {};
|
|
112
|
-
try {
|
|
113
|
-
const loaded = loadConfig();
|
|
114
|
-
if (loaded && typeof loaded === 'object' && !Array.isArray(loaded)) config = loaded;
|
|
115
|
-
} catch {
|
|
116
|
-
// Logout must still remove an invalid or unreadable credential file. The
|
|
117
|
-
// opt-out can be preserved only when the existing config is recoverable.
|
|
118
|
-
}
|
|
119
|
-
// A logout clears credentials and endpoint overrides, but the content-free
|
|
120
|
-
// analytics opt-out must continue suppressing anonymous MCP lifecycle events.
|
|
121
|
-
if (config.mcpAnalyticsOptOut === true) {
|
|
122
|
-
try {
|
|
123
|
-
saveConfig({ mcpAnalyticsOptOut: true });
|
|
124
|
-
return;
|
|
125
|
-
} catch {
|
|
126
|
-
// Preserving the marker is best-effort. A disk/write failure must never
|
|
127
|
-
// leave credentials behind, so fall through to unlinking the old file.
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
124
|
const { file } = getConfigPaths();
|
|
131
125
|
try { fs.unlinkSync(file); } catch (e) {}
|
|
132
126
|
}
|
|
@@ -135,13 +129,6 @@ function isRecord(value) {
|
|
|
135
129
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
136
130
|
}
|
|
137
131
|
|
|
138
|
-
function hasAuthenticationInConfig(config) {
|
|
139
|
-
return Boolean(
|
|
140
|
-
(process.env.TRACKLY_API_KEY || '').trim()
|
|
141
|
-
|| (isRecord(config) && (config.apiKey || config.token || config.refreshToken))
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
132
|
function configAfterCredentialRemoval(config, scope) {
|
|
146
133
|
const next = isRecord(config) ? { ...config } : {};
|
|
147
134
|
if (scope === 'all' || scope === 'apiKey') delete next.apiKey;
|
|
@@ -152,81 +139,14 @@ function configAfterCredentialRemoval(config, scope) {
|
|
|
152
139
|
return next;
|
|
153
140
|
}
|
|
154
141
|
|
|
155
|
-
async function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
let timeout;
|
|
161
|
-
try {
|
|
162
|
-
const preference = await Promise.race([
|
|
163
|
-
Promise.resolve().then(() => request(
|
|
164
|
-
'GET',
|
|
165
|
-
'/api/jobscout/analytics-preference',
|
|
166
|
-
null,
|
|
167
|
-
false,
|
|
168
|
-
true,
|
|
169
|
-
null,
|
|
170
|
-
null,
|
|
171
|
-
{ signal: controller.signal, timeoutMs }
|
|
172
|
-
)),
|
|
173
|
-
new Promise((_, reject) => {
|
|
174
|
-
timeout = setTimeout(() => {
|
|
175
|
-
controller.abort();
|
|
176
|
-
reject(new Error('analytics_preference_sync_timeout'));
|
|
177
|
-
}, timeoutMs);
|
|
178
|
-
}),
|
|
179
|
-
]);
|
|
180
|
-
if (typeof preference?.shareUsageAnalytics !== 'boolean') {
|
|
181
|
-
throw new Error('analytics_preference_invalid');
|
|
182
|
-
}
|
|
183
|
-
return preference.shareUsageAnalytics === false;
|
|
184
|
-
} catch {
|
|
185
|
-
// Fail closed: if the authoritative preference cannot be read before the
|
|
186
|
-
// final credential disappears, suppress future anonymous MCP capture.
|
|
187
|
-
return true;
|
|
188
|
-
} finally {
|
|
189
|
-
clearTimeout(timeout);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function synchronizeAnalyticsPreferenceBeforeLogout(
|
|
194
|
-
request = apiRequest,
|
|
195
|
-
options = {}
|
|
196
|
-
) {
|
|
197
|
-
const timeoutMs = options.timeoutMs ?? ANALYTICS_PREFERENCE_SYNC_TIMEOUT_MS;
|
|
198
|
-
try {
|
|
199
|
-
if (!hasAuth()) return;
|
|
200
|
-
} catch {
|
|
201
|
-
// A corrupt or unreadable config must never prevent logout. clearConfig()
|
|
202
|
-
// owns the best-effort removal path and will run immediately afterward.
|
|
142
|
+
async function removeCredentials({ scope = 'all' } = {}) {
|
|
143
|
+
if (scope === 'all') {
|
|
144
|
+
// Logout clears credentials, endpoint overrides and every other config key.
|
|
145
|
+
const { file } = getConfigPaths();
|
|
146
|
+
try { fs.unlinkSync(file); } catch (_) {}
|
|
203
147
|
return;
|
|
204
148
|
}
|
|
205
|
-
const optedOut = await resolveAnalyticsOptOutBeforeCredentialLoss(request, timeoutMs);
|
|
206
|
-
let config = {};
|
|
207
|
-
try {
|
|
208
|
-
const loaded = loadConfig();
|
|
209
|
-
if (loaded && typeof loaded === 'object' && !Array.isArray(loaded)) config = loaded;
|
|
210
|
-
} catch {
|
|
211
|
-
// Environment-provided authentication can still make the preference
|
|
212
|
-
// request valid when the on-disk config is corrupt. Replace only the
|
|
213
|
-
// recoverable analytics marker and let clearConfig() remove credentials.
|
|
214
|
-
}
|
|
215
|
-
if (optedOut) config.mcpAnalyticsOptOut = true;
|
|
216
|
-
else delete config.mcpAnalyticsOptOut;
|
|
217
|
-
try {
|
|
218
|
-
saveConfig(config);
|
|
219
|
-
} catch {
|
|
220
|
-
// Analytics synchronization is best-effort and must never block logout.
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
149
|
|
|
224
|
-
async function removeCredentials({
|
|
225
|
-
scope = 'all',
|
|
226
|
-
request = apiRequest,
|
|
227
|
-
timeoutMs = ANALYTICS_PREFERENCE_SYNC_TIMEOUT_MS,
|
|
228
|
-
preferenceUnavailable = false,
|
|
229
|
-
} = {}) {
|
|
230
150
|
let config = {};
|
|
231
151
|
try {
|
|
232
152
|
const loaded = loadConfig();
|
|
@@ -236,36 +156,7 @@ async function removeCredentials({
|
|
|
236
156
|
// path below. There is no safe credential state to preserve from it.
|
|
237
157
|
}
|
|
238
158
|
|
|
239
|
-
|
|
240
|
-
const removesEphemeralCredential = scope === 'all'
|
|
241
|
-
&& Boolean((process.env.TRACKLY_API_KEY || '').trim());
|
|
242
|
-
const removesFinalCredential = removesEphemeralCredential
|
|
243
|
-
|| (hasAuthenticationInConfig(config) && !hasAuthenticationInConfig(next));
|
|
244
|
-
|
|
245
|
-
if (removesFinalCredential) {
|
|
246
|
-
const optedOut = preferenceUnavailable
|
|
247
|
-
? true
|
|
248
|
-
: await resolveAnalyticsOptOutBeforeCredentialLoss(request, timeoutMs);
|
|
249
|
-
if (optedOut) next.mcpAnalyticsOptOut = true;
|
|
250
|
-
else delete next.mcpAnalyticsOptOut;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (scope === 'all') {
|
|
254
|
-
// Preserve logout's historical behavior of clearing endpoint overrides and
|
|
255
|
-
// all future config keys, with only the anonymous opt-out allowed through.
|
|
256
|
-
if (next.mcpAnalyticsOptOut === true) {
|
|
257
|
-
try {
|
|
258
|
-
saveConfig({ mcpAnalyticsOptOut: true });
|
|
259
|
-
return;
|
|
260
|
-
} catch {
|
|
261
|
-
// Credential removal wins over marker preservation on write failure.
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const { file } = getConfigPaths();
|
|
265
|
-
try { fs.unlinkSync(file); } catch (_) {}
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
159
|
+
const next = configAfterCredentialRemoval(config, scope);
|
|
269
160
|
if (Object.keys(next).length === 0) {
|
|
270
161
|
const { file } = getConfigPaths();
|
|
271
162
|
try { fs.unlinkSync(file); } catch (_) {}
|
|
@@ -661,9 +552,8 @@ async function _doRefresh() {
|
|
|
661
552
|
// every subsequent 401. Without this, a backend that signals failure via JSON instead of
|
|
662
553
|
// a 4xx status would leave the user stuck in a doomed-refresh loop forever.
|
|
663
554
|
// Use the centralized OAuth-only removal — NOT clearConfig() — so the user's
|
|
664
|
-
// baseUrl, apiKey, and other persisted settings survive a token expiry
|
|
665
|
-
|
|
666
|
-
try { await removeCredentials({ scope: 'oauth', preferenceUnavailable: true }); } catch (_) {}
|
|
555
|
+
// baseUrl, apiKey, and other persisted settings survive a token expiry.
|
|
556
|
+
try { await removeCredentials({ scope: 'oauth' }); } catch (_) {}
|
|
667
557
|
} catch (e) {
|
|
668
558
|
if (isMaintenanceError(e)) {
|
|
669
559
|
throw e;
|
|
@@ -675,7 +565,7 @@ async function _doRefresh() {
|
|
|
675
565
|
// purge the token. Surgical OAuth-only clear preserves baseUrl + apiKey + timeout overrides.
|
|
676
566
|
const status = e && typeof e.status === 'number' ? e.status : null;
|
|
677
567
|
if (status === 400 || status === 401 || status === 403) {
|
|
678
|
-
try { await removeCredentials({ scope: 'oauth'
|
|
568
|
+
try { await removeCredentials({ scope: 'oauth' }); } catch (_) {}
|
|
679
569
|
}
|
|
680
570
|
}
|
|
681
571
|
return null;
|
|
@@ -1024,7 +914,6 @@ module.exports = {
|
|
|
1024
914
|
refreshAccessToken,
|
|
1025
915
|
removeCredentials,
|
|
1026
916
|
saveConfig,
|
|
1027
|
-
synchronizeAnalyticsPreferenceBeforeLogout,
|
|
1028
917
|
CONFIG_DIR: DEFAULT_CONFIG_DIR,
|
|
1029
918
|
CONFIG_FILE: path.join(DEFAULT_CONFIG_DIR, 'config.json'),
|
|
1030
919
|
};
|
package/mcp/analytics.js
CHANGED
|
@@ -7,9 +7,7 @@ const {
|
|
|
7
7
|
ensureSecureUrl,
|
|
8
8
|
getRequestHeaders,
|
|
9
9
|
hasAuth,
|
|
10
|
-
loadConfig,
|
|
11
10
|
normalizeEndpoint,
|
|
12
|
-
saveConfig,
|
|
13
11
|
} = require('../lib/client');
|
|
14
12
|
|
|
15
13
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
@@ -23,7 +21,6 @@ const MAX_SANITIZE_STRING_LENGTH = 4_096;
|
|
|
23
21
|
const AUTHENTICATED_RELAY_PATH = '/api/jobscout/mcp-analytics';
|
|
24
22
|
const ANONYMOUS_RELAY_PATH = '/api/jobscout/mcp-analytics/anonymous';
|
|
25
23
|
const ANALYTICS_PREFERENCE_PATH = '/api/jobscout/analytics-preference';
|
|
26
|
-
const ANALYTICS_OPT_OUT_CONFIG_KEY = 'mcpAnalyticsOptOut';
|
|
27
24
|
const MCP_ANALYTICS_USER_AGENT = `trackly-mcp-analytics/${PACKAGE_VERSION}`;
|
|
28
25
|
const REDACTED = '[redacted]';
|
|
29
26
|
const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on', 'enabled']);
|
|
@@ -217,6 +214,9 @@ function truthy(value) {
|
|
|
217
214
|
return TRUE_VALUES.has(String(value || '').trim().toLowerCase());
|
|
218
215
|
}
|
|
219
216
|
|
|
217
|
+
// Internal test/CI isolation only (keeps the repo's own smoke and package
|
|
218
|
+
// tests from sending real analytics). Not a user setting: usage analytics are
|
|
219
|
+
// part of the free product and there is no user opt-out.
|
|
220
220
|
function isMcpAnalyticsEnabled(env = process.env) {
|
|
221
221
|
if (truthy(env.TRACKLY_MCP_ANALYTICS_DISABLED)) return false;
|
|
222
222
|
if (env.TRACKLY_MCP_ANALYTICS_ENABLED !== undefined
|
|
@@ -713,41 +713,13 @@ function runtimeEventProperties(env = process.env) {
|
|
|
713
713
|
|
|
714
714
|
function createBackendRelay(options = {}) {
|
|
715
715
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
716
|
-
const loadConfigImpl = options.loadConfig || loadConfig;
|
|
717
|
-
const saveConfigImpl = options.saveConfig || saveConfig;
|
|
718
716
|
const pending = new Set();
|
|
719
717
|
const maxPendingDeliveries = Number.isSafeInteger(options.maxPendingDeliveries)
|
|
720
718
|
&& options.maxPendingDeliveries > 0
|
|
721
719
|
? options.maxPendingDeliveries
|
|
722
720
|
: DEFAULT_MAX_PENDING_DELIVERIES;
|
|
723
|
-
let cachedOptOut = readCachedAnalyticsOptOut();
|
|
724
721
|
let preferenceLookup = null;
|
|
725
722
|
|
|
726
|
-
function readCachedAnalyticsOptOut() {
|
|
727
|
-
try {
|
|
728
|
-
return loadConfigImpl()[ANALYTICS_OPT_OUT_CONFIG_KEY] === true;
|
|
729
|
-
} catch {
|
|
730
|
-
return true;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function persistCachedAnalyticsOptOut(optedOut) {
|
|
735
|
-
cachedOptOut = optedOut;
|
|
736
|
-
try {
|
|
737
|
-
const config = loadConfigImpl();
|
|
738
|
-
if (optedOut && config[ANALYTICS_OPT_OUT_CONFIG_KEY] === true) return;
|
|
739
|
-
if (optedOut) config[ANALYTICS_OPT_OUT_CONFIG_KEY] = true;
|
|
740
|
-
else if (Object.hasOwn(config, ANALYTICS_OPT_OUT_CONFIG_KEY)) {
|
|
741
|
-
delete config[ANALYTICS_OPT_OUT_CONFIG_KEY];
|
|
742
|
-
} else {
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
saveConfigImpl(config);
|
|
746
|
-
} catch {
|
|
747
|
-
// The in-memory privacy gate remains authoritative for this process.
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
723
|
function authenticatedRequestContext() {
|
|
752
724
|
const headers = getRequestHeaders(false, MCP_ANALYTICS_USER_AGENT);
|
|
753
725
|
const authorization = headers.Authorization;
|
|
@@ -789,7 +761,6 @@ function createBackendRelay(options = {}) {
|
|
|
789
761
|
throw new Error('analytics_preference_invalid');
|
|
790
762
|
}
|
|
791
763
|
if (!isCurrentAuthenticatedContext(authContext.key)) return false;
|
|
792
|
-
persistCachedAnalyticsOptOut(!preference.shareUsageAnalytics);
|
|
793
764
|
return preference.shareUsageAnalytics;
|
|
794
765
|
})().catch(() => false).finally(() => {
|
|
795
766
|
if (preferenceLookup === lookup) preferenceLookup = null;
|
|
@@ -805,12 +776,6 @@ function createBackendRelay(options = {}) {
|
|
|
805
776
|
const authContext = authenticated ? authenticatedRequestContext() : null;
|
|
806
777
|
if (authenticated && !authContext) return;
|
|
807
778
|
if (!authenticated && !ANONYMOUS_RELAY_EVENTS.has(event.event)) return;
|
|
808
|
-
// Another MCP process can persist an opt-out before this process loses
|
|
809
|
-
// authentication. Refresh the anonymous gate from disk before every
|
|
810
|
-
// unauthenticated delivery, while preserving a known in-memory opt-out
|
|
811
|
-
// if its earlier persistence attempt failed.
|
|
812
|
-
if (!authenticated) cachedOptOut ||= readCachedAnalyticsOptOut();
|
|
813
|
-
if (!authenticated && cachedOptOut) return;
|
|
814
779
|
if (pending.size >= maxPendingDeliveries) return;
|
|
815
780
|
|
|
816
781
|
const endpoint = authenticated ? AUTHENTICATED_RELAY_PATH : ANONYMOUS_RELAY_PATH;
|
package/mcp/apply-tools.js
CHANGED
|
@@ -440,14 +440,15 @@ const applyExecutionSchema = z.object({
|
|
|
440
440
|
currentWave: executionWaveSchema.nullable(),
|
|
441
441
|
unresolvedWaves: z.array(executionWaveSchema).max(APPLY_EXECUTION_MAX_TARGET),
|
|
442
442
|
}).strict();
|
|
443
|
-
const
|
|
443
|
+
const getExecutionAccessReviewResponseObjectSchema = z.object({
|
|
444
444
|
success: z.literal(true),
|
|
445
445
|
execution: applyExecutionSchema,
|
|
446
446
|
progress: executionProgressSchema,
|
|
447
447
|
proposedWave: z.array(proposedWaveMemberSchema).max(APPLY_EXECUTION_MAX_TARGET),
|
|
448
448
|
accessProposal: accessProposalSchema,
|
|
449
|
-
}).strict()
|
|
450
|
-
const
|
|
449
|
+
}).strict();
|
|
450
|
+
const getExecutionAccessReviewResponseSchema = getExecutionAccessReviewResponseObjectSchema.superRefine(validateMatchingProposal);
|
|
451
|
+
const activeExecutionAccessReviewResponseSchema = getExecutionAccessReviewResponseObjectSchema.extend({
|
|
451
452
|
enabled: z.boolean().optional(),
|
|
452
453
|
active: z.boolean().optional(),
|
|
453
454
|
preserved: z.boolean().optional(),
|
package/mcp/server.js
CHANGED
|
@@ -448,7 +448,7 @@ function createServer() {
|
|
|
448
448
|
'trackly_update_experience_limits',
|
|
449
449
|
'Atomically replace the complete role-specific experience-limit map. Each value is the highest stated minimum years requirement the user wants for that job function. When Trackly enforcement is active, a job is included when its stated minimum is less than or equal to the limit. Jobs with no stated minimum remain visible. An empty object clears the saved limits. experienceFilterV2Available reports edit capability, not enforcement state. Read discoveryPreferenceRevision with trackly_get_preferences first. If the backend returns a revision conflict, refetch and reconcile with the user; never retry blindly with the newer revision.',
|
|
450
450
|
{
|
|
451
|
-
experienceLimitsByJobFunction: z.
|
|
451
|
+
experienceLimitsByJobFunction: z.partialRecord(z.enum(JOB_FUNCTIONS), z.number().int().min(0).max(60)).describe(
|
|
452
452
|
'Complete replacement map from canonical job-function slug to maximum acceptable stated minimum years. Use {} to turn role-specific experience filtering off.'
|
|
453
453
|
),
|
|
454
454
|
expectedPreferenceRevision: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).describe(
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackly-cli",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "trackly-cli",
|
|
9
|
-
"version": "0.22.
|
|
9
|
+
"version": "0.22.3",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
13
13
|
"@posthog/mcp": "0.11.0",
|
|
14
|
-
"fast-uri": "3.1.
|
|
15
|
-
"hono": "4.13.
|
|
16
|
-
"ip-address": "10.7.
|
|
14
|
+
"fast-uri": "3.1.9",
|
|
15
|
+
"hono": "4.13.11",
|
|
16
|
+
"ip-address": "10.7.4",
|
|
17
17
|
"posthog-node": "5.48.1",
|
|
18
|
-
"zod": "
|
|
18
|
+
"zod": "4.6.7"
|
|
19
19
|
},
|
|
20
20
|
"bin": {
|
|
21
21
|
"trackly": "bin/trackly"
|
|
@@ -812,18 +812,18 @@
|
|
|
812
812
|
}
|
|
813
813
|
},
|
|
814
814
|
"node_modules/body-parser": {
|
|
815
|
-
"version": "2.
|
|
816
|
-
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.
|
|
817
|
-
"integrity": "sha512-
|
|
815
|
+
"version": "2.4.0",
|
|
816
|
+
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz",
|
|
817
|
+
"integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==",
|
|
818
818
|
"license": "MIT",
|
|
819
819
|
"dependencies": {
|
|
820
820
|
"bytes": "^3.1.2",
|
|
821
|
-
"content-type": "^2.
|
|
821
|
+
"content-type": "^2.1.0",
|
|
822
822
|
"debug": "^4.4.3",
|
|
823
823
|
"http-errors": "^2.0.1",
|
|
824
|
-
"iconv-lite": "^0.7.
|
|
824
|
+
"iconv-lite": "^0.7.3",
|
|
825
825
|
"on-finished": "^2.4.1",
|
|
826
|
-
"qs": "^6.
|
|
826
|
+
"qs": "^6.16.1",
|
|
827
827
|
"raw-body": "^3.0.2",
|
|
828
828
|
"type-is": "^2.1.0"
|
|
829
829
|
},
|
|
@@ -836,9 +836,9 @@
|
|
|
836
836
|
}
|
|
837
837
|
},
|
|
838
838
|
"node_modules/body-parser/node_modules/content-type": {
|
|
839
|
-
"version": "2.
|
|
840
|
-
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.
|
|
841
|
-
"integrity": "sha512-
|
|
839
|
+
"version": "2.1.0",
|
|
840
|
+
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
|
841
|
+
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
|
842
842
|
"license": "MIT",
|
|
843
843
|
"engines": {
|
|
844
844
|
"node": ">=18"
|
|
@@ -1156,9 +1156,9 @@
|
|
|
1156
1156
|
"license": "MIT"
|
|
1157
1157
|
},
|
|
1158
1158
|
"node_modules/fast-uri": {
|
|
1159
|
-
"version": "3.1.
|
|
1160
|
-
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.
|
|
1161
|
-
"integrity": "sha512-
|
|
1159
|
+
"version": "3.1.9",
|
|
1160
|
+
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
|
|
1161
|
+
"integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
|
|
1162
1162
|
"funding": [
|
|
1163
1163
|
{
|
|
1164
1164
|
"type": "github",
|
|
@@ -1293,9 +1293,9 @@
|
|
|
1293
1293
|
}
|
|
1294
1294
|
},
|
|
1295
1295
|
"node_modules/hono": {
|
|
1296
|
-
"version": "4.13.
|
|
1297
|
-
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.
|
|
1298
|
-
"integrity": "sha512
|
|
1296
|
+
"version": "4.13.11",
|
|
1297
|
+
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.11.tgz",
|
|
1298
|
+
"integrity": "sha512-/SMX/RQNJn7oNmFwH6DtwDqcrzZU2otm1FD6OJe2cWF6cfy/F8hq0XVBv7xW3FTJshRQwuBnXHDq2kNsdZnshg==",
|
|
1299
1299
|
"license": "MIT",
|
|
1300
1300
|
"engines": {
|
|
1301
1301
|
"node": ">=16.9.0"
|
|
@@ -1322,9 +1322,9 @@
|
|
|
1322
1322
|
}
|
|
1323
1323
|
},
|
|
1324
1324
|
"node_modules/iconv-lite": {
|
|
1325
|
-
"version": "0.7.
|
|
1326
|
-
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.
|
|
1327
|
-
"integrity": "sha512-
|
|
1325
|
+
"version": "0.7.3",
|
|
1326
|
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
|
1327
|
+
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
|
1328
1328
|
"license": "MIT",
|
|
1329
1329
|
"dependencies": {
|
|
1330
1330
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
|
@@ -1344,9 +1344,9 @@
|
|
|
1344
1344
|
"license": "ISC"
|
|
1345
1345
|
},
|
|
1346
1346
|
"node_modules/ip-address": {
|
|
1347
|
-
"version": "10.7.
|
|
1348
|
-
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.
|
|
1349
|
-
"integrity": "sha512-
|
|
1347
|
+
"version": "10.7.4",
|
|
1348
|
+
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz",
|
|
1349
|
+
"integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==",
|
|
1350
1350
|
"license": "MIT",
|
|
1351
1351
|
"engines": {
|
|
1352
1352
|
"node": ">= 12"
|
|
@@ -1598,9 +1598,9 @@
|
|
|
1598
1598
|
}
|
|
1599
1599
|
},
|
|
1600
1600
|
"node_modules/qs": {
|
|
1601
|
-
"version": "6.16.
|
|
1602
|
-
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.
|
|
1603
|
-
"integrity": "sha512-
|
|
1601
|
+
"version": "6.16.1",
|
|
1602
|
+
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz",
|
|
1603
|
+
"integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==",
|
|
1604
1604
|
"license": "BSD-3-Clause",
|
|
1605
1605
|
"dependencies": {
|
|
1606
1606
|
"es-define-property": "^1.0.1",
|
|
@@ -2008,9 +2008,9 @@
|
|
|
2008
2008
|
}
|
|
2009
2009
|
},
|
|
2010
2010
|
"node_modules/zod": {
|
|
2011
|
-
"version": "
|
|
2012
|
-
"resolved": "https://registry.npmjs.org/zod/-/zod-
|
|
2013
|
-
"integrity": "sha512-
|
|
2011
|
+
"version": "4.6.7",
|
|
2012
|
+
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz",
|
|
2013
|
+
"integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==",
|
|
2014
2014
|
"license": "MIT",
|
|
2015
2015
|
"funding": {
|
|
2016
2016
|
"url": "https://github.com/sponsors/colinhacks"
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackly-cli",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.3",
|
|
4
4
|
"mcpName": "io.github.trackly-app/trackly",
|
|
5
|
-
"description": "Search
|
|
5
|
+
"description": "Search Thousands of jobs across Thousands of 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
|
},
|
|
@@ -72,20 +72,21 @@
|
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
74
74
|
"@posthog/mcp": "0.11.0",
|
|
75
|
-
"fast-uri": "3.1.
|
|
76
|
-
"hono": "4.13.
|
|
77
|
-
"ip-address": "10.7.
|
|
75
|
+
"fast-uri": "3.1.9",
|
|
76
|
+
"hono": "4.13.11",
|
|
77
|
+
"ip-address": "10.7.4",
|
|
78
78
|
"posthog-node": "5.48.1",
|
|
79
|
-
"zod": "
|
|
79
|
+
"zod": "4.6.7"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
82
|
"node": "^20.20.0 || >=22.22.0"
|
|
83
83
|
},
|
|
84
84
|
"overrides": {
|
|
85
85
|
"@hono/node-server": "2.0.12",
|
|
86
|
-
"fast-uri": "3.1.
|
|
87
|
-
"ip-address": "10.7.
|
|
88
|
-
"qs": "6.16.
|
|
86
|
+
"fast-uri": "3.1.9",
|
|
87
|
+
"ip-address": "10.7.4",
|
|
88
|
+
"qs": "6.16.1",
|
|
89
|
+
"body-parser": "2.4.0"
|
|
89
90
|
},
|
|
90
91
|
"devDependencies": {
|
|
91
92
|
"@babel/parser": "7.29.8",
|
|
@@ -12,7 +12,6 @@ const DEFAULT_POLICY_PATH = path.join(
|
|
|
12
12
|
'security',
|
|
13
13
|
'audit-exceptions.json',
|
|
14
14
|
);
|
|
15
|
-
const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
16
15
|
|
|
17
16
|
function selectAdvisoryFields(advisory) {
|
|
18
17
|
return {
|
|
@@ -112,10 +111,19 @@ function loadPolicy(policyPath = DEFAULT_POLICY_PATH) {
|
|
|
112
111
|
}
|
|
113
112
|
|
|
114
113
|
function runAudit() {
|
|
115
|
-
|
|
114
|
+
// npm.cmd is not directly executable on Windows. During npm run, invoke
|
|
115
|
+
// its CLI with Node so paths containing spaces or shell characters stay literal.
|
|
116
|
+
const npmCli = process.env.npm_execpath;
|
|
117
|
+
const command = npmCli ? process.execPath
|
|
118
|
+
: process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : 'npm';
|
|
119
|
+
const args = npmCli ? [npmCli, 'audit', '--json', '--audit-level=moderate']
|
|
120
|
+
: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm.cmd audit --json --audit-level=moderate']
|
|
121
|
+
: ['audit', '--json', '--audit-level=moderate'];
|
|
122
|
+
const result = spawnSync(command, args, {
|
|
116
123
|
cwd: path.join(__dirname, '..'),
|
|
117
124
|
encoding: 'utf8',
|
|
118
125
|
maxBuffer: 10 * 1024 * 1024,
|
|
126
|
+
windowsHide: true,
|
|
119
127
|
});
|
|
120
128
|
|
|
121
129
|
if (result.error) {
|
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": "Search
|
|
5
|
+
"description": "Search Thousands of jobs, Thousands of 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.22.
|
|
20
|
+
"version": "0.22.3",
|
|
21
21
|
"packages": [
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "trackly-cli",
|
|
25
|
-
"version": "0.22.
|
|
25
|
+
"version": "0.22.3",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"runtimeArguments": [
|
|
28
28
|
{
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: trackly-apply
|
|
3
|
-
description:
|
|
3
|
+
description: Help users finish Trackly Apply setup and resolve missing profile or resume requirements, then fill user-approved jobs through a supported ATS in a real browser and stop for manual review and submission. Use for requests such as “set up Apply,” “fix my incomplete profile,” “apply to my queue,” or “fill the next application” in Codex or Claude Code on macOS.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Trackly Apply
|
|
7
7
|
|
|
8
8
|
Use Trackly as the source of truth for profile answers, documents, queue decisions, and application state. Use this skill only for reusable browser mechanics; never store personal answers or application logs inside the skill.
|
|
9
9
|
|
|
10
|
+
Before application work, read [onboarding guidance](references/onboarding.md). Guide the user through only the missing requirements and verify saved answers, the stored default resume, and explicit profile confirmation.
|
|
11
|
+
|
|
10
12
|
## Skill version
|
|
11
13
|
|
|
12
|
-
Skill version: `4.9.
|
|
14
|
+
Skill version: `4.9.2`. Before browser readiness read [references/handoff-compatibility.md](references/handoff-compatibility.md). This policy takes precedence over returned-persistence-receipt or finalizer requirements elsewhere when using the documented markHandoff path. Other review and safety gates remain unchanged.
|
|
13
15
|
|
|
14
16
|
## Non-negotiable rules
|
|
15
17
|
|
|
@@ -39,7 +41,7 @@ value-free phase receipt with `scripts/validate-phase-checkpoint.js` before
|
|
|
39
41
|
claiming that phase complete.
|
|
40
42
|
Read [references/legal-clarifications.md](references/legal-clarifications.md) when a form asks for a legal, regulatory, consent, or employer-relationship decision.
|
|
41
43
|
|
|
42
|
-
1. Call `trackly_get_apply_protocol`. Skill 4.9.
|
|
44
|
+
1. Call `trackly_get_apply_protocol`. Skill 4.9.2 requires protocol 3.7.0 or newer for new reliability work. Protocol 3.2 remains valid only for an already-active explicit legacy single run; an already-active explicit 3.2 single run may finish through its legacy path. Preserve the protocol's documented recovery paths for already-active older work. Require `compatibleSkillMajor: 4` and `compatibleSkillMinimumVersion` no newer than this installed skill. Reject an older or incompatible version for new work and report that the backend must finish updating or `trackly agent setup` must update the skill. Protocol 3.2 added exact-origin trust for jobs Trackly ingested directly from employer careers sources; do not recreate the retired ownership-timestamp gate in the client.
|
|
43
45
|
2. Call `trackly_get_profile_onboarding` or fetch both the profile schema and application profile. When present, render `schema.screens` in ascending `order` as one grouped question packet per screen. Within a screen, preserve category `order` and then field `order`; use each field's user-facing `rationale` when the user asks why it is needed. Ask only unknown or confirmation-needed fields and honor `consistencyRules` before submitting profile changes, resolving contradictions with the user instead of guessing. When `schema.screens` is absent on a legacy backend, fall back to the existing category-based onboarding behavior. Treat `completeness.percent` as required onboarding readiness only. Use `coverage.missingReusableKeys` to explain reusable optional gaps, while `coverage.contextualKeys` are intentionally asked only on the relevant employer form. Do not claim that 100% required completeness answers every possible application question.
|
|
44
46
|
3. Save answers with `trackly_update_application_profile`:
|
|
45
47
|
- Use `answered`, `intentionally_blank`, `declined`, or `unknown` exactly.
|
|
@@ -85,7 +87,7 @@ Read [references/legal-clarifications.md](references/legal-clarifications.md) wh
|
|
|
85
87
|
- For each fixed batch member, preserve an explicit job ID -> application run ID -> browser tab mapping. Complete the full start -> conditional resume preparation/confirmation/verification when an upload control exists -> form completion -> `review_ready` lifecycle for every member. A review-ready run does not block the next member. Never submit any of them.
|
|
86
88
|
6. When the selected job or current fixed batch member has no `runId`, call `trackly_start_apply_run` with the complete `batchId`, `memberId`, `expectedMemberVersion`, `expectedInspectionEpoch`, and `leaseToken` returned by Trackly. This is the sanctioned idempotent lookup for a recovered member whose `runId` is absent; call it with the exact recovered binding and Trackly will reuse the bound active run. When the member already has a `runId`, do not call `trackly_start_apply_run` in this or any subsequent start step: reuse that exact run and bind or recover its browser surface through `trackly_bind_apply_surface` as required. Never create a replacement run because browser control was interrupted.
|
|
87
89
|
- If a bound start returns a transport failure, a non-access HTTP 5xx response, or an error explicitly marked `retryable: true`, preserve the frozen member and browser state. Refetch the active batch, renew its lease, and retry the same complete binding once. Classify the retry response independently with these same rules: route canonical `maintenance_mode` or legacy `planned_maintenance` directly through **Resume after maintenance**; surface controlled-access/request errors marked `retryable: false` and every other HTTP 4xx response unchanged; only a second transport failure, non-access HTTP 5xx response, or explicitly retryable error becomes `backend_run_start_unavailable`. Route maintenance on either attempt without consuming or relabeling the retry, and never relabel a permanent retry response as an outage. For `backend_run_start_unavailable`, continue other members and report a Trackly control-plane failure. Do not call `trackly_checkpoint_apply_batch` for this condition because start failure has not produced the required run ID. The unchanged frozen member is the durable resume point. Never switch that frozen member to an unbound legacy run or blame the employer form.
|
|
88
|
-
- Require `run.protocolVersion` to be 3.1.0 or newer and to share protocol major 3 with the fetched protocol. A new reliability execution member requires 3.7.0 or newer; already-active older work may use only the recovery path published by its fetched protocol. Never continue a pre-evidence 3.0.x run under skill 4.9.
|
|
90
|
+
- Require `run.protocolVersion` to be 3.1.0 or newer and to share protocol major 3 with the fetched protocol. A new reliability execution member requires 3.7.0 or newer; already-active older work may use only the recovery path published by its fetched protocol. Never continue a pre-evidence 3.0.x run under skill 4.9.2. Preserve that run instead of starting a replacement, record it `blocked` with a value-free protocol-upgrade reason when possible, and tell the user the saved job can be retried only after the stale run is cleared through Trackly's supported lifecycle. Stop and refetch the protocol and active execution or batch if support level, execution mode, provider, required scenarios, authorized origin policy, member version, or inspection epoch changes after run creation.
|
|
89
91
|
- In guided mode, inspect the page before preparing any resume bytes. Confirm the employer, role, HTTPS origin, reachable review path, semantic controls, whether an attachment control exists, and absence of a credential, verification, CAPTCHA, or submit-only wall. A missing file input is not itself a blocker; skip the resume path when the application has no attachment control. Any other failed precondition is an execution blocker, not permission to improvise.
|
|
90
92
|
7. Pass the browser readiness gate before preparing resume bytes:
|
|
91
93
|
- Use semantic browser control through Codex in-app browser controls, Chrome MCP/extension browser control, or Claude in Chrome.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Finish Apply setup
|
|
2
|
+
|
|
3
|
+
Treat setup as a guided conversation. Read the current profile and schema through trackly_get_profile_onboarding or equivalent profile reads. Derive next steps from authoritative completeness and saved states. Explain Apply capability separately from profile completion. Incomplete setup is not a rollout failure, and absent answers or an absent upload do not resolve by waiting.
|
|
4
|
+
|
|
5
|
+
1. Check availability before interpreting saved state. If profile or schema is unavailable, report the failed check and a concrete next action without asking the user to repeat saved setup.
|
|
6
|
+
2. Explain remaining requirements in the user's language. Ask only canonical questions that remain unknown or need confirmation. Preserve answered, declined and intentionally blank fields. Save only user-confirmed facts and scopes using the current revision, then refetch the profile. Education updates replace the entire saved list. Never send a partial education list. Guide missing education through https://usetrackly.app/profile with the same account to edit only missing fields and preserve existing records, then refetch the profile after they save. Never invent missing facts or claim an unsaved conversational answer was stored.
|
|
7
|
+
3. Treat documents.default_resume as a file requirement. If no stored default resume is available, give https://usetrackly.app/profile and explain how to sign in with the same Trackly account, upload the original resume and select Set Default if needed. A PDF attached to a conversation or permission to use it does not store the file in Trackly. Do not invent an incoming chat-file import operation. After the user confirms the upload, refetch the profile and verify it returns a stored default resume. Employer attachment is a later step.
|
|
8
|
+
4. When requirements are complete but the profile is unconfirmed, ask the user to review and explicitly confirm it. Save confirmProfile: true with the current expectedRevision through trackly_update_application_profile only after confirmation. Refetch the profile and verify confirmedAt is present, a stored default resume exists and no missing requirements remain before continuing.
|
|
9
|
+
5. Name any failed operation and the next concrete action. Avoid repeated polling. Capability disablement, incomplete setup, unavailable checks and a host without a controlled browser are distinct blockers. Profile readiness does not prove browser capability. Never submit an application or infer submission from setup completion.
|