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 CHANGED
@@ -18,7 +18,7 @@ Zero dependencies · Node 20+ · Exactly one place your source can go
18
18
 
19
19
  [![Node.js CI](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/test.yml?branch=main&logo=github&logoColor=white&label=Node.js%20CI)](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
20
20
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/codeql.yml?branch=main&logo=github&logoColor=white&label=CodeQL)](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
21
- [![tests](https://img.shields.io/badge/tests-3%2C614%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/en/develop.md)
21
+ [![tests](https://img.shields.io/badge/tests-3%2C848%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/en/develop.md)
22
22
 
23
23
  [![dependencies](https://img.shields.io/badge/dependencies-0-1a7f37)](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
24
24
  [![ESM](https://img.shields.io/badge/ESM-Node%2020%2B-5FA04E?logo=javascript&logoColor=white)](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.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
 
@@ -363,7 +363,8 @@ Names follow Claude Code / Codex conventions.
363
363
  | Command | What it does |
364
364
  |---|---|
365
365
  | `/help` | Command list |
366
- | `/lang [ko\|en]` | Screen language. Falls back to Korean for anything not translated yet |
366
+ | `/lang [ko\|en] [prompt lang]` | Screen language. **The prompt language is a separate axis** — `/lang en ko` instructs the model in Korean while answering you in English. Korean screen + English prompt cuts the 8k fixed share by 23% |
367
+ | `/keys` | Press keys to see what your terminal actually sends — for when new lines will not work |
367
368
  | `/bell [on\|off]` | Ring and set the window title when a turn ends, or when deel needs an answer |
368
369
  | `/consult <profile> <question>` | Ask a second model one question. Your current model stays put |
369
370
  | `/export` | This conversation as a **one-page HTML report** — asked, changed, verified. Self-contained, opens on any network |
@@ -496,11 +497,13 @@ Names and arguments match Claude Code, so skills written for that convention wor
496
497
  | `Write` | Write / overwrite a file (**several at once via the `files` array**) |
497
498
  | `Append` | Append to the end of a file — **how large files get written in pieces** |
498
499
  | `Edit` | Replace an exact string (`replace_all`; **several sites at once via the `edits` array**) |
500
+ | `Move` | Move / rename files and folders — **how you restructure** (`moves` array; covered by undo) |
499
501
  | `Glob` | Find files by name pattern |
500
502
  | `Grep` | Regex search file contents |
501
503
  | `Bash` | Run a command (**`background: true` for anything that does not finish**) |
502
504
  | `Skill` | Expand a skill body (shown to the model only when skills exist) |
503
505
  | `WebFetch` | Read a web page (read-only; hidden under `--offline`) |
506
+ | `Ask` | **Ask you back** at a fork — offers the choices, takes a single number |
504
507
  | `Recall` | Search **past conversations** — the model digs up "that thing last time" itself |
505
508
  | `Remember` | One line that outlives the session — known from the start next time |
506
509
  | `TodoWrite` | Checklist — breaks long work into steps and shows progress |
@@ -511,8 +514,8 @@ Names and arguments match Claude Code, so skills written for that convention wor
511
514
  | `Def` | **Where a name is defined** — only shown when a language server is installed |
512
515
  | `Refs` | **Every place a name is used** — only shown when a language server is installed |
513
516
 
514
- Seven tools here are not in Claude Code — `Append`, `Recall`, `Remember`, `Outline`,
515
- `Verify`, `Task`, `Jobs`. Each tool costs 150-400 tokens of schema on **every request**,
517
+ Nine tools here are not in Claude Code — `Append`, `Move`, `Ask`, `Recall`, `Remember`,
518
+ `Outline`, `Verify`, `Task`, `Jobs`. Each tool costs 150-400 tokens of schema on **every request**,
516
519
  so a test stops you every time the list grows (`test/loop.test.js`). The last four earned
517
520
  their cost; here is why.
518
521
 
@@ -929,7 +932,7 @@ Stored in `~/.deel/config.json`. A `.deel/config.json` in the project folder tak
929
932
  ## Development
930
933
 
931
934
  ```bash
932
- npm test Full suite (3,693 checks)
935
+ npm test Full suite (3,848 checks)
933
936
  npm run coverage Which lines the tests actually execute
934
937
  npm run verify Import + network checks only
935
938
  npm run bench Edit success rate
@@ -981,6 +984,8 @@ so one run tells you everything.
981
984
 
982
985
  | Version | What changed |
983
986
  |---|---|
987
+ | **[1.5.7](docs/en/releases.md#157)** | Korean folders on macOS made your own files "out of scope" · paste · ESC |
988
+ | **[1.5.6](docs/en/releases.md#156)** | It asks when stuck — instead of writing a question and ending the turn |
984
989
  | **[1.5.5](docs/en/releases.md#155)** | A plan fills the room — "only one person" was a bug, not a design |
985
990
  | [1.5.4](docs/en/releases.md#154) | A day passes in the room — morning to night, a wall clock, a cold coffee |
986
991
  | [1.5.3](docs/en/releases.md#153) | Three things a Mac user tripped over — newline hint, office stays put, one `/motion` |
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  [![Node.js CI](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/test.yml?branch=main&logo=github&logoColor=white&label=Node.js%20CI)](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
20
20
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/codeql.yml?branch=main&logo=github&logoColor=white&label=CodeQL)](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
21
- [![tests](https://img.shields.io/badge/tests-3%2C614%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/ko/develop.md)
21
+ [![tests](https://img.shields.io/badge/tests-3%2C848%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/ko/develop.md)
22
22
 
23
23
  [![dependencies](https://img.shields.io/badge/dependencies-0-1a7f37)](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
24
24
  [![ESM](https://img.shields.io/badge/ESM-Node%2020%2B-5FA04E?logo=javascript&logoColor=white)](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.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
 
@@ -342,7 +342,8 @@ LM Studio 는 `/api/v0/models`, llama.cpp 는 `/props`. 못 알아보면 `(추
342
342
  |---|---|
343
343
  | `/help` | 명령 목록 |
344
344
  | `/bell [on|off]` | 다 됐을 때·물어볼 때 종소리와 창 제목 |
345
- | `/lang [ko|en]` | 화면 영어권 사람이 때. 모델이 읽는 글도 같이 따라간다(영어 쪽이 1,400토큰 먹는다). 코드는 그대로 |
345
+ | `/lang [ko|en] [시킬말]` | 화면 말. **시킬 말을 따로 줄 수 있다** `/lang ko en` 이면 모델에게는 영어로 시키고 답은 한국어로 받는다(8k 창에서 고정 23% 감소). 코드는 그대로 |
346
+ | `/keys` | 이 터미널이 무슨 키를 보내는지 눌러서 확인 — 줄바꿈이 안 될 때 |
346
347
  | `/consult <프로필> <질문>` | 다른 모델에게 한 번 물어보기 — 지금 쓰는 것은 안 바꿈 |
347
348
  | `/export` | 이 대화를 **보고서 한 장**(HTML)으로 — 시킨 것·바뀐 것·확인한 것. 폐쇄망에서 여는 자기완결 파일 |
348
349
  | `/lsp [on\|off]` | 언어 서버 — 무엇이 깔려 있고 `Def`·`Refs` 를 쓸 수 있는지. `off` 는 고친 뒤 진단만 끔 |
@@ -471,11 +472,13 @@ LM Studio 는 `/api/v0/models`, llama.cpp 는 `/props`. 못 알아보면 `(추
471
472
  | `Write` | 파일 쓰기·덮어쓰기 (**여러 개는 `files` 배열로 한 번에**) |
472
473
  | `Append` | 파일 끝에 이어 붙이기 — **큰 파일을 나눠 쓰는 자리** |
473
474
  | `Edit` | 정확한 문자열 바꾸기 (`replace_all` · **여러 군데는 `edits` 배열로 한 번에**) |
475
+ | `Move` | 파일·폴더 옮기기·이름 바꾸기 — **구조를 바꾸는 자리** (`moves` 배열로 한 번에 · 되돌리기에 잡힘) |
474
476
  | `Glob` | 이름 패턴으로 파일 찾기 |
475
477
  | `Grep` | 내용 정규식 검색 |
476
478
  | `Bash` | 명령 실행 (**끝나지 않는 것은 `background: true`**) |
477
479
  | `Skill` | 스킬 본문 펼쳐 읽기 (스킬이 있을 때만 모델에게 보임) |
478
480
  | `WebFetch` | 웹 페이지 읽기 (읽기 전용 · `--offline` 이면 숨김) |
481
+ | `Ask` | 갈림길에서 **사람에게 되묻기** — 고를 것을 주고 숫자 하나로 받는다 |
479
482
  | `Recall` | **지난 대화**에서 찾기 — "저번에 그거" 를 모델이 스스로 뒤진다 |
480
483
  | `Remember` | 대화가 끝나도 남길 것 한 줄 — 다음에 켤 때 처음부터 안다 |
481
484
  | `TodoWrite` | 할 일 목록 — 긴 일을 쪼개서 어디까지 했는지 화면에 띄움 |
@@ -486,8 +489,8 @@ LM Studio 는 `/api/v0/models`, llama.cpp 는 `/props`. 못 알아보면 `(추
486
489
  | `Def` | 이름이 **어디 정의됐는지** — 언어 서버가 깔려 있을 때만 보입니다 |
487
490
  | `Refs` | 이름을 **어디서 쓰는지** 전부 — 언어 서버가 깔려 있을 때만 보입니다 |
488
491
 
489
- Claude Code 에 없는 것은 일곱입니다 — `Append` · `Recall` · `Remember` ·
490
- `Outline` · `Verify` · `Task` · `Jobs`. 도구 하나가 스키마로 150~400토큰을 먹고
492
+ Claude Code 에 없는 것은 아홉입니다 — `Append` · `Move` · `Ask` · `Recall` ·
493
+ `Remember` · `Outline` · `Verify` · `Task` · `Jobs`. 도구 하나가 스키마로 150~400토큰을 먹고
491
494
  그게 **매 요청마다** 나가므로, 늘릴 때마다 검사에서 한 번 멈추게 해 뒀습니다
492
495
  (`test/loop.test.js`). 뒤의 넷은 그 값을 치르고도 넣을 만해서 넣은 것들이라,
493
496
  아래에 왜인지를 적어 뒀습니다.
@@ -907,7 +910,7 @@ deel sbom --only sbom # SBOM 한 장만
907
910
  ## 개발
908
911
 
909
912
  ```bash
910
- npm test 전체 검증 (3,693항목)
913
+ npm test 전체 검증 (3,848항목)
911
914
  npm run coverage 검사가 소스의 어디를 밟았는지
912
915
  npm run verify 반입·통신 검증만
913
916
  npm run bench 편집 성공률 측정
@@ -972,6 +975,8 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
972
975
 
973
976
  | 판 | 무엇이 바뀌었나 |
974
977
  |---|---|
978
+ | **[1.5.7](docs/ko/releases.md#157)** | 맥 한글 폴더에서 제 파일이 「범위 밖」이던 것 · 붙여넣기 · ESC |
979
+ | **[1.5.6](docs/ko/releases.md#156)** | 막히면 물어본다 — 글로 묻고 턴을 끝내는 대신에 |
975
980
  | **[1.5.5](docs/ko/releases.md#155)** | 계획을 세우면 방이 찬다 — 「한 명뿐」은 고정이 아니라 버그였다 |
976
981
  | [1.5.4](docs/ko/releases.md#154) | 방에 하루가 흐른다 — 아침·낮·노을·밤 · 벽시계 · 식은 커피 |
977
982
  | [1.5.3](docs/ko/releases.md#153) | 맥에서 걸린 세 가지 — 줄바꿈 안내 · 사무실이 안 사라짐 · `/motion` 하나로 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
4
4
  "description": "로컬 모델·사내 게이트웨이 전용 코딩 에이전트 CLI — 외부 의존성 0개 / Zero-dependency coding agent CLI for local LLMs and private gateways",
5
5
  "type": "module",
6
6
  "license": "MIT",
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
 
@@ -15,7 +15,10 @@
15
15
  // 묻기 모드에도 준다: "저번에 이거 어떻게 했더라" 가 딱 묻기 모드의 일이다.
16
16
  // Def·Refs 도 읽기다 — 아무것도 안 바꾼다. 언어 서버가 없는 자리에서는
17
17
  // toolSchemas 가 알아서 빼므로 여기서는 갈래만 정한다.
18
- const 읽기 = ['Read', 'Outline', 'Glob', 'Grep', 'Def', 'Refs', 'WebFetch', 'Skill', 'Recall'];
18
+ // Ask **모든 모드**에 있다(읽기 갈래에 이유가 그것이다). 갈림길은
19
+ // 어느 모드에서나 생기고, 물어볼 길이 없으면 모델은 글로 "알려주세요" 하고
20
+ // 턴을 끝내 버린다 — 그러면 여태 조사한 것이 통째로 버려진다.
21
+ const 읽기 = ['Read', 'Outline', 'Glob', 'Grep', 'Def', 'Refs', 'WebFetch', 'Skill', 'Recall', 'Ask'];
19
22
  // 계획을 적는 도구. 파일을 안 건드리므로 읽기 전용 모드에서도 준다.
20
23
  //
21
24
  // Remember 도 여기 있다. 기억은 사용자의 소스를 안 건드리고 .deel/memory.md
@@ -27,7 +30,9 @@ const 계획 = ['TodoWrite', 'Remember'];
27
30
  // Write 를 주는 자리에는 반드시 같이 준다. 하나만 주면 나눠 쓸 방법이 없어진다.
28
31
  // Jobs 는 Bash 와 짝이다. Bash 를 주는 자리에는 반드시 같이 준다 —
29
32
  // background 로 띄워 놓고 읽을 길이 없으면 띄운 것이 그냥 유령이 된다.
30
- const 쓰기 = ['Write', 'Append', 'Edit', 'Bash', 'Jobs'];
33
+ // Move 구조를 바꾸는 유일한 길이다. 이게 없으면 "폴더를 역할대로 나눠 줘"
34
+ // 에 남는 길이 Bash mv 뿐인데, 그건 매번 승인을 묻고 되돌리기에 안 잡힌다.
35
+ const 쓰기 = ['Write', 'Append', 'Edit', 'Move', 'Bash', 'Jobs'];
31
36
  // 만든 것을 확인하는 도구. 아무것도 안 바꾸지만 **쓰는 모드에만** 준다 —
32
37
  // 안 만든 모드에서 확인할 것이 없고, 도구 정의로 나가는 자리만 먹는다.
33
38
  const 확인 = ['Verify'];
@@ -45,7 +50,7 @@ const 확인 = ['Verify'];
45
50
  */
46
51
  const 쪼개기 = ['Task'];
47
52
 
48
- import { 언어 } from '../i18n/index.js';
53
+ import { 언어, 지시말 } from '../i18n/index.js';
49
54
 
50
55
  export const MODES = {
51
56
  // 처음에는 여기서 시작한다.
@@ -347,7 +352,7 @@ export function 말(id, ctx) {
347
352
  * 아무 지시도 없는 채로 도는데, 그게 화면 빈칸보다 훨씬 나쁘다 —
348
353
  * 모드가 있는 것처럼 보이면서 실제로는 아무 일도 안 한다.
349
354
  */
350
- if (언어() === 'en') {
355
+ if (지시말() === 'en') {
351
356
  if (좁은가 && m.say짧게En) return m.say짧게En;
352
357
  if (m.sayEn) return m.sayEn;
353
358
  }
@@ -8,7 +8,7 @@ import { 매김, 급말, 값 as 급값, 지켜본것 } from './grade.js';
8
8
  import { 지문 } from './project.js';
9
9
  import { 프롬프트토막 as 기억토막 } from './memory.js';
10
10
  import { 못박기 } from './pins.js';
11
- import { 언어 } from '../i18n/index.js';
11
+ import { 언어, 지시말 } from '../i18n/index.js';
12
12
 
13
13
  // 토큰 추정 — 정확한 토크나이저 없이 대략만 센다.
14
14
  // 한글은 글자당 약 1토큰, 영문·코드는 약 4글자당 1토큰으로 본다.
@@ -29,6 +29,13 @@ const BASE_RULES = `너는 deel 다. 사용자의 작업 폴더 안에서 코드
29
29
 
30
30
  - 바로 시작한다. 없는 파일·폴더는 만든다. 그게 시킨 일의 일부다.
31
31
  - 되묻는 것은 **도구로도 못 알아낼 때뿐**이다. 정할 수 있으면 정하고 무엇으로 정했는지 말한다.
32
+ - 그래도 물어야 하면 **Ask 도구로 묻는다.** 글로 "알려주세요" 하고 끝내지 마라 — 그러면 그 턴이
33
+ 끝나서 여태 읽은 것이 다 버려지고, 사람은 아무것도 안 된 화면을 본다. Ask 는 답이 그 자리로
34
+ 돌아와 하던 일이 이어진다. 고를 것을 2~4개 같이 준다.
35
+ - **이미 시킨 것을 다시 묻지 마라.** "파일 정리해 줘" 는 이미 답이다. 어떻게 정리할지 정하는
36
+ 것이 네 일이지, 그걸 되물으면 사람은 같은 말을 두 번 하게 된다.
37
+ - 도구가 자꾸 실패하면 **그것을 말해라.** 못 읽은 파일이 몇 개인지 적고 무엇이 막혔는지 알려라.
38
+ 실패를 삼킨 채 "무엇을 도와드릴까요" 로 끝내면, 사람은 왜 안 됐는지 영영 모른다.
32
39
  - 여러 파일을 만들고 나눠 담아야 하는 일이면 그렇게 한다. 하나만 건드려 놓고 멈추지 마라.
33
40
  - 다 했으면 확인한다. 돌려 보고 안 되면 고친다. 확인 못 했으면 "확인 못 했다" 고 말한다.
34
41
 
@@ -66,6 +73,8 @@ const BASE_RULES_짧게 = `너는 deel 다. 사용자의 작업 폴더에서 코
66
73
  시킨 일을 끝까지 해낸다. 계획만 내고 멈추지 마라.
67
74
  - 바로 시작한다. 없는 파일·폴더는 만든다.
68
75
  - 도구로 알아낼 수 있으면 되묻지 말고 정한다. 무엇으로 정했는지는 말한다.
76
+ - 그래도 물어야 하면 Ask 도구로 묻는다. 글로 묻고 끝내면 턴이 끝나 여태 읽은 것이 버려진다.
77
+ - 이미 시킨 것을 다시 묻지 마라. 도구가 자꾸 실패하면 몇 개가 막혔는지 말해라.
69
78
  - 파일이 여럿이면 다 만든다. 하나만 하고 멈추지 마라.
70
79
  - 끝내기 전에 Verify 로 확인한다. 확인 못 했으면 "확인 못 했다" 고 말한다.
71
80
 
@@ -101,6 +110,13 @@ const BASE_RULES_EN = `You are deel, a tool that reads and edits code inside the
101
110
 
102
111
  - Start now. Create missing files and folders — that is part of the job.
103
112
  - Ask back **only when no tool can tell you**. If you can decide, decide, and say what you decided it from.
113
+ - When you truly must ask, **ask with the Ask tool.** Do not write "let me know what you want" and stop — that ends
114
+ the turn, everything you read is thrown away, and the user sees a screen where nothing happened. An Ask answer
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.
104
120
  - If the job needs several files, make them all. Do not touch one and stop.
105
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."
106
122
 
@@ -131,6 +147,8 @@ const BASE_RULES_짧게_EN = `You are deel. You read and edit code in the user's
131
147
  Finish the job. Do not stop at a plan.
132
148
  - Start now. Create missing files and folders.
133
149
  - If a tool can tell you, decide instead of asking. Say what you decided it from.
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.
134
152
  - If there are several files, make them all. Do not do one and stop.
135
153
  - Verify before you finish. If you could not verify, say so.
136
154
 
@@ -150,8 +168,22 @@ Finish the job. Do not stop at a plan.
150
168
  */
151
169
  function 기본규칙(ctx) {
152
170
  const 짧게 = Number(ctx) > 0 && Number(ctx) < 24000;
153
- if (언어() === 'en') return 짧게 ? BASE_RULES_짧게_EN : BASE_RULES_EN;
154
- return 짧게 ? BASE_RULES_짧게 : BASE_RULES;
171
+ /*
172
+ * 시키는 말은 지시말() 정한다 — 화면 말과 다른 축이다(i18n/index.js).
173
+ *
174
+ * 그래서 "영어로 시키고 한국어로 받기" 가 된다. 규칙 글은 영어판을 쓰되,
175
+ * **답하는 말**만 다시 못 박는다. 안 박으면 영어 규칙 안의
176
+ * "Answer the user in English" 가 그대로 먹어서, 한국 사람이 영어 답을
177
+ * 받는다 — 값을 아끼려다 읽을 수 없는 답을 받는 셈이다.
178
+ */
179
+ const 시키는말 = 지시말();
180
+ const 글 = 시키는말 === 'en'
181
+ ? (짧게 ? BASE_RULES_짧게_EN : BASE_RULES_EN)
182
+ : (짧게 ? BASE_RULES_짧게 : BASE_RULES);
183
+ if (시키는말 === 언어()) return 글;
184
+ return `${글}\n\n${언어() === 'ko'
185
+ ? '**답은 한국어로 해라.** 위 규칙이 영어로 적혀 있어도 사용자에게 하는 말은 한국어다.'
186
+ : '**Answer in English.** The rules above are in Korean, but what you say to the user is English.'}`;
155
187
  }
156
188
 
157
189
  export class Session {
package/src/commands.js CHANGED
@@ -8,7 +8,7 @@ import { allowEndpoint } from './safety/network.js';
8
8
  import { pick } from './ui/prompt.js';
9
9
  import { load, save, resolveKey, upsert } from './config.js';
10
10
  import { 종, 알릴만한초 } from './ui/notify.js';
11
- import { 말, 언어, 언어정하기, 언어고르기, 옮긴만큼 } from './i18n/index.js';
11
+ import { 말, 언어, 언어정하기, 언어고르기, 옮긴만큼, 지시말, 지시말정하기, 지시말따로정했나 } from './i18n/index.js';
12
12
  import { 프로필찾기, 쓸수있나, 연결만들기, 알릴말, 목록보기 } from './agent/models.js';
13
13
  import { allowTemporarily } from './safety/network.js';
14
14
  import { chat } from './backend/adapter.js';
@@ -110,6 +110,7 @@ const 명령들 = {
110
110
  memory: { arg: true },
111
111
  level: { arg: true },
112
112
  bell: { arg: true },
113
+ keys: { arg: false },
113
114
  motion: { arg: true },
114
115
  consult: { arg: true },
115
116
  lang: { arg: true },
@@ -312,16 +313,34 @@ export async function handle(line, session, ctx) {
312
313
  }
313
314
 
314
315
  case 'lang': {
315
- const 값 = String(arg ?? '').trim();
316
+ /*
317
+ * 두 축이다 — 화면 말, 그리고 **모델에게 시키는 말**.
318
+ *
319
+ * /lang en 둘 다 영어
320
+ * /lang ko en 화면은 한국어, 시키는 말만 영어 ← "영어로 시키고 한국어로 받기"
321
+ * /lang ko auto 다시 하나로 (시키는 말이 화면 말을 따라간다)
322
+ *
323
+ * 굳이 나눈 이유는 값이다. 한글은 글자당 약 1토큰이고 영문은 약 3.6자당
324
+ * 1토큰이라(session.js 의 estimateTokens), 시키는 말만 영어로 돌려도
325
+ * 고정 몫이 눈에 띄게 준다. 작은 창일수록 그 차이가 크다.
326
+ * 답은 그대로 한국어로 온다 — 기본규칙() 이 그 한 줄을 따로 못 박는다.
327
+ */
328
+ const 쪼갠것 = String(arg ?? '').trim().split(/\s+/).filter(Boolean);
329
+ const 값 = 쪼갠것[0] ?? '';
330
+ const 시킬말 = 쪼갠것[1] ?? null;
316
331
  const 이름 = (l) => 말(`lang.${l}`);
317
332
  if (!값) {
318
333
  const p = 옮긴만큼();
319
334
  say('');
320
335
  say(` ${c.gray(말('lang.now', { 이름: 이름(언어()) }))}`);
336
+ say(` ${c.gray(말('lang.promptNow', {
337
+ 이름: 지시말따로정했나() ? 이름(지시말()) : 말('lang.follows'),
338
+ }))}`);
321
339
  say(` ${c.gray(말('lang.progress', { 언어: 언어(), 옮김: p.옮김, 전체: p.전체 }))}`);
322
340
  if (p.남음) say(` ${c.gray(말('lang.partial'))}`);
323
341
  say(` ${c.gray(말('lang.codeStays'))}`);
324
342
  say(` ${c.gray(말('lang.howto'))} ${c.cyan('/lang ko')} ${c.gray('·')} ${c.cyan('/lang en')}`);
343
+ say(` ${c.gray(말('lang.splitHowto'))} ${c.cyan('/lang ko en')}`);
325
344
  say(` ${c.gray(말('lang.envHint'))}`);
326
345
  say('');
327
346
  return { handled: true };
@@ -331,13 +350,27 @@ export async function handle(line, session, ctx) {
331
350
  say('');
332
351
  return { handled: true };
333
352
  }
353
+ // 시킬 말을 같이 줬으면 그것부터 본다 — 화면 말만 바꿔 놓고 거절하면
354
+ // 사람이 시킨 것의 절반만 먹은 채로 끝난다.
355
+ if (시킬말 !== null && !지시말정하기(시킬말)) {
356
+ say(` ${c.gray(말('lang.unknown'))}`);
357
+ say('');
358
+ return { handled: true };
359
+ }
334
360
  언어정하기(값);
335
361
  const cfg = load();
336
362
  cfg.lang = 언어();
363
+ // 따로 안 정했으면 설정에서도 지운다. 남겨 두면 다음에 켤 때
364
+ // 「화면 말을 따라간다」 가 아니라 옛 값이 되살아난다.
365
+ if (지시말따로정했나()) cfg.promptLang = 지시말();
366
+ else delete cfg.promptLang;
337
367
  try { save(cfg); } catch { /* 못 남겨도 이번 세션에는 먹는다 */ }
338
368
  const p = 옮긴만큼();
339
369
  say('');
340
370
  say(` ${mark.ok} ${말('lang.changed', { 이름: 이름(언어()) })}`);
371
+ if (지시말() !== 언어()) {
372
+ say(` ${c.gray(말('lang.promptChanged', { 이름: 이름(지시말()), 답: 이름(언어()) }))}`);
373
+ }
341
374
  say(` ${c.gray(말('lang.progress', { 언어: 언어(), 옮김: p.옮김, 전체: p.전체 }))}`);
342
375
  if (p.남음) say(` ${c.gray(말('lang.partial'))}`);
343
376
  say('');
@@ -421,6 +454,24 @@ export async function handle(line, session, ctx) {
421
454
  return { handled: true };
422
455
  }
423
456
 
457
+ /*
458
+ * 이 터미널이 무슨 바이트를 보내나 (repl.js 의 키확인).
459
+ *
460
+ * 줄바꿈이 안 된다는 말은 여러 판째 나왔는데, 그때마다 문서에 "터미널
461
+ * 설정을 바꾸세요" 라고만 적었다. 그건 고친 것이 아니다 — 사람은 제
462
+ * 터미널에서 무엇이 오는지 볼 길이 없었다. 여기서는 짐작 대신 눌러 본다.
463
+ */
464
+ case 'keys': {
465
+ if (typeof ctx?.키확인 !== 'function') {
466
+ say('');
467
+ say(` ${c.gray('키 확인은 상자 화면에서만 됩니다 (--no-tui 나 파이프에서는 못 씁니다).')}`);
468
+ say('');
469
+ return { handled: true };
470
+ }
471
+ await ctx.키확인();
472
+ return { handled: true };
473
+ }
474
+
424
475
  case 'bell': {
425
476
  const cfg = load();
426
477
  const 켜는말 = ['on', '켬', '켜', '켜기', 'y', 'yes', 'ㅇ'];
@@ -521,6 +572,20 @@ export async function handle(line, session, ctx) {
521
572
  if (줄 && 칸 && (줄 < 최소높이 || 칸 < 최소폭)) {
522
573
  say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.tooSmall', { 줄, 칸, 최소줄: 최소높이, 최소칸: 최소폭 }))}`);
523
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
+ }
524
589
  }
525
590
  if (process.env.DEEL_MOTION || process.env.DEEL_OFFICE) {
526
591
  say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.envWins'))}`);
package/src/i18n/en.js CHANGED
@@ -80,12 +80,13 @@ export const en = {
80
80
  'cmd.memory.arg': '[forget <n>|clear|<text to remember>]',
81
81
  'cmd.level.desc': 'user level (beginner/developer)',
82
82
  'cmd.level.arg': '[level]',
83
+ 'cmd.keys.desc': 'press keys to see what your terminal actually sends — when new lines will not work',
83
84
  'cmd.bell.desc': 'bell and window title when a turn ends or deel needs you (on/off)',
84
85
  'cmd.bell.arg': '[on|off]',
85
86
  'cmd.motion.desc': 'what moves while deel works — knight, animals, office',
86
87
  'cmd.motion.arg': '[knight|animal|office|default|off]',
87
88
  'cmd.lang.desc': 'screen language — 한국어 / English',
88
- 'cmd.lang.arg': '[ko|en]',
89
+ 'cmd.lang.arg': '[ko|en] [prompt lang]',
89
90
  'cmd.init.desc': 'create a DEEL.md rules file',
90
91
  'cmd.exit.desc': 'quit',
91
92
  'cmd.quit.desc': 'quit',
@@ -131,7 +132,8 @@ export const en = {
131
132
  'head.shiftTab': 'Shift+Tab',
132
133
  'head.shiftTabHint': ' changes it · ',
133
134
  'head.tabHint': 'Tab completes the / command you are typing',
134
- 'head.newlineHint': 'New line: end a line with space + ` then Enter works everywhere (Alt+Enter only if your terminal sends it)',
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
+ 'head.star': 'Enjoying this tool? Star us on GitHub —',
135
137
  'head.thisPC': 'found',
136
138
  'head.skills': 'skills {n}',
137
139
  'head.commands': 'commands {n}',
@@ -198,6 +200,7 @@ export const en = {
198
200
  'motion.pickOne': 'Pick one of:',
199
201
  'motion.envWins': 'DEEL_MOTION / DEEL_OFFICE is set, and the environment wins.',
200
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)",
201
204
 
202
205
  // ── Screen language ───────────────────────────────────────────────────
203
206
  'lang.now': 'Screen language is {이름}.',
@@ -208,7 +211,11 @@ export const en = {
208
211
  'lang.unknown': 'Please say ko or en.',
209
212
  'lang.progress': '{언어}: {옮김} of {전체} strings translated. The rest come through in Korean.',
210
213
  'lang.partial': 'Not everything is translated yet — untranslated lines show Korean rather than a blank.',
211
- 'lang.codeStays': 'What the model reads follows too English costs about 1,400 fewer tokens per request. Code, tool names and argument names stay as they are.',
214
+ 'lang.codeStays': 'Unless you set it separately, what the model reads follows the screen. Code, tool names and argument names stay as they are.',
215
+ 'lang.promptNow': 'The model is instructed in {이름}.',
216
+ 'lang.follows': 'the same as the screen',
217
+ 'lang.splitHowto': 'To be instructed in English but answered in Korean:',
218
+ 'lang.promptChanged': 'Instructions in {이름}, answers in {답} — 23% off the fixed share on an 8k window.',
212
219
  'lang.envHint': 'To start in English every time, set DEEL_LANG=en',
213
220
 
214
221
  'cmd.lsp.desc': 'language servers — what is installed, and whether Def/Refs are available',
@@ -273,6 +280,13 @@ export const en = {
273
280
  'level.developer.name': 'developer',
274
281
  'level.developer.hint': 'everything by hand',
275
282
 
283
+ // ── Ask box (the Ask tool) ────────────────────────────────────────────
284
+ 'ask.title': 'A question for you',
285
+ 'ask.pickHint': 'Pick a number, or type your own answer',
286
+ 'ask.freeHint': 'Type your answer',
287
+ 'ask.pickPrompt': '1-{끝}, or type your own',
288
+ 'ask.freePrompt': 'Answer',
289
+
276
290
  // ── Common ────────────────────────────────────────────────────────────
277
291
  'common.unknownCommand': 'Unknown command',
278
292
  'common.seeHelp': 'Run /help for the list',
package/src/i18n/index.js CHANGED
@@ -67,9 +67,45 @@ export function 언어정하기(값) {
67
67
  */
68
68
  export function 언어잡기({ env = process.env, cfg = null } = {}) {
69
69
  지금 = 언어고르기(env.DEEL_LANG) ?? 언어고르기(cfg?.lang) ?? 기본언어;
70
+ 지시 = 언어고르기(env.DEEL_PROMPT_LANG) ?? 언어고르기(cfg?.promptLang) ?? null;
70
71
  return 지금;
71
72
  }
72
73
 
74
+ /*
75
+ * ── 시키는 말과 보는 말은 다른 축이다 ──────────────────────────────────
76
+ *
77
+ * 여태 하나였다. /lang en 을 켜면 화면도, **모델에게 주는 글도**, 모델이
78
+ * 답하는 말도 한꺼번에 영어가 됐다. 그래서 "영어로 시키고 한국어로 받기" 가
79
+ * 아예 안 됐는데, 그게 실제로 쓸모가 있다:
80
+ *
81
+ * · 값이 눅다. 한글은 글자당 약 1토큰, 영문은 약 3.6자당 1토큰이다
82
+ * (session.js 의 estimateTokens). 8k 창에서 고정 몫이 눈에 띄게 준다.
83
+ * · 작은 모델이 영어 지시를 더 잘 따른다. 학습에 그쪽이 훨씬 많았다.
84
+ *
85
+ * 그런데 **읽는 사람은 한국 사람**이다. 그래서 둘을 갈랐다. 화면 말은
86
+ * 언어() 가, 모델에게 주는 글은 지시말() 이 정한다.
87
+ *
88
+ * null 이면 "따로 안 정했다" 는 뜻이고, 그때는 화면 말을 따라간다.
89
+ * 여태 쓰던 사람에게 아무것도 안 바뀌게 하려고 그렇게 뒀다.
90
+ */
91
+ let 지시 = null;
92
+
93
+ /** 모델에게 주는 글의 말. 따로 안 정했으면 화면 말을 따른다. */
94
+ export function 지시말() { return 지시 ?? 지금; }
95
+
96
+ /** 지시말을 따로 정한다. 빈 값·auto 면 다시 화면 말을 따라간다. */
97
+ export function 지시말정하기(값) {
98
+ const v = String(값 ?? '').trim().toLowerCase();
99
+ if (!v || v === 'auto' || v === '따라') { 지시 = null; return true; }
100
+ const 고른것 = 언어고르기(v);
101
+ if (!고른것) return false;
102
+ 지시 = 고른것;
103
+ return true;
104
+ }
105
+
106
+ /** 따로 정해 뒀나. 화면에 "따라감" 과 "영어로 못 박음" 을 갈라 보이려고 쓴다. */
107
+ export function 지시말따로정했나() { return 지시 !== null; }
108
+
73
109
  /*
74
110
  * 조사는 앞 글자의 받침을 보고 갈린다.
75
111
  *