jobly-mcp 0.1.0 → 2.0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # jobly-mcp
2
2
 
3
- A CLI to add the [JoblyAI](https://github.com/JoblyAI) MCP server to your OpenCode configuration.
3
+ A CLI to add the [JoblyAI](https://github.com/JoblyAI) MCP server to your **OpenCode**, **Claude Code**, or **Codex CLI** configuration.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -9,20 +9,38 @@ npx jobly-mcp
9
9
  ```
10
10
 
11
11
  The CLI will:
12
- 1. Prompt for your JoblyAI API key (masked input)
13
- 2. Ask whether to install globally or in the current project
14
- 3. Write the MCP server config to `opencode.json` (or `opencode.jsonc`)
12
+ 1. Ask which CLIs to configure (OpenCode, Claude Code, Codex CLI)
13
+ 2. Prompt for your JoblyAI API key (masked input)
14
+ 3. Ask whether to install globally or in the current project
15
+ 4. Write the MCP server entry to each selected CLI's config
16
+
17
+ ## Target a specific CLI
18
+
19
+ ```bash
20
+ npx jobly-mcp --claude # Claude Code only
21
+ npx jobly-mcp --codex # Codex CLI only
22
+ npx jobly-mcp --opencode # OpenCode only
23
+ npx jobly-mcp --claude --codex # Claude Code + Codex CLI
24
+ npx jobly-mcp --all # all three
25
+ ```
26
+
27
+ Passing target flags skips the "which CLIs" prompt. One shared API key and one global/local choice apply to every selected target.
15
28
 
16
29
  ## Uninstall
17
30
 
18
31
  ```bash
19
- npx jobly-mcp --uninstall
32
+ npx jobly-mcp --uninstall # choose which CLIs to remove from
33
+ npx jobly-mcp --uninstall --claude # remove from Claude Code only
20
34
  ```
21
35
 
22
36
  ## Flags
23
37
 
24
38
  | Flag | Description |
25
39
  |------|-------------|
40
+ | `--claude` | Configure Claude Code |
41
+ | `--codex` | Configure Codex CLI |
42
+ | `--opencode` | Configure OpenCode |
43
+ | `--all` | Configure all supported CLIs |
26
44
  | `-u, --uninstall` | Remove the jobly-mcp entry instead of adding it |
27
45
  | `-y, --yes` | Skip confirmation prompts (overwrite, comment-loss) |
28
46
  | `-V, --version` | Print version |
@@ -30,13 +48,21 @@ npx jobly-mcp --uninstall
30
48
 
31
49
  ## Where configs are written
32
50
 
33
- | Scope | Path |
34
- |-------|------|
35
- | Global | `~/.config/opencode/opencode.jsonc` (or `$XDG_CONFIG_HOME/opencode/opencode.jsonc`) |
36
- | Local | `<nearest-git-root>/opencode.json` |
51
+ | CLI | Global | Local |
52
+ |-----|--------|-------|
53
+ | OpenCode | `~/.config/opencode/opencode.jsonc` (or `$XDG_CONFIG_HOME/opencode/`) | `<git-root>/opencode.json` |
54
+ | Claude Code | `~/.claude.json` (top-level `mcpServers`) | `<git-root>/.mcp.json` |
55
+ | Codex CLI | `~/.codex/config.toml` | `<git-root>/.codex/config.toml` |
56
+
57
+ > **Uninstall scope note:** uninstall only looks in the scope you choose (global or local). If an entry lives in the other scope, re-run with that scope.
58
+
59
+ ## Entry shape per CLI
60
+
61
+ - **OpenCode** (`mcp` → `jobly-mcp`): `{ "type": "remote", "url": "https://jobly.ai.vn/api/mcp", "enabled": true, "headers": { "Authorization": "Bearer <key>" } }`
62
+ - **Claude Code** (`mcpServers` → `jobly-mcp`): `{ "type": "http", "url": "https://jobly.ai.vn/api/mcp", "headers": { "Authorization": "Bearer <key>" } }`
63
+ - **Codex CLI** (`[mcp_servers."jobly-mcp"]`): `url = "https://jobly.ai.vn/api/mcp"` and `http_headers = { "Authorization" = "Bearer <key>" }`
37
64
 
38
65
  ## Requirements
39
66
 
40
67
  - Node.js 20+
41
- - An OpenCode installation
42
68
  - A JoblyAI API key (starts with `jobly_sk_`)
package/dist/cli.d.ts CHANGED
@@ -1 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+
4
+ declare function createProgram(): Command;
5
+ declare function run(argv?: string[]): Promise<void>;
6
+
7
+ export { createProgram, run };
package/dist/cli.js CHANGED
@@ -2,45 +2,200 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
+ import { pathToFileURL } from "url";
6
+ import { realpathSync } from "fs";
5
7
 
6
8
  // src/commands/setup.ts
7
- import fs4 from "fs";
9
+ import fs from "fs";
10
+
11
+ // src/utils/logger.ts
12
+ import pc from "picocolors";
13
+ var logger = {
14
+ info: (msg) => console.log(pc.cyan("\u2139") + " " + msg),
15
+ success: (msg) => console.log(pc.green("\u2713") + " " + msg),
16
+ warn: (msg) => console.warn(pc.yellow("\u26A0") + " " + msg),
17
+ error: (msg) => console.error(pc.red("\u2717") + " " + msg),
18
+ step: (msg) => console.log(pc.bold("\n" + msg))
19
+ };
20
+
21
+ // src/prompts/overwrite.ts
22
+ import { confirm } from "@inquirer/prompts";
23
+ async function promptOverwrite() {
24
+ return confirm({
25
+ message: "jobly-mcp is already configured. Overwrite?",
26
+ default: false
27
+ });
28
+ }
29
+
30
+ // src/prompts/confirm-comment-loss.ts
31
+ import { confirm as confirm2 } from "@inquirer/prompts";
32
+ async function promptConfirmCommentLoss() {
33
+ return confirm2({
34
+ message: "This file contains comments that will be lost when rewriting. Continue?",
35
+ default: false
36
+ });
37
+ }
38
+
39
+ // src/prompts/invalid-config.ts
40
+ import { select } from "@inquirer/prompts";
41
+ async function promptInvalidConfigAction(fileLabel) {
42
+ return select({
43
+ message: `${fileLabel} contains invalid content. What do you want to do?`,
44
+ choices: [
45
+ {
46
+ name: "Abort (recommended)",
47
+ value: "abort",
48
+ description: "Exit without making changes. Fix the file manually first."
49
+ },
50
+ {
51
+ name: "Back up the file and continue with a fresh config",
52
+ value: "backup",
53
+ description: "Renames the broken file to .bak-<timestamp> and writes a new one"
54
+ }
55
+ ]
56
+ });
57
+ }
58
+
59
+ // src/commands/setup.ts
60
+ async function runSetup(opts) {
61
+ const written = [];
62
+ for (const target of opts.targets) {
63
+ const filePath = target.resolveConfigFile(opts.scope);
64
+ const result = target.readConfig(filePath);
65
+ let config;
66
+ if (result.kind === "missing") {
67
+ config = target.createNewConfig(target.buildEntry(opts.apiKey));
68
+ } else if (result.kind === "invalid") {
69
+ logger.warn(`${target.label} config contains invalid content: ${result.error}`);
70
+ const action = await promptInvalidConfigAction(target.label);
71
+ if (action === "abort") {
72
+ logger.error("Aborted. Fix the file manually and try again.");
73
+ process.exit(3);
74
+ }
75
+ const backupPath = `${filePath}.bak-${Date.now()}`;
76
+ fs.renameSync(filePath, backupPath);
77
+ logger.info(`Backed up to ${backupPath}`);
78
+ config = target.createNewConfig(target.buildEntry(opts.apiKey));
79
+ } else {
80
+ config = result.config;
81
+ if (target.hasEntry(config) && !opts.force) {
82
+ const overwrite = await promptOverwrite();
83
+ if (!overwrite) {
84
+ logger.info("Aborted, no changes made.");
85
+ process.exit(130);
86
+ }
87
+ }
88
+ if (result.hadComments && !opts.force) {
89
+ const confirm3 = await promptConfirmCommentLoss();
90
+ if (!confirm3) {
91
+ logger.info("Aborted, no changes made.");
92
+ process.exit(130);
93
+ }
94
+ }
95
+ config = target.setEntry(config, target.buildEntry(opts.apiKey));
96
+ }
97
+ await target.writeConfig(filePath, config);
98
+ logger.success(`jobly-mcp added to ${filePath} (${target.label})`);
99
+ written.push(filePath);
100
+ }
101
+ if (written.length > 0) {
102
+ logger.step("Done");
103
+ for (const f of written) logger.info(` ${f}`);
104
+ }
105
+ }
106
+
107
+ // src/commands/uninstall.ts
108
+ async function runUninstall(opts) {
109
+ if (opts.targets.length === 0) {
110
+ logger.info("jobly-mcp is not configured.");
111
+ process.exit(0);
112
+ }
113
+ const changed = [];
114
+ for (const target of opts.targets) {
115
+ const filePath = target.resolveConfigFile(opts.scope);
116
+ const result = target.readConfig(filePath);
117
+ if (result.kind === "missing") {
118
+ logger.info(`not configured for ${target.label}`);
119
+ continue;
120
+ }
121
+ if (result.kind === "invalid") {
122
+ logger.warn(`${target.label} config contains invalid content: ${result.error}`);
123
+ const action = await promptInvalidConfigAction(target.label);
124
+ if (action === "abort") {
125
+ logger.error("Aborted. Fix the file manually and try again.");
126
+ process.exit(3);
127
+ }
128
+ logger.info(`Preserving the broken file. jobly-mcp not removed for ${target.label}.`);
129
+ continue;
130
+ }
131
+ if (!target.hasEntry(result.config)) {
132
+ logger.info(`not configured for ${target.label}`);
133
+ continue;
134
+ }
135
+ if (result.hadComments && !opts.force) {
136
+ const confirm3 = await promptConfirmCommentLoss();
137
+ if (!confirm3) {
138
+ logger.info("Aborted, no changes made.");
139
+ process.exit(130);
140
+ }
141
+ }
142
+ const config = target.removeEntry(result.config);
143
+ await target.writeConfig(filePath, config);
144
+ logger.success(`jobly-mcp removed from ${filePath} (${target.label})`);
145
+ changed.push(filePath);
146
+ }
147
+ if (changed.length === 0) {
148
+ logger.info("jobly-mcp is not configured.");
149
+ process.exit(0);
150
+ }
151
+ logger.step("Done");
152
+ for (const f of changed) logger.info(` ${f}`);
153
+ }
8
154
 
9
- // src/opencode/config-paths.ts
155
+ // src/targets/opencode/config-paths.ts
10
156
  import os from "os";
157
+ import path2 from "path";
158
+ import fs3 from "fs";
159
+
160
+ // src/targets/paths.ts
161
+ import fs2 from "fs";
11
162
  import path from "path";
12
- import fs from "fs";
13
- function getGlobalConfigDir() {
14
- const home = os.homedir();
15
- const xdg = process.env.XDG_CONFIG_HOME ?? path.join(home, ".config");
16
- return path.join(xdg, "opencode");
17
- }
18
- function getLocalConfigDir() {
163
+ function getLocalGitRoot() {
19
164
  let dir = process.cwd();
20
165
  while (true) {
21
- if (fs.existsSync(path.join(dir, ".git"))) return dir;
166
+ if (fs2.existsSync(path.join(dir, ".git"))) return dir;
22
167
  const parent = path.dirname(dir);
23
168
  if (parent === dir) return process.cwd();
24
169
  dir = parent;
25
170
  }
26
171
  }
172
+
173
+ // src/targets/opencode/config-paths.ts
174
+ function getGlobalConfigDir() {
175
+ const home = os.homedir();
176
+ const xdg = process.env.XDG_CONFIG_HOME ?? path2.join(home, ".config");
177
+ return path2.join(xdg, "opencode");
178
+ }
179
+ function getLocalConfigDir() {
180
+ return getLocalGitRoot();
181
+ }
27
182
  function resolveConfigFile(scope) {
28
183
  const dir = scope === "global" ? getGlobalConfigDir() : getLocalConfigDir();
29
- const jsonc = path.join(dir, "opencode.jsonc");
30
- const json = path.join(dir, "opencode.json");
31
- if (fs.existsSync(jsonc)) return jsonc;
32
- if (fs.existsSync(json)) return json;
184
+ const jsonc = path2.join(dir, "opencode.jsonc");
185
+ const json = path2.join(dir, "opencode.json");
186
+ if (fs3.existsSync(jsonc)) return jsonc;
187
+ if (fs3.existsSync(json)) return json;
33
188
  return scope === "global" ? jsonc : json;
34
189
  }
35
190
 
36
- // src/opencode/read-config.ts
37
- import fs2 from "fs";
191
+ // src/targets/opencode/read-config.ts
192
+ import fs4 from "fs";
38
193
  import stripJsonComments from "strip-json-comments";
39
194
  function readConfig(filePath) {
40
- if (!fs2.existsSync(filePath)) {
195
+ if (!fs4.existsSync(filePath)) {
41
196
  return { kind: "missing" };
42
197
  }
43
- const raw = fs2.readFileSync(filePath, "utf8");
198
+ const raw = fs4.readFileSync(filePath, "utf8");
44
199
  const stripped = stripJsonComments(raw);
45
200
  const hadComments = stripped !== raw;
46
201
  try {
@@ -51,13 +206,15 @@ function readConfig(filePath) {
51
206
  }
52
207
  }
53
208
 
54
- // src/opencode/write-config.ts
55
- import fs3 from "fs";
56
- import path2 from "path";
209
+ // src/targets/opencode/write-config.ts
210
+ import fs5 from "fs";
211
+ import path3 from "path";
57
212
 
58
- // src/opencode/types.ts
213
+ // src/targets/types.ts
59
214
  var JOBLY_MCP_KEY = "jobly-mcp";
60
215
  var JOBLY_MCP_URL = "https://jobly.ai.vn/api/mcp";
216
+
217
+ // src/targets/opencode/types.ts
61
218
  var OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
62
219
  function buildJoblyMcpEntry(apiKey) {
63
220
  return {
@@ -78,7 +235,7 @@ function createNewConfig(entry) {
78
235
  };
79
236
  }
80
237
 
81
- // src/opencode/write-config.ts
238
+ // src/targets/opencode/write-config.ts
82
239
  function hasMcpEntry(config) {
83
240
  return Boolean(config.mcp?.[JOBLY_MCP_KEY]);
84
241
  }
@@ -100,14 +257,253 @@ function removeMcpEntry(config) {
100
257
  return { ...config, mcp: rest };
101
258
  }
102
259
  async function writeConfig(filePath, config) {
103
- const dir = path2.dirname(filePath);
104
- if (!fs3.existsSync(dir)) {
105
- fs3.mkdirSync(dir, { recursive: true });
260
+ const dir = path3.dirname(filePath);
261
+ if (!fs5.existsSync(dir)) {
262
+ fs5.mkdirSync(dir, { recursive: true });
106
263
  }
107
264
  const tmp = filePath + ".tmp";
108
265
  const content = JSON.stringify(config, null, 2) + "\n";
109
- fs3.writeFileSync(tmp, content, "utf8");
110
- fs3.renameSync(tmp, filePath);
266
+ fs5.writeFileSync(tmp, content, "utf8");
267
+ fs5.renameSync(tmp, filePath);
268
+ }
269
+
270
+ // src/targets/opencode/adapter.ts
271
+ var openCodeAdapter = {
272
+ id: "opencode",
273
+ label: "OpenCode",
274
+ resolveConfigFile,
275
+ readConfig,
276
+ hasEntry: (config) => hasMcpEntry(config),
277
+ buildEntry: (apiKey) => buildJoblyMcpEntry(apiKey),
278
+ createNewConfig: (entry) => createNewConfig(entry),
279
+ setEntry: (config, entry) => setMcpEntry(config, entry),
280
+ removeEntry: (config) => removeMcpEntry(config),
281
+ writeConfig: (filePath, config) => writeConfig(filePath, config)
282
+ };
283
+
284
+ // src/targets/claude/config-paths.ts
285
+ import os2 from "os";
286
+ import path4 from "path";
287
+ function resolveClaudeConfigFile(scope) {
288
+ if (scope === "global") return path4.join(os2.homedir(), ".claude.json");
289
+ return path4.join(getLocalGitRoot(), ".mcp.json");
290
+ }
291
+
292
+ // src/targets/claude/read-config.ts
293
+ import fs6 from "fs";
294
+ import stripJsonComments2 from "strip-json-comments";
295
+ function readClaudeConfig(filePath) {
296
+ if (!fs6.existsSync(filePath)) return { kind: "missing" };
297
+ const raw = fs6.readFileSync(filePath, "utf8");
298
+ const stripped = stripJsonComments2(raw);
299
+ const hadComments = stripped !== raw;
300
+ try {
301
+ const config = JSON.parse(stripped);
302
+ return { kind: "ok", config, raw, hadComments };
303
+ } catch (err) {
304
+ return { kind: "invalid", error: err.message };
305
+ }
306
+ }
307
+
308
+ // src/targets/claude/write-config.ts
309
+ import fs7 from "fs";
310
+ import path5 from "path";
311
+ async function writeClaudeConfig(filePath, config) {
312
+ const dir = path5.dirname(filePath);
313
+ if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
314
+ const tmp = filePath + ".tmp";
315
+ fs7.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf8");
316
+ fs7.renameSync(tmp, filePath);
317
+ }
318
+
319
+ // src/targets/claude/types.ts
320
+ var CLAUDE_MCP_SERVERS_KEY = "mcpServers";
321
+ function buildClaudeEntry(apiKey) {
322
+ return {
323
+ type: "http",
324
+ url: JOBLY_MCP_URL,
325
+ headers: { Authorization: `Bearer ${apiKey}` }
326
+ };
327
+ }
328
+ function createNewClaudeConfig(entry) {
329
+ return { [CLAUDE_MCP_SERVERS_KEY]: { [JOBLY_MCP_KEY]: entry } };
330
+ }
331
+
332
+ // src/targets/claude/adapter.ts
333
+ function serversOf(config) {
334
+ return config[CLAUDE_MCP_SERVERS_KEY];
335
+ }
336
+ var claudeAdapter = {
337
+ id: "claude",
338
+ label: "Claude Code",
339
+ resolveConfigFile: resolveClaudeConfigFile,
340
+ readConfig: readClaudeConfig,
341
+ hasEntry(config) {
342
+ return Boolean(serversOf(config)?.[JOBLY_MCP_KEY]);
343
+ },
344
+ buildEntry: buildClaudeEntry,
345
+ createNewConfig: createNewClaudeConfig,
346
+ setEntry(config, entry) {
347
+ const servers = serversOf(config) ?? {};
348
+ return { ...config, [CLAUDE_MCP_SERVERS_KEY]: { ...servers, [JOBLY_MCP_KEY]: entry } };
349
+ },
350
+ removeEntry(config) {
351
+ const servers = serversOf(config);
352
+ if (!servers) return config;
353
+ const rest = {};
354
+ for (const [k, v] of Object.entries(servers)) {
355
+ if (k !== JOBLY_MCP_KEY) rest[k] = v;
356
+ }
357
+ return { ...config, [CLAUDE_MCP_SERVERS_KEY]: rest };
358
+ },
359
+ writeConfig: writeClaudeConfig
360
+ };
361
+
362
+ // src/targets/codex/config-paths.ts
363
+ import os3 from "os";
364
+ import path6 from "path";
365
+ function resolveCodexConfigFile(scope) {
366
+ if (scope === "global") return path6.join(os3.homedir(), ".codex", "config.toml");
367
+ return path6.join(getLocalGitRoot(), ".codex", "config.toml");
368
+ }
369
+
370
+ // src/targets/codex/read-config.ts
371
+ import fs8 from "fs";
372
+ import { parse as parseToml } from "smol-toml";
373
+ function hasTomlComments(raw) {
374
+ const withoutStrings = raw.replace(/"[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*'/g, '""');
375
+ return /#/.test(withoutStrings);
376
+ }
377
+ function readCodexConfig(filePath) {
378
+ if (!fs8.existsSync(filePath)) return { kind: "missing" };
379
+ const raw = fs8.readFileSync(filePath, "utf8");
380
+ const hadComments = hasTomlComments(raw);
381
+ try {
382
+ const config = raw.trim() === "" ? {} : parseToml(raw);
383
+ return { kind: "ok", config, raw, hadComments };
384
+ } catch (err) {
385
+ return { kind: "invalid", error: err.message };
386
+ }
387
+ }
388
+
389
+ // src/targets/codex/write-config.ts
390
+ import fs9 from "fs";
391
+ import path7 from "path";
392
+ import { stringify as stringifyToml } from "smol-toml";
393
+ async function writeCodexConfig(filePath, config) {
394
+ const dir = path7.dirname(filePath);
395
+ if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
396
+ const tmp = filePath + ".tmp";
397
+ fs9.writeFileSync(tmp, stringifyToml(config) + "\n", "utf8");
398
+ fs9.renameSync(tmp, filePath);
399
+ }
400
+
401
+ // src/targets/codex/types.ts
402
+ var CODEX_MCP_TABLE = "mcp_servers";
403
+ function buildCodexEntry(apiKey) {
404
+ return {
405
+ url: JOBLY_MCP_URL,
406
+ http_headers: { Authorization: `Bearer ${apiKey}` }
407
+ };
408
+ }
409
+ function createNewCodexConfig(entry) {
410
+ return { [CODEX_MCP_TABLE]: { [JOBLY_MCP_KEY]: entry } };
411
+ }
412
+
413
+ // src/targets/codex/adapter.ts
414
+ function tableOf(config) {
415
+ return config[CODEX_MCP_TABLE];
416
+ }
417
+ var codexAdapter = {
418
+ id: "codex",
419
+ label: "Codex CLI",
420
+ resolveConfigFile: resolveCodexConfigFile,
421
+ readConfig: readCodexConfig,
422
+ hasEntry(config) {
423
+ return Boolean(tableOf(config)?.[JOBLY_MCP_KEY]);
424
+ },
425
+ buildEntry: buildCodexEntry,
426
+ createNewConfig: createNewCodexConfig,
427
+ setEntry(config, entry) {
428
+ const table = tableOf(config) ?? {};
429
+ return { ...config, [CODEX_MCP_TABLE]: { ...table, [JOBLY_MCP_KEY]: entry } };
430
+ },
431
+ removeEntry(config) {
432
+ const table = tableOf(config);
433
+ if (!table) return config;
434
+ const rest = {};
435
+ for (const [k, v] of Object.entries(table)) {
436
+ if (k !== JOBLY_MCP_KEY) rest[k] = v;
437
+ }
438
+ return { ...config, [CODEX_MCP_TABLE]: rest };
439
+ },
440
+ writeConfig: writeCodexConfig
441
+ };
442
+
443
+ // src/targets/registry.ts
444
+ var TARGET_ADAPTERS = {
445
+ opencode: openCodeAdapter,
446
+ claude: claudeAdapter,
447
+ codex: codexAdapter
448
+ };
449
+ var ALL_TARGET_IDS = ["opencode", "claude", "codex"];
450
+ var ALL_ADAPTERS = ALL_TARGET_IDS.map(
451
+ (id) => TARGET_ADAPTERS[id]
452
+ );
453
+
454
+ // src/prompts/target-select.ts
455
+ import { checkbox } from "@inquirer/prompts";
456
+ async function promptTargetSelect(available, opts = {}) {
457
+ const checked = new Set(opts.checked ?? []);
458
+ return checkbox({
459
+ message: "Which CLIs should get the JoblyAI MCP server?",
460
+ required: opts.required ?? false,
461
+ loop: false,
462
+ choices: available.map((id) => ({
463
+ name: TARGET_ADAPTERS[id].label,
464
+ value: id,
465
+ checked: checked.has(id)
466
+ }))
467
+ });
468
+ }
469
+
470
+ // src/commands/targets.ts
471
+ function flaggedIds(flags) {
472
+ const ids = [];
473
+ if (flags.opencode) ids.push("opencode");
474
+ if (flags.claude) ids.push("claude");
475
+ if (flags.codex) ids.push("codex");
476
+ if (ids.length > 0 || flags.all) {
477
+ return flags.all ? ["opencode", "claude", "codex"] : ids;
478
+ }
479
+ return null;
480
+ }
481
+ async function resolveSetupTargets(flags, adapters = ALL_ADAPTERS) {
482
+ const byId = new Map(adapters.map((a) => [a.id, a]));
483
+ const ids = flaggedIds(flags);
484
+ if (ids) return ids.map((id) => byId.get(id));
485
+ const selected = await promptTargetSelect(
486
+ adapters.map((a) => a.id),
487
+ { required: true }
488
+ );
489
+ return selected.map((id) => byId.get(id));
490
+ }
491
+ async function resolveUninstallTargets(flags, scope, adapters = ALL_ADAPTERS) {
492
+ const byId = new Map(adapters.map((a) => [a.id, a]));
493
+ const ids = flaggedIds(flags);
494
+ if (ids) return ids.map((id) => byId.get(id));
495
+ const available = [];
496
+ for (const adapter of adapters) {
497
+ const result = adapter.readConfig(adapter.resolveConfigFile(scope));
498
+ if (result.kind === "ok" && adapter.hasEntry(result.config)) {
499
+ available.push(adapter.id);
500
+ } else if (result.kind === "invalid") {
501
+ logger.warn(`${adapter.label} config is invalid; skipping. Fix it manually.`);
502
+ }
503
+ }
504
+ if (available.length === 0) return [];
505
+ const selected = await promptTargetSelect(available, { required: false, checked: available });
506
+ return selected.map((id) => byId.get(id));
111
507
  }
112
508
 
113
509
  // src/prompts/api-key.ts
@@ -132,153 +528,25 @@ async function promptApiKey() {
132
528
  }
133
529
 
134
530
  // src/prompts/scope.ts
135
- import { select } from "@inquirer/prompts";
531
+ import { select as select2 } from "@inquirer/prompts";
136
532
  async function promptScope() {
137
- return select({
533
+ return select2({
138
534
  message: "Where should the config be installed?",
139
535
  choices: [
140
536
  {
141
- name: "Global (~/.config/opencode/)",
537
+ name: "Global (user home)",
142
538
  value: "global",
143
- description: "Available in all projects on this machine"
539
+ description: "Available across all projects on this machine"
144
540
  },
145
541
  {
146
542
  name: "Local (current project)",
147
543
  value: "local",
148
- description: "Written to the nearest git root as opencode.json"
544
+ description: "Scoped to the nearest git project"
149
545
  }
150
546
  ]
151
547
  });
152
548
  }
153
549
 
154
- // src/prompts/overwrite.ts
155
- import { confirm } from "@inquirer/prompts";
156
- async function promptOverwrite() {
157
- return confirm({
158
- message: "jobly-mcp is already configured. Overwrite?",
159
- default: false
160
- });
161
- }
162
-
163
- // src/prompts/confirm-comment-loss.ts
164
- import { confirm as confirm2 } from "@inquirer/prompts";
165
- async function promptConfirmCommentLoss() {
166
- return confirm2({
167
- message: "This file contains comments that will be lost when rewriting. Continue?",
168
- default: false
169
- });
170
- }
171
-
172
- // src/prompts/invalid-config.ts
173
- import { select as select2 } from "@inquirer/prompts";
174
- async function promptInvalidConfigAction() {
175
- return select2({
176
- message: "opencode.json contains invalid JSON. What do you want to do?",
177
- choices: [
178
- {
179
- name: "Abort (recommended)",
180
- value: "abort",
181
- description: "Exit without making changes. Fix the file manually first."
182
- },
183
- {
184
- name: "Back up the file and continue with a fresh config",
185
- value: "backup",
186
- description: "Renames the broken file to .bak-<timestamp> and writes a new one"
187
- }
188
- ]
189
- });
190
- }
191
-
192
- // src/utils/logger.ts
193
- import pc from "picocolors";
194
- var logger = {
195
- info: (msg) => console.log(pc.cyan("\u2139") + " " + msg),
196
- success: (msg) => console.log(pc.green("\u2713") + " " + msg),
197
- warn: (msg) => console.warn(pc.yellow("\u26A0") + " " + msg),
198
- error: (msg) => console.error(pc.red("\u2717") + " " + msg),
199
- step: (msg) => console.log(pc.bold("\n" + msg))
200
- };
201
-
202
- // src/commands/setup.ts
203
- async function runSetup(opts) {
204
- const apiKey = await promptApiKey();
205
- const scope = await promptScope();
206
- const filePath = resolveConfigFile(scope);
207
- const result = readConfig(filePath);
208
- let config;
209
- if (result.kind === "missing") {
210
- config = createNewConfig(buildJoblyMcpEntry(apiKey));
211
- } else if (result.kind === "invalid") {
212
- logger.warn(`opencode.json contains invalid JSON: ${result.error}`);
213
- const action = await promptInvalidConfigAction();
214
- if (action === "abort") {
215
- logger.error("Aborted. Fix the file manually and try again.");
216
- process.exit(3);
217
- }
218
- const backupPath = `${filePath}.bak-${Date.now()}`;
219
- fs4.renameSync(filePath, backupPath);
220
- logger.info(`Backed up to ${backupPath}`);
221
- config = createNewConfig(buildJoblyMcpEntry(apiKey));
222
- } else {
223
- config = result.config;
224
- const hadComments = result.hadComments;
225
- if (hasMcpEntry(config) && !opts.force) {
226
- const overwrite = await promptOverwrite();
227
- if (!overwrite) {
228
- logger.info("Aborted, no changes made.");
229
- process.exit(130);
230
- }
231
- }
232
- if (hadComments && !opts.force) {
233
- const confirm3 = await promptConfirmCommentLoss();
234
- if (!confirm3) {
235
- logger.info("Aborted, no changes made.");
236
- process.exit(130);
237
- }
238
- }
239
- config = setMcpEntry(config, buildJoblyMcpEntry(apiKey));
240
- }
241
- await writeConfig(filePath, config);
242
- logger.success(`jobly-mcp added to ${filePath}`);
243
- }
244
-
245
- // src/commands/uninstall.ts
246
- async function runUninstall(opts) {
247
- const scope = await promptScope();
248
- const filePath = resolveConfigFile(scope);
249
- const result = readConfig(filePath);
250
- if (result.kind === "missing") {
251
- logger.info("jobly-mcp is not configured.");
252
- process.exit(0);
253
- }
254
- if (result.kind === "invalid") {
255
- logger.warn(`opencode.json contains invalid JSON: ${result.error}`);
256
- const action = await promptInvalidConfigAction();
257
- if (action === "abort") {
258
- logger.error("Aborted. Fix the file manually and try again.");
259
- process.exit(3);
260
- }
261
- logger.info("Preserving the broken file. jobly-mcp not removed.");
262
- process.exit(0);
263
- }
264
- let config = result.config;
265
- const hadComments = result.hadComments;
266
- if (!hasMcpEntry(config)) {
267
- logger.info("jobly-mcp is not configured.");
268
- process.exit(0);
269
- }
270
- if (hadComments && !opts.force) {
271
- const confirm3 = await promptConfirmCommentLoss();
272
- if (!confirm3) {
273
- logger.info("Aborted, no changes made.");
274
- process.exit(130);
275
- }
276
- }
277
- config = removeMcpEntry(config);
278
- await writeConfig(filePath, config);
279
- logger.success(`jobly-mcp removed from ${filePath}`);
280
- }
281
-
282
550
  // src/utils/exit.ts
283
551
  function isCancelError(err) {
284
552
  return err instanceof Error && err.name === "ExitPromptError";
@@ -300,12 +568,45 @@ function handleCliError(err) {
300
568
  }
301
569
 
302
570
  // src/cli.ts
303
- var program = new Command();
304
- program.name("jobly-mcp").description("Setup JoblyAI MCP server in your OpenCode config").version("0.1.0").option("-u, --uninstall", "Remove the jobly-mcp entry instead of adding it").option("-y, --yes", "Skip confirmation prompts (overwrite, comment-loss)").action(async (opts) => {
305
- if (opts.uninstall) {
306
- await runUninstall({ force: opts.yes });
307
- } else {
308
- await runSetup({ force: opts.yes });
571
+ function createProgram() {
572
+ const program = new Command();
573
+ program.name("jobly-mcp").description("Setup JoblyAI MCP server in your OpenCode, Claude Code, or Codex CLI config").version("2.0.0").option("--claude", "Configure Claude Code").option("--codex", "Configure Codex CLI").option("--opencode", "Configure OpenCode").option("--all", "Configure all supported CLIs").option("-u, --uninstall", "Remove the jobly-mcp entry instead of adding it").option("-y, --yes", "Skip confirmation prompts (overwrite, comment-loss)").action(async (opts) => {
574
+ const flags = {
575
+ claude: Boolean(opts.claude),
576
+ codex: Boolean(opts.codex),
577
+ opencode: Boolean(opts.opencode),
578
+ all: Boolean(opts.all)
579
+ };
580
+ if (opts.uninstall) {
581
+ const scope = await promptScope();
582
+ const targets = await resolveUninstallTargets(flags, scope);
583
+ await runUninstall({ targets, scope, force: Boolean(opts.yes) });
584
+ } else {
585
+ const targets = await resolveSetupTargets(flags);
586
+ const apiKey = await promptApiKey();
587
+ const scope = await promptScope();
588
+ await runSetup({ targets, apiKey, scope, force: Boolean(opts.yes) });
589
+ }
590
+ });
591
+ return program;
592
+ }
593
+ async function run(argv = process.argv) {
594
+ await createProgram().parseAsync(argv);
595
+ }
596
+ function isMainModule() {
597
+ if (!process.argv[1]) return false;
598
+ try {
599
+ const argReal = realpathSync(process.argv[1]);
600
+ const modReal = realpathSync(pathToFileURL(import.meta.url));
601
+ return argReal === modReal;
602
+ } catch {
603
+ return true;
309
604
  }
310
- });
311
- program.parseAsync(process.argv).catch(handleCliError);
605
+ }
606
+ if (isMainModule()) {
607
+ run().catch(handleCliError);
608
+ }
609
+ export {
610
+ createProgram,
611
+ run
612
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jobly-mcp",
3
- "version": "0.1.0",
4
- "description": "Setup JoblyAI MCP server in your OpenCode config",
3
+ "version": "2.0.0",
4
+ "description": "Setup JoblyAI MCP server in your OpenCode, Claude Code, or Codex CLI config",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "jobly-mcp": "./dist/cli.js"
@@ -29,10 +29,14 @@
29
29
  "mcp",
30
30
  "model-context-protocol",
31
31
  "opencode",
32
+ "claude",
33
+ "claude-code",
34
+ "codex",
32
35
  "cli"
33
36
  ],
34
37
  "scripts": {
35
38
  "build": "tsup",
39
+ "typecheck": "tsc --noEmit",
36
40
  "test": "vitest run",
37
41
  "test:watch": "vitest",
38
42
  "test:coverage": "vitest run --coverage",
@@ -42,6 +46,7 @@
42
46
  "@inquirer/prompts": "^8.5.2",
43
47
  "commander": "^15.0.0",
44
48
  "picocolors": "^1.1.1",
49
+ "smol-toml": "^1.7.0",
45
50
  "strip-json-comments": "^5.0.3"
46
51
  },
47
52
  "devDependencies": {