deel-local-cli 1.5.8 → 1.6.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 +23 -13
- package/README.md +22 -12
- package/bin/deel.js +10 -0
- package/package.json +2 -2
- package/src/acp/map.js +143 -0
- package/src/acp/serve.js +160 -13
- package/src/agent/commit.js +511 -0
- package/src/agent/compact.js +269 -226
- package/src/agent/loop.js +154 -12
- package/src/agent/mention.js +56 -10
- package/src/agent/review.js +192 -0
- package/src/agent/session.js +20 -2
- package/src/agent/threads.js +1 -1
- package/src/backend/adapter.js +342 -280
- package/src/backend/azure.js +151 -0
- package/src/backend/ctxsize.js +19 -0
- package/src/backend/detect.js +106 -0
- package/src/backend/http.js +350 -30
- package/src/backend/probe.js +48 -1
- package/src/backend/proxy.js +151 -0
- package/src/backend/quota.js +133 -0
- package/src/backend/retry.js +132 -0
- package/src/backend/vision.js +185 -0
- package/src/commands.js +341 -8
- package/src/completion.js +264 -0
- package/src/config.js +117 -1
- package/src/i18n/en.js +13 -1
- package/src/i18n/index.js +22 -3
- package/src/i18n/ja.js +266 -0
- package/src/i18n/ko.js +12 -0
- package/src/i18n/zh.js +266 -0
- package/src/oneshot.js +28 -3
- package/src/pack/sbom.js +25 -0
- package/src/pack/selfpack.js +9 -3
- package/src/plugins/manage.js +34 -7
- package/src/repl.js +78 -6
- package/src/report.js +26 -2
- package/src/safety/guard.js +13 -2
- package/src/safety/keystore.js +237 -0
- package/src/safety/network.js +76 -14
- package/src/safety/policy.js +209 -0
- package/src/safety/secrets.js +32 -0
- package/src/setup.js +40 -7
- package/src/tools/clipboard.js +178 -0
- package/src/tools/fastgrep.js +217 -0
- package/src/tools/fsutil.js +55 -12
- package/src/tools/ignore.js +192 -0
- package/src/tools/index.js +226 -36
- package/src/tools/jobs.js +4 -12
- package/src/tools/outline.js +3 -1
- package/src/tools/pdf.js +1326 -0
- package/src/tools/shell.js +123 -0
- package/src/tools/verify.js +20 -4
- package/src/tools/webfetch.js +18 -5
- package/src/ui/inputbox.js +2 -2
- package/src/ui/screen.js +4 -1
- package/src/ui/status.js +22 -0
package/src/commands.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
// 슬래시 명령. 이름은 Claude Code / Codex 관례에 맞춘다.
|
|
2
2
|
import { writeFileSync, existsSync, statSync } from 'node:fs';
|
|
3
|
+
import { 볼것, 리뷰받기 } from './agent/review.js';
|
|
4
|
+
import { 마지막할당량, 할당량말, 아슬아슬한가 } from './backend/quota.js';
|
|
3
5
|
import { join } from 'node:path';
|
|
4
6
|
import { c, say, rule, pad, bar, mark, width, clip } from './ui/ansi.js';
|
|
5
7
|
import { compact } from './agent/compact.js';
|
|
6
8
|
import { 증거모으기, 증거적기 } from './agent/evidence.js';
|
|
9
|
+
import { 커밋준비, 커밋실행 } from './agent/commit.js';
|
|
7
10
|
import { allowEndpoint } from './safety/network.js';
|
|
8
|
-
import { pick } from './ui/prompt.js';
|
|
9
|
-
import { load, save, resolveKey, upsert } from './config.js';
|
|
11
|
+
import { pick, confirm } from './ui/prompt.js';
|
|
12
|
+
import { load, save, resolveKey, upsert, 열쇠보관 } from './config.js';
|
|
10
13
|
import { 종, 알릴만한초 } from './ui/notify.js';
|
|
11
|
-
import { 말, 언어, 언어정하기, 언어고르기, 옮긴만큼, 지시말, 지시말정하기, 지시말따로정했나 } from './i18n/index.js';
|
|
14
|
+
import { 말, 언어, 언어들, 언어정하기, 언어고르기, 옮긴만큼, 지시말, 지시말정하기, 지시말따로정했나 } from './i18n/index.js';
|
|
12
15
|
import { 프로필찾기, 쓸수있나, 연결만들기, 알릴말, 목록보기 } from './agent/models.js';
|
|
13
16
|
import { allowTemporarily } from './safety/network.js';
|
|
14
17
|
import { chat } from './backend/adapter.js';
|
|
18
|
+
import { 알림채움 } from './backend/retry.js';
|
|
19
|
+
import { 프록시고르기, 프록시설정 } from './backend/proxy.js';
|
|
20
|
+
import { 정한셸 } from './tools/shell.js';
|
|
15
21
|
import { TOOLS } from './tools/index.js';
|
|
16
22
|
import { 둘러보기, 프로젝트갈래 } from './lsp/servers.js';
|
|
17
23
|
import { 지금것들 } from './lsp/client.js';
|
|
@@ -48,6 +54,8 @@ import { LEVELS, ORDER as LEVEL_ORDER, DEFAULT as LEVEL_DEFAULT, normalize as no
|
|
|
48
54
|
import { diffLines, renderDiff, shortStat } from './ui/diff.js';
|
|
49
55
|
import { readTextFull } from './tools/fsutil.js';
|
|
50
56
|
import { 띄우기, 브라우저로 } from './preview/serve.js';
|
|
57
|
+
import { 클립보드그림, 그림앉히기 } from './tools/clipboard.js';
|
|
58
|
+
import { 크기말 } from './backend/vision.js';
|
|
51
59
|
const MODES = {
|
|
52
60
|
auto: '자율 — 전부 알아서. 되돌리기가 안전망',
|
|
53
61
|
confirm: '확인 — 되돌릴 수 없는 것만 물어봄',
|
|
@@ -104,6 +112,9 @@ const 명령들 = {
|
|
|
104
112
|
learned: { arg: true },
|
|
105
113
|
pin: { arg: true },
|
|
106
114
|
evidence: { arg: true },
|
|
115
|
+
commit: { arg: true },
|
|
116
|
+
review: { arg: false },
|
|
117
|
+
paste: { arg: false },
|
|
107
118
|
sessions: { arg: false },
|
|
108
119
|
recall: { arg: true },
|
|
109
120
|
mcp: { arg: false },
|
|
@@ -155,6 +166,21 @@ function 경로처럼보이나(line) {
|
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
// 반환: { handled, exit? } handled=false 면 모델에게 보낸다.
|
|
169
|
+
/** /mode 아래에 규칙을 늘어놓는다. 아무것도 안 걸려 있으면 아무 말도 안 한다. */
|
|
170
|
+
function 규칙보이기(규칙들) {
|
|
171
|
+
if (!규칙들) return;
|
|
172
|
+
const 있나 = (규칙들.allow?.length ?? 0) + (규칙들.deny?.length ?? 0);
|
|
173
|
+
if (!있나 && !규칙들.정책곳 && !규칙들.탈) return;
|
|
174
|
+
say('');
|
|
175
|
+
say(` ${c.gray('적어 둔 규칙 — 이건 모드보다 셉니다')}`);
|
|
176
|
+
for (const r of 규칙들.deny ?? []) say(` ${c.red('✗')} ${c.white(r.원문)} ${c.gray(r.출처)}`);
|
|
177
|
+
for (const r of 규칙들.allow ?? []) say(` ${c.green('✓')} ${c.white(r.원문)} ${c.gray(r.출처)}`);
|
|
178
|
+
if (규칙들.정책곳) say(` ${c.gray(`관리 정책 ${규칙들.정책곳} — 이 파일은 고칠 수 없습니다`)}`);
|
|
179
|
+
if (규칙들.baseUrl) say(` ${c.gray(`주소가 정책으로 못박혀 있습니다: ${규칙들.baseUrl}`)}`);
|
|
180
|
+
if (규칙들.offline) say(` ${c.gray('정책으로 오프라인이 켜져 있습니다 — 끌 수 없습니다')}`);
|
|
181
|
+
if (규칙들.탈) say(` ${mark.warn} ${c.yellow(규칙들.탈)}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
158
184
|
export async function handle(line, session, ctx) {
|
|
159
185
|
if (!line.startsWith('/')) return { handled: false };
|
|
160
186
|
if (경로처럼보이나(line)) return { handled: false };
|
|
@@ -256,7 +282,7 @@ export async function handle(line, session, ctx) {
|
|
|
256
282
|
|
|
257
283
|
// 그 한 번 동안만 연다. 끝나면 반드시 닫는다.
|
|
258
284
|
const 닫기 = 알림.다른자리 ? allowTemporarily(새conn.base) : null;
|
|
259
|
-
|
|
285
|
+
let 돌림 = spin(말('consult.asking', { 모델: 새conn.model }));
|
|
260
286
|
let 답 = null;
|
|
261
287
|
let 탈 = null;
|
|
262
288
|
const 잰때 = Date.now();
|
|
@@ -267,6 +293,11 @@ export async function handle(line, session, ctx) {
|
|
|
267
293
|
{ role: 'user', content: 질문 },
|
|
268
294
|
],
|
|
269
295
|
temperature: 0.3,
|
|
296
|
+
// 서버가 잠깐 막으면 돌림표 뒤에 숨기지 않고 말한다 — 왜 오래 걸리는지 보여야 한다.
|
|
297
|
+
onBackoff: (알림) => {
|
|
298
|
+
돌림.stop(` ${c.yellow('↻')} ${c.gray(말('loop.backoff', 알림채움(알림)))}`);
|
|
299
|
+
돌림 = spin(말('consult.asking', { 모델: 새conn.model }));
|
|
300
|
+
},
|
|
270
301
|
});
|
|
271
302
|
답 = String(m?.content ?? '').trim();
|
|
272
303
|
} catch (err) {
|
|
@@ -339,8 +370,19 @@ export async function handle(line, session, ctx) {
|
|
|
339
370
|
say(` ${c.gray(말('lang.progress', { 언어: 언어(), 옮김: p.옮김, 전체: p.전체 }))}`);
|
|
340
371
|
if (p.남음) say(` ${c.gray(말('lang.partial'))}`);
|
|
341
372
|
say(` ${c.gray(말('lang.codeStays'))}`);
|
|
342
|
-
|
|
343
|
-
|
|
373
|
+
// 고를 수 있는 말은 언어들 에서 그대로 만든다 — 손으로 적으면 새 말을
|
|
374
|
+
// 넣고 여기를 안 고쳐서, 있는 말이 없는 것처럼 보인다.
|
|
375
|
+
say(` ${c.gray(말('lang.howto'))} ${언어들.map((l) => c.cyan(`/lang ${l}`)).join(c.gray(' · '))}`);
|
|
376
|
+
/*
|
|
377
|
+
* 예시는 **지금 쓰는 말**로 만든다.
|
|
378
|
+
*
|
|
379
|
+
* 못 박아 두면 일본어 화면에서 "/lang ko en" 을 치라고 하게 되는데,
|
|
380
|
+
* 그대로 치면 화면이 한국어로 바뀐다. 알려 준 대로 했더니 엉뚱한
|
|
381
|
+
* 데로 가는 안내는 없느니만 못하다.
|
|
382
|
+
*/
|
|
383
|
+
// 화면이 이미 영어면 「영어로 시키고 …로 받기」가 뜻이 없다.
|
|
384
|
+
// 그때 이 줄을 그대로 내면 "/lang en en" 이라는 헛말이 나온다.
|
|
385
|
+
if (언어() !== 'en') say(` ${c.gray(말('lang.splitHowto'))} ${c.cyan(`/lang ${언어()} en`)}`);
|
|
344
386
|
say(` ${c.gray(말('lang.envHint'))}`);
|
|
345
387
|
say('');
|
|
346
388
|
return { handled: true };
|
|
@@ -617,8 +659,13 @@ export async function handle(line, session, ctx) {
|
|
|
617
659
|
case 'out': case '출력': return await 출력상한(session, arg), { handled: true };
|
|
618
660
|
|
|
619
661
|
case 'compact': {
|
|
620
|
-
|
|
621
|
-
const r = await compact(session
|
|
662
|
+
let s = spin('앞선 대화를 요약해 접는 중…');
|
|
663
|
+
const r = await compact(session, {
|
|
664
|
+
onBackoff: (알림) => {
|
|
665
|
+
s.stop(` ${c.yellow('↻')} ${c.gray(말('loop.backoff', 알림채움(알림)))}`);
|
|
666
|
+
s = spin('앞선 대화를 요약해 접는 중…');
|
|
667
|
+
},
|
|
668
|
+
});
|
|
622
669
|
if (!r.ok) {
|
|
623
670
|
s.stop(` ${c.gray(r.why ?? '접지 못했습니다.')}`);
|
|
624
671
|
say('');
|
|
@@ -728,6 +775,15 @@ export async function handle(line, session, ctx) {
|
|
|
728
775
|
say('');
|
|
729
776
|
say(` ${c.gray('지금은')} ${승인표시(session.mode)} ${c.gray('입니다. 상태줄 오른쪽에도 늘 떠 있습니다.')}`);
|
|
730
777
|
say(` ${c.gray('치지 않고 바꾸려면')} ${c.cyan('Shift+Tab')} ${c.gray('— 누를 때마다 차례로 돕니다.')}`);
|
|
778
|
+
/*
|
|
779
|
+
* 적어 둔 규칙은 모드보다 세다. 그러니 모드만 보여 주고 규칙을 안 보여
|
|
780
|
+
* 주면 화면이 거짓말을 하는 셈이다 — "안 묻습니다" 라고 적힌 모드에서
|
|
781
|
+
* 무언가 막히면 사람은 고장으로 읽는다.
|
|
782
|
+
*
|
|
783
|
+
* 어디에 적힌 규칙인지까지 적는다. 설정이면 제가 고치면 되고, 관리
|
|
784
|
+
* 정책이면 고칠 수 없다는 것을 알아야 관리자에게 말할 수 있다.
|
|
785
|
+
*/
|
|
786
|
+
규칙보이기(ctx.규칙들);
|
|
731
787
|
say('');
|
|
732
788
|
return { handled: true };
|
|
733
789
|
}
|
|
@@ -818,7 +874,21 @@ export async function handle(line, session, ctx) {
|
|
|
818
874
|
say(` ${c.gray(pad('입력 토큰', 14))} ${session.usage.in.toLocaleString()}`);
|
|
819
875
|
say(` ${c.gray(pad('출력 토큰', 14))} ${session.usage.out.toLocaleString()}`);
|
|
820
876
|
say(` ${c.gray(pad('도구 시간', 14))} ${(session.usage.ms / 1000).toFixed(1)}초`);
|
|
877
|
+
// 서버가 잠깐 막아 다시 부른 횟수. 0 이면 안 적는다 — 없는 일을 줄로 남기면 표만 길어진다.
|
|
878
|
+
if (session.usage.retries) say(` ${c.gray(pad(말('cost.retries'), 14))} ${session.usage.retries}회`);
|
|
821
879
|
say(` ${c.gray(pad('경과', 14))} ${mins}분`);
|
|
880
|
+
/*
|
|
881
|
+
* 서버가 남았다고 말해 준 할당량 (backend/quota.js).
|
|
882
|
+
*
|
|
883
|
+
* 안 알려주는 서버가 많다. 그러면 줄 자체를 안 적는다 — 모르는 것을
|
|
884
|
+
* 0 으로 적으면 멀쩡한데 다 썼다고 믿게 된다.
|
|
885
|
+
*/
|
|
886
|
+
const 남은것 = 마지막할당량();
|
|
887
|
+
if (남은것) {
|
|
888
|
+
const 말줄 = 할당량말(남은것);
|
|
889
|
+
say(` ${c.gray(pad('서버 할당량', 14))} ${아슬아슬한가(남은것) ? c.yellow(말줄) : 말줄}`
|
|
890
|
+
+ ` ${c.gray(`(${Math.round((Date.now() - 남은것.때) / 1000)}초 전 응답 기준)`)}`);
|
|
891
|
+
}
|
|
822
892
|
say('');
|
|
823
893
|
return { handled: true };
|
|
824
894
|
}
|
|
@@ -828,8 +898,18 @@ export async function handle(line, session, ctx) {
|
|
|
828
898
|
rule('연결', 70);
|
|
829
899
|
say(` ${c.gray(pad('규격', 10))} ${k.kind === 'ollama' ? 'Ollama' : 'OpenAI 호환'}`);
|
|
830
900
|
say(` ${c.gray(pad('주소', 10))} ${k.base}`);
|
|
901
|
+
// 프록시를 거치면 어느 것을, 어디서 읽었는지(env · config)까지. 안 거치면 줄 자체가 없다.
|
|
902
|
+
// 적어 놨는데 못 쓰는 것(socks5 등)이면 그 까닭을 — 조용히 직접 가면 사람은 프록시를 탄 줄 안다.
|
|
903
|
+
const 프록시 = 프록시고르기(k.base);
|
|
904
|
+
if (프록시) say(` ${c.gray(pad('프록시', 10))} ${프록시.url} ${c.gray(`(${프록시.출처})`)}`);
|
|
905
|
+
else if (프록시설정().탈) say(` ${c.gray(pad('프록시', 10))} ${c.yellow('못 씀')} ${c.gray(`— ${프록시설정().탈}`)}`);
|
|
831
906
|
say(` ${c.gray(pad('모델', 10))} ${k.model}`);
|
|
832
907
|
say(` ${c.gray(pad('작업 폴더', 10))} ${session.root}`);
|
|
908
|
+
// 명령이 어느 셸에서 도는지. "ls 가 왜 안 되지" 의 답이 이 줄에 있다.
|
|
909
|
+
say(` ${c.gray(pad('셸', 10))} ${정한셸().표시}`);
|
|
910
|
+
// 열쇠를 어디에 두고 있나. 사내 심사에서 제일 먼저 묻는 것이라
|
|
911
|
+
// 「어딘가 잠겨 있겠지」 로 두지 않고 지금 상태를 그대로 적는다.
|
|
912
|
+
if (k.auth !== 'none') say(` ${c.gray(pad('열쇠 보관', 10))} ${열쇠보관(load())}`);
|
|
833
913
|
say(` ${c.gray(pad('규칙', 10))} ${session.rules ? session.rules.name : '없음 (/init 으로 만들 수 있습니다)'}`);
|
|
834
914
|
const caps = [
|
|
835
915
|
k.tools ? c.green('도구') : c.red('도구'),
|
|
@@ -1081,6 +1161,25 @@ export async function handle(line, session, ctx) {
|
|
|
1081
1161
|
case 'evidence':
|
|
1082
1162
|
case '증거': return 증거명령(session, ctx, arg), { handled: true };
|
|
1083
1163
|
|
|
1164
|
+
case 'commit':
|
|
1165
|
+
case '커밋': return await 커밋명령(session, ctx, arg), { handled: true };
|
|
1166
|
+
|
|
1167
|
+
case 'review':
|
|
1168
|
+
case '리뷰': return await 리뷰명령(session, ctx), { handled: true };
|
|
1169
|
+
|
|
1170
|
+
/*
|
|
1171
|
+
* 클립보드에 든 화면 캡처를 그대로 붙인다.
|
|
1172
|
+
*
|
|
1173
|
+
* 파일로 앉힌 뒤 `@경로` 한 줄로 바꿔서 돌려준다. 붙이는 일은 이미
|
|
1174
|
+
* 검증된 @-붙이기 길(agent/mention.js)이 하게 두는 것이다 — 여기서
|
|
1175
|
+
* 따로 그림을 만들어 넣으면 예산 셈·눈 없는 모델 처리가 두 벌이 된다.
|
|
1176
|
+
*
|
|
1177
|
+
* 사람에게 **경로를 보여 준다.** 무엇이 나가는지 안 보이면, 실수로
|
|
1178
|
+
* 다른 캡처가 나가도 알아차릴 자리가 없다.
|
|
1179
|
+
*/
|
|
1180
|
+
case 'paste':
|
|
1181
|
+
case '붙여넣기': return 붙여넣기명령(session, ctx);
|
|
1182
|
+
|
|
1084
1183
|
case 'sessions': {
|
|
1085
1184
|
const rows = listSessions(session.root, { limit: 12 });
|
|
1086
1185
|
rule('이 폴더의 지난 대화', 70);
|
|
@@ -1682,6 +1781,227 @@ function 증거명령(session, ctx, arg = '') {
|
|
|
1682
1781
|
}
|
|
1683
1782
|
}
|
|
1684
1783
|
|
|
1784
|
+
/**
|
|
1785
|
+
* /commit — 이번 대화가 바꾼 것을 커밋한다.
|
|
1786
|
+
*
|
|
1787
|
+
* 화면에서 지키는 것 두 가지.
|
|
1788
|
+
*
|
|
1789
|
+
* 1) **찍기 전에 다 보여 준다.** 메시지 전문과 `git status --short` 를
|
|
1790
|
+
* 먼저 낸다. 커밋은 남는 것이고, 남을 것을 안 보여 주고 남기면 사람은
|
|
1791
|
+
* 나중에 `git log` 에서 처음 읽게 된다. 그때는 이미 늦다.
|
|
1792
|
+
* 2) **놀랄 자리를 미리 말한다.** 남이 먼저 담아 둔 것(index)이 있으면
|
|
1793
|
+
* 그것도 같이 실린다고 적는다. 우리가 말없이 풀어 버리면 남의 준비가
|
|
1794
|
+
* 사라지고, 말없이 실으면 남의 변경이 이 커밋에 묻어 들어간다.
|
|
1795
|
+
* 둘 다 나쁘니 풀지 않고 알린다.
|
|
1796
|
+
*
|
|
1797
|
+
* push 는 안 한다. 그건 되돌릴 수 없는 자리라 사람이 직접 할 일이다.
|
|
1798
|
+
*/
|
|
1799
|
+
/*
|
|
1800
|
+
* `/paste` — 클립보드에 든 화면 캡처를 붙인다 (tools/clipboard.js).
|
|
1801
|
+
*
|
|
1802
|
+
* 돌려주는 것은 `@경로` 한 줄이다. 그러면 바깥(repl.js)의 @-붙이기가 평소대로
|
|
1803
|
+
* 그림을 실어 보낸다 — 예산 셈도, 눈 없는 모델일 때의 처리도 그쪽에 이미
|
|
1804
|
+
* 있으니 두 벌로 만들지 않는다.
|
|
1805
|
+
*
|
|
1806
|
+
* 세 가지를 갈라 말한다. 「그림이 없다」(사람이 캡처를 다시 하면 된다),
|
|
1807
|
+
* 「못 꺼냈다」(까닭과 길을 준다), 「눈이 없는 모델이다」(붙여도 못 본다).
|
|
1808
|
+
* 셋을 뭉뚱그려 「안 됩니다」로 내면 사람은 무엇을 고쳐야 할지 모른다.
|
|
1809
|
+
*/
|
|
1810
|
+
export function 붙여넣기명령(session, ctx, { 꺼내기 = 클립보드그림 } = {}) {
|
|
1811
|
+
// 꺼내기를 갈아 끼울 수 있게 열어 둔다 — 검사가 **사람의 진짜 클립보드를
|
|
1812
|
+
// 건드리지 않고** 세 갈래(그림 있음·없음·못 꺼냄)를 다 재려면 이 자리가 필요하다.
|
|
1813
|
+
const r = 꺼내기();
|
|
1814
|
+
if (!r.ok) {
|
|
1815
|
+
if (r.없음) {
|
|
1816
|
+
say(` ${mark.warn} ${c.gray('클립보드에 그림이 없습니다. 화면을 캡처한 뒤 다시 /paste 하세요.')}`);
|
|
1817
|
+
say(` ${c.gray('(윈도우: Win+Shift+S · 맥: Cmd+Ctrl+Shift+4)')}`);
|
|
1818
|
+
} else {
|
|
1819
|
+
for (const 줄 of String(r.왜).split('\n')) say(` ${mark.warn} ${c.gray(줄)}`);
|
|
1820
|
+
}
|
|
1821
|
+
return { handled: true };
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
let 앉힌것;
|
|
1825
|
+
try {
|
|
1826
|
+
앉힌것 = 그림앉히기(r.buf, join(ctx?.scope?.root ?? session.root, '.deel'));
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
say(` ${mark.warn} ${c.gray(`그림을 저장 못 했습니다: ${err.message}`)}`);
|
|
1829
|
+
return { handled: true };
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const 보일 = ctx?.scope?.show ? ctx.scope.show(앉힌것.자리) : 앉힌것.자리;
|
|
1833
|
+
say(` ${c.blue('◧')} ${c.gray('클립보드에서 가져와 앉혔습니다')} ${c.white(보일)} ${c.gray(`(${크기말(앉힌것.바이트)})`)}`);
|
|
1834
|
+
|
|
1835
|
+
// 눈이 없는 모델이면 붙여도 못 본다. 보내기 전에 말한다 — 보내고 나서
|
|
1836
|
+
// "그림이 안 보인다" 는 답을 받으면 사람은 이 기능이 고장 난 줄 안다.
|
|
1837
|
+
if (!session.conn?.vision) {
|
|
1838
|
+
say(` ${mark.warn} ${c.gray('지금 모델은 그림을 못 봅니다 — 파일은 남았지만 글로만 나갑니다.')}`);
|
|
1839
|
+
}
|
|
1840
|
+
return { handled: false, text: `@${보일}` };
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/*
|
|
1844
|
+
* `/review` — 이번에 바꾼 것을 새 창에서 한 번 더 본다 (agent/review.js).
|
|
1845
|
+
*
|
|
1846
|
+
* 여기서는 **아무것도 안 고친다.** 찾은 것을 늘어놓고 끝이다. 고칠지 말지는
|
|
1847
|
+
* 사람이 정한다. 리뷰가 제 손으로 고치기 시작하면 사람이 무엇을 승인한 것인지
|
|
1848
|
+
* 흐려진다.
|
|
1849
|
+
*/
|
|
1850
|
+
async function 리뷰명령(session, ctx) {
|
|
1851
|
+
const 것 = 볼것(session, ctx);
|
|
1852
|
+
if (!것.ok) {
|
|
1853
|
+
say('');
|
|
1854
|
+
say(` ${c.gray(것.왜)}`);
|
|
1855
|
+
say('');
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
rule(`리뷰 — ${것.어디}`, 70);
|
|
1860
|
+
say(` ${c.gray(것.통계.split(/\r?\n/).pop()?.trim() || `${것.파일들.length}개 파일`)}`);
|
|
1861
|
+
say(` ${c.gray('지금 대화는 안 보냅니다 — 바뀐 코드만 새 창에서 봅니다.')}`);
|
|
1862
|
+
say('');
|
|
1863
|
+
|
|
1864
|
+
const s = spin('보는 중...');
|
|
1865
|
+
const r = await 리뷰받기(session, 것, {
|
|
1866
|
+
signal: ctx.signal ?? null,
|
|
1867
|
+
onBackoff: (다시) => s.set?.(`서버가 잠깐 막아 ${Math.round((다시.wait ?? 0) / 1000)}초 기다리는 중...`),
|
|
1868
|
+
});
|
|
1869
|
+
s.stop('');
|
|
1870
|
+
|
|
1871
|
+
if (!r.ok) {
|
|
1872
|
+
say(` ${mark.no} ${c.red('못 봤습니다')} ${c.gray(`— ${r.왜}`)}`);
|
|
1873
|
+
say('');
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
/*
|
|
1878
|
+
* 가른 것이 없으면 **원문을 그대로** 보여 준다.
|
|
1879
|
+
*
|
|
1880
|
+
* 모델이 형식을 안 지켰다고 아무것도 안 보여 주면, 사람은 리뷰가 아무것도
|
|
1881
|
+
* 못 찾은 줄 안다. 우리가 못 읽은 것이지 모델이 침묵한 것이 아니다.
|
|
1882
|
+
*/
|
|
1883
|
+
if (!r.찾은것.length) {
|
|
1884
|
+
for (const 줄 of r.글.split(/\r?\n/)) say(` ${줄}`);
|
|
1885
|
+
say('');
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
const 색 = { 심각: c.red, 보통: c.yellow, 사소: c.gray };
|
|
1890
|
+
for (const f of r.찾은것) {
|
|
1891
|
+
const 칠 = 색[f.급] ?? c.white;
|
|
1892
|
+
say(` ${칠(f.머리)}`);
|
|
1893
|
+
for (const 줄 of f.몸) if (줄.trim()) say(` ${c.gray(줄.trim())}`);
|
|
1894
|
+
say('');
|
|
1895
|
+
}
|
|
1896
|
+
const 셈 = ['심각', '보통', '사소'].map((g) => [g, r.찾은것.filter((x) => x.급 === g).length]).filter(([, n]) => n);
|
|
1897
|
+
say(` ${c.gray(셈.map(([g, n]) => `${g} ${n}`).join(' · '))}`);
|
|
1898
|
+
// 자리를 못 짚은 지적은 몇 개인지 적는다. 찾아갈 수 없는 지적은 값이 다르다.
|
|
1899
|
+
const 자리없음 = r.찾은것.filter((x) => !x.자리).length;
|
|
1900
|
+
if (자리없음) say(` ${c.gray(`${자리없음}개는 파일:줄 을 안 짚었습니다 — 그만큼 확인하기 어렵습니다.`)}`);
|
|
1901
|
+
say(` ${c.gray('아무것도 안 고쳤습니다. 고칠 것을 골라 시키세요.')}`);
|
|
1902
|
+
say('');
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
async function 커밋명령(session, ctx, arg = '') {
|
|
1906
|
+
const 말한것 = String(arg ?? '').trim();
|
|
1907
|
+
const 전부 = /^(전부|all)$/i.test(말한것);
|
|
1908
|
+
const 미리보기 = /^(미리보기|preview|dry|--dry-run)$/i.test(말한것);
|
|
1909
|
+
const 준제목 = 전부 || 미리보기 ? null : (말한것 || null);
|
|
1910
|
+
|
|
1911
|
+
say('');
|
|
1912
|
+
rule('커밋', 70);
|
|
1913
|
+
|
|
1914
|
+
const 도는말 = '메시지를 짓는 중…';
|
|
1915
|
+
let 돌림 = spin(도는말);
|
|
1916
|
+
const r = await 커밋준비(session, ctx, {
|
|
1917
|
+
전부,
|
|
1918
|
+
제목: 준제목,
|
|
1919
|
+
onBackoff: (알림) => {
|
|
1920
|
+
돌림.stop(` ${c.yellow('↻')} ${c.gray(말('loop.backoff', 알림채움(알림)))}`);
|
|
1921
|
+
돌림 = spin(도는말);
|
|
1922
|
+
},
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
if (!r.ok) {
|
|
1926
|
+
돌림.stop(` ${c.gray(r.why)}`);
|
|
1927
|
+
say('');
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
돌림.stop(` ${c.bold('담은 것')} ${c.gray(`— 파일 ${r.파일들.length}개`)}`);
|
|
1931
|
+
|
|
1932
|
+
for (const f of r.파일들.slice(0, 12)) say(` ${c.gray('·')} ${c.white(f)}`);
|
|
1933
|
+
if (r.파일들.length > 12) say(` ${c.gray(`… 그 밖에 ${r.파일들.length - 12}개`)}`);
|
|
1934
|
+
if (r.살림뺌) {
|
|
1935
|
+
say(` ${c.gray('· .deel/ 은 안 담았습니다 — 열쇠와 감사기록이 든 곳입니다')}`);
|
|
1936
|
+
}
|
|
1937
|
+
if (r.폴더통째?.length) {
|
|
1938
|
+
say('');
|
|
1939
|
+
say(` ${c.yellow('폴더째 바뀐 자리는 안 담았습니다')} ${c.gray(`— ${r.폴더통째.slice(0, 4).join(', ')}`)}`);
|
|
1940
|
+
say(` ${c.gray('그 안에는 남이 고치던 파일도 있습니다. 통째로 담으려면')} ${c.cyan('/commit 전부')}`);
|
|
1941
|
+
}
|
|
1942
|
+
if (r.남의것.length) {
|
|
1943
|
+
say('');
|
|
1944
|
+
say(` ${c.yellow('먼저 담겨 있던 것도 같이 실립니다')} ${c.gray(`— ${r.남의것.slice(0, 6).join(', ')}`)}`);
|
|
1945
|
+
say(` ${c.gray('빼려면 `git restore --staged <파일>` 뒤에 다시 부르세요.')}`);
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
say('');
|
|
1949
|
+
say(` ${c.bold('메시지')}`);
|
|
1950
|
+
for (const line of r.메시지.trimEnd().split('\n')) {
|
|
1951
|
+
say(` ${line.trim() ? c.white(clip(line, 76)) : ''}`);
|
|
1952
|
+
}
|
|
1953
|
+
if (r.사실로만) {
|
|
1954
|
+
say('');
|
|
1955
|
+
say(` ${c.yellow('모델이 메시지를 못 만들어 바뀐 것만 적었습니다.')}`);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
const 상태줄 = r.상태 ? r.상태.split('\n') : [];
|
|
1959
|
+
if (상태줄.length) {
|
|
1960
|
+
say('');
|
|
1961
|
+
say(` ${c.bold('git status --short')}`);
|
|
1962
|
+
for (const line of 상태줄.slice(0, 10)) say(` ${c.gray(clip(line, 76))}`);
|
|
1963
|
+
if (상태줄.length > 10) say(` ${c.gray(`… 그 밖에 ${상태줄.length - 10}줄`)}`);
|
|
1964
|
+
}
|
|
1965
|
+
say('');
|
|
1966
|
+
|
|
1967
|
+
if (미리보기) {
|
|
1968
|
+
say(` ${c.gray('미리보기입니다 — 찍지 않았습니다. 그대로 찍으려면')} ${c.cyan('/commit')}`);
|
|
1969
|
+
say('');
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// 엄격 모드에서만 묻는다. 여기서 무르면 담은 것은 그대로 둔다 —
|
|
1974
|
+
// 사람이 메시지만 다시 받고 싶을 수도 있는데, 담은 것까지 풀면 처음부터다.
|
|
1975
|
+
if (session.mode === 'strict') {
|
|
1976
|
+
const 예 = await confirm('이대로 커밋할까요?', true);
|
|
1977
|
+
if (!예) {
|
|
1978
|
+
say(` ${c.gray('안 찍었습니다. 담은 것은 그대로 둡니다.')}`);
|
|
1979
|
+
say('');
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
// 묻는 사이에 담긴 것이 바뀌었을 수 있다(다른 창·다른 도구). 보여 준 것과
|
|
1983
|
+
// 다른 것을 찍으면, 승인을 받은 의미가 없어진다.
|
|
1984
|
+
const 지금 = r.다시확인();
|
|
1985
|
+
if (JSON.stringify(지금) !== JSON.stringify(r.파일들)) {
|
|
1986
|
+
say(` ${c.yellow('묻는 사이에 담긴 것이 바뀌었습니다')} ${c.gray(`— 보여 준 ${r.파일들.length}개 → 지금 ${지금.length}개`)}`);
|
|
1987
|
+
say(` ${c.gray('안 찍었습니다. 다시')} ${c.cyan('/commit')} ${c.gray('으로 확인하세요.')}`);
|
|
1988
|
+
say('');
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const 찍음 = 커밋실행(r.뿌리, r.메시지, { audit: ctx?.audit, 파일들: r.파일들, 제목: r.제목 });
|
|
1994
|
+
if (!찍음.ok) {
|
|
1995
|
+
say(` ${c.red('✗')} ${c.gray(clip(찍음.why, 90))}`);
|
|
1996
|
+
say('');
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
say(` ${mark.ok} ${c.green(찍음.hash)} ${c.white(clip(r.제목, 60))}`);
|
|
2000
|
+
if (r.미확인 > 0) say(` ${c.yellow(`검증: ${r.확인}건 확인 · ${r.미확인}건 미확인`)} ${c.gray('— 메시지에도 적었습니다')}`);
|
|
2001
|
+
say(` ${c.gray('push 는 안 했습니다. 되돌리려면')} ${c.cyan('git reset --soft HEAD~1')}`);
|
|
2002
|
+
say('');
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1685
2005
|
/**
|
|
1686
2006
|
* 모델 카드 — 겪어 본 버릇과, 그 때문에 deel 이 바꾼 것.
|
|
1687
2007
|
*
|
|
@@ -2250,6 +2570,19 @@ async function 서버모델들(conn) {
|
|
|
2250
2570
|
const r = await req(`${conn.base.replace(/\/v1\/?$/, '')}/api/tags`, { timeout: 4000 });
|
|
2251
2571
|
return (r.json?.models ?? []).map((m) => m.name ?? m.model).filter(Boolean);
|
|
2252
2572
|
}
|
|
2573
|
+
/*
|
|
2574
|
+
* Azure 는 모델 목록이 `/models` 가 아니라 `/openai/deployments` 에 있다.
|
|
2575
|
+
* 여기를 안 고쳐 두면 `/model` 로 배포를 바꾸려는 순간
|
|
2576
|
+
* `.../deployments/gpt-4o?api-version=2024-10-21/models` 를 두드리고 404 다 —
|
|
2577
|
+
* 붙는 길만 고치고 바꾸는 길을 안 고치면 반쪽이다.
|
|
2578
|
+
*/
|
|
2579
|
+
const { 애저인가, 애저풀기, 배포목록 } = await import('./backend/azure.js');
|
|
2580
|
+
if (애저인가(conn.base)) {
|
|
2581
|
+
const 푼것 = 애저풀기(conn.base);
|
|
2582
|
+
const a = await req(푼것.목록주소, { headers: headersFor(conn.auth ?? 'none', conn.key), timeout: 4000 });
|
|
2583
|
+
if (!a.ok) return null;
|
|
2584
|
+
return 배포목록(a.json).map((m) => m.id);
|
|
2585
|
+
}
|
|
2253
2586
|
const r = await req(`${conn.base.replace(/\/$/, '')}/models`, {
|
|
2254
2587
|
headers: headersFor(conn.auth ?? 'none', conn.key), timeout: 4000,
|
|
2255
2588
|
});
|