deel-local-cli 1.5.7 → 1.6.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.
- package/README.en.md +24 -13
- package/README.md +23 -12
- package/bin/deel.js +10 -0
- package/package.json +2 -2
- package/src/acp/map.js +143 -0
- package/src/acp/serve.js +160 -13
- package/src/agent/commit.js +511 -0
- package/src/agent/compact.js +269 -226
- package/src/agent/loop.js +154 -12
- package/src/agent/mention.js +56 -10
- package/src/agent/review.js +192 -0
- package/src/agent/session.js +20 -2
- package/src/agent/threads.js +1 -1
- package/src/backend/adapter.js +342 -280
- package/src/backend/azure.js +151 -0
- package/src/backend/ctxsize.js +19 -0
- package/src/backend/detect.js +106 -0
- package/src/backend/http.js +350 -30
- package/src/backend/probe.js +48 -1
- package/src/backend/proxy.js +151 -0
- package/src/backend/quota.js +133 -0
- package/src/backend/retry.js +132 -0
- package/src/backend/vision.js +185 -0
- package/src/commands.js +341 -8
- package/src/completion.js +264 -0
- package/src/config.js +117 -1
- package/src/i18n/en.js +13 -1
- package/src/i18n/index.js +22 -3
- package/src/i18n/ja.js +266 -0
- package/src/i18n/ko.js +12 -0
- package/src/i18n/zh.js +266 -0
- package/src/oneshot.js +28 -3
- package/src/pack/sbom.js +25 -0
- package/src/pack/selfpack.js +9 -3
- package/src/plugins/manage.js +34 -7
- package/src/repl.js +113 -15
- package/src/report.js +26 -2
- package/src/safety/guard.js +82 -3
- package/src/safety/keystore.js +237 -0
- package/src/safety/network.js +76 -14
- package/src/safety/policy.js +209 -0
- package/src/safety/secrets.js +32 -0
- package/src/setup.js +40 -7
- package/src/tools/clipboard.js +178 -0
- package/src/tools/fastgrep.js +217 -0
- package/src/tools/fsutil.js +55 -12
- package/src/tools/ignore.js +192 -0
- package/src/tools/index.js +282 -39
- package/src/tools/jobs.js +4 -12
- package/src/tools/outline.js +3 -1
- package/src/tools/pdf.js +1326 -0
- package/src/tools/shell.js +123 -0
- package/src/tools/verify.js +20 -4
- package/src/tools/webfetch.js +18 -5
- package/src/ui/inputbox.js +2 -2
- package/src/ui/screen.js +4 -1
- package/src/ui/status.js +22 -0
package/src/tools/index.js
CHANGED
|
@@ -4,9 +4,11 @@ import { writeFileSync, appendFileSync, readFileSync, existsSync, mkdirSync, sta
|
|
|
4
4
|
import { dirname, join, relative, sep } from 'node:path';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
6
|
import { globToRegex, walk, readText, readTextFull, 내부살림 } from './fsutil.js';
|
|
7
|
+
import { 건너뜀말 } from './ignore.js';
|
|
7
8
|
import { encode, label as encLabel, decode as decodeBytes, consoleCodepage, looksBinary } from './encoding.js';
|
|
8
9
|
import { checkCommand, checkPaths, isMutating } from '../safety/guard.js';
|
|
9
10
|
import { 띄우기, JOBS_TOOL } from './jobs.js';
|
|
11
|
+
import { 셸명령 } from './shell.js';
|
|
10
12
|
import { findMatch, applySpans, reindent, TIER_LABELS } from './edit-match.js';
|
|
11
13
|
import { loadSkill } from '../skills/discover.js';
|
|
12
14
|
import { WEB_FETCH_TOOL } from './webfetch.js';
|
|
@@ -21,9 +23,12 @@ import { allow as allowedIn } from '../agent/modes.js';
|
|
|
21
23
|
import { 도구정의, 이름풀기 } from '../backend/mcp.js';
|
|
22
24
|
import { isExcelPath, readExcel, toText as excelText, summarize as excelSummary } from './excel.js';
|
|
23
25
|
import { isDocPath, readDoc, toText as docText, summarize as docSummary, looksOldHwp, 옛hwp안내, 문서는못고침 } from './docs.js';
|
|
26
|
+
import { isPdfPath, readPdf, toText as pdfText, summarize as pdfSummary, 못읽은말, pdf는못고침 } from './pdf.js';
|
|
24
27
|
import { diffLines } from '../ui/diff.js';
|
|
25
28
|
import { 읽을줄수, 찾을개수, 찾을줄수, 설명길이 } from '../agent/budget.js';
|
|
26
29
|
import { 도구설명EN } from './desc.en.js';
|
|
30
|
+
import { 그림인가, 그림읽기, 크기말, 기본한도 } from '../backend/vision.js';
|
|
31
|
+
import { 빠르게찾기, 엔진말, 안볼정규식 } from './fastgrep.js';
|
|
27
32
|
import { 지시말 } from '../i18n/index.js';
|
|
28
33
|
|
|
29
34
|
/*
|
|
@@ -41,7 +46,9 @@ const MAX_OUT = 30000;
|
|
|
41
46
|
// 한 파일에서 몇십 초를 쓰면 그동안 화면이 멈춘 것처럼 보인다.
|
|
42
47
|
const GREP_MAX_FILE = 2 * 1024 * 1024;
|
|
43
48
|
// 정규식으로 찾을 것이 없는 파일들. 열어 봐야 시간만 든다.
|
|
44
|
-
|
|
49
|
+
// 목록은 tools/fastgrep.js 에 한 벌만 둔다 — rg 도 같은 목록으로 걸러야
|
|
50
|
+
// 엔진이 달라도 같은 파일을 본다.
|
|
51
|
+
const 안읽을확장자 = 안볼정규식;
|
|
45
52
|
// 이보다 큰 파일은 바뀐 자리를 안 재고 넘어간다. 화면에 못 담을 양이기도 하고,
|
|
46
53
|
// 재는 값보다 기다리는 값이 커진다.
|
|
47
54
|
const MAX_DIFF_CHARS = 4_000_000;
|
|
@@ -66,6 +73,56 @@ function clip(s, n = MAX_OUT) {
|
|
|
66
73
|
return t.length > n ? t.slice(0, n) + `\n… (${t.length - n}자 잘림)` : t;
|
|
67
74
|
}
|
|
68
75
|
|
|
76
|
+
const 몇KB = (n) => (n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)}MB` : `${Math.round(n / 1024)}KB`);
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* ── 파일에 박힌 그림을 빼고 읽는다 ──────────────────────────────────────
|
|
80
|
+
*
|
|
81
|
+
* 사내 업무용 HTML 은 그림을 파일 안에 base64 로 박아 넣는다(사내망 문서가
|
|
82
|
+
* 다 그렇다). 그러면 919줄짜리 문서가 5MB 가 된다. 줄 수는 멀쩡한데 한 줄이
|
|
83
|
+
* 메가바이트다.
|
|
84
|
+
*
|
|
85
|
+
* 이걸 그대로 실어 보내면 이렇게 된다 — 실제로 재 본 값이다:
|
|
86
|
+
*
|
|
87
|
+
* 919줄 문서, 결과 상한 30,000자
|
|
88
|
+
* → 모델이 보는 것: **8줄**. 그중 99%가 base64.
|
|
89
|
+
*
|
|
90
|
+
* 여덟 줄로는 아무것도 못 한다. 그래서 모델은 같은 파일을 열한 번 읽고,
|
|
91
|
+
* 그러고도 못 봐서 결국 이런 명령을 쓴다 —
|
|
92
|
+
*
|
|
93
|
+
* node -e "s.replace(/data:image\/[\s\S]*?base64,[^\"']+/g,'data:[omitted]')"
|
|
94
|
+
*
|
|
95
|
+
* **base64 를 지워야 읽을 수 있다는 것을 모델이 스스로 알아낸 것이다.**
|
|
96
|
+
* 그런데 그 명령마저 울타리에 막혔다(정규식을 경로로 읽었다 — guard.js).
|
|
97
|
+
* 한 요청에 몇 분이 가고 아무것도 안 바뀐 화면이 그렇게 나왔다.
|
|
98
|
+
*
|
|
99
|
+
* 그러니 모델이 할 일이 아니라 여기서 할 일이다. 그림 자리는 몇 바이트인지만
|
|
100
|
+
* 남기고 뺀다 — 모델에게 필요한 것은 그림의 바이트가 아니라 **문서의 뼈대**다.
|
|
101
|
+
*
|
|
102
|
+
* ── 지우는 게 아니다 ────────────────────────────────────────────────────
|
|
103
|
+
*
|
|
104
|
+
* 파일은 한 글자도 안 바뀐다. 실어 보내는 값만 줄인다. Edit 은 파일을 직접
|
|
105
|
+
* 보므로 그림이 든 줄도 그대로 고칠 수 있다 — 다만 생략된 자리를 그대로 베껴
|
|
106
|
+
* old_string 에 넣으면 안 맞는다. 그래서 생략했다는 것을 결과에 또렷이 적는다.
|
|
107
|
+
*/
|
|
108
|
+
const 그림자리 = /(data:[\w.+-]+\/[\w.+-]+(?:;[\w.+-]+=[\w.+-]+)*;base64,)([A-Za-z0-9+/=\s]{200,})/g;
|
|
109
|
+
|
|
110
|
+
export function 붙박이그림줄이기(줄들) {
|
|
111
|
+
let 줄인바이트 = 0;
|
|
112
|
+
let 몇개 = 0;
|
|
113
|
+
const 나온것 = 줄들.map((l) => String(l).replace(그림자리, (_, 앞, 몸) => {
|
|
114
|
+
줄인바이트 += 몸.length;
|
|
115
|
+
몇개++;
|
|
116
|
+
return `${앞}…(${몇KB(몸.length)} 생략)…`;
|
|
117
|
+
}));
|
|
118
|
+
const 알림 = 몇개
|
|
119
|
+
? `\n\n[박힌 그림 ${몇개}개(${몇KB(줄인바이트)})를 빼고 보여 줍니다. 파일은 그대로입니다 —`
|
|
120
|
+
+ ' 생략 표시가 든 줄을 Edit 의 old_string 으로 그대로 쓰면 안 맞습니다.'
|
|
121
|
+
+ ' 그 줄을 고쳐야 하면 생략된 자리를 뺀 앞뒤 짧은 조각으로 가리키세요.]'
|
|
122
|
+
: '';
|
|
123
|
+
return { 줄들: 나온것, 알림, 줄인바이트, 몇개 };
|
|
124
|
+
}
|
|
125
|
+
|
|
69
126
|
// 엑셀 파일에 쓰려 할 때 하는 말. 왜 안 되는지와, 그럼 어떻게 하는지를 같이 준다.
|
|
70
127
|
/**
|
|
71
128
|
* 이 자리에 글을 써 넣으면 안 되는 파일인가.
|
|
@@ -258,6 +315,31 @@ function 문서읽기(abs, ctx) {
|
|
|
258
315
|
};
|
|
259
316
|
}
|
|
260
317
|
|
|
318
|
+
/**
|
|
319
|
+
* PDF 를 쪽마다 글로 읽어 돌려준다.
|
|
320
|
+
*
|
|
321
|
+
* 문서읽기와 같은 규칙 — ctx.seen 에 안 넣는다(고칠 수 있는 물건이 아니다).
|
|
322
|
+
* 다른 점은 **못 읽은 쪽을 반드시 말한다**는 것이다. PDF 는 글이 아예 안 든
|
|
323
|
+
* 쪽(스캔본)이 흔해서, 빈 글을 그냥 돌려주면 「그런 내용이 없는 문서」로
|
|
324
|
+
* 읽힌다. 모델은 그걸 근거로 답하고, 사람은 그 답을 믿는다.
|
|
325
|
+
*/
|
|
326
|
+
function pdf읽기(abs, ctx) {
|
|
327
|
+
const r = readPdf(abs);
|
|
328
|
+
if (!r.ok) return { error: r.error };
|
|
329
|
+
const { text, 잘림 } = pdfText(r);
|
|
330
|
+
const 못 = 못읽은말(r);
|
|
331
|
+
return {
|
|
332
|
+
content: clip(
|
|
333
|
+
`${text || '(글이 하나도 없는 PDF 입니다.)'}
|
|
334
|
+
|
|
335
|
+
(PDF 를 쪽마다 글로 바꿔서 보여준 것입니다. 이 파일은 Edit/Write 로 고칠 수 없습니다.)`
|
|
336
|
+
+ (못 ? `\n${못}` : '')
|
|
337
|
+
+ (잘림.length ? `\n(${잘림.join(' · ')})` : ''),
|
|
338
|
+
),
|
|
339
|
+
summary: pdfSummary(r) + (잘림.length ? ' · 일부만' : ''),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
261
343
|
/**
|
|
262
344
|
* 파일 하나를 쓴다 — Write 의 알맹이.
|
|
263
345
|
*
|
|
@@ -321,9 +403,19 @@ function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
321
403
|
* 그대로 되지만 되돌리기에는 안 잡힌다. 그런 폴더를 되돌리자고 수만 개를
|
|
322
404
|
* 뜨는 쪽이 훨씬 나쁘다.
|
|
323
405
|
*/
|
|
406
|
+
// 옮길 때는 .gitignore 를 안 본다 — 옮겨지는 것은 전부이고, 되돌리기도 전부를 떠야 한다.
|
|
407
|
+
const 훑은것 = 폴더인가 ? walk(앞, { ignore: false }) : null;
|
|
324
408
|
const 짝들 = 폴더인가
|
|
325
|
-
?
|
|
409
|
+
? 훑은것.map((f) => [f.path, join(뒤, relative(앞, f.path))])
|
|
326
410
|
: [[앞, 뒤]];
|
|
411
|
+
/*
|
|
412
|
+
* 훑기 상한에 걸리면 **되돌리기가 반쪽이 된다.**
|
|
413
|
+
*
|
|
414
|
+
* 옮기는 것 자체는 renameSync 가 폴더째 하므로 전부 옮겨진다. 그런데 이력에
|
|
415
|
+
* 뜨는 것은 여기서 훑은 것뿐이라, 2만 개가 넘는 폴더를 옮기면 `/undo` 가
|
|
416
|
+
* 앞의 2만 개만 되돌린다. 그걸 말 안 하면 사람은 되돌렸다고 믿고 넘어간다.
|
|
417
|
+
*/
|
|
418
|
+
const 되돌리기반쪽 = !!훑은것?.잘림;
|
|
327
419
|
for (const [a, b] of 짝들) {
|
|
328
420
|
ctx.history.snapshot(a, 'Move');
|
|
329
421
|
ctx.history.snapshot(b, 'Move');
|
|
@@ -349,10 +441,22 @@ function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
349
441
|
|
|
350
442
|
for (const [, b] of 짝들) ctx.seen.add(b);
|
|
351
443
|
const 무엇 = 폴더인가 ? `폴더 ${짝들.length}개 파일` : '';
|
|
444
|
+
const 경고 = 되돌리기반쪽
|
|
445
|
+
? `\n(파일이 ${훑은것.상한.toLocaleString('en-US')}개를 넘어 되돌리기에는 앞부분만 떴습니다 — 옮기기는 전부 됐지만 /undo 는 다 못 되돌립니다)`
|
|
446
|
+
: '';
|
|
352
447
|
return {
|
|
353
|
-
content: `옮김: ${ctx.scope.show(앞)} → ${ctx.scope.show(뒤)}${무엇 ? ` (${무엇})` : ''}`,
|
|
448
|
+
content: `옮김: ${ctx.scope.show(앞)} → ${ctx.scope.show(뒤)}${무엇 ? ` (${무엇})` : ''}${경고}`,
|
|
354
449
|
summary: `${ctx.scope.show(뒤)}`,
|
|
355
450
|
changed: 뒤,
|
|
451
|
+
/*
|
|
452
|
+
* 폴더를 옮겼으면 **옮겨진 파일 하나하나**를 적어 준다.
|
|
453
|
+
*
|
|
454
|
+
* 닿은 자리가 이미 있던 폴더면(`Move('새것', 'src')`) 그 폴더에는 남이
|
|
455
|
+
* 고치던 파일도 산다. 「이 폴더가 바뀌었다」 로만 적어 두면 나중에
|
|
456
|
+
* /commit 이 그 폴더를 통째로 담고, 남의 변경이 이 커밋에 실린다.
|
|
457
|
+
* 무엇이 실제로 움직였는지는 지금 이 자리만 안다.
|
|
458
|
+
*/
|
|
459
|
+
바뀐것들: 폴더인가 ? 짝들.map(([, b]) => b) : null,
|
|
356
460
|
};
|
|
357
461
|
}
|
|
358
462
|
|
|
@@ -393,6 +497,7 @@ function 한파일쓰기(args, ctx) {
|
|
|
393
497
|
// 문서(hwpx·docx·pptx)도 같은 이유로 또렷하게 거절한다. 일반 '바이너리'
|
|
394
498
|
// 오류로 넘기면 왜 안 되는지가 안 실려서, 모델이 우회로를 찾는다.
|
|
395
499
|
if (isDocPath(abs)) return { error: 문서는못고침(args.file_path) };
|
|
500
|
+
if (isPdfPath(abs)) return { error: pdf는못고침(args.file_path) };
|
|
396
501
|
// 엑셀만 막아서는 모자란다. hwp·pdf·png·zip 도 똑같이 그 순간 끝난다.
|
|
397
502
|
// 게다가 이런 파일은 되돌리기가 내용을 떠 놓지 못하는 종류라 되살릴 길이 없다.
|
|
398
503
|
// 확장자로 고르지 않고 실제 내용으로 본다 — 사내 파일은 확장자가 제각각이다.
|
|
@@ -491,6 +596,7 @@ function 한군데고치기(args, ctx) {
|
|
|
491
596
|
// '먼저 Read 로 읽어야 합니다' 라고만 하면 이미 읽은 쪽은 계속 헛돈다.
|
|
492
597
|
if (isExcelPath(abs)) return { error: 엑셀은못고침(args.file_path) };
|
|
493
598
|
if (isDocPath(abs)) return { error: 문서는못고침(args.file_path) };
|
|
599
|
+
if (isPdfPath(abs)) return { error: pdf는못고침(args.file_path) };
|
|
494
600
|
if (!ctx.seen.has(abs)) return { error: `먼저 Read 로 읽어야 합니다: ${args.file_path}` };
|
|
495
601
|
if (args.old_string === args.new_string) return { error: 'old_string 과 new_string 이 같습니다' };
|
|
496
602
|
|
|
@@ -604,6 +710,49 @@ function 여러군데고치기(목록, ctx) {
|
|
|
604
710
|
};
|
|
605
711
|
}
|
|
606
712
|
|
|
713
|
+
/*
|
|
714
|
+
* 그림 파일을 Read 로 열었을 때.
|
|
715
|
+
*
|
|
716
|
+
* 두 갈래다.
|
|
717
|
+
*
|
|
718
|
+
* 눈이 있는 모델 — 도구 결과에는 "열었다" 는 말만 남기고, 그림 자체는
|
|
719
|
+
* 루프가 바로 뒤에 사람 말로 붙인다 (vision.js 의 그림메시지 머리말 참고).
|
|
720
|
+
* 여기서 base64 를 content 에 넣으면 안 된다. 도구 결과는 글 한 덩어리라
|
|
721
|
+
* 그림으로 안 읽히고, 그냥 자리만 5MB 먹는다.
|
|
722
|
+
*
|
|
723
|
+
* 눈이 없는 모델 — **바이트를 아예 안 싣는다.** 대신 한 줄로 말한다.
|
|
724
|
+
* 안 보이는 모델에게 보내 봐야 400 이 오거나, 더 나쁘게는 서버가 조용히
|
|
725
|
+
* 무시하고 답을 지어낸다. 그러면 사람은 모델이 화면을 봤다고 믿는다.
|
|
726
|
+
*/
|
|
727
|
+
function 그림보기(abs, ctx) {
|
|
728
|
+
const show = ctx.scope.show(abs);
|
|
729
|
+
const 눈있나 = !!ctx.눈있나;
|
|
730
|
+
const 것 = 그림읽기(abs, { 한도: 기본한도 });
|
|
731
|
+
|
|
732
|
+
if (!것.ok) {
|
|
733
|
+
return { error: `${show} — ${것.왜}` };
|
|
734
|
+
}
|
|
735
|
+
ctx.seen.add(abs);
|
|
736
|
+
const 잰것 = `그림 · ${크기말(것.bytes)} · ${것.mime}`;
|
|
737
|
+
|
|
738
|
+
if (!눈있나) {
|
|
739
|
+
return {
|
|
740
|
+
content: `${show} 는 그림입니다 (${잰것}).\n`
|
|
741
|
+
+ '지금 붙어 있는 모델은 그림을 못 봅니다 — 그래서 보내지 않았습니다.\n'
|
|
742
|
+
+ '이 파일을 코드처럼 읽으려 하지 마세요. 무엇이 찍혀 있는지 알아야 한다면'
|
|
743
|
+
+ ' 사용자에게 말로 설명해 달라고 하세요.',
|
|
744
|
+
summary: `${잰것} · 이 모델은 못 봄`,
|
|
745
|
+
그림없음: true,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
content: `${show} 를 열었습니다 (${잰것}). 그림은 이 다음 메시지에 붙어 있습니다.`,
|
|
750
|
+
summary: 잰것,
|
|
751
|
+
// 루프가 이걸 보고 사람 말 자리에 그림을 붙인다. 모델에게 가는 글이 아니다.
|
|
752
|
+
그림: { b64: 것.b64, mime: 것.mime, bytes: 것.bytes, show },
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
607
756
|
export const TOOLS = {
|
|
608
757
|
Read: {
|
|
609
758
|
schema: {
|
|
@@ -630,11 +779,15 @@ export const TOOLS = {
|
|
|
630
779
|
const 막을이유 = 내부살림(abs);
|
|
631
780
|
if (막을이유) return { error: 막을이유 };
|
|
632
781
|
|
|
782
|
+
// 그림은 글로 읽지 않는다. 읽으면 깨진 글자 수천 자가 대화에 실린다.
|
|
783
|
+
if (그림인가(abs)) return 그림보기(abs, ctx);
|
|
633
784
|
// 엑셀 파일은 글이 아니라 압축 꾸러미다. 그냥 읽으면 '바이너리' 로 끝난다.
|
|
634
785
|
// 여기서 표로 바꿔 돌려준다 — 사람이 손으로 CSV 로 내보낼 일이 없게.
|
|
635
786
|
if (isExcelPath(abs)) return 엑셀읽기(abs, args, ctx);
|
|
636
787
|
// hwpx·docx·pptx 도 같다 — 속이 ZIP+XML 이라 글로 바꿔 돌려준다 (docs.js).
|
|
637
788
|
if (isDocPath(abs)) return 문서읽기(abs, ctx);
|
|
789
|
+
// PDF 도 같다. 속은 사전+흐름이라 zlib 만으로 쪽마다 글을 꺼낸다 (pdf.js).
|
|
790
|
+
if (isPdfPath(abs)) return pdf읽기(abs, ctx);
|
|
638
791
|
/*
|
|
639
792
|
* 구형 hwp 는 '바이너리' 로 끝내지 않는다. 그 오류에는 길이 없어서
|
|
640
793
|
* 모델이 우회로(새로 쓰기)를 찾는다 — 실제로 그렇게 원본이 죽은 적이
|
|
@@ -658,13 +811,16 @@ export const TOOLS = {
|
|
|
658
811
|
const 줄상한 = 읽을줄수(ctx.모델컨텍스트);
|
|
659
812
|
const count = Math.min(args.limit ?? 줄상한, 줄상한);
|
|
660
813
|
const slice = lines.slice(start, start + count);
|
|
661
|
-
const
|
|
814
|
+
const 줄인것 = 붙박이그림줄이기(slice);
|
|
815
|
+
const body = 줄인것.줄들.map((l, i) => `${String(start + i + 1).padStart(6)}\t${l}`).join('\n');
|
|
662
816
|
const more = lines.length > start + count ? `\n… 전체 ${lines.length}줄 중 ${start + count}줄까지` : '';
|
|
663
817
|
ctx.seen.add(abs);
|
|
664
818
|
const 별난인코딩 = 읽음.encoding !== 'utf-8';
|
|
665
819
|
return {
|
|
666
|
-
content: clip(body + more),
|
|
667
|
-
summary: `${lines.length}줄`
|
|
820
|
+
content: clip(body + more + 줄인것.알림),
|
|
821
|
+
summary: `${lines.length}줄`
|
|
822
|
+
+ (줄인것.줄인바이트 ? ` · 그림 ${몇KB(줄인것.줄인바이트)} 생략` : '')
|
|
823
|
+
+ (별난인코딩 ? ` · ${encLabel(읽음.encoding)}` : ''),
|
|
668
824
|
};
|
|
669
825
|
},
|
|
670
826
|
},
|
|
@@ -899,18 +1055,27 @@ export const TOOLS = {
|
|
|
899
1055
|
run(args, ctx) {
|
|
900
1056
|
const root = args.path ? ctx.scope.resolve(args.path) : ctx.scope.root;
|
|
901
1057
|
const re = globToRegex(args.pattern);
|
|
902
|
-
const
|
|
1058
|
+
const 전부 = walk(root);
|
|
1059
|
+
// .gitignore 로 건너뛴 것은 수를 말한다 — 조용히 빼면 '그 파일이 없다' 로 읽힌다 (tools/ignore.js).
|
|
1060
|
+
const 건너뜀 = 건너뜀말(전부.건너뜀, 전부.잘림, 전부.상한);
|
|
1061
|
+
const 맞는것 = 전부
|
|
903
1062
|
.filter((f) => re.test(f.rel) || re.test(f.rel.split('/').pop()))
|
|
904
1063
|
.sort((a, b) => b.mtime - a.mtime);
|
|
905
1064
|
const files = 맞는것.slice(0, 찾을개수(ctx.모델컨텍스트));
|
|
906
|
-
|
|
1065
|
+
// 훑기 상한에서 멈췄으면 '없다' 가 아니라 '본 데까지는 없다' 다.
|
|
1066
|
+
if (!files.length) {
|
|
1067
|
+
return {
|
|
1068
|
+
content: `${전부.잘림 ? '본 데까지는 찾은 파일 없음' : '찾은 파일 없음'}: ${args.pattern}${건너뜀}`,
|
|
1069
|
+
summary: 전부.잘림 ? '0개 (다 못 봄)' : '0개',
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
907
1072
|
// 잘랐으면 잘랐다고 말한다. 전에는 '200개' 라고만 해서, 모델이 그게 전부인 줄
|
|
908
1073
|
// 알고 "전부 확인했습니다" 로 답을 맺었다. 실제로는 1,400개 중 200개였다.
|
|
909
1074
|
const 잘림 = 맞는것.length > files.length
|
|
910
1075
|
? `\n\n… 모두 ${맞는것.length}개인데 최근 것 ${files.length}개만 보여 줍니다. 범위를 좁혀 다시 찾으세요.`
|
|
911
1076
|
: '';
|
|
912
1077
|
return {
|
|
913
|
-
content: files.map((f) => ctx.scope.show(f.path)).join('\n') +
|
|
1078
|
+
content: files.map((f) => ctx.scope.show(f.path)).join('\n') + 잘림 + 건너뜀,
|
|
914
1079
|
summary: 맞는것.length > files.length ? `${files.length}/${맞는것.length}개` : `${files.length}개`,
|
|
915
1080
|
};
|
|
916
1081
|
},
|
|
@@ -941,20 +1106,88 @@ export const TOOLS = {
|
|
|
941
1106
|
|
|
942
1107
|
const root = args.path ? ctx.scope.resolve(args.path) : ctx.scope.root;
|
|
943
1108
|
const isFile = existsSync(root) && statSync(root).isFile();
|
|
1109
|
+
|
|
1110
|
+
const mode = args.output_mode ?? 'files_with_matches';
|
|
1111
|
+
const limit = args.head_limit ?? 찾을줄수(ctx.모델컨텍스트);
|
|
1112
|
+
const hitFiles = [];
|
|
1113
|
+
const lines = [];
|
|
1114
|
+
let total = 0;
|
|
1115
|
+
|
|
1116
|
+
/*
|
|
1117
|
+
* 이 PC 에 rg 나 git grep 이 이미 있으면 빌려 쓴다 (tools/fastgrep.js).
|
|
1118
|
+
*
|
|
1119
|
+
* 깔지는 않는다 — 없으면 아래 자바스크립트 길로 그대로 간다. 5만 개짜리
|
|
1120
|
+
* 저장소에서 수십 초가 1초 안쪽이 된다. 대신 **무엇으로 찾았는지 반드시
|
|
1121
|
+
* 적는다.** rg 의 정규식 문법은 자바스크립트와 조금 달라서, 결과가
|
|
1122
|
+
* 다르게 나왔을 때 무엇으로 찾은 것인지 모르면 사람은 코드를 의심한다.
|
|
1123
|
+
*/
|
|
1124
|
+
const 무시파일 = join(ctx.scope.root, '.deelignore');
|
|
1125
|
+
const 빠른것 = isFile ? null : 빠르게찾기({
|
|
1126
|
+
무늬: args.pattern,
|
|
1127
|
+
자리: root,
|
|
1128
|
+
glob: args.glob ?? null,
|
|
1129
|
+
대소문자무시: !!args['-i'],
|
|
1130
|
+
무시파일: existsSync(무시파일) ? 무시파일 : null,
|
|
1131
|
+
최대: Math.max(limit * 4, 2000),
|
|
1132
|
+
});
|
|
1133
|
+
if (빠른것) {
|
|
1134
|
+
const 파일별 = new Map();
|
|
1135
|
+
for (const x of 빠른것.줄들) {
|
|
1136
|
+
// 우리 규칙(글 아닌 것·큰 파일)은 rg 쪽 옵션으로 이미 걸었다. 여기서는 세기만.
|
|
1137
|
+
const rel = ctx.scope.show(x.파일);
|
|
1138
|
+
파일별.set(rel, (파일별.get(rel) ?? 0) + 1);
|
|
1139
|
+
total += 1;
|
|
1140
|
+
if (mode === 'content' && lines.length < limit) {
|
|
1141
|
+
const num = args['-n'] === false ? '' : `:${x.줄}`;
|
|
1142
|
+
lines.push(`${rel}${num}: ${x.내용.trim().slice(0, 200)}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
for (const [rel, n] of 파일별) hitFiles.push({ rel, n });
|
|
1146
|
+
/*
|
|
1147
|
+
* 꼬리에 무엇을 적나.
|
|
1148
|
+
*
|
|
1149
|
+
* 자바스크립트 길은 '몇 개를 건너뛰었는지' 를 세어서 적는다. rg 는 그
|
|
1150
|
+
* 숫자를 안 알려준다. 그러면 **모르는 것을 안 적는다** — 지어낸 숫자를
|
|
1151
|
+
* 적느니 안 셌다고 말하는 편이 낫다.
|
|
1152
|
+
*/
|
|
1153
|
+
const 꼬리2 = [
|
|
1154
|
+
빠른것.잘림 ? '(결과가 많아 앞부분만 봤습니다 — 더 있을 수 있습니다)' : '',
|
|
1155
|
+
엔진말(빠른것.엔진),
|
|
1156
|
+
'(.gitignore·.deelignore 는 지켰습니다. 건너뛴 수는 안 셌습니다)',
|
|
1157
|
+
].filter(Boolean).join(' ');
|
|
1158
|
+
const 붙이기2 = (t) => (꼬리2 ? [t, '', 꼬리2].join('\n') : t);
|
|
1159
|
+
if (!total) return { content: 붙이기2(`일치 없음: ${args.pattern}`), summary: `0건 · ${빠른것.엔진}` };
|
|
1160
|
+
if (mode === 'content') return { content: 붙이기2(clip(lines.join('\n'))), summary: `${total}건 · ${빠른것.엔진}` };
|
|
1161
|
+
if (mode === 'count') {
|
|
1162
|
+
return {
|
|
1163
|
+
content: 붙이기2(hitFiles.slice(0, limit).map((f) => `${f.n}\t${f.rel}`).join('\n')),
|
|
1164
|
+
summary: `${hitFiles.length}개 파일 · ${빠른것.엔진}`,
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
return {
|
|
1168
|
+
content: 붙이기2(hitFiles.slice(0, limit).map((f) => f.rel).join('\n')),
|
|
1169
|
+
summary: `${hitFiles.length}개 파일 · ${total}건 · ${빠른것.엔진}`,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/*
|
|
1174
|
+
* ── 여기서부터 예전 길 (자바스크립트로 하나씩 연다) ──
|
|
1175
|
+
*
|
|
1176
|
+
* 폴더를 훑는 일(`walk`)을 여기까지 미뤄 두었다. 빠른 엔진이 답을 준
|
|
1177
|
+
* 경우에는 훑을 까닭이 없는데, 전에는 위에서 먼저 훑고 있었다 —
|
|
1178
|
+
* 그러면 rg 를 빌려 쓰고도 제일 오래 걸리는 일을 그대로 한 셈이 된다.
|
|
1179
|
+
*/
|
|
944
1180
|
let files = isFile
|
|
945
1181
|
? [{ path: root, rel: ctx.scope.show(root) }]
|
|
946
1182
|
: walk(root);
|
|
1183
|
+
const 안본것 = isFile ? '' : 건너뜀말(files.건너뜀, files.잘림, files.상한).trim(); // .gitignore 로 건너뛴 수 — 꼬리에 적는다
|
|
1184
|
+
// 훑기 상한에 걸렸으면 "일치 없음" 이라고 잘라 말하면 안 된다. 안 본 것이다.
|
|
1185
|
+
const 다못봄 = !isFile && !!files.잘림;
|
|
947
1186
|
if (args.glob) {
|
|
948
1187
|
const g = globToRegex(args.glob);
|
|
949
1188
|
files = files.filter((f) => g.test(f.rel) || g.test(f.rel.split('/').pop()));
|
|
950
1189
|
}
|
|
951
1190
|
|
|
952
|
-
const mode = args.output_mode ?? 'files_with_matches';
|
|
953
|
-
const limit = args.head_limit ?? 찾을줄수(ctx.모델컨텍스트);
|
|
954
|
-
const hitFiles = [];
|
|
955
|
-
const lines = [];
|
|
956
|
-
let total = 0;
|
|
957
|
-
|
|
958
1191
|
/*
|
|
959
1192
|
* 큰 파일과 글이 아닌 파일은 건너뛴다.
|
|
960
1193
|
*
|
|
@@ -993,10 +1226,16 @@ export const TOOLS = {
|
|
|
993
1226
|
멈춤 === '중단' ? '(중단하셔서 여기까지만 찾았습니다)' : '',
|
|
994
1227
|
멈춤 === '상한' ? `(${limit}개에서 멈췄습니다 — 더 있을 수 있습니다)` : '',
|
|
995
1228
|
건너뛴것 ? `(글이 아니거나 너무 큰 파일 ${건너뛴것}개는 건너뛰었습니다)` : '',
|
|
1229
|
+
안본것,
|
|
996
1230
|
].filter(Boolean).join(' ');
|
|
997
1231
|
const 붙이기 = (s) => (꼬리 ? `${s}\n\n${꼬리}` : s);
|
|
998
1232
|
|
|
999
|
-
if (!total)
|
|
1233
|
+
if (!total) {
|
|
1234
|
+
return {
|
|
1235
|
+
content: 붙이기(다못봄 ? `본 데까지는 일치 없음: ${args.pattern}` : `일치 없음: ${args.pattern}`),
|
|
1236
|
+
summary: 다못봄 ? '0건 (다 못 봄)' : '0건',
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1000
1239
|
if (mode === 'content') return { content: 붙이기(clip(lines.join('\n'))), summary: `${total}건` };
|
|
1001
1240
|
if (mode === 'count') {
|
|
1002
1241
|
return { content: 붙이기(hitFiles.map((f) => `${f.n}\t${f.rel}`).join('\n')), summary: `${hitFiles.length}개 파일` };
|
|
@@ -1105,27 +1344,9 @@ export const TOOLS = {
|
|
|
1105
1344
|
};
|
|
1106
1345
|
}
|
|
1107
1346
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
* 무슨 일이 있었나:
|
|
1112
|
-
* Node 는 인자를 넘길 때 따옴표를 \" 로 바꿔 준다. 그런데 cmd.exe 는
|
|
1113
|
-
* \" 를 모른다. 그래서 따옴표가 든 명령이 통째로 뭉개졌다 —
|
|
1114
|
-
* node -e "console.log(1)" → 아무것도 안 하고 **종료코드 0**
|
|
1115
|
-
* 출력도 없고 오류도 없이 '성공' 이다. 모델은 잘된 줄 알고 넘어간다.
|
|
1116
|
-
* `node -e`, `python -c`, `git commit -m "..."` 이 전부 이 자리였다.
|
|
1117
|
-
*
|
|
1118
|
-
* Node 의 exec() 가 안에서 하는 것과 똑같이 맞춘다 — 명령을 통째로
|
|
1119
|
-
* 따옴표로 감싸고, 인자를 손대지 말라고(verbatim) 일러 준다.
|
|
1120
|
-
* /s 는 그 감싼 따옴표 한 쌍을 벗기라는 뜻이라 짝이 맞는다.
|
|
1121
|
-
*/
|
|
1122
|
-
const shell = process.platform === 'win32'
|
|
1123
|
-
? {
|
|
1124
|
-
file: process.env.COMSPEC ?? 'cmd.exe',
|
|
1125
|
-
args: ['/d', '/s', '/c', `"${cmd}"`],
|
|
1126
|
-
verbatim: true,
|
|
1127
|
-
}
|
|
1128
|
-
: { file: '/bin/sh', args: ['-c', cmd] };
|
|
1347
|
+
// 어느 셸로 넘기나 — tools/shell.js 가 정한다. Jobs 와 같은 답이어야 하므로 한 군데다.
|
|
1348
|
+
// (윈도우 cmd 의 따옴표 문제와 그 해법도 거기 적혀 있다.)
|
|
1349
|
+
const shell = 셸명령(cmd);
|
|
1129
1350
|
|
|
1130
1351
|
const 제한 = args.timeout ?? 120000;
|
|
1131
1352
|
return new Promise((끝) => {
|
|
@@ -1529,6 +1750,26 @@ export function 설명줄이기(schema, 한도) {
|
|
|
1529
1750
|
* 빈 설명을 내보내지 않는다. 설명 없는 도구는 모델이 언제 쓰는지 모른 채로
|
|
1530
1751
|
* 목록에만 서 있게 되는데, 그건 없는 것보다 나쁘다.
|
|
1531
1752
|
*/
|
|
1753
|
+
/*
|
|
1754
|
+
* 그림 이야기는 **볼 수 있는 모델에게만** 한다.
|
|
1755
|
+
*
|
|
1756
|
+
* 못 보는 모델에게 "그림도 읽을 수 있다" 고 적어 두면 모델은 화면 사진을
|
|
1757
|
+
* 열려 들고, 열어 봐야 "이 모델은 못 봅니다" 를 받는다. 그 한 걸음이 매번
|
|
1758
|
+
* 헛간다 — 작은 로컬 모델일수록 그 한 걸음이 아깝다.
|
|
1759
|
+
*
|
|
1760
|
+
* 반대로 볼 수 있는데 안 적어 두면 그림 파일을 아예 안 연다. 사람이 화면
|
|
1761
|
+
* 사진을 폴더에 넣어 두고 "이거 봐" 라고 해도 이름만 보고 지나간다.
|
|
1762
|
+
*/
|
|
1763
|
+
function 눈붙이기(fn, 이름, vision) {
|
|
1764
|
+
if (이름 !== 'Read' || !vision) return fn;
|
|
1765
|
+
// 화면 말을 그대로 본다. 설명 글자를 보고 짐작하면 안 된다 — 줄이기가 문장
|
|
1766
|
+
// 한복판을 자르므로 마지막 글자가 무엇일지 정해져 있지 않다.
|
|
1767
|
+
const 덧말 = 지시말() === 'en'
|
|
1768
|
+
? ' Screenshots and images (.png/.jpg/.gif/.webp) can be opened too — this model can see them.'
|
|
1769
|
+
: ' 화면 사진·그림(.png/.jpg/.gif/.webp)도 그대로 열 수 있다 — 지금 붙어 있는 모델은 그림을 본다.';
|
|
1770
|
+
return { ...fn, description: String(fn.description ?? '') + 덧말 };
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1532
1773
|
function 영어설명(schema, 이름) {
|
|
1533
1774
|
if (지시말() !== 'en') return schema;
|
|
1534
1775
|
const 것 = 도구설명EN[이름];
|
|
@@ -1549,7 +1790,7 @@ function 영어설명(schema, 이름) {
|
|
|
1549
1790
|
|
|
1550
1791
|
// 모델에게 넘길 도구 정의 목록.
|
|
1551
1792
|
// 스킬이 없으면 Skill 도구는 빼서 자리를 아낀다.
|
|
1552
|
-
export function toolSchemas(names = null, { hasSkills = false, web = true, work = null, mcp = null, ctx = null, lsp = false } = {}) {
|
|
1793
|
+
export function toolSchemas(names = null, { hasSkills = false, web = true, work = null, mcp = null, ctx = null, lsp = false, vision = false } = {}) {
|
|
1553
1794
|
let list = names ?? Object.keys(TOOLS).filter((n) => {
|
|
1554
1795
|
if (n === 'Skill') return hasSkills;
|
|
1555
1796
|
if (n === 'WebFetch') return web;
|
|
@@ -1590,7 +1831,9 @@ export function toolSchemas(names = null, { hasSkills = false, web = true, work
|
|
|
1590
1831
|
*/
|
|
1591
1832
|
const 우리것 = list.map((n) => ({
|
|
1592
1833
|
type: 'function',
|
|
1593
|
-
|
|
1834
|
+
// 눈 이야기는 줄인 **뒤에** 붙인다. 먼저 붙이면 한도에 걸려 그 한 문장이
|
|
1835
|
+
// 그대로 잘려 나간다 — 좁은 창일수록 정작 알려야 할 때 안 실린다.
|
|
1836
|
+
function: 눈붙이기(설명줄이기(영어설명(TOOLS[n].schema, n), 한도), n, vision),
|
|
1594
1837
|
}));
|
|
1595
1838
|
|
|
1596
1839
|
/*
|
package/src/tools/jobs.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { spawn, execFileSync } from 'node:child_process';
|
|
27
27
|
import { decode as decodeBytes, consoleCodepage } from './encoding.js';
|
|
28
|
+
import { 셸명령 } from './shell.js';
|
|
28
29
|
|
|
29
30
|
// 한 일감이 **들고 있을** 출력 상한. 넘으면 앞을 버리고 뒤를 남긴다 —
|
|
30
31
|
// 오래 도는 것에서 필요한 건 언제나 **방금** 나온 쪽이다.
|
|
@@ -61,18 +62,9 @@ const 끝난것상한 = 8;
|
|
|
61
62
|
*/
|
|
62
63
|
const 죽는말기다림 = 700;
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
* 윈도우에서 여기가 조용히 틀리면 따옴표가 든 명령이 통째로 뭉개진다 —
|
|
68
|
-
* 출력도 오류도 없이 **종료코드 0** 이다. Bash 쪽과 같은 값을 쓴다.
|
|
69
|
-
* 한 군데서만 정의하는 것이 중요하다. 두 벌이 되면 한쪽만 고쳐진다.
|
|
70
|
-
*/
|
|
71
|
-
export function 셸명령(cmd) {
|
|
72
|
-
return process.platform === 'win32'
|
|
73
|
-
? { file: process.env.COMSPEC ?? 'cmd.exe', args: ['/d', '/s', '/c', `"${cmd}"`], verbatim: true }
|
|
74
|
-
: { file: '/bin/sh', args: ['-c', cmd], verbatim: false };
|
|
75
|
-
}
|
|
65
|
+
// 명령을 셸에 넘기는 방법은 tools/shell.js 한 군데서 정한다. Bash 도구와 같은 값이어야
|
|
66
|
+
// 한다 — 두 벌이 되면 한쪽만 고쳐지고, 윈도우에서 따옴표가 든 명령이 조용히 뭉개진다.
|
|
67
|
+
export { 셸명령 };
|
|
76
68
|
|
|
77
69
|
/**
|
|
78
70
|
* 어떻게 띄우나 — spawn 에 넘길 것들.
|
package/src/tools/outline.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
33
33
|
import { extname } from 'node:path';
|
|
34
34
|
import { walk, SKIP_DIRS, globToRegex, 내부살림 } from './fsutil.js';
|
|
35
|
+
import { 건너뜀말 } from './ignore.js';
|
|
35
36
|
import { decode, looksBinary } from './encoding.js';
|
|
36
37
|
import { 찾을개수, 뼈대줄수 } from '../agent/budget.js';
|
|
37
38
|
|
|
@@ -232,6 +233,7 @@ export const OUTLINE_TOOL = {
|
|
|
232
233
|
let 파일들 = 하나인가
|
|
233
234
|
? [{ path: 시작, rel: ctx.scope.show(시작), mtime: statSync(시작).mtimeMs }]
|
|
234
235
|
: walk(시작, { skipDirs: SKIP_DIRS });
|
|
236
|
+
const 건너뜀 = 하나인가 ? '' : 건너뜀말(파일들.건너뜀, 파일들.잘림, 파일들.상한); // .gitignore 로 건너뛴 수 (tools/ignore.js)
|
|
235
237
|
|
|
236
238
|
// 좁히는 방식은 Glob 도구와 **같은 것**을 쓴다. 두 도구가 같은 패턴에
|
|
237
239
|
// 다르게 답하면 모델이 둘 중 어느 것을 믿어야 할지 알 수 없다.
|
|
@@ -323,7 +325,7 @@ export const OUTLINE_TOOL = {
|
|
|
323
325
|
}
|
|
324
326
|
|
|
325
327
|
return {
|
|
326
|
-
content: 줄들.join('\n').trimEnd()
|
|
328
|
+
content: 줄들.join('\n').trimEnd() + 건너뜀,
|
|
327
329
|
summary: `${뼈대있는파일}개 파일 · ${항목수}곳`
|
|
328
330
|
+ (못읽은것.size ? ` · 못 읽음 ${[...못읽은것.values()].reduce((a, x) => a + x.length, 0)}개` : ''),
|
|
329
331
|
};
|