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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/auth/claude-code-creds.d.ts +23 -0
  5. package/lib/auth/claude-code-creds.js +172 -0
  6. package/lib/auth/device-flow.d.ts +55 -0
  7. package/lib/auth/device-flow.js +177 -0
  8. package/lib/auth/jwt.d.ts +10 -0
  9. package/lib/auth/jwt.js +25 -0
  10. package/lib/auth/oauth-flow.d.ts +91 -0
  11. package/lib/auth/oauth-flow.js +227 -0
  12. package/lib/auth/pkce.d.ts +31 -0
  13. package/lib/auth/pkce.js +35 -0
  14. package/lib/auth/rpc.d.ts +209 -0
  15. package/lib/auth/rpc.js +386 -0
  16. package/lib/auth/store.d.ts +196 -0
  17. package/lib/auth/store.js +468 -0
  18. package/lib/client/CursorCard.d.ts +9 -0
  19. package/lib/client/CursorCard.js +113 -0
  20. package/lib/client/ExternalUsageCards.d.ts +9 -0
  21. package/lib/client/ExternalUsageCards.js +72 -0
  22. package/lib/client/ImageGallery.d.ts +54 -0
  23. package/lib/client/ImageGallery.js +112 -0
  24. package/lib/client/ImageGenerateToolview.d.ts +48 -0
  25. package/lib/client/ImageGenerateToolview.js +133 -0
  26. package/lib/client/ProviderAccountManager.d.ts +13 -0
  27. package/lib/client/ProviderAccountManager.js +147 -0
  28. package/lib/client/ProviderModelEditor.d.ts +39 -0
  29. package/lib/client/ProviderModelEditor.js +107 -0
  30. package/lib/client/SpeedSelect.d.ts +71 -0
  31. package/lib/client/SpeedSelect.js +177 -0
  32. package/lib/client/SubscriptionUsageBadge.d.ts +116 -0
  33. package/lib/client/SubscriptionUsageBadge.js +543 -0
  34. package/lib/client/SubscriptionsSection.d.ts +140 -0
  35. package/lib/client/SubscriptionsSection.js +544 -0
  36. package/lib/client/UsageBadgeDisplaySetting.d.ts +7 -0
  37. package/lib/client/UsageBadgeDisplaySetting.js +36 -0
  38. package/lib/client/VideoGenerateToolview.d.ts +42 -0
  39. package/lib/client/VideoGenerateToolview.js +175 -0
  40. package/lib/client/account-preferences.d.ts +24 -0
  41. package/lib/client/account-preferences.js +21 -0
  42. package/lib/client/fast-command.d.ts +2 -0
  43. package/lib/client/fast-command.js +4 -0
  44. package/lib/client/host-contract.d.ts +90 -0
  45. package/lib/client/host-contract.js +101 -0
  46. package/lib/client/host-icons.d.ts +24 -0
  47. package/lib/client/host-icons.js +21 -0
  48. package/lib/client/index.d.ts +27 -0
  49. package/lib/client/index.js +136 -0
  50. package/lib/client/locales.d.ts +297 -0
  51. package/lib/client/locales.js +295 -0
  52. package/lib/client/subscription-card-styles.d.ts +3 -0
  53. package/lib/client/subscription-card-styles.js +53 -0
  54. package/lib/client/subscriptions-rpc.d.ts +14 -0
  55. package/lib/client/subscriptions-rpc.js +34 -0
  56. package/lib/client/usage-badge-preferences.d.ts +11 -0
  57. package/lib/client/usage-badge-preferences.js +41 -0
  58. package/lib/client.js +4472 -0
  59. package/lib/client.js.map +1 -0
  60. package/lib/compat.d.ts +36 -0
  61. package/lib/compat.js +20 -0
  62. package/lib/http.d.ts +6 -0
  63. package/lib/http.js +15 -0
  64. package/lib/index.d.ts +80 -0
  65. package/lib/index.js +14018 -0
  66. package/lib/model-defaults.d.ts +23 -0
  67. package/lib/model-defaults.js +237 -0
  68. package/lib/provider-settings.d.ts +39 -0
  69. package/lib/provider-settings.js +151 -0
  70. package/lib/providers/account-preferences.d.ts +41 -0
  71. package/lib/providers/account-preferences.js +153 -0
  72. package/lib/providers/accounts.d.ts +112 -0
  73. package/lib/providers/accounts.js +142 -0
  74. package/lib/providers/antigravity-oauth-client.d.ts +7 -0
  75. package/lib/providers/antigravity-oauth-client.js +7 -0
  76. package/lib/providers/antigravity.d.ts +110 -0
  77. package/lib/providers/antigravity.js +516 -0
  78. package/lib/providers/catalog-store.d.ts +48 -0
  79. package/lib/providers/catalog-store.js +243 -0
  80. package/lib/providers/claude-cli-version.d.ts +15 -0
  81. package/lib/providers/claude-cli-version.js +24 -0
  82. package/lib/providers/claude.d.ts +181 -0
  83. package/lib/providers/claude.js +680 -0
  84. package/lib/providers/codex-client-version.d.ts +8 -0
  85. package/lib/providers/codex-client-version.js +17 -0
  86. package/lib/providers/codex-search.d.ts +29 -0
  87. package/lib/providers/codex-search.js +198 -0
  88. package/lib/providers/codex.d.ts +229 -0
  89. package/lib/providers/codex.js +821 -0
  90. package/lib/providers/common.d.ts +389 -0
  91. package/lib/providers/common.js +656 -0
  92. package/lib/providers/copilot.d.ts +344 -0
  93. package/lib/providers/copilot.js +888 -0
  94. package/lib/providers/cursor-adapter.d.ts +15 -0
  95. package/lib/providers/cursor-adapter.js +39 -0
  96. package/lib/providers/cursor-auth.d.ts +43 -0
  97. package/lib/providers/cursor-auth.js +169 -0
  98. package/lib/providers/cursor-model-settings.d.ts +15 -0
  99. package/lib/providers/cursor-model-settings.js +51 -0
  100. package/lib/providers/cursor-usage.d.ts +9 -0
  101. package/lib/providers/cursor-usage.js +112 -0
  102. package/lib/providers/external-usage-controller.d.ts +19 -0
  103. package/lib/providers/external-usage-controller.js +32 -0
  104. package/lib/providers/external-usage.d.ts +8 -0
  105. package/lib/providers/external-usage.js +82 -0
  106. package/lib/providers/grok.d.ts +205 -0
  107. package/lib/providers/grok.js +713 -0
  108. package/lib/providers/image-pool.d.ts +30 -0
  109. package/lib/providers/image-pool.js +89 -0
  110. package/lib/providers/npm-cli-version.d.ts +56 -0
  111. package/lib/providers/npm-cli-version.js +98 -0
  112. package/lib/providers/pool-family.d.ts +56 -0
  113. package/lib/providers/pool-family.js +45 -0
  114. package/lib/providers/pool-health.d.ts +74 -0
  115. package/lib/providers/pool-health.js +150 -0
  116. package/lib/providers/pool-usage.d.ts +86 -0
  117. package/lib/providers/pool-usage.js +223 -0
  118. package/lib/providers/pool.d.ts +108 -0
  119. package/lib/providers/pool.js +368 -0
  120. package/lib/providers/rate-limit.d.ts +192 -0
  121. package/lib/providers/rate-limit.js +338 -0
  122. package/lib/tools/image-generate.d.ts +128 -0
  123. package/lib/tools/image-generate.js +438 -0
  124. package/lib/tools/registration.d.ts +15 -0
  125. package/lib/tools/registration.js +24 -0
  126. package/lib/tools/video-generate.d.ts +89 -0
  127. package/lib/tools/video-generate.js +257 -0
  128. package/lib/tools/x-search.d.ts +58 -0
  129. package/lib/tools/x-search.js +197 -0
  130. package/lib/translate/anthropic.d.ts +161 -0
  131. package/lib/translate/anthropic.js +515 -0
  132. package/lib/translate/antigravity-schema.d.ts +4 -0
  133. package/lib/translate/antigravity-schema.js +145 -0
  134. package/lib/translate/antigravity-thinking.d.ts +5 -0
  135. package/lib/translate/antigravity-thinking.js +44 -0
  136. package/lib/translate/antigravity.d.ts +114 -0
  137. package/lib/translate/antigravity.js +366 -0
  138. package/lib/translate/chat-completions.d.ts +120 -0
  139. package/lib/translate/chat-completions.js +382 -0
  140. package/lib/translate/resolved.d.ts +80 -0
  141. package/lib/translate/resolved.js +118 -0
  142. package/lib/translate/responses.d.ts +179 -0
  143. package/lib/translate/responses.js +408 -0
  144. package/lib/translate/sse.d.ts +21 -0
  145. package/lib/translate/sse.js +56 -0
  146. package/package.json +114 -0
  147. package/vendor/cursor/LICENSE +21 -0
  148. package/vendor/cursor/index.d.ts +18 -0
  149. package/vendor/cursor/index.js +3574 -0
  150. package/vendor/cursor/proto.js +254 -0
@@ -0,0 +1,438 @@
1
+ /**
2
+ * `image_generate` tool: generate images through a subscription image
3
+ * endpoint, save them under the harness home, and — when the deployment
4
+ * mounts an attachment store and the calling route declares image input —
5
+ * also commit the bytes as durable attachments so the images render inline
6
+ * and enter model context (the same path `read_image` uses).
7
+ *
8
+ * Provider selection: the `provider` argument names the preferred provider
9
+ * (default `gpt`, i.e. the ChatGPT/Codex subscription serving gpt-image-2 —
10
+ * mirrors codex-rs `codex-api/src/images.rs`); when the preferred one is
11
+ * logged out the other serves as fallback (`grok` is grok-imagine-image-2.0
12
+ * via `api.x.ai/v1/images/generations` with `response_format: 'b64_json'`).
13
+ * Both answer the OpenAI images shape (`data[].b64_json`).
14
+ */
15
+ import { mkdir, writeFile } from 'node:fs/promises';
16
+ import { basename, join } from 'node:path';
17
+ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
18
+ import { AttachmentId } from '@deepseek-ai/dsh-attachment';
19
+ import { defineTool } from '@deepseek-ai/dsh-tools';
20
+ import { AccountTokenManager } from '../providers/accounts.js';
21
+ import { ImageAccountPool } from '../providers/image-pool.js';
22
+ import { codexRateLimitReset } from '../providers/codex.js';
23
+ import { grokRateLimitReset } from '../providers/grok.js';
24
+ import { hostFetch } from '../http.js';
25
+ /** Endpoint the codex generation request is posted to. */
26
+ export const IMAGE_GENERATE_URL = 'https://chatgpt.com/backend-api/codex/images/generations';
27
+ export const IMAGE_EDIT_URL = 'https://chatgpt.com/backend-api/codex/images/edits';
28
+ /** The image model the codex subscription endpoint serves. */
29
+ export const IMAGE_GENERATE_MODEL = 'gpt-image-2';
30
+ /** Endpoint the grok generation request is posted to. */
31
+ export const GROK_IMAGE_GENERATE_URL = 'https://api.x.ai/v1/images/generations';
32
+ export const GROK_IMAGE_EDIT_URL = 'https://api.x.ai/v1/images/edits';
33
+ /** The image model the grok subscription endpoint serves. */
34
+ export const GROK_IMAGE_GENERATE_MODEL = 'grok-imagine-image-2.0';
35
+ /** Resolve references before any upstream request; never silently generate on invalid edits. */
36
+ async function resolveReferenceImages(refs, attachments, signal) {
37
+ if (refs === undefined)
38
+ return undefined;
39
+ if (!Array.isArray(refs) || refs.length < 1 || refs.length > 5) {
40
+ throw new Error('image_generate: referenceImages must contain 1–5 complete image references; omit only for a new image');
41
+ }
42
+ if (attachments === undefined)
43
+ throw new Error('image_generate: editing requires the DSH attachment service');
44
+ const seen = new Set();
45
+ let totalBytes = 0;
46
+ const urls = [];
47
+ for (const ref of refs) {
48
+ signal.throwIfAborted();
49
+ if (ref === null || typeof ref !== 'object'
50
+ || typeof ref.attachmentId !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(ref.attachmentId)
51
+ || !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(ref.mediaType)
52
+ || ![ref.bytes, ref.width, ref.height].every(value => Number.isSafeInteger(value) && value > 0)) {
53
+ throw new Error('image_generate: invalid referenceImages entry; copy a complete image reference or call read_image for a local file. Do not omit references to retry an edit.');
54
+ }
55
+ if (seen.has(ref.attachmentId))
56
+ throw new Error('image_generate: referenceImages contains duplicate images');
57
+ seen.add(ref.attachmentId);
58
+ if (refs.length > attachments.imageLimits.maxImagesPerMessage
59
+ || ref.bytes > attachments.imageLimits.maxImageBytes
60
+ || totalBytes + ref.bytes > attachments.imageLimits.maxMessageImageBytes) {
61
+ throw new Error('image_generate: referenceImages exceed DSH image limits');
62
+ }
63
+ const stored = await attachments.readImage(imageRefFromValue(ref), signal);
64
+ totalBytes += stored.data.byteLength;
65
+ if (totalBytes > attachments.imageLimits.maxMessageImageBytes) {
66
+ throw new Error('image_generate: referenceImages exceed DSH image limits');
67
+ }
68
+ urls.push(`data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString('base64')}`);
69
+ }
70
+ return urls;
71
+ }
72
+ /**
73
+ * Assemble the codex request body from tool arguments (hand-checks the
74
+ * non-empty prompt the schema DSL cannot express).
75
+ */
76
+ export function buildImageGenerateBody(args) {
77
+ const prompt = args.prompt.trim();
78
+ if (prompt.length === 0)
79
+ throw new Error('image_generate: prompt must be a non-empty string');
80
+ return {
81
+ prompt,
82
+ model: IMAGE_GENERATE_MODEL,
83
+ ...args.size === undefined ? {} : { size: args.size },
84
+ ...args.quality === undefined ? {} : { quality: args.quality },
85
+ };
86
+ }
87
+ /** The codex `size` values mapped onto grok aspect ratios. */
88
+ const GROK_ASPECT_RATIOS = {
89
+ '1024x1024': '1:1',
90
+ '1024x1536': '2:3',
91
+ '1536x1024': '3:2',
92
+ 'auto': 'auto',
93
+ };
94
+ /**
95
+ * Assemble the grok request body from the same tool arguments: `size` maps
96
+ * onto the nearest `aspect_ratio`, and `quality` folds into grok's low/medium
97
+ * pair (`high` → `medium`, `auto` → provider default).
98
+ */
99
+ export function buildGrokImageGenerateBody(args) {
100
+ const prompt = args.prompt.trim();
101
+ if (prompt.length === 0)
102
+ throw new Error('image_generate: prompt must be a non-empty string');
103
+ const quality = args.quality === 'low' ? 'low'
104
+ : args.quality === 'medium' || args.quality === 'high' ? 'medium'
105
+ : undefined;
106
+ return {
107
+ prompt,
108
+ model: GROK_IMAGE_GENERATE_MODEL,
109
+ response_format: 'b64_json',
110
+ ...args.size === undefined ? {} : { aspect_ratio: GROK_ASPECT_RATIOS[args.size] },
111
+ ...quality === undefined ? {} : { quality },
112
+ };
113
+ }
114
+ /**
115
+ * Parse the generations response into decodable images. Throws when the
116
+ * payload carries no usable `b64_json` entries.
117
+ */
118
+ export function parseImageGenerateResponse(payload) {
119
+ const body = typeof payload === 'object' && payload !== null ? payload : {};
120
+ const entries = Array.isArray(body.data) ? body.data : [];
121
+ const images = [];
122
+ for (const entry of entries) {
123
+ if (typeof entry !== 'object' || entry === null)
124
+ continue;
125
+ const record = entry;
126
+ if (typeof record.b64_json !== 'string' || record.b64_json.length === 0)
127
+ continue;
128
+ images.push({
129
+ data: Buffer.from(record.b64_json, 'base64'),
130
+ ...typeof record.revised_prompt === 'string' && record.revised_prompt.length > 0
131
+ ? { revisedPrompt: record.revised_prompt }
132
+ : {},
133
+ });
134
+ }
135
+ if (images.length === 0)
136
+ throw new Error('image_generate: the response carried no image data');
137
+ return images;
138
+ }
139
+ /** Directory the generated image files are written to. */
140
+ export function imagesDirectory() {
141
+ return dshHomePath('plugins', 'subscriptions', 'images');
142
+ }
143
+ /**
144
+ * Sniff a generated image's media type from its magic bytes (codex serves
145
+ * PNG; grok's format is undocumented, so trust the bytes). Unrecognized data
146
+ * defaults to PNG, matching the historical behavior.
147
+ */
148
+ export function sniffImageMediaType(data) {
149
+ if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff)
150
+ return 'image/jpeg';
151
+ if (data.length >= 12 && data.toString('latin1', 0, 4) === 'RIFF' && data.toString('latin1', 8, 12) === 'WEBP') {
152
+ return 'image/webp';
153
+ }
154
+ return 'image/png';
155
+ }
156
+ /** File extension for one sniffed media type. */
157
+ const MEDIA_TYPE_EXTENSIONS = {
158
+ 'image/png': 'png',
159
+ 'image/jpeg': 'jpg',
160
+ 'image/webp': 'webp',
161
+ };
162
+ /** Timestamped, collision-safe file name for one generated image. */
163
+ function imageFileName(index, mediaType) {
164
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
165
+ return `image-${stamp}-${Math.random().toString(36).slice(2, 8)}-${index}.${MEDIA_TYPE_EXTENSIONS[mediaType]}`;
166
+ }
167
+ /** Bound a call-card title's prompt. */
168
+ function truncate(text, max = 60) {
169
+ return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
170
+ }
171
+ /**
172
+ * Non-throwing image-capability check for the calling route (read_image's
173
+ * gate, softened: a generated image that cannot enter history degrades to the
174
+ * text-only result instead of failing the call). Resolves the session's
175
+ * latest routed provider/model and answers whether the exact route declares
176
+ * image input; any resolution failure means "no".
177
+ */
178
+ async function routeDeclaresImageInput(resolveLlm, exec) {
179
+ const llm = resolveLlm?.();
180
+ const routed = exec.agent?.session.requestHeader()?.config;
181
+ const provider = routed?.provider ?? exec.agent?.options.provider;
182
+ const model = routed?.model ?? exec.agent?.options.model;
183
+ if (llm === undefined || provider === undefined || model === undefined)
184
+ return false;
185
+ try {
186
+ const active = await llm.resolveModelInfo(provider, model, exec.signal);
187
+ return active.inputModalities?.includes('image') === true;
188
+ }
189
+ catch {
190
+ // An unresolvable route cannot be proven image-capable: degrade to text.
191
+ return false;
192
+ }
193
+ }
194
+ /** Re-brand one canonical image entry into the attachment reference an ImageBlock carries. */
195
+ function imageRefFromValue(image) {
196
+ return {
197
+ attachmentId: AttachmentId(image.attachmentId),
198
+ mediaType: image.mediaType,
199
+ bytes: image.bytes,
200
+ width: image.width,
201
+ height: image.height,
202
+ ...image.name === undefined ? {} : { name: image.name },
203
+ ...image.originalDimensions === undefined ? {} : { originalDimensions: image.originalDimensions },
204
+ };
205
+ }
206
+ /** Project the canonical value into the model-facing text + image blocks. */
207
+ function imageGenerateContent(value) {
208
+ return [
209
+ imageGenerateText(value),
210
+ ...(value.images ?? []).map(image => ({ type: 'image', attachment: imageRefFromValue(image) })),
211
+ ];
212
+ }
213
+ /** The text summary of one generation, shared by the model content and the UI card. */
214
+ function imageGenerateText(value) {
215
+ const text = `Saved ${value.paths.length} image(s):\n${value.paths.map(path => `- ${path}`).join('\n')}`
216
+ + (value.images?.length ? `\n\nImage references (for image_generate.referenceImages): ${JSON.stringify(value.images)}` : '')
217
+ + (value.revisedPrompt === undefined ? '' : `\n\nRevised prompt: ${value.revisedPrompt}`);
218
+ return { type: 'text', text };
219
+ }
220
+ /**
221
+ * Build the `image_generate` tool definition.
222
+ * @param options - codex session source, fetch implementation, and image directory.
223
+ * @returns the tool to register on `ctx.tools`.
224
+ */
225
+ export function createImageGenerateTool(options) {
226
+ const imagePool = options.imagePool ?? new ImageAccountPool();
227
+ return defineTool({
228
+ name: 'image_generate',
229
+ description: 'Generate an image with the ChatGPT subscription (gpt-image-2) or the Grok '
230
+ + 'subscription (grok-imagine-image-2.0) and save it as an image file. The `provider` '
231
+ + 'parameter picks the preferred provider (default gpt); when the preferred one is logged '
232
+ + 'out the other serves as fallback. '
233
+ + 'Within the selected provider, image requests use available subscription accounts; '
234
+ + 'quota or authentication rejection can switch accounts without changing providers. '
235
+ + 'Returns the saved file paths; on image-capable models the image itself is attached. '
236
+ + 'To edit or use existing images as references, pass referenceImages copied from the image reference text '
237
+ + 'or structured tool results (read_image.image or image_generate.images). Select only the images the user '
238
+ + 'intends, in prompt order. For local files, call read_image first. Omit referenceImages only for a new image. '
239
+ + 'If an edit reference fails, fix it and retry; never omit it to substitute text-to-image generation.',
240
+ parameters: {
241
+ referenceImages: {
242
+ type: 'array',
243
+ description: 'Optional 1–5 ordered complete DSH image references to edit or use as source images. Not file paths or URLs. Omit only for new images.',
244
+ items: {
245
+ type: 'object',
246
+ additionalProperties: false,
247
+ properties: {
248
+ attachmentId: { type: 'string', required: true },
249
+ mediaType: { type: 'string', enum: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'], required: true },
250
+ bytes: { type: 'integer', required: true },
251
+ width: { type: 'integer', required: true },
252
+ height: { type: 'integer', required: true },
253
+ name: { type: 'string' },
254
+ originalDimensions: {
255
+ type: 'object',
256
+ additionalProperties: false,
257
+ properties: {
258
+ width: { type: 'integer', required: true },
259
+ height: { type: 'integer', required: true },
260
+ },
261
+ },
262
+ },
263
+ },
264
+ },
265
+ prompt: { type: 'string', required: true, description: 'What the image should show.' },
266
+ size: {
267
+ type: 'string',
268
+ enum: ['1024x1024', '1024x1536', '1536x1024', 'auto'],
269
+ description: 'Image dimensions; omit for the provider default.',
270
+ },
271
+ quality: {
272
+ type: 'string',
273
+ enum: ['low', 'medium', 'high', 'auto'],
274
+ description: 'Rendering quality; omit for the provider default.',
275
+ },
276
+ provider: {
277
+ type: 'string',
278
+ enum: ['gpt', 'grok'],
279
+ description: 'Preferred provider (default gpt); the other one serves as fallback when the preferred is logged out.',
280
+ },
281
+ },
282
+ output: {
283
+ schema: {
284
+ type: 'object',
285
+ properties: {
286
+ paths: { type: 'array', items: { type: 'string' }, required: true },
287
+ images: {
288
+ type: 'array',
289
+ items: {
290
+ type: 'object',
291
+ additionalProperties: false,
292
+ properties: {
293
+ attachmentId: { type: 'string', required: true },
294
+ mediaType: { type: 'string', enum: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'], required: true },
295
+ bytes: { type: 'integer', required: true },
296
+ width: { type: 'integer', required: true },
297
+ height: { type: 'integer', required: true },
298
+ name: { type: 'string' },
299
+ },
300
+ },
301
+ },
302
+ revisedPrompt: { type: 'string' },
303
+ },
304
+ additionalProperties: false,
305
+ },
306
+ render: (_args, value) => imageGenerateContent(value),
307
+ },
308
+ presentCall: args => ({
309
+ card: 'generic',
310
+ title: `image_generate${args.referenceImages === undefined ? '' : ` (edit, ${args.referenceImages.length} images)`}: ${truncate(args.prompt)}`,
311
+ }),
312
+ // The web UI has no image surface on tool cards and flattens result blocks
313
+ // to text/JSON, so the completed card shows the text summary only; the
314
+ // image block stays model-facing in the render output.
315
+ presentResult: (_args, result) => ({
316
+ card: 'generic',
317
+ content: result.content.filter(block => block.type === 'text'),
318
+ }),
319
+ async execute(args, exec) {
320
+ const fetchFn = options.fetchFn ?? hostFetch;
321
+ // Validate the prompt even when references cannot be resolved.
322
+ buildImageGenerateBody(args);
323
+ const references = await resolveReferenceImages(args.referenceImages, options.resolveAttachments?.(), exec.signal);
324
+ // Provider selection: the preferred provider (default gpt) when logged
325
+ // in, the other one as the fallback. A configured-but-logged-out manager
326
+ // still resolves through `session()` below so the standard log-in hint
327
+ // surfaces.
328
+ const preferGrok = args.provider === 'grok';
329
+ const createdAt = exec.agent?.session.header?.createdAt;
330
+ const codexEnabled = options.providerEnabled?.('codex', createdAt) !== false;
331
+ const grokEnabled = options.providerEnabled?.('grok', createdAt) !== false;
332
+ const codexReady = codexEnabled && options.codexTokens !== undefined && (await options.codexTokens.list()).length > 0;
333
+ const grokReady = grokEnabled && options.grokTokens !== undefined && (await options.grokTokens.list()).length > 0;
334
+ const useGrok = preferGrok ? grokReady : grokReady && !codexReady;
335
+ const useCodex = !useGrok && codexReady;
336
+ let response;
337
+ if (useCodex && options.codexTokens !== undefined) {
338
+ response = await imagePool.request({
339
+ provider: 'codex', tokens: options.codexTokens, signal: exec.signal,
340
+ owner: exec.agent?.session, rateLimitReset: codexRateLimitReset,
341
+ send: session => fetchFn(references === undefined ? IMAGE_GENERATE_URL : IMAGE_EDIT_URL, {
342
+ method: 'POST',
343
+ headers: {
344
+ 'authorization': `Bearer ${session.accessToken}`,
345
+ 'chatgpt-account-id': session.accountId,
346
+ 'originator': 'codex_cli_rs',
347
+ 'content-type': 'application/json',
348
+ 'accept': 'application/json',
349
+ },
350
+ body: JSON.stringify({
351
+ ...buildImageGenerateBody(args),
352
+ ...references === undefined ? {} : { images: references.map(image_url => ({ image_url })) },
353
+ }),
354
+ signal: exec.signal,
355
+ }),
356
+ });
357
+ }
358
+ else if (useGrok && options.grokTokens !== undefined) {
359
+ response = await imagePool.request({
360
+ provider: 'grok', tokens: options.grokTokens, signal: exec.signal,
361
+ owner: exec.agent?.session, rateLimitReset: grokRateLimitReset,
362
+ send: session => fetchFn(references === undefined ? GROK_IMAGE_GENERATE_URL : GROK_IMAGE_EDIT_URL, {
363
+ method: 'POST',
364
+ headers: {
365
+ 'authorization': `Bearer ${session.accessToken}`,
366
+ 'content-type': 'application/json',
367
+ 'accept': 'application/json',
368
+ },
369
+ body: JSON.stringify({
370
+ ...buildGrokImageGenerateBody(args),
371
+ ...references === undefined ? {} : references.length === 1
372
+ ? { image: { type: 'image_url', url: references[0] } }
373
+ : { images: references.map(url => ({ type: 'image_url', url })) },
374
+ }),
375
+ signal: exec.signal,
376
+ }),
377
+ });
378
+ }
379
+ else {
380
+ if (!codexEnabled && !grokEnabled)
381
+ throw new Error('image_generate: disabled for this session');
382
+ const codex = codexEnabled ? options.codexTokens : undefined;
383
+ const grok = grokEnabled ? options.grokTokens : undefined;
384
+ const manager = preferGrok ? grok ?? codex : codex ?? grok;
385
+ if (manager === undefined)
386
+ throw new Error('image_generate: no image provider is configured');
387
+ await manager.session(); // logged out: throws the provider's log-in hint
388
+ throw new Error('image_generate: no image provider is logged in');
389
+ }
390
+ const images = parseImageGenerateResponse(await response.json());
391
+ const directory = options.imagesDir ?? imagesDirectory();
392
+ await mkdir(directory, { recursive: true });
393
+ const paths = [];
394
+ const mediaTypes = [];
395
+ for (const [index, image] of images.entries()) {
396
+ const mediaType = sniffImageMediaType(image.data);
397
+ const path = join(directory, imageFileName(index, mediaType));
398
+ await writeFile(path, image.data);
399
+ paths.push(path);
400
+ mediaTypes.push(mediaType);
401
+ }
402
+ // Inline display requires durable attachment references, and those may
403
+ // only enter session history on a route that declares image input.
404
+ // Either condition failing degrades to the text-only result.
405
+ const attachments = options.resolveAttachments?.();
406
+ const imageCapable = attachments !== undefined
407
+ && await routeDeclaresImageInput(options.resolveLlm, exec);
408
+ const refs = [];
409
+ if (attachments !== undefined && imageCapable) {
410
+ for (const [index, image] of images.entries()) {
411
+ const ref = await attachments.saveImage({
412
+ data: image.data,
413
+ mediaType: mediaTypes[index],
414
+ name: basename(paths[index]),
415
+ });
416
+ refs.push({
417
+ attachmentId: ref.attachmentId,
418
+ mediaType: ref.mediaType,
419
+ bytes: ref.bytes,
420
+ width: ref.width,
421
+ height: ref.height,
422
+ ...ref.name === undefined ? {} : { name: ref.name },
423
+ });
424
+ }
425
+ }
426
+ const revisedPrompt = images.find(image => image.revisedPrompt !== undefined)?.revisedPrompt;
427
+ const value = {
428
+ paths,
429
+ ...refs.length > 0 ? { images: refs } : {},
430
+ ...revisedPrompt === undefined ? {} : { revisedPrompt },
431
+ };
432
+ // Nested (Code Mode) dispatches need no defer here: the harness's code
433
+ // mode already defers any image-bearing sub-result as a user message, so
434
+ // deferring again would inject the same attachment twice.
435
+ return value;
436
+ },
437
+ });
438
+ }
@@ -0,0 +1,15 @@
1
+ import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
2
+ /** Tool names owned by this plugin and their collision fallbacks. */
3
+ export declare const TOOL_ALIASES: {
4
+ readonly x_search: "dsh_subscriptions_x_search";
5
+ readonly video_generate: "dsh_subscriptions_video_generate";
6
+ readonly image_generate: "dsh_subscriptions_image_generate";
7
+ };
8
+ export interface ToolRegistry {
9
+ register(definition: ToolDefinition): () => void;
10
+ }
11
+ /** Register a tool under its canonical name, then a plugin-scoped alias. */
12
+ export declare function registerWithAlias(registry: ToolRegistry, definition: ToolDefinition, warn?: (message: string) => void): {
13
+ name: string;
14
+ dispose: () => void;
15
+ } | undefined;
@@ -0,0 +1,24 @@
1
+ /** Tool names owned by this plugin and their collision fallbacks. */
2
+ export const TOOL_ALIASES = {
3
+ x_search: 'dsh_subscriptions_x_search',
4
+ video_generate: 'dsh_subscriptions_video_generate',
5
+ image_generate: 'dsh_subscriptions_image_generate',
6
+ };
7
+ /** Register a tool under its canonical name, then a plugin-scoped alias. */
8
+ export function registerWithAlias(registry, definition, warn = message => console.warn(message)) {
9
+ try {
10
+ return { name: definition.name, dispose: registry.register(definition) };
11
+ }
12
+ catch (error) {
13
+ const alias = TOOL_ALIASES[definition.name];
14
+ if (alias === undefined)
15
+ throw error;
16
+ try {
17
+ return { name: alias, dispose: registry.register({ ...definition, name: alias }) };
18
+ }
19
+ catch (aliasError) {
20
+ warn(`dsh-subscriptions: tool ${JSON.stringify(definition.name)} and alias ${JSON.stringify(alias)} are already registered; skipping (${aliasError instanceof Error ? aliasError.message : String(aliasError)})`);
21
+ return undefined;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,89 @@
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 type { ToolDefinition } from '@deepseek-ai/dsh-tools';
12
+ import type { GrokSession } from '../auth/store.js';
13
+ import { AccountTokenManager } from '../providers/accounts.js';
14
+ import type { FetchFn } from '../providers/common.js';
15
+ /** Endpoint the generation request is posted to. */
16
+ export declare const VIDEO_GENERATE_URL = "https://api.x.ai/v1/videos/generations";
17
+ /** The video model the grok subscription endpoint serves. */
18
+ export declare const VIDEO_GENERATE_MODEL = "grok-imagine-video-1.5";
19
+ /** Polling endpoint for one generation request. */
20
+ export declare function videoStatusUrl(requestId: string): string;
21
+ /** Default delay between two status polls. */
22
+ export declare const DEFAULT_POLL_INTERVAL_MS = 3000;
23
+ /** Default overall deadline for one generation (submit → done). */
24
+ export declare const DEFAULT_MAX_WAIT_MS: number;
25
+ /** Dependencies of the `video_generate` tool. */
26
+ export interface VideoGenerateToolOptions {
27
+ /** Grok session source; a missing session throws the log-in hint. */
28
+ tokens: AccountTokenManager<GrokSession>;
29
+ /** Fetch implementation (injectable for tests). */
30
+ fetchFn?: FetchFn;
31
+ /** Directory override for saved videos (defaults under the harness home). */
32
+ videosDir?: string;
33
+ /** Delay between status polls (injectable for tests). */
34
+ pollIntervalMs?: number;
35
+ /** Overall deadline from submit to completion. */
36
+ maxWaitMs?: number;
37
+ }
38
+ /** The wire request body for one generation call. */
39
+ export interface VideoGenerateRequestBody {
40
+ prompt: string;
41
+ model: string;
42
+ duration?: number;
43
+ aspect_ratio?: string;
44
+ resolution?: string;
45
+ image?: {
46
+ url: string;
47
+ };
48
+ }
49
+ /**
50
+ * Assemble the request body from tool arguments (hand-checks the non-empty
51
+ * prompt and the duration range the schema DSL cannot express).
52
+ */
53
+ export declare function buildVideoGenerateBody(args: {
54
+ prompt: string;
55
+ duration?: number;
56
+ aspect_ratio?: '16:9' | '9:16' | '1:1' | '4:3' | '3:4' | '3:2' | '2:3';
57
+ resolution?: '480p' | '720p' | '1080p';
58
+ image_url?: string;
59
+ }): VideoGenerateRequestBody;
60
+ /**
61
+ * Extract the request id from the submit response. Throws when the payload
62
+ * carries none.
63
+ */
64
+ export declare function parseVideoStartResponse(payload: unknown): string;
65
+ /** One decoded poll response. */
66
+ export type VideoStatus = {
67
+ status: 'pending';
68
+ } | {
69
+ status: 'done';
70
+ url: string;
71
+ duration?: number;
72
+ } | {
73
+ status: 'failed' | 'expired';
74
+ detail?: string;
75
+ };
76
+ /**
77
+ * Decode one poll response. A `done` payload without a video URL and an
78
+ * unrecognized status both throw (the poll loop cannot make progress on
79
+ * either).
80
+ */
81
+ export declare function parseVideoStatusResponse(payload: unknown): VideoStatus;
82
+ /** Directory the downloaded MP4 files are written to. */
83
+ export declare function videosDirectory(): string;
84
+ /**
85
+ * Build the `video_generate` tool definition.
86
+ * @param options - grok session source, fetch implementation, and video directory.
87
+ * @returns the tool to register on `ctx.tools`.
88
+ */
89
+ export declare function createVideoGenerateTool(options: VideoGenerateToolOptions): ToolDefinition;