trackly-cli 0.14.0 → 0.14.2

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.
@@ -284,6 +284,7 @@ function registerApplyTools(
284
284
  idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : undefined,
285
285
  )
286
286
  );
287
+ const discoveredRecoverableSources = new Map();
287
288
  const discoveredHandoffBindings = new Map();
288
289
  const discoveredHandoffIdsByExecution = new Map();
289
290
  server.tool(
@@ -418,9 +419,19 @@ function registerApplyTools(
418
419
  'trackly_list_recoverable_apply_executions',
419
420
  'List bounded, value-free exact-member recovery candidates after local context loss. Show the stable job identities to the user and obtain explicit confirmation before recovery; never infer or substitute candidates.',
420
421
  {},
421
- wrapTool(async () => recoverableExecutionsResponseSchema.parse(await applyControlRequest(
422
- 'GET', '/api/jobscout/apply/executions/recoverable',
423
- )), 'Failed to list recoverable apply executions')
422
+ wrapTool(async () => {
423
+ const response = recoverableExecutionsResponseSchema.parse(await applyControlRequest(
424
+ 'GET', '/api/jobscout/apply/executions/recoverable',
425
+ ));
426
+ discoveredRecoverableSources.clear();
427
+ for (const source of response.sources) {
428
+ discoveredRecoverableSources.set(source.sourceExecutionId, {
429
+ sourceSnapshotHash: source.sourceSnapshotHash,
430
+ candidateIds: new Set(source.candidates.map(({ candidateId }) => candidateId)),
431
+ });
432
+ }
433
+ return response;
434
+ }, 'Failed to list recoverable apply executions')
424
435
  );
425
436
 
426
437
  server.tool(
@@ -436,13 +447,23 @@ function registerApplyTools(
436
447
  explicitExactSetConfirmation: z.literal(true),
437
448
  idempotencyKey: z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),
438
449
  },
439
- wrapTool(async ({ idempotencyKey, ...input }) => validateExactRecoveryResponse(
440
- input,
441
- exactRecoveryResponseSchema.parse(await applyControlRequest('POST', '/api/jobscout/apply/executions/recover', {
442
- mode: 'recover_exact_members',
443
- ...input,
444
- }, idempotencyKey)),
445
- ), 'Failed to recover exact apply members')
450
+ wrapTool(async ({ idempotencyKey, ...input }) => {
451
+ const source = discoveredRecoverableSources.get(input.sourceExecutionId);
452
+ if (
453
+ !source
454
+ || source.sourceSnapshotHash !== input.sourceSnapshotHash
455
+ || input.candidateIds.some((candidateId) => !source.candidateIds.has(candidateId))
456
+ ) {
457
+ throw new Error('Exact recovery must use one source and candidate set from the latest discovery response.');
458
+ }
459
+ return validateExactRecoveryResponse(
460
+ input,
461
+ exactRecoveryResponseSchema.parse(await applyControlRequest('POST', '/api/jobscout/apply/executions/recover', {
462
+ mode: 'recover_exact_members',
463
+ ...input,
464
+ }, idempotencyKey)),
465
+ );
466
+ }, 'Failed to recover exact apply members')
446
467
  );
447
468
 
448
469
  server.tool(
package/mcp/server.js CHANGED
@@ -7,6 +7,7 @@ const { z } = require('zod');
7
7
  const { apiRequest, createTracklyAccessError, hasAuth, maintenanceOutput } = require('../lib/client');
8
8
  const { version: PACKAGE_VERSION } = require('../package.json');
9
9
  const { registerApplyTools } = require('./apply-tools');
10
+ const { configureMcpAnalytics, shutdownMcpAnalytics } = require('./analytics');
10
11
 
11
12
  const MCP_USER_AGENT = `trackly-mcp/${PACKAGE_VERSION}`;
12
13
  const MCP_MAINTENANCE_ERROR_CODE = -32002;
@@ -15,6 +16,8 @@ const MCP_AUTH_ERROR_CODE = -32004;
15
16
  const AUTH_HINT =
16
17
  'Existing members: run `trackly login` or set TRACKLY_API_KEY. ' +
17
18
  'New members need a private invite during the limited rollout; request access at https://usetrackly.app/early-access.';
19
+ const MCP_ANALYTICS_CONTEXT_DESCRIPTION =
20
+ 'Briefly explain why this tool helps the current job-search goal. Never include resume text, profile answers, demographic or work-authorization answers, application notes, credentials, or secrets.';
18
21
 
19
22
  // Mirrors `granola-followup-app/src/services/region-classifier.ts:8` REGION_TAGS.
20
23
  // Keep in sync when the backend enum changes.
@@ -48,6 +51,22 @@ const JOB_MODALITIES = ['full_time', 'internship', 'all'];
48
51
  // workArrangements query contract and job_postings constraint.
49
52
  const WORK_ARRANGEMENTS = ['remote', 'hybrid', 'in_person', 'unspecified'];
50
53
 
54
+ const MCP_MISSING_CAPABILITY_VALUES = [
55
+ 'search', 'inspect', 'compare', 'rank', 'recommend', 'track', 'update', 'apply',
56
+ 'submit', 'export', 'notify', 'schedule', 'integrate', 'authenticate', 'debug', 'other',
57
+ ];
58
+ const MCP_MISSING_ACTION_VALUES = [
59
+ 'find', 'read', 'compare', 'rank', 'create', 'update', 'delete', 'submit',
60
+ 'export', 'notify', 'schedule', 'connect', 'debug', 'other',
61
+ ];
62
+ const MCP_MISSING_RESOURCE_VALUES = [
63
+ 'job', 'company', 'application', 'contact', 'profile', 'resume', 'preference',
64
+ 'analytics', 'account', 'tool', 'other',
65
+ ];
66
+ const MCP_MISSING_DESTINATION_VALUES = [
67
+ 'trackly', 'external_site', 'file', 'email', 'calendar', 'crm', 'browser', 'other',
68
+ ];
69
+
51
70
  // `sort` enum matches backend handler at `jobscout.ts:3053` — NOT the pre-fix
52
71
  // `newest|oldest|company` (backend rejects oldest/company with HTTP 400).
53
72
  const SORT_VALUES = ['newest', 'match'];
@@ -169,7 +188,7 @@ function throwMcpResourceError(error) {
169
188
  }
170
189
 
171
190
  function wrapTool(handler, fallbackMessage) {
172
- return async (params) => {
191
+ return async (params, extra) => {
173
192
  try {
174
193
  if (!hasAuth()) {
175
194
  return createAuthErrorResult();
@@ -180,6 +199,11 @@ function wrapTool(handler, fallbackMessage) {
180
199
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
181
200
  };
182
201
  } catch (error) {
202
+ if (extra && typeof extra === 'object') {
203
+ extra.__mcp_analytics_error = error instanceof Error
204
+ ? error
205
+ : new Error(error?.message || fallbackMessage, { cause: error });
206
+ }
183
207
  return createErrorResult(
184
208
  error,
185
209
  fallbackMessage,
@@ -485,28 +509,119 @@ function createServer() {
485
509
  mcpUserAgent: MCP_USER_AGENT,
486
510
  throwMcpResourceError,
487
511
  });
512
+
513
+ server.tool(
514
+ 'get_more_tools',
515
+ 'Report a capability Trackly does not currently provide. Use structured categories; context is optional and is classified but never stored as raw text.',
516
+ {
517
+ requestedCapability: z.enum(MCP_MISSING_CAPABILITY_VALUES).optional(),
518
+ requestedAction: z.enum(MCP_MISSING_ACTION_VALUES).optional(),
519
+ requestedResource: z.enum(MCP_MISSING_RESOURCE_VALUES).optional(),
520
+ requestedDestination: z.enum(MCP_MISSING_DESTINATION_VALUES).optional(),
521
+ context: z.string().optional().describe(MCP_ANALYTICS_CONTEXT_DESCRIPTION),
522
+ },
523
+ async () => ({
524
+ content: [{
525
+ type: 'text',
526
+ text: JSON.stringify({ accepted: true, delivery: 'best_effort' }),
527
+ }],
528
+ }),
529
+ );
530
+
488
531
  return server;
489
532
  }
490
533
 
491
- async function startMcpServer() {
534
+ // Keep `createServer` a deterministic catalog factory for embedders and
535
+ // contract verification. Analytics belongs at the executable startup boundary,
536
+ // after every source tool has been registered.
537
+ function configureServerAnalytics(server, options = {}) {
538
+ if (typeof options.configureAnalytics === 'function') {
539
+ return options.configureAnalytics(server, options.analytics);
540
+ }
541
+ return configureMcpAnalytics(server, options.analytics);
542
+ }
543
+
544
+ async function startMcpServer(options = {}) {
492
545
  const server = createServer();
493
- const transport = new StdioServerTransport();
494
- await server.connect(transport);
546
+ configureServerAnalytics(server, options);
547
+ const transport = options.transport || new StdioServerTransport();
548
+ const shutdownAnalytics = options.shutdownAnalytics || shutdownMcpAnalytics;
549
+ const previousOnClose = server.server.onclose;
550
+ const beginAnalyticsShutdown = () => {
551
+ try {
552
+ Promise.resolve(shutdownAnalytics(server)).catch(() => {});
553
+ } catch {
554
+ // Analytics teardown must not affect transport teardown.
555
+ }
556
+ };
557
+ server.server.onclose = () => {
558
+ try {
559
+ previousOnClose?.();
560
+ } catch {
561
+ // Preserve fail-open behavior if an instrumentation callback misbehaves.
562
+ }
563
+ beginAnalyticsShutdown();
564
+ };
565
+
566
+ try {
567
+ await server.connect(transport);
568
+ } catch (error) {
569
+ try {
570
+ await shutdownAnalytics(server);
571
+ } catch {
572
+ // Preserve the original transport failure.
573
+ }
574
+ throw error;
575
+ }
495
576
  return server;
496
577
  }
497
578
 
579
+ function installMcpSignalHandlers(server, options = {}) {
580
+ const signalTarget = options.signalTarget || process;
581
+ const exit = options.exit || ((code) => process.exit(code));
582
+ const shutdownAnalytics = options.shutdownAnalytics || shutdownMcpAnalytics;
583
+ let terminating = false;
584
+ const handlers = new Map();
585
+ const cleanup = () => {
586
+ for (const [signal, handler] of handlers) {
587
+ signalTarget.removeListener(signal, handler);
588
+ }
589
+ handlers.clear();
590
+ };
591
+ for (const [signal, exitCode] of [['SIGINT', 130], ['SIGTERM', 143]]) {
592
+ const handler = () => {
593
+ if (terminating) return;
594
+ terminating = true;
595
+ void Promise.resolve()
596
+ .then(() => shutdownAnalytics(server))
597
+ .catch(() => {})
598
+ .finally(() => {
599
+ cleanup();
600
+ exit(exitCode);
601
+ });
602
+ };
603
+ handlers.set(signal, handler);
604
+ signalTarget.on(signal, handler);
605
+ }
606
+ return cleanup;
607
+ }
608
+
498
609
  if (require.main === module) {
499
- startMcpServer().catch((error) => {
500
- console.error('MCP server error:', error);
501
- process.exit(1);
502
- });
610
+ startMcpServer()
611
+ .then((server) => installMcpSignalHandlers(server))
612
+ .catch((error) => {
613
+ console.error('MCP server error:', error);
614
+ process.exit(1);
615
+ });
503
616
  }
504
617
 
505
618
  module.exports = {
506
619
  AUTH_HINT,
507
620
  createAuthErrorResult,
508
621
  createErrorResult,
622
+ configureServerAnalytics,
509
623
  createServer,
624
+ installMcpSignalHandlers,
510
625
  startMcpServer,
511
626
  throwMcpResourceError,
512
627
  };
@@ -1,18 +1,20 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "trackly-cli",
9
- "version": "0.14.0",
9
+ "version": "0.14.2",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "1.30.0",
13
+ "@posthog/mcp": "0.11.0",
13
14
  "fast-uri": "3.1.5",
14
15
  "hono": "4.13.0",
15
16
  "ip-address": "10.4.0",
17
+ "posthog-node": "5.48.1",
16
18
  "zod": "^3.23.0"
17
19
  },
18
20
  "bin": {
@@ -23,7 +25,7 @@
23
25
  "acorn": "8.18.0"
24
26
  },
25
27
  "engines": {
26
- "node": ">=20"
28
+ "node": "^20.20.0 || >=22.22.0"
27
29
  }
28
30
  },
29
31
  "node_modules/@babel/helper-string-parser": {
@@ -128,6 +130,37 @@
128
130
  }
129
131
  }
130
132
  },
133
+ "node_modules/@posthog/core": {
134
+ "version": "1.46.9",
135
+ "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.46.9.tgz",
136
+ "integrity": "sha512-EXO6y5ih+jBkTCpUCuYgQmajuDZuvy6vMvflkub6pLQyi0GPlCPWVSvZZkOeQw9e2MxoD5GteeGCt9R8+UJ/yQ==",
137
+ "license": "MIT",
138
+ "dependencies": {
139
+ "@posthog/types": "^1.402.2"
140
+ }
141
+ },
142
+ "node_modules/@posthog/mcp": {
143
+ "version": "0.11.0",
144
+ "resolved": "https://registry.npmjs.org/@posthog/mcp/-/mcp-0.11.0.tgz",
145
+ "integrity": "sha512-wxvIvx2/MuXqDH9Zy99hqop6Hyhymn8aoh7e7wYOH3AUdeohZhom2W8E2UDgBO63eR/vm8QGH/fidyA6H+KNyA==",
146
+ "license": "MIT",
147
+ "dependencies": {
148
+ "@posthog/core": "^1.46.9"
149
+ },
150
+ "engines": {
151
+ "node": "^20.20.0 || >=22.22.0"
152
+ },
153
+ "peerDependencies": {
154
+ "@modelcontextprotocol/sdk": ">=1.26.0",
155
+ "posthog-node": "^5.0.0"
156
+ }
157
+ },
158
+ "node_modules/@posthog/types": {
159
+ "version": "1.402.2",
160
+ "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.402.2.tgz",
161
+ "integrity": "sha512-ZZTiS4dLwF4/D0YTzS3gGSLFnhuNOY5yu4d9VGS9trGe5GW6FjIXo20p18K5BPW2RZSYSld8sdnSAY3AUXleUQ==",
162
+ "license": "MIT"
163
+ },
131
164
  "node_modules/accepts": {
132
165
  "version": "2.0.0",
133
166
  "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -909,6 +942,26 @@
909
942
  "node": ">=16.20.0"
910
943
  }
911
944
  },
945
+ "node_modules/posthog-node": {
946
+ "version": "5.48.1",
947
+ "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.48.1.tgz",
948
+ "integrity": "sha512-BxLX2SqGEQhPqCPTalpyo0RRv1NMbf7UaN7q9d/ED77ksD6XOmE7ko2vIKO8F0zPL1NtKxIi+DYXap9lvR0RaA==",
949
+ "license": "MIT",
950
+ "dependencies": {
951
+ "@posthog/core": "^1.46.9"
952
+ },
953
+ "engines": {
954
+ "node": "^20.20.0 || >=22.22.0"
955
+ },
956
+ "peerDependencies": {
957
+ "rxjs": "^7.0.0"
958
+ },
959
+ "peerDependenciesMeta": {
960
+ "rxjs": {
961
+ "optional": true
962
+ }
963
+ }
964
+ },
912
965
  "node_modules/proxy-addr": {
913
966
  "version": "2.0.7",
914
967
  "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "mcpName": "io.github.trackly-app/trackly",
5
5
  "description": "AI job search CLI + hosted MCP server with OAuth. 170K+ jobs, 3,800+ companies, 40+ ATS. Works with Claude, ChatGPT, Cursor, Windsurf, Codex via hosted streamable-http or local stdio.",
6
6
  "bin": {
@@ -63,13 +63,15 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@modelcontextprotocol/sdk": "1.30.0",
66
+ "@posthog/mcp": "0.11.0",
66
67
  "fast-uri": "3.1.5",
67
68
  "hono": "4.13.0",
68
69
  "ip-address": "10.4.0",
70
+ "posthog-node": "5.48.1",
69
71
  "zod": "^3.23.0"
70
72
  },
71
73
  "engines": {
72
- "node": ">=20"
74
+ "node": "^20.20.0 || >=22.22.0"
73
75
  },
74
76
  "overrides": {
75
77
  "@hono/node-server": "2.0.12",
package/server.json CHANGED
@@ -17,12 +17,12 @@
17
17
  ]
18
18
  }
19
19
  ],
20
- "version": "0.14.0",
20
+ "version": "0.14.2",
21
21
  "packages": [
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "trackly-cli",
25
- "version": "0.14.0",
25
+ "version": "0.14.2",
26
26
  "runtimeHint": "npx",
27
27
  "runtimeArguments": [
28
28
  {