deel-local-cli 1.4.0 → 1.4.2
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 +183 -1886
- package/README.md +174 -1930
- package/package.json +3 -2
- package/src/agent/card.js +19 -0
- package/src/agent/effort.js +5 -0
- package/src/agent/preset.js +113 -0
- package/src/agent/session.js +23 -7
- package/src/backend/adapter.js +14 -0
- package/src/commands.js +30 -0
- package/src/i18n/en.js +21 -0
- package/src/i18n/ko.js +21 -0
- package/src/preview/serve.js +37 -6
- package/src/safety/guard.js +4 -1
- package/src/tools/desc.en.js +4 -2
- package/src/tools/docs.js +252 -0
- package/src/tools/index.js +42 -1
- package/src/tools/webfetch.js +20 -6
- package/src/ui/ansi.js +9 -0
- package/src/ui/banner.js +245 -0
- package/src/ui/export.js +217 -0
- package/src/ui/intro.js +42 -10
- package/src/ui/status.js +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deel-local-cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "로컬 모델·사내 게이트웨이 전용 코딩 에이전트 CLI — 외부 의존성 0개 / Zero-dependency coding agent CLI for local LLMs and private gateways",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,10 +47,11 @@
|
|
|
47
47
|
"diagnose": "node bin/deel.js diagnose",
|
|
48
48
|
"chat": "node bin/deel.js",
|
|
49
49
|
"test": "node test/run.mjs",
|
|
50
|
+
"docs": "node tools/check-docs.mjs",
|
|
50
51
|
"verify": "node test/no-bundle.test.js && node test/network.test.js && node test/web.test.js",
|
|
51
52
|
"bench": "node test/edit-bench.js",
|
|
52
53
|
"demo": "node test/demo.js",
|
|
53
|
-
"check": "node --check bin/deel.js && node --check src/repl.js && node --check src/oneshot.js && node --check src/commands.js && node --check src/agent/loop.js && node --check src/agent/session.js && node --check src/agent/effort.js && node --check src/agent/modes.js && node --check src/agent/route.js && node --check src/agent/models.js && node --check src/agent/grade.js && node --check src/agent/mention.js && node --check src/agent/project.js && node --check src/tools/encoding.js && node --check src/tools/excel-com.js && node --check src/tools/excel.js && node --check src/tools/xlsx.js && node --check src/agent/compact.js && node --check src/agent/store.js && node --check src/agent/sessionui.js && node --check src/backend/adapter.js && node --check src/backend/ctxsize.js && node --check src/backend/probe.js && node --check src/backend/detect.js && node --check src/backend/http.js && node --check src/backend/scan.js && node --check src/backend/scanui.js && node --check src/tools/index.js && node --check src/tools/desc.en.js && node --check src/tools/lsp.js && node --check src/lsp/rpc.js && node --check src/lsp/servers.js && node --check src/lsp/client.js && node --check src/lsp/diag.js && node --check src/tools/fsutil.js && node --check src/tools/edit-match.js && node --check src/tools/webfetch.js && node --check src/tools/todo.js && node --check src/tools/jobs.js && node --check src/skills/discover.js && node --check src/plugins/manage.js && node --check src/pack/zip.js && node --check src/pack/tar.js && node --check src/pack/selfpack.js && node --check src/safety/guard.js && node --check src/safety/undo.js && node --check src/safety/audit.js && node --check src/safety/network.js && node --check src/setup.js && node --check src/report.js && node --check src/config.js && node --check src/ui/diff.js && node --check src/ui/ansi.js && node --check src/ui/status.js && node --check src/ui/level.js && node --check src/ui/prompt.js && node --check src/ui/spinner.js && node --check src/ui/motion.js && node --check src/ui/notify.js && node --check src/ui/intro.js && node --check src/i18n/index.js && node --check src/i18n/ko.js && node --check src/i18n/en.js && node --check src/preview/serve.js && node --check src/ui/working.js && node --check src/ui/inputbox.js && node --check src/ui/md.js && node --check src/agent/threads.js && node --check src/agent/evolve.js && node --check src/agent/pins.js && node --check src/agent/card.js && node --check src/agent/evidence.js && node --check src/acp/jsonrpc.js && node --check src/acp/map.js && node --check src/acp/serve.js && node --check src/pack/sbom.js && node --check src/safety/secrets.js && echo OK",
|
|
54
|
+
"check": "node --check bin/deel.js && node --check src/repl.js && node --check src/oneshot.js && node --check src/commands.js && node --check src/agent/loop.js && node --check src/agent/session.js && node --check src/agent/effort.js && node --check src/agent/modes.js && node --check src/agent/route.js && node --check src/agent/models.js && node --check src/agent/grade.js && node --check src/agent/mention.js && node --check src/agent/project.js && node --check src/tools/encoding.js && node --check src/tools/excel-com.js && node --check src/tools/excel.js && node --check src/tools/xlsx.js && node --check src/tools/docs.js && node --check src/agent/compact.js && node --check src/agent/store.js && node --check src/agent/sessionui.js && node --check src/backend/adapter.js && node --check src/backend/ctxsize.js && node --check src/backend/probe.js && node --check src/backend/detect.js && node --check src/backend/http.js && node --check src/backend/scan.js && node --check src/backend/scanui.js && node --check src/tools/index.js && node --check src/tools/desc.en.js && node --check src/tools/lsp.js && node --check src/lsp/rpc.js && node --check src/lsp/servers.js && node --check src/lsp/client.js && node --check src/lsp/diag.js && node --check src/tools/fsutil.js && node --check src/tools/edit-match.js && node --check src/tools/webfetch.js && node --check src/tools/todo.js && node --check src/tools/jobs.js && node --check src/skills/discover.js && node --check src/plugins/manage.js && node --check src/pack/zip.js && node --check src/pack/tar.js && node --check src/pack/selfpack.js && node --check src/safety/guard.js && node --check src/safety/undo.js && node --check src/safety/audit.js && node --check src/safety/network.js && node --check src/setup.js && node --check src/report.js && node --check src/config.js && node --check src/ui/diff.js && node --check src/ui/ansi.js && node --check src/ui/status.js && node --check src/ui/level.js && node --check src/ui/prompt.js && node --check src/ui/spinner.js && node --check src/ui/motion.js && node --check src/ui/notify.js && node --check src/ui/intro.js && node --check src/ui/banner.js && node --check src/ui/export.js && node --check src/i18n/index.js && node --check src/i18n/ko.js && node --check src/i18n/en.js && node --check src/preview/serve.js && node --check src/ui/working.js && node --check src/ui/inputbox.js && node --check src/ui/md.js && node --check src/agent/threads.js && node --check src/agent/evolve.js && node --check src/agent/pins.js && node --check src/agent/card.js && node --check src/agent/preset.js && node --check src/agent/evidence.js && node --check src/acp/jsonrpc.js && node --check src/acp/map.js && node --check src/acp/serve.js && node --check src/pack/sbom.js && node --check src/safety/secrets.js && echo OK",
|
|
54
55
|
"coverage": "node test/coverage.mjs",
|
|
55
56
|
"prepublishOnly": "npm run check && npm test"
|
|
56
57
|
},
|
package/src/agent/card.js
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
// 하네스를 조이면, 멀쩡한 모델을 붙들어 매는 셈이 된다 — 안 배우느니만 못하다.
|
|
31
31
|
// 그래서 걸음 상한과 비율 문턱을 둘 다 넘어야 움직인다.
|
|
32
32
|
|
|
33
|
+
import { 내장카드, 추론형왜 } from './preset.js';
|
|
34
|
+
|
|
33
35
|
/** 이 아래로는 판단하지 않는다. 걸음이 적으면 비율이 아무 뜻도 없다. */
|
|
34
36
|
export const 최소걸음 = 12;
|
|
35
37
|
/** 이 비율을 넘어야 '버릇' 으로 본다. */
|
|
@@ -76,6 +78,21 @@ export function 카드(모델, 본것 = null, 겪은것 = null) {
|
|
|
76
78
|
const 조정 = 기본조정();
|
|
77
79
|
const 왜 = [];
|
|
78
80
|
|
|
81
|
+
/*
|
|
82
|
+
* 겪기 전에 이미 아는 것 (preset.js).
|
|
83
|
+
*
|
|
84
|
+
* 최소걸음 관문 **앞**에 선다 — 이것만은 겪어서 아는 것이 아니라 공개 문서로
|
|
85
|
+
* 아는 것이라, 걸음 수와 무관하게 참이기 때문이다. 추론 모델은 생각이 출력
|
|
86
|
+
* 예산을 먹어 답이 잘린다; 열두 걸음 걸으며 다시 확인할 일이 아니다.
|
|
87
|
+
* 문서로 확인 가능한 이 한 가지만 미리 적용하고, 나머지 버릇은 지금까지처럼
|
|
88
|
+
* 겪어서만 잡는다.
|
|
89
|
+
*/
|
|
90
|
+
const 내장 = 내장카드(모델);
|
|
91
|
+
if (내장?.추론형) {
|
|
92
|
+
조정.상한먼저올리기 = true;
|
|
93
|
+
왜.push(추론형왜());
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
// 걸음이 모자라면 아무것도 안 바꾼다. 여기서 일찍 나가는 것이 안전장치다.
|
|
80
97
|
if (걸음 >= 최소걸음) {
|
|
81
98
|
if (버릇.잘린인자.율 >= 문턱) {
|
|
@@ -101,6 +118,8 @@ export function 카드(모델, 본것 = null, 겪은것 = null) {
|
|
|
101
118
|
보정: Number(겪은것?.보정) || 1,
|
|
102
119
|
조정,
|
|
103
120
|
왜,
|
|
121
|
+
// 미리 아는 모델이면 그 이름표. 모르면 null — 아는 척하지 않는다.
|
|
122
|
+
내장,
|
|
104
123
|
// 판단을 아직 못 하는 상태인지 화면이 알아야 한다 — '바꾼 것 없음' 과
|
|
105
124
|
// '아직 모름' 은 다른 말이다.
|
|
106
125
|
아직모름: 걸음 < 최소걸음,
|
package/src/agent/effort.js
CHANGED
|
@@ -36,18 +36,23 @@ export const STAGES = {
|
|
|
36
36
|
export const PROFILES = {
|
|
37
37
|
even: {
|
|
38
38
|
name: '균일',
|
|
39
|
+
// 화면 말이 영어일 때 쓰는 이름. modes.js 의 en 과 같은 방식이다 —
|
|
40
|
+
// i18n 표에 같은 말을 또 적어 두면 언젠가 둘이 갈라진다.
|
|
41
|
+
en: 'even',
|
|
39
42
|
desc: '모든 단계 같은 강도 — 예측 가능한 대신 느립니다',
|
|
40
43
|
shift: { plan: 0, work: 0, fix: 0 },
|
|
41
44
|
share: { plan: 0.40, work: 0.40, fix: 0.40 },
|
|
42
45
|
},
|
|
43
46
|
save: {
|
|
44
47
|
name: '절약',
|
|
48
|
+
en: 'save',
|
|
45
49
|
desc: '첫 판단만 세게, 이어가기는 얕게 — 대개 이게 낫습니다',
|
|
46
50
|
shift: { plan: 0, work: -1, fix: +1 },
|
|
47
51
|
share: { plan: 0.40, work: 0.35, fix: 0.45 },
|
|
48
52
|
},
|
|
49
53
|
deep: {
|
|
50
54
|
name: '깊게',
|
|
55
|
+
en: 'deep',
|
|
51
56
|
desc: '전 단계 한 칸씩 위로 — 어려운 일에만',
|
|
52
57
|
shift: { plan: +1, work: 0, fix: +1 },
|
|
53
58
|
share: { plan: 0.50, work: 0.45, fix: 0.50 },
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 내장 모델 카드 — 겪기 전에 이미 아는 버릇.
|
|
3
|
+
*
|
|
4
|
+
* ── 카드(card.js)와 무엇이 다른가 ───────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 카드는 겪어 본 버릇으로 하네스를 조정한다. 좋은데, **겪어야** 나온다 —
|
|
7
|
+
* 최소걸음(12)을 걷기 전에는 아무것도 안 바꾼다. 그런데 어떤 버릇은 겪기
|
|
8
|
+
* 전에 이미 안다. 추론 모델은 생각(thinking)이 출력 예산을 먹어 답이 잘린다 —
|
|
9
|
+
* 공개 문서에 있는 사실이고, 열두 걸음 걸으며 다시 확인할 일이 아니다.
|
|
10
|
+
* 이 프로그램이 그 함정을 처음 밟은 것도 qwen3 진단 때였다: 토큰 상한이
|
|
11
|
+
* 낮으면 본문이 통째로 비고 전부 thinking 으로 간다.
|
|
12
|
+
*
|
|
13
|
+
* ── 왜 국산 모델인가 ────────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* 이 표의 절반이 국산(EXAONE·HyperCLOVA X·Kanana·Midm·Solar)인 이유는 이
|
|
16
|
+
* 프로그램이 도는 자리가 그 모델들이 도는 자리라서다. 사내망 GPU 에 올라가는
|
|
17
|
+
* 것이 이 이름들이고, 해외 도구는 이 이름들을 모른다.
|
|
18
|
+
*
|
|
19
|
+
* ── 지키는 선 ───────────────────────────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* 미리 아는 것은 **공개 문서에서 오는 것만** 적는다. 안 겪은 것을 겪은
|
|
22
|
+
* 척하면 카드 전체가 거짓말이 된다. 그래서 여기 적는 조정은 문서로 확인
|
|
23
|
+
* 가능한 한 가지 — 추론형 여부 — 뿐이고, 나머지(잘림·되풀이·빗나감)는
|
|
24
|
+
* 지금까지처럼 겪어서만 잡는다. 겪어 본 것이 이 표를 이긴다.
|
|
25
|
+
*/
|
|
26
|
+
import { 언어 } from '../i18n/index.js';
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* 표. 위에서부터 먼저 맞는 것을 쓴다 — Deep 이 EXAONE 보다 먼저 서는
|
|
30
|
+
* 이유다(이름이 겹친다).
|
|
31
|
+
*
|
|
32
|
+
* 골라 는 Ollama·HuggingFace 의 이름 버릇을 다 받아야 한다:
|
|
33
|
+
* `exaone-deep:7.8b` · `hf.co/LGAI-EXAONE/EXAONE-4.0-32B-GGUF` ·
|
|
34
|
+
* `hyperclovax-seed-text-instruct-1.5b` 처럼 제각각이다.
|
|
35
|
+
*/
|
|
36
|
+
const 표 = [
|
|
37
|
+
{
|
|
38
|
+
골라: /exaone[-_.\s]?deep|exaone.*r1/i,
|
|
39
|
+
이름: 'EXAONE Deep (LG)',
|
|
40
|
+
추론형: true,
|
|
41
|
+
한줄ko: 'LG 의 추론 모델. 수학·코딩에 생각을 길게 쓴다 — 공개 문서 기준.',
|
|
42
|
+
한줄en: "LG's reasoning model. Thinks long on math and code — per its public docs.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
골라: /exaone/i,
|
|
46
|
+
이름: 'EXAONE (LG)',
|
|
47
|
+
추론형: false,
|
|
48
|
+
한줄ko: 'LG 의 한국어·영어 모델. 32k 컨텍스트 세대가 흔하다.',
|
|
49
|
+
한줄en: "LG's Korean-English model. The 32k-context generation is common.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
골라: /hyperclova|clova[-_.\s]?x/i,
|
|
53
|
+
이름: 'HyperCLOVA X SEED (네이버)',
|
|
54
|
+
추론형: false,
|
|
55
|
+
한줄ko: '네이버의 공개 소형 모델(0.5B~3B). 급 조정(grade.js)이 알아서 잡는다.',
|
|
56
|
+
한줄en: "Naver's open small models (0.5B-3B). The grade system already sizes for them.",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
골라: /kanana/i,
|
|
60
|
+
이름: 'Kanana (카카오)',
|
|
61
|
+
추론형: false,
|
|
62
|
+
한줄ko: '카카오의 공개 모델. nano(2.1B)부터 있다 — 작은 판은 급 조정이 잡는다.',
|
|
63
|
+
한줄en: "Kakao's open models, from nano (2.1B) up. Small sizes are handled by the grade system.",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
골라: /mi[-_.:\s]?dm|midm/i,
|
|
67
|
+
이름: 'Midm — 믿음 (KT)',
|
|
68
|
+
추론형: false,
|
|
69
|
+
한줄ko: 'KT 의 공개 한국어 모델.',
|
|
70
|
+
한줄en: "KT's open Korean model.",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
골라: /solar/i,
|
|
74
|
+
이름: 'Solar (업스테이지)',
|
|
75
|
+
추론형: false,
|
|
76
|
+
한줄ko: '업스테이지의 모델. 한국어가 강하다.',
|
|
77
|
+
한줄en: "Upstage's model, strong on Korean.",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
/*
|
|
81
|
+
* 국산은 아니지만 이 자리(로컬 한국어 사용자)에서 제일 흔한 이름이고,
|
|
82
|
+
* deel 이 이 함정을 실제로 밟았다 — 진단 때 답이 통째로 thinking 으로
|
|
83
|
+
* 새서 "기본 대화 안 됨" 으로 오판할 뻔했다.
|
|
84
|
+
*/
|
|
85
|
+
골라: /qwen3|qwq/i,
|
|
86
|
+
이름: 'Qwen3 (알리바바)',
|
|
87
|
+
추론형: true,
|
|
88
|
+
한줄ko: '생각을 켜고 끌 수 있는 모델. 켜져 있으면 생각이 출력 예산을 먹는다.',
|
|
89
|
+
한줄en: 'Hybrid thinking model. With thinking on, the thinking eats the output budget.',
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 이 모델을 미리 아는가.
|
|
95
|
+
* @returns {{이름, 추론형, 한줄}|null} 모르면 null — 모르는 것은 모른다고 한다.
|
|
96
|
+
*/
|
|
97
|
+
export function 내장카드(모델) {
|
|
98
|
+
const 이름 = String(모델 ?? '');
|
|
99
|
+
if (!이름) return null;
|
|
100
|
+
for (const p of 표) {
|
|
101
|
+
if (p.골라.test(이름)) {
|
|
102
|
+
return { 이름: p.이름, 추론형: p.추론형, 한줄: 언어() === 'en' ? p.한줄en : p.한줄ko };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 추론형에 붙는 왜 한 줄. card.js 가 쓴다. */
|
|
109
|
+
export function 추론형왜() {
|
|
110
|
+
return 언어() === 'en'
|
|
111
|
+
? 'A reasoning model (public docs) — thinking eats the output budget and answers get cut, so the cap starts generous.'
|
|
112
|
+
: '추론 모델입니다(공개 문서) — 생각이 출력 예산을 먹어 답이 잘리기 쉬우므로, 출력 상한을 처음부터 넉넉히 줍니다.';
|
|
113
|
+
}
|
package/src/agent/session.js
CHANGED
|
@@ -288,12 +288,6 @@ export class Session {
|
|
|
288
288
|
? `\nWorking folder: ${this.root}\nYou can neither read nor write files outside this folder.`
|
|
289
289
|
: `\n작업 폴더: ${this.root}\n이 폴더 밖의 파일은 읽지도 쓰지도 못한다.`);
|
|
290
290
|
|
|
291
|
-
// 지금 무슨 일을 하는 중인지. 도구 목록도 이 모드에 맞춰 이미 걸러져 있다.
|
|
292
|
-
const w = workMode(this.effectiveWork());
|
|
293
|
-
// 창이 좁으면 짧은 판을 쓴다 (modes.js 의 말()). 규칙은 같고 설득하는 문장만 빠진다.
|
|
294
|
-
parts.push(영
|
|
295
|
-
? `\n--- current mode: ${w.en} ---\n${모드말(this.effectiveWork(), this.conn?.ctx)}`
|
|
296
|
-
: `\n--- 지금 모드: ${w.name} (${w.en}) ---\n${모드말(this.effectiveWork(), this.conn?.ctx)}`);
|
|
297
291
|
/*
|
|
298
292
|
* 모델 급에 맞춘 한 문단 (grade.js).
|
|
299
293
|
*
|
|
@@ -359,12 +353,34 @@ export class Session {
|
|
|
359
353
|
: `(그 밖에 ${rest}개가 더 있으나 자리가 모자라 안 실었다.)`);
|
|
360
354
|
}
|
|
361
355
|
}
|
|
356
|
+
/*
|
|
357
|
+
* 지금 무슨 일을 하는 중인지 — **일부러 끝쪽에** 둔다.
|
|
358
|
+
*
|
|
359
|
+
* 이 절은 이 프롬프트에서 유일하게 **매 턴 바뀔 수 있는** 자리다. 말을
|
|
360
|
+
* 던질 때마다 알맞은 모드로 저절로 옮겨 가기 때문이다(route.js). 그런데
|
|
361
|
+
* Ollama·llama.cpp 의 프리픽스 캐시는 앞부분이 지난 요청과 같을 때만
|
|
362
|
+
* 계산을 재쓴다 — 이 절이 앞쪽에 있으면 모드가 바뀌는 순간 그 뒤 전부,
|
|
363
|
+
* 시스템 프롬프트 나머지에 대화 전체까지 다시 계산된다. 긴 대화일수록
|
|
364
|
+
* 매 턴 몇천 토큰이고, 로컬에서는 그게 그대로 몇 초다.
|
|
365
|
+
*
|
|
366
|
+
* 그래서 변하지 않는 것들(규칙·폴더·급말·지문·사용자 규칙·기억·스킬)을
|
|
367
|
+
* 앞에 굳히고 이 절을 뒤로 보냈다. 읽기 쪽으로도 손해가 아니다 — 끝자리는
|
|
368
|
+
* 가운데보다 오히려 잘 읽힌다(lost in the middle 의 반대편이다).
|
|
369
|
+
* 이 차례는 test/cache.test.js 가 지킨다.
|
|
370
|
+
*
|
|
371
|
+
* 도구 목록도 이 모드에 맞춰 이미 걸러져 있다. 창이 좁으면 짧은 판을
|
|
372
|
+
* 쓴다 (modes.js 의 말()) — 규칙은 같고 설득하는 문장만 빠진다.
|
|
373
|
+
*/
|
|
374
|
+
const w = workMode(this.effectiveWork());
|
|
375
|
+
parts.push(영
|
|
376
|
+
? `\n--- current mode: ${w.en} ---\n${모드말(this.effectiveWork(), this.conn?.ctx)}`
|
|
377
|
+
: `\n--- 지금 모드: ${w.name} (${w.en}) ---\n${모드말(this.effectiveWork(), this.conn?.ctx)}`);
|
|
362
378
|
/*
|
|
363
379
|
* 못 박은 것은 **맨 끝**에 붙인다 (agent/pins.js).
|
|
364
380
|
*
|
|
365
381
|
* 긴 글의 가운데는 흘려 읽힌다 — 'lost in the middle' 이라 부르는 것이고,
|
|
366
382
|
* 어느 모델에서나 잰다. 사람이 직접 못 박은 말은 그 가운데에 묻히면 안 되므로
|
|
367
|
-
* 가장 마지막, 대화 바로 앞에 둔다.
|
|
383
|
+
* 가장 마지막, 대화 바로 앞에 둔다. 모드 절보다도 뒤인 것도 그래서다.
|
|
368
384
|
*/
|
|
369
385
|
const 못박은글 = this.못박은것?.요약();
|
|
370
386
|
if (못박은글) parts.push(못박은글);
|
package/src/backend/adapter.js
CHANGED
|
@@ -20,6 +20,20 @@ export function buildBody(shape, { model, messages, tools, stream, json, think,
|
|
|
20
20
|
* 지금까지 이 값을 **한 번도 안 보냈다.** 보내면 그 길이로 올려 준다.
|
|
21
21
|
*/
|
|
22
22
|
if (ctx) body.options.num_ctx = ctx;
|
|
23
|
+
/*
|
|
24
|
+
* 모델을 내리지 말라고 말해 둔다.
|
|
25
|
+
*
|
|
26
|
+
* Ollama 는 5분 동안 조용하면 모델을 내린다. 다음 말을 걸면 다시 올리고
|
|
27
|
+
* **대화 전체를 다시 계산한다** — 프리픽스 캐시가 통째로 사라진 것과 같다.
|
|
28
|
+
* 로컬 대화는 사람이 생각하고 다른 창을 보다 돌아오는 것이라, 5분 넘게
|
|
29
|
+
* 조용한 것이 오히려 보통이다. 그 복귀 첫 마디가 제일 오래 걸리는 이유가
|
|
30
|
+
* 바로 이것이었다.
|
|
31
|
+
*
|
|
32
|
+
* -1(영원히)은 안 쓴다. 모델을 갈아탄 뒤에도 이전 모델이 램을 물고 있게
|
|
33
|
+
* 되는데, 8GB 램에서는 다음 모델이 못 올라온다는 뜻이다. 한 시간이면
|
|
34
|
+
* 일하는 호흡은 다 덮고, 퇴근하면 놓아 준다. DEEL_KEEP_ALIVE 로 바꾼다.
|
|
35
|
+
*/
|
|
36
|
+
body.keep_alive = process.env.DEEL_KEEP_ALIVE || '60m';
|
|
23
37
|
if (tools?.length) body.tools = tools;
|
|
24
38
|
if (json) body.format = json;
|
|
25
39
|
if (think !== undefined) body.think = think;
|
package/src/commands.js
CHANGED
|
@@ -15,6 +15,7 @@ import { chat } from './backend/adapter.js';
|
|
|
15
15
|
import { TOOLS } from './tools/index.js';
|
|
16
16
|
import { 둘러보기, 프로젝트갈래 } from './lsp/servers.js';
|
|
17
17
|
import { 지금것들 } from './lsp/client.js';
|
|
18
|
+
import { 보고서적기 } from './ui/export.js';
|
|
18
19
|
import { loadCommand, discover } from './skills/discover.js';
|
|
19
20
|
import { install, list, remove, pack } from './plugins/manage.js';
|
|
20
21
|
import { spin } from './ui/spinner.js';
|
|
@@ -91,6 +92,7 @@ const 명령들 = {
|
|
|
91
92
|
consult: { arg: true },
|
|
92
93
|
lang: { arg: true },
|
|
93
94
|
lsp: { arg: true },
|
|
95
|
+
export: { arg: true },
|
|
94
96
|
init: { arg: false },
|
|
95
97
|
exit: { arg: false },
|
|
96
98
|
quit: { arg: false },
|
|
@@ -330,6 +332,25 @@ export async function handle(line, session, ctx) {
|
|
|
330
332
|
* 대신 **여기서도 안 깔아 준다.** 깔 명령을 글자로 보여 줄 뿐이고,
|
|
331
333
|
* 칠지 말지는 사람이 정한다 (lsp/servers.js 머리말).
|
|
332
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
|
+
|
|
333
354
|
case 'lsp': {
|
|
334
355
|
const 값 = String(arg ?? '').trim().toLowerCase();
|
|
335
356
|
if (값 === 'off' || 값 === '끔' || 값 === '꺼') {
|
|
@@ -1515,6 +1536,15 @@ function 카드명령(session, ctx) {
|
|
|
1515
1536
|
|
|
1516
1537
|
rule('모델 카드', 70);
|
|
1517
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
|
+
}
|
|
1518
1548
|
say('');
|
|
1519
1549
|
|
|
1520
1550
|
const 줄 = (이름, v) => {
|
package/src/i18n/en.js
CHANGED
|
@@ -215,6 +215,27 @@ export const en = {
|
|
|
215
215
|
'lsp.neverInstalls': 'deel installs nothing. Those are commands for you to run yourself.',
|
|
216
216
|
'lsp.howto': 'Turn post-edit diagnostics on or off:',
|
|
217
217
|
|
|
218
|
+
'cmd.export.desc': 'this conversation as a one-page HTML report — asked, changed, verified',
|
|
219
|
+
'cmd.export.arg': '[filename]',
|
|
220
|
+
'export.untitled': 'Untitled conversation',
|
|
221
|
+
'export.model': 'Model',
|
|
222
|
+
'export.calls': '{수} tool calls',
|
|
223
|
+
'export.conversation': 'What was asked and answered',
|
|
224
|
+
'export.you': 'You',
|
|
225
|
+
'export.nothing': 'Nothing was said.',
|
|
226
|
+
'export.changed': 'Changed files',
|
|
227
|
+
'export.file': 'File',
|
|
228
|
+
'export.times': 'Times',
|
|
229
|
+
'export.noChanges': 'No files were changed.',
|
|
230
|
+
'export.evidence': 'What was run',
|
|
231
|
+
'export.nothingRan': 'No commands were run.',
|
|
232
|
+
'export.unproven': 'Not proven',
|
|
233
|
+
'export.allProven': 'None — every change has a later check behind it.',
|
|
234
|
+
'export.footer': 'Report generated by deel. No outbound links — opens on any network.',
|
|
235
|
+
'export.saved': 'Report saved:',
|
|
236
|
+
'export.openHint': 'Double-click to open. Attach it to a review or report as-is.',
|
|
237
|
+
'export.failed': 'Could not save the report. Check folder permissions.',
|
|
238
|
+
|
|
218
239
|
|
|
219
240
|
// ── Help, approval policy, user level ─────────────────────────────────
|
|
220
241
|
'help.titleCommon': 'commands (the ones you will use)',
|
package/src/i18n/ko.js
CHANGED
|
@@ -202,6 +202,27 @@ export const ko = {
|
|
|
202
202
|
'lsp.neverInstalls': 'deel 은 아무것도 깔지 않습니다. 위 명령은 사람이 직접 칠 것입니다.',
|
|
203
203
|
'lsp.howto': '고친 뒤 진단 켜고 끄기:',
|
|
204
204
|
|
|
205
|
+
'cmd.export.desc': '이 대화를 보고서 한 장(HTML)으로 — 시킨 것·바뀐 것·확인한 것',
|
|
206
|
+
'cmd.export.arg': '[파일이름]',
|
|
207
|
+
'export.untitled': '제목 없는 대화',
|
|
208
|
+
'export.model': '모델',
|
|
209
|
+
'export.calls': '도구 {수}회',
|
|
210
|
+
'export.conversation': '오간 말',
|
|
211
|
+
'export.you': '나',
|
|
212
|
+
'export.nothing': '오간 말이 없습니다.',
|
|
213
|
+
'export.changed': '바뀐 파일',
|
|
214
|
+
'export.file': '파일',
|
|
215
|
+
'export.times': '몇 번',
|
|
216
|
+
'export.noChanges': '바뀐 파일이 없습니다.',
|
|
217
|
+
'export.evidence': '돌려 본 것',
|
|
218
|
+
'export.nothingRan': '돌린 명령이 없습니다.',
|
|
219
|
+
'export.unproven': '증명 안 된 것',
|
|
220
|
+
'export.allProven': '없습니다 — 바꾼 것마다 뒤에 돌린 확인이 있습니다.',
|
|
221
|
+
'export.footer': 'deel 이 만든 보고서입니다. 바깥으로 나가는 주소가 없어 어느 망에서든 열립니다.',
|
|
222
|
+
'export.saved': '보고서를 남겼습니다:',
|
|
223
|
+
'export.openHint': '더블클릭으로 열립니다. 그대로 결재·보고에 첨부하면 됩니다.',
|
|
224
|
+
'export.failed': '보고서를 못 남겼습니다. 폴더 권한을 확인해 주세요.',
|
|
225
|
+
|
|
205
226
|
|
|
206
227
|
// ── 도움말 · 승인 방식 · 수준 ─────────────────────────────────────────
|
|
207
228
|
'help.titleCommon': '명령 (자주 쓰는 것)',
|
package/src/preview/serve.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* 4. 파일을 **주기만** 한다. PUT·POST·DELETE 는 받지 않는다.
|
|
23
23
|
*/
|
|
24
24
|
import { createServer } from 'node:http';
|
|
25
|
-
import { createReadStream, statSync, existsSync, readdirSync, watch } from 'node:fs';
|
|
25
|
+
import { createReadStream, statSync, existsSync, readdirSync, watch, realpathSync } from 'node:fs';
|
|
26
26
|
import { join, extname, relative, sep } from 'node:path';
|
|
27
27
|
import { spawn } from 'node:child_process';
|
|
28
28
|
|
|
@@ -60,12 +60,21 @@ const 되살림 = `
|
|
|
60
60
|
|
|
61
61
|
const 안전한경로 = (u) => {
|
|
62
62
|
// %00 이나 널바이트가 섞이면 아래 fs 가 경로를 잘라 읽는다. 통째로 거절한다.
|
|
63
|
+
// `\` 도 거절한다 — 앞의 `/` 는 지우지만 `\evil.com` 처럼 역슬래시로 시작하면
|
|
64
|
+
// 남아 있다가, 밑에서 `/${길}/` 로 리다이렉트 주소를 만들 때 일부 브라우저가
|
|
65
|
+
// `\` 를 `/` 로 봐서 `//evil.com/` 처럼 다른 도메인으로 새는 통로가 된다.
|
|
63
66
|
let p;
|
|
64
67
|
try { p = decodeURIComponent(String(u).split('?')[0].split('#')[0]); } catch { return null; }
|
|
65
|
-
if (p.includes('\0')) return null;
|
|
68
|
+
if (p.includes('\0') || p.includes('\\')) return null;
|
|
66
69
|
return p.replace(/^\/+/, '');
|
|
67
70
|
};
|
|
68
71
|
|
|
72
|
+
/** 이용자 파일 이름을 HTML 안에 그대로 꽂기 전에 씌운다 — 파일 이름에
|
|
73
|
+
* `<script>` 가 들어 있어도 태그로 안 읽히게. */
|
|
74
|
+
const html씌우기 = (s) => String(s).replace(/[&<>"']/g, (c) => ({
|
|
75
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
76
|
+
}[c]));
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
79
|
* 폴더 하나를 띄운다.
|
|
71
80
|
*
|
|
@@ -181,7 +190,24 @@ export function 띄우기({ 뿌리, scope, 되살리기 = true }) {
|
|
|
181
190
|
|
|
182
191
|
if (되살리기) {
|
|
183
192
|
try {
|
|
184
|
-
|
|
193
|
+
/*
|
|
194
|
+
* 감시할 자리는 **긴 이름으로 바꿔서** 준다.
|
|
195
|
+
*
|
|
196
|
+
* 윈도우가 알려 주는 파일 이름은 긴 이름인데, 우리가 준 자리가 짧은
|
|
197
|
+
* 이름(`RUNNER~1` 같은 8.3 이름)이거나 정션 너머면 둘이 안 맞는다.
|
|
198
|
+
* 그때 libuv 는 잡을 수 있는 오류를 내지 않고 **그냥 죽는다** —
|
|
199
|
+
*
|
|
200
|
+
* Assertion failed: !_wcsnicmp(filename, dir, dirlen),
|
|
201
|
+
* file src\win\fs-event.c, line 72
|
|
202
|
+
*
|
|
203
|
+
* try 로 감싸도 소용없다. assert 는 프로세스를 통째로 끝낸다.
|
|
204
|
+
* 미리보기 한 번 켰다가 deel 이 사라지는 셈이고, 화면에는 아무
|
|
205
|
+
* 이유도 안 남는다. GitHub 의 윈도우 러너 임시 폴더가 정확히 그
|
|
206
|
+
* 모양이라 검사가 거기서만 죽었고, 그 덕에 찾았다.
|
|
207
|
+
*/
|
|
208
|
+
let 감시자리 = 뿌리;
|
|
209
|
+
try { 감시자리 = realpathSync.native(뿌리); } catch { /* 못 바꾸면 준 대로 */ }
|
|
210
|
+
감시 = watch(감시자리, { recursive: true }, () => {
|
|
185
211
|
// 저장 한 번에 이벤트가 여러 개 온다. 몰아서 한 번만 알린다.
|
|
186
212
|
clearTimeout(늦추기);
|
|
187
213
|
늦추기 = setTimeout(알리기, 120);
|
|
@@ -277,15 +303,20 @@ function 목록주기(abs, 뿌리, res) {
|
|
|
277
303
|
let 것들 = [];
|
|
278
304
|
try { 것들 = readdirSync(abs, { withFileTypes: true }); } catch { /* 못 읽으면 빈 채로 */ }
|
|
279
305
|
const 여기 = relative(뿌리, abs).split(sep).join('/');
|
|
306
|
+
// 파일 이름은 디스크에서 온 것이라 `<script>` 같은 것이 그대로 들어 있을 수
|
|
307
|
+
// 있다 — 씌우지 않고 꽂으면 그 파일이 있는 폴더를 미리보기로 여는 사람 화면에서
|
|
308
|
+
// 그대로 실행된다(Stored XSS). href 는 이미 encodeURIComponent 로 씌웠지만
|
|
309
|
+
// 화면에 보이는 글자는 안 씌웠던 것이 문제였다.
|
|
280
310
|
const 줄 = (e) => {
|
|
281
|
-
const 이름 = e.name + (e.isDirectory() ? '/' : '');
|
|
311
|
+
const 이름 = html씌우기(e.name) + (e.isDirectory() ? '/' : '');
|
|
282
312
|
return `<li><a href="${encodeURIComponent(e.name)}${e.isDirectory() ? '/' : ''}">${이름}</a></li>`;
|
|
283
313
|
};
|
|
284
|
-
const
|
|
314
|
+
const 여기씌움 = html씌우기(여기);
|
|
315
|
+
const 몸 = `<!doctype html><meta charset="utf-8"><title>${여기씌움 || '/'}</title>`
|
|
285
316
|
+ '<style>body{font:14px/1.7 ui-monospace,monospace;max-width:44rem;margin:3rem auto;padding:0 1rem}'
|
|
286
317
|
+ 'h1{font-size:1rem;color:#888;font-weight:400}a{color:#0a7}li{list-style:none}'
|
|
287
318
|
+ 'p{color:#999}@media(prefers-color-scheme:dark){body{background:#111;color:#ddd}a{color:#4d9}}</style>'
|
|
288
|
-
+ `<h1>/${
|
|
319
|
+
+ `<h1>/${여기씌움}</h1>`
|
|
289
320
|
+ (여기 ? '<li><a href="../">../</a></li>' : '')
|
|
290
321
|
+ 것들.map(줄).join('')
|
|
291
322
|
+ '<p>index.html 이 없어서 목록을 보여 줍니다.</p>';
|
package/src/safety/guard.js
CHANGED
|
@@ -81,8 +81,11 @@ const BLOCKED = [
|
|
|
81
81
|
{ re: /\bdel\b[^|;&]*\s\/[sq]\b[^|;&]*(\\\*|\s[a-z]:\\?\s*$)/i, why: '하위 폴더까지 전부 지웁니다' },
|
|
82
82
|
// 파워셸도 같은 일을 한다. 드라이브 뿌리나 집 폴더를 통째로 미는 것만 본다 —
|
|
83
83
|
// 폴더 하나 지우는 평범한 Remove-Item 까지 막으면 도구를 못 쓴다.
|
|
84
|
+
// 가운데 `(-?[a-z]*\s*)*` 는 양쪽 다 빈 문자열을 허용해서, 안 맞는 긴 입력이
|
|
85
|
+
// 들어오면 되돌아가는 경우의 수가 기하급수로 늘어난다 — guard 검사 자체가
|
|
86
|
+
// 멈춰 버린다(ReDoS). 각 겹이 최소 한 글자는 먹게 고쳐서 그 길을 막는다.
|
|
84
87
|
{
|
|
85
|
-
re: /Remove-Item\b[^|;&]*\s-(Recurse|r)\b[^|;&]*\s(
|
|
88
|
+
re: /Remove-Item\b[^|;&]*\s-(Recurse|r)\b[^|;&]*\s(?:-?[a-z]+\s+)*([a-z]:\\?|~|\$HOME|\$env:USERPROFILE)\s*$/i,
|
|
86
89
|
why: '드라이브나 집 폴더를 통째로 지웁니다',
|
|
87
90
|
},
|
|
88
91
|
{ re: /git\s+push\b[^|;&]*--force(?!-with-lease)/i, why: '원격 이력을 덮어씁니다 (--force-with-lease 를 쓰세요)' },
|
package/src/tools/desc.en.js
CHANGED
|
@@ -31,8 +31,10 @@
|
|
|
31
31
|
export const 도구설명EN = {
|
|
32
32
|
Read: {
|
|
33
33
|
desc: 'Read one file. Line numbers come back with it. You must read a file before editing it.'
|
|
34
|
-
+ ' Excel files (.xlsx/.xlsm/.xls) can be read directly too — they come back as CSV per sheet
|
|
35
|
-
+ '
|
|
34
|
+
+ ' Excel files (.xlsx/.xlsm/.xls) can be read directly too — they come back as CSV per sheet.'
|
|
35
|
+
+ ' Korean HWP, Word and PowerPoint documents (.hwpx/.docx/.pptx) read directly as well —'
|
|
36
|
+
+ ' they come back as plain text. There is no need to ask the user to export anything.'
|
|
37
|
+
+ ' All of these are read-only here, though.',
|
|
36
38
|
params: {
|
|
37
39
|
file_path: 'path of the file to read',
|
|
38
40
|
offset: 'first line (1-based). Only for large files',
|