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 +36 -10
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +470 -169
- package/package.json +7 -2
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.
|
|
13
|
-
2.
|
|
14
|
-
3.
|
|
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
|
-
|
|
|
34
|
-
|
|
35
|
-
|
|
|
36
|
-
|
|
|
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
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
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
30
|
-
const json =
|
|
31
|
-
if (
|
|
32
|
-
if (
|
|
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
|
|
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 (!
|
|
195
|
+
if (!fs4.existsSync(filePath)) {
|
|
41
196
|
return { kind: "missing" };
|
|
42
197
|
}
|
|
43
|
-
const raw =
|
|
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
|
|
56
|
-
import
|
|
209
|
+
// src/targets/opencode/write-config.ts
|
|
210
|
+
import fs5 from "fs";
|
|
211
|
+
import path3 from "path";
|
|
57
212
|
|
|
58
|
-
// src/
|
|
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 =
|
|
104
|
-
if (!
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
533
|
+
return select2({
|
|
138
534
|
message: "Where should the config be installed?",
|
|
139
535
|
choices: [
|
|
140
536
|
{
|
|
141
|
-
name: "Global (
|
|
537
|
+
name: "Global (user home)",
|
|
142
538
|
value: "global",
|
|
143
|
-
description: "Available
|
|
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: "
|
|
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
|
-
|
|
304
|
-
program
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|