deel-local-cli 2.1.1 → 2.1.3
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 +6 -4
- package/README.md +6 -4
- package/bin/deel.js +64 -43
- package/package.json +1 -1
- package/src/acp/serve.js +89 -3
- package/src/agent/donecheck.js +19 -4
- package/src/agent/loop.js +44 -4
- package/src/agent/route.js +67 -1
- package/src/agent/session.js +35 -6
- package/src/backend/adapter.js +26 -4
- package/src/backend/http.js +2 -23
- package/src/backend/loosejson.js +109 -0
- package/src/backend/mcp.js +601 -41
- package/src/backend/probe.js +17 -3
- package/src/backend/textcalls.js +196 -0
- package/src/commands.js +26 -37
- package/src/completion.js +3 -0
- package/src/eval/run.js +73 -14
- package/src/exit.js +118 -0
- package/src/i18n/en.js +8 -1
- package/src/i18n/ja.js +8 -1
- package/src/i18n/ko.js +8 -1
- package/src/i18n/zh.js +8 -1
- package/src/lsp/diag.js +2 -1
- package/src/oneshot.js +163 -93
- package/src/repl.js +14 -2
- package/src/runlog.js +99 -0
- package/src/safety/authcmd.js +68 -17
- package/src/safety/gitsnap.js +286 -0
- package/src/safety/undo.js +107 -8
- package/src/safety/which.js +37 -0
- package/src/setup.js +3 -5
- package/src/slash.js +103 -0
- package/src/tools/importgraph.js +107 -0
- package/src/tools/index.js +24 -1
- package/src/tools/outline.js +21 -3
- package/src/tools/verify.js +103 -45
- package/src/ui/banner.js +8 -1
package/src/backend/probe.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
assistantMessage, toolMessage,
|
|
14
14
|
} from './adapter.js';
|
|
15
15
|
import { 벤더 } from './toolfit.js';
|
|
16
|
+
import { 글속부름 } from './textcalls.js';
|
|
16
17
|
import { 세션이름짓기, 기본카드 } from './wire.js';
|
|
17
18
|
import { 말 } from '../i18n/index.js';
|
|
18
19
|
|
|
@@ -446,18 +447,31 @@ export async function probe(conn, onStep = () => {}) {
|
|
|
446
447
|
* 판정한다 — 위 머리말과 거꾸로 된 틀림이다.
|
|
447
448
|
*/
|
|
448
449
|
const 모양틀림 = 잘린인자 && tcalls[0]?.argsCut === false;
|
|
450
|
+
/*
|
|
451
|
+
* 모양만 틀린 인자는 이제 deel 이 느슨하게 읽는다 (2.1.3 · adapter.js 인자읽기). 읽혔으니 「거절당한다」
|
|
452
|
+
* 는 아니지만, 이 모델이 규격 밖 JSON 을 쓴다는 것은 남긴다 — 다른 도구는 그걸 거절한다.
|
|
453
|
+
*/
|
|
454
|
+
const 고쳐읽음 = gotCall && tcalls[0]?.argsRepaired === true;
|
|
455
|
+
/*
|
|
456
|
+
* 도구를 **글로** 부르는 모델 (2.1.3 · backend/textcalls.js). 서버가 그 모델의 부름 꼴을 몰라
|
|
457
|
+
* tool_calls 는 비었는데 답 글에 <tool_call> 이 실렸다. 여태는 「글로만 답합니다」 로 적어 도구를
|
|
458
|
+
* 못 쓰는 모델처럼 보였다. deel 은 그 글을 알아듣는다 — 경고로 두고, 서버의 도구 틀을 켜라고 적는다.
|
|
459
|
+
*/
|
|
460
|
+
const 글로부름 = !gotCall && tl.ok ? 글속부름(읽기(tl).content, [READ_TOOL.function.name]) : null;
|
|
449
461
|
const 원문에있나 = 잘린인자 && !모양틀림 && String(tcalls[0]?.rawArgs ?? '').includes('config');
|
|
450
462
|
add({
|
|
451
463
|
id: 'tools',
|
|
452
464
|
label: '도구 호출',
|
|
453
|
-
status: gotCall ? (argOk || 원문에있나 ? 'ok' : 'warn') : 'no',
|
|
465
|
+
status: gotCall ? (고쳐읽음 ? 'warn' : argOk || 원문에있나 ? 'ok' : 'warn') : 글로부름 ? 'warn' : 'no',
|
|
454
466
|
detail: gotCall
|
|
455
|
-
? `${tcalls[0]?.name} 호출됨${
|
|
467
|
+
? `${tcalls[0]?.name} 호출됨${고쳐읽음 ? ' — 인자 JSON 모양이 틀려(홑따옴표·끝 쉼표 같은 것) deel 이 느슨하게 읽었습니다'
|
|
468
|
+
: argOk ? ''
|
|
456
469
|
: 원문에있나 ? ` — 인자가 우리 상한(${도구상한}토큰)에 잘렸을 뿐, 값은 제대로 왔습니다`
|
|
457
470
|
: 모양틀림 ? ' — 인자 JSON 모양이 틀렸습니다 (홑따옴표·끝 쉼표 같은 것 — 편집 도구가 자주 거절당합니다)'
|
|
458
471
|
: 잘린인자 ? ' — 인자 JSON 이 잘려 왔습니다 (상한을 올려 다시 보세요)'
|
|
459
472
|
: ' — 인자가 부정확, 편집 신뢰성 작업이 더 필요합니다'}`
|
|
460
|
-
:
|
|
473
|
+
: 글로부름 ? '도구를 글로 부릅니다(<tool_call> 같은 꼴) — deel 이 알아듣지만, 서버의 도구 틀(채팅 템플릿)을 켜면 더 낫습니다'
|
|
474
|
+
: tl.ok ? '도구를 안 부르고 글로만 답합니다' : serverMessage(tl),
|
|
461
475
|
ms: tl.ms,
|
|
462
476
|
});
|
|
463
477
|
facts.tools = gotCall;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 글 속에 적힌 도구 부름을 건진다 (2.1.3).
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 작은 모델은 도구를 **글로** 부른다. 서버가 그 모델의 부름 꼴을 모르거나(채팅 틀이 없는 GGUF ·
|
|
7
|
+
* 오래된 Ollama 틀 · 사내 게이트웨이의 변환 누락), 모델이 배운 꼴과 서버가 기다리는 꼴이 다르면
|
|
8
|
+
* `tool_calls` 는 비어 오고 답 글에 이런 것이 실린다.
|
|
9
|
+
*
|
|
10
|
+
* <tool_call>{"name": "Read", "arguments": {"file_path": "a.js"}}</tool_call> Hermes · Qwen
|
|
11
|
+
* <function=Read>{"file_path": "a.js"}</function> Llama 3.1
|
|
12
|
+
* <tool_call><function=Read><parameter=file_path>a.js</parameter></function>… Qwen3-Coder
|
|
13
|
+
* [TOOL_CALLS] [{"name": "Read", "arguments": {...}}] Mistral
|
|
14
|
+
* ```json {"name": "Read", "arguments": {...}} ``` 표 없는 것
|
|
15
|
+
*
|
|
16
|
+
* 여태 이것을 「도구를 안 부르고 글로만 답했다」 로 읽고 턴을 끝냈다. 모델은 일을 하려 했는데 우리가
|
|
17
|
+
* 못 알아들은 것이다. 이제 **우리가 내준 도구 이름**으로 적힌 것만 건져 진짜 부름으로 돌린다.
|
|
18
|
+
* 건진 부름도 네이티브 부름과 같은 관문(모드 · 승인 · 정책 바닥)을 지난다 — 건졌다고 덜 묻지 않는다.
|
|
19
|
+
*
|
|
20
|
+
* ── 무엇을 안 건지나 ────────────────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* · 내준 적 없는 이름. 글 속 예시(`{"name": "search", ...}`)를 부름으로 읽으면 안 된다. 한 덩이에
|
|
23
|
+
* 모르는 이름이 하나라도 섞이면 그 덩이는 통째로 안 건진다.
|
|
24
|
+
* · 울타리(```) 안에 든 표는 예시다 — 형식을 설명하는 답이 그렇게 적는다.
|
|
25
|
+
* · 울타리나 맨 JSON 꼴은 **답이 거의 그것 하나뿐일 때만** 건진다. 설명 글 사이에 든 예시를
|
|
26
|
+
* 부름으로 읽으면, 쓰기·명령 도구는 사람이 시키지 않은 일을 한다. 울타리가 둘 이상이면 예시를
|
|
27
|
+
* 늘어놓은 글로 본다. 표(<tool_call> 따위)가 붙은 꼴은 모델이 부르려고 쓴 것이라 어디에 있든 건진다.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { 읽어보기 } from './loosejson.js';
|
|
31
|
+
|
|
32
|
+
const 객체인가 = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
* 이름과 인자를 한 덩이에서 뽑는다. 모델마다 칸 이름이 다르다 —
|
|
36
|
+
* `arguments`(OpenAI · Hermes) · `parameters`(Llama) · `args` · `input`(Anthropic 을 흉내 낸 것).
|
|
37
|
+
* `function: {name, arguments}` 로 한 겹 싸서 오기도 한다.
|
|
38
|
+
*/
|
|
39
|
+
function 부름꼴(v) {
|
|
40
|
+
if (!객체인가(v)) return null;
|
|
41
|
+
const 속 = 객체인가(v.function) ? v.function : v;
|
|
42
|
+
const 이름 = typeof 속.name === 'string' ? 속.name : null;
|
|
43
|
+
if (!이름) return null;
|
|
44
|
+
let 인자 = 속.arguments ?? 속.parameters ?? 속.args ?? 속.input ?? {};
|
|
45
|
+
if (typeof 인자 === 'string') 인자 = 읽어보기(인자.trim() || '{}');
|
|
46
|
+
if (!객체인가(인자)) return null;
|
|
47
|
+
return { 이름, 인자 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 한 덩이 JSON 이 부름 하나거나 부름 여럿의 배열이면 [{이름, 인자}], 아니면 null. */
|
|
51
|
+
function 부름들로(v) {
|
|
52
|
+
const 목록 = Array.isArray(v) ? v : [v];
|
|
53
|
+
if (!목록.length) return null;
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const x of 목록) {
|
|
56
|
+
const 하나 = 부름꼴(x);
|
|
57
|
+
if (!하나) return null;
|
|
58
|
+
out.push(하나);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* Qwen3-Coder 꼴: <function=이름> <parameter=칸>값</parameter> … </function>. 값은 JSON 이면 JSON 으로.
|
|
65
|
+
*
|
|
66
|
+
* 수는 **글자 그대로 되돌아올 때만** 수로 읽는다. 칸 값은 날글이라 `01234`(번호) · `1.50`(판) 도 온다 —
|
|
67
|
+
* 수로 읽으면 앞 0 과 끝 0 이 사라진 다른 값이 도구로 간다(2차 눈이 짚었다).
|
|
68
|
+
*/
|
|
69
|
+
function 칸꼴(몸) {
|
|
70
|
+
const 인자 = {};
|
|
71
|
+
const 칸무늬 = /<parameter=([^>\s]+)>\s*([\s\S]*?)\s*<\/parameter>/g;
|
|
72
|
+
let m;
|
|
73
|
+
let 있었나 = false;
|
|
74
|
+
while ((m = 칸무늬.exec(몸))) {
|
|
75
|
+
있었나 = true;
|
|
76
|
+
const 날것 = m[2];
|
|
77
|
+
const t = 날것.trim();
|
|
78
|
+
let 읽은;
|
|
79
|
+
if (/^[[{"]|^(?:true|false|null|True|False|None)$/.test(t)) 읽은 = 읽어보기(t);
|
|
80
|
+
else if (/^-?\d/.test(t) && String(Number(t)) === t) 읽은 = Number(t);
|
|
81
|
+
인자[m[1]] = 읽은 === undefined ? 날것 : 읽은;
|
|
82
|
+
}
|
|
83
|
+
return 있었나 ? 인자 : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
* <function=이름> 덩이들. 한 <tool_call> 안에 여럿이 올 수 있다 — 한 무늬로 통째로 잡으면 앞 것의 끝 표까지
|
|
88
|
+
* 몸으로 삼켜 뒤 것 인자로 부름 **하나**가 됐다. 다음 <function= 앞에서도 끊는다.
|
|
89
|
+
*/
|
|
90
|
+
function 함수덩이들(몸) {
|
|
91
|
+
const out = [];
|
|
92
|
+
const 무늬 = /<function=([^>\s]+)>\s*([\s\S]*?)\s*(?:<\/function>|(?=<function=)|$)/g;
|
|
93
|
+
let m;
|
|
94
|
+
while ((m = 무늬.exec(몸))) {
|
|
95
|
+
const 인자 = 칸꼴(m[2]) ?? (m[2].trim() ? 읽어보기(m[2].trim()) : {});
|
|
96
|
+
if (!객체인가(인자)) return null;
|
|
97
|
+
out.push({ 이름: m[1], 인자 });
|
|
98
|
+
}
|
|
99
|
+
return out.length ? out : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 글 속 부름을 건진다.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} 글 모델이 낸 답 글
|
|
106
|
+
* @param {string[]} 이름들 이번에 내준 도구 이름들
|
|
107
|
+
* @returns {{부름: Array<{name, args}>, 남은글: string} | null} 건진 것이 없으면 null
|
|
108
|
+
*/
|
|
109
|
+
export function 글속부름(글, 이름들) {
|
|
110
|
+
const s = String(글 ?? '');
|
|
111
|
+
if (!s.trim() || !이름들?.length) return null;
|
|
112
|
+
// 이름은 대소문자를 안 가려 맞추고, 돌려줄 때는 우리 이름으로 적는다(read → Read).
|
|
113
|
+
const 우리이름 = new Map(이름들.map((n) => [String(n).toLowerCase(), String(n)]));
|
|
114
|
+
const 맞춤 = (n) => 우리이름.get(String(n ?? '').trim().toLowerCase()) ?? null;
|
|
115
|
+
const 부름 = [];
|
|
116
|
+
const 뗄곳 = [];
|
|
117
|
+
const 담기 = (목록, 시작, 끝) => {
|
|
118
|
+
const 맞춘 = [];
|
|
119
|
+
for (const x of 목록) {
|
|
120
|
+
const 이름 = 맞춤(x.이름);
|
|
121
|
+
if (!이름) return false;
|
|
122
|
+
맞춘.push({ name: 이름, args: x.인자 });
|
|
123
|
+
}
|
|
124
|
+
부름.push(...맞춘);
|
|
125
|
+
뗄곳.push([시작, 끝]);
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// ── 1. 표가 붙은 꼴 — 울타리 밖이면 어디에 있든 건진다 ──────────────────
|
|
130
|
+
// 울타리(```) 안의 표는 형식을 보여 주는 예시다. 건지면 설명하다 적은 명령이 돈다.
|
|
131
|
+
// 안 닫힌 울타리는 글 끝까지다 — 길이 제한으로 잘린 답이 예시를 열어 둔 채 끝난다.
|
|
132
|
+
const 울타리자리 = [...s.matchAll(/```[\s\S]*?(?:```|$)/g)].map((u) => [u.index, u.index + u[0].length]);
|
|
133
|
+
const 울안 = (i) => 울타리자리.some(([a, b]) => i >= a && i < b);
|
|
134
|
+
// <tool_call> … </tool_call> (끝 표 없이 글이 끝나도 받는다 — 서버가 멈춤 낱말로 끝 표를 떼는 일이 있다).
|
|
135
|
+
// 몸은 다음 <tool_call> 을 넘지 않는다 — 말로 꺼낸 표 하나가 뒤따르는 진짜 부름까지 몸으로 삼켰다.
|
|
136
|
+
const 표무늬 = /<tool_call>\s*((?:(?!<tool_call>)[\s\S])*?)\s*(?:<\/tool_call>|(?=<tool_call>)|$)/g;
|
|
137
|
+
let m;
|
|
138
|
+
while ((m = 표무늬.exec(s))) {
|
|
139
|
+
const 몸 = m[1];
|
|
140
|
+
if (!몸.trim() || 울안(m.index)) continue;
|
|
141
|
+
if (몸.trim().startsWith('<function=')) {
|
|
142
|
+
const 목록 = 함수덩이들(몸);
|
|
143
|
+
if (목록) 담기(목록, m.index, m.index + m[0].length);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const 목록 = 부름들로(읽어보기(몸.trim()));
|
|
147
|
+
if (목록) 담기(목록, m.index, m.index + m[0].length);
|
|
148
|
+
}
|
|
149
|
+
// <function=이름>{…}</function> 이 <tool_call> 밖에 홀로 온 꼴 (Llama 3.1)
|
|
150
|
+
const 함수무늬 = /<function=([^>\s]+)>\s*([\s\S]*?)\s*<\/function>/g;
|
|
151
|
+
while ((m = 함수무늬.exec(s))) {
|
|
152
|
+
if (울안(m.index) || 뗄곳.some(([a, b]) => m.index >= a && m.index < b)) continue;
|
|
153
|
+
const 인자 = 칸꼴(m[2]) ?? (m[2].trim() ? 읽어보기(m[2].trim()) : {});
|
|
154
|
+
if (객체인가(인자)) 담기([{ 이름: m[1], 인자 }], m.index, m.index + m[0].length);
|
|
155
|
+
}
|
|
156
|
+
// [TOOL_CALLS] [ … ] (Mistral) — 뒤는 JSON 배열 하나. 배열 뒤에 말이 붙기도 해서, 닫는 ] 를 뒤에서부터
|
|
157
|
+
// 하나씩 대 보며 부름 배열로 읽히는 첫 자리까지만 뗀다.
|
|
158
|
+
const 미스트랄 = /\[TOOL_CALLS\]\s*(?=\[)/.exec(s);
|
|
159
|
+
if (미스트랄 && !울안(미스트랄.index)) {
|
|
160
|
+
const 시작 = 미스트랄.index + 미스트랄[0].length;
|
|
161
|
+
for (let 끝 = s.lastIndexOf(']'), 번 = 0; 끝 > 시작 && 번 < 20; 끝 = s.lastIndexOf(']', 끝 - 1), 번++) {
|
|
162
|
+
const 목록 = 부름들로(읽어보기(s.slice(시작, 끝 + 1)));
|
|
163
|
+
if (!목록) continue;
|
|
164
|
+
담기(목록, 미스트랄.index, 끝 + 1);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (부름.length) return 마무리(s, 부름, 뗄곳);
|
|
169
|
+
|
|
170
|
+
// ── 2. 울타리·맨 JSON — 답이 거의 그것뿐일 때만 ─────────────────────
|
|
171
|
+
const 곁글한도 = 200;
|
|
172
|
+
const 울타리무늬 = /```(?:json|tool_call|tool|function)?\s*\n?([\s\S]*?)\n?```/gi;
|
|
173
|
+
const 울타리들 = [...s.matchAll(울타리무늬)];
|
|
174
|
+
// 울타리가 **하나일 때만** 본다. 둘 이상이면 예시를 늘어놓은 글이다 — 부름 여럿은 한 울타리에 배열로 온다.
|
|
175
|
+
if (울타리들.length === 1) {
|
|
176
|
+
const u = 울타리들[0];
|
|
177
|
+
const 목록 = 부름들로(읽어보기(u[1].trim()));
|
|
178
|
+
const 곁글 = (s.slice(0, u.index) + s.slice(u.index + u[0].length)).trim();
|
|
179
|
+
if (목록 && 곁글.length <= 곁글한도 && 담기(목록, u.index, u.index + u[0].length)) return 마무리(s, 부름, 뗄곳);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (울타리들.length) return null;
|
|
183
|
+
const 맨 = s.trim();
|
|
184
|
+
if (/^[[{]/.test(맨)) {
|
|
185
|
+
const 목록 = 부름들로(읽어보기(맨));
|
|
186
|
+
if (목록 && 담기(목록, 0, s.length)) return 마무리(s, 부름, 뗄곳);
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function 마무리(s, 부름, 뗄곳) {
|
|
192
|
+
const 차례 = [...뗄곳].sort((a, b) => b[0] - a[0]);
|
|
193
|
+
let 남은 = s;
|
|
194
|
+
for (const [a, b] of 차례) 남은 = 남은.slice(0, a) + 남은.slice(b);
|
|
195
|
+
return { 부름, 남은글: 남은.replace(/\n{3,}/g, '\n\n').trim() };
|
|
196
|
+
}
|
package/src/commands.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { 마지막할당량, 할당량말, 아슬아슬한가 } from './backend/quota.js';
|
|
4
4
|
import { 세션요금, 돈셈, 돈말, 어디서온값, 요금적는법 } from './backend/price.js';
|
|
5
|
+
import { 생성속도 } from './agent/session.js';
|
|
5
6
|
import { join } from 'node:path';
|
|
6
7
|
import { c, say, rule, pad, mark, width, clip } from './ui/ansi.js';
|
|
7
8
|
import { compact } from './agent/compact.js';
|
|
@@ -83,6 +84,8 @@ import { list as listSessions } from './agent/store.js';
|
|
|
83
84
|
import { MODES as WORK_MODES, ORDER as WORK_ORDER, normalize as normWork, get as getWork, canWrite, 보일이름, 보일한줄 } from './agent/modes.js';
|
|
84
85
|
import { COMMANDS, 설정남기기 } from './commands/common.js';
|
|
85
86
|
import { 딴이름들 } from './cmdnames.js';
|
|
87
|
+
// 슬래시 명령 찾기는 src/slash.js 에 있다 — deel run 이 이 파일 전체를 안 읽게 (2.1.3).
|
|
88
|
+
import { 슬래시명령찾기, 비슷한슬래시명령, MCP프롬프트펴기 } from './slash.js';
|
|
86
89
|
import { doPlugin, 미리보기, showSkills, 미리보기끄기 } from './commands/extend.js';
|
|
87
90
|
import { help, showLevel, 강조, showThink, showContext, showWork } from './commands/view.js';
|
|
88
91
|
import { 갈래명령, 증거명령, 붙여넣기명령, 리뷰명령, 커밋명령, 카드명령, 못박기명령, 배움명령, 바뀐것보기 } from './commands/work.js';
|
|
@@ -177,39 +180,8 @@ function 비슷한이름(친것, 이름들) {
|
|
|
177
180
|
.map(([n]) => n);
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
/*
|
|
184
|
-
* ── 이 PC 에서 찾은 슬래시 명령을 이름으로 찾는다 ───────────────────────
|
|
185
|
-
*
|
|
186
|
-
* 대화 화면(아래 default 갈래)과 배치(oneshot.js)가 **같은 규칙**을 써야 한다. 같은 글자를
|
|
187
|
-
* 쳤는데 창에 따라 되고 안 되면 사람은 어느 쪽을 믿을지 정할 수 없다. 그래서 두 벌로 적지
|
|
188
|
-
* 않고 여기 한 군데에 둔다.
|
|
189
|
-
*
|
|
190
|
-
* 찾는 차례는 셋이다 — 적은 그대로 · 대소문자 무시 · 꼬리 이름.
|
|
191
|
-
* 마지막 칸이 `x.name.split(':').pop() === 낮춘` 이었다 (8회차 그밖 명령2). 왼쪽은 파일에
|
|
192
|
-
* 적힌 그대로고 오른쪽은 낮춘 말이라 둘이 만날 수가 없다. `ext:ReviewCode` 는 `/ReviewCode`
|
|
193
|
-
* 로도 `/reviewcode` 로도 「모르는 명령」 이었고, 「비슷한 것」 에도 안 떴다 — 깔려 있는
|
|
194
|
-
* 명령이 어느 길로도 안 보이는 자리다. 배치에서는 그대로 1 로 선다.
|
|
195
|
-
*/
|
|
196
|
-
export function 슬래시명령찾기(목록, 부른이름) {
|
|
197
|
-
const 것들 = 목록 ?? [];
|
|
198
|
-
const 낮춘 = String(부른이름 ?? '').toLowerCase();
|
|
199
|
-
return 것들.find((x) => x.name === 부른이름)
|
|
200
|
-
?? 것들.find((x) => x.name.toLowerCase() === 낮춘)
|
|
201
|
-
?? 것들.find((x) => 꼬리이름(x.name) === 낮춘)
|
|
202
|
-
?? null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** 못 찾았을 때 「비슷한 것」 으로 늘어놓을 것. 찾기와 같은 잣대(낮춘 꼬리)를 쓴다. */
|
|
206
|
-
export function 비슷한슬래시명령(목록, 부른이름, 몇 = 5) {
|
|
207
|
-
const 낮춘 = String(부른이름 ?? '').toLowerCase();
|
|
208
|
-
if (!낮춘) return [];
|
|
209
|
-
return (목록 ?? [])
|
|
210
|
-
.filter((x) => x.name.toLowerCase().includes(낮춘) || 낮춘.includes(꼬리이름(x.name)))
|
|
211
|
-
.slice(0, 몇);
|
|
212
|
-
}
|
|
183
|
+
// 부르던 자리(검사 · 다른 모듈)가 그대로 쓰게 여기서도 내보낸다.
|
|
184
|
+
export { 슬래시명령찾기, 비슷한슬래시명령, MCP프롬프트펴기 };
|
|
213
185
|
|
|
214
186
|
export async function handle(line, session, ctx) {
|
|
215
187
|
// 일본어·중국어 입력기의 전각 빗금(/help). 그대로 두면 명령이 말이 되어 모델에게 간다(ui/complete.js).
|
|
@@ -1192,6 +1164,9 @@ export async function handle(line, session, ctx) {
|
|
|
1192
1164
|
})}`);
|
|
1193
1165
|
}
|
|
1194
1166
|
say(` ${c.gray(pad(말('cost.toolMs'), 14))} ${말('unit.sec', { n: (session.usage.ms / 1000).toFixed(1) })}`);
|
|
1167
|
+
// 생성 속도 — 흘려받은 부름만 잰다. 못 쟀으면 줄을 안 적는다 (session.js 의 생성속도).
|
|
1168
|
+
const 속도 = 생성속도(session.usage);
|
|
1169
|
+
if (속도 !== null) say(` ${c.gray(pad(말('cost.speed'), 14))} ${속도} tok/s`);
|
|
1195
1170
|
// 서버가 잠깐 막아 다시 부른 횟수. 0 이면 안 적는다 — 없는 일을 줄로 남기면 표만 길어진다.
|
|
1196
1171
|
if (session.usage.retries) say(` ${c.gray(pad(말('cost.retries'), 14))} ${말('unit.calls', { n: session.usage.retries })}`);
|
|
1197
1172
|
say(` ${c.gray(pad(말('cost.elapsed'), 14))} ${말('unit.min', { n: mins })}`);
|
|
@@ -1300,7 +1275,7 @@ export async function handle(line, session, ctx) {
|
|
|
1300
1275
|
* 있는 그대로 적는다 (safety/authcmd.js).
|
|
1301
1276
|
*/
|
|
1302
1277
|
if (session.conn.열쇠받기) {
|
|
1303
|
-
const 상태 = 지금열쇠상태();
|
|
1278
|
+
const 상태 = 지금열쇠상태(session.conn.열쇠받기);
|
|
1304
1279
|
const 남은말 = 상태
|
|
1305
1280
|
? 말('auth.leftMin', { 분: Math.round(상태.남은초 / 60) })
|
|
1306
1281
|
: 말('auth.notYet');
|
|
@@ -1592,7 +1567,7 @@ export async function handle(line, session, ctx) {
|
|
|
1592
1567
|
}
|
|
1593
1568
|
|
|
1594
1569
|
case 'mcp': {
|
|
1595
|
-
const { 설정읽기, 설정자리, 도구최대,
|
|
1570
|
+
const { 설정읽기, 설정자리, 도구최대, 되살리기최대, 적을주소 } = await import('./backend/mcp.js');
|
|
1596
1571
|
const 붙은것 = session.mcp ?? [];
|
|
1597
1572
|
rule('밖에서 붙인 도구 (MCP)', 70);
|
|
1598
1573
|
|
|
@@ -1610,6 +1585,8 @@ export async function handle(line, session, ctx) {
|
|
|
1610
1585
|
say('');
|
|
1611
1586
|
say(` ${c.gray('붙이려면')} ${c.white('.deel/mcp.json')} ${c.gray('에 이렇게 적습니다 —')}`);
|
|
1612
1587
|
say(` ${c.gray('{ "mcpServers": { "사내위키": { "command": "node", "args": ["wiki-mcp.js"] } } }')}`);
|
|
1588
|
+
// 주소로 붙는 서버도 받는다 (2.1.3) — 열쇠는 ${환경변수} 로 적어 저장소에 안 남긴다.
|
|
1589
|
+
say(` ${c.gray('{ "mcpServers": { "이슈": { "type": "http", "url": "https://mcp.사내.example/mcp", "headers": { "Authorization": "Bearer ${ISSUE_TOKEN}" } } } }')}`);
|
|
1613
1590
|
say('');
|
|
1614
1591
|
say(` ${c.yellow('※')} ${c.gray('MCP 서버는 남의 프로그램입니다. 사내 반입 심사를 따로 받으셔야 합니다.')}`);
|
|
1615
1592
|
say('');
|
|
@@ -1629,8 +1606,12 @@ export async function handle(line, session, ctx) {
|
|
|
1629
1606
|
*/
|
|
1630
1607
|
const 상태 = s.살아있나() ? c.green('●') : (s.대기 ? c.yellow('◐') : c.red('○'));
|
|
1631
1608
|
const 이름들 = s.도구.map((t) => t.name);
|
|
1632
|
-
|
|
1609
|
+
// HTTP 로 붙은 것은 주소를 적는다 — 이름·비밀번호·물음표 뒤는 떼고 (적을주소).
|
|
1610
|
+
const 어디 = s.설정.url ? 적을주소(s.설정.url) : s.설정.command;
|
|
1611
|
+
say(` ${상태} ${c.bold(s.이름)} ${c.gray(s.정보?.name ? `${s.정보.name} ${s.정보.version ?? ''}` : 어디)}${s.설정.url && s.정보?.name ? c.gray(` · ${어디}`) : ''}`);
|
|
1633
1612
|
say(` ${c.gray('도구 ' + s.도구.length + '개')} ${c.gray(clip(이름들.join(' · '), 60))}`);
|
|
1613
|
+
// 서버가 낸 프롬프트는 슬래시 명령으로 선다 (2.1.3 · 아래 default 갈래).
|
|
1614
|
+
if (s.프롬프트?.length) say(` ${c.gray('프롬프트 ' + s.프롬프트.length + '개')} ${c.gray(clip(s.프롬프트.map((p) => `/mcp__${s.이름}__${p.name}`).join(' · '), 60))}`);
|
|
1634
1615
|
if (s.대기) say(` ${c.gray('아직 안 띄웠습니다 — 적어 둔 목록입니다. 이 도구를 부르면 그때 뜹니다.')}`);
|
|
1635
1616
|
if (s.잘림) say(` ${mark.warn} ${c.gray(`${s.잘림}개는 뺐습니다 — 한 서버에 ${도구최대}개까지만 받습니다(컨텍스트가 줄어듭니다)`)}`);
|
|
1636
1617
|
if (s.달라짐) {
|
|
@@ -1836,8 +1817,16 @@ export async function handle(line, session, ctx) {
|
|
|
1836
1817
|
case 'plugins': return await doPlugin(session, arg), { handled: true };
|
|
1837
1818
|
|
|
1838
1819
|
default: {
|
|
1820
|
+
// MCP 서버가 낸 프롬프트인가 (2.1.3 · src/slash.js 의 MCP프롬프트펴기). 받은 글을 모델에게 보낸다.
|
|
1821
|
+
const 프롬프트 = await MCP프롬프트펴기(session.mcp, raw, arg);
|
|
1822
|
+
if (프롬프트?.탈) { say(` ${c.red(프롬프트.탈)}`); say(''); return { handled: true }; }
|
|
1823
|
+
if (프롬프트) {
|
|
1824
|
+
ctx?.audit?.write?.('mcp', { 이름: 프롬프트.서버이름, 프롬프트: 프롬프트.이름 });
|
|
1825
|
+
say(` ${c.cyan('⌘')} ${raw} ${c.gray(`MCP 프롬프트 · ${프롬프트.서버이름}`)}`);
|
|
1826
|
+
return { handled: false, text: 프롬프트.text };
|
|
1827
|
+
}
|
|
1839
1828
|
// 이 PC 에서 찾은 슬래시 명령인지 본다. 있으면 그 내용을 모델에게 보낸다.
|
|
1840
|
-
// 찾는 규칙은 배치(oneshot.js)와 한 군데에서 같이 쓴다 —
|
|
1829
|
+
// 찾는 규칙은 배치(oneshot.js)와 한 군데에서 같이 쓴다 — src/slash.js 의 슬래시명령찾기.
|
|
1841
1830
|
const found = 슬래시명령찾기(session.commands, raw);
|
|
1842
1831
|
if (found) {
|
|
1843
1832
|
const { text, error } = loadCommand(found, arg);
|
package/src/completion.js
CHANGED
|
@@ -91,6 +91,9 @@ export const 깃발들 = [
|
|
|
91
91
|
{ 이름: '--hard', 뜻: 'reset all 에서 되돌리기·감사기록까지', en: 'with reset all: undo snapshots and the audit log too', 값: false },
|
|
92
92
|
{ 이름: '--json', 뜻: '결과를 JSON 으로', en: 'output JSON', 값: false },
|
|
93
93
|
{ 이름: '--check', 뜻: 'run: 끝내기 전에 돌릴 검사 명령', en: 'run: check command to pass before finishing', 값: null },
|
|
94
|
+
{ 이름: '--check-rounds', 뜻: 'run: 검사를 몇 번까지 돌리나 (1~10)', en: 'run: how many times the check runs (1-10)', 값: null },
|
|
95
|
+
{ 이름: '--check-timeout', 뜻: 'run: 검사 한 번의 제한 시간(초)', en: 'run: time limit per check run (seconds)', 값: null },
|
|
96
|
+
{ 이름: '--events', 뜻: 'run: 사건을 이 파일에 JSONL 로', en: 'run: stream events to this file as JSONL', 값: '파일' },
|
|
94
97
|
{ 이름: '--repeat', 뜻: 'eval: 과제마다 몇 번 돌릴까', en: 'eval: runs per task', 값: null },
|
|
95
98
|
{ 이름: '--init', 뜻: 'eval: 예제 과제를 만든다', en: 'eval: write the example tasks', 값: false },
|
|
96
99
|
{ 이름: '--only', 뜻: 'eval: 고른 과제만 (쉼표) · sbom: 한 가지만', en: 'eval: only these tasks (comma); sbom: only one part', 값: null },
|
package/src/eval/run.js
CHANGED
|
@@ -97,20 +97,48 @@ export function 과제읽기(폴더) {
|
|
|
97
97
|
return 과제들;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// 수 칸은 수나 수를 적은 글만 읽는다. Number(true) = 1 · Number(null) = 0 이라 참/거짓이 수로 통과했다 (2.1.2 2차 눈).
|
|
101
|
+
const 수로 = (v) => (typeof v === 'number' || (typeof v === 'string' && v.trim() !== '') ? Number(v) : NaN);
|
|
102
|
+
|
|
100
103
|
/** task.json 이 쓸 수 있는 모양인가. 쓸 수 있으면 null, 아니면 까닭. */
|
|
101
104
|
export function 설정탈(v) {
|
|
102
105
|
if (!v || typeof v !== 'object' || Array.isArray(v)) return 'task.json 이 객체가 아닙니다';
|
|
103
106
|
if (typeof v.prompt !== 'string' || !v.prompt.trim()) return 'prompt(시킬 말)가 없습니다';
|
|
104
107
|
if (typeof v.check !== 'string' || !v.check.trim()) return 'check(정답 검사 명령)가 없습니다';
|
|
105
108
|
for (const 칸 of ['timeout', 'checkTimeout']) {
|
|
106
|
-
if (v[칸] !== undefined && !(Number.isFinite(
|
|
109
|
+
if (v[칸] !== undefined && !(Number.isFinite(수로(v[칸])) && 수로(v[칸]) > 0)) return `${칸} 은 0 보다 큰 초여야 합니다`;
|
|
107
110
|
}
|
|
108
111
|
for (const 칸 of ['doneCheck', 'work']) {
|
|
109
112
|
if (v[칸] !== undefined && typeof v[칸] !== 'string') return `${칸} 은 글이어야 합니다`;
|
|
110
113
|
}
|
|
114
|
+
// 과제마다 완료 검사를 몇 번·얼마나 돌리나 (2.1.2) — deel run 의 --check-rounds · --check-timeout 으로 넘긴다.
|
|
115
|
+
if (v.doneCheckRounds !== undefined && !(Number.isInteger(수로(v.doneCheckRounds)) && 수로(v.doneCheckRounds) >= 1)) {
|
|
116
|
+
return 'doneCheckRounds 는 1 이상의 정수여야 합니다';
|
|
117
|
+
}
|
|
118
|
+
if (v.doneCheckTimeout !== undefined && !(Number.isFinite(수로(v.doneCheckTimeout)) && 수로(v.doneCheckTimeout) > 0)) {
|
|
119
|
+
return 'doneCheckTimeout 은 0 보다 큰 초여야 합니다';
|
|
120
|
+
}
|
|
121
|
+
if ((v.doneCheckRounds !== undefined || v.doneCheckTimeout !== undefined) && !v.doneCheck) {
|
|
122
|
+
return 'doneCheckRounds · doneCheckTimeout 은 doneCheck(완료 검사 명령)와 같이 적어야 합니다';
|
|
123
|
+
}
|
|
111
124
|
return null;
|
|
112
125
|
}
|
|
113
126
|
|
|
127
|
+
/**
|
|
128
|
+
* 과제 하나에 띄울 `deel run` 의 인자. 시킬 말은 `--` 뒤 맨 끝이다 — 대시로 시작해도 깃발로 안 읽힌다.
|
|
129
|
+
*/
|
|
130
|
+
export function 실행인자(설정, 작업, 깃발들 = []) {
|
|
131
|
+
return [
|
|
132
|
+
진입점, 'run', '--json', '--yes', '--root', 작업,
|
|
133
|
+
...(설정.work ? ['--work', 설정.work] : []),
|
|
134
|
+
...(설정.doneCheck ? ['--check', 설정.doneCheck] : []),
|
|
135
|
+
...(설정.doneCheck && 설정.doneCheckRounds !== undefined ? ['--check-rounds', String(설정.doneCheckRounds)] : []),
|
|
136
|
+
...(설정.doneCheck && 설정.doneCheckTimeout !== undefined ? ['--check-timeout', String(설정.doneCheckTimeout)] : []),
|
|
137
|
+
...깃발들,
|
|
138
|
+
'--', 설정.prompt,
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
/**
|
|
115
143
|
* 폴더를 통째로 복사한다. 원본이 없으면 아무것도 안 한다(start/ 가 없는 과제는 빈 폴더에서 시작).
|
|
116
144
|
*
|
|
@@ -189,15 +217,8 @@ export async function 한번돌리기(과제, { 판 = 1, 깃발들 = [], 남김
|
|
|
189
217
|
const 작업 = mkdtempSync(join(tmpdir(), 'deel-eval-'));
|
|
190
218
|
try {
|
|
191
219
|
폴더복사(join(과제.자리, 'start'), 작업);
|
|
192
|
-
const 제한초 =
|
|
193
|
-
const 인자 =
|
|
194
|
-
진입점, 'run', '--json', '--yes', '--root', 작업,
|
|
195
|
-
...(설정.work ? ['--work', 설정.work] : []),
|
|
196
|
-
...(설정.doneCheck ? ['--check', 설정.doneCheck] : []),
|
|
197
|
-
...깃발들,
|
|
198
|
-
// `--` 뒤는 낱말이다 — 시킬 말이 대시로 시작해도 깃발로 안 읽힌다.
|
|
199
|
-
'--', 설정.prompt,
|
|
200
|
-
];
|
|
220
|
+
const 제한초 = 제한초고르기(설정, 시간초);
|
|
221
|
+
const 인자 = 실행인자(설정, 작업, 깃발들);
|
|
201
222
|
const 돌림 = await 돌려보기(process.execPath, 인자, {
|
|
202
223
|
timeout: 제한초 * 1000, maxBuffer: 64 * 1024 * 1024,
|
|
203
224
|
덤: { cwd: 작업, env: { ...process.env, NO_COLOR: '1' } },
|
|
@@ -251,8 +272,12 @@ export function 과제별(결과들) {
|
|
|
251
272
|
/**
|
|
252
273
|
* 지난 결과와 견준다. 다 통과하던 과제가 하나라도 떨어졌으면 나빠짐, 못 하던 과제를 다 해내면 나아짐.
|
|
253
274
|
* 되풀이 수가 다르면 비율로 본다.
|
|
275
|
+
*
|
|
276
|
+
* 지난번에 있던 과제가 이번에 없으면 **사라짐**, 처음 재는 과제는 **새것** 으로 적는다 (2.1.2). 여태
|
|
277
|
+
* 사라진 과제는 말없이 빠져, 떨어지던 과제 폴더를 지우고 돌려도 남은 과제끼리 「지난번과 같습니다」 가
|
|
278
|
+
* 나왔다. `일부만`(--only)이면 안 고른 과제가 다 사라짐이 되므로 사라짐은 안 센다.
|
|
254
279
|
*/
|
|
255
|
-
export function 견주기(지난것,
|
|
280
|
+
export function 견주기(지난것, 지금것, { 일부만 = false } = {}) {
|
|
256
281
|
const 비율 = (x) => (x.판 ? x.통과 / x.판 : 0);
|
|
257
282
|
const 지난표 = new Map((지난것 ?? []).map((x) => [x.과제, x]));
|
|
258
283
|
const 나빠짐 = [];
|
|
@@ -266,9 +291,28 @@ export function 견주기(지난것, 지금것) {
|
|
|
266
291
|
if (비율(x) < 비율(전)) 나빠짐.push(x.과제);
|
|
267
292
|
else if (비율(x) > 비율(전)) 나아짐.push(x.과제);
|
|
268
293
|
}
|
|
269
|
-
|
|
294
|
+
const 지금이름 = new Set(지금것.map((x) => x.과제));
|
|
295
|
+
const 사라짐 = 일부만 ? [] : [...지난표.keys()].filter((이름) => !지금이름.has(이름));
|
|
296
|
+
const 새것 = 지금것.filter((x) => !지난표.has(x.과제)).map((x) => x.과제);
|
|
297
|
+
return { 나빠짐, 나아짐, 견준, 사라짐, 새것 };
|
|
270
298
|
}
|
|
271
299
|
|
|
300
|
+
/**
|
|
301
|
+
* 「지난번과 같습니다」 를 어디까지 말할 수 있나. 달라진 과제가 있거나 견준 것이 없으면 null.
|
|
302
|
+
* 견주지 않은 과제(사라짐 · 새것 · --only 로 안 고른 것)가 있으면 '견준만큼' — 처음 재는 과제가 떨어져도
|
|
303
|
+
* 견준 과제끼리 같으면 「같습니다」 가 나와 그 실패가 묻혔다 (2.1.2 2차 눈).
|
|
304
|
+
*/
|
|
305
|
+
export function 같음범위(견줌, { 일부만 = false } = {}) {
|
|
306
|
+
if (!견줌?.견준 || 견줌.나빠짐.length || 견줌.나아짐.length) return null;
|
|
307
|
+
return 일부만 || 견줌.사라짐.length || 견줌.새것.length ? '견준만큼' : '전부';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 과제 하나의 제한 시간(초). task.json 에 적힌 것이 `--timeout` 을 이긴다 — pytest-timeout 의 표시나
|
|
312
|
+
* jest 의 과제별 시간과 같은 차례다. `--timeout` 은 제 시간을 안 적은 과제의 기본값이다.
|
|
313
|
+
*/
|
|
314
|
+
export const 제한초고르기 = (설정, 시간초) => Number(설정?.timeout ?? 시간초 ?? 기본시간초);
|
|
315
|
+
|
|
272
316
|
/**
|
|
273
317
|
* 제일 새 결과 파일. 깨졌거나 과제별이 없는 것(손으로 적은 `{}` 따위)은 건너뛴다 — 견줄 것이 없다.
|
|
274
318
|
*
|
|
@@ -377,6 +421,10 @@ export async function runEval({ 폴더 = null, init = false, repeat = 1, keep =
|
|
|
377
421
|
if (!Number.isInteger(되풀이) || 되풀이 < 1 || 되풀이 > 최대되풀이) {
|
|
378
422
|
return 못돌림(64, 글(`--repeat 는 1 ~ ${최대되풀이} 사이의 정수여야 합니다: ${repeat}`, `--repeat must be a whole number from 1 to ${최대되풀이}: ${repeat}`));
|
|
379
423
|
}
|
|
424
|
+
// 0 · 글 · 음수가 그대로 넘어가면 setTimeout(0) · NaN 이 되어 과제마다 deel 을 곧바로 죽인다 — 전부 「시간 초과」 실패.
|
|
425
|
+
if (시간초 !== null && !(Number.isFinite(Number(시간초)) && Number(시간초) > 0)) {
|
|
426
|
+
return 못돌림(64, 글(`--timeout 은 0 보다 큰 초여야 합니다: ${시간초}`, `--timeout must be a number of seconds above 0: ${시간초}`));
|
|
427
|
+
}
|
|
380
428
|
if (!existsSync(고른폴더)) {
|
|
381
429
|
return 못돌림(1, 글(`과제 모음이 없습니다 — ${보일}`, `No task folder — ${보일}`),
|
|
382
430
|
`${c.gray(글('예제부터 만들려면:', 'To start from examples:'))} ${c.cyan('deel eval --init')}`);
|
|
@@ -390,6 +438,9 @@ export async function runEval({ 폴더 = null, init = false, repeat = 1, keep =
|
|
|
390
438
|
|
|
391
439
|
말하기('');
|
|
392
440
|
말하기(` ${c.bold(글('골든셋', 'Golden set'))} ${c.gray(`— ${보일} · ${글(`과제 ${과제들.length}개 · 과제마다 ${되풀이}번`, `${과제들.length} tasks · ${되풀이} run(s) each`)}`)}`);
|
|
441
|
+
// --timeout 을 줬는데 제 시간을 적은 과제가 있으면 그 과제는 적힌 값을 쓴다고 미리 말한다 (제한초고르기).
|
|
442
|
+
const 제시간 = 시간초 !== null ? 과제들.filter((x) => x.설정?.timeout !== undefined).length : 0;
|
|
443
|
+
if (제시간) 말하기(` ${c.gray(글(`task.json 에 timeout 을 적은 과제 ${제시간}개는 --timeout 대신 그 값을 씁니다`, `${제시간} task(s) set their own timeout in task.json and use it instead of --timeout`))}`);
|
|
393
444
|
말하기('');
|
|
394
445
|
|
|
395
446
|
/*
|
|
@@ -436,7 +487,7 @@ export async function runEval({ 폴더 = null, init = false, repeat = 1, keep =
|
|
|
436
487
|
const 통과수 = 결과들.filter((r) => r.통과).length;
|
|
437
488
|
const 모델 = 결과들.find((r) => r.deel?.model)?.deel.model ?? null;
|
|
438
489
|
const 지난 = 지난결과(고른폴더, { 전체만: !only, 이름들: only ? 과제들.map((x) => x.이름) : null });
|
|
439
|
-
const 견줌 = 견주기(지난?.값?.과제별,
|
|
490
|
+
const 견줌 = 견주기(지난?.값?.과제별, 묶음, { 일부만: !!only });
|
|
440
491
|
const 초 = Math.round((Date.now() - t0) / 1000);
|
|
441
492
|
const 토큰 = 결과들.reduce((a, r) => ({ in: a.in + (r.deel?.usage?.in ?? 0), out: a.out + (r.deel?.usage?.out ?? 0) }), { in: 0, out: 0 });
|
|
442
493
|
|
|
@@ -475,7 +526,15 @@ export async function runEval({ 폴더 = null, init = false, repeat = 1, keep =
|
|
|
475
526
|
if (지난) {
|
|
476
527
|
if (견줌.나빠짐.length) 말하기(` ${c.red('▼')} ${글('지난번보다 나빠짐:', 'Worse than last time:')} ${견줌.나빠짐.join(' · ')}`);
|
|
477
528
|
if (견줌.나아짐.length) 말하기(` ${c.green('▲')} ${글('지난번보다 나아짐:', 'Better than last time:')} ${견줌.나아짐.join(' · ')}`);
|
|
478
|
-
if (
|
|
529
|
+
if (견줌.사라짐.length) 말하기(` ${c.yellow('⚠')} ${글('지난번에 있던 과제가 이번엔 없습니다:', 'Tasks from last time are missing now:')} ${견줌.사라짐.join(' · ')}`);
|
|
530
|
+
if (견줌.새것.length) 말하기(` ${c.gray(글(`처음 재는 과제: ${견줌.새것.join(' · ')}`, `Measured for the first time: ${견줌.새것.join(' · ')}`))}`);
|
|
531
|
+
// 견주지 않은 과제가 있으면 「같다」 는 견준 과제끼리의 말이다 — 그렇게 적는다.
|
|
532
|
+
const 같음 = 같음범위(견줌, { 일부만: !!only });
|
|
533
|
+
if (같음) {
|
|
534
|
+
말하기(` ${c.gray(같음 === '견준만큼'
|
|
535
|
+
? 글(`견준 과제 ${견줌.견준}개는 지난번(${지난.파일})과 같습니다`, `The ${견줌.견준} task(s) compared are the same as last time (${지난.파일})`)
|
|
536
|
+
: 글(`지난번(${지난.파일})과 같습니다`, `Same as last time (${지난.파일})`))}`);
|
|
537
|
+
}
|
|
479
538
|
}
|
|
480
539
|
if (남긴곳) 말하기(` ${c.gray(글(`결과: ${relative(뿌리, 남긴곳)}`, `Results: ${relative(뿌리, 남긴곳)}`))}`);
|
|
481
540
|
else if (못남긴까닭) {
|