deel-local-cli 2.1.4 → 2.1.5

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.4 ⌂ 이 안
326
+ deel 2.1.5 ⌂ 이 안
327
327
  보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
328
328
  ```
329
329
 
@@ -1341,7 +1341,8 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
1341
1341
 
1342
1342
  | 판 | 무엇이 바뀌었나 |
1343
1343
  |---|---|
1344
- | **[2.1.4](docs/ko/releases/2.1.md#214)** | 브라우저로 로그인 — `deel setup` 3번이 열쇠 없이 OpenRouter 계정 로그인(OAuth PKCE)으로 붙습니다. 열쇠 하나로 Claude · GPT · Grok · Kimi. SSH 는 `--no-browser`, `sk-or-` 열쇠를 알아보고, 긴 모델 목록은 이름으로 좁힙니다 |
1344
+ | **[2.1.5](docs/ko/releases/2.1.md#215)** | 급한 보안 수정 — 셸 명령이 작업 폴더 밖으로 새던 자리를 막았습니다. 빗금 없는 `..`(`find .. -delete` · `rd /s /q ..`) · `$PWD` · `%CD%` · `$OLDPWD` · 먼저 옮긴 적 없는 `cd -` · `C:..` · 파워셸의 다른 이름(`ri` · `del` …) |
1345
+ | [2.1.4](docs/ko/releases/2.1.md#214) | 브라우저로 로그인 — `deel setup` 3번이 열쇠 없이 OpenRouter 계정 로그인(OAuth PKCE)으로 붙습니다. 열쇠 하나로 Claude · GPT · Grok · Kimi. SSH 는 `--no-browser`, `sk-or-` 열쇠를 알아보고, 긴 모델 목록은 이름으로 좁힙니다 |
1345
1346
  | [2.1.3](docs/ko/releases/2.1.md#213) | 제품 기능 — 도구를 글로 부르는 모델도 알아듣고, `deel run --continue`·`--resume`·`--events` 와 결과의 바뀐 파일(`files`)이 생겼고, git 저장소면 스크립트가 바꾼 것도 `/undo` 로 돌아가고, 언어 서버 없이도 문법을 보고, Outline 이 많이 불리는 파일부터 싣고, MCP 가 주소로 붙어 자료 · 프롬프트까지 씁니다 |
1346
1347
  | [2.1.2](docs/ko/releases/2.1.md#212) | 정책 · 작은 공백 · 속도 — confirm 이 읽기만 하지 않는 MCP 도구를 묻고, 에디터(ACP) 세션은 16개를 넘으면 내려놓았다가 다음 말에 되살리고, `--check-rounds`·`--check-timeout` 과 과제별 검사 설정이 `deel eval` 에 생겼고, 시작 때 읽는 파일이 164개에서 18개로 줄었습니다 |
1347
1348
  | [2.1.1](docs/ko/releases/2.1.md#211) | 전체 검수에서 나온 것 — 한 칸 위를 물으면 `config explain` 이 열쇠를 보였고, 웹훅 주소·Sentry DSN 이 셸 환경 거르기와 가리기를 둘 다 지났고, `kill` 로 strict 를 지나 Jobs 서버를 껐고, Def·Refs 가 EUC-KR 을 못 읽었습니다 · `deel eval` 고침 |
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.4 ⌂ inside
399
+ deel 2.1.5 ⌂ inside
400
400
  Sends to this machine 127.0.0.1:11434 ← nowhere else
401
401
  ```
402
402
 
@@ -1424,7 +1424,8 @@ so one run tells you everything.
1424
1424
 
1425
1425
  | Version | What changed |
1426
1426
  |---|---|
1427
- | **[2.1.4](docs/en/releases/2.1.md#214)** | Log in with a browser — `deel setup` option 3 connects to OpenRouter by logging in (OAuth PKCE), no key to paste; one key for Claude, GPT, Grok and Kimi. `--no-browser` for SSH, `sk-or-` keys recognized, and long model lists narrow by name |
1427
+ | **[2.1.5](docs/en/releases/2.1.md#215)** | Urgent security fix — shell commands could reach outside the working folder. Now blocked: a bare `..` (`find .. -delete`, `rd /s /q ..`), `$PWD`, `%CD%`, `$OLDPWD`, a `cd -` with no earlier move, `C:..`, and PowerShell aliases (`ri`, `del` …) |
1428
+ | [2.1.4](docs/en/releases/2.1.md#214) | Log in with a browser — `deel setup` option 3 connects to OpenRouter by logging in (OAuth PKCE), no key to paste; one key for Claude, GPT, Grok and Kimi. `--no-browser` for SSH, `sk-or-` keys recognized, and long model lists narrow by name |
1428
1429
  | [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 |
1429
1430
  | [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 |
1430
1431
  | [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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
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",
@@ -351,7 +351,13 @@ const BLOCKED = [
351
351
  * 우리는 「빗금으로 **끝나는** 것」 만 뿌리로 봤다. 위 rm 규칙은 이미
352
352
  * 뿌리목표 로 별을 받고 있었다. 한쪽만 받으면 그건 규칙이 아니라 우연이다.
353
353
  */
354
- re: new RegExp(String.raw`Remove-Item\b(?=[^|;&]*\s-r[a-z]*\b)[^|;&]*\s${윈뿌리목표}(?=\s|$)`, 'i'),
354
+ /*
355
+ * 파워셸의 **다른 이름**도 같은 명령이다 (2.1.5 · 2.0.0 울타리 역공격에서 남김).
356
+ * `ri` · `rd` · `rmdir` · `del` · `erase` 는 파워셸에서 전부 Remove-Item 이다. 위 rd · del 규칙은
357
+ * cmd 의 `/s` · `/q` 만 보고, 여기는 긴 이름만 봐서 `ri -Recurse -Force C:\` 가 이 목록을 지나갔다
358
+ * (checkPaths 가 범위 밖으로 막아 줘서 여태 새지는 않았다 — 이 목록은 그와 따로 서는 겹이다).
359
+ */
360
+ re: new RegExp(String.raw`(?<![\w-])(?:Remove-Item|ri|rd|rmdir|del|erase)\b(?=[^|;&]*\s-r[a-z]*\b)[^|;&]*\s${윈뿌리목표}(?=\s|$)`, 'i'),
355
361
  why: '드라이브나 집 폴더를 통째로 지웁니다',
356
362
  },
357
363
  /*
@@ -560,7 +566,11 @@ export function checkPaths(cmd, scope) {
560
566
  if (봐주는자리(t)) continue;
561
567
  // cd 가 가리킨 자리는 위에서 **옮겨 간 자리 기준**으로 이미 봤다. 여기서
562
568
  // 또 보면 뿌리 기준으로 풀려서 `cd src/a && cd ../..` 가 밖으로 읽힌다.
563
- if (옮긴자리.has(t)) continue;
569
+ //
570
+ // 본 **횟수만큼만** 건너뛴다 (2.1.5). 글자로 건너뛰었더니, 빗금 없는 `..` 를 자리로 치자마자
571
+ // `cd src; cd ..; find .. -delete` 의 뒤 `..` 가 앞 cd 의 `..` 덕에 검사를 비켰다.
572
+ const 남은번 = 옮긴자리.get(t) ?? 0;
573
+ if (남은번 > 0) { 옮긴자리.set(t, 남은번 - 1); continue; }
564
574
  // 어디인지 모르는 이전 폴더는 밖으로 본다 (경로낱말 의 `~-` 머리말).
565
575
  if (/^~-(?=[/\\]|$)/.test(t)) {
566
576
  throw new ScopeError(`\`~-\` 는 셸의 이전 폴더(OLDPWD)라 어디인지 알 수 없어 작업 범위 밖으로 봅니다: ${t}`
@@ -620,6 +630,25 @@ function 자리표풀기(s) {
620
630
  * 따로 본다 — 어디인지 모르는 자리라 풀 값이 없다.)
621
631
  */
622
632
  .replace(/^~\+(?=[/\\]|$)/, '.')
633
+ /*
634
+ * `$PWD` · `%CD%` 도 **지금 폴더**다 — `~+` 와 같은 대접 (2.1.5).
635
+ *
636
+ * 자리표가 든 낱말은 아래 경로낱말() 이 「못 푼 것」 으로 보고 자리 검사를 건너뛴다. 이 둘은
637
+ * 값을 아는데도(셸은 작업 폴더에서 뜬다) 모르는 것으로 쳐서 그대로 새었다 —
638
+ *
639
+ * rm -rf ../옆프로젝트 막힘
640
+ * rm -rf $PWD/../옆프로젝트 통과 ← 같은 폴더
641
+ * rd /s /q %CD%\.. 통과 ← 작업 폴더의 부모 통째로
642
+ *
643
+ * 파워셸은 이름의 크기를 안 가려서(`$pwd`) 크기를 안 본다. 따옴표에 싼 `$(pwd)` 도 같은 값이다
644
+ * (안 싼 것은 괄호에서 잘려 `/../x` 가 절대경로로 걸린다). 파워셸의 `$pwd.Path` · `$pwd.ProviderPath`,
645
+ * 링크를 푼 `$(pwd -P)`, 옛 꼴 `` `pwd` `` 도 같은 자리다(Gemini 리뷰 — 넷 다 새고 있었다).
646
+ */
647
+ .replace(/^(?:\$\{PWD\}|\$PWD(?:\.(?:Provider)?Path)?|%CD%|\$\(pwd(?:\s+-[LP])?\)|`pwd(?:\s+-[LP])?`)(?=[/\\]|$)/i, '.')
648
+ /*
649
+ * `$OLDPWD` 는 `~-` 와 같은 자리(이전 폴더)다 — 어디인지 모르니 `~-` 로 바꿔 같은 길로 막는다.
650
+ */
651
+ .replace(/^(?:\$\{OLDPWD\}|\$OLDPWD)(?=[/\\]|$)/i, '~-')
623
652
  /*
624
653
  * `~이름` 은 **그 사람의 집**이다 (사냥5 R5-M1).
625
654
  *
@@ -938,6 +967,29 @@ export function 봐주는자리(경로) {
938
967
  // (`git commit --message=../notes` · `git log --format=%h/%s`). curl 의 `--data=@파일` 은 파일을 읽으니 안 뺀다.
939
968
  const 글받는옵션 = /^(?:message|format|pretty|title|body|subject|description|grep|author|committer|label|comment|notes?)$/i;
940
969
 
970
+ /*
971
+ * ── 빗금 없는 `..` 도 자리다 (2.1.5) ────────────────────────────────────
972
+ *
973
+ * 자리로 치는 것은 빗금이 든 낱말과 드라이브 한 칸(`C:`)뿐이었다. 그래서 **부모 폴더 그 자체**가
974
+ * 후보에도 못 들었다. 기본(auto) 모드에서는 셸 명령을 안 묻고 이 울타리가 마지막 문인데 —
975
+ *
976
+ * rm -rf ../* 막힘
977
+ * find .. -delete 통과 ← 작업 폴더의 부모가 통째로 비었다 (재 봤다)
978
+ * rd /s /q .. 통과 ← cmd
979
+ * Remove-Item -Recurse .. 통과 ← 파워셸
980
+ * git -C .. clean -fdx 통과 ←
981
+ * tar czf o.tgz .. 통과 ← 작업 폴더가 집 바로 밑이면 ~/.ssh 까지 묶여 읽힌다
982
+ *
983
+ * GNU rm 은 `..` 를 스스로 거절해서(`rm -rf ..`) 여태 안 보였을 뿐이다. find · cmd · 파워셸 · git 은
984
+ * 안 거절한다. 드라이브의 **지금 폴더 기준** 꼴(`C:..` · `C:.`)도 같다 — 윈도에서 `C:..` 는 그
985
+ * 드라이브의 지금 폴더의 부모다.
986
+ *
987
+ * `..` 가 **가운데** 든 것(`HEAD..main` · `{1..5}`)은 부모가 아니다. 낱말 **전체**가 `..` 일 때만.
988
+ */
989
+ function 자리꼴인가(s) {
990
+ return s.includes('/') || s.includes('\\') || s === '..' || /^[a-zA-Z]:(?:\.{1,2})?$/.test(s);
991
+ }
992
+
941
993
  /** 명령줄에서 경로처럼 보이는 낱말만 골라낸다. */
942
994
  export function 경로낱말(cmd) {
943
995
  const out = [];
@@ -1034,7 +1086,7 @@ export function 경로낱말(cmd) {
1034
1086
  */
1035
1087
  if (/^~-(?=[/\\]|$)/.test(풀린)) { out.push(풀린); continue; }
1036
1088
  const 살림이름 = 풀린 === '.deel' || 풀린.toLowerCase() === '.deel';
1037
- const 경로같나 = 풀린.includes('/') || 풀린.includes('\\') || /^[a-zA-Z]:$/.test(풀린) || 살림이름;
1089
+ const 경로같나 = 자리꼴인가(풀린) || 살림이름;
1038
1090
  if (!경로같나) continue;
1039
1091
  /*
1040
1092
  * 남은 자리표가 있으면(우리가 못 푼 것) **어디로 갈지 모른다.** 울타리
@@ -1083,7 +1135,7 @@ export function 경로낱말(cmd) {
1083
1135
  if (!/["'`]/.test(날것)) continue; // 따옴표가 없으면 위에서 이미 봤다
1084
1136
  const 뗀것 = 자리표풀기(날것.replace(/["'`]/g, ''));
1085
1137
  if (!뗀것 || 뗀것.startsWith('-')) continue;
1086
- if (!뗀것.includes('/') && !뗀것.includes('\\')) continue;
1138
+ if (!자리꼴인가(뗀것)) continue;
1087
1139
  if (/[$%]/.test(뗀것)) { if (내부살림(뗀것)) out.push(뗀것); continue; }
1088
1140
  if (코드조각인가(뗀것)) continue;
1089
1141
  if (!out.includes(뗀것)) out.push(뗀것);
@@ -1159,7 +1211,8 @@ function 낱말들(마디) {
1159
1211
  function 갈자리(뒤) {
1160
1212
  for (const w of 뒤) {
1161
1213
  if (!w.글) continue;
1162
- if (w.글.startsWith('-')) continue;
1214
+ // `-` 하나는 옵션이 아니라 이전 폴더다 (2.1.5). 옵션으로 건너뛰어 `cd - && rm -rf *` 가 안 보였다.
1215
+ if (w.글.startsWith('-') && w.글 !== '-') continue;
1163
1216
  if (/^\/d$/i.test(w.글)) continue;
1164
1217
  return w.글;
1165
1218
  }
@@ -1169,15 +1222,23 @@ function 갈자리(뒤) {
1169
1222
  /**
1170
1223
  * 폴더를 옮기는 명령을 따라가며, 옮겨 간 자리가 울타리 안인지 본다.
1171
1224
  *
1172
- * @returns {Set<string>} 여기서 이미 본 cd 목적지들. 부르는 쪽은 이걸 두 번
1173
- * 보지 않는다 — 뿌리 기준으로 다시 풀면 `cd src/a && cd ../..` 가 밖이 된다.
1225
+ * @returns {Map<string, number>} 여기서 이미 본 cd 목적지와 그 횟수. 부르는 쪽은 그만큼은
1226
+ * 두 번 보지 않는다 — 뿌리 기준으로 다시 풀면 `cd src/a && cd ../..` 가 밖이 된다.
1174
1227
  */
1175
- function 폴더옮김검사(cmd, scope, 시작 = null, 깊이 = 0, 본것 = new Set()) {
1228
+ function 폴더옮김검사(cmd, scope, 시작 = null, 깊이 = 0, 본것 = new Map()) {
1176
1229
  let 여기 = 시작 ?? scope.root;
1177
1230
  const 쌓은것 = [];
1231
+ // 이 명령 안에서 cd 로 떠나온 자리. `cd -` 가 가는 곳이다 — 없으면 셸이 물려받은 OLDPWD 라 모른다.
1232
+ let 이전 = null;
1233
+ // 어디인지 모르는 자리로 옮겼으면 그 적은 글자(`-` · `~-` · `$OLDPWD`). 뒤에 명령이 오면 막는다.
1234
+ let 모르는자리 = null;
1178
1235
  for (const 마디 of 마디들(cmd)) {
1179
1236
  const 낱말 = 낱말들(마디);
1180
1237
  if (!낱말.length) continue;
1238
+ if (모르는자리 != null) {
1239
+ throw new ScopeError(`\`cd ${모르는자리}\` 는 셸의 이전 폴더(OLDPWD)로 가는데 어디인지 알 수 없어, 그 뒤 명령을 작업 범위 밖에서 도는 것으로 봅니다`
1240
+ + `\n 이 명령 안에 있습니다: ${cmd.slice(0, 120)}`);
1241
+ }
1181
1242
  /*
1182
1243
  * ── cmd 에서 `cd..` 는 `cd ..` 다 (사냥8) ────────────────────────────
1183
1244
  *
@@ -1214,25 +1275,53 @@ function 폴더옮김검사(cmd, scope, 시작 = null, 깊이 = 0, 본것 = new
1214
1275
  for (const w of 낱말.slice(1)) if (w.쌌나) 폴더옮김검사(w.글, scope, 여기, 깊이 + 1, 본것);
1215
1276
  continue;
1216
1277
  }
1217
- if (되돌리기.test(첫)) { 여기 = 쌓은것.pop() ?? 여기; continue; }
1278
+ if (되돌리기.test(첫)) {
1279
+ const 꺼낸 = 쌓은것.pop();
1280
+ if (꺼낸 != null) { 이전 = 여기; 여기 = 꺼낸; }
1281
+ continue;
1282
+ }
1218
1283
  if (!폴더옮김.test(첫)) continue;
1219
1284
 
1220
1285
  const 목적 = 붙은목적 ?? 갈자리(낱말.slice(1));
1221
- // 인자 없는 cd(집으로) · `cd -`(직전 자리) 는 어디로 가는지 여기서 모른다.
1222
- // 모르는 것을 막으면 멀쩡한 명령이 죽는다.
1223
- if (목적 == null || 목적 === '-') continue;
1286
+ // 인자 없는 cd 는 셸마다 뜻이 다르다(bash 는 집 · cmd 는 지금 자리를 찍기만). 모르는 것을
1287
+ // 막으면 멀쩡한 명령이 죽는다.
1288
+ if (목적 == null) continue;
1224
1289
  const 풀린 = 자리표풀기(목적);
1290
+ /*
1291
+ * ── `cd -` · `cd ~-` · `cd $OLDPWD` 는 이전 폴더로 간다 (2.1.5) ─────────────
1292
+ *
1293
+ * 이 명령 안에서 먼저 옮겼으면 그 떠나온 자리다 — `cd src && make && cd -` 는 뿌리로 돌아온다.
1294
+ * 먼저 옮긴 적이 없으면 셸이 물려받은 OLDPWD 라 어디인지 모른다. 여태 `cd -` 를 건너뛰어
1295
+ * `cd - && rm -rf *` 가 그냥 지나갔다. 그 **뒤에 오는 명령**을 밖에서 도는 것으로 본다 —
1296
+ * `cd -` 하나만 있는 것은 옮기고 끝이라 그대로 둔다(아래 모르는자리).
1297
+ *
1298
+ * 그 **아래**(`cd ~-/sub` · `cd $OLDPWD/sub`)도 같은 자리에서 시작한다 — `^~-$` 로 딱 그 꼴만 봤더니
1299
+ * 아래로 붙인 것은 작업 폴더 밑의 `./~-/sub` 라는 이름으로 읽혀 지나갔다(Gemini 리뷰).
1300
+ */
1301
+ const 이전아래 = /^~-(?=[/\\]|$)/.test(풀린);
1302
+ if (목적 === '-' || 이전아래) {
1303
+ // 경로낱말 은 `~-`(· `$OLDPWD`)를 밖으로 올린다. 여기서 풀어 준 것은 checkPaths 가 한 번 비킨다.
1304
+ 본것.set(풀린, (본것.get(풀린) ?? 0) + 1);
1305
+ if (이전 == null) { 모르는자리 = 목적; continue; }
1306
+ if (목적 === '-' || 풀린 === '~-') { [여기, 이전] = [이전, 여기]; continue; }
1307
+ // 이전 폴더 아래로 — 아래의 범위 검사를 그대로 받는다.
1308
+ }
1225
1309
  // 못 푼 자리표가 남아 있으면(`$BUILD_DIR`) 역시 어디로 갈지 모른다.
1226
1310
  if (/[$%]/.test(풀린)) continue;
1227
1311
 
1228
- const 열린 = MSYS풀기(풀린);
1229
- const abs = isAbsolute(열린) ? resolve(열린) : resolve(여기, 열린);
1230
- 본것.add(풀린);
1312
+ const 열린 = MSYS풀기(이전아래 ? 풀린.replace(/^~-[/\\]*/, '') : 풀린);
1313
+ const abs = isAbsolute(열린) ? resolve(열린) : resolve(이전아래 ? 이전 : 여기, 열린);
1314
+ /*
1315
+ * 비킬 낱말로 적는 것은 **따로 선 목적지**뿐이다. `cd..` 처럼 붙여 쓴 것은 경로낱말 에 `..` 로 안
1316
+ * 나오므로, 적어 두면 남은 한 번이 뒤의 딴 `..` 를 비켜 준다 — `cd src && cd.. && find .. -delete`
1317
+ * (Gemini 리뷰). 이전아래 는 위에서 이미 적었다.
1318
+ */
1319
+ if (!붙은목적 && !이전아래) 본것.set(풀린, (본것.get(풀린) ?? 0) + 1);
1231
1320
 
1232
1321
  // 프로그램이 있는 자리로 들어가는 것은 자료를 만지는 일이 아니다.
1233
1322
  // 푼 값이 아니라 **적힌 글자**로 본다 — 윈도우에서 resolve('/usr/bin') 은
1234
1323
  // `C:\usr\bin` 이 되어 목록에 안 걸린다 (checkPaths 도 낱말로 본다).
1235
- if (봐주는자리(풀린)) { 여기 = abs; continue; }
1324
+ if (봐주는자리(풀린)) { 이전 = 여기; 여기 = abs; continue; }
1236
1325
  try { scope.resolve(abs); }
1237
1326
  catch (e) {
1238
1327
  if (e instanceof ScopeError) {
@@ -1251,6 +1340,7 @@ function 폴더옮김검사(cmd, scope, 시작 = null, 깊이 = 0, 본것 = new
1251
1340
  if (이유) throw new BlockedError(`${이유}\n 이 명령 안에 있습니다: ${cmd.slice(0, 120)}`);
1252
1341
 
1253
1342
  if (/^(pushd|push-location)$/i.test(첫)) 쌓은것.push(여기);
1343
+ 이전 = 여기;
1254
1344
  여기 = abs;
1255
1345
  }
1256
1346
  return 본것;