trackly-cli 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/bin/trackly +224 -21
- package/contracts/trackly-apply-tools.json +26 -7
- package/docs/trackly-tools.md +26 -2
- package/lib/agent.js +197 -11
- package/lib/client.js +86 -2
- package/mcp/apply-tools.js +666 -0
- package/mcp/server.js +124 -210
- package/npm-shrinkwrap.json +1177 -0
- package/package.json +7 -2
- package/scripts/verify-audit-exceptions.js +190 -0
- package/security/audit-exceptions.json +70 -0
- package/server.json +2 -2
- package/skills/trackly-apply/SKILL.md +23 -16
- package/skills/trackly-apply/agents/openai.yaml +1 -1
- package/skills/trackly-apply/references/batch-orchestration.md +187 -0
- package/skills/trackly-apply/references/browser-lifecycle.md +93 -0
- package/skills/trackly-apply/references/review-handoff.md +53 -2
package/mcp/server.js
CHANGED
|
@@ -4,17 +4,17 @@ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
|
4
4
|
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
5
5
|
const { McpError } = require('@modelcontextprotocol/sdk/types.js');
|
|
6
6
|
const { z } = require('zod');
|
|
7
|
-
const { apiRequest, hasAuth, maintenanceOutput } = require('../lib/client');
|
|
8
|
-
const { prepareResume, verifyPreparedResume } = require('../lib/agent');
|
|
7
|
+
const { apiRequest, createTracklyAccessError, hasAuth, maintenanceOutput } = require('../lib/client');
|
|
9
8
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
10
|
-
const
|
|
9
|
+
const { registerApplyTools } = require('./apply-tools');
|
|
11
10
|
|
|
12
11
|
const MCP_USER_AGENT = `trackly-mcp/${PACKAGE_VERSION}`;
|
|
13
12
|
const MCP_MAINTENANCE_ERROR_CODE = -32002;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
13
|
+
const MCP_ACCESS_ERROR_CODE = -32003;
|
|
14
|
+
const MCP_AUTH_ERROR_CODE = -32004;
|
|
15
|
+
const AUTH_HINT =
|
|
16
|
+
'Existing members: run `trackly login` or set TRACKLY_API_KEY. ' +
|
|
17
|
+
'New members need a private invite during the limited rollout; request access at https://usetrackly.app/early-access.';
|
|
18
18
|
|
|
19
19
|
// Mirrors `granola-followup-app/src/services/region-classifier.ts:8` REGION_TAGS.
|
|
20
20
|
// Keep in sync when the backend enum changes.
|
|
@@ -60,21 +60,44 @@ const ACTION_TO_STAGE = { applied: 'applied', saved: 'backlog', dismissed: 'disc
|
|
|
60
60
|
|
|
61
61
|
function createErrorResult(error, fallbackMessage, extra = {}) {
|
|
62
62
|
const normalizedMaintenance = maintenanceOutput(error);
|
|
63
|
+
const normalizedAccess = createTracklyAccessError(error, error?.status);
|
|
63
64
|
const payload = normalizedMaintenance
|
|
64
65
|
? {
|
|
65
66
|
...normalizedMaintenance,
|
|
66
67
|
error: error?.error || error?.message || fallbackMessage,
|
|
67
68
|
...extra,
|
|
68
69
|
}
|
|
69
|
-
:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
: normalizedAccess
|
|
71
|
+
? {
|
|
72
|
+
error: normalizedAccess.error,
|
|
73
|
+
message: normalizedAccess.message,
|
|
74
|
+
status: normalizedAccess.status,
|
|
75
|
+
code: normalizedAccess.code,
|
|
76
|
+
retryable: normalizedAccess.retryable,
|
|
77
|
+
...extra,
|
|
78
|
+
}
|
|
79
|
+
: {
|
|
80
|
+
error: error?.error || error?.message || fallbackMessage,
|
|
81
|
+
...extra,
|
|
82
|
+
};
|
|
73
83
|
|
|
74
84
|
if (error?.status) {
|
|
75
85
|
payload.status = error.status;
|
|
76
86
|
}
|
|
77
87
|
|
|
88
|
+
if (
|
|
89
|
+
error?.code === 'experience_filter_v2_unavailable'
|
|
90
|
+
|| error?.code === 'invalid_preference_revision'
|
|
91
|
+
) {
|
|
92
|
+
payload.code = error.code;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (error?.status === 409 && error?.error === 'preference_revision_conflict') {
|
|
96
|
+
payload.preferences = error.preferences;
|
|
97
|
+
payload.discoveryPreferenceRevision = error.discoveryPreferenceRevision;
|
|
98
|
+
payload.hint = 'Preferences changed elsewhere. Refetch with trackly_get_preferences, reconcile with the user, and never retry blindly with the newer revision.';
|
|
99
|
+
}
|
|
100
|
+
|
|
78
101
|
if (error?.status === 429 && !payload.hint) {
|
|
79
102
|
payload.hint = 'Daily limit reached (20 natural language queries per day).';
|
|
80
103
|
}
|
|
@@ -102,6 +125,25 @@ function throwMcpResourceError(error) {
|
|
|
102
125
|
normalizedMaintenance,
|
|
103
126
|
);
|
|
104
127
|
}
|
|
128
|
+
const normalizedAccess = createTracklyAccessError(error, error?.status);
|
|
129
|
+
if (normalizedAccess) {
|
|
130
|
+
throw new McpError(
|
|
131
|
+
MCP_ACCESS_ERROR_CODE,
|
|
132
|
+
normalizedAccess.message,
|
|
133
|
+
{
|
|
134
|
+
status: normalizedAccess.status,
|
|
135
|
+
code: normalizedAccess.code,
|
|
136
|
+
retryable: normalizedAccess.retryable,
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (error?.status === 401) {
|
|
141
|
+
throw new McpError(
|
|
142
|
+
MCP_AUTH_ERROR_CODE,
|
|
143
|
+
error?.message || 'Not authenticated',
|
|
144
|
+
{ status: 401, hint: AUTH_HINT },
|
|
145
|
+
);
|
|
146
|
+
}
|
|
105
147
|
throw error;
|
|
106
148
|
}
|
|
107
149
|
|
|
@@ -126,6 +168,17 @@ function wrapTool(handler, fallbackMessage) {
|
|
|
126
168
|
};
|
|
127
169
|
}
|
|
128
170
|
|
|
171
|
+
function projectPreferenceResponse(result, experienceFilterV2Available = null) {
|
|
172
|
+
const user = result?.user || {};
|
|
173
|
+
return {
|
|
174
|
+
success: result?.success === true,
|
|
175
|
+
experienceFilterV2Available: experienceFilterV2Available === null
|
|
176
|
+
? user.experienceFilterV2Available === true
|
|
177
|
+
: experienceFilterV2Available === true,
|
|
178
|
+
preferences: user.preferences || result?.preferences || {},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
129
182
|
function createServer() {
|
|
130
183
|
const server = new McpServer({
|
|
131
184
|
name: 'trackly',
|
|
@@ -239,6 +292,62 @@ function createServer() {
|
|
|
239
292
|
}, 'Failed to fetch stats')
|
|
240
293
|
);
|
|
241
294
|
|
|
295
|
+
server.tool(
|
|
296
|
+
'trackly_get_preferences',
|
|
297
|
+
'Read the authenticated user\'s discovery preferences, including selected and effective job functions, role-specific experience limits and active state, and the discoveryPreferenceRevision required for a safe update.',
|
|
298
|
+
{},
|
|
299
|
+
wrapTool(async () => {
|
|
300
|
+
const result = await apiRequest('GET', '/api/jobscout/me', null, false, false, MCP_USER_AGENT);
|
|
301
|
+
return projectPreferenceResponse(result);
|
|
302
|
+
}, 'Failed to fetch preferences')
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
server.tool(
|
|
306
|
+
'trackly_update_experience_limits',
|
|
307
|
+
'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.',
|
|
308
|
+
{
|
|
309
|
+
experienceLimitsByJobFunction: z.record(z.enum(JOB_FUNCTIONS), z.number().int().min(0).max(60)).describe(
|
|
310
|
+
'Complete replacement map from canonical job-function slug to maximum acceptable stated minimum years. Use {} to turn role-specific experience filtering off.'
|
|
311
|
+
),
|
|
312
|
+
expectedPreferenceRevision: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).describe(
|
|
313
|
+
'The non-negative safe-integer discoveryPreferenceRevision returned by the latest trackly_get_preferences call. A stale revision is rejected instead of overwriting newer choices.'
|
|
314
|
+
),
|
|
315
|
+
},
|
|
316
|
+
wrapTool(async ({ experienceLimitsByJobFunction, expectedPreferenceRevision }) => {
|
|
317
|
+
const current = await apiRequest('GET', '/api/jobscout/me', null, false, false, MCP_USER_AGENT);
|
|
318
|
+
if (current?.user?.experienceFilterV2Available !== true) {
|
|
319
|
+
throw {
|
|
320
|
+
status: 403,
|
|
321
|
+
code: 'experience_filter_v2_unavailable',
|
|
322
|
+
error: 'Role-specific experience preferences are not available for this account yet. No changes were saved.',
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const currentPreferences = current?.user?.preferences || {};
|
|
326
|
+
const currentRevision = currentPreferences.discoveryPreferenceRevision;
|
|
327
|
+
if (!Number.isSafeInteger(currentRevision) || currentRevision < 0) {
|
|
328
|
+
throw {
|
|
329
|
+
status: 502,
|
|
330
|
+
code: 'invalid_preference_revision',
|
|
331
|
+
error: 'Trackly did not return a valid preference revision. No changes were saved.',
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (currentRevision !== expectedPreferenceRevision) {
|
|
335
|
+
throw {
|
|
336
|
+
status: 409,
|
|
337
|
+
error: 'preference_revision_conflict',
|
|
338
|
+
preferences: currentPreferences,
|
|
339
|
+
discoveryPreferenceRevision: currentRevision,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const result = await apiRequest('PUT', '/api/jobscout/preferences', {
|
|
343
|
+
experienceLimitsByJobFunction,
|
|
344
|
+
experienceFilterVersion: 2,
|
|
345
|
+
expectedPreferenceRevision,
|
|
346
|
+
}, false, false, MCP_USER_AGENT);
|
|
347
|
+
return projectPreferenceResponse(result, true);
|
|
348
|
+
}, 'Failed to update experience limits')
|
|
349
|
+
);
|
|
350
|
+
|
|
242
351
|
server.tool(
|
|
243
352
|
'trackly_update_status',
|
|
244
353
|
'Update a job application status (apply, save, or dismiss).',
|
|
@@ -350,206 +459,11 @@ function createServer() {
|
|
|
350
459
|
}, 'Failed to request company')
|
|
351
460
|
);
|
|
352
461
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
{ limit: z.number().int().min(1).max(100).optional() },
|
|
358
|
-
wrapTool(async ({ limit }) => {
|
|
359
|
-
const qs = limit ? `?limit=${limit}` : '';
|
|
360
|
-
return apiRequest('GET', `/api/jobscout/apply/queue${qs}`, null, false, false, MCP_USER_AGENT);
|
|
361
|
-
}, 'Failed to fetch apply queue')
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
server.tool(
|
|
365
|
-
'trackly_get_application_profile',
|
|
366
|
-
'Get the versioned application profile. Sensitive values are returned only after the user opted into encrypted storage.',
|
|
367
|
-
{
|
|
368
|
-
includeSensitive: z.boolean().optional(),
|
|
369
|
-
provider: z.string().max(100).optional(),
|
|
370
|
-
companyId: z.string().max(100).optional(),
|
|
371
|
-
},
|
|
372
|
-
wrapTool(async ({ includeSensitive, provider, companyId }) => {
|
|
373
|
-
const qs = new URLSearchParams();
|
|
374
|
-
if (includeSensitive) qs.set('includeSensitive', 'true');
|
|
375
|
-
if (provider) qs.set('provider', provider);
|
|
376
|
-
if (companyId) qs.set('companyId', companyId);
|
|
377
|
-
return apiRequest('GET', `/api/jobscout/application-profile?${qs.toString()}`, null, false, false, MCP_USER_AGENT);
|
|
378
|
-
}, 'Failed to fetch application profile')
|
|
379
|
-
);
|
|
380
|
-
|
|
381
|
-
server.tool(
|
|
382
|
-
'trackly_get_profile_onboarding',
|
|
383
|
-
'Get the backend-owned profile schema and onboarding questions. Ask only fields whose state is unknown or needs confirmation.',
|
|
384
|
-
{},
|
|
385
|
-
wrapTool(async () => {
|
|
386
|
-
const [schema, profile] = await Promise.all([
|
|
387
|
-
apiRequest('GET', '/api/jobscout/application-profile/schema', null, false, false, MCP_USER_AGENT),
|
|
388
|
-
apiRequest('GET', '/api/jobscout/application-profile', null, false, false, MCP_USER_AGENT),
|
|
389
|
-
]);
|
|
390
|
-
return { schema, profile };
|
|
391
|
-
}, 'Failed to fetch profile onboarding')
|
|
392
|
-
);
|
|
393
|
-
|
|
394
|
-
server.tool(
|
|
395
|
-
'trackly_update_application_profile',
|
|
396
|
-
'Update confirmed profile answers with optimistic concurrency. Use global scope only for an explicit always-answer preference.',
|
|
397
|
-
{
|
|
398
|
-
expectedRevision: z.number().int().min(1),
|
|
399
|
-
source: z.enum(['web', 'ios', 'macos', 'codex', 'claude', 'mcp']).optional(),
|
|
400
|
-
changes: z.array(z.discriminatedUnion('scope', [
|
|
401
|
-
z.object({
|
|
402
|
-
key: z.string().min(1).max(200), state: z.enum(['unknown', 'answered', 'intentionally_blank', 'declined']),
|
|
403
|
-
value: z.any().optional(), scope: z.literal('global'), questionLabel: z.string().max(1000).optional(),
|
|
404
|
-
}),
|
|
405
|
-
z.object({
|
|
406
|
-
key: z.string().min(1).max(200), state: z.enum(['unknown', 'answered', 'intentionally_blank', 'declined']),
|
|
407
|
-
value: z.any().optional(), scope: z.literal('provider'), scopeValue: z.string().min(1).max(200),
|
|
408
|
-
questionLabel: z.string().max(1000).optional(),
|
|
409
|
-
}),
|
|
410
|
-
z.object({
|
|
411
|
-
key: z.string().min(1).max(200), state: z.enum(['unknown', 'answered', 'intentionally_blank', 'declined']),
|
|
412
|
-
value: z.any().optional(), scope: z.literal('company'), scopeValue: z.string().min(1).max(200),
|
|
413
|
-
questionLabel: z.string().max(1000).optional(),
|
|
414
|
-
}),
|
|
415
|
-
])).max(100).optional(),
|
|
416
|
-
education: z.array(z.object({
|
|
417
|
-
school: z.string().min(1).max(500),
|
|
418
|
-
degree: z.string().max(500).nullable().optional(),
|
|
419
|
-
fieldOfStudy: z.string().max(500).nullable().optional(),
|
|
420
|
-
gpa: z.string().max(50).nullable().optional(),
|
|
421
|
-
startDate: z.string().max(50).nullable().optional(),
|
|
422
|
-
endDate: z.string().max(50).nullable().optional(),
|
|
423
|
-
})).max(20).optional(),
|
|
424
|
-
confirmProfile: z.boolean().optional(),
|
|
425
|
-
sensitiveStorageConsent: z.boolean().optional(),
|
|
426
|
-
},
|
|
427
|
-
wrapTool(async (params) => apiRequest('PATCH', '/api/jobscout/application-profile', params, false, false, MCP_USER_AGENT), 'Failed to update application profile')
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
server.tool(
|
|
431
|
-
'trackly_start_apply_run',
|
|
432
|
-
'Start a manual-submit browser run for a job already in the approved queue. If maintenance interrupts an existing run, do not call this tool again: wait, refetch protocol/profile state, and resume that same run.',
|
|
433
|
-
{ jobId: z.number().int().min(1), clientName: z.string().max(100).optional() },
|
|
434
|
-
wrapTool(async (params) => apiRequest('POST', '/api/jobscout/apply/runs', params, false, false, MCP_USER_AGENT), 'Failed to start apply run')
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
server.tool(
|
|
438
|
-
'trackly_get_apply_evidence',
|
|
439
|
-
'Get the authenticated user\'s aggregate, value-free Apply beta evidence and release gate. The report never returns answers, contact values, addresses, or page text.',
|
|
440
|
-
{
|
|
441
|
-
windowDays: z.number().int().min(1).max(365).optional(),
|
|
442
|
-
targetReviewedRuns: z.number().int().min(1).max(1000).optional(),
|
|
443
|
-
},
|
|
444
|
-
wrapTool(async ({ windowDays, targetReviewedRuns }) => {
|
|
445
|
-
const qs = new URLSearchParams();
|
|
446
|
-
if (windowDays !== undefined) qs.set('windowDays', String(windowDays));
|
|
447
|
-
if (targetReviewedRuns !== undefined) qs.set('targetReviewedRuns', String(targetReviewedRuns));
|
|
448
|
-
const query = qs.toString();
|
|
449
|
-
const suffix = query ? `?${query}` : '';
|
|
450
|
-
return apiRequest('GET', `/api/jobscout/apply/evidence${suffix}`, null, false, false, MCP_USER_AGENT);
|
|
451
|
-
}, 'Failed to fetch apply evidence')
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
server.tool(
|
|
455
|
-
'trackly_get_apply_protocol',
|
|
456
|
-
'Get the current browser workflow, ATS support matrix, integrity rules, and compatible public-skill major version. Fetch at the start of every run and again after maintenance before resuming the existing run.',
|
|
457
|
-
{},
|
|
458
|
-
wrapTool(async () => apiRequest('GET', '/api/jobscout/apply/protocol', null, false, false, MCP_USER_AGENT), 'Failed to fetch apply protocol')
|
|
459
|
-
);
|
|
460
|
-
|
|
461
|
-
server.tool(
|
|
462
|
-
'trackly_report_apply_observation',
|
|
463
|
-
'Report a redacted ATS mechanics or scenario-coverage observation. Never include answer values, addresses, contact data, OTPs, or free-form page content.',
|
|
464
|
-
{
|
|
465
|
-
runId: z.number().int().min(1),
|
|
466
|
-
provider: z.string().regex(SAFE_OBSERVATION_CODE),
|
|
467
|
-
fieldLabel: z.string().min(1).max(1000),
|
|
468
|
-
observationType: z.string().regex(SAFE_OBSERVATION_CODE),
|
|
469
|
-
resolutionCode: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
470
|
-
metadata: z.object({
|
|
471
|
-
controlType: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
472
|
-
required: z.boolean().optional(),
|
|
473
|
-
errorCode: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
474
|
-
committed: z.boolean(),
|
|
475
|
-
scenarioCode: z.enum(APPLY_SCENARIO_CODES),
|
|
476
|
-
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
477
|
-
browserBindingHash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
478
|
-
resumedAfterHandoff: z.boolean().optional(),
|
|
479
|
-
}),
|
|
480
|
-
},
|
|
481
|
-
wrapTool(async (params) => apiRequest('POST', '/api/jobscout/apply/observations', params, false, false, MCP_USER_AGENT), 'Failed to report apply observation')
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
server.tool(
|
|
485
|
-
'trackly_record_application_outcome',
|
|
486
|
-
'Record review readiness or a user-confirmed outcome. Mark submitted only after a success page or explicit user confirmation.',
|
|
487
|
-
{
|
|
488
|
-
runId: z.number().int().min(1),
|
|
489
|
-
outcome: z.enum(['review_ready', 'submitted', 'failed', 'blocked']),
|
|
490
|
-
confirmation: z.string().max(500).optional(),
|
|
491
|
-
},
|
|
492
|
-
wrapTool(async ({ runId, ...body }) => apiRequest('POST', `/api/jobscout/apply/runs/${runId}/outcome`, body, false, false, MCP_USER_AGENT), 'Failed to record application outcome')
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
server.tool(
|
|
496
|
-
'trackly_prepare_resume',
|
|
497
|
-
'Download the authenticated default resume into a mode-0600 temporary Trackly cache and return exact-file proof for user confirmation before browser upload.',
|
|
498
|
-
{
|
|
499
|
-
runId: z.number().int().min(1),
|
|
500
|
-
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
501
|
-
browserBindingHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
502
|
-
},
|
|
503
|
-
wrapTool(async ({ runId, browserSurface, browserBindingHash }) =>
|
|
504
|
-
prepareResume(runId, browserSurface, browserBindingHash), 'Failed to prepare default resume')
|
|
505
|
-
);
|
|
506
|
-
|
|
507
|
-
server.tool(
|
|
508
|
-
'trackly_verify_prepared_resume',
|
|
509
|
-
'Immediately before attachment, recompute the prepared resume fingerprint, validate its run and expiration, and lock the confirmed file read-only.',
|
|
510
|
-
{
|
|
511
|
-
runId: z.number().int().min(1),
|
|
512
|
-
confirmationId: z.string().min(1).max(200),
|
|
513
|
-
exactLocalPath: z.string().min(1).max(4096),
|
|
514
|
-
sha256: z.string().regex(/^[a-f0-9]{64}$/i),
|
|
515
|
-
sizeBytes: z.number().int().min(1),
|
|
516
|
-
expiresAt: z.string().datetime(),
|
|
517
|
-
},
|
|
518
|
-
wrapTool(async (proof) => verifyPreparedResume(proof), 'Prepared resume integrity verification failed')
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
server.registerPrompt('trackly-apply', {
|
|
522
|
-
title: 'Apply to the next Trackly job',
|
|
523
|
-
description: 'Run the manual-submit Trackly Apply workflow for the next user-approved job.',
|
|
524
|
-
}, async () => ({
|
|
525
|
-
messages: [{
|
|
526
|
-
role: 'user',
|
|
527
|
-
content: {
|
|
528
|
-
type: 'text',
|
|
529
|
-
text: 'Compatibility gate: before starting a new run, require the fetched Trackly Apply protocol to be version 3.1.0 or newer. After trackly_start_apply_run returns, or before resuming an existing run, require the returned or stored run.protocolVersion to be version 3.1.0 or newer. Never continue or replace a pre-evidence 3.0.x run; preserve it, record it blocked when possible, and stop for supported lifecycle cleanup.',
|
|
530
|
-
},
|
|
531
|
-
}, {
|
|
532
|
-
role: 'user',
|
|
533
|
-
content: {
|
|
534
|
-
type: 'text',
|
|
535
|
-
text: 'Fetch the Trackly Apply protocol, profile onboarding, profile, and approved queue. Resolve missing answers with me. Treat required completeness separately from optional reusable coverage and employer-specific contextual questions. Before starting anything, stop on every non-null executionBlocker and every manual_only item. Start only the selected approved queue item, require major(run.protocolVersion) === major(protocol.version), require protocol.compatibleSkillMajor === 4, preserve the stored version for a resumed run, and require its provider, atsCapability, required scenarios, and originPolicy to match the queue preflight. Reclaim semantic browser control, verify the exact job/run/tab binding, hash that value-free binding, and report the same-run browser_ready attestation with committed=true. Before entering private data, require the visible company and role to match the run binding and, when available, the requisition identifier to match the stored job URL. When job_identity_match is required, report a value-free committed scenario_coverage attestation only after that visible identity check passes; never include the company, role, URL, requisition identifier, page text, or any profile value in that observation. On exact-origin fallback, revalidate the frozen company, role, and available requisition identity after every navigation or redirect and before entering any additional private data. Normalize every page, redirect, and data-receiving iframe URL; accept an exact authorized origin or hostname only when host === allowedDomain or host.endsWith("." + allowedDomain), never by substring or page text. When originPolicy.verification is trackly_employer_source_exact_origin, authorize only the exact origin in authorizedOrigins: never promote it to a host suffix and never carry it across a redirect or iframe origin change. For every other vendor-hosted ATS policy, require both originPolicy.tenantRule and originPolicy.verifiedAtsTenant to be non-null or stop before private data entry. Execute the backend-owned originPolicy.tenantRule exactly after every redirect or data-receiving iframe change, including its extraction, exact-host-depth, locale, percent-decoding, normalization, and fail-closed semantics, then require the normalized result to equal originPolicy.verifiedAtsTenant; never invent or reinterpret a strategy token. Obey every capability stop condition. Determine whether the form has a semantically identified Resume or CV attachment control. Only when that specific control exists, prepare the run-bound resume locally with that browser surface and binding hash, show me its exact path, filename, size, SHA-256, run, and expiration, and obtain my explicit confirmation. Treat cover-letter, portfolio, transcript, and other supporting-document controls separately according to the profile and protocol; never upload a resume to them. Immediately before attaching the resume, use the local verifier to validate the signed proof, recompute hash and size, check expiration, and lock the file read-only. Fill the form through semantic controls, verify committed values and every required error, and report a same-run passed or corrected scenario_coverage observation with committed=true for every backend-required scenario except browser_reclaim, which is satisfied only by browser_ready with the binding hash. Before every review_ready outcome, also report value-free committed critical_contact_integrity and manual_submit_boundary evidence; never include contact values, answers, page text, or local paths. If a required or universal review scenario cannot pass, record blocked rather than review_ready. Stop before Submit. If maintenance interrupts the run, retain the run and browser context, wait for the advertised window, refetch protocol, queue, and profile state, and resume the existing agent_browser run. Never start a duplicate run, blindly retry a mutation, enter credentials or verification codes, evade human verification, or click Submit.',
|
|
536
|
-
},
|
|
537
|
-
}],
|
|
538
|
-
}));
|
|
539
|
-
|
|
540
|
-
server.registerResource('trackly-apply-protocol', 'trackly://apply/protocol', {
|
|
541
|
-
title: 'Current Trackly Apply protocol',
|
|
542
|
-
description: 'Versioned browser mechanics and compatibility contract.',
|
|
543
|
-
mimeType: 'application/json',
|
|
544
|
-
}, async (uri) => {
|
|
545
|
-
try {
|
|
546
|
-
const result = await apiRequest('GET', '/api/jobscout/apply/protocol', null, false, false, MCP_USER_AGENT);
|
|
547
|
-
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result) }] };
|
|
548
|
-
} catch (error) {
|
|
549
|
-
return throwMcpResourceError(error);
|
|
550
|
-
}
|
|
462
|
+
registerApplyTools(server, {
|
|
463
|
+
wrapTool,
|
|
464
|
+
mcpUserAgent: MCP_USER_AGENT,
|
|
465
|
+
throwMcpResourceError,
|
|
551
466
|
});
|
|
552
|
-
|
|
553
467
|
return server;
|
|
554
468
|
}
|
|
555
469
|
|