deel-local-cli 1.2.0 → 1.4.1
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 +301 -1537
- package/README.md +272 -1582
- package/bin/deel.js +73 -4
- package/package.json +8 -3
- package/src/acp/jsonrpc.js +230 -0
- package/src/acp/map.js +219 -0
- package/src/acp/serve.js +556 -0
- package/src/agent/card.js +129 -0
- package/src/agent/compact.js +10 -2
- package/src/agent/effort.js +5 -0
- package/src/agent/evidence.js +186 -0
- package/src/agent/grade.js +20 -0
- package/src/agent/loop.js +134 -13
- package/src/agent/models.js +169 -0
- package/src/agent/modes.js +47 -1
- package/src/agent/pins.js +140 -0
- package/src/agent/preset.js +113 -0
- package/src/agent/project.js +10 -4
- package/src/agent/session.js +233 -13
- package/src/agent/store.js +31 -0
- package/src/backend/adapter.js +14 -0
- package/src/commands.js +616 -52
- package/src/i18n/en.js +265 -0
- package/src/i18n/index.js +126 -0
- package/src/i18n/ko.js +252 -0
- package/src/lsp/client.js +459 -0
- package/src/lsp/diag.js +112 -0
- package/src/lsp/rpc.js +84 -0
- package/src/lsp/servers.js +218 -0
- package/src/oneshot.js +19 -0
- package/src/pack/sbom.js +218 -0
- package/src/pack/selfpack.js +19 -3
- package/src/preview/serve.js +19 -2
- package/src/repl.js +193 -8
- package/src/safety/secrets.js +205 -0
- package/src/safety/undo.js +10 -3
- package/src/tools/desc.en.js +221 -0
- package/src/tools/docs.js +252 -0
- package/src/tools/index.js +190 -6
- package/src/tools/lsp.js +327 -0
- package/src/tools/task.js +30 -2
- package/src/ui/ansi.js +45 -0
- package/src/ui/approve.js +25 -21
- package/src/ui/banner.js +245 -0
- package/src/ui/export.js +217 -0
- package/src/ui/inputbox.js +37 -7
- package/src/ui/intro.js +206 -0
- package/src/ui/level.js +11 -5
- package/src/ui/notify.js +101 -0
- package/src/ui/status.js +181 -35
package/src/commands.js
CHANGED
|
@@ -3,17 +3,26 @@ import { writeFileSync, existsSync, statSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { c, say, rule, pad, bar, mark, width, clip } from './ui/ansi.js';
|
|
5
5
|
import { compact } from './agent/compact.js';
|
|
6
|
+
import { 증거모으기, 증거적기 } from './agent/evidence.js';
|
|
6
7
|
import { allowEndpoint } from './safety/network.js';
|
|
7
8
|
import { pick } from './ui/prompt.js';
|
|
8
9
|
import { load, save, resolveKey, upsert } from './config.js';
|
|
10
|
+
import { 종, 알릴만한초 } from './ui/notify.js';
|
|
11
|
+
import { 말, 언어, 언어정하기, 언어고르기, 옮긴만큼 } from './i18n/index.js';
|
|
12
|
+
import { 프로필찾기, 쓸수있나, 연결만들기, 알릴말, 목록보기 } from './agent/models.js';
|
|
13
|
+
import { allowTemporarily } from './safety/network.js';
|
|
14
|
+
import { chat } from './backend/adapter.js';
|
|
9
15
|
import { TOOLS } from './tools/index.js';
|
|
16
|
+
import { 둘러보기, 프로젝트갈래 } from './lsp/servers.js';
|
|
17
|
+
import { 지금것들 } from './lsp/client.js';
|
|
18
|
+
import { 보고서적기 } from './ui/export.js';
|
|
10
19
|
import { loadCommand, discover } from './skills/discover.js';
|
|
11
20
|
import { install, list, remove, pack } from './plugins/manage.js';
|
|
12
21
|
import { spin } from './ui/spinner.js';
|
|
13
22
|
import { PROFILES, LEVELS as THINK_LEVELS, normalizeProfile, table as effortTable } from './agent/effort.js';
|
|
14
23
|
import { scanLocal, toProfiles } from './backend/scan.js';
|
|
15
24
|
import { list as listSessions } from './agent/store.js';
|
|
16
|
-
import { MODES as WORK_MODES, ORDER as WORK_ORDER, DEFAULT as WORK_DEFAULT, normalize as normWork, get as getWork, canWrite } from './agent/modes.js';
|
|
25
|
+
import { MODES as WORK_MODES, ORDER as WORK_ORDER, DEFAULT as WORK_DEFAULT, normalize as normWork, get as getWork, canWrite, 보일이름, 보일한줄 } from './agent/modes.js';
|
|
17
26
|
import { LEVELS, ORDER as LEVEL_ORDER, DEFAULT as LEVEL_DEFAULT, normalize as normLevel, shows as levelShows } from './ui/level.js';
|
|
18
27
|
import { diffLines, renderDiff, shortStat } from './ui/diff.js';
|
|
19
28
|
import { readTextFull } from './tools/fsutil.js';
|
|
@@ -24,48 +33,83 @@ const MODES = {
|
|
|
24
33
|
strict: '엄격 — 파일 변경·명령 전부 물어봄',
|
|
25
34
|
};
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
36
|
+
/*
|
|
37
|
+
* 슬래시 명령 목록.
|
|
38
|
+
*
|
|
39
|
+
* ── 왜 설명이 여기 없나 ─────────────────────────────────────────────────
|
|
40
|
+
*
|
|
41
|
+
* 이름과 인자 유무만 여기 두고, **사람이 읽는 글은 i18n/ko.js 에 있다.**
|
|
42
|
+
* 명령 이름은 원래부터 영어였는데(help·model·undo…) 그 옆의 설명이 전부
|
|
43
|
+
* 한글이라, 저장소를 처음 연 영어권 사람은 `/help` 를 쳐 봐도 무엇을 하는
|
|
44
|
+
* 명령인지 알 수가 없었다. 그래서 설명만 갈아 끼울 수 있게 뺐다.
|
|
45
|
+
*
|
|
46
|
+
* desc·arg 를 **게터로** 둔다. 켤 때 한 번 읽어 굳히면 /lang 으로 바꿔도
|
|
47
|
+
* 목록이 안 따라오고, 그러면 바뀐 것처럼 보이다가 안 바뀐 자리가 남는다.
|
|
48
|
+
* 볼 때마다 지금 언어로 읽는 편이 어긋날 자리가 없다.
|
|
49
|
+
*
|
|
50
|
+
* 없는 열쇠는 열쇠 이름이 그대로 나온다 — 빈칸이 아니다. 명령을 새로 넣고
|
|
51
|
+
* ko.js 에 적는 것을 빠뜨리면 `cmd.foo.desc` 가 화면에 찍혀서 바로 눈에 띈다.
|
|
52
|
+
* (검사도 그걸 잡는다.)
|
|
53
|
+
*/
|
|
54
|
+
const 명령들 = {
|
|
55
|
+
help: { arg: false },
|
|
56
|
+
clear: { arg: false },
|
|
57
|
+
context: { arg: false },
|
|
58
|
+
ctx: { arg: true },
|
|
59
|
+
out: { arg: true },
|
|
60
|
+
grade: { arg: true },
|
|
61
|
+
compact: { arg: false },
|
|
62
|
+
model: { arg: true },
|
|
63
|
+
think: { arg: true },
|
|
64
|
+
mode: { arg: true },
|
|
65
|
+
work: { arg: true },
|
|
66
|
+
auto: { arg: false },
|
|
67
|
+
code: { arg: false },
|
|
68
|
+
plan: { arg: false },
|
|
69
|
+
architect: { arg: false },
|
|
70
|
+
debug: { arg: false },
|
|
71
|
+
ask: { arg: false },
|
|
72
|
+
orchestrator: { arg: false },
|
|
73
|
+
undo: { arg: true },
|
|
74
|
+
diff: { arg: true },
|
|
75
|
+
preview: { arg: true },
|
|
76
|
+
tools: { arg: false },
|
|
77
|
+
skills: { arg: true },
|
|
78
|
+
plugin: { arg: true },
|
|
79
|
+
cost: { arg: false },
|
|
80
|
+
status: { arg: false },
|
|
81
|
+
scan: { arg: true },
|
|
82
|
+
thread: { arg: true },
|
|
83
|
+
learned: { arg: true },
|
|
84
|
+
pin: { arg: true },
|
|
85
|
+
evidence: { arg: true },
|
|
86
|
+
sessions: { arg: false },
|
|
87
|
+
recall: { arg: true },
|
|
88
|
+
mcp: { arg: false },
|
|
89
|
+
memory: { arg: true },
|
|
90
|
+
level: { arg: true },
|
|
91
|
+
bell: { arg: true },
|
|
92
|
+
consult: { arg: true },
|
|
93
|
+
lang: { arg: true },
|
|
94
|
+
lsp: { arg: true },
|
|
95
|
+
export: { arg: true },
|
|
96
|
+
init: { arg: false },
|
|
97
|
+
exit: { arg: false },
|
|
98
|
+
quit: { arg: false },
|
|
67
99
|
};
|
|
68
100
|
|
|
101
|
+
/**
|
|
102
|
+
* 화면에 낼 명령표. desc·arg 는 볼 때마다 지금 언어로 읽는다.
|
|
103
|
+
*
|
|
104
|
+
* @type {Record<string, {desc: string, arg?: string}>}
|
|
105
|
+
*/
|
|
106
|
+
export const COMMANDS = Object.fromEntries(Object.entries(명령들).map(([이름, 꼴]) => {
|
|
107
|
+
const 것 = { get desc() { return 말(`cmd.${이름}.desc`); } };
|
|
108
|
+
if (꼴.arg) Object.defineProperty(것, 'arg', { get: () => 말(`cmd.${이름}.arg`), enumerable: true });
|
|
109
|
+
return [이름, 것];
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
|
|
69
113
|
/**
|
|
70
114
|
* 이 줄이 명령이 아니라 '경로' 인가.
|
|
71
115
|
*
|
|
@@ -98,6 +142,295 @@ export async function handle(line, session, ctx) {
|
|
|
98
142
|
switch (name) {
|
|
99
143
|
case 'help': return help(session), { handled: true };
|
|
100
144
|
case 'level': return showLevel(session, arg), { handled: true };
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
* 종소리 · 창 제목.
|
|
148
|
+
*
|
|
149
|
+
* 끄는 길을 굳이 명령으로 둔 이유가 있다. 사무실에서 소리가 나면 곤란한
|
|
150
|
+
* 사람이 있고, 그런 사람은 알림 자체를 못 쓰게 되는 것이 아니라 **그 자리에서
|
|
151
|
+
* 껐다 켤 수 있어야** 한다. 환경변수만 두면 프로그램을 껐다 켜야 한다.
|
|
152
|
+
*/
|
|
153
|
+
/*
|
|
154
|
+
* 화면 말 — 한국어 / English.
|
|
155
|
+
*
|
|
156
|
+
* 코드는 한국어로 둔다. 함수 이름도 변수 이름도 한글이고 그게 이 저장소의
|
|
157
|
+
* 뜻이다. 바꾸는 것은 **화면에 나가는 말**뿐이다.
|
|
158
|
+
*
|
|
159
|
+
* 얼마나 옮겼는지를 숨기지 않고 그대로 적는다. 다 된 척하는 것보다
|
|
160
|
+
* "298개 중 47개는 아직 한국어" 가 낫다 — 그래야 안 옮긴 자리를 봤을 때
|
|
161
|
+
* 사람이 고장으로 안 읽고, 도와줄 사람도 어디를 도울지 안다.
|
|
162
|
+
*/
|
|
163
|
+
/*
|
|
164
|
+
* 다른 모델에게 한 번 물어보기.
|
|
165
|
+
*
|
|
166
|
+
* ── 왜 따로 두나 ────────────────────────────────────────────────────
|
|
167
|
+
*
|
|
168
|
+
* 로컬에서는 모델 하나를 골라도 늘 어딘가 아쉽다. 7B 는 계획을 잘 세우는데
|
|
169
|
+
* 파일 열 개를 고치다 창이 차고, 1.5B 는 창은 넉넉한데 무엇을 할지를 못
|
|
170
|
+
* 정한다. 그런데 지금까지는 다른 것에게 물어보려면 /model 로 **갈아타야**
|
|
171
|
+
* 했고, 갈아타면 하던 대화가 그 모델의 것이 된다 — 한마디 물어보자고
|
|
172
|
+
* 판을 통째로 옮기는 셈이었다.
|
|
173
|
+
*
|
|
174
|
+
* ── 무엇을 보내나 ───────────────────────────────────────────────────
|
|
175
|
+
*
|
|
176
|
+
* **친 질문만** 보낸다. 지금 대화를 통째로 딸려 보내지 않는다.
|
|
177
|
+
*
|
|
178
|
+
* 이건 편의가 아니라 경계선 문제다. 대화에는 이 폴더의 소스가 통째로
|
|
179
|
+
* 들어 있는데, 그것이 어느 서버로 가는지는 사람이 정할 일이지 명령 하나가
|
|
180
|
+
* 조용히 정할 일이 아니다. 필요한 배경은 질문에 적으면 된다.
|
|
181
|
+
*
|
|
182
|
+
* 답은 화면에 보여 주고 **대화에도 넣는다.** 안 넣으면 사람이 그 답을
|
|
183
|
+
* 손으로 옮겨 적어야 하는데, 그러면 이건 다른 창에서 물어보는 것과 다를
|
|
184
|
+
* 것이 없다. 다만 누가 한 말인지 표를 달아 넣는다 — 지금 쓰는 모델이
|
|
185
|
+
* 제가 한 말로 착각하면 안 된다.
|
|
186
|
+
*/
|
|
187
|
+
case 'consult': {
|
|
188
|
+
const 조각 = String(arg ?? '').trim().split(/\s+/);
|
|
189
|
+
const 이름 = 조각.shift() ?? '';
|
|
190
|
+
const 질문 = 조각.join(' ').trim();
|
|
191
|
+
const 목록 = 목록보기();
|
|
192
|
+
|
|
193
|
+
if (!이름) {
|
|
194
|
+
say('');
|
|
195
|
+
rule(말('consult.title'), 70);
|
|
196
|
+
if (!목록.length) {
|
|
197
|
+
say(` ${c.gray(말('consult.noProfiles'))}`);
|
|
198
|
+
} else {
|
|
199
|
+
for (const p of 목록) {
|
|
200
|
+
const 표 = p.로컬 ? c.hgreen('⌂') : c.hyellow('↗');
|
|
201
|
+
say(` ${표} ${c.cyan(pad(p.id, 16))} ${c.white(pad(p.model, 26))} ${c.gray(p.어디)}${p.지금 ? c.gray(` ${말('consult.current')}`) : ''}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
say('');
|
|
205
|
+
say(` ${c.gray(말('consult.usage'))}`);
|
|
206
|
+
say(` ${c.gray(말('consult.questionOnly'))}`);
|
|
207
|
+
say('');
|
|
208
|
+
return { handled: true };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const 찾음 = 프로필찾기(이름);
|
|
212
|
+
if (!찾음.ok) {
|
|
213
|
+
say(` ${c.gray(찾음.why)}`);
|
|
214
|
+
if (찾음.후보.length) say(` ${c.gray(말('consult.candidates'))} ${찾음.후보.map((p) => c.cyan(p.id)).join(c.gray(' · '))}`);
|
|
215
|
+
else if (목록.length) say(` ${c.gray(말('consult.have'))} ${목록.map((p) => c.cyan(p.id)).join(c.gray(' · '))}`);
|
|
216
|
+
say('');
|
|
217
|
+
return { handled: true };
|
|
218
|
+
}
|
|
219
|
+
const 되나 = 쓸수있나(찾음.prof);
|
|
220
|
+
if (!되나.ok) { say(` ${c.yellow('!')} ${c.gray(되나.why)}`); say(''); return { handled: true }; }
|
|
221
|
+
if (!질문) {
|
|
222
|
+
say(` ${c.gray(말('consult.needQuestion', { 이름: 찾음.prof.id }))}`);
|
|
223
|
+
say('');
|
|
224
|
+
return { handled: true };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const 새conn = 연결만들기(찾음.prof);
|
|
228
|
+
const 알림 = 알릴말(session.conn, 새conn);
|
|
229
|
+
say('');
|
|
230
|
+
say(알림.밖으로
|
|
231
|
+
? ` ${c.hyellow('↗')} ${c.hyellow(알림.말)} ${c.gray(말('consult.goesOut'))}`
|
|
232
|
+
: ` ${c.hgreen('⌂')} ${c.gray(알림.말)}`);
|
|
233
|
+
|
|
234
|
+
// 그 한 번 동안만 연다. 끝나면 반드시 닫는다.
|
|
235
|
+
const 닫기 = 알림.다른자리 ? allowTemporarily(새conn.base) : null;
|
|
236
|
+
const 돌림 = spin(말('consult.asking', { 모델: 새conn.model }));
|
|
237
|
+
let 답 = null;
|
|
238
|
+
let 탈 = null;
|
|
239
|
+
const 잰때 = Date.now();
|
|
240
|
+
try {
|
|
241
|
+
const m = await chat(새conn, {
|
|
242
|
+
messages: [
|
|
243
|
+
{ role: 'system', content: 말('consult.system') },
|
|
244
|
+
{ role: 'user', content: 질문 },
|
|
245
|
+
],
|
|
246
|
+
temperature: 0.3,
|
|
247
|
+
});
|
|
248
|
+
답 = String(m?.content ?? '').trim();
|
|
249
|
+
} catch (err) {
|
|
250
|
+
탈 = String(err?.message ?? err);
|
|
251
|
+
} finally {
|
|
252
|
+
돌림.stop();
|
|
253
|
+
try { 닫기?.(); } catch { /* 닫다 터져도 이번 대화는 이어간다 */ }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (탈) {
|
|
257
|
+
say(` ${c.yellow('!')} ${c.gray(말('consult.failed', { 왜: clip(탈, 120) }))}`);
|
|
258
|
+
say('');
|
|
259
|
+
return { handled: true };
|
|
260
|
+
}
|
|
261
|
+
if (!답) {
|
|
262
|
+
say(` ${c.gray(말('consult.empty'))}`);
|
|
263
|
+
say('');
|
|
264
|
+
return { handled: true };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const 초 = ((Date.now() - 잰때) / 1000).toFixed(1);
|
|
268
|
+
say(` ${c.gray('─'.repeat(2))} ${c.gray(`${찾음.prof.id} · ${초}초`)}`);
|
|
269
|
+
say('');
|
|
270
|
+
for (const l of 답.split('\n')) say(` ${l}`);
|
|
271
|
+
say('');
|
|
272
|
+
|
|
273
|
+
/*
|
|
274
|
+
* 대화에 넣을 때 **누가 한 말인지 표를 단다.**
|
|
275
|
+
*
|
|
276
|
+
* 안 달면 지금 쓰는 모델이 제가 아까 한 말로 읽는다. 작은 모델이 대충
|
|
277
|
+
* 답한 것을 제 판단으로 삼아 그 위에 쌓으면, 틀린 자리가 어디서 왔는지
|
|
278
|
+
* 아무도 못 찾는다.
|
|
279
|
+
*/
|
|
280
|
+
session.push({
|
|
281
|
+
role: 'user',
|
|
282
|
+
content: `[${찾음.prof.id} (${새conn.model}) 에게 따로 물어본 결과다. 네가 한 말이 아니다.]\n`
|
|
283
|
+
+ `물음: ${질문}\n답: ${답}`,
|
|
284
|
+
});
|
|
285
|
+
ctx.audit.tool('Consult', { 프로필: 찾음.prof.id, 모델: 새conn.model, 질문: clip(질문, 200) },
|
|
286
|
+
{ summary: `${알림.말} · ${답.length}자` });
|
|
287
|
+
say(` ${c.gray(말('consult.added'))}`);
|
|
288
|
+
say('');
|
|
289
|
+
return { handled: true };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
case 'lang': {
|
|
293
|
+
const 값 = String(arg ?? '').trim();
|
|
294
|
+
const 이름 = (l) => 말(`lang.${l}`);
|
|
295
|
+
if (!값) {
|
|
296
|
+
const p = 옮긴만큼();
|
|
297
|
+
say('');
|
|
298
|
+
say(` ${c.gray(말('lang.now', { 이름: 이름(언어()) }))}`);
|
|
299
|
+
say(` ${c.gray(말('lang.progress', { 언어: 언어(), 옮김: p.옮김, 전체: p.전체 }))}`);
|
|
300
|
+
if (p.남음) say(` ${c.gray(말('lang.partial'))}`);
|
|
301
|
+
say(` ${c.gray(말('lang.codeStays'))}`);
|
|
302
|
+
say(` ${c.gray(말('lang.howto'))} ${c.cyan('/lang ko')} ${c.gray('·')} ${c.cyan('/lang en')}`);
|
|
303
|
+
say(` ${c.gray(말('lang.envHint'))}`);
|
|
304
|
+
say('');
|
|
305
|
+
return { handled: true };
|
|
306
|
+
}
|
|
307
|
+
if (!언어고르기(값)) {
|
|
308
|
+
say(` ${c.gray(말('lang.unknown'))}`);
|
|
309
|
+
say('');
|
|
310
|
+
return { handled: true };
|
|
311
|
+
}
|
|
312
|
+
언어정하기(값);
|
|
313
|
+
const cfg = load();
|
|
314
|
+
cfg.lang = 언어();
|
|
315
|
+
try { save(cfg); } catch { /* 못 남겨도 이번 세션에는 먹는다 */ }
|
|
316
|
+
const p = 옮긴만큼();
|
|
317
|
+
say('');
|
|
318
|
+
say(` ${mark.ok} ${말('lang.changed', { 이름: 이름(언어()) })}`);
|
|
319
|
+
say(` ${c.gray(말('lang.progress', { 언어: 언어(), 옮김: p.옮김, 전체: p.전체 }))}`);
|
|
320
|
+
if (p.남음) say(` ${c.gray(말('lang.partial'))}`);
|
|
321
|
+
say('');
|
|
322
|
+
return { handled: true };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/*
|
|
326
|
+
* 언어 서버 — 무엇이 깔려 있고, 지금 무엇이 떠 있나.
|
|
327
|
+
*
|
|
328
|
+
* 이 화면이 필요한 이유는 하나다. Def·Refs 는 서버가 없으면 **목록에 아예
|
|
329
|
+
* 안 나온다.** 그러면 사용자 눈에는 "왜 어떤 프로젝트에서는 되고 어떤
|
|
330
|
+
* 데서는 안 되지" 로 보인다. 여기서 그 까닭과 깔 방법을 대신 말해 준다.
|
|
331
|
+
*
|
|
332
|
+
* 대신 **여기서도 안 깔아 준다.** 깔 명령을 글자로 보여 줄 뿐이고,
|
|
333
|
+
* 칠지 말지는 사람이 정한다 (lsp/servers.js 머리말).
|
|
334
|
+
*/
|
|
335
|
+
/*
|
|
336
|
+
* 대화 → 보고서 한 장 (ui/export.js).
|
|
337
|
+
*
|
|
338
|
+
* 폐쇄망에는 세션 공유 링크가 없다. 이 자리의 공유는 파일이다 —
|
|
339
|
+
* 결재는 첨부로 돌고, 보고는 한 장으로 한다.
|
|
340
|
+
*/
|
|
341
|
+
case 'export': {
|
|
342
|
+
const 자리 = 보고서적기(ctx.scope.root, session, { scope: ctx.scope, audit: ctx.audit });
|
|
343
|
+
say('');
|
|
344
|
+
if (자리) {
|
|
345
|
+
say(` ${mark.ok} ${말('export.saved')} ${c.white(ctx.scope.show(자리))}`);
|
|
346
|
+
say(` ${c.gray(말('export.openHint'))}`);
|
|
347
|
+
} else {
|
|
348
|
+
say(` ${c.red('✗')} ${말('export.failed')}`);
|
|
349
|
+
}
|
|
350
|
+
say('');
|
|
351
|
+
return { handled: true };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
case 'lsp': {
|
|
355
|
+
const 값 = String(arg ?? '').trim().toLowerCase();
|
|
356
|
+
if (값 === 'off' || 값 === '끔' || 값 === '꺼') {
|
|
357
|
+
if (ctx?.lsp) ctx.lsp.켬 = false;
|
|
358
|
+
say('');
|
|
359
|
+
say(` ${mark.ok} ${말('lsp.diagOff')}`);
|
|
360
|
+
say('');
|
|
361
|
+
return { handled: true };
|
|
362
|
+
}
|
|
363
|
+
if (값 === 'on' || 값 === '켬' || 값 === '켜') {
|
|
364
|
+
if (ctx?.lsp) ctx.lsp.켬 = true;
|
|
365
|
+
say('');
|
|
366
|
+
say(` ${mark.ok} ${말('lsp.diagOn')}`);
|
|
367
|
+
say('');
|
|
368
|
+
return { handled: true };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const { 있는것, 없는것 } = 둘러보기();
|
|
372
|
+
const 뿌리것 = ctx?.scope?.root ? 프로젝트갈래(ctx.scope.root) : null;
|
|
373
|
+
const 떠있는것 = 지금것들();
|
|
374
|
+
say('');
|
|
375
|
+
if (있는것.length) {
|
|
376
|
+
say(` ${c.hcyan('◈')} ${c.white(말('lsp.found', { 수: 있는것.length }))}`);
|
|
377
|
+
for (const it of 있는것) say(` ${c.hgreen('✓')} ${c.white(it.갈래.padEnd(5))} ${c.gray(it.이름)}`);
|
|
378
|
+
} else {
|
|
379
|
+
say(` ${c.gray(말('lsp.none'))}`);
|
|
380
|
+
}
|
|
381
|
+
say('');
|
|
382
|
+
say(` ${c.gray(말('lsp.thisFolder'))} ${뿌리것 ? c.hcyan(뿌리것.갈래) + c.gray(` · ${말('lsp.fileCount', { 수: 뿌리것.개수 })}`) : c.gray(말('lsp.noneHere'))}`);
|
|
383
|
+
say(` ${c.gray(말('lsp.tools'))} ${session.lsp ? c.hgreen('Def · Refs') : c.gray(말('lsp.toolsHidden'))}`);
|
|
384
|
+
say(` ${c.gray(말('lsp.diag'))} ${ctx?.lsp?.켬 === false ? c.gray(말('lsp.stateOff')) : c.hgreen(말('lsp.stateOn'))}`);
|
|
385
|
+
if (떠있는것.length) {
|
|
386
|
+
for (const it of 떠있는것) {
|
|
387
|
+
const 꼴 = it.죽음 ? c.yellow(it.죽음) : (it.준비 ? c.hgreen(말('lsp.ready')) : c.gray(말('lsp.starting')));
|
|
388
|
+
say(` ${c.gray('·')} ${c.white(it.이름 ?? it.갈래)} ${꼴}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (없는것.length && !있는것.length) {
|
|
392
|
+
say('');
|
|
393
|
+
say(` ${c.gray(말('lsp.installHint'))}`);
|
|
394
|
+
for (const it of 없는것.slice(0, 4)) say(` ${c.gray(`${it.갈래.padEnd(5)} ${it.깔기}`)}`);
|
|
395
|
+
say(` ${c.gray(말('lsp.neverInstalls'))}`);
|
|
396
|
+
}
|
|
397
|
+
say(` ${c.gray(말('lsp.howto'))} ${c.cyan('/lsp on')} ${c.gray('·')} ${c.cyan('/lsp off')}`);
|
|
398
|
+
say('');
|
|
399
|
+
return { handled: true };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
case 'bell': {
|
|
403
|
+
const cfg = load();
|
|
404
|
+
const 켜는말 = ['on', '켬', '켜', '켜기', 'y', 'yes', 'ㅇ'];
|
|
405
|
+
const 끄는말 = ['off', '끔', '꺼', '끄기', 'n', 'no', 'ㄴ'];
|
|
406
|
+
const 값 = String(arg ?? '').trim().toLowerCase();
|
|
407
|
+
if (!값) {
|
|
408
|
+
const 켜져있나 = cfg.bell !== false;
|
|
409
|
+
const 상태 = 켜져있나 ? c.hgreen(말('bell.stateOn')) : c.gray(말('bell.stateOff'));
|
|
410
|
+
say('');
|
|
411
|
+
say(` ${c.gray(말('bell.now', { 상태 }))}`);
|
|
412
|
+
say(` ${c.gray(말('bell.when', { 초: 알릴만한초 }))}`);
|
|
413
|
+
say(` ${c.gray(말('bell.title'))}`);
|
|
414
|
+
say(` ${c.gray(말('bell.howto'))} ${c.cyan('/bell on')} ${c.gray('·')} ${c.cyan('/bell off')}`);
|
|
415
|
+
if (!process.stdout.isTTY && !process.stderr.isTTY) say(` ${c.gray(말('bell.notATty'))}`);
|
|
416
|
+
say('');
|
|
417
|
+
return { handled: true };
|
|
418
|
+
}
|
|
419
|
+
if (!켜는말.includes(값) && !끄는말.includes(값)) {
|
|
420
|
+
say(` ${c.gray(말('bell.needOnOff'))}`);
|
|
421
|
+
say('');
|
|
422
|
+
return { handled: true };
|
|
423
|
+
}
|
|
424
|
+
cfg.bell = 켜는말.includes(값);
|
|
425
|
+
try { save(cfg); } catch { /* 못 남겨도 이번 세션에는 먹는다 */ }
|
|
426
|
+
say('');
|
|
427
|
+
say(` ${mark.ok} ${말(cfg.bell ? 'bell.turnedOn' : 'bell.turnedOff')}`);
|
|
428
|
+
// 켤 때는 한 번 울려 준다. "켰다는데 소리가 나나?" 를 그 자리에서 확인하게.
|
|
429
|
+
if (cfg.bell) { 종(); say(` ${c.gray(말('bell.justRang'))}`); }
|
|
430
|
+
say(` ${c.gray(말('bell.appliesNow'))}`);
|
|
431
|
+
say('');
|
|
432
|
+
return { handled: true };
|
|
433
|
+
}
|
|
101
434
|
case 'exit':
|
|
102
435
|
case 'quit': return { handled: true, exit: true };
|
|
103
436
|
|
|
@@ -139,7 +472,10 @@ export async function handle(line, session, ctx) {
|
|
|
139
472
|
return { handled: true };
|
|
140
473
|
}
|
|
141
474
|
|
|
142
|
-
case 'model':
|
|
475
|
+
case 'model':
|
|
476
|
+
// `/model 카드` 는 모델을 바꾸는 게 아니라 이 모델을 겪어 본 결과를 본다.
|
|
477
|
+
if (/^(카드|card)$/i.test(String(arg ?? '').trim())) return 카드명령(session, ctx), { handled: true };
|
|
478
|
+
return await switchModel(session, ctx, arg), { handled: true };
|
|
143
479
|
|
|
144
480
|
/*
|
|
145
481
|
* /think — '얼마나 생각하나' 하나만 정한다.
|
|
@@ -246,15 +582,52 @@ export async function handle(line, session, ctx) {
|
|
|
246
582
|
return { handled: true };
|
|
247
583
|
}
|
|
248
584
|
|
|
585
|
+
/*
|
|
586
|
+
* 되돌리기 — 파일과 **대화를 같이** 되감는다.
|
|
587
|
+
*
|
|
588
|
+
* 전에는 파일만 되돌렸다. 대화에는 "src/runner.js 를 고쳤습니다" 가 그대로
|
|
589
|
+
* 남아 있어서, 그다음 턴에 모델은 이미 고쳐 놓은 줄 알고 그 위에 이어
|
|
590
|
+
* 일했다 — 없는 코드를 고치려 들고, 없는 함수를 부른다. 사람 눈에는 모델이
|
|
591
|
+
* 헛소리하는 것으로 보이지만, 사실은 우리가 모델에게 거짓말을 남겨 둔 것이다.
|
|
592
|
+
*
|
|
593
|
+
* 세 군데를 같이 맞춰야 한 벌이 된다 — 오간 말, 적어 둔 파일(--resume 이
|
|
594
|
+
* 이어 여는 자리), 그리고 /diff 가 세는 '바뀐 파일'. 하나라도 빠지면
|
|
595
|
+
* 되돌린 것이 다음 순간 되살아난다.
|
|
596
|
+
*/
|
|
249
597
|
case 'undo': {
|
|
250
598
|
const n = Math.max(1, parseInt(arg, 10) || 1);
|
|
251
599
|
const r = ctx.history.undo(n);
|
|
252
600
|
ctx.audit.undo({ turns: r.turns, files: r.restored.length });
|
|
253
601
|
if (!r.restored.length) {
|
|
254
|
-
say(` ${c.gray('
|
|
602
|
+
say(` ${c.gray(말('undo.nothing'))}`);
|
|
603
|
+
say('');
|
|
604
|
+
return { handled: true };
|
|
605
|
+
}
|
|
606
|
+
say(` ${mark.ok} ${말('undo.done', { turns: r.turns, files: r.restored.length })}`);
|
|
607
|
+
for (const f of r.restored) say(` ${c.gray(ctx.scope.show(f.path))} ${c.gray(f.how)}`);
|
|
608
|
+
|
|
609
|
+
// /diff 가 세던 것에서도 뺀다. 되돌린 파일이 '바뀐 파일' 로 남아 있으면
|
|
610
|
+
// 사람도 되돌아간 줄 모른다.
|
|
611
|
+
for (const f of r.restored) if (!f.skipped) session.changes.delete(f.path);
|
|
612
|
+
// 상태줄의 '되돌릴 턴' 도 여기서 줄여 준다. 안 줄이면 방금 되돌린 것을
|
|
613
|
+
// 아직 되돌릴 수 있는 것처럼 세고 있게 된다.
|
|
614
|
+
try { session.되돌릴턴 = ctx.history.turns().length; } catch { /* 못 세면 그냥 둔다 */ }
|
|
615
|
+
|
|
616
|
+
const 되감음 = session.되감기(r.turnIds ?? []);
|
|
617
|
+
if (되감음.걷은것) {
|
|
618
|
+
say(` ${c.gray(말('undo.alsoTalk', { n: 되감음.걷은것 }))}`);
|
|
619
|
+
if (되감음.고친것) say(` ${c.gray(말('undo.repaired', { n: 되감음.고친것 }))}`);
|
|
620
|
+
try { ctx?.갈래?.현재store?.()?.replace(session.messages, `되돌리기 — ${r.turns}개 턴`); }
|
|
621
|
+
catch { /* 적어 두지 못해도 이번 대화는 이어진다 */ }
|
|
622
|
+
if (되감음.사람말) {
|
|
623
|
+
const 한줄 = 되감음.사람말.replace(/\s+/g, ' ').trim();
|
|
624
|
+
say(` ${c.gray(말('undo.saidWas'))} ${c.cyan(한줄.length > 60 ? `${한줄.slice(0, 60)}…` : 한줄)}`);
|
|
625
|
+
}
|
|
255
626
|
} else {
|
|
256
|
-
|
|
257
|
-
|
|
627
|
+
// 접기·요약이 그 자리를 이미 가져갔을 때다. 파일은 되돌아갔지만 대화는
|
|
628
|
+
// 못 걷었다는 것을 숨기지 않는다 — 숨기면 위의 사고가 그대로 난다.
|
|
629
|
+
say(` ${c.gray(말('undo.talkKept'))}`);
|
|
630
|
+
say(` ${c.gray(말('undo.talkKeptWhy'))}`);
|
|
258
631
|
}
|
|
259
632
|
say('');
|
|
260
633
|
return { handled: true };
|
|
@@ -331,7 +704,7 @@ export async function handle(line, session, ctx) {
|
|
|
331
704
|
session.routed = null;
|
|
332
705
|
const w = getWork(골라진);
|
|
333
706
|
say('');
|
|
334
|
-
say(` ${c.hcyan(w.glyph)} ${c.bold(w.
|
|
707
|
+
say(` ${c.hcyan(w.glyph)} ${c.bold(보일이름(w.id))} ${c.gray('(' + w.en + ')')} ${c.gray(보일한줄(w.id))}`);
|
|
335
708
|
say(` ${c.gray('도구')} ${canWrite(골라진) ? c.yellow('읽기 + 파일 바꾸기') : c.green('읽기만 — 파일을 못 바꿉니다')}`);
|
|
336
709
|
say(` ${c.gray('생각')} ${c.white(w.think ?? session.think)}${c.gray('·')}${c.magenta(w.effort)} ${c.gray('최대 ' + w.steps + '걸음')}`);
|
|
337
710
|
if (골라진 === 'auto') {
|
|
@@ -538,6 +911,12 @@ export async function handle(line, session, ctx) {
|
|
|
538
911
|
case 'learned':
|
|
539
912
|
case '배움': return 배움명령(session, ctx, arg), { handled: true };
|
|
540
913
|
|
|
914
|
+
case 'pin':
|
|
915
|
+
case '못박기': return 못박기명령(session, ctx, arg), { handled: true };
|
|
916
|
+
|
|
917
|
+
case 'evidence':
|
|
918
|
+
case '증거': return 증거명령(session, ctx, arg), { handled: true };
|
|
919
|
+
|
|
541
920
|
case 'sessions': {
|
|
542
921
|
const rows = listSessions(session.root, { limit: 12 });
|
|
543
922
|
rule('이 폴더의 지난 대화', 70);
|
|
@@ -857,7 +1236,7 @@ function help(session) {
|
|
|
857
1236
|
const level = session?.level ?? LEVEL_DEFAULT;
|
|
858
1237
|
const 쉬움 = !levelShows(level, '__전부__'); // 개발자면 show 가 null 이라 전부 참
|
|
859
1238
|
say('');
|
|
860
|
-
rule(쉬움 ? '
|
|
1239
|
+
rule(말(쉬움 ? 'help.titleCommon' : 'help.title'), 70);
|
|
861
1240
|
let 감춘수 = 0;
|
|
862
1241
|
for (const [n, m] of Object.entries(COMMANDS)) {
|
|
863
1242
|
if (n === 'quit') continue;
|
|
@@ -867,11 +1246,11 @@ function help(session) {
|
|
|
867
1246
|
say('');
|
|
868
1247
|
// 감춘 것은 '못 쓰는 것' 이 아니다. 그 말을 분명히 해 둔다.
|
|
869
1248
|
if (감춘수) {
|
|
870
|
-
say(` ${c.gray(
|
|
871
|
-
say(` ${c.gray('
|
|
1249
|
+
say(` ${c.gray(말('help.moreHidden', { n: 감춘수 }))}`);
|
|
1250
|
+
say(` ${c.gray(말('help.showAll'))} ${c.cyan('/level developer')}`);
|
|
872
1251
|
say('');
|
|
873
1252
|
}
|
|
874
|
-
say(` ${c.gray('
|
|
1253
|
+
say(` ${c.gray(말('help.restGoesToModel'))}`);
|
|
875
1254
|
say('');
|
|
876
1255
|
}
|
|
877
1256
|
|
|
@@ -1075,6 +1454,190 @@ function 갈래명령(session, ctx, arg = '') {
|
|
|
1075
1454
|
* 모델이 왜 그렇게 답했는지 알 수 없게 된다 — 여기서 통째로 볼 수 있어야
|
|
1076
1455
|
* '자동으로 쌓인다' 가 무섭지 않은 말이 된다. 지우는 길도 같이 둔다.
|
|
1077
1456
|
*/
|
|
1457
|
+
/**
|
|
1458
|
+
* 증거 — 「다 됐습니다」 대신 검토할 수 있는 것.
|
|
1459
|
+
*
|
|
1460
|
+
* 화면에서 제일 중요한 것은 맨 아래 '증명 안 된 것' 이다. 바꾼 것을 늘어놓는
|
|
1461
|
+
* 일은 /diff 도 한다. 안 한 것을 말하는 자리는 여기뿐이다.
|
|
1462
|
+
*/
|
|
1463
|
+
function 증거명령(session, ctx, arg = '') {
|
|
1464
|
+
const e = 증거모으기(session, { audit: ctx?.audit });
|
|
1465
|
+
const 말 = String(arg ?? '').trim();
|
|
1466
|
+
say('');
|
|
1467
|
+
rule('작업 증거', 70);
|
|
1468
|
+
|
|
1469
|
+
if (!e.바꾼것.length && !e.돌린것.length) {
|
|
1470
|
+
say(` ${c.gray('이번 대화에서 아직 바꾸거나 돌린 것이 없습니다.')}`);
|
|
1471
|
+
say('');
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
if (e.바꾼것.length) {
|
|
1476
|
+
say(` ${c.bold('바꾼 것')} ${c.gray(`— 파일 ${e.셈.파일}개 · +${e.셈.더한줄} / -${e.셈.뺀줄}`)}`);
|
|
1477
|
+
for (const x of e.바꾼것.slice(0, 12)) {
|
|
1478
|
+
const 표 = x.증명 ? c.green('✓') : c.yellow('?');
|
|
1479
|
+
const 뒤 = x.증명 ? c.gray(`← ${x.증명}`) : c.yellow('확인 안 됨');
|
|
1480
|
+
say(` ${표} ${pad(x.파일, 34)} ${c.gray(`+${x.더한줄} -${x.뺀줄}`)} ${뒤}`);
|
|
1481
|
+
}
|
|
1482
|
+
if (e.바꾼것.length > 12) say(` ${c.gray(`… 그 밖에 ${e.바꾼것.length - 12}개`)}`);
|
|
1483
|
+
say('');
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
if (e.돌린것.length) {
|
|
1487
|
+
say(` ${c.bold('돌린 것')} ${c.gray(`— ${e.셈.돌린것}개${e.셈.실패한것 ? `, 그중 ${e.셈.실패한것}개 실패` : ''}`)}`);
|
|
1488
|
+
for (const x of e.돌린것.slice(-10)) {
|
|
1489
|
+
const 표 = x.됐나 ? c.green('✓') : c.red('✗');
|
|
1490
|
+
say(` ${표} ${clip(x.무엇 || x.도구, 44)} ${c.gray(clip(x.남긴말 ?? '', 24))}`);
|
|
1491
|
+
}
|
|
1492
|
+
say('');
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// 여기가 요점이다.
|
|
1496
|
+
if (e.증명안된것.length) {
|
|
1497
|
+
say(` ${c.yellow('증명 안 된 것')} ${c.gray(`— ${e.증명안된것.length}개`)}`);
|
|
1498
|
+
for (const x of e.증명안된것.slice(0, 8)) {
|
|
1499
|
+
say(` ${c.yellow('·')} ${c.bold(x.파일)}`);
|
|
1500
|
+
say(` ${c.gray(x.왜)}`);
|
|
1501
|
+
}
|
|
1502
|
+
say('');
|
|
1503
|
+
say(` ${c.gray('돌려 볼 것이 있으면 지금 돌리고 다시 보세요.')}`);
|
|
1504
|
+
} else {
|
|
1505
|
+
say(` ${c.green('증명 안 된 것 없음')} ${c.gray('— 바꾼 것마다 그 뒤에 돌린 확인이 있습니다.')}`);
|
|
1506
|
+
}
|
|
1507
|
+
say('');
|
|
1508
|
+
|
|
1509
|
+
if (/^(파일|file|저장|save)$/i.test(말)) {
|
|
1510
|
+
const 이름 = `${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '')}`;
|
|
1511
|
+
const 자리 = 증거적기(session.root, e, 이름);
|
|
1512
|
+
if (자리) say(` ${mark.ok} ${c.gray('남겼습니다 —')} ${c.cyan(자리)}`);
|
|
1513
|
+
else say(` ${c.gray('파일로 못 남겼습니다. 화면 것만 쓰세요.')}`);
|
|
1514
|
+
say('');
|
|
1515
|
+
} else {
|
|
1516
|
+
say(` ${c.gray('파일로 남기려면')} ${c.cyan('/evidence 파일')}`);
|
|
1517
|
+
say('');
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* 모델 카드 — 겪어 본 버릇과, 그 때문에 deel 이 바꾼 것.
|
|
1523
|
+
*
|
|
1524
|
+
* 여기서 중요한 것은 아래쪽 '그래서 바꾼 것' 이다. 위쪽 숫자만 보여 주면
|
|
1525
|
+
* 그냥 통계지만, 무엇이 달라졌는지까지 보여야 사람이 판단할 수 있다 —
|
|
1526
|
+
* "이 모델을 계속 쓸까, 다른 걸 받을까" 가 실제로 묻는 것이다.
|
|
1527
|
+
*/
|
|
1528
|
+
function 카드명령(session, ctx) {
|
|
1529
|
+
const 장 = ctx?.카드다시?.() ?? ctx?.카드 ?? null;
|
|
1530
|
+
say('');
|
|
1531
|
+
if (!장) {
|
|
1532
|
+
say(` ${c.gray('이 자리에서는 못 봅니다 — 대화 화면에서만 됩니다.')}`);
|
|
1533
|
+
say('');
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
rule('모델 카드', 70);
|
|
1538
|
+
say(` ${c.bold(장.모델 || '(이름 없음)')} ${c.gray(`· 같이 걸어 본 걸음 ${장.걸음}`)}`);
|
|
1539
|
+
/*
|
|
1540
|
+
* 미리 아는 모델이면 이름표 한 줄 (agent/preset.js). 겪어서 안 것이 아니라
|
|
1541
|
+
* 공개 문서로 아는 것이라, '겪어 본 버릇' 과 섞이지 않게 따로 선다.
|
|
1542
|
+
*/
|
|
1543
|
+
if (장.내장) {
|
|
1544
|
+
const 표지 = 언어() === 'en' ? 'known model' : '아는 모델';
|
|
1545
|
+
say(` ${c.hcyan('◆')} ${c.bold(장.내장.이름)} ${c.gray(`· ${표지}`)}`);
|
|
1546
|
+
if (장.내장.한줄) say(` ${c.gray(장.내장.한줄)}`);
|
|
1547
|
+
}
|
|
1548
|
+
say('');
|
|
1549
|
+
|
|
1550
|
+
const 줄 = (이름, v) => {
|
|
1551
|
+
if (!v?.n) return;
|
|
1552
|
+
const 센가 = v.율 >= 0.15;
|
|
1553
|
+
const 표 = 센가 ? c.yellow('●') : c.gray('○');
|
|
1554
|
+
say(` ${표} ${pad(이름, 18)} ${pad(String(v.n), 5, 'right')} ${c.gray(`(${Math.round(v.율 * 100)}%)`)}`);
|
|
1555
|
+
};
|
|
1556
|
+
say(` ${c.bold('겪어 본 버릇')}`);
|
|
1557
|
+
줄('인자가 잘림', 장.버릇.잘린인자);
|
|
1558
|
+
줄('빈 답', 장.버릇.빈답);
|
|
1559
|
+
줄('편집이 빗나감', 장.버릇.편집실패);
|
|
1560
|
+
줄('같은 것 되풀이', 장.버릇.되풀이);
|
|
1561
|
+
if (!Object.values(장.버릇).some((v) => v.n)) say(` ${c.gray('아직 걸린 것이 없습니다.')}`);
|
|
1562
|
+
if (장.보정 && Math.abs(장.보정 - 1) > 0.01) {
|
|
1563
|
+
say(` ${c.gray(pad('토큰 추정 보정', 18))} ${pad(`×${장.보정.toFixed(2)}`, 5, 'right')}`);
|
|
1564
|
+
}
|
|
1565
|
+
say('');
|
|
1566
|
+
|
|
1567
|
+
say(` ${c.bold('그래서 바꾼 것')}`);
|
|
1568
|
+
if (장.아직모름) {
|
|
1569
|
+
say(` ${c.gray('아직 판단하지 않습니다 — 몇 걸음 안 걸어 보고 바꾸면 멀쩡한 모델을 붙들어 맵니다.')}`);
|
|
1570
|
+
} else if (!장.왜.length) {
|
|
1571
|
+
say(` ${c.gray('바꾼 것 없음. 이 모델은 그대로 두어도 괜찮습니다.')}`);
|
|
1572
|
+
} else {
|
|
1573
|
+
for (const w of 장.왜) say(` ${c.hcyan('→')} ${w}`);
|
|
1574
|
+
}
|
|
1575
|
+
say('');
|
|
1576
|
+
say(` ${c.gray('이 카드는 프롬프트에 안 실립니다 — deel 이 제 행동을 바꾸는 것이라 모델에게 말할 필요가 없습니다.')}`);
|
|
1577
|
+
say('');
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* 못 박기.
|
|
1582
|
+
*
|
|
1583
|
+
* 여기서 하는 일은 화면과 말뿐이다. 들고 있는 것은 agent/pins.js 이고,
|
|
1584
|
+
* 그것이 session 에 붙어 있어서 접기·요약이 닿지 못한다.
|
|
1585
|
+
*/
|
|
1586
|
+
function 못박기명령(session, ctx, arg = '') {
|
|
1587
|
+
const 못 = session?.못박은것;
|
|
1588
|
+
say('');
|
|
1589
|
+
if (!못) {
|
|
1590
|
+
say(` ${c.gray('이 자리에서는 못 씁니다 — 대화 화면에서만 됩니다.')}`);
|
|
1591
|
+
say('');
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
const 적어두기 = () => { try { ctx?.갈래?.현재store?.()?.못박기목록(못.직렬화()); } catch { /* 못 적어도 대화는 계속된다 */ } };
|
|
1596
|
+
const 말 = String(arg ?? '').trim();
|
|
1597
|
+
const [머리, ...나머지] = 말.split(/\s+/);
|
|
1598
|
+
|
|
1599
|
+
if (/^(지우기|빼기|clear|rm|remove)$/i.test(머리 ?? '')) {
|
|
1600
|
+
const r = 못.지우기(나머지.join(' ') || '전부');
|
|
1601
|
+
if (!r.ok) say(` ${c.red(r.why)}`);
|
|
1602
|
+
else if (typeof r.뺀것 === 'number') say(` ${mark.ok} ${c.gray(`못 박아 둔 것 ${r.뺀것}개를 뺐습니다.`)}`);
|
|
1603
|
+
else say(` ${mark.ok} ${c.gray('뺐습니다 —')} ${r.뺀것}`);
|
|
1604
|
+
적어두기();
|
|
1605
|
+
say('');
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (말) {
|
|
1610
|
+
const r = 못.더하기(말);
|
|
1611
|
+
if (!r.ok) {
|
|
1612
|
+
say(` ${c.red(r.why)}`);
|
|
1613
|
+
say('');
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
적어두기();
|
|
1617
|
+
say(` ${c.hcyan('📌')} ${c.bold(`${r.번호}.`)} ${말}`);
|
|
1618
|
+
say(` ${c.gray('접거나 요약해도 안 지워집니다. 빼려면')} ${c.cyan(`/pin 지우기 ${r.번호}`)}`);
|
|
1619
|
+
say('');
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
const 목록 = 못.목록();
|
|
1624
|
+
rule('못 박은 것', 70);
|
|
1625
|
+
if (!목록.length) {
|
|
1626
|
+
say(` ${c.gray('아직 없습니다. 대화 내내 지켜야 할 말을 박아 두세요.')}`);
|
|
1627
|
+
say(` ${c.gray('예:')} ${c.cyan('/pin 운영 DB 는 건드리지 마라')}`);
|
|
1628
|
+
say('');
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
for (const x of 목록) say(` ${c.hcyan(`${x.번호}.`)} ${x.말}`);
|
|
1632
|
+
say('');
|
|
1633
|
+
const 실린것 = 못.실린것();
|
|
1634
|
+
say(` ${c.gray(`매 턴 프롬프트에 실립니다 — 지금 ${실린것.개수}개, ${실린것.토큰}토큰.`)}`);
|
|
1635
|
+
if (!실린것.다실렸나) {
|
|
1636
|
+
say(` ${c.yellow(`자리가 모자라 뒤의 ${목록.length - 실린것.개수}개는 안 실립니다.`)} ${c.gray('짧게 줄이거나 빼세요.')}`);
|
|
1637
|
+
}
|
|
1638
|
+
say('');
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1078
1641
|
function 배움명령(session, ctx, arg = '') {
|
|
1079
1642
|
const 배움 = ctx?.배움;
|
|
1080
1643
|
say('');
|
|
@@ -1743,8 +2306,9 @@ function showWork(session) {
|
|
|
1743
2306
|
const w = WORK_MODES[k];
|
|
1744
2307
|
const 지금 = k === (normWork(session.work) ?? WORK_DEFAULT);
|
|
1745
2308
|
const 표 = 지금 ? c.hgreen('●') : c.gray('·');
|
|
1746
|
-
const
|
|
1747
|
-
|
|
2309
|
+
const 보임 = 보일이름(w.id);
|
|
2310
|
+
const 이름 = 지금 ? c.bold(c.white(pad(보임, 8))) : c.gray(pad(보임, 8));
|
|
2311
|
+
say(` ${표} ${c.hcyan(w.glyph)} ${이름}${c.gray(pad(w.en, 14))}${c.gray(보일한줄(w.id))}`);
|
|
1748
2312
|
say(` ${canWrite(k) ? c.gray('파일 바꿈') : c.green('읽기만')}${c.gray(' · 생각 ' + (w.think ?? '그대로') + '·' + w.effort + ' · 최대 ' + w.steps + '걸음')}`);
|
|
1749
2313
|
}
|
|
1750
2314
|
say('');
|