deel-local-cli 0.8.0 → 0.9.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/src/repl.js CHANGED
@@ -5,17 +5,20 @@ import { statusLine, headerLines, contextWarning } from './ui/status.js';
5
5
  import { STAGES } from './agent/effort.js';
6
6
  import { handle } from './commands.js';
7
7
  import { next as nextWork, get as getWork, canWrite } from './agent/modes.js';
8
+ import { route } from './agent/route.js';
8
9
  import { run } from './agent/loop.js';
9
10
  import { Session } from './agent/session.js';
10
11
  import { makeScope } from './safety/guard.js';
11
12
  import { History } from './safety/undo.js';
12
13
  import { Audit } from './safety/audit.js';
13
- import { activeProfile, load, resolveKey } from './config.js';
14
+ import { activeProfile, load, resolveKey, save as saveCfg } from './config.js';
14
15
  import { discover } from './skills/discover.js';
15
16
  import { allowEndpoint, setOffline, isOffline, isLocalHost } from './safety/network.js';
16
17
  import { Store, latest, prune } from './agent/store.js';
17
18
  import { askHidden } from './ui/prompt.js';
19
+ import { spin } from './ui/spinner.js';
18
20
  import { explain } from './ui/level.js';
21
+ import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
19
22
 
20
23
  // 도구마다 눈에 띄는 글자를 다르게 준다. 훑을 때 종류가 먼저 보인다.
21
24
  const TOOL_GLYPH = {
@@ -63,7 +66,13 @@ export async function chatLoop(opts = {}) {
63
66
  const conn = {
64
67
  kind: prof.kind, base: prof.baseUrl, auth: prof.auth,
65
68
  key: resolveKey(prof), model: prof.model,
66
- ctx: prof.ctx ?? 32768, streaming: prof.streaming ?? false,
69
+ // 컨텍스트 길이. 순서가 우선순위다
70
+ // deel --ctx 655360 > 프로필에 저장된 값 > 기본값
71
+ // 기본값으로 떨어졌다는 것은 '아직 못 쟀다' 는 뜻이다. 아래에서 그렇다고 말해 준다.
72
+ ctx: opts.ctx ?? prof.ctx ?? CTX_DEFAULT,
73
+ // 답 길이 상한. 컨텍스트와 다른 축이다 — 없으면 effort.js 의 울타리를 쓴다.
74
+ maxTokens: prof.maxTokens ?? null,
75
+ streaming: prof.streaming ?? false,
67
76
  tools: prof.tools ?? false, json: prof.json ?? false, think: prof.think ?? false,
68
77
  };
69
78
 
@@ -74,6 +83,8 @@ export async function chatLoop(opts = {}) {
74
83
  const session = new Session(conn, {
75
84
  root,
76
85
  mode: opts.mode ?? 'auto',
86
+ // 처음부터 원하는 모드로 시작할 수 있다 — deel --work plan
87
+ work: opts.work ?? null,
77
88
  // 수준은 설정에 남는다. 한 번 고르면 다음에 켤 때도 그대로다.
78
89
  level: opts.level ?? cfg.level ?? null,
79
90
  think: opts.think ?? 'medium',
@@ -196,12 +207,48 @@ export async function chatLoop(opts = {}) {
196
207
  },
197
208
  };
198
209
 
210
+ // ── 컨텍스트 길이를 모델에서 긁어온다 ─────────────────────────────────
211
+ //
212
+ // 켤 때마다 서버에 물어본다. 저장된 값을 그대로 믿지 않는다 —
213
+ // 같은 이름의 모델이라도 서버에서 몇 k 로 올렸는지가 그때그때 다르고,
214
+ // 그 차이를 화면에 못 보면 조용히 작아진 채로 쓰게 된다.
215
+ //
216
+ // --ctx 로 직접 주신 값이 있으면 안 건드린다. 사람이 고른 것을 뒤집지 않는다.
217
+ const 길이알림 = []; // 잘 된 소식
218
+ const 길이경고 = []; // 손을 봐야 하는 것
219
+ if (opts.ctx == null) {
220
+ // 이 컴퓨터 안의 서버면 눈 깜짝할 새다. 사내 게이트웨이는 몇 초 걸릴 수 있어
221
+ // 무슨 일이 일어나는 중인지 알려 준다 — 멈춘 것처럼 보이면 안 된다.
222
+ const s = spin('모델에 걸린 컨텍스트 길이를 확인하는 중…');
223
+ let r = null;
224
+ try { r = await probeCtx(conn, { timeout: 6000 }); } catch { /* 못 물어보면 아래에서 처리 */ }
225
+ s.stop('');
226
+ if (r?.value) {
227
+ const 전 = conn.ctx;
228
+ conn.ctx = r.value;
229
+ // 알아낸 값은 프로필에 남긴다. 다음에 켤 때 화면이 곧바로 맞게 뜬다.
230
+ if (prof.ctx !== r.value) {
231
+ prof.ctx = r.value;
232
+ try { const cfg2 = load(); const t = cfg2.profiles.find((p) => p.id === prof.id); if (t) { t.ctx = r.value; saveCfg(cfg2); } } catch { /* 못 남겨도 이번 세션에는 먹는다 */ }
233
+ }
234
+ if (전 !== r.value) 길이알림.push(`컨텍스트를 ${전.toLocaleString()} ${c.gray('→')} ${c.white(r.value.toLocaleString())} 로 맞췄습니다 ${c.gray('(' + (r.source ?? '서버') + '에서 읽음)')}`);
235
+ if (r.max && r.loaded && r.max > r.loaded) {
236
+ 길이경고.push(`이 모델은 ${c.white(r.max.toLocaleString())} 까지 됩니다 — 서버에서 더 올린 뒤 ${c.cyan('/ctx auto')}`);
237
+ }
238
+ } else if (prof.ctx == null) {
239
+ 길이경고.push(`컨텍스트를 서버가 안 알려줍니다 — 우선 ${CTX_DEFAULT.toLocaleString()} 으로 잡았습니다. ${c.cyan('/ctx 655360')} 처럼 직접 지정하세요`);
240
+ }
241
+ }
242
+
199
243
  // ── 머리말 ────────────────────────────────────────────────────────────
200
244
  say('');
201
245
  for (const l of box(headerLines(session, found), { tone: c.gray })) say(' ' + l);
202
246
  const warn = [];
203
247
  if (!conn.tools) warn.push('도구 호출이 확인되지 않았습니다 — deel diagnose 로 점검하세요');
204
248
  if (!conn.streaming) warn.push('스트리밍이 없어 응답이 한 번에 나옵니다');
249
+ warn.push(...길이경고);
250
+ // 잘 된 것은 경고 표시를 달지 않는다. ⚠ 가 붙으면 뭘 고쳐야 하나 싶어진다.
251
+ for (const l of 길이알림) say(` ${mark.ok} ${c.gray(l)}`);
205
252
  for (const w of warn) say(` ${mark.warn} ${c.gray(w)}`);
206
253
  say(` ${c.gray('/help 명령 목록')} ${c.gray('/think 추론 강도')} ${c.gray('Ctrl+C 중단·끝내기')}`);
207
254
 
@@ -245,6 +292,24 @@ export async function chatLoop(opts = {}) {
245
292
  if (cmd.handled) continue;
246
293
  const toSend = cmd.text ?? text; // 슬래시 명령이면 펼쳐진 내용을 보낸다
247
294
 
295
+ // 종합 모드면 이 한마디가 무슨 일인지 보고 알맞은 모드로 옮긴다.
296
+ //
297
+ // 기본 모드는 안 건드린다 — 다음 한마디는 다시 처음부터 고른다.
298
+ // 사용자가 직접 고른 모드가 있으면 여기 안 들어온다. 사람이 고른 것을 뒤집지 않는다.
299
+ session.routed = null;
300
+ if (session.work === 'auto') {
301
+ const 골라진 = route(toSend);
302
+ if (골라진.mode) {
303
+ session.routed = 골라진.mode;
304
+ const w = getWork(골라진.mode);
305
+ say('');
306
+ say(` ${c.hcyan(w.glyph)} ${c.bold(w.name)} ${c.gray('(' + w.en + ')')}`
307
+ + ` ${c.gray('말 속에 ' + 골라진.why + ' 가 있어서')}`
308
+ + (canWrite(골라진.mode) ? '' : ` ${c.green('· 파일은 안 바꿉니다')}`));
309
+ say(` ${c.gray('다르면')} ${c.cyan('/code')} ${c.gray('처럼 직접 고르세요. 그때부터는 안 바뀝니다.')}`);
310
+ }
311
+ }
312
+
248
313
  say('');
249
314
  const started = Date.now();
250
315
  const before = { in: session.usage.in, out: session.usage.out };
package/src/report.js CHANGED
@@ -57,7 +57,7 @@ export function verdict(facts, results) {
57
57
  if (get('json') !== 'ok') notes.push('구조적 출력이 약합니다 — 편집 형식을 프롬프트로 강제하고 검사를 붙입니다.');
58
58
  if (get('stream') !== 'ok') notes.push('스트리밍이 없습니다 — 화면은 스피너로 대체하고 기능은 동일하게 갑니다.');
59
59
  if (get('system') === 'warn') notes.push('시스템 지시를 약하게 따릅니다 — 스킬을 적게, 짧게 올려야 합니다.');
60
- if (get('think') === 'ok') notes.push('추론 강도가 모델 층에서 먹습니다 — /think 로 바로 조절됩니다.');
60
+ if (get('think') === 'ok') notes.push('추론 강도가 모델 층에서 적용됩니다 — /think 로 바로 조절됩니다.');
61
61
  else notes.push('추론 강도는 루프 층(계획 강제·도구 호출 상한·자기검증 횟수)으로 조절합니다.');
62
62
  if (!facts.ctx) notes.push('컨텍스트 길이를 서버가 안 알려줍니다 — 설정에서 직접 넣어야 합니다.');
63
63
 
package/src/ui/ansi.js CHANGED
@@ -5,6 +5,15 @@ const ON = (process.stdout.isTTY || process.env.FORCE_COLOR === '1') && process.
5
5
 
6
6
  const E = (n) => (s) => (ON ? `\x1b[${n}m${s}\x1b[0m` : String(s));
7
7
 
8
+ // 흐린 글자의 밝기. 256색을 못 쓰는 옛 콘솔이면 90 으로 되돌린다.
9
+ function GRAY() {
10
+ const 뜻 = String(process.env.DEEL_CONTRAST ?? '').toLowerCase();
11
+ if (뜻 === 'low') return 90;
12
+ const 옛콘솔 = process.env.TERM === 'dumb';
13
+ if (옛콘솔) return 뜻 === 'high' ? 37 : 90;
14
+ return 뜻 === 'high' ? '38;5;252' : '38;5;245';
15
+ }
16
+
8
17
  export const c = {
9
18
  dim: E(2),
10
19
  bold: E(1),
@@ -17,7 +26,16 @@ export const c = {
17
26
  magenta: E(35),
18
27
  cyan: E(36),
19
28
  white: E(37),
20
- gray: E(90),
29
+ // 흐린 글자.
30
+ //
31
+ // 예전에는 90(밝은 검정)이었는데, 배경이 어두우면 배경에 묻히고 밝으면
32
+ // 더 안 보인다. 실제로 "연한 글자가 잘 안 보인다" 는 말을 들었다.
33
+ // 그래서 어느 배경에서도 읽히는 중간 회색을 기본으로 쓴다.
34
+ //
35
+ // 눈에 맞게 바꿀 수 있다:
36
+ // DEEL_CONTRAST=high 더 밝게 (밝은 배경이나 눈이 피로할 때)
37
+ // DEEL_CONTRAST=low 예전처럼 흐리게
38
+ gray: E(GRAY()),
21
39
  // 밝은 계열 — 어두운 배경에서 본문과 구분이 필요할 때
22
40
  hred: E(91),
23
41
  hgreen: E(92),
package/src/ui/level.js CHANGED
@@ -18,8 +18,10 @@ export const LEVELS = {
18
18
  hint: '권장값으로 바로 시작',
19
19
  // 목록에 띄울 것. 나머지는 쳐도 먹지만 안 보인다.
20
20
  show: [
21
- 'help', 'code', 'plan', 'ask',
22
- 'model', 'scan', 'undo', 'clear', 'sessions', 'cost', 'level', 'exit',
21
+ 'help', 'work', 'auto', 'code', 'plan', 'ask',
22
+ // ctx 초보 목록에 넣는다. 컨텍스트가 작게 잡히면 "왜 파일을 조금만 읽지"
23
+ // 가 되는데, 초보일수록 그 원인을 못 찾는다. 명령 하나로 끝나는 문제다.
24
+ 'model', 'ctx', 'scan', 'undo', 'clear', 'sessions', 'cost', 'level', 'exit',
23
25
  ],
24
26
  // 첫 실행에서 훑어 추천까지 해 준다
25
27
  autoScan: true,
package/src/ui/status.js CHANGED
@@ -31,9 +31,13 @@ export const SEGMENTS = {
31
31
  work: {
32
32
  desc: '작업 모드',
33
33
  make: (s) => {
34
- const w = workMode(s.work);
35
- const 잠김 = !canWrite(s.work);
36
- return `${c.hcyan(w.glyph)} ${잠김 ? c.green(w.name) : c.white(w.name)}${잠김 ? c.green(' 읽기만') : ''}`;
34
+ const 지금 = s.effectiveWork ? s.effectiveWork() : s.work;
35
+ const w = workMode(지금);
36
+ const 잠김 = !canWrite(지금);
37
+ // 저절로 옮겨 간 것인지 사람이 고른 것인지 구분해서 보여준다.
38
+ // 이게 없으면 "왜 갑자기 읽기만 되지" 를 알 길이 없다.
39
+ const 저절로 = s.routed ? c.gray('~') : '';
40
+ return `${c.hcyan(w.glyph)} ${저절로}${잠김 ? c.green(w.name) : c.white(w.name)}${잠김 ? c.green(' 읽기만') : ''}`;
37
41
  },
38
42
  },
39
43
 
@@ -83,12 +87,18 @@ function base(p) {
83
87
  return parts[parts.length - 1] ?? '~';
84
88
  }
85
89
 
86
- // 12345 → 12.3k. 상태줄은 자리가 없다.
90
+ // 12345 → 12.1k. 상태줄은 자리가 없다.
91
+ //
92
+ // 1000 이 아니라 1024 로 나눈다. 컨텍스트 길이는 죄다 2의 거듭제곱이라
93
+ // 1000 으로 나누면 131,072 가 '131k' 로 나온다 — 아무도 그렇게 안 부른다.
94
+ // 1024 로 나눠야 128k · 32k · 640k 처럼 실제로 부르는 이름이 나온다.
95
+ // 무엇보다 /ctx 가 받는 단위와 같아야 한다. 화면에 655k 라고 띄워 놓고
96
+ // 655k 를 치면 다른 값이 되는 게 가장 나쁘다.
87
97
  function short(n) {
88
98
  const v = Number(n) || 0;
89
- if (v < 1000) return String(v);
90
- if (v < 1000000) return (v / 1000).toFixed(v < 10000 ? 1 : 0) + 'k';
91
- return (v / 1000000).toFixed(1) + 'M';
99
+ if (v < 1024) return String(v);
100
+ if (v < 1024 * 1024) return (v / 1024).toFixed(v < 10240 ? 1 : 0) + 'k';
101
+ return (v / (1024 * 1024)).toFixed(1) + 'M';
92
102
  }
93
103
 
94
104
  /**