deel-local-cli 0.9.0 → 1.0.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 +470 -27
- package/README.md +479 -31
- package/bin/deel.js +234 -186
- package/package.json +3 -2
- package/src/agent/compact.js +131 -138
- package/src/agent/effort.js +44 -11
- package/src/agent/loop.js +574 -251
- package/src/agent/memory.js +152 -0
- package/src/agent/mention.js +164 -0
- package/src/agent/modes.js +11 -3
- package/src/agent/recall.js +209 -0
- package/src/agent/salvage.js +182 -0
- package/src/agent/session.js +140 -10
- package/src/agent/store.js +36 -11
- package/src/backend/adapter.js +266 -183
- package/src/backend/ctxsize.js +133 -61
- package/src/backend/detect.js +10 -1
- package/src/backend/learn.js +102 -0
- package/src/backend/mcp.js +304 -0
- package/src/commands.js +493 -53
- package/src/oneshot.js +327 -0
- package/src/repl.js +715 -479
- package/src/report.js +19 -5
- package/src/safety/guard.js +123 -7
- package/src/safety/undo.js +96 -11
- package/src/tools/encoding.js +24 -2
- package/src/tools/fsutil.js +70 -0
- package/src/tools/index.js +471 -27
- package/src/tools/webfetch.js +37 -1
- package/src/ui/ansi.js +13 -1
- package/src/ui/diff.js +255 -0
- package/src/ui/level.js +3 -1
- package/src/ui/prompt.js +13 -1
- package/src/ui/screen.js +169 -0
- package/src/ui/status.js +81 -17
- package/src/ui/tui.js +419 -0
- package/src/version.js +28 -0
package/src/commands.js
CHANGED
|
@@ -15,6 +15,8 @@ import { scanLocal, toProfiles } from './backend/scan.js';
|
|
|
15
15
|
import { list as listSessions } from './agent/store.js';
|
|
16
16
|
import { MODES as WORK_MODES, ORDER as WORK_ORDER, DEFAULT as WORK_DEFAULT, normalize as normWork, get as getWork, canWrite } from './agent/modes.js';
|
|
17
17
|
import { LEVELS, ORDER as LEVEL_ORDER, DEFAULT as LEVEL_DEFAULT, normalize as normLevel, shows as levelShows } from './ui/level.js';
|
|
18
|
+
import { diffLines, renderDiff, shortStat } from './ui/diff.js';
|
|
19
|
+
import { readTextFull } from './tools/fsutil.js';
|
|
18
20
|
const MODES = {
|
|
19
21
|
auto: '자율 — 전부 알아서. 되돌리기가 안전망',
|
|
20
22
|
confirm: '확인 — 되돌릴 수 없는 것만 물어봄',
|
|
@@ -26,6 +28,7 @@ export const COMMANDS = {
|
|
|
26
28
|
clear: { desc: '대화 비우기' },
|
|
27
29
|
context: { desc: '컨텍스트 사용량 보기' },
|
|
28
30
|
ctx: { desc: '컨텍스트 길이 — 모델에 맞춰 다시 재거나 직접 지정', arg: '[auto|숫자|640k]' },
|
|
31
|
+
out: { desc: '한 번에 받을 답 길이 상한 — 큰 파일이 잘리면 여기를 올린다', arg: '[숫자|32k|auto]' },
|
|
29
32
|
compact: { desc: '오래된 대화 줄이기' },
|
|
30
33
|
model: { desc: '연결·모델 바꾸기 (이름 일부 · list · models)', arg: '[이름|list|models]' },
|
|
31
34
|
think: { desc: '추론 강도 (off/low/medium/high/max)', arg: '<수준>' },
|
|
@@ -41,6 +44,7 @@ export const COMMANDS = {
|
|
|
41
44
|
ask: { desc: '작업 모드 → 묻기 (설명만)' },
|
|
42
45
|
orchestrator: { desc: '작업 모드 → 총괄 (큰 일을 쪼개서)' },
|
|
43
46
|
undo: { desc: '직전 작업 되돌리기', arg: '[턴수]' },
|
|
47
|
+
diff: { desc: '이번 대화에서 바뀐 파일 · 바뀐 자리 보기', arg: '[파일]' },
|
|
44
48
|
tools: { desc: '쓸 수 있는 도구 보기' },
|
|
45
49
|
skills: { desc: '스킬 보기·검색·골라 올리기', arg: '[검색어|all|off]' },
|
|
46
50
|
plugin: { desc: '플러그인 목록·설치·삭제·반입묶음', arg: '[install|remove|pack]' },
|
|
@@ -48,6 +52,9 @@ export const COMMANDS = {
|
|
|
48
52
|
status: { desc: '연결 상태' },
|
|
49
53
|
scan: { desc: '이 PC 의 로컬 모델 서버 훑기', arg: '[save]' },
|
|
50
54
|
sessions:{ desc: '이 폴더의 지난 대화 목록' },
|
|
55
|
+
recall: { desc: '지난 대화에서 찾기 — 목록 말고 내용으로', arg: '<찾을 말>' },
|
|
56
|
+
mcp: { desc: '밖에서 붙인 도구(MCP) 서버 보기' },
|
|
57
|
+
memory: { desc: '대화가 끝나도 남는 기억 — 보기·지우기', arg: '[지우기 <번호>|비우기|<적을 말>]' },
|
|
51
58
|
level: { desc: '사용자 수준 (쉬움/개발자)', arg: '[수준]' },
|
|
52
59
|
init: { desc: 'DEEL.md 규칙 파일 만들기' },
|
|
53
60
|
exit: { desc: '끝내기' },
|
|
@@ -99,6 +106,10 @@ export async function handle(line, session, ctx) {
|
|
|
99
106
|
|
|
100
107
|
case 'ctx': return await ctxLength(session, arg), { handled: true };
|
|
101
108
|
|
|
109
|
+
// 한 번에 받을 답 길이. 컨텍스트(/ctx)와는 다른 축이라 명령을 따로 둔다 —
|
|
110
|
+
// /ctx out 안에 숨겨 두니 아무도 못 찾았고, 정작 큰 파일이 안 만들어지는 원인이었다.
|
|
111
|
+
case 'out': case '출력': return await 출력상한(session, arg), { handled: true };
|
|
112
|
+
|
|
102
113
|
case 'compact': {
|
|
103
114
|
const s = spin('앞선 대화를 요약해 접는 중…');
|
|
104
115
|
const r = await compact(session);
|
|
@@ -121,23 +132,67 @@ export async function handle(line, session, ctx) {
|
|
|
121
132
|
|
|
122
133
|
case 'model': return await switchModel(session, ctx, arg), { handled: true };
|
|
123
134
|
|
|
135
|
+
/*
|
|
136
|
+
* /think — '얼마나 생각하나' 하나만 정한다.
|
|
137
|
+
*
|
|
138
|
+
* 전에는 한 명령이 두 축을 맡았다. `/think high` 는 강도(5단계)를 정하고
|
|
139
|
+
* `/think save` 는 배분(3가지)을 정했다 — 같은 이름으로 다른 것을 정하니
|
|
140
|
+
* 화면을 봐도 지금 무엇이 무엇인지 읽히지 않았다. 게다가 부를 때마다
|
|
141
|
+
* 단계표가 통째로 펼쳐졌고, 그 표의 '출력상한' 칸 세 줄은 늘 같은 값이었다.
|
|
142
|
+
*
|
|
143
|
+
* 그래서 갈랐다.
|
|
144
|
+
* /think high 강도
|
|
145
|
+
* /think 배분 절약 단계별 배분
|
|
146
|
+
* /think 자세히 단계표
|
|
147
|
+
* /out 출력 상한 (아예 다른 축이라 명령을 따로 뺐다)
|
|
148
|
+
*
|
|
149
|
+
* 옛 이름(/think save)도 그대로 받는다. 쓰던 사람의 손버릇을 깨지 않는다.
|
|
150
|
+
*/
|
|
124
151
|
case 'think': {
|
|
125
|
-
const
|
|
152
|
+
const 말 = String(arg ?? '').trim();
|
|
153
|
+
// 낱말 끝을 \b 로 잡으면 안 된다. \b 는 \w(=[A-Za-z0-9_])를 기준으로 하는데
|
|
154
|
+
// 한글은 \w 가 아니다 — '배분 절약' 의 '분' 과 공백은 **둘 다 비낱말**이라
|
|
155
|
+
// 그 사이에 경계가 없다. 그래서 `/^배분\b/` 는 영영 안 맞았고,
|
|
156
|
+
// /think 배분 … 은 통째로 죽은 명령이었다(영어 prof 만 먹혔다).
|
|
157
|
+
// 공백이나 줄 끝으로 직접 끊는다.
|
|
158
|
+
const 배분말 = /^(배분|profile|prof)(\s|$)/.test(말) ? 말.replace(/^(배분|profile|prof)\s*/, '') : null;
|
|
159
|
+
|
|
160
|
+
if (배분말 !== null) {
|
|
161
|
+
const p = normalizeProfile(배분말);
|
|
162
|
+
if (!p) {
|
|
163
|
+
say(` ${c.gray('배분')} ${Object.entries(PROFILES).map(([k, v]) => `${k}(${v.name})`).join(' · ')}`);
|
|
164
|
+
say(` ${c.gray('예')} ${c.cyan('/think 배분 절약')}`);
|
|
165
|
+
say('');
|
|
166
|
+
return { handled: true };
|
|
167
|
+
}
|
|
168
|
+
session.effort = p;
|
|
169
|
+
session.effortSet = true; // 사용자가 직접 정했다 — 작업 모드보다 우선한다
|
|
170
|
+
say(` ${mark.ok} 배분 ${c.bold(PROFILES[p].name)} ${c.gray('— ' + PROFILES[p].desc)}`);
|
|
171
|
+
showThink(session);
|
|
172
|
+
return { handled: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (/^(자세히|detail|-v)$/.test(말)) { showThink(session, { 자세히: true }); return { handled: true }; }
|
|
176
|
+
|
|
177
|
+
// 옛 이름 — /think save 처럼 배분 이름을 바로 친 경우.
|
|
178
|
+
const asProfile = normalizeProfile(말);
|
|
126
179
|
if (asProfile) {
|
|
127
180
|
session.effort = asProfile;
|
|
128
|
-
session.effortSet = true;
|
|
181
|
+
session.effortSet = true;
|
|
129
182
|
say(` ${mark.ok} 배분 ${c.bold(PROFILES[asProfile].name)} ${c.gray('— ' + PROFILES[asProfile].desc)}`);
|
|
183
|
+
say(` ${c.gray('이제')} ${c.cyan('/think 배분 ' + PROFILES[asProfile].name)} ${c.gray('로도 됩니다 — 강도와 헷갈리지 않게 갈랐습니다.')}`);
|
|
130
184
|
showThink(session);
|
|
131
185
|
return { handled: true };
|
|
132
186
|
}
|
|
133
|
-
|
|
187
|
+
|
|
188
|
+
if (!THINK_LEVELS.includes(말)) {
|
|
134
189
|
showThink(session);
|
|
135
190
|
return { handled: true };
|
|
136
191
|
}
|
|
137
|
-
session.think =
|
|
192
|
+
session.think = 말;
|
|
138
193
|
session.thinkSet = true; // 사용자가 직접 정했다 — 작업 모드보다 우선한다
|
|
139
|
-
say(` ${mark.ok} 추론 강도 ${c.bold(
|
|
140
|
-
if (!session.conn.think &&
|
|
194
|
+
say(` ${mark.ok} 추론 강도 ${c.bold(말)}`);
|
|
195
|
+
if (!session.conn.think && 말 !== 'off') {
|
|
141
196
|
say(` ${c.yellow('이 연결은 모델 층 조절이 적용되지 않습니다.')} ${c.gray('루프 층(도구 호출 상한)으로만 조절됩니다.')}`);
|
|
142
197
|
}
|
|
143
198
|
showThink(session);
|
|
@@ -171,6 +226,8 @@ export async function handle(line, session, ctx) {
|
|
|
171
226
|
return { handled: true };
|
|
172
227
|
}
|
|
173
228
|
|
|
229
|
+
case 'diff': return 바뀐것보기(session, ctx, arg), { handled: true };
|
|
230
|
+
|
|
174
231
|
case 'tools': {
|
|
175
232
|
rule('도구', 70);
|
|
176
233
|
for (const [n, t] of Object.entries(TOOLS)) {
|
|
@@ -215,13 +272,20 @@ export async function handle(line, session, ctx) {
|
|
|
215
272
|
case 'auto':
|
|
216
273
|
case 'code': case 'plan': case 'architect':
|
|
217
274
|
case 'debug': case 'ask': case 'orchestrator': {
|
|
218
|
-
|
|
219
|
-
if (name === 'work' &&
|
|
220
|
-
|
|
275
|
+
// 인자 없이 /work 만 치면 지금 모드와 고를 수 있는 것을 보여 준다.
|
|
276
|
+
if (name === 'work' && !arg.trim()) { showWork(session); return { handled: true }; }
|
|
277
|
+
|
|
278
|
+
const 골라진 = normWork(name === 'work' ? arg : name);
|
|
221
279
|
if (!골라진) {
|
|
280
|
+
// 오타에는 목록만 보여 주면 안 된다.
|
|
281
|
+
//
|
|
282
|
+
// 예전에는 /work 오타 가 조용히 목록으로 빠졌다. 그러면 사람은 자기가
|
|
283
|
+
// 친 말이 틀렸다는 걸 모른 채 '왜 안 바뀌지' 만 하게 된다. 못 알아들었다는
|
|
284
|
+
// 말을 먼저 하고, 그 다음에 쓸 수 있는 것을 보여 준다.
|
|
222
285
|
say(` ${mark.warn} 그런 모드는 없습니다: ${c.white(arg)}`);
|
|
223
286
|
say(` ${c.gray('쓸 수 있는 것:')} ${WORK_ORDER.map((k) => c.cyan(WORK_MODES[k].name)).join(c.gray(' · '))}`);
|
|
224
287
|
say('');
|
|
288
|
+
showWork(session);
|
|
225
289
|
return { handled: true };
|
|
226
290
|
}
|
|
227
291
|
session.work = 골라진;
|
|
@@ -270,6 +334,167 @@ export async function handle(line, session, ctx) {
|
|
|
270
334
|
return { handled: true };
|
|
271
335
|
}
|
|
272
336
|
|
|
337
|
+
/*
|
|
338
|
+
* /recall — 지난 대화에서 **내용으로** 찾는다.
|
|
339
|
+
*
|
|
340
|
+
* /sessions 는 목록만 보여 준다. 언제 무슨 모델로 몇 턴 했는지는 알겠는데
|
|
341
|
+
* "저번에 그 인코딩 문제 어떻게 풀었더라" 는 못 찾는다. 기록이 있는데
|
|
342
|
+
* 못 찾으면 없는 것과 같다.
|
|
343
|
+
*/
|
|
344
|
+
case 'recall': {
|
|
345
|
+
const 말 = String(arg ?? '').trim();
|
|
346
|
+
if (!말) {
|
|
347
|
+
say(` ${c.gray('찾을 말을 적어 주세요 —')} ${c.cyan('/recall CP949 인코딩')}`);
|
|
348
|
+
say(` ${c.gray('낱말 두세 개가 가장 잘 맞습니다. 조사는 붙어 있어도 됩니다.')}`);
|
|
349
|
+
say('');
|
|
350
|
+
return { handled: true };
|
|
351
|
+
}
|
|
352
|
+
const { 찾기 } = await import('./agent/recall.js');
|
|
353
|
+
const r = 찾기(session.root, 말, { limit: 10 });
|
|
354
|
+
|
|
355
|
+
rule(`지난 대화에서 "${clip(말, 30)}"`, 70);
|
|
356
|
+
if (!r.낱말.length) {
|
|
357
|
+
say(` ${mark.no} 찾을 낱말이 없습니다 ${c.gray('— 두 글자 이상으로 적어 주세요.')}`);
|
|
358
|
+
say('');
|
|
359
|
+
return { handled: true };
|
|
360
|
+
}
|
|
361
|
+
if (!r.맞은것.length) {
|
|
362
|
+
// 못 찾은 것과 안 찾아본 것을 구분해서 말한다.
|
|
363
|
+
say(r.예산초과
|
|
364
|
+
? ` ${mark.warn} 지난 대화 ${r.전체파일}개 중 ${r.본파일}개까지만 뒤졌습니다 ${c.gray('(양이 많아 멈췄습니다)')}`
|
|
365
|
+
: ` ${c.gray(`지난 대화 ${r.본파일}개를 다 뒤졌지만 없습니다.`)}`);
|
|
366
|
+
say(` ${c.gray('찾은 낱말:')} ${c.white(r.낱말.join(' · '))}`);
|
|
367
|
+
say('');
|
|
368
|
+
return { handled: true };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const h of r.맞은것) {
|
|
372
|
+
const 날 = h.언제 instanceof Date ? h.언제.toISOString().slice(0, 16).replace('T', ' ') : '';
|
|
373
|
+
const 색 = h.role === 'user' ? c.hcyan : h.role === 'assistant' ? c.white : c.gray;
|
|
374
|
+
say(` ${c.gray(pad(날, 17))} ${색(pad(h.누구, 7))} ${c.gray(h.세션)}`);
|
|
375
|
+
say(` ${강조(h.토막, r.낱말)}`);
|
|
376
|
+
}
|
|
377
|
+
say('');
|
|
378
|
+
say(` ${c.gray(`${r.전체맞음}건 중 ${r.맞은것.length}건 · 대화 ${r.본파일}개를 뒤졌습니다`)}`
|
|
379
|
+
+ (r.예산초과 ? ` ${c.yellow(`(${r.전체파일}개 중 ${r.본파일}개까지만)`)}` : ''));
|
|
380
|
+
say(` ${c.gray('그 대화를 이어하려면')} ${c.cyan('deel --resume ' + r.맞은것[0].세션)}`);
|
|
381
|
+
say('');
|
|
382
|
+
return { handled: true };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/*
|
|
386
|
+
* /mcp — 밖에서 붙인 도구 서버.
|
|
387
|
+
*
|
|
388
|
+
* 여기서 제일 중요한 줄은 도구 목록이 아니라 **경고**다. MCP 서버는 남의
|
|
389
|
+
* 프로그램이고 우리 작업 범위를 안 지킨다. 화면 어디에도 그 말이 없으면
|
|
390
|
+
* 사람은 우리 도구와 똑같이 안전한 줄 안다.
|
|
391
|
+
*/
|
|
392
|
+
/*
|
|
393
|
+
* /memory — 대화가 끝나도 남는 것.
|
|
394
|
+
*
|
|
395
|
+
* 사람이 반드시 **볼 수 있고 지울 수 있어야** 한다. 모델이 잘못 적은 줄은
|
|
396
|
+
* 매 요청마다 실려 나가면서 계속 틀리게 만든다. 틀린 기억은 없느니만 못하다.
|
|
397
|
+
*/
|
|
398
|
+
case 'memory': {
|
|
399
|
+
const M = await import('./agent/memory.js');
|
|
400
|
+
const 말 = String(arg ?? '').trim();
|
|
401
|
+
|
|
402
|
+
if (/^(비우기|clear|지우기전부)$/.test(말)) {
|
|
403
|
+
M.비우기(session.root);
|
|
404
|
+
session.memory = M.프롬프트토막(session.root);
|
|
405
|
+
say(` ${mark.ok} 기억을 비웠습니다.`);
|
|
406
|
+
say('');
|
|
407
|
+
return { handled: true };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const 지움 = /^(지우기|잊어|forget|rm)\s+(\d+)$/.exec(말);
|
|
411
|
+
if (지움) {
|
|
412
|
+
const r = M.지우기(session.root, 지움[2]);
|
|
413
|
+
if (!r.ok) say(` ${mark.no} ${r.why}`);
|
|
414
|
+
else {
|
|
415
|
+
session.memory = M.프롬프트토막(session.root);
|
|
416
|
+
say(` ${mark.ok} 잊었습니다: ${c.gray(clip(r.뺀것, 60))}`);
|
|
417
|
+
}
|
|
418
|
+
say('');
|
|
419
|
+
return { handled: true };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 그 밖의 말은 '이걸 기억해라' 로 본다. 모델을 안 거치고 바로 적는 길이다.
|
|
423
|
+
if (말) {
|
|
424
|
+
const r = M.더하기(session.root, 말);
|
|
425
|
+
if (!r.ok) say(` ${mark.warn} ${r.why}`);
|
|
426
|
+
else {
|
|
427
|
+
session.memory = M.프롬프트토막(session.root);
|
|
428
|
+
say(` ${mark.ok} 기억했습니다 ${c.gray(`(${r.줄수}줄)`)}`);
|
|
429
|
+
if (r.넘침) say(` ${c.gray('자리가 차서 오래된 것을 뺐습니다.')}`);
|
|
430
|
+
}
|
|
431
|
+
say('');
|
|
432
|
+
return { handled: true };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const 기억 = M.읽기(session.root);
|
|
436
|
+
rule('기억 — 대화가 끝나도 남는 것', 70);
|
|
437
|
+
if (!기억.줄들.length) {
|
|
438
|
+
say(` ${c.gray('아직 없습니다.')}`);
|
|
439
|
+
say('');
|
|
440
|
+
say(` ${c.gray('모델이 스스로 적기도 하고, 직접 적으셔도 됩니다 —')}`);
|
|
441
|
+
say(` ${c.cyan('/memory 사내 문서는 CP949 로 읽고 CP949 로 되돌려 쓴다')}`);
|
|
442
|
+
say('');
|
|
443
|
+
return { handled: true };
|
|
444
|
+
}
|
|
445
|
+
for (const [i, l] of 기억.줄들.entries()) {
|
|
446
|
+
say(` ${c.gray(String(i + 1).padStart(2))} ${c.white(clip(l, 82))}`);
|
|
447
|
+
}
|
|
448
|
+
const 글자 = 기억.줄들.join('\n').length;
|
|
449
|
+
say('');
|
|
450
|
+
say(` ${c.gray(`${기억.줄들.length}줄 · 약 ${Math.round(글자 / 3).toLocaleString()}토큰이 매 요청마다 함께 나갑니다`)}`);
|
|
451
|
+
say(` ${c.gray('파일')} ${c.white(기억.자리)} ${c.gray('— 직접 고치셔도 됩니다')}`);
|
|
452
|
+
say(` ${c.cyan('/memory 지우기 3')} ${c.cyan('/memory 비우기')}`);
|
|
453
|
+
say('');
|
|
454
|
+
return { handled: true };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
case 'mcp': {
|
|
458
|
+
const { 설정읽기, 설정자리, 도구최대 } = await import('./backend/mcp.js');
|
|
459
|
+
const 붙은것 = session.mcp ?? [];
|
|
460
|
+
rule('밖에서 붙인 도구 (MCP)', 70);
|
|
461
|
+
|
|
462
|
+
if (!붙은것.length) {
|
|
463
|
+
const 설정 = 설정읽기(session.root);
|
|
464
|
+
if (설정.오류) say(` ${mark.no} ${설정.오류}`);
|
|
465
|
+
else if (!설정.있음) {
|
|
466
|
+
say(` ${c.gray('붙인 것이 없습니다.')} ${c.gray('설정 파일이 없습니다 —')} ${c.white(설정자리(session.root))}`);
|
|
467
|
+
} else if (!설정.서버들.length) {
|
|
468
|
+
say(` ${c.gray('설정은 있는데 띄울 서버가 없습니다.')} ${c.gray(설정자리(session.root))}`);
|
|
469
|
+
} else {
|
|
470
|
+
say(` ${mark.warn} 설정에 ${설정.서버들.length}대가 적혀 있는데 하나도 안 붙었습니다.`);
|
|
471
|
+
say(` ${c.gray('오프라인 잠금 중이거나, 켤 때 못 떴습니다. 머리말의 경고를 보세요.')}`);
|
|
472
|
+
}
|
|
473
|
+
say('');
|
|
474
|
+
say(` ${c.gray('붙이려면')} ${c.white('.deel/mcp.json')} ${c.gray('에 이렇게 적습니다 —')}`);
|
|
475
|
+
say(` ${c.gray('{ "mcpServers": { "사내위키": { "command": "node", "args": ["wiki-mcp.js"] } } }')}`);
|
|
476
|
+
say('');
|
|
477
|
+
say(` ${c.yellow('※')} ${c.gray('MCP 서버는 남의 프로그램입니다. 사내 반입 심사를 따로 받으셔야 합니다.')}`);
|
|
478
|
+
say('');
|
|
479
|
+
return { handled: true };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (const s of 붙은것) {
|
|
483
|
+
const 상태 = s.살아있나() ? c.green('●') : c.red('○');
|
|
484
|
+
const 이름들 = s.도구.map((t) => t.name);
|
|
485
|
+
say(` ${상태} ${c.bold(s.이름)} ${c.gray(s.정보?.name ? `${s.정보.name} ${s.정보.version ?? ''}` : s.설정.command)}`);
|
|
486
|
+
say(` ${c.gray('도구 ' + s.도구.length + '개')} ${c.gray(clip(이름들.join(' · '), 60))}`);
|
|
487
|
+
if (s.잘림) say(` ${mark.warn} ${c.gray(`${s.잘림}개는 뺐습니다 — 한 서버에 ${도구최대}개까지만 받습니다(컨텍스트가 줄어듭니다)`)}`);
|
|
488
|
+
if (!s.살아있나()) say(` ${c.red(s.죽음 ?? '죽었습니다')}`);
|
|
489
|
+
}
|
|
490
|
+
say('');
|
|
491
|
+
say(` ${c.gray('모델에게는')} ${c.white('mcp__<서버>__<도구>')} ${c.gray('라는 이름으로 보입니다.')}`);
|
|
492
|
+
say(` ${c.yellow('※')} ${c.gray('이 도구들은 남의 프로그램이 돌립니다 —')} ${c.white('작업 범위(' + session.root + ') 를 안 지킵니다.')}`);
|
|
493
|
+
say(` ${c.gray(' 무엇을 불렀는지는')} ${c.white('.deel/audit.jsonl')} ${c.gray('에 남습니다.')}`);
|
|
494
|
+
say('');
|
|
495
|
+
return { handled: true };
|
|
496
|
+
}
|
|
497
|
+
|
|
273
498
|
case 'sessions': {
|
|
274
499
|
const rows = listSessions(session.root, { limit: 12 });
|
|
275
500
|
rule('이 폴더의 지난 대화', 70);
|
|
@@ -432,6 +657,10 @@ function showSkills(session, arg) {
|
|
|
432
657
|
return;
|
|
433
658
|
}
|
|
434
659
|
|
|
660
|
+
// 스킬은 남의 폴더·플러그인에서 온다. 앞머리(frontmatter)가 빠진 파일이 섞이면
|
|
661
|
+
// 이름이나 설명이 없다. 그걸 그대로 만지면 목록 하나 보려다 대화가 끝난다.
|
|
662
|
+
const 낮게 = (v) => String(v ?? '').toLowerCase();
|
|
663
|
+
|
|
435
664
|
const q = arg.trim();
|
|
436
665
|
if (q === 'all') {
|
|
437
666
|
all.forEach((s) => { s.enabled = true; });
|
|
@@ -450,7 +679,7 @@ function showSkills(session, arg) {
|
|
|
450
679
|
const term = q.slice(3).trim().toLowerCase();
|
|
451
680
|
let n = 0;
|
|
452
681
|
for (const s of all) {
|
|
453
|
-
s.enabled = s.name
|
|
682
|
+
s.enabled = 낮게(s.name).includes(term) || 낮게(s.description).includes(term);
|
|
454
683
|
if (s.enabled) n++;
|
|
455
684
|
}
|
|
456
685
|
say(` ${mark.ok} "${term}" 에 걸리는 ${n}개만 올립니다.`);
|
|
@@ -459,14 +688,17 @@ function showSkills(session, arg) {
|
|
|
459
688
|
}
|
|
460
689
|
|
|
461
690
|
const hits = q
|
|
462
|
-
? all.filter((s) => s.name
|
|
691
|
+
? all.filter((s) => 낮게(s.name).includes(q.toLowerCase()) || 낮게(s.description).includes(q.toLowerCase()))
|
|
463
692
|
: session.listedSkills();
|
|
464
693
|
|
|
465
694
|
say('');
|
|
466
695
|
rule(q ? `스킬 검색: ${q}` : '지금 올라간 스킬', 74);
|
|
467
696
|
for (const s of hits.slice(0, 30)) {
|
|
468
697
|
const tag = s.enabled ? c.green('●') : c.gray('○');
|
|
469
|
-
|
|
698
|
+
// 설명이 없을 수 있다. 스킬은 남의 폴더·플러그인에서 오는 것이라
|
|
699
|
+
// 앞머리(frontmatter)가 빠진 파일이 섞인다. 여기서 터지면 목록 하나 보려다
|
|
700
|
+
// 대화가 통째로 끝난다 — 화면 그리기는 무슨 일이 있어도 안 죽어야 한다.
|
|
701
|
+
say(` ${tag} ${c.cyan(pad(s.name, 32))} ${c.gray(String(s.description ?? '').slice(0, 60))}`);
|
|
470
702
|
}
|
|
471
703
|
if (hits.length > 30) say(` ${c.gray(`… 그 밖에 ${hits.length - 30}개`)}`);
|
|
472
704
|
say('');
|
|
@@ -543,25 +775,80 @@ function showLevel(session, arg) {
|
|
|
543
775
|
say('');
|
|
544
776
|
}
|
|
545
777
|
|
|
778
|
+
/**
|
|
779
|
+
* 찾은 낱말에 색을 입힌다.
|
|
780
|
+
*
|
|
781
|
+
* 토막만 보여 주면 **왜 이게 걸렸는지** 안 보인다. 특히 조사를 떼고 찾기
|
|
782
|
+
* 때문에("인코딩을" 로 "인코딩" 을 찾는다) 눈으로는 안 맞는 것처럼 보이는
|
|
783
|
+
* 경우가 있다. 맞은 자리를 칠해 주면 그 의심이 사라진다.
|
|
784
|
+
*/
|
|
785
|
+
function 강조(글, 낱말들) {
|
|
786
|
+
let 조각 = [{ 글, 맞음: false }];
|
|
787
|
+
for (const w of 낱말들) {
|
|
788
|
+
const 다음 = [];
|
|
789
|
+
for (const p of 조각) {
|
|
790
|
+
if (p.맞음) { 다음.push(p); continue; }
|
|
791
|
+
const 낮은 = p.글.toLowerCase();
|
|
792
|
+
let i = 0;
|
|
793
|
+
let 자리 = 낮은.indexOf(w);
|
|
794
|
+
while (자리 >= 0) {
|
|
795
|
+
if (자리 > i) 다음.push({ 글: p.글.slice(i, 자리), 맞음: false });
|
|
796
|
+
다음.push({ 글: p.글.slice(자리, 자리 + w.length), 맞음: true });
|
|
797
|
+
i = 자리 + w.length;
|
|
798
|
+
자리 = 낮은.indexOf(w, i);
|
|
799
|
+
}
|
|
800
|
+
if (i < p.글.length) 다음.push({ 글: p.글.slice(i), 맞음: false });
|
|
801
|
+
}
|
|
802
|
+
조각 = 다음;
|
|
803
|
+
}
|
|
804
|
+
return 조각.map((p) => (p.맞음 ? c.hyellow(p.글) : c.gray(p.글))).join('');
|
|
805
|
+
}
|
|
806
|
+
|
|
546
807
|
// 추론 강도는 값 하나가 아니라 '단계별 배분' 이다. 그 배분을 눈에 보이게 그린다.
|
|
547
|
-
|
|
808
|
+
/**
|
|
809
|
+
* 지금 추론 강도가 어떻게 되어 있는지.
|
|
810
|
+
*
|
|
811
|
+
* 기본은 **한 줄**이다. 사람이 알고 싶은 것은 '지금 얼마나 생각하나' 이지
|
|
812
|
+
* 단계별 표가 아니다. 표는 /think 자세히 로 뺐다.
|
|
813
|
+
*
|
|
814
|
+
* 쉬움 수준에서는 배분 이야기를 아예 안 꺼낸다 — 고를 일이 없는 사람에게
|
|
815
|
+
* 고르는 법을 보여 주면 그것부터 걱정하게 된다.
|
|
816
|
+
*/
|
|
817
|
+
function showThink(session, { 자세히 = false } = {}) {
|
|
548
818
|
const b = session.breakdown();
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
819
|
+
const 아는상한 = session.conn.maxTokens ?? session.conn.maxOut ?? null;
|
|
820
|
+
const t = effortTable(session.think, session.effort, { ctx: b.total, used: b.used, max: 아는상한 });
|
|
821
|
+
const 개발자 = session.level === '개발자';
|
|
822
|
+
const 단계 = t.rows.map((r) => `${r.label} ${r.level}`).join(c.gray(' · '));
|
|
823
|
+
|
|
554
824
|
say('');
|
|
555
|
-
say(` ${c.gray(
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
825
|
+
say(` ${c.gray('추론 강도')} ${c.bold(session.think)} ${c.gray(`(${단계})`)}`);
|
|
826
|
+
if (개발자 || 자세히) say(` ${c.gray('배분')} ${c.bold(t.name)} ${c.gray(t.desc)}`);
|
|
827
|
+
|
|
828
|
+
if (자세히) {
|
|
829
|
+
say('');
|
|
830
|
+
say(` ${c.gray(pad('단계', 12) + pad('강도', 10) + pad('출력상한', 10) + '언제')}`);
|
|
831
|
+
for (const r of t.rows) {
|
|
832
|
+
const 화살 = r.moved > 0 ? c.yellow('↑') : r.moved < 0 ? c.cyan('↓') : c.gray('·');
|
|
833
|
+
say(` ${pad(r.label, 12)}${화살} ${pad(r.level, 8)}${pad(r.cap.toLocaleString(), 10, 'right')} ${c.gray(r.why)}`);
|
|
834
|
+
}
|
|
835
|
+
say('');
|
|
836
|
+
// 상한이 어디서 왔는지 밝힌다. 세 줄이 같은 값일 때 그게 고장인지 아닌지
|
|
837
|
+
// 이 한 줄로 갈린다 — 아는 상한이 낮으면 셋이 같아지는 것이 맞다.
|
|
838
|
+
say(` ${c.gray('출력 상한은')} ${c.white((아는상한 ?? 16384).toLocaleString())} ${c.gray(
|
|
839
|
+
session.conn.maxTokens ? '(직접 정하신 값)' : session.conn.maxOut ? '(서버에서 알아낸 값)' : '(모르는 값이라 기본값)',
|
|
840
|
+
)}${c.gray(' 안에서 나눕니다 —')} ${c.cyan('/out')}`);
|
|
841
|
+
say(` ${c.gray('컨텍스트')} ${c.white(t.ctx.toLocaleString())}${c.gray(' · 지금 찬 양 ')}${c.white(t.used.toLocaleString())}`);
|
|
842
|
+
say('');
|
|
843
|
+
say(` ${c.gray('강도')} ${THINK_LEVELS.join(' · ')} ${c.gray('예')} ${c.cyan('/think high')}`);
|
|
844
|
+
say(` ${c.gray('배분')} ${Object.entries(PROFILES).map(([k, v]) => `${k}(${v.name})`).join(' · ')} ${c.gray('예')} ${c.cyan('/think 배분 절약')}`);
|
|
845
|
+
} else if (개발자) {
|
|
846
|
+
say(` ${c.gray('강도')} ${c.cyan('/think high')} ${c.gray('배분')} ${c.cyan('/think 배분 절약')} ${c.gray('자세히')} ${c.cyan('/think 자세히')}`);
|
|
847
|
+
} else {
|
|
848
|
+
say(` ${c.gray('더 세게')} ${c.cyan('/think high')} ${c.gray('더 빠르게')} ${c.cyan('/think low')}`);
|
|
559
849
|
}
|
|
560
|
-
|
|
561
|
-
say(` ${c.gray('강도')} ${THINK_LEVELS.join(' · ')} ${c.gray('예')} ${c.cyan('/think high')}`);
|
|
562
|
-
say(` ${c.gray('배분')} ${Object.entries(PROFILES).map(([k, v]) => `${k}(${v.name})`).join(' · ')} ${c.gray('예')} ${c.cyan('/think save')}`);
|
|
850
|
+
|
|
563
851
|
if (!session.conn.think && session.think !== 'off') {
|
|
564
|
-
say('');
|
|
565
852
|
say(` ${c.yellow('이 연결은 모델 층 강도 조절이 적용되지 않습니다.')} ${c.gray('출력 상한만 단계별로 적용됩니다.')}`);
|
|
566
853
|
}
|
|
567
854
|
say('');
|
|
@@ -587,6 +874,149 @@ function showContext(session) {
|
|
|
587
874
|
say('');
|
|
588
875
|
}
|
|
589
876
|
|
|
877
|
+
/**
|
|
878
|
+
* 이번 대화에서 무엇이 바뀌었는지 보여 준다.
|
|
879
|
+
*
|
|
880
|
+
* /diff 바뀐 파일 목록과 늘고 준 줄 수
|
|
881
|
+
* /diff src/a.js 그 파일이 처음과 지금 사이에 어떻게 달라졌는지
|
|
882
|
+
*
|
|
883
|
+
* 파일 하나를 볼 때는 이번 대화의 '맨 처음' 모습과 견준다. 세 번 고쳤어도
|
|
884
|
+
* 사람이 알고 싶은 것은 '내가 시키기 전과 지금이 뭐가 다른가' 이지
|
|
885
|
+
* 마지막 한 번이 아니다. 되돌리기 이력이 그 맨 처음 모습을 들고 있다.
|
|
886
|
+
*/
|
|
887
|
+
function 바뀐것보기(session, ctx, arg = '') {
|
|
888
|
+
const 말 = String(arg ?? '').trim();
|
|
889
|
+
say('');
|
|
890
|
+
|
|
891
|
+
if (!말) {
|
|
892
|
+
const 목록 = [...session.changes.entries()];
|
|
893
|
+
if (!목록.length) {
|
|
894
|
+
say(` ${c.gray('이번 대화에서 바뀐 파일이 없습니다.')}`);
|
|
895
|
+
say(` ${c.gray('파일을 고치고 나면 여기에 무엇이 얼마나 바뀌었는지 모입니다.')}`);
|
|
896
|
+
say('');
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
rule('이번 대화에서 바뀐 파일', 70);
|
|
900
|
+
let a = 0;
|
|
901
|
+
let r = 0;
|
|
902
|
+
for (const [p, v] of 목록) {
|
|
903
|
+
a += v.added;
|
|
904
|
+
r += v.removed;
|
|
905
|
+
const 몇번 = v.times > 1 ? c.gray(` ${v.times}번`) : '';
|
|
906
|
+
say(` ${pad(clip(ctx.scope.show(p), 46), 46)} ${c.hgreen(pad(`+${v.added}`, 6, 'right'))} ${c.hred(pad(`−${v.removed}`, 6, 'right'))}${몇번}`);
|
|
907
|
+
}
|
|
908
|
+
say(` ${c.gray('─'.repeat(60))}`);
|
|
909
|
+
say(` ${pad(`${목록.length}개 파일`, 46)} ${c.hgreen(pad(`+${a}`, 6, 'right'))} ${c.hred(pad(`−${r}`, 6, 'right'))}`);
|
|
910
|
+
say('');
|
|
911
|
+
say(` ${c.gray('한 파일을 자세히 보려면')} ${c.cyan('/diff <파일>')}${c.gray(', 되돌리려면')} ${c.cyan('/undo')}`);
|
|
912
|
+
say('');
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
let abs;
|
|
917
|
+
try { abs = ctx.scope.resolve(말); }
|
|
918
|
+
catch (err) { say(` ${mark.warn} ${err.message}`); say(''); return; }
|
|
919
|
+
|
|
920
|
+
// 이번 대화에서 이 파일을 처음 건드리기 직전의 모습.
|
|
921
|
+
const 처음 = ctx.history.all().find((x) => x.path === abs);
|
|
922
|
+
if (!처음) {
|
|
923
|
+
say(` ${c.gray('이번 대화에서 안 바꾼 파일입니다:')} ${ctx.scope.show(abs)}`);
|
|
924
|
+
say(` ${c.gray('바뀐 것들을 보려면')} ${c.cyan('/diff')}`);
|
|
925
|
+
say('');
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const 옛것 = 처음.before === null ? null
|
|
930
|
+
: (처음.enc === 'b64' ? Buffer.from(처음.before, 'base64').toString('utf8') : 처음.before);
|
|
931
|
+
let 지금 = null;
|
|
932
|
+
if (existsSync(abs)) {
|
|
933
|
+
try { 지금 = readTextFull(abs).text; }
|
|
934
|
+
catch (err) { say(` ${mark.warn} 지금 내용을 못 읽습니다: ${err.message}`); say(''); return; }
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const d = diffLines(옛것, 지금);
|
|
938
|
+
rule(ctx.scope.show(abs), 70);
|
|
939
|
+
if (!d.changed) {
|
|
940
|
+
say(` ${c.gray('고쳤다가 되돌아와서, 처음과 지금이 같습니다.')}`);
|
|
941
|
+
say('');
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (d.isNew) say(` ${c.gray('이번 대화에서 새로 만든 파일입니다.')}`);
|
|
945
|
+
if (d.isGone) say(` ${c.gray('이번 대화에서 없어진 파일입니다.')}`);
|
|
946
|
+
say(` ${shortStat(d)}`);
|
|
947
|
+
say('');
|
|
948
|
+
for (const l of renderDiff(d, { maxLines: session.level === '개발자' ? 200 : 60 })) say(l);
|
|
949
|
+
say('');
|
|
950
|
+
say(` ${c.gray('되돌리려면')} ${c.cyan('/undo')}`);
|
|
951
|
+
say('');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* 한 번에 받을 답 길이 상한.
|
|
956
|
+
*
|
|
957
|
+
* /out 지금 값과 어디서 나온 값인지
|
|
958
|
+
* /out 32k 직접 지정
|
|
959
|
+
* /out auto 모른다고 두고 안전한 기본값으로 (16,384)
|
|
960
|
+
*
|
|
961
|
+
* 왜 따로 있나:
|
|
962
|
+
* 컨텍스트(/ctx)와 다른 축이다. 컨텍스트가 655k 여도 '한 번에 뱉을 수 있는 답'
|
|
963
|
+
* 은 대개 훨씬 작다. 그 두 값이 하나인 줄 알면 큰 파일이 왜 안 만들어지는지
|
|
964
|
+
* 영영 알 수 없다 — 컨텍스트는 넉넉한데 답이 잘리기 때문이다.
|
|
965
|
+
*
|
|
966
|
+
* 전에는 이 기능이 /ctx out 안에 숨어 있었고, 게다가 **먹지도 않았다**
|
|
967
|
+
* (effort.js 의 클램프가 다시 조였다). 있는데 안 먹는 것이 가장 나쁘다 —
|
|
968
|
+
* 문서에 적혀 있으니 사람이 그걸 믿고 쓴다.
|
|
969
|
+
*/
|
|
970
|
+
async function 출력상한(session, arg = '') {
|
|
971
|
+
const { parseSize } = await import('./backend/ctxsize.js');
|
|
972
|
+
const 말 = String(arg ?? '').trim().toLowerCase();
|
|
973
|
+
const cfg = load();
|
|
974
|
+
const prof = cfg.profiles.find((p) => p.id === cfg.active) ?? cfg.profiles[0];
|
|
975
|
+
const 알아낸것 = session.conn.maxOut ?? null;
|
|
976
|
+
|
|
977
|
+
if (말 === 'auto' || 말 === '자동') {
|
|
978
|
+
session.conn.maxTokens = null;
|
|
979
|
+
if (prof) { delete prof.maxTokens; try { save(cfg); } catch {} }
|
|
980
|
+
say(` ${mark.ok} 직접 정한 값을 지웠습니다.`);
|
|
981
|
+
say(` ${c.gray(알아낸것 ? `서버에서 알아낸 ${알아낸것.toLocaleString()} 토큰을 씁니다.` : '모르는 값이라 16,384 토큰으로 갑니다.')}`);
|
|
982
|
+
say('');
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (말) {
|
|
987
|
+
const 값 = parseSize(말);
|
|
988
|
+
if (!값) {
|
|
989
|
+
say(` ${mark.no} 숫자를 못 읽었습니다: ${c.white(arg)}`);
|
|
990
|
+
say(` ${c.gray('이렇게 쓰세요 —')} ${c.cyan('/out 32k')} ${c.cyan('/out 65536')} ${c.cyan('/out auto')}`);
|
|
991
|
+
say('');
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
session.conn.maxTokens = 값;
|
|
995
|
+
if (prof) { prof.maxTokens = 값; try { save(cfg); } catch {} }
|
|
996
|
+
say(` ${mark.ok} 답 길이 상한 ${c.bold(값.toLocaleString())} 토큰`);
|
|
997
|
+
say(` ${c.gray('모델이 못 내는 값을 넣으면 서버가 거절합니다. 거절당하면')} ${c.cyan('/out auto')} ${c.gray('로 되돌리세요.')}`);
|
|
998
|
+
say('');
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const 쓰는값 = session.conn.maxTokens ?? 알아낸것 ?? 16384;
|
|
1003
|
+
const 어디서 = session.conn.maxTokens ? '직접 정하신 값'
|
|
1004
|
+
: 알아낸것 ? '서버에서 알아낸 값'
|
|
1005
|
+
: '모르는 값이라 안전한 기본값';
|
|
1006
|
+
say('');
|
|
1007
|
+
rule('한 번에 받을 답 길이', 70);
|
|
1008
|
+
say(` ${c.gray('지금 상한')} ${c.white(쓰는값.toLocaleString())} ${c.gray(`토큰 — ${어디서}`)}`);
|
|
1009
|
+
say(` ${c.gray('컨텍스트')} ${c.white((session.conn.ctx ?? 0).toLocaleString())} ${c.gray('토큰')} ${c.gray('(다른 축입니다 — /ctx)')}`);
|
|
1010
|
+
say('');
|
|
1011
|
+
say(` ${c.gray('이 값이 한 번에 만들 수 있는 파일 크기를 정합니다.')}`);
|
|
1012
|
+
say(` ${c.gray('1,000줄짜리 HTML 이 대략 12,000~18,000 토큰입니다.')}`);
|
|
1013
|
+
say(` ${c.gray('여기서 잘려도 받은 데까지는 파일에 쓰고 이어 붙입니다 — 다만 몇 번 더 오갑니다.')}`);
|
|
1014
|
+
say('');
|
|
1015
|
+
say(` ${c.cyan('/out 32k')} ${c.gray('직접 지정 (k 는 1024)')}`);
|
|
1016
|
+
say(` ${c.cyan('/out auto')} ${c.gray('직접 정한 값을 지우고 알아낸 값/기본값으로')}`);
|
|
1017
|
+
say('');
|
|
1018
|
+
}
|
|
1019
|
+
|
|
590
1020
|
/**
|
|
591
1021
|
* 컨텍스트 길이를 보고·다시 재고·직접 지정한다.
|
|
592
1022
|
*
|
|
@@ -617,30 +1047,13 @@ async function ctxLength(session, arg = '') {
|
|
|
617
1047
|
say('');
|
|
618
1048
|
};
|
|
619
1049
|
|
|
620
|
-
// 0) 답 길이 상한 —
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
|
|
624
|
-
// 큰 모델을 쓰면 여기서 올릴 수 있다.
|
|
625
|
-
if (/^(out|답|출력)\b/.test(말)) {
|
|
626
|
-
const 값 = parseSize(말.replace(/^(out|답|출력)\s*/, ''));
|
|
627
|
-
const cfg = load();
|
|
628
|
-
const prof = cfg.profiles.find((p) => p.id === cfg.active) ?? cfg.profiles[0];
|
|
629
|
-
if (!값) {
|
|
630
|
-
say(` ${c.gray('한 번에 받을 답 길이 상한')} ${c.white((session.conn.maxTokens ?? 16384).toLocaleString())} ${c.gray('토큰')}`);
|
|
631
|
-
say(` ${c.gray('올리려면')} ${c.cyan('/ctx out 32k')} ${c.gray('— 모델이 못 내는 값을 넣으면 서버가 거절합니다.')}`);
|
|
632
|
-
say('');
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
session.conn.maxTokens = 값;
|
|
636
|
-
if (prof) { prof.maxTokens = 값; try { save(cfg); } catch {} }
|
|
637
|
-
say(` ${mark.ok} 답 길이 상한 ${c.bold(값.toLocaleString())} 토큰 ${c.gray('— 컨텍스트와는 다른 축입니다')}`);
|
|
638
|
-
say('');
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
1050
|
+
// 0) 답 길이 상한 — 이제 /out 이 본자리다. 여기서는 그리로 넘긴다.
|
|
1051
|
+
// \b 가 아니라 공백·줄 끝으로 끊는다. 한글은 \w 가 아니라서
|
|
1052
|
+
// `/^답\b/` 는 '답 32k' 에도 '답' 에도 안 맞는다(위 '배분' 과 같은 함정).
|
|
1053
|
+
if (/^(out|답|출력)(\s|$)/.test(말)) return await 출력상한(session, 말.replace(/^(out|답|출력)\s*/, ''));
|
|
641
1054
|
|
|
642
1055
|
// 1) 직접 지정
|
|
643
|
-
if (말 &&
|
|
1056
|
+
if (말 && !['auto', '자동', '다시', '자세히', 'detail', '-v'].includes(말)) {
|
|
644
1057
|
const 값 = parseSize(말);
|
|
645
1058
|
if (!값) {
|
|
646
1059
|
say(` ${mark.no} 숫자를 못 읽었습니다: ${c.white(arg)}`);
|
|
@@ -656,17 +1069,44 @@ async function ctxLength(session, arg = '') {
|
|
|
656
1069
|
return;
|
|
657
1070
|
}
|
|
658
1071
|
|
|
659
|
-
// 2) 다시 재기
|
|
1072
|
+
// 2) 다시 재기 (auto · 다시 · 자세히)
|
|
660
1073
|
if (말) {
|
|
1074
|
+
const 자세히 = /자세히|detail|-v/.test(말);
|
|
661
1075
|
const s = spin('모델에 걸린 길이를 서버에 묻는 중…');
|
|
662
1076
|
let r;
|
|
663
1077
|
try { r = await probeCtx(session.conn); }
|
|
664
1078
|
catch (err) { s.stop(` ${mark.no} 못 물어봤습니다 — ${c.gray(String(err?.message ?? err))}`); say(''); return; }
|
|
665
1079
|
s.stop('');
|
|
1080
|
+
|
|
1081
|
+
// 어디를 두드렸고 무엇이 나왔는지. 값이 이상할 때 사람이 원인을 짚을 수 있어야 한다.
|
|
1082
|
+
if (자세히) {
|
|
1083
|
+
say('');
|
|
1084
|
+
say(` ${c.gray('두드린 자리')}`);
|
|
1085
|
+
for (const t of r.tried) {
|
|
1086
|
+
const 표 = t.ok ? c.green('응답함') : c.gray(`${t.status || '연결 실패'}`);
|
|
1087
|
+
say(` ${c.gray(pad(t.label, 20))} ${표} ${c.gray(clip(t.url, 60))}`);
|
|
1088
|
+
}
|
|
1089
|
+
say('');
|
|
1090
|
+
say(` ${c.gray('읽어 낸 값')}`);
|
|
1091
|
+
say(` ${c.gray(pad('모델 최대', 20))} ${r.max ? c.white(r.max.toLocaleString()) : c.gray('못 찾음')}`
|
|
1092
|
+
+ (r.maxKey ? c.gray(` ← ${r.maxKey}`) : ''));
|
|
1093
|
+
say(` ${c.gray(pad('지금 올린 길이', 20))} ${r.loaded ? c.white(r.loaded.toLocaleString()) : c.gray('못 찾음')}`
|
|
1094
|
+
+ (r.loadedKey ? c.gray(` ← ${r.loadedKey}`) : ''));
|
|
1095
|
+
say(` ${c.gray(pad('답 길이 상한', 20))} ${r.out ? c.white(r.out.toLocaleString()) : c.gray('못 찾음')}`
|
|
1096
|
+
+ (r.outSource ? c.gray(` ← ${r.outSource}`) : ''));
|
|
1097
|
+
say('');
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// 답 길이 상한도 같이 알아냈으면 받아 둔다. 사람이 정한 값은 안 덮는다.
|
|
1101
|
+
if (r.out) session.conn.maxOut = r.out;
|
|
1102
|
+
|
|
666
1103
|
if (!r.value) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
say(`
|
|
1104
|
+
// 못 알아낸 것을 아는 척하지 않는다. 조용히 32,768 로 깔고 앉으면
|
|
1105
|
+
// 655k 모델을 5% 만 쓰거나, 8k 서버에 128k 를 보내 조용히 잘린다.
|
|
1106
|
+
say(` ${mark.warn} 서버가 컨텍스트 길이를 안 알려줍니다. ${c.gray(r.why ?? '')}`);
|
|
1107
|
+
if (!자세히) for (const t of r.tried) say(` ${c.gray(pad(t.label, 20))} ${c.gray(t.ok ? '응답함(값 없음)' : `${t.status || '연결 실패'}`)}`);
|
|
1108
|
+
say(` ${c.gray(`지금은 ${(session.conn.ctx ?? 0).toLocaleString()} 으로 잡혀 있습니다 — 이건 알아낸 값이 아니라 기본값입니다.`)}`);
|
|
1109
|
+
say(` ${c.gray('직접 넣어 주세요 —')} ${c.cyan('/ctx 655360')} ${c.gray('어디를 두드렸는지 보려면')} ${c.cyan('/ctx 자세히')}`);
|
|
670
1110
|
say('');
|
|
671
1111
|
return;
|
|
672
1112
|
}
|
|
@@ -692,7 +1132,7 @@ async function ctxLength(session, arg = '') {
|
|
|
692
1132
|
say('');
|
|
693
1133
|
say(` ${c.cyan('/ctx auto')} ${c.gray('서버에 다시 물어 모델에 맞춥니다')}`);
|
|
694
1134
|
say(` ${c.cyan('/ctx 655360')} ${c.gray('직접 지정 (640k · 128k · 1m 도 됩니다 — k 는 1024)')}`);
|
|
695
|
-
say(` ${c.cyan('/
|
|
1135
|
+
say(` ${c.cyan('/out 32k')} ${c.gray('한 번에 받을 답 길이 상한 (지금 ' + (session.conn.maxTokens ?? session.conn.maxOut ?? 16384).toLocaleString() + ') — 다른 축입니다')}`);
|
|
696
1136
|
say('');
|
|
697
1137
|
}
|
|
698
1138
|
|