deel-local-cli 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +686 -28
- package/README.md +571 -31
- package/bin/deel.js +234 -160
- package/package.json +3 -2
- package/src/agent/compact.js +131 -138
- package/src/agent/effort.js +44 -11
- package/src/agent/loop.js +574 -251
- package/src/agent/memory.js +152 -0
- package/src/agent/mention.js +164 -0
- package/src/agent/modes.js +127 -20
- package/src/agent/recall.js +209 -0
- package/src/agent/route.js +156 -0
- package/src/agent/salvage.js +182 -0
- package/src/agent/session.js +155 -11
- package/src/agent/store.js +36 -11
- package/src/backend/adapter.js +266 -183
- package/src/backend/ctxsize.js +246 -0
- package/src/backend/detect.js +10 -1
- package/src/backend/http.js +23 -0
- package/src/backend/learn.js +102 -0
- package/src/backend/mcp.js +304 -0
- package/src/backend/probe.js +15 -14
- package/src/backend/scan.js +97 -3
- package/src/commands.js +825 -50
- package/src/oneshot.js +327 -0
- package/src/repl.js +715 -414
- package/src/report.js +20 -6
- package/src/safety/guard.js +123 -7
- package/src/safety/undo.js +96 -11
- package/src/tools/encoding.js +24 -2
- package/src/tools/fsutil.js +70 -0
- package/src/tools/index.js +471 -27
- package/src/tools/webfetch.js +37 -1
- package/src/ui/ansi.js +32 -2
- package/src/ui/diff.js +255 -0
- package/src/ui/level.js +6 -2
- package/src/ui/prompt.js +13 -1
- package/src/ui/screen.js +169 -0
- package/src/ui/status.js +98 -24
- package/src/ui/tui.js +419 -0
- package/src/version.js +28 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 기억 — 대화가 끝나도 남는 것.
|
|
3
|
+
*
|
|
4
|
+
* 왜 필요한가:
|
|
5
|
+
* 대화는 켤 때마다 처음부터다. 그래서 매번 같은 것을 설명하게 된다 —
|
|
6
|
+
* "우리 문서는 CP949 다", "검증 포트로 7080 은 쓰지 마라", "빌드는 npm run b".
|
|
7
|
+
* /compact 로 접히면 그마저 사라진다. 사람은 두 번째부터 짜증이 나고,
|
|
8
|
+
* 세 번째부터는 그냥 안 쓴다.
|
|
9
|
+
*
|
|
10
|
+
* 지난 대화 찾기(recall.js)와는 다른 물건이다. 그쪽은 **찾아야** 나오고,
|
|
11
|
+
* 이쪽은 **처음부터 들어가 있다.** 매번 찾으라고 할 수 없는 것 —
|
|
12
|
+
* 지켜야 하는 규칙, 되풀이하면 안 되는 실수 — 이 여기 온다.
|
|
13
|
+
*
|
|
14
|
+
* 어디에 두나:
|
|
15
|
+
* .deel/memory.md — 사람이 열어 고칠 수 있는 글이다. 데이터베이스가 아니다.
|
|
16
|
+
* 이게 중요하다. 모델이 잘못 적은 것을 사람이 지울 수 있어야 한다.
|
|
17
|
+
* 틀린 기억은 없느니만 못하다 — 매 요청에 실려 계속 틀리게 만든다.
|
|
18
|
+
*
|
|
19
|
+
* 왜 상한을 두나:
|
|
20
|
+
* 기억은 **매 요청마다** 통째로 나간다. 백 줄이면 백 줄이 매번 나간다.
|
|
21
|
+
* 그래서 자리를 정해 두고, 넘으면 넘었다고 말한다.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
|
|
26
|
+
// 기억 전체가 차지할 수 있는 최대 글자. 대략 2,000 토큰쯤이다.
|
|
27
|
+
export const 기억최대 = 6000;
|
|
28
|
+
// 한 줄이 이보다 길면 자른다. 모델이 파일을 통째로 기억에 넣으려 드는 일이 있다.
|
|
29
|
+
export const 한줄최대 = 400;
|
|
30
|
+
// 줄 수 상한. 자리가 남아도 이보다 많으면 사람이 못 읽는다.
|
|
31
|
+
export const 줄최대 = 60;
|
|
32
|
+
|
|
33
|
+
export const 자리 = (root) => join(root, '.deel', 'memory.md');
|
|
34
|
+
|
|
35
|
+
const 머리말 = `# 기억
|
|
36
|
+
|
|
37
|
+
이 파일은 deel 이 대화 사이에 들고 다니는 메모입니다.
|
|
38
|
+
매 요청마다 모델에게 통째로 실려 나갑니다 — 그러니 짧게, 오래 갈 것만 적습니다.
|
|
39
|
+
|
|
40
|
+
직접 고치셔도 됩니다. 틀린 줄은 지우세요. 지우면 그걸로 끝입니다.
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
/** 파일에서 기억 줄을 읽어 온다. 머리말과 빈 줄은 뺀다. */
|
|
44
|
+
export function 읽기(root) {
|
|
45
|
+
const p = 자리(root);
|
|
46
|
+
if (!existsSync(p)) return { 줄들: [], 자리: p, 있음: false };
|
|
47
|
+
let 글;
|
|
48
|
+
try { 글 = readFileSync(p, 'utf8'); } catch { return { 줄들: [], 자리: p, 있음: false }; }
|
|
49
|
+
const 줄들 = [];
|
|
50
|
+
for (const raw of 글.split(/\r?\n/)) {
|
|
51
|
+
const l = raw.trim();
|
|
52
|
+
if (!l) continue;
|
|
53
|
+
if (l.startsWith('#')) continue; // 머리말
|
|
54
|
+
if (l.startsWith('이 파일은') || l.startsWith('매 요청마다')) continue;
|
|
55
|
+
if (l.startsWith('직접 고치셔도') || l.startsWith('직접 고치')) continue;
|
|
56
|
+
줄들.push(l.replace(/^[-*]\s*/, '')); // 목록 표시는 떼고 담는다
|
|
57
|
+
}
|
|
58
|
+
return { 줄들, 자리: p, 있음: true };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 줄들을 파일로 되돌려 쓴다. */
|
|
62
|
+
export function 쓰기(root, 줄들) {
|
|
63
|
+
const p = 자리(root);
|
|
64
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
65
|
+
const 몸 = 줄들.map((l) => `- ${l}`).join('\n');
|
|
66
|
+
writeFileSync(p, `${머리말}\n${몸}\n`, 'utf8');
|
|
67
|
+
return p;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 한 줄을 더한다.
|
|
72
|
+
*
|
|
73
|
+
* 이미 있는 것과 거의 같으면 안 더한다. 안 그러면 같은 말이 조금씩 다른 꼴로
|
|
74
|
+
* 스무 줄 쌓인다 — 모델은 자기가 방금 적은 것을 기억 못 하기 때문이다.
|
|
75
|
+
*
|
|
76
|
+
* @returns {{ok: boolean, why?: string, 줄: string, 줄수: number, 넘침?: boolean}}
|
|
77
|
+
*/
|
|
78
|
+
export function 더하기(root, 글) {
|
|
79
|
+
const 줄 = 다듬기(글);
|
|
80
|
+
if (!줄) return { ok: false, why: '적을 내용이 비었습니다', 줄: '', 줄수: 0 };
|
|
81
|
+
|
|
82
|
+
const { 줄들 } = 읽기(root);
|
|
83
|
+
if (줄들.some((x) => 같은말(x, 줄))) {
|
|
84
|
+
return { ok: false, why: '이미 기억하고 있습니다', 줄, 줄수: 줄들.length };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const 새것 = [...줄들, 줄];
|
|
88
|
+
// 자리가 넘치면 **오래된 것부터** 뺀다. 최근에 정한 것이 대개 맞다.
|
|
89
|
+
let 넘침 = false;
|
|
90
|
+
while (새것.length > 줄최대 || 새것.join('\n').length > 기억최대) {
|
|
91
|
+
if (새것.length <= 1) break;
|
|
92
|
+
새것.shift();
|
|
93
|
+
넘침 = true;
|
|
94
|
+
}
|
|
95
|
+
쓰기(root, 새것);
|
|
96
|
+
return { ok: true, 줄, 줄수: 새것.length, 넘침 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 번호로 지운다(1부터). 사람이 /memory 화면을 보고 고르는 자리다. */
|
|
100
|
+
export function 지우기(root, 번호) {
|
|
101
|
+
const { 줄들 } = 읽기(root);
|
|
102
|
+
const i = Number(번호) - 1;
|
|
103
|
+
if (!Number.isInteger(i) || i < 0 || i >= 줄들.length) return { ok: false, why: '그런 번호가 없습니다' };
|
|
104
|
+
const 뺀것 = 줄들[i];
|
|
105
|
+
const 남은것 = 줄들.filter((_, n) => n !== i);
|
|
106
|
+
쓰기(root, 남은것);
|
|
107
|
+
return { ok: true, 뺀것, 줄수: 남은것.length };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 통째로 비운다. */
|
|
111
|
+
export function 비우기(root) {
|
|
112
|
+
쓰기(root, []);
|
|
113
|
+
return { ok: true };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 한 줄로 다듬는다 — 줄바꿈을 없애고 길이를 자른다. */
|
|
117
|
+
function 다듬기(글) {
|
|
118
|
+
let s = String(글 ?? '').replace(/\s+/g, ' ').trim();
|
|
119
|
+
s = s.replace(/^[-*]\s*/, '');
|
|
120
|
+
if (s.length > 한줄최대) s = s.slice(0, 한줄최대) + '…';
|
|
121
|
+
return s;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 두 줄이 사실상 같은 말인가.
|
|
126
|
+
*
|
|
127
|
+
* 글자 그대로 비교하면 "CP949 를 쓴다" 와 "CP949를 쓴다" 가 다른 줄이 된다.
|
|
128
|
+
* 공백·조사·문장부호를 털고 견준다. 완벽할 필요는 없다 —
|
|
129
|
+
* 여기서 하려는 것은 '똑같은 말이 쌓이는 것' 을 막는 것뿐이다.
|
|
130
|
+
*/
|
|
131
|
+
function 같은말(a, b) {
|
|
132
|
+
const 털기 = (s) => String(s).toLowerCase().replace(/[\s.,!?~·'"()[\]]/g, '');
|
|
133
|
+
const x = 털기(a);
|
|
134
|
+
const y = 털기(b);
|
|
135
|
+
if (x === y) return true;
|
|
136
|
+
// 한쪽이 다른 쪽을 통째로 품고 있고 길이 차가 크지 않으면 같은 말로 본다.
|
|
137
|
+
const [짧, 긴] = x.length <= y.length ? [x, y] : [y, x];
|
|
138
|
+
return 짧.length >= 8 && 긴.includes(짧) && 긴.length - 짧.length <= 짧.length * 0.5;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 시스템 프롬프트에 넣을 토막.
|
|
143
|
+
*
|
|
144
|
+
* 없으면 빈 글을 돌려준다 — "기억: (없음)" 같은 줄을 넣으면 그 자체가
|
|
145
|
+
* 매 요청마다 나가는 쓰레기가 된다.
|
|
146
|
+
*/
|
|
147
|
+
export function 프롬프트토막(root) {
|
|
148
|
+
const { 줄들 } = 읽기(root);
|
|
149
|
+
if (!줄들.length) return '';
|
|
150
|
+
return '\n--- 기억 (지난 대화에서 정한 것. 사용자가 다시 말하지 않아도 지킨다) ---\n'
|
|
151
|
+
+ 줄들.map((l) => `- ${l}`).join('\n');
|
|
152
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// @파일 지목 — 말 속에 @경로 를 쓰면 그 파일을 바로 붙여 보낸다.
|
|
2
|
+
//
|
|
3
|
+
// 왜 필요한가:
|
|
4
|
+
// 지금은 파일 하나 보여주려면 모델이 Read 도구를 스스로 불러야 한다. 왕복이
|
|
5
|
+
// 한 번 더 든다. 로컬 모델은 도구 호출이 약해서 그 한 번이 자주 헛돈다 —
|
|
6
|
+
// 엉뚱한 경로를 부르거나, 아예 안 부르고 지어낸다. 사람은 이미 어느 파일인지
|
|
7
|
+
// 알고 있는데 모델더러 찾아보라고 시키는 셈이다.
|
|
8
|
+
//
|
|
9
|
+
// 여기서 어려운 것은 붙이는 일이 아니라 '아닌 것을 파일로 오해하지 않기' 다.
|
|
10
|
+
// @ 로 시작하는 것은 세상에 널렸다 — 이메일 주소, CSS 의 @media, 파이썬 장식자,
|
|
11
|
+
// npm 의 @scope/pkg, 사람 아이디. 잘못 붙이면 엉뚱한 글이 대화에 섞여 들어가고,
|
|
12
|
+
// 사람은 자기가 안 보낸 것이 왜 거기 있는지 모른다.
|
|
13
|
+
//
|
|
14
|
+
// 그래서 규칙을 하나로 뒀다: **실제로 있는 경로일 때만 붙인다.**
|
|
15
|
+
// 없으면 아무 말 없이 글자 그대로 둔다. 지목이 아니었을 테니 조용한 편이 맞다.
|
|
16
|
+
import { existsSync, statSync, readdirSync } from 'node:fs';
|
|
17
|
+
import { readTextFull } from '../tools/fsutil.js';
|
|
18
|
+
import { estimateTokens } from './session.js';
|
|
19
|
+
|
|
20
|
+
// 붙일 수 있는 최대 토큰. 부르는 쪽에서 컨텍스트에 맞춰 넘겨준다.
|
|
21
|
+
const 기본예산 = 6000;
|
|
22
|
+
const 폴더최대 = 60; // 폴더를 지목했을 때 늘어놓을 항목 수
|
|
23
|
+
const 뒤에붙는것 = /[,.?!;:)\]}"'」』]+$/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 글에서 @지목을 찾는다. 붙일지 말지는 여기서 안 정한다 — 자리만 찾는다.
|
|
27
|
+
*
|
|
28
|
+
* @returns {Array<{path:string, start:number, end:number, raw:string}>}
|
|
29
|
+
*/
|
|
30
|
+
export function findMentions(text) {
|
|
31
|
+
const s = String(text ?? '');
|
|
32
|
+
const out = [];
|
|
33
|
+
for (let i = 0; i < s.length; i++) {
|
|
34
|
+
if (s[i] !== '@') continue;
|
|
35
|
+
// 앞이 글자면 지목이 아니다 — hong@example.com 의 @ 가 여기서 걸러진다.
|
|
36
|
+
const 앞 = i > 0 ? s[i - 1] : ' ';
|
|
37
|
+
if (!/[\s(\[{'"]/.test(앞)) continue;
|
|
38
|
+
|
|
39
|
+
// @"띄어쓰기 든 이름.txt" — 따옴표로 묶으면 통째로 하나다.
|
|
40
|
+
if (s[i + 1] === '"' || s[i + 1] === "'") {
|
|
41
|
+
const 따옴 = s[i + 1];
|
|
42
|
+
const 끝 = s.indexOf(따옴, i + 2);
|
|
43
|
+
if (끝 > i + 2) {
|
|
44
|
+
out.push({ path: s.slice(i + 2, 끝), start: i, end: 끝 + 1, raw: s.slice(i, 끝 + 1) });
|
|
45
|
+
i = 끝;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let j = i + 1;
|
|
51
|
+
while (j < s.length && !/[\s"'`]/.test(s[j])) j++;
|
|
52
|
+
if (j === i + 1) continue; // @ 뒤가 비었다
|
|
53
|
+
const raw = s.slice(i, j);
|
|
54
|
+
out.push({ path: s.slice(i + 1, j), start: i, end: j, raw });
|
|
55
|
+
i = j - 1;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 이 지목이 가리키는 실제 자리. 못 찾거나 범위 밖이면 왜인지 같이 준다. */
|
|
61
|
+
function 찾아보기(m, scope) {
|
|
62
|
+
const 후보 = [m.path];
|
|
63
|
+
// 뒤에 붙은 문장부호를 떼고도 한 번 본다 — "@src/a.js, 이것" 처럼 쓴다.
|
|
64
|
+
const 뗀것 = m.path.replace(뒤에붙는것, '');
|
|
65
|
+
if (뗀것 && 뗀것 !== m.path) 후보.push(뗀것);
|
|
66
|
+
|
|
67
|
+
for (const p of 후보) {
|
|
68
|
+
if (!p) continue;
|
|
69
|
+
let abs;
|
|
70
|
+
try { abs = scope.resolve(p); }
|
|
71
|
+
catch (err) { return { why: 'blocked', note: err.message, path: p }; }
|
|
72
|
+
if (existsSync(abs)) return { abs, path: p };
|
|
73
|
+
}
|
|
74
|
+
return { why: 'missing', path: m.path };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function 폴더내용(abs, show) {
|
|
78
|
+
let 목록 = [];
|
|
79
|
+
try { 목록 = readdirSync(abs, { withFileTypes: true }); }
|
|
80
|
+
catch (err) { return `(폴더를 못 읽었습니다: ${err.message})`; }
|
|
81
|
+
const 줄들 = 목록.slice(0, 폴더최대).map((e) => (e.isDirectory() ? `${e.name}/` : e.name));
|
|
82
|
+
const 더 = 목록.length > 폴더최대 ? `\n… 전체 ${목록.length}개 중 ${폴더최대}개까지` : '';
|
|
83
|
+
return `${show} 는 폴더입니다. 안에 든 것:\n${줄들.join('\n')}${더}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @지목을 실제 내용으로 바꿔 붙인다.
|
|
88
|
+
*
|
|
89
|
+
* 글 자체는 안 건드린다 — 사람이 쓴 말은 그대로 두고, 뒤에 파일을 덧붙인다.
|
|
90
|
+
* 지목한 자리를 내용으로 갈아치우면 "@src/a.js 이거 왜 느려?" 가 코드 덩어리
|
|
91
|
+
* 한복판에서 끝나는 문장이 되어 버려서, 모델이 무엇을 물었는지 놓친다.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} text
|
|
94
|
+
* @param {{scope?:object, budget?:number, seen?:Set<string>, onRead?:Function}} opts
|
|
95
|
+
* seen 을 주면 통째로 붙은 파일을 '읽은 것' 으로 적어 둔다 — 그래야 Edit 이
|
|
96
|
+
* 'Read 부터 하라' 며 되돌려 보내지 않는다. 잘린 파일은 안 적는다.
|
|
97
|
+
* 안 본 데를 본 것으로 치면 그 자리를 그냥 고쳐 버린다.
|
|
98
|
+
*/
|
|
99
|
+
export function expand(text, { scope = null, budget = 기본예산, seen = null, onRead = null } = {}) {
|
|
100
|
+
const 원문 = String(text ?? '');
|
|
101
|
+
const 빈답 = { text: 원문, attached: [], missing: [], blocked: [] };
|
|
102
|
+
if (!원문.includes('@') || !scope?.resolve) return 빈답;
|
|
103
|
+
|
|
104
|
+
const 지목들 = findMentions(원문);
|
|
105
|
+
if (!지목들.length) return 빈답;
|
|
106
|
+
|
|
107
|
+
const attached = [];
|
|
108
|
+
const missing = [];
|
|
109
|
+
const blocked = [];
|
|
110
|
+
const 붙일것 = [];
|
|
111
|
+
const 본것 = new Set();
|
|
112
|
+
let 남은예산 = Math.max(200, Number(budget) || 기본예산);
|
|
113
|
+
|
|
114
|
+
for (const m of 지목들) {
|
|
115
|
+
const 자리 = 찾아보기(m, scope);
|
|
116
|
+
if (자리.why === 'blocked') { blocked.push({ path: 자리.path, why: 자리.note }); continue; }
|
|
117
|
+
if (자리.why === 'missing') { missing.push(자리.path); continue; }
|
|
118
|
+
if (본것.has(자리.abs)) continue; // 같은 파일을 두 번 적지 않는다
|
|
119
|
+
본것.add(자리.abs);
|
|
120
|
+
|
|
121
|
+
const show = scope.show ? scope.show(자리.abs) : 자리.path;
|
|
122
|
+
let 몸통;
|
|
123
|
+
let 통째로 = true;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
if (statSync(자리.abs).isDirectory()) {
|
|
127
|
+
몸통 = 폴더내용(자리.abs, show);
|
|
128
|
+
} else {
|
|
129
|
+
// 사내 파일은 CP949 가 흔하다. 그냥 읽으면 통째로 깨진다.
|
|
130
|
+
const 읽음 = readTextFull(자리.abs);
|
|
131
|
+
const 줄들 = 읽음.text.split('\n');
|
|
132
|
+
const 붙은글 = 줄들.map((l, i) => `${String(i + 1).padStart(6)}\t${l}`).join('\n');
|
|
133
|
+
if (estimateTokens(붙은글) <= 남은예산) {
|
|
134
|
+
몸통 = 붙은글;
|
|
135
|
+
} else {
|
|
136
|
+
// 예산만큼만 앞에서 자른다. 글자 수로 어림잡되 남는 쪽으로 잡는다.
|
|
137
|
+
통째로 = false;
|
|
138
|
+
const 어림 = Math.max(1, Math.floor(남은예산 * 3));
|
|
139
|
+
const 자른것 = 붙은글.slice(0, 어림);
|
|
140
|
+
const 마지막줄 = 자른것.split('\n').length;
|
|
141
|
+
몸통 = `${자른것}\n… 전체 ${줄들.length}줄 중 ${마지막줄}줄까지만 붙였습니다 (나머지는 Read 로 읽으세요)`;
|
|
142
|
+
}
|
|
143
|
+
if (onRead) onRead(자리.abs, 읽음.text);
|
|
144
|
+
if (통째로 && seen) seen.add(자리.abs);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// 못 읽는 파일(바이너리 등)은 붙이지 않는다. 그렇다고 말은 해 준다.
|
|
148
|
+
몸통 = `(못 읽었습니다: ${err.message})`;
|
|
149
|
+
통째로 = false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const 토막 = `\n\n--- ${show} ---\n${몸통}\n--- ${show} 끝 ---`;
|
|
153
|
+
남은예산 -= estimateTokens(토막);
|
|
154
|
+
붙일것.push(토막);
|
|
155
|
+
attached.push({ path: 자리.abs, show, full: 통째로 });
|
|
156
|
+
if (남은예산 <= 0) break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!붙일것.length) return { text: 원문, attached, missing, blocked };
|
|
160
|
+
const 머리 = attached.length === 1
|
|
161
|
+
? '아래는 사용자가 @ 로 지목한 파일입니다. 이미 읽은 것으로 치고 답하세요.'
|
|
162
|
+
: `아래는 사용자가 @ 로 지목한 파일 ${attached.length}개입니다. 이미 읽은 것으로 치고 답하세요.`;
|
|
163
|
+
return { text: `${원문}\n\n${머리}${붙일것.join('')}`, attached, missing, blocked };
|
|
164
|
+
}
|
package/src/agent/modes.js
CHANGED
|
@@ -11,13 +11,51 @@
|
|
|
11
11
|
// 오프라인일 때 웹 도구를 숨기는 것과 같은 방식이다.
|
|
12
12
|
|
|
13
13
|
// 읽기만 하는 도구. 무엇을 바꾸지 않는다.
|
|
14
|
-
|
|
14
|
+
// Recall 은 지난 대화를 찾는다 — 파일은 안 건드리므로 읽기 쪽이다.
|
|
15
|
+
// 묻기 모드에도 준다: "저번에 이거 어떻게 했더라" 가 딱 묻기 모드의 일이다.
|
|
16
|
+
const 읽기 = ['Read', 'Glob', 'Grep', 'WebFetch', 'Skill', 'Recall'];
|
|
15
17
|
// 계획을 적는 도구. 파일을 안 건드리므로 읽기 전용 모드에서도 준다.
|
|
16
|
-
|
|
18
|
+
//
|
|
19
|
+
// Remember 도 여기 있다. 기억은 사용자의 소스를 안 건드리고 .deel/memory.md
|
|
20
|
+
// 한 줄을 더할 뿐이다. 오히려 계획·설계 모드에서 정한 것이야말로 다음에
|
|
21
|
+
// 이어질 때 필요하다 — 거기서 못 적으면 정한 것이 그대로 날아간다.
|
|
22
|
+
const 계획 = ['TodoWrite', 'Remember'];
|
|
17
23
|
// 바꾸는 도구.
|
|
18
|
-
|
|
24
|
+
// Append 는 Write 와 짝이다 — 출력 상한이 작은 모델이 큰 파일을 나눠 쓰는 길이다.
|
|
25
|
+
// Write 를 주는 자리에는 반드시 같이 준다. 하나만 주면 나눠 쓸 방법이 없어진다.
|
|
26
|
+
const 쓰기 = ['Write', 'Append', 'Edit', 'Bash'];
|
|
19
27
|
|
|
20
28
|
export const MODES = {
|
|
29
|
+
// 처음에는 여기서 시작한다.
|
|
30
|
+
//
|
|
31
|
+
// 무슨 일을 시킬지 미리 정해 두고 켜는 사람은 드물다. 그래서 기본은 '종합' 이고,
|
|
32
|
+
// 한마디를 할 때마다 그 말이 무슨 일인지 보고 알맞은 모드로 저절로 옮겨 간다
|
|
33
|
+
// (agent/route.js). 애매하면 안 옮기고 여기 그대로 둔다.
|
|
34
|
+
//
|
|
35
|
+
// 사용자가 /plan 처럼 직접 고르면 저절로 옮기는 것은 멈춘다. 사람이 고른 것을
|
|
36
|
+
// 뒤집지 않는다. 다시 맡기려면 /work 종합 이다.
|
|
37
|
+
auto: {
|
|
38
|
+
id: 'auto',
|
|
39
|
+
name: '종합',
|
|
40
|
+
en: 'Auto',
|
|
41
|
+
glyph: '◎',
|
|
42
|
+
hint: '요청에 따라 알맞은 모드로',
|
|
43
|
+
tools: [...읽기, ...계획, ...쓰기],
|
|
44
|
+
effort: 'save',
|
|
45
|
+
think: null,
|
|
46
|
+
steps: 24,
|
|
47
|
+
say: [
|
|
48
|
+
'지금은 **종합** 모드다. 무슨 일이 올지 정해져 있지 않다.',
|
|
49
|
+
'',
|
|
50
|
+
'- 시키는 일이 무엇인지 먼저 가늠하고, 그에 맞는 방식으로 해라.',
|
|
51
|
+
' 고치는 일이면 읽고 나서 고치고, 원인을 찾는 일이면 확인부터 하고,',
|
|
52
|
+
' 설명하는 일이면 근거를 파일에서 대라.',
|
|
53
|
+
'- 큰 일이면 TodoWrite 로 쪼개 적고 하나씩 해라.',
|
|
54
|
+
'- 확인할 수 있는 것은 확인해라. 확인 못 한 것을 됐다고 하지 마라.',
|
|
55
|
+
'- 시키지 않은 일을 벌이지 마라.',
|
|
56
|
+
].join('\n'),
|
|
57
|
+
},
|
|
58
|
+
|
|
21
59
|
code: {
|
|
22
60
|
id: 'code',
|
|
23
61
|
name: '코드',
|
|
@@ -28,7 +66,17 @@ export const MODES = {
|
|
|
28
66
|
effort: 'save', // 첫 판단만 세게, 이어가기는 얕게
|
|
29
67
|
think: null, // 사용자가 정한 값을 그대로 쓴다
|
|
30
68
|
steps: 24,
|
|
31
|
-
say:
|
|
69
|
+
say: [
|
|
70
|
+
"지금 하는 일은 **구현**이다. 아래 순서를 지켜라.",
|
|
71
|
+
"",
|
|
72
|
+
"1. 고칠 파일을 먼저 Read 로 읽는다. 안 읽고 고치려 하면 도구가 거절한다.",
|
|
73
|
+
"2. 주변 코드의 관례를 따른다 — 이름 짓는 법, 오류 다루는 법, 주석 밀도까지.",
|
|
74
|
+
" 새 관례를 들여오지 마라. 이 코드가 이미 하고 있는 대로 한다.",
|
|
75
|
+
"3. 한 번에 하나씩 고친다. 여러 파일에 흩뿌리지 마라.",
|
|
76
|
+
"4. 고친 뒤에는 확인할 방법이 있으면 실제로 돌려 본다 (검사·빌드·실행).",
|
|
77
|
+
" 확인 못 했으면 \"확인 못 했다\" 고 말한다. 됐다고 하지 마라.",
|
|
78
|
+
"5. 끝나면 무엇을 왜 바꿨는지 한두 줄로 말한다. 코드를 통째로 붙여넣지 마라.",
|
|
79
|
+
].join('\n'),
|
|
32
80
|
},
|
|
33
81
|
|
|
34
82
|
architect: {
|
|
@@ -41,9 +89,23 @@ export const MODES = {
|
|
|
41
89
|
effort: 'deep',
|
|
42
90
|
think: 'high',
|
|
43
91
|
steps: 20,
|
|
44
|
-
say:
|
|
45
|
-
|
|
46
|
-
|
|
92
|
+
say: [
|
|
93
|
+
"지금 하는 일은 **설계**다. 파일을 바꾸는 도구는 주어지지 않았다.",
|
|
94
|
+
"",
|
|
95
|
+
"먼저 읽어라. 지금 구조를 모르면 설계가 아니라 상상이다.",
|
|
96
|
+
" - 관련된 파일을 Glob/Grep 으로 찾고 Read 로 실제로 읽는다",
|
|
97
|
+
" - 무엇이 무엇에 기대고 있는지(의존 방향)를 파악한다",
|
|
98
|
+
"",
|
|
99
|
+
"그 다음 이 차례로 답한다.",
|
|
100
|
+
" 1. 지금 구조 — 파일과 역할, 경계가 어디인지 (경로:줄 로 짚어라)",
|
|
101
|
+
" 2. 무엇이 문제인가 — 왜 지금 모양으로는 안 되는지",
|
|
102
|
+
" 3. 선택지 2~3개 — 각각 무엇을 얻고 무엇을 잃는지, 손이 얼마나 가는지",
|
|
103
|
+
" 4. 추천 하나와 그 이유, 버린 것을 왜 버렸는지",
|
|
104
|
+
" 5. 영향받는 파일 목록",
|
|
105
|
+
"",
|
|
106
|
+
"이 코드가 이미 쓰는 관례 안에서 답을 찾아라. 새 틀을 들여오는 것은 마지막",
|
|
107
|
+
"수단이고, 그때는 왜 기존 관례로 안 되는지를 먼저 밝혀라.",
|
|
108
|
+
].join('\n'),
|
|
47
109
|
},
|
|
48
110
|
|
|
49
111
|
ask: {
|
|
@@ -56,8 +118,14 @@ export const MODES = {
|
|
|
56
118
|
effort: 'even',
|
|
57
119
|
think: 'low',
|
|
58
120
|
steps: 8,
|
|
59
|
-
say:
|
|
60
|
-
|
|
121
|
+
say: [
|
|
122
|
+
"지금 하는 일은 **설명**이다. 아무것도 바꾸지 않는다.",
|
|
123
|
+
"",
|
|
124
|
+
"- 근거를 파일에서 대라. 경로와 줄 번호를 같이 적어라 (src/a.js:42).",
|
|
125
|
+
"- 모르면 모른다고 하라. 그럴듯한 답을 지어내지 마라.",
|
|
126
|
+
"- 짧게, 물어본 것에만 답하라.",
|
|
127
|
+
"- 고치라는 말이 없으면 고칠 것을 제안하지 마라. 시키지 않은 일을 벌이지 마라.",
|
|
128
|
+
].join('\n'),
|
|
61
129
|
},
|
|
62
130
|
|
|
63
131
|
debug: {
|
|
@@ -70,9 +138,20 @@ export const MODES = {
|
|
|
70
138
|
effort: 'deep',
|
|
71
139
|
think: 'high',
|
|
72
140
|
steps: 32, // 원인 찾기는 왔다 갔다 하므로 여유를 준다
|
|
73
|
-
say:
|
|
74
|
-
|
|
75
|
-
|
|
141
|
+
say: [
|
|
142
|
+
"지금 하는 일은 **원인 찾기**다. 짐작으로 고치지 마라.",
|
|
143
|
+
"",
|
|
144
|
+
"이 순서를 지켜라.",
|
|
145
|
+
" 1. 증상을 한 문장으로 다시 적는다 — 무엇을 했을 때 무엇이 일어나는가.",
|
|
146
|
+
" 2. 재현 방법을 확보한다. 없으면 만든다. 재현 못 하면 고쳤는지도 알 수 없다.",
|
|
147
|
+
" 3. 가설을 2~3개 세운다. 각각 \"이게 맞다면 무엇이 보여야 하는가\" 를 같이 적는다.",
|
|
148
|
+
" 4. 하나씩 실제로 확인한다 — 로그를 보고, 작게 돌려 보고, 값을 찍어 본다.",
|
|
149
|
+
" 확인한 것만 사실이다. 확인 안 한 것은 아직 가설이다.",
|
|
150
|
+
" 5. 원인을 짚을 때는 증거를 같이 댄다. \"아마\" 로 시작하는 원인은 원인이 아니다.",
|
|
151
|
+
" 6. 고친 뒤 2번의 재현 절차로 다시 확인한다. 안 고쳐졌으면 3번으로 돌아간다.",
|
|
152
|
+
"",
|
|
153
|
+
"한 번에 여러 곳을 고치지 마라. 무엇이 고쳤는지 알 수 없게 된다.",
|
|
154
|
+
].join('\n'),
|
|
76
155
|
},
|
|
77
156
|
|
|
78
157
|
plan: {
|
|
@@ -85,9 +164,26 @@ export const MODES = {
|
|
|
85
164
|
effort: 'deep',
|
|
86
165
|
think: 'high',
|
|
87
166
|
steps: 16,
|
|
88
|
-
say:
|
|
89
|
-
|
|
90
|
-
|
|
167
|
+
say: [
|
|
168
|
+
"지금 하는 일은 **계획 세우기**다. 파일을 바꾸는 도구는 주어지지 않았다.",
|
|
169
|
+
"코드를 고치려 들지 마라. 계획을 내고 멈춘다.",
|
|
170
|
+
"",
|
|
171
|
+
"먼저 확인하라 — 지금 무엇이 어떻게 되어 있는지 모르면 계획이 아니라 희망이다.",
|
|
172
|
+
" Glob/Grep 으로 관련 파일을 찾고, Read 로 실제로 읽어라.",
|
|
173
|
+
"",
|
|
174
|
+
"그 다음 이 차례로 적어라.",
|
|
175
|
+
" 1. 목표 — 무엇이 끝나면 다 된 것인가 (확인할 수 있는 문장으로)",
|
|
176
|
+
" 2. 지금 상태 — 관련된 파일과 지금 동작 (경로:줄 로 짚어라)",
|
|
177
|
+
" 3. 바꿀 것 — 파일별로 무엇을 왜",
|
|
178
|
+
" 4. 순서 — 단계별로. 각 단계는 따로 확인할 수 있는 크기로 쪼개라",
|
|
179
|
+
" 5. 위험 — 무엇이 깨질 수 있고, 깨지면 어떻게 되돌리는가",
|
|
180
|
+
" 6. 확인 방법 — 무엇을 돌려 보면 됐는지 알 수 있는가",
|
|
181
|
+
"",
|
|
182
|
+
"단계는 TodoWrite 로도 적어라. 승인 뒤 그대로 이어서 하게 된다.",
|
|
183
|
+
"모르는 것이 있으면 지어내지 말고 \"이건 확인이 필요하다\" 고 적어라.",
|
|
184
|
+
"",
|
|
185
|
+
"마지막에 \"이대로 진행할까요?\" 로 끝내라. 승인을 받으면 /code 로 바꿔 실행한다.",
|
|
186
|
+
].join('\n'),
|
|
91
187
|
},
|
|
92
188
|
|
|
93
189
|
orchestrator: {
|
|
@@ -100,14 +196,24 @@ export const MODES = {
|
|
|
100
196
|
effort: 'save',
|
|
101
197
|
think: null,
|
|
102
198
|
steps: 40, // 여러 갈래를 끝까지 끌고 가야 한다
|
|
103
|
-
say:
|
|
104
|
-
|
|
105
|
-
|
|
199
|
+
say: [
|
|
200
|
+
"지금 하는 일은 **큰 일을 끝까지 끌고 가기**다.",
|
|
201
|
+
"",
|
|
202
|
+
" 1. 시작하자마자 TodoWrite 로 전체를 단계로 쪼개 적어라. 머릿속에만 두지 마라.",
|
|
203
|
+
" 단계는 각각 따로 확인할 수 있는 크기여야 한다.",
|
|
204
|
+
" 2. 한 번에 하나만 진행 중으로 둬라. 끝나면 바로 표시하고 다음으로 넘어가라.",
|
|
205
|
+
" 3. 각 단계 끝에서 확인하라. 확인 없이 넘어가면 어디서 어긋났는지 못 찾는다.",
|
|
206
|
+
" 4. 막히면 멈추고 무엇에 막혔는지 보고하라. 우회로를 몰래 타지 마라.",
|
|
207
|
+
" 5. 계획이 틀린 것을 알게 되면 목록을 고쳐라. 틀린 계획을 끝까지 밀지 마라.",
|
|
208
|
+
"",
|
|
209
|
+
"다 끝나면 무엇을 했는지, 무엇을 못 했는지 같이 요약하라.",
|
|
210
|
+
"못 한 것을 빼고 요약하지 마라.",
|
|
211
|
+
].join('\n'),
|
|
106
212
|
},
|
|
107
213
|
};
|
|
108
214
|
|
|
109
|
-
export const ORDER = ['code', 'plan', 'architect', 'debug', 'ask', 'orchestrator'];
|
|
110
|
-
export const DEFAULT = '
|
|
215
|
+
export const ORDER = ['auto', 'code', 'plan', 'architect', 'debug', 'ask', 'orchestrator'];
|
|
216
|
+
export const DEFAULT = 'auto';
|
|
111
217
|
|
|
112
218
|
/** 이름을 관대하게 받는다. 한글·영문·줄임말 다 통한다. */
|
|
113
219
|
export function normalize(v) {
|
|
@@ -115,6 +221,7 @@ export function normalize(v) {
|
|
|
115
221
|
if (!s) return null;
|
|
116
222
|
if (MODES[s]) return s;
|
|
117
223
|
const 별명 = {
|
|
224
|
+
'종합': 'auto', '자동': 'auto', '기본': 'auto', 'auto': 'auto', '맡김': 'auto',
|
|
118
225
|
'코드': 'code', 'c': 'code',
|
|
119
226
|
'계획': 'plan', '플랜': 'plan', 'p': 'plan',
|
|
120
227
|
'설계': 'architect', '아키': 'architect', 'arch': 'architect', 'a': 'architect',
|