deel-local-cli 1.7.0 → 1.10.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.ko.md +1166 -0
- package/README.md +1223 -1107
- package/bin/deel.js +78 -12
- package/package.json +4 -4
- package/src/acp/map.js +1 -1
- package/src/acp/serve.js +40 -3
- package/src/agent/compact.js +314 -296
- package/src/agent/effort.js +27 -7
- package/src/agent/evidence.js +2 -0
- package/src/agent/filemem.js +141 -0
- package/src/agent/grade.js +51 -1
- package/src/agent/loop.js +218 -40
- package/src/agent/session.js +136 -14
- package/src/agent/store.js +186 -9
- package/src/agent/threads.js +26 -1
- package/src/backend/adapter.js +272 -23
- package/src/backend/learn.js +24 -0
- package/src/backend/mcp.js +96 -5
- package/src/backend/probe.js +131 -61
- package/src/backend/quota.js +29 -5
- package/src/backend/retry.js +3 -0
- package/src/backend/scanui.js +1 -1
- package/src/backend/toolfit.js +325 -0
- package/src/commands.js +170 -61
- package/src/completion.js +20 -2
- package/src/config.js +47 -2
- package/src/i18n/en.js +210 -3
- package/src/i18n/index.js +40 -0
- package/src/i18n/ja.js +204 -3
- package/src/i18n/ko.js +270 -3
- package/src/i18n/zh.js +204 -3
- package/src/lsp/client.js +49 -5
- package/src/oneshot.js +92 -5
- package/src/pack/sbom.js +30 -4
- package/src/pack/selfpack.js +26 -10
- package/src/pack/sheet.en.js +288 -0
- package/src/pack/tar.js +65 -2
- package/src/plugins/manage.js +46 -9
- package/src/repl.js +344 -61
- package/src/reset.js +397 -0
- package/src/safety/audit.js +95 -8
- package/src/safety/authcmd.js +316 -0
- package/src/safety/guard.js +143 -0
- package/src/safety/keystore.js +114 -38
- package/src/safety/undo.js +25 -6
- package/src/tools/desc.en.js +19 -14
- package/src/tools/index.js +349 -75
- package/src/tools/jobs.js +175 -42
- package/src/tools/lsp.js +5 -4
- package/src/tools/outline.js +7 -3
- package/src/tools/task.js +10 -6
- package/src/tools/todo.js +9 -2
- package/src/tools/verify.js +42 -7
- package/src/tools/webfetch.js +94 -10
- package/src/ui/export.js +1 -1
- package/src/ui/md.js +201 -5
- package/src/ui/motion.js +0 -1
- package/src/ui/pastechip.js +50 -3
- package/src/ui/pick.js +115 -0
- package/src/ui/screen.js +23 -3
- package/README.en.md +0 -1117
package/src/tools/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { globToRegex, walk, readText, readTextFull, 내부살림 } from './fsuti
|
|
|
7
7
|
import { 건너뜀말 } from './ignore.js';
|
|
8
8
|
import { encode, label as encLabel, decode as decodeBytes, consoleCodepage, looksBinary } from './encoding.js';
|
|
9
9
|
import { checkCommand, checkPaths, isMutating } from '../safety/guard.js';
|
|
10
|
-
import { 띄우기, JOBS_TOOL } from './jobs.js';
|
|
10
|
+
import { 띄우기, 나무끊기, 무리끊기, JOBS_TOOL } from './jobs.js';
|
|
11
11
|
import { 셸명령 } from './shell.js';
|
|
12
12
|
import { findMatch, applySpans, reindent, TIER_LABELS } from './edit-match.js';
|
|
13
13
|
import { loadSkill } from '../skills/discover.js';
|
|
@@ -20,7 +20,7 @@ import { DEF_TOOL, REFS_TOOL } from './lsp.js';
|
|
|
20
20
|
import { 편집후진단, 붙이기 as 진단붙이기, 데우기 } from '../lsp/diag.js';
|
|
21
21
|
import { 프로젝트갈래 } from '../lsp/servers.js';
|
|
22
22
|
import { allow as allowedIn } from '../agent/modes.js';
|
|
23
|
-
import { 도구정의,
|
|
23
|
+
import { 도구정의, 이름풀기, 열쇠뺀환경 } from '../backend/mcp.js';
|
|
24
24
|
import { isExcelPath, readExcel, toText as excelText, summarize as excelSummary } from './excel.js';
|
|
25
25
|
import { isDocPath, readDoc, toText as docText, summarize as docSummary, looksOldHwp, 옛hwp안내, 문서는못고침 } from './docs.js';
|
|
26
26
|
import { 바꿔볼까, 직접못읽나, 변환기찾기, 글로바꾸기, 못바꿈말 } from './convert.js';
|
|
@@ -31,7 +31,25 @@ import { 읽을줄수, 찾을개수, 찾을줄수, 설명길이 } from '../agent
|
|
|
31
31
|
import { 도구설명EN } from './desc.en.js';
|
|
32
32
|
import { 그림인가, 그림읽기, 크기말, 기본한도 } from '../backend/vision.js';
|
|
33
33
|
import { 빠르게찾기, 엔진말, 안볼정규식 } from './fastgrep.js';
|
|
34
|
-
import {
|
|
34
|
+
import { 지시말, 말, 세말 } from '../i18n/index.js';
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* 도구 결과 한 줄을 잇는다 — `1개 파일 · 3건 · rg`.
|
|
38
|
+
*
|
|
39
|
+
* 빈 조각은 버린다. 조건부 조각을 `조건 ? '· 뭐' : ''` 로 이어 붙이면
|
|
40
|
+
* 어느 말에서는 가운뎃점이 둘 붙거나(`· ·`) 줄 끝에 하나 남는다.
|
|
41
|
+
* 그 자리를 부르는 쪽마다 살피게 두면 반드시 어딘가는 빠진다.
|
|
42
|
+
*/
|
|
43
|
+
const 이어 = (...조각들) => 조각들.filter((x) => x != null && String(x) !== '').join(' · ');
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Bash 가 한 번에 받아 두는 출력의 상한.
|
|
47
|
+
*
|
|
48
|
+
* 넘으면 Node 가 자식을 죽이고 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' 를 준다.
|
|
49
|
+
* 그때 무엇이 일어났는지를 사람 말로 적어 줘야 한다 — 그러지 않으면 모델은
|
|
50
|
+
* 같은 명령을 그대로 다시 부른다.
|
|
51
|
+
*/
|
|
52
|
+
const 출력상한 = 8 * 1024 * 1024;
|
|
35
53
|
import { 표몇군데, 표막는말 } from '../safety/secrets.js';
|
|
36
54
|
|
|
37
55
|
/*
|
|
@@ -505,6 +523,38 @@ function pdf읽기(abs, ctx) {
|
|
|
505
523
|
* **손대지 않는다** — 옮겨진 자리에 그대로 남는다. 되돌아가지는 않지만
|
|
506
524
|
* 없어지지도 않는다. 안전망이 파일을 지우는 것보다 이쪽이 낫다.
|
|
507
525
|
*/
|
|
526
|
+
/*
|
|
527
|
+
* ── 복사와 지우기를 한 try 로 묶으면 안 된다 ──────────────────────────────
|
|
528
|
+
*
|
|
529
|
+
* 드라이브가 다르면(EXDEV) rename 이 안 되니 복사하고 원본을 지운다. 그 둘을
|
|
530
|
+
* 한 try 에 묶어 두면 **복사는 다 됐는데 원본을 못 지운** 경우까지 「못
|
|
531
|
+
* 옮겼습니다」가 된다. 윈도우에서 아주 흔하다 — 원본 폴더 안의 파일 하나를
|
|
532
|
+
* 편집기나 탐색기가 잡고 있으면 EPERM/EBUSY 로 rmSync 만 깨진다.
|
|
533
|
+
* 그때 화면은 실패라고 말하는데 실제로는 **양쪽에 다 있다.** 사람은 실패로
|
|
534
|
+
* 알고 다시 옮기고, 원본은 계속 남는다.
|
|
535
|
+
*
|
|
536
|
+
* 세 결과는 서로 다른 말이므로 서로 다르게 돌려준다.
|
|
537
|
+
* {} 다 됐다
|
|
538
|
+
* { 복사깨짐: 까닭 } 닿은 자리가 반쯤 됐다 — 실패다
|
|
539
|
+
* { 원본남음: 까닭 } 닿은 자리는 다 됐고 원본이 남았다 — 실패가 아니다
|
|
540
|
+
*
|
|
541
|
+
* fs 를 밖에서 넣을 수 있게 해 둔 것은 검사 때문이다. 진짜 EXDEV 와 진짜
|
|
542
|
+
* '지우기만 깨짐' 은 어느 PC 에서나 똑같이 만들어 낼 수 없다.
|
|
543
|
+
*/
|
|
544
|
+
export function 복사해옮기기(앞, 뒤, { 복사 = cpSync, 지우기 = rmSync } = {}) {
|
|
545
|
+
try {
|
|
546
|
+
복사(앞, 뒤, { recursive: true });
|
|
547
|
+
} catch (err) {
|
|
548
|
+
return { 복사깨짐: err.message };
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
지우기(앞, { recursive: true, force: true });
|
|
552
|
+
} catch (err) {
|
|
553
|
+
return { 원본남음: err.message };
|
|
554
|
+
}
|
|
555
|
+
return {};
|
|
556
|
+
}
|
|
557
|
+
|
|
508
558
|
function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
509
559
|
if (typeof from !== 'string' || !from) return { error: 'from 이 없습니다' };
|
|
510
560
|
if (typeof to !== 'string' || !to) return { error: 'to 가 없습니다' };
|
|
@@ -534,7 +584,7 @@ function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
534
584
|
* 겹쳤다고 알려 주면 모델이 이름을 고쳐서 다시 부른다.
|
|
535
585
|
*/
|
|
536
586
|
if (existsSync(뒤) && !overwrite) {
|
|
537
|
-
return { error:
|
|
587
|
+
return { error: `${말('err.alreadyThere', { 경로: to })}\n ${말('err.overwriteHint')}` };
|
|
538
588
|
}
|
|
539
589
|
|
|
540
590
|
/*
|
|
@@ -564,6 +614,7 @@ function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
564
614
|
}
|
|
565
615
|
|
|
566
616
|
mkdirSync(dirname(뒤), { recursive: true });
|
|
617
|
+
let 원본남음 = null;
|
|
567
618
|
try {
|
|
568
619
|
renameSync(앞, 뒤);
|
|
569
620
|
} catch (err) {
|
|
@@ -573,19 +624,23 @@ function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
573
624
|
* 복사 도중에 끊기면 양쪽에 반씩 남기 때문이다.
|
|
574
625
|
*/
|
|
575
626
|
if (err.code !== 'EXDEV') return { error: `못 옮겼습니다: ${err.message}` };
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
return { error: `못 옮겼습니다: ${
|
|
627
|
+
const 벌어진일 = 복사해옮기기(앞, 뒤);
|
|
628
|
+
if (벌어진일.복사깨짐) {
|
|
629
|
+
// 닿은 자리를 여기서 지우지 않는다 — 이미 있던 폴더로 옮기는 중이었으면
|
|
630
|
+
// 남의 파일까지 지운다. 어디에 반쯤 남았는지만 정확히 말한다.
|
|
631
|
+
return { error: `못 옮겼습니다: ${벌어진일.복사깨짐}\n(옮기다 만 것이 ${ctx.scope.show(뒤)} 에 남아 있을 수 있습니다 — 지우기 전에 확인하세요)` };
|
|
581
632
|
}
|
|
633
|
+
원본남음 = 벌어진일.원본남음 ?? null;
|
|
582
634
|
}
|
|
583
635
|
|
|
584
636
|
for (const [, b] of 짝들) ctx.seen.add(b);
|
|
585
637
|
const 무엇 = 폴더인가 ? `폴더 ${짝들.length}개 파일` : '';
|
|
586
|
-
const 경고 = 되돌리기반쪽
|
|
638
|
+
const 경고 = (되돌리기반쪽
|
|
587
639
|
? `\n(파일이 ${훑은것.상한.toLocaleString('en-US')}개를 넘어 되돌리기에는 앞부분만 떴습니다 — 옮기기는 전부 됐지만 /undo 는 다 못 되돌립니다)`
|
|
588
|
-
: ''
|
|
640
|
+
: '')
|
|
641
|
+
+ (원본남음
|
|
642
|
+
? `\n(닿은 자리에는 다 옮겼는데 원본을 못 지웠습니다 — 지금 ${ctx.scope.show(앞)} 에도 그대로 있습니다: ${원본남음})`
|
|
643
|
+
: '');
|
|
589
644
|
return {
|
|
590
645
|
content: `옮김: ${ctx.scope.show(앞)} → ${ctx.scope.show(뒤)}${무엇 ? ` (${무엇})` : ''}${경고}`,
|
|
591
646
|
summary: `${ctx.scope.show(뒤)}`,
|
|
@@ -636,7 +691,7 @@ function 여러개옮기기(목록, ctx) {
|
|
|
636
691
|
+ 된것.map((c) => ` ✓ ${c.replace(/^옮김: /, '')}`).join('\n')
|
|
637
692
|
+ (안된것.length ? `\n${안된것.map((e) => ` ✗ ${e.split('\n')[0]}`).join('\n')}` : '')
|
|
638
693
|
+ (안된것.length ? '\n\n실패한 것만 다시 보내세요. 된 것은 다시 안 보내도 됩니다.' : ''),
|
|
639
|
-
summary:
|
|
694
|
+
summary: 이어(말('sum.moved', { n: 된것.length }), 안된것.length ? 말('sum.filesFailed', { n: 안된것.length }) : ''),
|
|
640
695
|
바뀐것들,
|
|
641
696
|
};
|
|
642
697
|
}
|
|
@@ -691,7 +746,7 @@ function 한파일쓰기(args, ctx) {
|
|
|
691
746
|
const 표기 = 원래 !== 'utf-8' ? ` · ${encLabel(원래)}` : '';
|
|
692
747
|
return {
|
|
693
748
|
content: `${existed ? '덮어씀' : '새로 만듦'}: ${ctx.scope.show(abs)} (${n}줄${표기})`,
|
|
694
|
-
summary:
|
|
749
|
+
summary: 이어(세말('lines', n), 원래 !== 'utf-8' ? encLabel(원래) : ''),
|
|
695
750
|
changed: abs,
|
|
696
751
|
diff: 바뀐자리(이전, args.content),
|
|
697
752
|
};
|
|
@@ -735,8 +790,11 @@ function 여러파일쓰기(목록, ctx) {
|
|
|
735
790
|
content: `${된것.length}개 만들었습니다${안된것.length ? `, ${안된것.length}개 실패` : ''}.\n`
|
|
736
791
|
+ 줄들.join('\n')
|
|
737
792
|
+ (안된것.length ? '\n\n실패한 것만 다시 보내세요. 된 것은 다시 안 보내도 됩니다.' : ''),
|
|
738
|
-
summary:
|
|
739
|
-
|
|
793
|
+
summary: 이어(
|
|
794
|
+
세말('count', 된것.length),
|
|
795
|
+
세말('lines', 된것.reduce((a, r) => a + (r.lines ?? 0), 0)),
|
|
796
|
+
안된것.length ? 말('sum.filesFailed', { n: 안된것.length }) : '',
|
|
797
|
+
),
|
|
740
798
|
// 화면과 루프가 파일별로 처리하도록 그대로 넘긴다. changed 는 안 넣는다 —
|
|
741
799
|
// 넣으면 그 한 개만 세어지고 나머지가 조용히 빠진다.
|
|
742
800
|
여럿: 결과,
|
|
@@ -752,7 +810,7 @@ function 여러파일쓰기(목록, ctx) {
|
|
|
752
810
|
*/
|
|
753
811
|
function 한군데고치기(args, ctx) {
|
|
754
812
|
const abs = ctx.scope.resolve(args.file_path);
|
|
755
|
-
if (!existsSync(abs)) return { error:
|
|
813
|
+
if (!existsSync(abs)) return { error: 말('err.noSuchFile', { 경로: args.file_path }) };
|
|
756
814
|
const 못고치는이유 = 내부살림(abs);
|
|
757
815
|
if (못고치는이유) return { error: 못고치는이유 };
|
|
758
816
|
// 엑셀 파일은 Read 로 읽히긴 하지만 고칠 수 있는 물건이 아니다.
|
|
@@ -809,7 +867,12 @@ function 한군데고치기(args, ctx) {
|
|
|
809
867
|
const 표기 = 읽음.encoding !== 'utf-8' ? ` · ${encLabel(읽음.encoding)}` : '';
|
|
810
868
|
return {
|
|
811
869
|
content: `고침: ${ctx.scope.show(abs)} (${n}군데${how}${표기})`,
|
|
812
|
-
summary:
|
|
870
|
+
summary: 이어(
|
|
871
|
+
세말('spots', n),
|
|
872
|
+
// 화면 말로. TIER_LABELS 는 한국어 원본이라 content 쪽에만 남긴다.
|
|
873
|
+
m.tier === 'exact' ? '' : 말(`tier.${m.tier}`),
|
|
874
|
+
읽음.encoding !== 'utf-8' ? encLabel(읽음.encoding) : '',
|
|
875
|
+
),
|
|
813
876
|
changed: abs,
|
|
814
877
|
tier: m.tier,
|
|
815
878
|
diff: 바뀐자리(text, next),
|
|
@@ -869,7 +932,11 @@ function 여러군데고치기(목록, ctx) {
|
|
|
869
932
|
? '\n\n실패한 것만 다시 보내세요. 된 것은 다시 안 보내도 됩니다.'
|
|
870
933
|
+ ' 앞엣것이 이미 반영됐으니 **파일을 다시 Read 해서** 지금 내용을 보고 old_string 을 잡으세요.'
|
|
871
934
|
: ''),
|
|
872
|
-
summary:
|
|
935
|
+
summary: 이어(
|
|
936
|
+
세말('files', 파일수),
|
|
937
|
+
세말('spots', 군데수),
|
|
938
|
+
안된것.length ? 말('sum.spotsFailed', { n: 안된것.length }) : '',
|
|
939
|
+
),
|
|
873
940
|
// 화면과 루프가 자리별로 처리하도록 그대로 넘긴다. changed 는 안 넣는다 —
|
|
874
941
|
// 넣으면 그 한 개만 세어지고 나머지가 조용히 빠진다 (여러파일쓰기 와 같다).
|
|
875
942
|
여럿: 결과,
|
|
@@ -900,7 +967,7 @@ function 그림보기(abs, ctx) {
|
|
|
900
967
|
return { error: `${show} — ${것.왜}` };
|
|
901
968
|
}
|
|
902
969
|
ctx.seen.add(abs);
|
|
903
|
-
const 잰것 =
|
|
970
|
+
const 잰것 = 이어(말('sum.image'), 크기말(것.bytes), 것.mime);
|
|
904
971
|
|
|
905
972
|
if (!눈있나) {
|
|
906
973
|
return {
|
|
@@ -908,7 +975,7 @@ function 그림보기(abs, ctx) {
|
|
|
908
975
|
+ '지금 붙어 있는 모델은 그림을 못 봅니다 — 그래서 보내지 않았습니다.\n'
|
|
909
976
|
+ '이 파일을 코드처럼 읽으려 하지 마세요. 무엇이 찍혀 있는지 알아야 한다면'
|
|
910
977
|
+ ' 사용자에게 말로 설명해 달라고 하세요.',
|
|
911
|
-
summary:
|
|
978
|
+
summary: 이어(잰것, 말('sum.noVision')),
|
|
912
979
|
그림없음: true,
|
|
913
980
|
};
|
|
914
981
|
}
|
|
@@ -940,7 +1007,7 @@ export const TOOLS = {
|
|
|
940
1007
|
},
|
|
941
1008
|
async run(args, ctx) {
|
|
942
1009
|
const abs = ctx.scope.resolve(args.file_path);
|
|
943
|
-
if (!existsSync(abs)) return { error:
|
|
1010
|
+
if (!existsSync(abs)) return { error: 말('err.noSuchFile', { 경로: args.file_path }) };
|
|
944
1011
|
if (statSync(abs).isDirectory()) return { error: `폴더입니다. Glob 을 쓰세요: ${args.file_path}` };
|
|
945
1012
|
// 제 살림·남의 도구 살림은 안 읽는다 — fsutil 의 내부살림() 머리말 참고.
|
|
946
1013
|
const 막을이유 = 내부살림(abs);
|
|
@@ -1021,11 +1088,40 @@ export const TOOLS = {
|
|
|
1021
1088
|
*/
|
|
1022
1089
|
const 준줄수 = slice.length;
|
|
1023
1090
|
const 다못줌 = start + count < lines.length || start > 0;
|
|
1091
|
+
const 통째로 = body + more + 줄인것.알림;
|
|
1092
|
+
const 실린것 = clip(통째로);
|
|
1024
1093
|
return {
|
|
1025
|
-
content:
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1094
|
+
content: 실린것,
|
|
1095
|
+
/*
|
|
1096
|
+
* 어느 파일을 읽었는지 알려 준다 — 대화에 실을 때 앞서 읽은 것과
|
|
1097
|
+
* 견주려면 열쇠가 있어야 한다(agent/filemem.js).
|
|
1098
|
+
*
|
|
1099
|
+
* `부분인가` 는 **우리가 들고 있는 것이 파일 전부가 아니다** 는 뜻이다.
|
|
1100
|
+
* 줄로 잘렸든(offset·limit·창 상한), 그림을 줄였든, clip 에 걸렸든
|
|
1101
|
+
* 마찬가지다. 조각을 기억해 두고 나중에 「그대로입니다」 라고 하면
|
|
1102
|
+
* 모델은 안 본 데를 봤다고 여긴다 — 줄이려다 잃는 자리다.
|
|
1103
|
+
*/
|
|
1104
|
+
읽은것: abs,
|
|
1105
|
+
부분인가: 다못줌 || 줄인것.줄인바이트 > 0 || 실린것.length < 통째로.length,
|
|
1106
|
+
summary: 이어(
|
|
1107
|
+
다못줌
|
|
1108
|
+
? `${말('sum.linesOf', { 준: 준줄수, 전체: lines.length })} (${말('sum.partial')})`
|
|
1109
|
+
: 세말('lines', lines.length),
|
|
1110
|
+
줄인것.줄인바이트 ? 말('sum.imageDropped', { 크기: 몇KB(줄인것.줄인바이트) }) : '',
|
|
1111
|
+
/*
|
|
1112
|
+
* 짐작한 인코딩은 짐작이라고 적는다.
|
|
1113
|
+
*
|
|
1114
|
+
* 앞머리 표식(BOM)이 없고 UTF-8 규칙에도 안 맞으면 남은 길은 내용을
|
|
1115
|
+
* 보고 점수를 매기는 것뿐이다 (encoding.js 의 detect). CP949 와
|
|
1116
|
+
* CP932 는 바이트 범위가 겹쳐서 짧은 파일일수록 자주 뒤집힌다.
|
|
1117
|
+
* 여태 그 확신도(sure)를 재 놓고 아무 데서도 안 읽어서, 화면에는
|
|
1118
|
+
* 짐작이 사실처럼 `CP949` 한 낱말로 떴다. 사람은 잘 읽힌 줄 알고
|
|
1119
|
+
* 그 위에서 고치고, 그때 원본이 상한다.
|
|
1120
|
+
*/
|
|
1121
|
+
읽음.sure
|
|
1122
|
+
? (별난인코딩 ? encLabel(읽음.encoding) : '')
|
|
1123
|
+
: 말('sum.encGuess', { 인코딩: encLabel(읽음.encoding) }),
|
|
1124
|
+
),
|
|
1029
1125
|
};
|
|
1030
1126
|
},
|
|
1031
1127
|
},
|
|
@@ -1137,7 +1233,11 @@ export const TOOLS = {
|
|
|
1137
1233
|
return {
|
|
1138
1234
|
content: `${existed ? '이어 붙임' : '새로 만듦'}: ${ctx.scope.show(abs)}`
|
|
1139
1235
|
+ ` (+${붙인줄}줄, 지금 전체 ${전체줄}줄${표기})`,
|
|
1140
|
-
summary:
|
|
1236
|
+
summary: 이어(
|
|
1237
|
+
말('sum.addedLines', { 줄: 세말('lines', 붙인줄) }),
|
|
1238
|
+
말('sum.total', { 줄: 세말('lines', 전체줄) }),
|
|
1239
|
+
원래 !== 'utf-8' ? encLabel(원래) : '',
|
|
1240
|
+
),
|
|
1141
1241
|
changed: abs,
|
|
1142
1242
|
// 이어 붙이기는 앞부분이 그대로다. 전후를 통째로 견줄 이유가 없다 —
|
|
1143
1243
|
// 큰 파일에서 그 비용이 그대로 기다리는 시간이 된다.
|
|
@@ -1277,7 +1377,7 @@ export const TOOLS = {
|
|
|
1277
1377
|
if (!files.length) {
|
|
1278
1378
|
return {
|
|
1279
1379
|
content: `${전부.잘림 ? '본 데까지는 찾은 파일 없음' : '찾은 파일 없음'}: ${args.pattern}${건너뜀}`,
|
|
1280
|
-
summary: 전부.잘림 ? '0
|
|
1380
|
+
summary: 전부.잘림 ? `${세말('count', 0)} (${말('sum.notAllSeen')})` : 세말('count', 0),
|
|
1281
1381
|
};
|
|
1282
1382
|
}
|
|
1283
1383
|
// 잘랐으면 잘랐다고 말한다. 전에는 '200개' 라고만 해서, 모델이 그게 전부인 줄
|
|
@@ -1287,7 +1387,9 @@ export const TOOLS = {
|
|
|
1287
1387
|
: '';
|
|
1288
1388
|
return {
|
|
1289
1389
|
content: files.map((f) => ctx.scope.show(f.path)).join('\n') + 잘림 + 건너뜀,
|
|
1290
|
-
summary: 맞는것.length > files.length
|
|
1390
|
+
summary: 맞는것.length > files.length
|
|
1391
|
+
? 말('sum.countOf', { n: files.length, 전체: 맞는것.length })
|
|
1392
|
+
: 세말('count', files.length),
|
|
1291
1393
|
};
|
|
1292
1394
|
},
|
|
1293
1395
|
},
|
|
@@ -1371,17 +1473,17 @@ export const TOOLS = {
|
|
|
1371
1473
|
'(.gitignore·.deelignore 는 지켰습니다. 건너뛴 수는 안 셌습니다)',
|
|
1372
1474
|
].filter(Boolean).join(' ');
|
|
1373
1475
|
const 붙이기2 = (t) => (꼬리2 ? [t, '', 꼬리2].join('\n') : t);
|
|
1374
|
-
if (!total) return { content: 붙이기2(`일치 없음: ${args.pattern}`), summary:
|
|
1375
|
-
if (mode === 'content') return { content: 붙이기2(clip(lines.join('\n'))), summary:
|
|
1476
|
+
if (!total) return { content: 붙이기2(`일치 없음: ${args.pattern}`), summary: 이어(세말('hits', 0), 빠른것.엔진) };
|
|
1477
|
+
if (mode === 'content') return { content: 붙이기2(clip(lines.join('\n'))), summary: 이어(세말('hits', total), 빠른것.엔진) };
|
|
1376
1478
|
if (mode === 'count') {
|
|
1377
1479
|
return {
|
|
1378
1480
|
content: 붙이기2(hitFiles.slice(0, limit).map((f) => `${f.n}\t${f.rel}`).join('\n')),
|
|
1379
|
-
summary:
|
|
1481
|
+
summary: 이어(세말('files', hitFiles.length), 빠른것.엔진),
|
|
1380
1482
|
};
|
|
1381
1483
|
}
|
|
1382
1484
|
return {
|
|
1383
1485
|
content: 붙이기2(hitFiles.slice(0, limit).map((f) => f.rel).join('\n')),
|
|
1384
|
-
summary:
|
|
1486
|
+
summary: 이어(세말('files', hitFiles.length), 세말('hits', total), 빠른것.엔진),
|
|
1385
1487
|
};
|
|
1386
1488
|
}
|
|
1387
1489
|
|
|
@@ -1449,14 +1551,14 @@ export const TOOLS = {
|
|
|
1449
1551
|
if (!total) {
|
|
1450
1552
|
return {
|
|
1451
1553
|
content: 붙이기(다못봄 ? `본 데까지는 일치 없음: ${args.pattern}` : `일치 없음: ${args.pattern}`),
|
|
1452
|
-
summary: 다못봄 ? '0
|
|
1554
|
+
summary: 다못봄 ? `${세말('hits', 0)} (${말('sum.notAllSeen')})` : 세말('hits', 0),
|
|
1453
1555
|
};
|
|
1454
1556
|
}
|
|
1455
|
-
if (mode === 'content') return { content: 붙이기(clip(lines.join('\n'))), summary:
|
|
1557
|
+
if (mode === 'content') return { content: 붙이기(clip(lines.join('\n'))), summary: 세말('hits', total) };
|
|
1456
1558
|
if (mode === 'count') {
|
|
1457
|
-
return { content: 붙이기(hitFiles.map((f) => `${f.n}\t${f.rel}`).join('\n')), summary:
|
|
1559
|
+
return { content: 붙이기(hitFiles.map((f) => `${f.n}\t${f.rel}`).join('\n')), summary: 세말('files', hitFiles.length) };
|
|
1458
1560
|
}
|
|
1459
|
-
return { content: 붙이기(hitFiles.map((f) => f.rel).join('\n')), summary:
|
|
1561
|
+
return { content: 붙이기(hitFiles.map((f) => f.rel).join('\n')), summary: 이어(세말('files', hitFiles.length), 세말('hits', total)) };
|
|
1460
1562
|
},
|
|
1461
1563
|
},
|
|
1462
1564
|
|
|
@@ -1488,7 +1590,8 @@ export const TOOLS = {
|
|
|
1488
1590
|
ctx.loadedSkills?.add(hit.name);
|
|
1489
1591
|
return {
|
|
1490
1592
|
content: `# 스킬: ${hit.name}\n\n${body}`,
|
|
1491
|
-
summary:
|
|
1593
|
+
summary: 말('unit.kchars', { n: Math.round(body.length / 100) / 10 })
|
|
1594
|
+
+ (cut ? ` (${말('sum.cut')})` : ''),
|
|
1492
1595
|
};
|
|
1493
1596
|
},
|
|
1494
1597
|
},
|
|
@@ -1552,8 +1655,11 @@ export const TOOLS = {
|
|
|
1552
1655
|
return {
|
|
1553
1656
|
content: `${r.번호}번으로 띄웠습니다: ${cmd}\n`
|
|
1554
1657
|
+ (r.출력?.trim() ? `\n${clip(r.출력, 4000)}\n` : '')
|
|
1555
|
-
|
|
1556
|
-
|
|
1658
|
+
// 여기 적는 이름은 **Jobs 설명서에 실린 그 이름**이어야 한다.
|
|
1659
|
+
// 별칭(번호·끝내기)도 받아 주긴 하지만, 설명서와 다른 이름을
|
|
1660
|
+
// 모델에게 시키면 엄격한 게이트웨이가 그 호출을 튕긴다.
|
|
1661
|
+
+ `\n계속 돌고 있습니다. 새 출력은 Jobs({job: ${r.번호}}) 로 읽고, 일이 끝나면 Jobs({job: ${r.번호}, stop: true}) 로 정리해라.`,
|
|
1662
|
+
summary: 말('sum.jobStarted', { n: r.번호 }),
|
|
1557
1663
|
일감번호: r.번호,
|
|
1558
1664
|
뒤에서: true,
|
|
1559
1665
|
되돌릴것: 뜬것,
|
|
@@ -1582,9 +1688,29 @@ export const TOOLS = {
|
|
|
1582
1688
|
// '�Ľ� ����' 이 된다. 바이트로 받아 이 컴퓨터가 쓰는 것으로 해독한다.
|
|
1583
1689
|
const kid = execFile(shell.file, shell.args, {
|
|
1584
1690
|
cwd: ctx.scope.root,
|
|
1585
|
-
|
|
1586
|
-
|
|
1691
|
+
// 열쇠만 빼고 나머지는 그대로 물려준다 (backend/mcp.js 열쇠뺀환경 머리말).
|
|
1692
|
+
// 안 빼면 `env` 한 줄이 열쇠를 화면과 대화 기록에 그대로 싣는다.
|
|
1693
|
+
env: 열쇠뺀환경(),
|
|
1694
|
+
// 우리 시계(아래 뒷북)가 제한에서 먼저 나무를 끊는다. 이건 그것마저
|
|
1695
|
+
// 못 돌았을 때 서는 마지막 그물이라 뒤에 세운다.
|
|
1696
|
+
timeout: 제한 + 3000,
|
|
1697
|
+
maxBuffer: 출력상한,
|
|
1587
1698
|
windowsHide: true,
|
|
1699
|
+
/*
|
|
1700
|
+
* 유닉스에서는 **무리를 만들어 둔다.**
|
|
1701
|
+
*
|
|
1702
|
+
* 나중에 만들어 줄 방법이 없어서 띄우는 순간에 정해야 한다. 안 만들면
|
|
1703
|
+
* 나중에 손자를 가리킬 길이 아예 없다 — `sh -c "cd app && npm start"`
|
|
1704
|
+
* 처럼 복합 명령이면 sh 가 exec 로 갈아치우지 않고 fork 하므로,
|
|
1705
|
+
* kid.kill() 은 sh 만 죽이고 npm·node·vite 는 포트를 문 채로 남는다.
|
|
1706
|
+
* 화면에는 「시간 초과로 중단됨」 이 뜬다. 이 도구가 없애려던 바로 그
|
|
1707
|
+
* 상태다.
|
|
1708
|
+
*
|
|
1709
|
+
* 윈도우에서는 반대로 안 만든다 — 거기서 detached 는 새 콘솔 창을
|
|
1710
|
+
* 띄우는 쪽으로 작동해서, 조용히 돌아야 할 것이 화면에 튀어나온다.
|
|
1711
|
+
* 윈도우는 taskkill /t 가 나무를 훑는다. (jobs.js 띄우기옵션 과 같은 판단)
|
|
1712
|
+
*/
|
|
1713
|
+
detached: process.platform !== 'win32',
|
|
1588
1714
|
windowsVerbatimArguments: shell.verbatim === true,
|
|
1589
1715
|
encoding: 'buffer',
|
|
1590
1716
|
}, (err, stdoutBuf, stderrBuf) => {
|
|
@@ -1601,7 +1727,9 @@ export const TOOLS = {
|
|
|
1601
1727
|
const out = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
1602
1728
|
|
|
1603
1729
|
if (끊겼나) return done({ error: '사용자가 중단했습니다', content: clip(out) });
|
|
1604
|
-
if (err && err.killed)
|
|
1730
|
+
if (시간초과 || (err && err.killed)) {
|
|
1731
|
+
return done({ error: `시간 초과로 중단됨 (${제한}ms)`, content: clip(out) });
|
|
1732
|
+
}
|
|
1605
1733
|
|
|
1606
1734
|
/*
|
|
1607
1735
|
* 결과를 사실대로 말한다.
|
|
@@ -1613,13 +1741,39 @@ export const TOOLS = {
|
|
|
1613
1741
|
* 종료코드도 모델에게 준다. 전에는 화면에만 적고 대화에는 안 실었다.
|
|
1614
1742
|
* 그러면 모델은 명령이 실패한 줄 모른 채 다음 단계로 넘어간다.
|
|
1615
1743
|
*/
|
|
1744
|
+
/*
|
|
1745
|
+
* err.code 가 **늘 종료코드인 것은 아니다.**
|
|
1746
|
+
*
|
|
1747
|
+
* 프로세스가 0 이 아닌 값으로 끝나면 숫자가 온다. 그런데 Node 자신이
|
|
1748
|
+
* 못 견뎌서 끝낸 경우에는 글자가 온다 —
|
|
1749
|
+
* 출력이 maxBuffer 를 넘으면 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER',
|
|
1750
|
+
* 명령을 못 찾으면 'ENOENT'.
|
|
1751
|
+
*
|
|
1752
|
+
* 그걸 그대로 종료코드로 썼다. 그래서 12MB 를 뱉는 명령을 부르면
|
|
1753
|
+
* 화면과 모델이 「종료코드 ERR_CHILD_PROCESS_STDIO_MAXBUFFER」 라는
|
|
1754
|
+
* 뜻 모를 말을 받았다. exitCode 가 숫자가 아니라 글자로 나가는 것도
|
|
1755
|
+
* 규격 위반이고, 무엇보다 **무엇을 고쳐야 하는지가 어디에도 없어서**
|
|
1756
|
+
* 모델이 같은 명령을 그대로 다시 부른다.
|
|
1757
|
+
*/
|
|
1616
1758
|
const 시그널 = err?.signal ?? null;
|
|
1617
|
-
const
|
|
1618
|
-
const
|
|
1619
|
-
const
|
|
1759
|
+
const 넘침 = err?.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
|
|
1760
|
+
const 숫자코드 = typeof err?.code === 'number' ? err.code : null;
|
|
1761
|
+
const 글자코드 = typeof err?.code === 'string' ? err.code : null;
|
|
1762
|
+
const code = (시그널 || (err && 숫자코드 === null)) ? null : (숫자코드 ?? 0);
|
|
1763
|
+
const 잘됨 = !err && !시그널;
|
|
1764
|
+
const 꼬리 = 잘됨 ? ''
|
|
1765
|
+
: 시그널 ? `\n\n[${시그널} 시그널로 죽었습니다 — 정상 종료가 아닙니다]`
|
|
1766
|
+
: 넘침 ? `\n\n[출력이 ${출력상한 / (1024 * 1024)}MB 를 넘어 받다 말았습니다 — 명령을 끝까지 못 봤습니다.`
|
|
1767
|
+
+ ' `| head -n 200` 처럼 줄이거나 파일로 받아서 다시 부르세요]'
|
|
1768
|
+
: 숫자코드 === null ? `\n\n[${글자코드} — 명령을 아예 못 돌렸습니다]`
|
|
1769
|
+
: `\n\n[종료코드 ${code}]`;
|
|
1620
1770
|
done({
|
|
1621
1771
|
content: clip(out || '(출력 없음)') + 꼬리,
|
|
1622
|
-
summary: 잘됨 ? '
|
|
1772
|
+
summary: 잘됨 ? 말('sum.ok')
|
|
1773
|
+
: 시그널 ? 말('sum.killedBy', { 시그널 })
|
|
1774
|
+
: 넘침 ? 말('sum.tooMuchOut')
|
|
1775
|
+
: 숫자코드 === null ? 말('sum.cantRun', { 왜: 글자코드 })
|
|
1776
|
+
: 말('sum.exitCode', { code }),
|
|
1623
1777
|
failed: !잘됨,
|
|
1624
1778
|
exitCode: code,
|
|
1625
1779
|
signal: 시그널,
|
|
@@ -1635,23 +1789,71 @@ export const TOOLS = {
|
|
|
1635
1789
|
* Ctrl+C 는 다음 요청에나 반영됐다. 터미널을 닫는 수밖에 없었다.
|
|
1636
1790
|
*/
|
|
1637
1791
|
let 끊겼나 = false;
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1792
|
+
let 시간초과 = false;
|
|
1793
|
+
const 죽이기 = ({ 파이프끊기 = true, 더줄까 = 0 } = {}) => {
|
|
1794
|
+
/*
|
|
1795
|
+
* 윈도우에서는 자식만 죽이면 손자가 남는다. 트리째 끝내야 한다.
|
|
1796
|
+
*
|
|
1797
|
+
* 그리고 **기다린다.** 전에는 execFile(비동기)로 불렀는데, 그러면
|
|
1798
|
+
* 바로 아래 kid.kill() 이 먼저 돌아 cmd 를 죽인다. 나무의 뿌리가
|
|
1799
|
+
* 없어진 뒤에 taskkill 이 뜨므로 손자 — `npm run dev` 의 진짜
|
|
1800
|
+
* 서버 — 를 못 찾고, 그놈은 그대로 남아 포트를 문다. 「먼저 부른다」
|
|
1801
|
+
* 는 비동기에서는 「먼저 돈다」 가 아니다.
|
|
1802
|
+
*
|
|
1803
|
+
* jobs.js 의 나무죽이기() 가 이미 같은 자리에서 같은 결론에 닿았다.
|
|
1804
|
+
* 여기만 그 교훈 밖에 있었다.
|
|
1805
|
+
*
|
|
1806
|
+
* ── 판단은 베끼지 않고 **가져다 쓴다** ─────────────────────────
|
|
1807
|
+
*
|
|
1808
|
+
* 전에는 여기서 taskkill 오류를 통째로 삼켰다. 잘린 것과 이미 죽은
|
|
1809
|
+
* 것을 안 가른 채 곧장 뿌리(cmd.exe)를 죽였으니, 컴퓨터가 바쁘면
|
|
1810
|
+
* 손자가 남고 화면에는 「중단됨」 이 떴다. jobs.js 는 그 구분을
|
|
1811
|
+
* 갖고 있었는데 여기만 없었다 — 같은 판단이 두 벌로 있으면 늘
|
|
1812
|
+
* 한쪽만 고쳐진다. 그래서 이제 그 함수 하나를 같이 쓴다.
|
|
1813
|
+
*
|
|
1814
|
+
* 여기는 jobs.js 와 달리 **다시 올 자리가 없다.** 이 부름은 지금
|
|
1815
|
+
* 끝난다. 그러니 못 훑었으면 한 번 더 준 다음 뿌리를 죽인다.
|
|
1816
|
+
* 뿌리를 그냥 살려 두면 이번에는 cmd.exe 까지 남는다.
|
|
1817
|
+
*
|
|
1818
|
+
* 더 줄 시간은 **부르는 쪽이 정한다.** 여기는 동기라 기다리는 동안
|
|
1819
|
+
* 화면이 멈춘다. 시간 초과는 어차피 기다리던 자리라 넉넉히 줘도
|
|
1820
|
+
* 되지만, Ctrl+C 는 사람이 지금 당장 돌려받고 싶어 누른 것이다.
|
|
1821
|
+
* 거기서 몇 초를 더 얼리면 「안 멈춘다」 고 또 누르게 된다 —
|
|
1822
|
+
* 손자를 살리려다 사람을 붙잡는 셈이라, 그 자리는 짧게 준다.
|
|
1823
|
+
*/
|
|
1642
1824
|
if (process.platform === 'win32' && kid.pid) {
|
|
1643
|
-
|
|
1825
|
+
if (!나무끊기(kid.pid, 1500) && 더줄까 > 0) 나무끊기(kid.pid, 더줄까);
|
|
1826
|
+
} else if (kid.pid) {
|
|
1827
|
+
/*
|
|
1828
|
+
* 유닉스는 무리째 끊는다. 위 detached 와 짝이다.
|
|
1829
|
+
*
|
|
1830
|
+
* 한동안 이 갈래가 통째로 없었다. 윈도우 쪽만 고쳐 놓고 「중단하면
|
|
1831
|
+
* 진짜 멈춘다」 를 판 소개에 적었는데, 유닉스에서는 sh 만 죽고
|
|
1832
|
+
* 그 아래가 그대로 남아 있었다. 검사가 초록이었던 것도 까닭이
|
|
1833
|
+
* 있다 — 검사가 쓰는 명령이 `node ticker.cjs` 하나뿐이라
|
|
1834
|
+
* 유닉스 sh 가 그걸 exec 로 갈아치웠고, 애초에 손자가 안 생겼다.
|
|
1835
|
+
*/
|
|
1836
|
+
무리끊기(kid.pid);
|
|
1644
1837
|
}
|
|
1645
1838
|
try { kid.kill(); } catch {}
|
|
1646
1839
|
// 죽이라고 시켰다고 곧바로 죽는 것은 아니다. 그동안 이 프로세스가
|
|
1647
1840
|
// 그 손을 붙들고 있으면 deel 을 끝내도 안 끝난다 — 실제로 검사가
|
|
1648
1841
|
// 60초를 더 기다렸다. 놓아 주고 우리 갈 길을 간다.
|
|
1649
1842
|
try { kid.unref(); } catch {}
|
|
1650
|
-
|
|
1843
|
+
/*
|
|
1844
|
+
* 파이프는 부르는 쪽이 고른다.
|
|
1845
|
+
*
|
|
1846
|
+
* 끊으면 이벤트 루프를 안 붙잡아서 좋은데, 그 순간 아직 안 읽힌
|
|
1847
|
+
* 것이 통째로 사라진다. 하필 그게 제일 중요한 몇 줄이다 — 서버가
|
|
1848
|
+
* 뻗으며 남긴 스택 트레이스가 거기 있다. 시간 초과에서는 그 몇
|
|
1849
|
+
* 줄을 받아야 하므로 안 끊는다. (jobs.js 의 끝내기() 와 같은 판단)
|
|
1850
|
+
*/
|
|
1851
|
+
if (파이프끊기) { try { kid.stdout?.destroy(); kid.stderr?.destroy(); } catch {} }
|
|
1651
1852
|
};
|
|
1652
1853
|
const 끊기 = () => {
|
|
1653
1854
|
끊겼나 = true;
|
|
1654
|
-
|
|
1855
|
+
// Ctrl+C 는 짧게 — 사람이 지금 돌려받으려고 누른 것이다.
|
|
1856
|
+
죽이기({ 더줄까: 1200 });
|
|
1655
1857
|
clearTimeout(뒷북);
|
|
1656
1858
|
// 콜백을 기다리지 않는다. 손자가 파이프를 물고 있으면 안 불릴 수도 있다.
|
|
1657
1859
|
done({ error: '사용자가 중단했습니다' });
|
|
@@ -1660,17 +1862,35 @@ export const TOOLS = {
|
|
|
1660
1862
|
else ctx.signal?.addEventListener?.('abort', 끊기, { once: true });
|
|
1661
1863
|
|
|
1662
1864
|
/*
|
|
1663
|
-
*
|
|
1865
|
+
* ── 시간 초과는 **우리가** 끊는다 ───────────────────────────────
|
|
1866
|
+
*
|
|
1867
|
+
* execFile 의 timeout 은 바로 아래 자식(윈도우면 cmd.exe)에게만
|
|
1868
|
+
* 시그널을 보낸다. 손자는 안 건드린다. 그래서 오래 이랬다 —
|
|
1869
|
+
*
|
|
1870
|
+
* Bash(npm run dev) → 시간 초과로 중단됨 (120000ms)
|
|
1871
|
+
* 실제로는 → 서버가 그대로 돌면서 포트를 물고 있다
|
|
1664
1872
|
*
|
|
1665
|
-
*
|
|
1666
|
-
*
|
|
1667
|
-
*
|
|
1873
|
+
* 화면에는 「중단됨」 이 뜨는데 프로세스는 살아 있다. 무엇이 포트를
|
|
1874
|
+
* 물고 있는지 알 길이 없는, 이 프로그램이 없애려던 바로 그 상태다.
|
|
1875
|
+
*
|
|
1876
|
+
* 그렇다고 콜백에서 뒤늦게 나무를 끊어도 소용없다. 그때는 cmd 가
|
|
1877
|
+
* 이미 죽어 나무의 뿌리가 없어서 taskkill 이 손자를 못 찾는다.
|
|
1878
|
+
* **cmd 가 살아 있는 동안** 끊어야 한다. 그래서 우리 시계를 제한에
|
|
1879
|
+
* 두고, execFile 의 timeout 은 그 뒤에 서는 마지막 그물로 남긴다.
|
|
1880
|
+
*
|
|
1881
|
+
* 끊고 나서 곧장 끝맺지 않는다. 나무가 끊기면 대개 곧바로 콜백이
|
|
1882
|
+
* 오는데, 그때라야 죽기 직전에 나온 몇 줄을 같이 실어 줄 수 있다.
|
|
1883
|
+
* 그것마저 안 오면 아래 그물이 대신 끝맺는다.
|
|
1668
1884
|
*/
|
|
1669
1885
|
const 뒷북 = setTimeout(() => {
|
|
1670
1886
|
if (끝났나 || 끊겼나) return;
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1887
|
+
시간초과 = true;
|
|
1888
|
+
죽이기({ 파이프끊기: false, 더줄까: 4000 });
|
|
1889
|
+
const 그물 = setTimeout(() => {
|
|
1890
|
+
done({ error: `시간 초과로 중단됨 (${제한}ms) — 자식 프로세스가 안 끝나 강제로 끝냈습니다` });
|
|
1891
|
+
}, 1000);
|
|
1892
|
+
그물.unref?.();
|
|
1893
|
+
}, 제한);
|
|
1674
1894
|
// 이 시계 때문에 프로그램이 안 끝나면 안 된다.
|
|
1675
1895
|
뒷북.unref?.();
|
|
1676
1896
|
});
|
|
@@ -1718,7 +1938,7 @@ export const TOOLS = {
|
|
|
1718
1938
|
* 첫 문장에 제일 중요한 것을 둔다 — 좁은 창에서는 설명줄이기가
|
|
1719
1939
|
* **뒤에서부터 문장째로** 잘라 낸다. 8k 에서 남는 것은 앞 90자다.
|
|
1720
1940
|
*/
|
|
1721
|
-
description: '갈림길에서 사람에게 하나 묻는다. **먼저
|
|
1941
|
+
description: '갈림길에서 사람에게 하나 묻는다. **먼저 `understanding` 에 이번 요청을 무엇으로'
|
|
1722
1942
|
+ ' 알아들었는지 한 줄로 적고**, 그러고도 정말 정할 것이 남았을 때만 묻는다.'
|
|
1723
1943
|
+ ' 글로 "알려주세요" 하고 끝내지 마라 — 턴이 끝나서 여태 본 것이 버려진다.'
|
|
1724
1944
|
+ ' options 에 2~4개를 주면 숫자로 답한다.'
|
|
@@ -1727,7 +1947,19 @@ export const TOOLS = {
|
|
|
1727
1947
|
parameters: {
|
|
1728
1948
|
type: 'object',
|
|
1729
1949
|
properties: {
|
|
1730
|
-
|
|
1950
|
+
/*
|
|
1951
|
+
* 인자 이름은 **영문·숫자·`_.-` 만** 쓴다.
|
|
1952
|
+
*
|
|
1953
|
+
* 이 이름은 우리끼리 쓰는 것이 아니라 도구 설명서에 실려 서버로 나간다.
|
|
1954
|
+
* Bedrock 은 `^[a-zA-Z0-9_.-]{1,64}$` 로 검사하고, 안 맞으면 **첫 한마디가
|
|
1955
|
+
* 통째로 400** 이다. 화면에는 그냥 「BadRequest」 라서, 사람은 열쇠가
|
|
1956
|
+
* 틀린 줄 알고 다시 발급받으러 간다.
|
|
1957
|
+
*
|
|
1958
|
+
* 로컬 모델도 OpenAI 호환 서버도 이 검사를 안 한다. 그래서 바깥에
|
|
1959
|
+
* 붙어 보기 전까지는 멀쩡해 보였다. 검사가 이제 그 자리를 잰다
|
|
1960
|
+
* (test/toolargs.test.js).
|
|
1961
|
+
*/
|
|
1962
|
+
understanding: {
|
|
1731
1963
|
type: 'string',
|
|
1732
1964
|
description: '이번 요청을 무엇으로 알아들었는지 한 줄. 사람은 이 줄을 보고 네가'
|
|
1733
1965
|
+ ' 제대로 읽었는지 판단한다. "요청을 이해했습니다" 같은 인사말은 안 된다 —'
|
|
@@ -1740,7 +1972,7 @@ export const TOOLS = {
|
|
|
1740
1972
|
description: '고를 것 2~4개. 각각 한 줄로, 무엇이 달라지는지 알 수 있게',
|
|
1741
1973
|
},
|
|
1742
1974
|
},
|
|
1743
|
-
required: ['
|
|
1975
|
+
required: ['understanding', 'question'],
|
|
1744
1976
|
},
|
|
1745
1977
|
},
|
|
1746
1978
|
async run(args, ctx) {
|
|
@@ -1749,7 +1981,9 @@ export const TOOLS = {
|
|
|
1749
1981
|
|
|
1750
1982
|
const 고를것 = (Array.isArray(args.options) ? args.options : [])
|
|
1751
1983
|
.map((x) => String(x ?? '').trim()).filter(Boolean).slice(0, 4);
|
|
1752
|
-
|
|
1984
|
+
// 옛 이름(`이해`)도 받는다. 이름을 바꾼 것은 서버가 검사하기 때문이지
|
|
1985
|
+
// 뜻이 달라져서가 아니다 — 옛 이름으로 부르던 자리가 조용히 비면 안 된다.
|
|
1986
|
+
const 이해 = String(args.understanding ?? args.이해 ?? '').trim();
|
|
1753
1987
|
|
|
1754
1988
|
/*
|
|
1755
1989
|
* 물어볼 자리가 없는 데서도 안 죽어야 한다 — `deel -p` 한 방 실행, 파이프,
|
|
@@ -1860,7 +2094,7 @@ export const TOOLS = {
|
|
|
1860
2094
|
return `[${h.세션} · ${날} · ${h.누구}] ${h.토막}`;
|
|
1861
2095
|
});
|
|
1862
2096
|
return {
|
|
1863
|
-
summary:
|
|
2097
|
+
summary: 말('sum.recall', { 전체: r.전체맞음, 맞음: r.맞은것.length })
|
|
1864
2098
|
+ (r.예산초과 ? ` (${r.전체파일}개 중 ${r.본파일}개만 뒤짐)` : ''),
|
|
1865
2099
|
hits: r.맞은것.map((h) => ({ session: h.세션, when: h.언제, who: h.누구, text: h.토막 })),
|
|
1866
2100
|
text: 줄들.join('\n'),
|
|
@@ -1900,7 +2134,7 @@ export const TOOLS = {
|
|
|
1900
2134
|
const r = 더하기(ctx.scope.root, args.text);
|
|
1901
2135
|
if (!r.ok) return { summary: r.why, remembered: false };
|
|
1902
2136
|
return {
|
|
1903
|
-
summary:
|
|
2137
|
+
summary: 이어(말('sum.remembered', { n: r.줄수 }), r.넘침 ? 말('sum.rememberFull') : ''),
|
|
1904
2138
|
remembered: true,
|
|
1905
2139
|
line: r.줄,
|
|
1906
2140
|
// 화면에 무엇을 적었는지 보여 주려고 같이 넘긴다. 사람이 못 보면
|
|
@@ -1972,7 +2206,7 @@ export function 설명줄이기(schema, 한도) {
|
|
|
1972
2206
|
// 이름만 봐도 아는 인자는 아주 좁은 창에서 설명을 통째로 뺀다.
|
|
1973
2207
|
// `file_path` 가 무엇인지 설명하는 데 토큰을 쓰는 것은, 8k 모델에서는
|
|
1974
2208
|
// 그 토큰만큼 대화를 잘라먹는 것과 같다. 헷갈릴 만한 것(offset·files·
|
|
1975
|
-
// replace_all
|
|
2209
|
+
// replace_all·purpose·task)은 그대로 둔다 — 거기서 틀리면 일이 안 된다.
|
|
1976
2210
|
if (한도 <= 100 && 뻔한인자.has(이름)) { 새속성[이름] = { type: 값.type }; continue; }
|
|
1977
2211
|
/*
|
|
1978
2212
|
* 배열 안쪽 설명은 좁은 창에서 통째로 뺀다.
|
|
@@ -2031,14 +2265,28 @@ function 눈붙이기(fn, 이름, vision) {
|
|
|
2031
2265
|
if (이름 !== 'Read' || !vision) return fn;
|
|
2032
2266
|
// 화면 말을 그대로 본다. 설명 글자를 보고 짐작하면 안 된다 — 줄이기가 문장
|
|
2033
2267
|
// 한복판을 자르므로 마지막 글자가 무엇일지 정해져 있지 않다.
|
|
2034
|
-
|
|
2268
|
+
// 위 영어설명() 과 같은 갈림이어야 한다. 여기만 'en' 을 보면, 일본어로
|
|
2269
|
+
// 켠 사람은 영어 설명 뒤에 한국어 한 문장이 붙은 것을 받는다.
|
|
2270
|
+
const 덧말 = 지시말() !== 'ko'
|
|
2035
2271
|
? ' Screenshots and images (.png/.jpg/.gif/.webp) can be opened too — this model can see them.'
|
|
2036
2272
|
: ' 화면 사진·그림(.png/.jpg/.gif/.webp)도 그대로 열 수 있다 — 지금 붙어 있는 모델은 그림을 본다.';
|
|
2037
2273
|
return { ...fn, description: String(fn.description ?? '') + 덧말 };
|
|
2038
2274
|
}
|
|
2039
2275
|
|
|
2040
|
-
|
|
2041
|
-
|
|
2276
|
+
/*
|
|
2277
|
+
* 도구 설명을 영어 표로 갈아 끼운다.
|
|
2278
|
+
*
|
|
2279
|
+
* 쓸말을 밖에서 받는다. 두 군데가 서로 다른 말을 봐야 해서다 —
|
|
2280
|
+
* **모델에게 주는 글**은 지시말() 을 따르고, `/tools` 로 **사람이 보는
|
|
2281
|
+
* 화면**은 언어() 를 따른다. 여태 한 함수가 지시말() 만 봐서, `/tools` 는
|
|
2282
|
+
* 이 함수를 아예 안 거치고 원본(한국어)을 그대로 찍고 있었다.
|
|
2283
|
+
*
|
|
2284
|
+
* ko 가 아니면 영어를 쓴다. 일본어·중국어 표에는 도구 설명이 없는데,
|
|
2285
|
+
* 그 사람들에게는 한국어보다 영어가 낫다 — i18n 의 물러날곳(ja→en→ko)이
|
|
2286
|
+
* 이미 그렇게 정해 두었고, 여기만 그 규칙 밖에 있었다.
|
|
2287
|
+
*/
|
|
2288
|
+
export function 영어설명(schema, 이름, 쓸말 = 지시말()) {
|
|
2289
|
+
if (쓸말 === 'ko') return schema;
|
|
2042
2290
|
const 것 = 도구설명EN[이름];
|
|
2043
2291
|
if (!것) return schema;
|
|
2044
2292
|
|
|
@@ -2093,8 +2341,9 @@ export function toolSchemas(names = null, { hasSkills = false, web = true, work
|
|
|
2093
2341
|
* 다음 영어로 통째로 바꾸는 셈이라, 자른 것이 아무 뜻이 없어지고 영어 글은
|
|
2094
2342
|
* 한도를 넘긴 채로 실린다.
|
|
2095
2343
|
*
|
|
2096
|
-
* 이름과 인자 이름은 안 건드린다 — 그건 식별자다.
|
|
2097
|
-
*
|
|
2344
|
+
* 이름과 인자 이름은 안 건드린다 — 그건 식별자다. 여기서 인자 이름을
|
|
2345
|
+
* 갈아 끼우면 모델이 부른 이름과 우리가 읽는 이름이 어긋나 도구가 아예
|
|
2346
|
+
* 안 불린다. (인자 이름 자체는 이미 다 영문이다 — test/toolargs.test.js.)
|
|
2098
2347
|
*/
|
|
2099
2348
|
const 우리것 = list.map((n) => ({
|
|
2100
2349
|
type: 'function',
|
|
@@ -2203,8 +2452,28 @@ export async function runTool(name, args, ctx) {
|
|
|
2203
2452
|
* 그러면 같은 편집이 두 번 들어가거나, 되돌리기 기록과 어긋난다.
|
|
2204
2453
|
* 읽기만 한 것은 버려도 잃을 것이 없으니 중단으로 끝낸다.
|
|
2205
2454
|
*/
|
|
2455
|
+
/*
|
|
2456
|
+
* 바꿔 놨는지는 **도구가 무엇을 돌려줬나**로 판단한다. 이름으로 짐작하면 안 된다.
|
|
2457
|
+
*
|
|
2458
|
+
* 전에는 `isMutating(name)` 이었다. isMutating 은 **셸 명령줄**을 받아
|
|
2459
|
+
* `mv|cp|rm|git commit…` 을 찾는 함수인데(safety/guard.js), 거기에 도구
|
|
2460
|
+
* 이름을 넣고 있었다. 그래서 `Move` 만 우연히 참이 되고 Write·Edit·
|
|
2461
|
+
* Append·Bash 는 전부 거짓이었다 — 바로 위 주석이 지키겠다고 적은
|
|
2462
|
+
* 도구들이 통째로 빠져 있었다.
|
|
2463
|
+
*
|
|
2464
|
+
* 그 결과가 이렇다. 파일을 다 쓰고 난 뒤 ESC 를 누르면 화면과 모델은
|
|
2465
|
+
* 「중단했습니다」 만 받는다. changed 가 같이 떨어져 나가서 턴 끝의 파일
|
|
2466
|
+
* 목록에도, /diff 에도 안 잡힌다. 그런데 디스크는 이미 바뀌어 있고
|
|
2467
|
+
* 되돌리기 기록에도 올라가 있다. 모델은 안 고쳐진 줄 알고 또 고친다.
|
|
2468
|
+
*
|
|
2469
|
+
* 결과를 보면 짐작할 것이 없다. changed 가 있으면 한 파일을 바꾼 것이고,
|
|
2470
|
+
* 바뀐것들·여럿 이 있으면 여러 개를 바꾼 것이다.
|
|
2471
|
+
*/
|
|
2206
2472
|
if (ctx.signal?.aborted) {
|
|
2207
|
-
|
|
2473
|
+
const 바꿔놨나 = !!(r.changed
|
|
2474
|
+
|| r.바뀐것들?.length
|
|
2475
|
+
|| (Array.isArray(r.여럿) && r.여럿.some((x) => x?.ok && x.path)));
|
|
2476
|
+
if (!바꿔놨나 || r.error) {
|
|
2208
2477
|
return { error: '중단했습니다.', 끝났다: true, 중단됨: true };
|
|
2209
2478
|
}
|
|
2210
2479
|
return { ...r, 중단됨: true, 중단전에끝남: true };
|
|
@@ -2235,11 +2504,16 @@ async function runMcpTool(name, args, ctx) {
|
|
|
2235
2504
|
if (!서버) return { error: `${갈린것.서버} 서버가 붙어 있지 않습니다 — /mcp 로 확인하세요` };
|
|
2236
2505
|
if (!서버.살아있나()) return { error: `${갈린것.서버} 서버가 죽었습니다: ${서버.죽음 ?? '이유 모름'}` };
|
|
2237
2506
|
try {
|
|
2238
|
-
|
|
2507
|
+
// 사람이 누른 ESC 를 남의 프로그램에까지 데려간다. 안 넘기면 도구 한 번에
|
|
2508
|
+
// 60초(부르기제한)를 꼬박 기다리는데, 그 사이 ESC 는 아무 일도 안 한다.
|
|
2509
|
+
const out = await 서버.부르기(갈린것.도구, args, { signal: ctx?.signal ?? null });
|
|
2239
2510
|
if (out.isError) return { error: out.text || '도구가 오류를 냈습니다' };
|
|
2240
2511
|
const 글 = out.text ?? '';
|
|
2241
2512
|
const 줄 = 글 ? 글.split(/\r?\n/).length : 0;
|
|
2242
|
-
return {
|
|
2513
|
+
return {
|
|
2514
|
+
summary: 글 ? 이어(세말('lines', 줄), 말('unit.chars', { n: 글.length.toLocaleString() })) : 말('sum.emptyAnswer'),
|
|
2515
|
+
content: clip(글),
|
|
2516
|
+
};
|
|
2243
2517
|
} catch (e) {
|
|
2244
2518
|
return { error: e.message };
|
|
2245
2519
|
}
|