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/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "deel-local-cli",
3
- "version": "1.5.0",
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 (if your terminal sends it), or end a line with ` then 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
- (Object.prototype.hasOwnProperty.call(채움, 이름) ? String(채움[이름]) : 통째));
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': '줄바꿈: Alt+Enter(터미널이 지원하면) 또는 줄 끝에 ` 를 붙이고 Enter',
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, 미리보기끄기 } from './commands.js';
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) 안에 그대로 넣지는 않는다 접어쓰기() 가 \n 을
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
- * 그래서 rl.line·rl.cursor 를 손으로 이어붙인다.
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 = `${앞}\n${뒤}`;
489
- rl.cursor = 앞.length + 1;
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' 이벤트가 한다