@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.
- package/ARCHITECTURE.md +13 -14
- package/README.md +11 -12
- package/docs/architecture/integrations.md +75 -93
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +0 -2
- package/src/__tests__/bundled-asset.test.ts +1 -1
- package/src/__tests__/checker.test.ts +31 -28
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +6 -6
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/error-handler-friendly-messages.test.ts +46 -0
- package/src/__tests__/managed-twitter-guardrails.test.ts +5 -1
- package/src/__tests__/onboarding-template-contract.test.ts +0 -10
- package/src/__tests__/provider-fail-open-selection.test.ts +12 -2
- package/src/__tests__/send-endpoint-busy.test.ts +0 -3
- package/src/__tests__/session-confirmation-signals.test.ts +7 -45
- package/src/__tests__/starter-task-flow.test.ts +9 -19
- package/src/__tests__/system-prompt.test.ts +3 -4
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/twitter-platform-proxy-client.test.ts +43 -18
- package/src/cli/commands/amazon/index.ts +4 -39
- package/src/cli/commands/amazon/session.ts +18 -26
- package/src/cli/commands/twitter/__tests__/cli-read-routing.test.ts +58 -196
- package/src/cli/commands/twitter/__tests__/cli-routing.test.ts +26 -186
- package/src/cli/commands/twitter/__tests__/oauth-client.test.ts +1 -47
- package/src/cli/commands/twitter/index.ts +95 -835
- package/src/cli/commands/twitter/oauth-client.ts +1 -35
- package/src/cli/commands/twitter/router.ts +70 -115
- package/src/cli/commands/twitter/types.ts +30 -0
- package/src/cli/reference.ts +2 -2
- package/src/config/bundled-skills/amazon/SKILL.md +0 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -6
- package/src/config/bundled-skills/app-builder/TOOLS.json +0 -4
- package/src/config/bundled-skills/doordash/SKILL.md +0 -1
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +1 -82
- package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -28
- package/src/config/bundled-skills/doordash/lib/session.ts +21 -17
- package/src/config/bundled-skills/twitter/SKILL.md +53 -166
- package/src/config/feature-flag-registry.json +8 -0
- package/src/daemon/handlers/session-history.ts +41 -9
- package/src/daemon/lifecycle.ts +4 -17
- package/src/daemon/message-types/apps.ts +0 -25
- package/src/daemon/message-types/integrations.ts +1 -7
- package/src/daemon/message-types/sessions.ts +6 -1
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/ride-shotgun-handler.ts +33 -1
- package/src/daemon/seed-files.ts +3 -27
- package/src/daemon/server.ts +2 -18
- package/src/daemon/session-agent-loop-handlers.ts +24 -2
- package/src/daemon/session-runtime-assembly.ts +0 -7
- package/src/daemon/session-surfaces.ts +185 -33
- package/src/daemon/session.ts +2 -28
- package/src/memory/app-store.ts +0 -18
- package/src/memory/schema/infrastructure.ts +0 -8
- package/src/permissions/defaults.ts +3 -3
- package/src/prompts/system-prompt.ts +4 -5
- package/src/prompts/templates/BOOTSTRAP.md +0 -3
- package/src/providers/registry.ts +2 -4
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
- package/src/runtime/auth/__tests__/scopes.test.ts +2 -1
- package/src/runtime/auth/route-policy.ts +0 -4
- package/src/runtime/auth/scopes.ts +1 -0
- package/src/runtime/auth/token-service.ts +1 -1
- package/src/runtime/http-types.ts +10 -0
- package/src/runtime/middleware/error-handler.ts +14 -1
- package/src/runtime/routes/app-management-routes.ts +61 -64
- package/src/runtime/routes/brain-graph/brain-graph.html +1845 -0
- package/src/runtime/routes/brain-graph-routes.ts +4 -42
- package/src/runtime/routes/conversation-routes.ts +9 -6
- package/src/runtime/routes/diagnostics-routes.ts +91 -14
- package/src/runtime/routes/settings-routes.ts +3 -93
- package/src/tools/AGENTS.md +38 -0
- package/src/tools/apps/executors.ts +0 -6
- package/src/tools/document/editor-template.ts +10 -8
- package/src/twitter/platform-proxy-client.ts +6 -3
- package/src/util/errors.ts +12 -0
- package/src/__tests__/home-base-bootstrap.test.ts +0 -84
- package/src/__tests__/prebuilt-home-base-seed.test.ts +0 -79
- package/src/cli/commands/twitter/__tests__/cli-error-shaping.test.ts +0 -265
- package/src/cli/commands/twitter/client.ts +0 -989
- package/src/cli/commands/twitter/session.ts +0 -121
- package/src/home-base/app-link-store.ts +0 -78
- package/src/home-base/bootstrap.ts +0 -74
- package/src/home-base/prebuilt/brain-graph.html +0 -1483
- package/src/home-base/prebuilt/index.html +0 -702
- package/src/home-base/prebuilt/seed-metadata.json +0 -21
- package/src/home-base/prebuilt/seed.ts +0 -122
- package/src/home-base/prebuilt-home-base-updater.ts +0 -36
- 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
|
|
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
|
-
|
|
72
|
-
|
|
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:
|
|
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
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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" --
|
|
156
|
-
$ assistant x post "Hello world" --
|
|
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
|
-
//
|
|
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
|
+
// status — check OAuth and managed mode info
|
|
281
115
|
// =========================================================================
|
|
282
116
|
tw.command("status")
|
|
283
|
-
.description("Check Twitter
|
|
117
|
+
.description("Check Twitter OAuth and managed mode status")
|
|
284
118
|
.addHelpText(
|
|
285
119
|
"after",
|
|
286
120
|
`
|
|
287
|
-
Shows the current state of
|
|
121
|
+
Shows the current state of authentication:
|
|
288
122
|
|
|
289
|
-
|
|
290
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
|
145
|
+
// Daemon may not be running; report what we can
|
|
320
146
|
oauthInfo = {
|
|
321
147
|
oauthConnected: undefined,
|
|
322
148
|
oauthAccount: undefined,
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
.
|
|
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
|
|
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
|
|
439
|
-
|
|
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"
|
|
449
|
-
$ assistant x post "Hello world" --
|
|
450
|
-
$ assistant x post "Hello world" --
|
|
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: {
|
|
193
|
+
opts: { managed?: boolean; oauthToken?: string },
|
|
457
194
|
cmd: Command,
|
|
458
195
|
) => {
|
|
459
196
|
await run(cmd, async () => {
|
|
460
|
-
const
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
|
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.
|
|
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!"
|
|
513
|
-
$ assistant x reply 1234567890 "Interesting thread" --
|
|
514
|
-
$ assistant x reply 1234567890 "Nice!" --
|
|
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: {
|
|
244
|
+
opts: { managed?: boolean; oauthToken?: string },
|
|
521
245
|
cmd: Command,
|
|
522
246
|
) => {
|
|
523
247
|
await run(cmd, async () => {
|
|
524
|
-
const
|
|
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
|
-
|
|
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
|
|
574
|
-
then retrieves their tweet timeline. The --count flag controls
|
|
575
|
-
to return (default: 20).
|
|
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
|
|
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
|
-
{
|
|
298
|
+
{ mode },
|
|
604
299
|
);
|
|
605
300
|
const { result: tweets } = await routedGetUserTweets(
|
|
606
301
|
user.userId,
|
|
607
302
|
parseInt(opts.count, 10),
|
|
608
|
-
{
|
|
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
|
|
633
|
-
last numeric segment of the input. Returns an array of tweets
|
|
634
|
-
|
|
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
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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 (
|
|
371
|
+
.action(async (query: string, opts: { product: string }, cmd: Command) => {
|
|
866
372
|
await run(cmd, async () => {
|
|
867
|
-
const
|
|
868
|
-
const
|
|
869
|
-
const
|
|
870
|
-
|
|
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
|
|
953
|
-
*
|
|
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"
|
|
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
|
-
}
|