deel-local-cli 1.7.0 → 1.10.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 (61) hide show
  1. package/README.ko.md +1166 -0
  2. package/README.md +1223 -1107
  3. package/bin/deel.js +78 -12
  4. package/package.json +4 -4
  5. package/src/acp/map.js +1 -1
  6. package/src/acp/serve.js +40 -3
  7. package/src/agent/compact.js +314 -296
  8. package/src/agent/effort.js +27 -7
  9. package/src/agent/evidence.js +2 -0
  10. package/src/agent/filemem.js +141 -0
  11. package/src/agent/grade.js +51 -1
  12. package/src/agent/loop.js +218 -40
  13. package/src/agent/session.js +136 -14
  14. package/src/agent/store.js +186 -9
  15. package/src/agent/threads.js +26 -1
  16. package/src/backend/adapter.js +272 -23
  17. package/src/backend/learn.js +24 -0
  18. package/src/backend/mcp.js +96 -5
  19. package/src/backend/probe.js +131 -61
  20. package/src/backend/quota.js +29 -5
  21. package/src/backend/retry.js +3 -0
  22. package/src/backend/scanui.js +1 -1
  23. package/src/backend/toolfit.js +325 -0
  24. package/src/commands.js +170 -61
  25. package/src/completion.js +20 -2
  26. package/src/config.js +47 -2
  27. package/src/i18n/en.js +210 -3
  28. package/src/i18n/index.js +40 -0
  29. package/src/i18n/ja.js +204 -3
  30. package/src/i18n/ko.js +270 -3
  31. package/src/i18n/zh.js +204 -3
  32. package/src/lsp/client.js +49 -5
  33. package/src/oneshot.js +92 -5
  34. package/src/pack/sbom.js +30 -4
  35. package/src/pack/selfpack.js +26 -10
  36. package/src/pack/sheet.en.js +288 -0
  37. package/src/pack/tar.js +65 -2
  38. package/src/plugins/manage.js +46 -9
  39. package/src/repl.js +344 -61
  40. package/src/reset.js +397 -0
  41. package/src/safety/audit.js +95 -8
  42. package/src/safety/authcmd.js +316 -0
  43. package/src/safety/guard.js +143 -0
  44. package/src/safety/keystore.js +114 -38
  45. package/src/safety/undo.js +25 -6
  46. package/src/tools/desc.en.js +19 -14
  47. package/src/tools/index.js +349 -75
  48. package/src/tools/jobs.js +175 -42
  49. package/src/tools/lsp.js +5 -4
  50. package/src/tools/outline.js +7 -3
  51. package/src/tools/task.js +10 -6
  52. package/src/tools/todo.js +9 -2
  53. package/src/tools/verify.js +42 -7
  54. package/src/tools/webfetch.js +94 -10
  55. package/src/ui/export.js +1 -1
  56. package/src/ui/md.js +201 -5
  57. package/src/ui/motion.js +0 -1
  58. package/src/ui/pastechip.js +50 -3
  59. package/src/ui/pick.js +115 -0
  60. package/src/ui/screen.js +23 -3
  61. package/README.en.md +0 -1117
@@ -36,12 +36,25 @@ import { join } from 'node:path';
36
36
  /** 잠긴 값의 꼴. `dpapi:<base64>` · `keychain:<이름>` */
37
37
  const 꼴 = /^(dpapi|keychain):(.*)$/s;
38
38
 
39
- /** 키체인에 넣을 때 쓰는 이름. 사람이 키체인 앱에서 찾아 지울 수 있어야 한다. */
40
- export const 키체인이름 = 'deel-gateway-key';
39
+ /** 키체인에 넣을 때 쓰는 기본 이름. 사람이 키체인 앱에서 찾아 지울 수 있어야 한다. */
40
+ export const 기본키체인이름 = 'deel-gateway-key';
41
41
 
42
- let 마지막탈 = null;
43
- /** 마지막으로 잠그거나 풀다 탈. 화면·심사서가 읽는다. */
44
- export function 열쇠탈() { return 마지막탈; }
42
+ /**
43
+ * 실제로 키체인 이름.
44
+ *
45
+ * 기본값은 **바꾸지 않는다.** 이미 넣어 둔 열쇠를 계속 찾아야 하기 때문이다 —
46
+ * 이름을 갈면 사용자의 열쇠가 키체인에 남은 채로 안 보이게 된다.
47
+ *
48
+ * DEEL_KEYCHAIN_NAME 은 **따로 두고 쓰라고** 있는 자리다. 이름이 고정이라
49
+ * DEEL_HOME 을 임시 폴더로 돌려놓아도 키체인만은 진짜 자리를 썼고, 맥에서
50
+ * 이 저장소의 검사를 돌리면 잠그기/풀기가 **사용자의 진짜 게이트웨이 열쇠를
51
+ * 덮어썼다.** 검사 하나가 사람의 열쇠를 지우는 셈이다. 그래서 검사는 제 이름을
52
+ * 주고 쓰고, 끝나면 제 것만 지운다. 윈도우(DPAPI)는 잠근 덩이가 설정 파일 안에
53
+ * 있어서 이 이름과 상관이 없다.
54
+ */
55
+ export function 키체인이름() {
56
+ return process.env.DEEL_KEYCHAIN_NAME || 기본키체인이름;
57
+ }
45
58
 
46
59
  /** 이 값이 우리가 잠근 것인가. 평문 열쇠와 헷갈리면 안 된다. */
47
60
  export function 잠긴것인가(값) { return 꼴.test(String(값 ?? '')); }
@@ -102,7 +115,7 @@ function 키체인넣기(글) {
102
115
  const 계정 = userInfo().username;
103
116
  // 값 자체는 base64 로 넣는다. 키체인 도구가 줄바꿈·따옴표를 만나면 거기서 끊긴다.
104
117
  const 값 = Buffer.from(글, 'utf8').toString('base64');
105
- const 명령 = `add-generic-password -a ${계정} -s ${키체인이름} -w ${값} -U\n`;
118
+ const 명령 = `add-generic-password -a ${계정} -s ${키체인이름()} -w ${값} -U\n`;
106
119
  마지막인자 = ['security', '-i'];
107
120
  const r = spawnSync('security', ['-i'], { input: 명령, encoding: 'utf8', timeout: 20000 });
108
121
  if (r.error) return { ok: false, err: r.error.message };
@@ -111,8 +124,8 @@ function 키체인넣기(글) {
111
124
 
112
125
  function 키체인읽기() {
113
126
  const 계정 = userInfo().username;
114
- 마지막인자 = ['security', 'find-generic-password', '-a', 계정, '-s', 키체인이름, '-w'];
115
- const r = spawnSync('security', ['find-generic-password', '-a', 계정, '-s', 키체인이름, '-w'], {
127
+ 마지막인자 = ['security', 'find-generic-password', '-a', 계정, '-s', 키체인이름(), '-w'];
128
+ const r = spawnSync('security', ['find-generic-password', '-a', 계정, '-s', 키체인이름(), '-w'], {
116
129
  encoding: 'utf8', timeout: 20000,
117
130
  });
118
131
  if (r.error) return { ok: false, text: '', err: r.error.message };
@@ -133,19 +146,19 @@ export function 쓸수있나() {
133
146
  // 꺼 뒀으면 켜져 있는 척하지 않는다. 이 값을 화면 문구도 그대로 쓰기 때문에,
134
147
  // 여기서 거짓으로 답하면 "다음 저장 때 잠급니다" 라고 해 놓고 안 잠근다.
135
148
  if (process.env.DEEL_KEYSTORE === 'off') {
136
- return { 되나: false, 방식: '파일', 왜: 'DEEL_KEYSTORE=off 로 꺼 두었습니다' };
149
+ return { 되나: false, 방식: '파일', 왜: 'DEEL_KEYSTORE=off 로 꺼 두었습니다', 왜영어: 'turned off with DEEL_KEYSTORE=off' };
137
150
  }
138
151
  if (process.platform === 'win32') {
139
152
  const r = 파워셸실행('[Console]::Out.Write("ok")', '');
140
153
  if (r.ok && r.out === 'ok') return { 되나: true, 방식: 'dpapi', 왜: '' };
141
- return { 되나: false, 방식: '파일', 왜: r.err || '파워셸을 못 돌렸습니다 (정책으로 막혔을 수 있습니다)' };
154
+ return { 되나: false, 방식: '파일', 왜: r.err || '파워셸을 못 돌렸습니다 (정책으로 막혔을 수 있습니다)', 왜영어: r.err || 'PowerShell could not be run (execution policy may block it)' };
142
155
  }
143
156
  if (process.platform === 'darwin') {
144
157
  const r = spawnSync('security', ['-h'], { encoding: 'utf8', timeout: 10000 });
145
158
  if (!r.error) return { 되나: true, 방식: 'keychain', 왜: '' };
146
- return { 되나: false, 방식: '파일', 왜: 'security 명령을 못 찾았습니다' };
159
+ return { 되나: false, 방식: '파일', 왜: 'security 명령을 못 찾았습니다', 왜영어: 'the security command was not found' };
147
160
  }
148
- return { 되나: false, 방식: '파일', 왜: '이 운영체제에서는 파일 권한(0600)으로만 둡니다' };
161
+ return { 되나: false, 방식: '파일', 왜: '이 운영체제에서는 파일 권한(0600)으로만 둡니다', 왜영어: 'this OS has no keystore, so file permissions (0600) are all there is' };
149
162
  }
150
163
 
151
164
  /**
@@ -157,19 +170,16 @@ export function 쓸수있나() {
157
170
  export function 잠그기(글) {
158
171
  const 값 = String(글 ?? '');
159
172
  if (!값) return null;
160
- 마지막탈 = null;
161
173
 
162
174
  if (process.platform === 'win32') {
163
175
  const r = 파워셸실행(잠그는스크립트, Buffer.from(값, 'utf8').toString('base64'));
164
176
  if (r.ok && r.out) return `dpapi:${r.out}`;
165
- 마지막탈 = `열쇠를 잠갔습니다${r.err || '파워셸이 답을 줬습니다'}`;
166
- return null;
177
+ return null; // 사유는 어디로도 간다 잠긴 것인지 아닌지는 보관방식() 따로 말한다
167
178
  }
168
179
  if (process.platform === 'darwin') {
169
180
  const r = 키체인넣기(값);
170
- if (r.ok) return `keychain:${키체인이름}`;
171
- 마지막탈 = `열쇠를 키체인에 넣었습니다${r.err || 'security 실패했습니다'}`;
172
- return null;
181
+ if (r.ok) return `keychain:${키체인이름()}`;
182
+ return null; // 사유는 어디로도 간다잠긴 것인지 아닌지는 보관방식() 이 따로 말한다
173
183
  }
174
184
  return null;
175
185
  }
@@ -187,51 +197,117 @@ export function 풀기(태그) {
187
197
  const m = 꼴.exec(String(태그 ?? ''));
188
198
  if (!m) return { ok: false, text: '', why: '잠긴 열쇠가 아닙니다' };
189
199
  const [, 갈래, 값] = m;
190
- 마지막탈 = null;
191
200
 
192
201
  if (갈래 === 'dpapi') {
193
202
  if (process.platform !== 'win32') {
194
- 마지막탈 = '이 열쇠는 윈도우에서 잠근 것이라 여기서는 못 풉니다 — deel setup 으로 다시 넣으세요.';
195
- return { ok: false, text: '', why: 마지막탈 };
203
+ return { ok: false, text: '', why: '이 열쇠는 윈도우에서 잠근 것이라 여기서는 못 풉니다 — deel setup 으로 다시 넣으세요.' };
196
204
  }
197
205
  const r = 파워셸실행(푸는스크립트, 값);
198
206
  if (r.ok && r.out) {
199
207
  try { return { ok: true, text: Buffer.from(r.out, 'base64').toString('utf8'), why: '' }; }
200
208
  catch { /* 아래로 */ }
201
209
  }
202
- 마지막탈 = '잠근 열쇠를 못 풉니다 — 이 PC 의 이 계정에서 잠근 것만 풀립니다. deel setup 으로 다시 넣으세요.';
203
- return { ok: false, text: '', why: 마지막탈 };
210
+ return { ok: false, text: '', why: '잠근 열쇠를 못 풉니다 — 이 PC 의 이 계정에서 잠근 것만 풀립니다. deel setup 으로 다시 넣으세요.' };
204
211
  }
205
212
 
206
213
  if (갈래 === 'keychain') {
207
214
  if (process.platform !== 'darwin') {
208
- 마지막탈 = '이 열쇠는 맥 키체인에 있습니다 — 여기서는 못 읽습니다. deel setup 으로 다시 넣으세요.';
209
- return { ok: false, text: '', why: 마지막탈 };
215
+ return { ok: false, text: '', why: '이 열쇠는 맥 키체인에 있습니다 — 여기서는 못 읽습니다. deel setup 으로 다시 넣으세요.' };
210
216
  }
211
217
  const r = 키체인읽기();
212
218
  if (r.ok) return { ok: true, text: r.text, why: '' };
213
- 마지막탈 = `키체인에서 열쇠를 못 읽었습니다 — ${r.err}`;
214
- return { ok: false, text: '', why: 마지막탈 };
219
+ return { ok: false, text: '', why: `키체인에서 열쇠를 못 읽었습니다 — ${r.err}` };
215
220
  }
216
221
 
217
222
  return { ok: false, text: '', why: '모르는 잠금 방식입니다' };
218
223
  }
219
224
 
220
- /** 화면·심사서에 그대로 쓰는 한 줄. 되는 척도 안 되는 척도 안 한다. */
221
- export function 보관방식(값 = null) {
225
+ /**
226
+ * 화면·심사서에 그대로 쓰는 한 줄. 되는 척도 안 되는 척도 안 한다.
227
+ *
228
+ * 영어 심사서(pack/sheet.en.js)도 **이 함수**를 쓴다. 사실을 가려내는 규칙은
229
+ * 하나여야 한다 — 영어판에 같은 판단을 다시 적어 두면, 잠금장치가 달라졌을 때
230
+ * 한쪽만 고쳐져서 두 서류가 서로 다른 말을 하게 된다.
231
+ */
232
+ export function 보관방식(값 = null, { lang = 'ko' } = {}) {
233
+ const 한 = lang === 'ko';
222
234
  if (값 && 잠긴것인가(값)) {
223
- return 값.startsWith('dpapi:')
224
- ? 'DPAPI — 이 PC 의 이 계정만 풉니다'
225
- : ' 키체인 계정의 로그인 키체인에 있습니다';
235
+ if (값.startsWith('dpapi:')) {
236
+ return 한 ? 'DPAPI — 이 PC 의 이 계정만 풉니다'
237
+ : 'DPAPI - only this account on this machine can unseal it';
238
+ }
239
+ return 한 ? '맥 키체인 — 이 계정의 로그인 키체인에 있습니다'
240
+ : 'macOS keychain - stored in this account\'s login keychain';
226
241
  }
227
242
  const 쓸수 = 쓸수있나();
228
243
  if (값) {
229
244
  // 평문인데 잠글 수는 있는 상태. 다음 저장에서 잠긴다.
230
- return 쓸수.되나
231
- ? `파일에 평문 — 다음 저장 때 ${쓸수.방식 === 'dpapi' ? 'DPAPI' : '키체인'} 로 잠급니다`
232
- : `파일에 평문 + 권한 0600 ${쓸수.왜}`;
245
+ const 장치 = 쓸수.방식 === 'dpapi' ? 'DPAPI' : (한 ? '키체인' : 'the keychain');
246
+ if (쓸수.되나) {
247
+ return 한 ? `파일에 평문 다음 저장 ${장치} 로 잠급니다`
248
+ : `Plain text in the file - it will be sealed with ${장치} on the next save`;
249
+ }
250
+ return 한 ? `파일에 평문 + 권한 0600 — ${쓸수.왜}`
251
+ : `Plain text in the file, mode 0600 - ${쓸수.왜영어 ?? 'no OS keystore is available here'}`;
252
+ }
253
+ if (process.platform === 'win32') {
254
+ return 한 ? 'DPAPI (윈도우) · 저장된 열쇠 없음' : 'DPAPI (Windows) - no key stored';
255
+ }
256
+ if (process.platform === 'darwin') {
257
+ return 한 ? '맥 키체인 · 저장된 열쇠 없음' : 'macOS keychain - no key stored';
258
+ }
259
+ return 한 ? '파일 권한 0600 · 저장된 열쇠 없음' : 'File mode 0600 - no key stored';
260
+ }
261
+
262
+ /**
263
+ * 잠금장치에서 열쇠를 지운다 (`deel reset model` 이 부른다).
264
+ *
265
+ * ── 왜 따로 있어야 하나 ────────────────────────────────────────────────
266
+ *
267
+ * 설정 파일만 지우면 **잠금장치에는 열쇠가 그대로 남는다.** 화면에는
268
+ * 「초기화했습니다」 가 뜨는데 열쇠는 살아 있는 상태다. 「초기화」 라고
269
+ * 말하려면 그것까지 지워야 하고, 못 지웠으면 못 지웠다고 말해야 한다.
270
+ *
271
+ * 두 방식이 다르게 생겼다:
272
+ *
273
+ * 윈도우(DPAPI) 잠근 덩이가 설정 파일 **안에** 있다. 파일을 지우면 같이
274
+ * 사라진다 — 잠금장치에 따로 남는 것이 없다. 그래서 여기서
275
+ * 할 일이 없고, 없다고 정직하게 답한다. 「지웠다」 고 하면
276
+ * 안 한 일을 했다고 말하는 것이다.
277
+ * 맥(키체인) 설정에는 이름표만 있고 실물은 로그인 키체인에 남는다.
278
+ * 이건 우리가 지워야 한다. 안 지우면 설정을 지운 뒤에도
279
+ * 키체인 앱에 `deel-gateway-key` 가 그대로 보인다.
280
+ *
281
+ * 열쇠는 여기서도 명령줄에 안 올린다 — 지울 때 넘기는 것은 이름뿐이다.
282
+ *
283
+ * @param {string|null} 값 설정에 적혀 있던 잠긴 값. 없으면 이 PC 방식으로 판단한다.
284
+ * @returns {{지움: boolean, 방식: 'dpapi'|'keychain'|'없음', 왜: string}}
285
+ */
286
+ export function 잠금지우기(값 = null) {
287
+ const m = 꼴.exec(String(값 ?? ''));
288
+ const 갈래 = m ? m[1] : (process.platform === 'darwin' ? 'keychain' : null);
289
+
290
+ if (갈래 === 'dpapi' || (!갈래 && process.platform === 'win32')) {
291
+ return { 지움: false, 방식: 'dpapi', 왜: '설정 파일 안에 있어서 파일과 같이 지워집니다' };
292
+ }
293
+ if (갈래 !== 'keychain') {
294
+ return { 지움: false, 방식: '없음', 왜: '잠금장치에 따로 둔 것이 없습니다' };
295
+ }
296
+ if (process.platform !== 'darwin') {
297
+ return { 지움: false, 방식: 'keychain', 왜: '맥에서 넣은 열쇠라 여기서는 못 지웁니다 — 그 맥에서 지우세요' };
298
+ }
299
+
300
+ const 계정 = userInfo().username;
301
+ const 인자 = ['delete-generic-password', '-a', 계정, '-s', 키체인이름()];
302
+ 마지막인자 = ['security', ...인자];
303
+ const r = spawnSync('security', 인자, { encoding: 'utf8', timeout: 20000 });
304
+ if (r.error) return { 지움: false, 방식: 'keychain', 왜: r.error.message };
305
+ // 없는 것을 지우라고 해도 실패로 온다. 그건 탈이 아니라 이미 없는 것이다.
306
+ if (r.status !== 0) {
307
+ const 말 = (r.stderr ?? '').trim();
308
+ return /could not be found|SecKeychainSearchCopyNext/i.test(말)
309
+ ? { 지움: false, 방식: 'keychain', 왜: '키체인에 이미 없습니다' }
310
+ : { 지움: false, 방식: 'keychain', 왜: 말 || 'security 가 실패했습니다' };
233
311
  }
234
- if (process.platform === 'win32') return 'DPAPI (윈도우) · 저장된 열쇠 없음';
235
- if (process.platform === 'darwin') return '맥 키체인 · 저장된 열쇠 없음';
236
- return '파일 권한 0600 · 저장된 열쇠 없음';
312
+ return { 지움: true, 방식: 'keychain', 왜: '' };
237
313
  }
@@ -3,6 +3,7 @@
3
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
+ import { 말 } from '../i18n/index.js';
6
7
 
7
8
  // 되돌리기 이력은 파일 내용을 통째로 담는다. 이만큼 커지면 오래된 턴을 버린다.
8
9
  const MAX_BYTES = 32 * 1024 * 1024;
@@ -167,7 +168,7 @@ export class History {
167
168
  continue;
168
169
  }
169
170
  rmSync(path, { force: true });
170
- restored.push({ path, how: '삭제됨(원래 없던 파일)' });
171
+ restored.push({ path, how: 말('undo.wayDeleted'), ok: true });
171
172
  } else {
172
173
  /*
173
174
  * 담고 있던 폴더가 없어졌을 수 있다 — Move 로 폴더째 옮긴 경우다.
@@ -178,16 +179,34 @@ export class History {
178
179
  mkdirSync(dirname(path), { recursive: true });
179
180
  // enc 가 붙어 있으면 UTF-8 로 담을 수 없던 파일이다 — 바이트를 그대로 되돌린다.
180
181
  writeFileSync(path, rec.enc === 'b64' ? Buffer.from(rec.before, 'base64') : Buffer.from(rec.before, 'utf8'));
181
- restored.push({ path, how: '되돌림' });
182
+ restored.push({ path, how: 말('undo.wayRestored'), ok: true });
182
183
  }
183
184
  } catch (err) {
184
- restored.push({ path, how: `실패: ${err.message}` });
185
+ restored.push({ path, how: `실패: ${err.message}`, ok: false, 못했나: true });
185
186
  }
186
187
  }
187
- // 되돌린 기록은 잘라낸다.
188
- const keep = recs.filter((r) => !turns.includes(r.turn));
188
+ /*
189
+ * ── 되돌린 것의 기록은 **남긴다** ──────────────────────────────
190
+ *
191
+ * 전에는 성패를 안 가리고 그 턴의 기록을 통째로 잘라냈다. 그러면 한 번
192
+ * 실패한 파일은 스냅샷이 사라져서 **다시 시도할 길이 없다.** 화면은
193
+ * 「파일 N개를 되돌렸습니다」 라고 하고(실패한 것까지 셌다), 감사기록에도
194
+ * 그 부풀린 수가 남고, 부르는 쪽은 /diff 목록에서까지 그 파일을 지운다.
195
+ * 파일만 고쳐진 채로 남고 아무도 그걸 모른다.
196
+ *
197
+ * 되돌린 것만 잘라낸다. 못한 것은 그대로 두어 한 번 더 시도할 수 있게 한다.
198
+ */
199
+ const 못한것 = new Set(restored.filter((x) => x.ok === false).map((x) => x.path));
200
+ const keep = recs.filter((r) => !turns.includes(r.turn) || 못한것.has(r.path));
189
201
  writeFileSync(this.file, keep.map((r) => JSON.stringify(r)).join('\n') + (keep.length ? '\n' : ''), 'utf8');
190
- return { restored, turns: turns.length, turnIds: turns.slice() };
202
+ return {
203
+ restored,
204
+ // 화면·감사기록이 쓰는 수. **진짜로 되돌아간 것만** 센다.
205
+ 되돌린수: restored.filter((x) => x.ok === true).length,
206
+ 못한것: restored.filter((x) => x.ok === false),
207
+ turns: turns.length,
208
+ turnIds: turns.slice(),
209
+ };
191
210
  }
192
211
  }
193
212
 
@@ -25,8 +25,12 @@
25
25
  * Bash “끝나지 않는 것은 background” · 시간 초과로 죽는 것을 막는 자리
26
26
  * Verify“확인 못 한 것은 못 했다고” · 이 프로그램이 거짓말을 안 하게 하는 자리
27
27
  *
28
- * 도구 이름과 인자 이름은 **안 옮긴다.** 그건 식별자다. Task 목적·할일처럼
29
- * 한글로 인자 이름도 그대로 둔다 이름을 바꾸면 도구가 아예 안 불린다.
28
+ * 도구 이름과 인자 이름은 **안 옮긴다.** 그건 식별자다 이름을 바꾸면 그 도구가
29
+ * 아예 불린다. 여기 적힌 이름은 tools/*.js 설명서와 **글자까지 같아야**
30
+ * 한다. 다르면 그 인자만 영어 설명이 조용히 빠진다.
31
+ *
32
+ * 인자 이름은 전부 영문이다. 우리 취향이 아니라 서버가 검사한다 — Bedrock 은
33
+ * `^[a-zA-Z0-9_.-]{1,64}$` 가 아니면 첫 한마디를 통째로 400 으로 돌려보낸다.
30
34
  */
31
35
  export const 도구설명EN = {
32
36
  Read: {
@@ -134,14 +138,14 @@ export const 도구설명EN = {
134
138
  },
135
139
  },
136
140
  Ask: {
137
- desc: 'Ask the person one question at a genuine fork. **First fill in `이해` with what you'
141
+ desc: 'Ask the person one question at a genuine fork. **First fill in `understanding` with what you'
138
142
  + ' understood this request to be**, and only ask if something is genuinely left to decide.'
139
143
  + ' Never write "let me know" and stop; asking in prose ends the turn and throws away'
140
144
  + ' everything you have looked at. Give 2-4 options and they answer with a single number.'
141
145
  + ' **Never ask back something the user already said.** "Tidy up the files" is the answer —'
142
146
  + ' go and do it. Deciding how is your job, not theirs.',
143
147
  params: {
144
- 이해: 'one line: what you understood this request to be. The person reads this line to judge'
148
+ understanding: 'one line: what you understood this request to be. The person reads this line to judge'
145
149
  + ' whether you actually read them. "I understand your request" is not an understanding —'
146
150
  + ' name the actual work',
147
151
  question: 'one sentence. Make it clear what has to be decided',
@@ -197,14 +201,14 @@ export const 도구설명EN = {
197
201
  + ' not pile up in your window. That is why work that creates or edits several files has to be'
198
202
  + ' divided this way to get to the end. One chunk must be finishable on its own (e.g. "create'
199
203
  + ' index.html and style.css"). The subtask cannot see your conversation — put everything it'
200
- + ' needs into 할일. Do not use this for one short job. Doing it yourself is faster.',
204
+ + ' needs into `task`. Do not use this for one short job. Doing it yourself is faster.',
201
205
  params: {
202
- 목적: 'this chunk in one line (e.g. "build the dashboard page skeleton")',
203
- 할일: 'everything the subtask has to do. It cannot see this conversation, so put the background,'
206
+ purpose: 'this chunk in one line (e.g. "build the dashboard page skeleton")',
207
+ task: 'everything the subtask has to do. It cannot see this conversation, so put the background,'
204
208
  + ' the decisions, and the file paths here. Say what counts as done, too.',
205
- 모드: 'how the subtask works: code (builds and edits) · debug (finds causes) · ask (reads and'
209
+ mode: 'how the subtask works: code (builds and edits) · debug (finds causes) · ask (reads and'
206
210
  + ' answers only). Defaults to code.',
207
- 모델: 'hand this chunk to a **different model**. Only profile names the user has configured'
211
+ model: 'hand this chunk to a **different model**. Only profile names the user has configured'
208
212
  + ' work (do not invent an address — it will not be accepted). Left out, it stays on the model'
209
213
  + ' you are using. Handing routine work (formatting, repetitive edits, short summaries) to a'
210
214
  + ' small model keeps your window from filling. Do the work that needs judgement yourself.',
@@ -236,12 +240,13 @@ export const 도구설명EN = {
236
240
  },
237
241
  },
238
242
  Jobs: {
239
- desc: 'List, read, and end background commands (Bash with background). Called with no number,'
240
- + ' you get the list. Given a number, you get whatever output arrived since last time.'
241
- + ' If you started a server, you must end it when the job is done.',
243
+ desc: 'List, read, and end background commands (Bash with background). Called with no `job`,'
244
+ + ' you get the list. Given a `job`, you get whatever output arrived since last time.'
245
+ + ' If you started a server, you must end it with `stop` when the job is done.',
242
246
  params: {
243
- 번호: 'job number to look at. Left out, the list',
244
- 끝내기: 'true to end that job (stop)',
247
+ job: 'job number to look at. Left out, the list',
248
+ stop: 'true to end that job',
249
+ from_start: 'true to read from the beginning again',
245
250
  },
246
251
  },
247
252
  };