deel-local-cli 2.1.1 → 2.1.3

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.ko.md CHANGED
@@ -323,7 +323,7 @@ deel --offline
323
323
  무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
324
324
 
325
325
  ```
326
- deel 2.1.1 ⌂ 이 안
326
+ deel 2.1.3 ⌂ 이 안
327
327
  보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
328
328
  ```
329
329
 
@@ -1142,7 +1142,7 @@ AWS 키 · Google 키 · JWT · 주소에 박힌 열쇠 · `Authorization` 계
1142
1142
  | 모드 | 언제 물어보나 |
1143
1143
  |---|---|
1144
1144
  | `auto` (기본) | 안 물어봄 — **맡기는 모드**. 되돌리기가 안전망. 모델이 되묻거나 계획부터 내도 60초 안에 답이 없으면 알아서 진행(`askWait`) |
1145
- | `confirm` | 되돌릴 수 없는 명령만 |
1145
+ | `confirm` | 되돌릴 수 없는 명령, 그리고 MCP 도구(서버가 읽기만 한다고 적은 것은 빼고) |
1146
1146
  | `strict` | 파일 변경·명령 전부. **바뀔 내용(diff)을 먼저 보여 주고** 묻습니다 |
1147
1147
 
1148
1148
  회사에서 「바꾸기 전에 늘 사람이 본다」 를 정해야 하면 관리 정책에 `"approval": "strict"` 한 줄을
@@ -1151,7 +1151,7 @@ AWS 키 · Google 키 · JWT · 주소에 박힌 열쇠 · `Authorization` 계
1151
1151
 
1152
1152
  **「다 됐다」 는 검사가 정합니다.** 설정에 `"check": "npm test"` 를 적거나 `deel run --check` 로 주면,
1153
1153
  모델이 무엇을 바꾸고 끝내려 할 때 deel 이 그 검사를 **직접** 돌립니다. 실패하면 출력을 돌려주고 이어서
1154
- 고치게 하고(기본 3판), 끝내 실패면 실패로 끝냅니다 — `deel run` 은 종료코드 8. 검사 명령도 모델의
1154
+ 고치게 하고(검사는 기본 세 번까지), 끝내 실패면 실패로 끝냅니다 — `deel run` 은 종료코드 8. 검사 명령도 모델의
1155
1155
  명령과 같은 관문(규칙·훅·승인·위험 명령 차단)을 지납니다 — [완료 검사](docs/ko/company.md#완료-검사).
1156
1156
 
1157
1157
  **얼마나 해내나는 골든셋으로 잽니다.** `deel eval --init` 이 예제 과제 다섯을 만들고, `deel eval` 이
@@ -1333,7 +1333,9 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
1333
1333
 
1334
1334
  | 판 | 무엇이 바뀌었나 |
1335
1335
  |---|---|
1336
- | **[2.1.1](docs/ko/releases/2.1.md#211)** | 전체 검수에서 나온 것 — 한 칸 위를 물으면 `config explain` 이 열쇠를 보였고, 웹훅 주소·Sentry DSN 이 셸 환경 거르기와 가리기를 둘 다 지났고, `kill` 로 strict 를 지나 Jobs 서버를 껐고, Def·Refs 가 EUC-KR 을 못 읽었습니다 · `deel eval` 고침 |
1336
+ | **[2.1.3](docs/ko/releases/2.1.md#213)** | 제품 기능 — 도구를 글로 부르는 모델도 알아듣고, `deel run --continue`·`--resume`·`--events` 와 결과의 바뀐 파일(`files`)이 생겼고, git 저장소면 스크립트가 바꾼 것도 `/undo` 로 돌아가고, 언어 서버 없이도 문법을 보고, Outline 이 많이 불리는 파일부터 싣고, MCP 가 주소로 붙어 자료 · 프롬프트까지 씁니다 |
1337
+ | [2.1.2](docs/ko/releases/2.1.md#212) | 정책 · 작은 공백 · 속도 — confirm 이 읽기만 하지 않는 MCP 도구를 묻고, 에디터(ACP) 세션은 16개를 넘으면 내려놓았다가 다음 말에 되살리고, `--check-rounds`·`--check-timeout` 과 과제별 검사 설정이 `deel eval` 에 생겼고, 시작 때 읽는 파일이 164개에서 18개로 줄었습니다 |
1338
+ | [2.1.1](docs/ko/releases/2.1.md#211) | 전체 검수에서 나온 것 — 한 칸 위를 물으면 `config explain` 이 열쇠를 보였고, 웹훅 주소·Sentry DSN 이 셸 환경 거르기와 가리기를 둘 다 지났고, `kill` 로 strict 를 지나 Jobs 서버를 껐고, Def·Refs 가 EUC-KR 을 못 읽었습니다 · `deel eval` 고침 |
1337
1339
  | [2.1.0](docs/ko/releases/2.1.md#210) | 맡기면 멈추지 않고, 묻게 하면 diff 를 보여 줍니다 — 「다 됐다」 는 검사가 정합니다(`check`) · 얼마나 해내나는 골든셋으로 잽니다(`deel eval`) · 관리 정책 `"approval"` 로 회사가 승인 바닥을 겁니다 |
1338
1340
  | [2.0.3](docs/ko/releases/2.0.md#203) | 2.0.2 는 npm 에 못 나갔습니다 — 이제 GitHub 릴리스는 npm 에 올라간 뒤에만 생깁니다 |
1339
1341
  | [2.0.2](docs/ko/releases/2.0.md#202) | 2.0.1 까지 넘겨 둔 것을 다 잡았습니다 — `deel run` 에만 없던 MCP 도구, 끝난 명령의 번호로 가던 `taskkill`, 한 번 넘어지면 세션 내내 못 쓰던 MCP 서버 |
package/README.md CHANGED
@@ -396,7 +396,7 @@ deel --offline
396
396
  The destination is printed at the top of every session:
397
397
 
398
398
  ```
399
- deel 2.1.1 ⌂ inside
399
+ deel 2.1.3 ⌂ inside
400
400
  Sends to this machine 127.0.0.1:11434 ← nowhere else
401
401
  ```
402
402
 
@@ -1244,7 +1244,7 @@ does not ask.
1244
1244
  | Mode | Asks when |
1245
1245
  |---|---|
1246
1246
  | `auto` (default) | Never — **the hand-off mode**; undo is the safety net. If the model asks a question or offers a plan first, it carries on by itself after 60 seconds with no answer (`askWait`) |
1247
- | `confirm` | Irreversible commands only |
1247
+ | `confirm` | Irreversible commands, and MCP tools unless the server marks a tool read-only |
1248
1248
  | `strict` | All file changes and commands. **Shows the change (a diff) first**, then asks |
1249
1249
 
1250
1250
  If your organisation needs "a person sees every change before it lands", put `"approval": "strict"`
@@ -1253,7 +1253,7 @@ floor — see [Rolling it out at work](docs/en/company.md).
1253
1253
 
1254
1254
  **A check, not the model, says when it is done.** Put `"check": "npm test"` in the config or pass
1255
1255
  `deel run --check`, and when the model has changed something and tries to finish, deel runs that check
1256
- **itself**. A failure goes back to the model to keep fixing (3 rounds by default); still failing ends as
1256
+ **itself**. A failure goes back to the model to keep fixing (the check runs up to 3 times by default); still failing ends as
1257
1257
  failed — `deel run` exits with 8. The check goes through the same gates as the model's own commands
1258
1258
  (rules, hooks, approval, the dangerous-command guard) — see [Completion check](docs/en/company.md#completion-check).
1259
1259
 
@@ -1415,7 +1415,9 @@ so one run tells you everything.
1415
1415
 
1416
1416
  | Version | What changed |
1417
1417
  |---|---|
1418
- | **[2.1.1](docs/en/releases/2.1.md#211)** | From a full review — `config explain` one level up showed keys, webhook URLs and Sentry DSNs passed both the shell-env filter and masking, `kill` stopped a Jobs server past strict, Def/Refs could not read EUC-KR, and `deel eval` fixes |
1418
+ | **[2.1.3](docs/en/releases/2.1.md#213)** | Product features — models that write tool calls as text are understood, `deel run --continue`/`--resume`/`--events` and changed `files` in the result, `/undo` brings back what a script changed in a git repository, a syntax check without a language server, Outline loads the most imported files first, and MCP attaches by URL with resources and prompts |
1419
+ | [2.1.2](docs/en/releases/2.1.md#212) | Policy, small gaps and speed — confirm asks before MCP tools that are not read-only, editor (ACP) sessions past 16 are let go and brought back on the next message, `--check-rounds`/`--check-timeout` and per-task check settings in `deel eval`, and startup reads 18 files instead of 164 |
1420
+ | [2.1.1](docs/en/releases/2.1.md#211) | From a full review — `config explain` one level up showed keys, webhook URLs and Sentry DSNs passed both the shell-env filter and masking, `kill` stopped a Jobs server past strict, Def/Refs could not read EUC-KR, and `deel eval` fixes |
1419
1421
  | [2.1.0](docs/en/releases/2.1.md#210) | Hand it off and it keeps going; make it ask and it shows the diff — a check decides when it is done (`check`), a golden set measures how much gets done (`deel eval`), and `"approval"` in the managed policy lets a company set an approval floor |
1420
1422
  | [2.0.3](docs/en/releases/2.0.md#203) | 2.0.2 never reached npm — GitHub releases now appear only after npm has the version |
1421
1423
  | [2.0.2](docs/en/releases/2.0.md#202) | Everything 2.0.1 left open, closed — MCP tools missing only from `deel run`, `taskkill` sent to an exited command's pid, MCP servers that stayed down for the session after one crash |
package/bin/deel.js CHANGED
@@ -3,27 +3,22 @@
3
3
  import { join, resolve } from 'node:path';
4
4
  import { readFileSync, writeFileSync, statSync } from 'node:fs';
5
5
  import { c, say, mark, rule, clip, width } from '../src/ui/ansi.js';
6
- import { runSetup, runDiagnose, showStatus, banner } from '../src/setup.js';
7
- import { chatLoop } from '../src/repl.js';
8
- import { runOnce, EXIT, 실패덩이 } from '../src/oneshot.js';
9
- import { packSelf, audit, reviewSheet } from '../src/pack/selfpack.js';
10
- import { sbom, 심사명세, 명세요약 } from '../src/pack/sbom.js';
11
- import { runScan } from '../src/backend/scanui.js';
12
- import { closeConnections } from '../src/backend/http.js';
13
- import { parseSize } from '../src/backend/ctxsize.js';
14
- import { runSessions } from '../src/agent/sessionui.js';
15
- import { acp } from '../src/acp/serve.js';
16
- import { runCompletion } from '../src/completion.js';
17
- import { runReset } from '../src/reset.js';
18
- import { 마크다운, 읽는갈래 } from '../src/tools/doc2md.js';
6
+ import { banner } from '../src/ui/banner.js';
7
+ import { EXIT, 실패덩이, closeConnections } from '../src/exit.js';
19
8
  import { 언어잡기, 언어, 말 } from '../src/i18n/index.js';
20
9
  import { 믿기, 안믿기, 믿는목록, 프로젝트금지칸, 너무넓은자리 as 넓은자리 } from '../src/safety/trust.js';
21
10
  import { load as 설정읽기, homeDir, 이PC설정값 } from '../src/config.js';
22
11
  import { 규칙모으기, 어떻게할까, 확인목록, 확인인자, 확인돌리기 } from '../src/safety/policy.js';
23
12
  import { 기록자리, 세기, 도구차례, 막힘차례, 셈JSON } from '../src/stats.js';
24
- import { 진찰 } from '../src/doctor.js';
25
- import { 설명, 설명줄들 } from '../src/configexplain.js';
26
- import { runEval } from '../src/eval/run.js';
13
+
14
+ /*
15
+ * ── 무거운 것은 그 명령을 칠 때 불러온다 (2.1.2) ─────────────────────────
16
+ *
17
+ * 여기 맨 위에서 대화 화면·한 번 돌리기·에디터 서버·평가·반입 묶음을 다 불러와, `deel --version`
18
+ * 도 `deel trust` 도 파일 164개(4.4MB)를 읽고 나서야 첫 줄을 냈다. 맨 위에는 모든 명령이 쓰는
19
+ * 가벼운 것(화면 글·말·설정·신뢰·규칙)만 두고, 나머지는 쓰는 자리에서 `await import()` 한다.
20
+ * test/cli.test.js 가 맨 위에서 닿는 파일 수를 못 박는다.
21
+ */
27
22
 
28
23
  const MIN_NODE = 20;
29
24
 
@@ -60,7 +55,8 @@ function 말정하기() {
60
55
  }
61
56
 
62
57
  // 사내 반입용 묶음 만들기.
63
- function runPack(flags) {
58
+ async function runPack(flags) {
59
+ const { packSelf } = await import('../src/pack/selfpack.js');
64
60
  const 한국어 = 언어() === 'ko';
65
61
  const out = flags.out ? String(flags.out) : join(process.cwd(), 한국어 ? 'deel-반입.zip' : 'deel-import.zip');
66
62
  const r = packSelf(out);
@@ -112,7 +108,7 @@ function runPack(flags) {
112
108
  * 적어 놨는데 안 먹는다」 가 흔하다. 그때 사람은 그 파일만 고치다가 설정이라는
113
109
  * 것을 안 믿게 된다. 층을 열어 보여 주면 5초에 끝나는 일이다.
114
110
  */
115
- function runConfig(args, flags) {
111
+ async function runConfig(args, flags) {
116
112
  const 무엇 = String(args[0] ?? '');
117
113
  const 칸 = String(args[1] ?? '');
118
114
  if (무엇 !== 'explain' || !칸) {
@@ -122,6 +118,7 @@ function runConfig(args, flags) {
122
118
  say('');
123
119
  return 1;
124
120
  }
121
+ const { 설명, 설명줄들 } = await import('../src/configexplain.js');
125
122
  const r = 설명(칸, { root: flags.root ? String(flags.root) : process.cwd() });
126
123
  /*
127
124
  * 찾았나 못 찾았나를 **끝값으로도** 말한다.
@@ -161,6 +158,7 @@ async function runDoctor(flags) {
161
158
  await import('../src/config.js');
162
159
  const { 지금모드, 바깥인가, 나갈수있나, 봉인됐나 } = await import('../src/safety/runmode.js');
163
160
  const { allowEndpoint } = await import('../src/safety/network.js');
161
+ const { 진찰 } = await import('../src/doctor.js');
164
162
 
165
163
  banner();
166
164
  let cfg = null;
@@ -514,7 +512,8 @@ function runRules(args, flags) {
514
512
  }
515
513
 
516
514
  // 묶지 않고 심사 내용만 보기.
517
- function runAudit() {
515
+ async function runAudit() {
516
+ const { audit, reviewSheet } = await import('../src/pack/selfpack.js');
518
517
  say('');
519
518
  say(reviewSheet(audit(), new Date().toISOString().replace('T', ' ').slice(0, 19)));
520
519
  return 0;
@@ -536,7 +535,8 @@ function runAudit() {
536
535
  * `--out` 이 없으면 표준출력으로 낸다. 파이프에 바로 물리는 자리라
537
536
  * 여기서는 say() 를 안 쓴다 (completion 과 같은 규칙이다).
538
537
  */
539
- function runDoc2md(args, flags) {
538
+ async function runDoc2md(args, flags) {
539
+ const { 마크다운, 읽는갈래 } = await import('../src/tools/doc2md.js');
540
540
  const 파일 = args[0];
541
541
  if (!파일) {
542
542
  say(` ${mark.warn} 바꿀 파일을 주세요: ${c.white('deel doc2md 보고서.pptx')}`);
@@ -568,7 +568,7 @@ function runDoc2md(args, flags) {
568
568
  return 0;
569
569
  }
570
570
 
571
- function runSbom(flags) {
571
+ async function runSbom(flags) {
572
572
  const 어느것 = String(flags.only ?? '').toLowerCase();
573
573
  /*
574
574
  * 모르는 `--only` 는 멈춘다.
@@ -581,6 +581,8 @@ function runSbom(flags) {
581
581
  process.stderr.write(`\n --only 에는 sbom · 명세(spec) 중 하나를 주세요: ${String(flags.only)}\n\n`);
582
582
  return EXIT.usage;
583
583
  }
584
+ const { audit } = await import('../src/pack/selfpack.js');
585
+ const { sbom, 심사명세, 명세요약 } = await import('../src/pack/sbom.js');
584
586
  const a = audit();
585
587
  const at = new Date();
586
588
  const 낼것 = 어느것 === 'sbom' ? sbom(a, { at })
@@ -651,6 +653,10 @@ const 값깃발 = new Set([
651
653
  'out', 'only', 'days', 'tool', 'url', 'key', 'model', 'host', 'ports', 'timeout', 'rm', 'delete',
652
654
  // run --check <명령> (agent/donecheck.js) · eval --repeat <수> (src/eval/run.js).
653
655
  'check', 'repeat',
656
+ // run --check-rounds <수> · --check-timeout <초> — 설정의 checkRounds · checkTimeout 을 이 실행에서만 덮는다 (2.1.2).
657
+ 'check-rounds', 'check-timeout',
658
+ // run --events <파일> — 사건을 한 줄에 하나씩 JSON 으로 흘린다 (2.1.3 · src/runlog.js).
659
+ 'events',
654
660
  ]);
655
661
  // 값을 줘도 되고 안 줘도 되는 깃발. `--resume` 만 치면 이어 할 대화를 고른다.
656
662
  const 값골라깃발 = new Set(['resume']);
@@ -909,6 +915,9 @@ function help() {
909
915
  say(` ${c.gray('--yes')} 승인이 필요한 것도 그냥 실행. ${c.yellow('기본은 거부입니다')}`);
910
916
  // 새로 넣는 도움말 줄은 말() 로 적는다 — 영어로 켠 사람에게 한국어 줄을 더 새게 하지 않는다(test/langleak.test.js 의 래칫).
911
917
  say(` ${c.gray('--check <cmd>')} ${말('cli.checkFlag')}`);
918
+ say(` ${c.gray('--check-rounds <n> · --check-timeout <s>')} ${말('cli.checkRoundsFlag')}`);
919
+ say(` ${c.gray('--continue · --resume <id>')} ${말('cli.runResumeFlag')}`);
920
+ say(` ${c.gray('--events <file>')} ${말('cli.eventsFlag')}`);
912
921
  /*
913
922
  * 답의 모양을 못 박는 자리 (src/agent/outschema.js).
914
923
  *
@@ -952,6 +961,19 @@ async function main() {
952
961
  읽은인자 = 읽음;
953
962
  const { cmd: 친명령, args, flags, 탈: 인자탈들 } = 읽음;
954
963
 
964
+ /*
965
+ * 판 번호.
966
+ *
967
+ * 연결이 없어도 답해야 한다. `deel --version` 은 "이게 깔려 있나, 무슨 판인가"
968
+ * 를 묻는 것이지 일을 시키는 것이 아닌데, 전에는 설정이 없다는 말이 먼저 나와서
969
+ * 깔린 것 자체가 아닌 줄 알았다. 사내에 반입한 판을 확인할 때 제일 먼저 치는
970
+ * 명령이기도 하다.
971
+ *
972
+ * 그리고 **설정을 읽기 전에** 답한다 (2.1.2). 설정 읽기는 평문 열쇠를 잠그러 옮기며
973
+ * PowerShell 을 띄우고 설정 파일을 고쳐 쓸 수 있다 — 「무슨 판인가」 를 물은 한 줄이 할 일이 아니다.
974
+ */
975
+ if (flags.version || 친명령 === 'version') { say(판번호()); return 0; }
976
+
955
977
  // 서류를 뽑는 명령(audit·sbom·pack)이 어느 말로 나갈지를 여기서 정한다.
956
978
  말정하기();
957
979
 
@@ -966,16 +988,6 @@ async function main() {
966
988
  ? (flags[친명령] = true, '')
967
989
  : 친명령;
968
990
 
969
- /*
970
- * 판 번호.
971
- *
972
- * 연결이 없어도 답해야 한다. `deel --version` 은 "이게 깔려 있나, 무슨 판인가"
973
- * 를 묻는 것이지 일을 시키는 것이 아닌데, 전에는 설정이 없다는 말이 먼저 나와서
974
- * 깔린 것 자체가 아닌 줄 알았다. 사내에 반입한 판을 확인할 때 제일 먼저 치는
975
- * 명령이기도 하다.
976
- */
977
- if (flags.version || cmd === 'version') { say(판번호()); return 0; }
978
-
979
991
  if (flags.help || cmd === 'help') { help(); return 0; }
980
992
 
981
993
  /*
@@ -1091,6 +1103,9 @@ async function main() {
1091
1103
  * 거절한다. 같은 값을 문에 따라 다르게 받으면, 깃발로 준 사람만 제 값이 먹는 줄 안다.
1092
1104
  * `auto` 는 「서버에 맞춤」 이라는 뜻이라 안 준 것으로 받는다.
1093
1105
  */
1106
+ const parseSize = flags.ctx !== undefined || flags['max-tokens'] !== undefined
1107
+ ? (await import('../src/backend/ctxsize.js')).parseSize
1108
+ : () => null;
1094
1109
  for (const 이름 of ['ctx', 'max-tokens']) {
1095
1110
  if (flags[이름] === undefined) continue;
1096
1111
  if (String(flags[이름]).trim().toLowerCase() === 'auto') { delete flags[이름]; continue; }
@@ -1109,7 +1124,7 @@ async function main() {
1109
1124
  // deel -p "..." 처럼 치면 그대로 여기로 온다.
1110
1125
  case 'run':
1111
1126
  case '-p':
1112
- return runOnce({
1127
+ return (await import('../src/oneshot.js')).runOnce({
1113
1128
  prompt: args.join(' '),
1114
1129
  root: flags.root ? String(flags.root) : undefined,
1115
1130
  mode: flags.mode ? String(flags.mode) : undefined,
@@ -1128,10 +1143,16 @@ async function main() {
1128
1143
  outputSchema: flags['output-schema'] ? String(flags['output-schema']) : undefined,
1129
1144
  // 끝내려는 자리에서 돌릴 검사 (agent/donecheck.js). 설정의 check 를 이긴다.
1130
1145
  check: flags.check !== undefined ? String(flags.check) : undefined,
1146
+ checkRounds: flags['check-rounds'] !== undefined ? String(flags['check-rounds']) : undefined,
1147
+ checkTimeout: flags['check-timeout'] !== undefined ? String(flags['check-timeout']) : undefined,
1148
+ // 대화를 잇는다 (2.1.3). 값 없는 --resume 은 true 로 넘어가 runOnce 가 64 로 세운다 — 여기는 고를 사람이 없다.
1149
+ continue: flags.continue === true,
1150
+ resume: flags.resume,
1151
+ events: flags.events !== undefined ? String(flags.events) : undefined,
1131
1152
  });
1132
1153
  case '':
1133
1154
  case 'chat':
1134
- return chatLoop({
1155
+ return (await import('../src/repl.js')).chatLoop({
1135
1156
  root: flags.root ? String(flags.root) : undefined,
1136
1157
  mode: flags.mode ? String(flags.mode) : undefined,
1137
1158
  work: flags.work ? String(flags.work) : undefined,
@@ -1160,7 +1181,7 @@ async function main() {
1160
1181
  * 왜 아무 반응이 없는지는 표준오류에 적어 둔다.
1161
1182
  */
1162
1183
  case 'acp':
1163
- return acp({
1184
+ return (await import('../src/acp/serve.js')).acp({
1164
1185
  root: flags.root ? String(flags.root) : undefined,
1165
1186
  mode: flags.mode ? String(flags.mode) : undefined,
1166
1187
  work: flags.work ? String(flags.work) : undefined,
@@ -1173,11 +1194,11 @@ async function main() {
1173
1194
  online: flags.online === true || flags.online === 'true',
1174
1195
  });
1175
1196
  case 'status':
1176
- return showStatus();
1197
+ return (await import('../src/setup.js')).showStatus();
1177
1198
  case 'setup':
1178
- return runSetup(flags);
1199
+ return (await import('../src/setup.js')).runSetup(flags);
1179
1200
  case 'diagnose':
1180
- return runDiagnose(flags);
1201
+ return (await import('../src/setup.js')).runDiagnose(flags);
1181
1202
  /*
1182
1203
  * doctor 는 diagnose 의 **앞자락**이다.
1183
1204
  *
@@ -1203,14 +1224,14 @@ async function main() {
1203
1224
  return runDoc2md(args, flags);
1204
1225
  // 과제 모음(골든셋)을 실제 모델로 돌려 성공률을 잰다 (src/eval/run.js).
1205
1226
  case 'eval':
1206
- return runEval({
1227
+ return (await import('../src/eval/run.js')).runEval({
1207
1228
  폴더: args[0] ?? null,
1208
1229
  init: flags.init === true || flags.init === 'true',
1209
1230
  repeat: flags.repeat ?? 1,
1210
1231
  keep: flags.keep === true || flags.keep === 'true',
1211
1232
  json: flags.json === true || flags.json === 'true',
1212
1233
  only: flags.only !== undefined ? String(flags.only) : null,
1213
- 시간초: flags.timeout !== undefined ? Number(flags.timeout) : null,
1234
+ 시간초: flags.timeout !== undefined ? flags.timeout : null,
1214
1235
  // 과제마다 띄우는 `deel run` 에 그대로 넘길 것 — 바깥 게이트웨이면 --online 이 있어야 돈다.
1215
1236
  깃발들: [
1216
1237
  ...(flags.online === true || flags.online === 'true' ? ['--online'] : []),
@@ -1222,10 +1243,10 @@ async function main() {
1222
1243
  case 'sbom':
1223
1244
  return runSbom(flags);
1224
1245
  case 'scan':
1225
- return runScan(flags);
1246
+ return (await import('../src/backend/scanui.js')).runScan(flags);
1226
1247
  case 'sessions':
1227
1248
  case 'ls':
1228
- return runSessions(flags);
1249
+ return (await import('../src/agent/sessionui.js')).runSessions(flags);
1229
1250
  /*
1230
1251
  * 초기화.
1231
1252
  *
@@ -1233,7 +1254,7 @@ async function main() {
1233
1254
  * 깨져서인데, 읽고 시작하면 그 자리에서 못 넘어간다.
1234
1255
  */
1235
1256
  case 'reset':
1236
- return runReset(args, flags);
1257
+ return (await import('../src/reset.js')).runReset(args, flags);
1237
1258
  /*
1238
1259
  * 탭 완성 스크립트를 낸다 (src/completion.js).
1239
1260
  *
@@ -1241,7 +1262,7 @@ async function main() {
1241
1262
  * 처럼 바로 파이프에 물릴 수 있어야 한다. 그래서 여기서는 say() 를 안 쓴다.
1242
1263
  */
1243
1264
  case 'completion':
1244
- return runCompletion(args);
1265
+ return (await import('../src/completion.js')).runCompletion(args);
1245
1266
  default:
1246
1267
  say('');
1247
1268
  say(` ${c.red('모르는 명령')} ${c.bold(cmd)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "A coding-agent CLI that runs on local models and private gateways. Zero dependencies, Node 20+.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/acp/serve.js CHANGED
@@ -194,6 +194,29 @@ export async function acp(opts = {}) {
194
194
  * 폴더가 다르면 따로 띄운다. 옆 프로젝트의 도구가 딸려 오면 안 된다.
195
195
  */
196
196
  const mcp캐시 = new Map();
197
+
198
+ /*
199
+ * ── 방은 쌓이기만 했다 (2.1.2) ────────────────────────────────────────────
200
+ *
201
+ * 에디터는 대화마다 session/new 를 부르고, 이 프로세스는 에디터가 켜져 있는 동안 산다. 방을
202
+ * 지우는 자리가 없어 하루 종일 연 대화가 전부 메모리에 남았다 — 대화 전체 · 되돌리기 기록 ·
203
+ * 감사기록 · 그 폴더의 MCP 서버까지.
204
+ *
205
+ * 상한을 넘으면 제일 오래 안 쓴 **쉬는** 방을 내려놓는다(도는 턴·차례를 기다리는 턴이 있으면
206
+ * 안 건드린다). 대화 파일은 폴더에 남아 있으므로 그 이름으로 다시 말이 오면 되살려 잇는다
207
+ * (방되찾기) — 에디터 탭은 닫힌 적이 없으니 「그런 세션이 없습니다」 로 답하면 안 된다. 에디터가
208
+ * 고른 작업 모드와 「앞으로 묻지 않기」 도 같이 들고 있다가 돌려준다. 그 폴더의 마지막 방이면
209
+ * MCP 서버도 닫는다.
210
+ */
211
+ const 최대방 = (() => {
212
+ const n = Number(process.env.DEEL_ACP_MAX_SESSIONS);
213
+ return Number.isInteger(n) && n >= 1 ? n : 16;
214
+ })();
215
+ const 치운방 = new Map(); // 아이디 → { root, work, 늘허락 }
216
+ const 되찾는중 = new Map(); // 아이디 → 되살리는 약속 (겹쳐 온 말이 둘을 짓지 않게)
217
+ // 마지막으로 쓴 차례. 시각(Date.now)으로 적으면 한 밀리초 안에 쓴 두 방이 같은 값이 되어, 오래 안 쓴
218
+ // 방 대신 먼저 연 방이 내려놓였다 — 늘 커지는 번호로 적는다.
219
+ let 쓴셈 = 0;
197
220
  let 클라이언트 = null; // initialize 로 받은 저쪽 소개
198
221
  let 시작했나 = false;
199
222
 
@@ -528,6 +551,8 @@ export async function acp(opts = {}) {
528
551
  const 방 = {
529
552
  id: store.id,
530
553
  root, conn, session, store,
554
+ 캐시열쇠,
555
+ 쓴때: ++쓴셈,
531
556
  턴: null,
532
557
  늘허락: new Set(), // 이 세션에서 "앞으로 묻지 않기" 를 고른 도구들
533
558
  mcp: mcp붙임.서버들,
@@ -604,13 +629,70 @@ export async function acp(opts = {}) {
604
629
  } catch { /* 설정값 그대로 간다 */ }
605
630
  }
606
631
 
632
+ /*
633
+ * 내려놓았던 방이면(방되찾기 · session/load) 에디터가 고른 작업 모드와 「앞으로 묻지 않기」 를
634
+ * 돌려준다. **방들 에 넣기 전에** 채운다 — session/load 로 연 방은 이것을 잃었고(2.1.2 2차 눈),
635
+ * 넣은 뒤에 채우면 그 사이 온 말이 빈 채인 방을 받는다.
636
+ */
637
+ const 치운것 = 치운방.get(방.id);
638
+ if (치운것) {
639
+ if (치운것.work) 방.session.work = 치운것.work;
640
+ for (const 이름 of 치운것.늘허락 ?? []) 방.늘허락.add(이름);
641
+ }
607
642
  방들.set(방.id, 방);
643
+ 치운방.delete(방.id); // 되살아났으면 내려놓은 표는 지운다
644
+ 방솎기(방);
608
645
  return 방;
609
646
  }
610
647
 
648
+ /** 상한을 넘었으면 제일 오래 안 쓴 쉬는 방부터 내려놓는다 (위 최대방 머리말). */
649
+ function 방솎기(새방) {
650
+ while (방들.size > 최대방) {
651
+ let 고른 = null;
652
+ for (const 방 of 방들.values()) {
653
+ if (방 === 새방 || 방.턴 || 방.턴끝) continue;
654
+ if (!고른 || 방.쓴때 < 고른.쓴때) 고른 = 방;
655
+ }
656
+ if (!고른) return; // 다 돌고 있으면 넘긴 채 둔다 — 도는 턴을 끊지 않는다
657
+ 방들.delete(고른.id);
658
+ 치운방.set(고른.id, { root: 고른.root, work: 고른.session.work, 늘허락: 고른.늘허락 });
659
+ 로그(`${고른.id} — 오래 안 쓴 세션을 내려놓았습니다 (열어 두는 세션 ${최대방}개 상한). 다시 말하면 이어서 엽니다.`);
660
+ /*
661
+ * 그 폴더의 MCP 서버는 **쓰는 방이 하나도 없을 때만** 닫는다. 짓는 중인 방(열린자리 에는 있고
662
+ * 방들 에는 아직 없는 것)도 쓰는 방이다 — 붙임 약속을 이미 받아 갔다.
663
+ */
664
+ const 아직씀 = [...열린자리].some(([id, 자리]) => !치운방.has(id) && 자리열쇠(자리) === 고른.캐시열쇠);
665
+ if (!아직씀 && mcp캐시.has(고른.캐시열쇠)) {
666
+ const 붙임 = mcp캐시.get(고른.캐시열쇠);
667
+ mcp캐시.delete(고른.캐시열쇠);
668
+ Promise.resolve(붙임).then((b) => {
669
+ for (const 서버 of b?.서버들 ?? []) { try { 서버.닫기(); } catch { /* 닫다 터져도 방은 내려놓는다 */ } }
670
+ }).catch(() => {});
671
+ }
672
+ }
673
+ }
674
+
611
675
  function 방찾기(id) {
612
676
  const 방 = 방들.get(String(id ?? ''));
613
677
  if (!방) throw 잘못된인자오류(`그런 세션이 없습니다: ${id}`);
678
+ 방.쓴때 = ++쓴셈; // 내려놓을 차례는 연 때가 아니라 마지막으로 쓴 때로 정한다
679
+ return 방;
680
+ }
681
+
682
+ /** 내려놓은 방이면 파일에서 되살린다. 아니면 방찾기 와 같다. */
683
+ async function 방되찾기(id) {
684
+ const 아이디 = String(id ?? '');
685
+ if (방들.has(아이디) || !치운방.has(아이디)) return 방찾기(아이디);
686
+ if (!되찾는중.has(아이디)) {
687
+ const 적힌것 = 치운방.get(아이디);
688
+ 되찾는중.set(아이디, (async () => {
689
+ const 방 = await 방만들기({ cwd: 적힌것.root }, { 아이디 }); // 모드 · 늘허락 은 방짓기 가 돌려준다
690
+ 로그(`${아이디} — 내려놓았던 세션을 다시 열었습니다 (${방.session.work})`);
691
+ return 방;
692
+ })().finally(() => 되찾는중.delete(아이디)));
693
+ }
694
+ const 방 = await 되찾는중.get(아이디);
695
+ 방.쓴때 = ++쓴셈;
614
696
  return 방;
615
697
  }
616
698
 
@@ -903,7 +985,7 @@ export async function acp(opts = {}) {
903
985
  // 완료 검사 (agent/donecheck.js). 에디터에도 무엇을 돌렸고 어떻게 됐는지 적는다.
904
986
  case 'check_start':
905
987
  말하기(`\n\n_(${옮긴말('check.start', { 판: ev.판, 최대: ev.최대, 명령: ev.명령 })})_\n\n`);
906
- for (const 줄 of ev.줄임 ?? []) 말하기(`_(${옮긴말('check.clamped', 줄)})_\n\n`);
988
+ for (const 줄 of ev.줄임 ?? []) 말하기(`_(${옮긴말(줄.틀림 ? 'check.invalid' : 'check.clamped', 줄)})_\n\n`);
907
989
  break;
908
990
 
909
991
  case 'check':
@@ -912,6 +994,10 @@ export async function acp(opts = {}) {
912
994
  : 옮긴말('check.skipped', { 까닭: ev.까닭 })})_\n\n`);
913
995
  break;
914
996
 
997
+ case 'textcalls':
998
+ 말하기(`\n\n_(${옮긴말('ev.textCalls', { n: ev.count, 이름: ev.names.join(', ') })})_\n\n`);
999
+ break;
1000
+
915
1001
  case 'nudge':
916
1002
  말하기(`\n\n_(${ev.why === '요청누락'
917
1003
  ? 옮긴말('ev.nudgeMissed', { n: ev.빠진?.length ?? 0 })
@@ -1231,7 +1317,7 @@ export async function acp(opts = {}) {
1231
1317
  }
1232
1318
 
1233
1319
  case 'session/prompt': {
1234
- const 방 = 방찾기(인자?.sessionId);
1320
+ const 방 = await 방되찾기(인자?.sessionId); // 내려놓은 방이면 파일에서 되살려 잇는다
1235
1321
  return await 한턴(방, 인자?.prompt);
1236
1322
  }
1237
1323
 
@@ -1243,7 +1329,7 @@ export async function acp(opts = {}) {
1243
1329
  }
1244
1330
 
1245
1331
  case 'session/set_mode': {
1246
- const 방 = 방찾기(인자?.sessionId);
1332
+ const 방 = await 방되찾기(인자?.sessionId);
1247
1333
  const 준것 = String(인자?.modeId ?? '');
1248
1334
  const 고른것 = 모드정리(준것);
1249
1335
  /*
@@ -32,7 +32,10 @@
32
32
  // 고친 뒤 실패를 받고 **다시 아무것도 안 바꾼 채** 끝내려 하면 다시 돌리지 않는다 —
33
33
  // 같은 결과가 나올 것을 또 기다리게 하지 않고, 실패로 끝낸다.
34
34
 
35
- /** 한 턴에 몇 번까지 「고치고 → 다시 검사」 를 돌까. 기본과 상한. */
35
+ /**
36
+ * 한 턴에 검사를 **몇 번 돌리나**(checkRounds). 기본과 상한. 실패하면 마지막 판을 빼고 출력을 모델에게
37
+ * 돌려주므로 고칠 기회는 판수 − 1 이다 — 3 이면 처음 검사 뒤 두 번 더 고쳐 본다.
38
+ */
36
39
  export const 기본판수 = 3;
37
40
  export const 최대판수 = 10;
38
41
  /** 검사 한 번의 제한 시간(ms). Bash 도구의 상한(10분)과 같다 — 넘겨 봐야 거기서 잘린다. */
@@ -53,11 +56,16 @@ export function 검사설정(cfg = {}, 덮을명령 = undefined) {
53
56
  if (!명령) return null;
54
57
  /*
55
58
  * 판수는 1 ~ 10 에서만 받는다. 0 이나 음수를 「끈다」 로 읽으면 명령을 적어 둔 사람이
56
- * 왜 안 도는지 모른다. 끄려면 check 를 지운다. 숫자가 아니면 기본값이다.
59
+ * 왜 안 도는지 모른다. 끄려면 check 를 지운다. 못 쓰는 값이면 기본값이다.
60
+ *
61
+ * 수와 수로 된 글("5")만 수로 읽는다. 여태 Number() 를 그대로 써서 true 가 1 이 되어 고칠 기회
62
+ * 없이 한 번에 끝났다. 못 쓰는 값은 **기본값을 썼다고 적는다** (아래 줄임 · 2.1.2).
57
63
  */
58
- const n = Number(cfg?.checkRounds);
64
+ const 수로 = (v) => (typeof v === 'number' || (typeof v === 'string' && v.trim() !== '') ? Number(v) : NaN);
65
+ const 적었나 = (v) => v !== undefined && v !== null;
66
+ const n = 수로(cfg?.checkRounds);
59
67
  const 판수 = Number.isInteger(n) && n >= 1 ? Math.min(n, 최대판수) : 기본판수;
60
- const 초 = Number(cfg?.checkTimeout);
68
+ const 초 = 수로(cfg?.checkTimeout);
61
69
  const 시간 = Number.isFinite(초) && 초 > 0 ? Math.min(기본시간, Math.max(최소시간, Math.round(초 * 1000))) : 기본시간;
62
70
  /*
63
71
  * 상한을 넘겨 **줄인 것**은 적어 둔다 — 첫 검사 시작 사건에 실려 화면이 말한다.
@@ -67,6 +75,13 @@ export function 검사설정(cfg = {}, 덮을명령 = undefined) {
67
75
  const 줄임 = [];
68
76
  if (Number.isInteger(n) && n > 최대판수) 줄임.push({ 칸: 'checkRounds', 준값: n, 쓴값: 최대판수 });
69
77
  if (Number.isFinite(초) && 초 * 1000 > 기본시간) 줄임.push({ 칸: 'checkTimeout', 준값: 초, 쓴값: 기본시간 / 1000 });
78
+ // 못 쓰는 값 — 상한과 같은 자리로 말하되 「넘었다」 가 아니라 「못 쓴다」 로 (틀림).
79
+ if (적었나(cfg?.checkRounds) && !(Number.isInteger(n) && n >= 1)) {
80
+ 줄임.push({ 칸: 'checkRounds', 준값: JSON.stringify(cfg.checkRounds), 쓴값: 기본판수, 틀림: true });
81
+ }
82
+ if (적었나(cfg?.checkTimeout) && !(Number.isFinite(초) && 초 > 0)) {
83
+ 줄임.push({ 칸: 'checkTimeout', 준값: JSON.stringify(cfg.checkTimeout), 쓴값: 기본시간 / 1000, 틀림: true });
84
+ }
70
85
  return { 명령, 판수, 시간, 줄임 };
71
86
  }
72
87