deel-local-cli 2.0.0 → 2.0.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/README.ko.md +14 -5
- package/README.md +14 -5
- package/package.json +1 -1
- package/src/acp/serve.js +49 -12
- package/src/agent/commit.js +2 -2
- package/src/agent/compact.js +4 -4
- package/src/agent/grade.js +1 -1
- package/src/agent/loop.js +2 -2
- package/src/agent/memory.js +1 -1
- package/src/agent/models.js +1 -1
- package/src/agent/outschema.js +1 -1
- package/src/agent/pins.js +7 -2
- package/src/agent/project.js +1 -1
- package/src/agent/recall.js +1 -1
- package/src/agent/store.js +13 -26
- package/src/backend/ctxsize.js +2 -2
- package/src/backend/http.js +1 -1
- package/src/backend/learn.js +27 -1
- package/src/backend/mcp.js +97 -16
- package/src/backend/proxy.js +1 -1
- package/src/backend/quota.js +2 -2
- package/src/commands/common.js +2 -2
- package/src/commands/extend.js +0 -1
- package/src/commands/model.js +1 -2
- package/src/commands.js +4 -2
- package/src/completion.js +13 -3
- package/src/config.js +35 -13
- package/src/configexplain.js +3 -2
- package/src/doctor.js +1 -2
- package/src/jsonl.js +42 -0
- package/src/lsp/client.js +6 -46
- package/src/oneshot.js +22 -0
- package/src/pack/selfpack.js +2 -2
- package/src/pack/zip.js +18 -1
- package/src/plugins/manage.js +81 -14
- package/src/reap.js +70 -0
- package/src/report.js +1 -1
- package/src/safety/audit.js +4 -19
- package/src/safety/authcmd.js +1 -1
- package/src/safety/guard.js +81 -7
- package/src/safety/hooks.js +1 -1
- package/src/safety/keystore.js +18 -3
- package/src/safety/shellenv.js +1 -1
- package/src/safety/undo.js +44 -20
- package/src/skills/discover.js +1 -1
- package/src/tools/doc2md.js +25 -3
- package/src/tools/docs.js +3 -3
- package/src/tools/edit-match.js +1 -1
- package/src/tools/encoding.js +1 -1
- package/src/tools/excel-com.js +56 -30
- package/src/tools/fastgrep.js +65 -10
- package/src/tools/fsutil.js +8 -4
- package/src/tools/ignore.js +21 -1
- package/src/tools/index.js +77 -7
- package/src/tools/jobs.js +27 -5
- package/src/tools/pdf.js +1 -3
- package/src/tools/shell.js +27 -0
- package/src/tools/todo.js +1 -1
- package/src/tools/verify.js +17 -3
- package/src/tools/webfetch.js +12 -2
- package/src/tools/xlsx.js +4 -4
- package/src/ui/banner.js +1 -1
- package/src/ui/inputbox.js +1 -1
- package/src/ui/md.js +1 -1
- package/src/ui/motion.js +1 -1
package/src/backend/mcp.js
CHANGED
|
@@ -30,9 +30,13 @@ import { join, normalize, isAbsolute, resolve } from 'node:path';
|
|
|
30
30
|
import { VERSION } from '../version.js';
|
|
31
31
|
// 남의 저장소에 딸려 온 mcp.json 으로 남의 프로그램을 띄우지 않는다 (다붙이기 머리말).
|
|
32
32
|
import { 믿나, BOM떼기 } from '../safety/trust.js';
|
|
33
|
+
// 신호로 끝날 때도 띄운 서버를 거둔다 — 손은 한 자리에만 단다 (reap.js 머리말).
|
|
34
|
+
import { 신호에거두기 } from '../reap.js';
|
|
33
35
|
|
|
34
36
|
// 붙는 데 이만큼 넘게 걸리면 포기한다. 시작이 느려지면 안 쓰게 된다.
|
|
35
|
-
|
|
37
|
+
const 붙기제한 = 8000;
|
|
38
|
+
// 쓰던 서버가 저 혼자 죽으면 한 세션에 이만큼까지 다시 띄운다 (MCP서버.깨우기 머리말).
|
|
39
|
+
export const 되살리기최대 = 2;
|
|
36
40
|
// 도구 하나 부르고 이만큼 기다린다.
|
|
37
41
|
export const 부르기제한 = 60000;
|
|
38
42
|
// 한 서버에서 받을 도구 수. 스키마가 통째로 매 요청에 실리므로 무한정 받으면
|
|
@@ -277,6 +281,22 @@ export function 모두닫기() {
|
|
|
277
281
|
* 검사가 밖에서 확인할 길이 없다. 걷어내도 아무도 모르는 그물은 없는 것과 같다.
|
|
278
282
|
*/
|
|
279
283
|
process.once('exit', 모두닫기);
|
|
284
|
+
신호에거두기(모두닫기);
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 약속을 기다리되 신호가 오면 **기다리기만** 그만둔다. 약속 자체는 안 끊는다 (부르기 · MB4).
|
|
288
|
+
*/
|
|
289
|
+
function 끊기며기다리기(약속, signal) {
|
|
290
|
+
if (!signal) return 약속;
|
|
291
|
+
if (signal.aborted) return Promise.reject(new Error('중단했습니다'));
|
|
292
|
+
let 떼기 = () => {};
|
|
293
|
+
const 끊김 = new Promise((_, 실패) => {
|
|
294
|
+
const 손 = () => 실패(new Error('중단했습니다'));
|
|
295
|
+
signal.addEventListener?.('abort', 손, { once: true });
|
|
296
|
+
떼기 = () => signal.removeEventListener?.('abort', 손);
|
|
297
|
+
});
|
|
298
|
+
return Promise.race([약속, 끊김]).finally(() => 떼기());
|
|
299
|
+
}
|
|
280
300
|
|
|
281
301
|
/**
|
|
282
302
|
* 서버 하나와의 연결.
|
|
@@ -316,11 +336,20 @@ export class MCP서버 {
|
|
|
316
336
|
* 폴더 뿌리가 아니고, 그러면 남의 폴더에 메모를 적는다.
|
|
317
337
|
*/
|
|
318
338
|
this.뿌리 = null;
|
|
339
|
+
/** 저 혼자 죽어 다시 띄운 수. 되살리기최대 를 넘으면 더 안 띄운다. */
|
|
340
|
+
this.되살린수 = 0;
|
|
341
|
+
/** 우리가 닫았나(닫기). 닫은 것은 되살리지 않는다. */
|
|
342
|
+
this.닫음 = false;
|
|
319
343
|
}
|
|
320
344
|
|
|
321
345
|
살아있나() { return !!this.kid && this.kid.exitCode === null && !this.죽음; }
|
|
322
|
-
/** 지금 도구를 부를 수 있나. 대기 중이면 부르는 순간 뜬다. */
|
|
323
|
-
쓸수있나() { return this.살아있나() || (this.대기 && !this.죽음); }
|
|
346
|
+
/** 지금 도구를 부를 수 있나. 대기 중이면 부르는 순간 뜬다. 저 혼자 죽은 것도 되살린다. */
|
|
347
|
+
쓸수있나() { return this.살아있나() || (this.대기 && !this.죽음) || this.되살릴수있나(); }
|
|
348
|
+
/**
|
|
349
|
+
* 죽었지만 다음 부름에 다시 띄울 것인가 (깨우기 머리말).
|
|
350
|
+
* 우리가 닫은 것(닫음)과 한 세션에 되살리기최대 번을 넘긴 것은 안 띄운다.
|
|
351
|
+
*/
|
|
352
|
+
되살릴수있나() { return !!this.죽음 && !this.닫음 && !this.대기 && this.되살린수 < 되살리기최대; }
|
|
324
353
|
|
|
325
354
|
/**
|
|
326
355
|
* 적어 둔 목록으로 세워 둔다. **안 띄운다.**
|
|
@@ -354,12 +383,32 @@ export class MCP서버 {
|
|
|
354
383
|
*/
|
|
355
384
|
if (this.깨우는중) return this.깨우는중;
|
|
356
385
|
if (this.살아있나()) return true;
|
|
357
|
-
|
|
386
|
+
/*
|
|
387
|
+
* ── 쓰던 서버가 죽으면 **다음 부름에 다시 띄운다** (2.0.2 · M1 · MB3) ─────────
|
|
388
|
+
*
|
|
389
|
+
* 여기서 `죽음` 이면 곧장 false 였다. 그래서 아래 「죽음 을 지우고 다시 붙는다」 는 한 번도
|
|
390
|
+
* 닿지 않는 줄이었고, 도중에 한 번 넘어진 서버(메모리 부족 · 제 오류로 끝남)는 세션 내내
|
|
391
|
+
* 「죽었습니다」 였다 — 도구가 통째로 사라진 채 대화를 이어 가야 했다.
|
|
392
|
+
*
|
|
393
|
+
* 떠서 쓰던 서버가 **저 혼자** 죽었으면 되살린다. 한 세션에 되살리기최대 번까지만 —
|
|
394
|
+
* 뜨자마자 죽는 서버를 부를 때마다 띄우면 부름마다 붙기제한만큼 멈춘다. 우리가 닫은 것
|
|
395
|
+
* (넘침 · 깨우다 실패 · 끝날 때)은 안 되살린다(되살릴수있나).
|
|
396
|
+
*/
|
|
397
|
+
if (this.죽음 && !this.되살릴수있나()) return false;
|
|
398
|
+
const 되살림 = !!this.죽음;
|
|
358
399
|
const 적어둔것 = this.도구.map((t) => t.name).join('\0');
|
|
359
400
|
// 속까지 견주려면 정의를 그대로 들고 있어야 한다 (아래 바뀐것).
|
|
360
401
|
const 적어둔도구 = this.도구;
|
|
361
402
|
this.깨우는중 = (async () => {
|
|
362
|
-
|
|
403
|
+
if (되살림) {
|
|
404
|
+
this.되살린수++;
|
|
405
|
+
// 옛 아이를 명부에서 빼고, 받다 만 조각은 버린다 — 새 아이의 첫 줄에 붙으면 안 된다.
|
|
406
|
+
띄운것들.delete(this);
|
|
407
|
+
try { this.kid?.kill(); } catch { /* 이미 죽었다 */ }
|
|
408
|
+
this.찌꺼기 = '';
|
|
409
|
+
this.마지막말 = undefined;
|
|
410
|
+
}
|
|
411
|
+
// 죽음 은 끝냄() 이 남긴다. 다시 붙으려면 지워 두고 시작한다.
|
|
363
412
|
this.죽음 = null;
|
|
364
413
|
const ok = await this.붙기({ timeout });
|
|
365
414
|
this.대기 = false;
|
|
@@ -438,14 +487,21 @@ export class MCP서버 {
|
|
|
438
487
|
// 떠 있으므로, 여기서 안 적으면 그 아이는 아무도 안 거두는 아이가 된다.
|
|
439
488
|
띄운것들.add(this);
|
|
440
489
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
490
|
+
/*
|
|
491
|
+
* **이 아이의** 소리만 듣는다 (2.0.2 · M1). 되살리면 한 서버에 아이가 둘 거쳐 간다 —
|
|
492
|
+
* 옛 아이의 늦은 'exit' 이 새 아이를 죽음 으로 적거나, 옛 아이의 마지막 조각이 새 아이의
|
|
493
|
+
* 첫 줄에 붙으면 안 된다.
|
|
494
|
+
*/
|
|
495
|
+
const 아이 = this.kid;
|
|
496
|
+
const 지금아이 = () => this.kid === 아이;
|
|
497
|
+
아이.on('error', (e) => { if (지금아이()) this.끝냄(`오류: ${e.message}`); });
|
|
498
|
+
아이.on('exit', (code, sig) => { if (지금아이()) this.끝냄(`끝났습니다 (코드 ${code ?? sig})`); });
|
|
499
|
+
아이.stdout.setEncoding('utf8');
|
|
500
|
+
아이.stdout.on('data', (d) => { if (지금아이()) this.받음(d); });
|
|
445
501
|
// 서버가 stderr 에 로그를 쏟는 일이 흔하다. 화면에 흘리면 대화가 뒤덮인다.
|
|
446
502
|
// 마지막 것만 들고 있다가 죽었을 때 원인으로 보여 준다.
|
|
447
|
-
|
|
448
|
-
|
|
503
|
+
아이.stderr.setEncoding('utf8');
|
|
504
|
+
아이.stderr.on('data', (d) => { if (지금아이()) this.마지막말 = String(d).trim().slice(-400); });
|
|
449
505
|
|
|
450
506
|
try {
|
|
451
507
|
const r = await this.보내고기다리기('initialize', {
|
|
@@ -643,14 +699,27 @@ export class MCP서버 {
|
|
|
643
699
|
}
|
|
644
700
|
|
|
645
701
|
async 부르기(도구이름, args, { timeout = 부르기제한, signal = null } = {}) {
|
|
702
|
+
// 이미 멈췄으면 깨우지도 않는다 — 안 쓸 서버를 띄워 두는 것이다.
|
|
703
|
+
if (signal?.aborted) throw new Error('중단했습니다');
|
|
646
704
|
/*
|
|
647
705
|
* 대기 중이면 **여기서** 띄운다. 이게 지연 로딩의 전부다.
|
|
648
706
|
*
|
|
649
707
|
* 첫 부름 하나만 붙는 시간을 치르고, 그 뒤로는 여느 때와 똑같다. 켤 때
|
|
650
708
|
* 다 띄우던 값을 「그 도구를 실제로 쓰는 사람」 에게만 물리는 셈이다.
|
|
651
709
|
*/
|
|
652
|
-
if (this.대기) {
|
|
653
|
-
|
|
710
|
+
if (this.대기 || this.되살릴수있나()) {
|
|
711
|
+
/*
|
|
712
|
+
* ── 깨우는 동안에도 ESC 가 먹는다 (2.0.2 · MB4) ──────────────────────────
|
|
713
|
+
*
|
|
714
|
+
* 깨우기에는 신호를 안 넘겼다. 그래서 붙는 동안(붙기제한 8초까지) ESC 가 아무것도 안
|
|
715
|
+
* 했다. 신호를 깨우기에 넘기면 안 된다 — 깨우는 약속은 **같이 부른 도구들이 나눠
|
|
716
|
+
* 기다린다**(MB1). 한 사람의 ESC 가 그 약속을 끊으면 같이 기다리던 부름까지 끊기고,
|
|
717
|
+
* 반쯤 붙은 서버가 죽음 으로 남아 세션 내내 못 쓴다.
|
|
718
|
+
*
|
|
719
|
+
* 그래서 **이 부름만** 기다리기를 그만둔다. 깨우기는 뒤에서 제 길을 가고, 다 뜨면
|
|
720
|
+
* 다음 부름이 곧장 쓴다.
|
|
721
|
+
*/
|
|
722
|
+
const ok = await 끊기며기다리기(this.깨우기(), signal);
|
|
654
723
|
if (!ok) throw new Error(this.죽음 ?? '띄우지 못했습니다');
|
|
655
724
|
/*
|
|
656
725
|
* 띄우고 보니 그 도구가 없어졌으면 **그렇다고 말한다.**
|
|
@@ -703,6 +772,7 @@ export class MCP서버 {
|
|
|
703
772
|
}
|
|
704
773
|
|
|
705
774
|
닫기() {
|
|
775
|
+
this.닫음 = true;
|
|
706
776
|
this.끝냄('닫았습니다');
|
|
707
777
|
띄운것들.delete(this);
|
|
708
778
|
try {
|
|
@@ -894,7 +964,15 @@ export async function 다붙이기(root, { offline = false, timeout = 붙기제
|
|
|
894
964
|
if (설정.오류) return { 서버들: [], 못한것: [{ 이름: '(설정)', 왜: 설정.오류 }], 설정 };
|
|
895
965
|
// 규격 때문에 안 받은 것은 어느 길로 끝나든 같이 내놓는다 (설정읽기 머리말).
|
|
896
966
|
const 안받은것 = 설정.못받은것 ?? [];
|
|
897
|
-
|
|
967
|
+
/*
|
|
968
|
+
* 서버를 다 빼면 적어 둔 목록도 걷는다 (2.0.2 · M4). 여기서 곧장 돌아서서, 다 뺀 판에는
|
|
969
|
+
* 메모쓰기 의 걷기(남길이름)가 한 번도 안 돌았다 — 지운 서버들의 도구 목록이 파일에 그대로
|
|
970
|
+
* 남는다. 설정에 없는 이름은 안 읽으니 해는 없지만, 남은 파일은 「아직 쓰는 서버」 로 읽힌다.
|
|
971
|
+
*/
|
|
972
|
+
if (!설정.서버들.length) {
|
|
973
|
+
메모쓰기(root, [], { 남길이름: [] });
|
|
974
|
+
return { 서버들: [], 못한것: [...안받은것], 설정 };
|
|
975
|
+
}
|
|
898
976
|
|
|
899
977
|
/*
|
|
900
978
|
* ── 믿는 폴더에서만 띄운다 ──────────────────────────────────────────
|
|
@@ -984,10 +1062,13 @@ export async function 다붙이기(root, { offline = false, timeout = 붙기제
|
|
|
984
1062
|
* 매번 새로 찍혀서 「일주일이면 다시 확인한다」 가 영영 안 온다. 그건 세 겹
|
|
985
1063
|
* 그물 중 하나를 우리 손으로 걷는 것이다.
|
|
986
1064
|
*/
|
|
987
|
-
|
|
1065
|
+
// 다 대기로 섰어도 설정에서 빠진 서버의 메모는 걷는다 (2.0.2 · M4 — 안 걷으면 새로 띄울 때까지 남는다).
|
|
1066
|
+
const 이름들 = 설정.서버들.map((s) => s.이름);
|
|
1067
|
+
const 걷을것 = Object.keys(메모들).some((n) => !이름들.includes(n));
|
|
1068
|
+
if (게으르게 && (새로띄운게있나 || 걷을것)) {
|
|
988
1069
|
// 대기 중인 것은 안 넘긴다(적은때를 새로 찍으면 안 된다). 그래도 그 메모는
|
|
989
1070
|
// 안 지워진다 — 메모쓰기 가 겹쳐 쓴다(그 머리말).
|
|
990
|
-
메모쓰기(root, 붙은것.filter((s) => !s.대기), { 남길이름:
|
|
1071
|
+
메모쓰기(root, 붙은것.filter((s) => !s.대기), { 남길이름: 이름들 });
|
|
991
1072
|
}
|
|
992
1073
|
return { 서버들: 붙은것, 못한것, 설정, 게으르게 };
|
|
993
1074
|
}
|
package/src/backend/proxy.js
CHANGED
package/src/backend/quota.js
CHANGED
|
@@ -271,8 +271,8 @@ export function 할당량읽기(머리) {
|
|
|
271
271
|
* 그때는 남은 수 자체로 본다 — 요청 20회 아래, 토큰 20,000 아래.
|
|
272
272
|
* 넉넉할 때 자꾸 띄우면 사람이 그 줄을 안 읽게 된다.
|
|
273
273
|
*/
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
const 요청바닥 = 20;
|
|
275
|
+
const 토큰바닥 = 20000;
|
|
276
276
|
|
|
277
277
|
export function 아슬아슬한가(것) {
|
|
278
278
|
if (!것?.있나) return false;
|
package/src/commands/common.js
CHANGED
|
@@ -28,8 +28,8 @@ export const COMMANDS = Object.fromEntries(Object.entries(명령들).map(([이
|
|
|
28
28
|
*
|
|
29
29
|
* @returns {boolean} 남겼나
|
|
30
30
|
*/
|
|
31
|
-
export function 설정남기기(cfg
|
|
32
|
-
const r = 저장시도(cfg
|
|
31
|
+
export function 설정남기기(cfg) {
|
|
32
|
+
const r = 저장시도(cfg);
|
|
33
33
|
if (!r.ok) say(` ${mark.warn} ${c.yellow(말('common.cfgSaveFailed', { 왜: clip(r.왜, 70) }))}`);
|
|
34
34
|
return r.ok;
|
|
35
35
|
}
|
package/src/commands/extend.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { existsSync, statSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { c, say, rule, pad, mark, clip } from '../ui/ansi.js';
|
|
6
|
-
import { 말 } from '../i18n/index.js';
|
|
7
6
|
import { discover } from '../skills/discover.js';
|
|
8
7
|
import { install, list, remove, pack } from '../plugins/manage.js';
|
|
9
8
|
import { spin } from '../ui/spinner.js';
|
package/src/commands/model.js
CHANGED
|
@@ -9,7 +9,6 @@ import { load, resolveKey, upsert } from '../config.js';
|
|
|
9
9
|
import { 받기설정, 잊기 as 받은열쇠잊기 } from '../safety/authcmd.js';
|
|
10
10
|
// 열쇠받기 명령을 정책이 못박아 뒀을 수 있다 — 받기설정 이 그 값을 같이 본다.
|
|
11
11
|
import { 정책읽기 } from '../safety/policy.js';
|
|
12
|
-
import { 말 } from '../i18n/index.js';
|
|
13
12
|
import { 더할머리, 규격이름 } from '../backend/adapter.js';
|
|
14
13
|
import { 규격맞추기, 규격갈래 } from '../agent/session.js';
|
|
15
14
|
import { 잠잠기본, 무소식기본 } from '../backend/http.js';
|
|
@@ -43,7 +42,7 @@ export async function 출력상한(session, arg = '') {
|
|
|
43
42
|
|
|
44
43
|
if (말 === 'auto' || 말 === '자동') {
|
|
45
44
|
session.conn.maxTokens = null;
|
|
46
|
-
|
|
45
|
+
if (prof) { delete prof.maxTokens; 설정남기기(cfg); }
|
|
47
46
|
say(` ${mark.ok} 직접 정한 값을 지웠습니다. ${c.gray('이번 대화에 바로 먹습니다.')}`);
|
|
48
47
|
/*
|
|
49
48
|
* 프로필을 못 찾으면 다음에 켤 때 그 값이 되살아난다. 숨기면 안 된다.
|
package/src/commands.js
CHANGED
|
@@ -1572,7 +1572,7 @@ export async function handle(line, session, ctx) {
|
|
|
1572
1572
|
}
|
|
1573
1573
|
|
|
1574
1574
|
case 'mcp': {
|
|
1575
|
-
const { 설정읽기, 설정자리,
|
|
1575
|
+
const { 설정읽기, 설정자리, 도구최대, 되살리기최대 } = await import('./backend/mcp.js');
|
|
1576
1576
|
const 붙은것 = session.mcp ?? [];
|
|
1577
1577
|
rule('밖에서 붙인 도구 (MCP)', 70);
|
|
1578
1578
|
|
|
@@ -1602,7 +1602,7 @@ export async function handle(line, session, ctx) {
|
|
|
1602
1602
|
*
|
|
1603
1603
|
* ● 떠 있다 지금 프로세스가 돌고 있다
|
|
1604
1604
|
* ◐ 대기 적어 둔 목록으로 서 있다 — 그 도구를 부르면 뜬다
|
|
1605
|
-
* ○ 죽었다 띄웠는데 안 됐거나 도중에 죽었다
|
|
1605
|
+
* ○ 죽었다 띄웠는데 안 됐거나 도중에 죽었다 (도중에 저 혼자 죽은 것은 다음 부름에 되살린다)
|
|
1606
1606
|
*
|
|
1607
1607
|
* 대기를 죽음으로 뭉개면 사람은 없는 탈을 고치러 간다. 반대로 대기를
|
|
1608
1608
|
* 초록으로 적으면 「띄워 봤다」 는 말이 거짓이 된다.
|
|
@@ -1621,6 +1621,8 @@ export async function handle(line, session, ctx) {
|
|
|
1621
1621
|
say(` ${mark.warn} ${c.gray(`띄워 보니 목록이 달랐습니다 — ${것}`)}`);
|
|
1622
1622
|
}
|
|
1623
1623
|
if (!s.살아있나() && !s.대기) say(` ${c.red(s.죽음 ?? '죽었습니다')}`);
|
|
1624
|
+
// 저 혼자 죽은 것은 다음 부름에 되살린다(backend/mcp.js 깨우기 · 2.0.2 M1). 안 말하면 사람은 세션을 다시 켠다.
|
|
1625
|
+
if (s.되살릴수있나?.()) say(` ${c.gray(`다음에 이 서버 도구를 부르면 다시 띄웁니다 — 이번 세션에 ${되살리기최대 - s.되살린수}번 남았습니다.`)}`);
|
|
1624
1626
|
}
|
|
1625
1627
|
say('');
|
|
1626
1628
|
say(` ${c.gray('모델에게는')} ${c.white('mcp__<서버>__<도구>')} ${c.gray('라는 이름으로 보입니다.')}`);
|
package/src/completion.js
CHANGED
|
@@ -156,11 +156,21 @@ function bash판({ zsh = false } = {}) {
|
|
|
156
156
|
# echo 'source ~/.deel-completion.${zsh ? 'zsh' : 'bash'}' >> ~/.${zsh ? 'zshrc' : 'bashrc'}
|
|
157
157
|
${zsh ? '\nautoload -U +X bashcompinit && bashcompinit\n' : ''}
|
|
158
158
|
# 파일(-f)·폴더(-d) 후보를 한 줄에 하나씩 담는다. 빈칸 든 이름이 조각나지 않게.
|
|
159
|
+
# 여는 따옴표를 친 채 누르면 cur 에 따옴표가 붙어 온다 — 떼고 찾는다.
|
|
160
|
+
# bash 3.2(맥 기본)에는 compopt 가 없다 — 그때는 빈칸 든 이름을 우리가 막아 넣는다.
|
|
159
161
|
_deel_paths() {
|
|
160
162
|
COMPREPLY=()
|
|
161
|
-
local _deel_l
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
local _deel_l _deel_c="$cur" _deel_q=""
|
|
164
|
+
case "$_deel_c" in
|
|
165
|
+
[\\"\\']*) _deel_q="\${_deel_c:0:1}"; _deel_c="\${_deel_c:1}" ;;
|
|
166
|
+
esac
|
|
167
|
+
while IFS= read -r _deel_l; do COMPREPLY+=("$_deel_l"); done < <(compgen "$1" -- "$_deel_c")
|
|
168
|
+
if type compopt >/dev/null 2>&1; then
|
|
169
|
+
compopt -o filenames 2>/dev/null
|
|
170
|
+
elif [[ -z "$_deel_q" ]]; then
|
|
171
|
+
local _deel_i
|
|
172
|
+
for _deel_i in "\${!COMPREPLY[@]}"; do COMPREPLY[$_deel_i]=$(printf '%q' "\${COMPREPLY[$_deel_i]}"); done
|
|
173
|
+
fi
|
|
164
174
|
return 0
|
|
165
175
|
}
|
|
166
176
|
|
package/src/config.js
CHANGED
|
@@ -148,9 +148,11 @@ function 잠금옮기기(cfg, root = process.cwd()) {
|
|
|
148
148
|
if (!쓸수있나().되나) return;
|
|
149
149
|
|
|
150
150
|
let 옮김 = 0;
|
|
151
|
+
// 까닭은 **진 순간에** 받아 둔다 — 뒤의 열쇠가 잠기면 keystore 가 옛 까닭을 내려놓는다 (2.0.2 · 530).
|
|
152
|
+
let 진까닭 = null;
|
|
151
153
|
for (const p of 평문) {
|
|
152
154
|
const 잠근것 = 잠그기(p.apiKey, { 프로필id: p.id });
|
|
153
|
-
if (잠근것) { p.apiKey = 잠근것; 옮김 += 1; }
|
|
155
|
+
if (잠근것) { p.apiKey = 잠근것; 옮김 += 1; } else 진까닭 ??= 잠그다실패한까닭();
|
|
154
156
|
}
|
|
155
157
|
if (!옮김) return;
|
|
156
158
|
/*
|
|
@@ -164,7 +166,7 @@ function 잠금옮기기(cfg, root = process.cwd()) {
|
|
|
164
166
|
if (옮김 < 평문.length) {
|
|
165
167
|
const 못한것 = 평문.length - 옮김;
|
|
166
168
|
소식 = `게이트웨이 열쇠 ${평문.length}개 가운데 ${못한것}개를 못 잠갔습니다`
|
|
167
|
-
+ `${
|
|
169
|
+
+ `${진까닭 ? ` (${진까닭})` : ''}`
|
|
168
170
|
+ ' — 반쯤 잠근 파일을 안 남기려고 설정 파일은 그대로 두었습니다 (파일에는 평문 그대로입니다)';
|
|
169
171
|
return;
|
|
170
172
|
}
|
|
@@ -225,24 +227,33 @@ export function load({ root = process.cwd() } = {}) {
|
|
|
225
227
|
*/
|
|
226
228
|
const 정책흔적 = Symbol('정책이덮은것');
|
|
227
229
|
|
|
230
|
+
// 프로필 짝을 무엇으로 찾나. id 가 있으면 id — upsert 가 프로필을 **새 객체로 갈아 끼운다**
|
|
231
|
+
// (`cfg.profiles[i] = { ...옛것, ...새것 }`). 객체로만 찾으면 갈아 끼운 프로필을 못 알아보고
|
|
232
|
+
// 정책 주소가 그대로 사람 파일에 적혔다 (2.0.2 · 524).
|
|
233
|
+
const 프로필열쇠 = (p) => (p && p.id != null ? `id:${p.id}` : p);
|
|
234
|
+
|
|
228
235
|
function 정책덮기(cfg) {
|
|
229
236
|
const 정책 = 정책읽기().값;
|
|
230
237
|
if (!정책 || typeof 정책 !== 'object') return cfg;
|
|
231
238
|
|
|
232
|
-
const 덮기전 = { 주소: new Map(), offline: cfg.offline, deny: cfg.permissions?.deny };
|
|
239
|
+
const 덮기전 = { 주소: new Map(), 켠offline: false, offline: cfg.offline, 더한deny: [], deny: cfg.permissions?.deny };
|
|
233
240
|
|
|
234
241
|
if (typeof 정책.baseUrl === 'string' && 정책.baseUrl.trim()) {
|
|
235
242
|
for (const 프로필 of cfg.profiles ?? []) {
|
|
236
|
-
덮기전.주소.set(
|
|
243
|
+
덮기전.주소.set(프로필열쇠(프로필), 프로필.baseUrl);
|
|
237
244
|
프로필.baseUrl = 정책.baseUrl.trim();
|
|
238
245
|
}
|
|
239
246
|
cfg.정책주소 = 정책.baseUrl.trim();
|
|
240
247
|
}
|
|
241
248
|
// 끄지는 못한다 — 켜기만. 글자 'true' 도 켠 것이다 (safety/runmode.js 의 봉인됐나).
|
|
242
|
-
if (정책.offline === true || 정책.offline === 'true')
|
|
249
|
+
if (정책.offline === true || 정책.offline === 'true') {
|
|
250
|
+
덮기전.켠offline = cfg.offline !== true;
|
|
251
|
+
cfg.offline = true;
|
|
252
|
+
}
|
|
243
253
|
if (Array.isArray(정책.permissions?.deny)) {
|
|
244
254
|
cfg.permissions = cfg.permissions ?? {};
|
|
245
255
|
const 있던것 = Array.isArray(cfg.permissions.deny) ? cfg.permissions.deny : [];
|
|
256
|
+
덮기전.더한deny = 정책.permissions.deny.filter((x) => !있던것.includes(x));
|
|
246
257
|
cfg.permissions.deny = [...new Set([...있던것, ...정책.permissions.deny])];
|
|
247
258
|
}
|
|
248
259
|
cfg[정책흔적] = 덮기전;
|
|
@@ -257,6 +268,13 @@ function 정책덮기(cfg) {
|
|
|
257
268
|
* 그러면 사람이 적어 둔 주소는 사라지고 정책 주소가 제 파일에 박힌다.
|
|
258
269
|
* 관리자가 나중에 정책을 걷어도 그 값은 남고, 그때 `deel config explain` 은
|
|
259
270
|
* 「이 PC 설정 = …」 이라며 **사람 본인을 범인으로 가리킨다.**
|
|
271
|
+
*
|
|
272
|
+
* ── 벗기는 것은 **정책이 얹은 그 값**뿐이다 (2.0.2 · 524) ──────────────────
|
|
273
|
+
*
|
|
274
|
+
* 여기가 load 때 떠 둔 값으로 칸을 통째로 되돌렸다. 그러면 load 뒤에 바뀐 것까지
|
|
275
|
+
* 같이 사라진다 — 그 사이 더한 금지 규칙, 새로 적은 주소. 그래서 칸마다 「정책이
|
|
276
|
+
* 얹은 것이 **아직 그대로** 있나」 를 보고 그것만 뗀다: 주소는 정책 주소 그대로일
|
|
277
|
+
* 때만, 금지는 정책이 더한 줄만, offline 은 정책이 켠 판에만.
|
|
260
278
|
*/
|
|
261
279
|
function 정책벗기기(cfg) {
|
|
262
280
|
const 덮기전 = cfg?.[정책흔적];
|
|
@@ -264,17 +282,21 @@ function 정책벗기기(cfg) {
|
|
|
264
282
|
const 사본 = { ...cfg };
|
|
265
283
|
delete 사본.정책주소;
|
|
266
284
|
사본.profiles = (cfg.profiles ?? []).map((p) => {
|
|
267
|
-
|
|
285
|
+
const 열쇠 = 프로필열쇠(p);
|
|
286
|
+
if (!덮기전.주소.has(열쇠) || p.baseUrl !== cfg.정책주소) return p;
|
|
268
287
|
const q = { ...p };
|
|
269
|
-
const 원래 = 덮기전.주소.get(
|
|
288
|
+
const 원래 = 덮기전.주소.get(열쇠);
|
|
270
289
|
if (원래 === undefined) delete q.baseUrl; else q.baseUrl = 원래;
|
|
271
290
|
return q;
|
|
272
291
|
});
|
|
273
|
-
if (
|
|
274
|
-
|
|
292
|
+
if (덮기전.켠offline && cfg.offline === true) {
|
|
293
|
+
if (덮기전.offline === undefined) delete 사본.offline; else 사본.offline = 덮기전.offline;
|
|
294
|
+
}
|
|
295
|
+
if (cfg.permissions && 덮기전.더한deny.length && Array.isArray(cfg.permissions.deny)) {
|
|
275
296
|
사본.permissions = { ...cfg.permissions };
|
|
276
|
-
|
|
277
|
-
|
|
297
|
+
const 남길것 = cfg.permissions.deny.filter((x) => !덮기전.더한deny.includes(x));
|
|
298
|
+
if (!남길것.length && 덮기전.deny === undefined) delete 사본.permissions.deny;
|
|
299
|
+
else 사본.permissions.deny = 남길것;
|
|
278
300
|
if (!Object.keys(사본.permissions).length) delete 사본.permissions;
|
|
279
301
|
}
|
|
280
302
|
return 사본;
|
|
@@ -637,8 +659,8 @@ export function save(cfg) {
|
|
|
637
659
|
*
|
|
638
660
|
* @returns {{ok: true, 자리: string} | {ok: false, 왜: string}}
|
|
639
661
|
*/
|
|
640
|
-
export function 저장시도(cfg
|
|
641
|
-
try { return { ok: true, 자리: save(cfg
|
|
662
|
+
export function 저장시도(cfg) {
|
|
663
|
+
try { return { ok: true, 자리: save(cfg) }; }
|
|
642
664
|
catch (err) { return { ok: false, 왜: err?.message ?? String(err) }; }
|
|
643
665
|
}
|
|
644
666
|
|
package/src/configexplain.js
CHANGED
|
@@ -77,7 +77,7 @@ const 환경변수 = {
|
|
|
77
77
|
* 파이프를 타고 로그로, 사내 티켓으로 간다 — 화면만 가려 놓고 안전하다고
|
|
78
78
|
* 하는 것이 이런 자리에서 제일 흔한 사고다.
|
|
79
79
|
*/
|
|
80
|
-
|
|
80
|
+
const 비밀표시 = '(적혀 있음 — 값은 안 보여 줍니다)';
|
|
81
81
|
const 지운값 = (칸, v) => (v !== undefined && 가릴칸.has(칸) ? 비밀표시 : v);
|
|
82
82
|
|
|
83
83
|
const 값보이기 = (칸, v) => {
|
|
@@ -123,7 +123,8 @@ export function 설명(칸, { root = process.cwd(), env = process.env } = {}) {
|
|
|
123
123
|
}
|
|
124
124
|
// 물려받은 이름(`constructor` · `toString`)으로 파고들면 **없는 칸이
|
|
125
125
|
// 있는 것이 된다.** 여기서 막아야 아래 세 층이 다 안전하다.
|
|
126
|
-
|
|
126
|
+
// 위 루프 머리에서 여기가 object 임을 이미 봤다 — 또 보지 않는다.
|
|
127
|
+
여기 = Object.hasOwn(여기, 이름) ? 여기[이름] : undefined;
|
|
127
128
|
}
|
|
128
129
|
return 여기;
|
|
129
130
|
};
|
package/src/doctor.js
CHANGED
|
@@ -69,13 +69,12 @@ function 주소살펴보기(주소) {
|
|
|
69
69
|
* 이 자리의 조건을 다 본다. 화면 그리기는 안 한다 — 부르는 쪽이 그린다.
|
|
70
70
|
*
|
|
71
71
|
* @param {object} o
|
|
72
|
-
* @param {object|null} o.cfg 읽은 설정
|
|
73
72
|
* @param {object|null} o.prof 고른 프로필
|
|
74
73
|
* @param {string} o.root 작업 폴더
|
|
75
74
|
* @param {boolean} o.바깥가도되나 네트워크를 실제로 두드려도 되나
|
|
76
75
|
*/
|
|
77
76
|
export async function 진찰(o = {}) {
|
|
78
|
-
const {
|
|
77
|
+
const { prof = null, root = process.cwd(), 바깥가도되나 = true, 설정자리 = null, 설정탈 = null } = o;
|
|
79
78
|
const 줄들 = [];
|
|
80
79
|
|
|
81
80
|
// ── 1. 이 프로그램이 도는 자리 ───────────────────────────────────────
|
package/src/jsonl.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 한 줄씩 덧붙여 가는 기록 파일(대화 · 감사기록 · 되돌리기 이력)이 같이 쓰는 두 손질.
|
|
3
|
+
*
|
|
4
|
+
* agent/store.js · safety/audit.js · safety/undo.js 에 바이트까지 같은 사본이 세 벌 있었다
|
|
5
|
+
* (2.0.0 7회차에 적어 둠). 한 벌을 고치고 두 벌을 잊는 날이 오므로 여기 한 벌만 둔다 (2.0.2).
|
|
6
|
+
*
|
|
7
|
+
* node:fs 말고는 아무것도 안 들여온다 — 되돌리기는 설정이 깨져도 돌아야 한다(undo.js 머리말).
|
|
8
|
+
* 그래서 `진짜자리()` 쌍둥이(guard · undo)는 여기로 안 모은다.
|
|
9
|
+
*/
|
|
10
|
+
import { statSync, openSync, readSync, closeSync, chmodSync } from 'node:fs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 파일이 개행으로 끝나나. 없거나 비었거나 못 읽으면 참 — 붙일 반쪽이 없다.
|
|
14
|
+
*
|
|
15
|
+
* 앞 판이 한 줄을 반만 적고 죽으면, 이어 적는 첫 줄이 그 반쪽 **뒤에 그대로 붙어** 한 줄이
|
|
16
|
+
* 되고, 그 줄은 JSON 이 아니라 다음에 읽을 때 통째로 건너뛴다. 부르는 쪽은 파일마다 처음
|
|
17
|
+
* 적을 때 한 번 이것을 보고, 아니면 개행부터 붙인다.
|
|
18
|
+
*/
|
|
19
|
+
export function 줄로끝나나(file) {
|
|
20
|
+
let fd = null;
|
|
21
|
+
try {
|
|
22
|
+
const 크기 = statSync(file).size;
|
|
23
|
+
if (!크기) return true;
|
|
24
|
+
fd = openSync(file, 'r');
|
|
25
|
+
const 한바이트 = Buffer.alloc(1);
|
|
26
|
+
readSync(fd, 한바이트, 0, 1, 크기 - 1);
|
|
27
|
+
return 한바이트[0] === 0x0a;
|
|
28
|
+
} catch { return true; } // 없거나 못 읽으면 붙일 반쪽도 없다
|
|
29
|
+
finally { if (fd != null) { try { closeSync(fd); } catch { /* 닫다 터져도 적기는 한다 */ } } }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 본인만 읽게 잠근다. 건 결과를 돌려준다 — `{모드}` 면 걸었고, `{못함}` 이면 못 걸었다.
|
|
34
|
+
*
|
|
35
|
+
* 윈도우에서 chmod 는 **아무 일도 안 하고 성공한다.** 그 성공을 그대로 적으면 「잠근 척」
|
|
36
|
+
* 이 된다 — 같은 PC 를 여럿이 쓰는 사람이 화면만 보고 안심한다. 안 걸었으면 안 걸었다고 적는다.
|
|
37
|
+
*/
|
|
38
|
+
export function 본인만잠그기(file) {
|
|
39
|
+
if (process.platform === 'win32') return { 못함: 'windows' };
|
|
40
|
+
try { chmodSync(file, 0o600); return { 모드: 0o600 }; }
|
|
41
|
+
catch (err) { return { 못함: err?.code ?? String(err) }; }
|
|
42
|
+
}
|
package/src/lsp/client.js
CHANGED
|
@@ -30,6 +30,7 @@ import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
|
30
30
|
import { readFileSync } from 'node:fs';
|
|
31
31
|
import { 갈래, 고르기, 언어아이디 } from './servers.js';
|
|
32
32
|
import { 틀, 받개 } from './rpc.js';
|
|
33
|
+
import { 신호에거두기, 다시보낼까 } from '../reap.js';
|
|
33
34
|
|
|
34
35
|
// 켜는 데 이만큼 넘게 걸리면 포기한다. rust-analyzer 처럼 무거운 것도
|
|
35
36
|
// **악수(initialize)** 자체는 빠르다 — 오래 걸리는 것은 그 뒤의 색인이다.
|
|
@@ -606,53 +607,12 @@ export const 아이들데려가기 = () => {
|
|
|
606
607
|
process.on('exit', 아이들데려가기);
|
|
607
608
|
|
|
608
609
|
/*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
* SIGINT·SIGTERM 에 손이 하나라도 달려 있으면 **기본 동작(끝내기)을 안 한다.**
|
|
613
|
-
* 그래서 이 파일이 한 번이라도 불려 들어온 프로그램은 —
|
|
614
|
-
*
|
|
615
|
-
* kill <pid> → 언어 서버는 죽고, deel 은 그대로 산다
|
|
616
|
-
*
|
|
617
|
-
* 유닉스에서 SIGTERM 을 **삼키는** 프로그램이 되어 있었다. 잡 관리자·컨테이너
|
|
618
|
-
* 종료·systemd 가 전부 이 신호로 정리하는데, 그것들이 하나같이 시간을 다
|
|
619
|
-
* 기다린 뒤 SIGKILL 로 때려잡게 된다. 그러면 정작 아이 정리는 못 하고 죽는다.
|
|
620
|
-
*
|
|
621
|
-
* 그래서 치운 다음 우리 손을 떼고 같은 신호를 자신에게 다시 보낸다. 그러면
|
|
622
|
-
* 기본 동작이 돌아 128+n 로 끝난다 — 셸이 기대하는 그 값이다.
|
|
623
|
-
*
|
|
624
|
-
* 다만 **남이 그 신호를 맡고 있으면 안 건드린다.** 한 방 실행은 SIGINT 로
|
|
625
|
-
* 턴만 끊고 살아 있어야 하고(oneshot.js), 대화 화면의 Ctrl+C 는 readline 이
|
|
626
|
-
* 따로 받는다. 우리 것 말고 다른 손이 달려 있으면 그쪽 뜻이 먼저다.
|
|
627
|
-
*/
|
|
628
|
-
/**
|
|
629
|
-
* 치운 다음 그 신호로 진짜 죽어야 하나.
|
|
630
|
-
*
|
|
631
|
-
* 갈림을 함수로 빼 둔 까닭: 윈도우에서는 이 길을 통째로 밟아 볼 수가 없다
|
|
632
|
-
* (SIGTERM 을 보내면 손이 돌기도 전에 Node 가 죽인다). 그런데 갈림 자체가
|
|
633
|
-
* 틀리면 유닉스에서 한 방 실행의 Ctrl+C 가 턴만 끊는 대신 프로그램을 죽인다 —
|
|
634
|
-
* 어느 판에서든 재어 볼 수 있어야 하는 자리다.
|
|
635
|
-
*
|
|
636
|
-
* @param 셈 이 신호에 달린 손의 수. 우리 것 하나만이면 우리가 마지막이다.
|
|
610
|
+
* 신호(SIGINT·SIGTERM)로 끝날 때도 거둔다. 손은 reap.js 에 하나만 단다 — 여기 따로 달면
|
|
611
|
+
* MCP·일감이 거둬지지 않고, 둘이 따로 달면 서로 「남이 맡았다」 고 보고 deel 이 SIGTERM 을
|
|
612
|
+
* 삼킨다(그 머리말). 다시보낼까 는 검사가 여기서 부르던 그대로 내보낸다.
|
|
637
613
|
*/
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
for (const 신호 of ['SIGINT', 'SIGTERM']) {
|
|
643
|
-
const 손 = () => {
|
|
644
|
-
/*
|
|
645
|
-
* 갈림을 **거두기보다 먼저** 본다. 거두고 나서 갈랐었다 — 그래서 대화 화면의 Ctrl+C 가
|
|
646
|
-
* 턴만 끊고 세션은 사는데 언어 서버는 전부 죽고 풀도 비었다(2.0.0 6회차 사냥). 남이
|
|
647
|
-
* 맡은 신호로 결국 끝나면 'exit' 그물(아이들데려가기)이 그때 거둔다.
|
|
648
|
-
*/
|
|
649
|
-
if (!다시보낼까(신호)) return; // 남이 맡고 있다 — 그쪽 뜻이 먼저다
|
|
650
|
-
아이들데려가기();
|
|
651
|
-
process.removeListener(신호, 손);
|
|
652
|
-
try { process.kill(process.pid, 신호); } catch { process.exit(신호 === 'SIGINT' ? 130 : 143); }
|
|
653
|
-
};
|
|
654
|
-
process.on(신호, 손);
|
|
655
|
-
}
|
|
614
|
+
신호에거두기(아이들데려가기);
|
|
615
|
+
export { 다시보낼까 };
|
|
656
616
|
|
|
657
617
|
/**
|
|
658
618
|
* 켠 지 얼마 안 됐나.
|
package/src/oneshot.js
CHANGED
|
@@ -33,6 +33,7 @@ import { 알림채움, 알림말 } from './backend/retry.js';
|
|
|
33
33
|
import { 전선붙이기, 세션이름짓기 } from './backend/wire.js';
|
|
34
34
|
import { newId } from './agent/store.js';
|
|
35
35
|
import { discover, loadCommand } from './skills/discover.js';
|
|
36
|
+
import { 다붙이기 } from './backend/mcp.js';
|
|
36
37
|
import { 명령들 } from './cmdnames.js';
|
|
37
38
|
// 슬래시 명령을 찾는 규칙은 대화 화면과 **같은 것 하나**를 쓴다 (commands.js 의 슬래시명령찾기 머리말).
|
|
38
39
|
import { 슬래시명령찾기, 비슷한슬래시명령 } from './commands.js';
|
|
@@ -193,6 +194,8 @@ export async function runOnce(opts = {}) {
|
|
|
193
194
|
let 출력스키마 = null;
|
|
194
195
|
// 실패덩이에 실을 모델 이름 (사냥5 B5-10). 프로필을 찾기 전에 선 실패는 null 이다.
|
|
195
196
|
let 알려진모델 = null;
|
|
197
|
+
// 붙인 MCP 서버. 아래 내놓기 가 닫는다 — 그래서 여기서 선언한다 (2.0.2 · 문 맞춤).
|
|
198
|
+
let mcp서버들 = [];
|
|
196
199
|
|
|
197
200
|
const 내놓기 = (r) => {
|
|
198
201
|
/*
|
|
@@ -208,6 +211,8 @@ export async function runOnce(opts = {}) {
|
|
|
208
211
|
// 이건 아무 말 없이 한다 — 사용자가 띄우라고 한 적이 없는 것이라,
|
|
209
212
|
// 껐다는 말부터 하면 "그건 또 뭐냐" 가 된다.
|
|
210
213
|
언어서버다끄기().catch(() => {});
|
|
214
|
+
// 붙인 MCP 서버도 거둔다. 안 닫으면 잡이 끝난 뒤에도 남의 프로그램이 폴더를 물고 남는다.
|
|
215
|
+
for (const 서버 of mcp서버들) { try { 서버.닫기(); } catch { /* 닫다 터져도 끝맺음은 간다 */ } }
|
|
211
216
|
/*
|
|
212
217
|
* 문서를 글로 바꾸며 떨군 임시 파일도 거둔다 (tools/convert.js 의 임시치우기).
|
|
213
218
|
*
|
|
@@ -461,6 +466,21 @@ export async function runOnce(opts = {}) {
|
|
|
461
466
|
|
|
462
467
|
const found = discover(root);
|
|
463
468
|
session.skills = found.skills;
|
|
469
|
+
/*
|
|
470
|
+
* 밖에서 붙인 도구(MCP) — 대화 화면·에디터와 **같은 규칙으로** 붙인다 (2.0.2 · 문 맞춤).
|
|
471
|
+
*
|
|
472
|
+
* 한 번 실행만 이 자리가 없었다. `.deel/mcp.json` 에 적어 둔 도구가 대화 화면에서는 되고
|
|
473
|
+
* `deel run` 에서는 **말없이 없었다** — 모델은 없는 도구 대신 셸로 돌아가고, 왜 없는지는
|
|
474
|
+
* 어디에도 안 남았다. 기본은 꺼져 있고(사람이 직접 적어야 뜬다) 봉인이면 안 띄우는 것도 같다.
|
|
475
|
+
* 못 붙은 것은 표준오류에 적는다 — 표준출력은 답 자리다.
|
|
476
|
+
*/
|
|
477
|
+
const mcp붙임 = await 다붙이기(root, {
|
|
478
|
+
offline: 봉인됐나({ 깃발: opts.offline, prof, cfg }),
|
|
479
|
+
audit: new Audit(root, { 열쇠들: 열쇠묻기(conn) }),
|
|
480
|
+
}).catch((err) => ({ 서버들: [], 못한것: [{ 이름: '(전부)', 왜: String(err?.message ?? err) }] }));
|
|
481
|
+
mcp서버들 = mcp붙임.서버들;
|
|
482
|
+
session.mcp = mcp붙임.서버들;
|
|
483
|
+
for (const m of mcp붙임.못한것 ?? []) 곁(` ${mark.warn} ${c.yellow(`MCP ${m.이름} 을 못 붙였습니다 — ${m.왜}`)}`);
|
|
464
484
|
session.commands = found.commands;
|
|
465
485
|
session.plugins = found.plugins;
|
|
466
486
|
|
|
@@ -581,6 +601,8 @@ export async function runOnce(opts = {}) {
|
|
|
581
601
|
history: new History(root),
|
|
582
602
|
audit: new Audit(root, { 열쇠들: 열쇠묻기(conn) }),
|
|
583
603
|
seen: new Set(),
|
|
604
|
+
// 붙은 MCP 서버. 도구를 부를 때 여기서 찾는다.
|
|
605
|
+
mcp: mcp붙임.서버들,
|
|
584
606
|
skills: found.skills,
|
|
585
607
|
loadedSkills: new Set(),
|
|
586
608
|
// 고친 뒤 진단을 볼지 (lsp/diag.js). 아래 내놓기() 에서 반드시 거둔다 —
|
package/src/pack/selfpack.js
CHANGED
|
@@ -34,7 +34,7 @@ function walk(dir, base, out) {
|
|
|
34
34
|
return out;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function shippedFiles(root = repoRoot()) {
|
|
38
38
|
const out = [];
|
|
39
39
|
for (const d of SHIP) if (existsSync(join(root, d))) walk(join(root, d), root, out);
|
|
40
40
|
for (const f of SHIP_FILES) if (existsSync(join(root, f))) out.push(f);
|
|
@@ -59,7 +59,7 @@ const PROBES = [
|
|
|
59
59
|
note: '없어야 정상입니다' },
|
|
60
60
|
];
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
function scanCalls(root = repoRoot(), files = shippedFiles(root)) {
|
|
63
63
|
const found = Object.fromEntries(PROBES.map((p) => [p.id, []]));
|
|
64
64
|
for (const f of files) {
|
|
65
65
|
if (!f.endsWith('.js')) continue;
|