@@ -5,6 +5,8 @@ import 'models/account.dart';
55import 'models/version.dart' ;
66import 'services/log_store.dart' ;
77import 'services/updater.dart' ;
8+ import 'services/cost_sync.dart' ;
9+ import 'services/pricing_db.dart' ;
810import 'server/server_controller.dart' ;
911import '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
1922Commands:
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+
121273Future <void > _waitForSignal () async {
122274 final completer = Completer <void >();
123275 final sigintSub = ProcessSignal .sigint.watch ().listen ((_) {
0 commit comments