using Astralis; using Inversion; using InvercargillSqlInversion; using Spry; using Spry.Authentication; using Statum; using ClankerSaddle; using ClankerSaddle.Api; using ClankerSaddle.Data; using ClankerSaddle.Services; int main(string[] args) { int port = args.length > 1 ? int.parse(args[1]) : 8080; print("ClankerSaddle - LLM task orchestration platform\n"); print(@"Website starting on port $(port)\n"); try { var application = new WebApplication(port); application.use_compression(); application.add_module(); application.add_module(); var db_path = Environment.get_variable("SPRY_DB_PATH") ?? ""; if (db_path.strip().length == 0) { db_path = "./clanker-saddle.sqlite"; } var db = application.container.configure_with(); db.register_connection(@"sqlite://$(db_path)"); // SQLite serialisation posture (the flake family where a SELECT // stepping against a concurrent write lock — the test harness's // fixture writes share this file — returned SQLITE_BUSY, which // the ORM's lazy enumerator reports as end-of-rows: an EMPTY // result for data that exists): WAL readers never block on (and // are never blocked by) writers, and the busy timeout absorbs // the remaining writer-writer window. try { var connection = application.container.create_scope() .resolve(); connection.execute("PRAGMA journal_mode = WAL"); connection.execute("PRAGMA busy_timeout = 5000"); } catch (Error pragma_error) { print(@"[ClankerSaddle] sqlite pragmas failed: $(pragma_error.message)\n"); } db.add_migration(); db.add_migration(); db.add_entity(Project.entity_mapping); db.add_entity(ProjectLink.entity_mapping); db.add_entity(TaskEntity.entity_mapping); db.add_entity(TaskComment.entity_mapping); db.add_entity(TaskBlock.entity_mapping); db.add_entity(TaskProgress.entity_mapping); db.add_entity(Saddle.entity_mapping); db.add_entity(Session.entity_mapping); db.add_entity(SessionCommand.entity_mapping); db.add_entity(SaddleEntryDedup.entity_mapping); db.add_entity(Rule.entity_mapping); db.add_entity(RuleProject.entity_mapping); db.add_entity(TaskRule.entity_mapping); db.add_entity(ProjectNote.entity_mapping); db.add_entity(SessionRawLog.entity_mapping); db.add_entity(Capability.entity_mapping); db.add_entity(SaddleCapability.entity_mapping); db.add_entity(TaskCapability.entity_mapping); db.add_entity(PushSubscription.entity_mapping); db.add_entity(TaskNotifyPref.entity_mapping); db.add_entity(Goal.entity_mapping); db.add_entity(GoalLink.entity_mapping); db.migrate_on_startup(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_scoped(); application.add_singleton(); application.add_singleton(); application.add_singleton(); application.add_singleton(); // Web Push (plan 11 task E): the VAPID keypair loads or // generates + persists BEFORE the server accepts requests, so // every subscriber sees the same public key from the first // page render. application.container.create_scope() .resolve().ensure_keys_from_config(); var statum = application.configure_with(); statum.global(StatePulse.SLOT_TYPE, new ClankerSaddle.Services.PulsePublic()); // spry:pages-begin statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); statum.add_page(); // spry:pages-end // spry:actions-begin statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); statum.action(); // spry:actions-end // spry:resources-begin statum.add_resource(); statum.add_resource(); statum.add_resource(); // spry:resources-end // spry:auth-begin // spry:auth-end application.add_endpoint( new EndpointRoute("/api/saddle/next-task", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/chunks", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/results", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/note", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/task/create", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/task/delete", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/task/enqueue", Method.POST)); application.add_endpoint( new EndpointRoute("/api/saddle/tasks", Method.GET)); application.add_endpoint( new EndpointRoute("/api/saddle/task/{id}", Method.GET)); application.add_endpoint( new EndpointRoute("/api/saddle/capabilities", Method.GET)); application.add_endpoint( new EndpointRoute("/terminal-stream/{session_id}", Method.GET)); application.add_endpoint( new EndpointRoute("/sw.js", Method.GET)); GLib.Idle.add(() => { seed_admin.begin(application.container); start_dispatcher.begin(application.container); return false; }); application.run(); return 0; } catch (Error e) { printerr("Error: %s\n", e.message); return 1; } } /** * Seeds the admin account when BOTH SPRY_SEED_ADMIN_USERNAME and * SPRY_SEED_ADMIN_PASSWORD are set; skipped silently otherwise. The * password is never logged. */ private async void seed_admin(Container container) { var username = Environment.get_variable("SPRY_SEED_ADMIN_USERNAME") ?? ""; var password = Environment.get_variable("SPRY_SEED_ADMIN_PASSWORD") ?? ""; if (username.length == 0 || password.length == 0) { return; } try { var scope = container.create_scope(); yield SpryAuth.ensure_admin(scope.resolve(), username, @"$(username)@example.com", password); print("[ClankerSaddle] Seeded admin account: %s\n", username); } catch (Error e) { printerr("[ClankerSaddle] Admin seeding failed: %s\n", e.message); } } private async void start_dispatcher(Container container) { try { var scope = container.create_scope(); scope.resolve().start(); } catch (Error e) { printerr("[ClankerSaddle] Dispatcher startup failed: %s\n", e.message); } }