deel-local-cli 2.1.2 → 2.1.4
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 +16 -6
- package/README.md +17 -6
- package/bin/deel.js +15 -1
- package/package.json +1 -1
- package/src/acp/serve.js +4 -0
- package/src/agent/loop.js +38 -2
- package/src/agent/route.js +67 -1
- package/src/agent/session.js +13 -1
- package/src/backend/adapter.js +26 -4
- package/src/backend/loosejson.js +109 -0
- package/src/backend/mcp.js +586 -41
- package/src/backend/probe.js +17 -3
- package/src/backend/textcalls.js +196 -0
- package/src/commands.js +26 -37
- package/src/completion.js +2 -0
- package/src/exit.js +3 -1
- package/src/i18n/en.js +5 -0
- package/src/i18n/ja.js +5 -0
- package/src/i18n/ko.js +5 -0
- package/src/i18n/zh.js +5 -0
- package/src/login.js +268 -0
- package/src/lsp/diag.js +2 -1
- package/src/oneshot.js +143 -5
- package/src/preview/serve.js +2 -31
- package/src/providers/custom.js +1 -1
- package/src/providers/index.js +2 -1
- package/src/providers/openrouter.js +62 -0
- package/src/repl.js +13 -1
- package/src/runlog.js +99 -0
- package/src/safety/authcmd.js +68 -17
- package/src/safety/gitsnap.js +286 -0
- package/src/safety/undo.js +30 -1
- package/src/safety/which.js +37 -0
- package/src/setup.js +62 -6
- package/src/slash.js +103 -0
- package/src/tools/importgraph.js +107 -0
- package/src/tools/index.js +24 -1
- package/src/tools/outline.js +21 -3
- package/src/tools/verify.js +103 -45
- package/src/ui/browser.js +81 -0
package/src/login.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 브라우저로 로그인 — 열쇠를 붙여 넣지 않고 계정으로 받아 온다 (2.1.4).
|
|
3
|
+
*
|
|
4
|
+
* 제공자 데이터에 `링크로그인: { 여는곳, 바꾸는곳, 열쇠이름 }` 이 있으면 이 흐름을 탄다
|
|
5
|
+
* (지금은 openrouter.js 하나). OAuth PKCE 한 바퀴다:
|
|
6
|
+
*
|
|
7
|
+
* 1. 검증자(무작위 32바이트)를 짓고, 그 sha256 을 도전으로 싣은 로그인 주소를 브라우저로 연다.
|
|
8
|
+
* 2. 로그인이 끝나면 로그인 쪽이 코드를 준다 — 이 PC 의 콜백 문으로, 또는 화면에 (원격).
|
|
9
|
+
* 3. 코드 + 검증자를 바꾸는곳에 POST 하면 열쇠가 온다.
|
|
10
|
+
*
|
|
11
|
+
* ── 열쇠는 우리만 받을 수 있다 ─────────────────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* 코드만으로는 열쇠가 안 나온다 — 검증자가 있어야 한다. 검증자는 브라우저·콜백 길로는 한 번도
|
|
14
|
+
* 안 지나고(그 길로 가는 것은 해시인 도전뿐), 교환 창구에 HTTPS 로 한 번 직접 간다. 다른
|
|
15
|
+
* 프로그램이나 웹 페이지가 콜백에 제 코드를 끼워 넣어도, 그 코드를 낸 도전은 우리 것이 아니라
|
|
16
|
+
* 교환에서 403 이다. 콜백 길도 판마다 새로 지은 16바이트라 밖에서 맞혀 부를 수가 없다.
|
|
17
|
+
*
|
|
18
|
+
* ── 나가는 문 ───────────────────────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* 우리가 직접 나가는 것은 **교환 POST 한 번**이다. http.js 의 req() — 나가는 문 하나 — 로 가고,
|
|
21
|
+
* 자물쇠는 그 동안만 바꾸는곳 하나를 연다(allowTemporarily). 로그인 화면은 사람의 브라우저가
|
|
22
|
+
* 연다. 봉인(offline)이면 둘 다 안 한다 — 브라우저도 안 열고 묻지도 않는다.
|
|
23
|
+
*/
|
|
24
|
+
import { createServer } from 'node:http';
|
|
25
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
26
|
+
import { req, serverMessage } from './backend/http.js';
|
|
27
|
+
import { allowTemporarily } from './safety/network.js';
|
|
28
|
+
import { 바깥인가 } from './safety/runmode.js';
|
|
29
|
+
import { 브라우저로 } from './ui/browser.js';
|
|
30
|
+
import { ask } from './ui/prompt.js';
|
|
31
|
+
import { say, c, clip } from './ui/ansi.js';
|
|
32
|
+
|
|
33
|
+
/** 코드는 10분 뒤 만료된다(OpenRouter). 그 절반을 기다린다. */
|
|
34
|
+
export const 기다림기본 = 5 * 60_000;
|
|
35
|
+
|
|
36
|
+
/** PKCE 한 쌍. 검증자는 RFC 7636 글자(base64url) 43자, 도전은 그 sha256 의 base64url. */
|
|
37
|
+
export function PKCE만들기() {
|
|
38
|
+
const 검증자 = randomBytes(32).toString('base64url');
|
|
39
|
+
const 도전 = createHash('sha256').update(검증자).digest('base64url');
|
|
40
|
+
return { 검증자, 도전 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 브라우저가 이 PC 에 없을 것 같은가.
|
|
45
|
+
*
|
|
46
|
+
* SSH 로 들어왔거나, 리눅스인데 화면(X · Wayland)이 없으면 그렇다고 본다 — 컨테이너·원격 서버.
|
|
47
|
+
* 그때는 콜백 문을 열어 봐야 브라우저가 못 닿으므로, 로그인 쪽이 코드를 화면에 띄우게 하고
|
|
48
|
+
* 사람이 붙여 넣는다. WSL 은 윈도 브라우저가 localhost 로 닿으므로 이 자리로 친다.
|
|
49
|
+
*
|
|
50
|
+
* 틀려도 갇히지 않는다: 이 자리로 잘못 봤으면 `deel setup --no-browser` 로 붙여넣기 길을 간다.
|
|
51
|
+
* 원격으로 잘못 봤으면 붙여넣기 길이 어디서나 되므로 코드만 옮기면 된다.
|
|
52
|
+
*/
|
|
53
|
+
export function 원격인가(env = process.env, platform = process.platform) {
|
|
54
|
+
if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY) return true;
|
|
55
|
+
if (platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY && !env.WSL_DISTRO_NAME) return true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 사람이 붙여 넣은 것에서 코드를 꺼낸다. 못 꺼내면 null.
|
|
61
|
+
*
|
|
62
|
+
* 코드만 붙여도, 주소창의 주소를 통째로(`…?code=…`) 붙여도 된다. 주소 속 코드는 퍼센트
|
|
63
|
+
* 인코딩돼 있으므로 풀어서 준다 — 안 풀면 `%2B` 가 그대로 가서 403 이다.
|
|
64
|
+
*/
|
|
65
|
+
export function 코드꺼내기(글) {
|
|
66
|
+
const s = String(글 ?? '').trim();
|
|
67
|
+
if (!s) return null;
|
|
68
|
+
// 조각(#) 뒤에 실려 오는 꼴도 받는다.
|
|
69
|
+
const 걸림 = /(?:^|[?&#\s])code=([^&#\s]+)/.exec(s);
|
|
70
|
+
if (걸림) {
|
|
71
|
+
let 코드;
|
|
72
|
+
try { 코드 = decodeURIComponent(걸림[1]); } catch { return null; }
|
|
73
|
+
return 코드 && !/[\s\x00-\x1f]/.test(코드) ? 코드 : null;
|
|
74
|
+
}
|
|
75
|
+
// 코드 없이 온 주소·문장은 코드가 아니다(로그인 쪽 주소를 잘못 붙였거나, 거절하고 돌아온 주소).
|
|
76
|
+
// `=` 는 받는다 — base64 패딩이 붙은 코드가 있다.
|
|
77
|
+
return /^[^\s/:?&#]+$/.test(s) ? s : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* ── 콜백 문: 127.0.0.1 과 ::1 에 같은 포트로 ────────────────────────────
|
|
82
|
+
*
|
|
83
|
+
* 콜백 주소는 `localhost` 다(OpenRouter 가 아무 포트나 받는 이름). 브라우저는 그것을 127.0.0.1
|
|
84
|
+
* 로도 ::1 로도 풀 수 있어서 둘 다에 듣는다. 같은 포트의 ::1 을 **남이 쥐고 있으면** 그 포트는
|
|
85
|
+
* 버리고 새로 고른다 — 안 그러면 ::1 로 푼 브라우저가 코드를 그 남에게 넘기고, 우리는 5분을
|
|
86
|
+
* 기다리다 끝난다(2.1.4 Gemini 검토). IPv6 가 아예 없는 기계는 127.0.0.1 하나로 간다.
|
|
87
|
+
*/
|
|
88
|
+
function 듣기(서버, 포트, 호스트) {
|
|
89
|
+
return new Promise((풀기, 버리기) => {
|
|
90
|
+
const 탈 = (e) => { 서버.off('listening', 됨); 버리기(e); };
|
|
91
|
+
// 듣기 시작한 뒤의 서버 오류(받기 중 EMFILE 따위)는 삼킨다 — 받이가 없으면 프로세스가 죽는다.
|
|
92
|
+
// 로그인은 기다림이 끝나면 저절로 실패로 끝난다.
|
|
93
|
+
const 됨 = () => { 서버.off('error', 탈); 서버.on('error', () => {}); 풀기(); };
|
|
94
|
+
서버.once('error', 탈);
|
|
95
|
+
서버.once('listening', 됨);
|
|
96
|
+
서버.listen(포트, 호스트);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/*
|
|
100
|
+
* 닫는다. 쉬는 연결(브라우저가 미리 열어 둔 것)은 바로 닫고, 아직 답을 보내는 연결은 조금 기다렸다가
|
|
101
|
+
* 끊는다 — 답이 나가기 전에 끊으면 브라우저에는 「로그인됐습니다」 대신 연결 끊김이 뜬다.
|
|
102
|
+
*/
|
|
103
|
+
function 다닫기(서버들) {
|
|
104
|
+
return Promise.all(서버들.map((s) => new Promise((풀기) => {
|
|
105
|
+
try {
|
|
106
|
+
s.close(() => 풀기());
|
|
107
|
+
s.closeIdleConnections?.();
|
|
108
|
+
setTimeout(() => s.closeAllConnections?.(), 1000).unref();
|
|
109
|
+
} catch { 풀기(); }
|
|
110
|
+
})));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @returns {Promise<{포트: number, 귀들: number, 닫기: () => Promise<void>}>} */
|
|
114
|
+
export async function 콜백귀열기(처리, { 첫포트 = 0, 시도 = 8 } = {}) {
|
|
115
|
+
let 포트 = 첫포트;
|
|
116
|
+
for (let n = 0; n < 시도; n++) {
|
|
117
|
+
const 넷 = createServer(처리);
|
|
118
|
+
try { await 듣기(넷, 포트, '127.0.0.1'); } catch { 포트 = 0; continue; }
|
|
119
|
+
const p = 넷.address().port;
|
|
120
|
+
const 여섯 = createServer(처리);
|
|
121
|
+
let 탈 = null;
|
|
122
|
+
try { await 듣기(여섯, p, '::1'); } catch (e) { 탈 = e; }
|
|
123
|
+
if (!탈) return { 포트: p, 귀들: 2, 닫기: () => 다닫기([넷, 여섯]) };
|
|
124
|
+
if (탈.code === 'EADDRINUSE') { await 다닫기([넷]); 포트 = 0; continue; }
|
|
125
|
+
return { 포트: p, 귀들: 1, 닫기: () => 다닫기([넷]) };
|
|
126
|
+
}
|
|
127
|
+
const 넷 = createServer(처리);
|
|
128
|
+
await 듣기(넷, 0, '127.0.0.1');
|
|
129
|
+
return { 포트: 넷.address().port, 귀들: 1, 닫기: () => 다닫기([넷]) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 브라우저에 돌려줄 한 쪽. 코드는 안 싣는다. 이 쪽 주소에 코드가 있으므로 남에게 안 넘기고
|
|
133
|
+
// (no-referrer) 어디에도 안 남긴다(no-store). 밖의 것을 하나도 안 불러온다.
|
|
134
|
+
function 쪽보내기(res, 됐나) {
|
|
135
|
+
const 몸 = `<!doctype html><html lang="ko"><meta charset="utf-8"><title>deel</title>`
|
|
136
|
+
+ `<h1>${됐나 ? '로그인됐습니다' : '로그인이 안 됐습니다'}</h1>`
|
|
137
|
+
+ `<p>${됐나 ? '터미널로 돌아가세요. 이 창은 닫아도 됩니다.' : '터미널로 돌아가 다시 해 보세요.'}</p></html>`;
|
|
138
|
+
const buf = Buffer.from(몸, 'utf8');
|
|
139
|
+
res.writeHead(됐나 ? 200 : 400, {
|
|
140
|
+
'content-type': 'text/html; charset=utf-8',
|
|
141
|
+
'content-length': buf.length,
|
|
142
|
+
'cache-control': 'no-store',
|
|
143
|
+
'referrer-policy': 'no-referrer',
|
|
144
|
+
'content-security-policy': "default-src 'none'",
|
|
145
|
+
connection: 'close',
|
|
146
|
+
});
|
|
147
|
+
res.end(buf);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const 이름만 = (제공자) => String(제공자?.이름 ?? '').replace(/\s*\(.*\)\s*$/, '') || '로그인';
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 로그인해서 열쇠를 받아 온다. 던지지 않는다.
|
|
154
|
+
*
|
|
155
|
+
* @param 제공자 `링크로그인` 칸이 있는 제공자
|
|
156
|
+
* @param o.봉인 offline 이 켜져 있나. 켜져 있고 로그인 창구가 바깥이면 아무것도 안 한다.
|
|
157
|
+
* @param o.원격 브라우저가 이 PC 에 없나 — 그러면 콜백 대신 코드를 붙여 넣게 한다.
|
|
158
|
+
* @param o.열기 주소를 브라우저로 여는 것 (검사가 브라우저 흉내로 갈아 끼운다)
|
|
159
|
+
* @param o.받기 한 줄 묻기 (붙여넣기 길)
|
|
160
|
+
* @returns {Promise<{ok: true, 열쇠: string} | {ok: false, 까닭: string}>}
|
|
161
|
+
*/
|
|
162
|
+
export async function 링크로그인(제공자, {
|
|
163
|
+
봉인 = false, 원격 = 원격인가(), 열기 = 브라우저로, 받기 = (물음) => ask(물음), 기다림 = 기다림기본, 말 = say,
|
|
164
|
+
} = {}) {
|
|
165
|
+
const 곳 = 제공자?.링크로그인;
|
|
166
|
+
if (!곳?.여는곳 || !곳?.바꾸는곳) return { ok: false, 까닭: '이 제공자는 브라우저 로그인을 열지 않습니다.' };
|
|
167
|
+
if (봉인 && (바깥인가(곳.여는곳) || 바깥인가(곳.바꾸는곳))) {
|
|
168
|
+
return { ok: false, 까닭: '봉인되어 있어 로그인하러 나가지 않습니다 — 관리 정책이나 설정에 offline 이 켜져 있습니다.' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { 검증자, 도전 } = PKCE만들기();
|
|
172
|
+
const 로그인주소 = (콜백) => {
|
|
173
|
+
const u = new URL(곳.여는곳);
|
|
174
|
+
if (콜백) u.searchParams.set('callback_url', 콜백);
|
|
175
|
+
u.searchParams.set('code_challenge', 도전);
|
|
176
|
+
u.searchParams.set('code_challenge_method', 'S256');
|
|
177
|
+
if (곳.열쇠이름) u.searchParams.set('key_label', 곳.열쇠이름);
|
|
178
|
+
return u.href;
|
|
179
|
+
};
|
|
180
|
+
const 누구 = 이름만(제공자);
|
|
181
|
+
|
|
182
|
+
let 코드;
|
|
183
|
+
if (원격) {
|
|
184
|
+
// 콜백 없이 가면 로그인 쪽이 코드를 화면에 띄운다. 어느 기기의 브라우저로 열어도 된다.
|
|
185
|
+
말('');
|
|
186
|
+
말(` ${c.gray(`아래 주소를 아무 기기의 브라우저로 여세요. ${누구} 에 로그인하면 코드가 뜹니다.`)}`);
|
|
187
|
+
말(` ${로그인주소(null)}`);
|
|
188
|
+
말('');
|
|
189
|
+
const 받은것 = await 받기('로그인 뒤 뜬 코드 (주소째 붙여도 됩니다)');
|
|
190
|
+
코드 = 코드꺼내기(받은것);
|
|
191
|
+
if (!코드) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
까닭: String(받은것 ?? '').trim()
|
|
195
|
+
? '붙여 넣은 것에서 코드를 못 찾았습니다 — 로그인 뒤 화면에 뜬 코드만, 또는 code= 가 든 주소를 넣으세요.'
|
|
196
|
+
: '코드가 비었습니다 — 아무것도 안 바꿨습니다.',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
const 비밀길 = `/deel-${randomBytes(16).toString('hex')}`;
|
|
201
|
+
let 알리기 = null;
|
|
202
|
+
const 돌아옴 = new Promise((풀기) => { 알리기 = 풀기; });
|
|
203
|
+
const 처리 = (rq, rs) => {
|
|
204
|
+
let u = null;
|
|
205
|
+
try { u = new URL(rq.url, 'http://localhost'); } catch { /* 아래에서 404 */ }
|
|
206
|
+
if (rq.method !== 'GET' || u?.pathname !== 비밀길) {
|
|
207
|
+
rs.writeHead(404, { 'content-type': 'text/plain; charset=utf-8', connection: 'close' });
|
|
208
|
+
rs.end('not found');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const 받은코드 = u.searchParams.get('code');
|
|
212
|
+
// 쪽이 다 나간 뒤에 받는다 — 받자마자 문을 닫으면 쪽이 끊긴다.
|
|
213
|
+
// 처음 온 것 하나만 받는다 — 새로고침·미리 불러오기로 두 번 와도 약속은 한 번만 풀린다.
|
|
214
|
+
rs.once('finish', () => 알리기(받은코드 ? { 코드: 받은코드 } : { 오류: u.searchParams.get('error') || '코드 없이 돌아왔습니다' }));
|
|
215
|
+
쪽보내기(rs, !!받은코드);
|
|
216
|
+
};
|
|
217
|
+
let 귀;
|
|
218
|
+
try { 귀 = await 콜백귀열기(처리); }
|
|
219
|
+
catch (e) { return { ok: false, 까닭: `로그인을 받을 문을 못 열었습니다 — ${e?.message ?? e}. deel setup --no-browser 로 해 보세요.` }; }
|
|
220
|
+
let 타이머 = null;
|
|
221
|
+
try {
|
|
222
|
+
const 주소 = 로그인주소(`http://localhost:${귀.포트}${비밀길}`);
|
|
223
|
+
말('');
|
|
224
|
+
말(` ${c.gray(`브라우저에서 ${누구} 로그인을 엽니다. 안 열리면 이 주소를 여세요:`)}`);
|
|
225
|
+
말(` ${주소}`);
|
|
226
|
+
말(` ${c.gray(`로그인을 기다립니다 (${Math.max(1, Math.round(기다림 / 60000))}분 · 그만두려면 Ctrl+C).`)}`);
|
|
227
|
+
말(` ${c.gray('브라우저가 다른 기기에 있으면 Ctrl+C 뒤')} ${c.cyan('deel setup --no-browser')}`);
|
|
228
|
+
try { 열기(주소); } catch { /* 주소는 화면에 있다 */ }
|
|
229
|
+
const 결과 = await Promise.race([
|
|
230
|
+
돌아옴,
|
|
231
|
+
new Promise((풀기) => { 타이머 = setTimeout(() => 풀기({ 시간: true }), 기다림); }),
|
|
232
|
+
]);
|
|
233
|
+
if (결과.시간) {
|
|
234
|
+
return { ok: false, 까닭: '로그인이 안 돌아와 기다리는 시간이 지났습니다 — 아무것도 안 바꿨습니다. 다시 하려면 deel setup' };
|
|
235
|
+
}
|
|
236
|
+
if (!결과.코드) return { ok: false, 까닭: `로그인하고 돌아오지 않았습니다 — ${clip(String(결과.오류), 120)}` };
|
|
237
|
+
코드 = 결과.코드;
|
|
238
|
+
} finally {
|
|
239
|
+
clearTimeout(타이머);
|
|
240
|
+
await 귀.닫기();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── 코드 → 열쇠 ────────────────────────────────────────────────────────
|
|
245
|
+
let r;
|
|
246
|
+
const 닫기 = allowTemporarily(곳.바꾸는곳);
|
|
247
|
+
try {
|
|
248
|
+
r = await req(곳.바꾸는곳, {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
headers: { 'content-type': 'application/json' },
|
|
251
|
+
body: { code: 코드, code_verifier: 검증자, code_challenge_method: 'S256' },
|
|
252
|
+
timeout: 20000,
|
|
253
|
+
});
|
|
254
|
+
} catch (e) {
|
|
255
|
+
// 자물쇠(NetBlocked)·끊음만 던진다. 통신 실패는 r.ok=false 로 온다.
|
|
256
|
+
return { ok: false, 까닭: `열쇠로 못 바꿨습니다 — ${e?.message ?? e}` };
|
|
257
|
+
} finally {
|
|
258
|
+
닫기();
|
|
259
|
+
}
|
|
260
|
+
if (!r.ok) {
|
|
261
|
+
return { ok: false, 까닭: `열쇠로 못 바꿨습니다 (${r.status ? `HTTP ${r.status}` : '안 닿음'}) — ${clip(serverMessage(r).replace(/\s+/g, ' '), 160)}` };
|
|
262
|
+
}
|
|
263
|
+
const 열쇠 = r.json?.key;
|
|
264
|
+
if (typeof 열쇠 !== 'string' || !열쇠 || /\s/.test(열쇠)) {
|
|
265
|
+
return { ok: false, 까닭: `로그인은 됐는데 열쇠가 안 왔습니다 — ${누구} 쪽 답이 뜻밖의 꼴입니다. 열쇠를 직접 붙여 넣어 주세요.` };
|
|
266
|
+
}
|
|
267
|
+
return { ok: true, 열쇠 };
|
|
268
|
+
}
|
package/src/lsp/diag.js
CHANGED
|
@@ -100,7 +100,8 @@ export async function 편집후진단(뿌리, abs, { 시한 = 기다릴시한 }
|
|
|
100
100
|
*/
|
|
101
101
|
export function 붙이기(결과, 진단, 보일이름) {
|
|
102
102
|
if (!진단 || (!진단.오류 && !진단.경고)) return 결과;
|
|
103
|
-
|
|
103
|
+
// 언어 서버가 못 준 자리에서 문법만 본 것(tools/verify.js 의 한파일문법)은 그렇게 적는다 — 뜻까지 본 것처럼 읽히면 안 된다.
|
|
104
|
+
const 머리 = `${진단.출처 ? `문법 검사(${진단.출처})` : '언어 서버'} — ${보일이름}: `
|
|
104
105
|
+ [진단.오류 ? `오류 ${진단.오류}개` : '', 진단.경고 ? `경고 ${진단.경고}개` : ''].filter(Boolean).join(' · ');
|
|
105
106
|
return {
|
|
106
107
|
...결과,
|
package/src/oneshot.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// 그래서 '묻는 자리' 를 전부 없앤 길을 따로 낸다. 에이전트 루프는 그대로 쓴다 —
|
|
12
12
|
// 여기서 루프를 다시 짜면 두 벌이 되고, 언젠가 한쪽만 고쳐진다.
|
|
13
13
|
import { statSync } from 'node:fs';
|
|
14
|
+
import { resolve } from 'node:path';
|
|
14
15
|
import { c, mark, clip, 화면글거르기 } from './ui/ansi.js';
|
|
15
16
|
import { 규칙모으기, 정책읽기, 승인바닥 } from './safety/policy.js';
|
|
16
17
|
import { 훅읽기 } from './safety/hooks.js';
|
|
@@ -19,7 +20,7 @@ import { 스키마읽기, 맞나, 답에서JSON뽑기, 시킬말 as 스키마시
|
|
|
19
20
|
import { 남길것읽기 } from './safety/shellenv.js';
|
|
20
21
|
import { 받기설정 } from './safety/authcmd.js';
|
|
21
22
|
import { run } from './agent/loop.js';
|
|
22
|
-
import { Session, 요청잘리나,
|
|
23
|
+
import { Session, 요청잘리나, 못박을길이, 생성속도, repairToolPairs } from './agent/session.js';
|
|
23
24
|
import { 양수크기 } from './agent/models.js';
|
|
24
25
|
import { estimateTokens } from './backend/tokens.js';
|
|
25
26
|
import { makeScope } from './safety/guard.js';
|
|
@@ -31,12 +32,14 @@ import { activeProfile, load, resolveKey, 소식줄들 } from './config.js';
|
|
|
31
32
|
import { 말 as 옮긴말 } from './i18n/index.js';
|
|
32
33
|
import { 알림채움, 알림말 } from './backend/retry.js';
|
|
33
34
|
import { 전선붙이기, 세션이름짓기 } from './backend/wire.js';
|
|
34
|
-
import { newId } from './agent/store.js';
|
|
35
|
+
import { newId, Store, latest, prune } from './agent/store.js';
|
|
36
|
+
import { 못박기 } from './agent/pins.js';
|
|
37
|
+
import { 바뀐파일모음, 사건적기열기 } from './runlog.js';
|
|
35
38
|
import { discover, loadCommand } from './skills/discover.js';
|
|
36
39
|
import { 다붙이기 } from './backend/mcp.js';
|
|
37
40
|
import { 명령들 } from './cmdnames.js';
|
|
38
|
-
// 슬래시 명령을 찾는 규칙은 대화 화면과 **같은 것 하나**를 쓴다 (
|
|
39
|
-
import { 슬래시명령찾기,
|
|
41
|
+
// 슬래시 명령을 찾는 규칙은 대화 화면과 **같은 것 하나**를 쓴다 (slash.js 의 슬래시명령찾기 머리말).
|
|
42
|
+
import { 슬래시명령찾기, 비슷한슬래시명령, MCP프롬프트펴기 } from './slash.js';
|
|
40
43
|
import { allowEndpoint, setOffline } from './safety/network.js';
|
|
41
44
|
import { 지금모드, 바깥인가, 나갈수있나, 봉인됐나 } from './safety/runmode.js';
|
|
42
45
|
import { 주소가리기 } from './safety/secrets.js';
|
|
@@ -124,8 +127,12 @@ export async function runOnce(opts = {}) {
|
|
|
124
127
|
let 알려진모델 = null;
|
|
125
128
|
// 붙인 MCP 서버. 아래 내놓기 가 닫는다 — 그래서 여기서 선언한다 (2.0.2 · 문 맞춤).
|
|
126
129
|
let mcp서버들 = [];
|
|
130
|
+
// --events 로 연 사건 파일 (runlog.js). 아래 내놓기 가 결과 줄을 적고 닫는다 (2.1.3).
|
|
131
|
+
let 사건 = null;
|
|
127
132
|
|
|
128
133
|
const 내놓기 = (r) => {
|
|
134
|
+
// 사건 파일의 마지막 줄은 --json 과 같은 결과 한 덩이다. 어느 끝맺음이든 여기를 지난다.
|
|
135
|
+
if (사건) { 사건.결과(r); 사건.닫기(); }
|
|
129
136
|
/*
|
|
130
137
|
* 뒤에서 돌던 명령을 반드시 거둔다.
|
|
131
138
|
*
|
|
@@ -234,6 +241,30 @@ export async function runOnce(opts = {}) {
|
|
|
234
241
|
if (opts.checkTimeout !== undefined && !(Number.isFinite(Number(opts.checkTimeout)) && Number(opts.checkTimeout) > 0)) {
|
|
235
242
|
return 못함('usage', `--check-timeout 은 0 보다 큰 초여야 합니다: ${opts.checkTimeout}`);
|
|
236
243
|
}
|
|
244
|
+
/*
|
|
245
|
+
* 값 없는 --resume (2.1.3). 대화 화면은 목록을 띄워 고르게 하지만 여기는 고를 사람이 없다 —
|
|
246
|
+
* 아무거나 집어 이으면 스크립트는 제가 무엇을 이었는지 모른다. 치는 자리에서 고칠 것이라 64 다.
|
|
247
|
+
*/
|
|
248
|
+
if (opts.resume === true) {
|
|
249
|
+
return 못함('usage', '--resume 에 이어 할 대화 이름을 주세요 (deel sessions 로 확인) — 가장 최근 것이면 --continue');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/*
|
|
253
|
+
* ── 사건 흐름 (--events <파일>) · 2.1.3 ──────────────────────────────
|
|
254
|
+
*
|
|
255
|
+
* 일을 시작하기 **전에** 연다. 다 돌고 나서 못 열었다고 하면 지켜보려던 사람은 아무것도 못 봤다.
|
|
256
|
+
* 경로는 --output-schema 처럼 부른 자리 기준이다.
|
|
257
|
+
*/
|
|
258
|
+
if (opts.events != null) {
|
|
259
|
+
const 경로 = resolve(String(opts.events));
|
|
260
|
+
try {
|
|
261
|
+
사건 = 사건적기열기(경로, {
|
|
262
|
+
알림: (왜) => 삐끗(` ${mark.warn} ${c.yellow(`사건 파일에 더 못 적습니다 (${왜}) — 일은 계속합니다`)}`),
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
return 못함('events', `사건 파일을 못 열었습니다 (--events): ${경로} — ${err?.code ?? err?.message ?? err}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
237
268
|
|
|
238
269
|
// ── 시킬 말 ───────────────────────────────────────────────────────────
|
|
239
270
|
// 인자로 준 것이 먼저다. 없을 때만 표준입력을 읽는다 —
|
|
@@ -252,6 +283,41 @@ export async function runOnce(opts = {}) {
|
|
|
252
283
|
* 고친다 — acp/serve.js 의 방만들기가 이미 고친 바로 그 섞임이다.
|
|
253
284
|
*/
|
|
254
285
|
const root = opts.root ? String(opts.root) : process.cwd();
|
|
286
|
+
|
|
287
|
+
/*
|
|
288
|
+
* ── 이어 할 대화 (--continue · --resume <이름>) · 2.1.3 ────────────────
|
|
289
|
+
*
|
|
290
|
+
* 여태 `deel run` 은 오간 말을 어디에도 안 남겼다. 스크립트가 「방금 그 일에 이어서 테스트도 고쳐」 를
|
|
291
|
+
* 시킬 길이 없어 매번 처음부터 설명했고, 모델은 방금 읽은 파일을 다시 읽었다. 이제 대화 화면과 같은
|
|
292
|
+
* 자리(.deel/sessions)에 적고, 같은 두 깃발로 잇는다 — 대화 화면에서 한 대화를 배치가 이어도 된다.
|
|
293
|
+
*
|
|
294
|
+
* 설정을 읽기 **전에** 찾는다. 없는 이름이면 모델을 부르기 전에 서야 한다. 대화 화면은 못 찾으면
|
|
295
|
+
* 새로 시작하지만 여기서는 선다 — 이름을 집어 준 스크립트는 그 대화에 이어진다고 믿고 「아까 그것」 을
|
|
296
|
+
* 시킨다. 처음 보는 모델이 그 말을 받으면 짐작으로 일을 하고 0 으로 끝난다. `--continue` 는 다르다 —
|
|
297
|
+
* 「있으면 잇고 없으면 새로」 가 스크립트의 첫 판이 기대하는 뜻이다.
|
|
298
|
+
*/
|
|
299
|
+
let 이어받은것 = null;
|
|
300
|
+
{
|
|
301
|
+
const 이을이름 = typeof opts.resume === 'string' ? opts.resume
|
|
302
|
+
: (opts.continue === true ? (latest(root)?.id ?? null) : null);
|
|
303
|
+
if (opts.continue === true && typeof opts.resume !== 'string' && !이을이름) {
|
|
304
|
+
곁(` ${c.gray('· 이어 할 대화가 없어 새로 시작합니다.')}`);
|
|
305
|
+
}
|
|
306
|
+
if (이을이름) {
|
|
307
|
+
const 곳 = new Store(root, 이을이름);
|
|
308
|
+
const { messages: 적힌것, 못읽음 } = 곳.이름틀림 ? { messages: [] } : 곳.load();
|
|
309
|
+
// 도구가 도는 중에 죽은 대화는 호출만 있고 결과가 없다 — 그대로 보내면 서버가 400 을 낸다 (repl.js 와 같다).
|
|
310
|
+
const { messages, 고친것 } = repairToolPairs(적힌것);
|
|
311
|
+
if (!messages.length) {
|
|
312
|
+
return 못함('no-session', 곳.이름틀림
|
|
313
|
+
? `대화 이름이 아닙니다 (--resume): ${이을이름}`
|
|
314
|
+
: 못읽음
|
|
315
|
+
? `그 대화를 못 읽었습니다 (--resume): ${이을이름} — ${못읽음}`
|
|
316
|
+
: `그런 대화가 없거나 비어 있습니다 (--resume): ${이을이름} — deel sessions 로 이름을 확인하세요`);
|
|
317
|
+
}
|
|
318
|
+
이어받은것 = { 곳, messages, 고친것 };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
255
321
|
/*
|
|
256
322
|
* 설정을 못 읽어도 **이 문의 끝맺음으로** 끝낸다.
|
|
257
323
|
*
|
|
@@ -361,6 +427,20 @@ export async function runOnce(opts = {}) {
|
|
|
361
427
|
// session 에 실어 두는 까닭은, 대화 도중 /model 로 옮겨도 같은 자리를 보게 하려는 것이다.
|
|
362
428
|
session.실행모드 = 실행모드;
|
|
363
429
|
|
|
430
|
+
/*
|
|
431
|
+
* 찾아 둔 대화를 싣는다 (위 「이어 할 대화」). 못 박은 것 · 남은 할 일 · 시킨 말 원문도 같이 —
|
|
432
|
+
* 오간 말만 돌아오고 그 셋이 빠지면 대화 화면에서 막아 둔 「껐다 켜면 까먹는다」 가 여기서 되살아난다.
|
|
433
|
+
* 적는 자리(store)는 아래 턴 바로 앞에서 연다 — 그 사이에 선 실패가 빈 대화 파일을 남기지 않게.
|
|
434
|
+
*/
|
|
435
|
+
if (이어받은것) {
|
|
436
|
+
session.messages = 이어받은것.messages;
|
|
437
|
+
const 박힌것 = 이어받은것.곳.못박은것읽기();
|
|
438
|
+
if (박힌것.length) session.못박은것 = new 못박기(박힌것);
|
|
439
|
+
이어받은것.곳.살림따라가기(session);
|
|
440
|
+
곁(` ${mark.ok} ${c.gray(`${이어받은것.곳.id} — 메시지 ${이어받은것.messages.length}개를 이어 받았습니다.`)}`
|
|
441
|
+
+ (이어받은것.고친것 ? ` ${c.gray(`(중단된 도구 호출 ${이어받은것.고친것}개는 걷어냈습니다)`)}` : ''));
|
|
442
|
+
}
|
|
443
|
+
|
|
364
444
|
/*
|
|
365
445
|
* ── 열쇠를 받아 오는 명령 (safety/authcmd.js) ───────────────────────
|
|
366
446
|
*
|
|
@@ -484,7 +564,13 @@ export async function runOnce(opts = {}) {
|
|
|
484
564
|
* 두 벌이니 한쪽만 고치면 또 갈린다. 그래서 규칙은 commands.js 한 군데에만 둔다.
|
|
485
565
|
*/
|
|
486
566
|
const 찾은 = 붙박이 ? null : 슬래시명령찾기(found.commands, 부른이름);
|
|
487
|
-
|
|
567
|
+
// MCP 서버가 낸 프롬프트도 대화 화면과 같이 부른다 (2.1.3 · commands.js 의 MCP프롬프트펴기).
|
|
568
|
+
const 프롬프트 = 붙박이 || 찾은 ? null : await MCP프롬프트펴기(mcp서버들, 부른이름, 인자);
|
|
569
|
+
if (프롬프트?.탈) return 못함('command-read', `/${부른이름} — ${프롬프트.탈}`);
|
|
570
|
+
if (프롬프트) {
|
|
571
|
+
곁(` ${c.cyan('⌘')} ${부른이름} ${c.gray(`MCP 프롬프트 · ${프롬프트.서버이름}`)}`);
|
|
572
|
+
시킬말 = 프롬프트.text;
|
|
573
|
+
} else if (붙박이) {
|
|
488
574
|
return 못함('repl-only', `/${부른이름} 은 대화 화면에서만 도는 명령입니다.`
|
|
489
575
|
+ ' 여기서 되는 것은 파일로 적어 둔 슬래시 명령뿐입니다 (.claude/commands · .deel/commands).');
|
|
490
576
|
} else if (찾은) {
|
|
@@ -673,6 +759,28 @@ export async function runOnce(opts = {}) {
|
|
|
673
759
|
}
|
|
674
760
|
}
|
|
675
761
|
|
|
762
|
+
/*
|
|
763
|
+
* ── 오간 말을 남긴다 (2.1.3) ─────────────────────────────────────────
|
|
764
|
+
*
|
|
765
|
+
* 대화 화면처럼 걸음마다 적는다. 끝에 한 번에 적으면 걸음 수 상한이나 Ctrl+C 로 선 잡의 대화가
|
|
766
|
+
* 통째로 없다 — 이어서 시킬 일이 제일 많은 게 바로 그 잡이다. 여기까지 온 뒤에 연다 — 위에서 선
|
|
767
|
+
* 실패(없는 명령 · 너무 큰 말)가 머리글만 있는 빈 대화를 남기지 않게.
|
|
768
|
+
*/
|
|
769
|
+
const store = 이어받은것?.곳 ?? new Store(root);
|
|
770
|
+
store.begin({ model: conn.model, base: 주소가리기(conn.base), root });
|
|
771
|
+
if (!이어받은것) store.살림따라가기(session);
|
|
772
|
+
// 지금 이어 쓰는 대화는 정리에서 뺀다 — 한 달 넘은 대화를 이으면 바로 여기서 지워졌다 (store.js 의 prune 머리말).
|
|
773
|
+
try { prune(root, { 남길것: [store.id] }); } catch { /* 못 치워도 이 잡은 돈다 — 다음 판이 다시 해 본다 */ }
|
|
774
|
+
let 적은데까지 = session.messages.length; // 이어 받은 말은 이미 그 파일에 있다
|
|
775
|
+
const 적기 = () => {
|
|
776
|
+
// 접기 · 비우기가 이력을 통째로 바꿨는데 그 사건을 못 봤으면(모양 되물음 도중) 덧붙이지 말고 새로 적는다.
|
|
777
|
+
if (session.messages.length < 적은데까지) store.replace(session.messages, '압축');
|
|
778
|
+
else for (const m of session.messages.slice(적은데까지)) store.append(m);
|
|
779
|
+
적은데까지 = session.messages.length;
|
|
780
|
+
};
|
|
781
|
+
// 무엇이 바뀌었나 — --json 의 files (runlog.js).
|
|
782
|
+
const 바뀐 = 바뀐파일모음(root);
|
|
783
|
+
|
|
676
784
|
// ── 한 턴 ─────────────────────────────────────────────────────────────
|
|
677
785
|
const turn = new AbortController();
|
|
678
786
|
const 끊김 = () => { if (!turn.signal.aborted) turn.abort(); };
|
|
@@ -697,10 +805,13 @@ export async function runOnce(opts = {}) {
|
|
|
697
805
|
const 안쪽 = (ev) => (ev.depth ? ' '.repeat(ev.depth) : '');
|
|
698
806
|
|
|
699
807
|
for await (const ev of run(session, ctx, 보낼글, { signal: turn.signal })) {
|
|
808
|
+
사건?.적기(ev); // 지켜보는 쪽에 먼저 흘린다 (--events)
|
|
809
|
+
바뀐.사건(ev);
|
|
700
810
|
switch (ev.type) {
|
|
701
811
|
case 'stage':
|
|
702
812
|
steps = ev.step;
|
|
703
813
|
이번단계글 = '';
|
|
814
|
+
적기(); // 걸음마다 적어 둔다 — 도중에 죽어도 여기까지는 남는다
|
|
704
815
|
break;
|
|
705
816
|
|
|
706
817
|
// 중간 단계의 말은 표준출력에 안 싣는다. "이제 파일을 읽어보겠습니다" 까지
|
|
@@ -711,6 +822,7 @@ export async function runOnce(opts = {}) {
|
|
|
711
822
|
|
|
712
823
|
case 'tool': {
|
|
713
824
|
tools++;
|
|
825
|
+
적기();
|
|
714
826
|
/*
|
|
715
827
|
* 실패는 실패로 보여야 한다.
|
|
716
828
|
*
|
|
@@ -817,6 +929,9 @@ export async function runOnce(opts = {}) {
|
|
|
817
929
|
? ` ${c.cyan('◱')} ${c.yellow(`요약을 못 받아 옛 대화 ${ev.folded}개를 잘라 냈습니다 (${ev.before.toLocaleString()} → ${ev.after.toLocaleString()} 토큰)`)}`
|
|
818
930
|
+ (ev.why ? ` ${c.gray(`— ${clip(String(ev.why), 100)}`)}` : '')
|
|
819
931
|
: ` ${c.cyan('◱')} ${c.gray(`대화 ${ev.folded}개를 요약으로 접었습니다 (${ev.before.toLocaleString()} → ${ev.after.toLocaleString()} 토큰)`)}`);
|
|
932
|
+
// 접히면 이력이 통째로 바뀐다. 덧붙이면 접히기 전과 뒤가 겹쳐 남으니 새로 적는다 (repl.js 와 같다).
|
|
933
|
+
store.replace(session.messages, ev.fallback ? `압축 못 함 — 옛 대화 ${ev.folded}개를 잘라 냄` : `압축 — ${ev.folded}개를 요약으로`);
|
|
934
|
+
적은데까지 = session.messages.length;
|
|
820
935
|
break;
|
|
821
936
|
|
|
822
937
|
case 'compact_failed':
|
|
@@ -875,6 +990,10 @@ export async function runOnce(opts = {}) {
|
|
|
875
990
|
} else 곁(` ${c.yellow('⊘')} ${c.gray(옮긴말('check.skipped', { 까닭: ev.까닭 }))}`);
|
|
876
991
|
break;
|
|
877
992
|
|
|
993
|
+
case 'textcalls':
|
|
994
|
+
곁(` ${c.gray(`↳ ${옮긴말('ev.textCalls', { n: ev.count, 이름: ev.names.join(', ') })}`)}`);
|
|
995
|
+
break;
|
|
996
|
+
|
|
878
997
|
case 'nudge':
|
|
879
998
|
곁(` ${c.gray(`↺ ${ev.why === '요청누락'
|
|
880
999
|
? 옮긴말('ev.nudgeMissed', { n: ev.빠진?.length ?? 0 })
|
|
@@ -915,6 +1034,8 @@ export async function runOnce(opts = {}) {
|
|
|
915
1034
|
곁(` ${c.cyan('◱')} ${c.gray((ev.kept?.할일 ?? 0)
|
|
916
1035
|
? 옮긴말('ev.reset', { 버린수: ev.dropped, 할일: ev.kept.할일 })
|
|
917
1036
|
: 옮긴말('ev.resetBare', { 버린수: ev.dropped }))}`);
|
|
1037
|
+
store.replace(session.messages, `자리부족 — 앞선 대화 ${ev.dropped}개를 비움`);
|
|
1038
|
+
적은데까지 = session.messages.length;
|
|
918
1039
|
/*
|
|
919
1040
|
* ── 비우면서 **시킨 말 뒤가 잘렸으면** 여기서 선다 (사냥5 B5-01) ────────────
|
|
920
1041
|
*
|
|
@@ -1050,6 +1171,7 @@ export async function runOnce(opts = {}) {
|
|
|
1050
1171
|
let 다시터짐 = '';
|
|
1051
1172
|
try {
|
|
1052
1173
|
for await (const ev of run(session, ctx, 스키마시킬말(출력스키마, { 다시: 잰것.탈 }), { signal: turn.signal, 고쳐쓰기: true })) {
|
|
1174
|
+
사건?.적기(ev);
|
|
1053
1175
|
if (ev.type === 'content') 다시글 += ev.text;
|
|
1054
1176
|
else if (ev.type === 'done') 다시글 = ev.text ?? 다시글;
|
|
1055
1177
|
else if (ev.type === 'aborted') { 다시끊김 = true; break; }
|
|
@@ -1115,12 +1237,22 @@ export async function runOnce(opts = {}) {
|
|
|
1115
1237
|
*/
|
|
1116
1238
|
process.removeListener('SIGINT', 끊김);
|
|
1117
1239
|
|
|
1240
|
+
/*
|
|
1241
|
+
* 남은 말을 다 적고, 못 적은 것이 있으면 **말한다.** 이어 하려던 스크립트가 다음 판에서야
|
|
1242
|
+
* 「그 대화가 반쪽이다」 를 알면 늦다 — 그래서 못 적은 판은 --json 의 session 을 비운다.
|
|
1243
|
+
*/
|
|
1244
|
+
try { 적기(); } catch { /* 셈은 store 가 한다 — 아래에서 본다 */ }
|
|
1245
|
+
const 저장탈 = store.처음못쓴것();
|
|
1246
|
+
if (저장탈) 삐끗(` ${mark.warn} ${c.yellow(옮긴말('run.notSaving', { n: 저장탈.수, 까닭: 저장탈.까닭 }))}`);
|
|
1247
|
+
const 대화이름 = store.이름틀림 || store.못쓴것() ? null : store.id;
|
|
1248
|
+
|
|
1118
1249
|
const code = EXIT[reason] ?? EXIT.error;
|
|
1119
1250
|
if (why) 삐끗(` ${reason === 'done' ? c.gray('·') : c.red('✗')} ${why}`);
|
|
1120
1251
|
if (!json && !quiet) {
|
|
1121
1252
|
const 조각 = [`${((Date.now() - t0) / 1000).toFixed(1)}초`];
|
|
1122
1253
|
if (tools) 조각.push(`도구 ${tools}회`);
|
|
1123
1254
|
조각.push(`↑${(session.usage.prompt || session.usage.in).toLocaleString()} ↓${session.usage.out.toLocaleString()}`);
|
|
1255
|
+
if (생성속도(session.usage) !== null) 조각.push(`${생성속도(session.usage)} tok/s`);
|
|
1124
1256
|
곁(` ${c.gray('── ' + 조각.join(' · '))}`);
|
|
1125
1257
|
}
|
|
1126
1258
|
|
|
@@ -1146,8 +1278,14 @@ export async function runOnce(opts = {}) {
|
|
|
1146
1278
|
// 이 칸이 없으면 받는 쪽이 0 을 실측으로 읽는다 (backend/adapter.js 의 잰것).
|
|
1147
1279
|
못잰것: session.usage.못잰것 ?? 0,
|
|
1148
1280
|
retries: session.usage.retries ?? 0,
|
|
1281
|
+
// 생성 속도 (2.1.3). 흘려받은 부름만 잰다 — 못 쟀으면 null (session.js 의 생성속도).
|
|
1282
|
+
genMs: session.usage.genMs ?? 0,
|
|
1283
|
+
tokPerSec: 생성속도(session.usage),
|
|
1149
1284
|
},
|
|
1150
1285
|
model: conn.model,
|
|
1286
|
+
// 바뀐 파일 (작업 폴더 기준 · 슬래시) 과 이 대화의 이름 — `--resume <이름>` 으로 잇는다 (2.1.3).
|
|
1287
|
+
files: 바뀐.목록(),
|
|
1288
|
+
session: 대화이름,
|
|
1151
1289
|
// 모양을 못 박았을 때만 실린다. --json 으로 받는 쪽은 text 를 다시 파싱할
|
|
1152
1290
|
// 필요 없이 이 칸을 그대로 쓰면 된다.
|
|
1153
1291
|
...(스키마값 !== null ? { schema: 스키마값 } : {}),
|
package/src/preview/serve.js
CHANGED
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
import { createServer } from 'node:http';
|
|
28
28
|
import { createReadStream, statSync, existsSync, readdirSync, watch, realpathSync, openSync, readSync, closeSync } from 'node:fs';
|
|
29
29
|
import { join, extname, relative, sep, isAbsolute, basename } from 'node:path';
|
|
30
|
-
import { spawn } from 'node:child_process';
|
|
31
30
|
import { 내부살림 } from '../tools/fsutil.js';
|
|
32
31
|
import { detect } from '../tools/encoding.js';
|
|
33
32
|
|
|
@@ -541,33 +540,5 @@ function 목록주기(abs, 뿌리, res) {
|
|
|
541
540
|
res.end(buf);
|
|
542
541
|
}
|
|
543
542
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
*
|
|
547
|
-
* 실패해도 조용히 넘어간다 — 주소는 이미 화면에 찍혀 있으므로 손으로 열면 된다.
|
|
548
|
-
* 여기서 오류를 띄우면 '띄우기는 됐는데 실패한 것처럼' 보인다.
|
|
549
|
-
*/
|
|
550
|
-
export function 브라우저로(url) {
|
|
551
|
-
/*
|
|
552
|
-
* 열면 안 되는 자리가 있다.
|
|
553
|
-
*
|
|
554
|
-
* 검사가 돌 때마다 진짜 브라우저 창이 뜨면 사람 화면이 난장판이 된다.
|
|
555
|
-
* 파이프로 넘길 때(로그·캡처)도 열 이유가 없다 — 볼 사람이 없다.
|
|
556
|
-
* 상자 검사는 isTTY 를 거짓말하게 만들어 자식을 띄우므로, TTY 만 봐서는
|
|
557
|
-
* 못 막는다. 그래서 env 로도 막을 수 있게 뒀고 검사 돌리개가 그걸 켠다.
|
|
558
|
-
*/
|
|
559
|
-
if (process.env.DEEL_NO_OPEN) return false;
|
|
560
|
-
if (!process.stdout.isTTY) return false;
|
|
561
|
-
try {
|
|
562
|
-
const [cmd, args] = process.platform === 'win32'
|
|
563
|
-
// start 는 cmd 안엣말이다. 첫 따옴표 한 쌍은 창 제목 자리라 비워 둬야 한다.
|
|
564
|
-
? ['cmd', ['/d', '/s', '/c', 'start', '', url]]
|
|
565
|
-
: process.platform === 'darwin' ? ['open', [url]] : ['xdg-open', [url]];
|
|
566
|
-
const kid = spawn(cmd, args, { stdio: 'ignore', detached: process.platform !== 'win32', windowsHide: true });
|
|
567
|
-
kid.on('error', () => { /* 없으면 그만 */ });
|
|
568
|
-
kid.unref();
|
|
569
|
-
return true;
|
|
570
|
-
} catch {
|
|
571
|
-
return false;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
543
|
+
// 기본 브라우저로 여는 것은 ui/browser.js 로 옮겼다 — 로그인(2.1.4)도 같은 문을 쓴다.
|
|
544
|
+
export { 브라우저로 } from '../ui/browser.js';
|
package/src/providers/custom.js
CHANGED
package/src/providers/index.js
CHANGED
|
@@ -39,11 +39,12 @@
|
|
|
39
39
|
import { 제공자 as openai } from './openai.js';
|
|
40
40
|
import { 제공자 as anthropic } from './anthropic.js';
|
|
41
41
|
import { 제공자 as gemini } from './gemini.js';
|
|
42
|
+
import { 제공자 as openrouter } from './openrouter.js';
|
|
42
43
|
import { 제공자 as bedrock } from './bedrock.js';
|
|
43
44
|
import { 제공자 as 직접 } from './custom.js';
|
|
44
45
|
|
|
45
46
|
/** 화면에 보일 차례. 「직접 넣기」 는 언제나 목록에 있고, 화면에서는 위에 둔다. */
|
|
46
|
-
export const 제공자들 = [openai, anthropic, gemini, bedrock, 직접];
|
|
47
|
+
export const 제공자들 = [openai, anthropic, gemini, openrouter, bedrock, 직접];
|
|
47
48
|
|
|
48
49
|
/** id 로 하나 집는다. 모르면 null — 지어내지 않는다. */
|
|
49
50
|
export function 제공자고르기(id) {
|