deel-local-cli 1.16.0 → 1.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +55 -9
- package/README.md +55 -8
- package/bin/deel.js +385 -5
- package/package.json +1 -1
- package/src/acp/serve.js +39 -1
- package/src/agent/agents.js +203 -0
- package/src/agent/loop.js +209 -17
- package/src/agent/models.js +4 -0
- package/src/agent/outschema.js +351 -0
- package/src/agent/session.js +13 -2
- package/src/backend/adapter.js +38 -2
- package/src/backend/clientcert.js +164 -0
- package/src/backend/http.js +232 -26
- package/src/backend/mcp.js +194 -30
- package/src/backend/probe.js +27 -1
- package/src/backend/wire.js +10 -0
- package/src/cmdnames.js +2 -0
- package/src/commands.js +139 -2
- package/src/completion.js +34 -4
- package/src/config.js +86 -9
- package/src/configexplain.js +177 -0
- package/src/doctor.js +195 -0
- package/src/i18n/en.js +37 -0
- package/src/i18n/ja.js +37 -0
- package/src/i18n/ko.js +37 -0
- package/src/i18n/zh.js +37 -0
- package/src/lsp/client.js +2 -2
- package/src/oneshot.js +181 -4
- package/src/pack/sbom.js +77 -3
- package/src/pack/selfpack.js +1 -1
- package/src/pack/sheet.en.js +1 -1
- package/src/repl.js +74 -3
- package/src/safety/hooks.js +389 -0
- package/src/safety/policy.js +74 -0
- package/src/safety/shellenv.js +116 -0
- package/src/safety/trust.js +230 -0
- package/src/setup.js +9 -0
- package/src/stats.js +162 -0
- package/src/tools/docs.js +12 -2
- package/src/tools/hwpxwrite.js +302 -0
- package/src/tools/index.js +128 -9
- package/src/tools/jobs.js +11 -6
- package/src/tools/spawn.js +19 -1
- package/src/tools/task.js +8 -2
package/src/lsp/client.js
CHANGED
|
@@ -394,7 +394,7 @@ export class 언어서버 {
|
|
|
394
394
|
// 뿌리+언어를 열쇠로 잡아 둔다. 켤 때 값이 들어서 한 번 켠 것은 계속 쓴다.
|
|
395
395
|
|
|
396
396
|
const 풀 = new Map();
|
|
397
|
-
const 열쇠 = (뿌리, g) => `${뿌리}
|
|
397
|
+
const 열쇠 = (뿌리, g) => `${뿌리}\0${g}`;
|
|
398
398
|
|
|
399
399
|
/**
|
|
400
400
|
* 이 파일에 맞는 서버를 얻는다. 못 쓰면 null.
|
|
@@ -420,7 +420,7 @@ export async function 얻기(뿌리, 파일, { 켜기까지 = true, env = proces
|
|
|
420
420
|
/** 지금 떠 있는 것들. /lsp 화면과 시험이 쓴다. */
|
|
421
421
|
export function 지금것들() {
|
|
422
422
|
return [...풀.entries()].map(([k, v]) => {
|
|
423
|
-
const [뿌리, 갈래열쇠] = k.split('
|
|
423
|
+
const [뿌리, 갈래열쇠] = k.split('\0');
|
|
424
424
|
return { 뿌리, 갈래: 갈래열쇠, 이름: v.서버?.이름, 살았나: v.살았나(), 준비: !!v.능력.ready, 죽음: v.죽음 ?? null };
|
|
425
425
|
});
|
|
426
426
|
}
|
package/src/oneshot.js
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
// 여기서 루프를 다시 짜면 두 벌이 되고, 언젠가 한쪽만 고쳐진다.
|
|
13
13
|
import { c, mark, clip } from './ui/ansi.js';
|
|
14
14
|
import { 규칙모으기, 정책읽기 } from './safety/policy.js';
|
|
15
|
+
import { 훅읽기 } from './safety/hooks.js';
|
|
16
|
+
import { 에이전트읽기 } from './agent/agents.js';
|
|
17
|
+
import { 프로젝트설정줄들 } from './safety/trust.js';
|
|
18
|
+
import { 스키마읽기, 맞나, 답에서JSON뽑기, 시킬말 as 스키마시킬말 } from './agent/outschema.js';
|
|
19
|
+
import { 남길것읽기 } from './safety/shellenv.js';
|
|
15
20
|
import { 받기설정 } from './safety/authcmd.js';
|
|
16
21
|
import { run } from './agent/loop.js';
|
|
17
22
|
import { Session } from './agent/session.js';
|
|
@@ -19,7 +24,7 @@ import { makeScope } from './safety/guard.js';
|
|
|
19
24
|
import { 모두끄기 as 언어서버다끄기 } from './lsp/client.js';
|
|
20
25
|
import { History } from './safety/undo.js';
|
|
21
26
|
import { Audit, 열쇠묻기 } from './safety/audit.js';
|
|
22
|
-
import { activeProfile, load, resolveKey, 잠금소식,
|
|
27
|
+
import { activeProfile, load, resolveKey, 잠금소식, 열쇠탈소식, 프로젝트설정소식 } from './config.js';
|
|
23
28
|
import { 말 as 옮긴말 } from './i18n/index.js';
|
|
24
29
|
import { 알림채움 } from './backend/retry.js';
|
|
25
30
|
import { 전선붙이기, 세션이름짓기 } from './backend/wire.js';
|
|
@@ -30,6 +35,8 @@ import { allowEndpoint, setOffline } from './safety/network.js';
|
|
|
30
35
|
import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js';
|
|
31
36
|
import { 주소가리기 } from './safety/secrets.js';
|
|
32
37
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
38
|
+
import { 잠잠기본 } from './backend/http.js';
|
|
39
|
+
import { 인증서설정 } from './backend/clientcert.js';
|
|
33
40
|
import { route } from './agent/route.js';
|
|
34
41
|
import { get as getWork } from './agent/modes.js';
|
|
35
42
|
import { 모두끝내기 as 일감모두끝내기, 일감인자 } from './tools/jobs.js';
|
|
@@ -68,6 +75,17 @@ export const EXIT = {
|
|
|
68
75
|
* 열쇠를 보는 일이고, 거절은 **시킨 말을 바꾸는 일**이다.
|
|
69
76
|
*/
|
|
70
77
|
refusal: 6,
|
|
78
|
+
/*
|
|
79
|
+
* 답이 --output-schema 에 안 맞았다.
|
|
80
|
+
*
|
|
81
|
+
* 여기에도 자기 코드가 있어야 한다. 「모양을 못 박아 달라」 고 한 사람에게
|
|
82
|
+
* 모양이 안 맞는 것을 0 으로 넘기면, 파이프 뒤 스크립트가 그걸 온전한
|
|
83
|
+
* 것으로 받아 쓴다 — 그게 이 기능이 없애려던 바로 그 상황이다.
|
|
84
|
+
*
|
|
85
|
+
* 오류(1)와도 가른다. 고칠 자리가 다르다 — 오류는 연결을 보는 일이고,
|
|
86
|
+
* 이건 스키마나 시킨 말을 바꾸는 일이다.
|
|
87
|
+
*/
|
|
88
|
+
schema: 7,
|
|
71
89
|
};
|
|
72
90
|
|
|
73
91
|
/**
|
|
@@ -131,6 +149,10 @@ export async function runOnce(opts = {}) {
|
|
|
131
149
|
const 삐끗 = (s = '') => process.stderr.write(s + '\n');
|
|
132
150
|
const 곁 = (s = '') => { if (!quiet) 삐끗(s); };
|
|
133
151
|
|
|
152
|
+
// 아래 내놓기 가 이걸 본다 — 그래서 읽기 전에 선언해 둔다. 값은 시킬 말을
|
|
153
|
+
// 정하기 전에 채운다 (모델을 부르기 전에 스키마부터 읽는 자리).
|
|
154
|
+
let 출력스키마 = null;
|
|
155
|
+
|
|
134
156
|
const 내놓기 = (r) => {
|
|
135
157
|
/*
|
|
136
158
|
* 뒤에서 돌던 명령을 반드시 거둔다.
|
|
@@ -146,6 +168,23 @@ export async function runOnce(opts = {}) {
|
|
|
146
168
|
// 껐다는 말부터 하면 "그건 또 뭐냐" 가 된다.
|
|
147
169
|
언어서버다끄기().catch(() => {});
|
|
148
170
|
if (json) process.stdout.write(JSON.stringify(r) + '\n');
|
|
171
|
+
/*
|
|
172
|
+
* 모양을 못 박았으면 표준출력은 **그 JSON 하나**다.
|
|
173
|
+
*
|
|
174
|
+
* 모델이 낸 글자 그대로가 아니라 우리가 읽어서 다시 적은 것을 낸다.
|
|
175
|
+
* 그래야 ```울타리나 앞뒤 인사말이 절대 안 섞이고, `| jq` 가 첫 판부터
|
|
176
|
+
* 그냥 먹는다. 어차피 맞는지 이미 쟀으므로 다시 적어도 잃는 것이 없다.
|
|
177
|
+
*/
|
|
178
|
+
else if (r.schema !== undefined) process.stdout.write(JSON.stringify(r.schema) + '\n');
|
|
179
|
+
/*
|
|
180
|
+
* 모양을 못 박았는데 못 맞췄으면 **표준출력은 비운다.**
|
|
181
|
+
*
|
|
182
|
+
* 여기서 모델이 낸 글을 그대로 흘리면, 파이프 뒤 `jq` 는 모양이 안 맞는
|
|
183
|
+
* JSON 이나 그냥 산문을 받는다. 종료코드는 7 이지만 `cmd | jq` 처럼 쓴
|
|
184
|
+
* 자리에서는 앞 명령의 코드가 안 보인다. 아무것도 안 주는 쪽이 낫다 —
|
|
185
|
+
* 무엇이 왔었는지는 표준오류에 이미 적혀 있다.
|
|
186
|
+
*/
|
|
187
|
+
else if (출력스키마) { /* 일부러 아무것도 안 낸다 */ }
|
|
149
188
|
else if (r.text) process.stdout.write(r.text.endsWith('\n') ? r.text : r.text + '\n');
|
|
150
189
|
return r.code;
|
|
151
190
|
};
|
|
@@ -153,11 +192,32 @@ export async function runOnce(opts = {}) {
|
|
|
153
192
|
const 못함 = (reason, message) => {
|
|
154
193
|
삐끗(` ${c.red('✗')} ${message}`);
|
|
155
194
|
return 내놓기({
|
|
156
|
-
|
|
195
|
+
// 갈래에 제 코드가 있으면 그것으로 끝낸다. 없는 갈래(no-config·no-prompt)는
|
|
196
|
+
// 예전대로 1 이다 — 여기가 EXIT.error 로 못 박혀 있어서, 스키마 파일을
|
|
197
|
+
// 못 읽은 것과 게이트웨이가 없는 것이 같은 1 로 나갔다. 고칠 자리가
|
|
198
|
+
// 서로 완전히 다른 둘이라 스크립트가 갈라 대응할 수 없었다.
|
|
199
|
+
ok: false, reason, code: EXIT[reason] ?? EXIT.error, text: '', why: message,
|
|
157
200
|
tools: 0, steps: 0, usage: { in: 0, out: 0, calls: 0, ms: 0, retries: 0 }, ms: 0,
|
|
158
201
|
});
|
|
159
202
|
};
|
|
160
203
|
|
|
204
|
+
/*
|
|
205
|
+
* 답의 모양을 못 박았으면 **일을 시작하기 전에** 스키마부터 읽는다.
|
|
206
|
+
*
|
|
207
|
+
* 다 돌리고 나서 「스키마 파일이 없습니다」 라고 하면 모델을 부른 값을
|
|
208
|
+
* 통째로 버리는 셈이다. 그리고 그 실패는 사람이 오타 하나 고치면 되는
|
|
209
|
+
* 것이라, 제일 먼저 알려 줘야 한다.
|
|
210
|
+
*/
|
|
211
|
+
if (opts.outputSchema) {
|
|
212
|
+
const r = 스키마읽기(String(opts.outputSchema));
|
|
213
|
+
if (!r.ok) return 못함('schema', r.왜);
|
|
214
|
+
출력스키마 = r.스키마;
|
|
215
|
+
// 우리가 안 보는 열쇠는 안 본다고 말한다. 조용히 넘기면 사람은 잰 줄 안다.
|
|
216
|
+
if (r.모른것.length) {
|
|
217
|
+
곁(` ${mark.warn} ${c.yellow(`스키마에서 안 재는 열쇠가 있습니다: ${r.모른것.join(' · ')}`)}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
161
221
|
// ── 시킬 말 ───────────────────────────────────────────────────────────
|
|
162
222
|
// 인자로 준 것이 먼저다. 없을 때만 표준입력을 읽는다 —
|
|
163
223
|
// 둘 다 있을 때 무엇이 이기는지가 헷갈리면 스크립트가 조용히 엉뚱한 일을 한다.
|
|
@@ -176,6 +236,18 @@ export async function runOnce(opts = {}) {
|
|
|
176
236
|
const 잠금 = 잠금소식();
|
|
177
237
|
if (잠금) { try { process.stderr.write(` ${잠금}\n`); } catch { /* 못 써도 그만 */ } }
|
|
178
238
|
|
|
239
|
+
/*
|
|
240
|
+
* 프로젝트 설정을 안 읽었거나 일부를 걷어냈으면 표준오류로 낸다.
|
|
241
|
+
*
|
|
242
|
+
* 배치 자리라 더 중요하다 — 사람이 앞에 없으면 「내가 적어 둔 설정이
|
|
243
|
+
* 안 먹고 있다」 를 알아챌 다른 길이 없다. 표준출력은 JSON 을 읽는
|
|
244
|
+
* 쪽이 쓰고 있으므로 여기도 섞지 않는다.
|
|
245
|
+
*/
|
|
246
|
+
for (const 줄 of 프로젝트설정줄들(프로젝트설정소식())) {
|
|
247
|
+
if (!줄) continue;
|
|
248
|
+
try { process.stderr.write(줄 + String.fromCharCode(10)); } catch { /* 못 써도 그만 */ }
|
|
249
|
+
}
|
|
250
|
+
|
|
179
251
|
const root = opts.root ? String(opts.root) : process.cwd();
|
|
180
252
|
const conn = {
|
|
181
253
|
kind: prof.kind, base: prof.baseUrl, auth: prof.auth,
|
|
@@ -194,6 +266,17 @@ export async function runOnce(opts = {}) {
|
|
|
194
266
|
streaming: prof.streaming ?? false,
|
|
195
267
|
tools: prof.tools ?? false, json: prof.json ?? false, think: prof.think ?? false,
|
|
196
268
|
vision: prof.vision ?? false,
|
|
269
|
+
/*
|
|
270
|
+
* 흘려 받다가 이만큼 잠잠하면 끊는다 (밀리초, backend/http.js).
|
|
271
|
+
*
|
|
272
|
+
* 답이 다 오는 데 걸린 시간이 아니라 **아무것도 안 온 시간**이다. 그래서
|
|
273
|
+
* 30분짜리 답은 안 끊기고 30초 멎은 연결은 30초에 끊긴다. 답을 통째로
|
|
274
|
+
* 모았다가 한 번에 주는 사내 게이트웨이면 이 값을 올린다.
|
|
275
|
+
*/
|
|
276
|
+
잠잠: prof.잠잠 ?? prof.streamIdleMs ?? 잠잠기본,
|
|
277
|
+
// 게이트웨이가 우리 인증서를 요구하면 (mTLS, backend/clientcert.js).
|
|
278
|
+
// 파일 경로만 싣는다 — 알맹이는 요청 직전에 읽는다.
|
|
279
|
+
인증서: 인증서설정(prof),
|
|
197
280
|
};
|
|
198
281
|
|
|
199
282
|
/*
|
|
@@ -329,6 +412,17 @@ export async function runOnce(opts = {}) {
|
|
|
329
412
|
// 정말 맡기고 싶은 사람은 --yes 로 그 뜻을 명시한다.
|
|
330
413
|
const 승인필요 = session.mode !== 'auto';
|
|
331
414
|
const 자동승인 = opts.yes === true;
|
|
415
|
+
/*
|
|
416
|
+
* 사람이 적어 둔 훅을 읽는다 (safety/hooks.js).
|
|
417
|
+
*
|
|
418
|
+
* 여기서 **한 번만** 읽는다. 도구를 부를 때마다 읽으면 대화 도중에 훅
|
|
419
|
+
* 파일이 바뀌는 것이 곧 「방금 통과한 것이 다음 걸음에 막힌다」 가 되고,
|
|
420
|
+
* 그건 원인을 찾을 길이 없는 화면이다. 설정을 켤 때 한 번 읽는 것과 같다.
|
|
421
|
+
*/
|
|
422
|
+
const 훅정보 = 훅읽기(root, { 켜짐: opts.hooks === false ? false : null });
|
|
423
|
+
// 이름 붙인 하위 작업 (agent/agents.js). 스킬처럼 켜질 때 한 번 찾는다.
|
|
424
|
+
const 에이전트정보 = 에이전트읽기(root);
|
|
425
|
+
|
|
332
426
|
const ctx = {
|
|
333
427
|
scope: makeScope(root),
|
|
334
428
|
// 도구가 한 번에 돌려줄 양을 이 값에서 뽑는다 (agent/budget.js).
|
|
@@ -338,6 +432,14 @@ export async function runOnce(opts = {}) {
|
|
|
338
432
|
get 눈있나() { return !!conn.vision; },
|
|
339
433
|
// 적어 둔 허락·금지 규칙 (safety/policy.js). 승인 모드보다 먼저 본다.
|
|
340
434
|
규칙들: 규칙모으기(cfg),
|
|
435
|
+
// 사람이 적어 둔 훅 (safety/hooks.js). 프로젝트 파일은 믿는 폴더에서만 읽는다.
|
|
436
|
+
훅들: 훅정보.훅들,
|
|
437
|
+
// 이름 붙인 하위 작업. 목록은 Task 스키마에, 지침은 고른 뒤에만 실린다.
|
|
438
|
+
에이전트들: 에이전트정보.에이전트들,
|
|
439
|
+
// Bash 자식에게 되살려 줄 환경변수 이름 (safety/shellenv.js).
|
|
440
|
+
// 기본은 열쇠처럼 생긴 이름을 다 빼는 것이고, 여기 적은 것만 되살린다 —
|
|
441
|
+
// 사내 저장소를 쓰는 사람은 npm ci 에 NPM_TOKEN 이 실제로 필요하다.
|
|
442
|
+
셸남길것: 남길것읽기(cfg),
|
|
341
443
|
history: new History(root),
|
|
342
444
|
audit: new Audit(root, { 열쇠들: 열쇠묻기(conn) }),
|
|
343
445
|
seen: new Set(),
|
|
@@ -367,10 +469,13 @@ export async function runOnce(opts = {}) {
|
|
|
367
469
|
// 거부만 하고 이유를 안 알리면 모델은 같은 호출을 몇 번이고 다시 한다.
|
|
368
470
|
// 사람이 '안 돼요' 라고 한 줄 알고, 다시 물어보면 이번엔 된다고 믿는다.
|
|
369
471
|
// 그러면 걸음 수만 다 쓰고 아무것도 못 한 채 끝난다.
|
|
370
|
-
const
|
|
472
|
+
const 보낼글바탕 = (승인필요 && !자동승인)
|
|
371
473
|
? `${시킬말}\n\n(비대화 모드다. 사람이 없어 승인을 물어볼 수 없고, 승인이 필요한 도구 호출은 자동으로 거부된다.`
|
|
372
474
|
+ ' 승인 없이 되는 방법을 골라라. 그래도 안 되면 무엇이 막혔는지 말로 알려라.)'
|
|
373
475
|
: 시킬말;
|
|
476
|
+
// 스키마는 시킨 말 **뒤에** 붙인다. 앞에 붙이면 모델이 「무엇을 하라」 보다
|
|
477
|
+
// 「어떻게 적으라」 를 먼저 읽고, 일보다 모양에 힘을 쓴다.
|
|
478
|
+
const 보낼글 = 출력스키마 ? `${보낼글바탕}\n${스키마시킬말(출력스키마)}` : 보낼글바탕;
|
|
374
479
|
|
|
375
480
|
// ── 컨텍스트 길이 ─────────────────────────────────────────────────────
|
|
376
481
|
//
|
|
@@ -546,7 +651,17 @@ export async function runOnce(opts = {}) {
|
|
|
546
651
|
*/
|
|
547
652
|
case 'cutoff':
|
|
548
653
|
말없이끊겼나 = true;
|
|
549
|
-
곁(` ${c.yellow('⚠')} ${c.gray(
|
|
654
|
+
곁(` ${c.yellow('⚠')} ${c.gray(ev.멎은초
|
|
655
|
+
? 옮긴말('ev.stalled', { n: ev.멎은초 })
|
|
656
|
+
: 옮긴말('ev.cutoff'))}`);
|
|
657
|
+
break;
|
|
658
|
+
|
|
659
|
+
case 'hook_note':
|
|
660
|
+
for (const 줄 of String(ev.말).split('\n').slice(0, 8)) 곁(` ${c.gray(`· ${줄}`)}`);
|
|
661
|
+
break;
|
|
662
|
+
|
|
663
|
+
case 'hook_block':
|
|
664
|
+
곁(` ${c.yellow('✗')} ${c.gray(옮긴말('ev.hookBlock'))}`);
|
|
550
665
|
break;
|
|
551
666
|
|
|
552
667
|
case 'nudge':
|
|
@@ -646,6 +761,65 @@ export async function runOnce(opts = {}) {
|
|
|
646
761
|
why = 옮긴말('ev.cutoff');
|
|
647
762
|
}
|
|
648
763
|
|
|
764
|
+
/*
|
|
765
|
+
* ── 답이 정해진 모양에 맞나 ──────────────────────────────────────────
|
|
766
|
+
*
|
|
767
|
+
* 「JSON 으로 답해 줘」 라고 부탁만 하고 안 재면, 지키는 날과 안 지키는
|
|
768
|
+
* 날이 생긴다. 안 지킨 날에 깨지는 것은 우리가 아니라 **뒤에 붙은 남의
|
|
769
|
+
* 코드**다. 부탁은 계약이 아니다. 계약이라고 부르려면 재야 한다.
|
|
770
|
+
*
|
|
771
|
+
* 안 맞으면 무엇이 어떻게 안 맞는지를 그대로 돌려주고 한 번만 더 시킨다.
|
|
772
|
+
* 두 번 세 번 되풀이하지 않는다 — 한 번에 못 고치는 것은 대개 스키마와
|
|
773
|
+
* 시킨 말이 서로 안 맞는 것이고, 그건 사람이 볼 일이다.
|
|
774
|
+
*/
|
|
775
|
+
let 스키마값 = null;
|
|
776
|
+
if (출력스키마 && reason === 'done') {
|
|
777
|
+
const 재보기 = (글) => {
|
|
778
|
+
const 뽑은것 = 답에서JSON뽑기(글);
|
|
779
|
+
if (!뽑은것.ok) return { ok: false, 탈: [뽑은것.왜] };
|
|
780
|
+
const r = 맞나(뽑은것.값, 출력스키마);
|
|
781
|
+
return r.ok ? { ok: true, 값: 뽑은것.값, 군말: 뽑은것.군말 } : r;
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
let 잰것 = 재보기(답 ?? '');
|
|
785
|
+
if (!잰것.ok) {
|
|
786
|
+
곁(` ${mark.warn} ${c.yellow(`답이 스키마에 안 맞습니다 — 한 번 더 시킵니다 (${잰것.탈.length}건)`)}`);
|
|
787
|
+
for (const t of 잰것.탈.slice(0, 5)) 곁(` ${c.gray(t)}`);
|
|
788
|
+
|
|
789
|
+
/*
|
|
790
|
+
* 다시 받는 자리는 **글만** 받는다.
|
|
791
|
+
*
|
|
792
|
+
* 이건 새 일이 아니라 이미 낸 답을 모양에 맞춰 다시 적는 일이라,
|
|
793
|
+
* 도구 기록도 걸음 수도 다시 세지 않는다. `고쳐쓰기` 는 그 자리에
|
|
794
|
+
* 되밀기가 안 걸리게 한다 — 안 끄면 되묻기 한 번에 모델을 두 번 부른다
|
|
795
|
+
* (agent/loop.js 의 고쳐쓰기 설명).
|
|
796
|
+
*/
|
|
797
|
+
let 다시글 = '';
|
|
798
|
+
try {
|
|
799
|
+
for await (const ev of run(session, ctx, 스키마시킬말(출력스키마, { 다시: 잰것.탈 }), { signal: turn.signal, 고쳐쓰기: true })) {
|
|
800
|
+
if (ev.type === 'content') 다시글 += ev.text;
|
|
801
|
+
else if (ev.type === 'done') 다시글 = ev.text ?? 다시글;
|
|
802
|
+
else if (ev.type === 'error') { why = String(ev.text ?? why); break; }
|
|
803
|
+
}
|
|
804
|
+
} catch (err) { why = String(err?.message ?? err); }
|
|
805
|
+
|
|
806
|
+
const 두번째 = 재보기(다시글);
|
|
807
|
+
if (두번째.ok) { 잰것 = 두번째; 답 = 다시글; }
|
|
808
|
+
else 잰것 = { ok: false, 탈: 두번째.탈 };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (잰것.ok) {
|
|
812
|
+
스키마값 = 잰것.값;
|
|
813
|
+
// 울타리나 인사말을 걷어내고 뽑았으면 그렇게 말한다. 조용히 걷어내면
|
|
814
|
+
// 사람은 모델이 깨끗하게 냈다고 여기고 시킴말을 안 고친다.
|
|
815
|
+
if (잰것.군말) 곁(` ${c.gray('· 답에 붙은 군말을 걷어내고 JSON 만 냈습니다.')}`);
|
|
816
|
+
} else {
|
|
817
|
+
reason = 'schema';
|
|
818
|
+
why = `답이 스키마에 안 맞습니다 (${잰것.탈.length}건)\n`
|
|
819
|
+
+ 잰것.탈.slice(0, 8).map((x) => ` - ${x}`).join('\n');
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
649
823
|
const code = EXIT[reason] ?? EXIT.error;
|
|
650
824
|
if (why) 삐끗(` ${reason === 'done' ? c.gray('·') : c.red('✗')} ${why}`);
|
|
651
825
|
if (!json && !quiet) {
|
|
@@ -679,6 +853,9 @@ export async function runOnce(opts = {}) {
|
|
|
679
853
|
retries: session.usage.retries ?? 0,
|
|
680
854
|
},
|
|
681
855
|
model: conn.model,
|
|
856
|
+
// 모양을 못 박았을 때만 실린다. --json 으로 받는 쪽은 text 를 다시 파싱할
|
|
857
|
+
// 필요 없이 이 칸을 그대로 쓰면 된다.
|
|
858
|
+
...(스키마값 !== null ? { schema: 스키마값 } : {}),
|
|
682
859
|
ms: Date.now() - t0,
|
|
683
860
|
...(why ? { why } : {}),
|
|
684
861
|
});
|
package/src/pack/sbom.js
CHANGED
|
@@ -28,8 +28,27 @@ import { 보관방식 } from '../safety/keystore.js';
|
|
|
28
28
|
import { 언어 } from '../i18n/index.js';
|
|
29
29
|
import { specEn, egressEn, keyStorageEn, auditLogEn, specSummaryEn } from './sheet.en.js';
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* 우리가 내놓는 SBOM 규격. 스캐너가 이 숫자를 보고 읽는 법을 정한다.
|
|
33
|
+
*
|
|
34
|
+
* 1.5 에서 1.7 로 올렸다. 판을 올린 것 자체가 목적이 아니라, CISA 가
|
|
35
|
+
* 2026-07-29 로 「SBOM 최소 요소」 를 새로 냈고(2021년 NTIA 문서를 대체한다)
|
|
36
|
+
* 거기서 더해진 네 가지를 적을 자리가 1.5 에는 없었다.
|
|
37
|
+
*
|
|
38
|
+
* 부품 해시 알고리즘 hashes[].alg — 1.5 에도 있었고 이미 적고 있었다
|
|
39
|
+
* 부품 라이선스 components[].licenses
|
|
40
|
+
* 만든 도구 이름 metadata.tools.components[]
|
|
41
|
+
* 만든 맥락 metadata.lifecycles[]
|
|
42
|
+
*
|
|
43
|
+
* 앞의 두 개는 자리가 있었고 우리가 안 적었다. 뒤의 두 개는 1.5 의 모양이
|
|
44
|
+
* 다르다 — metadata.tools 가 1.5 에서는 {vendor,name,version} 배열이고
|
|
45
|
+
* 1.6 부터 부품 목록으로 바뀌었으며, lifecycles 는 아예 없다.
|
|
46
|
+
*
|
|
47
|
+
* 판을 안 올리고 필드만 끼워 넣는 길도 있었지만 그러면 규격에 안 맞는
|
|
48
|
+
* SBOM 이 된다. 스캐너가 1.5 로 검사하다 모르는 열쇠에서 멈추면, 담당자
|
|
49
|
+
* 화면에는 「이 SBOM 은 읽을 수 없음」 한 줄만 남는다.
|
|
50
|
+
*/
|
|
51
|
+
export const CDX판 = '1.7';
|
|
33
52
|
|
|
34
53
|
/**
|
|
35
54
|
* CycloneDX SBOM.
|
|
@@ -59,6 +78,24 @@ export function sbom(a, { at = new Date(), serial = null, lang = 언어() } = {}
|
|
|
59
78
|
: 'A coding agent CLI for local models and in-house gateways',
|
|
60
79
|
};
|
|
61
80
|
|
|
81
|
+
// 부품마다 붙일 라이선스. CISA 2026 최소 요소의 하나이고, 여태 뿌리
|
|
82
|
+
// 부품에만 적고 파일에는 안 적었다. 스캐너 쪽에서 보면 「라이선스를 모르는
|
|
83
|
+
// 파일 152개」 라 검토 대기로 쌓인다.
|
|
84
|
+
const 라이선스 = a.license ? [{ license: { id: a.license } }] : [];
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
* `$schema` 는 안 적는다.
|
|
88
|
+
*
|
|
89
|
+
* 규격이 허락하는 칸이고 편집기에서 편하기도 하다. 그런데 그 값은 규격을
|
|
90
|
+
* 내는 곳의 주소이고, 그러면 **우리 소스에 바깥 주소가 하나 생긴다.**
|
|
91
|
+
* 이 프로그램의 심사 논거는 「나가는 주소가 사람이 정한 하나뿐」
|
|
92
|
+
* 이고, 그 주장은 소스를 grep 해서 확인된다. 거기서 안 부르는 주소 하나가
|
|
93
|
+
* 나오면 담당자는 그것부터 물어야 하고, 우리는 「그건 안 부릅니다」 를
|
|
94
|
+
* 설명해야 한다. 한 줄 편하자고 살 값이 아니다.
|
|
95
|
+
*
|
|
96
|
+
* 스캐너가 읽는 법을 정하는 것은 아래 bomFormat + specVersion 이다.
|
|
97
|
+
* $schema 가 없다고 못 읽는 검사기는 없다.
|
|
98
|
+
*/
|
|
62
99
|
return {
|
|
63
100
|
bomFormat: 'CycloneDX',
|
|
64
101
|
specVersion: CDX판,
|
|
@@ -66,13 +103,48 @@ export function sbom(a, { at = new Date(), serial = null, lang = 언어() } = {}
|
|
|
66
103
|
version: 1,
|
|
67
104
|
metadata: {
|
|
68
105
|
timestamp: at.toISOString(),
|
|
106
|
+
/*
|
|
107
|
+
* 만든 맥락 — 이 SBOM 이 **언제 찍힌 것인가**.
|
|
108
|
+
*
|
|
109
|
+
* 같은 물건이라도 소스를 보고 만든 것(build)과 돌아가는 것을 훑어 만든
|
|
110
|
+
* 것(operations)은 내용이 다르다. 그 구별이 없으면 담당자는 두 SBOM 이
|
|
111
|
+
* 어긋날 때 어느 쪽이 맞는지 정할 근거가 없다.
|
|
112
|
+
*
|
|
113
|
+
* 우리 것은 언제나 build 다 — selfpack.js 가 배포될 파일을 그 자리에서
|
|
114
|
+
* 읽어 해시를 뜬다.
|
|
115
|
+
*/
|
|
116
|
+
lifecycles: [{ phase: 'build' }],
|
|
69
117
|
component: 본체,
|
|
70
|
-
|
|
118
|
+
/*
|
|
119
|
+
* 만든 도구. 1.6 부터 {vendor,name,version} 배열이 아니라 부품 목록이다.
|
|
120
|
+
*
|
|
121
|
+
* 옛 모양도 아직 읽히지만 안 쓴다 — 규격이 물러나라고 표시해 둔 자리를
|
|
122
|
+
* 굳이 쓰면, 그 자리가 사라지는 판에서 조용히 빈칸이 된다.
|
|
123
|
+
*/
|
|
124
|
+
tools: {
|
|
125
|
+
components: [{
|
|
126
|
+
type: 'application',
|
|
127
|
+
name: 'deel pack',
|
|
128
|
+
version: a.version,
|
|
129
|
+
publisher: 'deel',
|
|
130
|
+
}],
|
|
131
|
+
},
|
|
71
132
|
properties: [
|
|
72
133
|
{ name: 'deel:runtime', value: `node ${a.node}` },
|
|
73
134
|
{ name: 'deel:dependencies', value: String(a.deps.length) },
|
|
74
135
|
{ name: 'deel:devDependencies', value: String(a.devDeps.length) },
|
|
75
136
|
{ name: 'deel:installScripts', value: a.lifecycle.length ? a.lifecycle.join(',') : 'none' },
|
|
137
|
+
/*
|
|
138
|
+
* 진위는 여기서 주장하지 않고, **어디서 확인하는지**만 적는다.
|
|
139
|
+
*
|
|
140
|
+
* CycloneDX 에는 서명 칸(signature)이 있다. 거기에 우리가 만든 서명을
|
|
141
|
+
* 넣을 수도 있지만, 그 서명을 검증할 열쇠를 같이 넘겨야 뜻이 있고 —
|
|
142
|
+
* 그러면 「우리가 우리를 보증한다」 가 된다. 담당자에게 아무 값이 없다.
|
|
143
|
+
*
|
|
144
|
+
* 실제 서명은 npm 배포 때 붙는 SLSA 증명이고, 그건 우리 손을 안 거친다.
|
|
145
|
+
* 확인하는 명령을 적어 두는 편이 서명 한 덩이보다 낫다.
|
|
146
|
+
*/
|
|
147
|
+
{ name: 'deel:provenance', value: `npm audit signatures — ${a.name}@${a.version} (SLSA v1, npm 신뢰 배포)` },
|
|
76
148
|
],
|
|
77
149
|
},
|
|
78
150
|
// 파일마다 하나씩. 담긴 것이 담겨야 할 것과 같은지 여기서 대조한다.
|
|
@@ -80,6 +152,8 @@ export function sbom(a, { at = new Date(), serial = null, lang = 언어() } = {}
|
|
|
80
152
|
type: 'file',
|
|
81
153
|
'bom-ref': `file:${f.path}`,
|
|
82
154
|
name: f.path,
|
|
155
|
+
version: a.version,
|
|
156
|
+
licenses: 라이선스,
|
|
83
157
|
hashes: [{ alg: 'SHA-256', content: f.sha }],
|
|
84
158
|
properties: [{ name: 'deel:bytes', value: String(f.bytes) }],
|
|
85
159
|
})),
|
package/src/pack/selfpack.js
CHANGED
|
@@ -289,7 +289,7 @@ export function packSelf(outFile, { root = repoRoot(), at = new Date(), lang =
|
|
|
289
289
|
'심사 담당자께',
|
|
290
290
|
' 반입심사서.txt 사람이 읽는 심사 자료. 의존성·설치 스크립트·네트워크',
|
|
291
291
|
' 호출 자리·파일별 SHA-256 이 전부 적혀 있습니다.',
|
|
292
|
-
' sbom.cdx.json SBOM (CycloneDX 1.
|
|
292
|
+
' sbom.cdx.json SBOM (CycloneDX 1.7). 스캐너에 그대로 넣으시면 됩니다.',
|
|
293
293
|
' 심사명세.json 통신 목록 · 감사기록 사양 · 파일 해시. 기계가 읽는 형식입니다.',
|
|
294
294
|
'',
|
|
295
295
|
' 세 파일 모두 소스를 훑어 자동으로 만든 것입니다. 손으로 적은 값이 아닙니다.',
|
package/src/pack/sheet.en.js
CHANGED
|
@@ -158,7 +158,7 @@ export function readMeEn() {
|
|
|
158
158
|
'For the security reviewer',
|
|
159
159
|
' import-review.txt The review package in prose: dependencies, install scripts,',
|
|
160
160
|
' every network call site, and a SHA-256 for each file.',
|
|
161
|
-
' sbom.cdx.json SBOM (CycloneDX 1.
|
|
161
|
+
' sbom.cdx.json SBOM (CycloneDX 1.7). Feed it straight to your scanner.',
|
|
162
162
|
' audit-spec.json Egress list, audit-log specification, file hashes - machine readable.',
|
|
163
163
|
'',
|
|
164
164
|
' All three are generated by scanning the source. None of it is written by hand.',
|
package/src/repl.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// 대화 화면. 루프가 보내는 이벤트를 Claude Code 풍으로 그린다.
|
|
2
2
|
import { createInterface, emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { 규칙모으기, 정책읽기 } from './safety/policy.js';
|
|
4
|
+
import { 훅읽기 } from './safety/hooks.js';
|
|
5
|
+
import { 에이전트읽기 } from './agent/agents.js';
|
|
4
6
|
import { 받기설정 } from './safety/authcmd.js';
|
|
5
7
|
import { 주소가리기 } from './safety/secrets.js';
|
|
6
8
|
import { homedir } from 'node:os';
|
|
@@ -24,7 +26,9 @@ import { 언어서버있나 } from './tools/index.js';
|
|
|
24
26
|
import { 모두끄기 as 언어서버다끄기 } from './lsp/client.js';
|
|
25
27
|
import { History } from './safety/undo.js';
|
|
26
28
|
import { Audit, 열쇠묻기 } from './safety/audit.js';
|
|
27
|
-
import { activeProfile, load, resolveKey, save as saveCfg, homeDir, 잠금소식,
|
|
29
|
+
import { activeProfile, load, resolveKey, save as saveCfg, homeDir, 잠금소식, 열쇠탈소식, 프로젝트설정소식 } from './config.js';
|
|
30
|
+
import { 프로젝트설정줄들 } from './safety/trust.js';
|
|
31
|
+
import { 남길것읽기 } from './safety/shellenv.js';
|
|
28
32
|
import { discover } from './skills/discover.js';
|
|
29
33
|
import { allowEndpoint, setOffline, isOffline } from './safety/network.js';
|
|
30
34
|
import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js';
|
|
@@ -44,6 +48,8 @@ import { 접을까 as 붙임접을까, 표만들기 as 붙임표, 펼치기 as
|
|
|
44
48
|
import { 고른것풀기, 계획답풀기 } from './ui/pick.js';
|
|
45
49
|
import { 이력지킴이 } from './ui/histline.js';
|
|
46
50
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
51
|
+
import { 잠잠기본 } from './backend/http.js';
|
|
52
|
+
import { 인증서설정 } from './backend/clientcert.js';
|
|
47
53
|
import { renderDiff, shortStat } from './ui/diff.js';
|
|
48
54
|
import { expand as expandMentions } from './agent/mention.js';
|
|
49
55
|
import { 크기말 } from './backend/vision.js';
|
|
@@ -172,6 +178,15 @@ export async function chatLoop(opts = {}) {
|
|
|
172
178
|
바로쓰기(` ${mark.ok} ${잠금}`);
|
|
173
179
|
}
|
|
174
180
|
|
|
181
|
+
/*
|
|
182
|
+
* 이 폴더의 프로젝트 설정을 안 읽었거나 일부를 걷어냈으면 그렇다고 한 줄.
|
|
183
|
+
*
|
|
184
|
+
* 조용히 무시하면 적어 둔 사람은 걸린 줄 알고, 실제로는 안 걸린 채로 일이
|
|
185
|
+
* 돈다. 그 어긋남이 설정 전체를 못 믿게 만든다 — 「.deel/config.json 은
|
|
186
|
+
* 가끔 먹는 파일」 이 되는 순간 아무도 안 쓴다.
|
|
187
|
+
*/
|
|
188
|
+
for (const 줄 of 프로젝트설정줄들(프로젝트설정소식())) 바로쓰기(줄);
|
|
189
|
+
|
|
175
190
|
/*
|
|
176
191
|
* 잠근 열쇠를 못 풀었으면 **그 까닭을** 적는다.
|
|
177
192
|
*
|
|
@@ -226,6 +241,17 @@ export async function chatLoop(opts = {}) {
|
|
|
226
241
|
tools: prof.tools ?? false, json: prof.json ?? false, think: prof.think ?? false,
|
|
227
242
|
// 그림을 볼 수 있는 모델인지. 못 보면 바이트를 아예 안 싣는다 (backend/vision.js).
|
|
228
243
|
vision: prof.vision ?? false,
|
|
244
|
+
/*
|
|
245
|
+
* 흘려 받다가 이만큼 잠잠하면 끊는다 (밀리초, backend/http.js).
|
|
246
|
+
*
|
|
247
|
+
* 답이 다 오는 데 걸린 시간이 아니라 **아무것도 안 온 시간**이다. 그래서
|
|
248
|
+
* 30분짜리 답은 안 끊기고 30초 멎은 연결은 30초에 끊긴다. 답을 통째로
|
|
249
|
+
* 모았다가 한 번에 주는 사내 게이트웨이면 이 값을 올린다.
|
|
250
|
+
*/
|
|
251
|
+
잠잠: prof.잠잠 ?? prof.streamIdleMs ?? 잠잠기본,
|
|
252
|
+
// 게이트웨이가 우리 인증서를 요구하면 (mTLS, backend/clientcert.js).
|
|
253
|
+
// 파일 경로만 싣는다 — 알맹이는 요청 직전에 읽는다.
|
|
254
|
+
인증서: 인증서설정(prof),
|
|
229
255
|
};
|
|
230
256
|
|
|
231
257
|
// conn 을 짓느라 resolveKey 가 방금 불렸다. 못 푼 것이 있으면 여기서 말한다.
|
|
@@ -1123,6 +1149,17 @@ export async function chatLoop(opts = {}) {
|
|
|
1123
1149
|
for (const x of 빠진) say(` ${c.gray(`${x.번호}. ${clip(x.글, 68)}`)}`);
|
|
1124
1150
|
};
|
|
1125
1151
|
|
|
1152
|
+
/*
|
|
1153
|
+
* 사람이 적어 둔 훅을 읽는다 (safety/hooks.js).
|
|
1154
|
+
*
|
|
1155
|
+
* 여기서 **한 번만** 읽는다. 도구를 부를 때마다 읽으면 대화 도중에 훅
|
|
1156
|
+
* 파일이 바뀌는 것이 곧 「방금 통과한 것이 다음 걸음에 막힌다」 가 되고,
|
|
1157
|
+
* 그건 원인을 찾을 길이 없는 화면이다. 설정을 켤 때 한 번 읽는 것과 같다.
|
|
1158
|
+
*/
|
|
1159
|
+
const 훅정보 = 훅읽기(root, { 켜짐: opts.hooks === false ? false : null });
|
|
1160
|
+
// 이름 붙인 하위 작업 (agent/agents.js). 스킬처럼 켜질 때 한 번 찾는다.
|
|
1161
|
+
const 에이전트정보 = 에이전트읽기(root);
|
|
1162
|
+
|
|
1126
1163
|
const ctx = {
|
|
1127
1164
|
scope: makeScope(root),
|
|
1128
1165
|
// 도구가 한 번에 돌려줄 양을 이 값에서 뽑는다 (agent/budget.js).
|
|
@@ -1132,6 +1169,14 @@ export async function chatLoop(opts = {}) {
|
|
|
1132
1169
|
get 눈있나() { return !!conn.vision; },
|
|
1133
1170
|
// 적어 둔 허락·금지 규칙 (safety/policy.js). 승인 모드보다 먼저 본다.
|
|
1134
1171
|
규칙들: 규칙모으기(cfg),
|
|
1172
|
+
// 사람이 적어 둔 훅 (safety/hooks.js). 프로젝트 파일은 믿는 폴더에서만 읽는다.
|
|
1173
|
+
훅들: 훅정보.훅들,
|
|
1174
|
+
// 이름 붙인 하위 작업. 목록은 Task 스키마에, 지침은 고른 뒤에만 실린다.
|
|
1175
|
+
에이전트들: 에이전트정보.에이전트들,
|
|
1176
|
+
// Bash 자식에게 되살려 줄 환경변수 이름 (safety/shellenv.js).
|
|
1177
|
+
// 기본은 열쇠처럼 생긴 이름을 다 빼는 것이고, 여기 적은 것만 되살린다 —
|
|
1178
|
+
// 사내 저장소를 쓰는 사람은 npm ci 에 NPM_TOKEN 이 실제로 필요하다.
|
|
1179
|
+
셸남길것: 남길것읽기(cfg),
|
|
1135
1180
|
history: new History(root),
|
|
1136
1181
|
audit: new Audit(root, { 열쇠들: 열쇠묻기(conn) }),
|
|
1137
1182
|
seen: new Set(),
|
|
@@ -2212,8 +2257,15 @@ export async function chatLoop(opts = {}) {
|
|
|
2212
2257
|
case 'cutoff':
|
|
2213
2258
|
clearThinking();
|
|
2214
2259
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
2215
|
-
|
|
2216
|
-
|
|
2260
|
+
if (ev.멎은초) {
|
|
2261
|
+
// 흐름이 멎어서 우리가 끊은 자리. 손댈 곳이 다르므로 다른 말을 한다 —
|
|
2262
|
+
// 「다시 물어 보세요」 는 여기서 틀린 조언이다. 같은 게이트웨이면 또 멎는다.
|
|
2263
|
+
say(` ${mark.warn} ${c.gray(`${ev.멎은초}초 동안 아무것도 안 와서 끊었습니다 — 받은 데까지만 보여 드립니다.`)}`);
|
|
2264
|
+
say(` ${c.gray('답을 통째로 모았다가 한 번에 주는 게이트웨이면 이렇게 됩니다. 설정의 잠잠 을 올려 보세요.')}`);
|
|
2265
|
+
} else {
|
|
2266
|
+
say(` ${mark.warn} ${c.gray('서버가 끝났다는 말 없이 답을 멈췄습니다 — 중간에서 끊겼을 수 있습니다.')}`);
|
|
2267
|
+
say(` ${c.gray('중계 프록시·게이트웨이를 거치면 나는 일입니다. 같은 것을 다시 물어 보세요.')}`);
|
|
2268
|
+
}
|
|
2217
2269
|
break;
|
|
2218
2270
|
|
|
2219
2271
|
case 'compact_failed':
|
|
@@ -2300,6 +2352,25 @@ export async function chatLoop(opts = {}) {
|
|
|
2300
2352
|
* 조용히 밀면 모델이 말을 들은 것인지 우연히 다시 생각한 것인지
|
|
2301
2353
|
* 구분이 안 된다. 한 줄 적어 둠다 — 또 이러면 사람이 알아볼 수 있어야 한다.
|
|
2302
2354
|
*/
|
|
2355
|
+
/*
|
|
2356
|
+
* 훅이 무슨 말을 했다 (safety/hooks.js).
|
|
2357
|
+
*
|
|
2358
|
+
* 조용히 삼키면 안 된다. 훅은 사람이 제 손으로 건 것이고, 그게
|
|
2359
|
+
* 도는지 안 도는지 볼 길이 없으면 아무도 못 믿는다.
|
|
2360
|
+
*/
|
|
2361
|
+
case 'hook_note':
|
|
2362
|
+
clearThinking();
|
|
2363
|
+
for (const 줄 of String(ev.말).split('\n').slice(0, 8)) {
|
|
2364
|
+
say(` ${c.gray(`· ${clip(줄, 80)}`)}`);
|
|
2365
|
+
}
|
|
2366
|
+
break;
|
|
2367
|
+
|
|
2368
|
+
case 'hook_block':
|
|
2369
|
+
clearThinking();
|
|
2370
|
+
say('');
|
|
2371
|
+
say(` ${c.yellow('✗')} ${c.white(옮긴말('ev.hookBlock'))} ${c.gray(`(${ev.훅?.이름 ?? ev.훅?.명령 ?? ''})`)}`);
|
|
2372
|
+
break;
|
|
2373
|
+
|
|
2303
2374
|
case 'nudge':
|
|
2304
2375
|
clearThinking();
|
|
2305
2376
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|