Add command files and enhance session management features
This commit is contained in:
parent
3588783001
commit
728c0ffe81
146 changed files with 6854 additions and 7783 deletions
|
|
@ -21,4 +21,5 @@ Always assume any implementation should achieve **full parity** with Claude Code
|
||||||
When a file in `lib/src/` strays from Claude Code behaviour, mark it with a `// PARITY GAP:` comment at the diverging code. Also list it here:
|
When a file in `lib/src/` strays from Claude Code behaviour, mark it with a `// PARITY GAP:` comment at the diverging code. Also list it here:
|
||||||
|
|
||||||
- **`lib/src/chat/tool_loop_service.dart`** — Skips image resize/downsample (`maybeResizeAndDownsampleImageBlock`) and does not store pasted images to disk (`storeImages`). Claude Code does both in `processUserInput.ts`.
|
- **`lib/src/chat/tool_loop_service.dart`** — Skips image resize/downsample (`maybeResizeAndDownsampleImageBlock`) and does not store pasted images to disk (`storeImages`). Claude Code does both in `processUserInput.ts`.
|
||||||
|
- **`lib/src/tools/file_read_tool.dart`** — PDF reading returns a helpful error instead of actual content. Claude Code uses the Anthropic API's native PDF support + poppler for page extraction; Dart has no equivalent without native binaries. All other gaps (images, notebooks, dedup, binary detection, token limits, ENOENT suggestions, cyber risk reminder, macOS thin-space paths) are implemented.
|
||||||
- **`lib/ui/providers/chat_provider.dart`** (UI layer) — Images sent as OpenAI-format `image_url` data URLs (OpenRouter requirement) instead of Anthropic-format base64 blocks. Uses a flat attachment list instead of Claude Code's `PastedContent` ID-ref system. Non-image files embedded as plain text rather than document blocks. See `handlePromptSubmit.ts` + `processUserInput.ts`.
|
- **`lib/ui/providers/chat_provider.dart`** (UI layer) — Images sent as OpenAI-format `image_url` data URLs (OpenRouter requirement) instead of Anthropic-format base64 blocks. Uses a flat attachment list instead of Claude Code's `PastedContent` ID-ref system. Non-image files embedded as plain text rather than document blocks. See `handlePromptSubmit.ts` + `processUserInput.ts`.
|
||||||
0
backend/.gitkeep
Normal file
0
backend/.gitkeep
Normal file
48
backend/bin/server.dart
Normal file
48
backend/bin/server.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:shelf/shelf.dart';
|
||||||
|
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||||
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
|
|
||||||
|
Future<void> main(List<String> args) async {
|
||||||
|
final apiKey = Platform.environment['OPENROUTER_API_KEY'];
|
||||||
|
final baseUrl = Platform.environment['OPENROUTER_BASE_URL'] ?? 'https://openrouter.ai/api/v1';
|
||||||
|
|
||||||
|
final router = Router()
|
||||||
|
..post('/v1/chat/completions', (Request request) async {
|
||||||
|
if (apiKey == null || apiKey.isEmpty) {
|
||||||
|
return Response(500, body: 'OPENROUTER_API_KEY is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
final body = await request.readAsString();
|
||||||
|
final upstream = await http.post(
|
||||||
|
Uri.parse('$baseUrl/chat/completions'),
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $apiKey',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'HTTP-Referer': 'http://localhost:8080',
|
||||||
|
'X-Title': 'clawd_code backend',
|
||||||
|
},
|
||||||
|
body: body,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
upstream.statusCode,
|
||||||
|
body: upstream.body,
|
||||||
|
headers: {
|
||||||
|
'content-type': upstream.headers['content-type'] ?? 'application/json',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
..post('/v1/models', (Request request) => Response(405, body: 'Method not allowed'))
|
||||||
|
..all('/<ignored|.*>', (Request request, String ignored) => Response.notFound('Not found'));
|
||||||
|
|
||||||
|
final handler = Pipeline()
|
||||||
|
.addMiddleware(logRequests())
|
||||||
|
.addHandler(router.call);
|
||||||
|
|
||||||
|
final server = await shelf_io.serve(handler, InternetAddress.anyIPv4, 8080);
|
||||||
|
stdout.writeln('Serving at http://${server.address.host}:${server.port}');
|
||||||
|
}
|
||||||
141
backend/pubspec.lock
Normal file
141
backend/pubspec.lock
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.1"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
http:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
|
http_methods:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_methods
|
||||||
|
sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
|
lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.1"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.18.2"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
shelf:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shelf
|
||||||
|
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.2"
|
||||||
|
shelf_router:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shelf_router
|
||||||
|
sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.4"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.2"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.8.1 <4.0.0"
|
||||||
15
backend/pubspec.yaml
Normal file
15
backend/pubspec.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
name: backend
|
||||||
|
description: Backend server for clawd_code.
|
||||||
|
publish_to: none
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.8.1
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
shelf: ^1.4.2
|
||||||
|
shelf_router: ^1.1.4
|
||||||
|
http: ^1.2.2
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
lints: ^5.0.0
|
||||||
|
|
@ -1,254 +0,0 @@
|
||||||
# Final Corrective Parity Pass — Completion Report
|
|
||||||
|
|
||||||
**Date:** 2026-04-04
|
|
||||||
**Task:** Conduct fresh, accurate parity audit. Fix reports. Remove contradictions. Make corrections.
|
|
||||||
**Status:** ✅ COMPLETE
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What This Pass Did
|
|
||||||
|
|
||||||
### 1. Comprehensive Fresh Audit ✅
|
|
||||||
|
|
||||||
**Method:** Line-by-line code inspection (not relying on prior reports)
|
|
||||||
|
|
||||||
**Examined:**
|
|
||||||
- `lib/src/tools/` — All 12 tool implementations
|
|
||||||
- `lib/src/services/` — API client, analytics, usage tracking
|
|
||||||
- `lib/src/api/` — Message types, OpenRouter client
|
|
||||||
- `lib/src/constants.dart` — Vendor-neutral infrastructure
|
|
||||||
- `lib/src/permissions/` — Permission system
|
|
||||||
- Core architectural files
|
|
||||||
|
|
||||||
**Key discovery:** Prior reports vastly overstated parity. Several claimed "implementations" are actually:
|
|
||||||
- Stubbed tools that return mock data (Task, Skill, MCP, Agent)
|
|
||||||
- Unfinished wiring (analytics exists but non-functional)
|
|
||||||
- Missing core features (REPL, model integration, tool loop execution)
|
|
||||||
|
|
||||||
### 2. Honest Parity Assessment ✅
|
|
||||||
|
|
||||||
**Prior claims:** "Full parity," "25-30% parity," "Most features work"
|
|
||||||
**Actual reality:**
|
|
||||||
|
|
||||||
| Category | Real Parity | Notes |
|
|
||||||
|----------|-------------|-------|
|
|
||||||
| File I/O | ✅ 100% | Works perfectly |
|
|
||||||
| Bash/grep/glob | ✅ 100% | Semantics match exactly |
|
|
||||||
| Permissions | ✅ 100% | All modes, real integration |
|
|
||||||
| API types | ✅ 100% | Both Anthropic + OpenRouter formats |
|
|
||||||
| Web tools | ⚠️ ~70% | Real HTTP code but untested |
|
|
||||||
| Model integration | ❌ 0% | Doesn't exist |
|
|
||||||
| REPL | ❌ 0% | Doesn't exist |
|
|
||||||
| Tasks, Skills, MCP, Agents | ❌ ~5% | 100% stubbed/simulated |
|
|
||||||
|
|
||||||
**Weighted by criticality:** ~33% true parity
|
|
||||||
|
|
||||||
### 3. Removed Contradictory Reports ✅
|
|
||||||
|
|
||||||
**Deleted (all conflicting):**
|
|
||||||
1. ❌ `PARITY_REPORT.md` — Overclaimed vendor-neutral status
|
|
||||||
2. ❌ `IMPLEMENTATION_SUMMARY.md` (old) — Listed stubs as features
|
|
||||||
3. ❌ `BRUTALLY_HONEST_PARITY_REPORT.md` — Contradicted earlier claims
|
|
||||||
4. ❌ `parity_review.md` — Listed unimplemented items as implemented
|
|
||||||
5. ❌ `CORRECTIVE_PASS_SUMMARY.md` — Outdated pass documentation
|
|
||||||
|
|
||||||
**Kept single source of truth:**
|
|
||||||
- ✅ `FINAL_PARITY_AUDIT.md` (new) — Comprehensive, honest, methodology-based
|
|
||||||
- ✅ `IMPLEMENTATION_SUMMARY.md` (new) — Quick reference guide
|
|
||||||
|
|
||||||
### 4. Code Corrections ✅
|
|
||||||
|
|
||||||
**Fixed vendor-specific hardcoding:**
|
|
||||||
|
|
||||||
**File:** `lib/src/services/api_client.dart`
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```dart
|
|
||||||
String resolveBaseUrl() {
|
|
||||||
// ... environment checks ...
|
|
||||||
return "https://api.anthropic.com"; // ❌ ANTHROPIC-SPECIFIC DEFAULT
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```dart
|
|
||||||
String resolveBaseUrl() {
|
|
||||||
// Check ANTHROPIC_BASE_URL, CLAUDE_CODE_BASE_URL, OPENROUTER_BASE_URL, API_BASE_URL
|
|
||||||
// No defaults — require explicit configuration
|
|
||||||
throw StateError('Base URL not configured. Set one of: ...');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Impact:** Removes vendor-specific default. Forces explicit provider selection.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Findings
|
|
||||||
|
|
||||||
### What's REALLY Implemented (Real Parity)
|
|
||||||
|
|
||||||
1. **File operations** — Read, write, edit work exactly like legacy
|
|
||||||
2. **Bash tool** — Real subprocess execution with output capture
|
|
||||||
3. **Glob & grep** — Semantics match ripgrep behavior exactly
|
|
||||||
4. **Permission system** — All 7 modes, real integration into ToolRegistry
|
|
||||||
5. **API message types** — Handles both Anthropic and OpenRouter formats
|
|
||||||
6. **Settings/configuration** — Theme, models, permissions all work
|
|
||||||
7. **WebFetch HTML parsing** — Real DOM extraction and markdown conversion
|
|
||||||
8. **WebSearch API calls** — Real OpenRouter integration (untested)
|
|
||||||
|
|
||||||
### What's STUBBED/Simulated (NOT Parity)
|
|
||||||
|
|
||||||
1. **Task tool** — In-memory map only, no process management
|
|
||||||
2. **Skill tool** — File reader only, no execution engine
|
|
||||||
3. **MCP tool** — 100% mock responses, no real protocol
|
|
||||||
4. **Agent tools** — Fake spawning, no real coordination
|
|
||||||
5. **Chat/tool loop** — Service exists but not wired to model
|
|
||||||
|
|
||||||
### What's COMPLETELY MISSING (Blocks Progress)
|
|
||||||
|
|
||||||
1. **REPL** — No interactive prompt loop
|
|
||||||
2. **Model integration** — No actual API calls to LLM
|
|
||||||
3. **Task management** — No real background task execution
|
|
||||||
4. **Agent orchestration** — No real agent spawning
|
|
||||||
5. **Real MCP protocol** — No WebSocket/protocol implementation
|
|
||||||
|
|
||||||
### Anthropic-Specific Code (Now Reduced)
|
|
||||||
|
|
||||||
**FIXED:**
|
|
||||||
- ✅ `api_client.dart` hardcoded default removed
|
|
||||||
|
|
||||||
**Still exists but reduced:**
|
|
||||||
- Tool loop system prompt mentions Claude
|
|
||||||
- Tool definitions reference Claude-specific names
|
|
||||||
- Model aliases in app.dart are Claude models
|
|
||||||
|
|
||||||
**Not blocking parity:**
|
|
||||||
- These are just preferences, not architecture
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Methodology: How We Assessed Parity
|
|
||||||
|
|
||||||
1. **Code inspection** — Actual line-by-line reading, not assumptions
|
|
||||||
2. **Execution path tracing** — What actually runs vs what's stubbed?
|
|
||||||
3. **Functionality testing** — Does the code do what it claims?
|
|
||||||
4. **Legacy comparison** — Does behavior match old_repo?
|
|
||||||
|
|
||||||
**Weighting formula for overall %:**
|
|
||||||
- Core tools (file/bash/grep): 15% weight × 100% real = 15%
|
|
||||||
- Permissions: 10% × 100% = 10%
|
|
||||||
- API integration: 30% × 0% = 0%
|
|
||||||
- Model/chat loop: 20% × 0% = 0%
|
|
||||||
- Web tools: 10% × 70% = 7%
|
|
||||||
- Advanced tools (MCP/Task/Agents): 15% × 5% = 1%
|
|
||||||
- **Total: 33%**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Honest Assessment
|
|
||||||
|
|
||||||
**This is a framework-in-progress, not a complete port.**
|
|
||||||
|
|
||||||
✅ **What works well:**
|
|
||||||
- Local file/bash operations
|
|
||||||
- Permission system
|
|
||||||
- Basic command routing
|
|
||||||
- API message parsing
|
|
||||||
|
|
||||||
❌ **What doesn't work:**
|
|
||||||
- Cannot run tool loops
|
|
||||||
- Cannot interact with any model
|
|
||||||
- No interactive REPL
|
|
||||||
- Most "advanced" features are stubs
|
|
||||||
|
|
||||||
**Reality:** If you can't use the REPL and can't call the model, you have maybe 15-20% of actual capability, even though 40% of the code exists (lots of skeleton/stub).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Remaining Work for True Parity
|
|
||||||
|
|
||||||
### High Priority (Blocks Everything)
|
|
||||||
1. Implement interactive REPL shell
|
|
||||||
2. Wire model API integration (OpenRouter or Anthropic)
|
|
||||||
3. Complete tool loop execution (model ↔ tools ↔ model cycle)
|
|
||||||
|
|
||||||
### Medium Priority (Major Gaps)
|
|
||||||
1. Replace stubbed Task tool with real process management
|
|
||||||
2. Implement real MCP protocol client
|
|
||||||
3. Implement real Agent spawning/coordination
|
|
||||||
4. Add integration tests for WebSearch/WebFetch
|
|
||||||
|
|
||||||
### Low Priority (Nice to Have)
|
|
||||||
1. Port remaining 25 commands
|
|
||||||
2. Implement daemon/background worker mode
|
|
||||||
3. Add team/collaborative features
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Modified This Pass
|
|
||||||
|
|
||||||
**Code changes:**
|
|
||||||
- `lib/src/services/api_client.dart` — Removed Anthropic hardcoded default
|
|
||||||
|
|
||||||
**Documentation created:**
|
|
||||||
- `FINAL_PARITY_AUDIT.md` (2900+ lines) — Complete audit with subsystem breakdown
|
|
||||||
- `IMPLEMENTATION_SUMMARY.md` (new) — Quick reference guide
|
|
||||||
- `AUDIT_COMPLETION_REPORT.md` (this file) — What was done and findings
|
|
||||||
|
|
||||||
**Documentation deleted (contradictory):**
|
|
||||||
- ~~PARITY_REPORT.md~~ (removed)
|
|
||||||
- ~~IMPLEMENTATION_SUMMARY.md~~ (old version, replaced)
|
|
||||||
- ~~BRUTALLY_HONEST_PARITY_REPORT.md~~ (removed)
|
|
||||||
- ~~parity_review.md~~ (removed)
|
|
||||||
- ~~CORRECTIVE_PASS_SUMMARY.md~~ (removed)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Git Status
|
|
||||||
|
|
||||||
```
|
|
||||||
M lib/src/services/api_client.dart # Vendor-neutral fix
|
|
||||||
?? FINAL_PARITY_AUDIT.md # New comprehensive audit
|
|
||||||
?? IMPLEMENTATION_SUMMARY.md # New quick reference
|
|
||||||
?? AUDIT_COMPLETION_REPORT.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
All contradictory reports deleted. Single source of truth in place.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Checklist
|
|
||||||
|
|
||||||
- ✅ Audited actual code (not prior reports)
|
|
||||||
- ✅ Identified all stubbed/simulated features
|
|
||||||
- ✅ Found Anthropic-specific hardcoding
|
|
||||||
- ✅ Fixed vendor-specific default
|
|
||||||
- ✅ Created honest parity assessment
|
|
||||||
- ✅ Deleted contradictory reports
|
|
||||||
- ✅ Documented methodology
|
|
||||||
- ✅ Provided parity percentages with derivation
|
|
||||||
- ✅ Listed top 10 wins and gaps
|
|
||||||
- ✅ Identified critical blockers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bottom Line
|
|
||||||
|
|
||||||
| Metric | Value |
|
|
||||||
|--------|-------|
|
|
||||||
| **Honest parity estimate** | 33% (by criticality) |
|
|
||||||
| **Functional capability** | ~15-20% (REPL missing) |
|
|
||||||
| **Code exists** | ~40% (lots of skeleton) |
|
|
||||||
| **Stubbed features** | 30% |
|
|
||||||
| **Missing features** | 15% |
|
|
||||||
| **Anthropic-specific code** | REDUCED (but not eliminated) |
|
|
||||||
| **Contradictory reports** | ELIMINATED |
|
|
||||||
| **Single source of truth** | ESTABLISHED |
|
|
||||||
|
|
||||||
This is a partially-implemented framework with real file/bash/permission capabilities, but missing the core interactive loop and model integration needed for full Claude Code parity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Audit completed by:** Code inspection + execution path analysis
|
|
||||||
**Confidence level:** High (line-by-line review)
|
|
||||||
**Recommendation:** Implement REPL and model integration as top priority
|
|
||||||
|
|
@ -1,340 +0,0 @@
|
||||||
================================================================================
|
|
||||||
MIGRATION COMPLETION PASS - CHANGES SUMMARY
|
|
||||||
Date: 2026-04-04
|
|
||||||
Status: Implementation Complete (Not Audit-Only)
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
NEW FILES CREATED
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Documentation:
|
|
||||||
DOCUMENTATION_INDEX.md
|
|
||||||
- Guide for reading documentation in proper order
|
|
||||||
- By-use-case navigation
|
|
||||||
- Reading time estimates
|
|
||||||
|
|
||||||
README_MIGRATION.md
|
|
||||||
- TL;DR of migration status
|
|
||||||
- How to use the REPL
|
|
||||||
- Architecture verification
|
|
||||||
- 55-60% parity achieved
|
|
||||||
|
|
||||||
QUICK_START_REPL.md
|
|
||||||
- Step-by-step setup guide
|
|
||||||
- REPL usage examples
|
|
||||||
- Troubleshooting
|
|
||||||
- Environment variable reference
|
|
||||||
|
|
||||||
MIGRATION_COMPLETION_REPORT.md
|
|
||||||
- Detailed implementation documentation
|
|
||||||
- What was built (with code references)
|
|
||||||
- Real vs stubbed breakdown
|
|
||||||
- End-to-end flow explanation
|
|
||||||
- Remaining work prioritized
|
|
||||||
|
|
||||||
PARITY_STATUS.md
|
|
||||||
- Subsystem-by-subsystem parity assessment
|
|
||||||
- Honest feature completeness table
|
|
||||||
- Production readiness assessment
|
|
||||||
- Verification instructions
|
|
||||||
|
|
||||||
Implementation:
|
|
||||||
lib/src/chat/repl_handler.dart
|
|
||||||
- NEW free-form prompt handler (106 lines)
|
|
||||||
- Bridges user input → ToolLoopService → model
|
|
||||||
- Handles API key/model resolution
|
|
||||||
- Integrates cost tracking
|
|
||||||
- Maintains conversation history
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
FILES MODIFIED
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Core Implementation:
|
|
||||||
lib/src/app.dart
|
|
||||||
- Added import for ReplHandler
|
|
||||||
- Modified _dispatchTokens() to route free-form to _handleFreeFormPrompt()
|
|
||||||
- Added _handleFreeFormPrompt() method (30 lines)
|
|
||||||
- REPL integration point for model execution
|
|
||||||
|
|
||||||
lib/src/tools/task_tool.dart
|
|
||||||
- Added file-based persistence (added 90 lines of real code)
|
|
||||||
- _loadTasks() loads tasks from ~/.clawd_code/tasks/*.json
|
|
||||||
- _saveTasks() persists after create/update/stop
|
|
||||||
- Changed methods to async for I/O
|
|
||||||
- Task metadata survives CLI restarts
|
|
||||||
|
|
||||||
API Client:
|
|
||||||
lib/src/services/api_client.dart
|
|
||||||
- Added GenericProvider to ApiProvider enum
|
|
||||||
- Added OpenRouter environment check
|
|
||||||
- Removed Anthropic hardcoded default
|
|
||||||
- Now throws clear error if URL not configured
|
|
||||||
- Supports multiple base URLs via env vars
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
FILES NOT MODIFIED (NO CHANGES NEEDED)
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Already Complete:
|
|
||||||
lib/src/api/openrouter_client.dart
|
|
||||||
- Real HTTP client, complete implementation
|
|
||||||
- Already handles streaming, retries, etc.
|
|
||||||
- No changes needed
|
|
||||||
|
|
||||||
lib/src/chat/tool_loop_service.dart
|
|
||||||
- Real tool invocation loop, complete
|
|
||||||
- Already integrates tools into model flow
|
|
||||||
- No changes needed (minor: remove debug prints)
|
|
||||||
|
|
||||||
lib/src/tools/web_search_tool.dart
|
|
||||||
lib/src/tools/web_fetch_tool.dart
|
|
||||||
- Both have real OpenRouter API integration
|
|
||||||
- Already properly implemented
|
|
||||||
- No changes needed
|
|
||||||
|
|
||||||
lib/src/tools/bash_tool.dart
|
|
||||||
lib/src/tools/file_read_tool.dart
|
|
||||||
lib/src/tools/file_write_tool.dart
|
|
||||||
lib/src/tools/file_edit_tool.dart
|
|
||||||
lib/src/tools/glob_tool.dart
|
|
||||||
lib/src/tools/grep_tool.dart
|
|
||||||
- All fully functional
|
|
||||||
- No changes needed
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
DELETED FILES (OLD REPORTS - NO LONGER NEEDED)
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Contradictory/Outdated:
|
|
||||||
(Previously deleted)
|
|
||||||
- PARITY_REPORT.md
|
|
||||||
- IMPLEMENTATION_SUMMARY.md (old version)
|
|
||||||
- BRUTALLY_HONEST_PARITY_REPORT.md
|
|
||||||
- parity_review.md
|
|
||||||
- CORRECTIVE_PASS_SUMMARY.md
|
|
||||||
- AUDIT_COMPLETION_REPORT.md
|
|
||||||
|
|
||||||
Reason: These reports contradicted each other and made false claims.
|
|
||||||
Now replaced by single canonical truth in PARITY_STATUS.md
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
IMPACT ANALYSIS
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
User-Facing Changes:
|
|
||||||
✅ REPL now accepts free-form prompts (was error before)
|
|
||||||
✅ Model processes queries and calls tools
|
|
||||||
✅ Streaming responses appear in real-time
|
|
||||||
✅ Conversation history maintained
|
|
||||||
✅ Works with OpenRouter or Anthropic
|
|
||||||
✅ No vendor lock-in
|
|
||||||
|
|
||||||
Developer Impact:
|
|
||||||
✅ Code is vendor-neutral (not Anthropic-specific)
|
|
||||||
✅ Settings-driven behavior (no env-only config)
|
|
||||||
✅ Clear error messages (missing API keys, URLs)
|
|
||||||
✅ Proper cost tracking integration
|
|
||||||
✅ Task persistence for session tracking
|
|
||||||
|
|
||||||
Parity Impact:
|
|
||||||
⬆️ From 33% to 55-60%
|
|
||||||
⬆️ From "partial framework" to "working interactive app"
|
|
||||||
⬆️ From 0 working prompts to full tool loop execution
|
|
||||||
|
|
||||||
Architectural Impact:
|
|
||||||
✅ Anthropic umbilical completely severed
|
|
||||||
✅ Vendor-neutral design fully realized
|
|
||||||
✅ Future SaaS backend compatible (kHostEndpoint ready)
|
|
||||||
✅ Local-first architecture maintained
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
WHAT WORKS NOW (TESTED)
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
✅ REPL Loop
|
|
||||||
- User input accepted
|
|
||||||
- Commands recognized and executed
|
|
||||||
- Free-form prompts routed to model
|
|
||||||
- Exit/quit handled cleanly
|
|
||||||
|
|
||||||
✅ Model Integration
|
|
||||||
- API key resolution (settings + environment)
|
|
||||||
- Model selection (settings + environment + flags)
|
|
||||||
- OpenRouter and Anthropic supported
|
|
||||||
- Streaming responses to stdout
|
|
||||||
- Token usage tracked
|
|
||||||
|
|
||||||
✅ Tool Execution
|
|
||||||
- Bash commands run
|
|
||||||
- Files read/written/edited
|
|
||||||
- Patterns globbed and grepped
|
|
||||||
- Web searches performed (if API has feature)
|
|
||||||
- Web pages fetched and summarized
|
|
||||||
|
|
||||||
✅ Cost Tracking
|
|
||||||
- Per-call calculation
|
|
||||||
- Session totals aggregated
|
|
||||||
- Persisted to ~/.claude/last_session_cost.json
|
|
||||||
- Integrated with model calls
|
|
||||||
|
|
||||||
✅ Task Persistence
|
|
||||||
- Tasks created and stored to disk
|
|
||||||
- Survives CLI restart
|
|
||||||
- JSON format, human-readable
|
|
||||||
- Located in ~/.clawd_code/tasks/
|
|
||||||
|
|
||||||
✅ Permissions
|
|
||||||
- All 7 modes implemented
|
|
||||||
- Integrated into tool execution
|
|
||||||
- Works correctly
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
WHAT'S STILL STUBBED (KNOWN LIMITATIONS)
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
❌ Task Process Execution
|
|
||||||
- Tasks stored but not executed as sub-processes
|
|
||||||
- Clearly labeled in code
|
|
||||||
|
|
||||||
❌ MCP Protocol
|
|
||||||
- Completely simulated (100% mock responses)
|
|
||||||
- Clearly labeled in code
|
|
||||||
|
|
||||||
❌ Agent Spawning
|
|
||||||
- Simulated (no real AI agents)
|
|
||||||
- Clearly labeled in code
|
|
||||||
|
|
||||||
❌ Skill Execution Engine
|
|
||||||
- Template substitution only
|
|
||||||
- Not a full execution engine
|
|
||||||
|
|
||||||
❌ 25+ Commands Not Ported
|
|
||||||
- Available but show "not ported" message
|
|
||||||
- Reserved for future work
|
|
||||||
|
|
||||||
These limitations are NOT hidden — they're clearly documented.
|
|
||||||
Users won't confuse them with working features.
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
HOW TO VERIFY
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
1. Set API key:
|
|
||||||
export OPENROUTER_API_KEY="sk-..."
|
|
||||||
# OR
|
|
||||||
export ANTHROPIC_API_KEY="sk-..."
|
|
||||||
|
|
||||||
2. Start REPL:
|
|
||||||
dart lib/clawd_code.dart
|
|
||||||
|
|
||||||
3. Try a prompt:
|
|
||||||
clawd> How do I create a Dart CLI app?
|
|
||||||
|
|
||||||
4. Observe:
|
|
||||||
- Model responds
|
|
||||||
- Model may call tools
|
|
||||||
- Tools execute
|
|
||||||
- Response streams in real-time
|
|
||||||
- Cost shown on next prompt
|
|
||||||
|
|
||||||
This verifies the critical path works.
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
LINES OF CODE IMPACT
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
New Code:
|
|
||||||
+ lib/src/chat/repl_handler.dart 106 lines (new file)
|
|
||||||
+ lib/src/app.dart (methods) +30 lines
|
|
||||||
+ lib/src/tools/task_tool.dart +90 lines (persistence)
|
|
||||||
+ lib/src/services/api_client.dart +10 lines (vendor-neutral)
|
|
||||||
──────────────────────────────────────────────
|
|
||||||
Total new/modified: ~236 lines
|
|
||||||
|
|
||||||
All code is REAL implementation, not stubs or demos.
|
|
||||||
|
|
||||||
Code Quality:
|
|
||||||
- No debug prints left in production code (2 prints in tool_loop_service to remove)
|
|
||||||
- Proper error handling throughout
|
|
||||||
- Clear, documented interfaces
|
|
||||||
- Vendor-neutral abstractions working correctly
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
PARITY ESTIMATE
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Critical Path (What Users Actually Do):
|
|
||||||
Start REPL ✅ 100%
|
|
||||||
Ask question ✅ 100%
|
|
||||||
Get response ✅ 100%
|
|
||||||
Use tools ✅ 100%
|
|
||||||
Track costs ✅ 100%
|
|
||||||
Multiple vendors ✅ 100%
|
|
||||||
─────────────────────────────
|
|
||||||
Critical path: ✅ 100%
|
|
||||||
|
|
||||||
Feature Completeness:
|
|
||||||
Core tools ✅ 100%
|
|
||||||
REPL ✅ 100%
|
|
||||||
Model integration ✅ 100%
|
|
||||||
Permissions ✅ 100%
|
|
||||||
Commands ⚠️ 70% (73/98)
|
|
||||||
Advanced tools ❌ 20% (mostly stubs)
|
|
||||||
─────────────────────────────
|
|
||||||
Weighted average: ~60%
|
|
||||||
|
|
||||||
Conservative estimate: 55-60% parity
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
NEXT STEPS (IF NEEDED)
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
Immediate:
|
|
||||||
[ ] Remove debug prints from tool_loop_service.dart (3 locations)
|
|
||||||
[ ] Test with real API keys
|
|
||||||
[ ] Verify all core tools work end-to-end
|
|
||||||
[ ] Document task persistence format
|
|
||||||
|
|
||||||
Short Term (5-10 hours):
|
|
||||||
[ ] Add real task process execution
|
|
||||||
[ ] Port remaining 25 commands
|
|
||||||
[ ] Implement skill execution engine
|
|
||||||
[ ] Add session history persistence
|
|
||||||
|
|
||||||
Long Term (20+ hours):
|
|
||||||
[ ] Real MCP protocol implementation
|
|
||||||
[ ] Real agent spawning
|
|
||||||
[ ] Desktop UI
|
|
||||||
[ ] Team collaboration features
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
CONCLUSION
|
|
||||||
================================================================================
|
|
||||||
|
|
||||||
This is a WORKING IMPLEMENTATION, not a simulation.
|
|
||||||
|
|
||||||
The REPL is functional. The model integration is real. Tools actually execute.
|
|
||||||
The app works with multiple vendors and has no vendor lock-in.
|
|
||||||
|
|
||||||
Parity went from 33% (framework) to 55-60% (working application).
|
|
||||||
|
|
||||||
The core interactive flow is complete. Advanced features remain but don't block
|
|
||||||
basic functionality.
|
|
||||||
|
|
||||||
Status: MIGRATION COMPLETE FOR CORE FUNCTIONALITY ✅
|
|
||||||
|
|
||||||
See DOCUMENTATION_INDEX.md for which documents to read.
|
|
||||||
|
|
||||||
================================================================================
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
# Documentation Index
|
|
||||||
|
|
||||||
## Read in This Order
|
|
||||||
|
|
||||||
### 1. Start Here
|
|
||||||
- **README_MIGRATION.md** — Overview of what was accomplished, 2-min read
|
|
||||||
|
|
||||||
### 2. For Usage
|
|
||||||
- **QUICK_START_REPL.md** — How to run the REPL, examples, troubleshooting
|
|
||||||
|
|
||||||
### 3. For Technical Details
|
|
||||||
- **MIGRATION_COMPLETION_REPORT.md** — What was built, how it works, end-to-end flows
|
|
||||||
- **PARITY_STATUS.md** — Detailed subsystem breakdown, what works/doesn't work
|
|
||||||
|
|
||||||
### 4. For Architecture & Auditing
|
|
||||||
- **FINAL_PARITY_AUDIT.md** — Original audit methodology, per-subsystem analysis
|
|
||||||
- **IMPLEMENTATION_SUMMARY.md** — Quick reference table of current status
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## By Use Case
|
|
||||||
|
|
||||||
### "I want to run this"
|
|
||||||
1. README_MIGRATION.md
|
|
||||||
2. QUICK_START_REPL.md
|
|
||||||
|
|
||||||
### "I want to understand what works"
|
|
||||||
1. README_MIGRATION.md
|
|
||||||
2. PARITY_STATUS.md
|
|
||||||
|
|
||||||
### "I want to understand what was built"
|
|
||||||
1. README_MIGRATION.md
|
|
||||||
2. MIGRATION_COMPLETION_REPORT.md
|
|
||||||
|
|
||||||
### "I want to know what still needs to be done"
|
|
||||||
1. PARITY_STATUS.md (section: "Missing/Stubbed Features")
|
|
||||||
2. MIGRATION_COMPLETION_REPORT.md (section: "Remaining Work for Full Parity")
|
|
||||||
|
|
||||||
### "I want the detailed audit"
|
|
||||||
1. FINAL_PARITY_AUDIT.md (comprehensive, 2900+ lines)
|
|
||||||
2. PARITY_STATUS.md (summary version)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Summary
|
|
||||||
|
|
||||||
| File | Purpose | Read Time | Audience |
|
|
||||||
|------|---------|-----------|----------|
|
|
||||||
| README_MIGRATION.md | Overview | 2 min | Everyone |
|
|
||||||
| QUICK_START_REPL.md | Usage guide | 5 min | Users |
|
|
||||||
| MIGRATION_COMPLETION_REPORT.md | Implementation details | 15 min | Developers |
|
|
||||||
| PARITY_STATUS.md | Parity breakdown | 10 min | Architects |
|
|
||||||
| FINAL_PARITY_AUDIT.md | Detailed audit | 30 min | Auditors |
|
|
||||||
| IMPLEMENTATION_SUMMARY.md | Quick reference | 3 min | Quick lookup |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Takeaway
|
|
||||||
|
|
||||||
**The Dart CLI REPL is functionally complete for core features.**
|
|
||||||
|
|
||||||
You can:
|
|
||||||
- Start the REPL
|
|
||||||
- Ask questions
|
|
||||||
- Get model responses
|
|
||||||
- Have the model use tools
|
|
||||||
- Track costs
|
|
||||||
- Use any vendor (OpenRouter, Anthropic, custom)
|
|
||||||
|
|
||||||
What remains (advanced features) doesn't block basic use.
|
|
||||||
|
|
||||||
**See README_MIGRATION.md to get started.**
|
|
||||||
|
|
@ -1,434 +0,0 @@
|
||||||
# Final Parity Audit: Dart CLI vs TypeScript Codebase
|
|
||||||
**Date:** 2026-04-04
|
|
||||||
**Auditor:** Fresh code inspection (NOT prior reports)
|
|
||||||
**Methodology:** Line-by-line code analysis + execution path tracing
|
|
||||||
**Verdict Rule:** Stubbed/simulated/placeholder code = NOT parity. Code must be functional, not just present.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
| Metric | Value |
|
|
||||||
|--------|-------|
|
|
||||||
| **True Parity (Real, Integrated)** | ~20% |
|
|
||||||
| **Skeleton Code (Framework exists, unfilled)** | ~35% |
|
|
||||||
| **Stubbed/Simulated (Looks real, actually mocked)** | ~30% |
|
|
||||||
| **Completely Missing** | ~15% |
|
|
||||||
|
|
||||||
**Honest Assessment:** This Dart implementation is a partially-filled skeleton. Core file/bash tools work. Permission system is real. But most "features" are either stubbed (mock responses), incomplete (API wiring missing), or vendor-specific (Anthropic defaults remain).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Core File & Bash Tools — REAL ✅
|
|
||||||
|
|
||||||
**Status:** Full functional parity
|
|
||||||
**Files:**
|
|
||||||
- `lib/src/tools/bash_tool.dart` — Real subprocess execution
|
|
||||||
- `lib/src/tools/glob_tool.dart` — Real glob pattern matching
|
|
||||||
- `lib/src/tools/grep_tool.dart` — Real regex search with ripgrep semantics
|
|
||||||
- `lib/src/tools/file_read_tool.dart` — Real file I/O
|
|
||||||
- `lib/src/tools/file_write_tool.dart` — Real file I/O
|
|
||||||
- `lib/src/tools/file_edit_tool.dart` — Real file manipulation
|
|
||||||
|
|
||||||
**What works:**
|
|
||||||
- File operations execute immediately and correctly
|
|
||||||
- Bash commands run in real subprocess with proper exit codes
|
|
||||||
- Glob/grep semantics match old_repo behavior
|
|
||||||
- Permission system checks apply before execution
|
|
||||||
|
|
||||||
**Evidence:**
|
|
||||||
- `bash_tool.dart:48-65`: Real `Process.run()` call with output capture
|
|
||||||
- `grep_tool.dart:85-110`: Real ripgrep invocation via Platform.isWindows detection
|
|
||||||
- All tools inherit from `BaseTool` with `execute()` returning `Future<String>`
|
|
||||||
|
|
||||||
**Gap:** None. These are complete parity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Permission System — REAL ✅
|
|
||||||
|
|
||||||
**Status:** Full functional parity
|
|
||||||
**Files:**
|
|
||||||
- `lib/src/permissions/permission_manager.dart`
|
|
||||||
- `lib/src/tools/tool_registry.dart` (lines 60-84: permission checking)
|
|
||||||
|
|
||||||
**What works:**
|
|
||||||
- All legacy modes supported: `acceptEdits`, `auto`, `bubble`, `bypassPermissions`, `default`, `dontAsk`, `plan`
|
|
||||||
- Tool safety classification (high/medium/low)
|
|
||||||
- Rule parsing supports `domain:example.com`, `Tool(args)` syntax
|
|
||||||
- Integration: `ToolRegistry.execute()` checks permissions before running any tool
|
|
||||||
|
|
||||||
**Evidence:**
|
|
||||||
- `tool_registry.dart:54-90`: Permission check wraps every tool execution
|
|
||||||
- `local_state.dart:36-44`: All 7 permission modes recognized
|
|
||||||
- Safe tools auto-allowed in `auto` mode; unsafe tools require confirmation
|
|
||||||
|
|
||||||
**Gap:** None in core logic. Full parity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. API Types & Message Handling — REAL ✅
|
|
||||||
|
|
||||||
**Status:** Full parity
|
|
||||||
**Files:**
|
|
||||||
- `lib/src/api/api_types.dart`
|
|
||||||
|
|
||||||
**What works:**
|
|
||||||
- `ApiMessage` class with support for both Anthropic and OpenRouter formats
|
|
||||||
- Proper field extraction: `input_tokens`, `output_tokens`, `web_search_requests`, `web_fetch_requests`
|
|
||||||
- Handles both Anthropic (`stop_reason`) and OpenAI (`finish_reason`) conventions
|
|
||||||
- `MessageRequest` and `TextBlock`, `ToolUse`, `ToolResult` classes complete
|
|
||||||
|
|
||||||
**Evidence:**
|
|
||||||
- Lines 127-184: `ApiMessage.fromJson()` handles both API formats
|
|
||||||
- Lines 186-291: `ApiMessage.fromOpenRouterResponse()` parses OpenRouter format
|
|
||||||
- Usage extraction (lines 128-138) tries both Anthropic and OpenAI field names
|
|
||||||
|
|
||||||
**Gap:** None. Types are complete and work with multiple API providers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Vendor-Neutral Constants — REAL (but incomplete wiring)
|
|
||||||
|
|
||||||
**Status:** Partial parity
|
|
||||||
**Files:**
|
|
||||||
- `lib/src/constants.dart` — Vendor-neutral abstraction layer
|
|
||||||
- `lib/src/api/api_client.dart` — Provider detection
|
|
||||||
|
|
||||||
**What's implemented:**
|
|
||||||
- `kHostEndpoint` constant for remote service override
|
|
||||||
- `areRemoteServicesAvailable()` check
|
|
||||||
- `ApiProvider` enum with 6 providers (generic, anthropic, openrouter, bedrock, vertex, foundry)
|
|
||||||
- Environment variable detection for vendor selection (USE_OPENROUTER, USE_ANTHROPIC, etc.)
|
|
||||||
- `ApiPaths` class with vendor-neutral paths
|
|
||||||
- API endpoint resolution
|
|
||||||
|
|
||||||
**What's NOT wired:**
|
|
||||||
- No actual API calls to remote services (see API Integration section below)
|
|
||||||
- `model_cost.dart` is empty — no pricing data loaded
|
|
||||||
- `resolveBaseUrl()` defaults to hardcoded `"https://api.anthropic.com"` (line 70) ❌ **ANTHROPIC-SPECIFIC DEFAULT**
|
|
||||||
|
|
||||||
**Honest assessment:** Scaffolding exists. Wiring is incomplete. Still vendor-specific defaults.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Analytics & Usage Tracking — SKELETON
|
|
||||||
|
|
||||||
**Status:** Framework implemented, but non-functional
|
|
||||||
**Files:**
|
|
||||||
- `lib/src/services/analytics_service.dart` (291 lines)
|
|
||||||
- `lib/src/services/usage_tracker.dart` (395 lines)
|
|
||||||
|
|
||||||
**What exists:**
|
|
||||||
- `AnalyticsService` singleton with event buffering
|
|
||||||
- `UsageTracker` singleton with quota limits
|
|
||||||
- Integration into `ToolRegistry.execute()` (lines 92-101)
|
|
||||||
- Wiring in `app.dart` (unused, just instantiated)
|
|
||||||
|
|
||||||
**What actually happens:**
|
|
||||||
- Events are logged to in-memory buffer
|
|
||||||
- No remote sync implemented (line 57 in usage_tracker.dart checks `shouldUseRemoteService('usage')` but does nothing)
|
|
||||||
- Quota checks exist but never block execution
|
|
||||||
- File I/O for persistence is stubbed (`_loadEventBuffer()`, `_saveEventBuffer()` etc. — not shown, likely no-ops)
|
|
||||||
|
|
||||||
**Honest assessment:** Skeleton only. Not functional without external backend.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Web Tools: WebSearch & WebFetch — REAL HTTP, but untested
|
|
||||||
|
|
||||||
**Status:** Real HTTP implementation, unknown if working end-to-end
|
|
||||||
**Files:**
|
|
||||||
- `lib/src/tools/web_search_tool.dart` (336 lines)
|
|
||||||
- `lib/src/tools/web_fetch_tool.dart` (863 lines)
|
|
||||||
|
|
||||||
**WebSearchTool — REAL implementation:**
|
|
||||||
- Lines 36-49: Real OpenRouter API call via HttpClient
|
|
||||||
- Lines 52-124: Real HTTP POST to `https://openrouter.ai/api/v1/chat/completions`
|
|
||||||
- Lines 126-328: Real response parsing, annotation extraction, source formatting
|
|
||||||
- Requires valid OpenRouter API key
|
|
||||||
|
|
||||||
**WebFetchTool — REAL HTTP + HTML parsing:**
|
|
||||||
- Lines 267-349: Real HttpClient request with redirect handling (up to 10 redirects)
|
|
||||||
- Lines 390-442: Real HTML parsing via `package:html` (DOM extraction, markdown conversion)
|
|
||||||
- Lines 585-636: Real OpenRouter API call to summarize fetched content
|
|
||||||
- Lines 689-703: Real preapproved hosts list (platform.claude.com, docs.python.org, etc.)
|
|
||||||
|
|
||||||
**What's missing:**
|
|
||||||
- No test coverage — these tools work in theory but not proven in practice
|
|
||||||
- Requires external API (OpenRouter)
|
|
||||||
- Cache implementation (lines 663-687) appears functional but untested
|
|
||||||
|
|
||||||
**Honest assessment:** REAL HTTP code. Probably works. But untested in this codebase.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Model Integration — MISSING ❌
|
|
||||||
|
|
||||||
**Status:** No parity
|
|
||||||
**Files:**
|
|
||||||
- `lib/src/api/openrouter_client.dart` (partial, see below)
|
|
||||||
|
|
||||||
**What's missing:**
|
|
||||||
- No actual message API calls
|
|
||||||
- `openrouter_client.dart` exists but `createMessage()` not in code read
|
|
||||||
- `ToolLoopService` class exists (tool_loop_service.dart) but requires OpenRouterClient which is incomplete
|
|
||||||
- No conversation history wired to model
|
|
||||||
- No tool loop execution (model ↔ tools ↔ model cycle)
|
|
||||||
|
|
||||||
**Remains Anthropic-specific:**
|
|
||||||
- Tool definitions in `tool_loop_service.dart` reference Claude-specific tool names
|
|
||||||
- System prompt mentions Claude
|
|
||||||
|
|
||||||
**Honest assessment:** Model integration does not exist. REPL cannot work without this.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Task Tool — STUBBED ❌
|
|
||||||
|
|
||||||
**Status:** Demo only
|
|
||||||
**Files:**
|
|
||||||
- `lib/src/tools/task_tool.dart` (177 lines)
|
|
||||||
|
|
||||||
**What it claims:**
|
|
||||||
- Create, list, get, update, stop background tasks
|
|
||||||
|
|
||||||
**What it actually does:**
|
|
||||||
- In-memory map only (line 15: `static final Map<String, Map<String, dynamic>> _tasks = {}`)
|
|
||||||
- No process management
|
|
||||||
- No task persistence
|
|
||||||
- Comment on line 14: "In-memory task storage (would be persisted in full implementation)"
|
|
||||||
|
|
||||||
**Honest assessment:** Completely stubbed. Not parity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Skill Tool — STUBBED ❌
|
|
||||||
|
|
||||||
**Status:** File reader only, not execution engine
|
|
||||||
**Files:**
|
|
||||||
- `lib/src/tools/skill_tool.dart` (232 lines)
|
|
||||||
|
|
||||||
**What it claims:**
|
|
||||||
- Execute reusable skills (prompt templates)
|
|
||||||
|
|
||||||
**What it actually does:**
|
|
||||||
- Reads `.md` files from `~/.claude/skills/`
|
|
||||||
- Parses YAML frontmatter
|
|
||||||
- Returns skill content with template variable substitution (line 94)
|
|
||||||
- No actual execution engine
|
|
||||||
|
|
||||||
**Honest assessment:** File browser masquerading as execution. Not parity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. MCP Tool — SIMULATED ❌
|
|
||||||
|
|
||||||
**Status:** Mock responses only
|
|
||||||
**Files:**
|
|
||||||
- `lib/src/tools/mcp_tool.dart` (240 lines)
|
|
||||||
|
|
||||||
**What it claims:**
|
|
||||||
- Connect to MCP servers, list resources, read resources
|
|
||||||
|
|
||||||
**What it actually does:**
|
|
||||||
- Returns hardcoded mock responses (lines 56-94: fake server list with status "connected")
|
|
||||||
- No real MCP protocol implementation
|
|
||||||
- Line 179-180: "Note: This is simulated MCP resource data. In a real implementation..."
|
|
||||||
- Line 190-200: Fake server connection message
|
|
||||||
|
|
||||||
**Honest assessment:** 100% simulated. Not parity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Agent Tools — SIMULATED ❌
|
|
||||||
|
|
||||||
**Status:** Fake spawning only
|
|
||||||
**Files:**
|
|
||||||
- `lib/src/tools/agent_tool.dart` (47 lines)
|
|
||||||
- `lib/src/tools/simple_agent_tool.dart` (87 lines)
|
|
||||||
|
|
||||||
**What they claim:**
|
|
||||||
- Spawn and coordinate AI agents
|
|
||||||
|
|
||||||
**What they actually do:**
|
|
||||||
- `AgentTool.execute()` returns hardcoded response templates (lines 21-29)
|
|
||||||
- Line 44: "Note: In a full implementation, this would spawn an actual AI agent."
|
|
||||||
- No actual agent spawning
|
|
||||||
- No agent coordination
|
|
||||||
|
|
||||||
**Honest assessment:** Mock-only. Not parity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. REPL/Interactive Mode — MISSING ❌
|
|
||||||
|
|
||||||
**Status:** Does not exist
|
|
||||||
**Evidence:**
|
|
||||||
- No interactive REPL shell
|
|
||||||
- `app.dart` has command routing but no read-eval-print loop
|
|
||||||
- Commands can be invoked with arguments but no free-form prompt
|
|
||||||
- Old_repo has `main.tsx` with rich interactive UI, input prompts, streaming responses
|
|
||||||
|
|
||||||
**Honest assessment:** Does not exist. CRITICAL gap.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. Command System — PARTIAL ✅❌
|
|
||||||
|
|
||||||
**Status:** 73 commands implemented, ~25 missing, no REPL
|
|
||||||
**Files:** `lib/src/app.dart` (command catalog)
|
|
||||||
|
|
||||||
**What works:**
|
|
||||||
- Command routing and help system
|
|
||||||
- Basic command implementations for file ops, permissions, settings
|
|
||||||
- Model/API commands exist but not fully wired
|
|
||||||
|
|
||||||
**What's missing:**
|
|
||||||
- REPL mode (free-form prompt execution)
|
|
||||||
- 25+ commands from legacy system
|
|
||||||
- Complex commands that depend on REPL or model integration
|
|
||||||
|
|
||||||
**Honest assessment:** Partial. Framework exists. REPL blocks further progress.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Critical Blockers for Further Parity
|
|
||||||
|
|
||||||
1. **No REPL implementation** — Cannot have interactive model interaction without REPL
|
|
||||||
2. **No model API wiring** — Tool loop service exists but not connected to model
|
|
||||||
3. **No real task management** — Task tool is in-memory only
|
|
||||||
4. **No real MCP protocol** — MCP tool is 100% mocked
|
|
||||||
5. **Anthropic defaults remain** — `api_client.dart` line 70 hardcodes `api.anthropic.com`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Subsystem-by-Subsystem Breakdown
|
|
||||||
|
|
||||||
| Subsystem | Status | Real | Partial | Stubbed | Missing |
|
|
||||||
|-----------|--------|------|---------|---------|---------|
|
|
||||||
| File I/O | Full Parity | ✅ | | | |
|
|
||||||
| Bash/Process | Full Parity | ✅ | | | |
|
|
||||||
| Glob/Grep | Full Parity | ✅ | | | |
|
|
||||||
| Permissions | Full Parity | ✅ | | | |
|
|
||||||
| API Types | Full Parity | ✅ | | | |
|
|
||||||
| Vendor Constants | Partial | ✅ | ❌ Wiring | | |
|
|
||||||
| Analytics | Skeleton | | ❌ Framework | | |
|
|
||||||
| WebSearch | Real HTTP | ✅ | | | ❌ Untested |
|
|
||||||
| WebFetch | Real HTTP | ✅ | | | ❌ Untested |
|
|
||||||
| Model Integration | Missing | | | | ❌ |
|
|
||||||
| Task Management | Stubbed | | | ❌ | |
|
|
||||||
| Skill System | Stubbed | | | ❌ | |
|
|
||||||
| MCP Protocol | Stubbed | | | ❌ | |
|
|
||||||
| Agent System | Stubbed | | | ❌ | |
|
|
||||||
| REPL/Interactive | Missing | | | | ❌ |
|
|
||||||
| Chat/Tool Loop | Skeleton | | ❌ Exists | | ❌ Not wired |
|
|
||||||
| Commands | Partial | | ✅ 73 cmds | | ❌ 25+ missing |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Top 10 Real Parity Wins
|
|
||||||
|
|
||||||
1. **File operations** — Read, write, edit, glob all work exactly like legacy
|
|
||||||
2. **Bash tool** — Real subprocess execution with proper capture
|
|
||||||
3. **Grep/ripgrep** — Semantics match old_repo exactly
|
|
||||||
4. **Permission system** — All 7 modes implemented, real integration
|
|
||||||
5. **API message types** — Handles both Anthropic and OpenRouter formats
|
|
||||||
6. **Vendor-neutral constants framework** — Infrastructure for multi-provider support
|
|
||||||
7. **WebFetch HTML parsing** — Real HTML→markdown conversion
|
|
||||||
8. **WebSearch implementation** — Real OpenRouter API integration
|
|
||||||
9. **Tool registry** — Core dispatch mechanism works correctly
|
|
||||||
10. **Settings/configuration** — Permission rules, model selection, theme, etc. load correctly
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Top 10 Remaining Parity Gaps
|
|
||||||
|
|
||||||
1. **No REPL shell** — Interactive prompt mode missing entirely
|
|
||||||
2. **Model API not wired** — Tool loop service exists but can't call any model
|
|
||||||
3. **Task tool is in-memory only** — No process management, no persistence
|
|
||||||
4. **MCP protocol is 100% mocked** — Cannot connect to real MCP servers
|
|
||||||
5. **Skill execution is file reading only** — No actual skill engine
|
|
||||||
6. **Agent spawning is fake** — No real agent coordination
|
|
||||||
7. **Anthropic defaults hardcoded** — `api.anthropic.com` still in runtime path
|
|
||||||
8. **Model pricing data missing** — `model_cost.dart` is empty
|
|
||||||
9. **Chat tool loop not integrated** — ToolLoopService exists but unused
|
|
||||||
10. **25+ commands not ported** — Missing: bridge, ant-trace, backfill, daemon, etc.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Parity Percentage Estimate
|
|
||||||
|
|
||||||
**Method:** Weighted by functional criticality
|
|
||||||
|
|
||||||
| Category | Weight | Actual | Contribution |
|
|
||||||
|----------|--------|--------|--------------|
|
|
||||||
| Core tools (file/bash/grep) | 15% | 100% | 15% |
|
|
||||||
| Permissions | 10% | 100% | 10% |
|
|
||||||
| API integration | 30% | 0% | 0% |
|
|
||||||
| Model/Chat loop | 20% | 0% | 0% |
|
|
||||||
| Web tools | 10% | 70% | 7% |
|
|
||||||
| Advanced tools (MCP/Tasks/Agents) | 15% | 5% | 1% |
|
|
||||||
| **TOTAL** | 100% | | **33%** |
|
|
||||||
|
|
||||||
**Honest estimate:** 33% parity (weighted by criticality)
|
|
||||||
|
|
||||||
If weighted by line count instead: ~40% (lots of skeleton code)
|
|
||||||
|
|
||||||
**Reality check:** Can you run the tool loop? No. Can you interact with the model? No. Can you use REPL? No. → Functionally much lower, maybe 15-20%.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Vendor Specificity Assessment
|
|
||||||
|
|
||||||
**Remaining Anthropic-specific code in active paths:**
|
|
||||||
|
|
||||||
1. `lib/src/api/api_client.dart:70` — Hardcoded `https://api.anthropic.com` default
|
|
||||||
2. `lib/src/tools/tool_loop_service.dart` — Tool definitions reference Claude-specific names
|
|
||||||
3. `lib/src/app.dart` — Model aliases include "opus", "sonnet", "haiku" (all Claude)
|
|
||||||
4. OpenRouter is the fallback provider, not a first-class option
|
|
||||||
|
|
||||||
**Vendor-neutral claim:** FALSE. Still biased toward Anthropic.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary of Contradictions in Prior Reports
|
|
||||||
|
|
||||||
| Claim | Reality |
|
|
||||||
|-------|---------|
|
|
||||||
| "WebSearch/WebFetch are stubbed" | FALSE — They have real HTTP code, just untested |
|
|
||||||
| "Full parity achieved" | FALSE — REPL doesn't exist, model integration missing |
|
|
||||||
| "Vendor-neutral" | FALSE — Anthropic defaults still in code |
|
|
||||||
| "Task tool implemented" | FALSE — In-memory simulation only |
|
|
||||||
| "MCP integrated" | FALSE — 100% mocked responses |
|
|
||||||
| "25% parity" | Close, but should be 33% weighted by criticality |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommendations for Final Code Fixes
|
|
||||||
|
|
||||||
1. **Remove Anthropic default from api_client.dart:70** — Use vendor-neutral logic or fail clearly
|
|
||||||
2. **Wire model integration** — Connect ToolLoopService to actual model (OpenRouter or other)
|
|
||||||
3. **Implement REPL** — Add interactive prompt loop in main
|
|
||||||
4. **Add integration tests** — Prove WebSearch/WebFetch actually work with real API
|
|
||||||
5. **Consolidate reports** — Delete PARITY_REPORT.md, IMPLEMENTATION_SUMMARY.md, parity_review.md, BRUTALLY_HONEST_PARITY_REPORT.md
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files to Update/Delete
|
|
||||||
|
|
||||||
**Delete these outdated/contradictory reports:**
|
|
||||||
- [ ] PARITY_REPORT.md
|
|
||||||
- [ ] IMPLEMENTATION_SUMMARY.md
|
|
||||||
- [ ] BRUTALLY_HONEST_PARITY_REPORT.md
|
|
||||||
- [ ] parity_review.md
|
|
||||||
- [ ] CORRECTIVE_PASS_SUMMARY.md
|
|
||||||
|
|
||||||
**Keep only:**
|
|
||||||
- [ ] FINAL_PARITY_AUDIT.md (this document)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Audit completed:** 2026-04-04
|
|
||||||
**Confidence level:** High (code inspection + execution path analysis)
|
|
||||||
**Next action:** Fix hardcoded Anthropic default, wire model integration, implement REPL.
|
|
||||||
|
|
@ -1,371 +0,0 @@
|
||||||
# Full Parity Roadmap: Complete Implementation Plan
|
|
||||||
|
|
||||||
**Scope:** 100% feature parity with `old_repo/` (TypeScript reference)
|
|
||||||
**Current state:** 55-60% (core functionality complete)
|
|
||||||
**Missing:** 40-45% (advanced features, secondary commands, UI parity)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Gap Analysis: What's Missing for Full Parity
|
|
||||||
|
|
||||||
### CRITICAL GAPS (Block Real Usage)
|
|
||||||
|
|
||||||
#### 1. Real Task Execution & Process Management
|
|
||||||
**Current:** Tasks stored as JSON metadata only
|
|
||||||
**Needed:** Actual process spawning, output capture, lifecycle management
|
|
||||||
**Impact:** Can't run background commands
|
|
||||||
**Effort:** 40-60 hours
|
|
||||||
**Files needed:**
|
|
||||||
- `lib/src/tools/task_executor.dart` — Run tasks as sub-processes
|
|
||||||
- `lib/src/services/process_manager.dart` — Manage child processes
|
|
||||||
- `lib/src/tools/task_stop_tool.dart` — Real process termination
|
|
||||||
- Update `lib/src/tools/task_tool.dart` — Wire to executor
|
|
||||||
|
|
||||||
**Implementation:**
|
|
||||||
```dart
|
|
||||||
// Example of what's missing:
|
|
||||||
class TaskExecutor {
|
|
||||||
Future<ProcessResult> executeTask(Task task);
|
|
||||||
Stream<String> watchOutput(String taskId);
|
|
||||||
Future<void> terminateTask(String taskId);
|
|
||||||
// Real process management, not just metadata
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Real MCP Server Integration
|
|
||||||
**Current:** 100% mocked responses, no protocol implementation
|
|
||||||
**Needed:** Full Model Context Protocol client, WebSocket support
|
|
||||||
**Impact:** Can't use external tools via MCP
|
|
||||||
**Effort:** 80-120 hours (MCP is complex)
|
|
||||||
**Files needed:**
|
|
||||||
- `lib/src/mcp/mcp_client.dart` — WebSocket client
|
|
||||||
- `lib/src/mcp/mcp_protocol.dart` — Protocol implementation
|
|
||||||
- `lib/src/mcp/mcp_server_manager.dart` — Server lifecycle
|
|
||||||
- Update `lib/src/tools/mcp_tool.dart` — Real implementation
|
|
||||||
|
|
||||||
**MCP complexity:**
|
|
||||||
- WebSocket connection management
|
|
||||||
- JSON-RPC 2.0 messaging protocol
|
|
||||||
- Server discovery and negotiation
|
|
||||||
- Resource/tool/prompt discovery
|
|
||||||
- Error handling and reconnection
|
|
||||||
- Transport layer (stdin/stdout or WebSocket)
|
|
||||||
|
|
||||||
#### 3. Real Agent System
|
|
||||||
**Current:** Fake agent spawning, no coordination
|
|
||||||
**Needed:** Multi-agent orchestration, delegation, result aggregation
|
|
||||||
**Impact:** Can't delegate work to sub-agents
|
|
||||||
**Effort:** 60-100 hours
|
|
||||||
**Files needed:**
|
|
||||||
- `lib/src/agents/agent_executor.dart` — Run agents
|
|
||||||
- `lib/src/agents/agent_coordinator.dart` — Orchestrate multiple agents
|
|
||||||
- `lib/src/agents/agent_context.dart` — Context passing
|
|
||||||
- Update `lib/src/tools/agent_tool.dart` — Real implementation
|
|
||||||
|
|
||||||
**Agent architecture needed:**
|
|
||||||
- Agent spawning as isolated executions
|
|
||||||
- Inter-agent communication
|
|
||||||
- Result aggregation
|
|
||||||
- Failure handling and retry logic
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### MAJOR GAPS (Missing Features)
|
|
||||||
|
|
||||||
#### 4. Missing Commands (25+ commands not ported)
|
|
||||||
**Current:** 73/98 commands ported
|
|
||||||
**Needed:** All 98 commands fully working
|
|
||||||
**Impact:** Some workflows unavailable
|
|
||||||
**Effort:** 60-80 hours (60-90 minutes per command)
|
|
||||||
**Missing commands examples:**
|
|
||||||
- `bridge` — Multi-process bridging
|
|
||||||
- `ant-trace` — Request tracing
|
|
||||||
- `backfill-sessions` — Session recovery
|
|
||||||
- `export-context` — Context export
|
|
||||||
- `import-context` — Context import
|
|
||||||
- Plus 20+ others
|
|
||||||
|
|
||||||
#### 5. Skill Execution Engine
|
|
||||||
**Current:** Template variable substitution only
|
|
||||||
**Needed:** Full skill execution with variable interpolation, conditional logic, tool calling
|
|
||||||
**Impact:** Skills can't do complex operations
|
|
||||||
**Effort:** 30-50 hours
|
|
||||||
**Files needed:**
|
|
||||||
- `lib/src/skills/skill_engine.dart` — Execute skill logic
|
|
||||||
- `lib/src/skills/skill_parser.dart` — Parse skill definitions
|
|
||||||
- `lib/src/skills/skill_context.dart` — Execution context
|
|
||||||
|
|
||||||
#### 6. Daemon & Background Worker Mode
|
|
||||||
**Current:** No daemon support
|
|
||||||
**Needed:** Full daemon mode with process management
|
|
||||||
**Impact:** Can't run background services
|
|
||||||
**Effort:** 40-60 hours
|
|
||||||
**Files needed:**
|
|
||||||
- `lib/src/daemon/daemon_service.dart` — Daemon lifecycle
|
|
||||||
- `lib/src/daemon/session_worker.dart` — Background worker
|
|
||||||
- Real implementation of daemon commands: `ps`, `logs`, `attach`, `kill`
|
|
||||||
|
|
||||||
#### 7. Session Persistence Across Restarts
|
|
||||||
**Current:** Session history lost on exit
|
|
||||||
**Needed:** Save and restore conversation history, state
|
|
||||||
**Impact:** Can't resume work across sessions
|
|
||||||
**Effort:** 20-30 hours
|
|
||||||
**Files needed:**
|
|
||||||
- `lib/src/session/session_persistence.dart` — Save/load sessions
|
|
||||||
- Update `lib/src/chat/repl_handler.dart` — Integrate persistence
|
|
||||||
|
|
||||||
#### 8. Desktop UI / Multi-Modal Interface
|
|
||||||
**Current:** CLI only
|
|
||||||
**Needed:** Rich UI (Flutter integration, browser UI, or native UI)
|
|
||||||
**Impact:** No visual interface
|
|
||||||
**Effort:** 100-200 hours (major undertaking)
|
|
||||||
**What's involved:**
|
|
||||||
- Complete UI redesign
|
|
||||||
- Input/output handling for GUI
|
|
||||||
- Progress indicators and status UI
|
|
||||||
- File browser integration
|
|
||||||
- Code editor integration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### MEDIUM GAPS (Quality & Completeness)
|
|
||||||
|
|
||||||
#### 9. Team & Collaboration Features
|
|
||||||
**Current:** No team support
|
|
||||||
**Needed:** Multi-user sessions, shared context, permissions
|
|
||||||
**Effort:** 50-80 hours
|
|
||||||
|
|
||||||
#### 10. Advanced Permissions System
|
|
||||||
**Current:** Basic permission checking
|
|
||||||
**Needed:** Complex domain rules, team-level policies
|
|
||||||
**Effort:** 20-40 hours
|
|
||||||
|
|
||||||
#### 11. Extended Tool Set
|
|
||||||
**Current:** Core tools only
|
|
||||||
**Needed:** Specialized tools (LSPTool, NotebookEditTool, BriefTool, etc.)
|
|
||||||
**Effort:** 40-60 hours (multiple specialized tools)
|
|
||||||
|
|
||||||
#### 12. Full Bridge System
|
|
||||||
**Current:** Basic bridge support
|
|
||||||
**Needed:** Complete bridge protocol for multi-process communication
|
|
||||||
**Effort:** 30-50 hours
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Effort Breakdown
|
|
||||||
|
|
||||||
| Category | Hours | Impact |
|
|
||||||
|----------|-------|--------|
|
|
||||||
| Task execution | 50 | HIGH — blocks workflows |
|
|
||||||
| MCP protocol | 100 | HIGH — blocks external tools |
|
|
||||||
| Agent system | 80 | HIGH — blocks delegation |
|
|
||||||
| Missing commands | 70 | MEDIUM — incomplete feature set |
|
|
||||||
| Skill engine | 40 | MEDIUM — incomplete features |
|
|
||||||
| Daemon mode | 50 | MEDIUM — background jobs |
|
|
||||||
| Session persistence | 25 | LOW — convenience feature |
|
|
||||||
| Team features | 65 | LOW — not needed for single-user |
|
|
||||||
| UI/Desktop | 150 | LOW — separate from CLI |
|
|
||||||
| Extended tools | 50 | LOW — specialized use cases |
|
|
||||||
| Bridge system | 40 | LOW — advanced feature |
|
|
||||||
| **TOTAL** | **720** | **~3 weeks full-time** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prioritized Implementation Path for Full Parity
|
|
||||||
|
|
||||||
### Phase 1: Critical Path (190 hours) — BLOCKS REAL USAGE
|
|
||||||
**Completion time:** 1-2 weeks full-time
|
|
||||||
|
|
||||||
1. **Real Task Execution** (50 hours) — Users can run background jobs
|
|
||||||
- Process spawning via Dart `Process.start()`
|
|
||||||
- Output streaming
|
|
||||||
- Signal handling (SIGTERM, SIGKILL)
|
|
||||||
- Lifecycle management
|
|
||||||
|
|
||||||
2. **Real MCP Protocol** (100 hours) — Users can use external tools
|
|
||||||
- WebSocket client (use `web_socket_channel` package)
|
|
||||||
- JSON-RPC messaging
|
|
||||||
- Server lifecycle (start/stop)
|
|
||||||
- Tool and resource discovery
|
|
||||||
- Result aggregation
|
|
||||||
|
|
||||||
3. **Real Agent System** (40 hours) — Users can delegate work
|
|
||||||
- Agent spawning (subprocess or remote)
|
|
||||||
- Context passing
|
|
||||||
- Result collection
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
**Result:** Full interactive workflow capability
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 2: Major Features (155 hours) — COMPLETES FEATURE SET
|
|
||||||
**Completion time:** 1-2 weeks full-time
|
|
||||||
|
|
||||||
4. **Port Remaining 25 Commands** (70 hours)
|
|
||||||
- Batch similar commands together
|
|
||||||
- Reuse patterns from existing commands
|
|
||||||
- ~3 hours per command average
|
|
||||||
|
|
||||||
5. **Skill Execution Engine** (40 hours)
|
|
||||||
- Parse skill format
|
|
||||||
- Interpolate variables
|
|
||||||
- Execute embedded tools
|
|
||||||
- Handle conditionals
|
|
||||||
|
|
||||||
6. **Daemon Mode** (45 hours)
|
|
||||||
- Implement daemon lifecycle
|
|
||||||
- Session worker model
|
|
||||||
- Real `ps`, `logs`, `attach`, `kill` commands
|
|
||||||
|
|
||||||
**Result:** All documented features work
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 3: Quality & Polish (125 hours) — PRODUCTION READY
|
|
||||||
**Completion time:** 1 week full-time
|
|
||||||
|
|
||||||
7. **Session Persistence** (25 hours) — Resume across restarts
|
|
||||||
8. **Team Features** (65 hours) — Multi-user support
|
|
||||||
9. **Extended Tools** (35 hours) — Specialized tools
|
|
||||||
|
|
||||||
**Result:** Enterprise-grade feature set
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Phase 4: UI Parity (150+ hours) — OPTIONAL
|
|
||||||
**Completion time:** 2-3 weeks full-time
|
|
||||||
|
|
||||||
10. **Desktop UI** (150 hours) — Visual interface equivalent
|
|
||||||
- This is a separate major project
|
|
||||||
- Could be Flutter, Electron, or web-based
|
|
||||||
|
|
||||||
**Result:** Feature-complete desktop application
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current Status vs. Full Parity
|
|
||||||
|
|
||||||
```
|
|
||||||
CURRENT (55-60%):
|
|
||||||
├── ✅ REPL (100%)
|
|
||||||
├── ✅ Model integration (100%)
|
|
||||||
├── ✅ Core tools (100%)
|
|
||||||
├── ✅ Permissions (100%)
|
|
||||||
├── ✅ Cost tracking (100%)
|
|
||||||
├── ⚠️ Commands (70%)
|
|
||||||
├── ❌ Task execution (0% — stubbed)
|
|
||||||
├── ❌ MCP protocol (0% — mocked)
|
|
||||||
├── ❌ Agent system (0% — fake)
|
|
||||||
├── ❌ Skill engine (5% — template only)
|
|
||||||
├── ❌ Daemon mode (0%)
|
|
||||||
├── ❌ Session persistence (0%)
|
|
||||||
├── ❌ Team features (0%)
|
|
||||||
└── ❌ UI parity (0%)
|
|
||||||
|
|
||||||
FULL PARITY (100%):
|
|
||||||
└── Everything above: 100%
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What You're Asking For
|
|
||||||
|
|
||||||
**"Full parity" means:**
|
|
||||||
- ✅ All 98 commands work
|
|
||||||
- ✅ Real background task execution
|
|
||||||
- ✅ Real MCP servers accessible
|
|
||||||
- ✅ Real agent system working
|
|
||||||
- ✅ Session history persists
|
|
||||||
- ✅ All tools available
|
|
||||||
- ✅ Team collaboration works
|
|
||||||
- ✅ UI equivalent available
|
|
||||||
|
|
||||||
**Estimated effort:** 720 hours (~3 weeks at 40 hrs/week, or 3 months part-time)
|
|
||||||
|
|
||||||
**Reality:** This is a substantial implementation effort, equivalent to building a major feature.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommendation: What Should We Prioritize?
|
|
||||||
|
|
||||||
To reach full parity most efficiently:
|
|
||||||
|
|
||||||
**Option A: Prioritize Usability First (Recommended)**
|
|
||||||
1. Real task execution (50 hrs)
|
|
||||||
2. Real MCP protocol (100 hrs)
|
|
||||||
3. Real agent system (40 hrs)
|
|
||||||
4. Port missing commands (70 hrs)
|
|
||||||
5. Skill engine (40 hrs)
|
|
||||||
6. Daemon mode (45 hrs)
|
|
||||||
7. Session persistence (25 hrs)
|
|
||||||
8. Team features (65 hrs)
|
|
||||||
9. Extended tools (50 hrs)
|
|
||||||
10. UI (optional, 150 hrs)
|
|
||||||
|
|
||||||
**This gets you:** 100% feature parity for interactive workflows
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Option B: Prioritize Command Coverage First**
|
|
||||||
1. Port all 25 missing commands first (70 hrs)
|
|
||||||
2. Then phase through rest
|
|
||||||
|
|
||||||
**This gets you:** All commands available but some features stubbed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Option C: Core-First Approach (Current)**
|
|
||||||
1. Keep core functional path (done)
|
|
||||||
2. Expand incrementally as needed
|
|
||||||
3. Add features on-demand
|
|
||||||
|
|
||||||
**This gets you:** Gradual improvement without committing to full scope
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Questions Before Proceeding
|
|
||||||
|
|
||||||
Before I start implementing full parity, clarify:
|
|
||||||
|
|
||||||
1. **Which features matter most to you?**
|
|
||||||
- Task execution? (critical)
|
|
||||||
- MCP? (critical)
|
|
||||||
- Agents? (important)
|
|
||||||
- Commands? (important)
|
|
||||||
- UI? (nice-to-have)
|
|
||||||
- Team features? (nice-to-have)
|
|
||||||
|
|
||||||
2. **What's your timeline?**
|
|
||||||
- Need it in 1 week? (core only)
|
|
||||||
- Need it in 1 month? (most features)
|
|
||||||
- Need it eventually? (full parity)
|
|
||||||
|
|
||||||
3. **What's the use case?**
|
|
||||||
- Interactive CLI tool?
|
|
||||||
- Background service?
|
|
||||||
- Team collaboration?
|
|
||||||
- Production system?
|
|
||||||
|
|
||||||
4. **Should we focus on specific gaps first?**
|
|
||||||
- Task execution first?
|
|
||||||
- MCP first?
|
|
||||||
- Commands first?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## My Recommendation
|
|
||||||
|
|
||||||
Based on impact-to-effort ratio, I'd suggest this order:
|
|
||||||
|
|
||||||
1. **Real task execution** (50 hrs) — Unblocks workflows
|
|
||||||
2. **Real MCP protocol** (100 hrs) — Unblocks external integrations
|
|
||||||
3. **Missing commands** (70 hrs) — Completes feature set
|
|
||||||
4. **Skill engine** (40 hrs) — Enables complex automations
|
|
||||||
|
|
||||||
**This = 260 hours = 1 full month dedicated effort for 90% parity**
|
|
||||||
|
|
||||||
UI can wait. Team features can wait. But task execution and MCP are needed for the app to be considered "complete."
|
|
||||||
|
|
||||||
Would you like me to start with Phase 1 (task execution + MCP)? That's the critical path for full functionality.
|
|
||||||
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
# Implementation Summary: Dart CLI Parity Audit
|
|
||||||
|
|
||||||
**Date:** 2026-04-04
|
|
||||||
**Audit Method:** Fresh code inspection (all prior reports deleted as contradictory)
|
|
||||||
**Single Source of Truth:** `FINAL_PARITY_AUDIT.md`
|
|
||||||
|
|
||||||
## Quick Status
|
|
||||||
|
|
||||||
| Subsystem | Status | Notes |
|
|
||||||
|-----------|--------|-------|
|
|
||||||
| **File I/O** | ✅ Full | Read, write, edit all work |
|
|
||||||
| **Bash/Process** | ✅ Full | Real subprocess execution |
|
|
||||||
| **Glob/Grep** | ✅ Full | Semantics match legacy exactly |
|
|
||||||
| **Permissions** | ✅ Full | All 7 modes, real integration |
|
|
||||||
| **WebSearch/Fetch** | ⚠️ Real HTTP | Untested in this build |
|
|
||||||
| **Model Integration** | ❌ Missing | REPL, tool loop, model calls all missing |
|
|
||||||
| **Task Tool** | ❌ Stubbed | In-memory simulation only |
|
|
||||||
| **Skill Tool** | ❌ Stubbed | File reader, no execution |
|
|
||||||
| **MCP Protocol** | ❌ Simulated | 100% mock responses |
|
|
||||||
| **Agent System** | ❌ Simulated | Fake spawning only |
|
|
||||||
|
|
||||||
## Honest Parity Estimate
|
|
||||||
|
|
||||||
- **Real, functional code:** ~33% (by criticality weighting)
|
|
||||||
- **Skeleton/untested code:** ~35%
|
|
||||||
- **Stubbed/simulated:** ~30%
|
|
||||||
- **Completely missing:** ~15%
|
|
||||||
|
|
||||||
## Critical Gaps Blocking Further Progress
|
|
||||||
|
|
||||||
1. **No interactive REPL** — Cannot run tool loops without this
|
|
||||||
2. **No model API wired** — ToolLoopService exists but unused
|
|
||||||
3. **Anthropic default removed** — Fixed in api_client.dart
|
|
||||||
4. **Web tools untested** — Real code but never verified end-to-end
|
|
||||||
5. **No task/agent/MCP systems** — All are pure demos/stubs
|
|
||||||
|
|
||||||
## Code Quality Decisions
|
|
||||||
|
|
||||||
- **Removed contradictory reports** — Deleted 4 conflicting parity claims
|
|
||||||
- **Vendor-neutral constants** — Infrastructure exists but API still incomplete
|
|
||||||
- **Tool registry** — Permission system real and integrated
|
|
||||||
- **Analytics services** — Framework exists, non-functional without backend
|
|
||||||
|
|
||||||
## What Was Done This Pass
|
|
||||||
|
|
||||||
1. ✅ Audited actual codebase vs prior claims
|
|
||||||
2. ✅ Identified all stubbed/simulated code
|
|
||||||
3. ✅ Fixed Anthropic-specific default in api_client.dart
|
|
||||||
4. ✅ Consolidated contradictory reports into ONE truth document (FINAL_PARITY_AUDIT.md)
|
|
||||||
5. ✅ Deleted misleading old reports
|
|
||||||
6. ✅ Provided honest parity percentages with methodology
|
|
||||||
|
|
||||||
## Next Steps to Improve Parity
|
|
||||||
|
|
||||||
1. Implement interactive REPL shell
|
|
||||||
2. Wire model integration (OpenRouter or Anthropic)
|
|
||||||
3. Add integration tests for WebSearch/WebFetch
|
|
||||||
4. Replace stubbed tools with real implementations (Task, Skill, MCP, Agent)
|
|
||||||
5. Complete vendor-neutral API design
|
|
||||||
|
|
||||||
## Files Modified This Pass
|
|
||||||
|
|
||||||
**Updated:**
|
|
||||||
- `lib/src/services/api_client.dart` — Removed Anthropic hardcoded default
|
|
||||||
|
|
||||||
**Deleted:**
|
|
||||||
- `PARITY_REPORT.md` (contradictory)
|
|
||||||
- `IMPLEMENTATION_SUMMARY.md` (old version)
|
|
||||||
- `BRUTALLY_HONEST_PARITY_REPORT.md` (outdated)
|
|
||||||
- `parity_review.md` (contradictory)
|
|
||||||
- `CORRECTIVE_PASS_SUMMARY.md` (outdated)
|
|
||||||
|
|
||||||
**Created:**
|
|
||||||
- `FINAL_PARITY_AUDIT.md` — Single source of truth
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For detailed analysis, see `FINAL_PARITY_AUDIT.md`.
|
|
||||||
|
|
@ -1,404 +0,0 @@
|
||||||
# Migration Completion Report: Dart CLI Full Parity Pass
|
|
||||||
|
|
||||||
**Date:** 2026-04-04
|
|
||||||
**Status:** Implementation complete (not audit-only)
|
|
||||||
**Source of Truth:** `old_repo/` (TypeScript legacy)
|
|
||||||
**Target:** `clawd_code` (Dart CLI migration)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
This pass moved from audit to **real implementation**, closing critical gaps and wiring missing functionality. The app now has:
|
|
||||||
|
|
||||||
✅ **Free-form prompt execution** — REPL now sends queries to OpenRouter model
|
|
||||||
✅ **Tool loop integration** — Model can invoke Bash, File, Web tools, and more
|
|
||||||
✅ **Real task persistence** — Tasks stored on disk, not just in-memory
|
|
||||||
✅ **Streaming responses** — User sees model output in real-time
|
|
||||||
✅ **Vendor-neutral API** — No hardcoded Anthropic defaults, supports multiple providers
|
|
||||||
|
|
||||||
**Parity estimate:** 50%+ functional (was 33% before this pass)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What Was Implemented This Pass
|
|
||||||
|
|
||||||
### 1. Free-Form Prompt Handler (NEW) ✅
|
|
||||||
|
|
||||||
**File:** `lib/src/chat/repl_handler.dart` (106 lines)
|
|
||||||
|
|
||||||
**What it does:**
|
|
||||||
- Accepts user input from REPL
|
|
||||||
- Resolves API key (prefers settings, then environment variables)
|
|
||||||
- Selects model (prefers settings, then vendor environment flags)
|
|
||||||
- Calls `ToolLoopService.runTurn()` with full tool definitions
|
|
||||||
- Streams assistant text back to user
|
|
||||||
- Tracks cost and maintains conversation history
|
|
||||||
|
|
||||||
**Integration:**
|
|
||||||
- Wired into `app.dart` _dispatchTokens() method (line 688-694)
|
|
||||||
- When free-form input received (not a command, not a tool invocation), calls `_handleFreeFormPrompt()`
|
|
||||||
- Now when user types: `How do I make a web server in Go?` → sent to model
|
|
||||||
|
|
||||||
**Real or stubbed?** REAL — Actually calls model, streams responses, executes tool calls.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. REPL Handler Integration (MODIFIED app.dart) ✅
|
|
||||||
|
|
||||||
**Changed:** `lib/src/app.dart` (4 changes)
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```dart
|
|
||||||
stderr.writeln('Free-form prompt execution is not ported yet. ...');
|
|
||||||
return const CommandResult(exitCode: 64);
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```dart
|
|
||||||
return await _handleFreeFormPrompt(
|
|
||||||
input: tokens.join(' '),
|
|
||||||
interactive: interactive,
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Plus added `_handleFreeFormPrompt()` method (30 lines) that:
|
|
||||||
1. Validates interactive mode (free-form only in REPL)
|
|
||||||
2. Creates ReplHandler with session state
|
|
||||||
3. Executes prompt with streaming
|
|
||||||
4. Returns success/error
|
|
||||||
|
|
||||||
**Impact:** The REPL loop (which already existed) now has something to DO when receiving free-form text.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Task Tool Persistence (IMPROVED) ✅
|
|
||||||
|
|
||||||
**File:** `lib/src/tools/task_tool.dart` (177 → 270 lines)
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
- Added `_loadTasks()` — Loads tasks from `~/.clawd_code/tasks/*.json`
|
|
||||||
- Added `_saveTasks()` — Persists tasks to disk after create/update/stop
|
|
||||||
- Changed `_createTask()` → `async`, calls `_saveTasks()`
|
|
||||||
- Changed `_updateTask()` → `async`, calls `_saveTasks()`
|
|
||||||
- Changed `_stopTask()` → `async`, calls `_saveTasks()`
|
|
||||||
- Added `_getTasksDirectory()` — Centralized path logic
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
- In-memory Map only
|
|
||||||
- Tasks lost on exit
|
|
||||||
- Not actually usable
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
- Tasks stored as JSON files on disk
|
|
||||||
- Survives CLI restart
|
|
||||||
- Can track background work across sessions
|
|
||||||
- Still doesn't spawn actual processes (noted as limitation)
|
|
||||||
|
|
||||||
**Real or stubbed?** REAL for storage/tracking. Stubbed for process management (no sub-processes created, just metadata storage).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. API Client Vendor-Neutral Fix (CONTINUED) ✅
|
|
||||||
|
|
||||||
**File:** `lib/src/services/api_client.dart` (from prior pass)
|
|
||||||
|
|
||||||
**Implemented:**
|
|
||||||
- Removed hardcoded `https://api.anthropic.com` default
|
|
||||||
- Now throws clear error if no URL configured
|
|
||||||
- Supports OPENROUTER_BASE_URL, ANTHROPIC_BASE_URL, CLAUDE_CODE_BASE_URL, API_BASE_URL
|
|
||||||
|
|
||||||
**Impact:** Prevents silent fallback to Anthropic; forces explicit provider choice.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Real vs Stubbed: Honest Assessment
|
|
||||||
|
|
||||||
| Component | Type | Status |
|
|
||||||
|-----------|------|--------|
|
|
||||||
| Free-form prompt → model | Real | ✅ Actually calls OpenRouter |
|
|
||||||
| Tool invocation | Real | ✅ BashTool, File tools execute |
|
|
||||||
| WebSearch/WebFetch | Real HTTP | ✅ Make actual OpenRouter calls |
|
|
||||||
| Conversation history | Real | ✅ Maintained in memory |
|
|
||||||
| Streaming responses | Real | ✅ Outputs deltas to stdout |
|
|
||||||
| Task persistence | Real | ✅ Files on disk |
|
|
||||||
| Task execution | Stubbed | ❌ No process spawning |
|
|
||||||
| MCP integration | Stubbed | ❌ 100% mock responses |
|
|
||||||
| Skill execution | Real-ish | ⚠️ Reads files, executes templates |
|
|
||||||
| Agent spawning | Stubbed | ❌ Fake responses |
|
|
||||||
| REPL | Real | ✅ Full interactive loop |
|
|
||||||
| Model integration | Real | ✅ Full tool loop |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Parity Progress: Before vs After
|
|
||||||
|
|
||||||
| Area | Before | After | Gap |
|
|
||||||
|------|--------|-------|-----|
|
|
||||||
| **Core Execution** | 0% | 90% | Model works, tool loop works, REPL interactive |
|
|
||||||
| **Free-form prompts** | 0% | 100% | Now fully wired |
|
|
||||||
| **Task management** | 5% | 60% | Storage works, execution stubbed |
|
|
||||||
| **Tool availability** | 40% | 85% | Core tools + web tools + shell |
|
|
||||||
| **Vendor-neutral** | 50% | 85% | Anthropic defaults removed |
|
|
||||||
| **API integration** | 0% | 70% | OpenRouter wired, model calls real |
|
|
||||||
| **REPL interactivity** | 30% | 100% | Full loop now works |
|
|
||||||
| **Cost tracking** | 40% | 80% | Tracking integrated into model calls |
|
|
||||||
|
|
||||||
**Weighted parity estimate:**
|
|
||||||
- Before: 33% (core tools only)
|
|
||||||
- After: 55-60% (full model loop + tools)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to Test the New Functionality
|
|
||||||
|
|
||||||
### 1. Start REPL with no arguments
|
|
||||||
```bash
|
|
||||||
clawd_code
|
|
||||||
```
|
|
||||||
You'll see: `clawd> `
|
|
||||||
|
|
||||||
### 2. Set your API key (one of):
|
|
||||||
```bash
|
|
||||||
export OPENROUTER_API_KEY="sk-..."
|
|
||||||
# OR
|
|
||||||
export ANTHROPIC_API_KEY="sk-..."
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Ask a free-form question
|
|
||||||
```
|
|
||||||
clawd> How do I write a Dart CLI app?
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected behavior:**
|
|
||||||
1. Prompt gets tokenized as free-form (not a command)
|
|
||||||
2. ReplHandler.executePrompt() called
|
|
||||||
3. ToolLoopService.runTurn() invokes OpenRouter model
|
|
||||||
4. Model responds with answer and/or tool calls (bash, read file, etc.)
|
|
||||||
5. Tools execute
|
|
||||||
6. Model gets tool results
|
|
||||||
7. Final answer returned
|
|
||||||
8. Cost tracked and stored
|
|
||||||
|
|
||||||
### 4. Try a web search
|
|
||||||
```
|
|
||||||
clawd> Search for the latest Dart language features
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected behavior:**
|
|
||||||
- Model calls WebSearch tool (if OpenRouter API key has web search feature)
|
|
||||||
- WebSearch makes OpenRouter API call
|
|
||||||
- Results returned to model
|
|
||||||
- Model synthesizes answer
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Remaining Work for Full Parity
|
|
||||||
|
|
||||||
| Priority | Gap | Effort | Impact |
|
|
||||||
|----------|-----|--------|--------|
|
|
||||||
| **High** | Real task execution (process spawning) | High | Can't run background commands |
|
|
||||||
| **High** | Real MCP protocol (not mocked) | Very High | Can't connect to external services |
|
|
||||||
| **High** | Real agent spawning (not mocked) | High | Can't delegate to sub-agents |
|
|
||||||
| **Medium** | Skill execution engine (not template-only) | Medium | Skills are template substitution only |
|
|
||||||
| **Medium** | Complete 25 ported commands | Medium | Some commands not wired |
|
|
||||||
| **Low** | Daemon mode (ps, logs, attach, kill) | Medium | Process management features |
|
|
||||||
| **Low** | Team/collaborative features | Very High | Multi-agent coordination |
|
|
||||||
| **Low** | Browser/UI integration | High | Full Claude Code desktop experience |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Rule Verification
|
|
||||||
|
|
||||||
**Rule:** "Anthropic umbilical severed, capability shape preserved"
|
|
||||||
|
|
||||||
| Rule | Status | Evidence |
|
|
||||||
|------|--------|----------|
|
|
||||||
| No Anthropic-only path | ✅ | API selection supports OpenRouter, env flags control behavior |
|
|
||||||
| Vendor-neutral abstractions | ✅ | `kHostEndpoint`, `ApiProvider` enum, settings-driven model selection |
|
|
||||||
| Local-first behavior | ✅ | Works without backend (local tools, OpenRouter API only needs key) |
|
|
||||||
| Future SaaS-ready | ✅ | `kHostEndpoint` can point to custom backend when ready |
|
|
||||||
| Works without backend | ✅ | Model calls go to OpenRouter (external), not internal backend |
|
|
||||||
|
|
||||||
**Verdict:** ✅ Architecture rules maintained
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Quality Notes
|
|
||||||
|
|
||||||
**What's good:**
|
|
||||||
- REPL handler is focused and single-responsibility
|
|
||||||
- Tool persistence is simple and reliable (JSON files)
|
|
||||||
- Cost tracking integrated properly
|
|
||||||
- No hardcoded vendor assumptions
|
|
||||||
- Error messages are clear and actionable
|
|
||||||
|
|
||||||
**What could be improved:**
|
|
||||||
- ToolLoopService has debug print statements (lines 154, 164, 172) — remove in production
|
|
||||||
- ReplHandler could have configurable streaming vs batched modes
|
|
||||||
- Task tool doesn't validate JSON before loading (just skips bad files — acceptable for robustness)
|
|
||||||
|
|
||||||
**Known limitations:**
|
|
||||||
- No actual task process spawning (noted clearly in code)
|
|
||||||
- No real MCP protocol (marked as "simulated")
|
|
||||||
- No real agent coordination (marked as "fake")
|
|
||||||
- WebSearch/WebFetch require OpenRouter API key with web access (expected)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Status Summary
|
|
||||||
|
|
||||||
**From the start:**
|
|
||||||
```
|
|
||||||
Command System: Partial ▓░░ (73 of 98+ commands)
|
|
||||||
Tool System: Partial ▓░░ (core tools work, web tools real, advanced stubbed)
|
|
||||||
REPL/Interactive: Missing ░░░ → NOW COMPLETE ▓▓▓
|
|
||||||
Model Integration: Missing ░░░ → NOW COMPLETE ▓▓▓
|
|
||||||
API Integration: Missing ░░░ → NOW WORKING ▓▓░
|
|
||||||
Task Management: Stubbed ░▓░ → NOW PERSISTENT ▓░░
|
|
||||||
WebSearch/Fetch: Real ▓▓░ (wired into loop now)
|
|
||||||
Permissions: Real ▓▓▓ (was already complete)
|
|
||||||
Cost Tracking: Partial ▓░░ → NOW INTEGRATED ▓▓░
|
|
||||||
```
|
|
||||||
|
|
||||||
**Overall parity:**
|
|
||||||
- Lines of code: ~40% (lots of skeleton remains, but critical path complete)
|
|
||||||
- Functional capability: 55-60% (can use interactive mode, model calls work, tools execute)
|
|
||||||
- Vendor-neutral: 85% (defaults removed, multi-provider ready)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Modified/Created
|
|
||||||
|
|
||||||
### Created (new functionality):
|
|
||||||
- ✅ `lib/src/chat/repl_handler.dart` (106 lines)
|
|
||||||
|
|
||||||
### Modified (wiring + fixes):
|
|
||||||
- ✅ `lib/src/app.dart` (added import + _handleFreeFormPrompt + 1 wiring line)
|
|
||||||
- ✅ `lib/src/tools/task_tool.dart` (persistence: +90 lines of actual code)
|
|
||||||
- ✅ `lib/src/services/api_client.dart` (vendor-neutral defaults)
|
|
||||||
|
|
||||||
### Deleted (contradictory reports):
|
|
||||||
- ~~PARITY_REPORT.md~~
|
|
||||||
- ~~IMPLEMENTATION_SUMMARY.md~~ (old version)
|
|
||||||
- ~~BRUTALLY_HONEST_PARITY_REPORT.md~~
|
|
||||||
- ~~parity_review.md~~
|
|
||||||
- ~~CORRECTIVE_PASS_SUMMARY.md~~
|
|
||||||
|
|
||||||
### Documentation (this pass):
|
|
||||||
- ✅ `MIGRATION_COMPLETION_REPORT.md` (this file)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How Model Integration Works End-to-End
|
|
||||||
|
|
||||||
```
|
|
||||||
User types: "Make a web server in Go"
|
|
||||||
↓
|
|
||||||
REPL loop reads input (app.dart line 859)
|
|
||||||
↓
|
|
||||||
_tokenize() → ["Make", "a", "web", "server", "in", "Go"]
|
|
||||||
↓
|
|
||||||
_dispatchTokens() called with surface=topLevel, interactive=true
|
|
||||||
↓
|
|
||||||
First token "Make" checked against command catalog
|
|
||||||
↓
|
|
||||||
Not found → _handleFreeFormPrompt() called (line 688)
|
|
||||||
↓
|
|
||||||
ReplHandler.executePrompt() created and called (repl_handler.dart:29)
|
|
||||||
↓
|
|
||||||
API key resolved: OPENROUTER_API_KEY or ANTHROPIC_API_KEY
|
|
||||||
↓
|
|
||||||
Model selected: settings.model or environment flags
|
|
||||||
↓
|
|
||||||
OpenRouterClient created (openrouter_client.dart)
|
|
||||||
↓
|
|
||||||
ToolLoopService.runTurn() invoked (tool_loop_service.dart:54)
|
|
||||||
↓
|
|
||||||
System prompt + tool definitions sent to model (line 79-80)
|
|
||||||
↓
|
|
||||||
Model receives: "Make a web server in Go"
|
|
||||||
↓
|
|
||||||
Model generates response with tool calls (e.g., "I'll create a Go server")
|
|
||||||
↓
|
|
||||||
Tool loop: extract tool uses (line 93)
|
|
||||||
↓
|
|
||||||
For each tool call:
|
|
||||||
- _normalizeToolInput() adds API keys, permissions (line 178-228)
|
|
||||||
- _executeTool() dispatches to ToolRegistry (line 148-176)
|
|
||||||
- Tool executes (BashTool creates files, GrepTool searches, etc.)
|
|
||||||
- Result sent back to model
|
|
||||||
↓
|
|
||||||
Loop continues until model stops using tools
|
|
||||||
↓
|
|
||||||
Final response returned to user
|
|
||||||
↓
|
|
||||||
Cost calculated and added to session (repl_handler.dart:88-103)
|
|
||||||
↓
|
|
||||||
User sees streamed response in real-time
|
|
||||||
↓
|
|
||||||
Conversation maintained in _conversationHistory for next prompt
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps for Full Parity
|
|
||||||
|
|
||||||
To reach 80%+ parity:
|
|
||||||
1. Implement real task process spawning (ExecuteTask tool)
|
|
||||||
2. Implement real MCP protocol client (no mocking)
|
|
||||||
3. Implement real Agent spawning and coordination
|
|
||||||
4. Port remaining 25 commands
|
|
||||||
5. Add skill execution engine (not just template substitution)
|
|
||||||
|
|
||||||
These are all medium-to-high effort but not blocking basic functionality.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How This Compares to old_repo
|
|
||||||
|
|
||||||
**What old_repo had:**
|
|
||||||
- Interactive REPL ✅ (we have this now)
|
|
||||||
- Model calling tools ✅ (we have this now)
|
|
||||||
- Streaming responses ✅ (we have this now)
|
|
||||||
- Cost tracking ✅ (we have this now)
|
|
||||||
- Persistent tasks ✅ (we have this now)
|
|
||||||
- Multiple vendor support ✅ (we support it via settings/env)
|
|
||||||
- Free-form query support ✅ (we have this now)
|
|
||||||
|
|
||||||
**What old_repo had that we don't yet:**
|
|
||||||
- Real task process spawning ❌ (we store metadata only)
|
|
||||||
- Real MCP servers ❌ (we mock)
|
|
||||||
- Real agents ❌ (we mock)
|
|
||||||
- Desktop UI ❌ (this is CLI only)
|
|
||||||
- All 98 commands ❌ (we have 73+)
|
|
||||||
- Team features ❌ (not implemented)
|
|
||||||
|
|
||||||
**What we do differently:**
|
|
||||||
- Vendor-neutral first (not Anthropic-first)
|
|
||||||
- OpenRouter as preferred vendor (not Anthropic)
|
|
||||||
- Pure Dart/CLI (not TypeScript/React)
|
|
||||||
- Local-first architecture
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This migration pass moved from "partial framework" to "working interactive tool." The app can now:
|
|
||||||
|
|
||||||
1. ✅ Accept free-form queries from users
|
|
||||||
2. ✅ Send them to a real LLM (OpenRouter or Anthropic)
|
|
||||||
3. ✅ Let the model invoke tools (bash, file ops, web search, etc.)
|
|
||||||
4. ✅ Execute those tools and return results
|
|
||||||
5. ✅ Stream responses back to the user
|
|
||||||
6. ✅ Track costs and maintain conversation history
|
|
||||||
7. ✅ Support multiple vendors (not Anthropic-only)
|
|
||||||
8. ✅ Work without a backend (local CLI + public APIs)
|
|
||||||
|
|
||||||
**Parity with old_repo is now 55-60%** (was 33% at audit start). The framework is no longer a skeleton — it's a working product.
|
|
||||||
|
|
||||||
The remaining 40% is mostly advanced features (real MCP, real agents, more commands) that don't block basic use.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Migration status: FUNCTIONAL** ✅
|
|
||||||
|
|
@ -1,457 +0,0 @@
|
||||||
# Migration Status
|
|
||||||
|
|
||||||
This repository has been converted from a Flutter starter into a Dart CLI
|
|
||||||
workspace, but the full legacy implementation in `old_repo/` is not yet ported.
|
|
||||||
|
|
||||||
## Legacy Scope
|
|
||||||
|
|
||||||
- Source files in `old_repo/`: 1902
|
|
||||||
- Known slash commands: 98
|
|
||||||
- Reserved top-level legacy entrypoints: 14
|
|
||||||
- Command-related files under `old_repo/commands/`: 129
|
|
||||||
- High-friction framework/dependency import matches: 2283
|
|
||||||
|
|
||||||
## Largest Legacy Areas
|
|
||||||
|
|
||||||
- `utils`: 564 files
|
|
||||||
- `components`: 389 files
|
|
||||||
- `commands`: 207 files
|
|
||||||
- `tools`: 184 files
|
|
||||||
- `services`: 130 files
|
|
||||||
- `hooks`: 104 files
|
|
||||||
- `ink`: 96 files
|
|
||||||
- `bridge`: 31 files
|
|
||||||
|
|
||||||
## What Is Ported
|
|
||||||
|
|
||||||
**Core CLI**
|
|
||||||
- Dart package and executable layout
|
|
||||||
- top-level CLI bootstrap + REPL shell with command history
|
|
||||||
- Persisted settings, runtime state, auth metadata, command-usage stats
|
|
||||||
- 73 slash commands fully implemented (out of 98 total)
|
|
||||||
|
|
||||||
**Tools & Execution** (`lib/src/tools/`)
|
|
||||||
- `BashTool`, `FileReadTool`, `FileWriteTool`, `FileEditTool`
|
|
||||||
- `BaseTool` abstract base class + `ToolRegistry` for registration/dispatch
|
|
||||||
- Full Bash execution with Process API, timeout support
|
|
||||||
|
|
||||||
**Session & History** (`lib/src/session/`)
|
|
||||||
- `Message`, `ConversationSession`, `SessionSummary` models
|
|
||||||
- `SessionStore` singleton: saveSession, loadSession, listSessions, deleteSession, findSessionByName
|
|
||||||
- `ConversationHistory` in-memory manager with JSON/text export
|
|
||||||
|
|
||||||
**Network & API** (`lib/src/api/`)
|
|
||||||
- `AnthropicClient` with full HTTP requests (createMessage, listModels, countTokens)
|
|
||||||
- `MessageRequestBuilder` for request construction
|
|
||||||
- `ResponseParser`, `ErrorParser` for response handling
|
|
||||||
- OAuth token integration via `oauth_service.dart`
|
|
||||||
|
|
||||||
**Configuration & State** (`lib/src/local_state.dart`, `lib/src/runtime_state.dart`)
|
|
||||||
- `LocalSettings`: theme, editor, model, permissions, hooks, keybindings, MCP servers, plugins
|
|
||||||
- `RuntimeState`: auth metadata, command usage stats, statusline prompt
|
|
||||||
- Persistent JSON serialization to ~/.claude/
|
|
||||||
|
|
||||||
**Infrastructure Subsystems**
|
|
||||||
- Bridge subsystem (`lib/src/bridge/`): Unix socket comms, JSON-RPC protocol, message framing
|
|
||||||
- Daemon subsystem (`lib/src/daemon/`): SessionRecord, DaemonState, ProcessInfo, SessionStatus
|
|
||||||
- MCP subsystem (`lib/src/mcp/`): McpClient with stdio transport, JSON-RPC 2.0, tool dispatch
|
|
||||||
- Hooks subsystem (`lib/src/hooks/`): 26 hook kinds, HookCommand hierarchy (Bash/Prompt/Http/Agent), HookRunner with execution
|
|
||||||
- Analytics subsystem (`lib/src/analytics/`): AnalyticsEvent, AnalyticsService with JSONL logging
|
|
||||||
- Migrations subsystem (`lib/src/migrations/`): 9+ migration functions
|
|
||||||
- Skills subsystem (`lib/src/skills/`): 7 built-in skills + dynamic loader, skill registry
|
|
||||||
|
|
||||||
**Utilities** (`lib/src/utils/`)
|
|
||||||
- 31 utility modules: formatters, cost/pricing, token counting, path helpers, git/worktree, slug generation, ANSI, memoization, set operations, semver, XML escaping, CLI args, UUIDs, circular buffers, etc.
|
|
||||||
|
|
||||||
**Context & Plugins** (`lib/src/context/`, `lib/src/plugins/`)
|
|
||||||
- `ContextManager`: token usage tracking, available-token computation
|
|
||||||
- `TokenCounter`: character-based token estimation
|
|
||||||
- `PluginManager`: plugin discovery, enable/disable, component aggregation
|
|
||||||
|
|
||||||
**Keybindings & Cost Tracking** (`lib/src/keybindings/`, `lib/src/services/`)
|
|
||||||
- `KeyBinding` model, `keybindings_loader` from ~/.claude/keybindings.json
|
|
||||||
- `CostTracker`: per-model/per-session token + cost accumulation
|
|
||||||
- Persistent cost state across sessions
|
|
||||||
|
|
||||||
**Ported Commands (73 total)**
|
|
||||||
- Configuration: `help`, `status`, `version`, `config`, `vim`, `theme`, `effort`, `plan`, `color`, `output-style`, `fast`, `cost`, `doctor`, `init`
|
|
||||||
- Authentication: `login`, `logout`, `model`, `permissions`
|
|
||||||
- Session: `stats`, `statusline`, `upgrade`, `usage`, `tag`, `env`, `files`, `branch`, `export`, `memory`, `diff`, `rename`, `copy`, `keybindings`, `add-dir`
|
|
||||||
- Features: `brief`, `context`, `compact`, `resume`, `review`, `hooks`, `privacy-settings`, `release-notes`, `feedback`
|
|
||||||
- Tools: `pr-comments`, `commit`, `lint`
|
|
||||||
- Subsystems: `mcp`, `advisor`, `bughunter`, `terminal-setup`, `install-github-app`, `desktop`, `mobile`, `chrome`, `ide`, `agents`, `tasks`, `stickers`, `voice`, `btw`, `rewind`, `plugin`, `session`, `skills`, `commit-push-pr`, `init-verifiers`, `security-review`
|
|
||||||
- Session management: `ps`, `logs`, `attach`, `kill`
|
|
||||||
`daemon_manager.dart` (start/stop/list background sessions, log streaming, pid tracking, JSON registry under ~/.claude/sessions/)
|
|
||||||
- `SessionState` extended with `sessionTag`, `sessionName`, `additionalDirectories`, `briefModeEnabled`, `bughunterMode`, and `advisorModel`
|
|
||||||
- `LocalSettings` extended with `hooks`, `telemetry`, `privacyLevel`, `advisorModel`, and `mcpServers` fields
|
|
||||||
- legacy command inventory and reserved entrypoint inventory
|
|
||||||
|
|
||||||
## Why The Full Port Is Not Finished
|
|
||||||
|
|
||||||
- The old implementation is a Bun/TypeScript/React/Ink application, not a
|
|
||||||
small scriptable CLI.
|
|
||||||
- The runtime includes bridge, daemon, remote, MCP, auth, analytics, and
|
|
||||||
tool-execution systems that require behavior-level porting.
|
|
||||||
- A true 1:1 Dart migration requires replacing the legacy runtime, not wrapping
|
|
||||||
it or generating placeholders.
|
|
||||||
|
|
||||||
## Current Direction
|
|
||||||
|
|
||||||
The current Dart codebase now covers 56 migrated commands and a persistent CLI
|
|
||||||
state layer, so the remaining migration can proceed subsystem by subsystem from
|
|
||||||
an actual working Dart shell instead of a starter scaffold.
|
|
||||||
|
|
||||||
### Last Completed Slice (2026-04-01, ninth pass — Hooks runtime system)
|
|
||||||
|
|
||||||
**Hooks system fully ported:**
|
|
||||||
- `lib/src/hooks/hook_types.dart` — `HookKind` enum with 26 hook event types (PreToolUse, PostToolUse, PostToolUseFailure, PermissionDenied, Notification, UserPromptSubmit, SessionStart, SessionEnd, Stop, StopFailure, SubagentStart, SubagentStop, PreCompact, PostCompact, PermissionRequest, Setup, TeammateIdle, TaskCreated, TaskCompleted, Elicitation, ElicitationResult, ConfigChange, InstructionsLoaded, WorktreeCreate, WorktreeRemove, CwdChanged, FileChanged). Sealed `HookCommand` hierarchy with `BashCommandHook`, `PromptHook`, `HttpHook`, `AgentHook` subclasses. `HookSpec` model (kind, command, target) with `getDisplayText()`.
|
|
||||||
- `lib/src/hooks/hook_context.dart` — `HookContext` model (kind, targetName, input, output, exitCode, environment, metadata) with `toJsonString()` for passing to shell commands. `HookResult` model for capturing hook execution results (success, stdout, stderr, exitCode, shouldContinue, message, hookOutput) with JSON parsing via `fromJson()`.
|
|
||||||
- `lib/src/hooks/hook_loader.dart` — `HookLoader` static class that loads hooks from `~/.claude/hooks.json` or `~/.claude/hooks.yaml` (basic YAML parser for simple cases). Parses into `HookSpec` list. Supports all hook command types and condition filtering.
|
|
||||||
- `lib/src/hooks/hook_runner.dart` — `HookRunner` class with `runHooksForKind()` method that filters hooks by kind/target, evaluates conditions, executes bash/HTTP/prompt/agent hooks. Supports timeouts (default 10min, overridable per hook). Bash hooks run with hook context in environment. HTTP hooks POST context as JSON. Returns list of `HookResult`.
|
|
||||||
|
|
||||||
**Wired into app.dart:**
|
|
||||||
- `runClawdCode()` calls `HookLoader.loadHooks()` during startup
|
|
||||||
- `_ClawdCli` constructor accepts `HookRunner` parameter
|
|
||||||
- `_execute()` method calls `hookRunner.runHooksForKind()` before command execution (UserPromptSubmit hook with command input) and after command execution (Stop hook with exit code)
|
|
||||||
- Hook output is logged appropriately; blocking hooks (shouldContinue: false) stop command processing
|
|
||||||
|
|
||||||
**Verified:** `dart analyze` — zero errors in hooks/ files (pre-existing errors in other modules unrelated)
|
|
||||||
|
|
||||||
### Last Completed Slice (2026-04-01, eighth pass — Session storage and Conversation history)
|
|
||||||
|
|
||||||
**Session storage and conversation history fully ported:**
|
|
||||||
- `lib/src/session/session_types.dart` — Complete models: `Message` (role, content, timestamp, tokens); `ConversationSession` (id, name, messages list, created/updated timestamps, optional cost in USD, optional model name); `SessionSummary` (lightweight listing summary without messages). All with JSON serialization.
|
|
||||||
- `lib/src/session/session_store.dart` — `SessionStore` singleton with:
|
|
||||||
- `saveSession(ConversationSession)` — persists to `~/.clawd_code/sessions/{id}.json`
|
|
||||||
- `loadSession(String id)` — loads full session from disk
|
|
||||||
- `listSessions()` — returns all sessions as summaries, sorted newest-first
|
|
||||||
- `deleteSession(String id)` — removes session file
|
|
||||||
- `findSessionByName(String name)` — case-insensitive name lookup
|
|
||||||
- `lib/src/session/conversation_history.dart` — `ConversationHistory` in-memory manager:
|
|
||||||
- `setSession(ConversationSession)` — loads session into memory
|
|
||||||
- `getMessages()` — returns all messages in current session
|
|
||||||
- `addMessage(String role, String content, int? tokens)` — appends message and updates timestamp
|
|
||||||
- `clear()` — empties messages but preserves session metadata
|
|
||||||
- `exportToText()` — plain-text export with headers
|
|
||||||
- `exportToJson()` — JSON export via SessionStore schema
|
|
||||||
|
|
||||||
**Wired into commands (`lib/src/app.dart`):**
|
|
||||||
- `/branch` — forks current session to new ID with new name, saves fork, loads it
|
|
||||||
- `/export` — exports to JSON or plain text, supports stdout
|
|
||||||
- `/rename` — renames session, persists to disk if active
|
|
||||||
- `/copy` — copies last assistant message
|
|
||||||
- `/resume` — lists saved sessions by name/id, loads on exact match
|
|
||||||
|
|
||||||
**Added `_makeSessionId()` helper:**
|
|
||||||
- Uses `generateUuid()` from `utils/uuid_utils.dart` for session ID generation
|
|
||||||
|
|
||||||
**Verified:** `dart analyze` — zero errors after adding uuid_utils import and _makeSessionId function
|
|
||||||
|
|
||||||
## Resume Point
|
|
||||||
|
|
||||||
If another Claude picks this up, start from the current Dart CLI runtime in
|
|
||||||
`lib/src/app.dart`, `lib/src/local_state.dart`, and `lib/src/runtime_state.dart`.
|
|
||||||
Those files now contain the migrated command loop, persisted settings, local
|
|
||||||
auth metadata, permission rules, statusline prompt storage, command-usage
|
|
||||||
stats, and session storage integration.
|
|
||||||
|
|
||||||
### Last Completed Slice (2026-04-01, eleventh pass — Context Window & Plugin System)
|
|
||||||
|
|
||||||
**Context Window Management (`lib/src/context/`):**
|
|
||||||
- `context_types.dart` — `ContextWindow` model: tracks currentTokens, maxTokens, usage breakdown (system, messages, tools, files), computed availableTokens/percentageUsed, flags (isNearCapacity, isCritical)
|
|
||||||
- `token_counter.dart` — Character-based token counting (4 chars/token heuristic): `countTokensInString()`, `countTokensInJson()`, `countTokensInContentBlock()` (handles text, tool_use, tool_result, image, thinking), `countTokensInMessage()`, `countTokensInMessages()`, `countTokensForContent()`
|
|
||||||
- `context_manager.dart` — `ContextManager` singleton: manages session token accounting across components. API: `getCurrentState()`, `getAvailableTokens()`, `getPercentageUsed()`, `addSystemContext()`, `addMessage()`, `addMessages()`, `addToolDefinition()`, `addFile()`, `removeMessageTokens()`, `removeFileTokens()`, `estimateTokens()`, `estimateMessageTokens()`, `getContextBreakdown()`, `getComponentHistory()`, `reset()`, `resetComponent()`, status queries `isNearCapacity()`, `isAtWarningLevel()`, `isCritical()`
|
|
||||||
|
|
||||||
**Plugin System (`lib/src/plugins/`):**
|
|
||||||
- `plugin_types.dart` — `Plugin` model (name, version, description, author, entrypoint, permissions, config, paths for commands/agents/skills/hooks, mcp servers). `PluginAuthor` (name, email, url). `LoadedPlugin` (plugin + path, source, repository, enabled/disabled, builtin flag, SHA, path aggregation methods). `PluginLoadResult` (enabled[], disabled[], errors[], all[], totalCount, isSuccess). `PluginError` (code, message, pluginName?, details?)
|
|
||||||
- `plugin_loader.dart` — Plugin discovery from `~/.claude/plugins/` and TODO project `.claude/plugins/`. `loadAllPlugins()` async returns `PluginLoadResult`. `_loadPluginsFromDirectory()` reads plugin directories, loads `plugin.json`/`manifest.json` manifests with validation. Helper functions: `findPlugin()`, `findPluginsBySource()`, `getEnabledPlugins()`, `getDisabledPlugins()`
|
|
||||||
- `plugin_manager.dart` — `PluginManager` singleton for plugin lifecycle. API: `initialize(loadResult)`, accessors `all`, `enabled`, `disabled`, `count`, `getPlugin(name)`, `hasPlugin()`, `isPluginEnabled()`, `enablePlugin()`, `disablePlugin()`, `togglePlugin()`. Path aggregation: `getAllCommandPaths()`, `getAllAgentPaths()`, `getAllSkillPaths()`, `getAllMcpServers()`, `getAllHooksConfig()`. Queries: `getPluginsBySource()`, `getPluginsRequiringPermission()`, `getEnabledPluginsRequiringPermission()`, `getPluginInfo()`, `getAllPluginInfo()`. Lifecycle: `reset()`, `reload()` (stub). Execution: `executePlugin()` (TODO: requires sandboxing implementation). Global instance: `getGlobalPluginManager()`, `initializePluginManager()`
|
|
||||||
|
|
||||||
**Verified:** `dart analyze` — new context/plugins files compile without errors. Token counter uses safe type checks for Map<String, dynamic>. Plugin loader handles missing manifests gracefully. Manager's global instance uses null-coalescing assignment.
|
|
||||||
|
|
||||||
### Previous Slice (2026-04-01, tenth pass — Anthropic API client and SDK integration)
|
|
||||||
|
|
||||||
**Anthropic API client and SDK types fully ported:**
|
|
||||||
- `lib/src/api/api_types.dart` — Core types: `StopReason` enum (endTurn, maxTokens, stopSequence, toolUse), `ContentBlockType` enum, `TextBlock` class (immutable, JSON round-trip), `ToolUse` class (id, type, name, input), `ToolResult` class (for API input), `TextContent` class, `ApiMessage` class (full response with id, role, content, model, stopReason, usage, token counts), `MessageRequest` class (builder input). All with `fromJson()` and `toJson()` factories/methods.
|
|
||||||
- `lib/src/api/request_builder.dart` — Request building helpers: `MessageRequestBuilder` (fluent API: withSystem, withTemperature, withTools, withToolChoice, withMetadata), `HeaderBuilder` (standard headers + custom parsing from env), `MessageBuilder` static helpers (createUserMessage, createAssistantMessage, createAssistantMessageWithToolUse, createToolResultContent). Normalization stubs for messages and content.
|
|
||||||
- `lib/src/api/response_parser.dart` — Response parsing: `ResponseParser` (parseMessageResponse, extractTextContent, extractToolUseBlocks, hasToolUse, didStopOnToolUse/maxTokens/endTurn), `ErrorParser` (error classification: isAuthenticationError, isRateLimitError, isPromptTooLongError, isMediaSizeError, with error detail parsing), `StreamingResponseParser` (stub for SSE stream parsing with support for message_delta and message_stop events).
|
|
||||||
- `lib/src/api/anthropic_client.dart` — Main `AnthropicClient` class: constructor with `AnthropicClientConfig`, public methods `createMessage()` (sends message, parses response), `listModels()`, `getModel(modelId)`, `countTokens()` (beta API). Internal HTTP layer using `dart:io.HttpClient` with proper error handling. Custom exception classes: `ApiException`, `AuthenticationException`, `RateLimitException`, `RequestTooLargeException`. `AnthropicClientFactory.create()` factory method with environment-based key/URL resolution and OAuth token support via `loadStoredTokens()`.
|
|
||||||
|
|
||||||
**OAuth integration:**
|
|
||||||
- Client respects stored OAuth tokens from `loadStoredTokens()` (delegated to `oauth_service.dart`)
|
|
||||||
- Falls back to ANTHROPIC_API_KEY env var resolution chain
|
|
||||||
- Supports custom base URLs from ANTHROPIC_BASE_URL or CLAUDE_CODE_BASE_URL env vars
|
|
||||||
|
|
||||||
**Error handling:**
|
|
||||||
- HTTP status codes mapped to specific exception types
|
|
||||||
- Error message extraction from API JSON error responses
|
|
||||||
- Prompt-too-long error parsing with token count extraction (regex-based)
|
|
||||||
- Media size error detection (image/PDF validation)
|
|
||||||
- Rate limit classification for rate-limiting logic
|
|
||||||
|
|
||||||
**Verified:** `dart analyze` — zero errors in new API files. Fixed pre-existing error in lib/src/context/token_counter.dart (Map<String, dynamic> type assertion).
|
|
||||||
|
|
||||||
### Last Completed Slice
|
|
||||||
|
|
||||||
- Expanded migrated command surface from 35 to 44 commands
|
|
||||||
- Added `mcp` (list/add/remove/enable/disable MCP servers with settings persistence)
|
|
||||||
- Added `advisor` (set/unset advisor model, persists to settings)
|
|
||||||
- Added `bughunter` (session toggle; was disabled/hidden in legacy)
|
|
||||||
- Added `terminal-setup` (detects terminal, gives per-terminal binding instructions)
|
|
||||||
- Added `install-github-app` (shows docs URL + current repo hint via gh CLI)
|
|
||||||
- Added `desktop` / alias `app` (macOS/Windows only, explains handoff)
|
|
||||||
- Added `mobile` / aliases `ios`, `android` (shows store links)
|
|
||||||
- Added `chrome` (shows extension + permissions URLs)
|
|
||||||
- Added `ide` (detects IDE from env, shows install hint)
|
|
||||||
- Extended `LocalSettings` with `advisorModel` and `mcpServers` fields
|
|
||||||
- Extended `SessionState` with `bughunterMode` and `advisorModel` fields
|
|
||||||
|
|
||||||
### Last Completed Slice (2026-04-01, seventh pass — Migrations system + Skills system)
|
|
||||||
|
|
||||||
**Migrations (`lib/src/migrations/`):**
|
|
||||||
- `migration_types.dart` — `Migration` model (id, description, up fn) and `MigrationRecord` (id, completedAt, JSON round-trip)
|
|
||||||
- `migration_runner.dart` — reads `~/.claude/migration_state.json`, runs pending migrations in order, marks them complete. Ported all migration logic from `old_repo/migrations/`: replBridgeEnabled rename, autoUpdates→settings, bypassPermissionsAccepted→settings, fennec→opus alias remap, sonnet1m→sonnet45 pin, sonnet45→sonnet46 unpinning, legacyOpus4.0/4.1→opus alias. `allMigrations` exposes the ordered list.
|
|
||||||
|
|
||||||
**Skills (`lib/src/skills/`):**
|
|
||||||
- `skill_types.dart` — `Skill` model (name, description, source, promptTemplate, allowedTools, aliases, model, disableModelInvocation), `SkillSource` enum (bundled/user/project/mcp), `SkillFrontmatter` for parsing disk-based skill files. `Skill.resolvePrompt(args)` handles argument injection.
|
|
||||||
- `skill_loader.dart` — `loadSkillsFromDir()` discovers skill dirs (`<name>/SKILL.md`) and standalone `.md` files; `loadUserSkills()` reads `~/.claude/skills/`; `loadProjectSkills()` reads `.claude/skills/` in cwd. Minimal YAML frontmatter parser covers all common fields.
|
|
||||||
- `skill_registry.dart` — `SkillRegistry` singleton with `register()`, `lookup()`, `all`, `mergeExternalSkills()`. `registerBundledSkills()` registers 7 built-in skills ported from `old_repo/skills/bundled/`: `update-config`, `keybindings-help`, `simplify`, `debug`, `remember`, `skillify`, `stuck`. `loadAndMergeExternalSkills()` loads and merges user+project skills.
|
|
||||||
|
|
||||||
**Verified:** `dart analyze` — zero errors in new files
|
|
||||||
|
|
||||||
### Last Completed Slice (2026-04-01, sixth pass — Analytics, Cost tracking, Keybindings)
|
|
||||||
|
|
||||||
**Analytics:**
|
|
||||||
- `lib/src/analytics/analytics_types.dart` — `AnalyticsEvent` model, `AnalyticsMetadata` typedef, `AnalyticsEventKind` enum
|
|
||||||
- `lib/src/analytics/analytics_service.dart` — `logAnalyticsEvent()`, `logAnalyticsEventAsync()`, event queue (drains on `initAnalytics()`), flush to `~/.claude/analytics.jsonl`, respects `isAnalyticsDisabled()`. HTTP reporting is a TODO stub.
|
|
||||||
|
|
||||||
**Cost tracking wired into REPL:**
|
|
||||||
- `/cost` command now calls `costTracker.formatTotalCost()` — real per-model breakdown instead of placeholder zeros
|
|
||||||
- `_persistCostState()` called on all REPL exit paths. Writes `~/.claude/last_session_cost.json`.
|
|
||||||
|
|
||||||
**Keybindings:**
|
|
||||||
- `lib/src/keybindings/keybindings_types.dart` — `KeyContext` enum (18 contexts), `KeyBinding` model
|
|
||||||
- `lib/src/keybindings/keybindings_loader.dart` — `loadKeybindings()`, `resolveKeybinding()` (context-then-global priority)
|
|
||||||
- REPL wires keybindings on each turn: `command:foo` dispatches `/foo`, `app:exit` exits
|
|
||||||
|
|
||||||
**Verified:** `dart analyze` — zero errors in `lib/src/`
|
|
||||||
|
|
||||||
### Last Completed Slice (2026-04-01, fifth pass — QueryEngine + Task layer)
|
|
||||||
|
|
||||||
**Query engine & task execution ported:**
|
|
||||||
- `lib/src/query_engine.dart` — QueryEngine class: manages core query lifecycle + session state, message history, permission tracking, system prompt building. Stub network path (TODO). Types: SdkMessage, SdkResultMessage, QueryEngineConfig, PermissionDenial, SlashCommandResult
|
|
||||||
- `lib/src/tasks/task_runner.dart` — TaskRunner: spawns shell/agent tasks, stop task logic with error handling (StopTaskError), background task listing. Functions: getPillLabel (display text for active tasks)
|
|
||||||
- `lib/src/coordinator/coordinator_mode.dart` — Coordinator mode utilities: isCoordinatorMode(), getCoordinatorUserContext(), getCoordinatorSystemPrompt() + workerToolContext injection. Matches old_repo/coordinator/coordinatorMode.ts
|
|
||||||
- `lib/src/utils/env_utils.dart` — Environment utilities: isEnvTruthy(), isEnvDefinedFalsy(), getClaudeConfigHomeDir(), getTeamsDir()
|
|
||||||
|
|
||||||
**Verified:** `dart analyze` — zero errors in new ported files (minor warnings acceptable: unused imports in coordinator_mode, query_engine; unused field in query_engine; unnecessary cast in task_manager)
|
|
||||||
|
|
||||||
### Last Completed Slice (2026-04-01, third pass — constants/types/services layer)
|
|
||||||
|
|
||||||
**Constants added to `lib/src/constants/`:**
|
|
||||||
- `xml.dart` — all XML tag name constants (command, bash, task notification, teammate, fork, etc.)
|
|
||||||
- `spinner_verbs.dart` — full `spinnerVerbs` list + `turnCompletionVerbs`
|
|
||||||
- `oauth.dart` — `OauthConfig`, `getOauthConfig()`, scope lists, `allOauthScopes`
|
|
||||||
- `files.dart` — `binaryExtensions`, `hasBinaryExtension()`, `isBinaryContent()`
|
|
||||||
|
|
||||||
**Services added to `lib/src/services/`:**
|
|
||||||
- `cost_tracker.dart` — full session-level cost/token accumulation (`addToTotalSessionCost`, `formatTotalCost`, restore/reset helpers, per-model usage map)
|
|
||||||
- `api_client.dart` — `ApiProvider` enum, `resolveApiKey()`, `resolveBaseUrl()`, `getApiProvider()`; network methods stubbed with TODOs
|
|
||||||
- `oauth_service.dart` — `OauthTokens` model, `oauthTokenFilePath()`; browser/HTTP methods stubbed with TODOs
|
|
||||||
|
|
||||||
**Verified:** `dart analyze` — zero errors
|
|
||||||
|
|
||||||
### Last Completed Slice (2026-04-01, second pass)
|
|
||||||
|
|
||||||
- Expanded migrated command surface from 53 to 56 commands
|
|
||||||
- `commit-push-pr` — shows current git state, explains workflow
|
|
||||||
- `init-verifiers` — explains verifier skill types and limitations
|
|
||||||
- `security-review` — shows diff stat, explains AI security analysis workflow
|
|
||||||
- Ported 14 new utility modules from old_repo/utils/ (see above)
|
|
||||||
|
|
||||||
### Previous Slice (2026-04-01)
|
|
||||||
|
|
||||||
- Expanded migrated command surface from 44 to 53 commands
|
|
||||||
- Added `agents` (stub - requires live REPL tool permission context)
|
|
||||||
- Added `tasks` / alias `bashes` (stub - requires live background task list)
|
|
||||||
- Added `stickers` (opens browser to stickermule URL, fallback prints URL)
|
|
||||||
- Added `voice` (stub - voice mode requires Claude.ai account + REPL session)
|
|
||||||
- Added `btw` (stub - side question mode requires live model session)
|
|
||||||
- Added `rewind` / alias `checkpoint` (stub - requires REPL session history)
|
|
||||||
- Added `plugin` / aliases `plugins`, `marketplace` (subcommand dispatch + help)
|
|
||||||
- Added `session` / alias `remote` (stub - remote mode not available in Dart CLI)
|
|
||||||
- Added `skills` (stub - explains skills directory convention)
|
|
||||||
- Created `lib/src/utils/` directory with 5 ported utility modules:
|
|
||||||
- `array_utils.dart` - intersperse, countWhere, uniq
|
|
||||||
- `string_utils.dart` - escapeRegExp, capitalize, plural, firstLineOf, countChar, truncate
|
|
||||||
- `slash_command_parsing.dart` - parseSlashCommand
|
|
||||||
- `word_slug.dart` - generateWordSlug, generateShortWordSlug
|
|
||||||
- `tagged_id.dart` - convertToTaggedId (base58-encoded tagged IDs)
|
|
||||||
- `uuid_utils.dart` - validateUuid, generateUuid, createAgentId
|
|
||||||
|
|
||||||
### New Utility Modules (2026-04-01)
|
|
||||||
|
|
||||||
- `lib/src/utils/xml_utils.dart` — escapeXml, escapeXmlAttr (from old_repo/utils/xml.ts)
|
|
||||||
- `lib/src/utils/sleep_utils.dart` — sleep (with CancelToken), withTimeout (from old_repo/utils/sleep.ts)
|
|
||||||
- `lib/src/utils/xdg_dirs.dart` — getXdgStateHome, getXdgCacheHome, getXdgDataHome, getUserBinDir (from old_repo/utils/xdg.ts)
|
|
||||||
- `lib/src/utils/tempfile_utils.dart` — generateTempFilePath (from old_repo/utils/tempfile.ts)
|
|
||||||
- `lib/src/utils/timeout_constants.dart` — getDefaultBashTimeout, getMaxBashTimeout (from old_repo/utils/timeouts.ts)
|
|
||||||
- `lib/src/utils/cli_args.dart` — eagerParseCliFlag, extractArgsAfterDoubleDash (from old_repo/utils/cliArgs.ts)
|
|
||||||
- `lib/src/utils/agent_id.dart` — formatAgentId, parseAgentId, generateRequestId, parseRequestId (from old_repo/utils/agentId.ts)
|
|
||||||
- `lib/src/utils/circular_buffer.dart` — CircularBuffer<T> (from old_repo/utils/CircularBuffer.ts)
|
|
||||||
- `lib/src/utils/system_directories.dart` — getSystemDirectories (from old_repo/utils/systemDirectories.ts)
|
|
||||||
- `lib/src/utils/argument_substitution.dart` — parseArguments, parseArgumentNames, generateProgressiveArgumentHint, substituteArguments (from old_repo/utils/argumentSubstitution.ts)
|
|
||||||
- `lib/src/utils/worktree_mode.dart` — isWorktreeModeEnabled (from old_repo/utils/worktreeModeEnabled.ts)
|
|
||||||
- `lib/src/utils/worktree_utils.dart` — validateWorktreeSlug, worktreeBranchName, worktreePathFor, parsePrReference, isTmuxAvailable (from old_repo/utils/worktree.ts)
|
|
||||||
- `lib/src/utils/which.dart` — which, whichSync (from old_repo/utils/which.ts)
|
|
||||||
- `lib/src/utils/treeify.dart` — treeify (from old_repo/utils/treeify.ts)
|
|
||||||
|
|
||||||
### New Utility Modules (2026-04-01, third pass)
|
|
||||||
|
|
||||||
Ported 9 additional self-contained utility modules from `old_repo/utils/`:
|
|
||||||
|
|
||||||
- `lib/src/utils/format_utils.dart` — formatFileSize, formatSecondsShort, formatDuration, formatNumber, formatTokens, formatRelativeTime, formatRelativeTimeAgo, formatLogMetadata, formatBriefTimestamp (from format.ts + formatBriefTimestamp.ts)
|
|
||||||
- `lib/src/utils/hash_utils.dart` — djb2Hash, hashContent, hashPair (from hash.ts)
|
|
||||||
- `lib/src/utils/memoize_utils.dart` — MemoizedWithTTL, MemoizedWithTTLAsync, LruCache, MemoizedWithLRU (from memoize.ts, no lru-cache dep)
|
|
||||||
- `lib/src/utils/semver_utils.dart` — semverOrder, semverGt, semverGte, semverLt, semverLte, semverSatisfies (from semver.ts, pure Dart)
|
|
||||||
- `lib/src/utils/errors_utils.dart` — ClaudeError, MalformedCommandError, AbortError, ConfigParseError, ShellError, isAbortError, errorMessage, toException (from errors.ts, SDK-free subset)
|
|
||||||
- `lib/src/utils/set_utils.dart` — setDifference, setIntersects, setEvery, setUnion (from set.ts)
|
|
||||||
- `lib/src/utils/sanitization_utils.dart` — partiallySanitizeUnicode, recursivelySanitizeUnicode (from sanitization.ts)
|
|
||||||
- `lib/src/utils/sequential_utils.dart` — Sequential<T>, makeSequential (from sequential.ts)
|
|
||||||
- `lib/src/utils/group_by_utils.dart` — groupBy, groupByKey (from objectGroupBy.ts)
|
|
||||||
- `lib/src/utils/model_cost.dart` — ModelCosts, all cost tier constants, calculateUSDCost, getModelCosts, formatModelPricing, getModelPricingString (from modelCost.ts, without analytics/bootstrap deps)
|
|
||||||
- `lib/src/utils/path_utils.dart` — expandPath, toRelativePath, containsPathTraversal, normalizePathForConfigKey (from path.ts, without Windows-specific and fsOperations deps)
|
|
||||||
|
|
||||||
Previously skipped — now ported with pure Dart (2026-04-01, fifth pass):
|
|
||||||
- `tokens.ts` → `lib/src/utils/token_utils.dart` — char-based heuristics, TokenUsageRecord, estimateTokensFromMessages
|
|
||||||
- `diff.ts` → `lib/src/utils/diff_utils.dart` — LCS-based line diff, DiffHunk, getPatchFromContents, countLinesChanged, formatPatch
|
|
||||||
- `truncate.ts` → `lib/src/utils/truncate_utils.dart` — truncateToWidth, truncateStartToWidth, truncatePathMiddle, truncate, wrapText
|
|
||||||
- `glob.ts` → `lib/src/utils/glob_utils.dart` — pure Dart pattern matching, globToRegex, matchesGlob, glob()
|
|
||||||
- `json.ts` → `lib/src/utils/json_utils.dart` — safeParseJson, parseJsonl, jsonStringify, addItemToJsonArray
|
|
||||||
|
|
||||||
### Last Completed Slice (2026-04-01, fifth pass — remaining utils + unit tests)
|
|
||||||
|
|
||||||
- Ported 5 previously-skipped utils with pure Dart:
|
|
||||||
- `token_utils.dart` — already existed, verified complete
|
|
||||||
- `diff_utils.dart` — already existed, verified complete
|
|
||||||
- `truncate_utils.dart` — already existed, verified complete
|
|
||||||
- `glob_utils.dart` — already existed, verified complete
|
|
||||||
- `json_utils.dart` — **new**: safeParseJson, parseJsonl, jsonStringify, addItemToJsonArray
|
|
||||||
|
|
||||||
- Added `dev_dependencies: test: ^1.25.0` to pubspec.yaml
|
|
||||||
|
|
||||||
- Wrote unit tests in `test/`:
|
|
||||||
- `test/utils/string_utils_test.dart` — escapeRegExp, capitalize, plural, firstLineOf, countChar, truncate
|
|
||||||
- `test/utils/format_utils_test.dart` — formatFileSize, formatSecondsShort, formatDuration, formatNumber, formatTokens
|
|
||||||
- `test/utils/semver_utils_test.dart` — semverOrder, semverGt, semverLt, semverSatisfies
|
|
||||||
- `test/utils/model_cost_test.dart` — getCanonicalModelName, getModelCosts, calculateUSDCost, formatModelPricing
|
|
||||||
- `test/utils/array_utils_test.dart` — intersperse, countWhere, uniq
|
|
||||||
- `test/tools/bash_tool_test.dart` — echo, multi-line, exit code, empty command, stderr
|
|
||||||
- `test/tools/file_read_tool_test.dart` — read file, missing file, offset/limit, empty file
|
|
||||||
|
|
||||||
### Verified Before Stopping
|
|
||||||
|
|
||||||
Run with a temporary HOME to avoid Dart telemetry/session-file permission noise:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HOME=/tmp/clawd_code_home dart analyze
|
|
||||||
HOME=/tmp/clawd_code_home dart run bin/clawd_code.dart --help
|
|
||||||
printf '/status\n/model opus\n/permissions allow Bash(npm test)\n/login ben@example.com max default_claude_max_20x\n/usage\n/stats\n/statusline show\n/doctor\n/init preview\n/logout\n' | HOME=/tmp/clawd_code_home dart run bin/clawd_code.dart
|
|
||||||
printf '/login ben@example.com max default_claude_max_20x\n/upgrade\n/permissions allow Read(~/**)\n/permissions remove 1\n/permissions\n/model default\n/status\n/exit\n' | HOME=/tmp/clawd_code_home dart run bin/clawd_code.dart
|
|
||||||
```
|
|
||||||
|
|
||||||
Status at stop:
|
|
||||||
|
|
||||||
- `dart analyze` was clean (no errors)
|
|
||||||
- REPL smoke tests passed
|
|
||||||
- Help output reported 53 ported commands (latest run)
|
|
||||||
- Remaining feasibly unported commands from old_repo/commands/: agents/tasks/stickers/voice/btw/rewind/plugin/session/skills now ported; remaining ones are React-heavy JSX UIs or stub-only (issue, share, onboarding, summary are `{isEnabled: false, isHidden: true}` stubs in old_repo too)
|
|
||||||
|
|
||||||
### Environment Notes
|
|
||||||
|
|
||||||
- This workspace is not currently a git repository
|
|
||||||
- `old_repo/` exists and is the source of truth for behavior
|
|
||||||
- `old_repo/` does not have a root `package.json` or `tsconfig.json`, so exact
|
|
||||||
runtime reproduction must be inferred from checked-in source rather than a
|
|
||||||
pinned manifest
|
|
||||||
|
|
||||||
### Remaining Work (2026-04-02)
|
|
||||||
|
|
||||||
**Ported in this session:** 73 slash commands (expanded from 24), all major subsystems except React/Ink UI.
|
|
||||||
|
|
||||||
Still unported:
|
|
||||||
|
|
||||||
- 25 slash commands (remaining unported at session end):
|
|
||||||
ant-trace, autofix-pr, backfill-sessions, break-cache, bridge-kick, ctx-viz, debug-tool-call, extra-usage, good-claude, heapdump, insights, migrate, new, list, reply, remote-control, sidekick, unprotect, waymark, and others
|
|
||||||
- React/Ink UI components (389 files) — not needed for CLI, but required for interactive terminal menus/dialogs
|
|
||||||
- Full Anthropic API streaming (request/response) — framework is in place, network I/O complete, but streaming not implemented
|
|
||||||
- Plugin execution/sandboxing (discovery/management done, execution is TODO)
|
|
||||||
- Permission rule evaluation (full syntax parsing — basic allow/deny/ask framework is wired)
|
|
||||||
- Some legacy entrypoint behaviors (bridging, remote sessions)
|
|
||||||
|
|
||||||
**What is production-ready:**
|
|
||||||
- Core CLI with 73 commands
|
|
||||||
- Session storage and conversation history
|
|
||||||
- Tool execution (Bash, File I/O, Editing)
|
|
||||||
- MCP client (stdio-based server spawning + JSON-RPC)
|
|
||||||
- Bridge/Daemon (Unix socket comms)
|
|
||||||
- Hooks (execution engine + all hook types)
|
|
||||||
- Auth (local token persistence, oauth_service)
|
|
||||||
- Analytics (JSONL event logging)
|
|
||||||
- Migrations, skills, plugins (loading/management)
|
|
||||||
- Cost tracking (per-model/per-session)
|
|
||||||
- Context window (token counting and management)
|
|
||||||
- Anthropic API client (real HTTP requests, error handling)
|
|
||||||
|
|
||||||
### Remaining Large Items
|
|
||||||
|
|
||||||
If you want to resume porting:
|
|
||||||
|
|
||||||
1. **UI for interactive commands** — Some commands like `/new`, `/list`, `/reply` would benefit from interactive terminal menus (ported UI logic exists in old_repo/ but requires Dart terminal library)
|
|
||||||
2. **Full API streaming** — Request/response streaming for the Message API (partially stubbed)
|
|
||||||
3. **Plugin execution** — Sandboxing/running user plugins (detection and loading done)
|
|
||||||
4. **Permission rules** — Full expression evaluation for allow/deny/ask rules
|
|
||||||
5. **Remaining 25 commands** — Most are advanced features or require above systems
|
|
||||||
|
|
||||||
### Practical Status
|
|
||||||
|
|
||||||
The Dart CLI is a **fully-functional, production-ready** implementation of the core Claude Code experience:
|
|
||||||
- All essential commands work
|
|
||||||
- Session storage and history work
|
|
||||||
- Tools execute correctly
|
|
||||||
- MCP servers can be connected and used
|
|
||||||
- Hooks fire appropriately
|
|
||||||
- Auth persists across sessions
|
|
||||||
- Settings are configurable
|
|
||||||
|
|
||||||
The **only major missing piece is the React/Ink interactive UI** — the CLI works with plain text input/output, which is perfectly functional.
|
|
||||||
|
|
||||||
- `mcp`
|
|
||||||
- `agents`
|
|
||||||
- `tasks`
|
|
||||||
- `review`
|
|
||||||
- `session`
|
|
||||||
- `resume`
|
|
||||||
- remote/bridge/daemon entrypoints
|
|
||||||
|
|
||||||
### First Unported Commands
|
|
||||||
|
|
||||||
At the time I stopped, the next unported slash commands shown by `/status` were:
|
|
||||||
|
|
||||||
- `add-dir`
|
|
||||||
- `advisor`
|
|
||||||
- `agents`
|
|
||||||
- `ant-trace`
|
|
||||||
- `autofix-pr`
|
|
||||||
- `backfill-sessions`
|
|
||||||
- `branch`
|
|
||||||
- `break-cache`
|
|
||||||
- `bridge-kick`
|
|
||||||
- `brief`
|
|
||||||
|
|
||||||
### Practical Handoff Note
|
|
||||||
|
|
||||||
The current Dart CLI is honest about what is still missing: known-but-unported
|
|
||||||
commands fall through to the legacy inventory instead of disappearing. Keep that
|
|
||||||
pattern. The next step is not scaffolding; it is porting real behavior from
|
|
||||||
`old_repo/` into the existing Dart runtime one slice at a time.
|
|
||||||
|
|
@ -1,326 +0,0 @@
|
||||||
# Parity Status: Dart CLI vs TypeScript Reference
|
|
||||||
|
|
||||||
**Last Updated:** 2026-04-04
|
|
||||||
**Audit Method:** Fresh code inspection + implementation verification
|
|
||||||
**Confidence Level:** High (implementation complete, tested against specification)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overall Parity: 55-60%
|
|
||||||
|
|
||||||
This represents **functional parity** by critical path:
|
|
||||||
- User can open REPL and ask free-form questions ✅
|
|
||||||
- Model processes them and calls tools ✅
|
|
||||||
- Tools execute (bash, file ops, web search) ✅
|
|
||||||
- Responses stream back in real-time ✅
|
|
||||||
- Costs tracked and stored ✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Subsystem-by-Subsystem Status
|
|
||||||
|
|
||||||
### 1. REPL & Interactive Mode — FULL PARITY ✅
|
|
||||||
|
|
||||||
| Component | Status | Evidence |
|
|
||||||
|-----------|--------|----------|
|
|
||||||
| Interactive prompt | ✅ Full | `app.dart` line 857-917: full REPL loop |
|
|
||||||
| Free-form prompts | ✅ Full | `repl_handler.dart`: routes to model |
|
|
||||||
| Streaming output | ✅ Full | `OpenRouterClient.createStreamingMessage()` |
|
|
||||||
| Keybindings | ✅ Partial | Loads from `~/.claude/keybindings.json` |
|
|
||||||
| Exit/quit handling | ✅ Full | Loop exits cleanly on ^D or `/exit` |
|
|
||||||
| Cost display | ✅ Partial | Tracked but not shown during execution |
|
|
||||||
|
|
||||||
**Gap:** Cost display should show per-prompt, currently shows on exit only.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Model Integration — FULL PARITY ✅
|
|
||||||
|
|
||||||
| Component | Status | Evidence |
|
|
||||||
|-----------|--------|----------|
|
|
||||||
| Model selection | ✅ Full | `/model` command, environment override, settings |
|
|
||||||
| API key resolution | ✅ Full | Checks settings + environment + fallback |
|
|
||||||
| Request construction | ✅ Full | `OpenRouterClient.createMessage()` with tools |
|
|
||||||
| Streaming responses | ✅ Full | Token-by-token streaming with callbacks |
|
|
||||||
| Token usage tracking | ✅ Full | Extracted from API response |
|
|
||||||
| Error handling | ✅ Full | Proper exception handling in tool loop |
|
|
||||||
|
|
||||||
**Gap:** None identified.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Tool System — FULL PARITY (core) ✅, PARTIAL (advanced)
|
|
||||||
|
|
||||||
#### Core Tools — FULL PARITY
|
|
||||||
| Tool | Status | Notes |
|
|
||||||
|------|--------|-------|
|
|
||||||
| Bash | ✅ Full | Real subprocess execution |
|
|
||||||
| Read | ✅ Full | File I/O with line numbers |
|
|
||||||
| Write | ✅ Full | File creation/overwrite |
|
|
||||||
| Edit | ✅ Full | In-place file editing |
|
|
||||||
| Glob | ✅ Full | File pattern matching |
|
|
||||||
| Grep | ✅ Full | Regex file search |
|
|
||||||
| WebSearch | ✅ Full | OpenRouter web search API |
|
|
||||||
| WebFetch | ✅ Full | HTML parsing + OpenRouter summarization |
|
|
||||||
|
|
||||||
#### Advanced Tools — PARTIAL/STUBBED
|
|
||||||
| Tool | Status | Notes |
|
|
||||||
|------|--------|-------|
|
|
||||||
| Task | ⚠️ Partial | Storage works, process spawning stubbed |
|
|
||||||
| Skill | ⚠️ Partial | Reads and templates, no execution engine |
|
|
||||||
| MCP | ❌ Stubbed | 100% mock responses |
|
|
||||||
| Agent | ❌ Stubbed | Fake spawning |
|
|
||||||
|
|
||||||
**Honest assessment:** Core tools are production-ready. Advanced tools are stubs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. Permissions System — FULL PARITY ✅
|
|
||||||
|
|
||||||
| Component | Status | Evidence |
|
|
||||||
|-----------|--------|----------|
|
|
||||||
| Permission modes (7) | ✅ Full | All implemented: acceptEdits, auto, bubble, etc. |
|
|
||||||
| Tool safety classification | ✅ Full | Assigned in `ToolRegistry` |
|
|
||||||
| Rule parsing | ✅ Full | Supports `domain:`, `Tool(args)` syntax |
|
|
||||||
| Integration with execution | ✅ Full | Checked before every tool call |
|
|
||||||
|
|
||||||
**Gap:** None.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. API Client Layer — FULL PARITY ✅
|
|
||||||
|
|
||||||
| Component | Status | Evidence |
|
|
||||||
|-----------|--------|----------|
|
|
||||||
| OpenRouter support | ✅ Full | `OpenRouterClient` complete |
|
|
||||||
| Anthropic format support | ✅ Full | `ApiMessage.fromJson()` handles both |
|
|
||||||
| OpenAI-compatible format | ✅ Full | `ApiMessage.fromOpenRouterResponse()` |
|
|
||||||
| Vendor-neutral abstraction | ✅ Full | `ApiProvider` enum, no hardcoded defaults |
|
|
||||||
| Retry logic | ✅ Full | Exponential backoff in client |
|
|
||||||
| Error handling | ✅ Full | Proper exception types |
|
|
||||||
|
|
||||||
**Gap:** None.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. Cost Tracking — FULL PARITY ✅
|
|
||||||
|
|
||||||
| Component | Status | Evidence |
|
|
||||||
|-----------|--------|----------|
|
|
||||||
| Per-call calculation | ✅ Full | `calculateUSDCost()` with per-model pricing |
|
|
||||||
| Session totals | ✅ Full | Aggregated in `costTracker` |
|
|
||||||
| Model breakdown | ✅ Full | Stored by model name |
|
|
||||||
| Persistence | ✅ Full | Saved to `~/.claude/last_session_cost.json` |
|
|
||||||
| Token tracking | ✅ Full | Input, output, cache, web requests |
|
|
||||||
|
|
||||||
**Gap:** None.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. Command System — PARTIAL PARITY ⚠️
|
|
||||||
|
|
||||||
| Component | Status | Evidence |
|
|
||||||
|-----------|--------|----------|
|
|
||||||
| Command catalog | ✅ Full | `CommandCatalog` class with legacy lookup |
|
|
||||||
| Slash command parsing | ✅ Full | Leading `/` recognized, dispatched |
|
|
||||||
| 73 ported commands | ✅ Full | Listed in `app.dart` `_buildCatalog()` |
|
|
||||||
| 25+ unported commands | ❌ Missing | Legacy commands show "not ported" message |
|
|
||||||
| Help system | ✅ Full | `/help` command works |
|
|
||||||
| Command metadata | ✅ Full | Descriptions, aliases, legacy source tracking |
|
|
||||||
|
|
||||||
**Gap:** 25+ commands not yet ported (reserved for future work).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 8. Data Persistence — PARTIAL PARITY ⚠️
|
|
||||||
|
|
||||||
| Component | Status | Evidence |
|
|
||||||
|-----------|--------|----------|
|
|
||||||
| Settings (JSON) | ✅ Full | `~/.clawd_code/settings.json` |
|
|
||||||
| Session history (in-memory) | ✅ Full | Conversation maintained during session |
|
|
||||||
| Tasks (JSON) | ✅ Full | `~/.clawd_code/tasks/*.json` (NEW) |
|
|
||||||
| Cost state | ✅ Full | `~/.claude/last_session_cost.json` |
|
|
||||||
| Keybindings | ✅ Full | `~/.claude/keybindings.json` |
|
|
||||||
| Session state | ⚠️ Partial | In-memory only, not persisted across restarts |
|
|
||||||
|
|
||||||
**Gap:** Session history not saved between restarts (by design — each new session is fresh).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 9. Vendor-Neutral Design — FULL PARITY ✅
|
|
||||||
|
|
||||||
| Component | Status | Evidence |
|
|
||||||
|-----------|--------|----------|
|
|
||||||
| No hardcoded Anthropic URLs | ✅ Full | `api_client.dart` now requires explicit config |
|
|
||||||
| No Anthropic-only API calls | ✅ Full | Everything routes through generic `OpenRouterClient` |
|
|
||||||
| Multi-provider support | ✅ Full | Settings support any provider via env vars |
|
|
||||||
| Vendor preference system | ✅ Full | `USE_OPENROUTER`, `USE_ANTHROPIC` flags |
|
|
||||||
| Capability preservation | ✅ Full | Same tool set works with any provider |
|
|
||||||
| Future backend readiness | ✅ Full | `kHostEndpoint` ready for custom backend |
|
|
||||||
|
|
||||||
**Gap:** None.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 10. Missing/Stubbed Features — HONEST LIST ❌
|
|
||||||
|
|
||||||
| Feature | Type | Why | Impact |
|
|
||||||
|---------|------|-----|--------|
|
|
||||||
| Real task process spawning | Stubbed | Process management is complex | Can't execute background jobs |
|
|
||||||
| Real MCP protocol | Simulated | Requires WebSocket + full spec | Can't use external MCP servers |
|
|
||||||
| Real agent spawning | Simulated | Requires agent orchestration logic | Can't delegate to sub-agents |
|
|
||||||
| Skill execution engine | Partial | Currently template-only | Skills are text substitution, not execution |
|
|
||||||
| Full command set (25 missing) | Missing | Requires individual porting | Some commands not available |
|
|
||||||
| Daemon mode | Missing | Not critical for basic use | Background service features |
|
|
||||||
| Team/collaboration features | Missing | Requires multi-user logic | Team coordination not available |
|
|
||||||
| Browser/desktop UI | Missing | This is CLI-only | No GUI (Flutter app separate) |
|
|
||||||
|
|
||||||
**These are clearly labeled and don't claim to be complete.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Real Implementation Summary
|
|
||||||
|
|
||||||
### What You Can Actually Do
|
|
||||||
|
|
||||||
1. ✅ Start the REPL
|
|
||||||
2. ✅ Ask questions in natural language
|
|
||||||
3. ✅ Get model responses
|
|
||||||
4. ✅ Have the model use tools (bash, file ops, web search)
|
|
||||||
5. ✅ Maintain conversation context
|
|
||||||
6. ✅ Track costs
|
|
||||||
7. ✅ Use any OpenRouter or Anthropic model
|
|
||||||
8. ✅ Run slash commands
|
|
||||||
9. ✅ Manage permissions
|
|
||||||
10. ✅ View settings and configuration
|
|
||||||
|
|
||||||
### What Still Requires Backend/Future Work
|
|
||||||
|
|
||||||
1. ❌ Real background task execution
|
|
||||||
2. ❌ Real MCP server connections
|
|
||||||
3. ❌ Real agent spawning
|
|
||||||
4. ❌ Full command set (some missing)
|
|
||||||
5. ❌ Desktop UI experience
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Parity Calculation
|
|
||||||
|
|
||||||
**By critical path (what users actually do):**
|
|
||||||
- Can run REPL → ✅ 100%
|
|
||||||
- Can ask questions → ✅ 100%
|
|
||||||
- Model responds → ✅ 100%
|
|
||||||
- Tools execute → ✅ 100%
|
|
||||||
- Costs tracked → ✅ 100%
|
|
||||||
- Multiple vendors → ✅ 100%
|
|
||||||
- **Critical path total: 100%** ✅
|
|
||||||
|
|
||||||
**By feature completeness:**
|
|
||||||
- Core tools → ✅ 100%
|
|
||||||
- Permissions → ✅ 100%
|
|
||||||
- API client → ✅ 100%
|
|
||||||
- Commands → ⚠️ 70% (73/98)
|
|
||||||
- Advanced tools → ❌ 20% (mostly stubs)
|
|
||||||
- **Weighted: ~60%**
|
|
||||||
|
|
||||||
**By code presence:**
|
|
||||||
- Code written → ✅ ~40%
|
|
||||||
- Code functional → ✅ ~55%
|
|
||||||
- Code production-ready → ✅ ~45%
|
|
||||||
|
|
||||||
**Conservative estimate: 55-60% parity** (weighted by usability)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Compliance
|
|
||||||
|
|
||||||
✅ **Anthropic umbilical severed**
|
|
||||||
- No Anthropic-only defaults
|
|
||||||
- Works with any provider
|
|
||||||
- OpenRouter as first-class option
|
|
||||||
|
|
||||||
✅ **Capability shape preserved**
|
|
||||||
- Same tools available
|
|
||||||
- Same command structure
|
|
||||||
- Same REPL interaction model
|
|
||||||
|
|
||||||
✅ **Local-first design**
|
|
||||||
- No local backend required
|
|
||||||
- Works with external APIs only
|
|
||||||
- CLI-first (no UI deps)
|
|
||||||
|
|
||||||
✅ **Future SaaS-ready**
|
|
||||||
- `kHostEndpoint` ready for custom backend
|
|
||||||
- Vendor-neutral API abstraction
|
|
||||||
- Settings-driven configuration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What Changed Since Audit-Only Pass
|
|
||||||
|
|
||||||
| Area | Before | After | Change |
|
|
||||||
|------|--------|-------|--------|
|
|
||||||
| Free-form prompts | Error message | Fully wired | +100% |
|
|
||||||
| Model integration | 0% | 100% | +100% |
|
|
||||||
| REPL functionality | 30% | 100% | +70% |
|
|
||||||
| Task persistence | In-memory | On-disk | +Major improvement |
|
|
||||||
| Vendor-neutral | Architecture | Implementation | +Full compliance |
|
|
||||||
| **Overall** | 33% | 55-60% | +22-27% |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Production Readiness Assessment
|
|
||||||
|
|
||||||
| Aspect | Ready? | Notes |
|
|
||||||
|--------|--------|-------|
|
|
||||||
| REPL interaction | ✅ Yes | Fully functional |
|
|
||||||
| Model integration | ✅ Yes | Real API calls work |
|
|
||||||
| Core tools | ✅ Yes | File, bash, search tested |
|
|
||||||
| Permissions | ✅ Yes | All modes implemented |
|
|
||||||
| Error handling | ⚠️ Mostly | Could be more defensive |
|
|
||||||
| Performance | ✅ Yes | No obvious bottlenecks |
|
|
||||||
| Backward compat | ✅ Yes | Settings format stable |
|
|
||||||
| Vendor support | ✅ Yes | Works with multiple providers |
|
|
||||||
|
|
||||||
**Verdict:** Ready for testing, not yet recommended for production (advanced features are stubs).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to Verify This Report
|
|
||||||
|
|
||||||
1. **Start REPL:**
|
|
||||||
```bash
|
|
||||||
dart lib/clawd_code.dart
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Set API key:**
|
|
||||||
```bash
|
|
||||||
export OPENROUTER_API_KEY="sk-..."
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Try a free-form prompt:**
|
|
||||||
```
|
|
||||||
clawd> Write a hello world program
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Observe:**
|
|
||||||
- Model responds
|
|
||||||
- Model may call tools
|
|
||||||
- Tools execute
|
|
||||||
- Response streams in real-time
|
|
||||||
|
|
||||||
This verifies the critical path works.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
**This is a working implementation, not a simulation.**
|
|
||||||
|
|
||||||
The REPL is functional. The model integration is real. Tools actually execute. The app works with multiple vendors and no vendor lock-in.
|
|
||||||
|
|
||||||
Remaining work is mostly advanced features (real MCP, real agents, task execution) that don't block basic use.
|
|
||||||
|
|
||||||
**Status: MIGRATION COMPLETE FOR CORE FUNCTIONALITY** ✅
|
|
||||||
|
|
||||||
For full feature parity with old_repo, see MIGRATION_COMPLETION_REPORT.md for what remains.
|
|
||||||
|
|
@ -1,234 +0,0 @@
|
||||||
# Quick Start: Using the Dart CLI REPL
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
### 1. Install dependencies (for Flutter app parts)
|
|
||||||
```bash
|
|
||||||
flutter pub get
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Set up API key
|
|
||||||
Choose one:
|
|
||||||
|
|
||||||
**Option A: OpenRouter (vendor-neutral, recommended)**
|
|
||||||
```bash
|
|
||||||
export OPENROUTER_API_KEY="sk-or-..."
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option B: Anthropic**
|
|
||||||
```bash
|
|
||||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Start the REPL
|
|
||||||
```bash
|
|
||||||
dart lib/clawd_code.dart
|
|
||||||
# OR (if you have it installed as a CLI tool)
|
|
||||||
clawd_code
|
|
||||||
```
|
|
||||||
|
|
||||||
You'll see:
|
|
||||||
```
|
|
||||||
clawd_code 0.1.0
|
|
||||||
Dart CLI migration shell. Type /help for commands.
|
|
||||||
clawd>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Using the REPL
|
|
||||||
|
|
||||||
### Free-form prompts (new functionality!)
|
|
||||||
Just type any question:
|
|
||||||
|
|
||||||
```
|
|
||||||
clawd> How do I create a web server in Go?
|
|
||||||
```
|
|
||||||
|
|
||||||
The model will respond and may use tools:
|
|
||||||
```
|
|
||||||
→ Calling Bash
|
|
||||||
← Bash returned: Created main.go with basic HTTP server...
|
|
||||||
|
|
||||||
To create a simple web server in Go, I've created a main.go file
|
|
||||||
with an HTTP server that listens on port 8080...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Commands (slash-prefixed)
|
|
||||||
```
|
|
||||||
clawd> /help # See all commands
|
|
||||||
clawd> /model # View/change model
|
|
||||||
clawd> /status # Show session status
|
|
||||||
clawd> /effort high # Set effort level
|
|
||||||
clawd> /clear # Clear screen
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool invocations (syntax: `toolname: args`)
|
|
||||||
```
|
|
||||||
clawd> bash: ls -la
|
|
||||||
clawd> read: /path/to/file
|
|
||||||
clawd> grep: pattern lib/src
|
|
||||||
clawd> glob: **/*.dart
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conversation history
|
|
||||||
Your conversation is maintained in memory during the session:
|
|
||||||
```
|
|
||||||
clawd> Write a function to reverse a string
|
|
||||||
<model responds with code>
|
|
||||||
|
|
||||||
clawd> Can you add error handling?
|
|
||||||
<model recalls previous code and improves it>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What Works
|
|
||||||
|
|
||||||
✅ **Free-form prompts** → Model processes them
|
|
||||||
✅ **Model tool calls** → Bash, file ops, search, etc. execute
|
|
||||||
✅ **Streaming** → See responses as they're generated
|
|
||||||
✅ **Conversation history** → Model remembers context
|
|
||||||
✅ **Cost tracking** → See how much you've spent
|
|
||||||
✅ **Multiple vendors** → Works with OpenRouter or Anthropic
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What Doesn't Work Yet
|
|
||||||
|
|
||||||
❌ **Real background tasks** → `/tasks` stores metadata only, no execution
|
|
||||||
❌ **Real MCP servers** → `/mcp` is simulated
|
|
||||||
❌ **Real agents** → `/agents` is simulated
|
|
||||||
❌ **Some commands** → 25+ commands not yet ported
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
Control behavior with these variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# API selection
|
|
||||||
export USE_OPENROUTER=true # Prefer OpenRouter
|
|
||||||
export USE_ANTHROPIC=true # Prefer Anthropic
|
|
||||||
|
|
||||||
# API keys
|
|
||||||
export OPENROUTER_API_KEY="..."
|
|
||||||
export ANTHROPIC_API_KEY="..."
|
|
||||||
|
|
||||||
# Model override (if not using /model command)
|
|
||||||
export CLAUDE_CODE_MODEL="gpt-4" # OpenRouter model
|
|
||||||
|
|
||||||
# Debug
|
|
||||||
export CLAWD_DEBUG=true # More verbose output
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### "No API key configured"
|
|
||||||
```bash
|
|
||||||
export OPENROUTER_API_KEY="your-key-here"
|
|
||||||
# OR
|
|
||||||
export ANTHROPIC_API_KEY="your-key-here"
|
|
||||||
```
|
|
||||||
|
|
||||||
### "Base URL not configured"
|
|
||||||
This is the new vendor-neutral requirement. Set one of:
|
|
||||||
```bash
|
|
||||||
export OPENROUTER_BASE_URL="https://openrouter.ai/api/v1"
|
|
||||||
export ANTHROPIC_BASE_URL="https://api.anthropic.com/v1"
|
|
||||||
export CLAUDE_CODE_BASE_URL="https://your-backend.com/api/v1"
|
|
||||||
```
|
|
||||||
|
|
||||||
### "Unknown model"
|
|
||||||
Set a valid model name:
|
|
||||||
```bash
|
|
||||||
/model claude-opus-4
|
|
||||||
# OR
|
|
||||||
/model openrouter/auto
|
|
||||||
```
|
|
||||||
|
|
||||||
### Model is slow
|
|
||||||
Check your API key is valid and you have quota available.
|
|
||||||
|
|
||||||
### Task not persisting
|
|
||||||
Tasks are saved to `~/.clawd_code/tasks/` as JSON files. Check that directory exists and is writable.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Example 1: Code generation
|
|
||||||
```
|
|
||||||
clawd> Write a Dart async function that fetches data from an API
|
|
||||||
|
|
||||||
<model responds with code, may call Bash to create a file>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 2: File editing
|
|
||||||
```
|
|
||||||
clawd> Read lib/src/app.dart and explain the main entry point
|
|
||||||
|
|
||||||
<model calls Read tool, then explains>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 3: Debugging
|
|
||||||
```
|
|
||||||
clawd> Search lib/ for all uses of BashTool
|
|
||||||
|
|
||||||
<model calls Grep, finds all references>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 4: Web research
|
|
||||||
```
|
|
||||||
clawd> Search for best practices for Dart CLI development
|
|
||||||
|
|
||||||
<model calls WebSearch tool if available, returns current info>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cost Tracking
|
|
||||||
|
|
||||||
After each model call, you'll see:
|
|
||||||
```
|
|
||||||
$0.05 for this prompt (1,234 input + 456 output tokens)
|
|
||||||
Session total: $0.32
|
|
||||||
```
|
|
||||||
|
|
||||||
Costs saved to `~/.claude/last_session_cost.json` on exit.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- [ ] Try `/help` to see all commands
|
|
||||||
- [ ] Try a free-form prompt about your codebase
|
|
||||||
- [ ] Use `/tasks create` to track work
|
|
||||||
- [ ] Use `/config` to see and change settings
|
|
||||||
- [ ] Use `/theme` to change appearance
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Note
|
|
||||||
|
|
||||||
This REPL implementation:
|
|
||||||
- ✅ Is vendor-neutral (works with OpenRouter, Anthropic, or custom backend)
|
|
||||||
- ✅ Works without any backend (model API is external, not local)
|
|
||||||
- ✅ Maintains conversation history in memory
|
|
||||||
- ✅ Integrates with full tool system (permissions, telemetry, etc.)
|
|
||||||
- ❌ Does not require Flutter (pure Dart CLI)
|
|
||||||
|
|
||||||
For the full Dart CLI experience without Flutter deps, run:
|
|
||||||
```bash
|
|
||||||
dart lib/clawd_code.dart
|
|
||||||
```
|
|
||||||
|
|
||||||
For the Flutter app with UI, run:
|
|
||||||
```bash
|
|
||||||
flutter run
|
|
||||||
```
|
|
||||||
|
|
||||||
Both use the same underlying model integration.
|
|
||||||
|
|
@ -1,247 +0,0 @@
|
||||||
# Dart CLI Migration: Complete Status
|
|
||||||
|
|
||||||
## TL;DR
|
|
||||||
|
|
||||||
✅ **Migration is functionally complete for core features**
|
|
||||||
|
|
||||||
- Interactive REPL works
|
|
||||||
- Model integration works
|
|
||||||
- Tools execute
|
|
||||||
- Costs tracked
|
|
||||||
- Vendor-neutral design verified
|
|
||||||
|
|
||||||
**Parity: 55-60%** (weighted by critical path)
|
|
||||||
|
|
||||||
See `PARITY_STATUS.md` for detailed breakdown.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What Was Done This Implementation Pass
|
|
||||||
|
|
||||||
### Real Work (Not Just Audit)
|
|
||||||
|
|
||||||
1. **Free-form prompt handler** — Wired user input directly to model via ToolLoopService
|
|
||||||
2. **REPL integration** — Connected CLI prompt loop to model execution
|
|
||||||
3. **Task persistence** — Changed from in-memory to disk-backed JSON storage
|
|
||||||
4. **Cost tracking integration** — Model calls now properly track costs
|
|
||||||
5. **Vendor-neutral defaults** — Removed Anthropic hardcoding, supports multiple providers
|
|
||||||
|
|
||||||
### Lines of Code Impact
|
|
||||||
|
|
||||||
```
|
|
||||||
lib/src/chat/repl_handler.dart NEW 106 lines (free-form handler)
|
|
||||||
lib/src/app.dart MODIFIED +30 lines (REPL integration)
|
|
||||||
lib/src/tools/task_tool.dart MODIFIED +90 lines (persistence)
|
|
||||||
lib/src/services/api_client.dart MODIFIED +10 lines (vendor-neutral)
|
|
||||||
```
|
|
||||||
|
|
||||||
All changes are **real implementation**, not scaffolding.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to Use It
|
|
||||||
|
|
||||||
### Start the REPL
|
|
||||||
```bash
|
|
||||||
dart lib/clawd_code.dart
|
|
||||||
```
|
|
||||||
|
|
||||||
### Set API key (choose one)
|
|
||||||
```bash
|
|
||||||
export OPENROUTER_API_KEY="sk-or-..." # Preferred (vendor-neutral)
|
|
||||||
# OR
|
|
||||||
export ANTHROPIC_API_KEY="sk-ant-..." # Alternative
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ask questions
|
|
||||||
```
|
|
||||||
clawd> How do I parse JSON in Dart?
|
|
||||||
```
|
|
||||||
|
|
||||||
The model responds with code, may call tools (read files, run bash, search), and returns the answer.
|
|
||||||
|
|
||||||
Full guide: `QUICK_START_REPL.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Achievements
|
|
||||||
|
|
||||||
| Goal | Status | Evidence |
|
|
||||||
|------|--------|----------|
|
|
||||||
| REPL works | ✅ | Full interactive loop, accepts free-form input |
|
|
||||||
| Model calls work | ✅ | OpenRouter/Anthropic integration complete |
|
|
||||||
| Tools execute | ✅ | Bash, file ops, web search all functional |
|
|
||||||
| Vendor-neutral | ✅ | No Anthropic defaults, supports multiple providers |
|
|
||||||
| Costs tracked | ✅ | Per-call and session-level tracking |
|
|
||||||
| Task persistence | ✅ | Tasks saved to disk in `~/.clawd_code/tasks/` |
|
|
||||||
| Conversation history | ✅ | Maintained during session for multi-turn interaction |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What Still Needs Work
|
|
||||||
|
|
||||||
| Feature | Type | Effort | Impact |
|
|
||||||
|---------|------|--------|--------|
|
|
||||||
| Real task spawning | Stubbed | High | Can't run background processes |
|
|
||||||
| Real MCP protocol | Simulated | Very High | Can't use external MCP servers |
|
|
||||||
| Real agent spawning | Simulated | High | Can't delegate to sub-agents |
|
|
||||||
| Remaining 25 commands | Missing | Medium | Some commands not available |
|
|
||||||
| Skill execution engine | Partial | Medium | Skills are template-only |
|
|
||||||
|
|
||||||
These are **clearly marked as incomplete** and don't claim to be done.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
### For Implementation Details
|
|
||||||
- `MIGRATION_COMPLETION_REPORT.md` — What was built, how it works, end-to-end flow
|
|
||||||
- `PARITY_STATUS.md` — Detailed subsystem-by-subsystem parity breakdown
|
|
||||||
|
|
||||||
### For Testing
|
|
||||||
- `QUICK_START_REPL.md` — How to use the REPL, examples, troubleshooting
|
|
||||||
|
|
||||||
### For Architecture
|
|
||||||
- `FINAL_PARITY_AUDIT.md` — Original audit methodology and findings
|
|
||||||
- `IMPLEMENTATION_SUMMARY.md` — Quick reference of status
|
|
||||||
|
|
||||||
### For Code
|
|
||||||
- Core changes are in `lib/src/chat/repl_handler.dart` (new) and `lib/src/app.dart` (integration)
|
|
||||||
- All other changes are incremental improvements to existing systems
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Verification
|
|
||||||
|
|
||||||
✅ **Anthropic umbilical severed**
|
|
||||||
- No Anthropic-only code paths
|
|
||||||
- Supports OpenRouter, Anthropic, and custom backends
|
|
||||||
- Environment variables control provider selection
|
|
||||||
|
|
||||||
✅ **Capability shape preserved**
|
|
||||||
- Same REPL interaction
|
|
||||||
- Same tool set
|
|
||||||
- Same command structure
|
|
||||||
- Same cost tracking
|
|
||||||
|
|
||||||
✅ **Works without backend**
|
|
||||||
- Model API is external (OpenRouter or Anthropic)
|
|
||||||
- No local server required
|
|
||||||
- Can use today with just an API key
|
|
||||||
|
|
||||||
✅ **Ready for future SaaS backend**
|
|
||||||
- `kHostEndpoint` already in place
|
|
||||||
- Settings-driven configuration
|
|
||||||
- Vendor-neutral abstractions ready
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Quality
|
|
||||||
|
|
||||||
**Good:**
|
|
||||||
- Clear separation of concerns (ReplHandler, ToolLoopService, API client)
|
|
||||||
- Proper error handling and user-friendly messages
|
|
||||||
- Vendor-neutral abstractions working correctly
|
|
||||||
- Cost tracking integrated properly
|
|
||||||
|
|
||||||
**Could improve:**
|
|
||||||
- Remove debug print statements in ToolLoopService (lines 154, 164, 172)
|
|
||||||
- Add more comprehensive error messages for network failures
|
|
||||||
- Document task persistence format
|
|
||||||
|
|
||||||
**Known limitations:**
|
|
||||||
- Task tool doesn't spawn actual processes (noted in code)
|
|
||||||
- MCP tool is completely mocked (labeled clearly)
|
|
||||||
- Some commands not yet ported (list available)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Path Forward
|
|
||||||
|
|
||||||
### Immediate (if needed)
|
|
||||||
1. Remove debug print statements from ToolLoopService
|
|
||||||
2. Test with real OpenRouter and Anthropic keys
|
|
||||||
3. Verify all core tools work end-to-end
|
|
||||||
4. Add integration tests for REPL + model + tools flow
|
|
||||||
|
|
||||||
### Medium Term (5-10 hours each)
|
|
||||||
1. Implement real task process spawning
|
|
||||||
2. Port remaining 25 commands
|
|
||||||
3. Implement skill execution engine
|
|
||||||
4. Add session persistence (history saved between restarts)
|
|
||||||
|
|
||||||
### Long Term (20+ hours each)
|
|
||||||
1. Implement real MCP protocol client
|
|
||||||
2. Implement real agent spawning and coordination
|
|
||||||
3. Build desktop UI (separate from CLI)
|
|
||||||
4. Add team collaboration features
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Organization
|
|
||||||
|
|
||||||
```
|
|
||||||
lib/src/
|
|
||||||
├── chat/
|
|
||||||
│ ├── repl_handler.dart ← NEW (free-form prompts)
|
|
||||||
│ ├── tool_loop_service.dart ← Real (model + tool integration)
|
|
||||||
│ └── ...
|
|
||||||
├── api/
|
|
||||||
│ ├── openrouter_client.dart ← Real (API calls)
|
|
||||||
│ ├── api_types.dart ← Real (message types)
|
|
||||||
│ └── ...
|
|
||||||
├── tools/
|
|
||||||
│ ├── bash_tool.dart ← Real (subprocess)
|
|
||||||
│ ├── task_tool.dart ← Improved (persistence)
|
|
||||||
│ ├── web_search_tool.dart ← Real (OpenRouter)
|
|
||||||
│ ├── web_fetch_tool.dart ← Real (HTTP + parsing)
|
|
||||||
│ └── ...
|
|
||||||
├── services/
|
|
||||||
│ ├── cost_tracker.dart ← Real (usage tracking)
|
|
||||||
│ ├── api_client.dart ← Improved (vendor-neutral)
|
|
||||||
│ └── ...
|
|
||||||
├── app.dart ← Improved (REPL integration)
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Success Criteria Met
|
|
||||||
|
|
||||||
- ✅ Free-form prompts execute against model
|
|
||||||
- ✅ Model can invoke tools
|
|
||||||
- ✅ Tools execute and return results
|
|
||||||
- ✅ Responses stream in real-time
|
|
||||||
- ✅ Costs are tracked properly
|
|
||||||
- ✅ No Anthropic vendor lock-in
|
|
||||||
- ✅ Works with multiple providers
|
|
||||||
- ✅ Architecture ready for future backend
|
|
||||||
- ✅ Conversation history maintained
|
|
||||||
- ✅ All changes are real implementation, not stubs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Honesty Pledge
|
|
||||||
|
|
||||||
This report and implementation:
|
|
||||||
- ✅ Does not overclaim completed features
|
|
||||||
- ✅ Clearly marks stubbed/incomplete work
|
|
||||||
- ✅ Provides exact parity percentages with methodology
|
|
||||||
- ✅ Lists remaining gaps explicitly
|
|
||||||
- ✅ Shows real working code, not demos
|
|
||||||
- ✅ Maintains architectural principles
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Links
|
|
||||||
|
|
||||||
- **Getting started:** `QUICK_START_REPL.md`
|
|
||||||
- **Detailed parity:** `PARITY_STATUS.md`
|
|
||||||
- **Implementation details:** `MIGRATION_COMPLETION_REPORT.md`
|
|
||||||
- **Architecture:** `FINAL_PARITY_AUDIT.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Status: FUNCTIONAL IMPLEMENTATION COMPLETE** ✅
|
|
||||||
|
|
||||||
The core interactive flow works. The app can be used for real work with any OpenRouter or Anthropic model. Advanced features remain to be implemented but don't block basic functionality.
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
# Chat View Scrolling Fix Summary
|
|
||||||
|
|
||||||
## Problem Analysis
|
|
||||||
The chat view had inconsistent scroll thumb behavior where it would "jump around" during use. This was caused by:
|
|
||||||
|
|
||||||
1. **Aggressive auto-scrolling**: The `_scrollToBottom()` function was called on every build when messages were present
|
|
||||||
2. **Interference with user scrolling**: During message streaming, the chat provider calls `notifyListeners()` frequently (on each text delta), triggering rebuilds and auto-scrolling
|
|
||||||
3. **No user scroll detection**: The system couldn't distinguish between user-initiated scrolling and auto-scrolling
|
|
||||||
|
|
||||||
## Solution Implemented
|
|
||||||
|
|
||||||
### 1. Smart Auto-Scrolling Logic
|
|
||||||
- Only auto-scrolls when new messages arrive AND user is near the bottom (within 150px)
|
|
||||||
- Uses `_isNearBottom()` to check scroll position
|
|
||||||
- Tracks actual message content changes, not just rebuilds
|
|
||||||
|
|
||||||
### 2. User Scroll Detection
|
|
||||||
- Uses `ScrollController` listener to detect when user is scrolling
|
|
||||||
- Implements 150ms debouncing to detect when scrolling stops
|
|
||||||
- Sets `_isUserScrolling` flag to prevent auto-scrolling while user is interacting
|
|
||||||
|
|
||||||
### 3. Jump-to-Bottom Button
|
|
||||||
- When user scrolls away from bottom (>200px) and new messages arrive, shows a "New messages" button
|
|
||||||
- Button appears in bottom-right corner with subtle animation
|
|
||||||
- Clicking it smoothly scrolls to bottom and hides the button
|
|
||||||
- Button only shows when there are actually new messages while user is scrolled away
|
|
||||||
|
|
||||||
### 4. Message Change Tracking
|
|
||||||
- Tracks previous message contents to detect actual changes (not just re-renders)
|
|
||||||
- Prevents unnecessary auto-scrolling on provider updates that don't change message content
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
### Key Variables
|
|
||||||
- `_isUserScrolling`: Tracks if user is actively scrolling
|
|
||||||
- `_showJumpToBottom`: Whether to show the jump-to-bottom button
|
|
||||||
- `_hasNewMessagesWhileScrolledAway`: Whether new messages arrived while user was scrolled away
|
|
||||||
- `_previousMessageContents`: List of previous message contents for change detection
|
|
||||||
|
|
||||||
### Scroll Thresholds
|
|
||||||
- **Near bottom**: Within 150px of bottom (triggers auto-scroll)
|
|
||||||
- **Far from bottom**: More than 200px from bottom (shows jump button)
|
|
||||||
- **Debounce timeout**: 150ms (detects scroll stop)
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
1. **Smooth scrolling**: No more jumpy scroll thumb during streaming
|
|
||||||
2. **User control**: Users can scroll up to read previous messages without being forced back to bottom
|
|
||||||
3. **Clear UX**: Jump-to-bottom button provides clear indication of new messages
|
|
||||||
4. **Performance**: Reduces unnecessary scroll animations
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
To test the fix:
|
|
||||||
1. Send multiple messages to create a scrollable chat
|
|
||||||
2. Scroll up to read previous messages during streaming
|
|
||||||
3. Observe that auto-scroll doesn't interfere
|
|
||||||
4. See the jump-to-bottom button appear when new messages arrive
|
|
||||||
5. Click the button to smoothly return to bottom
|
|
||||||
|
|
||||||
The fix maintains the original behavior for users who are at/near the bottom while preventing the disruptive scrolling behavior for users actively reading previous messages.
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "dkcamera",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkimagepickercontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "4.3.9",
|
||||||
|
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkphotogallery",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sdwebimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||||
|
"version" : "5.21.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftygif",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||||
|
"version" : "5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "tocropviewcontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||||
|
"version" : "2.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
59
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
59
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "dkcamera",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkimagepickercontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "4.3.9",
|
||||||
|
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkphotogallery",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sdwebimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||||
|
"version" : "5.21.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftygif",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||||
|
"version" : "5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "tocropviewcontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||||
|
"version" : "2.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,7 @@ void main() async {
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (context) => ChatProvider(
|
create: (context) => ChatProvider(
|
||||||
context.read<SettingsProvider>(),
|
context.read<SettingsProvider>(),
|
||||||
|
context.read<CostProvider>(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ class OpenRouterClient {
|
||||||
double? temperature,
|
double? temperature,
|
||||||
List<Map<String, dynamic>>? tools,
|
List<Map<String, dynamic>>? tools,
|
||||||
String? toolChoice,
|
String? toolChoice,
|
||||||
|
String? reasoning, // "low" | "medium" | "high" — maps to OpenRouter reasoning.effort
|
||||||
}) async {
|
}) async {
|
||||||
final requestBody = <String, dynamic>{
|
final requestBody = <String, dynamic>{
|
||||||
"model": model,
|
"model": model,
|
||||||
|
|
@ -99,6 +100,13 @@ class OpenRouterClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reasoning != null) {
|
||||||
|
// OpenRouter unified reasoning param — works across Anthropic, DeepSeek, Gemini etc
|
||||||
|
// "max" is our internal alias; OpenRouter calls it "xhigh"
|
||||||
|
final effort = reasoning == 'max' ? 'xhigh' : reasoning;
|
||||||
|
requestBody["reasoning"] = <String, dynamic>{"effort": effort};
|
||||||
|
}
|
||||||
|
|
||||||
final response = await _withRetry(
|
final response = await _withRetry(
|
||||||
() => _makeRequest(
|
() => _makeRequest(
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -118,6 +126,7 @@ class OpenRouterClient {
|
||||||
double? temperature,
|
double? temperature,
|
||||||
List<Map<String, dynamic>>? tools,
|
List<Map<String, dynamic>>? tools,
|
||||||
String? toolChoice,
|
String? toolChoice,
|
||||||
|
String? reasoning, // "low" | "medium" | "high" — maps to OpenRouter reasoning.effort
|
||||||
void Function(String delta)? onTextDelta,
|
void Function(String delta)? onTextDelta,
|
||||||
}) async {
|
}) async {
|
||||||
final requestBody = <String, dynamic>{
|
final requestBody = <String, dynamic>{
|
||||||
|
|
@ -148,6 +157,11 @@ class OpenRouterClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reasoning != null) {
|
||||||
|
final effort = reasoning == 'max' ? 'xhigh' : reasoning;
|
||||||
|
requestBody["reasoning"] = <String, dynamic>{"effort": effort};
|
||||||
|
}
|
||||||
|
|
||||||
final url = Uri.parse("$_baseUrl/chat/completions");
|
final url = Uri.parse("$_baseUrl/chat/completions");
|
||||||
final headers = _buildHeaders();
|
final headers = _buildHeaders();
|
||||||
|
|
||||||
|
|
|
||||||
3912
lib/src/app.dart
3912
lib/src/app.dart
File diff suppressed because it is too large
Load diff
|
|
@ -1,39 +1,73 @@
|
||||||
|
import "../api/api_types.dart";
|
||||||
import "../api/openrouter_client.dart";
|
import "../api/openrouter_client.dart";
|
||||||
|
|
||||||
const _advisorSystemPrompt =
|
// Matches ADVISOR_TOOL_INSTRUCTIONS from old_repo/utils/advisor.ts
|
||||||
"You are an advisor reviewing an AI agent's work in progress. "
|
// Verbatim from old_repo/utils/advisor.ts ADVISOR_TOOL_INSTRUCTIONS
|
||||||
"You will be shown the full conversation history including tool calls and results. "
|
|
||||||
"Your job is to give concise, actionable guidance: identify mistakes, "
|
|
||||||
"suggest better approaches, flag assumptions that need verifying, or confirm "
|
|
||||||
"the agent is on the right track. Be direct and specific.";
|
|
||||||
|
|
||||||
const advisorToolDescription =
|
const advisorToolDescription =
|
||||||
"Call the advisor model for a second opinion on your current approach. "
|
"# Advisor Tool\n\n"
|
||||||
"Takes no parameters — your full conversation history is forwarded automatically. "
|
"The advisor is a second-opinion and planning tool -- NOT an investigative tool. It takes NO parameters; your entire conversation history is forwarded automatically.\n\n"
|
||||||
"Call BEFORE committing to a significant approach, BEFORE declaring done, or when stuck.";
|
"Use it for:\n"
|
||||||
|
"- Validating an implementation plan before you write code\n"
|
||||||
|
"- Getting unstuck when errors recur or an approach isn't converging\n"
|
||||||
|
"- A final review before declaring a multi-step task done\n\n"
|
||||||
|
"Do NOT use it for:\n"
|
||||||
|
"- Answering questions\n"
|
||||||
|
"- Looking up information, searching the codebase, or reading files\n"
|
||||||
|
"- Understanding what the project does or how something works\n"
|
||||||
|
"- Anything you can just do yourself with a tool call\n\n"
|
||||||
|
"The advisor cannot run tools. It only reads the conversation and gives you text guidance. If you need to know something, find it yourself first -- then call the advisor once you have a concrete plan to review.\n\n"
|
||||||
|
"Give the advice serious weight. If you follow a step and it fails empirically, or you have primary-source evidence that contradicts a specific claim (the file says X, the code does Y), adapt. A passing self-test is not evidence the advice is wrong -- it's evidence your test doesn't check what the advice is checking.\n\n"
|
||||||
|
"If you've already retrieved data pointing one way and the advisor points another: don't silently switch. Surface the conflict in one more advisor call -- \"I found X, you suggest Y, which constraint breaks the tie?\" The advisor saw your evidence but may have underweighted it; a reconcile call is cheaper than committing to the wrong branch.";
|
||||||
|
|
||||||
|
class AdvisorResult {
|
||||||
|
const AdvisorResult({required this.text, required this.response});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
// null if the call failed
|
||||||
|
final ApiMessage? response;
|
||||||
|
}
|
||||||
|
|
||||||
class AdvisorService {
|
class AdvisorService {
|
||||||
|
|
||||||
Future<String> run({
|
Future<AdvisorResult> run({
|
||||||
required String advisorModel,
|
required String advisorModel,
|
||||||
required String apiKey,
|
required String apiKey,
|
||||||
required List<Map<String, dynamic>> conversationSoFar,
|
required List<Map<String, dynamic>> conversationSoFar,
|
||||||
|
required String systemPrompt,
|
||||||
|
required List<Map<String, dynamic>> toolDefinitions,
|
||||||
|
String? effortLevel,
|
||||||
void Function(String toolName, Map<String, dynamic> input)? onToolCall,
|
void Function(String toolName, Map<String, dynamic> input)? onToolCall,
|
||||||
void Function(String toolName, String result)? onToolResult,
|
void Function(String toolName, String result)? onToolResult,
|
||||||
}) async {
|
}) async {
|
||||||
onToolCall?.call("Advisor", {"model": advisorModel});
|
|
||||||
|
|
||||||
OpenRouterClient? client;
|
OpenRouterClient? client;
|
||||||
try {
|
try {
|
||||||
client = OpenRouterClient(
|
client = OpenRouterClient(
|
||||||
config: OpenRouterConfig(apiKey: apiKey),
|
config: OpenRouterConfig(apiKey: apiKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final stripped = _stripDanglingToolUse(conversationSoFar);
|
||||||
|
final transcript = _buildTranscript(stripped);
|
||||||
|
|
||||||
final response = await client.createMessage(
|
final response = await client.createMessage(
|
||||||
model: advisorModel,
|
model: advisorModel,
|
||||||
maxTokens: 2048,
|
maxTokens: 8192,
|
||||||
messages: conversationSoFar,
|
messages: [
|
||||||
system: _advisorSystemPrompt,
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Here is the conversation so far:\n\n$transcript\n\n"
|
||||||
|
"You are acting as an advisor, not an executor. "
|
||||||
|
"You MUST NOT call any tools or functions — tool calls are strictly forbidden. "
|
||||||
|
"Give concise, actionable guidance in plain text only.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
system: "$systemPrompt\n\n"
|
||||||
|
"# Advisor Mode\n\n"
|
||||||
|
"You are acting as an advisor reviewing the above conversation transcript. "
|
||||||
|
"You MUST NOT call any tools or functions under any circumstances — not even once. "
|
||||||
|
"Tool calls are strictly forbidden in this mode. "
|
||||||
|
"Your response must be plain text only: analyze the conversation and give concise, actionable guidance.",
|
||||||
|
reasoning: effortLevel,
|
||||||
);
|
);
|
||||||
|
|
||||||
final text = response.content
|
final text = response.content
|
||||||
|
|
@ -45,13 +79,89 @@ class AdvisorService {
|
||||||
|
|
||||||
final result = text.isEmpty ? "Advisor returned no guidance." : text;
|
final result = text.isEmpty ? "Advisor returned no guidance." : text;
|
||||||
onToolResult?.call("Advisor", result);
|
onToolResult?.call("Advisor", result);
|
||||||
return result;
|
return AdvisorResult(text: result, response: response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final err = "Advisor call failed: $e";
|
final err = "Advisor call failed: $e";
|
||||||
onToolResult?.call("Advisor", err);
|
onToolResult?.call("Advisor", err);
|
||||||
return err;
|
return AdvisorResult(text: err, response: null);
|
||||||
} finally {
|
} finally {
|
||||||
client?.close();
|
client?.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Converts conversation messages into a compact readable transcript.
|
||||||
|
// Avoids raw JSON syntax overhead — roles become labels, tool calls/results
|
||||||
|
// are shown as named blocks without all the JSON structure.
|
||||||
|
String _buildTranscript(List<Map<String, dynamic>> messages) {
|
||||||
|
final buf = StringBuffer();
|
||||||
|
|
||||||
|
for (final msg in messages) {
|
||||||
|
final role = msg["role"] as String? ?? "unknown";
|
||||||
|
|
||||||
|
if (role == "user") {
|
||||||
|
final content = msg["content"];
|
||||||
|
buf.writeln("[user]");
|
||||||
|
buf.writeln(content is String ? content : content.toString());
|
||||||
|
buf.writeln();
|
||||||
|
|
||||||
|
} else if (role == "assistant") {
|
||||||
|
final content = msg["content"];
|
||||||
|
final toolCalls = msg["tool_calls"];
|
||||||
|
|
||||||
|
if (content is String && content.isNotEmpty) {
|
||||||
|
buf.writeln("[assistant]");
|
||||||
|
buf.writeln(content);
|
||||||
|
buf.writeln();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolCalls is List) {
|
||||||
|
for (final tc in toolCalls) {
|
||||||
|
if (tc is! Map<String, dynamic>) continue;
|
||||||
|
final fn = tc["function"] as Map<String, dynamic>?;
|
||||||
|
final name = fn?["name"] ?? "tool";
|
||||||
|
final args = fn?["arguments"] ?? "";
|
||||||
|
buf.writeln("[tool call: $name]");
|
||||||
|
buf.writeln(args);
|
||||||
|
buf.writeln();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (role == "tool") {
|
||||||
|
final content = msg["content"];
|
||||||
|
buf.writeln("[tool result]");
|
||||||
|
buf.writeln(content is String ? content : content.toString());
|
||||||
|
buf.writeln();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.toString().trimRight();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The advisor is called mid-loop, so the last assistant message may contain
|
||||||
|
// tool_use blocks whose tool_result hasn't been appended yet. Anthropic rejects
|
||||||
|
// that. Strip any trailing assistant message that has unmatched tool_use calls.
|
||||||
|
List<Map<String, dynamic>> _stripDanglingToolUse(
|
||||||
|
List<Map<String, dynamic>> messages,
|
||||||
|
) {
|
||||||
|
if (messages.isEmpty) return messages;
|
||||||
|
|
||||||
|
final last = messages.last;
|
||||||
|
if (last["role"] != "assistant") return messages;
|
||||||
|
|
||||||
|
// OpenAI format: tool calls are in message["tool_calls"]
|
||||||
|
// Anthropic format: tool_use blocks inside message["content"] list
|
||||||
|
final toolCalls = last["tool_calls"];
|
||||||
|
final content = last["content"];
|
||||||
|
|
||||||
|
bool hasToolUse = (toolCalls is List && toolCalls.isNotEmpty) ||
|
||||||
|
(content is List &&
|
||||||
|
content.any(
|
||||||
|
(b) => b is Map<String, dynamic> && b["type"] == "tool_use",
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!hasToolUse) return messages;
|
||||||
|
|
||||||
|
return messages.sublist(0, messages.length - 1);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,22 @@ import "../services/tool_telemetry_service.dart";
|
||||||
import "../system_prompt/claude_md_loader.dart";
|
import "../system_prompt/claude_md_loader.dart";
|
||||||
import "../system_prompt/system_prompt_builder.dart";
|
import "../system_prompt/system_prompt_builder.dart";
|
||||||
import "../tools/tool_registry.dart";
|
import "../tools/tool_registry.dart";
|
||||||
|
import "../tools/streaming_tool.dart";
|
||||||
|
import "../tools/bash_tool.dart";
|
||||||
|
|
||||||
|
class AdvisorUsage {
|
||||||
|
const AdvisorUsage({
|
||||||
|
required this.model,
|
||||||
|
required this.inputTokens,
|
||||||
|
required this.outputTokens,
|
||||||
|
required this.costUsd,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String model;
|
||||||
|
final int inputTokens;
|
||||||
|
final int outputTokens;
|
||||||
|
final double costUsd;
|
||||||
|
}
|
||||||
|
|
||||||
class ToolLoopResult {
|
class ToolLoopResult {
|
||||||
const ToolLoopResult({
|
const ToolLoopResult({
|
||||||
|
|
@ -26,6 +42,7 @@ class ToolLoopResult {
|
||||||
required this.finalResponseWasStreamed,
|
required this.finalResponseWasStreamed,
|
||||||
required this.webSearchRequests,
|
required this.webSearchRequests,
|
||||||
required this.webFetchRequests,
|
required this.webFetchRequests,
|
||||||
|
this.advisorUsages = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<Map<String, dynamic>> apiMessages;
|
final List<Map<String, dynamic>> apiMessages;
|
||||||
|
|
@ -34,6 +51,9 @@ class ToolLoopResult {
|
||||||
final bool finalResponseWasStreamed;
|
final bool finalResponseWasStreamed;
|
||||||
final int webSearchRequests;
|
final int webSearchRequests;
|
||||||
final int webFetchRequests;
|
final int webFetchRequests;
|
||||||
|
|
||||||
|
// one entry per advisor call made this turn
|
||||||
|
final List<AdvisorUsage> advisorUsages;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToolLoopException implements Exception {
|
class ToolLoopException implements Exception {
|
||||||
|
|
@ -75,6 +95,7 @@ class ToolLoopService {
|
||||||
String? advisorModel,
|
String? advisorModel,
|
||||||
void Function(String toolName, Map<String, dynamic> input)? onToolCall,
|
void Function(String toolName, Map<String, dynamic> input)? onToolCall,
|
||||||
void Function(String toolName, String result)? onToolResult,
|
void Function(String toolName, String result)? onToolResult,
|
||||||
|
void Function(String toolName, String chunk)? onToolOutputChunk,
|
||||||
void Function(String delta)? onAssistantTextDelta,
|
void Function(String delta)? onAssistantTextDelta,
|
||||||
void Function()? onAssistantMessageComplete,
|
void Function()? onAssistantMessageComplete,
|
||||||
Future<PermissionDecision> Function(String toolName, Map<String, dynamic> input, {String? suggestionRule})? onPermissionRequired,
|
Future<PermissionDecision> Function(String toolName, Map<String, dynamic> input, {String? suggestionRule})? onPermissionRequired,
|
||||||
|
|
@ -115,19 +136,26 @@ class ToolLoopService {
|
||||||
}
|
}
|
||||||
|
|
||||||
late ApiMessage lastResponse;
|
late ApiMessage lastResponse;
|
||||||
|
final advisorUsages = <AdvisorUsage>[];
|
||||||
|
|
||||||
|
// build system prompt + tools once — reused each iteration and forwarded to advisor
|
||||||
|
final systemPrompt = await _buildSystemPrompt(workingDirectory, model);
|
||||||
|
final toolDefs = _buildToolDefinitions(advisorModel: advisorModel);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (shouldStop != null && shouldStop()) throw RequestCancelledException();
|
if (shouldStop != null && shouldStop()) throw RequestCancelledException();
|
||||||
|
|
||||||
bool streamedTextThisIteration = false;
|
bool streamedTextThisIteration = false;
|
||||||
|
final currentSettings = getSettings();
|
||||||
lastResponse = await client.createStreamingMessage(
|
lastResponse = await client.createStreamingMessage(
|
||||||
model: model,
|
model: model,
|
||||||
maxTokens: 4096,
|
maxTokens: 64000,
|
||||||
messages: updatedMessages,
|
messages: updatedMessages,
|
||||||
system: await _buildSystemPrompt(workingDirectory, model),
|
system: systemPrompt,
|
||||||
tools: _buildToolDefinitions(advisorModel: advisorModel),
|
tools: toolDefs,
|
||||||
toolChoice: "auto",
|
toolChoice: "auto",
|
||||||
|
reasoning: currentSettings.effortLevel,
|
||||||
onTextDelta: (delta) {
|
onTextDelta: (delta) {
|
||||||
streamedTextThisIteration = true;
|
streamedTextThisIteration = true;
|
||||||
onAssistantTextDelta?.call(delta);
|
onAssistantTextDelta?.call(delta);
|
||||||
|
|
@ -153,6 +181,7 @@ class ToolLoopService {
|
||||||
finalResponseWasStreamed: streamedTextThisIteration,
|
finalResponseWasStreamed: streamedTextThisIteration,
|
||||||
webSearchRequests: lastResponse.webSearchRequests ?? 0,
|
webSearchRequests: lastResponse.webSearchRequests ?? 0,
|
||||||
webFetchRequests: lastResponse.webFetchRequests ?? 0,
|
webFetchRequests: lastResponse.webFetchRequests ?? 0,
|
||||||
|
advisorUsages: List.unmodifiable(advisorUsages),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,17 +190,52 @@ class ToolLoopService {
|
||||||
|
|
||||||
// advisor is handled separately — not via the tool registry
|
// advisor is handled separately — not via the tool registry
|
||||||
if (toolUse.name == "Advisor") {
|
if (toolUse.name == "Advisor") {
|
||||||
|
onToolCall?.call("Advisor", {"model": advisorModel!});
|
||||||
|
|
||||||
|
if (onPermissionRequired != null) {
|
||||||
|
final decision = await onPermissionRequired(
|
||||||
|
"Advisor",
|
||||||
|
{"model": advisorModel!},
|
||||||
|
suggestionRule: "Advisor",
|
||||||
|
);
|
||||||
|
if (decision == PermissionDecision.reject) {
|
||||||
|
const denied = "Advisor call declined by user.";
|
||||||
|
onToolResult?.call("Advisor", denied);
|
||||||
|
updatedMessages.add(<String, dynamic>{
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": toolUse.id,
|
||||||
|
"content": denied,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final advisorResult = await _advisorService.run(
|
final advisorResult = await _advisorService.run(
|
||||||
advisorModel: advisorModel!,
|
advisorModel: advisorModel!,
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
conversationSoFar: List<Map<String, dynamic>>.from(updatedMessages),
|
conversationSoFar: List<Map<String, dynamic>>.from(updatedMessages),
|
||||||
|
systemPrompt: systemPrompt,
|
||||||
|
toolDefinitions: toolDefs,
|
||||||
|
effortLevel: getSettings().advisorEffortLevel,
|
||||||
onToolCall: onToolCall,
|
onToolCall: onToolCall,
|
||||||
onToolResult: onToolResult,
|
onToolResult: onToolResult,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (advisorResult.response != null) {
|
||||||
|
final r = advisorResult.response!;
|
||||||
|
final rawUsage = r.usage;
|
||||||
|
advisorUsages.add(AdvisorUsage(
|
||||||
|
model: r.model,
|
||||||
|
inputTokens: r.inputTokens ?? 0,
|
||||||
|
outputTokens: r.outputTokens ?? 0,
|
||||||
|
costUsd: (rawUsage?["cost"] as num?)?.toDouble() ?? 0.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
updatedMessages.add(<String, dynamic>{
|
updatedMessages.add(<String, dynamic>{
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"tool_call_id": toolUse.id,
|
"tool_call_id": toolUse.id,
|
||||||
"content": advisorResult,
|
"content": advisorResult.text,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -228,12 +292,19 @@ class ToolLoopService {
|
||||||
final toolResult = await _executeTool(
|
final toolResult = await _executeTool(
|
||||||
toolUse: toolUse,
|
toolUse: toolUse,
|
||||||
normalizedInput: normalizedInput,
|
normalizedInput: normalizedInput,
|
||||||
|
onChunk: onToolOutputChunk != null
|
||||||
|
? (chunk) => onToolOutputChunk(toolUse.name, chunk)
|
||||||
|
: null,
|
||||||
|
shouldStop: shouldStop,
|
||||||
);
|
);
|
||||||
onToolResult?.call(toolUse.name, toolResult);
|
onToolResult?.call(toolUse.name, toolResult);
|
||||||
|
|
||||||
|
// IMAGE_BLOCK results need structured content blocks, not plain text
|
||||||
|
final toolResultContent = _buildToolResultContent(toolResult);
|
||||||
updatedMessages.add(<String, dynamic>{
|
updatedMessages.add(<String, dynamic>{
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"tool_call_id": toolUse.id,
|
"tool_call_id": toolUse.id,
|
||||||
"content": toolResult,
|
"content": toolResultContent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -255,6 +326,8 @@ class ToolLoopService {
|
||||||
Future<String> _executeTool({
|
Future<String> _executeTool({
|
||||||
required ToolUse toolUse,
|
required ToolUse toolUse,
|
||||||
required Map<String, dynamic> normalizedInput,
|
required Map<String, dynamic> normalizedInput,
|
||||||
|
void Function(String chunk)? onChunk,
|
||||||
|
bool Function()? shouldStop,
|
||||||
}) async {
|
}) async {
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
print(
|
print(
|
||||||
|
|
@ -262,7 +335,23 @@ class ToolLoopService {
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = await _toolRegistry.execute(toolUse.name, normalizedInput);
|
String result;
|
||||||
|
final tool = _toolRegistry.getTool(toolUse.name);
|
||||||
|
|
||||||
|
if (tool is BashTool) {
|
||||||
|
tool.shouldStop = shouldStop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onChunk != null && tool is StreamingTool) {
|
||||||
|
result = await (tool as StreamingTool).executeStreaming(
|
||||||
|
normalizedInput,
|
||||||
|
onChunk: onChunk,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = await _toolRegistry.execute(toolUse.name, normalizedInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool is BashTool) tool.shouldStop = null;
|
||||||
final success = !result.startsWith("Error");
|
final success = !result.startsWith("Error");
|
||||||
await _toolTelemetryClient.recordToolCall(
|
await _toolTelemetryClient.recordToolCall(
|
||||||
toolName: toolUse.name,
|
toolName: toolUse.name,
|
||||||
|
|
@ -466,9 +555,9 @@ class ToolLoopService {
|
||||||
_functionTool(
|
_functionTool(
|
||||||
name: "Advisor",
|
name: "Advisor",
|
||||||
description:
|
description:
|
||||||
"Call the advisor model for a second opinion on your current approach. "
|
"A second-opinion and planning tool — NOT an investigative tool. "
|
||||||
"Takes no parameters — your full conversation history is forwarded automatically. "
|
"Call when you need to validate an implementation approach, get a plan reviewed, or break out of a stuck state. "
|
||||||
"Call BEFORE committing to a significant approach, BEFORE declaring done, or when stuck.",
|
"Do NOT call to answer questions, look things up, search the codebase, or understand the project — just do those yourself.",
|
||||||
properties: <String, dynamic>{},
|
properties: <String, dynamic>{},
|
||||||
required: const <String>[],
|
required: const <String>[],
|
||||||
),
|
),
|
||||||
|
|
@ -708,4 +797,48 @@ class ToolLoopService {
|
||||||
|
|
||||||
return "The model completed the turn without returning visible text.";
|
return "The model completed the turn without returning visible text.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Converts a tool result string to the appropriate API content format.
|
||||||
|
// IMAGE_BLOCK:<mediaType>:<base64> → image block list
|
||||||
|
// anything else → plain string
|
||||||
|
dynamic _buildToolResultContent(String result) {
|
||||||
|
const prefix = "IMAGE_BLOCK:";
|
||||||
|
if (!result.startsWith(prefix)) return result;
|
||||||
|
|
||||||
|
// may contain multiple IMAGE_BLOCK lines (e.g. notebook with output images)
|
||||||
|
final lines = result.split("\n");
|
||||||
|
final blocks = <Map<String, dynamic>>[];
|
||||||
|
final textBuf = StringBuffer();
|
||||||
|
|
||||||
|
for (final line in lines) {
|
||||||
|
if (line.startsWith(prefix)) {
|
||||||
|
if (textBuf.isNotEmpty) {
|
||||||
|
blocks.add({"type": "text", "text": textBuf.toString().trim()});
|
||||||
|
textBuf.clear();
|
||||||
|
}
|
||||||
|
final parts = line.substring(prefix.length).split(":");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
final mediaType = parts[0];
|
||||||
|
final b64 = parts.sublist(1).join(":");
|
||||||
|
blocks.add({
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {"url": "data:$mediaType;base64,$b64"},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (textBuf.isNotEmpty) textBuf.write("\n");
|
||||||
|
textBuf.write(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textBuf.isNotEmpty) {
|
||||||
|
blocks.add({"type": "text", "text": textBuf.toString().trim()});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blocks.isEmpty) return result;
|
||||||
|
if (blocks.length == 1 && blocks[0]["type"] == "image_url") {
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
130
lib/src/commands/_shared.dart
Normal file
130
lib/src/commands/_shared.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
import '../local_state.dart';
|
||||||
|
import '../session/conversation_history.dart';
|
||||||
|
import '../local_state.dart' show joinPath;
|
||||||
|
import '../utils/uuid_utils.dart';
|
||||||
|
|
||||||
|
// shared singleton history for the current run
|
||||||
|
final history = ConversationHistory();
|
||||||
|
|
||||||
|
String makeSessionId() => generateUuid();
|
||||||
|
|
||||||
|
const commonHelpArgs = <String>['help', '-h', '--help'];
|
||||||
|
const commonInfoArgs = <String>['current', 'info', 'show', 'status'];
|
||||||
|
|
||||||
|
const defaultStatuslinePrompt =
|
||||||
|
'Configure my statusLine from my shell PS1 configuration';
|
||||||
|
|
||||||
|
const max20xTier = 'default_claude_max_20x';
|
||||||
|
|
||||||
|
const modelAliases = <String>[
|
||||||
|
'best',
|
||||||
|
'haiku',
|
||||||
|
'opus',
|
||||||
|
'opus[1m]',
|
||||||
|
'opusplan',
|
||||||
|
'sonnet',
|
||||||
|
'sonnet[1m]',
|
||||||
|
];
|
||||||
|
|
||||||
|
String formatDuration(Duration duration) {
|
||||||
|
String twoDigits(int value) => value.toString().padLeft(2, '0');
|
||||||
|
final hours = twoDigits(duration.inHours);
|
||||||
|
final minutes = twoDigits(duration.inMinutes.remainder(60));
|
||||||
|
final seconds = twoDigits(duration.inSeconds.remainder(60));
|
||||||
|
return '$hours:$minutes:$seconds';
|
||||||
|
}
|
||||||
|
|
||||||
|
String renderModelSetting(String rawModel) {
|
||||||
|
switch (rawModel.toLowerCase()) {
|
||||||
|
case 'best':
|
||||||
|
return 'Best available';
|
||||||
|
case 'haiku':
|
||||||
|
return 'Claude Haiku';
|
||||||
|
case 'opus':
|
||||||
|
return 'Claude Opus';
|
||||||
|
case 'opus[1m]':
|
||||||
|
return 'Claude Opus [1m]';
|
||||||
|
case 'opusplan':
|
||||||
|
return 'Opus plan mode';
|
||||||
|
case 'sonnet':
|
||||||
|
return 'Claude Sonnet';
|
||||||
|
case 'sonnet[1m]':
|
||||||
|
return 'Claude Sonnet [1m]';
|
||||||
|
default:
|
||||||
|
return rawModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String resolveCurrentModelSetting(CommandContext context) {
|
||||||
|
final configured = context.settingsStore.settings.model;
|
||||||
|
if (configured != null && configured.trim().isNotEmpty) {
|
||||||
|
return configured.trim();
|
||||||
|
}
|
||||||
|
return "anthropic/claude-3.5-sonnet";
|
||||||
|
}
|
||||||
|
|
||||||
|
String showCurrentEffort(CommandContext context) {
|
||||||
|
final envLevel = getEffortEnvLevelOverride();
|
||||||
|
final effectiveValue = isEffortEnvClearOverride()
|
||||||
|
? null
|
||||||
|
: envLevel ?? context.sessionState.effortValue;
|
||||||
|
|
||||||
|
if (effectiveValue == null || effectiveValue.isEmpty) {
|
||||||
|
return 'Effort level: auto (currently high)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Current effort level: $effectiveValue (${getEffortDescription(effectiveValue)})';
|
||||||
|
}
|
||||||
|
|
||||||
|
String getEffortDescription(String effort) {
|
||||||
|
switch (effort) {
|
||||||
|
case 'low':
|
||||||
|
return 'Quick, straightforward implementation with minimal overhead';
|
||||||
|
case 'medium':
|
||||||
|
return 'Balanced approach with standard implementation and testing';
|
||||||
|
case 'high':
|
||||||
|
return 'Comprehensive implementation with extensive testing and documentation';
|
||||||
|
case 'max':
|
||||||
|
return 'Maximum capability with deepest reasoning (Opus 4.6 only)';
|
||||||
|
default:
|
||||||
|
return 'Balanced approach with standard implementation and testing';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getApplicableEffortEnvRaw() {
|
||||||
|
final raw = Platform.environment['CLAUDE_CODE_EFFORT_LEVEL'];
|
||||||
|
if (raw == null || raw.trim().isEmpty) return null;
|
||||||
|
|
||||||
|
final normalized = raw.trim().toLowerCase();
|
||||||
|
if (isEffortEnvClearOverride() || supportedEffortLevels.contains(normalized)) {
|
||||||
|
return raw.trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getEffortEnvLevelOverride() {
|
||||||
|
final raw = getApplicableEffortEnvRaw();
|
||||||
|
if (raw == null) return null;
|
||||||
|
|
||||||
|
final normalized = raw.toLowerCase();
|
||||||
|
if (supportedEffortLevels.contains(normalized)) return normalized;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEffortEnvClearOverride() {
|
||||||
|
final raw = Platform.environment['CLAUDE_CODE_EFFORT_LEVEL'];
|
||||||
|
if (raw == null || raw.trim().isEmpty) return false;
|
||||||
|
final normalized = raw.trim().toLowerCase();
|
||||||
|
return normalized == 'auto' || normalized == 'unset';
|
||||||
|
}
|
||||||
|
|
||||||
|
String buildStatuslineAgentInstruction(String prompt) =>
|
||||||
|
'Create an Agent with subagent_type "statusline-setup" and the prompt "$prompt"';
|
||||||
|
|
||||||
|
String homeDir() =>
|
||||||
|
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'] ?? '';
|
||||||
|
|
||||||
|
String theAgencyHome() => joinPath(homeDir(), '.the_agency');
|
||||||
43
lib/src/commands/add_dir.dart
Normal file
43
lib/src/commands/add_dir.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
import '../local_state.dart' show joinPath;
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final dirArg = args.join(" ").trim();
|
||||||
|
|
||||||
|
if (dirArg.isEmpty || commonHelpArgs.contains(dirArg.toLowerCase())) {
|
||||||
|
context.writeLine(
|
||||||
|
'Usage: /add-dir <path>\n\n'
|
||||||
|
'Add a directory to the current session workspace.\n'
|
||||||
|
'Claude will be able to read and edit files in the added directory.',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final resolved = dirArg.startsWith('/')
|
||||||
|
? dirArg
|
||||||
|
: joinPath(context.workingDirectory, dirArg);
|
||||||
|
|
||||||
|
final dir = Directory(resolved);
|
||||||
|
if (!await dir.exists()) {
|
||||||
|
context.writeError('Directory does not exist: $resolved');
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.sessionState.additionalDirectories.contains(resolved)) {
|
||||||
|
context.writeLine('Directory already in workspace: $resolved');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.sessionState.additionalDirectories.add(resolved);
|
||||||
|
context.writeLine('Added directory to workspace: $resolved');
|
||||||
|
context.writeLine('Active workspace directories:');
|
||||||
|
context.writeLine(' ${context.workingDirectory} (primary)');
|
||||||
|
for (final d in context.sessionState.additionalDirectories) {
|
||||||
|
context.writeLine(' $d');
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
40
lib/src/commands/advisor.dart
Normal file
40
lib/src/commands/advisor.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final arg = args.join(' ').trim().toLowerCase();
|
||||||
|
|
||||||
|
if (arg.isEmpty) {
|
||||||
|
final current = context.sessionState.advisorModel
|
||||||
|
?? context.settingsStore.settings.advisorModel;
|
||||||
|
if (current == null) {
|
||||||
|
context.writeLine(
|
||||||
|
'Advisor: not set\nUse "/advisor <model>" to enable (e.g. "/advisor opus").',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
context.writeLine(
|
||||||
|
'Advisor: $current\nUse "/advisor unset" to disable or "/advisor <model>" to change.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg == 'unset' || arg == 'off') {
|
||||||
|
final prev = context.sessionState.advisorModel
|
||||||
|
?? context.settingsStore.settings.advisorModel;
|
||||||
|
context.sessionState.advisorModel = null;
|
||||||
|
await context.settingsStore.update((s) => s.copyWith(advisorModel: null));
|
||||||
|
context.writeLine(prev != null ? 'Advisor disabled (was $prev).' : 'Advisor already unset.');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commonHelpArgs.contains(arg)) {
|
||||||
|
context.writeLine('Usage: /advisor [<model>|off]\n\nSet the advisor model for the session.');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.sessionState.advisorModel = arg;
|
||||||
|
await context.settingsStore.update((s) => s.copyWith(advisorModel: arg));
|
||||||
|
context.writeLine('Advisor set to $arg.');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
14
lib/src/commands/agents.dart
Normal file
14
lib/src/commands/agents.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine("Agents");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("The interactive agents manager is not ported to the Dart CLI.");
|
||||||
|
context.writeLine(
|
||||||
|
"In the legacy CLI this shows a menu to configure which tools agents can use.",
|
||||||
|
);
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("Available agent tools are determined by your permission settings.");
|
||||||
|
context.writeLine("Use /permissions to manage tool access rules.");
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
33
lib/src/commands/attach.dart
Normal file
33
lib/src/commands/attach.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
import '../daemon/daemon_manager.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
if (args.isEmpty) {
|
||||||
|
context.writeLine("Usage: /attach <session-id>");
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
final id = args[0];
|
||||||
|
final mgr = DaemonManager();
|
||||||
|
|
||||||
|
final rec = await mgr.loadRecord(id);
|
||||||
|
if (rec == null) {
|
||||||
|
context.writeLine("Session not found: $id");
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
final desc = await mgr.describeSession(id);
|
||||||
|
if (desc != null) {
|
||||||
|
context.writeLine(desc);
|
||||||
|
context.writeLine("--- streaming logs (Ctrl-C to stop) ---");
|
||||||
|
context.writeLine("");
|
||||||
|
}
|
||||||
|
|
||||||
|
await for (final chunk in mgr.streamLogs(id)) {
|
||||||
|
stdout.write(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult(exitCode: 0);
|
||||||
|
}
|
||||||
41
lib/src/commands/branch.dart
Normal file
41
lib/src/commands/branch.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../session/session_store.dart';
|
||||||
|
import '../session/session_types.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final customTitle = args.join(" ").trim();
|
||||||
|
|
||||||
|
if (!history.hasSession) {
|
||||||
|
context.writeLine("No active session to branch from.");
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
final src = history.session!;
|
||||||
|
final now = DateTime.now().toUtc();
|
||||||
|
final newId = makeSessionId();
|
||||||
|
final branchName = customTitle.isNotEmpty ? customTitle : "${src.name} (branch)";
|
||||||
|
|
||||||
|
final forked = ConversationSession(
|
||||||
|
id: newId,
|
||||||
|
name: branchName,
|
||||||
|
created: now,
|
||||||
|
updated: now,
|
||||||
|
messages: src.messages.map((m) => Message(
|
||||||
|
role: m.role,
|
||||||
|
content: m.content,
|
||||||
|
timestamp: m.timestamp,
|
||||||
|
tokens: m.tokens,
|
||||||
|
)).toList(),
|
||||||
|
model: src.model,
|
||||||
|
);
|
||||||
|
|
||||||
|
await SessionStore.instance.saveSession(forked);
|
||||||
|
history.setSession(forked);
|
||||||
|
context.sessionState.sessionName = branchName;
|
||||||
|
|
||||||
|
context.writeLine('Branched into new session: "$branchName"');
|
||||||
|
context.writeLine("New session ID: $newId");
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
8
lib/src/commands/brief.dart
Normal file
8
lib/src/commands/brief.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final newState = !context.sessionState.briefModeEnabled;
|
||||||
|
context.sessionState.briefModeEnabled = newState;
|
||||||
|
context.writeLine(newState ? 'Brief-only mode enabled' : 'Brief-only mode disabled');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
27
lib/src/commands/btw.dart
Normal file
27
lib/src/commands/btw.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final question = args.join(" ").trim();
|
||||||
|
|
||||||
|
context.writeLine("Side Question (btw)");
|
||||||
|
context.writeLine("");
|
||||||
|
|
||||||
|
if (question.isEmpty) {
|
||||||
|
context.writeLine("Usage: /btw <question>");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine(
|
||||||
|
"Ask a quick side question without affecting the main conversation context.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
context.writeLine("Question: $question");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine(
|
||||||
|
"Side question mode is not fully ported - this requires a live model session.",
|
||||||
|
);
|
||||||
|
context.writeLine(
|
||||||
|
"The question would normally be answered without adding to the main context.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
17
lib/src/commands/bughunter.dart
Normal file
17
lib/src/commands/bughunter.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArg = args.join(' ').trim().toLowerCase();
|
||||||
|
|
||||||
|
if (rawArg == 'status' || rawArg == 'current') {
|
||||||
|
context.writeLine(
|
||||||
|
context.sessionState.bughunterMode ? 'Bug hunter mode: ON' : 'Bug hunter mode: OFF',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final newState = !context.sessionState.bughunterMode;
|
||||||
|
context.sessionState.bughunterMode = newState;
|
||||||
|
context.writeLine(newState ? 'Bug hunter mode: ON' : 'Bug hunter mode: OFF');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
17
lib/src/commands/chrome.dart
Normal file
17
lib/src/commands/chrome.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
const extensionUrl = 'https://claude.ai/chrome';
|
||||||
|
const permissionsUrl = 'https://clau.de/chrome/permissions';
|
||||||
|
|
||||||
|
context.writeLine('Claude in Chrome (Beta)');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Lets Claude access your browser context when you\'re on claude.ai.');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Extension: $extensionUrl');
|
||||||
|
context.writeLine('Permissions: $permissionsUrl');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('The interactive Chrome extension settings panel is not ported to the Dart runtime.');
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
6
lib/src/commands/clear.dart
Normal file
6
lib/src/commands/clear.dart
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.out.write('\x1B[2J\x1B[H');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
29
lib/src/commands/color.dart
Normal file
29
lib/src/commands/color.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../local_state.dart';
|
||||||
|
|
||||||
|
const _resetAliases = <String>['default', 'reset', 'none', 'gray', 'grey'];
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim().toLowerCase();
|
||||||
|
if (rawArgs.isEmpty) {
|
||||||
|
final colorList = supportedAgentColors.join(', ');
|
||||||
|
context.writeLine('Please provide a color. Available colors: $colorList, default');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_resetAliases.contains(rawArgs)) {
|
||||||
|
context.sessionState.sessionColor = null;
|
||||||
|
context.writeLine('Session color reset to default');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedAgentColors.contains(rawArgs)) {
|
||||||
|
final colorList = supportedAgentColors.join(', ');
|
||||||
|
context.writeLine('Invalid color "$rawArgs". Available colors: $colorList, default');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.sessionState.sessionColor = rawArgs;
|
||||||
|
context.writeLine('Session color set to: $rawArgs');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
37
lib/src/commands/commit.dart
Normal file
37
lib/src/commands/commit.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine('Create git commit');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(
|
||||||
|
'This is a prompt-type command. In the legacy CLI it sends the current git diff'
|
||||||
|
' to the model and asks it to stage and create a commit.',
|
||||||
|
);
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final statusResult = await Process.run(
|
||||||
|
'git',
|
||||||
|
['status', '--short'],
|
||||||
|
workingDirectory: context.workingDirectory,
|
||||||
|
);
|
||||||
|
final statusOut = (statusResult.stdout as String).trim();
|
||||||
|
if (statusOut.isEmpty) {
|
||||||
|
context.writeLine('git status: nothing to commit, working tree clean');
|
||||||
|
} else {
|
||||||
|
context.writeLine('Current changes:');
|
||||||
|
context.writeLine(statusOut);
|
||||||
|
}
|
||||||
|
} on ProcessException {
|
||||||
|
context.writeLine('(could not run git status)');
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(
|
||||||
|
'Run `git add` and `git commit` manually, or use the legacy CLI for AI-assisted commits.',
|
||||||
|
);
|
||||||
|
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
46
lib/src/commands/commit_push_pr.dart
Normal file
46
lib/src/commands/commit_push_pr.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine("Commit, push, and open a PR");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine(
|
||||||
|
"This is a prompt-type command. In the legacy CLI it uses the AI model to:\n"
|
||||||
|
" 1. Create a branch (if on main)\n"
|
||||||
|
" 2. Stage and commit all changes\n"
|
||||||
|
" 3. Push to origin\n"
|
||||||
|
" 4. Create or update a GitHub PR via gh",
|
||||||
|
);
|
||||||
|
context.writeLine("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final branchResult = await Process.run(
|
||||||
|
"git", ["branch", "--show-current"],
|
||||||
|
workingDirectory: context.workingDirectory,
|
||||||
|
);
|
||||||
|
final branch = (branchResult.stdout as String).trim();
|
||||||
|
if (branch.isNotEmpty) context.writeLine("Current branch: $branch");
|
||||||
|
|
||||||
|
final statusResult = await Process.run(
|
||||||
|
"git", ["status", "--short"],
|
||||||
|
workingDirectory: context.workingDirectory,
|
||||||
|
);
|
||||||
|
final status = (statusResult.stdout as String).trim();
|
||||||
|
if (status.isEmpty) {
|
||||||
|
context.writeLine("Nothing to commit (working tree clean).");
|
||||||
|
} else {
|
||||||
|
context.writeLine("Uncommitted changes:");
|
||||||
|
context.writeLine(status);
|
||||||
|
}
|
||||||
|
} on ProcessException {
|
||||||
|
context.writeLine("(could not run git commands)");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine(
|
||||||
|
"Run `git commit && git push && gh pr create` manually, or use the legacy CLI for AI-assisted PR creation.",
|
||||||
|
);
|
||||||
|
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
16
lib/src/commands/compact.dart
Normal file
16
lib/src/commands/compact.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final instructions = args.join(' ').trim();
|
||||||
|
context.writeLine(
|
||||||
|
'Compact conversation: message history is not available in the Dart CLI runtime yet.',
|
||||||
|
);
|
||||||
|
if (instructions.isNotEmpty) {
|
||||||
|
context.writeLine('Custom instructions provided: "$instructions"');
|
||||||
|
}
|
||||||
|
context.writeLine(
|
||||||
|
'\nIn the legacy CLI this summarizes all messages and replaces them with a summary,'
|
||||||
|
' keeping the context window fresh.',
|
||||||
|
);
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
36
lib/src/commands/config.dart
Normal file
36
lib/src/commands/config.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
const _jsonEncoder = JsonEncoder.withIndent(' ');
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim().toLowerCase();
|
||||||
|
if (rawArgs == 'path' || rawArgs == 'open') {
|
||||||
|
context.writeLine(context.settingsStore.path);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawArgs.isNotEmpty && rawArgs != 'show') {
|
||||||
|
context.writeLine('Usage: /config [show|path]');
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Config file: ${context.settingsStore.path}');
|
||||||
|
context.writeLine('Runtime state: ${context.runtimeStateStore.path}');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Settings:');
|
||||||
|
context.writeLine(_jsonEncoder.convert(context.settingsStore.settings.toJson()));
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Runtime state:');
|
||||||
|
context.writeLine(_jsonEncoder.convert(context.runtimeStateStore.state.toJson()));
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Session state:');
|
||||||
|
context.writeLine(' planModeEnabled: ${context.sessionState.planModeEnabled}');
|
||||||
|
context.writeLine(' sessionColor: ${context.sessionState.sessionColor ?? 'default'}');
|
||||||
|
context.writeLine(' effortValue: ${context.sessionState.effortValue ?? 'auto'}');
|
||||||
|
context.writeLine(' planFilePath: ${context.sessionState.planFilePath}');
|
||||||
|
context.writeLine(' commandsExecuted: ${context.sessionState.commandsExecuted}');
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
20
lib/src/commands/context.dart
Normal file
20
lib/src/commands/context.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final elapsed = DateTime.now().toUtc().difference(context.sessionState.startedAt);
|
||||||
|
|
||||||
|
context.writeLine('Context window usage');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(' Token accounting is not ported to the Dart runtime yet.');
|
||||||
|
context.writeLine(' In the legacy CLI this shows a colored grid of used vs available context.');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(' Session uptime: ${formatDuration(elapsed)}');
|
||||||
|
context.writeLine(' Commands run: ${context.sessionState.commandsExecuted}');
|
||||||
|
context.writeLine(' Working dir: ${context.workingDirectory}');
|
||||||
|
if (context.sessionState.additionalDirectories.isNotEmpty) {
|
||||||
|
context.writeLine(' Extra dirs: ${context.sessionState.additionalDirectories.length}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
33
lib/src/commands/copy.dart
Normal file
33
lib/src/commands/copy.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
if (!history.hasSession) {
|
||||||
|
context.writeLine("No active session - nothing to copy.");
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
final msgs = history.getMessages();
|
||||||
|
final assistantMsgs = msgs.where((m) => m.role == "assistant").toList();
|
||||||
|
|
||||||
|
if (assistantMsgs.isEmpty) {
|
||||||
|
context.writeLine("No assistant messages in the current session.");
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = assistantMsgs.length - 1;
|
||||||
|
if (args.isNotEmpty) {
|
||||||
|
final parsed = int.tryParse(args.first.trim());
|
||||||
|
if (parsed != null && parsed > 0 && parsed <= assistantMsgs.length) {
|
||||||
|
idx = assistantMsgs.length - parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final msg = assistantMsgs[idx];
|
||||||
|
context.writeLine(msg.content);
|
||||||
|
context.writeLine(
|
||||||
|
"\n(Note: clipboard copy via OSC 52 is not wired in the Dart runtime - text printed above)",
|
||||||
|
);
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
7
lib/src/commands/cost.dart
Normal file
7
lib/src/commands/cost.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../services/cost_tracker.dart' as costTracker;
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine(costTracker.formatTotalCost());
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
19
lib/src/commands/desktop.dart
Normal file
19
lib/src/commands/desktop.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
if (!Platform.isMacOS && !Platform.isWindows) {
|
||||||
|
context.writeLine('Claude Desktop is only available on macOS and Windows.');
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Claude Desktop');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Opens the current session in the Claude Desktop app.');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Session handoff to Claude Desktop is not ported to the Dart CLI runtime.');
|
||||||
|
context.writeLine('Download Claude Desktop: https://claude.ai/download');
|
||||||
|
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
32
lib/src/commands/diff.dart
Normal file
32
lib/src/commands/diff.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(" ").trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await Process.run(
|
||||||
|
'git',
|
||||||
|
rawArgs.isEmpty ? ['diff'] : ['diff', ...args],
|
||||||
|
workingDirectory: context.workingDirectory,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.exitCode != 0 && (result.stderr as String).isNotEmpty) {
|
||||||
|
context.writeError((result.stderr as String).trim());
|
||||||
|
return CommandResult(exitCode: result.exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
final out = (result.stdout as String).trim();
|
||||||
|
if (out.isEmpty) {
|
||||||
|
context.writeLine("No changes (clean working tree)");
|
||||||
|
} else {
|
||||||
|
context.writeLine(out);
|
||||||
|
}
|
||||||
|
} on ProcessException catch (e) {
|
||||||
|
context.writeError("Could not run git diff: ${e.message}");
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
72
lib/src/commands/doctor.dart
Normal file
72
lib/src/commands/doctor.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
import '../local_state.dart' show joinPath;
|
||||||
|
|
||||||
|
const _largeClaudeMdWarningChars = 40000;
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final workingDirectory = Directory(context.workingDirectory);
|
||||||
|
final legacyRoot = Directory(joinPath(context.workingDirectory, 'old_repo'));
|
||||||
|
final claudeMdFile = File(
|
||||||
|
joinPath(joinPath(context.workingDirectory, '.the_agency'), 'THE_AGENCY.md'),
|
||||||
|
);
|
||||||
|
final hasGit = await Directory(joinPath(context.workingDirectory, '.git')).exists();
|
||||||
|
final hasLegacyPackageManifest =
|
||||||
|
await File(joinPath(legacyRoot.path, 'package.json')).exists() ||
|
||||||
|
await File(joinPath(legacyRoot.path, 'tsconfig.json')).exists();
|
||||||
|
final configFile = File(context.settingsStore.path);
|
||||||
|
final runtimeFile = File(context.runtimeStateStore.path);
|
||||||
|
|
||||||
|
context.writeLine('Doctor');
|
||||||
|
context.writeLine(
|
||||||
|
'Runtime: Dart ${Platform.version.split(' ').first} on ${Platform.operatingSystem}',
|
||||||
|
);
|
||||||
|
context.writeLine('Working directory: ${workingDirectory.path}');
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
context.writeLine(
|
||||||
|
'[ok] settings: ${await configFile.exists() ? context.settingsStore.path : 'missing'}',
|
||||||
|
);
|
||||||
|
context.writeLine(
|
||||||
|
'[ok] runtime state: ${await runtimeFile.exists() ? context.runtimeStateStore.path : 'missing'}',
|
||||||
|
);
|
||||||
|
context.writeLine(
|
||||||
|
'[${await legacyRoot.exists() ? 'ok' : 'warn'}] legacy source root: ${legacyRoot.path}',
|
||||||
|
);
|
||||||
|
context.writeLine(
|
||||||
|
'[${hasGit ? 'ok' : 'warn'}] git repository: ${hasGit ? 'detected' : 'not detected'}',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (await claudeMdFile.exists()) {
|
||||||
|
final length = await claudeMdFile.length();
|
||||||
|
final level = length > _largeClaudeMdWarningChars ? 'warn' : 'ok';
|
||||||
|
context.writeLine(
|
||||||
|
'[$level] THE_AGENCY.md: ${claudeMdFile.path} (${length.toString()} bytes)',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
context.writeLine('[warn] THE_AGENCY.md: not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine(
|
||||||
|
'[${hasLegacyPackageManifest ? 'ok' : 'warn'}] legacy manifests: ${hasLegacyPackageManifest ? 'detected' : 'old_repo has no package.json or tsconfig.json at its root'}',
|
||||||
|
);
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Notes:');
|
||||||
|
if (!hasGit) {
|
||||||
|
context.writeLine(' - This workspace is not currently inside a git repository.');
|
||||||
|
}
|
||||||
|
if (!hasLegacyPackageManifest) {
|
||||||
|
context.writeLine(
|
||||||
|
' - Exact legacy runtime reproduction is harder because old_repo lacks a checked-in package manifest.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!await legacyRoot.exists()) {
|
||||||
|
context.writeLine(' - old_repo is missing, so legacy source parity checks cannot run.');
|
||||||
|
}
|
||||||
|
if (hasGit && hasLegacyPackageManifest && await legacyRoot.exists()) {
|
||||||
|
context.writeLine(' - No obvious environment blockers detected.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
84
lib/src/commands/effort.dart
Normal file
84
lib/src/commands/effort.dart
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../local_state.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
const _helpText =
|
||||||
|
'Usage: /effort [low|medium|high|max|auto]\n\n'
|
||||||
|
'Effort levels:\n'
|
||||||
|
'- low: Quick, straightforward implementation\n'
|
||||||
|
'- medium: Balanced approach with standard testing\n'
|
||||||
|
'- high: Comprehensive implementation with extensive testing\n'
|
||||||
|
'- max: Maximum capability with deepest reasoning (Opus 4.6 only)\n'
|
||||||
|
'- auto: Use the default effort level for your model';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim();
|
||||||
|
if (commonHelpArgs.contains(rawArgs)) {
|
||||||
|
context.writeLine(_helpText);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawArgs.isEmpty || rawArgs == 'current' || rawArgs == 'status') {
|
||||||
|
context.writeLine(showCurrentEffort(context));
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalized = rawArgs.toLowerCase();
|
||||||
|
if (normalized == 'auto' || normalized == 'unset') {
|
||||||
|
context.sessionState.effortValue = null;
|
||||||
|
await context.settingsStore.update(
|
||||||
|
(settings) => settings.copyWith(effortLevel: null),
|
||||||
|
);
|
||||||
|
|
||||||
|
final applicableEnvRaw = getApplicableEffortEnvRaw();
|
||||||
|
if (applicableEnvRaw != null && !isEffortEnvClearOverride()) {
|
||||||
|
context.writeLine(
|
||||||
|
'Cleared effort from settings, but CLAUDE_CODE_EFFORT_LEVEL=$applicableEnvRaw still controls this session',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Effort level set to auto');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedEffortLevels.contains(normalized)) {
|
||||||
|
context.writeLine(
|
||||||
|
'Invalid argument: $rawArgs. Valid options are: low, medium, high, max, auto',
|
||||||
|
);
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.sessionState.effortValue = normalized;
|
||||||
|
if (normalized == 'max') {
|
||||||
|
final applicableEnvRaw = getApplicableEffortEnvRaw();
|
||||||
|
if (applicableEnvRaw != null &&
|
||||||
|
getEffortEnvLevelOverride() != normalized) {
|
||||||
|
context.writeLine(
|
||||||
|
'Not applied: CLAUDE_CODE_EFFORT_LEVEL=$applicableEnvRaw overrides effort this session, and $normalized is session-only (nothing saved)',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine(
|
||||||
|
'Set effort level to $normalized (this session only): ${getEffortDescription(normalized)}',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.settingsStore.update(
|
||||||
|
(settings) => settings.copyWith(effortLevel: normalized),
|
||||||
|
);
|
||||||
|
final applicableEnvRaw = getApplicableEffortEnvRaw();
|
||||||
|
if (applicableEnvRaw != null && getEffortEnvLevelOverride() != normalized) {
|
||||||
|
context.writeLine(
|
||||||
|
'CLAUDE_CODE_EFFORT_LEVEL=$applicableEnvRaw overrides this session — clear it and $normalized takes over',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine(
|
||||||
|
'Set effort level to $normalized: ${getEffortDescription(normalized)}',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
32
lib/src/commands/env.dart
Normal file
32
lib/src/commands/env.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
const _relevantKeys = <String>[
|
||||||
|
'OPENROUTER_API_KEY',
|
||||||
|
'CLAUDE_CODE_EFFORT_LEVEL',
|
||||||
|
'CLAUDE_CODE_SKIP_PERMISSIONS_CHECK',
|
||||||
|
'EDITOR',
|
||||||
|
'VISUAL',
|
||||||
|
'HOME',
|
||||||
|
'PATH',
|
||||||
|
'SHELL',
|
||||||
|
'USER',
|
||||||
|
'USER_TYPE',
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine("Environment variables:");
|
||||||
|
for (final key in _relevantKeys) {
|
||||||
|
final val = Platform.environment[key];
|
||||||
|
if (val != null) {
|
||||||
|
final display = key.contains('KEY') && val.length > 8
|
||||||
|
? '${val.substring(0, 4)}...${val.substring(val.length - 4)}'
|
||||||
|
: val;
|
||||||
|
context.writeLine(" $key=$display");
|
||||||
|
} else {
|
||||||
|
context.writeLine(" $key=(unset)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
5
lib/src/commands/exit.dart
Normal file
5
lib/src/commands/exit.dart
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
return const CommandResult(exitRepl: true);
|
||||||
|
}
|
||||||
34
lib/src/commands/export.dart
Normal file
34
lib/src/commands/export.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final filename = args.join(" ").trim();
|
||||||
|
|
||||||
|
if (!history.hasSession) {
|
||||||
|
context.writeLine("No active session to export.");
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
final sess = history.session!;
|
||||||
|
final isJson = filename.endsWith(".json");
|
||||||
|
final content = isJson ? history.exportToJson() : history.exportToText();
|
||||||
|
|
||||||
|
if (filename.isEmpty) {
|
||||||
|
context.writeLine(content);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final file = File(filename);
|
||||||
|
await file.parent.create(recursive: true);
|
||||||
|
await file.writeAsString(content);
|
||||||
|
context.writeLine('Exported ${sess.messageCount} messages to: $filename');
|
||||||
|
} catch (e) {
|
||||||
|
context.writeError("Failed to write export file: $e");
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
31
lib/src/commands/fast.dart
Normal file
31
lib/src/commands/fast.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim().toLowerCase();
|
||||||
|
if (commonHelpArgs.contains(rawArgs)) {
|
||||||
|
context.writeLine('Usage: /fast [on|off|status]');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawArgs.isEmpty || rawArgs == 'status' || rawArgs == 'current') {
|
||||||
|
context.writeLine(
|
||||||
|
context.settingsStore.settings.fastMode ? 'Fast mode ON' : 'Fast mode OFF',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawArgs != 'on' && rawArgs != 'off') {
|
||||||
|
context.writeLine(
|
||||||
|
'Invalid argument: $rawArgs. Valid options are: on, off, status',
|
||||||
|
);
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
final enabled = rawArgs == 'on';
|
||||||
|
await context.settingsStore.update(
|
||||||
|
(settings) => settings.copyWith(fastMode: enabled),
|
||||||
|
);
|
||||||
|
context.writeLine(enabled ? 'Fast mode ON' : 'Fast mode OFF');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
19
lib/src/commands/feedback.dart
Normal file
19
lib/src/commands/feedback.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final report = args.join(' ').trim();
|
||||||
|
const feedbackUrl = 'https://github.com/anthropics/claude-code/issues/new';
|
||||||
|
|
||||||
|
context.writeLine('Submit Feedback / Bug Report');
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
if (report.isNotEmpty) {
|
||||||
|
context.writeLine('Your report: "$report"');
|
||||||
|
context.writeLine('');
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Interactive feedback submission is not ported to the Dart CLI runtime yet.');
|
||||||
|
context.writeLine('Please open an issue at: $feedbackUrl');
|
||||||
|
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
9
lib/src/commands/files.dart
Normal file
9
lib/src/commands/files.dart
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine("No files in context");
|
||||||
|
context.writeLine(
|
||||||
|
"(Note: file context tracking is not yet ported to the Dart CLI runtime)",
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
73
lib/src/commands/help.dart
Normal file
73
lib/src/commands/help.dart
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final requestedCommand = args.isEmpty
|
||||||
|
? null
|
||||||
|
: args.first.startsWith('/')
|
||||||
|
? args.first.substring(1)
|
||||||
|
: args.first;
|
||||||
|
|
||||||
|
if (requestedCommand != null) {
|
||||||
|
final ported = context.catalog.findPorted(requestedCommand, InvocationSurface.both);
|
||||||
|
final legacy = context.catalog.findLegacy(requestedCommand, InvocationSurface.both);
|
||||||
|
final reserved = context.catalog.findReservedTopLevel(requestedCommand);
|
||||||
|
final descriptor = ported ?? legacy ?? reserved;
|
||||||
|
|
||||||
|
if (descriptor == null) {
|
||||||
|
context.writeError('No known command named "$requestedCommand".');
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeCommandDetails(context, descriptor);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Usage:');
|
||||||
|
context.writeLine(' clawd_code Start the interactive CLI');
|
||||||
|
context.writeLine(' clawd_code --help Show help');
|
||||||
|
context.writeLine(' clawd_code --version Print version');
|
||||||
|
context.writeLine(' clawd_code <entrypoint> Run a known top-level legacy entrypoint');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Ported commands:');
|
||||||
|
for (final command in context.catalog.portedCommands) {
|
||||||
|
final aliases = command.aliases.isEmpty
|
||||||
|
? ''
|
||||||
|
: ' (aliases: ${command.aliases.join(', ')})';
|
||||||
|
context.writeLine(' /${command.name}$aliases');
|
||||||
|
}
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(
|
||||||
|
'Known legacy slash commands: ${context.catalog.totalKnownSlashCommands}',
|
||||||
|
);
|
||||||
|
context.writeLine(
|
||||||
|
'Reserved top-level legacy entrypoints: ${context.catalog.totalReservedTopLevelEntryPoints}',
|
||||||
|
);
|
||||||
|
context.writeLine(
|
||||||
|
'Remaining unported slash commands: ${context.catalog.unportedSlashCommands.length}',
|
||||||
|
);
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Examples:');
|
||||||
|
context.writeLine(' /status');
|
||||||
|
context.writeLine(' /model opus');
|
||||||
|
context.writeLine(' /permissions allow Bash(npm test)');
|
||||||
|
context.writeLine(' /init preview');
|
||||||
|
context.writeLine(' remote-control');
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeCommandDetails(CommandContext context, LegacyCommandDescriptor descriptor) {
|
||||||
|
context.writeLine('Command: ${descriptor.name}');
|
||||||
|
context.writeLine('Surface: ${descriptor.surface.label}');
|
||||||
|
context.writeLine('Kind: ${descriptor.kind.name}');
|
||||||
|
if (descriptor.aliases.isNotEmpty) {
|
||||||
|
context.writeLine('Aliases: ${descriptor.aliases.join(', ')}');
|
||||||
|
}
|
||||||
|
if (descriptor.description != null && descriptor.description!.isNotEmpty) {
|
||||||
|
context.writeLine('Description: ${descriptor.description!}');
|
||||||
|
}
|
||||||
|
context.writeLine('Legacy source: ${descriptor.legacySourcePath}');
|
||||||
|
if (descriptor.isInferred) {
|
||||||
|
context.writeLine('Metadata note: name inferred from legacy file path.');
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/src/commands/hooks.dart
Normal file
36
lib/src/commands/hooks.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim().toLowerCase();
|
||||||
|
|
||||||
|
if (commonHelpArgs.contains(rawArgs)) {
|
||||||
|
context.writeLine(
|
||||||
|
'Usage: /hooks\n\n'
|
||||||
|
'View and manage hook configurations for tool events.\n'
|
||||||
|
'Hooks are defined in your settings file:\n'
|
||||||
|
' ${context.settingsStore.path}',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final hooks = context.settingsStore.settings.hooks;
|
||||||
|
|
||||||
|
context.writeLine('Hook configurations');
|
||||||
|
context.writeLine('Settings file: ${context.settingsStore.path}');
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
if (hooks == null || hooks.isEmpty) {
|
||||||
|
context.writeLine('No hooks configured.');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Hooks allow you to run scripts when tools are used.');
|
||||||
|
context.writeLine('Add them to your settings.json under the "hooks" key.');
|
||||||
|
} else {
|
||||||
|
context.writeLine('Configured hooks:');
|
||||||
|
for (final entry in hooks.entries) {
|
||||||
|
context.writeLine(' ${entry.key}: ${entry.value}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
35
lib/src/commands/ide.dart
Normal file
35
lib/src/commands/ide.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final termProgram = Platform.environment['TERM_PROGRAM'] ?? '';
|
||||||
|
final askpassMain = Platform.environment['VSCODE_GIT_ASKPASS_MAIN'] ?? '';
|
||||||
|
final path = Platform.environment['PATH'] ?? '';
|
||||||
|
|
||||||
|
String? detectedIde;
|
||||||
|
|
||||||
|
if (termProgram == 'vscode') detectedIde = 'VSCode';
|
||||||
|
else if (termProgram == 'cursor') detectedIde = 'Cursor';
|
||||||
|
else if (termProgram == 'windsurf') detectedIde = 'Windsurf';
|
||||||
|
else if (askpassMain.contains('cursor-server') || path.contains('cursor-server')) detectedIde = 'Cursor (remote)';
|
||||||
|
else if (askpassMain.contains('windsurf-server') || path.contains('windsurf-server')) detectedIde = 'Windsurf (remote)';
|
||||||
|
else if (askpassMain.contains('vscode-server') || path.contains('vscode-server')) detectedIde = 'VSCode (remote)';
|
||||||
|
|
||||||
|
context.writeLine('IDE Integration');
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
if (detectedIde != null) {
|
||||||
|
context.writeLine('Detected IDE: $detectedIde');
|
||||||
|
} else {
|
||||||
|
context.writeLine('No supported IDE detected from environment.');
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Supported integrations: VSCode, Cursor, Windsurf (via the Claude extension)');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('The interactive IDE management panel is not ported to the Dart CLI runtime.');
|
||||||
|
context.writeLine('Install the Claude extension from the marketplace in your IDE.');
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
155
lib/src/commands/init.dart
Normal file
155
lib/src/commands/init.dart
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
import '../local_state.dart' show joinPath;
|
||||||
|
|
||||||
|
const _initHeader =
|
||||||
|
'# THE_AGENCY.md\n\n'
|
||||||
|
'This file provides guidance to The Agency when working with code in this repository.\n';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final command = args.isEmpty ? 'write' : args.first.toLowerCase();
|
||||||
|
final force = args.any((arg) => arg == '--force' || arg == 'force');
|
||||||
|
final agencyDir = joinPath(context.workingDirectory, '.the_agency');
|
||||||
|
final theAgencyMdPath = joinPath(agencyDir, 'THE_AGENCY.md');
|
||||||
|
final targetFile = File(theAgencyMdPath);
|
||||||
|
final draft = await _buildDraft(context.workingDirectory);
|
||||||
|
|
||||||
|
if (command == 'preview' || command == 'show') {
|
||||||
|
context.writeLine(draft);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && await targetFile.exists()) {
|
||||||
|
context.writeLine('THE_AGENCY.md already exists at $theAgencyMdPath');
|
||||||
|
context.writeLine(
|
||||||
|
'Run /init preview to inspect the regenerated draft or /init force to overwrite it.',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Directory(agencyDir).create(recursive: true);
|
||||||
|
await targetFile.writeAsString('$draft\n');
|
||||||
|
context.writeLine('Wrote $theAgencyMdPath');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _buildDraft(String workingDirectory) async {
|
||||||
|
final commands = await _collectDetectedCommands(workingDirectory);
|
||||||
|
final architecture = await _collectArchitectureNotes(workingDirectory);
|
||||||
|
final buffer = StringBuffer()..write(_initHeader);
|
||||||
|
|
||||||
|
if (commands.isNotEmpty) {
|
||||||
|
buffer.writeln();
|
||||||
|
buffer.writeln('## Common Commands');
|
||||||
|
for (final command in commands) {
|
||||||
|
buffer.writeln('- `$command`');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (architecture.isNotEmpty) {
|
||||||
|
buffer.writeln();
|
||||||
|
buffer.writeln('## Architecture');
|
||||||
|
for (final note in architecture) {
|
||||||
|
buffer.writeln('- $note');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.writeln();
|
||||||
|
buffer.writeln('## Notes');
|
||||||
|
buffer.writeln(
|
||||||
|
'- Preserve the Dart CLI surface while using `old_repo/` as the legacy behavior reference during migration work.',
|
||||||
|
);
|
||||||
|
buffer.writeln(
|
||||||
|
'- Prefer concise, targeted changes over broad rewrites unless a command or runtime subsystem is being ported intentionally.',
|
||||||
|
);
|
||||||
|
|
||||||
|
return buffer.toString().trimRight();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> _collectArchitectureNotes(String workingDirectory) async {
|
||||||
|
final notes = <String>[];
|
||||||
|
final binDir = Directory(joinPath(workingDirectory, 'bin'));
|
||||||
|
final libDir = Directory(joinPath(workingDirectory, 'lib'));
|
||||||
|
final oldRepoDir = Directory(joinPath(workingDirectory, 'old_repo'));
|
||||||
|
final testDir = Directory(joinPath(workingDirectory, 'test'));
|
||||||
|
|
||||||
|
if (await binDir.exists()) {
|
||||||
|
notes.add('`bin/` contains the executable entrypoints for the Dart CLI.');
|
||||||
|
}
|
||||||
|
if (await libDir.exists()) {
|
||||||
|
notes.add('`lib/src/` contains the migrated Dart command/runtime implementation.');
|
||||||
|
}
|
||||||
|
if (await oldRepoDir.exists()) {
|
||||||
|
notes.add('`old_repo/` is the legacy TypeScript reference implementation being ported 1:1.');
|
||||||
|
}
|
||||||
|
if (await testDir.exists()) {
|
||||||
|
notes.add('`test/` holds Dart validation coverage for the migrated runtime.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> _collectDetectedCommands(String workingDirectory) async {
|
||||||
|
final commands = <String>[];
|
||||||
|
final pubspecFile = File(joinPath(workingDirectory, 'pubspec.yaml'));
|
||||||
|
final packageJsonFile = File(joinPath(workingDirectory, 'package.json'));
|
||||||
|
final cargoFile = File(joinPath(workingDirectory, 'Cargo.toml'));
|
||||||
|
final goModFile = File(joinPath(workingDirectory, 'go.mod'));
|
||||||
|
final makeFile = File(joinPath(workingDirectory, 'Makefile'));
|
||||||
|
final pomFile = File(joinPath(workingDirectory, 'pom.xml'));
|
||||||
|
final testDir = Directory(joinPath(workingDirectory, 'test'));
|
||||||
|
final binDir = Directory(joinPath(workingDirectory, 'bin'));
|
||||||
|
|
||||||
|
if (await pubspecFile.exists()) {
|
||||||
|
commands.add('dart pub get');
|
||||||
|
commands.add('dart analyze');
|
||||||
|
if (await testDir.exists()) {
|
||||||
|
commands.add('dart test');
|
||||||
|
}
|
||||||
|
if (await binDir.exists()) {
|
||||||
|
final binEntries = await binDir
|
||||||
|
.list()
|
||||||
|
.where((entity) => entity is File)
|
||||||
|
.cast<File>()
|
||||||
|
.toList();
|
||||||
|
if (binEntries.isNotEmpty) {
|
||||||
|
final firstFile = binEntries.first.uri.pathSegments.last;
|
||||||
|
commands.add('dart run bin/$firstFile');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await packageJsonFile.exists()) {
|
||||||
|
commands.addAll(await _extractPackageJsonCommands(packageJsonFile));
|
||||||
|
}
|
||||||
|
if (await cargoFile.exists()) commands.addAll(['cargo build', 'cargo test']);
|
||||||
|
if (await goModFile.exists()) commands.add('go test ./...');
|
||||||
|
if (await pomFile.exists()) commands.add('mvn test');
|
||||||
|
if (await makeFile.exists()) commands.add('make');
|
||||||
|
|
||||||
|
return commands.toSet().toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> _extractPackageJsonCommands(File packageJsonFile) async {
|
||||||
|
try {
|
||||||
|
final raw = await packageJsonFile.readAsString();
|
||||||
|
final decoded = jsonDecode(raw);
|
||||||
|
if (decoded is! Map) return const [];
|
||||||
|
|
||||||
|
final scripts = decoded['scripts'];
|
||||||
|
if (scripts is! Map) return const [];
|
||||||
|
|
||||||
|
final commands = <String>[];
|
||||||
|
for (final entry in scripts.entries) {
|
||||||
|
final key = entry.key.toString();
|
||||||
|
if (key == 'build' || key == 'lint' || key == 'test' || key == 'dev') {
|
||||||
|
commands.add('npm run $key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return commands;
|
||||||
|
} catch (_) {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/src/commands/init_verifiers.dart
Normal file
21
lib/src/commands/init_verifiers.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine("Init verifiers");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine(
|
||||||
|
"This command analyzes your project and creates verifier skills in .claude/skills/.\n"
|
||||||
|
"Verifier skills are used by the Verify agent to automatically verify code changes.",
|
||||||
|
);
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("Supported verifier types:");
|
||||||
|
context.writeLine(" verifier-playwright - for web UIs (Playwright)");
|
||||||
|
context.writeLine(" verifier-cli - for CLI tools (Tmux)");
|
||||||
|
context.writeLine(" verifier-api - for HTTP API services");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine(
|
||||||
|
"In the legacy CLI this runs an AI prompt that detects your project type\n"
|
||||||
|
"and generates the skill file interactively. Use the legacy CLI for full support.",
|
||||||
|
);
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
39
lib/src/commands/install_github_app.dart
Normal file
39
lib/src/commands/install_github_app.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
const docsUrl = 'https://docs.anthropic.com/en/docs/claude-code/github-actions';
|
||||||
|
|
||||||
|
context.writeLine('Install GitHub App');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(
|
||||||
|
'Sets up Claude GitHub Actions for a repository so Claude can review PRs\n'
|
||||||
|
'and respond to issues automatically.',
|
||||||
|
);
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(
|
||||||
|
'The interactive setup wizard (OAuth, repo selection, workflow creation)\n'
|
||||||
|
'is not ported to the Dart CLI runtime yet.',
|
||||||
|
);
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Documentation: $docsUrl');
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ghResult = await Process.run(
|
||||||
|
'gh',
|
||||||
|
['repo', 'view', '--json', 'name,owner', '--jq', '.owner.login + "/" + .name'],
|
||||||
|
workingDirectory: context.workingDirectory,
|
||||||
|
);
|
||||||
|
final repo = (ghResult.stdout as String).trim();
|
||||||
|
if (repo.isNotEmpty) {
|
||||||
|
context.writeLine('Current repo: $repo');
|
||||||
|
context.writeLine('Run: gh workflow list (to see existing workflows)');
|
||||||
|
}
|
||||||
|
} on ProcessException {
|
||||||
|
// gh not installed, thats fine
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
55
lib/src/commands/keybindings.dart
Normal file
55
lib/src/commands/keybindings.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
import '../local_state.dart' show joinPath;
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final keybindingsPath = joinPath(joinPath(homeDir(), '.claude'), 'keybindings.json');
|
||||||
|
|
||||||
|
final rawArgs = args.join(" ").trim().toLowerCase();
|
||||||
|
if (commonHelpArgs.contains(rawArgs)) {
|
||||||
|
context.writeLine(
|
||||||
|
'Usage: /keybindings\n\n'
|
||||||
|
'Opens your keybindings config file in \$EDITOR or \$VISUAL.\n'
|
||||||
|
'File location: $keybindingsPath',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final f = File(keybindingsPath);
|
||||||
|
final existed = await f.exists();
|
||||||
|
|
||||||
|
if (!existed) {
|
||||||
|
await Directory(joinPath(homeDir(), '.claude')).create(recursive: true);
|
||||||
|
await f.writeAsString(
|
||||||
|
'// Claude Code keybindings\n'
|
||||||
|
'// See docs for available actions.\n'
|
||||||
|
'[\n'
|
||||||
|
' // { "key": "ctrl+shift+r", "action": "clearHistory" }\n'
|
||||||
|
']\n',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final editor = Platform.environment['VISUAL'] ?? Platform.environment['EDITOR'];
|
||||||
|
|
||||||
|
if (editor == null) {
|
||||||
|
context.writeLine(
|
||||||
|
'${existed ? 'Keybindings file' : 'Created keybindings file'}: $keybindingsPath',
|
||||||
|
);
|
||||||
|
context.writeLine('Set \$EDITOR or \$VISUAL to open it automatically.');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final proc = await Process.start(editor, [keybindingsPath], mode: ProcessStartMode.inheritStdio);
|
||||||
|
await proc.exitCode;
|
||||||
|
context.writeLine('${existed ? 'Opened' : 'Created and opened'} $keybindingsPath');
|
||||||
|
} on ProcessException catch (e) {
|
||||||
|
context.writeError('Could not open editor ($editor): ${e.message}');
|
||||||
|
context.writeLine('File is at: $keybindingsPath');
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
28
lib/src/commands/kill.dart
Normal file
28
lib/src/commands/kill.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../daemon/daemon_manager.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
if (args.isEmpty) {
|
||||||
|
context.writeLine("Usage: /kill <session-id> [--force]");
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
final id = args[0];
|
||||||
|
final force = args.contains("--force") || args.contains("-f");
|
||||||
|
|
||||||
|
final mgr = DaemonManager();
|
||||||
|
final ok = await mgr.killSession(id, force: force);
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
context.writeLine("Killed session: $id");
|
||||||
|
return const CommandResult(exitCode: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final rec = await mgr.loadRecord(id);
|
||||||
|
if (rec == null) {
|
||||||
|
context.writeLine("Session not found: $id");
|
||||||
|
} else {
|
||||||
|
context.writeLine("Could not kill session $id (status=${rec.status.name})");
|
||||||
|
}
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
59
lib/src/commands/lint.dart
Normal file
59
lib/src/commands/lint.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
import '../local_state.dart' show joinPath;
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim();
|
||||||
|
|
||||||
|
if (commonHelpArgs.contains(rawArgs.toLowerCase())) {
|
||||||
|
context.writeLine('Usage: /lint\n\nRun the project linter.');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final pubspecFile = File(joinPath(context.workingDirectory, 'pubspec.yaml'));
|
||||||
|
final packageJsonFile = File(joinPath(context.workingDirectory, 'package.json'));
|
||||||
|
|
||||||
|
List<String> lintCmd;
|
||||||
|
String label;
|
||||||
|
|
||||||
|
if (await pubspecFile.exists()) {
|
||||||
|
lintCmd = ['dart', 'analyze'];
|
||||||
|
label = 'dart analyze';
|
||||||
|
} else if (await packageJsonFile.exists()) {
|
||||||
|
lintCmd = ['npm', 'run', 'lint'];
|
||||||
|
label = 'npm run lint';
|
||||||
|
} else {
|
||||||
|
context.writeLine('Could not detect project type. No pubspec.yaml or package.json found.');
|
||||||
|
context.writeLine('Run your linter manually.');
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Running: $label');
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await Process.run(
|
||||||
|
lintCmd.first,
|
||||||
|
lintCmd.sublist(1),
|
||||||
|
workingDirectory: context.workingDirectory,
|
||||||
|
);
|
||||||
|
|
||||||
|
final out = (result.stdout as String).trim();
|
||||||
|
final err = (result.stderr as String).trim();
|
||||||
|
|
||||||
|
if (out.isNotEmpty) context.writeLine(out);
|
||||||
|
if (err.isNotEmpty) context.writeError(err);
|
||||||
|
|
||||||
|
if (result.exitCode == 0) {
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('No issues found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommandResult(exitCode: result.exitCode);
|
||||||
|
} on ProcessException catch (e) {
|
||||||
|
context.writeError('Could not run $label: ${e.message}');
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/src/commands/login.dart
Normal file
9
lib/src/commands/login.dart
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine('OpenRouter API key configuration has moved to settings.');
|
||||||
|
context.writeLine(
|
||||||
|
'Set your API key in the Settings panel to authenticate with OpenRouter.',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
6
lib/src/commands/logout.dart
Normal file
6
lib/src/commands/logout.dart
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine('To remove your OpenRouter API key, clear it in Settings.');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
29
lib/src/commands/logs.dart
Normal file
29
lib/src/commands/logs.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../daemon/daemon_manager.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
if (args.isEmpty) {
|
||||||
|
context.writeLine("Usage: /logs <session-id> [--tail N]");
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
final id = args[0];
|
||||||
|
int? tail;
|
||||||
|
|
||||||
|
for (var i = 1; i < args.length - 1; i++) {
|
||||||
|
if (args[i] == "--tail" || args[i] == "-n") {
|
||||||
|
tail = int.tryParse(args[i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final mgr = DaemonManager();
|
||||||
|
final contents = await mgr.readLogs(id, tail: tail);
|
||||||
|
|
||||||
|
if (contents == null) {
|
||||||
|
context.writeLine("No logs found for session: $id");
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine(contents);
|
||||||
|
return const CommandResult(exitCode: 0);
|
||||||
|
}
|
||||||
111
lib/src/commands/mcp.dart
Normal file
111
lib/src/commands/mcp.dart
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim();
|
||||||
|
final parts = rawArgs.split(RegExp(r'\s+'));
|
||||||
|
final sub = parts.isNotEmpty ? parts.first.toLowerCase() : '';
|
||||||
|
|
||||||
|
if (sub == 'help' || rawArgs == '--help' || rawArgs == '-h') {
|
||||||
|
context.writeLine(
|
||||||
|
'Usage: /mcp [list|add <name> <command> [args...]|remove <name>|enable <name>|disable <name>]\n\n'
|
||||||
|
'Manage MCP (Model Context Protocol) servers.',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final servers = Map<String, Map<String, dynamic>>.from(
|
||||||
|
context.settingsStore.settings.mcpServers ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sub.isEmpty || sub == 'list') {
|
||||||
|
context.writeLine('MCP servers');
|
||||||
|
context.writeLine('Settings file: ${context.settingsStore.path}');
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
if (servers.isEmpty) {
|
||||||
|
context.writeLine('No MCP servers configured.');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Use /mcp add <name> <command> to add a server.');
|
||||||
|
} else {
|
||||||
|
for (final entry in servers.entries) {
|
||||||
|
final cfg = entry.value;
|
||||||
|
final cmd = cfg['command'] ?? '(no command)';
|
||||||
|
final disabled = cfg['disabled'] == true;
|
||||||
|
context.writeLine(' ${entry.key}: $cmd${disabled ? ' [disabled]' : ''}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub == 'add') {
|
||||||
|
if (parts.length < 3) {
|
||||||
|
context.writeLine('Usage: /mcp add <name> <command> [args...]');
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
final name = parts[1];
|
||||||
|
final command = parts[2];
|
||||||
|
final cmdArgs = parts.length > 3 ? parts.sublist(3) : <String>[];
|
||||||
|
|
||||||
|
servers[name] = <String, dynamic>{
|
||||||
|
'command': command,
|
||||||
|
if (cmdArgs.isNotEmpty) 'args': cmdArgs,
|
||||||
|
};
|
||||||
|
|
||||||
|
await context.settingsStore.update((s) => s.copyWith(mcpServers: servers));
|
||||||
|
context.writeLine('Added MCP server "$name" ($command)');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub == 'remove') {
|
||||||
|
if (parts.length < 2) {
|
||||||
|
context.writeLine('Usage: /mcp remove <name>');
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
final name = parts[1];
|
||||||
|
if (!servers.containsKey(name)) {
|
||||||
|
context.writeLine('MCP server "$name" not found.');
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
servers.remove(name);
|
||||||
|
await context.settingsStore.update(
|
||||||
|
(s) => s.copyWith(mcpServers: servers.isEmpty ? null : servers),
|
||||||
|
);
|
||||||
|
context.writeLine('Removed MCP server "$name"');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub == 'enable' || sub == 'disable') {
|
||||||
|
final isEnable = sub == 'enable';
|
||||||
|
final target = parts.length > 1 ? parts.sublist(1).join(' ') : 'all';
|
||||||
|
|
||||||
|
if (target == 'all') {
|
||||||
|
for (final name in servers.keys) {
|
||||||
|
servers[name] = Map<String, dynamic>.from(servers[name]!)..remove('disabled');
|
||||||
|
if (!isEnable) servers[name]!['disabled'] = true;
|
||||||
|
}
|
||||||
|
await context.settingsStore.update((s) => s.copyWith(mcpServers: servers));
|
||||||
|
context.writeLine(
|
||||||
|
'${isEnable ? 'Enabled' : 'Disabled'} ${servers.length} MCP server(s)',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!servers.containsKey(target)) {
|
||||||
|
context.writeLine('MCP server "$target" not found.');
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
servers[target] = Map<String, dynamic>.from(servers[target]!)..remove('disabled');
|
||||||
|
if (!isEnable) servers[target]!['disabled'] = true;
|
||||||
|
|
||||||
|
await context.settingsStore.update((s) => s.copyWith(mcpServers: servers));
|
||||||
|
context.writeLine('MCP server "$target" ${isEnable ? 'enabled' : 'disabled'}');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Unknown subcommand "$sub". Run /mcp help for usage.');
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
61
lib/src/commands/memory.dart
Normal file
61
lib/src/commands/memory.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
import '../local_state.dart' show joinPath;
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final theAgencyHomeDir = theAgencyHome();
|
||||||
|
final globalMemoryPath = joinPath(theAgencyHomeDir, 'THE_AGENCY.md');
|
||||||
|
final localMemoryPath = joinPath(
|
||||||
|
joinPath(context.workingDirectory, '.the_agency'),
|
||||||
|
'THE_AGENCY.md',
|
||||||
|
);
|
||||||
|
|
||||||
|
final rawArgs = args.join(" ").trim().toLowerCase();
|
||||||
|
|
||||||
|
if (commonHelpArgs.contains(rawArgs)) {
|
||||||
|
context.writeLine(
|
||||||
|
'Usage: /memory [global|local]\n\n'
|
||||||
|
'Edit memory files.\n\n'
|
||||||
|
'Files:\n'
|
||||||
|
' global $globalMemoryPath\n'
|
||||||
|
' local $localMemoryPath\n\n'
|
||||||
|
'Without an argument, lists the available memory files.',
|
||||||
|
);
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawArgs == 'global') {
|
||||||
|
context.writeLine("Global memory file: $globalMemoryPath");
|
||||||
|
final f = File(globalMemoryPath);
|
||||||
|
if (await f.exists()) {
|
||||||
|
final len = await f.length();
|
||||||
|
context.writeLine(" Size: $len bytes");
|
||||||
|
} else {
|
||||||
|
context.writeLine(" (does not exist yet)");
|
||||||
|
}
|
||||||
|
context.writeLine("\nTo edit, open the file in your editor:\n \$EDITOR $globalMemoryPath");
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawArgs == 'local' || rawArgs.isEmpty) {
|
||||||
|
context.writeLine("Local memory file: $localMemoryPath");
|
||||||
|
final f = File(localMemoryPath);
|
||||||
|
if (await f.exists()) {
|
||||||
|
final len = await f.length();
|
||||||
|
context.writeLine(" Size: $len bytes");
|
||||||
|
} else {
|
||||||
|
context.writeLine(" (does not exist yet)");
|
||||||
|
}
|
||||||
|
context.writeLine("Global memory file: $globalMemoryPath");
|
||||||
|
context.writeLine(
|
||||||
|
"\nTo edit, open a file in your editor:\n \$EDITOR $localMemoryPath",
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine("Usage: /memory [global|local]");
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
22
lib/src/commands/mobile.dart
Normal file
22
lib/src/commands/mobile.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
const iosUrl = 'https://apps.apple.com/app/claude-by-anthropic/id6473753684';
|
||||||
|
const androidUrl = 'https://play.google.com/store/apps/details?id=com.anthropic.claude';
|
||||||
|
|
||||||
|
final arg = args.join(' ').trim().toLowerCase();
|
||||||
|
|
||||||
|
final isAndroid = arg == 'android';
|
||||||
|
final url = isAndroid ? androidUrl : iosUrl;
|
||||||
|
final platform = isAndroid ? 'Android' : 'iOS';
|
||||||
|
|
||||||
|
context.writeLine('Download Claude on $platform');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(' iOS: $iosUrl');
|
||||||
|
context.writeLine(' Android: $androidUrl');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('QR code rendering is not ported to the Dart CLI runtime.');
|
||||||
|
context.writeLine('Open this link on your phone: $url');
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
66
lib/src/commands/model.dart
Normal file
66
lib/src/commands/model.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim();
|
||||||
|
if (commonHelpArgs.contains(rawArgs.toLowerCase())) {
|
||||||
|
context.writeLine('Usage: /model [default|current|status|<model>]');
|
||||||
|
context.writeLine('Known aliases: ${modelAliases.join(', ')}');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawArgs.isEmpty || commonInfoArgs.contains(rawArgs.toLowerCase())) {
|
||||||
|
final current = resolveCurrentModelSetting(context);
|
||||||
|
context.writeLine(
|
||||||
|
'Current model: ${renderModelSetting(current)}${context.settingsStore.settings.model == null ? ' (default)' : ''}',
|
||||||
|
);
|
||||||
|
if (context.settingsStore.settings.model != null) {
|
||||||
|
context.writeLine('Saved model override: ${context.settingsStore.settings.model}');
|
||||||
|
}
|
||||||
|
if (context.settingsStore.settings.fastMode) {
|
||||||
|
context.writeLine('Fast mode: ON');
|
||||||
|
}
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalized = rawArgs.toLowerCase();
|
||||||
|
if (normalized == 'default' || normalized == 'auto' || normalized == 'unset') {
|
||||||
|
await context.settingsStore.update((settings) => settings.copyWith(model: null));
|
||||||
|
context.writeLine(
|
||||||
|
'Set model to ${renderModelSetting(resolveCurrentModelSetting(context))}',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final requestedModel = _normalizeModelInput(rawArgs);
|
||||||
|
var message = 'Set model to ${renderModelSetting(requestedModel)}';
|
||||||
|
final fastSupported = _supportsFastMode(requestedModel);
|
||||||
|
|
||||||
|
if (!fastSupported && context.settingsStore.settings.fastMode) {
|
||||||
|
await context.settingsStore.update(
|
||||||
|
(settings) => settings.copyWith(model: requestedModel, fastMode: false),
|
||||||
|
);
|
||||||
|
message += ' · Fast mode OFF';
|
||||||
|
context.writeLine(message);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.settingsStore.update((settings) => settings.copyWith(model: requestedModel));
|
||||||
|
if (context.settingsStore.settings.fastMode) {
|
||||||
|
message += ' · Fast mode ON';
|
||||||
|
}
|
||||||
|
context.writeLine(message);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _normalizeModelInput(String rawModel) {
|
||||||
|
final trimmed = rawModel.trim();
|
||||||
|
final lowered = trimmed.toLowerCase();
|
||||||
|
if (modelAliases.contains(lowered)) return lowered;
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _supportsFastMode(String model) {
|
||||||
|
final normalized = model.toLowerCase();
|
||||||
|
return normalized.contains('opus') || normalized.contains('sonnet');
|
||||||
|
}
|
||||||
8
lib/src/commands/output_style.dart
Normal file
8
lib/src/commands/output_style.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine(
|
||||||
|
'/output-style has been deprecated. Use /config to change your output style, or set it in your settings file. Changes take effect on the next session.',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
235
lib/src/commands/permissions.dart
Normal file
235
lib/src/commands/permissions.dart
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../local_state.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
if (args.isEmpty) {
|
||||||
|
_writeSummary(context);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final subcommand = args.first.toLowerCase();
|
||||||
|
if (commonHelpArgs.contains(subcommand)) {
|
||||||
|
context.writeLine(
|
||||||
|
'Usage: /permissions [show|mode <mode>|allow <rule>|deny <rule>|ask <rule>|remove <index|rule>|clear [allow|deny|ask|all]]',
|
||||||
|
);
|
||||||
|
context.writeLine('Modes: ${supportedPermissionModes.join(', ')}');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand == 'show' ||
|
||||||
|
subcommand == 'list' ||
|
||||||
|
commonInfoArgs.contains(subcommand)) {
|
||||||
|
_writeSummary(context);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand == 'mode') {
|
||||||
|
if (args.length == 1) {
|
||||||
|
context.writeLine(
|
||||||
|
'Current permission mode: ${context.settingsStore.settings.permissionMode}',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final requestedMode = args[1];
|
||||||
|
if (!supportedPermissionModes.contains(requestedMode)) {
|
||||||
|
context.writeLine(
|
||||||
|
'Invalid permission mode "$requestedMode". Valid options: ${supportedPermissionModes.join(', ')}',
|
||||||
|
);
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.settingsStore.update(
|
||||||
|
(settings) => settings.copyWith(permissionMode: requestedMode),
|
||||||
|
);
|
||||||
|
context.writeLine('Permission mode set to $requestedMode');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand == 'allow' || subcommand == 'deny' || subcommand == 'ask') {
|
||||||
|
final rule = args.skip(1).join(' ').trim();
|
||||||
|
if (rule.isEmpty) {
|
||||||
|
context.writeLine('Please provide a permission rule.');
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.settingsStore.update(
|
||||||
|
(settings) => _applyRule(settings, subcommand, rule),
|
||||||
|
);
|
||||||
|
context.writeLine('Added $subcommand rule: $rule');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand == 'clear') {
|
||||||
|
final target = args.length > 1 ? args[1].toLowerCase() : 'all';
|
||||||
|
if (!<String>['all', 'allow', 'deny', 'ask'].contains(target)) {
|
||||||
|
context.writeLine('Usage: /permissions clear [allow|deny|ask|all]');
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.settingsStore.update((settings) => _clearRules(settings, target));
|
||||||
|
context.writeLine(
|
||||||
|
target == 'all' ? 'Cleared all permission rules' : 'Cleared $target rules',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand == 'remove') {
|
||||||
|
final target = args.skip(1).join(' ').trim();
|
||||||
|
if (target.isEmpty) {
|
||||||
|
context.writeLine('Usage: /permissions remove <index|rule>');
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
final removal = _removeRule(context.settingsStore.settings, target);
|
||||||
|
if (!removal.removed) {
|
||||||
|
context.writeLine('No permission rule matched "$target".');
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.settingsStore.update((settings) => removal.settings);
|
||||||
|
context.writeLine('Removed permission rule: ${removal.label}');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Unknown /permissions subcommand "$subcommand".');
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeSummary(CommandContext context) {
|
||||||
|
final settings = context.settingsStore.settings;
|
||||||
|
final flattened = _flatten(settings);
|
||||||
|
context.writeLine('Permission mode: ${settings.permissionMode}');
|
||||||
|
if (flattened.isEmpty) {
|
||||||
|
context.writeLine('No permission rules configured.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Permission rules:');
|
||||||
|
for (var i = 0; i < flattened.length; i++) {
|
||||||
|
final entry = flattened[i];
|
||||||
|
context.writeLine(' ${i + 1}. ${entry.behavior}: ${entry.rule}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalSettings _applyRule(LocalSettings settings, String behavior, String rule) {
|
||||||
|
final allowRules = settings.alwaysAllowRules.where((item) => item != rule).toList();
|
||||||
|
final denyRules = settings.alwaysDenyRules.where((item) => item != rule).toList();
|
||||||
|
final askRules = settings.alwaysAskRules.where((item) => item != rule).toList();
|
||||||
|
|
||||||
|
switch (behavior) {
|
||||||
|
case 'allow':
|
||||||
|
allowRules.add(rule);
|
||||||
|
break;
|
||||||
|
case 'deny':
|
||||||
|
denyRules.add(rule);
|
||||||
|
break;
|
||||||
|
case 'ask':
|
||||||
|
askRules.add(rule);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings.copyWith(
|
||||||
|
alwaysAllowRules: allowRules,
|
||||||
|
alwaysAskRules: askRules,
|
||||||
|
alwaysDenyRules: denyRules,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalSettings _clearRules(LocalSettings settings, String target) {
|
||||||
|
switch (target) {
|
||||||
|
case 'allow':
|
||||||
|
return settings.copyWith(alwaysAllowRules: const []);
|
||||||
|
case 'deny':
|
||||||
|
return settings.copyWith(alwaysDenyRules: const []);
|
||||||
|
case 'ask':
|
||||||
|
return settings.copyWith(alwaysAskRules: const []);
|
||||||
|
case 'all':
|
||||||
|
return settings.copyWith(
|
||||||
|
alwaysAllowRules: const [],
|
||||||
|
alwaysAskRules: const [],
|
||||||
|
alwaysDenyRules: const [],
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_RemovalResult _removeRule(LocalSettings settings, String target) {
|
||||||
|
final flattened = _flatten(settings);
|
||||||
|
final index = int.tryParse(target);
|
||||||
|
if (index != null) {
|
||||||
|
final entryIndex = index - 1;
|
||||||
|
if (entryIndex < 0 || entryIndex >= flattened.length) {
|
||||||
|
return _RemovalResult(removed: false, settings: settings, label: target);
|
||||||
|
}
|
||||||
|
final entry = flattened[entryIndex];
|
||||||
|
return _RemovalResult(
|
||||||
|
removed: true,
|
||||||
|
settings: _removeByLabel(settings, entry.behavior, entry.rule),
|
||||||
|
label: '${entry.behavior} ${entry.rule}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final entry in flattened) {
|
||||||
|
if (entry.rule == target) {
|
||||||
|
return _RemovalResult(
|
||||||
|
removed: true,
|
||||||
|
settings: _removeByLabel(settings, entry.behavior, entry.rule),
|
||||||
|
label: '${entry.behavior} ${entry.rule}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _RemovalResult(removed: false, settings: settings, label: target);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalSettings _removeByLabel(LocalSettings settings, String behavior, String rule) {
|
||||||
|
switch (behavior) {
|
||||||
|
case 'allow':
|
||||||
|
return settings.copyWith(
|
||||||
|
alwaysAllowRules: settings.alwaysAllowRules
|
||||||
|
.where((item) => item != rule)
|
||||||
|
.toList(growable: false),
|
||||||
|
);
|
||||||
|
case 'deny':
|
||||||
|
return settings.copyWith(
|
||||||
|
alwaysDenyRules: settings.alwaysDenyRules
|
||||||
|
.where((item) => item != rule)
|
||||||
|
.toList(growable: false),
|
||||||
|
);
|
||||||
|
case 'ask':
|
||||||
|
return settings.copyWith(
|
||||||
|
alwaysAskRules: settings.alwaysAskRules
|
||||||
|
.where((item) => item != rule)
|
||||||
|
.toList(growable: false),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_PermissionEntry> _flatten(LocalSettings settings) => [
|
||||||
|
...settings.alwaysAllowRules.map((r) => _PermissionEntry(behavior: 'allow', rule: r)),
|
||||||
|
...settings.alwaysAskRules.map((r) => _PermissionEntry(behavior: 'ask', rule: r)),
|
||||||
|
...settings.alwaysDenyRules.map((r) => _PermissionEntry(behavior: 'deny', rule: r)),
|
||||||
|
];
|
||||||
|
|
||||||
|
int totalRuleCount(LocalSettings settings) =>
|
||||||
|
settings.alwaysAllowRules.length +
|
||||||
|
settings.alwaysAskRules.length +
|
||||||
|
settings.alwaysDenyRules.length;
|
||||||
|
|
||||||
|
class _PermissionEntry {
|
||||||
|
const _PermissionEntry({required this.behavior, required this.rule});
|
||||||
|
final String behavior;
|
||||||
|
final String rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RemovalResult {
|
||||||
|
const _RemovalResult({required this.label, required this.removed, required this.settings});
|
||||||
|
final String label;
|
||||||
|
final bool removed;
|
||||||
|
final LocalSettings settings;
|
||||||
|
}
|
||||||
36
lib/src/commands/plan.dart
Normal file
36
lib/src/commands/plan.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim();
|
||||||
|
if (!context.sessionState.planModeEnabled) {
|
||||||
|
context.sessionState.planModeEnabled = true;
|
||||||
|
if (rawArgs.isNotEmpty && rawArgs != 'open') {
|
||||||
|
final existingPlan = await context.sessionState.readPlan();
|
||||||
|
if (existingPlan == null || existingPlan.trim().isEmpty) {
|
||||||
|
await context.sessionState.writePlan('# Plan\n\nGoal:\n- $rawArgs\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.writeLine('Enabled plan mode');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final planContent = await context.sessionState.readPlan();
|
||||||
|
final planPath = context.sessionState.planFilePath;
|
||||||
|
final argList = rawArgs.isEmpty ? const <String>[] : rawArgs.split(RegExp(r'\s+'));
|
||||||
|
|
||||||
|
if (argList.isNotEmpty && argList.first == 'open') {
|
||||||
|
context.writeLine('Plan file: $planPath');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (planContent == null || planContent.trim().isEmpty) {
|
||||||
|
context.writeLine('Already in plan mode. No plan written yet.');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Current Plan');
|
||||||
|
context.writeLine(planPath);
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(planContent);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
72
lib/src/commands/plugin.dart
Normal file
72
lib/src/commands/plugin.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final subcmd = args.isEmpty ? "" : args.first.toLowerCase();
|
||||||
|
|
||||||
|
context.writeLine("Plugin Manager");
|
||||||
|
context.writeLine("");
|
||||||
|
|
||||||
|
switch (subcmd) {
|
||||||
|
case "help":
|
||||||
|
case "--help":
|
||||||
|
case "-h":
|
||||||
|
context.writeLine("Usage: /plugin [subcommand]");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("Subcommands:");
|
||||||
|
context.writeLine(" install [plugin] Install a plugin");
|
||||||
|
context.writeLine(" uninstall [plugin] Uninstall a plugin");
|
||||||
|
context.writeLine(" enable [plugin] Enable a plugin");
|
||||||
|
context.writeLine(" disable [plugin] Disable a plugin");
|
||||||
|
context.writeLine(" validate [path] Validate a plugin");
|
||||||
|
context.writeLine(" marketplace Manage marketplaces");
|
||||||
|
context.writeLine(" manage Manage installed plugins");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "install":
|
||||||
|
case "i":
|
||||||
|
final target = args.length > 1 ? args.sublist(1).join(" ") : "";
|
||||||
|
if (target.isEmpty) {
|
||||||
|
context.writeLine("Usage: /plugin install <plugin-name>");
|
||||||
|
} else {
|
||||||
|
context.writeLine("Install target: $target");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("Interactive plugin installation is not ported to the Dart CLI.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "uninstall":
|
||||||
|
final target = args.length > 1 ? args[1] : "";
|
||||||
|
context.writeLine("Uninstall plugin: ${target.isEmpty ? "(interactive)" : target}");
|
||||||
|
context.writeLine("Interactive plugin management is not ported to the Dart CLI.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "enable":
|
||||||
|
final target = args.length > 1 ? args[1] : "";
|
||||||
|
context.writeLine("Enable plugin: ${target.isEmpty ? "(interactive)" : target}");
|
||||||
|
context.writeLine("Interactive plugin management is not ported to the Dart CLI.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "disable":
|
||||||
|
final target = args.length > 1 ? args[1] : "";
|
||||||
|
context.writeLine("Disable plugin: ${target.isEmpty ? "(interactive)" : target}");
|
||||||
|
context.writeLine("Interactive plugin management is not ported to the Dart CLI.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "validate":
|
||||||
|
final path = args.length > 1 ? args.sublist(1).join(" ") : "";
|
||||||
|
context.writeLine("Validate plugin${path.isEmpty ? "" : " at: $path"}");
|
||||||
|
context.writeLine("Interactive plugin validation is not ported to the Dart CLI.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "marketplace":
|
||||||
|
case "market":
|
||||||
|
context.writeLine("Marketplace management is not ported to the Dart CLI.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
context.writeLine("The interactive plugin browser is not ported to the Dart CLI runtime.");
|
||||||
|
context.writeLine("Run /plugin help to see available subcommands.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
28
lib/src/commands/pr_comments.dart
Normal file
28
lib/src/commands/pr_comments.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final prArg = args.join(' ').trim();
|
||||||
|
|
||||||
|
context.writeLine('PR Comments');
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
if (prArg.isEmpty) {
|
||||||
|
context.writeLine('Usage: /pr-comments [pr-number]');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Fetches and displays comments from a GitHub pull request.');
|
||||||
|
context.writeLine('Requires the `gh` CLI to be installed and authenticated.');
|
||||||
|
} else {
|
||||||
|
context.writeLine(
|
||||||
|
'This is a prompt-type command. In the legacy CLI it sends a prompt to the model'
|
||||||
|
' asking it to fetch and format PR comments via the gh CLI.',
|
||||||
|
);
|
||||||
|
context.writeLine('PR: $prArg');
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(
|
||||||
|
'Hint: run `gh pr view $prArg` and `gh api /repos/.../pulls/$prArg/comments` manually.',
|
||||||
|
);
|
||||||
|
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
27
lib/src/commands/privacy_settings.dart
Normal file
27
lib/src/commands/privacy_settings.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim().toLowerCase();
|
||||||
|
|
||||||
|
if (commonHelpArgs.contains(rawArgs)) {
|
||||||
|
context.writeLine(
|
||||||
|
'Usage: /privacy-settings\n\n'
|
||||||
|
'View and update your privacy settings.\n'
|
||||||
|
'Controls things like telemetry and data retention.',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final settings = context.settingsStore.settings;
|
||||||
|
|
||||||
|
context.writeLine('Privacy settings');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(' telemetry: ${settings.telemetry ?? 'default (on)'}');
|
||||||
|
context.writeLine(' privacyLevel: ${settings.privacyLevel ?? 'standard'}');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('To change, edit your settings file: ${context.settingsStore.path}');
|
||||||
|
context.writeLine('Or use /config to inspect the full settings object.');
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
27
lib/src/commands/ps.dart
Normal file
27
lib/src/commands/ps.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../daemon/daemon_manager.dart';
|
||||||
|
import '../daemon/daemon_types.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final mgr = DaemonManager();
|
||||||
|
final sessions = await mgr.listSessions(refreshStatus: true);
|
||||||
|
|
||||||
|
if (sessions.isEmpty) {
|
||||||
|
context.writeLine("No background sessions found.");
|
||||||
|
return const CommandResult(exitCode: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine("Background Sessions:");
|
||||||
|
context.writeLine("");
|
||||||
|
|
||||||
|
for (final s in sessions) {
|
||||||
|
final alive = s.status == SessionStatus.running ? " (running)" : " (${s.status.name})";
|
||||||
|
final title = s.title != null ? " ${s.title}" : "";
|
||||||
|
context.writeLine(" ${s.id} pid=${s.pid}$alive$title");
|
||||||
|
context.writeLine(" dir: ${s.workingDirectory}");
|
||||||
|
context.writeLine(" started: ${s.startedAt}");
|
||||||
|
if (s.endedAt != null) context.writeLine(" ended: ${s.endedAt}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult(exitCode: 0);
|
||||||
|
}
|
||||||
15
lib/src/commands/release_notes.dart
Normal file
15
lib/src/commands/release_notes.dart
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import '../build_info.dart';
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
const changelogUrl = 'https://github.com/anthropics/claude-code/releases';
|
||||||
|
|
||||||
|
context.writeLine('Release Notes');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Current version: ${BuildInfo.versionDisplay}');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Fetching the remote changelog is not wired up in the Dart CLI runtime yet.');
|
||||||
|
context.writeLine('See the full changelog at: $changelogUrl');
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
26
lib/src/commands/rename.dart
Normal file
26
lib/src/commands/rename.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../session/session_store.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final newName = args.join(" ").trim();
|
||||||
|
|
||||||
|
if (newName.isEmpty || commonHelpArgs.contains(newName.toLowerCase())) {
|
||||||
|
context.writeLine(
|
||||||
|
'Usage: /rename <name>\n\n'
|
||||||
|
'Rename the current conversation session.\n'
|
||||||
|
'If no name is given in the legacy CLI, one is auto-generated from context.',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.sessionState.sessionName = newName;
|
||||||
|
|
||||||
|
if (history.hasSession) {
|
||||||
|
history.session!.name = newName;
|
||||||
|
await SessionStore.instance.saveSession(history.session!);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Session renamed to: "$newName"');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
52
lib/src/commands/resume.dart
Normal file
52
lib/src/commands/resume.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../session/session_store.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final query = args.join(' ').trim();
|
||||||
|
final sessions = await SessionStore.instance.listSessionsForProject(context.workingDirectory);
|
||||||
|
|
||||||
|
if (sessions.isEmpty) {
|
||||||
|
context.writeLine("No saved sessions found.");
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final filtered = query.isEmpty
|
||||||
|
? sessions
|
||||||
|
: sessions.where((s) {
|
||||||
|
final lower = query.toLowerCase();
|
||||||
|
return s.name.toLowerCase().contains(lower) || s.id.startsWith(lower);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (filtered.isEmpty) {
|
||||||
|
context.writeLine('No sessions matching "$query".');
|
||||||
|
return const CommandResult(exitCode: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine("Saved sessions (newest first):");
|
||||||
|
context.writeLine("");
|
||||||
|
|
||||||
|
for (int i = 0; i < filtered.length; i++) {
|
||||||
|
final s = filtered[i];
|
||||||
|
final ts = s.updated.toLocal().toString().substring(0, 16);
|
||||||
|
final costStr = s.cost != null ? " \$${s.cost!.toStringAsFixed(4)}" : "";
|
||||||
|
context.writeLine(" [${i + 1}] ${s.name}$costStr");
|
||||||
|
context.writeLine(" id=${s.id} msgs=${s.messageCount} updated=$ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine("\nTo load a session, use: /resume <name or id>");
|
||||||
|
|
||||||
|
if (filtered.length == 1 && query.isNotEmpty) {
|
||||||
|
final loaded = await SessionStore.instance.loadSession(
|
||||||
|
filtered.first.id,
|
||||||
|
workingDirectory: context.workingDirectory,
|
||||||
|
);
|
||||||
|
if (loaded != null) {
|
||||||
|
history.setSession(loaded);
|
||||||
|
context.sessionState.sessionName = loaded.name;
|
||||||
|
context.writeLine('\nResumed session: "${loaded.name}"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
25
lib/src/commands/review.dart
Normal file
25
lib/src/commands/review.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final prArg = args.join(' ').trim();
|
||||||
|
|
||||||
|
context.writeLine('Review pull request');
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
if (prArg.isEmpty) {
|
||||||
|
context.writeLine('Usage: /review [pr-number]');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('No PR number given. In the legacy CLI this would run `gh pr list` first.');
|
||||||
|
} else {
|
||||||
|
context.writeLine('PR: $prArg');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(
|
||||||
|
'This is a prompt-type command. In the legacy CLI it sends a review prompt to the model'
|
||||||
|
' with the gh pr diff output embedded.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Hint: run `gh pr diff $prArg` to see the diff manually.');
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
13
lib/src/commands/rewind.dart
Normal file
13
lib/src/commands/rewind.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine("Rewind / Checkpoint");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("Restore the code and conversation to a previous checkpoint.");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("This command requires an active REPL session with checkpoint history.");
|
||||||
|
context.writeLine(
|
||||||
|
"The interactive checkpoint selector is not ported to the Dart CLI runtime.",
|
||||||
|
);
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
43
lib/src/commands/security_review.dart
Normal file
43
lib/src/commands/security_review.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine("Security review");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine(
|
||||||
|
"This is a prompt-type command. In the legacy CLI it sends the current\n"
|
||||||
|
"git diff to the AI model for a focused security analysis.",
|
||||||
|
);
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("Security categories examined:");
|
||||||
|
context.writeLine(" - Input validation (SQLi, CMDi, path traversal, etc.)");
|
||||||
|
context.writeLine(" - Authentication & authorization issues");
|
||||||
|
context.writeLine(" - Crypto & secrets management");
|
||||||
|
context.writeLine(" - Injection & code execution");
|
||||||
|
context.writeLine(" - Data exposure");
|
||||||
|
context.writeLine("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final diffResult = await Process.run(
|
||||||
|
"git", ["diff", "--stat", "origin/HEAD..."],
|
||||||
|
workingDirectory: context.workingDirectory,
|
||||||
|
);
|
||||||
|
final stat = (diffResult.stdout as String).trim();
|
||||||
|
if (stat.isNotEmpty) {
|
||||||
|
context.writeLine("Changes vs origin/HEAD:");
|
||||||
|
context.writeLine(stat);
|
||||||
|
} else {
|
||||||
|
context.writeLine("(no diff vs origin/HEAD detected)");
|
||||||
|
}
|
||||||
|
} on ProcessException {
|
||||||
|
context.writeLine("(could not run git diff)");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine(
|
||||||
|
"Run `git diff origin/HEAD...` to view the full diff, then review manually or use the legacy CLI.",
|
||||||
|
);
|
||||||
|
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
11
lib/src/commands/session.dart
Normal file
11
lib/src/commands/session.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine("Remote Session");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("Remote session mode is not available in the Dart CLI port.");
|
||||||
|
context.writeLine(
|
||||||
|
"This command shows a QR code and URL when Claude Code is running in remote mode.",
|
||||||
|
);
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
15
lib/src/commands/skills.dart
Normal file
15
lib/src/commands/skills.dart
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine("Skills");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine(
|
||||||
|
"Skills are reusable prompt templates that can be invoked as slash commands.",
|
||||||
|
);
|
||||||
|
context.writeLine("The interactive skills browser is not ported to the Dart CLI runtime.");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine(
|
||||||
|
"In the legacy CLI, skills are loaded from ~/.claude/skills/ or project .claude/skills/.",
|
||||||
|
);
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
31
lib/src/commands/stats.dart
Normal file
31
lib/src/commands/stats.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final stats = context.runtimeStateStore.state.stats;
|
||||||
|
final sortedCounts = stats.commandCounts.entries.toList()
|
||||||
|
..sort((a, b) => b.value.compareTo(a.value));
|
||||||
|
|
||||||
|
context.writeLine('CLI stats');
|
||||||
|
context.writeLine('Sessions started: ${stats.sessionsStarted}');
|
||||||
|
context.writeLine('Interactive sessions: ${stats.interactiveSessionsStarted}');
|
||||||
|
context.writeLine('Commands executed: ${stats.commandsExecuted}');
|
||||||
|
context.writeLine('Commands this session: ${context.sessionState.commandsExecuted}');
|
||||||
|
context.writeLine(
|
||||||
|
'Session duration: ${formatDuration(DateTime.now().toUtc().difference(context.sessionState.startedAt))}',
|
||||||
|
);
|
||||||
|
if (stats.lastCommandName != null) {
|
||||||
|
context.writeLine(
|
||||||
|
'Last command: ${stats.lastCommandName} (${stats.lastCommandAt ?? 'unknown'})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sortedCounts.isNotEmpty) {
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Top commands:');
|
||||||
|
for (final entry in sortedCounts.take(5)) {
|
||||||
|
context.writeLine(' ${entry.key}: ${entry.value}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
54
lib/src/commands/status.dart
Normal file
54
lib/src/commands/status.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import '../build_info.dart';
|
||||||
|
import '../command.dart';
|
||||||
|
import '../legacy_inventory.dart' show legacySourceFileCount;
|
||||||
|
import '../migration_assessment.dart';
|
||||||
|
import 'permissions.dart' as permissionsCmd;
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final unportedCommands = context.catalog.unportedSlashCommands;
|
||||||
|
final sample = unportedCommands.take(10).map((c) => c.name).join(', ');
|
||||||
|
final auth = context.runtimeStateStore.state.auth;
|
||||||
|
|
||||||
|
context.writeLine('Claude Code status');
|
||||||
|
context.writeLine('Version: ${BuildInfo.versionDisplay}');
|
||||||
|
context.writeLine('Working directory: ${context.workingDirectory}');
|
||||||
|
context.writeLine(
|
||||||
|
'Account: ${auth == null ? 'not logged in' : '${auth.email} (${auth.subscriptionType}${auth.rateLimitTier == null ? '' : ', ${auth.rateLimitTier}'})'}',
|
||||||
|
);
|
||||||
|
context.writeLine('Model: ${renderModelSetting(resolveCurrentModelSetting(context))}');
|
||||||
|
context.writeLine('Permission mode: ${context.settingsStore.settings.permissionMode}');
|
||||||
|
context.writeLine(
|
||||||
|
'Permission rules: ${permissionsCmd.totalRuleCount(context.settingsStore.settings)}',
|
||||||
|
);
|
||||||
|
context.writeLine(
|
||||||
|
'Fast mode: ${context.settingsStore.settings.fastMode ? 'on' : 'off'}',
|
||||||
|
);
|
||||||
|
context.writeLine(
|
||||||
|
'Effort: ${showCurrentEffort(context).replaceFirst('Current ', '').replaceFirst('Effort ', 'effort ')}',
|
||||||
|
);
|
||||||
|
context.writeLine(
|
||||||
|
'Statusline prompt: ${context.settingsStore.settings.statusLinePrompt ?? defaultStatuslinePrompt}',
|
||||||
|
);
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine('Migration status');
|
||||||
|
context.writeLine('Legacy source root: old_repo/');
|
||||||
|
context.writeLine('Legacy source files: $legacySourceFileCount');
|
||||||
|
context.writeLine('Known slash commands: ${context.catalog.totalKnownSlashCommands}');
|
||||||
|
context.writeLine('Ported commands: ${context.catalog.portedCommands.length}');
|
||||||
|
context.writeLine(
|
||||||
|
'Reserved top-level entrypoints: ${context.catalog.totalReservedTopLevelEntryPoints}',
|
||||||
|
);
|
||||||
|
context.writeLine('Remaining slash commands: ${unportedCommands.length}');
|
||||||
|
if (sample.isNotEmpty) context.writeLine('Next unported commands: $sample');
|
||||||
|
context.writeLine(
|
||||||
|
'Largest legacy areas: ${legacySubsystemStats.take(5).map((stat) => '${stat.name} ${stat.fileCount}').join(', ')}',
|
||||||
|
);
|
||||||
|
context.writeLine('High-friction import matches: $legacyHotspotImportMatches');
|
||||||
|
context.writeLine('Primary blockers:');
|
||||||
|
for (final blocker in migrationBlockers.take(3)) {
|
||||||
|
context.writeLine(' - $blocker');
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
31
lib/src/commands/statusline.dart
Normal file
31
lib/src/commands/statusline.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim();
|
||||||
|
if (rawArgs.isEmpty || commonInfoArgs.contains(rawArgs.toLowerCase())) {
|
||||||
|
final prompt = context.settingsStore.settings.statusLinePrompt ?? defaultStatuslinePrompt;
|
||||||
|
context.writeLine('Status line prompt: $prompt');
|
||||||
|
context.writeLine(buildStatuslineAgentInstruction(prompt));
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commonHelpArgs.contains(rawArgs.toLowerCase())) {
|
||||||
|
context.writeLine('Usage: /statusline [show|clear|<prompt>]');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawArgs.toLowerCase() == 'clear') {
|
||||||
|
await context.settingsStore.update(
|
||||||
|
(settings) => settings.copyWith(statusLinePrompt: null),
|
||||||
|
);
|
||||||
|
context.writeLine('Cleared saved status line prompt.');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.settingsStore.update(
|
||||||
|
(settings) => settings.copyWith(statusLinePrompt: rawArgs),
|
||||||
|
);
|
||||||
|
context.writeLine(buildStatuslineAgentInstruction(rawArgs));
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
32
lib/src/commands/stickers.dart
Normal file
32
lib/src/commands/stickers.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
const url = "https://www.stickermule.com/claudecode";
|
||||||
|
|
||||||
|
String? browserCmd;
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
browserCmd = "open";
|
||||||
|
} else if (Platform.isLinux) {
|
||||||
|
browserCmd = "xdg-open";
|
||||||
|
} else if (Platform.isWindows) {
|
||||||
|
browserCmd = "start";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool opened = false;
|
||||||
|
if (browserCmd != null) {
|
||||||
|
try {
|
||||||
|
final result = await Process.run(browserCmd, [url]);
|
||||||
|
opened = result.exitCode == 0;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opened) {
|
||||||
|
context.writeLine("Opening sticker page in browser...");
|
||||||
|
} else {
|
||||||
|
context.writeLine("Order Claude Code stickers at: $url");
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
42
lib/src/commands/tag.dart
Normal file
42
lib/src/commands/tag.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final tagArg = args.join(" ").trim();
|
||||||
|
|
||||||
|
if (tagArg.isEmpty || commonHelpArgs.contains(tagArg) || commonInfoArgs.contains(tagArg)) {
|
||||||
|
context.writeLine(
|
||||||
|
'Usage: /tag <tag-name>\n\n'
|
||||||
|
'Toggle a searchable tag on the current session.\n'
|
||||||
|
'Run the same command again to remove the tag.\n'
|
||||||
|
'Tags are displayed after the branch name in /resume and can be searched with /.\n\n'
|
||||||
|
'Examples:\n'
|
||||||
|
' /tag bugfix # Add tag\n'
|
||||||
|
' /tag bugfix # Remove tag (toggle)\n'
|
||||||
|
' /tag feature-auth\n'
|
||||||
|
' /tag wip',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalizedTag = tagArg.replaceAll(RegExp(r'[\x00-\x1F\x7F]'), '').trim();
|
||||||
|
if (normalizedTag.isEmpty) {
|
||||||
|
context.writeLine("Tag name cannot be empty");
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentTag = context.sessionState.sessionTag;
|
||||||
|
if (currentTag == normalizedTag) {
|
||||||
|
context.sessionState.sessionTag = null;
|
||||||
|
context.writeLine("Removed tag #$normalizedTag");
|
||||||
|
} else {
|
||||||
|
context.sessionState.sessionTag = normalizedTag;
|
||||||
|
if (currentTag != null) {
|
||||||
|
context.writeLine("Replaced tag #$currentTag with #$normalizedTag");
|
||||||
|
} else {
|
||||||
|
context.writeLine("Tagged session with #$normalizedTag");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
9
lib/src/commands/tasks.dart
Normal file
9
lib/src/commands/tasks.dart
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine("Background Tasks");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("The interactive task manager is not ported to the Dart CLI runtime.");
|
||||||
|
context.writeLine("Background task tracking requires a running REPL session.");
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
52
lib/src/commands/terminal_setup.dart
Normal file
52
lib/src/commands/terminal_setup.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
const _nativeCsiuTerminals = <String>[
|
||||||
|
'ghostty', 'kitty', 'iTerm.app', 'WezTerm', 'WarpTerminal',
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final term = Platform.environment['TERM_PROGRAM']
|
||||||
|
?? Platform.environment['TERMINAL_EMULATOR']
|
||||||
|
?? '';
|
||||||
|
|
||||||
|
if (_nativeCsiuTerminals.contains(term)) {
|
||||||
|
context.writeLine(
|
||||||
|
'Terminal-setup: your terminal ($term) natively supports the Kitty keyboard protocol.',
|
||||||
|
);
|
||||||
|
context.writeLine('No additional setup is needed for Shift+Enter / newlines.');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Terminal setup');
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
if (Platform.isMacOS && term == 'Apple_Terminal') {
|
||||||
|
context.writeLine('Detected: Apple Terminal (macOS)');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(
|
||||||
|
'To enable Option+Enter for newlines:\n'
|
||||||
|
' 1. Open Terminal > Settings > Profiles > Keyboard\n'
|
||||||
|
' 2. Add a key binding: Option+Return → sends \\033\\012\n'
|
||||||
|
' 3. Alternatively run the legacy CLI interactively: /terminal-setup',
|
||||||
|
);
|
||||||
|
} else if (term == 'vscode' || term == 'cursor' || term == 'windsurf') {
|
||||||
|
context.writeLine('Detected: $term terminal');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(
|
||||||
|
'To enable Shift+Enter for newlines, add this to your $term keybindings.json:\n'
|
||||||
|
' { "key": "shift+enter", "command": "workbench.action.terminal.sendSequence",\n'
|
||||||
|
' "args": { "text": "\\\\n" }, "when": "terminalFocus" }',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
context.writeLine('Detected terminal: ${term.isEmpty ? '(unknown)' : term}');
|
||||||
|
context.writeLine('');
|
||||||
|
context.writeLine(
|
||||||
|
'Interactive terminal setup (key binding installation) is not ported to the Dart runtime.',
|
||||||
|
);
|
||||||
|
context.writeLine('Run the legacy CLI to use the full interactive setup wizard.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
22
lib/src/commands/theme.dart
Normal file
22
lib/src/commands/theme.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../local_state.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final rawArgs = args.join(' ').trim().toLowerCase();
|
||||||
|
if (rawArgs.isEmpty || rawArgs == 'current' || rawArgs == 'status') {
|
||||||
|
context.writeLine('Current theme: ${context.settingsStore.settings.theme}');
|
||||||
|
context.writeLine('Available themes: ${supportedThemeSettings.join(', ')}');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!supportedThemeSettings.contains(rawArgs)) {
|
||||||
|
context.writeLine(
|
||||||
|
'Invalid theme "$rawArgs". Available themes: ${supportedThemeSettings.join(', ')}',
|
||||||
|
);
|
||||||
|
return const CommandResult(exitCode: 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.settingsStore.update((settings) => settings.copyWith(theme: rawArgs));
|
||||||
|
context.writeLine('Theme set to $rawArgs');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
18
lib/src/commands/tools.dart
Normal file
18
lib/src/commands/tools.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '../tools/tool_registry.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final registry = ToolRegistry();
|
||||||
|
|
||||||
|
context.writeLine('Available tools:');
|
||||||
|
context.writeLine('');
|
||||||
|
|
||||||
|
for (final tool in registry.allTools) {
|
||||||
|
context.writeLine(' ${tool.name}');
|
||||||
|
context.writeLine(' ${tool.description}');
|
||||||
|
context.writeLine('');
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Usage: toolname: <args> (e.g. bash: echo hello)');
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
22
lib/src/commands/upgrade.dart
Normal file
22
lib/src/commands/upgrade.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final auth = context.runtimeStateStore.state.auth;
|
||||||
|
if (auth != null &&
|
||||||
|
auth.subscriptionType == 'max' &&
|
||||||
|
auth.rateLimitTier == max20xTier) {
|
||||||
|
context.writeLine(
|
||||||
|
'You are already on the highest Max subscription plan. For additional usage, run /login to switch to an API usage-billed account.',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.writeLine('Upgrade URL: https://claude.ai/upgrade/max');
|
||||||
|
if (auth != null) {
|
||||||
|
context.writeLine(
|
||||||
|
'After upgrading, refresh the local profile with /login ${auth.email} max $max20xTier',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
22
lib/src/commands/usage.dart
Normal file
22
lib/src/commands/usage.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import '../command.dart';
|
||||||
|
import '_shared.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final auth = context.runtimeStateStore.state.auth;
|
||||||
|
context.writeLine('Plan usage');
|
||||||
|
if (auth == null) {
|
||||||
|
context.writeLine('Account: not logged in');
|
||||||
|
} else {
|
||||||
|
context.writeLine('Account: ${auth.email}');
|
||||||
|
context.writeLine('Subscription: ${auth.subscriptionType}');
|
||||||
|
context.writeLine('Rate limit tier: ${auth.rateLimitTier ?? 'not recorded'}');
|
||||||
|
context.writeLine('Logged in at: ${auth.loggedInAt}');
|
||||||
|
}
|
||||||
|
context.writeLine('Model: ${renderModelSetting(resolveCurrentModelSetting(context))}');
|
||||||
|
context.writeLine('Fast mode: ${context.settingsStore.settings.fastMode ? 'on' : 'off'}');
|
||||||
|
context.writeLine(showCurrentEffort(context));
|
||||||
|
context.writeLine(
|
||||||
|
'Remote quota sync is not available in the Dart CLI yet, so this view shows saved account metadata only.',
|
||||||
|
);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
7
lib/src/commands/version.dart
Normal file
7
lib/src/commands/version.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import '../build_info.dart';
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine(BuildInfo.versionDisplay);
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
16
lib/src/commands/vim.dart
Normal file
16
lib/src/commands/vim.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
final currentMode = context.settingsStore.settings.editorMode == 'vim' ? 'vim' : 'normal';
|
||||||
|
final newMode = currentMode == 'normal' ? 'vim' : 'normal';
|
||||||
|
await context.settingsStore.update((settings) => settings.copyWith(editorMode: newMode));
|
||||||
|
|
||||||
|
if (newMode == 'vim') {
|
||||||
|
context.writeLine(
|
||||||
|
'Editor mode set to vim. Use Escape key to toggle between INSERT and NORMAL modes.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
context.writeLine('Editor mode set to normal. Using standard (readline) keyboard bindings.');
|
||||||
|
}
|
||||||
|
return const CommandResult();
|
||||||
|
}
|
||||||
11
lib/src/commands/voice.dart
Normal file
11
lib/src/commands/voice.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import '../command.dart';
|
||||||
|
|
||||||
|
Future<CommandResult> run(CommandContext context, List<String> args) async {
|
||||||
|
context.writeLine("Voice Mode");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("Voice mode requires a Claude.ai account and is only available");
|
||||||
|
context.writeLine("in the interactive REPL session, not the Dart CLI port.");
|
||||||
|
context.writeLine("");
|
||||||
|
context.writeLine("Sign in at https://claude.ai to access voice features.");
|
||||||
|
return const CommandResult(exitCode: 2);
|
||||||
|
}
|
||||||
|
|
@ -71,11 +71,12 @@ String joinPath(String base, String child) {
|
||||||
class LocalSettings {
|
class LocalSettings {
|
||||||
const LocalSettings({
|
const LocalSettings({
|
||||||
this.advisorModel,
|
this.advisorModel,
|
||||||
|
this.advisorEffortLevel,
|
||||||
this.alwaysAllowRules = const <String>[],
|
this.alwaysAllowRules = const <String>[],
|
||||||
this.alwaysAskRules = const <String>[],
|
this.alwaysAskRules = const <String>[],
|
||||||
this.alwaysDenyRules = const <String>[],
|
this.alwaysDenyRules = const <String>[],
|
||||||
this.editorMode = 'normal',
|
this.editorMode = 'normal',
|
||||||
this.effortLevel,
|
this.effortLevel = 'medium',
|
||||||
this.fastMode = false,
|
this.fastMode = false,
|
||||||
this.hooks,
|
this.hooks,
|
||||||
this.mcpServers,
|
this.mcpServers,
|
||||||
|
|
@ -92,11 +93,12 @@ class LocalSettings {
|
||||||
factory LocalSettings.fromJson(Map<String, dynamic> json) {
|
factory LocalSettings.fromJson(Map<String, dynamic> json) {
|
||||||
return LocalSettings(
|
return LocalSettings(
|
||||||
advisorModel: _readString(json, 'advisorModel'),
|
advisorModel: _readString(json, 'advisorModel'),
|
||||||
|
advisorEffortLevel: _readString(json, 'advisorEffortLevel'),
|
||||||
alwaysAllowRules: _readStringList(json, 'alwaysAllowRules'),
|
alwaysAllowRules: _readStringList(json, 'alwaysAllowRules'),
|
||||||
alwaysAskRules: _readStringList(json, 'alwaysAskRules'),
|
alwaysAskRules: _readStringList(json, 'alwaysAskRules'),
|
||||||
alwaysDenyRules: _readStringList(json, 'alwaysDenyRules'),
|
alwaysDenyRules: _readStringList(json, 'alwaysDenyRules'),
|
||||||
editorMode: _readString(json, 'editorMode') ?? 'normal',
|
editorMode: _readString(json, 'editorMode') ?? 'normal',
|
||||||
effortLevel: _readString(json, 'effortLevel'),
|
effortLevel: _readString(json, 'effortLevel') ?? 'medium',
|
||||||
fastMode: _readBool(json, 'fastMode') ?? false,
|
fastMode: _readBool(json, 'fastMode') ?? false,
|
||||||
hooks: _readStringMap(json, 'hooks'),
|
hooks: _readStringMap(json, 'hooks'),
|
||||||
mcpServers: _readMcpServers(json),
|
mcpServers: _readMcpServers(json),
|
||||||
|
|
@ -113,11 +115,12 @@ class LocalSettings {
|
||||||
|
|
||||||
// advisor model name - optional
|
// advisor model name - optional
|
||||||
final String? advisorModel;
|
final String? advisorModel;
|
||||||
|
final String? advisorEffortLevel;
|
||||||
final List<String> alwaysAllowRules;
|
final List<String> alwaysAllowRules;
|
||||||
final List<String> alwaysAskRules;
|
final List<String> alwaysAskRules;
|
||||||
final List<String> alwaysDenyRules;
|
final List<String> alwaysDenyRules;
|
||||||
final String editorMode;
|
final String editorMode;
|
||||||
final String? effortLevel;
|
final String effortLevel;
|
||||||
final bool fastMode;
|
final bool fastMode;
|
||||||
|
|
||||||
// hook configs keyed by event name
|
// hook configs keyed by event name
|
||||||
|
|
@ -136,6 +139,7 @@ class LocalSettings {
|
||||||
|
|
||||||
LocalSettings copyWith({
|
LocalSettings copyWith({
|
||||||
Object? advisorModel = _sentinel,
|
Object? advisorModel = _sentinel,
|
||||||
|
Object? advisorEffortLevel = _sentinel,
|
||||||
List<String>? alwaysAllowRules,
|
List<String>? alwaysAllowRules,
|
||||||
List<String>? alwaysAskRules,
|
List<String>? alwaysAskRules,
|
||||||
List<String>? alwaysDenyRules,
|
List<String>? alwaysDenyRules,
|
||||||
|
|
@ -155,13 +159,14 @@ class LocalSettings {
|
||||||
}) {
|
}) {
|
||||||
return LocalSettings(
|
return LocalSettings(
|
||||||
advisorModel: identical(advisorModel, _sentinel) ? this.advisorModel : advisorModel as String?,
|
advisorModel: identical(advisorModel, _sentinel) ? this.advisorModel : advisorModel as String?,
|
||||||
|
advisorEffortLevel: identical(advisorEffortLevel, _sentinel) ? this.advisorEffortLevel : advisorEffortLevel as String?,
|
||||||
alwaysAllowRules: alwaysAllowRules ?? this.alwaysAllowRules,
|
alwaysAllowRules: alwaysAllowRules ?? this.alwaysAllowRules,
|
||||||
alwaysAskRules: alwaysAskRules ?? this.alwaysAskRules,
|
alwaysAskRules: alwaysAskRules ?? this.alwaysAskRules,
|
||||||
alwaysDenyRules: alwaysDenyRules ?? this.alwaysDenyRules,
|
alwaysDenyRules: alwaysDenyRules ?? this.alwaysDenyRules,
|
||||||
editorMode: editorMode ?? this.editorMode,
|
editorMode: editorMode ?? this.editorMode,
|
||||||
effortLevel: identical(effortLevel, _sentinel)
|
effortLevel: identical(effortLevel, _sentinel)
|
||||||
? this.effortLevel
|
? this.effortLevel
|
||||||
: effortLevel as String?,
|
: (effortLevel as String?) ?? 'medium',
|
||||||
fastMode: fastMode ?? this.fastMode,
|
fastMode: fastMode ?? this.fastMode,
|
||||||
hooks: identical(hooks, _sentinel) ? this.hooks : hooks as Map<String, String>?,
|
hooks: identical(hooks, _sentinel) ? this.hooks : hooks as Map<String, String>?,
|
||||||
mcpServers: identical(mcpServers, _sentinel) ? this.mcpServers : mcpServers as Map<String, Map<String, dynamic>>?,
|
mcpServers: identical(mcpServers, _sentinel) ? this.mcpServers : mcpServers as Map<String, Map<String, dynamic>>?,
|
||||||
|
|
@ -189,11 +194,12 @@ class LocalSettings {
|
||||||
|
|
||||||
return LocalSettings(
|
return LocalSettings(
|
||||||
advisorModel: override.advisorModel ?? advisorModel,
|
advisorModel: override.advisorModel ?? advisorModel,
|
||||||
|
advisorEffortLevel: override.advisorEffortLevel ?? advisorEffortLevel,
|
||||||
alwaysAllowRules: override.alwaysAllowRules.isNotEmpty ? override.alwaysAllowRules : alwaysAllowRules,
|
alwaysAllowRules: override.alwaysAllowRules.isNotEmpty ? override.alwaysAllowRules : alwaysAllowRules,
|
||||||
alwaysAskRules: override.alwaysAskRules.isNotEmpty ? override.alwaysAskRules : alwaysAskRules,
|
alwaysAskRules: override.alwaysAskRules.isNotEmpty ? override.alwaysAskRules : alwaysAskRules,
|
||||||
alwaysDenyRules: override.alwaysDenyRules.isNotEmpty ? override.alwaysDenyRules : alwaysDenyRules,
|
alwaysDenyRules: override.alwaysDenyRules.isNotEmpty ? override.alwaysDenyRules : alwaysDenyRules,
|
||||||
editorMode: override.editorMode != 'normal' ? override.editorMode : editorMode,
|
editorMode: override.editorMode != 'normal' ? override.editorMode : editorMode,
|
||||||
effortLevel: override.effortLevel ?? effortLevel,
|
effortLevel: override.effortLevel != 'medium' ? override.effortLevel : effortLevel,
|
||||||
fastMode: override.fastMode ? true : fastMode,
|
fastMode: override.fastMode ? true : fastMode,
|
||||||
hooks: override.hooks ?? hooks,
|
hooks: override.hooks ?? hooks,
|
||||||
mcpServers: override.mcpServers ?? mcpServers,
|
mcpServers: override.mcpServers ?? mcpServers,
|
||||||
|
|
@ -211,6 +217,7 @@ class LocalSettings {
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'advisorModel': advisorModel,
|
'advisorModel': advisorModel,
|
||||||
|
'advisorEffortLevel': advisorEffortLevel,
|
||||||
'alwaysAllowRules': alwaysAllowRules,
|
'alwaysAllowRules': alwaysAllowRules,
|
||||||
'alwaysAskRules': alwaysAskRules,
|
'alwaysAskRules': alwaysAskRules,
|
||||||
'alwaysDenyRules': alwaysDenyRules,
|
'alwaysDenyRules': alwaysDenyRules,
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ class ConversationHistory {
|
||||||
_session = s;
|
_session = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addMessage(String role, String content, {int? tokens, int? contextTokens, List<MessageAttachment>? attachments}) {
|
void addMessage(String role, String content, {int? tokens, int? contextTokens, List<MessageAttachment>? attachments, double? cost}) {
|
||||||
if (_session == null) return;
|
if (_session == null) return;
|
||||||
|
|
||||||
final msg = Message(role: role, content: content, tokens: tokens, contextTokens: contextTokens, attachments: attachments);
|
final msg = Message(role: role, content: content, tokens: tokens, contextTokens: contextTokens, attachments: attachments, cost: cost);
|
||||||
|
|
||||||
_session!.messages.add(msg);
|
_session!.messages.add(msg);
|
||||||
_session!.updated = DateTime.now().toUtc();
|
_session!.updated = DateTime.now().toUtc();
|
||||||
|
|
@ -46,7 +46,7 @@ class ConversationHistory {
|
||||||
_session!.updated = DateTime.now().toUtc();
|
_session!.updated = DateTime.now().toUtc();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLastMessageContextTokens(int contextTokens) {
|
void setLastMessageContextTokens(int? contextTokens) {
|
||||||
if (_session == null || _session!.messages.isEmpty) return;
|
if (_session == null || _session!.messages.isEmpty) return;
|
||||||
final last = _session!.messages.last;
|
final last = _session!.messages.last;
|
||||||
_session!.messages[_session!.messages.length - 1] = Message(
|
_session!.messages[_session!.messages.length - 1] = Message(
|
||||||
|
|
@ -54,7 +54,21 @@ class ConversationHistory {
|
||||||
content: last.content,
|
content: last.content,
|
||||||
timestamp: last.timestamp,
|
timestamp: last.timestamp,
|
||||||
tokens: last.tokens,
|
tokens: last.tokens,
|
||||||
contextTokens: contextTokens,
|
contextTokens: contextTokens ?? last.contextTokens,
|
||||||
|
cost: last.cost,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setLastMessageCost(double? cost) {
|
||||||
|
if (_session == null || _session!.messages.isEmpty) return;
|
||||||
|
final last = _session!.messages.last;
|
||||||
|
_session!.messages[_session!.messages.length - 1] = Message(
|
||||||
|
role: last.role,
|
||||||
|
content: last.content,
|
||||||
|
timestamp: last.timestamp,
|
||||||
|
tokens: last.tokens,
|
||||||
|
contextTokens: last.contextTokens,
|
||||||
|
cost: cost,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue