@goodandready/dsh-image-gen 0.10.35 → 0.11.1

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.
@@ -7,48 +7,16 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
7
7
  import path from 'node:path'
8
8
  import { mkdir, writeFile } from 'node:fs/promises'
9
9
  import {
10
- computeGenerationHash,
11
- IMAGE_SIZES,
12
- tryGenerate,
13
- fallbackOrder,
14
- embedPngMetadata,
15
- removeBackgroundFal,
16
- upscaleImageFal,
17
- traceToSvg,
18
- estimateCost,
19
- estimateSharpnessAndVariance,
20
- applyStylePreset,
21
- resolveStylePreset,
22
- blendImagesFal,
23
10
  toLosslessJson,
24
11
  saveAttachmentSafe,
25
12
  } from '../providers.js'
26
- import { resolveConversationImage, analyzeImageWithVision, buildDualOutputMarkdown } from '../resolve-image.js'
27
- import { sanitizeNegativePrompt, supportsNegativePrompt } from '../negative-sanitizer.js'
28
- import { executeWithQualityGate } from '../quality-gate.js'
29
- import { calculateGenerationCost, recordSpend, assertBudgetAvailable } from '../cost-meter.js'
30
- import { trackAndAssertLoopGuard } from '../loop-guard.js'
31
13
  import { sanitizeErrorAndLogs } from '../security.js'
32
- import { getCachedGeneration, setCachedGeneration } from '../generation-cache.js'
33
- import {
34
- extractDesignTokens,
35
- generateCssGradient,
36
- checkWcagContrast,
37
- optimizeSvgContent,
38
- generatePwaIconSuite,
39
- extractSampleColorsFromBuffer,
40
- } from '../frontend-assets.js'
41
-
42
-
43
14
 
44
15
  export function registerProcessingAdvancedTools(ctx, deps) {
45
16
  const {
46
- config,
47
17
  live,
48
- saveAndAttachResult,
49
18
  resolveSource,
50
19
  slugify,
51
- resolveApiKey,
52
20
  } = deps
53
21
 
54
22
  ctx.effect(() => {
@@ -7,44 +7,21 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
7
7
  import path from 'node:path'
8
8
  import { mkdir, writeFile } from 'node:fs/promises'
9
9
  import {
10
- computeGenerationHash,
11
- IMAGE_SIZES,
12
- tryGenerate,
13
- fallbackOrder,
14
- embedPngMetadata,
15
10
  removeBackgroundFal,
16
11
  upscaleImageFal,
17
12
  traceToSvg,
18
- estimateCost,
19
- estimateSharpnessAndVariance,
20
- applyStylePreset,
21
- resolveStylePreset,
22
13
  blendImagesFal,
23
14
  toLosslessJson,
24
15
  saveAttachmentSafe,
25
16
  } from '../providers.js'
26
- import { resolveConversationImage, analyzeImageWithVision, buildDualOutputMarkdown } from '../resolve-image.js'
27
- import { sanitizeNegativePrompt, supportsNegativePrompt } from '../negative-sanitizer.js'
28
- import { executeWithQualityGate } from '../quality-gate.js'
29
- import { calculateGenerationCost, recordSpend, assertBudgetAvailable } from '../cost-meter.js'
17
+ import { buildDualOutputMarkdown } from '../resolve-image.js'
18
+ import { assertBudgetAvailable } from '../cost-meter.js'
30
19
  import { trackAndAssertLoopGuard } from '../loop-guard.js'
31
20
  import { sanitizeErrorAndLogs } from '../security.js'
32
- import { getCachedGeneration, setCachedGeneration } from '../generation-cache.js'
33
- import {
34
- extractDesignTokens,
35
- generateCssGradient,
36
- checkWcagContrast,
37
- optimizeSvgContent,
38
- generatePwaIconSuite,
39
- extractSampleColorsFromBuffer,
40
- } from '../frontend-assets.js'
41
-
42
21
 
43
22
  export function registerProcessingTools(ctx, deps) {
44
23
  const {
45
- config,
46
24
  live,
47
- saveAndAttachResult,
48
25
  resolveSource,
49
26
  slugify,
50
27
  resolveApiKey,
@@ -14,7 +14,6 @@ import {
14
14
  } from '../providers.js'
15
15
  import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
16
16
  import {
17
- DEFAULT_MATRIX_STYLES,
18
17
  normalizeMatrixStyles,
19
18
  buildMatrixCellPrompt,
20
19
  formatMatrixMarkdown,
@@ -127,7 +126,7 @@ export function registerStyleMatrixTools(ctx, deps) {
127
126
 
128
127
  const provider = PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal'
129
128
  let subscriptionImages
130
- try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_) { subscriptionImages = undefined }
129
+ try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch { subscriptionImages = undefined }
131
130
 
132
131
  const chain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
133
132
  const quadrantIds = ['A', 'B', 'C', 'D']
@@ -0,0 +1,250 @@
1
+ // theme-pair.js — generate_theme_pair tool (#189).
2
+ // Synchronized dual generation of light and dark theme visuals with adaptive CSS snippets.
3
+
4
+ import { defineTool } from '@deepseek-ai/dsh-tools'
5
+ import path from 'node:path'
6
+ import { mkdir, writeFile } from 'node:fs/promises'
7
+ import {
8
+ ASPECT_RATIOS,
9
+ buildSidecar,
10
+ makeProviders,
11
+ toLosslessJson,
12
+ saveAttachmentSafe,
13
+ } from '../providers.js'
14
+ import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
15
+ import { renderToolOutput } from '../attachment-helper.js'
16
+ import {
17
+ THEME_STYLES,
18
+ buildThemePairPrompts,
19
+ buildThemePairSnippet,
20
+ } from '../theme-pair-helpers.js'
21
+
22
+ export function registerThemePairTools(ctx, deps) {
23
+ const {
24
+ live,
25
+ slugify,
26
+ resolveApiKey,
27
+ } = deps
28
+
29
+ ctx.effect(() => {
30
+ ctx.tools.register(
31
+ defineTool({
32
+ name: 'generate_theme_pair',
33
+ description:
34
+ 'Generate synchronized dual illustrations or UI graphics designed specifically for light and dark application themes. '
35
+ + 'Produces both light (#FFFFFF clean backdrop) and dark (#0F172A obsidian backdrop with luminous accents) variants with matched composition, plus ready-to-use HTML/CSS responsive markup.',
36
+ parameters: {
37
+ prompt: {
38
+ type: 'string',
39
+ required: true,
40
+ description: 'Core subject or illustration motif, e.g. "cloud server network topology" or "productivity dashboard banner".',
41
+ },
42
+ aspect_ratio: {
43
+ type: 'string',
44
+ enum: Object.keys(ASPECT_RATIOS),
45
+ description: 'Aspect ratio for both variants: "1:1", "16:9", "4:3", etc. (default: "16:9").',
46
+ },
47
+ style: {
48
+ type: 'string',
49
+ enum: THEME_STYLES,
50
+ description: 'Visual style aesthetic (default: "minimalist").',
51
+ },
52
+ dark_contrast_boost: {
53
+ type: 'number',
54
+ description: 'Multiplier for glow and highlight contrast in the dark variant (default: 1.2).',
55
+ },
56
+ output_name: {
57
+ type: 'string',
58
+ description: 'Base filename prefix for the pair (default: auto-slugified from prompt).',
59
+ },
60
+ seed: {
61
+ type: 'integer',
62
+ description: 'Base random seed to synchronize composition between variants.',
63
+ },
64
+ },
65
+ output: {
66
+ schema: {
67
+ type: 'object',
68
+ additionalProperties: true,
69
+ properties: {
70
+ summary: { type: 'string' },
71
+ prompt: { type: 'string' },
72
+ style: { type: 'string' },
73
+ light: {
74
+ type: 'object',
75
+ additionalProperties: true,
76
+ properties: {
77
+ path: { type: 'string' },
78
+ url: { type: 'string' },
79
+ attachment: { type: 'object', additionalProperties: true },
80
+ },
81
+ },
82
+ dark: {
83
+ type: 'object',
84
+ additionalProperties: true,
85
+ properties: {
86
+ path: { type: 'string' },
87
+ url: { type: 'string' },
88
+ attachment: { type: 'object', additionalProperties: true },
89
+ },
90
+ },
91
+ html_snippet: { type: 'string' },
92
+ css_snippet: { type: 'string' },
93
+ },
94
+ },
95
+ render(_args, value) {
96
+ return renderToolOutput(value)
97
+ },
98
+ },
99
+ isConcurrencySafe: () => false,
100
+ async execute(args, exec) {
101
+ const cfg = live()
102
+ if (cfg.enabled === false) {
103
+ throw new Error('Image generation is disabled in settings.')
104
+ }
105
+
106
+ const prompt = String(args.prompt || '').trim()
107
+ if (!prompt) {
108
+ throw new Error('Prompt is required for generate_theme_pair')
109
+ }
110
+
111
+ const style = THEME_STYLES.includes(args.style) ? args.style : 'minimalist'
112
+ const aspectRatio = args.aspect_ratio || '16:9'
113
+ const aspectPixels = ASPECT_RATIOS[aspectRatio] || [1344, 768]
114
+ const darkContrastBoost = typeof args.dark_contrast_boost === 'number' ? args.dark_contrast_boost : 1.2
115
+
116
+ const { lightPrompt, darkPrompt } = buildThemePairPrompts({
117
+ prompt,
118
+ style,
119
+ darkContrastBoost,
120
+ })
121
+
122
+ const primaryProvider = cfg.provider || 'fal'
123
+ const chain = resolveFallbackChain(primaryProvider, cfg.fallbackChain)
124
+ const baseSeed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 1000000)
125
+
126
+ const sessionCwd = exec.agent?.session?.header?.cwd
127
+ const targetDir = cfg.outputDir || 'generated/images'
128
+ const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
129
+ await mkdir(outDir, { recursive: true })
130
+
131
+ const slug = args.output_name ? slugify(args.output_name) : slugify(prompt).slice(0, 36)
132
+ const ts = Date.now().toString(36)
133
+
134
+ // 1. Generate Light theme variant
135
+ const lightJob = {
136
+ prompt: lightPrompt,
137
+ format: 'png',
138
+ aspectPixels,
139
+ aspectRatio,
140
+ seed: baseSeed,
141
+ signal: exec.signal,
142
+ }
143
+ const lightGen = await executeWithFallback({
144
+ chain,
145
+ liveConfig: cfg,
146
+ makeProvidersFn: (deps, job) => makeProviders(deps, job),
147
+ resolveApiKeyFn: resolveApiKey,
148
+ job: lightJob,
149
+ })
150
+
151
+ const lightFilename = `${slug}-light-${ts}.png`
152
+ const lightPath = path.join(outDir, lightFilename)
153
+ await writeFile(lightPath, lightGen.bytes)
154
+
155
+ const lightAttachment = await saveAttachmentSafe(ctx, {
156
+ bytes: lightGen.bytes,
157
+ mediaType: 'image/png',
158
+ filename: lightFilename,
159
+ width: lightGen.width || aspectPixels[0],
160
+ height: lightGen.height || aspectPixels[1],
161
+ })
162
+ const lightUrl = lightAttachment.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(lightAttachment.attachmentId)}` : ''
163
+
164
+ // 2. Generate Dark theme variant (using matched seed)
165
+ const darkJob = {
166
+ prompt: darkPrompt,
167
+ format: 'png',
168
+ aspectPixels,
169
+ aspectRatio,
170
+ seed: baseSeed,
171
+ signal: exec.signal,
172
+ }
173
+ const darkGen = await executeWithFallback({
174
+ chain,
175
+ liveConfig: cfg,
176
+ makeProvidersFn: (deps, job) => makeProviders(deps, job),
177
+ resolveApiKeyFn: resolveApiKey,
178
+ job: darkJob,
179
+ })
180
+
181
+ const darkFilename = `${slug}-dark-${ts}.png`
182
+ const darkPath = path.join(outDir, darkFilename)
183
+ await writeFile(darkPath, darkGen.bytes)
184
+
185
+ const darkAttachment = await saveAttachmentSafe(ctx, {
186
+ bytes: darkGen.bytes,
187
+ mediaType: 'image/png',
188
+ filename: darkFilename,
189
+ width: darkGen.width || aspectPixels[0],
190
+ height: darkGen.height || aspectPixels[1],
191
+ })
192
+ const darkUrl = darkAttachment.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(darkAttachment.attachmentId)}` : ''
193
+
194
+ // Sidecars
195
+ await writeFile(
196
+ path.join(outDir, `${slug}-pair-${ts}.json`),
197
+ JSON.stringify(buildSidecar({
198
+ prompt,
199
+ style,
200
+ aspectRatio,
201
+ size: `${aspectPixels[0]}x${aspectPixels[1]}`,
202
+ format: 'png',
203
+ seed: baseSeed,
204
+ light: { path: lightPath, url: lightUrl, attachmentId: lightAttachment.attachmentId },
205
+ dark: { path: darkPath, url: darkUrl, attachmentId: darkAttachment.attachmentId },
206
+ cost: (lightGen.cost || 0) + (darkGen.cost || 0),
207
+ }), null, 2),
208
+ )
209
+
210
+ const { html, css } = buildThemePairSnippet({
211
+ lightSrc: lightFilename,
212
+ darkSrc: darkFilename,
213
+ alt: prompt,
214
+ })
215
+
216
+ const summary = `Generated Theme Pair (${style}, ${aspectRatio}):\n`
217
+ + `- Light: ${lightFilename} (seed: ${baseSeed})\n`
218
+ + `- Dark: ${darkFilename} (seed: ${baseSeed})\n\n`
219
+ + `\`\`\`html\n${html}\n\`\`\``
220
+
221
+ return toLosslessJson({
222
+ summary,
223
+ prompt,
224
+ style,
225
+ aspect_ratio: aspectRatio,
226
+ light: {
227
+ path: lightPath,
228
+ url: lightUrl,
229
+ filename: lightFilename,
230
+ width: lightGen.width || aspectPixels[0],
231
+ height: lightGen.height || aspectPixels[1],
232
+ attachment: lightAttachment,
233
+ },
234
+ dark: {
235
+ path: darkPath,
236
+ url: darkUrl,
237
+ filename: darkFilename,
238
+ width: darkGen.width || aspectPixels[0],
239
+ height: darkGen.height || aspectPixels[1],
240
+ attachment: darkAttachment,
241
+ },
242
+ html_snippet: html,
243
+ css_snippet: css,
244
+ attachment: lightAttachment,
245
+ })
246
+ },
247
+ }),
248
+ )
249
+ }, 'dsh-image-gen: tool generate_theme_pair')
250
+ }
@@ -123,7 +123,7 @@ export function registerUiAssetTools(ctx, deps) {
123
123
 
124
124
  const provider = PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal'
125
125
  let subscriptionImages
126
- try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_) { subscriptionImages = undefined }
126
+ try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch { subscriptionImages = undefined }
127
127
 
128
128
  const providers = makeProviders(
129
129
  { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
package/lib/updater.js CHANGED
@@ -3,7 +3,6 @@ import { existsSync, readFileSync } from 'node:fs'
3
3
  import { readFile } from 'node:fs/promises'
4
4
  import { homedir } from 'node:os'
5
5
  import { basename, dirname, isAbsolute, resolve } from 'node:path'
6
- import { fileURLToPath } from 'node:url'
7
6
  import { isLoopbackAddress, isPrivateLanAddress, extractHostName } from './security.js'
8
7
 
9
8
  /**
@@ -21,14 +20,6 @@ function header(request, name) {
21
20
  return Array.isArray(value) ? value[0] : value
22
21
  }
23
22
 
24
- function isLoopback(value) {
25
- return isLoopbackAddress(value)
26
- }
27
-
28
- function isPrivateLan(value) {
29
- return isPrivateLanAddress(value)
30
- }
31
-
32
23
  export function isTrustedUpdateRequest(request) {
33
24
  if (header(request, UPDATE_HEADER) !== '1') return false
34
25
  const remote = request.socket?.remoteAddress
package/lib/vault.js ADDED
@@ -0,0 +1,183 @@
1
+ import { existsSync, unlinkSync } from 'node:fs'
2
+ import { readHistory, writeHistory } from './history.js'
3
+ import { isTrustedLocalRequest } from './security.js'
4
+
5
+ /**
6
+ * Match image dimensions against an aspect ratio label or ratio value.
7
+ */
8
+ export function matchAspectRatio(width, height, targetAspect) {
9
+ if (!targetAspect || targetAspect === 'any' || targetAspect === 'all') return true
10
+ if (!width || !height) return false
11
+ const ratio = width / height
12
+
13
+ switch (targetAspect) {
14
+ case '1:1':
15
+ case 'square':
16
+ return Math.abs(ratio - 1.0) < 0.1
17
+ case '16:9':
18
+ case 'landscape':
19
+ return Math.abs(ratio - (16 / 9)) < 0.15
20
+ case '9:16':
21
+ case 'portrait':
22
+ return Math.abs(ratio - (9 / 16)) < 0.15
23
+ case '4:3':
24
+ return Math.abs(ratio - (4 / 3)) < 0.15
25
+ case '3:4':
26
+ return Math.abs(ratio - (3 / 4)) < 0.15
27
+ case '21:9':
28
+ case 'ultrawide':
29
+ return Math.abs(ratio - (21 / 9)) < 0.2
30
+ default:
31
+ return true
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Filter, sort, and paginate vault entries with path sanitization.
37
+ */
38
+ export function filterVaultEntries(entries, options = {}, existsFn = existsSync) {
39
+ const {
40
+ q = '',
41
+ provider = '',
42
+ aspect = '',
43
+ sort = 'newest',
44
+ offset = 0,
45
+ limit = 30,
46
+ } = options
47
+
48
+ const safeOffset = Math.max(0, Number.parseInt(offset, 10) || 0)
49
+ const safeLimit = Math.min(100, Math.max(1, Number.parseInt(limit, 10) || 30))
50
+ const cleanQ = String(q).trim().toLowerCase()
51
+ const cleanProvider = String(provider).trim().toLowerCase()
52
+
53
+ const existing = (Array.isArray(entries) ? entries : []).filter((e) => e && e.path && existsFn(e.path))
54
+
55
+ const filtered = existing.filter((e) => {
56
+ if (cleanQ && !String(e.prompt || '').toLowerCase().includes(cleanQ)) {
57
+ return false
58
+ }
59
+ if (cleanProvider && String(e.provider || '').toLowerCase() !== cleanProvider) {
60
+ return false
61
+ }
62
+ if (aspect && !matchAspectRatio(e.width, e.height, aspect)) {
63
+ return false
64
+ }
65
+ return true
66
+ })
67
+
68
+ filtered.sort((a, b) => {
69
+ const timeA = a.createdAt ? Date.parse(a.createdAt) : 0
70
+ const timeB = b.createdAt ? Date.parse(b.createdAt) : 0
71
+ if (sort === 'oldest') {
72
+ return timeA - timeB
73
+ }
74
+ return timeB - timeA
75
+ })
76
+
77
+ const total = filtered.length
78
+ const sliced = filtered.slice(safeOffset, safeOffset + safeLimit)
79
+
80
+ const items = sliced.map((e) => {
81
+ const { path: _discardPath, ...safeEntry } = e
82
+ return {
83
+ id: e.id || e.attachmentId || String(e.createdAt || Math.random()),
84
+ ...safeEntry,
85
+ thumbnailUrl: e.thumbnailUrl || (e.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(e.attachmentId)}` : ''),
86
+ url: e.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(e.attachmentId)}` : '',
87
+ }
88
+ })
89
+
90
+ return {
91
+ items,
92
+ total,
93
+ offset: safeOffset,
94
+ limit: safeLimit,
95
+ hasMore: safeOffset + safeLimit < total,
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Delete entry and associated disk files (image + sidecar json).
101
+ */
102
+ export async function deleteVaultEntry(id) {
103
+ if (!id) return { success: false, error: 'Missing entry id' }
104
+ const entries = await readHistory()
105
+ const idx = entries.findIndex((e) => e.id === id || e.attachmentId === id)
106
+ if (idx === -1) {
107
+ return { success: false, error: 'Vault entry not found' }
108
+ }
109
+
110
+ const [entry] = entries.splice(idx, 1)
111
+ if (entry && entry.path) {
112
+ try {
113
+ unlinkSync(entry.path)
114
+ } catch { /* ignore missing file */ }
115
+ try {
116
+ unlinkSync(entry.path.replace(/\.[^.]+$/, '.json'))
117
+ } catch { /* ignore missing sidecar */ }
118
+ }
119
+
120
+ await writeHistory(entries)
121
+ return { success: true, id }
122
+ }
123
+
124
+ /**
125
+ * Register vault HTTP routes on DSH webServer.
126
+ */
127
+ export function registerVaultRoutes(ctx) {
128
+ ctx.effect(() => ctx.webServer.register({
129
+ kind: 'exact',
130
+ path: '/dsh-image-gen/vault',
131
+ handler: async (req, res) => {
132
+ if (!isTrustedLocalRequest(req)) {
133
+ res.writeHead(403, { 'Content-Type': 'application/json' })
134
+ res.end(JSON.stringify({ error: 'forbidden' }))
135
+ return
136
+ }
137
+
138
+ if (req.method === 'GET') {
139
+ const u = new URL(req.url, 'http://localhost')
140
+ const q = u.searchParams.get('q') || ''
141
+ const provider = u.searchParams.get('provider') || ''
142
+ const aspect = u.searchParams.get('aspect') || ''
143
+ const sort = u.searchParams.get('sort') || 'newest'
144
+ const offset = u.searchParams.get('offset') || '0'
145
+ const limit = u.searchParams.get('limit') || '30'
146
+
147
+ const entries = await readHistory()
148
+ const result = filterVaultEntries(entries, { q, provider, aspect, sort, offset, limit })
149
+ res.writeHead(200, { 'Content-Type': 'application/json' })
150
+ res.end(JSON.stringify({ ok: true, ...result }))
151
+ return
152
+ }
153
+
154
+ if (req.method === 'DELETE') {
155
+ const u = new URL(req.url, 'http://localhost')
156
+ let id = u.searchParams.get('id')
157
+
158
+ if (!id) {
159
+ try {
160
+ const chunks = []
161
+ for await (const chunk of req) chunks.push(chunk)
162
+ const body = JSON.parse(Buffer.concat(chunks).toString('utf-8'))
163
+ id = body.id
164
+ } catch { /* body parse error */ }
165
+ }
166
+
167
+ if (!id) {
168
+ res.writeHead(400, { 'Content-Type': 'application/json' })
169
+ res.end(JSON.stringify({ ok: false, error: 'Missing id parameter' }))
170
+ return
171
+ }
172
+
173
+ const result = await deleteVaultEntry(id)
174
+ res.writeHead(result.success ? 200 : 404, { 'Content-Type': 'application/json' })
175
+ res.end(JSON.stringify({ ok: result.success, ...result }))
176
+ return
177
+ }
178
+
179
+ res.writeHead(405, { 'Content-Type': 'application/json' })
180
+ res.end(JSON.stringify({ error: 'Method not allowed' }))
181
+ },
182
+ }), 'dsh-image-gen: vault route')
183
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.10.35",
3
+ "version": "0.11.1",
4
4
  "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -71,5 +71,11 @@
71
71
  "scripts": {
72
72
  "test": "node scripts/build-client.mjs && node --check lib/client.js && node --test test/*.test.mjs",
73
73
  "build:client": "node scripts/build-client.mjs"
74
+ },
75
+ "publishConfig": {
76
+ "access": "public"
77
+ },
78
+ "devDependencies": {
79
+ "@deepseek-ai/schemastery": "^3.18.1"
74
80
  }
75
81
  }