deel-local-cli 1.15.0 → 1.16.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.ko.md +70 -13
- package/README.md +71 -13
- package/bin/deel.js +45 -1
- package/package.json +1 -1
- package/src/acp/jsonrpc.js +23 -0
- package/src/acp/serve.js +62 -5
- package/src/agent/asks.js +58 -2
- package/src/agent/budget.js +33 -0
- package/src/agent/effort.js +69 -0
- package/src/agent/loop.js +10 -2
- package/src/agent/modes.js +179 -17
- package/src/agent/route.js +119 -8
- package/src/backend/adapter.js +56 -5
- package/src/cmdnames.js +1 -0
- package/src/commands.js +31 -13
- package/src/completion.js +1 -0
- package/src/i18n/en.js +13 -0
- package/src/i18n/ja.js +13 -0
- package/src/i18n/ko.js +13 -0
- package/src/i18n/zh.js +13 -0
- package/src/tools/doc2md.js +266 -0
- package/src/tools/docs.js +14 -4
- package/src/tools/fig.js +393 -0
- package/src/tools/index.js +49 -8
- package/src/tools/kiwi.js +293 -0
package/src/agent/route.js
CHANGED
|
@@ -31,6 +31,9 @@ const 표 = {
|
|
|
31
31
|
[/error/i, 3], [/\bexception\b|\bcrash|\bfail(s|ed|ing)?\b|\bbroken\b/i, 3],
|
|
32
32
|
[/고장/, 3], [/왜\s*이(래|러|렇게\s*나)/, 2],
|
|
33
33
|
[/왜\s*(못|안)/, 3], [/뭐\s*때문(이|에)/, 3],
|
|
34
|
+
// 원인을 짚으라는 말은 점검이 아니라 디버그다 — 증상이 이미 있다는 뜻이다.
|
|
35
|
+
// 이게 없으면 "오류 원인 분석해줘" 가 둘 사이에서 비겨 종합에 그대로 남는다.
|
|
36
|
+
[/원인(을|이|은)?\s*(찾|짚|파악|밝|알아|분석|살펴|봐)/, 4],
|
|
34
37
|
],
|
|
35
38
|
|
|
36
39
|
plan: [
|
|
@@ -56,9 +59,50 @@ const 표 = {
|
|
|
56
59
|
[/\bdesign\b|\bstructure\b|\brefactor(ing)?\s*(방향|plan)/i, 3],
|
|
57
60
|
],
|
|
58
61
|
|
|
62
|
+
/*
|
|
63
|
+
* 점검 — 「지금 무엇이 잘못돼 있나」 를 찾는 일.
|
|
64
|
+
*
|
|
65
|
+
* 이 갈래가 없던 동안 이런 말이 전부 묻기(ask)로 갔다. 「설명해줘」 한 마디가
|
|
66
|
+
* 5점이라 그것만으로 문턱을 넘기 때문이다. 그런데 묻기는 짧게 답하고, 얕게
|
|
67
|
+
* 생각하고(low), 열두 걸음에서 멈춘다 — 파일 열 개를 훑어야 하는 일에는
|
|
68
|
+
* 셋 다 틀린 값이다. 실제로 그렇게 돌아 본 턴이 있고, 같은 질문을 깊게 본
|
|
69
|
+
* 쪽과 견주면 답의 깊이가 눈에 띄게 달랐다.
|
|
70
|
+
*
|
|
71
|
+
* 여기 모은 낱말은 하나같이 **읽어야 답이 나오는** 말이다. 잘못 고르면
|
|
72
|
+
* 파일을 못 고쳐서 사람이 막히므로, 문턱은 읽기 전용답게 높인다.
|
|
73
|
+
*/
|
|
74
|
+
inspect: [
|
|
75
|
+
[/점검(해|좀|을|이)/, 5], [/분석(해|좀|을|이)/, 5],
|
|
76
|
+
[/검토(해|좀|를)/, 5], [/리뷰(해|좀)?/, 5],
|
|
77
|
+
// '감사합니다' 는 안 걸린다 — '합' 은 '해·하' 가 아니다.
|
|
78
|
+
[/감사(를)?\s*(해|하)/, 4], [/훑어\s*(봐|보고|줘)/, 4],
|
|
79
|
+
[/취약(점|한|성)/, 5], [/보안\s*(점검|검토|문제|취약|구멍)/, 5],
|
|
80
|
+
[/경합|레이스\s*컨디션|race\s*condition/i, 5],
|
|
81
|
+
[/데이터\s*(손실|유실)|덮어쓰(기|면)|유실/, 4],
|
|
82
|
+
[/동시\s*(편집|접근|요청|쓰기|성)/, 4],
|
|
83
|
+
[/충돌/, 3], [/불일치|어긋/, 3], [/누락(된|되는|이|을)/, 3],
|
|
84
|
+
[/위험(한|이|을|해)/, 3], [/결함/, 3],
|
|
85
|
+
[/가능성(이|을)?\s*있는/, 3],
|
|
86
|
+
[/문제(가|를|점)?\s*(있|찾|없|만)/, 3],
|
|
87
|
+
[/찾아\s*(줘|주세요|봐|보)/, 2],
|
|
88
|
+
[/\baudit\b|\breview\b|\banaly[sz]e\b|\binspect\b|\bvulnerab/i, 4],
|
|
89
|
+
[/\bdata\s*loss\b|\bdeadlock\b|\blost\s*update\b/i, 4],
|
|
90
|
+
],
|
|
91
|
+
|
|
59
92
|
ask: [
|
|
60
93
|
// 설명은 아무것도 안 바꾸는 모드라 잘못 고르면 답답해진다. 신호를 엄격히 본다.
|
|
61
94
|
[/뭐(야|예요|입니까|지)\s*\??$/m, 5], [/무엇(인가|입니까|이야)/, 5],
|
|
95
|
+
/*
|
|
96
|
+
* 「뭐 하는 파일이야?」 — 터미널에서 돌려 보다 걸린 자리다.
|
|
97
|
+
*
|
|
98
|
+
* 윗줄은 「뭐」 바로 뒤에 「야」 가 붙어야 걸린다. 그런데 사람이 실제로
|
|
99
|
+
* 치는 말은 「이거 뭐 하는 파일이야?」 처럼 **사이에 말이 낀다.** 그래서
|
|
100
|
+
* 제일 흔한 물음 하나가 통째로 안 걸렸고, 종합 모드에 그대로 남았다.
|
|
101
|
+
* 막히지는 않지만 물어본 것에 답만 하면 되는 턴이 파일을 고칠 수 있는
|
|
102
|
+
* 모드로 돌았다는 뜻이다.
|
|
103
|
+
*/
|
|
104
|
+
[/뭐\s*하는\s*(파일|함수|코드|모듈|클래스|녀석|건|거|것)/, 5],
|
|
105
|
+
[/뭐\s*하(는|나|니|냐)/, 3], [/무슨\s*(일|역할)/, 3],
|
|
62
106
|
[/설명(해|좀|을)/, 5], [/알려\s*(줘|주세요|줄래)/, 3],
|
|
63
107
|
[/어떻게\s*(동작|작동|돌아가)/, 5],
|
|
64
108
|
[/무슨\s*(뜻|의미)|의미(가|는)\s*뭐/, 5],
|
|
@@ -107,6 +151,7 @@ const 문턱 = {
|
|
|
107
151
|
orchestrator: 4,
|
|
108
152
|
plan: 5,
|
|
109
153
|
architect: 5,
|
|
154
|
+
inspect: 5,
|
|
110
155
|
ask: 5,
|
|
111
156
|
};
|
|
112
157
|
|
|
@@ -135,6 +180,45 @@ const 계획먼저 = new RegExp(`(${계획말})\\s*(?:을|를)?\\s*(?:${잇는
|
|
|
135
180
|
// 영어도 같이 본다 — "plan and then build it" 처럼 붙여 넣는 일이 있다.
|
|
136
181
|
const 영어겹침 = /\b(plan|design|outline|draft)\b[\s\S]{0,60}?\b(?:and|then|,)\s*(?:then\s*)?\b(build|implement|create|write|make|code)\b/i;
|
|
137
182
|
|
|
183
|
+
/*
|
|
184
|
+
* ── 두 말이 **얼마나 붙어 있어야** 겹침인가 ─────────────────────────────
|
|
185
|
+
*
|
|
186
|
+
* 예순 자다. 영어 쪽은 처음부터 `[\s\S]{0,60}?` 로 거리를 두고 봤는데
|
|
187
|
+
* 한국어 쪽만 「그 뒤 어디든」 이었다. 그 차이가 긴 글에서 그대로 터졌다.
|
|
188
|
+
*
|
|
189
|
+
* 실제로 겪은 것 — 1,300자짜리 지시문이었다. 「…배포 전 점검한다고 가정하고
|
|
190
|
+
* 전체적으로 검토해줘 … 실제로 개선하는 것이다 … 코드 수정만 수행해라 …
|
|
191
|
+
* 중간에 멈춰서 확인을 요청하지 말고 스스로 진행해라」.
|
|
192
|
+
*
|
|
193
|
+
* 여기서 800자 떨어진 두 조각 — 「검토해서」(9번째 문단)와 「추가하」(보고
|
|
194
|
+
* 항목 이름) — 이 짝지어져 겹침으로 잡혔다. 그래서 **계획 모드**로 갔다.
|
|
195
|
+
* 계획은 파일을 바꾸는 도구가 없는 모드이고, 그 지시문은 「고쳐라 · 테스트를
|
|
196
|
+
* 만들어라 · 멈추지 마라」 였다. 사람은 고쳐 달라고 했는데 계획서 한 장을
|
|
197
|
+
* 받고, 마지막에 「이대로 진행할까요?」 를 봤다 — 묻지 말라고 적어 둔 바로
|
|
198
|
+
* 그 자리에서.
|
|
199
|
+
*
|
|
200
|
+
* 이 규칙이 잡으려던 것은 「정리해서 만들어줘」 같은 **한 문장**이다. 한 문장
|
|
201
|
+
* 안에서 두 말은 예순 자를 안 넘는다. 그러니 거리를 재는 것이 규칙을 좁히는
|
|
202
|
+
* 것이 아니라 **원래 뜻대로 되돌리는 것**이다.
|
|
203
|
+
*/
|
|
204
|
+
const 겹침거리 = 60;
|
|
205
|
+
|
|
206
|
+
/*
|
|
207
|
+
* ── 「묻지 말고 끝까지」 라고 이미 적어 둔 사람 ─────────────────────────
|
|
208
|
+
*
|
|
209
|
+
* 겹침의 값은 「계획을 보여 주고 승인을 받는다」 다. 그런데 승인을 받지
|
|
210
|
+
* 말라고 **글로 적어 둔** 사람에게 그 값은 손해뿐이다. 계획 모드는 계획을
|
|
211
|
+
* 내고 멈추므로, 그 사람은 시킨 일의 절반도 못 받는다.
|
|
212
|
+
*
|
|
213
|
+
* 이건 짐작이 아니라 사람이 직접 쓴 말이다. 쓴 말이 규칙을 이긴다.
|
|
214
|
+
*/
|
|
215
|
+
const 멈추지마 = /(?:중간에\s*)?(?:멈추지|묻지|물어보지|확인\s*요청하지|되묻지)\s*(?:는)?\s*(?:말|마)|끝까지\s*(?:수행|진행|해)|스스로\s*진행|알아서\s*(?:진행|해)|승인\s*없이|\bwithout\s+(?:asking|stopping|confirmation)\b|\bdon'?t\s+(?:stop|ask)\b/i;
|
|
216
|
+
|
|
217
|
+
/** 사람이 「묻지 말고 끝까지 하라」 고 적어 뒀나. */
|
|
218
|
+
export function 묻지말라했나(text) {
|
|
219
|
+
return 멈추지마.test(String(text ?? ''));
|
|
220
|
+
}
|
|
221
|
+
|
|
138
222
|
/**
|
|
139
223
|
* 계획과 실행이 한 말에 같이 들었나.
|
|
140
224
|
*
|
|
@@ -142,14 +226,23 @@ const 영어겹침 = /\b(plan|design|outline|draft)\b[\s\S]{0,60}?\b(?:and|then|
|
|
|
142
226
|
*/
|
|
143
227
|
export function 겹친요청(text) {
|
|
144
228
|
const s = String(text ?? '');
|
|
229
|
+
// 묻지 말라고 적어 둔 사람에게 승인 창을 띄우지 않는다. 위 머리말 참고.
|
|
230
|
+
if (묻지말라했나(s)) return { 겹침: false, why: '' };
|
|
231
|
+
|
|
145
232
|
const 영어 = s.match(영어겹침);
|
|
146
233
|
if (영어) return { 겹침: true, why: `'${영어[1]}' 과 '${영어[2]}'` };
|
|
147
234
|
|
|
148
235
|
const 앞 = s.match(계획먼저);
|
|
149
236
|
if (!앞) return { 겹침: false, why: '' };
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
237
|
+
/*
|
|
238
|
+
* 실행말이 계획말 **바로 뒤에** 와야 한다.
|
|
239
|
+
*
|
|
240
|
+
* 「뒤에」 였지 「글 어디든」 이 아니었다 — 거리를 안 재면 긴 지시문에서는
|
|
241
|
+
* 언제나 짝이 하나쯤 걸린다(위 겹침거리 머리말). "만들고 나서 정리해줘" 가
|
|
242
|
+
* 안 걸리는 것도 그대로다. 그건 만드는 일이 먼저고, 미리 볼 계획이 없다.
|
|
243
|
+
*/
|
|
244
|
+
const 남은것 = s.slice(앞.index + 앞[0].length, 앞.index + 앞[0].length + 겹침거리);
|
|
245
|
+
const 뒤 = 남은것.match(실행말);
|
|
153
246
|
if (!뒤) return { 겹침: false, why: '' };
|
|
154
247
|
return { 겹침: true, why: `'${앞[0].trim()}' 뒤에 '${뒤[0].trim()}'` };
|
|
155
248
|
}
|
|
@@ -173,13 +266,31 @@ export function 겹친요청(text) {
|
|
|
173
266
|
* 하나는 읽고 말하라는 말이고 하나는 고치라는 말이다. 그래서 어미가 아니라
|
|
174
267
|
* **동사 목록**으로 본다. 목록에 없으면 손대라는 말이 아니다.
|
|
175
268
|
*/
|
|
176
|
-
const 손대는동사 = '
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
269
|
+
const 손대는동사 = '만들|고쳐|고치|바꿔|바꾸|추가|넣어|지워|삭제|옮겨|나눠|나누|합쳐|정리|개선|구현|작성|적용|설치|배포|리팩터링|리팩터|분리|통일|세팅|갈아|손봐|손보|수정';
|
|
270
|
+
/*
|
|
271
|
+
* 동사 바로 뒤에 시킴꼴이 붙어야 한다. 빈칸을 안 넘으므로 "나누는 설계만 봐줘"
|
|
272
|
+
* 는 안 걸린다 — 그건 나누라는 말이 아니라 나누는 안을 보자는 말이다.
|
|
273
|
+
*
|
|
274
|
+
* ── 해라체가 통째로 빠져 있었다 ─────────────────────────────────────────
|
|
275
|
+
*
|
|
276
|
+
* 끝맺음 목록이 전부 **부탁하는 말**이었다(줘 · 주세요 · 해라 · 할래). 그래서
|
|
277
|
+
* 시키는 말투로 쓴 사람은 이 검사에 한 번도 안 걸렸다 —
|
|
278
|
+
*
|
|
279
|
+
* 「재현 검사를 먼저 만들어라 … 최소 범위의 코드 수정만 수행해라
|
|
280
|
+
* … 중간에 멈추지 말고 스스로 진행해라」
|
|
281
|
+
*
|
|
282
|
+
* 이 지시문이 「고치라는 말이 아니다」 로 읽혀서 읽기 전용 모드로 갔다.
|
|
283
|
+
* 사람은 고쳐 달라고 세 번 적었는데 아무것도 안 바뀐 화면을 받는다.
|
|
284
|
+
*
|
|
285
|
+
* 「어라 · 아라」 와 맨 「라」 가 그 문을 연다. 맨 라 는 낱말 끝일 때만 본다 —
|
|
286
|
+
* 「지워라」 는 지우라는 말이지만 「바꾸라는 말」 은 아니다. 뒤에 무엇이
|
|
287
|
+
* 붙어 있나로 그 둘이 갈린다.
|
|
288
|
+
* 「만들어라」 는 언제나 만들라는 말이다.
|
|
289
|
+
*/
|
|
290
|
+
const 손대라는말 = new RegExp(`(${손대는동사})\\S{0,3}(줘|주세요|주라|해라|하라|하자|해다오|해줄래|할래|어라|아라|여라|라(?=[\s.!?~,]|$))`);
|
|
180
291
|
|
|
181
292
|
// 파일을 못 고치는 모드. modes.js 에서 쓰기 도구를 안 주는 것들과 같아야 한다.
|
|
182
|
-
const 읽기만하는모드 = new Set(['architect', 'plan', 'ask']);
|
|
293
|
+
const 읽기만하는모드 = new Set(['architect', 'plan', 'ask', 'inspect']);
|
|
183
294
|
|
|
184
295
|
/** 파일을 손대라는 말이 들었나. */
|
|
185
296
|
export function 손대라했나(text) {
|
package/src/backend/adapter.js
CHANGED
|
@@ -26,9 +26,53 @@ export function endpoint(shape) {
|
|
|
26
26
|
return '/chat/completions';
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/*
|
|
30
|
+
* ── 이 대화가 한 덩어리라고 **머리에** 적는다 ───────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* 몸통에도 같은 것을 적는 자리가 있다(buildBody 의 세션이름). 그런데 그 자리는
|
|
33
|
+
* 규격이 받는 칸이 있을 때만 쓴다 — OpenAI 직통은 `user`, Anthropic 은
|
|
34
|
+
* `metadata.user_id`. 그 둘이 아닌 곳, 즉 **게이트웨이 뒤**에는 아무것도 안
|
|
35
|
+
* 나갔다. 문서에 없는 칸을 몸통에 실으면 400 이고, 그 400 은 화면에서 열쇠가
|
|
36
|
+
* 틀린 것과 구별이 안 되기 때문이다(backend/wire.js 의 세션자리).
|
|
37
|
+
*
|
|
38
|
+
* 그래서 몸통이 아니라 **머리**에 적는다. 머리는 모르면 그냥 무시된다 —
|
|
39
|
+
* 400 이 날 자리가 아니다. 그러니 창구를 가릴 이유도 없고, 「이 창구만 예외」
|
|
40
|
+
* 라는 갈래가 하나도 안 는다.
|
|
41
|
+
*
|
|
42
|
+
* ── 이게 없으면 무슨 일이 나나 ──────────────────────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* 게이트웨이는 이름이 없는 요청마다 **새 대화를 하나씩 연다.** 대시보드에
|
|
45
|
+
* 한 대화가 아홉 줄로 흩어지는 것은 눈에 보이는 쪽이고, 안 보이는 쪽이 훨씬
|
|
46
|
+
* 비싸다 — 모델 하나 뒤에 창구가 여럿이면(지역이 다른 Bedrock 프로파일 같은
|
|
47
|
+
* 것) 요청마다 다른 창구로 갈린다. 프롬프트 캐시는 **창구마다 따로** 있으므로,
|
|
48
|
+
* 갈리는 순간 앞머리가 통째로 다시 나간다. 캐시가 90% 를 넘다가 갑자기 20%
|
|
49
|
+
* 로 떨어지는 것이 그 모양이다.
|
|
50
|
+
*
|
|
51
|
+
* 이름이 있으면 게이트웨이가 그 대화를 첫 요청이 닿은 창구에 붙여 둔다
|
|
52
|
+
* (LiteLLM 의 session affinity). 그러면 캐시가 한자리에 쌓인다.
|
|
53
|
+
*
|
|
54
|
+
* ── 왜 이름을 둘로 적나 ─────────────────────────────────────────────────
|
|
55
|
+
*
|
|
56
|
+
* x-litellm-session-id 문서에 이름이 그대로 적힌 칸이다.
|
|
57
|
+
* x-deel-session-id `x-<우리이름>-session-id` 라는 **일반 규칙**으로도
|
|
58
|
+
* 읽어 주는 게이트웨이가 있다. 우리 이름으로 적어 두면
|
|
59
|
+
* 남의 도구 이름을 사칭하지 않고도 그 규칙에 걸린다.
|
|
60
|
+
*
|
|
61
|
+
* 값은 같다. 둘 다 대화 번호 하나뿐이고 경로·주소·열쇠는 안 들어간다
|
|
62
|
+
* (backend/wire.js 의 세션이름짓기).
|
|
63
|
+
*/
|
|
64
|
+
function 세션머리(세션이름) {
|
|
65
|
+
const 이름 = String(세션이름 ?? '').trim();
|
|
66
|
+
if (!이름) return {};
|
|
67
|
+
return { 'x-litellm-session-id': 이름, 'x-deel-session-id': 이름 };
|
|
68
|
+
}
|
|
69
|
+
|
|
29
70
|
/** 이 규격이 더 요구하는 머리. 없으면 빈 것. */
|
|
30
|
-
export function 더할머리(shape) {
|
|
31
|
-
return
|
|
71
|
+
export function 더할머리(shape, 세션이름 = null) {
|
|
72
|
+
return {
|
|
73
|
+
...(shape === 'anthropic' ? { 'anthropic-version': ANTHROPIC_VERSION } : {}),
|
|
74
|
+
...세션머리(세션이름),
|
|
75
|
+
};
|
|
32
76
|
}
|
|
33
77
|
|
|
34
78
|
/**
|
|
@@ -856,11 +900,18 @@ export function toolMessage(shape, { callId, name, content }) {
|
|
|
856
900
|
* 안 붙는다. 못 받았다는 것은 부르는 쪽이 onAuth 로 듣고 화면에 적는다.
|
|
857
901
|
*/
|
|
858
902
|
async function 머리말짓기(conn, opts, { 다시 = false } = {}) {
|
|
903
|
+
/*
|
|
904
|
+
* 대화 이름은 **세 갈래가 다 지나는 자리**에서 한 번만 얹는다.
|
|
905
|
+
*
|
|
906
|
+
* 갈래마다 손으로 적게 두면 언젠가 한 곳이 빠지고, 그 한 곳만 새 대화로
|
|
907
|
+
* 잡힌다 — 하필 열쇠를 새로 받은 뒤의 요청이 그 자리다.
|
|
908
|
+
*/
|
|
909
|
+
const 이름 = opts.세션이름 ?? conn.세션이름 ?? null;
|
|
859
910
|
const 설정 = conn.열쇠받기 ?? null;
|
|
860
911
|
const 판단 = 쓸수있나(설정, { auth: conn.auth });
|
|
861
912
|
if (!설정 || !판단.된다) {
|
|
862
913
|
if (설정 && 판단.왜) opts.onAuth?.({ ok: false, 왜: 판단.왜, 안부름: true });
|
|
863
|
-
return headersFor(conn.auth, conn.key ?? '', 더할머리(conn.kind));
|
|
914
|
+
return headersFor(conn.auth, conn.key ?? '', 더할머리(conn.kind, 이름));
|
|
864
915
|
}
|
|
865
916
|
const r = await 열쇠받아오기(설정, {
|
|
866
917
|
다시, signal: opts.signal ?? null,
|
|
@@ -869,10 +920,10 @@ async function 머리말짓기(conn, opts, { 다시 = false } = {}) {
|
|
|
869
920
|
});
|
|
870
921
|
if (!r.ok) {
|
|
871
922
|
opts.onAuth?.({ ...r, ok: false });
|
|
872
|
-
return headersFor(conn.auth, conn.key ?? '', 더할머리(conn.kind));
|
|
923
|
+
return headersFor(conn.auth, conn.key ?? '', 더할머리(conn.kind, 이름));
|
|
873
924
|
}
|
|
874
925
|
if (!r.그대로) opts.onAuth?.({ ok: true, 만료: r.만료, ms: r.ms });
|
|
875
|
-
return headersFor(conn.auth, r.token, { ...더할머리(conn.kind), ...r.headers });
|
|
926
|
+
return headersFor(conn.auth, r.token, { ...더할머리(conn.kind, 이름), ...r.headers });
|
|
876
927
|
}
|
|
877
928
|
|
|
878
929
|
/*
|
package/src/cmdnames.js
CHANGED
package/src/commands.js
CHANGED
|
@@ -16,6 +16,7 @@ import { load, save, 저장시도, resolveKey, upsert, 열쇠보관, configPath
|
|
|
16
16
|
import { 지금상태 as 지금열쇠상태, 받기설정, 잊기 as 받은열쇠잊기 } from './safety/authcmd.js';
|
|
17
17
|
// 열쇠받기 명령을 정책이 못박아 뒀을 수 있다 — 받기설정 이 그 값을 같이 본다.
|
|
18
18
|
import { 정책읽기 } from './safety/policy.js';
|
|
19
|
+
import { 걸음수 } from './agent/budget.js';
|
|
19
20
|
import { 제공자고르기 } from './providers/index.js';
|
|
20
21
|
import { 종, 알릴만한초 } from './ui/notify.js';
|
|
21
22
|
/*
|
|
@@ -1057,7 +1058,7 @@ export async function handle(line, session, ctx) {
|
|
|
1057
1058
|
case 'work':
|
|
1058
1059
|
case 'auto':
|
|
1059
1060
|
case 'code': case 'plan': case 'architect':
|
|
1060
|
-
case 'debug': case 'ask': case 'orchestrator': {
|
|
1061
|
+
case 'debug': case 'ask': case 'inspect': case 'orchestrator': {
|
|
1061
1062
|
// 인자 없이 /work 만 치면 지금 모드와 고를 수 있는 것을 보여 준다.
|
|
1062
1063
|
if (name === 'work' && !arg.trim()) { showWork(session); return { handled: true }; }
|
|
1063
1064
|
|
|
@@ -1080,13 +1081,15 @@ export async function handle(line, session, ctx) {
|
|
|
1080
1081
|
session.routed = null;
|
|
1081
1082
|
const w = getWork(골라진);
|
|
1082
1083
|
say('');
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
say(` ${c.
|
|
1084
|
+
// 영어 화면에서 이름과 영문 이름은 같은 글자다 — 「Inspect (Inspect)」 를 안 만든다.
|
|
1085
|
+
const 곁이름 = 언어() === 'en' ? '' : ` ${c.gray(`(${w.en})`)}`;
|
|
1086
|
+
say(` ${c.hcyan(w.glyph)} ${c.bold(보일이름(w.id))}${곁이름} ${c.gray(보일한줄(w.id))}`);
|
|
1087
|
+
say(` ${c.gray(pad(말('work.tools'), 6))} ${canWrite(골라진) ? c.yellow(말('work.toolsWrite')) : c.green(말('work.toolsRead'))}`);
|
|
1088
|
+
say(` ${c.gray(pad(말('work.think'), 6))} ${c.white(w.think ?? session.think)}${c.gray('·')}${c.magenta(w.effort)} ${c.gray(말('work.steps', { n: 걸음수(w.id, session.conn?.ctx) }))}`);
|
|
1086
1089
|
if (골라진 === 'auto') {
|
|
1087
|
-
say(` ${c.gray('
|
|
1090
|
+
say(` ${c.gray(말('work.autoOn'))}`);
|
|
1088
1091
|
} else {
|
|
1089
|
-
say(` ${c.gray('
|
|
1092
|
+
say(` ${c.gray(말('work.pinned'))} ${c.cyan(`/work ${보일이름('auto')}`)}`);
|
|
1090
1093
|
}
|
|
1091
1094
|
say('');
|
|
1092
1095
|
return { handled: true };
|
|
@@ -3145,18 +3148,33 @@ const INIT_TEMPLATE = `# DEEL.md
|
|
|
3145
3148
|
|
|
3146
3149
|
// /work 를 인자 없이 부르면 지금 모드와 고를 수 있는 것들을 보여 준다.
|
|
3147
3150
|
function showWork(session) {
|
|
3148
|
-
rule('
|
|
3151
|
+
rule(말('work.title'), 70);
|
|
3152
|
+
/*
|
|
3153
|
+
* 이름 칸을 **제일 긴 이름에 맞춰** 잡는다.
|
|
3154
|
+
*
|
|
3155
|
+
* 8칸으로 못 박아 뒀더니 Orchestrator(12자)가 칸을 넘어 다음 칸을 밀었다.
|
|
3156
|
+
* 화면에 `OrchestratorOrchestrator` 라고 붙어 나왔다 — 사진을 찍어 보고서야
|
|
3157
|
+
* 알았다. 목록의 값은 칸이 맞아야 목록이다.
|
|
3158
|
+
*/
|
|
3159
|
+
const 이름들 = WORK_ORDER.map((k) => 보일이름(WORK_MODES[k].id));
|
|
3160
|
+
const 칸너비 = Math.max(...이름들.map((n) => width(n))) + 2;
|
|
3161
|
+
// 영어 화면에서는 이름과 영문 이름이 같은 글자다. 같은 것을 두 번 적지 않는다.
|
|
3162
|
+
const 영문칸 = 언어() === 'en' ? 0 : 14;
|
|
3163
|
+
|
|
3149
3164
|
for (const k of WORK_ORDER) {
|
|
3150
3165
|
const w = WORK_MODES[k];
|
|
3151
3166
|
const 지금 = k === (normWork(session.work) ?? WORK_DEFAULT);
|
|
3152
3167
|
const 표 = 지금 ? c.hgreen('●') : c.gray('·');
|
|
3153
|
-
const 보임 = 보일이름(w.id);
|
|
3154
|
-
const 이름 = 지금 ? c.bold(c.white(
|
|
3155
|
-
say(` ${표} ${c.hcyan(w.glyph)} ${이름}${c.gray(pad(w.en,
|
|
3156
|
-
|
|
3168
|
+
const 보임 = pad(보일이름(w.id), 칸너비);
|
|
3169
|
+
const 이름 = 지금 ? c.bold(c.white(보임)) : c.gray(보임);
|
|
3170
|
+
say(` ${표} ${c.hcyan(w.glyph)} ${이름}${영문칸 ? c.gray(pad(w.en, 영문칸)) : ''}${c.gray(보일한줄(w.id))}`);
|
|
3171
|
+
const 강도 = `${w.think ?? '—'}·${w.effort}`;
|
|
3172
|
+
const 걸음 = 말('work.steps', { n: 걸음수(k, session.conn?.ctx) });
|
|
3173
|
+
say(` ${canWrite(k) ? c.gray(말('work.canEdit')) : c.green(말('work.readOnly'))}`
|
|
3174
|
+
+ `${c.gray(` · ${말('work.think')} ${강도} · ${걸음}`)}`);
|
|
3157
3175
|
}
|
|
3158
3176
|
say('');
|
|
3159
|
-
say(` ${c.gray('
|
|
3160
|
-
say(` ${c.gray('
|
|
3177
|
+
say(` ${c.gray(말('work.howTo', { a: c.cyan('/plan'), b: c.cyan('/code'), c: c.cyan('/debug'), 키: c.cyan('Ctrl+O') }))}`);
|
|
3178
|
+
say(` ${c.gray(말('work.axis', { 명령: c.cyan('/mode') }))}`);
|
|
3161
3179
|
say('');
|
|
3162
3180
|
}
|
package/src/completion.js
CHANGED
|
@@ -39,6 +39,7 @@ export const 명령들 = [
|
|
|
39
39
|
{ 이름: 'doctor', 뜻: 'diagnose 와 같다', en: 'same as diagnose' },
|
|
40
40
|
{ 이름: 'pack', 뜻: '오프라인 반입용으로 묶는다', en: 'bundle for offline transfer' },
|
|
41
41
|
{ 이름: 'audit', 뜻: '무엇을 했는지 기록을 본다', en: 'show the record of what it did' },
|
|
42
|
+
{ 이름: 'doc2md', 뜻: '문서·시안을 마크다운으로 (hwpx·docx·pptx·xlsx·pdf·fig)', en: 'document or design to Markdown (hwpx/docx/pptx/xlsx/pdf/fig)' },
|
|
42
43
|
{ 이름: 'sbom', 뜻: '무엇이 들었는지 목록을 낸다', en: 'list what is inside (SBOM)' },
|
|
43
44
|
{ 이름: 'scan', 뜻: '게이트웨이에 어떤 모델이 있나 훑는다', en: 'scan the gateway for available models' },
|
|
44
45
|
{ 이름: 'sessions', 뜻: '지난 대화 목록', en: 'list past conversations' },
|
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]',
|
|
@@ -314,6 +315,18 @@ export const en = {
|
|
|
314
315
|
'approve.strict.name': 'ask always',
|
|
315
316
|
'approve.strict.short': 'always',
|
|
316
317
|
'approve.strict.line': 'asks before every file edit and every command',
|
|
318
|
+
'work.title': 'Work modes',
|
|
319
|
+
'work.canEdit': 'edits files',
|
|
320
|
+
'work.readOnly': 'read-only',
|
|
321
|
+
'work.think': 'thinking',
|
|
322
|
+
'work.steps': 'up to {n} steps',
|
|
323
|
+
'work.tools': 'tools',
|
|
324
|
+
'work.toolsWrite': 'read + change files',
|
|
325
|
+
'work.toolsRead': 'read only — cannot change files',
|
|
326
|
+
'work.howTo': 'To switch: {a} {b} {c} … or {키} to cycle',
|
|
327
|
+
'work.axis': 'A different axis from the approval policy ({명령}) — what you do / how much it asks.',
|
|
328
|
+
'work.autoOn': 'From here it moves to the right mode by itself, message by message.',
|
|
329
|
+
'work.pinned': 'You chose this, so it will not change by itself. To hand it back:',
|
|
317
330
|
'level.beginner.name': 'beginner',
|
|
318
331
|
'level.beginner.hint': 'start on the recommended settings',
|
|
319
332
|
'level.developer.name': 'developer',
|
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': '[ターン数]',
|
|
@@ -281,6 +282,18 @@ export const ja = {
|
|
|
281
282
|
'approve.strict.name': '毎回訊く',
|
|
282
283
|
'approve.strict.short': '毎回',
|
|
283
284
|
'approve.strict.line': 'ファイルを直すたび、コマンドを打つたびに訊きます',
|
|
285
|
+
'work.title': '作業モード',
|
|
286
|
+
'work.canEdit': 'ファイルを変更',
|
|
287
|
+
'work.readOnly': '読むだけ',
|
|
288
|
+
'work.think': '思考',
|
|
289
|
+
'work.steps': '最大 {n} ステップ',
|
|
290
|
+
'work.tools': 'ツール',
|
|
291
|
+
'work.toolsWrite': '読み取り + ファイル変更',
|
|
292
|
+
'work.toolsRead': '読み取りのみ — ファイルは変更できません',
|
|
293
|
+
'work.howTo': '切り替え: {a} {b} {c} … または {키} で順に',
|
|
294
|
+
'work.axis': '承認ポリシー({명령})とは別の軸です — 何をするか / どれだけ聞くか。',
|
|
295
|
+
'work.autoOn': 'これからは一言ごとに適したモードへ自動で移ります。',
|
|
296
|
+
'work.pinned': 'ご自分で選んだので自動では変わりません。任せ直すには',
|
|
284
297
|
'level.beginner.name': 'かんたん',
|
|
285
298
|
'level.beginner.hint': '勧められた設定のまま始めます',
|
|
286
299
|
'level.developer.name': '開発者',
|
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': '[턴수]',
|
|
@@ -303,6 +304,18 @@ export const ko = {
|
|
|
303
304
|
'approve.strict.name': '모두 확인',
|
|
304
305
|
'approve.strict.short': '모두',
|
|
305
306
|
'approve.strict.line': '파일을 고치거나 명령을 돌리기 전에 매번 물어봅니다',
|
|
307
|
+
'work.title': '작업 모드',
|
|
308
|
+
'work.canEdit': '파일 바꿈',
|
|
309
|
+
'work.readOnly': '읽기만',
|
|
310
|
+
'work.think': '생각',
|
|
311
|
+
'work.steps': '최대 {n}걸음',
|
|
312
|
+
'work.tools': '도구',
|
|
313
|
+
'work.toolsWrite': '읽기 + 파일 바꾸기',
|
|
314
|
+
'work.toolsRead': '읽기만 — 파일을 못 바꿉니다',
|
|
315
|
+
'work.howTo': '바꾸려면 {a} {b} {c} … 또는 {키} 로 차례로',
|
|
316
|
+
'work.axis': '이건 승인 정책({명령})과 다른 축입니다 — 무엇을 하느냐 / 얼마나 물어보냐.',
|
|
317
|
+
'work.autoOn': '앞으로는 한마디마다 알맞은 모드로 저절로 옮겨 갑니다.',
|
|
318
|
+
'work.pinned': '직접 고르셨으므로 저절로 바뀌지 않습니다. 다시 맡기려면',
|
|
306
319
|
'level.beginner.name': '쉬움',
|
|
307
320
|
'level.beginner.hint': '권장값으로 바로 시작',
|
|
308
321
|
'level.developer.name': '개발자',
|
package/src/i18n/zh.js
CHANGED
|
@@ -57,6 +57,7 @@ export const zh = {
|
|
|
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': '[轮数]',
|
|
@@ -281,6 +282,18 @@ export const zh = {
|
|
|
281
282
|
'approve.strict.name': '每次都问',
|
|
282
283
|
'approve.strict.short': '每次',
|
|
283
284
|
'approve.strict.line': '每改一个文件、每跑一条命令都问',
|
|
285
|
+
'work.title': '工作模式',
|
|
286
|
+
'work.canEdit': '会改文件',
|
|
287
|
+
'work.readOnly': '只读',
|
|
288
|
+
'work.think': '思考',
|
|
289
|
+
'work.steps': '最多 {n} 步',
|
|
290
|
+
'work.tools': '工具',
|
|
291
|
+
'work.toolsWrite': '读取 + 修改文件',
|
|
292
|
+
'work.toolsRead': '只读 — 不能修改文件',
|
|
293
|
+
'work.howTo': '切换:{a} {b} {c} … 或用 {키} 依次切换',
|
|
294
|
+
'work.axis': '这与审批策略({명령})是两条不同的轴 — 做什么 / 问多少。',
|
|
295
|
+
'work.autoOn': '之后会按每句话自动切到合适的模式。',
|
|
296
|
+
'work.pinned': '你自己选的,不会自动改变。要交回自动请用',
|
|
284
297
|
'level.beginner.name': '新手',
|
|
285
298
|
'level.beginner.hint': '按推荐设置开始',
|
|
286
299
|
'level.developer.name': '开发者',
|