deel-local-cli 0.5.0 → 0.8.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 +526 -135
- package/README.md +652 -149
- package/bin/deel.js +67 -1
- package/package.json +4 -3
- package/src/agent/compact.js +138 -0
- package/src/agent/effort.js +139 -0
- package/src/agent/loop.js +186 -60
- package/src/agent/modes.js +153 -0
- package/src/agent/session.js +14 -3
- package/src/agent/sessionui.js +59 -0
- package/src/agent/store.js +193 -0
- package/src/backend/adapter.js +12 -2
- package/src/backend/http.js +21 -3
- package/src/backend/scan.js +162 -0
- package/src/backend/scanui.js +147 -0
- package/src/commands.js +307 -17
- package/src/config.js +19 -6
- package/src/pack/selfpack.js +248 -0
- package/src/pack/tar.js +69 -0
- package/src/pack/zip.js +217 -0
- package/src/plugins/manage.js +272 -0
- package/src/repl.js +230 -38
- package/src/safety/network.js +95 -0
- package/src/safety/undo.js +46 -1
- package/src/setup.js +4 -0
- package/src/tools/encoding.js +333 -0
- package/src/tools/excel-com.js +254 -0
- package/src/tools/excel.js +118 -0
- package/src/tools/fsutil.js +47 -4
- package/src/tools/index.js +128 -14
- package/src/tools/todo.js +92 -0
- package/src/tools/webfetch.js +110 -0
- package/src/tools/xlsx.js +319 -0
- package/src/ui/ansi.js +78 -4
- package/src/ui/level.js +104 -0
- package/src/ui/prompt.js +34 -0
- package/src/ui/status.js +151 -0
package/src/commands.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
// 슬래시 명령. 이름은 Claude Code / Codex 관례에 맞춘다.
|
|
2
2
|
import { writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { c, say, rule, pad, bar, mark, width } from './ui/ansi.js';
|
|
4
|
+
import { c, say, rule, pad, bar, mark, width, clip } from './ui/ansi.js';
|
|
5
|
+
import { compact } from './agent/compact.js';
|
|
6
|
+
import { allowEndpoint } from './safety/network.js';
|
|
5
7
|
import { pick } from './ui/prompt.js';
|
|
6
|
-
import { load, save, resolveKey } from './config.js';
|
|
8
|
+
import { load, save, resolveKey, upsert } from './config.js';
|
|
7
9
|
import { TOOLS } from './tools/index.js';
|
|
8
|
-
import { loadCommand } from './skills/discover.js';
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
import { loadCommand, discover } from './skills/discover.js';
|
|
11
|
+
import { install, list, remove, pack } from './plugins/manage.js';
|
|
12
|
+
import { spin } from './ui/spinner.js';
|
|
13
|
+
import { PROFILES, LEVELS as THINK_LEVELS, normalizeProfile, table as effortTable } from './agent/effort.js';
|
|
14
|
+
import { scanLocal, toProfiles } from './backend/scan.js';
|
|
15
|
+
import { list as listSessions } from './agent/store.js';
|
|
16
|
+
import { MODES as WORK_MODES, ORDER as WORK_ORDER, normalize as normWork, get as getWork, canWrite } from './agent/modes.js';
|
|
17
|
+
import { LEVELS, ORDER as LEVEL_ORDER, DEFAULT as LEVEL_DEFAULT, normalize as normLevel, shows as levelShows } from './ui/level.js';
|
|
11
18
|
const MODES = {
|
|
12
19
|
auto: '자율 — 전부 알아서. 되돌리기가 안전망',
|
|
13
20
|
confirm: '확인 — 되돌릴 수 없는 것만 물어봄',
|
|
@@ -21,12 +28,23 @@ export const COMMANDS = {
|
|
|
21
28
|
compact: { desc: '오래된 대화 줄이기' },
|
|
22
29
|
model: { desc: '모델·연결 바꾸기' },
|
|
23
30
|
think: { desc: '추론 강도 (off/low/medium/high/max)', arg: '<수준>' },
|
|
24
|
-
mode: { desc: '
|
|
31
|
+
mode: { desc: '승인 정책 — 얼마나 물어보나 (auto/confirm/strict)', arg: '<모드>' },
|
|
32
|
+
work: { desc: '작업 모드 — 무슨 일을 하는 중인가', arg: '[모드]' },
|
|
33
|
+
code: { desc: '작업 모드 → 코드 (고치고 만든다)' },
|
|
34
|
+
plan: { desc: '작업 모드 → 계획 (먼저 계획만)' },
|
|
35
|
+
architect:{ desc: '작업 모드 → 설계 (구조를 짠다)' },
|
|
36
|
+
debug: { desc: '작업 모드 → 디버그 (원인을 찾는다)' },
|
|
37
|
+
ask: { desc: '작업 모드 → 묻기 (설명만)' },
|
|
38
|
+
orchestrator: { desc: '작업 모드 → 총괄 (큰 일을 쪼개서)' },
|
|
25
39
|
undo: { desc: '직전 작업 되돌리기', arg: '[턴수]' },
|
|
26
40
|
tools: { desc: '쓸 수 있는 도구 보기' },
|
|
27
41
|
skills: { desc: '스킬 보기·검색·골라 올리기', arg: '[검색어|all|off]' },
|
|
42
|
+
plugin: { desc: '플러그인 목록·설치·삭제·반입묶음', arg: '[install|remove|pack]' },
|
|
28
43
|
cost: { desc: '이번 세션 사용량' },
|
|
29
44
|
status: { desc: '연결 상태' },
|
|
45
|
+
scan: { desc: '이 PC 의 로컬 모델 서버 훑기', arg: '[save]' },
|
|
46
|
+
sessions:{ desc: '이 폴더의 지난 대화 목록' },
|
|
47
|
+
level: { desc: '사용자 수준 (쉬움/개발자)', arg: '[수준]' },
|
|
30
48
|
init: { desc: 'DEEL.md 규칙 파일 만들기' },
|
|
31
49
|
exit: { desc: '끝내기' },
|
|
32
50
|
quit: { desc: '끝내기' },
|
|
@@ -40,7 +58,8 @@ export async function handle(line, session, ctx) {
|
|
|
40
58
|
const arg = rest.join(' ');
|
|
41
59
|
|
|
42
60
|
switch (name) {
|
|
43
|
-
case 'help': return help(), { handled: true };
|
|
61
|
+
case 'help': return help(session), { handled: true };
|
|
62
|
+
case 'level': return showLevel(session, arg), { handled: true };
|
|
44
63
|
case 'exit':
|
|
45
64
|
case 'quit': return { handled: true, exit: true };
|
|
46
65
|
|
|
@@ -53,10 +72,21 @@ export async function handle(line, session, ctx) {
|
|
|
53
72
|
case 'context': return showContext(session), { handled: true };
|
|
54
73
|
|
|
55
74
|
case 'compact': {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
75
|
+
const s = spin('앞선 대화를 요약해 접는 중…');
|
|
76
|
+
const r = await compact(session);
|
|
77
|
+
if (!r.ok) {
|
|
78
|
+
s.stop(` ${c.gray(r.why ?? '접지 못했습니다.')}`);
|
|
79
|
+
say('');
|
|
80
|
+
return { handled: true };
|
|
81
|
+
}
|
|
82
|
+
const 줄인 = r.before - r.after;
|
|
83
|
+
s.stop(` ${mark.ok} 대화 ${r.folded}개를 요약으로 접었습니다.`);
|
|
84
|
+
say(` ${c.gray(r.before.toLocaleString())} ${c.gray('→')} ${c.white(r.after.toLocaleString())} ${c.gray('토큰')} ${c.green(`${Math.round((줄인 / Math.max(1, r.before)) * 100)}% 줄어듦`)}`);
|
|
85
|
+
if (r.fallback) say(` ${c.yellow('요약을 못 받아 그냥 줄였습니다.')}`);
|
|
86
|
+
else if (r.summary) {
|
|
87
|
+
say('');
|
|
88
|
+
for (const line of r.summary.split('\n').slice(0, 14)) say(` ${c.gray(clip(line, 76))}`);
|
|
89
|
+
}
|
|
60
90
|
say('');
|
|
61
91
|
return { handled: true };
|
|
62
92
|
}
|
|
@@ -64,18 +94,25 @@ export async function handle(line, session, ctx) {
|
|
|
64
94
|
case 'model': return await switchModel(session, ctx), { handled: true };
|
|
65
95
|
|
|
66
96
|
case 'think': {
|
|
97
|
+
const asProfile = normalizeProfile(arg);
|
|
98
|
+
if (asProfile) {
|
|
99
|
+
session.effort = asProfile;
|
|
100
|
+
session.effortSet = true; // 사용자가 직접 정했다 — 작업 모드보다 우선한다
|
|
101
|
+
say(` ${mark.ok} 배분 ${c.bold(PROFILES[asProfile].name)} ${c.gray('— ' + PROFILES[asProfile].desc)}`);
|
|
102
|
+
showThink(session);
|
|
103
|
+
return { handled: true };
|
|
104
|
+
}
|
|
67
105
|
if (!THINK_LEVELS.includes(arg)) {
|
|
68
|
-
|
|
69
|
-
say(` ${c.gray('예')} /think high`);
|
|
70
|
-
say('');
|
|
106
|
+
showThink(session);
|
|
71
107
|
return { handled: true };
|
|
72
108
|
}
|
|
73
109
|
session.think = arg;
|
|
110
|
+
session.thinkSet = true; // 사용자가 직접 정했다 — 작업 모드보다 우선한다
|
|
74
111
|
say(` ${mark.ok} 추론 강도 ${c.bold(arg)}`);
|
|
75
112
|
if (!session.conn.think && arg !== 'off') {
|
|
76
113
|
say(` ${c.yellow('이 연결은 모델 층 조절이 안 먹습니다.')} ${c.gray('루프 층(도구 호출 상한)으로만 조절됩니다.')}`);
|
|
77
114
|
}
|
|
78
|
-
|
|
115
|
+
showThink(session);
|
|
79
116
|
return { handled: true };
|
|
80
117
|
}
|
|
81
118
|
|
|
@@ -146,6 +183,75 @@ export async function handle(line, session, ctx) {
|
|
|
146
183
|
return { handled: true };
|
|
147
184
|
}
|
|
148
185
|
|
|
186
|
+
case 'work':
|
|
187
|
+
case 'code': case 'plan': case 'architect':
|
|
188
|
+
case 'debug': case 'ask': case 'orchestrator': {
|
|
189
|
+
const 원하는 = name === 'work' ? normWork(arg) : name;
|
|
190
|
+
if (name === 'work' && !원하는) { showWork(session); return { handled: true }; }
|
|
191
|
+
const 골라진 = normWork(원하는);
|
|
192
|
+
if (!골라진) {
|
|
193
|
+
say(` ${mark.warn} 그런 모드는 없습니다: ${c.white(arg)}`);
|
|
194
|
+
say(` ${c.gray('쓸 수 있는 것:')} ${WORK_ORDER.map((k) => c.cyan(WORK_MODES[k].name)).join(c.gray(' · '))}`);
|
|
195
|
+
say('');
|
|
196
|
+
return { handled: true };
|
|
197
|
+
}
|
|
198
|
+
session.work = 골라진;
|
|
199
|
+
const w = getWork(골라진);
|
|
200
|
+
say('');
|
|
201
|
+
say(` ${c.hcyan(w.glyph)} ${c.bold(w.name)} ${c.gray('(' + w.en + ')')} ${c.gray(w.hint)}`);
|
|
202
|
+
say(` ${c.gray('도구')} ${canWrite(골라진) ? c.yellow('읽기 + 파일 바꾸기') : c.green('읽기만 — 파일을 못 바꿉니다')}`);
|
|
203
|
+
say(` ${c.gray('생각')} ${c.white(w.think ?? session.think)}${c.gray('·')}${c.magenta(w.effort)} ${c.gray('최대 ' + w.steps + '걸음')}`);
|
|
204
|
+
say('');
|
|
205
|
+
return { handled: true };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case 'scan': {
|
|
209
|
+
const s2 = spin('이 PC 의 로컬 서버를 찾는 중…');
|
|
210
|
+
const found = await scanLocal({ timeout: 1500 });
|
|
211
|
+
s2.stop(` ${found.length ? mark.ok : mark.warn} ${found.length}곳 찾음`);
|
|
212
|
+
if (!found.length) {
|
|
213
|
+
say(` ${c.gray('떠 있는 로컬 서버가 없습니다. 다른 포트라면')} ${c.cyan('deel scan --ports ...')}`);
|
|
214
|
+
say('');
|
|
215
|
+
return { handled: true };
|
|
216
|
+
}
|
|
217
|
+
for (const f of found) {
|
|
218
|
+
say(` ${c.hcyan('◆')} ${c.bold(pad(f.runtime, 12))}${c.gray(pad(`${f.host}:${f.port}`, 22))}${c.gray(`모델 ${f.models.length}개`)}`);
|
|
219
|
+
for (const m of f.models.slice(0, 6)) say(` ${c.gray('·')} ${clip(m.id, 44)}`);
|
|
220
|
+
if (f.models.length > 6) say(` ${c.gray(`… 그 밖에 ${f.models.length - 6}개`)}`);
|
|
221
|
+
}
|
|
222
|
+
say('');
|
|
223
|
+
if (arg === 'save') {
|
|
224
|
+
const cfg2 = load();
|
|
225
|
+
const ps = toProfiles(found, cfg2.profiles);
|
|
226
|
+
for (const x of ps) upsert(cfg2, x);
|
|
227
|
+
save(cfg2);
|
|
228
|
+
say(` ${mark.ok} ${ps.length}개 등록했습니다. ${c.cyan('/model')} 로 고르세요.`);
|
|
229
|
+
} else {
|
|
230
|
+
say(` ${c.gray('등록하려면')} ${c.cyan('/scan save')} ${c.gray('고르려면')} ${c.cyan('/model')}`);
|
|
231
|
+
}
|
|
232
|
+
say('');
|
|
233
|
+
return { handled: true };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
case 'sessions': {
|
|
237
|
+
const rows = listSessions(session.root, { limit: 12 });
|
|
238
|
+
rule('이 폴더의 지난 대화', 70);
|
|
239
|
+
if (!rows.length) {
|
|
240
|
+
say(` ${c.gray('아직 없습니다. 지금 이 대화가 첫 번째입니다.')}`);
|
|
241
|
+
say('');
|
|
242
|
+
return { handled: true };
|
|
243
|
+
}
|
|
244
|
+
for (const [i, r] of rows.entries()) {
|
|
245
|
+
say(` ${i === 0 ? c.hgreen('●') : c.gray('·')} ${c.bold(pad(r.id, 17))}${c.gray(pad(`${r.turns}턴`, 6, 'right'))} ${c.gray(clip(r.model, 22))}`);
|
|
246
|
+
say(` ${c.gray(clip(r.first, 66))}`);
|
|
247
|
+
}
|
|
248
|
+
say('');
|
|
249
|
+
say(` ${c.gray('이어하려면 나갔다가')} ${c.cyan('deel --continue')} ${c.gray('또는')} ${c.cyan(`deel --resume ${rows[0].id}`)}`);
|
|
250
|
+
say(` ${c.gray('지금 대화는 나가지 않아도 계속 저장되고 있습니다.')}`);
|
|
251
|
+
say('');
|
|
252
|
+
return { handled: true };
|
|
253
|
+
}
|
|
254
|
+
|
|
149
255
|
case 'init': {
|
|
150
256
|
const p = join(session.root, 'DEEL.md');
|
|
151
257
|
if (existsSync(p)) {
|
|
@@ -162,6 +268,9 @@ export async function handle(line, session, ctx) {
|
|
|
162
268
|
|
|
163
269
|
case 'skills': return showSkills(session, arg), { handled: true };
|
|
164
270
|
|
|
271
|
+
case 'plugin':
|
|
272
|
+
case 'plugins': return await doPlugin(session, arg), { handled: true };
|
|
273
|
+
|
|
165
274
|
default: {
|
|
166
275
|
// 이 PC 에서 찾은 슬래시 명령인지 본다. 있으면 그 내용을 모델에게 보낸다.
|
|
167
276
|
const found = (session.commands ?? []).find((x) => x.name === raw)
|
|
@@ -185,6 +294,97 @@ export async function handle(line, session, ctx) {
|
|
|
185
294
|
}
|
|
186
295
|
}
|
|
187
296
|
|
|
297
|
+
async function doPlugin(session, arg) {
|
|
298
|
+
const [sub = '', ...rest] = arg.trim().split(/\s+/);
|
|
299
|
+
const param = rest.join(' ').trim();
|
|
300
|
+
|
|
301
|
+
// 설치·삭제 뒤에는 다시 훑어 이번 대화에 바로 반영한다.
|
|
302
|
+
const rescan = () => {
|
|
303
|
+
const f = discover(session.root);
|
|
304
|
+
session.skills = f.skills;
|
|
305
|
+
session.commands = f.commands;
|
|
306
|
+
session.plugins = f.plugins;
|
|
307
|
+
return f;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
if (sub === 'install' || sub === 'add') {
|
|
311
|
+
if (!param) {
|
|
312
|
+
say(` ${c.gray('예')} /plugin install affaan-m/ECC`);
|
|
313
|
+
say(` ${c.gray(' ')} /plugin install owner/repo#가지이름`);
|
|
314
|
+
say('');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
say('');
|
|
318
|
+
const s = spin(`${param} 받는 중...`);
|
|
319
|
+
const r = await install(param, { onStep: (m) => {} });
|
|
320
|
+
if (r.error) {
|
|
321
|
+
s.stop(` ${mark.no} ${c.red(r.error.split('\n')[0])}`);
|
|
322
|
+
for (const line of r.error.split('\n').slice(1)) say(` ${c.gray(line.trim())}`);
|
|
323
|
+
say(` ${c.gray('오프라인이면 zip 을 ~/.deel/plugins/ 에 직접 풀면 됩니다.')}`);
|
|
324
|
+
say('');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
s.stop(` ${mark.ok} ${c.bold(r.name)}${r.version ? ' ' + c.gray(r.version) : ''} ${c.gray(`(${r.license ?? '라이선스 미상'})`)}`);
|
|
328
|
+
say(` ${c.gray('스킬')} ${r.skills}개 ${c.gray('명령')} ${r.commands}개 ${c.gray(`· ${r.how} 로 받음`)}`);
|
|
329
|
+
if (r.hooks) say(` ${mark.warn} ${c.yellow(`실행 스크립트 ${r.hooks}개는 쓰지 않습니다`)} ${c.gray('(hook 미지원 · 반입 심사 대상)')}`);
|
|
330
|
+
const f = rescan();
|
|
331
|
+
say(` ${c.gray(`이제 스킬 ${f.skills.length}개 · 명령 ${f.commands.length}개`)}`);
|
|
332
|
+
say('');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (sub === 'remove' || sub === 'rm' || sub === 'uninstall') {
|
|
337
|
+
if (!param) { say(` ${c.gray('예')} /plugin remove ecc`); say(''); return; }
|
|
338
|
+
const r = remove(param);
|
|
339
|
+
if (r.error) { say(` ${mark.no} ${c.red(r.error)}`); say(''); return; }
|
|
340
|
+
const f = rescan();
|
|
341
|
+
say(` ${mark.ok} ${c.bold(r.removed)} 지웠습니다. ${c.gray(`이제 스킬 ${f.skills.length}개`)}`);
|
|
342
|
+
say('');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (sub === 'pack') {
|
|
347
|
+
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
348
|
+
const out = param || join(session.root, `deel-plugins-${stamp}.zip`);
|
|
349
|
+
const r = pack(out, { only: null });
|
|
350
|
+
if (r.error) { say(` ${mark.no} ${c.red(r.error)}`); say(''); return; }
|
|
351
|
+
say('');
|
|
352
|
+
say(` ${mark.ok} ${c.cyan(r.out)}`);
|
|
353
|
+
say(` ${c.gray('플러그인')} ${r.plugins.length}개 ${c.gray('· 파일')} ${r.files}개 ${c.gray('·')} ${(r.bytes / 1024).toFixed(0)}KB`);
|
|
354
|
+
if (r.skipped) say(` ${c.gray(`실행 스크립트 ${r.skipped}개는 뺐습니다`)}`);
|
|
355
|
+
say('');
|
|
356
|
+
rule('담긴 것', 74);
|
|
357
|
+
for (const p of r.plugins) {
|
|
358
|
+
say(` ${c.cyan(pad(p.name, 22))} ${c.gray(pad(p.version || '-', 9))} ${c.gray(pad(p.license || '미상', 14))} ${c.gray(`스킬 ${p.skills} · 명령 ${p.commands}`)}`);
|
|
359
|
+
}
|
|
360
|
+
say('');
|
|
361
|
+
say(` ${c.gray('오프라인 기기의')} ~/.deel/plugins/ ${c.gray('에 풀면 바로 인식됩니다. 설치 명령은 필요 없습니다.')}`);
|
|
362
|
+
say(` ${c.gray('묶음 안에 라이선스 표가 담긴 사용안내.txt 가 함께 들어 있습니다.')}`);
|
|
363
|
+
say('');
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 인자 없으면 목록
|
|
368
|
+
const items = list();
|
|
369
|
+
if (!items.length) {
|
|
370
|
+
say('');
|
|
371
|
+
say(` ${c.gray('설치된 플러그인이 없습니다.')}`);
|
|
372
|
+
say(` ${c.gray('받기')} /plugin install owner/repo`);
|
|
373
|
+
say(` ${c.gray('직접')} ~/.deel/plugins/ 에 폴더를 풀어 넣어도 됩니다`);
|
|
374
|
+
say('');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
say('');
|
|
378
|
+
rule('플러그인', 74);
|
|
379
|
+
for (const p of items) {
|
|
380
|
+
say(` ${c.cyan(pad(p.name, 22))} ${c.gray(pad(p.version || '-', 9))} ${c.gray(pad(p.license || '미상', 14))} ${c.gray(`스킬 ${p.skills} · 명령 ${p.commands}`)}`);
|
|
381
|
+
say(` ${c.gray(p.from)}`);
|
|
382
|
+
}
|
|
383
|
+
say('');
|
|
384
|
+
say(` ${c.gray('/plugin install <owner/repo> /plugin remove <이름> /plugin pack [파일]')}`);
|
|
385
|
+
say('');
|
|
386
|
+
}
|
|
387
|
+
|
|
188
388
|
function showSkills(session, arg) {
|
|
189
389
|
const all = session.skills ?? [];
|
|
190
390
|
if (!all.length) {
|
|
@@ -248,18 +448,88 @@ function showSkills(session, arg) {
|
|
|
248
448
|
say('');
|
|
249
449
|
}
|
|
250
450
|
|
|
251
|
-
function help() {
|
|
451
|
+
function help(session) {
|
|
452
|
+
const level = session?.level ?? LEVEL_DEFAULT;
|
|
453
|
+
const 쉬움 = !levelShows(level, '__전부__'); // 개발자면 show 가 null 이라 전부 참
|
|
252
454
|
say('');
|
|
253
|
-
rule('명령', 70);
|
|
455
|
+
rule(쉬움 ? '명령 (자주 쓰는 것)' : '명령', 70);
|
|
456
|
+
let 감춘수 = 0;
|
|
254
457
|
for (const [n, m] of Object.entries(COMMANDS)) {
|
|
255
458
|
if (n === 'quit') continue;
|
|
459
|
+
if (!levelShows(level, n)) { 감춘수++; continue; }
|
|
256
460
|
say(` ${c.cyan(pad('/' + n + (m.arg ? ' ' + m.arg : ''), 22))} ${c.gray(m.desc)}`);
|
|
257
461
|
}
|
|
258
462
|
say('');
|
|
463
|
+
// 감춘 것은 '못 쓰는 것' 이 아니다. 그 말을 분명히 해 둔다.
|
|
464
|
+
if (감춘수) {
|
|
465
|
+
say(` ${c.gray(`이 밖에 ${감춘수}개가 더 있습니다 — 쳐 넣으면 그대로 먹습니다.`)}`);
|
|
466
|
+
say(` ${c.gray('전부 보려면')} ${c.cyan('/level 개발자')}`);
|
|
467
|
+
say('');
|
|
468
|
+
}
|
|
259
469
|
say(` ${c.gray('그 밖의 입력은 모델에게 보냅니다. 빈 줄에서 Ctrl+C 로 끝냅니다.')}`);
|
|
260
470
|
say('');
|
|
261
471
|
}
|
|
262
472
|
|
|
473
|
+
// /level — 수준 보기·바꾸기. 설정 파일에 남겨 다음에도 그대로 쓴다.
|
|
474
|
+
function showLevel(session, arg) {
|
|
475
|
+
if (!arg) {
|
|
476
|
+
rule('사용자 수준', 70);
|
|
477
|
+
for (const k of LEVEL_ORDER) {
|
|
478
|
+
const lv = LEVELS[k];
|
|
479
|
+
const 지금 = k === (normLevel(session.level) ?? LEVEL_DEFAULT);
|
|
480
|
+
say(` ${지금 ? c.hgreen('●') : c.gray('·')} ${지금 ? c.bold(c.white(pad(lv.name, 10))) : c.gray(pad(lv.name, 10))}${c.gray(lv.hint)}`);
|
|
481
|
+
}
|
|
482
|
+
say('');
|
|
483
|
+
say(` ${c.gray('안전 장치는 두 수준이 똑같습니다 — 되돌리기·작업 범위·위험 명령 차단.')}`);
|
|
484
|
+
say(` ${c.gray('수준은 무엇을 보여줄지만 정합니다.')}`);
|
|
485
|
+
say('');
|
|
486
|
+
say(` ${c.gray('바꾸려면')} ${c.cyan('/level 쉬움')} ${c.gray('또는')} ${c.cyan('/level 개발자')}`);
|
|
487
|
+
say('');
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const 골라진 = normLevel(arg);
|
|
491
|
+
if (!골라진) {
|
|
492
|
+
say(` ${mark.warn} 그런 수준은 없습니다: ${c.white(arg)} ${c.gray('(쉬움 · 개발자)')}`);
|
|
493
|
+
say('');
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
session.level = 골라진;
|
|
497
|
+
try {
|
|
498
|
+
const cfg = load();
|
|
499
|
+
cfg.level = 골라진;
|
|
500
|
+
save(cfg);
|
|
501
|
+
} catch { /* 못 남겨도 이번 세션에는 먹는다 */ }
|
|
502
|
+
const lv = LEVELS[골라진];
|
|
503
|
+
say('');
|
|
504
|
+
say(` ${mark.ok} ${c.bold(lv.name)} ${c.gray('— ' + lv.hint)}`);
|
|
505
|
+
say(` ${c.gray('안전 장치는 그대로입니다.')}`);
|
|
506
|
+
say('');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 추론 강도는 값 하나가 아니라 '단계별 배분' 이다. 그 배분을 눈에 보이게 그린다.
|
|
510
|
+
function showThink(session) {
|
|
511
|
+
const b = session.breakdown();
|
|
512
|
+
const t = effortTable(session.think, session.effort, { ctx: b.total, used: b.used, max: session.conn.maxTokens ?? null });
|
|
513
|
+
say('');
|
|
514
|
+
rule('추론 강도', 70);
|
|
515
|
+
say(` 기준 ${c.bold(session.think)} 배분 ${c.bold(t.name)} ${c.gray(t.desc)}`);
|
|
516
|
+
say(` ${c.gray('상한은 이 모델 기준으로 계산합니다 — 컨텍스트')} ${c.white(t.ctx.toLocaleString())}${c.gray(', 지금 찬 양')} ${c.white(t.used.toLocaleString())}`);
|
|
517
|
+
say('');
|
|
518
|
+
say(` ${c.gray(pad('단계', 12) + pad('강도', 10) + pad('출력상한', 10) + '언제')}`);
|
|
519
|
+
for (const r of t.rows) {
|
|
520
|
+
const 화살 = r.moved > 0 ? c.yellow('↑') : r.moved < 0 ? c.cyan('↓') : c.gray('·');
|
|
521
|
+
say(` ${pad(r.label, 12)}${화살} ${pad(r.level, 8)}${pad(r.cap.toLocaleString(), 10, 'right')} ${c.gray(r.why)}`);
|
|
522
|
+
}
|
|
523
|
+
say('');
|
|
524
|
+
say(` ${c.gray('강도')} ${THINK_LEVELS.join(' · ')} ${c.gray('예')} ${c.cyan('/think high')}`);
|
|
525
|
+
say(` ${c.gray('배분')} ${Object.entries(PROFILES).map(([k, v]) => `${k}(${v.name})`).join(' · ')} ${c.gray('예')} ${c.cyan('/think save')}`);
|
|
526
|
+
if (!session.conn.think && session.think !== 'off') {
|
|
527
|
+
say('');
|
|
528
|
+
say(` ${c.yellow('이 연결은 모델 층 강도 조절이 안 먹습니다.')} ${c.gray('출력 상한만 단계별로 적용됩니다.')}`);
|
|
529
|
+
}
|
|
530
|
+
say('');
|
|
531
|
+
}
|
|
532
|
+
|
|
263
533
|
function showContext(session) {
|
|
264
534
|
const b = session.breakdown();
|
|
265
535
|
say('');
|
|
@@ -302,6 +572,9 @@ async function switchModel(session, ctx) {
|
|
|
302
572
|
kind: p.kind, base: p.baseUrl, auth: p.auth, key: resolveKey(p), model: p.model,
|
|
303
573
|
ctx: p.ctx, streaming: p.streaming, tools: p.tools, json: p.json, think: p.think,
|
|
304
574
|
});
|
|
575
|
+
// 자물쇠도 같이 옮긴다. 이걸 빼먹으면 옛 주소가 열린 채로 남고 새 주소는 막혀
|
|
576
|
+
// 다음 한마디에서 바로 "허용되지 않은 주소" 가 난다.
|
|
577
|
+
allowEndpoint(p.baseUrl);
|
|
305
578
|
say(` ${mark.ok} ${c.bold(p.name)} ${c.gray(p.model)} 로 바꿨습니다. 대화는 이어집니다.`);
|
|
306
579
|
say('');
|
|
307
580
|
}
|
|
@@ -325,3 +598,20 @@ const INIT_TEMPLATE = `# DEEL.md
|
|
|
325
598
|
- 고치기 전에 관련 파일을 먼저 읽는다
|
|
326
599
|
- (프로젝트에 맞는 규칙을 적으세요)
|
|
327
600
|
`;
|
|
601
|
+
|
|
602
|
+
// /work 를 인자 없이 부르면 지금 모드와 고를 수 있는 것들을 보여 준다.
|
|
603
|
+
function showWork(session) {
|
|
604
|
+
rule('작업 모드', 70);
|
|
605
|
+
for (const k of WORK_ORDER) {
|
|
606
|
+
const w = WORK_MODES[k];
|
|
607
|
+
const 지금 = k === (normWork(session.work) ?? 'code');
|
|
608
|
+
const 표 = 지금 ? c.hgreen('●') : c.gray('·');
|
|
609
|
+
const 이름 = 지금 ? c.bold(c.white(pad(w.name, 8))) : c.gray(pad(w.name, 8));
|
|
610
|
+
say(` ${표} ${c.hcyan(w.glyph)} ${이름}${c.gray(pad(w.en, 14))}${c.gray(w.hint)}`);
|
|
611
|
+
say(` ${canWrite(k) ? c.gray('파일 바꿈') : c.green('읽기만')}${c.gray(' · 생각 ' + (w.think ?? '그대로') + '·' + w.effort + ' · 최대 ' + w.steps + '걸음')}`);
|
|
612
|
+
}
|
|
613
|
+
say('');
|
|
614
|
+
say(` ${c.gray('바꾸려면')} ${c.cyan('/plan')} ${c.cyan('/code')} ${c.cyan('/debug')} ${c.gray('… 또는')} ${c.cyan('Shift+Tab')} ${c.gray('으로 차례로')}`);
|
|
615
|
+
say(` ${c.gray('이건 승인 정책(')}${c.cyan('/mode')}${c.gray(')과 다른 축입니다 — 무엇을 하느냐 / 얼마나 물어보냐.')}`);
|
|
616
|
+
say('');
|
|
617
|
+
}
|
package/src/config.js
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
// 연결 프로필 저장/읽기. ~/.deel/config.json (프로젝트 폴더의 .deel/config.json 이 우선)
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join, dirname } from 'node:path';
|
|
3
|
+
import { join, dirname, resolve } from 'node:path';
|
|
4
4
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// 설정이 놓이는 자리.
|
|
7
|
+
//
|
|
8
|
+
// DEEL_HOME 을 주면 그 폴더를 쓴다. 두 군데서 필요했다 —
|
|
9
|
+
// 1) 사내에서 USB·공유폴더에 통째로 넣어 쓰는 휴대용 설치.
|
|
10
|
+
// 집 폴더가 로밍 프로필이면 설정이 엉뚱한 데로 따라다닌다.
|
|
11
|
+
// 2) 검사. 예전에는 검사가 진짜 설정 파일에 값을 써 버렸다.
|
|
12
|
+
// 실제로 /level 검사가 사용자 설정을 바꾼 것을 보고 이걸 넣었다.
|
|
13
|
+
//
|
|
14
|
+
// 모듈을 읽을 때가 아니라 쓸 때마다 본다. 그래야 부르는 쪽에서 언제 정하든 먹는다.
|
|
15
|
+
function userDir() {
|
|
16
|
+
return process.env.DEEL_HOME ? resolve(process.env.DEEL_HOME) : join(homedir(), '.deel');
|
|
17
|
+
}
|
|
18
|
+
function projectDir() {
|
|
19
|
+
return join(process.cwd(), '.deel');
|
|
20
|
+
}
|
|
8
21
|
|
|
9
22
|
export function configPath() {
|
|
10
|
-
const local = join(
|
|
23
|
+
const local = join(projectDir(), 'config.json');
|
|
11
24
|
if (existsSync(local)) return local;
|
|
12
|
-
return join(
|
|
25
|
+
return join(userDir(), 'config.json');
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
const EMPTY = { version: 1, active: null, profiles: [] };
|
|
@@ -26,7 +39,7 @@ export function load() {
|
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
export function save(cfg, { toProject = false } = {}) {
|
|
29
|
-
const dir = toProject ?
|
|
42
|
+
const dir = toProject ? projectDir() : userDir();
|
|
30
43
|
const p = join(dir, 'config.json');
|
|
31
44
|
mkdirSync(dir, { recursive: true });
|
|
32
45
|
writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|