diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index e0203a89d..a2a673ab5 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -1,12 +1,12 @@ { "schema": 5, - "scope": "complete fork-vs-base delta of packages/{app,ui,session-ui,schema,core,sdk}", + "scope": "complete fork-vs-base delta of packages/{app,ui,session-ui,schema,core,sdk,opencode}", "fork_ref": "local/amicode", "fork_tag": "v1.18.29-amicode.30-11-g0667bccd6d", "fork_sha": "517921eecaa56eb9861a7b96a5bd2ee36ee42e86", "upstream_base": "7fe993879f98aa17cecc70f70d3f40d6f0f11689", "upstream_base_sha": "7fe993879f98aa17cecc70f70d3f40d6f0f11689", - "promoted_at": "2026-09-13T20:54:42.012Z", + "promoted_at": "2026-09-13T22:15:25.839Z", "files": { "packages/app/CONTEXT.md": "7dc4bf176a32ee578a54593a58a6b97907ffc5a9baf7ae7c16a8fa920d8e6e86", "packages/app/docs/adr/0001-chat-first-landing.md": "d19c42a8aa56f585f92a3e0ffd072c55804ca029c6d19fb7d772c69d7e5de3be", @@ -362,6 +362,131 @@ "packages/core/test/location-mutation.test.ts": "2c4c8faa220cc8fabfe12d57bd74ec8dd8aac8485d0ecafd2761cdee1806d6b0", "packages/core/test/system-context/builtins.test.ts": "2331a35ab10b3e69f5f5d2dd462ec56000246a747bccec77db783144a91ebbea", "packages/core/test/tool-question.test.ts": "4df41393f15104a1d3a9c3d06e30cef515108490b41fcf4d9a7d9399fd4ed945", + "packages/opencode/package.json": "01d9338ee64108d0bd6af80b971fa2da4c7bf63e7efae59cc97cd58a9cfce55c", + "packages/opencode/pasqal-live-test.mjs": "2ff0edf222ce29660ef8d901c0b6f10951943ef01824d2d264339f8d720206f1", + "packages/opencode/pasqal-live-test.sh": "0bb0089eb413956f0252939f933a139c2f11520466887ad91fcae362f1698e41", + "packages/opencode/script/build.ts": "876e7d569aa71ce060c1d4b24c96e9bf13245cbf4ca743828d645560a95aa641", + "packages/opencode/script/httpapi-exercise.ts": "772bd2cf5748ca90c5d5790c2d044237518cc8ede0411fc4ba28b9d7440ba28c", + "packages/opencode/src/agent/agent.ts": "512372a4c62997e9565266638ada19b9dc0a3685b0c0483fd28fede2abdb5c9b", + "packages/opencode/src/cli/cmd/run/footer.question.tsx": "c9a1b335c9c7d8446d15c58c8808ea857f8f238c8a4047121f49599296922f39", + "packages/opencode/src/cli/cmd/run/question.shared.ts": "f4b8ab69c18e66d3efea505f3d2c8c5476bba78d46b0f30bc52da4386b19e478", + "packages/opencode/src/mcp/browser.ts": "ecdda42cbd55a97b8d14050aa15c6e6450a1d63b0a641c8c8e5b81e50f957c87", + "packages/opencode/src/plugin/openai/codex.ts": "7f4342ee6a25eca6b66a47732a154449fbf318f6ce70ed446cf3e6788833954b", + "packages/opencode/src/project/instance-context.ts": "65a35792e50e96a50ea14cd9f928045c0e2fc15eb49506f5bbceee38c4af1457", + "packages/opencode/src/project/instance-store.ts": "c22d3e2499e63dd8d9395d58189df75444d306774df833e746f22e327edcf2ab", + "packages/opencode/src/provider/auth.ts": "b0ab1322c1039e27176bf9e3500603eae27506065ac141a69710220f3361f6b8", + "packages/opencode/src/provider/provider.ts": "671f6d8de83f919ddaa69b8240cacd82e2b8202ab505d4334175f2858bbd8c32", + "packages/opencode/src/server/amicode/connections.ts": "4498adc8a1ba0d81f92a2a23cc36641597b738bb0ac81d658bd21e10c35e39fc", + "packages/opencode/src/server/amicode/credentials.ts": "f3a15c82496f9a3b6e8c1006a6a4ab419a02156e9a93a43c6c2319632c30063f", + "packages/opencode/src/server/amicode/dashboard.ts": "031a4f40bfdc847984a1deecfefbe0fd27f42cbd1caf8823dc43c7eb5bcb9af0", + "packages/opencode/src/server/amicode/file-resolve.ts": "3f0548fc97aad431d3f7707fd0b911e66fb4fd68ac4f643da71a2f00a73beffe", + "packages/opencode/src/server/amicode/library.ts": "36013b19f7c57afaa1bab63e5ff34d79f09ec42640e2a8c4a40cb14c97c4b859", + "packages/opencode/src/server/amicode/pasqal-secret.ts": "a0b53498bb7415c96e14b435367ffa93bd3d298fe12f7584cd401e649312fd69", + "packages/opencode/src/server/amicode/problems.ts": "d56513cff90193ab436dcca379f065a4ce963eeb45c22178cf9a0151b49c9487", + "packages/opencode/src/server/amicode/profile.ts": "521338a7990deb8d0f352ed96d0f0a96fd56daeeec407c3ae133b02dfb2ed5c1", + "packages/opencode/src/server/amicode/project.ts": "7f0a8a15aea0ef367a00ab31ba4329ecf2344282965afce553005b5355be63c3", + "packages/opencode/src/server/amicode/run-terminal.ts": "c6857036c1dca90e8b462e916f49df707757f227d0ed74f072b8e94d08d74cd0", + "packages/opencode/src/server/amicode/toml-lite.ts": "0001fb071c53c1a3ab330e40550ccfba497b78e2a0cc45084e1c016d8f215831", + "packages/opencode/src/server/amicode/vault-browser.ts": "8a8252fb0b46583c75e9475d7c89e68c092f5255b21476f3c3d0b9d379ea2d48", + "packages/opencode/src/server/amicode/vaults.ts": "e48c90c64de27abe24a510f188def6b56297d9bb1fa1ae08cbcc94888d42044a", + "packages/opencode/src/server/amicode/warrants.ts": "13ef47a676245d577617f6e5af2c8a2beb3102346d2dbb43076159f3f43c01a7", + "packages/opencode/src/server/amicode/widget-frame-html.ts": "3df10eeeccbb81d2b163195ceaa1d5abc24e90506471ff6d6b2a5fc40e52db52", + "packages/opencode/src/server/amicode/widget-manifest.ts": "5753daf16ef4c1e74c97d4a515b30d123f7afbfee29bb07381c6f10ecc14cebf", + "packages/opencode/src/server/amicode/widget-runtime.ts": "a5fe95eba46df87c4b4c54bc07bb91587b73e88eb90db889bc87358ece4b3f27", + "packages/opencode/src/server/amicode/widgets-src/jump-back-in.ts": "a82bf11004b71c7ac6e0357c45a0d7532aad4ee6af16e048ae86ef9bf3799fef", + "packages/opencode/src/server/amicode/widgets.ts": "d15481086c41b365c28937f002bfcdeb8c2a9f68066413da695fbac8b7acdb61", + "packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts": "944a0aee215a917e867fd9482f3c2bd40f49082bd3174c8a7d967ca40e34ec15", + "packages/opencode/src/server/routes/instance/httpapi/groups/file.ts": "ed165131c62eeb29375db39edf89db8f87488e24ef99fc6f2cf19705ba35751a", + "packages/opencode/src/server/routes/instance/httpapi/groups/provider.ts": "34c6f1ab1a085d1b047c220bbff32a36032dc5e924784e9272cda166cf0ec84b", + "packages/opencode/src/server/routes/instance/httpapi/groups/session.ts": "1421f8c07948b121c7e2005bd9b0fcd6b2c456565ddd1da32d8e6c35d03fac9c", + "packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts": "69ca3dd48d95157fc822e1f76eb42ac873df50f22616b85194619ca658613867", + "packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts": "10bcb79b117beebee04184fb48c1ad789e782e2f7ae4b0c68603edc00f03aee9", + "packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts": "8baf16cad0992c5351b61b0c0fb27903bb25b17e1b25fc7c5d77f807c24413a6", + "packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts": "b37ff4136c4aea85b23083bf441f03e09820e3e4cdd95e4118ca570d8435b94f", + "packages/opencode/src/server/routes/instance/httpapi/server.ts": "85c200797fed41c0cd08e7aa699ce13aeb8a3d23aa2b9eddc4d72e72b8b5082c", + "packages/opencode/src/server/server.ts": "56b3c2047917aa80d1dfb29bfa9839bcc2ad7f5a6f4db12103cd962a98fba866", + "packages/opencode/src/server/shared/public-ui.ts": "a843b4e84673d9a5e34f36eb7cffbb499850f7588154c23061fc8a81cd1369ef", + "packages/opencode/src/server/shared/ui.ts": "eba93fdd47c786d723d2593e788f6b7278b072548acb916d19eeb2e9616a4197", + "packages/opencode/src/server/shared/workspace-routing.ts": "3704b6cf218fc165ce9c1cdad0ee42fc9cf604976e4a552478200f85db80876a", + "packages/opencode/src/session/compaction.ts": "fd6159a8b04050706b6968c7a1b091f79d06e6e4aa988a12629a7616b8128e94", + "packages/opencode/src/session/external-diff.ts": "8f40051b3791551d3811045e4060884266687ec2208465ea2cc116aad939f819", + "packages/opencode/src/session/instruction.ts": "19640d02c96b8ec764edd49b0ce74879432fd2d2cbb4d2ad0076caa200dd4797", + "packages/opencode/src/session/llm/request.ts": "248db5d00c4513fa7101b4631950ed39fe8fe6de8228c2f527c57ca72e0b55f0", + "packages/opencode/src/session/message-v2.ts": "7c5bcf4f68ffd8b5e7f637b7a7550dd154d79e69c8939e4a172446ea2cce2240", + "packages/opencode/src/session/prompt.ts": "65f53031ee84f8e16e6295b5c8aaf9f63f9411f30799a4323f53e2c19eebe05a", + "packages/opencode/src/session/prompt/anthropic.txt": "e3e1a55159f0104ceec69098620eb281cd8927cfa808ab5feaa753719af27f05", + "packages/opencode/src/session/prompt/communicating.txt": "e14e1daf86eb4fa2fc1222cc941145913d540827bde3d27ebfe0fe30181f3e97", + "packages/opencode/src/session/prompt/default.txt": "bfb0d6ab93fb82a8edad591101bd84839565a0fb401770ceb0045b1fd3b081bc", + "packages/opencode/src/session/prompt/gemini.txt": "d24815ea833b058b304d95d3212ad2e091bc07064a22a6af9401872cdc3871a9", + "packages/opencode/src/session/prose-guard.ts": "dd6f8dfdc41b9cad3406ac586db9a60f7ee0a040b41550d6c55da2d040dc7c13", + "packages/opencode/src/session/session.ts": "260a27d72fb53b11c272987a921d3c2099b1e009afb943106336d7f190752c62", + "packages/opencode/src/session/summary.ts": "09f546db362a992156bdb4901cce58e495bd38bab6ba41b4386cca77bebab7f3", + "packages/opencode/src/session/system.ts": "c91d00de07d6d4e3910561d59a598579eeafb725583a9d6475a158abdd4a17c4", + "packages/opencode/src/session/turn-output.ts": "889624ca54a8149d35f200c7efa732771dfca36984e8a322a9781b1a25434762", + "packages/opencode/src/snapshot/index.ts": "4d76a78c48597bda2379e2c3396f4a53828b21c6ce03e1975d2911150822c6de", + "packages/opencode/src/tool/apply_patch.ts": "37535a783d115ac2032def7a28cce56e304f4e7c75d3ecac05b9a37054ae5b4f", + "packages/opencode/src/tool/edit.ts": "38aa4ce0751ff99bbb0807bcda52f8b9a1d32b6e342df4cc2aedab1f03d342b1", + "packages/opencode/src/tool/question.txt": "49bbecb33a825620c2ed14ec9b61faa294359c3673c33277e32f57c1070d2806", + "packages/opencode/src/tool/registry.ts": "2b0e8546f01643e78b9dee504357bfc33f2d7075ec45f8a1462d8c3ee8e7aff5", + "packages/opencode/src/tool/truncate.ts": "92c59d83c4a660116d6ecc48e900aeb16bde93962bfae9f520193fda18273379", + "packages/opencode/src/tool/widget-author.ts": "912be8636b162adca8796a501e927019c2449393c14bb5f3ff478b02083b73a9", + "packages/opencode/src/tool/widget-author.txt": "a2572913debbbbd2d4a644dcdf4576accbc7efdcc8217c0fc9f5a011e7a15ec4", + "packages/opencode/src/tool/write.ts": "094661a3bdd3fab2cd439c0b6cb942b6a2653d3e9451420523b33acbc762d1a7", + "packages/opencode/src/worktree/index.ts": "a86f542b579ef40f2940d36e2c8011ce5592d4811cd5ed437bafe614789fb87e", + "packages/opencode/test/amicode-release-workflow.test.ts": "f6dfcb3ed9c572ca8e8b21ff82e038872b635b25c68f9580f6d6f51ee2813189", + "packages/opencode/test/cli/run/question.shared.test.ts": "fbb3bdb20fae7d77f0e54444847c658c721b132feca9ad939a963721d2ddd772", + "packages/opencode/test/cli/run/run-process.test.ts": "13e6386bf1919688cff2f6f9f68bcddcfbbec822ef8fc271ee68347a542827b8", + "packages/opencode/test/event-manifest.test.ts": "e2f1542c3ee8e981934044bb14cb87a58a676f3b6eb5d160f6e854f136ba445c", + "packages/opencode/test/fixture/fixture.ts": "357bdaa06c6233a91069db20cc5f1cbb2d6eafa3bc7ba3e89b019859d195d6ef", + "packages/opencode/test/lib/cli-process.ts": "110c43d247930e834ffffb9068682c5ab2e1b95460ec1ac3d0a75cafbdee60ff", + "packages/opencode/test/preload.ts": "5a3c7e7a8341dc735513b0cbeac6029ee406ad64767a1b883bdcb3e99269dd7e", + "packages/opencode/test/project/worktree-rename.test.ts": "3ec168c9f8595249b75832895a251483ec1f3641ce44ad0bd5a664b5bbd6dad1", + "packages/opencode/test/provider/harmoniqs-no-tools.test.ts": "ac455d754c1a61791fa45b18db5322ec2d472ff71778fbed1d412ec749b4b7b9", + "packages/opencode/test/provider/provider.test.ts": "5b2cbed8994d3bbc968a72d01459460c1de72b6117a0795c2bc6a10abbf02926", + "packages/opencode/test/server/amicode-connections-routes.test.ts": "2852289427fadaf6133b556af02f7e7b369b2a4df53ababc39531fe85bcc9d17", + "packages/opencode/test/server/amicode-connections.test.ts": "0d916321a42e321d44e0db13a6966985deaf77cea48470d11eb8b434a464b7cd", + "packages/opencode/test/server/amicode-credentials.test.ts": "ba77d064fd2be799930524510ce6e32c6f6a0d144d3cdf41673a8283dbe95bf6", + "packages/opencode/test/server/amicode-dashboard.test.ts": "e25b284b9eb434da7f07dfa8b72b7919a0cd1ffaed275a6725d9cd44cd1298c0", + "packages/opencode/test/server/amicode-file-resolve.test.ts": "39db84780125c8fd4551137e4f6a0ea6b0a9dce45b04e26ed15f649efe251311", + "packages/opencode/test/server/amicode-library.test.ts": "87b986c43390e1f94d6bcded0bb5d22be24beaab2c336773bb438ed1fc6a2d95", + "packages/opencode/test/server/amicode-problems.test.ts": "4a8a9011f3796df4d6cde9999b86b507a8126fdc9138cc3ba305f930968c3392", + "packages/opencode/test/server/amicode-profile.test.ts": "5e5dcfd2b3db5cb8d7aba2cf1f74ed2d76716af6e4f761f54b94e77174b314de", + "packages/opencode/test/server/amicode-project.test.ts": "18991a015b5b8fd5c25b7f9d875d7ff8fbb28fe8b8df02bde390420be9189841", + "packages/opencode/test/server/amicode-toml-lite.test.ts": "3c10b68cde693e27cbb6520f7d40c283d78c5ce30ca6e47871072b6db59600fa", + "packages/opencode/test/server/amicode-vault-browser.test.ts": "03347a6a9b6ba250b7a1150449cbe44b8817dbc0ebc433c89a703f4818492a5a", + "packages/opencode/test/server/amicode-vaults.test.ts": "3f7c8ecfdc250ff360c0ed66d9cc6354d9069e66001281b3f6e30981b9f71c35", + "packages/opencode/test/server/amicode-warrants.test.ts": "a7ad74da87f77f4488f334debf8c126671d563839891eb3a19b3d1a134dbc504", + "packages/opencode/test/server/amicode-widget-frame.test.ts": "c96818aadd94711a2f435d98fe9fcabd95bf4202d695899956c6e74d90c6c531", + "packages/opencode/test/server/amicode-widget-manifest.test.ts": "ac404d1737bdde8ea3464cf0b5fa12f0a6f9e630a0698ca5fdec38449cb66d0f", + "packages/opencode/test/server/amicode-widgets-src.test.ts": "fa95521084f95e0c421e7f7f13bf61de7e6b5cc9f42aa9fade486f5c1b53a65d", + "packages/opencode/test/server/amicode-widgets.test.ts": "14fc6ccff4e83a5b005a724936e9f0d7ce411fab74109f319e8bc748665f7ab8", + "packages/opencode/test/server/fixtures/credentials/cloud.json": "431bdf114031da38975538384e21ab0ec7206a0859859f0bf87345b8c360829e", + "packages/opencode/test/server/fixtures/credentials/pasqal.json": "d3c775cc498fe3029d9ac234d9eaa9eaffa1ad8656e12a5aca1355c733276736", + "packages/opencode/test/server/httpapi-compression.test.ts": "5f7603d7018e0b95f1c2f36c01dd297c01770bfcdd91ce29778e03b7d6143f75", + "packages/opencode/test/server/httpapi-exercise/index.ts": "59048fa30e60348240de62cca862a5c6abbe2e9e0f6340b77b0a9bbd997bec27", + "packages/opencode/test/server/httpapi-file.test.ts": "d3386129bc81467ecbcc57a3861f53767ee56ac41b4ff6ad58d8b168fb3b3373", + "packages/opencode/test/server/httpapi-instance-route-auth.test.ts": "4b73a89d2431c02bc3b665d93f9002914e51eb1add979dc1867efbcbc0c36e25", + "packages/opencode/test/server/httpapi-provider.test.ts": "9e8b3e085b478e3f99b689ffff600e10c0b4b50b3cabdbd749f88dae19bf5904", + "packages/opencode/test/server/httpapi-ui.test.ts": "d029e959cfd9347fff4cf17c5686dd125b0bd3a41afb3408a23b31418f12e3b0", + "packages/opencode/test/server/pasqal-secret.test.ts": "54be8c666160dad22cd37bbf041f5bd40aec120271818f3c56c94107d987b9fb", + "packages/opencode/test/server/session-diff-missing-patch.test.ts": "70526ca7fb86741843fd167f8f2f9c6f6d7e5e415a20a5448d17d24229bdd65b", + "packages/opencode/test/server/session-diff-scoped.test.ts": "4d062ab6881fe6c10f656b363405828b6ebc4c0c66d55f5e694e058f61370e03", + "packages/opencode/test/server/workspace-routing.test.ts": "2574731ac379c540f450edc15f8750ac5360e8c33b4a0dad7c61e8eb686137bb", + "packages/opencode/test/session/instruction.test.ts": "8b4d954eb10afa852a671e6c0a94d19604deacc9a62f9c3651c7456cc32736b7", + "packages/opencode/test/session/llm-native-recorded.test.ts": "7139b58569af0172862af521e8f8d7a78132f40f06dcceb4baf8ca3a2f07d3bd", + "packages/opencode/test/session/message-v2.test.ts": "9edc16ae57b7f2d34aaed7378d82fcf9d07a82075a05730bfb3caae39d70ac7f", + "packages/opencode/test/session/prompt.test.ts": "d9654be772eed1e73191b3a693190674eb087cf7f545385351c0917114bac9cb", + "packages/opencode/test/session/prose-guard.test.ts": "7239a758cbf27df4c3879fa9ee0374964559c1461773c2f5a85196f64632e6c4", + "packages/opencode/test/session/session.test.ts": "e2870fe0a4d759d4c467f45b1903e48ac5ee5aefc6dcd9ae6b6da20714d03646", + "packages/opencode/test/session/turn-output.test.ts": "3e92f545d10054e501ad2c8857b43719f5a8f768065643d343c0c37c2e6fb60c", + "packages/opencode/test/share/share-next.test.ts": "d94076118b558644c06de3ef315041a6fd8895098377da843e98530311e9e609", + "packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap": "e1b0c536581dd36599aebcb5562ffe49923b09629cc8fe5d64ba0a2d7fe43630", + "packages/opencode/test/tool/apply_patch.test.ts": "4a22d83eead95e6b9eb26d42dae3b421bf07d5ac9ab3e1fdcb396c2941262c25", + "packages/opencode/test/tool/edit.test.ts": "ec1ed35002b1af4e2ff508aeac456c3edbb2e1dde3f6df4abd957eda9ef6d056", + "packages/opencode/test/tool/question.test.ts": "75760ba6ed98f2fdfd75945c3b492342dfe91026f99cde86db19cf52444260c2", + "packages/opencode/test/tool/registry-schema.test.ts": "053cb9732299ea6554db007bad873d8d61cd3769aa9514d66c70c3bf0d405b90", + "packages/opencode/test/tool/truncation.test.ts": "582ef7199b3f294b1c1588fadda8a1c0ea43ca9af0424a97f48747ade1654ad1", + "packages/opencode/test/tool/write.test.ts": "820d36fde2314abbd939ce43ca5bcc4cfa0139d12a4c669716249060d9a955ff", "packages/schema/src/index.ts": "007e8bc04cc91c2c0aba3a78212c47c4fed6a8edd0d4be05286667d303448d11", "packages/schema/src/location.ts": "078649bbc39b8e3413311b5af85392437873597d8954494016943cfb13564805", "packages/schema/src/prompt.ts": "eb3b42b98ef856f85f32154482b19de546272f1eabe552ee9207db030346291b", diff --git a/packages/app-bundle/overlay/packages/opencode/package.json b/packages/app-bundle/overlay/packages/opencode/package.json new file mode 100644 index 000000000..8067f4456 --- /dev/null +++ b/packages/app-bundle/overlay/packages/opencode/package.json @@ -0,0 +1,159 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "version": "1.18.12", + "name": "opencode", + "type": "module", + "license": "MIT", + "private": true, + "scripts": { + "typecheck": "tsgo --noEmit", + "test": "bun test --timeout 30000 --only-failures", + "test:httpapi": "bun run script/httpapi-exercise.ts --mode coverage --fail-on-missing --fail-on-skip && bun run script/httpapi-exercise.ts --mode auth --fail-on-missing --fail-on-skip && bun run script/httpapi-exercise.ts --mode effect --fail-on-missing --fail-on-skip", + "bench:test": "bun run script/bench-test-suite.ts", + "profile:test": "bun run script/profile-test-files.ts", + "build": "bun run script/build.ts", + "dev": "bun run --conditions=browser ./src/index.ts", + "dev:temporary": "bun run --conditions=browser ./src/temporary.ts" + }, + "bin": { + "opencode": "./bin/opencode" + }, + "exports": { + "./*": "./src/*.ts" + }, + "imports": { + "#db": { + "bun": "./src/storage/db.bun.ts", + "node": "./src/storage/db.node.ts", + "default": "./src/storage/db.bun.ts" + } + }, + "devDependencies": { + "@babel/core": "7.28.4", + "@octokit/webhooks-types": "7.6.1", + "@opencode-ai/core": "workspace:*", + "@opencode-ai/http-recorder": "workspace:*", + "@opencode-ai/script": "workspace:*", + "@standard-schema/spec": "1.0.0", + "@tsconfig/bun": "catalog:", + "@types/babel__core": "7.20.5", + "@types/bun": "catalog:", + "@types/cross-spawn": "catalog:", + "@types/mime-types": "3.0.1", + "@types/npm-package-arg": "6.1.4", + "@types/semver": "^7.5.8", + "@types/turndown": "5.0.5", + "@types/yargs": "17.0.33", + "@typescript/native-preview": "catalog:", + "drizzle-orm": "catalog:", + "prettier": "3.6.2", + "typescript": "catalog:", + "vscode-languageserver-types": "3.17.5", + "why-is-node-running": "3.2.2" + }, + "dependencies": { + "@actions/core": "1.11.1", + "@actions/github": "6.0.1", + "@agentclientprotocol/sdk": "0.21.0", + "@ai-sdk/alibaba": "1.0.17", + "@ai-sdk/amazon-bedrock": "4.0.112", + "@ai-sdk/anthropic": "3.0.82", + "@ai-sdk/azure": "3.0.88", + "@ai-sdk/cerebras": "2.0.60", + "@ai-sdk/cohere": "3.0.27", + "@ai-sdk/deepinfra": "2.0.41", + "@ai-sdk/gateway": "3.0.104", + "@ai-sdk/google": "3.0.73", + "@ai-sdk/google-vertex": "4.0.128", + "@ai-sdk/groq": "3.0.31", + "@ai-sdk/mistral": "3.0.51", + "@ai-sdk/openai": "3.0.84", + "@ai-sdk/openai-compatible": "2.0.41", + "@ai-sdk/perplexity": "3.0.26", + "@ai-sdk/provider": "3.0.8", + "@ai-sdk/togetherai": "2.0.41", + "@ai-sdk/vercel": "2.0.39", + "@ai-sdk/xai": "3.0.102", + "@aws-sdk/credential-providers": "3.1057.0", + "@clack/prompts": "1.0.0-alpha.1", + "@effect/opentelemetry": "catalog:", + "@effect/platform-node": "catalog:", + "@ff-labs/fff-bun": "0.9.4", + "@gitlab/opencode-gitlab-auth": "1.3.3", + "@modelcontextprotocol/sdk": "1.29.0", + "@napi-rs/keyring": "1.3.0", + "@octokit/graphql": "9.0.2", + "@octokit/rest": "catalog:", + "@openauthjs/openauth": "catalog:", + "@opencode-ai/codemode": "workspace:*", + "@opencode-ai/llm": "workspace:*", + "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/protocol": "workspace:*", + "@opencode-ai/schema": "workspace:*", + "@opencode-ai/script": "workspace:*", + "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/server": "workspace:*", + "@opencode-ai/tui": "workspace:*", + "@openrouter/ai-sdk-provider": "2.9.0", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/context-async-hooks": "2.6.1", + "@opentelemetry/exporter-trace-otlp-http": "0.214.0", + "@opentelemetry/sdk-trace-base": "2.6.1", + "@opentelemetry/sdk-trace-node": "2.6.1", + "@opentui/core": "catalog:", + "@opentui/keymap": "catalog:", + "@opentui/solid": "catalog:", + "@parcel/watcher": "2.5.1", + "@pierre/diffs": "catalog:", + "@silvia-odwyer/photon-node": "0.3.4", + "@solid-primitives/event-bus": "1.1.2", + "@solid-primitives/scheduled": "1.5.2", + "@standard-schema/spec": "1.0.0", + "@types/ws": "8.18.1", + "@zip.js/zip.js": "2.7.62", + "ai": "catalog:", + "ai-gateway-provider": "3.1.2", + "bonjour-service": "1.3.0", + "chokidar": "4.0.3", + "cross-spawn": "catalog:", + "decimal.js": "10.5.0", + "diff": "catalog:", + "drizzle-orm": "catalog:", + "effect": "catalog:", + "fuzzysort": "3.1.0", + "gitlab-ai-provider": "6.12.1", + "glob": "13.0.5", + "google-auth-library": "10.5.0", + "gray-matter": "4.0.3", + "htmlparser2": "8.0.2", + "ignore": "7.0.5", + "immer": "11.1.4", + "jsonc-parser": "3.3.1", + "mime-types": "3.0.2", + "minimatch": "10.0.3", + "npm-package-arg": "13.0.2", + "open": "10.1.2", + "opencode-gitlab-auth": "2.1.0", + "opencode-poe-auth": "0.0.1", + "opentui-spinner": "catalog:", + "partial-json": "0.1.7", + "remeda": "catalog:", + "semver": "^7.6.3", + "solid-js": "catalog:", + "strip-ansi": "7.1.2", + "tree-sitter-bash": "0.25.0", + "tree-sitter-powershell": "0.25.10", + "turndown": "7.2.0", + "ulid": "catalog:", + "venice-ai-sdk-provider": "2.1.1", + "vscode-jsonrpc": "8.2.1", + "web-tree-sitter": "0.25.10", + "ws": "8.21.0", + "xdg-basedir": "5.1.0", + "yargs": "18.0.0", + "zod": "catalog:" + }, + "overrides": { + "drizzle-orm": "catalog:" + } +} diff --git a/packages/app-bundle/overlay/packages/opencode/pasqal-live-test.mjs b/packages/app-bundle/overlay/packages/opencode/pasqal-live-test.mjs new file mode 100644 index 000000000..bfc8da0ab --- /dev/null +++ b/packages/app-bundle/overlay/packages/opencode/pasqal-live-test.mjs @@ -0,0 +1,89 @@ +// Live Pasqal test for the #194 keychain silent-re-auth workaround. +// Runs the REAL fork-server code paths (submitCredentialResponse / +// revalidateResponse / disconnectResponse) against LIVE Pasqal via the staged +// validator + pasqal_cloud SDK, and the REAL OS keychain. Isolated: token + +// status files go to a temp dir, the keychain uses a "live-test" slot, and +// everything is cleaned up at the end — your real ~/.amico state is untouched. +// +// Driven by pasqal-live-test.sh (which collects the password without echo). +// Credentials arrive via env: PASQAL_LIVE_USER / PASQAL_LIVE_PW / PASQAL_LIVE_PROJECT. +import { mkdtempSync, rmSync, existsSync, readFileSync } from "node:fs" +import { tmpdir } from "node:os" +import path from "node:path" + +const user = process.env.PASQAL_LIVE_USER ?? "" +const pw = process.env.PASQAL_LIVE_PW ?? "" +const project = process.env.PASQAL_LIVE_PROJECT ?? "" +if (!user || !pw || !project) { + console.error("missing PASQAL_LIVE_USER / PASQAL_LIVE_PW / PASQAL_LIVE_PROJECT — run via pasqal-live-test.sh") + process.exit(2) +} + +// Isolate at-rest state to a temp dir BEFORE importing the module (it reads +// these env vars on first use). Keychain is isolated by using a distinct slot. +const dir = mkdtempSync(path.join(tmpdir(), "pasqal-live-")) +process.env.AMICO_PASQAL_FILE = path.join(dir, "pasqal.json") +process.env.AMICODE_CONNECTIONS_FILE = path.join(dir, "connections.json") +// AMICODE_OPS_DIR + AMICO_PYTHON left at defaults so the REAL staged validator +// (~/.amico/amicode/scripts/pasqal-connector/pasqal_validate.py) + pasqal_cloud run. + +const { submitCredentialResponse, revalidateResponse, disconnectResponse, PASQAL_SECRET_ACCOUNT } = await import( + "./src/server/amicode/connections.ts" +) +const { keychainSecretStore, setPasqalSecretStore } = await import("./src/server/amicode/pasqal-secret.ts") + +// Re-point the keychain slot to a test account so we never touch "default". +const LIVE_ACCOUNT = "live-test" +setPasqalSecretStore({ + read: (a) => keychainSecretStore.read(a === PASQAL_SECRET_ACCOUNT ? LIVE_ACCOUNT : a), + write: (a, s) => keychainSecretStore.write(a === PASQAL_SECRET_ACCOUNT ? LIVE_ACCOUNT : a, s), + clear: (a) => keychainSecretStore.clear(a === PASQAL_SECRET_ACCOUNT ? LIVE_ACCOUNT : a), +}) + +const line = (b) => JSON.parse(b).connection ?? JSON.parse(b) +const show = (label, b) => { + const c = line(b) + console.log(`\n${label}`) + console.log(" state :", c.state) + console.log(" identity :", c.identity ?? "—") + console.log(" devices :", (c.devices ?? []).map((d) => d.name).join(", ") || "—") + console.log(" expires :", c.expires_at ?? "—") + if (JSON.parse(b).error) console.log(" note :", JSON.parse(b).error) +} +const tokenOnDisk = () => + existsSync(process.env.AMICO_PASQAL_FILE) ? JSON.parse(readFileSync(process.env.AMICO_PASQAL_FILE, "utf8")).token : null + +try { + console.log("=== #194 live Pasqal test — real validator, real Pasqal, real keychain (isolated) ===") + + // 1) LIVE connect: real ROPC against Pasqal via the staged validator. + const body = JSON.stringify({ id: "pasqal-cloud", username: user, password: pw, project_id: project }) + show("1) Connect (live password grant)", await submitCredentialResponse(body)) + const t1 = tokenOnDisk() + const stored = keychainSecretStore.read(LIVE_ACCOUNT) + console.log(" token minted & on disk:", t1 ? `yes (${t1.slice(0, 6)}…, ${t1.length} chars)` : "NO") + console.log(" password in keychain :", stored ? `yes (${stored.username})` : "NO") + if (!t1) { + console.log("\nConnect did not mint a token — check the credentials/project and the validator output above.") + process.exit(1) + } + + // 2) Force the token expired on disk, then revalidate → SILENT RE-MINT (a + // second real ROPC from the keychain password, no prompt). + const cred = JSON.parse(readFileSync(process.env.AMICO_PASQAL_FILE, "utf8")) + cred.expires_at = "2020-01-01T00:00:00+00:00" + ;(await import("node:fs")).writeFileSync(process.env.AMICO_PASQAL_FILE, JSON.stringify(cred)) + console.log("\n2) Forced token expiry on disk (simulating the ~24h lapse)…") + show(" Revalidate (silent re-mint from keychain)", await revalidateResponse(JSON.stringify({ id: "pasqal-cloud" }))) + const t2 = tokenOnDisk() + console.log(" token re-minted:", t2 && t2 !== cred.token ? "yes (fresh token, no prompt)" : t2 ? "same value returned" : "NO") + console.log(" → this is the workaround: an expired token renewed itself with zero user interaction.") + + // 3) Disconnect wipes the keychain slot. + show("3) Disconnect", disconnectResponse(JSON.stringify({ id: "pasqal-cloud" }))) + console.log(" keychain after disconnect:", keychainSecretStore.read(LIVE_ACCOUNT) ?? "wiped ✓") + console.log("\n=== done — live connect + silent re-mint + disconnect all exercised ===") +} finally { + keychainSecretStore.clear(LIVE_ACCOUNT) // belt-and-suspenders: never leave a test secret behind + rmSync(dir, { recursive: true, force: true }) +} diff --git a/packages/app-bundle/overlay/packages/opencode/pasqal-live-test.sh b/packages/app-bundle/overlay/packages/opencode/pasqal-live-test.sh new file mode 100644 index 000000000..861fee386 --- /dev/null +++ b/packages/app-bundle/overlay/packages/opencode/pasqal-live-test.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Live test for the #194 Pasqal keychain silent-re-auth workaround. +# Collects your Pasqal credentials WITHOUT echoing the password, then runs the +# real server code against live Pasqal + the real macOS keychain (isolated to a +# temp token file and a "live-test" keychain slot — your real ~/.amico state is +# never touched). Nothing is written to shell history or any argv. +set -euo pipefail +cd "$(dirname "$0")" + +DEFAULT_PROJECT="8b948e29-93cb-4042-b5dc-6916f379d575" # Harmoniqs Tests (Jack's notes) + +read -r -p "Pasqal username (email): " PASQAL_LIVE_USER +read -r -s -p "Pasqal password (hidden): " PASQAL_LIVE_PW; echo +read -r -p "Project ID [${DEFAULT_PROJECT}]: " PASQAL_LIVE_PROJECT +PASQAL_LIVE_PROJECT="${PASQAL_LIVE_PROJECT:-$DEFAULT_PROJECT}" +export PASQAL_LIVE_USER PASQAL_LIVE_PW PASQAL_LIVE_PROJECT + +PATH="$HOME/.bun/bin:$PATH" bun ./pasqal-live-test.mjs diff --git a/packages/app-bundle/overlay/packages/opencode/script/build.ts b/packages/app-bundle/overlay/packages/opencode/script/build.ts new file mode 100644 index 000000000..1adc6c35e --- /dev/null +++ b/packages/app-bundle/overlay/packages/opencode/script/build.ts @@ -0,0 +1,269 @@ +#!/usr/bin/env bun + +import { $ } from "bun" +import path from "path" +import { fileURLToPath } from "url" +import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const dir = path.resolve(__dirname, "..") + +process.chdir(dir) + +const generated = await import("./generate.ts") + +import { Script } from "@opencode-ai/script" +import pkg from "../package.json" + +const singleFlag = process.argv.includes("--single") +const baselineFlag = process.argv.includes("--baseline") +const skipInstall = process.argv.includes("--skip-install") +const sourcemapsFlag = process.argv.includes("--sourcemaps") +const plugin = createSolidTransformPlugin() +const skipEmbedWebUi = process.argv.includes("--skip-embed-web-ui") + +const createEmbeddedWebUIBundle = async () => { + console.log(`Building Web UI to embed in the binary`) + const appDir = path.join(import.meta.dirname, "../../app") + const dist = path.join(appDir, "dist") + await $`OPENCODE_CHANNEL=${Script.channel} bun run --cwd ${appDir} build` + const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: dist }))) + .map((file) => file.replaceAll("\\", "/")) + .filter((file) => !file.endsWith(".map")) + .sort() + const imports = files.map((file, i) => { + const spec = path.relative(dir, path.join(dist, file)).replaceAll("\\", "/") + return `import file_${i} from ${JSON.stringify(spec.startsWith(".") ? spec : `./${spec}`)} with { type: "file" };` + }) + const entries = files.map((file, i) => ` ${JSON.stringify(file)}: file_${i},`) + return [ + `// Import all files as file_$i with type: "file"`, + ...imports, + `// Export with original mappings`, + `export default {`, + ...entries, + `}`, + ].join("\n") +} + +const embeddedFileMap = skipEmbedWebUi ? null : await createEmbeddedWebUIBundle() +const treeSitterWorker = await Bun.file(fileURLToPath(import.meta.resolve("@opentui/core/parser.worker"))).text() + +const allTargets: { + os: string + arch: "arm64" | "x64" + abi?: "musl" + avx2?: false +}[] = [ + { + os: "linux", + arch: "arm64", + }, + { + os: "linux", + arch: "x64", + }, + { + os: "linux", + arch: "x64", + avx2: false, + }, + { + os: "linux", + arch: "arm64", + abi: "musl", + }, + { + os: "linux", + arch: "x64", + abi: "musl", + }, + { + os: "linux", + arch: "x64", + abi: "musl", + avx2: false, + }, + { + os: "darwin", + arch: "arm64", + }, + { + os: "darwin", + arch: "x64", + }, + { + os: "darwin", + arch: "x64", + avx2: false, + }, + { + os: "win32", + arch: "arm64", + }, + { + os: "win32", + arch: "x64", + }, + { + os: "win32", + arch: "x64", + avx2: false, + }, +] + +const targets = singleFlag + ? allTargets.filter((item) => { + if (item.os !== process.platform || item.arch !== process.arch) { + return false + } + + // When building for the current platform, prefer a single native binary by default. + // Baseline binaries require additional Bun artifacts and can be flaky to download. + if (item.avx2 === false) { + return baselineFlag + } + + // also skip abi-specific builds for the same reason + if (item.abi !== undefined) { + return false + } + + return true + }) + : allTargets + +// amicode: optional explicit target filter, e.g. +// OPENCODE_BUILD_TARGETS="opencode-linux-x64,opencode-darwin-arm64" bun run script/build.ts +// Matches the constructed artifact names; unset -> stock behavior. +const requestedTargets = (process.env.OPENCODE_BUILD_TARGETS ?? "") + .split(",") + .map((value) => value.trim()) + .filter(Boolean) +const buildTargets = requestedTargets.length + ? allTargets.filter((item) => + requestedTargets.includes( + [ + pkg.name, + item.os === "win32" ? "windows" : item.os, + item.arch, + item.avx2 === false ? "baseline" : undefined, + item.abi === undefined ? undefined : item.abi, + ] + .filter(Boolean) + .join("-"), + ), + ) + : targets + +await $`rm -rf dist` + +const binaries: Record = {} +if (!skipInstall) { + await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` + await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}` + await $`bun install --os="*" --cpu="*" @ff-labs/fff-bun@${pkg.dependencies["@ff-labs/fff-bun"]}` +} +for (const item of buildTargets) { + const name = [ + pkg.name, + // changing to win32 flags npm for some reason + item.os === "win32" ? "windows" : item.os, + item.arch, + item.avx2 === false ? "baseline" : undefined, + item.abi === undefined ? undefined : item.abi, + ] + .filter(Boolean) + .join("-") + console.log(`building ${name}`) + await $`mkdir -p dist/${name}/bin` + + const workerPath = "./src/cli/tui/worker.ts" + const treeSitterWorkerPath = "opentui-tree-sitter-worker.js" + const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/" + + await Bun.build({ + conditions: ["bun", "node"], + tsconfig: "./tsconfig.json", + plugins: [plugin], + external: ["node-gyp"], + format: "esm", + minify: true, + sourcemap: sourcemapsFlag ? "linked" : "none", + splitting: true, + compile: { + autoloadBunfig: false, + autoloadDotenv: false, + autoloadTsconfig: true, + autoloadPackageJson: true, + target: name.replace(pkg.name, "bun") as any, + outfile: `dist/${name}/bin/opencode`, + execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"], + windows: {}, + }, + files: { + [treeSitterWorkerPath]: treeSitterWorker, + ...(embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}), + }, + entrypoints: [ + "./src/index.ts", + workerPath, + treeSitterWorkerPath, + ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : []), + ], + define: { + FFF_LIBC: JSON.stringify(item.abi === "musl" ? "musl" : "gnu"), + OPENCODE_VERSION: `'${Script.version}'`, + OPENCODE_MODELS_DEV: generated.modelsData, + OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + treeSitterWorkerPath, + OPENCODE_WORKER_PATH: workerPath, + OPENCODE_CHANNEL: `'${Script.channel}'`, + OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", + ...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}), + }, + }) + + // Smoke test: only run if binary is for current platform + if (item.os === process.platform && item.arch === process.arch && !item.abi) { + const binaryPath = `dist/${name}/bin/opencode` + console.log(`Running smoke test: ${binaryPath} --version`) + try { + const versionOutput = await $`${binaryPath} --version`.text() + console.log(`Smoke test passed: ${versionOutput.trim()}`) + } catch (e) { + console.error(`Smoke test failed for ${name}:`, e) + process.exit(1) + } + } + + await $`rm -rf ./dist/${name}/bin/tui` + await Bun.file(`dist/${name}/package.json`).write( + JSON.stringify( + { + name, + version: Script.version, + preferUnplugged: true, + os: [item.os], + cpu: [item.arch], + ...(item.abi ? { libc: [item.abi] } : {}), + }, + null, + 2, + ), + ) + binaries[name] = Script.version +} + +if (Script.release) { + for (const key of Object.keys(binaries)) { + if (key.includes("linux")) { + await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`) + } else { + await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`) + } + } + await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber --repo ${process.env.GH_REPO}` +} + +export { binaries } diff --git a/packages/app-bundle/overlay/packages/opencode/script/httpapi-exercise.ts b/packages/app-bundle/overlay/packages/opencode/script/httpapi-exercise.ts new file mode 100644 index 000000000..1a0a522bc --- /dev/null +++ b/packages/app-bundle/overlay/packages/opencode/script/httpapi-exercise.ts @@ -0,0 +1,3 @@ +// Establish the isolated XDG paths before any harness dependency resolves Global.Path. +await import("../test/server/httpapi-exercise/environment") +await import("../test/server/httpapi-exercise/index") diff --git a/packages/app-bundle/overlay/packages/opencode/src/agent/agent.ts b/packages/app-bundle/overlay/packages/opencode/src/agent/agent.ts new file mode 100644 index 000000000..4c578c899 --- /dev/null +++ b/packages/app-bundle/overlay/packages/opencode/src/agent/agent.ts @@ -0,0 +1,461 @@ +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { PermissionV1 } from "@opencode-ai/core/v1/permission" +import { Config } from "@/config/config" +import { serviceUse } from "@opencode-ai/core/effect/service-use" +import { Provider } from "@/provider/provider" + +import { generateObject, streamObject, type ModelMessage } from "ai" +import { Truncate } from "@/tool/truncate" +import { Auth } from "../auth" +import { ProviderTransform } from "@/provider/transform" + +import PROMPT_GENERATE from "./generate.txt" +import PROMPT_COMPACTION from "./prompt/compaction.txt" +import PROMPT_EXPLORE from "./prompt/explore.txt" +import PROMPT_SUMMARY from "./prompt/summary.txt" +import PROMPT_TITLE from "./prompt/title.txt" +import { Permission } from "@/permission" +import { mergeDeep, pipe, sortBy, values } from "remeda" +import { Global } from "@opencode-ai/core/global" +import path from "path" +import { Plugin } from "@/plugin" +import { Skill } from "../skill" +import { Effect, Context, Layer, Schema } from "effect" +import { InstanceState } from "@/effect/instance-state" +import * as Option from "effect/Option" +import * as OtelTracer from "@effect/opentelemetry/Tracer" +import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { ModelV2 } from "@opencode-ai/core/model" +import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services" +import { Reference } from "@opencode-ai/core/reference" +import { Location } from "@opencode-ai/core/location" +import { PluginV2 } from "@opencode-ai/core/plugin" + +export const Info = Schema.Struct({ + name: Schema.String, + description: Schema.optional(Schema.String), + mode: Schema.Literals(["subagent", "primary", "all"]), + native: Schema.optional(Schema.Boolean), + hidden: Schema.optional(Schema.Boolean), + topP: Schema.optional(Schema.Finite), + temperature: Schema.optional(Schema.Finite), + color: Schema.optional(Schema.String), + permission: PermissionV1.Ruleset, + model: Schema.optional( + Schema.Struct({ + modelID: ModelV2.ID, + providerID: ProviderV2.ID, + }), + ), + variant: Schema.optional(Schema.String), + prompt: Schema.optional(Schema.String), + options: Schema.Record(Schema.String, Schema.Unknown), + steps: Schema.optional(Schema.Finite), +}).annotate({ identifier: "Agent" }) +export type Info = DeepMutable> + +const GeneratedAgent = Schema.Struct({ + identifier: Schema.String, + whenToUse: Schema.String, + systemPrompt: Schema.String, +}) + +export interface Interface { + readonly get: (agent: string) => Effect.Effect + readonly list: () => Effect.Effect + readonly defaultInfo: () => Effect.Effect + readonly defaultAgent: () => Effect.Effect + readonly generate: (input: { + description: string + model?: { providerID: ProviderV2.ID; modelID: ModelV2.ID } + }) => Effect.Effect< + { + identifier: string + whenToUse: string + systemPrompt: string + }, + Provider.DefaultModelError + > +} + +type State = Omit + +export class Service extends Context.Service()("@opencode/Agent") {} + +export const use = serviceUse(Service) + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const config = yield* Config.Service + const auth = yield* Auth.Service + const plugin = yield* Plugin.Service + const skill = yield* Skill.Service + const provider = yield* Provider.Service + const locations = yield* LocationServiceMap.Service + + const state = yield* InstanceState.make( + Effect.fn("Agent.state")(function* (ctx) { + const cfg = yield* config.get() + const skillDirs = yield* skill.dirs() + const referenceDirs = Object.keys(cfg.references ?? cfg.reference ?? {}).length + ? yield* Effect.gen(function* () { + yield* (yield* PluginV2.Service).wait(PluginV2.ID.make("core/config-reference")) + return (yield* (yield* Reference.Service).list()).map((reference) => reference.path) + }).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) })))) + : [] + const whitelistedDirs = [ + Truncate.GLOB, + path.join(Global.Path.tmp, "*"), + ...skillDirs.map((dir) => path.join(dir, "*")), + ...referenceDirs.map((dir) => path.join(dir, "*")), + ] + const readonlyExternalDirectory = { + "*": "ask", + ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])), + } satisfies Record + + const defaults = Permission.fromConfig({ + "*": "allow", + doom_loop: "ask", + external_directory: { + "*": "ask", + ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])), + }, + question: "deny", + plan_enter: "deny", + plan_exit: "deny", + // mirrors github.com/github/gitignore Node.gitignore pattern for .env files + read: { + "*": "allow", + "*.env": "ask", + "*.env.*": "ask", + "*.env.example": "allow", + }, + }) + + const user = Permission.fromConfig(cfg.permission ?? {}) + + const agents: Record = { + build: { + name: "build", + description: "The default agent. Executes tools based on configured permissions.", + options: {}, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + question: "allow", + plan_enter: "allow", + }), + user, + ), + mode: "primary", + native: true, + }, + plan: { + name: "plan", + description: "Plan mode. Disallows all edit tools.", + options: {}, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + question: "allow", + plan_exit: "allow", + task: { + general: "deny", + }, + external_directory: { + [path.join(Global.Path.data, "plans", "*")]: "allow", + }, + edit: { + "*": "deny", + [path.join(".opencode", "plans", "*.md")]: "allow", + [path.relative(ctx.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow", + }, + }), + user, + ), + mode: "primary", + native: true, + }, + general: { + name: "general", + description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + todowrite: "deny", + }), + user, + ), + options: {}, + mode: "subagent", + native: true, + }, + explore: { + name: "explore", + permission: Permission.merge( + defaults, + Permission.fromConfig({ + "*": "deny", + grep: "allow", + glob: "allow", + list: "allow", + bash: "allow", + webfetch: "allow", + websearch: "allow", + read: "allow", + external_directory: readonlyExternalDirectory, + }), + user, + ), + description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`, + prompt: PROMPT_EXPLORE, + options: {}, + mode: "subagent", + native: true, + }, + compaction: { + name: "compaction", + mode: "primary", + native: true, + hidden: true, + prompt: PROMPT_COMPACTION, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + "*": "deny", + }), + user, + ), + options: {}, + }, + title: { + name: "title", + mode: "primary", + options: {}, + native: true, + hidden: true, + temperature: 0.5, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + "*": "deny", + }), + user, + ), + prompt: PROMPT_TITLE, + }, + summary: { + name: "summary", + mode: "primary", + options: {}, + native: true, + hidden: true, + permission: Permission.merge( + defaults, + Permission.fromConfig({ + "*": "deny", + }), + user, + ), + prompt: PROMPT_SUMMARY, + }, + } + + for (const [key, value] of Object.entries(cfg.agent ?? {})) { + if (value.disable) { + delete agents[key] + continue + } + let item = agents[key] + if (!item) + item = agents[key] = { + name: key, + mode: "all", + permission: Permission.merge(defaults, user), + options: {}, + native: false, + } + if (value.model) item.model = Provider.parseModel(value.model) + item.variant = value.variant ?? item.variant + item.prompt = value.prompt ?? item.prompt + item.description = value.description ?? item.description + item.temperature = value.temperature ?? item.temperature + item.topP = value.top_p ?? item.topP + item.mode = value.mode ?? item.mode + item.color = value.color ?? item.color + item.hidden = value.hidden ?? item.hidden + item.name = value.name ?? item.name + item.steps = value.steps ?? item.steps + item.options = mergeDeep(item.options, value.options ?? {}) + item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {})) + } + + // Ensure Truncate.GLOB is allowed unless explicitly configured + for (const name in agents) { + const agent = agents[name] + const explicit = agent.permission.some((r) => { + if (r.permission !== "external_directory") return false + if (r.action !== "deny") return false + return r.pattern === Truncate.GLOB + }) + if (explicit) continue + + agents[name].permission = Permission.merge( + agents[name].permission, + Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }), + ) + } + + const get = Effect.fnUntraced(function* (agent: string) { + return agents[agent] + }) + + const list = Effect.fnUntraced(function* () { + const cfg = yield* config.get() + const order = cfg.agent_order ?? [] + return pipe( + agents, + values(), + sortBy( + [ + (x) => { + const index = order.indexOf(x.name) + return index === -1 ? Number.MAX_SAFE_INTEGER : index + }, + "asc", + ], + [(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"], + [(x) => x.name, "asc"], + ), + ) + }) + + const defaultInfo = Effect.fnUntraced(function* () { + const c = yield* config.get() + if (c.default_agent) { + const agent = agents[c.default_agent] + if (!agent) throw new Error(`default agent "${c.default_agent}" not found`) + if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`) + if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`) + return agent + } + const visible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true) + if (!visible) throw new Error("no primary visible agent found") + return visible + }) + + const defaultAgent = Effect.fnUntraced(function* () { + return (yield* defaultInfo()).name + }) + + return { + get, + list, + defaultInfo, + defaultAgent, + } satisfies State + }), + ) + + return Service.of({ + get: Effect.fn("Agent.get")(function* (agent: string) { + return yield* InstanceState.useEffect(state, (s) => s.get(agent)) + }), + list: Effect.fn("Agent.list")(function* () { + return yield* InstanceState.useEffect(state, (s) => s.list()) + }), + defaultInfo: Effect.fn("Agent.defaultInfo")(function* () { + return yield* InstanceState.useEffect(state, (s) => s.defaultInfo()) + }), + defaultAgent: Effect.fn("Agent.defaultAgent")(function* () { + return yield* InstanceState.useEffect(state, (s) => s.defaultAgent()) + }), + generate: Effect.fn("Agent.generate")(function* (input: { + description: string + model?: { providerID: ProviderV2.ID; modelID: ModelV2.ID } + }) { + const cfg = yield* config.get() + const model = input.model ?? (yield* provider.defaultModel()) + const resolved = yield* provider.getModel(model.providerID, model.modelID) + const language = yield* provider.getLanguage(resolved) + const tracer = cfg.experimental?.openTelemetry + ? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer)) + : undefined + + const system = [PROMPT_GENERATE] + yield* plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system }) + const existing = yield* InstanceState.useEffect(state, (s) => s.list()) + + // TODO: clean this up so provider specific logic doesnt bleed over + const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie) + const isOpenaiOauth = model.providerID === "openai" && authInfo?.type === "oauth" + + const params = { + experimental_telemetry: { + isEnabled: cfg.experimental?.openTelemetry, + tracer, + metadata: { + userId: cfg.username ?? "unknown", + }, + }, + temperature: 0.3, + messages: [ + ...(isOpenaiOauth + ? [] + : system.map( + (item): ModelMessage => ({ + role: "system", + content: item, + }), + )), + { + role: "user", + content: `Create an agent configuration based on this request: "${input.description}".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`, + }, + ], + model: language, + schema: Object.assign( + Schema.toStandardSchemaV1(GeneratedAgent), + Schema.toStandardJSONSchemaV1(GeneratedAgent), + ), + } satisfies Parameters[0] + + if (isOpenaiOauth) { + return yield* Effect.promise(async () => { + const result = streamObject({ + ...params, + providerOptions: ProviderTransform.providerOptions(resolved, { + instructions: system.join("\n"), + store: false, + }), + onError: () => {}, + }) + for await (const part of result.fullStream) { + if (part.type === "error") throw part.error + } + return result.object + }) + } + + return yield* Effect.promise(() => generateObject(params).then((r) => r.object)) + }), + }) + }), +) + +const locationServiceMapNode = LayerNode.make({ + service: LocationServiceMap.Service, + layer: locationServiceMapLayer, + deps: [], +}) + +export const node = LayerNode.make({ + service: Service, + layer: layer, + deps: [Config.node, Auth.node, Plugin.node, Skill.node, Provider.node, locationServiceMapNode], +}) + +export * as Agent from "./agent" diff --git a/packages/app-bundle/overlay/packages/opencode/src/cli/cmd/run/footer.question.tsx b/packages/app-bundle/overlay/packages/opencode/src/cli/cmd/run/footer.question.tsx new file mode 100644 index 000000000..4039c6aef --- /dev/null +++ b/packages/app-bundle/overlay/packages/opencode/src/cli/cmd/run/footer.question.tsx @@ -0,0 +1,631 @@ +// Question UI body for the direct-mode footer. +// +// Renders inside the footer when the reducer pushes a FooterView of type +// "question". Supports single-question and multi-question flows: +// +// Single question: options list with up/down selection, digit shortcuts, +// and optional custom text input. +// +// Multi-question: tabbed interface where each question is a tab, plus a +// final "Confirm" tab that shows all answers for review. Tab/shift-tab +// or left/right to navigate between questions. +// +// All state logic lives in question.shared.ts as a pure state machine. +// This component just renders it and dispatches keyboard events. +/** @jsxImportSource @opentui/solid */ +import type { TextareaRenderable } from "@opentui/core" +import { useKeyboard, useTerminalDimensions } from "@opentui/solid" +import { For, Show, createEffect, createMemo, createSignal } from "solid-js" +import type { QuestionRequest } from "@opencode-ai/sdk/v2" +import { + createQuestionBodyState, + questionConfirm, + questionCustom, + questionInfo, + questionInput, + questionMove, + questionOther, + questionPicked, + questionReject, + questionSave, + questionSelect, + questionSetEditing, + questionSetSelected, + questionSetSubmitting, + questionSetTab, + questionSingle, + questionStoreCustom, + questionSubmit, + questionSync, + questionTabs, + questionText, + questionTotal, +} from "./question.shared" +import { footerWidthPolicy } from "./footer.width" +import type { RunFooterTheme } from "./theme" +import type { QuestionReject, QuestionReply } from "./types" + +export function RunQuestionBody(props: { + request: QuestionRequest + theme: RunFooterTheme + onReply: (input: QuestionReply) => void | Promise + onReject: (input: QuestionReject) => void | Promise +}) { + const dims = useTerminalDimensions() + const [state, setState] = createSignal(createQuestionBodyState(props.request.id)) + const single = createMemo(() => questionSingle(props.request)) + const confirm = createMemo(() => questionConfirm(props.request, state())) + const info = createMemo(() => questionInfo(props.request, state())) + // A Free-form Question (amicode#245) renders as a text card: the header plus + // a bare text input with submit — no option rows, no pseudo-option. The card + // is always in editing mode; its answer rides the typed-custom-answer path. + const text = createMemo(() => !confirm() && questionText(props.request, state())) + const editing = createMemo(() => state().editing || text()) + const input = createMemo(() => questionInput(state())) + const other = createMemo(() => questionOther(props.request, state())) + const picked = createMemo(() => questionPicked(state())) + const disabled = createMemo(() => state().submitting) + const narrow = createMemo(() => footerWidthPolicy(dims().width).dialog.narrow) + const verb = createMemo(() => { + if (confirm()) { + return "submit" + } + + if (info()?.multiple) { + return "toggle" + } + + if (single()) { + return "submit" + } + + return "confirm" + }) + let area: TextareaRenderable | undefined + + createEffect(() => { + setState((prev) => questionSync(prev, props.request.id)) + }) + + const setTab = (tab: number) => { + setState((prev) => questionSetTab(prev, tab)) + } + + const move = (dir: -1 | 1) => { + setState((prev) => questionMove(prev, props.request, dir)) + } + + const beginReply = async (input: QuestionReply) => { + setState((prev) => questionSetSubmitting(prev, true)) + + try { + await props.onReply(input) + } catch { + setState((prev) => questionSetSubmitting(prev, false)) + } + } + + const beginReject = async (input: QuestionReject) => { + setState((prev) => questionSetSubmitting(prev, true)) + + try { + await props.onReject(input) + } catch { + setState((prev) => questionSetSubmitting(prev, false)) + } + } + + const saveCustom = () => { + const cur = state() + const next = questionSave(cur, props.request) + if (next.state !== cur) { + setState(next.state) + } + + if (!next.reply) { + return + } + + void beginReply(next.reply) + } + + const choose = (selected: number) => { + const base = state() + const cur = questionSetSelected(base, selected) + const next = questionSelect(cur, props.request) + if (next.state !== base) { + setState(next.state) + } + + if (!next.reply) { + return + } + + void beginReply(next.reply) + } + + const mark = (selected: number) => { + setState((prev) => questionSetSelected(prev, selected)) + } + + const select = () => { + const cur = state() + const next = questionSelect(cur, props.request) + if (next.state !== cur) { + setState(next.state) + } + + if (!next.reply) { + return + } + + void beginReply(next.reply) + } + + const submit = () => { + void beginReply(questionSubmit(props.request, state())) + } + + const reject = () => { + void beginReject(questionReject(props.request)) + } + + useKeyboard((event) => { + const cur = state() + if (cur.submitting) { + event.preventDefault() + return + } + + if (editing()) { + if (event.name === "escape") { + // A text card has no non-editing mode: esc dismisses the card, the + // same rejection a choice card's esc performs outside editing. + if (text()) { + reject() + } else { + setState((prev) => questionSetEditing(prev, false)) + } + event.preventDefault() + return + } + + return + } + + if (!single() && (event.name === "left" || event.name === "h")) { + setTab((cur.tab - 1 + questionTabs(props.request)) % questionTabs(props.request)) + event.preventDefault() + return + } + + if (!single() && (event.name === "right" || event.name === "l")) { + setTab((cur.tab + 1) % questionTabs(props.request)) + event.preventDefault() + return + } + + if (!single() && event.name === "tab") { + const dir = event.shift ? -1 : 1 + setTab((cur.tab + dir + questionTabs(props.request)) % questionTabs(props.request)) + event.preventDefault() + return + } + + if (questionConfirm(props.request, cur)) { + if (event.name === "return") { + submit() + event.preventDefault() + return + } + + if (event.name === "escape") { + reject() + event.preventDefault() + } + return + } + + const total = questionTotal(props.request, cur) + const max = Math.min(total, 9) + const digit = Number(event.name) + if (!Number.isNaN(digit) && digit >= 1 && digit <= max) { + choose(digit - 1) + event.preventDefault() + return + } + + if (event.name === "up" || event.name === "k") { + move(-1) + event.preventDefault() + return + } + + if (event.name === "down" || event.name === "j") { + move(1) + event.preventDefault() + return + } + + if (event.name === "return") { + select() + event.preventDefault() + return + } + + if (event.name === "escape") { + reject() + event.preventDefault() + } + }) + + createEffect(() => { + if (!editing() || !area || area.isDestroyed) { + return + } + + if (area.plainText !== input()) { + area.setText(input()) + area.cursorOffset = input().length + } + + queueMicrotask(() => { + if (!area || area.isDestroyed || !editing()) { + return + } + + area.focus() + area.cursorOffset = area.plainText.length + }) + }) + + return ( + + + + + + {(item, index) => { + const active = () => state().tab === index() + const answered = () => (state().answers[index()]?.length ?? 0) > 0 + return ( + { + if (!disabled()) setTab(index()) + }} + > + + {item.header} + + + ) + }} + + { + if (!disabled()) setTab(props.request.questions.length) + }} + > + Confirm + + + + + + + + + Review + + + {(item, index) => { + const value = () => state().answers[index()]?.join(", ") ?? "" + const answered = () => Boolean(value()) + return ( + + + {item.header}:{" "} + + {answered() ? value() : "(not answered)"} + + + + ) + }} + + + + + } + > + + + + {info()?.question} + {info()?.multiple ? " (select all that apply)" : ""} + + + + +