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 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
- For dev:
19
+ ## Local server
20
20
 
21
21
  ```bash
22
- npx -y refresh-cv@latest --client codex --dev
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
- - `--name <name>`: override the Codex MCP server name. Defaults to `refresh_cv`.
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.0';
7
+ const PACKAGE_VERSION = '0.1.2';
8
8
  const PRODUCTION_MCP_URL = 'https://mcp.refresh.cv/mcp';
9
- const DEV_MCP_URL = 'https://mcp.dev.refresh.cv/mcp';
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
- --name <name> Codex MCP server name (default: ${DEFAULT_SERVER_NAME})
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 --dev
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
- const serverName = options.name ?? DEFAULT_SERVER_NAME;
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.dev ? DEV_MCP_URL : PRODUCTION_MCP_URL);
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
- console.log(
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
- console.log(
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
- console.log(
103
- `Codex started the OAuth login flow for "${serverName}". Finish the browser approval if prompted.`,
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
- console.log(`Opening Codex OAuth login for "${serverName}"...`);
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 development URLs.');
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
- console.log(`Adding refresh.cv MCP to Codex as "${serverName}"...`);
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
- process.stdout.write(text);
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
- process.stderr.write(text);
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
- console.log(`Configured refresh.cv MCP for Codex as "${serverName}".`);
449
+ logOk(`Configured "${serverName}" in Codex.`);
341
450
  }
342
451
 
343
452
  function removeCodexMcpSection(config, serverName) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "refresh-cv",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Install refresh.cv MCP connections for supported agent clients.",
5
5
  "type": "module",
6
6
  "bin": {