specrails-desktop 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/specrails/batch-implement.md +287 -0
- package/.claude/commands/specrails/compat-check.md +271 -0
- package/.claude/commands/specrails/doctor.md +62 -0
- package/.claude/commands/specrails/enrich.md +1635 -0
- package/.claude/commands/specrails/explore-spec.md +173 -0
- package/.claude/commands/specrails/health-check.md +527 -0
- package/.claude/commands/specrails/implement.md +1457 -0
- package/.claude/commands/specrails/memory-inspect.md +259 -0
- package/.claude/commands/specrails/opsx-diff.md +419 -0
- package/.claude/commands/specrails/propose-spec.md +102 -0
- package/.claude/commands/specrails/reconfig.md +89 -0
- package/.claude/commands/specrails/refactor-recommender.md +212 -0
- package/.claude/commands/specrails/retry.md +363 -0
- package/.claude/commands/specrails/telemetry.md +552 -0
- package/.claude/commands/specrails/why.md +96 -0
- package/LICENSE +21 -0
- package/README.md +290 -0
- package/cli/dist/specrails-desktop.js +1098 -0
- package/client/dist/assets/ActivityFeedPage-Gy4x8dBt.js +1 -0
- package/client/dist/assets/AgentsPage-CPgu--Fb.js +86 -0
- package/client/dist/assets/AnalyticsPage-B5sJEee2.js +1 -0
- package/client/dist/assets/BarChart-7IMQ8HY1.js +33 -0
- package/client/dist/assets/CodePage-CBdFvbwe.js +2 -0
- package/client/dist/assets/DesktopAnalyticsPage-w0rdTq4w.js +1 -0
- package/client/dist/assets/DocsDialog-BZUYM7wm.js +11 -0
- package/client/dist/assets/DocsPage-9QglWl46.js +11 -0
- package/client/dist/assets/ExportDropdown-BLZFXtNi.js +1 -0
- package/client/dist/assets/IntegrationsPage-BxBE4y99.js +3 -0
- package/client/dist/assets/JobDetailPage-DydWx_5S.js +16 -0
- package/client/dist/assets/JobsPage-20ibw0IO.js +1 -0
- package/client/dist/assets/abap-Bw6f2wDG.js +1 -0
- package/client/dist/assets/activity-BEIp_Y1A.js +1 -0
- package/client/dist/assets/activity-BdrPln96.js +1 -0
- package/client/dist/assets/activity-CpkRS8Sx.js +1 -0
- package/client/dist/assets/activity-DKCpESPt.js +1 -0
- package/client/dist/assets/activity-DOUVEjJi.js +1 -0
- package/client/dist/assets/activity-DRwkql_y.js +1 -0
- package/client/dist/assets/activity-DcDQ7tjw.js +1 -0
- package/client/dist/assets/activity-Dv6H7wEr.js +1 -0
- package/client/dist/assets/addon-image-3WCl5Vhd.js +1 -0
- package/client/dist/assets/addon-ligatures-C5OdliKs.js +2 -0
- package/client/dist/assets/addon-webgl-BbX6pSjl.js +44 -0
- package/client/dist/assets/addspec-B5yl4Loj.js +1 -0
- package/client/dist/assets/addspec-BEeF5-zc.js +1 -0
- package/client/dist/assets/addspec-D33ocMxf.js +1 -0
- package/client/dist/assets/addspec-DFswZ0jK.js +1 -0
- package/client/dist/assets/addspec-DRE-jZv7.js +1 -0
- package/client/dist/assets/addspec-DVZ15Jp8.js +1 -0
- package/client/dist/assets/addspec-Fkv91Opc.js +1 -0
- package/client/dist/assets/addspec-GWm4ffKl.js +1 -0
- package/client/dist/assets/agents-1nCDWRmP.js +1 -0
- package/client/dist/assets/agents-Bm9rPqnt.js +1 -0
- package/client/dist/assets/agents-CMxtJMLD.js +1 -0
- package/client/dist/assets/agents-DK-Dlc0i.js +1 -0
- package/client/dist/assets/agents-Q6Ldfpxx.js +1 -0
- package/client/dist/assets/agents-TeOSy-ax.js +1 -0
- package/client/dist/assets/agents-iTqjRajS.js +1 -0
- package/client/dist/assets/agents-s87sMGzL.js +1 -0
- package/client/dist/assets/agentstudio-B6Wb59E7.js +1 -0
- package/client/dist/assets/agentstudio-BADhZ41e.js +1 -0
- package/client/dist/assets/agentstudio-BSnWLR63.js +1 -0
- package/client/dist/assets/agentstudio-BdidyBzZ.js +1 -0
- package/client/dist/assets/agentstudio-CxlUllqI.js +1 -0
- package/client/dist/assets/agentstudio-D3I62TLJ.js +1 -0
- package/client/dist/assets/agentstudio-DuH9TogZ.js +1 -0
- package/client/dist/assets/agentstudio-Kw88_dUF.js +1 -0
- package/client/dist/assets/aiedit-BWxHGsYA.js +1 -0
- package/client/dist/assets/aiedit-D2ji6Qy0.js +1 -0
- package/client/dist/assets/aiedit-DAhZTvtk.js +1 -0
- package/client/dist/assets/aiedit-DJMny-D5.js +1 -0
- package/client/dist/assets/aiedit-DOcxERkU.js +1 -0
- package/client/dist/assets/aiedit-DvrcbwGv.js +1 -0
- package/client/dist/assets/aiedit-TTwzL1TS.js +1 -0
- package/client/dist/assets/aiedit-WBSjT_C1.js +1 -0
- package/client/dist/assets/analytics-BIdr0YfL.js +1 -0
- package/client/dist/assets/analytics-C6EzgtdE.js +1 -0
- package/client/dist/assets/analytics-C9Zc-rkM.js +1 -0
- package/client/dist/assets/analytics-CVx3YOc0.js +1 -0
- package/client/dist/assets/analytics-CYj0tfj7.js +1 -0
- package/client/dist/assets/analytics-CnY4kNG3.js +1 -0
- package/client/dist/assets/analytics-CrPCZRJ-.js +1 -0
- package/client/dist/assets/analytics-DMCto-TF.js +1 -0
- package/client/dist/assets/apex-Cw8_REBo.js +1 -0
- package/client/dist/assets/atom-one-dark-B-oHczHB.css +1 -0
- package/client/dist/assets/attachments-BIsSSnHJ.js +1 -0
- package/client/dist/assets/attachments-BW4L3l2L.js +1 -0
- package/client/dist/assets/attachments-Bcf6BG6V.js +1 -0
- package/client/dist/assets/attachments-Bke8sCU4.js +1 -0
- package/client/dist/assets/attachments-COcrGRFz.js +1 -0
- package/client/dist/assets/attachments-DYHGA2Dj.js +1 -0
- package/client/dist/assets/attachments-Dd92KpUH.js +1 -0
- package/client/dist/assets/attachments-DzdU6DV6.js +1 -0
- package/client/dist/assets/azcli-Cz6HAoOw.js +1 -0
- package/client/dist/assets/bat-CcJ-xyqL.js +1 -0
- package/client/dist/assets/bicep-z1WDCKYz.js +2 -0
- package/client/dist/assets/browser-5ErDlJoR.js +1 -0
- package/client/dist/assets/browser-Bc-YdlVg.js +1 -0
- package/client/dist/assets/browser-BlYF4OOq.js +1 -0
- package/client/dist/assets/browser-CT-ReZGt.js +1 -0
- package/client/dist/assets/browser-DGITz3fC.js +1 -0
- package/client/dist/assets/browser-JsAIGCEW.js +1 -0
- package/client/dist/assets/browser-M5-rbPlw.js +1 -0
- package/client/dist/assets/browser-Qya9cARy.js +1 -0
- package/client/dist/assets/cameligo-BRewOpfa.js +1 -0
- package/client/dist/assets/chat-BEGuC03z.js +1 -0
- package/client/dist/assets/chat-BEW60P_u.js +1 -0
- package/client/dist/assets/chat-BQNMD0PL.js +1 -0
- package/client/dist/assets/chat-BsbNGPW9.js +1 -0
- package/client/dist/assets/chat-CboQguCi.js +1 -0
- package/client/dist/assets/chat-DRCa9pOt.js +1 -0
- package/client/dist/assets/chat-DwUm6W9z.js +1 -0
- package/client/dist/assets/chat-yoXwguQu.js +1 -0
- package/client/dist/assets/chunk-CilyBKbf.js +1 -0
- package/client/dist/assets/clojure-DBjRWN6g.js +1 -0
- package/client/dist/assets/clsx-DnqN-uhr.js +1 -0
- package/client/dist/assets/code-AL1rVIMb.js +1 -0
- package/client/dist/assets/code-C0BKpkht.js +1 -0
- package/client/dist/assets/code-C0FTS3ew.js +1 -0
- package/client/dist/assets/code-CPcHxzxw.js +1 -0
- package/client/dist/assets/code-D3ryDniw.js +1 -0
- package/client/dist/assets/code-D3zVVQTj.js +1 -0
- package/client/dist/assets/code-PCmfS3dn.js +1 -0
- package/client/dist/assets/code-exI0G5Wd.js +1 -0
- package/client/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/client/dist/assets/coffee-Cfk_XHGR.js +1 -0
- package/client/dist/assets/commands-B772IyDa.js +1 -0
- package/client/dist/assets/commands-BDDp6xFG.js +1 -0
- package/client/dist/assets/commands-CJxCry-o.js +1 -0
- package/client/dist/assets/commands-CfgY-_of.js +1 -0
- package/client/dist/assets/commands-DLrvnPNg.js +1 -0
- package/client/dist/assets/commands-IXMOKBYt.js +1 -0
- package/client/dist/assets/commands-UD1NzmwX.js +1 -0
- package/client/dist/assets/commands-sqrqsxyE.js +1 -0
- package/client/dist/assets/common-DCr6VzJ7.js +1 -0
- package/client/dist/assets/common-Dard9UNH.js +1 -0
- package/client/dist/assets/common-DeDELLZJ.js +1 -0
- package/client/dist/assets/common-DltqHaAe.js +1 -0
- package/client/dist/assets/common-Dmm1GhdD.js +1 -0
- package/client/dist/assets/common-DnjcgkPH.js +1 -0
- package/client/dist/assets/common-GbpxfPG8.js +1 -0
- package/client/dist/assets/common-wA36jmj1.js +1 -0
- package/client/dist/assets/cpp-BVob6BaP.js +1 -0
- package/client/dist/assets/csharp-C4fbRuOu.js +1 -0
- package/client/dist/assets/csp-DthFP_vT.js +1 -0
- package/client/dist/assets/css-CGMH0hcW.js +3 -0
- package/client/dist/assets/css.worker-Wv5dxAWO.js +89 -0
- package/client/dist/assets/cssMode-Cc6ozl-J.js +1 -0
- package/client/dist/assets/cypher-Pnf68BRV.js +1 -0
- package/client/dist/assets/dart-PMMOtxZX.js +1 -0
- package/client/dist/assets/dashboard-B4ixDVk8.js +1 -0
- package/client/dist/assets/dashboard-BZBADHSj.js +1 -0
- package/client/dist/assets/dashboard-C1MfeUHs.js +1 -0
- package/client/dist/assets/dashboard-C7SK6xu5.js +1 -0
- package/client/dist/assets/dashboard-CB6Le1yN.js +1 -0
- package/client/dist/assets/dashboard-CoTpMOBM.js +1 -0
- package/client/dist/assets/dashboard-Duo4DDCW.js +1 -0
- package/client/dist/assets/dashboard-I19DXBxw.js +1 -0
- package/client/dist/assets/dist-js-BY-Fv_fg.js +1 -0
- package/client/dist/assets/dist-js-Bakc4uxT.js +1 -0
- package/client/dist/assets/dockerfile-di1nsJCc.js +1 -0
- package/client/dist/assets/ecl-D_WVcB5M.js +1 -0
- package/client/dist/assets/editor-Br_kD0ds.css +1 -0
- package/client/dist/assets/editor.api2-XLGzZfbc.js +872 -0
- package/client/dist/assets/editor.main-CfXxHimg.js +6 -0
- package/client/dist/assets/editor.worker-Bd9IXS8d.js +26 -0
- package/client/dist/assets/elixir-OAdJEMOn.js +1 -0
- package/client/dist/assets/explore-4mFpnrKU.js +1 -0
- package/client/dist/assets/explore-A8Ltoblq.js +1 -0
- package/client/dist/assets/explore-B9A3iN2W.js +1 -0
- package/client/dist/assets/explore-BV5Xxlsn.js +1 -0
- package/client/dist/assets/explore-BrBJvfjP.js +1 -0
- package/client/dist/assets/explore-C3FSE42C.js +1 -0
- package/client/dist/assets/explore-D2EFgt8J.js +1 -0
- package/client/dist/assets/explore-hFc3HFcp.js +1 -0
- package/client/dist/assets/flow9-D3QEZjgn.js +1 -0
- package/client/dist/assets/format-command-CwGuwzGA.js +1 -0
- package/client/dist/assets/freemarker2-DP7J1gG3.js +3 -0
- package/client/dist/assets/fsharp-BF0k_8N8.js +1 -0
- package/client/dist/assets/go-BAQO5Jsz.js +1 -0
- package/client/dist/assets/graphql-hdFVFkiV.js +1 -0
- package/client/dist/assets/handlebars-BjRlucw6.js +1 -0
- package/client/dist/assets/hcl-DWnl1o-X.js +1 -0
- package/client/dist/assets/html-OumBQJ-U.js +1 -0
- package/client/dist/assets/html.worker-CQP8QQsS.js +502 -0
- package/client/dist/assets/htmlMode-CStc3zXM.js +1 -0
- package/client/dist/assets/index-CimDRRi7.css +2 -0
- package/client/dist/assets/index-XGZaKl_u.js +142 -0
- package/client/dist/assets/ini-CB-6OVu3.js +1 -0
- package/client/dist/assets/integrations-C3p12Ms6.js +1 -0
- package/client/dist/assets/integrations-Cr6hH7XR.js +1 -0
- package/client/dist/assets/integrations-Cublz3m6.js +1 -0
- package/client/dist/assets/integrations-D28q1kF6.js +1 -0
- package/client/dist/assets/integrations-DRdbki5W.js +1 -0
- package/client/dist/assets/integrations-DaC4SzzL.js +1 -0
- package/client/dist/assets/integrations-DmQYCUvN.js +1 -0
- package/client/dist/assets/integrations-HIlUxXVs.js +1 -0
- package/client/dist/assets/java-d1CmfiHX.js +1 -0
- package/client/dist/assets/javascript-CMk--e7g.js +1 -0
- package/client/dist/assets/jobs-BE1siB0M.js +1 -0
- package/client/dist/assets/jobs-BHcQ_Faf.js +1 -0
- package/client/dist/assets/jobs-CFfc2dNX.js +1 -0
- package/client/dist/assets/jobs-CSi5n8X_.js +1 -0
- package/client/dist/assets/jobs-Dc3X86PY.js +1 -0
- package/client/dist/assets/jobs-De5tASex.js +1 -0
- package/client/dist/assets/jobs-DsoXEdo7.js +1 -0
- package/client/dist/assets/jobs-Wl-ApPMb.js +1 -0
- package/client/dist/assets/json.worker-DzV-CpCQ.js +58 -0
- package/client/dist/assets/jsonMode-C2h3ZcjZ.js +7 -0
- package/client/dist/assets/julia-Bgv08lKa.js +1 -0
- package/client/dist/assets/kotlin-u98kaVTf.js +1 -0
- package/client/dist/assets/less-CjYwpgg5.js +2 -0
- package/client/dist/assets/lexon-YTjaAFBB.js +1 -0
- package/client/dist/assets/lib-CPxTMOAq.js +7 -0
- package/client/dist/assets/liquid-mI3KJrBE.js +1 -0
- package/client/dist/assets/lspLanguageFeatures-DU09ggWi.js +4 -0
- package/client/dist/assets/lua-BzmkWv27.js +1 -0
- package/client/dist/assets/m3-CFwk9fw0.js +1 -0
- package/client/dist/assets/markdown-CR5iMpSZ.js +1 -0
- package/client/dist/assets/mdx-C41VDTR_.js +1 -0
- package/client/dist/assets/mips-CcEalc17.js +1 -0
- package/client/dist/assets/monaco.contribution-CPObAXMC.js +2 -0
- package/client/dist/assets/msdax-BQbkawnr.js +1 -0
- package/client/dist/assets/mysql-GTlaaW_P.js +1 -0
- package/client/dist/assets/nav-0fwkrgHt.js +1 -0
- package/client/dist/assets/nav-BEL3MTwK.js +1 -0
- package/client/dist/assets/nav-B_G-TJDW.js +1 -0
- package/client/dist/assets/nav-C2YXcbZS.js +1 -0
- package/client/dist/assets/nav-ClzOE4mA.js +1 -0
- package/client/dist/assets/nav-CtYwmMgu.js +1 -0
- package/client/dist/assets/nav-D2bOGSEg.js +1 -0
- package/client/dist/assets/nav-iH1V5j6o.js +1 -0
- package/client/dist/assets/objective-c-Byu1T5if.js +1 -0
- package/client/dist/assets/pascal-BrfzBfRm.js +1 -0
- package/client/dist/assets/pascaligo-BXXKFUeo.js +1 -0
- package/client/dist/assets/perl-B3OikKq-.js +1 -0
- package/client/dist/assets/pgsql-CTsa0Acc.js +1 -0
- package/client/dist/assets/php-DiQh3FUW.js +1 -0
- package/client/dist/assets/pla-92uH8Fzm.js +1 -0
- package/client/dist/assets/postiats-BbeWkKUr.js +1 -0
- package/client/dist/assets/powerquery-DgDMzpsm.js +1 -0
- package/client/dist/assets/powershell-BfdUUzaG.js +1 -0
- package/client/dist/assets/preload-helper-DSXbuxSR.js +1 -0
- package/client/dist/assets/protobuf-BojW2ftW.js +2 -0
- package/client/dist/assets/pug-BxqTg3IU.js +1 -0
- package/client/dist/assets/python-Y27rKQtk.js +1 -0
- package/client/dist/assets/qsharp-BX_A-MW9.js +1 -0
- package/client/dist/assets/r-D9BMnxvJ.js +1 -0
- package/client/dist/assets/razor-Cd5-q9Bp.js +1 -0
- package/client/dist/assets/redis-5cJqEQJJ.js +1 -0
- package/client/dist/assets/redshift-d8BBqiwb.js +1 -0
- package/client/dist/assets/restructuredtext-C8a6yIcZ.js +1 -0
- package/client/dist/assets/ruby-egeh-6KX.js +1 -0
- package/client/dist/assets/rust-a3r9IInB.js +1 -0
- package/client/dist/assets/sb-y8iRIDei.js +1 -0
- package/client/dist/assets/scala-BPDK2AmK.js +1 -0
- package/client/dist/assets/scheme-BIWUEoOs.js +1 -0
- package/client/dist/assets/scss-CA-PSzwg.js +3 -0
- package/client/dist/assets/settings-55oDcbSh.js +1 -0
- package/client/dist/assets/settings-Bd4Tq1RB.js +1 -0
- package/client/dist/assets/settings-CCSM-Fhn.js +1 -0
- package/client/dist/assets/settings-D3e_bDoW.js +1 -0
- package/client/dist/assets/settings-DKbTkbn7.js +1 -0
- package/client/dist/assets/settings-Dxpo6_w7.js +1 -0
- package/client/dist/assets/settings-bt84e3Aa.js +1 -0
- package/client/dist/assets/settings-nu68QukM.js +1 -0
- package/client/dist/assets/setup-BMqwfbW9.js +1 -0
- package/client/dist/assets/setup-Bb5LcG28.js +1 -0
- package/client/dist/assets/setup-BeEx2_da.js +1 -0
- package/client/dist/assets/setup-CCCrB53Q.js +1 -0
- package/client/dist/assets/setup-CJA0ATmd.js +1 -0
- package/client/dist/assets/setup-CeiDbZcb.js +1 -0
- package/client/dist/assets/setup-Cus7TApA.js +1 -0
- package/client/dist/assets/setup-D9qOs2Xo.js +1 -0
- package/client/dist/assets/shell--LiT1Bja.js +1 -0
- package/client/dist/assets/solidity-DdqZccZg.js +1 -0
- package/client/dist/assets/sophia-S6-YxNG_.js +1 -0
- package/client/dist/assets/sparql-BSf5kMp2.js +1 -0
- package/client/dist/assets/specs-BFfu3u-a.js +1 -0
- package/client/dist/assets/specs-B__C8-8a.js +1 -0
- package/client/dist/assets/specs-CZ1PsXsC.js +1 -0
- package/client/dist/assets/specs-D2FzlLn9.js +1 -0
- package/client/dist/assets/specs-DaUTrNF9.js +1 -0
- package/client/dist/assets/specs-Dyc5hYeE.js +1 -0
- package/client/dist/assets/specs-cKEh2LXt.js +1 -0
- package/client/dist/assets/specs-k0PyLDVt.js +1 -0
- package/client/dist/assets/sql-D7KgjR8G.js +1 -0
- package/client/dist/assets/st-BnoDa-Ml.js +1 -0
- package/client/dist/assets/swift-DEUHTkUX.js +1 -0
- package/client/dist/assets/systemverilog-Tqb_KPnW.js +1 -0
- package/client/dist/assets/tcl-BmBFS2qq.js +1 -0
- package/client/dist/assets/terminal-80yDMgMF.js +1 -0
- package/client/dist/assets/terminal-Bje4ziIa.js +1 -0
- package/client/dist/assets/terminal-C2WYcFHF.js +1 -0
- package/client/dist/assets/terminal-CSONJOex.js +1 -0
- package/client/dist/assets/terminal-DEqzGtcr.js +1 -0
- package/client/dist/assets/terminal-DeWzh6ys.js +1 -0
- package/client/dist/assets/terminal-YOlsJCQj.js +1 -0
- package/client/dist/assets/terminal-lkZYR4wJ.js +1 -0
- package/client/dist/assets/tickets-CB7N30gm.js +1 -0
- package/client/dist/assets/tickets-CF2PYelu.js +1 -0
- package/client/dist/assets/tickets-DNOANUXr.js +1 -0
- package/client/dist/assets/tickets-DU1aqsbr.js +1 -0
- package/client/dist/assets/tickets-DYvafSaY.js +1 -0
- package/client/dist/assets/tickets-DlpC_iTg.js +1 -0
- package/client/dist/assets/tickets-DucYgtdl.js +1 -0
- package/client/dist/assets/tickets-clefmXLv.js +1 -0
- package/client/dist/assets/ts.worker-METxwbDZ.js +67719 -0
- package/client/dist/assets/tsMode-B0y_xEci.js +11 -0
- package/client/dist/assets/twig-BQV8igWC.js +1 -0
- package/client/dist/assets/typescript-BzK0OgwW.js +1 -0
- package/client/dist/assets/typespec-DlFroUGY.js +1 -0
- package/client/dist/assets/useProjectCache-DSaiGFjV.js +1 -0
- package/client/dist/assets/vb-BlrJpIMX.js +1 -0
- package/client/dist/assets/wgsl-BWgIc6FZ.js +298 -0
- package/client/dist/assets/workers-rt--R2Qy.js +1 -0
- package/client/dist/assets/xml-eX9QXAmI.js +1 -0
- package/client/dist/assets/yaml-fcsNkpOt.js +1 -0
- package/client/dist/index.html +246 -0
- package/docs/README.md +54 -0
- package/docs/cli.md +198 -0
- package/docs/codex.md +210 -0
- package/docs/creating-specs.md +197 -0
- package/docs/customizing.md +197 -0
- package/docs/getting-started.md +140 -0
- package/docs/internals/README.md +25 -0
- package/docs/internals/adding-a-provider.md +238 -0
- package/docs/internals/api-reference.md +634 -0
- package/docs/internals/architecture.md +332 -0
- package/docs/internals/configuration.md +172 -0
- package/docs/internals/openspec-workflow.md +282 -0
- package/docs/internals/operations-runbook.md +198 -0
- package/docs/internals/profiles.md +152 -0
- package/docs/platforms/macos.md +130 -0
- package/docs/platforms/windows.md +81 -0
- package/docs/running-pipelines.md +240 -0
- package/docs/terminal.md +138 -0
- package/docs/tracking-cost.md +155 -0
- package/package.json +82 -0
- package/server/dist/agent-generator.js +232 -0
- package/server/dist/agent-refine-db.js +124 -0
- package/server/dist/agent-refine-manager.js +526 -0
- package/server/dist/ai-invocations.js +111 -0
- package/server/dist/attachment-manager.js +299 -0
- package/server/dist/auth.js +207 -0
- package/server/dist/binary-probe.js +35 -0
- package/server/dist/browser-capture-manager.js +576 -0
- package/server/dist/browser-capture-types.js +28 -0
- package/server/dist/browser-network.js +149 -0
- package/server/dist/browser-playwright.js +888 -0
- package/server/dist/build-dirs.js +44 -0
- package/server/dist/changes-reader.js +120 -0
- package/server/dist/chat-manager.js +1060 -0
- package/server/dist/chromium-resolver.js +311 -0
- package/server/dist/code-explorer-router.js +788 -0
- package/server/dist/codex-otel-bridge.js +235 -0
- package/server/dist/command-resolver.js +102 -0
- package/server/dist/config.js +306 -0
- package/server/dist/context-budget.js +113 -0
- package/server/dist/context-scope.js +279 -0
- package/server/dist/contract-refine-runner.js +521 -0
- package/server/dist/core-compat.js +207 -0
- package/server/dist/core-package.js +14 -0
- package/server/dist/db.js +1034 -0
- package/server/dist/desktop-analytics.js +156 -0
- package/server/dist/desktop-db.js +456 -0
- package/server/dist/desktop-router.js +735 -0
- package/server/dist/docs-router.js +207 -0
- package/server/dist/explore-contract-refine.js +421 -0
- package/server/dist/explore-cwd-manager.js +242 -0
- package/server/dist/explore-draft-title.js +47 -0
- package/server/dist/explore-smash.js +450 -0
- package/server/dist/feature-flags.js +17 -0
- package/server/dist/file-provenance.js +382 -0
- package/server/dist/file-summary-generator.js +221 -0
- package/server/dist/file-summary-manager.js +689 -0
- package/server/dist/hooks.js +102 -0
- package/server/dist/ids.js +7 -0
- package/server/dist/index.js +586 -0
- package/server/dist/metrics.js +136 -0
- package/server/dist/mobile/index.js +16 -0
- package/server/dist/mobile/mobile-admin-router.js +84 -0
- package/server/dist/mobile/mobile-auth.js +67 -0
- package/server/dist/mobile/mobile-devices.js +80 -0
- package/server/dist/mobile/mobile-event-bus.js +39 -0
- package/server/dist/mobile/mobile-gateway.js +285 -0
- package/server/dist/mobile/mobile-mdns.js +81 -0
- package/server/dist/mobile/mobile-pairing.js +179 -0
- package/server/dist/mobile/mobile-redact.js +53 -0
- package/server/dist/mobile/mobile-router.js +411 -0
- package/server/dist/mobile/mobile-tls.js +86 -0
- package/server/dist/mobile/mobile-types.js +9 -0
- package/server/dist/mobile/mobile-ws.js +275 -0
- package/server/dist/path-resolver.js +298 -0
- package/server/dist/plugin-manager.js +617 -0
- package/server/dist/plugins/claude-approval.js +179 -0
- package/server/dist/plugins/claude-md-mutation.js +146 -0
- package/server/dist/plugins/codex-mcp.js +108 -0
- package/server/dist/plugins/contributors.js +72 -0
- package/server/dist/plugins/drift.js +58 -0
- package/server/dist/plugins/index.js +14 -0
- package/server/dist/plugins/json-mutation.js +120 -0
- package/server/dist/plugins/manager.js +32 -0
- package/server/dist/plugins/ownership.js +86 -0
- package/server/dist/plugins/paths.js +37 -0
- package/server/dist/plugins/prereq-installer.js +104 -0
- package/server/dist/plugins/rail-integration.js +79 -0
- package/server/dist/plugins/serena/index.js +13 -0
- package/server/dist/plugins/serena/install.js +91 -0
- package/server/dist/plugins/serena/instructions-content.js +21 -0
- package/server/dist/plugins/serena/manifest.js +111 -0
- package/server/dist/plugins/serena/verify.js +78 -0
- package/server/dist/plugins-router.js +215 -0
- package/server/dist/pricing.js +89 -0
- package/server/dist/profile-manager.js +310 -0
- package/server/dist/profiles-router.js +759 -0
- package/server/dist/project-registry.js +443 -0
- package/server/dist/project-router.js +4016 -0
- package/server/dist/proposal-manager.js +291 -0
- package/server/dist/provider-selection.js +69 -0
- package/server/dist/providers/claude-adapter.js +281 -0
- package/server/dist/providers/codex-adapter.js +264 -0
- package/server/dist/providers/index.js +23 -0
- package/server/dist/providers/registry.js +37 -0
- package/server/dist/providers/types.js +22 -0
- package/server/dist/queue-manager.js +1511 -0
- package/server/dist/rails-router.js +362 -0
- package/server/dist/rails-store.js +116 -0
- package/server/dist/result-event.js +106 -0
- package/server/dist/schemas/profile.v1.json +151 -0
- package/server/dist/setup-manager.js +1165 -0
- package/server/dist/setup-prerequisites.js +372 -0
- package/server/dist/smash-runner.js +663 -0
- package/server/dist/spec-draft-parser.js +133 -0
- package/server/dist/spec-launcher-manager.js +174 -0
- package/server/dist/spec-models.js +32 -0
- package/server/dist/specrails-tech-client.js +82 -0
- package/server/dist/spending.js +448 -0
- package/server/dist/telemetry-compactor.js +180 -0
- package/server/dist/telemetry-export.js +317 -0
- package/server/dist/telemetry-receiver.js +224 -0
- package/server/dist/terminal-manager.js +633 -0
- package/server/dist/terminal-marks-store.js +117 -0
- package/server/dist/terminal-osc-parser.js +159 -0
- package/server/dist/terminal-settings.js +282 -0
- package/server/dist/terminal-shell-integration.js +196 -0
- package/server/dist/ticket-broadcast.js +47 -0
- package/server/dist/ticket-store.js +397 -0
- package/server/dist/ticket-watcher.js +117 -0
- package/server/dist/types.js +10 -0
- package/server/dist/user-mcp-config.js +117 -0
- package/server/dist/util/cli-prompt.js +181 -0
- package/server/dist/util/secure-fs.js +50 -0
- package/server/dist/util/win-spawn.js +43 -0
- package/server/dist/webhook-manager.js +89 -0
- package/server/dist/ws-routing.js +47 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDesktopAnalytics = getDesktopAnalytics;
|
|
4
|
+
exports.getDesktopRecentJobs = getDesktopRecentJobs;
|
|
5
|
+
exports.getDesktopTodayStats = getDesktopTodayStats;
|
|
6
|
+
function resolveBounds(opts) {
|
|
7
|
+
const now = new Date();
|
|
8
|
+
const toISO = (d) => d.toISOString().slice(0, 10);
|
|
9
|
+
if (opts.period === 'all') {
|
|
10
|
+
return { current: { from: null, to: null }, label: 'All time' };
|
|
11
|
+
}
|
|
12
|
+
if (opts.period === 'custom') {
|
|
13
|
+
// B42: from/to are unvalidated query params. A malformed date flows into
|
|
14
|
+
// buildWhere's `new Date(bounds.to).toISOString()` and throws an unhandled
|
|
15
|
+
// RangeError ("Invalid time value") → 500. Drop any unparseable bound (and
|
|
16
|
+
// fall back to all-time when neither is valid) instead of crashing.
|
|
17
|
+
const validFrom = opts.from && !Number.isNaN(Date.parse(opts.from)) ? opts.from : null;
|
|
18
|
+
const validTo = opts.to && !Number.isNaN(Date.parse(opts.to)) ? opts.to : null;
|
|
19
|
+
return {
|
|
20
|
+
current: { from: validFrom, to: validTo },
|
|
21
|
+
label: validFrom || validTo ? `${validFrom ?? '…'} to ${validTo ?? '…'}` : 'All time',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const days = opts.period === '7d' ? 7 : opts.period === '30d' ? 30 : 90;
|
|
25
|
+
const from = toISO(new Date(now.getTime() - days * 86400000));
|
|
26
|
+
const to = toISO(now);
|
|
27
|
+
const label = opts.period === '7d' ? 'Last 7 days'
|
|
28
|
+
: opts.period === '30d' ? 'Last 30 days'
|
|
29
|
+
: 'Last 90 days';
|
|
30
|
+
return { current: { from, to }, label };
|
|
31
|
+
}
|
|
32
|
+
function buildWhere(bounds) {
|
|
33
|
+
if (!bounds.from && !bounds.to)
|
|
34
|
+
return { clause: '', params: [] };
|
|
35
|
+
if (bounds.from && bounds.to) {
|
|
36
|
+
const nextDay = new Date(new Date(bounds.to).getTime() + 86400000).toISOString().slice(0, 10);
|
|
37
|
+
return { clause: 'WHERE started_at >= ? AND started_at < ?', params: [bounds.from, nextDay] };
|
|
38
|
+
}
|
|
39
|
+
if (bounds.from)
|
|
40
|
+
return { clause: 'WHERE started_at >= ?', params: [bounds.from] };
|
|
41
|
+
const nextDay = new Date(new Date(bounds.to).getTime() + 86400000).toISOString().slice(0, 10);
|
|
42
|
+
return { clause: 'WHERE started_at < ?', params: [nextDay] };
|
|
43
|
+
}
|
|
44
|
+
function queryProjectKpi(db, clause, params) {
|
|
45
|
+
return db.prepare(`
|
|
46
|
+
SELECT
|
|
47
|
+
COALESCE(SUM(total_cost_usd), 0) as totalCostUsd,
|
|
48
|
+
COUNT(*) as totalJobs,
|
|
49
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as successCount,
|
|
50
|
+
AVG(CASE WHEN duration_ms IS NOT NULL THEN duration_ms END) as avgDurationMs
|
|
51
|
+
FROM jobs ${clause}
|
|
52
|
+
`).get(...params);
|
|
53
|
+
}
|
|
54
|
+
function queryProjectTimeline(db, clause, params) {
|
|
55
|
+
return db.prepare(`
|
|
56
|
+
SELECT
|
|
57
|
+
strftime('%Y-%m-%d', started_at) as date,
|
|
58
|
+
COALESCE(SUM(total_cost_usd), 0) as costUsd
|
|
59
|
+
FROM jobs ${clause}
|
|
60
|
+
GROUP BY date
|
|
61
|
+
ORDER BY date ASC
|
|
62
|
+
`).all(...params);
|
|
63
|
+
}
|
|
64
|
+
// ─── Main export ──────────────────────────────────────────────────────────────
|
|
65
|
+
function getDesktopAnalytics(registry, opts) {
|
|
66
|
+
const { current, label } = resolveBounds(opts);
|
|
67
|
+
const { clause, params } = buildWhere(current);
|
|
68
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
69
|
+
const tomorrow = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
|
|
70
|
+
const todayClause = 'WHERE started_at >= ? AND started_at < ?';
|
|
71
|
+
const todayParams = [today, tomorrow];
|
|
72
|
+
const contexts = registry.listContexts();
|
|
73
|
+
let totalCostUsd = 0;
|
|
74
|
+
let totalJobs = 0;
|
|
75
|
+
let totalSuccess = 0;
|
|
76
|
+
let costToday = 0;
|
|
77
|
+
let jobsToday = 0;
|
|
78
|
+
const projectBreakdown = [];
|
|
79
|
+
const timelineMap = new Map();
|
|
80
|
+
// Iterate sequentially to avoid SQLite contention
|
|
81
|
+
for (const ctx of contexts) {
|
|
82
|
+
const kpi = queryProjectKpi(ctx.db, clause, params);
|
|
83
|
+
const todayKpi = queryProjectKpi(ctx.db, todayClause, todayParams);
|
|
84
|
+
const timeline = queryProjectTimeline(ctx.db, clause, params);
|
|
85
|
+
totalCostUsd += kpi.totalCostUsd;
|
|
86
|
+
totalJobs += kpi.totalJobs;
|
|
87
|
+
totalSuccess += kpi.successCount;
|
|
88
|
+
costToday += todayKpi.totalCostUsd;
|
|
89
|
+
jobsToday += todayKpi.totalJobs;
|
|
90
|
+
projectBreakdown.push({
|
|
91
|
+
projectId: ctx.project.id,
|
|
92
|
+
projectName: ctx.project.name,
|
|
93
|
+
totalCostUsd: kpi.totalCostUsd,
|
|
94
|
+
totalJobs: kpi.totalJobs,
|
|
95
|
+
successRate: kpi.totalJobs > 0 ? kpi.successCount / kpi.totalJobs : 0,
|
|
96
|
+
avgDurationMs: kpi.avgDurationMs,
|
|
97
|
+
});
|
|
98
|
+
for (const row of timeline) {
|
|
99
|
+
timelineMap.set(row.date, (timelineMap.get(row.date) ?? 0) + row.costUsd);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Build sorted cost timeline
|
|
103
|
+
const costTimeline = Array.from(timelineMap.entries())
|
|
104
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
105
|
+
.map(([date, costUsd]) => ({ date, costUsd }));
|
|
106
|
+
// Sort projects by cost descending
|
|
107
|
+
projectBreakdown.sort((a, b) => b.totalCostUsd - a.totalCostUsd);
|
|
108
|
+
return {
|
|
109
|
+
period: { label, from: current.from, to: current.to },
|
|
110
|
+
kpi: {
|
|
111
|
+
totalCostUsd,
|
|
112
|
+
totalJobs,
|
|
113
|
+
successRate: totalJobs > 0 ? totalSuccess / totalJobs : 0,
|
|
114
|
+
costToday,
|
|
115
|
+
jobsToday,
|
|
116
|
+
},
|
|
117
|
+
projectBreakdown,
|
|
118
|
+
costTimeline,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function getDesktopRecentJobs(registry, limit = 10) {
|
|
122
|
+
const all = [];
|
|
123
|
+
for (const ctx of registry.listContexts()) {
|
|
124
|
+
const rows = ctx.db.prepare(`
|
|
125
|
+
SELECT id, command, started_at, finished_at, status, total_cost_usd
|
|
126
|
+
FROM jobs
|
|
127
|
+
ORDER BY started_at DESC
|
|
128
|
+
LIMIT ?
|
|
129
|
+
`).all(limit);
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
all.push({ ...row, projectId: ctx.project.id, projectName: ctx.project.name });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return all
|
|
135
|
+
.sort((a, b) => b.started_at.localeCompare(a.started_at))
|
|
136
|
+
.slice(0, limit);
|
|
137
|
+
}
|
|
138
|
+
function getDesktopTodayStats(registry) {
|
|
139
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
140
|
+
const tomorrow = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
|
|
141
|
+
const clause = 'WHERE started_at >= ? AND started_at < ?';
|
|
142
|
+
const params = [today, tomorrow];
|
|
143
|
+
let costToday = 0;
|
|
144
|
+
let jobsToday = 0;
|
|
145
|
+
for (const ctx of registry.listContexts()) {
|
|
146
|
+
const row = ctx.db.prepare(`
|
|
147
|
+
SELECT
|
|
148
|
+
COALESCE(SUM(total_cost_usd), 0) as costToday,
|
|
149
|
+
COUNT(*) as jobsToday
|
|
150
|
+
FROM jobs ${clause}
|
|
151
|
+
`).get(...params);
|
|
152
|
+
costToday += row.costToday;
|
|
153
|
+
jobsToday += row.jobsToday;
|
|
154
|
+
}
|
|
155
|
+
return { costToday, jobsToday };
|
|
156
|
+
}
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getDesktopDbPath = getDesktopDbPath;
|
|
7
|
+
exports.initDesktopDb = initDesktopDb;
|
|
8
|
+
exports.listProjects = listProjects;
|
|
9
|
+
exports.getProject = getProject;
|
|
10
|
+
exports.getProjectBySlug = getProjectBySlug;
|
|
11
|
+
exports.getProjectByPath = getProjectByPath;
|
|
12
|
+
exports.addProject = addProject;
|
|
13
|
+
exports.removeProject = removeProject;
|
|
14
|
+
exports.touchProject = touchProject;
|
|
15
|
+
exports.getDesktopSetting = getDesktopSetting;
|
|
16
|
+
exports.setDesktopSetting = setDesktopSetting;
|
|
17
|
+
exports.setProjectSetupSession = setProjectSetupSession;
|
|
18
|
+
exports.getProjectSetupSession = getProjectSetupSession;
|
|
19
|
+
exports.clearProjectSetupSession = clearProjectSetupSession;
|
|
20
|
+
exports.listAgents = listAgents;
|
|
21
|
+
exports.getAgent = getAgent;
|
|
22
|
+
exports.getAgentBySlug = getAgentBySlug;
|
|
23
|
+
exports.addAgent = addAgent;
|
|
24
|
+
exports.updateAgent = updateAgent;
|
|
25
|
+
exports.findAgentByCurrentJobId = findAgentByCurrentJobId;
|
|
26
|
+
exports.clearAgentJob = clearAgentJob;
|
|
27
|
+
exports.listWebhooks = listWebhooks;
|
|
28
|
+
exports.listWebhooksForProject = listWebhooksForProject;
|
|
29
|
+
exports.getWebhook = getWebhook;
|
|
30
|
+
exports.addWebhook = addWebhook;
|
|
31
|
+
exports.updateWebhook = updateWebhook;
|
|
32
|
+
exports.removeWebhook = removeWebhook;
|
|
33
|
+
const fs_1 = __importDefault(require("fs"));
|
|
34
|
+
const path_1 = __importDefault(require("path"));
|
|
35
|
+
const os_1 = __importDefault(require("os"));
|
|
36
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
37
|
+
const secure_fs_1 = require("./util/secure-fs");
|
|
38
|
+
function parseProviders(raw, primary) {
|
|
39
|
+
if (raw) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((x) => typeof x === 'string')) {
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* fall through to single-provider fallback */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [primary];
|
|
51
|
+
}
|
|
52
|
+
function mapProjectRow(raw) {
|
|
53
|
+
if (!raw)
|
|
54
|
+
return undefined;
|
|
55
|
+
const provider = (raw.provider ?? 'claude');
|
|
56
|
+
return { ...raw, provider, providers: parseProviders(raw.providers, provider) };
|
|
57
|
+
}
|
|
58
|
+
// ─── Desktop DB path ──────────────────────────────────────────────────────────
|
|
59
|
+
function getDesktopDbPath() {
|
|
60
|
+
return path_1.default.join(os_1.default.homedir(), '.specrails', 'desktop.sqlite');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Rebrand migration (Specrails Hub → Specrails Desktop): when the legacy
|
|
64
|
+
* `hub.sqlite` exists next to the requested `desktop.sqlite` and the new file
|
|
65
|
+
* does not exist yet, rename it (plus any `-wal`/`-shm` siblings) so user data
|
|
66
|
+
* is preserved across the rename. Legacy filename allowed here only —
|
|
67
|
+
* migration/compat code.
|
|
68
|
+
*/
|
|
69
|
+
function migrateLegacyDbFile(dbPath) {
|
|
70
|
+
if (dbPath === ':memory:')
|
|
71
|
+
return;
|
|
72
|
+
if (path_1.default.basename(dbPath) !== 'desktop.sqlite')
|
|
73
|
+
return;
|
|
74
|
+
const legacyPath = path_1.default.join(path_1.default.dirname(dbPath), 'hub.sqlite'); // legacy (pre-rebrand) filename
|
|
75
|
+
try {
|
|
76
|
+
if (fs_1.default.existsSync(legacyPath) && !fs_1.default.existsSync(dbPath)) {
|
|
77
|
+
fs_1.default.renameSync(legacyPath, dbPath);
|
|
78
|
+
for (const suffix of ['-wal', '-shm']) {
|
|
79
|
+
if (fs_1.default.existsSync(legacyPath + suffix)) {
|
|
80
|
+
fs_1.default.renameSync(legacyPath + suffix, dbPath + suffix);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.warn('[desktop-db] could not migrate legacy hub.sqlite:', err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function getProjectDbPath(slug) {
|
|
90
|
+
return path_1.default.join(os_1.default.homedir(), '.specrails', 'projects', slug, 'jobs.sqlite');
|
|
91
|
+
}
|
|
92
|
+
// ─── Schema migrations ────────────────────────────────────────────────────────
|
|
93
|
+
// NOTE: existing migrations below intentionally keep the legacy `hub_settings`
|
|
94
|
+
// table name — they are append-only history and must stay byte-identical for
|
|
95
|
+
// already-migrated databases. Migration 13 renames the table to
|
|
96
|
+
// `desktop_settings`; fresh databases run 1→13 in order and converge on the
|
|
97
|
+
// new name. Legacy identifiers allowed here only — migration/compat code.
|
|
98
|
+
function applyDesktopMigrations(db) {
|
|
99
|
+
db.exec(`
|
|
100
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
101
|
+
version INTEGER PRIMARY KEY,
|
|
102
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
103
|
+
);
|
|
104
|
+
`);
|
|
105
|
+
const appliedVersions = new Set(db.prepare('SELECT version FROM schema_migrations').all()
|
|
106
|
+
.map((r) => r.version));
|
|
107
|
+
const migrations = [
|
|
108
|
+
// Migration 1: projects and hub_settings tables
|
|
109
|
+
() => {
|
|
110
|
+
db.exec(`
|
|
111
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
112
|
+
id TEXT PRIMARY KEY,
|
|
113
|
+
slug TEXT NOT NULL UNIQUE,
|
|
114
|
+
name TEXT NOT NULL,
|
|
115
|
+
path TEXT NOT NULL UNIQUE,
|
|
116
|
+
db_path TEXT NOT NULL,
|
|
117
|
+
added_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
118
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_projects_slug ON projects(slug);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_projects_path ON projects(path);
|
|
123
|
+
|
|
124
|
+
CREATE TABLE IF NOT EXISTS hub_settings (
|
|
125
|
+
key TEXT PRIMARY KEY,
|
|
126
|
+
value TEXT NOT NULL
|
|
127
|
+
);
|
|
128
|
+
`);
|
|
129
|
+
},
|
|
130
|
+
// Migration 2: agents table
|
|
131
|
+
() => {
|
|
132
|
+
db.exec(`
|
|
133
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
134
|
+
id TEXT PRIMARY KEY,
|
|
135
|
+
slug TEXT NOT NULL UNIQUE,
|
|
136
|
+
name TEXT NOT NULL,
|
|
137
|
+
role TEXT,
|
|
138
|
+
status TEXT NOT NULL DEFAULT 'idle',
|
|
139
|
+
current_job_id TEXT,
|
|
140
|
+
last_heartbeat_at TEXT,
|
|
141
|
+
config TEXT,
|
|
142
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
CREATE INDEX IF NOT EXISTS idx_agents_slug ON agents(slug);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_agents_current_job_id ON agents(current_job_id);
|
|
147
|
+
`);
|
|
148
|
+
},
|
|
149
|
+
// Migration 3: add provider column to projects
|
|
150
|
+
() => {
|
|
151
|
+
db.exec(`ALTER TABLE projects ADD COLUMN provider TEXT NOT NULL DEFAULT 'claude'`);
|
|
152
|
+
},
|
|
153
|
+
// Migration 4: webhooks table
|
|
154
|
+
() => {
|
|
155
|
+
db.exec(`
|
|
156
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
157
|
+
id TEXT PRIMARY KEY,
|
|
158
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
159
|
+
url TEXT NOT NULL,
|
|
160
|
+
secret TEXT NOT NULL DEFAULT '',
|
|
161
|
+
events TEXT NOT NULL DEFAULT '["job.completed","job.failed"]',
|
|
162
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
163
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
164
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_project_id ON webhooks(project_id);
|
|
168
|
+
`);
|
|
169
|
+
},
|
|
170
|
+
// Migration 5: seed terminal-settings defaults under reserved settings keys.
|
|
171
|
+
// Idempotent — uses INSERT OR IGNORE so re-running never overwrites a user's
|
|
172
|
+
// chosen value. See server/terminal-settings.ts for the typed access layer.
|
|
173
|
+
() => {
|
|
174
|
+
const seed = db.prepare('INSERT OR IGNORE INTO hub_settings (key, value) VALUES (?, ?)');
|
|
175
|
+
seed.run('terminal.fontFamily', "'DM Mono', 'JetBrains Mono', ui-monospace, Menlo, monospace");
|
|
176
|
+
seed.run('terminal.fontSize', '12');
|
|
177
|
+
seed.run('terminal.renderMode', 'auto');
|
|
178
|
+
seed.run('terminal.copyOnSelect', 'false');
|
|
179
|
+
seed.run('terminal.shellIntegrationEnabled', 'true');
|
|
180
|
+
seed.run('terminal.notifyOnCompletion', 'true');
|
|
181
|
+
seed.run('terminal.imageRendering', 'true');
|
|
182
|
+
seed.run('terminal.longCommandThresholdMs', '60000');
|
|
183
|
+
},
|
|
184
|
+
// Migration 6: seed browserShortcutUrl default for the new Browser shortcut button.
|
|
185
|
+
() => {
|
|
186
|
+
db.prepare('INSERT OR IGNORE INTO hub_settings (key, value) VALUES (?, ?)').run('terminal.browserShortcutUrl', 'https://specrails.dev');
|
|
187
|
+
},
|
|
188
|
+
// Migration 7: seed quickScript default for the Quick Script shortcut button.
|
|
189
|
+
// Uses the OS username so the default greets you by name.
|
|
190
|
+
() => {
|
|
191
|
+
let username = 'friend';
|
|
192
|
+
try {
|
|
193
|
+
username = os_1.default.userInfo().username || 'friend';
|
|
194
|
+
}
|
|
195
|
+
catch { /* keep fallback */ }
|
|
196
|
+
const value = `echo "Wake up, ${username} (edit this snippet in settings to help your local development)"`;
|
|
197
|
+
db.prepare('INSERT OR IGNORE INTO hub_settings (key, value) VALUES (?, ?)').run('terminal.quickScript', value);
|
|
198
|
+
},
|
|
199
|
+
// Migration 8: seed default ui_theme for the app-wide theme system.
|
|
200
|
+
// Allow-list enforced at the route layer (server/desktop-router.ts) and the
|
|
201
|
+
// client (client/src/lib/themes.ts).
|
|
202
|
+
() => {
|
|
203
|
+
db.prepare('INSERT OR IGNORE INTO hub_settings (key, value) VALUES (?, ?)').run('ui_theme', 'specrails');
|
|
204
|
+
},
|
|
205
|
+
// Migration 9: seed code-explorer app settings — summary language + monthly
|
|
206
|
+
// budget cap. Enforced by FileSummaryManager + desktop-router.
|
|
207
|
+
() => {
|
|
208
|
+
const seed = db.prepare('INSERT OR IGNORE INTO hub_settings (key, value) VALUES (?, ?)');
|
|
209
|
+
seed.run('summary_language', 'en');
|
|
210
|
+
seed.run('summary_monthly_budget_usd', '5.00');
|
|
211
|
+
},
|
|
212
|
+
// Migration 10: multi-provider per project. Add a `providers` JSON-array
|
|
213
|
+
// column alongside the existing primary `provider` column and backfill it
|
|
214
|
+
// from the current single provider so legacy projects become
|
|
215
|
+
// providers=["<provider>"] (length 1 → behaves exactly as before).
|
|
216
|
+
() => {
|
|
217
|
+
db.exec(`ALTER TABLE projects ADD COLUMN providers TEXT NOT NULL DEFAULT '[]'`);
|
|
218
|
+
db.exec(`UPDATE projects SET providers = json_array(provider) WHERE providers IS NULL OR providers = '[]'`);
|
|
219
|
+
},
|
|
220
|
+
// Migration 11: self-heal the `providers` column. An earlier WIP of the
|
|
221
|
+
// multi-provider feature also numbered a migration #10 but used different
|
|
222
|
+
// column names (`installed_engines` / `default_engine`). On a DB that ran
|
|
223
|
+
// that variant, version 10 is already recorded so Migration 10 above is
|
|
224
|
+
// skipped and `providers` is missing — addProject's INSERT then fails. This
|
|
225
|
+
// higher-numbered migration is guarded by a column check so it is a no-op on
|
|
226
|
+
// fresh DBs (where #10 already added the column) and repairs the orphaned-
|
|
227
|
+
// WIP DBs, backfilling from `installed_engines` when present else `provider`.
|
|
228
|
+
() => {
|
|
229
|
+
const cols = db.prepare('PRAGMA table_info(projects)').all().map((c) => c.name);
|
|
230
|
+
if (!cols.includes('providers')) {
|
|
231
|
+
db.exec(`ALTER TABLE projects ADD COLUMN providers TEXT NOT NULL DEFAULT '[]'`);
|
|
232
|
+
}
|
|
233
|
+
const hasInstalledEngines = cols.includes('installed_engines');
|
|
234
|
+
const rows = db.prepare(`SELECT id, provider, providers${hasInstalledEngines ? ', installed_engines' : ''} FROM projects`).all();
|
|
235
|
+
const upd = db.prepare('UPDATE projects SET providers = ? WHERE id = ?');
|
|
236
|
+
for (const r of rows) {
|
|
237
|
+
if (r.providers && r.providers !== '[]')
|
|
238
|
+
continue;
|
|
239
|
+
let value = null;
|
|
240
|
+
if (hasInstalledEngines && r.installed_engines) {
|
|
241
|
+
try {
|
|
242
|
+
const parsed = JSON.parse(r.installed_engines);
|
|
243
|
+
if (Array.isArray(parsed) && parsed.length > 0)
|
|
244
|
+
value = JSON.stringify(parsed);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
/* fall through to provider-based backfill */
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (!value)
|
|
251
|
+
value = JSON.stringify([r.provider ?? 'claude']);
|
|
252
|
+
upd.run(value, r.id);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
// Migration 12: mobile companion devices. Each paired phone/tablet gets a
|
|
256
|
+
// hashed per-device bearer token (never the master server token), scoped to
|
|
257
|
+
// `companion`, bound to the gateway cert fingerprint active at pair time
|
|
258
|
+
// (rotating the cert revokes every device), with a sliding-expiry last_seen
|
|
259
|
+
// and a one-tap revoke (revoked_at). See server/mobile/* for the gateway.
|
|
260
|
+
() => {
|
|
261
|
+
db.exec(`
|
|
262
|
+
CREATE TABLE IF NOT EXISTS mobile_devices (
|
|
263
|
+
id TEXT PRIMARY KEY,
|
|
264
|
+
name TEXT NOT NULL,
|
|
265
|
+
platform TEXT NOT NULL,
|
|
266
|
+
token_hash TEXT NOT NULL,
|
|
267
|
+
scopes TEXT NOT NULL DEFAULT 'companion',
|
|
268
|
+
cert_fingerprint TEXT NOT NULL,
|
|
269
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
270
|
+
last_seen_at TEXT,
|
|
271
|
+
last_ip TEXT,
|
|
272
|
+
revoked_at TEXT
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
CREATE INDEX IF NOT EXISTS idx_mobile_devices_token ON mobile_devices(token_hash);
|
|
276
|
+
`);
|
|
277
|
+
},
|
|
278
|
+
// Migration 13: Specrails Hub → Specrails Desktop rebrand. Renames the
|
|
279
|
+
// legacy `hub_settings` table to `desktop_settings` (guard: skip if a
|
|
280
|
+
// future code path ever created `desktop_settings` directly), migrates the
|
|
281
|
+
// `hub_daily_budget_usd` settings key to `desktop_daily_budget_usd`, and
|
|
282
|
+
// rewrites stored webhook event subscriptions from
|
|
283
|
+
// `hub_daily_budget_exceeded` to `desktop_daily_budget_exceeded`.
|
|
284
|
+
// Legacy identifiers allowed here only — migration/compat code.
|
|
285
|
+
() => {
|
|
286
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all()
|
|
287
|
+
.map((t) => t.name);
|
|
288
|
+
if (tables.includes('hub_settings') && !tables.includes('desktop_settings')) {
|
|
289
|
+
db.exec('ALTER TABLE hub_settings RENAME TO desktop_settings');
|
|
290
|
+
}
|
|
291
|
+
db.prepare("UPDATE OR IGNORE desktop_settings SET key = 'desktop_daily_budget_usd' WHERE key = 'hub_daily_budget_usd'").run();
|
|
292
|
+
const hooks = db.prepare("SELECT id, events FROM webhooks WHERE events LIKE '%hub_daily_budget_exceeded%'").all();
|
|
293
|
+
const upd = db.prepare('UPDATE webhooks SET events = ? WHERE id = ?');
|
|
294
|
+
for (const h of hooks) {
|
|
295
|
+
upd.run(h.events.split('hub_daily_budget_exceeded').join('desktop_daily_budget_exceeded'), h.id);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
for (let i = 0; i < migrations.length; i++) {
|
|
300
|
+
const version = i + 1;
|
|
301
|
+
if (!appliedVersions.has(version)) {
|
|
302
|
+
migrations[i]();
|
|
303
|
+
db.prepare('INSERT OR IGNORE INTO schema_migrations (version) VALUES (?)').run(version);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
308
|
+
function initDesktopDb(dbPath = getDesktopDbPath()) {
|
|
309
|
+
const dir = path_1.default.dirname(dbPath);
|
|
310
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
311
|
+
if (dbPath !== ':memory:')
|
|
312
|
+
(0, secure_fs_1.secureDir)(dir); // H-13: owner-only ~/.specrails
|
|
313
|
+
// Rebrand compat: pick up a pre-rename hub.sqlite before opening.
|
|
314
|
+
migrateLegacyDbFile(dbPath);
|
|
315
|
+
const db = new better_sqlite3_1.default(dbPath);
|
|
316
|
+
db.pragma('journal_mode = WAL');
|
|
317
|
+
db.pragma('foreign_keys = ON');
|
|
318
|
+
applyDesktopMigrations(db);
|
|
319
|
+
// H-13: desktop.sqlite stores webhook HMAC secrets in plaintext — restrict it
|
|
320
|
+
// (and its WAL sidecars) to owner read/write.
|
|
321
|
+
if (dbPath !== ':memory:')
|
|
322
|
+
(0, secure_fs_1.secureDbFile)(dbPath);
|
|
323
|
+
return db;
|
|
324
|
+
}
|
|
325
|
+
function listProjects(db) {
|
|
326
|
+
return db.prepare('SELECT * FROM projects ORDER BY added_at ASC').all().map((r) => mapProjectRow(r));
|
|
327
|
+
}
|
|
328
|
+
function getProject(db, id) {
|
|
329
|
+
return mapProjectRow(db.prepare('SELECT * FROM projects WHERE id = ?').get(id));
|
|
330
|
+
}
|
|
331
|
+
function getProjectBySlug(db, slug) {
|
|
332
|
+
return mapProjectRow(db.prepare('SELECT * FROM projects WHERE slug = ?').get(slug));
|
|
333
|
+
}
|
|
334
|
+
function getProjectByPath(db, projectPath) {
|
|
335
|
+
return mapProjectRow(db.prepare('SELECT * FROM projects WHERE path = ?').get(projectPath));
|
|
336
|
+
}
|
|
337
|
+
function addProject(db, project) {
|
|
338
|
+
const dbPath = getProjectDbPath(project.slug);
|
|
339
|
+
const providers = project.providers && project.providers.length > 0
|
|
340
|
+
? project.providers
|
|
341
|
+
: [project.provider ?? 'claude'];
|
|
342
|
+
// Primary provider = explicit override, else the first selected provider.
|
|
343
|
+
const provider = project.provider ?? providers[0];
|
|
344
|
+
db.prepare(`
|
|
345
|
+
INSERT INTO projects (id, slug, name, path, db_path, provider, providers)
|
|
346
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
347
|
+
`).run(project.id, project.slug, project.name, project.path, dbPath, provider, JSON.stringify(providers));
|
|
348
|
+
return getProject(db, project.id);
|
|
349
|
+
}
|
|
350
|
+
function removeProject(db, id) {
|
|
351
|
+
db.prepare('DELETE FROM projects WHERE id = ?').run(id);
|
|
352
|
+
}
|
|
353
|
+
function touchProject(db, id) {
|
|
354
|
+
db.prepare("UPDATE projects SET last_seen_at = datetime('now') WHERE id = ?").run(id);
|
|
355
|
+
}
|
|
356
|
+
function getDesktopSetting(db, key) {
|
|
357
|
+
const row = db.prepare('SELECT value FROM desktop_settings WHERE key = ?').get(key);
|
|
358
|
+
return row?.value;
|
|
359
|
+
}
|
|
360
|
+
function setDesktopSetting(db, key, value) {
|
|
361
|
+
db.prepare('INSERT OR REPLACE INTO desktop_settings (key, value) VALUES (?, ?)').run(key, value);
|
|
362
|
+
}
|
|
363
|
+
// ─── Setup session persistence ────────────────────────────────────────────────
|
|
364
|
+
function setProjectSetupSession(db, projectId, sessionId) {
|
|
365
|
+
setDesktopSetting(db, `setup_session:${projectId}`, sessionId);
|
|
366
|
+
}
|
|
367
|
+
function getProjectSetupSession(db, projectId) {
|
|
368
|
+
return getDesktopSetting(db, `setup_session:${projectId}`);
|
|
369
|
+
}
|
|
370
|
+
function clearProjectSetupSession(db, projectId) {
|
|
371
|
+
db.prepare('DELETE FROM desktop_settings WHERE key = ?').run(`setup_session:${projectId}`);
|
|
372
|
+
}
|
|
373
|
+
// ─── Agent CRUD ───────────────────────────────────────────────────────────────
|
|
374
|
+
function listAgents(db) {
|
|
375
|
+
return db.prepare('SELECT * FROM agents ORDER BY created_at ASC').all();
|
|
376
|
+
}
|
|
377
|
+
function getAgent(db, id) {
|
|
378
|
+
return db.prepare('SELECT * FROM agents WHERE id = ?').get(id);
|
|
379
|
+
}
|
|
380
|
+
function getAgentBySlug(db, slug) {
|
|
381
|
+
return db.prepare('SELECT * FROM agents WHERE slug = ?').get(slug);
|
|
382
|
+
}
|
|
383
|
+
function addAgent(db, agent) {
|
|
384
|
+
db.prepare(`
|
|
385
|
+
INSERT INTO agents (id, slug, name, role, config)
|
|
386
|
+
VALUES (?, ?, ?, ?, ?)
|
|
387
|
+
`).run(agent.id, agent.slug, agent.name, agent.role ?? null, agent.config ?? null);
|
|
388
|
+
return db.prepare('SELECT * FROM agents WHERE id = ?').get(agent.id);
|
|
389
|
+
}
|
|
390
|
+
// B72: column names are interpolated into the SET clause below. TS restricts the
|
|
391
|
+
// keys at compile time, but a runtime caller passing an attacker-influenced
|
|
392
|
+
// object would otherwise inject SQL via the key. Gate on an explicit allow-list.
|
|
393
|
+
const UPDATABLE_AGENT_COLUMNS = new Set([
|
|
394
|
+
'name', 'role', 'status', 'current_job_id', 'last_heartbeat_at', 'config',
|
|
395
|
+
]);
|
|
396
|
+
function updateAgent(db, id, updates) {
|
|
397
|
+
const fields = Object.keys(updates)
|
|
398
|
+
.filter((f) => UPDATABLE_AGENT_COLUMNS.has(f));
|
|
399
|
+
if (fields.length === 0)
|
|
400
|
+
return getAgent(db, id);
|
|
401
|
+
const setClauses = fields.map((f) => `${f} = ?`).join(', ');
|
|
402
|
+
const values = fields.map((f) => updates[f] ?? null);
|
|
403
|
+
db.prepare(`UPDATE agents SET ${setClauses} WHERE id = ?`).run(...values, id);
|
|
404
|
+
return db.prepare('SELECT * FROM agents WHERE id = ?').get(id);
|
|
405
|
+
}
|
|
406
|
+
function findAgentByCurrentJobId(db, jobId) {
|
|
407
|
+
return db.prepare('SELECT * FROM agents WHERE current_job_id = ?').get(jobId);
|
|
408
|
+
}
|
|
409
|
+
function clearAgentJob(db, jobId) {
|
|
410
|
+
db.prepare("UPDATE agents SET status = 'idle', current_job_id = NULL WHERE current_job_id = ? AND status != 'idle'").run(jobId);
|
|
411
|
+
}
|
|
412
|
+
function listWebhooks(db) {
|
|
413
|
+
return db.prepare('SELECT * FROM webhooks ORDER BY created_at ASC').all();
|
|
414
|
+
}
|
|
415
|
+
function listWebhooksForProject(db, projectId) {
|
|
416
|
+
return db.prepare('SELECT * FROM webhooks WHERE enabled = 1 AND (project_id IS NULL OR project_id = ?) ORDER BY created_at ASC').all(projectId);
|
|
417
|
+
}
|
|
418
|
+
function getWebhook(db, id) {
|
|
419
|
+
return db.prepare('SELECT * FROM webhooks WHERE id = ?').get(id);
|
|
420
|
+
}
|
|
421
|
+
function addWebhook(db, webhook) {
|
|
422
|
+
const events = JSON.stringify(webhook.events ?? ['job.completed', 'job.failed']);
|
|
423
|
+
db.prepare(`
|
|
424
|
+
INSERT INTO webhooks (id, project_id, url, secret, events)
|
|
425
|
+
VALUES (?, ?, ?, ?, ?)
|
|
426
|
+
`).run(webhook.id, webhook.projectId ?? null, webhook.url, webhook.secret ?? '', events);
|
|
427
|
+
return db.prepare('SELECT * FROM webhooks WHERE id = ?').get(webhook.id);
|
|
428
|
+
}
|
|
429
|
+
function updateWebhook(db, id, updates) {
|
|
430
|
+
const fields = [];
|
|
431
|
+
const values = [];
|
|
432
|
+
if (updates.url !== undefined) {
|
|
433
|
+
fields.push('url = ?');
|
|
434
|
+
values.push(updates.url);
|
|
435
|
+
}
|
|
436
|
+
if (updates.secret !== undefined) {
|
|
437
|
+
fields.push('secret = ?');
|
|
438
|
+
values.push(updates.secret);
|
|
439
|
+
}
|
|
440
|
+
if (updates.events !== undefined) {
|
|
441
|
+
fields.push('events = ?');
|
|
442
|
+
values.push(JSON.stringify(updates.events));
|
|
443
|
+
}
|
|
444
|
+
if (updates.enabled !== undefined) {
|
|
445
|
+
fields.push('enabled = ?');
|
|
446
|
+
values.push(updates.enabled ? 1 : 0);
|
|
447
|
+
}
|
|
448
|
+
if (fields.length === 0)
|
|
449
|
+
return getWebhook(db, id);
|
|
450
|
+
fields.push("updated_at = datetime('now')");
|
|
451
|
+
db.prepare(`UPDATE webhooks SET ${fields.join(', ')} WHERE id = ?`).run(...values, id);
|
|
452
|
+
return db.prepare('SELECT * FROM webhooks WHERE id = ?').get(id);
|
|
453
|
+
}
|
|
454
|
+
function removeWebhook(db, id) {
|
|
455
|
+
db.prepare('DELETE FROM webhooks WHERE id = ?').run(id);
|
|
456
|
+
}
|