deel-local-cli 1.5.6 → 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 +4 -3
- package/README.md +4 -3
- package/package.json +1 -1
- package/src/agent/loop.js +75 -3
- package/src/agent/session.js +10 -0
- package/src/commands.js +14 -0
- package/src/i18n/en.js +2 -1
- package/src/i18n/ko.js +2 -1
- package/src/repl.js +143 -17
- package/src/safety/guard.js +36 -3
- package/src/tools/desc.en.js +4 -3
- package/src/tools/index.js +3 -1
- package/src/ui/ansi.js +19 -0
- package/src/ui/inputbox.js +21 -4
package/README.en.md
CHANGED
|
@@ -18,7 +18,7 @@ Zero dependencies · Node 20+ · Exactly one place your source can go
|
|
|
18
18
|
|
|
19
19
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
20
20
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
21
|
-
[](docs/en/develop.md)
|
|
22
22
|
|
|
23
23
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
24
24
|
[](package.json)
|
|
@@ -100,7 +100,7 @@ This page is the **summary**. Each section links to the detail behind it.
|
|
|
100
100
|
| [Speed and spend](docs/en/tuning.md) | Per-stage effort · the prefix cache · context length |
|
|
101
101
|
| [Safety and corporate review](docs/en/safety.md) | Undo · working scope · audit log · the review package |
|
|
102
102
|
| [Configuration](docs/en/config.md) · [Development](docs/en/develop.md) | Env vars · run flags · running the tests · folder layout |
|
|
103
|
-
| [Release notes](docs/en/releases.md) | [1.5.6](docs/en/releases.md#156) · [1.5.5](docs/en/releases.md#155) · [1.5.4](docs/en/releases.md#154) · [1.5.3](docs/en/releases.md#153) · [1.5.2](docs/en/releases.md#152) · [1.5.1](docs/en/releases.md#151) · [1.5.0](docs/en/releases.md#150) · [1.4.3](docs/en/releases.md#143) · [1.4.2](docs/en/releases.md#142) · [1.4.1](docs/en/releases.md#141) · [1.4.0](docs/en/releases.md#140) · [1.3.0](docs/en/releases.md#130) · [1.2.0](docs/en/releases.md#120) |
|
|
103
|
+
| [Release notes](docs/en/releases.md) | [1.5.7](docs/en/releases.md#157) · [1.5.6](docs/en/releases.md#156) · [1.5.5](docs/en/releases.md#155) · [1.5.4](docs/en/releases.md#154) · [1.5.3](docs/en/releases.md#153) · [1.5.2](docs/en/releases.md#152) · [1.5.1](docs/en/releases.md#151) · [1.5.0](docs/en/releases.md#150) · [1.4.3](docs/en/releases.md#143) · [1.4.2](docs/en/releases.md#142) · [1.4.1](docs/en/releases.md#141) · [1.4.0](docs/en/releases.md#140) · [1.3.0](docs/en/releases.md#130) · [1.2.0](docs/en/releases.md#120) |
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
@@ -932,7 +932,7 @@ Stored in `~/.deel/config.json`. A `.deel/config.json` in the project folder tak
|
|
|
932
932
|
## Development
|
|
933
933
|
|
|
934
934
|
```bash
|
|
935
|
-
npm test Full suite (3,
|
|
935
|
+
npm test Full suite (3,848 checks)
|
|
936
936
|
npm run coverage Which lines the tests actually execute
|
|
937
937
|
npm run verify Import + network checks only
|
|
938
938
|
npm run bench Edit success rate
|
|
@@ -984,6 +984,7 @@ so one run tells you everything.
|
|
|
984
984
|
|
|
985
985
|
| Version | What changed |
|
|
986
986
|
|---|---|
|
|
987
|
+
| **[1.5.7](docs/en/releases.md#157)** | Korean folders on macOS made your own files "out of scope" · paste · ESC |
|
|
987
988
|
| **[1.5.6](docs/en/releases.md#156)** | It asks when stuck — instead of writing a question and ending the turn |
|
|
988
989
|
| **[1.5.5](docs/en/releases.md#155)** | A plan fills the room — "only one person" was a bug, not a design |
|
|
989
990
|
| [1.5.4](docs/en/releases.md#154) | A day passes in the room — morning to night, a wall clock, a cold coffee |
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
20
20
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
21
|
-
[](docs/ko/develop.md)
|
|
22
22
|
|
|
23
23
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
24
24
|
[](package.json)
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
| [속도와 씀씀이](docs/ko/tuning.md) | 단계별 추론 강도 · 프리픽스 캐시 · 컨텍스트 길이 |
|
|
102
102
|
| [안전망과 사내 반입](docs/ko/safety.md) | 되돌리기 · 작업 범위 · 감사기록 · 심사 서류 |
|
|
103
103
|
| [설정](docs/ko/config.md) · [개발](docs/ko/develop.md) | 환경변수 · 실행 옵션 · 검사 돌리기 · 폴더 구조 |
|
|
104
|
-
| [릴리스 노트](docs/ko/releases.md) | [1.5.6](docs/ko/releases.md#156) · [1.5.5](docs/ko/releases.md#155) · [1.5.4](docs/ko/releases.md#154) · [1.5.3](docs/ko/releases.md#153) · [1.5.2](docs/ko/releases.md#152) · [1.5.1](docs/ko/releases.md#151) · [1.5.0](docs/ko/releases.md#150) · [1.4.3](docs/ko/releases.md#143) · [1.4.2](docs/ko/releases.md#142) · [1.4.1](docs/ko/releases.md#141) · [1.4.0](docs/ko/releases.md#140) · [1.3.0](docs/ko/releases.md#130) · [1.2.0](docs/ko/releases.md#120) |
|
|
104
|
+
| [릴리스 노트](docs/ko/releases.md) | [1.5.7](docs/ko/releases.md#157) · [1.5.6](docs/ko/releases.md#156) · [1.5.5](docs/ko/releases.md#155) · [1.5.4](docs/ko/releases.md#154) · [1.5.3](docs/ko/releases.md#153) · [1.5.2](docs/ko/releases.md#152) · [1.5.1](docs/ko/releases.md#151) · [1.5.0](docs/ko/releases.md#150) · [1.4.3](docs/ko/releases.md#143) · [1.4.2](docs/ko/releases.md#142) · [1.4.1](docs/ko/releases.md#141) · [1.4.0](docs/ko/releases.md#140) · [1.3.0](docs/ko/releases.md#130) · [1.2.0](docs/ko/releases.md#120) |
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
@@ -910,7 +910,7 @@ deel sbom --only sbom # SBOM 한 장만
|
|
|
910
910
|
## 개발
|
|
911
911
|
|
|
912
912
|
```bash
|
|
913
|
-
npm test 전체 검증 (3,
|
|
913
|
+
npm test 전체 검증 (3,848항목)
|
|
914
914
|
npm run coverage 검사가 소스의 어디를 밟았는지
|
|
915
915
|
npm run verify 반입·통신 검증만
|
|
916
916
|
npm run bench 편집 성공률 측정
|
|
@@ -975,6 +975,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
975
975
|
|
|
976
976
|
| 판 | 무엇이 바뀌었나 |
|
|
977
977
|
|---|---|
|
|
978
|
+
| **[1.5.7](docs/ko/releases.md#157)** | 맥 한글 폴더에서 제 파일이 「범위 밖」이던 것 · 붙여넣기 · ESC |
|
|
978
979
|
| **[1.5.6](docs/ko/releases.md#156)** | 막히면 물어본다 — 글로 묻고 턴을 끝내는 대신에 |
|
|
979
980
|
| **[1.5.5](docs/ko/releases.md#155)** | 계획을 세우면 방이 찬다 — 「한 명뿐」은 고정이 아니라 버그였다 |
|
|
980
981
|
| [1.5.4](docs/ko/releases.md#154) | 방에 하루가 흐른다 — 아침·낮·노을·밤 · 벽시계 · 식은 커피 |
|
package/package.json
CHANGED
package/src/agent/loop.js
CHANGED
|
@@ -14,6 +14,7 @@ import { 프로필찾기, 쓸수있나, 연결만들기, 알릴말, 목록보기
|
|
|
14
14
|
import { allowTemporarily, isOffline } from '../safety/network.js';
|
|
15
15
|
import { 가리기, 훑기, 가렸다는말, 봤다는말, 가릴도구 } from '../safety/secrets.js';
|
|
16
16
|
import { get as workMode } from './modes.js';
|
|
17
|
+
import { 지시말 } from '../i18n/index.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* 아무 내용도 없는 답인가.
|
|
@@ -165,8 +166,15 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
165
166
|
* 결과가 달라졌으면 그대로 싣는다. 고치고 나서 다시 읽는 것은 정상이다.
|
|
166
167
|
*/
|
|
167
168
|
const 부른것 = new Map();
|
|
169
|
+
/*
|
|
170
|
+
* 서명은 한 모양으로 맞춰 놓고 견준다 (NFC).
|
|
171
|
+
*
|
|
172
|
+
* 맥은 파일 이름을 자모를 쪼개서 돌려준다. 그래서 `Read(PDF_경로.md)` 와
|
|
173
|
+
* `Read(PDF_경로.md)` 가 글자로는 다른 호출이 된다 — 같은 파일을 두 번 읽고
|
|
174
|
+
* 두 번 싣고도 되풀이로 안 잡힌다. 실제로 3,250줄짜리 파일이 그렇게 두 번 들어갔다.
|
|
175
|
+
*/
|
|
168
176
|
const 서명만들기 = (call) => {
|
|
169
|
-
try { return `${call.name}(${JSON.stringify(call.args ?? {})})
|
|
177
|
+
try { return `${call.name}(${JSON.stringify(call.args ?? {})})`.normalize('NFC'); }
|
|
170
178
|
catch { return `${call.name}(?)`; }
|
|
171
179
|
};
|
|
172
180
|
|
|
@@ -247,6 +255,60 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
247
255
|
let steps = 0;
|
|
248
256
|
let lastToolFailed = false; // 직전 단계에서 도구가 오류를 냈나 → 다음 판단은 세게
|
|
249
257
|
|
|
258
|
+
/*
|
|
259
|
+
* ── 조사만 하고 "무엇을 도와드릴까요" 로 끝내는 것 ────────────
|
|
260
|
+
*
|
|
261
|
+
* 실제로 이렇게 끝난 턴이다. 할 일 셋을 적어 놓고, 파일 스물일곱 개를
|
|
262
|
+
* 읽고, 그중 하나도 안 끝낸 채로 —
|
|
263
|
+
*
|
|
264
|
+
* ☰ TodoWrite(3건) ▶ 문서와 실행 구조를 확인한다 ☐ README 를 정리한다 …
|
|
265
|
+
* ◧ Read × 스물일곱
|
|
266
|
+
* ▌ 무엇을 도와드릴까요? 프로젝트 구조와 실행 방식은 확인했습니다.
|
|
267
|
+
* ── 30.9초 · 도구 27회
|
|
268
|
+
*
|
|
269
|
+
* 사람은 "문서 정리해줘" 라고 말했고 30초를 기다렸는데, 바뀐 파일은 없다.
|
|
270
|
+
* 다시 "정리해줘" 라고 하면 또 처음부터 읽는다 — 조사한 것은 매번 버려진다.
|
|
271
|
+
* 걸음 수 상한으로도, 반복 감지로도 이 자리는 안 잡힌다. 모델은 오류 없이,
|
|
272
|
+
* 정중하게, 제 일을 사람에게 되돌려주고 끝낸 것이다.
|
|
273
|
+
*
|
|
274
|
+
* 그래서 한 번만 되민다. 세 가지가 다 맞을 때만이다 —
|
|
275
|
+
* · 이번 턴에 바꾼 파일이 하나도 없다
|
|
276
|
+
* · 파일을 바꿀 수 있는 모드다 (설계·묻기 모드는 안 바꾸는 것이 정상이다)
|
|
277
|
+
* · 되묻는 말로 끝났거나, 남은 할 일이 있는데 답이 한 줄이다
|
|
278
|
+
*
|
|
279
|
+
* 한 번뿐인 이유: 두 번 밀면 못 하는 일을 두고 실랑이가 된다. 한 번 밀었는데도
|
|
280
|
+
* 또 물으면 그건 진짜 막힌 것이니, 그 말을 사람에게 그대로 보여 준다.
|
|
281
|
+
*
|
|
282
|
+
* 물어볼 것이 진짜로 있을 때는 Ask 도구가 있다. 그것은 도구 호출이라
|
|
283
|
+
* 여기까지 오지 않는다 — 막히는 것은 글로 되묻고 턴을 닫는 쪽뿐이다.
|
|
284
|
+
*/
|
|
285
|
+
let 민적 = false;
|
|
286
|
+
const 되묻는말 = /무엇을\s*도와|무엇을\s*해\s*드릴|원하시는\s*(작업|것)|어떤\s*(작업|것)\s*(을|부터)|말씀해\s*주세요|알려\s*주세요|지시해\s*주세요|어떻게\s*할까요|해\s*드릴까요|진행할까요|what would you like|how can i (help|assist)|let me know (what|which|how)|shall i\b|would you like me to/i;
|
|
287
|
+
|
|
288
|
+
/** 이번 답을 되밀까. 밀 이유를 돌려주고, 아니면 null. */
|
|
289
|
+
const 밀어줄까 = (글) => {
|
|
290
|
+
if (민적 || 깊이) return null;
|
|
291
|
+
if (손댄파일.size) return null; // 뭐라도 바꿨으면 일은 한 것이다
|
|
292
|
+
if (!모드.tools.includes('Write')) return null; // 안 바꾸는 모드는 그게 맞다
|
|
293
|
+
const 말 = String(글 ?? '').trim();
|
|
294
|
+
if (되묻는말.test(말)) return '되물음';
|
|
295
|
+
const 남은 = (ctx.todos ?? []).filter((x) => x.state !== 'done');
|
|
296
|
+
// 진짜 보고는 길다. 한 줄로 끝났고 할 일이 남았으면 놓은 것이다.
|
|
297
|
+
if (남은.length && 말.length < 120) return '할일남음';
|
|
298
|
+
return null;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const 되미는말 = () => {
|
|
302
|
+
const 시킨 = String(userText ?? '').replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
303
|
+
return 지시말() === 'en'
|
|
304
|
+
? `Nothing has been changed yet. What was asked: "${시킨}"\n`
|
|
305
|
+
+ 'The reading is done. Do it now — do not ask back. Deciding how is your job.\n'
|
|
306
|
+
+ 'If a tool is genuinely blocking you, say in one line what blocked you.'
|
|
307
|
+
: `아직 아무것도 안 바꿨습니다. 시킨 것은 이것입니다: "${시킨}"\n`
|
|
308
|
+
+ '읽는 것은 끝났습니다. 되묻지 말고 지금 하세요 — 어떻게 할지 정하는 것이 당신 일입니다.\n'
|
|
309
|
+
+ '정말 도구가 막고 있으면 무엇이 막았는지 한 줄로 말하세요.';
|
|
310
|
+
};
|
|
311
|
+
|
|
250
312
|
while (steps < maxSteps) {
|
|
251
313
|
steps++;
|
|
252
314
|
/*
|
|
@@ -444,6 +506,13 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
444
506
|
session.push(assistantMessage(conn.kind, msg));
|
|
445
507
|
|
|
446
508
|
if (!msg.toolCalls?.length) {
|
|
509
|
+
const 왜 = 밀어줄까(msg.content);
|
|
510
|
+
if (왜) {
|
|
511
|
+
민적 = true;
|
|
512
|
+
session.push({ role: 'user', content: 되미는말() });
|
|
513
|
+
yield { type: 'nudge', why: 왜, text: msg.content };
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
447
516
|
yield { type: 'done', steps, text: msg.content, files: 마무리() };
|
|
448
517
|
return;
|
|
449
518
|
}
|
|
@@ -826,8 +895,11 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
826
895
|
let 실을것 = result.error ? `오류: ${result.error}` : result.content ?? '';
|
|
827
896
|
if (!result.error) {
|
|
828
897
|
const 서명 = 서명만들기(call);
|
|
898
|
+
// 결과도 한 모양으로 맞춰 견준다. 여러 도구가 받은 경로를 답에 그대로
|
|
899
|
+
// 되돌려 쓰므로, 자모가 쪼개진 경로로 부르면 결과까지 달라 보인다.
|
|
900
|
+
const 같은꼴 = 실을것.normalize('NFC');
|
|
829
901
|
const 앞것 = 부른것.get(서명);
|
|
830
|
-
if (앞것 !== undefined && 앞것 ===
|
|
902
|
+
if (앞것 !== undefined && 앞것 === 같은꼴) {
|
|
831
903
|
session.본것?.본것('되풀이');
|
|
832
904
|
const n = (막힘.get(`반복|${서명}`) ?? 0) + 1;
|
|
833
905
|
막힘.set(`반복|${서명}`, n);
|
|
@@ -835,7 +907,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0 }
|
|
|
835
907
|
+ ' 같은 것을 또 부르지 말고 다음 일로 넘어가세요.';
|
|
836
908
|
if (n + 1 >= MAX_SAME) 멈출까 = `${call.name} 을 같은 인자로 계속 부르고 있습니다 — 결과가 매번 같습니다`;
|
|
837
909
|
} else {
|
|
838
|
-
부른것.set(서명,
|
|
910
|
+
부른것.set(서명, 같은꼴);
|
|
839
911
|
}
|
|
840
912
|
}
|
|
841
913
|
|
package/src/agent/session.js
CHANGED
|
@@ -32,6 +32,10 @@ const BASE_RULES = `너는 deel 다. 사용자의 작업 폴더 안에서 코드
|
|
|
32
32
|
- 그래도 물어야 하면 **Ask 도구로 묻는다.** 글로 "알려주세요" 하고 끝내지 마라 — 그러면 그 턴이
|
|
33
33
|
끝나서 여태 읽은 것이 다 버려지고, 사람은 아무것도 안 된 화면을 본다. Ask 는 답이 그 자리로
|
|
34
34
|
돌아와 하던 일이 이어진다. 고를 것을 2~4개 같이 준다.
|
|
35
|
+
- **이미 시킨 것을 다시 묻지 마라.** "파일 정리해 줘" 는 이미 답이다. 어떻게 정리할지 정하는
|
|
36
|
+
것이 네 일이지, 그걸 되물으면 사람은 같은 말을 두 번 하게 된다.
|
|
37
|
+
- 도구가 자꾸 실패하면 **그것을 말해라.** 못 읽은 파일이 몇 개인지 적고 무엇이 막혔는지 알려라.
|
|
38
|
+
실패를 삼킨 채 "무엇을 도와드릴까요" 로 끝내면, 사람은 왜 안 됐는지 영영 모른다.
|
|
35
39
|
- 여러 파일을 만들고 나눠 담아야 하는 일이면 그렇게 한다. 하나만 건드려 놓고 멈추지 마라.
|
|
36
40
|
- 다 했으면 확인한다. 돌려 보고 안 되면 고친다. 확인 못 했으면 "확인 못 했다" 고 말한다.
|
|
37
41
|
|
|
@@ -70,6 +74,7 @@ const BASE_RULES_짧게 = `너는 deel 다. 사용자의 작업 폴더에서 코
|
|
|
70
74
|
- 바로 시작한다. 없는 파일·폴더는 만든다.
|
|
71
75
|
- 도구로 알아낼 수 있으면 되묻지 말고 정한다. 무엇으로 정했는지는 말한다.
|
|
72
76
|
- 그래도 물어야 하면 Ask 도구로 묻는다. 글로 묻고 끝내면 턴이 끝나 여태 읽은 것이 버려진다.
|
|
77
|
+
- 이미 시킨 것을 다시 묻지 마라. 도구가 자꾸 실패하면 몇 개가 막혔는지 말해라.
|
|
73
78
|
- 파일이 여럿이면 다 만든다. 하나만 하고 멈추지 마라.
|
|
74
79
|
- 끝내기 전에 Verify 로 확인한다. 확인 못 했으면 "확인 못 했다" 고 말한다.
|
|
75
80
|
|
|
@@ -108,6 +113,10 @@ const BASE_RULES_EN = `You are deel, a tool that reads and edits code inside the
|
|
|
108
113
|
- When you truly must ask, **ask with the Ask tool.** Do not write "let me know what you want" and stop — that ends
|
|
109
114
|
the turn, everything you read is thrown away, and the user sees a screen where nothing happened. An Ask answer
|
|
110
115
|
comes straight back to where you are, so the work carries on. Give 2–4 options to pick from.
|
|
116
|
+
- **Never ask back what you were already told.** "Tidy up the files" is already the answer. Deciding how to tidy
|
|
117
|
+
them is your job; asking it back makes the person say the same thing twice.
|
|
118
|
+
- If tools keep failing, **say so.** Report how many files you could not read and what blocked you. Swallowing
|
|
119
|
+
the failures and ending with "what can I help you with?" leaves the person with no idea why nothing happened.
|
|
111
120
|
- If the job needs several files, make them all. Do not touch one and stop.
|
|
112
121
|
- When you are done, check. Run it, and fix it if it fails. If you could not check, say "I could not verify this."
|
|
113
122
|
|
|
@@ -139,6 +148,7 @@ Finish the job. Do not stop at a plan.
|
|
|
139
148
|
- Start now. Create missing files and folders.
|
|
140
149
|
- If a tool can tell you, decide instead of asking. Say what you decided it from.
|
|
141
150
|
- If you must ask, use the Ask tool. Asking in prose ends the turn and throws away what you read.
|
|
151
|
+
- Never ask back what you were told. If tools keep failing, say how many failed.
|
|
142
152
|
- If there are several files, make them all. Do not do one and stop.
|
|
143
153
|
- Verify before you finish. If you could not verify, say so.
|
|
144
154
|
|
package/src/commands.js
CHANGED
|
@@ -572,6 +572,20 @@ export async function handle(line, session, ctx) {
|
|
|
572
572
|
if (줄 && 칸 && (줄 < 최소높이 || 칸 < 최소폭)) {
|
|
573
573
|
say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.tooSmall', { 줄, 칸, 최소줄: 최소높이, 최소칸: 최소폭 }))}`);
|
|
574
574
|
}
|
|
575
|
+
/*
|
|
576
|
+
* 맥 기본 터미널에서 방이 갈라져 보이는 것은 우리가 못 고친다.
|
|
577
|
+
*
|
|
578
|
+
* 방은 한 칸에 픽셀 둘을 넣는다 — 위는 글자색, 아래는 배경색, 글자는
|
|
579
|
+
* 반칸(▀). 그런데 기본 터미널은 줄 간격이 1.0 보다 넓게 잡혀 있어서
|
|
580
|
+
* 칸과 칸 사이에 빈 띠가 남는다. 색이 꽉 차야 할 자리에 가로줄이
|
|
581
|
+
* 그어지는 것이 그 모습이다. 우리가 무엇을 보내든 그 틈은 안 메워진다.
|
|
582
|
+
*
|
|
583
|
+
* 그래서 고치는 자리를 알려 준다. iTerm2 에서 멀쩡한 것도 같은 이유다 —
|
|
584
|
+
* 거기는 줄 간격이 기본 1.0 이다.
|
|
585
|
+
*/
|
|
586
|
+
if (process.env.TERM_PROGRAM === 'Apple_Terminal') {
|
|
587
|
+
say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.appleTerminal'))}`);
|
|
588
|
+
}
|
|
575
589
|
}
|
|
576
590
|
if (process.env.DEEL_MOTION || process.env.DEEL_OFFICE) {
|
|
577
591
|
say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.envWins'))}`);
|
package/src/i18n/en.js
CHANGED
|
@@ -132,7 +132,7 @@ export const en = {
|
|
|
132
132
|
'head.shiftTab': 'Shift+Tab',
|
|
133
133
|
'head.shiftTabHint': ' changes it · ',
|
|
134
134
|
'head.tabHint': 'Tab completes the / command you are typing',
|
|
135
|
-
'head.newlineHint': 'New line:
|
|
135
|
+
'head.newlineHint': 'New line: Alt+Enter (Option+Enter on Mac) breaks the line inside the box · if your terminal swallows it, end the line with space + ` then Enter',
|
|
136
136
|
'head.star': 'Enjoying this tool? Star us on GitHub —',
|
|
137
137
|
'head.thisPC': 'found',
|
|
138
138
|
'head.skills': 'skills {n}',
|
|
@@ -200,6 +200,7 @@ export const en = {
|
|
|
200
200
|
'motion.pickOne': 'Pick one of:',
|
|
201
201
|
'motion.envWins': 'DEEL_MOTION / DEEL_OFFICE is set, and the environment wins.',
|
|
202
202
|
'motion.tooSmall': 'This terminal is {줄} x {칸} — the office needs at least {최소줄} rows and {최소칸} columns.',
|
|
203
|
+
'motion.appleTerminal': "macOS Terminal spaces lines apart, so the room gets horizontal seams — Terminal › Settings › Profiles › Text › Font \"Change\" › set Line Spacing to 1.0. (iTerm2 defaults to 1.0, which is why it looks right there)",
|
|
203
204
|
|
|
204
205
|
// ── Screen language ───────────────────────────────────────────────────
|
|
205
206
|
'lang.now': 'Screen language is {이름}.',
|
package/src/i18n/ko.js
CHANGED
|
@@ -119,7 +119,7 @@ export const ko = {
|
|
|
119
119
|
'head.shiftTab': 'Shift+Tab',
|
|
120
120
|
'head.shiftTabHint': '으로 바꿉니다 · ',
|
|
121
121
|
'head.tabHint': 'Tab 은 치던 / 명령을 채웁니다',
|
|
122
|
-
'head.newlineHint': '줄바꿈:
|
|
122
|
+
'head.newlineHint': '줄바꿈: Alt+Enter(맥은 Option+Enter) 로 상자 안에서 줄이 바뀝니다 · 안 먹는 터미널이면 줄 끝에 빈칸+` 를 붙이고 Enter',
|
|
123
123
|
'head.star': '쓸 만하면 별 하나 주세요 —',
|
|
124
124
|
'head.thisPC': '이 PC',
|
|
125
125
|
'head.skills': '스킬 {n}',
|
|
@@ -187,6 +187,7 @@ export const ko = {
|
|
|
187
187
|
'motion.pickOne': '고를 수 있는 것:',
|
|
188
188
|
'motion.envWins': '환경변수(DEEL_MOTION·DEEL_OFFICE)가 켜져 있어 그쪽이 이깁니다.',
|
|
189
189
|
'motion.tooSmall': '이 터미널은 {줄}줄 × {칸}칸입니다 — 사무실은 {최소줄}줄 × {최소칸}칸부터 뜹니다.',
|
|
190
|
+
'motion.appleTerminal': '맥 기본 터미널은 줄 간격이 넓어 방에 가로줄이 그어집니다 — 터미널 › 설정 › 프로파일 › 텍스트 › 글꼴 「변경」 › 행간을 1.0 으로. (iTerm2 는 기본이 1.0 이라 멀쩡합니다)',
|
|
190
191
|
|
|
191
192
|
// ── 화면 말 (/lang) ───────────────────────────────────────────────────
|
|
192
193
|
'lang.now': '화면 말은 지금 {이름} 입니다.',
|
package/src/repl.js
CHANGED
|
@@ -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,7 +451,13 @@ export async function chatLoop(opts = {}) {
|
|
|
390
451
|
보낼것 = 이었나 ? [...이어쓰기줄들, l].join('\n') : l;
|
|
391
452
|
이어쓰기줄들 = null;
|
|
392
453
|
화면.입력지움();
|
|
393
|
-
|
|
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
|
+
}
|
|
394
461
|
} else if (l.trim() || 이어쓰기줄들 !== null) {
|
|
395
462
|
/*
|
|
396
463
|
* 일하는 도중에 미리 쳐 둔 것.
|
|
@@ -464,7 +531,52 @@ export async function chatLoop(opts = {}) {
|
|
|
464
531
|
*/
|
|
465
532
|
if (process.stdin.isTTY) {
|
|
466
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
|
+
|
|
467
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
|
+
}
|
|
468
580
|
// Shift+Tab — 승인 방식 (자동 → 위험만 → 모두)
|
|
469
581
|
if (key && key.name === 'tab' && key.shift) {
|
|
470
582
|
const 앞 = 승인고르기(session.mode);
|
|
@@ -494,10 +606,10 @@ export async function chatLoop(opts = {}) {
|
|
|
494
606
|
* 뺏어야 하는데, 그건 훨씬 자주 쓰는 기능이다.
|
|
495
607
|
*/
|
|
496
608
|
if (key && key.name === 'tab' && !key.shift && 상자쓰나 && 입력기다림 && 묻는중 === null) {
|
|
497
|
-
const 채울 = 채울글(rl.line
|
|
609
|
+
const 채울 = 채울글(펴기(rl.line), 지금추천(펴기(rl.line)));
|
|
498
610
|
if (채울) rl.write(채울);
|
|
499
611
|
// 채울 게 없어도 그리기는 한다 — 후보 목록이 그대로 남아 있어야 한다.
|
|
500
|
-
화면.입력갱신(session, rl.line
|
|
612
|
+
화면.입력갱신(session, 펴기(rl.line), rl.cursor ?? 0, 지금추천(펴기(rl.line)));
|
|
501
613
|
return;
|
|
502
614
|
}
|
|
503
615
|
/*
|
|
@@ -526,15 +638,15 @@ export async function chatLoop(opts = {}) {
|
|
|
526
638
|
* key.meta 로 여기서 골라낼 수 있다. rl.write('\n') 은 못 쓴다 —
|
|
527
639
|
* 그것도 내부적으로 줄을 끝내는 처리를 그대로 타 버린다(직접 확인).
|
|
528
640
|
*
|
|
529
|
-
* 커서
|
|
530
|
-
*
|
|
531
|
-
*
|
|
641
|
+
* 커서 자리에 줄바꿈을 **끼워 넣는다.** 상자가 그만큼 늘어나고, 위로
|
|
642
|
+
* 올라간 줄이 없으니 백스페이스로 지우는 것도 그대로 된다. \n 이
|
|
643
|
+
* 아니라 줄표를 넣는 이유는 위 선언부에 적어 두었다.
|
|
532
644
|
*/
|
|
533
645
|
if (key.meta && 입력기다림 && 묻는중 === null) {
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
rl.line =
|
|
537
|
-
rl.cursor =
|
|
646
|
+
const 글 = rl.line ?? '';
|
|
647
|
+
const 자리 = Math.min(rl.cursor ?? 글.length, 글.length);
|
|
648
|
+
rl.line = 글.slice(0, 자리) + 줄표 + 글.slice(자리);
|
|
649
|
+
rl.cursor = 자리 + 1;
|
|
538
650
|
} else {
|
|
539
651
|
return; // 줄이 끝나는 것은 'line' 이 맡는다
|
|
540
652
|
}
|
|
@@ -543,7 +655,7 @@ export async function chatLoop(opts = {}) {
|
|
|
543
655
|
const 앞 = 묻는중;
|
|
544
656
|
setImmediate(() => {
|
|
545
657
|
if (묻는중 === null) return;
|
|
546
|
-
process.stdout.write(`\r\x1b[2K${앞}${c.white(rl.line
|
|
658
|
+
process.stdout.write(`\r\x1b[2K${앞}${c.white(펴기(rl.line))}`);
|
|
547
659
|
});
|
|
548
660
|
return;
|
|
549
661
|
}
|
|
@@ -559,7 +671,7 @@ export async function chatLoop(opts = {}) {
|
|
|
559
671
|
setImmediate(() => {
|
|
560
672
|
그릴예정 = false;
|
|
561
673
|
if (입력기다림) {
|
|
562
|
-
화면.입력갱신(session, rl.line
|
|
674
|
+
화면.입력갱신(session, 펴기(rl.line), rl.cursor ?? 0, 지금추천(펴기(rl.line)));
|
|
563
675
|
} else {
|
|
564
676
|
/*
|
|
565
677
|
* 일하는 도중에 치고 있는 글.
|
|
@@ -572,7 +684,7 @@ export async function chatLoop(opts = {}) {
|
|
|
572
684
|
* 답이 흘러나오는 동안(줄 중간)에는 그려도 답 줄을 덮으므로,
|
|
573
685
|
* 상자 쪽에서 알아서 넘긴다. 글은 그대로 살아 있다.
|
|
574
686
|
*/
|
|
575
|
-
화면.대기갱신(rl.line
|
|
687
|
+
화면.대기갱신(펴기(rl.line), queue.length);
|
|
576
688
|
}
|
|
577
689
|
});
|
|
578
690
|
});
|
|
@@ -944,7 +1056,9 @@ export async function chatLoop(opts = {}) {
|
|
|
944
1056
|
// 잘 된 것은 경고 표시를 달지 않는다. ⚠ 가 붙으면 뭘 고쳐야 하나 싶어진다.
|
|
945
1057
|
for (const l of 길이알림) say(` ${mark.ok} ${c.gray(l)}`);
|
|
946
1058
|
for (const w of warn) say(` ${mark.warn} ${c.gray(w)}`);
|
|
947
|
-
|
|
1059
|
+
// ESC 를 앞에 둔다 — 멈추는 길로는 이것이 먼저다. Ctrl+C 는 두 번 누르면
|
|
1060
|
+
// 끝나 버려서, 급히 멈추려던 사람이 대화를 통째로 닫는 일이 실제로 있었다.
|
|
1061
|
+
say(` ${c.gray('/help 명령 목록')} ${c.gray('ESC 중단')} ${c.gray('Ctrl+C 중단·끝내기')}`);
|
|
948
1062
|
|
|
949
1063
|
/*
|
|
950
1064
|
* 입력 자리. 어떻게 생겼는지는 화면 쪽이 정한다 —
|
|
@@ -956,7 +1070,7 @@ export async function chatLoop(opts = {}) {
|
|
|
956
1070
|
* 치면 멀쩡히 보내진다 — 화면만 거짓말을 하는 셈이라 더 나쁘다.
|
|
957
1071
|
*/
|
|
958
1072
|
const prompt = () => {
|
|
959
|
-
const 글 = 상자쓰나 && 입력기다림 ? (rl.line
|
|
1073
|
+
const 글 = 상자쓰나 && 입력기다림 ? 펴기(rl.line) : '';
|
|
960
1074
|
화면.입력자리(session, 글, 글 ? (rl.cursor ?? 0) : 0, 글 ? 지금추천(글) : []);
|
|
961
1075
|
};
|
|
962
1076
|
|
|
@@ -965,7 +1079,6 @@ export async function chatLoop(opts = {}) {
|
|
|
965
1079
|
// 입력을 기다리는 중 → 한 번은 경고, 두 번이면 끝낸다
|
|
966
1080
|
// 느린 로컬 모델이 엉뚱한 답을 길게 뽑기 시작했을 때 끝까지 기다리지 않아도 된다.
|
|
967
1081
|
let interrupted = false;
|
|
968
|
-
let turn = null; // 지금 도는 턴의 AbortController
|
|
969
1082
|
rl.on('SIGINT', () => {
|
|
970
1083
|
/*
|
|
971
1084
|
* 쌓아 둔 줄이 있으면 먼저 버린다. 이게 없으면 이어쓰기를 **취소할 길이
|
|
@@ -1553,6 +1666,19 @@ export async function chatLoop(opts = {}) {
|
|
|
1553
1666
|
오류보이기(ev.text);
|
|
1554
1667
|
break;
|
|
1555
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
|
+
|
|
1556
1682
|
case 'done':
|
|
1557
1683
|
clearThinking();
|
|
1558
1684
|
if (streamed) { 답비우기(); say(''); }
|
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/tools/desc.en.js
CHANGED
|
@@ -134,10 +134,11 @@ export const 도구설명EN = {
|
|
|
134
134
|
},
|
|
135
135
|
},
|
|
136
136
|
Ask: {
|
|
137
|
-
desc: 'Ask the person one question
|
|
138
|
-
+ ' If you do not know what to do, never write "let me know" and stop; call this instead —'
|
|
137
|
+
desc: 'Ask the person one question at a genuine fork. Never write "let me know" and stop;'
|
|
139
138
|
+ ' asking in prose ends the turn and throws away everything you have looked at.'
|
|
140
|
-
+ ' Give 2-4 options and they answer with a single number.'
|
|
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.',
|
|
141
142
|
params: {
|
|
142
143
|
question: 'one sentence. Make it clear what has to be decided',
|
|
143
144
|
options: '2-4 choices, one line each, so the difference is visible',
|
package/src/tools/index.js
CHANGED
|
@@ -1282,7 +1282,9 @@ export const TOOLS = {
|
|
|
1282
1282
|
* **뒤에서부터 문장째로** 잘라 낸다. 8k 에서 남는 것은 앞 90자다.
|
|
1283
1283
|
*/
|
|
1284
1284
|
description: '갈림길에서 사람에게 하나 묻는다. 글로 "알려주세요" 하고 끝내지 마라 —'
|
|
1285
|
-
+ ' 턴이 끝나서 여태 본 것이 버려진다. options 에 2~4개를 주면 숫자로 답한다.'
|
|
1285
|
+
+ ' 턴이 끝나서 여태 본 것이 버려진다. options 에 2~4개를 주면 숫자로 답한다.'
|
|
1286
|
+
+ ' **이미 사용자가 말한 것을 다시 묻지 마라.** "파일 정리해 줘" 라고 했으면'
|
|
1287
|
+
+ ' 그게 답이다 — 그대로 하면 된다. 어떻게 할지 정하는 것은 네 일이다.',
|
|
1286
1288
|
parameters: {
|
|
1287
1289
|
type: 'object',
|
|
1288
1290
|
properties: {
|
package/src/ui/ansi.js
CHANGED
|
@@ -71,6 +71,25 @@ export const cursor = {
|
|
|
71
71
|
보임: ON ? '\x1b[?25h' : '',
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
+
/*
|
|
75
|
+
* 한 판을 다 그린 뒤에 한 번에 보여 달라 (synchronized output, DEC 2026).
|
|
76
|
+
*
|
|
77
|
+
* 한 번의 write 로 보내도 터미널은 받는 대로 그린다. 그래서 열두 줄이 같이
|
|
78
|
+
* 바뀌는 판에서는 고쳐 그리는 중간 모습이 눈에 들어온다 — 잔상·찢김으로 보인다.
|
|
79
|
+
* 이 두 표 사이에 든 것은 다 받은 뒤에 한 번에 바뀐다.
|
|
80
|
+
*
|
|
81
|
+
* 모르는 터미널은 **그냥 무시한다**. 사설 모드라 못 알아들으면 아무 일도
|
|
82
|
+
* 안 일어난다 — 켜지는 데서만 좋아지고 나머지는 그대로다. (맥 기본 터미널은
|
|
83
|
+
* 이걸 모른다. 거기 떨림은 이걸로 안 잡힌다.)
|
|
84
|
+
*
|
|
85
|
+
* 색과 달리 ON 으로 안 막는다. 이걸 쓰는 자리는 입력 상자 하나뿐인데 그
|
|
86
|
+
* 화면은 터미널일 때만 켜진다 — 거기서 한 겹 더 막으면 막힌 것을 모르고
|
|
87
|
+
* 「안 나가는데」를 좋게 겪는다(처음 넣을 때 실제로 그랬다). 색이 아니므로
|
|
88
|
+
* NO_COLOR 과도 상관없다.
|
|
89
|
+
*/
|
|
90
|
+
export const 판시작 = '\x1b[?2026h';
|
|
91
|
+
export const 판끝 = '\x1b[?2026l';
|
|
92
|
+
|
|
74
93
|
// 한글·한자·가나는 터미널에서 두 칸을 차지한다. 표 정렬이 이걸 모르면 어긋난다.
|
|
75
94
|
export function width(str) {
|
|
76
95
|
let w = 0;
|
package/src/ui/inputbox.js
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* 2) 그릴 것을 값으로 내놓는다(프레임). 검사는 파이프로 도니 화면을 못 본다 —
|
|
34
34
|
* 값으로 내놓아야 한 칸도 안 어긋나는지 잴 수 있다.
|
|
35
35
|
*/
|
|
36
|
-
import { c, width, cursor, clip } from './ansi.js';
|
|
36
|
+
import { c, width, cursor, clip, 판시작, 판끝 } from './ansi.js';
|
|
37
37
|
import { isLocalHost } from '../safety/network.js';
|
|
38
38
|
import { statusLine, contextWarning } from './status.js';
|
|
39
39
|
import { 접어쓰기 } from './wrap.js';
|
|
@@ -457,7 +457,9 @@ export class InputBox {
|
|
|
457
457
|
// 예약해 둔 것이 있으면 잊지 않게 세어 준다. 몇 분짜리 일이 끝난 뒤
|
|
458
458
|
// 엉뚱한 말이 저절로 나가는 것처럼 보이면 안 된다.
|
|
459
459
|
this.일감.예약수 ? `${this.일감.예약수}건 예약됨` : '',
|
|
460
|
-
|
|
460
|
+
// 도는 동안 보이는 자리라 여기서는 ESC 만 말한다. Ctrl+C 도 되지만
|
|
461
|
+
// 두 번 누르면 끝나 버려서, 급히 멈추려는 사람에게 권할 것이 못 된다.
|
|
462
|
+
'ESC 중단',
|
|
461
463
|
].filter(Boolean).join(' · '),
|
|
462
464
|
대기: this.일감.대기,
|
|
463
465
|
});
|
|
@@ -633,7 +635,22 @@ export class InputBox {
|
|
|
633
635
|
|| 남은줄 > 0
|
|
634
636
|
|| 지금때 - this.마지막통째로 >= 자가치유주기;
|
|
635
637
|
|
|
636
|
-
|
|
638
|
+
/*
|
|
639
|
+
* ── 한 판을 통째로 바꿔 달라고 터미널에 부탁한다 ──────────────────────
|
|
640
|
+
*
|
|
641
|
+
* 여기서 나가는 것은 이미 한 번의 write 다. 그래도 떨림이 보이는 터미널이
|
|
642
|
+
* 있다 — write 는 한 번이라도 터미널은 받는 대로 그리므로, 열두 줄을
|
|
643
|
+
* 고쳐 그리는 중간 모습이 그대로 눈에 들어온다. 사무실처럼 한 판에 여러
|
|
644
|
+
* 줄이 같이 바뀌는 화면에서 제일 심하다(잔상·찢김).
|
|
645
|
+
*
|
|
646
|
+
* `?2026` 은 "이 사이에 들어오는 것은 다 그린 뒤에 한 번에 보여 달라" 는
|
|
647
|
+
* 뜻이다. 지원하는 터미널(iTerm2 · WezTerm · kitty · Windows Terminal ·
|
|
648
|
+
* Ghostty)에서는 떨림이 사라지고, 모르는 터미널은 **그냥 무시한다** —
|
|
649
|
+
* 사설 모드라 못 알아들으면 아무 일도 안 일어난다. 그래서 잃을 것이 없다.
|
|
650
|
+
*
|
|
651
|
+
* 맥 기본 터미널은 이걸 모른다. 거기 떨림은 이걸로 안 잡힌다.
|
|
652
|
+
*/
|
|
653
|
+
let 나갈것 = 판시작 + cursor.숨김 + '\r' + 위로(윗줄까지);
|
|
637
654
|
if (통째로) {
|
|
638
655
|
나갈것 += 줄들.map((l) => l + 줄끝지움).join('\n') + '\n';
|
|
639
656
|
for (let i = 0; i < 남은줄; i++) 나갈것 += 줄끝지움 + '\n';
|
|
@@ -655,7 +672,7 @@ export class InputBox {
|
|
|
655
672
|
*/
|
|
656
673
|
const 오른쪽 = Math.max(0, 자리.열 - 1);
|
|
657
674
|
나갈것 += 위로(남은줄 + 자리.위 + 1) + '\r' + (오른쪽 ? `\x1b[${오른쪽}C` : '');
|
|
658
|
-
나갈것 += cursor
|
|
675
|
+
나갈것 += cursor.보임 + 판끝;
|
|
659
676
|
process.stdout.write(나갈것);
|
|
660
677
|
|
|
661
678
|
this.그린줄 = 줄들.length;
|