deel-local-cli 1.5.5 → 1.5.6
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 +10 -6
- package/README.md +10 -6
- package/package.json +1 -1
- package/src/agent/modes.js +9 -4
- package/src/agent/session.js +25 -3
- package/src/commands.js +53 -2
- package/src/i18n/en.js +15 -2
- package/src/i18n/index.js +36 -0
- package/src/i18n/ko.js +16 -3
- package/src/repl.js +164 -7
- package/src/safety/undo.js +8 -1
- package/src/tools/desc.en.js +21 -0
- package/src/tools/index.js +262 -7
- package/src/ui/inputbox.js +27 -10
- package/src/ui/status.js +20 -0
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.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.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.
|
|
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
|
-
|
|
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,
|
|
935
|
+
npm test Full suite (3,785 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,7 @@ so one run tells you everything.
|
|
|
981
984
|
|
|
982
985
|
| Version | What changed |
|
|
983
986
|
|---|---|
|
|
987
|
+
| **[1.5.6](docs/en/releases.md#156)** | It asks when stuck — instead of writing a question and ending the turn |
|
|
984
988
|
| **[1.5.5](docs/en/releases.md#155)** | A plan fills the room — "only one person" was a bug, not a design |
|
|
985
989
|
| [1.5.4](docs/en/releases.md#154) | A day passes in the room — morning to night, a wall clock, a cold coffee |
|
|
986
990
|
| [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
|
[](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.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.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]` | 화면
|
|
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 에 없는 것은
|
|
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,
|
|
913
|
+
npm test 전체 검증 (3,785항목)
|
|
911
914
|
npm run coverage 검사가 소스의 어디를 밟았는지
|
|
912
915
|
npm run verify 반입·통신 검증만
|
|
913
916
|
npm run bench 편집 성공률 측정
|
|
@@ -972,6 +975,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
972
975
|
|
|
973
976
|
| 판 | 무엇이 바뀌었나 |
|
|
974
977
|
|---|---|
|
|
978
|
+
| **[1.5.6](docs/ko/releases.md#156)** | 막히면 물어본다 — 글로 묻고 턴을 끝내는 대신에 |
|
|
975
979
|
| **[1.5.5](docs/ko/releases.md#155)** | 계획을 세우면 방이 찬다 — 「한 명뿐」은 고정이 아니라 버그였다 |
|
|
976
980
|
| [1.5.4](docs/ko/releases.md#154) | 방에 하루가 흐른다 — 아침·낮·노을·밤 · 벽시계 · 식은 커피 |
|
|
977
981
|
| [1.5.3](docs/ko/releases.md#153) | 맥에서 걸린 세 가지 — 줄바꿈 안내 · 사무실이 안 사라짐 · `/motion` 하나로 |
|
package/package.json
CHANGED
package/src/agent/modes.js
CHANGED
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
// 묻기 모드에도 준다: "저번에 이거 어떻게 했더라" 가 딱 묻기 모드의 일이다.
|
|
16
16
|
// Def·Refs 도 읽기다 — 아무것도 안 바꾼다. 언어 서버가 없는 자리에서는
|
|
17
17
|
// toolSchemas 가 알아서 빼므로 여기서는 갈래만 정한다.
|
|
18
|
-
|
|
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
|
-
|
|
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 {
|
|
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 (
|
|
355
|
+
if (지시말() === 'en') {
|
|
351
356
|
if (좁은가 && m.say짧게En) return m.say짧게En;
|
|
352
357
|
if (m.sayEn) return m.sayEn;
|
|
353
358
|
}
|
package/src/agent/session.js
CHANGED
|
@@ -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 {
|
|
11
|
+
import { 언어, 지시말 } from '../i18n/index.js';
|
|
12
12
|
|
|
13
13
|
// 토큰 추정 — 정확한 토크나이저 없이 대략만 센다.
|
|
14
14
|
// 한글은 글자당 약 1토큰, 영문·코드는 약 4글자당 1토큰으로 본다.
|
|
@@ -29,6 +29,9 @@ const BASE_RULES = `너는 deel 다. 사용자의 작업 폴더 안에서 코드
|
|
|
29
29
|
|
|
30
30
|
- 바로 시작한다. 없는 파일·폴더는 만든다. 그게 시킨 일의 일부다.
|
|
31
31
|
- 되묻는 것은 **도구로도 못 알아낼 때뿐**이다. 정할 수 있으면 정하고 무엇으로 정했는지 말한다.
|
|
32
|
+
- 그래도 물어야 하면 **Ask 도구로 묻는다.** 글로 "알려주세요" 하고 끝내지 마라 — 그러면 그 턴이
|
|
33
|
+
끝나서 여태 읽은 것이 다 버려지고, 사람은 아무것도 안 된 화면을 본다. Ask 는 답이 그 자리로
|
|
34
|
+
돌아와 하던 일이 이어진다. 고를 것을 2~4개 같이 준다.
|
|
32
35
|
- 여러 파일을 만들고 나눠 담아야 하는 일이면 그렇게 한다. 하나만 건드려 놓고 멈추지 마라.
|
|
33
36
|
- 다 했으면 확인한다. 돌려 보고 안 되면 고친다. 확인 못 했으면 "확인 못 했다" 고 말한다.
|
|
34
37
|
|
|
@@ -66,6 +69,7 @@ const BASE_RULES_짧게 = `너는 deel 다. 사용자의 작업 폴더에서 코
|
|
|
66
69
|
시킨 일을 끝까지 해낸다. 계획만 내고 멈추지 마라.
|
|
67
70
|
- 바로 시작한다. 없는 파일·폴더는 만든다.
|
|
68
71
|
- 도구로 알아낼 수 있으면 되묻지 말고 정한다. 무엇으로 정했는지는 말한다.
|
|
72
|
+
- 그래도 물어야 하면 Ask 도구로 묻는다. 글로 묻고 끝내면 턴이 끝나 여태 읽은 것이 버려진다.
|
|
69
73
|
- 파일이 여럿이면 다 만든다. 하나만 하고 멈추지 마라.
|
|
70
74
|
- 끝내기 전에 Verify 로 확인한다. 확인 못 했으면 "확인 못 했다" 고 말한다.
|
|
71
75
|
|
|
@@ -101,6 +105,9 @@ const BASE_RULES_EN = `You are deel, a tool that reads and edits code inside the
|
|
|
101
105
|
|
|
102
106
|
- Start now. Create missing files and folders — that is part of the job.
|
|
103
107
|
- Ask back **only when no tool can tell you**. If you can decide, decide, and say what you decided it from.
|
|
108
|
+
- When you truly must ask, **ask with the Ask tool.** Do not write "let me know what you want" and stop — that ends
|
|
109
|
+
the turn, everything you read is thrown away, and the user sees a screen where nothing happened. An Ask answer
|
|
110
|
+
comes straight back to where you are, so the work carries on. Give 2–4 options to pick from.
|
|
104
111
|
- If the job needs several files, make them all. Do not touch one and stop.
|
|
105
112
|
- 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
113
|
|
|
@@ -131,6 +138,7 @@ const BASE_RULES_짧게_EN = `You are deel. You read and edit code in the user's
|
|
|
131
138
|
Finish the job. Do not stop at a plan.
|
|
132
139
|
- Start now. Create missing files and folders.
|
|
133
140
|
- If a tool can tell you, decide instead of asking. Say what you decided it from.
|
|
141
|
+
- If you must ask, use the Ask tool. Asking in prose ends the turn and throws away what you read.
|
|
134
142
|
- If there are several files, make them all. Do not do one and stop.
|
|
135
143
|
- Verify before you finish. If you could not verify, say so.
|
|
136
144
|
|
|
@@ -150,8 +158,22 @@ Finish the job. Do not stop at a plan.
|
|
|
150
158
|
*/
|
|
151
159
|
function 기본규칙(ctx) {
|
|
152
160
|
const 짧게 = Number(ctx) > 0 && Number(ctx) < 24000;
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
/*
|
|
162
|
+
* 시키는 말은 지시말() 이 정한다 — 화면 말과 다른 축이다(i18n/index.js).
|
|
163
|
+
*
|
|
164
|
+
* 그래서 "영어로 시키고 한국어로 받기" 가 된다. 규칙 글은 영어판을 쓰되,
|
|
165
|
+
* **답하는 말**만 다시 못 박는다. 안 박으면 영어 규칙 안의
|
|
166
|
+
* "Answer the user in English" 가 그대로 먹어서, 한국 사람이 영어 답을
|
|
167
|
+
* 받는다 — 값을 아끼려다 읽을 수 없는 답을 받는 셈이다.
|
|
168
|
+
*/
|
|
169
|
+
const 시키는말 = 지시말();
|
|
170
|
+
const 글 = 시키는말 === 'en'
|
|
171
|
+
? (짧게 ? BASE_RULES_짧게_EN : BASE_RULES_EN)
|
|
172
|
+
: (짧게 ? BASE_RULES_짧게 : BASE_RULES);
|
|
173
|
+
if (시키는말 === 언어()) return 글;
|
|
174
|
+
return `${글}\n\n${언어() === 'ko'
|
|
175
|
+
? '**답은 한국어로 해라.** 위 규칙이 영어로 적혀 있어도 사용자에게 하는 말은 한국어다.'
|
|
176
|
+
: '**Answer in English.** The rules above are in Korean, but what you say to the user is English.'}`;
|
|
155
177
|
}
|
|
156
178
|
|
|
157
179
|
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 { 말, 언어, 언어정하기, 언어고르기,
|
|
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
|
-
|
|
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', 'ㅇ'];
|
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',
|
|
@@ -132,6 +133,7 @@ export const en = {
|
|
|
132
133
|
'head.shiftTabHint': ' changes it · ',
|
|
133
134
|
'head.tabHint': 'Tab completes the / command you are typing',
|
|
134
135
|
'head.newlineHint': 'New line: end a line with space + ` then Enter — works everywhere (Alt+Enter only if your terminal sends it)',
|
|
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}',
|
|
@@ -208,7 +210,11 @@ export const en = {
|
|
|
208
210
|
'lang.unknown': 'Please say ko or en.',
|
|
209
211
|
'lang.progress': '{언어}: {옮김} of {전체} strings translated. The rest come through in Korean.',
|
|
210
212
|
'lang.partial': 'Not everything is translated yet — untranslated lines show Korean rather than a blank.',
|
|
211
|
-
'lang.codeStays': '
|
|
213
|
+
'lang.codeStays': 'Unless you set it separately, what the model reads follows the screen. Code, tool names and argument names stay as they are.',
|
|
214
|
+
'lang.promptNow': 'The model is instructed in {이름}.',
|
|
215
|
+
'lang.follows': 'the same as the screen',
|
|
216
|
+
'lang.splitHowto': 'To be instructed in English but answered in Korean:',
|
|
217
|
+
'lang.promptChanged': 'Instructions in {이름}, answers in {답} — 23% off the fixed share on an 8k window.',
|
|
212
218
|
'lang.envHint': 'To start in English every time, set DEEL_LANG=en',
|
|
213
219
|
|
|
214
220
|
'cmd.lsp.desc': 'language servers — what is installed, and whether Def/Refs are available',
|
|
@@ -273,6 +279,13 @@ export const en = {
|
|
|
273
279
|
'level.developer.name': 'developer',
|
|
274
280
|
'level.developer.hint': 'everything by hand',
|
|
275
281
|
|
|
282
|
+
// ── Ask box (the Ask tool) ────────────────────────────────────────────
|
|
283
|
+
'ask.title': 'A question for you',
|
|
284
|
+
'ask.pickHint': 'Pick a number, or type your own answer',
|
|
285
|
+
'ask.freeHint': 'Type your answer',
|
|
286
|
+
'ask.pickPrompt': '1-{끝}, or type your own',
|
|
287
|
+
'ask.freePrompt': 'Answer',
|
|
288
|
+
|
|
276
289
|
// ── Common ────────────────────────────────────────────────────────────
|
|
277
290
|
'common.unknownCommand': 'Unknown command',
|
|
278
291
|
'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
|
*
|
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': '끝내기',
|
|
@@ -119,6 +120,7 @@ export const ko = {
|
|
|
119
120
|
'head.shiftTabHint': '으로 바꿉니다 · ',
|
|
120
121
|
'head.tabHint': 'Tab 은 치던 / 명령을 채웁니다',
|
|
121
122
|
'head.newlineHint': '줄바꿈: 줄 끝에 빈칸+` 를 붙이고 Enter — 어디서나 됩니다 (Alt+Enter 는 터미널이 보내 줄 때만)',
|
|
123
|
+
'head.star': '쓸 만하면 별 하나 주세요 —',
|
|
122
124
|
'head.thisPC': '이 PC',
|
|
123
125
|
'head.skills': '스킬 {n}',
|
|
124
126
|
'head.commands': '명령 {n}',
|
|
@@ -195,8 +197,12 @@ export const ko = {
|
|
|
195
197
|
'lang.unknown': 'ko 나 en 으로 적어 주세요.',
|
|
196
198
|
'lang.progress': '{언어}: {옮김}/{전체} 마디가 옮겨져 있습니다. 나머지는 한국어로 나옵니다.',
|
|
197
199
|
'lang.partial': '아직 다 안 옮겼습니다 — 빈칸을 내는 대신 한국어를 그대로 냅니다.',
|
|
198
|
-
'lang.codeStays': '모델이 읽는 글도
|
|
199
|
-
'lang.
|
|
200
|
+
'lang.codeStays': '따로 안 정하면 모델이 읽는 글도 화면 말을 따라갑니다. 코드와 도구·인자 이름은 그대로입니다.',
|
|
201
|
+
'lang.promptNow': '모델에게 시키는 말은 {이름}.',
|
|
202
|
+
'lang.follows': '화면 말을 따라갑니다',
|
|
203
|
+
'lang.splitHowto': '영어로 시키고 한국어로 받으려면',
|
|
204
|
+
'lang.promptChanged': '시키는 말은 {이름}, 답은 {답:으로} 받습니다 — 8k 창에서 고정 몫이 23% 줄어듭니다.',
|
|
205
|
+
'lang.envHint': '켤 때부터 영어로 쓰려면 DEEL_LANG=en (시키는 말만 영어로는 DEEL_PROMPT_LANG=en)',
|
|
200
206
|
|
|
201
207
|
'cmd.lsp.desc': '언어 서버 — 무엇이 깔려 있고 Def·Refs 를 쓸 수 있는지',
|
|
202
208
|
'cmd.lsp.arg': '[on|off]',
|
|
@@ -260,6 +266,13 @@ export const ko = {
|
|
|
260
266
|
'level.developer.name': '개발자',
|
|
261
267
|
'level.developer.hint': '전부 직접 만짐',
|
|
262
268
|
|
|
269
|
+
// ── 되묻기 상자 (Ask 도구) ────────────────────────────────────────────
|
|
270
|
+
'ask.title': '물어봅니다',
|
|
271
|
+
'ask.pickHint': '숫자로 고르거나, 그냥 쳐서 답하세요',
|
|
272
|
+
'ask.freeHint': '답을 쳐 주세요',
|
|
273
|
+
'ask.pickPrompt': '1-{끝} 또는 직접 입력',
|
|
274
|
+
'ask.freePrompt': '답',
|
|
275
|
+
|
|
263
276
|
// ── 자주 나오는 것 ────────────────────────────────────────────────────
|
|
264
277
|
'common.unknownCommand': '모르는 명령입니다',
|
|
265
278
|
'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';
|
|
@@ -391,14 +391,33 @@ export async function chatLoop(opts = {}) {
|
|
|
391
391
|
이어쓰기줄들 = null;
|
|
392
392
|
화면.입력지움();
|
|
393
393
|
if (l.trim() || 이었나) say(`${이었나 ? ' ' : ` ${c.hcyan('❯')} `}${c.white(l)}`);
|
|
394
|
-
} else if (l.trim()) {
|
|
394
|
+
} else if (l.trim() || 이어쓰기줄들 !== null) {
|
|
395
395
|
/*
|
|
396
396
|
* 일하는 도중에 미리 쳐 둔 것.
|
|
397
397
|
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
398
|
+
* ── 여기서도 줄 끝 백틱이 먹어야 한다 ────────────────────────────
|
|
399
|
+
*
|
|
400
|
+
* 예전에는 이 갈래에 이어쓰기가 아예 없었다. 그래서 **일하는 도중에**
|
|
401
|
+
* 여러 줄을 치면 백틱이 그냥 글자로 나갔다 — 첫 줄이 백틱을 단 채로
|
|
402
|
+
* 혼자 보내지고, 다음 줄은 따로 또 보내진다. 사람 눈에는 줄바꿈이
|
|
403
|
+
* 안 먹는 것으로 보인다. 기다릴 때는 되고 일할 때는 안 되니, 되는
|
|
404
|
+
* 자리를 찾기도 어렵다.
|
|
405
|
+
*
|
|
406
|
+
* 미리 치는 것은 일하는 중에 하는 짓이라 오히려 여기가 더 흔하다.
|
|
407
|
+
*
|
|
408
|
+
* 그리는 것은 다르게 한다. 줄쌓기() 는 대화에 줄을 찍는데, 일하는
|
|
409
|
+
* 도중에 찍으면 이미 보낸 것처럼 보인다. 여기서는 조용히 쌓고
|
|
400
410
|
* 상자에 몇 건이 밀려 있는지만 세어 준다.
|
|
401
411
|
*/
|
|
412
|
+
if (이어쓰기표시(l)) {
|
|
413
|
+
(이어쓰기줄들 ??= []).push(l.slice(0, -1).replace(/\s+$/, ''));
|
|
414
|
+
화면.대기갱신('', queue.length + 1);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (이어쓰기줄들 !== null) {
|
|
418
|
+
보낼것 = [...이어쓰기줄들, l].join('\n');
|
|
419
|
+
이어쓰기줄들 = null;
|
|
420
|
+
}
|
|
402
421
|
화면.대기갱신('', queue.length + 1);
|
|
403
422
|
}
|
|
404
423
|
}
|
|
@@ -646,6 +665,127 @@ export async function chatLoop(opts = {}) {
|
|
|
646
665
|
const pw = await askHidden(rl, label, nextLine);
|
|
647
666
|
return pw === null || pw === '' ? null : pw;
|
|
648
667
|
},
|
|
668
|
+
/*
|
|
669
|
+
* ── 모델이 사람에게 되묻는 자리 (Ask 도구) ──────────────────────────
|
|
670
|
+
*
|
|
671
|
+
* 계획 승인 상자와 같은 꼴로 그린다. 사람이 이미 아는 모양이라 새로 배울
|
|
672
|
+
* 것이 없고, 무엇보다 **숫자 하나로 끝난다.**
|
|
673
|
+
*
|
|
674
|
+
* 자유롭게 쳐도 그대로 간다. 고를 것이 늘 맞지는 않아서, 넷 다 아닐 때
|
|
675
|
+
* 다시 물어보게 하면 그게 제일 답답하다.
|
|
676
|
+
*/
|
|
677
|
+
/*
|
|
678
|
+
* ── 이 터미널이 무슨 바이트를 보내는지 본다 (/keys) ──────────────────
|
|
679
|
+
*
|
|
680
|
+
* 맥에서 줄바꿈이 안 된다는 말을 몇 판째 듣고도 못 고쳤다. 고칠 데를
|
|
681
|
+
* 못 찾은 게 아니라 **무슨 일이 나는지를 몰랐다.**
|
|
682
|
+
*
|
|
683
|
+
* Alt+Enter 는 앞에 ESC 가 붙어 올 때만 잡을 수 있는데(위 keypress 참고),
|
|
684
|
+
* macOS 터미널은 기본값이 Option 을 그렇게 안 보낸다. Shift+Enter·
|
|
685
|
+
* Ctrl+Enter 는 대부분 터미널에서 평범한 Enter 와 **바이트가 똑같다.**
|
|
686
|
+
* 즉 여기서는 셋을 구분할 길이 아예 없다 — 안 오는 것을 잡을 수는 없다.
|
|
687
|
+
*
|
|
688
|
+
* 그래서 문서에 "터미널 설정을 이렇게 바꾸세요" 라고만 적어 뒀는데, 그건
|
|
689
|
+
* 고친 것이 아니다. 사람이 제 터미널에서 **눌러 보고 바로 알 수 있어야**
|
|
690
|
+
* 한다. 여기서 보여 주는 것은 짐작이 아니라 실제로 온 바이트다.
|
|
691
|
+
*/
|
|
692
|
+
키확인: async () => {
|
|
693
|
+
if (!process.stdin.isTTY) {
|
|
694
|
+
say('');
|
|
695
|
+
say(` ${c.gray('키 확인은 터미널에서만 됩니다 (지금은 파이프로 들어와 있습니다).')}`);
|
|
696
|
+
say('');
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
say('');
|
|
700
|
+
say(` ${c.hcyan('┌')} ${c.bold('키 확인')}`);
|
|
701
|
+
say(` ${c.hcyan('│')} ${c.gray('아무 키나 눌러 보세요. 이 터미널이 실제로 보낸 바이트를 그대로 보여 줍니다.')}`);
|
|
702
|
+
say(` ${c.hcyan('│')} ${c.gray('줄바꿈에 쓰려던 것을 눌러 보세요 — Alt+Enter · Option+Enter · Shift+Enter.')}`);
|
|
703
|
+
say(` ${c.hcyan('└')} ${c.gray('끝내려면 Ctrl+C 또는 q')}`);
|
|
704
|
+
say('');
|
|
705
|
+
|
|
706
|
+
const 본것 = [];
|
|
707
|
+
await new Promise((끝) => {
|
|
708
|
+
const 듣기 = (ch, key) => {
|
|
709
|
+
const 이름 = key?.name ?? (ch ? JSON.stringify(ch) : '?');
|
|
710
|
+
if (이름 === 'q' || (key?.ctrl && key?.name === 'c')) {
|
|
711
|
+
process.stdin.off('keypress', 듣기);
|
|
712
|
+
return 끝();
|
|
713
|
+
}
|
|
714
|
+
// 실제로 온 바이트. 이것이 이 화면의 전부다.
|
|
715
|
+
const 날것 = JSON.stringify(String(key?.sequence ?? ch ?? ''))
|
|
716
|
+
.replace(/\\u001b/g, '\\e');
|
|
717
|
+
const 꾸밈 = [key?.ctrl && 'ctrl', key?.meta && 'meta', key?.shift && 'shift']
|
|
718
|
+
.filter(Boolean).join('+');
|
|
719
|
+
본것.push({ 이름, 날것, 꾸밈 });
|
|
720
|
+
say(` ${c.hcyan('·')} ${c.white(이름.padEnd(10))} ${c.gray(날것.padEnd(14))}`
|
|
721
|
+
+ `${꾸밈 ? c.hgreen(꾸밈) : c.gray('꾸밈 없음')}`);
|
|
722
|
+
};
|
|
723
|
+
process.stdin.on('keypress', 듣기);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
/*
|
|
727
|
+
* 눌러 본 것을 놓고 **이 터미널에서 되는 길**을 알려 준다.
|
|
728
|
+
*
|
|
729
|
+
* Enter 를 여러 번 눌렀는데 meta 가 한 번도 안 붙었으면, 그 터미널은
|
|
730
|
+
* Option/Alt 를 ESC 로 안 보내는 것이다. 그 자리에서는 아무리 눌러도
|
|
731
|
+
* 안 되므로, 백틱 쪽을 알려 주는 것이 맞다.
|
|
732
|
+
*/
|
|
733
|
+
const 엔터들 = 본것.filter((k) => k.이름 === 'return' || k.이름 === 'enter');
|
|
734
|
+
const 메타본적있나 = 엔터들.some((k) => /meta/.test(k.꾸밈));
|
|
735
|
+
say('');
|
|
736
|
+
if (!엔터들.length) {
|
|
737
|
+
say(` ${c.gray('Enter 를 안 눌러 봤습니다. 줄바꿈을 보려면 Alt/Option+Enter 를 눌러 보세요.')}`);
|
|
738
|
+
} else if (메타본적있나) {
|
|
739
|
+
say(` ${mark.ok} ${c.white('이 터미널은 Alt/Option+Enter 를 보냅니다')} ${c.gray('— 그대로 쓰시면 됩니다.')}`);
|
|
740
|
+
} else {
|
|
741
|
+
say(` ${mark.warn} ${c.white('이 터미널은 Alt/Option+Enter 를 안 보냅니다.')}`);
|
|
742
|
+
say(` ${c.gray(' 누른 Enter 가 평범한 Enter 와 바이트가 같아서, 프로그램 쪽에서 가려낼 길이 없습니다.')}`);
|
|
743
|
+
say('');
|
|
744
|
+
say(` ${c.white('줄바꿈은 이렇게 하세요')} ${c.gray('— 어느 터미널에서나 됩니다')}`);
|
|
745
|
+
say(` ${c.gray(' 줄 끝에')} ${c.cyan('빈칸 + `')} ${c.gray('를 붙이고 Enter. 마지막 줄은 그냥 Enter.')}`);
|
|
746
|
+
say('');
|
|
747
|
+
say(` ${c.gray(' Option 을 굳이 쓰시려면 터미널 설정을 바꿔야 합니다:')}`);
|
|
748
|
+
say(` ${c.gray(' Terminal.app 설정 → 프로파일 → 키보드 → ')}${c.cyan('Option을 Meta 키로 사용')}`);
|
|
749
|
+
say(` ${c.gray(' iTerm2 Settings → Profiles → Keys → Left Option → ')}${c.cyan('Esc+')}`);
|
|
750
|
+
say(` ${c.gray(' VS Code ')}${c.cyan('terminal.integrated.macOptionIsMeta')}${c.gray(' 를 true 로')}`);
|
|
751
|
+
}
|
|
752
|
+
say('');
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
ask물음: async (물음, 고를것 = []) => {
|
|
756
|
+
if (closed) return null;
|
|
757
|
+
say('');
|
|
758
|
+
say(` ${c.hcyan('┌')} ${c.bold(옮긴말('ask.title'))}`);
|
|
759
|
+
for (const 줄 of 접어쓰기(물음, Math.max(40, Math.min(88, (process.stdout.columns || 80) - 6)) - 4)) {
|
|
760
|
+
say(` ${c.hcyan('│')} ${c.white(줄)}`);
|
|
761
|
+
}
|
|
762
|
+
if (고를것.length) {
|
|
763
|
+
say(` ${c.hcyan('│')}`);
|
|
764
|
+
고를것.forEach((것, i) => {
|
|
765
|
+
say(` ${c.hcyan('│')} ${c.hcyan(String(i + 1))} ${c.white(것)}`);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
say(` ${c.hcyan('└')} ${c.gray(옮긴말(고를것.length ? 'ask.pickHint' : 'ask.freeHint'))}`);
|
|
769
|
+
say('');
|
|
770
|
+
|
|
771
|
+
// 막혀 있는 것은 기다린 만큼 그대로 손해다. confirm 과 같은 이유로 알린다.
|
|
772
|
+
if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
|
|
773
|
+
창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
|
|
774
|
+
const 답 = String(await ask(
|
|
775
|
+
고를것.length ? 옮긴말('ask.pickPrompt', { 끝: 고를것.length }) : 옮긴말('ask.freePrompt'),
|
|
776
|
+
{ def: 고를것.length ? '1' : '' },
|
|
777
|
+
)).trim();
|
|
778
|
+
창제목(제목글('도는중', { 폴더: 알림.폴더 }));
|
|
779
|
+
|
|
780
|
+
if (!답) return null;
|
|
781
|
+
// 숫자로 골랐으면 그 줄의 글을 그대로 돌려준다 — 모델에게 "2" 만 가면
|
|
782
|
+
// 무엇을 고른 것인지 모른다.
|
|
783
|
+
const n = Number(답);
|
|
784
|
+
const 고른것 = Number.isInteger(n) && n >= 1 && n <= 고를것.length ? 고를것[n - 1] : 답;
|
|
785
|
+
say(` ${c.hgreen('▶')} ${c.gray(clip(고른것, 70))}`);
|
|
786
|
+
say('');
|
|
787
|
+
return 고른것;
|
|
788
|
+
},
|
|
649
789
|
confirm: async (name, args) => {
|
|
650
790
|
say('');
|
|
651
791
|
say(` ${c.yellow('?')} ${toolLabel(name, args)}`);
|
|
@@ -1492,9 +1632,26 @@ export async function chatLoop(opts = {}) {
|
|
|
1492
1632
|
if (['n', 'no', 'ㄴ', '취소', '아니', '아니요', '아니오'].includes(낮춘)) {
|
|
1493
1633
|
say(` ${c.gray('그만뒀습니다. 계획은 위에 남아 있으니 이어서 말씀하셔도 됩니다.')}`);
|
|
1494
1634
|
} else if (['y', 'yes', 'ㅇ', 'ㅇㅇ', 'ㄱ', 'ㄱㄱ', '네', '응', '진행', 'ok'].includes(낮춘)) {
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1635
|
+
/*
|
|
1636
|
+
* 승인한 계획을 **글자 그대로 실어 보낸다.**
|
|
1637
|
+
*
|
|
1638
|
+
* 예전에는 "위 계획을 승인받았다 · 적어 둔 단계를 하나씩 끝내라" 라고만
|
|
1639
|
+
* 보냈다. 사람 눈에는 계획이 바로 위에 있으니 말이 되는데, 모델에게
|
|
1640
|
+
* 이건 **가리키기만 하고 안 주는 말**이다. 그래서 작은 모델은 "적어 둔"
|
|
1641
|
+
* 것을 찾으러 Recall 로 지난 대화를 뒤지기 시작했다 — 세 번 뒤지고
|
|
1642
|
+
* 세 번 못 찾고, 정작 시킨 일은 시작도 안 했다.
|
|
1643
|
+
*
|
|
1644
|
+
* 계획은 이미 손에 있다(ctx.todos). 가리키지 말고 준다. 두 번 적는
|
|
1645
|
+
* 것이 아까워서 안 준 것이었는데, 아낀 토큰보다 헛도는 값이 훨씬 컸다.
|
|
1646
|
+
*/
|
|
1647
|
+
const 단계글 = 할일.map((t, i) => `${i + 1}. ${String(t.text ?? '')}`).join('\n');
|
|
1648
|
+
이어갈것 = (할일.length
|
|
1649
|
+
? `방금 낸 계획을 사람이 승인했다. 계획은 아래 그대로다.\n\n${단계글}\n\n`
|
|
1650
|
+
+ '지금부터 이 단계를 하나씩 실제로 해라.'
|
|
1651
|
+
: '바로 위에 네가 적은 계획을 사람이 승인했다.'
|
|
1652
|
+
+ ' 그 계획을 다시 적지 말고 지금부터 그대로 해라.')
|
|
1653
|
+
+ ' 계획을 다시 적지 마라. **지난 대화를 뒤지지 마라** — 필요한 것은'
|
|
1654
|
+
+ ' 이 대화 안에 다 있다. 다 끝나면 무엇을 만들었는지만 짧게 알려라.';
|
|
1498
1655
|
이어갈모드 = 'code';
|
|
1499
1656
|
say('');
|
|
1500
1657
|
say(` ${c.hgreen('▶')} ${c.gray('계획대로 진행합니다.')}`);
|
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,27 @@ 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 fork, **do not pick for them** — use this.'
|
|
138
|
+
+ ' If you do not know what to do, never write "let me know" and stop; call this instead —'
|
|
139
|
+
+ ' 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.',
|
|
141
|
+
params: {
|
|
142
|
+
question: 'one sentence. Make it clear what has to be decided',
|
|
143
|
+
options: '2-4 choices, one line each, so the difference is visible',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
125
146
|
Recall: {
|
|
126
147
|
desc: 'Search past sessions in this folder. When the user points back ("last time"), use this'
|
|
127
148
|
+ ' instead of asking again. This does not search file contents — that is Grep.',
|
package/src/tools/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 도구. 이름과 인자를 Claude Code 와 같게 맞춘다 —
|
|
2
2
|
// 그래야 그 관례로 쓰인 스킬·명령이 그대로 먹는다.
|
|
3
|
-
import { writeFileSync, appendFileSync, readFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
4
|
-
import { dirname } from 'node:path';
|
|
3
|
+
import { writeFileSync, appendFileSync, readFileSync, existsSync, mkdirSync, statSync, renameSync, cpSync, rmSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
6
|
import { globToRegex, walk, readText, readTextFull, 내부살림 } from './fsutil.js';
|
|
7
7
|
import { encode, label as encLabel, decode as decodeBytes, consoleCodepage, looksBinary } from './encoding.js';
|
|
@@ -24,7 +24,7 @@ import { isDocPath, readDoc, toText as docText, summarize as docSummary, looksOl
|
|
|
24
24
|
import { diffLines } from '../ui/diff.js';
|
|
25
25
|
import { 읽을줄수, 찾을개수, 찾을줄수, 설명길이 } from '../agent/budget.js';
|
|
26
26
|
import { 도구설명EN } from './desc.en.js';
|
|
27
|
-
import {
|
|
27
|
+
import { 지시말 } from '../i18n/index.js';
|
|
28
28
|
|
|
29
29
|
/*
|
|
30
30
|
* 한 번에 돌려줄 양은 **모델에 맞춰** 정한다 (agent/budget.js).
|
|
@@ -265,6 +265,121 @@ function 문서읽기(abs, ctx) {
|
|
|
265
265
|
* 것이 중요하다. 한 덩이로 뜨면 `/undo` 가 전부-아니면-전무가 되어, 넷 중
|
|
266
266
|
* 하나만 잘못 만들었을 때 나머지 셋까지 날려야 한다.
|
|
267
267
|
*/
|
|
268
|
+
/*
|
|
269
|
+
* ── 하나 옮기기 ─────────────────────────────────────────────────────────
|
|
270
|
+
*
|
|
271
|
+
* 되돌리기는 **내용을 떠 놓는** 방식이다(safety/undo.js). 옮기기는 그 틀에
|
|
272
|
+
* 이렇게 얹는다: 떠난 자리와 닿을 자리를 **둘 다** 뜬다.
|
|
273
|
+
*
|
|
274
|
+
* 떠난 자리 — 내용이 떠지니 되돌리면 그 자리에 다시 쓰인다
|
|
275
|
+
* 닿을 자리 — 없던 파일이라 null 로 떠지고, 되돌리면 지워진다
|
|
276
|
+
*
|
|
277
|
+
* 둘이 합쳐져야 '옮기기 전' 이 된다. 하나만 뜨면 되돌린 뒤 파일이 두 군데
|
|
278
|
+
* 있거나(닿을 자리를 안 뜬 경우) 한 군데도 없다(떠난 자리를 안 뜬 경우).
|
|
279
|
+
*
|
|
280
|
+
* 그림·zip 같은 바이너리는 내용을 못 뜬다(skipped). 그때 되돌리기는 그 파일을
|
|
281
|
+
* **손대지 않는다** — 옮겨진 자리에 그대로 남는다. 되돌아가지는 않지만
|
|
282
|
+
* 없어지지도 않는다. 안전망이 파일을 지우는 것보다 이쪽이 낫다.
|
|
283
|
+
*/
|
|
284
|
+
function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
285
|
+
if (typeof from !== 'string' || !from) return { error: 'from 이 없습니다' };
|
|
286
|
+
if (typeof to !== 'string' || !to) return { error: 'to 가 없습니다' };
|
|
287
|
+
|
|
288
|
+
const 앞 = ctx.scope.resolve(from);
|
|
289
|
+
const 뒤 = ctx.scope.resolve(to);
|
|
290
|
+
if (!existsSync(앞)) return { error: `없는 파일입니다: ${from}` };
|
|
291
|
+
if (앞 === 뒤) return { error: `옮길 자리가 지금 자리와 같습니다: ${from}` };
|
|
292
|
+
|
|
293
|
+
// 읽기만 막고 옮기기를 열어 두면 .deel/config.json 을 옮겨 연결을 끊을 수 있다.
|
|
294
|
+
for (const p of [앞, 뒤]) { const 왜 = 내부살림(p); if (왜) return { error: 왜 }; }
|
|
295
|
+
|
|
296
|
+
const 폴더인가 = statSync(앞).isDirectory();
|
|
297
|
+
/*
|
|
298
|
+
* 폴더를 **제 안으로** 옮기면 그 폴더가 통째로 사라진다 (`mv a a/b`).
|
|
299
|
+
* 셸에서도 잘 나는 사고라 여기서 막는다.
|
|
300
|
+
*/
|
|
301
|
+
if (폴더인가 && (뒤 + sep).startsWith(앞 + sep)) {
|
|
302
|
+
return { error: `폴더를 제 안으로 옮길 수 없습니다: ${from} → ${to}` };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/*
|
|
306
|
+
* 닿을 자리에 이미 있으면 **기본값은 거절**이다.
|
|
307
|
+
*
|
|
308
|
+
* 조용히 덮어쓰면 그 파일 내용이 그 자리에서 없어진다. 구조를 바꾸는 일은
|
|
309
|
+
* 파일을 스무 개씩 옮기는 일이라, 이름이 겹치는 것이 드물지 않다.
|
|
310
|
+
* 겹쳤다고 알려 주면 모델이 이름을 고쳐서 다시 부른다.
|
|
311
|
+
*/
|
|
312
|
+
if (existsSync(뒤) && !overwrite) {
|
|
313
|
+
return { error: `이미 있습니다: ${to}\n 덮어쓰려면 overwrite: true 를 주세요.` };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/*
|
|
317
|
+
* 되돌릴 수 있게 뜬다.
|
|
318
|
+
*
|
|
319
|
+
* 폴더면 안의 파일을 하나씩 짝지어 뜬다. walk 는 node_modules 같은 것을
|
|
320
|
+
* 건너뛰므로(fsutil.js 의 SKIP_DIRS) 그 안은 이력에 안 남는다 — 옮기기는
|
|
321
|
+
* 그대로 되지만 되돌리기에는 안 잡힌다. 그런 폴더를 되돌리자고 수만 개를
|
|
322
|
+
* 뜨는 쪽이 훨씬 나쁘다.
|
|
323
|
+
*/
|
|
324
|
+
const 짝들 = 폴더인가
|
|
325
|
+
? walk(앞).map((f) => [f.path, join(뒤, relative(앞, f.path))])
|
|
326
|
+
: [[앞, 뒤]];
|
|
327
|
+
for (const [a, b] of 짝들) {
|
|
328
|
+
ctx.history.snapshot(a, 'Move');
|
|
329
|
+
ctx.history.snapshot(b, 'Move');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
mkdirSync(dirname(뒤), { recursive: true });
|
|
333
|
+
try {
|
|
334
|
+
renameSync(앞, 뒤);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
/*
|
|
337
|
+
* 드라이브가 다르면 rename 이 안 된다 (윈도우 C: → D:, 리눅스 마운트 경계).
|
|
338
|
+
* 그때만 복사해서 옮긴다. 늘 복사하지 않는 것은 큰 폴더에서 값이 크고,
|
|
339
|
+
* 복사 도중에 끊기면 양쪽에 반씩 남기 때문이다.
|
|
340
|
+
*/
|
|
341
|
+
if (err.code !== 'EXDEV') return { error: `못 옮겼습니다: ${err.message}` };
|
|
342
|
+
try {
|
|
343
|
+
cpSync(앞, 뒤, { recursive: true });
|
|
344
|
+
rmSync(앞, { recursive: true, force: true });
|
|
345
|
+
} catch (err2) {
|
|
346
|
+
return { error: `못 옮겼습니다: ${err2.message}` };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const [, b] of 짝들) ctx.seen.add(b);
|
|
351
|
+
const 무엇 = 폴더인가 ? `폴더 ${짝들.length}개 파일` : '';
|
|
352
|
+
return {
|
|
353
|
+
content: `옮김: ${ctx.scope.show(앞)} → ${ctx.scope.show(뒤)}${무엇 ? ` (${무엇})` : ''}`,
|
|
354
|
+
summary: `${ctx.scope.show(뒤)}`,
|
|
355
|
+
changed: 뒤,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** 여러 개를 한 번에. 하나가 막혀도 나머지는 옮기고, 무엇이 막혔는지 다 알린다. */
|
|
360
|
+
function 여러개옮기기(목록, ctx) {
|
|
361
|
+
const 된것 = [];
|
|
362
|
+
const 안된것 = [];
|
|
363
|
+
for (const 하나 of 목록) {
|
|
364
|
+
const r = 한개옮기기(하나, ctx);
|
|
365
|
+
if (r.error) 안된것.push(`${하나.from ?? '?'} → ${하나.to ?? '?'}: ${r.error}`);
|
|
366
|
+
else 된것.push(r.content);
|
|
367
|
+
}
|
|
368
|
+
/*
|
|
369
|
+
* 하나도 못 옮겼으면 오류로 돌려준다. 그래야 모델이 '됐다' 고 넘어가지
|
|
370
|
+
* 않는다. 하나라도 됐으면 결과로 돌려주되 막힌 것을 그 안에 다 적는다 —
|
|
371
|
+
* 절반만 옮겨진 상태를 모르고 지나가는 것이 제일 나쁘다.
|
|
372
|
+
*/
|
|
373
|
+
if (!된것.length) return { error: 안된것.join('\n') || '옮길 것이 없습니다' };
|
|
374
|
+
return {
|
|
375
|
+
content: `${된것.length}개 옮겼습니다${안된것.length ? `, ${안된것.length}개 실패` : ''}.\n`
|
|
376
|
+
+ 된것.map((c) => ` ✓ ${c.replace(/^옮김: /, '')}`).join('\n')
|
|
377
|
+
+ (안된것.length ? `\n${안된것.map((e) => ` ✗ ${e.split('\n')[0]}`).join('\n')}` : '')
|
|
378
|
+
+ (안된것.length ? '\n\n실패한 것만 다시 보내세요. 된 것은 다시 안 보내도 됩니다.' : ''),
|
|
379
|
+
summary: `${된것.length}개 옮김${안된것.length ? ` · ${안된것.length}개 실패` : ''}`,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
268
383
|
function 한파일쓰기(args, ctx) {
|
|
269
384
|
const abs = ctx.scope.resolve(args.file_path);
|
|
270
385
|
if (typeof args.content !== 'string') return { error: 'content 가 문자열이 아닙니다' };
|
|
@@ -712,6 +827,62 @@ export const TOOLS = {
|
|
|
712
827
|
},
|
|
713
828
|
},
|
|
714
829
|
|
|
830
|
+
/*
|
|
831
|
+
* ── 파일을 옮긴다 ───────────────────────────────────────────────────────
|
|
832
|
+
*
|
|
833
|
+
* 이게 없어서 「폴더 구조를 역할에 맞게 바꿔 줘」 가 계속 헛돌았다.
|
|
834
|
+
*
|
|
835
|
+
* 도구 목록에 옮기는 것이 없으니 길은 `Bash mv` 하나뿐이었는데, mv 는 위험
|
|
836
|
+
* 명령으로 잡혀 있어서(safety/guard.js 의 MUTATING) 승인 방식에 따라
|
|
837
|
+
* **파일 하나 옮길 때마다** 물었다. 스무 개면 스무 번이다. 게다가 Bash 로
|
|
838
|
+
* 옮긴 것은 되돌리기에 안 잡혀서, /undo 를 눌러도 구조가 안 돌아왔다.
|
|
839
|
+
*
|
|
840
|
+
* 그래서 시키는 일에 맞는 도구가 없었던 것이지, 모델이 말을 안 들은 게
|
|
841
|
+
* 아니었다. 도구를 주면 한 번에 스무 개를 옮기고 되돌리기에도 잡힌다.
|
|
842
|
+
*/
|
|
843
|
+
Move: {
|
|
844
|
+
schema: {
|
|
845
|
+
name: 'Move',
|
|
846
|
+
description: '파일·폴더를 옮기거나 이름을 바꾼다. 구조를 바꾸는 일은 이걸로 한다.'
|
|
847
|
+
+ ' **여러 개는 moves 배열로 한 번에** — 스무 개를 옮기려고 스무 번 부르지 마라.'
|
|
848
|
+
+ ' Bash 의 mv 를 쓰지 마라. 되돌리기에 안 잡히고 매번 승인을 묻는다.',
|
|
849
|
+
parameters: {
|
|
850
|
+
type: 'object',
|
|
851
|
+
properties: {
|
|
852
|
+
from: { type: 'string', description: '옮길 파일·폴더 (한 개일 때)' },
|
|
853
|
+
to: { type: 'string', description: '옮겨 갈 자리. 없는 폴더는 만든다 (한 개일 때)' },
|
|
854
|
+
moves: {
|
|
855
|
+
type: 'array',
|
|
856
|
+
description: '여러 개를 한 번에. 이걸 쓰면 from·to 는 안 쓴다.',
|
|
857
|
+
items: {
|
|
858
|
+
type: 'object',
|
|
859
|
+
properties: {
|
|
860
|
+
from: { type: 'string', description: '옮길 파일·폴더' },
|
|
861
|
+
to: { type: 'string', description: '옮겨 갈 자리' },
|
|
862
|
+
},
|
|
863
|
+
required: ['from', 'to'],
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
overwrite: {
|
|
867
|
+
type: 'boolean',
|
|
868
|
+
description: '옮겨 갈 자리에 이미 있어도 덮어쓴다. 기본은 false 라 겹치면 거절한다',
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
required: [],
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
run(args, ctx) {
|
|
875
|
+
const 목록 = Array.isArray(args.moves) ? args.moves.filter((x) => x && typeof x === 'object') : [];
|
|
876
|
+
if (목록.length) {
|
|
877
|
+
return 여러개옮기기(목록.map((m) => ({ ...m, overwrite: m.overwrite ?? args.overwrite })), ctx);
|
|
878
|
+
}
|
|
879
|
+
if (typeof args.from !== 'string' || !args.from) {
|
|
880
|
+
return { error: 'from 이 없습니다. 한 개면 from·to 를, 여러 개면 moves 배열을 주세요.' };
|
|
881
|
+
}
|
|
882
|
+
return 한개옮기기(args, ctx);
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
|
|
715
886
|
Glob: {
|
|
716
887
|
schema: {
|
|
717
888
|
name: 'Glob',
|
|
@@ -1083,6 +1254,83 @@ export const TOOLS = {
|
|
|
1083
1254
|
*
|
|
1084
1255
|
* 이 폴더의 기록만 본다. 작업 범위 밖은 애초에 읽을 수 없다.
|
|
1085
1256
|
*/
|
|
1257
|
+
/*
|
|
1258
|
+
* ── 사람에게 되묻기 ─────────────────────────────────────────────────────
|
|
1259
|
+
*
|
|
1260
|
+
* 이게 없어서 실제로 이런 일이 났다. 큰 폴더를 "역할에 맞게 구조 바꿔 줘"
|
|
1261
|
+
* 라고 시켰더니, 모델이 파일을 스무 개쯤 읽고 나서 대화창에 이렇게 적고
|
|
1262
|
+
* 멈췄다:
|
|
1263
|
+
*
|
|
1264
|
+
* "수행할 구체적인 작업 요청이 없습니다. 원하는 변경 사항을 알려주세요."
|
|
1265
|
+
*
|
|
1266
|
+
* 요청은 있었다. 모델이 **어떻게 물어야 할지 몰랐던** 것이다. 물어볼 길이
|
|
1267
|
+
* 없으니 글로 적고 턴을 끝냈고, 사람은 아무것도 안 된 화면을 봤다.
|
|
1268
|
+
*
|
|
1269
|
+
* 되묻기가 도구여야 하는 이유:
|
|
1270
|
+
* · 글로 물으면 그 턴이 **끝난다.** 사람이 답해도 앞의 조사 결과는 이미
|
|
1271
|
+
* 끝난 턴에 있고, 모델은 처음부터 다시 읽는다.
|
|
1272
|
+
* · 도구로 물으면 답이 도구 결과로 돌아와 **하던 자리에서 이어진다.**
|
|
1273
|
+
*
|
|
1274
|
+
* 고를 것을 같이 주게 하는 것이 핵심이다. "어떻게 할까요?" 만 오면 사람은
|
|
1275
|
+
* 다시 생각해야 하지만, 세 갈래를 주면 숫자 하나로 끝난다.
|
|
1276
|
+
*/
|
|
1277
|
+
Ask: {
|
|
1278
|
+
schema: {
|
|
1279
|
+
name: 'Ask',
|
|
1280
|
+
/*
|
|
1281
|
+
* 첫 문장에 제일 중요한 것을 둔다 — 좁은 창에서는 설명줄이기가
|
|
1282
|
+
* **뒤에서부터 문장째로** 잘라 낸다. 8k 에서 남는 것은 앞 90자다.
|
|
1283
|
+
*/
|
|
1284
|
+
description: '갈림길에서 사람에게 하나 묻는다. 글로 "알려주세요" 하고 끝내지 마라 —'
|
|
1285
|
+
+ ' 턴이 끝나서 여태 본 것이 버려진다. options 에 2~4개를 주면 숫자로 답한다.',
|
|
1286
|
+
parameters: {
|
|
1287
|
+
type: 'object',
|
|
1288
|
+
properties: {
|
|
1289
|
+
question: { type: 'string', description: '한 문장짜리 질문. 무엇을 정해야 하는지 분명하게' },
|
|
1290
|
+
options: {
|
|
1291
|
+
type: 'array',
|
|
1292
|
+
items: { type: 'string' },
|
|
1293
|
+
description: '고를 것 2~4개. 각각 한 줄로, 무엇이 달라지는지 알 수 있게',
|
|
1294
|
+
},
|
|
1295
|
+
},
|
|
1296
|
+
required: ['question'],
|
|
1297
|
+
},
|
|
1298
|
+
},
|
|
1299
|
+
async run(args, ctx) {
|
|
1300
|
+
const 물음 = String(args.question ?? '').trim();
|
|
1301
|
+
if (!물음) return { error: 'question 이 비었습니다' };
|
|
1302
|
+
|
|
1303
|
+
const 고를것 = (Array.isArray(args.options) ? args.options : [])
|
|
1304
|
+
.map((x) => String(x ?? '').trim()).filter(Boolean).slice(0, 4);
|
|
1305
|
+
|
|
1306
|
+
/*
|
|
1307
|
+
* 물어볼 자리가 없는 데서도 안 죽어야 한다 — `deel -p` 한 방 실행, 파이프,
|
|
1308
|
+
* 하위 작업. 거기서는 **막히지 말고** 스스로 판단하라고 돌려준다.
|
|
1309
|
+
* 답을 기다리며 서 있으면 그 실행은 영영 안 끝난다.
|
|
1310
|
+
*/
|
|
1311
|
+
/*
|
|
1312
|
+
* 결과는 반드시 `content` 로 돌려준다.
|
|
1313
|
+
*
|
|
1314
|
+
* loop.js 가 대화에 싣는 것은 `result.content` 하나다(그 자리 하나로
|
|
1315
|
+
* 비밀 가리기·중복 셈이 다 걸린다). 처음에 `answer` 로만 돌려줬더니
|
|
1316
|
+
* 화면에는 답이 멀쩡히 뜨는데 **모델에게는 빈 글이 갔다** — 물어보고
|
|
1317
|
+
* 답을 받고도 못 들은 셈이라, 없느니만 못한 자리가 될 뻔했다.
|
|
1318
|
+
*/
|
|
1319
|
+
if (typeof ctx.ask물음 !== 'function') {
|
|
1320
|
+
return {
|
|
1321
|
+
content: '지금은 사람에게 물을 수 없는 자리입니다(한 방 실행·하위 작업).'
|
|
1322
|
+
+ ' 되묻지 말고 가장 그럴듯한 쪽으로 진행하고, 무엇을 가정했는지 끝에 적으세요.',
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
const 답 = await ctx.ask물음(물음, 고를것);
|
|
1327
|
+
if (답 === null || 답 === undefined || String(답).trim() === '') {
|
|
1328
|
+
return { content: '사람이 답하지 않았습니다. 되묻지 말고 스스로 판단해 이어가세요.' };
|
|
1329
|
+
}
|
|
1330
|
+
return { content: `사람의 답: ${String(답)}` };
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
|
|
1086
1334
|
Recall: {
|
|
1087
1335
|
schema: {
|
|
1088
1336
|
name: 'Recall',
|
|
@@ -1206,7 +1454,10 @@ export const TOOLS = {
|
|
|
1206
1454
|
*
|
|
1207
1455
|
* 인자 설명도 같이 줄인다. 괄호로 붙인 보충(`(한 개일 때)`)이 먼저 떨어진다.
|
|
1208
1456
|
*/
|
|
1209
|
-
|
|
1457
|
+
// from·to 는 Move 만 쓴다. 옮기는 도구에서 "어디서 → 어디로" 는 이름만으로 안다.
|
|
1458
|
+
const 뻔한인자 = new Set([
|
|
1459
|
+
'file_path', 'content', 'pattern', 'path', 'command', 'text', 'name', 'question', 'from', 'to',
|
|
1460
|
+
]);
|
|
1210
1461
|
|
|
1211
1462
|
export function 설명줄이기(schema, 한도) {
|
|
1212
1463
|
if (!Number.isFinite(한도)) return schema;
|
|
@@ -1266,14 +1517,18 @@ export function 설명줄이기(schema, 한도) {
|
|
|
1266
1517
|
}
|
|
1267
1518
|
|
|
1268
1519
|
/**
|
|
1269
|
-
* 도구 설명을
|
|
1520
|
+
* 도구 설명을 **모델에게 주는 말**에 맞춘다 (i18n 의 지시말).
|
|
1521
|
+
*
|
|
1522
|
+
* 화면 말이 아니라 지시말을 본다. 도구 설명은 사람이 아니라 모델이 읽는
|
|
1523
|
+
* 글이라, '한국어 화면 + 영어 지시' 를 고른 사람에게는 여기도 영어로
|
|
1524
|
+
* 가야 한다 — 그래야 아끼려던 토큰이 실제로 아껴진다.
|
|
1270
1525
|
*
|
|
1271
|
-
* 표에 없는 도구·인자는 한글 설명이 그대로
|
|
1526
|
+
* 표에 없는 도구·인자는 한글 설명이 그대로 나간다.
|
|
1272
1527
|
* 빈 설명을 내보내지 않는다. 설명 없는 도구는 모델이 언제 쓰는지 모른 채로
|
|
1273
1528
|
* 목록에만 서 있게 되는데, 그건 없는 것보다 나쁘다.
|
|
1274
1529
|
*/
|
|
1275
1530
|
function 영어설명(schema, 이름) {
|
|
1276
|
-
if (
|
|
1531
|
+
if (지시말() !== 'en') return schema;
|
|
1277
1532
|
const 것 = 도구설명EN[이름];
|
|
1278
1533
|
if (!것) return schema;
|
|
1279
1534
|
|
package/src/ui/inputbox.js
CHANGED
|
@@ -522,21 +522,38 @@ export class InputBox {
|
|
|
522
522
|
const 폭 = 칸수 ?? 80;
|
|
523
523
|
|
|
524
524
|
/*
|
|
525
|
-
*
|
|
525
|
+
* ── 크기가 바뀌었을 때 ──────────────────────────────────────────────
|
|
526
526
|
*
|
|
527
|
-
* 터미널이
|
|
528
|
-
* 커서를 올리면 엉뚱한 줄에 덮어쓴다 — 대화가 지워지는
|
|
527
|
+
* 폭이 바뀌면 터미널이 **이미 제 손으로 줄을 다시 접어 놨다.** 옛 폭에서
|
|
528
|
+
* 센 값으로 커서를 올리면 엉뚱한 줄에 덮어쓴다 — 대화가 지워지는 길이다.
|
|
529
529
|
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
530
|
+
* 그래서 예전에는 크기가 조금이라도 다르면 그냥 다 버리고 지금 자리에서
|
|
531
|
+
* 새로 그렸다. "낡은 상자가 한 번 위에 남지만 스크롤 자국일 뿐" 이라는
|
|
532
|
+
* 셈이었는데, **한 번이 아니었다.**
|
|
533
|
+
*
|
|
534
|
+
* 창을 마우스로 끌면 터미널은 한 칸마다 크기를 새로 알려 준다. 끌면서
|
|
535
|
+
* 쉰 번이 오면 상자를 쉰 벌 찍고 하나도 안 지운다 — 사무실이 화면을
|
|
536
|
+
* 다다다 타고 내려가는 그 현상이 이것이다. 세로로 늘릴 때 특히 심하다.
|
|
537
|
+
* 28줄을 넘는 순간 사무실 열두 줄이 통째로 더 붙기 때문이다.
|
|
538
|
+
*
|
|
539
|
+
* 나눠서 다룬다. **접힘이 그대로면 들고 있던 수도 그대로 맞다.**
|
|
540
|
+
*
|
|
541
|
+
* 높이만 바뀌었다 — 줄 접힘은 폭이 정한다. 폭이 그대로면 접힘도 그대로라
|
|
542
|
+
* 그린줄·커서위 가 아직 맞다. 평소처럼 제대로 지운다.
|
|
543
|
+
* 폭이 바뀌었다 — 옛 수는 다 틀어졌다. 대신 커서 아래를 통째로 쓸어낸다.
|
|
544
|
+
* 상자는 늘 맨 아래에 있어서 그 아래에는 아무것도 없다.
|
|
545
|
+
* 위로는 안 올라가니 대화를 건드릴 일이 없다.
|
|
534
546
|
*/
|
|
535
547
|
const 높이 = process.stdout.rows ?? null;
|
|
536
548
|
if (this.그린폭 !== 폭 || this.그린높이 !== 높이) {
|
|
537
|
-
this
|
|
538
|
-
|
|
539
|
-
|
|
549
|
+
if (this.그린폭 === 폭) {
|
|
550
|
+
this.지우기();
|
|
551
|
+
} else {
|
|
552
|
+
if (this.그린줄) process.stdout.write('\r\x1b[J');
|
|
553
|
+
this.앞줄들 = null;
|
|
554
|
+
this.그린줄 = 0;
|
|
555
|
+
this.커서위 = 0;
|
|
556
|
+
}
|
|
540
557
|
this.그린폭 = 폭;
|
|
541
558
|
this.그린높이 = 높이;
|
|
542
559
|
}
|
package/src/ui/status.js
CHANGED
|
@@ -10,6 +10,9 @@ import { COMPACT_AT, FOLD_AT } from '../agent/compact.js';
|
|
|
10
10
|
import { 말, 언어 } from '../i18n/index.js';
|
|
11
11
|
import { 표시 as 승인표시, 고르기 as 승인고르기 } from './approve.js';
|
|
12
12
|
|
|
13
|
+
// 저장소 주소. package.json 의 repository 와 같아야 한다 — 검사가 지킨다.
|
|
14
|
+
export const GITHUB = 'https://github.com/jysvai/deel-local-cli';
|
|
15
|
+
|
|
13
16
|
// 한 조각씩 따로 만든다. 좁은 화면에서는 뒤에서부터 떨군다.
|
|
14
17
|
// 순서 = 중요도 순. 앞쪽이 끝까지 살아남는다.
|
|
15
18
|
export const SEGMENTS = {
|
|
@@ -479,5 +482,22 @@ export function headerLines(session, found, 상자쓰나 = true) {
|
|
|
479
482
|
if (found.skills.length || found.commands.length) {
|
|
480
483
|
lines.push(`${c.gray(자리(말('head.thisPC')))}${c.white(말('head.skills', { n: found.skills.length }))}${c.gray(' · ')}${c.white(말('head.commands', { n: found.commands.length }))}${c.gray(' · ')}${c.white(말('head.plugins', { n: found.plugins.length }))}`);
|
|
481
484
|
}
|
|
485
|
+
/*
|
|
486
|
+
* 별 부탁 한 줄.
|
|
487
|
+
*
|
|
488
|
+
* 맨 아래에 둔다. 위쪽은 **어디로 소스가 나가는지**를 읽는 자리라, 그
|
|
489
|
+
* 사이에 부탁을 끼우면 정작 읽어야 할 줄이 묻힌다.
|
|
490
|
+
*
|
|
491
|
+
* 회색 한 줄로만 둔다. 켤 때마다 나오는 것이라, 눈에 띄게 만들면 열 번째
|
|
492
|
+
* 켤 때부터는 부탁이 아니라 방해가 된다.
|
|
493
|
+
*
|
|
494
|
+
* 사람이 보고 있을 때만 낸다. 상자를 쓰나(상자쓰나)가 아니라 화면이 터미널인가로
|
|
495
|
+
* 가른다 — `--no-tui` 로 쓰는 사람도 사람이지만, 파이프·CI 로 흘러 들어가는
|
|
496
|
+
* 기록에 부탁 줄이 섞이면 그건 그냥 잡음이다.
|
|
497
|
+
*/
|
|
498
|
+
if (process.stdout.isTTY) {
|
|
499
|
+
lines.push('');
|
|
500
|
+
lines.push(`${빈자리}${c.gray(말('head.star'))} ${c.cyan(GITHUB)}`);
|
|
501
|
+
}
|
|
482
502
|
return lines;
|
|
483
503
|
}
|