deel-local-cli 1.1.1 → 1.4.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.en.md +826 -19
- package/README.md +689 -14
- package/bin/deel.js +73 -4
- package/package.json +7 -3
- package/src/acp/jsonrpc.js +230 -0
- package/src/acp/map.js +219 -0
- package/src/acp/serve.js +556 -0
- package/src/agent/budget.js +0 -14
- package/src/agent/card.js +110 -0
- package/src/agent/compact.js +95 -0
- package/src/agent/evidence.js +186 -0
- package/src/agent/evolve.js +213 -0
- package/src/agent/grade.js +20 -0
- package/src/agent/loop.js +172 -14
- package/src/agent/models.js +169 -0
- package/src/agent/modes.js +49 -1
- package/src/agent/pins.js +140 -0
- package/src/agent/project.js +10 -4
- package/src/agent/session.js +359 -11
- package/src/agent/store.js +31 -0
- package/src/agent/threads.js +154 -0
- package/src/commands.js +751 -55
- package/src/config.js +10 -1
- package/src/i18n/en.js +244 -0
- package/src/i18n/index.js +126 -0
- package/src/i18n/ko.js +231 -0
- package/src/lsp/client.js +459 -0
- package/src/lsp/diag.js +112 -0
- package/src/lsp/rpc.js +84 -0
- package/src/lsp/servers.js +218 -0
- package/src/oneshot.js +23 -0
- package/src/pack/sbom.js +218 -0
- package/src/pack/selfpack.js +19 -3
- package/src/repl.js +273 -22
- package/src/safety/guard.js +37 -2
- package/src/safety/secrets.js +205 -0
- package/src/safety/undo.js +11 -4
- package/src/setup.js +2 -2
- package/src/skills/discover.js +1 -1
- package/src/tools/desc.en.js +219 -0
- package/src/tools/edit-match.js +16 -5
- package/src/tools/excel-com.js +1 -1
- package/src/tools/excel.js +1 -1
- package/src/tools/fsutil.js +0 -8
- package/src/tools/index.js +149 -6
- package/src/tools/lsp.js +327 -0
- package/src/tools/task.js +30 -2
- package/src/tools/todo.js +19 -3
- package/src/ui/ansi.js +42 -2
- package/src/ui/approve.js +25 -21
- package/src/ui/inputbox.js +69 -13
- package/src/ui/intro.js +174 -0
- package/src/ui/level.js +11 -5
- package/src/ui/md.js +227 -0
- package/src/ui/notify.js +101 -0
- package/src/ui/prompt.js +1 -1
- package/src/ui/screen.js +1 -1
- package/src/ui/status.js +190 -34
- package/src/ui/working.js +0 -3
- package/src/ui/wrap.js +1 -8
package/src/repl.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
// 대화 화면. 루프가 보내는 이벤트를 Claude Code 풍으로 그린다.
|
|
2
2
|
import { createInterface, emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
4
|
+
import { resolve, basename } from 'node:path';
|
|
5
5
|
import { c, say as 바로쓰기, mark, clip } from './ui/ansi.js';
|
|
6
6
|
import { headerLines } from './ui/status.js';
|
|
7
|
+
import { 종, 창제목, 제목되돌리기, 알릴까, 제목글 } from './ui/notify.js';
|
|
8
|
+
import { 보이기 as 인트로, 기본곁말 } from './ui/intro.js';
|
|
9
|
+
import { 언어잡기 } from './i18n/index.js';
|
|
7
10
|
import { 화면고르기 } from './ui/screen.js';
|
|
8
11
|
import { STAGES } from './agent/effort.js';
|
|
9
12
|
import { handle, COMMANDS, 미리보기끄기 } from './commands.js';
|
|
10
|
-
import { next as nextWork, get as getWork, canWrite } from './agent/modes.js';
|
|
13
|
+
import { next as nextWork, get as getWork, canWrite, 보일이름, 보일한줄 } from './agent/modes.js';
|
|
11
14
|
import { route } from './agent/route.js';
|
|
12
15
|
import { run } from './agent/loop.js';
|
|
13
|
-
import { Session } from './agent/session.js';
|
|
16
|
+
import { Session, repairToolPairs } from './agent/session.js';
|
|
14
17
|
import { makeScope } from './safety/guard.js';
|
|
18
|
+
import { 언어서버있나 } from './tools/index.js';
|
|
19
|
+
import { 모두끄기 as 언어서버다끄기 } from './lsp/client.js';
|
|
15
20
|
import { History } from './safety/undo.js';
|
|
16
21
|
import { Audit } from './safety/audit.js';
|
|
17
|
-
import { activeProfile, load, resolveKey, save as saveCfg } from './config.js';
|
|
22
|
+
import { activeProfile, load, resolveKey, save as saveCfg, homeDir } from './config.js';
|
|
18
23
|
import { discover } from './skills/discover.js';
|
|
19
24
|
import { allowEndpoint, setOffline, isOffline, isLocalHost } from './safety/network.js';
|
|
20
25
|
import { Store, latest, prune } from './agent/store.js';
|
|
26
|
+
import { Threads } from './agent/threads.js';
|
|
27
|
+
import { 못박기 } from './agent/pins.js';
|
|
28
|
+
import { 카드 } from './agent/card.js';
|
|
29
|
+
import { 배움 } from './agent/evolve.js';
|
|
30
|
+
import { 마크다운 } from './ui/md.js';
|
|
21
31
|
import { askHidden } from './ui/prompt.js';
|
|
22
32
|
import { explain, shows as levelShows } from './ui/level.js';
|
|
23
33
|
import { 고르기 as 승인고르기, 다음 as 승인다음 } from './ui/approve.js';
|
|
@@ -27,7 +37,7 @@ import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
|
27
37
|
import { renderDiff, shortStat } from './ui/diff.js';
|
|
28
38
|
import { expand as expandMentions } from './agent/mention.js';
|
|
29
39
|
import { 다붙이기 } from './backend/mcp.js';
|
|
30
|
-
import {
|
|
40
|
+
import { 읽기 as 기억읽기 } from './agent/memory.js';
|
|
31
41
|
import { 갈래고르기 } from './ui/working.js';
|
|
32
42
|
import { 모두끝내기 as 일감모두끝내기, 일감인자 } from './tools/jobs.js';
|
|
33
43
|
|
|
@@ -157,6 +167,27 @@ export async function chatLoop(opts = {}) {
|
|
|
157
167
|
allowEndpoint(conn.base);
|
|
158
168
|
if (opts.offline ?? prof.offline) setOffline(true);
|
|
159
169
|
|
|
170
|
+
/*
|
|
171
|
+
* ── 끝났을 때 알리기 ─────────────────────────────────────────────────
|
|
172
|
+
*
|
|
173
|
+
* 로컬 모델은 느리다. 7B 를 CPU 로 돌리면 한 턴에 2~3분이 예사고, 그동안
|
|
174
|
+
* 사람은 다른 창으로 간다. 돌아와 보면 5분 전에 끝나 있거나, 더 나쁘게는
|
|
175
|
+
* "실행할까요?" 에서 3분째 멈춰 서 있다 — 물어본 줄을 몰라서.
|
|
176
|
+
*
|
|
177
|
+
* 폴더 이름을 같이 들고 있는다. 창을 여럿 띄워 놓고 쓰는 사람이 많은데
|
|
178
|
+
* 전부 'deel' 이면 어느 탭이 끝난 건지 알 수가 없다. (ui/notify.js)
|
|
179
|
+
*/
|
|
180
|
+
const 알림 = { 켬: cfg.bell !== false, 폴더: basename(root) };
|
|
181
|
+
|
|
182
|
+
/*
|
|
183
|
+
* 화면 말을 여기서 한 번 정한다.
|
|
184
|
+
*
|
|
185
|
+
* 켤 때 딱 한 번이면 된다 — 그 뒤로는 /lang 이 바꾼다. 환경변수가 설정을
|
|
186
|
+
* 이긴다. 한 번만 영어로 켜 보려는 사람이 설정을 건드리지 않고
|
|
187
|
+
* `DEEL_LANG=en deel` 로 할 수 있어야 하기 때문이다. (i18n/index.js)
|
|
188
|
+
*/
|
|
189
|
+
언어잡기({ cfg });
|
|
190
|
+
|
|
160
191
|
const session = new Session(conn, {
|
|
161
192
|
root,
|
|
162
193
|
mode: opts.mode ?? 'auto',
|
|
@@ -179,11 +210,30 @@ export async function chatLoop(opts = {}) {
|
|
|
179
210
|
say(` ${c.gray('이어할 대화가 없습니다. 새로 시작합니다.')}`);
|
|
180
211
|
} else {
|
|
181
212
|
store = new Store(root, target);
|
|
182
|
-
const { messages } = store.load();
|
|
213
|
+
const { messages: 적힌것 } = store.load();
|
|
214
|
+
/*
|
|
215
|
+
* 도구가 도는 중에 죽었으면 호출만 적히고 결과가 없다. 그대로 보내면
|
|
216
|
+
* 규격 서버가 400 을 내서 이어받자마자 첫 마디에서 죽는다. 손봐서 받는다.
|
|
217
|
+
*/
|
|
218
|
+
const { messages, 고친것 } = repairToolPairs(적힌것);
|
|
183
219
|
if (messages.length) {
|
|
184
220
|
session.messages = messages;
|
|
185
221
|
say('');
|
|
186
222
|
say(` ${mark.ok} ${c.bold(target)} ${c.gray(`— 메시지 ${messages.length}개를 이어 받았습니다.`)}`);
|
|
223
|
+
if (고친것) {
|
|
224
|
+
say(` ${c.gray(`중단된 도구 호출 ${고친것}개를 걷어냈습니다 — 그때 하던 일은 다시 시켜 주세요.`)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/*
|
|
228
|
+
* 못 박아 둔 것도 같이 되살린다 (agent/pins.js).
|
|
229
|
+
*
|
|
230
|
+
* 이걸 안 되살리면 '접어도 요약해도 안 지워진다' 가 **껐다 켜는 한 번에**
|
|
231
|
+
* 거짓이 된다. 사람은 이미 말했다고 믿고 있으니 다시 말하지 않는다.
|
|
232
|
+
*/
|
|
233
|
+
const 박힌것 = store.못박은것읽기();
|
|
234
|
+
if (박힌것.length) {
|
|
235
|
+
session.못박은것 = new 못박기(박힌것);
|
|
236
|
+
say(` ${c.gray(`못 박아 둔 것 ${session.못박은것.개수()}개도 그대로 이어 받았습니다 —`)} ${c.cyan('/pin')}`);
|
|
187
237
|
}
|
|
188
238
|
}
|
|
189
239
|
}
|
|
@@ -211,6 +261,14 @@ export async function chatLoop(opts = {}) {
|
|
|
211
261
|
// 세션도 알아야 한다 — 밖에서 붙인 도구도 스키마가 매 요청에 실린다.
|
|
212
262
|
// 안 세면 컨텍스트가 그만큼 조용히 줄어든다.
|
|
213
263
|
session.mcp = mcp붙임.서버들;
|
|
264
|
+
/*
|
|
265
|
+
* 이 자리에 언어 서버가 깔려 있나 (Def·Refs 를 목록에 넣을지).
|
|
266
|
+
*
|
|
267
|
+
* 여기서 한 번만 잰다. PATH 훑기와 폴더 훑기라 값이 있고, 세션 도중에
|
|
268
|
+
* 답이 바뀌지 않는다. 아무것도 안 깔아 준다 — 있으면 쓰고 없으면 없는 대로
|
|
269
|
+
* Grep·Outline 으로 간다 (lsp/servers.js 머리말).
|
|
270
|
+
*/
|
|
271
|
+
session.lsp = 언어서버있나(root);
|
|
214
272
|
// 지난 대화에서 정해 둔 것을 들고 시작한다.
|
|
215
273
|
// 기억은 Session 이 켤 때 직접 읽는다 (session.js 생성자) — deel run 도 같은 것을 들고 시작하게.
|
|
216
274
|
|
|
@@ -346,7 +404,7 @@ export async function chatLoop(opts = {}) {
|
|
|
346
404
|
session.work = nextWork(session.work);
|
|
347
405
|
const w = getWork(session.work);
|
|
348
406
|
화면.입력지움();
|
|
349
|
-
say(` ${c.hcyan(w.glyph)} ${c.bold(w.
|
|
407
|
+
say(` ${c.hcyan(w.glyph)} ${c.bold(보일이름(w.id))} ${c.gray('(' + w.en + ')')} ${c.gray(보일한줄(w.id))}`
|
|
350
408
|
+ (canWrite(session.work) ? '' : ` ${c.green('· 파일을 못 바꿉니다')}`));
|
|
351
409
|
prompt();
|
|
352
410
|
return;
|
|
@@ -490,6 +548,8 @@ export async function chatLoop(opts = {}) {
|
|
|
490
548
|
mcp: mcp붙임.서버들,
|
|
491
549
|
skills: found.skills,
|
|
492
550
|
loadedSkills: new Set(),
|
|
551
|
+
// 고친 뒤 진단을 볼지. /lsp off 로 끈다 (lsp/diag.js).
|
|
552
|
+
lsp: { 켬: true },
|
|
493
553
|
ask,
|
|
494
554
|
// 암호는 여기서만 받는다. 받은 값은 도구가 쓰고 버린다 —
|
|
495
555
|
// 설정에도, 세션 기록에도, 감사기록에도, 명령줄에도 안 남는다.
|
|
@@ -501,11 +561,55 @@ export async function chatLoop(opts = {}) {
|
|
|
501
561
|
confirm: async (name, args) => {
|
|
502
562
|
say('');
|
|
503
563
|
say(` ${c.yellow('?')} ${toolLabel(name, args)}`);
|
|
564
|
+
/*
|
|
565
|
+
* 여기서 종을 울린다. 끝난 것은 늦게 알아도 되지만 **막혀 있는 것은**
|
|
566
|
+
* 기다린 만큼 그대로 손해다 — 다른 창에 가 있는 사이 3분째 이 줄에서
|
|
567
|
+
* 멈춰 서 있는 일이 실제로 잦다. 시간 문턱도 안 본다, 0초여도 알린다.
|
|
568
|
+
*/
|
|
569
|
+
if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
|
|
570
|
+
창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
|
|
504
571
|
const a = (await ask('실행할까요? (y/n)', { def: 'y' })).toLowerCase();
|
|
572
|
+
창제목(제목글('도는중', { 폴더: 알림.폴더 }));
|
|
505
573
|
return a === 'y' || a === 'yes' || a === 'ㅇ';
|
|
506
574
|
},
|
|
507
575
|
};
|
|
508
576
|
|
|
577
|
+
/*
|
|
578
|
+
* 겪어 본 것 (agent/evolve.js). 쓸수록 이 PC 에 맞춰 나아지는 자리다.
|
|
579
|
+
*
|
|
580
|
+
* 켤 때 두 가지를 받아 온다.
|
|
581
|
+
* · 프롬프트에 실을 몇 줄 — 여기서 되는 명령, 이 모델의 버릇
|
|
582
|
+
* · 지난번에 알아낸 토큰 배수 — 첫 턴부터 제대로 셈한다
|
|
583
|
+
*
|
|
584
|
+
* 둘 다 없으면 아무 일도 안 일어난다. 처음 켠 PC 는 지금과 똑같이 돈다.
|
|
585
|
+
*/
|
|
586
|
+
ctx.배움 = new 배움(root, homeDir());
|
|
587
|
+
session.배움요약 = ctx.배움.요약(conn.model);
|
|
588
|
+
const 아는배수 = ctx.배움.아는보정(conn.model);
|
|
589
|
+
if (아는배수) { session.보정 = 아는배수; session.보정잰것 = 1; }
|
|
590
|
+
|
|
591
|
+
/*
|
|
592
|
+
* 모델 카드 (agent/card.js).
|
|
593
|
+
*
|
|
594
|
+
* 겪어 본 버릇을 **하네스 설정으로** 바꾼다. 프롬프트에 "이렇게 해라" 고 적는
|
|
595
|
+
* 것과 다르다 — 작은 모델은 그 말을 잘 안 듣는다. 여기서 바꾸는 것은 deel 의
|
|
596
|
+
* 행동이라 모델의 협조가 필요 없다.
|
|
597
|
+
*
|
|
598
|
+
* 턴마다 다시 만든다. 이번 대화에서 겪는 것이 계속 쌓이므로, 켤 때 한 번
|
|
599
|
+
* 만들어 두면 그 뒤에 알아낸 것이 이번 대화에서는 안 쓰인다.
|
|
600
|
+
*/
|
|
601
|
+
ctx.카드다시 = () => {
|
|
602
|
+
ctx.카드 = 카드(conn.model, session.본것, ctx.배움?.현황(conn.model)?.모델);
|
|
603
|
+
return ctx.카드;
|
|
604
|
+
};
|
|
605
|
+
ctx.카드다시();
|
|
606
|
+
|
|
607
|
+
/*
|
|
608
|
+
* 대화 갈래. 연결·도구·되돌리기는 같이 쓰고 오간 말만 여러 벌 갖는다.
|
|
609
|
+
* 갈래마다 저장 파일을 따로 열어서, 나중에 `/sessions` 로 각각 찾아갈 수 있다.
|
|
610
|
+
*/
|
|
611
|
+
ctx.갈래 = new Threads(session, ctx, () => new Store(root).begin({ model: conn.model, base: conn.base, root }), store);
|
|
612
|
+
|
|
509
613
|
// ── 컨텍스트 길이를 모델에서 긁어온다 ─────────────────────────────────
|
|
510
614
|
//
|
|
511
615
|
// 켤 때마다 서버에 물어본다. 저장된 값을 그대로 믿지 않는다 —
|
|
@@ -554,6 +658,26 @@ export async function chatLoop(opts = {}) {
|
|
|
554
658
|
}
|
|
555
659
|
}
|
|
556
660
|
|
|
661
|
+
/*
|
|
662
|
+
* ── 켤 때 도는 글자 모션 ──────────────────────────────────────────────
|
|
663
|
+
*
|
|
664
|
+
* 사람은 `deel` 이라고 치고 들어온다. 그 글자가 그 자리에서 deel-local 로
|
|
665
|
+
* 자라고, 아래로 선이 그어지며 닫힌다. 꾸미기만은 아니다 — 이 프로그램이
|
|
666
|
+
* 하는 말이 딱 하나인데(소스가 이 컴퓨터 밖으로 안 나간다) 그 말을 켜는
|
|
667
|
+
* 첫 1초에 그림으로 한 번 하는 것이다. 바깥 게이트웨이면 같은 선이 노랗게
|
|
668
|
+
* 그어진다. 그러면 사람이 그 자리에서 안다.
|
|
669
|
+
*
|
|
670
|
+
* 여기서 도는 이유는 **연결이 정해진 다음**이라야 색을 제대로 칠하기
|
|
671
|
+
* 때문이다. 더 일찍 돌면 초록으로 그려 놓고 나중에 바깥이었다고 정정하는
|
|
672
|
+
* 꼴이 되는데, 그건 안 보여 주느니만 못하다.
|
|
673
|
+
*/
|
|
674
|
+
{
|
|
675
|
+
let 바깥 = false;
|
|
676
|
+
try { 바깥 = !isLocalHost(new URL(conn.base).hostname); } catch { 바깥 = false; }
|
|
677
|
+
say('');
|
|
678
|
+
await 인트로({ 바깥, 곁말: 기본곁말(바깥), 쓰기: (t) => process.stdout.write(t) });
|
|
679
|
+
}
|
|
680
|
+
|
|
557
681
|
// ── 머리말 ────────────────────────────────────────────────────────────
|
|
558
682
|
화면.머리말(headerLines(session, found));
|
|
559
683
|
const warn = [];
|
|
@@ -729,7 +853,7 @@ export async function chatLoop(opts = {}) {
|
|
|
729
853
|
계획승인받나 = 골라진.겹침 === true;
|
|
730
854
|
const w = getWork(골라진.mode);
|
|
731
855
|
say('');
|
|
732
|
-
say(` ${c.hcyan(w.glyph)} ${c.bold(w.
|
|
856
|
+
say(` ${c.hcyan(w.glyph)} ${c.bold(보일이름(w.id))} ${c.gray('(' + w.en + ')')}`
|
|
733
857
|
+ ` ${c.gray('말 속에 ' + 골라진.why + ' 가 있어서')}`
|
|
734
858
|
+ (canWrite(골라진.mode) ? '' : ` ${c.green('· 파일은 안 바꿉니다')}`));
|
|
735
859
|
// 겹친 요청은 '파일을 안 바꾼다' 로 끝나면 안 된다. 시킨 일의 절반만 한 것이다.
|
|
@@ -747,10 +871,24 @@ export async function chatLoop(opts = {}) {
|
|
|
747
871
|
const started = Date.now();
|
|
748
872
|
const before = { in: session.usage.in, out: session.usage.out };
|
|
749
873
|
|
|
874
|
+
/*
|
|
875
|
+
* 창 제목에 흐른 시간을 띄운다. 탭 이름만 봐도 도는 중인지 알게 하려는 것이다.
|
|
876
|
+
*
|
|
877
|
+
* 화면에 보이는 글자를 건드리지 않는다 — OSC 는 커서를 안 옮기고 아무것도
|
|
878
|
+
* 안 찍으므로, 한창 그리는 중에 끼어들어도 화면이 깨지지 않는다.
|
|
879
|
+
* unref 를 거는 이유는, 이 시계 하나 때문에 프로그램이 안 꺼지면 안 되기 때문이다.
|
|
880
|
+
*/
|
|
881
|
+
창제목(제목글('도는중', { 폴더: 알림.폴더, 초: 0 }));
|
|
882
|
+
const 제목시계 = setInterval(
|
|
883
|
+
() => 창제목(제목글('도는중', { 폴더: 알림.폴더, 초: (Date.now() - started) / 1000 })),
|
|
884
|
+
1000,
|
|
885
|
+
);
|
|
886
|
+
제목시계.unref?.();
|
|
887
|
+
|
|
750
888
|
// 어디까지 적었는지. 도중에 죽어도 여기까지는 남아 있게 자주 흘려 보낸다.
|
|
751
889
|
let saved = session.messages.length;
|
|
752
890
|
const flush = () => {
|
|
753
|
-
for (const m of session.messages.slice(saved)) store.append(m);
|
|
891
|
+
for (const m of session.messages.slice(saved)) ctx.갈래.현재store().append(m);
|
|
754
892
|
saved = session.messages.length;
|
|
755
893
|
};
|
|
756
894
|
let tools = 0;
|
|
@@ -759,6 +897,18 @@ export async function chatLoop(opts = {}) {
|
|
|
759
897
|
let 턴탈났나 = false;
|
|
760
898
|
let thinkChars = 0;
|
|
761
899
|
let streamed = false;
|
|
900
|
+
/*
|
|
901
|
+
* 답을 그리는 자리. 턴마다 새로 만든다 — 앞 턴의 코드 울타리 상태가
|
|
902
|
+
* 다음 턴으로 새면 멀쩡한 답이 통째로 코드 블록으로 그려진다.
|
|
903
|
+
*/
|
|
904
|
+
const 답그림 = new 마크다운({ 폭: (process.stdout.columns || 80) - 6 });
|
|
905
|
+
// 답 흐름이 끊기는 자리마다 남은 반 줄을 비운다. 안 비우면 마지막 줄이 사라진다.
|
|
906
|
+
const 답비우기 = () => {
|
|
907
|
+
for (const 조각 of 답그림.끝()) {
|
|
908
|
+
if (typeof 조각 === 'string') 화면.붙임(` ${답표시} ${조각}\n`);
|
|
909
|
+
else 화면.붙임(조각.이어붙임 + (조각.끝났나 ? '\n' : ''));
|
|
910
|
+
}
|
|
911
|
+
};
|
|
762
912
|
let thinkingShown = false;
|
|
763
913
|
let stage = null;
|
|
764
914
|
// 접는 중 표시. 끝나거나 다른 글을 찍기 전에 반드시 멈춰야 한다.
|
|
@@ -779,6 +929,9 @@ export async function chatLoop(opts = {}) {
|
|
|
779
929
|
* 그렇게 날아간다. 테두리를 그대로 두고 안엣것만 바꾸는 이유다.
|
|
780
930
|
*/
|
|
781
931
|
화면.일시작(session, '생각');
|
|
932
|
+
// 이번 턴에 쓸 카드를 지금 만든다. 앞 턴에서 겪은 것까지 반영돼야
|
|
933
|
+
// '겪을수록 나아진다' 가 이번 대화 안에서도 참이 된다.
|
|
934
|
+
ctx.카드다시?.();
|
|
782
935
|
try {
|
|
783
936
|
for await (const ev of run(session, ctx, 보낼글, { signal: turn.signal })) {
|
|
784
937
|
/*
|
|
@@ -830,15 +983,28 @@ export async function chatLoop(opts = {}) {
|
|
|
830
983
|
* 세로줄 하나면 된다. 칸을 나눠 그리는 화면으로 갈 이유가 없다 —
|
|
831
984
|
* 파이프로 넘기거나 기록으로 남길 때도 그대로 읽힌다.
|
|
832
985
|
*/
|
|
986
|
+
/*
|
|
987
|
+
* 답을 마크다운으로 그린다 (ui/md.js).
|
|
988
|
+
*
|
|
989
|
+
* 줄이 끝나야 그릴 수 있다 — `**굵` 까지 왔을 때는 그게 굵은 글씨가
|
|
990
|
+
* 될지 알 수 없고, 한 번 찍은 글자는 되돌릴 수 없다. 그래서 md 가
|
|
991
|
+
* 줄 단위로 모았다가 내놓는다. 줄이 화면 폭보다 길어지면 거기까지를
|
|
992
|
+
* 날것으로 흘려보낸다(md 가 알아서 한다) — 긴 문단에서 몇 초씩
|
|
993
|
+
* 아무것도 안 나오면 멈춘 것처럼 보이기 때문이다.
|
|
994
|
+
*/
|
|
833
995
|
case 'content':
|
|
834
996
|
clearThinking();
|
|
835
|
-
if (!streamed) { streamed = true; 화면.일바꿈('답');
|
|
836
|
-
|
|
997
|
+
if (!streamed) { streamed = true; 화면.일바꿈('답'); }
|
|
998
|
+
for (const 조각 of 답그림.넣기(ev.text)) {
|
|
999
|
+
if (typeof 조각 === 'string') { 화면.붙임(` ${답표시} ${조각}\n`); continue; }
|
|
1000
|
+
// 아직 안 끝난 긴 줄. 그 줄이 처음 나가는 것이면 세로줄을 앞에 세운다.
|
|
1001
|
+
화면.붙임((조각.첫조각 ? ` ${답표시} ` : '') + 조각.이어붙임 + (조각.끝났나 ? '\n' : ''));
|
|
1002
|
+
}
|
|
837
1003
|
break;
|
|
838
1004
|
|
|
839
1005
|
case 'tool_start':
|
|
840
1006
|
clearThinking();
|
|
841
|
-
if (streamed) { say(''); streamed = false; }
|
|
1007
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
842
1008
|
// 문구를 지금 하는 일에 맞춘다. 아무 말이나 돌려 대면 두 번째부터
|
|
843
1009
|
// 아무도 안 읽고, 그때부터는 화면이 조용한 것과 같아진다.
|
|
844
1010
|
화면.일바꿈(갈래고르기(ev.name));
|
|
@@ -849,7 +1015,7 @@ export async function chatLoop(opts = {}) {
|
|
|
849
1015
|
// 여럿을 같이 돌린다 — 한 줄로 알리고, 이름은 결과와 붙여서 그린다.
|
|
850
1016
|
case 'tools_start':
|
|
851
1017
|
clearThinking();
|
|
852
|
-
if (streamed) { say(''); streamed = false; }
|
|
1018
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
853
1019
|
화면.일바꿈(갈래고르기(ev.names?.[0]));
|
|
854
1020
|
say('');
|
|
855
1021
|
say(` ${c.gray(`${ev.count}개를 함께 돌립니다`)} ${c.gray('·')} ${c.gray(ev.names.join(' '))}`);
|
|
@@ -861,7 +1027,7 @@ export async function chatLoop(opts = {}) {
|
|
|
861
1027
|
// '시작' 을 안 거쳤다. 그래서 '생각 중…' 줄이 안 지워진 채로 결과가
|
|
862
1028
|
// 그 줄 뒤에 가서 붙고, 이름도 없이 "└ 인자가 잘렸습니다" 만 남는다.
|
|
863
1029
|
// 무슨 도구가 왜 그랬는지 알 수 없는 화면이 된다.
|
|
864
|
-
if (ev.showLabel) { clearThinking(); if (streamed) { say(''); streamed = false; } say(''); }
|
|
1030
|
+
if (ev.showLabel) { clearThinking(); if (streamed) { 답비우기(); say(''); streamed = false; } say(''); }
|
|
865
1031
|
// 같이 돈 것은 이름을 다시 적어 준다. 안 그러면 어느 결과인지 모른다.
|
|
866
1032
|
if (ev.parallel || ev.showLabel) say(` ${toolLabel(ev.name, ev.args)}`);
|
|
867
1033
|
if (ev.name === 'TodoWrite' && ev.result?.todos) {
|
|
@@ -872,6 +1038,19 @@ export async function chatLoop(opts = {}) {
|
|
|
872
1038
|
}
|
|
873
1039
|
} else {
|
|
874
1040
|
say(` ${toolResultLine(ev.result, ev.ms ?? 0)}`);
|
|
1041
|
+
/*
|
|
1042
|
+
* 비밀이 지나갔으면 반드시 말한다.
|
|
1043
|
+
*
|
|
1044
|
+
* 가렸으면 가렸다고, 못 가렸으면 못 가렸다고 적는다. 파일에서
|
|
1045
|
+
* 읽어 온 글은 일부러 안 가린다 — 가리면 모델이 그 가린 글을
|
|
1046
|
+
* 되돌려 써서 진짜 열쇠가 지워진다. 대신 사람에게 알린다.
|
|
1047
|
+
* 조용히 넘어가면 사람은 제 열쇠가 대화에 들어간 줄을 모른다.
|
|
1048
|
+
*/
|
|
1049
|
+
if (ev.비밀) {
|
|
1050
|
+
say(ev.비밀.가렸나
|
|
1051
|
+
? ` ${c.gray(`⊘ ${ev.비밀.말} — 모델에 가려서 보냈습니다`)}`
|
|
1052
|
+
: ` ${c.yellow('!')} ${c.yellow(ev.비밀.말)} ${c.gray('— 파일 내용은 가리지 않습니다 (가리면 되돌려 쓸 때 지워집니다)')}`);
|
|
1053
|
+
}
|
|
875
1054
|
/*
|
|
876
1055
|
* 파일을 바꾸는 명령이었으면, 무엇을 떠 뒀는지 적는다.
|
|
877
1056
|
*
|
|
@@ -879,6 +1058,18 @@ export async function chatLoop(opts = {}) {
|
|
|
879
1058
|
* 수 있는지 없는지 알 길이 없어서, 사람은 되돌릴 수 있는 줄 알고
|
|
880
1059
|
* 넘어가거나 반대로 못 되돌리는 줄 알고 겁을 낸다. 사실을 적는다.
|
|
881
1060
|
*/
|
|
1061
|
+
/*
|
|
1062
|
+
* 확인한 것을 센다. 상태줄이 이 숫자를 본다.
|
|
1063
|
+
*
|
|
1064
|
+
* 모델이 "다 됐습니다" 로 답을 맺는 것과 **실제로 돌려 본 것**은
|
|
1065
|
+
* 다른 일이다. 화면에 초록 ✓ 가 없으면 아직 아무도 안 돌려 본
|
|
1066
|
+
* 것이고, 그 구분이 상태줄에 있어야 사람이 속지 않는다.
|
|
1067
|
+
*/
|
|
1068
|
+
if (ev.name === 'Verify' && ev.result) {
|
|
1069
|
+
session.검증.돈횟수 += 1;
|
|
1070
|
+
session.검증.확인 += ev.result.확인됨 ?? 0;
|
|
1071
|
+
session.검증.탈 += ev.result.탈 ?? 0;
|
|
1072
|
+
}
|
|
882
1073
|
if (ev.result?.되돌릴것?.length) {
|
|
883
1074
|
const 것들 = ev.result.되돌릴것;
|
|
884
1075
|
say(` ${c.gray(`↩ ${것들.slice(0, 3).join(' · ')}${것들.length > 3 ? ` 외 ${것들.length - 3}개` : ''} 는 떠 뒀습니다 — /undo 로 되돌아갑니다`)}`);
|
|
@@ -936,17 +1127,29 @@ export async function chatLoop(opts = {}) {
|
|
|
936
1127
|
*/
|
|
937
1128
|
case 'task_start':
|
|
938
1129
|
clearThinking();
|
|
939
|
-
if (streamed) { say(''); streamed = false; }
|
|
1130
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
940
1131
|
화면.일바꿈('하위', clip(ev.목적, 24));
|
|
941
1132
|
say('');
|
|
942
1133
|
say(` ${c.hmagenta('⌥')} ${c.bold('하위 작업')} ${c.white(clip(ev.목적, 60))}`
|
|
943
|
-
+ ` ${c.gray(`· ${
|
|
1134
|
+
+ ` ${c.gray(`· ${보일이름(ev.모드)} · 최대 ${ev.steps}걸음`)}`);
|
|
944
1135
|
say(` ${c.gray('여기서부터는 따로 떨어진 대화입니다 — 결과 요약만 위로 올라옵니다.')}`);
|
|
1136
|
+
/*
|
|
1137
|
+
* 다른 모델에게 떼어 줬으면 여기서 말한다.
|
|
1138
|
+
*
|
|
1139
|
+
* 상태줄의 ⌂ 는 이 세션의 연결을 보고 있어서 하위가 딴 데로
|
|
1140
|
+
* 나가는 것을 모른다. 이 줄이 그 사실을 남기는 유일한 자리다.
|
|
1141
|
+
* 바깥으로 나가면 노랗게 — 그냥 지나칠 수 없게.
|
|
1142
|
+
*/
|
|
1143
|
+
if (ev.모델) {
|
|
1144
|
+
say(ev.밖으로
|
|
1145
|
+
? ` ${c.hyellow('↗')} ${c.hyellow(ev.모델)} ${c.gray('— 이 덩이는 바깥으로 나갑니다')}`
|
|
1146
|
+
: ` ${c.hgreen('⌂')} ${c.gray(`${ev.모델} — 이 컴퓨터 안입니다`)}`);
|
|
1147
|
+
}
|
|
945
1148
|
break;
|
|
946
1149
|
|
|
947
1150
|
case 'task_done': {
|
|
948
1151
|
clearThinking();
|
|
949
|
-
if (streamed) { say(''); streamed = false; }
|
|
1152
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
950
1153
|
const 끝 = ev.끝 ?? {};
|
|
951
1154
|
const 잘됨 = 끝.type === 'done';
|
|
952
1155
|
const 왜 = { done: '끝냈습니다', limit: '걸음 수를 다 써서 멈췄습니다 — 다 못 했습니다',
|
|
@@ -954,7 +1157,7 @@ export async function chatLoop(opts = {}) {
|
|
|
954
1157
|
aborted: '중단했습니다' }[끝.type] ?? '끝난 이유를 알 수 없습니다';
|
|
955
1158
|
say('');
|
|
956
1159
|
say(` ${잘됨 ? c.green('✓') : c.yellow('⚠')} ${c.gray('하위 작업')} ${c.white(clip(ev.목적, 50))}`
|
|
957
|
-
+ ` ${c.gray(`— ${왜}`)} ${c.gray(`(${끝.steps ?? 0}
|
|
1160
|
+
+ ` ${c.gray(`— ${왜}`)} ${c.gray(`(${끝.steps ?? 0}걸음${ev.모델 ? ` · ${ev.모델}` : ''})`)}`);
|
|
958
1161
|
// 무엇이 실제로 생겼는지는 하위가 한 말이 아니라 디스크가 말한다.
|
|
959
1162
|
만든파일보이기(끝.files);
|
|
960
1163
|
if (끝.why) say(` ${c.gray(`막힌 데: ${clip(끝.why, 80)}`)}`);
|
|
@@ -981,6 +1184,30 @@ export async function chatLoop(opts = {}) {
|
|
|
981
1184
|
접는중 = true;
|
|
982
1185
|
break;
|
|
983
1186
|
|
|
1187
|
+
/*
|
|
1188
|
+
* 요약 압축보다 먼저 오는 것. 대화는 안 건드리고 옛 도구 결과만 접었다.
|
|
1189
|
+
* 조용히 하면 사람은 어느 파일 내용이 왜 사라졌는지 모른다 — 한 줄로 알린다.
|
|
1190
|
+
*/
|
|
1191
|
+
case 'folded': {
|
|
1192
|
+
clearThinking();
|
|
1193
|
+
say(` ${c.cyan('◲')} ${c.gray(`오래된 도구 결과 ${ev.접은것}개를 접었습니다 — `)}`
|
|
1194
|
+
+ `${c.white(ev.아낀토큰.toLocaleString())} ${c.gray('토큰을 비웠습니다. 대화는 그대로입니다.')}`);
|
|
1195
|
+
/*
|
|
1196
|
+
* 무엇을 접었는지 이름으로 보여 준다.
|
|
1197
|
+
*
|
|
1198
|
+
* 조용히 버리면 "아까 그 파일 내용 어디 갔지" 를 사람이 스스로 답할 수
|
|
1199
|
+
* 없다. 개발자 수준에서만 편다 — 쉬움 수준에서는 숫자 한 줄이면 된다.
|
|
1200
|
+
*/
|
|
1201
|
+
const 접은것들 = ev.접은것들 ?? [];
|
|
1202
|
+
if (접은것들.length && session.level !== '쉬움') {
|
|
1203
|
+
const 보일것 = 접은것들.slice(0, 4)
|
|
1204
|
+
.map((x) => `${x.도구}${x.곳 ? `(${x.곳})` : ''}`).join(' · ');
|
|
1205
|
+
const 남은수 = 접은것들.length - Math.min(4, 접은것들.length);
|
|
1206
|
+
say(` ${c.gray(`${보일것}${남은수 > 0 ? ` 외 ${남은수}개` : ''}`)}`);
|
|
1207
|
+
}
|
|
1208
|
+
break;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
984
1211
|
case 'compacted': {
|
|
985
1212
|
접기멈춤();
|
|
986
1213
|
const 줄인 = ev.before - ev.after;
|
|
@@ -989,7 +1216,7 @@ export async function chatLoop(opts = {}) {
|
|
|
989
1216
|
`${c.green(`(${Math.round((줄인 / Math.max(1, ev.before)) * 100)}% 줄어듦)`)}`);
|
|
990
1217
|
if (ev.fallback) say(` ${c.yellow('요약을 못 받아 그냥 줄였습니다.')}`);
|
|
991
1218
|
// 접히면 이력이 통째로 바뀐다. 덧붙이기로는 못 맞추니 새로 적는다.
|
|
992
|
-
store.replace(session.messages, `압축 — ${ev.folded}개를 요약으로`);
|
|
1219
|
+
ctx.갈래.현재store().replace(session.messages, `압축 — ${ev.folded}개를 요약으로`);
|
|
993
1220
|
saved = session.messages.length;
|
|
994
1221
|
break;
|
|
995
1222
|
}
|
|
@@ -1033,7 +1260,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1033
1260
|
case 'stuck':
|
|
1034
1261
|
턴탈났나 = true;
|
|
1035
1262
|
clearThinking();
|
|
1036
|
-
if (streamed) { say(''); streamed = false; }
|
|
1263
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
1037
1264
|
say('');
|
|
1038
1265
|
say(` ${c.yellow('⊘')} ${c.bold('같은 자리에서 헛돌고 있어 멈췄습니다.')}`);
|
|
1039
1266
|
say(` ${c.gray(ev.why)}`);
|
|
@@ -1055,7 +1282,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1055
1282
|
case 'aborted':
|
|
1056
1283
|
턴탈났나 = true;
|
|
1057
1284
|
clearThinking();
|
|
1058
|
-
if (streamed) { say(''); streamed = false; }
|
|
1285
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
1059
1286
|
say('');
|
|
1060
1287
|
// 남았는지 아닌지를 **사실대로** 말한다. 전에는 무조건 '남아 있다' 고 했는데
|
|
1061
1288
|
// 실제로는 아무것도 안 남는 경우가 있었다. 그러면 "이어서 해줘" 라고 했을 때
|
|
@@ -1074,7 +1301,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1074
1301
|
|
|
1075
1302
|
case 'done':
|
|
1076
1303
|
clearThinking();
|
|
1077
|
-
if (streamed) say('');
|
|
1304
|
+
if (streamed) { 답비우기(); say(''); }
|
|
1078
1305
|
만든파일보이기(ev.files);
|
|
1079
1306
|
break;
|
|
1080
1307
|
}
|
|
@@ -1090,6 +1317,22 @@ export async function chatLoop(opts = {}) {
|
|
|
1090
1317
|
// 어떻게 끝났든 일하는 표시는 반드시 걷는다. 오류로 빠져나온 길에서
|
|
1091
1318
|
// 안 걷으면 돌아가는 표시가 화면에 붙박이로 남고, 시계도 계속 돈다.
|
|
1092
1319
|
화면.일끝();
|
|
1320
|
+
clearInterval(제목시계);
|
|
1321
|
+
/*
|
|
1322
|
+
* 오래 걸린 턴만 알린다. "안녕" 에 1초 만에 답할 때마다 딩 소리가 나면
|
|
1323
|
+
* 사람은 이틀 만에 알림을 꺼 버리고, 그러면 정작 3분짜리 턴도 못 듣는다.
|
|
1324
|
+
* 알림은 아껴 써야 알림이다. (문턱은 ui/notify.js 의 알릴만한초)
|
|
1325
|
+
*/
|
|
1326
|
+
/*
|
|
1327
|
+
* 되돌릴 수 있는 턴 수를 여기서 한 번 센다.
|
|
1328
|
+
*
|
|
1329
|
+
* 상태줄이 직접 세게 하면 안 된다 — 상태줄은 사람이 글자 하나 칠 때마다
|
|
1330
|
+
* 다시 그려지고, 거기서 이력 파일을 열면 타이핑이 끊긴다. 턴 경계에서
|
|
1331
|
+
* 한 번 세어 두면 화면은 그 숫자만 읽으면 된다.
|
|
1332
|
+
*/
|
|
1333
|
+
try { session.되돌릴턴 = ctx.history.turns().length; } catch { /* 못 세면 그냥 둔다 */ }
|
|
1334
|
+
if (알릴까({ 걸린밀리초: Date.now() - started, 켬: 알림.켬 })) 종();
|
|
1335
|
+
창제목(제목글(턴탈났나 ? '탈남' : '끝남', { 폴더: 알림.폴더 }));
|
|
1093
1336
|
turn = null;
|
|
1094
1337
|
interrupted = false; // 중단은 '끝내기' 의사가 아니다. 종료 카운트를 되돌린다.
|
|
1095
1338
|
flush(); // 오류로 끝났어도 여기까지는 남긴다
|
|
@@ -1119,6 +1362,9 @@ export async function chatLoop(opts = {}) {
|
|
|
1119
1362
|
say('');
|
|
1120
1363
|
계획상자(할일);
|
|
1121
1364
|
say('');
|
|
1365
|
+
// 계획을 다 내놓고 사람 답을 기다리는 자리다. 위 confirm 과 같은 이유로 알린다.
|
|
1366
|
+
if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
|
|
1367
|
+
창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
|
|
1122
1368
|
const 답 = String(await ask(
|
|
1123
1369
|
`이대로 진행할까요? ${c.gray('⏎ 진행 · n 취소 · 그 밖엔 고칠 점')}`,
|
|
1124
1370
|
{ def: 'y' },
|
|
@@ -1145,9 +1391,14 @@ export async function chatLoop(opts = {}) {
|
|
|
1145
1391
|
}
|
|
1146
1392
|
|
|
1147
1393
|
rl.close();
|
|
1394
|
+
// 남의 터미널을 우리 이름으로 두고 나가지 않는다. 켤 때 있던 제목으로 돌려놓는다.
|
|
1395
|
+
제목되돌리기();
|
|
1148
1396
|
// 띄운 남의 프로세스는 반드시 거둔다. 안 거두면 deel 을 껐는데도
|
|
1149
1397
|
// 그 서버가 계속 돌고 있게 된다 — 사람 눈에는 안 보이는 채로.
|
|
1150
1398
|
for (const s of mcp붙임.서버들) s.닫기();
|
|
1399
|
+
// 언어 서버도 같이 거둔다. 이건 사람 눈에 안 보이는 채로 메모리를 몇백 MB
|
|
1400
|
+
// 물고 있는 종류라, 남겨 두면 나중에 작업 관리자를 열기 전에는 모른다.
|
|
1401
|
+
언어서버다끄기().catch(() => {});
|
|
1151
1402
|
// 뒤에서 돌던 명령도 같이 거둔다. 이걸 조용히 하면 안 된다 —
|
|
1152
1403
|
// 사람은 dev 서버가 아직 떠 있다고 여기고 브라우저를 새로 고치다가
|
|
1153
1404
|
// "왜 안 되지" 로 시간을 쓴다. 몇 개를 껐는지 말해 준다.
|
package/src/safety/guard.js
CHANGED
|
@@ -72,12 +72,47 @@ const BLOCKED = [
|
|
|
72
72
|
{ re: /\brm\s+(-[a-z]*[rR][a-z]*f|-[a-z]*f[a-z]*[rR])\b[^|;&]*\s(\/|~|\$HOME)\s*$/i, why: '뿌리 폴더를 통째로 지우려 합니다' },
|
|
73
73
|
{ re: /\b(mkfs|fdisk|diskpart)\b/i, why: '디스크를 초기화하는 명령입니다' },
|
|
74
74
|
{ re: /\bformat\s+[a-z]:/i, why: '드라이브를 포맷하는 명령입니다' },
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// 윈도우 짝을 빠뜨리면 안 된다 — deel 은 윈도우에서 주로 돈다.
|
|
76
|
+
//
|
|
77
|
+
// 옛 규칙은 스위치가 **차례대로** 오고 끝에 `\*` 가 붙은 모양만 봤다.
|
|
78
|
+
// 그래서 `del /f /s /q C:\` 와 `rd /q /s C:\` 가 그대로 새어 나갔다.
|
|
79
|
+
// 스위치는 아무 차례로나 오므로 차례를 안 따진다.
|
|
80
|
+
{ re: /\b(rd|rmdir)\b[^|;&]*\s\/s\b/i, why: '폴더를 통째로 지웁니다 — 정션이 있으면 원본까지 딸려 갑니다' },
|
|
81
|
+
{ re: /\bdel\b[^|;&]*\s\/[sq]\b[^|;&]*(\\\*|\s[a-z]:\\?\s*$)/i, why: '하위 폴더까지 전부 지웁니다' },
|
|
82
|
+
// 파워셸도 같은 일을 한다. 드라이브 뿌리나 집 폴더를 통째로 미는 것만 본다 —
|
|
83
|
+
// 폴더 하나 지우는 평범한 Remove-Item 까지 막으면 도구를 못 쓴다.
|
|
84
|
+
{
|
|
85
|
+
re: /Remove-Item\b[^|;&]*\s-(Recurse|r)\b[^|;&]*\s(-?[a-z]*\s*)*([a-z]:\\?|~|\$HOME|\$env:USERPROFILE)\s*$/i,
|
|
86
|
+
why: '드라이브나 집 폴더를 통째로 지웁니다',
|
|
87
|
+
},
|
|
77
88
|
{ re: /git\s+push\b[^|;&]*--force(?!-with-lease)/i, why: '원격 이력을 덮어씁니다 (--force-with-lease 를 쓰세요)' },
|
|
78
89
|
// 낱말만 보면 안 된다. `node scripts/shutdown.js` 나 `npm run reboot` 이
|
|
79
90
|
// '시스템을 끕니다' 로 막히고 감사기록에까지 남았다. 명령의 **첫 낱말**일 때만 본다.
|
|
80
91
|
{ re: /(^|[|;&]\s*)(sudo\s+)?(shutdown|reboot|halt|poweroff)\b/i, why: '시스템을 끕니다' },
|
|
92
|
+
// 포크 폭탄.
|
|
93
|
+
//
|
|
94
|
+
// 껐다 켜면 기계는 돌아오니 '되돌릴 수 없는 것' 은 아니다. 그런데 되돌아오지
|
|
95
|
+
// 않는 것이 하나 있다 — **아직 안 적은 것들.** 기계가 굳으면 열어 둔 것을
|
|
96
|
+
// 저장할 수도, 하던 대화를 끝낼 수도 없다. 그래서 목록에 넣는다.
|
|
97
|
+
//
|
|
98
|
+
// 이름은 아무거나 될 수 있으니(`:` 든 `bomb` 든) 이름 자체를 보지 않고 **모양**을
|
|
99
|
+
// 본다: 함수를 만들고, 그 안에서 **저를 불러 곧바로 파이프나 뒤로 보내기에**
|
|
100
|
+
// 물리고, 밖에서 그 이름을 부른다. 뒷갈이(\2)로 같은 이름인지까지 본다.
|
|
101
|
+
//
|
|
102
|
+
// '저를 부른 **바로 뒤**가 | 나 &' 인 것이 열쇠다. 그냥 '이름이 몸 안에 있고
|
|
103
|
+
// & 도 있다' 로 보면 `up(){ docker compose up -d & }; up` 같은 평범한 함수가
|
|
104
|
+
// 걸린다 — 거기서는 up 뒤에 `-d` 가 온다.
|
|
105
|
+
//
|
|
106
|
+
// 이름 앞뒤로 낱말 경계도 본다. 그래야 `tee -a out.log` 의 log 나
|
|
107
|
+
// `deploy_app` 의 deploy 를 저를 부르는 것으로 잘못 읽지 않는다.
|
|
108
|
+
// 앞자리에 따옴표와 여는 괄호도 넣는다. 안 넣으면 `bash -c ":(){ :|:& };:"` 처럼
|
|
109
|
+
// 한 겹 싸는 것만으로 그대로 새어 나간다.
|
|
110
|
+
{
|
|
111
|
+
re: /(^|[|;&\s"'(])([\w.:]{1,16})\s*\(\s*\)\s*\{[^}]*(?<![\w.])\2(?![\w.])\s*[|&][^}]*\}\s*;?\s*(?<![\w.])\2(?![\w.])/,
|
|
112
|
+
why: '스스로를 끝없이 불려 기계를 멈춰 세웁니다 (포크 폭탄)',
|
|
113
|
+
},
|
|
114
|
+
// 같은 것을 한 줄로 적는 오래된 방법. 이 모양으로 쓸 다른 일이 없다.
|
|
115
|
+
{ re: /\bfork\s+while\s+fork\b/i, why: '스스로를 끝없이 불려 기계를 멈춰 세웁니다 (포크 폭탄)' },
|
|
81
116
|
{ re: /curl[^|]*\|\s*(ba)?sh/i, why: '받은 스크립트를 그대로 실행합니다' },
|
|
82
117
|
{ re: /\biwr\b[^|]*\|\s*iex\b/i, why: '받은 스크립트를 그대로 실행합니다' },
|
|
83
118
|
];
|