deel-local-cli 1.16.0 → 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.
- package/README.ko.md +54 -9
- package/README.md +54 -8
- package/bin/deel.js +385 -5
- package/package.json +1 -1
- package/src/acp/serve.js +39 -1
- package/src/agent/agents.js +203 -0
- package/src/agent/loop.js +209 -17
- package/src/agent/models.js +4 -0
- package/src/agent/outschema.js +351 -0
- package/src/agent/session.js +13 -2
- package/src/backend/adapter.js +38 -2
- package/src/backend/clientcert.js +164 -0
- package/src/backend/http.js +232 -26
- package/src/backend/mcp.js +194 -30
- package/src/backend/wire.js +10 -0
- package/src/cmdnames.js +2 -0
- package/src/commands.js +127 -2
- package/src/completion.js +34 -4
- package/src/config.js +86 -9
- package/src/configexplain.js +177 -0
- package/src/doctor.js +195 -0
- package/src/i18n/en.js +37 -0
- package/src/i18n/ja.js +37 -0
- package/src/i18n/ko.js +37 -0
- package/src/i18n/zh.js +37 -0
- package/src/lsp/client.js +2 -2
- package/src/oneshot.js +181 -4
- package/src/pack/sbom.js +77 -3
- package/src/pack/selfpack.js +1 -1
- package/src/pack/sheet.en.js +1 -1
- package/src/repl.js +74 -3
- package/src/safety/hooks.js +389 -0
- package/src/safety/policy.js +74 -0
- package/src/safety/shellenv.js +116 -0
- package/src/safety/trust.js +230 -0
- package/src/setup.js +9 -0
- package/src/stats.js +162 -0
- package/src/tools/docs.js +12 -2
- package/src/tools/hwpxwrite.js +268 -0
- package/src/tools/index.js +128 -9
- package/src/tools/jobs.js +11 -6
- package/src/tools/spawn.js +19 -1
- package/src/tools/task.js +8 -2
package/bin/deel.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// deel 진입점. 외부 의존성 없음 — Node 표준 기능만 씁니다.
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { c, say, mark, rule } from '../src/ui/ansi.js';
|
|
5
|
+
import { c, say, mark, rule, clip, width } from '../src/ui/ansi.js';
|
|
6
6
|
import { runSetup, runDiagnose, showStatus, banner } from '../src/setup.js';
|
|
7
7
|
import { chatLoop } from '../src/repl.js';
|
|
8
8
|
import { runOnce } from '../src/oneshot.js';
|
|
@@ -16,8 +16,13 @@ import { acp } from '../src/acp/serve.js';
|
|
|
16
16
|
import { runCompletion } from '../src/completion.js';
|
|
17
17
|
import { runReset } from '../src/reset.js';
|
|
18
18
|
import { 마크다운, 읽는갈래 } from '../src/tools/doc2md.js';
|
|
19
|
-
import { 언어잡기,
|
|
19
|
+
import { 언어잡기, 언어, 말 } from '../src/i18n/index.js';
|
|
20
|
+
import { 믿기, 안믿기, 믿는목록, 프로젝트금지칸 } from '../src/safety/trust.js';
|
|
20
21
|
import { load as 설정읽기 } from '../src/config.js';
|
|
22
|
+
import { 규칙모으기, 어떻게할까, 확인목록, 확인인자, 확인돌리기 } from '../src/safety/policy.js';
|
|
23
|
+
import { 기록자리, 세기, 도구차례, 막힘차례, 셈JSON } from '../src/stats.js';
|
|
24
|
+
import { 진찰 } from '../src/doctor.js';
|
|
25
|
+
import { 설명, 설명줄들 } from '../src/configexplain.js';
|
|
21
26
|
|
|
22
27
|
const MIN_NODE = 20;
|
|
23
28
|
|
|
@@ -99,6 +104,302 @@ function runPack(flags) {
|
|
|
99
104
|
return 0;
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
/*
|
|
108
|
+
* `deel config explain <칸>` — 이 값은 어디서 왔나 (src/configexplain.js).
|
|
109
|
+
*
|
|
110
|
+
* 설정을 읽는 자리가 넷이라(정책·환경변수·프로젝트·이 PC), 「파일에 분명히
|
|
111
|
+
* 적어 놨는데 안 먹는다」 가 흔하다. 그때 사람은 그 파일만 고치다가 설정이라는
|
|
112
|
+
* 것을 안 믿게 된다. 층을 열어 보여 주면 5초에 끝나는 일이다.
|
|
113
|
+
*/
|
|
114
|
+
function runConfig(args, flags) {
|
|
115
|
+
const 무엇 = String(args[0] ?? '');
|
|
116
|
+
const 칸 = String(args[1] ?? '');
|
|
117
|
+
if (무엇 !== 'explain' || !칸) {
|
|
118
|
+
say('');
|
|
119
|
+
say(` ${c.cyan('deel config explain <칸>')} ${c.gray('이 값이 어디서 왔는지')}`);
|
|
120
|
+
say(` ${c.gray('보기:')} ${c.white('deel config explain offline')} ${c.gray('·')} ${c.white('deel config explain profiles.사내.model')}`);
|
|
121
|
+
say('');
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
const r = 설명(칸, { root: flags.root ? String(flags.root) : process.cwd() });
|
|
125
|
+
if (flags.json === true) { process.stdout.write(JSON.stringify(r) + '\n'); return 0; }
|
|
126
|
+
|
|
127
|
+
const 표 = { 값: c.bold('='), 없음: c.gray('·'), 이김: mark.ok, 아래: c.gray('·'), 안읽음: c.yellow('⚠'), 참고: c.gray('i') };
|
|
128
|
+
say('');
|
|
129
|
+
say(` ${c.bold(칸)}`);
|
|
130
|
+
say('');
|
|
131
|
+
for (const 줄 of 설명줄들(r)) {
|
|
132
|
+
const 글 = 줄.갈래 === '값' ? c.white(줄.글) : 줄.글;
|
|
133
|
+
say(` ${표[줄.갈래] ?? ' '} ${글}${줄.곁 ? ` ${c.gray(줄.곁)}` : ''}`);
|
|
134
|
+
}
|
|
135
|
+
say('');
|
|
136
|
+
return r.이긴층 ? 0 : 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/*
|
|
140
|
+
* `deel doctor` — 붙기 전에 이 자리의 조건을 하나씩 본다 (src/doctor.js).
|
|
141
|
+
*
|
|
142
|
+
* 화면 한 장이 그대로 담당자에게 보낼 질문이 되게 하는 것이 목표다. 그래서
|
|
143
|
+
* 값마다 **어디서 온 값인지**를 같이 적는다 — 환경변수가 파일을 이기고 있는
|
|
144
|
+
* 것을 모르면 사람은 파일만 백 번 고친다.
|
|
145
|
+
*/
|
|
146
|
+
async function runDoctor(flags) {
|
|
147
|
+
const { load: 읽기, activeProfile: 고른것, resolveKey: 열쇠풀기, configPath: 설정경로 } =
|
|
148
|
+
await import('../src/config.js');
|
|
149
|
+
const { 지금모드, 바깥인가, 나갈수있나 } = await import('../src/safety/runmode.js');
|
|
150
|
+
const { allowEndpoint } = await import('../src/safety/network.js');
|
|
151
|
+
|
|
152
|
+
banner();
|
|
153
|
+
let cfg = null;
|
|
154
|
+
try { cfg = 읽기(); } catch { /* 설정이 망가져도 아래에서 말한다 */ }
|
|
155
|
+
const prof = cfg ? 고른것(cfg) : null;
|
|
156
|
+
|
|
157
|
+
/*
|
|
158
|
+
* 두드려도 되는 자리인지 먼저 본다.
|
|
159
|
+
*
|
|
160
|
+
* 진단한다고 자물쇠를 넘어가면 안 된다 — 여기서 한 번 나가 버리면 「나가는
|
|
161
|
+
* 주소는 사람이 정한 하나뿐」 이 진단 명령 하나로 깨진다.
|
|
162
|
+
*/
|
|
163
|
+
const 모드 = 지금모드({ online: flags.online === true, offline: !!(flags.offline ?? prof?.offline ?? cfg?.offline) });
|
|
164
|
+
const 나감 = prof ? 나갈수있나(모드, { 바깥: 바깥인가(prof.baseUrl), 허가: prof.online === true }) : { 물어볼까: false };
|
|
165
|
+
const 두드려도되나 = !!prof && !나감.물어볼까 && !모드.허가무시;
|
|
166
|
+
if (두드려도되나) allowEndpoint(prof.baseUrl);
|
|
167
|
+
|
|
168
|
+
const { 줄들 } = await 진찰({
|
|
169
|
+
cfg, prof,
|
|
170
|
+
root: flags.root ? String(flags.root) : process.cwd(),
|
|
171
|
+
설정자리: 설정경로(),
|
|
172
|
+
열쇠: prof ? 열쇠풀기(prof) : '',
|
|
173
|
+
바깥가도되나: 두드려도되나,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const 표 = { ok: mark.ok, warn: mark.warn, no: c.red('✗'), unknown: c.gray('?') };
|
|
177
|
+
say('');
|
|
178
|
+
for (const x of 줄들) {
|
|
179
|
+
const 이름 = String(x.이름) + ' '.repeat(Math.max(0, 14 - width(String(x.이름))));
|
|
180
|
+
say(` ${표[x.상태] ?? ' '} ${c.gray(이름)} ${x.값}${x.덧말 ? c.gray(` — ${x.덧말}`) : ''}`);
|
|
181
|
+
}
|
|
182
|
+
say('');
|
|
183
|
+
const 탈 = 줄들.filter((x) => x.상태 === 'no').length;
|
|
184
|
+
if (탈) {
|
|
185
|
+
say(` ${c.red(`${탈}군데가 막혀 있습니다.`)} ${c.gray('위 줄을 그대로 담당자에게 보내시면 됩니다.')}`);
|
|
186
|
+
say('');
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
say(` ${c.gray('여기까지는 괜찮습니다. 모델이 도구를 부를 수 있는지는')} ${c.cyan('deel diagnose')}`);
|
|
190
|
+
say('');
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/*
|
|
195
|
+
* `deel stats [--days N] [--all] [--json]` — 이 폴더에서 무엇을 했나.
|
|
196
|
+
*
|
|
197
|
+
* 감사기록은 여태 쓰기만 하고 아무도 안 읽는 파일이었다. 무엇을 언제 어떻게
|
|
198
|
+
* 했는지 전부 남는다고 팔아 놓고, 적힌 것을 사람 말로 되돌려 주는 명령이
|
|
199
|
+
* 없었다 (src/stats.js 머리말).
|
|
200
|
+
*/
|
|
201
|
+
function runStats(flags) {
|
|
202
|
+
const 자리 = 기록자리(flags.root ? String(flags.root) : process.cwd());
|
|
203
|
+
const 전부 = flags.all === true || flags.all === 'true';
|
|
204
|
+
const 날수 = 전부 ? null : Math.max(1, parseInt(String(flags.days ?? '30'), 10) || 30);
|
|
205
|
+
const 셈 = 세기(자리, { 날수 });
|
|
206
|
+
const json = flags.json === true || flags.json === 'true';
|
|
207
|
+
|
|
208
|
+
if (json) { process.stdout.write(JSON.stringify(셈JSON(셈)) + '\n'); return 셈.있나 ? 0 : 1; }
|
|
209
|
+
|
|
210
|
+
say('');
|
|
211
|
+
if (!셈.있나) {
|
|
212
|
+
// 없는 것을 「0회」 로 찍으면 안 쓴 것과 기록이 없는 것이 같아 보인다.
|
|
213
|
+
say(` ${mark.warn} ${말('stats.none')}`);
|
|
214
|
+
say(` ${c.gray(자리)}`);
|
|
215
|
+
say('');
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const 기간 = 셈.처음 ? `${셈.처음.slice(0, 10)} → ${셈.마지막.slice(0, 10)}` : '(빈 기록)';
|
|
220
|
+
say(` ${c.bold(말('stats.title'))} ${c.gray(자리)}`);
|
|
221
|
+
say('');
|
|
222
|
+
/*
|
|
223
|
+
* 이름 칸은 **화면 칸수**로 맞춘다.
|
|
224
|
+
*
|
|
225
|
+
* 한글은 한 글자가 두 칸이라 padEnd 로 맞추면 어긋난다 — 「막힘」 과
|
|
226
|
+
* 「되돌리기」 가 같은 자리에서 시작하지 않는다. 말을 바꿀 때마다 손으로
|
|
227
|
+
* 다시 맞추게 되는 것도 같은 까닭이다 (bin/deel.js 의 반입 묶음 화면에
|
|
228
|
+
* 같은 이야기가 한 번 적혀 있다).
|
|
229
|
+
*/
|
|
230
|
+
const 칸 = (s) => ` ${c.gray(String(s) + ' '.repeat(Math.max(0, 12 - width(String(s)))))}`;
|
|
231
|
+
say(`${칸(말('stats.period'))} ${기간} ${c.gray(말('stats.activeDays', { n: 셈.날.size }))}`
|
|
232
|
+
+ (셈.날수 ? c.gray(` · ${말('stats.window', { n: 셈.날수 })}`) : ''));
|
|
233
|
+
say(`${칸(말('stats.turns'))} ${셈.대화.toLocaleString()} ${c.gray(말('stats.sessions', { n: 셈.세션.size }))}`);
|
|
234
|
+
const 실패말 = 셈.도구
|
|
235
|
+
? c.gray(` · ${말('stats.failed', { n: 셈.도구실패 })} (${((셈.도구실패 / 셈.도구) * 100).toFixed(1)}%)`)
|
|
236
|
+
: '';
|
|
237
|
+
say(`${칸(말('stats.tools'))} ${셈.도구.toLocaleString()}${실패말}`);
|
|
238
|
+
|
|
239
|
+
const 표 = 도구차례(셈);
|
|
240
|
+
if (표.length) {
|
|
241
|
+
say('');
|
|
242
|
+
for (const x of 표) {
|
|
243
|
+
const 실패 = x.실패 ? c.yellow(` ${말('stats.failed', { n: x.실패 })}`) : '';
|
|
244
|
+
say(` ${c.white(x.이름.padEnd(12))} ${String(x.수).padStart(6)}${실패}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/*
|
|
249
|
+
* 막힌 것은 0 이어도 적는다.
|
|
250
|
+
*
|
|
251
|
+
* 규칙을 적어 둔 사람이 알아야 하는 것은 「몇 번 막혔나」 보다 **한 번도 안
|
|
252
|
+
* 걸렸다**는 사실이다. 안 걸리는 규칙은 규칙이 아니라 적어 둔 글이다
|
|
253
|
+
* (`deel rules check` 가 같은 이야기를 다른 쪽에서 한다).
|
|
254
|
+
*/
|
|
255
|
+
say('');
|
|
256
|
+
say(`${칸(말('stats.blocked'))} ${셈.막힘.toLocaleString()}`);
|
|
257
|
+
for (const x of 막힘차례(셈)) say(` ${c.gray('·')} ${x.왜} ${c.gray(`${x.수}회`)}`);
|
|
258
|
+
say(`${칸(말('stats.undos'))} ${셈.되돌림.toLocaleString()}`);
|
|
259
|
+
if (셈.비밀) say(`${칸(말('stats.masked'))} ${셈.비밀.toLocaleString()}`);
|
|
260
|
+
|
|
261
|
+
// 못 읽은 줄을 조용히 넘기면 위 숫자가 통째로 거짓말이 된다.
|
|
262
|
+
if (셈.깨진줄) {
|
|
263
|
+
say('');
|
|
264
|
+
say(` ${mark.warn} ${c.yellow(말('stats.broken', { n: 셈.깨진줄 }))}`);
|
|
265
|
+
}
|
|
266
|
+
if (셈.지난줄) say(` ${c.gray(말('stats.older', { n: 셈.지난줄 }))}`);
|
|
267
|
+
// 토큰과 돈은 이 파일에 없다. 없는 것을 지어내지 않고, 어디서 보는지 말한다.
|
|
268
|
+
say('');
|
|
269
|
+
say(` ${c.gray(말('stats.noCost'))}`);
|
|
270
|
+
say('');
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/*
|
|
275
|
+
* `deel trust [--off] [--list]` — 이 폴더의 프로젝트 설정을 읽을 것인가.
|
|
276
|
+
*
|
|
277
|
+
* 설정 파일 `.deel/config.json` 은 저장소에 딸려 온다. 남의 저장소 하나를
|
|
278
|
+
* 받은 것만으로 오간 말이 다른 주소로 가고, `열쇠받기` 명령이 이 계정
|
|
279
|
+
* 권한으로 도는 길이 여기다 — 그 명령은 도구 승인 화면보다 **앞**이라
|
|
280
|
+
* 어떤 승인 정책으로도 안 걸린다.
|
|
281
|
+
*
|
|
282
|
+
* 그래서 기본은 안 읽는 것이고, 읽게 하는 것은 **사람이 먼저 낸 명령**이다.
|
|
283
|
+
* 켤 때마다 물어보는 길도 있었지만 그 물음은 맨 앞에 뜨고, 앞에 뜨는 물음은
|
|
284
|
+
* 안 읽힌다 — 사람은 대화를 하러 온 것이지 물음에 답하러 온 것이 아니다.
|
|
285
|
+
*/
|
|
286
|
+
function runTrust(flags) {
|
|
287
|
+
const 여기 = process.cwd();
|
|
288
|
+
|
|
289
|
+
if (flags.list === true || flags.list === 'true') {
|
|
290
|
+
const 것들 = 믿는목록();
|
|
291
|
+
say('');
|
|
292
|
+
say(` ${c.bold(말('trust.listTitle'))}`);
|
|
293
|
+
say('');
|
|
294
|
+
if (!것들.length) say(` ${c.gray(말('trust.listNone'))}`);
|
|
295
|
+
for (const x of 것들) say(` ${c.gray('·')} ${x}`);
|
|
296
|
+
say('');
|
|
297
|
+
say(` ${c.gray(말('trust.blockedTitle'))}`);
|
|
298
|
+
for (const { 칸, 열쇠 } of 프로젝트금지칸) say(` ${c.gray('·')} ${c.white(칸)} ${c.gray(말(열쇠))}`);
|
|
299
|
+
say('');
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (flags.off === true || flags.off === 'true') {
|
|
304
|
+
const r = 안믿기(여기);
|
|
305
|
+
say('');
|
|
306
|
+
if (!r.ok) { say(` ${mark.warn} ${말('trust.saveFail')} — ${r.왜}`); say(''); return 1; }
|
|
307
|
+
say(` ${mark.ok} ${r.뺐나 ? 말('trust.removed') : 말('trust.notListed')} ${c.gray(여기)}`);
|
|
308
|
+
// 위 폴더 때문에 아직 믿기는 것을 말 안 하면, 「뺐습니다」 가 거짓말이 된다.
|
|
309
|
+
if (r.위폴더) say(` ${mark.warn} ${말('trust.parentStill')} ${c.gray(r.위폴더)}`);
|
|
310
|
+
say('');
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const r = 믿기(여기);
|
|
315
|
+
say('');
|
|
316
|
+
if (!r.ok) { say(` ${mark.warn} ${말('trust.saveFail')} — ${r.왜}`); say(''); return 1; }
|
|
317
|
+
say(` ${mark.ok} ${r.이미 ? 말('trust.already') : 말('trust.trusted')} ${c.gray(여기)}`);
|
|
318
|
+
say('');
|
|
319
|
+
say(` ${c.gray(말('trust.blockedTitle'))}`);
|
|
320
|
+
for (const { 칸, 열쇠 } of 프로젝트금지칸) say(` ${c.gray('·')} ${c.white(칸)} ${c.gray(말(열쇠))}`);
|
|
321
|
+
say('');
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/*
|
|
326
|
+
* `deel rules [check [명령]] [--tool Bash]` — 적어 둔 규칙이 진짜 그렇게 도나.
|
|
327
|
+
*
|
|
328
|
+
* 규칙은 적어 두면 조용히 돈다. 그래서 **잘못 적은 규칙은 티가 안 난다.**
|
|
329
|
+
* `Bash(rm -rf*)` 는 `rm -rf /` 를 막지만 `sudo rm -rf /` 는 안 막는다 —
|
|
330
|
+
* 무늬가 앞부터 맞아야 하기 때문이다. 적은 사람은 막힌 줄 알고 지내고,
|
|
331
|
+
* 안 막혔다는 것은 진짜로 지워진 날에야 안다.
|
|
332
|
+
*
|
|
333
|
+
* 그래서 이 명령이 있다. 규칙을 적자마자 확인할 수 있어야 한다.
|
|
334
|
+
*/
|
|
335
|
+
function runRules(args, flags) {
|
|
336
|
+
const cfg = 설정읽기();
|
|
337
|
+
const 규칙들 = 규칙모으기(cfg);
|
|
338
|
+
const 어느도구 = flags.tool ? String(flags.tool) : 'Bash';
|
|
339
|
+
|
|
340
|
+
// `deel rules check "명령"` — 이 하나를 어느 규칙이 어떻게 정하나.
|
|
341
|
+
if (args[0] === 'check' && args[1]) {
|
|
342
|
+
const 값 = args.slice(1).join(' ');
|
|
343
|
+
const r = 어떻게할까(규칙들, 어느도구, 확인인자(어느도구, 값));
|
|
344
|
+
const 표 = { deny: c.red('막힙니다'), allow: c.green('묻지 않고 합니다'), 모름: c.yellow('모드가 정합니다') };
|
|
345
|
+
say('');
|
|
346
|
+
say(` ${c.gray(어느도구)} ${c.white(값)}`);
|
|
347
|
+
say(` ${표[r.답]}`);
|
|
348
|
+
// 어느 줄 때문인지까지 말해야 사람이 제 설정을 고칠 수 있다. 「막힙니다」
|
|
349
|
+
// 만 말하면 무엇을 지워야 풀리는지 알 길이 없다.
|
|
350
|
+
if (r.규칙) say(` ${c.gray(`${r.출처}의 ${r.규칙}`)}`);
|
|
351
|
+
else say(` ${c.gray('걸리는 규칙이 없습니다')}`);
|
|
352
|
+
say('');
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// `deel rules check` — 설정에 적어 둔 보기를 다 돌린다. CI 에 거는 모양이다.
|
|
357
|
+
if (args[0] === 'check') {
|
|
358
|
+
const 보기들 = 확인목록(cfg);
|
|
359
|
+
if (!보기들.length) {
|
|
360
|
+
say('');
|
|
361
|
+
say(` ${mark.warn} 확인할 보기가 없습니다.`);
|
|
362
|
+
say(` ${c.gray('설정의 permissions 에 이렇게 적어 두면 여기서 돌립니다:')}`);
|
|
363
|
+
say(` ${c.gray(' "확인": [{ "도구": "Bash", "값": "sudo rm -rf /tmp", "이래야": "deny" }]')}`);
|
|
364
|
+
say(` ${c.gray('이래야 는 deny · allow · 모름 셋 중 하나입니다.')}`);
|
|
365
|
+
say('');
|
|
366
|
+
return 1;
|
|
367
|
+
}
|
|
368
|
+
const 결과 = 확인돌리기(규칙들, 보기들);
|
|
369
|
+
const 틀린것 = 결과.filter((x) => !x.맞나);
|
|
370
|
+
say('');
|
|
371
|
+
for (const x of 결과) {
|
|
372
|
+
const 표시 = x.맞나 ? c.green('✓') : c.red('✗');
|
|
373
|
+
const 곁 = x.맞나
|
|
374
|
+
? c.gray(x.규칙 ? `${x.출처}의 ${x.규칙}` : '걸리는 규칙 없음')
|
|
375
|
+
: c.red(`${x.이래야} 이어야 하는데 ${x.나온것}`);
|
|
376
|
+
say(` ${표시} ${c.gray(x.도구)} ${c.white(clip(x.값, 44))} ${곁}`);
|
|
377
|
+
}
|
|
378
|
+
say('');
|
|
379
|
+
say(틀린것.length
|
|
380
|
+
? ` ${mark.warn} ${결과.length}개 중 ${틀린것.length}개가 적어 둔 것과 다릅니다.`
|
|
381
|
+
: ` ${mark.ok} ${결과.length}개 다 적어 둔 대로입니다.`);
|
|
382
|
+
say('');
|
|
383
|
+
// 틀리면 0 이 아닌 값으로 끝낸다. CI 가 보는 것은 이 숫자다.
|
|
384
|
+
return 틀린것.length ? 1 : 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 그냥 `deel rules` — 지금 걸려 있는 것을 늘어놓는다.
|
|
388
|
+
say('');
|
|
389
|
+
say(` ${c.bold('적어 둔 규칙')} ${c.gray('— 이건 승인 모드보다 셉니다')}`);
|
|
390
|
+
say('');
|
|
391
|
+
if (!규칙들.deny.length && !규칙들.allow.length) say(` ${c.gray('적어 둔 규칙이 없습니다.')}`);
|
|
392
|
+
for (const r of 규칙들.deny) say(` ${c.red('✗')} ${c.white(r.원문)} ${c.gray(r.출처)}`);
|
|
393
|
+
for (const r of 규칙들.allow) say(` ${c.green('✓')} ${c.white(r.원문)} ${c.gray(r.출처)}`);
|
|
394
|
+
if (규칙들.정책곳) say(` ${c.gray(`관리 정책 ${규칙들.정책곳} — 이 파일은 고칠 수 없습니다`)}`);
|
|
395
|
+
if (규칙들.탈) say(` ${mark.warn} ${c.yellow(규칙들.탈)}`);
|
|
396
|
+
say('');
|
|
397
|
+
say(` ${c.gray('이 명령이 어떻게 되는지:')} ${c.cyan('deel rules check "sudo rm -rf /tmp"')}`);
|
|
398
|
+
say(` ${c.gray('적어 둔 보기를 다 돌리기:')} ${c.cyan('deel rules check')}`);
|
|
399
|
+
say('');
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
102
403
|
// 묶지 않고 심사 내용만 보기.
|
|
103
404
|
function runAudit() {
|
|
104
405
|
say('');
|
|
@@ -189,7 +490,7 @@ function runSbom(flags) {
|
|
|
189
490
|
// 실제로 이랬다 — deel run --json "검사 돌려줘" 를 쳤더니 --json 이 뒤의 말을
|
|
190
491
|
// 통째로 삼켰다. 시킬 말이 사라졌으니 "무엇을 시킬지 적어 주세요" 가 떴는데,
|
|
191
492
|
// 화면만 보면 왜 그런지 알 길이 없다. 깃발을 앞에 두는 것은 아주 흔한 습관이다.
|
|
192
|
-
const BOOL = new Set(['help', 'version', 'offline', 'online', 'continue', 'json', 'quiet', 'yes', 'no-tui', 'tui']);
|
|
493
|
+
const BOOL = new Set(['help', 'version', 'offline', 'online', 'continue', 'json', 'quiet', 'yes', 'no-tui', 'tui', 'all', 'no-hooks']);
|
|
193
494
|
|
|
194
495
|
function parse(argv) {
|
|
195
496
|
const flags = {};
|
|
@@ -261,6 +562,52 @@ function help() {
|
|
|
261
562
|
say(` ${c.cyan('deel pack')} 위 셋 + 소스를 zip 하나로 묶기`);
|
|
262
563
|
say(` ${c.gray('--out <파일>')} 묶음 파일 이름. 기본은 deel-반입.zip`);
|
|
263
564
|
say('');
|
|
565
|
+
/*
|
|
566
|
+
* 이 대목만 영어를 같이 적어 둔다.
|
|
567
|
+
*
|
|
568
|
+
* `--help` 는 아직 통째로 한국어다(langleak 검사가 그 줄 수를 못박고 있다).
|
|
569
|
+
* 그 빚은 따로 갚을 것이고, **여기서 그 빚을 더 키우지는 않는다.** 영어로
|
|
570
|
+
* 켠 사람이 이 대목을 못 읽으면 「내가 적은 설정이 왜 안 먹지」 를 영영
|
|
571
|
+
* 모른 채로 남는데, 그건 못 읽어도 되는 줄이 아니다.
|
|
572
|
+
*/
|
|
573
|
+
if (언어() === 'ko') {
|
|
574
|
+
say(` ${c.bold('프로젝트 설정 신뢰')}`);
|
|
575
|
+
say('');
|
|
576
|
+
say(` ${c.cyan('deel trust')} 이 폴더의 ${c.gray('.deel/config.json')} 을 읽게 합니다`);
|
|
577
|
+
say(` ${c.gray('--off')} 다시 안 읽게`);
|
|
578
|
+
say(` ${c.gray('--list')} 믿는 폴더와, 믿어도 프로젝트가 못 정하는 칸`);
|
|
579
|
+
say(` ${c.gray('설정 파일은 저장소에 딸려 옵니다. 남의 것을 그냥 읽으면 주소도 열쇠 받는 명령도 그쪽이 정합니다.')}`);
|
|
580
|
+
say('');
|
|
581
|
+
say(` ${c.cyan('deel rules')} 적어 둔 승인 규칙을 늘어놓습니다`);
|
|
582
|
+
say(` ${c.cyan('deel rules check "<명령>"')} 그 명령을 어느 규칙이 어떻게 정하는지`);
|
|
583
|
+
say(` ${c.cyan('deel rules check')} 설정에 적어 둔 보기를 다 돌립니다 ${c.gray('(CI 에 겁니다)')}`);
|
|
584
|
+
say(` ${c.gray('Bash(rm -rf*) 는 sudo rm -rf 를 안 막습니다. 적은 사람은 막힌 줄 알고 지냅니다.')}`);
|
|
585
|
+
say('');
|
|
586
|
+
say(` ${c.cyan('deel stats')} 이 폴더에서 무엇을 했는지 ${c.gray('(.deel/audit.jsonl 요약)')}`);
|
|
587
|
+
say(` ${c.gray('--days N · --all · --json')} 기간을 바꾸거나, 전부, 또는 기계가 읽을 모양으로`);
|
|
588
|
+
say('');
|
|
589
|
+
say(` ${c.cyan('deel doctor')} 붙기 전에 무엇이 막고 있는지 ${c.gray('(프록시·인증서·열쇠·모델 목록)')}`);
|
|
590
|
+
say(` ${c.cyan('deel config explain <칸>')} 그 값이 어느 층에서 온 것인지 ${c.gray('(정책 · 환경변수 · 프로젝트 · 이 PC)')}`);
|
|
591
|
+
} else {
|
|
592
|
+
say(` ${c.bold('Project config trust')}`);
|
|
593
|
+
say('');
|
|
594
|
+
say(` ${c.cyan('deel trust')} Read this folder's ${c.gray('.deel/config.json')}`);
|
|
595
|
+
say(` ${c.gray('--off')} Stop reading it again`);
|
|
596
|
+
say(` ${c.gray('--list')} Trusted folders, and the keys a project may never set`);
|
|
597
|
+
say(` ${c.gray('A config file ships with the repository. Read a stranger’s and it picks the endpoint, and the command that fetches your key.')}`);
|
|
598
|
+
say('');
|
|
599
|
+
say(` ${c.cyan('deel rules')} List the approval rules in force`);
|
|
600
|
+
say(` ${c.cyan('deel rules check "<cmd>"')} Which rule decides that command, and how`);
|
|
601
|
+
say(` ${c.cyan('deel rules check')} Run the examples from your config ${c.gray('(for CI)')}`);
|
|
602
|
+
say(` ${c.gray('Bash(rm -rf*) does not stop sudo rm -rf. The person who wrote it believes it does.')}`);
|
|
603
|
+
say('');
|
|
604
|
+
say(` ${c.cyan('deel stats')} What happened in this folder ${c.gray('(summarises .deel/audit.jsonl)')}`);
|
|
605
|
+
say(` ${c.gray('--days N · --all · --json')} Change the window, read everything, or emit JSON`);
|
|
606
|
+
say('');
|
|
607
|
+
say(` ${c.cyan('deel doctor')} What is blocking the connection ${c.gray('(proxy · certs · key · model list)')}`);
|
|
608
|
+
say(` ${c.cyan('deel config explain <key>')} Which layer that value came from ${c.gray('(policy · env · project · this PC)')}`);
|
|
609
|
+
}
|
|
610
|
+
say('');
|
|
264
611
|
say(` ${c.bold('대화 시작 옵션')}`);
|
|
265
612
|
say('');
|
|
266
613
|
say(` ${c.gray('--root <폴더>')} 작업 범위. 기본은 지금 폴더`);
|
|
@@ -272,6 +619,7 @@ function help() {
|
|
|
272
619
|
say(` ${c.gray('--think <수준>')} off / low / medium(기본) / high / xhigh / max`);
|
|
273
620
|
say(` ${c.gray('--effort <배분>')} even(균일) / save(절약, 기본) / deep(깊게)`);
|
|
274
621
|
say(` ${c.gray('--no-tui')} 입력 상자 없이 줄 화면으로 (파이프·기록·좁은 터미널)`);
|
|
622
|
+
say(` ${c.gray('--no-hooks')} 이번 판만 훅을 끄고 돕니다 (.deel/hooks.json · /hooks)`);
|
|
275
623
|
// 실행 모드 셋. 기본이 잠겨 있다는 것을 여기서 분명히 말한다 —
|
|
276
624
|
// 「--online 이 있다」 보다 「기본은 안 나간다」 가 사람이 알아야 할 쪽이다.
|
|
277
625
|
say(` ${c.gray('(기본)')} ⌂ 이 안 — 바깥 주소면 한 번 물어보고 기억합니다`);
|
|
@@ -286,16 +634,25 @@ function help() {
|
|
|
286
634
|
say(` ${c.cyan('echo "..." | deel run')} ${c.gray('시킬 말을 표준입력으로 넣어도 됩니다')}`);
|
|
287
635
|
// 대화 화면에서 손으로 치던 슬래시 명령을 그대로 배치에 옮기는 자리 (src/oneshot.js).
|
|
288
636
|
say(` ${c.cyan('deel run /배포점검 서버3')} ${c.gray('.claude/commands 에 적어 둔 슬래시 명령도 그대로')}`);
|
|
637
|
+
say(` ${c.cyan('deel run --output-schema out.schema.json "..." | jq -r .term')}`);
|
|
289
638
|
say(` ${c.gray('deel -p "..." 도 같습니다.')} ${c.gray('위 대화 시작 옵션을 그대로 씁니다.')}`);
|
|
290
639
|
say('');
|
|
291
640
|
say(` ${c.gray('--json')} 결과를 JSON 한 덩이로 (답·도구 횟수·토큰·끝난 까닭)`);
|
|
292
641
|
say(` ${c.gray('--quiet')} 도구가 무엇을 했는지 안 적음 (오류는 그래도 적음)`);
|
|
293
642
|
say(` ${c.gray('--yes')} 승인이 필요한 것도 그냥 실행. ${c.yellow('기본은 거부입니다')}`);
|
|
643
|
+
/*
|
|
644
|
+
* 답의 모양을 못 박는 자리 (src/agent/outschema.js).
|
|
645
|
+
*
|
|
646
|
+
* 이걸 주면 표준출력은 **그 JSON 하나**다 — 울타리도 인사말도 안 섞인다.
|
|
647
|
+
* 안 맞으면 한 번 더 시키고, 그래도 안 맞으면 7 로 끝낸다. 파이프 뒤에
|
|
648
|
+
* `jq` 를 붙일 수 있게 하는 것이 이 깃발의 전부다.
|
|
649
|
+
*/
|
|
650
|
+
say(` ${c.gray('--output-schema <파일>')} 답을 JSON Schema 모양으로 받음 ${c.gray('(안 맞으면 7)')}`);
|
|
294
651
|
say(` ${c.gray('답은 표준출력, 도구 기록은 표준오류로 나갑니다 — 파이프로 넘겨도 답만 넘어갑니다.')}`);
|
|
295
652
|
// 이 줄은 src/oneshot.js 의 EXIT 와 짝이다. test/exitcode.test.js 가 둘이
|
|
296
653
|
// 어긋나면 빨개진다 — 한 판 동안 refusal(6) 이 여기서 빠져 있었고, 그
|
|
297
654
|
// 사실을 말해 주는 자리가 아무 데도 없었다.
|
|
298
|
-
say(` ${c.gray('끝난 까닭이 종료코드에 담깁니다:')} ${c.gray('0 끝냄 · 1 오류 · 2 걸음수상한 · 3 헛돎 · 4 중단 · 5 말없이끊김 · 6 거절')}`);
|
|
655
|
+
say(` ${c.gray('끝난 까닭이 종료코드에 담깁니다:')} ${c.gray('0 끝냄 · 1 오류 · 2 걸음수상한 · 3 헛돎 · 4 중단 · 5 말없이끊김 · 6 거절 · 7 모양안맞음')}`);
|
|
299
656
|
say('');
|
|
300
657
|
say(` ${c.bold('진단 직접 지정')} ${c.gray('— 설정을 남기지 않고 확인만 할 때')}`);
|
|
301
658
|
say('');
|
|
@@ -363,8 +720,11 @@ async function main() {
|
|
|
363
720
|
offline: flags.offline === true || flags.offline === 'true',
|
|
364
721
|
online: flags.online === true || flags.online === 'true',
|
|
365
722
|
yes: flags.yes === true || flags.yes === 'true',
|
|
723
|
+
hooks: flags['no-hooks'] === true ? false : undefined,
|
|
366
724
|
json: flags.json === true || flags.json === 'true',
|
|
367
725
|
quiet: flags.quiet === true || flags.quiet === 'true',
|
|
726
|
+
// 답의 모양을 못 박는다. 이게 있으면 표준출력은 그 JSON 하나다.
|
|
727
|
+
outputSchema: flags['output-schema'] ? String(flags['output-schema']) : undefined,
|
|
368
728
|
});
|
|
369
729
|
case '':
|
|
370
730
|
case 'chat':
|
|
@@ -381,6 +741,9 @@ async function main() {
|
|
|
381
741
|
// --no-tui 입력 상자 없이 줄 화면으로 (파이프·기록·좁은 터미널)
|
|
382
742
|
// --tui 터미널이면 무조건 입력 상자를 켠다
|
|
383
743
|
tui: flags['no-tui'] === true ? false : (flags.tui === true ? true : null),
|
|
744
|
+
// 훅을 이번 판만 끈다 (safety/hooks.js). 훅이 망가졌을 때 고칠 길이
|
|
745
|
+
// 훅 파일을 지우는 것뿐이면 안 된다.
|
|
746
|
+
hooks: flags['no-hooks'] === true ? false : undefined,
|
|
384
747
|
offline: flags.offline === true || flags.offline === 'true',
|
|
385
748
|
online: flags.online === true || flags.online === 'true',
|
|
386
749
|
continue: flags.continue === true || flags.c === true,
|
|
@@ -402,6 +765,7 @@ async function main() {
|
|
|
402
765
|
maxTokens: flags['max-tokens'] ? parseSize(String(flags['max-tokens'])) : undefined,
|
|
403
766
|
think: flags.think ? String(flags.think) : undefined,
|
|
404
767
|
effort: flags.effort ? String(flags.effort) : undefined,
|
|
768
|
+
hooks: flags['no-hooks'] === true ? false : undefined,
|
|
405
769
|
offline: flags.offline === true || flags.offline === 'true',
|
|
406
770
|
online: flags.online === true || flags.online === 'true',
|
|
407
771
|
});
|
|
@@ -410,12 +774,28 @@ async function main() {
|
|
|
410
774
|
case 'setup':
|
|
411
775
|
return runSetup();
|
|
412
776
|
case 'diagnose':
|
|
413
|
-
case 'doctor':
|
|
414
777
|
return runDiagnose(flags);
|
|
778
|
+
/*
|
|
779
|
+
* doctor 는 diagnose 의 **앞자락**이다.
|
|
780
|
+
*
|
|
781
|
+
* diagnose 는 「모델이 일을 할 수 있나」 를 잰다. 그건 붙은 다음 이야기고,
|
|
782
|
+
* 사내에서 막히는 자리는 대부분 그 앞이다 — 프록시·사내 루트·열쇠·인증서.
|
|
783
|
+
* 그래서 자리 조건을 먼저 하나씩 보고, 닿으면 이어서 모델까지 본다.
|
|
784
|
+
*/
|
|
785
|
+
case 'doctor':
|
|
786
|
+
return runDoctor(flags);
|
|
415
787
|
case 'pack':
|
|
416
788
|
return runPack(flags);
|
|
417
789
|
case 'audit':
|
|
418
790
|
return runAudit();
|
|
791
|
+
case 'stats':
|
|
792
|
+
return runStats(flags);
|
|
793
|
+
case 'config':
|
|
794
|
+
return runConfig(args, flags);
|
|
795
|
+
case 'trust':
|
|
796
|
+
return runTrust(flags);
|
|
797
|
+
case 'rules':
|
|
798
|
+
return runRules(args, flags);
|
|
419
799
|
case 'doc2md':
|
|
420
800
|
return runDoc2md(args, flags);
|
|
421
801
|
case 'sbom':
|
package/package.json
CHANGED
package/src/acp/serve.js
CHANGED
|
@@ -31,6 +31,9 @@ import { existsSync } from 'node:fs';
|
|
|
31
31
|
import { join, resolve } from 'node:path';
|
|
32
32
|
import { VERSION } from '../version.js';
|
|
33
33
|
import { 규칙모으기, 늘허락, 정책읽기 } from '../safety/policy.js';
|
|
34
|
+
import { 훅읽기 } from '../safety/hooks.js';
|
|
35
|
+
import { 에이전트읽기 } from '../agent/agents.js';
|
|
36
|
+
import { 남길것읽기 } from '../safety/shellenv.js';
|
|
34
37
|
import { 받기설정 } from '../safety/authcmd.js';
|
|
35
38
|
import { run } from '../agent/loop.js';
|
|
36
39
|
import { Session, repairToolPairs } from '../agent/session.js';
|
|
@@ -47,6 +50,8 @@ import { allowEndpoint, setOffline } from '../safety/network.js';
|
|
|
47
50
|
import { 지금모드, 바깥인가, 나갈수있나 } from '../safety/runmode.js';
|
|
48
51
|
import { 주소가리기 } from '../safety/secrets.js';
|
|
49
52
|
import { probeCtx, 기본값 as CTX_DEFAULT } from '../backend/ctxsize.js';
|
|
53
|
+
import { 잠잠기본 } from '../backend/http.js';
|
|
54
|
+
import { 인증서설정 } from '../backend/clientcert.js';
|
|
50
55
|
import { 다붙이기 } from '../backend/mcp.js';
|
|
51
56
|
import { 배움 } from '../agent/evolve.js';
|
|
52
57
|
import { 카드 } from '../agent/card.js';
|
|
@@ -216,6 +221,17 @@ export async function acp(opts = {}) {
|
|
|
216
221
|
* test/doorparity.test.js 가 세 문의 열쇠 집합을 맞춰 본다.
|
|
217
222
|
*/
|
|
218
223
|
vision: prof.vision ?? false,
|
|
224
|
+
/*
|
|
225
|
+
* 흘려 받다가 이만큼 잠잠하면 끊는다 (밀리초, backend/http.js).
|
|
226
|
+
*
|
|
227
|
+
* 답이 다 오는 데 걸린 시간이 아니라 **아무것도 안 온 시간**이다. 그래서
|
|
228
|
+
* 30분짜리 답은 안 끊기고 30초 멎은 연결은 30초에 끊긴다. 답을 통째로
|
|
229
|
+
* 모았다가 한 번에 주는 사내 게이트웨이면 이 값을 올린다.
|
|
230
|
+
*/
|
|
231
|
+
잠잠: prof.잠잠 ?? prof.streamIdleMs ?? 잠잠기본,
|
|
232
|
+
// 게이트웨이가 우리 인증서를 요구하면 (mTLS, backend/clientcert.js).
|
|
233
|
+
// 파일 경로만 싣는다 — 알맹이는 요청 직전에 읽는다.
|
|
234
|
+
인증서: 인증서설정(prof),
|
|
219
235
|
열쇠받기: 받기설정(prof, { 정책값: 정책읽기().값 }),
|
|
220
236
|
};
|
|
221
237
|
/*
|
|
@@ -332,6 +348,10 @@ export async function acp(opts = {}) {
|
|
|
332
348
|
돌던도구: new Map(), // 도구 이름 → 아직 안 끝난 호출 아이디들
|
|
333
349
|
};
|
|
334
350
|
|
|
351
|
+
// 훅은 방마다 새로 읽는다 — 에디터는 폴더마다 방을 하나씩 여니까 (safety/hooks.js).
|
|
352
|
+
const 훅정보 = 훅읽기(root, { 켜짐: opts.hooks === false ? false : null });
|
|
353
|
+
const 에이전트정보 = 에이전트읽기(root);
|
|
354
|
+
|
|
335
355
|
방.ctx = {
|
|
336
356
|
scope: makeScope(root),
|
|
337
357
|
get 모델컨텍스트() { return conn.ctx ?? null; },
|
|
@@ -339,6 +359,14 @@ export async function acp(opts = {}) {
|
|
|
339
359
|
get 눈있나() { return !!conn.vision; },
|
|
340
360
|
// 적어 둔 허락·금지 규칙 (safety/policy.js). 승인 모드보다 먼저 본다.
|
|
341
361
|
규칙들: 규칙모으기(cfg),
|
|
362
|
+
// 사람이 적어 둔 훅 (safety/hooks.js). 에디터 안에서도 같은 것이 돌아야 한다 —
|
|
363
|
+
// 여기만 빠지면 「터미널에서는 막히고 에디터에서는 안 막힌다」 가 된다.
|
|
364
|
+
훅들: 훅정보.훅들,
|
|
365
|
+
에이전트들: 에이전트정보.에이전트들,
|
|
366
|
+
// Bash 자식에게 되살려 줄 환경변수 이름 (safety/shellenv.js).
|
|
367
|
+
// 에디터 안에서도 같은 값이어야 한다 — 여기만 빠지면 「에디터에서만
|
|
368
|
+
// 빌드가 된다·안 된다」 가 되고, 그건 원인을 찾을 길이 없다.
|
|
369
|
+
셸남길것: 남길것읽기(cfg),
|
|
342
370
|
history: new History(root),
|
|
343
371
|
audit: new Audit(root, { 열쇠들: 열쇠묻기(conn) }),
|
|
344
372
|
seen: new Set(),
|
|
@@ -552,7 +580,17 @@ export async function acp(opts = {}) {
|
|
|
552
580
|
* 넘어가는데, 정작 답의 뒷부분은 오지도 않았다.
|
|
553
581
|
*/
|
|
554
582
|
case 'cutoff':
|
|
555
|
-
말하기(`\n\n_(${옮긴말('ev.cutoff')})_\n\n`);
|
|
583
|
+
말하기(`\n\n_(${ev.멎은초 ? 옮긴말('ev.stalled', { n: ev.멎은초 }) : 옮긴말('ev.cutoff')})_\n\n`);
|
|
584
|
+
break;
|
|
585
|
+
|
|
586
|
+
// 훅이 무슨 말을 했다 (safety/hooks.js). 에디터에도 적어 둔다 —
|
|
587
|
+
// 안 적으면 「터미널에서는 뭐라고 하는데 에디터에서는 조용하다」 가 된다.
|
|
588
|
+
case 'hook_note':
|
|
589
|
+
말하기(`\n\n_(${옮긴말('ev.hookNote', { 말: String(ev.말).split('\n')[0] })})_\n\n`);
|
|
590
|
+
break;
|
|
591
|
+
|
|
592
|
+
case 'hook_block':
|
|
593
|
+
말하기(`\n\n_(${옮긴말('ev.hookBlock')})_\n\n`);
|
|
556
594
|
break;
|
|
557
595
|
|
|
558
596
|
case 'nudge':
|