deel-local-cli 2.1.0 → 2.1.2
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 +7 -5
- package/README.md +7 -5
- package/bin/deel.js +56 -43
- package/package.json +1 -1
- package/src/acp/serve.js +85 -2
- package/src/agent/donecheck.js +28 -5
- package/src/agent/loop.js +26 -6
- package/src/agent/route.js +2 -1
- package/src/agent/session.js +40 -6
- package/src/backend/http.js +2 -23
- package/src/backend/mcp.js +15 -0
- package/src/completion.js +6 -0
- package/src/configexplain.js +18 -1
- package/src/eval/run.js +142 -26
- package/src/eval/starter.js +1 -1
- package/src/exit.js +116 -0
- package/src/i18n/en.js +5 -1
- package/src/i18n/ja.js +5 -1
- package/src/i18n/ko.js +5 -1
- package/src/i18n/zh.js +5 -1
- package/src/lsp/client.js +3 -1
- package/src/oneshot.js +22 -88
- package/src/plugins/manage.js +8 -2
- package/src/repl.js +1 -0
- package/src/safety/secrets.js +37 -0
- package/src/safety/shellenv.js +8 -2
- package/src/safety/undo.js +77 -7
- package/src/setup.js +3 -5
- package/src/tools/lsp.js +13 -4
- package/src/ui/approve.js +3 -1
- package/src/ui/banner.js +8 -1
package/src/i18n/ja.js
CHANGED
|
@@ -286,11 +286,12 @@ export const ja = {
|
|
|
286
286
|
'approve.auto.line': '訊かずに直します。/undo が受け皿です',
|
|
287
287
|
'approve.confirm.name': '危ないものだけ',
|
|
288
288
|
'approve.confirm.short': '危険時',
|
|
289
|
-
'approve.confirm.line': '
|
|
289
|
+
'approve.confirm.line': '取り消せないコマンドと MCP ツールの前に訊きます。ファイルは訊かずに直します',
|
|
290
290
|
'approve.strict.name': '毎回訊く',
|
|
291
291
|
'approve.strict.short': '毎回',
|
|
292
292
|
'approve.strict.line': 'ファイルを直すたび、コマンドを打つたびに訊きます',
|
|
293
293
|
'approve.newFile': '新しいファイル',
|
|
294
|
+
'approve.noChange': '変更なし — 今と同じ内容',
|
|
294
295
|
'approve.locked': '管理ポリシーが承認を {바닥} より下げられないようにしています — {곳}',
|
|
295
296
|
'approve.lockedUnknown': '管理ポリシーの approval の値「{값}」が分からないため、いちばん狭い strict とみなします — {곳}',
|
|
296
297
|
'approve.noAlways': '管理ポリシーが承認を固定しているため「今後は訊かない」はありません',
|
|
@@ -476,12 +477,15 @@ export const ja = {
|
|
|
476
477
|
'ev.hookBlock': 'フックが止めました',
|
|
477
478
|
'ev.nudgeRead': '読むだけで終えようとしたので、一度押し戻しました',
|
|
478
479
|
'check.start': '完了チェック ({판}/{최대}) — {명령}',
|
|
480
|
+
'check.clamped': '{칸} {준값} は上限を超えるため {쓴값} にしました',
|
|
481
|
+
'check.invalid': '{칸} {준값} は使えない値のため既定の {쓴값} を使います',
|
|
479
482
|
'check.pass': '完了チェック合格 — {명령}',
|
|
480
483
|
'check.fail': '完了チェック失敗 ({판}/{최대}) — {요약}',
|
|
481
484
|
'check.retry': '失敗内容を返して直させます',
|
|
482
485
|
'check.gaveUp': '{판}回回しても失敗しました — 失敗として終えます',
|
|
483
486
|
'check.skipped': '完了チェックを回せませんでした — {까닭}',
|
|
484
487
|
'cli.checkFlag': '終える前にこのチェックを回し、通れば終了(失敗なら直させ、最後まで失敗なら 8)',
|
|
488
|
+
'cli.checkRoundsFlag': 'この実行だけ:チェックを回す回数(1〜10、直す機会は回数−1)と1回の制限時間(秒)',
|
|
485
489
|
'cli.evalTitle': 'ゴールデンセット',
|
|
486
490
|
'cli.evalWhy': '— 課題集を実際のモデルで回し、いくつこなせるかを測ります',
|
|
487
491
|
'cli.evalInit': 'golden/ に例題を五つ作ります',
|
package/src/i18n/ko.js
CHANGED
|
@@ -308,11 +308,12 @@ export const ko = {
|
|
|
308
308
|
'approve.auto.line': '안 묻고 고칩니다. 되돌리기(/undo)가 안전망입니다',
|
|
309
309
|
'approve.confirm.name': '위험만 확인',
|
|
310
310
|
'approve.confirm.short': '위험만',
|
|
311
|
-
'approve.confirm.line': '되돌릴 수 없는
|
|
311
|
+
'approve.confirm.line': '되돌릴 수 없는 명령·MCP 도구만 물어봅니다. 파일은 안 묻고 고칩니다',
|
|
312
312
|
'approve.strict.name': '모두 확인',
|
|
313
313
|
'approve.strict.short': '모두',
|
|
314
314
|
'approve.strict.line': '파일을 고치거나 명령을 돌리기 전에 매번 물어봅니다',
|
|
315
315
|
'approve.newFile': '새 파일',
|
|
316
|
+
'approve.noChange': '바뀌는 것 없음 — 지금과 같은 내용',
|
|
316
317
|
'approve.locked': '관리 정책이 승인을 {바닥} 아래로 못 내리게 걸어 두었습니다 — {곳}',
|
|
317
318
|
'approve.lockedUnknown': '관리 정책의 approval 값 "{값}" 을 몰라 제일 좁은 strict 로 칩니다 — {곳}',
|
|
318
319
|
'approve.noAlways': '관리 정책이 승인을 걸어 두어 「앞으로 묻지 않기」 는 없습니다',
|
|
@@ -550,12 +551,15 @@ export const ko = {
|
|
|
550
551
|
'ev.hookBlock': '훅이 막았습니다',
|
|
551
552
|
'ev.nudgeRead': '읽기만 하고 끝내려고 해서 한 번 되밀었습니다',
|
|
552
553
|
'check.start': '완료 검사 ({판}/{최대}) — {명령}',
|
|
554
|
+
'check.clamped': '{칸} {준값} 은 상한을 넘어 {쓴값} 으로 줄였습니다',
|
|
555
|
+
'check.invalid': '{칸} {준값} 은 쓸 수 없는 값이라 기본값 {쓴값} 을 씁니다',
|
|
553
556
|
'check.pass': '완료 검사 통과 — {명령}',
|
|
554
557
|
'check.fail': '완료 검사 실패 ({판}/{최대}) — {요약}',
|
|
555
558
|
'check.retry': '실패 내용을 돌려주고 다시 고치게 합니다',
|
|
556
559
|
'check.gaveUp': '{판}번 돌렸는데도 실패했습니다 — 실패로 끝냅니다',
|
|
557
560
|
'check.skipped': '완료 검사를 못 돌렸습니다 — {까닭}',
|
|
558
561
|
'cli.checkFlag': '끝내려 할 때 이 검사를 돌려 통과해야 끝남 (실패하면 고치게 하고, 끝내 실패면 8)',
|
|
562
|
+
'cli.checkRoundsFlag': '이번 실행에서만 검사 판수(1~10, 고칠 기회는 판수−1)·한 번의 제한 시간(초)을 덮음',
|
|
559
563
|
'cli.evalTitle': '골든셋',
|
|
560
564
|
'cli.evalWhy': '— 과제 모음을 실제 모델로 돌려 몇 개를 해내나 잽니다',
|
|
561
565
|
'cli.evalInit': '예제 과제 다섯을 golden/ 에 만듭니다',
|
package/src/i18n/zh.js
CHANGED
|
@@ -286,11 +286,12 @@ export const zh = {
|
|
|
286
286
|
'approve.auto.line': '不问就改。/undo 是兜底',
|
|
287
287
|
'approve.confirm.name': '只问危险的',
|
|
288
288
|
'approve.confirm.short': '危险时',
|
|
289
|
-
'approve.confirm.line': '
|
|
289
|
+
'approve.confirm.line': '收不回的命令和 MCP 工具之前会问。改文件不问',
|
|
290
290
|
'approve.strict.name': '每次都问',
|
|
291
291
|
'approve.strict.short': '每次',
|
|
292
292
|
'approve.strict.line': '每改一个文件、每跑一条命令都问',
|
|
293
293
|
'approve.newFile': '新文件',
|
|
294
|
+
'approve.noChange': '没有变化 — 与现在内容相同',
|
|
294
295
|
'approve.locked': '管理策略把审批锁在 {바닥} 或更严 — {곳}',
|
|
295
296
|
'approve.lockedUnknown': '不认识管理策略的 approval 值「{값}」,按最严的 strict 处理 — {곳}',
|
|
296
297
|
'approve.noAlways': '管理策略锁定了审批,没有「以后不再问」',
|
|
@@ -476,12 +477,15 @@ export const zh = {
|
|
|
476
477
|
'ev.hookBlock': '钩子拦下了这一步',
|
|
477
478
|
'ev.nudgeRead': '只看不动就要收尾,已经推回去一次',
|
|
478
479
|
'check.start': '完成检查 ({판}/{최대}) — {명령}',
|
|
480
|
+
'check.clamped': '{칸} {준값} 超过上限,改用 {쓴값}',
|
|
481
|
+
'check.invalid': '{칸} {준값} 不是可用的值,改用默认值 {쓴값}',
|
|
479
482
|
'check.pass': '完成检查通过 — {명령}',
|
|
480
483
|
'check.fail': '完成检查失败 ({판}/{최대}) — {요약}',
|
|
481
484
|
'check.retry': '把失败内容交回去重新修',
|
|
482
485
|
'check.gaveUp': '跑了 {판} 次仍然失败 — 按失败结束',
|
|
483
486
|
'check.skipped': '没能跑完成检查 — {까닭}',
|
|
484
487
|
'cli.checkFlag': '结束前跑这个检查,通过才结束(失败就让它修,最后仍失败则返回 8)',
|
|
488
|
+
'cli.checkRoundsFlag': '仅本次运行:检查跑几次(1~10,修复机会 = 次数 − 1)和每次的时限(秒)',
|
|
485
489
|
'cli.evalTitle': '黄金集',
|
|
486
490
|
'cli.evalWhy': '— 用真实模型跑一组任务,测它能完成多少',
|
|
487
491
|
'cli.evalInit': '在 golden/ 里建五个示例任务',
|
package/src/lsp/client.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { spawn } from 'node:child_process';
|
|
29
29
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
30
30
|
import { readFileSync } from 'node:fs';
|
|
31
|
+
import { decode } from '../tools/encoding.js';
|
|
31
32
|
import { 갈래, 고르기, 언어아이디 } from './servers.js';
|
|
32
33
|
import { 틀, 받개 } from './rpc.js';
|
|
33
34
|
import { 신호에거두기, 다시보낼까 } from '../reap.js';
|
|
@@ -419,7 +420,8 @@ export class 언어서버 {
|
|
|
419
420
|
const uri = pathToFileURL(abs).href;
|
|
420
421
|
let 내용 = 글;
|
|
421
422
|
if (내용 == null) {
|
|
422
|
-
|
|
423
|
+
// 도구(tools/lsp.js)와 같은 자로 읽는다 — EUC-KR 소스를 날 UTF-8 로 보여 주면 서버가 깨진 글을 색인한다.
|
|
424
|
+
try { 내용 = decode(readFileSync(abs)).text; } catch { return null; }
|
|
423
425
|
}
|
|
424
426
|
// 판 번호는 **경로**로 센다. 서버가 주소를 다르게 적어 돌려주므로
|
|
425
427
|
// (pyright 의 `file:///c%3A/…`), 글자 그대로 열쇠를 삼으면 진단이 왔을 때
|
package/src/oneshot.js
CHANGED
|
@@ -49,93 +49,10 @@ import { 모두끝내기 as 일감모두끝내기, 일감인자 } from './tools/
|
|
|
49
49
|
import { 첫이름 } from './tools/label.js';
|
|
50
50
|
import { 검사설정 } from './agent/donecheck.js';
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* 스크립트는 화면 글이 아니라 이 숫자만 본다. 그래서 '끝났다' 와 '끝난 척했다'
|
|
56
|
-
* 를 반드시 갈라 놔야 한다 — 걸음 수 상한에 걸려 멈춘 것을 0 으로 돌려주면
|
|
57
|
-
* 야간 배치가 아무 일도 안 하고 초록불을 켠다. 그게 제일 나쁜 결말이다.
|
|
58
|
-
*/
|
|
59
|
-
export const EXIT = {
|
|
60
|
-
done: 0, // 끝까지 답했다
|
|
61
|
-
error: 1, // 오류로 끝났다 (연결 없음 · 시킬 말 없음 · 모델 오류)
|
|
62
|
-
limit: 2, // 도구 호출 걸음 수 상한에 닿았다
|
|
63
|
-
stuck: 3, // 같은 자리에서 헛돌아 스스로 멈췄다
|
|
64
|
-
aborted: 4, // 도중에 끊겼다 (Ctrl+C)
|
|
65
|
-
/*
|
|
66
|
-
* 서버가 끝났다는 말을 한 번도 안 주고 답을 멈췄다.
|
|
67
|
-
*
|
|
68
|
-
* 여기에 자기 코드가 있어야 한다. 전에는 이걸 아예 안 보고 `done`(0) 으로
|
|
69
|
-
* 끝냈다. `deel -p` 는 잡·CI 에서 돌고 그 뒤에 스크립트가 붙는데, 반쪽짜리
|
|
70
|
-
* 답이 0 으로 넘어가면 그 스크립트는 온전한 답으로 알고 그대로 쓴다.
|
|
71
|
-
* 사람이 나중에 결과를 보고 왜 반쪽인지 되짚을 방법이 없다.
|
|
72
|
-
*/
|
|
73
|
-
cutoff: 5,
|
|
74
|
-
/*
|
|
75
|
-
* 모델이 안 하겠다고 했다 (안전 판정).
|
|
76
|
-
*
|
|
77
|
-
* 이것도 자기 코드가 있어야 한다. 거절한 답은 **아무 일도 안 한 답**인데,
|
|
78
|
-
* 도구 호출이 없다는 것 말고는 짧은 답과 겉모습이 같다. 0 으로 끝내면
|
|
79
|
-
* 뒤에 붙은 스크립트는 일이 된 줄 알고 그 다음 단계로 넘어간다.
|
|
80
|
-
*
|
|
81
|
-
* 오류(1)와도 갈라 둔다. 고칠 자리가 다르기 때문이다 — 오류는 연결이나
|
|
82
|
-
* 열쇠를 보는 일이고, 거절은 **시킨 말을 바꾸는 일**이다.
|
|
83
|
-
*/
|
|
84
|
-
refusal: 6,
|
|
85
|
-
/*
|
|
86
|
-
* 답이 --output-schema 에 안 맞았다.
|
|
87
|
-
*
|
|
88
|
-
* 여기에도 자기 코드가 있어야 한다. 「모양을 못 박아 달라」 고 한 사람에게
|
|
89
|
-
* 모양이 안 맞는 것을 0 으로 넘기면, 파이프 뒤 스크립트가 그걸 온전한
|
|
90
|
-
* 것으로 받아 쓴다 — 그게 이 기능이 없애려던 바로 그 상황이다.
|
|
91
|
-
*
|
|
92
|
-
* 오류(1)와도 가른다. 고칠 자리가 다르다 — 오류는 연결을 보는 일이고,
|
|
93
|
-
* 이건 스키마나 시킨 말을 바꾸는 일이다.
|
|
94
|
-
*/
|
|
95
|
-
schema: 7,
|
|
96
|
-
/*
|
|
97
|
-
* 완료 검사가 통과하지 않은 채 끝났다 (agent/donecheck.js · 2.1.0).
|
|
98
|
-
*
|
|
99
|
-
* 모델은 「다 됐습니다」 라고 했는데 사람이 정해 둔 검사(`--check` · 설정의 `check`)가
|
|
100
|
-
* 정해 둔 판수를 다 돌고도 빨갛거나, 관문에 막혀 아예 못 돌았다. 0 으로 끝내면 야간
|
|
101
|
-
* 배치가 빨간 검사 위에 초록불을 켠다 — 이 기능이 없애려던 바로 그 결말이다.
|
|
102
|
-
*
|
|
103
|
-
* 오류(1)와 가른다. 고칠 자리가 다르다 — 연결이 아니라 코드나 검사 쪽이다.
|
|
104
|
-
*/
|
|
105
|
-
check: 8,
|
|
106
|
-
/*
|
|
107
|
-
* 인자를 잘못 줬다 — 모르는 깃발 · 값 없는 깃발 · 모르는 모드 이름 · 못 읽는 --ctx.
|
|
108
|
-
*
|
|
109
|
-
* 여기에 **따로** 둔다. 처음에는 `--work` 오타를 2 로 끝냈는데, 2 는 이 표에서 이미
|
|
110
|
-
* 「걸음 수 상한」 이다. `deel run --jsn …` 을 CI 에 건 사람은 오타 하나로 「일이 커서
|
|
111
|
-
* 멈췄다」 를 받고 작업을 쪼개러 간다. 고칠 자리가 스크립트 한 줄인데.
|
|
112
|
-
*
|
|
113
|
-
* 64 는 sysexits.h 의 EX_USAGE 다. 1~8 과 안 겹치고, 셸·CI 도구들이 「부른 모양이
|
|
114
|
-
* 틀렸다」 로 이미 알아듣는 수라 새로 지어내지 않았다. 이 경우는 모델을 한 번도 안 부른다.
|
|
115
|
-
*/
|
|
116
|
-
usage: 64,
|
|
117
|
-
};
|
|
52
|
+
// 종료코드 표와 실패덩이는 src/exit.js 에 있다 — 진입점이 이 파일 없이 쓰게 (2.1.2).
|
|
53
|
+
export { EXIT, 실패덩이 } from './exit.js';
|
|
54
|
+
import { EXIT, 실패덩이 } from './exit.js';
|
|
118
55
|
|
|
119
|
-
/**
|
|
120
|
-
* 모델을 부르기 전에 선 실패 한 덩이 (사냥5 B5-10).
|
|
121
|
-
*
|
|
122
|
-
* 성공한 `--json` 에는 model 과 usage.prompt·cacheRead·cacheWrite·못잰것 이 있는데, 모델을
|
|
123
|
-
* 부르기 전에 선 실패(64 · 7 · 시킬 말 없음 · 없는 뿌리 · 연결 없음 · 대화 화면 전용)에는
|
|
124
|
-
* 그 칸들이 없었다. `jq .usage.prompt` 는 null 을, 모양을 못 박은 파서는 오류를 낸다 —
|
|
125
|
-
* 실패를 제일 잘 다뤄야 할 자리에서 파서가 먼저 죽는다. 칸은 늘 같게 두고 값만 비운다.
|
|
126
|
-
*
|
|
127
|
-
* bin/deel.js 의 인자탈·마지막 catch 도 이걸로 짓는다. 세 벌이면 언젠가 한 벌만 고친다.
|
|
128
|
-
*/
|
|
129
|
-
export function 실패덩이({ reason, code, why = '', model = null }) {
|
|
130
|
-
return {
|
|
131
|
-
ok: false, reason, code, text: '',
|
|
132
|
-
tools: 0, steps: 0,
|
|
133
|
-
usage: { in: 0, out: 0, prompt: 0, cacheRead: 0, cacheWrite: 0, calls: 0, ms: 0, 못잰것: 0, retries: 0 },
|
|
134
|
-
model,
|
|
135
|
-
ms: 0,
|
|
136
|
-
...(why ? { why } : {}),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
56
|
|
|
140
57
|
/**
|
|
141
58
|
* 표준입력에 실려 온 말을 통째로 읽는다.
|
|
@@ -307,6 +224,17 @@ export async function runOnce(opts = {}) {
|
|
|
307
224
|
}
|
|
308
225
|
}
|
|
309
226
|
|
|
227
|
+
/*
|
|
228
|
+
* --check-rounds · --check-timeout (2.1.2). 치는 자리에서 바로 고칠 수 있으니 틀린 값은 사용법 틀림(64)이다 —
|
|
229
|
+
* 설정 파일에 적힌 틀린 값은 기본값을 쓰고 화면에 말하는 것(donecheck.js 의 줄임)과 다르다.
|
|
230
|
+
*/
|
|
231
|
+
if (opts.checkRounds !== undefined && !(Number.isInteger(Number(opts.checkRounds)) && Number(opts.checkRounds) >= 1)) {
|
|
232
|
+
return 못함('usage', `--check-rounds 는 1 이상의 정수여야 합니다: ${opts.checkRounds}`);
|
|
233
|
+
}
|
|
234
|
+
if (opts.checkTimeout !== undefined && !(Number.isFinite(Number(opts.checkTimeout)) && Number(opts.checkTimeout) > 0)) {
|
|
235
|
+
return 못함('usage', `--check-timeout 은 0 보다 큰 초여야 합니다: ${opts.checkTimeout}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
310
238
|
// ── 시킬 말 ───────────────────────────────────────────────────────────
|
|
311
239
|
// 인자로 준 것이 먼저다. 없을 때만 표준입력을 읽는다 —
|
|
312
240
|
// 둘 다 있을 때 무엇이 이기는지가 헷갈리면 스크립트가 조용히 엉뚱한 일을 한다.
|
|
@@ -629,7 +557,11 @@ export async function runOnce(opts = {}) {
|
|
|
629
557
|
// 안 거두면 배치가 끝나고도 언어 서버가 폴더를 물고 남는다.
|
|
630
558
|
lsp: { 켬: true },
|
|
631
559
|
// 끝내려는 자리에서 돌릴 검사 (agent/donecheck.js). `--check` 가 설정의 check 를 이긴다.
|
|
632
|
-
완료검사: 검사설정(
|
|
560
|
+
완료검사: 검사설정({
|
|
561
|
+
...cfg,
|
|
562
|
+
...(opts.checkRounds !== undefined ? { checkRounds: Number(opts.checkRounds) } : {}),
|
|
563
|
+
...(opts.checkTimeout !== undefined ? { checkTimeout: Number(opts.checkTimeout) } : {}),
|
|
564
|
+
}, opts.check),
|
|
633
565
|
// 되물을 사람이 없으니 기본값을 그대로 돌려준다.
|
|
634
566
|
ask: async (_label, o = {}) => o?.def ?? '',
|
|
635
567
|
// 엑셀 암호를 여기서 기다리면 그대로 선다. 없다고 바로 답한다 —
|
|
@@ -929,6 +861,7 @@ export async function runOnce(opts = {}) {
|
|
|
929
861
|
|
|
930
862
|
case 'check_start':
|
|
931
863
|
곁(` ${c.cyan('⧗')} ${c.gray(옮긴말('check.start', { 판: ev.판, 최대: ev.최대, 명령: ev.명령 }))}`);
|
|
864
|
+
for (const 줄 of ev.줄임 ?? []) 곁(` ${c.yellow('⚠')} ${c.gray(옮긴말(줄.틀림 ? 'check.invalid' : 'check.clamped', 줄))}`);
|
|
932
865
|
break;
|
|
933
866
|
|
|
934
867
|
case 'check':
|
|
@@ -936,7 +869,8 @@ export async function runOnce(opts = {}) {
|
|
|
936
869
|
else if (ev.ok === false) {
|
|
937
870
|
곁(` ${c.red('✗')} ${c.gray(옮긴말('check.fail', { 판: ev.판, 최대: ev.최대, 요약: ev.요약 }))}`);
|
|
938
871
|
// 실패 출력의 끝 몇 줄 — 배치 로그를 보는 사람이 무엇이 틀렸는지 여기서 안다.
|
|
939
|
-
|
|
872
|
+
// 빈 줄을 **먼저** 거르고 자른다 — 거꾸로 하면 원인 뒤 빈 줄 여럿이 8칸을 다 먹는다 (repl.js 와 같은 차례).
|
|
873
|
+
for (const 줄 of String(ev.꼬리 ?? '').trim().split('\n').filter((l) => l.trim()).slice(-8)) 곁(` ${c.gray(줄)}`);
|
|
940
874
|
곁(` ${c.gray(ev.판 < ev.최대 ? 옮긴말('check.retry') : 옮긴말('check.gaveUp', { 판: ev.판 }))}`);
|
|
941
875
|
} else 곁(` ${c.yellow('⊘')} ${c.gray(옮긴말('check.skipped', { 까닭: ev.까닭 }))}`);
|
|
942
876
|
break;
|
package/src/plugins/manage.js
CHANGED
|
@@ -247,7 +247,7 @@ async function fetchInto(spec, dest, onStep) {
|
|
|
247
247
|
* 이 함수가 없으면 tar 엔트리 이름을 아무리 잘 막아도 소용이 없다. 묶음이
|
|
248
248
|
* **제 이름으로** 나가기 때문이다.
|
|
249
249
|
*/
|
|
250
|
-
export function 이름한칸(
|
|
250
|
+
export function 이름한칸(이름, { 점허용 = false } = {}) {
|
|
251
251
|
const 글 = String(이름 ?? '').replace(/\\/g, '/').trim();
|
|
252
252
|
if (!글) return null;
|
|
253
253
|
// 마지막 칸만 쓴다. `a/b/../c` 같은 것도 여기서 한 칸이 된다.
|
|
@@ -266,6 +266,12 @@ export function 이름한칸(이름) {
|
|
|
266
266
|
*/
|
|
267
267
|
if (/[<>"|?*]/.test(한칸) || [...한칸].some((ch) => ch.charCodeAt(0) < 32 || ch.charCodeAt(0) === 127)) return null;
|
|
268
268
|
if (/[. ]$/.test(한칸)) return null;
|
|
269
|
+
/*
|
|
270
|
+
* 점으로 **시작**하는 이름도 안 된다. 깔리기는 하는데 list()·이름으로찾기() 가 점 이름을 숨겨서
|
|
271
|
+
* (`.tmp-*`·`.old-*` 를 가리려고 둔 규칙) 영영 안 보이고, 스킬 찾기도 점 폴더를 안 내려간다 (2.1.1 검수).
|
|
272
|
+
* 지우는 자리(remove)만 `점허용` 으로 받는다 — 이 막이 전에 깔린 것을 폴더 이름으로 지울 길은 남긴다.
|
|
273
|
+
*/
|
|
274
|
+
if (!점허용 && 한칸.startsWith('.')) return null;
|
|
269
275
|
if (/^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i.test(한칸)) return null;
|
|
270
276
|
if (한칸.length > 100) return null;
|
|
271
277
|
return 한칸;
|
|
@@ -515,7 +521,7 @@ export function remove(name, { home = homeDir() } = {}) {
|
|
|
515
521
|
* 설치 때 막아도 이미 깔려 있던 것에는 못 쓴 이름이 남아 있을 수 있다.
|
|
516
522
|
*/
|
|
517
523
|
const base = pluginsDir(home);
|
|
518
|
-
const 한칸 = 이름한칸(name);
|
|
524
|
+
const 한칸 = 이름한칸(name, { 점허용: true });
|
|
519
525
|
const 찾는이름 = String(name ?? '').trim();
|
|
520
526
|
/*
|
|
521
527
|
* 폴더 이름으로 못 찾으면 **매니페스트 이름**으로 찾는다. 목록(list)은 매니페스트
|
package/src/repl.js
CHANGED
|
@@ -3057,6 +3057,7 @@ export async function chatLoop(opts = {}) {
|
|
|
3057
3057
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
3058
3058
|
say('');
|
|
3059
3059
|
say(` ${c.cyan('⧗')} ${c.gray(옮긴말('check.start', { 판: ev.판, 최대: ev.최대, 명령: ev.명령 }))}`);
|
|
3060
|
+
for (const 줄 of ev.줄임 ?? []) say(` ${c.yellow('⚠')} ${c.gray(옮긴말(줄.틀림 ? 'check.invalid' : 'check.clamped', 줄))}`);
|
|
3060
3061
|
break;
|
|
3061
3062
|
|
|
3062
3063
|
case 'check':
|
package/src/safety/secrets.js
CHANGED
|
@@ -34,6 +34,26 @@
|
|
|
34
34
|
/** 가려진 자리에 남는 표. 종류를 같이 적어 무엇이 가려졌는지 알 수 있게 한다. */
|
|
35
35
|
const 표 = (종류) => `«가림:${종류}»`;
|
|
36
36
|
|
|
37
|
+
/*
|
|
38
|
+
* 웹훅 주소의 머리 — 여기까지는 누구 것이든 같고, 뒤가 열쇠다.
|
|
39
|
+
* 가리기(아래 갈래 의 웹훅)와 셸 환경 거르기(shellenv.js)가 이 한 벌을 같이 쓴다. 두 벌이면
|
|
40
|
+
* 한쪽에만 새 서비스를 더하고 다른 쪽은 새는 날이 반드시 온다.
|
|
41
|
+
*/
|
|
42
|
+
const 웹훅머리 = [
|
|
43
|
+
String.raw`\bhttps:\/\/hooks\.slack\.com\/(?:services|workflows|triggers)\/`,
|
|
44
|
+
String.raw`\bhttps:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/`,
|
|
45
|
+
String.raw`\bhttps:\/\/[\w-]+\.webhook\.office\.com\/webhookb2\/`,
|
|
46
|
+
String.raw`\bhttps:\/\/outlook\.office(?:365)?\.com\/webhook\/`,
|
|
47
|
+
].join('|');
|
|
48
|
+
const 웹훅주소무늬 = new RegExp(`^(?:${웹훅머리})[^\\s"'<>]{6,}`, 'i');
|
|
49
|
+
const 센트리무늬 = /^https?:\/\/[0-9a-f]{32}(?::[0-9a-f]{32})?@/i;
|
|
50
|
+
|
|
51
|
+
/** 값이 웹훅 주소나 Sentry DSN 인가 — 이름으로는 못 거르는 비밀. shellenv.js 가 쓴다. */
|
|
52
|
+
export function 웹훅주소인가(값) {
|
|
53
|
+
const s = typeof 값 === 'string' ? 값.trim() : '';
|
|
54
|
+
return 웹훅주소무늬.test(s) || 센트리무늬.test(s);
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
/**
|
|
38
58
|
* 찾는 것들.
|
|
39
59
|
*
|
|
@@ -207,6 +227,23 @@ export const 갈래 = [
|
|
|
207
227
|
{ id: 'openai', re: /\bsk-[A-Za-z0-9_-]{20,}/g },
|
|
208
228
|
{ id: 'github', re: /\b(?:gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,})\b/g },
|
|
209
229
|
{ id: 'slack', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
230
|
+
{
|
|
231
|
+
/*
|
|
232
|
+
* 웹훅 주소 — 주소 **자체가** 열쇠다. 아는 사람은 누구나 그 채널에 글을 올린다.
|
|
233
|
+
* 이름(SLACK_WEBHOOK_URL)도 값의 꼴(사람:암호@)도 비밀이라고 말하지 않아서 `env` 한 줄에
|
|
234
|
+
* 그대로 실렸다 (2.1.1 검수). 호스트와 갈래(services/…)는 남긴다 — 어디로 가는 주소인지는
|
|
235
|
+
* 사람이 봐야 한다. 셸 환경(shellenv.js)도 같은 자(웹훅주소)로 값을 거른다.
|
|
236
|
+
*/
|
|
237
|
+
id: '웹훅',
|
|
238
|
+
re: new RegExp(`(${웹훅머리})[^\\s"'<>]{6,}`, 'gi'),
|
|
239
|
+
바꾸기: (m, 앞) => `${앞}${표('웹훅')}`,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
// Sentry DSN — `https://<공개열쇠 32자>@o1.ingest.sentry.io/42`. 비밀번호 칸이 없어 주소속열쇠 에 안 걸린다.
|
|
243
|
+
id: 'sentry',
|
|
244
|
+
re: /(\bhttps?:\/\/)[0-9a-f]{32}(?::[0-9a-f]{32})?@/gi,
|
|
245
|
+
바꾸기: (m, 앞) => `${앞}${표('sentry')}@`,
|
|
246
|
+
},
|
|
210
247
|
{ id: 'aws', re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
|
|
211
248
|
{ id: 'google', re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
212
249
|
// `stripe.setKey("sk_live_…")` 처럼 이름이 비밀을 말하지 않는 자리는 값의 꼴로만 안다(2.0.0 3회차 사냥).
|
package/src/safety/shellenv.js
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
// 그래서 **무엇을 뺐는지 이름을 돌려준다.** 값은 절대 안 돌려준다.
|
|
50
50
|
// 부르는 쪽이 그 이름을 도구 결과에 적고, 되살리는 법 한 줄을 같이 적는다.
|
|
51
51
|
import { 열쇠환경인가 } from '../config.js';
|
|
52
|
+
import { 웹훅주소인가 } from './secrets.js';
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* 이 마디가 있으면 뺀다.
|
|
@@ -115,9 +116,14 @@ function 이름마디들(이름) {
|
|
|
115
116
|
*/
|
|
116
117
|
const 자격든주소 = /^[a-z][a-z0-9+.-]*:\/\/[^\s/?#@:]*:[^\s/?#@]+@/i;
|
|
117
118
|
|
|
118
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* 이 값이 자격이 든 주소인가. 이름으로는 못 거르는 DATABASE_URL 같은 것을 거른다.
|
|
121
|
+
*
|
|
122
|
+
* 웹훅 주소·Sentry DSN 도 본다 — 주소 자체가 열쇠인데 이름(SLACK_WEBHOOK_URL · TEAMS_URL)도
|
|
123
|
+
* 꼴(사람:암호@)도 그렇다고 말하지 않는다. 무늬는 가리기와 한 벌이다(secrets.js 의 웹훅주소인가).
|
|
124
|
+
*/
|
|
119
125
|
function 비밀값인가(값) {
|
|
120
|
-
return typeof 값 === 'string' && 자격든주소.test(값.trim());
|
|
126
|
+
return typeof 값 === 'string' && (자격든주소.test(값.trim()) || 웹훅주소인가(값));
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
/**
|
package/src/safety/undo.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 되돌리기. 승인 프롬프트를 안 쓰는 대신 이게 안전망이다.
|
|
2
2
|
// 파일을 고치기 전에 항상 이전 내용을 떠 놓고, /undo 로 턴 단위로 되돌린다.
|
|
3
3
|
import { join, dirname, basename, resolve, relative, isAbsolute, sep } from 'node:path';
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, rmdirSync, appendFileSync, statSync, renameSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, rmdirSync, appendFileSync, statSync, renameSync, realpathSync, openSync, readSync, closeSync } from 'node:fs';
|
|
5
5
|
import { 줄로끝나나, 본인만잠그기 } from '../jsonl.js';
|
|
6
6
|
import { looksBinary } from '../tools/encoding.js';
|
|
7
7
|
import { 살림폴더만들기 } from './audit.js';
|
|
@@ -357,7 +357,7 @@ export class History {
|
|
|
357
357
|
if (남길것.length !== recs.length) {
|
|
358
358
|
try {
|
|
359
359
|
writeFileSync(this.file, 남길것.map(적을줄).join('\n') + (남길것.length ? '\n' : ''), 'utf8');
|
|
360
|
-
this.#잠갔나 = false; this.#잠그기();
|
|
360
|
+
this.#잠갔나 = false; this.#잠그기(); this.#턴표 = null;
|
|
361
361
|
} catch { return false; }
|
|
362
362
|
}
|
|
363
363
|
if (this.#이번턴.get(rec.path) === rec) {
|
|
@@ -473,7 +473,7 @@ export class History {
|
|
|
473
473
|
try {
|
|
474
474
|
writeFileSync(this.file, 남길것.map(적을줄).join('\n') + (남길것.length ? '\n' : ''), 'utf8');
|
|
475
475
|
// 통째로 다시 쓰면 새 파일일 수 있다 — 빗장을 다시 건다 (agent/store.js 도 같다).
|
|
476
|
-
this.#잠갔나 = false; this.#잠그기();
|
|
476
|
+
this.#잠갔나 = false; this.#잠그기(); this.#턴표 = null;
|
|
477
477
|
this.줄이기못함 = null;
|
|
478
478
|
} catch (err) {
|
|
479
479
|
/*
|
|
@@ -540,10 +540,80 @@ export class History {
|
|
|
540
540
|
return 것들;
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
+
/*
|
|
544
|
+
* ── 턴 수는 덧붙인 만큼만 읽는다 (2.1.2) ────────────────────────────────
|
|
545
|
+
*
|
|
546
|
+
* 대화 화면은 턴이 끝날 때마다 여기로 되돌릴 턴 수를 센다(repl.js). 여태 all() 을 불러 이력
|
|
547
|
+
* 파일을 **매번 처음부터** 읽고 줄마다 JSON.parse 했다 — 이 파일은 고치기 전 원문을 통째로
|
|
548
|
+
* 담아 32MB 까지 자란다. 이력은 거의 늘 덧붙이기라, 지난번에 센 자리 뒤만 읽으면 된다.
|
|
549
|
+
*
|
|
550
|
+
* 센 자리는 **온전한 줄의 끝**(개행 뒤)에만 둔다. 뒤에 개행 없는 반쪽 줄이 있으면 적는 중일 수
|
|
551
|
+
* 있으니 이번 셈에만 넣고 자리는 안 옮긴다. 다른 쪽(다른 탭·다른 deel)이 파일을 통째로 다시
|
|
552
|
+
* 썼으면 처음부터 다시 센다 — 줄었거나, 크기가 같은데 시각이 바뀌었거나, 센 자리 앞뒤 조각이
|
|
553
|
+
* 달라졌으면 다시 쓴 것으로 본다. 이 이력이 제 손으로 다시 쓰는 자리(prune·undo·버리기)는
|
|
554
|
+
* 표를 버린다.
|
|
555
|
+
*/
|
|
556
|
+
#턴표 = null;
|
|
557
|
+
|
|
558
|
+
/** [시작, 끝) 바이트를 읽는다. 끝이 파일보다 크면 있는 만큼. */
|
|
559
|
+
#바이트(시작, 끝) {
|
|
560
|
+
const 길이 = Math.max(0, 끝 - 시작);
|
|
561
|
+
const buf = Buffer.alloc(길이);
|
|
562
|
+
if (!길이) return buf;
|
|
563
|
+
const fd = openSync(this.file, 'r');
|
|
564
|
+
try {
|
|
565
|
+
let 읽음 = 0;
|
|
566
|
+
while (읽음 < 길이) {
|
|
567
|
+
const n = readSync(fd, buf, 읽음, 길이 - 읽음, 시작 + 읽음);
|
|
568
|
+
if (!n) break;
|
|
569
|
+
읽음 += n;
|
|
570
|
+
}
|
|
571
|
+
return buf.subarray(0, 읽음);
|
|
572
|
+
} finally { closeSync(fd); }
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** 한 줄을 표에 센다. all() 과 같은 자로 — 깨졌거나 기록 꼴이 아니면 깨진 줄이다. */
|
|
576
|
+
static #줄셈(표, 줄) {
|
|
577
|
+
if (!줄) return;
|
|
578
|
+
let r;
|
|
579
|
+
try { r = JSON.parse(줄); } catch { 표.깨진 += 1; return; }
|
|
580
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) { 표.깨진 += 1; return; }
|
|
581
|
+
if (!표.본턴.has(r.turn)) { 표.본턴.add(r.turn); 표.턴들.push(r.turn); }
|
|
582
|
+
}
|
|
583
|
+
|
|
543
584
|
turns() {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
585
|
+
let 판;
|
|
586
|
+
try { 판 = statSync(this.file); } catch { this.#턴표 = null; this.깨진줄 = 0; return []; }
|
|
587
|
+
const 조각 = 64;
|
|
588
|
+
let 표 = this.#턴표;
|
|
589
|
+
const 다시썼나 = !표
|
|
590
|
+
|| 판.size < 표.크기
|
|
591
|
+
|| (판.size === 표.크기 && 판.mtimeMs !== 표.시각)
|
|
592
|
+
|| (표.크기 > 0 && !this.#바이트(0, Math.min(조각, 표.크기)).equals(표.머리))
|
|
593
|
+
|| (표.크기 > 0 && !this.#바이트(Math.max(0, 표.크기 - 조각), 표.크기).equals(표.끝));
|
|
594
|
+
if (다시썼나) 표 = { 크기: 0, 시각: 0, 머리: Buffer.alloc(0), 끝: Buffer.alloc(0), 턴들: [], 본턴: new Set(), 깨진: 0 };
|
|
595
|
+
|
|
596
|
+
// 센 자리 뒤를 읽는다. 온전한 줄까지만 표에 넣고, 반쪽 줄은 이번 셈에만.
|
|
597
|
+
const 새것 = 판.size > 표.크기 ? this.#바이트(표.크기, 판.size) : Buffer.alloc(0);
|
|
598
|
+
const 마지막개행 = 새것.lastIndexOf(0x0a);
|
|
599
|
+
if (마지막개행 >= 0) {
|
|
600
|
+
for (const 줄 of 새것.subarray(0, 마지막개행 + 1).toString('utf8').split('\n')) History.#줄셈(표, 줄);
|
|
601
|
+
표.크기 += 마지막개행 + 1;
|
|
602
|
+
표.머리 = this.#바이트(0, Math.min(조각, 표.크기));
|
|
603
|
+
표.끝 = this.#바이트(Math.max(0, 표.크기 - 조각), 표.크기);
|
|
604
|
+
}
|
|
605
|
+
표.시각 = 판.mtimeMs;
|
|
606
|
+
this.#턴표 = 표;
|
|
607
|
+
|
|
608
|
+
const 꼬리 = 마지막개행 >= 0 ? 새것.subarray(마지막개행 + 1) : 새것;
|
|
609
|
+
if (!꼬리.length) {
|
|
610
|
+
this.깨진줄 = 표.깨진;
|
|
611
|
+
return [...표.턴들];
|
|
612
|
+
}
|
|
613
|
+
const 이번 = { 턴들: [...표.턴들], 본턴: new Set(표.본턴), 깨진: 표.깨진 };
|
|
614
|
+
History.#줄셈(이번, 꼬리.toString('utf8'));
|
|
615
|
+
this.깨진줄 = 이번.깨진;
|
|
616
|
+
return 이번.턴들;
|
|
547
617
|
}
|
|
548
618
|
|
|
549
619
|
/**
|
|
@@ -723,7 +793,7 @@ export class History {
|
|
|
723
793
|
let 이력줄임 = { ok: true };
|
|
724
794
|
try {
|
|
725
795
|
writeFileSync(this.file, keep.map(적을줄).join('\n') + (keep.length ? '\n' : ''), 'utf8');
|
|
726
|
-
this.#잠갔나 = false; this.#잠그기();
|
|
796
|
+
this.#잠갔나 = false; this.#잠그기(); this.#턴표 = null;
|
|
727
797
|
} catch (err) {
|
|
728
798
|
이력줄임 = { ok: false, 왜: err?.message ?? String(err) };
|
|
729
799
|
}
|
package/src/setup.js
CHANGED
|
@@ -16,11 +16,9 @@ import { 바깥인가, 봉인됐나 } from './safety/runmode.js';
|
|
|
16
16
|
import { 제공자들, 제공자고르기, 어디것일까, 주소후보, 막힌까닭, 열쇠다듬기 } from './providers/index.js';
|
|
17
17
|
import { writeFileSync } from 'node:fs';
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
say('');
|
|
23
|
-
}
|
|
19
|
+
// 머리 한 줄은 ui/banner.js 에 있다 — 진입점이 이 파일(40개를 끌고 온다) 없이 쓰게 (2.1.2).
|
|
20
|
+
export { banner } from './ui/banner.js';
|
|
21
|
+
import { banner } from './ui/banner.js';
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
24
|
* 사람이 친 주소가 주소 꼴인가. 틀렸으면 까닭(사람 말), 멀쩡하면 null.
|
package/src/tools/lsp.js
CHANGED
|
@@ -23,12 +23,21 @@
|
|
|
23
23
|
* 그대로 보여 주고 고르게 한다 — 하나를 골라 주고 아닌 척하지 않는다.
|
|
24
24
|
*/
|
|
25
25
|
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { decode } from './encoding.js';
|
|
26
27
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
28
|
import { 얻기, 색인중일까 } from '../lsp/client.js';
|
|
28
29
|
import { 갈래, 프로젝트갈래 } from '../lsp/servers.js';
|
|
29
30
|
import { 찾을개수 } from '../agent/budget.js';
|
|
30
31
|
import { 말, 세말 } from '../i18n/index.js';
|
|
31
32
|
|
|
33
|
+
/*
|
|
34
|
+
* 소스를 읽는다 — Read·Edit 와 같은 자(encoding.js)로.
|
|
35
|
+
*
|
|
36
|
+
* 날 UTF-8 로 읽으면 EUC-KR·CP949 소스의 한글 이름이 깨진 글자(U+FFFD)가 되어 「그 줄에
|
|
37
|
+
* 없다」 로 실패하고, 서버에 보여 주는 내용(didOpen)도 깨진 글이 된다 (2.1.1 검수).
|
|
38
|
+
*/
|
|
39
|
+
const 소스읽기 = (abs) => decode(readFileSync(abs)).text;
|
|
40
|
+
|
|
32
41
|
/**
|
|
33
42
|
* 한 자리를 사람이 읽을 한 줄로. 그 줄의 글까지 붙여야 열어 보지 않고도 안다.
|
|
34
43
|
*
|
|
@@ -51,7 +60,7 @@ function 한줄(scope, uri, 범위, 줄표 = new Map()) {
|
|
|
51
60
|
// 윈도우는 서버가 드라이브 글자를 소문자로 적어 온다(c%3A) — 같은 파일을 두 번 읽지 않게.
|
|
52
61
|
const 열쇠 = process.platform === 'win32' ? abs.toLowerCase() : abs;
|
|
53
62
|
if (!줄표.has(열쇠)) {
|
|
54
|
-
try { 줄표.set(열쇠,
|
|
63
|
+
try { 줄표.set(열쇠, 소스읽기(abs).split(/\r?\n/)); } catch { 줄표.set(열쇠, null); /* 못 읽으면 자리만 준다 */ }
|
|
55
64
|
}
|
|
56
65
|
let 글 = (줄표.get(열쇠)?.[줄번호 - 1] ?? '').trim();
|
|
57
66
|
if (글.length > 160) 글 = 글.slice(0, 160) + '…';
|
|
@@ -105,7 +114,7 @@ async function 자리잡기(서버, scope, { 이름, 파일, 줄 }) {
|
|
|
105
114
|
let abs;
|
|
106
115
|
try { abs = scope.resolve(파일); } catch (e) { return { 오류: e.message }; }
|
|
107
116
|
let 줄들;
|
|
108
|
-
try { 줄들 =
|
|
117
|
+
try { 줄들 = 소스읽기(abs).split(/\r?\n/); } catch { return { 오류: `못 읽었습니다: ${파일}` }; }
|
|
109
118
|
서버.보여주기(abs, 줄들.join('\n'));
|
|
110
119
|
|
|
111
120
|
const 볼줄 = Number.isFinite(줄) && 줄 > 0 ? [줄 - 1] : 줄들.map((_, i) => i);
|
|
@@ -168,7 +177,7 @@ async function 자리잡기(서버, scope, { 이름, 파일, 줄 }) {
|
|
|
168
177
|
if (!position) {
|
|
169
178
|
try {
|
|
170
179
|
const abs = fileURLToPath(첫.uri);
|
|
171
|
-
const 줄들 =
|
|
180
|
+
const 줄들 = 소스읽기(abs).split(/\r?\n/);
|
|
172
181
|
for (let i = 0; i < 줄들.length; i++) {
|
|
173
182
|
const 칸 = 칸찾기(줄들[i], 이름);
|
|
174
183
|
if (칸 >= 0) { position = { line: i, character: 칸 }; break; }
|
|
@@ -181,7 +190,7 @@ async function 자리잡기(서버, scope, { 이름, 파일, 줄 }) {
|
|
|
181
190
|
// 서버가 아무것도 못 준다.
|
|
182
191
|
try {
|
|
183
192
|
const abs = fileURLToPath(첫.uri);
|
|
184
|
-
const 줄글 =
|
|
193
|
+
const 줄글 = 소스읽기(abs).split(/\r?\n/)[position.line] ?? '';
|
|
185
194
|
const 칸 = 칸찾기(줄글, 이름);
|
|
186
195
|
if (칸 >= 0) position = { line: position.line, character: 칸 };
|
|
187
196
|
서버.보여주기(abs);
|
package/src/ui/approve.js
CHANGED
|
@@ -111,7 +111,9 @@ export function 미리보기줄들(미리보기, { 줄수 = 40 } = {}) {
|
|
|
111
111
|
let d = null;
|
|
112
112
|
try { d = diffLines(것.전, 것.후); } catch { d = null; }
|
|
113
113
|
const 셈 = d ? shortStat(d) : '';
|
|
114
|
-
|
|
114
|
+
// 같은 내용으로 다시 쓰는 부름이면 그렇다고 적는다 — 이름 한 줄만 뜨면 사람은 무언가 바뀌는 줄 안다.
|
|
115
|
+
const 딸린말 = 것.전 === null ? ` (${말('approve.newFile')})` : 것.전 === 것.후 ? ` (${말('approve.noChange')})` : '';
|
|
116
|
+
out.push(` ${c.white(것.보일경로)}${딸린말 ? c.gray(딸린말) : ''}${셈 ? ` ${셈}` : ''}`);
|
|
115
117
|
if (d && 몫 >= 3) out.push(...renderDiff(d, { maxLines: 몫, indent: ' ' }));
|
|
116
118
|
}
|
|
117
119
|
return out;
|
package/src/ui/banner.js
CHANGED
|
@@ -39,7 +39,14 @@
|
|
|
39
39
|
* 조각으로 남는다. 여기 쓰는 것들은 입력 상자가 이미 쓰고 있는 것이라
|
|
40
40
|
* 이 프로그램이 도는 자리에서는 이미 한 번 검증된 셈이다.
|
|
41
41
|
*/
|
|
42
|
-
import { c, 번호색, 색256가능, width } from './ansi.js';
|
|
42
|
+
import { c, say, 번호색, 색256가능, width } from './ansi.js';
|
|
43
|
+
|
|
44
|
+
/** 도움말·설정·진단 맨 위의 한 줄. */
|
|
45
|
+
export function banner() {
|
|
46
|
+
say('');
|
|
47
|
+
say(` ${c.cyan('deel')} ${c.gray('— 로컬 모델 코딩 에이전트')}`);
|
|
48
|
+
say('');
|
|
49
|
+
}
|
|
43
50
|
|
|
44
51
|
/*
|
|
45
52
|
* 글꼴. 한 글자가 일곱 줄이다.
|