@opencode/client 0.0.0-reserved → 2.0.1

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.
Files changed (62) hide show
  1. package/README.md +24 -2
  2. package/dist/contract.d.ts +1 -0
  3. package/dist/contract.js +1 -0
  4. package/dist/effect/api/api.d.ts +2698 -0
  5. package/dist/effect/api/api.js +0 -0
  6. package/dist/effect/api.d.ts +8 -0
  7. package/dist/effect/api.js +0 -0
  8. package/dist/effect/client.d.ts +2464 -0
  9. package/dist/effect/client.js +29 -0
  10. package/dist/effect/generated/client-error.d.ts +7 -0
  11. package/dist/effect/generated/client-error.js +5 -0
  12. package/dist/effect/generated/client.d.ts +2463 -0
  13. package/dist/effect/generated/client.js +603 -0
  14. package/dist/effect/generated/index.d.ts +2 -0
  15. package/dist/effect/generated/index.js +2 -0
  16. package/dist/effect/index.d.ts +35 -0
  17. package/dist/effect/index.js +30 -0
  18. package/dist/effect/rpc.d.ts +20 -0
  19. package/dist/effect/rpc.js +49 -0
  20. package/dist/effect/service.d.ts +78 -0
  21. package/dist/effect/service.js +259 -0
  22. package/dist/promise/api.d.ts +24 -0
  23. package/dist/promise/api.js +0 -0
  24. package/dist/promise/client.d.ts +257 -0
  25. package/dist/promise/client.js +13 -0
  26. package/dist/promise/generated/client-error.d.ts +6 -0
  27. package/dist/promise/generated/client-error.js +8 -0
  28. package/dist/promise/generated/client.d.ts +264 -0
  29. package/dist/promise/generated/client.js +1426 -0
  30. package/dist/promise/generated/index.d.ts +3 -0
  31. package/dist/promise/generated/index.js +3 -0
  32. package/dist/promise/generated/types.d.ts +8777 -0
  33. package/dist/promise/generated/types.js +30 -0
  34. package/dist/promise/index.d.ts +6 -0
  35. package/dist/promise/index.js +2 -0
  36. package/dist/promise/rpc.d.ts +32 -0
  37. package/dist/promise/rpc.js +86 -0
  38. package/dist/promise/service.d.ts +26 -0
  39. package/dist/promise/service.js +247 -0
  40. package/dist/pty-handoff.d.ts +9 -0
  41. package/dist/pty-handoff.js +121 -0
  42. package/dist/rpc-runtime.d.ts +21 -0
  43. package/dist/rpc-runtime.js +32 -0
  44. package/dist/service-contender.d.ts +11 -0
  45. package/dist/service-contender.js +56 -0
  46. package/dist/service-timing.d.ts +13 -0
  47. package/dist/service-timing.js +19 -0
  48. package/dist/service-version.d.ts +2 -0
  49. package/dist/service-version.js +9 -0
  50. package/dist/service.d.ts +52 -0
  51. package/dist/service.js +0 -0
  52. package/dist/shared-events.d.ts +11 -0
  53. package/dist/shared-events.js +137 -0
  54. package/dist/solid/connection.d.ts +37 -0
  55. package/dist/solid/connection.js +246 -0
  56. package/dist/solid/data.d.ts +201 -0
  57. package/dist/solid/data.js +1731 -0
  58. package/dist/solid/index.d.ts +3 -0
  59. package/dist/solid/index.js +3 -0
  60. package/dist/solid/pty.d.ts +22 -0
  61. package/dist/solid/pty.js +43 -0
  62. package/package.json +76 -6
@@ -0,0 +1,35 @@
1
+ import type { Effect } from "effect";
2
+ import type { OpenCode } from "./client.js";
3
+ export * from "./generated/index";
4
+ export { OpenCode } from "./client.js";
5
+ export type { AgentApi, AppApi, CatalogApi, CommandApi, ConfigApi, EventApi, IntegrationApi, ModelApi, PluginApi, ProviderApi, ReferenceApi, RpcApi, RpcClient, WebSearchApi, SessionApi, SkillApi, } from "./api.js";
6
+ export { Agent } from "@opencode/schema/agent";
7
+ export { Command } from "@opencode/schema/command";
8
+ export { Config } from "@opencode/schema/config";
9
+ export { Credential } from "@opencode/schema/credential";
10
+ export { Event } from "@opencode/schema/event";
11
+ export { EventLog } from "@opencode/schema/event-log";
12
+ export { FileSystem } from "@opencode/schema/filesystem";
13
+ export { Form } from "@opencode/schema/form";
14
+ export { Integration } from "@opencode/schema/integration";
15
+ export { Location } from "@opencode/schema/location";
16
+ export { Model } from "@opencode/schema/model";
17
+ export { Permission } from "@opencode/schema/permission";
18
+ export { PermissionSaved } from "@opencode/schema/permission-saved";
19
+ export { Project } from "@opencode/schema/project";
20
+ export { Worktree } from "@opencode/schema/worktree";
21
+ export { Vcs } from "@opencode/schema/vcs";
22
+ export { Provider } from "@opencode/schema/provider";
23
+ export { Pty } from "@opencode/schema/pty";
24
+ export { Question } from "@opencode/schema/question";
25
+ export { Reference } from "@opencode/schema/reference";
26
+ export { WebSearch } from "@opencode/schema/websearch";
27
+ export { AbsolutePath, RelativePath } from "@opencode/schema/schema";
28
+ export { Session } from "@opencode/schema/session";
29
+ export { SessionInbox } from "@opencode/schema/session-inbox";
30
+ export { SessionMessage } from "@opencode/schema/session-message";
31
+ export { Skill } from "@opencode/schema/skill";
32
+ export { Prompt } from "@opencode/schema/prompt";
33
+ export { PromptInput } from "@opencode/schema/prompt-input";
34
+ export type { OpenCodeEvent } from "@opencode/protocol/groups/event";
35
+ export type OpenCodeClient = Effect.Success<ReturnType<typeof OpenCode.make>>;
@@ -0,0 +1,30 @@
1
+ export * from "./generated/index";
2
+ export { OpenCode } from "./client.js";
3
+ export { Agent } from "@opencode/schema/agent";
4
+ export { Command } from "@opencode/schema/command";
5
+ export { Config } from "@opencode/schema/config";
6
+ export { Credential } from "@opencode/schema/credential";
7
+ export { Event } from "@opencode/schema/event";
8
+ export { EventLog } from "@opencode/schema/event-log";
9
+ export { FileSystem } from "@opencode/schema/filesystem";
10
+ export { Form } from "@opencode/schema/form";
11
+ export { Integration } from "@opencode/schema/integration";
12
+ export { Location } from "@opencode/schema/location";
13
+ export { Model } from "@opencode/schema/model";
14
+ export { Permission } from "@opencode/schema/permission";
15
+ export { PermissionSaved } from "@opencode/schema/permission-saved";
16
+ export { Project } from "@opencode/schema/project";
17
+ export { Worktree } from "@opencode/schema/worktree";
18
+ export { Vcs } from "@opencode/schema/vcs";
19
+ export { Provider } from "@opencode/schema/provider";
20
+ export { Pty } from "@opencode/schema/pty";
21
+ export { Question } from "@opencode/schema/question";
22
+ export { Reference } from "@opencode/schema/reference";
23
+ export { WebSearch } from "@opencode/schema/websearch";
24
+ export { AbsolutePath, RelativePath } from "@opencode/schema/schema";
25
+ export { Session } from "@opencode/schema/session";
26
+ export { SessionInbox } from "@opencode/schema/session-inbox";
27
+ export { SessionMessage } from "@opencode/schema/session-message";
28
+ export { Skill } from "@opencode/schema/skill";
29
+ export { Prompt } from "@opencode/schema/prompt";
30
+ export { PromptInput } from "@opencode/schema/prompt-input";
@@ -0,0 +1,20 @@
1
+ export * as RpcClientRuntime from "./rpc.js";
2
+ import type { Rpc } from "@opencode/schema/rpc";
3
+ import type { RpcError, RpcInternalError } from "@opencode/protocol/errors";
4
+ import type { OpenCodeEvent } from "@opencode/protocol/groups/event";
5
+ import { Effect, Schema, Stream } from "effect";
6
+ import type { RpcArguments, RpcCallOptions } from "../promise/rpc.js";
7
+ import type { RpcCallInput, RpcCallOutput } from "./api/api.js";
8
+ type DecodeError<S> = S extends Schema.Top ? Schema.SchemaError : never;
9
+ export type RpcClient<D extends Rpc.Definition, E = never, Options = RpcCallOptions, EventError = E> = {
10
+ readonly [Name in keyof D["methods"]]: (...args: RpcArguments<Rpc.Input<D["methods"][Name]["input"]>, Options>) => Effect.Effect<Rpc.Output<D["methods"][Name]["output"]>, Rpc.MethodError<D["methods"][Name]> | DecodeError<D["methods"][Name]["output"]> | E>;
11
+ } & {
12
+ readonly events: {
13
+ readonly subscribe: <Name extends keyof D["events"] & string>(name: Name) => Stream.Stream<Rpc.EventPayload<D, Name>, DecodeError<D["events"][Name]["schema"]> | EventError>;
14
+ };
15
+ };
16
+ export interface RpcApi<E = never, Options = RpcCallOptions, EventError = E> {
17
+ <D extends Rpc.Definition>(definition: D): RpcClient<D, E, Options, EventError>;
18
+ }
19
+ export declare function make<CallError, EventError>(call: (input: RpcCallInput, options?: RpcCallOptions) => Effect.Effect<RpcCallOutput, CallError>, subscribe: () => Stream.Stream<OpenCodeEvent, EventError>): RpcApi<Exclude<CallError, RpcError | RpcInternalError> | Rpc.SystemError, RpcCallOptions, EventError>;
20
+ export declare function aborted(signal: AbortSignal): Effect.Effect<void, never, never>;
@@ -0,0 +1,49 @@
1
+ export * as RpcClientRuntime from "./rpc.js";
2
+ import { Effect, Schema, Stream } from "effect";
3
+ import { RpcRuntime } from "../rpc-runtime.js";
4
+ export function make(call, subscribe) {
5
+ return (definition) => {
6
+ const methods = Object.fromEntries(Object.entries(definition.methods).map(([name, method]) => [
7
+ name,
8
+ (input, options) => {
9
+ const result = Effect.gen(function* () {
10
+ const response = yield* call({
11
+ rpcID: definition.id,
12
+ method: name,
13
+ input,
14
+ location: options?.location,
15
+ }, options);
16
+ return yield* RpcRuntime.read(method.output, response.output);
17
+ }).pipe(Effect.catch((error) => RpcRuntime.readError(method, error)));
18
+ const signal = options?.signal;
19
+ if (!signal)
20
+ return result;
21
+ return Effect.suspend(() => signal.aborted
22
+ ? Effect.interrupt
23
+ : Effect.raceFirst(result, Effect.andThen(aborted(signal), Effect.interrupt)));
24
+ },
25
+ ]));
26
+ // SAFETY: Every runtime key comes from this definition, and each value is decoded through its corresponding schema.
27
+ // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion
28
+ return Object.assign(methods, {
29
+ events: {
30
+ subscribe: (name) => {
31
+ const type = RpcRuntime.eventType(definition, name);
32
+ if (!Object.hasOwn(definition.events, name))
33
+ return Stream.fail(new Error(`Unknown RPC event: ${type}`));
34
+ const schema = definition.events[name];
35
+ return subscribe().pipe(Stream.filter((event) => event.type === type), Stream.mapEffect((event) => RpcRuntime.event(definition, name, schema, event)));
36
+ },
37
+ },
38
+ });
39
+ };
40
+ }
41
+ export function aborted(signal) {
42
+ return Effect.callback((resume) => {
43
+ if (signal.aborted)
44
+ return resume(Effect.void);
45
+ const abort = () => resume(Effect.void);
46
+ signal.addEventListener("abort", abort, { once: true });
47
+ return Effect.sync(() => signal.removeEventListener("abort", abort));
48
+ });
49
+ }
@@ -0,0 +1,78 @@
1
+ import { Effect, FileSystem, Schema } from "effect";
2
+ import type { DiscoverOptions, Endpoint, EnsureOptions, StopOptions } from "../service.js";
3
+ export * from "../service.js";
4
+ /** Contents of the local service registration file. */
5
+ export type Info = import("../service.js").Info;
6
+ /** Discover a healthy, compatible local service without starting one. */
7
+ export declare const discover: (options?: DiscoverOptions | undefined) => Effect.Effect<{
8
+ url: string;
9
+ auth: {
10
+ type: "basic";
11
+ username: string;
12
+ password: string;
13
+ } | undefined;
14
+ } | undefined, never, FileSystem.FileSystem>;
15
+ /** Recognize an authenticated compatible service bound to an expected URL, including while it starts or fails. */
16
+ export declare const incumbent: (options: DiscoverOptions & {
17
+ readonly url: string;
18
+ }) => Effect.Effect<{
19
+ endpoint: {
20
+ url: string;
21
+ auth: {
22
+ type: "basic";
23
+ username: string;
24
+ password: string;
25
+ } | undefined;
26
+ };
27
+ state: "failed" | "ready" | "waiting";
28
+ } | undefined, never, FileSystem.FileSystem>;
29
+ /** Ensure a healthy, compatible local service is running. */
30
+ export declare const ensure: (options?: EnsureOptions | undefined) => Effect.Effect<Endpoint, Error, FileSystem.FileSystem>;
31
+ /** Stop the registered local service. */
32
+ export declare const stop: (options?: StopOptions | undefined) => Effect.Effect<void, Error, FileSystem.FileSystem>;
33
+ /** Create HTTP authentication headers for a service endpoint. */
34
+ export declare function headers(endpoint: Endpoint): {
35
+ authorization: string;
36
+ } | undefined;
37
+ /** Schema for the local service registration file. */
38
+ export declare const Info: Schema.Struct<{
39
+ readonly id: Schema.optional<Schema.String>;
40
+ readonly version: Schema.optional<Schema.String>;
41
+ readonly url: Schema.String;
42
+ readonly pid: Schema.Int;
43
+ readonly password: Schema.optional<Schema.String>;
44
+ }>;
45
+ /** Effect-based local service lifecycle operations. */
46
+ export declare const Service: {
47
+ discover: (options?: DiscoverOptions | undefined) => Effect.Effect<{
48
+ url: string;
49
+ auth: {
50
+ type: "basic";
51
+ username: string;
52
+ password: string;
53
+ } | undefined;
54
+ } | undefined, never, FileSystem.FileSystem>;
55
+ incumbent: (options: DiscoverOptions & {
56
+ readonly url: string;
57
+ }) => Effect.Effect<{
58
+ endpoint: {
59
+ url: string;
60
+ auth: {
61
+ type: "basic";
62
+ username: string;
63
+ password: string;
64
+ } | undefined;
65
+ };
66
+ state: "failed" | "ready" | "waiting";
67
+ } | undefined, never, FileSystem.FileSystem>;
68
+ ensure: (options?: EnsureOptions | undefined) => Effect.Effect<Endpoint, Error, FileSystem.FileSystem>;
69
+ stop: (options?: StopOptions | undefined) => Effect.Effect<void, Error, FileSystem.FileSystem>;
70
+ headers: typeof headers;
71
+ Info: Schema.Struct<{
72
+ readonly id: Schema.optional<Schema.String>;
73
+ readonly version: Schema.optional<Schema.String>;
74
+ readonly url: Schema.String;
75
+ readonly pid: Schema.Int;
76
+ readonly password: Schema.optional<Schema.String>;
77
+ }>;
78
+ };
@@ -0,0 +1,259 @@
1
+ import { ServiceStatus } from "@opencode/protocol/groups/health";
2
+ import { Effect, FileSystem, Option, Schedule, Schema } from "effect";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { contenderFailure, contenderFinished, spawnServiceContender, } from "../service-contender.js";
6
+ import { defaultEnsureTiming, ensureTiming } from "../service-timing.js";
7
+ import { matchesVersion } from "../service-version.js";
8
+ import { PtyHandoff } from "../pty-handoff.js";
9
+ export * from "../service.js";
10
+ // Find, start, and stop the local opencode background service.
11
+ //
12
+ // The service daemon advertises itself through a registration file in the
13
+ // user's state directory: url, pid, version, and the private password, with
14
+ // 0600 permissions. That file is the complete discovery contract — reading it
15
+ // is all a client needs to connect. The daemon's own configuration (port,
16
+ // persisted password) is CLI-owned and never read here.
17
+ // Read-only lookup: registration file plus health check and version gate.
18
+ // Never spawns; escalation to ensure() is the caller's policy.
19
+ /** Discover a healthy, compatible local service without starting one. */
20
+ export const discover = Effect.fn("service.discover")(function* (options = {}) {
21
+ const found = (yield* registered(options.file)).service;
22
+ if (found?.state !== "ready")
23
+ return undefined;
24
+ if (!matchesVersion(found.version, options))
25
+ return undefined;
26
+ return found.endpoint;
27
+ });
28
+ /** Recognize an authenticated compatible service bound to an expected URL, including while it starts or fails. */
29
+ export const incumbent = Effect.fn("service.incumbent")(function* (options) {
30
+ const info = yield* read(options.file);
31
+ const found = info === undefined ? undefined : yield* probe({ ...info, url: options.url });
32
+ if (found === undefined || found.legacy)
33
+ return undefined;
34
+ if (!matchesVersion(found.version, options))
35
+ return undefined;
36
+ return { endpoint: found.endpoint, state: found.state };
37
+ });
38
+ // Idempotent ensure-running: reuses a healthy compatible server, replaces a
39
+ // version-mismatched one, and otherwise spawns small contenders until a server
40
+ // becomes discoverable. A contender is never killed merely for slow startup.
41
+ /** Ensure a healthy, compatible local service is running. */
42
+ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
43
+ const timing = ensureTiming(options);
44
+ const contenders = new Set();
45
+ let timeouts;
46
+ let announced = false;
47
+ let lastSpawn = 0;
48
+ let spawnDelay = timing.spawnDelay;
49
+ const announce = (reason, previousVersion) => Effect.sync(() => {
50
+ if (announced)
51
+ return;
52
+ announced = true;
53
+ options.onStart?.(reason, previousVersion);
54
+ });
55
+ const spawnContender = Effect.gen(function* () {
56
+ const [command, ...args] = options.command ?? ["opencode", "serve", "--service"];
57
+ if (command === undefined)
58
+ return yield* Effect.fail(new Error("Missing service command"));
59
+ const env = yield* Effect.tryPromise(() => PtyHandoff.environment(options.file ?? fallback(), options.env));
60
+ return yield* Effect.try({
61
+ try: () => {
62
+ return spawnServiceContender(command, args, env);
63
+ },
64
+ catch: (cause) => new Error("Failed to start server", { cause }),
65
+ });
66
+ });
67
+ const found = yield* Effect.gen(function* () {
68
+ const registration = yield* registered(options.file, true, timing.requestTimeout);
69
+ const info = registration.info;
70
+ const service = registration.service;
71
+ if (registration.timedOut && info !== undefined) {
72
+ timeouts = {
73
+ info,
74
+ count: timeouts !== undefined && same(timeouts.info, info) ? timeouts.count + 1 : 1,
75
+ };
76
+ if (timeouts.count >= 3) {
77
+ yield* announce("missing");
78
+ yield* Effect.logWarning("Background service is unresponsive; recovery cannot preserve persistent terminals");
79
+ yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()));
80
+ yield* terminate(info, options, timing);
81
+ timeouts = undefined;
82
+ lastSpawn = Date.now() - spawnDelay;
83
+ }
84
+ }
85
+ else
86
+ timeouts = undefined;
87
+ if (service !== undefined) {
88
+ spawnDelay = timing.spawnDelay;
89
+ const compatible = !service.legacy && matchesVersion(service.version, options);
90
+ if (compatible && service.state === "ready") {
91
+ yield* Effect.tryPromise(() => PtyHandoff.complete(options.file ?? fallback(), service.info));
92
+ return Option.some(service);
93
+ }
94
+ if (compatible && service.state === "failed")
95
+ return yield* Effect.fail(new Error("Background service failed to start"));
96
+ if (compatible)
97
+ return Option.none();
98
+ yield* announce("version-mismatch", service.version);
99
+ if (!service.legacy && service.state !== "ready")
100
+ yield* Effect.logWarning("Background service is not ready; replacement cannot preserve persistent terminals");
101
+ yield* stop({
102
+ file: options.file,
103
+ pty: !service.legacy && service.state === "ready" ? "handoff" : "clear",
104
+ }).pipe(Effect.ignore);
105
+ lastSpawn = 0;
106
+ return Option.none();
107
+ }
108
+ else if (lastSpawn === 0 && info !== undefined)
109
+ lastSpawn = Date.now();
110
+ const finished = [...contenders].filter(contenderFinished);
111
+ const failure = finished.map(contenderFailure).find((error) => error !== undefined);
112
+ if (finished.some((item) => item.child.exitCode === 0)) {
113
+ spawnDelay = Math.min(spawnDelay * 2, timing.maxSpawnDelay);
114
+ }
115
+ finished.forEach((item) => contenders.delete(item));
116
+ if (failure !== undefined && contenders.size === 0)
117
+ return yield* Effect.fail(failure);
118
+ // Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
119
+ if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
120
+ yield* announce("missing");
121
+ contenders.add(yield* spawnContender);
122
+ lastSpawn = Date.now();
123
+ }
124
+ return Option.none();
125
+ }).pipe(Effect.repeat({
126
+ until: Option.isSome,
127
+ schedule: Schedule.max([Schedule.spaced(timing.pollInterval), Schedule.recurs(timing.attempts)]),
128
+ }), Effect.ensuring(Effect.sync(() => contenders.forEach((contender) => contender.release()))));
129
+ if (Option.isNone(found))
130
+ return yield* Effect.fail(new Error("Timed out waiting for the background service to start"));
131
+ return found.value.endpoint;
132
+ });
133
+ /** Stop the registered local service. */
134
+ export const stop = Effect.fn("service.stop")(function* (options = {}) {
135
+ const info = yield* read(options.file);
136
+ if (options.pty === "handoff" && info !== undefined)
137
+ yield* Effect.tryPromise(() => PtyHandoff.prepare(options.file ?? fallback(), info, defaultEnsureTiming.requestTimeout));
138
+ else
139
+ yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()));
140
+ if (info !== undefined)
141
+ yield* terminate(info, options, defaultEnsureTiming);
142
+ });
143
+ function fallback() {
144
+ const state = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state");
145
+ return join(state, "opencode", "service.json");
146
+ }
147
+ /** Create HTTP authentication headers for a service endpoint. */
148
+ export function headers(endpoint) {
149
+ if (endpoint.auth === undefined)
150
+ return undefined;
151
+ return { authorization: "Basic " + btoa(endpoint.auth.username + ":" + endpoint.auth.password) };
152
+ }
153
+ /** Schema for the local service registration file. */
154
+ export const Info = Schema.Struct({
155
+ id: Schema.optional(Schema.String),
156
+ version: Schema.optional(Schema.String),
157
+ url: Schema.String,
158
+ pid: Schema.Int.check(Schema.isGreaterThan(0)),
159
+ password: Schema.optional(Schema.String),
160
+ });
161
+ const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info));
162
+ const decodeHealth = Schema.decodeUnknownOption(ServiceStatus.Health);
163
+ const decodeLegacyHealth = Schema.decodeUnknownOption(Schema.Struct({ healthy: Schema.Literal(true) }));
164
+ // A missing or corrupt file means no valid info; callers treat both
165
+ // the same (the registering server self-evicts, clients rediscover).
166
+ const read = Effect.fnUntraced(function* (file) {
167
+ const fs = yield* FileSystem.FileSystem;
168
+ const text = yield* fs.readFileString(file ?? fallback()).pipe(Effect.option);
169
+ if (Option.isNone(text))
170
+ return undefined;
171
+ return yield* decode(text.value).pipe(Effect.option, Effect.map(Option.getOrUndefined));
172
+ });
173
+ const probe = Effect.fnUntraced(function* (info, allowLegacy = false) {
174
+ return (yield* probeResult(info, allowLegacy)).service;
175
+ });
176
+ const probeResult = Effect.fnUntraced(function* (info, allowLegacy = false, timeout = defaultEnsureTiming.requestTimeout) {
177
+ const endpoint = {
178
+ url: info.url,
179
+ auth: info.password === undefined
180
+ ? undefined
181
+ : { type: "basic", username: "opencode", password: info.password },
182
+ };
183
+ const signal = AbortSignal.timeout(timeout);
184
+ const result = yield* Effect.promise(() => fetch(new URL("/api/health", info.url), {
185
+ headers: headers(endpoint),
186
+ signal,
187
+ })
188
+ .then(async (response) => ({ response, body: (await response.json()) }))
189
+ .then((value) => ({ value }), (cause) => ({ cause })));
190
+ if ("cause" in result)
191
+ return { service: undefined, timedOut: signal.aborted };
192
+ const response = result.value.response;
193
+ const body = result.value.body;
194
+ const health = decodeHealth(body);
195
+ if (Option.isSome(health)) {
196
+ if (health.value.pid !== info.pid)
197
+ return { service: undefined, timedOut: false };
198
+ if (info.version !== undefined && health.value.version !== info.version)
199
+ return { service: undefined, timedOut: false };
200
+ return {
201
+ service: {
202
+ info,
203
+ endpoint,
204
+ version: health.value.version,
205
+ state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
206
+ legacy: false,
207
+ },
208
+ timedOut: false,
209
+ };
210
+ }
211
+ if (!allowLegacy ||
212
+ Option.isNone(decodeLegacyHealth(body)) ||
213
+ (typeof body === "object" && body !== null && ("version" in body || "pid" in body)))
214
+ return { service: undefined, timedOut: false };
215
+ return {
216
+ service: { info, endpoint, state: "ready", legacy: true },
217
+ timedOut: false,
218
+ };
219
+ });
220
+ const registered = Effect.fnUntraced(function* (file, allowLegacy = false, timeout) {
221
+ const info = yield* read(file);
222
+ if (info === undefined)
223
+ return { info: undefined, service: undefined, timedOut: false };
224
+ return { info, ...(yield* probeResult(info, allowLegacy, timeout)) };
225
+ });
226
+ // 50ms cadence bounded at ~5s, shared by stop escalation and each ensure
227
+ // discovery window.
228
+ const poll = (timing) => Schedule.max([Schedule.spaced(timing.stopPollInterval), Schedule.recurs(timing.stopPollAttempts)]);
229
+ const signal = (pid, name) => Effect.try({ try: () => process.kill(pid, name), catch: (cause) => cause }).pipe(Effect.ignore);
230
+ const stopped = Effect.fnUntraced(function* (pid) {
231
+ const running = yield* Effect.try({ try: () => process.kill(pid, 0), catch: () => false }).pipe(Effect.orElseSucceed(() => false));
232
+ if (!running)
233
+ return true;
234
+ return yield* Effect.fail(new Error(`Server process ${pid} is still running`));
235
+ });
236
+ function same(left, right) {
237
+ return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid;
238
+ }
239
+ const terminate = Effect.fnUntraced(function* (info, options, timing) {
240
+ const current = yield* read(options.file);
241
+ if (current === undefined || !same(current, info))
242
+ return;
243
+ yield* signal(info.pid, "SIGTERM");
244
+ const done = yield* stopped(info.pid).pipe(Effect.retry(poll(timing)), Effect.option);
245
+ if (Option.isNone(done)) {
246
+ const latest = yield* read(options.file);
247
+ if (latest === undefined || !same(latest, info))
248
+ return;
249
+ yield* signal(info.pid, "SIGKILL");
250
+ yield* stopped(info.pid).pipe(Effect.retry(poll(timing)));
251
+ }
252
+ const latest = yield* read(options.file);
253
+ if (latest === undefined || !same(latest, info))
254
+ return;
255
+ const fs = yield* FileSystem.FileSystem;
256
+ yield* fs.remove(options.file ?? fallback()).pipe(Effect.ignore);
257
+ });
258
+ /** Effect-based local service lifecycle operations. */
259
+ export const Service = { discover, incumbent, ensure, stop, headers, Info };
@@ -0,0 +1,24 @@
1
+ import type { OpenCode } from "./client.js";
2
+ type Client = ReturnType<typeof OpenCode.make>;
3
+ export type { RpcApi, RpcCallOptions, RpcClient, RpcEventPayload } from "./rpc.js";
4
+ export type AgentApi = Client["agent"];
5
+ export type CommandApi = Client["command"];
6
+ export type ConfigApi = Client["config"];
7
+ export type EventApi = Client["event"];
8
+ export type GenerateApi = Client["generate"];
9
+ export type IntegrationApi = Client["integration"];
10
+ export type McpApi = Client["mcp"];
11
+ export type ModelApi = Client["model"];
12
+ export type PluginApi = Client["plugin"];
13
+ export type PermissionApi = Client["permission"];
14
+ export type ProviderApi = Client["provider"];
15
+ export type ReferenceApi = Client["reference"];
16
+ export type WebSearchApi = Client["websearch"];
17
+ export type SessionApi = Client["session"];
18
+ export type SkillApi = Client["skill"];
19
+ export type VcsApi = Client["vcs"];
20
+ export type WorktreeApi = Client["worktree"];
21
+ export interface CatalogApi {
22
+ readonly provider: ProviderApi;
23
+ readonly model: ModelApi;
24
+ }
File without changes