leerness 1.9.188 → 1.9.190
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/CHANGELOG.md +209 -0
- package/README.md +3 -3
- package/bin/harness.js +117 -24
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,214 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.190 — 2026-05-21
|
|
4
|
+
|
|
5
|
+
**🚨 설치 가이드 Ctrl+C 미작동 BUG fix (사용자 명시 — npx 진행 차단).**
|
|
6
|
+
|
|
7
|
+
자율 모드 120 라운드. 사용자 명시 BUG:
|
|
8
|
+
> `npx --yes leerness@latest init` 으로 설치 가이드에서 Ctrl+C 누르면 설치 진행이 취소되어야 하는데, 진행되는 버그.
|
|
9
|
+
|
|
10
|
+
### 근본 원인 (코드 audit)
|
|
11
|
+
|
|
12
|
+
1.9.34~1.9.189 의 `_selectOne` / `_selectMany` 의 raw mode 코드:
|
|
13
|
+
```js
|
|
14
|
+
} else if (key === '\x03' || key === 'q' || key === '\x1b') {
|
|
15
|
+
cleanup();
|
|
16
|
+
stdout.write('\n 취소됨\n');
|
|
17
|
+
resolve(opts.defaultIndex != null ? options[opts.defaultIndex] : null); // ← BUG
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Ctrl+C (`\x03`)가 `q`/`\x1b`(ESC)와 같은 분기로 들어가서 **default 값 반환** → install 흐름이 default 옵션으로 계속 진행 → 사용자 의도(취소)와 정반대 동작.
|
|
22
|
+
|
|
23
|
+
또한 1.9.184에서 추가한 `process.on('SIGINT', _sigintHandler)` 의 2단계 confirm 은 readline raw mode 가 SIGINT signal 자체를 가로채서 호출되지 않음.
|
|
24
|
+
|
|
25
|
+
### Fix #1 — `_selectOne` / `_selectMany`: Ctrl+C 명시 분기 + 즉시 종료
|
|
26
|
+
|
|
27
|
+
```diff
|
|
28
|
+
} else if (key === '\x03') {
|
|
29
|
+
+ // 1.9.190 (사용자 명시 BUG fix): raw mode 에서 Ctrl+C → 즉시 설치 종료 (npx 진행 차단).
|
|
30
|
+
+ // 이전 (1.9.34~1.9.189): default 값 반환 → install 흐름 계속 진행 (사용자 BUG 보고).
|
|
31
|
+
cleanup();
|
|
32
|
+
stdout.write('\n \x1b[31m✗ 설치 중단됨 (Ctrl+C)\x1b[0m\n');
|
|
33
|
+
process.exit(130);
|
|
34
|
+
+} else if (key === '\x1b' || key === 'q' || key === 'Q') {
|
|
35
|
+
+ // ESC/q/Q → 단순 취소 (default 반환, install 흐름 계속)
|
|
36
|
+
cleanup();
|
|
37
|
+
stdout.write('\n 취소됨\n');
|
|
38
|
+
resolve(opts.defaultIndex != null ? options[opts.defaultIndex] : null);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Ctrl+C (\x03) 와 ESC/q 분기를 **완전 분리**:
|
|
43
|
+
- **Ctrl+C** → `process.exit(130)` 즉시 종료
|
|
44
|
+
- **ESC/q/Q** → default 값 반환 (취소만, 흐름 계속)
|
|
45
|
+
|
|
46
|
+
### Fix #2 — `ask()` readline SIGINT 명시 처리
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
function ask(question) {
|
|
50
|
+
return new Promise(resolve => {
|
|
51
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
52
|
+
// 1.9.190: readline 의 자체 SIGINT 처리 — Ctrl+C 시 즉시 종료.
|
|
53
|
+
rl.on('SIGINT', () => {
|
|
54
|
+
try { rl.close(); } catch {}
|
|
55
|
+
process.stdout.write('\n \x1b[31m✗ 설치 중단됨 (Ctrl+C)\x1b[0m\n');
|
|
56
|
+
process.exit(130);
|
|
57
|
+
});
|
|
58
|
+
rl.question(question, answer => { rl.close(); resolve(String(answer || '').trim()); });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
readline 의 `'SIGINT'` 이벤트 → 명시 처리 → 즉시 exit.
|
|
64
|
+
|
|
65
|
+
### Fix #3 — `install()` SIGINT 핸들러 단순화 (2단계 confirm 제거)
|
|
66
|
+
|
|
67
|
+
```diff
|
|
68
|
+
- // 1.9.184: 2단계 confirm (첫 누름 → 안내, 2초 내 두 번째 → 종료)
|
|
69
|
+
- let _sigintCount = 0; let _sigintTimer = null;
|
|
70
|
+
- const _sigintHandler = () => {
|
|
71
|
+
- _sigintCount++;
|
|
72
|
+
- if (_sigintCount === 1) {
|
|
73
|
+
- process.stdout.write('Ctrl+C 를 2초 이내에 한 번 더 누르면 종료됩니다...');
|
|
74
|
+
- _sigintTimer = setTimeout(() => { _sigintCount = 0; }, 2000);
|
|
75
|
+
- return;
|
|
76
|
+
- }
|
|
77
|
+
- process.exit(130);
|
|
78
|
+
- };
|
|
79
|
+
+ // 1.9.184+1.9.190: 설치 도중 Ctrl+C 시 즉시 종료 (사용자 명시 BUG fix — npx 진행 차단).
|
|
80
|
+
+ // readline raw mode 가 SIGINT 가로채서 1.9.184 2단계 confirm 동작 안 함 → 즉시 종료로 단순화.
|
|
81
|
+
+ const _sigintHandler = () => {
|
|
82
|
+
+ process.stdout.write('\n \x1b[31m✗ 설치 중단됨 (Ctrl+C)\x1b[0m\n');
|
|
83
|
+
+ process.exit(130);
|
|
84
|
+
+ };
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 시각 효과 (모든 fix 통합)
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
$ npx --yes leerness@latest init
|
|
91
|
+
🎁 leerness 설치 시작...
|
|
92
|
+
설치 언어를 선택하세요
|
|
93
|
+
↑↓ 이동, Enter 확정, q 취소
|
|
94
|
+
❯ 자동 감지
|
|
95
|
+
한국어
|
|
96
|
+
English
|
|
97
|
+
^C
|
|
98
|
+
✗ 설치 중단됨 (Ctrl+C)
|
|
99
|
+
$
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
(이전엔 Ctrl+C 후에도 설치가 계속 진행되어 default 값으로 모든 파일 생성됨 → 사용자 의도와 정반대)
|
|
103
|
+
|
|
104
|
+
### Verified
|
|
105
|
+
- stress-v135: **13/13 PASS** (사용자 명시 5 + 회귀 2 + 누적 6)
|
|
106
|
+
- e2e 217/217 baseline 유지
|
|
107
|
+
- `--yes` non-interactive install: 회귀 없음
|
|
108
|
+
- ESC/q 단순 취소: 회귀 없음 (default 반환)
|
|
109
|
+
- VERSION = 1.9.190 · autonomous-rounds = 120 · main 자동 push 51 라운드 연속
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 1.9.189 — 2026-05-21
|
|
114
|
+
|
|
115
|
+
**⌨️ "/" slash 명령 자동 list + Tab cycle 한 줄 갱신 (사용자 명시 3종).**
|
|
116
|
+
|
|
117
|
+
자율 모드 119 라운드. 사용자 명시:
|
|
118
|
+
1. *"권한 기본값 설정을 / 으로 설정해둘 수 있고 (모든 모델 공통)"* — `/` slash default trigger
|
|
119
|
+
2. *"/ 를 입력하면 관련 명령어가 첨부이미지처럼 나열"* — claude code 영감 UX
|
|
120
|
+
3. *"탭키로 모델 변경시, 채팅 이력으로 남아서 지져분"* — 한 줄 갱신 (in-place)
|
|
121
|
+
|
|
122
|
+
### Fix #1 — `/` slash 명령 자동 list
|
|
123
|
+
|
|
124
|
+
**Before** (claude code 화면 처럼 자동 안 됨):
|
|
125
|
+
```
|
|
126
|
+
agent[claude/actor/▶]> /
|
|
127
|
+
(처리 안 됨 또는 에러)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**After** (claude code 스타일):
|
|
131
|
+
```
|
|
132
|
+
agent[claude · opus-4-7/actor/▶]> /
|
|
133
|
+
|
|
134
|
+
Available commands (also accepts ":" prefix)
|
|
135
|
+
────────────────────────────────────────────────...
|
|
136
|
+
meta
|
|
137
|
+
/help 명령 목록
|
|
138
|
+
/quit REPL 종료 (세션 자동 저장)
|
|
139
|
+
/clear 대화 히스토리 초기화
|
|
140
|
+
/status 현재 provider / model / role / perms 상태
|
|
141
|
+
/provider <id> provider 전환
|
|
142
|
+
/model <id> 현재 provider의 모델 전환
|
|
143
|
+
/stream on|off 실시간 스트리밍 토글
|
|
144
|
+
review
|
|
145
|
+
/review "<req>" 무조건 구현 전 사전 검토
|
|
146
|
+
/permissions <m> 권한 변경 basic|extended|full
|
|
147
|
+
internal
|
|
148
|
+
/verify /audit /handoff /health
|
|
149
|
+
memory
|
|
150
|
+
/lessons /brainstorm <q> /tasks /plan
|
|
151
|
+
bridge
|
|
152
|
+
/web <op> /pc <op> /lsp <op>
|
|
153
|
+
────────────────────────────────────────────────...
|
|
154
|
+
💡 "/" 또는 ":" 접두사로 명령 호출
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`/` 와 `:` **양쪽 동등** (alias 처리). 모든 모델 공통.
|
|
158
|
+
|
|
159
|
+
### Fix #2 — Tab cycle 한 줄 갱신 (in-place overwrite)
|
|
160
|
+
|
|
161
|
+
**Before** (1.9.188까지) — 매 Tab마다 새 줄 누적 → 채팅 이력 지저분:
|
|
162
|
+
```
|
|
163
|
+
⇄ provider [3/5]: codex ✓ ready
|
|
164
|
+
└ 5개 모델 catalog · Shift+Tab으로 model cycle
|
|
165
|
+
⇄ provider [4/5]: gemini ✓ ready
|
|
166
|
+
└ 3개 모델 catalog · Shift+Tab으로 model cycle
|
|
167
|
+
⇄ provider [5/5]: copilot ✓ ready
|
|
168
|
+
└ 1개 모델 catalog · Shift+Tab으로 model cycle
|
|
169
|
+
⇄ provider [1/5]: ollama
|
|
170
|
+
└ 4개 모델 catalog · Shift+Tab으로 model cycle
|
|
171
|
+
...
|
|
172
|
+
(채팅 영역이 cycle 흔적으로 가득 참)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**After** (1.9.189) — 마지막 1건만 보임:
|
|
176
|
+
```
|
|
177
|
+
⇄ provider [2/5]: claude ✓ ready
|
|
178
|
+
└ 5개 모델 catalog · Shift+Tab으로 model cycle
|
|
179
|
+
agent[claude · opus-4-7/actor/▶]> _
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
// 1.9.189: ANSI cursor up + line clear 로 이전 cycle 라인 덮어씀
|
|
184
|
+
let _lastCycleLines = 0;
|
|
185
|
+
const _clearLastCycle = () => {
|
|
186
|
+
if (!isTty || _lastCycleLines === 0) return;
|
|
187
|
+
process.stdout.write('\r\x1b[K'); // 현재 prompt 줄 클리어
|
|
188
|
+
for (let i = 0; i < _lastCycleLines; i++) {
|
|
189
|
+
process.stdout.write('\x1b[1A\x1b[K'); // 한 줄 위로 + 클리어
|
|
190
|
+
}
|
|
191
|
+
_lastCycleLines = 0;
|
|
192
|
+
};
|
|
193
|
+
// cycleProvider/cycleModel 시작 시 호출 → 이전 cycle 출력 제거 → 새 cycle 출력
|
|
194
|
+
// 사용자 line 입력 시 _lastCycleLines = 0 reset (cycle 흔적 더 이상 클리어 X)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Fix #3 — Welcome 안내 갱신
|
|
198
|
+
```
|
|
199
|
+
───────────────────────────── 채팅 시작 ─────────────────────────────
|
|
200
|
+
메시지 입력 후 Enter · "/" 입력 시 명령 list · :help 으로도 가능 · Ctrl+C 로 종료
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Verified
|
|
204
|
+
- stress-v134: **15/15 PASS** (사용자 명시 7 + live 2 + 누적 6)
|
|
205
|
+
- e2e 217/217 baseline 유지
|
|
206
|
+
- `/` 접두사 7개 명령 모두 정상 동작 (alias 처리)
|
|
207
|
+
- Tab cycle in-place overwrite — 채팅 영역 깔끔 유지
|
|
208
|
+
- VERSION = 1.9.189 · autonomous-rounds = 119 · main 자동 push 50 라운드 연속
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
3
212
|
## 1.9.188 — 2026-05-21
|
|
4
213
|
|
|
5
214
|
**🐛 REPL 한글 prompt 전달 BUG fix (stdin) + 세부 모델 표시 + 입력 구분선 (사용자 명시 3종).**
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []() []() []() []() []() []() []()
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
13
13
|
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
14
14
|
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
15
|
-
║ v1.9.
|
|
15
|
+
║ v1.9.190 AI Agent Reliability Harness + Sandbox ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · sandbox · drift ║
|
|
17
|
-
║
|
|
17
|
+
║ 🚨 설치 Ctrl+C 즉시 종료 BUG fix · 51 라운드 main push ║
|
|
18
18
|
╚══════════════════════════════════════════════════════════════╝
|
|
19
19
|
```
|
|
20
20
|
|
package/bin/harness.js
CHANGED
|
@@ -7,7 +7,7 @@ const cp = require('child_process');
|
|
|
7
7
|
const os = require('os'); // 1.9.178: _publishToNpm 에서 os.tmpdir() 사용 (전역 import)
|
|
8
8
|
const readline = require('readline');
|
|
9
9
|
|
|
10
|
-
const VERSION = '1.9.
|
|
10
|
+
const VERSION = '1.9.190';
|
|
11
11
|
|
|
12
12
|
// 1.9.184: DEP0190 (child_process shell: true) deprecation warning 억제 (사용자 명시).
|
|
13
13
|
// leerness 는 cross-platform PATH resolution 을 위해 shell: true 를 의도적으로 사용 (claude.cmd / ollama.cmd 등 Windows .cmd 처리).
|
|
@@ -163,6 +163,13 @@ function fm(role, readWhen, updateWhen, body) {
|
|
|
163
163
|
function ask(question) {
|
|
164
164
|
return new Promise(resolve => {
|
|
165
165
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
166
|
+
// 1.9.190 (사용자 명시 BUG fix): readline 의 자체 SIGINT 처리 — Ctrl+C 시 즉시 종료.
|
|
167
|
+
// process.on('SIGINT') 핸들러는 readline raw mode 에 가려져 호출 안 됨 → 여기서 명시 처리.
|
|
168
|
+
rl.on('SIGINT', () => {
|
|
169
|
+
try { rl.close(); } catch {}
|
|
170
|
+
process.stdout.write('\n \x1b[31m✗ 설치 중단됨 (Ctrl+C)\x1b[0m\n');
|
|
171
|
+
process.exit(130);
|
|
172
|
+
});
|
|
166
173
|
rl.question(question, answer => { rl.close(); resolve(String(answer || '').trim()); });
|
|
167
174
|
});
|
|
168
175
|
}
|
|
@@ -783,20 +790,12 @@ async function resolveInstallOptions(root, opts = {}) {
|
|
|
783
790
|
|
|
784
791
|
async function install(root, opts = {}) {
|
|
785
792
|
root = absRoot(root); mkdirp(root);
|
|
786
|
-
// 1.9.184
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
let
|
|
793
|
+
// 1.9.184+1.9.190: 설치 도중 Ctrl+C 시 즉시 종료 (사용자 명시 BUG fix — npx 설치 진행 차단).
|
|
794
|
+
// 1.9.184 의 2단계 confirm 은 readline raw mode 가 SIGINT 가로채서 호출 안 됨 → 1.9.190 즉시 종료로 단순화.
|
|
795
|
+
// _selectOne/_selectMany 는 stdin '\x03' 직접 처리, ask() 는 rl.on('SIGINT') 처리, 이 핸들러는 fallback.
|
|
796
|
+
let _sigintTimer = null; // back-compat: 1.9.184 placeholder
|
|
790
797
|
const _sigintHandler = () => {
|
|
791
|
-
|
|
792
|
-
if (_sigintCount === 1) {
|
|
793
|
-
try { process.stdout.write('\n\n ⚠ 설치 중단하시겠습니까? Ctrl+C 를 2초 이내에 한 번 더 누르면 종료됩니다. (그 외 → 계속 진행)\n'); } catch {}
|
|
794
|
-
clearTimeout(_sigintTimer);
|
|
795
|
-
_sigintTimer = setTimeout(() => { _sigintCount = 0; }, 2000);
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
clearTimeout(_sigintTimer);
|
|
799
|
-
try { process.stdout.write('\n ✗ 설치 중단됨 (사용자 요청)\n'); } catch {}
|
|
798
|
+
try { process.stdout.write('\n \x1b[31m✗ 설치 중단됨 (Ctrl+C)\x1b[0m\n'); } catch {}
|
|
800
799
|
process.exit(130);
|
|
801
800
|
};
|
|
802
801
|
if (!opts.migration && !opts.nonInteractive && process.stdin.isTTY) {
|
|
@@ -5539,7 +5538,13 @@ async function _selectOne(question, options, opts = {}) {
|
|
|
5539
5538
|
cleanup();
|
|
5540
5539
|
stdout.write('\n');
|
|
5541
5540
|
resolve(options[idx]);
|
|
5542
|
-
} else if (key === ''
|
|
5541
|
+
} else if (key === '') {
|
|
5542
|
+
// 1.9.190 (사용자 명시 BUG fix): raw mode 에서 Ctrl+C → 즉시 설치 종료 (npx 진행 차단).
|
|
5543
|
+
// 이전 (1.9.34~1.9.189): default 값 반환 → install 흐름 계속 진행 (사용자 BUG 보고).
|
|
5544
|
+
cleanup();
|
|
5545
|
+
stdout.write('\n \x1b[31m✗ 설치 중단됨 (Ctrl+C)\x1b[0m\n');
|
|
5546
|
+
process.exit(130);
|
|
5547
|
+
} else if (key === '' || key === 'q' || key === 'Q') {
|
|
5543
5548
|
cleanup();
|
|
5544
5549
|
stdout.write('\n' + C.dim(' 취소됨') + '\n');
|
|
5545
5550
|
resolve(opts.defaultIndex != null ? options[opts.defaultIndex] : null);
|
|
@@ -5607,7 +5612,13 @@ async function _selectMany(question, options, opts = {}) {
|
|
|
5607
5612
|
cleanup();
|
|
5608
5613
|
stdout.write('\n');
|
|
5609
5614
|
resolve([...selected].sort((a, b) => a - b).map(i => options[i]));
|
|
5610
|
-
} else if (key === ''
|
|
5615
|
+
} else if (key === '') {
|
|
5616
|
+
// 1.9.190 (사용자 명시 BUG fix): raw mode 에서 Ctrl+C → 즉시 설치 종료 (npx 진행 차단).
|
|
5617
|
+
// 이전 (1.9.34~1.9.189): default 값 반환 → install 흐름 계속 진행 (사용자 BUG 보고).
|
|
5618
|
+
cleanup();
|
|
5619
|
+
stdout.write('\n \x1b[31m✗ 설치 중단됨 (Ctrl+C)\x1b[0m\n');
|
|
5620
|
+
process.exit(130);
|
|
5621
|
+
} else if (key === '' || key === 'q' || key === 'Q') {
|
|
5611
5622
|
cleanup();
|
|
5612
5623
|
stdout.write('\n' + C.dim(' 취소됨 (기본값 사용)') + '\n');
|
|
5613
5624
|
resolve((opts.defaults || []).map(d => typeof d === 'number' ? options[d] : d).filter(Boolean));
|
|
@@ -11609,7 +11620,7 @@ async function _agentRepl(root, opts) {
|
|
|
11609
11620
|
// 환영 화면 끝에 명확한 구분선 + 안내 → 채팅 시작 명확.
|
|
11610
11621
|
log('');
|
|
11611
11622
|
log(C.dim(' ───────────────────────────── 채팅 시작 ─────────────────────────────'));
|
|
11612
|
-
log(C.dim(' 메시지 입력 후 Enter · :help
|
|
11623
|
+
log(C.dim(' 메시지 입력 후 Enter · ') + C.cy('"/"') + C.dim(' 입력 시 명령 list · ') + C.cy(':help') + C.dim(' 으로도 가능 · Ctrl+C 로 종료'));
|
|
11613
11624
|
// 1.9.155: REPL 진입 시 handoff 컨텍스트 자동 노출 (UX 개선 — 사용자가 매번 :handoff 안 해도 컨텍스트 인지)
|
|
11614
11625
|
try {
|
|
11615
11626
|
const hf = cp.spawnSync(process.execPath, [__filename, 'handoff', root, '--compact', '--no-drift-check', '--no-headline'], {
|
|
@@ -11664,17 +11675,31 @@ async function _agentRepl(root, opts) {
|
|
|
11664
11675
|
} catch {}
|
|
11665
11676
|
return _PROVIDER_CYCLE_ORDER.slice();
|
|
11666
11677
|
};
|
|
11667
|
-
// 1.9.180: cycleProvider/cycleModel —
|
|
11668
|
-
//
|
|
11678
|
+
// 1.9.180+1.9.189: cycleProvider/cycleModel — 한 줄 갱신 (in-place overwrite).
|
|
11679
|
+
// 1.9.180까지: 매 Tab 누름마다 새 줄 출력 → 채팅 이력으로 누적 (사용자 명시: "지져분해보여").
|
|
11680
|
+
// 1.9.189: ANSI cursor up + line clear 로 이전 cycle 라인 덮어씀 → 마지막 1건만 표시.
|
|
11681
|
+
let _lastCycleLines = 0; // 직전 cycle 출력 라인 수 (overwrite 용)
|
|
11682
|
+
const _clearLastCycle = () => {
|
|
11683
|
+
if (!isTty || _lastCycleLines === 0) return;
|
|
11684
|
+
// 현재 prompt 라인 + 이전 cycle 라인들 클리어
|
|
11685
|
+
// 현재 cursor는 prompt 줄. 위로 _lastCycleLines+1 만큼 올라가서 클리어.
|
|
11686
|
+
process.stdout.write('\r\x1b[K'); // 현재 prompt 줄 클리어
|
|
11687
|
+
for (let i = 0; i < _lastCycleLines; i++) {
|
|
11688
|
+
process.stdout.write('\x1b[1A\x1b[K'); // 한 줄 위로 + 클리어
|
|
11689
|
+
}
|
|
11690
|
+
_lastCycleLines = 0;
|
|
11691
|
+
};
|
|
11669
11692
|
const cycleProvider = (reverse) => {
|
|
11693
|
+
_clearLastCycle();
|
|
11670
11694
|
const list = getProviders();
|
|
11671
11695
|
let idx = list.indexOf(state.provider);
|
|
11672
11696
|
if (idx < 0) idx = 0;
|
|
11673
11697
|
idx = reverse ? (idx - 1 + list.length) % list.length : (idx + 1) % list.length;
|
|
11674
11698
|
state.provider = list[idx];
|
|
11675
11699
|
state.model = null;
|
|
11700
|
+
// 1.9.188: state.model auto-default → 다음 cycle/prompt 에 catalog 첫 모델
|
|
11676
11701
|
const cat = _PROVIDER_MODEL_CATALOG[state.provider];
|
|
11677
|
-
|
|
11702
|
+
if (cat && cat.length) state.model = cat[0].id;
|
|
11678
11703
|
let activeMark = '';
|
|
11679
11704
|
try {
|
|
11680
11705
|
if (state.provider !== 'ollama') {
|
|
@@ -11685,19 +11710,21 @@ async function _agentRepl(root, opts) {
|
|
|
11685
11710
|
}
|
|
11686
11711
|
}
|
|
11687
11712
|
} catch {}
|
|
11688
|
-
process.stdout.write('\r\x1b[K');
|
|
11689
11713
|
process.stdout.write(C.bold(C.green(` ⇄ provider [${idx + 1}/${list.length}]: ${state.provider}`)) + activeMark + '\n');
|
|
11714
|
+
_lastCycleLines = 1;
|
|
11690
11715
|
if (cat?.length) {
|
|
11691
11716
|
process.stdout.write(C.dim(` └ ${cat.length}개 모델 catalog · Shift+Tab으로 model cycle\n`));
|
|
11717
|
+
_lastCycleLines = 2;
|
|
11692
11718
|
}
|
|
11693
11719
|
rl.setPrompt(prompt());
|
|
11694
11720
|
rl.prompt(true);
|
|
11695
11721
|
};
|
|
11696
11722
|
const cycleModel = (reverse) => {
|
|
11723
|
+
_clearLastCycle();
|
|
11697
11724
|
const cat = _PROVIDER_MODEL_CATALOG[state.provider] || [];
|
|
11698
11725
|
if (cat.length === 0) {
|
|
11699
|
-
process.stdout.write(
|
|
11700
|
-
|
|
11726
|
+
process.stdout.write(C.yel(` ⚠ ${state.provider} 추천 모델 catalog 없음 (/model <name> 으로 직접 지정)\n`));
|
|
11727
|
+
_lastCycleLines = 1;
|
|
11701
11728
|
rl.prompt(true);
|
|
11702
11729
|
return;
|
|
11703
11730
|
}
|
|
@@ -11705,14 +11732,17 @@ async function _agentRepl(root, opts) {
|
|
|
11705
11732
|
if (idx < 0) idx = -1;
|
|
11706
11733
|
idx = reverse ? (idx - 1 + cat.length) % cat.length : (idx + 1) % cat.length;
|
|
11707
11734
|
state.model = cat[idx].id;
|
|
11708
|
-
process.stdout.write('\r\x1b[K');
|
|
11709
11735
|
process.stdout.write(C.bold(C.mag(` ⇄ model [${idx + 1}/${cat.length}]: ${state.model}`)) + '\n');
|
|
11736
|
+
_lastCycleLines = 1;
|
|
11710
11737
|
if (cat[idx].note) {
|
|
11711
11738
|
process.stdout.write(C.dim(` └ ${cat[idx].note}\n`));
|
|
11739
|
+
_lastCycleLines = 2;
|
|
11712
11740
|
}
|
|
11713
11741
|
rl.setPrompt(prompt());
|
|
11714
11742
|
rl.prompt(true);
|
|
11715
11743
|
};
|
|
11744
|
+
// 1.9.189: 사용자가 다른 입력 시작하면 cycle line clear 흔적 정리 (다음 Tab까지 유지하면 cycle 위치 보임)
|
|
11745
|
+
// 단순화: 사용자가 enter 누르면 _lastCycleLines=0 으로 reset (응답 후 다시 cycle 출력 시 새로 시작).
|
|
11716
11746
|
process.stdin.on('keypress', (str, key) => {
|
|
11717
11747
|
if (!key) return;
|
|
11718
11748
|
if (key.name === 'tab') {
|
|
@@ -11731,6 +11761,55 @@ async function _agentRepl(root, opts) {
|
|
|
11731
11761
|
}
|
|
11732
11762
|
}
|
|
11733
11763
|
|
|
11764
|
+
// 1.9.189 (사용자 명시): "/" 입력만으로 명령 list 표시 (claude code 영감 — autocomplete UX).
|
|
11765
|
+
// "/" 또는 "/?" → 명령 카탈로그 출력 후 prompt 복귀. 이미지 같은 list 스타일 (명령 + 한 줄 설명).
|
|
11766
|
+
const _showSlashCommandList = () => {
|
|
11767
|
+
log('');
|
|
11768
|
+
log(C.bold(' Available commands') + C.dim(' (also accepts ":" prefix)'));
|
|
11769
|
+
log(C.dim(' ────────────────────────────────────────────────────────────────────────────────'));
|
|
11770
|
+
const groups = [
|
|
11771
|
+
{ title: 'meta', items: [
|
|
11772
|
+
['/help', '명령 목록 (이 화면) — 또는 ":?"'],
|
|
11773
|
+
['/quit', 'REPL 종료 (세션 자동 저장)'],
|
|
11774
|
+
['/clear', '대화 히스토리 초기화'],
|
|
11775
|
+
['/status', '현재 provider / model / role / perms 상태'],
|
|
11776
|
+
['/provider <id>', 'provider 전환 (ollama, claude, codex, gemini, copilot)'],
|
|
11777
|
+
['/model <id>', '현재 provider의 모델 전환'],
|
|
11778
|
+
['/role <id>', '역할 전환 (actor / planner / reviewer)'],
|
|
11779
|
+
['/stream on|off', '실시간 스트리밍 토글 (default ON)']
|
|
11780
|
+
]},
|
|
11781
|
+
{ title: 'review', items: [
|
|
11782
|
+
['/review "<req>"', '무조건 구현 전 사전 검토 (1.9.176)'],
|
|
11783
|
+
['/permissions <m>', '권한 변경 basic|extended|full (1.9.174)']
|
|
11784
|
+
]},
|
|
11785
|
+
{ title: 'internal', items: [
|
|
11786
|
+
['/verify', 'task evidence 검증'],
|
|
11787
|
+
['/audit', 'audit 실행'],
|
|
11788
|
+
['/handoff', 'handoff context 출력'],
|
|
11789
|
+
['/health', 'health check']
|
|
11790
|
+
]},
|
|
11791
|
+
{ title: 'memory', items: [
|
|
11792
|
+
['/lessons', 'lessons.md 회수'],
|
|
11793
|
+
['/brainstorm <q>', 'brainstorm 회수 (memory + code + tasks)'],
|
|
11794
|
+
['/tasks', '현재 task list'],
|
|
11795
|
+
['/plan', 'plan.md milestones']
|
|
11796
|
+
]},
|
|
11797
|
+
{ title: 'bridge', items: [
|
|
11798
|
+
['/web <op>', 'playwright bridge (check/screenshot/extract)'],
|
|
11799
|
+
['/pc <op>', 'robotjs bridge (check/click/type/screenshot)'],
|
|
11800
|
+
['/lsp <op>', 'LSP adapter (check/symbols/references)']
|
|
11801
|
+
]}
|
|
11802
|
+
];
|
|
11803
|
+
for (const g of groups) {
|
|
11804
|
+
log(C.bold(` ${g.title}`));
|
|
11805
|
+
for (const [name, desc] of g.items) {
|
|
11806
|
+
log(' ' + C.cy(name.padEnd(22)) + C.dim(desc));
|
|
11807
|
+
}
|
|
11808
|
+
}
|
|
11809
|
+
log(C.dim(' ────────────────────────────────────────────────────────────────────────────────'));
|
|
11810
|
+
log(C.dim(' 💡 메시지는 직접 입력 · "/" 또는 ":" 접두사로 명령 호출 · Tab=provider · Shift+Tab=model'));
|
|
11811
|
+
log('');
|
|
11812
|
+
};
|
|
11734
11813
|
rl.prompt();
|
|
11735
11814
|
const handleMeta = async (cmd) => {
|
|
11736
11815
|
const [op, ...rest] = cmd.slice(1).split(/\s+/);
|
|
@@ -12020,7 +12099,21 @@ async function _agentRepl(root, opts) {
|
|
|
12020
12099
|
return new Promise(resolve => {
|
|
12021
12100
|
rl.on('line', async (line) => {
|
|
12022
12101
|
const input = line.trim();
|
|
12102
|
+
_lastCycleLines = 0; // 1.9.189: 사용자 입력 시 cycle overwrite 추적 reset
|
|
12023
12103
|
if (!input) { rl.prompt(); return; }
|
|
12104
|
+
// 1.9.189 (사용자 명시): "/" slash 입력만으로 명령 list 표시 (claude code 영감).
|
|
12105
|
+
// "/" 단독 → 명령 list 출력 후 prompt 복귀.
|
|
12106
|
+
// "/help", "/quit" 등 "/" 접두사 → ":" 동등 처리 (alias).
|
|
12107
|
+
if (input === '/' || input === '/?') {
|
|
12108
|
+
_showSlashCommandList();
|
|
12109
|
+
rl.prompt(); return;
|
|
12110
|
+
}
|
|
12111
|
+
// 1.9.189: "/" 접두사를 ":" 와 동등하게 처리 (사용자 명시 — "/ 가 모든 모델 공통 권한 trigger default")
|
|
12112
|
+
if (input.startsWith('/') && !input.startsWith('//')) {
|
|
12113
|
+
const shouldQuit = await handleMeta(':' + input.slice(1));
|
|
12114
|
+
if (shouldQuit) { resolve(); return; }
|
|
12115
|
+
rl.prompt(); return;
|
|
12116
|
+
}
|
|
12024
12117
|
if (input.startsWith(':')) {
|
|
12025
12118
|
const shouldQuit = await handleMeta(input);
|
|
12026
12119
|
if (shouldQuit) { resolve(); return; }
|