leerness 1.9.145 → 1.9.146
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 +46 -0
- package/README.md +2 -2
- package/bin/harness.js +275 -34
- package/package.json +1 -1
- package/scripts/e2e.js +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.146 — 2026-05-20
|
|
4
|
+
|
|
5
|
+
**사용자 명시 요청 5종 통합** — CLI 에이전트 모드 + 권한 시스템 + install 흐름 재구성.
|
|
6
|
+
|
|
7
|
+
### 설계 결정 (사용자 질문에 대한 답)
|
|
8
|
+
- **agent 모드는 별도 명령** (`leerness agent`) — 기존 명령에 영향 없음
|
|
9
|
+
- **권한은 공유 시스템** (`.harness/agent-permissions.json`) — basic/extended/full 프리셋
|
|
10
|
+
- **IDE 통합은 자동** — IDE가 leerness CLI 호출 시 `agent-permissions.json` 자동 적용 (별도 모드 불필요)
|
|
11
|
+
|
|
12
|
+
### ① 스킬 라이브러리 단순화 (사용자 요청)
|
|
13
|
+
- 인터랙티브 옵션 2개로 축소: **"표준 공식 5종 자동 설치"** / **"건너뛰기"**
|
|
14
|
+
- 이전: 빌트인 카탈로그 전체 + 추천 default + 직접 입력 등
|
|
15
|
+
- 비대화형 (--yes / --skills) 동작은 그대로
|
|
16
|
+
|
|
17
|
+
### ② install 흐름 재구성 (사용자 요청)
|
|
18
|
+
- 모든 prompt (언어 / 스킬 / agent 활성화 / 권한 모드) 응답 완료 후 → 일괄 설치
|
|
19
|
+
- 응답 수집 단계와 파일 생성 단계 명확히 분리
|
|
20
|
+
- "📦 응답 수집 완료 — leerness 파일 설치 시작" 메시지
|
|
21
|
+
|
|
22
|
+
### ③ Ollama CLI 에이전트 활성화 추가 (사용자 요청)
|
|
23
|
+
- `EXTERNAL_AGENTS` 에 ollama 추가 — `LEERNESS_ENABLE_OLLAMA=1`
|
|
24
|
+
- HTTP API (기본 `http://localhost:11434`), 모델 env: `LEERNESS_OLLAMA_MODEL`
|
|
25
|
+
- `agents list` 표에 표시
|
|
26
|
+
- install prompt: "Claude 단일 / Ollama 단일 / 전체 / 활성화 안함" 4지선다
|
|
27
|
+
|
|
28
|
+
### ④ leerness agent — 오픈소스 CLI 에이전트 모드 (사용자 요청)
|
|
29
|
+
- `leerness agent "<task 설명>"` — OpenClaw/Hermes 스타일
|
|
30
|
+
- handoff context 자동 회수 (compact preview)
|
|
31
|
+
- Ollama HTTP API 직접 호출 (MVP) — `_ollamaChat(prompt, model)`
|
|
32
|
+
- 다른 provider 는 `leerness agents dispatch` 또는 외부 CLI 직접 호출 안내
|
|
33
|
+
- `--dry-run` / `--provider <name>` 지원
|
|
34
|
+
- task-log 자동 기록
|
|
35
|
+
|
|
36
|
+
### ⑤ Agent 권한 시스템 (사용자 요청)
|
|
37
|
+
- `.harness/agent-permissions.json` — basic / extended / full 프리셋
|
|
38
|
+
- **basic**: `.harness/` 만 read/write, shell/network/mouse/keyboard/browser/admin 거부 (deny-by-default)
|
|
39
|
+
- **extended**: 프로젝트 폴더 + shell allowlist (npm/git/node/pnpm/yarn/pytest/jest/tsc), network localhost/github/npm 만
|
|
40
|
+
- **full**: 마우스/키보드/웹/관리자 전체 ⚠ IDE 통합 시에만 권장
|
|
41
|
+
- `leerness permissions list --json` 조회
|
|
42
|
+
- `leerness permissions set <mode>` 변경
|
|
43
|
+
- `permissionCheck(root, action, target)` 내부 헬퍼 — agent 작업 시 사전 검증
|
|
44
|
+
|
|
45
|
+
### Validation
|
|
46
|
+
- stress-v91: PASS
|
|
47
|
+
- e2e: 219/219 PASS
|
|
48
|
+
|
|
3
49
|
## 1.9.145 — 2026-05-20
|
|
4
50
|
|
|
5
51
|
**실행 환경 자동 감지 — 사용자 명시 요청.**
|
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,7 +12,7 @@
|
|
|
12
12
|
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
13
13
|
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
14
14
|
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
15
|
-
║ v1.9.
|
|
15
|
+
║ v1.9.146 AI Agent Reliability Harness ║
|
|
16
16
|
║ verify · remember · orchestrate · audit · prevent drift ║
|
|
17
17
|
╚══════════════════════════════════════════════════════════════╝
|
|
18
18
|
```
|
package/bin/harness.js
CHANGED
|
@@ -6,7 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const cp = require('child_process');
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
|
|
9
|
-
const VERSION = '1.9.
|
|
9
|
+
const VERSION = '1.9.146';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -681,35 +681,61 @@ async function resolveInstallOptions(root, opts = {}) {
|
|
|
681
681
|
lang = a === '2' ? 'ko' : a === '3' ? 'en' : detectLanguageValue(root, 'auto');
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
|
+
// 1.9.146: 스킬 라이브러리 — 표준 공식 추천 자동 설치 / 건너뛰기 2-option 단순화 (사용자 명시 요청 #1)
|
|
684
685
|
if (shouldAsk && !explicitSkills) {
|
|
685
686
|
if (useInteractive) {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
})
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
687
|
+
const opt = await _selectOne('스킬 라이브러리 설치 (표준 공식 5종)', [
|
|
688
|
+
{ label: '표준 공식 5종 자동 설치 (추천)', description: 'office · commerce-api · ai-verified-skill-publisher · feature-implementation · project-roadmap-generator', id: 'recommended' },
|
|
689
|
+
{ label: '건너뛰기 (필요할 때 leerness skill install 로 추가)', description: '하네스만 설치, 스킬은 없음', id: 'none' }
|
|
690
|
+
], { defaultIndex: 0 });
|
|
691
|
+
skills = (opt && opt.id === 'recommended') ? parseSkillsValue('recommended') : [];
|
|
692
|
+
} else {
|
|
693
|
+
log('\n스킬 라이브러리 설치를 선택하세요.');
|
|
694
|
+
log('1) 표준 공식 5종 자동 설치 (추천)');
|
|
695
|
+
log('2) 건너뛰기 (leerness skill install 로 추가 가능)');
|
|
696
|
+
const a = await ask('선택 [1]: ');
|
|
697
|
+
skills = (a === '2') ? [] : parseSkillsValue('recommended');
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// 1.9.146: CLI 에이전트 활성화 선택 (사용자 명시 요청 #3 — Ollama 추가)
|
|
701
|
+
// 설치 마지막에 .env.example 에 활성화 옵트인 키만 기록 (실제 토큰 입력은 사용자가 직접).
|
|
702
|
+
let agentsOptIn = null;
|
|
703
|
+
if (shouldAsk && !opts._skipAgentsPrompt) {
|
|
704
|
+
if (useInteractive) {
|
|
705
|
+
const aOpt = await _selectOne('CLI 에이전트 활성화 (sub-agent 위임용, opt-in)', [
|
|
706
|
+
{ label: '활성화 안함 (나중에 .env에서 직접 설정)', description: '권장 — 토큰/모델은 사용자가 직접 관리', id: 'none' },
|
|
707
|
+
{ label: 'Claude (LEERNESS_CLAUDE_ENABLED=1)', description: 'claude CLI 또는 ANTHROPIC_API_KEY', id: 'claude' },
|
|
708
|
+
{ label: 'Ollama (LEERNESS_OLLAMA_ENABLED=1) — 로컬 LLM', description: 'http://localhost:11434 (모델: llama3/qwen 등)', id: 'ollama' },
|
|
709
|
+
{ label: '여러 개 (claude+codex+gemini+copilot+ollama)', description: '전체 후보 활성화 (각각 별도 토큰 필요)', id: 'all' }
|
|
710
|
+
], { defaultIndex: 0 });
|
|
711
|
+
agentsOptIn = aOpt ? aOpt.id : 'none';
|
|
699
712
|
} else {
|
|
700
|
-
log('\
|
|
701
|
-
log('
|
|
702
|
-
log('1) 추천: office, commerce-api, ai-verified-skill-publisher, feature-implementation');
|
|
703
|
-
log('2) 전체 스킬 설치'); log('3) 직접 입력');
|
|
704
|
-
skillList();
|
|
713
|
+
log('\nCLI 에이전트 활성화 (opt-in, 나중에 .env에서 변경 가능):');
|
|
714
|
+
log('1) 활성화 안함 2) Claude 3) Ollama 4) 전체');
|
|
705
715
|
const a = await ask('선택 [1]: ');
|
|
706
|
-
|
|
707
|
-
else if (a === '2') skills = parseSkillsValue('all');
|
|
708
|
-
else if (a === '3') skills = parseSkillsValue(await ask('스킬 ID를 쉼표로 입력: '));
|
|
709
|
-
else if (a === '0') skills = [];
|
|
716
|
+
agentsOptIn = a === '2' ? 'claude' : a === '3' ? 'ollama' : a === '4' ? 'all' : 'none';
|
|
710
717
|
}
|
|
711
718
|
}
|
|
712
|
-
|
|
719
|
+
// 1.9.146: 권한 모드 (사용자 명시 요청 #5 — agent IDE 모드 사전 prompt)
|
|
720
|
+
let permissionMode = null;
|
|
721
|
+
if (shouldAsk && !opts._skipPermissionsPrompt) {
|
|
722
|
+
if (useInteractive) {
|
|
723
|
+
const pOpt = await _selectOne('agent 권한 모드 (leerness agent 사용 시 적용)', [
|
|
724
|
+
{ label: 'basic (안전) — 읽기/쓰기 .harness/ 만', description: '권장 — 파일시스템/네트워크 거부, .harness 안만 쓰기', id: 'basic' },
|
|
725
|
+
{ label: 'extended — 프로젝트 폴더 + shell allowlist', description: '프로젝트 폴더 read/write, 사전 정의된 명령만 exec', id: 'extended' },
|
|
726
|
+
{ label: 'full — 전체 (마우스/키보드/웹/관리자) ⚠ 위험', description: '⚠ IDE 통합 시에만 권장 — 모든 PC 작업 가능', id: 'full' }
|
|
727
|
+
], { defaultIndex: 0 });
|
|
728
|
+
permissionMode = pOpt ? pOpt.id : 'basic';
|
|
729
|
+
} else {
|
|
730
|
+
log('\nagent 권한 모드 (leerness agent 명령 사용 시):');
|
|
731
|
+
log('1) basic (안전) — .harness/ 만');
|
|
732
|
+
log('2) extended — 프로젝트 폴더 + shell allowlist');
|
|
733
|
+
log('3) full ⚠ — 마우스/키보드/웹/관리자 전체 (IDE 통합 시에만)');
|
|
734
|
+
const a = await ask('선택 [1]: ');
|
|
735
|
+
permissionMode = a === '2' ? 'extended' : a === '3' ? 'full' : 'basic';
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return { lang, skills, agentsOptIn, permissionMode };
|
|
713
739
|
}
|
|
714
740
|
|
|
715
741
|
async function install(root, opts = {}) {
|
|
@@ -731,10 +757,13 @@ async function install(root, opts = {}) {
|
|
|
731
757
|
const resolved = await resolveInstallOptions(root, opts);
|
|
732
758
|
const lang = resolved.lang;
|
|
733
759
|
const skills = resolved.skills;
|
|
734
|
-
|
|
760
|
+
// 1.9.146: 사용자 명시 요청 #2 — 모든 prompt 끝난 후 한꺼번에 설치 단계 진입 (응답 수집과 파일 쓰기 분리)
|
|
761
|
+
log(`\n📦 응답 수집 완료 — leerness 파일 설치 시작 (Leerness v${VERSION})`);
|
|
735
762
|
log(`Target: ${root}`);
|
|
736
763
|
log(`Language: ${lang}`);
|
|
737
|
-
log(`Skills: ${skills.length ? skills.join(', ') : 'none'}`);
|
|
764
|
+
log(`Skills: ${skills.length ? skills.join(', ') : 'none (건너뜀)'}`);
|
|
765
|
+
if (resolved.agentsOptIn && resolved.agentsOptIn !== 'none') log(`Agents 활성화: ${resolved.agentsOptIn}`);
|
|
766
|
+
if (resolved.permissionMode) log(`Agent 권한 모드: ${resolved.permissionMode}`);
|
|
738
767
|
// 1.9.10: 스킬 카탈로그 출처 안내
|
|
739
768
|
if (SKILLPACK_SOURCE === 'builtin') log(`Skill catalog source: builtin (leerness-skillpack 미설치 — \`npm i leerness-skillpack\`로 확장 가능)`);
|
|
740
769
|
else log(`Skill catalog source: ${SKILLPACK_SOURCE} (leerness-skillpack${SKILLPACK_META ? ` v${SKILLPACK_META.version}` : ''})`);
|
|
@@ -775,24 +804,32 @@ async function install(root, opts = {}) {
|
|
|
775
804
|
'.harness/skill-publish.local.json','.harness/**/*.local.json','.env.local',
|
|
776
805
|
'.harness/archive/','.harness/migration-report.md','.harness/cache/'
|
|
777
806
|
]);
|
|
807
|
+
// 1.9.146: agentsOptIn 선택에 따라 LEERNESS_ENABLE_* 플래그 자동 설정 (사용자 명시 요청 #3 — Ollama 추가)
|
|
808
|
+
const a = resolved.agentsOptIn || 'none';
|
|
809
|
+
const enable = (cli) => a === 'all' || a === cli;
|
|
778
810
|
mergeLinesFile(path.join(root, '.env.example'), [
|
|
779
811
|
'# Leerness uses environment variable names only. Do not store real secrets here.',
|
|
780
812
|
'LEERNESS_NPM_TOKEN=','LEERNESS_GITHUB_TOKEN=',
|
|
781
813
|
'# 1.9.22 — orchestrate opt-in. URL이 설정되면 leerness가 Ollama를 사용 가능. 미설정 시 LLM 호출 자동 시작 금지.',
|
|
782
|
-
|
|
783
|
-
'# 선택. 기본 모델 (orchestrate --model 로 override 가능).',
|
|
814
|
+
`LEERNESS_OLLAMA_BASE_URL=${enable('ollama') ? 'http://localhost:11434' : ''}`,
|
|
815
|
+
'# 선택. 기본 모델 (orchestrate --model 로 override 가능). 예: llama3 / qwen2.5-coder / gpt-oss',
|
|
784
816
|
'LEERNESS_OLLAMA_MODEL=',
|
|
785
|
-
'# 1.9.30 — 외부 AI CLI 활성화 플래그. 1=활성, 0/미설정=비활성. 메인 에이전트가 sub-agent 분배 시 활성 CLI들에 작업 위임 가능.',
|
|
786
|
-
'
|
|
787
|
-
'
|
|
788
|
-
'
|
|
789
|
-
'
|
|
817
|
+
'# 1.9.30+1.9.146 — 외부 AI CLI 활성화 플래그. 1=활성, 0/미설정=비활성. 메인 에이전트가 sub-agent 분배 시 활성 CLI들에 작업 위임 가능.',
|
|
818
|
+
`LEERNESS_ENABLE_CLAUDE=${enable('claude') ? 1 : 0}`,
|
|
819
|
+
`LEERNESS_ENABLE_CODEX=${enable('all') ? 1 : 0}`,
|
|
820
|
+
`LEERNESS_ENABLE_GEMINI=${enable('all') ? 1 : 0}`,
|
|
821
|
+
`LEERNESS_ENABLE_COPILOT=${enable('all') ? 1 : 0}`,
|
|
822
|
+
`LEERNESS_ENABLE_OLLAMA=${enable('ollama') ? 1 : 0}`,
|
|
790
823
|
'# 1.9.42 — agentskills.io 공개 표준 스킬 자동 탐색 (opt-in). URL 설정 시 `leerness skill discover` 사용 가능.',
|
|
791
824
|
'# 예: LEERNESS_SKILL_DISCOVER_URL=https://agentskills.io/llms.txt',
|
|
792
825
|
'LEERNESS_SKILL_DISCOVER_URL=',
|
|
793
826
|
'# (선택) 사용자 요청 분석 시 자동 매칭 스킬 추천. 1=활성, 0/미설정=비활성.',
|
|
794
827
|
'LEERNESS_SKILL_AUTO_DISCOVER=0'
|
|
795
828
|
]);
|
|
829
|
+
// 1.9.146: agent 권한 파일 자동 생성 (사용자 명시 요청 #5)
|
|
830
|
+
if (resolved.permissionMode) {
|
|
831
|
+
try { _writePermissionsPreset(root, resolved.permissionMode); } catch (e) { warn('permissions 생성 실패: ' + e.message); }
|
|
832
|
+
}
|
|
796
833
|
mergeLinesFile(path.join(root, '.gitattributes'), [
|
|
797
834
|
'* text=auto eol=lf','*.bat text eol=crlf','*.ps1 text eol=crlf'
|
|
798
835
|
]);
|
|
@@ -4430,7 +4467,10 @@ const EXTERNAL_AGENTS = [
|
|
|
4430
4467
|
{ id: 'gemini', bin: 'gemini', envFlag: 'LEERNESS_ENABLE_GEMINI', versionArgs: ['--version'], desc: 'Google Gemini CLI (--yolo 모드 워크스페이스 직접 수정 가능)',
|
|
4431
4468
|
installCmd: 'npm i -g @google/gemini-cli', installHint: 'https://github.com/google-gemini/gemini-cli' },
|
|
4432
4469
|
{ id: 'copilot', bin: 'gh', envFlag: 'LEERNESS_ENABLE_COPILOT', versionArgs: ['copilot', '--version'], desc: 'GitHub Copilot CLI (gh copilot)',
|
|
4433
|
-
installCmd: 'gh extension install github/gh-copilot', installHint: 'https://github.com/github/gh-copilot (gh CLI 선행 설치 필요)' }
|
|
4470
|
+
installCmd: 'gh extension install github/gh-copilot', installHint: 'https://github.com/github/gh-copilot (gh CLI 선행 설치 필요)' },
|
|
4471
|
+
// 1.9.146: Ollama 추가 (사용자 명시 요청 #3) — 로컬 LLM, HTTP API 11434
|
|
4472
|
+
{ id: 'ollama', bin: 'ollama', envFlag: 'LEERNESS_ENABLE_OLLAMA', versionArgs: ['--version'], desc: 'Ollama 로컬 LLM (http://localhost:11434, llama3/qwen 등)',
|
|
4473
|
+
installCmd: 'curl -fsSL https://ollama.com/install.sh | sh (또는 https://ollama.com/download)', installHint: 'ollama serve 실행 + ollama pull <model>' }
|
|
4434
4474
|
];
|
|
4435
4475
|
|
|
4436
4476
|
// 1.9.36: 작업 키워드 분석으로 최적 CLI 추천
|
|
@@ -9786,6 +9826,203 @@ function envDetectCmd(root, opts = {}) {
|
|
|
9786
9826
|
if (diff.missing && diff.missing.length) process.exitCode = 1;
|
|
9787
9827
|
}
|
|
9788
9828
|
|
|
9829
|
+
// ===== 1.9.146: Agent 권한 시스템 (사용자 명시 요청 #5) =====
|
|
9830
|
+
// .harness/agent-permissions.json — leerness agent 명령 실행 시 적용. 기본 deny-by-default.
|
|
9831
|
+
function _permissionsPath(root) { return path.join(absRoot(root), '.harness', 'agent-permissions.json'); }
|
|
9832
|
+
function _permissionsPreset(mode) {
|
|
9833
|
+
// basic: 안전 — .harness/ 안만 쓰기
|
|
9834
|
+
// extended: 프로젝트 폴더 + shell allowlist
|
|
9835
|
+
// full: 전체 (mouse/keyboard/web/admin) — IDE 통합 시
|
|
9836
|
+
const presets = {
|
|
9837
|
+
basic: {
|
|
9838
|
+
mode: 'basic',
|
|
9839
|
+
filesystem: { read: true, write: true, restrictTo: ['.harness/', 'progress-tracker.md', 'session-handoff.md'], delete: false },
|
|
9840
|
+
shell: { exec: false, allowList: [] },
|
|
9841
|
+
network: { fetch: false, outboundAllowList: [] },
|
|
9842
|
+
mouse: false, keyboard: false, browser: false, admin: false,
|
|
9843
|
+
requireConfirmation: ['shell.exec', 'filesystem.delete', 'network.fetch']
|
|
9844
|
+
},
|
|
9845
|
+
extended: {
|
|
9846
|
+
mode: 'extended',
|
|
9847
|
+
filesystem: { read: true, write: true, restrictTo: ['./'], delete: false },
|
|
9848
|
+
shell: { exec: true, allowList: ['npm', 'git', 'node', 'pnpm', 'yarn', 'pytest', 'jest', 'tsc'] },
|
|
9849
|
+
network: { fetch: true, outboundAllowList: ['localhost', 'github.com', 'api.github.com', 'npmjs.org'] },
|
|
9850
|
+
mouse: false, keyboard: false, browser: false, admin: false,
|
|
9851
|
+
requireConfirmation: ['filesystem.delete', 'shell.exec_outside_allowlist']
|
|
9852
|
+
},
|
|
9853
|
+
full: {
|
|
9854
|
+
mode: 'full',
|
|
9855
|
+
filesystem: { read: true, write: true, restrictTo: ['./'], delete: true },
|
|
9856
|
+
shell: { exec: true, allowList: ['*'] },
|
|
9857
|
+
network: { fetch: true, outboundAllowList: ['*'] },
|
|
9858
|
+
mouse: true, keyboard: true, browser: true, admin: true,
|
|
9859
|
+
requireConfirmation: ['filesystem.delete_outside_project', 'admin_action']
|
|
9860
|
+
}
|
|
9861
|
+
};
|
|
9862
|
+
return presets[mode] || presets.basic;
|
|
9863
|
+
}
|
|
9864
|
+
function _writePermissionsPreset(root, mode) {
|
|
9865
|
+
const preset = _permissionsPreset(mode);
|
|
9866
|
+
preset.generatedAt = new Date().toISOString();
|
|
9867
|
+
preset.leernessVersion = VERSION;
|
|
9868
|
+
mkdirp(path.dirname(_permissionsPath(root)));
|
|
9869
|
+
writeUtf8(_permissionsPath(root), JSON.stringify(preset, null, 2) + '\n');
|
|
9870
|
+
return preset;
|
|
9871
|
+
}
|
|
9872
|
+
function _readPermissions(root) {
|
|
9873
|
+
const p = _permissionsPath(root);
|
|
9874
|
+
if (!exists(p)) return _permissionsPreset('basic');
|
|
9875
|
+
try { return JSON.parse(read(p)); } catch { return _permissionsPreset('basic'); }
|
|
9876
|
+
}
|
|
9877
|
+
function permissionsListCmd(root) {
|
|
9878
|
+
root = absRoot(root || process.cwd());
|
|
9879
|
+
const p = _readPermissions(root);
|
|
9880
|
+
if (has('--json')) { log(JSON.stringify(p, null, 2)); return; }
|
|
9881
|
+
log(`# leerness permissions (1.9.146)`);
|
|
9882
|
+
log(`mode: ${p.mode || 'basic'} · generated: ${p.generatedAt || '(없음)'}`);
|
|
9883
|
+
log('');
|
|
9884
|
+
log(`📂 filesystem: read=${p.filesystem?.read} write=${p.filesystem?.write} delete=${p.filesystem?.delete}`);
|
|
9885
|
+
if (p.filesystem?.restrictTo?.length) log(` restrict to: ${p.filesystem.restrictTo.join(', ')}`);
|
|
9886
|
+
log(`💻 shell.exec: ${p.shell?.exec} · allowList: ${(p.shell?.allowList || []).join(', ') || '(없음)'}`);
|
|
9887
|
+
log(`🌐 network.fetch: ${p.network?.fetch} · outbound: ${(p.network?.outboundAllowList || []).join(', ') || '(없음)'}`);
|
|
9888
|
+
log(`🖱 mouse=${p.mouse} ⌨ keyboard=${p.keyboard} 🌐 browser=${p.browser} 👑 admin=${p.admin}`);
|
|
9889
|
+
if (p.requireConfirmation?.length) log(`\n⚠ 확인 필요: ${p.requireConfirmation.join(', ')}`);
|
|
9890
|
+
}
|
|
9891
|
+
function permissionsSetCmd(root, mode) {
|
|
9892
|
+
root = absRoot(root || process.cwd());
|
|
9893
|
+
if (!['basic', 'extended', 'full'].includes(mode)) {
|
|
9894
|
+
return fail(`mode 는 basic / extended / full — 받음: ${mode || '(없음)'}`);
|
|
9895
|
+
}
|
|
9896
|
+
const p = _writePermissionsPreset(root, mode);
|
|
9897
|
+
ok(`permissions mode set: ${p.mode}`);
|
|
9898
|
+
log(` → 수정: ${_permissionsPath(root).replace(root, '.').replace(/\\/g, '/')}`);
|
|
9899
|
+
if (mode === 'full') warn(`⚠ full 모드 — IDE 통합 외 환경에서는 위험. agent 작업 시작 전 leerness permissions list 로 재확인 권장.`);
|
|
9900
|
+
}
|
|
9901
|
+
function permissionCheck(root, action, target) {
|
|
9902
|
+
// leerness agent 호출 시 권한 검증 — true(허용) / false(거부) 반환
|
|
9903
|
+
const p = _readPermissions(root);
|
|
9904
|
+
try {
|
|
9905
|
+
if (action === 'filesystem.read') return !!p.filesystem?.read;
|
|
9906
|
+
if (action === 'filesystem.write') {
|
|
9907
|
+
if (!p.filesystem?.write) return false;
|
|
9908
|
+
const restrict = p.filesystem?.restrictTo || [];
|
|
9909
|
+
if (!restrict.length || restrict.includes('./') || restrict.includes('*')) return true;
|
|
9910
|
+
return restrict.some(prefix => (target || '').startsWith(prefix));
|
|
9911
|
+
}
|
|
9912
|
+
if (action === 'filesystem.delete') return !!p.filesystem?.delete;
|
|
9913
|
+
if (action === 'shell.exec') {
|
|
9914
|
+
if (!p.shell?.exec) return false;
|
|
9915
|
+
const allow = p.shell?.allowList || [];
|
|
9916
|
+
if (allow.includes('*')) return true;
|
|
9917
|
+
const first = String(target || '').split(/\s+/)[0];
|
|
9918
|
+
return allow.includes(first);
|
|
9919
|
+
}
|
|
9920
|
+
if (action === 'network.fetch') return !!p.network?.fetch;
|
|
9921
|
+
if (action === 'mouse') return !!p.mouse;
|
|
9922
|
+
if (action === 'keyboard') return !!p.keyboard;
|
|
9923
|
+
if (action === 'browser') return !!p.browser;
|
|
9924
|
+
if (action === 'admin') return !!p.admin;
|
|
9925
|
+
} catch {}
|
|
9926
|
+
return false;
|
|
9927
|
+
}
|
|
9928
|
+
|
|
9929
|
+
// ===== 1.9.146: leerness agent — OpenClaw/Hermes 스타일 오픈소스 CLI 에이전트 모드 (사용자 명시 요청 #4) =====
|
|
9930
|
+
// MVP: handoff 컨텍스트 자동 로드 → 활성 CLI (claude/codex/gemini/ollama) 1개에 작업 위임.
|
|
9931
|
+
// 권한은 .harness/agent-permissions.json 기준. 실제 LLM 호출은 외부 CLI 또는 Ollama HTTP API.
|
|
9932
|
+
function _activeCliAgents() {
|
|
9933
|
+
const out = [];
|
|
9934
|
+
if (process.env.LEERNESS_ENABLE_CLAUDE === '1') out.push('claude');
|
|
9935
|
+
if (process.env.LEERNESS_ENABLE_CODEX === '1') out.push('codex');
|
|
9936
|
+
if (process.env.LEERNESS_ENABLE_GEMINI === '1') out.push('gemini');
|
|
9937
|
+
if (process.env.LEERNESS_ENABLE_COPILOT === '1') out.push('copilot');
|
|
9938
|
+
if (process.env.LEERNESS_ENABLE_OLLAMA === '1') out.push('ollama');
|
|
9939
|
+
return out;
|
|
9940
|
+
}
|
|
9941
|
+
async function _ollamaChat(prompt, model) {
|
|
9942
|
+
// Ollama HTTP API — 기본 http://localhost:11434/api/generate
|
|
9943
|
+
const url = (process.env.LEERNESS_OLLAMA_BASE_URL || 'http://localhost:11434').replace(/\/+$/, '') + '/api/generate';
|
|
9944
|
+
const mdl = model || process.env.LEERNESS_OLLAMA_MODEL || 'llama3';
|
|
9945
|
+
return new Promise((resolve) => {
|
|
9946
|
+
try {
|
|
9947
|
+
const body = JSON.stringify({ model: mdl, prompt, stream: false });
|
|
9948
|
+
const u = new URL(url);
|
|
9949
|
+
const lib = u.protocol === 'https:' ? require('https') : require('http');
|
|
9950
|
+
const req = lib.request({
|
|
9951
|
+
hostname: u.hostname, port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
9952
|
+
path: u.pathname + (u.search || ''), method: 'POST',
|
|
9953
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
9954
|
+
timeout: 60000
|
|
9955
|
+
}, (res) => {
|
|
9956
|
+
let data = '';
|
|
9957
|
+
res.on('data', c => { data += c; });
|
|
9958
|
+
res.on('end', () => {
|
|
9959
|
+
try { const j = JSON.parse(data); resolve({ ok: res.statusCode === 200, response: j.response || '', model: mdl }); }
|
|
9960
|
+
catch { resolve({ ok: false, error: 'invalid JSON response', model: mdl }); }
|
|
9961
|
+
});
|
|
9962
|
+
});
|
|
9963
|
+
req.on('error', e => resolve({ ok: false, error: e.message, model: mdl }));
|
|
9964
|
+
req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout', model: mdl }); });
|
|
9965
|
+
req.write(body); req.end();
|
|
9966
|
+
} catch (e) { resolve({ ok: false, error: e.message, model: mdl }); }
|
|
9967
|
+
});
|
|
9968
|
+
}
|
|
9969
|
+
async function agentCmd(root, taskArg) {
|
|
9970
|
+
root = absRoot(root || process.cwd());
|
|
9971
|
+
const task = (taskArg || arg('--task', '') || '').trim();
|
|
9972
|
+
if (!task) {
|
|
9973
|
+
log('# leerness agent (1.9.146) — 오픈소스 CLI 에이전트 모드');
|
|
9974
|
+
log('');
|
|
9975
|
+
log('사용법:');
|
|
9976
|
+
log(' leerness agent "<task 설명>" # 1회 위임');
|
|
9977
|
+
log(' leerness agent --provider ollama # 명시적 provider 선택');
|
|
9978
|
+
log(' leerness agent --dry-run # LLM 호출 없이 흐름만 확인');
|
|
9979
|
+
log('');
|
|
9980
|
+
log('현재 활성 provider: ' + (_activeCliAgents().join(', ') || '(없음) — .env에서 LEERNESS_ENABLE_* 활성화'));
|
|
9981
|
+
log('권한 모드: ' + (_readPermissions(root).mode || 'basic'));
|
|
9982
|
+
return;
|
|
9983
|
+
}
|
|
9984
|
+
const dryRun = has('--dry-run');
|
|
9985
|
+
const providerArg = arg('--provider', null);
|
|
9986
|
+
const active = _activeCliAgents();
|
|
9987
|
+
const provider = providerArg || active[0] || null;
|
|
9988
|
+
log(`# leerness agent (1.9.146)`);
|
|
9989
|
+
log(`task: ${task.slice(0, 120)}${task.length > 120 ? '…' : ''}`);
|
|
9990
|
+
log(`provider: ${provider || '(없음 — .env 에서 LEERNESS_ENABLE_* 활성화 필요)'}`);
|
|
9991
|
+
const perms = _readPermissions(root);
|
|
9992
|
+
log(`permission mode: ${perms.mode || 'basic'}`);
|
|
9993
|
+
// handoff 자동 회수 (compact 모드)
|
|
9994
|
+
try {
|
|
9995
|
+
const hf = cp.spawnSync(process.execPath, [__filename, 'handoff', root, '--compact', '--no-drift-check'], { encoding: 'utf8', timeout: 10000, env: { ...process.env, LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1' } });
|
|
9996
|
+
if (hf.status === 0 && hf.stdout) {
|
|
9997
|
+
const preview = hf.stdout.split('\n').slice(0, 6).join('\n');
|
|
9998
|
+
log('\n[handoff context (preview)]\n' + preview);
|
|
9999
|
+
}
|
|
10000
|
+
} catch {}
|
|
10001
|
+
if (dryRun) { log('\n(dry-run) LLM 호출 스킵 — provider/권한/컨텍스트만 출력'); return; }
|
|
10002
|
+
if (!provider) { fail('활성 provider 없음 — .env 에서 LEERNESS_ENABLE_OLLAMA=1 또는 LEERNESS_ENABLE_CLAUDE=1 활성화'); process.exitCode = 1; return; }
|
|
10003
|
+
// MVP: Ollama 지원 (로컬). 다른 CLI 는 사용자가 직접 호출 (leerness agents dispatch 이미 존재).
|
|
10004
|
+
if (provider === 'ollama') {
|
|
10005
|
+
log('\n[ollama 호출 중...]');
|
|
10006
|
+
const r = await _ollamaChat(task);
|
|
10007
|
+
if (r.ok) {
|
|
10008
|
+
log('\n[response (model=' + r.model + ')]\n' + r.response);
|
|
10009
|
+
// task-log 자동 기록
|
|
10010
|
+
try {
|
|
10011
|
+
const tlp = taskLogPath(root);
|
|
10012
|
+
const block = `\n## ${today()} leerness agent (ollama:${r.model})\n- task: ${task.slice(0, 200)}\n- response (preview): ${r.response.slice(0, 240).replace(/\n+/g, ' ')}\n`;
|
|
10013
|
+
append(tlp, block);
|
|
10014
|
+
} catch {}
|
|
10015
|
+
} else {
|
|
10016
|
+
fail(`ollama 호출 실패: ${r.error || 'unknown'}`);
|
|
10017
|
+
log(` → ollama serve 실행 + LEERNESS_OLLAMA_BASE_URL 확인`);
|
|
10018
|
+
process.exitCode = 1;
|
|
10019
|
+
}
|
|
10020
|
+
return;
|
|
10021
|
+
}
|
|
10022
|
+
// 그 외 provider: 사용자에게 직접 dispatch 안내
|
|
10023
|
+
log(`\n💡 ${provider} provider 는 \`leerness agents dispatch "<task>" --to ${provider}\` 또는 외부 CLI 직접 호출 권장`);
|
|
10024
|
+
}
|
|
10025
|
+
|
|
9789
10026
|
// 1.9.85: leerness health — 종합 헬스 체크 (drift + 보안 + skill + MCP + 누적)
|
|
9790
10027
|
function healthCmd(root) {
|
|
9791
10028
|
root = absRoot(root || process.cwd());
|
|
@@ -10287,6 +10524,10 @@ async function main() {
|
|
|
10287
10524
|
if (cmd === 'env' && args[1] === 'sync') return envSyncCmd(args[2] || arg('--path', process.cwd()));
|
|
10288
10525
|
// 1.9.145: 실행 환경 자동 감지 + 변동 추적 (사용자 명시)
|
|
10289
10526
|
if (cmd === 'env' && args[1] === 'detect') return envDetectCmd(args[2] || arg('--path', process.cwd()));
|
|
10527
|
+
// 1.9.146: agent 권한 시스템 + CLI 에이전트 모드 (사용자 명시 요청 #4, #5)
|
|
10528
|
+
if (cmd === 'permissions' && args[1] === 'list') return permissionsListCmd(arg('--path', process.cwd()));
|
|
10529
|
+
if (cmd === 'permissions' && args[1] === 'set') return permissionsSetCmd(arg('--path', process.cwd()), args[2]);
|
|
10530
|
+
if (cmd === 'agent') return agentCmd(arg('--path', process.cwd()), args.slice(1).filter(x => !x.startsWith('--')).join(' '));
|
|
10290
10531
|
// 1.9.85: leerness health — 종합 헬스 체크
|
|
10291
10532
|
if (cmd === 'health') return healthCmd(args[1] || arg('--path', process.cwd()));
|
|
10292
10533
|
if (cmd === 'whats-new') return whatsNewCmd(args[1] || arg('--path', process.cwd()));
|
package/package.json
CHANGED
package/scripts/e2e.js
CHANGED
|
@@ -807,13 +807,14 @@ total++;
|
|
|
807
807
|
&& /\| gemini \|/.test(r1.stdout)
|
|
808
808
|
&& /\| copilot \|/.test(r1.stdout);
|
|
809
809
|
// env 모두 0 → 비활성
|
|
810
|
-
|
|
810
|
+
// 1.9.146: Ollama 추가 → 5 CLI
|
|
811
|
+
const env2 = { ...process.env, LEERNESS_ENABLE_CLAUDE: '0', LEERNESS_ENABLE_CODEX: '0', LEERNESS_ENABLE_GEMINI: '0', LEERNESS_ENABLE_COPILOT: '0', LEERNESS_ENABLE_OLLAMA: '0' };
|
|
811
812
|
const r2 = cp.spawnSync(process.execPath, [CLI, 'agents', 'list', '--json'], { encoding: 'utf8', timeout: 15000, env: env2 });
|
|
812
813
|
let parsed = null;
|
|
813
814
|
try { parsed = JSON.parse(r2.stdout); } catch {}
|
|
814
|
-
const okJson = parsed && Array.isArray(parsed.agents) && parsed.agents.length ===
|
|
815
|
+
const okJson = parsed && Array.isArray(parsed.agents) && parsed.agents.length === 5 && parsed.agents.every(a => a.status !== 'ready');
|
|
815
816
|
const ok = okList && okJson;
|
|
816
|
-
console.log(ok ? '✓ B(1.9.30) agents list:
|
|
817
|
+
console.log(ok ? '✓ B(1.9.30+1.9.146) agents list: 5 CLI 정의 (claude/codex/gemini/copilot/ollama)' : `✗ agents list 실패 (list=${okList} json=${okJson})`);
|
|
817
818
|
if (!ok) { failed++; console.log(r1.stdout.slice(0, 500)); }
|
|
818
819
|
}
|
|
819
820
|
|
|
@@ -851,10 +852,11 @@ total++;
|
|
|
851
852
|
const r2 = cp.spawnSync(process.execPath, [CLI, 'agents', 'quota', '--json'], { encoding: 'utf8', timeout: 15000, env });
|
|
852
853
|
let parsed = null;
|
|
853
854
|
try { parsed = JSON.parse(r2.stdout); } catch {}
|
|
854
|
-
|
|
855
|
+
// 1.9.146: Ollama 추가 → 5 CLI
|
|
856
|
+
const okJson = parsed && Array.isArray(parsed.quota) && parsed.quota.length === 5
|
|
855
857
|
&& parsed.quota.every(q => typeof q.id === 'string' && typeof q.status === 'string' && (q.hint === null || typeof q.hint === 'string'));
|
|
856
858
|
const ok = okText && okJson;
|
|
857
|
-
console.log(ok ? '✓ B(1.9.31) agents quota:
|
|
859
|
+
console.log(ok ? '✓ B(1.9.31+1.9.146) agents quota: 5 CLI 사용량/안내' : `✗ quota 실패 (text=${okText} json=${okJson})`);
|
|
858
860
|
if (!ok) { failed++; console.log(r.stdout.slice(0, 500)); }
|
|
859
861
|
}
|
|
860
862
|
|