deel-local-cli 1.20.12 → 2.0.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.ko.md +96 -77
- package/README.md +119 -80
- package/bin/deel.js +398 -32
- package/package.json +2 -1
- package/src/acp/jsonrpc.js +87 -7
- package/src/acp/map.js +62 -13
- package/src/acp/serve.js +338 -30
- package/src/agent/agents.js +110 -12
- package/src/agent/askcheck.js +50 -6
- package/src/agent/asks.js +59 -5
- package/src/agent/budget.js +9 -2
- package/src/agent/card.js +8 -2
- package/src/agent/commit.js +168 -16
- package/src/agent/compact.js +93 -21
- package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
- package/src/agent/effort.js +85 -15
- package/src/agent/evidence.js +63 -7
- package/src/agent/evolve.js +132 -24
- package/src/agent/grade.js +12 -3
- package/src/agent/loop.js +408 -81
- package/src/agent/memory.js +176 -29
- package/src/agent/mention.js +22 -5
- package/src/agent/models.js +31 -7
- package/src/agent/modes.js +25 -10
- package/src/agent/outschema.js +381 -56
- package/src/agent/pins.js +33 -2
- package/src/agent/project.js +73 -8
- package/src/agent/recall.js +40 -9
- package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
- package/src/agent/review.js +143 -13
- package/src/agent/route.js +506 -32
- package/src/agent/salvage.js +2 -1
- package/src/agent/session.js +457 -31
- package/src/agent/store.js +232 -22
- package/src/agent/threads.js +52 -3
- package/src/backend/adapter.js +508 -85
- package/src/backend/azure.js +12 -1
- package/src/backend/cachemark.js +31 -5
- package/src/backend/clientcert.js +23 -1
- package/src/backend/ctxsize.js +56 -9
- package/src/backend/detect.js +252 -19
- package/src/backend/http.js +383 -42
- package/src/backend/learn.js +101 -11
- package/src/backend/mcp.js +333 -24
- package/src/backend/price.js +61 -13
- package/src/backend/probe.js +188 -36
- package/src/backend/proxy.js +108 -14
- package/src/backend/quota.js +226 -30
- package/src/backend/retry.js +46 -3
- package/src/backend/scan.js +47 -10
- package/src/backend/scanui.js +57 -7
- package/src/backend/toolfit.js +204 -22
- package/src/backend/vision.js +14 -1
- package/src/backend/wire.js +58 -10
- package/src/cmdnames.js +16 -0
- package/src/commands/common.js +70 -0
- package/src/commands/extend.js +300 -0
- package/src/commands/model.js +852 -0
- package/src/commands/view.js +328 -0
- package/src/commands/work.js +823 -0
- package/src/commands.js +193 -2082
- package/src/completion.js +45 -3
- package/src/config.js +452 -39
- package/src/configexplain.js +125 -14
- package/src/doctor.js +164 -26
- package/src/i18n/en.js +22 -0
- package/src/i18n/index.js +31 -1
- package/src/i18n/ja.js +25 -1
- package/src/i18n/ko.js +25 -2
- package/src/i18n/zh.js +25 -1
- package/src/lsp/client.js +111 -9
- package/src/lsp/servers.js +86 -22
- package/src/oneshot.js +325 -65
- package/src/pack/sbom.js +28 -5
- package/src/pack/selfpack.js +54 -5
- package/src/pack/sheet.en.js +24 -3
- package/src/pack/tar.js +45 -8
- package/src/pack/zip.js +103 -8
- package/src/plugins/manage.js +165 -14
- package/src/preview/serve.js +239 -23
- package/src/providers/bedrock.js +30 -5
- package/src/providers/gemini.js +8 -0
- package/src/providers/index.js +17 -1
- package/src/repl.js +254 -61
- package/src/report.js +7 -2
- package/src/reset.js +151 -25
- package/src/safety/audit.js +108 -8
- package/src/safety/authcmd.js +75 -5
- package/src/safety/guard.js +507 -15
- package/src/safety/hooks.js +172 -20
- package/src/safety/keystore.js +178 -18
- package/src/safety/network.js +209 -6
- package/src/safety/policy.js +377 -36
- package/src/safety/runmode.js +9 -2
- package/src/safety/secrets.js +243 -17
- package/src/safety/shellenv.js +64 -3
- package/src/safety/trust.js +298 -32
- package/src/safety/undo.js +409 -17
- package/src/setup.js +175 -17
- package/src/skills/discover.js +122 -19
- package/src/stats.js +57 -16
- package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
- package/src/tools/clipboard.js +53 -20
- package/src/tools/convert.js +118 -37
- package/src/tools/desc.en.js +48 -5
- package/src/tools/doc2md.js +11 -1
- package/src/tools/docs.js +94 -18
- package/src/tools/edit-match.js +212 -13
- package/src/tools/encoding.js +525 -28
- package/src/tools/excel-com.js +63 -15
- package/src/tools/excel.js +12 -7
- package/src/tools/fastgrep.js +444 -36
- package/src/tools/fig.js +40 -9
- package/src/tools/fsutil.js +235 -21
- package/src/tools/hwpxwrite.js +80 -19
- package/src/tools/ignore.js +55 -7
- package/src/tools/index.js +1029 -145
- package/src/tools/jobs.js +325 -50
- package/src/tools/kiwi.js +50 -5
- package/src/tools/lsp.js +164 -32
- package/src/tools/outline.js +52 -12
- package/src/tools/pdf.js +366 -56
- package/src/tools/shell.js +12 -4
- package/src/tools/spawn.js +203 -18
- package/src/tools/todo.js +73 -4
- package/src/tools/verify.js +312 -43
- package/src/tools/webfetch.js +184 -25
- package/src/tools/xlsx.js +232 -31
- package/src/ui/ansi.js +209 -15
- package/src/ui/banner.js +2 -1
- package/src/ui/complete.js +46 -4
- package/src/ui/diff.js +4 -2
- package/src/ui/export.js +91 -17
- package/src/ui/inputbox.js +58 -18
- package/src/ui/intro.js +9 -2
- package/src/ui/level.js +32 -7
- package/src/ui/md.js +101 -15
- package/src/ui/motion.js +5 -2
- package/src/ui/notify.js +7 -1
- package/src/ui/office.js +37 -5
- package/src/ui/pastechip.js +3 -2
- package/src/ui/pick.js +85 -12
- package/src/ui/prompt.js +176 -13
- package/src/ui/screen.js +2 -1
- package/src/ui/spinner.js +6 -1
- package/src/ui/status.js +80 -12
- package/src/ui/wrap.js +23 -37
- /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
- /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
- /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
- /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/agent/project.js
CHANGED
|
@@ -27,7 +27,13 @@
|
|
|
27
27
|
* 느려지고, 어차피 프롬프트에 넣을 양은 한 줄뿐이다.
|
|
28
28
|
*
|
|
29
29
|
* 이 글도 **고정 몫**이다 — 매 요청에 통째로 나간다. 그래서 창 크기에 맞춘다.
|
|
30
|
-
*
|
|
30
|
+
*
|
|
31
|
+
* 줄이는 자는 **줄 수가 아니라 줄 길이**다 (2.0.0 8회차 판정에서 바로잡음). 여기는
|
|
32
|
+
* 「8k 에서는 두 줄, 큰 창에서는 네 줄」 이라 적어 뒀는데 어느 창에서나 네 줄이
|
|
33
|
+
* 나갔다 — 창에 따라 달라지는 것은 위쪽 몇 개(10·16·24)와 명령 몇 개(4·6·8)다.
|
|
34
|
+
* 줄을 빼지 않는 데는 까닭이 있다: 위쪽 목록을 통째로 빼면 「그 밖에 N개」 라는
|
|
35
|
+
* 말도 같이 사라져, 모델은 적힌 것이 이 폴더의 전부인 줄 안다.
|
|
36
|
+
* 늘리면 test/compact.test.js 가 먼저 빨개진다.
|
|
31
37
|
*/
|
|
32
38
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
33
39
|
import { join } from 'node:path';
|
|
@@ -40,6 +46,44 @@ const 안적을것 = new Set([
|
|
|
40
46
|
'coverage', '.DS_Store', 'Thumbs.db',
|
|
41
47
|
]);
|
|
42
48
|
|
|
49
|
+
/*
|
|
50
|
+
* ── 여기 옮기는 글자는 **남이 적은 것**이다 (2.0.0 4회차 사냥) ─────────────
|
|
51
|
+
*
|
|
52
|
+
* package.json 의 이름·스크립트 이름, 위쪽 파일 이름, .git/HEAD 의 가지 — 전부
|
|
53
|
+
* 저장소를 만든 사람이 정한다. 그걸 그대로 옮기고 있었다. 이름에 줄바꿈 하나를
|
|
54
|
+
* 넣으면 시스템 글 한가운데 `--- 사용자 규칙 (위 원칙보다 우선) ---` 같은 머리줄을
|
|
55
|
+
* 제 손으로 세울 수 있었고, 5만 자짜리 이름은 이 파일 머리말이 「고정 몫」 이라고
|
|
56
|
+
* 아껴 둔 자리를 통째로 먹었다.
|
|
57
|
+
*
|
|
58
|
+
* 그래서 두 가지를 못 박는다.
|
|
59
|
+
* · **한 줄**이다 — 제어 글자(줄바꿈·CR·탭·줄 나눔 U+2028/2029)가 든 것은 안 싣는다.
|
|
60
|
+
* 바꿔 적지 않고 뺀다. 바꿔 적은 스크립트 이름은 이 폴더에 없는 명령이 되고,
|
|
61
|
+
* 없는 명령을 알려 주면 모델이 그걸 부른다(명령들 머리말).
|
|
62
|
+
* · **길이 상한**이 있다 — 넘으면 이름은 잘라 표시를 붙이고, 명령·파일 이름은 뺀다.
|
|
63
|
+
*/
|
|
64
|
+
const 제어글자 = /[\u0000-\u001f\u007f\u2028\u2029]/;
|
|
65
|
+
const 한줄인가 = (s, 최대) => typeof s === 'string' && s.length > 0 && s.length <= 최대 && !제어글자.test(s);
|
|
66
|
+
const 이름최대 = 80;
|
|
67
|
+
const 명령이름최대 = 60;
|
|
68
|
+
const 가지최대 = 120;
|
|
69
|
+
|
|
70
|
+
/** 떨어져 나온 머리(detached HEAD). 135-136 주석이 적은 말과 같은 글이어야 한다. */
|
|
71
|
+
export const 떨어진머리 = '가지 없음 (detached)';
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* 확장자로만 갈래가 정해지는 것들 (2.0.0 8회차 판정).
|
|
75
|
+
*
|
|
76
|
+
* 머리말은 갈래에 c# 을 적어 뒀는데 아래 표식에는 없었다 — .NET 은 이름이 고정된
|
|
77
|
+
* 파일이 없고 `<이름>.csproj` · `<이름>.sln` 처럼 이름이 프로젝트마다 다르다.
|
|
78
|
+
* 그래서 파일 이름 대조로는 영영 안 걸렸고, C# 폴더에서 켜면 갈래가 null 이었다.
|
|
79
|
+
* 위쪽 한 겹은 어차피 읽으므로(위쪽) 확장자만 한 번 더 본다.
|
|
80
|
+
*/
|
|
81
|
+
const 확장자표식 = [
|
|
82
|
+
{ 확장자: '.csproj', 갈래: 'c#' },
|
|
83
|
+
{ 확장자: '.sln', 갈래: 'c#' },
|
|
84
|
+
{ 확장자: '.fsproj', 갈래: 'f#' },
|
|
85
|
+
];
|
|
86
|
+
|
|
43
87
|
// 파일 하나로 갈래가 정해지는 것들. 위에 있는 것이 먼저 이긴다.
|
|
44
88
|
const 표식 = [
|
|
45
89
|
{ 파일: 'package.json', 갈래: 'node' },
|
|
@@ -63,6 +107,7 @@ function 위쪽(root, 상한) {
|
|
|
63
107
|
const 파일 = [];
|
|
64
108
|
for (const n of 것들) {
|
|
65
109
|
if (안적을것.has(n) || n.startsWith('.')) continue;
|
|
110
|
+
if (!한줄인가(n, 명령이름최대)) continue; // 리눅스에서는 파일 이름에도 줄바꿈이 들어간다
|
|
66
111
|
try { (statSync(join(root, n)).isDirectory() ? 폴더 : 파일).push(n); }
|
|
67
112
|
catch { /* 읽는 중에 사라졌으면 없는 셈 */ }
|
|
68
113
|
}
|
|
@@ -85,7 +130,7 @@ function 명령들(root, 갈래, 상한) {
|
|
|
85
130
|
if (!s || typeof s !== 'object') return [];
|
|
86
131
|
// 사람이 실제로 부르는 것부터. 나머지는 알파벳순으로 채운다.
|
|
87
132
|
const 앞선것 = ['dev', 'start', 'test', 'build', 'lint', 'typecheck'];
|
|
88
|
-
const 있는것 = Object.keys(s);
|
|
133
|
+
const 있는것 = Object.keys(s).filter((n) => 한줄인가(n, 명령이름최대));
|
|
89
134
|
const 골라 = [
|
|
90
135
|
...앞선것.filter((n) => 있는것.includes(n)),
|
|
91
136
|
...있는것.filter((n) => !앞선것.includes(n)).sort(),
|
|
@@ -99,7 +144,11 @@ function 이름(root, 갈래) {
|
|
|
99
144
|
if (갈래 !== 'node') return null;
|
|
100
145
|
try {
|
|
101
146
|
const n = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))?.name;
|
|
102
|
-
|
|
147
|
+
if (typeof n !== 'string' || !n.trim()) return null;
|
|
148
|
+
// 이름은 알려 주는 값이지 부르는 값이 아니라서 빼지 않고 한 줄로 펴서 자른다.
|
|
149
|
+
const 편것 = n.replace(new RegExp(제어글자.source, 'g'), ' ').replace(/\s+/g, ' ').trim();
|
|
150
|
+
if (!편것) return null;
|
|
151
|
+
return 편것.length > 이름최대 ? `${편것.slice(0, 이름최대)}…` : 편것;
|
|
103
152
|
} catch { return null; }
|
|
104
153
|
}
|
|
105
154
|
|
|
@@ -114,11 +163,22 @@ export function git가지(root) {
|
|
|
114
163
|
try {
|
|
115
164
|
const head = readFileSync(join(root, '.git', 'HEAD'), 'utf8').trim();
|
|
116
165
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
117
|
-
|
|
118
|
-
|
|
166
|
+
// 가지 이름도 저장소에 딸려 온 파일에서 온다. 한 줄이 아니거나 너무 길면 가지를 모르는 셈 친다.
|
|
167
|
+
if (m) return 한줄인가(m[1], 가지최대) ? m[1] : null;
|
|
168
|
+
return head ? 떨어진머리 : null;
|
|
119
169
|
} catch { return null; }
|
|
120
170
|
}
|
|
121
171
|
|
|
172
|
+
/** 위쪽 한 겹에서 확장자만 보고 갈래를 집는다. 못 찾으면 null. */
|
|
173
|
+
function 확장자로갈래(root) {
|
|
174
|
+
let 것들;
|
|
175
|
+
try { 것들 = readdirSync(root, { withFileTypes: true }); } catch { return null; }
|
|
176
|
+
for (const t of 확장자표식) {
|
|
177
|
+
if (것들.some((d) => d.isFile() && d.name.toLowerCase().endsWith(t.확장자))) return t.갈래;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
122
182
|
/**
|
|
123
183
|
* 이 폴더의 지문.
|
|
124
184
|
*
|
|
@@ -137,14 +197,20 @@ export function 지문(root, 창 = null) {
|
|
|
137
197
|
for (const t of 표식) {
|
|
138
198
|
if (existsSync(join(root, t.파일))) { 갈래 = t.갈래; break; }
|
|
139
199
|
}
|
|
200
|
+
// 이름이 고정된 파일이 없는 갈래(.NET)는 확장자로 본다 — 위 확장자표식 머리말.
|
|
201
|
+
if (!갈래) 갈래 = 확장자로갈래(root);
|
|
140
202
|
|
|
141
203
|
const 줄들 = [];
|
|
142
204
|
const 이 = 이름(root, 갈래);
|
|
143
205
|
const 가지 = git가지(root);
|
|
144
206
|
|
|
207
|
+
// 이 토막은 모델이 읽는 글이라 화면 말을 따라간다 — 머리줄도 마찬가지다.
|
|
208
|
+
// 여태 여기만 한국어가 박혀 있어서 영어 화면에도 「node 프로젝트」 가 나갔다 (8회차 판정).
|
|
209
|
+
const 영 = 언어() === 'en';
|
|
210
|
+
const 가지말 = 가지 === 떨어진머리 && 영 ? 'no branch (detached)' : 가지;
|
|
145
211
|
const 머리 = [
|
|
146
|
-
갈래 ? `${갈래} 프로젝트${이 ? ` (${이})` : ''}` : null,
|
|
147
|
-
가지 ? `git ${
|
|
212
|
+
갈래 ? (영 ? `${갈래} project${이 ? ` (${이})` : ''}` : `${갈래} 프로젝트${이 ? ` (${이})` : ''}`) : null,
|
|
213
|
+
가지 ? `git ${가지말}` : null,
|
|
148
214
|
].filter(Boolean).join(' · ');
|
|
149
215
|
if (머리) 줄들.push(머리);
|
|
150
216
|
|
|
@@ -153,7 +219,6 @@ export function 지문(root, 창 = null) {
|
|
|
153
219
|
// '이 프로젝트에서 검사를 어떻게 돌리나' 가 여기 다 들어 있다.
|
|
154
220
|
// 이 토막도 모델이 읽는 글이라 화면 말을 따라간다. 값(명령 이름·파일 이름)은
|
|
155
221
|
// 그대로 둔다 — 그건 이 폴더에 실제로 있는 것이라 옮길 것이 아니다.
|
|
156
|
-
const 영 = 언어() === 'en';
|
|
157
222
|
if (명.length) 줄들.push(`${영 ? 'Runnable here' : '돌릴 수 있는 것'}: ${명.join(' · ')}`);
|
|
158
223
|
|
|
159
224
|
const w = 위쪽(root, 위쪽상한);
|
package/src/agent/recall.js
CHANGED
|
@@ -35,16 +35,34 @@ const 메시지최대 = 200 * 1024;
|
|
|
35
35
|
* (의존성 0). 이 정도만 해도 "인코딩을 어떻게" 로 "인코딩" 을 찾아낸다.
|
|
36
36
|
*/
|
|
37
37
|
export function 낱말쪼개기(물음) {
|
|
38
|
+
return 낱말묶기(물음).flat();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 같은 낱말의 여러 꼴을 **한 묶음**으로 쪼갠다 — `[['인코딩을','인코딩'], ['어떻게']]`.
|
|
43
|
+
*
|
|
44
|
+
* 점수는 「몇 낱말이 맞았나」 로 매기는데(찾기 의 점수 머리말), 꼴을 낱낱이 세면
|
|
45
|
+
* 조사 하나가 낱말 하나로 셈된다. `인코딩을 어떻게` 는 낱말이 **셋**(인코딩을 ·
|
|
46
|
+
* 인코딩 · 어떻게)이 되어, 본문이 `인코딩 어떻게` 라고 적힌 — 사람이 찾던 바로
|
|
47
|
+
* 그 — 글이 「다 맞음」 에 못 들었다. 조사를 똑같이 붙여 적은 글만 12점을 받았다.
|
|
48
|
+
* 묶음으로 세면 조사가 무엇이든 같은 낱말이다.
|
|
49
|
+
*/
|
|
50
|
+
export function 낱말묶기(물음) {
|
|
38
51
|
const 조사 = /(을|를|이|가|은|는|에|에서|으로|로|와|과|의|도|만|까지|부터|에게|한테|보다|처럼|랑|이랑)$/;
|
|
39
52
|
const 본 = String(물음 ?? '').toLowerCase().split(/[\s,.;:!?()[\]{}"'`]+/).filter(Boolean);
|
|
40
|
-
const
|
|
53
|
+
const 묶음 = [];
|
|
54
|
+
const 본것 = new Set();
|
|
41
55
|
for (const w of 본) {
|
|
42
56
|
if (w.length < 2) continue; // 한 글자는 아무 데나 걸린다
|
|
43
|
-
낱말.add(w);
|
|
44
57
|
const 짧은 = w.replace(조사, '');
|
|
45
|
-
|
|
58
|
+
// 긴 꼴을 앞에 둔다 — 셀 때 제일 긴 것 하나만 센다(찾기).
|
|
59
|
+
const 꼴 = 짧은.length >= 2 && 짧은 !== w ? [w, 짧은] : [w];
|
|
60
|
+
const 열쇠 = 꼴.at(-1); // 조사를 뗀 꼴이 그 낱말의 이름이다
|
|
61
|
+
if (본것.has(열쇠)) continue;
|
|
62
|
+
본것.add(열쇠);
|
|
63
|
+
묶음.push(꼴);
|
|
46
64
|
}
|
|
47
|
-
return
|
|
65
|
+
return 묶음;
|
|
48
66
|
}
|
|
49
67
|
|
|
50
68
|
/** 맞은 자리 앞뒤를 잘라 보여 줄 토막을 만든다. */
|
|
@@ -105,7 +123,10 @@ function 때(meta) {
|
|
|
105
123
|
*
|
|
106
124
|
* @param {string} root 작업 폴더
|
|
107
125
|
* @param {string} 물음
|
|
108
|
-
*
|
|
126
|
+
* 적는 옵션은 **여기서 실제로 읽는 것**만이다. 안 읽는 이름을 적어 두면 부르는 쪽이
|
|
127
|
+
* 그것을 넘기고, 넘겼으니 걸러진 줄 알고 지낸다 — `지금세션` 이 그랬다 (2.0.0 8회차 스키마).
|
|
128
|
+
*
|
|
129
|
+
* @param {{limit?: number, 예산?: number, 도구결과까지?: boolean}} o
|
|
109
130
|
* @returns {{맞은것: object[], 본파일: number, 전체파일: number, 읽은바이트: number, 예산초과: boolean, 낱말: string[]}}
|
|
110
131
|
*/
|
|
111
132
|
export function 찾기(root, 물음, o = {}) {
|
|
@@ -113,6 +134,7 @@ export function 찾기(root, 물음, o = {}) {
|
|
|
113
134
|
const 예산 = o.예산 ?? 읽기예산;
|
|
114
135
|
const 도구결과까지 = o.도구결과까지 ?? false;
|
|
115
136
|
const 낱말 = 낱말쪼개기(물음);
|
|
137
|
+
const 묶음 = 낱말묶기(물음);
|
|
116
138
|
const dir = sessionsDir(root);
|
|
117
139
|
|
|
118
140
|
const 빈결과 = { 맞은것: [], 본파일: 0, 전체파일: 0, 읽은바이트: 0, 예산초과: false, 낱말 };
|
|
@@ -165,10 +187,19 @@ export function 찾기(root, 물음, o = {}) {
|
|
|
165
187
|
|
|
166
188
|
let 맞은낱말 = 0;
|
|
167
189
|
let 횟수 = 0;
|
|
168
|
-
for (const
|
|
190
|
+
for (const 그룹 of 묶음) {
|
|
191
|
+
/*
|
|
192
|
+
* 한 낱말의 여러 꼴은 **한 낱말**이다. 긴 꼴부터 보고 맞는 것 하나만 센다 —
|
|
193
|
+
* 둘 다 세면 `인코딩을` 이 든 글은 `인코딩` 까지 같이 맞아 횟수가 부풀고,
|
|
194
|
+
* 조사를 똑같이 붙여 적은 글만 위로 올라간다.
|
|
195
|
+
*/
|
|
169
196
|
let n = 0;
|
|
170
|
-
|
|
171
|
-
|
|
197
|
+
for (const w of 그룹) {
|
|
198
|
+
let c = 0;
|
|
199
|
+
let i = 낮은.indexOf(w);
|
|
200
|
+
while (i >= 0 && c < 50) { c += 1; i = 낮은.indexOf(w, i + w.length); }
|
|
201
|
+
if (c) { n = c; break; }
|
|
202
|
+
}
|
|
172
203
|
if (n) { 맞은낱말 += 1; 횟수 += n; }
|
|
173
204
|
}
|
|
174
205
|
if (!맞은낱말) continue;
|
|
@@ -179,7 +210,7 @@ export function 찾기(root, 물음, o = {}) {
|
|
|
179
210
|
* 한 낱말이 열 번 나오는 글보다 거의 언제나 원하는 것이다.
|
|
180
211
|
* 최근일수록 조금 올린다. 같은 것을 두 번 물어본 경우 최근 답이 낫다.
|
|
181
212
|
*/
|
|
182
|
-
const 다맞음 = 맞은낱말 ===
|
|
213
|
+
const 다맞음 = 맞은낱말 === 묶음.length ? 12 : 0;
|
|
183
214
|
const 오래됨 = Math.max(0, (가장최근 - f.at.getTime()) / (1000 * 60 * 60 * 24));
|
|
184
215
|
const 최근점 = Math.max(0, 6 - 오래됨 / 7);
|
|
185
216
|
맞은것.push({
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ── 왜 따로 있나 ────────────────────────────────────────────────────────
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* confidence.js 는 「이 셈을 얼마나 믿을까」 를 정한다. 여기는 그 앞이다 —
|
|
7
7
|
* 「이게 애초에 우리가 적은 모양이 맞나」. 통계와 문지기는 다른 일이고,
|
|
8
8
|
* 섞어 두면 통계 문턱을 만질 때마다 문지기도 같이 흔들린다.
|
|
9
9
|
*
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 들어가는 문이고, 문에는 자물쇠가 있어야 한다.
|
|
18
18
|
*
|
|
19
19
|
* 여기서 재는 것은 「참인가」 가 아니라 **「실려도 되는 꼴인가」** 다.
|
|
20
|
-
* 참인지는
|
|
20
|
+
* 참인지는 confidence.js 가 정한다.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
const 열쇠최대 = 40;
|
|
@@ -36,18 +36,26 @@ const 받을꼴 = /^[A-Za-z0-9 _.:@+/-]+$/;
|
|
|
36
36
|
|
|
37
37
|
export function 성한열쇠인가(열쇠) {
|
|
38
38
|
const s = String(열쇠 ?? '');
|
|
39
|
-
|
|
39
|
+
// 빈칸뿐인 열쇠는 명령 이름이 아니다 — 받을꼴에 빈칸이 들어 있어 그냥 지났다.
|
|
40
|
+
if (!s.trim() || s.length > 열쇠최대) return false;
|
|
40
41
|
return 받을꼴.test(s);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export function 성한기록인가(r) {
|
|
44
|
-
if (!r || typeof r !== 'object') return false;
|
|
45
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) return false;
|
|
46
|
+
/*
|
|
47
|
+
* 「숫자로 바꿔지면 숫자」 가 아니다.
|
|
48
|
+
*
|
|
49
|
+
* Number() 로 재면 '3'·null·true·[] 가 전부 통과한다 — 우리가 그렇게 적은 적이
|
|
50
|
+
* 없는 꼴이다(evolve.js 는 0 에서 시작해 1씩 더한다). 이 머리말이 적어 둔 대로
|
|
51
|
+
* **무엇만 받을지**를 적는다. 남의 손이 닿은 파일을 고쳐서 받지도 않는다.
|
|
52
|
+
*/
|
|
45
53
|
for (const 칸 of ['ok', 'no']) {
|
|
46
|
-
const v =
|
|
47
|
-
if (!Number.isFinite(v) || v < 0) return false;
|
|
54
|
+
const v = r[칸];
|
|
55
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v < 0) return false;
|
|
48
56
|
}
|
|
49
57
|
// 날짜가 없거나 못 읽으면 못 믿는다. 나이를 모르는 주장은 안 싣는다
|
|
50
|
-
// (
|
|
58
|
+
// (confidence.js 의 며칠됐나 가 그때 무한대를 돌려준다).
|
|
51
59
|
return Number.isFinite(Date.parse(String(r.at ?? '')));
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -59,7 +67,10 @@ export function 성한기록인가(r) {
|
|
|
59
67
|
*/
|
|
60
68
|
export function 거른표(표) {
|
|
61
69
|
const out = {};
|
|
62
|
-
|
|
70
|
+
// 목록은 표가 아니다. Object.entries([r]) 는 ['0', r] 을 주고 '0' 은 받을꼴에
|
|
71
|
+
// 걸려 통과한다 — 명령 이름 자리에 0 이 적힌 채 프롬프트로 나간다.
|
|
72
|
+
if (!표 || typeof 표 !== 'object' || Array.isArray(표)) return out;
|
|
73
|
+
for (const [k, v] of Object.entries(표)) {
|
|
63
74
|
if (성한열쇠인가(k) && 성한기록인가(v)) out[k] = v;
|
|
64
75
|
}
|
|
65
76
|
return out;
|
package/src/agent/review.js
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
// 리뷰가 제 손으로 고치기 시작하면 사람이 무엇을 승인한 것인지 흐려지고,
|
|
17
17
|
// 찾은 것 중 무엇이 진짜인지 가릴 기회가 없어진다. 고칠지 말지는 사람이
|
|
18
18
|
// 정한다 — 그게 리뷰의 전부다.
|
|
19
|
-
import {
|
|
19
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { 깃, 저장소뿌리, 이번에바꾼것, 글자그대로, 살림경로인가, 살림에닿나 } from './commit.js';
|
|
20
22
|
import { chat } from '../backend/adapter.js';
|
|
21
23
|
|
|
22
24
|
/** 모델에게 보여 줄 diff 길이 상한(글자). 넘으면 자르고 잘랐다고 적는다. */
|
|
@@ -78,16 +80,98 @@ export function 볼것(session, ctx) {
|
|
|
78
80
|
// 이번 대화가 손댄 파일 (agent/commit.js 가 쓰는 것과 같은 셈법 — 살림 폴더와
|
|
79
81
|
// 저장소 밖은 빠지고, 폴더째 적힌 것은 안 딸려 온다).
|
|
80
82
|
const 안쪽 = [...이번에바꾼것(session, 뿌리)];
|
|
83
|
+
// 경로는 글자 그대로 넘긴다 — `[ab].txt` 가 글롭으로 풀려 남의 a.txt 가 딸려 오면 안 된다
|
|
84
|
+
// (commit.js 의 글자그대로 머리말, 사냥5 H5-4).
|
|
85
|
+
const 경로 = 안쪽.map(글자그대로);
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
/*
|
|
88
|
+
* ── 「바뀐 것이 없다」 로 잘못 답하던 세 자리 (사냥5 H5-5) ─────────────────
|
|
89
|
+
*
|
|
90
|
+
* 여기는 `git diff HEAD` 한 벌만 보고, 빈 글이 오면 「볼 것이 없습니다」 라고 했다.
|
|
91
|
+
*
|
|
92
|
+
* 1) 첫 커밋 전 저장소 — HEAD 가 없어 git 이 오류로 죽는다. 빈 나무와 견준다.
|
|
93
|
+
* 2) git 이 답을 못 했다 — index 가 깨졌거나 diff 가 64MB 를 넘으면(ENOBUFS)
|
|
94
|
+
* 빈 글이 온다. 못 읽은 것은 **못 읽었다고** 돌려준다. 사람은 「없다」 를
|
|
95
|
+
* 보면 리뷰를 접고, 「못 읽었다」 를 보면 저장소를 손본다.
|
|
96
|
+
* 3) 대화가 새로 만든 파일 — git 이 아직 모르는 파일은 diff 에 아예 안 나온다.
|
|
97
|
+
* 아래에서 따로 싣는다.
|
|
98
|
+
*
|
|
99
|
+
* 셋 다 리뷰가 제일 봐야 할 자리다. 새 파일 하나에 eval(받은글) 을 넣어 두고
|
|
100
|
+
* 리뷰를 시켰는데 「볼 것이 없습니다」 를 받던 꼴이다.
|
|
101
|
+
*/
|
|
102
|
+
let 기준 = 'HEAD';
|
|
103
|
+
if (!깃(뿌리, ['rev-parse', '--verify', '-q', 'HEAD'], {}).ok) {
|
|
104
|
+
// 빈 나무는 git 이 따로 안 적어 둬도 아는 물건이다. 해시는 저장소 방식(SHA-1·256)마다 달라 물어서 쓴다.
|
|
105
|
+
const 빈나무 = 깃(뿌리, ['hash-object', '-t', 'tree', '--stdin'], { 입력: '' });
|
|
106
|
+
if (빈나무.ok && 빈나무.out.trim()) 기준 = 빈나무.out.trim();
|
|
107
|
+
}
|
|
108
|
+
/*
|
|
109
|
+
* ── 살림을 **이름부터** 턴다 (2.0.0 8회차 판정) ─────────────────────────
|
|
110
|
+
*
|
|
111
|
+
* 여기는 `git diff 기준 -- ...경로` 한 벌이었다. `경로` 는 이번 대화가 손댄
|
|
112
|
+
* 파일인데, **아무것도 안 손댔으면 빈 배열**이다. 그러면 git 은 경로 제한 없이
|
|
113
|
+
* 저장소 전체를 뜬다 — 거기 `.deel/config.json` 이 추적 중이면 게이트웨이
|
|
114
|
+
* 열쇠가 그대로 diff 에 실린다. 리뷰는 그 글을 **바깥 모델로 보내는** 기능이다.
|
|
115
|
+
*
|
|
116
|
+
* 아래 새 파일 쪽은 이미 같은 막이를 걸며 그 위험을 주석으로 적어 뒀다
|
|
117
|
+
* (「.deel/config.json 이 안 무시된 저장소면 열쇠가 리뷰로 나간다」). 한쪽에만
|
|
118
|
+
* 붙은 고침이었다 — 새 파일은 막고 **이미 추적 중인 살림은 그냥 나갔다.**
|
|
119
|
+
*
|
|
120
|
+
* 그래서 이름을 먼저 받아 살림을 턴 다음, 남은 것으로만 몸통·통계를 받는다.
|
|
121
|
+
* 자는 새 파일 쪽과 같은 것(`살림경로인가`·`살림에닿나`)을 쓴다 — 두 벌을 두면
|
|
122
|
+
* 한쪽만 고쳐진다.
|
|
123
|
+
*/
|
|
124
|
+
const 이름 = 깃(뿌리, ['diff', 기준, '--name-only', '-z', '--', ...경로], {});
|
|
125
|
+
const 모름 = 깃(뿌리, ['ls-files', '--others', '--exclude-standard', '-z', '--', ...경로], {});
|
|
126
|
+
const 볼파일 = !이름.ok ? [] : 이름.out.split(String.fromCharCode(0)).filter(Boolean)
|
|
127
|
+
.filter((f) => !살림경로인가(f) && !살림에닿나(join(뿌리, f)));
|
|
128
|
+
const 빈것 = { ok: true, out: '', err: '' };
|
|
129
|
+
const 몸통 = 볼파일.length ? 깃(뿌리, ['diff', 기준, '--', ...볼파일.map(글자그대로)], {}) : 빈것;
|
|
130
|
+
const 통계 = 볼파일.length ? 깃(뿌리, ['diff', 기준, '--stat', '--', ...볼파일.map(글자그대로)], {}) : 빈것;
|
|
131
|
+
const 탈 = [몸통, 이름, 모름].find((r) => !r.ok);
|
|
132
|
+
if (탈) {
|
|
133
|
+
const 까닭 = String(탈.err || 탈.out || 'git 이 실패했습니다').trim().split('\n')[0];
|
|
134
|
+
return { ok: false, 왜: `git 이 바뀐 것을 못 읽었습니다 — ${까닭}` };
|
|
135
|
+
}
|
|
136
|
+
// 이름 목록도 NUL 로 끊는다. 줄로 받아 trim 하면 ` a.js` 가 `a.js` 로 적히고,
|
|
137
|
+
// 리눅스에서는 탭·따옴표 든 이름이 C 따옴표로 싸여 온다 (6회차 커밋 C5).
|
|
138
|
+
const 파일들 = 볼파일;
|
|
89
139
|
|
|
90
|
-
|
|
140
|
+
// 3) 새 파일. 살림은 여기서도 뺀다 — .deel/config.json 이 안 무시된 저장소면 열쇠가 리뷰로 나간다.
|
|
141
|
+
const 새것들 = 모름.out.split(String.fromCharCode(0)).filter(Boolean)
|
|
142
|
+
.filter((f) => !살림경로인가(f) && !살림에닿나(join(뿌리, f)));
|
|
143
|
+
const 새몸 = [];
|
|
144
|
+
const 새통계 = [];
|
|
145
|
+
let 실은길이 = 몸통.out.length;
|
|
146
|
+
for (const f of 새것들) {
|
|
147
|
+
let st;
|
|
148
|
+
try { st = statSync(join(뿌리, f)); } catch { continue; }
|
|
149
|
+
if (!st.isFile()) continue;
|
|
150
|
+
if (!파일들.includes(f)) 파일들.push(f);
|
|
151
|
+
const 머리 = `diff --git a/${f} b/${f}\nnew file mode 100644\n`;
|
|
152
|
+
// 다 싣지 않는다. 어차피 보낼것() 이 DIFF상한 에서 자르므로, 넘친 뒤로는 이름만 남긴다.
|
|
153
|
+
if (실은길이 >= DIFF상한 || st.size > 1024 * 1024) {
|
|
154
|
+
새몸.push(`${머리}(새 파일 ${st.size}바이트 — 길어서 내용은 안 실었습니다)\n`);
|
|
155
|
+
새통계.push(` ${f} | 새 파일 (내용 안 실음)`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
let 버퍼;
|
|
159
|
+
try { 버퍼 = readFileSync(join(뿌리, f)); } catch { continue; }
|
|
160
|
+
if (버퍼.includes(0)) {
|
|
161
|
+
새몸.push(`${머리}Binary files /dev/null and b/${f} differ\n`);
|
|
162
|
+
새통계.push(` ${f} | Bin (새 파일)`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const 줄들 = 버퍼.toString('utf8').split('\n');
|
|
166
|
+
if (줄들.length && 줄들[줄들.length - 1] === '') 줄들.pop();
|
|
167
|
+
const 조각 = `${머리}--- /dev/null\n+++ b/${f}\n@@ -0,0 +1,${줄들.length} @@\n${줄들.map((l) => `+${l}`).join('\n')}\n`;
|
|
168
|
+
새몸.push(조각);
|
|
169
|
+
새통계.push(` ${f} | ${줄들.length} + (새 파일)`);
|
|
170
|
+
실은길이 += 조각.length;
|
|
171
|
+
}
|
|
172
|
+
const diff = 몸통.out + 새몸.join('');
|
|
173
|
+
|
|
174
|
+
if (!diff.trim()) {
|
|
91
175
|
return {
|
|
92
176
|
ok: false,
|
|
93
177
|
왜: 안쪽.length
|
|
@@ -98,8 +182,8 @@ export function 볼것(session, ctx) {
|
|
|
98
182
|
return {
|
|
99
183
|
ok: true,
|
|
100
184
|
어디: 안쪽.length ? '이번 대화가 바꾼 것' : '저장소의 바뀐 것 전부',
|
|
101
|
-
diff
|
|
102
|
-
통계: 통계.out.trimEnd(),
|
|
185
|
+
diff,
|
|
186
|
+
통계: [통계.out.trimEnd(), ...새통계].filter(Boolean).join('\n'),
|
|
103
187
|
파일들,
|
|
104
188
|
};
|
|
105
189
|
}
|
|
@@ -185,8 +269,54 @@ export async function 리뷰받기(session, 것, { signal = null, onBackoff = nu
|
|
|
185
269
|
}
|
|
186
270
|
}
|
|
187
271
|
|
|
272
|
+
/*
|
|
273
|
+
* ── `경로:줄` 찾기 ─────────────────────────────────────────────────────
|
|
274
|
+
*
|
|
275
|
+
* 보낼것() 은 리뷰어에게 「자리를 못 짚겠으면 그 지적은 쓰지 마라」 고 시킨다.
|
|
276
|
+
* 그러면 **받는 쪽이 못 알아보는 것**은 리뷰어가 제대로 짚어도 버려진 것과 같다.
|
|
277
|
+
*
|
|
278
|
+
* 여기는 `.확장자` 가 있어야만 자리로 봤다 (2.0.0 8회차 판정). 그래서 셋이 샜다.
|
|
279
|
+
*
|
|
280
|
+
* Makefile:12 · Dockerfile:3 확장자가 없다
|
|
281
|
+
* .env:7 점으로 열고 뒤가 없다
|
|
282
|
+
* app/[id]/page.tsx:42 대괄호가 글자표에 없어 `/page.tsx` 만 남았다
|
|
283
|
+
*
|
|
284
|
+
* 그래서 **모양이 아니라 자리로** 가른다 — 글자 토막에 경로 가름표나 점이 있으면
|
|
285
|
+
* 파일이고, 아무것도 없으면 대문자로 여는 이름(Makefile 꼴)만 파일로 본다.
|
|
286
|
+
* 반대쪽도 그대로 지킨다: 「3:14 는 원주율」 · 「오후 3:14」 는 자리가 아니다.
|
|
287
|
+
*/
|
|
288
|
+
const 자리표 = /([A-Za-z0-9_.가-힣@+~\u00c0-\u024f\[\]()-]+(?:[/\\][A-Za-z0-9_.가-힣@+~\u00c0-\u024f\[\]()-]+)*):(\d+)/g;
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 이 토막이 파일 이름처럼 생겼나. 맨 숫자와 **점도 경로도 없는** 이름을 걸러 낸다.
|
|
292
|
+
*
|
|
293
|
+
* 여기서 가를 수 있는 것은 거기까지다. `localhost:8080` 은 점이 없어 걸리지만
|
|
294
|
+
* `example.com:8080` 은 점이 있어 `a.js` 와 모양이 같다 — 그건 아래 자리찾기 가
|
|
295
|
+
* 주소를 통째로 지워서 가른다. 머리말에 「호스트 이름을 걸러 낸다」 고 적어 두고
|
|
296
|
+
* 점 없는 것만 걸렀던 자리다 (막판 훑기).
|
|
297
|
+
*/
|
|
298
|
+
function 파일같나(파일) {
|
|
299
|
+
const 끝 = 파일.split(/[/\\]/).pop();
|
|
300
|
+
if (!/[A-Za-z가-힣_]/.test(끝)) return false; // 3:14 — 맨 숫자는 자리가 아니다
|
|
301
|
+
if (파일.includes('/') || 파일.includes('\\')) return true; // 경로가 붙었으면 파일이다
|
|
302
|
+
if (끝.includes('.')) return true; // a.js · .env
|
|
303
|
+
return /^[A-Z]/.test(끝); // Makefile · Dockerfile · Jenkinsfile
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/*
|
|
307
|
+
* 주소 속 `host:port` 는 자리가 아니다.
|
|
308
|
+
*
|
|
309
|
+
* `https://api.example.com:8080` 의 호스트는 점이 있어 위 파일같나 를 그냥 지난다.
|
|
310
|
+
* 그러면 리뷰가 적은 주소 하나가 「example.com 파일의 8080번째 줄」 이라는 자리로
|
|
311
|
+
* 굳어, 사람은 없는 파일을 찾아가고 그 지적이 정말 어디를 가리키는지는 못 본다.
|
|
312
|
+
* 주소 토막을 통째로 지운 뒤에 찾는다 — askcheck.js 의 물음글 과 같은 자다.
|
|
313
|
+
*/
|
|
314
|
+
const 주소토막 = /\b[a-z][a-z0-9+.-]*:\/\/\S+/gi;
|
|
315
|
+
|
|
188
316
|
/** `경로:줄` 을 찾는다. 못 찾으면 null — 지어내지 않는다. */
|
|
189
317
|
export function 자리찾기(줄) {
|
|
190
|
-
const m
|
|
191
|
-
|
|
318
|
+
for (const m of String(줄 ?? '').replace(주소토막, ' ').matchAll(자리표)) {
|
|
319
|
+
if (파일같나(m[1])) return { 파일: m[1], 줄: Number(m[2]) };
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
192
322
|
}
|