deel-local-cli 1.20.10 → 1.20.12
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 +4 -2
- package/README.md +4 -2
- package/bin/deel.js +13 -3
- package/package.json +1 -1
- package/src/acp/serve.js +5 -3
- package/src/agent/commit.js +105 -21
- package/src/agent/loop.js +39 -4
- package/src/agent/session.js +279 -33
- package/src/agent/sessionui.js +14 -4
- package/src/agent/store.js +133 -33
- package/src/agent/threads.js +52 -1
- package/src/commands.js +146 -13
- package/src/config.js +87 -9
- package/src/i18n/en.js +7 -5
- package/src/i18n/ja.js +4 -2
- package/src/i18n/ko.js +7 -5
- package/src/i18n/zh.js +4 -2
- package/src/lsp/client.js +82 -19
- package/src/oneshot.js +3 -2
- package/src/plugins/manage.js +15 -6
- package/src/repl.js +55 -2
- package/src/reset.js +37 -6
- package/src/safety/hooks.js +4 -1
- package/src/safety/runmode.js +15 -0
- package/src/safety/undo.js +98 -8
- package/src/setup.js +26 -5
- package/src/skills/discover.js +10 -1
- package/src/tools/fsutil.js +7 -1
- package/src/tools/index.js +427 -30
- package/src/tools/webfetch.js +84 -20
- package/src/ui/intro.js +2 -1
- package/src/ui/md.js +123 -25
- package/src/ui/motion.js +40 -9
- package/src/ui/office.js +22 -1
- package/src/ui/screen.js +60 -4
- package/src/ui/working.js +4 -1
package/src/agent/store.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// 마지막 줄까지는 성하다. 통째로 다시 쓰는 방식이면 그 순간 파일이 깨진다.
|
|
15
15
|
import {
|
|
16
16
|
existsSync, mkdirSync, readdirSync, readFileSync,
|
|
17
|
-
appendFileSync, writeFileSync, statSync, rmSync, chmodSync,
|
|
17
|
+
appendFileSync, writeFileSync, statSync, rmSync, chmodSync, renameSync,
|
|
18
18
|
} from 'node:fs';
|
|
19
19
|
import { join } from 'node:path';
|
|
20
20
|
|
|
@@ -41,7 +41,14 @@ export function freeId(dir, at = new Date()) {
|
|
|
41
41
|
const id = `${base}-${n}`;
|
|
42
42
|
if (!existsSync(join(dir, `${id}.jsonl`))) return id;
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
// 여기까지 왔으면 마지막 이름도 비어 있는지 봐야 한다. 안 보고 돌려주면
|
|
45
|
+
// begin() 이 같은 이름으로 쉰 번을 되풀이하다 **아무 파일도 없이** 대화를
|
|
46
|
+
// 시작한다 — 저장한다고 적어 놓고 한 줄도 안 남는 판이 그때 만들어진다.
|
|
47
|
+
for (let n = 0; n < 1000; n++) {
|
|
48
|
+
const id = n ? `${base}-${process.pid}-${n}` : `${base}-${process.pid}`;
|
|
49
|
+
if (!existsSync(join(dir, `${id}.jsonl`))) return id;
|
|
50
|
+
}
|
|
51
|
+
return `${base}-${process.pid}-${Date.now().toString(36)}`;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export class Store {
|
|
@@ -86,6 +93,12 @@ export class Store {
|
|
|
86
93
|
#잠그기() {
|
|
87
94
|
if (this.#잠갔나) return;
|
|
88
95
|
this.#잠갔나 = true;
|
|
96
|
+
/*
|
|
97
|
+
* 윈도우에서 chmod 는 **아무 일도 안 하고 성공한다.** 그 성공을 그대로
|
|
98
|
+
* 적으면 바로 위 머리말이 스스로 금지한 「잠근 척」 이 된다 — 같은 PC 를
|
|
99
|
+
* 여럿이 쓰는 사람이 화면만 보고 안심한다. 안 걸었으면 안 걸었다고 적는다.
|
|
100
|
+
*/
|
|
101
|
+
if (process.platform === 'win32') { this.잠금 = { 못함: 'windows' }; return; }
|
|
89
102
|
try { chmodSync(this.file, 0o600); this.잠금 = { 모드: 0o600 }; }
|
|
90
103
|
catch (err) { this.잠금 = { 못함: err?.code ?? String(err) }; }
|
|
91
104
|
}
|
|
@@ -96,8 +109,10 @@ export class Store {
|
|
|
96
109
|
// 끊긴다 — 기록을 남기려다 작업을 죽이면 본말이 뒤집힌다.
|
|
97
110
|
// 조용히 넘기는 것도 아니다: 폴더가 없으면 바로 아래 쓰기가 깨지고,
|
|
98
111
|
// 그 자리에서 세어 화면에 오른다.
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
// 못 만들었으면 **연 것이 아니다.** 연 것으로 적어 두면 잠깐 막혔던 것이
|
|
113
|
+
// 풀린 뒤에도 두 번 다시 안 만들어 본다 — 그 대화는 끝까지 한 줄도 안 남는다.
|
|
114
|
+
try { mkdirSync(this.dir, { recursive: true }); this.opened = true; }
|
|
115
|
+
catch { /* 아래 쓰기가 센다. 다음 줄에서 다시 만들어 본다 */ }
|
|
101
116
|
}
|
|
102
117
|
|
|
103
118
|
/*
|
|
@@ -161,14 +176,19 @@ export class Store {
|
|
|
161
176
|
this.#use(freeId(this.dir)); // 누가 채 갔다. 옆자리로.
|
|
162
177
|
}
|
|
163
178
|
}
|
|
179
|
+
// 쉰 번을 다 써도 자리를 못 잡았다. 여기서 잠자코 돌아가면 화면은
|
|
180
|
+
// 「계속 저장되고 있습니다」 라고 말하는데 파일은 하나도 없다.
|
|
181
|
+
this.#못썼다({ code: 'EEXIST' });
|
|
164
182
|
return this;
|
|
165
183
|
}
|
|
166
184
|
|
|
185
|
+
/** 한 줄 적는다. 적었으면 true. 못 적었으면 false 이고 셈에 오른다. */
|
|
167
186
|
#write(obj) {
|
|
168
187
|
try {
|
|
169
188
|
appendFileSync(this.file, JSON.stringify(obj) + '\n', 'utf8');
|
|
170
189
|
this.#잠그기();
|
|
171
|
-
|
|
190
|
+
return true;
|
|
191
|
+
} catch (err) { this.#못썼다(err); return false; } // 계속은 하되, 몇 줄을 잃었는지는 센다
|
|
172
192
|
}
|
|
173
193
|
|
|
174
194
|
// 메시지 하나를 덧붙인다. 대화가 진행되는 대로 즉시 남긴다.
|
|
@@ -225,17 +245,24 @@ export class Store {
|
|
|
225
245
|
* 그 값은 매번 사람이 기다리는 시간이다. pins 를 사람이 고칠 때만 적는
|
|
226
246
|
* 것과 같은 뜻이다.
|
|
227
247
|
*/
|
|
248
|
+
/*
|
|
249
|
+
* 적은 것으로 치는 것은 **정말 적힌 뒤**다.
|
|
250
|
+
*
|
|
251
|
+
* 못 적었는데 적은 셈으로 표시해 두면, 디스크가 다시 나아져도 그 값은 이미
|
|
252
|
+
* 「안 바뀐 것」 이라 두 번 다시 안 적힌다. 잠깐 막혔던 것이 영영 사라지는
|
|
253
|
+
* 자리다 — 여기가 바로 남은 할 일과 시킨 말이 조용히 없어지던 길이다.
|
|
254
|
+
*/
|
|
228
255
|
살림적기(session = this.따라갈세션) {
|
|
229
256
|
if (!session) return;
|
|
230
257
|
const 할일 = JSON.stringify(session.할일 ?? []);
|
|
231
|
-
if (할일 !== this.적은할일
|
|
258
|
+
if (할일 !== this.적은할일
|
|
259
|
+
&& this.#write({ t: 'todo', at: new Date().toISOString(), 목록: session.할일 ?? [] })) {
|
|
232
260
|
this.적은할일 = 할일;
|
|
233
|
-
this.#write({ t: 'todo', at: new Date().toISOString(), 목록: session.할일 ?? [] });
|
|
234
261
|
}
|
|
235
262
|
const 요청 = String(session.이번요청 ?? '');
|
|
236
|
-
if (요청 !== this.적은요청
|
|
263
|
+
if (요청 !== this.적은요청
|
|
264
|
+
&& this.#write({ t: 'request', at: new Date().toISOString(), 글: 요청 })) {
|
|
237
265
|
this.적은요청 = 요청;
|
|
238
|
-
this.#write({ t: 'request', at: new Date().toISOString(), 글: 요청 });
|
|
239
266
|
}
|
|
240
267
|
}
|
|
241
268
|
|
|
@@ -289,15 +316,28 @@ export class Store {
|
|
|
289
316
|
if (이번요청) lines.push(JSON.stringify({ t: 'request', at: new Date().toISOString(), 글: 이번요청 }));
|
|
290
317
|
for (const m of messages) lines.push(JSON.stringify({ t: 'msg', m }));
|
|
291
318
|
/*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
319
|
+
* 옆에 다 쓰고 나서 **한 번에 갈아 끼운다.**
|
|
320
|
+
*
|
|
321
|
+
* 이 파일 머리말이 jsonl 을 고른 까닭이 「통째로 다시 쓰면 그 순간 파일이
|
|
322
|
+
* 깨진다」 인데, 정작 통째로 다시 쓰는 자리가 여기다. 디스크가 반쯤 쓰다
|
|
323
|
+
* 차면 원본은 이미 잘려 있고 새것은 안 끝났다 — 그 한 번에 대화 전체가
|
|
324
|
+
* 없어진다. 옆에 써 두고 이름만 바꾸면 성한 쪽 아니면 성한 쪽이다.
|
|
325
|
+
*
|
|
326
|
+
* 실패하면 옆에 쓴 것을 치운다. 안 치우면 폴더에 부스러기가 쌓이고,
|
|
327
|
+
* 다음에 볼 사람은 그게 대화인지 찌꺼기인지 모른다.
|
|
295
328
|
*/
|
|
329
|
+
const 옆 = `${this.file}.새로`;
|
|
296
330
|
try {
|
|
297
|
-
writeFileSync(
|
|
331
|
+
writeFileSync(옆, lines.join('\n') + '\n', 'utf8');
|
|
332
|
+
// 윈도우에서는 읽기 전용 파일 위로 이름을 못 바꾼다. 우리 파일이니 푼다.
|
|
333
|
+
try { chmodSync(this.file, 0o600); } catch { /* 없거나 이미 쓸 수 있다 */ }
|
|
334
|
+
renameSync(옆, this.file);
|
|
298
335
|
this.#잠갔나 = false;
|
|
299
336
|
this.#잠그기();
|
|
300
|
-
} catch (err) {
|
|
337
|
+
} catch (err) {
|
|
338
|
+
try { rmSync(옆, { force: true }); } catch { /* 못 치워도 원본은 성하다 */ }
|
|
339
|
+
this.#못썼다(err);
|
|
340
|
+
}
|
|
301
341
|
}
|
|
302
342
|
|
|
303
343
|
readMeta() {
|
|
@@ -343,8 +383,16 @@ export class Store {
|
|
|
343
383
|
|
|
344
384
|
// 사람에게 보여줄 첫 마디. 이게 목록에서 대화를 알아보는 유일한 단서다.
|
|
345
385
|
function firstAsk(messages) {
|
|
346
|
-
const
|
|
347
|
-
|
|
386
|
+
const 쓸만한 = messages.filter((x) => x.role === 'user' && typeof x.content === 'string' && x.content.trim());
|
|
387
|
+
/*
|
|
388
|
+
* 대괄호로 여는 것은 우리가 끼워 넣은 글이라 건너뛴다. 그런데 사람도
|
|
389
|
+
* 「[급함] 로그인이 안 됩니다」 처럼 적는다 — 그 대화는 목록에서 「(빈
|
|
390
|
+
* 대화)」 가 됐다. 있는 대화를 없다고 적은 것이다.
|
|
391
|
+
*
|
|
392
|
+
* 그래서 건너뛰되, 남는 것이 하나도 없으면 첫 마디를 그대로 쓴다.
|
|
393
|
+
* 알아보기 어려운 제목이 「빈 대화」 라는 거짓말보다 낫다.
|
|
394
|
+
*/
|
|
395
|
+
const m = 쓸만한.find((x) => !x.content.trim().startsWith('[')) ?? 쓸만한[0];
|
|
348
396
|
return m ? m.content.replace(/\s+/g, ' ').trim() : '(빈 대화)';
|
|
349
397
|
}
|
|
350
398
|
|
|
@@ -387,29 +435,73 @@ export function list(root, { limit = 20, 속까지 = true } = {}) {
|
|
|
387
435
|
const out = [];
|
|
388
436
|
for (const c of 후보) {
|
|
389
437
|
if (out.length >= limit) break;
|
|
390
|
-
const
|
|
391
|
-
if (
|
|
392
|
-
out.push({
|
|
393
|
-
...c,
|
|
394
|
-
model: meta?.model ?? '?',
|
|
395
|
-
turns: messages.filter((m) => m.role === 'user').length,
|
|
396
|
-
messages: messages.length,
|
|
397
|
-
first: firstAsk(messages),
|
|
398
|
-
});
|
|
438
|
+
const 줄 = 한줄(root, c);
|
|
439
|
+
if (줄) out.push(줄);
|
|
399
440
|
}
|
|
400
441
|
return out;
|
|
401
442
|
}
|
|
402
443
|
|
|
444
|
+
/**
|
|
445
|
+
* 후보 하나를 열어 목록 한 줄로 만든다. 빈 파일이면 null.
|
|
446
|
+
*
|
|
447
|
+
* **못 읽은 것은 없는 것이 아니다.**
|
|
448
|
+
*
|
|
449
|
+
* load() 는 까닭을 담아 돌려주는데 부르는 쪽이 그것을 버리고 있었다. 그래서
|
|
450
|
+
* 권한이 막히거나 다른 프로그램이 잡고 있는 대화는 목록에서 통째로 사라졌고,
|
|
451
|
+
* 그런 것뿐이면 화면은 「아직 없습니다. 지금 이 대화가 첫 번째입니다」 라고
|
|
452
|
+
* 말했다 — 어제 한 일이 그대로 있는데 없다고 한 것이다. 이어할 수는 없어도
|
|
453
|
+
* **거기 있다는 것**은 말해야 사람이 손을 쓴다.
|
|
454
|
+
*/
|
|
455
|
+
function 한줄(root, c) {
|
|
456
|
+
const { meta, messages, 못읽음 } = new Store(root, c.id).load();
|
|
457
|
+
if (못읽음) {
|
|
458
|
+
return { ...c, 못읽음, model: '?', turns: 0, messages: 0, first: `못 읽었습니다 — ${못읽음}` };
|
|
459
|
+
}
|
|
460
|
+
if (!messages.length) return null; // 빈 파일은 목록에 안 올린다
|
|
461
|
+
return {
|
|
462
|
+
...c,
|
|
463
|
+
model: meta?.model ?? '?',
|
|
464
|
+
turns: messages.filter((m) => m.role === 'user').length,
|
|
465
|
+
messages: messages.length,
|
|
466
|
+
first: firstAsk(messages),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
403
470
|
export function remove(root, id) {
|
|
471
|
+
/*
|
|
472
|
+
* 이름에 상위 경로가 섞이면 대화 폴더 **밖**을 지운다.
|
|
473
|
+
* `deel sessions --rm ../../어딘가` 는 화면에 「지웠습니다」 라고 적고
|
|
474
|
+
* 엉뚱한 파일을 없앤다 — 무엇을 지웠는지 사람이 못 알아본다.
|
|
475
|
+
*/
|
|
476
|
+
if (!/^[^\\/:*?"<>|]+$/.test(String(id)) || String(id).includes('..')) {
|
|
477
|
+
return { error: `대화 이름이 아닙니다: ${id}` };
|
|
478
|
+
}
|
|
404
479
|
const f = join(sessionsDir(root), `${id}.jsonl`);
|
|
405
480
|
if (!existsSync(f)) return { error: `그런 대화가 없습니다: ${id}` };
|
|
406
|
-
rmSync(f, { force: true });
|
|
481
|
+
try { rmSync(f, { force: true }); }
|
|
482
|
+
catch (err) { return { error: `못 지웠습니다: ${id} — ${err?.code ?? err?.message ?? String(err)}` }; }
|
|
407
483
|
return { removed: id };
|
|
408
484
|
}
|
|
409
485
|
|
|
410
|
-
/**
|
|
486
|
+
/**
|
|
487
|
+
* 가장 최근 대화. --continue 가 집어 오는 것.
|
|
488
|
+
*
|
|
489
|
+
* 못 읽는 것은 건너뛴다. 그것을 집어 오면 --continue 가 **빈 대화**로 열리고,
|
|
490
|
+
* 사람은 어제 하던 것이 이어진 줄 알고 말을 건다.
|
|
491
|
+
*/
|
|
411
492
|
export function latest(root) {
|
|
412
|
-
|
|
493
|
+
/*
|
|
494
|
+
* **한 개씩 열어 보고 첫 성한 것에서 멈춘다.**
|
|
495
|
+
*
|
|
496
|
+
* `list(root, { limit: 20 })` 로 스무 개를 통째로 읽으면 안 된다 — 이 파일이
|
|
497
|
+
* 위에서 경고한 그 자리다. 대화 파일 하나가 수 MB 이고, 이건 켤 때마다 돈다.
|
|
498
|
+
* 보통은 첫 파일 하나만 읽고 끝난다.
|
|
499
|
+
*/
|
|
500
|
+
for (const c of list(root, { limit: 20, 속까지: false })) {
|
|
501
|
+
const 줄 = 한줄(root, c);
|
|
502
|
+
if (줄 && !줄.못읽음) return 줄;
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
413
505
|
}
|
|
414
506
|
|
|
415
507
|
/**
|
|
@@ -417,10 +509,18 @@ export function latest(root) {
|
|
|
417
509
|
* 최근 keep 개는 무조건 남기고, 그보다 오래되고 days 를 넘긴 것만 지운다.
|
|
418
510
|
*/
|
|
419
511
|
export function prune(root, { keep = 30, days = 30 } = {}) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
512
|
+
/*
|
|
513
|
+
* 지울지 말지는 파일 시각만 보면 안다. 내용은 필요 없다 —
|
|
514
|
+
* 여기서 전부 읽던 것이 켤 때 몇 초씩 멈추던 원인이었다.
|
|
515
|
+
*
|
|
516
|
+
* 개수를 1000 으로 끊어 놓으면 정작 **제일 오래된 것들**이 그 뒤에 있어서
|
|
517
|
+
* 영영 안 지워진다. 폴더가 끝없이 자라지 말라고 있는 함수가 딱 그 일을
|
|
518
|
+
* 못 하게 된다. 열지도 않고 시각만 보는 목록이라 전부 봐도 싸다.
|
|
519
|
+
*/
|
|
520
|
+
const all = list(root, { limit: Infinity, 속까지: false });
|
|
423
521
|
const 자를것 = all.slice(keep).filter((s) => (Date.now() - s.at.getTime()) > days * 86400000);
|
|
424
|
-
|
|
425
|
-
|
|
522
|
+
// 지웠다고 세는 것은 **정말 지워진 것**만이다.
|
|
523
|
+
let 지운수 = 0;
|
|
524
|
+
for (const s of 자를것) { try { rmSync(s.file, { force: true }); 지운수 += 1; } catch { /* 잡혀 있으면 다음에 */ } }
|
|
525
|
+
return 지운수;
|
|
426
526
|
}
|
package/src/agent/threads.js
CHANGED
|
@@ -68,8 +68,53 @@ export class Threads {
|
|
|
68
68
|
}];
|
|
69
69
|
this.자리 = 0;
|
|
70
70
|
this.센것 = 1;
|
|
71
|
+
/*
|
|
72
|
+
* ── 닫은 갈래가 **쓴 돈**은 어디로 가나 ────────────────────────
|
|
73
|
+
*
|
|
74
|
+
* 여태 아무 데도 안 갔다. 닫기() 는 갈래를 splice 로 빼기만 했고,
|
|
75
|
+
* 그 갈래가 태운 토큰·호출·시간은 그 자리에서 사라졌다. 곁가지에서
|
|
76
|
+
* 오래 헤매다 닫고 본줄기로 돌아오면 `/cost` 는 그 값을 못 본다 —
|
|
77
|
+
* **실제로 낸 돈보다 적은 금액**이 화면에 뜬다.
|
|
78
|
+
*
|
|
79
|
+
* 갈래를 지운다고 청구서가 지워지지는 않는다. 그래서 여기에 쌓는다.
|
|
80
|
+
*/
|
|
81
|
+
this.닫힌셈 = { in: 0, out: 0, prompt: 0, calls: 0, ms: 0, retries: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, 못잰것: 0 };
|
|
82
|
+
this.닫힌수 = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* **모든 갈래**가 지금까지 쓴 것을 합친다. 닫은 갈래 것까지 센다.
|
|
87
|
+
*
|
|
88
|
+
* `/cost` 가 session.usage 하나만 보고 있었다. 그건 지금 갈래 것이다.
|
|
89
|
+
* 갈래를 셋 굴리면 화면의 금액은 그중 하나 몫이고, 갈래를 바꾸는
|
|
90
|
+
* 것만으로 숫자가 내려간다 — 돈은 그대로 나가는데.
|
|
91
|
+
*
|
|
92
|
+
* 지금 갈래 값은 **session 쪽**에서 가져온다.
|
|
93
|
+
*
|
|
94
|
+
* 처음에 여기 「갈래 표에 담긴 것은 마지막으로 옮길 때 담아 둔 값이라
|
|
95
|
+
* 이번 턴 것이 빠져 있다」 라고 적어 뒀는데, **그건 사실이 아니다.**
|
|
96
|
+
* #꺼내기() 가 `session.usage = g.usage` 로 **같은 객체를 물려** 주고,
|
|
97
|
+
* session 쪽에서 그 객체를 갈아 끼우는 자리는 생성자 말고는 없다.
|
|
98
|
+
* 그러니 지금은 둘이 늘 같은 값이다 — 어긋내기 판이 이 줄을 표 쪽으로
|
|
99
|
+
* 바꿔도 검사가 안 빨개져서 그걸로 알았다.
|
|
100
|
+
*
|
|
101
|
+
* 그래도 session 쪽을 읽는다. 언젠가 누가 usage 를 통째로 갈아 끼우면
|
|
102
|
+
* (되감기·이어받기가 그럴 만한 자리다) 표 쪽만 읽는 코드는 그 순간부터
|
|
103
|
+
* 조용히 적은 금액을 적는다. 값을 두 군데서 들고 있을 때는 **살아 있는
|
|
104
|
+
* 쪽**을 읽는 것이 맞다.
|
|
105
|
+
*/
|
|
106
|
+
전체usage() {
|
|
107
|
+
const 합 = { ...this.닫힌셈 };
|
|
108
|
+
for (let i = 0; i < this.갈래들.length; i++) {
|
|
109
|
+
const u = (i === this.자리 ? this.session.usage : this.갈래들[i].usage) ?? {};
|
|
110
|
+
for (const k of Object.keys(합)) 합[k] += Number(u[k] ?? 0);
|
|
111
|
+
}
|
|
112
|
+
return 합;
|
|
71
113
|
}
|
|
72
114
|
|
|
115
|
+
/** 지금 갈래 것과 전부의 것이 다른가. 다를 때만 화면에 두 줄이 필요하다. */
|
|
116
|
+
갈래여럿인가() { return this.갈래들.length > 1 || this.닫힌수 > 0; }
|
|
117
|
+
|
|
73
118
|
현재() { return this.갈래들[this.자리]; }
|
|
74
119
|
현재store() { return this.현재().store; }
|
|
75
120
|
개수() { return this.갈래들.length; }
|
|
@@ -128,7 +173,10 @@ export class Threads {
|
|
|
128
173
|
const g = {
|
|
129
174
|
이름: String(이름 ?? '').trim() || 기본이름(this.센것),
|
|
130
175
|
messages: 물려줄것 ? [...물려줄것] : [],
|
|
131
|
-
usage
|
|
176
|
+
// session.usage 와 **같은 모양**이어야 한다. 여태 다섯 칸만 있어서
|
|
177
|
+
// 갈래에서는 캐시·생각·못잰것 칸이 아예 없었다. 더하는 자리들이 전부
|
|
178
|
+
// `?? 0` 을 써서 터지지는 않았지만, 모양이 둘인 것 자체가 다음 사람의 함정이다.
|
|
179
|
+
usage: { in: 0, out: 0, prompt: 0, calls: 0, ms: 0, retries: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, 못잰것: 0 },
|
|
132
180
|
todos: null,
|
|
133
181
|
// 할 일도 todos 와 같이 비워서 시작한다. 곁가지는 다른 일을 하러 나가는
|
|
134
182
|
// 것이고, 물려받으면 본줄기의 남은 일을 곁가지 파일에도 적게 된다.
|
|
@@ -191,6 +239,9 @@ export class Threads {
|
|
|
191
239
|
if (i < 0) return { ok: false, why: '그런 갈래가 없습니다.' };
|
|
192
240
|
if (i === this.자리) this.#담아두기();
|
|
193
241
|
const [닫은것] = this.갈래들.splice(i, 1);
|
|
242
|
+
// 갈래는 지워도 그 갈래가 낸 돈은 안 지워진다 (생성자의 닫힌셈).
|
|
243
|
+
for (const k of Object.keys(this.닫힌셈)) this.닫힌셈[k] += Number(닫은것.usage?.[k] ?? 0);
|
|
244
|
+
this.닫힌수 += 1;
|
|
194
245
|
// 닫힌 자리보다 뒤에 있었으면 번호가 하나 당겨진다.
|
|
195
246
|
const 다음 = i === this.자리 ? Math.max(0, i - 1) : (this.자리 > i ? this.자리 - 1 : this.자리);
|
|
196
247
|
this.자리 = 다음;
|
package/src/commands.js
CHANGED
|
@@ -42,7 +42,7 @@ import { 보고서적기 } from './ui/export.js';
|
|
|
42
42
|
import { loadCommand, discover } from './skills/discover.js';
|
|
43
43
|
import { install, list, remove, pack } from './plugins/manage.js';
|
|
44
44
|
import { spin } from './ui/spinner.js';
|
|
45
|
-
import { 그림고르기설정,
|
|
45
|
+
import { 그림고르기설정, 끔설정, 환경그림, 환경으로껐나 } from './ui/motion.js';
|
|
46
46
|
import { 사무실설정, 최소높이, 최소폭 } from './ui/office.js';
|
|
47
47
|
|
|
48
48
|
/**
|
|
@@ -63,6 +63,28 @@ export function 적용하기(고른것) {
|
|
|
63
63
|
// 지워 버리면, 껐다고 믿는 자리에서 그림이 다시 돈다. 설정용 값은 따로 둔다.
|
|
64
64
|
끔설정(값 === '끔');
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `/motion` 이 환경변수에 대해 해야 하는 말. 없으면 빈 배열.
|
|
69
|
+
*
|
|
70
|
+
* 여태 이 자리는 「DEEL_MOTION·DEEL_OFFICE 가 켜져 있으면 그쪽이 이깁니다」
|
|
71
|
+
* 한 줄이 전부였다. 그래서 두 가지를 틀리게 말했다.
|
|
72
|
+
*
|
|
73
|
+
* · **DEEL_NO_MOTION 을 통째로 안 봤다.** 그게 켜져 있으면 무엇을 골라도
|
|
74
|
+
* 한 칸짜리 돌림표가 도는데, 화면은 「기사로 바꿨습니다 · 그 자리에서
|
|
75
|
+
* 바뀝니다」 라고 말한다. 아무것도 안 바뀐다.
|
|
76
|
+
* · **모르는 값도 이긴다고 말했다.** `DEEL_MOTION=cat` 은 무시되고 설정
|
|
77
|
+
* 파일이 그대로 쓰이는데, 「환경변수가 이깁니다」 라고 적혔다. 사람은
|
|
78
|
+
* 설정을 바꿔도 소용없다고 믿는다 — 사실은 그 설정이 이기고 있는데.
|
|
79
|
+
*/
|
|
80
|
+
function 환경이하는말() {
|
|
81
|
+
const 말들 = [];
|
|
82
|
+
const 그림 = 환경그림();
|
|
83
|
+
if (그림?.이김 || process.env.DEEL_OFFICE) 말들.push(c.gray(말('motion.envWins')));
|
|
84
|
+
if (그림 && !그림.이김) 말들.push(c.gray(말('motion.envUnknown', { 값: 그림.값 })));
|
|
85
|
+
if (환경으로껐나()) 말들.push(c.gray(말('motion.envOff')));
|
|
86
|
+
return 말들;
|
|
87
|
+
}
|
|
66
88
|
import { PROFILES, LEVELS as THINK_LEVELS, normalizeProfile, table as effortTable, 가벼운강도, shiftLevel } from './agent/effort.js';
|
|
67
89
|
import { 전선붙이기, 전선말 } from './backend/wire.js';
|
|
68
90
|
import { scanLocal, toProfiles } from './backend/scan.js';
|
|
@@ -607,10 +629,8 @@ export async function handle(line, session, ctx) {
|
|
|
607
629
|
say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.tooSmall', { 줄, 칸, 최소줄: 최소높이, 최소칸: 최소폭 }))}`);
|
|
608
630
|
say('');
|
|
609
631
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
say('');
|
|
613
|
-
}
|
|
632
|
+
const 환경말 = 환경이하는말();
|
|
633
|
+
if (환경말.length) { for (const 줄 of 환경말) say(` ${줄}`); say(''); }
|
|
614
634
|
return { handled: true };
|
|
615
635
|
}
|
|
616
636
|
|
|
@@ -652,10 +672,17 @@ export async function handle(line, session, ctx) {
|
|
|
652
672
|
say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.appleTerminal'))}`);
|
|
653
673
|
}
|
|
654
674
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
675
|
+
for (const 줄 of 환경이하는말()) say(` ${c.yellow(mark.warn)} ${줄}`);
|
|
676
|
+
/*
|
|
677
|
+
* 「그 자리에서 바뀝니다」 는 **정말로 바뀔 때만** 적는다.
|
|
678
|
+
*
|
|
679
|
+
* DEEL_NO_MOTION 이 켜져 있으면 무엇을 골라도 한 칸짜리 돌림표가 돈다.
|
|
680
|
+
* 그런데 이 자리는 그걸 안 보고 늘 「바뀝니다」 를 적고 있었다. 바로
|
|
681
|
+
* 위에서 사무실이 작은 터미널에 안 뜬다고 굳이 말해 주는 것과 같은
|
|
682
|
+
* 까닭인데(「켰다고 말해 놓고 안 보이면 그게 고장이다」), 정작 같은
|
|
683
|
+
* 일이 환경변수로 일어날 때는 아무 말도 안 했다.
|
|
684
|
+
*/
|
|
685
|
+
if (!환경으로껐나()) say(` ${c.gray(말('motion.appliesNow'))}`);
|
|
659
686
|
say('');
|
|
660
687
|
return { handled: true };
|
|
661
688
|
}
|
|
@@ -929,9 +956,40 @@ export async function handle(line, session, ctx) {
|
|
|
929
956
|
// 사람도 되돌아간 줄 모른다. **되돌아간 것만** 뺀다 — 못 되돌린 파일을
|
|
930
957
|
// 여기서 빼면 진짜로 바뀌어 있는 파일이 /diff 에서도 사라진다.
|
|
931
958
|
for (const f of r.restored) if (f.ok === true) session.changes.delete(f.path);
|
|
959
|
+
/*
|
|
960
|
+
* ── 되돌리기는 됐는데 **이력을 못 지웠으면** 그것도 말한다 ──────────
|
|
961
|
+
*
|
|
962
|
+
* 파일은 이미 되돌아가 있다. 그런데 그 턴이 이력에 그대로 남으면,
|
|
963
|
+
* 사람이 /undo 를 한 번 더 쳤을 때 **같은 턴**이 또 되돌아간다.
|
|
964
|
+
* 두 턴을 되돌린 줄 알지만 한 턴이다 (safety/undo.js 의 이력줄임).
|
|
965
|
+
*/
|
|
966
|
+
if (r.이력줄임 && r.이력줄임.ok === false) {
|
|
967
|
+
say(` ${mark.warn} ${c.gray('파일은 되돌렸지만 되돌리기 기록을 못 지웠습니다')} ${c.gray(`— ${r.이력줄임.왜}`)}`);
|
|
968
|
+
say(` ${c.gray('그래서 이 턴이 기록에 그대로 남아 있습니다. 여기서')} ${c.cyan('/undo')} ${c.gray('를 또 치면 같은 턴을 또 되돌립니다.')}`);
|
|
969
|
+
}
|
|
970
|
+
/*
|
|
971
|
+
* 깨진 줄은 **되돌릴 수 없는 파일**이다.
|
|
972
|
+
*
|
|
973
|
+
* 이력 한 줄이 곧 한 파일의 원문이라, 줄이 깨졌다는 것은 그 파일을
|
|
974
|
+
* 되돌릴 길이 사라졌다는 뜻이다. 안 말하면 「파일 3개를 되돌렸습니다」
|
|
975
|
+
* 만 남고 넷째가 왜 안 돌아왔는지는 아무 데도 안 적힌다.
|
|
976
|
+
*/
|
|
977
|
+
if (r.깨진줄) {
|
|
978
|
+
say(` ${mark.warn} ${c.gray(`되돌리기 기록에서 ${r.깨진줄}줄을 못 읽었습니다 — 그 줄에 걸린 파일은 되돌릴 길이 없습니다.`)}`);
|
|
979
|
+
}
|
|
980
|
+
|
|
932
981
|
// 상태줄의 '되돌릴 턴' 도 여기서 줄여 준다. 안 줄이면 방금 되돌린 것을
|
|
933
982
|
// 아직 되돌릴 수 있는 것처럼 세고 있게 된다.
|
|
934
|
-
|
|
983
|
+
/*
|
|
984
|
+
* 못 세면 **모른다고 말한다.** 그냥 두면 방금 되돌린 턴이 상태줄에
|
|
985
|
+
* 그대로 남아, 아직 되돌릴 수 있는 것처럼 보인다 (repl.js 에서 같은
|
|
986
|
+
* 자리를 이미 고쳤다 — 안전망 숫자가 조용히 틀리면 안 된다).
|
|
987
|
+
*/
|
|
988
|
+
try { session.되돌릴턴 = ctx.history.turns().length; }
|
|
989
|
+
catch (e) {
|
|
990
|
+
session.되돌릴턴 = null;
|
|
991
|
+
say(` ${mark.warn} ${c.gray(`되돌릴 턴이 몇 개 남았는지 못 셌습니다 — ${e?.message ?? e}`)}`);
|
|
992
|
+
}
|
|
935
993
|
|
|
936
994
|
const 되감음 = session.되감기(r.turnIds ?? []);
|
|
937
995
|
if (되감음.걷은것) {
|
|
@@ -1037,6 +1095,22 @@ export async function handle(line, session, ctx) {
|
|
|
1037
1095
|
// 서버가 잠깐 막아 다시 부른 횟수. 0 이면 안 적는다 — 없는 일을 줄로 남기면 표만 길어진다.
|
|
1038
1096
|
if (session.usage.retries) say(` ${c.gray(pad(말('cost.retries'), 14))} ${말('unit.calls', { n: session.usage.retries })}`);
|
|
1039
1097
|
say(` ${c.gray(pad(말('cost.elapsed'), 14))} ${말('unit.min', { n: mins })}`);
|
|
1098
|
+
/*
|
|
1099
|
+
* ── 갈래를 쓰면 이 위의 숫자는 **한 갈래 것**이다 ────────────────
|
|
1100
|
+
*
|
|
1101
|
+
* session.usage 는 지금 갈래의 셈이다. 갈래를 셋 굴리면 화면의
|
|
1102
|
+
* 금액은 그중 하나 몫이고, 갈래를 바꾸는 것만으로 숫자가 내려간다 —
|
|
1103
|
+
* 돈은 그대로 나가는데. 닫은 갈래 것은 아예 안 보였다.
|
|
1104
|
+
*
|
|
1105
|
+
* 위 줄들을 갈래 합으로 바꾸지는 않는다. 「이 갈래에서 뭘 했나」 도
|
|
1106
|
+
* 봐야 하는 값이라서다. 대신 **다를 때만** 한 줄을 더 붙인다.
|
|
1107
|
+
*/
|
|
1108
|
+
const 갈래셈 = ctx?.갈래?.갈래여럿인가?.() ? ctx.갈래.전체usage() : null;
|
|
1109
|
+
if (갈래셈) {
|
|
1110
|
+
const 갈래수 = (ctx.갈래.개수?.() ?? 1) + (ctx.갈래.닫힌수 ?? 0);
|
|
1111
|
+
say(` ${c.gray(pad('갈래 전부', 14))} ${c.gray(`${갈래수}갈래 · 호출 ${갈래셈.calls.toLocaleString()} · 입력 ${갈래셈.in.toLocaleString()} · 출력 ${갈래셈.out.toLocaleString()}`)}`);
|
|
1112
|
+
say(` ${c.gray(pad('', 14))} ${c.gray('위 숫자는 지금 갈래 것입니다.')}`);
|
|
1113
|
+
}
|
|
1040
1114
|
/*
|
|
1041
1115
|
* 돈.
|
|
1042
1116
|
*
|
|
@@ -1153,6 +1227,29 @@ export async function handle(line, session, ctx) {
|
|
|
1153
1227
|
이름: session.규칙못읽음.이름, 까닭: session.규칙못읽음.까닭,
|
|
1154
1228
|
}))}`);
|
|
1155
1229
|
}
|
|
1230
|
+
/*
|
|
1231
|
+
* 반만 실린 것도 같은 자리에서 말한다.
|
|
1232
|
+
*
|
|
1233
|
+
* 이름만 적으면 사람은 규칙이 통째로 걸려 있다고 믿는다. 2만 자를
|
|
1234
|
+
* 넘긴 규칙 파일의 뒷부분은 모델에게 한 글자도 안 간다 — 「운영 DB 는
|
|
1235
|
+
* 건드리지 마라」 가 하필 그 뒤에 있으면 그게 제일 나쁜 모양이다.
|
|
1236
|
+
*/
|
|
1237
|
+
if (session.규칙잘림) {
|
|
1238
|
+
const j = session.규칙잘림;
|
|
1239
|
+
say(` ${c.gray(pad('', 10))} ${mark.warn} ${c.yellow(`${j.이름} 은 ${j.원본.toLocaleString()}자라 앞 ${j.실린.toLocaleString()}자만 실렸습니다`)}`);
|
|
1240
|
+
say(` ${c.gray(pad('', 10))} ${c.gray('그 뒤에 적은 규칙은 모델에게 안 갑니다. 중요한 것을 앞으로 옮기세요.')}`);
|
|
1241
|
+
}
|
|
1242
|
+
/*
|
|
1243
|
+
* 도구 정의를 못 쟀으면 그것도 적는다.
|
|
1244
|
+
*
|
|
1245
|
+
* 도구 정의는 매 요청에 통째로 실려 나간다. 못 재면 /context 는 그만큼
|
|
1246
|
+
* 빈자리가 있다고 말하고, 그 값으로 출력 상한이 잡혀 답이 이유 없이
|
|
1247
|
+
* 짧아진다. MCP 서버가 이상한 스키마를 주면 실제로 나는 일이다.
|
|
1248
|
+
*/
|
|
1249
|
+
if (session.도구못쟀나) {
|
|
1250
|
+
say(` ${c.gray(pad('', 10))} ${mark.warn} ${c.yellow(`도구 정의 크기를 못 쟀습니다 — ${String(session.도구못쟀나).slice(0, 60)}`)}`);
|
|
1251
|
+
say(` ${c.gray(pad('', 10))} ${c.gray('/context 의 도구 칸이 0 으로 나오고, 답이 이유 없이 짧아질 수 있습니다.')}`);
|
|
1252
|
+
}
|
|
1156
1253
|
const caps = [
|
|
1157
1254
|
k.tools ? c.green(말('status.capTools')) : c.red(말('status.capTools')),
|
|
1158
1255
|
k.streaming ? c.green(말('status.capStream')) : c.gray(말('status.capStream')),
|
|
@@ -1575,11 +1672,18 @@ export async function handle(line, session, ctx) {
|
|
|
1575
1672
|
return { handled: true };
|
|
1576
1673
|
}
|
|
1577
1674
|
for (const [i, r] of rows.entries()) {
|
|
1578
|
-
|
|
1579
|
-
|
|
1675
|
+
// 못 읽은 것도 올린다 (agent/store.js 의 list). 없는 것과 못 여는 것은 다르다.
|
|
1676
|
+
const 앞 = r.못읽음 ? c.yellow(mark.warn) : i === 0 ? c.hgreen('●') : c.gray('·');
|
|
1677
|
+
say(` ${앞} ${c.bold(pad(r.id, 17))}${c.gray(pad(r.못읽음 ? '' : `${r.turns}턴`, 6, 'right'))} ${c.gray(clip(r.model, 22))}`);
|
|
1678
|
+
say(` ${r.못읽음 ? c.yellow(clip(r.first, 66)) : c.gray(clip(r.first, 66))}`);
|
|
1580
1679
|
}
|
|
1581
1680
|
say('');
|
|
1582
|
-
|
|
1681
|
+
const 읽히는것 = rows.filter((r) => !r.못읽음);
|
|
1682
|
+
if (읽히는것.length) {
|
|
1683
|
+
say(` ${c.gray('이어하려면 나갔다가')} ${c.cyan('deel --continue')} ${c.gray('또는')} ${c.cyan(`deel --resume ${읽히는것[0].id}`)}`);
|
|
1684
|
+
} else {
|
|
1685
|
+
say(` ${mark.warn} ${c.yellow('여기 있는 대화를 하나도 못 열었습니다 — 이어할 수 없습니다.')}`);
|
|
1686
|
+
}
|
|
1583
1687
|
/*
|
|
1584
1688
|
* 바로 아래 줄이 약속이다. 못 지키고 있으면 약속을 되풀이하면 안 된다.
|
|
1585
1689
|
*
|
|
@@ -2487,6 +2591,18 @@ async function 커밋명령(session, ctx, arg = '') {
|
|
|
2487
2591
|
say('');
|
|
2488
2592
|
say(` ${c.yellow('모델이 메시지를 못 만들어 바뀐 것만 적었습니다.')}`);
|
|
2489
2593
|
}
|
|
2594
|
+
/*
|
|
2595
|
+
* diff 를 못 읽고 지은 메시지라면 그렇다고 적는다.
|
|
2596
|
+
*
|
|
2597
|
+
* 담긴 것이 64MB 를 넘으면 git 이 몸통을 못 뱉는다. 파일 목록은 멀쩡해서
|
|
2598
|
+
* 커밋은 그대로 이어지는데, 그때 메시지는 **내용을 한 줄도 안 보고** 지은
|
|
2599
|
+
* 것이다. 그 사실이 화면에 없으면 사람은 여느 메시지와 똑같이 믿는다.
|
|
2600
|
+
*/
|
|
2601
|
+
if (r.diff못읽음) {
|
|
2602
|
+
say('');
|
|
2603
|
+
say(` ${c.yellow('내용(diff)을 못 읽고 지은 메시지입니다')} ${c.gray(`— ${clip(r.diff못읽음, 60)}`)}`);
|
|
2604
|
+
say(` ${c.gray('파일 이름과 줄 수만 보고 썼습니다. 찍기 전에 한 번 읽어 주세요.')}`);
|
|
2605
|
+
}
|
|
2490
2606
|
|
|
2491
2607
|
const 상태줄 = r.상태 ? r.상태.split('\n') : [];
|
|
2492
2608
|
if (상태줄.length) {
|
|
@@ -2812,6 +2928,23 @@ function showContext(session) {
|
|
|
2812
2928
|
} else {
|
|
2813
2929
|
say(` ${c.gray(말('ctx.estimate'))}`);
|
|
2814
2930
|
}
|
|
2931
|
+
/*
|
|
2932
|
+
* 재 봤는데 **못 믿어서 안 쓴** 것도 말한다.
|
|
2933
|
+
*
|
|
2934
|
+
* session.배운다() 는 서버가 알려 준 값이 우리 추정의 절반 아래거나 두 배
|
|
2935
|
+
* 위면 표본을 버린다. 튄 값 하나에 게이지가 휘둘리면 안 되니 버리는 것은
|
|
2936
|
+
* 맞다. 그런데 버린 것을 아무 데도 안 남기면, 「아직 한 번도 못 재 봤다」 와
|
|
2937
|
+
* 「재 봤는데 세 배가 나와서 못 믿었다」 가 화면에서 똑같아진다.
|
|
2938
|
+
*
|
|
2939
|
+
* 뒤엣것은 우리 추정이 크게 틀렸다는 단서다. 그 말을 안 하면 게이지는 영영
|
|
2940
|
+
* 짐작인 채로 도는데 사람은 그저 「추정입니다」 만 읽는다.
|
|
2941
|
+
*/
|
|
2942
|
+
if (session?.보정버림 > 0) {
|
|
2943
|
+
const 배 = session.보정마지막버린비율;
|
|
2944
|
+
say(` ${c.gray(`서버가 알려 준 값이 우리 추정과 너무 달라 ${session.보정버림}번은 안 썼습니다`
|
|
2945
|
+
+ (배 ? ` (마지막에 잰 것은 추정의 ${배.toFixed(1)}배)` : ''))}`);
|
|
2946
|
+
say(` ${c.gray('이 창구에서는 위 숫자가 실제와 크게 다를 수 있습니다.')}`);
|
|
2947
|
+
}
|
|
2815
2948
|
say('');
|
|
2816
2949
|
}
|
|
2817
2950
|
|