@otto-code/client 0.8.12 → 0.8.14

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.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { AgentSnapshotPayload, CreateAgentRequestMessage, FetchWorkspacesRequestMessage, FetchWorkspacesResponseMessage, GetProvidersSnapshotResponseMessage, ListAvailableProvidersResponse, ListProviderFeaturesRequestMessage, ListProviderFeaturesResponseMessage, ListProviderModelsResponseMessage, ListProviderModesResponseMessage, MutableDaemonConfig, MutableDaemonConfigPatch, ProviderDiagnosticResponseMessage, ProjectPlacementPayload, RefreshProvidersSnapshotResponseMessage, SendAgentMessageRequest, SessionOutboundMessage, WorkspaceDescriptorPayload } from "@otto-code/protocol/messages";
1
+ import type { AgentSnapshotPayload, CreateAgentRequestMessage, FetchWorkspacesRequestMessage, FetchWorkspacesResponseMessage, GetProvidersSnapshotResponseMessage, ListAvailableProvidersResponse, ListProviderFeaturesRequestMessage, ListProviderFeaturesResponseMessage, ListProviderModelsResponseMessage, ListProviderModesResponseMessage, MutableDaemonConfig, MutableDaemonConfigPatch, ProviderDiagnosticResponseMessage, ProjectPlacementPayload, RefreshProvidersSnapshotResponseMessage, SendAgentMessageRequest, SessionOutboundMessage, WorkspaceDescriptorPayload, WorkspaceCreateRequest } from "@otto-code/protocol/messages";
2
2
  import { DaemonClient } from "./daemon-client.js";
3
- import type { FetchAgentTimelineCursor, FetchAgentTimelineDirection, FetchAgentTimelinePayload, FetchAgentTimelineProjection } from "./daemon-client.js";
3
+ import type { FetchAgentsEntry, FetchAgentsOptions, FetchAgentsPageInfo, FetchAgentTimelineCursor, FetchAgentTimelineDirection, FetchAgentTimelinePayload, FetchAgentTimelineProjection, WaitForFinishResult } from "./daemon-client.js";
4
4
  export { DaemonClient };
5
5
  export type { DaemonClientConfig, DaemonEvent, BrowserAutomationExecuteRequestMessage, BrowserAutomationExecuteResponseMessage, CodeRenameApplyOutcome, CodeRenameApplyQuery, CodeRenamePlan, CodeRenameUndoOutcome, HostingAuthStatusPayload, HostingListOwnersPayload, HostingListRepositoriesPayload, HostingSearchPayload, ProjectScaffoldPayload, WebSocketFactory, WebSocketLike, } from "./daemon-client.js";
6
6
  export type ConnectionState = {
@@ -46,6 +46,13 @@ export interface OttoClientConfig {
46
46
  }
47
47
  export type OttoWorkspace = WorkspaceDescriptorPayload;
48
48
  export type OttoAgent = AgentSnapshotPayload;
49
+ export type OttoAgentListOptions = FetchAgentsOptions;
50
+ export interface OttoAgentListResult {
51
+ requestId: string;
52
+ subscriptionId?: string | null;
53
+ entries: FetchAgentsEntry[];
54
+ pageInfo: FetchAgentsPageInfo;
55
+ }
49
56
  export type OttoWorkspaceListOptions = Omit<FetchWorkspacesRequestMessage, "type" | "requestId"> & {
50
57
  requestId?: string;
51
58
  };
@@ -59,11 +66,9 @@ export interface OttoWorkspaceOpenOptions {
59
66
  cwd: string;
60
67
  requestId?: string;
61
68
  }
62
- export interface OttoWorkspaceOpenResult {
63
- requestId: string;
64
- workspace: OttoWorkspaceHandle | null;
65
- error: string | null;
66
- }
69
+ export type OttoWorkspaceCreateOptions = Omit<WorkspaceCreateRequest, "type" | "requestId"> & {
70
+ requestId?: string;
71
+ };
67
72
  export interface OttoWorkspaceArchiveResult {
68
73
  requestId: string;
69
74
  workspaceId: string;
@@ -74,19 +79,17 @@ export type OttoWorkspaceUpdate = Extract<SessionOutboundMessage, {
74
79
  type: "workspace_update";
75
80
  }>["payload"];
76
81
  export type OttoWorkspaceUpdateHandler = (update: OttoWorkspaceUpdate) => void;
77
- /**
78
- * A handle is a stable typed reference to a daemon resource. Its identity is the
79
- * daemon id, and `latest()` only returns the most recent snapshot this handle has
80
- * seen through construction, `refetch()`, or this handle's local subscription.
81
- */
82
82
  export interface OttoWorkspaceHandle {
83
83
  readonly id: string;
84
- latest(): OttoWorkspace | null;
85
- /**
86
- * Fetches a fresh workspace snapshot through the existing workspace list RPC,
87
- * exact-matches this handle id from the result, and updates `latest()`.
88
- */
89
- refetch(options?: {
84
+ readonly projectId: string | null;
85
+ readonly directory: string | null;
86
+ readonly name: string | null;
87
+ readonly status: OttoWorkspace["status"] | null;
88
+ readonly agents: {
89
+ create(options: OttoWorkspaceAgentCreateOptions): Promise<OttoAgentHandle>;
90
+ };
91
+ current(): OttoWorkspace | null;
92
+ refresh(options?: {
90
93
  requestId?: string;
91
94
  }): Promise<OttoWorkspace | null>;
92
95
  archive(requestId?: string): Promise<OttoWorkspaceArchiveResult>;
@@ -101,8 +104,8 @@ export interface OttoWorkspaceHandle {
101
104
  export interface OttoWorkspaceActions {
102
105
  list(options?: OttoWorkspaceListOptions): Promise<OttoWorkspaceListResult>;
103
106
  ref(workspace: string | OttoWorkspace): OttoWorkspaceHandle;
104
- open(input: string | OttoWorkspaceOpenOptions, requestId?: string): Promise<OttoWorkspaceOpenResult>;
105
- create(input: string | OttoWorkspaceOpenOptions, requestId?: string): Promise<OttoWorkspaceOpenResult>;
107
+ open(input: string | OttoWorkspaceOpenOptions, requestId?: string): Promise<OttoWorkspaceHandle>;
108
+ create(options: OttoWorkspaceCreateOptions): Promise<OttoWorkspaceHandle>;
106
109
  archive(workspace: string | OttoWorkspaceHandle, requestId?: string): Promise<OttoWorkspaceArchiveResult>;
107
110
  /**
108
111
  * Local event subscription over the low-level driver's workspace_update stream.
@@ -111,23 +114,38 @@ export interface OttoWorkspaceActions {
111
114
  subscribe(handler: OttoWorkspaceUpdateHandler): () => void;
112
115
  }
113
116
  type OttoAgentSessionConfig = CreateAgentRequestMessage["config"];
114
- type OttoAgentProvider = OttoAgentSessionConfig["provider"];
115
- type OttoAgentConfigOverrides = Partial<Omit<OttoAgentSessionConfig, "provider" | "cwd">>;
116
- export interface OttoAgentCreateOptions extends OttoAgentConfigOverrides {
117
- config?: OttoAgentSessionConfig;
118
- provider?: CreateAgentRequestMessage["config"]["provider"];
119
- cwd?: string;
120
- workspaceId?: string;
121
- callerAgentId?: string;
122
- initialPrompt?: string;
117
+ export type OttoAgentProvider = OttoAgentSessionConfig["provider"];
118
+ export type OttoProviderFeatureValues = Record<string, unknown>;
119
+ export interface OttoAgentConfig {
120
+ /** Provider and model in `provider/model` format. */
121
+ provider: string;
122
+ modeId?: OttoAgentSessionConfig["modeId"];
123
+ thinkingOptionId?: OttoAgentSessionConfig["thinkingOptionId"];
124
+ featureValues?: OttoProviderFeatureValues;
125
+ /** JSON-safe provider-native settings, validated by the selected provider. */
126
+ options?: OttoAgentSessionConfig["providerOptions"];
127
+ systemPrompt?: OttoAgentSessionConfig["systemPrompt"];
128
+ toolPolicy?: OttoAgentSessionConfig["toolPolicy"];
129
+ mcpServers?: OttoAgentSessionConfig["mcpServers"];
130
+ }
131
+ export interface OttoAgentCreateOptions {
132
+ config: OttoAgentConfig;
133
+ cwd: string;
134
+ parent?: string | OttoAgentHandle;
135
+ title?: OttoAgentSessionConfig["title"];
136
+ env?: CreateAgentRequestMessage["env"];
137
+ prompt?: string;
123
138
  clientMessageId?: string;
124
139
  outputSchema?: Record<string, unknown>;
125
140
  images?: CreateAgentRequestMessage["images"];
126
141
  attachments?: CreateAgentRequestMessage["attachments"];
127
142
  git?: CreateAgentRequestMessage["git"];
143
+ worktree?: CreateAgentRequestMessage["worktree"];
144
+ autoArchive?: CreateAgentRequestMessage["autoArchive"];
128
145
  requestId?: string;
129
146
  labels?: Record<string, string>;
130
147
  }
148
+ export type OttoWorkspaceAgentCreateOptions = Omit<OttoAgentCreateOptions, "cwd">;
131
149
  export interface OttoAgentRefetchResult {
132
150
  agent: OttoAgent;
133
151
  project: ProjectPlacementPayload | null;
@@ -147,6 +165,10 @@ export interface OttoAgentSendOptions {
147
165
  }>;
148
166
  attachments?: SendAgentMessageRequest["attachments"];
149
167
  }
168
+ export interface OttoAgentRunOptions extends OttoAgentSendOptions {
169
+ timeoutMs?: number;
170
+ }
171
+ export type OttoAgentRunResult = WaitForFinishResult;
150
172
  export type OttoAgentUpdate = Extract<SessionOutboundMessage, {
151
173
  type: "agent_update";
152
174
  }>["payload"];
@@ -157,8 +179,8 @@ export type OttoAgentUpdateHandler = (update: OttoAgentUpdate) => void;
157
179
  export interface OttoAgentTimelineHandle {
158
180
  /**
159
181
  * Fetches a fresh timeline page through the existing daemon RPC. If the daemon
160
- * includes an agent snapshot in the response, the parent handle's `latest()`
161
- * is updated to that snapshot.
182
+ * includes an agent snapshot in the response, the parent handle is updated to
183
+ * that value.
162
184
  */
163
185
  refetch(options?: OttoAgentTimelineRefetchOptions): Promise<FetchAgentTimelinePayload>;
164
186
  /**
@@ -167,18 +189,19 @@ export interface OttoAgentTimelineHandle {
167
189
  */
168
190
  subscribe(handler: (event: OttoAgentStream) => void): () => void;
169
191
  }
170
- /**
171
- * Agent handles follow the same identity/snapshot rule as workspace handles:
172
- * `id` is stable, while `latest()` is only the newest snapshot observed by this
173
- * handle through construction, `refetch()`, timeline refetch, archive, or local
174
- * agent_update subscription.
175
- */
176
192
  export interface OttoAgentHandle {
177
193
  readonly id: string;
194
+ readonly workspaceId: string | null;
195
+ readonly cwd: string | null;
196
+ readonly status: OttoAgent["status"] | null;
178
197
  readonly timeline: OttoAgentTimelineHandle;
179
- latest(): OttoAgent | null;
180
- refetch(requestId?: string): Promise<OttoAgentRefetchResult | null>;
198
+ current(): OttoAgent | null;
199
+ refresh(requestId?: string): Promise<OttoAgentRefetchResult | null>;
181
200
  send(text: string, options?: OttoAgentSendOptions): Promise<void>;
201
+ /** Sends a prompt and resolves when that turn finishes or needs attention. */
202
+ run(text: string, options?: OttoAgentRunOptions): Promise<OttoAgentRunResult>;
203
+ /** Waits for the current turn, including one started with `prompt`. */
204
+ waitForFinish(timeoutMs?: number): Promise<OttoAgentRunResult>;
182
205
  archive(): Promise<{
183
206
  archivedAt: string;
184
207
  }>;
@@ -186,6 +209,7 @@ export interface OttoAgentHandle {
186
209
  subscribe(handler: (update: OttoAgentUpdate) => void): () => void;
187
210
  }
188
211
  export interface OttoAgentActions {
212
+ list(options?: OttoAgentListOptions): Promise<OttoAgentListResult>;
189
213
  ref(agent: string | OttoAgent): OttoAgentHandle;
190
214
  create(options: OttoAgentCreateOptions): Promise<OttoAgentHandle>;
191
215
  /**
@@ -194,19 +218,13 @@ export interface OttoAgentActions {
194
218
  */
195
219
  subscribe(handler: OttoAgentUpdateHandler): () => void;
196
220
  }
197
- export interface OttoProviderConfig extends OttoProviderConfigInput {
198
- provider: OttoAgentProvider;
199
- }
200
- export type OttoProviderFeatureValues = Record<string, unknown>;
201
- export interface OttoProviderConfigInput {
202
- model?: string;
203
- modeId?: string;
204
- thinkingOptionId?: string;
205
- featureValues?: OttoProviderFeatureValues;
206
- }
207
221
  export type OttoProviderModelsResult = ListProviderModelsResponseMessage["payload"];
208
222
  export type OttoProviderModesResult = ListProviderModesResponseMessage["payload"];
209
- export type OttoProviderFeaturesInput = ListProviderFeaturesRequestMessage["draftConfig"];
223
+ type OttoProviderFeaturesDraft = ListProviderFeaturesRequestMessage["draftConfig"];
224
+ export interface OttoProviderFeaturesInput extends Omit<OttoProviderFeaturesDraft, "provider" | "model"> {
225
+ /** Provider and model in `provider/model` format. */
226
+ provider: string;
227
+ }
210
228
  export type OttoProviderFeaturesResult = ListProviderFeaturesResponseMessage["payload"];
211
229
  export type OttoProviderAvailabilityResult = ListAvailableProvidersResponse["payload"];
212
230
  export type OttoProviderSnapshotResult = GetProvidersSnapshotResponseMessage["payload"];
@@ -224,12 +242,10 @@ export interface OttoProviderRefreshOptions {
224
242
  providers?: OttoAgentProvider[];
225
243
  requestId?: string;
226
244
  }
245
+ export interface OttoProviderWaitOptions extends OttoProviderListOptions {
246
+ timeoutMs?: number;
247
+ }
227
248
  export interface OttoProviderActions {
228
- codex(input?: OttoProviderConfigInput): OttoProviderConfig;
229
- claude(input?: OttoProviderConfigInput): OttoProviderConfig;
230
- opencode(input?: OttoProviderConfigInput): OttoProviderConfig;
231
- copilot(input?: OttoProviderConfigInput): OttoProviderConfig;
232
- config(provider: OttoAgentProvider, input?: OttoProviderConfigInput): OttoProviderConfig;
233
249
  listModels(provider: OttoAgentProvider, options?: OttoProviderListOptions): Promise<OttoProviderModelsResult>;
234
250
  listModes(provider: OttoAgentProvider, options?: OttoProviderListOptions): Promise<OttoProviderModesResult>;
235
251
  listFeatures(draftConfig: OttoProviderFeaturesInput, options?: {
@@ -239,6 +255,8 @@ export interface OttoProviderActions {
239
255
  requestId?: string;
240
256
  }): Promise<OttoProviderAvailabilityResult>;
241
257
  snapshot(options?: OttoProviderListOptions): Promise<OttoProviderSnapshotResult>;
258
+ /** Resolves after the daemon's lazy provider discovery has finished. */
259
+ waitForReady(options?: OttoProviderWaitOptions): Promise<OttoProviderSnapshotResult>;
242
260
  refresh(options?: OttoProviderRefreshOptions): Promise<OttoProviderRefreshResult>;
243
261
  diagnostic(provider: OttoAgentProvider, options?: {
244
262
  requestId?: string;
package/dist/index.js CHANGED
@@ -1,45 +1,74 @@
1
1
  import { DaemonClient } from "./daemon-client.js";
2
2
  export { DaemonClient };
3
+ /**
4
+ * Coding turns routinely run for minutes, so the handle waits far longer than
5
+ * the transport's own conservative default.
6
+ */
7
+ const DEFAULT_WAIT_FOR_FINISH_MS = 10 * 60000;
3
8
  export function createOttoClient(config) {
4
9
  const daemonClient = new DaemonClient({
5
10
  ...config,
6
11
  clientId: config.clientId ?? createGeneratedClientId(),
7
12
  clientType: "cli",
8
13
  });
9
- const createWorkspaceHandle = createWorkspaceHandleFactory(daemonClient);
10
14
  const createAgentHandle = createAgentHandleFactory(daemonClient);
15
+ const createAgent = async (options, placement) => {
16
+ const { config: agentConfig, cwd, parent, title, prompt, ...requestOptions } = options;
17
+ const { provider: providerModel, options: providerOptions, ...runtimeConfig } = agentConfig;
18
+ const { provider, model } = parseProviderModel(providerModel);
19
+ const effectiveCwd = placement?.cwd ?? cwd;
20
+ const agent = await daemonClient.createAgent({
21
+ ...requestOptions,
22
+ config: {
23
+ ...runtimeConfig,
24
+ provider,
25
+ model,
26
+ cwd: effectiveCwd,
27
+ ...(title !== undefined ? { title } : {}),
28
+ ...(providerOptions !== undefined ? { providerOptions } : {}),
29
+ },
30
+ ...(placement ? { workspaceId: placement.workspaceId } : {}),
31
+ ...(parent ? { callerAgentId: resolveAgentId(parent) } : {}),
32
+ ...(prompt !== undefined ? { initialPrompt: prompt } : {}),
33
+ });
34
+ return createAgentHandle(agent);
35
+ };
36
+ const createWorkspaceHandle = createWorkspaceHandleFactory(daemonClient, createAgent);
11
37
  return {
12
38
  workspaces: {
13
39
  list: (options) => daemonClient.fetchWorkspaces(options),
14
40
  ref: (workspace) => createWorkspaceHandle(workspace),
15
41
  open: (input, requestId) => openWorkspace(daemonClient, createWorkspaceHandle, input, requestId),
16
- create: (input, requestId) => openWorkspace(daemonClient, createWorkspaceHandle, input, requestId),
42
+ create: async ({ requestId, ...options }) => {
43
+ const result = await daemonClient.createWorkspace(options, requestId);
44
+ if (result.error || !result.workspace) {
45
+ throw new Error(result.error ?? "The daemon did not create a workspace");
46
+ }
47
+ return createWorkspaceHandle(result.workspace);
48
+ },
17
49
  archive: (workspace, requestId) => daemonClient.archiveWorkspace(resolveWorkspaceId(workspace), { requestId }),
18
50
  subscribe: (handler) => daemonClient.on("workspace_update", (message) => {
19
51
  handler(message.payload);
20
52
  }),
21
53
  },
22
54
  agents: {
55
+ list: (options) => daemonClient.fetchAgents(options),
23
56
  ref: (agent) => createAgentHandle(agent),
24
- create: async (options) => {
25
- const agent = await daemonClient.createAgent(options);
26
- return createAgentHandle(agent);
27
- },
57
+ create: (options) => createAgent(options),
28
58
  subscribe: (handler) => daemonClient.on("agent_update", (message) => {
29
59
  handler(message.payload);
30
60
  }),
31
61
  },
32
62
  providers: {
33
- codex: (input) => providerConfig("codex", input),
34
- claude: (input) => providerConfig("claude", input),
35
- opencode: (input) => providerConfig("opencode", input),
36
- copilot: (input) => providerConfig("copilot", input),
37
- config: (provider, input) => providerConfig(provider, input),
38
63
  listModels: (provider, options) => daemonClient.listProviderModels(provider, options),
39
64
  listModes: (provider, options) => daemonClient.listProviderModes(provider, options),
40
- listFeatures: (draftConfig, options) => daemonClient.listProviderFeatures(draftConfig, options),
65
+ listFeatures: ({ provider: providerModel, ...draftConfig }, options) => {
66
+ const { provider, model } = parseProviderModel(providerModel);
67
+ return daemonClient.listProviderFeatures({ ...draftConfig, provider, model }, options);
68
+ },
41
69
  listAvailable: (options) => daemonClient.listAvailableProviders(options),
42
70
  snapshot: (options) => daemonClient.getProvidersSnapshot(options),
71
+ waitForReady: (options) => waitForProvidersReady(daemonClient, options),
43
72
  refresh: (options) => daemonClient.refreshProvidersSnapshot(options),
44
73
  diagnostic: (provider, options) => daemonClient.getProviderDiagnostic(provider, options),
45
74
  subscribe: (handler) => daemonClient.on("providers_snapshot_update", (message) => {
@@ -56,39 +85,69 @@ export function createOttoClient(config) {
56
85
  getConnectionState: () => daemonClient.getConnectionState(),
57
86
  };
58
87
  }
59
- function createWorkspaceHandleFactory(daemonClient) {
88
+ function createWorkspaceHandleFactory(daemonClient, createAgent) {
60
89
  return (workspace) => {
61
90
  const id = typeof workspace === "string" ? workspace : workspace.id;
62
- let latest = typeof workspace === "string" ? null : workspace;
63
- return {
64
- id,
65
- latest: () => latest,
66
- refetch: async (options) => {
67
- // Best-effort: fetches one page and matches by id client-side, so a workspace beyond
68
- // the first page won't be found. TODO: add a "get workspace by id" lookup and resolve
69
- // by exact id instead of paging.
91
+ let current = typeof workspace === "string" ? null : workspace;
92
+ const refresh = async (options) => {
93
+ let cursor;
94
+ let requestId = options?.requestId;
95
+ do {
70
96
  const result = await daemonClient.fetchWorkspaces({
71
- requestId: options?.requestId,
72
- page: { limit: 25 },
97
+ requestId,
98
+ page: { limit: 200, ...(cursor ? { cursor } : {}) },
73
99
  });
74
- latest = result.entries.find((entry) => entry.id === id) ?? null;
75
- return latest;
100
+ const match = result.entries.find((entry) => entry.id === id);
101
+ if (match) {
102
+ current = match;
103
+ return current;
104
+ }
105
+ cursor = result.pageInfo.nextCursor ?? undefined;
106
+ requestId = undefined;
107
+ } while (cursor);
108
+ current = null;
109
+ return current;
110
+ };
111
+ return {
112
+ id,
113
+ get projectId() {
114
+ return current?.projectId ?? null;
115
+ },
116
+ get directory() {
117
+ return current?.workspaceDirectory ?? null;
118
+ },
119
+ get name() {
120
+ return current?.name ?? null;
121
+ },
122
+ get status() {
123
+ return current?.status ?? null;
124
+ },
125
+ agents: {
126
+ create: async (options) => {
127
+ const snapshot = current ?? (await refresh());
128
+ if (!snapshot?.workspaceDirectory) {
129
+ throw new Error(`Workspace ${id} has no available directory`);
130
+ }
131
+ return createAgent({ ...options, cwd: snapshot.workspaceDirectory }, { workspaceId: id, cwd: snapshot.workspaceDirectory });
132
+ },
76
133
  },
134
+ current: () => current,
135
+ refresh,
77
136
  archive: async (requestId) => {
78
137
  const result = await daemonClient.archiveWorkspace(id, { requestId });
79
- if (latest) {
80
- latest = { ...latest, archivingAt: result.archivedAt };
138
+ if (current) {
139
+ current = { ...current, archivingAt: result.archivedAt };
81
140
  }
82
141
  return result;
83
142
  },
84
143
  subscribe: (handler) => daemonClient.on("workspace_update", (message) => {
85
144
  const update = message.payload;
86
145
  if (update.kind === "upsert" && update.workspace.id === id) {
87
- latest = update.workspace;
146
+ current = update.workspace;
88
147
  handler(update);
89
148
  }
90
149
  if (update.kind === "remove" && update.id === id) {
91
- latest = null;
150
+ current = null;
92
151
  handler(update);
93
152
  }
94
153
  }),
@@ -98,14 +157,14 @@ function createWorkspaceHandleFactory(daemonClient) {
98
157
  function createAgentHandleFactory(daemonClient) {
99
158
  return (agent) => {
100
159
  const id = typeof agent === "string" ? agent : agent.id;
101
- let latest = typeof agent === "string" ? null : agent;
160
+ let current = typeof agent === "string" ? null : agent;
102
161
  const handle = {
103
162
  id,
104
163
  timeline: {
105
164
  refetch: async (options) => {
106
165
  const result = await daemonClient.fetchAgentTimeline(id, options);
107
166
  if (result.agent) {
108
- latest = result.agent;
167
+ current = result.agent;
109
168
  }
110
169
  return result;
111
170
  },
@@ -115,10 +174,35 @@ function createAgentHandleFactory(daemonClient) {
115
174
  }
116
175
  }),
117
176
  },
118
- latest: () => latest,
119
- refetch: async (requestId) => {
177
+ get workspaceId() {
178
+ return current?.workspaceId ?? null;
179
+ },
180
+ get cwd() {
181
+ return current?.cwd ?? null;
182
+ },
183
+ get status() {
184
+ return current?.status ?? null;
185
+ },
186
+ current: () => current,
187
+ refresh: async (requestId) => {
120
188
  const result = await daemonClient.fetchAgent({ agentId: id, requestId });
121
- latest = result?.agent ?? null;
189
+ current = result?.agent ?? null;
190
+ return result;
191
+ },
192
+ run: async (text, options) => {
193
+ const { timeoutMs, ...sendOptions } = options ?? {};
194
+ await daemonClient.sendAgentMessage(id, text, sendOptions);
195
+ const result = await daemonClient.waitForFinish(id, timeoutMs ?? DEFAULT_WAIT_FOR_FINISH_MS);
196
+ if (result.final) {
197
+ current = result.final;
198
+ }
199
+ return result;
200
+ },
201
+ waitForFinish: async (timeoutMs) => {
202
+ const result = await daemonClient.waitForFinish(id, timeoutMs ?? DEFAULT_WAIT_FOR_FINISH_MS);
203
+ if (result.final) {
204
+ current = result.final;
205
+ }
122
206
  return result;
123
207
  },
124
208
  send: async (text, options) => {
@@ -126,8 +210,8 @@ function createAgentHandleFactory(daemonClient) {
126
210
  },
127
211
  archive: async () => {
128
212
  const result = await daemonClient.archiveAgent(id);
129
- if (latest) {
130
- latest = { ...latest, archivedAt: result.archivedAt };
213
+ if (current) {
214
+ current = { ...current, archivedAt: result.archivedAt };
131
215
  }
132
216
  return result;
133
217
  },
@@ -137,11 +221,11 @@ function createAgentHandleFactory(daemonClient) {
137
221
  subscribe: (handler) => daemonClient.on("agent_update", (message) => {
138
222
  const update = message.payload;
139
223
  if (update.kind === "upsert" && update.agent.id === id) {
140
- latest = update.agent;
224
+ current = update.agent;
141
225
  handler(update);
142
226
  }
143
227
  if (update.kind === "remove" && update.agentId === id) {
144
- latest = null;
228
+ current = null;
145
229
  handler(update);
146
230
  }
147
231
  }),
@@ -152,23 +236,99 @@ function createAgentHandleFactory(daemonClient) {
152
236
  async function openWorkspace(daemonClient, createWorkspaceHandle, input, requestId) {
153
237
  const options = typeof input === "string" ? { cwd: input, requestId } : input;
154
238
  const result = await daemonClient.openProject(options.cwd, options.requestId);
155
- return {
156
- ...result,
157
- workspace: result.workspace ? createWorkspaceHandle(result.workspace) : null,
158
- };
239
+ if (result.error || !result.workspace) {
240
+ throw new Error(result.error ?? `The daemon did not open a workspace for ${options.cwd}`);
241
+ }
242
+ return createWorkspaceHandle(result.workspace);
159
243
  }
160
244
  function resolveWorkspaceId(workspace) {
161
245
  return typeof workspace === "string" ? workspace : workspace.id;
162
246
  }
163
- function providerConfig(provider, input = {}) {
247
+ function resolveAgentId(agent) {
248
+ return typeof agent === "string" ? agent : agent.id;
249
+ }
250
+ function parseProviderModel(selection) {
251
+ const separator = selection.indexOf("/");
252
+ if (separator <= 0 || separator === selection.length - 1) {
253
+ throw new Error('Expected config.provider in "provider/model" format');
254
+ }
164
255
  return {
165
- provider,
166
- ...(input.model !== undefined ? { model: input.model } : {}),
167
- ...(input.modeId !== undefined ? { modeId: input.modeId } : {}),
168
- ...(input.thinkingOptionId !== undefined ? { thinkingOptionId: input.thinkingOptionId } : {}),
169
- ...(input.featureValues !== undefined ? { featureValues: input.featureValues } : {}),
256
+ provider: selection.slice(0, separator),
257
+ model: selection.slice(separator + 1),
170
258
  };
171
259
  }
260
+ function waitForProvidersReady(daemonClient, options = {}) {
261
+ // COMPAT(providersSnapshotCwd): added in v0.3.2, remove gate after 2027-02-10.
262
+ if (daemonClient.getLastServerInfoMessage()?.features?.providersSnapshotCwd !== true) {
263
+ return Promise.reject(new Error("Update the host to wait for provider discovery."));
264
+ }
265
+ const { timeoutMs = 60000, ...snapshotOptions } = options;
266
+ return new Promise((resolve, reject) => {
267
+ let settled = false;
268
+ let requestId = null;
269
+ let snapshotCwd;
270
+ const pendingUpdates = new Map();
271
+ let latestEntries = [];
272
+ const cleanup = () => {
273
+ clearTimeout(timeout);
274
+ unsubscribe();
275
+ };
276
+ const finish = (snapshot) => {
277
+ if (settled)
278
+ return;
279
+ settled = true;
280
+ cleanup();
281
+ resolve(snapshot);
282
+ };
283
+ const fail = (error) => {
284
+ if (settled)
285
+ return;
286
+ settled = true;
287
+ cleanup();
288
+ reject(error instanceof Error ? error : new Error(String(error)));
289
+ };
290
+ const updateMatches = (update) => update.cwd === snapshotCwd;
291
+ const unsubscribe = daemonClient.on("providers_snapshot_update", (message) => {
292
+ const update = message.payload;
293
+ if (!requestId) {
294
+ pendingUpdates.set(update.cwd, update);
295
+ return;
296
+ }
297
+ if (!updateMatches(update))
298
+ return;
299
+ latestEntries = update.entries;
300
+ if (update.entries.some((entry) => entry.status === "loading"))
301
+ return;
302
+ finish({ ...update, requestId });
303
+ });
304
+ const timeout = setTimeout(() => {
305
+ const loading = latestEntries
306
+ .filter((entry) => entry.status === "loading")
307
+ .map((entry) => entry.provider)
308
+ .join(", ");
309
+ fail(new Error(loading
310
+ ? `Timed out waiting for providers: ${loading}`
311
+ : "Timed out waiting for provider discovery"));
312
+ }, timeoutMs);
313
+ void daemonClient
314
+ .getProvidersSnapshot(snapshotOptions)
315
+ .then((snapshot) => {
316
+ requestId = snapshot.requestId;
317
+ snapshotCwd = snapshot.cwd;
318
+ latestEntries = snapshot.entries;
319
+ if (!snapshot.entries.some((entry) => entry.status === "loading")) {
320
+ finish(snapshot);
321
+ return;
322
+ }
323
+ const pendingUpdate = pendingUpdates.get(snapshotCwd);
324
+ if (pendingUpdate && !pendingUpdate.entries.some((entry) => entry.status === "loading")) {
325
+ finish({ ...pendingUpdate, requestId });
326
+ }
327
+ return undefined;
328
+ })
329
+ .catch(fail);
330
+ });
331
+ }
172
332
  function createGeneratedClientId() {
173
333
  const randomId = typeof globalThis.crypto?.randomUUID === "function"
174
334
  ? globalThis.crypto.randomUUID()
@@ -56,6 +56,7 @@ export class TerminalStreamRouter {
56
56
  payload: encodeTerminalResizePayload({
57
57
  rows: message.rows,
58
58
  cols: message.cols,
59
+ ...(message.intent ? { intent: message.intent } : {}),
59
60
  }),
60
61
  });
61
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otto-code/client",
3
- "version": "0.8.12",
3
+ "version": "0.8.14",
4
4
  "description": "Otto client SDK package",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "files": [
@@ -21,6 +21,10 @@
21
21
  "./internal/daemon-client-transport-types": {
22
22
  "types": "./dist/daemon-client-transport-types.d.ts",
23
23
  "default": "./dist/daemon-client-transport-types.js"
24
+ },
25
+ "./internal/daemon-client-websocket-transport": {
26
+ "types": "./dist/daemon-client-websocket-transport.d.ts",
27
+ "default": "./dist/daemon-client-websocket-transport.js"
24
28
  }
25
29
  },
26
30
  "publishConfig": {
@@ -36,8 +40,8 @@
36
40
  "test": "vitest run"
37
41
  },
38
42
  "dependencies": {
39
- "@otto-code/protocol": "0.8.12",
40
- "@otto-code/relay": "0.8.12",
43
+ "@otto-code/protocol": "0.8.14",
44
+ "@otto-code/relay": "0.8.14",
41
45
  "zod": "^4.4.3"
42
46
  },
43
47
  "devDependencies": {