@otto-code/cli 0.5.0

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 (200) hide show
  1. package/bin/otto +3 -0
  2. package/dist/classify.d.ts +19 -0
  3. package/dist/classify.js +49 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.js +113 -0
  6. package/dist/commands/agent/archive.d.ts +18 -0
  7. package/dist/commands/agent/archive.js +102 -0
  8. package/dist/commands/agent/attach.d.ts +11 -0
  9. package/dist/commands/agent/attach.js +159 -0
  10. package/dist/commands/agent/delete.d.ts +15 -0
  11. package/dist/commands/agent/delete.js +107 -0
  12. package/dist/commands/agent/import.d.ts +14 -0
  13. package/dist/commands/agent/import.js +122 -0
  14. package/dist/commands/agent/index.d.ts +3 -0
  15. package/dist/commands/agent/index.js +48 -0
  16. package/dist/commands/agent/inspect.d.ts +15 -0
  17. package/dist/commands/agent/inspect.js +209 -0
  18. package/dist/commands/agent/logs.d.ts +19 -0
  19. package/dist/commands/agent/logs.js +180 -0
  20. package/dist/commands/agent/ls.d.ts +44 -0
  21. package/dist/commands/agent/ls.js +204 -0
  22. package/dist/commands/agent/mode.d.ts +18 -0
  23. package/dist/commands/agent/mode.js +100 -0
  24. package/dist/commands/agent/reload.d.ts +15 -0
  25. package/dist/commands/agent/reload.js +75 -0
  26. package/dist/commands/agent/run.d.ts +44 -0
  27. package/dist/commands/agent/run.js +464 -0
  28. package/dist/commands/agent/send.d.ts +19 -0
  29. package/dist/commands/agent/send.js +187 -0
  30. package/dist/commands/agent/stop.d.ts +17 -0
  31. package/dist/commands/agent/stop.js +115 -0
  32. package/dist/commands/agent/update.d.ts +18 -0
  33. package/dist/commands/agent/update.js +139 -0
  34. package/dist/commands/agent/wait.d.ts +17 -0
  35. package/dist/commands/agent/wait.js +158 -0
  36. package/dist/commands/chat/create.d.ts +9 -0
  37. package/dist/commands/chat/create.js +23 -0
  38. package/dist/commands/chat/delete.d.ts +6 -0
  39. package/dist/commands/chat/delete.js +20 -0
  40. package/dist/commands/chat/index.d.ts +3 -0
  41. package/dist/commands/chat/index.js +47 -0
  42. package/dist/commands/chat/inspect.d.ts +6 -0
  43. package/dist/commands/chat/inspect.js +20 -0
  44. package/dist/commands/chat/ls.d.ts +6 -0
  45. package/dist/commands/chat/ls.js +20 -0
  46. package/dist/commands/chat/post.d.ts +9 -0
  47. package/dist/commands/chat/post.js +28 -0
  48. package/dist/commands/chat/read.d.ts +11 -0
  49. package/dist/commands/chat/read.js +40 -0
  50. package/dist/commands/chat/schema.d.ts +36 -0
  51. package/dist/commands/chat/schema.js +81 -0
  52. package/dist/commands/chat/shared.d.ts +19 -0
  53. package/dist/commands/chat/shared.js +118 -0
  54. package/dist/commands/chat/wait.d.ts +9 -0
  55. package/dist/commands/chat/wait.js +45 -0
  56. package/dist/commands/daemon/index.d.ts +3 -0
  57. package/dist/commands/daemon/index.js +57 -0
  58. package/dist/commands/daemon/local-daemon.d.ts +78 -0
  59. package/dist/commands/daemon/local-daemon.js +544 -0
  60. package/dist/commands/daemon/pair.d.ts +9 -0
  61. package/dist/commands/daemon/pair.js +76 -0
  62. package/dist/commands/daemon/restart.d.ts +12 -0
  63. package/dist/commands/daemon/restart.js +99 -0
  64. package/dist/commands/daemon/runtime-toolchain.d.ts +6 -0
  65. package/dist/commands/daemon/runtime-toolchain.js +87 -0
  66. package/dist/commands/daemon/set-password.d.ts +17 -0
  67. package/dist/commands/daemon/set-password.js +82 -0
  68. package/dist/commands/daemon/start.d.ts +6 -0
  69. package/dist/commands/daemon/start.js +56 -0
  70. package/dist/commands/daemon/status.d.ts +10 -0
  71. package/dist/commands/daemon/status.js +338 -0
  72. package/dist/commands/daemon/stop.d.ts +15 -0
  73. package/dist/commands/daemon/stop.js +60 -0
  74. package/dist/commands/hooks.d.ts +19 -0
  75. package/dist/commands/hooks.js +89 -0
  76. package/dist/commands/loop/index.d.ts +3 -0
  77. package/dist/commands/loop/index.js +18 -0
  78. package/dist/commands/loop/inspect.d.ts +13 -0
  79. package/dist/commands/loop/inspect.js +100 -0
  80. package/dist/commands/loop/logs.d.ts +8 -0
  81. package/dist/commands/loop/logs.js +69 -0
  82. package/dist/commands/loop/ls.d.ts +18 -0
  83. package/dist/commands/loop/ls.js +62 -0
  84. package/dist/commands/loop/run.d.ts +30 -0
  85. package/dist/commands/loop/run.js +162 -0
  86. package/dist/commands/loop/stop.d.ts +15 -0
  87. package/dist/commands/loop/stop.js +56 -0
  88. package/dist/commands/loop/types.d.ts +138 -0
  89. package/dist/commands/loop/types.js +2 -0
  90. package/dist/commands/onboard.d.ts +10 -0
  91. package/dist/commands/onboard.js +416 -0
  92. package/dist/commands/open.d.ts +2 -0
  93. package/dist/commands/open.js +81 -0
  94. package/dist/commands/permit/allow.d.ts +20 -0
  95. package/dist/commands/permit/allow.js +133 -0
  96. package/dist/commands/permit/deny.d.ts +12 -0
  97. package/dist/commands/permit/deny.js +105 -0
  98. package/dist/commands/permit/index.d.ts +3 -0
  99. package/dist/commands/permit/index.js +27 -0
  100. package/dist/commands/permit/ls.d.ts +18 -0
  101. package/dist/commands/permit/ls.js +66 -0
  102. package/dist/commands/provider/index.d.ts +3 -0
  103. package/dist/commands/provider/index.js +16 -0
  104. package/dist/commands/provider/ls.d.ts +19 -0
  105. package/dist/commands/provider/ls.js +73 -0
  106. package/dist/commands/provider/models.d.ts +20 -0
  107. package/dist/commands/provider/models.js +53 -0
  108. package/dist/commands/schedule/create.d.ts +18 -0
  109. package/dist/commands/schedule/create.js +40 -0
  110. package/dist/commands/schedule/delete.d.ts +10 -0
  111. package/dist/commands/schedule/delete.js +32 -0
  112. package/dist/commands/schedule/index.d.ts +3 -0
  113. package/dist/commands/schedule/index.js +66 -0
  114. package/dist/commands/schedule/inspect.d.ts +6 -0
  115. package/dist/commands/schedule/inspect.js +24 -0
  116. package/dist/commands/schedule/logs.d.ts +6 -0
  117. package/dist/commands/schedule/logs.js +23 -0
  118. package/dist/commands/schedule/ls.d.ts +5 -0
  119. package/dist/commands/schedule/ls.js +23 -0
  120. package/dist/commands/schedule/pause.d.ts +5 -0
  121. package/dist/commands/schedule/pause.js +23 -0
  122. package/dist/commands/schedule/resume.d.ts +5 -0
  123. package/dist/commands/schedule/resume.js +23 -0
  124. package/dist/commands/schedule/run-once.d.ts +5 -0
  125. package/dist/commands/schedule/run-once.js +23 -0
  126. package/dist/commands/schedule/schema.d.ts +21 -0
  127. package/dist/commands/schedule/schema.js +67 -0
  128. package/dist/commands/schedule/shared.d.ts +56 -0
  129. package/dist/commands/schedule/shared.js +346 -0
  130. package/dist/commands/schedule/types.d.ts +168 -0
  131. package/dist/commands/schedule/types.js +2 -0
  132. package/dist/commands/schedule/update.d.ts +21 -0
  133. package/dist/commands/schedule/update.js +39 -0
  134. package/dist/commands/speech/index.d.ts +3 -0
  135. package/dist/commands/speech/index.js +5 -0
  136. package/dist/commands/terminal/capture.d.ts +10 -0
  137. package/dist/commands/terminal/capture.js +68 -0
  138. package/dist/commands/terminal/create.d.ts +10 -0
  139. package/dist/commands/terminal/create.js +38 -0
  140. package/dist/commands/terminal/index.d.ts +3 -0
  141. package/dist/commands/terminal/index.js +43 -0
  142. package/dist/commands/terminal/kill.d.ts +6 -0
  143. package/dist/commands/terminal/kill.js +36 -0
  144. package/dist/commands/terminal/ls.d.ts +10 -0
  145. package/dist/commands/terminal/ls.js +21 -0
  146. package/dist/commands/terminal/schema.d.ts +18 -0
  147. package/dist/commands/terminal/schema.js +23 -0
  148. package/dist/commands/terminal/send-keys.d.ts +7 -0
  149. package/dist/commands/terminal/send-keys.js +76 -0
  150. package/dist/commands/terminal/shared.d.ts +12 -0
  151. package/dist/commands/terminal/shared.js +61 -0
  152. package/dist/commands/worktree/archive.d.ts +20 -0
  153. package/dist/commands/worktree/archive.js +104 -0
  154. package/dist/commands/worktree/create-input.d.ts +15 -0
  155. package/dist/commands/worktree/create-input.js +56 -0
  156. package/dist/commands/worktree/create.d.ts +11 -0
  157. package/dist/commands/worktree/create.js +58 -0
  158. package/dist/commands/worktree/index.d.ts +3 -0
  159. package/dist/commands/worktree/index.js +25 -0
  160. package/dist/commands/worktree/ls.d.ts +19 -0
  161. package/dist/commands/worktree/ls.js +97 -0
  162. package/dist/index.d.ts +2 -0
  163. package/dist/index.js +6 -0
  164. package/dist/output/index.d.ts +45 -0
  165. package/dist/output/index.js +47 -0
  166. package/dist/output/json.d.ts +11 -0
  167. package/dist/output/json.js +37 -0
  168. package/dist/output/quiet.d.ts +9 -0
  169. package/dist/output/quiet.js +22 -0
  170. package/dist/output/render.d.ts +15 -0
  171. package/dist/output/render.js +83 -0
  172. package/dist/output/table.d.ts +13 -0
  173. package/dist/output/table.js +135 -0
  174. package/dist/output/types.d.ts +73 -0
  175. package/dist/output/types.js +8 -0
  176. package/dist/output/with-output.d.ts +41 -0
  177. package/dist/output/with-output.js +87 -0
  178. package/dist/output/yaml.d.ts +11 -0
  179. package/dist/output/yaml.js +38 -0
  180. package/dist/run.d.ts +14 -0
  181. package/dist/run.js +33 -0
  182. package/dist/utils/client-id.d.ts +2 -0
  183. package/dist/utils/client-id.js +37 -0
  184. package/dist/utils/client.d.ts +44 -0
  185. package/dist/utils/client.js +330 -0
  186. package/dist/utils/command-options.d.ts +6 -0
  187. package/dist/utils/command-options.js +17 -0
  188. package/dist/utils/duration.d.ts +7 -0
  189. package/dist/utils/duration.js +38 -0
  190. package/dist/utils/errors.d.ts +5 -0
  191. package/dist/utils/errors.js +10 -0
  192. package/dist/utils/paths.d.ts +12 -0
  193. package/dist/utils/paths.js +22 -0
  194. package/dist/utils/provider-model.d.ts +11 -0
  195. package/dist/utils/provider-model.js +49 -0
  196. package/dist/utils/timeline.d.ts +11 -0
  197. package/dist/utils/timeline.js +11 -0
  198. package/dist/version.d.ts +2 -0
  199. package/dist/version.js +10 -0
  200. package/package.json +48 -0
@@ -0,0 +1,330 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { loadConfig, resolveOttoHome } from "@otto-code/server";
3
+ import { buildDaemonWebSocketUrl, buildRelayWebSocketUrl, normalizeHostPort, parseConnectionUri, shouldUseTlsForDefaultHostedRelay, } from "@otto-code/protocol/daemon-endpoints";
4
+ import { parseConnectionOfferFromUrl, } from "@otto-code/protocol/connection-offer";
5
+ import { DaemonClient } from "@otto-code/client/internal/daemon-client";
6
+ import path from "node:path";
7
+ import { WebSocket } from "ws";
8
+ import { getOrCreateCliClientId } from "./client-id.js";
9
+ import { resolveCliVersion } from "../version.js";
10
+ const DEFAULT_HOST = "localhost:6868";
11
+ const DEFAULT_TIMEOUT = 15000;
12
+ const PID_FILENAME = "otto.pid";
13
+ /**
14
+ * Get the daemon host from environment or options
15
+ */
16
+ export function getDaemonHost(options) {
17
+ return resolveDaemonHostCandidates(options)[0] ?? DEFAULT_HOST;
18
+ }
19
+ export function normalizeDaemonHost(raw) {
20
+ const trimmed = raw.trim();
21
+ if (!trimmed) {
22
+ return null;
23
+ }
24
+ if (trimmed.startsWith("tcp://")) {
25
+ try {
26
+ const parsed = parseConnectionUri(trimmed);
27
+ const endpoint = normalizeHostPort(parsed.isIpv6 ? `[${parsed.host}]:${parsed.port}` : `${parsed.host}:${parsed.port}`);
28
+ const query = new URLSearchParams();
29
+ if (parsed.useTls) {
30
+ query.set("ssl", "true");
31
+ }
32
+ if (parsed.password) {
33
+ query.set("password", parsed.password);
34
+ }
35
+ const queryString = query.toString();
36
+ const suffix = queryString ? `?${queryString}` : "";
37
+ return `tcp://${endpoint}${suffix}`;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ if (trimmed.startsWith("unix://") ||
44
+ trimmed.startsWith("pipe://") ||
45
+ trimmed.startsWith("\\\\.\\pipe\\")) {
46
+ return trimmed.startsWith("\\\\.\\pipe\\") ? `pipe://${trimmed}` : trimmed;
47
+ }
48
+ if (trimmed.startsWith("/") || trimmed.startsWith("~")) {
49
+ return `unix://${trimmed}`;
50
+ }
51
+ // Windows absolute paths (e.g. C:\Users\foo) are filesystem paths, not TCP or IPC targets.
52
+ if (/^[A-Za-z]:[/\\]/.test(trimmed)) {
53
+ return null;
54
+ }
55
+ if (/^\d+$/.test(trimmed)) {
56
+ return `127.0.0.1:${trimmed}`;
57
+ }
58
+ return trimmed.includes(":") ? trimmed : null;
59
+ }
60
+ export function resolveDefaultDaemonHost(env = process.env) {
61
+ return resolveDefaultDaemonHosts(env)[0] ?? DEFAULT_HOST;
62
+ }
63
+ function isIpcDaemonHost(host) {
64
+ return host !== null && (host.startsWith("unix://") || host.startsWith("pipe://"));
65
+ }
66
+ function isTcpDaemonHost(host) {
67
+ return host !== null && !isIpcDaemonHost(host);
68
+ }
69
+ function readPidSocketTarget(ottoHome) {
70
+ const pidPath = path.join(ottoHome, PID_FILENAME);
71
+ if (!existsSync(pidPath)) {
72
+ return null;
73
+ }
74
+ try {
75
+ const parsed = JSON.parse(readFileSync(pidPath, "utf-8"));
76
+ if (typeof parsed.listen === "string")
77
+ return parsed.listen;
78
+ if (typeof parsed.sockPath === "string")
79
+ return parsed.sockPath;
80
+ return null;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ function resolveConfiguredIpcDaemonHost(env, ottoHome) {
87
+ const directEnvHost = normalizeDaemonHost(env.OTTO_LISTEN ?? "");
88
+ if (isIpcDaemonHost(directEnvHost)) {
89
+ return directEnvHost;
90
+ }
91
+ const pidHost = normalizeDaemonHost(readPidSocketTarget(ottoHome) ?? "");
92
+ if (isIpcDaemonHost(pidHost)) {
93
+ return pidHost;
94
+ }
95
+ const config = loadConfig(ottoHome, { env });
96
+ const configuredHost = normalizeDaemonHost(config.listen);
97
+ return isIpcDaemonHost(configuredHost) ? configuredHost : null;
98
+ }
99
+ function resolveConfiguredTcpDaemonHost(env, ottoHome) {
100
+ const configuredHost = normalizeDaemonHost(loadConfig(ottoHome, { env }).listen);
101
+ if (!isTcpDaemonHost(configuredHost)) {
102
+ return null;
103
+ }
104
+ return configuredHost === "127.0.0.1:6868" ? null : configuredHost;
105
+ }
106
+ export function resolveDefaultDaemonHosts(env = process.env) {
107
+ const ottoHome = resolveOttoHome(env);
108
+ const candidates = [];
109
+ const configuredIpcHost = resolveConfiguredIpcDaemonHost(env, ottoHome);
110
+ if (configuredIpcHost) {
111
+ candidates.push(configuredIpcHost);
112
+ }
113
+ const configuredTcpHost = resolveConfiguredTcpDaemonHost(env, ottoHome);
114
+ if (configuredTcpHost) {
115
+ candidates.push(configuredTcpHost);
116
+ }
117
+ candidates.push(DEFAULT_HOST);
118
+ return Array.from(new Set(candidates));
119
+ }
120
+ function resolveDaemonHostCandidates(options) {
121
+ const explicitHost = options?.host ?? process.env.OTTO_HOST;
122
+ if (explicitHost) {
123
+ return [explicitHost];
124
+ }
125
+ return resolveDefaultDaemonHosts();
126
+ }
127
+ function stripIpcPrefix(trimmed) {
128
+ if (trimmed.startsWith("unix://"))
129
+ return trimmed.slice("unix://".length).trim();
130
+ if (trimmed.startsWith("pipe://"))
131
+ return trimmed.slice("pipe://".length).trim();
132
+ return trimmed;
133
+ }
134
+ export function resolveDaemonTarget(host) {
135
+ const trimmed = host.trim();
136
+ if (trimmed.startsWith("unix://") ||
137
+ trimmed.startsWith("pipe://") ||
138
+ trimmed.startsWith("\\\\.\\pipe\\")) {
139
+ const socketPath = stripIpcPrefix(trimmed);
140
+ if (!socketPath) {
141
+ throw new Error("Invalid IPC daemon target: missing socket path");
142
+ }
143
+ const isUnixSocket = trimmed.startsWith("unix://");
144
+ return {
145
+ type: "ipc",
146
+ url: isUnixSocket ? `ws+unix://${socketPath}:/ws` : "ws://localhost/ws",
147
+ socketPath,
148
+ };
149
+ }
150
+ if (trimmed.startsWith("tcp://")) {
151
+ const parsed = parseConnectionUri(trimmed);
152
+ const endpoint = normalizeHostPort(parsed.isIpv6 ? `[${parsed.host}]:${parsed.port}` : `${parsed.host}:${parsed.port}`);
153
+ return {
154
+ type: "tcp",
155
+ url: buildDaemonWebSocketUrl(endpoint, { useTls: parsed.useTls }),
156
+ };
157
+ }
158
+ return {
159
+ type: "tcp",
160
+ url: `ws://${trimmed}/ws`,
161
+ };
162
+ }
163
+ export function resolveDaemonPassword(host) {
164
+ const trimmed = host.trim();
165
+ if (trimmed.startsWith("tcp://")) {
166
+ const fromUri = parseConnectionUri(trimmed).password;
167
+ if (fromUri)
168
+ return fromUri;
169
+ }
170
+ const fromEnv = process.env.OTTO_PASSWORD;
171
+ return fromEnv && fromEnv.length > 0 ? fromEnv : undefined;
172
+ }
173
+ /**
174
+ * Create a WebSocket factory that works in Node.js
175
+ */
176
+ function createNodeWebSocketFactory() {
177
+ return (url, options) => {
178
+ return new WebSocket(url, options?.protocols, {
179
+ headers: options?.headers,
180
+ ...(options?.socketPath ? { socketPath: options.socketPath } : {}),
181
+ });
182
+ };
183
+ }
184
+ /**
185
+ * Create and connect a daemon client
186
+ * Returns the connected client or throws if connection fails
187
+ */
188
+ async function tryConnectHost(host, password, clientId, timeout, nodeWebSocketFactory) {
189
+ const target = resolveDaemonTarget(host);
190
+ const client = new DaemonClient({
191
+ url: target.url,
192
+ clientId,
193
+ clientType: "cli",
194
+ appVersion: resolveCliVersion(),
195
+ password,
196
+ connectTimeoutMs: timeout,
197
+ webSocketFactory: (url, config) => nodeWebSocketFactory(url, {
198
+ headers: config?.headers,
199
+ protocols: config?.protocols,
200
+ ...(target.type === "ipc" ? { socketPath: target.socketPath } : {}),
201
+ }),
202
+ reconnect: { enabled: false },
203
+ });
204
+ try {
205
+ await client.connect();
206
+ return { client };
207
+ }
208
+ catch (error) {
209
+ await client.close().catch(() => { });
210
+ return { error };
211
+ }
212
+ }
213
+ async function connectViaRelayOffer(offer, clientId, timeout, nodeWebSocketFactory) {
214
+ const url = buildRelayWebSocketUrl({
215
+ endpoint: offer.relay.endpoint,
216
+ serverId: offer.serverId,
217
+ role: "client",
218
+ useTls: offer.relay.useTls ?? shouldUseTlsForDefaultHostedRelay(offer.relay.endpoint),
219
+ });
220
+ const client = new DaemonClient({
221
+ url,
222
+ clientId,
223
+ clientType: "cli",
224
+ appVersion: resolveCliVersion(),
225
+ connectTimeoutMs: timeout,
226
+ webSocketFactory: (target, config) => nodeWebSocketFactory(target, { headers: config?.headers, protocols: config?.protocols }),
227
+ e2ee: { enabled: true, daemonPublicKeyB64: offer.daemonPublicKeyB64 },
228
+ reconnect: { enabled: false },
229
+ });
230
+ try {
231
+ await client.connect();
232
+ return client;
233
+ }
234
+ catch (error) {
235
+ await client.close().catch(() => { });
236
+ const message = error instanceof Error ? error.message : String(error);
237
+ const lastError = client.lastError ? ` (${client.lastError})` : "";
238
+ throw new Error(`Failed to connect via relay offer: ${message}${lastError}`, { cause: error });
239
+ }
240
+ }
241
+ function parseHostOfferOrNull(host) {
242
+ if (!host)
243
+ return null;
244
+ try {
245
+ return parseConnectionOfferFromUrl(host);
246
+ }
247
+ catch (error) {
248
+ const message = error instanceof Error ? error.message : String(error);
249
+ throw new Error(`Invalid pairing offer URL: ${message}`, { cause: error });
250
+ }
251
+ }
252
+ export async function connectToDaemon(options) {
253
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
254
+ const clientId = await getOrCreateCliClientId();
255
+ const nodeWebSocketFactory = createNodeWebSocketFactory();
256
+ const explicitHost = options?.host ?? process.env.OTTO_HOST;
257
+ const offer = parseHostOfferOrNull(explicitHost);
258
+ if (offer) {
259
+ return connectViaRelayOffer(offer, clientId, timeout, nodeWebSocketFactory);
260
+ }
261
+ const hosts = resolveDaemonHostCandidates(options);
262
+ async function tryNext(index, lastError) {
263
+ if (index >= hosts.length) {
264
+ if (lastError instanceof Error)
265
+ throw lastError;
266
+ throw new Error(`Unable to connect to Otto daemon via ${hosts.join(", ")}`);
267
+ }
268
+ const host = hosts[index];
269
+ const password = resolveDaemonPassword(host);
270
+ const result = await tryConnectHost(host, password, clientId, timeout, nodeWebSocketFactory);
271
+ if ("client" in result) {
272
+ return result.client;
273
+ }
274
+ return tryNext(index + 1, result.error);
275
+ }
276
+ return tryNext(0, null);
277
+ }
278
+ /**
279
+ * Try to connect to the daemon, returns null if connection fails
280
+ */
281
+ export async function tryConnectToDaemon(options) {
282
+ try {
283
+ return await connectToDaemon(options);
284
+ }
285
+ catch {
286
+ return null;
287
+ }
288
+ }
289
+ /**
290
+ * Resolve an agent ID from a partial ID or name.
291
+ * Supports:
292
+ * - Full ID match
293
+ * - Prefix match (first N characters)
294
+ * - Title/name match (case-insensitive)
295
+ *
296
+ * Returns the full agent ID if found, null otherwise.
297
+ */
298
+ export function resolveAgentId(idOrName, agents) {
299
+ if (!idOrName || agents.length === 0) {
300
+ return null;
301
+ }
302
+ const query = idOrName.toLowerCase();
303
+ // Try exact ID match first
304
+ const exactMatch = agents.find((a) => a.id === idOrName);
305
+ if (exactMatch) {
306
+ return exactMatch.id;
307
+ }
308
+ // Try ID prefix match
309
+ const prefixMatches = agents.filter((a) => a.id.toLowerCase().startsWith(query));
310
+ if (prefixMatches.length === 1 && prefixMatches[0]) {
311
+ return prefixMatches[0].id;
312
+ }
313
+ // Try title/name match (case-insensitive)
314
+ const titleMatches = agents.filter((a) => a.title?.toLowerCase() === query);
315
+ if (titleMatches.length === 1 && titleMatches[0]) {
316
+ return titleMatches[0].id;
317
+ }
318
+ // Try partial title match
319
+ const partialTitleMatches = agents.filter((a) => a.title?.toLowerCase().includes(query));
320
+ if (partialTitleMatches.length === 1 && partialTitleMatches[0]) {
321
+ return partialTitleMatches[0].id;
322
+ }
323
+ // If we have multiple prefix matches and no unique title match, return first prefix match
324
+ const firstPrefixMatch = prefixMatches[0];
325
+ if (firstPrefixMatch) {
326
+ return firstPrefixMatch.id;
327
+ }
328
+ return null;
329
+ }
330
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,6 @@
1
+ import type { Command } from "commander";
2
+ export declare function collectMultiple(value: string, previous: string[]): string[];
3
+ export declare function addJsonOption<T extends Command>(command: T): T;
4
+ export declare function addDaemonHostOption<T extends Command>(command: T): T;
5
+ export declare function addJsonAndDaemonHostOptions<T extends Command>(command: T): T;
6
+ //# sourceMappingURL=command-options.d.ts.map
@@ -0,0 +1,17 @@
1
+ const JSON_OPTION_DESCRIPTION = "Output in JSON format";
2
+ const DAEMON_HOST_OPTION_DESCRIPTION = "Daemon host target: host:port or tcp://host:port?ssl=true&password=secret (default: local socket/pipe, then localhost:6868)";
3
+ export function collectMultiple(value, previous) {
4
+ return previous.concat([value]);
5
+ }
6
+ export function addJsonOption(command) {
7
+ command.option("--json", JSON_OPTION_DESCRIPTION);
8
+ return command;
9
+ }
10
+ export function addDaemonHostOption(command) {
11
+ command.option("--host <host>", DAEMON_HOST_OPTION_DESCRIPTION);
12
+ return command;
13
+ }
14
+ export function addJsonAndDaemonHostOptions(command) {
15
+ return addDaemonHostOption(addJsonOption(command));
16
+ }
17
+ //# sourceMappingURL=command-options.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Parse duration string to milliseconds.
3
+ * Supports formats like: 5m, 30s, 1h, 2h30m, 90, etc.
4
+ * If no unit is specified, assumes seconds.
5
+ */
6
+ export declare function parseDuration(input: string): number;
7
+ //# sourceMappingURL=duration.d.ts.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Parse duration string to milliseconds.
3
+ * Supports formats like: 5m, 30s, 1h, 2h30m, 90, etc.
4
+ * If no unit is specified, assumes seconds.
5
+ */
6
+ export function parseDuration(input) {
7
+ const trimmed = input.trim();
8
+ // If it's just a number, treat as seconds
9
+ if (/^\d+$/.test(trimmed)) {
10
+ return parseInt(trimmed, 10) * 1000;
11
+ }
12
+ // Parse duration with units
13
+ let totalMs = 0;
14
+ const regex = /(\d+)([smh])/g;
15
+ let match;
16
+ let hasMatch = false;
17
+ while ((match = regex.exec(trimmed)) !== null) {
18
+ hasMatch = true;
19
+ const value = parseInt(match[1], 10);
20
+ const unit = match[2];
21
+ switch (unit) {
22
+ case "s":
23
+ totalMs += value * 1000;
24
+ break;
25
+ case "m":
26
+ totalMs += value * 60 * 1000;
27
+ break;
28
+ case "h":
29
+ totalMs += value * 60 * 60 * 1000;
30
+ break;
31
+ }
32
+ }
33
+ if (!hasMatch) {
34
+ throw new Error(`Invalid duration format: ${input}. Use formats like: 5m, 30s, 1h, 2h30m`);
35
+ }
36
+ return totalMs;
37
+ }
38
+ //# sourceMappingURL=duration.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Convert unknown thrown values into a user-safe error string.
3
+ */
4
+ export declare function getErrorMessage(error: unknown): string;
5
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Convert unknown thrown values into a user-safe error string.
3
+ */
4
+ export function getErrorMessage(error) {
5
+ if (error instanceof Error) {
6
+ return error.message;
7
+ }
8
+ return String(error);
9
+ }
10
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Path utilities for cwd filtering in agent commands.
3
+ */
4
+ /**
5
+ * Check if `candidatePath` is the same directory as `basePath` or a descendant of it.
6
+ *
7
+ * Handles both Unix (/) and Windows (\) path separators, including mixed separators.
8
+ * This is important because agent cwd paths come from the agent's OS (could be Windows)
9
+ * while the CLI filter path comes from the user (could also be Windows or Unix).
10
+ */
11
+ export declare function isSameOrDescendantPath(basePath: string, candidatePath: string): boolean;
12
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Path utilities for cwd filtering in agent commands.
3
+ */
4
+ /**
5
+ * Check if `candidatePath` is the same directory as `basePath` or a descendant of it.
6
+ *
7
+ * Handles both Unix (/) and Windows (\) path separators, including mixed separators.
8
+ * This is important because agent cwd paths come from the agent's OS (could be Windows)
9
+ * while the CLI filter path comes from the user (could also be Windows or Unix).
10
+ */
11
+ export function isSameOrDescendantPath(basePath, candidatePath) {
12
+ // Normalize both paths: replace all backslashes with forward slashes, strip trailing separator
13
+ let normalizedBase = basePath.replace(/\\/g, "/").replace(/\/$/, "");
14
+ let normalizedCandidate = candidatePath.replace(/\\/g, "/").replace(/\/$/, "");
15
+ // Windows paths are case-insensitive — detect by drive letter prefix (e.g. "C:/")
16
+ if (/^[a-zA-Z]:\//.test(normalizedBase) || /^[a-zA-Z]:\//.test(normalizedCandidate)) {
17
+ normalizedBase = normalizedBase.toLowerCase();
18
+ normalizedCandidate = normalizedCandidate.toLowerCase();
19
+ }
20
+ return (normalizedCandidate === normalizedBase || normalizedCandidate.startsWith(normalizedBase + "/"));
21
+ }
22
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,11 @@
1
+ export interface ResolveProviderAndModelOptions {
2
+ provider?: string;
3
+ model?: string;
4
+ defaultProvider?: string;
5
+ }
6
+ export interface ResolvedProviderModel {
7
+ provider: string;
8
+ model: string | undefined;
9
+ }
10
+ export declare function resolveProviderAndModel(options: ResolveProviderAndModelOptions): ResolvedProviderModel;
11
+ //# sourceMappingURL=provider-model.d.ts.map
@@ -0,0 +1,49 @@
1
+ export function resolveProviderAndModel(options) {
2
+ const providerInput = options.provider?.trim() || options.defaultProvider;
3
+ const modelInput = options.model?.trim();
4
+ if (!providerInput) {
5
+ const error = {
6
+ code: "MISSING_PROVIDER",
7
+ message: "Provider is required",
8
+ details: "Pass --provider <provider> or --provider <provider>/<model>. Use `otto provider ls` to see providers and `otto provider models <provider>` to see models.",
9
+ };
10
+ throw error;
11
+ }
12
+ if (options.model !== undefined && !modelInput) {
13
+ const error = {
14
+ code: "INVALID_MODEL",
15
+ message: "--model cannot be empty",
16
+ };
17
+ throw error;
18
+ }
19
+ const slashIndex = providerInput.indexOf("/");
20
+ if (slashIndex === -1) {
21
+ return {
22
+ provider: providerInput,
23
+ model: modelInput,
24
+ };
25
+ }
26
+ const provider = providerInput.slice(0, slashIndex).trim();
27
+ const modelFromProvider = providerInput.slice(slashIndex + 1).trim();
28
+ if (!provider || !modelFromProvider) {
29
+ const error = {
30
+ code: "INVALID_PROVIDER",
31
+ message: "Invalid --provider value",
32
+ details: "Use --provider <provider> or --provider <provider>/<model>",
33
+ };
34
+ throw error;
35
+ }
36
+ if (modelInput && modelInput !== modelFromProvider) {
37
+ const error = {
38
+ code: "CONFLICTING_MODEL_OPTIONS",
39
+ message: "Conflicting model values provided",
40
+ details: `--provider specifies model ${modelFromProvider}, but --model specifies ${modelInput}`,
41
+ };
42
+ throw error;
43
+ }
44
+ return {
45
+ provider,
46
+ model: modelInput ?? modelFromProvider,
47
+ };
48
+ }
49
+ //# sourceMappingURL=provider-model.js.map
@@ -0,0 +1,11 @@
1
+ import type { DaemonClient } from "@otto-code/client/internal/daemon-client";
2
+ import type { AgentTimelineItem } from "@otto-code/protocol/agent-types";
3
+ export declare const LIVE_HISTORY_FETCH_TIMEOUT_MS = 2000;
4
+ interface FetchProjectedTimelineItemsInput {
5
+ client: DaemonClient;
6
+ agentId: string;
7
+ timeoutMs?: number;
8
+ }
9
+ export declare function fetchProjectedTimelineItems(input: FetchProjectedTimelineItemsInput): Promise<AgentTimelineItem[]>;
10
+ export {};
11
+ //# sourceMappingURL=timeline.d.ts.map
@@ -0,0 +1,11 @@
1
+ export const LIVE_HISTORY_FETCH_TIMEOUT_MS = 2000;
2
+ export async function fetchProjectedTimelineItems(input) {
3
+ const timeline = await input.client.fetchAgentTimeline(input.agentId, {
4
+ direction: "tail",
5
+ limit: 0,
6
+ projection: "projected",
7
+ timeout: input.timeoutMs,
8
+ });
9
+ return timeline.entries.map((entry) => entry.item);
10
+ }
11
+ //# sourceMappingURL=timeline.js.map
@@ -0,0 +1,2 @@
1
+ export declare function resolveCliVersion(): string;
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,10 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ export function resolveCliVersion() {
4
+ const packageJson = require("../package.json");
5
+ if (typeof packageJson.version === "string" && packageJson.version.trim().length > 0) {
6
+ return packageJson.version.trim();
7
+ }
8
+ throw new Error("Unable to resolve @otto-code/cli version from package.json.");
9
+ }
10
+ //# sourceMappingURL=version.js.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@otto-code/cli",
3
+ "version": "0.5.0",
4
+ "description": "Otto CLI - control your AI coding agents from the command line",
5
+ "bin": {
6
+ "otto": "bin/otto"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "dist",
11
+ "!dist/**/*.map"
12
+ ],
13
+ "type": "module",
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "scripts": {
18
+ "clean": "node ../../scripts/clean-package-dist.mjs",
19
+ "build": "tsc -p tsconfig.json --incremental false",
20
+ "build:clean": "npm run clean && npm run build",
21
+ "prepack": "npm run build:clean",
22
+ "typecheck": "tsgo --noEmit",
23
+ "test": "npm run test:local",
24
+ "test:local": "tsx tests/run-all.ts",
25
+ "test:e2e": "npm run test:local",
26
+ "test:e2e:lifecycle": "npx tsx tests/e2e/agent-lifecycle.test.ts"
27
+ },
28
+ "dependencies": {
29
+ "@clack/prompts": "^1.0.0",
30
+ "@otto-code/client": "0.5.0",
31
+ "@otto-code/protocol": "0.5.0",
32
+ "@otto-code/server": "0.5.0",
33
+ "chalk": "^5.3.0",
34
+ "commander": "^12.0.0",
35
+ "mime-types": "^2.1.35",
36
+ "tree-kill": "^1.2.2",
37
+ "ws": "^8.14.2",
38
+ "yaml": "^2.8.4"
39
+ },
40
+ "devDependencies": {
41
+ "@types/mime-types": "^3.0.1",
42
+ "@types/ws": "^8.5.8",
43
+ "tsx": "^4.6.0",
44
+ "typescript": "^5.2.2",
45
+ "vitest": "^4.1.6",
46
+ "zx": "^8.8.5"
47
+ }
48
+ }