deel-local-cli 1.2.0 → 1.4.1
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 +301 -1537
- package/README.md +272 -1582
- package/bin/deel.js +73 -4
- package/package.json +8 -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/card.js +129 -0
- package/src/agent/compact.js +10 -2
- package/src/agent/effort.js +5 -0
- package/src/agent/evidence.js +186 -0
- package/src/agent/grade.js +20 -0
- package/src/agent/loop.js +134 -13
- package/src/agent/models.js +169 -0
- package/src/agent/modes.js +47 -1
- package/src/agent/pins.js +140 -0
- package/src/agent/preset.js +113 -0
- package/src/agent/project.js +10 -4
- package/src/agent/session.js +233 -13
- package/src/agent/store.js +31 -0
- package/src/backend/adapter.js +14 -0
- package/src/commands.js +616 -52
- package/src/i18n/en.js +265 -0
- package/src/i18n/index.js +126 -0
- package/src/i18n/ko.js +252 -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 +19 -0
- package/src/pack/sbom.js +218 -0
- package/src/pack/selfpack.js +19 -3
- package/src/preview/serve.js +19 -2
- package/src/repl.js +193 -8
- package/src/safety/secrets.js +205 -0
- package/src/safety/undo.js +10 -3
- package/src/tools/desc.en.js +221 -0
- package/src/tools/docs.js +252 -0
- package/src/tools/index.js +190 -6
- package/src/tools/lsp.js +327 -0
- package/src/tools/task.js +30 -2
- package/src/ui/ansi.js +45 -0
- package/src/ui/approve.js +25 -21
- package/src/ui/banner.js +245 -0
- package/src/ui/export.js +217 -0
- package/src/ui/inputbox.js +37 -7
- package/src/ui/intro.js +206 -0
- package/src/ui/level.js +11 -5
- package/src/ui/notify.js +101 -0
- package/src/ui/status.js +181 -35
package/src/repl.js
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
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
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
22
|
import { activeProfile, load, resolveKey, save as saveCfg, homeDir } from './config.js';
|
|
18
23
|
import { discover } from './skills/discover.js';
|
|
19
|
-
import { allowEndpoint, setOffline, isOffline } from './safety/network.js';
|
|
24
|
+
import { allowEndpoint, setOffline, isOffline, isLocalHost } from './safety/network.js';
|
|
20
25
|
import { Store, latest, prune } from './agent/store.js';
|
|
21
26
|
import { Threads } from './agent/threads.js';
|
|
27
|
+
import { 못박기 } from './agent/pins.js';
|
|
28
|
+
import { 카드 } from './agent/card.js';
|
|
22
29
|
import { 배움 } from './agent/evolve.js';
|
|
23
30
|
import { 마크다운 } from './ui/md.js';
|
|
24
31
|
import { askHidden } from './ui/prompt.js';
|
|
@@ -160,6 +167,27 @@ export async function chatLoop(opts = {}) {
|
|
|
160
167
|
allowEndpoint(conn.base);
|
|
161
168
|
if (opts.offline ?? prof.offline) setOffline(true);
|
|
162
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
|
+
|
|
163
191
|
const session = new Session(conn, {
|
|
164
192
|
root,
|
|
165
193
|
mode: opts.mode ?? 'auto',
|
|
@@ -196,6 +224,17 @@ export async function chatLoop(opts = {}) {
|
|
|
196
224
|
say(` ${c.gray(`중단된 도구 호출 ${고친것}개를 걷어냈습니다 — 그때 하던 일은 다시 시켜 주세요.`)}`);
|
|
197
225
|
}
|
|
198
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')}`);
|
|
237
|
+
}
|
|
199
238
|
}
|
|
200
239
|
}
|
|
201
240
|
if (!store) store = new Store(root);
|
|
@@ -222,6 +261,14 @@ export async function chatLoop(opts = {}) {
|
|
|
222
261
|
// 세션도 알아야 한다 — 밖에서 붙인 도구도 스키마가 매 요청에 실린다.
|
|
223
262
|
// 안 세면 컨텍스트가 그만큼 조용히 줄어든다.
|
|
224
263
|
session.mcp = mcp붙임.서버들;
|
|
264
|
+
/*
|
|
265
|
+
* 이 자리에 언어 서버가 깔려 있나 (Def·Refs 를 목록에 넣을지).
|
|
266
|
+
*
|
|
267
|
+
* 여기서 한 번만 잰다. PATH 훑기와 폴더 훑기라 값이 있고, 세션 도중에
|
|
268
|
+
* 답이 바뀌지 않는다. 아무것도 안 깔아 준다 — 있으면 쓰고 없으면 없는 대로
|
|
269
|
+
* Grep·Outline 으로 간다 (lsp/servers.js 머리말).
|
|
270
|
+
*/
|
|
271
|
+
session.lsp = 언어서버있나(root);
|
|
225
272
|
// 지난 대화에서 정해 둔 것을 들고 시작한다.
|
|
226
273
|
// 기억은 Session 이 켤 때 직접 읽는다 (session.js 생성자) — deel run 도 같은 것을 들고 시작하게.
|
|
227
274
|
|
|
@@ -357,7 +404,7 @@ export async function chatLoop(opts = {}) {
|
|
|
357
404
|
session.work = nextWork(session.work);
|
|
358
405
|
const w = getWork(session.work);
|
|
359
406
|
화면.입력지움();
|
|
360
|
-
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))}`
|
|
361
408
|
+ (canWrite(session.work) ? '' : ` ${c.green('· 파일을 못 바꿉니다')}`));
|
|
362
409
|
prompt();
|
|
363
410
|
return;
|
|
@@ -501,6 +548,8 @@ export async function chatLoop(opts = {}) {
|
|
|
501
548
|
mcp: mcp붙임.서버들,
|
|
502
549
|
skills: found.skills,
|
|
503
550
|
loadedSkills: new Set(),
|
|
551
|
+
// 고친 뒤 진단을 볼지. /lsp off 로 끈다 (lsp/diag.js).
|
|
552
|
+
lsp: { 켬: true },
|
|
504
553
|
ask,
|
|
505
554
|
// 암호는 여기서만 받는다. 받은 값은 도구가 쓰고 버린다 —
|
|
506
555
|
// 설정에도, 세션 기록에도, 감사기록에도, 명령줄에도 안 남는다.
|
|
@@ -512,7 +561,15 @@ export async function chatLoop(opts = {}) {
|
|
|
512
561
|
confirm: async (name, args) => {
|
|
513
562
|
say('');
|
|
514
563
|
say(` ${c.yellow('?')} ${toolLabel(name, args)}`);
|
|
564
|
+
/*
|
|
565
|
+
* 여기서 종을 울린다. 끝난 것은 늦게 알아도 되지만 **막혀 있는 것은**
|
|
566
|
+
* 기다린 만큼 그대로 손해다 — 다른 창에 가 있는 사이 3분째 이 줄에서
|
|
567
|
+
* 멈춰 서 있는 일이 실제로 잦다. 시간 문턱도 안 본다, 0초여도 알린다.
|
|
568
|
+
*/
|
|
569
|
+
if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
|
|
570
|
+
창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
|
|
515
571
|
const a = (await ask('실행할까요? (y/n)', { def: 'y' })).toLowerCase();
|
|
572
|
+
창제목(제목글('도는중', { 폴더: 알림.폴더 }));
|
|
516
573
|
return a === 'y' || a === 'yes' || a === 'ㅇ';
|
|
517
574
|
},
|
|
518
575
|
};
|
|
@@ -531,6 +588,22 @@ export async function chatLoop(opts = {}) {
|
|
|
531
588
|
const 아는배수 = ctx.배움.아는보정(conn.model);
|
|
532
589
|
if (아는배수) { session.보정 = 아는배수; session.보정잰것 = 1; }
|
|
533
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
|
+
|
|
534
607
|
/*
|
|
535
608
|
* 대화 갈래. 연결·도구·되돌리기는 같이 쓰고 오간 말만 여러 벌 갖는다.
|
|
536
609
|
* 갈래마다 저장 파일을 따로 열어서, 나중에 `/sessions` 로 각각 찾아갈 수 있다.
|
|
@@ -585,6 +658,26 @@ export async function chatLoop(opts = {}) {
|
|
|
585
658
|
}
|
|
586
659
|
}
|
|
587
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
|
+
|
|
588
681
|
// ── 머리말 ────────────────────────────────────────────────────────────
|
|
589
682
|
화면.머리말(headerLines(session, found));
|
|
590
683
|
const warn = [];
|
|
@@ -760,7 +853,7 @@ export async function chatLoop(opts = {}) {
|
|
|
760
853
|
계획승인받나 = 골라진.겹침 === true;
|
|
761
854
|
const w = getWork(골라진.mode);
|
|
762
855
|
say('');
|
|
763
|
-
say(` ${c.hcyan(w.glyph)} ${c.bold(w.
|
|
856
|
+
say(` ${c.hcyan(w.glyph)} ${c.bold(보일이름(w.id))} ${c.gray('(' + w.en + ')')}`
|
|
764
857
|
+ ` ${c.gray('말 속에 ' + 골라진.why + ' 가 있어서')}`
|
|
765
858
|
+ (canWrite(골라진.mode) ? '' : ` ${c.green('· 파일은 안 바꿉니다')}`));
|
|
766
859
|
// 겹친 요청은 '파일을 안 바꾼다' 로 끝나면 안 된다. 시킨 일의 절반만 한 것이다.
|
|
@@ -778,6 +871,20 @@ export async function chatLoop(opts = {}) {
|
|
|
778
871
|
const started = Date.now();
|
|
779
872
|
const before = { in: session.usage.in, out: session.usage.out };
|
|
780
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
|
+
|
|
781
888
|
// 어디까지 적었는지. 도중에 죽어도 여기까지는 남아 있게 자주 흘려 보낸다.
|
|
782
889
|
let saved = session.messages.length;
|
|
783
890
|
const flush = () => {
|
|
@@ -822,6 +929,9 @@ export async function chatLoop(opts = {}) {
|
|
|
822
929
|
* 그렇게 날아간다. 테두리를 그대로 두고 안엣것만 바꾸는 이유다.
|
|
823
930
|
*/
|
|
824
931
|
화면.일시작(session, '생각');
|
|
932
|
+
// 이번 턴에 쓸 카드를 지금 만든다. 앞 턴에서 겪은 것까지 반영돼야
|
|
933
|
+
// '겪을수록 나아진다' 가 이번 대화 안에서도 참이 된다.
|
|
934
|
+
ctx.카드다시?.();
|
|
825
935
|
try {
|
|
826
936
|
for await (const ev of run(session, ctx, 보낼글, { signal: turn.signal })) {
|
|
827
937
|
/*
|
|
@@ -928,6 +1038,19 @@ export async function chatLoop(opts = {}) {
|
|
|
928
1038
|
}
|
|
929
1039
|
} else {
|
|
930
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
|
+
}
|
|
931
1054
|
/*
|
|
932
1055
|
* 파일을 바꾸는 명령이었으면, 무엇을 떠 뒀는지 적는다.
|
|
933
1056
|
*
|
|
@@ -935,6 +1058,18 @@ export async function chatLoop(opts = {}) {
|
|
|
935
1058
|
* 수 있는지 없는지 알 길이 없어서, 사람은 되돌릴 수 있는 줄 알고
|
|
936
1059
|
* 넘어가거나 반대로 못 되돌리는 줄 알고 겁을 낸다. 사실을 적는다.
|
|
937
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
|
+
}
|
|
938
1073
|
if (ev.result?.되돌릴것?.length) {
|
|
939
1074
|
const 것들 = ev.result.되돌릴것;
|
|
940
1075
|
say(` ${c.gray(`↩ ${것들.slice(0, 3).join(' · ')}${것들.length > 3 ? ` 외 ${것들.length - 3}개` : ''} 는 떠 뒀습니다 — /undo 로 되돌아갑니다`)}`);
|
|
@@ -996,8 +1131,20 @@ export async function chatLoop(opts = {}) {
|
|
|
996
1131
|
화면.일바꿈('하위', clip(ev.목적, 24));
|
|
997
1132
|
say('');
|
|
998
1133
|
say(` ${c.hmagenta('⌥')} ${c.bold('하위 작업')} ${c.white(clip(ev.목적, 60))}`
|
|
999
|
-
+ ` ${c.gray(`· ${
|
|
1134
|
+
+ ` ${c.gray(`· ${보일이름(ev.모드)} · 최대 ${ev.steps}걸음`)}`);
|
|
1000
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
|
+
}
|
|
1001
1148
|
break;
|
|
1002
1149
|
|
|
1003
1150
|
case 'task_done': {
|
|
@@ -1010,7 +1157,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1010
1157
|
aborted: '중단했습니다' }[끝.type] ?? '끝난 이유를 알 수 없습니다';
|
|
1011
1158
|
say('');
|
|
1012
1159
|
say(` ${잘됨 ? c.green('✓') : c.yellow('⚠')} ${c.gray('하위 작업')} ${c.white(clip(ev.목적, 50))}`
|
|
1013
|
-
+ ` ${c.gray(`— ${왜}`)} ${c.gray(`(${끝.steps ?? 0}
|
|
1160
|
+
+ ` ${c.gray(`— ${왜}`)} ${c.gray(`(${끝.steps ?? 0}걸음${ev.모델 ? ` · ${ev.모델}` : ''})`)}`);
|
|
1014
1161
|
// 무엇이 실제로 생겼는지는 하위가 한 말이 아니라 디스크가 말한다.
|
|
1015
1162
|
만든파일보이기(끝.files);
|
|
1016
1163
|
if (끝.why) say(` ${c.gray(`막힌 데: ${clip(끝.why, 80)}`)}`);
|
|
@@ -1041,11 +1188,25 @@ export async function chatLoop(opts = {}) {
|
|
|
1041
1188
|
* 요약 압축보다 먼저 오는 것. 대화는 안 건드리고 옛 도구 결과만 접었다.
|
|
1042
1189
|
* 조용히 하면 사람은 어느 파일 내용이 왜 사라졌는지 모른다 — 한 줄로 알린다.
|
|
1043
1190
|
*/
|
|
1044
|
-
case 'folded':
|
|
1191
|
+
case 'folded': {
|
|
1045
1192
|
clearThinking();
|
|
1046
1193
|
say(` ${c.cyan('◲')} ${c.gray(`오래된 도구 결과 ${ev.접은것}개를 접었습니다 — `)}`
|
|
1047
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
|
+
}
|
|
1048
1208
|
break;
|
|
1209
|
+
}
|
|
1049
1210
|
|
|
1050
1211
|
case 'compacted': {
|
|
1051
1212
|
접기멈춤();
|
|
@@ -1156,6 +1317,22 @@ export async function chatLoop(opts = {}) {
|
|
|
1156
1317
|
// 어떻게 끝났든 일하는 표시는 반드시 걷는다. 오류로 빠져나온 길에서
|
|
1157
1318
|
// 안 걷으면 돌아가는 표시가 화면에 붙박이로 남고, 시계도 계속 돈다.
|
|
1158
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
|
+
창제목(제목글(턴탈났나 ? '탈남' : '끝남', { 폴더: 알림.폴더 }));
|
|
1159
1336
|
turn = null;
|
|
1160
1337
|
interrupted = false; // 중단은 '끝내기' 의사가 아니다. 종료 카운트를 되돌린다.
|
|
1161
1338
|
flush(); // 오류로 끝났어도 여기까지는 남긴다
|
|
@@ -1185,6 +1362,9 @@ export async function chatLoop(opts = {}) {
|
|
|
1185
1362
|
say('');
|
|
1186
1363
|
계획상자(할일);
|
|
1187
1364
|
say('');
|
|
1365
|
+
// 계획을 다 내놓고 사람 답을 기다리는 자리다. 위 confirm 과 같은 이유로 알린다.
|
|
1366
|
+
if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
|
|
1367
|
+
창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
|
|
1188
1368
|
const 답 = String(await ask(
|
|
1189
1369
|
`이대로 진행할까요? ${c.gray('⏎ 진행 · n 취소 · 그 밖엔 고칠 점')}`,
|
|
1190
1370
|
{ def: 'y' },
|
|
@@ -1211,9 +1391,14 @@ export async function chatLoop(opts = {}) {
|
|
|
1211
1391
|
}
|
|
1212
1392
|
|
|
1213
1393
|
rl.close();
|
|
1394
|
+
// 남의 터미널을 우리 이름으로 두고 나가지 않는다. 켤 때 있던 제목으로 돌려놓는다.
|
|
1395
|
+
제목되돌리기();
|
|
1214
1396
|
// 띄운 남의 프로세스는 반드시 거둔다. 안 거두면 deel 을 껐는데도
|
|
1215
1397
|
// 그 서버가 계속 돌고 있게 된다 — 사람 눈에는 안 보이는 채로.
|
|
1216
1398
|
for (const s of mcp붙임.서버들) s.닫기();
|
|
1399
|
+
// 언어 서버도 같이 거둔다. 이건 사람 눈에 안 보이는 채로 메모리를 몇백 MB
|
|
1400
|
+
// 물고 있는 종류라, 남겨 두면 나중에 작업 관리자를 열기 전에는 모른다.
|
|
1401
|
+
언어서버다끄기().catch(() => {});
|
|
1217
1402
|
// 뒤에서 돌던 명령도 같이 거둔다. 이걸 조용히 하면 안 된다 —
|
|
1218
1403
|
// 사람은 dev 서버가 아직 떠 있다고 여기고 브라우저를 새로 고치다가
|
|
1219
1404
|
// "왜 안 되지" 로 시간을 쓴다. 몇 개를 껐는지 말해 준다.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// 비밀이 모델에게, 그리고 디스크에 남는 대화 기록에 끼어드는 것을 막는다.
|
|
2
|
+
//
|
|
3
|
+
// ── 어디서 새나 ────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// 사람이 열쇠를 붙여 넣는 일은 드물다. 새는 자리는 거의 항상 **명령 출력**이다.
|
|
6
|
+
//
|
|
7
|
+
// env · printenv · set 환경변수를 통째로 찍는다
|
|
8
|
+
// git remote -v https://사람:토큰@github.com/... 이 그대로
|
|
9
|
+
// docker inspect · kubectl get -o 설정에 박힌 열쇠가 그대로
|
|
10
|
+
// curl -v Authorization 헤더가 그대로
|
|
11
|
+
// 검사 실패 로그 연결 문자열이 통째로
|
|
12
|
+
//
|
|
13
|
+
// 이게 왜 문제인가. 그 글은 모델에게 실려 가고 — 사내 게이트웨이라 해도
|
|
14
|
+
// 그쪽 로그에 남는다 — 동시에 `.deel/sessions/*.jsonl` 로 디스크에 적힌다.
|
|
15
|
+
// 그 파일은 나중에 `/recall` 로 다시 읽히고, `deel pack` 에 딸려 갈 수도 있다.
|
|
16
|
+
// 한 번 새면 여러 벌이 된다.
|
|
17
|
+
//
|
|
18
|
+
// ── 파일 내용은 안 가린다 ───────────────────────────────────────────────
|
|
19
|
+
//
|
|
20
|
+
// 여기서 제일 중요한 결정이다. **읽은 파일은 손대지 않는다.**
|
|
21
|
+
//
|
|
22
|
+
// 가리고 싶은 마음이 드는 자리가 바로 `.env` 인데, 거기를 가리면 이렇게 된다 —
|
|
23
|
+
// 모델이 가려진 글을 보고, 그걸 고쳐서 Write 로 되돌려 쓴다. 그러면 진짜
|
|
24
|
+
// 열쇠가 있던 자리에 `«가림»` 이 적힌다. **사람의 열쇠가 우리 손에 지워진다.**
|
|
25
|
+
// 비밀을 지키려다 비밀을 파괴하는 셈이다.
|
|
26
|
+
//
|
|
27
|
+
// 그래서 파일 쪽은 가리는 대신 **알린다** — 감사기록에 남기고 화면에 띄운다.
|
|
28
|
+
// 무엇을 못 막는지 분명히 말하는 편이, 막았다고 해 놓고 파일을 망가뜨리는 것보다
|
|
29
|
+
// 언제나 낫다.
|
|
30
|
+
//
|
|
31
|
+
// 가리는 것은 **한 번 쓰고 버려지는 출력**뿐이다. 명령 출력·웹에서 받아온 글은
|
|
32
|
+
// 모델이 그대로 되돌려 쓸 일이 없다.
|
|
33
|
+
|
|
34
|
+
/** 가려진 자리에 남는 표. 종류를 같이 적어 무엇이 가려졌는지 알 수 있게 한다. */
|
|
35
|
+
const 표 = (종류) => `«가림:${종류}»`;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 찾는 것들.
|
|
39
|
+
*
|
|
40
|
+
* 순서가 뜻을 갖는다 — 좁은 것을 먼저 본다. `sk-ant-...` 를 `sk-...` 가 먼저
|
|
41
|
+
* 집어삼키면 무엇이 가려졌는지가 뭉개진다.
|
|
42
|
+
*
|
|
43
|
+
* 넓게 잡지 않는다. 코드에 흔한 글자를 비밀로 오인해 가려 버리면, 모델이 보는
|
|
44
|
+
* 코드가 조용히 달라진다. 그건 여기서 막으려는 것보다 더 나쁜 고장이다.
|
|
45
|
+
*/
|
|
46
|
+
export const 갈래 = [
|
|
47
|
+
{
|
|
48
|
+
id: '사설키',
|
|
49
|
+
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
50
|
+
},
|
|
51
|
+
{ id: 'anthropic', re: /\bsk-ant-[A-Za-z0-9_-]{16,}/g },
|
|
52
|
+
{ id: 'openai', re: /\bsk-[A-Za-z0-9_-]{20,}/g },
|
|
53
|
+
{ id: 'github', re: /\b(?:gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,})\b/g },
|
|
54
|
+
{ id: 'slack', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
55
|
+
{ id: 'aws', re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
|
|
56
|
+
{ id: 'google', re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
57
|
+
{
|
|
58
|
+
// 머리.몸통.서명 세 토막. 안에 무엇이 들었는지 우리는 안 열어 본다.
|
|
59
|
+
id: 'jwt',
|
|
60
|
+
re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
// https://사람:토큰@호스트 — git remote -v 가 이 모양으로 찍는다.
|
|
64
|
+
// 사람 이름은 남긴다. 무슨 계정인지 모르면 사람이 손을 못 쓴다.
|
|
65
|
+
id: '주소속열쇠',
|
|
66
|
+
re: /([a-z][a-z0-9+.-]*:\/\/)([^\s/:@]{1,64}):([^\s/@]{1,256})@/gi,
|
|
67
|
+
바꾸기: (m, 앞, 사람) => `${앞}${사람}:${표('주소속열쇠')}@`,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
/*
|
|
71
|
+
* 줄 끝까지 가린다.
|
|
72
|
+
*
|
|
73
|
+
* `Authorization: Bearer eyJ…` 에서 `\S+` 만 잡으면 'Bearer' 만 가려지고
|
|
74
|
+
* 진짜 값은 그 뒤에 그대로 남는다. 앞의 낱말을 가리고 뒤의 열쇠를 남기는
|
|
75
|
+
* 것은 안 가린 것만 못하다 — 가렸다고 믿게 만들기 때문이다.
|
|
76
|
+
*
|
|
77
|
+
* 이미 가린 자리는 다시 안 건드린다. 두 번 가리면 무엇이 가려졌는지가
|
|
78
|
+
* 뭉개지고 셈도 부풀려진다.
|
|
79
|
+
*/
|
|
80
|
+
id: '헤더',
|
|
81
|
+
re: /\b(Authorization|Proxy-Authorization|X-Api-Key|Api-Key|X-Auth-Token)(\s*[:=]\s*)(?!«)([^\r\n]+)/gi,
|
|
82
|
+
바꾸기: (m, 이름, 사이) => `${이름}${사이}${표('헤더')}`,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
/*
|
|
86
|
+
* 이름이 열쇠라고 말하는 환경변수.
|
|
87
|
+
*
|
|
88
|
+
* 값이 비었거나 이미 가려진 것은 그냥 둔다 — `API_KEY=` 만 있는 줄까지
|
|
89
|
+
* 손대면 화면에 «가림» 만 늘어나고 알아볼 것이 없어진다.
|
|
90
|
+
*/
|
|
91
|
+
id: '환경변수',
|
|
92
|
+
re: /\b([A-Z][A-Z0-9_]{2,}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIALS?))(\s*[:=]\s*)(["']?)(?!«)([^\s"']{4,})\3/g,
|
|
93
|
+
바꾸기: (m, 이름, 사이) => `${이름}${사이}${표('환경변수')}`,
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 안 고치고 세기만 한다.
|
|
99
|
+
*
|
|
100
|
+
* 파일을 읽었을 때 쓰는 길이다. 무엇이 들어왔는지는 알려야 하고, 글자는
|
|
101
|
+
* 건드리면 안 된다.
|
|
102
|
+
*
|
|
103
|
+
* @returns {Array<{종류: string, 몇번: number}>}
|
|
104
|
+
*/
|
|
105
|
+
export function 훑기(글, { 열쇠들 = [] } = {}) {
|
|
106
|
+
/*
|
|
107
|
+
* 가려 보고 그 결과만 센다. 글은 안 돌려준다.
|
|
108
|
+
*
|
|
109
|
+
* 세는 길을 따로 두면 두 벌이 되고, 두 벌은 언젠가 어긋난다 — "가릴 때는
|
|
110
|
+
* 3군데였는데 셀 때는 5군데" 같은 식으로. 겹치는 규칙 때문에 실제로 그렇게
|
|
111
|
+
* 된다. 한 길로만 간다.
|
|
112
|
+
*/
|
|
113
|
+
return 가리기(글, { 열쇠들 }).가린것;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 가린다.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} 글
|
|
120
|
+
* @param {object} o
|
|
121
|
+
* @param {string[]} o.열쇠들 정확히 아는 비밀 (설정에 든 게이트웨이 열쇠 등).
|
|
122
|
+
* 이건 짐작이 아니라 아는 값이라 언제나 먼저 지운다.
|
|
123
|
+
* @returns {{글: string, 가린것: Array<{종류, 몇번}>}}
|
|
124
|
+
*/
|
|
125
|
+
export function 가리기(글, { 열쇠들 = [] } = {}) {
|
|
126
|
+
const 원본 = String(글 ?? '');
|
|
127
|
+
if (!원본) return { 글: 원본, 가린것: [] };
|
|
128
|
+
|
|
129
|
+
let s = 원본;
|
|
130
|
+
|
|
131
|
+
// 아는 값부터. 짐작보다 아는 것이 먼저다.
|
|
132
|
+
for (const 열쇠 of 아는열쇠(열쇠들)) {
|
|
133
|
+
if (s.includes(열쇠)) s = s.split(열쇠).join(표('설정한열쇠'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const g of 갈래) {
|
|
137
|
+
g.re.lastIndex = 0;
|
|
138
|
+
s = s.replace(g.re, (...인자) => (g.바꾸기 ? g.바꾸기(...인자) : 표(g.id)));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { 글: s, 가린것: 남은표세기(s) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
* 몇 군데를 가렸는지는 **결과를 보고** 센다. 바꾼 횟수를 세면 안 된다.
|
|
146
|
+
*
|
|
147
|
+
* 규칙끼리 겹치기 때문이다 — `Authorization: Bearer eyJ…` 는 jwt 가 먼저
|
|
148
|
+
* 가리고 헤더가 그 위를 다시 가린다. 바꾼 횟수로 세면 "2군데를 가렸습니다"
|
|
149
|
+
* 가 되는데 글에는 표가 하나뿐이다. 사람이 세어 보면 안 맞는다.
|
|
150
|
+
*/
|
|
151
|
+
function 남은표세기(글) {
|
|
152
|
+
const 센것 = new Map();
|
|
153
|
+
for (const m of String(글).matchAll(/«가림:([^»]+)»/g)) {
|
|
154
|
+
센것.set(m[1], (센것.get(m[1]) ?? 0) + 1);
|
|
155
|
+
}
|
|
156
|
+
return [...센것].map(([종류, 몇번]) => ({ 종류, 몇번 }));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 모델에게 붙일 한마디.
|
|
161
|
+
*
|
|
162
|
+
* 표만 남기고 말이 없으면 모델은 «가림:openai» 를 진짜 값으로 알고 그걸
|
|
163
|
+
* 명령에 다시 써 넣는다. 그러면 명령이 실패하고, 왜 실패했는지 모른 채
|
|
164
|
+
* 같은 것을 되풀이한다. 한 줄이면 그 고리가 안 생긴다.
|
|
165
|
+
*/
|
|
166
|
+
export function 가렸다는말(가린것) {
|
|
167
|
+
if (!가린것?.length) return '';
|
|
168
|
+
const 셈 = 가린것.reduce((a, x) => a + x.몇번, 0);
|
|
169
|
+
const 종류 = 가린것.map((x) => x.종류).join(' · ');
|
|
170
|
+
return `\n\n(deel 이 이 출력에서 비밀로 보이는 값 ${셈}군데를 가렸습니다: ${종류}.`
|
|
171
|
+
+ ' 「«가림:…»」 은 진짜 값이 아닙니다 — 명령에 그대로 써 넣지 마세요.'
|
|
172
|
+
+ ' 그 값이 꼭 필요하면 사용자에게 물어보세요.)';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 사람에게 보일 한 줄. 파일 쪽에서는 이것만 하고 글은 안 건드린다. */
|
|
176
|
+
export function 봤다는말(가린것) {
|
|
177
|
+
if (!가린것?.length) return '';
|
|
178
|
+
const 셈 = 가린것.reduce((a, x) => a + x.몇번, 0);
|
|
179
|
+
return `비밀로 보이는 값 ${셈}군데가 대화에 들어갔습니다 (${가린것.map((x) => x.종류).join(' · ')})`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 지금 설정에서 '정확히 아는 비밀' 을 모은다.
|
|
184
|
+
*
|
|
185
|
+
* 짧은 것은 뺀다. 세 글자짜리 열쇠를 그대로 찾아 지우면 멀쩡한 글에서
|
|
186
|
+
* 그 세 글자가 든 자리가 전부 뭉개진다 — 열쇠가 'test' 인 시험 설정에서
|
|
187
|
+
* 실제로 그럴 수 있다.
|
|
188
|
+
*/
|
|
189
|
+
export function 아는열쇠(것들) {
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const v of Array.isArray(것들) ? 것들 : [것들]) {
|
|
192
|
+
const s = typeof v === 'string' ? v.trim() : '';
|
|
193
|
+
if (s.length >= 8 && !out.includes(s)) out.push(s);
|
|
194
|
+
}
|
|
195
|
+
// 긴 것부터 지운다. 짧은 것이 긴 것의 일부일 때 순서가 뒤바뀌면 반쪽만 지워진다.
|
|
196
|
+
return out.sort((a, b) => b.length - a.length);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 이 도구의 결과를 가려도 되는가.
|
|
201
|
+
*
|
|
202
|
+
* 한 번 쓰고 버려지는 출력만 가린다. 파일에서 읽어 온 글은 모델이 그대로
|
|
203
|
+
* 되돌려 쓸 수 있으므로 절대 안 가린다 — 위 머리말 참고.
|
|
204
|
+
*/
|
|
205
|
+
export const 가릴도구 = new Set(['Bash', 'Verify', 'Jobs', 'WebFetch']);
|
package/src/safety/undo.js
CHANGED
|
@@ -131,11 +131,18 @@ export class History {
|
|
|
131
131
|
return seen;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
/**
|
|
135
|
+
* 최근 n 개 턴을 되돌린다. 되돌린 파일 목록을 반환.
|
|
136
|
+
*
|
|
137
|
+
* turnIds 를 같이 준다 — **어느** 턴을 되돌렸는지 부르는 쪽이 알아야 한다.
|
|
138
|
+
* 파일만 되돌리고 대화는 그대로 두면, 모델은 지워진 코드가 아직 있는 줄 알고
|
|
139
|
+
* 그 위에 이어서 일한다. 그 거짓말을 걷어내려면 개수가 아니라 번호가 필요하다.
|
|
140
|
+
* (session.되감기 를 볼 것.)
|
|
141
|
+
*/
|
|
135
142
|
undo(n = 1) {
|
|
136
143
|
const recs = this.all();
|
|
137
144
|
const turns = this.turns().slice(-n);
|
|
138
|
-
if (!turns.length) return { restored: [], turns: 0 };
|
|
145
|
+
if (!turns.length) return { restored: [], turns: 0, turnIds: [] };
|
|
139
146
|
|
|
140
147
|
const target = recs.filter((r) => turns.includes(r.turn));
|
|
141
148
|
// 같은 파일이 여러 번 바뀌었으면 가장 이른 상태로 되돌려야 한다.
|
|
@@ -173,7 +180,7 @@ export class History {
|
|
|
173
180
|
// 되돌린 기록은 잘라낸다.
|
|
174
181
|
const keep = recs.filter((r) => !turns.includes(r.turn));
|
|
175
182
|
writeFileSync(this.file, keep.map((r) => JSON.stringify(r)).join('\n') + (keep.length ? '\n' : ''), 'utf8');
|
|
176
|
-
return { restored, turns: turns.length };
|
|
183
|
+
return { restored, turns: turns.length, turnIds: turns.slice() };
|
|
177
184
|
}
|
|
178
185
|
}
|
|
179
186
|
|