deel-local-cli 1.12.0 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.ko.md +6 -5
  2. package/README.md +6 -5
  3. package/package.json +60 -60
  4. package/src/acp/map.js +30 -25
  5. package/src/acp/serve.js +810 -810
  6. package/src/agent/budget.js +156 -156
  7. package/src/agent/commit.js +32 -9
  8. package/src/agent/compact.js +441 -420
  9. package/src/agent/effort.js +308 -308
  10. package/src/agent/evolve.js +263 -263
  11. package/src/agent/loop.js +1728 -1707
  12. package/src/agent/memory.js +156 -156
  13. package/src/agent/mention.js +210 -210
  14. package/src/agent/recall.js +22 -9
  15. package/src/agent/session.js +1056 -1049
  16. package/src/backend/adapter.js +1355 -1159
  17. package/src/backend/cachemark.js +223 -150
  18. package/src/backend/detect.js +327 -327
  19. package/src/backend/http.js +3 -1
  20. package/src/backend/mcp.js +15 -3
  21. package/src/backend/price.js +69 -6
  22. package/src/backend/probe.js +17 -3
  23. package/src/backend/quota.js +274 -250
  24. package/src/backend/tokens.js +25 -3
  25. package/src/backend/toolfit.js +352 -352
  26. package/src/backend/wire.js +715 -600
  27. package/src/commands.js +3184 -3135
  28. package/src/config.js +39 -0
  29. package/src/i18n/en.js +545 -544
  30. package/src/i18n/ja.js +498 -497
  31. package/src/i18n/ko.js +592 -591
  32. package/src/i18n/zh.js +498 -497
  33. package/src/oneshot.js +625 -620
  34. package/src/pack/tar.js +23 -1
  35. package/src/pack/zip.js +19 -1
  36. package/src/plugins/manage.js +85 -5
  37. package/src/repl.js +2616 -2602
  38. package/src/safety/audit.js +22 -3
  39. package/src/safety/guard.js +124 -9
  40. package/src/safety/network.js +43 -0
  41. package/src/tools/fastgrep.js +15 -0
  42. package/src/tools/fsutil.js +291 -265
  43. package/src/tools/index.js +40 -3
  44. package/src/tools/jobs.js +87 -6
  45. package/src/tools/label.js +52 -0
  46. package/src/tools/spawn.js +114 -0
  47. package/src/tools/verify.js +358 -358
  48. package/src/tools/webfetch.js +28 -3
  49. package/src/ui/export.js +26 -10
  50. package/src/ui/pick.js +115 -115
  51. package/src/ui/status.js +617 -613
package/src/pack/tar.js CHANGED
@@ -121,8 +121,30 @@ export function stripTop(files) {
121
121
  * 통째로 갈아 치우므로, 그 결과가 뿌리로 시작하지 않는다.
122
122
  */
123
123
  export function 안쪽인가(뿌리, 이름) {
124
+ const 원래 = String(이름 ?? '');
125
+ /*
126
+ * ── 판마다 다르게 막으면 막은 게 아니다 ───────────────────────────────
127
+ *
128
+ * `resolve()` 는 윈도우에서만 역슬래시를 칸막이로 본다. 그래서
129
+ * `..\\..\\밖으로.txt` 는 윈도우에서는 밖으로 잡히고 리눅스·맥에서는
130
+ * **그냥 파일 이름 하나**로 통과한다. 같은 묶음이 판에 따라 다르게 처리되는
131
+ * 셈이다.
132
+ *
133
+ * 플러그인 묶음은 기계를 옮겨 다닌다. 리눅스에서 받아 둔 것을 윈도우로
134
+ * 옮기거나, 리눅스에서 푼 것을 그대로 다시 묶어 나눠 주는 일이 흔하다.
135
+ * 한쪽에서만 막으면 다른 쪽으로 새어 나갈 길이 남는다.
136
+ *
137
+ * 그래서 역슬래시를 **어느 판에서든** 칸막이로 본다. 이 방향으로만 틀리면
138
+ * 손해는 「이름에 역슬래시가 든 파일을 못 푼다」 뿐이고, 그런 이름이 정직한
139
+ * 플러그인에 있을 까닭이 없다. 반대 방향으로 틀리면 남의 글자 한 줄로
140
+ * 폴더 밖에 파일이 써진다.
141
+ */
142
+ const 이름꼴 = 원래.replace(/\\/g, '/');
143
+ // 드라이브 글자(`C:`)도 마찬가지다. 리눅스에서는 그냥 이름이라 통과하는데,
144
+ // 그 묶음을 윈도우에서 풀면 그 자리로 곧장 나간다.
145
+ if (/^[a-zA-Z]:/.test(이름꼴)) return false;
124
146
  const 뿌리절대 = resolve(String(뿌리 ?? ''));
125
- const 갈곳 = resolve(뿌리절대, String(이름 ?? ''));
147
+ const 갈곳 = resolve(뿌리절대, 이름꼴);
126
148
  return 갈곳 === 뿌리절대 || 갈곳.startsWith(뿌리절대 + sep);
127
149
  }
128
150
 
package/src/pack/zip.js CHANGED
@@ -194,7 +194,25 @@ export function readZip(buf, { only = null } = {}) {
194
194
  files.set(name, Buffer.from(body));
195
195
  } else if (method === 8) {
196
196
  try {
197
- const out = inflateRawSync(body);
197
+ /*
198
+ * ── 풀기 **전에** 상한을 정한다 ──────────────────────────────
199
+ *
200
+ * 여기가 `inflateRawSync(body)` 한 줄이었다. 다 풀어 놓고 나서
201
+ * 크기를 견줬으니, 크기가 안 맞는 것을 **알아내는 시점이 이미 늦다.**
202
+ *
203
+ * 작은 deflate 조각 + 수 GB 로 부푸는 내용 → 프로세스가 죽는다
204
+ *
205
+ * `.docx`·`.xlsx` 는 사람이 아무 데서나 받아 오는 파일이고, 우리는
206
+ * 모델이 시키는 대로 그것을 연다. 즉 남이 정한 바이트로 우리 메모리를
207
+ * 정하게 두고 있었다. 목록에 적힌 크기(rawSize)를 이미 읽어 뒀으면서
208
+ * 쓰지 않은 것이 아까운 자리다.
209
+ *
210
+ * zlib 는 `maxOutputLength` 로 그 자리에서 멈춰 준다. 다 풀고 재는
211
+ * 것과 달리 **메모리를 안 쓰고** 멈춘다.
212
+ */
213
+ const 한항목상한 = 64 * 1024 * 1024; // 이 프로그램이 여는 문서의 현실적 위쪽
214
+ const 상한 = Math.min(rawSize > 0 ? rawSize : 한항목상한, 한항목상한);
215
+ const out = inflateRawSync(body, { maxOutputLength: 상한 });
198
216
  if (rawSize && out.length !== rawSize) {
199
217
  skipped.push({ name, why: `푼 크기가 안 맞음 (${out.length} ≠ ${rawSize})` });
200
218
  continue;
@@ -4,7 +4,7 @@
4
4
  // 오프라인 기기에 반입한다. 오프라인에서는 압축만 풀면 그대로 인식된다.
5
5
  import { execFile } from 'node:child_process';
6
6
  import { homedir } from 'node:os';
7
- import { join, dirname, basename } from 'node:path';
7
+ import { join, dirname, basename, resolve, sep } from 'node:path';
8
8
  import {
9
9
  existsSync, mkdirSync, writeFileSync, readFileSync, rmSync,
10
10
  readdirSync, statSync,
@@ -146,6 +146,59 @@ async function fetchInto(spec, dest, onStep) {
146
146
  return { error: '받지 못했습니다 — 저장소 주소나 가지 이름을 확인하세요' };
147
147
  }
148
148
 
149
+ /**
150
+ * 플러그인 이름을 **폴더 이름 한 칸**으로 자른다.
151
+ *
152
+ * ── 왜 있어야 하나 ────────────────────────────────────────────────────
153
+ *
154
+ * 설치할 자리를 `join(플러그인폴더, 이름)` 으로 정하는데, 이 **이름은 남이 적은
155
+ * 글자**다. `.claude-plugin/plugin.json` 의 `name` 을 그대로 읽어 쓰고, 그 파일은
156
+ * 플러그인을 만든 사람이 통째로 정한다.
157
+ *
158
+ * 그래서 이름이 `../../Desktop/뭐시기` 면 설치할 자리가 홈 폴더 밖으로 나간다.
159
+ * `join` 은 `..` 를 정리해 줄 뿐 막아 주지 않는다. 그 자리에 대고 우리가 하는
160
+ * 일이 하필 이것이다 —
161
+ *
162
+ * rmSync(dest, { recursive: true, force: true }) ← 통째로 지운다
163
+ * copyDir(tmp, dest) ← 그 자리에 파일을 놓는다
164
+ *
165
+ * 묶음 안의 파일 확장자는 안 가린다(반입 묶음을 만들 때만 가린다). 그러니
166
+ * 이름 한 줄로 **남의 폴더를 지우고 그 자리에 실행 파일을 놓을 수 있다.**
167
+ * 시작프로그램 폴더를 노리면 다음 로그인부터 그것이 돈다.
168
+ *
169
+ * 빈 이름과 `.` 도 막아야 한다. 그 둘은 자리가 **플러그인 폴더 자신**이 되어,
170
+ * `rmSync` 가 설치해 둔 플러그인을 전부 지운다.
171
+ *
172
+ * 역슬래시도 칸막이로 본다. 윈도우에서 만든 묶음이 리눅스에서 이름 한 칸으로
173
+ * 통과하면, 그 묶음을 다시 윈도우로 옮겼을 때 그때 나간다 (pack/tar.js 의
174
+ * 안쪽인가 와 같은 판단이다).
175
+ *
176
+ * 이 함수가 없으면 tar 엔트리 이름을 아무리 잘 막아도 소용이 없다. 묶음이
177
+ * **제 이름으로** 나가기 때문이다.
178
+ */
179
+ export function 이름한칸(이름) {
180
+ const 글 = String(이름 ?? '').replace(/\\/g, '/').trim();
181
+ if (!글) return null;
182
+ // 마지막 칸만 쓴다. `a/b/../c` 같은 것도 여기서 한 칸이 된다.
183
+ const 한칸 = basename(글);
184
+ if (!한칸 || 한칸 === '.' || 한칸 === '..') return null;
185
+ // 드라이브 글자·칸막이가 남아 있으면 이름이 아니다.
186
+ if (/[/:]/.test(한칸)) return null;
187
+ return 한칸;
188
+ }
189
+
190
+ /**
191
+ * 그 자리가 정말 플러그인 폴더 **안**인가 — 이름을 자른 뒤에도 한 번 더 본다.
192
+ *
193
+ * 자르는 자가 언젠가 틀릴 수 있다. 그때 마지막으로 서는 그물이다. 지우고
194
+ * 쓰는 자리라 두 겹으로 막는다.
195
+ */
196
+ export function 플러그인자리인가(base, dest) {
197
+ const 뿌리 = resolve(base);
198
+ const 갈곳 = resolve(dest);
199
+ return 갈곳 !== 뿌리 && 갈곳.startsWith(뿌리 + sep);
200
+ }
201
+
149
202
  function manifestOf(dir) {
150
203
  const f = join(dir, '.claude-plugin', 'plugin.json');
151
204
  if (!existsSync(f)) return null;
@@ -217,8 +270,24 @@ export async function install(spec, { home = homedir(), onStep } = {}) {
217
270
  if (got.error) { rmSync(tmp, { recursive: true, force: true }); return { error: got.error }; }
218
271
 
219
272
  const info = manifestOf(tmp);
220
- const name = info?.name || (isLocal ? basename(asPath) : parsed.repo);
273
+ /*
274
+ * 이름은 **남이 적은 글자**다. 자리 조립에 쓰기 전에 한 칸으로 자른다
275
+ * (이름한칸 머리말 — 안 자르면 이름 한 줄로 남의 폴더가 지워진다).
276
+ * 매니페스트 이름이 못 쓸 것이면 우리가 아는 이름(폴더명·저장소명)으로 간다.
277
+ */
278
+ const 적힌이름 = 이름한칸(info?.name);
279
+ const 뒷이름 = 이름한칸(isLocal ? basename(asPath) : parsed.repo);
280
+ const name = 적힌이름 || 뒷이름;
281
+ if (!name) {
282
+ rmSync(tmp, { recursive: true, force: true });
283
+ return { error: '플러그인 이름을 읽지 못했습니다 — 이름이 비었거나 폴더 이름으로 쓸 수 없는 글자입니다.' };
284
+ }
221
285
  const dest = join(base, name);
286
+ // 두 겹째 그물. 여기서 걸리면 자르는 자가 틀린 것이므로 아예 멈춘다.
287
+ if (!플러그인자리인가(base, dest)) {
288
+ rmSync(tmp, { recursive: true, force: true });
289
+ return { error: `설치할 자리가 플러그인 폴더 밖입니다 — 설치하지 않았습니다 (${name}).` };
290
+ }
222
291
 
223
292
  const counts = countIn(tmp);
224
293
  if (!counts.skills && !counts.commands) {
@@ -267,10 +336,21 @@ export function list({ home = homedir() } = {}) {
267
336
  }
268
337
 
269
338
  export function remove(name, { home = homedir() } = {}) {
270
- const dir = join(pluginsDir(home), name);
271
- if (!existsSync(dir)) return { error: `설치돼 있지 않습니다: ${name}` };
339
+ /*
340
+ * 지우는 자리도 이름으로 정해진다. 그러니 여기도 칸으로 자른다.
341
+ *
342
+ * 사람이 직접 치는 자리라 안전할 것 같지만, 목록(list)이 보여 주는 이름을
343
+ * 그대로 복사해 붙이는 것이 보통이고 그 이름은 매니페스트에서 온다.
344
+ * 설치 때 막아도 이미 깔려 있던 것에는 못 쓴 이름이 남아 있을 수 있다.
345
+ */
346
+ const base = pluginsDir(home);
347
+ const 한칸 = 이름한칸(name);
348
+ const dir = 한칸 ? join(base, 한칸) : null;
349
+ if (!dir || !플러그인자리인가(base, dir) || !existsSync(dir)) {
350
+ return { error: `설치돼 있지 않습니다: ${name}` };
351
+ }
272
352
  rmSync(dir, { recursive: true, force: true });
273
- return { removed: name };
353
+ return { removed: 한칸 };
274
354
  }
275
355
 
276
356
  // 반입용 묶음. 실행 스크립트는 빼고 스킬·명령만 담는다.