@goodandready/dsh-image-gen 0.11.17 → 0.11.19
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/frontend-assets.js +14 -0
- package/lib/generation-helpers.js +250 -0
- package/lib/provider-utils.js +4 -2
- package/lib/resolve-image.js +14 -4
- package/lib/resolve-source.js +13 -3
- package/lib/tools/anchor.js +5 -0
- package/lib/tools/character-sheet.js +1 -0
- package/lib/tools/diagram.js +1 -0
- package/lib/tools/generation-pack.js +1 -0
- package/lib/tools/generation.js +496 -651
- package/lib/tools/inspect.js +20 -11
- package/lib/tools/style-matrix.js +1 -0
- package/lib/tools/theme-pair.js +1 -0
- package/lib/tools/ui-asset.js +1 -0
- package/lib/tools/vision-ocr.js +1 -0
- package/lib/updater.js +2 -2
- package/package.json +1 -1
package/lib/frontend-assets.js
CHANGED
|
@@ -317,6 +317,20 @@ export function optimizeSvgContent(svgString, options = {}) {
|
|
|
317
317
|
cleaned = cleaned.replace(/\s*(?:xmlns:inkscape|xmlns:sodipodi|xmlns:sketch|inkscape:[a-z-]+|sodipodi:[a-z-]+|sketch:[a-z-]+)="[^"]*"/gi, '')
|
|
318
318
|
cleaned = cleaned.replace(/<(?:metadata|sodipodi:namedview)[\s\S]*?<\/(?:metadata|sodipodi:namedview)>/gi, '')
|
|
319
319
|
|
|
320
|
+
// Security (#374): Strip active/dangerous SVG tags (<script>, <foreignObject>, <iframe>, <embed>, <object>, <applet>)
|
|
321
|
+
cleaned = cleaned.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
322
|
+
cleaned = cleaned.replace(/<script[^>]*\/?>/gi, '')
|
|
323
|
+
cleaned = cleaned.replace(/<foreignObject[\s\S]*?<\/foreignObject>/gi, '')
|
|
324
|
+
cleaned = cleaned.replace(/<foreignObject[^>]*\/?>/gi, '')
|
|
325
|
+
cleaned = cleaned.replace(/<(?:iframe|embed|object|applet)[\s\S]*?<\/(?:iframe|embed|object|applet)>/gi, '')
|
|
326
|
+
cleaned = cleaned.replace(/<(?:iframe|embed|object|applet)[^>]*\/?>/gi, '')
|
|
327
|
+
|
|
328
|
+
// Strip event handler attributes (onload, onerror, onclick, etc.)
|
|
329
|
+
cleaned = cleaned.replace(/\s+on[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
|
|
330
|
+
|
|
331
|
+
// Strip dangerous URI schemes (javascript:, data:text/html, vbscript:) in href/xlink:href/src/action
|
|
332
|
+
cleaned = cleaned.replace(/\s+(?:href|xlink:href|src|action)\s*=\s*(?:"\s*(?:javascript|data:text\/html|vbscript):[^"]*"|'\s*(?:javascript|data:text\/html|vbscript):[^']*')/gi, '')
|
|
333
|
+
|
|
320
334
|
cleaned = cleaned.replace(/\s{2,}/g, ' ')
|
|
321
335
|
cleaned = cleaned.replace(/>\s+</g, '><')
|
|
322
336
|
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// lib/generation-helpers.js
|
|
2
|
+
// Extracted execution and prompt/caching helpers for generate_image tool (#349).
|
|
3
|
+
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
6
|
+
import {
|
|
7
|
+
computeGenerationHash,
|
|
8
|
+
IMAGE_SIZES,
|
|
9
|
+
OUTPUT_FORMATS,
|
|
10
|
+
PROVIDER_KEYS,
|
|
11
|
+
buildSidecar,
|
|
12
|
+
ASPECT_RATIOS,
|
|
13
|
+
embedPngMetadata,
|
|
14
|
+
resolveStylePreset,
|
|
15
|
+
saveAttachmentSafe,
|
|
16
|
+
} from './providers.js'
|
|
17
|
+
import { enhancePrompt } from './prompt-enhancer.js'
|
|
18
|
+
import { buildFaceConsistencyPrompt, getSessionAnchor, applyAnchorPromptHints } from './anchor-helpers.js'
|
|
19
|
+
import { sanitizeNegativePrompt, supportsNegativePrompt } from './negative-sanitizer.js'
|
|
20
|
+
import { recordSpend } from './cost-meter.js'
|
|
21
|
+
import { setCachedGeneration } from './generation-cache.js'
|
|
22
|
+
import {
|
|
23
|
+
readHistory,
|
|
24
|
+
writeHistory,
|
|
25
|
+
pruneHistory,
|
|
26
|
+
} from './history.js'
|
|
27
|
+
|
|
28
|
+
/** Run an array of async task functions with a concurrency cap */
|
|
29
|
+
export async function asyncPool(tasks, concurrency = 3) {
|
|
30
|
+
const results = new Array(tasks.length)
|
|
31
|
+
let nextIdx = 0
|
|
32
|
+
async function worker() {
|
|
33
|
+
while (nextIdx < tasks.length) {
|
|
34
|
+
const idx = nextIdx++
|
|
35
|
+
results[idx] = await tasks[idx]()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker())
|
|
39
|
+
await Promise.all(workers)
|
|
40
|
+
return results
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Validates generation configuration and arguments */
|
|
44
|
+
export function validateGenerationParams(cfg, args) {
|
|
45
|
+
if (cfg.enabled === false) {
|
|
46
|
+
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
47
|
+
}
|
|
48
|
+
const provider = PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal'
|
|
49
|
+
const deliverAs = cfg.deliverAs
|
|
50
|
+
const size = args.aspect_ratio ? 'custom' : (args.image_size ?? cfg.defaultSize)
|
|
51
|
+
if (!args.aspect_ratio && !IMAGE_SIZES.includes(size)) {
|
|
52
|
+
throw new Error(`Invalid image_size "${size}". One of: ${IMAGE_SIZES.join(', ')}`)
|
|
53
|
+
}
|
|
54
|
+
const aspectPixels = args.aspect_ratio ? ASPECT_RATIOS[args.aspect_ratio] : undefined
|
|
55
|
+
const format = args.output_format ?? cfg.defaultFormat
|
|
56
|
+
if (!OUTPUT_FORMATS.includes(format)) {
|
|
57
|
+
throw new Error(`Invalid output_format "${format}". One of: ${OUTPUT_FORMATS.join(', ')}`)
|
|
58
|
+
}
|
|
59
|
+
return { provider, deliverAs, size, aspectPixels, format }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Prepares enhanced prompt, style hints, facial identity, anchors, and sanitized negative prompt */
|
|
63
|
+
export async function prepareGenerationPrompt({ ctx, cfg, args, exec, sessionId, provider, resolveSource }) {
|
|
64
|
+
const enhanced = await enhancePrompt(ctx, cfg, args.prompt, exec.signal, provider)
|
|
65
|
+
const effectiveStyle = args.style || cfg.stylePreset
|
|
66
|
+
const styleInfo = resolveStylePreset(effectiveStyle, args.negative_prompt, args.guidance_scale)
|
|
67
|
+
let effectivePrompt = styleInfo.promptSuffix ? `${enhanced.prompt}, ${styleInfo.promptSuffix}` : enhanced.prompt
|
|
68
|
+
let effectiveNegative = styleInfo.negativePrompt
|
|
69
|
+
const activeAnchor = getSessionAnchor(sessionId)
|
|
70
|
+
let resolvedFaceRef
|
|
71
|
+
|
|
72
|
+
const faceRefInput = args.face_reference || (activeAnchor && (activeAnchor.mode === 'face' || activeAnchor.mode === 'character') ? activeAnchor.image : undefined)
|
|
73
|
+
if (faceRefInput) {
|
|
74
|
+
try {
|
|
75
|
+
resolvedFaceRef = await resolveSource(ctx, exec, faceRefInput)
|
|
76
|
+
} catch (_err) {
|
|
77
|
+
/* non-fatal: face_reference resolve fallback */
|
|
78
|
+
resolvedFaceRef = { url: faceRefInput }
|
|
79
|
+
}
|
|
80
|
+
const facePlan = buildFaceConsistencyPrompt({
|
|
81
|
+
prompt: effectivePrompt,
|
|
82
|
+
faceStrength: args.face_strength || activeAnchor?.strength,
|
|
83
|
+
characterName: activeAnchor?.label,
|
|
84
|
+
})
|
|
85
|
+
effectivePrompt = facePlan.prompt
|
|
86
|
+
if (facePlan.negativePrompt) {
|
|
87
|
+
effectiveNegative = effectiveNegative ? `${effectiveNegative}, ${facePlan.negativePrompt}` : facePlan.negativePrompt
|
|
88
|
+
}
|
|
89
|
+
} else if (args.reference_image) {
|
|
90
|
+
effectivePrompt = `${effectivePrompt}, visual reference anchor`
|
|
91
|
+
} else if (activeAnchor) {
|
|
92
|
+
effectivePrompt = applyAnchorPromptHints(activeAnchor, effectivePrompt)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// #165: Negative prompt sanitizer for diffusion models
|
|
96
|
+
if (supportsNegativePrompt(provider, cfg.model || cfg.customModel)) {
|
|
97
|
+
effectiveNegative = sanitizeNegativePrompt({
|
|
98
|
+
positivePrompt: effectivePrompt,
|
|
99
|
+
userNegative: effectiveNegative,
|
|
100
|
+
autoInjectDefects: true,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
enhanced,
|
|
106
|
+
effectivePrompt,
|
|
107
|
+
effectiveNegative,
|
|
108
|
+
effectiveGuidance: styleInfo.guidanceScale,
|
|
109
|
+
styleInfo,
|
|
110
|
+
activeAnchor,
|
|
111
|
+
resolvedFaceRef,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Saves generated image to disk, sidecar, history, records spend, and builds return structure */
|
|
116
|
+
export async function saveGeneratedImageArtifact({
|
|
117
|
+
ctx,
|
|
118
|
+
exec,
|
|
119
|
+
cfg,
|
|
120
|
+
args,
|
|
121
|
+
gen,
|
|
122
|
+
promptArg,
|
|
123
|
+
jobSeed,
|
|
124
|
+
provider,
|
|
125
|
+
deliverAs,
|
|
126
|
+
size,
|
|
127
|
+
format,
|
|
128
|
+
effectiveNegative,
|
|
129
|
+
effectiveGuidance,
|
|
130
|
+
enhanced,
|
|
131
|
+
slugify,
|
|
132
|
+
estimatedCost,
|
|
133
|
+
totalCount,
|
|
134
|
+
}) {
|
|
135
|
+
const mediaType = gen.mediaType || 'image/png'
|
|
136
|
+
let bytes = gen.bytes
|
|
137
|
+
if (mediaType === 'image/png') {
|
|
138
|
+
bytes = embedPngMetadata(bytes, {
|
|
139
|
+
prompt: promptArg,
|
|
140
|
+
seed: gen.seed,
|
|
141
|
+
provider,
|
|
142
|
+
model: cfg.model || cfg.customModel,
|
|
143
|
+
negative_prompt: effectiveNegative,
|
|
144
|
+
guidance_scale: effectiveGuidance,
|
|
145
|
+
width: gen.width,
|
|
146
|
+
height: gen.height,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
const extension = mediaType === 'image/jpeg' ? 'jpg' : mediaType === 'image/webp' ? 'webp' : 'png'
|
|
150
|
+
const stem = `${slugify(args.output_name || promptArg)}-${Date.now().toString(36)}-${jobSeed}`
|
|
151
|
+
const name = `${stem}.${extension}`
|
|
152
|
+
|
|
153
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes, mediaType, name })
|
|
154
|
+
|
|
155
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
156
|
+
const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
|
|
157
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
158
|
+
await mkdir(outDir, { recursive: true })
|
|
159
|
+
const filePath = path.join(outDir, name)
|
|
160
|
+
await writeFile(filePath, bytes)
|
|
161
|
+
|
|
162
|
+
// Sidecar: metadata written alongside image file, making the output
|
|
163
|
+
// directory self-documenting without external database dependencies.
|
|
164
|
+
await writeFile(
|
|
165
|
+
path.join(outDir, `${stem}.json`),
|
|
166
|
+
JSON.stringify(buildSidecar({
|
|
167
|
+
prompt: promptArg,
|
|
168
|
+
size,
|
|
169
|
+
format,
|
|
170
|
+
seed: gen.seed,
|
|
171
|
+
provider,
|
|
172
|
+
deliverAs,
|
|
173
|
+
width: gen.width || attachment.width,
|
|
174
|
+
height: gen.height || attachment.height,
|
|
175
|
+
mediaType,
|
|
176
|
+
attachmentId: attachment.attachmentId,
|
|
177
|
+
url: deliverAs === 'image' && gen.sourceUrl ? gen.sourceUrl : localUrl,
|
|
178
|
+
cost: gen.cost,
|
|
179
|
+
}), null, 2),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const cacheHash = computeGenerationHash({
|
|
183
|
+
provider,
|
|
184
|
+
model: cfg.model || cfg.customModel,
|
|
185
|
+
prompt: promptArg,
|
|
186
|
+
seed: gen.seed,
|
|
187
|
+
size: args.image_size || cfg.defaultSize,
|
|
188
|
+
style: args.style || cfg.stylePreset,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// #170: Store in content-addressed disk cache
|
|
192
|
+
if (cfg.diskCache !== false) {
|
|
193
|
+
setCachedGeneration(cacheHash, {
|
|
194
|
+
bytes,
|
|
195
|
+
mediaType,
|
|
196
|
+
width: gen.width || attachment.width,
|
|
197
|
+
height: gen.height || attachment.height,
|
|
198
|
+
seed: gen.seed,
|
|
199
|
+
meta: { prompt: promptArg, provider, model: cfg.model || cfg.customModel, cost: gen.cost },
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
const entry = {
|
|
203
|
+
path: filePath,
|
|
204
|
+
prompt: promptArg,
|
|
205
|
+
provider,
|
|
206
|
+
model: cfg.model,
|
|
207
|
+
size,
|
|
208
|
+
format,
|
|
209
|
+
seed: gen.seed,
|
|
210
|
+
cacheHash,
|
|
211
|
+
thumbnailUrl: localUrl,
|
|
212
|
+
width: gen.width || attachment.width,
|
|
213
|
+
height: gen.height || attachment.height,
|
|
214
|
+
mediaType,
|
|
215
|
+
bytes: attachment.bytes,
|
|
216
|
+
createdAt: new Date().toISOString(),
|
|
217
|
+
attachmentId: attachment.attachmentId,
|
|
218
|
+
}
|
|
219
|
+
let current = await readHistory()
|
|
220
|
+
current = await pruneHistory(current, cfg.pruneDays)
|
|
221
|
+
current.unshift(entry)
|
|
222
|
+
if (current.length > (cfg.historyLimit || 50)) current.length = cfg.historyLimit || 50
|
|
223
|
+
await writeHistory(current)
|
|
224
|
+
|
|
225
|
+
// #167: Record spend in cost meter
|
|
226
|
+
recordSpend(gen.cost || (estimatedCost / totalCount), {
|
|
227
|
+
ctx,
|
|
228
|
+
meta: { provider, model: cfg.model || cfg.customModel, prompt: promptArg, seed: gen.seed },
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
path: filePath,
|
|
233
|
+
url: deliverAs === 'image' && gen.sourceUrl ? gen.sourceUrl : localUrl,
|
|
234
|
+
width: gen.width || attachment.width,
|
|
235
|
+
height: gen.height || attachment.height,
|
|
236
|
+
seed: gen.seed,
|
|
237
|
+
prompt: promptArg,
|
|
238
|
+
...(enhanced?.enhanced ? { originalPrompt: args.prompt } : {}),
|
|
239
|
+
cost: gen.cost,
|
|
240
|
+
format: mediaType.replace('image/', ''),
|
|
241
|
+
attachment: {
|
|
242
|
+
attachmentId: attachment.attachmentId,
|
|
243
|
+
mediaType: attachment.mediaType,
|
|
244
|
+
bytes: attachment.bytes,
|
|
245
|
+
width: attachment.width,
|
|
246
|
+
height: attachment.height,
|
|
247
|
+
name: attachment.name,
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
}
|
package/lib/provider-utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeErrorAndLogs } from './security.js'
|
|
1
2
|
// lib/provider-utils.js
|
|
2
3
|
// Shared provider utilities and size/format constants extracted from providers.js (#239)
|
|
3
4
|
|
|
@@ -234,11 +235,12 @@ export function formatErrorMessage(e, providerKey = '') {
|
|
|
234
235
|
msg = msg.replace(/\[object Object\]/g, 'unknown error object').trim()
|
|
235
236
|
}
|
|
236
237
|
|
|
238
|
+
const sanitized = sanitizeErrorAndLogs(msg)
|
|
237
239
|
if (providerKey) {
|
|
238
|
-
return `${providerKey}: ${
|
|
240
|
+
return `${providerKey}: ${sanitized}`
|
|
239
241
|
}
|
|
240
242
|
|
|
241
|
-
return
|
|
243
|
+
return sanitized
|
|
242
244
|
}
|
|
243
245
|
|
|
244
246
|
export function resolveSubscriptionSize(size, aspectPixels, aspectRatio) {
|
package/lib/resolve-image.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, stat } from 'node:fs/promises'
|
|
1
|
+
import { readFile, stat, realpath } from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { estimateSharpnessAndVariance } from './providers.js'
|
|
4
4
|
|
|
@@ -186,14 +186,24 @@ export async function resolveConversationImage(ctx, exec, targetRef) {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
// 3. Local filesystem resolution with strict workspace containment check
|
|
189
|
-
const root =
|
|
189
|
+
const root = path.resolve(sessionCwd ? sessionCwd : process.cwd())
|
|
190
190
|
const absPath = path.resolve(root, effectiveRef)
|
|
191
191
|
const rel = path.relative(root, absPath)
|
|
192
192
|
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
193
193
|
throw new Error(`Access denied: path "${effectiveRef}" is outside allowed workspace directory "${root}"`)
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
const
|
|
196
|
+
const realRoot = await realpath(root).catch(() => root)
|
|
197
|
+
const realTarget = await realpath(absPath).catch(() => null)
|
|
198
|
+
if (!realTarget) {
|
|
199
|
+
throw new Error(`Image file not found: ${effectiveRef}`)
|
|
200
|
+
}
|
|
201
|
+
const relReal = path.relative(realRoot, realTarget)
|
|
202
|
+
if (relReal.startsWith('..') || path.isAbsolute(relReal)) {
|
|
203
|
+
throw new Error(`Access denied: path "${effectiveRef}" points outside allowed workspace directory "${realRoot}"`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const fileStat = await stat(realTarget).catch(() => null)
|
|
197
207
|
if (!fileStat) {
|
|
198
208
|
throw new Error(`Image file not found: ${effectiveRef}`)
|
|
199
209
|
}
|
|
@@ -208,7 +218,7 @@ export async function resolveConversationImage(ctx, exec, targetRef) {
|
|
|
208
218
|
throw new Error(`Image file too large (${(fileStat.size / (1024 * 1024)).toFixed(1)} MB). Maximum allowed size is 50 MB.`)
|
|
209
219
|
}
|
|
210
220
|
|
|
211
|
-
const bytes = await readFile(
|
|
221
|
+
const bytes = await readFile(realTarget)
|
|
212
222
|
validateImageSignature(bytes, effectiveRef)
|
|
213
223
|
const mediaType = detectMediaType(absPath, bytes)
|
|
214
224
|
return {
|
package/lib/resolve-source.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// lib/resolve-source.js — Safe image source resolution for image tools (#317)
|
|
2
2
|
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
import { readFile, stat } from 'node:fs/promises'
|
|
4
|
+
import { readFile, stat, realpath } from 'node:fs/promises'
|
|
5
5
|
import { validateImageSignature } from './resolve-image.js'
|
|
6
6
|
|
|
7
7
|
/** Read source image for editing: filesystem path or attachment id. */
|
|
@@ -41,7 +41,17 @@ export async function resolveSource(ctx, exec, ref) {
|
|
|
41
41
|
throw new Error(`Access denied: path "${ref}" is outside allowed workspace directory "${root}"`)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const
|
|
44
|
+
const realRoot = await realpath(root).catch(() => root)
|
|
45
|
+
const realTarget = await realpath(target).catch(() => null)
|
|
46
|
+
if (!realTarget) {
|
|
47
|
+
throw new Error(`Source image not found: ${ref}`)
|
|
48
|
+
}
|
|
49
|
+
const relReal = path.relative(realRoot, realTarget)
|
|
50
|
+
if (relReal.startsWith('..') || path.isAbsolute(relReal)) {
|
|
51
|
+
throw new Error(`Access denied: path "${ref}" points outside allowed workspace directory "${realRoot}"`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fileStat = await stat(realTarget).catch(() => null)
|
|
45
55
|
if (!fileStat) {
|
|
46
56
|
throw new Error(`Source image not found: ${ref}`)
|
|
47
57
|
}
|
|
@@ -55,7 +65,7 @@ export async function resolveSource(ctx, exec, ref) {
|
|
|
55
65
|
throw new Error(`Source image too large (${(fileStat.size / (1024 * 1024)).toFixed(1)} MB). Maximum allowed size is 50 MB.`)
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
const bytes = await readFile(
|
|
68
|
+
const bytes = await readFile(realTarget)
|
|
59
69
|
validateImageSignature(bytes, ref)
|
|
60
70
|
const ext = path.extname(target).toLowerCase()
|
|
61
71
|
const mediaType = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg'
|
package/lib/tools/anchor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeErrorAndLogs } from '../security.js'
|
|
1
2
|
// anchor.js — set_style_anchor tool (#283).
|
|
2
3
|
// Manages persistent visual style & character reference anchors in session context.
|
|
3
4
|
|
|
@@ -69,6 +70,7 @@ export function registerAnchorTools(ctx, deps) {
|
|
|
69
70
|
},
|
|
70
71
|
isConcurrencySafe: () => true,
|
|
71
72
|
async execute(args, exec) {
|
|
73
|
+
try {
|
|
72
74
|
const cfg = live ? live() : {}
|
|
73
75
|
if (cfg.enabled === false) {
|
|
74
76
|
throw new Error('Image generation plugin is disabled in settings.')
|
|
@@ -121,6 +123,9 @@ export function registerAnchorTools(ctx, deps) {
|
|
|
121
123
|
anchor,
|
|
122
124
|
summary,
|
|
123
125
|
})
|
|
126
|
+
} catch (err) {
|
|
127
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
128
|
+
}
|
|
124
129
|
},
|
|
125
130
|
}),
|
|
126
131
|
)
|
package/lib/tools/diagram.js
CHANGED