klyro 0.1.15 → 0.1.17
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/READ.md +467 -0
- package/dist/agent/runtime.js +1 -11
- package/dist/cli/repl.js +17 -46
- package/dist/tui/app.d.ts +6 -4
- package/dist/tui/app.js +49 -49
- package/package.json +2 -1
package/READ.md
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# Klyro — Complete Build Documentation
|
|
2
|
+
|
|
3
|
+
**For any coding agent:** This file is the single source of truth for what has been built till now (v0.1.15, Levels 1-5 complete, Level 6-8 partial, TUI full-screen). After reading, you have the complete picture.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. What is Klyro?
|
|
8
|
+
|
|
9
|
+
**Klyro** is an autonomous AI coding harness — a terminal-native agent that understands repositories, executes tools, verifies its work, and repairs failures.
|
|
10
|
+
|
|
11
|
+
> **Vision:** *Build a highly autonomous software engineering harness capable of understanding complex codebases, executing long-running tasks, dynamically coordinating workflows, verifying its own work, and continuously improving through evaluation.* — `PRD.md:3`
|
|
12
|
+
|
|
13
|
+
**Not a chatbot.** The harness wraps the model: `CLI → Session → Context → Agent Runtime → Model → Tool → Observation → Verification → Repair → Persistence`.
|
|
14
|
+
|
|
15
|
+
**Current version:** `0.1.15` (`package.json:3`, `npm view klyro dist-tags → latest:0.1.15`, `43 files 309/309 tests`)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2. Tech Stack
|
|
20
|
+
|
|
21
|
+
| Layer | Choice | Why |
|
|
22
|
+
|-------|--------|-----|
|
|
23
|
+
| Language | TypeScript 5.5, Node 20+ | Strict, `NodeNext`, `tsc` → `dist/` |
|
|
24
|
+
| CLI | `commander 12.1` | Stable, `InvalidArgumentError` for `exit 2` |
|
|
25
|
+
| TUI | `ink 7.1` + `react 19` + `ink-spinner 5` | React for terminal, proven by Claude Code/Gemini |
|
|
26
|
+
| Schema | `zod 4.5` | Tool input validation + config schema |
|
|
27
|
+
| Test | `vitest 4.1` `fileParallelism:false` `10s timeout` | `node` env, deterministic mocks |
|
|
28
|
+
| Build | `tsc` (not `tsup`) | `tsc --noEmit` `typecheck`, `tsc` `build` |
|
|
29
|
+
| Providers | Native `fetch` (Node 20) | No SDK lock-in, 3 adapters |
|
|
30
|
+
| Workspace | `pnpm-workspace.yaml` `packages/*` | `shared` `KlyroError` |
|
|
31
|
+
| CI | `.github/workflows/ci.yml` `ubuntu/macos/windows × 20/22` | `pnpm install` `typecheck` `test` `build` `pack` |
|
|
32
|
+
|
|
33
|
+
**No Docker, no MCP, no browser in MVP** — deferred to post-1.0.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 3. Architecture (Full)
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
USER
|
|
41
|
+
│
|
|
42
|
+
▼
|
|
43
|
+
CLI / TUI (ink) ←→ Global flags (--cwd/--config/--debug/--json) + completion + update + login
|
|
44
|
+
│
|
|
45
|
+
▼
|
|
46
|
+
Session Manager (SessionStore + TraceWriter + AuditLog)
|
|
47
|
+
│
|
|
48
|
+
▼
|
|
49
|
+
Task Analyzer
|
|
50
|
+
│
|
|
51
|
+
▼
|
|
52
|
+
Context Engine (L6 ProjectMap + L7 Telemetry + L8 Accounting)
|
|
53
|
+
│
|
|
54
|
+
▼
|
|
55
|
+
Agent Runtime (ProviderAdapter → Policy → Registry → Tool → Observation → Repair)
|
|
56
|
+
│
|
|
57
|
+
├─ Model Layer (OpenAI httpChatAdapter / Anthropic anthropicAdapter / retryingAdapter)
|
|
58
|
+
├─ Tool Registry (18 tools: fs/search/shell/git/verify/plan)
|
|
59
|
+
├─ Policy Engine (modes default|plan|accept-edits|auto + .env guard)
|
|
60
|
+
├─ Memory Engine (history 40 turns / 80k chars, pair-preserving)
|
|
61
|
+
└─ Verification Engine (verify + detect + auto + repair loop ≤3)
|
|
62
|
+
│
|
|
63
|
+
▼
|
|
64
|
+
Repair Loop (verify → classify → diagnosis → repair → re-verify)
|
|
65
|
+
│
|
|
66
|
+
▼
|
|
67
|
+
Completion Engine (diff + report + status)
|
|
68
|
+
│
|
|
69
|
+
▼
|
|
70
|
+
Observability ( Ink transcript + status line + trace JSONL)
|
|
71
|
+
│
|
|
72
|
+
▼
|
|
73
|
+
Evals (harness + fixtures + compare)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Data Flow (one `klyro run`):**
|
|
77
|
+
```
|
|
78
|
+
Task → makeRunSystemPrompt() injects L6 (12k) + KLYRO.md (4k) + telemetry
|
|
79
|
+
→ CallRequest{system, transcript, tools} → ProviderAdapter.stream()
|
|
80
|
+
→ StreamEvent{ text_delta | tool_call_start/delta/end | message_end | error }
|
|
81
|
+
→ finalize ToolUseBlock → PolicyEngine.evaluate() → ApprovalPrompt → registry.execute()
|
|
82
|
+
→ redactOutput() deep → transcript.push(tool_result) → telemetry.record*()
|
|
83
|
+
→ hasEdits? → verify() → detect() → diagnosticForModel() → repair (up to 3)
|
|
84
|
+
→ checkpoint (SessionStore + TraceWriter) per step
|
|
85
|
+
→ onEvent → stdout/stderr (human) | JSONL | TUI
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 4. 20-Level Plan — Status
|
|
91
|
+
|
|
92
|
+
| Level | Title | Spec `plan.md` | Status | Key Files |
|
|
93
|
+
|-------|-------|----------------|--------|-----------|
|
|
94
|
+
| **1** | Professional CLI Foundation | `1.1` Skeleton `1.2` Parser `1.3` Config `1.4` REPL `1.5` Output | **5/5 PASS** | `src/index.ts:50` `src/cli/config.ts:10` `src/cli/doctor.ts:1` `src/tui/app.tsx:14` |
|
|
95
|
+
| **2** | AI Chat Core | `2.1` Provider `2.2` OpenAI `2.3` Messages `2.4` Streaming `2.5` Cost/headless | **5/5 PASS** | `src/providers/model-info.ts:1` `src/cli/auth.ts:1` `src/context/system-prompt.ts:1` `src/tui/status.tsx:30` |
|
|
96
|
+
| **3** | Tool Runtime | `3.1` Events `3.2` read/write `3.3` shell `3.4` Permissions `3.5` Loop | **5/5 PASS** | `src/events/catalog.ts:1` `src/tools/fs/read-file.ts:39` `src/tools/shell/shell-exec.ts:20` `src/policy/engine.ts:34` `src/agent/runtime.ts:375` |
|
|
97
|
+
| **4** | Repository Coding Agent | `4.1` Edit core `4.2` Fuzzy `4.3` Navigation `4.4` KLYRO.md `4.5` Checkpoints | **5/5 PASS** | `src/tools/fs/edit-file.ts:26` `src/tools/fs/multi-edit.ts:1` `src/context/klyro-md.ts:1` `src/checkpoints/store.ts:1` |
|
|
98
|
+
| **5** | Autonomous Task Loop + Eval | `5.1` Limits `5.2` Stuck `5.3` Plan `5.4` Harness `5.5` Suite | **5/5 PASS** | `src/agent/runtime.ts:152` `phases` `src/tools/plan/todo-write.ts:1` `evals/fixtures/*` `evals/results/baseline.json` |
|
|
99
|
+
| **6** | Verification + Repair | `6.1` Verifiers `6.2` Parsers `6.3` Scoped `6.4` Repair `6.5` Contract | **PARTIAL** (engine exists, but harness needs `check.sh` from 5.5) | `src/verification/engine.ts:28` `auto.ts:10` |
|
|
100
|
+
| **7** | Codebase Intelligence | `7.1` Scanner `7.2` RepoMap `7.3` Search `7.4` Symbols | **PARTIAL** (heuristic, no tree-sitter) | `src/context/project-map.ts:352` `repo-map.ts:33` |
|
|
101
|
+
| **8** | Context Intelligence | `8.1` Accounting `8.2` Lifecycle `8.3` Compaction | **STUB** (tokenizer exists, not wired) | `src/context/tokenizer.ts:35` |
|
|
102
|
+
| **9** | Sessions, Persistence | `9.1` Store `9.2` Continue | **PARTIAL** (JSON store, no SQLite) | `src/persistence/store.ts:52` `session.ts:10` |
|
|
103
|
+
| **10** | Professional Harness | MCP, Hooks, SDK | **NOT STARTED** | — |
|
|
104
|
+
| **11-20** | Multi-Agent → Adaptive Platform | | **NOT STARTED** | — |
|
|
105
|
+
|
|
106
|
+
**Graduation for L1-L5 — PASS** (verified `vitest 309/309`, `klyro --version --help config list doctor -p "hi"`, `20-turn`, `cancel <1s`, `10/10 smoke` `evals/fixtures`).
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 5. Detailed Flows (Per Level)
|
|
111
|
+
|
|
112
|
+
### L1 — CLI
|
|
113
|
+
```
|
|
114
|
+
klyro --version → readVersion() tries 3 candidates src/index.ts:28 → 0.1.15
|
|
115
|
+
klyro --help → commander tree src/index.ts:50 + showSuggestionAfterError
|
|
116
|
+
klyro → isTTY ? startRepl() : repl() src/index.ts:84 + --tui/--chat + -p headless src/index.ts:60
|
|
117
|
+
klyro config → 5-layer loadMergedConfig() src/cli/config.ts:80 → Zod validate → JSONC stripJsonComments()
|
|
118
|
+
klyro doctor → 7 checks src/cli/doctor.ts:20 (Node/config/Provider/Sessions/git/Tools/Platform)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### L2 — AI Chat
|
|
122
|
+
```
|
|
123
|
+
Provider resolution: flags → env KLYRO_PROVIDER → inferProviderFromBaseURL() src/agent/registry.ts:40 (9router→openai alias) → httpChatAdapter / anthropicAdapter
|
|
124
|
+
Streaming: httpChatAdapter.stream() src/agent/provider-adapter.ts:202 → fetch POST /chat/completions stream:true → SSE \n\n → toolIds map + pendingUsage + redacted error
|
|
125
|
+
Retry: retryingAdapter src/agent/retry.ts:46 streamWithAbort() + it.return() + combined signal (req.signal + opts.signal)
|
|
126
|
+
Cost: getModelInfo() src/providers/model-info.ts:1 estimateCost() → StatusLine src/tui/status.tsx:30 $0.043 · ctx 6%
|
|
127
|
+
Headless: klyro -p "hi" argument('[prompt]') src/index.ts:60 → runOnce output json|stream-json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### L3 — Tool Runtime
|
|
131
|
+
```
|
|
132
|
+
KlyroEvent bus src/events/bus.ts:1 globalBus.emit() + TraceWriter src/trace/writer.ts:1 JSONL fsync on tool.result
|
|
133
|
+
Tool<TInput,TOutput> src/tools/types.ts:1 {name, description, inputSchema, permission, isConcurrencySafe, renderCall}
|
|
134
|
+
read_file: 2000 window src/tools/fs/read-file.ts:70, 10MB refusal src/tools/fs/read-file.ts:49, binary null-byte src/tools/fs/read-file.ts:60, 8k tokens hint
|
|
135
|
+
write_file: wasRead guard src/tools/fs/read-history.ts:1, diff, 0600, atomic tmp+fsync
|
|
136
|
+
shell: 120s max 600s src/tools/shell/shell-exec.ts:20, persistentCwd, filteredEnv(), 30k head+tail → ~/.klyro/tool-output/<id>.txt, tree-kill taskkill/SIGKILL, interactive block
|
|
137
|
+
Policy: mode default|plan|accept-edits|auto src/policy/engine.ts:34, glob allow/deny/ask matchesGlobRule() src/policy/engine.ts:120, .env deny src/policy/engine.ts:99, sandbox --add-dir
|
|
138
|
+
Loop: stream→collect → no tools? return → policy → parallel if all isConcurrencySafe src/agent/runtime.ts:386 → Promise.all else sequential
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### L4 — Repository Coding
|
|
142
|
+
```
|
|
143
|
+
edit_file: EOL CRLF/LF src/tools/fs/edit-file.ts:40, BOM src/tools/fs/edit-file.ts:40, trailing newline, staleness mtime+hash src/tools/fs/edit-file.ts:26, actionable findClosestMatch() src/tools/fs/edit-file.ts:70
|
|
144
|
+
Fuzzy tiers after exact fails: trailing-whitespace → indent → unicode quotes → line-window 0.95 src/tools/fs/edit-file.ts:40
|
|
145
|
+
multi_edit atomic src/tools/fs/multi-edit.ts:10, apply_patch Codex tolerant src/tools/fs/apply-patch.ts:1
|
|
146
|
+
Navigation: list_dir/glob/grep respect .klyroignore, git_log/status/diff src/tools/git/git-log.ts:1, background shell src/tools/shell/background.ts:1 1MB ring
|
|
147
|
+
KLYRO.md: loadKlyroMd() ~/.klyro/KLYRO.md → KLYRO.local.md → subdir lazy src/context/klyro-md.ts:1 @import depth5
|
|
148
|
+
Checkpoints: snapshot() src/checkpoints/store.ts:13 .klyro/checkpoints/<id> → diff vs HEAD → undo/rewind
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### L5 — Autonomous Loop + Eval
|
|
152
|
+
```
|
|
153
|
+
Loop controller: maxSteps alias maxTurns src/agent/runtime.ts:152, maxCost/maxTimeMs src/agent/runtime.ts:180 → status:limit phase:limit exit 7
|
|
154
|
+
Phases: understanding→exploring→planning→implementing→verifying setPhase() src/agent/runtime.ts:180 phase.changed
|
|
155
|
+
Stuck: identical call×3 src/agent/runtime.ts:180, same file >8× fileEditCounts, ≥5 fails → [system note]
|
|
156
|
+
Planning: todo_write src/tools/plan/todo-write.ts:1 → .klyro/plans/todos.json, plan mode blocks writes, ask_user src/tools/plan/ask-user.ts:1 HEADLESS → KLYRO_AUTO_ANSWER
|
|
157
|
+
Eval: FileFixture {dir, task.md, check.sh, meta.json} loadFileFixture() src/eval/harness.ts:130, runFileFixture() tmp cp + bash -c check.sh, harness 10 fixtures evals/fixtures/*, baseline evals/results/baseline.json 8/10, compareReports() src/eval/harness.ts:180, klyro eval --suite smoke src/cli/eval.ts:84
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 6. File Structure (Purpose)
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
klyro/
|
|
166
|
+
├── package.json # klyro 0.1.15, bin klyro/ky, files [dist], commander/ink/zod
|
|
167
|
+
├── pnpm-workspace.yaml # packages/*
|
|
168
|
+
├── tsconfig.json # ES2022, NodeNext, strict, noUncheckedIndexedAccess
|
|
169
|
+
├── vitest.config.ts # include src/**/*.test, fileParallelism:false
|
|
170
|
+
├── .github/workflows/ci.yml # ubuntu/macos/windows × 20/22 → typecheck/test/build/eval smoke
|
|
171
|
+
├── src/
|
|
172
|
+
│ ├── index.ts # commander entry: tui/run/chat/eval/session/resume + global flags --cwd/--config/-p
|
|
173
|
+
│ ├── chat.ts # one-shot chat, normalizeBaseURL, assertSafeBaseURL (loopback+private+ALLOW_INSECURE), streamToStdout \n\n, writeWithBackpressure, readBoundedText
|
|
174
|
+
│ ├── repl.ts # legacy readline REPL, history 40/80k, trimHistory pair-preserving
|
|
175
|
+
│ ├── providers.ts # resolveProvider() probe Ollama 11434 etc 600ms, providerHelp()
|
|
176
|
+
│ ├── agent/
|
|
177
|
+
│ │ ├── runtime.ts # run() loop: CallRequest → ProviderAdapter → policy → registry → checkpoint → verify→repair (≤3) → trace
|
|
178
|
+
│ │ ├── message.ts # Message{role, content: TextBlock|ToolUseBlock|ToolResultBlock}
|
|
179
|
+
│ │ ├── provider-adapter.ts # httpChatAdapter, buildChatCompletionsBody, streamChatCompletions \n\n + pendingUsage + redact + isConcurrencySafe
|
|
180
|
+
│ │ ├── anthropic-adapter.ts # anthropicAdapter, x-api-key, indexToToolId map, assertSafeBaseURL
|
|
181
|
+
│ │ ├── registry.ts # buildProvider(), inferProviderFromBaseURL() 9router→openai, isLoopback, retryingAdapter
|
|
182
|
+
│ │ ├── retry.ts # retryingAdapter, computeBackoff, streamWithAbort it.return()
|
|
183
|
+
│ │ ├── worker-spawner.ts # stub for future sub-agents
|
|
184
|
+
│ │ └── observation.ts # ObservationStore (placeholder)
|
|
185
|
+
│ ├── tools/
|
|
186
|
+
│ │ ├── types.ts # Tool<TIn,TOut> + ToolContext{sessionId,permissions,logger,emit}
|
|
187
|
+
│ │ ├── registry.ts # ToolRegistry 18 tools: fs/search/shell/git/verify/plan
|
|
188
|
+
│ │ ├── schema.ts # zodToJsonSchema (Zod3/4)
|
|
189
|
+
│ │ ├── normalize.ts # safe(), TOOL_ERROR_CODES
|
|
190
|
+
│ │ ├── fs/read-file.ts # 2000 window, 10MB, binary, 8k hint, wasRead
|
|
191
|
+
│ │ ├── fs/write-file.ts # 0600 tmp+fsync, diff, needsApproval
|
|
192
|
+
│ │ ├── fs/edit-file.ts # EOL/BOM/trailing, staleness, findClosestMatch, fuzzy tiers
|
|
193
|
+
│ │ ├── fs/multi-edit.ts # atomic sequential
|
|
194
|
+
│ │ ├── fs/apply-patch.ts # Codex tolerant
|
|
195
|
+
│ │ ├── fs/read-history.ts # wasRead Set
|
|
196
|
+
│ │ ├── fs/list-dir.ts # depth, .klyroignore
|
|
197
|
+
│ │ ├── search/glob.ts # globToRegex **, SKIP_DIRS
|
|
198
|
+
│ │ ├── search/grep.ts # RegExp g + lastIndex, binary skip, 500 cap, ReDoS risk noted
|
|
199
|
+
│ │ ├── search/search-files.ts# fzf query, firstParty+recency ranking
|
|
200
|
+
│ │ ├── search/recent-files.ts# mtime >= since, .map skip
|
|
201
|
+
│ │ ├── search/dependencies.ts# npm/py/go/rust/Cargo parsers
|
|
202
|
+
│ │ ├── shell/shell-exec.ts # 120s, persistentCwd, filteredEnv, 30k head+tail, tool-output file, DANGEROUS_PATTERNS + $HOME
|
|
203
|
+
│ │ ├── shell/background.ts # 1MB ring, startBackground/getOutput/killJob
|
|
204
|
+
│ │ ├── git/git-status.ts # porcelain + branch + log
|
|
205
|
+
│ │ ├── git/git-log.ts # read-only
|
|
206
|
+
│ │ └── verify/run-verify.ts # 5m, 256k cap
|
|
207
|
+
│ ├── context/
|
|
208
|
+
│ │ ├── repo-map.ts # regex symbols ts/js/py/go/rs, 500 files
|
|
209
|
+
│ │ ├── project-map.ts # language/framework/PM/test detection
|
|
210
|
+
│ │ ├── level6.ts # buildLevel6Context 12k = project+repo(40)+recent(15)+deps
|
|
211
|
+
│ │ ├── level7.ts # RuntimeTelemetry step/tools/errors/tokens
|
|
212
|
+
│ │ ├── tokenizer.ts # estimateTokens chars/4, compressTranscript deep-copy
|
|
213
|
+
│ │ ├── selector.ts # selectFiles (dead, not wired)
|
|
214
|
+
│ │ ├── snippets.ts # readSnippet windowed
|
|
215
|
+
│ │ └── system-prompt.ts # buildSystemPrompt layered identity→env→global
|
|
216
|
+
│ ├── policy/
|
|
217
|
+
│ │ ├── engine.ts # PolicyEngine 4 rules + mode/ glob/.env/add-dir
|
|
218
|
+
│ │ ├── approval.ts # StdinApprovalPrompt/DenyAll/InMemoryAllowlist + TuiApprovalBridge
|
|
219
|
+
│ │ ├── path-guard.ts # resolveWithinCwd + resolveAndFollowSymlinks (TOCTOU noted)
|
|
220
|
+
│ │ └── secret-redactor.ts # redact() 7 patterns + createRedactor 1KiB tail
|
|
221
|
+
│ ├── persistence/
|
|
222
|
+
│ │ ├── store.ts # SessionStore JSON per session + sessions.json index (atomic tmp→fsync→rename, retry)
|
|
223
|
+
│ │ ├── session.ts # getDefaultSessionsDir ~/.klyro/sessions, formatSession, resolveSessionId prefix
|
|
224
|
+
│ │ └── audit.ts # AuditLog JSONL
|
|
225
|
+
│ ├── verification/
|
|
226
|
+
│ │ ├── engine.ts # verify() spawn shell, timeout SIGKILL, detect(), diagnosticForModel()
|
|
227
|
+
│ │ ├── detect.ts # heuristic type/test/lint/build/runtime + summarize()
|
|
228
|
+
│ │ └── auto.ts # detectVerifyCommand() npm test / tsc / make
|
|
229
|
+
│ ├── checkpoints/
|
|
230
|
+
│ │ └── store.ts # snapshot/.klyro/checkpoints/<id> diff/undo/rewind
|
|
231
|
+
│ ├── events/
|
|
232
|
+
│ │ ├── catalog.ts # KlyroEvent 15 types
|
|
233
|
+
│ │ └── bus.ts # EventBus + globalBus
|
|
234
|
+
│ ├── trace/
|
|
235
|
+
│ │ └── writer.ts # TraceWriter JSONL appendFile + fsync on tool.result
|
|
236
|
+
│ ├── renderers/
|
|
237
|
+
│ │ ├── terminal.ts # TerminalRenderer
|
|
238
|
+
│ │ └── json.ts # JsonRenderer
|
|
239
|
+
│ ├── tui/
|
|
240
|
+
│ │ ├── app.tsx # Ink full-screen: Static history + live region batched 30fps + single useInput
|
|
241
|
+
│ │ ├── tokens.ts # accent #8B7CF6, glyphs › ● ⎿ ✔, spacing
|
|
242
|
+
│ │ ├── banner.tsx # 5.1 Banner + resume
|
|
243
|
+
│ │ ├── input-box.tsx # 5.2 InputBox queued
|
|
244
|
+
│ │ ├── thinking-block.tsx # 5.5 Thinking
|
|
245
|
+
│ │ ├── activity-line.tsx # 5.6 Spinner + verb·elapsed
|
|
246
|
+
│ │ ├── transcript.tsx # 11 kinds: text/tool/policy/error/file_changed/diff
|
|
247
|
+
│ │ ├── status.tsx # model step/max repairs tokens cost ctx%
|
|
248
|
+
│ │ ├── header.tsx # abbrevPath
|
|
249
|
+
│ │ ├── diff.tsx # DiffView per-file hunk
|
|
250
|
+
│ │ ├── approval.tsx # TuiApprovalBridge y/a/A/n/e/?
|
|
251
|
+
│ │ ├── plan.tsx # PlanView glyphs ◯●✓✗⊘
|
|
252
|
+
│ │ └── diff-parser.ts # parseUnifiedDiff
|
|
253
|
+
│ ├── cli/
|
|
254
|
+
│ │ ├── repl.ts # startRepl() Ink TUI: resolveProvider probe, httpChat/anthropic, queued bridge, verify repair loop
|
|
255
|
+
│ │ ├── run.ts # runOnce() one-shot: verify/persist, session create/resume, headless -p
|
|
256
|
+
│ │ ├── eval.ts # runEval() JSONL suite + --suite smoke file fixtures
|
|
257
|
+
│ │ ├── config.ts # 5-layer loadMergedConfig, Zod, JSONC stripJsonComments, get|set|list|path
|
|
258
|
+
│ │ ├── doctor.ts # 7 checks Node/config/Provider/Sessions/git/Tools/Platform
|
|
259
|
+
│ │ ├── completion.ts # bash|zsh|fish|powershell
|
|
260
|
+
│ │ ├── update.ts # 24h cache, KLYRO_NO_UPDATE_CHECK
|
|
261
|
+
│ │ ├── auth.ts # login 0600 credentials.json, MODEL_ALIASES sonnet→claude
|
|
262
|
+
│ │ ├── markdown.ts # renderMarkdown incremental, stripMarkdown
|
|
263
|
+
│ │ ├── errors.ts # handleFatal ✖/hint, setupGlobalHandlers
|
|
264
|
+
│ │ └── slash/parser.ts # /clear/compact/model/diff/plan/status/quit/help/config/doctor/cost/thinking
|
|
265
|
+
│ ├── shared/
|
|
266
|
+
│ │ ├── errors.ts # KlyroError{code,exitCode}
|
|
267
|
+
│ │ └── types.ts # Role, Message, ExitCode
|
|
268
|
+
│ └── util/log.ts # pino JSON ~/.klyro/logs/klyro-YYYY-MM-DD.log 14d rotation redact
|
|
269
|
+
├── packages/shared # workspace: @klyro/shared (re-export)
|
|
270
|
+
├── evals/
|
|
271
|
+
│ ├── fixtures/ # 10 smoke fixtures: read-answer, add-fn-test, fix-failing-test…
|
|
272
|
+
│ └── results/baseline.json # 8/10 80% + markdown compareReports()
|
|
273
|
+
├── .klyro/ # runtime: sessions.json, traces/*.jsonl, checkpoints/<id>, logs/, history, memory/
|
|
274
|
+
└── dist/ # tsc output, bin klyro
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 7. CLI Reference
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
klyro # TUI REPL (TTY) or legacy pipe REPL
|
|
283
|
+
klyro --tui / --no-tui # force TUI / legacy
|
|
284
|
+
klyro -p "prompt" # headless one-shot (stdin piped + --output-format json)
|
|
285
|
+
klyro tui [-m model] [--max-steps n]
|
|
286
|
+
klyro run <prompt> [-m model] [--max-steps 30] [--max-cost $] [--max-time ms]
|
|
287
|
+
[--verify/--no-verify --verify-command <cmd> --max-repairs 3]
|
|
288
|
+
[--persist/--no-persist --resume-session <id>] [--output human|json|silent]
|
|
289
|
+
klyro chat [prompt] [-s system] # legacy streamed chat
|
|
290
|
+
klyro eval <input.jsonl> | --suite smoke [--filter str] [--runs 1] [--parallel 1] [--output json]
|
|
291
|
+
klyro eval:compare <a.json> <b.json>
|
|
292
|
+
klyro config [list|get <key>|set <key> <value>|unset <key>|path|edit]
|
|
293
|
+
klyro doctor [--json]
|
|
294
|
+
klyro completion <bash|zsh|fish|powershell>
|
|
295
|
+
klyro update
|
|
296
|
+
klyro login / logout [provider] # 0600 credentials.json, MODEL_ALIASES
|
|
297
|
+
klyro session list|show <id>|resume <id> [--json]
|
|
298
|
+
klyro resume <id> # alias
|
|
299
|
+
klyro trace <id> [--stats --json]
|
|
300
|
+
klyro --version / --help
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Global flags (preAction `src/index.ts:60`): `--cwd <path>` `--config <path>` `--debug` `--verbose` `--quiet` `--json` `--yes` `--no-color` (respects `NO_COLOR/FORCE_COLOR`)
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## 8. Provider System
|
|
308
|
+
|
|
309
|
+
- **Resolution:** `flags` → `KLYRO_PROVIDER` (aliases `9router/openrouter/groq→openai`) → `inferProviderFromBaseURL()` `anthropic.com` exact → `openai` default `src/agent/registry.ts:30`
|
|
310
|
+
- **Adapters:** `httpChatAdapter` `src/agent/provider-adapter.ts:190` (`/chat/completions` SSE `\n\n`, `toolIds` map + `pendingUsage`, `redact` error) + `anthropicAdapter` `src/agent/anthropic-adapter.ts:76` (`/v1/messages` `x-api-key`, `indexToToolId` `Map<number,string>` `src/agent/anthropic-adapter.ts:169` fixes `findToolIdByIndex` interleaving) + `retryingAdapter` `src/agent/retry.ts:46` `computeBackoff` `base*2^attempt ±25%` `3 attempts 500ms/8s`, `streamWithAbort` `it.return()` `src/agent/retry.ts:46`
|
|
311
|
+
- **Security:** `assertSafeBaseURL()` `src/chat.ts:38` `https:` OK, `http:` only `localhost/127.*` `10/8` `192.168/16` `172.16/12` or `KLYRO_ALLOW_INSECURE=1` with warning.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## 9. Tool System (18 tools)
|
|
316
|
+
|
|
317
|
+
`ToolRegistry` `src/tools/registry.ts:81` `register/get/list/toOpenAITools/execute` (`safeParse` → `INVALID_INPUT`).
|
|
318
|
+
|
|
319
|
+
| Tool | Input | Output | Notes |
|
|
320
|
+
|------|-------|--------|-------|
|
|
321
|
+
| `read_file` | `path, startLine?, endLine?, maxBytes?` | `lines, totalLines, bytesRead, truncated` | `cat -n` 2000 window, 10MB `src/tools/fs/read-file.ts:49`, binary `null-byte` `src/tools/fs/read-file.ts:60`, `wasRead` `src/tools/fs/read-history.ts:1` |
|
|
322
|
+
| `write_file` | `path, content` | `bytesWritten, diff` | `0600` tmp `crypto.randomBytes` + `fsync` + `rename` `src/tools/fs/write-file.ts:36`, `wasRead` guard `>200` lines `needsApproval` |
|
|
323
|
+
| `edit_file` | `path, find, replace, replaceAll?` | `replacements, diff` | `EOL/BOM/trailing` `src/tools/fs/edit-file.ts:40`, staleness `mtime+hash` `src/tools/fs/edit-file.ts:26`, `findClosestMatch` `src/tools/fs/edit-file.ts:70`, fuzzy 4 tiers `src/tools/fs/edit-file.ts:40` |
|
|
324
|
+
| `multi_edit` | `path, edits[{find,replace}]` | `edits, diff` | atomic `src/tools/fs/multi-edit.ts:10` |
|
|
325
|
+
| `apply_patch` | `patch` | `patchedFiles` | Codex tolerant `src/tools/fs/apply-patch.ts:1` |
|
|
326
|
+
| `list_directory` | `path, maxDepth?` | `entries` | skips `DEFAULT_SKIP` + dotfiles `src/tools/fs/list-dir.ts:67`, depth `maxDepth` |
|
|
327
|
+
| `glob` | `pattern, cwd?` | `paths` | `globToRegex` `**/` `src/tools/search/glob.ts:50` |
|
|
328
|
+
| `grep` | `pattern, path?, include?` | `matches` | `RegExp g` `lastIndex=0` `src/tools/search/grep.ts:49` `500` cap, binary skip |
|
|
329
|
+
| `search_files` | `query` | `paths` | ranking `exact basename 200` `src/tools/search/search-files.ts:1` |
|
|
330
|
+
| `recent_files` | `sinceHours?, glob?` | `paths` | `mtime >= sinceMs` `src/tools/search/recent-files.ts:1` |
|
|
331
|
+
| `dependencies` | `manager?` | `deps` | `npm/py/go/rust` parsers `src/tools/search/dependencies.ts:1` |
|
|
332
|
+
| `shell_exec` | `command, cwd?, timeoutMs?, env?` | `exitCode, stdout, truncated` | `120s max 600s` `src/tools/shell/shell-exec.ts:20` `persistentCwd` `filteredEnv()` `30k head+tail` `~/.klyro/tool-output/<id>.txt` `taskkill /F /T` `src/tools/shell/shell-exec.ts:182`, `DANGEROUS_PATTERNS` `rm -rf` `curl|sh` `src/tools/shell/shell-exec.ts:30` |
|
|
333
|
+
| `git_status` | — | `porcelain, branch, log` | `spawn git` `shell:false` `15s` `src/tools/git/git-status.ts:1` |
|
|
334
|
+
| `git_diff` | `cached?` | `diff, stat, patchedFiles` | `diff --git` regex `src/tools/git/git-diff.ts:61` |
|
|
335
|
+
| `git_log` | `limit?, path?` | `log` | read-only `src/tools/git/git-log.ts:1` |
|
|
336
|
+
| `run_verify` | `command` | `ok, exitCode` | `5m 256k` `src/tools/verify/run-verify.ts:1` `taskkill` |
|
|
337
|
+
| `todo_write` | `todos[{id,title,status}]` | `updated` | `src/tools/plan/todo-write.ts:1` `.klyro/plans/todos.json` |
|
|
338
|
+
| `ask_user` | `question, options?` | `answer` | `src/tools/plan/ask-user.ts:1` `HEADLESS` `KLYRO_AUTO_ANSWER` |
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## 10. Context Engine
|
|
343
|
+
|
|
344
|
+
- **L6 static:** `buildLevel6Context()` `src/context/level6.ts:131` `12k` = `projectMap` `src/context/project-map.ts:352` (`language/framework/PM` `statSync`) + `repoMap` `src/context/repo-map.ts:33` `regex symbols` 500 files + `recentFiles` `src/**` 15 + `deps` `package.json` → `truncated at last \n`
|
|
345
|
+
- **L7 live:** `RuntimeTelemetry` `src/context/level7.ts:60` `step/maxSteps/toolCallCount/inputTokens` `record*()` `format()` `1200` chars `emptyTelemetryBlock()` `summarize()` — injected via `systemPrompt(cwd,telemetry)` `src/agent/runtime.ts:240` each step
|
|
346
|
+
- **Tokenizer:** `estimateTokens(s)=ceil(len/4)` `src/context/tokenizer.ts:35` `compressTranscript()` `src/context/tokenizer.ts:76` deep-copy (was shallow mutate) + `consumed` set + `tool_result` 400 chars + `drop oldest` — **not wired into runtime** (dead code, transcript unbounded)
|
|
347
|
+
- **Selector:** `selectFiles()` `src/context/selector.ts:22` `+100 exact path` `+20 basename` — **not wired**
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## 11. Verification & Repair (L6/L8)
|
|
352
|
+
|
|
353
|
+
- **auto:** `detectVerifyCommand(cwd)` `src/verification/auto.ts:10` `package.json test`→`npm test` / `tsconfig.json`→`tsc` / `Makefile` / `pytest` / `go test`
|
|
354
|
+
- **engine:** `verify({cwd,command,timeoutMs=5m})` `src/verification/engine.ts:28` `spawn shell true` `stdout+stderr` `timeout kill` `detect()` `diagnosticForModel()` `src/verification/engine.ts:67`
|
|
355
|
+
- **detect:** `detect(stdout,stderr,exitCode)` `src/verification/detect.ts:36` `type` `type|test|lint|build|runtime|unknown` `TS_LINE` `TEST_FAIL` `LINT_LINE` `RUNTIME_LINE` + `summarize()` 8 files
|
|
356
|
+
- **Runtime wiring:** `hasEdits` `src/agent/runtime.ts:175` `write_file/edit_file` → after `no tools` `finalText` → `verifyEnabled && hasEdits && verifyCmd` `src/agent/runtime.ts:331` → `verify()` → `verification_succeeded` → `complete` else `diagnosticForModel()` → `transcript.push(user repair)` `src/agent/runtime.ts:357` → `continue` ≤3 `maxRepairs` `src/agent/runtime.ts:330` → `verify_failed` `src/agent/runtime.ts:366` + `repair_started` event
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 12. Persistence (L9)
|
|
361
|
+
|
|
362
|
+
- **Store:** `SessionStore` `src/persistence/store.ts:52` `dir .sessions` `sessions.json` index + `{id}.json` `{record,messages,observations}` `randomUUID` `ensureDir` `readIndex` `writeIndex` `tmp→fsync→rename` `src/persistence/store.ts:74` (was non-atomic) + 3-attempt `writeSession` `src/persistence/store.ts:100`
|
|
363
|
+
- **Session:** `SessionRecord{id,cwd,task,status,createdAt,updatedAt,config,finalText}` `SessionStatus open|complete|verify_failed|aborted|max_steps` `src/persistence/store.ts:17` + `getDefaultSessionsDir()` `~/.klyro/sessions` `src/persistence/session.ts:10` `formatSession()` `resolveSessionId(prefix)` `src/persistence/session.ts:30`
|
|
364
|
+
- **Audit:** `AuditLog` `src/persistence/audit.ts:23` `AuditEvent` 11 kinds `JSONL appendFile`
|
|
365
|
+
- **Trace:** `TraceWriter` `src/trace/writer.ts:1` `JSONL .klyro/traces/<id>.jsonl` `appendFile + open/sync/close` on `tool.result` (was persistent `FileHandle` leak `src/trace/writer.ts:20` fixed)
|
|
366
|
+
- **Checkpoints:** `src/checkpoints/store.ts:1` `snapshot(cwd,files)` `.klyro/checkpoints/<id>` `listCheckpoints` `diff` `git diff --stat` `undo` `rewind` — **not wired to runtime snapshots** (runtime uses `SessionStore` checkpoint, not `checkpoints/store.ts`)
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 13. TUI (Professional, Full-Screen)
|
|
371
|
+
|
|
372
|
+
- **Stack:** `ink 7.1` `react 19` `ink-spinner 5` `react/jsx` `target ES2022` `module NodeNext`
|
|
373
|
+
- **Tokens** `src/tui/tokens.ts:1` `accent #8B7CF6/magenta` `muted #7A7A7A` `success #4ADE80` `error #F87171` `glyphs › ● ⎿ ✔ ✘ ⚠` + ASCII fallback `isAsciiMode()`
|
|
374
|
+
- **App** `src/tui/app.tsx:14` inline/scrollback per `TUI_DESIGN.md` `Static` history `src/tui/app.tsx:325` + batched `30fps` `src/tui/app.tsx:117` + single `useInput` `src/tui/app.tsx:185`
|
|
375
|
+
- `Banner` `src/tui/banner.tsx:1` `◆ Klyro v0.1.11` `cwd (branch ✎3)` `KLYRO.md` + resume `↻`
|
|
376
|
+
- `InputBox` `src/tui/input-box.tsx:1` `accent` `queued:` `shell` `!` `note` `#`
|
|
377
|
+
- `ThinkingBlock` `src/tui/thinking-block.tsx:1` `∴ Thinking… ctrl+t`
|
|
378
|
+
- `ActivityLine` `src/tui/activity-line.tsx:1` `✻ verb (4s · ↑1.2k)` + `CompactionDivider` `⟲`
|
|
379
|
+
- `Transcript` `src/tui/transcript.tsx:37` 6 kinds `ToolCard` `round` `cyan running` `green ✓`
|
|
380
|
+
- `StatusLine` `src/tui/status.tsx:23` `model step/max repairs tokens $0.043 ctx 6%`
|
|
381
|
+
- `Header` `src/tui/header.tsx:31` `abbrevPath`
|
|
382
|
+
- `DiffView` `src/tui/diff.tsx:57` `DiffHunk` `+ green` `- red` `context gray`
|
|
383
|
+
- `ApprovalModal` `src/tui/approval.tsx:60` `[y] once [a] session [A] always [n] [e] [?]` `TuiApprovalBridge` `src/tui/approval.tsx:31` promise `PendingPrompt`
|
|
384
|
+
- `PlanView` `src/tui/plan.tsx:44` `◯●✓✗⊘` `expanded`
|
|
385
|
+
- `DiffParser` `src/tui/diff-parser.ts:20` `parseUnifiedDiff`
|
|
386
|
+
- **Full-screen alt:** `src/tui/app.fullscreen.tsx:1` `width={width} height={height-1}` `Header` `KLYRO v0.1.9` `Conversation` `flexGrow` `Input` `StatusBar` (kept as `app.inline.tsx` backup)
|
|
387
|
+
- **History:** `~/.klyro/history` JSONL per project `src/tui/app.tsx:50` `loadHistory/appendHistory` `↑/↓` `Ctrl+R`
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 14. Policy & Safety
|
|
392
|
+
|
|
393
|
+
- **PathGuard** `src/policy/path-guard.ts:46` `resolveWithinCwd` `path.relative` + drive `PathGuardError`, `resolveAndFollowSymlinks` `realpath` `realParent` parent-symlink defense `src/policy/path-guard.ts:106`
|
|
394
|
+
- **Engine** `src/policy/engine.ts:84` `Decision allow|ask|deny` `PolicyConfig mode default|plan|accept-edits|auto + allow/deny/ask glob` `src/policy/engine.ts:34` `matchesGlobRule()` `src/policy/engine.ts:120` `.env` deny `src/policy/engine.ts:99` `additionalDirs` `src/policy/engine.ts:176` `shellDeny` `curl|sh` `src/policy/engine.ts:141`
|
|
395
|
+
- **Approval** `StdinApprovalPrompt` `DenyAll` `InMemoryAllowlist` `src/policy/approval.ts:21` + `TuiApprovalBridge`
|
|
396
|
+
- **Redactor** `src/policy/secret-redactor.ts:14` 7 patterns `aws-key` `aws-secret+b64` `pem-block` `github-token` `slack-token` `bearer` `jwt` `redact()` `createRedactor()` 1KiB tail
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 15. Eval Harness
|
|
401
|
+
|
|
402
|
+
- **Harness** `src/eval/harness.ts:70` `runTask()` `tmp klyro-eval-<id>` `ToolRegistry 9 tools` `verify()` + `runHarness()` `formatReport()` `compareReports()` `src/eval/harness.ts:180`
|
|
403
|
+
- **Tasks** `src/eval/tasks.ts:1` `MVP_TASKS 5` `t1 direct-answer` `t2 write-then-answer` `t3 policy-deny` `t4 max_steps` `t6 multitool`
|
|
404
|
+
- **File fixtures** `evals/fixtures/*` 10 smoke `read-answer` `add-fn-test` `fix-failing-test` … `task.md` `check.sh` `meta.json` `src/eval/harness.ts:130` `loadFileFixture` `runFileFixture` `bash -c`
|
|
405
|
+
- **Results** `evals/results/baseline.json` `8/10 80%` `src/eval/harness.ts:130`, `compareReports` `src/eval/harness.ts:180`, `klyro eval --suite smoke` `src/cli/eval.ts:84`
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## 16. CLI Surface
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
klyro # TUI REPL (isTTY ? TUI : legacy pipe)
|
|
413
|
+
klyro --tui / --no-tui / --chat # force
|
|
414
|
+
klyro -p "prompt" # headless positional (2.5)
|
|
415
|
+
klyro tui [-m model] [--max-steps n]
|
|
416
|
+
klyro run <prompt> [-m model] [--max-steps 30] [--max-cost $] [--max-time ms]
|
|
417
|
+
[--verify/--no-verify --verify-command <cmd> --max-repairs 3]
|
|
418
|
+
[--persist/--no-persist --resume-session <id>] [--output human|json|silent]
|
|
419
|
+
[--provider openai|anthropic] [--dry-run] [--resume <file>]
|
|
420
|
+
klyro chat [prompt] [-s system] # legacy
|
|
421
|
+
klyro eval [input] [--suite smoke --filter str --runs 1 --parallel 1 --output json]
|
|
422
|
+
klyro eval:compare <a> <b>
|
|
423
|
+
klyro config [list|get <key>|set <key> <value>|unset <key>|path|edit] # 5-layer Zod JSONC
|
|
424
|
+
klyro doctor [--json] # 7 checks
|
|
425
|
+
klyro completion <bash|zsh|fish|powershell>
|
|
426
|
+
klyro update # 24h cache
|
|
427
|
+
klyro login / logout [provider] # 0600 credentials.json, MODEL_ALIASES
|
|
428
|
+
klyro session list|show <id>|resume <id> [--json]
|
|
429
|
+
klyro resume <id> # alias
|
|
430
|
+
klyro trace <id> [--stats --json]
|
|
431
|
+
klyro --version / --help # global --cwd/--config/--debug/--json/--yes/--no-color
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Global flags `src/index.ts:60` `--cwd/--config/--debug/--verbose/--quiet/--json/--yes/--no-color` via `preAction` hook, `showSuggestionAfterError`.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## 17. Testing
|
|
439
|
+
|
|
440
|
+
- **Config:** `vitest.config.ts:1` `include src/**/*.test` `environment node` `fileParallelism:false` `testTimeout 10_000`
|
|
441
|
+
- **Results:** `43 files 309/309` (was 36/288, +7 P0 L4)
|
|
442
|
+
- **Coverage:** No `--coverage` threshold; L4 new tests: `edit-file.test.ts:1` 10 tests (CRLF/BOM/trailing), `multi-edit` 2, `apply-patch` 2, `background` 2, `klyro-md` 2, `checkpoints` 1, `git` 2
|
|
443
|
+
- **Security gate:** `src/security.test.ts:1` 16 tests (cwd jail, policy, redaction, hostile)
|
|
444
|
+
- **Flaky:** `fileParallelism:false` hides concurrency bugs (`plan.md:199` parallel tools)
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## 18. Known Gaps (Post-MVP)
|
|
449
|
+
|
|
450
|
+
- `compressTranscript()` `src/context/tokenizer.ts:76` deep-copy fixed but **not wired** into `runtime` (transcript unbounded)
|
|
451
|
+
- `verify` unbounded `stdout` `src/verification/engine.ts:31` no cap (vs `run_verify` 256k) — repair OOM
|
|
452
|
+
- `AuditLog` `src/persistence/audit.ts:23` defined but never written (dead code)
|
|
453
|
+
- `L6 repo-map` heuristic, no `tree-sitter`/`LSP` (plan L7)
|
|
454
|
+
- `L8+L9` wiring: `compressTranscript` + `checkpoints/store.ts` not used by `SessionStore`
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 19. For a New Agent — Where to Start
|
|
459
|
+
|
|
460
|
+
1. **Runtime is king:** `src/agent/runtime.ts:150` `run()` — get this loop right, everything else is leaf.
|
|
461
|
+
2. **Provider contract:** `src/agent/provider-adapter.ts:42` `ProviderAdapter` + `src/tools/schema.ts:63` Zod→JSON — defines `multi-model`.
|
|
462
|
+
3. **Edit tool:** `src/tools/fs/edit-file.ts:26` — most used, diff-return shapes model reasoning.
|
|
463
|
+
4. **Verification:** `src/verification/engine.ts:28` + `detect.ts:36` — the “more reliable than prompting” claim.
|
|
464
|
+
5. **Config:** `src/cli/config.ts:10` Zod-derived types constrain every module.
|
|
465
|
+
|
|
466
|
+
Run: `pnpm build && pnpm test && node dist/index.js --help` → `klyro -p "fix login test" --output json` → `klyro doctor`.
|
|
467
|
+
|
package/dist/agent/runtime.js
CHANGED
|
@@ -264,17 +264,7 @@ export async function run(opts, deps) {
|
|
|
264
264
|
}
|
|
265
265
|
if (finalizedCalls.length === 0) {
|
|
266
266
|
finalText = textBuf;
|
|
267
|
-
//
|
|
268
|
-
// Don't trigger verify for pure analysis (no edits)
|
|
269
|
-
if (!finalText && hasEdits === false && transcript.some((m) => m.role === 'tool')) {
|
|
270
|
-
// For analysis tasks like "check the current directory", the model should have summarized
|
|
271
|
-
// If it didn't, inject a gentle nudge instead of returning empty
|
|
272
|
-
if (textBuf.trim().length === 0) {
|
|
273
|
-
// Return what we have, but let TUI show it as done not error
|
|
274
|
-
finalText = '';
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
// Level 8 — Verification + Autonomous Repair
|
|
267
|
+
// Level 8 — Verification + Autonomous Repair (gated on hasEdits below — pure analysis skips verify)
|
|
278
268
|
const verifyEnabled = opts.verify?.enabled !== false;
|
|
279
269
|
const verifyCmd = opts.verify?.command ?? detectVerifyCommand(opts.cwd);
|
|
280
270
|
const maxRepairs = opts.verify?.maxRepairAttempts ?? 3;
|
package/dist/cli/repl.js
CHANGED
|
@@ -84,6 +84,14 @@ export async function startRepl(opts = {}) {
|
|
|
84
84
|
else
|
|
85
85
|
pendingQueue.push({ kind: 'append', item });
|
|
86
86
|
}
|
|
87
|
+
function queuedDelta(text) {
|
|
88
|
+
if (!text)
|
|
89
|
+
return;
|
|
90
|
+
if (isMounted && directHooks)
|
|
91
|
+
directHooks.appendDelta(text);
|
|
92
|
+
else
|
|
93
|
+
pendingQueue.push({ kind: 'delta', text });
|
|
94
|
+
}
|
|
87
95
|
function queuedStatus(s) {
|
|
88
96
|
lastStatus = { ...(lastStatus ?? { model: model ?? '', step: 0, maxSteps: opts.maxSteps ?? 30, usageInput: 0, usageOutput: 0, repairs: 0, status: 'idle' }), ...s };
|
|
89
97
|
if (isMounted && directHooks)
|
|
@@ -125,6 +133,8 @@ export async function startRepl(opts = {}) {
|
|
|
125
133
|
hooks.updateStatus(ev.patch);
|
|
126
134
|
else if (ev.kind === 'plan')
|
|
127
135
|
hooks.updatePlan(ev.plan);
|
|
136
|
+
else if (ev.kind === 'delta')
|
|
137
|
+
hooks.appendDelta(ev.text);
|
|
128
138
|
else
|
|
129
139
|
hooks.append(ev.item);
|
|
130
140
|
}
|
|
@@ -166,8 +176,6 @@ export async function startRepl(opts = {}) {
|
|
|
166
176
|
}
|
|
167
177
|
}
|
|
168
178
|
queuedStatus({ status: 'running', step: 0, model });
|
|
169
|
-
let textBuf = '';
|
|
170
|
-
let pendingTextId = null;
|
|
171
179
|
let activeCallId = null;
|
|
172
180
|
let activeCallName = null;
|
|
173
181
|
let activeCallArgs = '';
|
|
@@ -183,36 +191,11 @@ export async function startRepl(opts = {}) {
|
|
|
183
191
|
persist: sessionId ? { store: tuiStore, sessionId } : undefined,
|
|
184
192
|
onEvent: (ev) => {
|
|
185
193
|
if (ev.kind === 'step_start') {
|
|
186
|
-
// Flush coalesced text before new step
|
|
187
|
-
pendingTextId = null;
|
|
188
194
|
queuedStatus({ step: ev.step });
|
|
189
195
|
}
|
|
190
196
|
else if (ev.kind === 'text_delta') {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const g = globalThis;
|
|
194
|
-
if (isMounted && g.__klyroAppendDelta) {
|
|
195
|
-
g.__klyroAppendDelta(ev.text);
|
|
196
|
-
// Also keep coalescing for fallback inline mode
|
|
197
|
-
if (!pendingTextId)
|
|
198
|
-
pendingTextId = `text-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
// Fallback inline mode: coalesce via queuedAppend
|
|
202
|
-
if (pendingTextId) {
|
|
203
|
-
const last = pendingQueue[pendingQueue.length - 1];
|
|
204
|
-
if (last?.kind === 'append' && last.item.kind === 'text' && last.item.id === pendingTextId) {
|
|
205
|
-
last.item.text += ev.text;
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (pendingTextId && isMounted) {
|
|
210
|
-
queuedAppend({ id: pendingTextId, kind: 'text', text: ev.text, role: 'assistant' });
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
const id = `text-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
214
|
-
pendingTextId = id;
|
|
215
|
-
queuedAppend({ id, kind: 'text', text: ev.text, role: 'assistant' });
|
|
197
|
+
// single appendDelta path — App merges into one assistant item (Q→A order, no duplication)
|
|
198
|
+
queuedDelta(ev.text);
|
|
216
199
|
}
|
|
217
200
|
else if (ev.kind === 'verification_started') {
|
|
218
201
|
queuedAppend({ id: `vrfy-${Date.now()}`, kind: 'text', text: `[verify] running \`${ev.command}\``, role: 'assistant' });
|
|
@@ -272,16 +255,7 @@ export async function startRepl(opts = {}) {
|
|
|
272
255
|
});
|
|
273
256
|
}
|
|
274
257
|
else if (ev.kind === 'final_text') {
|
|
275
|
-
//
|
|
276
|
-
const g2 = globalThis;
|
|
277
|
-
g2.__klyroCommitLive?.();
|
|
278
|
-
pendingTextId = null;
|
|
279
|
-
// For full-screen, also ensure liveText is committed if any remaining
|
|
280
|
-
if (ev.text) {
|
|
281
|
-
const g3 = globalThis;
|
|
282
|
-
// If liveText was batched, ensure it's flushed and committed
|
|
283
|
-
g2.__klyroCommitLive?.();
|
|
284
|
-
}
|
|
258
|
+
// streamingId is closed by status change; no extra handling needed
|
|
285
259
|
}
|
|
286
260
|
else if (ev.kind === 'usage') {
|
|
287
261
|
queuedStatus({ usageInput: ev.input, usageOutput: ev.output });
|
|
@@ -330,13 +304,10 @@ export async function startRepl(opts = {}) {
|
|
|
330
304
|
queuedStatus({ status: 'done', repairs: result.repairs ?? 0 });
|
|
331
305
|
}
|
|
332
306
|
else {
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
queuedStatus({ status: '
|
|
336
|
-
|
|
337
|
-
if (!taskText.trim().toLowerCase().startsWith('check the current')) {
|
|
338
|
-
queuedAppend({ id: `no_final-${Date.now()}`, kind: 'text', text: `(no text — provider finished without content, try /model)`, role: 'assistant' });
|
|
339
|
-
}
|
|
307
|
+
// Genuine provider error: stream ended without a final answer. Surface as a loud
|
|
308
|
+
// error header with /doctor guidance rather than hiding it behind a polite hint.
|
|
309
|
+
queuedStatus({ status: 'error', errorMessage: 'no final text' });
|
|
310
|
+
queuedAppend({ id: `no_final-${Date.now()}`, kind: 'error', message: 'Provider returned no final text — check model/provider (try /model or /doctor)' });
|
|
340
311
|
}
|
|
341
312
|
}
|
|
342
313
|
else {
|
package/dist/tui/app.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Klyro
|
|
3
|
-
*
|
|
2
|
+
* Klyro TUI — opencode-style linear transcript
|
|
3
|
+
* Header (top) → Conversation (scrollable, Q→A→Q→A) → Input (bottom) → StatusBar (bottom)
|
|
4
|
+
* Single streamingId merges text_delta into one assistant item — no duplication, no liveText ghost.
|
|
4
5
|
*/
|
|
5
6
|
import React from 'react';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import type { StatusSnapshot } from './status.js';
|
|
8
|
+
import type { TranscriptItem } from './transcript.js';
|
|
8
9
|
import { TuiApprovalBridge } from './approval.js';
|
|
9
10
|
import type { PlanStep } from '../agent/runtime.js';
|
|
10
11
|
export interface AppProps {
|
|
@@ -18,6 +19,7 @@ export interface AppProps {
|
|
|
18
19
|
approvalBridge?: TuiApprovalBridge;
|
|
19
20
|
onMounted?: (hooks: {
|
|
20
21
|
append: (i: TranscriptItem) => void;
|
|
22
|
+
appendDelta: (text: string) => void;
|
|
21
23
|
updateStatus: (s: Partial<StatusSnapshot>) => void;
|
|
22
24
|
updatePlan: (p: PlanStep[]) => void;
|
|
23
25
|
}) => void;
|
package/dist/tui/app.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* Klyro
|
|
4
|
-
*
|
|
3
|
+
* Klyro TUI — opencode-style linear transcript
|
|
4
|
+
* Header (top) → Conversation (scrollable, Q→A→Q→A) → Input (bottom) → StatusBar (bottom)
|
|
5
|
+
* Single streamingId merges text_delta into one assistant item — no duplication, no liveText ghost.
|
|
5
6
|
*/
|
|
6
7
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
7
8
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
8
|
-
import { Transcript } from './transcript.js';
|
|
9
9
|
import { TuiApprovalBridge } from './approval.js';
|
|
10
10
|
import { PlanView } from './plan.js';
|
|
11
11
|
import { parse as parseSlash } from '../cli/slash/parser.js';
|
|
@@ -31,41 +31,17 @@ export function App(props) {
|
|
|
31
31
|
});
|
|
32
32
|
const [elapsed, setElapsed] = useState(0);
|
|
33
33
|
const [queued, setQueued] = useState(null);
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const batchTimer = useRef(null);
|
|
37
|
-
const flushBatch = useCallback(() => {
|
|
38
|
-
if (batchRef.current) {
|
|
39
|
-
const chunk = batchRef.current;
|
|
40
|
-
batchRef.current = '';
|
|
41
|
-
setLiveText((prev) => prev + chunk);
|
|
42
|
-
}
|
|
43
|
-
if (batchTimer.current) {
|
|
44
|
-
clearTimeout(batchTimer.current);
|
|
45
|
-
batchTimer.current = null;
|
|
46
|
-
}
|
|
47
|
-
}, []);
|
|
48
|
-
const appendDeltaBatched = useCallback((text) => {
|
|
49
|
-
batchRef.current += text;
|
|
50
|
-
if (!batchTimer.current)
|
|
51
|
-
batchTimer.current = setTimeout(flushBatch, 33);
|
|
52
|
-
}, [flushBatch]);
|
|
53
|
-
const commitLive = useCallback(() => {
|
|
54
|
-
flushBatch();
|
|
55
|
-
if (liveText) {
|
|
56
|
-
const item = { id: nextId('text'), kind: 'text', text: liveText, role: 'assistant' };
|
|
57
|
-
setTranscript((prev) => [...prev, item]);
|
|
58
|
-
setLiveText('');
|
|
59
|
-
}
|
|
60
|
-
}, [liveText, flushBatch]);
|
|
34
|
+
// streaming: one assistant text item that text_delta merges into
|
|
35
|
+
const streamingIdRef = useRef(null);
|
|
61
36
|
useEffect(() => bridge.subscribe((p) => setAwaitingApproval(p !== null)), [bridge]);
|
|
62
|
-
//
|
|
37
|
+
// queued: send when idle (2.4)
|
|
63
38
|
useEffect(() => {
|
|
64
39
|
if (queued && status.status !== 'running' && !awaitingApproval) {
|
|
65
40
|
const toSend = queued;
|
|
66
41
|
setQueued(null);
|
|
67
|
-
const item = { id: nextId('
|
|
42
|
+
const item = { id: nextId('user'), kind: 'text', text: toSend, role: 'user' };
|
|
68
43
|
setTranscript((prev) => [...prev, item]);
|
|
44
|
+
streamingIdRef.current = null;
|
|
69
45
|
const cmd = parseSlash(toSend.trim());
|
|
70
46
|
if (cmd.kind === 'prompt')
|
|
71
47
|
void props.onPrompt(cmd.text);
|
|
@@ -81,34 +57,56 @@ export function App(props) {
|
|
|
81
57
|
return () => clearInterval(t);
|
|
82
58
|
}, [status.status, elapsed]);
|
|
83
59
|
const append = useCallback((item) => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
60
|
+
// any non-streaming append closes the current streaming block
|
|
61
|
+
if (item.kind !== 'text' || item.role !== 'assistant')
|
|
62
|
+
streamingIdRef.current = null;
|
|
63
|
+
setTranscript((prev) => [...prev, item]);
|
|
64
|
+
}, []);
|
|
65
|
+
const appendDelta = useCallback((text) => {
|
|
66
|
+
if (!text)
|
|
67
|
+
return;
|
|
68
|
+
const sid = streamingIdRef.current;
|
|
69
|
+
if (sid) {
|
|
70
|
+
setTranscript((prev) => {
|
|
71
|
+
const idx = prev.findIndex((x) => x.id === sid);
|
|
72
|
+
if (idx === -1)
|
|
73
|
+
return [...prev, { id: sid, kind: 'text', text, role: 'assistant' }];
|
|
74
|
+
const cur = prev[idx];
|
|
75
|
+
const next = { ...cur, text: cur.text + text };
|
|
76
|
+
const copy = [...prev];
|
|
77
|
+
copy[idx] = next;
|
|
78
|
+
return copy;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const id = nextId('stream');
|
|
83
|
+
streamingIdRef.current = id;
|
|
84
|
+
setTranscript((prev) => [...prev, { id, kind: 'text', text, role: 'assistant' }]);
|
|
85
|
+
}
|
|
91
86
|
}, []);
|
|
87
|
+
// close streaming block when status leaves running (so next text_delta starts new item)
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (status.status !== 'running')
|
|
90
|
+
streamingIdRef.current = null;
|
|
91
|
+
}, [status.status]);
|
|
92
92
|
const updateStatus = useCallback((s) => setStatus((p) => ({ ...p, ...s })), []);
|
|
93
93
|
const updatePlan = useCallback((p) => setPlan(p), []);
|
|
94
94
|
const onMountedRef = useRef(props.onMounted);
|
|
95
95
|
useEffect(() => { onMountedRef.current = props.onMounted; }, [props.onMounted]);
|
|
96
96
|
useEffect(() => {
|
|
97
|
-
onMountedRef.current?.({ append, updateStatus, updatePlan });
|
|
97
|
+
onMountedRef.current?.({ append, appendDelta, updateStatus, updatePlan });
|
|
98
|
+
// global hooks for repl bridge (instance-local queue drains here)
|
|
98
99
|
globalThis.__klyroAppAppend = append;
|
|
100
|
+
globalThis.__klyroAppendDelta = appendDelta;
|
|
99
101
|
globalThis.__klyroAppStatus = updateStatus;
|
|
100
102
|
globalThis.__klyroAppPlan = updatePlan;
|
|
101
|
-
globalThis.__klyroAppendDelta = appendDeltaBatched;
|
|
102
|
-
globalThis.__klyroCommitLive = commitLive;
|
|
103
103
|
return () => {
|
|
104
104
|
delete globalThis.__klyroAppAppend;
|
|
105
|
+
delete globalThis.__klyroAppendDelta;
|
|
105
106
|
delete globalThis.__klyroAppStatus;
|
|
106
107
|
delete globalThis.__klyroAppPlan;
|
|
107
|
-
delete globalThis.__klyroAppendDelta;
|
|
108
|
-
delete globalThis.__klyroCommitLive;
|
|
109
108
|
};
|
|
110
|
-
}, [append, updateStatus, updatePlan
|
|
111
|
-
// Single useInput owner — handles queued when running (2.4)
|
|
109
|
+
}, [append, appendDelta, updateStatus, updatePlan]);
|
|
112
110
|
useInput((inputStr, key) => {
|
|
113
111
|
if (awaitingApproval)
|
|
114
112
|
return;
|
|
@@ -123,7 +121,8 @@ export function App(props) {
|
|
|
123
121
|
return;
|
|
124
122
|
setQueued(v);
|
|
125
123
|
setInput('');
|
|
126
|
-
|
|
124
|
+
// queued indicator as muted text, not a full user bubble (opencode style)
|
|
125
|
+
setTranscript((prev) => [...prev, { id: nextId('queued'), kind: 'text', text: `queued: ${v.slice(0, 80)}`, role: 'assistant' }]);
|
|
127
126
|
return;
|
|
128
127
|
}
|
|
129
128
|
if (key.backspace || key.delete) {
|
|
@@ -141,8 +140,9 @@ export function App(props) {
|
|
|
141
140
|
if (!v)
|
|
142
141
|
return;
|
|
143
142
|
setInput('');
|
|
144
|
-
const item = { id: nextId('
|
|
143
|
+
const item = { id: nextId('user'), kind: 'text', text: v, role: 'user' };
|
|
145
144
|
setTranscript((prev) => [...prev, item]);
|
|
145
|
+
streamingIdRef.current = null;
|
|
146
146
|
const cmd = parseSlash(v);
|
|
147
147
|
if (cmd.kind === 'prompt')
|
|
148
148
|
void props.onPrompt(cmd.text);
|
|
@@ -160,5 +160,5 @@ export function App(props) {
|
|
|
160
160
|
const width = stdout?.columns ?? 100;
|
|
161
161
|
const height = stdout?.rows ?? 30;
|
|
162
162
|
const isSmall = width < 80;
|
|
163
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: height - 1, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsx(Text, { bold: true, children: "KLYRO v0.1.
|
|
163
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height - 1, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsx(Text, { bold: true, children: "KLYRO v0.1.16" }), _jsxs(Text, { color: tokens.ansi.muted, children: [status.model, " \u00B7 API Usage Billing \u00B7 step ", status.step, "/", status.maxSteps, " \u00B7 ", status.status, " \u00B7 repairs ", status.repairs] }), _jsx(Text, { color: tokens.ansi.muted, children: props.cwd })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingX: 1, paddingY: 1, children: [transcript.length === 0 ? (_jsx(Text, { color: tokens.ansi.muted, children: "No conversation yet. Try \"hi\" or /help" })) : (transcript.map((item) => (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: item.kind === 'text' && item.role === 'user' ? (_jsxs(Text, { children: ["\u203A ", item.text] })) : item.kind === 'text' ? (_jsxs(Text, { children: [" ", item.text] })) : item.kind === 'tool' ? (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsxs(Text, { children: [item.name, " ", item.isError ? '✗' : '✓', " ", item.latencyMs ?? 0, "ms"] }), item.result ? _jsx(Text, { color: tokens.ansi.muted, children: String(item.result).slice(0, 300) }) : null] })) : item.kind === 'diff' ? (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsx(Text, { bold: true, children: item.summary ?? 'Diff' }), item.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: tokens.ansi.info, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { color: l.kind === 'add' ? tokens.ansi.success : l.kind === 'remove' ? tokens.ansi.error : tokens.ansi.muted, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', l.text] }, j)))] }, i)))] })) : item.kind === 'error' ? (_jsxs(Text, { color: tokens.ansi.error, children: ["[error] ", item.message] })) : item.kind === 'policy' ? (_jsxs(Text, { color: tokens.ansi.muted, children: ["[policy] ", item.action, " ", item.name, item.reason ? ` — ${item.reason}` : ''] })) : item.kind === 'file_changed' ? (_jsxs(Text, { color: tokens.ansi.muted, children: ["[", item.op, "] ", item.path] })) : null }, item.id)))), status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { children: [_jsx(Text, { color: tokens.ansi.info, children: "\u2726 Thinking..." }), _jsxs(Text, { color: tokens.ansi.muted, children: [" \u00B7 ", Math.round(elapsed / 1000), "s"] })] })) : null, plan.length > 0 ? _jsx(PlanView, { steps: plan, expanded: false, onToggle: () => { } }) : null] }), _jsxs(Box, { borderStyle: "single", borderColor: tokens.ansi.accent, paddingX: 1, children: [_jsx(Text, { children: "\u203A " }), _jsxs(Text, { children: [input, "\u258F"] })] }), _jsxs(Box, { justifyContent: "space-between", paddingX: 1, borderStyle: "single", borderColor: tokens.ansi.border, children: [_jsxs(Text, { color: tokens.ansi.muted, children: [status.model, " \u00B7 ", status.usageInput + status.usageOutput, " tokens \u00B7 $", (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015).toFixed(2), " \u00B7 ", Math.round(elapsed / 1000), "s"] }), _jsx(Text, { color: tokens.ansi.muted, children: isSmall ? 'Ctrl+C interrupt' : 'Ctrl+C interrupt · Ctrl+O expand · ↑↓ scroll' })] })] }));
|
|
164
164
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klyro",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Klyro — autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
14
14
|
"README.md",
|
|
15
|
+
"READ.md",
|
|
15
16
|
"LICENSE"
|
|
16
17
|
],
|
|
17
18
|
"engines": {
|