deel-local-cli 1.1.1 → 1.4.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 +826 -19
- package/README.md +689 -14
- package/bin/deel.js +73 -4
- package/package.json +7 -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/budget.js +0 -14
- package/src/agent/card.js +110 -0
- package/src/agent/compact.js +95 -0
- package/src/agent/evidence.js +186 -0
- package/src/agent/evolve.js +213 -0
- package/src/agent/grade.js +20 -0
- package/src/agent/loop.js +172 -14
- package/src/agent/models.js +169 -0
- package/src/agent/modes.js +49 -1
- package/src/agent/pins.js +140 -0
- package/src/agent/project.js +10 -4
- package/src/agent/session.js +359 -11
- package/src/agent/store.js +31 -0
- package/src/agent/threads.js +154 -0
- package/src/commands.js +751 -55
- package/src/config.js +10 -1
- package/src/i18n/en.js +244 -0
- package/src/i18n/index.js +126 -0
- package/src/i18n/ko.js +231 -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 +23 -0
- package/src/pack/sbom.js +218 -0
- package/src/pack/selfpack.js +19 -3
- package/src/repl.js +273 -22
- package/src/safety/guard.js +37 -2
- package/src/safety/secrets.js +205 -0
- package/src/safety/undo.js +11 -4
- package/src/setup.js +2 -2
- package/src/skills/discover.js +1 -1
- package/src/tools/desc.en.js +219 -0
- package/src/tools/edit-match.js +16 -5
- package/src/tools/excel-com.js +1 -1
- package/src/tools/excel.js +1 -1
- package/src/tools/fsutil.js +0 -8
- package/src/tools/index.js +149 -6
- package/src/tools/lsp.js +327 -0
- package/src/tools/task.js +30 -2
- package/src/tools/todo.js +19 -3
- package/src/ui/ansi.js +42 -2
- package/src/ui/approve.js +25 -21
- package/src/ui/inputbox.js +69 -13
- package/src/ui/intro.js +174 -0
- package/src/ui/level.js +11 -5
- package/src/ui/md.js +227 -0
- package/src/ui/notify.js +101 -0
- package/src/ui/prompt.js +1 -1
- package/src/ui/screen.js +1 -1
- package/src/ui/status.js +190 -34
- package/src/ui/working.js +0 -3
- package/src/ui/wrap.js +1 -8
package/src/tools/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 도구. 이름과 인자를 Claude Code 와 같게 맞춘다 —
|
|
2
2
|
// 그래야 그 관례로 쓰인 스킬·명령이 그대로 먹는다.
|
|
3
3
|
import { writeFileSync, appendFileSync, readFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
|
-
import { globToRegex, walk, readText, readTextFull,
|
|
6
|
+
import { globToRegex, walk, readText, readTextFull, 내부살림 } from './fsutil.js';
|
|
7
7
|
import { encode, label as encLabel, decode as decodeBytes, consoleCodepage, looksBinary } from './encoding.js';
|
|
8
8
|
import { checkCommand, checkPaths, isMutating } from '../safety/guard.js';
|
|
9
9
|
import { 띄우기, JOBS_TOOL } from './jobs.js';
|
|
@@ -14,11 +14,16 @@ import { TODO_TOOL } from './todo.js';
|
|
|
14
14
|
import { TASK_TOOL } from './task.js';
|
|
15
15
|
import { OUTLINE_TOOL } from './outline.js';
|
|
16
16
|
import { VERIFY_TOOL } from './verify.js';
|
|
17
|
+
import { DEF_TOOL, REFS_TOOL } from './lsp.js';
|
|
18
|
+
import { 편집후진단, 붙이기 as 진단붙이기, 데우기 } from '../lsp/diag.js';
|
|
19
|
+
import { 프로젝트갈래 } from '../lsp/servers.js';
|
|
17
20
|
import { allow as allowedIn } from '../agent/modes.js';
|
|
18
21
|
import { 도구정의, 이름풀기 } from '../backend/mcp.js';
|
|
19
22
|
import { isExcelPath, readExcel, toText as excelText, summarize as excelSummary } from './excel.js';
|
|
20
23
|
import { diffLines } from '../ui/diff.js';
|
|
21
24
|
import { 읽을줄수, 찾을개수, 찾을줄수, 설명길이 } from '../agent/budget.js';
|
|
25
|
+
import { 도구설명EN } from './desc.en.js';
|
|
26
|
+
import { 언어 } from '../i18n/index.js';
|
|
22
27
|
|
|
23
28
|
/*
|
|
24
29
|
* 한 번에 돌려줄 양은 **모델에 맞춰** 정한다 (agent/budget.js).
|
|
@@ -180,6 +185,26 @@ function 줄수(abs, 인코딩) {
|
|
|
180
185
|
} catch { return 0; }
|
|
181
186
|
}
|
|
182
187
|
|
|
188
|
+
/**
|
|
189
|
+
* 그 줄과 앞뒤 몇 줄. Edit 이 빗나갔을 때 보여 줄 것.
|
|
190
|
+
*
|
|
191
|
+
* 줄 번호를 같이 붙인다 — 모델이 "몇 번 줄" 로 세어 다시 잡을 수 있어야 한다.
|
|
192
|
+
* 한 줄만 보여 주던 것을 넓힐 때, 넓힌 만큼 자리를 먹으니 짧게 자른다.
|
|
193
|
+
*/
|
|
194
|
+
function 둘레(text, 줄번호, 보일줄 = 1) {
|
|
195
|
+
const 줄들 = String(text ?? '').split('\n');
|
|
196
|
+
const 가운데 = Math.max(1, Number(줄번호) || 1);
|
|
197
|
+
const 반 = Math.floor(Math.max(1, 보일줄) / 2);
|
|
198
|
+
const 처음 = Math.max(1, 가운데 - 반);
|
|
199
|
+
const 끝 = Math.min(줄들.length, 처음 + Math.max(1, 보일줄) - 1);
|
|
200
|
+
const out = [];
|
|
201
|
+
for (let i = 처음; i <= 끝; i++) {
|
|
202
|
+
const 표 = i === 가운데 ? '→' : ' ';
|
|
203
|
+
out.push(` ${표} ${String(i).padStart(5)} | ${줄들[i - 1].slice(0, 120)}`);
|
|
204
|
+
}
|
|
205
|
+
return out.join('\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
183
208
|
function 엑셀은못고침(보인이름) {
|
|
184
209
|
return `엑셀 파일은 이 도구로 고칠 수 없습니다: ${보인이름}\n`
|
|
185
210
|
+ ' 읽기만 됩니다 (CSV 로 바꿔서 보여줍니다). 서식·수식·차트가 든 파일을\n'
|
|
@@ -335,8 +360,16 @@ function 한군데고치기(args, ctx) {
|
|
|
335
360
|
if (m.reason === 'ambiguous') {
|
|
336
361
|
return { error: `${m.count}군데에서 발견됐습니다 (${TIER_LABELS[m.tier]}). 앞뒤로 더 넓게 잡아 하나만 가리키거나 replace_all 을 쓰세요.` };
|
|
337
362
|
}
|
|
363
|
+
/*
|
|
364
|
+
* 비슷한 자리를 몇 줄이나 보여 줄까는 **이 모델을 겪어 본 만큼** 정한다
|
|
365
|
+
* (agent/card.js). Edit 이 자주 빗나가는 모델에는 한 줄만 보여 줘 봐야
|
|
366
|
+
* 다음 시도도 빗나간다 — 앞뒤를 같이 보여 주면 옮겨 담을 것이 분명해진다.
|
|
367
|
+
* 겪은 것이 모자라면 여태처럼 한 줄이다. 자리를 괜히 먹지 않는다.
|
|
368
|
+
*/
|
|
369
|
+
const 보일줄 = Math.max(1, ctx?.카드?.조정?.빗나갔을때보일줄 ?? 1);
|
|
338
370
|
const hint = m.near
|
|
339
|
-
? `\n 파일의 ${m.near.line}번 줄이 가장 비슷합니다:\n
|
|
371
|
+
? `\n 파일의 ${m.near.line}번 줄이 가장 비슷합니다:\n${둘레(text, m.near.line, 보일줄)}`
|
|
372
|
+
+ '\n 이 줄을 그대로 옮겨 담아 다시 시도하세요.'
|
|
340
373
|
: '\n Read 로 다시 읽어 실제 내용을 확인하세요.';
|
|
341
374
|
return { error: `찾지 못했습니다.${hint}` };
|
|
342
375
|
}
|
|
@@ -1111,6 +1144,16 @@ export const TOOLS = {
|
|
|
1111
1144
|
|
|
1112
1145
|
// 뒤에서 도는 명령 보기·끝내기. Bash(background) 와 짝이다 — jobs.js 머리말 참고.
|
|
1113
1146
|
Jobs: JOBS_TOOL,
|
|
1147
|
+
|
|
1148
|
+
/*
|
|
1149
|
+
* 언어 서버에게 묻는 둘. Grep·Outline 을 밀어내지 않고 **더한다** —
|
|
1150
|
+
* tools/lsp.js 머리말 참고.
|
|
1151
|
+
*
|
|
1152
|
+
* 언어 서버가 이 자리에 없으면 toolSchemas 가 목록에서 뺀다. 못 쓰는 도구를
|
|
1153
|
+
* 세워 두면 모델은 그걸 부르고, 실패를 받고, 또 부른다.
|
|
1154
|
+
*/
|
|
1155
|
+
Def: DEF_TOOL,
|
|
1156
|
+
Refs: REFS_TOOL,
|
|
1114
1157
|
};
|
|
1115
1158
|
|
|
1116
1159
|
/**
|
|
@@ -1181,14 +1224,49 @@ export function 설명줄이기(schema, 한도) {
|
|
|
1181
1224
|
};
|
|
1182
1225
|
}
|
|
1183
1226
|
|
|
1227
|
+
/**
|
|
1228
|
+
* 도구 설명을 지금 화면 말에 맞춘다.
|
|
1229
|
+
*
|
|
1230
|
+
* 표에 없는 도구·인자는 한글 설명이 그대로 나간다 — 화면 말과 같은 규칙이다.
|
|
1231
|
+
* 빈 설명을 내보내지 않는다. 설명 없는 도구는 모델이 언제 쓰는지 모른 채로
|
|
1232
|
+
* 목록에만 서 있게 되는데, 그건 없는 것보다 나쁘다.
|
|
1233
|
+
*/
|
|
1234
|
+
function 영어설명(schema, 이름) {
|
|
1235
|
+
if (언어() !== 'en') return schema;
|
|
1236
|
+
const 것 = 도구설명EN[이름];
|
|
1237
|
+
if (!것) return schema;
|
|
1238
|
+
|
|
1239
|
+
const p = schema.parameters ?? {};
|
|
1240
|
+
const 새속성 = {};
|
|
1241
|
+
for (const [인자, 값] of Object.entries(p.properties ?? {})) {
|
|
1242
|
+
const 글 = 것.params?.[인자];
|
|
1243
|
+
새속성[인자] = 글 ? { ...값, description: 글 } : 값;
|
|
1244
|
+
}
|
|
1245
|
+
return {
|
|
1246
|
+
...schema,
|
|
1247
|
+
description: 것.desc ?? schema.description,
|
|
1248
|
+
parameters: { ...p, properties: 새속성 },
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1184
1252
|
// 모델에게 넘길 도구 정의 목록.
|
|
1185
1253
|
// 스킬이 없으면 Skill 도구는 빼서 자리를 아낀다.
|
|
1186
|
-
export function toolSchemas(names = null, { hasSkills = false, web = true, work = null, mcp = null, ctx = null } = {}) {
|
|
1254
|
+
export function toolSchemas(names = null, { hasSkills = false, web = true, work = null, mcp = null, ctx = null, lsp = false } = {}) {
|
|
1187
1255
|
let list = names ?? Object.keys(TOOLS).filter((n) => {
|
|
1188
1256
|
if (n === 'Skill') return hasSkills;
|
|
1189
1257
|
if (n === 'WebFetch') return web;
|
|
1258
|
+
// 언어 서버가 없는 자리에서는 Def·Refs 를 아예 안 보여 준다.
|
|
1259
|
+
//
|
|
1260
|
+
// 웹 도구를 오프라인에서 숨기는 것과 같은 이유다. 못 쓰는 도구를 목록에
|
|
1261
|
+
// 세워 두면 모델은 그걸 부르고, "없습니다" 를 받고, 또 부른다. 그 왕복이
|
|
1262
|
+
// 도구 설명으로 나가는 자리보다 비싸다.
|
|
1263
|
+
if (n === 'Def' || n === 'Refs') return !!lsp;
|
|
1190
1264
|
return true;
|
|
1191
1265
|
});
|
|
1266
|
+
// 이름을 직접 준 경우(하위 작업이 부모 것을 물려받을 때)에도 같은 규칙을 건다.
|
|
1267
|
+
// 부모에게 있던 것이 하위에서 갑자기 못 쓰게 되지는 않지만, 시험·일회성 호출이
|
|
1268
|
+
// 이름을 통째로 넘기는 길이 있어서 여기서 한 겹 더 막는다.
|
|
1269
|
+
if (names && !lsp) list = list.filter((n) => n !== 'Def' && n !== 'Refs');
|
|
1192
1270
|
// 작업 모드가 정해져 있으면 그 모드가 쓰는 것만 남긴다.
|
|
1193
1271
|
//
|
|
1194
1272
|
// 설계·계획·묻기 모드에서 파일을 바꾸면 안 된다고 프롬프트로 부탁할 수도 있다.
|
|
@@ -1202,7 +1280,20 @@ export function toolSchemas(names = null, { hasSkills = false, web = true, work
|
|
|
1202
1280
|
* 이름과 인자는 그대로 남으므로 할 수 있는 일은 똑같다.
|
|
1203
1281
|
*/
|
|
1204
1282
|
const 한도 = 설명길이(ctx);
|
|
1205
|
-
|
|
1283
|
+
/*
|
|
1284
|
+
* 화면 말이 영어면 도구 설명도 영어로 갈아 끼운다 (tools/desc.en.js).
|
|
1285
|
+
*
|
|
1286
|
+
* 줄이기 **전에** 갈아 끼운다. 순서가 반대면 한글 설명을 한도에 맞춰 자른
|
|
1287
|
+
* 다음 영어로 통째로 바꾸는 셈이라, 자른 것이 아무 뜻이 없어지고 영어 글은
|
|
1288
|
+
* 한도를 넘긴 채로 실린다.
|
|
1289
|
+
*
|
|
1290
|
+
* 이름과 인자 이름은 안 건드린다 — 그건 식별자다. Task 의 목적·할일처럼
|
|
1291
|
+
* 한글로 된 인자 이름을 바꾸면 그 도구가 아예 안 불린다.
|
|
1292
|
+
*/
|
|
1293
|
+
const 우리것 = list.map((n) => ({
|
|
1294
|
+
type: 'function',
|
|
1295
|
+
function: 설명줄이기(영어설명(TOOLS[n].schema, n), 한도),
|
|
1296
|
+
}));
|
|
1206
1297
|
|
|
1207
1298
|
/*
|
|
1208
1299
|
* 밖에서 붙인 도구(MCP)를 뒤에 붙인다.
|
|
@@ -1215,6 +1306,58 @@ export function toolSchemas(names = null, { hasSkills = false, web = true, work
|
|
|
1215
1306
|
return 우리것;
|
|
1216
1307
|
}
|
|
1217
1308
|
|
|
1309
|
+
// 파일을 바꾸는 도구들. 이것만 고친 뒤 진단을 본다.
|
|
1310
|
+
const 고치는도구 = new Set(['Write', 'Append', 'Edit']);
|
|
1311
|
+
// 한 번에 볼 파일 수. 여덟 개를 한꺼번에 만들었다고 여덟 번 기다릴 수는 없다.
|
|
1312
|
+
const 진단볼파일 = 3;
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* 고친 직후에 그 파일이 성한지 본다 — lsp/diag.js 머리말 참고.
|
|
1316
|
+
*
|
|
1317
|
+
* 여기서 절대 죽으면 안 되고, 늦어서도 안 된다. 파일은 이미 고쳐졌다.
|
|
1318
|
+
* 진단은 **덤**이지 이 도구가 성공했는지의 판단 근거가 아니다. 그래서
|
|
1319
|
+
* 통째로 감싸고, 이미 떠 있는 서버가 없으면 아무것도 안 하고 그냥 지나간다.
|
|
1320
|
+
*/
|
|
1321
|
+
async function 고친뒤진단(name, r, ctx) {
|
|
1322
|
+
try {
|
|
1323
|
+
if (!고치는도구.has(name) || !r || r.error) return r;
|
|
1324
|
+
/*
|
|
1325
|
+
* **부른 쪽이 켠 자리에서만** 한다. 기본은 꺼짐이다.
|
|
1326
|
+
*
|
|
1327
|
+
* 여기서 하는 일은 남의 프로세스를 하나 띄우는 것이다. 그건 띄운 쪽이
|
|
1328
|
+
* 거둘 줄 알아야 한다 — 안 거두면 그 서버가 작업 폴더를 cwd 로 물고 있어서
|
|
1329
|
+
* 윈도우에서는 폴더 이름조차 못 바꾼다. 그래서 끄는 자리를 갖춘 쪽(repl·
|
|
1330
|
+
* oneshot)만 ctx.lsp.켬 을 켠다. 도구를 직접 부르는 자리는 안 켜진다.
|
|
1331
|
+
*/
|
|
1332
|
+
if (ctx.lsp?.켬 !== true) return r;
|
|
1333
|
+
const 뿌리 = ctx.scope?.root;
|
|
1334
|
+
if (!뿌리) return r;
|
|
1335
|
+
|
|
1336
|
+
const 바뀐 = r.changed
|
|
1337
|
+
? [r.changed]
|
|
1338
|
+
: (Array.isArray(r.여럿) ? r.여럿.filter((x) => x?.ok && x.path).map((x) => x.path) : []);
|
|
1339
|
+
const 볼것 = [...new Set(바뀐)].slice(0, 진단볼파일);
|
|
1340
|
+
if (!볼것.length) return r;
|
|
1341
|
+
|
|
1342
|
+
// 처음 고칠 때 뒤에서 하나 데워 둔다. 이번 것은 못 받아도 다음부터 받는다.
|
|
1343
|
+
데우기(뿌리, 볼것[0]);
|
|
1344
|
+
|
|
1345
|
+
const 것들 = await Promise.all(볼것.map((abs) => 편집후진단(뿌리, abs)));
|
|
1346
|
+
let 답 = r;
|
|
1347
|
+
for (let i = 0; i < 볼것.length; i++) {
|
|
1348
|
+
답 = 진단붙이기(답, 것들[i], ctx.scope.show(볼것[i]));
|
|
1349
|
+
}
|
|
1350
|
+
return 답;
|
|
1351
|
+
} catch {
|
|
1352
|
+
return r; // 진단 보다 터져서 편집이 실패로 보이는 일은 없어야 한다
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/** 이 폴더에서 언어 서버를 쓸 수 있나. repl 이 켤 때 한 번 물어본다. */
|
|
1357
|
+
export function 언어서버있나(뿌리) {
|
|
1358
|
+
try { return !!프로젝트갈래(뿌리); } catch { return false; }
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1218
1361
|
export async function runTool(name, args, ctx) {
|
|
1219
1362
|
// 밖에서 붙인 도구(MCP)는 이름 앞머리로 갈린다.
|
|
1220
1363
|
//
|
|
@@ -1228,7 +1371,7 @@ export async function runTool(name, args, ctx) {
|
|
|
1228
1371
|
try {
|
|
1229
1372
|
const r = await t.run(args ?? {}, ctx);
|
|
1230
1373
|
ctx.audit.tool(name, args, r);
|
|
1231
|
-
return r;
|
|
1374
|
+
return await 고친뒤진단(name, r, ctx);
|
|
1232
1375
|
} catch (err) {
|
|
1233
1376
|
const r = { error: err.message };
|
|
1234
1377
|
ctx.audit.tool(name, args, r);
|
package/src/tools/lsp.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Def · Refs — 언어 서버에게 "이게 어디 있나 / 어디서 쓰나" 를 묻는다.
|
|
3
|
+
*
|
|
4
|
+
* ── Grep 을 밀어내는 것이 아니다 ────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Grep 은 남는다. 언어 서버가 안 깔린 자리가 더 많고(사내망이 대개 그렇다),
|
|
7
|
+
* 깔려 있어도 못 읽는 파일이 있고, 무엇보다 Grep 은 **주석·설정·문서까지**
|
|
8
|
+
* 찾는다. 이름을 바꿀 때 정말 필요한 것은 그쪽까지다.
|
|
9
|
+
*
|
|
10
|
+
* 이 둘이 더해 주는 것은 딱 하나, **틀린 자리를 안 준다는 것**이다.
|
|
11
|
+
* `run` 을 Grep 으로 찾으면 수백 줄이 나오고 그중 진짜는 몇 개다. 모델은 그
|
|
12
|
+
* 수백 줄을 다 읽을 자리가 없어서 앞의 몇 개만 보고 고치기 시작한다. 놓친
|
|
13
|
+
* 자리는 돌려 본 뒤에야 드러나고, 그때는 이미 다른 것도 같이 고쳐 놓은 뒤다.
|
|
14
|
+
*
|
|
15
|
+
* ── 왜 자리(줄·칸)가 아니라 이름을 받나 ─────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* LSP 는 "이 파일 이 줄 이 칸에 있는 것" 을 묻는 규약이다. 그런데 모델은
|
|
18
|
+
* 칸 번호를 모른다. 알려면 파일을 먼저 Read 해야 하는데, 그러면 이 도구를
|
|
19
|
+
* 쓰는 값(파일을 안 읽고도 안다)이 통째로 사라진다.
|
|
20
|
+
*
|
|
21
|
+
* 그래서 이름을 받아 **workspace/symbol 로 자리를 먼저 찾고**, 그 자리로
|
|
22
|
+
* 다시 묻는다. 사람이 하는 것과 같은 순서다. 이름이 여럿이면 그 목록을
|
|
23
|
+
* 그대로 보여 주고 고르게 한다 — 하나를 골라 주고 아닌 척하지 않는다.
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
|
+
import { 얻기, 색인중일까 } from '../lsp/client.js';
|
|
28
|
+
import { 갈래, 프로젝트갈래 } from '../lsp/servers.js';
|
|
29
|
+
import { 찾을개수 } from '../agent/budget.js';
|
|
30
|
+
|
|
31
|
+
/** 한 자리를 사람이 읽을 한 줄로. 그 줄의 글까지 붙여야 열어 보지 않고도 안다. */
|
|
32
|
+
function 한줄(scope, uri, 범위) {
|
|
33
|
+
let abs;
|
|
34
|
+
try { abs = fileURLToPath(uri); } catch { abs = String(uri); }
|
|
35
|
+
const 줄번호 = (범위?.start?.line ?? 0) + 1;
|
|
36
|
+
let 글 = '';
|
|
37
|
+
try {
|
|
38
|
+
const 줄들 = readFileSync(abs, 'utf8').split(/\r?\n/);
|
|
39
|
+
글 = (줄들[줄번호 - 1] ?? '').trim();
|
|
40
|
+
} catch { /* 못 읽으면 자리만 준다 */ }
|
|
41
|
+
const 보일 = scope?.show ? (() => { try { return scope.show(abs); } catch { return abs; } })() : abs;
|
|
42
|
+
return { 파일: 보일, 줄: 줄번호, 글: 글.length > 160 ? 글.slice(0, 160) + '…' : 글, abs };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** LSP 의 답은 하나일 수도, 목록일 수도, LocationLink 일 수도 있다. 다 같은 모양으로 편다. */
|
|
46
|
+
function 자리들펴기(값) {
|
|
47
|
+
if (!값) return [];
|
|
48
|
+
const 목록 = Array.isArray(값) ? 값 : [값];
|
|
49
|
+
return 목록.map((it) => {
|
|
50
|
+
if (!it) return null;
|
|
51
|
+
if (it.targetUri) return { uri: it.targetUri, range: it.targetSelectionRange ?? it.targetRange };
|
|
52
|
+
if (it.uri) return { uri: it.uri, range: it.range };
|
|
53
|
+
if (it.location) return { uri: it.location.uri, range: it.location.range };
|
|
54
|
+
return null;
|
|
55
|
+
}).filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 이름이 그 줄 어디쯤에 있는지.
|
|
60
|
+
*
|
|
61
|
+
* 낱말 경계를 본다. `run` 을 찾을 때 `runner` 를 짚으면 서버는 runner 의
|
|
62
|
+
* 정의를 준다 — 틀린 답인데 맞는 답처럼 생겨서 제일 나쁘다.
|
|
63
|
+
*/
|
|
64
|
+
function 칸찾기(줄글, 이름) {
|
|
65
|
+
if (!줄글 || !이름) return -1;
|
|
66
|
+
let i = 0;
|
|
67
|
+
for (;;) {
|
|
68
|
+
const p = 줄글.indexOf(이름, i);
|
|
69
|
+
if (p < 0) return -1;
|
|
70
|
+
const 앞 = 줄글[p - 1] ?? ' ';
|
|
71
|
+
const 뒤 = 줄글[p + 이름.length] ?? ' ';
|
|
72
|
+
const 낱말 = (ch) => /[\p{L}\p{N}_$]/u.test(ch);
|
|
73
|
+
if (!낱말(앞) && !낱말(뒤)) return p;
|
|
74
|
+
i = p + 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 이름으로 자리를 찾는다.
|
|
80
|
+
*
|
|
81
|
+
* @returns {{자리: {uri, position}, 후보: object[]}|{오류: string}}
|
|
82
|
+
*/
|
|
83
|
+
async function 자리잡기(서버, scope, { 이름, 파일, 줄 }) {
|
|
84
|
+
// 1) 파일과 줄을 준 경우. 그게 제일 정확하다 — 모델이 방금 Read 했거나
|
|
85
|
+
// Grep 으로 좁혀 온 자리다.
|
|
86
|
+
if (파일) {
|
|
87
|
+
let abs;
|
|
88
|
+
try { abs = scope.resolve(파일); } catch (e) { return { 오류: e.message }; }
|
|
89
|
+
let 줄들;
|
|
90
|
+
try { 줄들 = readFileSync(abs, 'utf8').split(/\r?\n/); } catch { return { 오류: `못 읽었습니다: ${파일}` }; }
|
|
91
|
+
서버.보여주기(abs, 줄들.join('\n'));
|
|
92
|
+
|
|
93
|
+
const 볼줄 = Number.isFinite(줄) && 줄 > 0 ? [줄 - 1] : 줄들.map((_, i) => i);
|
|
94
|
+
for (const i of 볼줄) {
|
|
95
|
+
const 칸 = 칸찾기(줄들[i] ?? '', 이름);
|
|
96
|
+
if (칸 >= 0) return { 자리: { uri: pathToFileURL(abs).href, position: { line: i, character: 칸 } } };
|
|
97
|
+
}
|
|
98
|
+
return { 오류: `${파일}${Number.isFinite(줄) ? `:${줄}` : ''} 에서 ${이름} 을 못 찾았습니다` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* 2) 이름만 준 경우. 프로젝트 전체에서 그 이름을 찾는다.
|
|
103
|
+
*
|
|
104
|
+
* 방금 켠 서버는 빈손으로 답한다. 없어서가 아니라 아직 프로젝트를 다 못
|
|
105
|
+
* 훑어서다 — 악수는 몇십 ms 면 끝나지만 색인은 몇 초씩 걸린다. 실제로 켠 지
|
|
106
|
+
* 0.2초 만에 물었더니 없다고 했고, 0.5초 뒤에 물으니 나왔다.
|
|
107
|
+
*
|
|
108
|
+
* 이 둘을 구별 안 하면 "그런 이름 없습니다" 가 되고, 모델은 그 말을 믿고
|
|
109
|
+
* 이미 있는 것을 새로 만든다. 그래서 **켠 지 얼마 안 됐을 때만** 몇 번 더
|
|
110
|
+
* 물어본다. 오래 돈 서버에서 빈손이면 그건 정말 없는 것이라 안 기다린다.
|
|
111
|
+
*/
|
|
112
|
+
let 답 = await 서버.물어보기('workspace/symbol', { query: 이름 });
|
|
113
|
+
for (const 쉼 of [400, 800, 1500]) {
|
|
114
|
+
if (답.오류 || (Array.isArray(답.값) && 답.값.length)) break;
|
|
115
|
+
if (!색인중일까(서버)) break;
|
|
116
|
+
// 이 시계는 unref 하지 않는다. 여기는 도구가 도는 한가운데라,
|
|
117
|
+
// 놔 버리면 기다리는 사이에 프로그램이 그냥 끝나 버린다.
|
|
118
|
+
await new Promise((r) => setTimeout(r, 쉼));
|
|
119
|
+
답 = await 서버.물어보기('workspace/symbol', { query: 이름 });
|
|
120
|
+
}
|
|
121
|
+
if (답.오류) return { 오류: 답.오류 };
|
|
122
|
+
const 것들 = (Array.isArray(답.값) ? 답.값 : []).filter((s) => s?.name);
|
|
123
|
+
// 이름이 똑같은 것만. 서버는 대개 부분 일치까지 준다.
|
|
124
|
+
const 딱맞는 = 것들.filter((s) => s.name === 이름);
|
|
125
|
+
const 쓸것 = 딱맞는.length ? 딱맞는 : 것들;
|
|
126
|
+
if (!쓸것.length) {
|
|
127
|
+
return {
|
|
128
|
+
오류: `${이름} 을(를) 못 찾았습니다`
|
|
129
|
+
+ (색인중일까(서버) ? ' (언어 서버가 아직 프로젝트를 훑는 중일 수 있습니다)' : '')
|
|
130
|
+
+ '. Grep 으로 한 번 더 보세요.',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const 후보 = 쓸것.map((s) => {
|
|
135
|
+
const loc = s.location ?? {};
|
|
136
|
+
return { 이름: s.name, 갈래: s.kind, uri: loc.uri, range: loc.range ?? null, 컨테이너: s.containerName ?? '' };
|
|
137
|
+
}).filter((x) => x.uri);
|
|
138
|
+
if (!후보.length) return { 오류: `${이름} 의 자리를 못 받았습니다` };
|
|
139
|
+
|
|
140
|
+
const 첫 = 후보[0];
|
|
141
|
+
// WorkspaceSymbol 은 range 없이 오기도 한다. 그럼 파일을 열어 직접 짚는다.
|
|
142
|
+
let position = 첫.range?.start;
|
|
143
|
+
if (!position) {
|
|
144
|
+
try {
|
|
145
|
+
const abs = fileURLToPath(첫.uri);
|
|
146
|
+
const 줄들 = readFileSync(abs, 'utf8').split(/\r?\n/);
|
|
147
|
+
for (let i = 0; i < 줄들.length; i++) {
|
|
148
|
+
const 칸 = 칸찾기(줄들[i], 이름);
|
|
149
|
+
if (칸 >= 0) { position = { line: i, character: 칸 }; break; }
|
|
150
|
+
}
|
|
151
|
+
} catch { /* 아래에서 걸린다 */ }
|
|
152
|
+
}
|
|
153
|
+
if (!position) return { 오류: `${이름} 의 자리를 못 짚었습니다` };
|
|
154
|
+
|
|
155
|
+
// 정확히 이름 글자 위를 짚어야 한다. 정의 줄 맨 앞(`export function`)을 짚으면
|
|
156
|
+
// 서버가 아무것도 못 준다.
|
|
157
|
+
try {
|
|
158
|
+
const abs = fileURLToPath(첫.uri);
|
|
159
|
+
const 줄글 = readFileSync(abs, 'utf8').split(/\r?\n/)[position.line] ?? '';
|
|
160
|
+
const 칸 = 칸찾기(줄글, 이름);
|
|
161
|
+
if (칸 >= 0) position = { line: position.line, character: 칸 };
|
|
162
|
+
서버.보여주기(abs);
|
|
163
|
+
} catch { /* 그대로 간다 */ }
|
|
164
|
+
|
|
165
|
+
return { 자리: { uri: 첫.uri, position }, 후보 };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 두 도구가 같은 앞머리를 쓴다 — 서버 얻고 자리 잡는 데까지. */
|
|
169
|
+
async function 채비(args, ctx) {
|
|
170
|
+
const 이름 = String(args.name ?? '').trim();
|
|
171
|
+
if (!이름) return { 오류: 'name 이 비었습니다' };
|
|
172
|
+
|
|
173
|
+
const 파일 = args.file_path ? String(args.file_path) : null;
|
|
174
|
+
/*
|
|
175
|
+
* 어느 언어 서버에게 물을지.
|
|
176
|
+
*
|
|
177
|
+
* 파일을 줬으면 그 파일의 언어다. 안 줬으면 이 폴더에서 제일 많은 언어로
|
|
178
|
+
* 간다 — 모델은 `handleClick 어디 있어` 처럼 이름만 알고 부르는 것이 보통이라,
|
|
179
|
+
* 매번 파일을 요구하면 이 도구를 쓰는 값이 사라진다.
|
|
180
|
+
*/
|
|
181
|
+
const 볼것 = 파일 ?? 프로젝트갈래(ctx.scope.root)?.대표파일 ?? null;
|
|
182
|
+
if (!볼것) return { 오류: '이 폴더에서 쓸 수 있는 언어 서버가 없습니다. Grep · Outline 을 쓰세요.' };
|
|
183
|
+
if (!갈래(볼것)) return { 오류: `언어 서버가 없는 갈래입니다: ${볼것}` };
|
|
184
|
+
|
|
185
|
+
const 서버 = await 얻기(ctx.scope.root, 볼것);
|
|
186
|
+
if (!서버) return { 오류: '언어 서버가 이 자리에 없습니다. Grep · Outline 을 쓰세요.' };
|
|
187
|
+
|
|
188
|
+
const 잡음 = await 자리잡기(서버, ctx.scope, {
|
|
189
|
+
이름, 파일, 줄: Number.isFinite(args.line) ? Number(args.line) : null,
|
|
190
|
+
});
|
|
191
|
+
if (잡음.오류) return { 오류: 잡음.오류 };
|
|
192
|
+
return { 이름, 서버, 자리: 잡음.자리, 후보: 잡음.후보 ?? [] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 여러 곳에 같은 이름이 있으면 그대로 알려 준다. 하나를 골라 주고 아닌 척하지 않는다. */
|
|
196
|
+
function 여럿이면(후보, scope) {
|
|
197
|
+
if (!Array.isArray(후보) || 후보.length < 2) return null;
|
|
198
|
+
const 파일들 = new Set(후보.map((c) => { try { return scope.show(fileURLToPath(c.uri)); } catch { return c.uri; } }));
|
|
199
|
+
if (파일들.size < 2) return null;
|
|
200
|
+
return `같은 이름이 ${파일들.size}곳에 있습니다: ${[...파일들].slice(0, 5).join(' · ')}`
|
|
201
|
+
+ `${파일들.size > 5 ? ' …' : ''}. 다른 것을 뜻했다면 file_path 로 짚어 주세요.`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const DEF_TOOL = {
|
|
205
|
+
schema: {
|
|
206
|
+
name: 'Def',
|
|
207
|
+
description:
|
|
208
|
+
'이름 하나가 **어디에 정의돼 있는지** 언어 서버에게 묻는다. 파일을 안 읽고도 자리를 안다.'
|
|
209
|
+
+ ' Grep 과 다른 점은 틀린 자리를 안 준다는 것이다 — 주석에 든 같은 이름, 남의 라이브러리의'
|
|
210
|
+
+ ' 같은 이름, 문자열 안의 같은 이름을 안 섞어 준다.'
|
|
211
|
+
+ ' 남이 쓴 코드를 고치기 전에 이걸 먼저 불러라. 자리를 안 다음에 그 파일만 Read 하면 된다.'
|
|
212
|
+
+ ' 이름이 여러 곳에 있으면 그 목록을 준다. file_path 로 어느 것인지 짚어 주면 된다.',
|
|
213
|
+
parameters: {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
name: { type: 'string', description: '찾을 이름 (함수·클래스·변수)' },
|
|
217
|
+
file_path: { type: 'string', description: '그 이름이 쓰인 파일. 같은 이름이 여럿일 때 짚어 준다' },
|
|
218
|
+
line: { type: 'number', description: 'file_path 안에서 그 이름이 쓰인 줄 번호 (1부터)' },
|
|
219
|
+
},
|
|
220
|
+
required: ['name'],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
async run(args, ctx) {
|
|
225
|
+
const 준비 = await 채비(args, ctx);
|
|
226
|
+
if (준비.오류) return { error: 준비.오류 };
|
|
227
|
+
const { 이름, 서버, 자리, 후보 } = 준비;
|
|
228
|
+
|
|
229
|
+
const 답 = await 서버.물어보기('textDocument/definition', {
|
|
230
|
+
textDocument: { uri: 자리.uri },
|
|
231
|
+
position: 자리.position,
|
|
232
|
+
});
|
|
233
|
+
if (답.오류) return { error: `언어 서버: ${답.오류}` };
|
|
234
|
+
|
|
235
|
+
let 곳들 = 자리들펴기(답.값).map((x) => 한줄(ctx.scope, x.uri, x.range));
|
|
236
|
+
// 서버가 정의를 못 주면(선언만 있는 자리 등) 심볼 검색으로 잡은 자리를 준다.
|
|
237
|
+
// 빈손으로 돌려보내는 것보다 낫고, 어디서 온 값인지 같이 말해 준다.
|
|
238
|
+
let 어디서 = 'definition';
|
|
239
|
+
if (!곳들.length && 후보.length) {
|
|
240
|
+
곳들 = 후보.map((c) => 한줄(ctx.scope, c.uri, c.range));
|
|
241
|
+
어디서 = 'workspace/symbol';
|
|
242
|
+
}
|
|
243
|
+
if (!곳들.length) return { summary: `${이름}: 정의를 못 찾았습니다. Grep 으로 찾아보세요.`, found: 0 };
|
|
244
|
+
|
|
245
|
+
const 여럿 = 여럿이면(후보, ctx.scope);
|
|
246
|
+
return {
|
|
247
|
+
summary: `${이름} — 정의 ${곳들.length}곳${여럿 ? `\n${여럿}` : ''}`,
|
|
248
|
+
found: 곳들.length,
|
|
249
|
+
source: 어디서,
|
|
250
|
+
locations: 곳들,
|
|
251
|
+
content: 곳들.map((l) => `${l.파일}:${l.줄} ${l.글}`).join('\n'),
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const REFS_TOOL = {
|
|
257
|
+
schema: {
|
|
258
|
+
name: 'Refs',
|
|
259
|
+
description:
|
|
260
|
+
'이름 하나를 **어디서 쓰는지** 언어 서버에게 다 묻는다. 이름을 바꾸거나 함수를 고치기 전에'
|
|
261
|
+
+ ' 불러라 — 몇 군데를 같이 고쳐야 하는지가 여기서 나온다.'
|
|
262
|
+
+ ' Grep 이 주는 수백 줄과 달리 진짜 그것을 쓰는 자리만 나온다. 대신 Grep 은 주석·설정·문서까지'
|
|
263
|
+
+ ' 찾으니, 이름을 통째로 바꿀 때는 이걸로 코드를 잡고 Grep 으로 나머지를 훑어라.',
|
|
264
|
+
parameters: {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: {
|
|
267
|
+
name: { type: 'string', description: '찾을 이름 (함수·클래스·변수)' },
|
|
268
|
+
file_path: { type: 'string', description: '그 이름이 정의된 파일. 같은 이름이 여럿일 때 짚어 준다' },
|
|
269
|
+
line: { type: 'number', description: 'file_path 안에서 그 이름이 있는 줄 번호 (1부터)' },
|
|
270
|
+
include_declaration: { type: 'boolean', description: '정의한 자리도 넣을지. 기본 false' },
|
|
271
|
+
},
|
|
272
|
+
required: ['name'],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
async run(args, ctx) {
|
|
277
|
+
const 준비 = await 채비(args, ctx);
|
|
278
|
+
if (준비.오류) return { error: 준비.오류 };
|
|
279
|
+
const { 이름, 서버, 자리, 후보 } = 준비;
|
|
280
|
+
|
|
281
|
+
const 답 = await 서버.물어보기('textDocument/references', {
|
|
282
|
+
textDocument: { uri: 자리.uri },
|
|
283
|
+
position: 자리.position,
|
|
284
|
+
context: { includeDeclaration: args.include_declaration === true },
|
|
285
|
+
});
|
|
286
|
+
if (답.오류) return { error: `언어 서버: ${답.오류}` };
|
|
287
|
+
|
|
288
|
+
const 곳들 = 자리들펴기(답.값).map((x) => 한줄(ctx.scope, x.uri, x.range));
|
|
289
|
+
if (!곳들.length) {
|
|
290
|
+
return {
|
|
291
|
+
summary: `${이름}: 쓰는 자리가 없습니다.`
|
|
292
|
+
+ ' 정말 안 쓰는 것일 수도 있고, 언어 서버가 아직 색인 중일 수도 있습니다 —'
|
|
293
|
+
+ ' 지우기 전에 Grep 으로 한 번 더 보세요.',
|
|
294
|
+
found: 0,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 창에 맞춰 자른다. 자른 것은 자랐다고 말한다 — 조용히 자르면 모델은
|
|
299
|
+
// 그게 전부인 줄 알고 나머지 자리를 안 고친다.
|
|
300
|
+
const 한도 = 찾을개수(ctx.모델컨텍스트 ?? null);
|
|
301
|
+
const 보일것 = 곳들.slice(0, 한도);
|
|
302
|
+
const 남은 = 곳들.length - 보일것.length;
|
|
303
|
+
|
|
304
|
+
// 파일별로 묶어야 읽힌다. 같은 파일 열 줄이 흩어져 있으면 몇 파일을
|
|
305
|
+
// 고쳐야 하는지가 안 보인다.
|
|
306
|
+
const 묶음 = new Map();
|
|
307
|
+
for (const l of 보일것) {
|
|
308
|
+
if (!묶음.has(l.파일)) 묶음.set(l.파일, []);
|
|
309
|
+
묶음.get(l.파일).push(l);
|
|
310
|
+
}
|
|
311
|
+
const 글 = [...묶음.entries()]
|
|
312
|
+
.map(([f, 줄들]) => `${f} (${줄들.length})\n` + 줄들.map((l) => ` ${l.줄}: ${l.글}`).join('\n'))
|
|
313
|
+
.join('\n');
|
|
314
|
+
|
|
315
|
+
const 여럿 = 여럿이면(후보, ctx.scope);
|
|
316
|
+
return {
|
|
317
|
+
summary: `${이름} — 쓰는 자리 ${곳들.length}곳 · 파일 ${묶음.size}개`
|
|
318
|
+
+ (남은 ? ` (${남은}곳은 자리가 모자라 안 실었습니다)` : '')
|
|
319
|
+
+ (여럿 ? `\n${여럿}` : ''),
|
|
320
|
+
found: 곳들.length,
|
|
321
|
+
files: 묶음.size,
|
|
322
|
+
truncated: 남은 > 0,
|
|
323
|
+
locations: 보일것,
|
|
324
|
+
content: 글,
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
};
|
package/src/tools/task.js
CHANGED
|
@@ -59,6 +59,27 @@ export const TASK_TOOL = {
|
|
|
59
59
|
type: 'string',
|
|
60
60
|
description: '하위가 일할 방식: code(만들고 고침) · debug(원인 찾기) · ask(읽고 답만). 안 적으면 code.',
|
|
61
61
|
},
|
|
62
|
+
/*
|
|
63
|
+
* 다른 모델에게 떼어 주기.
|
|
64
|
+
*
|
|
65
|
+
* 로컬에서는 모델 하나로 다 하기가 늘 아쉽다. 7B 는 계획을 잘 세우는데
|
|
66
|
+
* 파일 열 개를 고치다 창이 차고, 1.5B 는 창은 넉넉한데 무엇을 할지를
|
|
67
|
+
* 못 정한다. 8GB 램이면 둘 다 올려 두고 나눠 쓰는 것이 실제로 되는
|
|
68
|
+
* 유일한 길이다 (7B q4 + 1.5B q4 ≒ 5.7GB).
|
|
69
|
+
*
|
|
70
|
+
* 설명에 '사람이 설정에 적어 둔 이름' 이라고 못을 박는다. 모델이
|
|
71
|
+
* 주소를 지어내면 그 자리로 나갈 뻔하는데, 그건 이 프로그램이 하는
|
|
72
|
+
* 약속을 그 자리에서 깨는 것이다. 실제 판단은 loop.js 가 한다 —
|
|
73
|
+
* 부탁으로 막지 않는다.
|
|
74
|
+
*/
|
|
75
|
+
모델: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description:
|
|
78
|
+
'이 덩이를 **다른 모델**에게 맡긴다. 사람이 설정에 적어 둔 프로필 이름만 쓸 수 있다'
|
|
79
|
+
+ ' (주소를 지어내지 마라 — 안 먹는다). 안 적으면 지금 쓰는 모델 그대로다.'
|
|
80
|
+
+ ' 잔일(형식 맞추기·반복 수정·짧은 요약)을 작은 모델에게 떼어 주면 네 창이 안 찬다.'
|
|
81
|
+
+ ' 판단이 필요한 일은 네가 직접 해라.',
|
|
82
|
+
},
|
|
62
83
|
},
|
|
63
84
|
required: ['목적', '할일'],
|
|
64
85
|
},
|
|
@@ -106,7 +127,7 @@ export function 하위모드(요청, 부모모드) {
|
|
|
106
127
|
* 못 끝낸 것을 빼면 안 된다. 부모는 그걸 보고 이어서 할지 정한다 —
|
|
107
128
|
* 다 됐다고 알면 안 끝난 일 위에 다음 일을 쌓는다.
|
|
108
129
|
*/
|
|
109
|
-
export function 하위요약({ 목적, 모드, 끝, 글자수, 보인이름 = (p) => p }) {
|
|
130
|
+
export function 하위요약({ 목적, 모드, 끝, 글자수, 모델 = null, 보인이름 = (p) => p }) {
|
|
110
131
|
const 줄 = [];
|
|
111
132
|
const w = workMode(모드);
|
|
112
133
|
const 왜끝났나 = {
|
|
@@ -116,7 +137,14 @@ export function 하위요약({ 목적, 모드, 끝, 글자수, 보인이름 = (p
|
|
|
116
137
|
aborted: '사용자가 중단함 — 다 못 했습니다',
|
|
117
138
|
}[끝?.type] ?? '끝난 이유를 알 수 없음';
|
|
118
139
|
|
|
119
|
-
|
|
140
|
+
/*
|
|
141
|
+
* 다른 모델이 한 일이면 **요약에도 적는다.**
|
|
142
|
+
*
|
|
143
|
+
* 부모 모델이 이 요약만 보고 이어서 일하는데, 누가 한 것인지 모르면 작은
|
|
144
|
+
* 모델이 대충 해 놓은 것을 제가 한 것처럼 믿는다. "1.5B 가 했다" 가 적혀
|
|
145
|
+
* 있으면 미심쩍은 자리를 다시 볼 여지가 생긴다.
|
|
146
|
+
*/
|
|
147
|
+
줄.push(`하위 작업 "${목적}" · ${w.name} 모드${모델 ? ` · ${모델}` : ''} · ${끝?.steps ?? 0}걸음 · ${왜끝났나}`);
|
|
120
148
|
|
|
121
149
|
const 파일들 = (끝?.files ?? []).filter((f) => !f.dir);
|
|
122
150
|
if (파일들.length) {
|
package/src/tools/todo.js
CHANGED
|
@@ -39,16 +39,32 @@ export function render(items) {
|
|
|
39
39
|
export const TODO_TOOL = {
|
|
40
40
|
schema: {
|
|
41
41
|
name: 'TodoWrite',
|
|
42
|
+
/*
|
|
43
|
+
* 문장 차례가 곧 중요도다.
|
|
44
|
+
*
|
|
45
|
+
* 창이 좁으면 tools/index.js 가 **뒤에서부터 문장째로 잘라 낸다**(설명길이).
|
|
46
|
+
* 그러니 규격(state·doing)이 뒤에 있으면 작은 모델에서 조용히 사라진다.
|
|
47
|
+
* 규격 → 규칙 → 조언 차례로 둔다.
|
|
48
|
+
*
|
|
49
|
+
* '세 단계 이상 걸리는 일이면' 이라고 적었더니 모델이 그걸 **세 개를 적으라는
|
|
50
|
+
* 말**로 읽었다. 열 단계짜리 일도 세 줄로 뭉쳐 나왔다. 그래서 개수 이야기를
|
|
51
|
+
* 따로 못 박는다 — 개수는 일의 크기가 정하는 것이고 상한이 없다.
|
|
52
|
+
*/
|
|
42
53
|
description:
|
|
43
|
-
'할 일 목록을 만들고 갱신한다.
|
|
54
|
+
'할 일 목록을 만들고 갱신한다. 여러 단계가 걸리는 일이면 먼저 목록을 만들고, '
|
|
44
55
|
+ '하나를 끝낼 때마다 바로 갱신한다. 목록 전체를 매번 통째로 보낸다. '
|
|
45
|
-
+ 'state 는 todo(아직) / doing(하는 중) / done(끝) 셋 중 하나이고, doing 은 한 번에 하나만 둔다.'
|
|
56
|
+
+ 'state 는 todo(아직) / doing(하는 중) / done(끝) 셋 중 하나이고, doing 은 한 번에 하나만 둔다. '
|
|
57
|
+
+ '단계 수는 일의 크기가 정한다 — 정해진 개수도 상한도 없다. '
|
|
58
|
+
+ '개수를 맞추려고 다른 일을 한 줄에 뭉치지 마라. '
|
|
59
|
+
+ '각 단계는 끝났는지 따로 확인할 수 있는 크기로 적어라.',
|
|
46
60
|
parameters: {
|
|
47
61
|
type: 'object',
|
|
48
62
|
properties: {
|
|
49
63
|
todos: {
|
|
50
64
|
type: 'array',
|
|
51
|
-
|
|
65
|
+
// 인자 설명은 따로 잘리므로, 개수 이야기를 여기에도 한 번 둔다.
|
|
66
|
+
// 창이 좁아 바깥 설명이 잘려 나가도 이건 남는다.
|
|
67
|
+
description: '할 일 전체 목록. 개수 제한 없음 — 일의 크기만큼 적는다',
|
|
52
68
|
items: {
|
|
53
69
|
type: 'object',
|
|
54
70
|
properties: {
|