Skip to content

Commit b9af9ce

Browse files
committed
feat: add cost-sync for CLI agents and TUI page 7
Feat: cost-sync command - Add commandcode-bridge cost-sync with interactive agent selection - Detect OpenCode, Aider, Goose configs and read configured models - Add per-model cost fields (input, output, cache_read per 1M tokens) - Filter to bridge providers only (Command Code name, localhost baseURL) - List all bridge providers, OpenAI and Anthropic compatible - Validate pricing against live /v1/models before syncing Feat: pricing table - Add pricing_db.dart with 44 models matching the live API exactly - Track knownMissingFromApi for models absent from /v1/models Feat: TUI page 7 - Add Cost Sync page with keymap [7], [c] to sync, [up/down] agent select - Show all priced models grouped Open Source/Premium, white/grey coloring - Show [c] sync hint on the page Chore: docs - Update CHANGELOG.md for v1.2.0 - Update AGENTS.md file structure, CLI contract, TUI pages, key bindings See CHANGELOG.md for full details.
1 parent ef6f329 commit b9af9ce

6 files changed

Lines changed: 1409 additions & 8 deletions

File tree

AGENTS.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,23 @@ commandcode-bridge/
2828
├── lib/
2929
│ ├── commandcode_bridge.dart # Barrel
3030
│ └── src/
31-
│ ├── main.dart # CLI wiring (run, run --server, help)
31+
│ ├── main.dart # CLI wiring (run, run --server, cost-sync, help)
3232
│ ├── models/
3333
│ │ ├── account.dart # Account + config store (port persist)
3434
│ │ ├── models_db.dart # 44 models with goAccessible field
3535
│ │ └── version.dart # Bridge version constant
3636
│ ├── services/
3737
│ │ ├── api_client.dart # HTTP client for api.commandcode.ai
38+
│ │ ├── cost_sync.dart # Cost sync: detect agents, update configs
3839
│ │ ├── log_store.dart # JSONL activity log (2000 entries)
40+
│ │ ├── pricing_db.dart # Hardcoded pricing table (44 models)
3941
│ │ └── updater.dart # Self-update: API cache + download .tgz + npm install -g
4042
│ ├── server/
4143
│ │ ├── server_controller.dart # HTTP server + routing
4244
│ │ ├── openai_handler.dart # OpenAI-compatible proxy
4345
│ │ └── anthropic_handler.dart # Anthropic-compatible proxy
4446
│ └── tui/
45-
│ └── app.dart # Nocterm TUI (9 panels + log + bars)
47+
│ └── app.dart # Nocterm TUI (10 panels + log + bars)
4648
├── docs/
4749
│ ├── INSTALL.md # Install options, platform support
4850
│ ├── API-REFERENCE.md # Proxy endpoints, client configs
@@ -74,6 +76,7 @@ commandcode-bridge/
7476
commandcode-bridge run Start the bridge in TUI mode
7577
commandcode-bridge run --server Start the bridge in headless server mode
7678
commandcode-bridge update Download and install latest stable release
79+
commandcode-bridge cost-sync Sync model pricing to CLI agent configs
7780
commandcode-bridge help Show help screen
7881
commandcode-bridge --version Print version string
7982
commandcode-bridge (no args) Show help screen
@@ -264,6 +267,7 @@ Copy triggers:
264267
| `4` | Rate Limits | `/alpha/billing/credits` (windowLimits) | 5-hour and weekly usage bars with exceed warnings, remaining, reset times |
265268
| `5` | Models | `_orderedModels` (sorted by plan, 44 total) | Text list with copy-on-Enter |
266269
| `6` | Proxy Config | Bridge state + endpoints | Text info |
270+
| `7` | Cost Sync | Local config files | Detected agents, model list with pricing, sync status |
267271

268272
Visualization uses `ProgressBar` from nocterm with color-coded fill:
269273
- Green: good (< 80% usage)
@@ -274,14 +278,15 @@ Visualization uses `ProgressBar` from nocterm with color-coded fill:
274278

275279
| Key | Context | Action |
276280
|-----|---------|--------|
277-
| `1-6` | Always | Switch info page |
281+
| `1-7` | Always | Switch info page |
278282
| `r` | Always | Refresh all API data |
279283
| `o` | Always | Copy OpenAI endpoint URL to clipboard |
280284
| `a` | Always | Copy Anthropic endpoint URL to clipboard |
285+
| `c` | Cost Sync page | Trigger cost sync to selected agent |
281286
| `p` | Always | Open port configuration panel |
282287
| `h` | Always | Open help panel |
283288
| `q` | Always | Open quit confirmation |
284-
| `up/down` | Main | Scroll / navigate models |
289+
| `up/down` | Main | Scroll / navigate models / select cost sync agent |
285290
| `PgUp/PgDn` | Main | Scroll 10 lines |
286291
| `Enter` | Models page | Copy selected model ID to clipboard |
287292
| `Ctrl+L` | Always | Toggle log sidebar |

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## v1.2.0 (2026-07-31)
6+
7+
### Features
8+
9+
- Cost sync: sync Command Code model pricing to CLI agent configs
10+
- `commandcode-bridge cost-sync` CLI command with interactive agent selection
11+
- Detects installed CLI agents (OpenCode, Aider, Goose)
12+
- Filters models by bridge provider only (matches "Command Code" in provider name, localhost only)
13+
- Reads user's configured models from each agent's config
14+
- Adds/updates per-model cost fields (input, output, cache_read per 1M tokens)
15+
- Hardcoded pricing table with 44 models, exactly matching the bridge `/v1/models` API
16+
- Live API validation: pricing is checked against the bridge `/v1/models` before syncing
17+
- Lists ALL bridge providers found in config (OpenAI and Anthropic compatible), not just one
18+
- JSONC parser with trailing comma support for OpenCode configs
19+
- TUI page 7 "Cost Sync" with keymap [7]
20+
- Shows ALL Command Code models with pricing (grouped: Open Source, Premium)
21+
- Models in bridge config: bright white + "(will be implemented)"
22+
- Models not in config: greyed out
23+
- [c] keymap triggers sync with progress feedback (hint shown on the page)
24+
- [up/down] agent selection when multiple agents detected
25+
526
## v1.1.1 (2026-07-29)
627

728
### Fixes

lib/src/main.dart

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import 'models/account.dart';
55
import 'models/version.dart';
66
import 'services/log_store.dart';
77
import 'services/updater.dart';
8+
import 'services/cost_sync.dart';
9+
import 'services/pricing_db.dart';
810
import 'server/server_controller.dart';
911
import 'tui/app.dart';
1012

@@ -14,12 +16,14 @@ Usage:
1416
commandcode-bridge run
1517
commandcode-bridge run --server
1618
commandcode-bridge update
19+
commandcode-bridge cost-sync
1720
commandcode-bridge help
1821
1922
Commands:
2023
run Start the bridge in TUI mode
2124
run --server Start the bridge in headless server mode
2225
update Download and install latest stable release
26+
cost-sync Sync model pricing to CLI agent configs
2327
help Show this help screen
2428
-v, --version Print version string
2529
''';
@@ -32,6 +36,7 @@ Future<void> main(List<String> args) async {
3236
final showHelp = noArgs || args.contains('help') || args.contains('--help') || args.contains('-h');
3337
final isRun = !noArgs && args.first == 'run';
3438
final isUpdate = !noArgs && args.first == 'update';
39+
final isCostSync = !noArgs && args.first == 'cost-sync';
3540

3641
if (args.contains('--version') || args.contains('-v')) {
3742
stdout.writeln('CommandCode Bridge v$bridgeVersion');
@@ -48,6 +53,11 @@ Future<void> main(List<String> args) async {
4853
return;
4954
}
5055

56+
if (isCostSync) {
57+
await _runCostSync();
58+
return;
59+
}
60+
5161
if (!isRun) {
5262
_printUsageErr();
5363
exit(1);
@@ -118,6 +128,148 @@ Future<void> _runUpdate() async {
118128
}
119129
}
120130

131+
Future<void> _runCostSync() async {
132+
stdout.writeln('CommandCode Bridge v$bridgeVersion');
133+
stdout.writeln('Cost Sync - Model pricing for CLI agents');
134+
stdout.writeln('');
135+
136+
// Load config to get the bridge port
137+
final configStore = ConfigStore();
138+
configStore.load();
139+
final bridgePort = configStore.config.serverPort;
140+
141+
// Try to fetch live models from the bridge to validate PricingDb
142+
stdout.writeln('Fetching live model list from bridge /v1/models on port $bridgePort...');
143+
final liveIds = await CostSyncService.fetchLiveModelIds(port: bridgePort);
144+
if (liveIds == null) {
145+
stdout.writeln(' Warning: Bridge not reachable. Continuing without live validation.');
146+
stdout.writeln(' Start the bridge first (commandcode-bridge run) to validate pricing against the live API.');
147+
stdout.writeln('');
148+
} else {
149+
stdout.writeln(' Got ${liveIds.length} models from live API.');
150+
final report = CostSyncService.validateAgainstApi(liveIds);
151+
if (report.missingFromApi.isNotEmpty) {
152+
stdout.writeln('');
153+
stdout.writeln(' WARNING: ${report.missingFromApi.length} priced model(s) NOT in live /v1/models:');
154+
for (final p in report.missingFromApi) {
155+
stdout.writeln(' ${p.modelId}');
156+
}
157+
stdout.writeln(' These are kept for backwards compatibility but will not be syncable.');
158+
}
159+
if (report.pricingForUnknown.isNotEmpty) {
160+
stdout.writeln('');
161+
stdout.writeln(' NOTE: ${report.pricingForUnknown.length} live model(s) have NO pricing data:');
162+
for (final id in report.pricingForUnknown) {
163+
stdout.writeln(' $id');
164+
}
165+
}
166+
if (report.isClean) {
167+
stdout.writeln(' All PricingDb entries validated against live API.');
168+
}
169+
stdout.writeln('');
170+
}
171+
172+
final agents = CostSyncService.detectAgents();
173+
final detected = agents.where((a) => a.detected).toList();
174+
final notDetected = agents.where((a) => !a.detected).toList();
175+
176+
if (detected.isEmpty) {
177+
stdout.writeln('No supported CLI agents found.');
178+
stdout.writeln('');
179+
for (final agent in notDetected) {
180+
stderr.writeln(' ${agent.displayName.padRight(10)} not found: ${agent.configPath}');
181+
}
182+
stdout.writeln('');
183+
stdout.writeln('Supported agents: OpenCode, Aider, Goose');
184+
stdout.writeln('If your CLI agent is not listed, it does not support cost tracking.');
185+
return;
186+
}
187+
188+
stdout.writeln('Detected CLI agents:');
189+
for (var i = 0; i < detected.length; i++) {
190+
stdout.writeln(' [${i + 1}] ${detected[i].displayName.padRight(10)} ${detected[i].configPath}');
191+
}
192+
for (final agent in notDetected) {
193+
stdout.writeln(' [-] ${agent.displayName.padRight(10)} not found');
194+
}
195+
stdout.writeln('');
196+
197+
if (detected.length == 1) {
198+
final agent = detected.first;
199+
stdout.writeln('Auto-selecting: ${agent.displayName}');
200+
await _syncAgent(agent);
201+
return;
202+
}
203+
204+
stdout.write('Select agent (1-${detected.length}): ');
205+
final input = stdin.readLineSync()?.trim();
206+
final choice = int.tryParse(input ?? '');
207+
if (choice == null || choice < 1 || choice > detected.length) {
208+
stderr.writeln('Invalid selection.');
209+
exit(1);
210+
}
211+
212+
await _syncAgent(detected[choice - 1]);
213+
}
214+
215+
Future<void> _syncAgent(CliAgentInfo agent) async {
216+
stdout.writeln('');
217+
218+
List<String> models;
219+
if (agent.type == CliAgentType.opencode) {
220+
final info = CostSyncService.getOpenCodeBridgeModels();
221+
models = info.models;
222+
if (info.providers.isNotEmpty) {
223+
stdout.writeln('Bridge provider:');
224+
for (final prov in info.providers) {
225+
stdout.writeln(' - ${prov.name} [${prov.host}:${prov.port}]');
226+
}
227+
}
228+
} else {
229+
models = CostSyncService.getUserModels(agent.type);
230+
}
231+
232+
stdout.writeln('Reading models from ${agent.displayName} config...');
233+
234+
if (models.isEmpty) {
235+
stdout.writeln('No bridge models found in ${agent.displayName} config.');
236+
return;
237+
}
238+
239+
stdout.writeln('Found ${models.length} bridge model(s):');
240+
for (final m in models) {
241+
final pricing = PricingDb.byId(m);
242+
if (pricing != null) {
243+
stdout.writeln(' ${m.padRight(40)} \$${pricing.inputPer1M}/\$${pricing.outputPer1M} per 1M');
244+
} else {
245+
stdout.writeln(' ${m.padRight(40)} (no pricing data)');
246+
}
247+
}
248+
stdout.writeln('');
249+
250+
stdout.write('Sync costs to ${agent.displayName}? [Y/n]: ');
251+
final confirm = stdin.readLineSync()?.trim().toLowerCase() ?? '';
252+
if (confirm == 'n' || confirm == 'no') {
253+
stdout.writeln('Cancelled.');
254+
return;
255+
}
256+
257+
stdout.writeln('Syncing...');
258+
final result = CostSyncService.syncCosts(agent.type, models);
259+
260+
for (final msg in result.messages) {
261+
stdout.writeln(' $msg');
262+
}
263+
stdout.writeln('');
264+
265+
if (result.success) {
266+
stdout.writeln('Done. ${result.updated} model(s) updated, ${result.skipped} already configured.');
267+
} else {
268+
stderr.writeln('Completed with errors. ${result.updated} updated, ${result.failed} failed.');
269+
exit(1);
270+
}
271+
}
272+
121273
Future<void> _waitForSignal() async {
122274
final completer = Completer<void>();
123275
final sigintSub = ProcessSignal.sigint.watch().listen((_) {

0 commit comments

Comments
 (0)