trackly-cli 0.2.8 → 0.4.0
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 +5 -3
- package/bin/trackly +151 -2
- package/docs/trackly-tools.md +2 -1
- package/lib/client.js +55 -5
- package/mcp/server.js +18 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ trackly jobs --function product
|
|
|
35
35
|
|
|
36
36
|
## At a Glance
|
|
37
37
|
|
|
38
|
-
1,900+ companies | 128K+ jobs | 40+ ATS types | CLI + MCP |
|
|
38
|
+
1,900+ companies | 128K+ jobs | 40+ ATS types | CLI + MCP | 11 MCP tools
|
|
39
39
|
|
|
40
40
|
## CLI Commands
|
|
41
41
|
|
|
@@ -63,6 +63,7 @@ trackly referral start 1234 # Start a referral campaign
|
|
|
63
63
|
trackly referral status 1234 # Check referral campaign status
|
|
64
64
|
trackly company-brief 243 # Get company brief (--refresh to regenerate)
|
|
65
65
|
trackly company-workspace 243 # Full company workspace view
|
|
66
|
+
trackly request-company "eBay" # Request a company be added (--url, --notes optional)
|
|
66
67
|
trackly api-key create # Generate API key
|
|
67
68
|
trackly api-key list # List API keys
|
|
68
69
|
trackly config # Show current CLI config
|
|
@@ -150,6 +151,7 @@ Then use natural language in any of these clients:
|
|
|
150
151
|
| trackly_get_job_brief | Get network brief for a job (company signal, top contact, actions) |
|
|
151
152
|
| trackly_contacts_at_company | Search contacts at a specific company |
|
|
152
153
|
| trackly_get_company_workspace | Full company workspace (jobs, contacts, hiring managers, campaigns) |
|
|
154
|
+
| trackly_request_company | Request a company be added to Trackly (rate-limited to 5 pending) |
|
|
153
155
|
|
|
154
156
|
## Authentication
|
|
155
157
|
|
|
@@ -209,7 +211,7 @@ trackly config --base-url http://127.0.0.1:3000 # Point at a different backend
|
|
|
209
211
|
| Job search + filters | Yes | Yes | Yes |
|
|
210
212
|
| Apply/save/dismiss | Yes | Yes | Yes |
|
|
211
213
|
| AI-powered search | Yes (trackly ask) | Yes | Yes |
|
|
212
|
-
| MCP integration | Yes (
|
|
214
|
+
| MCP integration | Yes (11 tools) | -- | -- |
|
|
213
215
|
| Browser required | No | Yes | No |
|
|
214
216
|
| Best for | Terminal + AI agents | Visual browsing | Custom integrations |
|
|
215
217
|
|
|
@@ -223,7 +225,7 @@ Install trackly-cli (`npm install -g trackly-cli`), authenticate with `trackly l
|
|
|
223
225
|
|
|
224
226
|
**What MCP servers exist for job searching?**
|
|
225
227
|
|
|
226
|
-
trackly-cli includes a built-in MCP server with
|
|
228
|
+
trackly-cli includes a built-in MCP server with 11 tools for job search, company lookup, and application tracking. Run `trackly mcp` or add it to Claude Code with `claude mcp add --scope user trackly -- trackly mcp`. It connects to a live database of 128,000+ jobs across 1,900+ companies.
|
|
227
229
|
|
|
228
230
|
**How do I use Claude Code for job hunting?**
|
|
229
231
|
|
package/bin/trackly
CHANGED
|
@@ -23,17 +23,56 @@ const BOOLEAN_FLAGS = new Set(['help', 'h', 'json', 'version', 'v', 'clear-api-k
|
|
|
23
23
|
const VALUE_FLAGS = new Set([
|
|
24
24
|
'api-key',
|
|
25
25
|
'base-url',
|
|
26
|
+
'company',
|
|
26
27
|
'function',
|
|
27
28
|
'job-type',
|
|
28
29
|
'keywords',
|
|
29
30
|
'limit',
|
|
30
31
|
'name',
|
|
32
|
+
'notes',
|
|
31
33
|
'offset',
|
|
32
34
|
'region',
|
|
33
35
|
'search',
|
|
34
36
|
'sort',
|
|
35
37
|
'status',
|
|
38
|
+
'url',
|
|
36
39
|
]);
|
|
40
|
+
|
|
41
|
+
// Flags accepted on ANY command. `api-key`/`base-url` are global overrides consumed
|
|
42
|
+
// in main() before dispatch; help/version are handled by early-returns.
|
|
43
|
+
const UNIVERSAL_FLAGS = new Set(['json', 'help', 'h', 'version', 'v', 'api-key', 'base-url']);
|
|
44
|
+
|
|
45
|
+
// Per-command allow-lists — each set is the flags that command's handler actually
|
|
46
|
+
// reads. Keyed at the command level (the union across a command's subcommands) so a
|
|
47
|
+
// valid filtered detail call like `jobs 123 --region us` is never falsely rejected,
|
|
48
|
+
// while typos (`--regoin`) and wrong-command flags (`jobs --url`, `jobs --search`)
|
|
49
|
+
// still are. `jobs` includes the deprecated `location`/`modality` so they reach the
|
|
50
|
+
// richer DEPRECATED_JOB_FLAGS migration message in cmdJobs rather than a generic error.
|
|
51
|
+
// Commands NOT listed here (e.g. `mcp`, or an unknown command) skip flag validation —
|
|
52
|
+
// the dispatcher reports unknown commands itself.
|
|
53
|
+
const COMMAND_FLAGS = {
|
|
54
|
+
login: [],
|
|
55
|
+
logout: [],
|
|
56
|
+
whoami: [],
|
|
57
|
+
jobs: ['function', 'company', 'region', 'job-type', 'remote', 'status', 'sort', 'limit', 'offset', 'keywords', 'location', 'modality'],
|
|
58
|
+
job: [],
|
|
59
|
+
companies: ['limit', 'offset'],
|
|
60
|
+
search: [],
|
|
61
|
+
stats: [],
|
|
62
|
+
status: [],
|
|
63
|
+
apply: [],
|
|
64
|
+
save: [],
|
|
65
|
+
dismiss: [],
|
|
66
|
+
ask: [],
|
|
67
|
+
contacts: ['search', 'limit', 'offset'],
|
|
68
|
+
referral: [],
|
|
69
|
+
brief: [],
|
|
70
|
+
'company-brief': ['refresh'],
|
|
71
|
+
'company-workspace': [],
|
|
72
|
+
'request-company': ['url', 'notes'],
|
|
73
|
+
'api-key': ['name'],
|
|
74
|
+
config: ['api-key', 'base-url', 'clear-api-key', 'clear-base-url'],
|
|
75
|
+
};
|
|
37
76
|
const AUTH_HELP_TEXT = `Not authenticated.
|
|
38
77
|
Option 1: trackly login (Google OAuth)
|
|
39
78
|
Option 2: trackly config --api-key trk_xxx (get key at usetrackly.app)
|
|
@@ -104,6 +143,59 @@ function parseArgs(args) {
|
|
|
104
143
|
return result;
|
|
105
144
|
}
|
|
106
145
|
|
|
146
|
+
// Tiny Levenshtein (no dep) for "did you mean" flag suggestions.
|
|
147
|
+
function levenshtein(a, b) {
|
|
148
|
+
const m = a.length;
|
|
149
|
+
const n = b.length;
|
|
150
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
151
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
152
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
153
|
+
for (let i = 1; i <= m; i++) {
|
|
154
|
+
for (let j = 1; j <= n; j++) {
|
|
155
|
+
dp[i][j] = Math.min(
|
|
156
|
+
dp[i - 1][j] + 1,
|
|
157
|
+
dp[i][j - 1] + 1,
|
|
158
|
+
dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return dp[m][n];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function nearestFlag(key, allowed) {
|
|
166
|
+
let best = null;
|
|
167
|
+
let bestDistance = Infinity;
|
|
168
|
+
for (const flag of allowed) {
|
|
169
|
+
const d = levenshtein(key, flag);
|
|
170
|
+
if (d < bestDistance) {
|
|
171
|
+
bestDistance = d;
|
|
172
|
+
best = flag;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return bestDistance <= 2 ? best : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Reject flags that the resolved command doesn't accept. Catches typos
|
|
179
|
+
// (`--regoin`) and wrong-command flags (`jobs --url`) that parseArgs would
|
|
180
|
+
// otherwise capture and the handler would silently ignore — returning, e.g., an
|
|
181
|
+
// unfiltered job feed that looks filtered. Unknown commands (and `mcp`) skip this;
|
|
182
|
+
// the dispatcher reports unknown commands itself.
|
|
183
|
+
function validateFlags(args) {
|
|
184
|
+
const cmd = args.command;
|
|
185
|
+
if (!cmd || !Object.prototype.hasOwnProperty.call(COMMAND_FLAGS, cmd)) return;
|
|
186
|
+
const allowed = new Set([...UNIVERSAL_FLAGS, ...COMMAND_FLAGS[cmd]]);
|
|
187
|
+
for (const key of Object.keys(args)) {
|
|
188
|
+
if (key === '_' || key === 'command') continue;
|
|
189
|
+
if (allowed.has(key)) continue;
|
|
190
|
+
// Strip non-printable chars so a crafted `--$'\x1b[…'` flag can't inject
|
|
191
|
+
// terminal escape sequences into the error message.
|
|
192
|
+
const safe = String(key).replace(/[^\x20-\x7e]/g, '');
|
|
193
|
+
const suggestion = nearestFlag(key, allowed);
|
|
194
|
+
const hint = suggestion ? ` Did you mean --${suggestion}?` : '';
|
|
195
|
+
failNow(`Unknown option --${safe} for 'trackly ${cmd}'.${hint} Run \`trackly --help\` for valid options.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
107
199
|
function failNow(message, exitCode = 1) {
|
|
108
200
|
outputError(message);
|
|
109
201
|
process.exit(exitCode);
|
|
@@ -668,6 +760,43 @@ async function cmdCompanyWorkspace(args) {
|
|
|
668
760
|
}
|
|
669
761
|
}
|
|
670
762
|
|
|
763
|
+
async function cmdRequestCompany(args) {
|
|
764
|
+
// Positional arg = company name. Allow multi-word names without forcing the user
|
|
765
|
+
// to quote, e.g. `trackly request-company Built Robotics --url ...` joins the
|
|
766
|
+
// free-floating positionals into "Built Robotics". Matches `trackly ask` UX.
|
|
767
|
+
const name = (args._ || []).join(' ').trim();
|
|
768
|
+
if (!name) {
|
|
769
|
+
failNow('Company name required. Usage: trackly request-company <name> [--url <url>] [--notes <notes>]');
|
|
770
|
+
}
|
|
771
|
+
if (!hasAuth()) failNow(AUTH_HELP_TEXT);
|
|
772
|
+
|
|
773
|
+
const url = typeof args.url === 'string' ? args.url : '';
|
|
774
|
+
const notes = typeof args.notes === 'string' ? args.notes : '';
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
const result = await apiRequest('POST', '/api/jobscout/companies/request', {
|
|
778
|
+
company_name: name,
|
|
779
|
+
company_url: url,
|
|
780
|
+
notes: notes,
|
|
781
|
+
source: 'cli',
|
|
782
|
+
});
|
|
783
|
+
if (isJSON()) return printJson(result);
|
|
784
|
+
const requestId = result.requestId || result.id || '?';
|
|
785
|
+
console.log(color('green', `Requested "${name}" (#${requestId}). Trackly team will review shortly.`));
|
|
786
|
+
} catch (e) {
|
|
787
|
+
// Backend returns 429 (rate limit, max 5 pending) and 409 (duplicate pending request)
|
|
788
|
+
// with a specific `error` message. Surface that message verbatim so the user knows
|
|
789
|
+
// exactly which gate fired without having to enable --json.
|
|
790
|
+
if (e.status === 429) {
|
|
791
|
+
setErrorExit(e.error || 'Maximum 5 pending requests allowed. Please wait for existing requests to be reviewed.');
|
|
792
|
+
} else if (e.status === 409) {
|
|
793
|
+
setErrorExit(e.error || 'You already have a pending request for this company.');
|
|
794
|
+
} else {
|
|
795
|
+
setErrorExit(e.error || e.message || 'Failed to request company');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
671
800
|
async function cmdConfig(args) {
|
|
672
801
|
if (args['api-key'] === null) failNow('API key value required. Usage: trackly config --api-key <key>');
|
|
673
802
|
if (args['base-url'] === null) failNow('Base URL value required. Usage: trackly config --base-url <url>');
|
|
@@ -708,7 +837,11 @@ async function cmdConfig(args) {
|
|
|
708
837
|
}
|
|
709
838
|
|
|
710
839
|
if (args['base-url'] !== undefined) {
|
|
711
|
-
|
|
840
|
+
try {
|
|
841
|
+
config.baseUrl = normalizeBaseUrlValue(args['base-url']);
|
|
842
|
+
} catch (e) {
|
|
843
|
+
failNow('Invalid base URL: ' + e.message);
|
|
844
|
+
}
|
|
712
845
|
updated = true;
|
|
713
846
|
console.log(color('green', `Base URL set to ${config.baseUrl}.`));
|
|
714
847
|
}
|
|
@@ -770,6 +903,7 @@ ${color('bold', 'Commands:')}
|
|
|
770
903
|
${color('cyan', 'brief <id>')} Get network brief for a job
|
|
771
904
|
${color('cyan', 'company-brief <id>')} Get company brief (--refresh to generate)
|
|
772
905
|
${color('cyan', 'company-workspace <id>')} Show company workspace (jobs, contacts, campaigns)
|
|
906
|
+
${color('cyan', 'request-company <name>')} Request a company be added to Trackly (--url, --notes)
|
|
773
907
|
${color('cyan', 'api-key create')} Generate an API key
|
|
774
908
|
${color('cyan', 'api-key list')} List your API keys
|
|
775
909
|
${color('cyan', 'config')} Show or update CLI config (--api-key, --base-url)
|
|
@@ -842,7 +976,11 @@ async function main() {
|
|
|
842
976
|
}
|
|
843
977
|
|
|
844
978
|
if (args.command !== 'config' && args['base-url']) {
|
|
845
|
-
|
|
979
|
+
try {
|
|
980
|
+
process.env.TRACKLY_BASE_URL = normalizeBaseUrlValue(args['base-url']);
|
|
981
|
+
} catch (e) {
|
|
982
|
+
failNow('Invalid base URL: ' + e.message);
|
|
983
|
+
}
|
|
846
984
|
}
|
|
847
985
|
|
|
848
986
|
if (args.version || args.v || args.command === 'version') {
|
|
@@ -855,6 +993,10 @@ async function main() {
|
|
|
855
993
|
process.exit(0);
|
|
856
994
|
}
|
|
857
995
|
|
|
996
|
+
// Reject unknown / wrong-command flags AFTER the help+version early-returns
|
|
997
|
+
// (so `trackly --help --weird` still shows help instead of erroring).
|
|
998
|
+
validateFlags(args);
|
|
999
|
+
|
|
858
1000
|
switch (args.command) {
|
|
859
1001
|
case 'login':
|
|
860
1002
|
await cmdLogin();
|
|
@@ -926,6 +1068,9 @@ async function main() {
|
|
|
926
1068
|
case 'company-workspace':
|
|
927
1069
|
await cmdCompanyWorkspace(args);
|
|
928
1070
|
break;
|
|
1071
|
+
case 'request-company':
|
|
1072
|
+
await cmdRequestCompany(args);
|
|
1073
|
+
break;
|
|
929
1074
|
case 'api-key':
|
|
930
1075
|
await cmdApiKey(args);
|
|
931
1076
|
break;
|
|
@@ -950,9 +1095,13 @@ if (require.main === module) {
|
|
|
950
1095
|
}
|
|
951
1096
|
|
|
952
1097
|
module.exports = {
|
|
1098
|
+
COMMAND_FLAGS,
|
|
1099
|
+
UNIVERSAL_FLAGS,
|
|
953
1100
|
getPublicConfig,
|
|
954
1101
|
hasJobListFilters,
|
|
1102
|
+
levenshtein,
|
|
955
1103
|
main,
|
|
1104
|
+
nearestFlag,
|
|
956
1105
|
normalizeBaseUrlValue,
|
|
957
1106
|
openExternalUrl,
|
|
958
1107
|
parseArgs,
|
package/docs/trackly-tools.md
CHANGED
|
@@ -72,7 +72,7 @@ You can also pass `TRACKLY_API_KEY` as an environment variable for one-off runs.
|
|
|
72
72
|
- `locationFilter`: pass ONE of
|
|
73
73
|
- a single scalar: `us`, `non_us`, `all`, or a region tag (`europe`, `latam`, `middle_east`, `asia`, `africa`, `canada`, `oceania`, `remote`, `unknown`), OR
|
|
74
74
|
- an array of region tags for multi-region (e.g. `["europe", "canada"]`). The array form excludes `us` — combining `us` with other tags in an array causes the backend to silently drop the others. To get "not US" use the scalar `non_us` alone.
|
|
75
|
-
- `jobModality`: `full_time`, `internship`, `all` — employment type, NOT work-location style. For remote
|
|
75
|
+
- `jobModality`: `full_time`, `internship`, `all` — employment type, NOT work-location style. For remote filtering, use the `remote` boolean or `locationFilter: "remote"`. Hybrid and onsite are not exposed as filters.
|
|
76
76
|
- `remote` (boolean): filter to remote jobs only. Maps to `usStates=REMOTE`.
|
|
77
77
|
- `status`: your application pipeline state. Values: `new`, `applying`, `applied_confirmed`, `check_later`, `not_interested`, `all`.
|
|
78
78
|
- `sort`: `newest` (default) or `match` (highest match score first; requires a resume on file). Backend rejects the deprecated values `oldest` and `company` with HTTP 400.
|
|
@@ -86,6 +86,7 @@ You can also pass `TRACKLY_API_KEY` as an environment variable for one-off runs.
|
|
|
86
86
|
- **trackly_get_job_brief** — Get network brief for a job (company signal, top contact, actions)
|
|
87
87
|
- **trackly_contacts_at_company** — Search contacts at a specific company
|
|
88
88
|
- **trackly_get_company_workspace** — Get full company workspace (jobs, contacts, hiring managers, campaigns)
|
|
89
|
+
- **trackly_request_company** — Request that a company be added to Trackly's tracked companies. Rate-limited to 5 pending requests per user. Parameters: `companyName` (required), `companyUrl` (optional), `notes` (optional).
|
|
89
90
|
|
|
90
91
|
### Example Prompts
|
|
91
92
|
|
package/lib/client.js
CHANGED
|
@@ -29,17 +29,28 @@ function getConfigPaths() {
|
|
|
29
29
|
|
|
30
30
|
function loadConfig() {
|
|
31
31
|
const { file } = getConfigPaths();
|
|
32
|
+
// Read the file directly rather than gating on fs.existsSync(): existsSync can
|
|
33
|
+
// mask a permission error (and races the read). Distinguish "no config yet"
|
|
34
|
+
// (ENOENT → {}) from "config exists but unreadable" (EACCES/EPERM → a clear,
|
|
35
|
+
// actionable error) from "config is corrupt JSON".
|
|
36
|
+
let raw;
|
|
32
37
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error && error.code === 'ENOENT') return {};
|
|
41
|
+
if (error && (error.code === 'EACCES' || error.code === 'EPERM')) {
|
|
42
|
+
throw new TracklyConfigError(`Trackly config is not readable (${file}). Check file permissions.`);
|
|
35
43
|
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(raw);
|
|
36
48
|
} catch (error) {
|
|
37
49
|
if (error instanceof SyntaxError) {
|
|
38
50
|
throw new TracklyConfigError(`Trackly config is invalid JSON. Fix or delete ${file}.`);
|
|
39
51
|
}
|
|
40
52
|
throw error;
|
|
41
53
|
}
|
|
42
|
-
return {};
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
function ensureConfigDir() {
|
|
@@ -190,7 +201,28 @@ function getRequestHeaders(skipAuth = false, userAgent = null) {
|
|
|
190
201
|
return headers;
|
|
191
202
|
}
|
|
192
203
|
|
|
193
|
-
|
|
204
|
+
// Single-flight wrapper around the refresh logic. Coalesces concurrent refreshes
|
|
205
|
+
// WITHIN THIS PROCESS so simultaneous 401s (e.g. parallel MCP tool calls in the
|
|
206
|
+
// long-lived stdio server) don't each fire `POST /api/auth/refresh`. The backend
|
|
207
|
+
// rotates the refresh token on use, so a second in-flight refresh would send a
|
|
208
|
+
// now-stale token → 4xx → clearOAuthTokens() → the user is silently logged out
|
|
209
|
+
// mid-session even though the first refresh succeeded. All concurrent callers
|
|
210
|
+
// await the same promise, then each retries reading the freshly-saved token.
|
|
211
|
+
//
|
|
212
|
+
// Scope: this does NOT coordinate across multiple `trackly` processes sharing
|
|
213
|
+
// ~/.trackly/config.json. That is acceptable — saveConfig() is atomic (renameSync)
|
|
214
|
+
// and concurrent multi-process refresh is rare; a cross-process file lock is a
|
|
215
|
+
// possible future addition, not in scope here.
|
|
216
|
+
let _refreshInFlight = null;
|
|
217
|
+
function refreshAccessToken() {
|
|
218
|
+
if (_refreshInFlight) return _refreshInFlight;
|
|
219
|
+
// _doRefresh resolves to a token-or-null and never rejects, so the cached
|
|
220
|
+
// promise can be safely shared and the finally() latch always clears.
|
|
221
|
+
_refreshInFlight = _doRefresh().finally(() => { _refreshInFlight = null; });
|
|
222
|
+
return _refreshInFlight;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function _doRefresh() {
|
|
194
226
|
const rt = getRefreshToken();
|
|
195
227
|
if (!rt) return null;
|
|
196
228
|
|
|
@@ -291,7 +323,25 @@ function apiRequest(method, endpoint, data = null, skipAuth = false, _isRetry =
|
|
|
291
323
|
if (aborted) return; // req.destroy already rejected via 'error' handler
|
|
292
324
|
|
|
293
325
|
let json;
|
|
294
|
-
try {
|
|
326
|
+
try {
|
|
327
|
+
json = body ? JSON.parse(body) : {};
|
|
328
|
+
} catch {
|
|
329
|
+
// Non-JSON body (HTML error page, plaintext, proxy notice, …). Keep the
|
|
330
|
+
// raw body for debugging but always attach a usable `message` so the
|
|
331
|
+
// downstream `e.error || e.message || fallback` chain has real context.
|
|
332
|
+
json = { raw: body, message: 'Received a non-JSON response from the Trackly API.' };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// The Trackly API is a same-origin JSON API that should never redirect on
|
|
336
|
+
// the paths the CLI/MCP call. node:http does not auto-follow 3xx, so a
|
|
337
|
+
// redirect would otherwise resolve as "success" with the redirect headers
|
|
338
|
+
// as bogus data. Treat it as an error with a clear, actionable message.
|
|
339
|
+
if (res.statusCode >= 300 && res.statusCode < 400) {
|
|
340
|
+
return reject({
|
|
341
|
+
status: res.statusCode,
|
|
342
|
+
message: `Unexpected redirect (HTTP ${res.statusCode}) from the Trackly API.`,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
295
345
|
|
|
296
346
|
if (res.statusCode === 401 && !skipAuth && authMethod === 'token' && !_isRetry) {
|
|
297
347
|
// Try token refresh (once only)
|
package/mcp/server.js
CHANGED
|
@@ -297,6 +297,24 @@ function createServer() {
|
|
|
297
297
|
}, 'Failed to fetch company workspace')
|
|
298
298
|
);
|
|
299
299
|
|
|
300
|
+
server.tool(
|
|
301
|
+
'trackly_request_company',
|
|
302
|
+
'Request that a company be added to Trackly\'s tracked companies. Use when the user asks about a company that isn\'t in trackly_search_companies / trackly_list_companies results. Rate-limited to 5 pending requests per user.',
|
|
303
|
+
{
|
|
304
|
+
companyName: z.string().min(1).max(200).describe('Company name (e.g. "eBay")'),
|
|
305
|
+
companyUrl: z.string().max(500).optional().describe('Optional careers page or homepage URL (e.g. "https://careers.ebay.com")'),
|
|
306
|
+
notes: z.string().max(1000).optional().describe('Optional context (e.g. "MBA hiring page", "specific role I want tracked")'),
|
|
307
|
+
},
|
|
308
|
+
wrapTool(async ({ companyName, companyUrl, notes }) => {
|
|
309
|
+
return apiRequest('POST', '/api/jobscout/companies/request', {
|
|
310
|
+
company_name: companyName,
|
|
311
|
+
company_url: companyUrl || '',
|
|
312
|
+
notes: notes || '',
|
|
313
|
+
source: 'mcp',
|
|
314
|
+
}, false, false, MCP_USER_AGENT);
|
|
315
|
+
}, 'Failed to request company')
|
|
316
|
+
);
|
|
317
|
+
|
|
300
318
|
return server;
|
|
301
319
|
}
|
|
302
320
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackly-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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.
|
|
20
|
+
"version": "0.4.0",
|
|
21
21
|
"packages": [
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "trackly-cli",
|
|
25
|
-
"version": "0.
|
|
25
|
+
"version": "0.4.0",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"runtimeArguments": [
|
|
28
28
|
{
|