@opencode-ai/client 0.0.0-next-17255 → 0.0.0-next-17268

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,10 +1,17 @@
1
1
  import { ServiceStatus } from "@opencode-ai/protocol/groups/health";
2
2
  import { Effect, FileSystem, Option, Schedule, Schema } from "effect";
3
- import { spawn } from "node:child_process";
4
3
  import { homedir } from "node:os";
5
4
  import { join } from "node:path";
5
+ import { contenderFailure, contenderFinished, spawnServiceContender, } from "../service-contender.js";
6
6
  import { defaultEnsureTiming, ensureTiming } from "../service-timing.js";
7
7
  export * from "../service.js";
8
+ // Find, start, and stop the local opencode background service.
9
+ //
10
+ // The service daemon advertises itself through a registration file in the
11
+ // user's state directory: url, pid, version, and the private password, with
12
+ // 0600 permissions. That file is the complete discovery contract — reading it
13
+ // is all a client needs to connect. The daemon's own configuration (port,
14
+ // persisted password) is CLI-owned and never read here.
8
15
  // Read-only lookup: registration file plus health check and version gate.
9
16
  // Never spawns; escalation to ensure() is the caller's policy.
10
17
  /** Discover a healthy, compatible local service without starting one. */
@@ -52,13 +59,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
52
59
  return yield* Effect.fail(new Error("Missing service command"));
53
60
  return yield* Effect.try({
54
61
  try: () => {
55
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
56
- let error;
57
- child.once("error", (cause) => {
58
- error = new Error("Failed to start server", { cause });
59
- });
60
- child.unref();
61
- return { child, error: () => error };
62
+ return spawnServiceContender(command, args);
62
63
  },
63
64
  catch: (cause) => new Error("Failed to start server", { cause }),
64
65
  });
@@ -115,24 +116,11 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
115
116
  }).pipe(Effect.repeat({
116
117
  until: Option.isSome,
117
118
  schedule: Schedule.max([Schedule.spaced(timing.pollInterval), Schedule.recurs(timing.attempts)]),
118
- }));
119
+ }), Effect.ensuring(Effect.sync(() => contenders.forEach((contender) => contender.release()))));
119
120
  if (Option.isNone(found))
120
121
  return yield* Effect.fail(new Error("Timed out waiting for the background service to start"));
121
122
  return found.value.endpoint;
122
123
  });
123
- function contenderFailure(contender) {
124
- const error = contender.error();
125
- if (error !== undefined)
126
- return error;
127
- if (contender.child.exitCode !== null && contender.child.exitCode !== 0)
128
- return new Error(`Server process exited with code ${contender.child.exitCode}`);
129
- if (contender.child.signalCode !== null)
130
- return new Error(`Server process terminated by ${contender.child.signalCode}`);
131
- return undefined;
132
- }
133
- function contenderFinished(contender) {
134
- return contender.error() !== undefined || contender.child.exitCode !== null || contender.child.signalCode !== null;
135
- }
136
124
  /** Stop the registered local service. */
137
125
  export const stop = Effect.fn("service.stop")(function* (options = {}) {
138
126
  const existing = yield* find(options);
@@ -73,7 +73,7 @@ export declare function make(options: ClientOptions): {
73
73
  log: (input: SessionLogInput, requestOptions?: RequestOptions) => AsyncIterable<SessionLogOutput>;
74
74
  interrupt: (input: SessionInterruptInput, requestOptions?: RequestOptions) => Promise<void>;
75
75
  background: (input: SessionBackgroundInput, requestOptions?: RequestOptions) => Promise<void>;
76
- message: (input: SessionMessageInput, requestOptions?: RequestOptions) => Promise<import("./types.js").SessionMessageAgentSelected | import("./types.js").SessionMessageSynthetic | import("./types.js").SessionMessageSystem | import("./types.js").SessionMessageSkill | import("./types.js").SessionMessageShell | import("./types.js").SessionMessageCompactionRunning | import("./types.js").SessionMessageCompactionCompleted | import("./types.js").SessionMessageModelSelected | import("./types.js").SessionMessageCompactionFailed | import("./types.js").SessionMessageUser | import("./types.js").SessionMessageAssistant>;
76
+ message: (input: SessionMessageInput, requestOptions?: RequestOptions) => Promise<import("./types.js").SessionMessageAgentSelected | import("./types.js").SessionMessageSynthetic | import("./types.js").SessionMessageSystem | import("./types.js").SessionMessageSkill | import("./types.js").SessionMessageShell | import("./types.js").SessionMessageCompactionRunning | import("./types.js").SessionMessageCompactionCompleted | import("./types.js").SessionMessageModelSelected | import("./types.js").SessionMessageLocationSwitched | import("./types.js").SessionMessageCompactionFailed | import("./types.js").SessionMessageUser | import("./types.js").SessionMessageAssistant>;
77
77
  };
78
78
  message: {
79
79
  list: (input: MessageListInput, requestOptions?: RequestOptions) => Promise<import("./types.js").SessionMessagesResponse>;
@@ -96,6 +96,7 @@ export type SessionMessageSystem = {
96
96
  };
97
97
  type: "system";
98
98
  text: string;
99
+ description?: string;
99
100
  };
100
101
  export type SessionMessageSkill = {
101
102
  id: string;
@@ -618,6 +619,24 @@ export type PermissionRule = {
618
619
  resource: string;
619
620
  effect: PermissionEffect;
620
621
  };
622
+ export type SessionMessageLocationSwitched = {
623
+ id: string;
624
+ metadata?: {
625
+ [x: string]: JsonValue;
626
+ };
627
+ time: {
628
+ created: number;
629
+ };
630
+ type: "location-switched";
631
+ location: LocationRef;
632
+ projectID?: string;
633
+ subpath?: string;
634
+ previous?: {
635
+ location: LocationRef;
636
+ projectID?: string;
637
+ subpath?: string;
638
+ };
639
+ };
621
640
  export type SessionCreated = {
622
641
  id: string;
623
642
  created: number;
@@ -2797,7 +2816,7 @@ export type SessionInputAdmitted = {
2797
2816
  input: SessionPendingMessage;
2798
2817
  };
2799
2818
  };
2800
- export type SessionMessageInfo = SessionMessageAgentSelected | SessionMessageModelSelected | SessionMessageUser | SessionMessageSynthetic | SessionMessageSystem | SessionMessageSkill | SessionMessageShell | SessionMessageAssistant | SessionMessageCompaction;
2819
+ export type SessionMessageInfo = SessionMessageAgentSelected | SessionMessageModelSelected | SessionMessageLocationSwitched | SessionMessageUser | SessionMessageSynthetic | SessionMessageSystem | SessionMessageSkill | SessionMessageShell | SessionMessageAssistant | SessionMessageCompaction;
2801
2820
  export type IntegrationMethod = IntegrationOAuthMethod | IntegrationCommandMethod | IntegrationKeyMethod | IntegrationEnvMethod;
2802
2821
  export type FormCreated = {
2803
2822
  id: string;
@@ -3331,6 +3350,29 @@ export type SessionImportInput = {
3331
3350
  readonly providerID: string;
3332
3351
  readonly variant?: string;
3333
3352
  };
3353
+ } | {
3354
+ readonly id: string;
3355
+ readonly metadata?: {
3356
+ readonly [x: string]: JsonValue;
3357
+ };
3358
+ readonly time: {
3359
+ readonly created: number;
3360
+ };
3361
+ readonly type: "location-switched";
3362
+ readonly location: {
3363
+ readonly directory: string;
3364
+ readonly workspaceID?: string;
3365
+ };
3366
+ readonly projectID?: string;
3367
+ readonly subpath?: string;
3368
+ readonly previous?: {
3369
+ readonly location: {
3370
+ readonly directory: string;
3371
+ readonly workspaceID?: string;
3372
+ };
3373
+ readonly projectID?: string;
3374
+ readonly subpath?: string;
3375
+ };
3334
3376
  } | {
3335
3377
  readonly id: string;
3336
3378
  readonly metadata?: {
@@ -3397,6 +3439,7 @@ export type SessionImportInput = {
3397
3439
  };
3398
3440
  readonly type: "system";
3399
3441
  readonly text: string;
3442
+ readonly description?: string;
3400
3443
  } | {
3401
3444
  readonly id: string;
3402
3445
  readonly metadata?: {
@@ -3714,6 +3757,29 @@ export type SessionImportInput = {
3714
3757
  readonly providerID: string;
3715
3758
  readonly variant?: string;
3716
3759
  };
3760
+ } | {
3761
+ readonly id: string;
3762
+ readonly metadata?: {
3763
+ readonly [x: string]: JsonValue;
3764
+ };
3765
+ readonly time: {
3766
+ readonly created: number;
3767
+ };
3768
+ readonly type: "location-switched";
3769
+ readonly location: {
3770
+ readonly directory: string;
3771
+ readonly workspaceID?: string;
3772
+ };
3773
+ readonly projectID?: string;
3774
+ readonly subpath?: string;
3775
+ readonly previous?: {
3776
+ readonly location: {
3777
+ readonly directory: string;
3778
+ readonly workspaceID?: string;
3779
+ };
3780
+ readonly projectID?: string;
3781
+ readonly subpath?: string;
3782
+ };
3717
3783
  } | {
3718
3784
  readonly id: string;
3719
3785
  readonly metadata?: {
@@ -3780,6 +3846,7 @@ export type SessionImportInput = {
3780
3846
  };
3781
3847
  readonly type: "system";
3782
3848
  readonly text: string;
3849
+ readonly description?: string;
3783
3850
  } | {
3784
3851
  readonly id: string;
3785
3852
  readonly metadata?: {
@@ -4097,6 +4164,29 @@ export type SessionImportInput = {
4097
4164
  readonly providerID: string;
4098
4165
  readonly variant?: string;
4099
4166
  };
4167
+ } | {
4168
+ readonly id: string;
4169
+ readonly metadata?: {
4170
+ readonly [x: string]: JsonValue;
4171
+ };
4172
+ readonly time: {
4173
+ readonly created: number;
4174
+ };
4175
+ readonly type: "location-switched";
4176
+ readonly location: {
4177
+ readonly directory: string;
4178
+ readonly workspaceID?: string;
4179
+ };
4180
+ readonly projectID?: string;
4181
+ readonly subpath?: string;
4182
+ readonly previous?: {
4183
+ readonly location: {
4184
+ readonly directory: string;
4185
+ readonly workspaceID?: string;
4186
+ };
4187
+ readonly projectID?: string;
4188
+ readonly subpath?: string;
4189
+ };
4100
4190
  } | {
4101
4191
  readonly id: string;
4102
4192
  readonly metadata?: {
@@ -4163,6 +4253,7 @@ export type SessionImportInput = {
4163
4253
  };
4164
4254
  readonly type: "system";
4165
4255
  readonly text: string;
4256
+ readonly description?: string;
4166
4257
  } | {
4167
4258
  readonly id: string;
4168
4259
  readonly metadata?: {
@@ -1,9 +1,14 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { spawn } from "node:child_process";
3
2
  import { homedir } from "node:os";
4
3
  import { join } from "node:path";
4
+ import { contenderFailure, contenderFinished, spawnServiceContender, } from "../service-contender.js";
5
5
  import { defaultEnsureTiming, ensureTiming } from "../service-timing.js";
6
6
  export * from "../service.js";
7
+ // Find, start, and stop the local opencode background service.
8
+ //
9
+ // The registration file is the complete discovery contract. This module is
10
+ // intentionally implemented with Node APIs so Promise clients do not need
11
+ // Effect or @effect/platform-node at runtime.
7
12
  /** Discover a healthy, compatible local service without starting one. */
8
13
  export async function discover(options = {}) {
9
14
  return (await discoverLocal(options))?.endpoint;
@@ -36,83 +41,69 @@ export async function ensure(options = {}) {
36
41
  if (command === undefined)
37
42
  throw new Error("Missing service command");
38
43
  try {
39
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
40
- let error;
41
- child.once("error", (cause) => {
42
- error = new Error("Failed to start server", { cause });
43
- });
44
- child.unref();
45
- return { child, error: () => error };
44
+ return spawnServiceContender(command, args);
46
45
  }
47
46
  catch (cause) {
48
47
  throw new Error("Failed to start server", { cause });
49
48
  }
50
49
  };
51
- while (true) {
52
- if (Date.now() >= deadline)
53
- throw new Error("Timed out waiting for the background service to start");
54
- const registration = await registered(options.file, true, timing.requestTimeout);
55
- if (registration.timedOut && registration.info !== undefined) {
56
- timeouts = {
57
- info: registration.info,
58
- count: timeouts !== undefined && same(timeouts.info, registration.info) ? timeouts.count + 1 : 1,
59
- };
60
- if (timeouts.count >= 3) {
61
- announce("missing");
62
- await evict(registration.info, options, timing);
63
- timeouts = undefined;
64
- lastSpawn = Date.now() - spawnDelay;
65
- }
66
- }
67
- else
68
- timeouts = undefined;
69
- if (registration.service !== undefined) {
70
- spawnDelay = timing.spawnDelay;
71
- const service = registration.service;
72
- const compatible = !service.legacy && (options.version === undefined || service.version === options.version);
73
- if (compatible && service.state === "ready")
74
- return service.endpoint;
75
- if (compatible && service.state === "failed")
76
- throw new Error("Background service failed to start");
77
- if (!compatible) {
78
- announce("version-mismatch", service.version);
79
- await kill(service, options, timing).catch(() => undefined);
80
- lastSpawn = 0;
50
+ try {
51
+ while (true) {
52
+ if (Date.now() >= deadline)
53
+ throw new Error("Timed out waiting for the background service to start");
54
+ const registration = await registered(options.file, true, timing.requestTimeout);
55
+ if (registration.timedOut && registration.info !== undefined) {
56
+ timeouts = {
57
+ info: registration.info,
58
+ count: timeouts !== undefined && same(timeouts.info, registration.info) ? timeouts.count + 1 : 1,
59
+ };
60
+ if (timeouts.count >= 3) {
61
+ announce("missing");
62
+ await evict(registration.info, options, timing);
63
+ timeouts = undefined;
64
+ lastSpawn = Date.now() - spawnDelay;
65
+ }
81
66
  }
82
- }
83
- else {
84
- if (lastSpawn === 0 && registration.info !== undefined)
85
- lastSpawn = Date.now();
86
- const finished = [...contenders].filter(contenderFinished);
87
- const failure = finished.map(contenderFailure).find((error) => error !== undefined);
88
- if (finished.some((item) => item.child.exitCode === 0)) {
89
- spawnDelay = Math.min(spawnDelay * 2, timing.maxSpawnDelay);
67
+ else
68
+ timeouts = undefined;
69
+ if (registration.service !== undefined) {
70
+ spawnDelay = timing.spawnDelay;
71
+ const service = registration.service;
72
+ const compatible = !service.legacy && (options.version === undefined || service.version === options.version);
73
+ if (compatible && service.state === "ready")
74
+ return service.endpoint;
75
+ if (compatible && service.state === "failed")
76
+ throw new Error("Background service failed to start");
77
+ if (!compatible) {
78
+ announce("version-mismatch", service.version);
79
+ await kill(service, options, timing).catch(() => undefined);
80
+ lastSpawn = 0;
81
+ }
90
82
  }
91
- finished.forEach((item) => contenders.delete(item));
92
- if (failure !== undefined && contenders.size === 0)
93
- throw failure;
94
- // Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
95
- if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
96
- announce("missing");
97
- contenders.add(spawnContender());
98
- lastSpawn = Date.now();
83
+ else {
84
+ if (lastSpawn === 0 && registration.info !== undefined)
85
+ lastSpawn = Date.now();
86
+ const finished = [...contenders].filter(contenderFinished);
87
+ const failure = finished.map(contenderFailure).find((error) => error !== undefined);
88
+ if (finished.some((item) => item.child.exitCode === 0)) {
89
+ spawnDelay = Math.min(spawnDelay * 2, timing.maxSpawnDelay);
90
+ }
91
+ finished.forEach((item) => contenders.delete(item));
92
+ if (failure !== undefined && contenders.size === 0)
93
+ throw failure;
94
+ // Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
95
+ if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
96
+ announce("missing");
97
+ contenders.add(spawnContender());
98
+ lastSpawn = Date.now();
99
+ }
99
100
  }
101
+ await delay(timing.pollInterval);
100
102
  }
101
- await delay(timing.pollInterval);
102
103
  }
103
- }
104
- function contenderFailure(contender) {
105
- const error = contender.error();
106
- if (error !== undefined)
107
- return error;
108
- if (contender.child.exitCode !== null && contender.child.exitCode !== 0)
109
- return new Error(`Server process exited with code ${contender.child.exitCode}`);
110
- if (contender.child.signalCode !== null)
111
- return new Error(`Server process terminated by ${contender.child.signalCode}`);
112
- return undefined;
113
- }
114
- function contenderFinished(contender) {
115
- return contender.error() !== undefined || contender.child.exitCode !== null || contender.child.signalCode !== null;
104
+ finally {
105
+ contenders.forEach((contender) => contender.release());
106
+ }
116
107
  }
117
108
  /** Stop the registered local service. */
118
109
  export async function stop(options = {}) {
@@ -0,0 +1,11 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ export type ServiceContender = {
3
+ readonly child: ChildProcess;
4
+ readonly error: () => Error | undefined;
5
+ readonly closed: () => boolean;
6
+ readonly stderr: () => string;
7
+ readonly release: () => void;
8
+ };
9
+ export declare function spawnServiceContender(command: string, args: ReadonlyArray<string>): ServiceContender;
10
+ export declare function contenderFailure(contender: ServiceContender): Error | undefined;
11
+ export declare function contenderFinished(contender: ServiceContender): boolean;
@@ -0,0 +1,52 @@
1
+ import { spawn } from "node:child_process";
2
+ const stderrLimit = 8 * 1024;
3
+ export function spawnServiceContender(command, args) {
4
+ const child = spawn(command, args, { detached: true, stdio: ["ignore", "ignore", "pipe"] });
5
+ let error;
6
+ let closed = false;
7
+ let stderr = Buffer.alloc(0);
8
+ const onStderr = (chunk) => {
9
+ const tail = chunk.subarray(-stderrLimit);
10
+ stderr =
11
+ tail.length === stderrLimit
12
+ ? Buffer.from(tail)
13
+ : Buffer.concat([stderr.subarray(-(stderrLimit - tail.length)), tail]);
14
+ };
15
+ child.stderr?.on("data", onStderr);
16
+ if (child.stderr !== null && "unref" in child.stderr && typeof child.stderr.unref === "function")
17
+ child.stderr.unref();
18
+ child.once("error", (cause) => {
19
+ error = new Error("Failed to start server", { cause });
20
+ });
21
+ child.once("close", () => {
22
+ closed = true;
23
+ });
24
+ child.unref();
25
+ return {
26
+ child,
27
+ error: () => error,
28
+ closed: () => closed,
29
+ stderr: () => stderr.toString("utf8").trim(),
30
+ release: () => {
31
+ child.stderr?.off("data", onStderr);
32
+ child.stderr?.resume();
33
+ stderr = Buffer.alloc(0);
34
+ },
35
+ };
36
+ }
37
+ export function contenderFailure(contender) {
38
+ const error = contender.error();
39
+ if (error !== undefined)
40
+ return error;
41
+ if (contender.child.exitCode !== null && contender.child.exitCode !== 0)
42
+ return startupError(`Server process exited with code ${contender.child.exitCode}`, contender.stderr());
43
+ if (contender.child.signalCode !== null)
44
+ return startupError(`Server process terminated by ${contender.child.signalCode}`, contender.stderr());
45
+ return undefined;
46
+ }
47
+ export function contenderFinished(contender) {
48
+ return contender.error() !== undefined || contender.closed();
49
+ }
50
+ function startupError(message, stderr) {
51
+ return new Error(stderr ? `${message}\n${stderr}` : message);
52
+ }
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-17255",
4
+ "version": "0.0.0-next-17268",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -53,8 +53,8 @@
53
53
  "typecheck": "tsgo --noEmit"
54
54
  },
55
55
  "dependencies": {
56
- "@opencode-ai/schema": "0.0.0-next-17255",
57
- "@opencode-ai/protocol": "0.0.0-next-17255"
56
+ "@opencode-ai/schema": "0.0.0-next-17268",
57
+ "@opencode-ai/protocol": "0.0.0-next-17268"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "effect": "4.0.0-beta.101"
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "devDependencies": {
68
68
  "@effect/platform-node": "4.0.0-beta.101",
69
- "@opencode-ai/httpapi-codegen": "0.0.0-next-17255",
69
+ "@opencode-ai/httpapi-codegen": "0.0.0-next-17268",
70
70
  "@tsconfig/bun": "1.0.9",
71
71
  "@types/bun": "1.3.13",
72
72
  "@typescript/native-preview": "7.0.0-dev.20251207.1",