@opencode-ai/client 0.0.0-next-15548 → 0.0.0-next-15555

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.
@@ -6,8 +6,16 @@ type EffectValue<A> = A extends Effect.Effect<infer Success, any, any> ? Success
6
6
  type StreamValue<A> = A extends Stream.Stream<infer Success, any, any> ? Success : never;
7
7
  export type Endpoint0_0Output = EffectValue<ReturnType<RawClient["server.health"]["health.get"]>>;
8
8
  export type HealthGetOperation<E = never> = () => Effect.Effect<Endpoint0_0Output, E>;
9
+ type Endpoint0_1Request = Parameters<RawClient["server.health"]["health.stop"]>[0];
10
+ export type Endpoint0_1Input = {
11
+ readonly instanceID: Endpoint0_1Request["payload"]["instanceID"];
12
+ readonly targetVersion?: Endpoint0_1Request["payload"]["targetVersion"];
13
+ };
14
+ export type Endpoint0_1Output = EffectValue<ReturnType<RawClient["server.health"]["health.stop"]>>;
15
+ export type HealthStopOperation<E = never> = (input: Endpoint0_1Input) => Effect.Effect<Endpoint0_1Output, E>;
9
16
  export interface HealthApi<E = never> {
10
17
  readonly get: HealthGetOperation<E>;
18
+ readonly stop: HealthStopOperation<E>;
11
19
  }
12
20
  export type Endpoint1_0Output = EffectValue<ReturnType<RawClient["server.server"]["server.get"]>>;
13
21
  export type ServerGetOperation<E = never> = () => Effect.Effect<Endpoint1_0Output, E>;
@@ -5,6 +5,11 @@ import { HttpApiClient } from "effect/unstable/httpapi";
5
5
  import { ClientApi } from "../../contract";
6
6
  import { ClientError } from "./client-error";
7
7
  type RawClient = HttpApiClient.ForApi<typeof ClientApi>;
8
+ type Endpoint0_1Request = Parameters<RawClient["server.health"]["health.stop"]>[0];
9
+ type Endpoint0_1Input = {
10
+ readonly instanceID: Endpoint0_1Request["payload"]["instanceID"];
11
+ readonly targetVersion?: Endpoint0_1Request["payload"]["targetVersion"];
12
+ };
8
13
  type Endpoint2_0Request = Parameters<RawClient["server.location"]["location.get"]>[0];
9
14
  type Endpoint2_0Input = {
10
15
  readonly location?: Endpoint2_0Request["query"]["location"];
@@ -506,6 +511,22 @@ export declare const make: (options?: {
506
511
  readonly healthy: true;
507
512
  readonly version: string;
508
513
  readonly pid: number;
514
+ readonly status: {
515
+ readonly type: "starting";
516
+ } | {
517
+ readonly type: "ready";
518
+ } | {
519
+ readonly type: "stopping";
520
+ readonly targetVersion?: string | undefined;
521
+ } | {
522
+ readonly type: "failed";
523
+ readonly message: string;
524
+ readonly action: string;
525
+ };
526
+ readonly instanceID?: string | undefined;
527
+ }, Schema.SchemaError | import("@opencode-ai/protocol/errors").InvalidRequestError | import("@opencode-ai/protocol/errors").UnauthorizedError | HttpClientError.HttpClientError | ClientError, never>;
528
+ stop: (input: Endpoint0_1Input) => Effect.Effect<{
529
+ readonly accepted: boolean;
509
530
  }, Schema.SchemaError | import("@opencode-ai/protocol/errors").InvalidRequestError | import("@opencode-ai/protocol/errors").UnauthorizedError | HttpClientError.HttpClientError | ClientError, never>;
510
531
  };
511
532
  server: {
@@ -9,7 +9,8 @@ const mapClientError = (error) => HttpClientError.isHttpClientError(error) || Sc
9
9
  ? new ClientError({ cause: error })
10
10
  : error;
11
11
  const Endpoint0_0 = (raw) => () => raw["health.get"]({}).pipe(Effect.mapError(mapClientError));
12
- const adaptGroup0 = (raw) => ({ get: Endpoint0_0(raw) });
12
+ const Endpoint0_1 = (raw) => (input) => raw["health.stop"]({ payload: { instanceID: input["instanceID"], targetVersion: input["targetVersion"] } }).pipe(Effect.mapError(mapClientError));
13
+ const adaptGroup0 = (raw) => ({ get: Endpoint0_0(raw), stop: Endpoint0_1(raw) });
13
14
  const Endpoint1_0 = (raw) => () => raw["server.get"]({}).pipe(Effect.mapError(mapClientError));
14
15
  const adaptGroup1 = (raw) => ({ get: Endpoint1_0(raw) });
15
16
  const Endpoint2_0 = (raw) => (input) => raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError));
@@ -1,3 +1,4 @@
1
+ import { ServiceStatus } from "@opencode-ai/protocol/groups/health";
1
2
  import { Effect, FileSystem, Schema } from "effect";
2
3
  export type Endpoint = {
3
4
  readonly url: string;
@@ -15,7 +16,23 @@ export type Options = {
15
16
  export type StartReason = "missing" | "version-mismatch";
16
17
  export type StartOptions = Options & {
17
18
  readonly onStart?: (reason: StartReason, existing?: Info) => void;
19
+ readonly onStatus?: (status: Status) => void;
18
20
  };
21
+ export type Status = {
22
+ readonly type: "missing";
23
+ } | {
24
+ readonly type: "unreachable";
25
+ } | {
26
+ readonly type: "unresponsive";
27
+ } | (ServiceStatus.State & {
28
+ readonly version?: string;
29
+ });
30
+ declare const FailedError_base: Schema.Class<FailedError, Schema.TaggedStruct<"ServiceFailedError", {
31
+ readonly message: Schema.String;
32
+ readonly action: Schema.String;
33
+ }>, import("effect/Cause").YieldableError>;
34
+ export declare class FailedError extends FailedError_base {
35
+ }
19
36
  export declare const discover: (options?: Options | undefined) => Effect.Effect<{
20
37
  url: string;
21
38
  auth: {
@@ -24,16 +41,15 @@ export declare const discover: (options?: Options | undefined) => Effect.Effect<
24
41
  password: string;
25
42
  } | undefined;
26
43
  } | undefined, never, FileSystem.FileSystem>;
27
- export declare const start: (options?: StartOptions | undefined) => Effect.Effect<{
28
- url: string;
29
- auth: {
30
- type: "basic";
31
- username: string;
32
- password: string;
33
- } | undefined;
34
- }, Error, FileSystem.FileSystem>;
35
- export declare const stop: (options?: Options | undefined) => Effect.Effect<void, Error, FileSystem.FileSystem>;
36
- export declare function headers(endpoint: Endpoint): RequestInit["headers"];
44
+ export declare const status: (options?: Options | undefined) => Effect.Effect<Status, never, FileSystem.FileSystem>;
45
+ export declare const start: (options?: StartOptions | undefined) => Effect.Effect<Endpoint, Error, FileSystem.FileSystem>;
46
+ export type StopMetadata = {
47
+ readonly targetVersion?: string;
48
+ };
49
+ export declare const stop: (options?: Options | undefined, metadata?: StopMetadata | undefined) => Effect.Effect<void, Error, FileSystem.FileSystem>;
50
+ export declare function headers(endpoint: Endpoint): {
51
+ authorization: string;
52
+ } | undefined;
37
53
  export declare const Info: Schema.Struct<{
38
54
  readonly id: Schema.optional<Schema.String>;
39
55
  readonly version: Schema.optional<Schema.String>;
@@ -1,55 +1,149 @@
1
+ import { ServiceStatus } from "@opencode-ai/protocol/groups/health";
1
2
  import { Effect, FileSystem, Option, Schedule, Schema } from "effect";
2
3
  import { spawn } from "node:child_process";
3
4
  import { homedir } from "node:os";
4
5
  import { join } from "node:path";
6
+ export class FailedError extends Schema.TaggedErrorClass()("ServiceFailedError", {
7
+ message: Schema.String,
8
+ action: Schema.String,
9
+ }) {
10
+ }
5
11
  // Read-only lookup: registration file plus health check and version gate.
6
12
  // Never spawns; escalation to start() is the caller's policy.
7
13
  export const discover = Effect.fn("service.discover")(function* (options = {}) {
8
14
  return (yield* discoverLocal(options))?.endpoint;
9
15
  });
16
+ export const status = Effect.fn("service.status")(function* (options = {}) {
17
+ const result = yield* registered(options.file, true);
18
+ if (result.info === undefined)
19
+ return { type: "missing" };
20
+ if (result.service === undefined)
21
+ return { type: "unreachable" };
22
+ return publicStatus(result.service);
23
+ });
24
+ function publicStatus(service) {
25
+ return { ...service.status, version: service.version };
26
+ }
10
27
  const discoverLocal = Effect.fnUntraced(function* (options) {
11
- const info = yield* read(options.file);
12
- if (info === undefined)
28
+ const found = (yield* registered(options.file)).service;
29
+ if (found?.status.type !== "ready")
13
30
  return undefined;
14
- if (options.version !== undefined && info.version !== options.version)
31
+ if (options.version !== undefined && found.version !== options.version)
15
32
  return undefined;
16
- return yield* probe(info, options.version);
33
+ return found;
17
34
  });
18
35
  // Idempotent ensure-running: reuses a healthy compatible server, replaces a
19
- // version-mismatched one, and otherwise spawns the service command detached.
36
+ // version-mismatched one, and otherwise spawns small contenders until a server
37
+ // becomes discoverable. A contender is never killed merely for slow startup.
20
38
  export const start = Effect.fn("service.start")(function* (options = {}) {
21
- const compatible = yield* discover(options);
22
- if (compatible !== undefined)
23
- return compatible;
24
- const existing = yield* find(options);
25
- if (existing?.version !== undefined && (options.version === undefined || existing.version === options.version))
26
- return existing.endpoint;
27
- yield* Effect.sync(() => options.onStart?.(existing === undefined ? "missing" : "version-mismatch", existing?.info));
28
- if (existing !== undefined)
29
- yield* kill(existing.info, options).pipe(Effect.ignore);
30
- const [command, ...args] = options.command ?? ["opencode", "serve", "--service"];
31
- if (command === undefined)
32
- return yield* Effect.fail(new Error("Missing service command"));
33
- const child = yield* Effect.try({
34
- try: () => {
35
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
36
- child.unref();
37
- return child;
38
- },
39
- catch: (cause) => new Error("Failed to start server", { cause }),
39
+ const contenders = new Set();
40
+ let announced = false;
41
+ let reported;
42
+ let lastSpawn = 0;
43
+ let spawnDelay = 5_000;
44
+ let ownerHeld = false;
45
+ const announce = (reason, existing) => Effect.sync(() => {
46
+ if (announced)
47
+ return;
48
+ announced = true;
49
+ options.onStart?.(reason, existing);
50
+ });
51
+ const spawnContender = Effect.gen(function* () {
52
+ const [command, ...args] = options.command ?? ["opencode", "serve", "--service"];
53
+ if (command === undefined)
54
+ return yield* Effect.fail(new Error("Missing service command"));
55
+ return yield* Effect.try({
56
+ try: () => {
57
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
58
+ let error;
59
+ child.once("error", (cause) => {
60
+ error = new Error("Failed to start server", { cause });
61
+ });
62
+ child.unref();
63
+ return { child, error: () => error };
64
+ },
65
+ catch: (cause) => new Error("Failed to start server", { cause }),
66
+ });
40
67
  });
41
- return yield* discoverLocal(options).pipe(Effect.flatMap((found) => found === undefined ? Effect.fail(new Error("Server is not ready")) : Effect.succeed(found)), Effect.retry(poll), Effect.tap((found) => found.info.pid === child.pid
42
- ? Effect.void
43
- : Effect.sync(() => {
44
- child.kill("SIGTERM");
45
- })), Effect.map((found) => found.endpoint), Effect.tapError(() => Effect.try({ try: () => child.kill("SIGTERM"), catch: () => undefined }).pipe(Effect.ignore)), Effect.mapError(() => new Error("Failed to start server")));
68
+ const found = yield* Effect.gen(function* () {
69
+ const registration = yield* registered(options.file, true);
70
+ const info = registration.info;
71
+ const service = registration.service;
72
+ const current = service === undefined ? { type: info === undefined ? "missing" : "unreachable" } : publicStatus(service);
73
+ const next = ownerHeld && service === undefined ? { type: "unresponsive" } : current;
74
+ yield* Effect.sync(() => {
75
+ if (sameStatus(reported, next))
76
+ return;
77
+ reported = next;
78
+ options.onStatus?.(next);
79
+ });
80
+ if (service !== undefined) {
81
+ ownerHeld = false;
82
+ spawnDelay = 5_000;
83
+ const compatible = !service.legacy && (options.version === undefined || service.version === options.version);
84
+ if (compatible && service.status.type === "ready")
85
+ return Option.some(service);
86
+ if (compatible && service.status.type === "failed")
87
+ return yield* new FailedError({ message: service.status.message, action: service.status.action });
88
+ if (compatible || service.status.type === "stopping")
89
+ return Option.none();
90
+ yield* announce("version-mismatch", service.info);
91
+ yield* kill(service, options, options.version).pipe(Effect.ignore);
92
+ lastSpawn = 0;
93
+ return Option.none();
94
+ }
95
+ else if (lastSpawn === 0 && info !== undefined)
96
+ lastSpawn = Date.now();
97
+ const failure = [...contenders].map(contenderFailure).find((error) => error !== undefined);
98
+ if (failure !== undefined)
99
+ return yield* Effect.fail(failure);
100
+ const finished = [...contenders].filter(contenderFinished);
101
+ if (finished.some((item) => item.child.exitCode === 0)) {
102
+ ownerHeld = true;
103
+ spawnDelay = Math.min(spawnDelay * 2, 30_000);
104
+ }
105
+ finished.forEach((item) => contenders.delete(item));
106
+ // Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
107
+ if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
108
+ yield* announce("missing");
109
+ contenders.add(yield* spawnContender);
110
+ lastSpawn = Date.now();
111
+ }
112
+ return Option.none();
113
+ }).pipe(Effect.repeat({ until: Option.isSome, schedule: Schedule.spaced("1 second") }));
114
+ return Option.getOrThrow(found).endpoint;
46
115
  });
47
- export const stop = Effect.fn("service.stop")(function* (options = {}) {
48
- const fs = yield* FileSystem.FileSystem;
116
+ function sameStatus(left, right) {
117
+ if (left?.type !== right.type)
118
+ return false;
119
+ if (right.type === "failed")
120
+ return (left.type === "failed" &&
121
+ left.version === right.version &&
122
+ left.message === right.message &&
123
+ left.action === right.action);
124
+ if (right.type === "stopping")
125
+ return left.type === "stopping" && left.version === right.version && left.targetVersion === right.targetVersion;
126
+ if (right.type === "starting" || right.type === "ready")
127
+ return left.type === right.type && left.version === right.version;
128
+ return true;
129
+ }
130
+ function contenderFailure(contender) {
131
+ const error = contender.error();
132
+ if (error !== undefined)
133
+ return error;
134
+ if (contender.child.exitCode !== null && contender.child.exitCode !== 0)
135
+ return new Error(`Server process exited with code ${contender.child.exitCode}`);
136
+ if (contender.child.signalCode !== null)
137
+ return new Error(`Server process terminated by ${contender.child.signalCode}`);
138
+ return undefined;
139
+ }
140
+ function contenderFinished(contender) {
141
+ return contender.error() !== undefined || contender.child.exitCode !== null || contender.child.signalCode !== null;
142
+ }
143
+ export const stop = Effect.fn("service.stop")(function* (options = {}, metadata = {}) {
49
144
  const existing = yield* find(options);
50
145
  if (existing !== undefined)
51
- yield* kill(existing.info, options);
52
- yield* fs.remove(options.file ?? fallback()).pipe(Effect.ignore);
146
+ yield* kill(existing, options, metadata.targetVersion);
53
147
  });
54
148
  function fallback() {
55
149
  const state = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state");
@@ -68,7 +162,7 @@ export const Info = Schema.Struct({
68
162
  password: Schema.optional(Schema.String),
69
163
  });
70
164
  const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info));
71
- const decodeHealth = Schema.decodeUnknownOption(Schema.Struct({ healthy: Schema.Literal(true), version: Schema.String, pid: Schema.Int }));
165
+ const decodeHealth = Schema.decodeUnknownOption(ServiceStatus.Health);
72
166
  const decodeLegacyHealth = Schema.decodeUnknownOption(Schema.Struct({ healthy: Schema.Literal(true) }));
73
167
  // A missing or corrupt file means no valid info; callers treat both
74
168
  // the same (the registering server self-evicts, clients rediscover).
@@ -79,7 +173,7 @@ const read = Effect.fnUntraced(function* (file) {
79
173
  return undefined;
80
174
  return yield* decode(text.value).pipe(Effect.option, Effect.map(Option.getOrUndefined));
81
175
  });
82
- const probe = Effect.fnUntraced(function* (info, version, allowLegacy = false) {
176
+ const probe = Effect.fnUntraced(function* (info, allowLegacy = false) {
83
177
  const endpoint = {
84
178
  url: info.url,
85
179
  auth: info.password === undefined
@@ -90,7 +184,7 @@ const probe = Effect.fnUntraced(function* (info, version, allowLegacy = false) {
90
184
  headers: headers(endpoint),
91
185
  signal: AbortSignal.timeout(2_000),
92
186
  })).pipe(Effect.option, Effect.map(Option.getOrUndefined));
93
- if (response === undefined || !response.ok)
187
+ if (response === undefined)
94
188
  return undefined;
95
189
  const body = yield* Effect.tryPromise(() => response.json()).pipe(Effect.option, Effect.map(Option.getOrUndefined));
96
190
  const health = decodeHealth(body);
@@ -99,25 +193,35 @@ const probe = Effect.fnUntraced(function* (info, version, allowLegacy = false) {
99
193
  return undefined;
100
194
  if (info.version !== undefined && health.value.version !== info.version)
101
195
  return undefined;
102
- if (version !== undefined && health.value.version !== version)
196
+ if (info.id !== undefined && health.value.instanceID !== undefined && health.value.instanceID !== info.id)
103
197
  return undefined;
104
- return { info, endpoint, version: health.value.version };
198
+ return {
199
+ info,
200
+ endpoint,
201
+ version: health.value.version,
202
+ status: health.value.status,
203
+ legacy: false,
204
+ };
105
205
  }
106
206
  if (!allowLegacy ||
107
207
  Option.isNone(decodeLegacyHealth(body)) ||
108
208
  (typeof body === "object" && body !== null && ("version" in body || "pid" in body)))
109
209
  return undefined;
110
- return { info, endpoint };
210
+ return { info, endpoint, status: { type: "ready" }, legacy: true };
211
+ });
212
+ const registered = Effect.fnUntraced(function* (file, allowLegacy = false) {
213
+ const info = yield* read(file);
214
+ if (info === undefined)
215
+ return { info: undefined, service: undefined };
216
+ return { info, service: yield* probe(info, allowLegacy) };
111
217
  });
112
- // Health-checked lookup without the version gate: lifecycle operations must be
218
+ // Health-checked lookup without the version gate: status operations must be
113
219
  // able to see (and replace or stop) a server from a different version.
114
220
  const find = Effect.fnUntraced(function* (options) {
115
- const info = yield* read(options.file);
116
- if (info === undefined)
117
- return undefined;
118
- return yield* probe(info, undefined, true);
221
+ return (yield* registered(options.file, true)).service;
119
222
  });
120
- // 50ms cadence bounded at ~5s, shared by stop escalation and start readiness.
223
+ // 50ms cadence bounded at ~5s, shared by stop escalation and each start
224
+ // discovery window.
121
225
  const poll = Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)));
122
226
  const signal = (pid, name) => Effect.try({ try: () => process.kill(pid, name), catch: (cause) => cause }).pipe(Effect.ignore);
123
227
  const stopped = Effect.fnUntraced(function* (pid) {
@@ -129,20 +233,43 @@ const stopped = Effect.fnUntraced(function* (pid) {
129
233
  function same(left, right) {
130
234
  return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid;
131
235
  }
132
- const kill = Effect.fnUntraced(function* (info, options) {
133
- // A stale registration may point at a PID that has since been reused by
134
- // another process. Only signal the PID after authenticating the server.
135
- const current = yield* find(options);
136
- if (current === undefined || !same(current.info, info))
236
+ const kill = Effect.fnUntraced(function* (service, options, targetVersion) {
237
+ const requested = yield* requestStop(service, targetVersion);
238
+ if (requested === "rejected")
137
239
  return;
138
- yield* signal(info.pid, "SIGTERM");
139
- const done = yield* stopped(info.pid).pipe(Effect.retry(poll), Effect.option);
240
+ if (requested === "unsupported") {
241
+ // A stale registration may point at a reused PID. Authenticate again
242
+ // immediately before the legacy signal fallback.
243
+ const current = yield* find(options);
244
+ if (current === undefined || !same(current.info, service.info))
245
+ return;
246
+ yield* signal(service.info.pid, "SIGTERM");
247
+ }
248
+ const done = yield* stopped(service.info.pid).pipe(Effect.retry(poll), Effect.option);
140
249
  if (Option.isSome(done))
141
250
  return;
142
251
  const latest = yield* find(options);
143
- if (latest === undefined || !same(latest.info, info))
252
+ if (latest === undefined || !same(latest.info, service.info))
144
253
  return;
145
- yield* signal(info.pid, "SIGKILL");
146
- yield* stopped(info.pid).pipe(Effect.retry(poll));
254
+ yield* signal(service.info.pid, "SIGKILL");
255
+ yield* stopped(service.info.pid).pipe(Effect.retry(poll));
256
+ });
257
+ const decodeStopResponse = Schema.decodeUnknownOption(ServiceStatus.StopResponse);
258
+ const requestStop = Effect.fnUntraced(function* (service, targetVersion) {
259
+ if (service.info.id === undefined || service.legacy)
260
+ return "unsupported";
261
+ const response = yield* Effect.tryPromise(() => fetch(new URL("/api/service/stop", service.info.url), {
262
+ method: "POST",
263
+ headers: { ...headers(service.endpoint), "content-type": "application/json" },
264
+ body: JSON.stringify({ instanceID: service.info.id, targetVersion }),
265
+ signal: AbortSignal.timeout(2_000),
266
+ })).pipe(Effect.option, Effect.map(Option.getOrUndefined));
267
+ if (response === undefined || response.status === 404 || response.status === 405)
268
+ return "unsupported";
269
+ const body = yield* Effect.tryPromise(() => response.json()).pipe(Effect.option, Effect.map(Option.getOrUndefined));
270
+ const decoded = decodeStopResponse(body);
271
+ if (!response.ok || Option.isNone(decoded) || !decoded.value.accepted)
272
+ return "rejected";
273
+ return "accepted";
147
274
  });
148
275
  export * as Service from "./service.js";
@@ -1,4 +1,4 @@
1
- import type { HealthGetOutput, ServerGetOutput, LocationGetInput, LocationGetOutput, AgentListInput, AgentListOutput, PluginListInput, PluginListOutput, SessionListInput, SessionCreateInput, SessionGetInput, SessionRemoveInput, SessionForkInput, SessionSwitchAgentInput, SessionSwitchModelInput, SessionRenameInput, SessionMoveInput, SessionPromptInput, SessionCommandInput, SessionSkillInput, SessionSyntheticInput, SessionShellInput, SessionCompactInput, SessionWaitInput, SessionRevertStageInput, SessionRevertClearInput, SessionRevertCommitInput, SessionContextInput, SessionPendingListInput, SessionInstructionsEntryListInput, SessionInstructionsEntryPutInput, SessionInstructionsEntryRemoveInput, SessionLogInput, SessionLogOutput, SessionInterruptInput, SessionBackgroundInput, SessionMessageInput, MessageListInput, ModelListInput, ModelListOutput, ModelDefaultInput, ModelDefaultOutput, GenerateTextInput, ProviderListInput, ProviderListOutput, ProviderGetInput, ProviderGetOutput, IntegrationListInput, IntegrationListOutput, IntegrationGetInput, IntegrationGetOutput, IntegrationConnectKeyInput, IntegrationConnectOauthInput, IntegrationConnectOauthOutput, IntegrationAttemptStatusInput, IntegrationAttemptStatusOutput, IntegrationAttemptCompleteInput, IntegrationAttemptCancelInput, McpListInput, McpListOutput, McpResourceCatalogInput, McpResourceCatalogOutput, CredentialUpdateInput, CredentialRemoveInput, ProjectListOutput, ProjectCurrentInput, ProjectDirectoriesInput, FormRequestListInput, FormRequestListOutput, FormListInput, FormCreateInput, FormGetInput, FormStateInput, FormReplyInput, FormCancelInput, PermissionRequestListInput, PermissionRequestListOutput, PermissionSavedListInput, PermissionSavedRemoveInput, PermissionCreateInput, PermissionListInput, PermissionGetInput, PermissionReplyInput, FileReadInput, FileReadOutput, FileListInput, FileListOutput, FileFindInput, FileFindOutput, CommandListInput, CommandListOutput, SkillListInput, SkillListOutput, EventSubscribeOutput, PtyListInput, PtyListOutput, PtyCreateInput, PtyCreateOutput, PtyGetInput, PtyGetOutput, PtyUpdateInput, PtyUpdateOutput, PtyRemoveInput, ShellListInput, ShellListOutput, ShellCreateInput, ShellCreateOutput, ShellGetInput, ShellGetOutput, ShellTimeoutInput, ShellTimeoutOutput, ShellOutputInput, ShellOutputOutput, ShellRemoveInput, QuestionRequestListInput, QuestionRequestListOutput, QuestionListInput, QuestionReplyInput, QuestionRejectInput, ReferenceListInput, ReferenceListOutput, ProjectCopyCreateInput, ProjectCopyRemoveInput, ProjectCopyRefreshInput, VcsStatusInput, VcsStatusOutput, VcsDiffInput, VcsDiffOutput, DebugLocationListOutput, DebugLocationEvictInput } from "./types";
1
+ import type { HealthStopInput, ServerGetOutput, LocationGetInput, LocationGetOutput, AgentListInput, AgentListOutput, PluginListInput, PluginListOutput, SessionListInput, SessionCreateInput, SessionGetInput, SessionRemoveInput, SessionForkInput, SessionSwitchAgentInput, SessionSwitchModelInput, SessionRenameInput, SessionMoveInput, SessionPromptInput, SessionCommandInput, SessionSkillInput, SessionSyntheticInput, SessionShellInput, SessionCompactInput, SessionWaitInput, SessionRevertStageInput, SessionRevertClearInput, SessionRevertCommitInput, SessionContextInput, SessionPendingListInput, SessionInstructionsEntryListInput, SessionInstructionsEntryPutInput, SessionInstructionsEntryRemoveInput, SessionLogInput, SessionLogOutput, SessionInterruptInput, SessionBackgroundInput, SessionMessageInput, MessageListInput, ModelListInput, ModelListOutput, ModelDefaultInput, ModelDefaultOutput, GenerateTextInput, ProviderListInput, ProviderListOutput, ProviderGetInput, ProviderGetOutput, IntegrationListInput, IntegrationListOutput, IntegrationGetInput, IntegrationGetOutput, IntegrationConnectKeyInput, IntegrationConnectOauthInput, IntegrationConnectOauthOutput, IntegrationAttemptStatusInput, IntegrationAttemptStatusOutput, IntegrationAttemptCompleteInput, IntegrationAttemptCancelInput, McpListInput, McpListOutput, McpResourceCatalogInput, McpResourceCatalogOutput, CredentialUpdateInput, CredentialRemoveInput, ProjectListOutput, ProjectCurrentInput, ProjectDirectoriesInput, FormRequestListInput, FormRequestListOutput, FormListInput, FormCreateInput, FormGetInput, FormStateInput, FormReplyInput, FormCancelInput, PermissionRequestListInput, PermissionRequestListOutput, PermissionSavedListInput, PermissionSavedRemoveInput, PermissionCreateInput, PermissionListInput, PermissionGetInput, PermissionReplyInput, FileReadInput, FileReadOutput, FileListInput, FileListOutput, FileFindInput, FileFindOutput, CommandListInput, CommandListOutput, SkillListInput, SkillListOutput, EventSubscribeOutput, PtyListInput, PtyListOutput, PtyCreateInput, PtyCreateOutput, PtyGetInput, PtyGetOutput, PtyUpdateInput, PtyUpdateOutput, PtyRemoveInput, ShellListInput, ShellListOutput, ShellCreateInput, ShellCreateOutput, ShellGetInput, ShellGetOutput, ShellTimeoutInput, ShellTimeoutOutput, ShellOutputInput, ShellOutputOutput, ShellRemoveInput, QuestionRequestListInput, QuestionRequestListOutput, QuestionListInput, QuestionReplyInput, QuestionRejectInput, ReferenceListInput, ReferenceListOutput, ProjectCopyCreateInput, ProjectCopyRemoveInput, ProjectCopyRefreshInput, VcsStatusInput, VcsStatusOutput, VcsDiffInput, VcsDiffOutput, DebugLocationListOutput, DebugLocationEvictInput } from "./types";
2
2
  export interface ClientOptions {
3
3
  readonly baseUrl: string;
4
4
  readonly fetch?: typeof globalThis.fetch;
@@ -10,7 +10,8 @@ export interface RequestOptions {
10
10
  }
11
11
  export declare function make(options: ClientOptions): {
12
12
  health: {
13
- get: (requestOptions?: RequestOptions) => Promise<HealthGetOutput>;
13
+ get: (requestOptions?: RequestOptions) => Promise<import("./types").ServiceHealth>;
14
+ stop: (input: HealthStopInput, requestOptions?: RequestOptions) => Promise<import("./types").ServiceStopResponse>;
14
15
  };
15
16
  server: {
16
17
  get: (requestOptions?: RequestOptions) => Promise<ServerGetOutput>;
@@ -129,6 +129,14 @@ export function make(options) {
129
129
  return {
130
130
  health: {
131
131
  get: (requestOptions) => request({ method: "GET", path: `/api/health`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, requestOptions),
132
+ stop: (input, requestOptions) => request({
133
+ method: "POST",
134
+ path: `/api/service/stop`,
135
+ body: { instanceID: input["instanceID"], targetVersion: input["targetVersion"] },
136
+ successStatus: 200,
137
+ declaredStatuses: [401, 400],
138
+ empty: false,
139
+ }, requestOptions),
132
140
  },
133
141
  server: {
134
142
  get: (requestOptions) => request({ method: "GET", path: `/api/server`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, requestOptions),
@@ -1,6 +1,21 @@
1
1
  export type JsonValue = null | boolean | number | string | Array<JsonValue> | {
2
2
  [key: string]: JsonValue;
3
3
  };
4
+ export type ServiceStatus = {
5
+ type: "starting";
6
+ } | {
7
+ type: "ready";
8
+ } | {
9
+ type: "stopping";
10
+ targetVersion?: string | null;
11
+ } | {
12
+ type: "failed";
13
+ message: string;
14
+ action: string;
15
+ };
16
+ export type ServiceStopResponse = {
17
+ accepted: boolean;
18
+ };
4
19
  export type ModelRef = {
5
20
  id: string;
6
21
  providerID: string;
@@ -737,6 +752,13 @@ export type VcsFileStatus = {
737
752
  deletions: number;
738
753
  status: "added" | "deleted" | "modified";
739
754
  };
755
+ export type ServiceHealth = {
756
+ healthy: true;
757
+ version: string;
758
+ pid: number;
759
+ instanceID?: string | null;
760
+ status?: ServiceStatus;
761
+ };
740
762
  export type SessionMessageModelSelected = {
741
763
  id: string;
742
764
  metadata?: {
@@ -3151,11 +3173,18 @@ export type ProjectCopyError = {
3151
3173
  };
3152
3174
  };
3153
3175
  export declare const isProjectCopyError: (value: unknown) => value is ProjectCopyError;
3154
- export type HealthGetOutput = {
3155
- healthy: true;
3156
- version: string;
3157
- pid: number;
3158
- };
3176
+ export type HealthGetOutput = ServiceHealth;
3177
+ export type HealthStopInput = {
3178
+ readonly instanceID: {
3179
+ readonly instanceID: string;
3180
+ readonly targetVersion?: string | undefined;
3181
+ }["instanceID"];
3182
+ readonly targetVersion?: {
3183
+ readonly instanceID: string;
3184
+ readonly targetVersion?: string | undefined;
3185
+ }["targetVersion"];
3186
+ };
3187
+ export type HealthStopOutput = ServiceStopResponse;
3159
3188
  export type ServerGetOutput = {
3160
3189
  urls: Array<string>;
3161
3190
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@opencode-ai/client",
4
- "version": "0.0.0-next-15548",
4
+ "version": "0.0.0-next-15555",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -45,8 +45,8 @@
45
45
  "typecheck": "tsgo --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@opencode-ai/schema": "0.0.0-next-15548",
49
- "@opencode-ai/protocol": "0.0.0-next-15548"
48
+ "@opencode-ai/schema": "0.0.0-next-15555",
49
+ "@opencode-ai/protocol": "0.0.0-next-15555"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "effect": "4.0.0-beta.83"
@@ -58,7 +58,7 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "@effect/platform-node": "4.0.0-beta.83",
61
- "@opencode-ai/httpapi-codegen": "0.0.0-next-15548",
61
+ "@opencode-ai/httpapi-codegen": "0.0.0-next-15555",
62
62
  "@tsconfig/bun": "1.0.9",
63
63
  "@types/bun": "1.3.13",
64
64
  "@typescript/native-preview": "7.0.0-dev.20251207.1",