dsh-subscriptions 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/cordis.patch.yml +12 -0
- package/lib/auth/claude-code-creds.d.ts +23 -0
- package/lib/auth/claude-code-creds.js +172 -0
- package/lib/auth/device-flow.d.ts +55 -0
- package/lib/auth/device-flow.js +177 -0
- package/lib/auth/jwt.d.ts +10 -0
- package/lib/auth/jwt.js +25 -0
- package/lib/auth/oauth-flow.d.ts +91 -0
- package/lib/auth/oauth-flow.js +227 -0
- package/lib/auth/pkce.d.ts +31 -0
- package/lib/auth/pkce.js +35 -0
- package/lib/auth/rpc.d.ts +209 -0
- package/lib/auth/rpc.js +386 -0
- package/lib/auth/store.d.ts +196 -0
- package/lib/auth/store.js +468 -0
- package/lib/client/CursorCard.d.ts +9 -0
- package/lib/client/CursorCard.js +113 -0
- package/lib/client/ExternalUsageCards.d.ts +9 -0
- package/lib/client/ExternalUsageCards.js +72 -0
- package/lib/client/ImageGallery.d.ts +54 -0
- package/lib/client/ImageGallery.js +112 -0
- package/lib/client/ImageGenerateToolview.d.ts +48 -0
- package/lib/client/ImageGenerateToolview.js +133 -0
- package/lib/client/ProviderAccountManager.d.ts +13 -0
- package/lib/client/ProviderAccountManager.js +147 -0
- package/lib/client/ProviderModelEditor.d.ts +39 -0
- package/lib/client/ProviderModelEditor.js +107 -0
- package/lib/client/SpeedSelect.d.ts +71 -0
- package/lib/client/SpeedSelect.js +177 -0
- package/lib/client/SubscriptionUsageBadge.d.ts +116 -0
- package/lib/client/SubscriptionUsageBadge.js +543 -0
- package/lib/client/SubscriptionsSection.d.ts +140 -0
- package/lib/client/SubscriptionsSection.js +544 -0
- package/lib/client/UsageBadgeDisplaySetting.d.ts +7 -0
- package/lib/client/UsageBadgeDisplaySetting.js +36 -0
- package/lib/client/VideoGenerateToolview.d.ts +42 -0
- package/lib/client/VideoGenerateToolview.js +175 -0
- package/lib/client/account-preferences.d.ts +24 -0
- package/lib/client/account-preferences.js +21 -0
- package/lib/client/fast-command.d.ts +2 -0
- package/lib/client/fast-command.js +4 -0
- package/lib/client/host-contract.d.ts +90 -0
- package/lib/client/host-contract.js +101 -0
- package/lib/client/host-icons.d.ts +24 -0
- package/lib/client/host-icons.js +21 -0
- package/lib/client/index.d.ts +27 -0
- package/lib/client/index.js +136 -0
- package/lib/client/locales.d.ts +297 -0
- package/lib/client/locales.js +295 -0
- package/lib/client/subscription-card-styles.d.ts +3 -0
- package/lib/client/subscription-card-styles.js +53 -0
- package/lib/client/subscriptions-rpc.d.ts +14 -0
- package/lib/client/subscriptions-rpc.js +34 -0
- package/lib/client/usage-badge-preferences.d.ts +11 -0
- package/lib/client/usage-badge-preferences.js +41 -0
- package/lib/client.js +4472 -0
- package/lib/client.js.map +1 -0
- package/lib/compat.d.ts +36 -0
- package/lib/compat.js +20 -0
- package/lib/http.d.ts +6 -0
- package/lib/http.js +15 -0
- package/lib/index.d.ts +80 -0
- package/lib/index.js +14018 -0
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +237 -0
- package/lib/provider-settings.d.ts +39 -0
- package/lib/provider-settings.js +151 -0
- package/lib/providers/account-preferences.d.ts +41 -0
- package/lib/providers/account-preferences.js +153 -0
- package/lib/providers/accounts.d.ts +112 -0
- package/lib/providers/accounts.js +142 -0
- package/lib/providers/antigravity-oauth-client.d.ts +7 -0
- package/lib/providers/antigravity-oauth-client.js +7 -0
- package/lib/providers/antigravity.d.ts +110 -0
- package/lib/providers/antigravity.js +516 -0
- package/lib/providers/catalog-store.d.ts +48 -0
- package/lib/providers/catalog-store.js +243 -0
- package/lib/providers/claude-cli-version.d.ts +15 -0
- package/lib/providers/claude-cli-version.js +24 -0
- package/lib/providers/claude.d.ts +181 -0
- package/lib/providers/claude.js +680 -0
- package/lib/providers/codex-client-version.d.ts +8 -0
- package/lib/providers/codex-client-version.js +17 -0
- package/lib/providers/codex-search.d.ts +29 -0
- package/lib/providers/codex-search.js +198 -0
- package/lib/providers/codex.d.ts +229 -0
- package/lib/providers/codex.js +821 -0
- package/lib/providers/common.d.ts +389 -0
- package/lib/providers/common.js +656 -0
- package/lib/providers/copilot.d.ts +344 -0
- package/lib/providers/copilot.js +888 -0
- package/lib/providers/cursor-adapter.d.ts +15 -0
- package/lib/providers/cursor-adapter.js +39 -0
- package/lib/providers/cursor-auth.d.ts +43 -0
- package/lib/providers/cursor-auth.js +169 -0
- package/lib/providers/cursor-model-settings.d.ts +15 -0
- package/lib/providers/cursor-model-settings.js +51 -0
- package/lib/providers/cursor-usage.d.ts +9 -0
- package/lib/providers/cursor-usage.js +112 -0
- package/lib/providers/external-usage-controller.d.ts +19 -0
- package/lib/providers/external-usage-controller.js +32 -0
- package/lib/providers/external-usage.d.ts +8 -0
- package/lib/providers/external-usage.js +82 -0
- package/lib/providers/grok.d.ts +205 -0
- package/lib/providers/grok.js +713 -0
- package/lib/providers/image-pool.d.ts +30 -0
- package/lib/providers/image-pool.js +89 -0
- package/lib/providers/npm-cli-version.d.ts +56 -0
- package/lib/providers/npm-cli-version.js +98 -0
- package/lib/providers/pool-family.d.ts +56 -0
- package/lib/providers/pool-family.js +45 -0
- package/lib/providers/pool-health.d.ts +74 -0
- package/lib/providers/pool-health.js +150 -0
- package/lib/providers/pool-usage.d.ts +86 -0
- package/lib/providers/pool-usage.js +223 -0
- package/lib/providers/pool.d.ts +108 -0
- package/lib/providers/pool.js +368 -0
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/tools/image-generate.d.ts +128 -0
- package/lib/tools/image-generate.js +438 -0
- package/lib/tools/registration.d.ts +15 -0
- package/lib/tools/registration.js +24 -0
- package/lib/tools/video-generate.d.ts +89 -0
- package/lib/tools/video-generate.js +257 -0
- package/lib/tools/x-search.d.ts +58 -0
- package/lib/tools/x-search.js +197 -0
- package/lib/translate/anthropic.d.ts +161 -0
- package/lib/translate/anthropic.js +515 -0
- package/lib/translate/antigravity-schema.d.ts +4 -0
- package/lib/translate/antigravity-schema.js +145 -0
- package/lib/translate/antigravity-thinking.d.ts +5 -0
- package/lib/translate/antigravity-thinking.js +44 -0
- package/lib/translate/antigravity.d.ts +114 -0
- package/lib/translate/antigravity.js +366 -0
- package/lib/translate/chat-completions.d.ts +120 -0
- package/lib/translate/chat-completions.js +382 -0
- package/lib/translate/resolved.d.ts +80 -0
- package/lib/translate/resolved.js +118 -0
- package/lib/translate/responses.d.ts +179 -0
- package/lib/translate/responses.js +408 -0
- package/lib/translate/sse.d.ts +21 -0
- package/lib/translate/sse.js +56 -0
- package/package.json +114 -0
- package/vendor/cursor/LICENSE +21 -0
- package/vendor/cursor/index.d.ts +18 -0
- package/vendor/cursor/index.js +3574 -0
- package/vendor/cursor/proto.js +254 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `video_generate` tool: generate videos through the grok subscription's
|
|
3
|
+
* Imagine video endpoint and save them as MP4 files under the harness home.
|
|
4
|
+
* The xAI API is asynchronous: POST `/v1/videos/generations` returns a
|
|
5
|
+
* `request_id`, GET `/v1/videos/{request_id}` is polled until the status
|
|
6
|
+
* leaves `pending`, and the completed response carries a temporary MP4 URL
|
|
7
|
+
* that is downloaded promptly (the URL expires). The canonical result is the
|
|
8
|
+
* saved file path; videos have no attachment surface, so the result stays
|
|
9
|
+
* text-only (unlike image_generate).
|
|
10
|
+
*/
|
|
11
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { basename, join } from 'node:path';
|
|
13
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
14
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
15
|
+
import { httpLlmError } from '../providers/common.js';
|
|
16
|
+
import { AccountTokenManager } from '../providers/accounts.js';
|
|
17
|
+
import { hostFetch } from '../http.js';
|
|
18
|
+
/** Endpoint the generation request is posted to. */
|
|
19
|
+
export const VIDEO_GENERATE_URL = 'https://api.x.ai/v1/videos/generations';
|
|
20
|
+
/** The video model the grok subscription endpoint serves. */
|
|
21
|
+
export const VIDEO_GENERATE_MODEL = 'grok-imagine-video-1.5';
|
|
22
|
+
/** Polling endpoint for one generation request. */
|
|
23
|
+
export function videoStatusUrl(requestId) {
|
|
24
|
+
return `https://api.x.ai/v1/videos/${encodeURIComponent(requestId)}`;
|
|
25
|
+
}
|
|
26
|
+
/** Default delay between two status polls. */
|
|
27
|
+
export const DEFAULT_POLL_INTERVAL_MS = 3_000;
|
|
28
|
+
/** Default overall deadline for one generation (submit → done). */
|
|
29
|
+
export const DEFAULT_MAX_WAIT_MS = 10 * 60_000;
|
|
30
|
+
/** xAI's supported clip length range in seconds. */
|
|
31
|
+
const DURATION_RANGE = { min: 1, max: 15 };
|
|
32
|
+
/**
|
|
33
|
+
* Assemble the request body from tool arguments (hand-checks the non-empty
|
|
34
|
+
* prompt and the duration range the schema DSL cannot express).
|
|
35
|
+
*/
|
|
36
|
+
export function buildVideoGenerateBody(args) {
|
|
37
|
+
const prompt = args.prompt.trim();
|
|
38
|
+
if (prompt.length === 0)
|
|
39
|
+
throw new Error('video_generate: prompt must be a non-empty string');
|
|
40
|
+
if (args.duration !== undefined
|
|
41
|
+
&& (!Number.isInteger(args.duration)
|
|
42
|
+
|| args.duration < DURATION_RANGE.min
|
|
43
|
+
|| args.duration > DURATION_RANGE.max)) {
|
|
44
|
+
throw new Error(`video_generate: duration must be an integer between ${String(DURATION_RANGE.min)} and ${String(DURATION_RANGE.max)} seconds`);
|
|
45
|
+
}
|
|
46
|
+
const imageUrl = args.image_url?.trim();
|
|
47
|
+
return {
|
|
48
|
+
prompt,
|
|
49
|
+
model: VIDEO_GENERATE_MODEL,
|
|
50
|
+
...args.duration === undefined ? {} : { duration: args.duration },
|
|
51
|
+
...args.aspect_ratio === undefined ? {} : { aspect_ratio: args.aspect_ratio },
|
|
52
|
+
...args.resolution === undefined ? {} : { resolution: args.resolution },
|
|
53
|
+
...imageUrl === undefined || imageUrl.length === 0 ? {} : { image: { url: imageUrl } },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function isRecord(value) {
|
|
57
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Extract the request id from the submit response. Throws when the payload
|
|
61
|
+
* carries none.
|
|
62
|
+
*/
|
|
63
|
+
export function parseVideoStartResponse(payload) {
|
|
64
|
+
const body = isRecord(payload) ? payload : {};
|
|
65
|
+
if (typeof body.request_id !== 'string' || body.request_id.length === 0) {
|
|
66
|
+
throw new Error('video_generate: the response carried no request_id');
|
|
67
|
+
}
|
|
68
|
+
return body.request_id;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Decode one poll response. A `done` payload without a video URL and an
|
|
72
|
+
* unrecognized status both throw (the poll loop cannot make progress on
|
|
73
|
+
* either).
|
|
74
|
+
*/
|
|
75
|
+
export function parseVideoStatusResponse(payload) {
|
|
76
|
+
const body = isRecord(payload) ? payload : {};
|
|
77
|
+
switch (body.status) {
|
|
78
|
+
case 'pending':
|
|
79
|
+
return { status: 'pending' };
|
|
80
|
+
case 'done': {
|
|
81
|
+
const video = isRecord(body.video) ? body.video : {};
|
|
82
|
+
if (typeof video.url !== 'string' || video.url.length === 0) {
|
|
83
|
+
throw new Error('video_generate: the completed response carried no video URL');
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
status: 'done',
|
|
87
|
+
url: video.url,
|
|
88
|
+
...typeof video.duration === 'number' ? { duration: video.duration } : {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
case 'failed':
|
|
92
|
+
case 'expired': {
|
|
93
|
+
const error = isRecord(body.error) ? body.error : {};
|
|
94
|
+
const detail = typeof error.message === 'string' && error.message.length > 0
|
|
95
|
+
? error.message
|
|
96
|
+
: typeof body.error === 'string' && body.error.length > 0 ? body.error : undefined;
|
|
97
|
+
return { status: body.status, ...detail === undefined ? {} : { detail } };
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
throw new Error(`video_generate: unexpected status ${JSON.stringify(body.status)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Directory the downloaded MP4 files are written to. */
|
|
104
|
+
export function videosDirectory() {
|
|
105
|
+
return dshHomePath('plugins', 'subscriptions', 'videos');
|
|
106
|
+
}
|
|
107
|
+
/** Timestamped, collision-safe file name for one generated video. */
|
|
108
|
+
function videoFileName() {
|
|
109
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
110
|
+
return `video-${stamp}-${Math.random().toString(36).slice(2, 8)}.mp4`;
|
|
111
|
+
}
|
|
112
|
+
/** Bound a call-card title's prompt. */
|
|
113
|
+
function truncate(text, max = 60) {
|
|
114
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
115
|
+
}
|
|
116
|
+
/** Abort-aware sleep between two polls. */
|
|
117
|
+
function sleep(ms, signal) {
|
|
118
|
+
if (ms <= 0)
|
|
119
|
+
return Promise.resolve();
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const onAbort = () => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error('video_generate: aborted'));
|
|
124
|
+
};
|
|
125
|
+
const timer = setTimeout(() => {
|
|
126
|
+
signal.removeEventListener('abort', onAbort);
|
|
127
|
+
resolve();
|
|
128
|
+
}, ms);
|
|
129
|
+
if (signal.aborted) {
|
|
130
|
+
onAbort();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Build the `video_generate` tool definition.
|
|
138
|
+
* @param options - grok session source, fetch implementation, and video directory.
|
|
139
|
+
* @returns the tool to register on `ctx.tools`.
|
|
140
|
+
*/
|
|
141
|
+
export function createVideoGenerateTool(options) {
|
|
142
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
143
|
+
const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
144
|
+
return defineTool({
|
|
145
|
+
name: 'video_generate',
|
|
146
|
+
description: `Generate a short video (1-15 seconds) with the grok subscription (${VIDEO_GENERATE_MODEL}) `
|
|
147
|
+
+ 'and save it as an MP4 file. Generation is asynchronous and may take a minute or more; '
|
|
148
|
+
+ 'the tool waits for completion and returns the saved file path. '
|
|
149
|
+
+ 'Optionally animate a still image by passing image_url (image-to-video).',
|
|
150
|
+
parameters: {
|
|
151
|
+
prompt: { type: 'string', required: true, description: 'What the video should show.' },
|
|
152
|
+
duration: {
|
|
153
|
+
type: 'integer',
|
|
154
|
+
description: 'Clip length in seconds (1-15); omit for the provider default.',
|
|
155
|
+
},
|
|
156
|
+
aspect_ratio: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
enum: ['16:9', '9:16', '1:1', '4:3', '3:4', '3:2', '2:3'],
|
|
159
|
+
description: 'Output aspect ratio; omit for the provider default (16:9).',
|
|
160
|
+
},
|
|
161
|
+
resolution: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
enum: ['480p', '720p', '1080p'],
|
|
164
|
+
description: 'Output resolution; omit for the provider default (480p). Higher is slower.',
|
|
165
|
+
},
|
|
166
|
+
image_url: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
description: 'Optional public URL or base64 data URL of a JPEG/PNG/WebP image to animate '
|
|
169
|
+
+ '(image-to-video); the image becomes the starting frame.',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
output: {
|
|
173
|
+
schema: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: {
|
|
176
|
+
path: { type: 'string', required: true },
|
|
177
|
+
url: { type: 'string', required: true },
|
|
178
|
+
duration: { type: 'number' },
|
|
179
|
+
},
|
|
180
|
+
additionalProperties: false,
|
|
181
|
+
},
|
|
182
|
+
render: (_args, value) => [{
|
|
183
|
+
type: 'text',
|
|
184
|
+
text: `Saved video to ${value.path}`
|
|
185
|
+
+ (value.duration === undefined ? '' : ` (${String(value.duration)}s)`)
|
|
186
|
+
+ `\nTemporary provider URL (expires soon): ${value.url}`,
|
|
187
|
+
}],
|
|
188
|
+
// The client toolview fetches the bytes by bare file name through the
|
|
189
|
+
// `/subscriptions-auth` `video` endpoint; meta hands it that name.
|
|
190
|
+
presentationMeta: (_args, value) => ({
|
|
191
|
+
fileName: basename(value.path),
|
|
192
|
+
...value.duration === undefined ? {} : { duration: value.duration },
|
|
193
|
+
}),
|
|
194
|
+
},
|
|
195
|
+
presentCall: args => ({
|
|
196
|
+
card: 'generic',
|
|
197
|
+
title: `video_generate: ${truncate(args.prompt)}`,
|
|
198
|
+
}),
|
|
199
|
+
async execute(args, exec) {
|
|
200
|
+
const body = buildVideoGenerateBody(args);
|
|
201
|
+
const session = await options.tokens.session();
|
|
202
|
+
const fetchFn = options.fetchFn ?? hostFetch;
|
|
203
|
+
const headers = {
|
|
204
|
+
'authorization': `Bearer ${session.accessToken}`,
|
|
205
|
+
'accept': 'application/json',
|
|
206
|
+
};
|
|
207
|
+
const submit = await fetchFn(VIDEO_GENERATE_URL, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { ...headers, 'content-type': 'application/json' },
|
|
210
|
+
body: JSON.stringify(body),
|
|
211
|
+
signal: exec.signal,
|
|
212
|
+
});
|
|
213
|
+
if (!submit.ok)
|
|
214
|
+
throw await httpLlmError(submit, 'video_generate');
|
|
215
|
+
const requestId = parseVideoStartResponse(await submit.json());
|
|
216
|
+
const deadline = Date.now() + maxWaitMs;
|
|
217
|
+
let done;
|
|
218
|
+
for (;;) {
|
|
219
|
+
await sleep(pollIntervalMs, exec.signal);
|
|
220
|
+
const poll = await fetchFn(videoStatusUrl(requestId), {
|
|
221
|
+
method: 'GET',
|
|
222
|
+
headers,
|
|
223
|
+
signal: exec.signal,
|
|
224
|
+
});
|
|
225
|
+
if (!poll.ok)
|
|
226
|
+
throw await httpLlmError(poll, 'video_generate');
|
|
227
|
+
const status = parseVideoStatusResponse(await poll.json());
|
|
228
|
+
if (status.status === 'done') {
|
|
229
|
+
done = status;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (status.status === 'failed' || status.status === 'expired') {
|
|
233
|
+
throw new Error(`video_generate: generation ${status.status} (request ${requestId})`
|
|
234
|
+
+ (status.detail === undefined ? '' : `: ${status.detail}`));
|
|
235
|
+
}
|
|
236
|
+
if (Date.now() >= deadline) {
|
|
237
|
+
throw new Error(`video_generate: timed out after ${String(maxWaitMs)}ms waiting for request ${requestId}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// The MP4 lives on a separate signed host: no auth header on purpose.
|
|
241
|
+
const download = await fetchFn(done.url, { method: 'GET', signal: exec.signal });
|
|
242
|
+
if (!download.ok)
|
|
243
|
+
throw await httpLlmError(download, 'video_generate download');
|
|
244
|
+
const data = Buffer.from(await download.arrayBuffer());
|
|
245
|
+
const directory = options.videosDir ?? videosDirectory();
|
|
246
|
+
await mkdir(directory, { recursive: true });
|
|
247
|
+
const path = join(directory, videoFileName());
|
|
248
|
+
await writeFile(path, data);
|
|
249
|
+
const value = {
|
|
250
|
+
path,
|
|
251
|
+
url: done.url,
|
|
252
|
+
...done.duration === undefined ? {} : { duration: done.duration },
|
|
253
|
+
};
|
|
254
|
+
return value;
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `x_search` tool: run xAI's hosted X (Twitter) search through the grok
|
|
3
|
+
* subscription's OAuth session. The wire call is a non-streaming Responses
|
|
4
|
+
* request carrying the built-in `x_search` tool definition; the canonical
|
|
5
|
+
* output is `{ answer, citations }`.
|
|
6
|
+
*/
|
|
7
|
+
import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
|
|
8
|
+
import type { GrokSession } from '../auth/store.js';
|
|
9
|
+
import { AccountTokenManager } from '../providers/accounts.js';
|
|
10
|
+
import type { FetchFn } from '../providers/common.js';
|
|
11
|
+
/** Endpoint the search request is posted to. */
|
|
12
|
+
export declare const X_SEARCH_URL = "https://api.x.ai/v1/responses";
|
|
13
|
+
/** Grok model the search runs on (a catalog model of the grok provider). */
|
|
14
|
+
export declare const X_SEARCH_MODEL = "grok-4";
|
|
15
|
+
/** Dependencies of the `x_search` tool. */
|
|
16
|
+
export interface XSearchToolOptions {
|
|
17
|
+
/** Grok session source; a missing session throws the log-in hint. */
|
|
18
|
+
tokens: AccountTokenManager<GrokSession>;
|
|
19
|
+
/** Fetch implementation (injectable for tests). */
|
|
20
|
+
fetchFn?: FetchFn;
|
|
21
|
+
}
|
|
22
|
+
/** Normalized, validated arguments of one search call. */
|
|
23
|
+
interface XSearchRequest {
|
|
24
|
+
query: string;
|
|
25
|
+
tool: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate and assemble the request facts from tool arguments. Throws plain
|
|
29
|
+
* Errors for argument problems the schema DSL cannot express (non-empty
|
|
30
|
+
* query, handle caps, mutually exclusive filters).
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildXSearchRequest(args: {
|
|
33
|
+
query: string;
|
|
34
|
+
allowed_x_handles?: string[];
|
|
35
|
+
excluded_x_handles?: string[];
|
|
36
|
+
from_date?: string;
|
|
37
|
+
to_date?: string;
|
|
38
|
+
enable_image_understanding?: boolean;
|
|
39
|
+
enable_video_understanding?: boolean;
|
|
40
|
+
}): XSearchRequest;
|
|
41
|
+
/** The canonical output of one successful search. */
|
|
42
|
+
interface XSearchOutput {
|
|
43
|
+
answer: string;
|
|
44
|
+
citations: string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Extract the answer text and citation URLs from a Responses payload: the
|
|
48
|
+
* `output_text` shortcut or message output parts for the answer, and both
|
|
49
|
+
* top-level `citations` and inline `url_citation` annotations for sources.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseXSearchResponse(payload: unknown): XSearchOutput;
|
|
52
|
+
/**
|
|
53
|
+
* Build the `x_search` tool definition.
|
|
54
|
+
* @param options - grok session source and fetch implementation.
|
|
55
|
+
* @returns the tool to register on `ctx.tools`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function createXSearchTool(options: XSearchToolOptions): ToolDefinition;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `x_search` tool: run xAI's hosted X (Twitter) search through the grok
|
|
3
|
+
* subscription's OAuth session. The wire call is a non-streaming Responses
|
|
4
|
+
* request carrying the built-in `x_search` tool definition; the canonical
|
|
5
|
+
* output is `{ answer, citations }`.
|
|
6
|
+
*/
|
|
7
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
|
+
import { httpLlmError } from '../providers/common.js';
|
|
9
|
+
import { AccountTokenManager } from '../providers/accounts.js';
|
|
10
|
+
import { hostFetch } from '../http.js';
|
|
11
|
+
/** Endpoint the search request is posted to. */
|
|
12
|
+
export const X_SEARCH_URL = 'https://api.x.ai/v1/responses';
|
|
13
|
+
/** Grok model the search runs on (a catalog model of the grok provider). */
|
|
14
|
+
export const X_SEARCH_MODEL = 'grok-4';
|
|
15
|
+
/** xAI caps each handle filter list at ten entries. */
|
|
16
|
+
const MAX_HANDLES = 10;
|
|
17
|
+
/**
|
|
18
|
+
* Validate and assemble the request facts from tool arguments. Throws plain
|
|
19
|
+
* Errors for argument problems the schema DSL cannot express (non-empty
|
|
20
|
+
* query, handle caps, mutually exclusive filters).
|
|
21
|
+
*/
|
|
22
|
+
export function buildXSearchRequest(args) {
|
|
23
|
+
const query = args.query.trim();
|
|
24
|
+
if (query.length === 0)
|
|
25
|
+
throw new Error('x_search: query must be a non-empty string');
|
|
26
|
+
const allowed = normalizeHandles(args.allowed_x_handles, 'allowed_x_handles');
|
|
27
|
+
const excluded = normalizeHandles(args.excluded_x_handles, 'excluded_x_handles');
|
|
28
|
+
if (allowed.length > 0 && excluded.length > 0) {
|
|
29
|
+
throw new Error('x_search: allowed_x_handles and excluded_x_handles cannot be used together');
|
|
30
|
+
}
|
|
31
|
+
const tool = { type: 'x_search' };
|
|
32
|
+
if (allowed.length > 0)
|
|
33
|
+
tool.allowed_x_handles = allowed;
|
|
34
|
+
if (excluded.length > 0)
|
|
35
|
+
tool.excluded_x_handles = excluded;
|
|
36
|
+
if (args.from_date !== undefined && args.from_date.trim().length > 0)
|
|
37
|
+
tool.from_date = args.from_date.trim();
|
|
38
|
+
if (args.to_date !== undefined && args.to_date.trim().length > 0)
|
|
39
|
+
tool.to_date = args.to_date.trim();
|
|
40
|
+
if (args.enable_image_understanding === true)
|
|
41
|
+
tool.enable_image_understanding = true;
|
|
42
|
+
if (args.enable_video_understanding === true)
|
|
43
|
+
tool.enable_video_understanding = true;
|
|
44
|
+
return { query, tool };
|
|
45
|
+
}
|
|
46
|
+
/** Strip `@` prefixes, drop blanks, and enforce the provider's handle cap. */
|
|
47
|
+
function normalizeHandles(value, field) {
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return [];
|
|
50
|
+
const handles = value.map(handle => handle.trim().replace(/^@+/, '')).filter(handle => handle.length > 0);
|
|
51
|
+
if (handles.length > MAX_HANDLES)
|
|
52
|
+
throw new Error(`x_search: ${field} supports at most ${MAX_HANDLES} handles`);
|
|
53
|
+
return handles;
|
|
54
|
+
}
|
|
55
|
+
function isRecord(value) {
|
|
56
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Extract the answer text and citation URLs from a Responses payload: the
|
|
60
|
+
* `output_text` shortcut or message output parts for the answer, and both
|
|
61
|
+
* top-level `citations` and inline `url_citation` annotations for sources.
|
|
62
|
+
*/
|
|
63
|
+
export function parseXSearchResponse(payload) {
|
|
64
|
+
const body = isRecord(payload) ? payload : {};
|
|
65
|
+
let answer = typeof body.output_text === 'string' ? body.output_text.trim() : '';
|
|
66
|
+
const citations = [];
|
|
67
|
+
const push = (url) => {
|
|
68
|
+
if (typeof url === 'string' && url.length > 0 && !citations.includes(url))
|
|
69
|
+
citations.push(url);
|
|
70
|
+
};
|
|
71
|
+
if (Array.isArray(body.citations)) {
|
|
72
|
+
for (const citation of body.citations)
|
|
73
|
+
push(citation);
|
|
74
|
+
}
|
|
75
|
+
const parts = [];
|
|
76
|
+
if (Array.isArray(body.output)) {
|
|
77
|
+
for (const item of body.output) {
|
|
78
|
+
if (!isRecord(item) || item.type !== 'message' || !Array.isArray(item.content))
|
|
79
|
+
continue;
|
|
80
|
+
for (const part of item.content) {
|
|
81
|
+
if (!isRecord(part))
|
|
82
|
+
continue;
|
|
83
|
+
if ((part.type === 'output_text' || part.type === 'text')
|
|
84
|
+
&& typeof part.text === 'string' && part.text.trim().length > 0) {
|
|
85
|
+
parts.push(part.text.trim());
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(part.annotations)) {
|
|
88
|
+
for (const annotation of part.annotations) {
|
|
89
|
+
if (isRecord(annotation) && annotation.type === 'url_citation')
|
|
90
|
+
push(annotation.url);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (answer.length === 0)
|
|
97
|
+
answer = parts.join('\n\n');
|
|
98
|
+
return { answer, citations };
|
|
99
|
+
}
|
|
100
|
+
/** Bound a call-card title's query. */
|
|
101
|
+
function truncate(text, max = 60) {
|
|
102
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build the `x_search` tool definition.
|
|
106
|
+
* @param options - grok session source and fetch implementation.
|
|
107
|
+
* @returns the tool to register on `ctx.tools`.
|
|
108
|
+
*/
|
|
109
|
+
export function createXSearchTool(options) {
|
|
110
|
+
return defineTool({
|
|
111
|
+
name: 'x_search',
|
|
112
|
+
description: "Search X (Twitter) posts, profiles, and threads using the grok subscription's hosted xAI x_search. "
|
|
113
|
+
+ 'Use this for current discussion, reactions, or claims on X rather than general web pages.',
|
|
114
|
+
parameters: {
|
|
115
|
+
query: { type: 'string', required: true, description: 'What to look up on X.' },
|
|
116
|
+
allowed_x_handles: {
|
|
117
|
+
type: 'array',
|
|
118
|
+
items: { type: 'string' },
|
|
119
|
+
description: 'X handles to include exclusively (max 10).',
|
|
120
|
+
},
|
|
121
|
+
excluded_x_handles: {
|
|
122
|
+
type: 'array',
|
|
123
|
+
items: { type: 'string' },
|
|
124
|
+
description: 'X handles to exclude (max 10).',
|
|
125
|
+
},
|
|
126
|
+
from_date: { type: 'string', description: 'Optional start date in YYYY-MM-DD format.' },
|
|
127
|
+
to_date: { type: 'string', description: 'Optional end date in YYYY-MM-DD format.' },
|
|
128
|
+
enable_image_understanding: {
|
|
129
|
+
type: 'boolean',
|
|
130
|
+
description: 'Whether xAI should analyze images attached to matching posts.',
|
|
131
|
+
},
|
|
132
|
+
enable_video_understanding: {
|
|
133
|
+
type: 'boolean',
|
|
134
|
+
description: 'Whether xAI should analyze videos attached to matching posts.',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
output: {
|
|
138
|
+
schema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
answer: { type: 'string', required: true },
|
|
142
|
+
citations: { type: 'array', items: { type: 'string' }, required: true },
|
|
143
|
+
},
|
|
144
|
+
additionalProperties: false,
|
|
145
|
+
},
|
|
146
|
+
render: (_args, value) => [{
|
|
147
|
+
type: 'text',
|
|
148
|
+
text: value.citations.length > 0
|
|
149
|
+
? `${value.answer}\n\nSources:\n${value.citations.map(citation => `- ${citation}`).join('\n')}`
|
|
150
|
+
: value.answer,
|
|
151
|
+
}],
|
|
152
|
+
presentationMeta: (_args, value) => ({ answer: value.answer, citations: value.citations }),
|
|
153
|
+
},
|
|
154
|
+
presentCall: args => ({
|
|
155
|
+
card: 'generic',
|
|
156
|
+
title: `x_search: ${truncate(args.query)}`,
|
|
157
|
+
kind: 'search',
|
|
158
|
+
}),
|
|
159
|
+
presentResult: (_args, result) => {
|
|
160
|
+
if (result.isError || !isRecord(result.meta))
|
|
161
|
+
return undefined;
|
|
162
|
+
const citations = Array.isArray(result.meta.citations) ? result.meta.citations : [];
|
|
163
|
+
return {
|
|
164
|
+
card: 'web',
|
|
165
|
+
kind: 'search',
|
|
166
|
+
sources: citations.filter((citation) => typeof citation === 'string')
|
|
167
|
+
.map(url => ({ url })),
|
|
168
|
+
...typeof result.meta.answer === 'string' && result.meta.answer.length > 0
|
|
169
|
+
? { answer: result.meta.answer }
|
|
170
|
+
: {},
|
|
171
|
+
truncated: false,
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
async execute(args, exec) {
|
|
175
|
+
const request = buildXSearchRequest(args);
|
|
176
|
+
const session = await options.tokens.session();
|
|
177
|
+
const response = await (options.fetchFn ?? hostFetch)(X_SEARCH_URL, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: {
|
|
180
|
+
'authorization': `Bearer ${session.accessToken}`,
|
|
181
|
+
'content-type': 'application/json',
|
|
182
|
+
'accept': 'application/json',
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
model: X_SEARCH_MODEL,
|
|
186
|
+
input: [{ role: 'user', content: request.query }],
|
|
187
|
+
tools: [request.tool],
|
|
188
|
+
store: false,
|
|
189
|
+
}),
|
|
190
|
+
signal: exec.signal,
|
|
191
|
+
});
|
|
192
|
+
if (!response.ok)
|
|
193
|
+
throw await httpLlmError(response, 'x_search');
|
|
194
|
+
return parseXSearchResponse(await response.json());
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate between the harness message vocabulary and the Anthropic Messages
|
|
3
|
+
* API wire format used by the claude provider: request message assembly, tool
|
|
4
|
+
* schema mapping, and a push-model SSE-event → StreamChunk state machine
|
|
5
|
+
* ({@link AnthropicStreamTranslator}) so tests need no streams.
|
|
6
|
+
*/
|
|
7
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
import type { StreamChunk, ToolSchema } from '@deepseek-ai/dsh-llm';
|
|
9
|
+
import type { TranslatableMessage } from './resolved.js';
|
|
10
|
+
/**
|
|
11
|
+
* The Claude Code identity block. The subscription endpoint rejects requests
|
|
12
|
+
* that do not present as Claude Code, so this block is REQUIRED as the first
|
|
13
|
+
* system entry on every request.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
16
|
+
/** Tags wrapping a mid-conversation system message where it sits in the history. */
|
|
17
|
+
export declare const SYSTEM_REMINDER_OPEN = "<system-reminder>";
|
|
18
|
+
export declare const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
|
|
19
|
+
/**
|
|
20
|
+
* How far apart consecutive message breakpoints sit, in content blocks.
|
|
21
|
+
*
|
|
22
|
+
* A breakpoint looks back at most 20 blocks for an entry an earlier request
|
|
23
|
+
* wrote, so marks must stay closer than that: one agentic turn can append a
|
|
24
|
+
* dozen tool_use/tool_result blocks at once, and a single trailing mark would
|
|
25
|
+
* silently fall out of range and rebuild the whole prefix.
|
|
26
|
+
*/
|
|
27
|
+
export declare const CACHE_BLOCK_STRIDE = 15;
|
|
28
|
+
/**
|
|
29
|
+
* Message breakpoints per request. Anthropic allows four in total and the
|
|
30
|
+
* last `system` block takes the fourth, so three are left for the history —
|
|
31
|
+
* enough to tolerate a turn appending roughly {@link CACHE_BLOCK_STRIDE} × 3
|
|
32
|
+
* blocks before a read is lost.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MESSAGE_CACHE_BREAKPOINTS = 3;
|
|
35
|
+
/** One Anthropic request message. */
|
|
36
|
+
export interface AnthropicMessage {
|
|
37
|
+
role: 'user' | 'assistant';
|
|
38
|
+
content: Record<string, unknown>[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Convert harness messages into Anthropic messages. Consecutive same-role
|
|
42
|
+
* messages merge into one message with multiple content blocks; tool results
|
|
43
|
+
* arrive as user messages with `tool_result` blocks, which a merged user
|
|
44
|
+
* message keeps in one leading run ({@link leadWithToolResults}); system-role
|
|
45
|
+
* messages before the conversation starts are handled by
|
|
46
|
+
* {@link toAnthropicSystem} and skipped here, while a later one rides in
|
|
47
|
+
* place as a user-role `<system-reminder>` block.
|
|
48
|
+
* Reasoning blocks are not replayed (v1). Images must arrive pre-resolved
|
|
49
|
+
* ({@link TranslatableMessage}); an unresolved ImageBlock is skipped because
|
|
50
|
+
* its bytes are unreachable here.
|
|
51
|
+
* @param messages - ordered conversation messages with resolved images.
|
|
52
|
+
* @returns Anthropic messages in conversation order.
|
|
53
|
+
*/
|
|
54
|
+
export declare function toAnthropicMessages(messages: readonly TranslatableMessage[]): AnthropicMessage[];
|
|
55
|
+
/**
|
|
56
|
+
* Mark the conversation's cache breakpoints in place: the last content block,
|
|
57
|
+
* then one every {@link CACHE_BLOCK_STRIDE} blocks backwards, {@link
|
|
58
|
+
* MESSAGE_CACHE_BREAKPOINTS} in total.
|
|
59
|
+
*
|
|
60
|
+
* The history is append-only, so the block one request marks last is
|
|
61
|
+
* byte-identical in the next — that entry is what the next request reads.
|
|
62
|
+
* Marks are counted across the flattened block sequence, not per message,
|
|
63
|
+
* because the lookback window Anthropic walks counts blocks the same way.
|
|
64
|
+
* @param messages - assembled Anthropic messages, marked in place.
|
|
65
|
+
*/
|
|
66
|
+
export declare function markMessageCache(messages: readonly AnthropicMessage[]): void;
|
|
67
|
+
/**
|
|
68
|
+
* Build the Anthropic `system` array: the mandatory Claude Code identity
|
|
69
|
+
* block, then the explicit system prompt, then any system-role messages.
|
|
70
|
+
* @param system - explicit system prompt, when set.
|
|
71
|
+
* @param messages - conversation messages; the system-role text preceding the
|
|
72
|
+
* conversation is appended, and a later one is left to {@link toAnthropicMessages}.
|
|
73
|
+
* @returns the system content blocks.
|
|
74
|
+
*/
|
|
75
|
+
export declare function toAnthropicSystem(system?: string, messages?: readonly TranslatableMessage[]): Record<string, unknown>[];
|
|
76
|
+
/**
|
|
77
|
+
* Map harness tool schemas to Anthropic tools, in name order.
|
|
78
|
+
*
|
|
79
|
+
* `tools` renders at position 0 of the cached prefix, so any reordering
|
|
80
|
+
* invalidates every cache entry behind it — `system` and the whole
|
|
81
|
+
* conversation included. Registration order belongs to the caller and plugin
|
|
82
|
+
* load order can differ between processes, so the wire order is fixed here
|
|
83
|
+
* instead. Anthropic selects a tool by name; the array order carries nothing.
|
|
84
|
+
* @param tools - tool schemas from the request.
|
|
85
|
+
* @returns Anthropic `tools` array entries, ordered by tool name.
|
|
86
|
+
*/
|
|
87
|
+
export declare function toAnthropicTools(tools: readonly ToolSchema[]): Record<string, unknown>[];
|
|
88
|
+
/** The subset of Anthropic SSE event shapes this translator reads. */
|
|
89
|
+
export interface AnthropicStreamEvent {
|
|
90
|
+
type: string;
|
|
91
|
+
index?: number;
|
|
92
|
+
message?: {
|
|
93
|
+
usage?: {
|
|
94
|
+
input_tokens?: number;
|
|
95
|
+
output_tokens?: number;
|
|
96
|
+
cache_read_input_tokens?: number;
|
|
97
|
+
cache_creation_input_tokens?: number;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
content_block?: {
|
|
101
|
+
type?: string;
|
|
102
|
+
id?: string;
|
|
103
|
+
name?: string;
|
|
104
|
+
};
|
|
105
|
+
delta?: {
|
|
106
|
+
type?: string;
|
|
107
|
+
text?: string;
|
|
108
|
+
thinking?: string;
|
|
109
|
+
partial_json?: string;
|
|
110
|
+
stop_reason?: string;
|
|
111
|
+
};
|
|
112
|
+
usage?: {
|
|
113
|
+
output_tokens?: number;
|
|
114
|
+
};
|
|
115
|
+
error?: {
|
|
116
|
+
type?: string;
|
|
117
|
+
message?: string;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Classify an Anthropic `error` event into a thrown LlmError.
|
|
122
|
+
* @param error - the wire error object.
|
|
123
|
+
* @returns the mapped error.
|
|
124
|
+
*/
|
|
125
|
+
export declare function anthropicFailure(error: {
|
|
126
|
+
type?: string;
|
|
127
|
+
message?: string;
|
|
128
|
+
} | undefined): LlmError;
|
|
129
|
+
/**
|
|
130
|
+
* Push-model Anthropic SSE translator: feed each parsed event object to
|
|
131
|
+
* {@link push} and collect the emitted harness StreamChunks. Block indexes
|
|
132
|
+
* are allocated in first-seen order; `usage` is emitted before the terminal
|
|
133
|
+
* `finish`, and nothing is emitted after it. `error` events throw
|
|
134
|
+
* {@link LlmError}.
|
|
135
|
+
*/
|
|
136
|
+
export declare class AnthropicStreamTranslator {
|
|
137
|
+
private blocks;
|
|
138
|
+
private nextIndex;
|
|
139
|
+
private sawAnyBlock;
|
|
140
|
+
private pendingUsage;
|
|
141
|
+
private outputTokens;
|
|
142
|
+
private stopReason;
|
|
143
|
+
private usageEmitted;
|
|
144
|
+
/** Set once `message_stop` produced the terminal finish chunk. */
|
|
145
|
+
terminated: boolean;
|
|
146
|
+
private open;
|
|
147
|
+
private emitUsage;
|
|
148
|
+
/**
|
|
149
|
+
* Process one parsed Anthropic SSE event.
|
|
150
|
+
* @param event - the parsed event object.
|
|
151
|
+
* @returns the StreamChunks this event produced (possibly none).
|
|
152
|
+
*/
|
|
153
|
+
push(event: AnthropicStreamEvent): StreamChunk[];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Consume an Anthropic SSE byte stream and yield harness StreamChunks.
|
|
157
|
+
* @param stream - raw response body.
|
|
158
|
+
* @param onActivity - transport-activity callback for the idle watchdog.
|
|
159
|
+
* @returns the chunk stream; throws when the stream ends before `message_stop`.
|
|
160
|
+
*/
|
|
161
|
+
export declare function streamAnthropic(stream: ReadableStream<Uint8Array>, onActivity?: () => void): AsyncGenerator<StreamChunk>;
|