deel-local-cli 1.6.3 → 1.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.ko.md +1182 -0
- package/README.md +850 -636
- package/bin/deel.js +44 -3
- package/package.json +3 -3
- package/src/acp/map.js +1 -1
- package/src/acp/serve.js +25 -2
- package/src/agent/loop.js +41 -9
- package/src/agent/session.js +15 -9
- package/src/backend/adapter.js +277 -4
- package/src/backend/ctxsize.js +16 -2
- package/src/backend/detect.js +104 -6
- package/src/backend/price.js +197 -0
- package/src/backend/retry.js +3 -0
- package/src/backend/scanui.js +2 -1
- package/src/backend/vision.js +26 -4
- package/src/commands.js +141 -38
- package/src/completion.js +21 -2
- package/src/i18n/en.js +160 -1
- package/src/i18n/index.js +22 -0
- package/src/i18n/ja.js +152 -1
- package/src/i18n/ko.js +173 -1
- package/src/i18n/zh.js +152 -1
- package/src/oneshot.js +39 -6
- package/src/pack/selfpack.js +1 -1
- package/src/providers/anthropic.js +48 -0
- package/src/providers/bedrock.js +102 -0
- package/src/providers/custom.js +65 -0
- package/src/providers/gemini.js +64 -0
- package/src/providers/index.js +121 -0
- package/src/providers/openai.js +68 -0
- package/src/repl.js +198 -33
- package/src/report.js +3 -2
- package/src/reset.js +397 -0
- package/src/safety/audit.js +5 -2
- package/src/safety/authcmd.js +305 -0
- package/src/safety/keystore.js +53 -0
- package/src/safety/runmode.js +123 -0
- package/src/safety/secrets.js +61 -0
- package/src/safety/undo.js +3 -2
- package/src/setup.js +215 -12
- package/src/tools/desc.en.js +19 -14
- package/src/tools/index.js +141 -43
- package/src/tools/jobs.js +17 -13
- package/src/tools/lsp.js +5 -4
- package/src/tools/outline.js +7 -3
- package/src/tools/task.js +10 -6
- package/src/tools/todo.js +9 -2
- package/src/tools/verify.js +9 -3
- package/src/tools/webfetch.js +2 -1
- package/src/ui/export.js +1 -1
- package/src/ui/histline.js +67 -0
- package/src/ui/status.js +88 -3
- package/README.en.md +0 -1025
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 요금 셈 — 쓴 토큰을 돈으로 옮긴다.
|
|
3
|
+
*
|
|
4
|
+
* ── 이 파일에는 값이 박힌 요금표가 없다 ────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 일부러 안 넣었다. 요금은 회사가 아무 때나 바꾸는 값이고, 소스에 박아 두면
|
|
7
|
+
* 고칠 사람이 없다. 그러면 이 도구는 **틀린 금액을 자신 있게 찍는 도구**가
|
|
8
|
+
* 된다. 토큰 수를 안 보여 주는 것보다 그쪽이 훨씬 나쁘다 — 사람은 화면에
|
|
9
|
+
* 뜬 숫자를 믿고 예산을 잡는다.
|
|
10
|
+
*
|
|
11
|
+
* 그래서 값의 출처는 둘뿐이다.
|
|
12
|
+
*
|
|
13
|
+
* 1. 사람이 설정에 적은 값 (.deel/config.json 의 `요금`)
|
|
14
|
+
* 2. 제공자 서술 파일이 들고 있는 값 (src/providers/*.js 의 `요금`)
|
|
15
|
+
*
|
|
16
|
+
* 2번은 지금 전부 비어 있다. 확인한 값만 넣기로 했고, 확인하지 못했다.
|
|
17
|
+
* 비어 있는 표는 「모른다」 고 말하지만, 지어낸 표는 거짓말을 한다.
|
|
18
|
+
*
|
|
19
|
+
* 모르면 돈을 안 찍는다. 토큰만 찍고 「요금 모름」 이라고 적는다.
|
|
20
|
+
*
|
|
21
|
+
* ── 단위 ───────────────────────────────────────────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* 100만 토큰당 달러로 적는다. 회사들이 요금표를 그 단위로 내놓기 때문이다.
|
|
24
|
+
* 사람이 요금표에서 본 숫자를 그대로 옮겨 적을 수 있어야 한다. 여기서
|
|
25
|
+
* 1000토큰당으로 받으면 1000배 틀린 값이 조용히 들어온다.
|
|
26
|
+
*/
|
|
27
|
+
import { 제공자고르기 } from '../providers/index.js';
|
|
28
|
+
|
|
29
|
+
// 100만 토큰당. 요금표에 적힌 숫자를 그대로 받기 위한 단위다.
|
|
30
|
+
export const 단위 = 1000000;
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* 기준 날짜가 이만큼 지나면 「오래됨」 이라고 덧붙인다.
|
|
34
|
+
*
|
|
35
|
+
* 값을 버리지는 않는다. 오래된 값도 자릿수는 맞으므로 안 보여 주는 것보다
|
|
36
|
+
* 낫다. 다만 그대로 믿게 두면 안 된다. 반년이면 회사가 요금을 한 번쯤
|
|
37
|
+
* 손대는 기간이라 이렇게 잡았다.
|
|
38
|
+
*/
|
|
39
|
+
export const 낡는날 = 180;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 설정에 적힌 값이 쓸 수 있는 요금인가.
|
|
43
|
+
*
|
|
44
|
+
* 문자열로 적어도 받는다 — 사람이 JSON 에 "2.5" 라고 적는 일은 흔하고,
|
|
45
|
+
* 그걸 튕기면 조용히 「요금 모름」 이 되어 왜 안 나오는지 알 길이 없다.
|
|
46
|
+
* 대신 숫자가 아닌 것·음수는 받지 않고, 왜 못 받았는지 말로 남긴다.
|
|
47
|
+
*/
|
|
48
|
+
function 값읽기(x) {
|
|
49
|
+
if (x == null || x === '') return null;
|
|
50
|
+
const v = typeof x === 'string' ? Number(x.trim()) : Number(x);
|
|
51
|
+
if (!Number.isFinite(v) || v < 0) return null;
|
|
52
|
+
return v;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 모델 이름이 이 열쇠에 걸리나.
|
|
57
|
+
*
|
|
58
|
+
* 딱 맞는 이름이 먼저다. `*` 를 쓴 열쇠(`gpt-4o*`)는 사람이 **일부러**
|
|
59
|
+
* 적었을 때만 걸린다 — 우리가 알아서 앞머리를 잘라 맞추면, 값이 다른
|
|
60
|
+
* 형제 모델(`-mini`)에 비싼 요금이 붙어도 아무도 모른다.
|
|
61
|
+
*/
|
|
62
|
+
function 걸리나(열쇠, 모델) {
|
|
63
|
+
if (열쇠 === 모델) return { 맞나: true, 딱: true };
|
|
64
|
+
if (!열쇠.includes('*')) return { 맞나: false };
|
|
65
|
+
const 무늬 = new RegExp(`^${열쇠.split('*').map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*')}$`);
|
|
66
|
+
return { 맞나: 무늬.test(모델), 딱: false };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function 한칸읽기(열쇠, 값, 어디서) {
|
|
70
|
+
const 입 = 값읽기(값?.입력 ?? 값?.in ?? 값?.input);
|
|
71
|
+
const 출 = 값읽기(값?.출력 ?? 값?.out ?? 값?.output);
|
|
72
|
+
if (입 == null || 출 == null) {
|
|
73
|
+
return { 탈: `${어디서} 의 '${열쇠}' 요금이 숫자가 아닙니다 (입력·출력 둘 다 필요합니다)` };
|
|
74
|
+
}
|
|
75
|
+
const 기준 = 값?.기준 ?? 값?.asOf ?? null;
|
|
76
|
+
return { 입력: 입, 출력: 출, 기준: 기준 ? String(기준) : null, 어디서, 열쇠 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 이 모델의 요금을 찾는다.
|
|
81
|
+
*
|
|
82
|
+
* 사람이 적은 값이 먼저다. 딸려 온 표가 낡았을 때 사람이 고칠 방법이
|
|
83
|
+
* 그것뿐이라 그렇다. 못 찾으면 null 을 준다 — 0 이 아니다. 0 을 주면
|
|
84
|
+
* 「공짜」 와 「모름」 이 화면에서 같아진다.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} 모델
|
|
87
|
+
* @param {{설정?: object|null, 제공자?: object|null, 이제?: number}} o
|
|
88
|
+
*/
|
|
89
|
+
export function 요금찾기(모델, { 설정 = null, 제공자 = null, 이제 = Date.now() } = {}) {
|
|
90
|
+
const 이름 = String(모델 ?? '').trim();
|
|
91
|
+
if (!이름) return null;
|
|
92
|
+
const 탈들 = [];
|
|
93
|
+
|
|
94
|
+
for (const [표, 어디서] of [[설정, '설정'], [제공자?.요금, '딸려 온 표']]) {
|
|
95
|
+
if (!표 || typeof 표 !== 'object') continue;
|
|
96
|
+
// 딱 맞는 이름 먼저, 그다음에 사람이 `*` 로 적어 둔 것.
|
|
97
|
+
for (const 딱먼저 of [true, false]) {
|
|
98
|
+
for (const [열쇠, 값] of Object.entries(표)) {
|
|
99
|
+
const g = 걸리나(열쇠, 이름);
|
|
100
|
+
if (!g.맞나 || !!g.딱 !== 딱먼저) continue;
|
|
101
|
+
const 읽은 = 한칸읽기(열쇠, 값, 어디서);
|
|
102
|
+
if (읽은.탈) { 탈들.push(읽은.탈); continue; }
|
|
103
|
+
const 기준 = 읽은.기준 ?? (어디서 === '딸려 온 표' ? 제공자?.요금기준 ?? null : null);
|
|
104
|
+
return { ...읽은, 기준, 낡았나: 낡았나(기준, 이제), 탈들 };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return 탈들.length ? { 없음: true, 탈들 } : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 이 대화에 걸린 요금값.
|
|
113
|
+
*
|
|
114
|
+
* 상태줄은 글자를 칠 때마다 다시 그려진다. 그때마다 표를 훑으면 아까우니
|
|
115
|
+
* 모델 이름이 그대로인 동안은 찾은 것을 들고 있는다. 모델을 갈아타면
|
|
116
|
+
* (/model) 이름이 달라지므로 저절로 다시 찾는다 — 갈아탈 때 이걸 지워
|
|
117
|
+
* 달라고 부탁하는 방식은 언젠가 한 군데를 빠뜨린다.
|
|
118
|
+
*/
|
|
119
|
+
export function 세션요금(session, { 이제 = Date.now() } = {}) {
|
|
120
|
+
const 모델 = String(session?.conn?.model ?? '');
|
|
121
|
+
if (session?.요금캐시 && session.요금캐시.모델 === 모델) return session.요금캐시.값;
|
|
122
|
+
const 값 = 요금찾기(모델, {
|
|
123
|
+
설정: session?.요금표 ?? null,
|
|
124
|
+
제공자: session?.제공자 ? 제공자고르기(session.제공자) : null,
|
|
125
|
+
이제,
|
|
126
|
+
});
|
|
127
|
+
if (session) session.요금캐시 = { 모델, 값 };
|
|
128
|
+
return 값;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 기준 날짜가 반년보다 오래됐나. 날짜가 없으면 「모른다」 — 오래됐다고 치지 않는다. */
|
|
132
|
+
export function 낡았나(기준, 이제 = Date.now()) {
|
|
133
|
+
if (!기준) return false;
|
|
134
|
+
const t = Date.parse(기준);
|
|
135
|
+
if (!Number.isFinite(t)) return false;
|
|
136
|
+
return (이제 - t) / 86400000 > 낡는날;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 토큰을 달러로.
|
|
141
|
+
*
|
|
142
|
+
* 요금을 모르면 null 이다. 여기서 0 을 돌려주면 부르는 쪽이 「$0.00」 을
|
|
143
|
+
* 찍게 되고, 그건 공짜라는 뜻이 되어 버린다.
|
|
144
|
+
*/
|
|
145
|
+
export function 돈셈({ in: 입 = 0, out: 출 = 0 } = {}, 값) {
|
|
146
|
+
if (!값 || 값.없음 || 값.입력 == null || 값.출력 == null) return null;
|
|
147
|
+
const 입돈 = (Number(입) || 0) / 단위 * 값.입력;
|
|
148
|
+
const 출돈 = (Number(출) || 0) / 단위 * 값.출력;
|
|
149
|
+
return { 달러: 입돈 + 출돈, 입력: 입돈, 출력: 출돈 };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 돈을 사람이 읽는 글로.
|
|
154
|
+
*
|
|
155
|
+
* 아주 작은 값을 `$0.00` 으로 반올림하면 안 된다. 로컬에서 한두 턴 돌린
|
|
156
|
+
* 뒤 화면에 `$0.00` 이 서 있으면 「공짜네」 로 읽히고, 그 상태로 백 턴을
|
|
157
|
+
* 돌린다. 자릿수가 안 되면 반올림하지 말고 미만이라고 적는다.
|
|
158
|
+
*/
|
|
159
|
+
export function 돈말(달러) {
|
|
160
|
+
const v = Number(달러);
|
|
161
|
+
if (!Number.isFinite(v) || v < 0) return null;
|
|
162
|
+
if (v === 0) return '$0';
|
|
163
|
+
if (v < 0.0001) return '<$0.0001';
|
|
164
|
+
if (v < 1) return `$${v.toFixed(4)}`;
|
|
165
|
+
if (v < 1000) return `$${v.toFixed(2)}`;
|
|
166
|
+
return `$${Math.round(v).toLocaleString()}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 이 금액이 어디서 온 값인지 한 줄로.
|
|
171
|
+
*
|
|
172
|
+
* 금액만 찍으면 사람은 그게 오늘 요금표에서 온 값이라고 믿는다. 어디서
|
|
173
|
+
* 왔는지·언제 기준인지를 같이 적어야 의심할 거리가 생긴다.
|
|
174
|
+
*/
|
|
175
|
+
export function 어디서온값(값) {
|
|
176
|
+
if (!값 || 값.없음) return null;
|
|
177
|
+
const 조각 = [값.어디서];
|
|
178
|
+
if (값.열쇠 && 값.열쇠.includes('*')) 조각.push(`'${값.열쇠}' 로 맞춤`);
|
|
179
|
+
if (값.기준) 조각.push(`${값.기준} 기준${값.낡았나 ? ' · 오래됨' : ''}`);
|
|
180
|
+
else 조각.push('기준 날짜 없음');
|
|
181
|
+
return 조각.join(' · ');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 요금을 모를 때 사람에게 할 말.
|
|
186
|
+
*
|
|
187
|
+
* 「모릅니다」 로 끝내면 사람이 할 수 있는 일이 없다. 어디에 무엇을 적으면
|
|
188
|
+
* 되는지까지 적는다. 요금표 주소를 아는 제공자면 그것도 같이 준다.
|
|
189
|
+
*/
|
|
190
|
+
export function 요금적는법(모델, { 제공자 = null, 설정파일 = '.deel/config.json' } = {}) {
|
|
191
|
+
const 줄 = [
|
|
192
|
+
`요금을 모릅니다. ${설정파일} 에 100만 토큰당 달러로 적으면 셈합니다.`,
|
|
193
|
+
` "요금": { "${모델 || '모델이름'}": { "입력": 0, "출력": 0, "기준": "${new Date().toISOString().slice(0, 10)}" } }`,
|
|
194
|
+
];
|
|
195
|
+
if (제공자?.요금표주소) 줄.push(` 요금표: ${제공자.요금표주소}`);
|
|
196
|
+
return 줄;
|
|
197
|
+
}
|
package/src/backend/retry.js
CHANGED
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
// · 세 번까지. 그 뒤로는 사실대로 말한다. 열 번 두드리는 것은 할당량을 더 빨리
|
|
18
18
|
// 태우는 짓이다.
|
|
19
19
|
// · 400·401·403·404 는 다시 불러 봐야 같다. 한 번만 부르고 서버가 한 말을 보여 준다.
|
|
20
|
+
// 401 에 딱 하나 예외가 있는데, 그것도 여기가 아니라 backend/adapter.js 가 한다 —
|
|
21
|
+
// 열쇠받기(safety/authcmd.js)가 걸려 있으면 **같은 열쇠로 다시 부르는 것이 아니라**
|
|
22
|
+
// 새 열쇠를 받아서 부르는 것이라, 「불러 봐야 같다」 에 해당하지 않는다. 딱 한 번이다.
|
|
20
23
|
// · ECONNREFUSED 는 서버가 꺼진 것이고, 시간 초과는 5분을 또 기다릴 일이 아니다.
|
|
21
24
|
// 둘 다 안 부른다.
|
|
22
25
|
//
|
package/src/backend/scanui.js
CHANGED
|
@@ -3,6 +3,7 @@ import { c, say, rule, pad, mark, clip, width } from '../ui/ansi.js';
|
|
|
3
3
|
import { pick } from '../ui/prompt.js';
|
|
4
4
|
import { spin } from '../ui/spinner.js';
|
|
5
5
|
import { scanLocal, toProfiles, KNOWN } from './scan.js';
|
|
6
|
+
import { 규격이름 } from './adapter.js';
|
|
6
7
|
import { load, save, upsert } from '../config.js';
|
|
7
8
|
|
|
8
9
|
// 코딩 에이전트로 쓰려면 도구 호출이 되어야 한다. 진단 전이라 확실히는 모르니,
|
|
@@ -66,7 +67,7 @@ export async function runScan(flags = {}) {
|
|
|
66
67
|
const wRun = Math.max(10, ...found.map((f) => width(f.runtime)));
|
|
67
68
|
for (const f of found) {
|
|
68
69
|
const 자리 = `${f.host}:${f.port}`;
|
|
69
|
-
const 규격 = f.kind
|
|
70
|
+
const 규격 = 규격이름(f.kind);
|
|
70
71
|
const 표시 = f.guessed ? c.gray(' (추정)') : '';
|
|
71
72
|
say(` ${c.hcyan('◆')} ${c.bold(pad(f.runtime, wRun))}${표시} ${c.gray(pad(자리, 22))}${c.gray(pad(규격, 14))}${c.gray(f.ms + 'ms')}`);
|
|
72
73
|
if (f.locked) {
|
package/src/backend/vision.js
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
//
|
|
14
14
|
// ── 무엇을 하나 ────────────────────────────────────────────────────────
|
|
15
15
|
//
|
|
16
|
-
// 그림이면 그림으로 싣는다. 규격이
|
|
16
|
+
// 그림이면 그림으로 싣는다. 규격이 셋이라 모양도 셋이다.
|
|
17
17
|
//
|
|
18
18
|
// OpenAI 호환 content 배열에 { type:'image_url', image_url:{ url:'data:…' } }
|
|
19
19
|
// Ollama 메시지에 images: ['<base64>'] (data: 머리말 없이)
|
|
20
|
+
// Anthropic content 배열에 { type:'image', source:{ type:'base64', media_type, data } }
|
|
20
21
|
//
|
|
21
22
|
// 못 보는 모델에게는 **바이트를 아예 안 보낸다.** 보내 봐야 400 이 오거나,
|
|
22
23
|
// 더 나쁘게는 서버가 조용히 무시하고 답을 지어낸다. 대신 한 줄로 말한다.
|
|
@@ -127,11 +128,15 @@ export function 그림읽기(abs, { 한도 = 기본한도 } = {}) {
|
|
|
127
128
|
*/
|
|
128
129
|
export const 그림한장토큰 = 1000;
|
|
129
130
|
|
|
130
|
-
/** 메시지 하나에 그림이 몇 장 실려 있나. 규격
|
|
131
|
+
/** 메시지 하나에 그림이 몇 장 실려 있나. 규격 세 가지를 다 본다. */
|
|
131
132
|
export function 그림장수(m) {
|
|
132
133
|
if (!m) return 0;
|
|
133
134
|
if (Array.isArray(m.images)) return m.images.length; // Ollama
|
|
134
|
-
|
|
135
|
+
// image_url 은 OpenAI 호환, image 는 Anthropic. 한쪽만 세면 그림을 실어
|
|
136
|
+
// 놓고도 안 실은 줄 알고 창 크기를 잘못 잡는다.
|
|
137
|
+
if (Array.isArray(m.content)) {
|
|
138
|
+
return m.content.filter((p) => p?.type === 'image_url' || p?.type === 'image').length;
|
|
139
|
+
}
|
|
135
140
|
return 0;
|
|
136
141
|
}
|
|
137
142
|
|
|
@@ -155,6 +160,23 @@ export function 글만(m) {
|
|
|
155
160
|
*/
|
|
156
161
|
export function 그림메시지(shape, { 글 = '', 그림들 = [] } = {}) {
|
|
157
162
|
const 것들 = 그림들.filter((g) => g?.b64);
|
|
163
|
+
/*
|
|
164
|
+
* Anthropic 은 그림을 data: 주소로 안 받는다. 종류와 알맹이를 따로 준다.
|
|
165
|
+
* image_url 로 보내면 「모르는 블록」 이라고 통째로 거절당한다 — 글까지
|
|
166
|
+
* 같이 안 간다는 뜻이라, 눈이 없는 것과 달리 대화가 아예 안 이어진다.
|
|
167
|
+
*/
|
|
168
|
+
if (shape === 'anthropic') {
|
|
169
|
+
return {
|
|
170
|
+
role: 'user',
|
|
171
|
+
content: [
|
|
172
|
+
{ type: 'text', text: 글 },
|
|
173
|
+
...것들.map((g) => ({
|
|
174
|
+
type: 'image',
|
|
175
|
+
source: { type: 'base64', media_type: g.mime, data: g.b64 },
|
|
176
|
+
})),
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
158
180
|
if (shape === 'ollama') {
|
|
159
181
|
// Ollama 는 data: 머리말을 안 받는다. base64 알맹이만 준다.
|
|
160
182
|
return { role: 'user', content: 글, images: 것들.map((g) => g.b64) };
|
|
@@ -176,7 +198,7 @@ export function 그림메시지(shape, { 글 = '', 그림들 = [] } = {}) {
|
|
|
176
198
|
*/
|
|
177
199
|
export const 한점PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
|
|
178
200
|
|
|
179
|
-
/** 눈 검사에 쓸 메시지. 규격
|
|
201
|
+
/** 눈 검사에 쓸 메시지. 규격 세 가지 다. */
|
|
180
202
|
export function 눈검사메시지(shape) {
|
|
181
203
|
return 그림메시지(shape, {
|
|
182
204
|
글: '이 그림에 무엇이 있습니까? 한 단어로 답하세요.',
|
package/src/commands.js
CHANGED
|
@@ -2,23 +2,28 @@
|
|
|
2
2
|
import { writeFileSync, existsSync, statSync } from 'node:fs';
|
|
3
3
|
import { 볼것, 리뷰받기 } from './agent/review.js';
|
|
4
4
|
import { 마지막할당량, 할당량말, 아슬아슬한가 } from './backend/quota.js';
|
|
5
|
+
import { 세션요금, 돈셈, 돈말, 어디서온값, 요금적는법 } from './backend/price.js';
|
|
5
6
|
import { join } from 'node:path';
|
|
6
7
|
import { c, say, rule, pad, bar, mark, width, clip } from './ui/ansi.js';
|
|
7
8
|
import { compact } from './agent/compact.js';
|
|
8
9
|
import { 증거모으기, 증거적기 } from './agent/evidence.js';
|
|
9
10
|
import { 커밋준비, 커밋실행 } from './agent/commit.js';
|
|
10
11
|
import { allowEndpoint } from './safety/network.js';
|
|
12
|
+
import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js';
|
|
13
|
+
import { 주소가리기 } from './safety/secrets.js';
|
|
11
14
|
import { pick, confirm } from './ui/prompt.js';
|
|
12
|
-
import { load, save, resolveKey, upsert,
|
|
15
|
+
import { load, save, resolveKey, upsert, 열쇠보관, configPath } from './config.js';
|
|
16
|
+
import { 지금상태 as 지금열쇠상태 } from './safety/authcmd.js';
|
|
17
|
+
import { 제공자고르기 } from './providers/index.js';
|
|
13
18
|
import { 종, 알릴만한초 } from './ui/notify.js';
|
|
14
19
|
import { 말, 언어, 언어들, 언어정하기, 언어고르기, 옮긴만큼, 지시말, 지시말정하기, 지시말따로정했나 } from './i18n/index.js';
|
|
15
20
|
import { 프로필찾기, 쓸수있나, 연결만들기, 알릴말, 목록보기 } from './agent/models.js';
|
|
16
21
|
import { allowTemporarily } from './safety/network.js';
|
|
17
|
-
import { chat } from './backend/adapter.js';
|
|
22
|
+
import { chat, 규격이름 } from './backend/adapter.js';
|
|
18
23
|
import { 알림채움 } from './backend/retry.js';
|
|
19
24
|
import { 프록시고르기, 프록시설정 } from './backend/proxy.js';
|
|
20
25
|
import { 정한셸 } from './tools/shell.js';
|
|
21
|
-
import { TOOLS } from './tools/index.js';
|
|
26
|
+
import { TOOLS, 영어설명 } from './tools/index.js';
|
|
22
27
|
import { 둘러보기, 프로젝트갈래 } from './lsp/servers.js';
|
|
23
28
|
import { 지금것들 } from './lsp/client.js';
|
|
24
29
|
import { 보고서적기 } from './ui/export.js';
|
|
@@ -859,9 +864,20 @@ export async function handle(line, session, ctx) {
|
|
|
859
864
|
case 'serve': return await 미리보기(session, ctx, arg), { handled: true };
|
|
860
865
|
|
|
861
866
|
case 'tools': {
|
|
862
|
-
rule('
|
|
867
|
+
rule(말('scr.tools'), 70);
|
|
868
|
+
/*
|
|
869
|
+
* 원본 schema.description 을 그대로 찍으면 안 된다.
|
|
870
|
+
*
|
|
871
|
+
* 그 글은 한국어다. 영어 표(tools/desc.en.js)는 **모델에게 줄 때만**
|
|
872
|
+
* 갈아 끼우고 있어서, 영어로 켠 사람의 `/tools` 는 스물한 줄이 통째로
|
|
873
|
+
* 한국어로 나왔다. `/lang` 은 그 사이에도 100% 라고 답했다 — 이 글들은
|
|
874
|
+
* 애초에 말 표에 들어간 적이 없어서 셈에 안 잡혔기 때문이다.
|
|
875
|
+
*
|
|
876
|
+
* 여기서는 **화면 말**(언어())로 고른다. 모델에게 주는 글의 말과는
|
|
877
|
+
* 다른 축이다 — 영어로 시키고 한국어로 보는 조합이 실제로 쓰인다.
|
|
878
|
+
*/
|
|
863
879
|
for (const [n, t] of Object.entries(TOOLS)) {
|
|
864
|
-
say(` ${c.cyan(pad(n, 8))} ${c.gray(t.schema.description)}`);
|
|
880
|
+
say(` ${c.cyan(pad(n, 8))} ${c.gray(영어설명(t.schema, n, 언어()).description)}`);
|
|
865
881
|
}
|
|
866
882
|
say('');
|
|
867
883
|
return { handled: true };
|
|
@@ -869,14 +885,41 @@ export async function handle(line, session, ctx) {
|
|
|
869
885
|
|
|
870
886
|
case 'cost': {
|
|
871
887
|
const mins = ((Date.now() - session.startedAt) / 60000).toFixed(1);
|
|
872
|
-
rule('
|
|
873
|
-
say(` ${c.gray(pad('
|
|
874
|
-
say(` ${c.gray(pad('
|
|
875
|
-
say(` ${c.gray(pad('
|
|
876
|
-
say(` ${c.gray(pad('
|
|
888
|
+
rule(말('scr.session'), 70);
|
|
889
|
+
say(` ${c.gray(pad(말('cost.calls'), 14))} ${말('unit.calls', { n: session.usage.calls })}`);
|
|
890
|
+
say(` ${c.gray(pad(말('cost.tokIn'), 14))} ${session.usage.in.toLocaleString()}`);
|
|
891
|
+
say(` ${c.gray(pad(말('cost.tokOut'), 14))} ${session.usage.out.toLocaleString()}`);
|
|
892
|
+
say(` ${c.gray(pad(말('cost.toolMs'), 14))} ${말('unit.sec', { n: (session.usage.ms / 1000).toFixed(1) })}`);
|
|
877
893
|
// 서버가 잠깐 막아 다시 부른 횟수. 0 이면 안 적는다 — 없는 일을 줄로 남기면 표만 길어진다.
|
|
878
|
-
if (session.usage.retries) say(` ${c.gray(pad(말('cost.retries'), 14))} ${session.usage.retries}
|
|
879
|
-
say(` ${c.gray(pad('
|
|
894
|
+
if (session.usage.retries) say(` ${c.gray(pad(말('cost.retries'), 14))} ${말('unit.calls', { n: session.usage.retries })}`);
|
|
895
|
+
say(` ${c.gray(pad(말('cost.elapsed'), 14))} ${말('unit.min', { n: mins })}`);
|
|
896
|
+
/*
|
|
897
|
+
* 돈.
|
|
898
|
+
*
|
|
899
|
+
* 이 도구에는 값이 박힌 요금표가 없다(backend/price.js). 지어낸 요금을
|
|
900
|
+
* 자신 있게 찍는 것보다 모른다고 말하는 편이 낫다고 봤다.
|
|
901
|
+
*
|
|
902
|
+
* 그래서 두 갈래다 — 아는 값이 있으면 금액과 **어디서 온 값인지**를
|
|
903
|
+
* 같이 적고, 모르면 어디에 무엇을 적으면 되는지를 적는다. 「모릅니다」
|
|
904
|
+
* 한 줄로 끝내면 사람이 할 수 있는 일이 없다.
|
|
905
|
+
*
|
|
906
|
+
* 로컬로만 쓰는 사람에게는 아예 안 띄운다. 로컬은 공짜라 그 자리가
|
|
907
|
+
* 영영 안 채워지고, 그러면 매번 쓸모없는 안내만 보게 된다.
|
|
908
|
+
*/
|
|
909
|
+
const 요금값 = 세션요금(session);
|
|
910
|
+
const 쓴돈 = 돈셈(session.usage, 요금값);
|
|
911
|
+
if (쓴돈) {
|
|
912
|
+
say(` ${c.gray(pad(말('cost.money'), 14))} ${c.bold(돈말(쓴돈.달러))}`
|
|
913
|
+
+ ` ${c.gray(`(${어디서온값(요금값)})`)}`);
|
|
914
|
+
} else if (바깥인가(session.conn.base) && (session.usage.in || session.usage.out)) {
|
|
915
|
+
const 붙은곳 = session.제공자 ? 제공자고르기(session.제공자) : null;
|
|
916
|
+
for (const 줄 of 요금적는법(session.conn.model, { 제공자: 붙은곳, 설정파일: configPath() })) {
|
|
917
|
+
say(` ${c.gray(줄)}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
// 설정에 적어는 뒀는데 숫자가 아니면 조용히 넘어가면 안 된다. 사람은
|
|
921
|
+
// 적었다고 믿고 있으므로, 안 나오는 까닭을 화면에서 알 수 있어야 한다.
|
|
922
|
+
for (const 탈 of 요금값?.탈들 ?? []) say(` ${c.yellow(mark.no)} ${c.gray(탈)}`);
|
|
880
923
|
/*
|
|
881
924
|
* 서버가 남았다고 말해 준 할당량 (backend/quota.js).
|
|
882
925
|
*
|
|
@@ -886,8 +929,8 @@ export async function handle(line, session, ctx) {
|
|
|
886
929
|
const 남은것 = 마지막할당량();
|
|
887
930
|
if (남은것) {
|
|
888
931
|
const 말줄 = 할당량말(남은것);
|
|
889
|
-
say(` ${c.gray(pad('
|
|
890
|
-
+ ` ${c.gray(`(${Math.round((Date.now() - 남은것.때) / 1000)}
|
|
932
|
+
say(` ${c.gray(pad(말('cost.quota'), 14))} ${아슬아슬한가(남은것) ? c.yellow(말줄) : 말줄}`
|
|
933
|
+
+ ` ${c.gray(`(${말('cost.quotaAge', { 초: Math.round((Date.now() - 남은것.때) / 1000) })})`)}`);
|
|
891
934
|
}
|
|
892
935
|
say('');
|
|
893
936
|
return { handled: true };
|
|
@@ -895,29 +938,46 @@ export async function handle(line, session, ctx) {
|
|
|
895
938
|
|
|
896
939
|
case 'status': {
|
|
897
940
|
const k = session.conn;
|
|
898
|
-
rule('
|
|
899
|
-
say(` ${c.gray(pad('
|
|
900
|
-
say(` ${c.gray(pad('
|
|
941
|
+
rule(말('scr.conn'), 70);
|
|
942
|
+
say(` ${c.gray(pad(말('status.kind'), 10))} ${규격이름(k.kind)}`);
|
|
943
|
+
say(` ${c.gray(pad(말('status.url'), 10))} ${k.base}`);
|
|
901
944
|
// 프록시를 거치면 어느 것을, 어디서 읽었는지(env · config)까지. 안 거치면 줄 자체가 없다.
|
|
902
945
|
// 적어 놨는데 못 쓰는 것(socks5 등)이면 그 까닭을 — 조용히 직접 가면 사람은 프록시를 탄 줄 안다.
|
|
903
946
|
const 프록시 = 프록시고르기(k.base);
|
|
904
|
-
if (프록시) say(` ${c.gray(pad('
|
|
905
|
-
else if (프록시설정().탈) say(` ${c.gray(pad('
|
|
906
|
-
say(` ${c.gray(pad('
|
|
907
|
-
say(` ${c.gray(pad('
|
|
947
|
+
if (프록시) say(` ${c.gray(pad(말('status.proxy'), 10))} ${프록시.url} ${c.gray(`(${프록시.출처})`)}`);
|
|
948
|
+
else if (프록시설정().탈) say(` ${c.gray(pad(말('status.proxy'), 10))} ${c.yellow(말('status.proxyOff'))} ${c.gray(`— ${프록시설정().탈}`)}`);
|
|
949
|
+
say(` ${c.gray(pad(말('status.model'), 10))} ${k.model}`);
|
|
950
|
+
say(` ${c.gray(pad(말('status.root'), 10))} ${session.root}`);
|
|
908
951
|
// 명령이 어느 셸에서 도는지. "ls 가 왜 안 되지" 의 답이 이 줄에 있다.
|
|
909
|
-
say(` ${c.gray(pad('
|
|
952
|
+
say(` ${c.gray(pad(말('status.shell'), 10))} ${정한셸().표시}`);
|
|
910
953
|
// 열쇠를 어디에 두고 있나. 사내 심사에서 제일 먼저 묻는 것이라
|
|
911
954
|
// 「어딘가 잠겨 있겠지」 로 두지 않고 지금 상태를 그대로 적는다.
|
|
912
|
-
|
|
913
|
-
|
|
955
|
+
/*
|
|
956
|
+
* 열쇠를 어디에 두고 있나 — 또는 **어디서 받아 오나.**
|
|
957
|
+
*
|
|
958
|
+
* 받아 오는 연결에서는 「보관」 을 적으면 거짓말이 된다. 우리는 그
|
|
959
|
+
* 열쇠를 갖고 있지 않다. 사내 심사에서 제일 먼저 묻는 자리라
|
|
960
|
+
* 있는 그대로 적는다 (safety/authcmd.js).
|
|
961
|
+
*/
|
|
962
|
+
if (session.conn.열쇠받기) {
|
|
963
|
+
const 상태 = 지금열쇠상태();
|
|
964
|
+
const 남은말 = 상태
|
|
965
|
+
? 말('auth.leftMin', { 분: Math.round(상태.남은초 / 60) })
|
|
966
|
+
: 말('auth.notYet');
|
|
967
|
+
say(` ${c.gray(pad(말('status.keyStore'), 10))} `
|
|
968
|
+
+ `${말(session.conn.열쇠받기.곳 === '정책' ? 'auth.fromPolicy' : 'auth.from')} ${c.gray(`· ${남은말}`)}`);
|
|
969
|
+
say(` ${c.gray(pad('', 10))} ${c.gray(clip(session.conn.열쇠받기.명령, 56))}`);
|
|
970
|
+
} else if (k.auth !== 'none') {
|
|
971
|
+
say(` ${c.gray(pad(말('status.keyStore'), 10))} ${열쇠보관(load())}`);
|
|
972
|
+
}
|
|
973
|
+
say(` ${c.gray(pad(말('status.rules'), 10))} ${session.rules ? session.rules.name : 말('status.noRules')}`);
|
|
914
974
|
const caps = [
|
|
915
|
-
k.tools ? c.green('
|
|
916
|
-
k.streaming ? c.green('
|
|
917
|
-
k.json ? c.green('
|
|
918
|
-
k.think ? c.green('
|
|
975
|
+
k.tools ? c.green(말('status.capTools')) : c.red(말('status.capTools')),
|
|
976
|
+
k.streaming ? c.green(말('status.capStream')) : c.gray(말('status.capStream')),
|
|
977
|
+
k.json ? c.green(말('status.capSchema')) : c.gray(말('status.capSchema')),
|
|
978
|
+
k.think ? c.green(말('status.capThink')) : c.gray(말('status.capThink')),
|
|
919
979
|
].join(c.gray(' · '));
|
|
920
|
-
say(` ${c.gray(pad('
|
|
980
|
+
say(` ${c.gray(pad(말('status.caps'), 10))} ${caps}`);
|
|
921
981
|
say('');
|
|
922
982
|
return { handled: true };
|
|
923
983
|
}
|
|
@@ -2188,8 +2248,8 @@ function 배움명령(session, ctx, arg = '') {
|
|
|
2188
2248
|
function showContext(session) {
|
|
2189
2249
|
const b = session.breakdown();
|
|
2190
2250
|
say('');
|
|
2191
|
-
rule('
|
|
2192
|
-
say(` ${c.bold(session.conn.model)} ${c.gray('·')} ${b.total.toLocaleString()}
|
|
2251
|
+
rule(말('scr.context'), 70);
|
|
2252
|
+
say(` ${c.bold(session.conn.model)} ${c.gray('·')} ${말('unit.tokens', { n: b.total.toLocaleString() })}`);
|
|
2193
2253
|
say('');
|
|
2194
2254
|
say(` ${bar(b.used, b.total, 32)} ${b.used.toLocaleString()} / ${b.total.toLocaleString()} ${c.gray(`${Math.round((b.used / b.total) * 100)}%`)}`);
|
|
2195
2255
|
say('');
|
|
@@ -2198,9 +2258,9 @@ function showContext(session) {
|
|
|
2198
2258
|
say(` ${c.gray(pad(r.label, 26))} ${pad(r.n.toLocaleString(), 8, 'right')}`);
|
|
2199
2259
|
}
|
|
2200
2260
|
say(` ${c.gray('─'.repeat(35))}`);
|
|
2201
|
-
say(` ${c.gray(pad('
|
|
2261
|
+
say(` ${c.gray(pad(말('ctx.left'), 26))} ${pad(b.left.toLocaleString(), 8, 'right')}`);
|
|
2202
2262
|
say('');
|
|
2203
|
-
say(` ${c.gray(
|
|
2263
|
+
say(` ${c.gray(`/compact ${말('ctx.compactHint')} /clear ${말('ctx.clearHint')}`)}`);
|
|
2204
2264
|
/*
|
|
2205
2265
|
* 추정이라고만 적어 두면 사람은 얼마나 믿어야 할지 모른다. 서버가 알려 준
|
|
2206
2266
|
* 실제값에 맞춰 가고 있으면 그 사실을 적는다 — '추정' 과 '맞춰 본 추정' 은
|
|
@@ -2208,9 +2268,9 @@ function showContext(session) {
|
|
|
2208
2268
|
*/
|
|
2209
2269
|
if (b.보정잰것 > 0) {
|
|
2210
2270
|
const 차이 = Math.round((b.보정 - 1) * 100);
|
|
2211
|
-
say(` ${c.gray(
|
|
2271
|
+
say(` ${c.gray(말('ctx.calibrated', { 부호: 차이 >= 0 ? '+' : '', 퍼센트: 차이, 번: b.보정잰것 }))}`);
|
|
2212
2272
|
} else {
|
|
2213
|
-
say(` ${c.gray('
|
|
2273
|
+
say(` ${c.gray(말('ctx.estimate'))}`);
|
|
2214
2274
|
}
|
|
2215
2275
|
say('');
|
|
2216
2276
|
}
|
|
@@ -2555,6 +2615,9 @@ function 연결적용(session, p) {
|
|
|
2555
2615
|
// 자물쇠도 같이 옮긴다. 이걸 빼먹으면 옛 주소가 열린 채로 남고 새 주소는 막혀
|
|
2556
2616
|
// 다음 한마디에서 바로 "허용되지 않은 주소" 가 난다.
|
|
2557
2617
|
allowEndpoint(p.baseUrl);
|
|
2618
|
+
// 어디 것인지도 같이 옮긴다. 요금표 주소 같은 안내가 옛 회사 것으로 남으면
|
|
2619
|
+
// 사람이 엉뚱한 요금표를 보러 간다.
|
|
2620
|
+
session.제공자 = p.제공자 ?? null;
|
|
2558
2621
|
}
|
|
2559
2622
|
|
|
2560
2623
|
/**
|
|
@@ -2616,7 +2679,7 @@ async function switchModel(session, ctx, arg = '') {
|
|
|
2616
2679
|
// 이름으로 바로 바꾸기 — 메뉴를 안 거친다.
|
|
2617
2680
|
if (말) {
|
|
2618
2681
|
const 찾은 = 이름으로찾기(cfg.profiles, 말);
|
|
2619
|
-
if (찾은.length === 1) return 골라적용(session, cfg, 찾은[0]);
|
|
2682
|
+
if (찾은.length === 1) return await 골라적용(session, cfg, 찾은[0]);
|
|
2620
2683
|
if (찾은.length > 1) {
|
|
2621
2684
|
say(` ${mark.warn} ${c.white(말)} ${c.gray('에 맞는 것이 여럿입니다.')}`);
|
|
2622
2685
|
for (const p of 찾은.slice(0, 12)) say(` ${c.cyan(p.name)} ${c.gray(p.model)}`);
|
|
@@ -2653,7 +2716,7 @@ async function switchModel(session, ctx, arg = '') {
|
|
|
2653
2716
|
ask: ctx?.ask,
|
|
2654
2717
|
});
|
|
2655
2718
|
if (i === cfg.profiles.length) return await 서버모델고르기(session, ctx, cfg);
|
|
2656
|
-
return 골라적용(session, cfg, cfg.profiles[i]);
|
|
2719
|
+
return await 골라적용(session, cfg, cfg.profiles[i]);
|
|
2657
2720
|
}
|
|
2658
2721
|
|
|
2659
2722
|
function 이름으로찾기(profiles, 말) {
|
|
@@ -2663,7 +2726,44 @@ function 이름으로찾기(profiles, 말) {
|
|
|
2663
2726
|
return profiles.filter((p) => `${p.name} ${p.model} ${p.id}`.toLowerCase().includes(q));
|
|
2664
2727
|
}
|
|
2665
2728
|
|
|
2666
|
-
|
|
2729
|
+
/*
|
|
2730
|
+
* 이 프로필로 갈아타면 바깥으로 나가는가. 나가면 한 번 묻는다.
|
|
2731
|
+
*
|
|
2732
|
+
* 켤 때 지나는 문(repl.js)과 **같은 문**이다. 여기를 안 지키면 자물쇠가
|
|
2733
|
+
* 반쪽이 된다 — 로컬로 켜서 물음을 지나친 다음, /model 로 바깥 프로필에
|
|
2734
|
+
* 갈아타는 순간 아무것도 안 묻고 나간다. 실제로 그렇게 쓴다: 한 시간쯤
|
|
2735
|
+
* 로컬로 하다가 「이건 큰 모델이 낫겠다」 하고 옮긴다.
|
|
2736
|
+
*
|
|
2737
|
+
* 허락은 프로필에 적힌다. 갈아탈 때마다 묻지 않는다.
|
|
2738
|
+
*/
|
|
2739
|
+
async function 나가도되나묻기(session, p) {
|
|
2740
|
+
const 모드 = session.실행모드 ?? 지금모드({});
|
|
2741
|
+
const 나감 = 나갈수있나(모드, { 바깥: 바깥인가(p.baseUrl), 허가: p.online === true });
|
|
2742
|
+
if (나감.되나) return true;
|
|
2743
|
+
|
|
2744
|
+
const 어디 = 주소가리기((() => { try { return new URL(p.baseUrl).host; } catch { return String(p.baseUrl); } })());
|
|
2745
|
+
if (!나감.물어볼까) {
|
|
2746
|
+
// 봉인이다. 여기서 바꿔 주면 다음 한마디에서 막히는데, 그 화면만 보고는
|
|
2747
|
+
// 왜 막혔는지 알 수 없다 — 바꾸기 전에 말하는 편이 언제나 낫다.
|
|
2748
|
+
say(` ${mark.warn} ${c.gray(`${어디} 는 이 컴퓨터 밖입니다. 지금은`)} ${c.white('봉인(--offline)')} ${c.gray('이라 안 바꿉니다.')}`);
|
|
2749
|
+
say('');
|
|
2750
|
+
return false;
|
|
2751
|
+
}
|
|
2752
|
+
say('');
|
|
2753
|
+
say(` ${c.yellow('↗')} ${c.bold(어디)} ${c.gray('는 이 컴퓨터 밖입니다.')}`);
|
|
2754
|
+
say(` ${c.gray('바꾸면 시킨 말과, 모델이 읽은 파일의 내용이 그리로 갑니다.')}`);
|
|
2755
|
+
const 예 = await confirm('나가도 될까요? (한 번 허락하면 이 연결은 다음부터 안 묻습니다)', true);
|
|
2756
|
+
if (!예) {
|
|
2757
|
+
say(` ${c.gray('안 바꿨습니다. 아무것도 안 보냈습니다.')}`);
|
|
2758
|
+
say('');
|
|
2759
|
+
return false;
|
|
2760
|
+
}
|
|
2761
|
+
p.online = true;
|
|
2762
|
+
return true;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
async function 골라적용(session, cfg, p) {
|
|
2766
|
+
if (!await 나가도되나묻기(session, p)) return;
|
|
2667
2767
|
cfg.active = p.id;
|
|
2668
2768
|
try { save(cfg); } catch { /* 못 남겨도 이번 세션에는 바뀐다 */ }
|
|
2669
2769
|
연결적용(session, p);
|
|
@@ -2691,6 +2791,9 @@ async function 모델만바꾸기(session, cfg, 모델) {
|
|
|
2691
2791
|
// 반대로 큰 데서 작은 데로 옮기면 긴 대화에서 서버가 거절한다. 아래에서 다시 잰다.
|
|
2692
2792
|
ctx: null,
|
|
2693
2793
|
};
|
|
2794
|
+
// 서버는 그대로지만 문은 같이 지난다. 등록 안 된 서버로 옮겨 가는 길도
|
|
2795
|
+
// 여기라서, 여기를 열어 두면 자물쇠에 구멍이 하나 남는다.
|
|
2796
|
+
if (!await 나가도되나묻기(session, p)) return;
|
|
2694
2797
|
if (!이미) { try { save(upsert(cfg, p)); } catch { /* 못 남겨도 이번 세션에는 바뀐다 */ } }
|
|
2695
2798
|
else { cfg.active = p.id; try { save(cfg); } catch {} }
|
|
2696
2799
|
연결적용(session, p);
|
package/src/completion.js
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { MODES } from './agent/modes.js';
|
|
25
25
|
import { LEVELS, PROFILES } from './agent/effort.js';
|
|
26
|
+
// 승인 모드(auto·confirm·strict). agent/modes.js 의 MODES 와 헷갈리기 쉬운데,
|
|
27
|
+
// 저쪽은 `--work` 가 받는 '무슨 일을 하는 중인가' 다. 실제로 한 번 뒤바뀌어
|
|
28
|
+
// 있었다 — `--mode` 를 누르면 code·plan 이 나오고 `--work` 는 폴더를 냈다.
|
|
29
|
+
import { 차례 as 승인모드 } from './ui/approve.js';
|
|
26
30
|
|
|
27
31
|
/** deel 이 받는 명령. bin/deel.js 의 switch 와 같아야 한다(검사가 지킨다). */
|
|
28
32
|
export const 명령들 = [
|
|
@@ -39,6 +43,7 @@ export const 명령들 = [
|
|
|
39
43
|
{ 이름: 'scan', 뜻: '게이트웨이에 어떤 모델이 있나 훑는다', en: 'scan the gateway for available models' },
|
|
40
44
|
{ 이름: 'sessions', 뜻: '지난 대화 목록', en: 'list past conversations' },
|
|
41
45
|
{ 이름: 'ls', 뜻: 'sessions 와 같다', en: 'same as sessions' },
|
|
46
|
+
{ 이름: 'reset', 뜻: '설정·기억·기록을 지운다 (그냥 치면 보여만 준다)', en: 'wipe settings, memory and records (bare: just shows)' },
|
|
42
47
|
{ 이름: 'completion', 뜻: '탭 완성 스크립트를 낸다', en: 'print a tab-completion script' },
|
|
43
48
|
{ 이름: 'version', 뜻: '판 번호', en: 'version number' },
|
|
44
49
|
{ 이름: 'help', 뜻: '도움말', en: 'help' },
|
|
@@ -55,15 +60,29 @@ export const 숨은명령 = ['-p'];
|
|
|
55
60
|
*/
|
|
56
61
|
export const 깃발들 = [
|
|
57
62
|
{ 이름: '--root', 뜻: '작업 폴더', en: 'working folder', 값: '폴더' },
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
/*
|
|
64
|
+
* 이 둘은 한 번 뒤바뀌어 있었다.
|
|
65
|
+
*
|
|
66
|
+
* `--mode` 는 **승인 모드**(auto·confirm·strict)인데 작업 모드 목록이
|
|
67
|
+
* 붙어 있었고, `--work` 는 **작업 모드**인데 「살림 폴더」 라고 적혀 폴더를
|
|
68
|
+
* 완성해 주고 있었다. 그래서 `deel --mode <탭>` 을 누르면 받지도 않는
|
|
69
|
+
* code·plan 이 나왔다.
|
|
70
|
+
*
|
|
71
|
+
* 검사도 이걸 못 잡았다 — 완성 목록이 agent/modes.js 에서 온 것이 맞나만
|
|
72
|
+
* 봤기 때문이다. 베낀 자리가 맞는지는 봤는데, 베껴 온 곳이 맞는지는 안 봤다.
|
|
73
|
+
* 지금은 도움말에 적힌 값과 맞춰 본다 (test/completion.test.js).
|
|
74
|
+
*/
|
|
75
|
+
{ 이름: '--work', 뜻: '무슨 일을 하는 중인가', en: 'what kind of work this is', 값: Object.keys(MODES) },
|
|
76
|
+
{ 이름: '--mode', 뜻: '무엇까지 물어보지 않고 할까', en: 'how much it may do without asking', 값: 승인모드 },
|
|
60
77
|
{ 이름: '--level', 뜻: '화면을 얼마나 자세히', en: 'how much detail on screen', 값: null },
|
|
61
78
|
{ 이름: '--ctx', 뜻: '창 크기 (예: 32k)', en: 'context window size (e.g. 32k)', 값: null },
|
|
62
79
|
{ 이름: '--max-tokens', 뜻: '한 번에 뱉을 최대 토큰', en: 'max tokens per reply', 값: null },
|
|
63
80
|
{ 이름: '--think', 뜻: '생각의 양', en: 'how much thinking', 값: LEVELS },
|
|
64
81
|
{ 이름: '--effort', 뜻: '생각을 어디에 몰아줄까', en: 'where to spend the thinking budget', 값: Object.keys(PROFILES) },
|
|
82
|
+
{ 이름: '--online', 뜻: '묻지 않고 바깥으로 나간다', en: 'go outside without asking', 값: false },
|
|
65
83
|
{ 이름: '--offline', 뜻: '바깥으로 아예 안 나간다', en: 'never reach the network', 값: false },
|
|
66
84
|
{ 이름: '--yes', 뜻: '물어보지 않고 진행', en: 'do not ask, just proceed', 값: false },
|
|
85
|
+
{ 이름: '--hard', 뜻: 'reset all 에서 되돌리기·감사기록까지', en: 'with reset all: undo snapshots and the audit log too', 값: false },
|
|
67
86
|
{ 이름: '--json', 뜻: '결과를 JSON 으로', en: 'output JSON', 값: false },
|
|
68
87
|
{ 이름: '--quiet', 뜻: '군말 없이', en: 'no chatter', 값: false },
|
|
69
88
|
{ 이름: '--tui', 뜻: '입력 상자를 켠다', en: 'force the input box on', 값: false },
|