@opencode/server 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { Session } from "@opencode/core/session";
2
- import { SessionNotFoundError, UnknownError } from "@opencode/protocol/errors";
2
+ import { MessageNotFoundError, SessionNotFoundError, UnknownError } from "@opencode/protocol/errors";
3
3
  import { Effect } from "effect";
4
4
  export function missingSession(error) {
5
5
  return new SessionNotFoundError({
@@ -7,7 +7,20 @@ export function missingSession(error) {
7
7
  message: `Session not found: ${error.sessionID}`
8
8
  });
9
9
  }
10
+ export function missingMessage(error) {
11
+ return new MessageNotFoundError({
12
+ sessionID: error.sessionID,
13
+ messageID: error.messageID,
14
+ message: `Message not found: ${error.messageID}`
15
+ });
16
+ }
10
17
  export function failedMessageDecode(error) {
11
18
  const ref = `err_${crypto.randomUUID().slice(0, 8)}`;
12
19
  return Effect.logError("failed to decode session message").pipe(Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }), Effect.andThen(Effect.fail(new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }))));
13
20
  }
21
+ export function failedSnapshot(operation, sessionID) {
22
+ return (error) => {
23
+ const ref = `err_${crypto.randomUUID().slice(0, 8)}`;
24
+ return Effect.logError(`failed to ${operation}`, { cause: error }).pipe(Effect.annotateLogs({ ref, sessionID }), Effect.andThen(Effect.fail(new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }))));
25
+ };
26
+ }
@@ -1,3 +1,3 @@
1
1
  import { Session } from "@opencode/core/session";
2
2
  import { SessionTransfer } from "@opencode/core/session/transfer";
3
- export declare const SessionHandler: import("effect/Layer").Layer<import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.session">, never, import("@opencode/protocol/middleware/schema-error").SchemaErrorMiddleware | import("@opencode/protocol/middleware/authorization").Authorization | Session.Service | import("../middleware/session-location").SessionLocationMiddleware | SessionTransfer.Service | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/database/database").Service>>;
3
+ export declare const SessionHandler: import("effect/Layer").Layer<import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.session">, never, Session.Service | import("../middleware/session-location").SessionLocationMiddleware | SessionTransfer.Service | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/database/database").Service> | (import("@opencode/protocol/middleware/schema-error").SchemaErrorMiddleware | import("@opencode/protocol/middleware/authorization").Authorization | import("../middleware/form-location").FormLocationMiddleware)>;
@@ -3,6 +3,7 @@ import { SessionStats } from "@opencode/core/session/stats";
3
3
  import { SessionTitle } from "@opencode/core/session/title";
4
4
  import { SessionTransfer } from "@opencode/core/session/transfer";
5
5
  import { InstructionEntry } from "@opencode/core/session/instruction-entry";
6
+ import { Form } from "@opencode/core/form";
6
7
  import { DateTime, Effect, Stream } from "effect";
7
8
  import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi";
8
9
  import { Api } from "../api";
@@ -11,26 +12,35 @@ import {
11
12
  ConflictError,
12
13
  CommandExecutionError,
13
14
  CommandNotFoundError,
15
+ FormAlreadySettledError,
16
+ FormInvalidAnswerError,
17
+ FormNotFoundError,
14
18
  InvalidRequestError,
15
19
  InvalidCursorError,
16
20
  MessageNotFoundError,
17
21
  ServiceUnavailableError,
18
22
  SessionBusyError,
19
- SkillNotFoundError,
20
- UnknownError
23
+ SkillNotFoundError
21
24
  } from "@opencode/protocol/errors";
22
25
  import { AbsolutePath } from "@opencode/core/schema";
23
- import { failedMessageDecode, missingSession } from "./session-error";
26
+ import { failedMessageDecode, failedSnapshot, missingMessage, missingSession } from "./session-error";
24
27
  const DefaultSessionsLimit = 50;
28
+ function missingForm(id) {
29
+ return new FormNotFoundError({ id, message: `Form not found: ${id}` });
30
+ }
25
31
  export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) => Effect.gen(function* () {
26
- const session = yield* Session.Service, transfer = yield* SessionTransfer.Service, busySession = (error) => new SessionBusyError({
32
+ const session = yield* Session.Service, transfer = yield* SessionTransfer.Service, requireOwnedForm = Effect.fnUntraced(function* (sessionID, formID) {
33
+ const form = yield* Form.Service, info = yield* form.get(formID).pipe(Effect.catchTag("Form.NotFoundError", () => missingForm(formID)));
34
+ if (info.sessionID !== sessionID)
35
+ return yield* missingForm(formID);
36
+ return { form, info };
37
+ }), busySession = (error) => new SessionBusyError({
27
38
  sessionID: error.sessionID,
28
39
  message: `Session is busy: ${error.sessionID}`
29
40
  }), pendingMutation = (effect, conflict) => effect.pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.InboxConflictError", (error) => new ConflictError({ resource: error.inboxID, message: `${conflict}: ${error.inboxID}` })), Effect.as(HttpApiSchema.NoContent.make()));
30
41
  return handlers.handle("session.list", Effect.fn(function* (ctx) {
31
42
  const query = ctx.query.cursor !== void 0 ? yield* SessionsCursor.parse(ctx.query.cursor).pipe(Effect.mapError(() => new InvalidCursorError({ message: "Invalid cursor" }))) : ctx.query, sessions = (yield* session.list({
32
43
  ...query,
33
- workspaceID: query.workspace,
34
44
  limit: ctx.query.limit ?? DefaultSessionsLimit
35
45
  })).data, first = sessions[0], last = sessions.at(-1);
36
46
  return {
@@ -105,7 +115,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
105
115
  data: yield* session.get(ctx.params.sessionID).pipe(Effect.catchTag("Session.NotFoundError", missingSession))
106
116
  };
107
117
  })).handle("session.view", Effect.fn(function* (ctx) {
108
- yield* session.view({ sessionID: ctx.params.sessionID, idle: ctx.payload.idle }).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
118
+ yield* session.view({ sessionID: ctx.params.sessionID, idle: DateTime.toEpochMillis(ctx.payload.idle) }).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
109
119
  return HttpApiSchema.NoContent.make();
110
120
  })).handle("session.remove", Effect.fn(function* (ctx) {
111
121
  yield* session.remove(ctx.params.sessionID).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
@@ -115,11 +125,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
115
125
  return HttpApiSchema.NoContent.make();
116
126
  })).handle("session.fork", Effect.fn(function* (ctx) {
117
127
  return {
118
- data: yield* session.fork({ sessionID: ctx.params.sessionID, boundary: ctx.payload.boundary }).pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.MessageNotFoundError", (error) => new MessageNotFoundError({
119
- sessionID: error.sessionID,
120
- messageID: error.messageID,
121
- message: `Message not found: ${error.messageID}`
122
- })), Effect.catchTag("Session.ForkEmptyError", (error) => new InvalidRequestError({ message: error.message, kind: "empty_session" })))
128
+ data: yield* session.fork({ sessionID: ctx.params.sessionID, before: ctx.payload.before }).pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.MessageNotFoundError", missingMessage), Effect.catchTag("Session.ForkEmptyError", (error) => new InvalidRequestError({ message: error.message, kind: "empty_session" })))
123
129
  };
124
130
  })).handle("session.switchAgent", Effect.fn(function* (ctx) {
125
131
  yield* session.switchAgent({ sessionID: ctx.params.sessionID, agent: ctx.payload.agent }).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
@@ -127,18 +133,20 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
127
133
  })).handle("session.switchModel", Effect.fn(function* (ctx) {
128
134
  yield* session.switchModel({ sessionID: ctx.params.sessionID, model: ctx.payload.model }).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
129
135
  return HttpApiSchema.NoContent.make();
130
- })).handle("session.rename", Effect.fn(function* (ctx) {
131
- if (ctx.payload.title) {
132
- yield* session.rename({ sessionID: ctx.params.sessionID, title: ctx.payload.title }).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
133
- return HttpApiSchema.NoContent.make();
134
- }
135
- yield* (yield* SessionTitle.Service).generate(ctx.params.sessionID);
136
+ })).handle("session.update", Effect.fn(function* (ctx) {
137
+ yield* session.get(ctx.params.sessionID).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
138
+ if (ctx.payload.title !== void 0)
139
+ if (ctx.payload.title)
140
+ yield* session.rename({ sessionID: ctx.params.sessionID, title: ctx.payload.title }).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
141
+ else
142
+ yield* (yield* SessionTitle.Service).generate(ctx.params.sessionID);
143
+ if (ctx.payload.permissions !== void 0)
144
+ yield* session.setPermissions({ sessionID: ctx.params.sessionID, permissions: ctx.payload.permissions }).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
136
145
  return HttpApiSchema.NoContent.make();
137
146
  })).handle("session.move", Effect.fn(function* (ctx) {
138
147
  yield* session.move({
139
148
  sessionID: ctx.params.sessionID,
140
149
  directory: ctx.payload.directory,
141
- workspaceID: ctx.payload.workspaceID,
142
150
  delivery: ctx.payload.delivery
143
151
  }).pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.DestinationNotFoundError", (error) => Effect.fail(new InvalidRequestError({ message: `Directory does not exist: ${error.directory}` }))), Effect.catchTag("Session.DestinationNotDirectoryError", (error) => Effect.fail(new InvalidRequestError({ message: `Not a directory: ${error.directory}` }))), Effect.catchTag("Session.DestinationUnavailableError", (error) => Effect.fail(new InvalidRequestError({ message: `Directory is unavailable: ${error.directory}` }))));
144
152
  return HttpApiSchema.NoContent.make();
@@ -162,7 +170,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
162
170
  })).handle("session.command", Effect.fn(function* (ctx) {
163
171
  yield* session.command({
164
172
  sessionID: ctx.params.sessionID,
165
- command: ctx.payload.command,
173
+ command: ctx.payload.name,
166
174
  text: ctx.payload.text,
167
175
  files: ctx.payload.files,
168
176
  agents: ctx.payload.agents,
@@ -179,8 +187,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
179
187
  })).handle("session.skill", Effect.fn(function* (ctx) {
180
188
  yield* session.skill({
181
189
  sessionID: ctx.params.sessionID,
182
- id: ctx.payload.id,
183
- skill: ctx.payload.skill,
190
+ skill: ctx.payload.id,
184
191
  resume: ctx.payload.resume
185
192
  }).pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.SkillNotFoundError", (error) => Effect.fail(new SkillNotFoundError({ skill: error.skill, message: `Skill not found: ${error.skill}` }))));
186
193
  return HttpApiSchema.NoContent.make();
@@ -217,27 +224,11 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
217
224
  files: ctx.payload.files
218
225
  });
219
226
  return {
220
- data: yield* session.revert.stage({ ...ctx.params, ...ctx.payload }).pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.MessageNotFoundError", (error) => new MessageNotFoundError({
221
- sessionID: error.sessionID,
222
- messageID: error.messageID,
223
- message: `Message not found: ${error.messageID}`
224
- })), Effect.catchTag("Session.BusyError", busySession), Effect.catchTag("Snapshot.Error", (error) => {
225
- const ref = `err_${crypto.randomUUID().slice(0, 8)}`;
226
- return Effect.logError("failed to stage session revert", { cause: error }).pipe(Effect.andThen(Effect.fail(new UnknownError({
227
- message: "Unexpected server error. Check server logs for details.",
228
- ref
229
- }))));
230
- }))
227
+ data: yield* session.revert.stage({ ...ctx.params, ...ctx.payload }).pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.MessageNotFoundError", missingMessage), Effect.catchTag("Session.BusyError", busySession), Effect.catchTag("Snapshot.Error", failedSnapshot("stage session revert", ctx.params.sessionID)))
231
228
  };
232
229
  })).handle("session.revert.clear", Effect.fn(function* (ctx) {
233
230
  yield* Effect.log("session.revert.clear", { sessionID: ctx.params.sessionID });
234
- yield* session.revert.clear(ctx.params.sessionID).pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.BusyError", busySession), Effect.catchTag("Snapshot.Error", (error) => {
235
- const ref = `err_${crypto.randomUUID().slice(0, 8)}`;
236
- return Effect.logError("failed to clear session revert", { cause: error }).pipe(Effect.andThen(Effect.fail(new UnknownError({
237
- message: "Unexpected server error. Check server logs for details.",
238
- ref
239
- }))));
240
- }));
231
+ yield* session.revert.clear(ctx.params.sessionID).pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.BusyError", busySession), Effect.catchTag("Snapshot.Error", failedSnapshot("clear session revert", ctx.params.sessionID)));
241
232
  return HttpApiSchema.NoContent.make();
242
233
  })).handle("session.revert.commit", Effect.fn(function* (ctx) {
243
234
  yield* Effect.log("session.revert.commit", { sessionID: ctx.params.sessionID });
@@ -247,16 +238,19 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
247
238
  return {
248
239
  data: yield* session.context(ctx.params.sessionID).pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.MessageDecodeError", failedMessageDecode))
249
240
  };
241
+ })).handle("session.diff", Effect.fn(function* (ctx) {
242
+ return {
243
+ data: yield* session.diff({ sessionID: ctx.params.sessionID, ...ctx.query }).pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.MessageNotFoundError", missingMessage), Effect.catchTag("Session.TurnRangeError", (error) => new InvalidRequestError({ message: error.message, field: error.field })), Effect.catchTag("Snapshot.Error", failedSnapshot("diff session turn", ctx.params.sessionID)))
244
+ };
250
245
  })).handle("session.inbox.list", Effect.fn(function* (ctx) {
251
246
  return {
252
247
  data: yield* session.inbox(ctx.params.sessionID).pipe(Effect.catchTag("Session.NotFoundError", missingSession))
253
248
  };
254
249
  })).handle("session.inbox.cancel", Effect.fn(function* (ctx) {
255
- return yield* pendingMutation(session.cancelInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }), "Pending input can no longer be cancelled");
256
- })).handle("session.inbox.steer", Effect.fn(function* (ctx) {
257
- return yield* pendingMutation(session.steerInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }), "Pending input is no longer queued");
258
- })).handle("session.inbox.queue", Effect.fn(function* (ctx) {
259
- return yield* pendingMutation(session.queueInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }), "Pending input is no longer a steer");
250
+ yield* session.cancelInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }).pipe(Effect.catchTag("Session.NotFoundError", missingSession), Effect.catchTag("Session.InboxConflictError", () => Effect.void));
251
+ return HttpApiSchema.NoContent.make();
252
+ })).handle("session.inbox.update", Effect.fn(function* (ctx) {
253
+ return yield* pendingMutation(ctx.payload.delivery === "steer" ? session.steerInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }) : session.queueInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }), `Pending input cannot change to ${ctx.payload.delivery}`);
260
254
  })).handle("session.instructions.entry.list", Effect.fn(function* (ctx) {
261
255
  return { data: yield* (yield* InstructionEntry.Service).list(ctx.params.sessionID) };
262
256
  })).handle("session.instructions.entry.put", Effect.fn(function* (ctx) {
@@ -271,7 +265,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
271
265
  yield* session.get(ctx.params.sessionID).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
272
266
  return session.log({ sessionID: ctx.params.sessionID, after: ctx.query.after, follow: ctx.query.follow }).pipe(Stream.orDie);
273
267
  })).handle("session.interrupt", Effect.fn(function* (ctx) {
274
- return { interrupted: yield* session.interrupt(ctx.params.sessionID, { continue: ctx.query.continue }) };
268
+ return { interrupted: yield* session.interrupt(ctx.params.sessionID, { resume: ctx.query.resume }) };
275
269
  })).handle("session.background", Effect.fn(function* (ctx) {
276
270
  yield* session.background(ctx.params.sessionID).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
277
271
  return HttpApiSchema.NoContent.make();
@@ -285,5 +279,34 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
285
279
  messageID: ctx.params.messageID,
286
280
  message: `Message not found: ${ctx.params.messageID}`
287
281
  });
282
+ })).handle("session.form.list", Effect.fn(function* (ctx) {
283
+ return { data: yield* (yield* Form.Service).list({ sessionID: ctx.params.sessionID }) };
284
+ })).handle("session.form.create", Effect.fn(function* (ctx) {
285
+ return { data: yield* (yield* Form.Service).create({
286
+ id: ctx.payload.id,
287
+ sessionID: ctx.params.sessionID,
288
+ title: ctx.payload.title,
289
+ metadata: ctx.payload.metadata,
290
+ fields: ctx.payload.fields
291
+ }).pipe(Effect.catchTags({
292
+ "Form.AlreadyExistsError": (error) => new ConflictError({ resource: error.id, message: error.message }),
293
+ "Form.InvalidFormError": (error) => new InvalidRequestError({ message: error.message, field: "fields" })
294
+ })) };
295
+ })).handle("session.form.get", Effect.fn(function* (ctx) {
296
+ const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID), state = yield* owned.form.state(ctx.params.formID).pipe(Effect.catchTag("Form.NotFoundError", () => missingForm(ctx.params.formID)));
297
+ return { data: { ...owned.info, state } };
298
+ })).handle("session.form.reply", Effect.fn(function* (ctx) {
299
+ yield* (yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID)).form.reply({ id: ctx.params.formID, answer: ctx.payload.answer }).pipe(Effect.catchTags({
300
+ "Form.AlreadySettledError": (error) => new FormAlreadySettledError({ id: error.id, message: error.message }),
301
+ "Form.InvalidAnswerError": (error) => new FormInvalidAnswerError({ id: error.id, message: error.message }),
302
+ "Form.NotFoundError": () => missingForm(ctx.params.formID)
303
+ }));
304
+ return HttpApiSchema.NoContent.make();
305
+ })).handle("session.form.cancel", Effect.fn(function* (ctx) {
306
+ yield* (yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID)).form.cancel(ctx.params.formID).pipe(Effect.catchTags({
307
+ "Form.AlreadySettledError": (error) => new FormAlreadySettledError({ id: error.id, message: error.message }),
308
+ "Form.NotFoundError": () => missingForm(ctx.params.formID)
309
+ }));
310
+ return HttpApiSchema.NoContent.make();
288
311
  }));
289
312
  }));
@@ -15,14 +15,11 @@ export const ShellHandler = HttpApiBuilder.group(Api, "server.shell", (handlers)
15
15
  })).handle("shell.get", Effect.fn(function* (ctx) {
16
16
  const shell = yield* Shell.Service;
17
17
  return yield* response(shell.get(ctx.params.id).pipe(Effect.catchTag("Shell.NotFoundError", () => new ShellNotFoundError({ id: ctx.params.id, message: `Shell command not found: ${ctx.params.id}` }))));
18
- })).handle("shell.timeout", Effect.fn(function* (ctx) {
19
- const shell = yield* Shell.Service;
20
- return yield* response(shell.timeout(ctx.params.id, ctx.payload.timeout).pipe(Effect.catchTag("Shell.NotFoundError", () => new ShellNotFoundError({ id: ctx.params.id, message: `Shell command not found: ${ctx.params.id}` }))));
21
18
  })).handle("shell.output", Effect.fn(function* (ctx) {
22
19
  const shell = yield* Shell.Service;
23
20
  return yield* response(shell.output(ctx.params.id, { cursor: ctx.query.cursor, limit: ctx.query.limit }).pipe(Effect.catchTag("Shell.NotFoundError", () => new ShellNotFoundError({ id: ctx.params.id, message: `Shell command not found: ${ctx.params.id}` }))));
24
21
  })).handle("shell.remove", Effect.fn(function* (ctx) {
25
- yield* (yield* Shell.Service).remove(ctx.params.id).pipe(Effect.catchTag("Shell.NotFoundError", () => new ShellNotFoundError({ id: ctx.params.id, message: `Shell command not found: ${ctx.params.id}` })));
22
+ yield* (yield* Shell.Service).remove(ctx.params.id).pipe(Effect.catchTag("Shell.NotFoundError", () => Effect.void));
26
23
  return HttpApiSchema.NoContent.make();
27
24
  }));
28
25
  }));
@@ -11,7 +11,7 @@ export const VcsHandler = HttpApiBuilder.group(Api, "server.vcs", (handlers) =>
11
11
  return yield* (yield* Vcs.Service).base().pipe(Effect.mapError((error) => new ServiceUnavailableError({ service: "vcs", message: error.message })));
12
12
  }))).handle("vcs.status", () => response(Effect.gen(function* () {
13
13
  return yield* (yield* Vcs.Service).status();
14
- }))).handle("vcs.branches", (ctx) => response(Effect.gen(function* () {
14
+ }))).handle("vcs.branch.list", (ctx) => response(Effect.gen(function* () {
15
15
  return yield* (yield* Vcs.Service).branches({ search: ctx.query.search, limit: Math.min(ctx.query.limit ?? 50, 100) });
16
16
  }))).handle("vcs.diff", (ctx) => response(Effect.gen(function* () {
17
17
  return yield* (yield* Vcs.Service).diff(ctx.query.mode, { context: ctx.query.context, base: ctx.query.base }).pipe(Effect.mapError((error) => new ServiceUnavailableError({ service: "vcs", message: error.message })));
@@ -1 +1,2 @@
1
- export declare const WorktreeHandler: import("effect/Layer").Layer<import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.worktree">, never, import("@opencode/protocol/middleware/schema-error").SchemaErrorMiddleware | import("@opencode/protocol/middleware/authorization").Authorization | import("../location").LocationMiddleware>;
1
+ import { Worktree } from "@opencode/core/worktree";
2
+ export declare const WorktreeHandler: import("effect/Layer").Layer<import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.worktree">, never, import("@opencode/protocol/middleware/schema-error").SchemaErrorMiddleware | import("@opencode/protocol/middleware/authorization").Authorization | Worktree.Service>;
@@ -1,20 +1,17 @@
1
1
  import { Git } from "@opencode/core/git";
2
2
  import { Worktree } from "@opencode/core/worktree";
3
- import { Plugin } from "@opencode/core/plugin";
3
+ import { Project } from "@opencode/core/project";
4
+ import { ProjectNotFoundError } from "@opencode/protocol/errors";
4
5
  import { WorktreeError } from "@opencode/protocol/groups/worktree";
5
6
  import { Effect } from "effect";
6
7
  import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi";
7
8
  import { Api } from "../api";
8
- export const WorktreeHandler = HttpApiBuilder.group(Api, "server.worktree", (handlers) => handlers.handle("worktree.list", () => run((worktrees) => worktrees.list())).handle("worktree.create", (ctx) => run((worktrees) => worktrees.create(ctx.payload))).handle("worktree.remove", (ctx) => run((worktrees) => worktrees.remove(ctx.payload)).pipe(Effect.as(HttpApiSchema.NoContent.make()))).handle("worktree.refresh", () => run((worktrees) => worktrees.refresh()).pipe(Effect.as(HttpApiSchema.NoContent.make()))));
9
- function run(action) {
10
- return Effect.gen(function* () {
11
- const plugins = yield* Plugin.Service, worktrees = yield* Worktree.Service;
12
- yield* plugins.awaitActivation;
13
- return yield* action(worktrees);
14
- }).pipe(badRequest);
15
- }
9
+ export const WorktreeHandler = HttpApiBuilder.group(Api, "server.worktree", (handlers) => Effect.gen(function* () {
10
+ const worktrees = yield* Worktree.Service;
11
+ return handlers.handle("worktree.list", (ctx) => worktrees.list(ctx.query).pipe(Effect.catchTag("Project.NotFoundError", missingProject))).handle("worktree.create", (ctx) => worktrees.create(ctx.payload).pipe(badRequest)).handle("worktree.remove", (ctx) => worktrees.remove(ctx.payload).pipe(badRequest, Effect.as(HttpApiSchema.NoContent.make()))).handle("worktree.refresh", (ctx) => worktrees.refresh(ctx.payload).pipe(badRequest, Effect.as(HttpApiSchema.NoContent.make())));
12
+ }));
16
13
  function badRequest(effect) {
17
- return effect.pipe(Effect.mapError((error) => new WorktreeError({
14
+ return effect.pipe(Effect.catchTag("Project.NotFoundError", missingProject), Effect.mapError((error) => error instanceof ProjectNotFoundError ? error : new WorktreeError({
18
15
  name: "WorktreeError",
19
16
  data: {
20
17
  message: message(error),
@@ -22,6 +19,9 @@ function badRequest(effect) {
22
19
  }
23
20
  })));
24
21
  }
22
+ function missingProject(error) {
23
+ return Effect.fail(new ProjectNotFoundError({ projectID: error.projectID, message: `Project not found: ${error.projectID}` }));
24
+ }
25
25
  function message(error) {
26
26
  if (error instanceof Worktree.SourceDirectoryNotFoundError)
27
27
  return error.directory ? `Worktree source not found: ${error.directory}` : `Worktree source not found for project: ${error.projectID}`;
@@ -33,7 +33,5 @@ function message(error) {
33
33
  return `Invalid worktree directory: ${error.directory}`;
34
34
  if (error instanceof Worktree.StrategyUnavailableError)
35
35
  return `Worktree strategy unavailable: ${error.strategy}`;
36
- if (error instanceof Worktree.UnsupportedLocationError)
37
- return "Worktree operations only support local locations";
38
36
  return error.message;
39
37
  }
@@ -1,2 +1,2 @@
1
1
  import { Layer } from "effect";
2
- export declare const handlers: Layer.Layer<import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.generate"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.message"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.model"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.provider"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.session"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.permission"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.fs"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.form"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.command"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.skill"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.rpc"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.event"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.agent"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.plugin"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.health"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.server"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.debug"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.pty"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.experimental"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.shell"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.reference"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.location"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.integration"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.websearch"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.mcp"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.credential"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.project"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.worktree"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.vcs"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.migration"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.config"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.workspace">, never, import("@opencode/protocol/middleware/schema-error").SchemaErrorMiddleware | import("@opencode/protocol/middleware/authorization").Authorization | import("@opencode/util/global").Service | import("@opencode/core/bus").Service | import("@opencode/core/workspace").Service | import("@opencode/core/persistent-pty/index").Service | import("@opencode/core/location-service-map").Service | import("@opencode/core/session").Service | import("@opencode/core/instance").Service | import("./location").LocationMiddleware | import("./middleware/form-location").FormLocationMiddleware | import("./middleware/session-location").SessionLocationMiddleware | import("@opencode/core/pty/ticket").Service | import("@opencode/core/session/transfer").Service | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/database/database").Service> | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/permission/saved").Service> | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/plugin/update").Service> | import("effect/unstable/http/HttpRouter").Request<"Requires", import("./server-info").Service> | import("./pty-environment").Service | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/wellknown").Service> | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/project").Service>>;
2
+ export declare const handlers: Layer.Layer<import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.generate"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.message"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.model"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.provider"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.session"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.permission"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.fs"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.form"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.command"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.skill"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.rpc"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.event"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.agent"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.plugin"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.server"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.debug"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.pty"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.experimental"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.shell"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.reference"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.location"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.integration"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.websearch"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.mcp"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.credential"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.project"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.worktree"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.vcs"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.migration"> | import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.config">, never, import("@opencode/protocol/middleware/schema-error").SchemaErrorMiddleware | import("@opencode/protocol/middleware/authorization").Authorization | import("@opencode/util/global").Service | import("@opencode/core/bus").Service | import("@opencode/core/persistent-pty/index").Service | import("@opencode/core/location-service-map").Service | import("@opencode/core/session").Service | import("@opencode/core/instance").Service | import("@opencode/core/worktree").Service | import("./location").LocationMiddleware | import("./middleware/form-location").FormLocationMiddleware | import("./middleware/session-location").SessionLocationMiddleware | import("@opencode/core/pty/ticket").Service | import("@opencode/core/session/transfer").Service | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/database/database").Service> | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/permission/saved").Service> | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/plugin/update").Service> | import("effect/unstable/http/HttpRouter").Request<"Requires", import("./server-info").Service> | import("./pty-environment").Service | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/wellknown").Service> | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/credential").Service> | import("effect/unstable/http/HttpRouter").Request<"Requires", import("@opencode/core/project").Service>>;
package/dist/handlers.js CHANGED
@@ -13,7 +13,6 @@ import { RpcHandler } from "./handlers/rpc";
13
13
  import { EventHandler } from "./handlers/event";
14
14
  import { AgentHandler } from "./handlers/agent";
15
15
  import { PluginHandler } from "./handlers/plugin";
16
- import { HealthHandler } from "./handlers/health";
17
16
  import { ServerHandler } from "./handlers/server";
18
17
  import { DebugHandler } from "./handlers/debug";
19
18
  import { PtyHandler } from "./handlers/pty";
@@ -31,5 +30,4 @@ import { VcsHandler } from "./handlers/vcs";
31
30
  import { EventFeed } from "./event-feed";
32
31
  import { MigrationHandler } from "./handlers/migration";
33
32
  import { ConfigHandler } from "./handlers/config";
34
- import { WorkspaceHandler } from "./handlers/workspace";
35
- export const handlers = Layer.mergeAll(HealthHandler, ServerHandler, DebugHandler, MigrationHandler, LocationHandler, AgentHandler, PluginHandler, SessionHandler, MessageHandler, ModelHandler, GenerateHandler, ProviderHandler, IntegrationHandler, WebSearchHandler, McpHandler, CredentialHandler, ProjectHandler, FormHandler, PermissionHandler, FileSystemHandler, CommandHandler, SkillHandler, RpcHandler, EventHandler.pipe(Layer.provide(EventFeed.layer)), PtyHandler, PersistentPtyHandler, ShellHandler, ReferenceHandler, WorktreeHandler, WorkspaceHandler, VcsHandler, ConfigHandler);
33
+ export const handlers = Layer.mergeAll(ServerHandler, DebugHandler, MigrationHandler, LocationHandler, AgentHandler, PluginHandler, SessionHandler, MessageHandler, ModelHandler, GenerateHandler, ProviderHandler, IntegrationHandler, WebSearchHandler, McpHandler, CredentialHandler, ProjectHandler, FormHandler, PermissionHandler, FileSystemHandler, CommandHandler, SkillHandler, RpcHandler, EventHandler.pipe(Layer.provide(EventFeed.layer)), PtyHandler, PersistentPtyHandler, ShellHandler, ReferenceHandler, WorktreeHandler, VcsHandler, ConfigHandler);
package/dist/location.js CHANGED
@@ -2,7 +2,6 @@ import { Location } from "@opencode/core/location";
2
2
  import { LocationServiceMap } from "@opencode/core/location-services";
3
3
  import { AbsolutePath } from "@opencode/core/schema";
4
4
  import { Session } from "@opencode/core/session";
5
- import { Workspace } from "@opencode/core/workspace";
6
5
  import { InvalidRequestError } from "@opencode/protocol/errors";
7
6
  import { Effect, Layer, Schema } from "effect";
8
7
  import { HttpServerRequest } from "effect/unstable/http";
@@ -17,7 +16,6 @@ export function response(data) {
17
16
  return {
18
17
  location: new Location.Info({
19
18
  directory: location.directory,
20
- workspaceID: location.workspaceID,
21
19
  project: location.project
22
20
  }),
23
21
  data: yield* data
@@ -30,10 +28,9 @@ export const sessionInfo = Effect.fnUntraced(function* (sessions, sessionID) {
30
28
  return yield* sessions.get(id).pipe(Effect.catchTag("Session.NotFoundError", missingSession));
31
29
  });
32
30
  export function requestRef(request) {
33
- const query = new URL(request.url, "http://localhost").searchParams, workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"], directory = query.get("location[directory]") || (request.headers["x-opencode-directory"] ? decode(request.headers["x-opencode-directory"]) : process.cwd());
31
+ const directory = new URL(request.url, "http://localhost").searchParams.get("location[directory]") || (request.headers["x-opencode-directory"] ? decode(request.headers["x-opencode-directory"]) : process.cwd());
34
32
  return Location.Ref.make({
35
- directory: AbsolutePath.make(directory),
36
- workspaceID: workspaceID ? Workspace.ID.make(workspaceID) : void 0
33
+ directory: AbsolutePath.make(directory)
37
34
  });
38
35
  }
39
36
  function decode(input) {
package/dist/process.js CHANGED
@@ -27,8 +27,14 @@ export const start = Effect.fn("ServerProcess.start")(function* (options, lifecy
27
27
  const password = options.password;
28
28
  if (!password)
29
29
  return yield* Effect.fail(Error("Missing server password"));
30
- const hostname = options.hostname ?? "127.0.0.1", port = Option.fromNullishOr(options.port), shutdown = yield* Latch.make(), status = yield* Status.make(), bound = yield* listen({ hostname, port }), application = yield* Ref.make(Option.none());
31
- yield* bound.http.serve(dispatch(password, status, application, options.app?.version ?? "unknown").pipe(HttpMiddleware.cors({ allowedOrigins: (origin) => isAllowedCorsOrigin(origin, options), maxAge: 86400 })), errorResponseLogger).pipe(withoutParentSpan);
30
+ const hostname = options.hostname ?? "127.0.0.1", port = Option.fromNullishOr(options.port), shutdown = yield* Latch.make(), status = yield* Status.make(), bound = yield* listen({ hostname, port }), urls = () => {
31
+ const address = bound.server.address();
32
+ if (address === null || typeof address === "string")
33
+ return [];
34
+ const host = address.family === "IPv6" ? `[${address.address}]` : address.address;
35
+ return ServerInfo.connectionURLs(`http://${host}:${address.port}`, hostname);
36
+ }, application = yield* Ref.make(Option.none());
37
+ yield* bound.http.serve(dispatch(password, status, application, options.app?.version ?? "unknown", urls).pipe(HttpMiddleware.cors({ allowedOrigins: (origin) => isAllowedCorsOrigin(origin, options), maxAge: 86400 })), errorResponseLogger).pipe(withoutParentSpan);
32
38
  if (lifecycle)
33
39
  yield* lifecycle.onListen(bound.http.address, shutdown.open.pipe(Effect.asVoid)).pipe(Effect.flatMap((cleanup) => Effect.addFinalizer(() => Scope.close(bound.scope, Exit.void).pipe(Effect.andThen(cleanup)))), Effect.uninterruptible);
34
40
  const parentScope = yield* Scope.Scope, applicationScope = yield* Scope.fork(parentScope);
@@ -37,13 +43,7 @@ export const start = Effect.fn("ServerProcess.start")(function* (options, lifecy
37
43
  const context = yield* Layer.buildWithScope(createRoutes({
38
44
  ...options,
39
45
  password
40
- }, () => {
41
- const address = bound.server.address();
42
- if (address === null || typeof address === "string")
43
- return [];
44
- const host = address.family === "IPv6" ? `[${address.address}]` : address.address;
45
- return ServerInfo.connectionURLs(`http://${host}:${address.port}`, hostname);
46
- }).pipe(Layer.provideMerge(NodeHttpServer.layerHttpServices)), applicationScope);
46
+ }, urls).pipe(Layer.provideMerge(NodeHttpServer.layerHttpServices)), applicationScope);
47
47
  if (lifecycle)
48
48
  yield* installRestartContinuity(Context.get(context, SessionRestart.Service)).pipe(Effect.provideService(Scope.Scope, applicationScope));
49
49
  const app = Context.get(context, HttpRouter.HttpRouter).asHttpEffect().pipe(HttpMiddleware.compression(), Effect.provideService(HttpPlatform.HttpPlatform, Context.get(context, HttpPlatform.HttpPlatform)));
@@ -87,14 +87,14 @@ function addressInUse(error) {
87
87
  const cause = error.cause;
88
88
  return typeof cause === "object" && cause !== null && "code" in cause && cause.code === "EADDRINUSE";
89
89
  }
90
- function dispatch(password, status, application, version) {
90
+ function dispatch(password, status, application, version, urls) {
91
91
  const auth = ServerAuth.Config.of({ password: Option.some(password), username: "opencode" });
92
92
  return Effect.gen(function* () {
93
93
  const request = yield* HttpServerRequest.HttpServerRequest, url = new URL(request.url, "http://localhost");
94
- if (request.method === "GET" && url.pathname === "/api/health") {
94
+ if (request.method === "GET" && url.pathname === "/api/status") {
95
95
  if (!(yield* authorizedRequest(request, auth)))
96
96
  return unauthorized();
97
- return yield* healthResponse(status, version);
97
+ return yield* statusResponse(status, version, urls);
98
98
  }
99
99
  const state = yield* status.current, app = yield* Ref.get(application), ready = state.type === "ready" && Option.isSome(app);
100
100
  if ((!ready || !hasPtyConnectTicketURL(url) && !hasPersistentPtyConnectTicketURL(url)) && !(yield* authorizedRequest(request, auth)))
@@ -110,9 +110,9 @@ function unauthorized() {
110
110
  headers: { "www-authenticate": 'Basic realm="Secure Area"' }
111
111
  });
112
112
  }
113
- const healthResponse = Effect.fnUntraced(function* (status, version) {
113
+ const statusResponse = Effect.fnUntraced(function* (status, version, urls) {
114
114
  const state = yield* status.current;
115
- return HttpServerResponse.jsonUnsafe({ healthy: !0, version, pid: process.pid }, {
115
+ return HttpServerResponse.jsonUnsafe({ version, pid: process.pid, urls: urls() }, {
116
116
  status: state.type === "ready" ? 200 : state.type === "failed" ? 500 : 503,
117
117
  headers: state.type === "starting" || state.type === "stopping" ? { "retry-after": "1" } : void 0
118
118
  });
package/dist/routes.d.ts CHANGED
@@ -7,6 +7,7 @@ import { PermissionSaved } from "@opencode/core/permission/saved";
7
7
  import { PtyTicket } from "@opencode/core/pty/ticket";
8
8
  import { PersistentPty } from "@opencode/core/persistent-pty";
9
9
  import { Project } from "@opencode/core/project";
10
+ import { Worktree } from "@opencode/core/worktree";
10
11
  import { Session } from "@opencode/core/session";
11
12
  import { Instance } from "@opencode/core/instance/service";
12
13
  import { SessionTransfer } from "@opencode/core/session/transfer";
@@ -23,7 +24,7 @@ import { HttpRouter } from "effect/unstable/http";
23
24
  import { Layer } from "effect";
24
25
  import { PtyEnvironment } from "./pty-environment";
25
26
  import type { ServerOptions } from "./options";
26
- export declare function createRoutes(options?: ServerOptions, serviceURLs?: () => ReadonlyArray<string>, overrides?: LayerNode.Replacements): Layer.Layer<Global.Service | Database.Service | Bus.Service | import("effect/unstable/http/HttpClient").HttpClient | Project.Service | Credential.Service | WellKnown.Service | Workspace.Service | PermissionSaved.Service | PersistentPty.Service | LocationServiceMap.Service | SdkPlugins.Service | PluginUpdate.Service | Session.Service | Job.Service | Instance.Service | SessionRestart.Service | PtyTicket.Service | SessionTransfer.Service | LocationActivity.Service | PtyEnvironment.Service | HttpRouter.HttpRouter, Error | PersistentPty.UnavailableError, import("effect/FileSystem").FileSystem | import("effect/Path").Path | import("effect/unstable/http/Etag").Generator | import("effect/unstable/http/HttpPlatform").HttpPlatform>;
27
+ export declare function createRoutes(options?: ServerOptions, serviceURLs?: () => ReadonlyArray<string>, overrides?: LayerNode.Replacements): Layer.Layer<Global.Service | Database.Service | Bus.Service | import("effect/unstable/http/HttpClient").HttpClient | Project.Service | Credential.Service | WellKnown.Service | Workspace.Service | PermissionSaved.Service | PersistentPty.Service | LocationServiceMap.Service | SdkPlugins.Service | PluginUpdate.Service | Session.Service | Job.Service | Instance.Service | Worktree.Service | SessionRestart.Service | PtyTicket.Service | SessionTransfer.Service | LocationActivity.Service | PtyEnvironment.Service | HttpRouter.HttpRouter, Error | PersistentPty.UnavailableError, import("effect/FileSystem").FileSystem | import("effect/Path").Path | import("effect/unstable/http/Etag").Generator | import("effect/unstable/http/HttpPlatform").HttpPlatform>;
27
28
  type InstanceNode = (replacements: () => LayerNode.Replacements) => LayerNode.Provider<Instance.Service, never, typeof Node.tags.values.global>;
28
- export declare function createEmbeddedRoutes(options?: ServerOptions, overrides?: LayerNode.Replacements, instances?: InstanceNode): Layer.Layer<Global.Service | Database.Service | Bus.Service | import("effect/unstable/http/HttpClient").HttpClient | Project.Service | Credential.Service | WellKnown.Service | Workspace.Service | PermissionSaved.Service | PersistentPty.Service | LocationServiceMap.Service | SdkPlugins.Service | PluginUpdate.Service | Session.Service | Job.Service | Instance.Service | SessionRestart.Service | PtyTicket.Service | SessionTransfer.Service | LocationActivity.Service | PtyEnvironment.Service | HttpRouter.HttpRouter, Error | PersistentPty.UnavailableError, import("effect/FileSystem").FileSystem | import("effect/Path").Path | import("effect/unstable/http/Etag").Generator | import("effect/unstable/http/HttpPlatform").HttpPlatform>;
29
+ export declare function createEmbeddedRoutes(options?: ServerOptions, overrides?: LayerNode.Replacements, instances?: InstanceNode): Layer.Layer<Global.Service | Database.Service | Bus.Service | import("effect/unstable/http/HttpClient").HttpClient | Project.Service | Credential.Service | WellKnown.Service | Workspace.Service | PermissionSaved.Service | PersistentPty.Service | LocationServiceMap.Service | SdkPlugins.Service | PluginUpdate.Service | Session.Service | Job.Service | Instance.Service | Worktree.Service | SessionRestart.Service | PtyTicket.Service | SessionTransfer.Service | LocationActivity.Service | PtyEnvironment.Service | HttpRouter.HttpRouter, Error | PersistentPty.UnavailableError, import("effect/FileSystem").FileSystem | import("effect/Path").Path | import("effect/unstable/http/Etag").Generator | import("effect/unstable/http/HttpPlatform").HttpPlatform>;
29
30
  export {};
package/dist/routes.js CHANGED
@@ -14,6 +14,7 @@ import { PermissionSaved } from "@opencode/core/permission/saved";
14
14
  import { PtyTicket } from "@opencode/core/pty/ticket";
15
15
  import { PersistentPty } from "@opencode/core/persistent-pty";
16
16
  import { Project } from "@opencode/core/project";
17
+ import { Worktree } from "@opencode/core/worktree";
17
18
  import { Session } from "@opencode/core/session";
18
19
  import { Instance } from "@opencode/core/instance/service";
19
20
  import { SessionTransfer } from "@opencode/core/session/transfer";
@@ -53,6 +54,7 @@ const applicationServiceNodes = [
53
54
  httpClient,
54
55
  Job.node,
55
56
  Project.node,
57
+ Worktree.node,
56
58
  Session.node,
57
59
  Instance.node,
58
60
  SessionTransfer.node,
@@ -110,7 +112,7 @@ function makeRoutes(auth, options, serviceURLs, overrides, instances) {
110
112
  const { simulationReplacements } = yield* Effect.promise(() => import("@opencode/simulation/backend")), simulation = yield* simulationReplacements({ version: App.make(options.app).version });
111
113
  return build([...overrides, ...simulation]);
112
114
  })) : build(overrides)).pipe(Layer.flatMap((context) => {
113
- const services = Layer.succeedContext(context), requestServices = Layer.merge(Layer.succeedContext(Context.pick(Database.Service, PermissionSaved.Service, PluginUpdate.Service, Project.Service, WellKnown.Service)(context)), ServerInfo.layer(serviceURLs, options.app)), api = HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(Layer.provide(handlers.pipe(Layer.provide(services), Layer.provide(Layer.succeed(CorsConfig, options)))), Layer.provide(formLocationLayer), Layer.provide(sessionLocationLayer), Layer.provide(layer), Layer.provide(authorizationLayer), Layer.provide(schemaErrorLayer), Layer.provide(auth), HttpRouter.provideRequest(requestServices), Layer.provideMerge(services), Layer.provideMerge(HttpRouter.layer));
115
+ const services = Layer.succeedContext(context), requestServices = Layer.merge(Layer.succeedContext(Context.pick(Database.Service, Credential.Service, PermissionSaved.Service, PluginUpdate.Service, Project.Service, WellKnown.Service)(context)), ServerInfo.layer(serviceURLs, options.app)), api = HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(Layer.provide(handlers.pipe(Layer.provide(services), Layer.provide(Layer.succeed(CorsConfig, options)))), Layer.provide(formLocationLayer), Layer.provide(sessionLocationLayer), Layer.provide(layer), Layer.provide(authorizationLayer), Layer.provide(schemaErrorLayer), Layer.provide(auth), HttpRouter.provideRequest(requestServices), Layer.provideMerge(services), Layer.provideMerge(HttpRouter.layer));
114
116
  return Layer.merge(api, V1Migration.layer.pipe(Layer.provide(services)));
115
117
  }));
116
118
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@opencode/server",
4
- "version": "2.0.2",
4
+ "version": "2.0.4",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -30,17 +30,17 @@
30
30
  "dependencies": {
31
31
  "@effect/platform-node": "4.0.0-rc.112",
32
32
  "@effect/platform-node-shared": "4.0.0-rc.112",
33
- "@opencode/core": "2.0.2",
34
- "@opencode/protocol": "2.0.2",
35
- "@opencode/schema": "2.0.2",
36
- "@opencode/simulation": "2.0.2",
37
- "@opencode/util": "2.0.2",
33
+ "@opencode/core": "2.0.4",
34
+ "@opencode/protocol": "2.0.4",
35
+ "@opencode/schema": "2.0.4",
36
+ "@opencode/simulation": "2.0.4",
37
+ "@opencode/util": "2.0.4",
38
38
  "drizzle-orm": "1.0.0-rc.5-169397b",
39
39
  "effect": "4.0.0-rc.112"
40
40
  },
41
41
  "devDependencies": {
42
- "@opencode/client": "2.0.2",
43
- "@opencode/plugin": "2.0.2",
42
+ "@opencode/client": "2.0.4",
43
+ "@opencode/plugin": "2.0.4",
44
44
  "@tsconfig/bun": "1.0.9",
45
45
  "@types/bun": "1.4.0",
46
46
  "@typescript/native-preview": "7.0.0-dev.20251207.1"
@@ -1,2 +0,0 @@
1
- import { ServerInfo } from "../server-info";
2
- export declare const HealthHandler: import("effect/Layer").Layer<import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.health">, never, import("@opencode/protocol/middleware/schema-error").SchemaErrorMiddleware | import("@opencode/protocol/middleware/authorization").Authorization | import("effect/unstable/http/HttpRouter").Request<"Requires", ServerInfo.Service>>;
@@ -1,11 +0,0 @@
1
- import { Effect } from "effect";
2
- import { HttpApiBuilder } from "effect/unstable/httpapi";
3
- import { Api } from "../api";
4
- import { ServerInfo } from "../server-info";
5
- export const HealthHandler = HttpApiBuilder.group(Api, "server.health", (handlers) => handlers.handle("health.get", () => Effect.gen(function* () {
6
- return {
7
- healthy: !0,
8
- version: (yield* ServerInfo.Service).app.version ?? "unknown",
9
- pid: process.pid ?? 0
10
- };
11
- })));
@@ -1,2 +0,0 @@
1
- import { Workspace } from "@opencode/core/workspace";
2
- export declare const WorkspaceHandler: import("effect/Layer").Layer<import("effect/unstable/httpapi/HttpApiGroup").Service<"server", "server.workspace">, never, import("@opencode/protocol/middleware/schema-error").SchemaErrorMiddleware | import("@opencode/protocol/middleware/authorization").Authorization | Workspace.Service>;
@@ -1,20 +0,0 @@
1
- import { Workspace } from "@opencode/core/workspace";
2
- import { ConflictError, ProviderNotFoundError, UnknownError } from "@opencode/protocol/errors";
3
- import { Effect } from "effect";
4
- import { HttpApiBuilder } from "effect/unstable/httpapi";
5
- import { Api } from "../api";
6
- export const WorkspaceHandler = HttpApiBuilder.group(Api, "server.workspace", (handlers) => Effect.gen(function* () {
7
- const workspace = yield* Workspace.Service;
8
- return handlers.handle("workspace.create", (ctx) => workspace.create(ctx.payload).pipe(Effect.map((workspaceID) => ({ data: workspaceID })), Effect.catchTags({
9
- "Workspace.CreateConflict": (error) => new ConflictError({
10
- resource: error.workspaceID,
11
- message: `Workspace ${error.workspaceID} already uses provider ${error.existingProvider}, not ${error.provider}`
12
- }),
13
- "WorkspaceDriver.ProviderNotFound": (error) => new ProviderNotFoundError({
14
- providerID: error.provider,
15
- message: `Workspace provider not found: ${error.provider}`
16
- })
17
- }))).handle("workspace.destroy", (ctx) => workspace.destroy(ctx.params.workspaceID).pipe(Effect.mapError((error) => new UnknownError({
18
- message: error._tag === "WorkspaceDriver.ProviderNotFound" ? `Workspace provider not found: ${error.provider}` : error.message ?? "Workspace provider failed to destroy the workspace"
19
- }))));
20
- }));