@computesdk/loops 0.0.0-stage → 0.1.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/API.md +106 -0
- package/README.md +221 -2
- package/bin/loops.mjs +188 -0
- package/bin/pi-loops-ui.mjs +4 -0
- package/client.ts +208 -0
- package/dist/assets/abap-BdImnpbu.js +1 -0
- package/dist/assets/abnfDiagram-VCTEODGH-DPUvPEpH.js +1 -0
- package/dist/assets/actionscript-3-B3316cI-.js +1 -0
- package/dist/assets/ada-BIOnG6TC.js +1 -0
- package/dist/assets/ahk-BrbAagtO.js +1 -0
- package/dist/assets/ahk2-8Zs4aa1G.js +1 -0
- package/dist/assets/andromeeda-C4gqWexZ.js +1 -0
- package/dist/assets/angular-html-D_SYSyRt.js +1 -0
- package/dist/assets/angular-ts-DGq0tZX5.js +1 -0
- package/dist/assets/apache-Pmp26Uib.js +1 -0
- package/dist/assets/apex-DhZFqWV2.js +1 -0
- package/dist/assets/apl-ChzvTHjC.js +1 -0
- package/dist/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/assets/ara-BRHolxvo.js +1 -0
- package/dist/assets/arc-C71M1We_.js +1 -0
- package/dist/assets/architectureDiagram-5GKGNRK7-C3PLmrM3.js +36 -0
- package/dist/assets/asciidoc-CSVQ5wI8.js +1 -0
- package/dist/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/assets/astro-DTjqU0_L.js +1 -0
- package/dist/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/assets/awk-DMzUqQB5.js +1 -0
- package/dist/assets/ayu-dark-DYE7WIF3.js +1 -0
- package/dist/assets/ayu-light-BA47KaF1.js +1 -0
- package/dist/assets/ayu-mirage-32ctXXKs.js +1 -0
- package/dist/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/assets/bat-CickPsom.js +1 -0
- package/dist/assets/beancount-BgSHGh8x.js +1 -0
- package/dist/assets/berry-uYugtg8r.js +1 -0
- package/dist/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/assets/bird2-Bx8U0n9b.js +1 -0
- package/dist/assets/blade-k33SngOZ.js +1 -0
- package/dist/assets/blockDiagram-I7D4REHJ-DFshwmKE.js +129 -0
- package/dist/assets/bsl-DlhNcFeZ.js +1 -0
- package/dist/assets/c-BIGW1oBm.js +1 -0
- package/dist/assets/c3-Dp5svz6Z.js +1 -0
- package/dist/assets/c4Diagram-7LVT6UL2-Dh29QjGI.js +38 -0
- package/dist/assets/cadence-Bv_4Rxtq.js +1 -0
- package/dist/assets/cairo-KRGpt6FW.js +1 -0
- package/dist/assets/catppuccin-frappe-CZL1YF0i.js +1 -0
- package/dist/assets/catppuccin-latte-DH-8KZSZ.js +1 -0
- package/dist/assets/catppuccin-macchiato-B7yYVSCf.js +1 -0
- package/dist/assets/catppuccin-mocha-Ct7hS0mc.js +1 -0
- package/dist/assets/channel-6AEG2wiQ.js +1 -0
- package/dist/assets/chapel-DTp_pixX.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-qZ7USlsH.js +1 -0
- package/dist/assets/chunk-5VM5RSS4-YQpRymiv.js +15 -0
- package/dist/assets/chunk-F27PBJKO-D2O95m9O.js +1 -0
- package/dist/assets/chunk-IMKFNOWR-CftSjfdl.js +231 -0
- package/dist/assets/chunk-JWPE2WC7-_1wtK-EO.js +1 -0
- package/dist/assets/chunk-POPQ4Y6H-DJ2Xmp8X.js +1 -0
- package/dist/assets/chunk-SVP7TREG-CAtSHcje.js +88 -0
- package/dist/assets/chunk-TICWLB2K-1BASqzB5.js +206 -0
- package/dist/assets/chunk-XXDRQBXY-CWs6lP_i.js +1 -0
- package/dist/assets/clarity-Dn5IMItf.js +1 -0
- package/dist/assets/classDiagram-ZZMXUADV-WtwW3JUM.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-WtwW3JUM.js +1 -0
- package/dist/assets/clojure-P80f7IUj.js +1 -0
- package/dist/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/assets/cobol-BBKr1ja6.js +1 -0
- package/dist/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/assets/codeql-DsOJ9woJ.js +1 -0
- package/dist/assets/coffee-Ch7k5sss.js +1 -0
- package/dist/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/dist/assets/coq-BqXF5kFF.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-9dCpEIjz.js +1 -0
- package/dist/assets/cpp-BMRokrvK.js +1 -0
- package/dist/assets/crystal-LUwhHfY5.js +1 -0
- package/dist/assets/csharp-B_TYUkTI.js +1 -0
- package/dist/assets/css-Z72CF80r.js +1 -0
- package/dist/assets/csv-fuZLfV_i.js +1 -0
- package/dist/assets/cue-D82EKSYY.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-EFt_R8HZ.js +166 -0
- package/dist/assets/cynefinDiagram-5FMLGOSQ-BuManjb1.js +62 -0
- package/dist/assets/cypher-COkxafJQ.js +1 -0
- package/dist/assets/cytoscape.esm-BDkkwuE4.js +321 -0
- package/dist/assets/d-85-TOEBH.js +1 -0
- package/dist/assets/dagre-GXQ25YYZ-MovLAGVL.js +4 -0
- package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/assets/dart-CtKP3W-j.js +1 -0
- package/dist/assets/dax-CEL-wOlO.js +1 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/assets/diagram-S7CK7UJ4-BiJw5LQ6.js +30 -0
- package/dist/assets/diagram-UQ7AKVKN-DRsX_xa6.js +41 -0
- package/dist/assets/diagram-VSXAHHWV-B7m6F8Dm.js +3 -0
- package/dist/assets/diagram-VX7I27RA-x4wioeVA.js +24 -0
- package/dist/assets/diagram-Z3DM3KII-0KYjyvJF.js +24 -0
- package/dist/assets/diff-D97Zzqfu.js +1 -0
- package/dist/assets/docker-BcOcwvcX.js +1 -0
- package/dist/assets/dotenv-De0Btta0.js +1 -0
- package/dist/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/assets/ebnfDiagram-PWID7BFC-BZXmt3oU.js +1 -0
- package/dist/assets/edge-CJaD-t2L.js +1 -0
- package/dist/assets/elixir-DkJLSxtU.js +1 -0
- package/dist/assets/elm-DbKCFpqz.js +1 -0
- package/dist/assets/emacs-lisp-DaIrSCgJ.js +1 -0
- package/dist/assets/erDiagram-RLTQ6QDP-CrfcYtD6.js +99 -0
- package/dist/assets/erb-CSMaCouu.js +1 -0
- package/dist/assets/erlang-DsQrWhSR.js +1 -0
- package/dist/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/assets/fennel-BYunw83y.js +1 -0
- package/dist/assets/fish-BvzEVeQv.js +1 -0
- package/dist/assets/flowDiagram-HODETNUW-BqudKoEy.js +168 -0
- package/dist/assets/fluent-C4IJs8-o.js +1 -0
- package/dist/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- package/dist/assets/fortran-free-form-BxgE0vQu.js +1 -0
- package/dist/assets/fsharp-CXgrBDvD.js +1 -0
- package/dist/assets/ganttDiagram-EL5Y4UJY-OBWjueb8.js +292 -0
- package/dist/assets/gdresource-TyuKm33G.js +1 -0
- package/dist/assets/gdscript-DqcFQ5yU.js +1 -0
- package/dist/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/assets/genie-D0YGMca9.js +1 -0
- package/dist/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/assets/git-rebase-r7XF79zn.js +1 -0
- package/dist/assets/gitGraphDiagram-WWUBYQGX-B30e199R.js +106 -0
- package/dist/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/assets/gleam-BspZqrRM.js +1 -0
- package/dist/assets/glimmer-js-Drrc9bTt.js +1 -0
- package/dist/assets/glimmer-ts-zeybXsvi.js +1 -0
- package/dist/assets/glsl-DplSGwfg.js +1 -0
- package/dist/assets/gn-8iBmfbXw.js +1 -0
- package/dist/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/assets/go-C27-OAKa.js +1 -0
- package/dist/assets/graphql-ChdNCCLP.js +1 -0
- package/dist/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/dist/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/dist/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/dist/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/dist/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/dist/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/dist/assets/hack-Ck6-gGSS.js +1 -0
- package/dist/assets/haml-GN_XdMnF.js +1 -0
- package/dist/assets/handlebars-DPT5vKgj.js +1 -0
- package/dist/assets/haskell-Df6bDoY_.js +1 -0
- package/dist/assets/haxe-CfZj7gIn.js +1 -0
- package/dist/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/assets/highlighted-body-JLMTFACH-DapGkHdb.js +1 -0
- package/dist/assets/hjson-D5-asLiD.js +1 -0
- package/dist/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/assets/horizon-BUw7H-hv.js +1 -0
- package/dist/assets/horizon-bright-CUuTKBJd.js +1 -0
- package/dist/assets/houston-DnULxvSX.js +1 -0
- package/dist/assets/html-BlS82ilI.js +1 -0
- package/dist/assets/html-derivative-DZhk3IEZ.js +1 -0
- package/dist/assets/http-jrhK8wxY.js +1 -0
- package/dist/assets/hurl-irOxFIW8.js +1 -0
- package/dist/assets/hxml-2-FPmUDs.js +1 -0
- package/dist/assets/hy-DFXneXwc.js +1 -0
- package/dist/assets/imba-DGztddWO.js +1 -0
- package/dist/assets/index-C_bKTXmh.js +900 -0
- package/dist/assets/index-hKoeppfe.css +1 -0
- package/dist/assets/infoDiagram-27XIBGKW-DPwVvB4K.js +2 -0
- package/dist/assets/ini-BEwlwnbL.js +1 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-5VMMS53U-B0fvRdQL.js +70 -0
- package/dist/assets/java-CylS5w8V.js +1 -0
- package/dist/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/assets/jinja-DFQZVq3b.js +1 -0
- package/dist/assets/jison-wvAkD_A8.js +1 -0
- package/dist/assets/journeyDiagram-3NMN7TZE-CZBLF7ld.js +139 -0
- package/dist/assets/json-Cp-IABpG.js +1 -0
- package/dist/assets/json5-C9tS-k6U.js +1 -0
- package/dist/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/assets/jssm-C2t-YnRu.js +1 -0
- package/dist/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/assets/julia-Cu79e8SL.js +1 -0
- package/dist/assets/just-CMxyWJJc.js +1 -0
- package/dist/assets/kanagawa-dragon-BuwD2xS4.js +1 -0
- package/dist/assets/kanagawa-lotus-C3DzagqV.js +1 -0
- package/dist/assets/kanagawa-wave-DFGoQZhC.js +1 -0
- package/dist/assets/kanban-definition-UXKFOSKX-wTBJWh9T.js +89 -0
- package/dist/assets/kdl-DV7GczEv.js +1 -0
- package/dist/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/assets/kusto-wEQ09or8.js +1 -0
- package/dist/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/assets/latex-DJVikpzR.js +1 -0
- package/dist/assets/layout-DYBs-gVA.js +1 -0
- package/dist/assets/lean-BZvkOJ9d.js +1 -0
- package/dist/assets/less-B1dDrJ26.js +1 -0
- package/dist/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/assets/linear-DTE3zVP_.js +1 -0
- package/dist/assets/liquid-B-duXHXw.js +1 -0
- package/dist/assets/llvm-BZoOZj88.js +1 -0
- package/dist/assets/log-2UxHyX5q.js +1 -0
- package/dist/assets/logo-BtOb2qkB.js +1 -0
- package/dist/assets/lua-BaeVxFsk.js +1 -0
- package/dist/assets/luau-nOZpfvQi.js +1 -0
- package/dist/assets/make-CHLpvVh8.js +1 -0
- package/dist/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/assets/marko-BYgDmDgU.js +1 -0
- package/dist/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/assets/matlab-D7o27uSR.js +1 -0
- package/dist/assets/mdc-DF4WjDMn.js +1 -0
- package/dist/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/assets/mermaid-CQcHuHx7.js +1 -0
- package/dist/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/assets/min-light-CTRr51gU.js +1 -0
- package/dist/assets/mindmap-definition-YA3MSWOX-tV3HPpzU.js +96 -0
- package/dist/assets/mipsasm-CKIfxQSi.js +1 -0
- package/dist/assets/mojo-DJz3ZmWd.js +1 -0
- package/dist/assets/monokai-CR4piiQ-.js +1 -0
- package/dist/assets/moonbit-Bag4Rp_g.js +1 -0
- package/dist/assets/move-el3G9tDJ.js +1 -0
- package/dist/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/assets/nextflow-BCxp2Aob.js +1 -0
- package/dist/assets/nextflow-groovy-CedDknKX.js +1 -0
- package/dist/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/assets/night-owl-light-CMTm3GFP.js +1 -0
- package/dist/assets/nim-nV83PyhX.js +1 -0
- package/dist/assets/nix-CwoSXNpI.js +1 -0
- package/dist/assets/nord-Ddv68eIx.js +1 -0
- package/dist/assets/nsis-BlV79W_Q.js +1 -0
- package/dist/assets/nushell-BXD5iret.js +1 -0
- package/dist/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/assets/odin-BBf5iR-q.js +1 -0
- package/dist/assets/one-dark-pro-CxGP3dtY.js +1 -0
- package/dist/assets/one-light-C3Wv6jpd.js +1 -0
- package/dist/assets/openscad-C4EeE6gA.js +1 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/org-6a7ccXN_.js +1 -0
- package/dist/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/assets/pegDiagram-XKGWAZYB-B6Yznbb5.js +1 -0
- package/dist/assets/perl-5klg-GXX.js +1 -0
- package/dist/assets/php-DoruYMF0.js +1 -0
- package/dist/assets/pieDiagram-E7YTZNPT-B6QW8pJH.js +39 -0
- package/dist/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/assets/po-BTJTHyun.js +1 -0
- package/dist/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/assets/polar-C0HS_06l.js +1 -0
- package/dist/assets/postcss-CXtECtnM.js +1 -0
- package/dist/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/assets/powershell-Dd0kTxKP.js +1 -0
- package/dist/assets/prisma-Vru482bI.js +1 -0
- package/dist/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/assets/pug-BtYPkAgq.js +1 -0
- package/dist/assets/puppet-BMWR74SV.js +1 -0
- package/dist/assets/purescript-CklMAg4u.js +1 -0
- package/dist/assets/python-B6aJPvgy.js +1 -0
- package/dist/assets/qml-3beO22l8.js +1 -0
- package/dist/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/assets/qss-IeuSbFQv.js +1 -0
- package/dist/assets/quadrantDiagram-AXDQQJYC-DgHGTtj0.js +7 -0
- package/dist/assets/r-Cf5RLm7j.js +1 -0
- package/dist/assets/racket-BqYA7rlc.js +1 -0
- package/dist/assets/railroadDiagram-O6MQD6OU-CgqVFEBK.js +1 -0
- package/dist/assets/raku-DXvB9xmW.js +1 -0
- package/dist/assets/razor-CyjPwjZj.js +1 -0
- package/dist/assets/rbs-CpoqiR4B.js +1 -0
- package/dist/assets/red-DMbB5_pp.js +1 -0
- package/dist/assets/reg-C-SQnVFl.js +1 -0
- package/dist/assets/regexp-CDVJQ6XC.js +1 -0
- package/dist/assets/rel-C3B-1QV4.js +1 -0
- package/dist/assets/requirementDiagram-BXWQKSXE-FmXMY-6j.js +84 -0
- package/dist/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/assets/ron-D8l8udqQ.js +1 -0
- package/dist/assets/rose-pine-5qJOZa0Y.js +1 -0
- package/dist/assets/rose-pine-dawn-zx0QlTCp.js +1 -0
- package/dist/assets/rose-pine-moon-C9pEdX9L.js +1 -0
- package/dist/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/assets/rst-CVOjT0e7.js +1 -0
- package/dist/assets/ruby-C8rp0wew.js +1 -0
- package/dist/assets/rust-DPvrY3IQ.js +1 -0
- package/dist/assets/sankeyDiagram-P5KCCOFB-AVMEYVKy.js +40 -0
- package/dist/assets/sas-BGn39htG.js +1 -0
- package/dist/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/assets/scala-CqE71os6.js +1 -0
- package/dist/assets/scheme-C98Dy4si.js +1 -0
- package/dist/assets/scss-Cw6XMfVs.js +1 -0
- package/dist/assets/sdbl-DVxCFoDh.js +1 -0
- package/dist/assets/sequenceDiagram-WJ2MYXX4-uOObReYi.js +162 -0
- package/dist/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/dist/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/assets/shellsession-BADoaaVG.js +1 -0
- package/dist/assets/sizeCapture-INFHLROL-BPOPqqxk.js +1 -0
- package/dist/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/assets/smalltalk-BOQMe2GC.js +1 -0
- package/dist/assets/smithy-cds9vsN8.js +1 -0
- package/dist/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/assets/solarized-dark-UcubFVte.js +1 -0
- package/dist/assets/solarized-light-DCG28T6R.js +1 -0
- package/dist/assets/solidity-DijEV5ha.js +1 -0
- package/dist/assets/soy-zscZBeg6.js +1 -0
- package/dist/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/assets/splunk-BtCnVYZw.js +1 -0
- package/dist/assets/sql-DkvoVjww.js +1 -0
- package/dist/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/assets/stata-DACIj5bg.js +1 -0
- package/dist/assets/stateDiagram-D77RDMKH-C1lXHNdu.js +1 -0
- package/dist/assets/stateDiagram-v2-MP3YSRHH-BHg8O_NZ.js +1 -0
- package/dist/assets/stylus-BEDo0Tqx.js +1 -0
- package/dist/assets/surrealql-Cjom0U5J.js +1 -0
- package/dist/assets/svelte-D84K1R2Q.js +1 -0
- package/dist/assets/swift-C2oV4EkX.js +1 -0
- package/dist/assets/swimlanes-42K2YHIH-C52GOi05.js +1 -0
- package/dist/assets/swimlanesDiagram-VR7AAH4N-DLB_mWZL.js +8 -0
- package/dist/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/assets/system-verilog-0hqHdDBg.js +1 -0
- package/dist/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/assets/talonscript-CkByrt1z.js +1 -0
- package/dist/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/assets/templ-Vpw5XpSW.js +1 -0
- package/dist/assets/terraform-BETggiCN.js +1 -0
- package/dist/assets/tex-D96PA37w.js +1 -0
- package/dist/assets/timeline-definition-24CTP7MA-T0BhAPL-.js +120 -0
- package/dist/assets/tokyo-night-hegEt444.js +1 -0
- package/dist/assets/toml-vGWfd6FD.js +1 -0
- package/dist/assets/ts-tags-Ls60c9xG.js +1 -0
- package/dist/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/assets/tsx-COt5Ahok.js +1 -0
- package/dist/assets/turtle-BsS91CYL.js +1 -0
- package/dist/assets/twig-D9-LOF2p.js +1 -0
- package/dist/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/assets/typespec-ClX509-Q.js +1 -0
- package/dist/assets/typst-DamkJZ1Q.js +1 -0
- package/dist/assets/v-BGw2Nkan.js +1 -0
- package/dist/assets/vala-CsfeWuGM.js +1 -0
- package/dist/assets/vb-Cu-pLBUe.js +1 -0
- package/dist/assets/vennDiagram-4TSXK5OY-DSGCn4iB.js +34 -0
- package/dist/assets/verilog-nZwndyjY.js +1 -0
- package/dist/assets/vesper-DRje8inN.js +1 -0
- package/dist/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/assets/viml-CJc9bBzg.js +1 -0
- package/dist/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/assets/vue-DSqPwMUe.js +1 -0
- package/dist/assets/vue-html-AaS7Mt5G.js +1 -0
- package/dist/assets/vue-vine-C0RcJ6UU.js +1 -0
- package/dist/assets/vyper-CDx5xZoG.js +1 -0
- package/dist/assets/wardleyDiagram-VM6X3IG4-BaCYx_03.js +78 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/assets/wikitext-BhOHFoWU.js +1 -0
- package/dist/assets/wit-5i3qLPDT.js +1 -0
- package/dist/assets/wolfram-lXgVvXCa.js +1 -0
- package/dist/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/assets/xychartDiagram-S5SC5T6Z-CgPwXvFv.js +7 -0
- package/dist/assets/yaml-Buea-lGh.js +1 -0
- package/dist/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/assets/zig-VOosw3JB.js +1 -0
- package/dist/fonts/geist-mono.woff2 +0 -0
- package/dist/fonts/geist-sans.woff2 +0 -0
- package/dist/index.html +14 -0
- package/dist/server.mjs +4787 -0
- package/package.json +76 -4
- package/src/shared/types.ts +180 -0
package/API.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# The API
|
|
2
|
+
|
|
3
|
+
The SPA in this package is one client of this API. Another app can be another one — that is the point of the split:
|
|
4
|
+
everything a UI needs is HTTP, and the rendering is yours.
|
|
5
|
+
|
|
6
|
+
- **Base**: `http://<host>:<port>/api/v1` (the same routes also answer on `/api`, which is what this package's own
|
|
7
|
+
client uses).
|
|
8
|
+
- **Auth**: `Authorization: Bearer <token>`. `GET /api/v1/meta` and `GET /api/health` are open. A server on loopback
|
|
9
|
+
may have no token at all; a server that listens on anything else always does.
|
|
10
|
+
- **Errors**: a failed request is `{"error": "..."}` with a 4xx/5xx. Success bodies are JSON unless noted.
|
|
11
|
+
- **CORS**: any origin unless the server was started with `--allow-origin a,b`. The token is the authorization, not a
|
|
12
|
+
cookie, so credentials are never sent implicitly.
|
|
13
|
+
|
|
14
|
+
A typed client with no dependencies lives in [`client.ts`](./client.ts):
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { LoopsClient } from "@computesdk/pi-loops-ui";
|
|
18
|
+
|
|
19
|
+
const loops = new LoopsClient({ server: "http://box:3000", token: process.env.LOOPS_TOKEN! });
|
|
20
|
+
const off = loops.events(chat, {
|
|
21
|
+
state: (s) => render(s), // whole picture, every change
|
|
22
|
+
error: (e) => console.warn(e), // the stream reconnects on its own
|
|
23
|
+
});
|
|
24
|
+
await loops.prompt(chat, "make search() faster; bench is node bench.mjs");
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Reading
|
|
28
|
+
|
|
29
|
+
| Route | Returns |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `GET /meta` | name, api version, package version, host, whether a token is required, and the workspaces |
|
|
32
|
+
| `GET /workspaces` | `Workspace[]`: `{ id, path, sessionDir, title }` — the repos this server may loop in |
|
|
33
|
+
| `GET /chats?ws=` | `ChatInfo[]`: `{ id, title, last, ws, summary }`, newest first, all repos or one |
|
|
34
|
+
| `GET /state?chat=` | `ChatState`: `{ chat, title, items, live, runs, questions, dialogs, workspaces, ws, proposals }` |
|
|
35
|
+
| `GET /chats/:id/runs` | `Run[]` for a canvas, without the transcript |
|
|
36
|
+
| `GET /config?chat=` | `{ cwd, keys: [{ key, value, source }] }` — each setting with the file it came from |
|
|
37
|
+
| `GET /session?run=&name=` | one attempt's transcript (`ParentItem[]`) |
|
|
38
|
+
| `GET /diff?run=&name=` | `text/plain` unified diff (`name` is an attempt label or a style; `best` works too) |
|
|
39
|
+
| `GET /shot?run=&attempt=&f=` | `image/png` — an attempt's screenshot |
|
|
40
|
+
| `GET /sheet?run=&f=` | `image/png` — the round's contact sheet |
|
|
41
|
+
| `GET /size?run=` | `{ bytes, dirs }` — what a run's worktrees hold (what `clean` would free) |
|
|
42
|
+
| `GET /ui?chat=` | `extension_ui_request[]` still waiting for an answer |
|
|
43
|
+
|
|
44
|
+
### The event stream
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
GET /chats/:id/events Accept: text/event-stream
|
|
48
|
+
|
|
49
|
+
id: 6f3a… ← the session entry cursor
|
|
50
|
+
event: state ← the same object GET /state returns
|
|
51
|
+
event: proposals ← an agent rewrote .pi/loops.json and the server held it
|
|
52
|
+
event: ping ← keep-alive, every 20s
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The first frame is always a full `state`, so a client paints immediately and never needs a second call to bootstrap.
|
|
56
|
+
On reconnect send `Last-Event-ID: <the last id you saw>`; the server answers with a snapshot stamped with the newest
|
|
57
|
+
cursor, so nothing is missed while the client was away. `EventSource` cannot set headers — use `fetch` + a stream
|
|
58
|
+
reader (the client above does) so the token can travel in `Authorization`.
|
|
59
|
+
|
|
60
|
+
## Writing
|
|
61
|
+
|
|
62
|
+
| Route | Body | Effect |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `POST /chat` | `{ ws? }` | new chat in a workspace (default: the server's repo) |
|
|
65
|
+
| `POST /rename` | `{ chat, name }` | names the Pi session, so the TUI sees it too |
|
|
66
|
+
| `POST /chat/delete` | `{ chat }` | deletes the conversation; its loops stay in the repo |
|
|
67
|
+
| `POST /workspaces` | `{ path }` | add a repo (path on the server's machine) |
|
|
68
|
+
| `POST /workspaces/remove` | `{ id }` | forget a repo (not the one the server was started in) |
|
|
69
|
+
| `POST /prompt` | `{ chat, text, behavior? }` | send a message; `behavior` is `steer`/`followUp` while running |
|
|
70
|
+
| `POST /abort` | `{ chat }` | stop the active turn |
|
|
71
|
+
| `POST /answer` | `{ chat, question, choice }` | answer a queued question (a "needs you" card) |
|
|
72
|
+
| `POST /ui` | `{ chat, id, response }` | answer an extension dialog: `{ value }`, `{ confirmed }` or `{ cancelled }` |
|
|
73
|
+
| `POST /steer` | `{ chat, run, action, … }` | engine actions, below |
|
|
74
|
+
| `POST /config` | `{ chat, key, value }` | write one setting (the engine's own writer) |
|
|
75
|
+
| `POST /doctor` | `{ chat }` | boot the app, run the check, ask the judge to read an image |
|
|
76
|
+
| `POST /talk` | `{ chat, run, name, text }` | message the attempt that is running |
|
|
77
|
+
| `POST /live` | `{ run }` | run the winner, return `{ url }` (one slot per server) |
|
|
78
|
+
| `POST /live/stop` | `{ run }` | stop it |
|
|
79
|
+
|
|
80
|
+
`POST /steer` — the actions a UI offers on a run:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{ "chat": "chat-…", "run": "loop-…", "action": "take", "attempt": "R1-bold" }
|
|
84
|
+
{ "chat": "chat-…", "run": "loop-…", "action": "apply" }
|
|
85
|
+
{ "chat": "chat-…", "run": "loop-…", "action": "stop" }
|
|
86
|
+
{ "chat": "chat-…", "run": "loop-…", "action": "clean" }
|
|
87
|
+
{ "chat": "chat-…", "run": "loop-…", "action": "rule", "rule": "never touch test.mjs" }
|
|
88
|
+
{ "chat": "chat-…", "run": "loop-…", "action": "talk", "attempt": "R1-safe", "text": "keep going" }
|
|
89
|
+
{ "chat": "chat-…", "run": "loop-…", "action": "more", "text": 2 }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`take` continues from an attempt even if the judge rejected it, `apply` lands the winner in the checkout and commits,
|
|
93
|
+
`clean` frees the attempt worktrees (the winner's stays), `more` asks the agent for another round. Responses say
|
|
94
|
+
`{"ok": true, "sent": "<disposition>"}`; `sent: "handled"` means an extension consumed the prompt and no run started.
|
|
95
|
+
|
|
96
|
+
## The loop lifecycle, for a client
|
|
97
|
+
|
|
98
|
+
1. `prompt` → the agent may call `loop`. In a repo with no config it first raises a **dialog** (`select`: save/change/
|
|
99
|
+
discuss the setup) — answer it with `POST /ui`, or the tool call waits forever.
|
|
100
|
+
2. `loop` returns as soon as the round starts; the round card arrives later as a chat message. Follow both through the
|
|
101
|
+
event stream: `state.live` is the turn, `state.runs[].rounds[].inProgress` is the round, `state.dialogs` are the
|
|
102
|
+
questions that block.
|
|
103
|
+
3. When the round is judged, `runs[].questions` gains a `veto` / `close` / `stall` / `apply` question. Answer it with
|
|
104
|
+
`POST /answer` — the answer becomes a message to the agent, which acts (`take`, `apply`, …).
|
|
105
|
+
4. Scores, per-attempt outcomes and the judge's reasons are in the round entry; screenshots and diffs are the
|
|
106
|
+
`GET /shot|/diff|/sheet` routes above.
|
package/README.md
CHANGED
|
@@ -1,3 +1,222 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @computesdk/loops
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Loops for people who do not live in a terminal: a `loops` CLI and a localhost web UI, both clients of
|
|
4
|
+
[`@computesdk/pi-loops`](../pi-loops). The extension is the foundation — it owns the loops, the records and every
|
|
5
|
+
capability. This package owns none of that; it drives the engine.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
loops status # what ran here, read straight from the records (no pi process, no tokens)
|
|
9
|
+
loops doctor --live # can a round work here? boots serve, runs check, asks the judge to read an image
|
|
10
|
+
loops run "<goal>" # a round — the one verb that costs tokens
|
|
11
|
+
loops clean | apply | take | rule | questions | answer
|
|
12
|
+
loops serve # the browser app for this repo, and the URL to open
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Installed from npm it is one command, and that is the one to hand someone:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npx @computesdk/loops serve
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
It is a **client of a real Pi session**, not a re-implementation of one. There is no database and no poll loop:
|
|
22
|
+
the session is the durable record, and `.pi/loops/` is a rebuildable index plus the worktrees it points at.
|
|
23
|
+
|
|
24
|
+
| What the UI shows | Where it comes from |
|
|
25
|
+
|---|---|
|
|
26
|
+
| the transcript | `get_messages` from the chat's `pi --mode rpc` child |
|
|
27
|
+
| a loop, its rounds, its scores | the `loops-state` snapshot and `loops-round` entries the extension appends to that session |
|
|
28
|
+
| the current winner / applied commit | the newest `loops-state` snapshot (`best`, `winner`, `baseCommit`) |
|
|
29
|
+
| a round in flight | the `loops-progress` entries it appends while it works (throttled; phase changes always) |
|
|
30
|
+
| questions for you | `loops-question` / `loops-answer` entries — append-only, so nothing is rewritten |
|
|
31
|
+
| the brief / approve card | the extension's `ctx.ui.select`, as an `extension_ui_request` |
|
|
32
|
+
| diffs, screenshots, sheets | read-only routes over the run's worktrees and archived shots under `.pi/loops/` |
|
|
33
|
+
|
|
34
|
+
A chat's repo is also recoverable from Pi itself: each session header records its `cwd`, and the session dir names the
|
|
35
|
+
workspace. `workspaces.json` is a cache — if it is lost or stale, the server rebuilds the chat-to-repo binding from the
|
|
36
|
+
sessions on startup, and writes it atomically.
|
|
37
|
+
|
|
38
|
+
## Local only, by design
|
|
39
|
+
|
|
40
|
+
The server binds `127.0.0.1` and refuses any other `--host`. It is not a service: it can prompt an agent that has
|
|
41
|
+
`bash` on this machine, and it serves code that attempts wrote. There is no auth because there is no remote. If you need
|
|
42
|
+
it from another device, tunnel to it (`ssh -L 8001:127.0.0.1:8001 …`) rather than exposing the port.
|
|
43
|
+
|
|
44
|
+
## Run it
|
|
45
|
+
|
|
46
|
+
Start it once for the machine, then point it at repos from the tab bar:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npx @computesdk/loops serve # from npm — the one command, from any repo
|
|
50
|
+
# from a checkout:
|
|
51
|
+
loops serve # or: node server/index.ts --port 8001
|
|
52
|
+
# or: pi-loops-ui --port 8001 --cwd ~/code/some-repo (seeds one tab)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
It ships as one bundled server plus the built SPA, so there is no build step on the other end. The `pi-loops` extension
|
|
56
|
+
is a runtime dependency and is resolved out of `node_modules`; `pi` itself still has to be on the machine for a round
|
|
57
|
+
to run, which the boot says out loud rather than at the first prompt.
|
|
58
|
+
|
|
59
|
+
It prints the address and opens it:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
open http://127.0.0.1:8001
|
|
63
|
+
loopback only
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`--no-open` (or `LOOPS_UI_OPEN=0`) prints the address without launching a browser. When the URL carries a token — off
|
|
67
|
+
loopback — the opened address is the attach form, `/?server=…&token=…`, which the app adopts once and strips from the
|
|
68
|
+
address bar.
|
|
69
|
+
|
|
70
|
+
A loop needs the engine, so the boot says when it is missing rather than leaving you to find out on the first prompt:
|
|
71
|
+
if `pi` is not on PATH, or an `--extensions` path does not exist, it prints what is wrong and the fix under `!! a loop
|
|
72
|
+
cannot run here yet`, and `/api/health` reports `ok: false` with the same list. The app still serves — you can add
|
|
73
|
+
repos and read what already ran.
|
|
74
|
+
|
|
75
|
+
A **tab is a repo** ("workspace"): its chats, its runs, its `.pi/loops.json`. `+` adds a repo — the path is resolved
|
|
76
|
+
server-side and must be a directory — and `×` closes it; loops and chats stay on disk. Workspaces, the tab you had
|
|
77
|
+
open, and chats are remembered in `~/.pi/loops-ui`, so a restart resumes everything. A chat remembers its repo, so
|
|
78
|
+
its `pi --mode rpc` child spawns in that directory with that directory's session dir.
|
|
79
|
+
|
|
80
|
+
Flags (each has a `LOOPS_UI_*` environment variable):
|
|
81
|
+
|
|
82
|
+
| flag | default | meaning |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `--port` | `8001` | where the app listens |
|
|
85
|
+
| `--cwd` | `process.cwd()` | the repo the loops work in |
|
|
86
|
+
| `--pi` | `pi` | the Pi executable each chat spawns |
|
|
87
|
+
| `--extensions` | `../pi-loops/extensions/loop.ts` | comma-separated `-e` paths for the child |
|
|
88
|
+
| `--data` | `~/.pi/loops-ui` | workspaces.json and the chats of every repo |
|
|
89
|
+
| `--host` | `127.0.0.1` | loopback only; anything else is refused (this drives an agent with tools) |
|
|
90
|
+
| `--model` | Pi's default | the model every chat starts on |
|
|
91
|
+
| `--no-open` | off | print the URL without opening a browser (`LOOPS_UI_OPEN=0`) |
|
|
92
|
+
| `--dev` | off | proxy the SPA to Vite on `:5173` (`npm run dev`) |
|
|
93
|
+
|
|
94
|
+
## Attach to a server on another machine
|
|
95
|
+
|
|
96
|
+
The SPA is a client. By default it talks to the process that served it; it can be pointed at another one:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
# on the box that owns the repos — off-loopback listening needs a token
|
|
100
|
+
node server/index.ts --port 3000 --host 0.0.0.0 --token # prints the URL + token once
|
|
101
|
+
|
|
102
|
+
# anywhere else: either open the served UI at box:3000, or attach this one to it
|
|
103
|
+
http://box:3000/?server=http://box:3000&token=<printed>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The `server`/`token` pair is adopted once and stripped from the address bar; after that the sidebar footer names the
|
|
107
|
+
server you are attached to (click it to change). A URL that lands in a log is not a problem because the token lives in
|
|
108
|
+
`localStorage` afterwards, but treat it as the password it is: `~/.pi/loops-ui/token`, mode 0600, `--token` to rotate.
|
|
109
|
+
|
|
110
|
+
**The API is the product.** Anything that wants its own chat UI, its own loop canvas, or its own needs-you inbox should
|
|
111
|
+
use it rather than this SPA: see [`API.md`](./API.md) for every route and the loop lifecycle, and [`client.ts`](./client.ts)
|
|
112
|
+
for a typed client (no dependencies) that can be imported straight into another app.
|
|
113
|
+
|
|
114
|
+
The routes are the contract, not the SPA: `GET /api/workspaces`, `/api/chats?ws=`, `/api/state?chat=`, `/api/config?chat=`,
|
|
115
|
+
`POST /api/chat|prompt|steer|answer|ui|config|live`, and **one event stream per chat**:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
GET /api/chats/:id/events Accept: text/event-stream
|
|
119
|
+
id: <session entry id> ← the cursor; send it back as Last-Event-ID after a drop
|
|
120
|
+
event: state ← the same snapshot /api/state returns
|
|
121
|
+
event: ping ← keep-alive every 20s
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
SSE over `fetch` (not `EventSource`) so the token can travel in a header, and so a reconnect resumes from the cursor
|
|
125
|
+
instead of starting blind. Any origin may attach (CORS is answered); the token is the authorization.
|
|
126
|
+
|
|
127
|
+
## What it is made of
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
server/
|
|
131
|
+
index.ts Hono: /api/state, /api/prompt, /api/ui, /api/steer, /api/shot, /api/diff, /ws, static
|
|
132
|
+
chats.ts one Chat per session: spawns `pi --mode rpc`, folds its events into one snapshot
|
|
133
|
+
pi-rpc.ts the RPC client: JSONL framing, id correlation, extension dialogs
|
|
134
|
+
runs.ts the durable session entries first, the `.pi/loops` index as fallback -> the shapes the canvas draws
|
|
135
|
+
workspaces.ts the repos this process may touch; chats are bound to one
|
|
136
|
+
config.ts settings with provenance, agent-write proposals, and the doctor checks
|
|
137
|
+
live.ts one live server for the winner, on demand
|
|
138
|
+
src/
|
|
139
|
+
shared/ the wire types both halves use
|
|
140
|
+
client/ the SPA (React 19 + Tailwind v4), ported from the POC at ~/factory/mock
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Why an event stream and not a poll
|
|
144
|
+
|
|
145
|
+
A loop runs **in the background** in Pi: the `loop` tool returns as soon as the round starts, and the round card arrives
|
|
146
|
+
later as a message. In a terminal that is fine — the status line and the card are enough. A canvas wants more, so the
|
|
147
|
+
extension appends a `loops-progress` entry (at most one per 1.5s, always on a phase change) and calls `setStatus`, which
|
|
148
|
+
reaches us as an `extension_ui_request`. Either one wakes the server, which re-reads the session with a cursor
|
|
149
|
+
(`get_entries?since=`). One event in, at most one cheap read out — and a browser that reloads mid-round rebuilds the same
|
|
150
|
+
picture from the entries alone.
|
|
151
|
+
|
|
152
|
+
### Proof that the judge saw the picture
|
|
153
|
+
|
|
154
|
+
A judge that never received an image used to score a visual round anyway — the visible symptom was
|
|
155
|
+
`reasons: "Image unavailable"` next to a 5–5 "tie". Two mechanisms make that impossible to miss:
|
|
156
|
+
|
|
157
|
+
- **Declared capability is checked first.** A judge (or panel member) whose model does not claim image input is
|
|
158
|
+
refused before an attempt runs, with the setting to change. No model, no round.
|
|
159
|
+
- **Every judged screenshot carries a code in its top-right corner.** The judge must read it back
|
|
160
|
+
(`"tokens": {"<label>": "<code>"}`); one edit apart counts as a misread, anything else does not count at all. A
|
|
161
|
+
candidate whose code was not read back is `unseen`: it gets **no score**, the round is marked **inconclusive**
|
|
162
|
+
(never "even"), and a question is raised — retry, switch the judge, or keep the current best.
|
|
163
|
+
|
|
164
|
+
The code lives in the pixels only, so it cannot be echoed from the prompt. `lib/shot.ts` does the stamping
|
|
165
|
+
(`stamp`, `canaryImage`), `lib/judge.ts` the comparison (`sameCode`), and `probeVision()` can settle a model's
|
|
166
|
+
capability offline — not inside a tool call, where creating a session waits on the runtime the turn holds.
|
|
167
|
+
|
|
168
|
+
### Live: one server, on demand
|
|
169
|
+
|
|
170
|
+
A screenshot is a still of an app that already runs — `lib/shot.ts` starts `serve` inside an attempt's worktree to take
|
|
171
|
+
it. `Run it` keeps that same server alive for the winner instead: one slot, started when you ask, killed when you stop
|
|
172
|
+
it, after ten idle minutes, or when this process exits. It is offered only when the repo has `serve` in
|
|
173
|
+
`.pi/loops.json`. The iframe gets an opaque origin (`sandbox`, no `allow-same-origin`), because the code inside it was
|
|
174
|
+
written by an attempt.
|
|
175
|
+
|
|
176
|
+
### Judging happens in a child process
|
|
177
|
+
|
|
178
|
+
An in-session `prompt(text, { images })` does not deliver the picture on this Pi — the message carries the block, the
|
|
179
|
+
provider request does not — so a judge that has to look at screenshots is asked through `pi --print` (the same binary,
|
|
180
|
+
model and auth, with `@file` arguments, which do carry images). Two plumbing rules matter there and both cost a bug
|
|
181
|
+
each: close stdin (`ignore`) or `--print` waits for EOF forever, and parse **stdout only** (pi warns on stderr).
|
|
182
|
+
|
|
183
|
+
### The checkout is not the attempts' to touch
|
|
184
|
+
|
|
185
|
+
An attempt has bash and inherits the repo's absolute path, so it can edit or commit into your checkout instead of its
|
|
186
|
+
worktree. Every round therefore checks, after the attempts: did HEAD move, did the working tree gain changes, while
|
|
187
|
+
every attempt's own worktree stayed at the base commit? If so the round is inconclusive (nothing of theirs exists to
|
|
188
|
+
compare), your tree is put back, and the card says what happened — the loop's promise is that your checkout is
|
|
189
|
+
untouched until `loop_apply`.
|
|
190
|
+
|
|
191
|
+
### Cleanup
|
|
192
|
+
|
|
193
|
+
Worktrees are scratch copies: the commit lives in the repo and on the branch `loops/<run>/rN-<style>`, so removing a
|
|
194
|
+
worktree loses only what the attempt left uncommitted. `clean up` (or `/loop clean [run|all]`) removes the attempts'
|
|
195
|
+
worktrees, drops the losing branches and runs `git worktree prune`, keeping the winner's worktree, the durable session
|
|
196
|
+
snapshot, the JSON index, the sessions and the archived screenshots. `loop_apply` does it for the losers as soon as a
|
|
197
|
+
winner lands.
|
|
198
|
+
|
|
199
|
+
### Settings that change under you
|
|
200
|
+
|
|
201
|
+
`.pi/loops.json` decides which models judge, what the checks are and how much runs. The server watches it and the app
|
|
202
|
+
shows what changed (`models judge exe/… → particle/…`) while it is open — an agent editing config is a real event, not
|
|
203
|
+
a detail.
|
|
204
|
+
|
|
205
|
+
### Actions
|
|
206
|
+
|
|
207
|
+
The buttons talk to the engine, not to a model: `/loop take`, `/loop apply`, `/loop stop`, `/loop rule`, `/loop talk`,
|
|
208
|
+
`/loop answer`. Anything that needs judgement ("two more rounds", "steer it this way") is sent to the agent as a message,
|
|
209
|
+
because that is a decision the agent owns.
|
|
210
|
+
|
|
211
|
+
## Parity
|
|
212
|
+
|
|
213
|
+
`npm run shots` writes `./shots/` at 1440, 1024 and 430 wide. The pass condition for this port was the screens the POC at
|
|
214
|
+
`~/factory/mock` shows, at the same density; see [`PARITY.md`](./PARITY.md) for the state-by-state comparison.
|
|
215
|
+
|
|
216
|
+
## Development
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
npm run dev # Hono on :8001 proxying the SPA to Vite on :5173
|
|
220
|
+
npm run check # typecheck the server and the client separately
|
|
221
|
+
npm run shots # headless-chrome screenshots of every state
|
|
222
|
+
```
|
package/bin/loops.mjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `loops` — the foundation without a TUI.
|
|
3
|
+
//
|
|
4
|
+
// Two kinds of verb, and the difference matters:
|
|
5
|
+
// • bookkeeping (status) reads the records the engine writes; no pi process, no tokens, scriptable
|
|
6
|
+
// • everything else asks the engine to do it, by running the same `/loop …` command a person would type.
|
|
7
|
+
// The CLI owns no loop logic. If it ever computed a run itself there would be two answers to "what is a run",
|
|
8
|
+
// and the one an agent used would disagree with the one you printed.
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
14
|
+
|
|
15
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const PKG = path.resolve(HERE, "..");
|
|
17
|
+
const argv = process.argv.slice(2);
|
|
18
|
+
const flag = (name) => argv.includes(`--${name}`);
|
|
19
|
+
const opt = (name, fallback = undefined) => {
|
|
20
|
+
const i = argv.indexOf(`--${name}`);
|
|
21
|
+
return i >= 0 ? argv[i + 1] : fallback;
|
|
22
|
+
};
|
|
23
|
+
/** Arguments that are not flags or flag values. */
|
|
24
|
+
function positional() {
|
|
25
|
+
const out = [];
|
|
26
|
+
const takesValue = new Set(["cwd", "port", "extension", "pi", "run", "attempt", "option", "goal"]);
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
if (argv[i].startsWith("--")) { if (takesValue.has(argv[i].slice(2))) i++; continue; }
|
|
29
|
+
out.push(argv[i]);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const CWD = path.resolve(opt("cwd", process.cwd()));
|
|
35
|
+
const STATE_DIR = path.join(CWD, ".pi", "loops");
|
|
36
|
+
const PI = opt("pi", process.env.LOOPS_PI ?? "pi");
|
|
37
|
+
|
|
38
|
+
/** The extension this CLI drives: the installed package if we can find it, else the sibling checkout. */
|
|
39
|
+
function extensionPath() {
|
|
40
|
+
const explicit = opt("extension", process.env.LOOPS_EXTENSION);
|
|
41
|
+
if (explicit) return path.resolve(explicit);
|
|
42
|
+
const candidates = [
|
|
43
|
+
path.resolve(PKG, "../pi-loops/extensions/loop.ts"), // monorepo checkout
|
|
44
|
+
path.resolve(PKG, "node_modules/@computesdk/pi-loops/extensions/loop.ts"), // installed next to us
|
|
45
|
+
path.resolve(HERE, "../../../pi-loops/extensions/loop.ts"),
|
|
46
|
+
];
|
|
47
|
+
for (const c of candidates) if (fs.existsSync(c)) return c;
|
|
48
|
+
// npm keeps the dependency flat: ask Node where that package actually is
|
|
49
|
+
try {
|
|
50
|
+
const req = spawnSync(process.execPath, ["-e", "console.log(require.resolve('@computesdk/pi-loops/package.json'))"], { encoding: "utf8", cwd: PKG });
|
|
51
|
+
const found = req.stdout?.trim();
|
|
52
|
+
if (found) return path.join(path.dirname(found), "extensions", "loop.ts");
|
|
53
|
+
} catch {}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const usage = `loops — hill-climbing loops for a repo, without the TUI
|
|
58
|
+
|
|
59
|
+
loops status [--json] [--run <id>] what ran here: goal, rounds, best, whether it is still working
|
|
60
|
+
loops doctor [--live] [--json] can a round work here? (--live boots serve, runs check, tests the judge)
|
|
61
|
+
loops config every setting in force, and which file it came from
|
|
62
|
+
loops run "<goal>" [loop options] start a round (costs tokens; this one needs a model)
|
|
63
|
+
loops questions decisions waiting for you
|
|
64
|
+
loops answer <id> <option> answer one
|
|
65
|
+
loops take <R1-bold> [run] keep that attempt and climb from it
|
|
66
|
+
loops rule <run|last> "<rule>" add a judging rule
|
|
67
|
+
loops apply [run] land the winner in your checkout
|
|
68
|
+
loops clean [run|all] remove attempt worktrees (commits and the winner stay)
|
|
69
|
+
loops serve [--port 8001] [--no-open]
|
|
70
|
+
the browser app for this repo (localhost only), prints the URL and opens it
|
|
71
|
+
loops ui same thing, by the older name
|
|
72
|
+
|
|
73
|
+
--cwd <repo> the checkout to work on (default: this directory)
|
|
74
|
+
--extension <p> the pi-loops extension to load
|
|
75
|
+
--pi <path> the pi executable
|
|
76
|
+
--json machine-readable output where the verb supports it
|
|
77
|
+
|
|
78
|
+
Anything that needs a model runs the same /loop command you would type in pi.`;
|
|
79
|
+
|
|
80
|
+
const say = (line = "") => process.stderr.write(`${line}\n`);
|
|
81
|
+
|
|
82
|
+
/** Runs are read straight off the records the engine writes: no process, no tokens. */
|
|
83
|
+
function runs({ run } = {}) {
|
|
84
|
+
if (!fs.existsSync(STATE_DIR)) return [];
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const f of fs.readdirSync(STATE_DIR).filter((f) => f.endsWith(".json"))) {
|
|
87
|
+
try {
|
|
88
|
+
const s = JSON.parse(fs.readFileSync(path.join(STATE_DIR, f), "utf8"));
|
|
89
|
+
if (!s?.id || (run && s.id !== run)) continue;
|
|
90
|
+
const judged = (s.rounds ?? []).filter((r) => r.judge);
|
|
91
|
+
out.push({
|
|
92
|
+
id: s.id, goal: s.goal, kind: s.kind, status: s.status,
|
|
93
|
+
rounds: (s.rounds ?? []).length, judged: judged.length,
|
|
94
|
+
best: s.best?.label ?? null, bestRound: s.best?.round ?? null, score: s.best?.score ?? null,
|
|
95
|
+
unit: s.metric?.unit ?? "", escaped: (s.rounds ?? []).some((r) => r.escaped),
|
|
96
|
+
inconclusive: (s.rounds ?? []).some((r) => r.inconclusive),
|
|
97
|
+
startedAt: s.rounds?.[0]?.startedAt ?? null, endedAt: s.rounds?.at(-1)?.endedAt ?? null,
|
|
98
|
+
file: path.relative(CWD, path.join(STATE_DIR, f)),
|
|
99
|
+
});
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
return out.sort((a, b) => String(b.startedAt ?? "").localeCompare(String(a.startedAt ?? "")));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fmt = (r) => {
|
|
106
|
+
const score = r.score == null ? "–" : `${r.score}${r.unit ? ` ${r.unit}` : ""}`;
|
|
107
|
+
const state = r.status === "running" ? "running" : r.escaped ? "ran in your checkout" : r.inconclusive ? "inconclusive" : r.status;
|
|
108
|
+
return `${r.id} ${r.kind.padEnd(6)} ${state.padEnd(20)} r${r.rounds}/${r.judged} ${r.best ?? "–"} ${score}`.trimEnd() + `\n ${r.goal ?? ""}`;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function status() {
|
|
112
|
+
const list = runs({ run: opt("run") });
|
|
113
|
+
if (flag("json")) return process.stdout.write(JSON.stringify(list, null, 2) + "\n");
|
|
114
|
+
if (!list.length) return say(`no runs in ${path.relative(process.cwd(), STATE_DIR) || STATE_DIR} — start one with: loops run "<goal>"`);
|
|
115
|
+
for (const r of list) say(fmt(r) + "\n");
|
|
116
|
+
const live = list.filter((r) => r.status === "running").length;
|
|
117
|
+
say(`${list.length} run${list.length === 1 ? "" : "s"}${live ? `, ${live} running` : ""}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ---------- the verbs that ask pi ---------- */
|
|
121
|
+
|
|
122
|
+
function commandFor(verb) {
|
|
123
|
+
const p = positional().slice(1); // drop the verb itself
|
|
124
|
+
switch (verb) {
|
|
125
|
+
case "questions": return "/loop questions";
|
|
126
|
+
case "answer": return `/loop answer ${p[0] ?? ""} ${p.slice(1).join(" ")}`.trim();
|
|
127
|
+
case "take": return `/loop take ${p[0] ?? ""} ${p[1] ?? ""}`.trim();
|
|
128
|
+
case "rule": return `/loop rule ${p[0] ?? "last"} ${p.slice(1).join(" ")}`;
|
|
129
|
+
case "apply": return `/loop apply ${p[0] ?? ""}`.trim();
|
|
130
|
+
case "clean": return `/loop clean ${p[0] ?? ""}`.trim();
|
|
131
|
+
case "stop": return "/loop stop";
|
|
132
|
+
case "config": return "/loop config";
|
|
133
|
+
case "doctor": return flag("live") ? "/loop doctor live" : "/loop doctor";
|
|
134
|
+
default: return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Run one /loop command in a pi child and pass its output straight through. */
|
|
139
|
+
function askEngine(command) {
|
|
140
|
+
const ext = extensionPath();
|
|
141
|
+
if (!ext) {
|
|
142
|
+
say(`cannot find the pi-loops extension — pass --extension <path to extensions/loop.ts>`);
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
const args = ["-p", "-e", ext, command];
|
|
146
|
+
const child = spawn(PI, args, { cwd: CWD, stdio: ["ignore", "inherit", "inherit"], env: { ...process.env, PI_OFFLINE: process.env.PI_OFFLINE ?? "1" } });
|
|
147
|
+
return new Promise((resolve) => child.on("close", (code) => resolve(code ?? 1)));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A round needs a model, so this is the one verb that spends tokens. */
|
|
151
|
+
function runRound() {
|
|
152
|
+
const goal = positional()[1] ?? opt("goal");
|
|
153
|
+
if (!goal) { say(`usage: loops run "<goal>" [--bench "<cmd>"] [--check "<cmd>"] [--rounds n]`); return 2; }
|
|
154
|
+
const parts = [goal];
|
|
155
|
+
if (opt("check")) parts.push(`check is ${opt("check")}`);
|
|
156
|
+
if (opt("bench")) parts.push(`bench is ${opt("bench")} and prints ${opt("unit", "ms")}`);
|
|
157
|
+
say(`starting a round — this one costs tokens\n`);
|
|
158
|
+
return askEngine(parts.join("; "));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function main() {
|
|
162
|
+
const verb = positional()[0] ?? "";
|
|
163
|
+
if (!verb || verb === "help" || flag("help")) { say(usage); return 0; }
|
|
164
|
+
if (verb === "status") { status(); return 0; }
|
|
165
|
+
if (verb === "ui" || verb === "serve") {
|
|
166
|
+
// Published, the server is one bundled file — Node refuses to strip types inside node_modules, so the .ts is only
|
|
167
|
+
// the entry in a checkout, where node's stripper runs it directly. Same flags either way.
|
|
168
|
+
const bundle = path.join(PKG, "dist", "server.mjs");
|
|
169
|
+
const entry = fs.existsSync(bundle)
|
|
170
|
+
? [bundle]
|
|
171
|
+
: ["--experimental-strip-types", path.join(PKG, "server", "index.ts")];
|
|
172
|
+
const args = [...entry, "--cwd", CWD, "--port", String(opt("port", "8001"))];
|
|
173
|
+
if (flag("no-open")) args.push("--no-open");
|
|
174
|
+
// The extension is a runtime dependency, so it resolves on an install as well as in this workspace; without it the
|
|
175
|
+
// server's own boot check says so instead of the first prompt failing.
|
|
176
|
+
const ext = extensionPath();
|
|
177
|
+
if (ext) args.push("--extensions", ext);
|
|
178
|
+
const child = spawn(process.execPath, args, { stdio: "inherit", env: process.env });
|
|
179
|
+
return new Promise((resolve) => child.on("close", (code) => resolve(code ?? 1)));
|
|
180
|
+
}
|
|
181
|
+
if (verb === "run") return runRound();
|
|
182
|
+
const command = commandFor(verb);
|
|
183
|
+
if (command) return askEngine(command);
|
|
184
|
+
say(`unknown command "${verb}"\n\n${usage}`);
|
|
185
|
+
return 2;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
process.exitCode = await main();
|