deel-local-cli 1.15.1 → 1.17.1

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 (51) hide show
  1. package/README.ko.md +118 -17
  2. package/README.md +119 -16
  3. package/bin/deel.js +430 -6
  4. package/package.json +1 -1
  5. package/src/acp/serve.js +39 -1
  6. package/src/agent/agents.js +203 -0
  7. package/src/agent/asks.js +58 -2
  8. package/src/agent/budget.js +33 -0
  9. package/src/agent/effort.js +69 -0
  10. package/src/agent/loop.js +219 -19
  11. package/src/agent/models.js +4 -0
  12. package/src/agent/modes.js +179 -17
  13. package/src/agent/outschema.js +351 -0
  14. package/src/agent/route.js +119 -8
  15. package/src/agent/session.js +13 -2
  16. package/src/backend/adapter.js +94 -7
  17. package/src/backend/clientcert.js +164 -0
  18. package/src/backend/http.js +232 -26
  19. package/src/backend/mcp.js +194 -30
  20. package/src/backend/wire.js +10 -0
  21. package/src/cmdnames.js +3 -0
  22. package/src/commands.js +158 -15
  23. package/src/completion.js +35 -4
  24. package/src/config.js +86 -9
  25. package/src/configexplain.js +177 -0
  26. package/src/doctor.js +195 -0
  27. package/src/i18n/en.js +50 -0
  28. package/src/i18n/ja.js +50 -0
  29. package/src/i18n/ko.js +50 -0
  30. package/src/i18n/zh.js +50 -0
  31. package/src/lsp/client.js +2 -2
  32. package/src/oneshot.js +181 -4
  33. package/src/pack/sbom.js +77 -3
  34. package/src/pack/selfpack.js +1 -1
  35. package/src/pack/sheet.en.js +1 -1
  36. package/src/repl.js +74 -3
  37. package/src/safety/hooks.js +389 -0
  38. package/src/safety/policy.js +74 -0
  39. package/src/safety/shellenv.js +116 -0
  40. package/src/safety/trust.js +230 -0
  41. package/src/setup.js +9 -0
  42. package/src/stats.js +162 -0
  43. package/src/tools/doc2md.js +266 -0
  44. package/src/tools/docs.js +26 -6
  45. package/src/tools/fig.js +393 -0
  46. package/src/tools/hwpxwrite.js +268 -0
  47. package/src/tools/index.js +176 -16
  48. package/src/tools/jobs.js +11 -6
  49. package/src/tools/kiwi.js +293 -0
  50. package/src/tools/spawn.js +19 -1
  51. package/src/tools/task.js +8 -2
@@ -0,0 +1,177 @@
1
+ // `deel config explain <칸>` — 이 값은 **어디서 왔나**.
2
+ //
3
+ // ── 왜 필요한가 ────────────────────────────────────────────────────────
4
+ //
5
+ // 설정을 읽는 자리가 넷이다 — 관리 정책 · 환경변수 · 프로젝트 설정 · 이 PC
6
+ // 설정. 겹치는 규칙은 문서에 적어 두었지만, 문서를 읽는 것과 **지금 이 자리에서
7
+ // 무엇이 이겼는지 아는 것**은 다른 일이다.
8
+ //
9
+ // 막히는 모양은 늘 같다. 파일에 분명히 적어 놨는데 안 먹는다. 그러면 사람은
10
+ // 그 파일을 고치고, 또 고치고, 결국 설정이라는 것을 안 믿게 된다. 실제로 값을
11
+ // 쥐고 있던 것은 환경변수 하나이거나, 프로젝트 설정을 안 믿는 폴더여서
12
+ // 읽지도 않은 것이다. 둘 다 **말해 주면 5초에 끝나는** 일이다.
13
+ //
14
+ // 그래서 층을 하나씩 열어서 「이 칸을 적어 둔 곳」 을 다 보여 주고, 어느 것이
15
+ // 이겼는지 표시한다. 값이 겹칠 때만 쓸모 있는 화면이 아니라, **아무 데도 안
16
+ // 적혀 있다**는 것을 보여 줄 때도 똑같이 쓸모 있다.
17
+ //
18
+ // ── 비밀은 안 보여 준다 ────────────────────────────────────────────────
19
+ //
20
+ // 열쇠 칸(apiKey)은 값을 안 적고 「적혀 있음」 만 적는다. 이 화면은 그대로
21
+ // 캡처되어 사내 메신저로 간다.
22
+ import { existsSync, readFileSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import { homeDir } from './config.js';
25
+ import { 믿나 } from './safety/trust.js';
26
+ import { 정책읽기, 정책자리 } from './safety/policy.js';
27
+
28
+ /** 값을 안 보여 줄 칸. 이름만 봐도 알 수 있는 것만 넣는다. */
29
+ const 가릴칸 = new Set(['apiKey', 'key', 'passphrase', '암호']);
30
+
31
+ /**
32
+ * 이 칸을 정할 수 있는 환경변수.
33
+ *
34
+ * 손으로 적는다. 자동으로 긁으면 「무엇이 이 칸을 이길 수 있나」 가 코드를
35
+ * 읽어야만 아는 것이 되는데, 그건 이 명령이 없애려는 바로 그 상태다.
36
+ */
37
+ const 환경변수 = {
38
+ apiKey: ['DEEL_API_KEY', 'DEEL_KEY_<프로필ID>'],
39
+ shell: ['DEEL_SHELL'],
40
+ offline: [],
41
+ proxy: ['HTTPS_PROXY', 'HTTP_PROXY', 'ALL_PROXY', 'NO_PROXY'],
42
+ lang: ['DEEL_LANG'],
43
+ };
44
+
45
+ /*
46
+ * 비밀 칸의 값은 **읽는 자리에서** 바꿔치기한다.
47
+ *
48
+ * 그리는 자리에서만 가리면 `--json` 이 날것을 그대로 낸다. 그 출력은
49
+ * 파이프를 타고 로그로, 사내 티켓으로 간다 — 화면만 가려 놓고 안전하다고
50
+ * 하는 것이 이런 자리에서 제일 흔한 사고다.
51
+ */
52
+ export const 비밀표시 = '(적혀 있음 — 값은 안 보여 줍니다)';
53
+ const 지운값 = (칸, v) => (v !== undefined && 가릴칸.has(칸) ? 비밀표시 : v);
54
+
55
+ const 값보이기 = (칸, v) => {
56
+ if (v === undefined) return null;
57
+ if (v === null) return 'null';
58
+ if (typeof v === 'object') return JSON.stringify(v);
59
+ return String(v);
60
+ };
61
+
62
+ /**
63
+ * 한 칸이 어느 층에 적혀 있나.
64
+ *
65
+ * @param {string} 칸 `offline` 처럼 맨 위 칸, 또는 `profiles.사내.model` 처럼
66
+ * 프로필 안의 칸
67
+ * @returns {{칸:string, 층들:Array, 이긴층:object|null, 환경변수:string[]}}
68
+ */
69
+ export function 설명(칸, { root = process.cwd(), env = process.env } = {}) {
70
+ const 조각 = String(칸 ?? '').split('.').filter(Boolean);
71
+ const 층들 = [];
72
+
73
+ const 파일읽기 = (자리) => {
74
+ if (!existsSync(자리)) return { 있나: false };
75
+ try { return { 있나: true, 값: JSON.parse(readFileSync(자리, 'utf8')) }; }
76
+ catch (err) { return { 있나: true, 탈: err?.message ?? String(err) }; }
77
+ };
78
+
79
+ const 파고들기 = (덩이) => {
80
+ let 여기 = 덩이;
81
+ for (let i = 0; i < 조각.length; i++) {
82
+ if (여기 == null || typeof 여기 !== 'object') return undefined;
83
+ const 이름 = 조각[i];
84
+ /*
85
+ * `profiles.<이름>.<칸>` 은 배열을 이름으로 뒤진다.
86
+ *
87
+ * 사람이 설정에서 프로필을 부르는 이름은 배열 번호가 아니라 name 이다.
88
+ * 번호로 적게 하면 프로필 하나를 지웠을 때 이 명령이 조용히 딴 것을
89
+ * 가리킨다.
90
+ */
91
+ if (Array.isArray(여기)) {
92
+ 여기 = 여기.find((x) => x?.name === 이름 || x?.id === 이름);
93
+ continue;
94
+ }
95
+ 여기 = 여기[이름];
96
+ }
97
+ return 여기;
98
+ };
99
+
100
+ const 끝칸이름지금 = 조각[조각.length - 1] ?? '';
101
+
102
+ // ── 1. 이 PC 설정 (제일 아래) ───────────────────────────────────────
103
+ const 집자리 = join(homeDir(), 'config.json');
104
+ const 집 = 파일읽기(집자리);
105
+ if (집.있나) {
106
+ 층들.push({
107
+ 층: '이 PC 설정', 자리: 집자리, 읽나: !집.탈,
108
+ 값: 집.탈 ? undefined : 지운값(끝칸이름지금, 파고들기(집.값)), 탈: 집.탈 ?? null,
109
+ });
110
+ }
111
+
112
+ // ── 2. 프로젝트 설정 (믿는 폴더에서만) ──────────────────────────────
113
+ const 방자리 = join(root, '.deel', 'config.json');
114
+ if (existsSync(방자리)) {
115
+ const 믿나결과 = 믿나(root);
116
+ const 방 = 파일읽기(방자리);
117
+ 층들.push({
118
+ 층: '프로젝트 설정', 자리: 방자리, 읽나: 믿나결과 && !방.탈,
119
+ 값: 방.탈 ? undefined : 지운값(끝칸이름지금, 파고들기(방.값)), 탈: 방.탈 ?? null,
120
+ // 읽지도 않는 파일에 적어 둔 것이 이 화면에서 제일 자주 나오는 답이다.
121
+ 왜못읽나: 믿나결과 ? null : '믿는 폴더가 아닙니다 (deel trust)',
122
+ });
123
+ }
124
+
125
+ // ── 3. 환경변수 ─────────────────────────────────────────────────────
126
+ const 끝칸 = 조각[조각.length - 1] ?? '';
127
+ const 볼환경 = 환경변수[끝칸] ?? [];
128
+ const 켜진환경 = 볼환경.filter((이름) => !이름.includes('<') && env[이름]);
129
+ if (켜진환경.length) {
130
+ 층들.push({
131
+ 층: '환경변수', 자리: 켜진환경.join(' · '), 읽나: true,
132
+ 값: 지운값(끝칸, env[켜진환경[0]]),
133
+ });
134
+ }
135
+
136
+ // ── 4. 관리 정책 (제일 위) ──────────────────────────────────────────
137
+ const 정책 = 정책읽기({ env, 다시: true });
138
+ if (정책.값 && typeof 정책.값 === 'object') {
139
+ const 그값 = 정책.값[끝칸];
140
+ if (그값 !== undefined) {
141
+ 층들.push({ 층: '관리 정책', 자리: 정책.곳 ?? 정책자리(env)[0], 읽나: true, 값: 지운값(끝칸, 그값) });
142
+ }
143
+ }
144
+
145
+ // 이긴 층 — 뒤에 온 것이 이긴다. 다만 **읽는 층** 중에서만 고른다.
146
+ const 쓸수있는것 = 층들.filter((x) => x.읽나 && x.값 !== undefined);
147
+ const 이긴층 = 쓸수있는것.length ? 쓸수있는것[쓸수있는것.length - 1] : null;
148
+
149
+ return { 칸, 층들, 이긴층, 환경변수: 볼환경 };
150
+ }
151
+
152
+ /** 화면에 그릴 줄들. 색은 부르는 쪽이 입힌다. */
153
+ export function 설명줄들(r) {
154
+ const 줄 = [];
155
+ if (r.이긴층) 줄.push({ 갈래: '값', 글: `${값보이기(끝칸이름(r.칸), r.이긴층.값)}`, 곁: `${r.이긴층.층}` });
156
+ else 줄.push({ 갈래: '없음', 글: '아무 데도 안 적혀 있습니다 (기본값으로 돕니다)', 곁: null });
157
+
158
+ for (const 층 of r.층들) {
159
+ const 값 = 값보이기(끝칸이름(r.칸), 층.값);
160
+ 줄.push({
161
+ 갈래: 층 === r.이긴층 ? '이김' : (층.읽나 ? '아래' : '안읽음'),
162
+ 글: `${층.층}`,
163
+ 곁: [
164
+ 층.자리,
165
+ 값 !== null ? `= ${값}` : '(이 칸은 없음)',
166
+ 층.왜못읽나 ? `— ${층.왜못읽나}` : null,
167
+ 층.탈 ? `— 못 읽었습니다: ${층.탈}` : null,
168
+ ].filter(Boolean).join(' '),
169
+ });
170
+ }
171
+ if (r.환경변수.length) {
172
+ 줄.push({ 갈래: '참고', 글: '이 칸을 이길 수 있는 환경변수', 곁: r.환경변수.join(' · ') });
173
+ }
174
+ return 줄;
175
+ }
176
+
177
+ const 끝칸이름 = (칸) => String(칸 ?? '').split('.').filter(Boolean).pop() ?? '';
package/src/doctor.js ADDED
@@ -0,0 +1,195 @@
1
+ // `deel doctor` — **붙기 전에** 이 자리의 조건을 하나씩 본다.
2
+ //
3
+ // ── diagnose 와 무엇이 다른가 ──────────────────────────────────────────
4
+ //
5
+ // `deel diagnose` 는 **모델이 일을 할 수 있나**를 잰다 — 도구를 부르나, 답을
6
+ // 흘려보내나, 창이 얼마나 넓나. 그건 붙은 다음의 이야기다.
7
+ //
8
+ // 그런데 사내에서 막히는 자리는 대부분 그 앞이다. 프록시를 안 거쳐서, 사내
9
+ // 루트가 없어서, 열쇠를 못 꺼내서, 인증서 경로에 오타가 나서. 그때 사람이
10
+ // 받는 것은 「fetch failed」 나 TLS 악수 실패 한 줄이고, 그 한 줄로는 여섯
11
+ // 가지 원인 중 어느 것인지 알 수가 없다. 담당자에게 물어볼 말조차 못 만든다.
12
+ //
13
+ // 그래서 여기서는 **조건을 하나씩 따로 재고, 각각 어디서 온 값인지 적는다.**
14
+ // 화면 한 장이 그대로 담당자에게 보낼 질문이 되게 하는 것이 목표다.
15
+ //
16
+ // ── 규칙 ───────────────────────────────────────────────────────────────
17
+ //
18
+ // **모르는 것은 모른다고 적는다.** 못 잰 것을 초록으로 세면 이 화면은 사람을
19
+ // 엉뚱한 데로 보낸다. 못 잰 것은 `?` 로 적고 왜 못 쟀는지 붙인다.
20
+ //
21
+ // **비밀은 한 글자도 안 적는다.** 이 화면은 그대로 캡처되어 사내 메신저로
22
+ // 간다. 열쇠는 있나 없나와 어디서 왔나만, 인증서 암호는 어디서 왔나만 적는다.
23
+ import { existsSync, readFileSync, statSync } from 'node:fs';
24
+ import { join } from 'node:path';
25
+ import { req } from './backend/http.js';
26
+ import { 프록시설정, 프록시고르기 } from './backend/proxy.js';
27
+ import { 인증서설정, 인증서등록, 인증서찾기, 인증서말 } from './backend/clientcert.js';
28
+ import { 주소가리기 } from './safety/secrets.js';
29
+ import { 믿나 } from './safety/trust.js';
30
+ import { 훅읽기, 훅줄들 } from './safety/hooks.js';
31
+ import { 에이전트읽기, 에이전트줄들 } from './agent/agents.js';
32
+ import { headersFor } from './backend/http.js';
33
+
34
+ /** 한 줄. `상태` 는 ok · warn · no · unknown 넷뿐이다. */
35
+ const 줄 = (상태, 이름, 값, 덧말 = null) => ({ 상태, 이름, 값, 덧말 });
36
+
37
+ /**
38
+ * 이 자리의 조건을 다 본다. 화면 그리기는 안 한다 — 부르는 쪽이 그린다.
39
+ *
40
+ * @param {object} o
41
+ * @param {object|null} o.cfg 읽은 설정
42
+ * @param {object|null} o.prof 고른 프로필
43
+ * @param {string} o.root 작업 폴더
44
+ * @param {boolean} o.바깥가도되나 네트워크를 실제로 두드려도 되나
45
+ */
46
+ export async function 진찰(o = {}) {
47
+ const { cfg = null, prof = null, root = process.cwd(), 바깥가도되나 = true, 설정자리 = null } = o;
48
+ const 줄들 = [];
49
+
50
+ // ── 1. 이 프로그램이 도는 자리 ───────────────────────────────────────
51
+ const major = parseInt(String(process.versions.node).split('.')[0], 10);
52
+ 줄들.push(줄(major >= 20 ? 'ok' : 'no', 'Node', `${process.versions.node} · ${process.platform}`,
53
+ major >= 20 ? null : '20 이상이 필요합니다'));
54
+
55
+ // ── 2. 설정 ─────────────────────────────────────────────────────────
56
+ if (설정자리) 줄들.push(줄(existsSync(설정자리) ? 'ok' : 'warn', '설정 파일', 설정자리));
57
+ const 프로젝트설정 = join(root, '.deel', 'config.json');
58
+ if (existsSync(프로젝트설정)) {
59
+ /*
60
+ * 프로젝트 설정은 **믿는 폴더에서만** 읽는다 (safety/trust.js). 여기서
61
+ * 읽었는지 안 읽었는지를 말해 주지 않으면, 적어 둔 사람은 자기 설정이
62
+ * 왜 안 먹는지 영영 모른다 — 파일은 분명히 거기 있기 때문이다.
63
+ */
64
+ const 믿나결과 = 믿나(root);
65
+ 줄들.push(줄(믿나결과 ? 'ok' : 'warn', '프로젝트 설정', 프로젝트설정,
66
+ 믿나결과 ? '읽습니다' : '안 읽습니다 — 읽게 하려면 deel trust'));
67
+ }
68
+ if (!prof) {
69
+ 줄들.push(줄('no', '프로필', '(없음)', 'deel setup 을 먼저 하세요'));
70
+ return { 줄들, 붙어봤나: false };
71
+ }
72
+ 줄들.push(줄('ok', '프로필', `${prof.name ?? prof.id ?? '(이름 없음)'} · ${prof.model ?? '(모델 없음)'}`));
73
+ 줄들.push(줄('ok', '주소', 주소가리기(prof.baseUrl ?? '(없음)')));
74
+
75
+ // ── 3. 열쇠 ─────────────────────────────────────────────────────────
76
+ /*
77
+ * 값은 절대 안 적는다. 있나 없나와 **어디서 왔나**만 적는다 — 고칠 사람이
78
+ * 알아야 하는 것이 그 둘이다. 환경변수가 파일을 이기고 있는데 그걸 모르면
79
+ * 파일만 백 번 고친다.
80
+ */
81
+ const 열쇠어디 = (() => {
82
+ if (process.env.DEEL_API_KEY) return '환경변수 DEEL_API_KEY';
83
+ const 프로필키 = prof.id ? process.env[`DEEL_KEY_${String(prof.id).toUpperCase()}`] : null;
84
+ if (프로필키) return `환경변수 DEEL_KEY_${String(prof.id).toUpperCase()}`;
85
+ if (prof.열쇠받기 || prof.authCommand) return '명령으로 받아 옵니다';
86
+ if (prof.apiKey) return '설정 파일';
87
+ return null;
88
+ })();
89
+ const 열쇠있나 = !!(o.열쇠 ?? '');
90
+ if (prof.auth === 'none') 줄들.push(줄('ok', '열쇠', '안 씁니다 (auth: none)'));
91
+ else if (열쇠있나) 줄들.push(줄('ok', '열쇠', `있습니다 · ${열쇠어디 ?? '어디서 왔는지 모름'}`));
92
+ else 줄들.push(줄('no', '열쇠', '못 꺼냈습니다', 열쇠어디 ? `${열쇠어디} 에서 읽으려 했습니다` : '적어 둔 것이 없습니다'));
93
+
94
+ // ── 4. 우리 인증서 (mTLS) ───────────────────────────────────────────
95
+ const 인증 = 인증서설정(prof);
96
+ if (인증) {
97
+ 인증서등록(prof.baseUrl, 인증);
98
+ try {
99
+ 인증서찾기(prof.baseUrl); // 파일을 실제로 읽어 본다. 못 읽으면 던진다.
100
+ 줄들.push(줄('ok', '내 인증서', 인증서말(인증)));
101
+ } catch (err) {
102
+ 줄들.push(줄('no', '내 인증서', 인증서말(인증), String(err?.message ?? err).split('\n')[0]));
103
+ }
104
+ }
105
+
106
+ // ── 5. 사내 루트 (NODE_EXTRA_CA_CERTS) ──────────────────────────────
107
+ const ca = process.env.NODE_EXTRA_CA_CERTS;
108
+ if (ca) {
109
+ let 됐나 = false;
110
+ try { 됐나 = statSync(ca).isFile(); } catch { 됐나 = false; }
111
+ 줄들.push(줄(됐나 ? 'ok' : 'no', '사내 루트', ca, 됐나 ? null : '그 자리에 파일이 없습니다'));
112
+ }
113
+
114
+ // ── 6. 프록시 ───────────────────────────────────────────────────────
115
+ /*
116
+ * 「프록시를 켰다」 와 「이 주소가 그 프록시를 거친다」 는 다른 말이다.
117
+ * NO_PROXY 하나로 거치지 않게 되는데, 그때 화면이 「프록시 켜짐」 만
118
+ * 말하면 사람은 거치는 줄 알고 프록시 담당자에게 묻는다.
119
+ */
120
+ const 프록 = 프록시설정();
121
+ const 이주소 = 프록시고르기(prof.baseUrl ?? '');
122
+ if (프록.탈) 줄들.push(줄('warn', '프록시', '설정을 못 읽었습니다', 프록.탈));
123
+ if (!프록.켜짐) 줄들.push(줄('ok', '프록시', '안 거칩니다 (직접)'));
124
+ else if (이주소) {
125
+ 줄들.push(줄('ok', '프록시', `${주소가리기(이주소.url)} · ${이주소.출처}`,
126
+ 이주소.auth ? '인증 있음' : null));
127
+ } else {
128
+ 줄들.push(줄('warn', '프록시', `${프록.프록시들.map((x) => `${주소가리기(x.url)}(${x.출처})`).join(' · ')}`,
129
+ '이 주소는 프록시를 안 거칩니다 (NO_PROXY 나 이 컴퓨터 안)'));
130
+ }
131
+
132
+ // ── 7. MCP ──────────────────────────────────────────────────────────
133
+ const mcp자리 = [join(root, '.deel', 'mcp.json'), join(root, '.mcp.json')].filter((p) => existsSync(p));
134
+ if (mcp자리.length) {
135
+ let 몇 = 0;
136
+ try {
137
+ const j = JSON.parse(readFileSync(mcp자리[0], 'utf8'));
138
+ 몇 = Object.keys(j?.mcpServers ?? j?.servers ?? {}).length;
139
+ } catch { 몇 = -1; }
140
+ 줄들.push(몇 < 0
141
+ ? 줄('no', 'MCP', mcp자리[0], '읽지 못했습니다 (JSON 이 아닙니다)')
142
+ : 줄('ok', 'MCP', `${mcp자리[0]} · 서버 ${몇}개`, '실제로 띄워 보려면 deel 을 켜고 /mcp'));
143
+ }
144
+
145
+ // ── 7.5 훅 ───────────────────────────────────────────────────────────
146
+ //
147
+ // 훅은 사람이 적어 둔 명령을 돌리는 것이라, 안 도는 것도 도는 것도 둘 다 사고다.
148
+ // 안 도는 쪽이 더 나쁘다 — 사내 규칙을 걸어 둔 줄 알고 있는데 실제로는 안 걸린
149
+ // 상태이기 때문이다. 제일 흔한 까닭이 믿는 폴더가 아니어서인데, 그건 아무 데도 안
150
+ // 찍힌다. 여기서 말해 준다.
151
+ for (const 한줄 of 훅줄들(훅읽기(root))) 줄들.push(한줄);
152
+
153
+ // 이름 붙인 하위 작업 (agent/agents.js). 못 읽은 정의를 말해 주는 자리는 여기뿐이다.
154
+ for (const 한줄 of 에이전트줄들(에이전트읽기(root))) 줄들.push(한줄);
155
+
156
+ // ── 8. 진짜로 두드려 본다 ───────────────────────────────────────────
157
+ if (!바깥가도되나) {
158
+ 줄들.push(줄('unknown', '도달', '안 두드렸습니다', '자물쇠가 걸려 있거나 --offline 입니다'));
159
+ return { 줄들, 붙어봤나: false };
160
+ }
161
+
162
+ const 주소 = String(prof.baseUrl ?? '').replace(/\/+$/, '');
163
+ const t0 = Date.now();
164
+ const r = await req(`${주소}/models`, {
165
+ headers: headersFor(prof.auth ?? 'bearer', o.열쇠 ?? ''),
166
+ timeout: 15000,
167
+ });
168
+ const 걸린 = Date.now() - t0;
169
+
170
+ if (r.ok) {
171
+ const 목록 = Array.isArray(r.json?.data) ? r.json.data : (Array.isArray(r.json?.models) ? r.json.models : []);
172
+ 줄들.push(줄('ok', '도달', `${r.status} · ${걸린}ms`));
173
+ /*
174
+ * 모델 목록에 **그 모델이 있나**까지 본다.
175
+ *
176
+ * 붙기는 되는데 모델 이름이 한 글자 틀린 자리가 흔하다. 그때 서버가 주는
177
+ * 것은 404 나 400 이고, 그 화면은 「주소가 틀렸다」 와 구별이 안 된다.
178
+ */
179
+ const 이름들 = 목록.map((m) => String(m?.id ?? m?.name ?? '')).filter(Boolean);
180
+ if (!이름들.length) 줄들.push(줄('unknown', '모델 목록', '안 알려 줍니다', '목록을 안 내는 서버도 있습니다'));
181
+ else if (이름들.includes(String(prof.model))) 줄들.push(줄('ok', '모델', `${prof.model} · 목록 ${이름들.length}개`));
182
+ else {
183
+ const 비슷 = 이름들.filter((n) => n.includes(String(prof.model ?? '')) || String(prof.model ?? '').includes(n));
184
+ 줄들.push(줄('no', '모델', `${prof.model} 이 목록에 없습니다`,
185
+ (비슷.length ? `비슷한 것: ${비슷.slice(0, 3).join(' · ')}` : `있는 것: ${이름들.slice(0, 3).join(' · ')}${이름들.length > 3 ? ' …' : ''}`)));
186
+ }
187
+ } else if (r.status === 401 || r.status === 403) {
188
+ // 닿기는 닿았다. 그 둘을 갈라 줘야 사람이 프록시가 아니라 열쇠를 본다.
189
+ 줄들.push(줄('ok', '도달', `${r.status} · ${걸린}ms`, '주소는 닿습니다'));
190
+ 줄들.push(줄('no', '인증', `${r.status}`, '열쇠나 인증 방식(auth)을 보세요'));
191
+ } else {
192
+ 줄들.push(줄('no', '도달', r.status ? `${r.status}` : '못 닿았습니다', r.error ?? null));
193
+ }
194
+ return { 줄들, 붙어봤나: true };
195
+ }
package/src/i18n/en.js CHANGED
@@ -70,6 +70,7 @@ export const en = {
70
70
  'cmd.architect.desc': 'work mode → architect (shapes the structure)',
71
71
  'cmd.debug.desc': 'work mode → debug (finds the cause)',
72
72
  'cmd.ask.desc': 'work mode → ask (explains only)',
73
+ 'cmd.inspect.desc': 'work mode → inspect (audits for defects, changes nothing)',
73
74
  'cmd.orchestrator.desc': 'work mode → orchestrator (splits a big job up)',
74
75
  'cmd.undo.desc': 'undo the last turn — files and the conversation together',
75
76
  'cmd.undo.arg': '[turns]',
@@ -102,6 +103,8 @@ export const en = {
102
103
  'cmd.recall.desc': 'search past sessions — by what was said, not by title',
103
104
  'cmd.recall.arg': '<text>',
104
105
  'cmd.mcp.desc': 'show connected MCP tool servers',
106
+ 'cmd.hooks.desc': 'show configured hooks (where they came from, when they run)',
107
+ 'cmd.agents.desc': 'show named subagent definitions',
105
108
  'cmd.memory.desc': 'what survives the session — view and clear',
106
109
  'cmd.memory.arg': '[forget <n>|clear|<text to remember>]',
107
110
  'cmd.level.desc': 'user level (beginner/developer)',
@@ -314,6 +317,49 @@ export const en = {
314
317
  'approve.strict.name': 'ask always',
315
318
  'approve.strict.short': 'always',
316
319
  'approve.strict.line': 'asks before every file edit and every command',
320
+ 'work.title': 'Work modes',
321
+ 'work.canEdit': 'edits files',
322
+ 'work.readOnly': 'read-only',
323
+ 'work.think': 'thinking',
324
+ 'work.steps': 'up to {n} steps',
325
+ 'work.tools': 'tools',
326
+ 'work.toolsWrite': 'read + change files',
327
+ 'work.toolsRead': 'read only — cannot change files',
328
+ 'work.howTo': 'To switch: {a} {b} {c} … or {키} to cycle',
329
+ 'work.axis': 'A different axis from the approval policy ({명령}) — what you do / how much it asks.',
330
+ 'work.autoOn': 'From here it moves to the right mode by itself, message by message.',
331
+ 'work.pinned': 'You chose this, so it will not change by itself. To hand it back:',
332
+ 'trust.ignored': "This folder's project config was not read — the folder is not trusted yet",
333
+ 'trust.ignoredWhy': 'A config file ships with the repository. Reading a stranger\u2019s means it picks the endpoint and the command that fetches your key.',
334
+ 'trust.howTo': 'To read it:',
335
+ 'trust.filtered': 'Dropped the keys a project config may not set',
336
+ 'trust.trusted': "This folder's project config is being read",
337
+ 'trust.already': 'Already trusted',
338
+ 'trust.removed': 'Removed from the trusted list',
339
+ 'trust.notListed': 'That folder was not on the trusted list',
340
+ 'trust.parentStill': 'Still trusted, through a parent folder',
341
+ 'stats.title': 'What happened in this folder',
342
+ 'stats.none': 'Nothing recorded yet — the log starts the first time deel does something here.',
343
+ 'stats.period': 'Period',
344
+ 'stats.window': 'last {n} days',
345
+ 'stats.activeDays': '{n} active days',
346
+ 'stats.turns': 'Turns',
347
+ 'stats.sessions': '{n} sessions',
348
+ 'stats.tools': 'Tools',
349
+ 'stats.failed': '{n} failed',
350
+ 'stats.blocked': 'Blocked',
351
+ 'stats.undos': 'Undos',
352
+ 'stats.masked': 'Secrets masked',
353
+ 'stats.broken': '{n} lines could not be read — the numbers above are short by that much.',
354
+ 'stats.older': '{n} more lines fall outside the window (--all for everything).',
355
+ 'stats.noCost': 'Tokens and cost are not in this log. For the current conversation, use /cost.',
356
+ 'trust.listTitle': 'Trusted folders',
357
+ 'trust.listNone': 'No folder is trusted yet.',
358
+ 'trust.blockedTitle': 'Keys a project config may never set, even when trusted',
359
+ 'trust.saveFail': 'Could not write the trusted list',
360
+ 'trust.why.allow': 'It widens the approval rules. A deny, which narrows them, is still read',
361
+ 'trust.why.apiKey': 'A key written into a repository is not a setting, it is a leak',
362
+ 'trust.why.authCmd': 'It runs before the first request — ahead of tool approval, so nothing catches it',
317
363
  'level.beginner.name': 'beginner',
318
364
  'level.beginner.hint': 'start on the recommended settings',
319
365
  'level.developer.name': 'developer',
@@ -429,7 +475,10 @@ export const en = {
429
475
  'run.errKept': 'What arrived before the failure was kept in the conversation — you can carry on from there.',
430
476
  'ev.capped': 'The answer was cut off at {한계} tokens — raise the cap with /out, or have files written in parts',
431
477
  'ev.cutoff': 'The server stopped the answer without saying it had finished — it may have been cut mid-way',
478
+ 'ev.stalled': 'Nothing arrived for {n}s, so the connection was dropped — this is what came through',
432
479
  'ev.nudgeMissed': 'It was about to finish leaving {n} of the things you asked for undone — pushed back once',
480
+ 'ev.hookNote': 'hook: {말}',
481
+ 'ev.hookBlock': 'A hook blocked this',
433
482
  'ev.nudgeRead': 'It was about to finish having only read things — pushed back once',
434
483
  'ev.learnedCtx': 'Setting the context limit to {한계} as the server reported, and calling again',
435
484
  'ev.learnedOut': 'Setting the answer-length limit to {한계} as the server reported, and calling again',
@@ -463,6 +512,7 @@ export const en = {
463
512
  'status.root': 'Work folder',
464
513
  'status.shell': 'Shell',
465
514
  'status.keyStore': 'Key store',
515
+ 'status.clientCert': 'Client cert',
466
516
  'status.rules': 'Rules',
467
517
  'status.noRules': 'none (/init creates one)',
468
518
  'status.rulesUnread': '{이름} exists but could not be read ({까닭}) — this folder\'s rules are not in effect',
package/src/i18n/ja.js CHANGED
@@ -57,6 +57,7 @@ export const ja = {
57
57
  'cmd.architect.desc': '作業モード → architect (構造を決める)',
58
58
  'cmd.debug.desc': '作業モード → debug (原因を突き止める)',
59
59
  'cmd.ask.desc': '作業モード → ask (説明だけ)',
60
+ 'cmd.inspect.desc': '作業モード → inspect (欠陥を探す。直さない)',
60
61
  'cmd.orchestrator.desc': '作業モード → orchestrator (大きな仕事を分ける)',
61
62
  'cmd.undo.desc': '直前のターンを戻す — ファイルと会話をまとめて',
62
63
  'cmd.undo.arg': '[ターン数]',
@@ -89,6 +90,8 @@ export const ja = {
89
90
  'cmd.recall.desc': '過去の会話を探す — 題名ではなく中身で',
90
91
  'cmd.recall.arg': '<文字列>',
91
92
  'cmd.mcp.desc': 'つながっている MCP 道具サーバー',
93
+ 'cmd.hooks.desc': '設定したフックを見る',
94
+ 'cmd.agents.desc': '名前を付けた下位タスクを見る',
92
95
  'cmd.memory.desc': '会話をまたいで残るもの — 見る・消す',
93
96
  'cmd.memory.arg': '[forget <n>|clear|<覚えさせる文>]',
94
97
  'cmd.level.desc': '利用者の段階 (かんたん/開発者)',
@@ -281,6 +284,49 @@ export const ja = {
281
284
  'approve.strict.name': '毎回訊く',
282
285
  'approve.strict.short': '毎回',
283
286
  'approve.strict.line': 'ファイルを直すたび、コマンドを打つたびに訊きます',
287
+ 'work.title': '作業モード',
288
+ 'work.canEdit': 'ファイルを変更',
289
+ 'work.readOnly': '読むだけ',
290
+ 'work.think': '思考',
291
+ 'work.steps': '最大 {n} ステップ',
292
+ 'work.tools': 'ツール',
293
+ 'work.toolsWrite': '読み取り + ファイル変更',
294
+ 'work.toolsRead': '読み取りのみ — ファイルは変更できません',
295
+ 'work.howTo': '切り替え: {a} {b} {c} … または {키} で順に',
296
+ 'work.axis': '承認ポリシー({명령})とは別の軸です — 何をするか / どれだけ聞くか。',
297
+ 'work.autoOn': 'これからは一言ごとに適したモードへ自動で移ります。',
298
+ 'work.pinned': 'ご自分で選んだので自動では変わりません。任せ直すには',
299
+ 'trust.ignored': 'このフォルダのプロジェクト設定は読みませんでした — まだ信頼していないフォルダです',
300
+ 'trust.ignoredWhy': '設定ファイルはリポジトリに同梱されます。他人のものをそのまま読むと、接続先も鍵を取得するコマンドも向こうが決めます。',
301
+ 'trust.howTo': '読ませるには',
302
+ 'trust.filtered': 'プロジェクト設定が決められない項目を取り除きました',
303
+ 'trust.trusted': 'このフォルダのプロジェクト設定を読みます',
304
+ 'trust.already': 'すでに信頼しているフォルダです',
305
+ 'trust.removed': '信頼リストから外しました',
306
+ 'trust.notListed': '信頼リストに無いフォルダでした',
307
+ 'trust.parentStill': '親フォルダのため、まだ信頼されています',
308
+ 'stats.title': 'このフォルダでの作業',
309
+ 'stats.none': 'まだ記録がありません — このフォルダで deel を使うと残りはじめます。',
310
+ 'stats.period': '期間',
311
+ 'stats.window': '直近 {n} 日',
312
+ 'stats.activeDays': '{n} 日使用',
313
+ 'stats.turns': '会話',
314
+ 'stats.sessions': 'セッション {n} 件',
315
+ 'stats.tools': 'ツール',
316
+ 'stats.failed': '失敗 {n}',
317
+ 'stats.blocked': 'ブロック',
318
+ 'stats.undos': '取り消し',
319
+ 'stats.masked': '秘密の伏字',
320
+ 'stats.broken': '読めなかった行が {n} 件あります — 上の数はその分少なく数えています。',
321
+ 'stats.older': '期間外にさらに {n} 行あります (--all で全部)。',
322
+ 'stats.noCost': 'トークンと料金はこの記録にありません。今の会話ぶんは /cost で見られます。',
323
+ 'trust.listTitle': '信頼しているフォルダ',
324
+ 'trust.listNone': 'まだどのフォルダも信頼していません。',
325
+ 'trust.blockedTitle': '信頼しても プロジェクト設定が決められない項目',
326
+ 'trust.saveFail': '信頼リストを書けませんでした',
327
+ 'trust.why.allow': '承認ルールを広げる項目です。狭める deny はそのまま読みます',
328
+ 'trust.why.apiKey': 'リポジトリに書かれた鍵は設定ではなく漏洩です',
329
+ 'trust.why.authCmd': '最初のリクエスト前にコマンドが走ります — ツール承認より前なので何も止められません',
284
330
  'level.beginner.name': 'かんたん',
285
331
  'level.beginner.hint': '勧められた設定のまま始めます',
286
332
  'level.developer.name': '開発者',
@@ -387,7 +433,10 @@ export const ja = {
387
433
  'run.errKept': 'ここまで届いた分は会話に残しました — このまま続けて話せます。',
388
434
  'ev.capped': '答えが {한계} トークンで切れました — /out で上限を上げるか、ファイルは分けて書かせてください',
389
435
  'ev.cutoff': 'サーバーが終わったとも言わずに答えを止めました — 途中で切れた可能性があります',
436
+ 'ev.stalled': '{n}秒間なにも届かなかったので切りました — 届いたぶんだけ表示します',
390
437
  'ev.nudgeMissed': '頼んだこと {n} 件を残したまま終えようとしたので、一度押し戻しました',
438
+ 'ev.hookNote': 'フック: {말}',
439
+ 'ev.hookBlock': 'フックが止めました',
391
440
  'ev.nudgeRead': '読むだけで終えようとしたので、一度押し戻しました',
392
441
  'ev.learnedCtx': 'サーバーが教えてくれたコンテキスト上限 {한계} に合わせて呼び直します',
393
442
  'ev.learnedOut': 'サーバーが教えてくれた答えの長さ上限 {한계} に合わせて呼び直します',
@@ -420,6 +469,7 @@ export const ja = {
420
469
  'status.root': '作業フォルダ',
421
470
  'status.shell': 'シェル',
422
471
  'status.keyStore': '鍵の保管',
472
+ 'status.clientCert': '自分の証明書',
423
473
  'status.rules': 'ルール',
424
474
  'status.noRules': 'なし(/init で作れます)',
425
475
  'status.rulesUnread': '{이름} はありますが読めませんでした({까닭})— このフォルダのルールは効いていません',
package/src/i18n/ko.js CHANGED
@@ -59,6 +59,7 @@ export const ko = {
59
59
  'cmd.architect.desc': '작업 모드 → 설계 (구조를 짠다)',
60
60
  'cmd.debug.desc': '작업 모드 → 디버그 (원인을 찾는다)',
61
61
  'cmd.ask.desc': '작업 모드 → 묻기 (설명만)',
62
+ 'cmd.inspect.desc': '작업 모드 → 점검 (결함을 찾는다. 안 고침)',
62
63
  'cmd.orchestrator.desc': '작업 모드 → 총괄 (큰 일을 쪼개서)',
63
64
  'cmd.undo.desc': '직전 작업 되돌리기 — 파일과 대화를 같이',
64
65
  'cmd.undo.arg': '[턴수]',
@@ -91,6 +92,8 @@ export const ko = {
91
92
  'cmd.recall.desc': '지난 대화에서 찾기 — 목록 말고 내용으로',
92
93
  'cmd.recall.arg': '<찾을 말>',
93
94
  'cmd.mcp.desc': '밖에서 붙인 도구(MCP) 서버 보기',
95
+ 'cmd.hooks.desc': '적어 둔 훅 보기 (어디서 읽었고 언제 도는지)',
96
+ 'cmd.agents.desc': '이름 붙인 하위 작업 보기',
94
97
  'cmd.memory.desc': '대화가 끝나도 남는 기억 — 보기·지우기',
95
98
  'cmd.memory.arg': '[forget <번호>|clear|<적을 말>]',
96
99
  'cmd.level.desc': '사용자 수준 (쉬움/개발자)',
@@ -303,6 +306,49 @@ export const ko = {
303
306
  'approve.strict.name': '모두 확인',
304
307
  'approve.strict.short': '모두',
305
308
  'approve.strict.line': '파일을 고치거나 명령을 돌리기 전에 매번 물어봅니다',
309
+ 'work.title': '작업 모드',
310
+ 'work.canEdit': '파일 바꿈',
311
+ 'work.readOnly': '읽기만',
312
+ 'work.think': '생각',
313
+ 'work.steps': '최대 {n}걸음',
314
+ 'work.tools': '도구',
315
+ 'work.toolsWrite': '읽기 + 파일 바꾸기',
316
+ 'work.toolsRead': '읽기만 — 파일을 못 바꿉니다',
317
+ 'work.howTo': '바꾸려면 {a} {b} {c} … 또는 {키} 로 차례로',
318
+ 'work.axis': '이건 승인 정책({명령})과 다른 축입니다 — 무엇을 하느냐 / 얼마나 물어보냐.',
319
+ 'work.autoOn': '앞으로는 한마디마다 알맞은 모드로 저절로 옮겨 갑니다.',
320
+ 'work.pinned': '직접 고르셨으므로 저절로 바뀌지 않습니다. 다시 맡기려면',
321
+ 'trust.ignored': '이 폴더의 프로젝트 설정을 안 읽었습니다 — 아직 믿는 폴더가 아닙니다',
322
+ 'trust.ignoredWhy': '설정 파일은 저장소에 딸려 옵니다. 남의 것을 그냥 읽으면 주소도 열쇠 받는 명령도 그쪽이 정합니다.',
323
+ 'trust.howTo': '읽게 하려면',
324
+ 'trust.filtered': '프로젝트 설정에서 못 정하는 칸을 걷어냈습니다',
325
+ 'trust.trusted': '이 폴더의 프로젝트 설정을 읽습니다',
326
+ 'trust.already': '이미 믿는 폴더입니다',
327
+ 'trust.removed': '믿는 목록에서 뺐습니다',
328
+ 'trust.notListed': '믿는 목록에 없던 폴더입니다',
329
+ 'trust.parentStill': '그래도 위 폴더 때문에 아직 믿습니다',
330
+ 'stats.title': '이 폴더에서 한 일',
331
+ 'stats.none': '아직 기록이 없습니다 — 이 폴더에서 deel 로 무언가 하면 그때부터 남습니다.',
332
+ 'stats.period': '기간',
333
+ 'stats.window': '최근 {n}일',
334
+ 'stats.activeDays': '{n}일 사용',
335
+ 'stats.turns': '대화',
336
+ 'stats.sessions': '세션 {n}개',
337
+ 'stats.tools': '도구',
338
+ 'stats.failed': '실패 {n}',
339
+ 'stats.blocked': '막힘',
340
+ 'stats.undos': '되돌리기',
341
+ 'stats.masked': '비밀 가림',
342
+ 'stats.broken': '읽지 못한 줄이 {n}개 있습니다 — 위 숫자는 그만큼 덜 셌습니다.',
343
+ 'stats.older': '기간 밖에 {n}줄이 더 있습니다 (--all 로 전부).',
344
+ 'stats.noCost': '토큰과 요금은 이 기록에 없습니다. 이번 대화에서 쓴 것은 /cost 로 보세요.',
345
+ 'trust.listTitle': '믿는 폴더',
346
+ 'trust.listNone': '아직 아무 폴더도 안 믿습니다.',
347
+ 'trust.blockedTitle': '믿어도 프로젝트가 못 정하는 칸',
348
+ 'trust.saveFail': '믿는 목록을 못 적었습니다',
349
+ 'trust.why.allow': '승인 규칙을 넓히는 칸입니다. 좁히는 deny 는 그대로 읽습니다',
350
+ 'trust.why.apiKey': '열쇠가 저장소에 적혀 있으면 설정이 아니라 유출입니다',
351
+ 'trust.why.authCmd': '첫 요청 전에 명령이 돕니다 — 도구 승인보다 앞이라 안 걸립니다',
306
352
  'level.beginner.name': '쉬움',
307
353
  'level.beginner.hint': '권장값으로 바로 시작',
308
354
  'level.developer.name': '개발자',
@@ -460,7 +506,10 @@ export const ko = {
460
506
  */
461
507
  'ev.capped': '답이 {한계} 토큰에서 잘렸습니다 — /out 으로 상한을 올리거나, 파일은 나눠 쓰게 하세요',
462
508
  'ev.cutoff': '서버가 끝났다는 말 없이 답을 멈췄습니다 — 중간에서 끊겼을 수 있습니다',
509
+ 'ev.stalled': '{n}초 동안 아무것도 안 와서 끊었습니다 — 받은 데까지만 보여 드립니다',
463
510
  'ev.nudgeMissed': '시킨 것 {n}가지를 빼놓고 끝내려고 해서 한 번 되밀었습니다',
511
+ 'ev.hookNote': '훅: {말}',
512
+ 'ev.hookBlock': '훅이 막았습니다',
464
513
  'ev.nudgeRead': '읽기만 하고 끝내려고 해서 한 번 되밀었습니다',
465
514
  'ev.learnedCtx': '서버가 알려 준 컨텍스트 한계 {한계} 로 맞추고 다시 부릅니다',
466
515
  'ev.learnedOut': '서버가 알려 준 답 길이 한계 {한계} 로 맞추고 다시 부릅니다',
@@ -510,6 +559,7 @@ export const ko = {
510
559
  'status.root': '작업 폴더',
511
560
  'status.shell': '셸',
512
561
  'status.keyStore': '열쇠 보관',
562
+ 'status.clientCert': '내 인증서',
513
563
  'status.rules': '규칙',
514
564
  'status.noRules': '없음 (/init 으로 만들 수 있습니다)',
515
565
  'status.rulesUnread': '{이름} 이 있는데 못 읽었습니다 ({까닭}) — 이 폴더 규칙이 안 걸려 있습니다',