trackly-cli 0.7.3 → 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 +16 -4
- package/bin/trackly +2 -0
- package/contracts/trackly-apply-tools.json +26 -7
- package/docs/trackly-tools.md +14 -2
- package/lib/agent.js +197 -11
- package/lib/client.js +40 -2
- package/mcp/apply-tools.js +666 -0
- package/mcp/server.js +5 -204
- 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
|
@@ -5,9 +5,8 @@ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio
|
|
|
5
5
|
const { McpError } = require('@modelcontextprotocol/sdk/types.js');
|
|
6
6
|
const { z } = require('zod');
|
|
7
7
|
const { apiRequest, createTracklyAccessError, hasAuth, maintenanceOutput } = require('../lib/client');
|
|
8
|
-
const { prepareResume, verifyPreparedResume } = require('../lib/agent');
|
|
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;
|
|
@@ -16,9 +15,6 @@ const MCP_AUTH_ERROR_CODE = -32004;
|
|
|
16
15
|
const AUTH_HINT =
|
|
17
16
|
'Existing members: run `trackly login` or set TRACKLY_API_KEY. ' +
|
|
18
17
|
'New members need a private invite during the limited rollout; request access at https://usetrackly.app/early-access.';
|
|
19
|
-
const APPLY_BROWSER_SURFACES = APPLY_CONTRACT.constants.applyBrowserSurfaces;
|
|
20
|
-
const APPLY_SCENARIO_CODES = APPLY_CONTRACT.constants.applyScenarioCodes;
|
|
21
|
-
const SAFE_OBSERVATION_CODE = /^[a-z0-9][a-z0-9_:-]{0,99}$/;
|
|
22
18
|
|
|
23
19
|
// Mirrors `granola-followup-app/src/services/region-classifier.ts:8` REGION_TAGS.
|
|
24
20
|
// Keep in sync when the backend enum changes.
|
|
@@ -463,206 +459,11 @@ function createServer() {
|
|
|
463
459
|
}, 'Failed to request company')
|
|
464
460
|
);
|
|
465
461
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
{ limit: z.number().int().min(1).max(100).optional() },
|
|
471
|
-
wrapTool(async ({ limit }) => {
|
|
472
|
-
const qs = limit ? `?limit=${limit}` : '';
|
|
473
|
-
return apiRequest('GET', `/api/jobscout/apply/queue${qs}`, null, false, false, MCP_USER_AGENT);
|
|
474
|
-
}, 'Failed to fetch apply queue')
|
|
475
|
-
);
|
|
476
|
-
|
|
477
|
-
server.tool(
|
|
478
|
-
'trackly_get_application_profile',
|
|
479
|
-
'Get the versioned application profile. Sensitive values are returned only after the user opted into encrypted storage.',
|
|
480
|
-
{
|
|
481
|
-
includeSensitive: z.boolean().optional(),
|
|
482
|
-
provider: z.string().max(100).optional(),
|
|
483
|
-
companyId: z.string().max(100).optional(),
|
|
484
|
-
},
|
|
485
|
-
wrapTool(async ({ includeSensitive, provider, companyId }) => {
|
|
486
|
-
const qs = new URLSearchParams();
|
|
487
|
-
if (includeSensitive) qs.set('includeSensitive', 'true');
|
|
488
|
-
if (provider) qs.set('provider', provider);
|
|
489
|
-
if (companyId) qs.set('companyId', companyId);
|
|
490
|
-
return apiRequest('GET', `/api/jobscout/application-profile?${qs.toString()}`, null, false, false, MCP_USER_AGENT);
|
|
491
|
-
}, 'Failed to fetch application profile')
|
|
492
|
-
);
|
|
493
|
-
|
|
494
|
-
server.tool(
|
|
495
|
-
'trackly_get_profile_onboarding',
|
|
496
|
-
'Get the backend-owned profile schema and onboarding questions. Ask only fields whose state is unknown or needs confirmation.',
|
|
497
|
-
{},
|
|
498
|
-
wrapTool(async () => {
|
|
499
|
-
const [schema, profile] = await Promise.all([
|
|
500
|
-
apiRequest('GET', '/api/jobscout/application-profile/schema', null, false, false, MCP_USER_AGENT),
|
|
501
|
-
apiRequest('GET', '/api/jobscout/application-profile', null, false, false, MCP_USER_AGENT),
|
|
502
|
-
]);
|
|
503
|
-
return { schema, profile };
|
|
504
|
-
}, 'Failed to fetch profile onboarding')
|
|
505
|
-
);
|
|
506
|
-
|
|
507
|
-
server.tool(
|
|
508
|
-
'trackly_update_application_profile',
|
|
509
|
-
'Update confirmed profile answers with optimistic concurrency. Use global scope only for an explicit always-answer preference.',
|
|
510
|
-
{
|
|
511
|
-
expectedRevision: z.number().int().min(1),
|
|
512
|
-
source: z.enum(['web', 'ios', 'macos', 'codex', 'claude', 'mcp']).optional(),
|
|
513
|
-
changes: z.array(z.discriminatedUnion('scope', [
|
|
514
|
-
z.object({
|
|
515
|
-
key: z.string().min(1).max(200), state: z.enum(['unknown', 'answered', 'intentionally_blank', 'declined']),
|
|
516
|
-
value: z.any().optional(), scope: z.literal('global'), questionLabel: z.string().max(1000).optional(),
|
|
517
|
-
}),
|
|
518
|
-
z.object({
|
|
519
|
-
key: z.string().min(1).max(200), state: z.enum(['unknown', 'answered', 'intentionally_blank', 'declined']),
|
|
520
|
-
value: z.any().optional(), scope: z.literal('provider'), scopeValue: z.string().min(1).max(200),
|
|
521
|
-
questionLabel: z.string().max(1000).optional(),
|
|
522
|
-
}),
|
|
523
|
-
z.object({
|
|
524
|
-
key: z.string().min(1).max(200), state: z.enum(['unknown', 'answered', 'intentionally_blank', 'declined']),
|
|
525
|
-
value: z.any().optional(), scope: z.literal('company'), scopeValue: z.string().min(1).max(200),
|
|
526
|
-
questionLabel: z.string().max(1000).optional(),
|
|
527
|
-
}),
|
|
528
|
-
])).max(100).optional(),
|
|
529
|
-
education: z.array(z.object({
|
|
530
|
-
school: z.string().min(1).max(500),
|
|
531
|
-
degree: z.string().max(500).nullable().optional(),
|
|
532
|
-
fieldOfStudy: z.string().max(500).nullable().optional(),
|
|
533
|
-
gpa: z.string().max(50).nullable().optional(),
|
|
534
|
-
startDate: z.string().max(50).nullable().optional(),
|
|
535
|
-
endDate: z.string().max(50).nullable().optional(),
|
|
536
|
-
})).max(20).optional(),
|
|
537
|
-
confirmProfile: z.boolean().optional(),
|
|
538
|
-
sensitiveStorageConsent: z.boolean().optional(),
|
|
539
|
-
},
|
|
540
|
-
wrapTool(async (params) => apiRequest('PATCH', '/api/jobscout/application-profile', params, false, false, MCP_USER_AGENT), 'Failed to update application profile')
|
|
541
|
-
);
|
|
542
|
-
|
|
543
|
-
server.tool(
|
|
544
|
-
'trackly_start_apply_run',
|
|
545
|
-
'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.',
|
|
546
|
-
{ jobId: z.number().int().min(1), clientName: z.string().max(100).optional() },
|
|
547
|
-
wrapTool(async (params) => apiRequest('POST', '/api/jobscout/apply/runs', params, false, false, MCP_USER_AGENT), 'Failed to start apply run')
|
|
548
|
-
);
|
|
549
|
-
|
|
550
|
-
server.tool(
|
|
551
|
-
'trackly_get_apply_evidence',
|
|
552
|
-
'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.',
|
|
553
|
-
{
|
|
554
|
-
windowDays: z.number().int().min(1).max(365).optional(),
|
|
555
|
-
targetReviewedRuns: z.number().int().min(1).max(1000).optional(),
|
|
556
|
-
},
|
|
557
|
-
wrapTool(async ({ windowDays, targetReviewedRuns }) => {
|
|
558
|
-
const qs = new URLSearchParams();
|
|
559
|
-
if (windowDays !== undefined) qs.set('windowDays', String(windowDays));
|
|
560
|
-
if (targetReviewedRuns !== undefined) qs.set('targetReviewedRuns', String(targetReviewedRuns));
|
|
561
|
-
const query = qs.toString();
|
|
562
|
-
const suffix = query ? `?${query}` : '';
|
|
563
|
-
return apiRequest('GET', `/api/jobscout/apply/evidence${suffix}`, null, false, false, MCP_USER_AGENT);
|
|
564
|
-
}, 'Failed to fetch apply evidence')
|
|
565
|
-
);
|
|
566
|
-
|
|
567
|
-
server.tool(
|
|
568
|
-
'trackly_get_apply_protocol',
|
|
569
|
-
'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.',
|
|
570
|
-
{},
|
|
571
|
-
wrapTool(async () => apiRequest('GET', '/api/jobscout/apply/protocol', null, false, false, MCP_USER_AGENT), 'Failed to fetch apply protocol')
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
server.tool(
|
|
575
|
-
'trackly_report_apply_observation',
|
|
576
|
-
'Report a redacted ATS mechanics or scenario-coverage observation. Never include answer values, addresses, contact data, OTPs, or free-form page content.',
|
|
577
|
-
{
|
|
578
|
-
runId: z.number().int().min(1),
|
|
579
|
-
provider: z.string().regex(SAFE_OBSERVATION_CODE),
|
|
580
|
-
fieldLabel: z.string().min(1).max(1000),
|
|
581
|
-
observationType: z.string().regex(SAFE_OBSERVATION_CODE),
|
|
582
|
-
resolutionCode: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
583
|
-
metadata: z.object({
|
|
584
|
-
controlType: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
585
|
-
required: z.boolean().optional(),
|
|
586
|
-
errorCode: z.string().regex(SAFE_OBSERVATION_CODE).optional(),
|
|
587
|
-
committed: z.boolean(),
|
|
588
|
-
scenarioCode: z.enum(APPLY_SCENARIO_CODES),
|
|
589
|
-
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
590
|
-
browserBindingHash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
591
|
-
resumedAfterHandoff: z.boolean().optional(),
|
|
592
|
-
}),
|
|
593
|
-
},
|
|
594
|
-
wrapTool(async (params) => apiRequest('POST', '/api/jobscout/apply/observations', params, false, false, MCP_USER_AGENT), 'Failed to report apply observation')
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
server.tool(
|
|
598
|
-
'trackly_record_application_outcome',
|
|
599
|
-
'Record review readiness or a user-confirmed outcome. Mark submitted only after a success page or explicit user confirmation.',
|
|
600
|
-
{
|
|
601
|
-
runId: z.number().int().min(1),
|
|
602
|
-
outcome: z.enum(['review_ready', 'submitted', 'failed', 'blocked']),
|
|
603
|
-
confirmation: z.string().max(500).optional(),
|
|
604
|
-
},
|
|
605
|
-
wrapTool(async ({ runId, ...body }) => apiRequest('POST', `/api/jobscout/apply/runs/${runId}/outcome`, body, false, false, MCP_USER_AGENT), 'Failed to record application outcome')
|
|
606
|
-
);
|
|
607
|
-
|
|
608
|
-
server.tool(
|
|
609
|
-
'trackly_prepare_resume',
|
|
610
|
-
'Download the authenticated default resume into a mode-0600 temporary Trackly cache and return exact-file proof for user confirmation before browser upload.',
|
|
611
|
-
{
|
|
612
|
-
runId: z.number().int().min(1),
|
|
613
|
-
browserSurface: z.enum(APPLY_BROWSER_SURFACES),
|
|
614
|
-
browserBindingHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
615
|
-
},
|
|
616
|
-
wrapTool(async ({ runId, browserSurface, browserBindingHash }) =>
|
|
617
|
-
prepareResume(runId, browserSurface, browserBindingHash), 'Failed to prepare default resume')
|
|
618
|
-
);
|
|
619
|
-
|
|
620
|
-
server.tool(
|
|
621
|
-
'trackly_verify_prepared_resume',
|
|
622
|
-
'Immediately before attachment, recompute the prepared resume fingerprint, validate its run and expiration, and lock the confirmed file read-only.',
|
|
623
|
-
{
|
|
624
|
-
runId: z.number().int().min(1),
|
|
625
|
-
confirmationId: z.string().min(1).max(200),
|
|
626
|
-
exactLocalPath: z.string().min(1).max(4096),
|
|
627
|
-
sha256: z.string().regex(/^[a-f0-9]{64}$/i),
|
|
628
|
-
sizeBytes: z.number().int().min(1),
|
|
629
|
-
expiresAt: z.string().datetime(),
|
|
630
|
-
},
|
|
631
|
-
wrapTool(async (proof) => verifyPreparedResume(proof), 'Prepared resume integrity verification failed')
|
|
632
|
-
);
|
|
633
|
-
|
|
634
|
-
server.registerPrompt('trackly-apply', {
|
|
635
|
-
title: 'Apply to the next Trackly job',
|
|
636
|
-
description: 'Run the manual-submit Trackly Apply workflow for the next user-approved job.',
|
|
637
|
-
}, async () => ({
|
|
638
|
-
messages: [{
|
|
639
|
-
role: 'user',
|
|
640
|
-
content: {
|
|
641
|
-
type: 'text',
|
|
642
|
-
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.',
|
|
643
|
-
},
|
|
644
|
-
}, {
|
|
645
|
-
role: 'user',
|
|
646
|
-
content: {
|
|
647
|
-
type: 'text',
|
|
648
|
-
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.',
|
|
649
|
-
},
|
|
650
|
-
}],
|
|
651
|
-
}));
|
|
652
|
-
|
|
653
|
-
server.registerResource('trackly-apply-protocol', 'trackly://apply/protocol', {
|
|
654
|
-
title: 'Current Trackly Apply protocol',
|
|
655
|
-
description: 'Versioned browser mechanics and compatibility contract.',
|
|
656
|
-
mimeType: 'application/json',
|
|
657
|
-
}, async (uri) => {
|
|
658
|
-
try {
|
|
659
|
-
const result = await apiRequest('GET', '/api/jobscout/apply/protocol', null, false, false, MCP_USER_AGENT);
|
|
660
|
-
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result) }] };
|
|
661
|
-
} catch (error) {
|
|
662
|
-
return throwMcpResourceError(error);
|
|
663
|
-
}
|
|
462
|
+
registerApplyTools(server, {
|
|
463
|
+
wrapTool,
|
|
464
|
+
mcpUserAgent: MCP_USER_AGENT,
|
|
465
|
+
throwMcpResourceError,
|
|
664
466
|
});
|
|
665
|
-
|
|
666
467
|
return server;
|
|
667
468
|
}
|
|
668
469
|
|