deel-local-cli 1.8.0 → 1.12.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 +10 -25
- package/README.md +43 -51
- package/bin/deel.js +56 -13
- package/package.json +60 -60
- package/src/acp/serve.js +810 -732
- package/src/agent/budget.js +156 -153
- package/src/agent/compact.js +420 -296
- package/src/agent/effort.js +308 -177
- package/src/agent/evidence.js +2 -0
- package/src/agent/evolve.js +263 -213
- package/src/agent/filemem.js +155 -0
- package/src/agent/grade.js +51 -1
- package/src/agent/loop.js +1707 -1349
- package/src/agent/memory.js +156 -152
- package/src/agent/mention.js +210 -210
- package/src/agent/session.js +1049 -832
- package/src/agent/store.js +186 -9
- package/src/agent/threads.js +26 -1
- package/src/backend/adapter.js +1159 -615
- package/src/backend/cachemark.js +150 -0
- package/src/backend/detect.js +327 -288
- 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 +250 -133
- package/src/backend/retry.js +30 -6
- package/src/backend/tokens.js +37 -0
- package/src/backend/toolfit.js +352 -0
- package/src/backend/wire.js +600 -0
- package/src/commands.js +3135 -2918
- package/src/config.js +47 -2
- package/src/i18n/en.js +544 -466
- package/src/i18n/index.js +18 -0
- package/src/i18n/ja.js +497 -417
- package/src/i18n/ko.js +591 -466
- package/src/i18n/zh.js +497 -417
- package/src/lsp/client.js +49 -5
- package/src/oneshot.js +620 -471
- package/src/pack/sbom.js +30 -4
- package/src/pack/selfpack.js +25 -9
- package/src/pack/sheet.en.js +288 -0
- package/src/pack/tar.js +65 -2
- package/src/plugins/manage.js +46 -9
- package/src/providers/bedrock.js +17 -0
- package/src/repl.js +2602 -2286
- package/src/safety/audit.js +92 -8
- package/src/safety/authcmd.js +14 -3
- package/src/safety/guard.js +143 -0
- package/src/safety/keystore.js +62 -39
- package/src/safety/undo.js +24 -6
- package/src/tools/fsutil.js +265 -250
- package/src/tools/index.js +239 -34
- package/src/tools/jobs.js +158 -29
- package/src/tools/verify.js +358 -328
- package/src/tools/webfetch.js +92 -9
- 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/src/ui/status.js +613 -610
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';
|
|
@@ -41,6 +41,15 @@ import { 지시말, 말, 세말 } from '../i18n/index.js';
|
|
|
41
41
|
* 그 자리를 부르는 쪽마다 살피게 두면 반드시 어딘가는 빠진다.
|
|
42
42
|
*/
|
|
43
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;
|
|
44
53
|
import { 표몇군데, 표막는말 } from '../safety/secrets.js';
|
|
45
54
|
|
|
46
55
|
/*
|
|
@@ -514,6 +523,38 @@ function pdf읽기(abs, ctx) {
|
|
|
514
523
|
* **손대지 않는다** — 옮겨진 자리에 그대로 남는다. 되돌아가지는 않지만
|
|
515
524
|
* 없어지지도 않는다. 안전망이 파일을 지우는 것보다 이쪽이 낫다.
|
|
516
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
|
+
|
|
517
558
|
function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
518
559
|
if (typeof from !== 'string' || !from) return { error: 'from 이 없습니다' };
|
|
519
560
|
if (typeof to !== 'string' || !to) return { error: 'to 가 없습니다' };
|
|
@@ -573,6 +614,7 @@ function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
573
614
|
}
|
|
574
615
|
|
|
575
616
|
mkdirSync(dirname(뒤), { recursive: true });
|
|
617
|
+
let 원본남음 = null;
|
|
576
618
|
try {
|
|
577
619
|
renameSync(앞, 뒤);
|
|
578
620
|
} catch (err) {
|
|
@@ -582,19 +624,23 @@ function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
582
624
|
* 복사 도중에 끊기면 양쪽에 반씩 남기 때문이다.
|
|
583
625
|
*/
|
|
584
626
|
if (err.code !== 'EXDEV') return { error: `못 옮겼습니다: ${err.message}` };
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
return { error: `못 옮겼습니다: ${
|
|
627
|
+
const 벌어진일 = 복사해옮기기(앞, 뒤);
|
|
628
|
+
if (벌어진일.복사깨짐) {
|
|
629
|
+
// 닿은 자리를 여기서 지우지 않는다 — 이미 있던 폴더로 옮기는 중이었으면
|
|
630
|
+
// 남의 파일까지 지운다. 어디에 반쯤 남았는지만 정확히 말한다.
|
|
631
|
+
return { error: `못 옮겼습니다: ${벌어진일.복사깨짐}\n(옮기다 만 것이 ${ctx.scope.show(뒤)} 에 남아 있을 수 있습니다 — 지우기 전에 확인하세요)` };
|
|
590
632
|
}
|
|
633
|
+
원본남음 = 벌어진일.원본남음 ?? null;
|
|
591
634
|
}
|
|
592
635
|
|
|
593
636
|
for (const [, b] of 짝들) ctx.seen.add(b);
|
|
594
637
|
const 무엇 = 폴더인가 ? `폴더 ${짝들.length}개 파일` : '';
|
|
595
|
-
const 경고 = 되돌리기반쪽
|
|
638
|
+
const 경고 = (되돌리기반쪽
|
|
596
639
|
? `\n(파일이 ${훑은것.상한.toLocaleString('en-US')}개를 넘어 되돌리기에는 앞부분만 떴습니다 — 옮기기는 전부 됐지만 /undo 는 다 못 되돌립니다)`
|
|
597
|
-
: ''
|
|
640
|
+
: '')
|
|
641
|
+
+ (원본남음
|
|
642
|
+
? `\n(닿은 자리에는 다 옮겼는데 원본을 못 지웠습니다 — 지금 ${ctx.scope.show(앞)} 에도 그대로 있습니다: ${원본남음})`
|
|
643
|
+
: '');
|
|
598
644
|
return {
|
|
599
645
|
content: `옮김: ${ctx.scope.show(앞)} → ${ctx.scope.show(뒤)}${무엇 ? ` (${무엇})` : ''}${경고}`,
|
|
600
646
|
summary: `${ctx.scope.show(뒤)}`,
|
|
@@ -1042,14 +1088,39 @@ export const TOOLS = {
|
|
|
1042
1088
|
*/
|
|
1043
1089
|
const 준줄수 = slice.length;
|
|
1044
1090
|
const 다못줌 = start + count < lines.length || start > 0;
|
|
1091
|
+
const 통째로 = body + more + 줄인것.알림;
|
|
1092
|
+
const 실린것 = clip(통째로);
|
|
1045
1093
|
return {
|
|
1046
|
-
content:
|
|
1094
|
+
content: 실린것,
|
|
1095
|
+
/*
|
|
1096
|
+
* 어느 파일을 읽었는지 알려 준다 — 대화에 실을 때 앞서 읽은 것과
|
|
1097
|
+
* 견주려면 열쇠가 있어야 한다(agent/filemem.js).
|
|
1098
|
+
*
|
|
1099
|
+
* `부분인가` 는 **우리가 들고 있는 것이 파일 전부가 아니다** 는 뜻이다.
|
|
1100
|
+
* 줄로 잘렸든(offset·limit·창 상한), 그림을 줄였든, clip 에 걸렸든
|
|
1101
|
+
* 마찬가지다. 조각을 기억해 두고 나중에 「그대로입니다」 라고 하면
|
|
1102
|
+
* 모델은 안 본 데를 봤다고 여긴다 — 줄이려다 잃는 자리다.
|
|
1103
|
+
*/
|
|
1104
|
+
읽은것: abs,
|
|
1105
|
+
부분인가: 다못줌 || 줄인것.줄인바이트 > 0 || 실린것.length < 통째로.length,
|
|
1047
1106
|
summary: 이어(
|
|
1048
1107
|
다못줌
|
|
1049
1108
|
? `${말('sum.linesOf', { 준: 준줄수, 전체: lines.length })} (${말('sum.partial')})`
|
|
1050
1109
|
: 세말('lines', lines.length),
|
|
1051
1110
|
줄인것.줄인바이트 ? 말('sum.imageDropped', { 크기: 몇KB(줄인것.줄인바이트) }) : '',
|
|
1052
|
-
|
|
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) }),
|
|
1053
1124
|
),
|
|
1054
1125
|
};
|
|
1055
1126
|
},
|
|
@@ -1617,9 +1688,29 @@ export const TOOLS = {
|
|
|
1617
1688
|
// '�Ľ� ����' 이 된다. 바이트로 받아 이 컴퓨터가 쓰는 것으로 해독한다.
|
|
1618
1689
|
const kid = execFile(shell.file, shell.args, {
|
|
1619
1690
|
cwd: ctx.scope.root,
|
|
1620
|
-
|
|
1621
|
-
|
|
1691
|
+
// 열쇠만 빼고 나머지는 그대로 물려준다 (backend/mcp.js 열쇠뺀환경 머리말).
|
|
1692
|
+
// 안 빼면 `env` 한 줄이 열쇠를 화면과 대화 기록에 그대로 싣는다.
|
|
1693
|
+
env: 열쇠뺀환경(),
|
|
1694
|
+
// 우리 시계(아래 뒷북)가 제한에서 먼저 나무를 끊는다. 이건 그것마저
|
|
1695
|
+
// 못 돌았을 때 서는 마지막 그물이라 뒤에 세운다.
|
|
1696
|
+
timeout: 제한 + 3000,
|
|
1697
|
+
maxBuffer: 출력상한,
|
|
1622
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',
|
|
1623
1714
|
windowsVerbatimArguments: shell.verbatim === true,
|
|
1624
1715
|
encoding: 'buffer',
|
|
1625
1716
|
}, (err, stdoutBuf, stderrBuf) => {
|
|
@@ -1636,7 +1727,9 @@ export const TOOLS = {
|
|
|
1636
1727
|
const out = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
1637
1728
|
|
|
1638
1729
|
if (끊겼나) return done({ error: '사용자가 중단했습니다', content: clip(out) });
|
|
1639
|
-
if (err && err.killed)
|
|
1730
|
+
if (시간초과 || (err && err.killed)) {
|
|
1731
|
+
return done({ error: `시간 초과로 중단됨 (${제한}ms)`, content: clip(out) });
|
|
1732
|
+
}
|
|
1640
1733
|
|
|
1641
1734
|
/*
|
|
1642
1735
|
* 결과를 사실대로 말한다.
|
|
@@ -1648,15 +1741,39 @@ export const TOOLS = {
|
|
|
1648
1741
|
* 종료코드도 모델에게 준다. 전에는 화면에만 적고 대화에는 안 실었다.
|
|
1649
1742
|
* 그러면 모델은 명령이 실패한 줄 모른 채 다음 단계로 넘어간다.
|
|
1650
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
|
+
*/
|
|
1651
1758
|
const 시그널 = err?.signal ?? null;
|
|
1652
|
-
const
|
|
1653
|
-
const
|
|
1654
|
-
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}]`;
|
|
1655
1770
|
done({
|
|
1656
1771
|
content: clip(out || '(출력 없음)') + 꼬리,
|
|
1657
1772
|
summary: 잘됨 ? 말('sum.ok')
|
|
1658
1773
|
: 시그널 ? 말('sum.killedBy', { 시그널 })
|
|
1659
|
-
: 말('sum.
|
|
1774
|
+
: 넘침 ? 말('sum.tooMuchOut')
|
|
1775
|
+
: 숫자코드 === null ? 말('sum.cantRun', { 왜: 글자코드 })
|
|
1776
|
+
: 말('sum.exitCode', { code }),
|
|
1660
1777
|
failed: !잘됨,
|
|
1661
1778
|
exitCode: code,
|
|
1662
1779
|
signal: 시그널,
|
|
@@ -1672,23 +1789,71 @@ export const TOOLS = {
|
|
|
1672
1789
|
* Ctrl+C 는 다음 요청에나 반영됐다. 터미널을 닫는 수밖에 없었다.
|
|
1673
1790
|
*/
|
|
1674
1791
|
let 끊겼나 = false;
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
+
*/
|
|
1679
1824
|
if (process.platform === 'win32' && kid.pid) {
|
|
1680
|
-
|
|
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);
|
|
1681
1837
|
}
|
|
1682
1838
|
try { kid.kill(); } catch {}
|
|
1683
1839
|
// 죽이라고 시켰다고 곧바로 죽는 것은 아니다. 그동안 이 프로세스가
|
|
1684
1840
|
// 그 손을 붙들고 있으면 deel 을 끝내도 안 끝난다 — 실제로 검사가
|
|
1685
1841
|
// 60초를 더 기다렸다. 놓아 주고 우리 갈 길을 간다.
|
|
1686
1842
|
try { kid.unref(); } catch {}
|
|
1687
|
-
|
|
1843
|
+
/*
|
|
1844
|
+
* 파이프는 부르는 쪽이 고른다.
|
|
1845
|
+
*
|
|
1846
|
+
* 끊으면 이벤트 루프를 안 붙잡아서 좋은데, 그 순간 아직 안 읽힌
|
|
1847
|
+
* 것이 통째로 사라진다. 하필 그게 제일 중요한 몇 줄이다 — 서버가
|
|
1848
|
+
* 뻗으며 남긴 스택 트레이스가 거기 있다. 시간 초과에서는 그 몇
|
|
1849
|
+
* 줄을 받아야 하므로 안 끊는다. (jobs.js 의 끝내기() 와 같은 판단)
|
|
1850
|
+
*/
|
|
1851
|
+
if (파이프끊기) { try { kid.stdout?.destroy(); kid.stderr?.destroy(); } catch {} }
|
|
1688
1852
|
};
|
|
1689
1853
|
const 끊기 = () => {
|
|
1690
1854
|
끊겼나 = true;
|
|
1691
|
-
|
|
1855
|
+
// Ctrl+C 는 짧게 — 사람이 지금 돌려받으려고 누른 것이다.
|
|
1856
|
+
죽이기({ 더줄까: 1200 });
|
|
1692
1857
|
clearTimeout(뒷북);
|
|
1693
1858
|
// 콜백을 기다리지 않는다. 손자가 파이프를 물고 있으면 안 불릴 수도 있다.
|
|
1694
1859
|
done({ error: '사용자가 중단했습니다' });
|
|
@@ -1697,17 +1862,35 @@ export const TOOLS = {
|
|
|
1697
1862
|
else ctx.signal?.addEventListener?.('abort', 끊기, { once: true });
|
|
1698
1863
|
|
|
1699
1864
|
/*
|
|
1700
|
-
*
|
|
1865
|
+
* ── 시간 초과는 **우리가** 끊는다 ───────────────────────────────
|
|
1866
|
+
*
|
|
1867
|
+
* execFile 의 timeout 은 바로 아래 자식(윈도우면 cmd.exe)에게만
|
|
1868
|
+
* 시그널을 보낸다. 손자는 안 건드린다. 그래서 오래 이랬다 —
|
|
1869
|
+
*
|
|
1870
|
+
* Bash(npm run dev) → 시간 초과로 중단됨 (120000ms)
|
|
1871
|
+
* 실제로는 → 서버가 그대로 돌면서 포트를 물고 있다
|
|
1872
|
+
*
|
|
1873
|
+
* 화면에는 「중단됨」 이 뜨는데 프로세스는 살아 있다. 무엇이 포트를
|
|
1874
|
+
* 물고 있는지 알 길이 없는, 이 프로그램이 없애려던 바로 그 상태다.
|
|
1701
1875
|
*
|
|
1702
|
-
*
|
|
1703
|
-
*
|
|
1704
|
-
*
|
|
1876
|
+
* 그렇다고 콜백에서 뒤늦게 나무를 끊어도 소용없다. 그때는 cmd 가
|
|
1877
|
+
* 이미 죽어 나무의 뿌리가 없어서 taskkill 이 손자를 못 찾는다.
|
|
1878
|
+
* **cmd 가 살아 있는 동안** 끊어야 한다. 그래서 우리 시계를 제한에
|
|
1879
|
+
* 두고, execFile 의 timeout 은 그 뒤에 서는 마지막 그물로 남긴다.
|
|
1880
|
+
*
|
|
1881
|
+
* 끊고 나서 곧장 끝맺지 않는다. 나무가 끊기면 대개 곧바로 콜백이
|
|
1882
|
+
* 오는데, 그때라야 죽기 직전에 나온 몇 줄을 같이 실어 줄 수 있다.
|
|
1883
|
+
* 그것마저 안 오면 아래 그물이 대신 끝맺는다.
|
|
1705
1884
|
*/
|
|
1706
1885
|
const 뒷북 = setTimeout(() => {
|
|
1707
1886
|
if (끝났나 || 끊겼나) return;
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1887
|
+
시간초과 = true;
|
|
1888
|
+
죽이기({ 파이프끊기: false, 더줄까: 4000 });
|
|
1889
|
+
const 그물 = setTimeout(() => {
|
|
1890
|
+
done({ error: `시간 초과로 중단됨 (${제한}ms) — 자식 프로세스가 안 끝나 강제로 끝냈습니다` });
|
|
1891
|
+
}, 1000);
|
|
1892
|
+
그물.unref?.();
|
|
1893
|
+
}, 제한);
|
|
1711
1894
|
// 이 시계 때문에 프로그램이 안 끝나면 안 된다.
|
|
1712
1895
|
뒷북.unref?.();
|
|
1713
1896
|
});
|
|
@@ -2269,8 +2452,28 @@ export async function runTool(name, args, ctx) {
|
|
|
2269
2452
|
* 그러면 같은 편집이 두 번 들어가거나, 되돌리기 기록과 어긋난다.
|
|
2270
2453
|
* 읽기만 한 것은 버려도 잃을 것이 없으니 중단으로 끝낸다.
|
|
2271
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
|
+
*/
|
|
2272
2472
|
if (ctx.signal?.aborted) {
|
|
2273
|
-
|
|
2473
|
+
const 바꿔놨나 = !!(r.changed
|
|
2474
|
+
|| r.바뀐것들?.length
|
|
2475
|
+
|| (Array.isArray(r.여럿) && r.여럿.some((x) => x?.ok && x.path)));
|
|
2476
|
+
if (!바꿔놨나 || r.error) {
|
|
2274
2477
|
return { error: '중단했습니다.', 끝났다: true, 중단됨: true };
|
|
2275
2478
|
}
|
|
2276
2479
|
return { ...r, 중단됨: true, 중단전에끝남: true };
|
|
@@ -2301,7 +2504,9 @@ async function runMcpTool(name, args, ctx) {
|
|
|
2301
2504
|
if (!서버) return { error: `${갈린것.서버} 서버가 붙어 있지 않습니다 — /mcp 로 확인하세요` };
|
|
2302
2505
|
if (!서버.살아있나()) return { error: `${갈린것.서버} 서버가 죽었습니다: ${서버.죽음 ?? '이유 모름'}` };
|
|
2303
2506
|
try {
|
|
2304
|
-
|
|
2507
|
+
// 사람이 누른 ESC 를 남의 프로그램에까지 데려간다. 안 넘기면 도구 한 번에
|
|
2508
|
+
// 60초(부르기제한)를 꼬박 기다리는데, 그 사이 ESC 는 아무 일도 안 한다.
|
|
2509
|
+
const out = await 서버.부르기(갈린것.도구, args, { signal: ctx?.signal ?? null });
|
|
2305
2510
|
if (out.isError) return { error: out.text || '도구가 오류를 냈습니다' };
|
|
2306
2511
|
const 글 = out.text ?? '';
|
|
2307
2512
|
const 줄 = 글 ? 글.split(/\r?\n/).length : 0;
|