@vellumai/assistant 0.4.43 → 0.4.44

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 (88) hide show
  1. package/ARCHITECTURE.md +13 -14
  2. package/README.md +11 -12
  3. package/docs/architecture/integrations.md +75 -93
  4. package/package.json +1 -1
  5. package/src/__tests__/approval-routes-http.test.ts +0 -2
  6. package/src/__tests__/bundled-asset.test.ts +1 -1
  7. package/src/__tests__/checker.test.ts +31 -28
  8. package/src/__tests__/conversation-routes-guardian-reply.test.ts +6 -6
  9. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  10. package/src/__tests__/error-handler-friendly-messages.test.ts +46 -0
  11. package/src/__tests__/managed-twitter-guardrails.test.ts +5 -1
  12. package/src/__tests__/onboarding-template-contract.test.ts +0 -10
  13. package/src/__tests__/provider-fail-open-selection.test.ts +12 -2
  14. package/src/__tests__/send-endpoint-busy.test.ts +0 -3
  15. package/src/__tests__/session-confirmation-signals.test.ts +7 -45
  16. package/src/__tests__/starter-task-flow.test.ts +9 -19
  17. package/src/__tests__/system-prompt.test.ts +3 -4
  18. package/src/__tests__/trust-store.test.ts +4 -4
  19. package/src/__tests__/twitter-platform-proxy-client.test.ts +43 -18
  20. package/src/cli/commands/amazon/index.ts +4 -39
  21. package/src/cli/commands/amazon/session.ts +18 -26
  22. package/src/cli/commands/twitter/__tests__/cli-read-routing.test.ts +58 -196
  23. package/src/cli/commands/twitter/__tests__/cli-routing.test.ts +26 -186
  24. package/src/cli/commands/twitter/__tests__/oauth-client.test.ts +1 -47
  25. package/src/cli/commands/twitter/index.ts +95 -835
  26. package/src/cli/commands/twitter/oauth-client.ts +1 -35
  27. package/src/cli/commands/twitter/router.ts +70 -115
  28. package/src/cli/commands/twitter/types.ts +30 -0
  29. package/src/cli/reference.ts +2 -2
  30. package/src/config/bundled-skills/amazon/SKILL.md +0 -1
  31. package/src/config/bundled-skills/app-builder/SKILL.md +0 -6
  32. package/src/config/bundled-skills/app-builder/TOOLS.json +0 -4
  33. package/src/config/bundled-skills/doordash/SKILL.md +0 -1
  34. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +1 -82
  35. package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -28
  36. package/src/config/bundled-skills/doordash/lib/session.ts +21 -17
  37. package/src/config/bundled-skills/twitter/SKILL.md +53 -166
  38. package/src/config/feature-flag-registry.json +8 -0
  39. package/src/daemon/handlers/session-history.ts +41 -9
  40. package/src/daemon/lifecycle.ts +4 -17
  41. package/src/daemon/message-types/apps.ts +0 -25
  42. package/src/daemon/message-types/integrations.ts +1 -7
  43. package/src/daemon/message-types/sessions.ts +6 -1
  44. package/src/daemon/message-types/surfaces.ts +2 -0
  45. package/src/daemon/ride-shotgun-handler.ts +33 -1
  46. package/src/daemon/seed-files.ts +3 -27
  47. package/src/daemon/server.ts +2 -18
  48. package/src/daemon/session-agent-loop-handlers.ts +24 -2
  49. package/src/daemon/session-runtime-assembly.ts +0 -7
  50. package/src/daemon/session-surfaces.ts +185 -33
  51. package/src/daemon/session.ts +2 -28
  52. package/src/memory/app-store.ts +0 -18
  53. package/src/memory/schema/infrastructure.ts +0 -8
  54. package/src/permissions/defaults.ts +3 -3
  55. package/src/prompts/system-prompt.ts +4 -5
  56. package/src/prompts/templates/BOOTSTRAP.md +0 -3
  57. package/src/providers/registry.ts +2 -4
  58. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  59. package/src/runtime/auth/__tests__/scopes.test.ts +2 -1
  60. package/src/runtime/auth/route-policy.ts +0 -4
  61. package/src/runtime/auth/scopes.ts +1 -0
  62. package/src/runtime/auth/token-service.ts +1 -1
  63. package/src/runtime/http-types.ts +10 -0
  64. package/src/runtime/middleware/error-handler.ts +14 -1
  65. package/src/runtime/routes/app-management-routes.ts +61 -64
  66. package/src/runtime/routes/brain-graph/brain-graph.html +1845 -0
  67. package/src/runtime/routes/brain-graph-routes.ts +4 -42
  68. package/src/runtime/routes/conversation-routes.ts +9 -6
  69. package/src/runtime/routes/diagnostics-routes.ts +91 -14
  70. package/src/runtime/routes/settings-routes.ts +3 -93
  71. package/src/tools/AGENTS.md +38 -0
  72. package/src/tools/apps/executors.ts +0 -6
  73. package/src/tools/document/editor-template.ts +10 -8
  74. package/src/twitter/platform-proxy-client.ts +6 -3
  75. package/src/util/errors.ts +12 -0
  76. package/src/__tests__/home-base-bootstrap.test.ts +0 -84
  77. package/src/__tests__/prebuilt-home-base-seed.test.ts +0 -79
  78. package/src/cli/commands/twitter/__tests__/cli-error-shaping.test.ts +0 -265
  79. package/src/cli/commands/twitter/client.ts +0 -989
  80. package/src/cli/commands/twitter/session.ts +0 -121
  81. package/src/home-base/app-link-store.ts +0 -78
  82. package/src/home-base/bootstrap.ts +0 -74
  83. package/src/home-base/prebuilt/brain-graph.html +0 -1483
  84. package/src/home-base/prebuilt/index.html +0 -702
  85. package/src/home-base/prebuilt/seed-metadata.json +0 -21
  86. package/src/home-base/prebuilt/seed.ts +0 -122
  87. package/src/home-base/prebuilt-home-base-updater.ts +0 -36
  88. package/src/util/cookie-session.ts +0 -98
@@ -1,30 +1,14 @@
1
1
  /**
2
2
  * CLI command group: `assistant twitter`
3
3
  *
4
- * Post tweets and manage Twitter sessions via the command line.
4
+ * Post tweets and interact with X via the command line.
5
5
  * All commands output JSON to stdout. Use --json for machine-readable output.
6
6
  */
7
7
 
8
- import { execFile } from "node:child_process";
9
- import { promisify } from "node:util";
10
-
11
8
  import { Command } from "commander";
12
9
 
13
- const execFileAsync = promisify(execFile);
14
-
15
10
  import { httpSend } from "../../http-client.js";
16
- import {
17
- getBookmarks,
18
- getFollowers,
19
- getFollowing,
20
- getHomeTimeline,
21
- getLikes,
22
- getNotifications,
23
- getUserByScreenName,
24
- getUserMedia,
25
- SessionExpiredError,
26
- } from "./client.js";
27
- import type { TwitterStrategy } from "./router.js";
11
+ import type { TwitterMode } from "./router.js";
28
12
  import {
29
13
  routedGetTweetDetail,
30
14
  routedGetUserByScreenName,
@@ -32,7 +16,6 @@ import {
32
16
  routedPostTweet,
33
17
  routedSearchTweets,
34
18
  } from "./router.js";
35
- import { clearSession, importFromRecording, loadSession } from "./session.js";
36
19
 
37
20
  // ---------------------------------------------------------------------------
38
21
  // Helpers
@@ -58,52 +41,33 @@ function getJson(cmd: Command): boolean {
58
41
  return false;
59
42
  }
60
43
 
61
- const SESSION_EXPIRED_MSG =
62
- "Your Twitter session has expired. Please sign in to Twitter in Chrome — " +
63
- "run `assistant twitter refresh` to capture your session automatically.";
64
-
65
44
  async function run(cmd: Command, fn: () => Promise<unknown>): Promise<void> {
66
45
  try {
67
46
  const result = await fn();
68
47
  output({ ok: true, ...(result as Record<string, unknown>) }, getJson(cmd));
69
48
  } catch (err) {
70
49
  const meta = err as Record<string, unknown>;
71
- if (err instanceof SessionExpiredError) {
72
- // Preserve backward-compatible error code while surfacing router metadata
50
+ // For routed errors with proxy error codes, emit structured JSON
51
+ if (err instanceof Error && meta.proxyErrorCode !== undefined) {
73
52
  const payload: Record<string, unknown> = {
74
53
  ok: false,
75
- error: "session_expired",
76
- message: SESSION_EXPIRED_MSG,
54
+ error: err.message,
77
55
  };
56
+ payload.proxyErrorCode = meta.proxyErrorCode;
57
+ if (meta.retryable !== undefined) payload.retryable = meta.retryable;
78
58
  if (meta.pathUsed !== undefined) payload.pathUsed = meta.pathUsed;
79
- if (meta.suggestAlternative !== undefined)
80
- payload.suggestAlternative = meta.suggestAlternative;
81
- if (meta.oauthError !== undefined) payload.oauthError = meta.oauthError;
82
59
  output(payload, getJson(cmd));
83
60
  process.exitCode = 1;
84
61
  return;
85
62
  }
86
- // For routed errors with any router metadata, emit structured JSON
87
- // so callers can see dual-path diagnostics (pathUsed, oauthError, etc.)
88
- if (
89
- err instanceof Error &&
90
- (meta.pathUsed !== undefined ||
91
- meta.suggestAlternative !== undefined ||
92
- meta.oauthError !== undefined ||
93
- meta.proxyErrorCode !== undefined)
94
- ) {
95
- const payload: Record<string, unknown> = {
96
- ok: false,
97
- error: err.message,
98
- };
99
- if (meta.pathUsed !== undefined) payload.pathUsed = meta.pathUsed;
100
- if (meta.suggestAlternative !== undefined)
101
- payload.suggestAlternative = meta.suggestAlternative;
102
- if (meta.oauthError !== undefined) payload.oauthError = meta.oauthError;
103
- if (meta.proxyErrorCode !== undefined)
104
- payload.proxyErrorCode = meta.proxyErrorCode;
105
- if (meta.retryable !== undefined) payload.retryable = meta.retryable;
106
- output(payload, getJson(cmd));
63
+ // For routed errors with pathUsed but no proxy error code (e.g. OAuth
64
+ // not configured, user not found, unsupported product type), preserve
65
+ // the pathUsed metadata in structured output.
66
+ if (err instanceof Error && meta.pathUsed !== undefined) {
67
+ output(
68
+ { ok: false, error: err.message, pathUsed: meta.pathUsed },
69
+ getJson(cmd),
70
+ );
107
71
  process.exitCode = 1;
108
72
  return;
109
73
  }
@@ -120,314 +84,93 @@ export function registerTwitterCommand(program: Command): void {
120
84
  .command("x")
121
85
  .alias("twitter")
122
86
  .description(
123
- "Post on X and manage connections. Supports managed (platform proxy), OAuth (official API), and browser session paths.",
87
+ "Post on X and manage connections. Supports managed (platform proxy) and OAuth (official API) paths.",
124
88
  )
125
89
  .option("--json", "Machine-readable JSON output");
126
90
 
127
91
  tw.addHelpText(
128
92
  "after",
129
93
  `
130
- Twitter (X) supports multiple paths for interacting with the platform:
94
+ Twitter (X) supports two paths for interacting with the platform:
131
95
 
132
96
  1. Managed (platform proxy) — routes Twitter API calls through the platform,
133
97
  which holds the OAuth credentials. Used when integrationMode is "managed".
134
98
  2. OAuth (official API) — uses an authenticated Twitter OAuth application for
135
99
  posting and replying. Requires a connected OAuth credential.
136
- 3. Browser session (Ride Shotgun) — uses cookies captured from a real Chrome
137
- session to call Twitter's internal GraphQL API. Supports all read operations
138
- and posting as a fallback.
139
-
140
- The strategy system controls which path is used for operations that support multiple:
141
- managed — route through the platform proxy (platform holds credentials)
142
- oauth — always use the OAuth API; fail if unavailable
143
- browser — always use the browser session; fail if unavailable
144
- auto — try OAuth first, fall back to browser session (default)
145
100
 
146
- Session management:
147
- - "login" imports cookies from a Ride Shotgun recording file
148
- - "refresh" launches Chrome with CDP, navigates to x.com/login, and runs a
149
- Ride Shotgun learn session to capture fresh cookies automatically
150
- - "status" shows whether browser session and OAuth are active
151
- - "logout" clears the saved browser session cookies
101
+ Write operations (post, reply) default to OAuth. Pass --managed to route
102
+ through the platform proxy instead. Read operations (timeline, tweet, search)
103
+ always use managed mode.
152
104
 
153
105
  Examples:
154
106
  $ assistant x status
155
- $ assistant x post "Hello world" --strategy managed
156
- $ assistant x post "Hello world" --strategy auto
107
+ $ assistant x post "Hello world" --managed
108
+ $ assistant x post "Hello world" --oauth-token "$TOKEN"
157
109
  $ assistant x timeline elonmusk --count 10
158
- $ assistant x search "from:vaborsh AI agents" --product Latest
159
- $ assistant x strategy set oauth`,
110
+ $ assistant x search "from:vaborsh AI agents" --product Latest`,
160
111
  );
161
112
 
162
113
  // =========================================================================
163
- // loginimport session from a recording
164
- // =========================================================================
165
- tw.command("login")
166
- .description("Import a Twitter session from a Ride Shotgun recording")
167
- .requiredOption("--recording <path>", "Path to the recording JSON file")
168
- .addHelpText(
169
- "after",
170
- `
171
- Imports cookies from a Ride Shotgun recording file to establish a browser
172
- session. The recording file is a JSON file produced by a Ride Shotgun learn
173
- session that contains captured cookies for x.com.
174
-
175
- After import, all browser-path commands (timeline, search, bookmarks, etc.)
176
- will use these cookies for authentication.
177
-
178
- Examples:
179
- $ assistant x login --recording /tmp/ride-shotgun/recording-abc123.json
180
- $ assistant x login --recording ~/recordings/twitter-session.json`,
181
- )
182
- .action(async (opts: { recording: string }, cmd: Command) => {
183
- await run(cmd, async () => {
184
- const session = await importFromRecording(opts.recording);
185
- return {
186
- message: "Session imported successfully",
187
- cookieCount: session.cookies.length,
188
- };
189
- });
190
- });
191
-
192
- // =========================================================================
193
- // logout — clear saved session
194
- // =========================================================================
195
- tw.command("logout")
196
- .description("Clear the saved Twitter session")
197
- .addHelpText(
198
- "after",
199
- `
200
- Deletes all saved browser session cookies. After logout, browser-path commands
201
- will fail until a new session is imported via "login" or captured via "refresh".
202
- OAuth credentials are not affected.
203
-
204
- Examples:
205
- $ assistant x logout`,
206
- )
207
- .action(async (_opts: unknown, cmd: Command) => {
208
- await clearSession();
209
- output({ ok: true, message: "Session cleared" }, getJson(cmd));
210
- });
211
-
212
- // =========================================================================
213
- // refresh — start Ride Shotgun learn to capture fresh cookies
214
- // =========================================================================
215
- tw.command("refresh")
216
- .description(
217
- "Start a Ride Shotgun learn session to capture fresh Twitter cookies. " +
218
- "Opens x.com in Chrome — sign in when prompted. " +
219
- "NOTE: Chrome will restart with debugging enabled; your tabs will be restored.",
220
- )
221
- .option("--duration <seconds>", "Recording duration in seconds", "180")
222
- .addHelpText(
223
- "after",
224
- `
225
- Restarts Chrome with CDP (Chrome DevTools Protocol) enabled, navigates to
226
- x.com/login, and runs a Ride Shotgun learn session to capture fresh cookies.
227
- Sign in when Chrome opens — the session will be recorded automatically.
228
-
229
- The --duration flag sets how long (in seconds) the recording runs before
230
- stopping. Default is 180 seconds (3 minutes). After the recording completes,
231
- cookies are imported automatically and Chrome is minimized.
232
-
233
- Requires the assistant to be running (Ride Shotgun runs via the assistant).
234
-
235
- Examples:
236
- $ assistant x refresh
237
- $ assistant x refresh --duration 120
238
- $ assistant x refresh --duration 300`,
239
- )
240
- .action(async (opts: { duration: string }, cmd: Command) => {
241
- const json = getJson(cmd);
242
- const duration = parseInt(opts.duration, 10);
243
-
244
- try {
245
- const result = await startLearnSession(duration);
246
- if (result.recordingPath) {
247
- const session = await importFromRecording(result.recordingPath);
248
-
249
- // Hide Chrome after capturing session
250
- try {
251
- await minimizeChrome();
252
- } catch {
253
- /* best-effort */
254
- }
255
-
256
- output(
257
- {
258
- ok: true,
259
- message: "Session refreshed successfully",
260
- cookieCount: session.cookies.length,
261
- },
262
- json,
263
- );
264
- } else {
265
- output(
266
- {
267
- ok: false,
268
- error: "Recording completed but no recording path returned",
269
- },
270
- json,
271
- );
272
- process.exitCode = 1;
273
- }
274
- } catch (err) {
275
- outputError(err instanceof Error ? err.message : String(err));
276
- }
277
- });
278
-
279
- // =========================================================================
280
- // status — check session status + OAuth and strategy info
114
+ // statuscheck OAuth and managed mode info
281
115
  // =========================================================================
282
116
  tw.command("status")
283
- .description("Check Twitter session, OAuth, and strategy status")
117
+ .description("Check Twitter OAuth and managed mode status")
284
118
  .addHelpText(
285
119
  "after",
286
120
  `
287
- Shows the current state of both authentication paths:
121
+ Shows the current state of authentication:
288
122
 
289
- Browser session — whether cookies are loaded and the cookie count.
290
- OAuth — whether an OAuth credential is connected, the linked account, the
291
- current strategy setting, and whether a strategy has been explicitly configured.
123
+ OAuth — whether an OAuth credential is connected and the linked account.
124
+ Managed — whether managed mode is available and prerequisite status.
292
125
 
293
- If the assistant is not running, OAuth fields will be reported as undefined.
126
+ If the assistant is not running, fields will be reported as undefined.
294
127
 
295
128
  Examples:
296
129
  $ assistant x status
297
130
  $ assistant x status --json`,
298
131
  )
299
132
  .action(async (_opts: unknown, cmd: Command) => {
300
- const session = await loadSession();
301
- const browserInfo: Record<string, unknown> = session
302
- ? {
303
- browserSessionActive: true,
304
- cookieCount: session.cookies.length,
305
- }
306
- : { browserSessionActive: false };
307
-
308
- // Query daemon for OAuth / strategy config
133
+ // Query daemon for OAuth / managed config
309
134
  let oauthInfo: Record<string, unknown> = {};
310
135
  try {
311
136
  const r = await sendTwitterConfigRequest("get");
312
137
  oauthInfo = {
313
138
  oauthConnected: r.connected ?? false,
314
139
  oauthAccount: r.accountInfo ?? undefined,
315
- preferredStrategy: r.strategy ?? "auto",
316
- strategyConfigured: r.strategyConfigured ?? false,
140
+ managedAvailable: r.managedAvailable ?? false,
141
+ managedPrerequisites: r.managedPrerequisites ?? undefined,
142
+ localClientConfigured: r.localClientConfigured ?? false,
317
143
  };
318
144
  } catch {
319
- // Daemon may not be running; report what we can from the local session
145
+ // Daemon may not be running; report what we can
320
146
  oauthInfo = {
321
147
  oauthConnected: undefined,
322
148
  oauthAccount: undefined,
323
- preferredStrategy: undefined,
324
- strategyConfigured: undefined,
149
+ managedAvailable: undefined,
150
+ managedPrerequisites: undefined,
151
+ localClientConfigured: undefined,
325
152
  };
326
153
  }
327
154
 
328
155
  output(
329
156
  {
330
157
  ok: true,
331
- loggedIn: !!session,
332
- ...browserInfo,
333
158
  ...oauthInfo,
334
159
  },
335
160
  getJson(cmd),
336
161
  );
337
162
  });
338
163
 
339
- // =========================================================================
340
- // strategy — get or set the Twitter operation strategy
341
- // =========================================================================
342
- const strategyCli = tw
343
- .command("strategy")
344
- .description(
345
- "Get or set the Twitter operation strategy (managed, oauth, browser, auto)",
346
- )
347
- .addHelpText(
348
- "after",
349
- `
350
- The strategy controls which authentication path is used for operations that
351
- support both OAuth and browser session:
352
-
353
- managed — route through the platform proxy (platform holds OAuth credentials).
354
- oauth — always use the official Twitter OAuth API. Fails if no OAuth
355
- credential is connected. Best for reliable posting.
356
- browser — always use the browser session (captured cookies). Fails if no
357
- session is loaded. Required for read-only endpoints not available
358
- via OAuth (bookmarks, notifications, search).
359
- auto — try OAuth first, fall back to browser session. This is the default.
360
-
361
- Run without a subcommand to display the current strategy. Use "set" to change it.
362
-
363
- Examples:
364
- $ assistant x strategy
365
- $ assistant x strategy set oauth
366
- $ assistant x strategy set auto`,
367
- )
368
- .action(async (_opts: unknown, cmd: Command) => {
369
- const json = getJson(cmd);
370
- try {
371
- const r = await sendTwitterConfigRequest("get_strategy");
372
- output({ ok: true, strategy: r.strategy ?? "auto" }, json);
373
- } catch (err) {
374
- outputError(err instanceof Error ? err.message : String(err));
375
- }
376
- });
377
-
378
- strategyCli
379
- .command("set")
380
- .description("Set the Twitter operation strategy")
381
- .argument("<value>", "Strategy value: oauth, browser, or auto")
382
- .addHelpText(
383
- "after",
384
- `
385
- Arguments:
386
- value Strategy to use: "oauth", "browser", or "auto"
387
-
388
- Sets the preferred strategy for Twitter operations that support dual-path
389
- routing. The setting is persisted by the assistant and applies to all subsequent
390
- operations until changed. Note: "managed" is determined by integration mode
391
- and cannot be set manually.
392
-
393
- Examples:
394
- $ assistant x strategy set oauth
395
- $ assistant x strategy set browser
396
- $ assistant x strategy set auto`,
397
- )
398
- .action(async (value: string, _opts: unknown, cmd: Command) => {
399
- const json = getJson(cmd);
400
- try {
401
- const r = await sendTwitterConfigRequest("set_strategy", {
402
- strategy: value,
403
- });
404
- if (r.success) {
405
- output({ ok: true, strategy: r.strategy }, json);
406
- } else {
407
- output(
408
- { ok: false, error: r.error ?? "Failed to set strategy" },
409
- json,
410
- );
411
- process.exitCode = 1;
412
- }
413
- } catch (err) {
414
- outputError(err instanceof Error ? err.message : String(err));
415
- }
416
- });
417
-
418
164
  // =========================================================================
419
165
  // post — post a tweet
420
166
  // =========================================================================
421
167
  tw.command("post")
422
168
  .description("Post a tweet")
423
169
  .argument("<text>", "Tweet text")
424
- .requiredOption(
425
- "--strategy <strategy>",
426
- "Operation strategy: oauth, browser, auto, or managed",
427
- )
170
+ .option("--managed", "Route through the platform proxy (managed mode)")
428
171
  .option(
429
172
  "--oauth-token <token>",
430
- "OAuth access token (required when strategy is oauth or auto)",
173
+ "OAuth access token (required for OAuth path)",
431
174
  )
432
175
  .addHelpText(
433
176
  "after",
@@ -435,41 +178,25 @@ Examples:
435
178
  Arguments:
436
179
  text The tweet text to post (max 280 characters)
437
180
 
438
- Posts a new tweet using the routed system. The --strategy flag controls which
439
- path is used. The response includes the tweet ID, URL, and which path was used.
440
-
441
- Strategies:
442
- oauth — use the local OAuth token directly
443
- browser — use the browser session (CDP)
444
- auto — try OAuth first, fall back to browser
445
- managed — route through the platform proxy (platform holds OAuth credentials)
181
+ Posts a new tweet. By default uses OAuth mode. Pass --managed to route through
182
+ the platform proxy instead. The response includes the tweet ID, URL, and which
183
+ path was used.
446
184
 
447
185
  Examples:
448
- $ assistant x post "Hello world" --strategy browser
449
- $ assistant x post "Hello world" --strategy oauth --oauth-token "$TOKEN"
450
- $ assistant x post "Hello world" --strategy auto --oauth-token "$TOKEN"
451
- $ assistant x post "Hello world" --strategy managed`,
186
+ $ assistant x post "Hello world"
187
+ $ assistant x post "Hello world" --oauth-token "$TOKEN"
188
+ $ assistant x post "Hello world" --managed`,
452
189
  )
453
190
  .action(
454
191
  async (
455
192
  text: string,
456
- opts: { strategy: string; oauthToken?: string },
193
+ opts: { managed?: boolean; oauthToken?: string },
457
194
  cmd: Command,
458
195
  ) => {
459
196
  await run(cmd, async () => {
460
- const strategy = opts.strategy as TwitterStrategy;
461
- if (
462
- strategy !== "oauth" &&
463
- strategy !== "browser" &&
464
- strategy !== "auto" &&
465
- strategy !== "managed"
466
- ) {
467
- throw new Error(
468
- `Invalid strategy "${opts.strategy}". Must be oauth, browser, auto, or managed.`,
469
- );
470
- }
197
+ const mode: TwitterMode = opts.managed ? "managed" : "oauth";
471
198
  const { result, pathUsed } = await routedPostTweet(text, {
472
- strategy,
199
+ mode,
473
200
  oauthToken: opts.oauthToken,
474
201
  });
475
202
  return {
@@ -489,13 +216,10 @@ Examples:
489
216
  .description("Reply to a tweet")
490
217
  .argument("<tweetUrl>", "Tweet URL or tweet ID")
491
218
  .argument("<text>", "Reply text")
492
- .requiredOption(
493
- "--strategy <strategy>",
494
- "Operation strategy: oauth, browser, auto, or managed",
495
- )
219
+ .option("--managed", "Route through the platform proxy (managed mode)")
496
220
  .option(
497
221
  "--oauth-token <token>",
498
- "OAuth access token (required when strategy is oauth or auto)",
222
+ "OAuth access token (required for OAuth path)",
499
223
  )
500
224
  .addHelpText(
501
225
  "after",
@@ -506,32 +230,22 @@ Arguments:
506
230
 
507
231
  Posts a reply to the specified tweet. Accepts either a full tweet URL or a bare
508
232
  numeric tweet ID. The tweet ID is extracted from the last numeric segment of the
509
- URL. The --strategy flag controls which path is used.
233
+ URL. By default uses OAuth mode. Pass --managed to route through the platform proxy.
510
234
 
511
235
  Examples:
512
- $ assistant x reply https://x.com/elonmusk/status/1234567890 "Great point!" --strategy browser
513
- $ assistant x reply 1234567890 "Interesting thread" --strategy oauth --oauth-token "$TOKEN"
514
- $ assistant x reply 1234567890 "Nice!" --strategy managed`,
236
+ $ assistant x reply https://x.com/elonmusk/status/1234567890 "Great point!"
237
+ $ assistant x reply 1234567890 "Interesting thread" --oauth-token "$TOKEN"
238
+ $ assistant x reply 1234567890 "Nice!" --managed`,
515
239
  )
516
240
  .action(
517
241
  async (
518
242
  tweetUrl: string,
519
243
  text: string,
520
- opts: { strategy: string; oauthToken?: string },
244
+ opts: { managed?: boolean; oauthToken?: string },
521
245
  cmd: Command,
522
246
  ) => {
523
247
  await run(cmd, async () => {
524
- const strategy = opts.strategy as TwitterStrategy;
525
- if (
526
- strategy !== "oauth" &&
527
- strategy !== "browser" &&
528
- strategy !== "auto" &&
529
- strategy !== "managed"
530
- ) {
531
- throw new Error(
532
- `Invalid strategy "${opts.strategy}". Must be oauth, browser, auto, or managed.`,
533
- );
534
- }
248
+ const mode: TwitterMode = opts.managed ? "managed" : "oauth";
535
249
  // Extract tweet ID: either a bare numeric ID or the last numeric segment of a URL
536
250
  const idMatch = tweetUrl.match(/(\d+)\s*$/);
537
251
  if (!idMatch) {
@@ -540,7 +254,7 @@ Examples:
540
254
  const inReplyToTweetId = idMatch[1];
541
255
  const { result, pathUsed } = await routedPostTweet(text, {
542
256
  inReplyToTweetId,
543
- strategy,
257
+ mode,
544
258
  oauthToken: opts.oauthToken,
545
259
  });
546
260
  return {
@@ -560,52 +274,33 @@ Examples:
560
274
  .description("Fetch a user's recent tweets")
561
275
  .argument("<screenName>", "Twitter screen name (without @)")
562
276
  .option("--count <n>", "Number of tweets to fetch", "20")
563
- .option(
564
- "--strategy <strategy>",
565
- "Operation strategy: managed or browser (default: browser)",
566
- )
567
277
  .addHelpText(
568
278
  "after",
569
279
  `
570
280
  Arguments:
571
281
  screenName Twitter screen name without the @ prefix (e.g. "elonmusk", not "@elonmusk")
572
282
 
573
- Fetches a user's recent tweets. Resolves the screen name to a user ID first,
574
- then retrieves their tweet timeline. The --count flag controls how many tweets
575
- to return (default: 20). Use --strategy managed to route through the platform proxy.
283
+ Fetches a user's recent tweets via managed mode. Resolves the screen name to a
284
+ user ID first, then retrieves their tweet timeline. The --count flag controls
285
+ how many tweets to return (default: 20).
576
286
 
577
287
  Examples:
578
288
  $ assistant x timeline elonmusk
579
289
  $ assistant x timeline vaborsh --count 50
580
- $ assistant x timeline openai --count 10 --json
581
- $ assistant x timeline elonmusk --strategy managed`,
290
+ $ assistant x timeline openai --count 10 --json`,
582
291
  )
583
292
  .action(
584
- async (
585
- screenName: string,
586
- opts: { count: string; strategy?: string },
587
- cmd: Command,
588
- ) => {
293
+ async (screenName: string, opts: { count: string }, cmd: Command) => {
589
294
  await run(cmd, async () => {
590
- const strategy = (opts.strategy ?? "browser") as TwitterStrategy;
591
- if (
592
- strategy !== "oauth" &&
593
- strategy !== "browser" &&
594
- strategy !== "auto" &&
595
- strategy !== "managed"
596
- ) {
597
- throw new Error(
598
- `Invalid strategy "${opts.strategy}". Must be oauth, browser, auto, or managed.`,
599
- );
600
- }
295
+ const mode: TwitterMode = "managed";
601
296
  const { result: user, pathUsed } = await routedGetUserByScreenName(
602
297
  screenName.replace(/^@/, ""),
603
- { strategy },
298
+ { mode },
604
299
  );
605
300
  const { result: tweets } = await routedGetUserTweets(
606
301
  user.userId,
607
302
  parseInt(opts.count, 10),
608
- { strategy },
303
+ { mode },
609
304
  );
610
305
  return { user, tweets, pathUsed };
611
306
  });
@@ -618,10 +313,6 @@ Examples:
618
313
  tw.command("tweet")
619
314
  .description("Fetch a tweet and its reply thread")
620
315
  .argument("<tweetIdOrUrl>", "Tweet ID or URL")
621
- .option(
622
- "--strategy <strategy>",
623
- "Operation strategy: managed or browser (default: browser)",
624
- )
625
316
  .addHelpText(
626
317
  "after",
627
318
  `
@@ -629,45 +320,28 @@ Arguments:
629
320
  tweetIdOrUrl A bare tweet ID (e.g. 1234567890) or a full tweet URL
630
321
  (e.g. https://x.com/user/status/1234567890)
631
322
 
632
- Fetches a single tweet and its reply thread. The tweet ID is extracted from the
633
- last numeric segment of the input. Returns an array of tweets representing the
634
- conversation thread. Use --strategy managed to route through the platform proxy.
323
+ Fetches a single tweet and its reply thread via managed mode. The tweet ID is
324
+ extracted from the last numeric segment of the input. Returns an array of tweets
325
+ representing the conversation thread.
635
326
 
636
327
  Examples:
637
328
  $ assistant x tweet 1234567890
638
329
  $ assistant x tweet https://x.com/elonmusk/status/1234567890
639
- $ assistant x tweet https://x.com/openai/status/9876543210 --json
640
- $ assistant x tweet 1234567890 --strategy managed`,
330
+ $ assistant x tweet https://x.com/openai/status/9876543210 --json`,
641
331
  )
642
- .action(
643
- async (
644
- tweetIdOrUrl: string,
645
- opts: { strategy?: string },
646
- cmd: Command,
647
- ) => {
648
- await run(cmd, async () => {
649
- const idMatch = tweetIdOrUrl.match(/(\d+)\s*$/);
650
- if (!idMatch)
651
- throw new Error(`Could not extract tweet ID from: ${tweetIdOrUrl}`);
652
- const strategy = (opts.strategy ?? "browser") as TwitterStrategy;
653
- if (
654
- strategy !== "oauth" &&
655
- strategy !== "browser" &&
656
- strategy !== "auto" &&
657
- strategy !== "managed"
658
- ) {
659
- throw new Error(
660
- `Invalid strategy "${opts.strategy}". Must be oauth, browser, auto, or managed.`,
661
- );
662
- }
663
- const { result: tweets, pathUsed } = await routedGetTweetDetail(
664
- idMatch[1],
665
- { strategy },
666
- );
667
- return { tweets, pathUsed };
668
- });
669
- },
670
- );
332
+ .action(async (tweetIdOrUrl: string, _opts: unknown, cmd: Command) => {
333
+ await run(cmd, async () => {
334
+ const idMatch = tweetIdOrUrl.match(/(\d+)\s*$/);
335
+ if (!idMatch)
336
+ throw new Error(`Could not extract tweet ID from: ${tweetIdOrUrl}`);
337
+ const mode: TwitterMode = "managed";
338
+ const { result: tweets, pathUsed } = await routedGetTweetDetail(
339
+ idMatch[1],
340
+ { mode },
341
+ );
342
+ return { tweets, pathUsed };
343
+ });
344
+ });
671
345
 
672
346
  // =========================================================================
673
347
  // search — search tweets
@@ -676,10 +350,6 @@ Examples:
676
350
  .description("Search tweets")
677
351
  .argument("<query>", "Search query")
678
352
  .option("--product <type>", "Top, Latest, People, or Media", "Top")
679
- .option(
680
- "--strategy <strategy>",
681
- "Operation strategy: managed or browser (default: browser)",
682
- )
683
353
  .addHelpText(
684
354
  "after",
685
355
  `
@@ -693,253 +363,23 @@ The --product flag selects the search result type:
693
363
  People — user accounts matching the query
694
364
  Media — tweets containing images or video
695
365
 
696
- Use --strategy managed to route through the platform proxy (uses Twitter's
697
- recent search API).
698
-
699
366
  Examples:
700
367
  $ assistant x search "AI agents"
701
368
  $ assistant x search "from:elonmusk SpaceX" --product Latest
702
- $ assistant x search "machine learning" --product Media --json
703
- $ assistant x search "AI agents" --strategy managed`,
704
- )
705
- .action(
706
- async (
707
- query: string,
708
- opts: { product: string; strategy?: string },
709
- cmd: Command,
710
- ) => {
711
- await run(cmd, async () => {
712
- const strategy = (opts.strategy ?? "browser") as TwitterStrategy;
713
- if (
714
- strategy !== "oauth" &&
715
- strategy !== "browser" &&
716
- strategy !== "auto" &&
717
- strategy !== "managed"
718
- ) {
719
- throw new Error(
720
- `Invalid strategy "${opts.strategy}". Must be oauth, browser, auto, or managed.`,
721
- );
722
- }
723
- const product = opts.product as "Top" | "Latest" | "People" | "Media";
724
- const { result: tweets, pathUsed } = await routedSearchTweets(
725
- query,
726
- product,
727
- { strategy },
728
- );
729
- return { query, tweets, pathUsed };
730
- });
731
- },
732
- );
733
-
734
- // =========================================================================
735
- // bookmarks — fetch bookmarks
736
- // =========================================================================
737
- tw.command("bookmarks")
738
- .description("Fetch your bookmarks")
739
- .option("--count <n>", "Number of bookmarks", "20")
740
- .addHelpText(
741
- "after",
742
- `
743
- Fetches the authenticated user's bookmarked tweets via the browser session.
744
- The --count flag controls how many bookmarks to return (default: 20).
745
-
746
- Requires an active browser session. Bookmarks are private and only available
747
- for the logged-in account.
748
-
749
- Examples:
750
- $ assistant x bookmarks
751
- $ assistant x bookmarks --count 50
752
- $ assistant x bookmarks --json`,
753
- )
754
- .action(async (opts: { count: string }, cmd: Command) => {
755
- await run(cmd, async () => {
756
- const tweets = await getBookmarks(parseInt(opts.count, 10));
757
- return { tweets };
758
- });
759
- });
760
-
761
- // =========================================================================
762
- // home — fetch home timeline
763
- // =========================================================================
764
- tw.command("home")
765
- .description("Fetch your home timeline")
766
- .option("--count <n>", "Number of tweets", "20")
767
- .addHelpText(
768
- "after",
769
- `
770
- Fetches the authenticated user's home timeline (the "For You" feed) via the
771
- browser session. The --count flag controls how many tweets to return (default: 20).
772
-
773
- Requires an active browser session.
774
-
775
- Examples:
776
- $ assistant x home
777
- $ assistant x home --count 50
778
- $ assistant x home --json`,
779
- )
780
- .action(async (opts: { count: string }, cmd: Command) => {
781
- await run(cmd, async () => {
782
- const tweets = await getHomeTimeline(parseInt(opts.count, 10));
783
- return { tweets };
784
- });
785
- });
786
-
787
- // =========================================================================
788
- // notifications — fetch notifications
789
- // =========================================================================
790
- tw.command("notifications")
791
- .description("Fetch your notifications")
792
- .option("--count <n>", "Number of notifications", "20")
793
- .addHelpText(
794
- "after",
795
- `
796
- Fetches the authenticated user's Twitter notifications (mentions, likes,
797
- retweets, follows, etc.) via the browser session. The --count flag controls
798
- how many notifications to return (default: 20).
799
-
800
- Requires an active browser session.
801
-
802
- Examples:
803
- $ assistant x notifications
804
- $ assistant x notifications --count 50
805
- $ assistant x notifications --json`,
806
- )
807
- .action(async (opts: { count: string }, cmd: Command) => {
808
- await run(cmd, async () => {
809
- const notifications = await getNotifications(parseInt(opts.count, 10));
810
- return { notifications };
811
- });
812
- });
813
-
814
- // =========================================================================
815
- // likes — fetch a user's liked tweets
816
- // =========================================================================
817
- tw.command("likes")
818
- .description("Fetch a user's liked tweets")
819
- .argument("<screenName>", "Twitter screen name (without @)")
820
- .option("--count <n>", "Number of likes", "20")
821
- .addHelpText(
822
- "after",
823
- `
824
- Arguments:
825
- screenName Twitter screen name without the @ prefix (e.g. "elonmusk", not "@elonmusk")
826
-
827
- Fetches tweets liked by the specified user via the browser session. Resolves the
828
- screen name to a user ID first. The --count flag controls how many liked tweets
829
- to return (default: 20).
830
-
831
- Examples:
832
- $ assistant x likes elonmusk
833
- $ assistant x likes vaborsh --count 50
834
- $ assistant x likes openai --json`,
835
- )
836
- .action(
837
- async (screenName: string, opts: { count: string }, cmd: Command) => {
838
- await run(cmd, async () => {
839
- const user = await getUserByScreenName(screenName.replace(/^@/, ""));
840
- const tweets = await getLikes(user.userId, parseInt(opts.count, 10));
841
- return { user, tweets };
842
- });
843
- },
844
- );
845
-
846
- // =========================================================================
847
- // followers — fetch a user's followers
848
- // =========================================================================
849
- tw.command("followers")
850
- .description("Fetch a user's followers")
851
- .argument("<screenName>", "Twitter screen name (without @)")
852
- .addHelpText(
853
- "after",
854
- `
855
- Arguments:
856
- screenName Twitter screen name without the @ prefix (e.g. "elonmusk", not "@elonmusk")
857
-
858
- Fetches the list of accounts following the specified user via the browser session.
859
- Resolves the screen name to a user ID first.
860
-
861
- Examples:
862
- $ assistant x followers elonmusk
863
- $ assistant x followers vaborsh --json`,
369
+ $ assistant x search "machine learning" --product Media --json`,
864
370
  )
865
- .action(async (screenName: string, _opts: unknown, cmd: Command) => {
371
+ .action(async (query: string, opts: { product: string }, cmd: Command) => {
866
372
  await run(cmd, async () => {
867
- const cleanName = screenName.replace(/^@/, "");
868
- const user = await getUserByScreenName(cleanName);
869
- const followers = await getFollowers(user.userId, cleanName);
870
- return { user, followers };
373
+ const mode: TwitterMode = "managed";
374
+ const product = opts.product as "Top" | "Latest" | "People" | "Media";
375
+ const { result: tweets, pathUsed } = await routedSearchTweets(
376
+ query,
377
+ product,
378
+ { mode },
379
+ );
380
+ return { query, tweets, pathUsed };
871
381
  });
872
382
  });
873
-
874
- // =========================================================================
875
- // following — fetch who a user follows
876
- // =========================================================================
877
- tw.command("following")
878
- .description("Fetch who a user follows")
879
- .argument("<screenName>", "Twitter screen name (without @)")
880
- .option("--count <n>", "Number of following", "20")
881
- .addHelpText(
882
- "after",
883
- `
884
- Arguments:
885
- screenName Twitter screen name without the @ prefix (e.g. "elonmusk", not "@elonmusk")
886
-
887
- Fetches the list of accounts the specified user follows via the browser session.
888
- Resolves the screen name to a user ID first. The --count flag controls how many
889
- results to return (default: 20).
890
-
891
- Examples:
892
- $ assistant x following elonmusk
893
- $ assistant x following vaborsh --count 100
894
- $ assistant x following openai --json`,
895
- )
896
- .action(
897
- async (screenName: string, opts: { count: string }, cmd: Command) => {
898
- await run(cmd, async () => {
899
- const user = await getUserByScreenName(screenName.replace(/^@/, ""));
900
- const following = await getFollowing(
901
- user.userId,
902
- parseInt(opts.count, 10),
903
- );
904
- return { user, following };
905
- });
906
- },
907
- );
908
-
909
- // =========================================================================
910
- // media — fetch a user's media tweets
911
- // =========================================================================
912
- tw.command("media")
913
- .description("Fetch a user's media tweets")
914
- .argument("<screenName>", "Twitter screen name (without @)")
915
- .option("--count <n>", "Number of media tweets", "20")
916
- .addHelpText(
917
- "after",
918
- `
919
- Arguments:
920
- screenName Twitter screen name without the @ prefix (e.g. "elonmusk", not "@elonmusk")
921
-
922
- Fetches tweets containing images or video from the specified user via the browser
923
- session. Resolves the screen name to a user ID first. The --count flag controls
924
- how many media tweets to return (default: 20).
925
-
926
- Examples:
927
- $ assistant x media elonmusk
928
- $ assistant x media nasa --count 50
929
- $ assistant x media openai --json`,
930
- )
931
- .action(
932
- async (screenName: string, opts: { count: string }, cmd: Command) => {
933
- await run(cmd, async () => {
934
- const user = await getUserByScreenName(screenName.replace(/^@/, ""));
935
- const tweets = await getUserMedia(
936
- user.userId,
937
- parseInt(opts.count, 10),
938
- );
939
- return { user, tweets };
940
- });
941
- },
942
- );
943
383
  }
944
384
 
945
385
  // ---------------------------------------------------------------------------
@@ -949,16 +389,13 @@ Examples:
949
389
  /**
950
390
  * Send a Twitter integration config request to the daemon via HTTP.
951
391
  *
952
- * Maps the old IPC `twitter_integration_config` message actions to HTTP
953
- * endpoints on the settings routes:
954
- * - "get" / "get_strategy" → GET /v1/integrations/twitter/auth/status
955
- * - "set_strategy" → PUT /v1/settings/client (key=twitter.strategy)
392
+ * Maps the "get" action to HTTP:
393
+ * - "get" GET /v1/integrations/twitter/auth/status
956
394
  */
957
395
  async function sendTwitterConfigRequest(
958
396
  action: string,
959
- extra?: Record<string, unknown>,
960
397
  ): Promise<Record<string, unknown>> {
961
- if (action === "get" || action === "get_strategy") {
398
+ if (action === "get") {
962
399
  const response = await httpSend("/v1/integrations/twitter/auth/status", {
963
400
  method: "GET",
964
401
  });
@@ -967,14 +404,11 @@ async function sendTwitterConfigRequest(
967
404
  throw new Error(`Assistant returned an error: ${text}`);
968
405
  }
969
406
  const data = (await response.json()) as Record<string, unknown>;
970
- // Map the HTTP response shape to the old IPC response shape
971
407
  return {
972
408
  type: "twitter_integration_config_response",
973
409
  success: true,
974
410
  connected: data.connected ?? false,
975
411
  accountInfo: data.accountInfo,
976
- strategy: data.strategy ?? "auto",
977
- strategyConfigured: data.strategyConfigured ?? false,
978
412
  mode: data.mode,
979
413
  managedAvailable: data.managedAvailable ?? false,
980
414
  managedPrerequisites: data.managedPrerequisites,
@@ -982,179 +416,5 @@ async function sendTwitterConfigRequest(
982
416
  };
983
417
  }
984
418
 
985
- if (action === "set_strategy") {
986
- const strategy = extra?.strategy as string | undefined;
987
- if (!strategy) throw new Error("strategy is required for set_strategy");
988
- const response = await httpSend("/v1/settings/client", {
989
- method: "PUT",
990
- body: JSON.stringify({ key: "twitter.strategy", value: strategy }),
991
- });
992
- if (!response.ok) {
993
- const text = await response.text();
994
- throw new Error(`Assistant returned an error: ${text}`);
995
- }
996
- return {
997
- type: "twitter_integration_config_response",
998
- success: true,
999
- strategy,
1000
- };
1001
- }
1002
-
1003
419
  throw new Error(`Unsupported twitter_integration_config action: ${action}`);
1004
420
  }
1005
-
1006
- // ---------------------------------------------------------------------------
1007
- // Chrome CDP helpers (via `assistant browser chrome` CLI)
1008
- // ---------------------------------------------------------------------------
1009
-
1010
- async function launchChromeCdp(
1011
- startUrl?: string,
1012
- ): Promise<{ baseUrl: string }> {
1013
- const args = ["browser", "chrome", "launch"];
1014
- if (startUrl) args.push("--start-url", startUrl);
1015
- const { stdout } = await execFileAsync("assistant", args);
1016
- const result = JSON.parse(stdout) as {
1017
- ok: boolean;
1018
- baseUrl?: string;
1019
- error?: string;
1020
- };
1021
- if (!result.ok || !result.baseUrl) {
1022
- throw new Error(result.error ?? "Failed to launch Chrome with CDP");
1023
- }
1024
- return { baseUrl: result.baseUrl };
1025
- }
1026
-
1027
- async function minimizeChrome(): Promise<void> {
1028
- try {
1029
- await execFileAsync("assistant", ["browser", "chrome", "minimize"]);
1030
- } catch {
1031
- // best-effort — same as the original
1032
- }
1033
- }
1034
-
1035
- // ---------------------------------------------------------------------------
1036
- // Ride Shotgun learn session helper
1037
- // ---------------------------------------------------------------------------
1038
-
1039
- interface LearnResult {
1040
- recordingId?: string;
1041
- recordingPath?: string;
1042
- }
1043
-
1044
- async function navigateToX(cdpBase: string): Promise<void> {
1045
- try {
1046
- const res = await fetch(`${cdpBase}/json/list`);
1047
- if (!res.ok) return;
1048
- const targets = (await res.json()) as Array<{
1049
- id: string;
1050
- type: string;
1051
- url: string;
1052
- }>;
1053
- const tab = targets.find((t) => t.type === "page");
1054
- if (!tab) return;
1055
- await fetch(
1056
- `${cdpBase}/json/navigate?url=${encodeURIComponent(
1057
- "https://x.com/login",
1058
- )}&id=${tab.id}`,
1059
- { method: "PUT" },
1060
- );
1061
- } catch {
1062
- // best-effort
1063
- }
1064
- }
1065
-
1066
- async function startLearnSession(
1067
- durationSeconds: number,
1068
- ): Promise<LearnResult> {
1069
- const cdpSession = await launchChromeCdp("https://x.com/login");
1070
- await navigateToX(cdpSession.baseUrl);
1071
-
1072
- // Start ride shotgun via HTTP
1073
- const response = await httpSend("/v1/computer-use/ride-shotgun/start", {
1074
- method: "POST",
1075
- body: JSON.stringify({
1076
- durationSeconds,
1077
- intervalSeconds: 5,
1078
- mode: "learn",
1079
- targetDomain: "x.com",
1080
- }),
1081
- });
1082
-
1083
- if (!response.ok) {
1084
- const body = await response.text();
1085
- throw new Error(
1086
- `Cannot connect to assistant: ${response.status} ${body}. Is the assistant running?`,
1087
- );
1088
- }
1089
-
1090
- const startResult = (await response.json()) as {
1091
- watchId?: string;
1092
- sessionId?: string;
1093
- };
1094
-
1095
- if (!startResult.watchId) {
1096
- throw new Error("Ride-shotgun start response missing watchId");
1097
- }
1098
-
1099
- // Poll the status endpoint using watchId to correlate completion
1100
- const { watchId } = startResult;
1101
- const timeoutMs = (durationSeconds + 30) * 1000;
1102
- const pollIntervalMs = 2000;
1103
- const startTime = Date.now();
1104
-
1105
- return new Promise<LearnResult>((resolve, reject) => {
1106
- const tick = async () => {
1107
- if (Date.now() - startTime > timeoutMs) {
1108
- reject(
1109
- new Error(`Learn session timed out after ${durationSeconds + 30}s`),
1110
- );
1111
- return;
1112
- }
1113
-
1114
- try {
1115
- const statusRes = await httpSend(
1116
- `/v1/computer-use/ride-shotgun/status/${watchId}`,
1117
- { method: "GET" },
1118
- );
1119
- if (!statusRes.ok) {
1120
- setTimeout(tick, pollIntervalMs);
1121
- return;
1122
- }
1123
-
1124
- const status = (await statusRes.json()) as {
1125
- status: string;
1126
- recordingId?: string;
1127
- savedRecordingPath?: string;
1128
- bootstrapFailureReason?: string;
1129
- };
1130
-
1131
- if (status.bootstrapFailureReason) {
1132
- reject(
1133
- new Error(`Learn session failed: ${status.bootstrapFailureReason}`),
1134
- );
1135
- return;
1136
- }
1137
-
1138
- if (status.status === "completed") {
1139
- if (status.recordingId) {
1140
- resolve({
1141
- recordingId: status.recordingId,
1142
- recordingPath: status.savedRecordingPath,
1143
- });
1144
- } else {
1145
- reject(
1146
- new Error("Learn session completed but no recording was saved."),
1147
- );
1148
- }
1149
- return;
1150
- }
1151
- } catch {
1152
- // Status endpoint not reachable — continue polling
1153
- }
1154
-
1155
- setTimeout(tick, pollIntervalMs);
1156
- };
1157
-
1158
- setTimeout(tick, pollIntervalMs);
1159
- });
1160
- }