pluriply 0.1.0 → 0.3.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 +28 -10
- package/bin/pluriply.js +122 -34
- package/package.json +1 -1
- package/src/hub/index.js +10 -7
- package/src/setup/clients.js +400 -32
- package/src/setup/hook-stop.js +157 -0
- package/src/setup/hooks.js +233 -0
- package/src/setup/run-setup.js +309 -41
- package/src/setup/toml-lite.js +87 -0
- package/src/shared/config.js +102 -0
- package/src/shared/identity.js +13 -2
- package/src/shared/lock.js +20 -0
- package/src/shared/mcp-register.js +25 -67
package/src/setup/clients.js
CHANGED
|
@@ -5,11 +5,61 @@ import {
|
|
|
5
5
|
writeFileSync,
|
|
6
6
|
copyFileSync,
|
|
7
7
|
renameSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
statSync,
|
|
10
|
+
chmodSync,
|
|
11
|
+
mkdirSync,
|
|
8
12
|
} from "node:fs";
|
|
9
13
|
import { join } from "node:path";
|
|
10
14
|
import { homedir } from "node:os";
|
|
11
15
|
import { agyCommand } from "../shared/agy.js";
|
|
12
|
-
import {
|
|
16
|
+
import { insertTomlKey, removeTomlSections } from "./toml-lite.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 도구별 pluriply MCP 서버 등록 명령. `list`로 이미 등록됐는지 보고 없으면 `add`한다.
|
|
20
|
+
* claude-code 는 워커 템플릿이 --mcp-config 로 인라인 등록하므로 여기 없다.
|
|
21
|
+
* @type {Record<string, {label: string, list: string[], add: (node: string, bin: string) => string[], hint: (bin: string) => string}>}
|
|
22
|
+
*/
|
|
23
|
+
export const MCP_REGISTRARS = {
|
|
24
|
+
codex: {
|
|
25
|
+
label: "Codex",
|
|
26
|
+
list: ["codex", "mcp", "list"],
|
|
27
|
+
add: (node, bin) => [
|
|
28
|
+
"codex",
|
|
29
|
+
"mcp",
|
|
30
|
+
"add",
|
|
31
|
+
"pluriply",
|
|
32
|
+
"--",
|
|
33
|
+
node,
|
|
34
|
+
bin,
|
|
35
|
+
"connector",
|
|
36
|
+
"--agent",
|
|
37
|
+
"codex",
|
|
38
|
+
],
|
|
39
|
+
hint: (bin) =>
|
|
40
|
+
`codex mcp add pluriply -- node "${bin}" connector --agent codex`,
|
|
41
|
+
},
|
|
42
|
+
antigravity: {
|
|
43
|
+
label: "Antigravity CLI",
|
|
44
|
+
// agy mcp add 는 user 스코프가 기본이다. `--` 는 필수: 뒤의 명령·인자가 `-` 로 시작할 수
|
|
45
|
+
// 있고(--agent), 플래그는 <name> 앞에만 올 수 있다(agy mcp add --help).
|
|
46
|
+
list: [agyCommand(), "mcp", "list"],
|
|
47
|
+
add: (node, bin) => [
|
|
48
|
+
agyCommand(),
|
|
49
|
+
"mcp",
|
|
50
|
+
"add",
|
|
51
|
+
"pluriply",
|
|
52
|
+
"--",
|
|
53
|
+
node,
|
|
54
|
+
bin,
|
|
55
|
+
"connector",
|
|
56
|
+
"--agent",
|
|
57
|
+
"antigravity",
|
|
58
|
+
],
|
|
59
|
+
hint: (bin) =>
|
|
60
|
+
`agy mcp add pluriply -- node "${bin}" connector --agent antigravity`,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
13
63
|
|
|
14
64
|
/**
|
|
15
65
|
* 어댑터가 쓰는 환경. 테스트는 exec/fs/homeDir/platform 을 주입한다.
|
|
@@ -24,28 +74,50 @@ export function makeEnv(o = {}) {
|
|
|
24
74
|
platform: process.platform,
|
|
25
75
|
node: process.execPath,
|
|
26
76
|
processEnv: process.env,
|
|
27
|
-
fs: {
|
|
77
|
+
fs: {
|
|
78
|
+
existsSync,
|
|
79
|
+
readFileSync,
|
|
80
|
+
writeFileSync,
|
|
81
|
+
copyFileSync,
|
|
82
|
+
renameSync,
|
|
83
|
+
realpathSync,
|
|
84
|
+
statSync,
|
|
85
|
+
chmodSync,
|
|
86
|
+
mkdirSync,
|
|
87
|
+
},
|
|
28
88
|
...o,
|
|
29
89
|
};
|
|
30
90
|
}
|
|
31
91
|
|
|
32
92
|
/** 테스트용 스킵 변수(구형 PLURIPLY_SKIP_CODEX_MCP 도 인정) */
|
|
33
|
-
function
|
|
93
|
+
export function isSkipped(env) {
|
|
34
94
|
return Boolean(
|
|
35
95
|
env.processEnv.PLURIPLY_SKIP_MCP_REGISTER ||
|
|
36
|
-
|
|
96
|
+
env.processEnv.PLURIPLY_SKIP_CODEX_MCP,
|
|
37
97
|
);
|
|
38
98
|
}
|
|
39
99
|
|
|
100
|
+
/**
|
|
101
|
+
* MCP 도구 호출 타임아웃(초). ask_agent·request_review 의 wait_seconds 상한 300 의 두 배(스펙 §5).
|
|
102
|
+
* Codex `tool_timeout_sec`, Antigravity CLI·IDE `timeoutSeconds` 에 쓴다. Claude Code 는 stdio 유휴
|
|
103
|
+
* 30분에 progress 로 유지되고 Claude Desktop 은 설정 키가 없어 대상이 아니다.
|
|
104
|
+
*/
|
|
105
|
+
export const TOOL_TIMEOUT_SEC = 600;
|
|
106
|
+
|
|
40
107
|
/**
|
|
41
108
|
* @param {"claude-desktop"|"antigravity-ide"} id
|
|
42
109
|
* Antigravity 는 2.x 부터 IDE(`~/.gemini/antigravity-ide`) 와 허브(`~/.gemini/antigravity`) 로
|
|
43
110
|
* 데이터 폴더가 갈라졌다. IDE 폴더가 있으면 그쪽, 없으면(구버전) 옛 경로.
|
|
44
111
|
*/
|
|
45
|
-
export function configPath(
|
|
112
|
+
export function configPath(
|
|
113
|
+
id,
|
|
114
|
+
{ homeDir, platform, processEnv = {}, fs = { existsSync } },
|
|
115
|
+
) {
|
|
46
116
|
if (id === "antigravity-ide") {
|
|
47
117
|
const ideDir = join(homeDir, ".gemini", "antigravity-ide");
|
|
48
|
-
const dir = fs.existsSync(ideDir)
|
|
118
|
+
const dir = fs.existsSync(ideDir)
|
|
119
|
+
? ideDir
|
|
120
|
+
: join(homeDir, ".gemini", "antigravity");
|
|
49
121
|
return join(dir, "mcp_config.json");
|
|
50
122
|
}
|
|
51
123
|
if (platform === "darwin")
|
|
@@ -65,8 +137,132 @@ export function configPath(id, { homeDir, platform, processEnv = {}, fs = { exis
|
|
|
65
137
|
return join(homeDir, ".config", "Claude", "claude_desktop_config.json");
|
|
66
138
|
}
|
|
67
139
|
|
|
140
|
+
/** codex 설정 파일. CODEX_HOME 이 있으면 그 폴더. */
|
|
141
|
+
export function codexConfigPath({ homeDir, processEnv = {} }) {
|
|
142
|
+
return join(processEnv.CODEX_HOME || join(homeDir, ".codex"), "config.toml");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Antigravity CLI(agy)가 `agy mcp add` 로 쓰는 파일 */
|
|
146
|
+
export function agyConfigPath({ homeDir }) {
|
|
147
|
+
return join(homeDir, ".gemini", "config", "mcp_config.json");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Claude Code 의 user-scope(`-s user`) pluriply 등록 여부만 판정한다.
|
|
152
|
+
* `claude mcp list`/`claude mcp get` 은 project-local 등 모든 스코프를 합쳐 보여주므로, 이 저장소처럼
|
|
153
|
+
* project-local 에도 pluriply 가 등록돼 있으면 user 스코프가 비어 있어도 "있다"고 오판해서
|
|
154
|
+
* `setup --remove` 로 user 스코프를 지운 뒤 `setup` 을 다시 돌려도 재등록을 건너뛰게 된다.
|
|
155
|
+
* user-scope 서버는 `~/.claude.json` 최상위 `mcpServers.<name>` 에만 저장되고(프로젝트별 항목은
|
|
156
|
+
* `projects["<경로>"].mcpServers` 아래) 이 파일은 읽기 전용으로만 다룬다 — 여기서 쓰지 않는다.
|
|
157
|
+
* 파일이 없으면(첫 실행 전) missing, 파싱 실패·루트가 객체가 아니면 null(= `mcp list` 폴백).
|
|
158
|
+
* CLAUDE_CONFIG_DIR 가 있으면 Claude Code 가 이 파일을 그 폴더로 옮기므로 홈 대신 그쪽을 본다.
|
|
159
|
+
* @param {object} env
|
|
160
|
+
* @returns {"present"|"missing"|null}
|
|
161
|
+
*/
|
|
162
|
+
function claudeUserScopeStatus(env) {
|
|
163
|
+
const dir = env.processEnv?.CLAUDE_CONFIG_DIR;
|
|
164
|
+
const path = join(dir || env.homeDir, ".claude.json");
|
|
165
|
+
if (!env.fs.existsSync(path)) return "missing";
|
|
166
|
+
try {
|
|
167
|
+
const doc = JSON.parse(env.fs.readFileSync(path, "utf8"));
|
|
168
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
|
|
169
|
+
const servers = doc.mcpServers;
|
|
170
|
+
// hasOwn: 프로토타입 체인을 타지 않는다 — `{"mcpServers":{}}` 의 constructor 같은 상속 키가
|
|
171
|
+
// 등록으로 보이면 안 된다.
|
|
172
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers))
|
|
173
|
+
return "missing";
|
|
174
|
+
return Object.hasOwn(servers, "pluriply") ? "present" : "missing";
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** JSON 설정 파일을 읽는다. 빈 파일은 { mcpServers: {} }, 루트가 객체가 아니면 던진다. */
|
|
181
|
+
function readJsonDoc(env, path) {
|
|
182
|
+
const raw = env.fs.readFileSync(path, "utf8");
|
|
183
|
+
if (raw.trim() === "") return { mcpServers: {} };
|
|
184
|
+
const doc = JSON.parse(raw);
|
|
185
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc))
|
|
186
|
+
throw new Error("config root is not an object");
|
|
187
|
+
return doc;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 대상이 아직 없을 때 쓰는 보수적인 모드. 새 설정 파일에 API 키가 들어갈 수 있다. */
|
|
191
|
+
const FRESH_FILE_MODE = 0o600;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* `.bak` 백업 뒤 tmp+rename 으로 쓴다(JSON·TOML 공용).
|
|
195
|
+
*
|
|
196
|
+
* 두 가지를 지킨다.
|
|
197
|
+
* - **심볼릭 링크는 따라간다**: dotfiles 저장소로 링크된 `~/.codex/config.toml` 을 rename 으로
|
|
198
|
+
* 덮으면 링크가 일반 파일로 바뀌어 dotfiles 사본이 더는 갱신되지 않는다. realpath 로 실제
|
|
199
|
+
* 파일을 찾아 그 폴더에서 tmp+rename 하므로 링크는 링크로 남고 대상 파일이 갱신된다.
|
|
200
|
+
* - **원본 모드를 지킨다**: 이 파일들엔 API 키·다른 MCP 서버의 env 가 들어 있어 사용자가
|
|
201
|
+
* 0600 으로 잠가두는 일이 흔하다. 기본 모드로 쓰면 umask 022 에서 0644(전체 읽기)로 넓어진다.
|
|
202
|
+
* `writeFileSync` 의 mode 옵션은 umask 에 깎이므로 `chmodSync` 로 한 번 더 못박는다.
|
|
203
|
+
* 대상이 아직 없으면(같은 실행에서 방금 만든 JSON) 0600 으로 시작한다.
|
|
204
|
+
*/
|
|
205
|
+
function writeFileAtomic(env, path, content) {
|
|
206
|
+
const fs = env.fs;
|
|
207
|
+
let target = path;
|
|
208
|
+
let mode = FRESH_FILE_MODE;
|
|
209
|
+
let exists = false;
|
|
210
|
+
try {
|
|
211
|
+
exists = fs.existsSync(path);
|
|
212
|
+
} catch {
|
|
213
|
+
exists = false;
|
|
214
|
+
}
|
|
215
|
+
if (exists) {
|
|
216
|
+
// realpath 가 없는 주입 fs(테스트) 나 경합으로 사라진 파일은 원래 경로 그대로 쓴다.
|
|
217
|
+
try {
|
|
218
|
+
target = fs.realpathSync ? fs.realpathSync(path) : path;
|
|
219
|
+
} catch {
|
|
220
|
+
target = path;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
mode = fs.statSync(target).mode & 0o777;
|
|
224
|
+
} catch {
|
|
225
|
+
mode = FRESH_FILE_MODE;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (exists) {
|
|
229
|
+
fs.copyFileSync(target, `${target}.bak`);
|
|
230
|
+
// 이미 있던 `.bak` 은 copyFileSync 가 모드를 물려받지 않으므로 직접 맞춘다.
|
|
231
|
+
chmodQuiet(fs, `${target}.bak`, mode);
|
|
232
|
+
}
|
|
233
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
234
|
+
fs.writeFileSync(tmp, content, { mode });
|
|
235
|
+
chmodQuiet(fs, tmp, mode);
|
|
236
|
+
fs.renameSync(tmp, target);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** chmod 을 못 쓰는 파일시스템(일부 Windows 공유)에서 쓰기 자체가 실패하면 안 된다. */
|
|
240
|
+
function chmodQuiet(fs, path, mode) {
|
|
241
|
+
try {
|
|
242
|
+
fs.chmodSync?.(path, mode);
|
|
243
|
+
} catch {
|
|
244
|
+
// 모드 고정 실패는 치명적이지 않다 — 내용은 이미 쓰였다
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function writeJsonAtomic(env, path, doc) {
|
|
249
|
+
writeFileAtomic(env, path, JSON.stringify(doc, null, 2) + "\n");
|
|
250
|
+
}
|
|
251
|
+
|
|
68
252
|
/** CLI 가 자체 `mcp list/add` 를 제공하는 클라이언트 */
|
|
69
|
-
function cliClient({
|
|
253
|
+
function cliClient({
|
|
254
|
+
id,
|
|
255
|
+
label,
|
|
256
|
+
agent,
|
|
257
|
+
command,
|
|
258
|
+
list,
|
|
259
|
+
add,
|
|
260
|
+
remove,
|
|
261
|
+
hint,
|
|
262
|
+
afterAdd,
|
|
263
|
+
afterRemove,
|
|
264
|
+
statusOverride,
|
|
265
|
+
}) {
|
|
70
266
|
const cmd = () => (typeof command === "function" ? command() : command);
|
|
71
267
|
return {
|
|
72
268
|
id,
|
|
@@ -75,7 +271,8 @@ function cliClient({ id, label, agent, command, list, add, hint }) {
|
|
|
75
271
|
kind: "cli",
|
|
76
272
|
detect(env) {
|
|
77
273
|
try {
|
|
78
|
-
const out = env
|
|
274
|
+
const out = env
|
|
275
|
+
.exec(cmd(), ["--version"], { stdio: "pipe" })
|
|
79
276
|
.toString()
|
|
80
277
|
.trim();
|
|
81
278
|
return { installed: true, detail: out.split("\n")[0] };
|
|
@@ -84,6 +281,12 @@ function cliClient({ id, label, agent, command, list, add, hint }) {
|
|
|
84
281
|
}
|
|
85
282
|
},
|
|
86
283
|
status(env) {
|
|
284
|
+
// 스코프 인식이 필요한 클라이언트(claude-code)는 statusOverride 가 먼저 판정하고,
|
|
285
|
+
// null 을 돌려줄 때만(파일 없음 이외의 이유로 못 읽을 때) 기존 `mcp list` 판정으로 폴백한다.
|
|
286
|
+
if (statusOverride) {
|
|
287
|
+
const overridden = statusOverride(env);
|
|
288
|
+
if (overridden != null) return overridden;
|
|
289
|
+
}
|
|
87
290
|
try {
|
|
88
291
|
const out = env.exec(cmd(), list, { stdio: "pipe" }).toString();
|
|
89
292
|
return out.includes("pluriply") ? "present" : "missing";
|
|
@@ -92,36 +295,81 @@ function cliClient({ id, label, agent, command, list, add, hint }) {
|
|
|
92
295
|
}
|
|
93
296
|
},
|
|
94
297
|
register(env) {
|
|
95
|
-
if (
|
|
298
|
+
if (isSkipped(env)) return "skipped";
|
|
96
299
|
const st = this.status(env);
|
|
97
300
|
if (st === "present") return "present";
|
|
98
301
|
if (typeof st === "object") {
|
|
99
|
-
env.log(
|
|
302
|
+
env.log(
|
|
303
|
+
`hint: make sure ${label} has pluriply registered: ${hint(env.binPath)}`,
|
|
304
|
+
);
|
|
100
305
|
return "failed";
|
|
101
306
|
}
|
|
102
307
|
try {
|
|
103
308
|
env.exec(cmd(), add(env.node, env.binPath), { stdio: "pipe" });
|
|
104
|
-
env.log(`registered pluriply MCP server in ${label}`);
|
|
105
|
-
return "registered";
|
|
106
309
|
} catch {
|
|
107
|
-
env.log(
|
|
310
|
+
env.log(
|
|
311
|
+
`hint: make sure ${label} has pluriply registered: ${hint(env.binPath)}`,
|
|
312
|
+
);
|
|
108
313
|
return "failed";
|
|
109
314
|
}
|
|
315
|
+
if (afterAdd) {
|
|
316
|
+
const r = afterAdd(env);
|
|
317
|
+
if (!r.ok) {
|
|
318
|
+
// 타임아웃을 못 쓰면 등록을 되돌려 missing 으로 남긴다(스펙 D4). registered 는 항상 완전한 상태다.
|
|
319
|
+
let rolledBack = true;
|
|
320
|
+
try {
|
|
321
|
+
env.exec(cmd(), remove, { stdio: "pipe" });
|
|
322
|
+
} catch {
|
|
323
|
+
// 되돌리기 자체가 실패하면 등록이 타임아웃 없이 남는다 — 메시지로 분명히 알린다
|
|
324
|
+
rolledBack = false;
|
|
325
|
+
}
|
|
326
|
+
env.log(
|
|
327
|
+
rolledBack
|
|
328
|
+
? `could not configure ${label}: ${r.reason}`
|
|
329
|
+
: `could not configure ${label}: ${r.reason} (registration left in place; run "pluriply setup --remove")`,
|
|
330
|
+
);
|
|
331
|
+
return `failed: ${r.reason}`;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
env.log(`registered pluriply MCP server in ${label}`);
|
|
335
|
+
return "registered";
|
|
336
|
+
},
|
|
337
|
+
unregister(env) {
|
|
338
|
+
if (isSkipped(env)) return "skipped";
|
|
339
|
+
const st = this.status(env);
|
|
340
|
+
if (st === "missing") return "absent";
|
|
341
|
+
if (typeof st === "object") {
|
|
342
|
+
env.log(`cannot check ${label}: ${st.error}`);
|
|
343
|
+
return "failed";
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
env.exec(cmd(), remove, { stdio: "pipe" });
|
|
347
|
+
} catch {
|
|
348
|
+
env.log(
|
|
349
|
+
`hint: remove pluriply from ${label} manually: ${cmd()} ${remove.join(" ")}`,
|
|
350
|
+
);
|
|
351
|
+
return "failed";
|
|
352
|
+
}
|
|
353
|
+
if (afterRemove) {
|
|
354
|
+
try {
|
|
355
|
+
afterRemove(env);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
env.log(
|
|
358
|
+
`hint: pluriply was removed from ${label} but leftover config could not be cleaned: ${err.message}`,
|
|
359
|
+
);
|
|
360
|
+
return "failed";
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
env.log(`removed pluriply MCP server from ${label}`);
|
|
364
|
+
return "removed";
|
|
110
365
|
},
|
|
111
366
|
};
|
|
112
367
|
}
|
|
113
368
|
|
|
114
369
|
/** `mcpServers` JSON 설정 파일을 쓰는 데스크톱 앱 */
|
|
115
|
-
function jsonClient({ id, label, agent }) {
|
|
370
|
+
function jsonClient({ id, label, agent, entryExtras = {} }) {
|
|
116
371
|
const file = (env) => configPath(id, env);
|
|
117
|
-
const read = (env) =>
|
|
118
|
-
const raw = env.fs.readFileSync(file(env), "utf8");
|
|
119
|
-
if (raw.trim() === "") return { mcpServers: {} };
|
|
120
|
-
const doc = JSON.parse(raw);
|
|
121
|
-
if (!doc || typeof doc !== "object" || Array.isArray(doc))
|
|
122
|
-
throw new Error("config root is not an object");
|
|
123
|
-
return doc;
|
|
124
|
-
};
|
|
372
|
+
const read = (env) => readJsonDoc(env, file(env));
|
|
125
373
|
return {
|
|
126
374
|
id,
|
|
127
375
|
label,
|
|
@@ -140,7 +388,7 @@ function jsonClient({ id, label, agent }) {
|
|
|
140
388
|
}
|
|
141
389
|
},
|
|
142
390
|
register(env) {
|
|
143
|
-
if (
|
|
391
|
+
if (isSkipped(env)) return "skipped";
|
|
144
392
|
const st = this.status(env);
|
|
145
393
|
if (st === "present") return "present";
|
|
146
394
|
if (typeof st === "object") {
|
|
@@ -155,15 +403,38 @@ function jsonClient({ id, label, agent }) {
|
|
|
155
403
|
pluriply: {
|
|
156
404
|
command: env.node,
|
|
157
405
|
args: [env.binPath, "connector", "--agent", agent],
|
|
406
|
+
...entryExtras,
|
|
158
407
|
},
|
|
159
408
|
};
|
|
160
|
-
env
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
env.log(`registered pluriply MCP server in ${label} (${path}, backup ${path}.bak)`);
|
|
409
|
+
writeJsonAtomic(env, path, doc);
|
|
410
|
+
env.log(
|
|
411
|
+
`registered pluriply MCP server in ${label} (${path}, backup ${path}.bak)`,
|
|
412
|
+
);
|
|
165
413
|
return "registered";
|
|
166
414
|
},
|
|
415
|
+
unregister(env) {
|
|
416
|
+
if (isSkipped(env)) return "skipped";
|
|
417
|
+
const st = this.status(env);
|
|
418
|
+
if (st === "missing") return "absent";
|
|
419
|
+
if (typeof st === "object") {
|
|
420
|
+
env.log(`cannot update ${label} config: ${st.error}`);
|
|
421
|
+
return "failed";
|
|
422
|
+
}
|
|
423
|
+
const path = file(env);
|
|
424
|
+
try {
|
|
425
|
+
const doc = read(env);
|
|
426
|
+
const { pluriply: _removed, ...rest } = doc.mcpServers ?? {};
|
|
427
|
+
doc.mcpServers = rest;
|
|
428
|
+
writeJsonAtomic(env, path, doc);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
env.log(`cannot update ${label} config: ${err.message}`);
|
|
431
|
+
return "failed";
|
|
432
|
+
}
|
|
433
|
+
env.log(
|
|
434
|
+
`removed pluriply MCP server from ${label} (${path}, backup ${path}.bak)`,
|
|
435
|
+
);
|
|
436
|
+
return "removed";
|
|
437
|
+
},
|
|
167
438
|
};
|
|
168
439
|
}
|
|
169
440
|
|
|
@@ -175,9 +446,22 @@ export const CLIENTS = [
|
|
|
175
446
|
command: "claude",
|
|
176
447
|
list: ["mcp", "list"],
|
|
177
448
|
add: (node, bin) => [
|
|
178
|
-
"mcp",
|
|
449
|
+
"mcp",
|
|
450
|
+
"add",
|
|
451
|
+
"-s",
|
|
452
|
+
"user",
|
|
453
|
+
"pluriply",
|
|
454
|
+
"--",
|
|
455
|
+
node,
|
|
456
|
+
bin,
|
|
457
|
+
"connector",
|
|
458
|
+
"--agent",
|
|
459
|
+
"claude-code",
|
|
179
460
|
],
|
|
180
|
-
hint: (bin) =>
|
|
461
|
+
hint: (bin) =>
|
|
462
|
+
`claude mcp add -s user pluriply -- node "${bin}" connector --agent claude-code`,
|
|
463
|
+
remove: ["mcp", "remove", "-s", "user", "pluriply"],
|
|
464
|
+
statusOverride: claudeUserScopeStatus,
|
|
181
465
|
}),
|
|
182
466
|
cliClient({
|
|
183
467
|
id: "codex",
|
|
@@ -187,6 +471,57 @@ export const CLIENTS = [
|
|
|
187
471
|
list: MCP_REGISTRARS.codex.list.slice(1),
|
|
188
472
|
add: (node, bin) => MCP_REGISTRARS.codex.add(node, bin).slice(1),
|
|
189
473
|
hint: MCP_REGISTRARS.codex.hint,
|
|
474
|
+
remove: ["mcp", "remove", "pluriply"],
|
|
475
|
+
// codex mcp add 는 타임아웃 플래그가 없어 config.toml 을 직접 편집한다(스펙 §5)
|
|
476
|
+
afterAdd(env) {
|
|
477
|
+
const path = codexConfigPath(env);
|
|
478
|
+
try {
|
|
479
|
+
if (!env.fs.existsSync(path))
|
|
480
|
+
return {
|
|
481
|
+
ok: false,
|
|
482
|
+
reason: `tool_timeout_sec not written (${path} missing)`,
|
|
483
|
+
};
|
|
484
|
+
const r = insertTomlKey(
|
|
485
|
+
env.fs.readFileSync(path, "utf8"),
|
|
486
|
+
"mcp_servers.pluriply",
|
|
487
|
+
"tool_timeout_sec",
|
|
488
|
+
String(TOOL_TIMEOUT_SEC),
|
|
489
|
+
);
|
|
490
|
+
if (r.reason === "no-header")
|
|
491
|
+
return {
|
|
492
|
+
ok: false,
|
|
493
|
+
reason: `tool_timeout_sec not written ([mcp_servers.pluriply] not found in ${path})`,
|
|
494
|
+
};
|
|
495
|
+
if (r.reason === "unsupported")
|
|
496
|
+
return {
|
|
497
|
+
ok: false,
|
|
498
|
+
reason: `tool_timeout_sec not written (${path} contains triple-quoted strings; add \`tool_timeout_sec = ${TOOL_TIMEOUT_SEC}\` under [mcp_servers.pluriply] by hand)`,
|
|
499
|
+
};
|
|
500
|
+
if (r.changed) writeFileAtomic(env, path, r.text);
|
|
501
|
+
return { ok: true };
|
|
502
|
+
} catch (err) {
|
|
503
|
+
return {
|
|
504
|
+
ok: false,
|
|
505
|
+
reason: `tool_timeout_sec not written (${err.message})`,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
// codex mcp remove 가 하위 테이블([mcp_servers.pluriply.tools.*])을 남길 수 있어 마저 지운다
|
|
510
|
+
afterRemove(env) {
|
|
511
|
+
const path = codexConfigPath(env);
|
|
512
|
+
if (!env.fs.existsSync(path)) return;
|
|
513
|
+
const { text, removed, reason } = removeTomlSections(
|
|
514
|
+
env.fs.readFileSync(path, "utf8"),
|
|
515
|
+
"mcp_servers.pluriply",
|
|
516
|
+
);
|
|
517
|
+
if (reason === "unsupported") {
|
|
518
|
+
env.log(
|
|
519
|
+
`hint: ${path} contains triple-quoted strings; if any [mcp_servers.pluriply*] sections remain, remove them by hand`,
|
|
520
|
+
);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (removed > 0) writeFileAtomic(env, path, text);
|
|
524
|
+
},
|
|
190
525
|
}),
|
|
191
526
|
cliClient({
|
|
192
527
|
id: "antigravity",
|
|
@@ -196,7 +531,40 @@ export const CLIENTS = [
|
|
|
196
531
|
list: MCP_REGISTRARS.antigravity.list.slice(1),
|
|
197
532
|
add: (node, bin) => MCP_REGISTRARS.antigravity.add(node, bin).slice(1),
|
|
198
533
|
hint: MCP_REGISTRARS.antigravity.hint,
|
|
534
|
+
remove: ["mcp", "remove", "pluriply"],
|
|
535
|
+
// agy mcp add 도 타임아웃 플래그가 없다. agy 는 JSONC 를 읽지만 우리는 JSON.parse 만 쓴다(스펙 §11).
|
|
536
|
+
afterAdd(env) {
|
|
537
|
+
const path = agyConfigPath(env);
|
|
538
|
+
try {
|
|
539
|
+
const doc = readJsonDoc(env, path);
|
|
540
|
+
const entry = doc.mcpServers?.pluriply;
|
|
541
|
+
if (!entry || typeof entry !== "object")
|
|
542
|
+
return {
|
|
543
|
+
ok: false,
|
|
544
|
+
reason: `timeoutSeconds not written (mcpServers.pluriply not found in ${path})`,
|
|
545
|
+
};
|
|
546
|
+
if (entry.timeoutSeconds === undefined) {
|
|
547
|
+
entry.timeoutSeconds = TOOL_TIMEOUT_SEC;
|
|
548
|
+
writeJsonAtomic(env, path, doc);
|
|
549
|
+
}
|
|
550
|
+
return { ok: true };
|
|
551
|
+
} catch (err) {
|
|
552
|
+
return {
|
|
553
|
+
ok: false,
|
|
554
|
+
reason: `timeoutSeconds not written (${err.message})`,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
}),
|
|
559
|
+
jsonClient({
|
|
560
|
+
id: "claude-desktop",
|
|
561
|
+
label: "Claude Desktop",
|
|
562
|
+
agent: "claude-desktop",
|
|
563
|
+
}),
|
|
564
|
+
jsonClient({
|
|
565
|
+
id: "antigravity-ide",
|
|
566
|
+
label: "Antigravity IDE",
|
|
567
|
+
agent: "antigravity-ide",
|
|
568
|
+
entryExtras: { timeoutSeconds: TOOL_TIMEOUT_SEC },
|
|
199
569
|
}),
|
|
200
|
-
jsonClient({ id: "claude-desktop", label: "Claude Desktop", agent: "claude-desktop" }),
|
|
201
|
-
jsonClient({ id: "antigravity-ide", label: "Antigravity IDE", agent: "antigravity-ide" }),
|
|
202
570
|
];
|