refresh-cv 0.1.0 → 0.1.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.
- package/README.md +12 -4
- package/bin/refresh-cv.js +136 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,17 +16,25 @@ https://mcp.refresh.cv/mcp
|
|
|
16
16
|
|
|
17
17
|
Then it starts the Codex OAuth login flow.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
## Local server
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npx -y refresh-cv@latest --client codex --
|
|
22
|
+
npx -y refresh-cv@latest --client codex --local
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
This configures Codex with:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
http://localhost:8010/mcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The default local server name is `refresh_cv_local`, so production and local MCP connections can coexist.
|
|
32
|
+
|
|
25
33
|
## Options
|
|
26
34
|
|
|
27
35
|
- `--client codex`: configure Codex.
|
|
28
|
-
- `--dev`: use `https://mcp.dev.refresh.cv/mcp`.
|
|
29
36
|
- `--url <url>`: override the MCP endpoint.
|
|
30
|
-
- `--
|
|
37
|
+
- `--local`: configure `http://localhost:8010/mcp` as `refresh_cv_local`.
|
|
38
|
+
- `--name <name>`: override the Codex MCP server name. Defaults to `refresh_cv`, or `refresh_cv_local` with `--local`.
|
|
31
39
|
- `--force`: replace an existing Codex MCP server with the same name.
|
|
32
40
|
- `--skip-login`: add the MCP server but skip OAuth login.
|
package/bin/refresh-cv.js
CHANGED
|
@@ -4,10 +4,11 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
|
|
7
|
-
const PACKAGE_VERSION = '0.1.
|
|
7
|
+
const PACKAGE_VERSION = '0.1.2';
|
|
8
8
|
const PRODUCTION_MCP_URL = 'https://mcp.refresh.cv/mcp';
|
|
9
|
-
const
|
|
9
|
+
const LOCAL_MCP_URL = 'http://localhost:8010/mcp';
|
|
10
10
|
const DEFAULT_SERVER_NAME = 'refresh_cv';
|
|
11
|
+
const LOCAL_DEFAULT_SERVER_NAME = 'refresh_cv_local';
|
|
11
12
|
const SIMPLE_TOML_KEY = /^[A-Za-z0-9_-]+$/;
|
|
12
13
|
|
|
13
14
|
const HELP = `refresh.cv MCP installer
|
|
@@ -17,9 +18,9 @@ Usage:
|
|
|
17
18
|
|
|
18
19
|
Options:
|
|
19
20
|
--client <client> Agent client to configure. Currently supports: codex
|
|
20
|
-
--dev Use the dev MCP endpoint (${DEV_MCP_URL})
|
|
21
21
|
--url <url> Override the MCP endpoint URL
|
|
22
|
-
--
|
|
22
|
+
--local Use ${LOCAL_MCP_URL} and default name ${LOCAL_DEFAULT_SERVER_NAME}
|
|
23
|
+
--name <name> Codex MCP server name (default: ${DEFAULT_SERVER_NAME}, or ${LOCAL_DEFAULT_SERVER_NAME} with --local)
|
|
23
24
|
--force Replace an existing Codex MCP server with the same name
|
|
24
25
|
--skip-login Add the server but do not run "codex mcp login"
|
|
25
26
|
--version Show this installer version
|
|
@@ -27,7 +28,7 @@ Options:
|
|
|
27
28
|
|
|
28
29
|
Examples:
|
|
29
30
|
npx -y refresh-cv@latest --client codex
|
|
30
|
-
npx -y refresh-cv@latest --client codex --
|
|
31
|
+
npx -y refresh-cv@latest --client codex --local
|
|
31
32
|
`;
|
|
32
33
|
|
|
33
34
|
main().catch((error) => {
|
|
@@ -57,24 +58,31 @@ async function main() {
|
|
|
57
58
|
);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
if (options.local && options.url) {
|
|
62
|
+
fail('Use either --local or --url, not both.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const serverName =
|
|
66
|
+
options.name ??
|
|
67
|
+
(options.local ? LOCAL_DEFAULT_SERVER_NAME : DEFAULT_SERVER_NAME);
|
|
61
68
|
const mcpUrl =
|
|
62
|
-
options.url ?? (options.
|
|
69
|
+
options.url ?? (options.local ? LOCAL_MCP_URL : PRODUCTION_MCP_URL);
|
|
63
70
|
|
|
64
71
|
assertValidServerName(serverName);
|
|
65
72
|
assertValidMcpUrl(mcpUrl);
|
|
66
73
|
assertCommandAvailable('codex');
|
|
67
74
|
|
|
75
|
+
printHeader({ mcpUrl, options, serverName });
|
|
76
|
+
|
|
68
77
|
const existing = getCodexMcp(serverName);
|
|
69
78
|
if (existing.exists) {
|
|
70
79
|
const existingUrl = existing.url;
|
|
71
80
|
if (existingUrl === mcpUrl) {
|
|
72
|
-
|
|
73
|
-
`refresh.cv MCP is already configured for Codex as "${serverName}".`,
|
|
74
|
-
);
|
|
81
|
+
logOk(`Codex already has "${serverName}" configured.`);
|
|
75
82
|
} else if (options.force && options.skipLogin) {
|
|
76
83
|
writeCodexMcpConfig(serverName, mcpUrl);
|
|
77
84
|
} else if (options.force) {
|
|
85
|
+
logStep(`Replacing existing Codex MCP "${serverName}"...`);
|
|
78
86
|
runCodex(['mcp', 'remove', serverName], {
|
|
79
87
|
errorMessage: `Failed to remove existing Codex MCP server "${serverName}".`,
|
|
80
88
|
});
|
|
@@ -92,32 +100,31 @@ async function main() {
|
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
if (options.skipLogin) {
|
|
95
|
-
|
|
96
|
-
`Skipped login. Run this when ready: codex mcp login ${serverName}`,
|
|
97
|
-
);
|
|
103
|
+
logMuted(`Login skipped. Run when ready: codex mcp login ${serverName}`);
|
|
98
104
|
return;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
if (options.loginStartedDuringAdd) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
108
|
+
logStep('Codex started OAuth during setup.');
|
|
109
|
+
logMuted('Approve the browser prompt to finish connecting refresh.cv.');
|
|
105
110
|
return;
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
|
|
113
|
+
logStep('Opening refresh.cv sign-in through Codex...');
|
|
109
114
|
await runCodexStreaming(['mcp', 'login', serverName], {
|
|
110
115
|
errorMessage: `Codex login did not complete. Retry with: codex mcp login ${serverName}`,
|
|
116
|
+
mode: 'codex-login',
|
|
111
117
|
});
|
|
118
|
+
logOk('Done. refresh.cv is ready in Codex.');
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
function parseArgs(args) {
|
|
115
122
|
const options = {
|
|
116
123
|
client: undefined,
|
|
117
|
-
dev: false,
|
|
118
124
|
force: false,
|
|
119
125
|
help: false,
|
|
120
126
|
loginStartedDuringAdd: false,
|
|
127
|
+
local: false,
|
|
121
128
|
name: undefined,
|
|
122
129
|
skipLogin: false,
|
|
123
130
|
url: undefined,
|
|
@@ -132,9 +139,6 @@ function parseArgs(args) {
|
|
|
132
139
|
case '--client':
|
|
133
140
|
options.client = readValue(args, ++index, '--client').toLowerCase();
|
|
134
141
|
break;
|
|
135
|
-
case '--dev':
|
|
136
|
-
options.dev = true;
|
|
137
|
-
break;
|
|
138
142
|
case '--force':
|
|
139
143
|
options.force = true;
|
|
140
144
|
break;
|
|
@@ -142,6 +146,9 @@ function parseArgs(args) {
|
|
|
142
146
|
case '-h':
|
|
143
147
|
options.help = true;
|
|
144
148
|
break;
|
|
149
|
+
case '--local':
|
|
150
|
+
options.local = true;
|
|
151
|
+
break;
|
|
145
152
|
case '--name':
|
|
146
153
|
options.name = readValue(args, ++index, '--name');
|
|
147
154
|
break;
|
|
@@ -211,7 +218,7 @@ function assertValidMcpUrl(value) {
|
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
if (parsed.protocol !== 'https:' && parsed.hostname !== 'localhost') {
|
|
214
|
-
fail('MCP URL must use https, except localhost
|
|
221
|
+
fail('MCP URL must use https, except localhost URLs.');
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
if (parsed.pathname !== '/mcp') {
|
|
@@ -261,11 +268,12 @@ function getCodexMcp(serverName) {
|
|
|
261
268
|
}
|
|
262
269
|
|
|
263
270
|
async function addCodexMcp(serverName, mcpUrl, options) {
|
|
264
|
-
|
|
271
|
+
logStep(`Adding refresh.cv MCP to Codex as "${serverName}"...`);
|
|
265
272
|
const result = await runCodexStreaming(
|
|
266
273
|
['mcp', 'add', serverName, '--url', mcpUrl],
|
|
267
274
|
{
|
|
268
275
|
errorMessage: 'Failed to add refresh.cv MCP to Codex.',
|
|
276
|
+
mode: 'codex-login',
|
|
269
277
|
},
|
|
270
278
|
);
|
|
271
279
|
|
|
@@ -284,20 +292,29 @@ function runCodex(args, { errorMessage }) {
|
|
|
284
292
|
}
|
|
285
293
|
}
|
|
286
294
|
|
|
287
|
-
function runCodexStreaming(args, { errorMessage }) {
|
|
295
|
+
function runCodexStreaming(args, { errorMessage, mode = 'raw' }) {
|
|
288
296
|
return new Promise((resolve) => {
|
|
289
297
|
const child = spawn('codex', args, { stdio: ['inherit', 'pipe', 'pipe'] });
|
|
298
|
+
const printer = mode === 'codex-login' ? createCodexLoginPrinter() : null;
|
|
290
299
|
let output = '';
|
|
291
300
|
|
|
292
301
|
child.stdout.on('data', (chunk) => {
|
|
293
302
|
const text = chunk.toString();
|
|
294
303
|
output += text;
|
|
295
|
-
|
|
304
|
+
if (printer) {
|
|
305
|
+
printer.write(text, process.stdout);
|
|
306
|
+
} else {
|
|
307
|
+
process.stdout.write(text);
|
|
308
|
+
}
|
|
296
309
|
});
|
|
297
310
|
child.stderr.on('data', (chunk) => {
|
|
298
311
|
const text = chunk.toString();
|
|
299
312
|
output += text;
|
|
300
|
-
|
|
313
|
+
if (printer) {
|
|
314
|
+
printer.write(text, process.stderr);
|
|
315
|
+
} else {
|
|
316
|
+
process.stderr.write(text);
|
|
317
|
+
}
|
|
301
318
|
});
|
|
302
319
|
child.on('error', (error) => {
|
|
303
320
|
if (error.code === 'ENOENT') {
|
|
@@ -306,6 +323,7 @@ function runCodexStreaming(args, { errorMessage }) {
|
|
|
306
323
|
fail(`${errorMessage} ${error.message}`);
|
|
307
324
|
});
|
|
308
325
|
child.on('close', (status) => {
|
|
326
|
+
printer?.flush();
|
|
309
327
|
if (status !== 0) {
|
|
310
328
|
fail(errorMessage);
|
|
311
329
|
}
|
|
@@ -314,6 +332,58 @@ function runCodexStreaming(args, { errorMessage }) {
|
|
|
314
332
|
});
|
|
315
333
|
}
|
|
316
334
|
|
|
335
|
+
function createCodexLoginPrinter() {
|
|
336
|
+
let pending = '';
|
|
337
|
+
let waitingForUrl = false;
|
|
338
|
+
let printedUrl = false;
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
write(text, stream) {
|
|
342
|
+
pending += text;
|
|
343
|
+
const lines = pending.split(/\r?\n/);
|
|
344
|
+
pending = lines.pop() ?? '';
|
|
345
|
+
for (const line of lines) {
|
|
346
|
+
handleCodexLoginLine(line, stream);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
flush() {
|
|
350
|
+
if (pending) {
|
|
351
|
+
handleCodexLoginLine(pending, process.stdout);
|
|
352
|
+
pending = '';
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
function handleCodexLoginLine(rawLine, stream) {
|
|
358
|
+
const line = rawLine.trim();
|
|
359
|
+
if (!line) return;
|
|
360
|
+
|
|
361
|
+
if (/^Authorize `[^`]+` by opening this URL in your browser:?$/.test(line)) {
|
|
362
|
+
waitingForUrl = true;
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if ((waitingForUrl || line.includes('/oauth/authorize')) && isUrl(line)) {
|
|
367
|
+
printAuthorizeUrl(line);
|
|
368
|
+
waitingForUrl = false;
|
|
369
|
+
printedUrl = true;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (line.startsWith('http') && isUrl(line)) {
|
|
374
|
+
printAuthorizeUrl(line);
|
|
375
|
+
printedUrl = true;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (printedUrl && /authorize|opening this url/i.test(line)) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
stream.write(` ${line}\n`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
317
387
|
function codexAddStartedOAuth(output) {
|
|
318
388
|
return (
|
|
319
389
|
output.includes('Detected OAuth support') ||
|
|
@@ -322,6 +392,45 @@ function codexAddStartedOAuth(output) {
|
|
|
322
392
|
);
|
|
323
393
|
}
|
|
324
394
|
|
|
395
|
+
function printHeader({ mcpUrl, options, serverName }) {
|
|
396
|
+
console.log('refresh.cv MCP');
|
|
397
|
+
console.log('');
|
|
398
|
+
console.log(' Client Codex');
|
|
399
|
+
console.log(` Profile ${serverName}`);
|
|
400
|
+
console.log(` Endpoint ${mcpUrl}`);
|
|
401
|
+
console.log(` Mode ${options.local ? 'local' : 'production'}`);
|
|
402
|
+
console.log('');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function logStep(message) {
|
|
406
|
+
console.log(`[..] ${message}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function logOk(message) {
|
|
410
|
+
console.log(`[ok] ${message}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function logMuted(message) {
|
|
414
|
+
console.log(` ${message}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function printAuthorizeUrl(url) {
|
|
418
|
+
console.log('');
|
|
419
|
+
console.log('Open this link to approve refresh.cv:');
|
|
420
|
+
console.log(` ${url}`);
|
|
421
|
+
console.log('');
|
|
422
|
+
console.log('Waiting for Codex to finish the login...');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function isUrl(value) {
|
|
426
|
+
try {
|
|
427
|
+
new URL(value);
|
|
428
|
+
return true;
|
|
429
|
+
} catch {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
325
434
|
function writeCodexMcpConfig(serverName, mcpUrl) {
|
|
326
435
|
const codexHome = process.env.CODEX_HOME || join(homedir(), '.codex');
|
|
327
436
|
const configPath = join(codexHome, 'config.toml');
|
|
@@ -337,7 +446,7 @@ url = ${tomlString(mcpUrl)}
|
|
|
337
446
|
`;
|
|
338
447
|
|
|
339
448
|
writeFileSync(configPath, next.trimStart());
|
|
340
|
-
|
|
449
|
+
logOk(`Configured "${serverName}" in Codex.`);
|
|
341
450
|
}
|
|
342
451
|
|
|
343
452
|
function removeCodexMcpSection(config, serverName) {
|