refresh-cv 0.1.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 +32 -0
- package/bin/refresh-cv.js +373 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# refresh-cv
|
|
2
|
+
|
|
3
|
+
Install the refresh.cv MCP server for supported agent clients.
|
|
4
|
+
|
|
5
|
+
## Codex
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx -y refresh-cv@latest --client codex
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This configures Codex with:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
https://mcp.refresh.cv/mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then it starts the Codex OAuth login flow.
|
|
18
|
+
|
|
19
|
+
For dev:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx -y refresh-cv@latest --client codex --dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Options
|
|
26
|
+
|
|
27
|
+
- `--client codex`: configure Codex.
|
|
28
|
+
- `--dev`: use `https://mcp.dev.refresh.cv/mcp`.
|
|
29
|
+
- `--url <url>`: override the MCP endpoint.
|
|
30
|
+
- `--name <name>`: override the Codex MCP server name. Defaults to `refresh_cv`.
|
|
31
|
+
- `--force`: replace an existing Codex MCP server with the same name.
|
|
32
|
+
- `--skip-login`: add the MCP server but skip OAuth login.
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const PACKAGE_VERSION = '0.1.0';
|
|
8
|
+
const PRODUCTION_MCP_URL = 'https://mcp.refresh.cv/mcp';
|
|
9
|
+
const DEV_MCP_URL = 'https://mcp.dev.refresh.cv/mcp';
|
|
10
|
+
const DEFAULT_SERVER_NAME = 'refresh_cv';
|
|
11
|
+
const SIMPLE_TOML_KEY = /^[A-Za-z0-9_-]+$/;
|
|
12
|
+
|
|
13
|
+
const HELP = `refresh.cv MCP installer
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
npx -y refresh-cv@latest --client codex [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--client <client> Agent client to configure. Currently supports: codex
|
|
20
|
+
--dev Use the dev MCP endpoint (${DEV_MCP_URL})
|
|
21
|
+
--url <url> Override the MCP endpoint URL
|
|
22
|
+
--name <name> Codex MCP server name (default: ${DEFAULT_SERVER_NAME})
|
|
23
|
+
--force Replace an existing Codex MCP server with the same name
|
|
24
|
+
--skip-login Add the server but do not run "codex mcp login"
|
|
25
|
+
--version Show this installer version
|
|
26
|
+
--help Show this help message
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
npx -y refresh-cv@latest --client codex
|
|
30
|
+
npx -y refresh-cv@latest --client codex --dev
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
main().catch((error) => {
|
|
34
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const options = parseArgs(process.argv.slice(2));
|
|
39
|
+
|
|
40
|
+
if (options.help) {
|
|
41
|
+
console.log(HELP);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (options.version) {
|
|
46
|
+
console.log(PACKAGE_VERSION);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!options.client) {
|
|
51
|
+
fail('Missing --client. Use: npx -y refresh-cv@latest --client codex');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (options.client !== 'codex') {
|
|
55
|
+
fail(
|
|
56
|
+
`Unsupported client "${options.client}". This installer currently supports: codex.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const serverName = options.name ?? DEFAULT_SERVER_NAME;
|
|
61
|
+
const mcpUrl =
|
|
62
|
+
options.url ?? (options.dev ? DEV_MCP_URL : PRODUCTION_MCP_URL);
|
|
63
|
+
|
|
64
|
+
assertValidServerName(serverName);
|
|
65
|
+
assertValidMcpUrl(mcpUrl);
|
|
66
|
+
assertCommandAvailable('codex');
|
|
67
|
+
|
|
68
|
+
const existing = getCodexMcp(serverName);
|
|
69
|
+
if (existing.exists) {
|
|
70
|
+
const existingUrl = existing.url;
|
|
71
|
+
if (existingUrl === mcpUrl) {
|
|
72
|
+
console.log(
|
|
73
|
+
`refresh.cv MCP is already configured for Codex as "${serverName}".`,
|
|
74
|
+
);
|
|
75
|
+
} else if (options.force && options.skipLogin) {
|
|
76
|
+
writeCodexMcpConfig(serverName, mcpUrl);
|
|
77
|
+
} else if (options.force) {
|
|
78
|
+
runCodex(['mcp', 'remove', serverName], {
|
|
79
|
+
errorMessage: `Failed to remove existing Codex MCP server "${serverName}".`,
|
|
80
|
+
});
|
|
81
|
+
await addCodexMcp(serverName, mcpUrl, options);
|
|
82
|
+
} else {
|
|
83
|
+
fail(
|
|
84
|
+
`Codex MCP server "${serverName}" already exists with URL ${existingUrl || '(unknown)'}.\n` +
|
|
85
|
+
`Use --force to replace it, or choose a different --name.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
} else if (options.skipLogin) {
|
|
89
|
+
writeCodexMcpConfig(serverName, mcpUrl);
|
|
90
|
+
} else {
|
|
91
|
+
await addCodexMcp(serverName, mcpUrl, options);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (options.skipLogin) {
|
|
95
|
+
console.log(
|
|
96
|
+
`Skipped login. Run this when ready: codex mcp login ${serverName}`,
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (options.loginStartedDuringAdd) {
|
|
102
|
+
console.log(
|
|
103
|
+
`Codex started the OAuth login flow for "${serverName}". Finish the browser approval if prompted.`,
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`Opening Codex OAuth login for "${serverName}"...`);
|
|
109
|
+
await runCodexStreaming(['mcp', 'login', serverName], {
|
|
110
|
+
errorMessage: `Codex login did not complete. Retry with: codex mcp login ${serverName}`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseArgs(args) {
|
|
115
|
+
const options = {
|
|
116
|
+
client: undefined,
|
|
117
|
+
dev: false,
|
|
118
|
+
force: false,
|
|
119
|
+
help: false,
|
|
120
|
+
loginStartedDuringAdd: false,
|
|
121
|
+
name: undefined,
|
|
122
|
+
skipLogin: false,
|
|
123
|
+
url: undefined,
|
|
124
|
+
version: false,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
128
|
+
const arg = args[index];
|
|
129
|
+
const [flag, inlineValue] = arg.includes('=') ? arg.split(/=(.*)/s, 2) : [];
|
|
130
|
+
|
|
131
|
+
switch (arg) {
|
|
132
|
+
case '--client':
|
|
133
|
+
options.client = readValue(args, ++index, '--client').toLowerCase();
|
|
134
|
+
break;
|
|
135
|
+
case '--dev':
|
|
136
|
+
options.dev = true;
|
|
137
|
+
break;
|
|
138
|
+
case '--force':
|
|
139
|
+
options.force = true;
|
|
140
|
+
break;
|
|
141
|
+
case '--help':
|
|
142
|
+
case '-h':
|
|
143
|
+
options.help = true;
|
|
144
|
+
break;
|
|
145
|
+
case '--name':
|
|
146
|
+
options.name = readValue(args, ++index, '--name');
|
|
147
|
+
break;
|
|
148
|
+
case '--skip-login':
|
|
149
|
+
case '--no-login':
|
|
150
|
+
options.skipLogin = true;
|
|
151
|
+
break;
|
|
152
|
+
case '--url':
|
|
153
|
+
options.url = readValue(args, ++index, '--url');
|
|
154
|
+
break;
|
|
155
|
+
case '--version':
|
|
156
|
+
case '-v':
|
|
157
|
+
options.version = true;
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
if (flag === '--client') {
|
|
161
|
+
options.client = readInlineValue(
|
|
162
|
+
inlineValue,
|
|
163
|
+
'--client',
|
|
164
|
+
).toLowerCase();
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
if (flag === '--name') {
|
|
168
|
+
options.name = readInlineValue(inlineValue, '--name');
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
if (flag === '--url') {
|
|
172
|
+
options.url = readInlineValue(inlineValue, '--url');
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
fail(`Unknown option "${arg}".\n\n${HELP}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return options;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readValue(args, index, flag) {
|
|
183
|
+
const value = args[index];
|
|
184
|
+
if (!value || value.startsWith('--')) {
|
|
185
|
+
fail(`Missing value for ${flag}.`);
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function readInlineValue(value, flag) {
|
|
191
|
+
if (!value) {
|
|
192
|
+
fail(`Missing value for ${flag}.`);
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function assertValidServerName(value) {
|
|
198
|
+
if (!SIMPLE_TOML_KEY.test(value)) {
|
|
199
|
+
fail(
|
|
200
|
+
'Codex MCP server name may only contain letters, numbers, underscores, and hyphens.',
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function assertValidMcpUrl(value) {
|
|
206
|
+
let parsed;
|
|
207
|
+
try {
|
|
208
|
+
parsed = new URL(value);
|
|
209
|
+
} catch {
|
|
210
|
+
fail(`Invalid MCP URL: ${value}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (parsed.protocol !== 'https:' && parsed.hostname !== 'localhost') {
|
|
214
|
+
fail('MCP URL must use https, except localhost development URLs.');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (parsed.pathname !== '/mcp') {
|
|
218
|
+
fail('MCP URL must point to the /mcp endpoint.');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function assertCommandAvailable(command) {
|
|
223
|
+
const result = spawnSync(command, ['--version'], {
|
|
224
|
+
encoding: 'utf8',
|
|
225
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
226
|
+
});
|
|
227
|
+
if (result.error?.code === 'ENOENT') {
|
|
228
|
+
fail(
|
|
229
|
+
`Could not find "${command}" in PATH. Install Codex first, then rerun this command.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (result.status !== 0) {
|
|
233
|
+
fail(
|
|
234
|
+
`Could not run "${command} --version". ${formatCommandOutput(result)}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getCodexMcp(serverName) {
|
|
240
|
+
const result = spawnSync('codex', ['mcp', 'get', serverName, '--json'], {
|
|
241
|
+
encoding: 'utf8',
|
|
242
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (result.status !== 0) {
|
|
246
|
+
return { exists: false, url: undefined };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const parsed = JSON.parse(result.stdout);
|
|
251
|
+
return {
|
|
252
|
+
exists: true,
|
|
253
|
+
url:
|
|
254
|
+
parsed?.transport?.type === 'streamable_http'
|
|
255
|
+
? parsed.transport.url
|
|
256
|
+
: undefined,
|
|
257
|
+
};
|
|
258
|
+
} catch {
|
|
259
|
+
return { exists: true, url: undefined };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function addCodexMcp(serverName, mcpUrl, options) {
|
|
264
|
+
console.log(`Adding refresh.cv MCP to Codex as "${serverName}"...`);
|
|
265
|
+
const result = await runCodexStreaming(
|
|
266
|
+
['mcp', 'add', serverName, '--url', mcpUrl],
|
|
267
|
+
{
|
|
268
|
+
errorMessage: 'Failed to add refresh.cv MCP to Codex.',
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (codexAddStartedOAuth(result.output)) {
|
|
273
|
+
options.loginStartedDuringAdd = true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function runCodex(args, { errorMessage }) {
|
|
278
|
+
const result = spawnSync('codex', args, { stdio: 'inherit' });
|
|
279
|
+
if (result.error?.code === 'ENOENT') {
|
|
280
|
+
fail('Could not find "codex" in PATH.');
|
|
281
|
+
}
|
|
282
|
+
if (result.status !== 0) {
|
|
283
|
+
fail(errorMessage);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function runCodexStreaming(args, { errorMessage }) {
|
|
288
|
+
return new Promise((resolve) => {
|
|
289
|
+
const child = spawn('codex', args, { stdio: ['inherit', 'pipe', 'pipe'] });
|
|
290
|
+
let output = '';
|
|
291
|
+
|
|
292
|
+
child.stdout.on('data', (chunk) => {
|
|
293
|
+
const text = chunk.toString();
|
|
294
|
+
output += text;
|
|
295
|
+
process.stdout.write(text);
|
|
296
|
+
});
|
|
297
|
+
child.stderr.on('data', (chunk) => {
|
|
298
|
+
const text = chunk.toString();
|
|
299
|
+
output += text;
|
|
300
|
+
process.stderr.write(text);
|
|
301
|
+
});
|
|
302
|
+
child.on('error', (error) => {
|
|
303
|
+
if (error.code === 'ENOENT') {
|
|
304
|
+
fail('Could not find "codex" in PATH.');
|
|
305
|
+
}
|
|
306
|
+
fail(`${errorMessage} ${error.message}`);
|
|
307
|
+
});
|
|
308
|
+
child.on('close', (status) => {
|
|
309
|
+
if (status !== 0) {
|
|
310
|
+
fail(errorMessage);
|
|
311
|
+
}
|
|
312
|
+
resolve({ status, output });
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function codexAddStartedOAuth(output) {
|
|
318
|
+
return (
|
|
319
|
+
output.includes('Detected OAuth support') ||
|
|
320
|
+
output.includes('Starting OAuth flow') ||
|
|
321
|
+
output.includes('Authorize `')
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function writeCodexMcpConfig(serverName, mcpUrl) {
|
|
326
|
+
const codexHome = process.env.CODEX_HOME || join(homedir(), '.codex');
|
|
327
|
+
const configPath = join(codexHome, 'config.toml');
|
|
328
|
+
mkdirSync(codexHome, { recursive: true });
|
|
329
|
+
|
|
330
|
+
const current = existsSync(configPath)
|
|
331
|
+
? readFileSync(configPath, 'utf8')
|
|
332
|
+
: '';
|
|
333
|
+
const next = `${removeCodexMcpSection(current, serverName).trimEnd()}
|
|
334
|
+
|
|
335
|
+
[mcp_servers.${serverName}]
|
|
336
|
+
url = ${tomlString(mcpUrl)}
|
|
337
|
+
`;
|
|
338
|
+
|
|
339
|
+
writeFileSync(configPath, next.trimStart());
|
|
340
|
+
console.log(`Configured refresh.cv MCP for Codex as "${serverName}".`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function removeCodexMcpSection(config, serverName) {
|
|
344
|
+
const lines = config.split(/\r?\n/);
|
|
345
|
+
const output = [];
|
|
346
|
+
let skipping = false;
|
|
347
|
+
const sectionPrefix = `[mcp_servers.${serverName}`;
|
|
348
|
+
|
|
349
|
+
for (const line of lines) {
|
|
350
|
+
const header = line.trim().match(/^\[[^\]]+\]$/)?.[0];
|
|
351
|
+
if (header) {
|
|
352
|
+
skipping = header.startsWith(sectionPrefix);
|
|
353
|
+
}
|
|
354
|
+
if (!skipping) {
|
|
355
|
+
output.push(line);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return output.join('\n');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function tomlString(value) {
|
|
363
|
+
return JSON.stringify(value);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function formatCommandOutput(result) {
|
|
367
|
+
return [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function fail(message) {
|
|
371
|
+
console.error(`refresh-cv: ${message}`);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "refresh-cv",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install refresh.cv MCP connections for supported agent clients.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"refresh-cv": "bin/refresh-cv.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://refresh.cv",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/refresh-dev/refresh-mcp-server.git",
|
|
20
|
+
"directory": "packages/refresh-cv"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/refresh-dev/refresh-mcp-server/issues"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"refresh.cv",
|
|
27
|
+
"mcp",
|
|
28
|
+
"codex",
|
|
29
|
+
"work-history",
|
|
30
|
+
"resume"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|