Prechádzať zdrojové kódy

feat(server): add form routes (#35099)

Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Aiden Cline 1 mesiac pred
rodič
commit
d9bf30fc22

+ 7 - 0
.github/actions/setup-bun/action.yml

@@ -8,6 +8,13 @@ inputs:
 runs:
   using: "composite"
   steps:
+    # node-gyp@latest (invoked via bunx for native install scripts) requires Node >=22;
+    # some runner images ship an older system Node on PATH
+    - name: Setup Node
+      uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+      with:
+        node-version: "24"
+
     - name: Get baseline download URL
       id: bun-url
       shell: bash

+ 290 - 196
packages/client/src/effect/generated/client.ts

@@ -552,36 +552,129 @@ const adaptGroup12 = (raw: RawClient["server.project"]) => ({
   directories: Endpoint12_1(raw),
 })
 
-type Endpoint13_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
+type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
 type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
-const Endpoint13_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint13_0Input) =>
+const Endpoint13_0 = (raw: RawClient["server.form"]) => (input?: Endpoint13_0Input) =>
+  raw["form.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
+
+type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0]
+type Endpoint13_1Input = { readonly sessionID: Endpoint13_1Request["params"]["sessionID"] }
+const Endpoint13_1 = (raw: RawClient["server.form"]) => (input: Endpoint13_1Input) =>
+  raw["session.form.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
+    Effect.mapError(mapClientError),
+    Effect.map((value) => value.data),
+  )
+
+type Endpoint13_2Request = Parameters<RawClient["server.form"]["session.form.create"]>[0]
+type Endpoint13_2Input = {
+  readonly sessionID: Endpoint13_2Request["params"]["sessionID"]
+  readonly id?: Endpoint13_2Request["payload"]["id"]
+  readonly title?: Endpoint13_2Request["payload"]["title"]
+  readonly metadata?: Endpoint13_2Request["payload"]["metadata"]
+  readonly mode: Endpoint13_2Request["payload"]["mode"]
+  readonly fields?: Endpoint13_2Request["payload"]["fields"]
+  readonly url?: Endpoint13_2Request["payload"]["url"]
+}
+const Endpoint13_2 = (raw: RawClient["server.form"]) => (input: Endpoint13_2Input) =>
+  raw["session.form.create"]({
+    params: { sessionID: input["sessionID"] },
+    payload: {
+      id: input["id"],
+      title: input["title"],
+      metadata: input["metadata"],
+      mode: input["mode"],
+      fields: input["fields"],
+      url: input["url"],
+    },
+  }).pipe(
+    Effect.mapError(mapClientError),
+    Effect.map((value) => value.data),
+  )
+
+type Endpoint13_3Request = Parameters<RawClient["server.form"]["session.form.get"]>[0]
+type Endpoint13_3Input = {
+  readonly sessionID: Endpoint13_3Request["params"]["sessionID"]
+  readonly formID: Endpoint13_3Request["params"]["formID"]
+}
+const Endpoint13_3 = (raw: RawClient["server.form"]) => (input: Endpoint13_3Input) =>
+  raw["session.form.get"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe(
+    Effect.mapError(mapClientError),
+    Effect.map((value) => value.data),
+  )
+
+type Endpoint13_4Request = Parameters<RawClient["server.form"]["session.form.state"]>[0]
+type Endpoint13_4Input = {
+  readonly sessionID: Endpoint13_4Request["params"]["sessionID"]
+  readonly formID: Endpoint13_4Request["params"]["formID"]
+}
+const Endpoint13_4 = (raw: RawClient["server.form"]) => (input: Endpoint13_4Input) =>
+  raw["session.form.state"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe(
+    Effect.mapError(mapClientError),
+    Effect.map((value) => value.data),
+  )
+
+type Endpoint13_5Request = Parameters<RawClient["server.form"]["session.form.reply"]>[0]
+type Endpoint13_5Input = {
+  readonly sessionID: Endpoint13_5Request["params"]["sessionID"]
+  readonly formID: Endpoint13_5Request["params"]["formID"]
+  readonly answer: Endpoint13_5Request["payload"]["answer"]
+}
+const Endpoint13_5 = (raw: RawClient["server.form"]) => (input: Endpoint13_5Input) =>
+  raw["session.form.reply"]({
+    params: { sessionID: input["sessionID"], formID: input["formID"] },
+    payload: { answer: input["answer"] },
+  }).pipe(Effect.mapError(mapClientError))
+
+type Endpoint13_6Request = Parameters<RawClient["server.form"]["session.form.cancel"]>[0]
+type Endpoint13_6Input = {
+  readonly sessionID: Endpoint13_6Request["params"]["sessionID"]
+  readonly formID: Endpoint13_6Request["params"]["formID"]
+}
+const Endpoint13_6 = (raw: RawClient["server.form"]) => (input: Endpoint13_6Input) =>
+  raw["session.form.cancel"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe(
+    Effect.mapError(mapClientError),
+  )
+
+const adaptGroup13 = (raw: RawClient["server.form"]) => ({
+  listRequests: Endpoint13_0(raw),
+  list: Endpoint13_1(raw),
+  create: Endpoint13_2(raw),
+  get: Endpoint13_3(raw),
+  state: Endpoint13_4(raw),
+  reply: Endpoint13_5(raw),
+  cancel: Endpoint13_6(raw),
+})
+
+type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
+type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
+const Endpoint14_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint14_0Input) =>
   raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint13_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
-type Endpoint13_1Input = { readonly projectID?: Endpoint13_1Request["query"]["projectID"] }
-const Endpoint13_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint13_1Input) =>
+type Endpoint14_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
+type Endpoint14_1Input = { readonly projectID?: Endpoint14_1Request["query"]["projectID"] }
+const Endpoint14_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint14_1Input) =>
   raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe(
     Effect.mapError(mapClientError),
     Effect.map((value) => value.data),
   )
 
-type Endpoint13_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
-type Endpoint13_2Input = { readonly id: Endpoint13_2Request["params"]["id"] }
-const Endpoint13_2 = (raw: RawClient["server.permission"]) => (input: Endpoint13_2Input) =>
+type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
+type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] }
+const Endpoint14_2 = (raw: RawClient["server.permission"]) => (input: Endpoint14_2Input) =>
   raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint13_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
-type Endpoint13_3Input = {
-  readonly sessionID: Endpoint13_3Request["params"]["sessionID"]
-  readonly id?: Endpoint13_3Request["payload"]["id"]
-  readonly action: Endpoint13_3Request["payload"]["action"]
-  readonly resources: Endpoint13_3Request["payload"]["resources"]
-  readonly save?: Endpoint13_3Request["payload"]["save"]
-  readonly metadata?: Endpoint13_3Request["payload"]["metadata"]
-  readonly source?: Endpoint13_3Request["payload"]["source"]
-  readonly agent?: Endpoint13_3Request["payload"]["agent"]
-}
-const Endpoint13_3 = (raw: RawClient["server.permission"]) => (input: Endpoint13_3Input) =>
+type Endpoint14_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
+type Endpoint14_3Input = {
+  readonly sessionID: Endpoint14_3Request["params"]["sessionID"]
+  readonly id?: Endpoint14_3Request["payload"]["id"]
+  readonly action: Endpoint14_3Request["payload"]["action"]
+  readonly resources: Endpoint14_3Request["payload"]["resources"]
+  readonly save?: Endpoint14_3Request["payload"]["save"]
+  readonly metadata?: Endpoint14_3Request["payload"]["metadata"]
+  readonly source?: Endpoint14_3Request["payload"]["source"]
+  readonly agent?: Endpoint14_3Request["payload"]["agent"]
+}
+const Endpoint14_3 = (raw: RawClient["server.permission"]) => (input: Endpoint14_3Input) =>
   raw["session.permission.create"]({
     params: { sessionID: input["sessionID"] },
     payload: {
@@ -598,87 +691,87 @@ const Endpoint13_3 = (raw: RawClient["server.permission"]) => (input: Endpoint13
     Effect.map((value) => value.data),
   )
 
-type Endpoint13_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
-type Endpoint13_4Input = { readonly sessionID: Endpoint13_4Request["params"]["sessionID"] }
-const Endpoint13_4 = (raw: RawClient["server.permission"]) => (input: Endpoint13_4Input) =>
+type Endpoint14_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
+type Endpoint14_4Input = { readonly sessionID: Endpoint14_4Request["params"]["sessionID"] }
+const Endpoint14_4 = (raw: RawClient["server.permission"]) => (input: Endpoint14_4Input) =>
   raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
     Effect.mapError(mapClientError),
     Effect.map((value) => value.data),
   )
 
-type Endpoint13_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
-type Endpoint13_5Input = {
-  readonly sessionID: Endpoint13_5Request["params"]["sessionID"]
-  readonly requestID: Endpoint13_5Request["params"]["requestID"]
+type Endpoint14_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
+type Endpoint14_5Input = {
+  readonly sessionID: Endpoint14_5Request["params"]["sessionID"]
+  readonly requestID: Endpoint14_5Request["params"]["requestID"]
 }
-const Endpoint13_5 = (raw: RawClient["server.permission"]) => (input: Endpoint13_5Input) =>
+const Endpoint14_5 = (raw: RawClient["server.permission"]) => (input: Endpoint14_5Input) =>
   raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
     Effect.mapError(mapClientError),
     Effect.map((value) => value.data),
   )
 
-type Endpoint13_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
-type Endpoint13_6Input = {
-  readonly sessionID: Endpoint13_6Request["params"]["sessionID"]
-  readonly requestID: Endpoint13_6Request["params"]["requestID"]
-  readonly reply: Endpoint13_6Request["payload"]["reply"]
-  readonly message?: Endpoint13_6Request["payload"]["message"]
+type Endpoint14_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
+type Endpoint14_6Input = {
+  readonly sessionID: Endpoint14_6Request["params"]["sessionID"]
+  readonly requestID: Endpoint14_6Request["params"]["requestID"]
+  readonly reply: Endpoint14_6Request["payload"]["reply"]
+  readonly message?: Endpoint14_6Request["payload"]["message"]
 }
-const Endpoint13_6 = (raw: RawClient["server.permission"]) => (input: Endpoint13_6Input) =>
+const Endpoint14_6 = (raw: RawClient["server.permission"]) => (input: Endpoint14_6Input) =>
   raw["session.permission.reply"]({
     params: { sessionID: input["sessionID"], requestID: input["requestID"] },
     payload: { reply: input["reply"], message: input["message"] },
   }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup13 = (raw: RawClient["server.permission"]) => ({
-  listRequests: Endpoint13_0(raw),
-  listSaved: Endpoint13_1(raw),
-  removeSaved: Endpoint13_2(raw),
-  create: Endpoint13_3(raw),
-  list: Endpoint13_4(raw),
-  get: Endpoint13_5(raw),
-  reply: Endpoint13_6(raw),
+const adaptGroup14 = (raw: RawClient["server.permission"]) => ({
+  listRequests: Endpoint14_0(raw),
+  listSaved: Endpoint14_1(raw),
+  removeSaved: Endpoint14_2(raw),
+  create: Endpoint14_3(raw),
+  list: Endpoint14_4(raw),
+  get: Endpoint14_5(raw),
+  reply: Endpoint14_6(raw),
 })
 
-type Endpoint14_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
-type Endpoint14_0Input = {
-  readonly location?: Endpoint14_0Request["query"]["location"]
-  readonly path?: Endpoint14_0Request["query"]["path"]
+type Endpoint15_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
+type Endpoint15_0Input = {
+  readonly location?: Endpoint15_0Request["query"]["location"]
+  readonly path?: Endpoint15_0Request["query"]["path"]
 }
-const Endpoint14_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint14_0Input) =>
+const Endpoint15_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint15_0Input) =>
   raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-type Endpoint14_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
-type Endpoint14_1Input = {
-  readonly location?: Endpoint14_1Request["query"]["location"]
-  readonly query: Endpoint14_1Request["query"]["query"]
-  readonly type?: Endpoint14_1Request["query"]["type"]
-  readonly limit?: Endpoint14_1Request["query"]["limit"]
+type Endpoint15_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
+type Endpoint15_1Input = {
+  readonly location?: Endpoint15_1Request["query"]["location"]
+  readonly query: Endpoint15_1Request["query"]["query"]
+  readonly type?: Endpoint15_1Request["query"]["type"]
+  readonly limit?: Endpoint15_1Request["query"]["limit"]
 }
-const Endpoint14_1 = (raw: RawClient["server.fs"]) => (input: Endpoint14_1Input) =>
+const Endpoint15_1 = (raw: RawClient["server.fs"]) => (input: Endpoint15_1Input) =>
   raw["fs.find"]({
     query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
   }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup14 = (raw: RawClient["server.fs"]) => ({ list: Endpoint14_0(raw), find: Endpoint14_1(raw) })
+const adaptGroup15 = (raw: RawClient["server.fs"]) => ({ list: Endpoint15_0(raw), find: Endpoint15_1(raw) })
 
-type Endpoint15_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
-type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] }
-const Endpoint15_0 = (raw: RawClient["server.command"]) => (input?: Endpoint15_0Input) =>
+type Endpoint16_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
+type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
+const Endpoint16_0 = (raw: RawClient["server.command"]) => (input?: Endpoint16_0Input) =>
   raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup15 = (raw: RawClient["server.command"]) => ({ list: Endpoint15_0(raw) })
+const adaptGroup16 = (raw: RawClient["server.command"]) => ({ list: Endpoint16_0(raw) })
 
-type Endpoint16_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
-type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
-const Endpoint16_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint16_0Input) =>
+type Endpoint17_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
+type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] }
+const Endpoint17_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint17_0Input) =>
   raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup16 = (raw: RawClient["server.skill"]) => ({ list: Endpoint16_0(raw) })
+const adaptGroup17 = (raw: RawClient["server.skill"]) => ({ list: Endpoint17_0(raw) })
 
-const Endpoint17_0 = (raw: RawClient["server.event"]) => () =>
+const Endpoint18_0 = (raw: RawClient["server.event"]) => () =>
   Stream.unwrap(
     raw["event.subscribe"]({}).pipe(
       Effect.mapError(mapClientError),
@@ -686,7 +779,7 @@ const Endpoint17_0 = (raw: RawClient["server.event"]) => () =>
     ),
   )
 
-const Endpoint17_1 = (raw: RawClient["server.event"]) => () =>
+const Endpoint18_1 = (raw: RawClient["server.event"]) => () =>
   Stream.unwrap(
     raw["event.changes"]({}).pipe(
       Effect.mapError(mapClientError),
@@ -694,23 +787,23 @@ const Endpoint17_1 = (raw: RawClient["server.event"]) => () =>
     ),
   )
 
-const adaptGroup17 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint17_0(raw), changes: Endpoint17_1(raw) })
+const adaptGroup18 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint18_0(raw), changes: Endpoint18_1(raw) })
 
-type Endpoint18_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
-type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] }
-const Endpoint18_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint18_0Input) =>
+type Endpoint19_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
+type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
+const Endpoint19_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint19_0Input) =>
   raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint18_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
-type Endpoint18_1Input = {
-  readonly location?: Endpoint18_1Request["query"]["location"]
-  readonly command?: Endpoint18_1Request["payload"]["command"]
-  readonly args?: Endpoint18_1Request["payload"]["args"]
-  readonly cwd?: Endpoint18_1Request["payload"]["cwd"]
-  readonly title?: Endpoint18_1Request["payload"]["title"]
-  readonly env?: Endpoint18_1Request["payload"]["env"]
+type Endpoint19_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
+type Endpoint19_1Input = {
+  readonly location?: Endpoint19_1Request["query"]["location"]
+  readonly command?: Endpoint19_1Request["payload"]["command"]
+  readonly args?: Endpoint19_1Request["payload"]["args"]
+  readonly cwd?: Endpoint19_1Request["payload"]["cwd"]
+  readonly title?: Endpoint19_1Request["payload"]["title"]
+  readonly env?: Endpoint19_1Request["payload"]["env"]
 }
-const Endpoint18_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint18_1Input) =>
+const Endpoint19_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint19_1Input) =>
   raw["pty.create"]({
     query: { location: input?.["location"] },
     payload: {
@@ -722,201 +815,201 @@ const Endpoint18_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint18_1Inpu
     },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint18_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
-type Endpoint18_2Input = {
-  readonly ptyID: Endpoint18_2Request["params"]["ptyID"]
-  readonly location?: Endpoint18_2Request["query"]["location"]
+type Endpoint19_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
+type Endpoint19_2Input = {
+  readonly ptyID: Endpoint19_2Request["params"]["ptyID"]
+  readonly location?: Endpoint19_2Request["query"]["location"]
 }
-const Endpoint18_2 = (raw: RawClient["server.pty"]) => (input: Endpoint18_2Input) =>
+const Endpoint19_2 = (raw: RawClient["server.pty"]) => (input: Endpoint19_2Input) =>
   raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-type Endpoint18_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
-type Endpoint18_3Input = {
-  readonly ptyID: Endpoint18_3Request["params"]["ptyID"]
-  readonly location?: Endpoint18_3Request["query"]["location"]
-  readonly title?: Endpoint18_3Request["payload"]["title"]
-  readonly size?: Endpoint18_3Request["payload"]["size"]
+type Endpoint19_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
+type Endpoint19_3Input = {
+  readonly ptyID: Endpoint19_3Request["params"]["ptyID"]
+  readonly location?: Endpoint19_3Request["query"]["location"]
+  readonly title?: Endpoint19_3Request["payload"]["title"]
+  readonly size?: Endpoint19_3Request["payload"]["size"]
 }
-const Endpoint18_3 = (raw: RawClient["server.pty"]) => (input: Endpoint18_3Input) =>
+const Endpoint19_3 = (raw: RawClient["server.pty"]) => (input: Endpoint19_3Input) =>
   raw["pty.update"]({
     params: { ptyID: input["ptyID"] },
     query: { location: input["location"] },
     payload: { title: input["title"], size: input["size"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint18_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
-type Endpoint18_4Input = {
-  readonly ptyID: Endpoint18_4Request["params"]["ptyID"]
-  readonly location?: Endpoint18_4Request["query"]["location"]
+type Endpoint19_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
+type Endpoint19_4Input = {
+  readonly ptyID: Endpoint19_4Request["params"]["ptyID"]
+  readonly location?: Endpoint19_4Request["query"]["location"]
 }
-const Endpoint18_4 = (raw: RawClient["server.pty"]) => (input: Endpoint18_4Input) =>
+const Endpoint19_4 = (raw: RawClient["server.pty"]) => (input: Endpoint19_4Input) =>
   raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-const adaptGroup18 = (raw: RawClient["server.pty"]) => ({
-  list: Endpoint18_0(raw),
-  create: Endpoint18_1(raw),
-  get: Endpoint18_2(raw),
-  update: Endpoint18_3(raw),
-  remove: Endpoint18_4(raw),
+const adaptGroup19 = (raw: RawClient["server.pty"]) => ({
+  list: Endpoint19_0(raw),
+  create: Endpoint19_1(raw),
+  get: Endpoint19_2(raw),
+  update: Endpoint19_3(raw),
+  remove: Endpoint19_4(raw),
 })
 
-type Endpoint19_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
-type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
-const Endpoint19_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint19_0Input) =>
+type Endpoint20_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
+type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
+const Endpoint20_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint20_0Input) =>
   raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint19_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
-type Endpoint19_1Input = {
-  readonly location?: Endpoint19_1Request["query"]["location"]
-  readonly command: Endpoint19_1Request["payload"]["command"]
-  readonly cwd?: Endpoint19_1Request["payload"]["cwd"]
-  readonly timeout?: Endpoint19_1Request["payload"]["timeout"]
-  readonly metadata?: Endpoint19_1Request["payload"]["metadata"]
+type Endpoint20_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
+type Endpoint20_1Input = {
+  readonly location?: Endpoint20_1Request["query"]["location"]
+  readonly command: Endpoint20_1Request["payload"]["command"]
+  readonly cwd?: Endpoint20_1Request["payload"]["cwd"]
+  readonly timeout?: Endpoint20_1Request["payload"]["timeout"]
+  readonly metadata?: Endpoint20_1Request["payload"]["metadata"]
 }
-const Endpoint19_1 = (raw: RawClient["server.shell"]) => (input: Endpoint19_1Input) =>
+const Endpoint20_1 = (raw: RawClient["server.shell"]) => (input: Endpoint20_1Input) =>
   raw["shell.create"]({
     query: { location: input["location"] },
     payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint19_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
-type Endpoint19_2Input = {
-  readonly id: Endpoint19_2Request["params"]["id"]
-  readonly location?: Endpoint19_2Request["query"]["location"]
+type Endpoint20_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
+type Endpoint20_2Input = {
+  readonly id: Endpoint20_2Request["params"]["id"]
+  readonly location?: Endpoint20_2Request["query"]["location"]
 }
-const Endpoint19_2 = (raw: RawClient["server.shell"]) => (input: Endpoint19_2Input) =>
+const Endpoint20_2 = (raw: RawClient["server.shell"]) => (input: Endpoint20_2Input) =>
   raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-type Endpoint19_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
-type Endpoint19_3Input = {
-  readonly id: Endpoint19_3Request["params"]["id"]
-  readonly location?: Endpoint19_3Request["query"]["location"]
-  readonly cursor?: Endpoint19_3Request["query"]["cursor"]
-  readonly limit?: Endpoint19_3Request["query"]["limit"]
+type Endpoint20_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
+type Endpoint20_3Input = {
+  readonly id: Endpoint20_3Request["params"]["id"]
+  readonly location?: Endpoint20_3Request["query"]["location"]
+  readonly cursor?: Endpoint20_3Request["query"]["cursor"]
+  readonly limit?: Endpoint20_3Request["query"]["limit"]
 }
-const Endpoint19_3 = (raw: RawClient["server.shell"]) => (input: Endpoint19_3Input) =>
+const Endpoint20_3 = (raw: RawClient["server.shell"]) => (input: Endpoint20_3Input) =>
   raw["shell.output"]({
     params: { id: input["id"] },
     query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint19_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
-type Endpoint19_4Input = {
-  readonly id: Endpoint19_4Request["params"]["id"]
-  readonly location?: Endpoint19_4Request["query"]["location"]
+type Endpoint20_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
+type Endpoint20_4Input = {
+  readonly id: Endpoint20_4Request["params"]["id"]
+  readonly location?: Endpoint20_4Request["query"]["location"]
 }
-const Endpoint19_4 = (raw: RawClient["server.shell"]) => (input: Endpoint19_4Input) =>
+const Endpoint20_4 = (raw: RawClient["server.shell"]) => (input: Endpoint20_4Input) =>
   raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-const adaptGroup19 = (raw: RawClient["server.shell"]) => ({
-  list: Endpoint19_0(raw),
-  create: Endpoint19_1(raw),
-  get: Endpoint19_2(raw),
-  output: Endpoint19_3(raw),
-  remove: Endpoint19_4(raw),
+const adaptGroup20 = (raw: RawClient["server.shell"]) => ({
+  list: Endpoint20_0(raw),
+  create: Endpoint20_1(raw),
+  get: Endpoint20_2(raw),
+  output: Endpoint20_3(raw),
+  remove: Endpoint20_4(raw),
 })
 
-type Endpoint20_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
-type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
-const Endpoint20_0 = (raw: RawClient["server.question"]) => (input?: Endpoint20_0Input) =>
+type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
+type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
+const Endpoint21_0 = (raw: RawClient["server.question"]) => (input?: Endpoint21_0Input) =>
   raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint20_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
-type Endpoint20_1Input = { readonly sessionID: Endpoint20_1Request["params"]["sessionID"] }
-const Endpoint20_1 = (raw: RawClient["server.question"]) => (input: Endpoint20_1Input) =>
+type Endpoint21_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
+type Endpoint21_1Input = { readonly sessionID: Endpoint21_1Request["params"]["sessionID"] }
+const Endpoint21_1 = (raw: RawClient["server.question"]) => (input: Endpoint21_1Input) =>
   raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
     Effect.mapError(mapClientError),
     Effect.map((value) => value.data),
   )
 
-type Endpoint20_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
-type Endpoint20_2Input = {
-  readonly sessionID: Endpoint20_2Request["params"]["sessionID"]
-  readonly requestID: Endpoint20_2Request["params"]["requestID"]
-  readonly answers: Endpoint20_2Request["payload"]["answers"]
+type Endpoint21_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
+type Endpoint21_2Input = {
+  readonly sessionID: Endpoint21_2Request["params"]["sessionID"]
+  readonly requestID: Endpoint21_2Request["params"]["requestID"]
+  readonly answers: Endpoint21_2Request["payload"]["answers"]
 }
-const Endpoint20_2 = (raw: RawClient["server.question"]) => (input: Endpoint20_2Input) =>
+const Endpoint21_2 = (raw: RawClient["server.question"]) => (input: Endpoint21_2Input) =>
   raw["session.question.reply"]({
     params: { sessionID: input["sessionID"], requestID: input["requestID"] },
     payload: { answers: input["answers"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint20_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
-type Endpoint20_3Input = {
-  readonly sessionID: Endpoint20_3Request["params"]["sessionID"]
-  readonly requestID: Endpoint20_3Request["params"]["requestID"]
+type Endpoint21_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
+type Endpoint21_3Input = {
+  readonly sessionID: Endpoint21_3Request["params"]["sessionID"]
+  readonly requestID: Endpoint21_3Request["params"]["requestID"]
 }
-const Endpoint20_3 = (raw: RawClient["server.question"]) => (input: Endpoint20_3Input) =>
+const Endpoint21_3 = (raw: RawClient["server.question"]) => (input: Endpoint21_3Input) =>
   raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
     Effect.mapError(mapClientError),
   )
 
-const adaptGroup20 = (raw: RawClient["server.question"]) => ({
-  listRequests: Endpoint20_0(raw),
-  list: Endpoint20_1(raw),
-  reply: Endpoint20_2(raw),
-  reject: Endpoint20_3(raw),
+const adaptGroup21 = (raw: RawClient["server.question"]) => ({
+  listRequests: Endpoint21_0(raw),
+  list: Endpoint21_1(raw),
+  reply: Endpoint21_2(raw),
+  reject: Endpoint21_3(raw),
 })
 
-type Endpoint21_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
-type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
-const Endpoint21_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint21_0Input) =>
+type Endpoint22_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
+type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] }
+const Endpoint22_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint22_0Input) =>
   raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup21 = (raw: RawClient["server.reference"]) => ({ list: Endpoint21_0(raw) })
+const adaptGroup22 = (raw: RawClient["server.reference"]) => ({ list: Endpoint22_0(raw) })
 
-type Endpoint22_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
-type Endpoint22_0Input = {
-  readonly projectID: Endpoint22_0Request["params"]["projectID"]
-  readonly location?: Endpoint22_0Request["query"]["location"]
-  readonly strategy: Endpoint22_0Request["payload"]["strategy"]
-  readonly directory: Endpoint22_0Request["payload"]["directory"]
-  readonly name?: Endpoint22_0Request["payload"]["name"]
+type Endpoint23_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
+type Endpoint23_0Input = {
+  readonly projectID: Endpoint23_0Request["params"]["projectID"]
+  readonly location?: Endpoint23_0Request["query"]["location"]
+  readonly strategy: Endpoint23_0Request["payload"]["strategy"]
+  readonly directory: Endpoint23_0Request["payload"]["directory"]
+  readonly name?: Endpoint23_0Request["payload"]["name"]
 }
-const Endpoint22_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint22_0Input) =>
+const Endpoint23_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint23_0Input) =>
   raw["projectCopy.create"]({
     params: { projectID: input["projectID"] },
     query: { location: input["location"] },
     payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint22_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
-type Endpoint22_1Input = {
-  readonly projectID: Endpoint22_1Request["params"]["projectID"]
-  readonly location?: Endpoint22_1Request["query"]["location"]
-  readonly directory: Endpoint22_1Request["payload"]["directory"]
-  readonly force: Endpoint22_1Request["payload"]["force"]
+type Endpoint23_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
+type Endpoint23_1Input = {
+  readonly projectID: Endpoint23_1Request["params"]["projectID"]
+  readonly location?: Endpoint23_1Request["query"]["location"]
+  readonly directory: Endpoint23_1Request["payload"]["directory"]
+  readonly force: Endpoint23_1Request["payload"]["force"]
 }
-const Endpoint22_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint22_1Input) =>
+const Endpoint23_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint23_1Input) =>
   raw["projectCopy.remove"]({
     params: { projectID: input["projectID"] },
     query: { location: input["location"] },
     payload: { directory: input["directory"], force: input["force"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint22_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
-type Endpoint22_2Input = {
-  readonly projectID: Endpoint22_2Request["params"]["projectID"]
-  readonly location?: Endpoint22_2Request["query"]["location"]
+type Endpoint23_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
+type Endpoint23_2Input = {
+  readonly projectID: Endpoint23_2Request["params"]["projectID"]
+  readonly location?: Endpoint23_2Request["query"]["location"]
 }
-const Endpoint22_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint22_2Input) =>
+const Endpoint23_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint23_2Input) =>
   raw["projectCopy.refresh"]({
     params: { projectID: input["projectID"] },
     query: { location: input["location"] },
   }).pipe(Effect.mapError(mapClientError))
 
-const adaptGroup22 = (raw: RawClient["server.projectCopy"]) => ({
-  create: Endpoint22_0(raw),
-  remove: Endpoint22_1(raw),
-  refresh: Endpoint22_2(raw),
+const adaptGroup23 = (raw: RawClient["server.projectCopy"]) => ({
+  create: Endpoint23_0(raw),
+  remove: Endpoint23_1(raw),
+  refresh: Endpoint23_2(raw),
 })
 
 const adaptClient = (raw: RawClient) => ({
@@ -933,16 +1026,17 @@ const adaptClient = (raw: RawClient) => ({
   "server.mcp": adaptGroup10(raw["server.mcp"]),
   credential: adaptGroup11(raw["server.credential"]),
   project: adaptGroup12(raw["server.project"]),
-  permission: adaptGroup13(raw["server.permission"]),
-  file: adaptGroup14(raw["server.fs"]),
-  command: adaptGroup15(raw["server.command"]),
-  skill: adaptGroup16(raw["server.skill"]),
-  event: adaptGroup17(raw["server.event"]),
-  pty: adaptGroup18(raw["server.pty"]),
-  shell: adaptGroup19(raw["server.shell"]),
-  question: adaptGroup20(raw["server.question"]),
-  reference: adaptGroup21(raw["server.reference"]),
-  projectCopy: adaptGroup22(raw["server.projectCopy"]),
+  form: adaptGroup13(raw["server.form"]),
+  permission: adaptGroup14(raw["server.permission"]),
+  file: adaptGroup15(raw["server.fs"]),
+  command: adaptGroup16(raw["server.command"]),
+  skill: adaptGroup17(raw["server.skill"]),
+  event: adaptGroup18(raw["server.event"]),
+  pty: adaptGroup19(raw["server.pty"]),
+  shell: adaptGroup20(raw["server.shell"]),
+  question: adaptGroup21(raw["server.question"]),
+  reference: adaptGroup22(raw["server.reference"]),
+  projectCopy: adaptGroup23(raw["server.projectCopy"]),
 })
 
 export const make = (options?: { readonly baseUrl?: URL | string }) =>

+ 1 - 0
packages/client/src/effect/index.ts

@@ -9,6 +9,7 @@ export { Credential } from "@opencode-ai/schema/credential"
 export { Event } from "@opencode-ai/schema/event"
 export { EventLog } from "@opencode-ai/schema/event-log"
 export { FileSystem } from "@opencode-ai/schema/filesystem"
+export { Form } from "@opencode-ai/schema/form"
 export { Integration } from "@opencode-ai/schema/integration"
 export { Location } from "@opencode-ai/schema/location"
 export { Model } from "@opencode-ai/schema/model"

+ 103 - 0
packages/client/src/promise/generated/client.ts

@@ -91,6 +91,20 @@ import type {
   ProjectCurrentOutput,
   ProjectDirectoriesInput,
   ProjectDirectoriesOutput,
+  FormListRequestsInput,
+  FormListRequestsOutput,
+  FormListInput,
+  FormListOutput,
+  FormCreateInput,
+  FormCreateOutput,
+  FormGetInput,
+  FormGetOutput,
+  FormStateInput,
+  FormStateOutput,
+  FormReplyInput,
+  FormReplyOutput,
+  FormCancelInput,
+  FormCancelOutput,
   PermissionListRequestsInput,
   PermissionListRequestsOutput,
   PermissionListSavedInput,
@@ -891,6 +905,95 @@ export function make(options: ClientOptions) {
           requestOptions,
         ),
     },
+    form: {
+      listRequests: (input?: FormListRequestsInput, requestOptions?: RequestOptions) =>
+        request<FormListRequestsOutput>(
+          {
+            method: "GET",
+            path: `/api/form/request`,
+            query: { location: input?.["location"] },
+            successStatus: 200,
+            declaredStatuses: [401, 400],
+            empty: false,
+          },
+          requestOptions,
+        ),
+      list: (input: FormListInput, requestOptions?: RequestOptions) =>
+        request<{ readonly data: FormListOutput }>(
+          {
+            method: "GET",
+            path: `/api/session/${encodeURIComponent(input.sessionID)}/form`,
+            successStatus: 200,
+            declaredStatuses: [404, 400, 401],
+            empty: false,
+          },
+          requestOptions,
+        ).then((value) => value.data),
+      create: (input: FormCreateInput, requestOptions?: RequestOptions) =>
+        request<{ readonly data: FormCreateOutput }>(
+          {
+            method: "POST",
+            path: `/api/session/${encodeURIComponent(input.sessionID)}/form`,
+            body: {
+              id: input["id"],
+              title: input["title"],
+              metadata: input["metadata"],
+              mode: input["mode"],
+              fields: input["fields"],
+              url: input["url"],
+            },
+            successStatus: 200,
+            declaredStatuses: [404, 409, 400, 401],
+            empty: false,
+          },
+          requestOptions,
+        ).then((value) => value.data),
+      get: (input: FormGetInput, requestOptions?: RequestOptions) =>
+        request<{ readonly data: FormGetOutput }>(
+          {
+            method: "GET",
+            path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`,
+            successStatus: 200,
+            declaredStatuses: [404, 400, 401],
+            empty: false,
+          },
+          requestOptions,
+        ).then((value) => value.data),
+      state: (input: FormStateInput, requestOptions?: RequestOptions) =>
+        request<{ readonly data: FormStateOutput }>(
+          {
+            method: "GET",
+            path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/state`,
+            successStatus: 200,
+            declaredStatuses: [404, 400, 401],
+            empty: false,
+          },
+          requestOptions,
+        ).then((value) => value.data),
+      reply: (input: FormReplyInput, requestOptions?: RequestOptions) =>
+        request<FormReplyOutput>(
+          {
+            method: "POST",
+            path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/reply`,
+            body: { answer: input["answer"] },
+            successStatus: 204,
+            declaredStatuses: [404, 409, 400, 401],
+            empty: true,
+          },
+          requestOptions,
+        ),
+      cancel: (input: FormCancelInput, requestOptions?: RequestOptions) =>
+        request<FormCancelOutput>(
+          {
+            method: "POST",
+            path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/cancel`,
+            successStatus: 204,
+            declaredStatuses: [404, 409, 400, 401],
+            empty: true,
+          },
+          requestOptions,
+        ),
+    },
     permission: {
       listRequests: (input?: PermissionListRequestsInput, requestOptions?: RequestOptions) =>
         request<PermissionListRequestsOutput>(

+ 894 - 0
packages/client/src/promise/generated/types.ts

@@ -106,6 +106,26 @@ export type ProviderNotFoundError = {
 export const isProviderNotFoundError = (value: unknown): value is ProviderNotFoundError =>
   typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProviderNotFoundError"
 
+export type FormNotFoundError = { readonly _tag: "FormNotFoundError"; readonly id: string; readonly message: string }
+export const isFormNotFoundError = (value: unknown): value is FormNotFoundError =>
+  typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormNotFoundError"
+
+export type FormAlreadySettledError = {
+  readonly _tag: "FormAlreadySettledError"
+  readonly id: string
+  readonly message: string
+}
+export const isFormAlreadySettledError = (value: unknown): value is FormAlreadySettledError =>
+  typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormAlreadySettledError"
+
+export type FormInvalidAnswerError = {
+  readonly _tag: "FormInvalidAnswerError"
+  readonly id: string
+  readonly message: string
+}
+export const isFormInvalidAnswerError = (value: unknown): value is FormInvalidAnswerError =>
+  typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormInvalidAnswerError"
+
 export type PermissionNotFoundError = {
   readonly _tag: "PermissionNotFoundError"
   readonly requestID: string
@@ -2408,6 +2428,880 @@ export type ProjectDirectoriesInput = {
 
 export type ProjectDirectoriesOutput = ReadonlyArray<{ readonly directory: string; readonly strategy?: string }>
 
+export type FormListRequestsInput = {
+  readonly location?: {
+    readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
+  }["location"]
+}
+
+export type FormListRequestsOutput = {
+  readonly location: {
+    readonly directory: string
+    readonly workspaceID?: string
+    readonly project: { readonly id: string; readonly directory: string }
+  }
+  readonly data: ReadonlyArray<
+    | {
+        readonly id: string
+        readonly sessionID: string
+        readonly title?: string
+        readonly metadata?: { readonly [x: string]: JsonValue }
+        readonly mode: "form"
+        readonly fields: ReadonlyArray<
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "string"
+              readonly format?: "email" | "uri" | "date" | "date-time"
+              readonly minLength?: number
+              readonly maxLength?: number
+              readonly pattern?: string
+              readonly placeholder?: string
+              readonly default?: string
+              readonly options?: ReadonlyArray<{
+                readonly value: string
+                readonly label: string
+                readonly description?: string
+              }>
+              readonly custom?: boolean
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "number"
+              readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "integer"
+              readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "boolean"
+              readonly default?: boolean
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "multiselect"
+              readonly options: ReadonlyArray<{
+                readonly value: string
+                readonly label: string
+                readonly description?: string
+              }>
+              readonly minItems?: number
+              readonly maxItems?: number
+              readonly custom?: boolean
+              readonly default?: ReadonlyArray<string>
+            }
+        >
+      }
+    | {
+        readonly id: string
+        readonly sessionID: string
+        readonly title?: string
+        readonly metadata?: { readonly [x: string]: JsonValue }
+        readonly mode: "url"
+        readonly url: string
+      }
+  >
+}
+
+export type FormListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
+
+export type FormListOutput = {
+  readonly data: ReadonlyArray<
+    | {
+        readonly id: string
+        readonly sessionID: string
+        readonly title?: string
+        readonly metadata?: { readonly [x: string]: JsonValue }
+        readonly mode: "form"
+        readonly fields: ReadonlyArray<
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "string"
+              readonly format?: "email" | "uri" | "date" | "date-time"
+              readonly minLength?: number
+              readonly maxLength?: number
+              readonly pattern?: string
+              readonly placeholder?: string
+              readonly default?: string
+              readonly options?: ReadonlyArray<{
+                readonly value: string
+                readonly label: string
+                readonly description?: string
+              }>
+              readonly custom?: boolean
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "number"
+              readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "integer"
+              readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "boolean"
+              readonly default?: boolean
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "multiselect"
+              readonly options: ReadonlyArray<{
+                readonly value: string
+                readonly label: string
+                readonly description?: string
+              }>
+              readonly minItems?: number
+              readonly maxItems?: number
+              readonly custom?: boolean
+              readonly default?: ReadonlyArray<string>
+            }
+        >
+      }
+    | {
+        readonly id: string
+        readonly sessionID: string
+        readonly title?: string
+        readonly metadata?: { readonly [x: string]: JsonValue }
+        readonly mode: "url"
+        readonly url: string
+      }
+  >
+}["data"]
+
+export type FormCreateInput = {
+  readonly sessionID: { readonly sessionID: string }["sessionID"]
+  readonly id?: {
+    readonly id?: string | null
+    readonly title?: string
+    readonly metadata?: { readonly [x: string]: JsonValue }
+    readonly mode: "form" | "url"
+    readonly fields?: ReadonlyArray<
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "string"
+          readonly format?: "email" | "uri" | "date" | "date-time"
+          readonly minLength?: number
+          readonly maxLength?: number
+          readonly pattern?: string
+          readonly placeholder?: string
+          readonly default?: string
+          readonly options?: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly custom?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "number"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "integer"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "boolean"
+          readonly default?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "multiselect"
+          readonly options: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly minItems?: number
+          readonly maxItems?: number
+          readonly custom?: boolean
+          readonly default?: ReadonlyArray<string>
+        }
+    > | null
+    readonly url?: string | null
+  }["id"]
+  readonly title?: {
+    readonly id?: string | null
+    readonly title?: string
+    readonly metadata?: { readonly [x: string]: JsonValue }
+    readonly mode: "form" | "url"
+    readonly fields?: ReadonlyArray<
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "string"
+          readonly format?: "email" | "uri" | "date" | "date-time"
+          readonly minLength?: number
+          readonly maxLength?: number
+          readonly pattern?: string
+          readonly placeholder?: string
+          readonly default?: string
+          readonly options?: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly custom?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "number"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "integer"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "boolean"
+          readonly default?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "multiselect"
+          readonly options: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly minItems?: number
+          readonly maxItems?: number
+          readonly custom?: boolean
+          readonly default?: ReadonlyArray<string>
+        }
+    > | null
+    readonly url?: string | null
+  }["title"]
+  readonly metadata?: {
+    readonly id?: string | null
+    readonly title?: string
+    readonly metadata?: { readonly [x: string]: JsonValue }
+    readonly mode: "form" | "url"
+    readonly fields?: ReadonlyArray<
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "string"
+          readonly format?: "email" | "uri" | "date" | "date-time"
+          readonly minLength?: number
+          readonly maxLength?: number
+          readonly pattern?: string
+          readonly placeholder?: string
+          readonly default?: string
+          readonly options?: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly custom?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "number"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "integer"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "boolean"
+          readonly default?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "multiselect"
+          readonly options: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly minItems?: number
+          readonly maxItems?: number
+          readonly custom?: boolean
+          readonly default?: ReadonlyArray<string>
+        }
+    > | null
+    readonly url?: string | null
+  }["metadata"]
+  readonly mode: {
+    readonly id?: string | null
+    readonly title?: string
+    readonly metadata?: { readonly [x: string]: JsonValue }
+    readonly mode: "form" | "url"
+    readonly fields?: ReadonlyArray<
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "string"
+          readonly format?: "email" | "uri" | "date" | "date-time"
+          readonly minLength?: number
+          readonly maxLength?: number
+          readonly pattern?: string
+          readonly placeholder?: string
+          readonly default?: string
+          readonly options?: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly custom?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "number"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "integer"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "boolean"
+          readonly default?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "multiselect"
+          readonly options: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly minItems?: number
+          readonly maxItems?: number
+          readonly custom?: boolean
+          readonly default?: ReadonlyArray<string>
+        }
+    > | null
+    readonly url?: string | null
+  }["mode"]
+  readonly fields?: {
+    readonly id?: string | null
+    readonly title?: string
+    readonly metadata?: { readonly [x: string]: JsonValue }
+    readonly mode: "form" | "url"
+    readonly fields?: ReadonlyArray<
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "string"
+          readonly format?: "email" | "uri" | "date" | "date-time"
+          readonly minLength?: number
+          readonly maxLength?: number
+          readonly pattern?: string
+          readonly placeholder?: string
+          readonly default?: string
+          readonly options?: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly custom?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "number"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "integer"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "boolean"
+          readonly default?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "multiselect"
+          readonly options: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly minItems?: number
+          readonly maxItems?: number
+          readonly custom?: boolean
+          readonly default?: ReadonlyArray<string>
+        }
+    > | null
+    readonly url?: string | null
+  }["fields"]
+  readonly url?: {
+    readonly id?: string | null
+    readonly title?: string
+    readonly metadata?: { readonly [x: string]: JsonValue }
+    readonly mode: "form" | "url"
+    readonly fields?: ReadonlyArray<
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "string"
+          readonly format?: "email" | "uri" | "date" | "date-time"
+          readonly minLength?: number
+          readonly maxLength?: number
+          readonly pattern?: string
+          readonly placeholder?: string
+          readonly default?: string
+          readonly options?: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly custom?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "number"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "integer"
+          readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+          readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "boolean"
+          readonly default?: boolean
+        }
+      | {
+          readonly key: string
+          readonly title?: string
+          readonly description?: string
+          readonly required?: boolean
+          readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+          readonly type: "multiselect"
+          readonly options: ReadonlyArray<{
+            readonly value: string
+            readonly label: string
+            readonly description?: string
+          }>
+          readonly minItems?: number
+          readonly maxItems?: number
+          readonly custom?: boolean
+          readonly default?: ReadonlyArray<string>
+        }
+    > | null
+    readonly url?: string | null
+  }["url"]
+}
+
+export type FormCreateOutput = {
+  readonly data:
+    | {
+        readonly id: string
+        readonly sessionID: string
+        readonly title?: string
+        readonly metadata?: { readonly [x: string]: JsonValue }
+        readonly mode: "form"
+        readonly fields: ReadonlyArray<
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "string"
+              readonly format?: "email" | "uri" | "date" | "date-time"
+              readonly minLength?: number
+              readonly maxLength?: number
+              readonly pattern?: string
+              readonly placeholder?: string
+              readonly default?: string
+              readonly options?: ReadonlyArray<{
+                readonly value: string
+                readonly label: string
+                readonly description?: string
+              }>
+              readonly custom?: boolean
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "number"
+              readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "integer"
+              readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "boolean"
+              readonly default?: boolean
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "multiselect"
+              readonly options: ReadonlyArray<{
+                readonly value: string
+                readonly label: string
+                readonly description?: string
+              }>
+              readonly minItems?: number
+              readonly maxItems?: number
+              readonly custom?: boolean
+              readonly default?: ReadonlyArray<string>
+            }
+        >
+      }
+    | {
+        readonly id: string
+        readonly sessionID: string
+        readonly title?: string
+        readonly metadata?: { readonly [x: string]: JsonValue }
+        readonly mode: "url"
+        readonly url: string
+      }
+}["data"]
+
+export type FormGetInput = {
+  readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
+  readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
+}
+
+export type FormGetOutput = {
+  readonly data:
+    | {
+        readonly id: string
+        readonly sessionID: string
+        readonly title?: string
+        readonly metadata?: { readonly [x: string]: JsonValue }
+        readonly mode: "form"
+        readonly fields: ReadonlyArray<
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "string"
+              readonly format?: "email" | "uri" | "date" | "date-time"
+              readonly minLength?: number
+              readonly maxLength?: number
+              readonly pattern?: string
+              readonly placeholder?: string
+              readonly default?: string
+              readonly options?: ReadonlyArray<{
+                readonly value: string
+                readonly label: string
+                readonly description?: string
+              }>
+              readonly custom?: boolean
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "number"
+              readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "integer"
+              readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
+              readonly default?: number | "Infinity" | "-Infinity" | "NaN"
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "boolean"
+              readonly default?: boolean
+            }
+          | {
+              readonly key: string
+              readonly title?: string
+              readonly description?: string
+              readonly required?: boolean
+              readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
+              readonly type: "multiselect"
+              readonly options: ReadonlyArray<{
+                readonly value: string
+                readonly label: string
+                readonly description?: string
+              }>
+              readonly minItems?: number
+              readonly maxItems?: number
+              readonly custom?: boolean
+              readonly default?: ReadonlyArray<string>
+            }
+        >
+      }
+    | {
+        readonly id: string
+        readonly sessionID: string
+        readonly title?: string
+        readonly metadata?: { readonly [x: string]: JsonValue }
+        readonly mode: "url"
+        readonly url: string
+      }
+}["data"]
+
+export type FormStateInput = {
+  readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
+  readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
+}
+
+export type FormStateOutput = {
+  readonly data:
+    | { readonly status: "pending" }
+    | {
+        readonly status: "answered"
+        readonly answer: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
+      }
+    | { readonly status: "cancelled" }
+}["data"]
+
+export type FormReplyInput = {
+  readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
+  readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
+  readonly answer: {
+    readonly answer: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
+  }["answer"]
+}
+
+export type FormReplyOutput = void
+
+export type FormCancelInput = {
+  readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
+  readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
+}
+
+export type FormCancelOutput = void
+
 export type PermissionListRequestsInput = {
   readonly location?: {
     readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined

+ 2 - 0
packages/core/src/location-services.ts

@@ -10,6 +10,7 @@ import { FileMutation } from "./file-mutation"
 import { FileSystem } from "./filesystem"
 import { FileSystemSearch } from "./filesystem/search"
 import { Generate } from "./generate"
+import { Form } from "./form"
 import { Watcher } from "./filesystem/watcher"
 import { Image } from "./image"
 import { Integration } from "./integration"
@@ -83,6 +84,7 @@ export const locationServices = LayerNode.group([
   ReferenceGuidance.node,
   SessionTodo.node,
   SessionContextEntry.node,
+  Form.node,
   QuestionV2.node,
   Generate.node,
   ReadToolFileSystem.node,

+ 72 - 0
packages/opencode/test/server/httpapi-exercise/index.ts

@@ -816,6 +816,78 @@ const scenarios: Scenario[] = [
     object(body.location)
     array(body.data)
   }),
+  http.protected.get("/api/form/request", "v2.form.request.list").json(200, (body) => {
+    object(body)
+    object(body.location)
+    array(body.data)
+  }),
+  http.protected
+    .get("/api/session/{sessionID}/form", "v2.session.form.list")
+    .seeded((ctx) => ctx.session({ title: "Form list owner" }))
+    .at((ctx) => ({
+      path: route("/api/session/{sessionID}/form", { sessionID: ctx.state.id }),
+      headers: ctx.headers(),
+    }))
+    .json(200, data(array)),
+  http.protected
+    .post("/api/session/{sessionID}/form", "v2.session.form.create")
+    .mutating()
+    .seeded((ctx) => ctx.session({ title: "Form create owner" }))
+    .at((ctx) => ({
+      path: route("/api/session/{sessionID}/form", { sessionID: ctx.state.id }),
+      headers: ctx.headers(),
+      body: { mode: "url", url: "https://example.com/form" },
+    }))
+    .json(200, (body) => {
+      object(body)
+      object(body.data)
+      check(typeof body.data.id === "string", "form create should return an ID")
+      check(body.data.mode === "url", "form create should preserve URL mode")
+    }),
+  http.protected
+    .get("/api/session/{sessionID}/form/{formID}", "v2.session.form.get")
+    .seeded((ctx) => ctx.session({ title: "Form get owner" }))
+    .at((ctx) => ({
+      path: route("/api/session/{sessionID}/form/{formID}", { sessionID: ctx.state.id, formID: "frm_httpapi_missing" }),
+      headers: ctx.headers(),
+    }))
+    .json(404, object, "status"),
+  http.protected
+    .get("/api/session/{sessionID}/form/{formID}/state", "v2.session.form.state")
+    .seeded((ctx) => ctx.session({ title: "Form state owner" }))
+    .at((ctx) => ({
+      path: route("/api/session/{sessionID}/form/{formID}/state", {
+        sessionID: ctx.state.id,
+        formID: "frm_httpapi_missing",
+      }),
+      headers: ctx.headers(),
+    }))
+    .json(404, object, "status"),
+  http.protected
+    .post("/api/session/{sessionID}/form/{formID}/reply", "v2.session.form.reply")
+    .mutating()
+    .seeded((ctx) => ctx.session({ title: "Form reply owner" }))
+    .at((ctx) => ({
+      path: route("/api/session/{sessionID}/form/{formID}/reply", {
+        sessionID: ctx.state.id,
+        formID: "frm_httpapi_missing",
+      }),
+      headers: ctx.headers(),
+      body: { answer: {} },
+    }))
+    .json(404, object, "status"),
+  http.protected
+    .post("/api/session/{sessionID}/form/{formID}/cancel", "v2.session.form.cancel")
+    .mutating()
+    .seeded((ctx) => ctx.session({ title: "Form cancel owner" }))
+    .at((ctx) => ({
+      path: route("/api/session/{sessionID}/form/{formID}/cancel", {
+        sessionID: ctx.state.id,
+        formID: "frm_httpapi_missing",
+      }),
+      headers: ctx.headers(),
+    }))
+    .json(404, object, "status"),
   http.protected
     .post("/api/session/{sessionID}/permission", "v2.session.permission.create")
     .seeded((ctx) => ctx.session({ title: "Permission create owner" }))

+ 255 - 188
packages/plugin/src/v2/effect/generated/api.ts

@@ -458,71 +458,137 @@ export interface ProjectApi<E = never> {
   readonly directories: ProjectDirectoriesOperation<E>
 }
 
-type Endpoint13_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
+type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
 export type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
-export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>>
+export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.form"]["form.request.list"]>>
+export type FormListRequestsOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
+
+type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0]
+export type Endpoint13_1Input = { readonly sessionID: Endpoint13_1Request["params"]["sessionID"] }
+export type Endpoint13_1Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.list"]>>["data"]
+export type FormListOperation<E = never> = (input: Endpoint13_1Input) => Effect.Effect<Endpoint13_1Output, E>
+
+type Endpoint13_2Request = Parameters<RawClient["server.form"]["session.form.create"]>[0]
+export type Endpoint13_2Input = {
+  readonly sessionID: Endpoint13_2Request["params"]["sessionID"]
+  readonly id?: Endpoint13_2Request["payload"]["id"]
+  readonly title?: Endpoint13_2Request["payload"]["title"]
+  readonly metadata?: Endpoint13_2Request["payload"]["metadata"]
+  readonly mode: Endpoint13_2Request["payload"]["mode"]
+  readonly fields?: Endpoint13_2Request["payload"]["fields"]
+  readonly url?: Endpoint13_2Request["payload"]["url"]
+}
+export type Endpoint13_2Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.create"]>>["data"]
+export type FormCreateOperation<E = never> = (input: Endpoint13_2Input) => Effect.Effect<Endpoint13_2Output, E>
+
+type Endpoint13_3Request = Parameters<RawClient["server.form"]["session.form.get"]>[0]
+export type Endpoint13_3Input = {
+  readonly sessionID: Endpoint13_3Request["params"]["sessionID"]
+  readonly formID: Endpoint13_3Request["params"]["formID"]
+}
+export type Endpoint13_3Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.get"]>>["data"]
+export type FormGetOperation<E = never> = (input: Endpoint13_3Input) => Effect.Effect<Endpoint13_3Output, E>
+
+type Endpoint13_4Request = Parameters<RawClient["server.form"]["session.form.state"]>[0]
+export type Endpoint13_4Input = {
+  readonly sessionID: Endpoint13_4Request["params"]["sessionID"]
+  readonly formID: Endpoint13_4Request["params"]["formID"]
+}
+export type Endpoint13_4Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.state"]>>["data"]
+export type FormStateOperation<E = never> = (input: Endpoint13_4Input) => Effect.Effect<Endpoint13_4Output, E>
+
+type Endpoint13_5Request = Parameters<RawClient["server.form"]["session.form.reply"]>[0]
+export type Endpoint13_5Input = {
+  readonly sessionID: Endpoint13_5Request["params"]["sessionID"]
+  readonly formID: Endpoint13_5Request["params"]["formID"]
+  readonly answer: Endpoint13_5Request["payload"]["answer"]
+}
+export type Endpoint13_5Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.reply"]>>
+export type FormReplyOperation<E = never> = (input: Endpoint13_5Input) => Effect.Effect<Endpoint13_5Output, E>
+
+type Endpoint13_6Request = Parameters<RawClient["server.form"]["session.form.cancel"]>[0]
+export type Endpoint13_6Input = {
+  readonly sessionID: Endpoint13_6Request["params"]["sessionID"]
+  readonly formID: Endpoint13_6Request["params"]["formID"]
+}
+export type Endpoint13_6Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.cancel"]>>
+export type FormCancelOperation<E = never> = (input: Endpoint13_6Input) => Effect.Effect<Endpoint13_6Output, E>
+
+export interface FormApi<E = never> {
+  readonly listRequests: FormListRequestsOperation<E>
+  readonly list: FormListOperation<E>
+  readonly create: FormCreateOperation<E>
+  readonly get: FormGetOperation<E>
+  readonly state: FormStateOperation<E>
+  readonly reply: FormReplyOperation<E>
+  readonly cancel: FormCancelOperation<E>
+}
+
+type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
+export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
+export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>>
 export type PermissionListRequestsOperation<E = never> = (
-  input?: Endpoint13_0Input,
-) => Effect.Effect<Endpoint13_0Output, E>
+  input?: Endpoint14_0Input,
+) => Effect.Effect<Endpoint14_0Output, E>
 
-type Endpoint13_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
-export type Endpoint13_1Input = { readonly projectID?: Endpoint13_1Request["query"]["projectID"] }
-export type Endpoint13_1Output = EffectValue<
+type Endpoint14_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
+export type Endpoint14_1Input = { readonly projectID?: Endpoint14_1Request["query"]["projectID"] }
+export type Endpoint14_1Output = EffectValue<
   ReturnType<RawClient["server.permission"]["permission.saved.list"]>
 >["data"]
 export type PermissionListSavedOperation<E = never> = (
-  input?: Endpoint13_1Input,
-) => Effect.Effect<Endpoint13_1Output, E>
+  input?: Endpoint14_1Input,
+) => Effect.Effect<Endpoint14_1Output, E>
 
-type Endpoint13_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
-export type Endpoint13_2Input = { readonly id: Endpoint13_2Request["params"]["id"] }
-export type Endpoint13_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>>
+type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
+export type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] }
+export type Endpoint14_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>>
 export type PermissionRemoveSavedOperation<E = never> = (
-  input: Endpoint13_2Input,
-) => Effect.Effect<Endpoint13_2Output, E>
-
-type Endpoint13_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
-export type Endpoint13_3Input = {
-  readonly sessionID: Endpoint13_3Request["params"]["sessionID"]
-  readonly id?: Endpoint13_3Request["payload"]["id"]
-  readonly action: Endpoint13_3Request["payload"]["action"]
-  readonly resources: Endpoint13_3Request["payload"]["resources"]
-  readonly save?: Endpoint13_3Request["payload"]["save"]
-  readonly metadata?: Endpoint13_3Request["payload"]["metadata"]
-  readonly source?: Endpoint13_3Request["payload"]["source"]
-  readonly agent?: Endpoint13_3Request["payload"]["agent"]
-}
-export type Endpoint13_3Output = EffectValue<
+  input: Endpoint14_2Input,
+) => Effect.Effect<Endpoint14_2Output, E>
+
+type Endpoint14_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
+export type Endpoint14_3Input = {
+  readonly sessionID: Endpoint14_3Request["params"]["sessionID"]
+  readonly id?: Endpoint14_3Request["payload"]["id"]
+  readonly action: Endpoint14_3Request["payload"]["action"]
+  readonly resources: Endpoint14_3Request["payload"]["resources"]
+  readonly save?: Endpoint14_3Request["payload"]["save"]
+  readonly metadata?: Endpoint14_3Request["payload"]["metadata"]
+  readonly source?: Endpoint14_3Request["payload"]["source"]
+  readonly agent?: Endpoint14_3Request["payload"]["agent"]
+}
+export type Endpoint14_3Output = EffectValue<
   ReturnType<RawClient["server.permission"]["session.permission.create"]>
 >["data"]
-export type PermissionCreateOperation<E = never> = (input: Endpoint13_3Input) => Effect.Effect<Endpoint13_3Output, E>
+export type PermissionCreateOperation<E = never> = (input: Endpoint14_3Input) => Effect.Effect<Endpoint14_3Output, E>
 
-type Endpoint13_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
-export type Endpoint13_4Input = { readonly sessionID: Endpoint13_4Request["params"]["sessionID"] }
-export type Endpoint13_4Output = EffectValue<
+type Endpoint14_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
+export type Endpoint14_4Input = { readonly sessionID: Endpoint14_4Request["params"]["sessionID"] }
+export type Endpoint14_4Output = EffectValue<
   ReturnType<RawClient["server.permission"]["session.permission.list"]>
 >["data"]
-export type PermissionListOperation<E = never> = (input: Endpoint13_4Input) => Effect.Effect<Endpoint13_4Output, E>
+export type PermissionListOperation<E = never> = (input: Endpoint14_4Input) => Effect.Effect<Endpoint14_4Output, E>
 
-type Endpoint13_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
-export type Endpoint13_5Input = {
-  readonly sessionID: Endpoint13_5Request["params"]["sessionID"]
-  readonly requestID: Endpoint13_5Request["params"]["requestID"]
+type Endpoint14_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
+export type Endpoint14_5Input = {
+  readonly sessionID: Endpoint14_5Request["params"]["sessionID"]
+  readonly requestID: Endpoint14_5Request["params"]["requestID"]
 }
-export type Endpoint13_5Output = EffectValue<
+export type Endpoint14_5Output = EffectValue<
   ReturnType<RawClient["server.permission"]["session.permission.get"]>
 >["data"]
-export type PermissionGetOperation<E = never> = (input: Endpoint13_5Input) => Effect.Effect<Endpoint13_5Output, E>
+export type PermissionGetOperation<E = never> = (input: Endpoint14_5Input) => Effect.Effect<Endpoint14_5Output, E>
 
-type Endpoint13_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
-export type Endpoint13_6Input = {
-  readonly sessionID: Endpoint13_6Request["params"]["sessionID"]
-  readonly requestID: Endpoint13_6Request["params"]["requestID"]
-  readonly reply: Endpoint13_6Request["payload"]["reply"]
-  readonly message?: Endpoint13_6Request["payload"]["message"]
+type Endpoint14_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
+export type Endpoint14_6Input = {
+  readonly sessionID: Endpoint14_6Request["params"]["sessionID"]
+  readonly requestID: Endpoint14_6Request["params"]["requestID"]
+  readonly reply: Endpoint14_6Request["payload"]["reply"]
+  readonly message?: Endpoint14_6Request["payload"]["message"]
 }
-export type Endpoint13_6Output = EffectValue<ReturnType<RawClient["server.permission"]["session.permission.reply"]>>
-export type PermissionReplyOperation<E = never> = (input: Endpoint13_6Input) => Effect.Effect<Endpoint13_6Output, E>
+export type Endpoint14_6Output = EffectValue<ReturnType<RawClient["server.permission"]["session.permission.reply"]>>
+export type PermissionReplyOperation<E = never> = (input: Endpoint14_6Input) => Effect.Effect<Endpoint14_6Output, E>
 
 export interface PermissionApi<E = never> {
   readonly listRequests: PermissionListRequestsOperation<E>
@@ -534,100 +600,100 @@ export interface PermissionApi<E = never> {
   readonly reply: PermissionReplyOperation<E>
 }
 
-type Endpoint14_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
-export type Endpoint14_0Input = {
-  readonly location?: Endpoint14_0Request["query"]["location"]
-  readonly path?: Endpoint14_0Request["query"]["path"]
+type Endpoint15_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
+export type Endpoint15_0Input = {
+  readonly location?: Endpoint15_0Request["query"]["location"]
+  readonly path?: Endpoint15_0Request["query"]["path"]
 }
-export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.fs"]["fs.list"]>>
-export type FileListOperation<E = never> = (input?: Endpoint14_0Input) => Effect.Effect<Endpoint14_0Output, E>
+export type Endpoint15_0Output = EffectValue<ReturnType<RawClient["server.fs"]["fs.list"]>>
+export type FileListOperation<E = never> = (input?: Endpoint15_0Input) => Effect.Effect<Endpoint15_0Output, E>
 
-type Endpoint14_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
-export type Endpoint14_1Input = {
-  readonly location?: Endpoint14_1Request["query"]["location"]
-  readonly query: Endpoint14_1Request["query"]["query"]
-  readonly type?: Endpoint14_1Request["query"]["type"]
-  readonly limit?: Endpoint14_1Request["query"]["limit"]
+type Endpoint15_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
+export type Endpoint15_1Input = {
+  readonly location?: Endpoint15_1Request["query"]["location"]
+  readonly query: Endpoint15_1Request["query"]["query"]
+  readonly type?: Endpoint15_1Request["query"]["type"]
+  readonly limit?: Endpoint15_1Request["query"]["limit"]
 }
-export type Endpoint14_1Output = EffectValue<ReturnType<RawClient["server.fs"]["fs.find"]>>
-export type FileFindOperation<E = never> = (input: Endpoint14_1Input) => Effect.Effect<Endpoint14_1Output, E>
+export type Endpoint15_1Output = EffectValue<ReturnType<RawClient["server.fs"]["fs.find"]>>
+export type FileFindOperation<E = never> = (input: Endpoint15_1Input) => Effect.Effect<Endpoint15_1Output, E>
 
 export interface FileApi<E = never> {
   readonly list: FileListOperation<E>
   readonly find: FileFindOperation<E>
 }
 
-type Endpoint15_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
-export type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] }
-export type Endpoint15_0Output = EffectValue<ReturnType<RawClient["server.command"]["command.list"]>>
-export type CommandListOperation<E = never> = (input?: Endpoint15_0Input) => Effect.Effect<Endpoint15_0Output, E>
+type Endpoint16_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
+export type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
+export type Endpoint16_0Output = EffectValue<ReturnType<RawClient["server.command"]["command.list"]>>
+export type CommandListOperation<E = never> = (input?: Endpoint16_0Input) => Effect.Effect<Endpoint16_0Output, E>
 
 export interface CommandApi<E = never> {
   readonly list: CommandListOperation<E>
 }
 
-type Endpoint16_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
-export type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
-export type Endpoint16_0Output = EffectValue<ReturnType<RawClient["server.skill"]["skill.list"]>>
-export type SkillListOperation<E = never> = (input?: Endpoint16_0Input) => Effect.Effect<Endpoint16_0Output, E>
+type Endpoint17_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
+export type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] }
+export type Endpoint17_0Output = EffectValue<ReturnType<RawClient["server.skill"]["skill.list"]>>
+export type SkillListOperation<E = never> = (input?: Endpoint17_0Input) => Effect.Effect<Endpoint17_0Output, E>
 
 export interface SkillApi<E = never> {
   readonly list: SkillListOperation<E>
 }
 
-export type Endpoint17_0Output = StreamValue<EffectValue<ReturnType<RawClient["server.event"]["event.subscribe"]>>>
-export type EventSubscribeOperation<E = never> = () => Stream.Stream<Endpoint17_0Output, E>
+export type Endpoint18_0Output = StreamValue<EffectValue<ReturnType<RawClient["server.event"]["event.subscribe"]>>>
+export type EventSubscribeOperation<E = never> = () => Stream.Stream<Endpoint18_0Output, E>
 
-export type Endpoint17_1Output = StreamValue<EffectValue<ReturnType<RawClient["server.event"]["event.changes"]>>>
-export type EventChangesOperation<E = never> = () => Stream.Stream<Endpoint17_1Output, E>
+export type Endpoint18_1Output = StreamValue<EffectValue<ReturnType<RawClient["server.event"]["event.changes"]>>>
+export type EventChangesOperation<E = never> = () => Stream.Stream<Endpoint18_1Output, E>
 
 export interface EventApi<E = never> {
   readonly subscribe: EventSubscribeOperation<E>
   readonly changes: EventChangesOperation<E>
 }
 
-type Endpoint18_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
-export type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] }
-export type Endpoint18_0Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.list"]>>
-export type PtyListOperation<E = never> = (input?: Endpoint18_0Input) => Effect.Effect<Endpoint18_0Output, E>
+type Endpoint19_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
+export type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
+export type Endpoint19_0Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.list"]>>
+export type PtyListOperation<E = never> = (input?: Endpoint19_0Input) => Effect.Effect<Endpoint19_0Output, E>
 
-type Endpoint18_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
-export type Endpoint18_1Input = {
-  readonly location?: Endpoint18_1Request["query"]["location"]
-  readonly command?: Endpoint18_1Request["payload"]["command"]
-  readonly args?: Endpoint18_1Request["payload"]["args"]
-  readonly cwd?: Endpoint18_1Request["payload"]["cwd"]
-  readonly title?: Endpoint18_1Request["payload"]["title"]
-  readonly env?: Endpoint18_1Request["payload"]["env"]
+type Endpoint19_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
+export type Endpoint19_1Input = {
+  readonly location?: Endpoint19_1Request["query"]["location"]
+  readonly command?: Endpoint19_1Request["payload"]["command"]
+  readonly args?: Endpoint19_1Request["payload"]["args"]
+  readonly cwd?: Endpoint19_1Request["payload"]["cwd"]
+  readonly title?: Endpoint19_1Request["payload"]["title"]
+  readonly env?: Endpoint19_1Request["payload"]["env"]
 }
-export type Endpoint18_1Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.create"]>>
-export type PtyCreateOperation<E = never> = (input?: Endpoint18_1Input) => Effect.Effect<Endpoint18_1Output, E>
+export type Endpoint19_1Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.create"]>>
+export type PtyCreateOperation<E = never> = (input?: Endpoint19_1Input) => Effect.Effect<Endpoint19_1Output, E>
 
-type Endpoint18_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
-export type Endpoint18_2Input = {
-  readonly ptyID: Endpoint18_2Request["params"]["ptyID"]
-  readonly location?: Endpoint18_2Request["query"]["location"]
+type Endpoint19_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
+export type Endpoint19_2Input = {
+  readonly ptyID: Endpoint19_2Request["params"]["ptyID"]
+  readonly location?: Endpoint19_2Request["query"]["location"]
 }
-export type Endpoint18_2Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.get"]>>
-export type PtyGetOperation<E = never> = (input: Endpoint18_2Input) => Effect.Effect<Endpoint18_2Output, E>
+export type Endpoint19_2Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.get"]>>
+export type PtyGetOperation<E = never> = (input: Endpoint19_2Input) => Effect.Effect<Endpoint19_2Output, E>
 
-type Endpoint18_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
-export type Endpoint18_3Input = {
-  readonly ptyID: Endpoint18_3Request["params"]["ptyID"]
-  readonly location?: Endpoint18_3Request["query"]["location"]
-  readonly title?: Endpoint18_3Request["payload"]["title"]
-  readonly size?: Endpoint18_3Request["payload"]["size"]
+type Endpoint19_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
+export type Endpoint19_3Input = {
+  readonly ptyID: Endpoint19_3Request["params"]["ptyID"]
+  readonly location?: Endpoint19_3Request["query"]["location"]
+  readonly title?: Endpoint19_3Request["payload"]["title"]
+  readonly size?: Endpoint19_3Request["payload"]["size"]
 }
-export type Endpoint18_3Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.update"]>>
-export type PtyUpdateOperation<E = never> = (input: Endpoint18_3Input) => Effect.Effect<Endpoint18_3Output, E>
+export type Endpoint19_3Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.update"]>>
+export type PtyUpdateOperation<E = never> = (input: Endpoint19_3Input) => Effect.Effect<Endpoint19_3Output, E>
 
-type Endpoint18_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
-export type Endpoint18_4Input = {
-  readonly ptyID: Endpoint18_4Request["params"]["ptyID"]
-  readonly location?: Endpoint18_4Request["query"]["location"]
+type Endpoint19_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
+export type Endpoint19_4Input = {
+  readonly ptyID: Endpoint19_4Request["params"]["ptyID"]
+  readonly location?: Endpoint19_4Request["query"]["location"]
 }
-export type Endpoint18_4Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.remove"]>>
-export type PtyRemoveOperation<E = never> = (input: Endpoint18_4Input) => Effect.Effect<Endpoint18_4Output, E>
+export type Endpoint19_4Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.remove"]>>
+export type PtyRemoveOperation<E = never> = (input: Endpoint19_4Input) => Effect.Effect<Endpoint19_4Output, E>
 
 export interface PtyApi<E = never> {
   readonly list: PtyListOperation<E>
@@ -637,47 +703,47 @@ export interface PtyApi<E = never> {
   readonly remove: PtyRemoveOperation<E>
 }
 
-type Endpoint19_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
-export type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
-export type Endpoint19_0Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.list"]>>
-export type ShellListOperation<E = never> = (input?: Endpoint19_0Input) => Effect.Effect<Endpoint19_0Output, E>
+type Endpoint20_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
+export type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
+export type Endpoint20_0Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.list"]>>
+export type ShellListOperation<E = never> = (input?: Endpoint20_0Input) => Effect.Effect<Endpoint20_0Output, E>
 
-type Endpoint19_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
-export type Endpoint19_1Input = {
-  readonly location?: Endpoint19_1Request["query"]["location"]
-  readonly command: Endpoint19_1Request["payload"]["command"]
-  readonly cwd?: Endpoint19_1Request["payload"]["cwd"]
-  readonly timeout?: Endpoint19_1Request["payload"]["timeout"]
-  readonly metadata?: Endpoint19_1Request["payload"]["metadata"]
+type Endpoint20_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
+export type Endpoint20_1Input = {
+  readonly location?: Endpoint20_1Request["query"]["location"]
+  readonly command: Endpoint20_1Request["payload"]["command"]
+  readonly cwd?: Endpoint20_1Request["payload"]["cwd"]
+  readonly timeout?: Endpoint20_1Request["payload"]["timeout"]
+  readonly metadata?: Endpoint20_1Request["payload"]["metadata"]
 }
-export type Endpoint19_1Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.create"]>>
-export type ShellCreateOperation<E = never> = (input: Endpoint19_1Input) => Effect.Effect<Endpoint19_1Output, E>
+export type Endpoint20_1Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.create"]>>
+export type ShellCreateOperation<E = never> = (input: Endpoint20_1Input) => Effect.Effect<Endpoint20_1Output, E>
 
-type Endpoint19_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
-export type Endpoint19_2Input = {
-  readonly id: Endpoint19_2Request["params"]["id"]
-  readonly location?: Endpoint19_2Request["query"]["location"]
+type Endpoint20_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
+export type Endpoint20_2Input = {
+  readonly id: Endpoint20_2Request["params"]["id"]
+  readonly location?: Endpoint20_2Request["query"]["location"]
 }
-export type Endpoint19_2Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.get"]>>
-export type ShellGetOperation<E = never> = (input: Endpoint19_2Input) => Effect.Effect<Endpoint19_2Output, E>
+export type Endpoint20_2Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.get"]>>
+export type ShellGetOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E>
 
-type Endpoint19_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
-export type Endpoint19_3Input = {
-  readonly id: Endpoint19_3Request["params"]["id"]
-  readonly location?: Endpoint19_3Request["query"]["location"]
-  readonly cursor?: Endpoint19_3Request["query"]["cursor"]
-  readonly limit?: Endpoint19_3Request["query"]["limit"]
+type Endpoint20_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
+export type Endpoint20_3Input = {
+  readonly id: Endpoint20_3Request["params"]["id"]
+  readonly location?: Endpoint20_3Request["query"]["location"]
+  readonly cursor?: Endpoint20_3Request["query"]["cursor"]
+  readonly limit?: Endpoint20_3Request["query"]["limit"]
 }
-export type Endpoint19_3Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.output"]>>
-export type ShellOutputOperation<E = never> = (input: Endpoint19_3Input) => Effect.Effect<Endpoint19_3Output, E>
+export type Endpoint20_3Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.output"]>>
+export type ShellOutputOperation<E = never> = (input: Endpoint20_3Input) => Effect.Effect<Endpoint20_3Output, E>
 
-type Endpoint19_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
-export type Endpoint19_4Input = {
-  readonly id: Endpoint19_4Request["params"]["id"]
-  readonly location?: Endpoint19_4Request["query"]["location"]
+type Endpoint20_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
+export type Endpoint20_4Input = {
+  readonly id: Endpoint20_4Request["params"]["id"]
+  readonly location?: Endpoint20_4Request["query"]["location"]
 }
-export type Endpoint19_4Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.remove"]>>
-export type ShellRemoveOperation<E = never> = (input: Endpoint19_4Input) => Effect.Effect<Endpoint19_4Output, E>
+export type Endpoint20_4Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.remove"]>>
+export type ShellRemoveOperation<E = never> = (input: Endpoint20_4Input) => Effect.Effect<Endpoint20_4Output, E>
 
 export interface ShellApi<E = never> {
   readonly list: ShellListOperation<E>
@@ -687,34 +753,34 @@ export interface ShellApi<E = never> {
   readonly remove: ShellRemoveOperation<E>
 }
 
-type Endpoint20_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
-export type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
-export type Endpoint20_0Output = EffectValue<ReturnType<RawClient["server.question"]["question.request.list"]>>
+type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
+export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
+export type Endpoint21_0Output = EffectValue<ReturnType<RawClient["server.question"]["question.request.list"]>>
 export type QuestionListRequestsOperation<E = never> = (
-  input?: Endpoint20_0Input,
-) => Effect.Effect<Endpoint20_0Output, E>
+  input?: Endpoint21_0Input,
+) => Effect.Effect<Endpoint21_0Output, E>
 
-type Endpoint20_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
-export type Endpoint20_1Input = { readonly sessionID: Endpoint20_1Request["params"]["sessionID"] }
-export type Endpoint20_1Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.list"]>>["data"]
-export type QuestionListOperation<E = never> = (input: Endpoint20_1Input) => Effect.Effect<Endpoint20_1Output, E>
+type Endpoint21_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
+export type Endpoint21_1Input = { readonly sessionID: Endpoint21_1Request["params"]["sessionID"] }
+export type Endpoint21_1Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.list"]>>["data"]
+export type QuestionListOperation<E = never> = (input: Endpoint21_1Input) => Effect.Effect<Endpoint21_1Output, E>
 
-type Endpoint20_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
-export type Endpoint20_2Input = {
-  readonly sessionID: Endpoint20_2Request["params"]["sessionID"]
-  readonly requestID: Endpoint20_2Request["params"]["requestID"]
-  readonly answers: Endpoint20_2Request["payload"]["answers"]
+type Endpoint21_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
+export type Endpoint21_2Input = {
+  readonly sessionID: Endpoint21_2Request["params"]["sessionID"]
+  readonly requestID: Endpoint21_2Request["params"]["requestID"]
+  readonly answers: Endpoint21_2Request["payload"]["answers"]
 }
-export type Endpoint20_2Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.reply"]>>
-export type QuestionReplyOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E>
+export type Endpoint21_2Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.reply"]>>
+export type QuestionReplyOperation<E = never> = (input: Endpoint21_2Input) => Effect.Effect<Endpoint21_2Output, E>
 
-type Endpoint20_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
-export type Endpoint20_3Input = {
-  readonly sessionID: Endpoint20_3Request["params"]["sessionID"]
-  readonly requestID: Endpoint20_3Request["params"]["requestID"]
+type Endpoint21_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
+export type Endpoint21_3Input = {
+  readonly sessionID: Endpoint21_3Request["params"]["sessionID"]
+  readonly requestID: Endpoint21_3Request["params"]["requestID"]
 }
-export type Endpoint20_3Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.reject"]>>
-export type QuestionRejectOperation<E = never> = (input: Endpoint20_3Input) => Effect.Effect<Endpoint20_3Output, E>
+export type Endpoint21_3Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.reject"]>>
+export type QuestionRejectOperation<E = never> = (input: Endpoint21_3Input) => Effect.Effect<Endpoint21_3Output, E>
 
 export interface QuestionApi<E = never> {
   readonly listRequests: QuestionListRequestsOperation<E>
@@ -723,43 +789,43 @@ export interface QuestionApi<E = never> {
   readonly reject: QuestionRejectOperation<E>
 }
 
-type Endpoint21_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
-export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
-export type Endpoint21_0Output = EffectValue<ReturnType<RawClient["server.reference"]["reference.list"]>>
-export type ReferenceListOperation<E = never> = (input?: Endpoint21_0Input) => Effect.Effect<Endpoint21_0Output, E>
+type Endpoint22_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
+export type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] }
+export type Endpoint22_0Output = EffectValue<ReturnType<RawClient["server.reference"]["reference.list"]>>
+export type ReferenceListOperation<E = never> = (input?: Endpoint22_0Input) => Effect.Effect<Endpoint22_0Output, E>
 
 export interface ReferenceApi<E = never> {
   readonly list: ReferenceListOperation<E>
 }
 
-type Endpoint22_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
-export type Endpoint22_0Input = {
-  readonly projectID: Endpoint22_0Request["params"]["projectID"]
-  readonly location?: Endpoint22_0Request["query"]["location"]
-  readonly strategy: Endpoint22_0Request["payload"]["strategy"]
-  readonly directory: Endpoint22_0Request["payload"]["directory"]
-  readonly name?: Endpoint22_0Request["payload"]["name"]
+type Endpoint23_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
+export type Endpoint23_0Input = {
+  readonly projectID: Endpoint23_0Request["params"]["projectID"]
+  readonly location?: Endpoint23_0Request["query"]["location"]
+  readonly strategy: Endpoint23_0Request["payload"]["strategy"]
+  readonly directory: Endpoint23_0Request["payload"]["directory"]
+  readonly name?: Endpoint23_0Request["payload"]["name"]
 }
-export type Endpoint22_0Output = EffectValue<ReturnType<RawClient["server.projectCopy"]["projectCopy.create"]>>
-export type ProjectCopyCreateOperation<E = never> = (input: Endpoint22_0Input) => Effect.Effect<Endpoint22_0Output, E>
+export type Endpoint23_0Output = EffectValue<ReturnType<RawClient["server.projectCopy"]["projectCopy.create"]>>
+export type ProjectCopyCreateOperation<E = never> = (input: Endpoint23_0Input) => Effect.Effect<Endpoint23_0Output, E>
 
-type Endpoint22_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
-export type Endpoint22_1Input = {
-  readonly projectID: Endpoint22_1Request["params"]["projectID"]
-  readonly location?: Endpoint22_1Request["query"]["location"]
-  readonly directory: Endpoint22_1Request["payload"]["directory"]
-  readonly force: Endpoint22_1Request["payload"]["force"]
+type Endpoint23_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
+export type Endpoint23_1Input = {
+  readonly projectID: Endpoint23_1Request["params"]["projectID"]
+  readonly location?: Endpoint23_1Request["query"]["location"]
+  readonly directory: Endpoint23_1Request["payload"]["directory"]
+  readonly force: Endpoint23_1Request["payload"]["force"]
 }
-export type Endpoint22_1Output = EffectValue<ReturnType<RawClient["server.projectCopy"]["projectCopy.remove"]>>
-export type ProjectCopyRemoveOperation<E = never> = (input: Endpoint22_1Input) => Effect.Effect<Endpoint22_1Output, E>
+export type Endpoint23_1Output = EffectValue<ReturnType<RawClient["server.projectCopy"]["projectCopy.remove"]>>
+export type ProjectCopyRemoveOperation<E = never> = (input: Endpoint23_1Input) => Effect.Effect<Endpoint23_1Output, E>
 
-type Endpoint22_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
-export type Endpoint22_2Input = {
-  readonly projectID: Endpoint22_2Request["params"]["projectID"]
-  readonly location?: Endpoint22_2Request["query"]["location"]
+type Endpoint23_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
+export type Endpoint23_2Input = {
+  readonly projectID: Endpoint23_2Request["params"]["projectID"]
+  readonly location?: Endpoint23_2Request["query"]["location"]
 }
-export type Endpoint22_2Output = EffectValue<ReturnType<RawClient["server.projectCopy"]["projectCopy.refresh"]>>
-export type ProjectCopyRefreshOperation<E = never> = (input: Endpoint22_2Input) => Effect.Effect<Endpoint22_2Output, E>
+export type Endpoint23_2Output = EffectValue<ReturnType<RawClient["server.projectCopy"]["projectCopy.refresh"]>>
+export type ProjectCopyRefreshOperation<E = never> = (input: Endpoint23_2Input) => Effect.Effect<Endpoint23_2Output, E>
 
 export interface ProjectCopyApi<E = never> {
   readonly create: ProjectCopyCreateOperation<E>
@@ -781,6 +847,7 @@ export interface AppApi<E = never> {
   readonly "server.mcp": ServerMcpApi<E>
   readonly credential: CredentialApi<E>
   readonly project: ProjectApi<E>
+  readonly form: FormApi<E>
   readonly permission: PermissionApi<E>
   readonly file: FileApi<E>
   readonly command: CommandApi<E>

+ 3 - 0
packages/protocol/src/api.ts

@@ -8,6 +8,7 @@ import { ProviderGroup } from "./groups/provider.js"
 import { makeSessionGroup } from "./groups/session.js"
 import { makePermissionGroup } from "./groups/permission.js"
 import { FileSystemGroup } from "./groups/fs.js"
+import { makeFormGroup } from "./groups/form.js"
 import { CommandGroup } from "./groups/command.js"
 import { SkillGroup } from "./groups/skill.js"
 import { EventGroup, makeEventGroup } from "./groups/event.js"
@@ -59,6 +60,7 @@ type MixedMiddlewareGroups<
   | ReturnType<
       typeof makePermissionGroup<LocationId, LocationService, SessionLocationId, SessionLocationService>
     >
+  | ReturnType<typeof makeFormGroup<LocationId, LocationService, SessionLocationId, SessionLocationService>>
   | ReturnType<typeof makeQuestionGroup<LocationId, LocationService, SessionLocationId, SessionLocationService>>
 
 type ApiGroups<
@@ -119,6 +121,7 @@ const makeApiFromGroup = <
     .add(McpGroup.middleware(locationMiddleware))
     .add(CredentialGroup.middleware(locationMiddleware))
     .add(ProjectGroup.middleware(locationMiddleware))
+    .add(makeFormGroup(locationMiddleware, sessionLocationMiddleware))
     .add(makePermissionGroup(locationMiddleware, sessionLocationMiddleware))
     .add(FileSystemGroup.middleware(locationMiddleware))
     .add(CommandGroup.middleware(locationMiddleware))

+ 2 - 0
packages/protocol/src/client.ts

@@ -39,6 +39,7 @@ export const groupNames = {
   "server.provider": "provider",
   "server.integration": "integration",
   "server.credential": "credential",
+  "server.form": "form",
   "server.permission": "permission",
   "server.fs": "file",
   "server.command": "command",
@@ -68,6 +69,7 @@ export const endpointNames = {
   "permission.request.list": "listRequests",
   "permission.saved.list": "listSaved",
   "permission.saved.remove": "removeSaved",
+  "form.request.list": "listRequests",
   "question.request.list": "listRequests",
 } as const
 

+ 27 - 0
packages/protocol/src/errors.ts

@@ -131,6 +131,33 @@ export class QuestionNotFoundError extends Schema.TaggedErrorClass<QuestionNotFo
   { httpApiStatus: 404 },
 ) {}
 
+export class FormNotFoundError extends Schema.TaggedErrorClass<FormNotFoundError>()(
+  "FormNotFoundError",
+  {
+    id: Schema.String,
+    message: Schema.String,
+  },
+  { httpApiStatus: 404 },
+) {}
+
+export class FormAlreadySettledError extends Schema.TaggedErrorClass<FormAlreadySettledError>()(
+  "FormAlreadySettledError",
+  {
+    id: Schema.String,
+    message: Schema.String,
+  },
+  { httpApiStatus: 409 },
+) {}
+
+export class FormInvalidAnswerError extends Schema.TaggedErrorClass<FormInvalidAnswerError>()(
+  "FormInvalidAnswerError",
+  {
+    id: Schema.String,
+    message: Schema.String,
+  },
+  { httpApiStatus: 400 },
+) {}
+
 export class ForbiddenError extends Schema.TaggedErrorClass<ForbiddenError>()(
   "ForbiddenError",
   { message: Schema.String },

+ 144 - 0
packages/protocol/src/groups/form.ts

@@ -0,0 +1,144 @@
+import { Form } from "@opencode-ai/schema/form"
+import { Location } from "@opencode-ai/schema/location"
+import { Session } from "@opencode-ai/schema/session"
+import { Context, Schema } from "effect"
+import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
+import {
+  ConflictError,
+  FormAlreadySettledError,
+  FormInvalidAnswerError,
+  FormNotFoundError,
+  InvalidRequestError,
+  SessionNotFoundError,
+} from "../errors.js"
+import { LocationQuery, locationQueryOpenApi } from "./location.js"
+
+const CreatePayload = Schema.Struct({
+  id: Form.ID.pipe(Schema.optional),
+  title: Form.FormInfo.fields.title,
+  metadata: Form.FormInfo.fields.metadata,
+  mode: Schema.Literals(["form", "url"]),
+  fields: Form.FormInfo.fields.fields.pipe(Schema.optional),
+  url: Form.UrlInfo.fields.url.pipe(Schema.optional),
+}).annotate({ identifier: "Form.CreatePayload" })
+
+export type CreatePayload = typeof CreatePayload.Type
+
+export const makeFormGroup = <
+  LocationId extends HttpApiMiddleware.AnyId,
+  LocationService,
+  SessionLocationId extends HttpApiMiddleware.AnyId,
+  SessionLocationService,
+>(
+  locationMiddleware: Context.Key<LocationId, LocationService>,
+  sessionLocationMiddleware: Context.Key<SessionLocationId, SessionLocationService>,
+) =>
+  HttpApiGroup.make("server.form")
+    .add(
+      HttpApiEndpoint.get("form.request.list", "/api/form/request", {
+        query: LocationQuery,
+        success: Location.response(Schema.Array(Form.Info)),
+      })
+        .annotateMerge(locationQueryOpenApi)
+        .annotateMerge(
+          OpenApi.annotations({
+            identifier: "v2.form.request.list",
+            summary: "List pending form requests",
+            description: "Retrieve pending forms for a location.",
+          }),
+        ),
+    )
+    .middleware(locationMiddleware)
+    .add(
+      HttpApiEndpoint.get("session.form.list", "/api/session/:sessionID/form", {
+        params: { sessionID: Session.ID },
+        success: Schema.Struct({ data: Schema.Array(Form.Info) }),
+        error: SessionNotFoundError,
+      })
+        .middleware(sessionLocationMiddleware)
+        .annotateMerge(
+          OpenApi.annotations({
+            identifier: "v2.session.form.list",
+            summary: "List session forms",
+            description: "Retrieve pending forms for a session.",
+          }),
+        ),
+    )
+    .add(
+      HttpApiEndpoint.post("session.form.create", "/api/session/:sessionID/form", {
+        params: { sessionID: Session.ID },
+        payload: CreatePayload,
+        success: Schema.Struct({ data: Form.Info }),
+        error: [SessionNotFoundError, ConflictError, InvalidRequestError],
+      })
+        .middleware(sessionLocationMiddleware)
+        .annotateMerge(
+          OpenApi.annotations({
+            identifier: "v2.session.form.create",
+            summary: "Create session form",
+            description: "Create a form for a session.",
+          }),
+        ),
+    )
+    .add(
+      HttpApiEndpoint.get("session.form.get", "/api/session/:sessionID/form/:formID", {
+        params: { sessionID: Session.ID, formID: Form.ID },
+        success: Schema.Struct({ data: Form.Info }),
+        error: [SessionNotFoundError, FormNotFoundError],
+      })
+        .middleware(sessionLocationMiddleware)
+        .annotateMerge(
+          OpenApi.annotations({
+            identifier: "v2.session.form.get",
+            summary: "Get session form",
+            description: "Retrieve a form for a session.",
+          }),
+        ),
+    )
+    .add(
+      HttpApiEndpoint.get("session.form.state", "/api/session/:sessionID/form/:formID/state", {
+        params: { sessionID: Session.ID, formID: Form.ID },
+        success: Schema.Struct({ data: Form.State }),
+        error: [SessionNotFoundError, FormNotFoundError],
+      })
+        .middleware(sessionLocationMiddleware)
+        .annotateMerge(
+          OpenApi.annotations({
+            identifier: "v2.session.form.state",
+            summary: "Get form state",
+            description: "Retrieve the current state for a form.",
+          }),
+        ),
+    )
+    .add(
+      HttpApiEndpoint.post("session.form.reply", "/api/session/:sessionID/form/:formID/reply", {
+        params: { sessionID: Session.ID, formID: Form.ID },
+        payload: Form.Reply,
+        success: HttpApiSchema.NoContent,
+        error: [SessionNotFoundError, FormAlreadySettledError, FormInvalidAnswerError, FormNotFoundError],
+      })
+        .middleware(sessionLocationMiddleware)
+        .annotateMerge(
+          OpenApi.annotations({
+            identifier: "v2.session.form.reply",
+            summary: "Reply to form",
+            description: "Submit an answer to a pending form.",
+          }),
+        ),
+    )
+    .add(
+      HttpApiEndpoint.post("session.form.cancel", "/api/session/:sessionID/form/:formID/cancel", {
+        params: { sessionID: Session.ID, formID: Form.ID },
+        success: HttpApiSchema.NoContent,
+        error: [SessionNotFoundError, FormAlreadySettledError, FormNotFoundError],
+      })
+        .middleware(sessionLocationMiddleware)
+        .annotateMerge(
+          OpenApi.annotations({
+            identifier: "v2.session.form.cancel",
+            summary: "Cancel form",
+            description: "Cancel a pending form.",
+          }),
+        ),
+    )
+    .annotateMerge(OpenApi.annotations({ title: "forms", description: "Session form routes." }))

+ 251 - 7
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -76,6 +76,8 @@ import type {
   FindTextResponses,
   FormatterStatusErrors,
   FormatterStatusResponses,
+  FormCreatePayload2,
+  FormReply2,
   GlobalConfigGetErrors,
   GlobalConfigGetResponses,
   GlobalConfigUpdateErrors,
@@ -278,6 +280,8 @@ import type {
   V2EventChangesResponses,
   V2EventSubscribeErrors,
   V2EventSubscribeResponses,
+  V2FormRequestListErrors,
+  V2FormRequestListResponses,
   V2FsFindErrors,
   V2FsFindResponses,
   V2FsListErrors,
@@ -370,6 +374,18 @@ import type {
   V2SessionCreateResponses,
   V2SessionForkErrors,
   V2SessionForkResponses,
+  V2SessionFormCancelErrors,
+  V2SessionFormCancelResponses,
+  V2SessionFormCreateErrors,
+  V2SessionFormCreateResponses,
+  V2SessionFormGetErrors,
+  V2SessionFormGetResponses,
+  V2SessionFormListErrors,
+  V2SessionFormListResponses,
+  V2SessionFormReplyErrors,
+  V2SessionFormReplyResponses,
+  V2SessionFormStateErrors,
+  V2SessionFormStateResponses,
   V2SessionGetErrors,
   V2SessionGetResponses,
   V2SessionInterruptErrors,
@@ -5342,6 +5358,193 @@ export class Context extends HeyApiClient {
   }
 }
 
+export class Form extends HeyApiClient {
+  /**
+   * List session forms
+   *
+   * Retrieve pending forms for a session.
+   */
+  public list<ThrowOnError extends boolean = false>(
+    parameters: {
+      sessionID: string
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }])
+    return (options?.client ?? this.client).get<V2SessionFormListResponses, V2SessionFormListErrors, ThrowOnError>({
+      url: "/api/session/{sessionID}/form",
+      ...options,
+      ...params,
+    })
+  }
+
+  /**
+   * Create session form
+   *
+   * Create a form for a session.
+   */
+  public create<ThrowOnError extends boolean = false>(
+    parameters: {
+      sessionID: string
+      formCreatePayload: FormCreatePayload2
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "path", key: "sessionID" },
+            { key: "formCreatePayload", map: "body" },
+          ],
+        },
+      ],
+    )
+    return (options?.client ?? this.client).post<V2SessionFormCreateResponses, V2SessionFormCreateErrors, ThrowOnError>(
+      {
+        url: "/api/session/{sessionID}/form",
+        ...options,
+        ...params,
+        headers: {
+          "Content-Type": "application/json",
+          ...options?.headers,
+          ...params.headers,
+        },
+      },
+    )
+  }
+
+  /**
+   * Get session form
+   *
+   * Retrieve a form for a session.
+   */
+  public get<ThrowOnError extends boolean = false>(
+    parameters: {
+      sessionID: string
+      formID: string
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "path", key: "sessionID" },
+            { in: "path", key: "formID" },
+          ],
+        },
+      ],
+    )
+    return (options?.client ?? this.client).get<V2SessionFormGetResponses, V2SessionFormGetErrors, ThrowOnError>({
+      url: "/api/session/{sessionID}/form/{formID}",
+      ...options,
+      ...params,
+    })
+  }
+
+  /**
+   * Get form state
+   *
+   * Retrieve the current state for a form.
+   */
+  public state<ThrowOnError extends boolean = false>(
+    parameters: {
+      sessionID: string
+      formID: string
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "path", key: "sessionID" },
+            { in: "path", key: "formID" },
+          ],
+        },
+      ],
+    )
+    return (options?.client ?? this.client).get<V2SessionFormStateResponses, V2SessionFormStateErrors, ThrowOnError>({
+      url: "/api/session/{sessionID}/form/{formID}/state",
+      ...options,
+      ...params,
+    })
+  }
+
+  /**
+   * Reply to form
+   *
+   * Submit an answer to a pending form.
+   */
+  public reply<ThrowOnError extends boolean = false>(
+    parameters: {
+      sessionID: string
+      formID: string
+      formReply: FormReply2
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "path", key: "sessionID" },
+            { in: "path", key: "formID" },
+            { key: "formReply", map: "body" },
+          ],
+        },
+      ],
+    )
+    return (options?.client ?? this.client).post<V2SessionFormReplyResponses, V2SessionFormReplyErrors, ThrowOnError>({
+      url: "/api/session/{sessionID}/form/{formID}/reply",
+      ...options,
+      ...params,
+      headers: {
+        "Content-Type": "application/json",
+        ...options?.headers,
+        ...params.headers,
+      },
+    })
+  }
+
+  /**
+   * Cancel form
+   *
+   * Cancel a pending form.
+   */
+  public cancel<ThrowOnError extends boolean = false>(
+    parameters: {
+      sessionID: string
+      formID: string
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "path", key: "sessionID" },
+            { in: "path", key: "formID" },
+          ],
+        },
+      ],
+    )
+    return (options?.client ?? this.client).post<V2SessionFormCancelResponses, V2SessionFormCancelErrors, ThrowOnError>(
+      {
+        url: "/api/session/{sessionID}/form/{formID}/cancel",
+        ...options,
+        ...params,
+      },
+    )
+  }
+}
+
 export class Permission2 extends HeyApiClient {
   /**
    * List session permission requests
@@ -6235,6 +6438,11 @@ export class Session3 extends HeyApiClient {
     return (this._context ??= new Context({ client: this.client }))
   }
 
+  private _form?: Form
+  get form(): Form {
+    return (this._form ??= new Form({ client: this.client }))
+  }
+
   private _permission?: Permission2
   get permission(): Permission2 {
     return (this._permission ??= new Permission2({ client: this.client }))
@@ -6839,6 +7047,37 @@ export class Project2 extends HeyApiClient {
 }
 
 export class Request extends HeyApiClient {
+  /**
+   * List pending form requests
+   *
+   * Retrieve pending forms for a location.
+   */
+  public list<ThrowOnError extends boolean = false>(
+    parameters?: {
+      location?: {
+        directory?: string | null
+        workspace?: string | null
+      } | null
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }])
+    return (options?.client ?? this.client).get<V2FormRequestListResponses, V2FormRequestListErrors, ThrowOnError>({
+      url: "/api/form/request",
+      ...options,
+      ...params,
+    })
+  }
+}
+
+export class Form2 extends HeyApiClient {
+  private _request?: Request
+  get request(): Request {
+    return (this._request ??= new Request({ client: this.client }))
+  }
+}
+
+export class Request2 extends HeyApiClient {
   /**
    * List pending permission requests
    *
@@ -6915,9 +7154,9 @@ export class Saved extends HeyApiClient {
 }
 
 export class Permission3 extends HeyApiClient {
-  private _request?: Request
-  get request(): Request {
-    return (this._request ??= new Request({ client: this.client }))
+  private _request?: Request2
+  get request(): Request2 {
+    return (this._request ??= new Request2({ client: this.client }))
   }
 
   private _saved?: Saved
@@ -7519,7 +7758,7 @@ export class Shell extends HeyApiClient {
   }
 }
 
-export class Request2 extends HeyApiClient {
+export class Request3 extends HeyApiClient {
   /**
    * List pending question requests
    *
@@ -7548,9 +7787,9 @@ export class Request2 extends HeyApiClient {
 }
 
 export class Question3 extends HeyApiClient {
-  private _request?: Request2
-  get request(): Request2 {
-    return (this._request ??= new Request2({ client: this.client }))
+  private _request?: Request3
+  get request(): Request3 {
+    return (this._request ??= new Request3({ client: this.client }))
   }
 }
 
@@ -7755,6 +7994,11 @@ export class V2 extends HeyApiClient {
     return (this._project ??= new Project2({ client: this.client }))
   }
 
+  private _form?: Form2
+  get form(): Form2 {
+    return (this._form ??= new Form2({ client: this.client }))
+  }
+
   private _permission?: Permission3
   get permission(): Permission3 {
     return (this._permission ??= new Permission3({ client: this.client }))

+ 904 - 45
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -77,6 +77,9 @@ export type Event =
   | EventQuestionV2Asked
   | EventQuestionV2Replied
   | EventQuestionV2Rejected
+  | EventFormCreated
+  | EventFormReplied
+  | EventFormCancelled
   | EventTodoUpdated
   | EventLspUpdated
   | EventPermissionAsked
@@ -1475,6 +1478,30 @@ export type GlobalEvent = {
           requestID: string
         }
       }
+    | {
+        id: string
+        type: "form.created"
+        properties: {
+          form: FormFormInfo | FormUrlInfo
+        }
+      }
+    | {
+        id: string
+        type: "form.replied"
+        properties: {
+          id: string
+          sessionID: string
+          answer: FormAnswer
+        }
+      }
+    | {
+        id: string
+        type: "form.cancelled"
+        properties: {
+          id: string
+          sessionID: string
+        }
+      }
     | {
         id: string
         type: "todo.updated"
@@ -2941,6 +2968,24 @@ export type ProviderNotFoundError = {
   message: string
 }
 
+export type FormNotFoundError = {
+  _tag: "FormNotFoundError"
+  id: string
+  message: string
+}
+
+export type FormAlreadySettledError = {
+  _tag: "FormAlreadySettledError"
+  id: string
+  message: string
+}
+
+export type FormInvalidAnswerError = {
+  _tag: "FormInvalidAnswerError"
+  id: string
+  message: string
+}
+
 export type OutputFormat1 =
   | {
       type: "text"
@@ -3097,6 +3142,9 @@ export type V2Event =
   | QuestionV2Asked
   | QuestionV2Replied
   | QuestionV2Rejected
+  | FormCreated
+  | FormReplied
+  | FormCancelled
   | TodoUpdated
   | LspUpdated
   | PermissionAsked
@@ -3370,6 +3418,121 @@ export type QuestionV2Tool = {
 
 export type QuestionV2Answer = Array<string>
 
+export type FormMetadata = {
+  [key: string]: unknown
+}
+
+export type FormWhen = {
+  key: string
+  op: "eq" | "neq"
+  value: string
+}
+
+export type FormOption = {
+  value: string
+  label: string
+  description?: string
+}
+
+export type FormStringField = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen
+  type: "string"
+  format?: "email" | "uri" | "date" | "date-time"
+  minLength?: number
+  maxLength?: number
+  pattern?: string
+  placeholder?: string
+  default?: string
+  options?: Array<FormOption>
+  custom?: boolean
+}
+
+export type FormNumberField = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen
+  type: "number"
+  minimum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
+  maximum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
+  default?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
+}
+
+export type FormIntegerField = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen
+  type: "integer"
+  minimum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
+  maximum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
+  default?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
+}
+
+export type FormBooleanField = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen
+  type: "boolean"
+  default?: boolean
+}
+
+export type FormMultiselectField = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen
+  type: "multiselect"
+  options: Array<FormOption>
+  minItems?: number
+  maxItems?: number
+  custom?: boolean
+  default?: Array<string>
+}
+
+export type FormFormInfo = {
+  id: string
+  sessionID: string
+  title?: string
+  metadata?: FormMetadata
+  mode: "form"
+  fields: Array<FormStringField | FormNumberField | FormIntegerField | FormBooleanField | FormMultiselectField>
+}
+
+export type FormUrlInfo = {
+  id: string
+  sessionID: string
+  title?: string
+  metadata?: FormMetadata
+  mode: "url"
+  url: string
+}
+
+export type FormValue =
+  | string
+  | number
+  | "NaN"
+  | "Infinity"
+  | "-Infinity"
+  | "Infinity"
+  | "-Infinity"
+  | "NaN"
+  | boolean
+  | Array<string>
+
+export type FormAnswer = {
+  [key: string]: FormValue
+}
+
 export type ProjectVcs = "git"
 
 export type ProjectIcon = {
@@ -5391,6 +5554,31 @@ export type ProjectCurrent = {
   directory: string
 }
 
+export type FormCreatePayload = {
+  id?: string
+  title?: string
+  metadata?: FormMetadata
+  mode: "form" | "url"
+  fields?: Array<FormStringField | FormNumberField | FormIntegerField | FormBooleanField | FormMultiselectField>
+  url?: string
+}
+
+export type FormState =
+  | {
+      status: "pending"
+    }
+  | {
+      status: "answered"
+      answer: FormAnswer
+    }
+  | {
+      status: "cancelled"
+    }
+
+export type FormReply = {
+  answer: FormAnswer
+}
+
 export type PermissionV2Request = {
   id: string
   sessionID: string
@@ -6194,6 +6382,86 @@ export type QuestionV2Rejected = {
   }
 }
 
+export type FormNumberField1 = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen
+  type: "number"
+  minimum?: number | "NaN" | "Infinity" | "-Infinity"
+  maximum?: number | "NaN" | "Infinity" | "-Infinity"
+  default?: number | "NaN" | "Infinity" | "-Infinity"
+}
+
+export type FormIntegerField1 = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen
+  type: "integer"
+  minimum?: number | "NaN" | "Infinity" | "-Infinity"
+  maximum?: number | "NaN" | "Infinity" | "-Infinity"
+  default?: number | "NaN" | "Infinity" | "-Infinity"
+}
+
+export type FormCreated = {
+  id: string
+  metadata?: {
+    [key: string]: unknown
+  }
+  type: "form.created"
+  durable?: {
+    aggregateID: string
+    seq: number
+    version: number
+  }
+  location?: LocationRef
+  data: {
+    form: FormFormInfo | FormUrlInfo
+  }
+}
+
+export type FormValue1 = string | number | "NaN" | "Infinity" | "-Infinity" | boolean | Array<string>
+
+export type FormReplied = {
+  id: string
+  metadata?: {
+    [key: string]: unknown
+  }
+  type: "form.replied"
+  durable?: {
+    aggregateID: string
+    seq: number
+    version: number
+  }
+  location?: LocationRef
+  data: {
+    id: string
+    sessionID: string
+    answer: FormAnswer
+  }
+}
+
+export type FormCancelled = {
+  id: string
+  metadata?: {
+    [key: string]: unknown
+  }
+  type: "form.cancelled"
+  durable?: {
+    aggregateID: string
+    seq: number
+    version: number
+  }
+  location?: LocationRef
+  data: {
+    id: string
+    sessionID: string
+  }
+}
+
 export type TodoUpdated = {
   id: string
   metadata?: {
@@ -7505,6 +7773,57 @@ export type EventQuestionV2Rejected = {
   }
 }
 
+export type FormNumberField2 = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen
+  type: "number"
+  minimum?: number | "NaN" | "Infinity" | "-Infinity"
+  maximum?: number | "NaN" | "Infinity" | "-Infinity"
+  default?: number | "NaN" | "Infinity" | "-Infinity"
+}
+
+export type FormIntegerField2 = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen
+  type: "integer"
+  minimum?: number | "NaN" | "Infinity" | "-Infinity"
+  maximum?: number | "NaN" | "Infinity" | "-Infinity"
+  default?: number | "NaN" | "Infinity" | "-Infinity"
+}
+
+export type EventFormCreated = {
+  id: string
+  type: "form.created"
+  properties: {
+    form: FormFormInfo | FormUrlInfo
+  }
+}
+
+export type EventFormReplied = {
+  id: string
+  type: "form.replied"
+  properties: {
+    id: string
+    sessionID: string
+    answer: FormAnswer
+  }
+}
+
+export type EventFormCancelled = {
+  id: string
+  type: "form.cancelled"
+  properties: {
+    id: string
+    sessionID: string
+  }
+}
+
 export type EventTodoUpdated = {
   id: string
   type: "todo.updated"
@@ -9333,69 +9652,229 @@ export type ProjectDirectory2 = {
 
 export type ProjectDirectories2 = Array<ProjectDirectory2>
 
-export type PermissionV2Source2 = {
-  type: "tool"
-  messageID: string
-  callID: string
+export type FormMetadata2 = {
+  [key: string]: unknown
 }
 
-export type PermissionV2Request2 = {
-  id: string
-  sessionID: string
-  action: string
-  resources: Array<string>
-  save?: Array<string>
-  metadata?: {
-    [key: string]: unknown
-  }
-  source?: PermissionV2Source2
+export type FormWhen2 = {
+  key: string
+  op: "eq" | "neq"
+  value: string
 }
 
-export type PermissionSavedInfo2 = {
-  id: string
-  projectID: string
-  action: string
-  resource: string
+export type FormOption2 = {
+  value: string
+  label: string
+  description?: string
 }
 
-export type PermissionNotFoundErrorV2 = {
-  _tag: "PermissionNotFoundError"
-  requestID: string
-  message: string
+export type FormStringField2 = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen2
+  type: "string"
+  format?: "email" | "uri" | "date" | "date-time"
+  minLength?: number
+  maxLength?: number
+  pattern?: string
+  placeholder?: string
+  default?: string
+  options?: Array<FormOption2>
+  custom?: boolean
 }
 
-export type PermissionV2Reply2 = "once" | "always" | "reject"
+export type FormNumberField3 = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen2
+  type: "number"
+  minimum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
+  maximum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
+  default?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
+}
 
-export type FileSystemEntry2 = {
-  path: string
-  type: "file" | "directory"
+export type FormIntegerField3 = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen2
+  type: "integer"
+  minimum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
+  maximum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
+  default?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
 }
 
-export type CommandV2Info2 = {
-  name: string
-  template: string
+export type FormBooleanField2 = {
+  key: string
+  title?: string
   description?: string
-  agent?: string
-  model?: ModelRef2
-  subtask?: boolean
+  required?: boolean
+  when?: FormWhen2
+  type: "boolean"
+  default?: boolean
 }
 
-export type SkillV2Info2 = {
-  name: string
+export type FormMultiselectField2 = {
+  key: string
+  title?: string
   description?: string
-  slash?: boolean
-  autoinvoke?: boolean
-  location: string
-  content: string
+  required?: boolean
+  when?: FormWhen2
+  type: "multiselect"
+  options: Array<FormOption2>
+  minItems?: number
+  maxItems?: number
+  custom?: boolean
+  default?: Array<string>
 }
 
-export type ModelsDevRefreshed2 = {
+export type FormFormInfo2 = {
   id: string
-  metadata?: {
-    [key: string]: unknown
-  }
-  type: "models-dev.refreshed"
-  durable?: {
+  sessionID: string
+  title?: string
+  metadata?: FormMetadata2
+  mode: "form"
+  fields: Array<FormStringField2 | FormNumberField3 | FormIntegerField3 | FormBooleanField2 | FormMultiselectField2>
+}
+
+export type FormUrlInfo2 = {
+  id: string
+  sessionID: string
+  title?: string
+  metadata?: FormMetadata2
+  mode: "url"
+  url: string
+}
+
+export type FormCreatePayload2 = {
+  id?: string | null
+  title?: string
+  metadata?: FormMetadata2
+  mode: "form" | "url"
+  fields?: Array<
+    FormStringField2 | FormNumberField3 | FormIntegerField3 | FormBooleanField2 | FormMultiselectField2
+  > | null
+  url?: string | null
+}
+
+export type FormNotFoundErrorV2 = {
+  _tag: "FormNotFoundError"
+  id: string
+  message: string
+}
+
+export type FormValue2 =
+  | string
+  | number
+  | "NaN"
+  | "Infinity"
+  | "-Infinity"
+  | "Infinity"
+  | "-Infinity"
+  | "NaN"
+  | boolean
+  | Array<string>
+
+export type FormAnswer2 = {
+  [key: string]: FormValue2
+}
+
+export type FormState2 =
+  | {
+      status: "pending"
+    }
+  | {
+      status: "answered"
+      answer: FormAnswer2
+    }
+  | {
+      status: "cancelled"
+    }
+
+export type FormReply2 = {
+  answer: FormAnswer2
+}
+
+export type FormAlreadySettledErrorV2 = {
+  _tag: "FormAlreadySettledError"
+  id: string
+  message: string
+}
+
+export type FormInvalidAnswerErrorV2 = {
+  _tag: "FormInvalidAnswerError"
+  id: string
+  message: string
+}
+
+export type PermissionV2Source2 = {
+  type: "tool"
+  messageID: string
+  callID: string
+}
+
+export type PermissionV2Request2 = {
+  id: string
+  sessionID: string
+  action: string
+  resources: Array<string>
+  save?: Array<string>
+  metadata?: {
+    [key: string]: unknown
+  }
+  source?: PermissionV2Source2
+}
+
+export type PermissionSavedInfo2 = {
+  id: string
+  projectID: string
+  action: string
+  resource: string
+}
+
+export type PermissionNotFoundErrorV2 = {
+  _tag: "PermissionNotFoundError"
+  requestID: string
+  message: string
+}
+
+export type PermissionV2Reply2 = "once" | "always" | "reject"
+
+export type FileSystemEntry2 = {
+  path: string
+  type: "file" | "directory"
+}
+
+export type CommandV2Info2 = {
+  name: string
+  template: string
+  description?: string
+  agent?: string
+  model?: ModelRef2
+  subtask?: boolean
+}
+
+export type SkillV2Info2 = {
+  name: string
+  description?: string
+  slash?: boolean
+  autoinvoke?: boolean
+  location: string
+  content: string
+}
+
+export type ModelsDevRefreshed2 = {
+  id: string
+  metadata?: {
+    [key: string]: unknown
+  }
+  type: "models-dev.refreshed"
+  durable?: {
     aggregateID: string
     seq: number
     version: number
@@ -10619,6 +11098,112 @@ export type QuestionV2Rejected2 = {
   }
 }
 
+export type FormMetadata1 = {
+  [key: string]: unknown
+}
+
+export type FormNumberField12 = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen2
+  type: "number"
+  minimum?: number | "NaN" | "Infinity" | "-Infinity"
+  maximum?: number | "NaN" | "Infinity" | "-Infinity"
+  default?: number | "NaN" | "Infinity" | "-Infinity"
+}
+
+export type FormIntegerField12 = {
+  key: string
+  title?: string
+  description?: string
+  required?: boolean
+  when?: FormWhen2
+  type: "integer"
+  minimum?: number | "NaN" | "Infinity" | "-Infinity"
+  maximum?: number | "NaN" | "Infinity" | "-Infinity"
+  default?: number | "NaN" | "Infinity" | "-Infinity"
+}
+
+export type FormFormInfo1 = {
+  id: string
+  sessionID: string
+  title?: string
+  metadata?: FormMetadata1
+  mode: "form"
+  fields: Array<FormStringField2 | FormNumberField12 | FormIntegerField12 | FormBooleanField2 | FormMultiselectField2>
+}
+
+export type FormUrlInfo1 = {
+  id: string
+  sessionID: string
+  title?: string
+  metadata?: FormMetadata1
+  mode: "url"
+  url: string
+}
+
+export type FormCreated2 = {
+  id: string
+  metadata?: {
+    [key: string]: unknown
+  }
+  type: "form.created"
+  durable?: {
+    aggregateID: string
+    seq: number
+    version: number
+  }
+  location?: LocationRef2
+  data: {
+    form: FormFormInfo1 | FormUrlInfo1
+  }
+}
+
+export type FormValue12 = string | number | "NaN" | "Infinity" | "-Infinity" | boolean | Array<string>
+
+export type FormAnswer1 = {
+  [key: string]: FormValue12
+}
+
+export type FormReplied2 = {
+  id: string
+  metadata?: {
+    [key: string]: unknown
+  }
+  type: "form.replied"
+  durable?: {
+    aggregateID: string
+    seq: number
+    version: number
+  }
+  location?: LocationRef2
+  data: {
+    id: string
+    sessionID: string
+    answer: FormAnswer1
+  }
+}
+
+export type FormCancelled2 = {
+  id: string
+  metadata?: {
+    [key: string]: unknown
+  }
+  type: "form.cancelled"
+  durable?: {
+    aggregateID: string
+    seq: number
+    version: number
+  }
+  location?: LocationRef2
+  data: {
+    id: string
+    sessionID: string
+  }
+}
+
 export type TodoV2 = {
   /**
    * Brief description of the task
@@ -11131,6 +11716,9 @@ export type V2EventV2 =
   | QuestionV2Asked2
   | QuestionV2Replied2
   | QuestionV2Rejected2
+  | FormCreated2
+  | FormReplied2
+  | FormCancelled2
   | TodoUpdated2
   | SessionStatusV22
   | SessionIdle2
@@ -17268,6 +17856,277 @@ export type V2ProjectDirectoriesResponses = {
 
 export type V2ProjectDirectoriesResponse = V2ProjectDirectoriesResponses[keyof V2ProjectDirectoriesResponses]
 
+export type V2FormRequestListData = {
+  body?: never
+  path?: never
+  query?: {
+    location?: {
+      directory?: string | null
+      workspace?: string | null
+    } | null
+  }
+  url: "/api/form/request"
+}
+
+export type V2FormRequestListErrors = {
+  /**
+   * InvalidRequestError
+   */
+  400: InvalidRequestErrorV2
+  /**
+   * UnauthorizedError
+   */
+  401: UnauthorizedErrorV2
+}
+
+export type V2FormRequestListError = V2FormRequestListErrors[keyof V2FormRequestListErrors]
+
+export type V2FormRequestListResponses = {
+  /**
+   * Success
+   */
+  200: {
+    location: LocationInfo2
+    data: Array<FormFormInfo2 | FormUrlInfo2>
+  }
+}
+
+export type V2FormRequestListResponse = V2FormRequestListResponses[keyof V2FormRequestListResponses]
+
+export type V2SessionFormListData = {
+  body?: never
+  path: {
+    sessionID: string
+  }
+  query?: never
+  url: "/api/session/{sessionID}/form"
+}
+
+export type V2SessionFormListErrors = {
+  /**
+   * InvalidRequestError
+   */
+  400: InvalidRequestErrorV2
+  /**
+   * UnauthorizedError
+   */
+  401: UnauthorizedErrorV2
+  /**
+   * SessionNotFoundError
+   */
+  404: SessionNotFoundErrorV2
+}
+
+export type V2SessionFormListError = V2SessionFormListErrors[keyof V2SessionFormListErrors]
+
+export type V2SessionFormListResponses = {
+  /**
+   * Success
+   */
+  200: {
+    data: Array<FormFormInfo2 | FormUrlInfo2>
+  }
+}
+
+export type V2SessionFormListResponse = V2SessionFormListResponses[keyof V2SessionFormListResponses]
+
+export type V2SessionFormCreateData = {
+  body: FormCreatePayload2
+  path: {
+    sessionID: string
+  }
+  query?: never
+  url: "/api/session/{sessionID}/form"
+}
+
+export type V2SessionFormCreateErrors = {
+  /**
+   * InvalidRequestError
+   */
+  400: InvalidRequestError1 | InvalidRequestErrorV2
+  /**
+   * UnauthorizedError
+   */
+  401: UnauthorizedErrorV2
+  /**
+   * SessionNotFoundError
+   */
+  404: SessionNotFoundErrorV2
+  /**
+   * ConflictError
+   */
+  409: ConflictErrorV2
+}
+
+export type V2SessionFormCreateError = V2SessionFormCreateErrors[keyof V2SessionFormCreateErrors]
+
+export type V2SessionFormCreateResponses = {
+  /**
+   * Success
+   */
+  200: {
+    data: FormFormInfo2 | FormUrlInfo2
+  }
+}
+
+export type V2SessionFormCreateResponse = V2SessionFormCreateResponses[keyof V2SessionFormCreateResponses]
+
+export type V2SessionFormGetData = {
+  body?: never
+  path: {
+    sessionID: string
+    formID: string
+  }
+  query?: never
+  url: "/api/session/{sessionID}/form/{formID}"
+}
+
+export type V2SessionFormGetErrors = {
+  /**
+   * InvalidRequestError
+   */
+  400: InvalidRequestErrorV2
+  /**
+   * UnauthorizedError
+   */
+  401: UnauthorizedErrorV2
+  /**
+   * SessionNotFoundError | FormNotFoundError
+   */
+  404: FormNotFoundErrorV2 | SessionNotFoundErrorV2
+}
+
+export type V2SessionFormGetError = V2SessionFormGetErrors[keyof V2SessionFormGetErrors]
+
+export type V2SessionFormGetResponses = {
+  /**
+   * Success
+   */
+  200: {
+    data: FormFormInfo2 | FormUrlInfo2
+  }
+}
+
+export type V2SessionFormGetResponse = V2SessionFormGetResponses[keyof V2SessionFormGetResponses]
+
+export type V2SessionFormStateData = {
+  body?: never
+  path: {
+    sessionID: string
+    formID: string
+  }
+  query?: never
+  url: "/api/session/{sessionID}/form/{formID}/state"
+}
+
+export type V2SessionFormStateErrors = {
+  /**
+   * InvalidRequestError
+   */
+  400: InvalidRequestErrorV2
+  /**
+   * UnauthorizedError
+   */
+  401: UnauthorizedErrorV2
+  /**
+   * SessionNotFoundError | FormNotFoundError
+   */
+  404: FormNotFoundErrorV2 | SessionNotFoundErrorV2
+}
+
+export type V2SessionFormStateError = V2SessionFormStateErrors[keyof V2SessionFormStateErrors]
+
+export type V2SessionFormStateResponses = {
+  /**
+   * Success
+   */
+  200: {
+    data: FormState2
+  }
+}
+
+export type V2SessionFormStateResponse = V2SessionFormStateResponses[keyof V2SessionFormStateResponses]
+
+export type V2SessionFormReplyData = {
+  body: FormReply2
+  path: {
+    sessionID: string
+    formID: string
+  }
+  query?: never
+  url: "/api/session/{sessionID}/form/{formID}/reply"
+}
+
+export type V2SessionFormReplyErrors = {
+  /**
+   * FormInvalidAnswerError | InvalidRequestError
+   */
+  400: FormInvalidAnswerErrorV2 | InvalidRequestErrorV2
+  /**
+   * UnauthorizedError
+   */
+  401: UnauthorizedErrorV2
+  /**
+   * SessionNotFoundError | FormNotFoundError
+   */
+  404: FormNotFoundErrorV2 | SessionNotFoundErrorV2
+  /**
+   * FormAlreadySettledError
+   */
+  409: FormAlreadySettledErrorV2
+}
+
+export type V2SessionFormReplyError = V2SessionFormReplyErrors[keyof V2SessionFormReplyErrors]
+
+export type V2SessionFormReplyResponses = {
+  /**
+   * <No Content>
+   */
+  204: void
+}
+
+export type V2SessionFormReplyResponse = V2SessionFormReplyResponses[keyof V2SessionFormReplyResponses]
+
+export type V2SessionFormCancelData = {
+  body?: never
+  path: {
+    sessionID: string
+    formID: string
+  }
+  query?: never
+  url: "/api/session/{sessionID}/form/{formID}/cancel"
+}
+
+export type V2SessionFormCancelErrors = {
+  /**
+   * InvalidRequestError
+   */
+  400: InvalidRequestErrorV2
+  /**
+   * UnauthorizedError
+   */
+  401: UnauthorizedErrorV2
+  /**
+   * SessionNotFoundError | FormNotFoundError
+   */
+  404: FormNotFoundErrorV2 | SessionNotFoundErrorV2
+  /**
+   * FormAlreadySettledError
+   */
+  409: FormAlreadySettledErrorV2
+}
+
+export type V2SessionFormCancelError = V2SessionFormCancelErrors[keyof V2SessionFormCancelErrors]
+
+export type V2SessionFormCancelResponses = {
+  /**
+   * <No Content>
+   */
+  204: void
+}
+
+export type V2SessionFormCancelResponse = V2SessionFormCancelResponses[keyof V2SessionFormCancelResponses]
+
 export type V2PermissionRequestListData = {
   body?: never
   path?: never

+ 2 - 0
packages/server/src/handlers.ts

@@ -6,6 +6,7 @@ import { ProviderHandler } from "./handlers/provider"
 import { SessionHandler } from "./handlers/session"
 import { PermissionHandler } from "./handlers/permission"
 import { FileSystemHandler } from "./handlers/fs"
+import { FormHandler } from "./handlers/form"
 import { CommandHandler } from "./handlers/command"
 import { SkillHandler } from "./handlers/skill"
 import { EventHandler } from "./handlers/event"
@@ -37,6 +38,7 @@ export const handlers = Layer.mergeAll(
   McpHandler,
   CredentialHandler,
   ProjectHandler,
+  FormHandler,
   PermissionHandler,
   FileSystemHandler,
   CommandHandler,

+ 123 - 0
packages/server/src/handlers/form.ts

@@ -0,0 +1,123 @@
+import { Form } from "@opencode-ai/core/form"
+import {
+  ConflictError,
+  FormAlreadySettledError,
+  FormInvalidAnswerError,
+  FormNotFoundError,
+  InvalidRequestError,
+} from "@opencode-ai/protocol/errors"
+import { Effect } from "effect"
+import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
+import { Api } from "../api"
+import { response } from "../location"
+
+function missingForm(id: Form.ID) {
+  return new FormNotFoundError({ id, message: `Form not found: ${id}` })
+}
+
+export const FormHandler = HttpApiBuilder.group(Api, "server.form", (handlers) =>
+  Effect.gen(function* () {
+    const requireOwnedForm = Effect.fnUntraced(function* (sessionID: Form.Info["sessionID"], formID: Form.ID) {
+      const form = yield* Form.Service
+      const info = yield* form.get(formID).pipe(Effect.catchTag("Form.NotFoundError", () => missingForm(formID)))
+      if (info.sessionID !== sessionID) return yield* missingForm(formID)
+      return { form, info }
+    })
+
+    return handlers
+      .handle(
+        "form.request.list",
+        Effect.fn(function* () {
+          const form = yield* Form.Service
+          return yield* response(form.list())
+        }),
+      )
+      .handle(
+        "session.form.list",
+        Effect.fn(function* (ctx) {
+          const form = yield* Form.Service
+          const forms = yield* form.list({ sessionID: ctx.params.sessionID })
+          return { data: forms }
+        }),
+      )
+      .handle(
+        "session.form.create",
+        Effect.fn(function* (ctx) {
+          const form = yield* Form.Service
+          const common = {
+            id: ctx.payload.id,
+            sessionID: ctx.params.sessionID,
+            title: ctx.payload.title,
+            metadata: ctx.payload.metadata,
+          }
+          const input = yield* (() => {
+            if (ctx.payload.mode === "form") {
+              if (!ctx.payload.fields) {
+                return new InvalidRequestError({ message: "Form fields are required", field: "fields" })
+              }
+              return Effect.succeed({ ...common, mode: "form" as const, fields: ctx.payload.fields })
+            }
+            if (!ctx.payload.url) return new InvalidRequestError({ message: "Form URL is required", field: "url" })
+            return Effect.succeed({ ...common, mode: "url" as const, url: ctx.payload.url })
+          })()
+
+          const created = yield* form
+            .create(input)
+            .pipe(
+              Effect.catchTag(
+                "Form.AlreadyExistsError",
+                (error) => new ConflictError({ resource: error.id, message: error.message }),
+              ),
+            )
+          return { data: created }
+        }),
+      )
+      .handle(
+        "session.form.get",
+        Effect.fn(function* (ctx) {
+          const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID)
+          return { data: owned.info }
+        }),
+      )
+      .handle(
+        "session.form.state",
+        Effect.fn(function* (ctx) {
+          const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID)
+          const data = yield* owned.form
+            .state(ctx.params.formID)
+            .pipe(Effect.catchTag("Form.NotFoundError", () => missingForm(ctx.params.formID)))
+          return { data }
+        }),
+      )
+      .handle(
+        "session.form.reply",
+        Effect.fn(function* (ctx) {
+          const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID)
+          yield* owned.form.reply({ id: ctx.params.formID, answer: ctx.payload.answer }).pipe(
+            Effect.catchTags({
+              "Form.AlreadySettledError": (error) =>
+                new FormAlreadySettledError({ id: error.id, message: error.message }),
+              "Form.InvalidAnswerError": (error) =>
+                new FormInvalidAnswerError({ id: error.id, message: error.message }),
+              "Form.NotFoundError": () => missingForm(ctx.params.formID),
+            }),
+          )
+          return HttpApiSchema.NoContent.make()
+        }),
+      )
+      .handle(
+        "session.form.cancel",
+        Effect.fn(function* (ctx) {
+          const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID)
+          yield* owned.form.cancel(ctx.params.formID).pipe(
+            Effect.catchTags({
+              "Form.AlreadySettledError": (error) =>
+                new FormAlreadySettledError({ id: error.id, message: error.message }),
+              "Form.NotFoundError": () => missingForm(ctx.params.formID),
+            }),
+          )
+          return HttpApiSchema.NoContent.make()
+        }),
+      )
+  }),
+)