deel-local-cli 1.20.13 → 2.0.0

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 +96 -78
  2. package/README.md +119 -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 +300 -0
  58. package/src/commands/model.js +852 -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 +452 -39
  64. package/src/configexplain.js +125 -14
  65. package/src/doctor.js +164 -26
  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 +507 -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 +11 -1
  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 +444 -36
  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 +1029 -145
  118. package/src/tools/jobs.js +325 -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 +366 -56
  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 +312 -43
  127. package/src/tools/webfetch.js +184 -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
@@ -16,6 +16,21 @@
16
16
  // 화면에 한 번에 보여 줄 최대 개수. 더 많으면 목록이 화면을 밀어낸다.
17
17
  export const 최대추천 = 6;
18
18
 
19
+ /*
20
+ * 전각 빗금(/, U+FF0F).
21
+ *
22
+ * 일본어·중국어 입력기를 켠 채로 `/` 를 치면 이 글자가 나온다. 눈으로는 빗금인데 명령 자리는
23
+ * `/` 만 봤다 — 추천은 아무것도 안 뜨고, Enter 를 치면 `/help` 가 말로 모델에게 갔다.
24
+ * 사람은 명령이 없는 줄 안다. 맨 앞 한 글자만 바꾼다(글 가운데의 전각 빗금은 사람 말이다).
25
+ */
26
+ const 전각빗금 = String.fromCharCode(0xff0f);
27
+
28
+ /** 맨 앞의 전각 빗금을 `/` 로. 명령을 가르는 자리(commands.js)도 같은 것을 쓴다. */
29
+ export function 빗금펴기(글) {
30
+ const s = String(글 ?? '');
31
+ return s.startsWith(전각빗금) ? `/${s.slice(1)}` : s;
32
+ }
33
+
19
34
  /**
20
35
  * 지금 치고 있는 글에 맞는 명령들.
21
36
  *
@@ -27,7 +42,7 @@ export const 최대추천 = 6;
27
42
  * @param {string[]} 보일것 이 수준에서 보여 줄 이름들 (없으면 전부)
28
43
  */
29
44
  export function 추천(글, 명령표 = {}, 보일것 = null) {
30
- const s = String(글 ?? '');
45
+ const s = 빗금펴기(글);
31
46
  const m = /^\/([^\s]*)$/.exec(s);
32
47
  if (!m) return [];
33
48
 
@@ -37,8 +52,9 @@ export function 추천(글, 명령표 = {}, 보일것 = null) {
37
52
  /*
38
53
  * 앞에서부터 맞는 것을 먼저, 가운데 맞는 것을 뒤에.
39
54
  *
40
- * `/mo` 를 치면 mode·model 이 먼저 오고 memory 는 안 온다. 그런데 `/emo` 처럼
41
- * 앞을 틀리게 쳤을 때도 memory 가 나오면 고맙다 — 오타는 앞글자에서 제일 많이 난다.
55
+ * `/mo` 를 치면 mode·model 이 먼저 오고, 가운데가 맞는 memory 가 그 뒤에 붙는다.
56
+ * 떨구지 않는 것은 `/emo` 처럼 앞을 틀리게 쳤을 때 memory 가 나오면 고맙기 때문이다 —
57
+ * 오타는 앞글자에서 제일 많이 난다. 순서만으로 충분하고, 지울 이유는 없다.
42
58
  */
43
59
  const 앞 = [];
44
60
  const 안 = [];
@@ -63,7 +79,7 @@ export function 추천(글, 명령표 = {}, 보일것 = null) {
63
79
  * 넣어 버리면 사람이 원한 것이 아닐 때 지우는 수고가 더 든다.
64
80
  */
65
81
  export function 채울글(글, 후보들) {
66
- const s = String(글 ?? '');
82
+ const s = 빗금펴기(글);
67
83
  const m = /^\/([^\s]*)$/.exec(s);
68
84
  if (!m || !후보들?.length) return '';
69
85
  const 친것 = m[1];
@@ -87,3 +103,29 @@ export function 채울글(글, 후보들) {
87
103
  }
88
104
  return 같은데까지.length > 친것.length ? 같은데까지.slice(친것.length) : '';
89
105
  }
106
+
107
+ /**
108
+ * readline 에 다는 완성기 — 아무것도 채우지 않되, **붙여넣은 조각 끝의 탭은 살린다.**
109
+ *
110
+ * 빈 완성기를 달아 두는 까닭은 repl.js 에 적어 뒀다(Tab 이 줄에 박히지 않게, 채우기는 우리가
111
+ * rl.write 로). 그런데 readline 은 한 조각(data 한 번)의 **마지막 글자**가 탭이면 그것을 Tab 키로
112
+ * 보고 완성기에 넘긴다. 빈 완성기는 아무것도 안 내놓으니 그 탭이 사라진다 — 엑셀에서 복사한
113
+ * `이름\t부서` 가 조각 경계에 걸리면 `이름부서` 로 붙어 모델에게 간다. 조각 가운데의 탭은
114
+ * readline 이 글자로 넣으니 멀쩡해서, 됐다 안 됐다 한다.
115
+ *
116
+ * 사람이 Tab 을 누르면 조각이 탭 하나다. 그래서 **탭으로 끝나는데 한 글자보다 긴** 조각이면
117
+ * 붙여넣기로 보고 탭을 도로 넣는다. Shift+Tab(ESC [ Z)은 탭으로 안 끝나므로 걸리지 않는다.
118
+ *
119
+ * @param {import('node:stream').Readable} 입력 readline 이 읽는 입력(대개 process.stdin)
120
+ * @returns {(줄: string) => [string[], string]}
121
+ */
122
+ export function 붙임탭완성기(입력) {
123
+ let 끝이탭 = false;
124
+ // readline 보다 **먼저** 조각을 봐야 한다 — 완성기는 readline 이 조각을 푸는 도중에 불린다.
125
+ 입력?.prependListener?.('data', (조각) => {
126
+ const n = 조각?.length ?? 0;
127
+ const 끝 = typeof 조각 === 'string' ? 조각.charCodeAt(n - 1) : 조각?.[n - 1];
128
+ 끝이탭 = n > 1 && 끝 === 9;
129
+ });
130
+ return (줄) => (끝이탭 ? [[`${줄}\t`], 줄] : [[], 줄]);
131
+ }
package/src/ui/diff.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // 진짜 바뀐 곳을 못 찾는다. 그래서 그 경우를 따로 알아채서 말해 준다.
17
17
  // 3) 곁줄은 통째로 만들지 않는다. 20,000줄짜리에서 한 줄 고쳤다고 20,000개를
18
18
  // 늘어놓을 이유가 없다 — 필요한 자리만 그때그때 집어 온다.
19
- import { c, clip, cols } from './ansi.js';
19
+ import { c, clip, cols, 화면글거르기 } from './ansi.js';
20
20
 
21
21
  // LCS 표를 여기까지만 만든다. 넘으면 대충으로 물러선다. (4M 칸 = 16MB)
22
22
  const MAX_CELLS = 4_000_000;
@@ -183,7 +183,9 @@ function LCS대본(a, b, base) {
183
183
  return out;
184
184
  }
185
185
 
186
- const 보기좋게 = (s) => String(s ?? '').replace(/\t/g, TAB).replace(/\r/g, '');
186
+ // 줄은 **파일 내용**이다 — 남의 파일에 든 터미널 제어 순서를 먼저 뗀다(ansi.js 화면글거르기 머리말).
187
+ // 탭은 그 뒤에 칸으로 편다. 거르개는 탭을 남기고 \r 은 뗀다.
188
+ const 보기좋게 = (s) => 화면글거르기(s).replace(/\t/g, TAB);
187
189
 
188
190
  /**
189
191
  * 화면에 그릴 줄들을 만든다.
package/src/ui/export.js CHANGED
@@ -27,6 +27,9 @@ import { 증거모으기 } from '../agent/evidence.js';
27
27
  import { 부른것들, 본문글 } from '../backend/adapter.js';
28
28
  import { 말, 언어 } from '../i18n/index.js';
29
29
  import { 첫이름 } from '../tools/label.js';
30
+ import { 가리기 } from '../safety/secrets.js';
31
+ // 「무엇이 아는 열쇠인가」 는 감사기록과 같은 자를 쓴다 — 두 벌이면 한쪽만 샌다.
32
+ import { 열쇠묻기 } from '../safety/audit.js';
30
33
 
31
34
  /** HTML 로 안전하게. 문서 전체가 이 함수 하나를 지나야 한다. */
32
35
  function 글자(s) {
@@ -40,8 +43,13 @@ function 글자(s) {
40
43
  /** ANSI 제어문자 걷어내기. 도구 결과에는 색이 섞여 있을 수 있다. */
41
44
  const 민글 = (s) => String(s ?? '').replace(/\x1b\[[0-9;]*m/g, '');
42
45
 
43
- /** 도구 호출 한 줄 요약. 보고서는 기록이지 재현이 아니다 — 인자 전체를 안 싣는다. */
44
- function 도구줄(tc) {
46
+ /**
47
+ * 도구 호출 한 줄 요약. 보고서는 기록이지 재현이 아니다 — 인자 전체를 안 싣는다.
48
+ *
49
+ * 가린 **뒤에** 자른다. 먼저 자르면 열쇠가 80자 선에 걸쳤을 때 앞 조각만 남고,
50
+ * 조각은 정확히 아는 열쇠라도 못 알아본다 (safety/audit.js 의 turn 과 같은 까닭).
51
+ */
52
+ function 도구줄(tc, 가림 = (s) => s) {
45
53
  // 부른것들() 이 규격을 이미 펴서 { id, name, args } 로 준다.
46
54
  const 이름 = tc?.name ?? '?';
47
55
  let 핵심 = '';
@@ -51,7 +59,7 @@ function 도구줄(tc) {
51
59
  // 있어서, 웹을 읽은 줄이 보고서에서만 빈 괄호로 남았다.
52
60
  핵심 = 첫이름(a) ?? a.command ?? '';
53
61
  } catch { /* 인자가 잘렸어도 이름은 보여 준다 */ }
54
- 핵심 = String(핵심);
62
+ 핵심 = 가림(String(핵심));
55
63
  if (핵심.length > 80) 핵심 = 핵심.slice(0, 80) + '…';
56
64
  return 핵심 ? `${이름}(${핵심})` : 이름;
57
65
  }
@@ -67,8 +75,22 @@ function 도구줄(tc) {
67
75
  export function 보고서짓기(session, { scope = null, audit = null } = {}) {
68
76
  const 보임 = (p) => { try { return scope?.show ? scope.show(p) : String(p); } catch { return String(p); } };
69
77
 
78
+ /*
79
+ * ── 보고서에 싣기 전에 가린다 ─────────────────────────────────────────
80
+ *
81
+ * 보고서는 남에게 가는 글이다 — 결재 첨부로 돌고 메일로 나간다. 그런데 사람 말·
82
+ * 모델 말·도구 줄을 **그대로** 실었다. 같은 `curl -H "Authorization: Bearer …"`
83
+ * 가 감사기록에는 가려서 적히는데 보고서에는 평문으로 나갔다. 가리는 자리가
84
+ * 한 곳이라도 빠지면, 가려진 줄 알고 넘긴 사람이 그 한 곳으로 샌다.
85
+ *
86
+ * 아는 열쇠(이 연결의 열쇠·환경변수 열쇠)는 감사기록과 같은 자로 묻는다.
87
+ * 자유롭게 적히는 글만 가린다 — 파일 이름·숫자는 안 건드린다.
88
+ */
89
+ const 아는열쇠 = (() => { try { return 열쇠묻기(session.conn)(); } catch { return []; } })();
90
+ const 가림 = (s) => 가리기(String(s ?? ''), { 열쇠들: 아는열쇠 }).글;
91
+
70
92
  // ── 재료 ──────────────────────────────────────────────────────────────
71
- const 첫말 = 본문글((session.messages ?? []).find((m) => m.role === 'user'));
93
+ const 첫말 = 가림(본문글((session.messages ?? []).find((m) => m.role === 'user')));
72
94
  const 제목 = String(첫말 || 말('export.untitled')).split('\n')[0].slice(0, 80);
73
95
 
74
96
  const 흐름 = [];
@@ -84,11 +106,12 @@ export function 보고서짓기(session, { scope = null, audit = null } = {}) {
84
106
  const 글 = 본문글(m);
85
107
  if (m.role === 'user') {
86
108
  // Anthropic 꼴에서 사람 차례에 도구 결과가 섞여 온다. 그건 글이 아니다.
87
- if (글.trim()) 흐름.push({ 갈래: 'user', 글 });
109
+ if (글.trim()) 흐름.push({ 갈래: 'user', 글: 가림(글) });
88
110
  } else if (m.role === 'assistant') {
89
- const 부른것 = 부른것들(m).map(도구줄);
111
+ const 부른것 = 부른것들(m).map((tc) => 도구줄(tc, 가림));
90
112
  if (부른것.length) 흐름.push({ 갈래: 'tools', 글: 부른것.join(' · ') });
91
- if (글.trim()) 흐름.push({ 갈래: 'assistant', 글 });
113
+ // 색 글자를 먼저 걷고 가린다 — 열쇠 한가운데 색이 끼면 꼴이 안 맞는다.
114
+ if (글.trim()) 흐름.push({ 갈래: 'assistant', 글: 가림(민글(글)) });
92
115
  }
93
116
  // 도구 결과 알맹이는 안 싣는다 — 길고, 요점은 답과 증거 절에 이미 있다.
94
117
  }
@@ -96,8 +119,18 @@ export function 보고서짓기(session, { scope = null, audit = null } = {}) {
96
119
  const 바뀐것 = [...(session.changes ?? new Map()).entries()]
97
120
  .map(([p, v]) => ({ 파일: 보임(p), 더함: v.added ?? 0, 뺌: v.removed ?? 0, 몇번: v.times ?? 1 }));
98
121
 
122
+ /*
123
+ * 못 모은 것과 모을 것이 없는 것은 **다르다** (8회차 판정).
124
+ *
125
+ * 여기가 `catch { return null; }` 이었다. 그러면 아래에서 `증거?.증명안된것 ?? []`
126
+ * 가 빈 배열이 되고, 바뀐 파일이 있으면 「없습니다 — 바꾼 것마다 뒤에 돌린 확인이
127
+ * 있습니다」 가 찍힌다. 재 보니 감사기록을 못 읽는 자리에서 그 줄이 그대로 나왔다.
128
+ * **증거를 한 줄도 못 봤는데 다 증명됐다고 말한 것이다.** 보고서는 남는 물건이라
129
+ * 그 거짓말이 그대로 남는다. 그래서 까닭을 들고 온다.
130
+ */
131
+ let 증거못모은까닭 = null;
99
132
  const 증거 = (() => {
100
- try { return 증거모으기(session, { audit }); } catch { return null; }
133
+ try { return 증거모으기(session, { audit }); } catch (err) { 증거못모은까닭 = err?.message ?? String(err); return null; }
101
134
  })();
102
135
 
103
136
  const u = session.usage ?? {};
@@ -144,7 +177,9 @@ export function 보고서짓기(session, { scope = null, audit = null } = {}) {
144
177
  const 돌린 = 증거?.돌린것 ?? [];
145
178
  if (돌린.length) {
146
179
  조각.push('<ul class="ran">');
147
- for (const x of 돌린) 조각.push(`<li class="${x.됐나 ? 'ok' : 'bad'}">${x.됐나 ? '✓' : '✗'} <code>${글자(민글(x.무엇))}</code>${x.남긴말 ? ` — ${글자(민글(x.남긴말))}` : ''}</li>`);
180
+ // 돌린 명령도 도구 줄이다. 감사기록에서 온 것은 이미 가려져 있지만, 대화에서 모은
181
+ // 것은 아니다 — 어디서 왔든 여기서 한 번 더 지난다(이미 가린 표는 그대로 남는다).
182
+ for (const x of 돌린) 조각.push(`<li class="${x.됐나 ? 'ok' : 'bad'}">${x.됐나 ? '✓' : '✗'} <code>${글자(가림(민글(x.무엇)))}</code>${x.남긴말 ? ` — ${글자(가림(민글(x.남긴말)))}` : ''}</li>`);
148
183
  조각.push('</ul>');
149
184
  } else {
150
185
  조각.push(`<p class="none">${글자(말('export.nothingRan'))}</p>`);
@@ -152,9 +187,13 @@ export function 보고서짓기(session, { scope = null, audit = null } = {}) {
152
187
 
153
188
  조각.push(`<h3>${글자(말('export.unproven'))}</h3>`);
154
189
  const 미증명 = 증거?.증명안된것 ?? [];
155
- if (미증명.length) {
190
+ if (증거못모은까닭) {
191
+ 조각.push(`<p class="none">${글자(말('export.evidenceFailed', { 왜: 증거못모은까닭 }))}</p>`);
192
+ } else if (미증명.length) {
156
193
  조각.push('<ul class="unproven">');
157
- for (const x of 미증명) 조각.push(`<li><b>${글자(x.파일)}</b> — ${글자(x.왜 ?? '')}</li>`);
194
+ // 파일 이름은 바뀐것 표와 **같은 자로** 짧게 적는다 (8회차 판정) — 한 보고서
195
+ // 안에 짧은 경로와 날 절대경로가 섞여 있으면 같은 파일인지 사람이 못 가른다.
196
+ for (const x of 미증명) 조각.push(`<li><b>${글자(보임(x.파일))}</b> — ${글자(x.왜 ?? '')}</li>`);
158
197
  조각.push('</ul>');
159
198
  } else if (바뀐것.length) {
160
199
  조각.push(`<p class="none">${글자(말('export.allProven'))}</p>`);
@@ -209,7 +248,7 @@ ${조각.join('\n')}
209
248
  /**
210
249
  * 파일로 남긴다.
211
250
  *
212
- * .deel/보고서/ 아래에 시각으로 이름을 짓고, 같은 분(分)에 두 번 적으면
251
+ * .deel/export/ 아래에 시각으로 이름을 짓고, 같은 분(分)에 두 번 적으면
213
252
  * 번호를 붙인다 — **앞엣것을 절대 덮지 않는다.** 보고서는 남는 것이 일이다.
214
253
  *
215
254
  * @returns {string|null} 적은 자리. 못 적었으면 null — 못 적어도 대화는 계속된다.
@@ -222,11 +261,46 @@ export function 보고서적기(root, session, opts = {}, 탈받을것 = null) {
222
261
  const 판 = (n) => `${t.getFullYear()}${String(t.getMonth() + 1).padStart(2, '0')}${String(t.getDate()).padStart(2, '0')}`
223
262
  + `-${String(t.getHours()).padStart(2, '0')}${String(t.getMinutes()).padStart(2, '0')}`
224
263
  + (n ? `-${n}` : '') + '.html';
225
- let 이름 = 판(0);
226
- for (let n = 1; existsSync(join(폴더, 이름)) && n < 100; n++) 이름 = 판(n);
227
- const 자리 = join(폴더, 이름);
228
- writeFileSync(자리, 보고서짓기(session, opts), 'utf8');
229
- return 자리;
264
+ /*
265
+ * 사람이 이름을 준 자리(`/export 보고서`). 도움말이 `[파일이름]` 이라고
266
+ * 적어 두고도 여기서 한 번도 안 읽어서, 무엇을 적어 주든 늘 시각 이름으로
267
+ * 남았다 — 적힌 대로 안 되는 자리다.
268
+ *
269
+ * 폴더는 안 받는다. 준 글에서 **이름 한 칸만** 떼어 쓴다 —
270
+ * `../../어디` 로 내보내는 자리를 벗어나는 길을 막는다.
271
+ */
272
+ const 준이름 = String(opts.이름 ?? '').trim().replace(/[\\/]+/g, '/').split('/').pop() ?? '';
273
+ const 손질 = 준이름.replace(/^\.+/, '').trim();
274
+ if (준이름 && !손질) throw new Error(`쓸 수 없는 이름입니다: ${준이름}`);
275
+
276
+ let 이름 = 손질 ? (/\.html?$/i.test(손질) ? 손질 : `${손질}.html`) : 판(0);
277
+ // 이름을 준 자리는 번호를 안 붙인다. 있으면 덮지 않고 그대로 말한다.
278
+ if (손질 && existsSync(join(폴더, 이름))) throw new Error(`이미 있습니다: ${이름}`);
279
+ if (!손질) for (let n = 1; existsSync(join(폴더, 이름)) && n < 100; n++) 이름 = 판(n);
280
+ /*
281
+ * ── 백 번째에서 덮고 있었다 ──────────────────────────────────────────
282
+ *
283
+ * 번호는 99 에서 멈추고, 멈춘 자리가 있든 없든 그대로 적었다. 같은 분에 백 번을
284
+ * 넘기면(스크립트로 돌리면 금방이다) `…-99.html` 을 매번 덮었다 — 위 머리말의
285
+ * 「절대 덮지 않는다」 가 거짓이 된다. 두 창이 같은 순간에 같은 번호를 고르는
286
+ * 틈도 있었다.
287
+ * 그래서 `wx`(없을 때만 만들기)로 적고, 이미 있으면 프로세스 번호·시각·난수를
288
+ * 붙인 이름으로 다시 한다. 덮는 길이 아예 없다.
289
+ */
290
+ const 글 = 보고서짓기(session, opts);
291
+ for (let 번 = 0; ; 번++) {
292
+ const 자리 = join(폴더, 이름);
293
+ try {
294
+ writeFileSync(자리, 글, { encoding: 'utf8', flag: 'wx' });
295
+ return 자리;
296
+ } catch (e) {
297
+ // 사람이 이름을 준 자리는 다른 이름으로 바꿔 적지 않는다 — 적어 준 이름이
298
+ // 아닌 데 남으면 찾으러 갈 자리가 달라진다.
299
+ if (손질) throw new Error(`이미 있습니다: ${이름}`);
300
+ if (e?.code !== 'EEXIST' || 번 >= 20) throw e;
301
+ 이름 = 판(`${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`);
302
+ }
303
+ }
230
304
  } catch (e) {
231
305
  // 못 적은 까닭을 버리지 않는다 — 화면에 「못 남겼습니다」 만 뜨면 디스크가
232
306
  // 찼는지 폴더가 읽기 전용인지 알 길이 없다. 받을 그릇을 준 쪽에만 담는다.
@@ -33,7 +33,7 @@
33
33
  * 2) 그릴 것을 값으로 내놓는다(프레임). 검사는 파이프로 도니 화면을 못 본다 —
34
34
  * 값으로 내놓아야 한 칸도 안 어긋나는지 잴 수 있다.
35
35
  */
36
- import { c, width, cursor, clip, 판시작, 판끝 } from './ansi.js';
36
+ import { c, width, cursor, clip, 판시작, 판끝, 화면글거르기 } from './ansi.js';
37
37
  import { isLocalHost } from '../safety/network.js';
38
38
  import { statusLine, contextWarning, quotaWarning } from './status.js';
39
39
  import { 접어쓰기 } from './wrap.js';
@@ -43,6 +43,7 @@ import {
43
43
  import { 문구고르기, 걸린시간, 느긋해질때, 문구주기 } from './working.js';
44
44
  import { 그림고르기 } from './motion.js';
45
45
  import { 최대추천 } from './complete.js';
46
+ import { 말 } from '../i18n/index.js';
46
47
 
47
48
  /**
48
49
  * 이 연결이 이 컴퓨터 밖으로 나가나.
@@ -90,7 +91,25 @@ export const 대기최대 = 3;
90
91
  * 커서.위 = 마지막 줄에서 몇 줄 위인가, 커서.열 = 1부터 세는 칸
91
92
  */
92
93
  export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상태 = '', 경고 = '', 곁말 = '', 일감 = null, 추천 = [], 바깥 = false, 사무실 = null } = {}) {
93
- const 칸 = Math.max(20, 폭);
94
+ /*
95
+ * 칸은 창보다 넓게 잡지 않는다.
96
+ *
97
+ * `Math.max(20, 폭)` 이었다 — 10칸 창에 20칸짜리 줄을 냈다. 터미널은 그 줄을 두 줄로 접고
98
+ * 상자는 한 줄로 세니, 좁은 창(분할 창 · 옆에 붙인 창)에서 키를 칠 때마다 대화가 지워졌다.
99
+ * 아주 좁으면 상자가 제 모양을 못 갖추지만, 모양이 깨지는 것이 대화가 지워지는 것보다 낫다.
100
+ * 그래서 끝에서 한 번 더 폭 안으로 자른다(폭안).
101
+ */
102
+ const 칸 = Math.max(1, 폭);
103
+ const 폭안 = (줄들) => 줄들.map((l) => (width(l) > 칸 ? clip(l, 칸) : l));
104
+ /*
105
+ * 사람이 친 글을 상자 안 줄로 접는다.
106
+ *
107
+ * 탭은 빈칸 넷으로 그린다. 날것으로 내보내면 터미널이 다음 탭 자리까지 밀어 그려서 몇 칸인지
108
+ * 아무도 모른다 — 엑셀에서 한 줄을 붙이면 탭이 아홉 개 온다. 끝의 빈칸뿐인 줄도 버리지 않는다
109
+ * (커서가 거기 선다 — wrap.js 의 빈칸줄남김).
110
+ */
111
+ const 탭 = String.fromCharCode(9);
112
+ const 접기 = (s, w) => 접어쓰기(String(s ?? '').split(탭).join(' '), w, { 빈칸줄남김: true });
94
113
  /*
95
114
  * ── 테두리가 곧 경계선이다 ───────────────────────────────────────────
96
115
  *
@@ -106,7 +125,7 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
106
125
  * 화면이 그만큼은 말해 줘야 한다.
107
126
  */
108
127
  const 테 = 바깥 ? c.hyellow : c.gray;
109
- const 안쪽 = Math.max(4, 칸 - 앞머리 - 2); // 오른쪽 ' │' 두 칸
128
+ const 안쪽 = Math.max(2, 칸 - 앞머리 - 2); // 오른쪽 ' │' 두 칸
110
129
  const 줄들 = [];
111
130
 
112
131
  /*
@@ -137,7 +156,7 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
137
156
  if (상태) 줄들.push(clip(상태, 칸));
138
157
  if (경고) for (const 줄 of 접어쓰기(` ${경고}`, 칸)) 줄들.push(줄);
139
158
 
140
- const 가로 = '─'.repeat(칸 - 3);
159
+ const 가로 = '─'.repeat(Math.max(0, 칸 - 3));
141
160
  줄들.push(테(` ╭${가로}╮`));
142
161
 
143
162
  /*
@@ -158,8 +177,29 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
158
177
  * 어긋나 보인다.
159
178
  */
160
179
  const 일안쪽 = 안쪽 + 2;
161
- const 왼쪽 = `${c.hcyan(일감.돌림 ?? '⠋')} ${c.white(일감.말 ?? '')}${c.gray('…')}`;
162
- const 오른쪽 = c.gray(일감.곁 ?? '');
180
+ /*
181
+ * 여기 실리는 글은 **우리 것만이 아니다** — 들어온 제어 순서를 뗀다.
182
+ *
183
+ * 곁정보에는 모델이 적은 글이 온다(repl.js 가 `일바꿈('하위', clip(ev.목적, 24))` 로
184
+ * 넣는 그 값은 모델이 Task 를 부를 때 적은 목적이다). 같은 글이 say() 로 나갈 때는
185
+ * 걸러지는데(ansi.js 화면글거르기), 상자는 say 를 안 지나고 stdout 에 곧장 쓴다 —
186
+ * 한쪽만 막혀 있었다. 클립보드를 바꾸거나(OSC 52) 윗줄을 덮어써서 「✗ 실패」 를
187
+ * 「✓ 통과」 로 만드는 자리라, 폭이 어긋나는 것보다 무겁다.
188
+ *
189
+ * 우리 색은 남긴다 — 바로 아래 줄들이 그 색으로 그려진다(상태줄도 같은 길이다).
190
+ */
191
+ const 걸러 = (s) => 화면글거르기(s, { 색남김: true });
192
+ /*
193
+ * 줄바꿈과 탭은 거르개가 **일부러 남기는** 글자다(대화 글에는 있어야 한다). 그런데 여기는
194
+ * 한 줄이다. \n 하나가 들어오면 터미널은 두 줄에 그리는데 상자는 `줄들.length` 로 한 줄만
195
+ * 세고 그만큼만 커서를 올린다 — 그 뒤 판마다 한 줄씩 엇나간 자리에 덮어써서 위쪽 대화가
196
+ * 갉여 나간다. 탭은 터미널이 다음 탭 자리까지 밀어 그려 폭 셈이 통째로 틀어진다(사람이 친
197
+ * 글은 이미 위 접기 에서 빈칸 넷으로 편다). 한 줄 요약이니 빈칸 하나로 눕힌다 — 글자 수가
198
+ * 안 바뀌어서 부르는 쪽이 이미 잰 폭(clip 24)도 그대로다.
199
+ */
200
+ const 한줄로 = (s) => 걸러(s).replace(/[\n\t]/g, ' ');
201
+ const 왼쪽 = `${c.hcyan(일감.돌림 ?? '⠋')} ${c.white(한줄로(일감.말 ?? ''))}${c.gray('…')}`;
202
+ const 오른쪽 = c.gray(한줄로(일감.곁 ?? ''));
163
203
  const 남 = 일안쪽 - width(왼쪽) - width(오른쪽);
164
204
  /*
165
205
  * 자리가 모자라면 오른쪽(걸린 시간·중단 안내)을 버린다. 왼쪽이 본문이다.
@@ -191,7 +231,7 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
191
231
  * 실은 다 받고 있었다. 가린 줄이 있으면 ❯ 를 … 로 바꿔 알린다
192
232
  * (아래 본 상자가 쓰는 것과 같은 표시다).
193
233
  */
194
- const 대기줄들 = 일감.대기 ? 접어쓰기(일감.대기, 안쪽) : [];
234
+ const 대기줄들 = 일감.대기 ? 접기(일감.대기, 안쪽) : [];
195
235
  const 대기보일 = 대기줄들.slice(-대기최대);
196
236
  const 대기넘침 = 대기줄들.length > 대기최대;
197
237
  for (const [i, 한줄] of 대기보일.entries()) {
@@ -201,22 +241,22 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
201
241
 
202
242
  줄들.push(테(` ╰${가로}╯`));
203
243
  // 이 아래 줄도 폭 안에 넣는다 — 안내 문구는 43칸이라 좁은 창에서 접힌다.
204
- if (일감.대기) 줄들.push(clip(` ${c.gray('Enter 를 치면 지금 일이 끝난 뒤에 보냅니다')}`, 칸));
244
+ if (일감.대기) 줄들.push(clip(` ${c.gray(말('box.queuedHint'))}`, 칸));
205
245
  else if (곁말) 줄들.push(clip(` ${c.gray(곁말)}`, 칸));
206
246
 
207
247
  // 커서는 미리 치는 중일 때만 보여 준다. 아니면 칠 자리가 아니다.
208
248
  if (일감.대기) {
209
249
  const 마지막 = 대기보일.at(-1) ?? '';
210
- return { 줄들, 커서: { 위: 2, 열: Math.min(앞머리 + 1 + width(마지막), 칸 - 1) } };
250
+ return { 줄들: 폭안(줄들), 커서: { 위: 2, 열: Math.max(1, Math.min(앞머리 + 1 + width(마지막), 칸 - 1)) } };
211
251
  }
212
- return { 줄들, 커서: { 위: (곁말 ? 1 : 0), 열: 1 } };
252
+ return { 줄들: 폭안(줄들), 커서: { 위: (곁말 ? 1 : 0), 열: 1 } };
213
253
  }
214
254
 
215
255
  /*
216
256
  * 긴 입력은 접는다. 자르면 사람이 친 글이 안 보이는데, 안 보이는 채로
217
257
  * Enter 를 치게 되는 것이 제일 나쁘다 — 무엇을 보내는지 모르게 된다.
218
258
  */
219
- const 안쪽줄 = 글 ? 접어쓰기(글, 안쪽) : [''];
259
+ const 안쪽줄 = 글 ? 접기(글, 안쪽) : [''];
220
260
  const 넘침 = 안쪽줄.length > 안쪽최대;
221
261
  /*
222
262
  * 보일 자리는 **커서를 따라간다.**
@@ -230,7 +270,7 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
230
270
  * 끝에서 치는 보통 경우에는 예전과 똑같이 마지막 여덟 줄이 보인다.
231
271
  */
232
272
  const 커서앞줄수 = (커서자리 == null ? 글 : String(글).slice(0, 커서자리))
233
- ? 접어쓰기(커서자리 == null ? 글 : String(글).slice(0, 커서자리), 안쪽).length
273
+ ? 접기(커서자리 == null ? 글 : String(글).slice(0, 커서자리), 안쪽).length
234
274
  : 1;
235
275
  const 첫줄 = 넘침
236
276
  ? Math.min(Math.max(0, 커서앞줄수 - 안쪽최대), 안쪽줄.length - 안쪽최대)
@@ -273,7 +313,7 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
273
313
 
274
314
  추천줄수 = 보일것.length;
275
315
  if (추천.length > 보일것.length) {
276
- 줄들.push(` ${c.gray(`그 밖에 ${추천.length - 보일것.length}개 더`)}`);
316
+ 줄들.push(` ${c.gray(말('box.more', { n: 추천.length - 보일것.length }))}`);
277
317
  추천줄수 += 1;
278
318
  }
279
319
  }
@@ -289,7 +329,7 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
289
329
  * 엉뚱한 자리가 지워지는 것처럼 보인다.
290
330
  */
291
331
  const 앞글 = 커서자리 == null ? 글 : String(글).slice(0, 커서자리);
292
- const 앞줄들 = 앞글 ? 접어쓰기(앞글, 안쪽) : [''];
332
+ const 앞줄들 = 앞글 ? 접기(앞글, 안쪽) : [''];
293
333
 
294
334
  // 위로 접어 둔 줄만큼 줄 번호가 밀린다 (보일 자리는 커서를 따라 옮긴다).
295
335
  const 커서줄 = Math.min(Math.max(0, 앞줄들.length - 1 - 첫줄), 보일줄.length - 1);
@@ -298,9 +338,9 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
298
338
  const 아래 = 1 + 추천줄수 + (곁말 ? 1 : 0);
299
339
  const 위 = 아래 + (보일줄.length - 1 - 커서줄);
300
340
  // 1부터 세는 칸. 앞머리(5칸) 다음이 6칸째다 — 거기가 첫 글자 자리다.
301
- const 열 = Math.min(앞머리 + 1 + width(앞줄들.at(-1) ?? ''), 칸 - 1);
341
+ const 열 = Math.max(1, Math.min(앞머리 + 1 + width(앞줄들.at(-1) ?? ''), 칸 - 1));
302
342
 
303
- return { 줄들, 커서: { 위, 열 } };
343
+ return { 줄들: 폭안(줄들), 커서: { 위, 열 } };
304
344
  }
305
345
 
306
346
  /**
@@ -526,11 +566,11 @@ export class InputBox {
526
566
  this.일감.곁정보,
527
567
  // 예약해 둔 것이 있으면 잊지 않게 세어 준다. 몇 분짜리 일이 끝난 뒤
528
568
  // 엉뚱한 말이 저절로 나가는 것처럼 보이면 안 된다.
529
- this.일감.예약수 ? `${this.일감.예약수}건 예약됨` : '',
569
+ this.일감.예약수 ? 말('box.queued', { n: this.일감.예약수 }) : '',
530
570
  // 도는 동안 보이는 자리라 여기서는 ESC 만 말한다. Ctrl+C 도 되지만
531
571
  // 두 번 누르면 끝나 버려서, 급히 멈추려는 사람에게 권할 것이 못 된다.
532
572
  // 이미 멈추는 중이면 안 권한다 — 또 누르라는 말로 읽힌다.
533
- 멈추나 ? '' : 'ESC 중단',
573
+ 멈추나 ? '' : 말('run.hintEsc'),
534
574
  ].filter(Boolean).join(' · '),
535
575
  대기: this.일감.대기,
536
576
  });
package/src/ui/intro.js CHANGED
@@ -105,11 +105,18 @@ export function 틀(n, { 바깥 = false, 곁말 = '' } = {}) {
105
105
  const 앞선글 = 색(자란글.slice(0, -1));
106
106
  const 방금글 = 자란글 ? c.bold(색(자란글.slice(-1))) : '';
107
107
  const 남은칸 = ' '.repeat(이름폭 - 앞.length - 자란글.length);
108
- // 커서는 글자 자리를 안 먹는다 — 남은 칸 안에서 그린다. 그래야 글이 안 밀린다.
108
+ /*
109
+ * 커서 한 칸은 이름 자리 **밖에** 미리 잡아 둔다.
110
+ *
111
+ * 남은 칸 안에서 그리면 이름이 다 자란 틀에서는 남은 칸이 없어 커서가 한 칸
112
+ * 밖으로 밀리고, 첫 줄 폭이 틀마다 12·13 으로 달라진다. 다시 그리기는 커서를
113
+ * 올려 같은 자리에 덮어쓰는 것이라, 넓었던 한 칸이 안 지워지고 조각으로 남는다.
114
+ * 미리 한 칸을 잡아 두면 글자는 안 밀리고 폭은 어느 틀에서나 같다.
115
+ */
109
116
  const 커서글 = f.커서 ? c.gray('▏') : ' ';
110
117
  // 다 자란 뒤에는 굵기를 뺀다. 굵은 채로 굳으면 '아직 찍히는 중' 으로 보인다.
111
118
  const 이름글 = f.커서 ? `${앞선글}${방금글}` : 색(자란글);
112
- const 첫줄 = `${들여}${c.bold(c.white(앞))}${이름글}${커서글}${남은칸.slice(1)}`;
119
+ const 첫줄 = `${들여}${c.bold(c.white(앞))}${이름글}${커서글}${남은칸}`;
113
120
 
114
121
  const 선 = f.선 ? 색('─'.repeat(f.선)) : '';
115
122
  const 곁 = f.곁 && 곁말 ? ` ${c.gray(곁말)}` : '';
package/src/ui/level.js CHANGED
@@ -7,8 +7,13 @@
7
7
  // 위험을 떠넘기는 것이다. 초보일수록 되돌릴 수 있어야 한다.
8
8
  //
9
9
  // 감추는 것도 '못 쓰게' 가 아니라 '안 보이게' 다. 초보 수준에서도 /think 를
10
- // 치면 그대로 먹는다. 목록에 안 띄울 뿐이다 — 처음 켠 사람에게 명령 열여덟
10
+ // 치면 그대로 먹는다. 목록에 안 띄울 뿐이다 — 처음 켠 사람에게 명령 쉰
11
11
  // 개를 들이밀면 아무것도 못 고른다.
12
+ //
13
+ // 「열여덟 개」 라고 적혀 있던 자리다. 그 줄을 쓸 때는 명령이 통틀어 열여덟
14
+ // 개였는데(초보 목록은 열두 개) 그 뒤 쉰 개가 넘게 늘도록 숫자만 그대로여서,
15
+ // 마침 열여덟 개가 된 아래 목록이 제 머리말을 어기는 꼴이 됐다. 숫자만 고치면
16
+ // 또 낡으므로 검사가 실제 명령 수와 대 본다 (8회차 LV2).
12
17
 
13
18
  import { 말 } from '../i18n/index.js';
14
19
 
@@ -84,12 +89,32 @@ export function shows(levelId, cmd) {
84
89
  *
85
90
  * 원인을 숨기지 않는다 — 무엇을 하면 되는지를 앞에 놓고, 원래 문구는 뒤에 남긴다.
86
91
  * 원인을 지우면 물어볼 수도 없게 된다.
92
+ *
93
+ * 순서가 뜻을 정한다. 먼저 걸린 것이 나가므로 **좁은 것을 넓은 것 위에** 둔다.
87
94
  */
88
95
  const 풀이 = [
96
+ /*
97
+ * 시간 초과가 맨 앞이다. Node 가 내는 이름은 `ETIMEDOUT` 인데, 그 안에는 timeout 이라는
98
+ * 글자가 없다(TIMEDOUT). 그래서 `read ETIMEDOUT` 은 어느 풀이에도 안 걸려 날것으로 나갔고,
99
+ * `connect ETIMEDOUT` 은 아래 넓은 `connect` 에 먼저 걸려 「모델이 안 켜져 있다」 는 틀린
100
+ * 풀이가 나갔다 — 모델은 떠 있고 느린 것이라 그 말대로 해도 안 고쳐진다 (8회차 LV1).
101
+ */
89
102
  {
90
- when: /ECONNREFUSED|연결할 수 없|connect|fetch failed/i,
91
- say: '모델이 안 켜져 있는 것 같습니다.\n LM Studio 나 Ollama 를 켜고 모델을 하나 올린 다음 다시 해보세요.\n 어떤 것이 떠 있는지 보려면 /scan',
103
+ when: /timeout|ETIMEDOUT|시간 초과/i,
104
+ say: '모델이 제때 답하지 않았습니다.\n 큰 모델이면 원래 느립니다. 잠시 뒤 다시 하거나 더 작은 모델을 골라 보세요.',
92
105
  },
106
+ /*
107
+ * 우리가 우리 말로 적은 풀이 셋이 `connect` **위**에 선다 (10회차 LV3).
108
+ *
109
+ * LV1 은 ETIMEDOUT 만 위로 올리고 이 셋은 그대로 뒀는데, 넓은 것은 ETIMEDOUT 만이
110
+ * 아니었다. `connect` 는 글자 넉 자라 **경로와 주소에 그대로 들어간다** — `connect-api/`,
111
+ * `connector.js`, 사내 게이트웨이 `connect.…`. 이 셋의 문구에는 그 경로·주소가 같이
112
+ * 실리므로, 아래에 있는 동안 「범위 밖입니다」 가 「모델이 안 켜져 있습니다 — LM Studio 를
113
+ * 켜세요」 로 나갔다. 모델은 켜져 있고, 시킨 대로 해도 안 고쳐진다.
114
+ *
115
+ * 반대 방향은 샐 일이 없다. 이 셋은 우리가 그 자리에서 지어 내는 문구라, 모델 서버가
116
+ * 보내온 연결 오류에 「작업 범위 밖」 이 섞여 올 길이 없다.
117
+ */
93
118
  {
94
119
  when: /허용되지 않은 주소/,
95
120
  say: '지금 연결된 곳이 아닌 데로 나가려 했습니다. 막힌 게 정상입니다.\n 다른 모델을 쓰시려면 /model 로 고르세요.',
@@ -103,12 +128,12 @@ const 풀이 = [
103
128
  say: '고치기 전에 파일을 먼저 읽게 되어 있습니다. 잠시 후 다시 시도합니다.',
104
129
  },
105
130
  {
106
- when: /401|403|인증|unauthorized/i,
107
- say: '열쇠(API 키)가 없거나 맞지 않습니다.\n deel setup 으로 다시 넣어 주세요.',
131
+ when: /ECONNREFUSED|연결할 수 없|connect|fetch failed/i,
132
+ say: '모델이 안 켜져 있는 것 같습니다.\n LM Studio 나 Ollama 를 켜고 모델을 하나 올린 다음 다시 해보세요.\n 어떤 것이 떠 있는지 보려면 /scan',
108
133
  },
109
134
  {
110
- when: /timeout|시간 초과/i,
111
- say: '모델이 제때 답하지 않았습니다.\n 큰 모델이면 원래 느립니다. 잠시 뒤 다시 하거나 더 작은 모델을 골라 보세요.',
135
+ when: /401|403|인증|unauthorized/i,
136
+ say: '열쇠(API 키)가 없거나 맞지 않습니다.\n deel setup 으로 다시 넣어 주세요.',
112
137
  },
113
138
  ];
114
139