deel-local-cli 2.1.2 → 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.
@@ -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} 호출됨${argOk ? ''
467
+ ? `${tcalls[0]?.name} 호출됨${고쳐읽음 ? ' — 인자 JSON 모양이 틀려(홑따옴표·끝 쉼표 같은 것) deel 이 느슨하게 읽었습니다'
468
+ : argOk ? ''
456
469
  : 원문에있나 ? ` — 인자가 우리 상한(${도구상한}토큰)에 잘렸을 뿐, 값은 제대로 왔습니다`
457
470
  : 모양틀림 ? ' — 인자 JSON 모양이 틀렸습니다 (홑따옴표·끝 쉼표 같은 것 — 편집 도구가 자주 거절당합니다)'
458
471
  : 잘린인자 ? ' — 인자 JSON 이 잘려 왔습니다 (상한을 올려 다시 보세요)'
459
472
  : ' — 인자가 부정확, 편집 신뢰성 작업이 더 필요합니다'}`
460
- : tl.ok ? '도구를 안 부르고 글로만 답합니다' : serverMessage(tl),
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
- /** `ext:ReviewCode` 의 꼬리 — 언제나 낮춰서 돌려준다. 찾기와 「비슷한 것」 이 같은 잣대를 쓰게 한다. */
181
- const 꼬리이름 = (이름) => String(이름 ?? '').split(':').pop().toLowerCase();
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 { 설정읽기, 설정자리, 도구최대, 되살리기최대 } = await import('./backend/mcp.js');
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
- say(` ${상태} ${c.bold(s.이름)} ${c.gray(s.정보?.name ? `${s.정보.name} ${s.정보.version ?? ''}` : s.설정.command)}`);
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
@@ -93,6 +93,7 @@ export const 깃발들 = [
93
93
  { 이름: '--check', 뜻: 'run: 끝내기 전에 돌릴 검사 명령', en: 'run: check command to pass before finishing', 값: null },
94
94
  { 이름: '--check-rounds', 뜻: 'run: 검사를 몇 번까지 돌리나 (1~10)', en: 'run: how many times the check runs (1-10)', 값: null },
95
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', 값: '파일' },
96
97
  { 이름: '--repeat', 뜻: 'eval: 과제마다 몇 번 돌릴까', en: 'eval: runs per task', 값: null },
97
98
  { 이름: '--init', 뜻: 'eval: 예제 과제를 만든다', en: 'eval: write the example tasks', 값: false },
98
99
  { 이름: '--only', 뜻: 'eval: 고른 과제만 (쉼표) · sbom: 한 가지만', en: 'eval: only these tasks (comma); sbom: only one part', 값: null },
package/src/exit.js CHANGED
@@ -84,8 +84,10 @@ export function 실패덩이({ reason, code, why = '', model = null }) {
84
84
  return {
85
85
  ok: false, reason, code, text: '',
86
86
  tools: 0, steps: 0,
87
- usage: { in: 0, out: 0, prompt: 0, cacheRead: 0, cacheWrite: 0, calls: 0, ms: 0, 못잰것: 0, retries: 0 },
87
+ usage: { in: 0, out: 0, prompt: 0, cacheRead: 0, cacheWrite: 0, calls: 0, ms: 0, 못잰것: 0, retries: 0, genMs: 0, tokPerSec: null },
88
88
  model,
89
+ // 바뀐 파일과 대화 이름 (2.1.3) — 시작도 못 했으니 비었다. 칸은 성공과 같게 둔다.
90
+ files: [], session: null,
89
91
  ms: 0,
90
92
  ...(why ? { why } : {}),
91
93
  };
package/src/i18n/en.js CHANGED
@@ -518,6 +518,7 @@ export const en = {
518
518
  'ev.hookNote': 'hook: {말}',
519
519
  'ev.hookBlock': 'A hook blocked this',
520
520
  'ev.nudgeRead': 'It was about to finish having only read things — pushed back once',
521
+ 'ev.textCalls': 'The model wrote {n} tool call(s) as text — running them: {이름}',
521
522
  'check.start': 'Completion check ({판}/{최대}) — {명령}',
522
523
  'check.clamped': '{칸} {준값} is over the limit — using {쓴값}',
523
524
  'check.invalid': '{칸} {준값} is not a usable value — using the default {쓴값}',
@@ -528,6 +529,8 @@ export const en = {
528
529
  'check.skipped': 'Could not run the completion check — {까닭}',
529
530
  'cli.checkFlag': 'run this check before finishing; failures go back to be fixed, and still failing ends with 8',
530
531
  'cli.checkRoundsFlag': 'for this run only: how many times the check runs (1-10; fixes = runs - 1) and the time limit per run (s)',
532
+ 'cli.runResumeFlag': 'runs are saved in .deel/sessions like chats and can be continued — the name is session in --json',
533
+ 'cli.eventsFlag': 'stream events to this file, one JSON per line (the last line is the result)',
531
534
  'cli.evalTitle': 'Golden set',
532
535
  'cli.evalWhy': '— run a set of tasks on the real model and measure how many it gets done',
533
536
  'cli.evalInit': 'create five example tasks in golden/',
@@ -577,11 +580,13 @@ export const en = {
577
580
  'cost.tokIn': 'Input tokens',
578
581
  'cost.tokOut': 'Output tokens',
579
582
  'cost.toolMs': 'Time in tools',
583
+ 'cost.speed': 'Generation speed',
580
584
  'cost.elapsed': 'Elapsed',
581
585
  'cost.money': 'Cost',
582
586
  // ── Cache, wire card, automatic effort ────────────────────────────────
583
587
  'status.wire': 'wire',
584
588
  'wire.think': 'thinking',
589
+ 'run.notSaving': 'Could not write all of this run to disk ({까닭}) — {n} lines lost. session is empty in --json — this run cannot be resumed.',
585
590
  'run.refusal': 'The model declined this request — stopping here instead of pushing again',
586
591
  'run.refusalWhy': 'reason {왜}',
587
592
  'wire.rungs': 'rungs',
package/src/i18n/ja.js CHANGED
@@ -476,6 +476,7 @@ export const ja = {
476
476
  'ev.hookNote': 'フック: {말}',
477
477
  'ev.hookBlock': 'フックが止めました',
478
478
  'ev.nudgeRead': '読むだけで終えようとしたので、一度押し戻しました',
479
+ 'ev.textCalls': 'モデルがテキストで書いたツール呼び出し{n}件を読み取って実行します — {이름}',
479
480
  'check.start': '完了チェック ({판}/{최대}) — {명령}',
480
481
  'check.clamped': '{칸} {준값} は上限を超えるため {쓴값} にしました',
481
482
  'check.invalid': '{칸} {준값} は使えない値のため既定の {쓴값} を使います',
@@ -486,6 +487,8 @@ export const ja = {
486
487
  'check.skipped': '完了チェックを回せませんでした — {까닭}',
487
488
  'cli.checkFlag': '終える前にこのチェックを回し、通れば終了(失敗なら直させ、最後まで失敗なら 8)',
488
489
  'cli.checkRoundsFlag': 'この実行だけ:チェックを回す回数(1〜10、直す機会は回数−1)と1回の制限時間(秒)',
490
+ 'cli.runResumeFlag': '会話画面と同じく .deel/sessions に残り、続けられます — 名前は --json の session',
491
+ 'cli.eventsFlag': 'イベントをこのファイルに1行1つの JSON で(最後の行は結果)',
489
492
  'cli.evalTitle': 'ゴールデンセット',
490
493
  'cli.evalWhy': '— 課題集を実際のモデルで回し、いくつこなせるかを測ります',
491
494
  'cli.evalInit': 'golden/ に例題を五つ作ります',
@@ -534,11 +537,13 @@ export const ja = {
534
537
  'cost.tokIn': '入力トークン',
535
538
  'cost.tokOut': '出力トークン',
536
539
  'cost.toolMs': 'ツール時間',
540
+ 'cost.speed': '生成速度',
537
541
  'cost.elapsed': '経過',
538
542
  'cost.money': '料金',
539
543
  // ── キャッシュ・配線カード・自動強度 ──────────────────────────────────
540
544
  'status.wire': '配線',
541
545
  'wire.think': '思考',
546
+ 'run.notSaving': 'この実行の会話をファイルに書き切れませんでした({까닭})— {n}行を失いました。--json の session は空です — この会話は続けられません。',
542
547
  'run.refusal': 'モデルがこの依頼を断りました — 押し直さずにここで止めます',
543
548
  'run.refusalWhy': '理由 {왜}',
544
549
  'wire.rungs': '目盛り',
package/src/i18n/ko.js CHANGED
@@ -550,6 +550,7 @@ export const ko = {
550
550
  'ev.hookNote': '훅: {말}',
551
551
  'ev.hookBlock': '훅이 막았습니다',
552
552
  'ev.nudgeRead': '읽기만 하고 끝내려고 해서 한 번 되밀었습니다',
553
+ 'ev.textCalls': '모델이 글로 적은 도구 부름 {n}개를 알아듣고 돌립니다 — {이름}',
553
554
  'check.start': '완료 검사 ({판}/{최대}) — {명령}',
554
555
  'check.clamped': '{칸} {준값} 은 상한을 넘어 {쓴값} 으로 줄였습니다',
555
556
  'check.invalid': '{칸} {준값} 은 쓸 수 없는 값이라 기본값 {쓴값} 을 씁니다',
@@ -560,6 +561,8 @@ export const ko = {
560
561
  'check.skipped': '완료 검사를 못 돌렸습니다 — {까닭}',
561
562
  'cli.checkFlag': '끝내려 할 때 이 검사를 돌려 통과해야 끝남 (실패하면 고치게 하고, 끝내 실패면 8)',
562
563
  'cli.checkRoundsFlag': '이번 실행에서만 검사 판수(1~10, 고칠 기회는 판수−1)·한 번의 제한 시간(초)을 덮음',
564
+ 'cli.runResumeFlag': '대화 화면처럼 .deel/sessions 에 남고 이어 합니다 — 이름은 --json 의 session',
565
+ 'cli.eventsFlag': '사건을 이 파일에 한 줄에 하나씩 JSON 으로 (마지막 줄은 결과)',
563
566
  'cli.evalTitle': '골든셋',
564
567
  'cli.evalWhy': '— 과제 모음을 실제 모델로 돌려 몇 개를 해내나 잽니다',
565
568
  'cli.evalInit': '예제 과제 다섯을 golden/ 에 만듭니다',
@@ -625,11 +628,13 @@ export const ko = {
625
628
  'cost.tokIn': '입력 토큰',
626
629
  'cost.tokOut': '출력 토큰',
627
630
  'cost.toolMs': '도구 시간',
631
+ 'cost.speed': '생성 속도',
628
632
  'cost.elapsed': '경과',
629
633
  'cost.money': '요금',
630
634
  // ── 캐시·전선·자동 강도 (backend/wire.js · backend/cachemark.js) ──────
631
635
  'status.wire': '전선',
632
636
  'wire.think': '생각',
637
+ 'run.notSaving': '이 실행의 대화를 파일에 다 못 적었습니다 ({까닭}) — {n}줄을 잃었습니다. --json 의 session 을 비웠습니다 — 이 대화는 이어 할 수 없습니다.',
633
638
  'run.refusal': '모델이 이 요청을 거절했습니다 — 되밀지 않고 여기서 멈춥니다',
634
639
  'run.refusalWhy': '까닭 {왜}',
635
640
  'wire.rungs': '눈금',
package/src/i18n/zh.js CHANGED
@@ -476,6 +476,7 @@ export const zh = {
476
476
  'ev.hookNote': '钩子:{말}',
477
477
  'ev.hookBlock': '钩子拦下了这一步',
478
478
  'ev.nudgeRead': '只看不动就要收尾,已经推回去一次',
479
+ 'ev.textCalls': '模型以文字写出了 {n} 个工具调用,已识别并执行 — {이름}',
479
480
  'check.start': '完成检查 ({판}/{최대}) — {명령}',
480
481
  'check.clamped': '{칸} {준값} 超过上限,改用 {쓴값}',
481
482
  'check.invalid': '{칸} {준값} 不是可用的值,改用默认值 {쓴값}',
@@ -486,6 +487,8 @@ export const zh = {
486
487
  'check.skipped': '没能跑完成检查 — {까닭}',
487
488
  'cli.checkFlag': '结束前跑这个检查,通过才结束(失败就让它修,最后仍失败则返回 8)',
488
489
  'cli.checkRoundsFlag': '仅本次运行:检查跑几次(1~10,修复机会 = 次数 − 1)和每次的时限(秒)',
490
+ 'cli.runResumeFlag': '与对话界面一样保存在 .deel/sessions 并可继续 — 名称是 --json 的 session',
491
+ 'cli.eventsFlag': '把事件逐行以 JSON 写入此文件(最后一行是结果)',
489
492
  'cli.evalTitle': '黄金集',
490
493
  'cli.evalWhy': '— 用真实模型跑一组任务,测它能完成多少',
491
494
  'cli.evalInit': '在 golden/ 里建五个示例任务',
@@ -534,11 +537,13 @@ export const zh = {
534
537
  'cost.tokIn': '输入 token',
535
538
  'cost.tokOut': '输出 token',
536
539
  'cost.toolMs': '工具耗时',
540
+ 'cost.speed': '生成速度',
537
541
  'cost.elapsed': '已用时',
538
542
  'cost.money': '费用',
539
543
  // ── 缓存 · 线路卡 · 自动强度 ──────────────────────────────────────────
540
544
  'status.wire': '线路',
541
545
  'wire.think': '思考',
546
+ 'run.notSaving': '本次运行的对话没能全部写入文件({까닭})— 丢失 {n} 行。--json 的 session 为空 — 无法继续这段对话。',
542
547
  'run.refusal': '模型拒绝了这个请求 — 不再重试,就此停止',
543
548
  'run.refusalWhy': '原因 {왜}',
544
549
  'wire.rungs': '档位',
package/src/lsp/diag.js CHANGED
@@ -100,7 +100,8 @@ export async function 편집후진단(뿌리, abs, { 시한 = 기다릴시한 }
100
100
  */
101
101
  export function 붙이기(결과, 진단, 보일이름) {
102
102
  if (!진단 || (!진단.오류 && !진단.경고)) return 결과;
103
- const 머리 = `언어 서버 — ${보일이름}: `
103
+ // 언어 서버가 못 준 자리에서 문법만 본 것(tools/verify.js 의 한파일문법)은 그렇게 적는다 — 뜻까지 본 것처럼 읽히면 안 된다.
104
+ const 머리 = `${진단.출처 ? `문법 검사(${진단.출처})` : '언어 서버'} — ${보일이름}: `
104
105
  + [진단.오류 ? `오류 ${진단.오류}개` : '', 진단.경고 ? `경고 ${진단.경고}개` : ''].filter(Boolean).join(' · ');
105
106
  return {
106
107
  ...결과,