deel-local-cli 1.20.13 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.ko.md +97 -78
  2. package/README.md +120 -81
  3. package/bin/deel.js +398 -32
  4. package/package.json +2 -1
  5. package/src/acp/jsonrpc.js +87 -7
  6. package/src/acp/map.js +23 -3
  7. package/src/acp/serve.js +333 -25
  8. package/src/agent/agents.js +110 -12
  9. package/src/agent/askcheck.js +50 -6
  10. package/src/agent/asks.js +59 -5
  11. package/src/agent/budget.js +9 -2
  12. package/src/agent/card.js +8 -2
  13. package/src/agent/commit.js +168 -16
  14. package/src/agent/compact.js +93 -21
  15. package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
  16. package/src/agent/effort.js +85 -15
  17. package/src/agent/evidence.js +63 -7
  18. package/src/agent/evolve.js +132 -24
  19. package/src/agent/grade.js +12 -3
  20. package/src/agent/loop.js +408 -81
  21. package/src/agent/memory.js +176 -29
  22. package/src/agent/mention.js +22 -5
  23. package/src/agent/models.js +31 -7
  24. package/src/agent/modes.js +25 -10
  25. package/src/agent/outschema.js +381 -56
  26. package/src/agent/pins.js +33 -2
  27. package/src/agent/project.js +73 -8
  28. package/src/agent/recall.js +40 -9
  29. package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
  30. package/src/agent/review.js +143 -13
  31. package/src/agent/route.js +253 -26
  32. package/src/agent/salvage.js +2 -1
  33. package/src/agent/session.js +457 -31
  34. package/src/agent/store.js +232 -22
  35. package/src/agent/threads.js +52 -3
  36. package/src/backend/adapter.js +508 -85
  37. package/src/backend/azure.js +12 -1
  38. package/src/backend/cachemark.js +31 -5
  39. package/src/backend/clientcert.js +23 -1
  40. package/src/backend/ctxsize.js +56 -9
  41. package/src/backend/detect.js +252 -19
  42. package/src/backend/http.js +383 -42
  43. package/src/backend/learn.js +101 -11
  44. package/src/backend/mcp.js +333 -24
  45. package/src/backend/price.js +61 -13
  46. package/src/backend/probe.js +188 -36
  47. package/src/backend/proxy.js +108 -14
  48. package/src/backend/quota.js +226 -30
  49. package/src/backend/retry.js +46 -3
  50. package/src/backend/scan.js +47 -10
  51. package/src/backend/scanui.js +57 -7
  52. package/src/backend/toolfit.js +204 -22
  53. package/src/backend/vision.js +14 -1
  54. package/src/backend/wire.js +58 -10
  55. package/src/cmdnames.js +16 -0
  56. package/src/commands/common.js +70 -0
  57. package/src/commands/extend.js +299 -0
  58. package/src/commands/model.js +851 -0
  59. package/src/commands/view.js +328 -0
  60. package/src/commands/work.js +823 -0
  61. package/src/commands.js +193 -2082
  62. package/src/completion.js +45 -3
  63. package/src/config.js +454 -41
  64. package/src/configexplain.js +127 -15
  65. package/src/doctor.js +164 -27
  66. package/src/i18n/en.js +22 -0
  67. package/src/i18n/index.js +31 -1
  68. package/src/i18n/ja.js +25 -1
  69. package/src/i18n/ko.js +25 -2
  70. package/src/i18n/zh.js +25 -1
  71. package/src/lsp/client.js +111 -9
  72. package/src/lsp/servers.js +86 -22
  73. package/src/oneshot.js +325 -65
  74. package/src/pack/sbom.js +28 -5
  75. package/src/pack/selfpack.js +54 -5
  76. package/src/pack/sheet.en.js +24 -3
  77. package/src/pack/tar.js +45 -8
  78. package/src/pack/zip.js +103 -8
  79. package/src/plugins/manage.js +165 -14
  80. package/src/preview/serve.js +239 -23
  81. package/src/providers/bedrock.js +30 -5
  82. package/src/providers/gemini.js +8 -0
  83. package/src/providers/index.js +17 -1
  84. package/src/repl.js +254 -61
  85. package/src/report.js +7 -2
  86. package/src/reset.js +151 -25
  87. package/src/safety/audit.js +108 -8
  88. package/src/safety/authcmd.js +75 -5
  89. package/src/safety/guard.js +531 -15
  90. package/src/safety/hooks.js +172 -20
  91. package/src/safety/keystore.js +178 -18
  92. package/src/safety/network.js +209 -6
  93. package/src/safety/policy.js +377 -36
  94. package/src/safety/runmode.js +9 -2
  95. package/src/safety/secrets.js +243 -17
  96. package/src/safety/shellenv.js +64 -3
  97. package/src/safety/trust.js +298 -32
  98. package/src/safety/undo.js +409 -17
  99. package/src/setup.js +175 -17
  100. package/src/skills/discover.js +122 -19
  101. package/src/stats.js +57 -16
  102. package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
  103. package/src/tools/clipboard.js +53 -20
  104. package/src/tools/convert.js +118 -37
  105. package/src/tools/desc.en.js +48 -5
  106. package/src/tools/doc2md.js +36 -4
  107. package/src/tools/docs.js +94 -18
  108. package/src/tools/edit-match.js +212 -13
  109. package/src/tools/encoding.js +525 -28
  110. package/src/tools/excel-com.js +63 -15
  111. package/src/tools/excel.js +12 -7
  112. package/src/tools/fastgrep.js +501 -38
  113. package/src/tools/fig.js +21 -4
  114. package/src/tools/fsutil.js +235 -21
  115. package/src/tools/hwpxwrite.js +80 -19
  116. package/src/tools/ignore.js +55 -7
  117. package/src/tools/index.js +1083 -148
  118. package/src/tools/jobs.js +328 -50
  119. package/src/tools/kiwi.js +50 -5
  120. package/src/tools/lsp.js +164 -32
  121. package/src/tools/outline.js +52 -12
  122. package/src/tools/pdf.js +367 -59
  123. package/src/tools/shell.js +12 -4
  124. package/src/tools/spawn.js +203 -18
  125. package/src/tools/todo.js +73 -4
  126. package/src/tools/verify.js +328 -45
  127. package/src/tools/webfetch.js +194 -25
  128. package/src/tools/xlsx.js +232 -31
  129. package/src/ui/ansi.js +209 -15
  130. package/src/ui/banner.js +2 -1
  131. package/src/ui/complete.js +46 -4
  132. package/src/ui/diff.js +4 -2
  133. package/src/ui/export.js +91 -17
  134. package/src/ui/inputbox.js +58 -18
  135. package/src/ui/intro.js +9 -2
  136. package/src/ui/level.js +32 -7
  137. package/src/ui/md.js +101 -15
  138. package/src/ui/motion.js +5 -2
  139. package/src/ui/notify.js +7 -1
  140. package/src/ui/office.js +37 -5
  141. package/src/ui/pastechip.js +3 -2
  142. package/src/ui/pick.js +85 -12
  143. package/src/ui/prompt.js +176 -13
  144. package/src/ui/screen.js +2 -1
  145. package/src/ui/spinner.js +6 -1
  146. package/src/ui/status.js +80 -12
  147. package/src/ui/wrap.js +23 -37
  148. /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
  149. /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
  150. /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
  151. /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
  152. /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
  153. /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
  154. /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
  155. /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/ui/prompt.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // 입력 받기 — 한 줄, 비밀번호(가림), 목록 선택, 예/아니오.
2
- import { c, say } from './ansi.js';
2
+ import { c, say, width } from './ansi.js';
3
3
 
4
4
  function raw() {
5
5
  return process.stdin.isTTY ? process.stdin.setRawMode.bind(process.stdin) : null;
@@ -24,18 +24,87 @@ function readKeys(onKey) {
24
24
  });
25
25
  }
26
26
 
27
+ /*
28
+ * ── ESC 로 시작하는 순서는 **통째로** 뗀다 ──────────────────────────────
29
+ *
30
+ * 예전에는 ESC 한 글자만 버리고 뒤따르는 것은 글자로 담았다. 화살표 하나가 `[D`, Delete 가
31
+ * `[3~`, 붙여넣기 표시가 `[200~` … `[201~` 로 남았다. `deel setup` 의 API 키 물음은 ● 로 가려
32
+ * 있어서 사람은 그걸 볼 수가 없다 — 오타를 고치려고 왼쪽 화살표를 한 번 누르면 열쇠가 조용히
33
+ * 틀어져 저장되고, 붙을 때 401 이 난다. 무엇이 틀렸는지는 아무 데도 안 남는다.
34
+ *
35
+ * 이 물음은 커서를 옮기는 편집기가 아니다(끝에 붙이고 끝에서 지우기만 한다). 그래서 화살표·Home·
36
+ * Delete 는 **아무 일도 안 하는 것**이 맞고, 붙여넣기 표시는 벗기고 알맹이만 받는다.
37
+ *
38
+ * 돌려주는 값: 순서의 마지막 글자 자리. 순서가 이 토막 끝에서 끊겼으면 -1 — 느린 원격 터미널은
39
+ * `ESC [` 와 `3~` 를 두 토막으로 보낸다. 그때는 남겨 뒀다가 다음 토막 앞에 붙여 다시 본다.
40
+ */
41
+ function 순서끝(글자들, i) {
42
+ const 둘째 = 글자들[i + 1];
43
+ if (둘째 === undefined) return -1;
44
+ if (둘째 === '[') {
45
+ // CSI — 매개 글자(0x30–0x3F)·사이 글자(0x20–0x2F) 뒤에 마침 글자(0x40–0x7E) 하나.
46
+ for (let j = i + 2; j < 글자들.length; j++) {
47
+ const k = 글자들[j].charCodeAt(0);
48
+ if (k >= 0x40 && k <= 0x7e) return j;
49
+ // 순서에 올 수 없는 글자(엔터·한글 …)가 끼었다. 거기서 끊고 그 글자는 제대로 읽게 둔다.
50
+ if (k < 0x20 || k > 0x3f) return j - 1;
51
+ }
52
+ return -1;
53
+ }
54
+ // SS3 — 일부 터미널의 화살표·F1~F4 (ESC O A).
55
+ //
56
+ // 셋째 글자를 무엇이든 끝 글자로 먹었다. ESC 를 누르고 O 를 치고 엔터를 치면 엔터가 순서의 끝으로
57
+ // 사라져 물음이 안 끝났다. SS3 의 끝 글자는 0x40–0x7E 뿐이다 — 아니면 홑 ESC(Alt+O)로 본다.
58
+ if (둘째 === 'O') {
59
+ const 셋째 = 글자들[i + 2];
60
+ if (셋째 === undefined) return -1;
61
+ const k = 셋째.charCodeAt(0);
62
+ return k >= 0x40 && k <= 0x7e ? i + 2 : i;
63
+ }
64
+ // 홑 ESC (ESC 키 · Alt+글자). 여태처럼 ESC 만 떼고 뒤 글자는 글자로 둔다.
65
+ return i;
66
+ }
67
+
68
+ /*
69
+ * ── 앞 물음이 덩이 끝의 \r 에서 끝났나 (Gemini 화면4) ──────────────────────
70
+ *
71
+ * CRLF 로 적은 답 파일을 파이프로 흘리면 `ans1\r` | `\nans2\r\n` 처럼 \r 과 \n 이 **두 덩이에
72
+ * 갈려** 올 수 있다. 첫 물음은 \r 에서 끝나고, 그 \n 은 다음 물음이 받아 **엔터로** 읽었다 —
73
+ * 빈 답이 끼고 뒤의 답이 한 칸씩 밀린다(주소 물음에 모델 이름이 들어간다). 한 덩이 안의 CRLF
74
+ * 는 이미 한 번으로 친다(아래). 덩이 사이는 물음이 바뀌므로 이 모듈이 기억한다.
75
+ *
76
+ * 다음 덩이의 **맨 앞 \n 하나만** 삼키고, **파이프일 때만** 기억한다. 터미널(raw)은 엔터를 늘 덩이 끝
77
+ * \r 하나로 보내 CRLF 가 갈릴 일이 없다 — 거기서도 기억하면 엔터 뒤 첫 키로 친 Ctrl+J(\n)가 먹혀
78
+ * 물음이 안 끝났다(Gemini 화면5).
79
+ */
80
+ let 덩이끝CR = false;
81
+
27
82
  // 한 줄 입력. mask=true 면 ● 로 가린다.
28
83
  export function ask(label, { mask = false, def = '' } = {}) {
29
84
  const prefix = ` ${c.gray('›')} ${label} `;
30
85
  process.stdout.write(prefix + (def ? c.gray(`[${def}] `) : ''));
31
86
  let buf = '';
87
+ // 앞 토막 끝에서 끊긴 ESC 순서. 다음 토막 앞에 붙여 다시 본다.
88
+ let 꼬리 = '';
32
89
  return readKeys((ch, done) => {
33
90
  // 글자 단위로 훑되 자리를 기억한다. 엔터에서 멈출 때 '남은 것' 을 알아야 해서다.
34
- const 글자들 = [...ch];
35
- for (let i = 0; i < 글자들.length; i++) {
91
+ const 글자들 = [...(꼬리 + ch)];
92
+ 꼬리 = '';
93
+ let 시작 = 0;
94
+ if (덩이끝CR) {
95
+ 덩이끝CR = false;
96
+ if (글자들[0] === '\n') 시작 = 1;
97
+ }
98
+ for (let i = 시작; i < 글자들.length; i++) {
36
99
  const ch1 = 글자들[i];
37
100
  const code = ch1.charCodeAt(0);
38
101
  if (code === 3) { say(''); process.exit(130); } // Ctrl+C
102
+ if (code === 27) { // ESC 순서
103
+ const 끝 = 순서끝(글자들, i);
104
+ if (끝 === -1) { 꼬리 = 글자들.slice(i).join(''); break; }
105
+ i = 끝;
106
+ continue;
107
+ }
39
108
  if (code === 13 || code === 10) { // Enter
40
109
  say('');
41
110
  // 한 덩어리에 여러 줄이 실려 올 수 있다 — 붙여넣기, 파이프 입력, 느린 터미널.
@@ -45,14 +114,32 @@ export function ask(label, { mask = false, def = '' } = {}) {
45
114
  // 그래서 되돌려 놓는다 — 다음에 읽는 쪽이 받아 간다.
46
115
  let 다음 = i + 1;
47
116
  if (code === 13 && 글자들[다음] === '\n') 다음++; // CRLF 는 한 번으로 친다
117
+ else if (code === 13 && 다음 === 글자들.length && !process.stdin.isTTY) 덩이끝CR = true; // \n 이 다음 덩이로 갈렸을 수 있다 (덩이끝CR 머리말)
48
118
  const 남은것 = 글자들.slice(다음).join('');
119
+ /*
120
+ * **먼저 손을 떼고** 되돌린다.
121
+ *
122
+ * 거꾸로 했었다 — 되돌리고 나서 done 으로 손을 뗐다. 그런데 흐르는 중인 입력에
123
+ * unshift 하면 그 자리에서 곧바로 'data' 가 다시 난다. 그걸 받는 것은 **아직 붙어
124
+ * 있는 이 물음**이라, `first\nsecond\n` 이 한 덩어리로 오면 첫 물음이 「firstsecond」
125
+ * 를 받고 둘째 물음은 영영 답을 못 받았다. `echo … | deel setup` 이 그 길이다.
126
+ * 손을 떼면(멈춤) 되돌린 것은 버퍼에 가만히 있다가 다음 물음이 받아 간다.
127
+ */
128
+ const 끝남 = done(buf.length ? buf : def);
49
129
  if (남은것) { try { process.stdin.unshift(남은것); } catch { /* 못 돌려놔도 이 줄은 살린다 */ } }
50
- return done(buf.length ? buf : def);
130
+ return 끝남;
51
131
  }
52
132
  if (code === 127 || code === 8) { // Backspace
53
133
  if (buf.length) {
54
- buf = buf.slice(0, -1);
55
- process.stdout.write('\b \b');
134
+ /*
135
+ * 코드 단위가 아니라 **글자 하나**를 지운다. 그림글자는 UTF-16 두 토막이라
136
+ * 한 토막만 떼면 반쪽 글자가 열쇠·이름에 남는다 — 화면에는 안 보이고 값만 깨진다.
137
+ * 지운 칸 수도 글자 폭만큼이다 — 한글 하나를 한 칸만 지우면 반쪽이 화면에 남는다.
138
+ */
139
+ const 뗀것 = [...buf].pop();
140
+ buf = buf.slice(0, -뗀것.length);
141
+ const 칸 = mask ? 1 : Math.max(1, width(뗀것));
142
+ process.stdout.write('\b'.repeat(칸) + ' '.repeat(칸) + '\b'.repeat(칸));
56
143
  }
57
144
  continue;
58
145
  }
@@ -63,6 +150,21 @@ export function ask(label, { mask = false, def = '' } = {}) {
63
150
  });
64
151
  }
65
152
 
153
+ /*
154
+ * 지금 암호를 받는 중인 readline 들.
155
+ *
156
+ * 암호는 **일하는 도중에** 묻는다(엑셀 도구). 그 동안 repl 의 입력 상자는 rl.line 을 「미리
157
+ * 쳐 둔 글」 로 그리고, Enter 친 줄을 「예약된 다음 말」 로 다룬다 — 둘 다 암호를 글자 그대로
158
+ * 다룬다는 뜻이다. 가로챌 것은 되비추기(_writeToOutput)만이 아니었다. 그래서 가리는 중인지를
159
+ * 밖에서 물어볼 수 있게 둔다(가림중).
160
+ */
161
+ const 가리는rl = new WeakSet();
162
+
163
+ /** 이 readline 이 지금 암호를 받는 중인가. repl 이 입력 상자에 그 글을 그릴지 정할 때 본다. */
164
+ export function 가림중(rl) {
165
+ return !!rl && typeof rl === 'object' && 가리는rl.has(rl);
166
+ }
167
+
66
168
  /**
67
169
  * REPL 안에서 암호를 받는다. 화면에 안 찍히게.
68
170
  *
@@ -80,23 +182,61 @@ export async function askHidden(rl, label, nextLine) {
80
182
  const 원래 = typeof rl?._writeToOutput === 'function' ? rl._writeToOutput : null;
81
183
  if (원래) {
82
184
  rl._writeToOutput = function (s) {
83
- // 줄바꿈·지우기 같은 제어는 그대로 두고, 글자만 가린다.
84
- if (/^[\r\n]+$/.test(s) || s.startsWith('\x1b')) return 원래.call(this, s);
85
- this.output.write(c.gray('●'.repeat([...s].length)));
185
+ /*
186
+ * 줄바꿈·제어 순서는 그대로 두고, 글자만 가린다 — **한 덩이 안에서** 갈라서.
187
+ *
188
+ * 첫 글자가 ESC 면 통째로 제어로 보고 흘렸었다. 그런데 readline 은 지우개·화살표에서 줄을
189
+ * 새로 그리며 「프롬프트 + 지금까지 친 글」 을 한 번에 넘긴다. 프롬프트에 색이 입혀져 있으면
190
+ * 그 첫 글자가 ESC 라, 친 암호가 글자 그대로 화면에 찍혔다(Gemini 화면4). 맨 프롬프트면
191
+ * 거꾸로 프롬프트까지 ● 로 덮였다.
192
+ *
193
+ * 그래서 앞의 프롬프트는 가리지 않고 떼어 두고, 나머지에서 제어 순서·줄바꿈만 살리고 글자는
194
+ * 전부 ● 로 바꾼다.
195
+ */
196
+ const 글 = String(s);
197
+ const 프롬프트 = typeof this.getPrompt === 'function' ? String(this.getPrompt() ?? '') : '';
198
+ const 앞 = 프롬프트 && 글.startsWith(프롬프트) ? 프롬프트 : '';
199
+ const 가림 = 글.slice(앞.length).replace(/(\x1b\[[0-?]*[ -/]*[@-~])|([\r\n]+)|([^\x1b\r\n]+)/g,
200
+ (통, 순서, 줄, 글자) => (글자 ? c.gray('●'.repeat([...글자].length)) : 통));
201
+ return 원래.call(this, 앞 + 가림);
86
202
  };
87
203
  } else {
88
204
  process.stdout.write(c.gray('(입력해도 화면에 안 보입니다) '));
89
205
  }
90
206
 
207
+ /*
208
+ * ── 입력 이력에 안 남긴다 ────────────────────────────────────────────
209
+ *
210
+ * readline 은 Enter 친 줄을 이력(historySize)에 쌓는다. 암호도 한 줄이라 그대로 쌓였다 —
211
+ * 암호를 넣고 나서 다음 입력칸에서 위 화살표를 한 번 누르면 암호가 글자 그대로 되살아났다.
212
+ * 어깨너머로 보이는 것은 물론이고, 그 줄을 모르고 Enter 치면 모델에게 간다.
213
+ *
214
+ * 받은 줄을 이력에서 **도로 뺀다.** 받는 동안 이력을 끄는 길(historySize = 0)은 못 쓴다 — Node 의
215
+ * readline 은 historySize 를 읽기만 하게 막아 두었다(대입하면 던진다). readline 은 줄을 이력
216
+ * **맨 앞**에 끼운 뒤에 'line' 을 쏘고, 우리는 그 줄을 받은 바로 그 자리에서 빼므로 그 사이에
217
+ * 다른 줄이 끼어들 틈이 없다.
218
+ */
219
+ const 이력 = Array.isArray(rl?.history) ? rl.history : null;
220
+ const 표시할수있나 = !!rl && typeof rl === 'object';
221
+ if (표시할수있나) 가리는rl.add(rl);
222
+
91
223
  try {
92
224
  const a = await nextLine();
225
+ if (이력 && a !== null && 이력[0] === a) 이력.shift();
93
226
  return a === null ? null : a.trim();
94
227
  } finally {
95
228
  if (원래) rl._writeToOutput = 원래;
229
+ if (표시할수있나) 가리는rl.delete(rl);
96
230
  say('');
97
231
  }
98
232
  }
99
233
 
234
+ // 전각 숫자(0–9)를 보통 숫자로. 일본어·중국어 입력기는 번호를 이렇게 낸다.
235
+ const 숫자펴기 = (s) => String(s ?? '').replace(/[0-9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0xfee0));
236
+
237
+ // 없는 번호를 몇 번까지 되묻나. 끝이 있어야 같은 틀린 답이 계속 오는 자리에서 안 멈춘다.
238
+ const 되묻기 = 2;
239
+
100
240
  // 목록에서 번호로 고르기.
101
241
  // REPL 안에서는 readline 이 stdin 을 쥐고 있으므로 ask 를 갈아끼워 쓴다.
102
242
  export async function pick(label, items, { def = 0, ask: askFn = ask } = {}) {
@@ -109,10 +249,33 @@ export async function pick(label, items, { def = 0, ask: askFn = ask } = {}) {
109
249
  say(` ${c.cyan(String(i + 1).padStart(2))} ${line}${note}${tag}`);
110
250
  });
111
251
  say('');
112
- const raw = await askFn('번호', { def: String(def + 1) });
113
- const n = parseInt(raw, 10);
114
- if (!Number.isFinite(n) || n < 1 || n > items.length) return def;
115
- return n - 1;
252
+ /*
253
+ * ── 없는 번호는 말없이 기본값으로 가지 않는다 ──────────────────────────
254
+ *
255
+ * `9` 나 `abc` 를 치면 아무 말 없이 기본값을 골랐다. 사람은 친 번호가 먹은 줄 알고 넘어가고,
256
+ * 붙는 것은 고른 적 없는 모델·리전이다. 화면 어디에도 「그건 없는 번호」 라는 말이 없다.
257
+ *
258
+ * `되묻기 = 2` 라 **두 번까지 더 묻는다** — 처음까지 세면 사람은 세 번 친다. 여기를
259
+ * 부르는 자리(모델 40개·리전·제공자 고르기)는 목록이 길어 손이 미끄러지기 쉽고, 잘못
260
+ * 고르면 고른 적 없는 모델에 붙는다. 그래서 기회를 한 번이 아니라 두 번 준다.
261
+ * 그래도 틀리면 기본값으로 가되 **그렇게 했다고 적는다.** 되묻는 횟수에
262
+ * 끝을 둔다 — 답을 파이프로 흘려 넣는 자리에서 같은 틀린 답이 계속 오면 영영 안 끝난다.
263
+ * 빈 답(그냥 Enter)은 틀린 답이 아니라 「기본으로」 라는 답이라 되묻지 않는다.
264
+ */
265
+ for (let 번째 = 0; ; 번째++) {
266
+ const 받은것 = await askFn('번호', { def: String(def + 1) });
267
+ const 글 = 숫자펴기(받은것).trim();
268
+ const n = parseInt(글, 10);
269
+ if (Number.isFinite(n) && n >= 1 && n <= items.length) return n - 1;
270
+ if (!글) return def;
271
+ if (번째 >= 되묻기) {
272
+ const 기본 = items[def];
273
+ const 이름 = typeof 기본 === 'string' ? 기본 : 기본?.label ?? '';
274
+ say(` ${c.yellow('!')} ${c.gray(`목록에 없는 번호라 기본값 ${def + 1}번${이름 ? `(${이름})` : ''}으로 둡니다.`)}`);
275
+ return def;
276
+ }
277
+ say(` ${c.yellow('!')} ${c.gray(`「${글}」 은 목록에 없습니다 — 1~${items.length} 사이 번호를 넣어 주세요.`)}`);
278
+ }
116
279
  }
117
280
 
118
281
  export async function confirm(label, def = true) {
package/src/ui/screen.js CHANGED
@@ -371,7 +371,8 @@ export class BoxScreen extends LineScreen {
371
371
  // '생각 중… 1,234자' 에서 숫자만 떼어 곁정보로 붙인다. 진짜로 늘어나는
372
372
  // 숫자가 하나 있어야 '살아 있다' 가 표시가 아니라 사실이 된다.
373
373
  const m = /([\d,]+)자/.exec(String(s ?? ''));
374
- this.상자.일바꿈('생각', m ? `생각 ${m[1]}자` : null);
374
+ // 「생각」 은 상자 왼쪽 문구가 이미 말한다 — 곁에 또 붙이면 한 줄에 두 번 나온다.
375
+ this.상자.일바꿈('생각', m ? `${m[1]}자` : null);
375
376
  }
376
377
 
377
378
  /**
package/src/ui/spinner.js CHANGED
@@ -1,4 +1,9 @@
1
- // 진행 중 표시. TTY가 아니면 조용히 한 줄만 남긴다.
1
+ // 진행 중 표시. TTY가 아니면 돌리지 않고 줄로만 남긴다.
2
+ //
3
+ // 「한 줄만」 이 아니다. 시작할 때 무엇을 하는 중인지 한 줄, 끝맺음 글을 받으면 그때 한 줄 —
4
+ // 부르는 열아홉 자리 중 열넷이 끝맺음 글을 준다(찾음 개수·연결됨·못 물어봤습니다…). 시작 줄을
5
+ // 빼면 로그에 결과만 남아 무엇을 하다 그리 됐는지가 사라지고, 끝맺음 줄을 빼면 그 열넷이
6
+ // 통째로 말을 잃는다. 끝맺음 글을 안 주는 다섯 자리에서만 한 줄로 끝난다.
2
7
  import { c, cursor } from './ansi.js';
3
8
 
4
9
  const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
package/src/ui/status.js CHANGED
@@ -2,13 +2,13 @@
2
2
  //
3
3
  // 남의 패키지를 붙이지 않는다. 필요한 숫자는 전부 session 이 이미 갖고 있고,
4
4
  // 화면 그리기는 ansi.js 만 쓴다. 반입 심사에 새로 설명할 것이 늘지 않게 하려는 뜻이다.
5
- import { c, 눈금게이지, width, clip, cols, mark } from './ansi.js';
5
+ import { c, 눈금게이지, width, clip, cols, mark, 화면글거르기 } from './ansi.js';
6
6
  import { 마지막할당량, 할당량말, 아슬아슬한가 } from '../backend/quota.js';
7
7
  import { 세션요금, 돈셈, 돈말 } from '../backend/price.js';
8
8
  import { PROFILES } from '../agent/effort.js';
9
9
  import { get as workMode, canWrite, 보일이름 } from '../agent/modes.js';
10
10
  import { isLocalHost, isOffline } from '../safety/network.js';
11
- import { 프록시고르기, 프록시설정 } from '../backend/proxy.js';
11
+ import { 프록시고르기, 프록시설정, 프록시비켜가나 } from '../backend/proxy.js';
12
12
  import { COMPACT_AT, FOLD_AT } from '../agent/compact.js';
13
13
  import { 말, 언어 } from '../i18n/index.js';
14
14
  import { 표시 as 승인표시, 고르기 as 승인고르기 } from './approve.js';
@@ -155,8 +155,10 @@ export const SEGMENTS = {
155
155
  },
156
156
  },
157
157
 
158
- // 지금 무슨 일을 하는 중인가. 파일을 못 바꾸는 모드면 자물쇠를 같이 그린다 —
159
- // 계획만 세우는 중인지 실제로 고치는 중인지 한눈에 보여야 한다.
158
+ // 지금 무슨 일을 하는 중인가. 파일을 못 바꾸는 모드면 이름을 초록으로 바꾸고
159
+ // 뒤에 「읽기만」(영어는 read-only)을 덧붙인다 — 계획만 세우는 중인지 실제로
160
+ // 고치는 중인지 한눈에 보여야 한다. 자물쇠 같은 기호는 안 쓴다: 터미널마다
161
+ // 한 칸이 되기도 두 칸이 되기도 해서 상태줄 폭 셈이 어긋난다(ui/motion.js 머리말).
160
162
  work: {
161
163
  get desc() { return 말('seg.work'); },
162
164
  make: (s) => {
@@ -338,6 +340,37 @@ export const SEGMENT_GROUPS = [
338
340
  // 옛 이름. 조각 이름을 직접 넘기던 자리(검사·설정)가 그대로 돌게 남긴다.
339
341
  export const DEFAULT_SEGMENTS = SEGMENT_GROUPS.flat();
340
342
 
343
+ /*
344
+ * 자리가 모자랄 때 **접는 차례.** 한 칸이 조각 이름 **배열**이고, 그 칸에 적힌
345
+ * 이름은 다 같이 빠진다(아래 statusLine 의 뺄것). 덩이를 그대로 옮겨 적은 칸도
346
+ * 있고 덩이의 일부만 적은 칸도 있다 — 마지막 `['work','think']` 이 그렇다.
347
+ * 첫 이름 하나로 덩이를 가리키는 것이 아니다. 그랬다면 같이 빠져야 할 `verify`·
348
+ * `undoable` 이 남아, 한 덩이가 반만 접힌 줄(`✓3 ↩2` 만 남은 줄)이 나온다.
349
+ *
350
+ * 여태는 차례가 없었다 — 그냥 뒤에서부터 떨궜다. 그런데 덩이 순서는 '읽기
351
+ * 좋은 순서' 로 정한 것이지 '덜 급한 순서' 가 아니다. 그래서 40~60칸으로
352
+ * 좁힌 창(screen.js 가 받는 폭이다)에서 이렇게 됐다 —
353
+ *
354
+ * ▏⌂ 내폴더 · 모델 ▏ ▰▰▱▱▱┆▱▱┆▱ 18%
355
+ *
356
+ * 컨텍스트 게이지는 남고 `⏵⏵ 자동` 이 사라진다. 그 한 조각이 **내 파일이
357
+ * 안 물어보고 바뀌는가**를 말하는 자리다. 게이지는 지금 몰라도 되고 아래
358
+ * contextWarning 이 급해지면 따로 말해 주는데, 승인 방식은 여기 말고 적히는
359
+ * 데가 없다. 이 파일이 두 군데(seg.mode 머리말·아래 급 접기)에 「둘 중 하나를
360
+ * 접어야 하면 언제나 이쪽」 이라고 적어 두고, 정작 떨구는 고리만 그 반대였다.
361
+ *
362
+ * 그래서 급·게이지가 승인 방식보다 **먼저** 접힌다. 폴더·모델은 늘 남는다.
363
+ * `mode` 는 이 차례에 아예 없다 — 제 발로는 안 접힌다.
364
+ */
365
+ export const 접는차례 = [
366
+ ['tok', 'cost'],
367
+ ['edits', 'verify', 'undoable'],
368
+ ['ctx', 'grade'],
369
+ // 같은 덩이라도 여기까지다. 무슨 판인지·얼마나 생각하는지는 지금 몰라도
370
+ // 되고, 승인 방식은 이 줄 말고는 적히는 데가 없다.
371
+ ['work', 'think'],
372
+ ];
373
+
341
374
  /**
342
375
  * 이 급을 화면에 낼 값어치가 있나.
343
376
  *
@@ -376,8 +409,11 @@ function 참값(s) {
376
409
  /**
377
410
  * 경계 한 글자.
378
411
  *
379
- * 못 읽는 주소면 아무것도 안 그린다. 여기서 애매하면 **초록을 안 쓴다** —
380
- * 초록은 '안 나간다' 는 약속이고, 확인 못 한 것을 확인한 낯으로 내밀면 안 된다.
412
+ * 못 읽는 주소면 회색 `?` 를 그린다 — 자리를 비우지는 않는다. 비우면 한 칸이
413
+ * 사라져 옆 조각이 통째로 밀리고, 무엇보다 '경계를 아직 모른다' 는 말을 할
414
+ * 자리가 없어진다(창 크기를 모를 때 `32k?` 로 적는 것과 같은 규칙이다).
415
+ * 여기서 애매하면 **초록을 안 쓴다** — 초록은 '안 나간다' 는 약속이고,
416
+ * 확인 못 한 것을 확인한 낯으로 내밀면 안 된다.
381
417
  */
382
418
  function 경계표(s) {
383
419
  let 로컬 = null;
@@ -413,7 +449,11 @@ function base(p) {
413
449
  return parts[parts.length - 1] ?? '~';
414
450
  }
415
451
 
416
- // 12345 → 12.1k. 상태줄은 자리가 없다.
452
+ // 5678 → 5.5k · 12345 → 12k · 131072 → 128k. 상태줄은 자리가 없다.
453
+ //
454
+ // 10k 아래에서만 소수 한 자리를 붙인다. 넷째 자리가 붙는 순간(10240 부터)
455
+ // 소수까지 적으면 다섯 칸이 되고, 좁은 터미널에서 그 한 칸이 승인 방식을
456
+ // 밀어낸다. 그래서 12345 는 '12.1k' 가 아니라 '12k' 다.
417
457
  //
418
458
  // 1000 이 아니라 1024 로 나눈다. 컨텍스트 길이는 죄다 2의 거듭제곱이라
419
459
  // 1000 으로 나누면 131,072 가 '131k' 로 나온다 — 아무도 그렇게 안 부른다.
@@ -446,7 +486,11 @@ export function statusLine(session, { segments = null, max = cols() - 2 } = {})
446
486
  if (뺄것?.has(k)) return null;
447
487
  const seg = SEGMENTS[k];
448
488
  if (!seg) return null;
449
- return 짧게 && seg.short ? seg.short(session) : seg.make(session);
489
+ /*
490
+ * 조각에는 바깥 글자가 실린다 — 서버가 알려 준 모델 이름, 사람이 지은 폴더·갈래 이름. 상태줄은 say 를
491
+ * 안 지나고 입력 상자가 곧장 그리므로 여기서 뗀다(ansi.js 화면글거르기). 조각마다 떼야 폭을 뗀 뒤로 잰다.
492
+ */
493
+ return 화면글거르기(짧게 && seg.short ? seg.short(session) : seg.make(session), { 색남김: true });
450
494
  })
451
495
  .filter((x) => x != null && x !== '')
452
496
  .join(안쪽))
@@ -486,7 +530,23 @@ export function statusLine(session, { segments = null, max = cols() - 2 } = {})
486
530
  */
487
531
  if (!맞나(parts) && !segments) parts = 그리기(true, new Set(['grade']));
488
532
 
489
- // 여전히 모자라면 뒤에서부터 떨군다. 앞쪽(폴더·모델·컨텍스트)이 마지막까지 남는다.
533
+ /*
534
+ * 여전히 모자라면 덩이를 통째로 뺀다. **뺄 차례는 위 접는차례 가 정한다.**
535
+ *
536
+ * 여기가 뒤에서부터 떨구던 자리다. 덩이 순서상 `tok/cost` · `edits/…` 다음이
537
+ * `work/think/mode` 라, 컨텍스트 게이지가 멀쩡히 서 있는 채로 승인 방식이
538
+ * 먼저 사라졌다 — 바로 위 두 토막이 하지 말라고 적어 둔 바로 그 일이다.
539
+ */
540
+ if (!segments) {
541
+ const 뺄것 = new Set(['grade']);
542
+ for (const 묶음 of 접는차례) {
543
+ if (맞나(parts)) break;
544
+ for (const k of 묶음) 뺄것.add(k);
545
+ parts = 그리기(true, 뺄것);
546
+ }
547
+ }
548
+
549
+ // 조각 이름을 직접 준 자리(옛 방식)는 순서를 사람이 정한 것이라 뒤에서 떨군다.
490
550
  while (parts.length > 1 && !맞나(parts)) parts.pop();
491
551
 
492
552
  /*
@@ -571,8 +631,9 @@ export function headerLines(session, found, 상자쓰나 = true) {
571
631
  const 프록시 = 프록시고르기(conn.base);
572
632
  if (프록시) 목적지 += c.gray(` ${말('head.viaProxy', { 프록시: `${프록시.host}:${프록시.port}` })}`);
573
633
  // 적어 놨는데 못 쓰는 프록시(socks5 등)는 여기서 한 번은 말해야 한다 — 조용히 직접 가면
574
- // 사람은 프록시를 탄 줄 알고, 막히면 엉뚱한 데를 고친다.
575
- else if (프록시설정().탈) 목적지 += c.yellow(` ${말('head.proxyBad')}`);
634
+ // 사람은 프록시를 탄 줄 알고, 막히면 엉뚱한 데를 고친다. 어차피 안 거칠 주소(이 PC · NO_PROXY)면
635
+ // 말하지 않는다 — 로컬 모델이 안 붙는 까닭을 상관없는 프록시에서 찾게 된다 (2.0.0 6회차 RP1b).
636
+ else if (프록시설정().탈 && !프록시비켜가나(conn.base)) 목적지 += c.yellow(` ${말('head.proxyBad')}`);
576
637
  } catch { 목적지 = c.gray(String(conn.base)); }
577
638
 
578
639
  /*
@@ -634,7 +695,14 @@ export function headerLines(session, found, 상자쓰나 = true) {
634
695
  lines.push(`${빈자리}${c.gray(말('head.shiftTab'))}${c.gray(말('head.shiftTabHint'))}${c.gray(말('head.tabHint'))}`);
635
696
  lines.push(`${빈자리}${c.gray(말('head.newlineHint'))}`);
636
697
  }
637
- if (found.skills.length || found.commands.length) {
698
+ /*
699
+ * 플러그인도 조건에 든다.
700
+ *
701
+ * 이 줄이 플러그인 개수를 적는 **유일한 자리**인데, 설 조건에는 스킬·명령만
702
+ * 있었다. 그래서 스킬도 명령도 없이 플러그인만 깐 사람에게는 깔았다는 사실
703
+ * 자체가 화면 어디에도 안 나왔다 — 안 읽힌 것인지 없는 것인지 알 길이 없다.
704
+ */
705
+ if (found.skills.length || found.commands.length || found.plugins.length) {
638
706
  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 }))}`);
639
707
  }
640
708
  /*
package/src/ui/wrap.js CHANGED
@@ -2,40 +2,10 @@
2
2
  //
3
3
  // 전에는 전체화면(tui.js)에만 쓰던 것이라 거기 있었다. 전체화면을 걷어내면서
4
4
  // 여기로 옮긴다 — 입력 상자가 긴 입력을 접을 때 같은 것이 필요하다.
5
- import { width } from './ansi.js';
5
+ // 한덩이 — 보이는 한 덩이 떼기. clip 도 같은 것을 써야 해서 ansi.js 로 옮겼다(그 머리말).
6
+ import { width, 한덩이 } from './ansi.js';
6
7
 
7
- /*
8
- * i 자리에서 **눈에 보이는 한 덩이**를 통째로 떼어 온다.
9
- *
10
- * 글자를 s[i] 로 한 칸씩 읽으면 UTF-16 코드 단위로 잘린다. 😀 같은 글자는
11
- * 두 칸을 차지하므로 하필 그 사이에서 줄이 접히면 반쪽짜리 서로게이트가
12
- * 양쪽 줄에 하나씩 남는다 — 접어쓰기("a😀", 2) 가 ["a\ud83d","\ude00"] 를
13
- * 내놨다. 화면에는 물음표 두 개로 찍히고, 폭 계산도 그때부터 어긋난다.
14
- *
15
- * 그래서 코드 포인트로 읽고, 뒤에 붙는 것들(살색 조절·변형 선택자·결합
16
- * 부호·ZWJ 로 이어붙인 다음 글자)까지 한 덩이로 본다. 완전한 문자소 분할은
17
- * 아니지만 — 그건 표가 있어야 한다 — 실제로 깨지던 자리는 이걸로 다 막힌다.
18
- */
19
- function 한덩이(s, i) {
20
- let 끝 = i + String.fromCodePoint(s.codePointAt(i)).length;
21
- for (;;) {
22
- if (끝 >= s.length) return s.slice(i, 끝);
23
- const cp = s.codePointAt(끝);
24
- // ZWJ 는 다음 글자까지 끌고 온다 (👨‍👩‍👧 같은 이어붙인 그림글자)
25
- if (cp === 0x200d) {
26
- const 다음 = 끝 + 1;
27
- if (다음 >= s.length) return s.slice(i, 끝);
28
- 끝 = 다음 + String.fromCodePoint(s.codePointAt(다음)).length;
29
- continue;
30
- }
31
- const 딸린것 = (cp >= 0xfe00 && cp <= 0xfe0f) // 변형 선택자
32
- || (cp >= 0x1f3fb && cp <= 0x1f3ff) // 살색 조절
33
- || (cp >= 0x0300 && cp <= 0x036f) // 결합 부호
34
- || cp === 0x20e3; // 키캡
35
- if (!딸린것) return s.slice(i, 끝);
36
- 끝 += String.fromCodePoint(cp).length;
37
- }
38
- }
8
+ const 색코드들 = /\x1b\[[0-9;]*m/g;
39
9
 
40
10
  /**
41
11
  * 색을 유지하면서 폭에 맞춰 접는다.
@@ -45,7 +15,16 @@ function 한덩이(s, i) {
45
15
  * 마지막으로 켜져 있던 색을 다음 줄 앞에 다시 켜 준다. 안 그러면 접힌 줄부터
46
16
  * 색이 풀려 화면이 얼룩덜룩해진다.
47
17
  */
48
- export function 접어쓰기(글, 폭) {
18
+ export function 접어쓰기(글, 폭, 설정 = {}) {
19
+ /*
20
+ * `빈칸줄남김` — 빈칸뿐인 마지막 줄을 버리지 않는다.
21
+ *
22
+ * 대화 글을 접을 때는 끝의 빈칸이 넘친 줄을 버리는 것이 맞다(보이는 것이 없는 줄이 한 줄
23
+ * 더 생길 뿐이다). 그런데 입력 상자는 **사람이 친 빈칸**을 그리고 그 뒤에 커서를 세운다.
24
+ * 터미널은 넘친 빈칸을 다음 줄로 보내 커서를 거기 두는데, 줄을 버리면 상자는 한 줄로 세고
25
+ * 커서를 윗줄 끝에 세운다 — 친 빈칸이 안 들어간 것처럼 보이고 줄 셈이 한 줄 어긋난다.
26
+ */
27
+ const { 빈칸줄남김 = false } = 설정 ?? {};
49
28
  const s = String(글 ?? '');
50
29
  /*
51
30
  * 줄바꿈(\n)이 있으면 그 자리에서 반드시 갈라진다.
@@ -57,7 +36,7 @@ export function 접어쓰기(글, 폭) {
57
36
  * 있으면 터미널이 실제로 그린 줄 수와 어긋나 화면이 깨진다. 문단마다
58
37
  * 따로 접어서 이 배열이 실제 화면 줄 수와 늘 같게 만든다.
59
38
  */
60
- if (s.includes('\n')) return s.split('\n').flatMap((줄) => 접어쓰기(줄, 폭));
39
+ if (s.includes('\n')) return s.split('\n').flatMap((줄) => 접어쓰기(줄, 폭, 설정));
61
40
  if (폭 < 2) return [s];
62
41
  if (width(s) <= 폭) return [s];
63
42
 
@@ -68,8 +47,13 @@ export function 접어쓰기(글, 폭) {
68
47
  * 시작한다. 화면에서 세로줄이 끊겨 보이고, 도구 결과의 ` └ …` 도
69
48
  * 두 번째 줄부터 갑자기 튀어나온다 — 무엇에 딸린 글인지 알 수 없게 된다.
70
49
  * 들여쓴 만큼은 글자 자리에서 빼야 하므로 접는 폭도 같이 줄인다.
50
+ *
51
+ * 빈칸을 세기 전에 맨 앞의 색 코드를 건너뛴다. 빈칸만 찾으면 색이 먼저 온 줄
52
+ * (`c.gray(' └ …')` 처럼 줄 전체에 색을 씌운 자리)에서 들여쓰기를 0 으로 재고,
53
+ * 접힌 줄이 왼쪽 끝에서 시작한다. 보이는 모양은 민 글과 똑같은데 접은 결과만
54
+ * 달라지는 자리라 화면으로는 늦게 잡힌다 (2.0.0 8회차 uimisc WR2).
71
55
  */
72
- const 들여 = (/^ */.exec(s)[0] ?? '').slice(0, Math.max(0, 폭 - 8));
56
+ const 들여 = ((/^(?:\x1b\[[0-9;]*m)*( *)/.exec(s) ?? [])[1] ?? '').slice(0, Math.max(0, 폭 - 8));
73
57
  const 이어폭 = Math.max(2, 폭 - 들여.length);
74
58
 
75
59
  const 줄들 = [];
@@ -101,6 +85,8 @@ export function 접어쓰기(글, 폭) {
101
85
  w += cw;
102
86
  i += ch.length;
103
87
  }
104
- if (지금.trim() !== '' || 줄들.length === 0) 줄들.push(지금 + (색 ? '\x1b[0m' : ''));
88
+ // 빈칸뿐인지는 색 코드를 떼고 본다. 색이 켜진 채 넘친 빈칸 줄은 코드가 붙어 있어 trim 으로는 안
89
+ // 비는데, 보이는 것은 똑같이 없다 — 색 없는 글과 줄 수가 달라진다 (2.0.0 6회차 WR1).
90
+ if (빈칸줄남김 || 지금.replace(색코드들, '').trim() !== '' || 줄들.length === 0) 줄들.push(지금 + (색 ? '\x1b[0m' : ''));
105
91
  return 줄들;
106
92
  }