pluriply 0.5.1 → 0.5.3
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/package.json +1 -1
- package/src/connector/tools.js +147 -49
- package/src/setup/clients.js +15 -5
package/package.json
CHANGED
package/src/connector/tools.js
CHANGED
|
@@ -51,6 +51,28 @@ function staleHubMessage(stale) {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** 허브가 hello 하지 않은 연결의 요청을 거절할 때 쓰는 문구(server.js #conn) */
|
|
55
|
+
const UNIDENTIFIED = "say hello first";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* hub-client 자신이 내는 연결 오류(허브의 거절이 아니다). 원인이 정체성이 아니므로 "정체성을
|
|
59
|
+
* 잃었다"로 감싸지 않고 원래 오류를 그대로 알린다: 끊김·시간 초과는 재접속 리스너가 곧
|
|
60
|
+
* 되살리고, unreachable(dead)은 이미 원인(예: unauthorized)과 재시작 안내를 담고 있다.
|
|
61
|
+
*/
|
|
62
|
+
const HUB_CLIENT_ERROR =
|
|
63
|
+
/^hub (connection closed|connection error|request timed out|unreachable)/;
|
|
64
|
+
|
|
65
|
+
/** 정체성을 되살리지 못했다 — 도구를 다시 시작해야 한다(안내 문구를 다른 안내로 감싸지 않게 구분한다) */
|
|
66
|
+
class IdentityLostError extends Error {
|
|
67
|
+
/** @param {string} reason */
|
|
68
|
+
constructor(reason) {
|
|
69
|
+
super(
|
|
70
|
+
`Pluriply lost this session's identity on the hub connection and could not restore it (${reason}). ` +
|
|
71
|
+
"Restart this AI tool (or reload its MCP server) to reconnect.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
/**
|
|
55
77
|
* 허브에 정체성을 알리고 인스턴스 ID를 받는다. 재접속 때는 알고 있는 ID를 실어 그대로 인정받는다.
|
|
56
78
|
* @param {import('./hub-client.js').HubClient} hub
|
|
@@ -90,12 +112,92 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
90
112
|
const delegationDepth = () =>
|
|
91
113
|
worker ? Number(process.env.PLURIPLY_DEPTH ?? 0) + 1 : 0;
|
|
92
114
|
|
|
115
|
+
/** 이 소켓에 알고 있는 instanceId 로 정체성을 다시 알린다 @param {{duringReconnect?: boolean}} [opts] */
|
|
116
|
+
async function helloAgain(opts = {}) {
|
|
117
|
+
state.instanceId = await hello(hub, {
|
|
118
|
+
agent,
|
|
119
|
+
worker,
|
|
120
|
+
instanceId: state.instanceId,
|
|
121
|
+
...opts,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 참여 중이던 채널에 이 소켓으로 다시 들어간다.
|
|
127
|
+
* @param {{duringReconnect?: boolean}} [opts] @returns {Promise<boolean>} 채널이 없으면 false
|
|
128
|
+
*/
|
|
129
|
+
async function rejoin(opts = {}) {
|
|
130
|
+
if (!state.currentChannel) return false;
|
|
131
|
+
await hub.request(
|
|
132
|
+
"channel.join",
|
|
133
|
+
{ channelCode: state.currentChannel },
|
|
134
|
+
opts,
|
|
135
|
+
);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 진행 중인 정체성 복구. 동시에 거절된 요청들이 hello 하나를 같이 기다린다. */
|
|
140
|
+
let recovering = null;
|
|
141
|
+
/**
|
|
142
|
+
* 재접속 리스너의 hello 가 실패해 이 소켓에 정체성이 없다고 알고 있는 상태. 조회 도구가 쓰는
|
|
143
|
+
* 허브 요청(channel.peers·task.list 등)은 정체성 없이도 통과하므로, 거절을 기다리면 복구가
|
|
144
|
+
* 일어나지 않고 이 인스턴스가 동료에게 offline 으로 남는다 — 그래서 요청 전에 먼저 복구한다.
|
|
145
|
+
*/
|
|
146
|
+
let identityLost = false;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 정체성을 되살리고 참여 중이던 채널에 다시 들어간다(hello 는 연결당 멱등).
|
|
150
|
+
* 재접속 리스너는 이 공유 promise 를 쓰면 안 된다 — 여기서 나가는 요청은 재접속 배리어를
|
|
151
|
+
* 기다리고, 배리어는 리스너가 끝나기를 기다리므로 서로를 기다리는 교착이 된다.
|
|
152
|
+
* @returns {Promise<void>} hello 가 허브에 거절되면 IdentityLostError, 연결 오류면 그 오류 그대로
|
|
153
|
+
*/
|
|
154
|
+
function recover() {
|
|
155
|
+
recovering ??= (async () => {
|
|
156
|
+
try {
|
|
157
|
+
await helloAgain();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (HUB_CLIENT_ERROR.test(err.message)) throw err;
|
|
160
|
+
throw new IdentityLostError(err.message);
|
|
161
|
+
}
|
|
162
|
+
identityLost = false;
|
|
163
|
+
// 채널이 사라졌으면 다시 보낸 요청(또는 다음 호출)이 알려준다
|
|
164
|
+
await rejoin().catch(() => {});
|
|
165
|
+
})().finally(() => {
|
|
166
|
+
recovering = null;
|
|
167
|
+
});
|
|
168
|
+
return recovering;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* hub.request 와 같되 이 소켓의 정체성을 지킨다. 정체성을 잃은 것을 알면 보내기 전에 복구하고,
|
|
173
|
+
* 허브가 "say hello first" 로 거절하면 복구한 뒤 한 번만 다시 보낸다. 허브는 정체성이 필요한
|
|
174
|
+
* 요청을 첫 줄(#conn)에서 거절하므로 다시 보내도 중복 부작용이 없다.
|
|
175
|
+
* 재접속 리스너의 duringReconnect 요청은 이 경로를 타지 않는다.
|
|
176
|
+
* @type {typeof hub.request}
|
|
177
|
+
*/
|
|
178
|
+
async function hubRequest(type, payload, opts) {
|
|
179
|
+
if (identityLost) await recover();
|
|
180
|
+
try {
|
|
181
|
+
return await hub.request(type, payload, opts);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (err.message !== UNIDENTIFIED) throw err;
|
|
184
|
+
}
|
|
185
|
+
await recover();
|
|
186
|
+
try {
|
|
187
|
+
return await hub.request(type, payload, opts);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err.message === UNIDENTIFIED)
|
|
190
|
+
throw new IdentityLostError(err.message);
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
93
195
|
/**
|
|
94
196
|
* 채널이 없을 때 허브에 직전 채널 복귀를 요청한다.
|
|
95
197
|
* @returns {Promise<string|null>} 복귀한 채널 코드
|
|
96
198
|
*/
|
|
97
199
|
async function resumeChannel() {
|
|
98
|
-
const { channelCode } = await
|
|
200
|
+
const { channelCode } = await hubRequest("agent.resume", {});
|
|
99
201
|
if (!channelCode) return null;
|
|
100
202
|
if (state.currentChannel) return null; // join_channel이 경합에서 이겼으니 그대로 둔다
|
|
101
203
|
state.currentChannel = channelCode;
|
|
@@ -113,6 +215,8 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
113
215
|
try {
|
|
114
216
|
resumed = await resumeChannel();
|
|
115
217
|
} catch (err) {
|
|
218
|
+
// join_channel 도 같은 이유로 실패하므로 그쪽을 권하지 않고 재시작 안내만 낸다
|
|
219
|
+
if (err instanceof IdentityLostError) return fail(err.message);
|
|
116
220
|
return fail(
|
|
117
221
|
`Join a channel first with join_channel. (auto-resume failed: ${err.message})`,
|
|
118
222
|
);
|
|
@@ -147,7 +251,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
147
251
|
const remaining = deadline - Date.now();
|
|
148
252
|
if (remaining <= 0) break;
|
|
149
253
|
const timeoutMs = Math.min(ASK_CHUNK_MS, remaining);
|
|
150
|
-
({ task } = await
|
|
254
|
+
({ task } = await hubRequest(
|
|
151
255
|
"task.wait",
|
|
152
256
|
{ channelCode: code, taskId, timeoutMs },
|
|
153
257
|
{ timeoutMs: timeoutMs + 5000 },
|
|
@@ -200,19 +304,17 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
200
304
|
// join_channel이 도구를 다시 시작할 때까지 "say hello first"로 막힌다.
|
|
201
305
|
// (hello는 인증이 필요한 요청이라, 토큰이 틀린 연결은 여기서 unauthorized를
|
|
202
306
|
// 받아 hub-client의 재접속 판정이 그것을 본다.)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
);
|
|
215
|
-
hub.emit("rejoined", state.currentChannel);
|
|
307
|
+
// recover() 의 공유 promise 는 쓰지 않는다(교착 — recover 주석 참고).
|
|
308
|
+
await helloAgain({ duringReconnect: true });
|
|
309
|
+
identityLost = false;
|
|
310
|
+
} catch {
|
|
311
|
+
// 다음 도구 호출이 요청을 보내기 전에 hubRequest() 에서 복구한다
|
|
312
|
+
identityLost = true;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
if (await rejoin({ duringReconnect: true }))
|
|
317
|
+
hub.emit("rejoined", state.currentChannel);
|
|
216
318
|
} catch {
|
|
217
319
|
// 채널이 사라졌으면 다음 도구 호출이 알려준다
|
|
218
320
|
}
|
|
@@ -234,8 +336,8 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
234
336
|
if (hub.stale) return fail(staleHubMessage(hub.stale));
|
|
235
337
|
try {
|
|
236
338
|
const code =
|
|
237
|
-
channel_code ?? (await
|
|
238
|
-
const { peers } = await
|
|
339
|
+
channel_code ?? (await hubRequest("channel.create")).channelCode;
|
|
340
|
+
const { peers } = await hubRequest("channel.join", {
|
|
239
341
|
channelCode: code,
|
|
240
342
|
});
|
|
241
343
|
state.currentChannel = code;
|
|
@@ -255,7 +357,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
255
357
|
inputSchema: {},
|
|
256
358
|
},
|
|
257
359
|
needChannel((_args, code) =>
|
|
258
|
-
|
|
360
|
+
hubRequest("channel.peers", { channelCode: code }),
|
|
259
361
|
),
|
|
260
362
|
);
|
|
261
363
|
|
|
@@ -268,15 +370,15 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
268
370
|
inputSchema: {},
|
|
269
371
|
},
|
|
270
372
|
needChannel(async (_args, code) => {
|
|
271
|
-
const { peers } = await
|
|
373
|
+
const { peers } = await hubRequest("channel.peers", {
|
|
272
374
|
channelCode: code,
|
|
273
375
|
});
|
|
274
|
-
const { tasks } = await
|
|
376
|
+
const { tasks } = await hubRequest("task.list", {
|
|
275
377
|
channelCode: code,
|
|
276
378
|
to: state.instanceId,
|
|
277
379
|
status: "submitted",
|
|
278
380
|
});
|
|
279
|
-
const { running } = await
|
|
381
|
+
const { running } = await hubRequest("worker.status", {
|
|
280
382
|
channelCode: code,
|
|
281
383
|
});
|
|
282
384
|
return {
|
|
@@ -335,7 +437,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
335
437
|
{ to, request, attachments = [], mode = "auto", cwd = process.cwd() },
|
|
336
438
|
code,
|
|
337
439
|
) =>
|
|
338
|
-
|
|
440
|
+
hubRequest("task.create", {
|
|
339
441
|
channelCode: code,
|
|
340
442
|
to,
|
|
341
443
|
request,
|
|
@@ -399,7 +501,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
399
501
|
`ask_agent takes a tool name (e.g. "codex"); use send_task to pin an instance like "${to}"`,
|
|
400
502
|
);
|
|
401
503
|
const waitS = clampWaitSeconds(wait_seconds);
|
|
402
|
-
const created = await
|
|
504
|
+
const created = await hubRequest("task.create", {
|
|
403
505
|
channelCode: code,
|
|
404
506
|
to,
|
|
405
507
|
request,
|
|
@@ -412,13 +514,11 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
412
514
|
const { taskId } = created;
|
|
413
515
|
if (created.dispatch !== "spawned" && created.dispatch !== "queued") {
|
|
414
516
|
// 워커가 뜨지 않았다: 쓰레기 submitted 태스크를 남기지 않는다
|
|
415
|
-
await
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
})
|
|
421
|
-
.catch(() => {});
|
|
517
|
+
await hubRequest("task.cancel", {
|
|
518
|
+
channelCode: code,
|
|
519
|
+
taskId,
|
|
520
|
+
reason: "ask_agent: worker not started",
|
|
521
|
+
}).catch(() => {});
|
|
422
522
|
return {
|
|
423
523
|
status: "not_started",
|
|
424
524
|
taskId,
|
|
@@ -510,7 +610,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
510
610
|
if (git_range !== undefined) review.gitRange = git_range;
|
|
511
611
|
if (paths !== undefined) review.paths = paths;
|
|
512
612
|
if (focus !== undefined) review.focus = focus;
|
|
513
|
-
const created = await
|
|
613
|
+
const created = await hubRequest("task.create", {
|
|
514
614
|
channelCode: code,
|
|
515
615
|
to,
|
|
516
616
|
request: request ?? "",
|
|
@@ -525,13 +625,11 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
525
625
|
if (wait_seconds === undefined) return created;
|
|
526
626
|
const { taskId } = created;
|
|
527
627
|
if (created.dispatch === "none") {
|
|
528
|
-
await
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
})
|
|
534
|
-
.catch(() => {});
|
|
628
|
+
await hubRequest("task.cancel", {
|
|
629
|
+
channelCode: code,
|
|
630
|
+
taskId,
|
|
631
|
+
reason: "request_review: worker not started",
|
|
632
|
+
}).catch(() => {});
|
|
535
633
|
return {
|
|
536
634
|
status: "not_started",
|
|
537
635
|
taskId,
|
|
@@ -567,7 +665,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
567
665
|
},
|
|
568
666
|
},
|
|
569
667
|
needChannel(({ task_id, reason }, code) =>
|
|
570
|
-
|
|
668
|
+
hubRequest("task.cancel", {
|
|
571
669
|
channelCode: code,
|
|
572
670
|
taskId: task_id,
|
|
573
671
|
reason,
|
|
@@ -602,7 +700,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
602
700
|
},
|
|
603
701
|
needChannel(
|
|
604
702
|
({ status, mine_only = true, sent_by_me = false, kind }, code) =>
|
|
605
|
-
|
|
703
|
+
hubRequest("task.list", {
|
|
606
704
|
channelCode: code,
|
|
607
705
|
to: sent_by_me ? undefined : mine_only ? state.instanceId : undefined,
|
|
608
706
|
from: sent_by_me ? state.instanceId : undefined,
|
|
@@ -621,7 +719,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
621
719
|
inputSchema: { task_id: z.string() },
|
|
622
720
|
},
|
|
623
721
|
needChannel(({ task_id }, code) =>
|
|
624
|
-
|
|
722
|
+
hubRequest("task.get", { channelCode: code, taskId: task_id }),
|
|
625
723
|
),
|
|
626
724
|
);
|
|
627
725
|
|
|
@@ -645,17 +743,17 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
645
743
|
},
|
|
646
744
|
},
|
|
647
745
|
needChannel(async ({ task_id, result, failed = false }, code) => {
|
|
648
|
-
const { task } = await
|
|
746
|
+
const { task } = await hubRequest("task.get", {
|
|
649
747
|
channelCode: code,
|
|
650
748
|
taskId: task_id,
|
|
651
749
|
});
|
|
652
750
|
if (task.status === "submitted") {
|
|
653
|
-
await
|
|
751
|
+
await hubRequest("task.claim", {
|
|
654
752
|
channelCode: code,
|
|
655
753
|
taskId: task_id,
|
|
656
754
|
});
|
|
657
755
|
}
|
|
658
|
-
return
|
|
756
|
+
return hubRequest("task.complete", {
|
|
659
757
|
channelCode: code,
|
|
660
758
|
taskId: task_id,
|
|
661
759
|
result,
|
|
@@ -691,14 +789,14 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
691
789
|
},
|
|
692
790
|
},
|
|
693
791
|
needChannel(async ({ task_id, verdict, findings = [], summary }, code) => {
|
|
694
|
-
const { task } = await
|
|
792
|
+
const { task } = await hubRequest("task.get", {
|
|
695
793
|
channelCode: code,
|
|
696
794
|
taskId: task_id,
|
|
697
795
|
});
|
|
698
796
|
if (task.status === "submitted") {
|
|
699
|
-
await
|
|
797
|
+
await hubRequest("task.claim", { channelCode: code, taskId: task_id });
|
|
700
798
|
}
|
|
701
|
-
return
|
|
799
|
+
return hubRequest("task.complete", {
|
|
702
800
|
channelCode: code,
|
|
703
801
|
taskId: task_id,
|
|
704
802
|
status: "completed",
|
|
@@ -722,7 +820,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
722
820
|
},
|
|
723
821
|
},
|
|
724
822
|
needChannel(({ summary, artifacts = [] }, code) =>
|
|
725
|
-
|
|
823
|
+
hubRequest("context.add", {
|
|
726
824
|
channelCode: code,
|
|
727
825
|
summary,
|
|
728
826
|
artifacts,
|
|
@@ -744,7 +842,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
744
842
|
},
|
|
745
843
|
},
|
|
746
844
|
needChannel(({ limit }, code) =>
|
|
747
|
-
|
|
845
|
+
hubRequest("context.list", { channelCode: code, limit }),
|
|
748
846
|
),
|
|
749
847
|
);
|
|
750
848
|
}
|
package/src/setup/clients.js
CHANGED
|
@@ -310,7 +310,8 @@ function cliClient({
|
|
|
310
310
|
// 안내만 한다 — 되돌리면 멀쩡한 등록을 지우게 된다.
|
|
311
311
|
if (afterAdd) {
|
|
312
312
|
const r = afterAdd(env);
|
|
313
|
-
if (!r.ok)
|
|
313
|
+
if (!r.ok)
|
|
314
|
+
env.log(`hint: ${label}: ${r.reason}${r.fix ? ` — ${r.fix}` : ""}`);
|
|
314
315
|
else if (r.changed)
|
|
315
316
|
env.log(`updated ${label}: added missing timeout settings`);
|
|
316
317
|
}
|
|
@@ -493,11 +494,14 @@ export const CLIENTS = [
|
|
|
493
494
|
// codex mcp add 는 타임아웃 플래그가 없어 config.toml 을 직접 편집한다(스펙 §5)
|
|
494
495
|
afterAdd(env) {
|
|
495
496
|
const path = codexConfigPath(env);
|
|
497
|
+
// present 경로(이미 등록됨)에서 채우지 못했을 때 사용자에게 보여 줄 손 수정 방법
|
|
498
|
+
const fix = `add \`tool_timeout_sec = ${TOOL_TIMEOUT_SEC}\` and \`startup_timeout_sec = ${CODEX_STARTUP_TIMEOUT_SEC}\` under [mcp_servers.pluriply] by hand in ${path}`;
|
|
496
499
|
try {
|
|
497
500
|
if (!env.fs.existsSync(path))
|
|
498
501
|
return {
|
|
499
502
|
ok: false,
|
|
500
|
-
|
|
503
|
+
fix,
|
|
504
|
+
reason: `timeout settings not written (${path} missing)`,
|
|
501
505
|
};
|
|
502
506
|
const r = insertTomlKey(
|
|
503
507
|
env.fs.readFileSync(path, "utf8"),
|
|
@@ -508,12 +512,14 @@ export const CLIENTS = [
|
|
|
508
512
|
if (r.reason === "no-header")
|
|
509
513
|
return {
|
|
510
514
|
ok: false,
|
|
511
|
-
|
|
515
|
+
fix,
|
|
516
|
+
reason: `timeout settings not written ([mcp_servers.pluriply] not found in ${path})`,
|
|
512
517
|
};
|
|
513
518
|
if (r.reason === "unsupported")
|
|
514
519
|
return {
|
|
515
520
|
ok: false,
|
|
516
|
-
|
|
521
|
+
fix,
|
|
522
|
+
reason: `timeout settings not written (${path} contains triple-quoted strings)`,
|
|
517
523
|
};
|
|
518
524
|
// 헤더·트리플쿼트 판정은 파일 단위라 위에서 통과했으면 두 번째 키도 같은 이유로는 실패하지 않는다.
|
|
519
525
|
// 이미 있는 값(사용자가 고친 값 포함)은 건드리지 않는다.
|
|
@@ -529,7 +535,8 @@ export const CLIENTS = [
|
|
|
529
535
|
} catch (err) {
|
|
530
536
|
return {
|
|
531
537
|
ok: false,
|
|
532
|
-
|
|
538
|
+
fix,
|
|
539
|
+
reason: `timeout settings not written (${err.message})`,
|
|
533
540
|
};
|
|
534
541
|
}
|
|
535
542
|
},
|
|
@@ -562,12 +569,14 @@ export const CLIENTS = [
|
|
|
562
569
|
// agy mcp add 도 타임아웃 플래그가 없다. agy 는 JSONC 를 읽지만 우리는 JSON.parse 만 쓴다(스펙 §11).
|
|
563
570
|
afterAdd(env) {
|
|
564
571
|
const path = agyConfigPath(env);
|
|
572
|
+
const fix = `add \`"timeoutSeconds": ${TOOL_TIMEOUT_SEC}\` to mcpServers.pluriply by hand in ${path}`;
|
|
565
573
|
try {
|
|
566
574
|
const doc = readJsonDoc(env, path);
|
|
567
575
|
const entry = doc.mcpServers?.pluriply;
|
|
568
576
|
if (!entry || typeof entry !== "object")
|
|
569
577
|
return {
|
|
570
578
|
ok: false,
|
|
579
|
+
fix,
|
|
571
580
|
reason: `timeoutSeconds not written (mcpServers.pluriply not found in ${path})`,
|
|
572
581
|
};
|
|
573
582
|
const changed = entry.timeoutSeconds === undefined;
|
|
@@ -579,6 +588,7 @@ export const CLIENTS = [
|
|
|
579
588
|
} catch (err) {
|
|
580
589
|
return {
|
|
581
590
|
ok: false,
|
|
591
|
+
fix,
|
|
582
592
|
reason: `timeoutSeconds not written (${err.message})`,
|
|
583
593
|
};
|
|
584
594
|
}
|