@goodandready/dsh-image-gen 0.11.38 → 0.11.40
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/capabilities.js +433 -0
- package/lib/client.js +10 -0
- package/lib/comfy-catalog.js +433 -0
- package/lib/config-schema.js +10 -0
- package/lib/index.js +76 -0
- package/lib/project-profiles.js +234 -0
- package/lib/providers/backends/local.js +35 -1
- package/lib/recipes.js +410 -0
- package/lib/register-tools.js +22 -1
- package/lib/tools/capabilities.js +139 -0
- package/lib/tools/project-profiles.js +139 -0
- package/lib/tools/recipes.js +250 -0
- package/package.json +1 -1
package/lib/register-tools.js
CHANGED
|
@@ -19,6 +19,9 @@ import { registerThemePairTools } from './tools/theme-pair.js'
|
|
|
19
19
|
import { registerCharacterSheetTools } from './tools/character-sheet.js'
|
|
20
20
|
import { registerVisionOcrTools } from './tools/vision-ocr.js'
|
|
21
21
|
import { registerDiagramTools } from './tools/diagram.js'
|
|
22
|
+
import { registerRecipeTools } from './tools/recipes.js'
|
|
23
|
+
import { registerCapabilityTools } from './tools/capabilities.js'
|
|
24
|
+
import { registerProjectProfileTools } from './tools/project-profiles.js'
|
|
22
25
|
import {
|
|
23
26
|
createLiveProgressSession,
|
|
24
27
|
publishLiveProgress,
|
|
@@ -38,6 +41,13 @@ export function registerAllTools(ctx, deps) {
|
|
|
38
41
|
const registeredNames = new Set()
|
|
39
42
|
|
|
40
43
|
const originalToolsRegister = ctx?.tools?.register ? ctx.tools.register.bind(ctx.tools) : null
|
|
44
|
+
const toolMap = new Map()
|
|
45
|
+
const toolDispatcher = async (name, params, exec) => {
|
|
46
|
+
const fn = toolMap.get(name)
|
|
47
|
+
if (!fn) throw new Error(`Tool "${name}" is not registered in current toolset profile`)
|
|
48
|
+
return fn(params, exec)
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
const wrappedTools = originalToolsRegister ? Object.create(ctx.tools, {
|
|
42
52
|
register: {
|
|
43
53
|
value: (def) => {
|
|
@@ -68,6 +78,7 @@ export function registerAllTools(ctx, deps) {
|
|
|
68
78
|
}
|
|
69
79
|
const res = originalToolsRegister(wrappedDef)
|
|
70
80
|
registeredNames.add(def.name)
|
|
81
|
+
toolMap.set(def.name, (p, e) => originalExecute(p, e))
|
|
71
82
|
if (typeof res === 'function') {
|
|
72
83
|
activeDisposers.push(res)
|
|
73
84
|
} else {
|
|
@@ -130,12 +141,22 @@ export function registerAllTools(ctx, deps) {
|
|
|
130
141
|
registerResponsiveTools(targetCtx, deps)
|
|
131
142
|
}
|
|
132
143
|
|
|
133
|
-
// 5. Creative toolset
|
|
144
|
+
// 5. Creative toolset & Recipes (#432, #433)
|
|
134
145
|
if (creative) {
|
|
135
146
|
registerSketchTools(targetCtx, deps)
|
|
136
147
|
registerSpritesheetTools(targetCtx, deps)
|
|
137
148
|
registerPatternTools(targetCtx, deps)
|
|
138
149
|
}
|
|
150
|
+
|
|
151
|
+
if (Boolean(sets.recipes)) {
|
|
152
|
+
registerRecipeTools(targetCtx, { ...deps, toolDispatcher })
|
|
153
|
+
}
|
|
154
|
+
if (Boolean(sets.capabilities)) {
|
|
155
|
+
registerCapabilityTools(targetCtx, deps)
|
|
156
|
+
}
|
|
157
|
+
if (Boolean(sets.profiles)) {
|
|
158
|
+
registerProjectProfileTools(targetCtx, deps)
|
|
159
|
+
}
|
|
139
160
|
}
|
|
140
161
|
|
|
141
162
|
function dispose() {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCapabilityRegistry,
|
|
3
|
+
recommendModelForTask,
|
|
4
|
+
validateModelCapability,
|
|
5
|
+
} from '../capabilities.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registers capability-based model selection tools (#434).
|
|
9
|
+
*/
|
|
10
|
+
export function registerCapabilityTools(ctx, deps = {}) {
|
|
11
|
+
ctx.tools.register({
|
|
12
|
+
name: 'select_image_model',
|
|
13
|
+
description: 'Recommends the best provider and model based on required task capabilities (text rendering, inpainting, face preservation, cost limits).',
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
operations: {
|
|
19
|
+
type: 'array',
|
|
20
|
+
items: { type: 'string' },
|
|
21
|
+
description: 'Required operations, e.g. ["txt2img", "inpaint", "img2img"].',
|
|
22
|
+
},
|
|
23
|
+
textRendering: {
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
description: 'Whether native high-precision text rendering is required.',
|
|
26
|
+
},
|
|
27
|
+
facePreservation: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
description: 'Whether facial identity likeness preservation is required.',
|
|
30
|
+
},
|
|
31
|
+
maskInpainting: {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
description: 'Whether pixel-accurate inpainting mask support is required.',
|
|
34
|
+
},
|
|
35
|
+
referenceConditioning: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
description: 'Whether reference image guidance is required.',
|
|
38
|
+
},
|
|
39
|
+
controlnet: {
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
description: 'Whether structural ControlNet guidance is required.',
|
|
42
|
+
},
|
|
43
|
+
minResolution: {
|
|
44
|
+
type: 'number',
|
|
45
|
+
description: 'Minimum resolution in pixels (e.g. 1024 or 2048).',
|
|
46
|
+
},
|
|
47
|
+
maxCostUSD: {
|
|
48
|
+
type: 'number',
|
|
49
|
+
description: 'Maximum estimated cost in USD per generation.',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
output: {
|
|
54
|
+
render(args, value) {
|
|
55
|
+
if (!value?.ok) {
|
|
56
|
+
return [{ type: 'text', text: `Capability selection: ${value?.message || 'No matching model found.'}` }]
|
|
57
|
+
}
|
|
58
|
+
const rec = value.recommended
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: `Recommended Model: ${rec.displayName} (${rec.provider}/${rec.modelId})\nReason: ${value.recommendationReason}\nEstimated Cost: $${rec.estimatedCostUSD} (Source: ${rec.pricingSource})`,
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
async execute(args) {
|
|
68
|
+
const requirements = {
|
|
69
|
+
operations: args.operations || [],
|
|
70
|
+
textRendering: Boolean(args.textRendering),
|
|
71
|
+
facePreservation: Boolean(args.facePreservation),
|
|
72
|
+
maskInpainting: Boolean(args.maskInpainting),
|
|
73
|
+
referenceConditioning: Boolean(args.referenceConditioning),
|
|
74
|
+
controlnet: Boolean(args.controlnet),
|
|
75
|
+
minResolution: args.minResolution || 0,
|
|
76
|
+
maxCost: args.maxCostUSD || Infinity,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check configured provider list from deps or runtime
|
|
80
|
+
const configuredProviders = deps.cfg?.provider ? [deps.cfg.provider] : null
|
|
81
|
+
return recommendModelForTask(requirements, { configuredProviders })
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
ctx.tools.register({
|
|
86
|
+
name: 'validate_model_features',
|
|
87
|
+
description: 'Pre-flight check if a model or provider supports specific generation features before running the task.',
|
|
88
|
+
parameters: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
properties: {
|
|
92
|
+
provider: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: 'Provider name (fal, gemini, replicate, local, seedream).',
|
|
95
|
+
},
|
|
96
|
+
modelId: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'Specific model identifier.',
|
|
99
|
+
},
|
|
100
|
+
operation: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
description: 'Requested operation (txt2img, img2img, inpaint).',
|
|
103
|
+
},
|
|
104
|
+
requiresMask: {
|
|
105
|
+
type: 'boolean',
|
|
106
|
+
description: 'Whether operation requires inpainting mask.',
|
|
107
|
+
},
|
|
108
|
+
requiresReference: {
|
|
109
|
+
type: 'boolean',
|
|
110
|
+
description: 'Whether operation requires reference image.',
|
|
111
|
+
},
|
|
112
|
+
requiresFacePreservation: {
|
|
113
|
+
type: 'boolean',
|
|
114
|
+
description: 'Whether operation requires face preservation.',
|
|
115
|
+
},
|
|
116
|
+
requiresTextRender: {
|
|
117
|
+
type: 'boolean',
|
|
118
|
+
description: 'Whether operation requires text rendering.',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
required: ['provider'],
|
|
122
|
+
},
|
|
123
|
+
output: {
|
|
124
|
+
render(args, value) {
|
|
125
|
+
return [
|
|
126
|
+
{
|
|
127
|
+
type: 'text',
|
|
128
|
+
text: value.valid
|
|
129
|
+
? `Validation PASSED for ${args.provider}: ${value.model?.displayName}`
|
|
130
|
+
: `Validation FAILED (${value.status}): ${value.reason}`,
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
async execute(args) {
|
|
136
|
+
return validateModelCapability(args.provider, args.modelId, args)
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAllProjectProfiles,
|
|
3
|
+
getProjectProfileById,
|
|
4
|
+
saveCustomProfile,
|
|
5
|
+
deleteCustomProfile,
|
|
6
|
+
resolveEffectiveParams,
|
|
7
|
+
} from '../project-profiles.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registers project visual profile management tools (#435).
|
|
11
|
+
*/
|
|
12
|
+
export function registerProjectProfileTools(ctx, deps = {}) {
|
|
13
|
+
ctx.tools.register({
|
|
14
|
+
name: 'manage_project_profiles',
|
|
15
|
+
description: 'Manages visual project and series profiles (list, get, save, delete) for consistent styling across generation sets.',
|
|
16
|
+
parameters: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
properties: {
|
|
20
|
+
action: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
enum: ['list', 'get', 'save', 'delete'],
|
|
23
|
+
description: 'Action to perform: list all profiles, get by id, save (create/update), or delete.',
|
|
24
|
+
},
|
|
25
|
+
id: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Profile ID for get or delete actions.',
|
|
28
|
+
},
|
|
29
|
+
profile: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
additionalProperties: true,
|
|
32
|
+
description: 'Profile specification object for save action (id, name, palette, stylePrompt, negativePrompt, format, rules).',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['action'],
|
|
36
|
+
},
|
|
37
|
+
output: {
|
|
38
|
+
render(args, value) {
|
|
39
|
+
if (args.action === 'list') {
|
|
40
|
+
const list = Array.isArray(value) ? value : []
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: `Project Profiles (${list.length}):\n` +
|
|
45
|
+
list.map((p) => `- [${p.id}] ${p.name} (v${p.version || '1.0.0'}) ${p.builtin ? '(builtin)' : '(custom)'}`).join('\n'),
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
if (args.action === 'get') {
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: value ? `Profile [${value.id}] ${value.name}:\nStyle: "${value.stylePrompt || ''}"\nPalette: ${(value.palette || []).join(', ')}` : 'Profile not found.',
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
if (args.action === 'save') {
|
|
58
|
+
return [{ type: 'text', text: `Saved profile [${value?.id}] ${value?.name} successfully.` }]
|
|
59
|
+
}
|
|
60
|
+
if (args.action === 'delete') {
|
|
61
|
+
return [{ type: 'text', text: value ? `Deleted profile ${args.id}.` : `Profile ${args.id} not found or cannot be deleted.` }]
|
|
62
|
+
}
|
|
63
|
+
return [{ type: 'text', text: JSON.stringify(value) }]
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
async execute(args) {
|
|
67
|
+
const storagePath = deps.profilesPath || null
|
|
68
|
+
if (args.action === 'list') {
|
|
69
|
+
return getAllProjectProfiles(storagePath)
|
|
70
|
+
}
|
|
71
|
+
if (args.action === 'get') {
|
|
72
|
+
return getProjectProfileById(args.id, storagePath)
|
|
73
|
+
}
|
|
74
|
+
if (args.action === 'save') {
|
|
75
|
+
return saveCustomProfile(args.profile, storagePath)
|
|
76
|
+
}
|
|
77
|
+
if (args.action === 'delete') {
|
|
78
|
+
return deleteCustomProfile(args.id, storagePath)
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Unknown action: ${args.action}`)
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
ctx.tools.register({
|
|
85
|
+
name: 'resolve_project_profile_params',
|
|
86
|
+
description: 'Calculates effective generation parameters by cascading request overrides over a project visual profile and system defaults.',
|
|
87
|
+
parameters: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
properties: {
|
|
91
|
+
profileId: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: 'Identifier of the project visual profile.',
|
|
94
|
+
},
|
|
95
|
+
prompt: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: 'Specific image request prompt.',
|
|
98
|
+
},
|
|
99
|
+
negativePrompt: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'Specific negative prompt overrides.',
|
|
102
|
+
},
|
|
103
|
+
size: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Specific dimensions (e.g. 1024x1024 or 1280x720).',
|
|
106
|
+
},
|
|
107
|
+
format: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'Output format (webp, png, jpeg).',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ['profileId'],
|
|
113
|
+
},
|
|
114
|
+
output: {
|
|
115
|
+
render(args, value) {
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
type: 'text',
|
|
119
|
+
text: `Effective Parameters for Profile [${value?.appliedProfile?.name || args.profileId}]:\n` +
|
|
120
|
+
`Prompt: "${value?.effectiveParams?.prompt || ''}"\n` +
|
|
121
|
+
`Negative: "${value?.effectiveParams?.negativePrompt || ''}"\n` +
|
|
122
|
+
`Dimensions: ${value?.effectiveParams?.size} (${value?.effectiveParams?.format})\n` +
|
|
123
|
+
`Provenance: ${JSON.stringify(value?.provenance || {})}`,
|
|
124
|
+
},
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
async execute(args) {
|
|
129
|
+
const storagePath = deps.profilesPath || null
|
|
130
|
+
const requestParams = {
|
|
131
|
+
prompt: args.prompt,
|
|
132
|
+
negativePrompt: args.negativePrompt,
|
|
133
|
+
size: args.size,
|
|
134
|
+
format: args.format,
|
|
135
|
+
}
|
|
136
|
+
return resolveEffectiveParams(requestParams, args.profileId, storagePath)
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// lib/tools/recipes.js — Tools for running recipes & validating ComfyUI workflows (#432, #433)
|
|
2
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
3
|
+
import {
|
|
4
|
+
getAllRecipes,
|
|
5
|
+
getRecipe,
|
|
6
|
+
saveCustomRecipe,
|
|
7
|
+
deleteCustomRecipe,
|
|
8
|
+
executeRecipe,
|
|
9
|
+
} from '../recipes.js'
|
|
10
|
+
import {
|
|
11
|
+
getComfyTemplates,
|
|
12
|
+
getComfyTemplate,
|
|
13
|
+
createComfyDependencyValidator,
|
|
14
|
+
prepareComfyWorkflowFromTemplate,
|
|
15
|
+
} from '../comfy-catalog.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Registers recipe execution and workflow catalog tools.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} ctx cordis context
|
|
21
|
+
* @param {object} deps shared runtime dependencies
|
|
22
|
+
*/
|
|
23
|
+
export function registerRecipeTools(ctx, deps) {
|
|
24
|
+
const { config, toolDispatcher, assertBudget } = deps
|
|
25
|
+
|
|
26
|
+
// 1. run_image_recipe (#432)
|
|
27
|
+
ctx.tools.register({
|
|
28
|
+
name: 'run_image_recipe',
|
|
29
|
+
description: 'Executes a repeatable sequential image recipe pipeline (e.g. product_card, social_promo, vector_asset, character_pipeline). Runs each step, validates budget and prerequisites, and provides intermediate and final results.',
|
|
30
|
+
parameters: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
required: ['recipe_id'],
|
|
34
|
+
properties: {
|
|
35
|
+
recipe_id: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Identifier of the recipe (e.g. "product_card", "social_promo", "vector_asset", "character_pipeline", or custom recipe ID).',
|
|
38
|
+
},
|
|
39
|
+
inputs: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
additionalProperties: true,
|
|
42
|
+
description: 'Input arguments required by the recipe (e.g. prompt, image, aspect, scale, character_name).',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
output: {
|
|
47
|
+
render(args, value) {
|
|
48
|
+
const steps = value?.completedSteps || []
|
|
49
|
+
const lines = [
|
|
50
|
+
`### Recipe: ${value?.recipeName || 'Recipe'} (${value?.recipeId || ''})`,
|
|
51
|
+
`Steps completed: ${steps.length}/${value?.stepsCount || 0} (Est. cost: $${value?.totalEstimatedCost || 0})`,
|
|
52
|
+
'',
|
|
53
|
+
]
|
|
54
|
+
for (const step of steps) {
|
|
55
|
+
lines.push(`- **Step ${step.stepIndex + 1}: ${step.tool}** (${step.durationMs}ms) — ${step.description}`)
|
|
56
|
+
}
|
|
57
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
execute: async (args) => {
|
|
61
|
+
const { recipe_id, inputs = {} } = args || {}
|
|
62
|
+
if (!recipe_id) throw new Error('recipe_id is required')
|
|
63
|
+
|
|
64
|
+
const res = await executeRecipe(recipe_id, inputs, {
|
|
65
|
+
toolDispatcher,
|
|
66
|
+
assertBudget,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return res
|
|
70
|
+
},
|
|
71
|
+
render(args, value) {
|
|
72
|
+
const steps = value?.completedSteps || []
|
|
73
|
+
const lines = [
|
|
74
|
+
`### Recipe: ${value?.recipeName || 'Recipe'} (${value?.recipeId || ''})`,
|
|
75
|
+
`Steps completed: ${steps.length}/${value?.stepsCount || 0} (Est. cost: $${value?.totalEstimatedCost || 0})`,
|
|
76
|
+
'',
|
|
77
|
+
]
|
|
78
|
+
for (const step of steps) {
|
|
79
|
+
lines.push(`- **Step ${step.stepIndex + 1}: ${step.tool}** (${step.durationMs}ms) — ${step.description}`)
|
|
80
|
+
}
|
|
81
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
82
|
+
},
|
|
83
|
+
isConcurrencySafe: () => false,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// 2. manage_image_recipes (#432)
|
|
87
|
+
ctx.tools.register({
|
|
88
|
+
name: 'manage_image_recipes',
|
|
89
|
+
description: 'Lists, inspects, saves, or deletes sequential image generation recipes.',
|
|
90
|
+
parameters: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
required: ['action'],
|
|
94
|
+
properties: {
|
|
95
|
+
action: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
enum: ['list', 'get', 'save', 'delete'],
|
|
98
|
+
description: 'Recipe management action.',
|
|
99
|
+
},
|
|
100
|
+
recipe_id: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
description: 'Recipe ID (required for get and delete).',
|
|
103
|
+
},
|
|
104
|
+
recipe: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
additionalProperties: true,
|
|
107
|
+
description: 'Complete recipe object to save (required for action="save"). Must specify id, name, inputs, and steps.',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
output: {
|
|
112
|
+
render(args, value) {
|
|
113
|
+
if (args?.action === 'list') {
|
|
114
|
+
const list = value?.recipes || []
|
|
115
|
+
const lines = [`Found ${list.length} available image recipes:`, '']
|
|
116
|
+
for (const r of list) {
|
|
117
|
+
lines.push(`- **${r.id}** (${r.name}): ${r.description} [${r.steps?.length || 0} steps]`)
|
|
118
|
+
}
|
|
119
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
120
|
+
}
|
|
121
|
+
return [{ type: 'text', text: JSON.stringify(value || {}, null, 2) }]
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
execute: async (args) => {
|
|
125
|
+
const { action, recipe_id, recipe } = args || {}
|
|
126
|
+
if (action === 'list') {
|
|
127
|
+
return { ok: true, recipes: getAllRecipes() }
|
|
128
|
+
}
|
|
129
|
+
if (action === 'get') {
|
|
130
|
+
if (!recipe_id) throw new Error('recipe_id is required for action="get"')
|
|
131
|
+
const found = getRecipe(recipe_id)
|
|
132
|
+
if (!found) throw new Error(`Recipe "${recipe_id}" not found`)
|
|
133
|
+
return { ok: true, recipe: found }
|
|
134
|
+
}
|
|
135
|
+
if (action === 'save') {
|
|
136
|
+
if (!recipe) throw new Error('recipe object is required for action="save"')
|
|
137
|
+
const saved = saveCustomRecipe(recipe)
|
|
138
|
+
return { ok: true, recipe: saved, message: `Recipe "${saved.id}" saved successfully` }
|
|
139
|
+
}
|
|
140
|
+
if (action === 'delete') {
|
|
141
|
+
if (!recipe_id) throw new Error('recipe_id is required for action="delete"')
|
|
142
|
+
const deleted = deleteCustomRecipe(recipe_id)
|
|
143
|
+
if (!deleted) throw new Error(`Custom recipe "${recipe_id}" not found or cannot be deleted`)
|
|
144
|
+
return { ok: true, deleted: true, recipe_id }
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Unsupported action "${action}"`)
|
|
147
|
+
},
|
|
148
|
+
render(args, value) {
|
|
149
|
+
if (args?.action === 'list') {
|
|
150
|
+
const list = value?.recipes || []
|
|
151
|
+
const lines = [`Found ${list.length} available image recipes:`, '']
|
|
152
|
+
for (const r of list) {
|
|
153
|
+
lines.push(`- **${r.id}** (${r.name}): ${r.description} [${r.steps?.length || 0} steps]`)
|
|
154
|
+
}
|
|
155
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
156
|
+
}
|
|
157
|
+
return [{ type: 'text', text: JSON.stringify(value || {}, null, 2) }]
|
|
158
|
+
},
|
|
159
|
+
isConcurrencySafe: () => true,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// 3. validate_comfy_workflow (#433)
|
|
163
|
+
ctx.tools.register({
|
|
164
|
+
name: 'validate_comfy_workflow',
|
|
165
|
+
description: 'Validates a vetted ComfyUI template or custom workflow graph against the connected ComfyUI server dependencies and models.',
|
|
166
|
+
parameters: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
additionalProperties: false,
|
|
169
|
+
properties: {
|
|
170
|
+
template_id: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: 'Identifier of the vetted ComfyUI template (e.g. "txt2img_sdxl", "img2img_sdxl", "face_instantid", "upscale_esrgan").',
|
|
173
|
+
},
|
|
174
|
+
base_url: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
description: 'ComfyUI server address (defaults to configured localBaseURL).',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
output: {
|
|
181
|
+
render(args, value) {
|
|
182
|
+
if (value?.templateId) {
|
|
183
|
+
const status = value.ok ? 'READY' : 'MISSING DEPENDENCIES'
|
|
184
|
+
const lines = [
|
|
185
|
+
`### ComfyUI Template: ${value.templateName} [${status}]`,
|
|
186
|
+
`Server: ${value.serverUrl}`,
|
|
187
|
+
]
|
|
188
|
+
if (value.missingNodes?.length) lines.push(`- Missing Nodes: ${value.missingNodes.join(', ')}`)
|
|
189
|
+
if (value.missingModels?.length) lines.push(`- Missing Models: ${value.missingModels.join(', ')}`)
|
|
190
|
+
if (value.ok) lines.push('All required custom nodes and model assets are available on the ComfyUI host.')
|
|
191
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
192
|
+
}
|
|
193
|
+
const list = value?.templates || []
|
|
194
|
+
const lines = [`Vetted ComfyUI Templates (${list.length}):`, '']
|
|
195
|
+
for (const t of list) lines.push(`- **${t.id}**: ${t.name} (${t.description})`)
|
|
196
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
execute: async (args) => {
|
|
200
|
+
const { template_id, base_url } = args || {}
|
|
201
|
+
const targetBase = base_url || config?.localBaseURL || 'http://127.0.0.1:8188'
|
|
202
|
+
|
|
203
|
+
if (template_id) {
|
|
204
|
+
const template = getComfyTemplate(template_id)
|
|
205
|
+
if (!template) {
|
|
206
|
+
throw new Error(`ComfyUI template "${template_id}" not found. Available: ${getComfyTemplates().map((t) => t.id).join(', ')}`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const validator = createComfyDependencyValidator()
|
|
210
|
+
const check = await validator(targetBase, template)
|
|
211
|
+
return {
|
|
212
|
+
ok: check.ok,
|
|
213
|
+
templateId: template.id,
|
|
214
|
+
templateName: template.name,
|
|
215
|
+
requiredNodes: template.requiredNodes,
|
|
216
|
+
requiredModels: template.requiredModels,
|
|
217
|
+
missingNodes: check.missingNodes,
|
|
218
|
+
missingModels: check.missingModels,
|
|
219
|
+
serverUrl: targetBase,
|
|
220
|
+
error: check.error,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// If no template specified, return catalog summary
|
|
225
|
+
return {
|
|
226
|
+
ok: true,
|
|
227
|
+
templates: getComfyTemplates(),
|
|
228
|
+
serverUrl: targetBase,
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
render(args, value) {
|
|
232
|
+
if (value?.templateId) {
|
|
233
|
+
const status = value.ok ? 'READY' : 'MISSING DEPENDENCIES'
|
|
234
|
+
const lines = [
|
|
235
|
+
`### ComfyUI Template: ${value.templateName} [${status}]`,
|
|
236
|
+
`Server: ${value.serverUrl}`,
|
|
237
|
+
]
|
|
238
|
+
if (value.missingNodes?.length) lines.push(`- Missing Nodes: ${value.missingNodes.join(', ')}`)
|
|
239
|
+
if (value.missingModels?.length) lines.push(`- Missing Models: ${value.missingModels.join(', ')}`)
|
|
240
|
+
if (value.ok) lines.push('All required custom nodes and model assets are available on the ComfyUI host.')
|
|
241
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
242
|
+
}
|
|
243
|
+
const list = value?.templates || []
|
|
244
|
+
const lines = [`Vetted ComfyUI Templates (${list.length}):`, '']
|
|
245
|
+
for (const t of list) lines.push(`- **${t.id}**: ${t.name} (${t.description})`)
|
|
246
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
247
|
+
},
|
|
248
|
+
isConcurrencySafe: () => true,
|
|
249
|
+
})
|
|
250
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.40",
|
|
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",
|