deel-local-cli 1.20.11 → 1.20.13
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 +4 -2
- package/README.md +4 -2
- package/bin/deel.js +13 -3
- package/package.json +1 -1
- package/src/acp/map.js +39 -10
- package/src/acp/serve.js +10 -8
- package/src/agent/commit.js +105 -21
- package/src/agent/route.js +257 -10
- package/src/agent/sessionui.js +14 -4
- package/src/agent/store.js +133 -33
- package/src/commands.js +22 -3
- package/src/config.js +87 -9
- package/src/i18n/en.js +4 -4
- package/src/i18n/ja.js +1 -1
- package/src/i18n/ko.js +4 -4
- package/src/i18n/zh.js +1 -1
- package/src/lsp/client.js +82 -19
- package/src/oneshot.js +3 -2
- package/src/plugins/manage.js +15 -6
- package/src/repl.js +3 -2
- package/src/reset.js +37 -6
- package/src/safety/hooks.js +4 -1
- package/src/safety/runmode.js +15 -0
- package/src/setup.js +26 -5
- package/src/skills/discover.js +10 -1
- package/src/tools/fig.js +19 -5
- package/src/tools/webfetch.js +84 -20
- package/src/ui/md.js +123 -25
- package/src/ui/screen.js +13 -0
package/README.ko.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
22
22
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
23
|
-
[](docs/ko/develop.md)
|
|
24
24
|
|
|
25
25
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
26
26
|
[](package.json)
|
|
@@ -301,7 +301,7 @@ deel --offline
|
|
|
301
301
|
무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
|
|
302
302
|
|
|
303
303
|
```
|
|
304
|
-
deel 1.20.
|
|
304
|
+
deel 1.20.13 ⌂ 이 안
|
|
305
305
|
보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
|
|
306
306
|
```
|
|
307
307
|
|
|
@@ -1289,6 +1289,8 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1289
1289
|
|
|
1290
1290
|
| 판 | 무엇이 바뀌었나 |
|
|
1291
1291
|
|---|---|
|
|
1292
|
+
| [1.20.13](docs/ko/releases/1.20.md#12013) | 띄어쓰기 한 칸이 시킨 말을 통째로 숨겼습니다 |
|
|
1293
|
+
| [1.20.12](docs/ko/releases/1.20.md#12012) | 켠다고 적은 자물쇠가 아무것도 안 잠그고 있었습니다 |
|
|
1292
1294
|
| [1.20.11](docs/ko/releases/1.20.md#12011) | 안전망이 있다고 말하는데 없었습니다 |
|
|
1293
1295
|
| [1.20.10](docs/ko/releases/1.20.md#12010) | 답하지 않았는데 답한 것이 되어 있었습니다 |
|
|
1294
1296
|
| [1.20.9](docs/ko/releases/1.20.md#1209) | 안 보이는 것이 아니라 다른 것이 보이고 있었습니다 |
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Vendor APIs connect too — **only when you say so**
|
|
|
20
20
|
|
|
21
21
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
22
22
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
23
|
-
[](docs/en/develop.md)
|
|
24
24
|
|
|
25
25
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
26
26
|
[](package.json)
|
|
@@ -374,7 +374,7 @@ deel --offline
|
|
|
374
374
|
The destination is printed at the top of every session:
|
|
375
375
|
|
|
376
376
|
```
|
|
377
|
-
deel 1.20.
|
|
377
|
+
deel 1.20.13 ⌂ inside
|
|
378
378
|
Sends to this machine 127.0.0.1:11434 ← nowhere else
|
|
379
379
|
```
|
|
380
380
|
|
|
@@ -1350,6 +1350,8 @@ so one run tells you everything.
|
|
|
1350
1350
|
|
|
1351
1351
|
| Version | What changed |
|
|
1352
1352
|
|---|---|
|
|
1353
|
+
| [1.20.13](docs/en/releases/1.20.md#12013) | One space hid the whole instruction |
|
|
1354
|
+
| [1.20.12](docs/en/releases/1.20.md#12012) | The lock we said was on was not attached to anything |
|
|
1353
1355
|
| [1.20.11](docs/en/releases/1.20.md#12011) | The safety net said it was there when it wasn't |
|
|
1354
1356
|
| [1.20.10](docs/en/releases/1.20.md#12010) | Nobody answered, yet an answer had been given |
|
|
1355
1357
|
| [1.20.9](docs/en/releases/1.20.md#1209) | Nothing was missing from the screen — something else was on it |
|
package/bin/deel.js
CHANGED
|
@@ -146,7 +146,7 @@ function runConfig(args, flags) {
|
|
|
146
146
|
async function runDoctor(flags) {
|
|
147
147
|
const { load: 읽기, activeProfile: 고른것, resolveKey: 열쇠풀기, configPath: 설정경로 } =
|
|
148
148
|
await import('../src/config.js');
|
|
149
|
-
const { 지금모드, 바깥인가,
|
|
149
|
+
const { 지금모드, 바깥인가, 나갈수있나, 봉인됐나 } = await import('../src/safety/runmode.js');
|
|
150
150
|
const { allowEndpoint } = await import('../src/safety/network.js');
|
|
151
151
|
|
|
152
152
|
banner();
|
|
@@ -160,7 +160,7 @@ async function runDoctor(flags) {
|
|
|
160
160
|
* 진단한다고 자물쇠를 넘어가면 안 된다 — 여기서 한 번 나가 버리면 「나가는
|
|
161
161
|
* 주소는 사람이 정한 하나뿐」 이 진단 명령 하나로 깨진다.
|
|
162
162
|
*/
|
|
163
|
-
const 모드 = 지금모드({ online: flags.online === true, offline:
|
|
163
|
+
const 모드 = 지금모드({ online: flags.online === true, offline: 봉인됐나({ 깃발: flags.offline, prof, cfg }) });
|
|
164
164
|
const 나감 = prof ? 나갈수있나(모드, { 바깥: 바깥인가(prof.baseUrl), 허가: prof.online === true }) : { 물어볼까: false };
|
|
165
165
|
const 두드려도되나 = !!prof && !나감.물어볼까 && !모드.허가무시;
|
|
166
166
|
if (두드려도되나) allowEndpoint(prof.baseUrl);
|
|
@@ -490,7 +490,17 @@ function runSbom(flags) {
|
|
|
490
490
|
// 실제로 이랬다 — deel run --json "검사 돌려줘" 를 쳤더니 --json 이 뒤의 말을
|
|
491
491
|
// 통째로 삼켰다. 시킬 말이 사라졌으니 "무엇을 시킬지 적어 주세요" 가 떴는데,
|
|
492
492
|
// 화면만 보면 왜 그런지 알 길이 없다. 깃발을 앞에 두는 것은 아주 흔한 습관이다.
|
|
493
|
-
const BOOL = new Set([
|
|
493
|
+
const BOOL = new Set([
|
|
494
|
+
'help', 'version', 'offline', 'online', 'continue', 'json', 'quiet', 'yes',
|
|
495
|
+
'no-tui', 'tui', 'all', 'no-hooks',
|
|
496
|
+
/*
|
|
497
|
+
* `hard` 가 여기 빠져 있었다. 그래서 `deel reset --hard all --yes` 는
|
|
498
|
+
* `--hard` 가 뒤의 `all` 을 제 값으로 삼켰다 — 지울 갈래가 사라지니 화면은
|
|
499
|
+
* 「무엇을 지울지 같이 주세요」 를 찍고 **종료코드 0** 으로 끝났다.
|
|
500
|
+
* 스크립트는 전체 초기화가 된 줄 알고 다음 줄로 넘어간다.
|
|
501
|
+
*/
|
|
502
|
+
'hard',
|
|
503
|
+
]);
|
|
494
504
|
|
|
495
505
|
function parse(argv) {
|
|
496
506
|
const flags = {};
|
package/package.json
CHANGED
package/src/acp/map.js
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
//
|
|
26
26
|
// 읽기만 하는 순수 함수를 들여오는 것은 순수함을 안 깬다. 규격을 읽는 규칙은
|
|
27
27
|
// backend/adapter.js 한 곳에만 있어야 한다 (집안 규칙: 자는 한 벌).
|
|
28
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
29
|
+
|
|
28
30
|
import { 부른것들, 결과들, 본문글 } from '../backend/adapter.js';
|
|
29
31
|
import { 첫이름 } from '../tools/label.js';
|
|
30
32
|
|
|
@@ -40,9 +42,12 @@ const 갈래표 = {
|
|
|
40
42
|
Write: 'edit',
|
|
41
43
|
Append: 'edit',
|
|
42
44
|
Edit: 'edit',
|
|
45
|
+
Move: 'move',
|
|
43
46
|
Glob: 'search',
|
|
44
47
|
Grep: 'search',
|
|
45
48
|
Recall: 'search',
|
|
49
|
+
Def: 'search',
|
|
50
|
+
Refs: 'search',
|
|
46
51
|
Bash: 'execute',
|
|
47
52
|
Jobs: 'execute',
|
|
48
53
|
Verify: 'execute',
|
|
@@ -50,6 +55,7 @@ const 갈래표 = {
|
|
|
50
55
|
Task: 'think',
|
|
51
56
|
TodoWrite: 'think',
|
|
52
57
|
Skill: 'think',
|
|
58
|
+
Ask: 'other',
|
|
53
59
|
Remember: 'other',
|
|
54
60
|
};
|
|
55
61
|
|
|
@@ -77,8 +83,15 @@ export function 도구이름표(이름, 인자) {
|
|
|
77
83
|
: null)
|
|
78
84
|
?? (Array.isArray(a.paths) && a.paths.length ? `${a.paths.length}개` : null)
|
|
79
85
|
?? (Array.isArray(a.todos) ? `${a.todos.length}건` : null);
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
/*
|
|
87
|
+
* 이름표는 에디터의 **한 줄** 머리다. 본문 줄이는 자르기() 를 쓰면 그 끝에
|
|
88
|
+
* `\n… (N자 줄임)` 이 붙어서 머리가 두 줄이 되고, 한 줄을 미리 정해 둔
|
|
89
|
+
* 자리라 그 줄이 깨진다. 긴 명령 하나면 바로 그렇게 된다.
|
|
90
|
+
*/
|
|
91
|
+
const 안 = 첫 == null ? '' : 한줄로(String(첫), 80);
|
|
92
|
+
// 이름이 없으면 「도구」 다. 앞칸만 채우면 화면에 `undefined(a.js)` 가 뜬다.
|
|
93
|
+
const 이름글 = String(이름 ?? '').trim() || '도구';
|
|
94
|
+
return 안 ? `${이름글}(${안})` : 이름글;
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
/**
|
|
@@ -88,11 +101,21 @@ export function 도구이름표(이름, 인자) {
|
|
|
88
101
|
* 있는데, 에디터는 절대 경로라야 연다. 인자만 보고 넘기면 눌러도 안 열리는
|
|
89
102
|
* 링크가 되고, 그건 없느니만 못하다.
|
|
90
103
|
*/
|
|
91
|
-
export function 도구자리(이름, 인자,
|
|
104
|
+
export function 도구자리(이름, 인자, 결과, { 뿌리 = null } = {}) {
|
|
92
105
|
const 모은것 = [];
|
|
93
106
|
const 넣기 = (p) => {
|
|
94
107
|
const s = typeof p === 'string' ? p.trim() : '';
|
|
95
|
-
if (s
|
|
108
|
+
if (!s) return;
|
|
109
|
+
/*
|
|
110
|
+
* 상대 경로는 **여기서** 절대 경로로 편다.
|
|
111
|
+
*
|
|
112
|
+
* 에디터는 절대 경로라야 연다. 상대 경로를 그대로 주면 눌러도 아무 일이
|
|
113
|
+
* 안 일어나는데, 화면에는 멀쩡한 링크로 보인다 — 없느니만 못하다.
|
|
114
|
+
* 도는 중(tool_call)에는 결과가 아직 없어서 인자에 적힌 상대 경로뿐이라,
|
|
115
|
+
* 이 자리를 안 펴면 도구가 도는 내내 링크가 다 죽어 있다.
|
|
116
|
+
*/
|
|
117
|
+
const 편것 = 뿌리 && !isAbsolute(s) ? resolve(뿌리, s) : s;
|
|
118
|
+
if (!모은것.includes(편것)) 모은것.push(편것);
|
|
96
119
|
};
|
|
97
120
|
|
|
98
121
|
넣기(결과?.changed);
|
|
@@ -141,7 +164,7 @@ export function 도구내용(결과, 최대 = 2000) {
|
|
|
141
164
|
* @param {string} 아이디 이 세션 안에서 유일한 번호
|
|
142
165
|
* @param {object} ev loop.js 가 흘린 `tool` 이벤트
|
|
143
166
|
*/
|
|
144
|
-
export function 도구끝남(아이디, ev) {
|
|
167
|
+
export function 도구끝남(아이디, ev, { 뿌리 = null } = {}) {
|
|
145
168
|
return {
|
|
146
169
|
sessionUpdate: 'tool_call_update',
|
|
147
170
|
toolCallId: 아이디,
|
|
@@ -149,12 +172,12 @@ export function 도구끝남(아이디, ev) {
|
|
|
149
172
|
kind: 도구갈래(ev?.name),
|
|
150
173
|
status: 도구탈났나(ev?.result) ? 'failed' : 'completed',
|
|
151
174
|
content: 도구내용(ev?.result),
|
|
152
|
-
locations: 도구자리(ev?.name, ev?.args, ev?.result),
|
|
175
|
+
locations: 도구자리(ev?.name, ev?.args, ev?.result, { 뿌리 }),
|
|
153
176
|
};
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
/** 이제 막 시작한 도구 호출. */
|
|
157
|
-
export function 도구시작(아이디, 이름,
|
|
180
|
+
export function 도구시작(아이디, 이름, 인자, { 뿌리 = null } = {}) {
|
|
158
181
|
return {
|
|
159
182
|
sessionUpdate: 'tool_call',
|
|
160
183
|
toolCallId: 아이디,
|
|
@@ -162,7 +185,7 @@ export function 도구시작(아이디, 이름, 인자) {
|
|
|
162
185
|
kind: 도구갈래(이름),
|
|
163
186
|
status: 'in_progress',
|
|
164
187
|
content: [],
|
|
165
|
-
locations: 도구자리(이름, 인자, null),
|
|
188
|
+
locations: 도구자리(이름, 인자, null, { 뿌리 }),
|
|
166
189
|
};
|
|
167
190
|
}
|
|
168
191
|
|
|
@@ -227,7 +250,7 @@ function 인자풀기(x) {
|
|
|
227
250
|
* @param {object[]} messages store.js 가 읽어 온 대화
|
|
228
251
|
* @returns {object[]} session/update 의 update 자리에 그대로 넣을 것들
|
|
229
252
|
*/
|
|
230
|
-
export function 되살린것(messages, { 최대내용 = 2000 } = {}) {
|
|
253
|
+
export function 되살린것(messages, { 최대내용 = 2000, 뿌리 = null } = {}) {
|
|
231
254
|
const 줄 = Array.isArray(messages) ? messages : [];
|
|
232
255
|
|
|
233
256
|
/*
|
|
@@ -315,7 +338,7 @@ export function 되살린것(messages, { 최대내용 = 2000 } = {}) {
|
|
|
315
338
|
content: 없음
|
|
316
339
|
? [{ type: 'content', content: { type: 'text', text: '결과가 안 남았습니다 — 이 도구가 도는 중에 끊겼습니다.' } }]
|
|
317
340
|
: [{ type: 'content', content: { type: 'text', text: 자르기(String(답), 최대내용) } }],
|
|
318
|
-
locations: 도구자리(이름, 인자, null),
|
|
341
|
+
locations: 도구자리(이름, 인자, null, { 뿌리 }),
|
|
319
342
|
});
|
|
320
343
|
}
|
|
321
344
|
}
|
|
@@ -365,3 +388,9 @@ function 자르기(s, n) {
|
|
|
365
388
|
const t = String(s ?? '');
|
|
366
389
|
return t.length > n ? `${t.slice(0, n)}\n… (${t.length - n}자 줄임)` : t;
|
|
367
390
|
}
|
|
391
|
+
|
|
392
|
+
/** 한 줄짜리 자리(이름표)에 쓴다. 줄바꿈을 빈칸으로 눕히고 말줄임만 붙인다. */
|
|
393
|
+
function 한줄로(s, n) {
|
|
394
|
+
const t = String(s ?? '').replace(/\s+/g, ' ').trim();
|
|
395
|
+
return t.length > n ? `${t.slice(0, n)}…` : t;
|
|
396
|
+
}
|
package/src/acp/serve.js
CHANGED
|
@@ -47,7 +47,7 @@ import { 알림채움, 알림말 } from '../backend/retry.js';
|
|
|
47
47
|
import { 전선붙이기, 세션이름짓기 } from '../backend/wire.js';
|
|
48
48
|
import { discover } from '../skills/discover.js';
|
|
49
49
|
import { allowEndpoint, setOffline } from '../safety/network.js';
|
|
50
|
-
import { 지금모드, 바깥인가,
|
|
50
|
+
import { 지금모드, 바깥인가, 나갈수있나, 봉인됐나 } from '../safety/runmode.js';
|
|
51
51
|
import { 주소가리기 } from '../safety/secrets.js';
|
|
52
52
|
import { probeCtx, 기본값 as CTX_DEFAULT } from '../backend/ctxsize.js';
|
|
53
53
|
import { 잠잠기본, 무소식기본 } from '../backend/http.js';
|
|
@@ -247,7 +247,8 @@ export async function acp(opts = {}) {
|
|
|
247
247
|
const 실행모드 = 지금모드({
|
|
248
248
|
online: opts.online === true,
|
|
249
249
|
// 관리 정책이 offline 을 못박아 뒀으면 옵션과 상관없이 켠다 (safety/policy.js).
|
|
250
|
-
|
|
250
|
+
// 셋 중 하나라도 켜져 있으면 켜진 것이다 (safety/runmode.js 의 봉인됐나).
|
|
251
|
+
offline: 봉인됐나({ 깃발: opts.offline, prof, cfg }),
|
|
251
252
|
});
|
|
252
253
|
const 나감 = 나갈수있나(실행모드, { 바깥: 바깥연결, 허가: prof.online === true });
|
|
253
254
|
if (나감.물어볼까) {
|
|
@@ -314,7 +315,8 @@ export async function acp(opts = {}) {
|
|
|
314
315
|
const 캐시열쇠 = resolve(root);
|
|
315
316
|
if (!mcp캐시.has(캐시열쇠)) {
|
|
316
317
|
mcp캐시.set(캐시열쇠, 다붙이기(root, {
|
|
317
|
-
|
|
318
|
+
// 봉인은 MCP 를 붙이는 자리에도 그대로 걸린다 (safety/runmode.js 의 봉인됐나).
|
|
319
|
+
offline: 봉인됐나({ 깃발: opts.offline, prof, cfg }),
|
|
318
320
|
audit: null,
|
|
319
321
|
}).then((붙임) => {
|
|
320
322
|
/*
|
|
@@ -443,7 +445,7 @@ export async function acp(opts = {}) {
|
|
|
443
445
|
title: 도구이름표(이름, 인자),
|
|
444
446
|
kind: 도구갈래(이름),
|
|
445
447
|
status: 'pending',
|
|
446
|
-
locations: 도구자리(이름, 인자, null),
|
|
448
|
+
locations: 도구자리(이름, 인자, null, { 뿌리: 방.root }),
|
|
447
449
|
rawInput: 인자 ?? {},
|
|
448
450
|
},
|
|
449
451
|
options: [
|
|
@@ -641,17 +643,17 @@ export async function acp(opts = {}) {
|
|
|
641
643
|
break;
|
|
642
644
|
|
|
643
645
|
case 'tool_start':
|
|
644
|
-
보내기(도구시작(도구맡기기(방, ev.name), ev.name, ev.args));
|
|
646
|
+
보내기(도구시작(도구맡기기(방, ev.name), ev.name, ev.args, { 뿌리: 방.root }));
|
|
645
647
|
break;
|
|
646
648
|
|
|
647
649
|
case 'tools_start':
|
|
648
650
|
for (const 이름 of ev.names ?? []) {
|
|
649
|
-
보내기(도구시작(도구맡기기(방, 이름), 이름, null));
|
|
651
|
+
보내기(도구시작(도구맡기기(방, 이름), 이름, null, { 뿌리: 방.root }));
|
|
650
652
|
}
|
|
651
653
|
break;
|
|
652
654
|
|
|
653
655
|
case 'tool':
|
|
654
|
-
보내기(도구끝남(도구찾기(방, ev.name), ev));
|
|
656
|
+
보내기(도구끝남(도구찾기(방, ev.name), ev, { 뿌리: 방.root }));
|
|
655
657
|
적기();
|
|
656
658
|
break;
|
|
657
659
|
|
|
@@ -880,7 +882,7 @@ export async function acp(opts = {}) {
|
|
|
880
882
|
} catch { /* 못 읽으면 기억하고 있는 것으로 그린다 */ }
|
|
881
883
|
|
|
882
884
|
let 흘린것 = 0;
|
|
883
|
-
for (const update of 되살린것(
|
|
885
|
+
for (const update of 되살린것(그릴것, { 뿌리: 방.root })) {
|
|
884
886
|
관.알림('session/update', { sessionId: 방.id, update });
|
|
885
887
|
흘린것++;
|
|
886
888
|
}
|
package/src/agent/commit.js
CHANGED
|
@@ -164,9 +164,17 @@ export function 이번에바꾼것(session, 뿌리) {
|
|
|
164
164
|
for (const p of (session?.changes?.keys?.() ?? [])) {
|
|
165
165
|
const abs = 진짜(isAbsolute(p) ? p : join(기준, p));
|
|
166
166
|
const rel = relative(기준, abs).replace(/\\/g, '/');
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
/*
|
|
168
|
+
* 저장소 밖은 담지 않는다. `..` 로 시작하면 밖이다.
|
|
169
|
+
* (지워진 파일도 여기서 안 새어 나간다 — 진짜() 가 있는 데까지 풀어 준다.)
|
|
170
|
+
*
|
|
171
|
+
* 절대경로도 밖이다. 윈도우에서 **드라이브가 다르면** relative() 가
|
|
172
|
+
* `..` 를 못 만들고 상대경로 대신 `D:\남의폴더\a.txt` 를 그대로 돌려준다.
|
|
173
|
+
* 그러면 위 두 검사를 그냥 지나가서, 저장소 밖 경로가 `git add` 에 실린다 —
|
|
174
|
+
* git 이 `fatal: … is outside repository` 로 죽고 /commit 이 통째로 안 된다.
|
|
175
|
+
* 담지 말아야 할 것 하나 때문에 담아야 할 것도 다 못 담는 꼴이다.
|
|
176
|
+
*/
|
|
177
|
+
if (!rel || rel.startsWith('../') || rel === '..' || isAbsolute(rel)) continue;
|
|
170
178
|
if (살림경로인가(rel) || 살림에닿나(abs)) continue;
|
|
171
179
|
let 폴더인가 = false;
|
|
172
180
|
try { 폴더인가 = statSync(abs).isDirectory(); } catch { /* 지워진 것은 파일로 친다 */ }
|
|
@@ -217,13 +225,41 @@ export function 살림도로빼기(뿌리, 파일들) {
|
|
|
217
225
|
return { 샌것, 못뺀것: 아직 };
|
|
218
226
|
}
|
|
219
227
|
|
|
220
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* 지금 담겨 있는 것.
|
|
230
|
+
*
|
|
231
|
+
* ── git 이 **답을 못 한 것**과 **답이 없는 것**은 다르다 ──────────────────
|
|
232
|
+
*
|
|
233
|
+
* 세 번 부르면서 ok 를 한 번도 안 봤다. 그래서 실패가 전부 「빈 값」 으로
|
|
234
|
+
* 내려가고, 부르는 쪽은 그것을 「바뀐 것이 없다」 로 읽었다. 실제로 겪는 꼴이
|
|
235
|
+
* 둘 있다.
|
|
236
|
+
*
|
|
237
|
+
* · 담긴 diff 가 64MB 를 넘으면 몸통만 ENOBUFS 로 죽는다(spawnSync 의
|
|
238
|
+
* maxBuffer). 파일 목록은 멀쩡히 오므로 커밋은 그대로 이어지고,
|
|
239
|
+
* **빈 diff 를 받은 모델**이 「무엇을 바꿨는지는 diff 에 이미 있다」 는
|
|
240
|
+
* 지시 아래 메시지를 짓는다 — 볼 것이 없으니 지어낸다. 큰 데이터 덤프나
|
|
241
|
+
* 묶은 번들은 이 도구가 실제로 만드는 것들이다.
|
|
242
|
+
* · `.git/index` 가 깨지면 이름 목록부터 죽는다. 그러면 화면에 「담을 것이
|
|
243
|
+
* 없습니다 — 바뀐 내용이 없습니다」 가 뜬다. 사람은 제 일이 안 담긴 줄
|
|
244
|
+
* 알고 다시 담으러 가는데, 실제로 필요한 것은 저장소 손보기다.
|
|
245
|
+
*
|
|
246
|
+
* 그래서 못 읽은 것을 못 읽었다고 같이 돌려준다.
|
|
247
|
+
*
|
|
248
|
+
* @returns {{파일들:string[], 통계:string, diff:string, 못읽음:Array<{무엇:string,왜:string}>}}
|
|
249
|
+
*/
|
|
221
250
|
export function 담긴것(뿌리) {
|
|
222
251
|
const 이름 = 깃(뿌리, ['diff', '--cached', '--name-only']);
|
|
223
252
|
const 통계 = 깃(뿌리, ['diff', '--cached', '--stat']);
|
|
224
253
|
const 몸통 = 깃(뿌리, ['diff', '--cached']);
|
|
225
254
|
const 파일들 = 이름.out.split('\n').map((x) => x.trim()).filter(Boolean);
|
|
226
|
-
|
|
255
|
+
const 못읽음 = [];
|
|
256
|
+
const 적기 = (무엇, r) => {
|
|
257
|
+
if (!r.ok) 못읽음.push({ 무엇, 왜: (r.err || r.out || `git ${무엇} 실패`).trim().split('\n')[0] });
|
|
258
|
+
};
|
|
259
|
+
적기('목록', 이름);
|
|
260
|
+
적기('통계', 통계);
|
|
261
|
+
적기('diff', 몸통);
|
|
262
|
+
return { 파일들, 통계: 통계.out.trimEnd(), diff: 몸통.out, 못읽음 };
|
|
227
263
|
}
|
|
228
264
|
|
|
229
265
|
/** 저장소가 쓰던 말투를 흉내 내라고 최근 제목을 보여 준다. */
|
|
@@ -283,10 +319,10 @@ export function 답가르기(글) {
|
|
|
283
319
|
} catch { /* JSON 인 척한 것뿐이면 아래로 */ }
|
|
284
320
|
}
|
|
285
321
|
|
|
286
|
-
const 표 = s.match(/^\s*제목\s*[
|
|
322
|
+
const 표 = s.match(/^\s*제목\s*[::]\s*(.+?)\s*$/m);
|
|
287
323
|
if (표) {
|
|
288
324
|
const 뒤 = s.slice(s.indexOf(표[0]) + 표[0].length);
|
|
289
|
-
const 본문표 = 뒤.match(/^\s*본문\s*[
|
|
325
|
+
const 본문표 = 뒤.match(/^\s*본문\s*[::]\s*/m);
|
|
290
326
|
const 본문 = 본문표 ? 뒤.slice(뒤.indexOf(본문표[0]) + 본문표[0].length) : 뒤;
|
|
291
327
|
return { 제목: 표[1].trim(), 본문: 본문.trim() };
|
|
292
328
|
}
|
|
@@ -332,12 +368,29 @@ export function 제어글자빼기(글) {
|
|
|
332
368
|
* 진짜로 읽는다. 우리 꼬리표(Generated-by)는 우리가 따로 붙이므로, 모델이
|
|
333
369
|
* 꼬리표를 쓸 이유가 아예 없다.
|
|
334
370
|
*/
|
|
335
|
-
const 가짜꼬리표 = /^\s*(signed-off-by|co-authored-by|reviewed-by|acked-by|tested-by|generated-by|claude-session
|
|
371
|
+
const 가짜꼬리표 = /^\s*(signed-off-by|co-authored-by|reviewed-by|acked-by|tested-by|generated-by|claude-session)\s*:/i;
|
|
372
|
+
|
|
373
|
+
/*
|
|
374
|
+
* 이슈를 **닫는** 낱말은 콜론이 없다.
|
|
375
|
+
*
|
|
376
|
+
* 위 목록에 closes·fixes 를 같이 넣어 두고 있었는데, 끝에 `\s*:` 가 붙어 있어
|
|
377
|
+
* 정작 깃허브가 알아보는 꼴(`Closes #123`)은 한 번도 안 걸렸다. 걸러내려던
|
|
378
|
+
* 바로 그것만 통과한 셈이다.
|
|
379
|
+
*
|
|
380
|
+
* 이게 왜 중요하냐면, 이 줄이 기본 가지에 실리는 순간 **깃허브가 그 이슈를
|
|
381
|
+
* 진짜로 닫는다.** 모델이 지어낸 번호면 남의 이슈가 닫힌다 — 커밋을 되돌려도
|
|
382
|
+
* 닫힌 이슈는 안 열린다.
|
|
383
|
+
*
|
|
384
|
+
* 그렇다고 `Fixes` 로 시작하는 줄을 통째로 지우면 안 된다. 「Fixes the crash
|
|
385
|
+
* when …」 은 본문에 있어야 할 진짜 문장이다. 그래서 **뒤에 `#숫자`나 주소만
|
|
386
|
+
* 달랑 오는 꼴**일 때만 지운다.
|
|
387
|
+
*/
|
|
388
|
+
const 닫는말 = /^\s*(clos(e|es|ed)|fix(|es|ed)|resolv(e|es|ed))\s*:?\s*(#\d+|https?:\/\/\S+)\s*$/i;
|
|
336
389
|
|
|
337
390
|
export function 꼬리표걸러내기(본문) {
|
|
338
391
|
return String(본문 ?? '')
|
|
339
392
|
.split('\n')
|
|
340
|
-
.filter((줄) => !가짜꼬리표.test(줄))
|
|
393
|
+
.filter((줄) => !가짜꼬리표.test(줄) && !닫는말.test(줄))
|
|
341
394
|
.join('\n')
|
|
342
395
|
.trim();
|
|
343
396
|
}
|
|
@@ -388,18 +441,32 @@ export function 메시지꾸리기({ 제목, 본문 = '', 확인 = 0, 미확인
|
|
|
388
441
|
* 대화를 통째로 보내면 창을 두 번 먹고, 모델은 제가 한 말을 근거로 제
|
|
389
442
|
* 커밋 메시지를 쓰게 된다. 커밋에 실릴 것은 **코드가 말하는 것**이어야 한다.
|
|
390
443
|
*/
|
|
391
|
-
export async function 메시지짓기(session, { 뿌리, diff, 통계, 파일들, 증거, 제목 = null, signal = null, onBackoff = null } = {}) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
444
|
+
export async function 메시지짓기(session, { 뿌리, diff, 통계, 파일들, 증거, 제목 = null, signal = null, onBackoff = null, diff못읽음 = null } = {}) {
|
|
445
|
+
/*
|
|
446
|
+
* diff 를 **못 읽은 것**을 「없는 것」 으로 주면 모델은 지어낸다.
|
|
447
|
+
*
|
|
448
|
+
* 지시문이 「무엇을 바꿨는지는 diff 에 이미 있다」 라고 못을 박아 두었다.
|
|
449
|
+
* 그 상태로 빈 diff 를 주면 모델은 통계 줄만 보고 그럴듯한 이야기를 만든다.
|
|
450
|
+
* 못 봤으면 못 봤다고 말해야 「diff 에서 실제로 보이는 것만 써라」 가 지켜진다.
|
|
451
|
+
*/
|
|
452
|
+
const 자른diff = diff못읽음
|
|
453
|
+
? `(diff 를 못 읽었습니다: ${diff못읽음})\n`
|
|
454
|
+
+ '내용을 한 줄도 못 봤습니다. 아래 통계에 적힌 파일 이름과 줄 수만 보고 쓰고,\n'
|
|
455
|
+
+ '내용에 대해서는 아무것도 단정하지 마라.'
|
|
456
|
+
: diff.length > DIFF상한
|
|
457
|
+
? `${diff.slice(0, DIFF상한)}\n… (diff 가 길어 여기서 잘랐습니다 — 나머지는 통계로만 보세요)`
|
|
458
|
+
: diff;
|
|
395
459
|
const 최근 = 최근제목들(뿌리 ?? process.cwd());
|
|
396
460
|
|
|
397
461
|
const 몫 = [];
|
|
398
462
|
if (최근.length) 몫.push(`이 저장소의 최근 커밋 제목:\n${최근.map((x) => `- ${x}`).join('\n')}`);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
463
|
+
// 증거는 있는데 셈 칸이 없을 수 있다. 바로 아래 커밋준비() 는 `증거?.셈?.` 으로
|
|
464
|
+
// 읽으면서 여기만 맨몸이었다 — 한쪽이 조심하고 한쪽이 안 하면 언젠가 터진다.
|
|
465
|
+
if (증거?.셈) {
|
|
466
|
+
const 셈 = 증거.셈;
|
|
467
|
+
const 확인 = (셈.파일 ?? 0) - (셈.증명안됨 ?? 0);
|
|
468
|
+
몫.push(`이번에 돌린 것: ${셈.돌린것 ?? 0}개${셈.실패한것 ? ` (실패 ${셈.실패한것}개)` : ''}`
|
|
469
|
+
+ `\n확인된 파일 ${확인}개 · 확인 안 된 파일 ${셈.증명안됨 ?? 0}개`);
|
|
403
470
|
}
|
|
404
471
|
몫.push(`바뀐 파일:\n${통계 || 파일들.map((f) => `- ${f}`).join('\n')}`);
|
|
405
472
|
몫.push(`----- diff -----\n${자른diff}`);
|
|
@@ -424,7 +491,7 @@ export async function 메시지짓기(session, { 뿌리, diff, 통계, 파일들
|
|
|
424
491
|
if (제목) {
|
|
425
492
|
// 본문만 달라고 했어도 작은 모델은 `제목:`/`본문:` 꼴을 그대로 흉내 낸다.
|
|
426
493
|
// 그걸 그대로 본문에 넣으면 커밋 안에 '제목:' 이라는 줄이 남는다.
|
|
427
|
-
const 갈린 = /^\s*제목\s*[
|
|
494
|
+
const 갈린 = /^\s*제목\s*[::]/m.test(글) ? 답가르기(글) : null;
|
|
428
495
|
return { 제목, 본문: 갈린?.본문 ?? 껍질벗기기(글) };
|
|
429
496
|
}
|
|
430
497
|
return 답가르기(글);
|
|
@@ -465,14 +532,28 @@ export async function 커밋준비(session, ctx, { 전부 = false, 제목 = null
|
|
|
465
532
|
return { ok: false, why: `열쇠가 든 자리(${뺀것.못뺀것.join(', ')})가 담긴 채로 안 빠집니다 — 커밋하지 않았습니다.` };
|
|
466
533
|
}
|
|
467
534
|
|
|
468
|
-
const { 파일들, 통계, diff } = 담긴것(뿌리);
|
|
535
|
+
const { 파일들, 통계, diff, 못읽음 } = 담긴것(뿌리);
|
|
536
|
+
/*
|
|
537
|
+
* **목록**을 못 읽었으면 「없다」 가 아니다.
|
|
538
|
+
*
|
|
539
|
+
* 깨진 index·잠긴 저장소에서 이 자리는 여태 「바뀐 내용이 없습니다」 를
|
|
540
|
+
* 냈다. 사람은 제 일이 안 담긴 줄 알고 다시 담으러 가는데, 실제로 할 일은
|
|
541
|
+
* 저장소를 손보는 것이다. 까닭을 그대로 옮긴다.
|
|
542
|
+
*/
|
|
543
|
+
const 목록못읽음 = 못읽음.find((x) => x.무엇 === '목록');
|
|
544
|
+
if (목록못읽음) {
|
|
545
|
+
return { ok: false, why: `git 이 담긴 것을 못 읽었습니다 — ${목록못읽음.왜}` };
|
|
546
|
+
}
|
|
469
547
|
if (!파일들.length) return { ok: false, why: '담을 것이 없습니다 — 바뀐 내용이 없습니다.' };
|
|
548
|
+
// 몸통(diff)만 죽는 판이 따로 있다 — 64MB 를 넘으면 그렇다. 파일 목록은
|
|
549
|
+
// 멀쩡하므로 커밋 자체는 이어 가되, **모델에게도 화면에도** 못 봤다고 말한다.
|
|
550
|
+
const diff못읽음 = 못읽음.find((x) => x.무엇 === 'diff')?.왜 ?? null;
|
|
470
551
|
|
|
471
552
|
const 증거 = (() => { try { return 증거모으기(session, { audit: ctx?.audit }); } catch { return null; } })();
|
|
472
553
|
const 미확인 = 증거?.셈?.증명안됨 ?? 0;
|
|
473
|
-
const 확인 =
|
|
554
|
+
const 확인 = 증거?.셈 ? (증거.셈.파일 ?? 0) - 미확인 : 0;
|
|
474
555
|
|
|
475
|
-
const 지은것 = await 메시지짓기(session, { 뿌리, diff, 통계, 파일들, 증거, 제목, signal, onBackoff });
|
|
556
|
+
const 지은것 = await 메시지짓기(session, { 뿌리, diff, 통계, 파일들, 증거, 제목, signal, onBackoff, diff못읽음 });
|
|
476
557
|
if (지은것?.중단) return { ok: false, why: '중단했습니다 — 담긴 것은 그대로 둡니다.', aborted: true };
|
|
477
558
|
|
|
478
559
|
const 사실 = !지은것;
|
|
@@ -506,6 +587,9 @@ export async function 커밋준비(session, ctx, { 전부 = false, 제목 = null
|
|
|
506
587
|
살림뺌: 살림바뀜 || 뺀것.샌것.length > 0,
|
|
507
588
|
링크로샌것: 뺀것.샌것,
|
|
508
589
|
폴더통째: 전부 ? [] : (내것.폴더 ?? []),
|
|
590
|
+
// git 이 답을 못 한 것. 화면이 그대로 말한다 — 못 본 것을 안 본 척하면
|
|
591
|
+
// 메시지가 무엇을 근거로 쓰였는지가 사라진다.
|
|
592
|
+
diff못읽음,
|
|
509
593
|
// 물어보는 사이에 담긴 것이 바뀌었는지 다시 보라고. 보여 준 것과 다른 것을
|
|
510
594
|
// 찍으면 승인을 받은 뜻이 없어진다.
|
|
511
595
|
다시확인: () => 담긴것(뿌리).파일들,
|