deel-local-cli 1.5.0 → 1.5.3
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 +1001 -997
- package/README.md +992 -988
- package/package.json +60 -60
- package/src/commands.js +99 -0
- package/src/i18n/en.js +16 -1
- package/src/i18n/index.js +52 -3
- package/src/i18n/ko.js +18 -3
- package/src/repl.js +59 -19
- package/src/ui/inputbox.js +633 -557
- package/src/ui/motion.js +20 -1
- package/src/ui/office.js +124 -14
- package/src/ui/screen.js +17 -0
- package/src/ui/status.js +13 -3
- package/src/ui/wrap.js +36 -2
package/package.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "deel-local-cli",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"description": "로컬 모델·사내 게이트웨이 전용 코딩 에이전트 CLI — 외부 의존성 0개 / Zero-dependency coding agent CLI for local LLMs and private gateways",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"bin": {
|
|
8
|
-
"deel": "bin/deel.js"
|
|
9
|
-
},
|
|
10
|
-
"engines": {
|
|
11
|
-
"node": ">=20"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"bin",
|
|
15
|
-
"src",
|
|
16
|
-
"README.md",
|
|
17
|
-
"README.en.md",
|
|
18
|
-
"LICENSE"
|
|
19
|
-
],
|
|
20
|
-
"keywords": [
|
|
21
|
-
"cli",
|
|
22
|
-
"coding-agent",
|
|
23
|
-
"local-llm",
|
|
24
|
-
"ollama",
|
|
25
|
-
"lm-studio",
|
|
26
|
-
"openai-compatible",
|
|
27
|
-
"offline",
|
|
28
|
-
"air-gapped",
|
|
29
|
-
"zero-dependency",
|
|
30
|
-
"agent",
|
|
31
|
-
"acp",
|
|
32
|
-
"agent-client-protocol",
|
|
33
|
-
"zed",
|
|
34
|
-
"korean"
|
|
35
|
-
],
|
|
36
|
-
"repository": {
|
|
37
|
-
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/jysvai/deel-local-cli.git"
|
|
39
|
-
},
|
|
40
|
-
"bugs": {
|
|
41
|
-
"url": "https://github.com/jysvai/deel-local-cli/issues"
|
|
42
|
-
},
|
|
43
|
-
"homepage": "https://github.com/jysvai/deel-local-cli#readme",
|
|
44
|
-
"scripts": {
|
|
45
|
-
"start": "node bin/deel.js",
|
|
46
|
-
"setup": "node bin/deel.js setup",
|
|
47
|
-
"diagnose": "node bin/deel.js diagnose",
|
|
48
|
-
"chat": "node bin/deel.js",
|
|
49
|
-
"test": "node test/run.mjs",
|
|
50
|
-
"docs": "node tools/check-docs.mjs",
|
|
51
|
-
"verify": "node test/no-bundle.test.js && node test/network.test.js && node test/web.test.js",
|
|
52
|
-
"bench": "node test/edit-bench.js",
|
|
53
|
-
"demo": "node test/demo.js",
|
|
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/office.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",
|
|
55
|
-
"coverage": "node test/coverage.mjs",
|
|
56
|
-
"prepublishOnly": "npm run check && npm test"
|
|
57
|
-
},
|
|
58
|
-
"dependencies": {},
|
|
59
|
-
"devDependencies": {}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "deel-local-cli",
|
|
3
|
+
"version": "1.5.3",
|
|
4
|
+
"description": "로컬 모델·사내 게이트웨이 전용 코딩 에이전트 CLI — 외부 의존성 0개 / Zero-dependency coding agent CLI for local LLMs and private gateways",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"deel": "bin/deel.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"src",
|
|
16
|
+
"README.md",
|
|
17
|
+
"README.en.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"cli",
|
|
22
|
+
"coding-agent",
|
|
23
|
+
"local-llm",
|
|
24
|
+
"ollama",
|
|
25
|
+
"lm-studio",
|
|
26
|
+
"openai-compatible",
|
|
27
|
+
"offline",
|
|
28
|
+
"air-gapped",
|
|
29
|
+
"zero-dependency",
|
|
30
|
+
"agent",
|
|
31
|
+
"acp",
|
|
32
|
+
"agent-client-protocol",
|
|
33
|
+
"zed",
|
|
34
|
+
"korean"
|
|
35
|
+
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/jysvai/deel-local-cli.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/jysvai/deel-local-cli/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/jysvai/deel-local-cli#readme",
|
|
44
|
+
"scripts": {
|
|
45
|
+
"start": "node bin/deel.js",
|
|
46
|
+
"setup": "node bin/deel.js setup",
|
|
47
|
+
"diagnose": "node bin/deel.js diagnose",
|
|
48
|
+
"chat": "node bin/deel.js",
|
|
49
|
+
"test": "node test/run.mjs",
|
|
50
|
+
"docs": "node tools/check-docs.mjs",
|
|
51
|
+
"verify": "node test/no-bundle.test.js && node test/network.test.js && node test/web.test.js",
|
|
52
|
+
"bench": "node test/edit-bench.js",
|
|
53
|
+
"demo": "node test/demo.js",
|
|
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/office.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/wrap.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",
|
|
55
|
+
"coverage": "node test/coverage.mjs",
|
|
56
|
+
"prepublishOnly": "npm run check && npm test"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {},
|
|
59
|
+
"devDependencies": {}
|
|
60
|
+
}
|
package/src/commands.js
CHANGED
|
@@ -19,6 +19,27 @@ import { 보고서적기 } from './ui/export.js';
|
|
|
19
19
|
import { loadCommand, discover } from './skills/discover.js';
|
|
20
20
|
import { install, list, remove, pack } from './plugins/manage.js';
|
|
21
21
|
import { spin } from './ui/spinner.js';
|
|
22
|
+
import { 그림고르기설정, 끔설정 } from './ui/motion.js';
|
|
23
|
+
import { 사무실설정, 최소높이, 최소폭 } from './ui/office.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 설정에 적힌 그림을 화면 쪽에 물린다.
|
|
27
|
+
*
|
|
28
|
+
* 켤 때(repl·oneshot)와 `/motion` 으로 바꿀 때가 **같은 길을 타야** 한다.
|
|
29
|
+
* 갈라 두면 명령으로 바꾼 것이 다음에 켤 때 안 살아나거나 그 반대가 된다.
|
|
30
|
+
*
|
|
31
|
+
* 사무실은 그림 하나가 아니라 화면 아래를 통째로 쓰는 것이라 따로 켠다.
|
|
32
|
+
* '사무실' 을 골랐으면 상자 안 그림은 조용한 돌림표로 돌아간다 — 둘 다
|
|
33
|
+
* 같은 것을 말하므로 나란히 두면 같은 소리를 두 번 하는 셈이다.
|
|
34
|
+
*/
|
|
35
|
+
export function 적용하기(고른것) {
|
|
36
|
+
const 값 = String(고른것 ?? '기본');
|
|
37
|
+
사무실설정(값 === '사무실');
|
|
38
|
+
그림고르기설정(값 === '사무실' ? '기본' : 값);
|
|
39
|
+
// 환경변수(DEEL_NO_MOTION)는 안 건드린다 — 사람이 직접 넣어 둔 것을 명령이
|
|
40
|
+
// 지워 버리면, 껐다고 믿는 자리에서 그림이 다시 돈다. 설정용 값은 따로 둔다.
|
|
41
|
+
끔설정(값 === '끔');
|
|
42
|
+
}
|
|
22
43
|
import { PROFILES, LEVELS as THINK_LEVELS, normalizeProfile, table as effortTable } from './agent/effort.js';
|
|
23
44
|
import { scanLocal, toProfiles } from './backend/scan.js';
|
|
24
45
|
import { list as listSessions } from './agent/store.js';
|
|
@@ -89,6 +110,7 @@ const 명령들 = {
|
|
|
89
110
|
memory: { arg: true },
|
|
90
111
|
level: { arg: true },
|
|
91
112
|
bell: { arg: true },
|
|
113
|
+
motion: { arg: true },
|
|
92
114
|
consult: { arg: true },
|
|
93
115
|
lang: { arg: true },
|
|
94
116
|
lsp: { arg: true },
|
|
@@ -431,6 +453,83 @@ export async function handle(line, session, ctx) {
|
|
|
431
453
|
say('');
|
|
432
454
|
return { handled: true };
|
|
433
455
|
}
|
|
456
|
+
/*
|
|
457
|
+
* 일하는 동안 뭐가 도나.
|
|
458
|
+
*
|
|
459
|
+
* 예전에는 DEEL_MOTION 과 DEEL_OFFICE 두 환경변수였다. 켜 보려면 터미널을
|
|
460
|
+
* 껐다 켜야 했고, 이름도 둘을 따로 외워야 했다 — 재미로 넣은 것을 켜는 데
|
|
461
|
+
* 그만한 품이 들면 아무도 안 켠다. 명령 하나로 합치고 그 자리에서 바뀐다.
|
|
462
|
+
*/
|
|
463
|
+
case 'motion': {
|
|
464
|
+
const cfg = load();
|
|
465
|
+
const 고를것 = [
|
|
466
|
+
{ 값: '기본', 별명: ['기본', 'default', 'plain', '돌림표'], 설명: 말('motion.plain') },
|
|
467
|
+
{ 값: '기사', 별명: ['기사', 'knight'], 설명: 말('motion.knight') },
|
|
468
|
+
{ 값: '동물', 별명: ['동물', 'animal', 'animals'], 설명: 말('motion.animal') },
|
|
469
|
+
{ 값: '사무실', 별명: ['사무실', 'office'], 설명: 말('motion.office') },
|
|
470
|
+
{ 값: '끔', 별명: ['끔', 'off', '꺼', '끄기', 'none'], 설명: 말('motion.off') },
|
|
471
|
+
];
|
|
472
|
+
const 지금값 = cfg.motion ?? '기본';
|
|
473
|
+
const 값 = String(arg ?? '').trim().toLowerCase();
|
|
474
|
+
|
|
475
|
+
if (!값) {
|
|
476
|
+
say('');
|
|
477
|
+
say(` ${c.gray(말('motion.now'))} ${c.hcyan(지금값)}`);
|
|
478
|
+
say('');
|
|
479
|
+
for (const 것 of 고를것) {
|
|
480
|
+
const 표 = 것.값 === 지금값 ? c.hgreen('●') : c.gray('○');
|
|
481
|
+
// padEnd 가 아니라 pad — 한글은 한 글자가 두 칸이라
|
|
482
|
+
// 글자 수로 채우면 '사무실' 줄만 두 칸 밀린다.
|
|
483
|
+
say(` ${표} ${c.cyan(pad(`/motion ${것.값}`, 18))} ${c.gray(것.설명)}`);
|
|
484
|
+
}
|
|
485
|
+
say('');
|
|
486
|
+
// 사무실은 화면이 좁거나 낮으면 안 뜬다. 켜 놓고 안 보이면 고장으로
|
|
487
|
+
// 보이므로, 지금 이 터미널이 되는지를 여기서 미리 말해 준다.
|
|
488
|
+
const 줄 = process.stdout.rows ?? 0;
|
|
489
|
+
const 칸 = process.stdout.columns ?? 0;
|
|
490
|
+
if (줄 && 칸 && (줄 < 최소높이 || 칸 < 최소폭)) {
|
|
491
|
+
say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.tooSmall', { 줄, 칸, 최소줄: 최소높이, 최소칸: 최소폭 }))}`);
|
|
492
|
+
say('');
|
|
493
|
+
}
|
|
494
|
+
if (process.env.DEEL_MOTION || process.env.DEEL_OFFICE) {
|
|
495
|
+
say(` ${c.gray(말('motion.envWins'))}`);
|
|
496
|
+
say('');
|
|
497
|
+
}
|
|
498
|
+
return { handled: true };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const 고른것 = 고를것.find((것) => 것.별명.includes(값));
|
|
502
|
+
if (!고른것) {
|
|
503
|
+
say('');
|
|
504
|
+
say(` ${c.gray(말('motion.unknown', { 값: arg }))}`);
|
|
505
|
+
say(` ${c.gray(말('motion.pickOne'))} ${고를것.map((것) => c.cyan(것.값)).join(c.gray(' · '))}`);
|
|
506
|
+
say('');
|
|
507
|
+
return { handled: true };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
cfg.motion = 고른것.값;
|
|
511
|
+
try { save(cfg); } catch { /* 못 남겨도 이번 세션에는 먹는다 */ }
|
|
512
|
+
적용하기(cfg.motion);
|
|
513
|
+
|
|
514
|
+
say('');
|
|
515
|
+
say(` ${mark.ok} ${말('motion.set', { 것: 고른것.값 })} ${c.gray('— ' + 고른것.설명)}`);
|
|
516
|
+
// 사무실을 골랐는데 이 터미널이 작으면 아무 일도 안 일어난 것처럼 보인다.
|
|
517
|
+
// 켰다고 말해 놓고 안 보이면 그게 고장이다. 그래서 그 자리에서 말한다.
|
|
518
|
+
if (고른것.값 === '사무실') {
|
|
519
|
+
const 줄 = process.stdout.rows ?? 0;
|
|
520
|
+
const 칸 = process.stdout.columns ?? 0;
|
|
521
|
+
if (줄 && 칸 && (줄 < 최소높이 || 칸 < 최소폭)) {
|
|
522
|
+
say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.tooSmall', { 줄, 칸, 최소줄: 최소높이, 최소칸: 최소폭 }))}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (process.env.DEEL_MOTION || process.env.DEEL_OFFICE) {
|
|
526
|
+
say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.envWins'))}`);
|
|
527
|
+
}
|
|
528
|
+
say(` ${c.gray(말('motion.appliesNow'))}`);
|
|
529
|
+
say('');
|
|
530
|
+
return { handled: true };
|
|
531
|
+
}
|
|
532
|
+
|
|
434
533
|
case 'exit':
|
|
435
534
|
case 'quit': return { handled: true, exit: true };
|
|
436
535
|
|
package/src/i18n/en.js
CHANGED
|
@@ -82,6 +82,8 @@ export const en = {
|
|
|
82
82
|
'cmd.level.arg': '[level]',
|
|
83
83
|
'cmd.bell.desc': 'bell and window title when a turn ends or deel needs you (on/off)',
|
|
84
84
|
'cmd.bell.arg': '[on|off]',
|
|
85
|
+
'cmd.motion.desc': 'what moves while deel works — knight, animals, office',
|
|
86
|
+
'cmd.motion.arg': '[knight|animal|office|default|off]',
|
|
85
87
|
'cmd.lang.desc': 'screen language — 한국어 / English',
|
|
86
88
|
'cmd.lang.arg': '[ko|en]',
|
|
87
89
|
'cmd.init.desc': 'create a DEEL.md rules file',
|
|
@@ -129,7 +131,7 @@ export const en = {
|
|
|
129
131
|
'head.shiftTab': 'Shift+Tab',
|
|
130
132
|
'head.shiftTabHint': ' changes it · ',
|
|
131
133
|
'head.tabHint': 'Tab completes the / command you are typing',
|
|
132
|
-
'head.newlineHint': 'New line: Alt+Enter
|
|
134
|
+
'head.newlineHint': 'New line: end a line with space + ` then Enter — works everywhere (Alt+Enter only if your terminal sends it)',
|
|
133
135
|
'head.thisPC': 'found',
|
|
134
136
|
'head.skills': 'skills {n}',
|
|
135
137
|
'head.commands': 'commands {n}',
|
|
@@ -184,6 +186,19 @@ export const en = {
|
|
|
184
186
|
'bell.justRang': 'That was one ring. If you heard nothing, check your terminal’s bell setting.',
|
|
185
187
|
'bell.appliesNow': 'Applies to this session right away.',
|
|
186
188
|
|
|
189
|
+
'motion.now': 'Now:',
|
|
190
|
+
'motion.plain': 'a one-cell spinner. Quiet',
|
|
191
|
+
'motion.knight': 'a knight swings a sword. Sometimes gets flattened',
|
|
192
|
+
'motion.animal': 'pixel animals wander about',
|
|
193
|
+
'motion.office': 'twelve rows of office above the prompt — the work as a room',
|
|
194
|
+
'motion.off': 'no drawing at all (for screen-reader users)',
|
|
195
|
+
'motion.set': 'Switched to {것}.',
|
|
196
|
+
'motion.appliesNow': 'Visible from the next frame. Saved to your config too.',
|
|
197
|
+
'motion.unknown': '"{값}" is not one of the drawings.',
|
|
198
|
+
'motion.pickOne': 'Pick one of:',
|
|
199
|
+
'motion.envWins': 'DEEL_MOTION / DEEL_OFFICE is set, and the environment wins.',
|
|
200
|
+
'motion.tooSmall': 'This terminal is {줄} x {칸} — the office needs at least {최소줄} rows and {최소칸} columns.',
|
|
201
|
+
|
|
187
202
|
// ── Screen language ───────────────────────────────────────────────────
|
|
188
203
|
'lang.now': 'Screen language is {이름}.',
|
|
189
204
|
'lang.ko': '한국어 (Korean)',
|
package/src/i18n/index.js
CHANGED
|
@@ -70,11 +70,54 @@ export function 언어잡기({ env = process.env, cfg = null } = {}) {
|
|
|
70
70
|
return 지금;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/*
|
|
74
|
+
* 조사는 앞 글자의 받침을 보고 갈린다.
|
|
75
|
+
*
|
|
76
|
+
* 끼워 넣는 값이 그때그때 달라지는 자리에서 조사를 글에 박아 두면
|
|
77
|
+
* "기사 으로 바꿨습니다" 같은 것이 나온다. 안 터지고, 검사도 안 잡고,
|
|
78
|
+
* 화면에서만 어색하다 — 그래서 오래 남는다.
|
|
79
|
+
*
|
|
80
|
+
* `{것:으로}` 처럼 자리 이름 뒤에 조사를 적으면 값을 보고 골라 붙인다.
|
|
81
|
+
* 영어 글에는 안 적으면 그만이라 `{것}` 그대로 둬도 된다.
|
|
82
|
+
*
|
|
83
|
+
* 값과 조사 사이에 뭔가 끼어야 할 때 — 따옴표가 대표적이다 — 이름 앞에
|
|
84
|
+
* `~` 를 붙이면 **조사만** 나온다: `"{값}"{~값:은는}` → `"없는거"는`.
|
|
85
|
+
*/
|
|
86
|
+
const 조사표 = {
|
|
87
|
+
은는: ['은', '는'],
|
|
88
|
+
이가: ['이', '가'],
|
|
89
|
+
을를: ['을', '를'],
|
|
90
|
+
과와: ['과', '와'],
|
|
91
|
+
으로: ['으로', '로'],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 이 값 뒤에 붙일 조사.
|
|
96
|
+
*
|
|
97
|
+
* 한글이 아닌 것으로 끝나면(영어 낱말·기호) **받침 없는 쪽**을 씁니다.
|
|
98
|
+
* 한국어 화면에 들어오는 값은 거의 한글이라 여기까지 오는 일이 드물고,
|
|
99
|
+
* 여기서 더 따지면 규칙만 늘고 맞는 자리는 안 늘어납니다.
|
|
100
|
+
*/
|
|
101
|
+
export function 조사붙이기(값, 갈래) {
|
|
102
|
+
const 짝 = 조사표[갈래];
|
|
103
|
+
if (!짝) return '';
|
|
104
|
+
// 색 입힌 값이 그대로 들어온다(`\x1b[92m⌂\x1b[0m`). 벗기지 않으면 마지막
|
|
105
|
+
// 글자가 언제나 리셋 문자열의 'm' 이라, 무슨 값이 와도 답이 하나로 굳는다.
|
|
106
|
+
const 끝 = String(값 ?? '').replace(/\x1b\[[0-9;]*m/g, '').trimEnd().slice(-1);
|
|
107
|
+
const cp = 끝.codePointAt(0);
|
|
108
|
+
if (cp === undefined || cp < 0xac00 || cp > 0xd7a3) return 짝[1];
|
|
109
|
+
const 받침 = (cp - 0xac00) % 28;
|
|
110
|
+
if (받침 === 0) return 짝[1];
|
|
111
|
+
// `으로` 만 다르다. ㄹ(받침 8) 뒤에서는 받침이 없는 것처럼 '로' 가 붙는다.
|
|
112
|
+
if (갈래 === '으로' && 받침 === 8) return 짝[1];
|
|
113
|
+
return 짝[0];
|
|
114
|
+
}
|
|
115
|
+
|
|
73
116
|
/**
|
|
74
117
|
* 말 한 마디.
|
|
75
118
|
*
|
|
76
119
|
* @param {string} 열쇠
|
|
77
|
-
* @param {Record<string, string|number>} [채움] `{이름}` 자리에 끼워 넣을 것
|
|
120
|
+
* @param {Record<string, string|number>} [채움] `{이름}` · `{이름:조사}` 자리에 끼워 넣을 것
|
|
78
121
|
* @returns {string}
|
|
79
122
|
*/
|
|
80
123
|
export function 말(열쇠, 채움 = null) {
|
|
@@ -93,8 +136,14 @@ export function 말(열쇠, 채움 = null) {
|
|
|
93
136
|
* 안 준 자리는 그대로 둔다. 지워 버리면 "0개 턴" 처럼 틀린 말이 되고,
|
|
94
137
|
* 틀린 숫자는 안 적힌 것보다 나쁘다.
|
|
95
138
|
*/
|
|
96
|
-
return String(글).replace(/\{([^{}\s]+)\}/g, (통째,
|
|
97
|
-
|
|
139
|
+
return String(글).replace(/\{([^{}\s:]+)(?::([^{}\s]+))?\}/g, (통째, 이름, 갈래) => {
|
|
140
|
+
const 조사만 = 이름.startsWith('~');
|
|
141
|
+
const 진짜이름 = 조사만 ? 이름.slice(1) : 이름;
|
|
142
|
+
if (!Object.prototype.hasOwnProperty.call(채움, 진짜이름)) return 통째;
|
|
143
|
+
const 값 = String(채움[진짜이름]);
|
|
144
|
+
if (조사만) return 갈래 ? 조사붙이기(값, 갈래) : '';
|
|
145
|
+
return 갈래 ? 값 + 조사붙이기(값, 갈래) : 값;
|
|
146
|
+
});
|
|
98
147
|
}
|
|
99
148
|
|
|
100
149
|
/** 이 열쇠가 지금 언어로 옮겨져 있나. 검사와 /lang 이 본다. */
|
package/src/i18n/ko.js
CHANGED
|
@@ -69,6 +69,8 @@ export const ko = {
|
|
|
69
69
|
'cmd.level.arg': '[수준]',
|
|
70
70
|
'cmd.bell.desc': '다 됐을 때·물어볼 때 종소리와 창 제목 (on/off)',
|
|
71
71
|
'cmd.bell.arg': '[on|off]',
|
|
72
|
+
'cmd.motion.desc': '일하는 동안 뭐가 도나 — 기사·동물·사무실',
|
|
73
|
+
'cmd.motion.arg': '[기사|동물|사무실|기본|끔]',
|
|
72
74
|
'cmd.lang.desc': '화면 말 — 한국어 / English',
|
|
73
75
|
'cmd.lang.arg': '[ko|en]',
|
|
74
76
|
'cmd.init.desc': 'DEEL.md 규칙 파일 만들기',
|
|
@@ -103,7 +105,7 @@ export const ko = {
|
|
|
103
105
|
'head.outside': '바깥',
|
|
104
106
|
'head.offlineLock': '[오프라인 잠금]',
|
|
105
107
|
'head.nowhereElse': '← 여기 말고는 어디로도 안 갑니다',
|
|
106
|
-
'head.glyphHint': '상태줄 맨 앞 {
|
|
108
|
+
'head.glyphHint': '상태줄 맨 앞 {안:이가} 이 뜻입니다. 바깥으로 가면 {밖:으로} 바뀝니다.',
|
|
107
109
|
'head.conn': '연결',
|
|
108
110
|
'head.streaming': '스트리밍',
|
|
109
111
|
'head.noStreaming': '스트리밍 없음',
|
|
@@ -116,7 +118,7 @@ export const ko = {
|
|
|
116
118
|
'head.shiftTab': 'Shift+Tab',
|
|
117
119
|
'head.shiftTabHint': '으로 바꿉니다 · ',
|
|
118
120
|
'head.tabHint': 'Tab 은 치던 / 명령을 채웁니다',
|
|
119
|
-
'head.newlineHint': '줄바꿈:
|
|
121
|
+
'head.newlineHint': '줄바꿈: 줄 끝에 빈칸+` 를 붙이고 Enter — 어디서나 됩니다 (Alt+Enter 는 터미널이 보내 줄 때만)',
|
|
120
122
|
'head.thisPC': '이 PC',
|
|
121
123
|
'head.skills': '스킬 {n}',
|
|
122
124
|
'head.commands': '명령 {n}',
|
|
@@ -171,12 +173,25 @@ export const ko = {
|
|
|
171
173
|
'bell.justRang': '방금 한 번 울렸습니다. 안 들렸으면 터미널 설정에서 소리를 확인해 주세요.',
|
|
172
174
|
'bell.appliesNow': '이 세션에도 바로 먹습니다.',
|
|
173
175
|
|
|
176
|
+
'motion.now': '지금:',
|
|
177
|
+
'motion.plain': '한 칸짜리 돌림표. 조용합니다',
|
|
178
|
+
'motion.knight': '중세 기사가 칼을 휘두릅니다. 가끔 깔려서 넘어집니다',
|
|
179
|
+
'motion.animal': '픽셀 동물들이 돌아다닙니다',
|
|
180
|
+
'motion.office': '입력칸 위에 사무실 12줄. 지금 무슨 일이 도는지를 방으로',
|
|
181
|
+
'motion.off': '그림을 아예 끕니다 (화면 읽기 프로그램을 쓰는 자리)',
|
|
182
|
+
'motion.set': '{것:으로} 바꿨습니다.',
|
|
183
|
+
'motion.appliesNow': '다음 판부터 바로 보입니다. 설정에도 남았습니다.',
|
|
184
|
+
'motion.unknown': '"{값}"{~값:은는} 모르는 그림입니다.',
|
|
185
|
+
'motion.pickOne': '고를 수 있는 것:',
|
|
186
|
+
'motion.envWins': '환경변수(DEEL_MOTION·DEEL_OFFICE)가 켜져 있어 그쪽이 이깁니다.',
|
|
187
|
+
'motion.tooSmall': '이 터미널은 {줄}줄 × {칸}칸입니다 — 사무실은 {최소줄}줄 × {최소칸}칸부터 뜹니다.',
|
|
188
|
+
|
|
174
189
|
// ── 화면 말 (/lang) ───────────────────────────────────────────────────
|
|
175
190
|
'lang.now': '화면 말은 지금 {이름} 입니다.',
|
|
176
191
|
'lang.ko': '한국어',
|
|
177
192
|
'lang.en': 'English (영어)',
|
|
178
193
|
'lang.howto': '바꾸려면',
|
|
179
|
-
'lang.changed': '화면 말을 {
|
|
194
|
+
'lang.changed': '화면 말을 {이름:으로} 바꿨습니다.',
|
|
180
195
|
'lang.unknown': 'ko 나 en 으로 적어 주세요.',
|
|
181
196
|
'lang.progress': '{언어}: {옮김}/{전체} 마디가 옮겨져 있습니다. 나머지는 한국어로 나옵니다.',
|
|
182
197
|
'lang.partial': '아직 다 안 옮겼습니다 — 빈칸을 내는 대신 한국어를 그대로 냅니다.',
|
package/src/repl.js
CHANGED
|
@@ -9,7 +9,7 @@ import { 보이기 as 인트로, 기본곁말 } from './ui/intro.js';
|
|
|
9
9
|
import { 언어잡기 } from './i18n/index.js';
|
|
10
10
|
import { 화면고르기 } from './ui/screen.js';
|
|
11
11
|
import { STAGES } from './agent/effort.js';
|
|
12
|
-
import { handle, COMMANDS,
|
|
12
|
+
import { handle, COMMANDS, 미리보기끄기, 적용하기 as 그림적용 } from './commands.js';
|
|
13
13
|
import { next as nextWork, get as getWork, canWrite, 보일이름, 보일한줄 } from './agent/modes.js';
|
|
14
14
|
import { route } from './agent/route.js';
|
|
15
15
|
import { run } from './agent/loop.js';
|
|
@@ -121,6 +121,23 @@ const DIFF_LINES = { 쉬움: 14, 개발자: 40 };
|
|
|
121
121
|
// 모델의 말 앞에 세우는 세로줄. 도구 줄과 눈으로 갈리게 하는 유일한 표시다.
|
|
122
122
|
const 답표시 = c.hcyan('▌');
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* 줄 끝 백틱이 "이어쓰기" 표시인가.
|
|
126
|
+
*
|
|
127
|
+
* 예전에는 백틱 하나로 끝나기만 하면 표시로 봤다. 그런데 코딩 도구에서
|
|
128
|
+
* "read `config.json`" · "echo `date`" 처럼 **인라인 코드로 끝나는 줄**은
|
|
129
|
+
* 아주 흔하다. 그 줄들이 전부 안 보내지고, 닫는 백틱까지 뜯긴 채 이어쓰기
|
|
130
|
+
* 모드에 갇혔다 — 사람 눈에는 Enter 가 그냥 안 먹는 것으로 보인다.
|
|
131
|
+
*
|
|
132
|
+
* 그래서 조건을 둘로 좁혔다. 백틱 앞이 **빈칸**이고(또는 백틱만 있는 줄이고),
|
|
133
|
+
* 그 줄에 백틱이 **그것 하나뿐**일 때만 표시로 본다. 인라인 코드는 백틱이
|
|
134
|
+
* 둘이라 안 걸리고, ``` 울타리는 앞이 백틱이라 안 걸린다.
|
|
135
|
+
*
|
|
136
|
+
* 검사에서 표를 만들어 재려고 밖으로 뺐다.
|
|
137
|
+
*/
|
|
138
|
+
export const 이어쓰기표시 = (l) =>
|
|
139
|
+
/(^|\s)`$/.test(l) && (String(l).match(/`/g) ?? []).length === 1;
|
|
140
|
+
|
|
124
141
|
export async function chatLoop(opts = {}) {
|
|
125
142
|
const cfg = load();
|
|
126
143
|
const prof = activeProfile(cfg);
|
|
@@ -179,6 +196,10 @@ export async function chatLoop(opts = {}) {
|
|
|
179
196
|
*/
|
|
180
197
|
const 알림 = { 켬: cfg.bell !== false, 폴더: basename(root) };
|
|
181
198
|
|
|
199
|
+
// 지난번에 /motion 으로 골라 둔 그림을 되살린다. 화면을 세우기 **전에**
|
|
200
|
+
// 해야 첫 판부터 맞다 — 뒤에 하면 켜자마자 한 번은 기본 그림이 스친다.
|
|
201
|
+
그림적용(cfg.motion);
|
|
202
|
+
|
|
182
203
|
/*
|
|
183
204
|
* 화면 말을 여기서 한 번 정한다.
|
|
184
205
|
*
|
|
@@ -327,9 +348,12 @@ export async function chatLoop(opts = {}) {
|
|
|
327
348
|
* 메시지로 묶어 보낼 때 쓴다. 확정된 줄은 여기 쌓아 두다가, ` 없이 끝나는
|
|
328
349
|
* 줄이 오면 그때 \n 으로 이어붙여 한 메시지로 내보낸다.
|
|
329
350
|
*
|
|
330
|
-
* 실제 개행을 상자(rl.line) 안에
|
|
331
|
-
*
|
|
332
|
-
*
|
|
351
|
+
* 실제 개행을 상자(rl.line) 안에 넣어서는 **안 된다**. 화면은 멀쩡히
|
|
352
|
+
* 그려지지만, Enter 로 보낼 때 readline 의 history 가 그 줄을 \n 에서
|
|
353
|
+
* 쪼개 거꾸로 쌓고 \r 로 다시 붙여 돌려준다. "안녕\n반가워" 를 보내면
|
|
354
|
+
* 'line' 이벤트에 오는 값은 "반가워\r안녕" 이다 — 사람이 쓴 순서가
|
|
355
|
+
* 뒤집혀서 모델에게 간다. historySize 를 0 으로 두면 안 그러니 history 가
|
|
356
|
+
* 하는 짓이 맞다. 그래서 개행은 rl.line 밖, 여기서만 다룬다.
|
|
333
357
|
*
|
|
334
358
|
* 백슬래시(\) 가 아니라 백틱을 고른 것은 일부러다 — 이 프로그램은 윈도우
|
|
335
359
|
* 경로(`C:\Users\...`)를 늘 다루는데, 그 끝은 흔히 백슬래시로 끝난다.
|
|
@@ -338,6 +362,15 @@ export async function chatLoop(opts = {}) {
|
|
|
338
362
|
*/
|
|
339
363
|
let 이어쓰기줄들 = null;
|
|
340
364
|
|
|
365
|
+
/** 줄을 쌓아 두고 "다음 줄" 이라고 알린다. 백틱 표시와 Alt+Enter 가 같이 쓴다. */
|
|
366
|
+
const 줄쌓기 = (줄) => {
|
|
367
|
+
const 첫줄 = 이어쓰기줄들 === null;
|
|
368
|
+
(이어쓰기줄들 ??= []).push(줄);
|
|
369
|
+
화면.입력지움();
|
|
370
|
+
say(`${첫줄 ? ` ${c.hcyan('❯')} ` : ' '}${c.white(줄)}`);
|
|
371
|
+
say(` ${c.gray('… 다음 줄. 그냥 Enter 로 보냅니다 · Ctrl+C 로 취소')}`);
|
|
372
|
+
};
|
|
373
|
+
|
|
341
374
|
rl.on('line', (l) => {
|
|
342
375
|
if (echo) say(c.gray(l));
|
|
343
376
|
let 보낼것 = l;
|
|
@@ -346,16 +379,9 @@ export async function chatLoop(opts = {}) {
|
|
|
346
379
|
// 되묻는 자리: 답을 그 줄에 남긴 채 줄만 넘긴다.
|
|
347
380
|
process.stdout.write(`\r\x1b[2K${묻는중}${c.white(l)}\n`);
|
|
348
381
|
} else if (입력기다림) {
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (/(^|[^`])`$/.test(l)) {
|
|
353
|
-
const 줄 = l.slice(0, -1);
|
|
354
|
-
const 첫줄 = 이어쓰기줄들 === null;
|
|
355
|
-
(이어쓰기줄들 ??= []).push(줄);
|
|
356
|
-
화면.입력지움();
|
|
357
|
-
say(`${첫줄 ? ` ${c.hcyan('❯')} ` : ' '}${c.white(줄)}`);
|
|
358
|
-
say(` ${c.gray('… 다음 줄. 그냥 Enter 로 보냅니다')}`);
|
|
382
|
+
// 표시 자체와 그 앞 빈칸은 사람이 친 글이 아니라 문법이다. 같이 뗀다.
|
|
383
|
+
if (이어쓰기표시(l)) {
|
|
384
|
+
줄쌓기(l.slice(0, -1).replace(/\s+$/, ''));
|
|
359
385
|
return;
|
|
360
386
|
}
|
|
361
387
|
// 상자를 걷어내고, 사람이 보낸 글을 대화에 남긴다. 안 남기면 스크롤을
|
|
@@ -480,13 +506,16 @@ export async function chatLoop(opts = {}) {
|
|
|
480
506
|
* 뒤에 더 올 것을 기다리는 동안 우리 keypress 가 먼저 온다. 그래서
|
|
481
507
|
* key.meta 로 여기서 골라낼 수 있다. rl.write('\n') 은 못 쓴다 —
|
|
482
508
|
* 그것도 내부적으로 줄을 끝내는 처리를 그대로 타 버린다(직접 확인).
|
|
483
|
-
*
|
|
509
|
+
*
|
|
510
|
+
* 커서 앞은 확정해서 쌓고, 뒤는 상자에 남긴다. rl.line 에 \n 을
|
|
511
|
+
* 넣어 이어붙이지 **않는다** — 그러면 화면은 맞는데 보낼 때
|
|
512
|
+
* history 가 줄 순서를 뒤집는다(이어쓰기줄들 선언부 참고).
|
|
484
513
|
*/
|
|
485
514
|
if (key.meta && 입력기다림 && 묻는중 === null) {
|
|
486
|
-
const 앞 = (rl.line ?? '').slice(0, rl.cursor ?? 0);
|
|
487
515
|
const 뒤 = (rl.line ?? '').slice(rl.cursor ?? 0);
|
|
488
|
-
rl.line
|
|
489
|
-
rl.
|
|
516
|
+
줄쌓기((rl.line ?? '').slice(0, rl.cursor ?? 0));
|
|
517
|
+
rl.line = 뒤;
|
|
518
|
+
rl.cursor = 0;
|
|
490
519
|
} else {
|
|
491
520
|
return; // 줄이 끝나는 것은 'line' 이 맡는다
|
|
492
521
|
}
|
|
@@ -738,7 +767,7 @@ export async function chatLoop(opts = {}) {
|
|
|
738
767
|
}
|
|
739
768
|
|
|
740
769
|
// ── 머리말 ────────────────────────────────────────────────────────────
|
|
741
|
-
화면.머리말(headerLines(session, found));
|
|
770
|
+
화면.머리말(headerLines(session, found, 상자쓰나));
|
|
742
771
|
const warn = [];
|
|
743
772
|
// 홈 폴더에서 켠 경우. 작업 범위가 집 전체가 된다.
|
|
744
773
|
//
|
|
@@ -798,6 +827,17 @@ export async function chatLoop(opts = {}) {
|
|
|
798
827
|
let interrupted = false;
|
|
799
828
|
let turn = null; // 지금 도는 턴의 AbortController
|
|
800
829
|
rl.on('SIGINT', () => {
|
|
830
|
+
/*
|
|
831
|
+
* 쌓아 둔 줄이 있으면 먼저 버린다. 이게 없으면 이어쓰기를 **취소할 길이
|
|
832
|
+
* 없다** — 표를 하나 붙여 놓고 마음이 바뀌어도, 다음에 치는 것이 무엇이든
|
|
833
|
+
* 앞줄에 붙어서 나간다. `/help` 같은 슬래시 명령까지 앞줄에 붙는 순간
|
|
834
|
+
* 더는 '/' 로 시작하지 않아 명령으로 안 읽히고 모델에게 넘어간다.
|
|
835
|
+
*/
|
|
836
|
+
if (이어쓰기줄들 !== null) {
|
|
837
|
+
이어쓰기줄들 = null;
|
|
838
|
+
say('');
|
|
839
|
+
say(` ${c.gray('이어쓰던 것을 버렸습니다.')}`);
|
|
840
|
+
}
|
|
801
841
|
if (turn && !turn.signal.aborted) {
|
|
802
842
|
turn.abort();
|
|
803
843
|
return; // 화면 정리는 루프 쪽 'aborted' 이벤트가 한다
|