leerness 1.9.244 → 1.9.246
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 +105 -0
- package/README.md +3 -3
- package/bin/harness.js +511 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,110 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.246 — 2026-05-26 — UR-0016 REPL UX/UI 개선
|
|
4
|
+
|
|
5
|
+
**🎯 사용자 명시 (UR-0016): REPL agent 채팅 입력칸 옆 컨텍스트 게이지 + 서브 에이전트 가시화 + 정상 완료 초록색 강조.**
|
|
6
|
+
|
|
7
|
+
### 사용자 명시 (UR-0016)
|
|
8
|
+
> *"REPL agent 모드의 기능과 UX UI를 개선해줘: 서브 에이전트 활성화 여부, 채팅입력칸 근처에는 컨텍스트 창 게이지 등 표시, 정상완료된 작업은 초록색 등으로 표시. 별도의 신규 프로젝트를 생성하여 직접 REPL agent 모드를 구동하여 유저가 사용하기 쉽게 개선해줘."*
|
|
9
|
+
|
|
10
|
+
dogfood 검증: `C:\Users\leehy\AppData\Local\Temp\dogfood-1.9.246` 신규 프로젝트에서 `leerness init` + REPL 진입 흐름 직접 검증.
|
|
11
|
+
|
|
12
|
+
### 1. 컨텍스트 창 게이지 (채팅 입력칸 직전 자동 표시)
|
|
13
|
+
모든 prompt 호출 직전에 status bar 한 줄 자동 출력:
|
|
14
|
+
```
|
|
15
|
+
💬 ctx 12msg ~3.4k/200k ████░░░░░░░░░░ 1.7% · agents: *claude ✓codex ·gemini ·copilot · ▶stream
|
|
16
|
+
```
|
|
17
|
+
- `ctx Nmsg` — 누적 대화 메시지 수
|
|
18
|
+
- `~XXk/YYk` — 추정 토큰 / provider별 컨텍스트 윈도우 (claude 200k, claude-4-7 1M, gpt-5 200k, gpt-4.1 1M, gemini 1M, copilot 64k, ollama 8k)
|
|
19
|
+
- `████░░░░░░░░░░ %` — 14문자 progress bar (50% 이상 cyan, 80% 이상 yellow 경고)
|
|
20
|
+
- 토큰 추정 휴리스틱: `chars / 3.5` (CJK/영문 평균치)
|
|
21
|
+
|
|
22
|
+
### 2. 서브 에이전트 활성화 가시화 (한 줄 5종 표시)
|
|
23
|
+
- `*claude` — 현재 활성 provider (bold + green)
|
|
24
|
+
- `✓codex` — 설치+인증 완료 ready (green)
|
|
25
|
+
- `·gemini` — 미설치 또는 비활성 (dim)
|
|
26
|
+
- 5종 모두 한 줄에 표시 → 사용자가 즉시 다른 provider 가용성 인지 → Tab 키로 전환
|
|
27
|
+
|
|
28
|
+
### 3. 정상 완료 작업 초록색 강조 (사용자 명시)
|
|
29
|
+
- assistant 응답 헤더에 `✓` 마커 + `C.green()` 적용
|
|
30
|
+
- stream 모드: `✓ [assistant: claude/sonnet-4-7, role=actor, 1234ms · 567자]` (green)
|
|
31
|
+
- non-stream: `✓ assistant (claude, role=actor, 1234ms)` (green bold)
|
|
32
|
+
- 기존 `[assistant: ...]` (dim) → `✓ [assistant: ...]` (green)
|
|
33
|
+
|
|
34
|
+
### 4. dogfood 신규 프로젝트 직접 구동 검증
|
|
35
|
+
- 임시 디렉토리에 빈 프로젝트 생성 → `leerness init` 정상 동작 확인
|
|
36
|
+
- `leerness agent --interactive` 진입 흐름 검증 (provider 없음 시 안내 출력)
|
|
37
|
+
- `--version` 등 핵심 명령 동작 확인
|
|
38
|
+
|
|
39
|
+
### 5. promptWithStatus wrapper 도입
|
|
40
|
+
- `rl.prompt()` 7회 호출처 모두 `promptWithStatus()` 로 통일
|
|
41
|
+
- line handler / cycle / 초기 진입 등 일관된 status bar 노출
|
|
42
|
+
- wrapper 내부 1건만 실제 `rl.prompt()` 호출 (DRY)
|
|
43
|
+
|
|
44
|
+
### 6. 누적 회귀 (1.9.207~245) — 모두 유지
|
|
45
|
+
- api-skill cache (1.9.245) + CJK 분류 (1.9.243) + env encoding --apply (1.9.242) + 모두 유지
|
|
46
|
+
- handoff JSON 11 필드 · MCP 70 도구 · CJK + py-check + 비정상종료 모두 유지
|
|
47
|
+
|
|
48
|
+
### 7. stress-v191 — **23/23 PASS · 100%**
|
|
49
|
+
- 1.9.246 (11): VERSION + 4 helper + promptWithStatus + 컨텍스트 bar + sub-agent 가시화 + 초록색 ✓ + dogfood init + 통일성
|
|
50
|
+
- 성능 (1): cold start avg 378ms
|
|
51
|
+
- 누적 회귀 (11): 1.9.207~245
|
|
52
|
+
|
|
53
|
+
### 8. 자동 release (108 라운드 main-push streak · 69 라운드 npm publish streak · R202)
|
|
54
|
+
|
|
55
|
+
🎯 **REPL UX 사용자 요구 100% 반영** — 컨텍스트 + 에이전트 + 초록색 강조 통합 status bar.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 1.9.245 — 2026-05-26 — UR-0015 API skill cache
|
|
60
|
+
|
|
61
|
+
**📚 사용자 명시 (UR-0015): API 문서/관련링크 자동 정리 + AI 자동 참조 시스템.**
|
|
62
|
+
|
|
63
|
+
### 사용자 명시 (UR-0015)
|
|
64
|
+
> *"API 문서/기능 요청 시 공식 문서·AI 탐색 내용을 스킬처럼 .harness/api-skills/ 에 정리하고 방향 지시도 함께 기록. 이후 같은 API 관련 수정/구현 요청 시 AI 가 정리해둔 파일이 자동 참조. URL 제공 시 본 URL + 관련 링크의 기능까지 참조."*
|
|
65
|
+
|
|
66
|
+
검증 예시: 쿠팡 상품 생성 API (https://developers.coupangcorp.com/hc/ko/articles/360033877853) — 실 fetch 성공 (title "상품 생성 - Open APIs" + 7 관련 링크 자동 수집).
|
|
67
|
+
|
|
68
|
+
### 1. `leerness api-skill` CLI 5종
|
|
69
|
+
- `add <url> [--direction "방향"] [--name "..."] [--no-crawl] [--skeleton]` — fetch + same-domain 관련 링크 1단계 crawl (max 10) → `.harness/api-skills/<id>.md`
|
|
70
|
+
- `list [--json]` — 저장된 skill 목록
|
|
71
|
+
- `show <id>` — 특정 skill 본문 출력 (AI 컨텍스트 적재용)
|
|
72
|
+
- `match <query> [--json]` — task 키워드 매칭 (CJK 한글 2자+ / ASCII 3자+ 모두 지원)
|
|
73
|
+
- `drop <id>` — 삭제
|
|
74
|
+
|
|
75
|
+
### 2. URL Fetch 방식 — 의존성 0 (Node built-in https)
|
|
76
|
+
- Mozilla 호환 User-Agent (Cloudflare/WAF 차단 회피)
|
|
77
|
+
- timeout 10s · max body 1MB · max 5 redirects
|
|
78
|
+
- HTML→text 변환 (script/style 제거, entity decode)
|
|
79
|
+
- same-domain 관련 링크 추출 (max 10, depth=1)
|
|
80
|
+
- 차단 시 (403/401/429) `--skeleton` fallback → 빈 .md 골격 생성
|
|
81
|
+
|
|
82
|
+
### 3. 자동 참조 — handoff body 자동 노출
|
|
83
|
+
- 현재 in-progress task description 키워드 + skill 매칭 → `## 📚 관련 API skill N건 발견 (참조 권장)`
|
|
84
|
+
- 본 URL + 방향 + 도메인 표시 (top 3)
|
|
85
|
+
- 매칭 0 시: 저장된 skill 수만 hint 표시
|
|
86
|
+
|
|
87
|
+
### 4. JSON 11번째 통합 필드 `apiSkills` (handoff/session close/health)
|
|
88
|
+
- `{ total, matched, matchedIds, ids }` — 외부 AI 가 단일 호출에서 API skill 컨텍스트 회수
|
|
89
|
+
- JSON 통합 매트릭스 10 → **11 필드** (3 명령 × 11 = 33 통합 포인트)
|
|
90
|
+
|
|
91
|
+
### 5. MCP **70번째 도구** `leerness_api_skill` 🎉
|
|
92
|
+
- sub: list / show / match / add / drop (외부 AI 가 MCP 호출로 직접 사용)
|
|
93
|
+
|
|
94
|
+
### 6. 누적 회귀 (1.9.207~244) — 모두 유지
|
|
95
|
+
- REPL HOTFIX (1.9.244) + CJK 분류 (1.9.243) + env encoding --apply (1.9.242) + 모두 유지
|
|
96
|
+
|
|
97
|
+
### 7. stress-v190 — **26/26 PASS · 100%**
|
|
98
|
+
- 1.9.245 신규 (15): VERSION + helper 함수 + CLI 5종 + 실 fetch + JSON 11 필드 × 3명령 + body 매칭 + skeleton fallback + MCP 70
|
|
99
|
+
- 성능 (1): cold start avg 331ms
|
|
100
|
+
- 누적 회귀 (10): 1.9.207~244
|
|
101
|
+
|
|
102
|
+
### 8. 자동 release (107 라운드 main-push streak · 68 라운드 npm publish streak)
|
|
103
|
+
|
|
104
|
+
📚 **API 지식 자동 누적·재사용** — 사용자가 한 번 정리하면 AI가 영구 기억. R201 진입.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
3
108
|
## 1.9.244 — 2026-05-26 🎉 R200 + 🚨 HOTFIX
|
|
4
109
|
|
|
5
110
|
**🚨 HOTFIX: REPL agent ReferenceError `_lastCycleLines` (1.9.189 회귀 버그) + 🎉 R200 마일스톤 도달.**
|
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.246 AI Agent Reliability Harness + Sandbox ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · sandbox · drift ║
|
|
17
|
-
║
|
|
17
|
+
║ 🎯 REPL UX (UR-0016) — 컨텍스트 게이지 + 에이전트 + 초록색 ✓ ║
|
|
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.246';
|
|
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 처리).
|
|
@@ -2588,6 +2588,338 @@ function envCmd(root, sub) {
|
|
|
2588
2588
|
log(dm(` → JSON 출력: leerness env --json`));
|
|
2589
2589
|
}
|
|
2590
2590
|
|
|
2591
|
+
// 1.9.245: API skill cache — 공식 문서 + 관련 링크 자동 정리 (사용자 명시 UR-0015)
|
|
2592
|
+
// .harness/api-skills/<id>.md 에 frontmatter + 본문 저장. handoff 자동 매칭.
|
|
2593
|
+
// Node built-in https (의존성 0), depth=1 same-domain crawl, max 10 links.
|
|
2594
|
+
function _apiSkillsDir(root) {
|
|
2595
|
+
return path.join(absRoot(root), '.harness', 'api-skills');
|
|
2596
|
+
}
|
|
2597
|
+
function _apiSkillId(url, name) {
|
|
2598
|
+
// 도메인 + path slug 로 id 생성. 예: developers.coupangcorp.com/articles/360033877853 → coupang-articles-360033877853
|
|
2599
|
+
try {
|
|
2600
|
+
const u = new URL(url);
|
|
2601
|
+
const host = u.hostname.replace(/^www\./, '').split('.').slice(0, -1).join('-') || u.hostname;
|
|
2602
|
+
const pathSlug = u.pathname.replace(/^\/+|\/+$/g, '').replace(/[^a-z0-9가-힣\-_]+/gi, '-').slice(-50);
|
|
2603
|
+
const base = `${host}-${pathSlug}`.replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
2604
|
+
return (name ? base + '-' + name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30) : base).replace(/-+/g, '-');
|
|
2605
|
+
} catch { return 'api-' + Date.now(); }
|
|
2606
|
+
}
|
|
2607
|
+
function _fetchUrl(url, opts = {}) {
|
|
2608
|
+
// Node https.get with timeout, follow redirects (max 5), max body 1MB
|
|
2609
|
+
const https = require('https');
|
|
2610
|
+
const http = require('http');
|
|
2611
|
+
const maxRedirects = opts.maxRedirects != null ? opts.maxRedirects : 5;
|
|
2612
|
+
const timeout = opts.timeout || 10000;
|
|
2613
|
+
return new Promise((resolve, reject) => {
|
|
2614
|
+
function go(u, redirects) {
|
|
2615
|
+
let parsed;
|
|
2616
|
+
try { parsed = new URL(u); } catch { return reject(new Error('invalid URL: ' + u)); }
|
|
2617
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return reject(new Error('unsupported protocol: ' + parsed.protocol));
|
|
2618
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
2619
|
+
const req = lib.get({
|
|
2620
|
+
hostname: parsed.hostname, port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
2621
|
+
path: parsed.pathname + parsed.search, timeout,
|
|
2622
|
+
// 1.9.245: 일부 사이트 (Cloudflare/AWS WAF) 가 비-브라우저 UA 차단 → Mozilla 호환 UA 로 호환성 ↑
|
|
2623
|
+
headers: {
|
|
2624
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 leerness/1.9.245',
|
|
2625
|
+
'Accept': 'text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8',
|
|
2626
|
+
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
2627
|
+
'Accept-Encoding': 'identity'
|
|
2628
|
+
}
|
|
2629
|
+
}, res => {
|
|
2630
|
+
// 3xx → follow
|
|
2631
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirects < maxRedirects) {
|
|
2632
|
+
const next = new URL(res.headers.location, u).toString();
|
|
2633
|
+
res.resume(); return go(next, redirects + 1);
|
|
2634
|
+
}
|
|
2635
|
+
let body = ''; let size = 0; const MAX = 1024 * 1024;
|
|
2636
|
+
res.on('data', chunk => { size += chunk.length; if (size > MAX) { res.destroy(); return reject(new Error('body > 1MB')); } body += chunk; });
|
|
2637
|
+
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body, finalUrl: u }));
|
|
2638
|
+
});
|
|
2639
|
+
req.on('error', reject);
|
|
2640
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout ' + timeout + 'ms')); });
|
|
2641
|
+
}
|
|
2642
|
+
go(url, 0);
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
function _htmlToText(html) {
|
|
2646
|
+
if (!html) return '';
|
|
2647
|
+
return html
|
|
2648
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
2649
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
2650
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
2651
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
2652
|
+
.replace(/<\/?(p|div|li|h[1-6]|tr|td|pre)[^>]*>/gi, '\n')
|
|
2653
|
+
.replace(/<[^>]+>/g, ' ')
|
|
2654
|
+
.replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'")
|
|
2655
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)))
|
|
2656
|
+
.replace(/[ \t]+/g, ' ').replace(/\n\s*\n\s*\n+/g, '\n\n').trim();
|
|
2657
|
+
}
|
|
2658
|
+
function _extractTitle(html) {
|
|
2659
|
+
const m = (html || '').match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
2660
|
+
if (!m) return '';
|
|
2661
|
+
return _htmlToText(m[1]).slice(0, 200);
|
|
2662
|
+
}
|
|
2663
|
+
function _extractLinks(html, baseUrl, maxLinks) {
|
|
2664
|
+
if (!html) return [];
|
|
2665
|
+
const base = new URL(baseUrl);
|
|
2666
|
+
const found = new Map();
|
|
2667
|
+
const re = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
|
|
2668
|
+
let m;
|
|
2669
|
+
while ((m = re.exec(html)) !== null) {
|
|
2670
|
+
let href = m[1];
|
|
2671
|
+
if (!href || href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:')) continue;
|
|
2672
|
+
let abs;
|
|
2673
|
+
try { abs = new URL(href, baseUrl).toString(); } catch { continue; }
|
|
2674
|
+
const u = new URL(abs);
|
|
2675
|
+
if (u.hostname !== base.hostname) continue; // same-domain only
|
|
2676
|
+
if (abs === baseUrl) continue;
|
|
2677
|
+
if (found.has(abs)) continue;
|
|
2678
|
+
const text = _htmlToText(m[2]).slice(0, 120);
|
|
2679
|
+
found.set(abs, { url: abs, text });
|
|
2680
|
+
if (found.size >= (maxLinks || 10)) break;
|
|
2681
|
+
}
|
|
2682
|
+
return Array.from(found.values());
|
|
2683
|
+
}
|
|
2684
|
+
async function _collectAPIDoc(url, opts = {}) {
|
|
2685
|
+
const direction = opts.direction || '';
|
|
2686
|
+
const noCrawl = opts.noCrawl;
|
|
2687
|
+
const r0 = await _fetchUrl(url);
|
|
2688
|
+
if (r0.status >= 400) throw new Error('fetch ' + url + ' → status ' + r0.status);
|
|
2689
|
+
const html = r0.body;
|
|
2690
|
+
const title = _extractTitle(html);
|
|
2691
|
+
const text = _htmlToText(html);
|
|
2692
|
+
const result = { url, title, text: text.slice(0, 8000), related_links: [] };
|
|
2693
|
+
if (!noCrawl) {
|
|
2694
|
+
const links = _extractLinks(html, r0.finalUrl || url, 10);
|
|
2695
|
+
for (const lk of links) {
|
|
2696
|
+
try {
|
|
2697
|
+
const rr = await _fetchUrl(lk.url, { timeout: 8000 });
|
|
2698
|
+
if (rr.status < 400) {
|
|
2699
|
+
const t = _extractTitle(rr.body) || lk.text;
|
|
2700
|
+
const excerpt = _htmlToText(rr.body).slice(0, 400);
|
|
2701
|
+
result.related_links.push({ url: lk.url, title: t.slice(0, 200), excerpt });
|
|
2702
|
+
}
|
|
2703
|
+
} catch {}
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
result.direction = direction;
|
|
2707
|
+
result.captured_at = new Date().toISOString();
|
|
2708
|
+
return result;
|
|
2709
|
+
}
|
|
2710
|
+
function _serializeAPISkill(id, name, urls, direction, doc) {
|
|
2711
|
+
const lines = [];
|
|
2712
|
+
lines.push('---');
|
|
2713
|
+
lines.push(`id: ${id}`);
|
|
2714
|
+
lines.push(`name: ${name}`);
|
|
2715
|
+
lines.push(`urls:`);
|
|
2716
|
+
urls.forEach(u => lines.push(` - ${u}`));
|
|
2717
|
+
lines.push(`direction: ${JSON.stringify(direction || '')}`);
|
|
2718
|
+
lines.push(`captured_at: ${doc.captured_at}`);
|
|
2719
|
+
if (doc.related_links && doc.related_links.length) {
|
|
2720
|
+
lines.push(`related_links:`);
|
|
2721
|
+
doc.related_links.forEach(rl => lines.push(` - { url: ${JSON.stringify(rl.url)}, title: ${JSON.stringify(rl.title || '')} }`));
|
|
2722
|
+
}
|
|
2723
|
+
lines.push('---');
|
|
2724
|
+
lines.push('');
|
|
2725
|
+
lines.push(`# ${name}`);
|
|
2726
|
+
lines.push('');
|
|
2727
|
+
if (direction) { lines.push('## 🎯 방향 지시 (사용자 또는 AI)'); lines.push(''); lines.push(direction); lines.push(''); }
|
|
2728
|
+
lines.push('## 📄 본 URL 내용 (정리)');
|
|
2729
|
+
lines.push('');
|
|
2730
|
+
if (doc.title) { lines.push(`**Title:** ${doc.title}`); lines.push(''); }
|
|
2731
|
+
lines.push('```');
|
|
2732
|
+
lines.push(doc.text.slice(0, 6000));
|
|
2733
|
+
lines.push('```');
|
|
2734
|
+
lines.push('');
|
|
2735
|
+
if (doc.related_links && doc.related_links.length) {
|
|
2736
|
+
lines.push('## 🔗 관련 링크 (depth=1, same-domain, max 10)');
|
|
2737
|
+
lines.push('');
|
|
2738
|
+
doc.related_links.forEach(rl => {
|
|
2739
|
+
lines.push(`### ${rl.title || rl.url}`);
|
|
2740
|
+
lines.push(`URL: ${rl.url}`);
|
|
2741
|
+
lines.push('');
|
|
2742
|
+
if (rl.excerpt) { lines.push(rl.excerpt); lines.push(''); }
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
return lines.join('\n');
|
|
2746
|
+
}
|
|
2747
|
+
function _loadAPISkill(root, id) {
|
|
2748
|
+
const fp = path.join(_apiSkillsDir(root), id + '.md');
|
|
2749
|
+
if (!fs.existsSync(fp)) return null;
|
|
2750
|
+
const content = fs.readFileSync(fp, 'utf8');
|
|
2751
|
+
const fm = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
2752
|
+
if (!fm) return { id, content, urls: [], name: id };
|
|
2753
|
+
const meta = {};
|
|
2754
|
+
fm[1].split('\n').forEach(l => {
|
|
2755
|
+
const m = l.match(/^(\w+):\s*(.*)$/);
|
|
2756
|
+
if (m) meta[m[1]] = m[2];
|
|
2757
|
+
});
|
|
2758
|
+
const urls = [];
|
|
2759
|
+
const urlMatch = fm[1].match(/urls:\n((?:\s+-\s+.+\n?)+)/);
|
|
2760
|
+
if (urlMatch) urlMatch[1].split('\n').forEach(l => { const u = l.match(/^\s+-\s+(.+)$/); if (u) urls.push(u[1].trim()); });
|
|
2761
|
+
return { id: meta.id || id, name: meta.name || id, urls, direction: (meta.direction || '').replace(/^"|"$/g, ''), captured_at: meta.captured_at, body: fm[2] };
|
|
2762
|
+
}
|
|
2763
|
+
function _listAPISkills(root) {
|
|
2764
|
+
const dir = _apiSkillsDir(root);
|
|
2765
|
+
if (!fs.existsSync(dir)) return [];
|
|
2766
|
+
return fs.readdirSync(dir).filter(f => f.endsWith('.md')).map(f => f.slice(0, -3))
|
|
2767
|
+
.map(id => _loadAPISkill(root, id)).filter(Boolean);
|
|
2768
|
+
}
|
|
2769
|
+
function _matchAPISkills(root, query) {
|
|
2770
|
+
if (!query) return [];
|
|
2771
|
+
const q = String(query).toLowerCase();
|
|
2772
|
+
// 1.9.245: CJK 한글/한자 토큰 (2자+) 허용, ASCII 토큰 (3자+)
|
|
2773
|
+
const tokens = q.split(/[\s,.;:/\(\)\[\]\{\}"'`]+/).filter(t => {
|
|
2774
|
+
if (!t) return false;
|
|
2775
|
+
const isCJK = /[ -鿿가-]/.test(t);
|
|
2776
|
+
return isCJK ? t.length >= 2 : t.length >= 3;
|
|
2777
|
+
});
|
|
2778
|
+
const skills = _listAPISkills(root);
|
|
2779
|
+
const scored = skills.map(s => {
|
|
2780
|
+
let score = 0;
|
|
2781
|
+
const haystack = (s.name + ' ' + (s.direction || '') + ' ' + s.urls.join(' ') + ' ' + s.body.slice(0, 3000)).toLowerCase();
|
|
2782
|
+
tokens.forEach(t => { if (haystack.includes(t)) score += 1; });
|
|
2783
|
+
return { skill: s, score };
|
|
2784
|
+
}).filter(x => x.score > 0).sort((a, b) => b.score - a.score);
|
|
2785
|
+
return scored.map(x => x.skill);
|
|
2786
|
+
}
|
|
2787
|
+
async function apiSkillCmd(root, sub) {
|
|
2788
|
+
root = absRoot(root);
|
|
2789
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
2790
|
+
const cy = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
2791
|
+
const gr = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
|
|
2792
|
+
const yl = s => isTty ? `\x1b[33m${s}\x1b[0m` : s;
|
|
2793
|
+
const rd = s => isTty ? `\x1b[31m${s}\x1b[0m` : s;
|
|
2794
|
+
const dm = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
2795
|
+
const dir = _apiSkillsDir(root);
|
|
2796
|
+
if (sub === 'list') {
|
|
2797
|
+
if (!fs.existsSync(dir)) {
|
|
2798
|
+
if (has('--json')) { log(JSON.stringify({ skills: [] })); return; }
|
|
2799
|
+
log(dm(`(아직 API skill 없음 — leerness api-skill add <url> 으로 생성)`));
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
const skills = _listAPISkills(root);
|
|
2803
|
+
if (has('--json')) {
|
|
2804
|
+
log(JSON.stringify({ skills: skills.map(s => ({ id: s.id, name: s.name, urls: s.urls, captured_at: s.captured_at })) }, null, 2));
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
log(cy(`# leerness api-skill list (1.9.245, UR-0015)`));
|
|
2808
|
+
log('');
|
|
2809
|
+
if (skills.length === 0) { log(dm(`(없음)`)); return; }
|
|
2810
|
+
skills.forEach(s => {
|
|
2811
|
+
log(` • ${gr(s.id)} — ${s.name}`);
|
|
2812
|
+
if (s.urls && s.urls.length) log(dm(` ${s.urls[0]}${s.urls.length > 1 ? ` (+${s.urls.length - 1})` : ''}`));
|
|
2813
|
+
if (s.direction) log(dm(` 방향: ${s.direction.slice(0, 80)}`));
|
|
2814
|
+
});
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
// 1.9.245: 위치 인자 robust 추출 — argv 에서 cmd/sub 뒤 첫 non-flag (flag value 도 skip)
|
|
2818
|
+
const _positionalAfterSub = () => {
|
|
2819
|
+
const argv = process.argv.slice(2); // ['api-skill', sub, ...]
|
|
2820
|
+
let i = 2; // skip cmd + sub
|
|
2821
|
+
while (i < argv.length) {
|
|
2822
|
+
const a = argv[i];
|
|
2823
|
+
if (a.startsWith('--')) {
|
|
2824
|
+
// --key value 패턴이면 value 도 skip
|
|
2825
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) i += 2;
|
|
2826
|
+
else i += 1;
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
return a;
|
|
2830
|
+
}
|
|
2831
|
+
return null;
|
|
2832
|
+
};
|
|
2833
|
+
if (sub === 'show') {
|
|
2834
|
+
const id = arg('--id') || _positionalAfterSub();
|
|
2835
|
+
if (!id) { log(rd('id 필요: leerness api-skill show <id>')); return; }
|
|
2836
|
+
const s = _loadAPISkill(root, id);
|
|
2837
|
+
if (!s) { log(rd(`api-skill 없음: ${id}`)); return; }
|
|
2838
|
+
if (has('--json')) { log(JSON.stringify(s, null, 2)); return; }
|
|
2839
|
+
log(fs.readFileSync(path.join(dir, id + '.md'), 'utf8'));
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
if (sub === 'match') {
|
|
2843
|
+
const query = arg('--query') || _positionalAfterSub() || '';
|
|
2844
|
+
const matches = _matchAPISkills(root, query);
|
|
2845
|
+
if (has('--json')) {
|
|
2846
|
+
log(JSON.stringify({ query, matches: matches.map(s => ({ id: s.id, name: s.name })) }, null, 2));
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
log(cy(`# leerness api-skill match "${query}" — ${matches.length}건`));
|
|
2850
|
+
matches.forEach(s => log(` • ${gr(s.id)} — ${s.name}`));
|
|
2851
|
+
if (matches.length === 0) log(dm(`(매칭 없음)`));
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
if (sub === 'drop') {
|
|
2855
|
+
const id = arg('--id') || _positionalAfterSub();
|
|
2856
|
+
if (!id) { log(rd('id 필요: leerness api-skill drop <id>')); return; }
|
|
2857
|
+
const fp = path.join(dir, id + '.md');
|
|
2858
|
+
if (!fs.existsSync(fp)) { log(rd(`없음: ${id}`)); return; }
|
|
2859
|
+
fs.unlinkSync(fp);
|
|
2860
|
+
log(gr(`✓ 삭제: ${id}`));
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
if (sub === 'add') {
|
|
2864
|
+
const url = arg('--url') || _positionalAfterSub();
|
|
2865
|
+
if (!url) { log(rd('URL 필요: leerness api-skill add <url> [--direction "..."]')); return; }
|
|
2866
|
+
const direction = arg('--direction', '');
|
|
2867
|
+
const name = arg('--name', '');
|
|
2868
|
+
const noCrawl = has('--no-crawl');
|
|
2869
|
+
log(cy(`# leerness api-skill add (1.9.245, UR-0015)`));
|
|
2870
|
+
log(` URL: ${url}`);
|
|
2871
|
+
if (direction) log(` 방향: ${direction}`);
|
|
2872
|
+
log(dm(` Fetch + crawl (depth=1, same-domain, max 10)...`));
|
|
2873
|
+
try {
|
|
2874
|
+
const doc = await _collectAPIDoc(url, { direction, noCrawl });
|
|
2875
|
+
const skillName = name || doc.title || new URL(url).hostname;
|
|
2876
|
+
const id = _apiSkillId(url, name);
|
|
2877
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
2878
|
+
const md = _serializeAPISkill(id, skillName, [url], direction, doc);
|
|
2879
|
+
fs.writeFileSync(path.join(dir, id + '.md'), md, 'utf8');
|
|
2880
|
+
log(gr(`✓ 저장: .harness/api-skills/${id}.md`));
|
|
2881
|
+
log(dm(` title: ${doc.title || '(none)'}`));
|
|
2882
|
+
log(dm(` related links: ${(doc.related_links || []).length}`));
|
|
2883
|
+
log(dm(` text size: ${doc.text.length} chars`));
|
|
2884
|
+
if (has('--json')) log(JSON.stringify({ id, name: skillName, url, related_count: (doc.related_links || []).length }, null, 2));
|
|
2885
|
+
} catch (e) {
|
|
2886
|
+
log(rd(`✗ Fetch 실패: ${e.message}`));
|
|
2887
|
+
// 1.9.245: --skeleton fallback — Cloudflare/WAF 차단된 URL 도 빈 .md 골격 생성 → 사용자가 수동 채움
|
|
2888
|
+
if (has('--skeleton') || /403|401|429/.test(e.message)) {
|
|
2889
|
+
const skillName = name || new URL(url).hostname;
|
|
2890
|
+
const id = _apiSkillId(url, name);
|
|
2891
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
2892
|
+
const stub = {
|
|
2893
|
+
url, title: skillName, text: `(fetch 실패 — 사용자가 직접 ${url} 에서 내용을 복사해 채워주세요)`,
|
|
2894
|
+
related_links: [], captured_at: new Date().toISOString(), direction
|
|
2895
|
+
};
|
|
2896
|
+
const md = _serializeAPISkill(id, skillName, [url], direction, stub);
|
|
2897
|
+
fs.writeFileSync(path.join(dir, id + '.md'), md, 'utf8');
|
|
2898
|
+
log(yl(`⚠ 빈 골격 생성 (--skeleton fallback): .harness/api-skills/${id}.md`));
|
|
2899
|
+
log(dm(` → 직접 ${url} 내용을 복사해서 .md 파일에 채워넣어주세요`));
|
|
2900
|
+
log(dm(` → 이후 leerness handoff 가 매칭 시 참조됩니다`));
|
|
2901
|
+
if (has('--json')) log(JSON.stringify({ id, skeleton: true, url, name: skillName }, null, 2));
|
|
2902
|
+
} else {
|
|
2903
|
+
process.exitCode = 1;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
// help
|
|
2909
|
+
log(cy(`# leerness api-skill (1.9.245, UR-0015) — API 문서·관련링크 자동 정리`));
|
|
2910
|
+
log('');
|
|
2911
|
+
log(` add <url> [--direction "..."] [--name "..."] [--no-crawl]`);
|
|
2912
|
+
log(` URL 페치 + same-domain 관련 링크 1단계 crawl (max 10) → .harness/api-skills/<id>.md`);
|
|
2913
|
+
log(` list [--json] → 저장된 api-skill 목록`);
|
|
2914
|
+
log(` show <id> → 특정 skill 본문 출력`);
|
|
2915
|
+
log(` match <query> [--json] → task 키워드 매칭`);
|
|
2916
|
+
log(` drop <id> → 삭제`);
|
|
2917
|
+
log('');
|
|
2918
|
+
log(dm(` 저장: .harness/api-skills/<id>.md (frontmatter: id/name/urls/direction/captured_at/related_links)`));
|
|
2919
|
+
log(dm(` fetch: Node built-in https (의존성 0), max 1MB body, 10s timeout, 5 redirect`));
|
|
2920
|
+
log(dm(` 자동 참조: handoff body 에 매칭 N건 자동 노출 (현재 task 키워드)`));
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2591
2923
|
// 1.9.231: leerness pulse — 한 줄 종합 요약 (10 핵심 지표)
|
|
2592
2924
|
// handoff 헤드라인의 축약 버전 — 사용자가 빠르게 상태 파악
|
|
2593
2925
|
// 응답 (--json): { version, roundCount, mcpTools, memorySurface, security, health, driftScore, nextMilestone, etaDays, abnormal }
|
|
@@ -6629,6 +6961,25 @@ function handoff(root) {
|
|
|
6629
6961
|
encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file)
|
|
6630
6962
|
};
|
|
6631
6963
|
} catch {}
|
|
6964
|
+
// 1.9.245: apiSkills 통합 (handoff JSON 11번째 통합 필드) — UR-0015 API 문서 캐시
|
|
6965
|
+
// 현재 task 키워드 기반 매칭 + 전체 카운트
|
|
6966
|
+
try {
|
|
6967
|
+
const allSkills = _listAPISkills(root);
|
|
6968
|
+
// 현재 task description 추출 (간단 휴리스틱: progress-tracker in-progress 첫 row)
|
|
6969
|
+
let currentTaskText = '';
|
|
6970
|
+
try {
|
|
6971
|
+
const rows = readProgressRows(root);
|
|
6972
|
+
const ip = rows.find(r => r.status === 'in-progress');
|
|
6973
|
+
if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
|
|
6974
|
+
} catch {}
|
|
6975
|
+
const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
|
|
6976
|
+
result.apiSkills = {
|
|
6977
|
+
total: allSkills.length,
|
|
6978
|
+
matched: matched.length,
|
|
6979
|
+
matchedIds: matched.slice(0, 5).map(s => s.id),
|
|
6980
|
+
ids: allSkills.slice(0, 10).map(s => s.id)
|
|
6981
|
+
};
|
|
6982
|
+
} catch {}
|
|
6632
6983
|
} catch {}
|
|
6633
6984
|
try {
|
|
6634
6985
|
const pwState = _loadPreWakeReport(root);
|
|
@@ -6995,6 +7346,40 @@ function handoff(root) {
|
|
|
6995
7346
|
}
|
|
6996
7347
|
} catch {}
|
|
6997
7348
|
|
|
7349
|
+
// 1.9.245: API skill cache 자동 참조 (사용자 명시 UR-0015)
|
|
7350
|
+
// 현재 task 키워드 기반으로 .harness/api-skills/ 매칭 → 사용자가 정리해둔 API 문서 자동 노출
|
|
7351
|
+
try {
|
|
7352
|
+
const allSkills = _listAPISkills(root);
|
|
7353
|
+
if (allSkills.length > 0) {
|
|
7354
|
+
let currentTaskText = '';
|
|
7355
|
+
try {
|
|
7356
|
+
const rows = readProgressRows(root);
|
|
7357
|
+
const ip = rows.find(r => r.status === 'in-progress');
|
|
7358
|
+
if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
|
|
7359
|
+
} catch {}
|
|
7360
|
+
const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
|
|
7361
|
+
if (matched.length > 0) {
|
|
7362
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
7363
|
+
const cy5 = s => isTty ? `\x1b[36m${s}\x1b[0m` : s;
|
|
7364
|
+
const gr5 = s => isTty ? `\x1b[32m${s}\x1b[0m` : s;
|
|
7365
|
+
const dm5 = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
7366
|
+
log('');
|
|
7367
|
+
log(cy5(`## 📚 관련 API skill ${matched.length}건 발견 (1.9.245, UR-0015) — 참조 권장`));
|
|
7368
|
+
matched.slice(0, 3).forEach(s => {
|
|
7369
|
+
log(gr5(` • ${s.id} — ${s.name}`));
|
|
7370
|
+
if (s.urls && s.urls.length) log(dm5(` ${s.urls[0]}${s.urls.length > 1 ? ` (+${s.urls.length - 1})` : ''}`));
|
|
7371
|
+
if (s.direction) log(dm5(` 방향: ${s.direction.slice(0, 100)}`));
|
|
7372
|
+
});
|
|
7373
|
+
if (matched.length > 3) log(dm5(` ... +${matched.length - 3}건 더`));
|
|
7374
|
+
log(dm5(` → 본문: leerness api-skill show <id> | leerness api-skill list`));
|
|
7375
|
+
} else {
|
|
7376
|
+
const isTty = process.stdout && process.stdout.isTTY;
|
|
7377
|
+
const dm5 = s => isTty ? `\x1b[2m${s}\x1b[0m` : s;
|
|
7378
|
+
log(dm5(`📚 API skill ${allSkills.length}건 저장됨 (현재 task 매칭 0 — leerness api-skill list)`));
|
|
7379
|
+
}
|
|
7380
|
+
}
|
|
7381
|
+
} catch {}
|
|
7382
|
+
|
|
6998
7383
|
// 1.9.237: handoff 본문 release/* 누적 경고 — 50+ branches merged 시 자동 안내
|
|
6999
7384
|
// 1.9.220 비정상종료 release-branch-pending 신호의 본문 보강 (handoff 헤드라인은 ‘🔌 비정상종료’ 만 표시)
|
|
7000
7385
|
try {
|
|
@@ -10871,6 +11256,23 @@ function sessionClose(root, opts = {}) {
|
|
|
10871
11256
|
encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file)
|
|
10872
11257
|
};
|
|
10873
11258
|
} catch {}
|
|
11259
|
+
// 1.9.245: apiSkills 통합 (session close JSON 11번째 통합 필드) — UR-0015
|
|
11260
|
+
try {
|
|
11261
|
+
const allSkills = _listAPISkills(root);
|
|
11262
|
+
let currentTaskText = '';
|
|
11263
|
+
try {
|
|
11264
|
+
const rows = readProgressRows(root);
|
|
11265
|
+
const ip = rows.find(r => r.status === 'in-progress');
|
|
11266
|
+
if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
|
|
11267
|
+
} catch {}
|
|
11268
|
+
const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
|
|
11269
|
+
jsonResult.apiSkills = {
|
|
11270
|
+
total: allSkills.length,
|
|
11271
|
+
matched: matched.length,
|
|
11272
|
+
matchedIds: matched.slice(0, 5).map(s => s.id),
|
|
11273
|
+
ids: allSkills.slice(0, 10).map(s => s.id)
|
|
11274
|
+
};
|
|
11275
|
+
} catch {}
|
|
10874
11276
|
} catch {}
|
|
10875
11277
|
try {
|
|
10876
11278
|
// 1.9.209: pre-wake-audit 자동 실행 + 저장 (sleep 전 자동 점검)
|
|
@@ -15060,7 +15462,8 @@ function mcpServeCmd(root) {
|
|
|
15060
15462
|
{ name: 'leerness_release_cleanup', description: '1.9.236 (1.9.235 자동 회수) — local release/* branches 정리. main 에 merge된 것만 후보 (unmerged 보호, 현재 branch 보호). 응답: { apply, keep, total, merged, unmerged, deleteCount, toDelete[], recent[], unmergedSample[] }. 외부 AI가 "운영 누적 release branches 정리 가능?"을 회수. 기본 dry-run, apply: true 시에만 실 삭제. 인자: { path?, apply? (default false), keep? (default 5) }', inputSchema: { type: 'object', properties: { path: { type: 'string' }, apply: { type: 'boolean' }, keep: { type: 'number' } } } },
|
|
15061
15463
|
{ name: 'leerness_py_check', description: '1.9.239 (사용자 명시 UR-0013) — Python 파일 분석. 의존성 0 regex fallback. .md 외 .py 도 leerness 인지. 응답: { totalFiles, totalLOC, totalImports, totalFuncs, totalClasses, totalTodos, biggest: [{ file, loc, funcs, classes }] }. 외부 AI가 "이 프로젝트 Python 표면이 얼마나 되나"를 회수. 인자: { path? }', inputSchema: { type: 'object', properties: { path: { type: 'string' } } } },
|
|
15062
15464
|
{ name: 'leerness_agent_mode', description: '1.9.239 (사용자 명시 UR-0013) — 자율 모드 전용 통합 명령. start: handoff + drift --auto-fix + session-resume --auto-fix (진입). tick: pulse 한 줄 (매 라운드). stop: session close --auto-apply-delivered --auto-cleanup-branches (마감). 외부 AI 가 자율 라운드 진입/매 라운드/마감을 단일 호출로 수행. 인자: { path?, sub (required: "start"|"tick"|"stop"|"help") }', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string', enum: ['start', 'tick', 'stop', 'help'] } }, required: ['sub'] } },
|
|
15063
|
-
{ name: 'leerness_env_info', description: '1.9.241 (사용자 명시 UR-0014) — 환경 종합 정보 회수. OS / 언어 (LANG, 코드페이지) / 한국어 Windows / 하드웨어 / 터미널 (TTY, PowerShell 버전) / 도구 버전 (git, npm, python). 외부 AI 가 환경 호환성을 미리 인지 → 인코딩 오류 예방. 응답: { os, node, locale, hardware, terminal, tools }. 인자: { path?, encodingCheck? }. encodingCheck: true 시 셸 스크립트 (.ps1/.bat/.cmd/.sh) BOM 없는 비-ASCII 위험 감지', inputSchema: { type: 'object', properties: { path: { type: 'string' }, encodingCheck: { type: 'boolean' } } } }
|
|
15465
|
+
{ name: 'leerness_env_info', description: '1.9.241 (사용자 명시 UR-0014) — 환경 종합 정보 회수. OS / 언어 (LANG, 코드페이지) / 한국어 Windows / 하드웨어 / 터미널 (TTY, PowerShell 버전) / 도구 버전 (git, npm, python). 외부 AI 가 환경 호환성을 미리 인지 → 인코딩 오류 예방. 응답: { os, node, locale, hardware, terminal, tools }. 인자: { path?, encodingCheck? }. encodingCheck: true 시 셸 스크립트 (.ps1/.bat/.cmd/.sh) BOM 없는 비-ASCII 위험 감지', inputSchema: { type: 'object', properties: { path: { type: 'string' }, encodingCheck: { type: 'boolean' } } } },
|
|
15466
|
+
{ name: 'leerness_api_skill', description: '1.9.245 (사용자 명시 UR-0015) — API 문서·관련링크 자동 캐시. 공식 API 문서 URL을 fetch 하고 1단계 same-domain 관련 링크까지 정리 → .harness/api-skills/<id>.md 저장. 후속 같은 API 관련 작업 시 자동 참조. 응답: list (skills 배열) / show (전체 본문) / match (task 매칭 결과). 인자: { path?, sub ("list"|"show"|"match"|"add"|"drop"), url? (add), id? (show/drop), query? (match), direction? (add: 구현 방향 텍스트) }. 외부 AI가 "이 프로젝트 어떤 API 문서가 정리되어 있나?" / "내 작업과 매칭되는 API skill 있나?" 회수.', inputSchema: { type: 'object', properties: { path: { type: 'string' }, sub: { type: 'string', enum: ['list', 'show', 'match', 'add', 'drop'] }, url: { type: 'string' }, id: { type: 'string' }, query: { type: 'string' }, direction: { type: 'string' } }, required: ['sub'] } }
|
|
15064
15467
|
];
|
|
15065
15468
|
|
|
15066
15469
|
function send(obj) {
|
|
@@ -15249,6 +15652,14 @@ function mcpServeCmd(root) {
|
|
|
15249
15652
|
// 1.9.241 (UR-0014): 환경 종합 + 인코딩 위험 감지
|
|
15250
15653
|
cliArgs = ['env', args.encodingCheck === true ? 'encoding' : 'summary', '--path', targetPath, '--json'];
|
|
15251
15654
|
break;
|
|
15655
|
+
case 'leerness_api_skill':
|
|
15656
|
+
// 1.9.245 (UR-0015): API skill cache — list/show/match/add/drop
|
|
15657
|
+
cliArgs = ['api-skill', args.sub || 'list', '--path', targetPath, '--json'];
|
|
15658
|
+
if (args.sub === 'add' && args.url) cliArgs.push('--url', args.url);
|
|
15659
|
+
if (args.sub === 'add' && args.direction) cliArgs.push('--direction', args.direction);
|
|
15660
|
+
if ((args.sub === 'show' || args.sub === 'drop') && args.id) cliArgs.push('--id', args.id);
|
|
15661
|
+
if (args.sub === 'match' && args.query) cliArgs.push('--query', args.query);
|
|
15662
|
+
break;
|
|
15252
15663
|
case 'leerness_release_cleanup':
|
|
15253
15664
|
// 1.9.236 (1.9.235): local release/* branches 정리
|
|
15254
15665
|
cliArgs = ['release', 'cleanup', '--json'];
|
|
@@ -16391,6 +16802,74 @@ async function _agentRepl(root, opts) {
|
|
|
16391
16802
|
const modelTag = modelShort ? C.dim(' · ') + C.mag(modelShort) : '';
|
|
16392
16803
|
return C.cy(`agent[${state.provider}${modelTag}${C.cy('/' + state.role)}${state.streamMode ? C.cy('/▶') : ''}${C.cy(']>')} `);
|
|
16393
16804
|
};
|
|
16805
|
+
// 1.9.246: REPL UX/UI 개선 (사용자 명시 UR-0016) — 컨텍스트 게이지 + 서브 에이전트 가시화 + 정상 작업 강조
|
|
16806
|
+
// 3종 통합 status bar를 채팅 입력칸 직전에 표시.
|
|
16807
|
+
const _maxContextTokens = (provider, model) => {
|
|
16808
|
+
// provider/model 별 컨텍스트 윈도우 (대략값, 사용자 정보 제공용)
|
|
16809
|
+
const m = (model || '').toLowerCase();
|
|
16810
|
+
if (provider === 'claude') {
|
|
16811
|
+
if (m.includes('opus-4-7') || m.includes('sonnet-4-7') || m.includes('4-7')) return 1000000; // 1M extended
|
|
16812
|
+
if (m.includes('opus') || m.includes('sonnet') || m.includes('haiku')) return 200000;
|
|
16813
|
+
return 200000;
|
|
16814
|
+
}
|
|
16815
|
+
if (provider === 'codex' || provider === 'openai') {
|
|
16816
|
+
if (m.includes('gpt-5') || m.includes('5.5')) return 200000;
|
|
16817
|
+
if (m.includes('gpt-4-1') || m.includes('4.1')) return 1000000;
|
|
16818
|
+
if (m.includes('gpt-4')) return 128000;
|
|
16819
|
+
return 128000;
|
|
16820
|
+
}
|
|
16821
|
+
if (provider === 'gemini') return 1000000; // 2M까지 가능하지만 보수적으로 1M
|
|
16822
|
+
if (provider === 'copilot') return 64000;
|
|
16823
|
+
if (provider === 'ollama') return 8000; // 모델별 다르나 default 8k
|
|
16824
|
+
return 100000;
|
|
16825
|
+
};
|
|
16826
|
+
const _estimateTokens = (history) => {
|
|
16827
|
+
// 단순 휴리스틱: 영문 4chars=1tok, CJK 2chars=1tok 평균치 → 3.5chars/tok
|
|
16828
|
+
let totalChars = 0;
|
|
16829
|
+
for (const m of (history || [])) totalChars += (m.content || '').length;
|
|
16830
|
+
return Math.round(totalChars / 3.5);
|
|
16831
|
+
};
|
|
16832
|
+
const _renderStatusBar = () => {
|
|
16833
|
+
if (!isTty) return;
|
|
16834
|
+
try {
|
|
16835
|
+
// (1) 컨텍스트 게이지
|
|
16836
|
+
const msgs = (state.history || []).length;
|
|
16837
|
+
const tokens = _estimateTokens(state.history);
|
|
16838
|
+
const maxTok = _maxContextTokens(state.provider, state.model);
|
|
16839
|
+
const ratio = Math.min(1, tokens / maxTok);
|
|
16840
|
+
const pct = Math.round(ratio * 100);
|
|
16841
|
+
const barWidth = 14;
|
|
16842
|
+
const filled = Math.round(ratio * barWidth);
|
|
16843
|
+
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
|
|
16844
|
+
const barColor = pct >= 80 ? C.yel : (pct >= 50 ? C.cy : C.green);
|
|
16845
|
+
const tokStr = tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : `${tokens}`;
|
|
16846
|
+
const maxStr = maxTok >= 1000 ? `${Math.round(maxTok / 1000)}k` : `${maxTok}`;
|
|
16847
|
+
// (2) 서브 에이전트 활성화 — EXTERNAL_AGENTS 5종 + ollama
|
|
16848
|
+
let ready = [];
|
|
16849
|
+
try {
|
|
16850
|
+
ready = EXTERNAL_AGENTS
|
|
16851
|
+
.map(a => ({ id: a.id, st: _checkAgent(a).status }))
|
|
16852
|
+
.filter(x => x.st === 'ready');
|
|
16853
|
+
} catch {}
|
|
16854
|
+
const allAgents = ['claude', 'codex', 'gemini', 'copilot'];
|
|
16855
|
+
const subBar = allAgents.map(id => {
|
|
16856
|
+
if (id === state.provider) return C.bold(C.green(`*${id}`));
|
|
16857
|
+
const isReady = ready.find(r => r.id === id);
|
|
16858
|
+
return isReady ? C.green(`✓${id}`) : C.dim(`·${id}`);
|
|
16859
|
+
}).join(' ');
|
|
16860
|
+
// (3) 한 줄 출력
|
|
16861
|
+
const line = ' ' + C.dim('💬 ctx ') + C.bold(`${msgs}msg`) + C.dim(` ~${tokStr}/${maxStr} `) + barColor(bar) + C.dim(` ${pct}%`)
|
|
16862
|
+
+ C.dim(' · agents: ') + subBar
|
|
16863
|
+
+ C.dim(' · ') + (state.streamMode ? C.green('▶stream') : C.dim('□stream'));
|
|
16864
|
+
process.stdout.write(line + '\n');
|
|
16865
|
+
} catch {}
|
|
16866
|
+
};
|
|
16867
|
+
// 1.9.246: rl.prompt() wrapper — status bar 자동 표시 (사용자 명시 UR-0016)
|
|
16868
|
+
const promptWithStatus = (forceRedraw) => {
|
|
16869
|
+
_renderStatusBar();
|
|
16870
|
+
if (forceRedraw === true) rl.prompt(true);
|
|
16871
|
+
else rl.prompt();
|
|
16872
|
+
};
|
|
16394
16873
|
rl.setPrompt(prompt());
|
|
16395
16874
|
|
|
16396
16875
|
// 1.9.244 HOTFIX: _lastCycleLines 를 outer 스코프로 hoist
|
|
@@ -16552,7 +17031,8 @@ async function _agentRepl(root, opts) {
|
|
|
16552
17031
|
log(C.dim(' 💡 메시지는 직접 입력 · "/" 또는 ":" 접두사로 명령 호출 · Tab=provider · Shift+Tab=model'));
|
|
16553
17032
|
log('');
|
|
16554
17033
|
};
|
|
16555
|
-
|
|
17034
|
+
// 1.9.246: 첫 prompt 표시 전에도 status bar 노출 (사용자 명시 UR-0016)
|
|
17035
|
+
promptWithStatus();
|
|
16556
17036
|
const handleMeta = async (cmd) => {
|
|
16557
17037
|
const [op, ...rest] = cmd.slice(1).split(/\s+/);
|
|
16558
17038
|
if (op === 'quit' || op === 'exit' || op === 'q') {
|
|
@@ -16842,24 +17322,24 @@ async function _agentRepl(root, opts) {
|
|
|
16842
17322
|
rl.on('line', async (line) => {
|
|
16843
17323
|
const input = line.trim();
|
|
16844
17324
|
_lastCycleLines = 0; // 1.9.189: 사용자 입력 시 cycle overwrite 추적 reset
|
|
16845
|
-
if (!input) {
|
|
17325
|
+
if (!input) { promptWithStatus(); return; }
|
|
16846
17326
|
// 1.9.189 (사용자 명시): "/" slash 입력만으로 명령 list 표시 (claude code 영감).
|
|
16847
17327
|
// "/" 단독 → 명령 list 출력 후 prompt 복귀.
|
|
16848
17328
|
// "/help", "/quit" 등 "/" 접두사 → ":" 동등 처리 (alias).
|
|
16849
17329
|
if (input === '/' || input === '/?') {
|
|
16850
17330
|
_showSlashCommandList();
|
|
16851
|
-
|
|
17331
|
+
promptWithStatus(); return;
|
|
16852
17332
|
}
|
|
16853
17333
|
// 1.9.189: "/" 접두사를 ":" 와 동등하게 처리 (사용자 명시 — "/ 가 모든 모델 공통 권한 trigger default")
|
|
16854
17334
|
if (input.startsWith('/') && !input.startsWith('//')) {
|
|
16855
17335
|
const shouldQuit = await handleMeta(':' + input.slice(1));
|
|
16856
17336
|
if (shouldQuit) { resolve(); return; }
|
|
16857
|
-
|
|
17337
|
+
promptWithStatus(); return;
|
|
16858
17338
|
}
|
|
16859
17339
|
if (input.startsWith(':')) {
|
|
16860
17340
|
const shouldQuit = await handleMeta(input);
|
|
16861
17341
|
if (shouldQuit) { resolve(); return; }
|
|
16862
|
-
|
|
17342
|
+
promptWithStatus(); return;
|
|
16863
17343
|
}
|
|
16864
17344
|
// LLM 호출
|
|
16865
17345
|
state.history.push({ role: 'user', content: input });
|
|
@@ -16882,18 +17362,19 @@ async function _agentRepl(root, opts) {
|
|
|
16882
17362
|
}
|
|
16883
17363
|
} else {
|
|
16884
17364
|
log(C.yel(` ⚠ ${state.provider} provider 미지원 — :provider ollama|claude|codex|gemini|copilot`));
|
|
16885
|
-
|
|
17365
|
+
promptWithStatus(); return;
|
|
16886
17366
|
}
|
|
16887
17367
|
const dt = Date.now() - t0;
|
|
16888
17368
|
_recordRun(root, { kind: 'agent_repl_turn', provider: state.provider, model: state.model, role: state.role, durationMs: dt, ok: result.ok, error: result.error, promptChars: finalPrompt.length, responseChars: (result.response || '').length });
|
|
16889
17369
|
if (result.ok) {
|
|
16890
17370
|
state.history.push({ role: 'assistant', content: result.response });
|
|
16891
17371
|
// 1.9.170: stream 모드에서는 이미 실시간으로 출력됐으므로 헤더만 표시 (응답 중복 방지)
|
|
17372
|
+
// 1.9.246: 정상 완료 작업은 초록색 ✓ 강조 (사용자 명시 UR-0016)
|
|
16892
17373
|
if (state.streamMode && ['claude', 'codex', 'gemini', 'copilot'].includes(state.provider)) {
|
|
16893
|
-
log(C.
|
|
17374
|
+
log(C.green(` ✓ [assistant: ${state.provider}/${state.model || 'default'}, role=${state.role}, ${dt}ms · ${result.response.length}자]`));
|
|
16894
17375
|
} else {
|
|
16895
17376
|
log('');
|
|
16896
|
-
log(C.
|
|
17377
|
+
log(C.green(`✓ assistant (${state.model || state.provider}, role=${state.role}, ${dt}ms)`));
|
|
16897
17378
|
log(result.response);
|
|
16898
17379
|
}
|
|
16899
17380
|
// 1.9.188 (사용자 명시): 응답 끝 + 입력 구분선 (입력칸 시각 명확)
|
|
@@ -16924,7 +17405,7 @@ async function _agentRepl(root, opts) {
|
|
|
16924
17405
|
}
|
|
16925
17406
|
} catch {}
|
|
16926
17407
|
}
|
|
16927
|
-
|
|
17408
|
+
promptWithStatus();
|
|
16928
17409
|
});
|
|
16929
17410
|
rl.on('close', () => { saveSession(); resolve(); });
|
|
16930
17411
|
});
|
|
@@ -17511,6 +17992,23 @@ function healthCmd(root) {
|
|
|
17511
17992
|
encodingRiskFiles: encScan.atRisk.slice(0, 5).map(r => r.file)
|
|
17512
17993
|
};
|
|
17513
17994
|
} catch { out.envInfo = { error: 'envInfo 점검 실패' }; }
|
|
17995
|
+
// 1.9.245: health --json apiSkills 통합 (3 명령 11 필드 — UR-0015)
|
|
17996
|
+
try {
|
|
17997
|
+
const allSkills = _listAPISkills(root);
|
|
17998
|
+
let currentTaskText = '';
|
|
17999
|
+
try {
|
|
18000
|
+
const rows = readProgressRows(root);
|
|
18001
|
+
const ip = rows.find(r => r.status === 'in-progress');
|
|
18002
|
+
if (ip) currentTaskText = (ip.title || '') + ' ' + (ip.notes || '');
|
|
18003
|
+
} catch {}
|
|
18004
|
+
const matched = currentTaskText ? _matchAPISkills(root, currentTaskText) : [];
|
|
18005
|
+
out.apiSkills = {
|
|
18006
|
+
total: allSkills.length,
|
|
18007
|
+
matched: matched.length,
|
|
18008
|
+
matchedIds: matched.slice(0, 5).map(s => s.id),
|
|
18009
|
+
ids: allSkills.slice(0, 10).map(s => s.id)
|
|
18010
|
+
};
|
|
18011
|
+
} catch { out.apiSkills = { error: 'apiSkills 점검 실패' }; }
|
|
17514
18012
|
// 1.9.163: 5능력 매트릭스 자동 평가 (1.9.155 sub-agent 점검 → 코드 기반 자동화)
|
|
17515
18013
|
// 각 능력을 코드 grep 으로 검출 → 0~100 점수. 사용자가 매 health 호출 시 leerness 자기 평가 확인.
|
|
17516
18014
|
try {
|
|
@@ -18960,6 +19458,8 @@ async function main() {
|
|
|
18960
19458
|
// 기존 env check/sync/detect (1.9.71) 와 충돌하지 않음 — summary/encoding 신규 서브
|
|
18961
19459
|
if (cmd === 'env' && (args[1] === 'summary' || args[1] === 'encoding' || args[1] === 'encoding-check'))
|
|
18962
19460
|
return envCmd(arg('--path', process.cwd()), args[1] === 'summary' ? null : 'encoding-check');
|
|
19461
|
+
// 1.9.245: API skill cache — 공식 문서·관련링크 자동 정리 (사용자 명시 UR-0015)
|
|
19462
|
+
if (cmd === 'api-skill') return apiSkillCmd(arg('--path', process.cwd()), args[1] || 'help');
|
|
18963
19463
|
// 1.9.208: leerness constraints <list|check|add> — 플랫폼/API 제약 사전 체크 (사용자 명시)
|
|
18964
19464
|
if (cmd === 'constraints') return constraintsCmd(arg('--path', process.cwd()), args[1], ...args.slice(2));
|
|
18965
19465
|
// 1.9.209: leerness pre-wake-audit — sleep 전 sub-agent audit (사용자 명시)
|