deel-local-cli 1.5.5 → 1.5.7
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 +11 -6
- package/README.md +11 -6
- package/package.json +1 -1
- package/src/agent/loop.js +75 -3
- package/src/agent/modes.js +9 -4
- package/src/agent/session.js +35 -3
- package/src/commands.js +67 -2
- package/src/i18n/en.js +17 -3
- package/src/i18n/index.js +36 -0
- package/src/i18n/ko.js +18 -4
- package/src/repl.js +307 -24
- package/src/safety/guard.js +36 -3
- package/src/safety/undo.js +8 -1
- package/src/tools/desc.en.js +22 -0
- package/src/tools/index.js +264 -7
- package/src/ui/ansi.js +19 -0
- package/src/ui/inputbox.js +48 -14
- package/src/ui/status.js +20 -0
package/src/i18n/ko.js
CHANGED
|
@@ -67,12 +67,13 @@ export const ko = {
|
|
|
67
67
|
'cmd.memory.arg': '[forget <번호>|clear|<적을 말>]',
|
|
68
68
|
'cmd.level.desc': '사용자 수준 (쉬움/개발자)',
|
|
69
69
|
'cmd.level.arg': '[수준]',
|
|
70
|
+
'cmd.keys.desc': '이 터미널이 무슨 키를 보내는지 눌러서 확인 — 줄바꿈이 안 될 때',
|
|
70
71
|
'cmd.bell.desc': '다 됐을 때·물어볼 때 종소리와 창 제목 (on/off)',
|
|
71
72
|
'cmd.bell.arg': '[on|off]',
|
|
72
73
|
'cmd.motion.desc': '일하는 동안 뭐가 도나 — 기사·동물·사무실',
|
|
73
74
|
'cmd.motion.arg': '[기사|동물|사무실|기본|끔]',
|
|
74
75
|
'cmd.lang.desc': '화면 말 — 한국어 / English',
|
|
75
|
-
'cmd.lang.arg': '[ko|en]',
|
|
76
|
+
'cmd.lang.arg': '[ko|en] [시킬말]',
|
|
76
77
|
'cmd.init.desc': 'DEEL.md 규칙 파일 만들기',
|
|
77
78
|
'cmd.exit.desc': '끝내기',
|
|
78
79
|
'cmd.quit.desc': '끝내기',
|
|
@@ -118,7 +119,8 @@ export const ko = {
|
|
|
118
119
|
'head.shiftTab': 'Shift+Tab',
|
|
119
120
|
'head.shiftTabHint': '으로 바꿉니다 · ',
|
|
120
121
|
'head.tabHint': 'Tab 은 치던 / 명령을 채웁니다',
|
|
121
|
-
'head.newlineHint': '줄바꿈:
|
|
122
|
+
'head.newlineHint': '줄바꿈: Alt+Enter(맥은 Option+Enter) 로 상자 안에서 줄이 바뀝니다 · 안 먹는 터미널이면 줄 끝에 빈칸+` 를 붙이고 Enter',
|
|
123
|
+
'head.star': '쓸 만하면 별 하나 주세요 —',
|
|
122
124
|
'head.thisPC': '이 PC',
|
|
123
125
|
'head.skills': '스킬 {n}',
|
|
124
126
|
'head.commands': '명령 {n}',
|
|
@@ -185,6 +187,7 @@ export const ko = {
|
|
|
185
187
|
'motion.pickOne': '고를 수 있는 것:',
|
|
186
188
|
'motion.envWins': '환경변수(DEEL_MOTION·DEEL_OFFICE)가 켜져 있어 그쪽이 이깁니다.',
|
|
187
189
|
'motion.tooSmall': '이 터미널은 {줄}줄 × {칸}칸입니다 — 사무실은 {최소줄}줄 × {최소칸}칸부터 뜹니다.',
|
|
190
|
+
'motion.appleTerminal': '맥 기본 터미널은 줄 간격이 넓어 방에 가로줄이 그어집니다 — 터미널 › 설정 › 프로파일 › 텍스트 › 글꼴 「변경」 › 행간을 1.0 으로. (iTerm2 는 기본이 1.0 이라 멀쩡합니다)',
|
|
188
191
|
|
|
189
192
|
// ── 화면 말 (/lang) ───────────────────────────────────────────────────
|
|
190
193
|
'lang.now': '화면 말은 지금 {이름} 입니다.',
|
|
@@ -195,8 +198,12 @@ export const ko = {
|
|
|
195
198
|
'lang.unknown': 'ko 나 en 으로 적어 주세요.',
|
|
196
199
|
'lang.progress': '{언어}: {옮김}/{전체} 마디가 옮겨져 있습니다. 나머지는 한국어로 나옵니다.',
|
|
197
200
|
'lang.partial': '아직 다 안 옮겼습니다 — 빈칸을 내는 대신 한국어를 그대로 냅니다.',
|
|
198
|
-
'lang.codeStays': '모델이 읽는 글도
|
|
199
|
-
'lang.
|
|
201
|
+
'lang.codeStays': '따로 안 정하면 모델이 읽는 글도 화면 말을 따라갑니다. 코드와 도구·인자 이름은 그대로입니다.',
|
|
202
|
+
'lang.promptNow': '모델에게 시키는 말은 {이름}.',
|
|
203
|
+
'lang.follows': '화면 말을 따라갑니다',
|
|
204
|
+
'lang.splitHowto': '영어로 시키고 한국어로 받으려면',
|
|
205
|
+
'lang.promptChanged': '시키는 말은 {이름}, 답은 {답:으로} 받습니다 — 8k 창에서 고정 몫이 23% 줄어듭니다.',
|
|
206
|
+
'lang.envHint': '켤 때부터 영어로 쓰려면 DEEL_LANG=en (시키는 말만 영어로는 DEEL_PROMPT_LANG=en)',
|
|
200
207
|
|
|
201
208
|
'cmd.lsp.desc': '언어 서버 — 무엇이 깔려 있고 Def·Refs 를 쓸 수 있는지',
|
|
202
209
|
'cmd.lsp.arg': '[on|off]',
|
|
@@ -260,6 +267,13 @@ export const ko = {
|
|
|
260
267
|
'level.developer.name': '개발자',
|
|
261
268
|
'level.developer.hint': '전부 직접 만짐',
|
|
262
269
|
|
|
270
|
+
// ── 되묻기 상자 (Ask 도구) ────────────────────────────────────────────
|
|
271
|
+
'ask.title': '물어봅니다',
|
|
272
|
+
'ask.pickHint': '숫자로 고르거나, 그냥 쳐서 답하세요',
|
|
273
|
+
'ask.freeHint': '답을 쳐 주세요',
|
|
274
|
+
'ask.pickPrompt': '1-{끝} 또는 직접 입력',
|
|
275
|
+
'ask.freePrompt': '답',
|
|
276
|
+
|
|
263
277
|
// ── 자주 나오는 것 ────────────────────────────────────────────────────
|
|
264
278
|
'common.unknownCommand': '모르는 명령입니다',
|
|
265
279
|
'common.seeHelp': '명령 목록은 /help',
|
package/src/repl.js
CHANGED
|
@@ -6,7 +6,7 @@ import { c, say as 바로쓰기, mark, clip } from './ui/ansi.js';
|
|
|
6
6
|
import { headerLines } from './ui/status.js';
|
|
7
7
|
import { 종, 창제목, 제목되돌리기, 알릴까, 제목글 } from './ui/notify.js';
|
|
8
8
|
import { 보이기 as 인트로, 기본곁말 } from './ui/intro.js';
|
|
9
|
-
import {
|
|
9
|
+
import { 언어잡기, 말 as 옮긴말 } from './i18n/index.js';
|
|
10
10
|
import { 화면고르기 } from './ui/screen.js';
|
|
11
11
|
import { STAGES } from './agent/effort.js';
|
|
12
12
|
import { handle, COMMANDS, 미리보기끄기, 적용하기 as 그림적용 } from './commands.js';
|
|
@@ -362,6 +362,53 @@ export async function chatLoop(opts = {}) {
|
|
|
362
362
|
*/
|
|
363
363
|
let 이어쓰기줄들 = null;
|
|
364
364
|
|
|
365
|
+
/*
|
|
366
|
+
* ── 상자 안에서 줄을 바꾼다 ────────────────────────────────────────────
|
|
367
|
+
*
|
|
368
|
+
* 전에는 Option+Enter 를 누르면 그때까지 친 줄을 상자 **위로** 올려 찍고
|
|
369
|
+
* 빈 상자를 다시 내줬다. 화면에는 이렇게 남는다 —
|
|
370
|
+
*
|
|
371
|
+
* ❯ 첫째 줄
|
|
372
|
+
* … 다음 줄. 그냥 Enter 로 보냅니다
|
|
373
|
+
* ╭──────────────────╮
|
|
374
|
+
* │ ❯ │
|
|
375
|
+
* ╰──────────────────╯
|
|
376
|
+
*
|
|
377
|
+
* 보낼 글이 두 군데로 쪼개져 있으니 고칠 수가 없다. 위로 올라간 줄은 이미
|
|
378
|
+
* 찍힌 글자라 백스페이스가 안 닿는다. "한 번에 보낼 거면 위로 올릴 이유가
|
|
379
|
+
* 뭐냐" 는 말이 맞다 — 사람이 원하는 것은 상자가 두 줄로 늘어나는 것이다.
|
|
380
|
+
*
|
|
381
|
+
* 못 하고 있었던 진짜 이유는 readline 의 이력이다. rl.line 에 \n 을 넣고
|
|
382
|
+
* Enter 를 치면 이력이 그 줄을 \n 에서 쪼개 거꾸로 쌓고 \r 로 도로 붙인다.
|
|
383
|
+
* 지금 이 자리에서 다시 재 봤다(node 24.19) —
|
|
384
|
+
*
|
|
385
|
+
* rl.line = 'a\nb' → 'line' 이벤트가 주는 값: "b\ra"
|
|
386
|
+
* historySize: 0 → "a\nb" (이력을 끄면 멀쩡하다)
|
|
387
|
+
*
|
|
388
|
+
* 이력을 끌 수는 없다. 위 화살표로 지난 입력을 부르는 것은 훨씬 자주 쓴다.
|
|
389
|
+
*
|
|
390
|
+
* 그래서 **줄바꿈을 \n 이 아닌 글자로 들고 있는다.** 이력에게는 그냥 평범한
|
|
391
|
+
* 글자 하나라 쪼갤 거리가 없고, 화면에 그리기 직전과 보내기 직전에만 \n 으로
|
|
392
|
+
* 바꾼다. 길이가 1:1 이라 rl.cursor 값도 그대로 쓸 수 있다.
|
|
393
|
+
*
|
|
394
|
+
* 사용자 영역(U+E000)에서 골랐다. 어느 글꼴에도 뜻이 없고 어느 문서에도
|
|
395
|
+
* 안 들어 있는 자리라, 붙여넣기로 우연히 섞여 들어올 일이 없다.
|
|
396
|
+
*/
|
|
397
|
+
const 줄표 = '\uE000';
|
|
398
|
+
const 펴기 = (s) => String(s ?? '').replaceAll(줄표, '\n');
|
|
399
|
+
|
|
400
|
+
/*
|
|
401
|
+
* 지금 도는 턴의 AbortController. 아래 SIGINT 와 키 처리(ESC)가 같이 본다.
|
|
402
|
+
*
|
|
403
|
+
* **여기 선언한 이유가 있다.** 키 처리는 이 아래에서 바로 걸리는데, 그
|
|
404
|
+
* 사이에 인트로를 기다리는 await 가 있다. 선언을 아래에 두면 인트로가
|
|
405
|
+
* 도는 동안 키를 누르는 순간 아직 만들어지지 않은 이름을 읽어 터진다
|
|
406
|
+
* (TDZ). 켜자마자 아무 키나 누르면 죽는 프로그램이 되는 셈이다.
|
|
407
|
+
*/
|
|
408
|
+
let turn = null;
|
|
409
|
+
// 붙여넣는 중인가 (bracketed paste). 키 처리와 'line' 이 같이 본다.
|
|
410
|
+
let 붙여넣는중 = false;
|
|
411
|
+
|
|
365
412
|
/** 줄을 쌓아 두고 "다음 줄" 이라고 알린다. 백틱 표시와 Alt+Enter 가 같이 쓴다. */
|
|
366
413
|
const 줄쌓기 = (줄) => {
|
|
367
414
|
const 첫줄 = 이어쓰기줄들 === null;
|
|
@@ -371,8 +418,22 @@ export async function chatLoop(opts = {}) {
|
|
|
371
418
|
say(` ${c.gray('… 다음 줄. 그냥 Enter 로 보냅니다 · Ctrl+C 로 취소')}`);
|
|
372
419
|
};
|
|
373
420
|
|
|
374
|
-
rl.on('line', (
|
|
421
|
+
rl.on('line', (원래줄) => {
|
|
422
|
+
// 상자 안에서 바꾼 줄을 여기서 진짜 줄바꿈으로 되돌린다. 아래 코드는
|
|
423
|
+
// 전부 평범한 \n 만 본다 — 줄표가 이 아래로는 한 글자도 안 새어 나간다.
|
|
424
|
+
const l = 펴기(원래줄);
|
|
375
425
|
if (echo) say(c.gray(l));
|
|
426
|
+
/*
|
|
427
|
+
* 붙여넣는 도중의 줄바꿈은 **사람이 Enter 를 친 것이 아니다.**
|
|
428
|
+
*
|
|
429
|
+
* 안 가려내면 스무 줄짜리를 붙였을 때 스무 번 물어본 것이 된다. 쌓아
|
|
430
|
+
* 두었다가 사람이 진짜로 Enter 를 칠 때 한 덩이로 나간다 — 줄 끝 백틱과
|
|
431
|
+
* 같은 자리에 쌓으므로, 아래 잇는 코드가 그대로 처리한다.
|
|
432
|
+
*
|
|
433
|
+
* 화면에는 아직 안 그린다. 붙이는 동안 줄마다 그리면 스무 줄이 주르륵
|
|
434
|
+
* 지나가고, 정작 무엇을 보낼지는 안 보인다. 끝날 때 몇 줄인지만 알린다.
|
|
435
|
+
*/
|
|
436
|
+
if (붙여넣는중) { (이어쓰기줄들 ??= []).push(l); return; }
|
|
376
437
|
let 보낼것 = l;
|
|
377
438
|
if (상자쓰나) {
|
|
378
439
|
if (묻는중 !== null) {
|
|
@@ -390,15 +451,40 @@ export async function chatLoop(opts = {}) {
|
|
|
390
451
|
보낼것 = 이었나 ? [...이어쓰기줄들, l].join('\n') : l;
|
|
391
452
|
이어쓰기줄들 = null;
|
|
392
453
|
화면.입력지움();
|
|
393
|
-
|
|
394
|
-
|
|
454
|
+
// 여러 줄이면 줄마다 찍는다. 한 번에 던지면 둘째 줄부터 왼쪽 끝에
|
|
455
|
+
// 붙어서, 사람이 보낸 글인지 프로그램이 뱉은 글인지 알 수 없다.
|
|
456
|
+
if (l.trim() || 이었나) {
|
|
457
|
+
for (const [i, 한줄] of l.split('\n').entries()) {
|
|
458
|
+
say(`${i === 0 && !이었나 ? ` ${c.hcyan('❯')} ` : ' '}${c.white(한줄)}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} else if (l.trim() || 이어쓰기줄들 !== null) {
|
|
395
462
|
/*
|
|
396
463
|
* 일하는 도중에 미리 쳐 둔 것.
|
|
397
464
|
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
465
|
+
* ── 여기서도 줄 끝 백틱이 먹어야 한다 ────────────────────────────
|
|
466
|
+
*
|
|
467
|
+
* 예전에는 이 갈래에 이어쓰기가 아예 없었다. 그래서 **일하는 도중에**
|
|
468
|
+
* 여러 줄을 치면 백틱이 그냥 글자로 나갔다 — 첫 줄이 백틱을 단 채로
|
|
469
|
+
* 혼자 보내지고, 다음 줄은 따로 또 보내진다. 사람 눈에는 줄바꿈이
|
|
470
|
+
* 안 먹는 것으로 보인다. 기다릴 때는 되고 일할 때는 안 되니, 되는
|
|
471
|
+
* 자리를 찾기도 어렵다.
|
|
472
|
+
*
|
|
473
|
+
* 미리 치는 것은 일하는 중에 하는 짓이라 오히려 여기가 더 흔하다.
|
|
474
|
+
*
|
|
475
|
+
* 그리는 것은 다르게 한다. 줄쌓기() 는 대화에 줄을 찍는데, 일하는
|
|
476
|
+
* 도중에 찍으면 이미 보낸 것처럼 보인다. 여기서는 조용히 쌓고
|
|
400
477
|
* 상자에 몇 건이 밀려 있는지만 세어 준다.
|
|
401
478
|
*/
|
|
479
|
+
if (이어쓰기표시(l)) {
|
|
480
|
+
(이어쓰기줄들 ??= []).push(l.slice(0, -1).replace(/\s+$/, ''));
|
|
481
|
+
화면.대기갱신('', queue.length + 1);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (이어쓰기줄들 !== null) {
|
|
485
|
+
보낼것 = [...이어쓰기줄들, l].join('\n');
|
|
486
|
+
이어쓰기줄들 = null;
|
|
487
|
+
}
|
|
402
488
|
화면.대기갱신('', queue.length + 1);
|
|
403
489
|
}
|
|
404
490
|
}
|
|
@@ -445,7 +531,52 @@ export async function chatLoop(opts = {}) {
|
|
|
445
531
|
*/
|
|
446
532
|
if (process.stdin.isTTY) {
|
|
447
533
|
emitKeypressEvents(process.stdin, rl);
|
|
534
|
+
/*
|
|
535
|
+
* ── 붙여넣기를 한 덩이로 받는다 ──────────────────────────────────────
|
|
536
|
+
*
|
|
537
|
+
* 여러 줄을 복사해 붙이면 터미널은 그것을 그냥 **타자로** 흘려보낸다.
|
|
538
|
+
* 줄바꿈마다 readline 이 'line' 을 하나씩 쏘므로, 스무 줄을 붙이면
|
|
539
|
+
* **스무 번 물어본 것**이 된다. 한 덩이로 보내려던 글이 스무 조각으로
|
|
540
|
+
* 쪼개져 나가고, 그만큼 요청도 스무 번 나간다.
|
|
541
|
+
*
|
|
542
|
+
* `\x1b[?2004h` 를 보내면 터미널이 붙여넣기를 표로 감싸 준다
|
|
543
|
+
* (bracketed paste). 시작에 `\x1b[200~`, 끝에 `\x1b[201~` 가 온다.
|
|
544
|
+
* 그 사이에 오는 줄은 **사람이 Enter 를 친 것이 아니므로** 안 보내고
|
|
545
|
+
* 쌓아 두었다가, 붙여넣기가 끝나고 사람이 Enter 를 칠 때 한 덩이로 낸다.
|
|
546
|
+
* 쌓는 자리는 줄 끝 백틱이 쓰는 것과 같다(이어쓰기줄들).
|
|
547
|
+
*
|
|
548
|
+
* 모르는 터미널은 이 글자를 그냥 무시한다 — 그러면 예전 그대로 돈다.
|
|
549
|
+
*/
|
|
550
|
+
process.stdout.write('\x1b[?2004h');
|
|
551
|
+
process.on('exit', () => { try { process.stdout.write('\x1b[?2004l'); } catch { /* 이미 닫혔다 */ } });
|
|
552
|
+
|
|
448
553
|
process.stdin.on('keypress', (_ch, key) => {
|
|
554
|
+
if (key?.sequence === '\x1b[200~') { 붙여넣는중 = true; return; }
|
|
555
|
+
if (key?.sequence === '\x1b[201~') {
|
|
556
|
+
붙여넣는중 = false;
|
|
557
|
+
// 쌓인 것이 몇 줄인지 보여 준다. 안 보이면 사라진 줄 알고 다시 붙인다.
|
|
558
|
+
if (이어쓰기줄들?.length && 입력기다림) {
|
|
559
|
+
화면.입력지움();
|
|
560
|
+
say(` ${c.gray(`${이어쓰기줄들.length + 1}줄을 붙여넣었습니다 — Enter 로 한 번에 보냅니다`)}`);
|
|
561
|
+
}
|
|
562
|
+
화면.입력갱신?.(session, 펴기(rl.line), rl.cursor ?? 0);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
/*
|
|
566
|
+
* ── ESC 로 하던 일을 멈춘다 ────────────────────────────────────────
|
|
567
|
+
*
|
|
568
|
+
* 여태 멈추는 길은 Ctrl+C 뿐이었다. 그런데 Ctrl+C 는 **한 번 더 누르면
|
|
569
|
+
* 프로그램을 끝낸다.** 급히 멈추려고 두 번 누르면 대화가 통째로 닫힌다.
|
|
570
|
+
* (Ctrl+Z 는 더 나쁘다 — 셸이 프로세스를 재워 버려서 대화가 끊긴다.)
|
|
571
|
+
*
|
|
572
|
+
* ESC 는 **멈추기만** 한다. 끝내지 않는다. 그래서 마음 놓고 누를 수 있다.
|
|
573
|
+
* 도는 중이 아닐 때는 아무것도 안 한다 — 입력칸에서 ESC 를 눌렀다고
|
|
574
|
+
* 치던 글이 날아가면 그게 더 놀랍다.
|
|
575
|
+
*/
|
|
576
|
+
if (key?.name === 'escape' && !key.ctrl && !key.meta && !key.shift) {
|
|
577
|
+
if (turn && !turn.signal.aborted) { turn.abort(); return; }
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
449
580
|
// Shift+Tab — 승인 방식 (자동 → 위험만 → 모두)
|
|
450
581
|
if (key && key.name === 'tab' && key.shift) {
|
|
451
582
|
const 앞 = 승인고르기(session.mode);
|
|
@@ -475,10 +606,10 @@ export async function chatLoop(opts = {}) {
|
|
|
475
606
|
* 뺏어야 하는데, 그건 훨씬 자주 쓰는 기능이다.
|
|
476
607
|
*/
|
|
477
608
|
if (key && key.name === 'tab' && !key.shift && 상자쓰나 && 입력기다림 && 묻는중 === null) {
|
|
478
|
-
const 채울 = 채울글(rl.line
|
|
609
|
+
const 채울 = 채울글(펴기(rl.line), 지금추천(펴기(rl.line)));
|
|
479
610
|
if (채울) rl.write(채울);
|
|
480
611
|
// 채울 게 없어도 그리기는 한다 — 후보 목록이 그대로 남아 있어야 한다.
|
|
481
|
-
화면.입력갱신(session, rl.line
|
|
612
|
+
화면.입력갱신(session, 펴기(rl.line), rl.cursor ?? 0, 지금추천(펴기(rl.line)));
|
|
482
613
|
return;
|
|
483
614
|
}
|
|
484
615
|
/*
|
|
@@ -507,15 +638,15 @@ export async function chatLoop(opts = {}) {
|
|
|
507
638
|
* key.meta 로 여기서 골라낼 수 있다. rl.write('\n') 은 못 쓴다 —
|
|
508
639
|
* 그것도 내부적으로 줄을 끝내는 처리를 그대로 타 버린다(직접 확인).
|
|
509
640
|
*
|
|
510
|
-
* 커서
|
|
511
|
-
*
|
|
512
|
-
*
|
|
641
|
+
* 커서 자리에 줄바꿈을 **끼워 넣는다.** 상자가 그만큼 늘어나고, 위로
|
|
642
|
+
* 올라간 줄이 없으니 백스페이스로 지우는 것도 그대로 된다. \n 이
|
|
643
|
+
* 아니라 줄표를 넣는 이유는 위 선언부에 적어 두었다.
|
|
513
644
|
*/
|
|
514
645
|
if (key.meta && 입력기다림 && 묻는중 === null) {
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
rl.line =
|
|
518
|
-
rl.cursor =
|
|
646
|
+
const 글 = rl.line ?? '';
|
|
647
|
+
const 자리 = Math.min(rl.cursor ?? 글.length, 글.length);
|
|
648
|
+
rl.line = 글.slice(0, 자리) + 줄표 + 글.slice(자리);
|
|
649
|
+
rl.cursor = 자리 + 1;
|
|
519
650
|
} else {
|
|
520
651
|
return; // 줄이 끝나는 것은 'line' 이 맡는다
|
|
521
652
|
}
|
|
@@ -524,7 +655,7 @@ export async function chatLoop(opts = {}) {
|
|
|
524
655
|
const 앞 = 묻는중;
|
|
525
656
|
setImmediate(() => {
|
|
526
657
|
if (묻는중 === null) return;
|
|
527
|
-
process.stdout.write(`\r\x1b[2K${앞}${c.white(rl.line
|
|
658
|
+
process.stdout.write(`\r\x1b[2K${앞}${c.white(펴기(rl.line))}`);
|
|
528
659
|
});
|
|
529
660
|
return;
|
|
530
661
|
}
|
|
@@ -540,7 +671,7 @@ export async function chatLoop(opts = {}) {
|
|
|
540
671
|
setImmediate(() => {
|
|
541
672
|
그릴예정 = false;
|
|
542
673
|
if (입력기다림) {
|
|
543
|
-
화면.입력갱신(session, rl.line
|
|
674
|
+
화면.입력갱신(session, 펴기(rl.line), rl.cursor ?? 0, 지금추천(펴기(rl.line)));
|
|
544
675
|
} else {
|
|
545
676
|
/*
|
|
546
677
|
* 일하는 도중에 치고 있는 글.
|
|
@@ -553,7 +684,7 @@ export async function chatLoop(opts = {}) {
|
|
|
553
684
|
* 답이 흘러나오는 동안(줄 중간)에는 그려도 답 줄을 덮으므로,
|
|
554
685
|
* 상자 쪽에서 알아서 넘긴다. 글은 그대로 살아 있다.
|
|
555
686
|
*/
|
|
556
|
-
화면.대기갱신(rl.line
|
|
687
|
+
화면.대기갱신(펴기(rl.line), queue.length);
|
|
557
688
|
}
|
|
558
689
|
});
|
|
559
690
|
});
|
|
@@ -646,6 +777,127 @@ export async function chatLoop(opts = {}) {
|
|
|
646
777
|
const pw = await askHidden(rl, label, nextLine);
|
|
647
778
|
return pw === null || pw === '' ? null : pw;
|
|
648
779
|
},
|
|
780
|
+
/*
|
|
781
|
+
* ── 모델이 사람에게 되묻는 자리 (Ask 도구) ──────────────────────────
|
|
782
|
+
*
|
|
783
|
+
* 계획 승인 상자와 같은 꼴로 그린다. 사람이 이미 아는 모양이라 새로 배울
|
|
784
|
+
* 것이 없고, 무엇보다 **숫자 하나로 끝난다.**
|
|
785
|
+
*
|
|
786
|
+
* 자유롭게 쳐도 그대로 간다. 고를 것이 늘 맞지는 않아서, 넷 다 아닐 때
|
|
787
|
+
* 다시 물어보게 하면 그게 제일 답답하다.
|
|
788
|
+
*/
|
|
789
|
+
/*
|
|
790
|
+
* ── 이 터미널이 무슨 바이트를 보내는지 본다 (/keys) ──────────────────
|
|
791
|
+
*
|
|
792
|
+
* 맥에서 줄바꿈이 안 된다는 말을 몇 판째 듣고도 못 고쳤다. 고칠 데를
|
|
793
|
+
* 못 찾은 게 아니라 **무슨 일이 나는지를 몰랐다.**
|
|
794
|
+
*
|
|
795
|
+
* Alt+Enter 는 앞에 ESC 가 붙어 올 때만 잡을 수 있는데(위 keypress 참고),
|
|
796
|
+
* macOS 터미널은 기본값이 Option 을 그렇게 안 보낸다. Shift+Enter·
|
|
797
|
+
* Ctrl+Enter 는 대부분 터미널에서 평범한 Enter 와 **바이트가 똑같다.**
|
|
798
|
+
* 즉 여기서는 셋을 구분할 길이 아예 없다 — 안 오는 것을 잡을 수는 없다.
|
|
799
|
+
*
|
|
800
|
+
* 그래서 문서에 "터미널 설정을 이렇게 바꾸세요" 라고만 적어 뒀는데, 그건
|
|
801
|
+
* 고친 것이 아니다. 사람이 제 터미널에서 **눌러 보고 바로 알 수 있어야**
|
|
802
|
+
* 한다. 여기서 보여 주는 것은 짐작이 아니라 실제로 온 바이트다.
|
|
803
|
+
*/
|
|
804
|
+
키확인: async () => {
|
|
805
|
+
if (!process.stdin.isTTY) {
|
|
806
|
+
say('');
|
|
807
|
+
say(` ${c.gray('키 확인은 터미널에서만 됩니다 (지금은 파이프로 들어와 있습니다).')}`);
|
|
808
|
+
say('');
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
say('');
|
|
812
|
+
say(` ${c.hcyan('┌')} ${c.bold('키 확인')}`);
|
|
813
|
+
say(` ${c.hcyan('│')} ${c.gray('아무 키나 눌러 보세요. 이 터미널이 실제로 보낸 바이트를 그대로 보여 줍니다.')}`);
|
|
814
|
+
say(` ${c.hcyan('│')} ${c.gray('줄바꿈에 쓰려던 것을 눌러 보세요 — Alt+Enter · Option+Enter · Shift+Enter.')}`);
|
|
815
|
+
say(` ${c.hcyan('└')} ${c.gray('끝내려면 Ctrl+C 또는 q')}`);
|
|
816
|
+
say('');
|
|
817
|
+
|
|
818
|
+
const 본것 = [];
|
|
819
|
+
await new Promise((끝) => {
|
|
820
|
+
const 듣기 = (ch, key) => {
|
|
821
|
+
const 이름 = key?.name ?? (ch ? JSON.stringify(ch) : '?');
|
|
822
|
+
if (이름 === 'q' || (key?.ctrl && key?.name === 'c')) {
|
|
823
|
+
process.stdin.off('keypress', 듣기);
|
|
824
|
+
return 끝();
|
|
825
|
+
}
|
|
826
|
+
// 실제로 온 바이트. 이것이 이 화면의 전부다.
|
|
827
|
+
const 날것 = JSON.stringify(String(key?.sequence ?? ch ?? ''))
|
|
828
|
+
.replace(/\\u001b/g, '\\e');
|
|
829
|
+
const 꾸밈 = [key?.ctrl && 'ctrl', key?.meta && 'meta', key?.shift && 'shift']
|
|
830
|
+
.filter(Boolean).join('+');
|
|
831
|
+
본것.push({ 이름, 날것, 꾸밈 });
|
|
832
|
+
say(` ${c.hcyan('·')} ${c.white(이름.padEnd(10))} ${c.gray(날것.padEnd(14))}`
|
|
833
|
+
+ `${꾸밈 ? c.hgreen(꾸밈) : c.gray('꾸밈 없음')}`);
|
|
834
|
+
};
|
|
835
|
+
process.stdin.on('keypress', 듣기);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
/*
|
|
839
|
+
* 눌러 본 것을 놓고 **이 터미널에서 되는 길**을 알려 준다.
|
|
840
|
+
*
|
|
841
|
+
* Enter 를 여러 번 눌렀는데 meta 가 한 번도 안 붙었으면, 그 터미널은
|
|
842
|
+
* Option/Alt 를 ESC 로 안 보내는 것이다. 그 자리에서는 아무리 눌러도
|
|
843
|
+
* 안 되므로, 백틱 쪽을 알려 주는 것이 맞다.
|
|
844
|
+
*/
|
|
845
|
+
const 엔터들 = 본것.filter((k) => k.이름 === 'return' || k.이름 === 'enter');
|
|
846
|
+
const 메타본적있나 = 엔터들.some((k) => /meta/.test(k.꾸밈));
|
|
847
|
+
say('');
|
|
848
|
+
if (!엔터들.length) {
|
|
849
|
+
say(` ${c.gray('Enter 를 안 눌러 봤습니다. 줄바꿈을 보려면 Alt/Option+Enter 를 눌러 보세요.')}`);
|
|
850
|
+
} else if (메타본적있나) {
|
|
851
|
+
say(` ${mark.ok} ${c.white('이 터미널은 Alt/Option+Enter 를 보냅니다')} ${c.gray('— 그대로 쓰시면 됩니다.')}`);
|
|
852
|
+
} else {
|
|
853
|
+
say(` ${mark.warn} ${c.white('이 터미널은 Alt/Option+Enter 를 안 보냅니다.')}`);
|
|
854
|
+
say(` ${c.gray(' 누른 Enter 가 평범한 Enter 와 바이트가 같아서, 프로그램 쪽에서 가려낼 길이 없습니다.')}`);
|
|
855
|
+
say('');
|
|
856
|
+
say(` ${c.white('줄바꿈은 이렇게 하세요')} ${c.gray('— 어느 터미널에서나 됩니다')}`);
|
|
857
|
+
say(` ${c.gray(' 줄 끝에')} ${c.cyan('빈칸 + `')} ${c.gray('를 붙이고 Enter. 마지막 줄은 그냥 Enter.')}`);
|
|
858
|
+
say('');
|
|
859
|
+
say(` ${c.gray(' Option 을 굳이 쓰시려면 터미널 설정을 바꿔야 합니다:')}`);
|
|
860
|
+
say(` ${c.gray(' Terminal.app 설정 → 프로파일 → 키보드 → ')}${c.cyan('Option을 Meta 키로 사용')}`);
|
|
861
|
+
say(` ${c.gray(' iTerm2 Settings → Profiles → Keys → Left Option → ')}${c.cyan('Esc+')}`);
|
|
862
|
+
say(` ${c.gray(' VS Code ')}${c.cyan('terminal.integrated.macOptionIsMeta')}${c.gray(' 를 true 로')}`);
|
|
863
|
+
}
|
|
864
|
+
say('');
|
|
865
|
+
},
|
|
866
|
+
|
|
867
|
+
ask물음: async (물음, 고를것 = []) => {
|
|
868
|
+
if (closed) return null;
|
|
869
|
+
say('');
|
|
870
|
+
say(` ${c.hcyan('┌')} ${c.bold(옮긴말('ask.title'))}`);
|
|
871
|
+
for (const 줄 of 접어쓰기(물음, Math.max(40, Math.min(88, (process.stdout.columns || 80) - 6)) - 4)) {
|
|
872
|
+
say(` ${c.hcyan('│')} ${c.white(줄)}`);
|
|
873
|
+
}
|
|
874
|
+
if (고를것.length) {
|
|
875
|
+
say(` ${c.hcyan('│')}`);
|
|
876
|
+
고를것.forEach((것, i) => {
|
|
877
|
+
say(` ${c.hcyan('│')} ${c.hcyan(String(i + 1))} ${c.white(것)}`);
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
say(` ${c.hcyan('└')} ${c.gray(옮긴말(고를것.length ? 'ask.pickHint' : 'ask.freeHint'))}`);
|
|
881
|
+
say('');
|
|
882
|
+
|
|
883
|
+
// 막혀 있는 것은 기다린 만큼 그대로 손해다. confirm 과 같은 이유로 알린다.
|
|
884
|
+
if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
|
|
885
|
+
창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
|
|
886
|
+
const 답 = String(await ask(
|
|
887
|
+
고를것.length ? 옮긴말('ask.pickPrompt', { 끝: 고를것.length }) : 옮긴말('ask.freePrompt'),
|
|
888
|
+
{ def: 고를것.length ? '1' : '' },
|
|
889
|
+
)).trim();
|
|
890
|
+
창제목(제목글('도는중', { 폴더: 알림.폴더 }));
|
|
891
|
+
|
|
892
|
+
if (!답) return null;
|
|
893
|
+
// 숫자로 골랐으면 그 줄의 글을 그대로 돌려준다 — 모델에게 "2" 만 가면
|
|
894
|
+
// 무엇을 고른 것인지 모른다.
|
|
895
|
+
const n = Number(답);
|
|
896
|
+
const 고른것 = Number.isInteger(n) && n >= 1 && n <= 고를것.length ? 고를것[n - 1] : 답;
|
|
897
|
+
say(` ${c.hgreen('▶')} ${c.gray(clip(고른것, 70))}`);
|
|
898
|
+
say('');
|
|
899
|
+
return 고른것;
|
|
900
|
+
},
|
|
649
901
|
confirm: async (name, args) => {
|
|
650
902
|
say('');
|
|
651
903
|
say(` ${c.yellow('?')} ${toolLabel(name, args)}`);
|
|
@@ -804,7 +1056,9 @@ export async function chatLoop(opts = {}) {
|
|
|
804
1056
|
// 잘 된 것은 경고 표시를 달지 않는다. ⚠ 가 붙으면 뭘 고쳐야 하나 싶어진다.
|
|
805
1057
|
for (const l of 길이알림) say(` ${mark.ok} ${c.gray(l)}`);
|
|
806
1058
|
for (const w of warn) say(` ${mark.warn} ${c.gray(w)}`);
|
|
807
|
-
|
|
1059
|
+
// ESC 를 앞에 둔다 — 멈추는 길로는 이것이 먼저다. Ctrl+C 는 두 번 누르면
|
|
1060
|
+
// 끝나 버려서, 급히 멈추려던 사람이 대화를 통째로 닫는 일이 실제로 있었다.
|
|
1061
|
+
say(` ${c.gray('/help 명령 목록')} ${c.gray('ESC 중단')} ${c.gray('Ctrl+C 중단·끝내기')}`);
|
|
808
1062
|
|
|
809
1063
|
/*
|
|
810
1064
|
* 입력 자리. 어떻게 생겼는지는 화면 쪽이 정한다 —
|
|
@@ -816,7 +1070,7 @@ export async function chatLoop(opts = {}) {
|
|
|
816
1070
|
* 치면 멀쩡히 보내진다 — 화면만 거짓말을 하는 셈이라 더 나쁘다.
|
|
817
1071
|
*/
|
|
818
1072
|
const prompt = () => {
|
|
819
|
-
const 글 = 상자쓰나 && 입력기다림 ? (rl.line
|
|
1073
|
+
const 글 = 상자쓰나 && 입력기다림 ? 펴기(rl.line) : '';
|
|
820
1074
|
화면.입력자리(session, 글, 글 ? (rl.cursor ?? 0) : 0, 글 ? 지금추천(글) : []);
|
|
821
1075
|
};
|
|
822
1076
|
|
|
@@ -825,7 +1079,6 @@ export async function chatLoop(opts = {}) {
|
|
|
825
1079
|
// 입력을 기다리는 중 → 한 번은 경고, 두 번이면 끝낸다
|
|
826
1080
|
// 느린 로컬 모델이 엉뚱한 답을 길게 뽑기 시작했을 때 끝까지 기다리지 않아도 된다.
|
|
827
1081
|
let interrupted = false;
|
|
828
|
-
let turn = null; // 지금 도는 턴의 AbortController
|
|
829
1082
|
rl.on('SIGINT', () => {
|
|
830
1083
|
/*
|
|
831
1084
|
* 쌓아 둔 줄이 있으면 먼저 버린다. 이게 없으면 이어쓰기를 **취소할 길이
|
|
@@ -1413,6 +1666,19 @@ export async function chatLoop(opts = {}) {
|
|
|
1413
1666
|
오류보이기(ev.text);
|
|
1414
1667
|
break;
|
|
1415
1668
|
|
|
1669
|
+
/*
|
|
1670
|
+
* 조사만 하고 되물어서 한 번 밀어 봤다 (agent/loop.js).
|
|
1671
|
+
*
|
|
1672
|
+
* 조용히 밀면 모델이 말을 들은 것인지 우연히 다시 생각한 것인지
|
|
1673
|
+
* 구분이 안 된다. 한 줄 적어 둠다 — 또 이러면 사람이 알아볼 수 있어야 한다.
|
|
1674
|
+
*/
|
|
1675
|
+
case 'nudge':
|
|
1676
|
+
clearThinking();
|
|
1677
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
1678
|
+
say('');
|
|
1679
|
+
say(` ${c.gray('↺ 읽기만 하고 끝내려고 해서 한 번 되밀었습니다')}`);
|
|
1680
|
+
break;
|
|
1681
|
+
|
|
1416
1682
|
case 'done':
|
|
1417
1683
|
clearThinking();
|
|
1418
1684
|
if (streamed) { 답비우기(); say(''); }
|
|
@@ -1492,9 +1758,26 @@ export async function chatLoop(opts = {}) {
|
|
|
1492
1758
|
if (['n', 'no', 'ㄴ', '취소', '아니', '아니요', '아니오'].includes(낮춘)) {
|
|
1493
1759
|
say(` ${c.gray('그만뒀습니다. 계획은 위에 남아 있으니 이어서 말씀하셔도 됩니다.')}`);
|
|
1494
1760
|
} else if (['y', 'yes', 'ㅇ', 'ㅇㅇ', 'ㄱ', 'ㄱㄱ', '네', '응', '진행', 'ok'].includes(낮춘)) {
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1761
|
+
/*
|
|
1762
|
+
* 승인한 계획을 **글자 그대로 실어 보낸다.**
|
|
1763
|
+
*
|
|
1764
|
+
* 예전에는 "위 계획을 승인받았다 · 적어 둔 단계를 하나씩 끝내라" 라고만
|
|
1765
|
+
* 보냈다. 사람 눈에는 계획이 바로 위에 있으니 말이 되는데, 모델에게
|
|
1766
|
+
* 이건 **가리키기만 하고 안 주는 말**이다. 그래서 작은 모델은 "적어 둔"
|
|
1767
|
+
* 것을 찾으러 Recall 로 지난 대화를 뒤지기 시작했다 — 세 번 뒤지고
|
|
1768
|
+
* 세 번 못 찾고, 정작 시킨 일은 시작도 안 했다.
|
|
1769
|
+
*
|
|
1770
|
+
* 계획은 이미 손에 있다(ctx.todos). 가리키지 말고 준다. 두 번 적는
|
|
1771
|
+
* 것이 아까워서 안 준 것이었는데, 아낀 토큰보다 헛도는 값이 훨씬 컸다.
|
|
1772
|
+
*/
|
|
1773
|
+
const 단계글 = 할일.map((t, i) => `${i + 1}. ${String(t.text ?? '')}`).join('\n');
|
|
1774
|
+
이어갈것 = (할일.length
|
|
1775
|
+
? `방금 낸 계획을 사람이 승인했다. 계획은 아래 그대로다.\n\n${단계글}\n\n`
|
|
1776
|
+
+ '지금부터 이 단계를 하나씩 실제로 해라.'
|
|
1777
|
+
: '바로 위에 네가 적은 계획을 사람이 승인했다.'
|
|
1778
|
+
+ ' 그 계획을 다시 적지 말고 지금부터 그대로 해라.')
|
|
1779
|
+
+ ' 계획을 다시 적지 마라. **지난 대화를 뒤지지 마라** — 필요한 것은'
|
|
1780
|
+
+ ' 이 대화 안에 다 있다. 다 끝나면 무엇을 만들었는지만 짧게 알려라.';
|
|
1498
1781
|
이어갈모드 = 'code';
|
|
1499
1782
|
say('');
|
|
1500
1783
|
say(` ${c.hgreen('▶')} ${c.gray('계획대로 진행합니다.')}`);
|
package/src/safety/guard.js
CHANGED
|
@@ -9,9 +9,36 @@ import { 내부살림 } from '../tools/fsutil.js';
|
|
|
9
9
|
export class ScopeError extends Error {}
|
|
10
10
|
export class BlockedError extends Error {}
|
|
11
11
|
|
|
12
|
+
/*
|
|
13
|
+
* ── 같은 이름이 두 가지 모양으로 온다 (맥의 한글 폴더) ──────────────────
|
|
14
|
+
*
|
|
15
|
+
* 맥은 파일 이름을 **자모 분리형(NFD)** 으로 저장한다. `챗` 은 디스크에
|
|
16
|
+
* `ᄎ + ᅢ + ᆺ` 세 글자로 들어가 있다. 반면 모델이 글로 쓰는 한글은 합쳐진
|
|
17
|
+
* 모양(NFC)이다. 눈에는 똑같이 보이는데 **자바스크립트 문자열로는 다르다.**
|
|
18
|
+
*
|
|
19
|
+
* '오후'.length → 2 (NFC, 모델이 쓴 것)
|
|
20
|
+
* '오후'.normalize('NFD') → 4 (맥이 준 것)
|
|
21
|
+
*
|
|
22
|
+
* 울타리는 이 둘을 글자로만 견줬다. 그래서 한글이 든 폴더에서 deel 을 켜면
|
|
23
|
+
* **제 작업 폴더 안의 파일이 전부 범위 밖으로 튕겼다.** 실제로 이런 화면이
|
|
24
|
+
* 나왔다 — 뿌리 자신을 가리키는데도:
|
|
25
|
+
*
|
|
26
|
+
* ❉ Outline(**\/*.py)
|
|
27
|
+
* └ 작업 범위 밖입니다: /Users/me/.Trash/Archive 오후 3.05.41
|
|
28
|
+
*
|
|
29
|
+
* 그러고는 같은 호출이 다음 판에 그냥 됐다. 어느 쪽 모양으로 오느냐에 따라
|
|
30
|
+
* 갈렸기 때문이다. 사람 눈에는 "됐다 안 됐다 하는" 것으로만 보인다.
|
|
31
|
+
*
|
|
32
|
+
* **견줄 때만** 한 모양으로 맞춘다. 돌려주는 경로는 원래 것 그대로 둔다 —
|
|
33
|
+
* 리눅스 파일시스템은 바이트를 그대로 보므로 NFC 와 NFD 가 **다른 파일**이고,
|
|
34
|
+
* 거기서 바꿔 버리면 멀쩡한 파일을 못 찾게 된다. 맥은 찾을 때 두 모양을
|
|
35
|
+
* 같은 것으로 보므로 원래 것을 그대로 넘겨도 열린다.
|
|
36
|
+
*/
|
|
37
|
+
const 같은꼴 = (s) => String(s).normalize('NFC');
|
|
38
|
+
|
|
12
39
|
/** 이 절대경로가 base 밖인가. */
|
|
13
40
|
function 밖(base, abs) {
|
|
14
|
-
const rel = relative(base, abs);
|
|
41
|
+
const rel = relative(같은꼴(base), 같은꼴(abs));
|
|
15
42
|
return rel.startsWith('..' + sep) || rel === '..' || (isAbsolute(rel) && rel !== '');
|
|
16
43
|
}
|
|
17
44
|
|
|
@@ -54,14 +81,20 @@ export function makeScope(root) {
|
|
|
54
81
|
const abs = isAbsolute(p) ? resolve(p) : resolve(base, p);
|
|
55
82
|
if (밖(base, abs)) throw new ScopeError(`작업 범위 밖입니다: ${p}\n 범위: ${base}`);
|
|
56
83
|
// 글자로는 안인데 링크를 따라가면 밖인 자리. 정션 하나로 울타리가 열린다.
|
|
84
|
+
//
|
|
85
|
+
// 여기 견주기도 한 모양으로 맞춘다. 맥에서 realpathSync 는 디스크에 있는
|
|
86
|
+
// 자모 분리형을 돌려주므로, 안 맞추면 링크가 하나도 없는 평범한 한글
|
|
87
|
+
// 폴더가 매번 '링크를 따라간 자리가 다르다' 로 잡힌다.
|
|
57
88
|
const 진짜 = 진짜자리(abs);
|
|
58
|
-
if (진짜 !== abs && 밖(진짜뿌리(), 진짜)) {
|
|
89
|
+
if (같은꼴(진짜) !== 같은꼴(abs) && 밖(진짜뿌리(), 진짜)) {
|
|
59
90
|
throw new ScopeError(`작업 범위 밖입니다 (링크가 밖을 가리킵니다): ${p}\n 실제 자리: ${진짜}\n 범위: ${base}`);
|
|
60
91
|
}
|
|
61
92
|
return abs;
|
|
62
93
|
},
|
|
63
94
|
show(abs) {
|
|
64
|
-
|
|
95
|
+
// 여기도 맞춰야 한다. 안 맞추면 제 폴더 안의 파일이
|
|
96
|
+
// `../../../Users/me/Archive 오후/main.py` 처럼 보인다.
|
|
97
|
+
const rel = relative(같은꼴(base), 같은꼴(abs));
|
|
65
98
|
return rel === '' ? '.' : rel.split(sep).join('/');
|
|
66
99
|
},
|
|
67
100
|
};
|
package/src/safety/undo.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 되돌리기. 승인 프롬프트를 안 쓰는 대신 이게 안전망이다.
|
|
2
2
|
// 파일을 고치기 전에 항상 이전 내용을 떠 놓고, /undo 로 턴 단위로 되돌린다.
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync, statSync } from 'node:fs';
|
|
5
5
|
import { looksBinary } from '../tools/encoding.js';
|
|
6
6
|
|
|
@@ -169,6 +169,13 @@ export class History {
|
|
|
169
169
|
rmSync(path, { force: true });
|
|
170
170
|
restored.push({ path, how: '삭제됨(원래 없던 파일)' });
|
|
171
171
|
} else {
|
|
172
|
+
/*
|
|
173
|
+
* 담고 있던 폴더가 없어졌을 수 있다 — Move 로 폴더째 옮긴 경우다.
|
|
174
|
+
* 그러면 writeFileSync 가 ENOENT 로 죽고, 되돌리기는 "실패" 만 남긴 채
|
|
175
|
+
* 파일을 못 되살린다. 되돌릴 내용은 손에 있는데 담을 자리가 없어서
|
|
176
|
+
* 못 넣는 것이라, 자리를 다시 만들어 준다.
|
|
177
|
+
*/
|
|
178
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
172
179
|
// enc 가 붙어 있으면 UTF-8 로 담을 수 없던 파일이다 — 바이트를 그대로 되돌린다.
|
|
173
180
|
writeFileSync(path, rec.enc === 'b64' ? Buffer.from(rec.before, 'base64') : Buffer.from(rec.before, 'utf8'));
|
|
174
181
|
restored.push({ path, how: '되돌림' });
|
package/src/tools/desc.en.js
CHANGED
|
@@ -122,6 +122,28 @@ export const 도구설명EN = {
|
|
|
122
122
|
+ ' material comes back cut (max 120000)',
|
|
123
123
|
},
|
|
124
124
|
},
|
|
125
|
+
Move: {
|
|
126
|
+
desc: 'Move or rename files and folders. This is how you restructure a tree.'
|
|
127
|
+
+ ' **Several at once via the moves array** — do not call it twenty times to move twenty files.'
|
|
128
|
+
+ ' Do not use Bash mv: it is not covered by undo and asks for approval every time.',
|
|
129
|
+
params: {
|
|
130
|
+
from: 'the file or folder to move (single form)',
|
|
131
|
+
to: 'where it goes. Missing folders are created (single form)',
|
|
132
|
+
moves: 'several at once. With this, from/to are unused',
|
|
133
|
+
overwrite: 'overwrite the destination if it already exists. Default false, so a clash is refused',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
Ask: {
|
|
137
|
+
desc: 'Ask the person one question at a genuine fork. Never write "let me know" and stop;'
|
|
138
|
+
+ ' asking in prose ends the turn and throws away everything you have looked at.'
|
|
139
|
+
+ ' Give 2-4 options and they answer with a single number.'
|
|
140
|
+
+ ' **Never ask back something the user already said.** "Tidy up the files" is the answer —'
|
|
141
|
+
+ ' go and do it. Deciding how is your job, not theirs.',
|
|
142
|
+
params: {
|
|
143
|
+
question: 'one sentence. Make it clear what has to be decided',
|
|
144
|
+
options: '2-4 choices, one line each, so the difference is visible',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
125
147
|
Recall: {
|
|
126
148
|
desc: 'Search past sessions in this folder. When the user points back ("last time"), use this'
|
|
127
149
|
+ ' instead of asking again. This does not search file contents — that is Grep.',
|