trackly-cli 0.10.0 → 0.10.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 +1 -1
- package/contracts/trackly-apply-tools.json +2 -2
- package/docs/trackly-tools.md +1 -1
- package/mcp/apply-tools.js +71 -2
- package/mcp/server.js +7 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -176,7 +176,7 @@ Then use natural language in any of these clients:
|
|
|
176
176
|
| trackly_get_apply_queue | Get user-approved jobs ready for execution |
|
|
177
177
|
| trackly_get_application_profile | Get versioned profile answers and provenance |
|
|
178
178
|
| trackly_get_profile_onboarding | Get schema plus missing questions |
|
|
179
|
-
| trackly_update_application_profile | Save scoped answers with optimistic concurrency |
|
|
179
|
+
| trackly_update_application_profile | Save scoped answers with optimistic concurrency; revoking sensitive storage takes a two-step confirmation |
|
|
180
180
|
| trackly_create_apply_batch | Freeze an exact recent-first approved batch |
|
|
181
181
|
| trackly_get_active_apply_batch | Recover the newest unexpired active batch after context loss |
|
|
182
182
|
| trackly_get_apply_batch | Read frozen membership with opaque pagination |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "3.
|
|
2
|
+
"contractVersion": "3.4.0",
|
|
3
3
|
"constants": {
|
|
4
4
|
"applyScenarioCodes": ["browser_reclaim", "resume_upload", "resume_parser_recheck", "semantic_boolean_commit", "custom_select_commit", "multi_step_navigation", "free_text_voice", "required_error_sweep", "final_consent", "handoff_reclaim", "critical_contact_integrity", "manual_submit_boundary", "job_identity_match"],
|
|
5
5
|
"applyBrowserSurfaces": ["codex_in_app", "chrome_extension", "claude_in_chrome"],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"trackly_get_apply_queue": "{limit:z.number().int().min(1).max(100).optional(),cursor:z.string().min(1).max(2048).optional()}",
|
|
16
16
|
"trackly_get_application_profile": "{includeSensitive:z.boolean().optional(),provider:z.string().max(100).optional(),companyId:z.string().max(100).optional()}",
|
|
17
17
|
"trackly_get_profile_onboarding": "{}",
|
|
18
|
-
"trackly_update_application_profile": "{expectedRevision:z.number().int().min(1),source:z.enum(['web','ios','macos','codex','claude','mcp']).optional(),changes:z.array(z.discriminatedUnion('scope',[z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('global'),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('provider'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('company'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()})])).max(100).optional(),education:z.array(z.object({school:z.string().min(1).max(500),degree:z.string().max(500).nullable().optional(),fieldOfStudy:z.string().max(500).nullable().optional(),gpa:z.string().max(50).nullable().optional(),startDate:z.string().max(50).nullable().optional(),endDate:z.string().max(50).nullable().optional()})).max(20).optional(),confirmProfile:z.boolean().optional(),sensitiveStorageConsent:z.boolean().optional()}",
|
|
18
|
+
"trackly_update_application_profile": "{expectedRevision:z.number().int().min(1),source:z.enum(['web','ios','macos','codex','claude','mcp']).optional(),changes:z.array(z.discriminatedUnion('scope',[z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('global'),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('provider'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('company'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()})])).max(100).optional(),education:z.array(z.object({school:z.string().min(1).max(500),degree:z.string().max(500).nullable().optional(),fieldOfStudy:z.string().max(500).nullable().optional(),gpa:z.string().max(50).nullable().optional(),startDate:z.string().max(50).nullable().optional(),endDate:z.string().max(50).nullable().optional()})).max(20).optional(),confirmProfile:z.boolean().optional(),sensitiveStorageConsent:z.boolean().optional(),sensitiveRevocationConfirmToken:z.string().regex(/^[a-f0-9]{64}$/).optional()}",
|
|
19
19
|
"trackly_create_apply_batch": "{limit:z.number().int().min(1).max(100),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
20
20
|
"trackly_get_active_apply_batch": "{limit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),cursor:z.string().min(1).max(2048).optional(),actionLimit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),actionCursor:z.string().min(1).max(2048).optional()}",
|
|
21
21
|
"trackly_get_apply_batch": "{batchId:z.number().int().min(1),limit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),cursor:z.string().min(1).max(2048).optional(),actionLimit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),actionCursor:z.string().min(1).max(2048).optional()}",
|
package/docs/trackly-tools.md
CHANGED
|
@@ -102,7 +102,7 @@ API-key creation attempts.
|
|
|
102
102
|
- **trackly_get_apply_queue** — Get user-approved check-later jobs in deterministic execution order.
|
|
103
103
|
- **trackly_get_application_profile** — Get the versioned, scoped application profile.
|
|
104
104
|
- **trackly_get_profile_onboarding** — Get backend-owned questions plus only missing/unconfirmed answers.
|
|
105
|
-
- **trackly_update_application_profile** — Save explicit answer states with optimistic concurrency and global/provider/company scope.
|
|
105
|
+
- **trackly_update_application_profile** — Save explicit answer states with optimistic concurrency and global/provider/company scope. Setting `sensitiveStorageConsent=false` is destructive and requires a second call echoing the `sensitiveRevocationConfirmToken` from the returned challenge.
|
|
106
106
|
- **trackly_create_apply_batch** — Freeze an exact recent-first set of approved jobs with an idempotency key.
|
|
107
107
|
- **trackly_get_active_apply_batch** — Recover the newest unexpired active batch before creating another after chat or browser context loss.
|
|
108
108
|
- **trackly_get_apply_batch** — Page a server-owned frozen batch without reordering or replacing members.
|
package/mcp/apply-tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { createHash } = require('node:crypto');
|
|
3
4
|
const { z } = require('zod');
|
|
4
5
|
const { apiRequest } = require('../lib/client');
|
|
5
6
|
const { prepareResume, verifyPreparedResume } = require('../lib/agent');
|
|
@@ -18,6 +19,26 @@ const APPLY_BATCH_MAX_MEMBERS = 100;
|
|
|
18
19
|
const APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST = 20;
|
|
19
20
|
const APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT = 25;
|
|
20
21
|
const APPLY_BATCH_MAX_BULK_MUTATIONS = 20;
|
|
22
|
+
|
|
23
|
+
function sensitiveRevocationConfirmation(profileResponse) {
|
|
24
|
+
const profile = profileResponse?.profile || {};
|
|
25
|
+
const currentRevision = Number(profile.revision);
|
|
26
|
+
const fields = profile.fields || {};
|
|
27
|
+
// Stored answer rows serialize an `encrypted` boolean plus their PERSISTED
|
|
28
|
+
// row sensitivity — the same predicate the backend DELETE uses. Profile-column
|
|
29
|
+
// and backfilled entries never carry `encrypted` and are never deleted.
|
|
30
|
+
const affectedKeys = Object.keys(fields)
|
|
31
|
+
.filter((key) => {
|
|
32
|
+
const entry = fields[key];
|
|
33
|
+
return entry && typeof entry.encrypted === 'boolean'
|
|
34
|
+
&& (entry.sensitivity === 'sensitive' || entry.sensitivity === 'restricted');
|
|
35
|
+
})
|
|
36
|
+
.sort();
|
|
37
|
+
const confirmationToken = createHash('sha256')
|
|
38
|
+
.update(`trackly:sensitive-revocation:v1:${currentRevision}:${affectedKeys.join(',')}`, 'utf8')
|
|
39
|
+
.digest('hex');
|
|
40
|
+
return { currentRevision, affectedKeys, confirmationToken };
|
|
41
|
+
}
|
|
21
42
|
const SAFE_OBSERVATION_CODE = /^[a-z0-9][a-z0-9_:-]{0,99}$/;
|
|
22
43
|
const SAFE_IDEMPOTENCY_KEY = /^[\x20-\x7e]+$/;
|
|
23
44
|
|
|
@@ -153,7 +174,7 @@ function registerApplyTools(
|
|
|
153
174
|
|
|
154
175
|
server.tool(
|
|
155
176
|
'trackly_update_application_profile',
|
|
156
|
-
'Update confirmed profile answers with optimistic concurrency. Use global scope only for an explicit always-answer preference.',
|
|
177
|
+
'Update confirmed profile answers with optimistic concurrency. Use global scope only for an explicit always-answer preference. Setting sensitiveStorageConsent=false permanently deletes every stored sensitive and restricted answer and is a two-step action: the first call saves nothing and returns a confirmation challenge; retry with the echoed sensitiveRevocationConfirmToken to proceed.',
|
|
157
178
|
{
|
|
158
179
|
expectedRevision: z.number().int().min(1),
|
|
159
180
|
source: z.enum(['web', 'ios', 'macos', 'codex', 'claude', 'mcp']).optional(),
|
|
@@ -183,8 +204,56 @@ function registerApplyTools(
|
|
|
183
204
|
})).max(20).optional(),
|
|
184
205
|
confirmProfile: z.boolean().optional(),
|
|
185
206
|
sensitiveStorageConsent: z.boolean().optional(),
|
|
207
|
+
sensitiveRevocationConfirmToken: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
186
208
|
},
|
|
187
|
-
wrapTool(async (params) =>
|
|
209
|
+
wrapTool(async (params) => {
|
|
210
|
+
const { sensitiveRevocationConfirmToken, ...body } = params;
|
|
211
|
+
if (body.sensitiveStorageConsent === false) {
|
|
212
|
+
const profileResponse = await apiRequest('GET', '/api/jobscout/application-profile', null, false, false, MCP_USER_AGENT);
|
|
213
|
+
const rawRevision = Number(profileResponse?.profile?.revision);
|
|
214
|
+
if (!Number.isSafeInteger(rawRevision) || rawRevision < 1) {
|
|
215
|
+
throw {
|
|
216
|
+
status: 502,
|
|
217
|
+
code: 'invalid_profile_revision',
|
|
218
|
+
error: 'Trackly did not return a valid profile revision. No changes were saved.',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const profileFieldsShape = profileResponse?.profile?.fields;
|
|
222
|
+
const profileEntriesValid = !!profileFieldsShape
|
|
223
|
+
&& typeof profileFieldsShape === 'object'
|
|
224
|
+
&& !Array.isArray(profileFieldsShape)
|
|
225
|
+
&& Object.keys(profileFieldsShape).length > 0
|
|
226
|
+
&& Object.values(profileFieldsShape).every((entry) => entry
|
|
227
|
+
&& typeof entry === 'object' && !Array.isArray(entry)
|
|
228
|
+
&& typeof entry.state === 'string'
|
|
229
|
+
&& (entry.sensitivity === 'standard' || entry.sensitivity === 'sensitive' || entry.sensitivity === 'restricted')
|
|
230
|
+
&& (!('encrypted' in entry) || typeof entry.encrypted === 'boolean'));
|
|
231
|
+
if (!profileEntriesValid) {
|
|
232
|
+
throw {
|
|
233
|
+
status: 502,
|
|
234
|
+
code: 'invalid_profile_response',
|
|
235
|
+
error: 'Trackly returned an incomplete profile, so the deletion scope cannot be verified. No changes were saved.',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const { currentRevision, affectedKeys, confirmationToken } =
|
|
239
|
+
sensitiveRevocationConfirmation(profileResponse);
|
|
240
|
+
if (sensitiveRevocationConfirmToken !== confirmationToken || body.expectedRevision !== currentRevision) {
|
|
241
|
+
throw {
|
|
242
|
+
status: 409,
|
|
243
|
+
code: 'sensitive_revocation_confirmation_required',
|
|
244
|
+
error: 'Revoking sensitive storage consent permanently deletes every stored sensitive and restricted answer. No changes were saved.',
|
|
245
|
+
confirmation: {
|
|
246
|
+
currentRevision,
|
|
247
|
+
affectedKeys,
|
|
248
|
+
alsoDeletes: 'Every provider- and company-scoped sensitive or restricted answer is also deleted, along with any stored rows not visible in this profile view; those keys are not listed here.',
|
|
249
|
+
confirmationToken,
|
|
250
|
+
instructions: 'Do not retry automatically. Show the user the affected keys and get explicit confirmation, then retry with the FULL original request body (including any changes, education, or confirmProfile fields) plus expectedRevision=currentRevision and sensitiveRevocationConfirmToken=confirmationToken. If the original call carried other changes, re-read the profile first and reconcile them against the current revision instead of resending stale changes blindly. The token becomes invalid whenever the profile revision changes.',
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return apiRequest('PATCH', '/api/jobscout/application-profile', body, false, false, MCP_USER_AGENT);
|
|
256
|
+
}, 'Failed to update application profile')
|
|
188
257
|
);
|
|
189
258
|
|
|
190
259
|
server.tool(
|
package/mcp/server.js
CHANGED
|
@@ -88,10 +88,17 @@ function createErrorResult(error, fallbackMessage, extra = {}) {
|
|
|
88
88
|
if (
|
|
89
89
|
error?.code === 'experience_filter_v2_unavailable'
|
|
90
90
|
|| error?.code === 'invalid_preference_revision'
|
|
91
|
+
|| error?.code === 'sensitive_revocation_confirmation_required'
|
|
92
|
+
|| error?.code === 'invalid_profile_revision'
|
|
93
|
+
|| error?.code === 'invalid_profile_response'
|
|
91
94
|
) {
|
|
92
95
|
payload.code = error.code;
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
if (error?.code === 'sensitive_revocation_confirmation_required' && error?.confirmation) {
|
|
99
|
+
payload.confirmation = error.confirmation;
|
|
100
|
+
}
|
|
101
|
+
|
|
95
102
|
if (error?.status === 409 && error?.error === 'preference_revision_conflict') {
|
|
96
103
|
payload.preferences = error.preferences;
|
|
97
104
|
payload.discoveryPreferenceRevision = error.discoveryPreferenceRevision;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackly-cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "trackly-cli",
|
|
9
|
-
"version": "0.10.
|
|
9
|
+
"version": "0.10.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "1.30.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackly-cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"mcpName": "io.github.trackly-app/trackly",
|
|
5
5
|
"description": "AI job search CLI + hosted MCP server with OAuth. 128K+ jobs, 1,900+ companies, 40+ ATS. Works with Claude, ChatGPT, Cursor, Windsurf, Codex via hosted streamable-http or local stdio.",
|
|
6
6
|
"bin": {
|
package/server.json
CHANGED
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
]
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
|
-
"version": "0.10.
|
|
20
|
+
"version": "0.10.1",
|
|
21
21
|
"packages": [
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "trackly-cli",
|
|
25
|
-
"version": "0.10.
|
|
25
|
+
"version": "0.10.1",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"runtimeArguments": [
|
|
28
28
|
{
|