deel-local-cli 1.7.0 → 1.10.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.
Files changed (61) hide show
  1. package/README.ko.md +1166 -0
  2. package/README.md +1223 -1107
  3. package/bin/deel.js +78 -12
  4. package/package.json +4 -4
  5. package/src/acp/map.js +1 -1
  6. package/src/acp/serve.js +40 -3
  7. package/src/agent/compact.js +314 -296
  8. package/src/agent/effort.js +27 -7
  9. package/src/agent/evidence.js +2 -0
  10. package/src/agent/filemem.js +141 -0
  11. package/src/agent/grade.js +51 -1
  12. package/src/agent/loop.js +218 -40
  13. package/src/agent/session.js +136 -14
  14. package/src/agent/store.js +186 -9
  15. package/src/agent/threads.js +26 -1
  16. package/src/backend/adapter.js +272 -23
  17. package/src/backend/learn.js +24 -0
  18. package/src/backend/mcp.js +96 -5
  19. package/src/backend/probe.js +131 -61
  20. package/src/backend/quota.js +29 -5
  21. package/src/backend/retry.js +3 -0
  22. package/src/backend/scanui.js +1 -1
  23. package/src/backend/toolfit.js +325 -0
  24. package/src/commands.js +170 -61
  25. package/src/completion.js +20 -2
  26. package/src/config.js +47 -2
  27. package/src/i18n/en.js +210 -3
  28. package/src/i18n/index.js +40 -0
  29. package/src/i18n/ja.js +204 -3
  30. package/src/i18n/ko.js +270 -3
  31. package/src/i18n/zh.js +204 -3
  32. package/src/lsp/client.js +49 -5
  33. package/src/oneshot.js +92 -5
  34. package/src/pack/sbom.js +30 -4
  35. package/src/pack/selfpack.js +26 -10
  36. package/src/pack/sheet.en.js +288 -0
  37. package/src/pack/tar.js +65 -2
  38. package/src/plugins/manage.js +46 -9
  39. package/src/repl.js +344 -61
  40. package/src/reset.js +397 -0
  41. package/src/safety/audit.js +95 -8
  42. package/src/safety/authcmd.js +316 -0
  43. package/src/safety/guard.js +143 -0
  44. package/src/safety/keystore.js +114 -38
  45. package/src/safety/undo.js +25 -6
  46. package/src/tools/desc.en.js +19 -14
  47. package/src/tools/index.js +349 -75
  48. package/src/tools/jobs.js +175 -42
  49. package/src/tools/lsp.js +5 -4
  50. package/src/tools/outline.js +7 -3
  51. package/src/tools/task.js +10 -6
  52. package/src/tools/todo.js +9 -2
  53. package/src/tools/verify.js +42 -7
  54. package/src/tools/webfetch.js +94 -10
  55. package/src/ui/export.js +1 -1
  56. package/src/ui/md.js +201 -5
  57. package/src/ui/motion.js +0 -1
  58. package/src/ui/pastechip.js +50 -3
  59. package/src/ui/pick.js +115 -0
  60. package/src/ui/screen.js +23 -3
  61. package/README.en.md +0 -1117
@@ -7,7 +7,12 @@
7
7
  import { req, headersFor, serverMessage } from './http.js';
8
8
  import { probeCtx } from './ctxsize.js';
9
9
  import { 눈검사메시지 } from './vision.js';
10
- import { 주소붙이기 } from './adapter.js';
10
+ import {
11
+ 주소붙이기, endpoint, 더할머리, buildBody, extractMessage,
12
+ assistantMessage, toolMessage,
13
+ } from './adapter.js';
14
+ import { 벤더 } from './toolfit.js';
15
+ import { 말 } from '../i18n/index.js';
11
16
 
12
17
  const READ_TOOL = {
13
18
  type: 'function',
@@ -22,32 +27,32 @@ const READ_TOOL = {
22
27
  },
23
28
  };
24
29
 
25
- // 규격별 요청 만들기 — 이 함수 하나가 openai/ollama 차이를 흡수한다.
26
- function build(shape, { model, messages, tools, stream, json, think, maxTokens = 128 }) {
27
- if (shape === 'ollama') {
28
- const body = { model, messages, stream: !!stream, options: { num_predict: maxTokens } };
29
- if (tools) body.tools = tools;
30
- if (json) body.format = json;
31
- if (think !== undefined) body.think = think;
32
- return { path: '/api/chat', body };
33
- }
34
- const body = { model, messages, stream: !!stream, max_tokens: maxTokens };
35
- if (tools) { body.tools = tools; body.tool_choice = 'auto'; }
36
- if (json) {
37
- body.response_format = { type: 'json_schema', json_schema: { name: 'probe', schema: json, strict: true } };
38
- }
39
- if (think !== undefined) body.reasoning_effort = think;
40
- return { path: '/chat/completions', body };
41
- }
30
+ /*
31
+ * ── 규격 차이는 여기서 흡수하지 않는다 ──────────────────────────────────
32
+ *
33
+ * 여기에 build() extract() 따로 있었다. 둘이 아는 규격은 `ollama` 와
34
+ * 「나머지 = OpenAI」 **둘뿐**이었고, `anthropic` 은 「나머지」 로 떨어졌다.
35
+ * 그래서 Claude 를 직접 붙이면 진단이 이렇게 갔다 —
36
+ *
37
+ * · 머리(anthropic-version)를 얹는다 → 그 하나로 400
38
+ * · 문 이름이 /chat/completions (있어야 할 것은 /messages)
39
+ * · 시킴말이 messages 안으로 들어간다 「모르는 역할」
40
+ * · 도구가 {type:'function', function:{…}} 모양으로 간다
41
+ * · 답을 choices[0].message 에서 찾는다 (실제로는 content 블록 배열)
42
+ *
43
+ * 첫 칸(기본 대화)이 400 으로 죽으면 나머지 일곱 칸은 전부 「확인 불가」 로
44
+ * 건너뛴다. 결과 `deel setup` 이 저장하는 프로필에 streaming·tools·json·
45
+ * vision **다 false** 로 적힌다 — Claude 를 붙였는데 도구를 아예 안 쓰는
46
+ * 연결이 만들어진다. 붙기는 붙으니 아무도 고장이라고 생각하지 않는다.
47
+ *
48
+ * adapter.js 첫 줄은 처음부터 "진단(probe)과 에이전트 루프가 같은 함수를
49
+ * 쓴다" 고 적어 두었다. 실제로는 안 썼다. 이제 쓴다 — 규격이 넷째가 되어도
50
+ * 이 파일은 안 고쳐도 된다.
51
+ */
42
52
 
43
- // 응답에서 본문과 도구호출을 꺼낸다.
44
- function extract(shape, json) {
45
- if (shape === 'ollama') {
46
- const m = json?.message ?? {};
47
- return { content: m.content ?? '', toolCalls: m.tool_calls ?? [], thinking: m.thinking ?? '' };
48
- }
49
- const m = json?.choices?.[0]?.message ?? {};
50
- return { content: m.content ?? '', toolCalls: m.tool_calls ?? [], thinking: m.reasoning_content ?? '' };
53
+ /** 회사가 무엇을 받는지까지 봐야 몸통이 맞다 (toolfit.js 의 벤더). */
54
+ function 몸통(conn, opts) {
55
+ return buildBody(conn.kind, { model: conn.model, 회사: 벤더(conn), maxTokens: 128, ...opts });
51
56
  }
52
57
 
53
58
  // 추론 모델일 때 기본 대화 칸에 덧붙일 설명.
@@ -55,6 +60,31 @@ function c_note(retried) {
55
60
  return retried ? ' (추론 모델 — 사고를 끄고 다시 물어 확인)' : ' (추론 모델)';
56
61
  }
57
62
 
63
+ /*
64
+ * ── 이 문으로는 안 나오는 모델 (Codex 계열) ─────────────────────────────
65
+ *
66
+ * `gpt-5-codex` 계열은 OpenAI 가 **Responses API(/responses)로만** 내준다.
67
+ * `/chat/completions` 로 물으면 404 나 400 이 오는데, 그 화면에는 상태 코드
68
+ * 한 줄만 남는다. 그러면 사람은 열쇠를 의심하거나 모델 이름을 잘못 적은 줄
69
+ * 알고, 맞는 이름을 몇 번씩 다시 넣어 본다 — 이름은 처음부터 맞았다.
70
+ *
71
+ * ── 왜 이름을 보나. 이 파일에서만 본다 ─────────────────────────────────
72
+ *
73
+ * 어디로 보낼지는 **주소로만** 정한다(1.9). 그 규칙은 여기서도 안 깬다 —
74
+ * 여기서 이름을 보는 것은 보낼 곳을 고르려는 것이 아니라, **이미 실패한
75
+ * 요청에 까닭을 붙이려는** 것뿐이다. 그래서 미리 막지 않는다. 요청은 그대로
76
+ * 나가고, 실패했을 때만 한 줄이 붙는다. 언젠가 OpenAI 가 이 문으로도 내주면
77
+ * 요청이 그냥 성공하고 이 줄은 저절로 안 나온다.
78
+ *
79
+ * 게이트웨이는 해당 없다. 그 뒤에 무엇이 걸려 있는지 우리는 모르고, 사내
80
+ * 게이트웨이가 제 나름대로 이 계열을 chat 창구로 내주는 일도 있다.
81
+ */
82
+ const CODEX계열 = /^(?:gpt-5-codex|codex-)/i;
83
+
84
+ export function 코덱스인가(conn) {
85
+ return 벤더(conn) === 'openai' && CODEX계열.test(String(conn?.model ?? ''));
86
+ }
87
+
58
88
  const SKIPPED = [
59
89
  ['system', '시스템 메시지'],
60
90
  ['stream', '스트리밍'],
@@ -69,7 +99,14 @@ const SKIPPED = [
69
99
  export async function probe(conn, onStep = () => {}) {
70
100
  const { kind: shape, base, auth, model } = conn;
71
101
  const key = conn.key ?? '';
72
- const H = () => headersFor(auth, key);
102
+ /*
103
+ * 판 머리를 여기서도 얹는다 (adapter.js 의 더할머리).
104
+ *
105
+ * Anthropic 규격은 `anthropic-version` 하나가 없으면 400 이다. 열쇠가
106
+ * 멀쩡해도 그렇다. 진단 화면은 그 400 을 「연결 실패」 로 적고, 사람은
107
+ * 열쇠를 다시 받으러 간다 — 여덟 칸이 전부 그 한 줄 때문에 빨개진다.
108
+ */
109
+ const H = () => headersFor(auth, key, 더할머리(shape));
73
110
  /*
74
111
  * 물음표 뒤를 끝에 남겨야 한다 (adapter.js 의 주소붙이기).
75
112
  *
@@ -85,23 +122,23 @@ export async function probe(conn, onStep = () => {}) {
85
122
  const facts = { shape, base, auth, model };
86
123
 
87
124
  const add = (r) => { results.push(r); onStep(r); return r; };
88
- const call = (opts) => {
89
- const { path, body } = build(shape, { model, ...opts });
90
- return req(url(path), {
91
- method: 'POST',
92
- headers: H(),
93
- body,
94
- timeout: opts.timeout ?? 60000,
95
- stream: opts.stream,
96
- });
97
- };
125
+ const call = (opts) => req(url(endpoint(shape)), {
126
+ method: 'POST',
127
+ headers: H(),
128
+ body: 몸통(conn, opts),
129
+ timeout: opts.timeout ?? 60000,
130
+ stream: opts.stream,
131
+ });
132
+ // 답 읽기도 루프와 같은 함수를 쓴다. 도구 부름은 { id, name, args } 로
133
+ // 고르게 나온다 — 규격마다 다른 자리를 여기서 또 헤아리지 않는다.
134
+ const 읽기 = (r) => extractMessage(shape, r?.json);
98
135
 
99
136
  // 1. 기본 대화 — 이게 안 되면 나머지는 볼 필요가 없다.
100
137
  // 추론 모델은 본문이 전부 thinking 으로 가고 토큰 상한에 잘린다.
101
138
  // 그걸 "안됨"으로 볼 수 없으므로, 사고를 끄고 넉넉히 한 번 더 물어본다.
102
139
  const ASK = { messages: [{ role: 'user', content: '1+1은? 숫자만 답하세요.' }] };
103
140
  let basic = await call({ ...ASK, maxTokens: 256 });
104
- let got = basic.ok ? extract(shape, basic.json) : { content: '', thinking: '' };
141
+ let got = basic.ok ? 읽기(basic) : { content: '', thinking: '' };
105
142
  let thinkingModel = false;
106
143
  let retried = false;
107
144
 
@@ -110,12 +147,12 @@ export async function probe(conn, onStep = () => {}) {
110
147
  retried = true;
111
148
  const second = await call({ ...ASK, maxTokens: 1024, think: false, timeout: 90000 });
112
149
  if (second.ok) {
113
- const e2 = extract(shape, second.json);
150
+ const e2 = 읽기(second);
114
151
  if (e2.content) { basic = second; got = e2; }
115
152
  else {
116
153
  // 사고를 못 끄는 서버 — 상한만 크게 올려 한 번 더.
117
154
  const third = await call({ ...ASK, maxTokens: 2048, timeout: 120000 });
118
- if (third.ok && extract(shape, third.json).content) { basic = third; got = extract(shape, third.json); }
155
+ if (third.ok && 읽기(third).content) { basic = third; got = 읽기(third); }
119
156
  }
120
157
  }
121
158
  }
@@ -136,7 +173,11 @@ export async function probe(conn, onStep = () => {}) {
136
173
  ? got.thinking
137
174
  ? '사고만 나오고 본문이 안 나옵니다 — 토큰 상한을 크게 올려야 합니다'
138
175
  : '응답이 비어 있습니다 — 모델 이름을 확인하세요'
139
- : serverMessage(basic),
176
+ // 못 붙은 까닭을 아는 자리가 하나 있다. 상태 코드만 남기면 사람은
177
+ // 열쇠를 의심하는데, 열쇠도 이름도 처음부터 맞았다 (코덱스인가 머리말).
178
+ : 코덱스인가(conn)
179
+ ? `${serverMessage(basic)}\n${말('probe.codexOnly', { 모델: model })}`
180
+ : serverMessage(basic),
140
181
  ms: basic.ms,
141
182
  });
142
183
  if (!basicOk) {
@@ -154,7 +195,7 @@ export async function probe(conn, onStep = () => {}) {
154
195
  { role: 'user', content: '안녕하세요' },
155
196
  ],
156
197
  });
157
- const sysHit = sys.ok && /DEEL/i.test(extract(shape, sys.json).content);
198
+ const sysHit = sys.ok && /DEEL/i.test(읽기(sys).content);
158
199
  add({
159
200
  id: 'system',
160
201
  label: '시스템 메시지',
@@ -209,15 +250,18 @@ export async function probe(conn, onStep = () => {}) {
209
250
  maxTokens: 512,
210
251
  timeout: 90000,
211
252
  });
212
- const tcalls = tl.ok ? extract(shape, tl.json).toolCalls : [];
253
+ const tcalls = tl.ok ? 읽기(tl).toolCalls : [];
213
254
  const gotCall = tcalls.length > 0;
214
- const argOk = gotCall && JSON.stringify(tcalls[0]?.function?.arguments ?? '').includes('config');
255
+ // 부름은 이미 { id, name, args } 로 고르게 나온다 (extractMessage).
256
+ // 예전에는 여기서 OpenAI 날모양(function.arguments)을 직접 팠다 —
257
+ // 그러면 Anthropic 은 도구를 제대로 불러도 「안 불렀다」 로 읽힌다.
258
+ const argOk = gotCall && JSON.stringify(tcalls[0]?.args ?? '').includes('config');
215
259
  add({
216
260
  id: 'tools',
217
261
  label: '도구 호출',
218
262
  status: gotCall ? (argOk ? 'ok' : 'warn') : 'no',
219
263
  detail: gotCall
220
- ? `${tcalls[0]?.function?.name} 호출됨${argOk ? '' : ' — 인자가 부정확, 편집 신뢰성 작업이 더 필요합니다'}`
264
+ ? `${tcalls[0]?.name} 호출됨${argOk ? '' : ' — 인자가 부정확, 편집 신뢰성 작업이 더 필요합니다'}`
221
265
  : tl.ok ? '도구를 안 부르고 글로만 답합니다' : serverMessage(tl),
222
266
  ms: tl.ms,
223
267
  });
@@ -227,12 +271,15 @@ export async function probe(conn, onStep = () => {}) {
227
271
  if (gotCall) {
228
272
  const tc = tcalls[0];
229
273
  const callId = tc.id ?? 'call_1';
230
- const assistantMsg = shape === 'ollama'
231
- ? { role: 'assistant', content: '', tool_calls: tcalls }
232
- : { role: 'assistant', content: null, tool_calls: [{ id: callId, type: 'function', function: tc.function }] };
233
- const toolMsg = shape === 'ollama'
234
- ? { role: 'tool', tool_name: tc.function?.name, content: '{"port": 7099}' }
235
- : { role: 'tool', tool_call_id: callId, content: '{"port": 7099}' };
274
+ /*
275
+ * 되돌려 넣는 모양도 루프와 같은 함수로 짓는다 (adapter.js).
276
+ *
277
+ * 여기에 규격별 갈래를 손으로 적어 두면 규격이 하나 늘 때마다 이 자리가
278
+ * 조용히 틀린다 Anthropic 에서는 도구 결과가 사람 차례로 가야 하는데
279
+ * `role:'tool'` 보내 「모르는 역할」 받고 있었다.
280
+ */
281
+ const assistantMsg = assistantMessage(shape, { content: '', toolCalls: [tc] });
282
+ const toolMsg = toolMessage(shape, { callId, name: tc.name, content: '{"port": 7099}' });
236
283
  const rt = await call({
237
284
  ...quiet,
238
285
  timeout: 90000,
@@ -245,7 +292,7 @@ export async function probe(conn, onStep = () => {}) {
245
292
  tools: [READ_TOOL],
246
293
  maxTokens: 256,
247
294
  });
248
- const said = rt.ok ? extract(shape, rt.json).content : '';
295
+ const said = rt.ok ? 읽기(rt).content : '';
249
296
  add({
250
297
  id: 'toolresult',
251
298
  label: '도구 결과 되돌리기',
@@ -279,8 +326,20 @@ export async function probe(conn, onStep = () => {}) {
279
326
  timeout: 90000,
280
327
  });
281
328
  if (!js.ok) break;
282
- raw = extract(shape, js.json).content ?? '';
283
- try { parsed = JSON.parse(raw); } catch {}
329
+ raw = 읽기(js).content ?? '';
330
+ /*
331
+ * **객체일 때만** 읽었다고 한다.
332
+ *
333
+ * 답 모양을 강제하는 칸이 없는 규격(Anthropic)에서는 이 물음에 그냥
334
+ * `21` 이라고 답한다. `JSON.parse('21')` 은 오류 없이 숫자 21 을 준다.
335
+ * 그런데 그다음 줄이 `'answer' in parsed` 라, 숫자에 `in` 을 써서
336
+ * **TypeError 로 진단 전체가 통째로 죽었다.** 스키마를 안 지킨 것은
337
+ * 「구조적 출력 안 됨」 이라고 적을 일이지 프로그램이 끝날 일이 아니다.
338
+ */
339
+ try {
340
+ const 읽힌것 = JSON.parse(raw);
341
+ if (읽힌것 && typeof 읽힌것 === 'object' && !Array.isArray(읽힌것)) parsed = 읽힌것;
342
+ } catch { /* 글로만 답하는 서버 — 아래에서 '스키마를 안 지킴' 으로 적는다 */ }
284
343
  }
285
344
  const jsonOk = !!(parsed && 'answer' in parsed);
286
345
  add({
@@ -316,7 +375,7 @@ export async function probe(conn, onStep = () => {}) {
316
375
  maxTokens: 32,
317
376
  timeout: 60000,
318
377
  });
319
- const 눈있음 = !!(눈.ok && extract(shape, 눈.json).content);
378
+ const 눈있음 = !!(눈.ok && 읽기().content);
320
379
  add({
321
380
  id: 'vision',
322
381
  label: '그림 보기',
@@ -330,9 +389,18 @@ export async function probe(conn, onStep = () => {}) {
330
389
  });
331
390
  facts.vision = 눈있음;
332
391
 
333
- // 7. 추론 강도 조절 — 낮음/높음이 실제로 다른 결과를 내느냐.
334
- // 상한에 걸리면 같은 숫자가 나와 비교가 무의미해진다. 넉넉히 준다.
335
- const THINK_CAP = 1500;
392
+ /*
393
+ * 7. 추론 강도 조절 낮음/높음이 실제로 다른 결과를 내느냐.
394
+ *
395
+ * 상한에 걸리면 둘 다 같은 숫자가 나와 비교가 무의미해진다. 넉넉히 준다.
396
+ *
397
+ * Anthropic 규격은 여기서 상한을 더 크게 잡아야 한다. 그쪽은 강도를 말이
398
+ * 아니라 **출력 상한 안에서 나가는 예산**으로 주는데(adapter.js 의
399
+ * 생각예산), 1,500 으로는 답에 남길 자리를 빼고 나면 최소 예산 1,024 를
400
+ * 못 만든다. 그러면 생각이 아예 안 켜진 채로 두 번을 부르고, 이 칸은
401
+ * 「차이 없음」 이라고 적는다 — 켤 수 있는 모델에 대고 못 켠다고 적는 셈이다.
402
+ */
403
+ const THINK_CAP = shape === 'anthropic' ? 8000 : 1500;
336
404
  const seen = [];
337
405
  for (const lv of ['low', 'high']) {
338
406
  const r = await call({
@@ -342,13 +410,15 @@ export async function probe(conn, onStep = () => {}) {
342
410
  timeout: 120000,
343
411
  });
344
412
  if (r.ok) {
345
- const e = extract(shape, r.json);
413
+ const e = 읽기(r);
346
414
  seen.push({
347
415
  lv,
348
416
  ms: r.ms,
349
417
  thought: (e.thinking ?? '').length,
350
- out: r.json?.usage?.completion_tokens ?? r.json?.eval_count ?? 0,
351
- capped: (r.json?.done_reason ?? r.json?.choices?.[0]?.finish_reason) === 'length',
418
+ // 토큰 수와 끝난 까닭도 규격마다 이름이 다르다. 읽는 자리를 하나로 모은다 —
419
+ // 여기서 OpenAI 이름만 보면 Anthropic 은 늘 0 이고 '안 잘림' 이 된다.
420
+ out: e.usage?.out ?? 0,
421
+ capped: e.stopped === 'length' || e.stopped === 'max_tokens',
352
422
  });
353
423
  } else {
354
424
  seen.push({ lv, ms: r.ms, err: serverMessage(r) });
@@ -16,13 +16,37 @@
16
16
  // `x-ratelimit-remaining-tokens` 를 쓰기도 하고 아예 안 주기도 한다.
17
17
  // 그래서 **아는 이름만 읽고, 없으면 없다고 한다.** 없는 것을 0 으로 치면
18
18
  // 화면에 "0 남음" 이 떠서, 멀쩡한데 다 썼다고 믿게 된다.
19
+ //
20
+ // Anthropic 은 `anthropic-` 을 앞에 달고, 남은 것을 **뒤에** 적는다
21
+ // (`anthropic-ratelimit-requests-remaining`). 그 이름을 몰라서, Claude 직통과
22
+ // Bedrock 게이트웨이에서는 이 줄이 **언제나 비어 있었다** — 서버는 매 응답에
23
+ // 남은 양을 실어 보내고 있었는데 우리만 못 읽었다. 그래서 그 두 자리에서는
24
+ // 아직도 429 를 맞아야만 알 수 있었다. 이 파일이 없애려던 바로 그 상황이다.
19
25
 
20
26
  // 읽을 이름들. 앞에서부터 처음 있는 것 하나를 쓴다.
21
- const 요청남음 = ['x-ratelimit-remaining-requests', 'ratelimit-remaining-requests', 'x-ratelimit-remaining'];
22
- const 토큰남음 = ['x-ratelimit-remaining-tokens', 'ratelimit-remaining-tokens'];
23
- const 요청한도 = ['x-ratelimit-limit-requests', 'ratelimit-limit-requests'];
24
- const 토큰한도 = ['x-ratelimit-limit-tokens', 'ratelimit-limit-tokens'];
25
- const 다시언제 = ['retry-after', 'x-ratelimit-reset-requests', 'x-ratelimit-reset-tokens', 'ratelimit-reset'];
27
+ const 요청남음 = [
28
+ 'x-ratelimit-remaining-requests', 'ratelimit-remaining-requests', 'x-ratelimit-remaining',
29
+ 'anthropic-ratelimit-requests-remaining',
30
+ ];
31
+ const 토큰남음 = [
32
+ 'x-ratelimit-remaining-tokens', 'ratelimit-remaining-tokens',
33
+ 'anthropic-ratelimit-tokens-remaining',
34
+ // 입력·출력을 따로 세는 판도 있다. 둘 중 먼저 바닥나는 쪽이 곧 막히는 쪽이다.
35
+ 'anthropic-ratelimit-input-tokens-remaining', 'anthropic-ratelimit-output-tokens-remaining',
36
+ ];
37
+ const 요청한도 = ['x-ratelimit-limit-requests', 'ratelimit-limit-requests', 'anthropic-ratelimit-requests-limit'];
38
+ const 토큰한도 = [
39
+ 'x-ratelimit-limit-tokens', 'ratelimit-limit-tokens', 'anthropic-ratelimit-tokens-limit',
40
+ 'anthropic-ratelimit-input-tokens-limit', 'anthropic-ratelimit-output-tokens-limit',
41
+ ];
42
+ /*
43
+ * 풀리는 때. Anthropic 은 초가 아니라 **날짜(RFC 3339)** 로 준다 —
44
+ * `언제풀리나` 가 Date.parse 로 받아 지금과의 차이를 초로 바꾼다.
45
+ */
46
+ const 다시언제 = [
47
+ 'retry-after', 'x-ratelimit-reset-requests', 'x-ratelimit-reset-tokens', 'ratelimit-reset',
48
+ 'anthropic-ratelimit-requests-reset', 'anthropic-ratelimit-tokens-reset',
49
+ ];
26
50
 
27
51
  function 골라(머리, 이름들) {
28
52
  if (!머리) return null;
@@ -17,6 +17,9 @@
17
17
  // · 세 번까지. 그 뒤로는 사실대로 말한다. 열 번 두드리는 것은 할당량을 더 빨리
18
18
  // 태우는 짓이다.
19
19
  // · 400·401·403·404 는 다시 불러 봐야 같다. 한 번만 부르고 서버가 한 말을 보여 준다.
20
+ // 401 에 딱 하나 예외가 있는데, 그것도 여기가 아니라 backend/adapter.js 가 한다 —
21
+ // 열쇠받기(safety/authcmd.js)가 걸려 있으면 **같은 열쇠로 다시 부르는 것이 아니라**
22
+ // 새 열쇠를 받아서 부르는 것이라, 「불러 봐야 같다」 에 해당하지 않는다. 딱 한 번이다.
20
23
  // · ECONNREFUSED 는 서버가 꺼진 것이고, 시간 초과는 5분을 또 기다릴 일이 아니다.
21
24
  // 둘 다 안 부른다.
22
25
  //
@@ -67,7 +67,7 @@ export async function runScan(flags = {}) {
67
67
  const wRun = Math.max(10, ...found.map((f) => width(f.runtime)));
68
68
  for (const f of found) {
69
69
  const 자리 = `${f.host}:${f.port}`;
70
- const 규격 = f.kind === 'ollama' ? 'Ollama 규격' : 규격이름(f.kind);
70
+ const 규격 = 규격이름(f.kind);
71
71
  const 표시 = f.guessed ? c.gray(' (추정)') : '';
72
72
  say(` ${c.hcyan('◆')} ${c.bold(pad(f.runtime, wRun))}${표시} ${c.gray(pad(자리, 22))}${c.gray(pad(규격, 14))}${c.gray(f.ms + 'ms')}`);
73
73
  if (f.locked) {