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.
- package/README.ko.md +3 -2
- package/README.md +3 -2
- package/bin/deel.js +8 -0
- package/package.json +1 -1
- package/src/acp/serve.js +4 -0
- package/src/agent/loop.js +38 -2
- package/src/agent/route.js +67 -1
- package/src/agent/session.js +13 -1
- package/src/backend/adapter.js +26 -4
- package/src/backend/loosejson.js +109 -0
- package/src/backend/mcp.js +586 -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 +1 -0
- package/src/exit.js +3 -1
- package/src/i18n/en.js +5 -0
- package/src/i18n/ja.js +5 -0
- package/src/i18n/ko.js +5 -0
- package/src/i18n/zh.js +5 -0
- package/src/lsp/diag.js +2 -1
- package/src/oneshot.js +143 -5
- package/src/repl.js +13 -1
- 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 +30 -1
- package/src/safety/which.js +37 -0
- 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/oneshot.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// 그래서 '묻는 자리' 를 전부 없앤 길을 따로 낸다. 에이전트 루프는 그대로 쓴다 —
|
|
12
12
|
// 여기서 루프를 다시 짜면 두 벌이 되고, 언젠가 한쪽만 고쳐진다.
|
|
13
13
|
import { statSync } from 'node:fs';
|
|
14
|
+
import { resolve } from 'node:path';
|
|
14
15
|
import { c, mark, clip, 화면글거르기 } from './ui/ansi.js';
|
|
15
16
|
import { 규칙모으기, 정책읽기, 승인바닥 } from './safety/policy.js';
|
|
16
17
|
import { 훅읽기 } from './safety/hooks.js';
|
|
@@ -19,7 +20,7 @@ import { 스키마읽기, 맞나, 답에서JSON뽑기, 시킬말 as 스키마시
|
|
|
19
20
|
import { 남길것읽기 } from './safety/shellenv.js';
|
|
20
21
|
import { 받기설정 } from './safety/authcmd.js';
|
|
21
22
|
import { run } from './agent/loop.js';
|
|
22
|
-
import { Session, 요청잘리나,
|
|
23
|
+
import { Session, 요청잘리나, 못박을길이, 생성속도, repairToolPairs } from './agent/session.js';
|
|
23
24
|
import { 양수크기 } from './agent/models.js';
|
|
24
25
|
import { estimateTokens } from './backend/tokens.js';
|
|
25
26
|
import { makeScope } from './safety/guard.js';
|
|
@@ -31,12 +32,14 @@ import { activeProfile, load, resolveKey, 소식줄들 } from './config.js';
|
|
|
31
32
|
import { 말 as 옮긴말 } from './i18n/index.js';
|
|
32
33
|
import { 알림채움, 알림말 } from './backend/retry.js';
|
|
33
34
|
import { 전선붙이기, 세션이름짓기 } from './backend/wire.js';
|
|
34
|
-
import { newId } from './agent/store.js';
|
|
35
|
+
import { newId, Store, latest, prune } from './agent/store.js';
|
|
36
|
+
import { 못박기 } from './agent/pins.js';
|
|
37
|
+
import { 바뀐파일모음, 사건적기열기 } from './runlog.js';
|
|
35
38
|
import { discover, loadCommand } from './skills/discover.js';
|
|
36
39
|
import { 다붙이기 } from './backend/mcp.js';
|
|
37
40
|
import { 명령들 } from './cmdnames.js';
|
|
38
|
-
// 슬래시 명령을 찾는 규칙은 대화 화면과 **같은 것 하나**를 쓴다 (
|
|
39
|
-
import { 슬래시명령찾기,
|
|
41
|
+
// 슬래시 명령을 찾는 규칙은 대화 화면과 **같은 것 하나**를 쓴다 (slash.js 의 슬래시명령찾기 머리말).
|
|
42
|
+
import { 슬래시명령찾기, 비슷한슬래시명령, MCP프롬프트펴기 } from './slash.js';
|
|
40
43
|
import { allowEndpoint, setOffline } from './safety/network.js';
|
|
41
44
|
import { 지금모드, 바깥인가, 나갈수있나, 봉인됐나 } from './safety/runmode.js';
|
|
42
45
|
import { 주소가리기 } from './safety/secrets.js';
|
|
@@ -124,8 +127,12 @@ export async function runOnce(opts = {}) {
|
|
|
124
127
|
let 알려진모델 = null;
|
|
125
128
|
// 붙인 MCP 서버. 아래 내놓기 가 닫는다 — 그래서 여기서 선언한다 (2.0.2 · 문 맞춤).
|
|
126
129
|
let mcp서버들 = [];
|
|
130
|
+
// --events 로 연 사건 파일 (runlog.js). 아래 내놓기 가 결과 줄을 적고 닫는다 (2.1.3).
|
|
131
|
+
let 사건 = null;
|
|
127
132
|
|
|
128
133
|
const 내놓기 = (r) => {
|
|
134
|
+
// 사건 파일의 마지막 줄은 --json 과 같은 결과 한 덩이다. 어느 끝맺음이든 여기를 지난다.
|
|
135
|
+
if (사건) { 사건.결과(r); 사건.닫기(); }
|
|
129
136
|
/*
|
|
130
137
|
* 뒤에서 돌던 명령을 반드시 거둔다.
|
|
131
138
|
*
|
|
@@ -234,6 +241,30 @@ export async function runOnce(opts = {}) {
|
|
|
234
241
|
if (opts.checkTimeout !== undefined && !(Number.isFinite(Number(opts.checkTimeout)) && Number(opts.checkTimeout) > 0)) {
|
|
235
242
|
return 못함('usage', `--check-timeout 은 0 보다 큰 초여야 합니다: ${opts.checkTimeout}`);
|
|
236
243
|
}
|
|
244
|
+
/*
|
|
245
|
+
* 값 없는 --resume (2.1.3). 대화 화면은 목록을 띄워 고르게 하지만 여기는 고를 사람이 없다 —
|
|
246
|
+
* 아무거나 집어 이으면 스크립트는 제가 무엇을 이었는지 모른다. 치는 자리에서 고칠 것이라 64 다.
|
|
247
|
+
*/
|
|
248
|
+
if (opts.resume === true) {
|
|
249
|
+
return 못함('usage', '--resume 에 이어 할 대화 이름을 주세요 (deel sessions 로 확인) — 가장 최근 것이면 --continue');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/*
|
|
253
|
+
* ── 사건 흐름 (--events <파일>) · 2.1.3 ──────────────────────────────
|
|
254
|
+
*
|
|
255
|
+
* 일을 시작하기 **전에** 연다. 다 돌고 나서 못 열었다고 하면 지켜보려던 사람은 아무것도 못 봤다.
|
|
256
|
+
* 경로는 --output-schema 처럼 부른 자리 기준이다.
|
|
257
|
+
*/
|
|
258
|
+
if (opts.events != null) {
|
|
259
|
+
const 경로 = resolve(String(opts.events));
|
|
260
|
+
try {
|
|
261
|
+
사건 = 사건적기열기(경로, {
|
|
262
|
+
알림: (왜) => 삐끗(` ${mark.warn} ${c.yellow(`사건 파일에 더 못 적습니다 (${왜}) — 일은 계속합니다`)}`),
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
return 못함('events', `사건 파일을 못 열었습니다 (--events): ${경로} — ${err?.code ?? err?.message ?? err}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
237
268
|
|
|
238
269
|
// ── 시킬 말 ───────────────────────────────────────────────────────────
|
|
239
270
|
// 인자로 준 것이 먼저다. 없을 때만 표준입력을 읽는다 —
|
|
@@ -252,6 +283,41 @@ export async function runOnce(opts = {}) {
|
|
|
252
283
|
* 고친다 — acp/serve.js 의 방만들기가 이미 고친 바로 그 섞임이다.
|
|
253
284
|
*/
|
|
254
285
|
const root = opts.root ? String(opts.root) : process.cwd();
|
|
286
|
+
|
|
287
|
+
/*
|
|
288
|
+
* ── 이어 할 대화 (--continue · --resume <이름>) · 2.1.3 ────────────────
|
|
289
|
+
*
|
|
290
|
+
* 여태 `deel run` 은 오간 말을 어디에도 안 남겼다. 스크립트가 「방금 그 일에 이어서 테스트도 고쳐」 를
|
|
291
|
+
* 시킬 길이 없어 매번 처음부터 설명했고, 모델은 방금 읽은 파일을 다시 읽었다. 이제 대화 화면과 같은
|
|
292
|
+
* 자리(.deel/sessions)에 적고, 같은 두 깃발로 잇는다 — 대화 화면에서 한 대화를 배치가 이어도 된다.
|
|
293
|
+
*
|
|
294
|
+
* 설정을 읽기 **전에** 찾는다. 없는 이름이면 모델을 부르기 전에 서야 한다. 대화 화면은 못 찾으면
|
|
295
|
+
* 새로 시작하지만 여기서는 선다 — 이름을 집어 준 스크립트는 그 대화에 이어진다고 믿고 「아까 그것」 을
|
|
296
|
+
* 시킨다. 처음 보는 모델이 그 말을 받으면 짐작으로 일을 하고 0 으로 끝난다. `--continue` 는 다르다 —
|
|
297
|
+
* 「있으면 잇고 없으면 새로」 가 스크립트의 첫 판이 기대하는 뜻이다.
|
|
298
|
+
*/
|
|
299
|
+
let 이어받은것 = null;
|
|
300
|
+
{
|
|
301
|
+
const 이을이름 = typeof opts.resume === 'string' ? opts.resume
|
|
302
|
+
: (opts.continue === true ? (latest(root)?.id ?? null) : null);
|
|
303
|
+
if (opts.continue === true && typeof opts.resume !== 'string' && !이을이름) {
|
|
304
|
+
곁(` ${c.gray('· 이어 할 대화가 없어 새로 시작합니다.')}`);
|
|
305
|
+
}
|
|
306
|
+
if (이을이름) {
|
|
307
|
+
const 곳 = new Store(root, 이을이름);
|
|
308
|
+
const { messages: 적힌것, 못읽음 } = 곳.이름틀림 ? { messages: [] } : 곳.load();
|
|
309
|
+
// 도구가 도는 중에 죽은 대화는 호출만 있고 결과가 없다 — 그대로 보내면 서버가 400 을 낸다 (repl.js 와 같다).
|
|
310
|
+
const { messages, 고친것 } = repairToolPairs(적힌것);
|
|
311
|
+
if (!messages.length) {
|
|
312
|
+
return 못함('no-session', 곳.이름틀림
|
|
313
|
+
? `대화 이름이 아닙니다 (--resume): ${이을이름}`
|
|
314
|
+
: 못읽음
|
|
315
|
+
? `그 대화를 못 읽었습니다 (--resume): ${이을이름} — ${못읽음}`
|
|
316
|
+
: `그런 대화가 없거나 비어 있습니다 (--resume): ${이을이름} — deel sessions 로 이름을 확인하세요`);
|
|
317
|
+
}
|
|
318
|
+
이어받은것 = { 곳, messages, 고친것 };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
255
321
|
/*
|
|
256
322
|
* 설정을 못 읽어도 **이 문의 끝맺음으로** 끝낸다.
|
|
257
323
|
*
|
|
@@ -361,6 +427,20 @@ export async function runOnce(opts = {}) {
|
|
|
361
427
|
// session 에 실어 두는 까닭은, 대화 도중 /model 로 옮겨도 같은 자리를 보게 하려는 것이다.
|
|
362
428
|
session.실행모드 = 실행모드;
|
|
363
429
|
|
|
430
|
+
/*
|
|
431
|
+
* 찾아 둔 대화를 싣는다 (위 「이어 할 대화」). 못 박은 것 · 남은 할 일 · 시킨 말 원문도 같이 —
|
|
432
|
+
* 오간 말만 돌아오고 그 셋이 빠지면 대화 화면에서 막아 둔 「껐다 켜면 까먹는다」 가 여기서 되살아난다.
|
|
433
|
+
* 적는 자리(store)는 아래 턴 바로 앞에서 연다 — 그 사이에 선 실패가 빈 대화 파일을 남기지 않게.
|
|
434
|
+
*/
|
|
435
|
+
if (이어받은것) {
|
|
436
|
+
session.messages = 이어받은것.messages;
|
|
437
|
+
const 박힌것 = 이어받은것.곳.못박은것읽기();
|
|
438
|
+
if (박힌것.length) session.못박은것 = new 못박기(박힌것);
|
|
439
|
+
이어받은것.곳.살림따라가기(session);
|
|
440
|
+
곁(` ${mark.ok} ${c.gray(`${이어받은것.곳.id} — 메시지 ${이어받은것.messages.length}개를 이어 받았습니다.`)}`
|
|
441
|
+
+ (이어받은것.고친것 ? ` ${c.gray(`(중단된 도구 호출 ${이어받은것.고친것}개는 걷어냈습니다)`)}` : ''));
|
|
442
|
+
}
|
|
443
|
+
|
|
364
444
|
/*
|
|
365
445
|
* ── 열쇠를 받아 오는 명령 (safety/authcmd.js) ───────────────────────
|
|
366
446
|
*
|
|
@@ -484,7 +564,13 @@ export async function runOnce(opts = {}) {
|
|
|
484
564
|
* 두 벌이니 한쪽만 고치면 또 갈린다. 그래서 규칙은 commands.js 한 군데에만 둔다.
|
|
485
565
|
*/
|
|
486
566
|
const 찾은 = 붙박이 ? null : 슬래시명령찾기(found.commands, 부른이름);
|
|
487
|
-
|
|
567
|
+
// MCP 서버가 낸 프롬프트도 대화 화면과 같이 부른다 (2.1.3 · commands.js 의 MCP프롬프트펴기).
|
|
568
|
+
const 프롬프트 = 붙박이 || 찾은 ? null : await MCP프롬프트펴기(mcp서버들, 부른이름, 인자);
|
|
569
|
+
if (프롬프트?.탈) return 못함('command-read', `/${부른이름} — ${프롬프트.탈}`);
|
|
570
|
+
if (프롬프트) {
|
|
571
|
+
곁(` ${c.cyan('⌘')} ${부른이름} ${c.gray(`MCP 프롬프트 · ${프롬프트.서버이름}`)}`);
|
|
572
|
+
시킬말 = 프롬프트.text;
|
|
573
|
+
} else if (붙박이) {
|
|
488
574
|
return 못함('repl-only', `/${부른이름} 은 대화 화면에서만 도는 명령입니다.`
|
|
489
575
|
+ ' 여기서 되는 것은 파일로 적어 둔 슬래시 명령뿐입니다 (.claude/commands · .deel/commands).');
|
|
490
576
|
} else if (찾은) {
|
|
@@ -673,6 +759,28 @@ export async function runOnce(opts = {}) {
|
|
|
673
759
|
}
|
|
674
760
|
}
|
|
675
761
|
|
|
762
|
+
/*
|
|
763
|
+
* ── 오간 말을 남긴다 (2.1.3) ─────────────────────────────────────────
|
|
764
|
+
*
|
|
765
|
+
* 대화 화면처럼 걸음마다 적는다. 끝에 한 번에 적으면 걸음 수 상한이나 Ctrl+C 로 선 잡의 대화가
|
|
766
|
+
* 통째로 없다 — 이어서 시킬 일이 제일 많은 게 바로 그 잡이다. 여기까지 온 뒤에 연다 — 위에서 선
|
|
767
|
+
* 실패(없는 명령 · 너무 큰 말)가 머리글만 있는 빈 대화를 남기지 않게.
|
|
768
|
+
*/
|
|
769
|
+
const store = 이어받은것?.곳 ?? new Store(root);
|
|
770
|
+
store.begin({ model: conn.model, base: 주소가리기(conn.base), root });
|
|
771
|
+
if (!이어받은것) store.살림따라가기(session);
|
|
772
|
+
// 지금 이어 쓰는 대화는 정리에서 뺀다 — 한 달 넘은 대화를 이으면 바로 여기서 지워졌다 (store.js 의 prune 머리말).
|
|
773
|
+
try { prune(root, { 남길것: [store.id] }); } catch { /* 못 치워도 이 잡은 돈다 — 다음 판이 다시 해 본다 */ }
|
|
774
|
+
let 적은데까지 = session.messages.length; // 이어 받은 말은 이미 그 파일에 있다
|
|
775
|
+
const 적기 = () => {
|
|
776
|
+
// 접기 · 비우기가 이력을 통째로 바꿨는데 그 사건을 못 봤으면(모양 되물음 도중) 덧붙이지 말고 새로 적는다.
|
|
777
|
+
if (session.messages.length < 적은데까지) store.replace(session.messages, '압축');
|
|
778
|
+
else for (const m of session.messages.slice(적은데까지)) store.append(m);
|
|
779
|
+
적은데까지 = session.messages.length;
|
|
780
|
+
};
|
|
781
|
+
// 무엇이 바뀌었나 — --json 의 files (runlog.js).
|
|
782
|
+
const 바뀐 = 바뀐파일모음(root);
|
|
783
|
+
|
|
676
784
|
// ── 한 턴 ─────────────────────────────────────────────────────────────
|
|
677
785
|
const turn = new AbortController();
|
|
678
786
|
const 끊김 = () => { if (!turn.signal.aborted) turn.abort(); };
|
|
@@ -697,10 +805,13 @@ export async function runOnce(opts = {}) {
|
|
|
697
805
|
const 안쪽 = (ev) => (ev.depth ? ' '.repeat(ev.depth) : '');
|
|
698
806
|
|
|
699
807
|
for await (const ev of run(session, ctx, 보낼글, { signal: turn.signal })) {
|
|
808
|
+
사건?.적기(ev); // 지켜보는 쪽에 먼저 흘린다 (--events)
|
|
809
|
+
바뀐.사건(ev);
|
|
700
810
|
switch (ev.type) {
|
|
701
811
|
case 'stage':
|
|
702
812
|
steps = ev.step;
|
|
703
813
|
이번단계글 = '';
|
|
814
|
+
적기(); // 걸음마다 적어 둔다 — 도중에 죽어도 여기까지는 남는다
|
|
704
815
|
break;
|
|
705
816
|
|
|
706
817
|
// 중간 단계의 말은 표준출력에 안 싣는다. "이제 파일을 읽어보겠습니다" 까지
|
|
@@ -711,6 +822,7 @@ export async function runOnce(opts = {}) {
|
|
|
711
822
|
|
|
712
823
|
case 'tool': {
|
|
713
824
|
tools++;
|
|
825
|
+
적기();
|
|
714
826
|
/*
|
|
715
827
|
* 실패는 실패로 보여야 한다.
|
|
716
828
|
*
|
|
@@ -817,6 +929,9 @@ export async function runOnce(opts = {}) {
|
|
|
817
929
|
? ` ${c.cyan('◱')} ${c.yellow(`요약을 못 받아 옛 대화 ${ev.folded}개를 잘라 냈습니다 (${ev.before.toLocaleString()} → ${ev.after.toLocaleString()} 토큰)`)}`
|
|
818
930
|
+ (ev.why ? ` ${c.gray(`— ${clip(String(ev.why), 100)}`)}` : '')
|
|
819
931
|
: ` ${c.cyan('◱')} ${c.gray(`대화 ${ev.folded}개를 요약으로 접었습니다 (${ev.before.toLocaleString()} → ${ev.after.toLocaleString()} 토큰)`)}`);
|
|
932
|
+
// 접히면 이력이 통째로 바뀐다. 덧붙이면 접히기 전과 뒤가 겹쳐 남으니 새로 적는다 (repl.js 와 같다).
|
|
933
|
+
store.replace(session.messages, ev.fallback ? `압축 못 함 — 옛 대화 ${ev.folded}개를 잘라 냄` : `압축 — ${ev.folded}개를 요약으로`);
|
|
934
|
+
적은데까지 = session.messages.length;
|
|
820
935
|
break;
|
|
821
936
|
|
|
822
937
|
case 'compact_failed':
|
|
@@ -875,6 +990,10 @@ export async function runOnce(opts = {}) {
|
|
|
875
990
|
} else 곁(` ${c.yellow('⊘')} ${c.gray(옮긴말('check.skipped', { 까닭: ev.까닭 }))}`);
|
|
876
991
|
break;
|
|
877
992
|
|
|
993
|
+
case 'textcalls':
|
|
994
|
+
곁(` ${c.gray(`↳ ${옮긴말('ev.textCalls', { n: ev.count, 이름: ev.names.join(', ') })}`)}`);
|
|
995
|
+
break;
|
|
996
|
+
|
|
878
997
|
case 'nudge':
|
|
879
998
|
곁(` ${c.gray(`↺ ${ev.why === '요청누락'
|
|
880
999
|
? 옮긴말('ev.nudgeMissed', { n: ev.빠진?.length ?? 0 })
|
|
@@ -915,6 +1034,8 @@ export async function runOnce(opts = {}) {
|
|
|
915
1034
|
곁(` ${c.cyan('◱')} ${c.gray((ev.kept?.할일 ?? 0)
|
|
916
1035
|
? 옮긴말('ev.reset', { 버린수: ev.dropped, 할일: ev.kept.할일 })
|
|
917
1036
|
: 옮긴말('ev.resetBare', { 버린수: ev.dropped }))}`);
|
|
1037
|
+
store.replace(session.messages, `자리부족 — 앞선 대화 ${ev.dropped}개를 비움`);
|
|
1038
|
+
적은데까지 = session.messages.length;
|
|
918
1039
|
/*
|
|
919
1040
|
* ── 비우면서 **시킨 말 뒤가 잘렸으면** 여기서 선다 (사냥5 B5-01) ────────────
|
|
920
1041
|
*
|
|
@@ -1050,6 +1171,7 @@ export async function runOnce(opts = {}) {
|
|
|
1050
1171
|
let 다시터짐 = '';
|
|
1051
1172
|
try {
|
|
1052
1173
|
for await (const ev of run(session, ctx, 스키마시킬말(출력스키마, { 다시: 잰것.탈 }), { signal: turn.signal, 고쳐쓰기: true })) {
|
|
1174
|
+
사건?.적기(ev);
|
|
1053
1175
|
if (ev.type === 'content') 다시글 += ev.text;
|
|
1054
1176
|
else if (ev.type === 'done') 다시글 = ev.text ?? 다시글;
|
|
1055
1177
|
else if (ev.type === 'aborted') { 다시끊김 = true; break; }
|
|
@@ -1115,12 +1237,22 @@ export async function runOnce(opts = {}) {
|
|
|
1115
1237
|
*/
|
|
1116
1238
|
process.removeListener('SIGINT', 끊김);
|
|
1117
1239
|
|
|
1240
|
+
/*
|
|
1241
|
+
* 남은 말을 다 적고, 못 적은 것이 있으면 **말한다.** 이어 하려던 스크립트가 다음 판에서야
|
|
1242
|
+
* 「그 대화가 반쪽이다」 를 알면 늦다 — 그래서 못 적은 판은 --json 의 session 을 비운다.
|
|
1243
|
+
*/
|
|
1244
|
+
try { 적기(); } catch { /* 셈은 store 가 한다 — 아래에서 본다 */ }
|
|
1245
|
+
const 저장탈 = store.처음못쓴것();
|
|
1246
|
+
if (저장탈) 삐끗(` ${mark.warn} ${c.yellow(옮긴말('run.notSaving', { n: 저장탈.수, 까닭: 저장탈.까닭 }))}`);
|
|
1247
|
+
const 대화이름 = store.이름틀림 || store.못쓴것() ? null : store.id;
|
|
1248
|
+
|
|
1118
1249
|
const code = EXIT[reason] ?? EXIT.error;
|
|
1119
1250
|
if (why) 삐끗(` ${reason === 'done' ? c.gray('·') : c.red('✗')} ${why}`);
|
|
1120
1251
|
if (!json && !quiet) {
|
|
1121
1252
|
const 조각 = [`${((Date.now() - t0) / 1000).toFixed(1)}초`];
|
|
1122
1253
|
if (tools) 조각.push(`도구 ${tools}회`);
|
|
1123
1254
|
조각.push(`↑${(session.usage.prompt || session.usage.in).toLocaleString()} ↓${session.usage.out.toLocaleString()}`);
|
|
1255
|
+
if (생성속도(session.usage) !== null) 조각.push(`${생성속도(session.usage)} tok/s`);
|
|
1124
1256
|
곁(` ${c.gray('── ' + 조각.join(' · '))}`);
|
|
1125
1257
|
}
|
|
1126
1258
|
|
|
@@ -1146,8 +1278,14 @@ export async function runOnce(opts = {}) {
|
|
|
1146
1278
|
// 이 칸이 없으면 받는 쪽이 0 을 실측으로 읽는다 (backend/adapter.js 의 잰것).
|
|
1147
1279
|
못잰것: session.usage.못잰것 ?? 0,
|
|
1148
1280
|
retries: session.usage.retries ?? 0,
|
|
1281
|
+
// 생성 속도 (2.1.3). 흘려받은 부름만 잰다 — 못 쟀으면 null (session.js 의 생성속도).
|
|
1282
|
+
genMs: session.usage.genMs ?? 0,
|
|
1283
|
+
tokPerSec: 생성속도(session.usage),
|
|
1149
1284
|
},
|
|
1150
1285
|
model: conn.model,
|
|
1286
|
+
// 바뀐 파일 (작업 폴더 기준 · 슬래시) 과 이 대화의 이름 — `--resume <이름>` 으로 잇는다 (2.1.3).
|
|
1287
|
+
files: 바뀐.목록(),
|
|
1288
|
+
session: 대화이름,
|
|
1151
1289
|
// 모양을 못 박았을 때만 실린다. --json 으로 받는 쪽은 text 를 다시 파싱할
|
|
1152
1290
|
// 필요 없이 이 칸을 그대로 쓰면 된다.
|
|
1153
1291
|
...(스키마값 !== null ? { schema: 스키마값 } : {}),
|
package/src/repl.js
CHANGED
|
@@ -22,7 +22,7 @@ import { handle, COMMANDS, 미리보기끄기, 적용하기 as 그림적용 } fr
|
|
|
22
22
|
import { next as nextWork, get as getWork, canWrite, 보일이름, 보일한줄 } from './agent/modes.js';
|
|
23
23
|
import { route } from './agent/route.js';
|
|
24
24
|
import { run } from './agent/loop.js';
|
|
25
|
-
import { Session, repairToolPairs } from './agent/session.js';
|
|
25
|
+
import { Session, repairToolPairs, 생성속도 } from './agent/session.js';
|
|
26
26
|
import { makeScope } from './safety/guard.js';
|
|
27
27
|
import { 언어서버있나 } from './tools/index.js';
|
|
28
28
|
import { 모두끄기 as 언어서버다끄기 } from './lsp/client.js';
|
|
@@ -2224,6 +2224,8 @@ export async function chatLoop(opts = {}) {
|
|
|
2224
2224
|
// 캐시가 걸린 턴일수록 실제보다 비싸게 뜬다.
|
|
2225
2225
|
cacheRead: session.usage.cacheRead ?? 0,
|
|
2226
2226
|
cacheWrite: session.usage.cacheWrite ?? 0,
|
|
2227
|
+
genMs: session.usage.genMs ?? 0,
|
|
2228
|
+
genOut: session.usage.genOut ?? 0,
|
|
2227
2229
|
};
|
|
2228
2230
|
|
|
2229
2231
|
/*
|
|
@@ -3042,6 +3044,13 @@ export async function chatLoop(opts = {}) {
|
|
|
3042
3044
|
}
|
|
3043
3045
|
break;
|
|
3044
3046
|
|
|
3047
|
+
// 글로 적은 도구 부름을 건졌다 (backend/textcalls.js) — 흘러 나온 표 글 뒤에 무엇을 했는지 한 줄.
|
|
3048
|
+
case 'textcalls':
|
|
3049
|
+
clearThinking();
|
|
3050
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
3051
|
+
say(` ${c.gray(`↳ ${옮긴말('ev.textCalls', { n: ev.count, 이름: ev.names.join(', ') })}`)}`);
|
|
3052
|
+
break;
|
|
3053
|
+
|
|
3045
3054
|
case 'nudge':
|
|
3046
3055
|
clearThinking();
|
|
3047
3056
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
@@ -3216,6 +3225,9 @@ export async function chatLoop(opts = {}) {
|
|
|
3216
3225
|
const d읽음 = (session.usage.cacheRead ?? 0) - before.cacheRead;
|
|
3217
3226
|
const d씀 = (session.usage.cacheWrite ?? 0) - before.cacheWrite;
|
|
3218
3227
|
if (dIn || dOut) bits.push(`↑${dIn.toLocaleString()} ↓${dOut.toLocaleString()}`);
|
|
3228
|
+
// 이번 턴의 생성 속도 — 흘려받은 부름만 잰다(session.js 의 생성속도). 못 쟀으면 안 적는다.
|
|
3229
|
+
const 이번속도 = 생성속도({ genMs: (session.usage.genMs ?? 0) - before.genMs, genOut: (session.usage.genOut ?? 0) - before.genOut });
|
|
3230
|
+
if (이번속도 !== null) bits.push(`${이번속도} tok/s`);
|
|
3219
3231
|
/*
|
|
3220
3232
|
* ── ↑ 안을 가른다 ────────────────────────────────────────────────────
|
|
3221
3233
|
*
|
package/src/runlog.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// `deel run` 이 밖으로 내는 두 가지 — 바뀐 파일 목록과 사건 흐름 (2.1.3).
|
|
2
|
+
//
|
|
3
|
+
// oneshot.js 에서 따로 둔다. 거기는 이미 천 줄이 넘고, 이 둘은 「루프가 내는 사건을 받아 밖에 적는다」
|
|
4
|
+
// 는 한 가지 일만 한다 — 루프의 속을 몰라도 되고, 그래서 따로 잴 수 있다.
|
|
5
|
+
|
|
6
|
+
import { openSync, writeSync, closeSync } from 'node:fs';
|
|
7
|
+
import { resolve, relative, isAbsolute, sep } from 'node:path';
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* ── 바뀐 파일 ─────────────────────────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* 루프는 턴이 끝날 때 손댄 파일을 `done` · `stuck` · `limit` 에 실어 준다(agent/loop.js 의 마무리).
|
|
13
|
+
* 그런데 끊기거나(aborted) 터진(error) 턴은 그 목록 없이 끝난다 — 스크립트가 제일 알고 싶은 것이
|
|
14
|
+
* 「반쯤 하다 멈췄는데 무엇을 건드렸나」 인데. 그래서 도구 사건마다 루프와 같은 세 칸을 보고 모은다.
|
|
15
|
+
* 끝 사건이 준 목록도 합친다 — 하위 작업이 바꾼 것은 거기에만 온다.
|
|
16
|
+
*
|
|
17
|
+
* 작업 폴더 기준 경로로, 슬래시로 적는다. 스크립트는 `jq -r '.files[]' | xargs git add` 처럼 쓴다 —
|
|
18
|
+
* 윈도우 역슬래시나 절대 경로가 섞이면 그 한 줄이 운영체제마다 달라진다. 폴더 밖은 절대 경로 그대로 둔다.
|
|
19
|
+
*/
|
|
20
|
+
export function 바뀐파일모음(root) {
|
|
21
|
+
const 모음 = new Set();
|
|
22
|
+
const 담기 = (p) => {
|
|
23
|
+
if (typeof p !== 'string' || !p) return;
|
|
24
|
+
const 온경로 = resolve(root, p);
|
|
25
|
+
const 상대 = relative(root, 온경로);
|
|
26
|
+
const 밖인가 = !상대 || 상대.startsWith('..') || isAbsolute(상대);
|
|
27
|
+
모음.add((밖인가 ? 온경로 : 상대).split(sep).join('/'));
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
사건(ev) {
|
|
31
|
+
if (!ev || typeof ev !== 'object') return;
|
|
32
|
+
if (ev.type === 'tool' && ev.result && !ev.result.error) {
|
|
33
|
+
const r = ev.result;
|
|
34
|
+
담기(r.changed);
|
|
35
|
+
for (const f of r.여럿 ?? []) if (f?.ok) 담기(f.path);
|
|
36
|
+
for (const f of r.바뀐것들 ?? []) 담기(f);
|
|
37
|
+
}
|
|
38
|
+
for (const f of Array.isArray(ev.files) ? ev.files : []) 담기(f?.path);
|
|
39
|
+
},
|
|
40
|
+
목록: () => [...모음].sort(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* ── 사건 흐름 (--events <파일>) ───────────────────────────────────────
|
|
46
|
+
*
|
|
47
|
+
* 끝날 때 한 덩이만 나오면 오래 도는 잡을 지켜볼 수가 없다. 사건을 한 줄에 하나씩 JSON 으로 흘린다 —
|
|
48
|
+
* `tail -f` 로 보고, 끝나면 `jq` 로 훑는다. 마지막 줄은 `--json` 과 같은 결과 한 덩이다(type: 'result').
|
|
49
|
+
*
|
|
50
|
+
* 글자 조각(content · thinking)은 안 싣는다. 흘려받는 서버면 한 턴에 수천 줄이 되고, 답은 마지막 줄에
|
|
51
|
+
* 이미 통째로 있다. 긴 글은 잘라 싣는다 — 큰 파일을 읽은 도구 결과 하나가 한 줄에 통째로 실리면
|
|
52
|
+
* 사건 파일이 읽은 파일들의 사본이 된다.
|
|
53
|
+
*
|
|
54
|
+
* `type` 칸은 약속이다. 나머지 칸은 판마다 늘 수 있다 — 루프가 화면에 주는 것을 그대로 옮긴다.
|
|
55
|
+
*/
|
|
56
|
+
export const 사건글한도 = 4000;
|
|
57
|
+
const 안싣는것 = new Set(['content', 'thinking']);
|
|
58
|
+
|
|
59
|
+
export function 사건줄(ev, 흐른ms, { 통째 = false } = {}) {
|
|
60
|
+
const 자르기 = (_k, v) => (typeof v === 'string' && v.length > 사건글한도
|
|
61
|
+
? `${v.slice(0, 사건글한도)}…(+${v.length - 사건글한도})`
|
|
62
|
+
: v);
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify({ ...ev, elapsed: 흐른ms }, 통째 ? undefined : 자르기);
|
|
65
|
+
} catch {
|
|
66
|
+
// 제자리를 도는 값 같은 것. 무엇이 있었는지는 남긴다 — 통째로 빼면 그 자리가 비어 보인다.
|
|
67
|
+
return JSON.stringify({ type: String(ev?.type ?? '?'), elapsed: 흐른ms, 못적음: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 사건 파일을 연다. 못 열면 던진다 — 부르는 쪽이 일을 시작하기 **전에** 선다.
|
|
73
|
+
*
|
|
74
|
+
* 다 돌고 나서 「사건 파일을 못 열었습니다」 면 지켜보려던 사람은 아무것도 못 봤다. 도중에 못 쓰게
|
|
75
|
+
* 되면(디스크 가득) 한 번 알리고 그만 적는다 — 기록을 남기려다 일을 죽이면 본말이 뒤집힌다.
|
|
76
|
+
*/
|
|
77
|
+
export function 사건적기열기(경로, { 시작 = Date.now(), 알림 = () => {} } = {}) {
|
|
78
|
+
const fd = openSync(경로, 'w');
|
|
79
|
+
let 막힘 = false;
|
|
80
|
+
let 닫힘 = false;
|
|
81
|
+
const 한줄 = (글) => {
|
|
82
|
+
if (막힘 || 닫힘) return;
|
|
83
|
+
try { writeSync(fd, `${글}\n`); }
|
|
84
|
+
catch (err) { 막힘 = true; 알림(err?.code ?? err?.message ?? String(err)); }
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
적기(ev) {
|
|
88
|
+
if (!ev || 안싣는것.has(ev.type)) return;
|
|
89
|
+
한줄(사건줄(ev, Date.now() - 시작));
|
|
90
|
+
},
|
|
91
|
+
// 결과 줄은 자르지 않는다 — 답이 거기에 통째로 있다는 것이 위 약속이다(--json 과 같은 덩이).
|
|
92
|
+
결과(r) { 한줄(사건줄({ type: 'result', ...r }, Date.now() - 시작, { 통째: true })); },
|
|
93
|
+
닫기() {
|
|
94
|
+
if (닫힘) return;
|
|
95
|
+
닫힘 = true;
|
|
96
|
+
try { closeSync(fd); } catch { /* 이미 닫혔으면 그만 */ }
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/safety/authcmd.js
CHANGED
|
@@ -69,14 +69,29 @@ let 마지막명령줄 = null;
|
|
|
69
69
|
*/
|
|
70
70
|
export function 마지막명령() { return 마지막명령줄; }
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
/*
|
|
73
|
+
* ── 받아 둔 것과 승인은 **명령마다** 따로 든다 (2.1.3) ───────────────────────
|
|
74
|
+
*
|
|
75
|
+
* 둘 다 모듈에 하나뿐이었다. 한 판에 연결이 하나면 그래도 맞다 — 그런데 `/consult` 와 모델을 고른
|
|
76
|
+
* Task 는 **다른 프로필의 연결**을 연다(agent/models.js). 그 연결이 제 명령으로 열쇠를 청하면 들고
|
|
77
|
+
* 있던 앞 프로필의 토큰이 먼저 돌아와, A 게이트웨이의 토큰이 B 게이트웨이로 나갔다. B 의 명령은
|
|
78
|
+
* 묻는 자리에 닿지도 않았고, 닿았어도 A 를 허락한 답이 B 의 답으로 읽혔다.
|
|
79
|
+
*
|
|
80
|
+
* 열쇠는 명령줄로 가른다 — 같은 명령이면 같은 열쇠다(받기설정 이 정책·설정 어느 쪽이든 명령을 준다).
|
|
81
|
+
*/
|
|
82
|
+
const 받은것들 = new Map(); // 명령 → { token, headers, 만료, 언제 } — 메모리에만. 파일로 안 나간다.
|
|
83
|
+
const 물어본것들 = new Map(); // 명령 → 이번 판에 사람이 뭐라고 답했나 (true/false)
|
|
84
|
+
let 마지막받은 = null; // 화면이 연결을 안 줄 때 보여 줄 것
|
|
74
85
|
|
|
75
86
|
/** 검사와 `/model` 갈아타기가 부른다. */
|
|
76
|
-
export function 잊기() {
|
|
87
|
+
export function 잊기() { 받은것들.clear(); 물어본것들.clear(); 마지막받은 = null; 마지막명령줄 = null; }
|
|
77
88
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
89
|
+
/**
|
|
90
|
+
* 지금 들고 있는 것. 없으면 null. 화면·심사서가 **토큰 없이** 상태만 볼 때.
|
|
91
|
+
* @param {object} [설정] 받기설정() 이 준 것 — 주면 그 명령의 것을, 안 주면 마지막에 받은 것을.
|
|
92
|
+
*/
|
|
93
|
+
export function 지금상태(설정 = null) {
|
|
94
|
+
const 받은것 = 설정 ? 받은것들.get(설정.명령) : 마지막받은;
|
|
80
95
|
if (!받은것) return null;
|
|
81
96
|
return { 있음: true, 만료: 받은것.만료, 남은초: Math.max(0, Math.round((받은것.만료 - Date.now()) / 1000)) };
|
|
82
97
|
}
|
|
@@ -251,16 +266,45 @@ export async function 한번받기(설정, { 기다림 = 기본기다림, signal
|
|
|
251
266
|
// 잘못된 것이고, 열어 두면 그 자리에서 영영 멈춘다.
|
|
252
267
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
253
268
|
windowsHide: true,
|
|
269
|
+
// 유닉스는 셸을 새 무리의 머리로 띄운다 — 끊을 때 무리째 거두게(거두기). 윈도우에서 detached 는 새 콘솔
|
|
270
|
+
// 창이라 안 쓴다(그쪽은 taskkill /T 가 나무를 따라간다). tools/index.js 의 Bash 와 같은 짝이다.
|
|
271
|
+
detached: process.platform !== 'win32',
|
|
254
272
|
});
|
|
255
273
|
} catch (err) {
|
|
256
274
|
return done({ ok: false, 왜: `명령을 못 띄웠습니다 (${err.message})`, 보인것: '' });
|
|
257
275
|
}
|
|
258
276
|
const 마치기 = (것) => { if (!끝났나) { 끝났나 = true; clearTimeout(시계); 끊기해제(); done(것); } };
|
|
277
|
+
/*
|
|
278
|
+
* ── 끊을 때는 **나무째** 거두고 우리 파이프를 놓는다 (2.1.3) ────────────────
|
|
279
|
+
*
|
|
280
|
+
* 명령은 셸을 거쳐 뜬다(tools/shell.js 의 셸명령). kill 은 셸만 죽이고, 윈도우에서는 그 밑의 진짜
|
|
281
|
+
* 로그인 도구(손자)가 살아 stdout 을 물고 있었다. 그러면 「못 받았습니다」 뒤에도 그 파이프가 deel 을
|
|
282
|
+
* 붙들어, 할 일을 다 마친 `deel run` 이 손자가 끝날 때까지 안 끝났다. 윈도우는 셸이 살아 있을 때
|
|
283
|
+
* taskkill /T 로 나무를 따라가야 하므로 kill 은 그 뒤에 한다(backend/mcp.js 닫기 와 같은 차례).
|
|
284
|
+
* 유닉스도 같았다 — 셸만 죽고 손자가 남았다(리눅스 CI 가 잡음). 무리(-pid)째 끊는다.
|
|
285
|
+
*/
|
|
286
|
+
const 거두기 = () => {
|
|
287
|
+
const 죽이기 = () => {
|
|
288
|
+
if (process.platform !== 'win32' && kid.pid) {
|
|
289
|
+
try { process.kill(-kid.pid, 'SIGKILL'); return; } catch { /* 무리가 없다 — 셸만 */ }
|
|
290
|
+
}
|
|
291
|
+
try { kid.kill('SIGKILL'); } catch { /* 이미 죽었으면 그만 */ }
|
|
292
|
+
};
|
|
293
|
+
if (process.platform === 'win32' && kid.pid && kid.exitCode === null) {
|
|
294
|
+
try {
|
|
295
|
+
const 나무 = spawn('taskkill', ['/pid', String(kid.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
296
|
+
나무.once('error', 죽이기);
|
|
297
|
+
나무.once('exit', 죽이기);
|
|
298
|
+
나무.unref();
|
|
299
|
+
} catch { 죽이기(); }
|
|
300
|
+
} else 죽이기();
|
|
301
|
+
try { kid.stdout?.destroy(); kid.stderr?.destroy(); } catch { /* 이미 닫힘 */ }
|
|
302
|
+
};
|
|
259
303
|
const 시계 = setTimeout(() => {
|
|
260
|
-
|
|
304
|
+
거두기();
|
|
261
305
|
마치기({ ok: false, 왜: `${Math.round(기다림 / 1000)}초를 기다렸는데 안 끝났습니다`, 보인것: '' });
|
|
262
306
|
}, 기다림);
|
|
263
|
-
const 끊기 = () => {
|
|
307
|
+
const 끊기 = () => { 거두기(); 마치기({ ok: false, 왜: '중단했습니다', 보인것: '' }); };
|
|
264
308
|
const 끊기해제 = () => signal?.removeEventListener?.('abort', 끊기);
|
|
265
309
|
if (signal?.aborted) return 끊기();
|
|
266
310
|
signal?.addEventListener?.('abort', 끊기, { once: true });
|
|
@@ -280,8 +324,8 @@ export async function 한번받기(설정, { 기다림 = 기본기다림, signal
|
|
|
280
324
|
kid.stdout.on('data', (b) => {
|
|
281
325
|
if (끝났나) return;
|
|
282
326
|
if (나온것.length + b.length > 받을상한) {
|
|
283
|
-
|
|
284
|
-
|
|
327
|
+
// 나무째 거두고 파이프를 놓는다 (위 거두기) — 셸 아래 손자가 계속 쓰고 있어도 EPIPE 를 맞고 멈춘다.
|
|
328
|
+
거두기();
|
|
285
329
|
// 먼저 자르고 가린다 — 1MB 짜리 한 줄을 통째로 가리기 에 넣으면 그 무늬들이
|
|
286
330
|
// 몇 분을 돈다(처음 고칠 때 검사가 여기서 멈춰 섰다).
|
|
287
331
|
const 첫줄 = 가리기(나온것.slice(0, 400).trim().split('\n')[0] ?? '', {}).글.slice(0, 120);
|
|
@@ -324,7 +368,8 @@ export async function 한번받기(설정, { 기다림 = 기본기다림, signal
|
|
|
324
368
|
export async function 열쇠(설정, { 다시 = false, 물어보기 = null, signal = null, 알림 = null, 기다림 = 기본기다림 } = {}) {
|
|
325
369
|
if (!설정) return { ok: false, 왜: '열쇠받기가 설정되어 있지 않습니다' };
|
|
326
370
|
|
|
327
|
-
if (다시)
|
|
371
|
+
if (다시) 받은것들.delete(설정.명령);
|
|
372
|
+
const 받은것 = 받은것들.get(설정.명령);
|
|
328
373
|
if (받은것 && 받은것.만료 - 미리 > Date.now()) {
|
|
329
374
|
return { ok: true, token: 받은것.token, headers: 받은것.headers, 만료: 받은것.만료, 그대로: true };
|
|
330
375
|
}
|
|
@@ -356,19 +401,22 @@ export async function 열쇠(설정, { 다시 = false, 물어보기 = null, sign
|
|
|
356
401
|
* 거기서까지 막으면 사람이 승인을 했는데도 열쇠가 안 붙는다.
|
|
357
402
|
*/
|
|
358
403
|
if (설정.곳 !== '정책') {
|
|
359
|
-
|
|
360
|
-
if (
|
|
404
|
+
const 답 = 물어본것들.get(설정.명령);
|
|
405
|
+
if (답 === false) return { ok: false, 왜: '이 판에서는 안 부르기로 했습니다' };
|
|
406
|
+
if (답 === undefined) {
|
|
361
407
|
if (typeof 물어보기 !== 'function') {
|
|
362
408
|
return { ok: false, 왜: '이 명령을 띄워도 되는지 물어볼 자리가 없습니다 — 승인 없이는 안 띄웁니다' };
|
|
363
409
|
}
|
|
364
|
-
|
|
365
|
-
|
|
410
|
+
const 허락 = !!(await 물어보기(설정));
|
|
411
|
+
물어본것들.set(설정.명령, 허락);
|
|
412
|
+
if (!허락) return { ok: false, 왜: '이 판에서는 안 부르기로 했습니다' };
|
|
366
413
|
}
|
|
367
414
|
}
|
|
368
415
|
|
|
369
416
|
const r = await 한번받기(설정, { signal, 알림, 기다림 });
|
|
370
417
|
if (!r.ok) return r;
|
|
371
|
-
|
|
418
|
+
마지막받은 = { token: r.token, headers: r.headers, 만료: r.만료, 언제: Date.now() };
|
|
419
|
+
받은것들.set(설정.명령, 마지막받은);
|
|
372
420
|
return { ok: true, token: r.token, headers: r.headers, 만료: r.만료, ms: r.ms, 그대로: false };
|
|
373
421
|
}
|
|
374
422
|
|
|
@@ -380,7 +428,10 @@ export async function 열쇠(설정, { 다시 = false, 물어보기 = null, sign
|
|
|
380
428
|
*/
|
|
381
429
|
export function 가림(글) {
|
|
382
430
|
const 것들 = [];
|
|
383
|
-
|
|
384
|
-
for (const
|
|
431
|
+
// 들고 있는 것 **전부**를 가린다 — 명령마다 따로 들게 된 뒤로(위 받은것들) 하나만 가리면 다른 연결의 토큰이 샌다.
|
|
432
|
+
for (const 받은것 of 받은것들.values()) {
|
|
433
|
+
if (받은것?.token) 것들.push(받은것.token);
|
|
434
|
+
for (const v of Object.values(받은것?.headers ?? {})) if (v) 것들.push(String(v));
|
|
435
|
+
}
|
|
385
436
|
return 가리기(String(글 ?? ''), { 열쇠들: 것들 }).글;
|
|
386
437
|
}
|