dsh-plugin-subscription-usage 0.0.0-stage → 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.
Files changed (167) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +38 -2
  3. package/cordis.patch.yml +5 -0
  4. package/icon.svg +9 -0
  5. package/lib/auth/claude-code-creds.d.ts +23 -0
  6. package/lib/auth/claude-code-creds.js +172 -0
  7. package/lib/auth/device-flow.d.ts +55 -0
  8. package/lib/auth/device-flow.js +177 -0
  9. package/lib/auth/jwt.d.ts +10 -0
  10. package/lib/auth/jwt.js +25 -0
  11. package/lib/auth/oauth-flow.d.ts +91 -0
  12. package/lib/auth/oauth-flow.js +227 -0
  13. package/lib/auth/pkce.d.ts +31 -0
  14. package/lib/auth/pkce.js +35 -0
  15. package/lib/auth/rpc.d.ts +233 -0
  16. package/lib/auth/rpc.js +501 -0
  17. package/lib/auth/store.d.ts +198 -0
  18. package/lib/auth/store.js +473 -0
  19. package/lib/client/Footer.d.ts +3 -0
  20. package/lib/client/Footer.js +73 -0
  21. package/lib/client/Gauge.d.ts +28 -0
  22. package/lib/client/Gauge.js +139 -0
  23. package/lib/client/ImageGallery.d.ts +54 -0
  24. package/lib/client/ImageGallery.js +112 -0
  25. package/lib/client/ImageGenerateToolview.d.ts +48 -0
  26. package/lib/client/ImageGenerateToolview.js +130 -0
  27. package/lib/client/Notice.d.ts +15 -0
  28. package/lib/client/Notice.js +93 -0
  29. package/lib/client/ProviderCard.d.ts +35 -0
  30. package/lib/client/ProviderCard.js +240 -0
  31. package/lib/client/SpeedSelect.d.ts +72 -0
  32. package/lib/client/SpeedSelect.js +214 -0
  33. package/lib/client/Usage.d.ts +7 -0
  34. package/lib/client/Usage.js +83 -0
  35. package/lib/client/VideoGenerateToolview.d.ts +42 -0
  36. package/lib/client/VideoGenerateToolview.js +172 -0
  37. package/lib/client/context.d.ts +16 -0
  38. package/lib/client/context.js +11 -0
  39. package/lib/client/fast-command.d.ts +2 -0
  40. package/lib/client/fast-command.js +4 -0
  41. package/lib/client/hooks.d.ts +2 -0
  42. package/lib/client/hooks.js +10 -0
  43. package/lib/client/index.d.ts +3 -0
  44. package/lib/client/index.js +113 -0
  45. package/lib/client/locales.d.ts +139 -0
  46. package/lib/client/locales.js +267 -0
  47. package/lib/client/meter.d.ts +91 -0
  48. package/lib/client/meter.js +196 -0
  49. package/lib/client/prefs.d.ts +51 -0
  50. package/lib/client/prefs.js +71 -0
  51. package/lib/client/rpc.d.ts +161 -0
  52. package/lib/client/rpc.js +31 -0
  53. package/lib/client/store.d.ts +67 -0
  54. package/lib/client/store.js +129 -0
  55. package/lib/client/styles.d.ts +7 -0
  56. package/lib/client/styles.js +176 -0
  57. package/lib/client.js +3933 -0
  58. package/lib/client.js.map +1 -0
  59. package/lib/compat.d.ts +55 -0
  60. package/lib/compat.js +20 -0
  61. package/lib/http.d.ts +134 -0
  62. package/lib/http.js +431 -0
  63. package/lib/index.d.ts +121 -0
  64. package/lib/index.js +13210 -0
  65. package/lib/json-text.d.ts +2 -0
  66. package/lib/json-text.js +4 -0
  67. package/lib/model-defaults.d.ts +23 -0
  68. package/lib/model-defaults.js +238 -0
  69. package/lib/model-speeds.d.ts +10 -0
  70. package/lib/model-speeds.js +48 -0
  71. package/lib/models-directory.d.ts +17 -0
  72. package/lib/models-directory.js +25 -0
  73. package/lib/provider-settings.d.ts +55 -0
  74. package/lib/provider-settings.js +215 -0
  75. package/lib/providers/account-preferences.d.ts +46 -0
  76. package/lib/providers/account-preferences.js +188 -0
  77. package/lib/providers/accounts.d.ts +113 -0
  78. package/lib/providers/accounts.js +142 -0
  79. package/lib/providers/antigravity-oauth-client.d.ts +7 -0
  80. package/lib/providers/antigravity-oauth-client.js +7 -0
  81. package/lib/providers/antigravity.d.ts +119 -0
  82. package/lib/providers/antigravity.js +552 -0
  83. package/lib/providers/catalog-refresh.d.ts +4 -0
  84. package/lib/providers/catalog-refresh.js +30 -0
  85. package/lib/providers/catalog-store.d.ts +47 -0
  86. package/lib/providers/catalog-store.js +283 -0
  87. package/lib/providers/claude-cli-version.d.ts +15 -0
  88. package/lib/providers/claude-cli-version.js +24 -0
  89. package/lib/providers/claude.d.ts +212 -0
  90. package/lib/providers/claude.js +726 -0
  91. package/lib/providers/codex-client-version.d.ts +8 -0
  92. package/lib/providers/codex-client-version.js +17 -0
  93. package/lib/providers/codex-search.d.ts +29 -0
  94. package/lib/providers/codex-search.js +198 -0
  95. package/lib/providers/codex.d.ts +253 -0
  96. package/lib/providers/codex.js +930 -0
  97. package/lib/providers/common.d.ts +415 -0
  98. package/lib/providers/common.js +679 -0
  99. package/lib/providers/copilot.d.ts +365 -0
  100. package/lib/providers/copilot.js +956 -0
  101. package/lib/providers/grok.d.ts +214 -0
  102. package/lib/providers/grok.js +750 -0
  103. package/lib/providers/image-pool.d.ts +30 -0
  104. package/lib/providers/image-pool.js +89 -0
  105. package/lib/providers/npm-cli-version.d.ts +56 -0
  106. package/lib/providers/npm-cli-version.js +98 -0
  107. package/lib/providers/pool-family.d.ts +56 -0
  108. package/lib/providers/pool-family.js +45 -0
  109. package/lib/providers/pool-health.d.ts +91 -0
  110. package/lib/providers/pool-health.js +164 -0
  111. package/lib/providers/pool-usage.d.ts +106 -0
  112. package/lib/providers/pool-usage.js +302 -0
  113. package/lib/providers/pool.d.ts +115 -0
  114. package/lib/providers/pool.js +461 -0
  115. package/lib/providers/rate-limit.d.ts +192 -0
  116. package/lib/providers/rate-limit.js +338 -0
  117. package/lib/providers/replay.d.ts +4 -0
  118. package/lib/providers/replay.js +37 -0
  119. package/lib/providers/reset-redemption.d.ts +20 -0
  120. package/lib/providers/reset-redemption.js +70 -0
  121. package/lib/tools/image-generate.d.ts +128 -0
  122. package/lib/tools/image-generate.js +438 -0
  123. package/lib/tools/registration.d.ts +15 -0
  124. package/lib/tools/registration.js +24 -0
  125. package/lib/tools/video-generate.d.ts +89 -0
  126. package/lib/tools/video-generate.js +257 -0
  127. package/lib/tools/x-search.d.ts +58 -0
  128. package/lib/tools/x-search.js +197 -0
  129. package/lib/translate/anthropic.d.ts +180 -0
  130. package/lib/translate/anthropic.js +534 -0
  131. package/lib/translate/antigravity-images.d.ts +11 -0
  132. package/lib/translate/antigravity-images.js +62 -0
  133. package/lib/translate/antigravity-schema.d.ts +4 -0
  134. package/lib/translate/antigravity-schema.js +145 -0
  135. package/lib/translate/antigravity-thinking.d.ts +5 -0
  136. package/lib/translate/antigravity-thinking.js +44 -0
  137. package/lib/translate/antigravity.d.ts +120 -0
  138. package/lib/translate/antigravity.js +412 -0
  139. package/lib/translate/chat-completions.d.ts +120 -0
  140. package/lib/translate/chat-completions.js +382 -0
  141. package/lib/translate/resolved.d.ts +98 -0
  142. package/lib/translate/resolved.js +186 -0
  143. package/lib/translate/responses.d.ts +192 -0
  144. package/lib/translate/responses.js +443 -0
  145. package/lib/translate/sse.d.ts +21 -0
  146. package/lib/translate/sse.js +62 -0
  147. package/lib/usage/headers.d.ts +21 -0
  148. package/lib/usage/headers.js +64 -0
  149. package/lib/usage/pace.d.ts +53 -0
  150. package/lib/usage/pace.js +48 -0
  151. package/lib/usage/service.d.ts +64 -0
  152. package/lib/usage/service.js +104 -0
  153. package/lib/usage-providers/common.d.ts +23 -0
  154. package/lib/usage-providers/common.js +28 -0
  155. package/lib/usage-providers/index.d.ts +42 -0
  156. package/lib/usage-providers/index.js +71 -0
  157. package/lib/usage-providers/keys.d.ts +11 -0
  158. package/lib/usage-providers/keys.js +52 -0
  159. package/lib/usage-providers/kimi.d.ts +4 -0
  160. package/lib/usage-providers/kimi.js +52 -0
  161. package/lib/usage-providers/ollama-cloud.d.ts +4 -0
  162. package/lib/usage-providers/ollama-cloud.js +42 -0
  163. package/lib/usage-providers/zai.d.ts +5 -0
  164. package/lib/usage-providers/zai.js +53 -0
  165. package/locale/en.json +6 -0
  166. package/locale/zh.json +6 -0
  167. package/package.json +115 -4
@@ -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 { proxiedFetch } 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 ?? proxiedFetch;
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 { proxiedFetch } 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 ?? proxiedFetch)(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
+ }