experimental.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { Account } from "@/account/account"
  2. import { Config } from "@/config"
  3. import { MCP } from "@/mcp"
  4. import { ToolRegistry } from "@/tool"
  5. import { Effect, Layer, Option, Schema } from "effect"
  6. import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
  7. import { Authorization } from "./auth"
  8. const ConsoleStateResponse = Schema.Struct({
  9. consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
  10. activeOrgName: Schema.optionalKey(Schema.String),
  11. switchableOrgCount: Schema.Number,
  12. }).annotate({ identifier: "ConsoleState" })
  13. const ConsoleOrgOption = Schema.Struct({
  14. accountID: Schema.String,
  15. accountEmail: Schema.String,
  16. accountUrl: Schema.String,
  17. orgID: Schema.String,
  18. orgName: Schema.String,
  19. active: Schema.Boolean,
  20. }).annotate({ identifier: "ConsoleOrgOption" })
  21. const ConsoleOrgList = Schema.Struct({
  22. orgs: Schema.Array(ConsoleOrgOption),
  23. }).annotate({ identifier: "ConsoleOrgList" })
  24. const ToolIDs = Schema.Array(Schema.String).annotate({ identifier: "ToolIDs" })
  25. export const ExperimentalPaths = {
  26. console: "/experimental/console",
  27. consoleOrgs: "/experimental/console/orgs",
  28. toolIDs: "/experimental/tool/ids",
  29. resource: "/experimental/resource",
  30. } as const
  31. export const ExperimentalApi = HttpApi.make("experimental")
  32. .add(
  33. HttpApiGroup.make("experimental")
  34. .add(
  35. HttpApiEndpoint.get("console", ExperimentalPaths.console, {
  36. success: ConsoleStateResponse,
  37. }).annotateMerge(
  38. OpenApi.annotations({
  39. identifier: "experimental.console.get",
  40. summary: "Get active Console provider metadata",
  41. description: "Get the active Console org name and the set of provider IDs managed by that Console org.",
  42. }),
  43. ),
  44. HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, {
  45. success: ConsoleOrgList,
  46. }).annotateMerge(
  47. OpenApi.annotations({
  48. identifier: "experimental.console.listOrgs",
  49. summary: "List switchable Console orgs",
  50. description: "Get the available Console orgs across logged-in accounts, including the current active org.",
  51. }),
  52. ),
  53. HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, {
  54. success: ToolIDs,
  55. }).annotateMerge(
  56. OpenApi.annotations({
  57. identifier: "tool.ids",
  58. summary: "List tool IDs",
  59. description:
  60. "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
  61. }),
  62. ),
  63. HttpApiEndpoint.get("resource", ExperimentalPaths.resource, {
  64. success: Schema.Record(Schema.String, MCP.Resource),
  65. }).annotateMerge(
  66. OpenApi.annotations({
  67. identifier: "experimental.resource.list",
  68. summary: "Get MCP resources",
  69. description: "Get all available MCP resources from connected servers. Optionally filter by name.",
  70. }),
  71. ),
  72. )
  73. .annotateMerge(
  74. OpenApi.annotations({
  75. title: "experimental",
  76. description: "Experimental HttpApi read-only routes.",
  77. }),
  78. )
  79. .middleware(Authorization),
  80. )
  81. .annotateMerge(
  82. OpenApi.annotations({
  83. title: "opencode experimental HttpApi",
  84. version: "0.0.1",
  85. description: "Experimental HttpApi surface for selected instance routes.",
  86. }),
  87. )
  88. export const experimentalHandlers = Layer.unwrap(
  89. Effect.gen(function* () {
  90. const account = yield* Account.Service
  91. const config = yield* Config.Service
  92. const mcp = yield* MCP.Service
  93. const registry = yield* ToolRegistry.Service
  94. const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () {
  95. const [state, groups] = yield* Effect.all(
  96. [config.getConsoleState(), account.orgsByAccount().pipe(Effect.orDie)],
  97. {
  98. concurrency: "unbounded",
  99. },
  100. )
  101. return {
  102. consoleManagedProviders: state.consoleManagedProviders,
  103. ...(state.activeOrgName ? { activeOrgName: state.activeOrgName } : {}),
  104. switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
  105. }
  106. })
  107. const listConsoleOrgs = Effect.fn("ExperimentalHttpApi.consoleOrgs")(function* () {
  108. const [groups, active] = yield* Effect.all(
  109. [account.orgsByAccount().pipe(Effect.orDie), account.active().pipe(Effect.orDie)],
  110. {
  111. concurrency: "unbounded",
  112. },
  113. )
  114. const info = Option.getOrUndefined(active)
  115. return {
  116. orgs: groups.flatMap((group) =>
  117. group.orgs.map((org) => ({
  118. accountID: group.account.id,
  119. accountEmail: group.account.email,
  120. accountUrl: group.account.url,
  121. orgID: org.id,
  122. orgName: org.name,
  123. active: !!info && info.id === group.account.id && info.active_org_id === org.id,
  124. })),
  125. ),
  126. }
  127. })
  128. const toolIDs = Effect.fn("ExperimentalHttpApi.toolIDs")(function* () {
  129. return yield* registry.ids()
  130. })
  131. const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () {
  132. return yield* mcp.resources()
  133. })
  134. return HttpApiBuilder.group(ExperimentalApi, "experimental", (handlers) =>
  135. handlers
  136. .handle("console", getConsole)
  137. .handle("consoleOrgs", listConsoleOrgs)
  138. .handle("toolIDs", toolIDs)
  139. .handle("resource", resource),
  140. )
  141. }),
  142. ).pipe(
  143. Layer.provide(Account.defaultLayer),
  144. Layer.provide(Config.defaultLayer),
  145. Layer.provide(MCP.defaultLayer),
  146. Layer.provide(ToolRegistry.defaultLayer),
  147. )