deel-local-cli 1.20.9 → 1.20.10
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 +3 -2
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/agent/effort.js +25 -2
- package/src/agent/evidence.js +12 -2
- package/src/agent/loop.js +3 -1
- package/src/agent/session.js +1 -1
- package/src/backend/ctxsize.js +17 -3
- package/src/commands.js +96 -13
- package/src/configexplain.js +15 -3
- package/src/repl.js +369 -24
- package/src/tools/index.js +11 -1
- package/src/ui/export.js +5 -2
- package/src/ui/pastechip.js +3 -0
- package/src/ui/status.js +20 -2
package/README.ko.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
22
22
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
23
|
-
[](docs/ko/develop.md)
|
|
24
24
|
|
|
25
25
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
26
26
|
[](package.json)
|
|
@@ -301,7 +301,7 @@ deel --offline
|
|
|
301
301
|
무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
|
|
302
302
|
|
|
303
303
|
```
|
|
304
|
-
deel 1.20.
|
|
304
|
+
deel 1.20.10 ⌂ 이 안
|
|
305
305
|
보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
|
|
306
306
|
```
|
|
307
307
|
|
|
@@ -1289,6 +1289,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1289
1289
|
|
|
1290
1290
|
| 판 | 무엇이 바뀌었나 |
|
|
1291
1291
|
|---|---|
|
|
1292
|
+
| [1.20.10](docs/ko/releases/1.20.md#12010) | 답하지 않았는데 답한 것이 되어 있었습니다 |
|
|
1292
1293
|
| [1.20.9](docs/ko/releases/1.20.md#1209) | 안 보이는 것이 아니라 다른 것이 보이고 있었습니다 |
|
|
1293
1294
|
| [1.20.8](docs/ko/releases/1.20.md#1208) | 못 쟀다는 것을 안 된다고 적고 있었습니다 |
|
|
1294
1295
|
| [1.20.7](docs/ko/releases/1.20.md#1207) | 서버가 말해 준 것과 반대를 배우고 있었습니다 |
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Vendor APIs connect too — **only when you say so**
|
|
|
20
20
|
|
|
21
21
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
22
22
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
23
|
-
[](docs/en/develop.md)
|
|
24
24
|
|
|
25
25
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
26
26
|
[](package.json)
|
|
@@ -374,7 +374,7 @@ deel --offline
|
|
|
374
374
|
The destination is printed at the top of every session:
|
|
375
375
|
|
|
376
376
|
```
|
|
377
|
-
deel 1.20.
|
|
377
|
+
deel 1.20.10 ⌂ inside
|
|
378
378
|
Sends to this machine 127.0.0.1:11434 ← nowhere else
|
|
379
379
|
```
|
|
380
380
|
|
|
@@ -1350,6 +1350,7 @@ so one run tells you everything.
|
|
|
1350
1350
|
|
|
1351
1351
|
| Version | What changed |
|
|
1352
1352
|
|---|---|
|
|
1353
|
+
| [1.20.10](docs/en/releases/1.20.md#12010) | Nobody answered, yet an answer had been given |
|
|
1353
1354
|
| [1.20.9](docs/en/releases/1.20.md#1209) | Nothing was missing from the screen — something else was on it |
|
|
1354
1355
|
| [1.20.8](docs/en/releases/1.20.md#1208) | We were writing down "can't" where the truth was "couldn't measure" |
|
|
1355
1356
|
| [1.20.7](docs/en/releases/1.20.md#1207) | We were learning the opposite of what the server said |
|
package/package.json
CHANGED
package/src/agent/effort.js
CHANGED
|
@@ -101,9 +101,32 @@ export const MAX_CAP = 16384;
|
|
|
101
101
|
|
|
102
102
|
const ALIAS = { 균일: 'even', 절약: 'save', 깊게: 'deep', uniform: 'even', thrifty: 'save' };
|
|
103
103
|
|
|
104
|
+
/*
|
|
105
|
+
* 배분 이름을 표의 열쇠로 되돌린다.
|
|
106
|
+
*
|
|
107
|
+
* ── 있는지 없는지는 hasOwn 으로만 묻는다 ──────────────────────────────
|
|
108
|
+
*
|
|
109
|
+
* `PROFILES[k] ?` 는 **없는 이름에도 참**이 된다. 모든 객체가 물려받는
|
|
110
|
+
* `constructor` · `toString` · `valueOf` 가 그 자리다. 그래서:
|
|
111
|
+
*
|
|
112
|
+
* /think 배분 constructor
|
|
113
|
+
* -> 「모르는 배분입니다」 가 아니라 `✓ 배분 Object — undefined` 가 찍힌다.
|
|
114
|
+
* session.effort 에 'constructor' 가 박히고, 곧바로 단계표를 그리다
|
|
115
|
+
* p.shift[stage] 에서 터진다.
|
|
116
|
+
* /think 배분 toString
|
|
117
|
+
* -> ALIAS 쪽이 **함수**를 돌려준다. `?? null` 은 함수를 못 거른다.
|
|
118
|
+
* PROFILES[함수] 는 undefined 라 이름을 찍다 그 자리에서 터진다.
|
|
119
|
+
*
|
|
120
|
+
* 터지는 것만 문제가 아니다. 앞엣것은 **✓ 를 먼저 찍는다.** 사람은 배분을
|
|
121
|
+
* 바꿨다고 믿고, 그 판의 남은 턴은 전부 걸음 예산 계산에서 터진다.
|
|
122
|
+
* modes.js · /mode · /grade · ui/level.js 에서 같은 함정을 이미 닫았는데
|
|
123
|
+
* 여기만 옛 모양으로 남아 있었다.
|
|
124
|
+
*/
|
|
104
125
|
export function normalizeProfile(v) {
|
|
105
|
-
const
|
|
106
|
-
|
|
126
|
+
const 친것 = String(v ?? '').trim();
|
|
127
|
+
const k = 친것.toLowerCase();
|
|
128
|
+
if (Object.hasOwn(PROFILES, k)) return k;
|
|
129
|
+
return Object.hasOwn(ALIAS, 친것) ? ALIAS[친것] : null;
|
|
107
130
|
}
|
|
108
131
|
|
|
109
132
|
// 기준 강도에서 몇 칸 옮긴다. 끝을 넘지 않는다.
|
package/src/agent/evidence.js
CHANGED
|
@@ -176,13 +176,23 @@ export function 증거글(증거, { 제목 = '작업 증거' } = {}) {
|
|
|
176
176
|
* 파일로 남긴다. 화면은 스크롤로 사라지고, 검토는 나중에 다른 사람이 한다.
|
|
177
177
|
*
|
|
178
178
|
* @returns {string|null} 적은 자리. 못 적었으면 null — 못 적어도 대화는 계속된다.
|
|
179
|
+
*
|
|
180
|
+
* 못 적은 **까닭**은 버리지 않는다. 디스크가 찼는지, `.deel` 이 읽기 전용인지,
|
|
181
|
+
* 경로가 너무 긴지는 화면에 적혀야 사람이 손을 쓸 수 있다. 이 저장소의
|
|
182
|
+
* 저장시도()(config.js)가 이미 그렇게 한다 — 여기만 옛 모양이었다.
|
|
183
|
+
* 받을 그릇을 주면 거기 `왜` 를 담는다. 안 주면 여태처럼 조용히 null 이다.
|
|
184
|
+
*
|
|
185
|
+
* @param {{왜?: string}} [탈받을것]
|
|
179
186
|
*/
|
|
180
|
-
export function 증거적기(root, 증거, 이름 = '증거') {
|
|
187
|
+
export function 증거적기(root, 증거, 이름 = '증거', 탈받을것 = null) {
|
|
181
188
|
try {
|
|
182
189
|
const dir = join(root, '.deel', '증거');
|
|
183
190
|
mkdirSync(dir, { recursive: true });
|
|
184
191
|
const 자리 = join(dir, `${String(이름).replace(/[^\w가-힣.-]/g, '_')}.md`);
|
|
185
192
|
writeFileSync(자리, 증거글(증거), 'utf8');
|
|
186
193
|
return 자리;
|
|
187
|
-
} catch {
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (탈받을것) 탈받을것.왜 = String(e?.message ?? e);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
188
198
|
}
|
package/src/agent/loop.js
CHANGED
|
@@ -1523,7 +1523,9 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
1523
1523
|
continue;
|
|
1524
1524
|
}
|
|
1525
1525
|
|
|
1526
|
-
|
|
1526
|
+
// 물려받은 이름(`constructor` 등)이면 여기가 참이 되어 「모르는 도구」
|
|
1527
|
+
// 관문을 그냥 통과했다. hasOwn 으로만 묻는다 (tools/index.js 도 같다).
|
|
1528
|
+
if (!Object.hasOwn(TOOLS, call.name)) {
|
|
1527
1529
|
거절(call, `모르는 도구입니다. 쓸 수 있는 것: ${Object.keys(TOOLS).join(', ')}`);
|
|
1528
1530
|
// 이것도 세야 한다. 없는 이름을 계속 부르며 걸음 수를 다 쓰는 길이 있었다 —
|
|
1529
1531
|
// 71초 증상과 겉모습이 똑같은데 문만 다르다. 아래 거부·중복도 마찬가지다.
|
package/src/agent/session.js
CHANGED
|
@@ -262,7 +262,7 @@ export class Session {
|
|
|
262
262
|
* 것만큼 나쁜 것이 없다. repl 이 턴이 끝날 때 한 번씩 채워 준다.
|
|
263
263
|
*/
|
|
264
264
|
this.되돌릴턴 = 0;
|
|
265
|
-
this.검증 = { 돈횟수: 0, 확인: 0, 탈: 0 };
|
|
265
|
+
this.검증 = { 돈횟수: 0, 확인: 0, 탈: 0, 못확인: 0 };
|
|
266
266
|
this.skills = []; // 켜질 때 이 PC 에서 찾은 것들
|
|
267
267
|
/*
|
|
268
268
|
* 이 자리에 언어 서버가 있나 (Def·Refs 를 목록에 넣을지).
|
package/src/backend/ctxsize.js
CHANGED
|
@@ -187,7 +187,10 @@ export async function probeCtx(conn, { timeout = 6000 } = {}) {
|
|
|
187
187
|
tried.push({ label, url, status: r.status ?? 0, ok: !!r.ok });
|
|
188
188
|
return { label, json: r.ok ? r.json : null };
|
|
189
189
|
} catch (err) {
|
|
190
|
-
|
|
190
|
+
// **누가 막았나**를 같이 적는다. 우리 문지기(safety/network.js)가 막은 것과
|
|
191
|
+
// 서버가 죽은 것은 사람이 할 일이 전혀 다르다 — 앞엣것은 허락을 주면 되고
|
|
192
|
+
// 뒤엣것은 서버를 봐야 한다. 아래에서 이 표시로 갈라 말한다.
|
|
193
|
+
tried.push({ label, url, status: 0, ok: false, why: String(err?.message ?? err), 막힘: err?.name === 'NetBlocked' });
|
|
191
194
|
return { label, json: null };
|
|
192
195
|
}
|
|
193
196
|
};
|
|
@@ -247,9 +250,20 @@ export async function probeCtx(conn, { timeout = 6000 } = {}) {
|
|
|
247
250
|
|
|
248
251
|
// 실제로 쓸 값: 올려 둔 길이가 있으면 그것. 없으면 모델 최대.
|
|
249
252
|
const value = loaded ?? max ?? null;
|
|
253
|
+
/*
|
|
254
|
+
* 못 알아낸 까닭. **서버 탓으로 뭉개지 않는다.**
|
|
255
|
+
*
|
|
256
|
+
* 두드린 자리가 전부 우리 문지기에 막혔으면 서버는 이 물음을 **받아 본 적도
|
|
257
|
+
* 없다.** 그런데 여태 이 자리는 그때도 「아무 응답도 못 받았습니다」 였고,
|
|
258
|
+
* repl 은 그걸 「서버가 안 알려줍니다」 로 옮겼다 — 켤 때 바깥 연결을 처음
|
|
259
|
+
* 붙이는 사람이 보는 첫 문장이 이것이다. 허락을 아직 안 준 것뿐인데 서버가
|
|
260
|
+
* 이상한 줄 알고 서버를 뒤진다.
|
|
261
|
+
*/
|
|
262
|
+
const 문지기가막음 = tried.length > 0 && tried.every((t) => t.막힘);
|
|
250
263
|
const why = value ? null
|
|
251
|
-
:
|
|
252
|
-
: '
|
|
264
|
+
: 문지기가막음 ? '이 주소로 나갈 허락이 아직 없어서 물어보지도 못했습니다'
|
|
265
|
+
: tried.some((t) => t.ok) ? '서버가 응답은 했지만 길이를 안 알려 줍니다'
|
|
266
|
+
: '두드린 자리에서 아무 응답도 못 받았습니다';
|
|
253
267
|
|
|
254
268
|
return { value, max, loaded, out, source, outSource, maxKey, loadedKey, tried, why };
|
|
255
269
|
}
|
package/src/commands.js
CHANGED
|
@@ -141,6 +141,29 @@ function 설정남기기(cfg, 옵션 = {}) {
|
|
|
141
141
|
return r.ok;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* **지금 이 연결이 온 프로필**을 찾는다. 못 찾으면 null 이다.
|
|
146
|
+
*
|
|
147
|
+
* 여태 `cfg.profiles.find(p => p.id === cfg.active) ?? cfg.profiles[0]` 였다.
|
|
148
|
+
* 그 뒤 갈래가 「못 찾았으면 첫 번째에 쓴다」 라, active 가 어긋나 있으면
|
|
149
|
+
* `/ctx 655360` 이 **엉뚱한 연결**에 박히고 화면에는 「프로필에 저장했습니다」
|
|
150
|
+
* 가 뜬다. 8k 서버 프로필에 655,360 이 적히면 다음에 켤 때 그 서버를
|
|
151
|
+
* 655,360 으로 믿고 시작한다 — 긴 대화에서 서버가 거절하고, 그 까닭을
|
|
152
|
+
* 짚을 자리가 화면에 하나도 없다.
|
|
153
|
+
*
|
|
154
|
+
* 그래서 두 번 본다. active 로 찾고, 없으면 **지금 붙어 있는 주소·모델**로
|
|
155
|
+
* 찾는다. 그래도 없으면 못 찾았다고 하고 아무 데도 안 적는다 (repl.js 의
|
|
156
|
+
* 길이 되살리기가 이미 그렇게 한다).
|
|
157
|
+
*/
|
|
158
|
+
function 이연결의프로필(cfg, session) {
|
|
159
|
+
const 있는것 = Array.isArray(cfg?.profiles) ? cfg.profiles : [];
|
|
160
|
+
const 딱 = 있는것.find((p) => p.id === cfg.active);
|
|
161
|
+
if (딱) return 딱;
|
|
162
|
+
const base = session?.conn?.base;
|
|
163
|
+
const model = session?.conn?.model;
|
|
164
|
+
return 있는것.find((p) => p.baseUrl === base && p.model === model) ?? null;
|
|
165
|
+
}
|
|
166
|
+
|
|
144
167
|
/** /mode 아래에 규칙을 늘어놓는다. 아무것도 안 걸려 있으면 아무 말도 안 한다. */
|
|
145
168
|
function 규칙보이기(규칙들) {
|
|
146
169
|
if (!규칙들) return;
|
|
@@ -428,13 +451,17 @@ export async function handle(line, session, ctx) {
|
|
|
428
451
|
* 결재는 첨부로 돌고, 보고는 한 장으로 한다.
|
|
429
452
|
*/
|
|
430
453
|
case 'export': {
|
|
431
|
-
|
|
454
|
+
// 못 남긴 까닭을 받아 온다 — 디스크가 찼는지, 폴더가 읽기 전용인지는
|
|
455
|
+
// 화면에 적혀야 손을 쓸 수 있다.
|
|
456
|
+
const 탈 = {};
|
|
457
|
+
const 자리 = 보고서적기(ctx.scope.root, session, { scope: ctx.scope, audit: ctx.audit }, 탈);
|
|
432
458
|
say('');
|
|
433
459
|
if (자리) {
|
|
434
460
|
say(` ${mark.ok} ${말('export.saved')} ${c.white(ctx.scope.show(자리))}`);
|
|
435
461
|
say(` ${c.gray(말('export.openHint'))}`);
|
|
436
462
|
} else {
|
|
437
463
|
say(` ${c.red('✗')} ${말('export.failed')}`);
|
|
464
|
+
if (탈.왜) say(` ${c.gray(clip(탈.왜, 90))}`);
|
|
438
465
|
}
|
|
439
466
|
say('');
|
|
440
467
|
return { handled: true };
|
|
@@ -1932,7 +1959,15 @@ function showLevel(session, arg) {
|
|
|
1932
1959
|
const cfg = load();
|
|
1933
1960
|
cfg.level = 골라진;
|
|
1934
1961
|
설정남기기(cfg);
|
|
1935
|
-
} catch {
|
|
1962
|
+
} catch (e) {
|
|
1963
|
+
/*
|
|
1964
|
+
* 설정 파일 자체를 못 읽는 경우다(JSON 이 깨졌다). 바로 위 머리글이
|
|
1965
|
+
* 「못 남기면 말한다」 라고 적어 놓고 여기만 통째로 삼키고 있었다.
|
|
1966
|
+
* 켤 때는 repl 이 말해 주지만 **대화 도중에 깨진 경우는 아무도 안 말한다** —
|
|
1967
|
+
* 화면은 `✓ 개발자` 를 찍고 다음에 켜면 그대로 쉬움이다.
|
|
1968
|
+
*/
|
|
1969
|
+
say(` ${mark.warn} ${c.yellow(말('common.cfgSaveFailed', { 왜: clip(String(e?.message ?? e), 70) }))}`);
|
|
1970
|
+
}
|
|
1936
1971
|
const lv = LEVELS[골라진];
|
|
1937
1972
|
say('');
|
|
1938
1973
|
say(` ${mark.ok} ${c.bold(lv.name)} ${c.gray('— ' + lv.hint)}`);
|
|
@@ -2254,9 +2289,13 @@ function 증거명령(session, ctx, arg = '') {
|
|
|
2254
2289
|
|
|
2255
2290
|
if (/^(파일|file|저장|save)$/i.test(말)) {
|
|
2256
2291
|
const 이름 = `${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '')}`;
|
|
2257
|
-
const
|
|
2292
|
+
const 탈 = {};
|
|
2293
|
+
const 자리 = 증거적기(session.root, e, 이름, 탈);
|
|
2258
2294
|
if (자리) say(` ${mark.ok} ${c.gray('남겼습니다 —')} ${c.cyan(자리)}`);
|
|
2259
|
-
else
|
|
2295
|
+
else {
|
|
2296
|
+
say(` ${c.gray('파일로 못 남겼습니다. 화면 것만 쓰세요.')}`);
|
|
2297
|
+
if (탈.왜) say(` ${c.gray(clip(탈.왜, 90))}`);
|
|
2298
|
+
}
|
|
2260
2299
|
say('');
|
|
2261
2300
|
} else {
|
|
2262
2301
|
say(` ${c.gray('파일로 남기려면')} ${c.cyan('/evidence 파일')}`);
|
|
@@ -2873,13 +2912,15 @@ async function 출력상한(session, arg = '') {
|
|
|
2873
2912
|
const { parseSize } = await import('./backend/ctxsize.js');
|
|
2874
2913
|
const 말 = String(arg ?? '').trim().toLowerCase();
|
|
2875
2914
|
const cfg = load();
|
|
2876
|
-
const prof = cfg
|
|
2915
|
+
const prof = 이연결의프로필(cfg, session);
|
|
2877
2916
|
const 알아낸것 = session.conn.maxOut ?? null;
|
|
2878
2917
|
|
|
2879
2918
|
if (말 === 'auto' || 말 === '자동') {
|
|
2880
2919
|
session.conn.maxTokens = null;
|
|
2881
|
-
|
|
2882
|
-
say(` ${mark.ok} 직접 정한 값을
|
|
2920
|
+
const 지웠나 = prof ? (delete prof.maxTokens, 설정남기기(cfg)) : false;
|
|
2921
|
+
say(` ${mark.ok} 직접 정한 값을 지웠습니다. ${c.gray('이번 대화에 바로 먹습니다.')}`);
|
|
2922
|
+
// 프로필을 못 찾으면 다음에 켤 때 그 값이 되살아난다. 숨기면 안 된다.
|
|
2923
|
+
if (!지웠나) say(` ${mark.warn} ${c.yellow('설정에서 이 연결을 못 찾아 파일에는 못 남겼습니다 — 다음에 켜면 옛 값입니다.')}`);
|
|
2883
2924
|
say(` ${c.gray(알아낸것 ? `서버에서 알아낸 ${알아낸것.toLocaleString()} 토큰을 씁니다.` : '모르는 값이라 16,384 토큰으로 갑니다.')}`);
|
|
2884
2925
|
say('');
|
|
2885
2926
|
return;
|
|
@@ -3025,13 +3066,15 @@ async function ctxLength(session, arg = '') {
|
|
|
3025
3066
|
const 남기기 = (값, 어디서) => {
|
|
3026
3067
|
session.conn.ctx = 값;
|
|
3027
3068
|
const cfg = load();
|
|
3028
|
-
const prof = cfg
|
|
3069
|
+
const prof = 이연결의프로필(cfg, session);
|
|
3029
3070
|
// 못 남기면 아래 「프로필에 저장했습니다」 가 거짓말이 된다. 살아있는지 들고 간다.
|
|
3030
3071
|
const 남김 = prof ? (prof.ctx = 값, 설정남기기(cfg)) : false;
|
|
3031
3072
|
const b = session.breakdown();
|
|
3032
3073
|
say(` ${mark.ok} 컨텍스트 ${c.bold(값.toLocaleString())} 토큰 ${c.gray(`(${fmtSize(값)}) — ${어디서}`)}`);
|
|
3033
3074
|
say(` ${c.gray('지금 찬 양')} ${c.white(b.used.toLocaleString())} ${c.gray('· 남음')} ${c.white(b.left.toLocaleString())}`);
|
|
3034
3075
|
if (남김) say(` ${c.gray('프로필에 저장했습니다. 다음에 켤 때도 이 값입니다.')}`);
|
|
3076
|
+
// 못 남긴 까닭이 '설정을 못 찾음' 이면 설정남기기() 가 아무 말도 안 한다.
|
|
3077
|
+
else if (!prof) say(` ${mark.warn} ${c.yellow('설정에서 이 연결을 못 찾아 파일에는 못 남겼습니다 — 다음에 켜면 옛 값입니다.')}`);
|
|
3035
3078
|
say('');
|
|
3036
3079
|
};
|
|
3037
3080
|
|
|
@@ -3212,6 +3255,15 @@ async function 서버모델들(conn) {
|
|
|
3212
3255
|
const { req, headersFor } = await import('./backend/http.js');
|
|
3213
3256
|
if (conn.kind === 'ollama') {
|
|
3214
3257
|
const r = await req(`${conn.base.replace(/\/v1\/?$/, '')}/api/tags`, { timeout: 4000 });
|
|
3258
|
+
/*
|
|
3259
|
+
* **못 받은 것**과 **안 내주는 것**은 다르다.
|
|
3260
|
+
*
|
|
3261
|
+
* req 는 통신 실패에 안 던지고 `{ok:false, status, error}` 를 준다. 여기만
|
|
3262
|
+
* 그 ok 를 안 보고 빈 배열을 돌려줘서, 404·500·시간초과·프록시 거절이
|
|
3263
|
+
* 전부 「이 서버는 모델 목록을 내주지 않습니다」 가 됐다. 다른 갈래는
|
|
3264
|
+
* 아래처럼 null 을 준다 — 같은 함수가 갈래마다 다른 약속을 하고 있었다.
|
|
3265
|
+
*/
|
|
3266
|
+
if (!r.ok) return null;
|
|
3215
3267
|
return (r.json?.models ?? []).map((m) => m.name ?? m.model).filter(Boolean);
|
|
3216
3268
|
}
|
|
3217
3269
|
/*
|
|
@@ -3285,7 +3337,12 @@ async function switchModel(session, ctx, arg = '') {
|
|
|
3285
3337
|
say('');
|
|
3286
3338
|
return;
|
|
3287
3339
|
}
|
|
3288
|
-
|
|
3340
|
+
// 목록을 **못 받은** 것을 「없다」 로 적으면, 사람은 있는 모델을 찾아 헤맨다.
|
|
3341
|
+
if (있는것 == null) {
|
|
3342
|
+
say(` ${mark.warn} ${c.white(말)} ${c.gray('에 맞는 연결이 없고, 서버에는 물어봤지만 목록을 못 받았습니다.')}`);
|
|
3343
|
+
} else {
|
|
3344
|
+
say(` ${mark.warn} ${c.white(말)} ${c.gray('에 맞는 연결도 모델도 없습니다.')}`);
|
|
3345
|
+
}
|
|
3289
3346
|
say(` ${c.gray('무엇이 있는지 보려면')} ${c.cyan('/model list')}${c.gray(', 서버에 물어보려면')} ${c.cyan('/model models')}`);
|
|
3290
3347
|
say('');
|
|
3291
3348
|
return;
|
|
@@ -3410,14 +3467,29 @@ async function 모델만바꾸기(session, cfg, 모델, ctx = null) {
|
|
|
3410
3467
|
async function 길이맞추기(session, cfg, prof) {
|
|
3411
3468
|
const { probeCtx, fmtSize, 기본값 } = await import('./backend/ctxsize.js');
|
|
3412
3469
|
let r = null;
|
|
3413
|
-
|
|
3470
|
+
// 물어보다 **터진 것**과 서버가 값을 **안 준 것**은 다르다. 아래에서 갈라 적는다.
|
|
3471
|
+
let 못물어본까닭 = null;
|
|
3472
|
+
try { r = await probeCtx(session.conn, { timeout: 8000 }); }
|
|
3473
|
+
catch (e) { 못물어본까닭 = String(e?.message ?? e); }
|
|
3414
3474
|
const 값 = r?.value ?? prof.ctx ?? 기본값;
|
|
3415
3475
|
session.conn.ctx = 값;
|
|
3416
3476
|
if (prof) {
|
|
3417
3477
|
prof.ctx = 값;
|
|
3418
3478
|
설정남기기(cfg);
|
|
3419
3479
|
}
|
|
3420
|
-
|
|
3480
|
+
/*
|
|
3481
|
+
* 어디서 온 값인지 **세 가지를 갈라** 적는다.
|
|
3482
|
+
*
|
|
3483
|
+
* 앞서는 물어보다 터진 것도, 서버가 안 준 것도, 프로필에 적혀 있던 값을
|
|
3484
|
+
* 물려받은 것도 전부 「서버가 안 알려줘 기본값」 이었다. 값이 655,360 인데
|
|
3485
|
+
* 출처는 「기본값」 이라고 적히는 판이 실제로 난다 — /ctx 화면은 출처를
|
|
3486
|
+
* 정성껏 가르는데 이 줄만 뭉갰다.
|
|
3487
|
+
*/
|
|
3488
|
+
const 어디서 = r?.source ? r.source + '에서 읽음'
|
|
3489
|
+
: 못물어본까닭 ? '못 물어봤습니다 — ' + clip(못물어본까닭, 50)
|
|
3490
|
+
: r?.value == null && prof?.ctx ? '이 프로필에 적혀 있던 값'
|
|
3491
|
+
: '서버가 안 알려줘 기본값';
|
|
3492
|
+
say(` ${c.gray('컨텍스트')} ${c.white(값.toLocaleString())} ${c.gray('토큰 (' + fmtSize(값) + ') — ' + 어디서)}`);
|
|
3421
3493
|
if (r?.max && r?.loaded && r.max > r.loaded) {
|
|
3422
3494
|
say(` ${c.yellow('이 모델은 ' + r.max.toLocaleString() + ' 까지 됩니다.')} ${c.gray('서버에서 더 올린 뒤')} ${c.cyan('/ctx auto')}`);
|
|
3423
3495
|
} else if (!r?.value) {
|
|
@@ -3441,10 +3513,21 @@ function 연결목록(cfg, session) {
|
|
|
3441
3513
|
async function 서버모델고르기(session, ctx, cfg) {
|
|
3442
3514
|
const s = spin('서버에 모델 목록을 물어보는 중…');
|
|
3443
3515
|
let 있는것;
|
|
3444
|
-
|
|
3516
|
+
/*
|
|
3517
|
+
* 무엇 때문에 못 받았는지를 **버리지 않는다.**
|
|
3518
|
+
*
|
|
3519
|
+
* 여기 catch 는 오프라인 잠금(NetBlocked)까지 삼켰다. 그러면 자물쇠가 막은
|
|
3520
|
+
* 것을 「이 서버는 목록을 안 내줍니다」 라고 서버 탓으로 돌리게 된다 —
|
|
3521
|
+
* 잠금이 화면에서 사라지는 것은 이 저장소가 여러 군데서 막으려던 그 꼴이다.
|
|
3522
|
+
*/
|
|
3523
|
+
let 못받은까닭 = null;
|
|
3524
|
+
try { 있는것 = await 서버모델들(session.conn); }
|
|
3525
|
+
catch (e) { 있는것 = null; 못받은까닭 = String(e?.message ?? e); }
|
|
3445
3526
|
s.stop('');
|
|
3446
3527
|
if (!있는것 || !있는것.length) {
|
|
3447
|
-
say(` ${mark.warn}
|
|
3528
|
+
if (못받은까닭) say(` ${mark.warn} 모델 목록을 못 받았습니다 ${c.gray('— ' + clip(못받은까닭, 70))}`);
|
|
3529
|
+
else if (있는것 == null) say(` ${mark.warn} 모델 목록을 못 받았습니다 ${c.gray('— 서버가 답을 안 주거나 주소·열쇠가 안 맞습니다.')}`);
|
|
3530
|
+
else say(` ${mark.warn} 이 서버는 모델 목록을 내주지 않습니다.`);
|
|
3448
3531
|
say(` ${c.gray('목록이 없는 게이트웨이도 있습니다. 그때는')} ${c.cyan('deel setup')} ${c.gray('에서 모델 이름을 직접 넣으세요.')}`);
|
|
3449
3532
|
say('');
|
|
3450
3533
|
return;
|
package/src/configexplain.js
CHANGED
|
@@ -92,7 +92,9 @@ export function 설명(칸, { root = process.cwd(), env = process.env } = {}) {
|
|
|
92
92
|
여기 = 여기.find((x) => x?.name === 이름 || x?.id === 이름);
|
|
93
93
|
continue;
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
// 물려받은 이름(`constructor` · `toString`)으로 파고들면 **없는 칸이
|
|
96
|
+
// 있는 것이 된다.** 여기서 막아야 아래 세 층이 다 안전하다.
|
|
97
|
+
여기 = (여기 && typeof 여기 === 'object' && Object.hasOwn(여기, 이름)) ? 여기[이름] : undefined;
|
|
96
98
|
}
|
|
97
99
|
return 여기;
|
|
98
100
|
};
|
|
@@ -124,7 +126,14 @@ export function 설명(칸, { root = process.cwd(), env = process.env } = {}) {
|
|
|
124
126
|
|
|
125
127
|
// ── 3. 환경변수 ─────────────────────────────────────────────────────
|
|
126
128
|
const 끝칸 = 조각[조각.length - 1] ?? '';
|
|
127
|
-
|
|
129
|
+
/*
|
|
130
|
+
* `환경변수[끝칸] ?? []` 였다.
|
|
131
|
+
*
|
|
132
|
+
* `deel config explain constructor` 는 배열이 아니라 **함수**를 받아
|
|
133
|
+
* 바로 다음 줄에서 터졌다 — 화면에 뜬 말은 「오류 볼환경.filter is not a
|
|
134
|
+
* function」 이다. 없는 칸을 물었으면 없다고 해야 한다.
|
|
135
|
+
*/
|
|
136
|
+
const 볼환경 = Object.hasOwn(환경변수, 끝칸) ? 환경변수[끝칸] : [];
|
|
128
137
|
const 켜진환경 = 볼환경.filter((이름) => !이름.includes('<') && env[이름]);
|
|
129
138
|
if (켜진환경.length) {
|
|
130
139
|
층들.push({
|
|
@@ -136,7 +145,10 @@ export function 설명(칸, { root = process.cwd(), env = process.env } = {}) {
|
|
|
136
145
|
// ── 4. 관리 정책 (제일 위) ──────────────────────────────────────────
|
|
137
146
|
const 정책 = 정책읽기({ env, 다시: true });
|
|
138
147
|
if (정책.값 && typeof 정책.값 === 'object') {
|
|
139
|
-
|
|
148
|
+
// 정책 파일은 JSON 이라 프로토타입을 그대로 물려받는다. hasOwn 없이 보면
|
|
149
|
+
// 정책에 없는 칸이 「관리 정책에 이렇게 적혀 있습니다」 로 한 줄 올라온다 —
|
|
150
|
+
// 관리자에게 따지러 갈 근거가 통째로 거짓이 되는 자리다.
|
|
151
|
+
const 그값 = Object.hasOwn(정책.값, 끝칸) ? 정책.값[끝칸] : undefined;
|
|
140
152
|
if (그값 !== undefined) {
|
|
141
153
|
층들.push({ 층: '관리 정책', 자리: 정책.곳 ?? 정책자리(env)[0], 읽나: true, 값: 지운값(끝칸, 그값) });
|
|
142
154
|
}
|
package/src/repl.js
CHANGED
|
@@ -39,12 +39,12 @@ import { 못박기 } from './agent/pins.js';
|
|
|
39
39
|
import { 카드 } from './agent/card.js';
|
|
40
40
|
import { 배움 } from './agent/evolve.js';
|
|
41
41
|
import { 마크다운 } from './ui/md.js';
|
|
42
|
-
import { askHidden
|
|
42
|
+
import { askHidden } from './ui/prompt.js';
|
|
43
43
|
import { explain, shows as levelShows } from './ui/level.js';
|
|
44
44
|
import { 고르기 as 승인고르기, 다음 as 승인다음 } from './ui/approve.js';
|
|
45
45
|
import { 추천, 채울글 } from './ui/complete.js';
|
|
46
46
|
import { 접어쓰기 } from './ui/wrap.js';
|
|
47
|
-
import { 접을까 as 붙임접을까, 표만들기 as 붙임표, 펼치기 as 붙임펼치기, 쓴번호들 as 붙임쓴번호들,
|
|
47
|
+
import { 접을까 as 붙임접을까, 표만들기 as 붙임표, 펼치기 as 붙임펼치기, 쓴번호들 as 붙임쓴번호들, 남길글, 안쪽최대 as 붙임안쪽최대 } from './ui/pastechip.js';
|
|
48
48
|
import { 고른것풀기, 계획답풀기 } from './ui/pick.js';
|
|
49
49
|
import { 이력지킴이 } from './ui/histline.js';
|
|
50
50
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
@@ -80,7 +80,7 @@ const TOOL_GLYPH = {
|
|
|
80
80
|
function toolLabel(name, args) {
|
|
81
81
|
const a = args ?? {};
|
|
82
82
|
// 이름 차례는 tools/label.js 한 곳에서 온다 (다섯 벌이던 것을 모았다).
|
|
83
|
-
// 여기서 더 하는 일은 화면 사정뿐이다 —
|
|
83
|
+
// 여기서 더 하는 일은 화면 사정뿐이다 — 56자에서 자른다.
|
|
84
84
|
const first =
|
|
85
85
|
첫이름(a) ??
|
|
86
86
|
(a.command ? String(a.command).replace(/\s+/g, ' ').slice(0, 52) : null) ??
|
|
@@ -105,7 +105,11 @@ function toolLabel(name, args) {
|
|
|
105
105
|
(Array.isArray(a.paths) && a.paths.length ? 세말('count', a.paths.length) : null) ??
|
|
106
106
|
// 할 일 목록은 보여줄 경로가 없다. 빈 괄호를 띄우느니 개수를 적는다.
|
|
107
107
|
(Array.isArray(a.todos) ? 세말('hits', a.todos.length) : null) ?? '';
|
|
108
|
-
|
|
108
|
+
// 물려받은 이름으로 오면 `?? ` 가 안 걸린다 — TOOL_GLYPH['constructor'] 는
|
|
109
|
+
// 함수라서 참이고, 그대로 글로 바뀌어 `function Object() { [native code] }`
|
|
110
|
+
// 가 도구 줄에 찍힌다. 모델이 그런 이름으로 도구를 부르면 실제로 여기 온다
|
|
111
|
+
// (loop.js 가 모르는 도구도 showLabel 로 올린다).
|
|
112
|
+
const g = Object.hasOwn(TOOL_GLYPH, name) ? TOOL_GLYPH[name] : c.cyan('⏺');
|
|
109
113
|
const 안 = clip(String(first ?? ''), 56);
|
|
110
114
|
return `${g} ${c.bold(name)}${안 ? `${c.gray('(')}${c.gray(안)}${c.gray(')')}` : ''}`;
|
|
111
115
|
}
|
|
@@ -352,7 +356,20 @@ export async function chatLoop(opts = {}) {
|
|
|
352
356
|
say(` ${mark.warn} ${c.gray('이 연결은 열쇠를 받아 옵니다. 아래 명령을 띄웁니다:')}`);
|
|
353
357
|
say(` ${c.white(clip(설정.명령, 88))}`);
|
|
354
358
|
say(` ${c.gray('받은 열쇠는 이 판이 도는 동안 메모리에만 있고, 파일로 안 적습니다.')}`);
|
|
355
|
-
|
|
359
|
+
/*
|
|
360
|
+
* 이 자리는 repl 자신의 ask() 를 쓴다.
|
|
361
|
+
*
|
|
362
|
+
* 여기 있던 것은 ui/prompt.js 의 confirm 이었는데, 그 파일이 스스로
|
|
363
|
+
* 「REPL 은 readline 이 stdin 을 쥐고 있어서 위의 ask 를 그대로 못
|
|
364
|
+
* 쓴다」 고 못박아 두었다(그래서 암호는 askHidden 을 따로 만들었다).
|
|
365
|
+
* 이 한 자리만 옛 길로 남아 셋을 한꺼번에 깨고 있었다 —
|
|
366
|
+
* · readline 도 같은 바이트를 받아 그 'y' 가 큐에 쌓인다
|
|
367
|
+
* · 끝나면서 raw 모드를 꺼서 ESC·Shift+Tab 이 그때부터 안 먹는다
|
|
368
|
+
* · 여기서 Ctrl+C 를 누르면 prompt.js 의 process.exit(130) 이 돌아
|
|
369
|
+
* MCP·언어 서버·뒤에서 돌던 명령을 하나도 안 거두고 죽는다
|
|
370
|
+
*/
|
|
371
|
+
const 답 = String(await ask('띄울까요?', { def: 'y', 끝나면: 'n', 멈추면: 'n' })).trim().toLowerCase();
|
|
372
|
+
const 예 = !['n', 'no', 'ㄴ', '아니', '아니요', '아니오', '취소', '그만'].includes(답);
|
|
356
373
|
say('');
|
|
357
374
|
return 예;
|
|
358
375
|
};
|
|
@@ -381,7 +398,7 @@ export async function chatLoop(opts = {}) {
|
|
|
381
398
|
say(` ${c.gray('이어할 대화가 없습니다. 새로 시작합니다.')}`);
|
|
382
399
|
} else {
|
|
383
400
|
store = new Store(root, target);
|
|
384
|
-
const { messages:
|
|
401
|
+
const { messages: 적힌것, 못읽음 } = store.load();
|
|
385
402
|
/*
|
|
386
403
|
* 도구가 도는 중에 죽었으면 호출만 적히고 결과가 없다. 그대로 보내면
|
|
387
404
|
* 규격 서버가 400 을 내서 이어받자마자 첫 마디에서 죽는다. 손봐서 받는다.
|
|
@@ -394,6 +411,29 @@ export async function chatLoop(opts = {}) {
|
|
|
394
411
|
if (고친것) {
|
|
395
412
|
say(` ${c.gray(`중단된 도구 호출 ${고친것}개를 걷어냈습니다 — 그때 하던 일은 다시 시켜 주세요.`)}`);
|
|
396
413
|
}
|
|
414
|
+
} else {
|
|
415
|
+
/*
|
|
416
|
+
* ── 못 이어받았으면 **말한다** ──────────────────────────────────
|
|
417
|
+
*
|
|
418
|
+
* 여태 이 갈래가 통째로 없었다. `--resume 오타난아이디` 를 치면
|
|
419
|
+
* 화면에 **아무 줄도 안 나오고** 곧바로 머리말이 뜬다. 그러고는
|
|
420
|
+
* 그 아이디로 새 파일이 만들어져서, 사람이 「이어서 해줘」 라고
|
|
421
|
+
* 하면 모델은 아무것도 모른다 — 이어받은 줄 알고 말을 잇는데
|
|
422
|
+
* 상대는 처음 보는 대화다.
|
|
423
|
+
*
|
|
424
|
+
* store.load() 는 못 읽은 까닭을 이미 돌려준다(agent/store.js 의
|
|
425
|
+
* 못읽음). 그것도 여기서 버리고 있었다 — 권한 때문인지 다른
|
|
426
|
+
* 프로그램이 잡고 있어서인지 화면 어디에도 안 남았다.
|
|
427
|
+
*/
|
|
428
|
+
say('');
|
|
429
|
+
if (못읽음) {
|
|
430
|
+
say(` ${mark.warn} ${c.bold(target)} ${c.gray('— 그 대화를 못 읽었습니다')} ${c.gray('(' + 못읽음 + ')')}`);
|
|
431
|
+
} else if (고친것) {
|
|
432
|
+
say(` ${mark.warn} ${c.bold(target)} ${c.gray(`— 남은 것이 중단된 도구 호출 ${고친것}개뿐이라 이어받을 말이 없습니다.`)}`);
|
|
433
|
+
} else {
|
|
434
|
+
say(` ${mark.warn} ${c.bold(target)} ${c.gray('— 그런 대화가 없거나 비어 있습니다.')}`);
|
|
435
|
+
}
|
|
436
|
+
say(` ${c.gray('새 대화로 시작합니다. 이어할 것을 찾으려면')} ${c.cyan('/sessions')}`);
|
|
397
437
|
}
|
|
398
438
|
/*
|
|
399
439
|
* 못 박아 둔 것도 같이 되살린다 (agent/pins.js).
|
|
@@ -410,7 +450,17 @@ export async function chatLoop(opts = {}) {
|
|
|
410
450
|
}
|
|
411
451
|
if (!store) store = new Store(root);
|
|
412
452
|
store.begin({ model: conn.model, base: 주소가리기(conn.base), root });
|
|
413
|
-
|
|
453
|
+
// 머리말보다 먼저 일어난 탈은 여기 모았다가 아래 경고 자리에서 같이 찍는다.
|
|
454
|
+
const 켤때경고 = [];
|
|
455
|
+
// 되돌리기 이력을 못 읽는다는 말은 한 판에 한 번만 한다 — 턴마다 세는 자리라
|
|
456
|
+
// 안 막으면 같은 줄이 턴 수만큼 쌓인다.
|
|
457
|
+
let 되돌리기못읽음말했나 = false;
|
|
458
|
+
/*
|
|
459
|
+
* 정리가 실패해도 켜는 것은 막지 않는다. 다만 **삼키지는 않는다.**
|
|
460
|
+
* 계속 실패하면 .deel/sessions 가 끝없이 커지는데 화면에는 영영 안 떴다.
|
|
461
|
+
* 쓰기 실패는 store 가 세어서 알리는데(아래 저장 경고) 이 자리만 옛 모양이었다.
|
|
462
|
+
*/
|
|
463
|
+
try { prune(root); } catch (e) { 켤때경고.push(`오래된 대화 정리를 못 했습니다 — ${clip(String(e?.message ?? e), 60)}`); }
|
|
414
464
|
|
|
415
465
|
/*
|
|
416
466
|
* 밖에서 붙인 도구(MCP) 서버를 띄운다.
|
|
@@ -646,6 +696,17 @@ export async function chatLoop(opts = {}) {
|
|
|
646
696
|
// 상자를 걷어내고, 사람이 보낸 글을 대화에 남긴다. 안 남기면 스크롤을
|
|
647
697
|
// 올렸을 때 답만 있고 무엇을 물었는지가 없다.
|
|
648
698
|
const 이었나 = 이어쓰기줄들 !== null;
|
|
699
|
+
/*
|
|
700
|
+
* **지우기 전에** 챙긴다.
|
|
701
|
+
*
|
|
702
|
+
* 아래 스물몇 줄 뒤에서 `이어쓴것` 을 보고 「앞줄은 이미 찍혔으니
|
|
703
|
+
* 마지막 줄만 이어 찍는다」 를 고르는데, 그 값이 바로 여기서 거짓으로
|
|
704
|
+
* 지워지고 있었다. 그래서 그 갈래는 **한 번도 안 골렸다** — 백틱으로
|
|
705
|
+
* 이어 쓰면 줄쌓기() 가 이미 찍어 둔 앞줄이 통째로 다시 찍혔다.
|
|
706
|
+
* 바로 그 자리의 주석이 「안 그러면 같은 줄이 두 번 보인다」 고
|
|
707
|
+
* 적어 둔 것인데, 코드가 정확히 그것을 하고 있었다.
|
|
708
|
+
*/
|
|
709
|
+
const 백틱으로이었나 = 이어쓴것;
|
|
649
710
|
보낼것 = 이었나 ? [...이어쓰기줄들, l].join('\n') : l;
|
|
650
711
|
이어쓰기줄들 = null; 이어쓴것 = false;
|
|
651
712
|
화면.입력지움();
|
|
@@ -666,10 +727,10 @@ export async function chatLoop(opts = {}) {
|
|
|
666
727
|
* 백틱으로 이어 쓴 경우에는 앞줄이 이미 찍혀 있으므로 마지막 줄만
|
|
667
728
|
* 이어 찍는다 — 안 그러면 같은 줄이 두 번 보인다.
|
|
668
729
|
*/
|
|
669
|
-
const 찍을것 = 남길글(
|
|
730
|
+
const 찍을것 = 남길글(백틱으로이었나 ? l : 보낼것, 붙인것들);
|
|
670
731
|
if (찍을것.trim() || 이었나) {
|
|
671
732
|
for (const [i, 한줄] of String(찍을것).split('\n').entries()) {
|
|
672
|
-
say(`${i === 0 &&
|
|
733
|
+
say(`${i === 0 && !백틱으로이었나 ? ` ${c.hcyan('❯')} ` : ' '}${c.white(한줄)}`);
|
|
673
734
|
}
|
|
674
735
|
}
|
|
675
736
|
} else if (l.trim() || 이어쓰기줄들 !== null) {
|
|
@@ -1077,7 +1138,42 @@ export async function chatLoop(opts = {}) {
|
|
|
1077
1138
|
* y 를 쳐도 화면에 아무것도 안 나타난다 — 먹은 건지 안 먹은 건지 모른다.
|
|
1078
1139
|
*/
|
|
1079
1140
|
묻는중 = 상자쓰나 ? 앞 : null;
|
|
1080
|
-
|
|
1141
|
+
/*
|
|
1142
|
+
* 쌓아 둔 이어쓰기를 버릴 때는 **말한다.**
|
|
1143
|
+
*
|
|
1144
|
+
* SIGINT 쪽(아래)은 「이어쓰던 것을 버렸습니다」 를 반드시 찍는데 여기만
|
|
1145
|
+
* 조용했다. 일하는 도중에 줄 끝 백틱으로 여러 줄을 쌓아 두면 그 글은
|
|
1146
|
+
* 화면에 안 찍힌 채 버퍼에만 있다 — 그 사이 물음이 뜨면 통째로 사라지고,
|
|
1147
|
+
* 사람은 자기가 친 세 줄이 어디로 갔는지 알 길이 없다.
|
|
1148
|
+
*/
|
|
1149
|
+
if (이어쓰기줄들 !== null) {
|
|
1150
|
+
const 버린줄수 = 이어쓰기줄들.length;
|
|
1151
|
+
이어쓰기줄들 = null; 이어쓴것 = false;
|
|
1152
|
+
say(` ${c.gray(`쌓아 두던 ${버린줄수}줄은 버렸습니다 — 물음이 끼어들었습니다.`)}`);
|
|
1153
|
+
}
|
|
1154
|
+
/*
|
|
1155
|
+
* ── 물음이 뜨기 **전에** 쳐 둔 줄은 이 물음의 답이 아니다 ────────────
|
|
1156
|
+
*
|
|
1157
|
+
* nextLine 은 큐에 뭐가 있으면 그것부터 준다. 그래서 일하는 동안 미리
|
|
1158
|
+
* 쳐 둔 「다음은 테스트도 돌려줘」 가, 30초 뒤에 뜬 「실행할까요?」 의
|
|
1159
|
+
* 답으로 튀어나왔다. y 가 아니니 거부로 읽히고, 그 지시는 큐에서
|
|
1160
|
+
* 사라져 모델에게 **영영 안 간다.** 화면에는 사람이 답한 것과 똑같은
|
|
1161
|
+
* 모양이 남는다 — 무엇이 답으로 쓰였는지 아무 데도 안 적힌다.
|
|
1162
|
+
*
|
|
1163
|
+
* Ask 도구는 더 나쁘다. 「3번 파일도 고쳐줘」 의 앞머리 숫자를 골라진
|
|
1164
|
+
* 항목으로 읽어서, 사람이 3번을 고른 것과 똑같은 화면이 된다.
|
|
1165
|
+
*
|
|
1166
|
+
* 잠깐 빼 뒀다가 되돌려 놓는다. 버리는 것이 아니다 — 턴이 끝나면
|
|
1167
|
+
* 평소대로 처리돼야 한다.
|
|
1168
|
+
*
|
|
1169
|
+
* ── 파이프는 다르다 ────────────────────────────────────────────────
|
|
1170
|
+
*
|
|
1171
|
+
* 손으로 치는 자리가 없으면 **미리 넣는 것 말고는 답할 길이 없다.**
|
|
1172
|
+
* 답을 미리 실어 보내는 것(`echo y | deel`)이 정상 사용이고,
|
|
1173
|
+
* 여기서 그것까지 빼 두면 물음이 영영 안 끝난다. 사람이 앞에 앉아
|
|
1174
|
+
* 있을 때만 「먼저 친 줄은 답이 아니다」 가 참이다.
|
|
1175
|
+
*/
|
|
1176
|
+
const 미리쳐둔 = process.stdin.isTTY ? queue.splice(0) : [];
|
|
1081
1177
|
const 멈춤 = o.signal ?? turn?.signal ?? null;
|
|
1082
1178
|
try {
|
|
1083
1179
|
const a = await nextLine(멈춤);
|
|
@@ -1087,9 +1183,21 @@ export async function chatLoop(opts = {}) {
|
|
|
1087
1183
|
say(` ${c.gray(옮긴말('ask.stopped'))}`);
|
|
1088
1184
|
return o.멈추면 ?? '';
|
|
1089
1185
|
}
|
|
1090
|
-
|
|
1186
|
+
/*
|
|
1187
|
+
* 입력이 **끝난 것**과 사람이 그냥 Enter 를 친 것은 다르다.
|
|
1188
|
+
*
|
|
1189
|
+
* 여태 둘 다 `def` 였다. 파이프로 들어왔거나 Ctrl+D 를 누르면 아무도
|
|
1190
|
+
* 답하지 않았는데 기본값이 답으로 잡힌다 — 「나가도 될까요?」 의 def 는
|
|
1191
|
+
* 'y' 라, 사람이 한 글자도 안 친 판에서 바깥 연결이 허락된다.
|
|
1192
|
+
* 부르는 쪽이 `끝나면` 으로 그 경우를 따로 정할 수 있게 한다.
|
|
1193
|
+
*/
|
|
1194
|
+
if (a === null) return o.끝나면 ?? o.def ?? '';
|
|
1091
1195
|
return a.trim() || o.def || '';
|
|
1092
|
-
} finally {
|
|
1196
|
+
} finally {
|
|
1197
|
+
묻는중 = null;
|
|
1198
|
+
// 빼 뒀던 것을 앞에 되돌린다. 차례가 바뀌면 안 된다.
|
|
1199
|
+
if (미리쳐둔.length) queue.unshift(...미리쳐둔);
|
|
1200
|
+
}
|
|
1093
1201
|
};
|
|
1094
1202
|
|
|
1095
1203
|
/**
|
|
@@ -1436,7 +1544,22 @@ export async function chatLoop(opts = {}) {
|
|
|
1436
1544
|
// --ctx 로 직접 주신 값이 있으면 안 건드린다. 사람이 고른 것을 뒤집지 않는다.
|
|
1437
1545
|
const 길이알림 = []; // 잘 된 소식
|
|
1438
1546
|
const 길이경고 = []; // 손을 봐야 하는 것
|
|
1439
|
-
|
|
1547
|
+
/*
|
|
1548
|
+
* ── 부르는 자리가 둘이라 함수로 뺀다 ──────────────────────────────────
|
|
1549
|
+
*
|
|
1550
|
+
* 바깥 연결에 아직 허락을 안 준 판에서는 이 두드림이 **우리 문지기에**
|
|
1551
|
+
* 막힌다(safety/network.js). 아래 「나가도 될까요?」 에 답해야 문이 열리는데,
|
|
1552
|
+
* 그 물음은 머리말 다음에 있어야 해서 여기보다 한참 뒤다.
|
|
1553
|
+
*
|
|
1554
|
+
* 그래서 여태 바깥 프로필을 처음 붙이는 사람은 **언제나** 이 줄을 봤다:
|
|
1555
|
+
* ⚠ 컨텍스트를 서버가 안 알려줍니다 — 우선 32,768 으로 잡았습니다
|
|
1556
|
+
* 서버는 이 물음을 받아 본 적도 없다. 그리고 허락한 뒤에도 다시 안 물어봐서,
|
|
1557
|
+
* 128k 짜리 모델을 그 판 내내 32,768 로 알고 쓴다 — 화면 어디에도 그 말이 없다.
|
|
1558
|
+
*
|
|
1559
|
+
* 매개변수 이름을 바깥 것과 똑같이 둔 것은 일부러다. 몸통이 그대로 먹는다.
|
|
1560
|
+
*/
|
|
1561
|
+
const 길이알아내기 = async (길이알림, 길이경고) => {
|
|
1562
|
+
if (opts.ctx != null) return;
|
|
1440
1563
|
// 이 컴퓨터 안의 서버면 눈 깜짝할 새다. 사내 게이트웨이는 몇 초 걸릴 수 있어
|
|
1441
1564
|
// 무슨 일이 일어나는 중인지 알려 준다 — 멈춘 것처럼 보이면 안 된다.
|
|
1442
1565
|
화면.돌리기(옮긴말('run.ctxProbe'));
|
|
@@ -1493,9 +1616,36 @@ export async function chatLoop(opts = {}) {
|
|
|
1493
1616
|
+ `\n 너무 길면 서버가 알려 주는 값으로 저절로 맞춥니다. 아는 값이 있으면 ${c.cyan('/ctx 8192')} 처럼 직접 정하세요`);
|
|
1494
1617
|
}
|
|
1495
1618
|
} else if (prof.ctx == null) {
|
|
1496
|
-
|
|
1619
|
+
/*
|
|
1620
|
+
* **왜** 못 알아냈는지까지 적는다.
|
|
1621
|
+
*
|
|
1622
|
+
* probeCtx 는 그 까닭을 이미 들고 온다(backend/ctxsize.js 의 why) —
|
|
1623
|
+
* 「나갈 허락이 없다」 · 「응답은 왔는데 길이를 안 준다」 · 「아무 응답도
|
|
1624
|
+
* 없다」 는 사람이 할 일이 셋 다 다르다. 여태 이 자리에서 그걸 버리고
|
|
1625
|
+
* 늘 「서버가 안 알려줍니다」 한 문장으로 뭉갰다.
|
|
1626
|
+
*/
|
|
1627
|
+
길이경고.push(`컨텍스트를 못 알아냈습니다 ${c.gray('(' + (r?.why ?? '까닭 모름') + ')')} — 우선 ${CTX_DEFAULT.toLocaleString()} 으로 잡았습니다. ${c.cyan('/ctx 655360')} 처럼 직접 지정하세요`);
|
|
1628
|
+
} else if (r?.why && !r?.tried?.some((t) => t.ok)) {
|
|
1629
|
+
/*
|
|
1630
|
+
* 프로필에 값이 적혀 있으면 **아무 말도 안 하던** 자리다.
|
|
1631
|
+
*
|
|
1632
|
+
* 이 블록 맨 위에 적어 둔 약속이 「켤 때마다 서버에 물어본다. 저장된
|
|
1633
|
+
* 값을 그대로 믿지 않는다」 인데, 못 물어본 판에서는 그 약속이 그냥
|
|
1634
|
+
* 거짓이 된다 — 머리말에는 저장된 숫자가 **방금 서버에서 확인한 값**
|
|
1635
|
+
* 처럼 뜬다. 서버에서 창을 줄여 놨어도 알 길이 없다.
|
|
1636
|
+
*
|
|
1637
|
+
* 서버가 **응답은 했는데 길이만 안 주는** 흔한 판에서는 조용히 있는다
|
|
1638
|
+
* (tried 에 ok 가 하나라도 있으면 그 경우다). 그건 늘 그런 것이라,
|
|
1639
|
+
* 켤 때마다 경고를 붙이면 곧 아무도 안 읽는다. 여기서 말하는 것은
|
|
1640
|
+
* **아무 데서도 대답을 못 받은** 판뿐이다.
|
|
1641
|
+
*/
|
|
1642
|
+
길이경고.push(`컨텍스트를 서버에 못 물어봤습니다 ${c.gray('(' + r.why + ')')}`
|
|
1643
|
+
+ ` — 프로필에 적힌 ${c.white(Number(prof.ctx).toLocaleString())} 을 그대로 씁니다. 서버에서 바뀌었으면 ${c.cyan('/ctx auto')}`);
|
|
1497
1644
|
}
|
|
1498
|
-
}
|
|
1645
|
+
};
|
|
1646
|
+
|
|
1647
|
+
// 허락을 물어야 하는 연결이면 여기서 안 두드린다 — 묻고 난 뒤에 부른다.
|
|
1648
|
+
if (!나감.물어볼까) await 길이알아내기(길이알림, 길이경고);
|
|
1499
1649
|
|
|
1500
1650
|
/*
|
|
1501
1651
|
* ── 켤 때 도는 글자 모션 ──────────────────────────────────────────────
|
|
@@ -1557,6 +1707,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1557
1707
|
}
|
|
1558
1708
|
if (!conn.tools) warn.push(옮긴말('run.noTools'));
|
|
1559
1709
|
if (!conn.streaming) warn.push(옮긴말('run.noStream'));
|
|
1710
|
+
warn.push(...켤때경고);
|
|
1560
1711
|
warn.push(...길이경고);
|
|
1561
1712
|
// 잘 된 것은 경고 표시를 달지 않는다. ⚠ 가 붙으면 뭘 고쳐야 하나 싶어진다.
|
|
1562
1713
|
for (const l of 길이알림) say(` ${mark.ok} ${c.gray(l)}`);
|
|
@@ -1576,6 +1727,22 @@ export async function chatLoop(opts = {}) {
|
|
|
1576
1727
|
* 뒤집으면서 쓰던 사람을 안 깨뜨리는 길이 이것이다. 막는 것이 목적이 아니라
|
|
1577
1728
|
* **모르고 나가는 것**을 없애는 것이 목적이다.
|
|
1578
1729
|
*/
|
|
1730
|
+
/*
|
|
1731
|
+
* 여기서 나가도 **띄운 것은 거둔다.**
|
|
1732
|
+
*
|
|
1733
|
+
* 이 자리의 `rl.close(); return 0;` 은 맨 아래 정리 블록을 통째로 건너뛰었다.
|
|
1734
|
+
* MCP 서버는 이미 떠 있다(위의 다붙이기) — 남의 프로세스가 그대로 남는다.
|
|
1735
|
+
* 「deel 을 껐는데도 그 서버가 계속 돌고 있게 된다」 는 바로 그 자리의
|
|
1736
|
+
* 주석이 없애겠다고 적어 둔 것인데, 이 길로 나가면 그대로 일어났다.
|
|
1737
|
+
*/
|
|
1738
|
+
const 일찍끝내기 = (코드) => {
|
|
1739
|
+
rl.close();
|
|
1740
|
+
for (const 서버 of mcp붙임.서버들) 서버.닫기();
|
|
1741
|
+
언어서버다끄기().catch(() => {});
|
|
1742
|
+
화면.close();
|
|
1743
|
+
return 코드;
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1579
1746
|
if (나감.물어볼까) {
|
|
1580
1747
|
const 어디 = (() => { try { return new URL(conn.base).host; } catch { return conn.base; } })();
|
|
1581
1748
|
say('');
|
|
@@ -1584,14 +1751,39 @@ export async function chatLoop(opts = {}) {
|
|
|
1584
1751
|
say(` ${c.gray('보내는 것 — 시킨 말, 그리고 모델이 읽은 파일의 내용입니다.')}`);
|
|
1585
1752
|
say(` ${c.gray('로컬 모델(127.0.0.1)이나 사내망 주소였다면 이 물음이 안 나옵니다.')}`);
|
|
1586
1753
|
say('');
|
|
1587
|
-
|
|
1754
|
+
|
|
1755
|
+
/*
|
|
1756
|
+
* ── 답할 사람이 없으면 **묻지 않는다** ────────────────────────────
|
|
1757
|
+
*
|
|
1758
|
+
* 이 자리가 이 프로그램이 파는 단 하나의 약속이다 — 「모르고 나가는
|
|
1759
|
+
* 것을 없앤다」. 그런데 파이프로 들어오면 그 약속이 조용히 꺼졌다:
|
|
1760
|
+
*
|
|
1761
|
+
* echo "이 폴더 요약해줘" | deel
|
|
1762
|
+
* › 나가도 될까요? ⏎ 허락하고 기억 · n 그만 [y] 이 폴더 요약해줘
|
|
1763
|
+
* 허락을 적어 뒀습니다 — 이 프로필은 다음부터 안 묻습니다.
|
|
1764
|
+
*
|
|
1765
|
+
* 사람은 한 글자도 답하지 않았다. 시킨 말이 답으로 먹혔고(그래서
|
|
1766
|
+
* 모델에게 가지도 않았다), online: true 가 설정 파일에 박혀 그 프로필은
|
|
1767
|
+
* 앞으로 영영 안 묻는다. 빈 파이프여도 같다 — EOF 가 def 'y' 로 잡혔다.
|
|
1768
|
+
*
|
|
1769
|
+
* 손으로 켠 판에서만 묻는다. 그 밖에는 안 나간다 — 미리 허락해 두는
|
|
1770
|
+
* 길(설정의 online, --online)이 이미 있고, 그 길은 사람이 마음먹고
|
|
1771
|
+
* 지나가는 자리다.
|
|
1772
|
+
*/
|
|
1773
|
+
if (!process.stdin.isTTY) {
|
|
1774
|
+
say(` ${mark.warn} ${c.gray('파이프로 들어와 있어 물어볼 수가 없습니다 — 나가지 않았습니다.')}`);
|
|
1775
|
+
say(` ${c.gray('내보내려면 먼저 손으로 한 번 켜서 허락하거나,')} ${c.cyan('--online')} ${c.gray('을 붙이세요.')}`);
|
|
1776
|
+
return 일찍끝내기(0);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// 멈춤(ESC)도 입력 끝(Ctrl+D)도 **허락이 아니다.** def 로 물러나면 둘 다 'y' 가 된다.
|
|
1780
|
+
const 답 = String(await ask(`나가도 될까요? ${c.gray('⏎ 허락하고 기억 · n 그만')}`, { def: 'y', 끝나면: 'n', 멈추면: 'n' }))
|
|
1588
1781
|
.trim().toLowerCase();
|
|
1589
1782
|
if (['n', 'no', 'ㄴ', '아니', '아니요', '아니오', '취소', '그만'].includes(답)) {
|
|
1590
1783
|
say('');
|
|
1591
1784
|
say(` ${c.gray('나가지 않았습니다. 아무것도 안 보냈습니다.')}`);
|
|
1592
1785
|
say(` ${c.gray('이 컴퓨터 안의 모델을 붙이려면')} ${c.cyan('deel setup')} ${c.gray('또는')} ${c.cyan('deel scan --save')} ${c.gray('를 쓰세요.')}`);
|
|
1593
|
-
|
|
1594
|
-
return 0;
|
|
1786
|
+
return 일찍끝내기(0);
|
|
1595
1787
|
}
|
|
1596
1788
|
prof.online = true;
|
|
1597
1789
|
try {
|
|
@@ -1603,6 +1795,18 @@ export async function chatLoop(opts = {}) {
|
|
|
1603
1795
|
say(` ${mark.warn} ${c.gray(`허락을 저장하지 못했습니다 (${e.message}). 이번에만 나갑니다.`)}`);
|
|
1604
1796
|
}
|
|
1605
1797
|
allowEndpoint(conn.base);
|
|
1798
|
+
|
|
1799
|
+
/*
|
|
1800
|
+
* 이제야 문이 열렸다. 컨텍스트 길이는 **여기서** 알아본다.
|
|
1801
|
+
*
|
|
1802
|
+
* 위쪽 자리에서 두드렸다면 방금 연 이 문 때문에 전부 막혀 있었다.
|
|
1803
|
+
* 늦게 찍히는 만큼 자리를 따로 잡아 준다 — 머리말에 섞여 들어가면
|
|
1804
|
+
* 방금 답한 물음과 아무 상관 없는 줄로 보인다.
|
|
1805
|
+
*/
|
|
1806
|
+
const 늦은알림 = []; const 늦은경고 = [];
|
|
1807
|
+
await 길이알아내기(늦은알림, 늦은경고);
|
|
1808
|
+
for (const l of 늦은알림) say(` ${mark.ok} ${c.gray(l)}`);
|
|
1809
|
+
for (const w of 늦은경고) say(` ${mark.warn} ${c.gray(w)}`);
|
|
1606
1810
|
}
|
|
1607
1811
|
|
|
1608
1812
|
/*
|
|
@@ -1704,12 +1908,55 @@ export async function chatLoop(opts = {}) {
|
|
|
1704
1908
|
text = line.trim();
|
|
1705
1909
|
if (!text) continue;
|
|
1706
1910
|
if (예약이었나 && 상자쓰나) {
|
|
1911
|
+
/*
|
|
1912
|
+
* ── 통째로 쏟지 않는다 ──────────────────────────────────────────
|
|
1913
|
+
*
|
|
1914
|
+
* 큐에 든 것은 **펼쳐진 원문**이다(위 붙임펼치기). 여기서 그대로
|
|
1915
|
+
* say 하면 도구가 도는 사이에 붙여넣은 50줄이 그 자리에 쏟아진다.
|
|
1916
|
+
* 기다릴 때 보낸 것은 남길글() 로 `[붙여넣기 #1 · 47줄 · 2.1KB]` 로
|
|
1917
|
+
* 줄여 남기는데 이 길만 옛 모양이었다.
|
|
1918
|
+
*
|
|
1919
|
+
* 넓이도 지켜야 한다. 상자는 아래 몇 줄을 제 자리로 잡아 두고 거기에
|
|
1920
|
+
* 다시 그리는데, 터미널보다 넓은 줄을 밀어 넣으면 그 줄이 접히면서
|
|
1921
|
+
* 줄 셈이 어긋나고 위쪽 대화를 갉아먹는다.
|
|
1922
|
+
*/
|
|
1923
|
+
const 폭 = Math.max(20, (process.stdout.columns ?? 80) - 8);
|
|
1924
|
+
const 줄들 = String(text).split('\n');
|
|
1925
|
+
const 넘치나 = 줄들.length > 붙임안쪽최대;
|
|
1926
|
+
const 보일것 = 넘치나 ? 줄들.slice(0, 2) : 줄들;
|
|
1707
1927
|
화면.입력지움();
|
|
1708
|
-
|
|
1928
|
+
for (const [i, 한줄] of 보일것.entries()) {
|
|
1929
|
+
say(`${i === 0 ? ` ${c.hcyan('❯')} ` : ' '}${c.white(clip(한줄, 폭))}`
|
|
1930
|
+
+ `${i === 0 ? ` ${c.gray('(미리 쳐 둔 것)')}` : ''}`);
|
|
1931
|
+
}
|
|
1932
|
+
if (넘치나) {
|
|
1933
|
+
say(` ${c.gray(`… 그 밖에 ${줄들.length - 2}줄 — 통째로 보냈습니다.`)}`);
|
|
1934
|
+
}
|
|
1709
1935
|
}
|
|
1710
1936
|
}
|
|
1711
1937
|
|
|
1712
|
-
|
|
1938
|
+
/*
|
|
1939
|
+
* ── 명령 하나가 대화를 끝내면 안 된다 ──────────────────────────────
|
|
1940
|
+
*
|
|
1941
|
+
* 여기는 어떤 try 안에도 없었다. 슬래시 명령 안에서 파일을 쓰다 던지면
|
|
1942
|
+
* (`.deel` 이 읽기 전용이거나 디스크가 찼을 때 writeFileSync 가 그렇다)
|
|
1943
|
+
* 그대로 위로 새어 나가 **프로세스가 끝났다.** `/memory 어쩌구` 한 줄에
|
|
1944
|
+
* 대화가 통째로 사라지는 것이다 — 오간 말도, 이어 갈 자리도 같이.
|
|
1945
|
+
*
|
|
1946
|
+
* 보이는 실패이긴 하지만 대가가 대화 전체다. 여기서 받아 한 줄 적고
|
|
1947
|
+
* 이어 간다. 무엇 때문인지도 같이 적는다 — 안 적으면 그 사람은 명령이
|
|
1948
|
+
* 왜 안 먹는지 영영 모른다.
|
|
1949
|
+
*/
|
|
1950
|
+
let cmd;
|
|
1951
|
+
try {
|
|
1952
|
+
cmd = await handle(text, session, ctx);
|
|
1953
|
+
} catch (e) {
|
|
1954
|
+
say('');
|
|
1955
|
+
say(` ${c.red('✗')} ${c.gray('명령을 처리하다 막혔습니다')} ${c.gray('— ' + clip(String(e?.message ?? e), 90))}`);
|
|
1956
|
+
say(` ${c.gray('대화는 그대로입니다. 다른 명령을 치거나 그냥 이어서 말씀하세요.')}`);
|
|
1957
|
+
say('');
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1713
1960
|
if (cmd.exit) break;
|
|
1714
1961
|
if (cmd.handled) continue;
|
|
1715
1962
|
const toSend = cmd.text ?? text; // 슬래시 명령이면 펼쳐진 내용을 보낸다
|
|
@@ -1818,6 +2065,9 @@ export async function chatLoop(opts = {}) {
|
|
|
1818
2065
|
// 이 턴이 탈 없이 끝났나. 끊겼거나 터진 뒤에 승인 창을 띄우면 안 된다 —
|
|
1819
2066
|
// 계획이 반만 나온 것을 두고 "이대로 진행할까요?" 를 묻는 꼴이 된다.
|
|
1820
2067
|
let 턴탈났나 = false;
|
|
2068
|
+
// 이 턴에 TodoWrite 가 실제로 돌았나. ctx.todos 는 세션 내내 살아 있어서
|
|
2069
|
+
// 이걸 안 세면 지난 턴의 남은 할 일을 이번 계획으로 착각한다 (아래 계획 승인).
|
|
2070
|
+
let 이번턴할일적었나 = false;
|
|
1821
2071
|
/*
|
|
1822
2072
|
* 걸음을 다 써서 중간에 끊긴 턴인가. 끊겼을 때 남은 할 일을 여기 담는다.
|
|
1823
2073
|
*
|
|
@@ -2024,6 +2274,7 @@ export async function chatLoop(opts = {}) {
|
|
|
2024
2274
|
// 같이 돈 것은 이름을 다시 적어 준다. 안 그러면 어느 결과인지 모른다.
|
|
2025
2275
|
if (ev.parallel || ev.showLabel) say(` ${toolLabel(ev.name, ev.args)}`);
|
|
2026
2276
|
if (ev.name === 'TodoWrite' && ev.result?.todos) {
|
|
2277
|
+
이번턴할일적었나 = true;
|
|
2027
2278
|
// 사무실 화이트보드도 이걸 본다. 할 일은 세션이 아니라 턴 문맥에
|
|
2028
2279
|
// 있어서 상자가 스스로 못 읽는다 — 아는 자리에서 넣어 준다.
|
|
2029
2280
|
화면.할일갱신(ev.result.todos);
|
|
@@ -2061,10 +2312,24 @@ export async function chatLoop(opts = {}) {
|
|
|
2061
2312
|
* 다른 일이다. 화면에 초록 ✓ 가 없으면 아직 아무도 안 돌려 본
|
|
2062
2313
|
* 것이고, 그 구분이 상태줄에 있어야 사람이 속지 않는다.
|
|
2063
2314
|
*/
|
|
2064
|
-
|
|
2315
|
+
/*
|
|
2316
|
+
* ── 안 돈 것을 돌았다고 세지 않는다 ──────────────────────────
|
|
2317
|
+
*
|
|
2318
|
+
* `ev.result` 만 보면 `{error:'거부됨'}` · `{error:'모르는 도구'}`
|
|
2319
|
+
* 에도 돈횟수 가 오른다. 상태줄은 돈횟수 가 0 이 아니고 탈 이
|
|
2320
|
+
* 없으면 **초록 ✓0** 을 그리는데(ui/status.js), 그 표시의 뜻은
|
|
2321
|
+
* 같은 파일이 「초록 ✓ 가 없으면 아직 아무도 안 돌려 본 것이다」
|
|
2322
|
+
* 라고 못박아 두었다. 거부된 Verify 가 초록 ✓ 로 뜨면 그 문장이
|
|
2323
|
+
* 거꾸로 선다 — 안 돌려 본 것을 돌려 봤다고 읽게 된다.
|
|
2324
|
+
*
|
|
2325
|
+
* 못 확인한 것도 센다. 도구 한 줄에서는 노랑으로 보여 주면서
|
|
2326
|
+
* 쌓는 자리에서만 사라지고 있었다.
|
|
2327
|
+
*/
|
|
2328
|
+
if (ev.name === 'Verify' && ev.result && !ev.result.error) {
|
|
2065
2329
|
session.검증.돈횟수 += 1;
|
|
2066
2330
|
session.검증.확인 += ev.result.확인됨 ?? 0;
|
|
2067
2331
|
session.검증.탈 += ev.result.탈 ?? 0;
|
|
2332
|
+
session.검증.못확인 = (session.검증.못확인 ?? 0) + (ev.result.못확인 ?? 0);
|
|
2068
2333
|
}
|
|
2069
2334
|
if (ev.result?.되돌릴것?.length) {
|
|
2070
2335
|
const 것들 = ev.result.되돌릴것;
|
|
@@ -2364,7 +2629,24 @@ export async function chatLoop(opts = {}) {
|
|
|
2364
2629
|
* (오류로 끝나면 뒤에 done 이 없다), 무엇이 잘렸는지 보라고 띄운
|
|
2365
2630
|
* 경고가 정작 그 답의 마지막 줄보다 **위에** 찍힌다.
|
|
2366
2631
|
*/
|
|
2632
|
+
/*
|
|
2633
|
+
* ── 이 넷도 「탈 없이 끝난 턴」 이 아니다 ────────────────────────
|
|
2634
|
+
*
|
|
2635
|
+
* 아래 계획 승인은 `계획승인받나 && !턴탈났나` 로 걸린다. 그 규칙을
|
|
2636
|
+
* 적어 둔 주석은 「계획이 반만 나온 것을 두고 이대로 진행할까요? 를
|
|
2637
|
+
* 묻는 꼴이 된다」 였는데, 정작 **계획이 반만 나오는 대표 사건 넷**이
|
|
2638
|
+
* 이 깃발을 안 세우고 있었다:
|
|
2639
|
+
* capped — 출력 상한에서 글이 잘렸다
|
|
2640
|
+
* cutoff — 서버가 끝난 까닭도 없이 흐름을 멈췄다
|
|
2641
|
+
* limit — 걸음 수를 다 썼다
|
|
2642
|
+
* refusal — 모델이 거절했다. 계획이 아예 없다
|
|
2643
|
+
* 넷 다 for-await 는 예외 없이 끝나므로 깃발은 거짓인 채 남았다.
|
|
2644
|
+
* 그래서 3단계에서 잘린 반쪽에 `⏎ 그대로 진행` 이 뜨고, 창 제목은
|
|
2645
|
+
* 「끝남」 이었다. limit 은 한 술 더 떠서, 이 갈래가 이겨 버리는 바람에
|
|
2646
|
+
* 「끊긴 자리에서 이어서」 물음(else if)이 아예 안 돌았다.
|
|
2647
|
+
*/
|
|
2367
2648
|
case 'capped':
|
|
2649
|
+
턴탈났나 = true;
|
|
2368
2650
|
clearThinking();
|
|
2369
2651
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
2370
2652
|
say(` ${mark.warn} ${c.gray(`답이 ${c.white(ev.cap.toLocaleString())} 토큰에서 잘렸습니다`)}`
|
|
@@ -2379,6 +2661,7 @@ export async function chatLoop(opts = {}) {
|
|
|
2379
2661
|
* /out 을 권하면 안 된다 — 상한 얘기가 아니라 연결 얘기다.
|
|
2380
2662
|
*/
|
|
2381
2663
|
case 'cutoff':
|
|
2664
|
+
턴탈났나 = true;
|
|
2382
2665
|
clearThinking();
|
|
2383
2666
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
2384
2667
|
if (ev.멎은초) {
|
|
@@ -2398,6 +2681,7 @@ export async function chatLoop(opts = {}) {
|
|
|
2398
2681
|
break;
|
|
2399
2682
|
|
|
2400
2683
|
case 'limit':
|
|
2684
|
+
턴탈났나 = true;
|
|
2401
2685
|
끊긴할일 = ev.남은할일 ?? [];
|
|
2402
2686
|
say('');
|
|
2403
2687
|
say(` ${mark.warn} 도구 호출 ${ev.steps}회에서 멈췄습니다.`);
|
|
@@ -2489,10 +2773,25 @@ export async function chatLoop(opts = {}) {
|
|
|
2489
2773
|
}
|
|
2490
2774
|
break;
|
|
2491
2775
|
|
|
2776
|
+
/*
|
|
2777
|
+
* 훅이 막았다. **왜 막았는지**까지 적는다.
|
|
2778
|
+
*
|
|
2779
|
+
* 여태 `ev.말` 을 안 썼다. 거기에는 safety/hooks.js 의 막힘말 이
|
|
2780
|
+
* 들어 있고, 그 안에 두 가지가 담긴다 — 훅이 뱉은 진짜 사유와,
|
|
2781
|
+
* 「훅을 못 돌렸습니다 — <오류>」·「훅이 <코드> 로 끝났습니다」.
|
|
2782
|
+
* 그걸 버리면 화면은 `✗ 훅이 막았습니다 (dlp-check)` 한 줄뿐이라,
|
|
2783
|
+
* 사내 정책이 진짜로 막은 것인지 훅 파일이 없어져 못 돈 것인지
|
|
2784
|
+
* 구별할 수가 없다. 뒤엣것은 사람이 고칠 수 있는 탈인데도.
|
|
2785
|
+
*/
|
|
2492
2786
|
case 'hook_block':
|
|
2493
2787
|
clearThinking();
|
|
2494
2788
|
say('');
|
|
2495
2789
|
say(` ${c.yellow('✗')} ${c.white(옮긴말('ev.hookBlock'))} ${c.gray(`(${ev.훅?.이름 ?? ev.훅?.명령 ?? ''})`)}`);
|
|
2790
|
+
if (ev.말) {
|
|
2791
|
+
for (const 한줄 of String(ev.말).split('\n').slice(0, 4)) {
|
|
2792
|
+
if (한줄.trim()) say(` ${c.gray(clip(한줄.trim(), 88))}`);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2496
2795
|
break;
|
|
2497
2796
|
|
|
2498
2797
|
case 'nudge':
|
|
@@ -2512,6 +2811,7 @@ export async function chatLoop(opts = {}) {
|
|
|
2512
2811
|
* 또 거절이고, 요금만 두 배가 된다. 거절은 거절이라고 적는다.
|
|
2513
2812
|
*/
|
|
2514
2813
|
case 'refusal': {
|
|
2814
|
+
턴탈났나 = true;
|
|
2515
2815
|
clearThinking();
|
|
2516
2816
|
// 아래에서 볼 값이라 **지우기 전에** 챙긴다. 순서를 놓치면
|
|
2517
2817
|
// `!streamed` 가 늘 참이 되어 화면에 이미 있는 답을 또 찍는다.
|
|
@@ -2571,6 +2871,10 @@ export async function chatLoop(opts = {}) {
|
|
|
2571
2871
|
// 중간에 끊겨도 자리는 비운다. 안 비우면 다음 턴이 시작될 때까지
|
|
2572
2872
|
// 아무도 안 도는 방에 사람이 앉아 있다.
|
|
2573
2873
|
화면.함께갱신(0);
|
|
2874
|
+
// 화면만 비우면 안 된다. 세는 값도 같이 내린다 — 하위 작업이 도는 중에
|
|
2875
|
+
// 턴이 터지면 task_done 이 안 오고, 그 1 이 남아서 다음 턴의 첫 하위
|
|
2876
|
+
// 작업이 2 로 뜬다. 사무실에 앉은 사람 수가 실제와 어긋난다.
|
|
2877
|
+
도는하위 = 0;
|
|
2574
2878
|
화면.하위갱신(0);
|
|
2575
2879
|
clearInterval(제목시계);
|
|
2576
2880
|
/*
|
|
@@ -2585,7 +2889,24 @@ export async function chatLoop(opts = {}) {
|
|
|
2585
2889
|
* 다시 그려지고, 거기서 이력 파일을 열면 타이핑이 끊긴다. 턴 경계에서
|
|
2586
2890
|
* 한 번 세어 두면 화면은 그 숫자만 읽으면 된다.
|
|
2587
2891
|
*/
|
|
2588
|
-
|
|
2892
|
+
/*
|
|
2893
|
+
* 못 세면 **옛 숫자를 지운다.**
|
|
2894
|
+
*
|
|
2895
|
+
* 여태 `catch {}` 라 직전 값이 그대로 남았고, 상태줄은 계속 `↩ 3` 을
|
|
2896
|
+
* 그렸다. 그 숫자를 status.js 는 「안전망의 잔량」 이라고 부른다 —
|
|
2897
|
+
* 이력을 못 읽는 상태는 곧 /undo 도 못 하는 상태인데, 화면에는 되돌릴
|
|
2898
|
+
* 것이 셋 남았다고 떠 있는 셈이다. 한 번은 말하고, 숫자는 내린다.
|
|
2899
|
+
*/
|
|
2900
|
+
try {
|
|
2901
|
+
session.되돌릴턴 = ctx.history.turns().length;
|
|
2902
|
+
} catch (e) {
|
|
2903
|
+
session.되돌릴턴 = 0;
|
|
2904
|
+
if (!되돌리기못읽음말했나) {
|
|
2905
|
+
되돌리기못읽음말했나 = true;
|
|
2906
|
+
say(` ${mark.warn} ${c.gray('되돌리기 이력을 못 읽었습니다')} ${c.gray('— ' + clip(String(e?.message ?? e), 60))}`);
|
|
2907
|
+
say(` ${c.gray('이 판에서는')} ${c.cyan('/undo')} ${c.gray('가 안 될 수 있습니다.')}`);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2589
2910
|
if (알릴까({ 걸린밀리초: Date.now() - started, 켬: 알림.켬 })) 종();
|
|
2590
2911
|
창제목(제목글(턴탈났나 ? '탈남' : '끝남', { 폴더: 알림.폴더 }));
|
|
2591
2912
|
turn = null;
|
|
@@ -2654,7 +2975,23 @@ export async function chatLoop(opts = {}) {
|
|
|
2654
2975
|
* 애초에 계획 같은 걸 안 보려 든다.
|
|
2655
2976
|
*/
|
|
2656
2977
|
if (계획승인받나 && !턴탈났나) {
|
|
2657
|
-
|
|
2978
|
+
/*
|
|
2979
|
+
* ── 이번 턴에 적은 계획만 이번 계획이다 ──────────────────────────
|
|
2980
|
+
*
|
|
2981
|
+
* `ctx` 는 켤 때 한 번 만들어 세션 내내 쓰이고, `ctx.todos` 를 비우는
|
|
2982
|
+
* 자리는 어디에도 없다. 그래서 이런 판이 난다:
|
|
2983
|
+
*
|
|
2984
|
+
* 턴 A 로그인 작업이 걸음 상한에 걸려 안 끝난 할 일 둘이 남는다
|
|
2985
|
+
* 턴 B 「결제 모듈 설계하고 만들어줘」 → 겹침이라 계획 승인이 켜진다
|
|
2986
|
+
* 모델은 TodoWrite 없이 산문으로만 설계를 낸다
|
|
2987
|
+
* 화면 ┌ 계획 / 1. 로그인 폼 만들기 / 2. 세션 쿠키 / ⏎ 그대로 진행
|
|
2988
|
+
*
|
|
2989
|
+
* ⏎ 를 누르면 그 **지난 턴 목록**이 「방금 낸 계획」 이라는 이름표를
|
|
2990
|
+
* 달고 모델에게 실려 나간다. 이 파일은 「TodoWrite 를 안 쓴 계획도
|
|
2991
|
+
* 있다」 를 이미 대비해 뒀지만(할일.length === 0 갈래), 목록이 **비지
|
|
2992
|
+
* 않고 옛것으로 차 있는** 경우가 그 그물을 통과했다.
|
|
2993
|
+
*/
|
|
2994
|
+
const 할일 = 이번턴할일적었나 ? (ctx.todos ?? []).filter((t) => t.state !== 'done') : [];
|
|
2658
2995
|
say('');
|
|
2659
2996
|
계획상자(할일);
|
|
2660
2997
|
say('');
|
|
@@ -2733,7 +3070,15 @@ export async function chatLoop(opts = {}) {
|
|
|
2733
3070
|
? 옮긴말('plan.replanStep', { 단계: 계획답.단계 })
|
|
2734
3071
|
: 옮긴말('plan.replan'))}`);
|
|
2735
3072
|
}
|
|
2736
|
-
|
|
3073
|
+
/*
|
|
3074
|
+
* 빈 배열은 「이어갈 것이 있다」 가 아니다.
|
|
3075
|
+
*
|
|
3076
|
+
* `끊긴할일 = ev.남은할일 ?? []` 라 할 일이 0개인 채 걸음 상한에
|
|
3077
|
+
* 걸려도 여기가 참이 됐다. 그러면 물음이 뜨고, ⏎ 한 번에 「하던
|
|
3078
|
+
* 자리에서 이어서 해라」 로 모델을 한 판 더 부른다 — 이어갈 자리가
|
|
3079
|
+
* 없는데.
|
|
3080
|
+
*/
|
|
3081
|
+
} else if (끊긴할일?.length) {
|
|
2737
3082
|
/*
|
|
2738
3083
|
* ── 끊긴 자리에서 ⏎ 하나로 잇는다 ────────────────────────────────
|
|
2739
3084
|
*
|
package/src/tools/index.js
CHANGED
|
@@ -2769,7 +2769,17 @@ export async function runTool(name, args, ctx) {
|
|
|
2769
2769
|
// 섞이면 "이 폴더 밖은 못 건드린다" 는 말이 거짓이 된다.
|
|
2770
2770
|
if (name.startsWith('mcp__')) return await runMcpTool(name, args, ctx);
|
|
2771
2771
|
|
|
2772
|
-
|
|
2772
|
+
/*
|
|
2773
|
+
* 있는 도구인지는 hasOwn 으로만 묻는다.
|
|
2774
|
+
*
|
|
2775
|
+
* 이름은 **모델이 지어서 보낸 것**이다. `constructor` · `toString` 으로
|
|
2776
|
+
* 오면 `TOOLS[name]` 이 물려받은 함수라 참이 되어 이 관문을 통과하고,
|
|
2777
|
+
* 조금 아래 `t.run(...)` 에서 터진다. 화면에 남는 말은 「모르는 도구:
|
|
2778
|
+
* toString」 이 아니라 `t.run is not a function` 이다 — 사람도 모델도
|
|
2779
|
+
* 무엇이 잘못됐는지 알 수 없는 글이고, 모델은 고칠 실마리가 없으니
|
|
2780
|
+
* 같은 이름으로 또 부른다.
|
|
2781
|
+
*/
|
|
2782
|
+
const t = Object.hasOwn(TOOLS, name) ? TOOLS[name] : null;
|
|
2773
2783
|
if (!t) return { error: `모르는 도구: ${name}` };
|
|
2774
2784
|
|
|
2775
2785
|
/*
|
package/src/ui/export.js
CHANGED
|
@@ -214,7 +214,7 @@ ${조각.join('\n')}
|
|
|
214
214
|
*
|
|
215
215
|
* @returns {string|null} 적은 자리. 못 적었으면 null — 못 적어도 대화는 계속된다.
|
|
216
216
|
*/
|
|
217
|
-
export function 보고서적기(root, session, opts = {}) {
|
|
217
|
+
export function 보고서적기(root, session, opts = {}, 탈받을것 = null) {
|
|
218
218
|
try {
|
|
219
219
|
const 폴더 = join(root, '.deel', 'export');
|
|
220
220
|
mkdirSync(폴더, { recursive: true });
|
|
@@ -227,7 +227,10 @@ export function 보고서적기(root, session, opts = {}) {
|
|
|
227
227
|
const 자리 = join(폴더, 이름);
|
|
228
228
|
writeFileSync(자리, 보고서짓기(session, opts), 'utf8');
|
|
229
229
|
return 자리;
|
|
230
|
-
} catch {
|
|
230
|
+
} catch (e) {
|
|
231
|
+
// 못 적은 까닭을 버리지 않는다 — 화면에 「못 남겼습니다」 만 뜨면 디스크가
|
|
232
|
+
// 찼는지 폴더가 읽기 전용인지 알 길이 없다. 받을 그릇을 준 쪽에만 담는다.
|
|
233
|
+
if (탈받을것) 탈받을것.왜 = String(e?.message ?? e);
|
|
231
234
|
return null;
|
|
232
235
|
}
|
|
233
236
|
}
|
package/src/ui/pastechip.js
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
// 바이트 그대로다. 되돌릴 수 없는 접기는 접기가 아니라 잘라내기다.
|
|
22
22
|
import { 접어쓰기 } from './wrap.js';
|
|
23
23
|
import { 안쪽최대 } from './inputbox.js';
|
|
24
|
+
// 붙여넣기를 다루는 쪽은 이 수를 같이 봐야 한다 — 화면에 몇 줄까지 펴서
|
|
25
|
+
// 남길지가 상자에 몇 줄이 들어가는지와 같은 값이라야 어긋나지 않는다.
|
|
26
|
+
export { 안쪽최대 };
|
|
24
27
|
import { 말, 말모두 } from '../i18n/index.js';
|
|
25
28
|
|
|
26
29
|
/** 사람이 읽는 크기. 47줄이 몇 KB 인지 알아야 뭘 붙였는지 감이 온다. */
|
package/src/ui/status.js
CHANGED
|
@@ -260,12 +260,30 @@ export const SEGMENTS = {
|
|
|
260
260
|
*/
|
|
261
261
|
verify: {
|
|
262
262
|
get desc() { return 말('seg.verify'); },
|
|
263
|
+
/*
|
|
264
|
+
* ── 못 확인한 것은 확인한 것이 아니다 ────────────────────────────
|
|
265
|
+
*
|
|
266
|
+
* 여태 갈래가 둘뿐이었다 — 탈이 있으면 빨강, 아니면 초록. 그래서
|
|
267
|
+
* 「확인 0 · 못 확인 5」 가 초록 ✓0 으로 떴다. 바로 위에 적어 둔
|
|
268
|
+
* 「초록 ✓ 가 없으면 아직 아무도 안 돌려 본 것이다」 를 뒤집어 읽으면,
|
|
269
|
+
* 초록이 떠 있으니 돌려 봤고 탈이 없다는 뜻이 된다 — 다섯 가지를
|
|
270
|
+
* 확인조차 못 한 판에서.
|
|
271
|
+
*
|
|
272
|
+
* 노랑을 하나 더 둔다. 빨강(탈이 났다) 과 초록(다 확인했다) 사이에
|
|
273
|
+
* 「아직 모른다」 가 있어야 이 표시가 거짓말을 안 한다.
|
|
274
|
+
*/
|
|
263
275
|
make: (s) => {
|
|
264
276
|
const v = s.검증;
|
|
265
277
|
if (!v?.돈횟수) return null;
|
|
266
|
-
|
|
278
|
+
if (v.탈) return c.hred(`✓${v.확인} ✗${v.탈}`);
|
|
279
|
+
return v.못확인 ? c.hyellow(`✓${v.확인} ?${v.못확인}`) : c.hgreen(`✓${v.확인}`);
|
|
280
|
+
},
|
|
281
|
+
short: (s) => {
|
|
282
|
+
const v = s.검증;
|
|
283
|
+
if (!v?.돈횟수) return null;
|
|
284
|
+
if (v.탈) return c.hred(`✗${v.탈}`);
|
|
285
|
+
return v.못확인 ? c.hyellow(`?${v.못확인}`) : c.hgreen(`✓${v.확인}`);
|
|
267
286
|
},
|
|
268
|
-
short: (s) => (s.검증?.돈횟수 ? (s.검증.탈 ? c.hred(`✗${s.검증.탈}`) : c.hgreen(`✓${s.검증.확인}`)) : null),
|
|
269
287
|
},
|
|
270
288
|
|
|
271
289
|
/*
|