deel-local-cli 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +4 -3
- package/README.md +4 -3
- package/package.json +1 -1
- package/src/acp/serve.js +1 -0
- package/src/agent/donecheck.js +9 -1
- package/src/agent/loop.js +20 -4
- package/src/agent/route.js +2 -1
- package/src/agent/session.js +18 -1
- package/src/completion.js +4 -0
- package/src/configexplain.js +18 -1
- package/src/eval/run.js +71 -14
- package/src/eval/starter.js +1 -1
- package/src/i18n/en.js +2 -0
- package/src/i18n/ja.js +2 -0
- package/src/i18n/ko.js +2 -0
- package/src/i18n/zh.js +2 -0
- package/src/lsp/client.js +3 -1
- package/src/oneshot.js +3 -1
- 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/tools/lsp.js +13 -4
- package/src/ui/approve.js +3 -1
package/README.ko.md
CHANGED
|
@@ -323,7 +323,7 @@ deel --offline
|
|
|
323
323
|
무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
|
|
324
324
|
|
|
325
325
|
```
|
|
326
|
-
deel 2.1.
|
|
326
|
+
deel 2.1.1 ⌂ 이 안
|
|
327
327
|
보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
|
|
328
328
|
```
|
|
329
329
|
|
|
@@ -591,7 +591,7 @@ LM Studio 는 `/api/v0/models`, llama.cpp 는 `/props`. 못 알아보면 `(추
|
|
|
591
591
|
</picture>
|
|
592
592
|
|
|
593
593
|
무슨 일을 하는 중인지에 따라 **줄 수 있는 도구와 추론 설정이 같이 바뀝니다.**
|
|
594
|
-
`
|
|
594
|
+
`Ctrl+O` 로 차례로 돌리거나, 이름을 그대로 치면 됩니다. (`Shift+Tab` 은 승인 방식을 돌립니다.)
|
|
595
595
|
|
|
596
596
|
| 모드 | 하는 일 | 파일을 고치나 | 추론 |
|
|
597
597
|
|---|---|---|---|
|
|
@@ -1333,7 +1333,8 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1333
1333
|
|
|
1334
1334
|
| 판 | 무엇이 바뀌었나 |
|
|
1335
1335
|
|---|---|
|
|
1336
|
-
| **[2.1.
|
|
1336
|
+
| **[2.1.1](docs/ko/releases/2.1.md#211)** | 전체 검수에서 나온 것 — 한 칸 위를 물으면 `config explain` 이 열쇠를 보였고, 웹훅 주소·Sentry DSN 이 셸 환경 거르기와 가리기를 둘 다 지났고, `kill` 로 strict 를 지나 Jobs 서버를 껐고, Def·Refs 가 EUC-KR 을 못 읽었습니다 · `deel eval` 고침 |
|
|
1337
|
+
| [2.1.0](docs/ko/releases/2.1.md#210) | 맡기면 멈추지 않고, 묻게 하면 diff 를 보여 줍니다 — 「다 됐다」 는 검사가 정합니다(`check`) · 얼마나 해내나는 골든셋으로 잽니다(`deel eval`) · 관리 정책 `"approval"` 로 회사가 승인 바닥을 겁니다 |
|
|
1337
1338
|
| [2.0.3](docs/ko/releases/2.0.md#203) | 2.0.2 는 npm 에 못 나갔습니다 — 이제 GitHub 릴리스는 npm 에 올라간 뒤에만 생깁니다 |
|
|
1338
1339
|
| [2.0.2](docs/ko/releases/2.0.md#202) | 2.0.1 까지 넘겨 둔 것을 다 잡았습니다 — `deel run` 에만 없던 MCP 도구, 끝난 명령의 번호로 가던 `taskkill`, 한 번 넘어지면 세션 내내 못 쓰던 MCP 서버 |
|
|
1339
1340
|
| [2.0.1](docs/ko/releases/2.0.md#201) | 울타리 하나가 안 걸렸습니다 — `sudo -u root bash` 가 `curl … \| bash` 막이를 지나갔습니다 — 그리고 관문 넷이 빨개질 수 없었습니다 |
|
package/README.md
CHANGED
|
@@ -396,7 +396,7 @@ deel --offline
|
|
|
396
396
|
The destination is printed at the top of every session:
|
|
397
397
|
|
|
398
398
|
```
|
|
399
|
-
deel 2.1.
|
|
399
|
+
deel 2.1.1 ⌂ inside
|
|
400
400
|
Sends to this machine 127.0.0.1:11434 ← nowhere else
|
|
401
401
|
```
|
|
402
402
|
|
|
@@ -660,7 +660,7 @@ Korean IME composition, paste, `Ctrl+A/E` and backspace all keep working.
|
|
|
660
660
|
</picture>
|
|
661
661
|
|
|
662
662
|
What you are working on changes **which tools the model is given and how hard it thinks.**
|
|
663
|
-
Cycle with `
|
|
663
|
+
Cycle with `Ctrl+O`, or type the name. (`Shift+Tab` cycles the approval policy.)
|
|
664
664
|
|
|
665
665
|
| Mode | For | Can edit files | Reasoning |
|
|
666
666
|
|---|---|---|---|
|
|
@@ -1415,7 +1415,8 @@ so one run tells you everything.
|
|
|
1415
1415
|
|
|
1416
1416
|
| Version | What changed |
|
|
1417
1417
|
|---|---|
|
|
1418
|
-
| **[2.1.
|
|
1418
|
+
| **[2.1.1](docs/en/releases/2.1.md#211)** | From a full review — `config explain` one level up showed keys, webhook URLs and Sentry DSNs passed both the shell-env filter and masking, `kill` stopped a Jobs server past strict, Def/Refs could not read EUC-KR, and `deel eval` fixes |
|
|
1419
|
+
| [2.1.0](docs/en/releases/2.1.md#210) | Hand it off and it keeps going; make it ask and it shows the diff — a check decides when it is done (`check`), a golden set measures how much gets done (`deel eval`), and `"approval"` in the managed policy lets a company set an approval floor |
|
|
1419
1420
|
| [2.0.3](docs/en/releases/2.0.md#203) | 2.0.2 never reached npm — GitHub releases now appear only after npm has the version |
|
|
1420
1421
|
| [2.0.2](docs/en/releases/2.0.md#202) | Everything 2.0.1 left open, closed — MCP tools missing only from `deel run`, `taskkill` sent to an exited command's pid, MCP servers that stayed down for the session after one crash |
|
|
1421
1422
|
| [2.0.1](docs/en/releases/2.0.md#201) | One fence in 2.0.0 did not hold — `sudo -u root bash` walked through the `curl … \| bash` guard — and four gates could not go red |
|
package/package.json
CHANGED
package/src/acp/serve.js
CHANGED
|
@@ -903,6 +903,7 @@ export async function acp(opts = {}) {
|
|
|
903
903
|
// 완료 검사 (agent/donecheck.js). 에디터에도 무엇을 돌렸고 어떻게 됐는지 적는다.
|
|
904
904
|
case 'check_start':
|
|
905
905
|
말하기(`\n\n_(${옮긴말('check.start', { 판: ev.판, 최대: ev.최대, 명령: ev.명령 })})_\n\n`);
|
|
906
|
+
for (const 줄 of ev.줄임 ?? []) 말하기(`_(${옮긴말('check.clamped', 줄)})_\n\n`);
|
|
906
907
|
break;
|
|
907
908
|
|
|
908
909
|
case 'check':
|
package/src/agent/donecheck.js
CHANGED
|
@@ -59,7 +59,15 @@ export function 검사설정(cfg = {}, 덮을명령 = undefined) {
|
|
|
59
59
|
const 판수 = Number.isInteger(n) && n >= 1 ? Math.min(n, 최대판수) : 기본판수;
|
|
60
60
|
const 초 = Number(cfg?.checkTimeout);
|
|
61
61
|
const 시간 = Number.isFinite(초) && 초 > 0 ? Math.min(기본시간, Math.max(최소시간, Math.round(초 * 1000))) : 기본시간;
|
|
62
|
-
|
|
62
|
+
/*
|
|
63
|
+
* 상한을 넘겨 **줄인 것**은 적어 둔다 — 첫 검사 시작 사건에 실려 화면이 말한다.
|
|
64
|
+
* 느린 통합 검사라 1200초를 적었는데 600초에서 잘려 「시간 초과」 로 판수를 다 쓰면, 사람은
|
|
65
|
+
* 검사가 진짜로 안 되는 줄 안다 (2.1.1 검수).
|
|
66
|
+
*/
|
|
67
|
+
const 줄임 = [];
|
|
68
|
+
if (Number.isInteger(n) && n > 최대판수) 줄임.push({ 칸: 'checkRounds', 준값: n, 쓴값: 최대판수 });
|
|
69
|
+
if (Number.isFinite(초) && 초 * 1000 > 기본시간) 줄임.push({ 칸: 'checkTimeout', 준값: 초, 쓴값: 기본시간 / 1000 });
|
|
70
|
+
return { 명령, 판수, 시간, 줄임 };
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
/**
|
package/src/agent/loop.js
CHANGED
|
@@ -14,6 +14,7 @@ import { compact, shouldCompact, shouldFold, foldToolResults, foldImages, 못박
|
|
|
14
14
|
import { 걸음수, 하위걸음수, 요약길이 } from './budget.js';
|
|
15
15
|
import { Session, estimateTokens } from './session.js';
|
|
16
16
|
import { 최대깊이, 하위모드, 하위요약 } from '../tools/task.js';
|
|
17
|
+
import { 일감인자 } from '../tools/jobs.js';
|
|
17
18
|
import { 찾기 as 에이전트찾기, 할일합치기, 도구줄이기 } from './agents.js';
|
|
18
19
|
import { 프로필찾기, 쓸수있나, 연결만들기, 알릴말, 목록보기 } from './models.js';
|
|
19
20
|
import { allowTemporarily, isOffline } from '../safety/network.js';
|
|
@@ -370,7 +371,8 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
370
371
|
const 명령 = 완료검사.명령;
|
|
371
372
|
const 최대 = 완료검사.판수;
|
|
372
373
|
const args = { command: 명령, description: '완료 검사', timeout: 완료검사.시간 };
|
|
373
|
-
|
|
374
|
+
// 상한에 걸려 줄인 설정은 첫 판에만 싣는다 — 판마다 같은 말을 되풀이하면 소음이다.
|
|
375
|
+
yield { type: 'check_start', 명령, 판, 최대, ...(판 === 1 && 완료검사.줄임?.length ? { 줄임: 완료검사.줄임 } : {}) };
|
|
374
376
|
const 못돌림 = (까닭) => ({ ok: null, 명령, 판, 최대, 까닭 });
|
|
375
377
|
|
|
376
378
|
let 결판 = null;
|
|
@@ -532,10 +534,22 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
532
534
|
if (!부른것.length) return;
|
|
533
535
|
// 결과가 어느 role 에 실리는지도 규격마다 다르다. `role === 'tool'` 로
|
|
534
536
|
// 세면 Anthropic 꼴에서 언제나 0 이라, 지어낸 번호가 전부 겹친다.
|
|
535
|
-
|
|
537
|
+
/*
|
|
538
|
+
* 지어낸 번호는 부름마다 새로 짓고, **이미 쓰인 번호는 건너뛴다.** 한 번만 세면 id 없는 부름 여럿이
|
|
539
|
+
* 같은 번호를 받고, 결과 수로만 세면 모델이 준 `call_1` 이나 접기 뒤 남은 옛 번호와 겹쳐 또 400 이다
|
|
540
|
+
* (2.1.1 검수 · 2차 눈). 빈 글 id 도 없는 것으로 친다.
|
|
541
|
+
*/
|
|
542
|
+
const 쓴번호 = new Set(session.messages.flatMap((m) => 부른것들(m).map((t) => t.id)).filter(Boolean));
|
|
543
|
+
let 이미 = session.messages.filter(도구결과인가).length;
|
|
544
|
+
const 새번호 = () => {
|
|
545
|
+
let id;
|
|
546
|
+
do { id = `call_${++이미}`; } while (쓴번호.has(id));
|
|
547
|
+
쓴번호.add(id);
|
|
548
|
+
return id;
|
|
549
|
+
};
|
|
536
550
|
for (const t of 부른것) {
|
|
537
551
|
session.push(toolMessage(conn.kind, {
|
|
538
|
-
callId: t.id
|
|
552
|
+
callId: t.id || 새번호(),
|
|
539
553
|
name: t.name ?? '?',
|
|
540
554
|
content: 왜,
|
|
541
555
|
}));
|
|
@@ -2036,7 +2050,9 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
2036
2050
|
const 정책이묻게함 = 승인바닥().바닥 !== 'auto';
|
|
2037
2051
|
// Jobs 는 대개 뒤에서 도는 명령의 출력을 **읽는** 호출이다. 끝내기(stop)만 바꾸는 것으로
|
|
2038
2052
|
// 친다 — 읽을 때마다 물으면 사람은 y 만 치다가 정작 물어야 할 것도 그렇게 넘긴다.
|
|
2039
|
-
|
|
2053
|
+
// 끄는가는 **도구가 읽는 자**(일감인자)로 가른다. `stop` 칸만 보면 작은 모델이 보내는
|
|
2054
|
+
// `kill` · `끝내기` 가 안 묻고 서버를 끄고, `stop:"false"` 는 `!!` 로 참이 되어 헛물음이 난다.
|
|
2055
|
+
const 바꾸는것 = (call.name === 'Jobs' ? 일감인자(call.args).끝내기 === true : 바꾸는도구.includes(call.name))
|
|
2040
2056
|
|| call.name.startsWith('mcp__');
|
|
2041
2057
|
const needsOk = (판정.답 === 'allow' && !정책이묻게함) ? false : session.mode === 'strict'
|
|
2042
2058
|
? 바꾸는것
|
package/src/agent/route.js
CHANGED
|
@@ -1381,7 +1381,8 @@ export function route(text, { 승인받을수있나 = true } = {}) {
|
|
|
1381
1381
|
|
|
1382
1382
|
// 1·2등이 붙으면 고르지 않는다. 반반인 것을 억지로 정하면 절반은 틀린다.
|
|
1383
1383
|
const 둘째 = 순위[1]?.[1] ?? 0;
|
|
1384
|
-
|
|
1384
|
+
// 둘째가 없을 수도 있다 — 지금 표로는 늘 둘이 남지만, 모드 표가 바뀌면 여기서 사람 말 한 줄이 죽는다.
|
|
1385
|
+
if (순위[1] && 점수 - 둘째 < 2 && 둘째 >= (문턱[순위[1][0]] ?? 4)) {
|
|
1385
1386
|
return {
|
|
1386
1387
|
mode: null, score: 점수, why: `${으뜸} 과 ${순위[1][0]} 가 비슷함`,
|
|
1387
1388
|
겹침: 겹.겹침, 점수들, 뺀것, 일부러: true,
|
package/src/agent/session.js
CHANGED
|
@@ -218,6 +218,9 @@ function 기본규칙(ctx) {
|
|
|
218
218
|
*/
|
|
219
219
|
const 규칙최대 = 20000;
|
|
220
220
|
|
|
221
|
+
/** 메시지 하나를 센 값 — 메시지 객체가 열쇠다(breakdown 의 「한 번 센 메시지」). 갈아 끼우면 저절로 빠진다. */
|
|
222
|
+
const 메시지셈 = new WeakMap();
|
|
223
|
+
|
|
221
224
|
export class Session {
|
|
222
225
|
#승인 = 'auto';
|
|
223
226
|
|
|
@@ -1154,6 +1157,18 @@ export class Session {
|
|
|
1154
1157
|
*/
|
|
1155
1158
|
let 도구결과수 = 0;
|
|
1156
1159
|
for (const m of this.messages) {
|
|
1160
|
+
/*
|
|
1161
|
+
* 한 번 센 메시지는 다시 안 센다 (2.1.1 검수).
|
|
1162
|
+
*
|
|
1163
|
+
* 상태줄은 일하는 동안 90ms 마다 그려지고 그때마다 여기를 부른다. 64만 토큰 창이면 초당
|
|
1164
|
+
* 스무 번 넘게 대화 전체를 글자 단위로 훑었다. 메시지는 제자리에서 안 고치고 **새 것으로
|
|
1165
|
+
* 갈아 끼우므로**(접기·요약·짝맞추기 다 그렇다) 메시지 자체를 열쇠로 잡아도 낡지 않는다.
|
|
1166
|
+
*/
|
|
1167
|
+
const 센것 = m && typeof m === 'object' ? 메시지셈.get(m) : undefined;
|
|
1168
|
+
if (센것) {
|
|
1169
|
+
if (센것.결과) { files += 센것.t; 도구결과수 += 1; } else history += 센것.t;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1157
1172
|
/*
|
|
1158
1173
|
* 그림은 글자 수로 세지 않는다.
|
|
1159
1174
|
*
|
|
@@ -1170,7 +1185,9 @@ export class Session {
|
|
|
1170
1185
|
// 도구 결과는 규격마다 다른 자리에 온다. `role` 만 보면 Anthropic 에서는
|
|
1171
1186
|
// 도구 결과가 통째로 '대화' 로 세어져서, /context 가 「도구 결과 0 토큰」
|
|
1172
1187
|
// 이라고 적는다 — 무엇을 접어야 할지 보라고 만든 표가 거꾸로 가리킨다.
|
|
1173
|
-
|
|
1188
|
+
const 결과 = 도구결과인가(m);
|
|
1189
|
+
if (m && typeof m === 'object') 메시지셈.set(m, { t, 결과 });
|
|
1190
|
+
if (결과) { files += t; 도구결과수 += 1; } else history += t;
|
|
1174
1191
|
}
|
|
1175
1192
|
|
|
1176
1193
|
// 도구 정의도 매 요청에 실려 나간다. 세는 값이라기보다 '이미 나간 값' 이다.
|
package/src/completion.js
CHANGED
|
@@ -92,6 +92,10 @@ export const 깃발들 = [
|
|
|
92
92
|
{ 이름: '--json', 뜻: '결과를 JSON 으로', en: 'output JSON', 값: false },
|
|
93
93
|
{ 이름: '--check', 뜻: 'run: 끝내기 전에 돌릴 검사 명령', en: 'run: check command to pass before finishing', 값: null },
|
|
94
94
|
{ 이름: '--repeat', 뜻: 'eval: 과제마다 몇 번 돌릴까', en: 'eval: runs per task', 값: null },
|
|
95
|
+
{ 이름: '--init', 뜻: 'eval: 예제 과제를 만든다', en: 'eval: write the example tasks', 값: false },
|
|
96
|
+
{ 이름: '--only', 뜻: 'eval: 고른 과제만 (쉼표) · sbom: 한 가지만', en: 'eval: only these tasks (comma); sbom: only one part', 값: null },
|
|
97
|
+
{ 이름: '--keep', 뜻: 'eval: 작업 폴더를 남긴다', en: 'eval: keep the work folders', 값: false },
|
|
98
|
+
{ 이름: '--timeout', 뜻: 'eval: 과제 하나의 제한 시간(초)', en: 'eval: time limit per task (seconds)', 값: null },
|
|
95
99
|
{ 이름: '--days', 뜻: 'stats 에서 볼 기간(일)', en: 'stats: how many days back', 값: null },
|
|
96
100
|
{ 이름: '--all', 뜻: 'stats 에서 기간을 안 자름', en: 'stats: no time window', 값: false },
|
|
97
101
|
// 값이 파일이라 셸이 파일을 완성해 줘야 한다. 목록으로 못 준다 —
|
package/src/configexplain.js
CHANGED
|
@@ -25,6 +25,7 @@ import { homeDir, 집설정파일인가, 열쇠환경이름 } from './config.js'
|
|
|
25
25
|
import { 믿나, 프로젝트거르기, BOM떼기, 연결칸 } from './safety/trust.js';
|
|
26
26
|
import { 정책읽기, 정책자리 } from './safety/policy.js';
|
|
27
27
|
import { 받기설정 } from './safety/authcmd.js';
|
|
28
|
+
import { 주소가리기 } from './safety/secrets.js';
|
|
28
29
|
|
|
29
30
|
/*
|
|
30
31
|
* 이 PC 프로필에 겹치는 저장소 프로필에서 연결 칸(baseUrl·kind·auth·제공자)을 걷는다.
|
|
@@ -78,7 +79,23 @@ const 환경변수 = {
|
|
|
78
79
|
* 하는 것이 이런 자리에서 제일 흔한 사고다.
|
|
79
80
|
*/
|
|
80
81
|
const 비밀표시 = '(적혀 있음 — 값은 안 보여 줍니다)';
|
|
81
|
-
|
|
82
|
+
/*
|
|
83
|
+
* **끝 칸 이름만** 보면 안 된다. `profiles.사내` 처럼 한 칸 위에서 멈추면 값이 객체이고,
|
|
84
|
+
* 그 안의 apiKey · 인증서 passphrase 가 통째로 그려졌다. 그래서 객체·배열은 안으로 파고들어
|
|
85
|
+
* 칸마다 같은 자로 본다.
|
|
86
|
+
*
|
|
87
|
+
* 주소 꼴의 글(`http://사람:암호@프록시:8080`)은 doctor 와 같은 자(주소가리기)로 가린다 —
|
|
88
|
+
* 호스트·포트는 남는다. 어디로 가는지는 사람이 봐야 한다.
|
|
89
|
+
*/
|
|
90
|
+
const 주소꼴 = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
91
|
+
const 지운값 = (칸, v) => {
|
|
92
|
+
if (v === undefined) return v;
|
|
93
|
+
if (가릴칸.has(칸)) return 비밀표시;
|
|
94
|
+
if (typeof v === 'string') return 주소꼴.test(v.trim()) ? 주소가리기(v) : v;
|
|
95
|
+
if (Array.isArray(v)) return v.map((x) => 지운값('', x));
|
|
96
|
+
if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, 지운값(k, x)]));
|
|
97
|
+
return v;
|
|
98
|
+
};
|
|
82
99
|
|
|
83
100
|
const 값보이기 = (칸, v) => {
|
|
84
101
|
if (v === undefined) return null;
|
package/src/eval/run.js
CHANGED
|
@@ -257,29 +257,76 @@ export function 견주기(지난것, 지금것) {
|
|
|
257
257
|
const 지난표 = new Map((지난것 ?? []).map((x) => [x.과제, x]));
|
|
258
258
|
const 나빠짐 = [];
|
|
259
259
|
const 나아짐 = [];
|
|
260
|
+
// 몇 과제를 견줬나 — 0 이면 「지난번과 같습니다」 라 말할 근거가 없다.
|
|
261
|
+
let 견준 = 0;
|
|
260
262
|
for (const x of 지금것) {
|
|
261
263
|
const 전 = 지난표.get(x.과제);
|
|
262
264
|
if (!전) continue;
|
|
265
|
+
견준 += 1;
|
|
263
266
|
if (비율(x) < 비율(전)) 나빠짐.push(x.과제);
|
|
264
267
|
else if (비율(x) > 비율(전)) 나아짐.push(x.과제);
|
|
265
268
|
}
|
|
266
|
-
return { 나빠짐,
|
|
269
|
+
return { 나빠짐, 나아짐, 견준 };
|
|
267
270
|
}
|
|
268
271
|
|
|
269
|
-
/**
|
|
270
|
-
|
|
272
|
+
/**
|
|
273
|
+
* 제일 새 결과 파일. 깨졌거나 과제별이 없는 것(손으로 적은 `{}` 따위)은 건너뛴다 — 견줄 것이 없다.
|
|
274
|
+
*
|
|
275
|
+
* `전체만` 이면 `--only` 로 일부만 돈 결과(`only` 칸이 있는 것)도 건너뛴다. 안 그러면 `--only a`
|
|
276
|
+
* 한 번 뒤의 전체 판이 과제 a 하나만 견주고, 나머지가 떨어져도 「지난번과 같습니다」 가 나온다
|
|
277
|
+
* (2.1.1 검수). 일부만 도는 판은 아무것과 견줘도 된다 — 견주기 가 지금 돈 과제만 본다.
|
|
278
|
+
*/
|
|
279
|
+
export function 지난결과(폴더, { 전체만 = false, 이름들 = null } = {}) {
|
|
271
280
|
const 자리 = join(폴더, 결과폴더);
|
|
272
|
-
|
|
273
|
-
|
|
281
|
+
let 파일들;
|
|
282
|
+
// 손으로 만든 `.results` **파일**이면 readdirSync 가 던진다 — 견줄 것이 없을 뿐이다.
|
|
283
|
+
try { 파일들 = readdirSync(자리).filter((f) => /^\d{8}-\d{6}.*\.json$/.test(f)).sort(); } catch { return null; }
|
|
274
284
|
for (const f of 파일들.reverse()) {
|
|
275
285
|
try {
|
|
276
286
|
const 값 = JSON.parse(readFileSync(join(자리, f), 'utf8'));
|
|
277
|
-
if (Array.isArray(값?.과제별))
|
|
287
|
+
if (!Array.isArray(값?.과제별)) continue;
|
|
288
|
+
if (전체만 && 값.only) continue;
|
|
289
|
+
// 일부만 도는 판은 **지금 과제가 든** 결과까지 거슬러 찾는다 — `--only a` 뒤의 `--only b` 가 a 만 든 판과
|
|
290
|
+
// 견주면 겹치는 과제가 없는데도 「지난번과 같습니다」 가 나온다 (2.1.1 2차 눈).
|
|
291
|
+
if (이름들 && !값.과제별.some((x) => 이름들.includes(x?.과제))) continue;
|
|
292
|
+
return { 파일: f, 값 };
|
|
278
293
|
} catch { /* 깨진 것은 건너뛴다 */ }
|
|
279
294
|
}
|
|
280
295
|
return null;
|
|
281
296
|
}
|
|
282
297
|
|
|
298
|
+
/**
|
|
299
|
+
* `--only` 로 과제를 고른다. 쉼표로 여럿.
|
|
300
|
+
*
|
|
301
|
+
* 이름이 **딱 맞는** 것이 있으면 그것만 고른다 — 이름 일부로만 고르면 `task-1` 이 `task-10` ·
|
|
302
|
+
* `task-11` 까지 불러 엉뚱한 과제에 시간을 쓴다 (2.1.1 검수). 딱 맞는 것이 없으면 예전처럼
|
|
303
|
+
* 이름 일부로 고른다(`--only 03` · `--only a-`).
|
|
304
|
+
*/
|
|
305
|
+
export function 과제고르기(과제들, only) {
|
|
306
|
+
const 고를것 = String(only ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
307
|
+
return 과제들.filter((x) => 고를것.some((k) => (과제들.some((y) => y.이름 === k) ? x.이름 === k : x.이름.includes(k))));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 과제 한 판을 돌리되 **터져도 그 과제만** 실패로 센다.
|
|
312
|
+
*
|
|
313
|
+
* 한번돌리기 는 임시 폴더를 치우는 finally 만 있고 catch 가 없다. 정답 복사가 권한·자리 부족으로
|
|
314
|
+
* 터지면 그 예외가 runEval 까지 올라가 남은 과제를 다 버리고 결과 파일도 안 남겼다 (2.1.1 검수).
|
|
315
|
+
* `돌리개` 는 검사가 터지는 판을 만들려고 갈아 끼우는 자리다.
|
|
316
|
+
*/
|
|
317
|
+
export async function 판돌리기(과제, 옵션 = {}, { 돌리개 = 한번돌리기 } = {}) {
|
|
318
|
+
try {
|
|
319
|
+
return await 돌리개(과제, 옵션);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
const 까닭 = String(err?.message ?? err);
|
|
322
|
+
return {
|
|
323
|
+
과제: 과제.이름, 판: 옵션.판 ?? 1, 통과: false, 초: 0,
|
|
324
|
+
deel: { code: null, reason: 'error', steps: 0, tools: 0, usage: { in: 0, out: 0 }, model: null },
|
|
325
|
+
검사: { code: null, 꼬리: 글(`평가 도중 터졌습니다: ${까닭}`, `The run itself failed: ${까닭}`) },
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
283
330
|
/**
|
|
284
331
|
* 결과 파일 이름. **UTC** 로 적고 `Z` 를 붙인다 — 지역 시각이면 시차가 다른 PC 의 결과가 한
|
|
285
332
|
* 폴더에 섞일 때(저장소에 올려 같이 쓰는 과제 모음) 이름 순서가 시각 순서와 어긋나고,
|
|
@@ -336,10 +383,7 @@ export async function runEval({ 폴더 = null, init = false, repeat = 1, keep =
|
|
|
336
383
|
}
|
|
337
384
|
|
|
338
385
|
let 과제들 = 과제읽기(고른폴더);
|
|
339
|
-
if (only)
|
|
340
|
-
const 고를것 = String(only).split(',').map((s) => s.trim()).filter(Boolean);
|
|
341
|
-
과제들 = 과제들.filter((x) => 고를것.some((k) => x.이름.includes(k)));
|
|
342
|
-
}
|
|
386
|
+
if (only) 과제들 = 과제고르기(과제들, only);
|
|
343
387
|
if (!과제들.length) {
|
|
344
388
|
return 못돌림(1, 글(`돌릴 과제가 없습니다 — ${보일} 밑에 task.json 이 든 폴더가 없습니다`, `Nothing to run — no folder with a task.json under ${보일}`));
|
|
345
389
|
}
|
|
@@ -373,7 +417,7 @@ export async function runEval({ 폴더 = null, init = false, repeat = 1, keep =
|
|
|
373
417
|
if (판 === 1) 말하기(` ${c.yellow('⊘')} ${과제.이름} ${c.gray(과제.탈)}`);
|
|
374
418
|
continue;
|
|
375
419
|
}
|
|
376
|
-
const r = await
|
|
420
|
+
const r = await 판돌리기(과제, { 판, 깃발들, 남김: keep, 시간초 });
|
|
377
421
|
결과들.push(r);
|
|
378
422
|
const 옆 = [`${r.초}${글('초', 's')}`];
|
|
379
423
|
if (r.deel.tools) 옆.push(글(`도구 ${r.deel.tools}회`, `${r.deel.tools} tool calls`));
|
|
@@ -391,7 +435,7 @@ export async function runEval({ 폴더 = null, init = false, repeat = 1, keep =
|
|
|
391
435
|
const 판수 = 결과들.length;
|
|
392
436
|
const 통과수 = 결과들.filter((r) => r.통과).length;
|
|
393
437
|
const 모델 = 결과들.find((r) => r.deel?.model)?.deel.model ?? null;
|
|
394
|
-
const 지난 = 지난결과(
|
|
438
|
+
const 지난 = 지난결과(고른폴더, { 전체만: !only, 이름들: only ? 과제들.map((x) => x.이름) : null });
|
|
395
439
|
const 견줌 = 견주기(지난?.값?.과제별, 묶음);
|
|
396
440
|
const 초 = Math.round((Date.now() - t0) / 1000);
|
|
397
441
|
const 토큰 = 결과들.reduce((a, r) => ({ in: a.in + (r.deel?.usage?.in ?? 0), out: a.out + (r.deel?.usage?.out ?? 0) }), { in: 0, out: 0 });
|
|
@@ -401,6 +445,8 @@ export async function runEval({ 폴더 = null, init = false, repeat = 1, keep =
|
|
|
401
445
|
model: 모델,
|
|
402
446
|
folder: 고른폴더,
|
|
403
447
|
repeat: 되풀이,
|
|
448
|
+
// 일부만 돈 판이라는 표시 — 다음 전체 판이 이것을 「지난번」 으로 안 친다 (지난결과 의 전체만).
|
|
449
|
+
...(only ? { only: String(only) } : {}),
|
|
404
450
|
passed: 통과수,
|
|
405
451
|
runs: 판수,
|
|
406
452
|
rate: 판수 ? Math.round((통과수 / 판수) * 1000) / 10 : 0,
|
|
@@ -411,11 +457,16 @@ export async function runEval({ 폴더 = null, init = false, repeat = 1, keep =
|
|
|
411
457
|
...(지난 ? { 견줌: { 지난파일: 지난.파일, ...견줌 } } : {}),
|
|
412
458
|
};
|
|
413
459
|
let 남긴곳 = null;
|
|
460
|
+
let 못남긴까닭 = null;
|
|
414
461
|
try {
|
|
415
462
|
mkdirSync(join(고른폴더, 결과폴더), { recursive: true });
|
|
416
463
|
남긴곳 = join(고른폴더, 결과폴더, `${시각이름()}.json`);
|
|
417
464
|
writeFileSync(남긴곳, JSON.stringify(기록, null, 2) + '\n');
|
|
418
|
-
} catch {
|
|
465
|
+
} catch (err) {
|
|
466
|
+
// 조용히 삼키면 사람은 다음 판에 견줄 것이 있는 줄 안다. 채점은 그대로 끝낸다.
|
|
467
|
+
남긴곳 = null;
|
|
468
|
+
못남긴까닭 = String(err?.code ?? err?.message ?? err);
|
|
469
|
+
}
|
|
419
470
|
|
|
420
471
|
if (json) process.stdout.write(JSON.stringify(기록) + '\n');
|
|
421
472
|
말하기('');
|
|
@@ -424,9 +475,15 @@ export async function runEval({ 폴더 = null, init = false, repeat = 1, keep =
|
|
|
424
475
|
if (지난) {
|
|
425
476
|
if (견줌.나빠짐.length) 말하기(` ${c.red('▼')} ${글('지난번보다 나빠짐:', 'Worse than last time:')} ${견줌.나빠짐.join(' · ')}`);
|
|
426
477
|
if (견줌.나아짐.length) 말하기(` ${c.green('▲')} ${글('지난번보다 나아짐:', 'Better than last time:')} ${견줌.나아짐.join(' · ')}`);
|
|
427
|
-
if (!견줌.나빠짐.length && !견줌.나아짐.length) 말하기(` ${c.gray(글(`지난번(${지난.파일})과 같습니다`, `Same as last time (${지난.파일})`))}`);
|
|
478
|
+
if (견줌.견준 && !견줌.나빠짐.length && !견줌.나아짐.length) 말하기(` ${c.gray(글(`지난번(${지난.파일})과 같습니다`, `Same as last time (${지난.파일})`))}`);
|
|
428
479
|
}
|
|
429
480
|
if (남긴곳) 말하기(` ${c.gray(글(`결과: ${relative(뿌리, 남긴곳)}`, `Results: ${relative(뿌리, 남긴곳)}`))}`);
|
|
481
|
+
else if (못남긴까닭) {
|
|
482
|
+
const 알림 = 글(`결과 파일을 못 남겼습니다 (${못남긴까닭}) — 다음 판은 이번 것과 못 견줍니다`,
|
|
483
|
+
`Could not save the results file (${못남긴까닭}) — the next run cannot compare with this one`);
|
|
484
|
+
if (json) process.stderr.write(`${알림}\n`);
|
|
485
|
+
else 말하기(` ${c.yellow('⚠')} ${알림}`);
|
|
486
|
+
}
|
|
430
487
|
말하기('');
|
|
431
488
|
return 통과수 === 판수 ? 0 : 1;
|
|
432
489
|
}
|
package/src/eval/starter.js
CHANGED
|
@@ -58,7 +58,7 @@ export const 예제과제 = {
|
|
|
58
58
|
'src/admin.js': "import { getUser } from './user.js';\n\nexport function isAdmin(id) {\n return getUser(id).id === 0;\n}\n",
|
|
59
59
|
},
|
|
60
60
|
golden: {
|
|
61
|
-
'golden-check.mjs': "import assert from 'node:assert/strict';\nimport { readdirSync, readFileSync } from 'node:fs';\nimport { fetchUser } from './src/user.js';\nimport { profileName } from './src/profile.js';\nimport { isAdmin } from './src/admin.js';\n\nassert.deepEqual(fetchUser(3), { id: 3, name: 'user3' });\nassert.equal(profileName(3), 'user3');\nassert.equal(isAdmin(0), true);\nassert.equal(isAdmin(1), false);\nconst
|
|
61
|
+
'golden-check.mjs': "import assert from 'node:assert/strict';\nimport { readdirSync, readFileSync } from 'node:fs';\nimport { fetchUser } from './src/user.js';\nimport { profileName } from './src/profile.js';\nimport { isAdmin } from './src/admin.js';\n\nassert.deepEqual(fetchUser(3), { id: 3, name: 'user3' });\nassert.equal(profileName(3), 'user3');\nassert.equal(isAdmin(0), true);\nassert.equal(isAdmin(1), false);\nconst 파일들 = (d) => readdirSync(d, { withFileTypes: true }).flatMap((e) => (e.isDirectory() ? 파일들(d + '/' + e.name) : e.isFile() ? [d + '/' + e.name] : []));\nconst 남은 = 파일들('src').filter((f) => readFileSync(f, 'utf8').includes('getUser'));\nassert.deepEqual(남은, [], '옛 이름이 남은 파일: ' + 남은.join(', '));\nconsole.log('ok');\n",
|
|
62
62
|
},
|
|
63
63
|
},
|
|
64
64
|
'04-빨간검사고치기': {
|
package/src/i18n/en.js
CHANGED
|
@@ -324,6 +324,7 @@ export const en = {
|
|
|
324
324
|
'approve.strict.short': 'always',
|
|
325
325
|
'approve.strict.line': 'asks before every file edit and every command',
|
|
326
326
|
'approve.newFile': 'new file',
|
|
327
|
+
'approve.noChange': 'no change — same content as now',
|
|
327
328
|
'approve.locked': 'A managed policy keeps approval at {바닥} or stricter — {곳}',
|
|
328
329
|
'approve.lockedUnknown': 'The managed policy\'s approval value "{값}" is unknown, so it counts as strict, the narrowest — {곳}',
|
|
329
330
|
'approve.noAlways': 'A managed policy sets approval, so "don\'t ask again" is not offered',
|
|
@@ -518,6 +519,7 @@ export const en = {
|
|
|
518
519
|
'ev.hookBlock': 'A hook blocked this',
|
|
519
520
|
'ev.nudgeRead': 'It was about to finish having only read things — pushed back once',
|
|
520
521
|
'check.start': 'Completion check ({판}/{최대}) — {명령}',
|
|
522
|
+
'check.clamped': '{칸} {준값} is over the limit — using {쓴값}',
|
|
521
523
|
'check.pass': 'Completion check passed — {명령}',
|
|
522
524
|
'check.fail': 'Completion check failed ({판}/{최대}) — {요약}',
|
|
523
525
|
'check.retry': 'Sending the failure back to fix',
|
package/src/i18n/ja.js
CHANGED
|
@@ -291,6 +291,7 @@ export const ja = {
|
|
|
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,6 +477,7 @@ export const ja = {
|
|
|
476
477
|
'ev.hookBlock': 'フックが止めました',
|
|
477
478
|
'ev.nudgeRead': '読むだけで終えようとしたので、一度押し戻しました',
|
|
478
479
|
'check.start': '完了チェック ({판}/{최대}) — {명령}',
|
|
480
|
+
'check.clamped': '{칸} {준값} は上限を超えるため {쓴값} にしました',
|
|
479
481
|
'check.pass': '完了チェック合格 — {명령}',
|
|
480
482
|
'check.fail': '完了チェック失敗 ({판}/{최대}) — {요약}',
|
|
481
483
|
'check.retry': '失敗内容を返して直させます',
|
package/src/i18n/ko.js
CHANGED
|
@@ -313,6 +313,7 @@ export const ko = {
|
|
|
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,6 +551,7 @@ export const ko = {
|
|
|
550
551
|
'ev.hookBlock': '훅이 막았습니다',
|
|
551
552
|
'ev.nudgeRead': '읽기만 하고 끝내려고 해서 한 번 되밀었습니다',
|
|
552
553
|
'check.start': '완료 검사 ({판}/{최대}) — {명령}',
|
|
554
|
+
'check.clamped': '{칸} {준값} 은 상한을 넘어 {쓴값} 으로 줄였습니다',
|
|
553
555
|
'check.pass': '완료 검사 통과 — {명령}',
|
|
554
556
|
'check.fail': '완료 검사 실패 ({판}/{최대}) — {요약}',
|
|
555
557
|
'check.retry': '실패 내용을 돌려주고 다시 고치게 합니다',
|
package/src/i18n/zh.js
CHANGED
|
@@ -291,6 +291,7 @@ export const zh = {
|
|
|
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,6 +477,7 @@ export const zh = {
|
|
|
476
477
|
'ev.hookBlock': '钩子拦下了这一步',
|
|
477
478
|
'ev.nudgeRead': '只看不动就要收尾,已经推回去一次',
|
|
478
479
|
'check.start': '完成检查 ({판}/{최대}) — {명령}',
|
|
480
|
+
'check.clamped': '{칸} {준값} 超过上限,改用 {쓴값}',
|
|
479
481
|
'check.pass': '完成检查通过 — {명령}',
|
|
480
482
|
'check.fail': '完成检查失败 ({판}/{최대}) — {요약}',
|
|
481
483
|
'check.retry': '把失败内容交回去重新修',
|
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
|
@@ -929,6 +929,7 @@ export async function runOnce(opts = {}) {
|
|
|
929
929
|
|
|
930
930
|
case 'check_start':
|
|
931
931
|
곁(` ${c.cyan('⧗')} ${c.gray(옮긴말('check.start', { 판: ev.판, 최대: ev.최대, 명령: ev.명령 }))}`);
|
|
932
|
+
for (const 줄 of ev.줄임 ?? []) 곁(` ${c.yellow('⚠')} ${c.gray(옮긴말('check.clamped', 줄))}`);
|
|
932
933
|
break;
|
|
933
934
|
|
|
934
935
|
case 'check':
|
|
@@ -936,7 +937,8 @@ export async function runOnce(opts = {}) {
|
|
|
936
937
|
else if (ev.ok === false) {
|
|
937
938
|
곁(` ${c.red('✗')} ${c.gray(옮긴말('check.fail', { 판: ev.판, 최대: ev.최대, 요약: ev.요약 }))}`);
|
|
938
939
|
// 실패 출력의 끝 몇 줄 — 배치 로그를 보는 사람이 무엇이 틀렸는지 여기서 안다.
|
|
939
|
-
|
|
940
|
+
// 빈 줄을 **먼저** 거르고 자른다 — 거꾸로 하면 원인 뒤 빈 줄 여럿이 8칸을 다 먹는다 (repl.js 와 같은 차례).
|
|
941
|
+
for (const 줄 of String(ev.꼬리 ?? '').trim().split('\n').filter((l) => l.trim()).slice(-8)) 곁(` ${c.gray(줄)}`);
|
|
940
942
|
곁(` ${c.gray(ev.판 < ev.최대 ? 옮긴말('check.retry') : 옮긴말('check.gaveUp', { 판: ev.판 }))}`);
|
|
941
943
|
} else 곁(` ${c.yellow('⊘')} ${c.gray(옮긴말('check.skipped', { 까닭: ev.까닭 }))}`);
|
|
942
944
|
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.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/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;
|