@goodandready/dsh-image-gen 0.11.12 → 0.11.14

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/lib/client.js CHANGED
@@ -287,6 +287,11 @@ window.__ModuleLoader__.load({
287
287
  { field: 'deliverAs', spec: selectField('deliverAs', DELIVERY_MODES), kind: 'select', options: DELIVERY_MODES, labelKey: 'f.deliverAs', hintKey: 'f.deliverAsHint', tab: 'general' },
288
288
  { field: 'outputDir', spec: textField('outputDir'), kind: 'text', labelKey: 'f.outputDir', hintKey: 'f.outputDirHint', placeholderKey: 'p.outputDir', tab: 'general' },
289
289
  { field: 'historyLimit', spec: numberField('historyLimit'), kind: 'number', labelKey: 'f.historyLimit', hintKey: 'f.historyLimitHint', placeholderKey: 'p.historyLimit', tab: 'general' },
290
+ { field: 'toolsetProfile', spec: selectField('toolsetProfile', ['minimal', 'all', 'custom']), kind: 'select', options: ['minimal', 'all', 'custom'], labelKey: 'f.toolsetProfile', hintKey: 'f.toolsetProfileHint', tab: 'general' },
291
+ { field: 'toolsetDesign', spec: booleanField('toolsetDesign'), kind: 'select', options: ['true', 'false'], labelKey: 'f.toolsetDesign', hintKey: 'f.toolsetDesignHint', tab: 'general' },
292
+ { field: 'toolsetProcessing', spec: booleanField('toolsetProcessing'), kind: 'select', options: ['true', 'false'], labelKey: 'f.toolsetProcessing', hintKey: 'f.toolsetProcessingHint', tab: 'general' },
293
+ { field: 'toolsetFrontend', spec: booleanField('toolsetFrontend'), kind: 'select', options: ['true', 'false'], labelKey: 'f.toolsetFrontend', hintKey: 'f.toolsetFrontendHint', tab: 'general' },
294
+ { field: 'toolsetCreative', spec: booleanField('toolsetCreative'), kind: 'select', options: ['true', 'false'], labelKey: 'f.toolsetCreative', hintKey: 'f.toolsetCreativeHint', tab: 'general' },
290
295
 
291
296
  // Provider: FAL
292
297
  { field: 'model', spec: textField('model'), kind: 'text', labelKey: 'f.model', hintKey: 'f.modelHint', placeholderKey: 'p.model', when: ['fal'], tab: 'provider' },
@@ -2458,6 +2463,16 @@ window.__ModuleLoader__.load({
2458
2463
  'p.outputDir': 'generated/images',
2459
2464
  'f.historyLimit': 'History Limit',
2460
2465
  'f.historyLimitHint': 'Maximum number of recent generation records kept in memory.',
2466
+ 'f.toolsetProfile': 'Toolset Profile',
2467
+ 'f.toolsetProfileHint': 'Select which tools to register into context: minimal (<= 3 core tools, saving ~8k prompt tokens), all (30 tools), or custom.',
2468
+ 'f.toolsetDesign': 'Design Toolset',
2469
+ 'f.toolsetDesignHint': 'Enable design tools: style_matrix, theme_pair, ui_asset, character_sheet, beautify_diagram, anchor.',
2470
+ 'f.toolsetProcessing': 'Processing Toolset',
2471
+ 'f.toolsetProcessingHint': 'Enable processing tools: remove_background, upscale, vectorize, convert, grid, smart_crop, asset_pack, replace_image_text.',
2472
+ 'f.toolsetFrontend': 'Frontend Toolset',
2473
+ 'f.toolsetFrontendHint': 'Enable frontend tools: extract_design_tokens, image_to_css_gradient, check_image_contrast, responsive_mockups.',
2474
+ 'f.toolsetCreative': 'Creative Toolset',
2475
+ 'f.toolsetCreativeHint': 'Enable creative tools: spritesheet, seamless_pattern, sketch_to_image, image_pack.',
2461
2476
  'p.historyLimit': '50',
2462
2477
 
2463
2478
  'f.model': 'FAL Model ID',
@@ -2724,6 +2739,16 @@ window.__ModuleLoader__.load({
2724
2739
  'p.outputDir': 'generated/images',
2725
2740
  'f.historyLimit': '历史记录数量',
2726
2741
  'f.historyLimitHint': '保存在内存历史列表中的最大条目数。',
2742
+ 'f.toolsetProfile': '工具集配置档',
2743
+ 'f.toolsetProfileHint': '选择注册到对话上下文的工具集:minimal(<= 3个核心工具,节省~8k提示词tokens),all(所有30个工具),或custom。',
2744
+ 'f.toolsetDesign': '设计工具集',
2745
+ 'f.toolsetDesignHint': '启用设计工具:style_matrix, theme_pair, ui_asset, character_sheet, beautify_diagram, anchor。',
2746
+ 'f.toolsetProcessing': '图像处理工具集',
2747
+ 'f.toolsetProcessingHint': '启用处理工具:remove_background, upscale, vectorize, convert, grid, smart_crop, asset_pack, replace_image_text。',
2748
+ 'f.toolsetFrontend': '前端工具集',
2749
+ 'f.toolsetFrontendHint': '启用前端工具:extract_design_tokens, image_to_css_gradient, check_image_contrast, responsive_mockups。',
2750
+ 'f.toolsetCreative': '创意工具集',
2751
+ 'f.toolsetCreativeHint': '启用创意工具:spritesheet, seamless_pattern, sketch_to_image, image_pack。',
2727
2752
  'p.historyLimit': '50',
2728
2753
 
2729
2754
  'f.model': 'FAL 模型 ID',
@@ -3076,6 +3101,12 @@ window.__ModuleLoader__.load({
3076
3101
  'smart_crop_image',
3077
3102
  'export_asset_pack',
3078
3103
  'generate_ui_asset',
3104
+ 'generate_character_sheet',
3105
+ 'replace_image_text',
3106
+ 'beautify_diagram',
3107
+ 'generate_seamless_pattern',
3108
+ 'generate_spritesheet',
3109
+ 'sketch_to_image',
3079
3110
  ]
3080
3111
 
3081
3112
  for (const toolName of toolviewTools) {
@@ -54,6 +54,40 @@ export const Config = z.object({
54
54
  .description('Default aspect ratio (square, landscape_16_9, portrait_9_16, etc.) used when prompt does not specify dimensions.')
55
55
  .default('square')
56
56
  .volatile(),
57
+ toolsetProfile: z
58
+ .string()
59
+ .description('Which toolset profile to register into conversation context: "minimal" (default, <= 3 core tools, saving ~8,000 prompt tokens), "all" (all 30 tools), or "custom".')
60
+ .default('minimal')
61
+ .volatile(),
62
+ toolsets: z
63
+ .object({
64
+ design: z.boolean().default(false).description('Design tools: style_matrix, theme_pair, ui_asset, character_sheet, beautify_diagram, anchor'),
65
+ processing: z.boolean().default(false).description('Processing tools: remove_background, upscale, vectorize, convert, grid, smart_crop, asset_pack, replace_image_text'),
66
+ frontend: z.boolean().default(false).description('Frontend tools: extract_design_tokens, image_to_css_gradient, check_image_contrast, responsive_mockups'),
67
+ creative: z.boolean().default(false).description('Creative tools: spritesheet, seamless_pattern, sketch_to_image, image_pack'),
68
+ })
69
+ .default({ design: false, processing: false, frontend: false, creative: false })
70
+ .volatile(),
71
+ toolsetDesign: z
72
+ .boolean()
73
+ .description('Enable design tools: style_matrix, theme_pair, ui_asset, character_sheet, beautify_diagram, anchor')
74
+ .default(false)
75
+ .volatile(),
76
+ toolsetProcessing: z
77
+ .boolean()
78
+ .description('Enable processing tools: remove_background, upscale, vectorize, convert, grid, smart_crop, asset_pack, replace_image_text')
79
+ .default(false)
80
+ .volatile(),
81
+ toolsetFrontend: z
82
+ .boolean()
83
+ .description('Enable frontend tools: extract_design_tokens, image_to_css_gradient, check_image_contrast, responsive_mockups')
84
+ .default(false)
85
+ .volatile(),
86
+ toolsetCreative: z
87
+ .boolean()
88
+ .description('Enable creative tools: spritesheet, seamless_pattern, sketch_to_image, image_pack')
89
+ .default(false)
90
+ .volatile(),
57
91
  deliverAs: z
58
92
  .string()
59
93
  .description('How generated images are returned: "link" keeps them on disk and embeds an inline preview link; "attachment" embeds full image bytes directly in the chat.')
package/lib/index.js CHANGED
@@ -312,7 +312,10 @@ export function apply(ctx, rawConfig) {
312
312
  }
313
313
  try {
314
314
  const u = new URL(req.url, 'http://localhost')
315
- const provider = u.searchParams.get('provider') || config.provider || 'fal'
315
+ const qProvider = u.searchParams.get('provider')
316
+ const provider = (typeof qProvider === 'string' && qProvider.trim())
317
+ || (typeof config?.provider === 'string' && config.provider.trim())
318
+ || 'fal'
316
319
  const deps = {
317
320
  fetchImpl: fetch,
318
321
  resolveKey: (ref) => resolveApiKey(ctx, ref),
package/lib/providers.js CHANGED
@@ -378,7 +378,7 @@ export async function blendImagesFal({ fetchImpl, resolveKey, cfg }, { images, w
378
378
  /** @internal — direct provider call path for integration tests */
379
379
  export async function editImageDirect(deps, job) {
380
380
  const providers = makeProviders(deps, job)
381
- const providerKey = job.provider || deps.cfg.defaultProvider || 'fal'
381
+ const providerKey = (typeof job?.provider === 'string' && job.provider.trim()) || (typeof deps?.cfg?.provider === 'string' && deps.cfg.provider.trim()) || (typeof deps?.cfg?.defaultProvider === 'string' && deps.cfg.defaultProvider.trim()) || 'fal'
382
382
  const fn = providers[providerKey] || providers.fal || providers.custom
383
383
  if (!fn) throw new Error(`Provider "${providerKey}" does not support image editing`)
384
384
  return fn(job.seed, job.prompt)
@@ -391,7 +391,7 @@ export async function varyImageDirect(deps, job) {
391
391
  ...job,
392
392
  strength: job.variationStrength ?? job.strength ?? 0.35,
393
393
  })
394
- const providerKey = job.provider || deps.cfg.defaultProvider || 'fal'
394
+ const providerKey = (typeof job?.provider === 'string' && job.provider.trim()) || (typeof deps?.cfg?.provider === 'string' && deps.cfg.provider.trim()) || (typeof deps?.cfg?.defaultProvider === 'string' && deps.cfg.defaultProvider.trim()) || 'fal'
395
395
  const fn = providers[providerKey] || providers.fal || providers.custom
396
396
  if (!fn) throw new Error(`Provider "${providerKey}" does not support image variations`)
397
397
  return fn(job.seed, job.prompt)
@@ -428,7 +428,11 @@ export function toLosslessJson(val) {
428
428
  export async function testProviderConnection(deps, providerId) {
429
429
  const { fetchImpl = fetch, resolveKey, cfg } = deps
430
430
  const start = Date.now()
431
- const p = (providerId || cfg.defaultProvider || 'fal').toLowerCase()
431
+ const rawProvider = (typeof providerId === 'string' && providerId.trim())
432
+ || (typeof cfg?.provider === 'string' && cfg.provider.trim())
433
+ || (typeof cfg?.defaultProvider === 'string' && cfg.defaultProvider.trim())
434
+ || 'fal'
435
+ const p = rawProvider.toLowerCase()
432
436
 
433
437
  try {
434
438
  if (p === 'fal') {
@@ -1,5 +1,5 @@
1
- // register-tools.js — orchestrates all image-gen tool registrations (#216).
2
- // Each tool lives in lib/tools/* and is registered inside a labeled ctx.effect.
1
+ // register-tools.js — orchestrates all image-gen tool registrations (#216, #337).
2
+ // Supports 'minimal' (default: <= 3 core tools), 'all' (all 30 tools), and 'custom' (toggled toolsets).
3
3
 
4
4
  import { registerGenerationTools } from './tools/generation.js'
5
5
  import { registerProcessingTools } from './tools/processing-basic.js'
@@ -20,27 +20,53 @@ import { registerVisionOcrTools } from './tools/vision-ocr.js'
20
20
  import { registerDiagramTools } from './tools/diagram.js'
21
21
 
22
22
  /**
23
- * Registers every image-gen tool on the host tools service.
23
+ * Registers every image-gen tool on the host tools service according to toolsetProfile (#337).
24
24
  *
25
25
  * @param {object} ctx cordis context
26
26
  * @param {object} deps shared runtime from apply()
27
27
  */
28
28
  export function registerAllTools(ctx, deps) {
29
- registerGenerationTools(ctx, deps)
30
- registerProcessingTools(ctx, deps)
31
- registerProcessingAdvancedTools(ctx, deps)
32
- registerEditingTools(ctx, deps)
33
- registerInspectTools(ctx, deps)
34
- registerFrontendTools(ctx, deps)
35
- registerSketchTools(ctx, deps)
36
- registerSpritesheetTools(ctx, deps)
37
- registerPatternTools(ctx, deps)
38
- registerResponsiveTools(ctx, deps)
39
- registerAnchorTools(ctx, deps)
40
- registerUiAssetTools(ctx, deps)
41
- registerStyleMatrixTools(ctx, deps)
42
- registerThemePairTools(ctx, deps)
43
- registerCharacterSheetTools(ctx, deps)
44
- registerVisionOcrTools(ctx, deps)
45
- registerDiagramTools(ctx, deps)
29
+ const cfg = typeof deps.live === 'function' ? deps.live() : (deps.config || {})
30
+ const profile = cfg.toolsetProfile || 'minimal'
31
+ const isAll = profile === 'all'
32
+ const sets = cfg.toolsets || {}
33
+ const design = isAll || Boolean(sets.design || cfg.toolsetDesign)
34
+ const processing = isAll || Boolean(sets.processing || cfg.toolsetProcessing)
35
+ const frontend = isAll || Boolean(sets.frontend || cfg.toolsetFrontend)
36
+ const creative = isAll || Boolean(sets.creative || cfg.toolsetCreative)
37
+
38
+ // 1. Core tools: generate_image, edit_image, inspect_image_quality (<= 3 tools in minimal)
39
+ registerGenerationTools(ctx, deps, { coreOnly: !creative })
40
+ registerEditingTools(ctx, deps, { coreOnly: !processing })
41
+ registerInspectTools(ctx, deps, { coreOnly: !processing })
42
+
43
+ // 2. Processing toolset
44
+ if (processing) {
45
+ registerProcessingTools(ctx, deps)
46
+ registerProcessingAdvancedTools(ctx, deps)
47
+ registerVisionOcrTools(ctx, deps)
48
+ }
49
+
50
+ // 3. Design toolset
51
+ if (design) {
52
+ registerAnchorTools(ctx, deps)
53
+ registerUiAssetTools(ctx, deps)
54
+ registerStyleMatrixTools(ctx, deps)
55
+ registerThemePairTools(ctx, deps)
56
+ registerCharacterSheetTools(ctx, deps)
57
+ registerDiagramTools(ctx, deps)
58
+ }
59
+
60
+ // 4. Frontend toolset
61
+ if (frontend) {
62
+ registerFrontendTools(ctx, deps)
63
+ registerResponsiveTools(ctx, deps)
64
+ }
65
+
66
+ // 5. Creative toolset
67
+ if (creative) {
68
+ registerSketchTools(ctx, deps)
69
+ registerSpritesheetTools(ctx, deps)
70
+ registerPatternTools(ctx, deps)
71
+ }
46
72
  }
package/lib/security.js CHANGED
@@ -64,7 +64,8 @@ export function enforceSecurePermissions(filePath) {
64
64
  try {
65
65
  chmodSync(filePath, 0o600)
66
66
  return true
67
- } catch {
67
+ } catch (_err) {
68
+ /* non-fatal: permission change error */
68
69
  return false
69
70
  }
70
71
  }
@@ -143,7 +144,7 @@ export function extractHostName(hostHeader) {
143
144
  try {
144
145
  const url = new URL(`http://${trimmed}`)
145
146
  return url.hostname.replace(/^\[|\]$/g, '')
146
- } catch {
147
+ } catch (_err) {
147
148
  return trimmed.replace(/^\[|\]$/g, '').split(':')[0] || ''
148
149
  }
149
150
  }
@@ -210,7 +211,7 @@ export function isTrustedLocalRequest(req) {
210
211
  if (url.host.toLowerCase() !== host.toLowerCase()) {
211
212
  return false
212
213
  }
213
- } catch {
214
+ } catch (_err) {
214
215
  return false
215
216
  }
216
217
  }
@@ -226,7 +227,7 @@ export function isTrustedLocalRequest(req) {
226
227
  if (!isLoopbackAddress(refererHostName) && !isPrivateLanAddress(refererHostName)) {
227
228
  return false
228
229
  }
229
- } catch {
230
+ } catch (_err) {
230
231
  return false
231
232
  }
232
233
  }
@@ -79,7 +79,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, setLiveConfi
79
79
  let payload
80
80
  try {
81
81
  payload = JSON.parse(buf.toString('utf8') || '{}')
82
- } catch {
82
+ } catch (_err) {
83
83
  res.writeHead(400, { 'Content-Type': 'application/json' })
84
84
  res.end(JSON.stringify({ ok: false, error: 'invalid JSON' }))
85
85
  return
@@ -89,11 +89,7 @@ export function registerCharacterSheetTools(ctx, deps) {
89
89
  },
90
90
  },
91
91
  },
92
- render: (result, renderCtx) =>
93
- renderToolOutput(result, renderCtx, {
94
- title: (res) => `Character Sheet (${res.layout || 'turnaround'}) — ${res.style || 'concept_art'}`,
95
- subtitle: (res) => (res.views ? `Views: ${res.views.join(' | ')}` : ''),
96
- }),
92
+ render: (_args, value) => renderToolOutput(value),
97
93
  },
98
94
  async execute(args, execCtx) {
99
95
  const cfg = live.value
@@ -85,11 +85,7 @@ export function registerDiagramTools(ctx, deps) {
85
85
  },
86
86
  },
87
87
  },
88
- render: (result, renderCtx) =>
89
- renderToolOutput(result, renderCtx, {
90
- title: (res) => `3D Architecture Diagram — ${res.title || res.style || 'Isometric'}`,
91
- subtitle: (res) => (res.detectedNodes?.length ? `Nodes: ${res.detectedNodes.slice(0, 4).join(', ')}` : ''),
92
- }),
88
+ render: (_args, value) => renderToolOutput(value),
93
89
  },
94
90
  async execute(args, execCtx) {
95
91
  const cfg = live?.value || (typeof live === 'function' ? live() : {})
@@ -16,7 +16,7 @@ import { trackAndAssertLoopGuard } from '../loop-guard.js'
16
16
  import { sanitizeErrorAndLogs } from '../security.js'
17
17
 
18
18
 
19
- export function registerEditingTools(ctx, deps) {
19
+ export function registerEditingTools(ctx, deps, options = {}) {
20
20
  const {
21
21
  config,
22
22
  live,
@@ -29,41 +29,36 @@ export function registerEditingTools(ctx, deps) {
29
29
  ctx.tools.register(
30
30
  defineTool({
31
31
  name: 'edit_image',
32
- description:
33
- 'Edit an existing image using inpainting, targeted modification, or object replacement. '
34
- + 'Describe the desired changes in prompt. '
35
- + 'If image is omitted or "latest", automatically resolves the most recent image from the conversation session (#144). '
36
- + 'Leverages @goodandready/dsh-vision-bridge for pre-edit composition analysis (#145). '
37
- + 'Optionally accepts mask (path or attachment id) and strength (0.1-1.0). Returns safe dual-output (#150).',
32
+ description: 'Edit an image using inpainting, targeted modification, or object replacement. If image is omitted, uses the latest image from conversation.',
38
33
  parameters: {
39
34
  prompt: {
40
35
  type: 'string',
41
36
  required: true,
42
- description: 'Detailed description of the modification or edit to perform on the image.',
37
+ description: 'Description of modification or edit to perform.',
43
38
  },
44
39
  image: {
45
40
  type: 'string',
46
- description: 'Path or attachment id of the source image. Defaults to "latest" to auto-resolve from session history.',
41
+ description: 'Path or attachment ID of source image. Defaults to "latest".',
47
42
  },
48
43
  mask: {
49
44
  type: 'string',
45
+ description: 'Optional inpaint mask path or attachment ID.',
50
46
  },
51
47
  mask_image: {
52
48
  type: 'string',
53
- description: 'Optional path or attachment id of the inpaint mask image (alias for mask).',
54
- description: 'Optional path or attachment id of the inpaint mask image.',
49
+ description: 'Alias for mask.',
55
50
  },
56
51
  strength: {
57
52
  type: 'number',
58
- description: 'Denoising / edit strength between 0.1 and 1.0 (default 0.75 for edits, 0.9 for mask inpainting).',
53
+ description: 'Edit strength between 0.1 and 1.0 (default 0.75).',
59
54
  },
60
55
  output_name: {
61
56
  type: 'string',
62
- description: 'Optional file name stem for the saved edited image.',
57
+ description: 'Optional file name stem for edited image.',
63
58
  },
64
59
  output_dir: {
65
60
  type: 'string',
66
- description: 'Optional custom directory to write edited image into.',
61
+ description: 'Optional custom directory for edited image.',
67
62
  },
68
63
  },
69
64
  output: {
@@ -165,6 +160,7 @@ export function registerEditingTools(ctx, deps) {
165
160
  }),
166
161
  )
167
162
  }, 'dsh-image-gen: tool edit_image')
163
+ if (!options.coreOnly) {
168
164
  ctx.effect(() => {
169
165
  ctx.tools.register(
170
166
  defineTool({
@@ -450,5 +446,5 @@ export function registerEditingTools(ctx, deps) {
450
446
  }),
451
447
  )
452
448
  }, 'dsh-image-gen: tool remix_image')
453
-
449
+ }
454
450
  }
@@ -1,22 +1,4 @@
1
1
  import { registerGenerationPackTool } from './generation-pack.js'
2
-
3
- /** Run an array of async task functions with a concurrency cap */
4
- async function asyncPool(tasks, concurrency = 3) {
5
- const results = new Array(tasks.length)
6
- let nextIdx = 0
7
- async function worker() {
8
- while (nextIdx < tasks.length) {
9
- const idx = nextIdx++
10
- results[idx] = await tasks[idx]()
11
- }
12
- }
13
- const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker())
14
- await Promise.all(workers)
15
- return results
16
- }
17
-
18
- // generation — image-gen tools (generate_image, generate_image_pack). Extracted from apply() (#216).
19
-
20
2
  import { defineTool } from '@deepseek-ai/dsh-tools'
21
3
  import path from 'node:path'
22
4
  import { mkdir, writeFile } from 'node:fs/promises'
@@ -53,8 +35,24 @@ import {
53
35
  cachedResult,
54
36
  } from '../history.js'
55
37
 
38
+ // generation — image-gen tools (generate_image, generate_image_pack). Extracted from apply() (#216).
56
39
 
57
- export function registerGenerationTools(ctx, deps) {
40
+ /** Run an array of async task functions with a concurrency cap */
41
+ async function asyncPool(tasks, concurrency = 3) {
42
+ const results = new Array(tasks.length)
43
+ let nextIdx = 0
44
+ async function worker() {
45
+ while (nextIdx < tasks.length) {
46
+ const idx = nextIdx++
47
+ results[idx] = await tasks[idx]()
48
+ }
49
+ }
50
+ const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker())
51
+ await Promise.all(workers)
52
+ return results
53
+ }
54
+
55
+ export function registerGenerationTools(ctx, deps, options = {}) {
58
56
  const {
59
57
  config,
60
58
  live,
@@ -66,17 +64,12 @@ export function registerGenerationTools(ctx, deps) {
66
64
  ctx.tools.register(
67
65
  defineTool({
68
66
  name: 'generate_image',
69
- description:
70
- 'Generate an image with the configured image provider. '
71
- + 'Saves the image to the session workspace and shows it in the conversation. '
72
- + 'Depending on how the deployment is configured the result carries either the image itself or a link to it; '
73
- + 'when you only get a link, answer from the prompt and the link rather than claiming to see the picture. '
74
- + 'Use for any text-to-image request. Pass count (1-4) to generate several variations in one call; cost scales with count (default 1).',
67
+ description: 'Generate an image from prompt. Saves the image to workspace and conversation. Pass count (1-4) for variations.',
75
68
  parameters: {
76
69
  prompt: {
77
70
  type: 'string',
78
71
  required: true,
79
- description: 'Detailed description of the image to generate (subject, style, lighting, composition, colors).',
72
+ description: 'Detailed description of the image to generate.',
80
73
  },
81
74
  image_size: {
82
75
  type: 'string',
@@ -85,7 +78,7 @@ export function registerGenerationTools(ctx, deps) {
85
78
  aspect_ratio: {
86
79
  type: 'string',
87
80
  enum: ['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3'],
88
- description: 'Optional aspect ratio; overrides image_size when both are given. Sent as width/height to providers that accept pixels.',
81
+ description: 'Optional aspect ratio (e.g. 16:9, 1:1), overrides image_size.',
89
82
  },
90
83
  seed: {
91
84
  type: 'integer',
@@ -98,11 +91,11 @@ export function registerGenerationTools(ctx, deps) {
98
91
  },
99
92
  output_name: {
100
93
  type: 'string',
101
- description: 'Optional file name stem for the saved image (defaults to a slug of the prompt).',
94
+ description: 'Optional file name stem for the saved image.',
102
95
  },
103
96
  output_dir: {
104
97
  type: 'string',
105
- description: 'Optional custom directory to write images into (relative to session cwd or absolute). Defaults to outputDir from settings.',
98
+ description: 'Optional custom directory to write images into.',
106
99
  },
107
100
  style_preset: {
108
101
  type: 'string',
@@ -111,11 +104,11 @@ export function registerGenerationTools(ctx, deps) {
111
104
  palette_colors: {
112
105
  type: 'array',
113
106
  items: { type: 'string' },
114
- description: 'Optional array of hex color codes (#1e1b4b) to constrain and harmonize the color palette.',
107
+ description: 'Optional hex color codes (#1e1b4b) to harmonize palette.',
115
108
  },
116
109
  count: {
117
110
  type: 'integer',
118
- description: 'Number of variations to generate in one call, 1-4 (default 1). Cost scales with count.',
111
+ description: 'Variations count (1-4, default 1).',
119
112
  },
120
113
  prompts: {
121
114
  type: 'array',
@@ -125,60 +118,60 @@ export function registerGenerationTools(ctx, deps) {
125
118
  { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } } },
126
119
  ],
127
120
  },
128
- description: 'Optional list of prompts (strings or {text}); one image is generated per prompt. When set, overrides count and the single prompt.',
121
+ description: 'Optional list of prompts for batch generation.',
129
122
  },
130
123
  negative_prompt: {
131
124
  type: 'string',
132
- description: 'Optional text describing what NOT to draw; sent to providers that support it (ignored otherwise).',
125
+ description: 'Optional text describing what NOT to draw.',
133
126
  },
134
127
  guidance_scale: {
135
128
  type: 'number',
136
- description: 'Optional prompt adherence (e.g. 1-20); sent to providers that support it (ignored otherwise).',
129
+ description: 'Optional prompt adherence (1-20).',
137
130
  },
138
131
  quality: {
139
132
  type: 'string',
140
133
  enum: ['auto', 'low', 'medium', 'high'],
141
- description: 'Optional quality for providers that support it (seedream, gemini). Ignored otherwise.',
134
+ description: 'Optional quality for supported providers.',
142
135
  },
143
136
  style: {
144
137
  type: 'string',
145
- description: 'Optional style preset for providers that support it (seedream, gemini). Ignored otherwise.',
138
+ description: 'Optional style preset for supported providers.',
146
139
  },
147
140
  source_image: {
148
141
  type: 'string',
149
- description: 'Optional path to an image file or an attachment id (sha256:...) to edit instead of drawing from scratch. Only providers that support image editing accept it; others refuse with a clear reason.',
142
+ description: 'Optional source image path or attachment ID for img2img.',
150
143
  },
151
144
  mask: {
152
145
  type: 'string',
153
- description: 'Optional path or attachment id of a mask image (same size as source) to restrict the edit to a region. Only used when source_image is set and the provider supports masks.',
146
+ description: 'Optional inpaint mask path or attachment ID.',
154
147
  },
155
148
  strength: {
156
149
  type: 'number',
157
- description: 'Optional edit strength (0-1) for providers that accept it; higher keeps more of the original.',
150
+ description: 'Optional edit strength (0-1).',
158
151
  },
159
152
  quality_gate: {
160
153
  type: 'boolean',
161
- description: 'Optional override to enable/disable automated quality gate and defect re-rolling for this call.',
154
+ description: 'Optional automated quality gate override.',
162
155
  },
163
156
  force: {
164
157
  type: 'boolean',
165
- description: 'Optional flag to bypass content-addressed cache and force a new API generation.',
158
+ description: 'Bypass cache and force new generation.',
166
159
  },
167
160
  reference_image: {
168
161
  type: 'string',
169
- description: 'Optional reference image (URL, path, or sha256:...) for character or style consistency.',
162
+ description: 'Optional reference image path or URL for consistency.',
170
163
  },
171
164
  reference_strength: {
172
165
  type: 'number',
173
- description: 'Optional strength of reference anchor influence (0.1 to 1.0).',
166
+ description: 'Strength of reference influence (0.1-1.0).',
174
167
  },
175
168
  face_reference: {
176
169
  type: 'string',
177
- description: 'Portrait reference image (URL, local path, or attachment id) for facial identity preservation via FaceID / IP-Adapter (#183).',
170
+ description: 'Portrait reference image for facial identity preservation.',
178
171
  },
179
172
  face_strength: {
180
173
  type: 'number',
181
- description: 'Facial identity similarity weight between 0.1 and 1.0 (default: 0.75).',
174
+ description: 'Facial identity similarity weight (0.1-1.0).',
182
175
  },
183
176
  },
184
177
  output: {
@@ -344,7 +337,7 @@ export function registerGenerationTools(ctx, deps) {
344
337
  // Subscriptions service is optional: without it, subscription providers
345
338
  // gracefully decline while other providers continue functioning.
346
339
  let subscriptionImages
347
- try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch { subscriptionImages = undefined }
340
+ try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
348
341
 
349
342
  const source = args.source_image ? await resolveSource(ctx, exec, args.source_image) : undefined
350
343
  const mask = args.mask ? await resolveSource(ctx, exec, args.mask) : undefined
@@ -358,7 +351,8 @@ export function registerGenerationTools(ctx, deps) {
358
351
  if (faceRefInput) {
359
352
  try {
360
353
  resolvedFaceRef = await resolveSource(ctx, exec, faceRefInput)
361
- } catch {
354
+ } catch (_err) {
355
+ /* non-fatal: face_reference resolve fallback */
362
356
  resolvedFaceRef = { url: faceRefInput }
363
357
  }
364
358
  const facePlan = buildFaceConsistencyPrompt({
@@ -651,5 +645,7 @@ export function registerGenerationTools(ctx, deps) {
651
645
  }),
652
646
  )
653
647
  }, 'dsh-image-gen: tool generate_image')
654
- registerGenerationPackTool(ctx, deps)
648
+ if (!options.coreOnly) {
649
+ registerGenerationPackTool(ctx, deps)
650
+ }
655
651
  }
@@ -7,12 +7,13 @@ import {
7
7
  estimateSharpnessAndVariance,
8
8
  } from '../providers.js'
9
9
 
10
- export function registerInspectTools(ctx, deps) {
10
+ export function registerInspectTools(ctx, deps, options = {}) {
11
11
  const { resolveSource } = deps
12
- ctx.effect(() => {
13
- ctx.tools.register(
14
- defineTool({
15
- name: 'compare_images',
12
+ if (!options.coreOnly) {
13
+ ctx.effect(() => {
14
+ ctx.tools.register(
15
+ defineTool({
16
+ name: 'compare_images',
16
17
  description: 'Compare two images (paths or attachment ids) and report the fraction of differing pixels. Use to check how much an edit changed the original.',
17
18
  parameters: {
18
19
  image_a: { type: 'string', required: true, description: 'Path or attachment id of the first image.' },
@@ -37,6 +38,7 @@ export function registerInspectTools(ctx, deps) {
37
38
  }),
38
39
  )
39
40
  }, 'dsh-image-gen: tool compare_images')
41
+ }
40
42
  ctx.effect(() => {
41
43
  ctx.tools.register(
42
44
  defineTool({
@@ -126,7 +126,7 @@ export function registerStyleMatrixTools(ctx, deps) {
126
126
 
127
127
  const provider = PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal'
128
128
  let subscriptionImages
129
- try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch { subscriptionImages = undefined }
129
+ try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
130
130
 
131
131
  const chain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
132
132
  const quadrantIds = ['A', 'B', 'C', 'D']
@@ -145,7 +145,7 @@ export function registerUiAssetTools(ctx, deps) {
145
145
 
146
146
  const provider = PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal'
147
147
  let subscriptionImages
148
- try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch { subscriptionImages = undefined }
148
+ try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
149
149
 
150
150
  const providers = makeProviders(
151
151
  { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
@@ -39,7 +39,7 @@ export function registerVisionOcrTools(ctx, deps) {
39
39
  },
40
40
  replacements: {
41
41
  type: 'array',
42
- description: 'Array of text replacements: [{ find: "Old Text", replace: "Новый Текст", bbox: [x1,y1,x2,y2] }].',
42
+ description: 'Array of text replacements: [{ find: "Old Text", replace: "New Text", bbox: [x1,y1,x2,y2] }].',
43
43
  items: {
44
44
  type: 'object',
45
45
  additionalProperties: true,
@@ -109,11 +109,7 @@ export function registerVisionOcrTools(ctx, deps) {
109
109
  },
110
110
  },
111
111
  },
112
- render: (result, renderCtx) =>
113
- renderToolOutput(result, renderCtx, {
114
- title: (res) => `Text Replaced (${res.replacedCount || 0} inscriptions)`,
115
- subtitle: (res) => (res.bboxes?.length ? `Detected regions: ${res.bboxes.length}` : ''),
116
- }),
112
+ render: (_args, value) => renderToolOutput(value),
117
113
  },
118
114
  async execute(args, execCtx) {
119
115
  const cfg = live?.value || (typeof live === 'function' ? live() : {})
@@ -145,8 +141,8 @@ export function registerVisionOcrTools(ctx, deps) {
145
141
  if (gRes?.bbox && gRes.bbox.length === 4) {
146
142
  bbox = gRes.bbox
147
143
  }
148
- } catch {
149
- // non-fatal vision_ground fallback
144
+ } catch (_err) {
145
+ /* non-fatal: vision_ground fallback */
150
146
  }
151
147
  }
152
148
  }
@@ -168,7 +164,7 @@ export function registerVisionOcrTools(ctx, deps) {
168
164
  const defaultBox = [150, 420, 850, 520]
169
165
  bboxes.push(defaultBox)
170
166
  overlayItems.push({
171
- text: args.target_language === 'ru' ? 'Заголовок' : 'Localized Text',
167
+ text: 'Localized Text',
172
168
  bbox: defaultBox,
173
169
  color: args.font_color || '#FFFFFF',
174
170
  })
@@ -216,8 +212,8 @@ export function registerVisionOcrTools(ctx, deps) {
216
212
  )
217
213
  inpaintedBytes = inpaintResult.bytes
218
214
  inpaintedUrl = inpaintResult.url
219
- } catch {
220
- // If inpaint provider unavailable, keep source bytes and proceed with overlay
215
+ } catch (_err) {
216
+ /* non-fatal: inpaint provider unavailable; use source bytes */
221
217
  inpaintedBytes = source.bytes
222
218
  }
223
219
 
package/lib/updater.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process'
2
- import { existsSync, readFileSync } from 'node:fs'
2
+ import { existsSync, readFileSync, unlinkSync, statSync } 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'
@@ -10,6 +10,60 @@ import { isLoopbackAddress, isPrivateLanAddress, extractHostName } from './secur
10
10
  * Implements the unified DSH plugin updater specification.
11
11
  */
12
12
 
13
+
14
+ export function isPidAlive(pid) {
15
+ if (!Number.isInteger(pid) || pid <= 0) return false
16
+ try {
17
+ process.kill(pid, 0)
18
+ return true
19
+ } catch (err) {
20
+ return err?.code === 'EPERM'
21
+ }
22
+ }
23
+
24
+ export function checkAndCleanProfileLock(profileDir) {
25
+ if (!profileDir) return { locked: false }
26
+ const lockPath = resolve(profileDir, 'package.json.lock')
27
+ if (!existsSync(lockPath)) return { locked: false }
28
+ let pid = null
29
+ try {
30
+ const raw = readFileSync(lockPath, 'utf8').trim()
31
+ try {
32
+ const parsed = JSON.parse(raw)
33
+ pid = parsed?.pid ?? Number(raw)
34
+ } catch (_e) {
35
+ pid = parseInt(raw, 10)
36
+ }
37
+ } catch (_err) {
38
+ /* lock unreadable */
39
+ }
40
+
41
+ if (Number.isInteger(pid) && pid > 0) {
42
+ if (isPidAlive(pid)) {
43
+ return { locked: true, pid }
44
+ }
45
+ // Stale lock: PID is not running
46
+ try {
47
+ unlinkSync(lockPath)
48
+ } catch (_err) {
49
+ /* non-fatal */
50
+ }
51
+ return { locked: false, staleCleaned: true, pid }
52
+ }
53
+
54
+ // Lockfile without valid PID: check age (> 10 min)
55
+ try {
56
+ const stat = statSync(lockPath)
57
+ if (Date.now() - stat.mtimeMs > 10 * 60_000) {
58
+ unlinkSync(lockPath)
59
+ return { locked: false, staleCleaned: true }
60
+ }
61
+ return { locked: true, pid: null }
62
+ } catch (_err) {
63
+ return { locked: false }
64
+ }
65
+ }
66
+
13
67
  const UPDATE_HEADER = 'x-dsh-plugin-update'
14
68
  const UPDATE_TIMEOUT_MS = 10 * 60_000
15
69
  const VERSION_CACHE_MS = 5 * 60_000
@@ -39,7 +93,7 @@ export function isTrustedUpdateRequest(request) {
39
93
  (isLoopbackAddress(hostName) || isPrivateLanAddress(hostName)) &&
40
94
  url.host.toLowerCase() === host.toLowerCase()
41
95
  )
42
- } catch {
96
+ } catch (_err) {
43
97
  return false
44
98
  }
45
99
  }
@@ -213,7 +267,6 @@ async function installExact(target, packageSpec, options) {
213
267
  '--profile',
214
268
  target.profileName,
215
269
  'add',
216
- '--config.minimumReleaseAge=0',
217
270
  packageSpec,
218
271
  `--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
219
272
  ],
@@ -232,9 +285,15 @@ async function installExact(target, packageSpec, options) {
232
285
  detail = (detail + String(chunk)).slice(-4000)
233
286
  })
234
287
  const timer = setTimeout(() => {
235
- child.kill()
288
+ try {
289
+ child.kill('SIGTERM')
290
+ setTimeout(() => {
291
+ try { child.kill('SIGKILL') } catch (_err) {}
292
+ checkAndCleanProfileLock(target.profileDir)
293
+ }, 1500)
294
+ } catch (_err) {}
236
295
  reject(new Error('Update timed out; use the normal DSH update flow.'))
237
- }, UPDATE_TIMEOUT_MS)
296
+ }, options.timeoutMs ?? UPDATE_TIMEOUT_MS)
238
297
  child.once('error', (error) => {
239
298
  clearTimeout(timer)
240
299
  reject(error)
@@ -290,6 +349,11 @@ export function registerPluginUpdater(ctx, options) {
290
349
  json(response, 409, { error: 'This plugin is already updating.' })
291
350
  return
292
351
  }
352
+ const lockState = checkAndCleanProfileLock(target.profileDir)
353
+ if (lockState.locked) {
354
+ json(response, 409, { error: `Another package installation is currently in progress${lockState.pid ? ` (PID ${lockState.pid})` : ''}.` })
355
+ return
356
+ }
293
357
  installing = true
294
358
  try {
295
359
  const before = await status(options, target)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.11.12",
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).",
3
+ "version": "0.11.14",
4
+ "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers \u2014 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",
7
7
  "dsh",
@@ -69,7 +69,7 @@
69
69
  "@deepseek-ai/schemastery": "^3.18.1"
70
70
  },
71
71
  "scripts": {
72
- "test": "node scripts/build-client.mjs --check && node --check lib/client.js && node --test test/*.test.mjs",
72
+ "test": "node scripts/build-client.mjs --check && node --check lib/client.js && node --import ./test/_setup.mjs --test test/*.test.mjs",
73
73
  "build:client": "node scripts/build-client.mjs",
74
74
  "publish:github": "bash scripts/publish-github.sh",
75
75
  "build:client:check": "node scripts/build-client.mjs --check"