trackly-cli
Advanced tools
| { | ||
| "contractVersion": "3.3.3", | ||
| "contractVersion": "3.4.0", | ||
| "constants": { | ||
@@ -18,3 +18,3 @@ "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"], | ||
| "trackly_get_profile_onboarding": "{}", | ||
| "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()}", | ||
| "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()}", | ||
| "trackly_create_apply_batch": "{limit:z.number().int().min(1).max(100),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}", | ||
@@ -21,0 +21,0 @@ "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()}", |
@@ -105,3 +105,3 @@ ## Trackly Job Tracker (MCP) | ||
| - **trackly_get_profile_onboarding** — Get backend-owned questions plus only missing/unconfirmed answers. | ||
| - **trackly_update_application_profile** — Save explicit answer states with optimistic concurrency and global/provider/company scope. | ||
| - **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. | ||
| - **trackly_create_apply_batch** — Freeze an exact recent-first set of approved jobs with an idempotency key. | ||
@@ -108,0 +108,0 @@ - **trackly_get_active_apply_batch** — Recover the newest unexpired active batch before creating another after chat or browser context loss. |
+71
-2
| 'use strict'; | ||
| const { createHash } = require('node:crypto'); | ||
| const { z } = require('zod'); | ||
@@ -21,2 +22,22 @@ const { apiRequest } = require('../lib/client'); | ||
| const APPLY_BATCH_MAX_BULK_MUTATIONS = 20; | ||
| function sensitiveRevocationConfirmation(profileResponse) { | ||
| const profile = profileResponse?.profile || {}; | ||
| const currentRevision = Number(profile.revision); | ||
| const fields = profile.fields || {}; | ||
| // Stored answer rows serialize an `encrypted` boolean plus their PERSISTED | ||
| // row sensitivity — the same predicate the backend DELETE uses. Profile-column | ||
| // and backfilled entries never carry `encrypted` and are never deleted. | ||
| const affectedKeys = Object.keys(fields) | ||
| .filter((key) => { | ||
| const entry = fields[key]; | ||
| return entry && typeof entry.encrypted === 'boolean' | ||
| && (entry.sensitivity === 'sensitive' || entry.sensitivity === 'restricted'); | ||
| }) | ||
| .sort(); | ||
| const confirmationToken = createHash('sha256') | ||
| .update(`trackly:sensitive-revocation:v1:${currentRevision}:${affectedKeys.join(',')}`, 'utf8') | ||
| .digest('hex'); | ||
| return { currentRevision, affectedKeys, confirmationToken }; | ||
| } | ||
| const SAFE_OBSERVATION_CODE = /^[a-z0-9][a-z0-9_:-]{0,99}$/; | ||
@@ -157,3 +178,3 @@ const SAFE_IDEMPOTENCY_KEY = /^[\x20-\x7e]+$/; | ||
| 'trackly_update_application_profile', | ||
| 'Update confirmed profile answers with optimistic concurrency. Use global scope only for an explicit always-answer preference.', | ||
| '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.', | ||
| { | ||
@@ -188,4 +209,52 @@ expectedRevision: z.number().int().min(1), | ||
| sensitiveStorageConsent: z.boolean().optional(), | ||
| sensitiveRevocationConfirmToken: z.string().regex(/^[a-f0-9]{64}$/).optional(), | ||
| }, | ||
| wrapTool(async (params) => apiRequest('PATCH', '/api/jobscout/application-profile', params, false, false, MCP_USER_AGENT), 'Failed to update application profile') | ||
| wrapTool(async (params) => { | ||
| const { sensitiveRevocationConfirmToken, ...body } = params; | ||
| if (body.sensitiveStorageConsent === false) { | ||
| const profileResponse = await apiRequest('GET', '/api/jobscout/application-profile', null, false, false, MCP_USER_AGENT); | ||
| const rawRevision = Number(profileResponse?.profile?.revision); | ||
| if (!Number.isSafeInteger(rawRevision) || rawRevision < 1) { | ||
| throw { | ||
| status: 502, | ||
| code: 'invalid_profile_revision', | ||
| error: 'Trackly did not return a valid profile revision. No changes were saved.', | ||
| }; | ||
| } | ||
| const profileFieldsShape = profileResponse?.profile?.fields; | ||
| const profileEntriesValid = !!profileFieldsShape | ||
| && typeof profileFieldsShape === 'object' | ||
| && !Array.isArray(profileFieldsShape) | ||
| && Object.keys(profileFieldsShape).length > 0 | ||
| && Object.values(profileFieldsShape).every((entry) => entry | ||
| && typeof entry === 'object' && !Array.isArray(entry) | ||
| && typeof entry.state === 'string' | ||
| && (entry.sensitivity === 'standard' || entry.sensitivity === 'sensitive' || entry.sensitivity === 'restricted') | ||
| && (!('encrypted' in entry) || typeof entry.encrypted === 'boolean')); | ||
| if (!profileEntriesValid) { | ||
| throw { | ||
| status: 502, | ||
| code: 'invalid_profile_response', | ||
| error: 'Trackly returned an incomplete profile, so the deletion scope cannot be verified. No changes were saved.', | ||
| }; | ||
| } | ||
| const { currentRevision, affectedKeys, confirmationToken } = | ||
| sensitiveRevocationConfirmation(profileResponse); | ||
| if (sensitiveRevocationConfirmToken !== confirmationToken || body.expectedRevision !== currentRevision) { | ||
| throw { | ||
| status: 409, | ||
| code: 'sensitive_revocation_confirmation_required', | ||
| error: 'Revoking sensitive storage consent permanently deletes every stored sensitive and restricted answer. No changes were saved.', | ||
| confirmation: { | ||
| currentRevision, | ||
| affectedKeys, | ||
| 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.', | ||
| confirmationToken, | ||
| 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.', | ||
| }, | ||
| }; | ||
| } | ||
| } | ||
| return apiRequest('PATCH', '/api/jobscout/application-profile', body, false, false, MCP_USER_AGENT); | ||
| }, 'Failed to update application profile') | ||
| ); | ||
@@ -192,0 +261,0 @@ |
+7
-0
@@ -91,2 +91,5 @@ 'use strict'; | ||
| || error?.code === 'invalid_preference_revision' | ||
| || error?.code === 'sensitive_revocation_confirmation_required' | ||
| || error?.code === 'invalid_profile_revision' | ||
| || error?.code === 'invalid_profile_response' | ||
| ) { | ||
@@ -96,2 +99,6 @@ payload.code = error.code; | ||
| if (error?.code === 'sensitive_revocation_confirmation_required' && error?.confirmation) { | ||
| payload.confirmation = error.confirmation; | ||
| } | ||
| if (error?.status === 409 && error?.error === 'preference_revision_conflict') { | ||
@@ -98,0 +105,0 @@ payload.preferences = error.preferences; |
| { | ||
| "name": "trackly-cli", | ||
| "version": "0.10.0", | ||
| "version": "0.10.1", | ||
| "lockfileVersion": 3, | ||
@@ -9,3 +9,3 @@ "requires": true, | ||
| "name": "trackly-cli", | ||
| "version": "0.10.0", | ||
| "version": "0.10.1", | ||
| "license": "MIT", | ||
@@ -12,0 +12,0 @@ "dependencies": { |
+1
-1
| { | ||
| "name": "trackly-cli", | ||
| "version": "0.10.0", | ||
| "version": "0.10.1", | ||
| "mcpName": "io.github.trackly-app/trackly", | ||
@@ -5,0 +5,0 @@ "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.", |
+1
-1
@@ -179,3 +179,3 @@ [](https://www.npmjs.com/package/trackly-cli) | ||
| | trackly_get_profile_onboarding | Get schema plus missing questions | | ||
| | trackly_update_application_profile | Save scoped answers with optimistic concurrency | | ||
| | trackly_update_application_profile | Save scoped answers with optimistic concurrency; revoking sensitive storage takes a two-step confirmation | | ||
| | trackly_create_apply_batch | Freeze an exact recent-first approved batch | | ||
@@ -182,0 +182,0 @@ | trackly_get_active_apply_batch | Recover the newest unexpired active batch after context loss | |
+2
-2
@@ -20,3 +20,3 @@ { | ||
| ], | ||
| "version": "0.10.0", | ||
| "version": "0.10.1", | ||
| "packages": [ | ||
@@ -26,3 +26,3 @@ { | ||
| "identifier": "trackly-cli", | ||
| "version": "0.10.0", | ||
| "version": "0.10.1", | ||
| "runtimeHint": "npx", | ||
@@ -29,0 +29,0 @@ "runtimeArguments": [ |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
393392
1.27%4640
1.67%