deel-local-cli 1.5.7 → 1.6.0
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.en.md +24 -13
- package/README.md +23 -12
- package/bin/deel.js +10 -0
- package/package.json +2 -2
- package/src/acp/map.js +143 -0
- package/src/acp/serve.js +160 -13
- package/src/agent/commit.js +511 -0
- package/src/agent/compact.js +269 -226
- package/src/agent/loop.js +154 -12
- package/src/agent/mention.js +56 -10
- package/src/agent/review.js +192 -0
- package/src/agent/session.js +20 -2
- package/src/agent/threads.js +1 -1
- package/src/backend/adapter.js +342 -280
- package/src/backend/azure.js +151 -0
- package/src/backend/ctxsize.js +19 -0
- package/src/backend/detect.js +106 -0
- package/src/backend/http.js +350 -30
- package/src/backend/probe.js +48 -1
- package/src/backend/proxy.js +151 -0
- package/src/backend/quota.js +133 -0
- package/src/backend/retry.js +132 -0
- package/src/backend/vision.js +185 -0
- package/src/commands.js +341 -8
- package/src/completion.js +264 -0
- package/src/config.js +117 -1
- package/src/i18n/en.js +13 -1
- package/src/i18n/index.js +22 -3
- package/src/i18n/ja.js +266 -0
- package/src/i18n/ko.js +12 -0
- package/src/i18n/zh.js +266 -0
- package/src/oneshot.js +28 -3
- package/src/pack/sbom.js +25 -0
- package/src/pack/selfpack.js +9 -3
- package/src/plugins/manage.js +34 -7
- package/src/repl.js +113 -15
- package/src/report.js +26 -2
- package/src/safety/guard.js +82 -3
- package/src/safety/keystore.js +237 -0
- package/src/safety/network.js +76 -14
- package/src/safety/policy.js +209 -0
- package/src/safety/secrets.js +32 -0
- package/src/setup.js +40 -7
- package/src/tools/clipboard.js +178 -0
- package/src/tools/fastgrep.js +217 -0
- package/src/tools/fsutil.js +55 -12
- package/src/tools/ignore.js +192 -0
- package/src/tools/index.js +282 -39
- package/src/tools/jobs.js +4 -12
- package/src/tools/outline.js +3 -1
- package/src/tools/pdf.js +1326 -0
- package/src/tools/shell.js +123 -0
- package/src/tools/verify.js +20 -4
- package/src/tools/webfetch.js +18 -5
- package/src/ui/inputbox.js +2 -2
- package/src/ui/screen.js +4 -1
- package/src/ui/status.js +22 -0
package/src/oneshot.js
CHANGED
|
@@ -11,13 +11,16 @@
|
|
|
11
11
|
// 그래서 '묻는 자리' 를 전부 없앤 길을 따로 낸다. 에이전트 루프는 그대로 쓴다 —
|
|
12
12
|
// 여기서 루프를 다시 짜면 두 벌이 되고, 언젠가 한쪽만 고쳐진다.
|
|
13
13
|
import { c, mark, clip } from './ui/ansi.js';
|
|
14
|
+
import { 규칙모으기 } from './safety/policy.js';
|
|
14
15
|
import { run } from './agent/loop.js';
|
|
15
16
|
import { Session } from './agent/session.js';
|
|
16
17
|
import { makeScope } from './safety/guard.js';
|
|
17
18
|
import { 모두끄기 as 언어서버다끄기 } from './lsp/client.js';
|
|
18
19
|
import { History } from './safety/undo.js';
|
|
19
20
|
import { Audit } from './safety/audit.js';
|
|
20
|
-
import { activeProfile, load, resolveKey } from './config.js';
|
|
21
|
+
import { activeProfile, load, resolveKey, 잠금소식 } from './config.js';
|
|
22
|
+
import { 말 as 옮긴말 } from './i18n/index.js';
|
|
23
|
+
import { 알림채움 } from './backend/retry.js';
|
|
21
24
|
import { discover } from './skills/discover.js';
|
|
22
25
|
import { allowEndpoint, setOffline } from './safety/network.js';
|
|
23
26
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
@@ -123,7 +126,7 @@ export async function runOnce(opts = {}) {
|
|
|
123
126
|
삐끗(` ${c.red('✗')} ${message}`);
|
|
124
127
|
return 내놓기({
|
|
125
128
|
ok: false, reason, code: EXIT.error, text: '', why: message,
|
|
126
|
-
tools: 0, steps: 0, usage: { in: 0, out: 0, calls: 0, ms: 0 }, ms: 0,
|
|
129
|
+
tools: 0, steps: 0, usage: { in: 0, out: 0, calls: 0, ms: 0, retries: 0 }, ms: 0,
|
|
127
130
|
});
|
|
128
131
|
};
|
|
129
132
|
|
|
@@ -140,6 +143,11 @@ export async function runOnce(opts = {}) {
|
|
|
140
143
|
const prof = activeProfile(cfg);
|
|
141
144
|
if (!prof) return 못함('no-config', '저장된 연결이 없습니다. deel setup 을 먼저 실행하세요.');
|
|
142
145
|
|
|
146
|
+
// 열쇠를 방금 잠갔으면 알린다. 여기서는 stdout 을 안 쓴다 —
|
|
147
|
+
// 배치로 부르는 쪽이 stdout 을 JSON 으로 읽고 있을 수 있다.
|
|
148
|
+
const 잠금 = 잠금소식();
|
|
149
|
+
if (잠금) { try { process.stderr.write(` ${잠금}\n`); } catch { /* 못 써도 그만 */ } }
|
|
150
|
+
|
|
143
151
|
const root = opts.root ? String(opts.root) : process.cwd();
|
|
144
152
|
const conn = {
|
|
145
153
|
kind: prof.kind, base: prof.baseUrl, auth: prof.auth,
|
|
@@ -149,12 +157,14 @@ export async function runOnce(opts = {}) {
|
|
|
149
157
|
maxTokens: opts.maxTokens ?? prof.maxTokens ?? null,
|
|
150
158
|
streaming: prof.streaming ?? false,
|
|
151
159
|
tools: prof.tools ?? false, json: prof.json ?? false, think: prof.think ?? false,
|
|
160
|
+
vision: prof.vision ?? false,
|
|
152
161
|
};
|
|
153
162
|
|
|
154
163
|
// 자물쇠는 대화 화면과 똑같이 건다. 비대화라고 느슨해질 이유가 없다 —
|
|
155
164
|
// 오히려 배치는 아무도 안 보는 자리라 더 단단해야 한다.
|
|
156
165
|
allowEndpoint(conn.base);
|
|
157
|
-
|
|
166
|
+
// 관리 정책이 offline 을 못박아 뒀으면 옵션과 상관없이 켠다 (safety/policy.js).
|
|
167
|
+
if (opts.offline ?? prof.offline ?? cfg?.offline) setOffline(true);
|
|
158
168
|
|
|
159
169
|
const session = new Session(conn, {
|
|
160
170
|
root,
|
|
@@ -185,6 +195,10 @@ export async function runOnce(opts = {}) {
|
|
|
185
195
|
// 도구가 한 번에 돌려줄 양을 이 값에서 뽑는다 (agent/budget.js).
|
|
186
196
|
// /model 로 갈아타면 conn 이 통째로 바뀌므로 그때마다 다시 읽는다.
|
|
187
197
|
get 모델컨텍스트() { return conn.ctx ?? null; },
|
|
198
|
+
// 이 모델이 그림을 볼 수 있나 — Read 가 그림을 만났을 때 무슨 말을 할지가 여기서 갈린다.
|
|
199
|
+
get 눈있나() { return !!conn.vision; },
|
|
200
|
+
// 적어 둔 허락·금지 규칙 (safety/policy.js). 승인 모드보다 먼저 본다.
|
|
201
|
+
규칙들: 규칙모으기(cfg),
|
|
188
202
|
history: new History(root),
|
|
189
203
|
audit: new Audit(root),
|
|
190
204
|
seen: new Set(),
|
|
@@ -337,10 +351,20 @@ export async function runOnce(opts = {}) {
|
|
|
337
351
|
곁(` ${c.yellow('↻')} ${c.gray(`${ev.why} — 상한을 ${ev.from} → ${ev.to} 로 올려 다시 부릅니다`)}`);
|
|
338
352
|
break;
|
|
339
353
|
|
|
354
|
+
// 서버가 잠깐 막아서 기다렸다 다시 부른 자리 (backend/retry.js).
|
|
355
|
+
// 배치 기록에 남아야 "그날 밤 왜 12분이 걸렸나" 를 나중에 읽을 수 있다.
|
|
356
|
+
case 'backoff':
|
|
357
|
+
곁(` ${c.yellow('↻')} ${c.gray(옮긴말('loop.backoff', 알림채움(ev)))}`);
|
|
358
|
+
break;
|
|
359
|
+
|
|
340
360
|
case 'folded':
|
|
341
361
|
곁(` ${c.cyan('◲')} ${c.gray(`오래된 도구 결과 ${ev.접은것}개를 접었습니다 (${ev.아낀토큰.toLocaleString()} 토큰을 비움)`)}`);
|
|
342
362
|
break;
|
|
343
363
|
|
|
364
|
+
case 'images_folded':
|
|
365
|
+
곁(` ${c.cyan('◲')} ${c.gray(`오래된 그림 ${(ev.뺀것들 ?? []).reduce((a3, x) => a3 + x.장수, 0)}장을 뺐습니다`)}`);
|
|
366
|
+
break;
|
|
367
|
+
|
|
344
368
|
case 'compacted':
|
|
345
369
|
곁(` ${c.cyan('◱')} ${c.gray(`대화 ${ev.folded}개를 요약으로 접었습니다 (${ev.before.toLocaleString()} → ${ev.after.toLocaleString()} 토큰)`)}`);
|
|
346
370
|
break;
|
|
@@ -405,6 +429,7 @@ export async function runOnce(opts = {}) {
|
|
|
405
429
|
usage: {
|
|
406
430
|
in: session.usage.in, out: session.usage.out,
|
|
407
431
|
calls: session.usage.calls, ms: session.usage.ms,
|
|
432
|
+
retries: session.usage.retries ?? 0,
|
|
408
433
|
},
|
|
409
434
|
model: conn.model,
|
|
410
435
|
ms: Date.now() - t0,
|
package/src/pack/sbom.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// 검사에서 확인한다 (test/sbom.test.js) — Audit 이 실제로 남긴 줄에 여기
|
|
25
25
|
// 적어 둔 칸이 다 있는지 본다.
|
|
26
26
|
import { randomUUID } from 'node:crypto';
|
|
27
|
+
import { 보관방식 } from '../safety/keystore.js';
|
|
27
28
|
|
|
28
29
|
/** 우리가 내놓는 SBOM 규격. 스캐너가 이 숫자를 보고 읽는 법을 정한다. */
|
|
29
30
|
export const CDX판 = '1.5';
|
|
@@ -102,6 +103,7 @@ export function 통신명세(a) {
|
|
|
102
103
|
어디로: 'deel setup 에서 정한 주소 딱 한 곳',
|
|
103
104
|
무엇이: '오간 말과, 모델이 읽은 파일 내용',
|
|
104
105
|
막는법: 'src/safety/network.js 가 요청마다 허용 목록을 확인합니다. 목록에 없으면 요청 자체를 만들지 않습니다.',
|
|
106
|
+
프록시: 'HTTPS_PROXY(또는 설정의 proxy)가 있으면 그 프록시를 거칩니다(CONNECT 터널). 목적지 허용 목록은 그대로이고, 이 컴퓨터 안 주소는 프록시를 안 탑니다.',
|
|
105
107
|
소스: 자리('net'),
|
|
106
108
|
},
|
|
107
109
|
{
|
|
@@ -146,6 +148,28 @@ export function 통신명세(a) {
|
|
|
146
148
|
};
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
/**
|
|
152
|
+
* 열쇠를 어디에 어떻게 두나.
|
|
153
|
+
*
|
|
154
|
+
* 심사에서 거의 언제나 나오는 질문이고, 전에는 이 문서에 답이 없었다.
|
|
155
|
+
* "홈 폴더에 평문" 이면 그 자리에서 끝나는 질문이라 더 그렇다.
|
|
156
|
+
* 지금 이 PC 에서 실제로 되는 방식을 그대로 적는다 — 되는 척은 안 한다.
|
|
157
|
+
*/
|
|
158
|
+
export function 열쇠명세() {
|
|
159
|
+
return {
|
|
160
|
+
어디: '~/.deel/config.json (또는 DEEL_HOME) 의 profiles[].apiKey',
|
|
161
|
+
이PC에서: 보관방식(),
|
|
162
|
+
방식: [
|
|
163
|
+
{ 운영체제: '윈도우', 방법: 'DPAPI ProtectedData (CurrentUser) — 이 PC 의 이 계정만 풉니다. 파일을 복사해 가도 못 풉니다.' },
|
|
164
|
+
{ 운영체제: '맥', 방법: '로그인 키체인 (security add-generic-password)' },
|
|
165
|
+
{ 운영체제: '그 밖', 방법: '파일 권한 0600. 잠금장치가 없으므로 그렇게 적습니다.' },
|
|
166
|
+
],
|
|
167
|
+
넘길때: '열쇠는 명령줄에 안 올립니다 — 언제나 stdin 으로 넘깁니다. 같은 PC 의 다른 사용자가 프로세스 목록으로 못 봅니다.',
|
|
168
|
+
안쓰려면: 'DEEL_API_KEY (또는 DEEL_KEY_<프로필>) 환경변수를 쓰면 파일에 아예 안 남습니다. 환경변수가 파일보다 우선합니다.',
|
|
169
|
+
끄려면: 'DEEL_KEYSTORE=off — 잠금장치를 안 씁니다(파일 권한만).',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
149
173
|
/**
|
|
150
174
|
* 감사기록 사양.
|
|
151
175
|
*
|
|
@@ -201,6 +225,7 @@ export function 심사명세(a, { at = new Date() } = {}) {
|
|
|
201
225
|
: '외부 모듈이 있습니다 — 위 목록을 확인하세요.',
|
|
202
226
|
},
|
|
203
227
|
통신: 통신명세(a),
|
|
228
|
+
열쇠보관: 열쇠명세(),
|
|
204
229
|
감사기록: 감사명세(),
|
|
205
230
|
파일: a.files.map((f) => ({ 경로: f.path, 바이트: f.bytes, sha256: f.sha })),
|
|
206
231
|
};
|
package/src/pack/selfpack.js
CHANGED
|
@@ -45,8 +45,10 @@ const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');
|
|
|
45
45
|
// 앞에 점이나 글자가 붙은 것은 뺀다. 그러지 않으면 정규식의 .exec( 까지
|
|
46
46
|
// '외부 명령 실행' 으로 세어 심사서가 거짓말을 한다.
|
|
47
47
|
const PROBES = [
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// fetch 말고도 http.js 의 프록시 터널(httpRequest·httpsRequest·tlsConnect)이 있다.
|
|
49
|
+
// 그것을 안 세면 심사서가 "네트워크 요청 N건" 을 거짓으로 적는다.
|
|
50
|
+
{ id: 'net', label: '네트워크 요청', re: /(?<![.\w])fetch\s*\(|(?:globalThis|window|self)\s*\.\s*fetch\s*\(|\bnew\s+WebSocket\s*\(|(?<![.\w])(?:httpRequest|httpsRequest|tlsConnect)\s*\(/g,
|
|
51
|
+
note: '사용자가 setup 에서 넣은 주소로만 나갑니다 (HTTPS_PROXY 가 있으면 그 프록시를 거쳐서)' },
|
|
50
52
|
{ id: 'exec', label: '외부 명령 실행', re: /(?<![.\w])(execFile|execFileSync|execSync|spawn|spawnSync|exec)\s*\(/g,
|
|
51
53
|
note: '사용자·모델이 지시한 명령, 그리고 플러그인 받을 때의 git' },
|
|
52
54
|
{ id: 'listen', label: '포트 열기', re: /(?<![.\w])(createServer)\s*\(/g,
|
|
@@ -83,7 +85,8 @@ const MODULE_NAME = /^[@\w./:-]+$/;
|
|
|
83
85
|
|
|
84
86
|
export function importSpecs(text) {
|
|
85
87
|
const out = [];
|
|
86
|
-
|
|
88
|
+
// 동적으로 부르는 import 도 문이다 — from 이 없어서 못 잡던 것을 잡는다.
|
|
89
|
+
const re = /(?:^|[\s;{(])(?:import[^'"()]*from\s*|import\s*\(\s*|import\s*|require\s*\(\s*)(['"])([^'"]+)\1/g;
|
|
87
90
|
for (const m of text.matchAll(re)) {
|
|
88
91
|
const spec = m[2];
|
|
89
92
|
if (MODULE_NAME.test(spec)) out.push(spec);
|
|
@@ -171,6 +174,9 @@ export function reviewSheet(a, at) {
|
|
|
171
174
|
L.push(' · 그 한 자리만 허용 목록에 오릅니다. 모델을 바꾸면 앞의 자리는 닫힙니다.');
|
|
172
175
|
L.push(' · src/safety/network.js 가 요청마다 확인하고, 목록에 없으면 요청 자체를');
|
|
173
176
|
L.push(' 만들지 않습니다.');
|
|
177
|
+
L.push(' · HTTPS_PROXY(또는 설정의 proxy)가 있으면 그 프록시를 **거쳐** 갑니다 (CONNECT');
|
|
178
|
+
L.push(' 터널). 목적지는 그대로 그 한 자리이고, 거치는 프록시는 켤 때 화면과 /status 에');
|
|
179
|
+
L.push(' 적힙니다. 이 컴퓨터 안(127.*, localhost)으로 가는 요청은 프록시를 안 탑니다.');
|
|
174
180
|
L.push('');
|
|
175
181
|
L.push(' [B] 웹 읽기 (WebFetch 도구) — 받아 오기만 하는 길');
|
|
176
182
|
L.push(' · GET 만 씁니다. 본문을 실어 보내지 않습니다 (소스·대화가 나갈 수 없음).');
|
package/src/plugins/manage.js
CHANGED
|
@@ -11,7 +11,19 @@ import {
|
|
|
11
11
|
} from 'node:fs';
|
|
12
12
|
import { untargz, stripTop } from '../pack/tar.js';
|
|
13
13
|
import { makeZip } from '../pack/zip.js';
|
|
14
|
-
import { allowTemporarily,
|
|
14
|
+
import { allowTemporarily, isOffline, NetBlocked } from '../safety/network.js';
|
|
15
|
+
import { 원시요청, 몸읽기 } from '../backend/http.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 플러그인을 받다 되돌림(redirect)이 오면 따라가도 되는 곳인가. 던지면 안 따라간다.
|
|
19
|
+
* github 가 제 다른 집으로 보내는 것만 따라간다 — https 이고, github.com · githubusercontent.com 뿐.
|
|
20
|
+
*/
|
|
21
|
+
export function 플러그인되돌림(다음) {
|
|
22
|
+
if (다음.protocol !== 'https:') throw new Error(`${다음.protocol} 로 되돌립니다 — 따라가지 않습니다`);
|
|
23
|
+
if (!/(^|\.)github\.com$|(^|\.)githubusercontent\.com$/i.test(다음.hostname)) {
|
|
24
|
+
throw new Error(`github 밖(${다음.hostname})으로 되돌립니다 — 따라가지 않습니다`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
15
27
|
import { copyDir } from '../tools/fsutil.js';
|
|
16
28
|
|
|
17
29
|
export const pluginsDir = (home = homedir()) => join(home, '.deel', 'plugins');
|
|
@@ -63,12 +75,27 @@ async function fetchInto(spec, dest, onStep) {
|
|
|
63
75
|
const url = `https://codeload.github.com/${spec.owner}/${spec.repo}/tar.gz/refs/heads/${branch}`;
|
|
64
76
|
// 사용자가 이 명령을 친 동안만 github 를 연다. 끝나면 바로 닫는다.
|
|
65
77
|
const close = allowTemporarily(url);
|
|
66
|
-
let
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
78
|
+
let gz;
|
|
79
|
+
// 한 문(backend/http.js)으로 나간다 — 프록시를 거치고, 되돌림은 홉마다 문지기를 지난다.
|
|
80
|
+
// github 는 받기 주소를 제 다른 집(githubusercontent 등)으로 되돌리므로 그 홉은 열어 준다 —
|
|
81
|
+
// 그 밖의 집이면 안 따라가고, 왜 못 받았는지를 말한다 (조용히 '주소를 확인하세요' 로 끝내지 않는다).
|
|
82
|
+
// 상한까지만 받는다. 플러그인 묶음이 64MB 를 넘을 일은 없고, 넘는 것을 다 받아 줄 이유도 없다.
|
|
83
|
+
const 열어둔 = [];
|
|
84
|
+
try {
|
|
85
|
+
const res = await 원시요청(url, {
|
|
86
|
+
timeout: 120000, stream: true,
|
|
87
|
+
되돌림: (다음) => { 플러그인되돌림(다음); 열어둔.push(allowTemporarily(다음.origin)); },
|
|
88
|
+
});
|
|
89
|
+
if (!res.ok) { await res.버리기?.(); continue; }
|
|
90
|
+
gz = await 몸읽기(res.res.body, 64 * 1024 * 1024);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (err instanceof NetBlocked || /되돌립니다/.test(String(err?.message))) return { error: `받지 못했습니다 — ${err.message}` };
|
|
93
|
+
continue;
|
|
94
|
+
} finally {
|
|
95
|
+
close();
|
|
96
|
+
for (const 닫기 of 열어둔) 닫기();
|
|
97
|
+
}
|
|
98
|
+
if (!gz) return { error: '받은 묶음이 너무 큽니다 (64MB 넘음) — 플러그인 저장소가 맞는지 확인하세요' };
|
|
72
99
|
const files = stripTop(untargz(gz));
|
|
73
100
|
if (!files.length) return { error: '받은 묶음이 비어 있습니다' };
|
|
74
101
|
rmSync(dest, { recursive: true, force: true });
|
package/src/repl.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// 대화 화면. 루프가 보내는 이벤트를 Claude Code 풍으로 그린다.
|
|
2
2
|
import { createInterface, emitKeypressEvents } from 'node:readline';
|
|
3
|
+
import { 규칙모으기 } from './safety/policy.js';
|
|
4
|
+
import { 주소가리기 } from './safety/secrets.js';
|
|
3
5
|
import { homedir } from 'node:os';
|
|
4
6
|
import { resolve, basename } from 'node:path';
|
|
5
7
|
import { c, say as 바로쓰기, mark, clip } from './ui/ansi.js';
|
|
@@ -7,6 +9,7 @@ import { headerLines } from './ui/status.js';
|
|
|
7
9
|
import { 종, 창제목, 제목되돌리기, 알릴까, 제목글 } from './ui/notify.js';
|
|
8
10
|
import { 보이기 as 인트로, 기본곁말 } from './ui/intro.js';
|
|
9
11
|
import { 언어잡기, 말 as 옮긴말 } from './i18n/index.js';
|
|
12
|
+
import { 알림채움 } from './backend/retry.js';
|
|
10
13
|
import { 화면고르기 } from './ui/screen.js';
|
|
11
14
|
import { STAGES } from './agent/effort.js';
|
|
12
15
|
import { handle, COMMANDS, 미리보기끄기, 적용하기 as 그림적용 } from './commands.js';
|
|
@@ -19,7 +22,7 @@ import { 언어서버있나 } from './tools/index.js';
|
|
|
19
22
|
import { 모두끄기 as 언어서버다끄기 } from './lsp/client.js';
|
|
20
23
|
import { History } from './safety/undo.js';
|
|
21
24
|
import { Audit } from './safety/audit.js';
|
|
22
|
-
import { activeProfile, load, resolveKey, save as saveCfg, homeDir } from './config.js';
|
|
25
|
+
import { activeProfile, load, resolveKey, save as saveCfg, homeDir, 잠금소식 } from './config.js';
|
|
23
26
|
import { discover } from './skills/discover.js';
|
|
24
27
|
import { allowEndpoint, setOffline, isOffline, isLocalHost } from './safety/network.js';
|
|
25
28
|
import { Store, latest, prune } from './agent/store.js';
|
|
@@ -36,6 +39,7 @@ import { 접어쓰기 } from './ui/wrap.js';
|
|
|
36
39
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
37
40
|
import { renderDiff, shortStat } from './ui/diff.js';
|
|
38
41
|
import { expand as expandMentions } from './agent/mention.js';
|
|
42
|
+
import { 크기말 } from './backend/vision.js';
|
|
39
43
|
import { 다붙이기 } from './backend/mcp.js';
|
|
40
44
|
import { 읽기 as 기억읽기 } from './agent/memory.js';
|
|
41
45
|
import { 갈래고르기 } from './ui/working.js';
|
|
@@ -150,6 +154,14 @@ export async function chatLoop(opts = {}) {
|
|
|
150
154
|
return 1;
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
// 평문으로 있던 열쇠를 방금 잠갔으면 그렇다고 한 줄. 조용히 바꾸면
|
|
158
|
+
// 나중에 설정 파일을 열어 본 사람이 제 열쇠가 사라진 줄 안다.
|
|
159
|
+
const 잠금 = 잠금소식();
|
|
160
|
+
if (잠금) {
|
|
161
|
+
바로쓰기('');
|
|
162
|
+
바로쓰기(` ${mark.ok} ${잠금}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
153
165
|
/*
|
|
154
166
|
* 여기서부터 화면에 나가는 것은 전부 `화면` 을 거친다.
|
|
155
167
|
*
|
|
@@ -178,11 +190,14 @@ export async function chatLoop(opts = {}) {
|
|
|
178
190
|
maxTokens: opts.maxTokens ?? prof.maxTokens ?? null,
|
|
179
191
|
streaming: prof.streaming ?? false,
|
|
180
192
|
tools: prof.tools ?? false, json: prof.json ?? false, think: prof.think ?? false,
|
|
193
|
+
// 그림을 볼 수 있는 모델인지. 못 보면 바이트를 아예 안 싣는다 (backend/vision.js).
|
|
194
|
+
vision: prof.vision ?? false,
|
|
181
195
|
};
|
|
182
196
|
|
|
183
197
|
// 이 자리 하나만 연다. 다른 어디로도 나가지 못한다.
|
|
184
198
|
allowEndpoint(conn.base);
|
|
185
|
-
|
|
199
|
+
// 관리 정책이 offline 을 못박아 뒀으면 옵션과 상관없이 켠다 (safety/policy.js).
|
|
200
|
+
if (opts.offline ?? prof.offline ?? cfg?.offline) setOffline(true);
|
|
186
201
|
|
|
187
202
|
/*
|
|
188
203
|
* ── 끝났을 때 알리기 ─────────────────────────────────────────────────
|
|
@@ -259,7 +274,7 @@ export async function chatLoop(opts = {}) {
|
|
|
259
274
|
}
|
|
260
275
|
}
|
|
261
276
|
if (!store) store = new Store(root);
|
|
262
|
-
store.begin({ model: conn.model, base: conn.base, root });
|
|
277
|
+
store.begin({ model: conn.model, base: 주소가리기(conn.base), root });
|
|
263
278
|
try { prune(root); } catch {}
|
|
264
279
|
|
|
265
280
|
/*
|
|
@@ -361,6 +376,15 @@ export async function chatLoop(opts = {}) {
|
|
|
361
376
|
* 잘못 읽힌다. 백틱은 그럴 일이 없다.
|
|
362
377
|
*/
|
|
363
378
|
let 이어쓰기줄들 = null;
|
|
379
|
+
/*
|
|
380
|
+
* 쌓인 줄을 이미 화면에 찍어 두었나.
|
|
381
|
+
*
|
|
382
|
+
* 줄 끝 백틱으로 이어 쓸 때는 줄쌓기() 가 치는 족족 찍는다. 붙여넣기는
|
|
383
|
+
* 일부러 안 찍는다(스무 줄이 주르륵 지나가지 않게). 보낼 때 무엇을 다시
|
|
384
|
+
* 찍을지가 그것으로 갈린다 — 이걸 안 가르면 같은 줄이 두 번 보이거나
|
|
385
|
+
* 붙여넣은 앞부분이 통째로 안 보인다.
|
|
386
|
+
*/
|
|
387
|
+
let 이어쓴것 = false;
|
|
364
388
|
|
|
365
389
|
/*
|
|
366
390
|
* ── 상자 안에서 줄을 바꾼다 ────────────────────────────────────────────
|
|
@@ -412,6 +436,7 @@ export async function chatLoop(opts = {}) {
|
|
|
412
436
|
/** 줄을 쌓아 두고 "다음 줄" 이라고 알린다. 백틱 표시와 Alt+Enter 가 같이 쓴다. */
|
|
413
437
|
const 줄쌓기 = (줄) => {
|
|
414
438
|
const 첫줄 = 이어쓰기줄들 === null;
|
|
439
|
+
이어쓴것 = true; // 아래에서 바로 찍는다
|
|
415
440
|
(이어쓰기줄들 ??= []).push(줄);
|
|
416
441
|
화면.입력지움();
|
|
417
442
|
say(`${첫줄 ? ` ${c.hcyan('❯')} ` : ' '}${c.white(줄)}`);
|
|
@@ -449,13 +474,29 @@ export async function chatLoop(opts = {}) {
|
|
|
449
474
|
// 올렸을 때 답만 있고 무엇을 물었는지가 없다.
|
|
450
475
|
const 이었나 = 이어쓰기줄들 !== null;
|
|
451
476
|
보낼것 = 이었나 ? [...이어쓰기줄들, l].join('\n') : l;
|
|
452
|
-
이어쓰기줄들 = null;
|
|
477
|
+
이어쓰기줄들 = null; 이어쓴것 = false;
|
|
453
478
|
화면.입력지움();
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
479
|
+
/*
|
|
480
|
+
* ── 보낸 것을 **보낸 그대로** 남긴다 ──────────────────────────────
|
|
481
|
+
*
|
|
482
|
+
* 여기서 `l` 만 찍고 있었다. 그런데 `l` 은 **마지막 줄**뿐이다 —
|
|
483
|
+
* 앞줄들은 이어쓰기줄들 에 쌓여 있다. 줄 끝 백틱으로 이어 쓸 때는
|
|
484
|
+
* 줄쌓기() 가 치는 족족 화면에 찍어 두었으니 그걸로 맞았는데,
|
|
485
|
+
* 붙여넣기는 일부러 안 찍는다(스무 줄이 주르륵 지나가지 않게).
|
|
486
|
+
*
|
|
487
|
+
* 그래서 붙여넣고 Enter 를 치면 **보낸 것의 마지막 조각만** 화면에
|
|
488
|
+
* 남았다. 사람은 앞부분이 안 갔다고 생각하고 다시 붙여넣는다.
|
|
489
|
+
*
|
|
490
|
+
* 찍을 것은 보낼것 이다. 화면에 남는 것과 모델에게 가는 것이 같아야
|
|
491
|
+
* 나중에 스크롤을 올려서 무엇을 시켰는지 알아볼 수 있다.
|
|
492
|
+
*
|
|
493
|
+
* 백틱으로 이어 쓴 경우에는 앞줄이 이미 찍혀 있으므로 마지막 줄만
|
|
494
|
+
* 이어 찍는다 — 안 그러면 같은 줄이 두 번 보인다.
|
|
495
|
+
*/
|
|
496
|
+
const 찍을것 = 이어쓴것 ? l : 보낼것;
|
|
497
|
+
if (찍을것.trim() || 이었나) {
|
|
498
|
+
for (const [i, 한줄] of String(찍을것).split('\n').entries()) {
|
|
499
|
+
say(`${i === 0 && !이어쓴것 ? ` ${c.hcyan('❯')} ` : ' '}${c.white(한줄)}`);
|
|
459
500
|
}
|
|
460
501
|
}
|
|
461
502
|
} else if (l.trim() || 이어쓰기줄들 !== null) {
|
|
@@ -483,7 +524,7 @@ export async function chatLoop(opts = {}) {
|
|
|
483
524
|
}
|
|
484
525
|
if (이어쓰기줄들 !== null) {
|
|
485
526
|
보낼것 = [...이어쓰기줄들, l].join('\n');
|
|
486
|
-
이어쓰기줄들 = null;
|
|
527
|
+
이어쓰기줄들 = null; 이어쓴것 = false;
|
|
487
528
|
}
|
|
488
529
|
화면.대기갱신('', queue.length + 1);
|
|
489
530
|
}
|
|
@@ -706,7 +747,7 @@ export async function chatLoop(opts = {}) {
|
|
|
706
747
|
* y 를 쳐도 화면에 아무것도 안 나타난다 — 먹은 건지 안 먹은 건지 모른다.
|
|
707
748
|
*/
|
|
708
749
|
묻는중 = 상자쓰나 ? 앞 : null;
|
|
709
|
-
이어쓰기줄들 = null; // 되묻는 사이에 걸쳐 있던 미완성 이어쓰기는 버린다
|
|
750
|
+
이어쓰기줄들 = null; 이어쓴것 = false; // 되묻는 사이에 걸쳐 있던 미완성 이어쓰기는 버린다
|
|
710
751
|
try {
|
|
711
752
|
const a = await nextLine();
|
|
712
753
|
if (a === null) return o.def ?? '';
|
|
@@ -760,6 +801,10 @@ export async function chatLoop(opts = {}) {
|
|
|
760
801
|
// 도구가 한 번에 돌려줄 양을 이 값에서 뽑는다 (agent/budget.js).
|
|
761
802
|
// /model 로 갈아타면 conn 이 통째로 바뀌므로 그때마다 다시 읽는다.
|
|
762
803
|
get 모델컨텍스트() { return conn.ctx ?? null; },
|
|
804
|
+
// 이 모델이 그림을 볼 수 있나 — Read 가 그림을 만났을 때 무슨 말을 할지가 여기서 갈린다.
|
|
805
|
+
get 눈있나() { return !!conn.vision; },
|
|
806
|
+
// 적어 둔 허락·금지 규칙 (safety/policy.js). 승인 모드보다 먼저 본다.
|
|
807
|
+
규칙들: 규칙모으기(cfg),
|
|
763
808
|
history: new History(root),
|
|
764
809
|
audit: new Audit(root),
|
|
765
810
|
seen: new Set(),
|
|
@@ -948,7 +993,7 @@ export async function chatLoop(opts = {}) {
|
|
|
948
993
|
* 대화 갈래. 연결·도구·되돌리기는 같이 쓰고 오간 말만 여러 벌 갖는다.
|
|
949
994
|
* 갈래마다 저장 파일을 따로 열어서, 나중에 `/sessions` 로 각각 찾아갈 수 있다.
|
|
950
995
|
*/
|
|
951
|
-
ctx.갈래 = new Threads(session, ctx, () => new Store(root).begin({ model: conn.model, base: conn.base, root }), store);
|
|
996
|
+
ctx.갈래 = new Threads(session, ctx, () => new Store(root).begin({ model: conn.model, base: 주소가리기(conn.base), root }), store);
|
|
952
997
|
|
|
953
998
|
// ── 컨텍스트 길이를 모델에서 긁어온다 ─────────────────────────────────
|
|
954
999
|
//
|
|
@@ -1087,7 +1132,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1087
1132
|
* 더는 '/' 로 시작하지 않아 명령으로 안 읽히고 모델에게 넘어간다.
|
|
1088
1133
|
*/
|
|
1089
1134
|
if (이어쓰기줄들 !== null) {
|
|
1090
|
-
이어쓰기줄들 = null;
|
|
1135
|
+
이어쓰기줄들 = null; 이어쓴것 = false;
|
|
1091
1136
|
say('');
|
|
1092
1137
|
say(` ${c.gray('이어쓰던 것을 버렸습니다.')}`);
|
|
1093
1138
|
}
|
|
@@ -1173,15 +1218,19 @@ export async function chatLoop(opts = {}) {
|
|
|
1173
1218
|
// 붙인 것은 화면에 반드시 알린다. 사람이 안 보낸 줄 아는 글이 대화에
|
|
1174
1219
|
// 들어가 있으면 안 된다 — 컨텍스트가 왜 줄었는지도 모르게 된다.
|
|
1175
1220
|
let 보낼글 = toSend;
|
|
1221
|
+
let 보낼그림 = null;
|
|
1176
1222
|
if (toSend.includes('@')) {
|
|
1177
1223
|
const 예산 = Math.min(20000, Math.floor((session.conn.ctx ?? CTX_DEFAULT) * 0.25));
|
|
1178
1224
|
const r = expandMentions(toSend, {
|
|
1179
1225
|
scope: ctx.scope, budget: 예산, seen: ctx.seen,
|
|
1180
1226
|
onRead: (p, t) => session.noteRead(p, t),
|
|
1227
|
+
눈있나: !!session.conn.vision,
|
|
1181
1228
|
});
|
|
1182
1229
|
보낼글 = r.text;
|
|
1230
|
+
보낼그림 = r.그림들?.length ? r.그림들 : null;
|
|
1183
1231
|
for (const a of r.attached) {
|
|
1184
|
-
|
|
1232
|
+
const 꼬리 = a.그림 ? c.gray(` (그림 · ${크기말(a.bytes)})`) : (a.full ? '' : c.gray(' (앞부분만)'));
|
|
1233
|
+
say(` ${c.blue('◧')} ${c.gray('붙임')} ${c.white(a.show)}${꼬리}`);
|
|
1185
1234
|
}
|
|
1186
1235
|
for (const b of r.blocked) {
|
|
1187
1236
|
say(` ${mark.warn} ${c.gray(`${b.path} 는 작업 범위 밖이라 안 붙였습니다.`)}`);
|
|
@@ -1287,7 +1336,23 @@ export async function chatLoop(opts = {}) {
|
|
|
1287
1336
|
// '겪을수록 나아진다' 가 이번 대화 안에서도 참이 된다.
|
|
1288
1337
|
ctx.카드다시?.();
|
|
1289
1338
|
try {
|
|
1290
|
-
|
|
1339
|
+
/*
|
|
1340
|
+
* 일하는 중에 친 말을 걸음마다 건네준다 (agent/loop.js 의 끼어들기).
|
|
1341
|
+
*
|
|
1342
|
+
* **슬래시 명령은 안 건넨다.** /mode 나 /undo 는 설정과 파일을 건드리는
|
|
1343
|
+
* 것이라, 도는 턴 한가운데서 먹으면 무엇이 어느 설정으로 돌았는지가
|
|
1344
|
+
* 흐려진다. 그건 큐에 그대로 두고 턴이 끝난 뒤에 평소대로 처리한다.
|
|
1345
|
+
*
|
|
1346
|
+
* 큐에서 **빼서** 넘긴다 — 넘기고 큐에도 남기면 같은 말이 두 번 나간다.
|
|
1347
|
+
*/
|
|
1348
|
+
const 끼어들기 = () => {
|
|
1349
|
+
const i = queue.findIndex((x) => !String(x ?? '').trimStart().startsWith('/'));
|
|
1350
|
+
if (i < 0) return null;
|
|
1351
|
+
const [것] = queue.splice(i, 1);
|
|
1352
|
+
화면.대기갱신('', queue.length);
|
|
1353
|
+
return String(것).trim() || null;
|
|
1354
|
+
};
|
|
1355
|
+
for await (const ev of run(session, ctx, 보낼글, { signal: turn.signal, 그림들: 보낼그림, 끼어들기 })) {
|
|
1291
1356
|
/*
|
|
1292
1357
|
* 하위 작업 안쪽에서 온 것이면 한 단 들여 그린다.
|
|
1293
1358
|
*
|
|
@@ -1296,6 +1361,15 @@ export async function chatLoop(opts = {}) {
|
|
|
1296
1361
|
*/
|
|
1297
1362
|
화면.들여쓰기(ev.depth ?? 0);
|
|
1298
1363
|
switch (ev.type) {
|
|
1364
|
+
/*
|
|
1365
|
+
* 도중에 낀 말. **화면에 그대로 보여 준다.**
|
|
1366
|
+
*
|
|
1367
|
+
* 안 보여 주면 사람은 제가 친 말이 먹혔는지 모른 채 기다린다.
|
|
1368
|
+
* 그러면 한 번 더 치게 되고, 같은 말이 두 번 나간다.
|
|
1369
|
+
*/
|
|
1370
|
+
case 'steer':
|
|
1371
|
+
say(` ${c.hcyan('❯')} ${c.white(ev.text)} ${c.gray('(도중에 낀 말 — 다음 걸음부터 반영됩니다)')}`);
|
|
1372
|
+
break;
|
|
1299
1373
|
// 어느 단계를 어떤 강도로 도는지 — 추론 강도 조절이 실제로 먹는지 눈으로 보인다.
|
|
1300
1374
|
case 'stage':
|
|
1301
1375
|
stage = ev;
|
|
@@ -1311,6 +1385,14 @@ export async function chatLoop(opts = {}) {
|
|
|
1311
1385
|
: ` ${c.yellow('↻')} ${c.gray(`${ev.why} — 상한을 ${ev.from} → ${ev.to} 로 올려 다시 부릅니다`)}`);
|
|
1312
1386
|
break;
|
|
1313
1387
|
|
|
1388
|
+
// 서버가 잠깐 막아서 기다렸다 다시 부르는 자리 (backend/retry.js).
|
|
1389
|
+
// 상태 코드·몇 초·몇 번째를 그대로 적는다 — 왜 느린지가 여기서만 읽힌다.
|
|
1390
|
+
// 쉬움 수준에서도 숫자를 뺀 채 내지 않는다. 429 는 설명이 필요한 숫자가 아니다.
|
|
1391
|
+
case 'backoff':
|
|
1392
|
+
clearThinking();
|
|
1393
|
+
say(` ${c.yellow('↻')} ${c.gray(옮긴말('loop.backoff', 알림채움(ev)))}`);
|
|
1394
|
+
break;
|
|
1395
|
+
|
|
1314
1396
|
case 'waiting':
|
|
1315
1397
|
화면.기다림(c.gray(꼬리표(stage) + '생각 중…'));
|
|
1316
1398
|
// 지워야 할 줄이 화면에 있다고 표시해 둔다.
|
|
@@ -1475,6 +1557,15 @@ export async function chatLoop(opts = {}) {
|
|
|
1475
1557
|
say(` ${c.red('✗')} ${c.white(f.보인이름 ?? '(경로 없음)')} ${c.gray(`— ${clip(String(f.error), 60)}`)}`);
|
|
1476
1558
|
}
|
|
1477
1559
|
}
|
|
1560
|
+
} else if (ev.result?.바뀐것들?.length) {
|
|
1561
|
+
/*
|
|
1562
|
+
* 폴더를 옮겼다. 옮겨진 파일을 **하나씩** 적어 둔다.
|
|
1563
|
+
*
|
|
1564
|
+
* 「이 폴더가 바뀌었다」 로 적으면 나중에 /commit 이 그 폴더를
|
|
1565
|
+
* 통째로 담고, 같은 폴더에 있던 남의 변경까지 실린다.
|
|
1566
|
+
* 줄 수는 0 이다 — 옮긴 것이지 고친 것이 아니다.
|
|
1567
|
+
*/
|
|
1568
|
+
for (const f of ev.result.바뀐것들) session.noteChange(f, { added: 0, removed: 0 });
|
|
1478
1569
|
}
|
|
1479
1570
|
}
|
|
1480
1571
|
flush(); // 도구가 하나 끝날 때마다 적어 둔다
|
|
@@ -1575,6 +1666,13 @@ export async function chatLoop(opts = {}) {
|
|
|
1575
1666
|
break;
|
|
1576
1667
|
}
|
|
1577
1668
|
|
|
1669
|
+
case 'images_folded': {
|
|
1670
|
+
clearThinking();
|
|
1671
|
+
const 장수 = (ev.뺀것들 ?? []).reduce((a2, x) => a2 + x.장수, 0);
|
|
1672
|
+
say(` ${c.cyan('◲')} ${c.gray(`오래된 그림 ${장수}장을 뺐습니다 — 사람이 쓴 말은 그대로입니다.`)}`);
|
|
1673
|
+
break;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1578
1676
|
case 'compacted': {
|
|
1579
1677
|
접기멈춤();
|
|
1580
1678
|
const 줄인 = ev.before - ev.after;
|
package/src/report.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
// 진단 결과를 화면에 표로 그리고, "이걸로 돌릴 수 있는지" 판정한다.
|
|
2
2
|
import { c, say, rule, pad, width, mark } from './ui/ansi.js';
|
|
3
|
+
import { 주소가리기 } from './safety/secrets.js';
|
|
4
|
+
import { 프록시고르기, 프록시설정 } from './backend/proxy.js';
|
|
5
|
+
import { 보관방식 } from './safety/keystore.js';
|
|
6
|
+
import { load, activeProfile } from './config.js';
|
|
7
|
+
|
|
8
|
+
// 열쇠를 어디에 두고 있나. 심사 담당자가 꼭 묻는 줄이라 보고서에 박아 둔다.
|
|
9
|
+
function 열쇠줄() {
|
|
10
|
+
try { return 보관방식(activeProfile(load())?.apiKey ?? null); } catch { return 보관방식(); }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 이 주소로 갈 때 거칠 프록시를 한 줄로. 안 거치면 null — 그때는 줄 자체를 안 만든다.
|
|
14
|
+
// 적어 놨는데 못 쓰는 것(socks5 등)이면 그 까닭을 적는다 — 진단 보고서에서 제일 먼저 볼 줄이다.
|
|
15
|
+
function 프록시줄(base) {
|
|
16
|
+
const p = 프록시고르기(base);
|
|
17
|
+
if (p) return `${p.url} (${p.출처})`;
|
|
18
|
+
const 탈 = 프록시설정().탈;
|
|
19
|
+
return 탈 ? `못 씀 — ${탈}` : null;
|
|
20
|
+
}
|
|
3
21
|
|
|
4
22
|
const ICON = { ok: mark.ok, no: mark.no, warn: mark.warn, skip: c.gray('·') };
|
|
5
23
|
const WORD = { ok: c.green('됨'), no: c.red('안됨'), warn: c.yellow('조건부'), skip: c.gray('확인불가') };
|
|
@@ -17,10 +35,14 @@ export function renderHeader(facts) {
|
|
|
17
35
|
const kindName = facts.shape === 'ollama' ? 'Ollama 자체 규격' : 'OpenAI 호환';
|
|
18
36
|
const rows = [
|
|
19
37
|
['규격', kindName],
|
|
20
|
-
['주소', facts.base],
|
|
38
|
+
['주소', 주소가리기(facts.base)],
|
|
21
39
|
['인증', facts.auth === 'none' ? '없음' : facts.auth],
|
|
22
40
|
['모델', facts.model],
|
|
23
41
|
];
|
|
42
|
+
// 프록시를 거치면 그 줄도 적는다. "연결 실패" 가 프록시 탓인지 게이트웨이 탓인지는
|
|
43
|
+
// 이 한 줄이 있어야 갈린다 — 진단 보고서를 받는 사내 담당자가 제일 먼저 보는 자리다.
|
|
44
|
+
const 프록시 = 프록시줄(facts.base);
|
|
45
|
+
if (프록시) rows.push(['프록시', 프록시]);
|
|
24
46
|
for (const [k, v] of rows) say(` ${c.gray(pad(k, 8))} ${v}`);
|
|
25
47
|
say('');
|
|
26
48
|
rule('검사', 74);
|
|
@@ -103,8 +125,10 @@ export function plainReport(facts, results, v) {
|
|
|
103
125
|
`생성 시각 ${at}`,
|
|
104
126
|
'',
|
|
105
127
|
`규격 ${facts.shape === 'ollama' ? 'Ollama 자체 규격' : 'OpenAI 호환'}`,
|
|
106
|
-
`주소 ${facts.base}`,
|
|
128
|
+
`주소 ${주소가리기(facts.base)}`,
|
|
129
|
+
...(프록시줄(facts.base) ? [`프록시 ${프록시줄(facts.base)}`] : []),
|
|
107
130
|
`인증 ${facts.auth === 'none' ? '없음' : facts.auth}`,
|
|
131
|
+
`열쇠 보관 ${열쇠줄()}`,
|
|
108
132
|
`모델 ${facts.model}`,
|
|
109
133
|
`컨텍스트 ${facts.ctx ? facts.ctx.toLocaleString() + ' 토큰' : '미상'}`,
|
|
110
134
|
'',
|