@goodandready/dsh-image-gen 0.11.4 → 0.11.5
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 +141 -56
- package/lib/commands.js +176 -0
- package/lib/history.js +38 -4
- package/lib/index.js +67 -7
- package/lib/resolve-image.js +48 -8
- package/lib/vault.js +44 -5
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -50,6 +50,16 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
51
51
|
}))
|
|
52
52
|
|
|
53
|
+
|
|
54
|
+
function resolveTranslator(props, ctx) {
|
|
55
|
+
if (typeof props?.t === 'function') return props.t
|
|
56
|
+
const loc = ctx?.locale || props?.ctx?.locale
|
|
57
|
+
if (loc) {
|
|
58
|
+
if (typeof loc.t === 'function') return (k, def) => loc.t(NS + '.' + k) || loc.t(k) || def || k
|
|
59
|
+
if (typeof loc.get === 'function') return (k, def) => loc.get(NS + '.' + k) || loc.get(k) || def || k
|
|
60
|
+
}
|
|
61
|
+
return (k, def) => def || k
|
|
62
|
+
}
|
|
53
63
|
// -------------------------------------------------------------- CSS & Design System
|
|
54
64
|
function ensureCss() {
|
|
55
65
|
if (typeof document === 'undefined') return
|
|
@@ -2519,6 +2529,23 @@ window.__ModuleLoader__.load({
|
|
|
2519
2529
|
'studio.parameters': 'Generation Parameters',
|
|
2520
2530
|
'studio.generate': 'Generate in Chat',
|
|
2521
2531
|
'studio.recent': 'Recent Generations',
|
|
2532
|
+
'studio.prompt': 'Prompt:',
|
|
2533
|
+
'studio.promptPlaceholder': 'Describe your vision...',
|
|
2534
|
+
'studio.stylePreset': 'Style Preset:',
|
|
2535
|
+
'studio.aspectRatio': 'Aspect Ratio:',
|
|
2536
|
+
'studio.provider': 'Provider:',
|
|
2537
|
+
'studio.seed': 'Seed (optional):',
|
|
2538
|
+
'studio.random': 'Random',
|
|
2539
|
+
'studio.randomizeSeed': 'Randomize seed',
|
|
2540
|
+
'studio.fullscreen': 'Fullscreen',
|
|
2541
|
+
'studio.exitFullscreen': 'Exit Fullscreen',
|
|
2542
|
+
'studio.copyMarkdown': 'Copy Markdown',
|
|
2543
|
+
'studio.copyHtml': 'Copy HTML',
|
|
2544
|
+
'studio.download': 'Download',
|
|
2545
|
+
'studio.emptyCanvas': 'Select an image from the recent strip below or click Generate.',
|
|
2546
|
+
'studio.copiedCommand': 'Copied generation command to clipboard!',
|
|
2547
|
+
'studio.copiedSnippet': 'Copied snippet to clipboard!',
|
|
2548
|
+
|
|
2522
2549
|
'vault.title': 'Asset Browser & Vault',
|
|
2523
2550
|
'vault.search': 'Search prompt or tags...',
|
|
2524
2551
|
'vault.loadMore': 'Load more assets',
|
|
@@ -2528,6 +2555,19 @@ window.__ModuleLoader__.load({
|
|
|
2528
2555
|
'vault.insert': 'Insert in Chat',
|
|
2529
2556
|
'vault.download': 'Download',
|
|
2530
2557
|
'vault.delete': 'Delete',
|
|
2558
|
+
'vault.allProviders': 'All Providers',
|
|
2559
|
+
'vault.allRatios': 'All Ratios',
|
|
2560
|
+
'vault.newest': 'Newest',
|
|
2561
|
+
'vault.oldest': 'Oldest',
|
|
2562
|
+
'vault.loading': 'Loading...',
|
|
2563
|
+
'vault.confirmDelete': 'Are you sure you want to delete this asset?',
|
|
2564
|
+
'vault.deleted': 'Asset deleted',
|
|
2565
|
+
'vault.deleteFailed': 'Failed to delete asset',
|
|
2566
|
+
'vault.copiedMarkdown': 'Markdown snippet copied to clipboard!',
|
|
2567
|
+
'vault.copiedPrompt': 'Prompt & seed copied to clipboard!',
|
|
2568
|
+
'vault.noPrompt': 'No prompt',
|
|
2569
|
+
'vault.seed': 'Seed',
|
|
2570
|
+
'vault.provider': 'Provider',
|
|
2531
2571
|
}
|
|
2532
2572
|
|
|
2533
2573
|
const zh = {
|
|
@@ -2754,6 +2794,23 @@ window.__ModuleLoader__.load({
|
|
|
2754
2794
|
'studio.parameters': '生成参数配置',
|
|
2755
2795
|
'studio.generate': '在对话中生成',
|
|
2756
2796
|
'studio.recent': '最近生成历史',
|
|
2797
|
+
'studio.prompt': '提示词:',
|
|
2798
|
+
'studio.promptPlaceholder': '描述你想创作的画面...',
|
|
2799
|
+
'studio.stylePreset': '风格预设:',
|
|
2800
|
+
'studio.aspectRatio': '宽高比:',
|
|
2801
|
+
'studio.provider': '生成引擎:',
|
|
2802
|
+
'studio.seed': '随机种子 (可选):',
|
|
2803
|
+
'studio.random': '随机',
|
|
2804
|
+
'studio.randomizeSeed': '随机生成种子',
|
|
2805
|
+
'studio.fullscreen': '全屏',
|
|
2806
|
+
'studio.exitFullscreen': '退出全屏',
|
|
2807
|
+
'studio.copyMarkdown': '复制 Markdown',
|
|
2808
|
+
'studio.copyHtml': '复制 HTML',
|
|
2809
|
+
'studio.download': '下载图像',
|
|
2810
|
+
'studio.emptyCanvas': '从下方最近生成列表中选择图像,或点击生成。',
|
|
2811
|
+
'studio.copiedCommand': '已将生成指令复制到剪贴板!',
|
|
2812
|
+
'studio.copiedSnippet': '代码片段已复制到剪贴板!',
|
|
2813
|
+
|
|
2757
2814
|
'vault.title': '图像素材库与浏览器',
|
|
2758
2815
|
'vault.search': '搜索提示词或标签...',
|
|
2759
2816
|
'vault.loadMore': '加载更多素材',
|
|
@@ -2763,6 +2820,19 @@ window.__ModuleLoader__.load({
|
|
|
2763
2820
|
'vault.insert': '插入到当前对话',
|
|
2764
2821
|
'vault.download': '下载图像',
|
|
2765
2822
|
'vault.delete': '删除',
|
|
2823
|
+
'vault.allProviders': '所有引擎',
|
|
2824
|
+
'vault.allRatios': '所有比例',
|
|
2825
|
+
'vault.newest': '最新',
|
|
2826
|
+
'vault.oldest': '最早',
|
|
2827
|
+
'vault.loading': '加载中...',
|
|
2828
|
+
'vault.confirmDelete': '确定要删除此素材吗?',
|
|
2829
|
+
'vault.deleted': '素材已删除',
|
|
2830
|
+
'vault.deleteFailed': '删除素材失败',
|
|
2831
|
+
'vault.copiedMarkdown': 'Markdown 片段已复制到剪贴板!',
|
|
2832
|
+
'vault.copiedPrompt': '提示词与种子已复制到剪贴板!',
|
|
2833
|
+
'vault.noPrompt': '无提示词',
|
|
2834
|
+
'vault.seed': '种子',
|
|
2835
|
+
'vault.provider': '引擎',
|
|
2766
2836
|
}
|
|
2767
2837
|
|
|
2768
2838
|
// -------------------------------------------------------------- Registration & Apply
|
|
@@ -2882,7 +2952,20 @@ window.__ModuleLoader__.load({
|
|
|
2882
2952
|
}
|
|
2883
2953
|
|
|
2884
2954
|
function apply(ctx) {
|
|
2885
|
-
if (ctx.locale && typeof ctx.locale.
|
|
2955
|
+
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
2956
|
+
try {
|
|
2957
|
+
if (typeof ctx.effect === 'function') {
|
|
2958
|
+
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-image-gen: locale')
|
|
2959
|
+
} else {
|
|
2960
|
+
ctx.locale.register(NS, { en, zh })
|
|
2961
|
+
}
|
|
2962
|
+
} catch (_) {
|
|
2963
|
+
try {
|
|
2964
|
+
ctx.locale.register(NS, 'en', en)
|
|
2965
|
+
ctx.locale.register(NS, 'zh', zh)
|
|
2966
|
+
} catch (_) { /* ignore fallback locale register error */ }
|
|
2967
|
+
}
|
|
2968
|
+
} else if (ctx.locale && typeof ctx.locale.define === 'function') {
|
|
2886
2969
|
ctx.locale.define('en', NS, en)
|
|
2887
2970
|
ctx.locale.define('zh', NS, zh)
|
|
2888
2971
|
}
|
|
@@ -3068,9 +3151,10 @@ window.__ModuleLoader__.load({
|
|
|
3068
3151
|
)
|
|
3069
3152
|
)
|
|
3070
3153
|
}
|
|
3071
|
-
// -------------------------------------------------------------- Asset Vault View (#159)
|
|
3154
|
+
// -------------------------------------------------------------- Asset Vault View (#159, #304)
|
|
3072
3155
|
function AssetVaultView(props) {
|
|
3073
3156
|
const { ctx, onSelectAsset } = props
|
|
3157
|
+
const t = resolveTranslator(props, ctx)
|
|
3074
3158
|
const [items, setItems] = react.useState([])
|
|
3075
3159
|
const [loading, setLoading] = react.useState(false)
|
|
3076
3160
|
const [q, setQ] = react.useState('')
|
|
@@ -3115,7 +3199,7 @@ window.__ModuleLoader__.load({
|
|
|
3115
3199
|
}, [q, provider, aspect, sort])
|
|
3116
3200
|
|
|
3117
3201
|
const handleDelete = (id) => {
|
|
3118
|
-
if (!window.confirm('Are you sure you want to delete this asset?')) return
|
|
3202
|
+
if (!window.confirm(t('vault.confirmDelete') || 'Are you sure you want to delete this asset?')) return
|
|
3119
3203
|
fetch('/dsh-image-gen/vault?id=' + encodeURIComponent(id), { method: 'DELETE' })
|
|
3120
3204
|
.then((r) => r.json())
|
|
3121
3205
|
.then((res) => {
|
|
@@ -3124,17 +3208,17 @@ window.__ModuleLoader__.load({
|
|
|
3124
3208
|
if (selected && (selected.id === id || selected.attachmentId === id)) {
|
|
3125
3209
|
setSelected(null)
|
|
3126
3210
|
}
|
|
3127
|
-
showFeedback('Asset deleted')
|
|
3211
|
+
showFeedback(t('vault.deleted') || 'Asset deleted')
|
|
3128
3212
|
}
|
|
3129
3213
|
})
|
|
3130
|
-
.catch(() => showFeedback('Failed to delete asset'))
|
|
3214
|
+
.catch(() => showFeedback(t('vault.deleteFailed') || 'Failed to delete asset'))
|
|
3131
3215
|
}
|
|
3132
3216
|
|
|
3133
3217
|
const handleInsert = (item) => {
|
|
3134
3218
|
const link = ' + ')'
|
|
3135
3219
|
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
3136
3220
|
navigator.clipboard.writeText(link)
|
|
3137
|
-
showFeedback('Markdown snippet copied to clipboard!')
|
|
3221
|
+
showFeedback(t('vault.copiedMarkdown') || 'Markdown snippet copied to clipboard!')
|
|
3138
3222
|
}
|
|
3139
3223
|
}
|
|
3140
3224
|
|
|
@@ -3146,7 +3230,7 @@ window.__ModuleLoader__.load({
|
|
|
3146
3230
|
const cmd = '/image ' + (item.prompt || '') + (item.seed !== undefined ? ' --seed ' + item.seed : '')
|
|
3147
3231
|
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
3148
3232
|
navigator.clipboard.writeText(cmd)
|
|
3149
|
-
showFeedback('Prompt & seed copied to clipboard!')
|
|
3233
|
+
showFeedback(t('vault.copiedPrompt') || 'Prompt & seed copied to clipboard!')
|
|
3150
3234
|
}
|
|
3151
3235
|
}
|
|
3152
3236
|
|
|
@@ -3160,7 +3244,7 @@ window.__ModuleLoader__.load({
|
|
|
3160
3244
|
type: 'text',
|
|
3161
3245
|
className: 'ig-input',
|
|
3162
3246
|
style: { flex: '1 1 200px', padding: '6px 12px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
|
|
3163
|
-
placeholder: 'Search prompt or tags...',
|
|
3247
|
+
placeholder: t('vault.search') || 'Search prompt or tags...',
|
|
3164
3248
|
value: q,
|
|
3165
3249
|
onChange: (e) => setQ(e.target.value),
|
|
3166
3250
|
}),
|
|
@@ -3172,7 +3256,7 @@ window.__ModuleLoader__.load({
|
|
|
3172
3256
|
onChange: (e) => setProvider(e.target.value),
|
|
3173
3257
|
style: { padding: '6px 10px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
|
|
3174
3258
|
},
|
|
3175
|
-
react.createElement('option', { value: 'all' }, 'All Providers'),
|
|
3259
|
+
react.createElement('option', { value: 'all' }, t('vault.allProviders') || 'All Providers'),
|
|
3176
3260
|
react.createElement('option', { value: 'fal' }, 'FAL.ai'),
|
|
3177
3261
|
react.createElement('option', { value: 'openai' }, 'OpenAI'),
|
|
3178
3262
|
react.createElement('option', { value: 'local' }, 'Local (ComfyUI)')
|
|
@@ -3185,7 +3269,7 @@ window.__ModuleLoader__.load({
|
|
|
3185
3269
|
onChange: (e) => setAspect(e.target.value),
|
|
3186
3270
|
style: { padding: '6px 10px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
|
|
3187
3271
|
},
|
|
3188
|
-
react.createElement('option', { value: 'all' }, 'All Ratios'),
|
|
3272
|
+
react.createElement('option', { value: 'all' }, t('vault.allRatios') || 'All Ratios'),
|
|
3189
3273
|
react.createElement('option', { value: '1:1' }, '1:1 Square'),
|
|
3190
3274
|
react.createElement('option', { value: '16:9' }, '16:9 Landscape'),
|
|
3191
3275
|
react.createElement('option', { value: '9:16' }, '9:16 Portrait'),
|
|
@@ -3200,12 +3284,12 @@ window.__ModuleLoader__.load({
|
|
|
3200
3284
|
onClick: () => setSort(sort === 'newest' ? 'oldest' : 'newest'),
|
|
3201
3285
|
style: { border: '1px solid var(--dsw-alias-border-l2)', padding: '6px 10px', borderRadius: '6px', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer' },
|
|
3202
3286
|
},
|
|
3203
|
-
sort === 'newest' ? '↓ Newest' : '↑ Oldest'
|
|
3287
|
+
sort === 'newest' ? ('↓ ' + (t('vault.newest') || 'Newest')) : ('↑ ' + (t('vault.oldest') || 'Oldest'))
|
|
3204
3288
|
)
|
|
3205
3289
|
),
|
|
3206
3290
|
feedback && react.createElement('div', { style: { padding: '6px 12px', borderRadius: '6px', background: 'color-mix(in srgb, var(--dsw-alias-state-brand-primary) 15%, transparent)', color: 'var(--dsw-alias-state-brand-primary)', fontSize: '12px' } }, feedback),
|
|
3207
3291
|
items.length === 0 && !loading
|
|
3208
|
-
? react.createElement('div', { style: { padding: '32px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, 'No assets found in vault.')
|
|
3292
|
+
? react.createElement('div', { style: { padding: '32px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, t('vault.empty') || 'No assets found in vault.')
|
|
3209
3293
|
: react.createElement(
|
|
3210
3294
|
'div',
|
|
3211
3295
|
{ className: 'ig-vault-grid' },
|
|
@@ -3222,19 +3306,19 @@ window.__ModuleLoader__.load({
|
|
|
3222
3306
|
react.createElement(
|
|
3223
3307
|
'div',
|
|
3224
3308
|
{ className: 'ig-vault-body' },
|
|
3225
|
-
react.createElement('div', { className: 'ig-vault-prompt', title: item.prompt }, item.prompt || 'Untitled'),
|
|
3309
|
+
react.createElement('div', { className: 'ig-vault-prompt', title: item.prompt }, item.prompt || (t('vault.noPrompt') || 'Untitled')),
|
|
3226
3310
|
react.createElement(
|
|
3227
3311
|
'div',
|
|
3228
3312
|
{ className: 'ig-vault-meta' },
|
|
3229
3313
|
react.createElement('span', null, item.provider || 'local'),
|
|
3230
|
-
item.seed !== undefined && react.createElement('span', null, 'Seed ' + item.seed)
|
|
3314
|
+
item.seed !== undefined && react.createElement('span', null, (t('vault.seed') || 'Seed') + ' ' + item.seed)
|
|
3231
3315
|
),
|
|
3232
3316
|
react.createElement(
|
|
3233
3317
|
'div',
|
|
3234
3318
|
{ style: { display: 'flex', gap: '4px', marginTop: '6px' } },
|
|
3235
|
-
react.createElement('button', { type: 'button', className: 'ig-rev-btn', style: { flex: 1 }, onClick: () => handleReroll(item), title: 'Re-roll with seed' }, '🎲 Re-roll'),
|
|
3236
|
-
react.createElement('button', { type: 'button', className: 'ig-rev-btn', style: { flex: 1 }, onClick: () => handleInsert(item), title: 'Insert into chat' }, '💬 Insert'),
|
|
3237
|
-
react.createElement('button', { type: 'button', className: 'ig-rev-btn', onClick: () => handleDelete(item.id || item.attachmentId), title: 'Delete' }, '🗑️')
|
|
3319
|
+
react.createElement('button', { type: 'button', className: 'ig-rev-btn', style: { flex: 1 }, onClick: () => handleReroll(item), title: t('vault.reroll') || 'Re-roll with seed' }, '🎲 ' + (t('vault.reroll') || 'Re-roll')),
|
|
3320
|
+
react.createElement('button', { type: 'button', className: 'ig-rev-btn', style: { flex: 1 }, onClick: () => handleInsert(item), title: t('vault.insert') || 'Insert into chat' }, '💬 ' + (t('vault.insert') || 'Insert')),
|
|
3321
|
+
react.createElement('button', { type: 'button', className: 'ig-rev-btn', onClick: () => handleDelete(item.id || item.attachmentId), title: t('vault.delete') || 'Delete' }, '🗑️')
|
|
3238
3322
|
)
|
|
3239
3323
|
)
|
|
3240
3324
|
)
|
|
@@ -3253,7 +3337,7 @@ window.__ModuleLoader__.load({
|
|
|
3253
3337
|
disabled: loading,
|
|
3254
3338
|
style: { padding: '8px 20px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer' },
|
|
3255
3339
|
},
|
|
3256
|
-
loading ? 'Loading...' : 'Load more assets'
|
|
3340
|
+
loading ? (t('vault.loading') || 'Loading...') : (t('vault.loadMore') || 'Load more assets')
|
|
3257
3341
|
)
|
|
3258
3342
|
),
|
|
3259
3343
|
selected &&
|
|
@@ -3272,7 +3356,7 @@ window.__ModuleLoader__.load({
|
|
|
3272
3356
|
react.createElement(
|
|
3273
3357
|
'div',
|
|
3274
3358
|
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3275
|
-
react.createElement('span', { style: { fontWeight: '700', fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } }, 'Asset Inspector'),
|
|
3359
|
+
react.createElement('span', { style: { fontWeight: '700', fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } }, t('vault.inspector') || 'Asset Inspector'),
|
|
3276
3360
|
react.createElement('button', { type: 'button', onClick: () => setSelected(null), style: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '16px', color: 'var(--dsw-alias-label-secondary)' } }, '✕')
|
|
3277
3361
|
),
|
|
3278
3362
|
react.createElement('img', {
|
|
@@ -3282,31 +3366,32 @@ window.__ModuleLoader__.load({
|
|
|
3282
3366
|
react.createElement(
|
|
3283
3367
|
'div',
|
|
3284
3368
|
{ style: { display: 'flex', flexWrap: 'wrap', gap: '6px', fontSize: '11px' } },
|
|
3285
|
-
selected.seed !== undefined && react.createElement('span', { className: 'ig-badge' }, 'Seed: ' + selected.seed),
|
|
3286
|
-
selected.provider && react.createElement('span', { className: 'ig-badge' }, 'Provider: ' + selected.provider),
|
|
3369
|
+
selected.seed !== undefined && react.createElement('span', { className: 'ig-badge' }, (t('vault.seed') || 'Seed') + ': ' + selected.seed),
|
|
3370
|
+
selected.provider && react.createElement('span', { className: 'ig-badge' }, (t('vault.provider') || 'Provider') + ': ' + selected.provider),
|
|
3287
3371
|
selected.width && react.createElement('span', { className: 'ig-badge' }, selected.width + '×' + selected.height),
|
|
3288
3372
|
selected.cost ? react.createElement('span', { className: 'ig-badge' }, '$' + Number(selected.cost).toFixed(4)) : null
|
|
3289
3373
|
),
|
|
3290
|
-
react.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary)', background: 'var(--dsw-alias-bg-layer-2)', padding: '10px', borderRadius: '6px', lineHeight: '1.4' } }, selected.prompt || 'No prompt'),
|
|
3374
|
+
react.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary)', background: 'var(--dsw-alias-bg-layer-2)', padding: '10px', borderRadius: '6px', lineHeight: '1.4' } }, selected.prompt || (t('vault.noPrompt') || 'No prompt')),
|
|
3291
3375
|
react.createElement(
|
|
3292
3376
|
'div',
|
|
3293
3377
|
{ style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '6px' } },
|
|
3294
|
-
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleReroll(selected) }, '🎲 Re-roll'),
|
|
3295
|
-
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleInsert(selected) }, '💬 Insert in Chat'),
|
|
3378
|
+
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleReroll(selected) }, '🎲 ' + (t('vault.reroll') || 'Re-roll')),
|
|
3379
|
+
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleInsert(selected) }, '💬 ' + (t('vault.insert') || 'Insert in Chat')),
|
|
3296
3380
|
react.createElement(
|
|
3297
3381
|
'a',
|
|
3298
3382
|
{ href: selected.url || selected.thumbnailUrl, download: 'vault-asset-' + (selected.id || 'image') + '.png', className: 'ig-tab-btn', style: { textDecoration: 'none', display: 'inline-flex', alignItems: 'center' } },
|
|
3299
|
-
'⬇ Download'
|
|
3383
|
+
'⬇ ' + (t('vault.download') || 'Download')
|
|
3300
3384
|
),
|
|
3301
|
-
react.createElement('button', { type: 'button', className: 'ig-tab-btn', style: { color: 'var(--dsw-alias-state-error-primary)' }, onClick: () => handleDelete(selected.id || selected.attachmentId) }, '🗑️ Delete')
|
|
3385
|
+
react.createElement('button', { type: 'button', className: 'ig-tab-btn', style: { color: 'var(--dsw-alias-state-error-primary)' }, onClick: () => handleDelete(selected.id || selected.attachmentId) }, '🗑️ ' + (t('vault.delete') || 'Delete'))
|
|
3302
3386
|
)
|
|
3303
3387
|
)
|
|
3304
3388
|
)
|
|
3305
3389
|
)
|
|
3306
3390
|
}
|
|
3307
|
-
// -------------------------------------------------------------- Fullscreen Image Studio (#158)
|
|
3391
|
+
// -------------------------------------------------------------- Fullscreen Image Studio (#158, #304)
|
|
3308
3392
|
function ImageStudioView(props) {
|
|
3309
3393
|
const { ctx } = props
|
|
3394
|
+
const t = resolveTranslator(props, ctx)
|
|
3310
3395
|
const [viewMode, setViewMode] = react.useState('studio') // 'studio' | 'vault'
|
|
3311
3396
|
const [isFullscreen, setIsFullscreen] = react.useState(false)
|
|
3312
3397
|
const [layoutGrid, setLayoutGrid] = react.useState('1x1') // '1x1' | '2x2' | '1x4'
|
|
@@ -3350,16 +3435,16 @@ window.__ModuleLoader__.load({
|
|
|
3350
3435
|
|
|
3351
3436
|
const updatePrompt = (val) => {
|
|
3352
3437
|
setPrompt(val)
|
|
3353
|
-
try { window.localStorage.setItem('dsh_studio_prompt', val) } catch (_) { /* storage
|
|
3438
|
+
try { window.localStorage.setItem('dsh_studio_prompt', val) } catch (_) { /* ignore storage quota or private mode error */ }
|
|
3354
3439
|
}
|
|
3355
3440
|
|
|
3356
|
-
const handleSelectFromVault = (
|
|
3357
|
-
if (!
|
|
3358
|
-
setActiveImages([
|
|
3359
|
-
if (asset.prompt) updatePrompt(asset.prompt)
|
|
3360
|
-
if (asset.seed !== undefined) setSeed(String(asset.seed))
|
|
3361
|
-
if (asset.provider) setProvider(asset.provider)
|
|
3441
|
+
const handleSelectFromVault = (item) => {
|
|
3442
|
+
if (!item) return
|
|
3443
|
+
setActiveImages([item])
|
|
3362
3444
|
setViewMode('studio')
|
|
3445
|
+
if (item.prompt) updatePrompt(item.prompt)
|
|
3446
|
+
if (item.provider) setProvider(item.provider)
|
|
3447
|
+
if (item.seed !== undefined) setSeed(String(item.seed))
|
|
3363
3448
|
}
|
|
3364
3449
|
|
|
3365
3450
|
const handleCopySnippet = (format) => {
|
|
@@ -3371,7 +3456,7 @@ window.__ModuleLoader__.load({
|
|
|
3371
3456
|
if (format === 'html') snippet = '<img src="' + url + '" alt="' + (first.prompt || 'Artwork') + '" />'
|
|
3372
3457
|
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
3373
3458
|
navigator.clipboard.writeText(snippet)
|
|
3374
|
-
setStatus('Copied ' + format.toUpperCase() + ' snippet!')
|
|
3459
|
+
setStatus(t('studio.copiedSnippet') || ('Copied ' + format.toUpperCase() + ' snippet!'))
|
|
3375
3460
|
setTimeout(() => setStatus(''), 2000)
|
|
3376
3461
|
}
|
|
3377
3462
|
}
|
|
@@ -3389,7 +3474,7 @@ window.__ModuleLoader__.load({
|
|
|
3389
3474
|
react.createElement(
|
|
3390
3475
|
'div',
|
|
3391
3476
|
{ style: { display: 'flex', gap: '8px', alignItems: 'center' } },
|
|
3392
|
-
react.createElement('span', { style: { fontWeight: '700', fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, '🎨 Image Studio'),
|
|
3477
|
+
react.createElement('span', { style: { fontWeight: '700', fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, '🎨 ' + (t('studio.title') || 'Image Studio')),
|
|
3393
3478
|
react.createElement(
|
|
3394
3479
|
'button',
|
|
3395
3480
|
{
|
|
@@ -3397,7 +3482,7 @@ window.__ModuleLoader__.load({
|
|
|
3397
3482
|
className: 'ig-tab-btn ' + (viewMode === 'studio' ? 'active' : ''),
|
|
3398
3483
|
onClick: () => setViewMode('studio'),
|
|
3399
3484
|
},
|
|
3400
|
-
'Studio Canvas'
|
|
3485
|
+
t('studio.canvas') || 'Studio Canvas'
|
|
3401
3486
|
),
|
|
3402
3487
|
react.createElement(
|
|
3403
3488
|
'button',
|
|
@@ -3406,7 +3491,7 @@ window.__ModuleLoader__.load({
|
|
|
3406
3491
|
className: 'ig-tab-btn ' + (viewMode === 'vault' ? 'active' : ''),
|
|
3407
3492
|
onClick: () => setViewMode('vault'),
|
|
3408
3493
|
},
|
|
3409
|
-
'🗃️ Asset Vault'
|
|
3494
|
+
'🗃️ ' + (t('studio.vault') || 'Asset Vault')
|
|
3410
3495
|
)
|
|
3411
3496
|
),
|
|
3412
3497
|
react.createElement(
|
|
@@ -3438,12 +3523,12 @@ window.__ModuleLoader__.load({
|
|
|
3438
3523
|
onClick: () => setIsFullscreen(!isFullscreen),
|
|
3439
3524
|
style: { border: '1px solid var(--dsw-alias-border-l2)' },
|
|
3440
3525
|
},
|
|
3441
|
-
isFullscreen ? '✕ Exit Fullscreen' : '⛶ Fullscreen'
|
|
3526
|
+
isFullscreen ? ('✕ ' + (t('studio.exitFullscreen') || 'Exit Fullscreen')) : ('⛶ ' + (t('studio.fullscreen') || 'Fullscreen'))
|
|
3442
3527
|
)
|
|
3443
3528
|
)
|
|
3444
3529
|
),
|
|
3445
3530
|
viewMode === 'vault'
|
|
3446
|
-
? react.createElement(AssetVaultView, { ctx, onSelectAsset: handleSelectFromVault })
|
|
3531
|
+
? react.createElement(AssetVaultView, { ctx, t, onSelectAsset: handleSelectFromVault })
|
|
3447
3532
|
: react.createElement(
|
|
3448
3533
|
react.Fragment,
|
|
3449
3534
|
null,
|
|
@@ -3453,16 +3538,16 @@ window.__ModuleLoader__.load({
|
|
|
3453
3538
|
react.createElement(
|
|
3454
3539
|
'div',
|
|
3455
3540
|
{ className: 'ig-studio-sidebar' },
|
|
3456
|
-
react.createElement('span', { style: { fontWeight: '600', fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, 'Generation Parameters'),
|
|
3541
|
+
react.createElement('span', { style: { fontWeight: '600', fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, t('studio.parameters') || 'Generation Parameters'),
|
|
3457
3542
|
react.createElement(
|
|
3458
3543
|
'div',
|
|
3459
3544
|
null,
|
|
3460
|
-
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Prompt:'),
|
|
3545
|
+
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, t('studio.prompt') || 'Prompt:'),
|
|
3461
3546
|
react.createElement('textarea', {
|
|
3462
3547
|
className: 'ig-input',
|
|
3463
3548
|
rows: 4,
|
|
3464
3549
|
style: { width: '100%', resize: 'vertical', borderRadius: '6px', padding: '8px', fontSize: '12px' },
|
|
3465
|
-
placeholder: 'Describe your vision...',
|
|
3550
|
+
placeholder: t('studio.promptPlaceholder') || 'Describe your vision...',
|
|
3466
3551
|
value: prompt,
|
|
3467
3552
|
onChange: (e) => updatePrompt(e.target.value),
|
|
3468
3553
|
})
|
|
@@ -3470,7 +3555,7 @@ window.__ModuleLoader__.load({
|
|
|
3470
3555
|
react.createElement(
|
|
3471
3556
|
'div',
|
|
3472
3557
|
null,
|
|
3473
|
-
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Style Preset:'),
|
|
3558
|
+
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, t('studio.stylePreset') || 'Style Preset:'),
|
|
3474
3559
|
react.createElement(
|
|
3475
3560
|
'div',
|
|
3476
3561
|
{ style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } },
|
|
@@ -3495,7 +3580,7 @@ window.__ModuleLoader__.load({
|
|
|
3495
3580
|
react.createElement(
|
|
3496
3581
|
'div',
|
|
3497
3582
|
null,
|
|
3498
|
-
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Aspect Ratio:'),
|
|
3583
|
+
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, t('studio.aspectRatio') || 'Aspect Ratio:'),
|
|
3499
3584
|
react.createElement(
|
|
3500
3585
|
'select',
|
|
3501
3586
|
{
|
|
@@ -3514,7 +3599,7 @@ window.__ModuleLoader__.load({
|
|
|
3514
3599
|
react.createElement(
|
|
3515
3600
|
'div',
|
|
3516
3601
|
null,
|
|
3517
|
-
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Provider:'),
|
|
3602
|
+
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, t('studio.provider') || 'Provider:'),
|
|
3518
3603
|
react.createElement(
|
|
3519
3604
|
'select',
|
|
3520
3605
|
{
|
|
@@ -3532,7 +3617,7 @@ window.__ModuleLoader__.load({
|
|
|
3532
3617
|
react.createElement(
|
|
3533
3618
|
'div',
|
|
3534
3619
|
null,
|
|
3535
|
-
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, 'Seed (optional):'),
|
|
3620
|
+
react.createElement('label', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', display: 'block', marginBottom: '4px' } }, t('studio.seed') || 'Seed (optional):'),
|
|
3536
3621
|
react.createElement(
|
|
3537
3622
|
'div',
|
|
3538
3623
|
{ style: { display: 'flex', gap: '6px' } },
|
|
@@ -3540,7 +3625,7 @@ window.__ModuleLoader__.load({
|
|
|
3540
3625
|
type: 'number',
|
|
3541
3626
|
className: 'ig-input',
|
|
3542
3627
|
style: { flex: 1, padding: '6px', fontSize: '12px' },
|
|
3543
|
-
placeholder: 'Random',
|
|
3628
|
+
placeholder: t('studio.random') || 'Random',
|
|
3544
3629
|
value: seed,
|
|
3545
3630
|
onChange: (e) => setSeed(e.target.value),
|
|
3546
3631
|
}),
|
|
@@ -3550,7 +3635,7 @@ window.__ModuleLoader__.load({
|
|
|
3550
3635
|
type: 'button',
|
|
3551
3636
|
className: 'ig-tab-btn',
|
|
3552
3637
|
onClick: () => setSeed(String(Math.floor(Math.random() * 2147483647))),
|
|
3553
|
-
title: 'Randomize seed',
|
|
3638
|
+
title: t('studio.randomizeSeed') || 'Randomize seed',
|
|
3554
3639
|
},
|
|
3555
3640
|
'🎲'
|
|
3556
3641
|
)
|
|
@@ -3566,12 +3651,12 @@ window.__ModuleLoader__.load({
|
|
|
3566
3651
|
const cmd = '/image ' + (prompt || 'Artwork') + (stylePreset !== 'none' ? ' --style ' + stylePreset : '') + ' --aspect ' + aspectRatio + ' --provider ' + provider + (seed ? ' --seed ' + seed : '')
|
|
3567
3652
|
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
3568
3653
|
navigator.clipboard.writeText(cmd)
|
|
3569
|
-
setStatus('Copied generation command to clipboard!')
|
|
3654
|
+
setStatus(t('studio.copiedCommand') || 'Copied generation command to clipboard!')
|
|
3570
3655
|
setTimeout(() => setStatus(''), 2500)
|
|
3571
3656
|
}
|
|
3572
3657
|
},
|
|
3573
3658
|
},
|
|
3574
|
-
'🎨 Generate in Chat'
|
|
3659
|
+
'🎨 ' + (t('studio.generate') || 'Generate in Chat')
|
|
3575
3660
|
),
|
|
3576
3661
|
status && react.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-state-brand-primary)', textAlign: 'center' } }, status)
|
|
3577
3662
|
),
|
|
@@ -3579,7 +3664,7 @@ window.__ModuleLoader__.load({
|
|
|
3579
3664
|
'div',
|
|
3580
3665
|
{ className: 'ig-studio-canvas' },
|
|
3581
3666
|
activeImages.length === 0
|
|
3582
|
-
? react.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, 'Select an image from the recent strip below or click Generate.')
|
|
3667
|
+
? react.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '13px' } }, t('studio.emptyCanvas') || 'Select an image from the recent strip below or click Generate.')
|
|
3583
3668
|
: react.createElement(
|
|
3584
3669
|
'div',
|
|
3585
3670
|
{
|
|
@@ -3604,12 +3689,12 @@ window.__ModuleLoader__.load({
|
|
|
3604
3689
|
react.createElement(
|
|
3605
3690
|
'div',
|
|
3606
3691
|
{ style: { display: 'flex', gap: '8px', marginTop: '10px' } },
|
|
3607
|
-
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleCopySnippet('markdown') }, 'Copy Markdown'),
|
|
3608
|
-
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleCopySnippet('html') }, 'Copy HTML'),
|
|
3692
|
+
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleCopySnippet('markdown') }, t('studio.copyMarkdown') || 'Copy Markdown'),
|
|
3693
|
+
react.createElement('button', { type: 'button', className: 'ig-tab-btn', onClick: () => handleCopySnippet('html') }, t('studio.copyHtml') || 'Copy HTML'),
|
|
3609
3694
|
react.createElement(
|
|
3610
3695
|
'a',
|
|
3611
3696
|
{ href: activeImages[0].url || activeImages[0].thumbnailUrl, download: 'studio-image.png', className: 'ig-tab-btn', style: { textDecoration: 'none' } },
|
|
3612
|
-
'⬇ Download'
|
|
3697
|
+
'⬇ ' + (t('studio.download') || 'Download')
|
|
3613
3698
|
)
|
|
3614
3699
|
)
|
|
3615
3700
|
)
|
|
@@ -3617,7 +3702,7 @@ window.__ModuleLoader__.load({
|
|
|
3617
3702
|
react.createElement(
|
|
3618
3703
|
'div',
|
|
3619
3704
|
{ style: { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '10px' } },
|
|
3620
|
-
react.createElement('span', { style: { fontSize: '12px', fontWeight: '600', color: 'var(--dsw-alias-label-secondary)' } }, 'Recent Generations:'),
|
|
3705
|
+
react.createElement('span', { style: { fontSize: '12px', fontWeight: '600', color: 'var(--dsw-alias-label-secondary)' } }, (t('studio.recent') || 'Recent Generations') + ':'),
|
|
3621
3706
|
react.createElement(
|
|
3622
3707
|
'div',
|
|
3623
3708
|
{ className: 'ig-studio-strip', style: { display: 'flex', gap: '8px', overflowX: 'auto', padding: '4px 0' } },
|
package/lib/commands.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// commands.js — Top-level slash command /image for direct generation without LLM reasoning (#152)
|
|
2
|
+
|
|
3
|
+
import { makeProviders, resolveStylePreset } from './providers.js'
|
|
4
|
+
import { ASPECT_RATIOS } from './providers/shared-helpers.js'
|
|
5
|
+
import { calculateGenerationCost } from './cost-meter.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse raw slash command arguments: /image <prompt> [--flags]
|
|
9
|
+
*
|
|
10
|
+
* @param {string} rawInput
|
|
11
|
+
* @returns {{ prompt: string, flags: Record<string, string>, error?: string }}
|
|
12
|
+
*/
|
|
13
|
+
export function parseImageCommandArgs(rawInput) {
|
|
14
|
+
const str = String(rawInput || '').trim()
|
|
15
|
+
if (!str) return { prompt: '', flags: {}, error: 'Missing prompt' }
|
|
16
|
+
|
|
17
|
+
const tokens = []
|
|
18
|
+
const re = /[^\s"']+|"([^"]*)"|'([^']*)'/g
|
|
19
|
+
let match
|
|
20
|
+
while ((match = re.exec(str)) !== null) {
|
|
21
|
+
tokens.push(match[1] || match[2] || match[0])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const flags = {}
|
|
25
|
+
const promptWords = []
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
28
|
+
const t = tokens[i]
|
|
29
|
+
if (t.startsWith('--')) {
|
|
30
|
+
const eqIdx = t.indexOf('=')
|
|
31
|
+
if (eqIdx !== -1) {
|
|
32
|
+
const key = t.slice(2, eqIdx).toLowerCase()
|
|
33
|
+
const val = t.slice(eqIdx + 1)
|
|
34
|
+
flags[key] = val
|
|
35
|
+
} else {
|
|
36
|
+
const key = t.slice(2).toLowerCase()
|
|
37
|
+
if (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) {
|
|
38
|
+
flags[key] = tokens[i + 1]
|
|
39
|
+
i++
|
|
40
|
+
} else {
|
|
41
|
+
flags[key] = 'true'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
promptWords.push(t)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const prompt = promptWords.join(' ').trim()
|
|
50
|
+
return { prompt, flags }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Registers the /image slash command via @deepseek-ai/dsh-commands service.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} ctx - Cordis context
|
|
57
|
+
* @param {object} deps - Shared plugin runtime dependencies
|
|
58
|
+
*/
|
|
59
|
+
export function registerImageCommand(ctx, deps) {
|
|
60
|
+
const { live, saveAndAttachResult, resolveApiKey, slugify } = deps
|
|
61
|
+
|
|
62
|
+
ctx.inject(['commands'], (cmdCtx) => {
|
|
63
|
+
const commands = cmdCtx.commands
|
|
64
|
+
if (typeof commands?.register !== 'function') return
|
|
65
|
+
|
|
66
|
+
const executeCommand = async (rawInput, invocation = {}) => {
|
|
67
|
+
const { prompt, flags } = parseImageCommandArgs(rawInput)
|
|
68
|
+
if (!prompt) {
|
|
69
|
+
return {
|
|
70
|
+
kind: 'error',
|
|
71
|
+
text: 'Usage: /image <prompt> [--size=...] [--aspect=...] [--provider=...] [--style=...] [--seed=...]',
|
|
72
|
+
toString() { return this.text },
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cfg = typeof live === 'function' ? live() : (deps.config || {})
|
|
77
|
+
const provider = flags.provider || cfg.provider || 'fal'
|
|
78
|
+
const size = flags.size || cfg.defaultSize || 'square'
|
|
79
|
+
const format = flags.format || cfg.defaultFormat || 'png'
|
|
80
|
+
const aspectRatio = flags.aspect || flags.aspect_ratio || cfg.defaultAspectRatio
|
|
81
|
+
const effectiveStyle = flags.style || cfg.stylePreset
|
|
82
|
+
const seed = flags.seed ? Number(flags.seed) : Math.floor(Math.random() * 2147483647)
|
|
83
|
+
|
|
84
|
+
const styleInfo = resolveStylePreset(effectiveStyle, flags.negative_prompt, flags.guidance_scale)
|
|
85
|
+
const effectivePrompt = styleInfo.promptSuffix ? `${prompt}, ${styleInfo.promptSuffix}` : prompt
|
|
86
|
+
|
|
87
|
+
const job = {
|
|
88
|
+
prompt: effectivePrompt,
|
|
89
|
+
size,
|
|
90
|
+
format,
|
|
91
|
+
aspectRatio,
|
|
92
|
+
aspectPixels: aspectRatio ? ASPECT_RATIOS[aspectRatio] : undefined,
|
|
93
|
+
seed,
|
|
94
|
+
signal: invocation?.signal,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const pdeps = {
|
|
98
|
+
fetchImpl: fetch,
|
|
99
|
+
resolveKey: (ref) => resolveApiKey(ctx, ref),
|
|
100
|
+
cfg,
|
|
101
|
+
ctx,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const providerFactory = deps.makeProviders || makeProviders
|
|
105
|
+
const providers = providerFactory(pdeps, job)
|
|
106
|
+
const generateFn = providers[provider] || providers.fal || providers.custom
|
|
107
|
+
if (typeof generateFn !== 'function') {
|
|
108
|
+
throw new Error(`Unsupported provider: "${provider}". One of: fal, openai, grok, gemini, replicate, local, seedream`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = await generateFn(seed, effectivePrompt)
|
|
112
|
+
const safeSlug = typeof slugify === 'function' ? slugify(prompt.slice(0, 32)) : 'image'
|
|
113
|
+
const stem = `${safeSlug}-${Date.now().toString(36)}`
|
|
114
|
+
const name = `${stem}.${format}`
|
|
115
|
+
|
|
116
|
+
const cost = calculateGenerationCost({
|
|
117
|
+
provider,
|
|
118
|
+
model: cfg.model || cfg.customModel || 'default',
|
|
119
|
+
size,
|
|
120
|
+
count: 1,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const attached = await saveAndAttachResult(ctx, invocation, cfg, {
|
|
124
|
+
bytes: result.bytes,
|
|
125
|
+
mediaType: result.mediaType || 'image/png',
|
|
126
|
+
name,
|
|
127
|
+
stem,
|
|
128
|
+
prompt,
|
|
129
|
+
size,
|
|
130
|
+
format,
|
|
131
|
+
seed,
|
|
132
|
+
provider,
|
|
133
|
+
model: cfg.model || cfg.customModel || 'default',
|
|
134
|
+
cost,
|
|
135
|
+
sourceUrl: result.sourceUrl,
|
|
136
|
+
deliverAs: cfg.deliverAs || 'link',
|
|
137
|
+
args: { prompt, ...flags },
|
|
138
|
+
action: 'generated via /image command',
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
kind: 'success',
|
|
143
|
+
text: attached.summary,
|
|
144
|
+
attachment: attached.attachment,
|
|
145
|
+
toString() { return attached.summary },
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const unregister = commands.register({
|
|
150
|
+
definitionId: '@goodandready/dsh-image-gen',
|
|
151
|
+
name: 'image',
|
|
152
|
+
description: 'Generate an image directly without chat model reasoning (/image <prompt> [--size=...] [--aspect=...] [--provider=...] [--style=...])',
|
|
153
|
+
input: { hint: '<prompt> [--size=...] [--aspect=...] [--provider=...] [--style=...]' },
|
|
154
|
+
handler: async (invocation) => {
|
|
155
|
+
try {
|
|
156
|
+
const raw = invocation?.rawInput || invocation?.text || (typeof invocation === 'string' ? invocation : '')
|
|
157
|
+
return await executeCommand(raw, invocation)
|
|
158
|
+
} catch (err) {
|
|
159
|
+
return {
|
|
160
|
+
kind: 'error',
|
|
161
|
+
text: `Image generation failed: ${err?.message || err}`,
|
|
162
|
+
toString() { return `Image generation failed: ${err?.message || err}` },
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
execute: async (rawArgs, invocation) => {
|
|
167
|
+
const res = await executeCommand(rawArgs, invocation)
|
|
168
|
+
return res.text
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
if (typeof ctx.effect === 'function') {
|
|
173
|
+
ctx.effect(() => () => unregister?.(), 'dsh-image-gen: /image slash command')
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
}
|
package/lib/history.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import os from 'node:os'
|
|
3
|
-
import { existsSync, unlinkSync } from 'node:fs'
|
|
3
|
+
import { existsSync, unlinkSync, chmodSync, readdirSync, statSync } from 'node:fs'
|
|
4
4
|
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
5
5
|
import { enforceSecurePermissions } from './security.js'
|
|
6
6
|
|
|
@@ -68,6 +68,37 @@ export async function pruneHistory(entries, pruneDays) {
|
|
|
68
68
|
return kept
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Repairs permissions on the history directory (0700) and history file (0600),
|
|
73
|
+
* migrating existing installations created under world-readable umasks (#307).
|
|
74
|
+
*/
|
|
75
|
+
export function repairHistoryPermissions() {
|
|
76
|
+
try {
|
|
77
|
+
const file = historyFile()
|
|
78
|
+
const dir = path.dirname(file)
|
|
79
|
+
if (existsSync(dir)) {
|
|
80
|
+
try { chmodSync(dir, 0o700) } catch (_err) { /* non-fatal permission repair */ }
|
|
81
|
+
try {
|
|
82
|
+
const files = readdirSync(dir)
|
|
83
|
+
for (const f of files) {
|
|
84
|
+
const p = path.join(dir, f)
|
|
85
|
+
try {
|
|
86
|
+
const st = statSync(p)
|
|
87
|
+
if (st.isDirectory()) {
|
|
88
|
+
chmodSync(p, 0o700)
|
|
89
|
+
} else {
|
|
90
|
+
chmodSync(p, 0o600)
|
|
91
|
+
}
|
|
92
|
+
} catch (_err) { /* skip unreadable entry */ }
|
|
93
|
+
}
|
|
94
|
+
} catch (_err) { /* skip unreadable directory */ }
|
|
95
|
+
}
|
|
96
|
+
if (existsSync(file)) {
|
|
97
|
+
try { chmodSync(file, 0o600) } catch (_err) { /* non-fatal permission repair */ }
|
|
98
|
+
}
|
|
99
|
+
} catch (_err) { /* non-fatal permission repair */ }
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
export async function readHistory() {
|
|
72
103
|
try {
|
|
73
104
|
const raw = await readFile(historyFile(), 'utf-8')
|
|
@@ -81,9 +112,12 @@ export async function readHistory() {
|
|
|
81
112
|
/** Save history to disk file (atomic overwrite). */
|
|
82
113
|
export async function writeHistory(entries) {
|
|
83
114
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
const file = historyFile()
|
|
116
|
+
const dir = path.dirname(file)
|
|
117
|
+
await mkdir(dir, { recursive: true, mode: 0o700 })
|
|
118
|
+
try { chmodSync(dir, 0o700) } catch (_err) { /* non-fatal permission repair */ }
|
|
119
|
+
await writeFile(file, JSON.stringify(entries, null, 2), { mode: 0o600 })
|
|
120
|
+
enforceSecurePermissions(file)
|
|
87
121
|
} catch (e) { /* history write failure is non-fatal */ }
|
|
88
122
|
}
|
|
89
123
|
|
package/lib/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import z from '@deepseek-ai/schemastery'
|
|
16
16
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
17
|
-
import { readFile } from 'node:fs/promises'
|
|
17
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
18
18
|
import { isTrustedLocalRequest } from './security.js'
|
|
19
19
|
import { saveAndAttachResult } from './attachment-helper.js'
|
|
20
20
|
import {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
readHistory,
|
|
28
28
|
writeHistory,
|
|
29
29
|
filterHistory,
|
|
30
|
+
repairHistoryPermissions,
|
|
30
31
|
} from './history.js'
|
|
31
32
|
import { existsSync } from 'node:fs'
|
|
32
33
|
import path from 'node:path'
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
} from './providers.js'
|
|
42
43
|
|
|
43
44
|
import { resolveConversationImage, analyzeImageWithVision } from './resolve-image.js'
|
|
45
|
+
import { registerImageCommand } from './commands.js'
|
|
44
46
|
import { registerAllTools } from './register-tools.js'
|
|
45
47
|
import { registerPluginUpdater } from './updater.js'
|
|
46
48
|
import { registerVaultRoutes } from './vault.js'
|
|
@@ -60,6 +62,7 @@ export {
|
|
|
60
62
|
readHistory,
|
|
61
63
|
writeHistory,
|
|
62
64
|
filterHistory,
|
|
65
|
+
repairHistoryPermissions,
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export const name = '@goodandready/dsh-image-gen'
|
|
@@ -349,16 +352,60 @@ export function publicConfig(cfg) {
|
|
|
349
352
|
/** Read source image for editing: filesystem path or attachment id. */
|
|
350
353
|
export async function resolveSource(ctx, exec, ref) {
|
|
351
354
|
if (!ref) return undefined
|
|
352
|
-
const sessionCwd = exec
|
|
355
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd || process.cwd()
|
|
353
356
|
if (ref.startsWith('sha256:')) {
|
|
354
357
|
const stored = await ctx.attachments.readImage({ attachmentId: ref, mediaType: 'image/png', bytes: 0, width: 0, height: 0 })
|
|
355
|
-
|
|
358
|
+
const bytes = Buffer.from(stored.data || stored.bytes || [])
|
|
359
|
+
validateImageSignature(bytes, ref)
|
|
360
|
+
return { bytes, mediaType: stored.ref?.mediaType || 'image/png' }
|
|
356
361
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
if (ref.startsWith('data:')) {
|
|
363
|
+
const commaIndex = ref.indexOf(',')
|
|
364
|
+
if (commaIndex > 0) {
|
|
365
|
+
const meta = ref.slice(0, commaIndex)
|
|
366
|
+
const data = ref.slice(commaIndex + 1).trim()
|
|
367
|
+
const approxBytes = Math.ceil((data.length * 3) / 4)
|
|
368
|
+
if (approxBytes > 50 * 1024 * 1024) {
|
|
369
|
+
throw new Error('Data URI payload too large. Maximum allowed size is 50 MB.')
|
|
370
|
+
}
|
|
371
|
+
const bytes = Buffer.from(data, 'base64')
|
|
372
|
+
if (bytes.length > 50 * 1024 * 1024) {
|
|
373
|
+
throw new Error('Decoded image data too large. Maximum allowed size is 50 MB.')
|
|
374
|
+
}
|
|
375
|
+
validateImageSignature(bytes, 'inline data URI')
|
|
376
|
+
const mediaMatch = meta.match(/data:([^;]+)/)
|
|
377
|
+
return { bytes, mediaType: mediaMatch ? mediaMatch[1] : 'image/png' }
|
|
378
|
+
}
|
|
379
|
+
throw new Error('Malformed data URI')
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const root = path.resolve(sessionCwd)
|
|
383
|
+
const target = path.resolve(root, ref)
|
|
384
|
+
const rel = path.relative(root, target)
|
|
385
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
386
|
+
throw new Error(`Access denied: path "${ref}" is outside allowed workspace directory "${root}"`)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const fileStat = await stat(target).catch(() => null)
|
|
390
|
+
if (!fileStat) {
|
|
391
|
+
throw new Error(`Source image not found: ${ref}`)
|
|
392
|
+
}
|
|
393
|
+
if (!fileStat.isFile()) {
|
|
394
|
+
throw new Error(`Target is not a regular file: ${ref}`)
|
|
395
|
+
}
|
|
396
|
+
if (fileStat.size === 0) {
|
|
397
|
+
throw new Error(`Image file is empty (0 bytes): ${ref}`)
|
|
398
|
+
}
|
|
399
|
+
if (fileStat.size > 50 * 1024 * 1024) {
|
|
400
|
+
throw new Error(`Source image too large (${(fileStat.size / (1024 * 1024)).toFixed(1)} MB). Maximum allowed size is 50 MB.`)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const bytes = await readFile(target)
|
|
404
|
+
validateImageSignature(bytes, ref)
|
|
405
|
+
const ext = path.extname(target).toLowerCase()
|
|
360
406
|
const mediaType = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg'
|
|
361
407
|
: ext === '.webp' ? 'image/webp'
|
|
408
|
+
: ext === '.gif' ? 'image/gif'
|
|
362
409
|
: ext === '.svg' ? 'image/svg+xml'
|
|
363
410
|
: 'image/png'
|
|
364
411
|
return { bytes, mediaType }
|
|
@@ -581,7 +628,10 @@ export function apply(ctx, rawConfig) {
|
|
|
581
628
|
})()
|
|
582
629
|
|
|
583
630
|
for (const path of ['/dsh-image-gen/image', '/dsh-fal-image-gen/image']) {
|
|
584
|
-
|
|
631
|
+
// Repair history file & directory permissions on startup (#307)
|
|
632
|
+
repairHistoryPermissions()
|
|
633
|
+
|
|
634
|
+
ctx.effect(() => ctx.webServer.register({
|
|
585
635
|
kind: 'exact',
|
|
586
636
|
path,
|
|
587
637
|
handler: imageHandler,
|
|
@@ -676,6 +726,16 @@ export function apply(ctx, rawConfig) {
|
|
|
676
726
|
}, 'dsh-image-gen: session cleanup')
|
|
677
727
|
|
|
678
728
|
// Tool registrations live in register-tools.js; each tool is a labeled ctx.effect (#216).
|
|
729
|
+
// Top-level /image slash command (#152)
|
|
730
|
+
registerImageCommand(ctx, {
|
|
731
|
+
config: baseConfig,
|
|
732
|
+
live,
|
|
733
|
+
saveAndAttachResult,
|
|
734
|
+
resolveSource,
|
|
735
|
+
slugify,
|
|
736
|
+
resolveApiKey,
|
|
737
|
+
})
|
|
738
|
+
|
|
679
739
|
registerAllTools(ctx, {
|
|
680
740
|
config: baseConfig,
|
|
681
741
|
live,
|
package/lib/resolve-image.js
CHANGED
|
@@ -65,6 +65,26 @@ export function findLastConversationImage(exec) {
|
|
|
65
65
|
/**
|
|
66
66
|
* Determine MIME type by extension or signature.
|
|
67
67
|
*/
|
|
68
|
+
/**
|
|
69
|
+
* Validate that buffer has a recognized image header (PNG, JPEG, WebP, GIF, SVG).
|
|
70
|
+
*/
|
|
71
|
+
export function validateImageSignature(bytes, refName = 'input') {
|
|
72
|
+
if (!bytes || bytes.length < 4) {
|
|
73
|
+
throw new Error(`Invalid image data in ${refName}: file is too small to be a valid image`)
|
|
74
|
+
}
|
|
75
|
+
const isPng = bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47
|
|
76
|
+
const isJpeg = bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF
|
|
77
|
+
const isWebp = bytes.length >= 12 && bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
|
|
78
|
+
bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50
|
|
79
|
+
const isGif = bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38
|
|
80
|
+
const head = bytes.slice(0, 128).toString('utf8').toLowerCase()
|
|
81
|
+
const isSvg = head.includes('<svg') || (head.includes('<?xml') && head.includes('<svg'))
|
|
82
|
+
|
|
83
|
+
if (!isPng && !isJpeg && !isWebp && !isGif && !isSvg) {
|
|
84
|
+
throw new Error(`Invalid image file format in ${refName}: file does not match any recognized image signature (PNG, JPEG, WebP, GIF, SVG)`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
export function detectMediaType(filePath, buffer) {
|
|
69
89
|
if (buffer && buffer.length >= 8) {
|
|
70
90
|
// PNG signature: 89 50 4E 47 0D 0A 1A 0A
|
|
@@ -135,15 +155,26 @@ export async function resolveConversationImage(ctx, exec, targetRef) {
|
|
|
135
155
|
}
|
|
136
156
|
}
|
|
137
157
|
|
|
138
|
-
// 2. Data URI
|
|
158
|
+
// 2. Data URI (enforce pre-allocation bounded check)
|
|
139
159
|
if (effectiveRef.startsWith('data:')) {
|
|
140
160
|
const commaIndex = effectiveRef.indexOf(',')
|
|
141
161
|
if (commaIndex > 0) {
|
|
142
162
|
const meta = effectiveRef.slice(0, commaIndex)
|
|
143
|
-
const
|
|
163
|
+
const b64Data = effectiveRef.slice(commaIndex + 1).trim()
|
|
164
|
+
const approxBytes = Math.ceil((b64Data.length * 3) / 4)
|
|
165
|
+
const MAX_IMAGE_BYTES = 50 * 1024 * 1024
|
|
166
|
+
if (approxBytes > MAX_IMAGE_BYTES) {
|
|
167
|
+
throw new Error(`Data URI payload too large (~${(approxBytes / (1024 * 1024)).toFixed(1)} MB). Maximum allowed size is 50 MB.`)
|
|
168
|
+
}
|
|
169
|
+
|
|
144
170
|
const mediaMatch = meta.match(/data:([^;]+)/)
|
|
145
171
|
const mediaType = mediaMatch ? mediaMatch[1] : 'image/png'
|
|
146
|
-
const bytes = Buffer.from(
|
|
172
|
+
const bytes = Buffer.from(b64Data, 'base64')
|
|
173
|
+
if (bytes.length > MAX_IMAGE_BYTES) {
|
|
174
|
+
throw new Error(`Decoded image data too large (${(bytes.length / (1024 * 1024)).toFixed(1)} MB). Maximum allowed size is 50 MB.`)
|
|
175
|
+
}
|
|
176
|
+
validateImageSignature(bytes, 'inline data URI')
|
|
177
|
+
|
|
147
178
|
return {
|
|
148
179
|
bytes,
|
|
149
180
|
mediaType,
|
|
@@ -151,25 +182,34 @@ export async function resolveConversationImage(ctx, exec, targetRef) {
|
|
|
151
182
|
name: 'inline-image',
|
|
152
183
|
}
|
|
153
184
|
}
|
|
185
|
+
throw new Error('Malformed data URI: missing comma separator')
|
|
154
186
|
}
|
|
155
187
|
|
|
156
|
-
// 3. Local filesystem resolution
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
// 3. Local filesystem resolution with strict workspace containment check
|
|
189
|
+
const root = sessionCwd ? path.resolve(sessionCwd) : process.cwd()
|
|
190
|
+
const absPath = path.resolve(root, effectiveRef)
|
|
191
|
+
const rel = path.relative(root, absPath)
|
|
192
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
193
|
+
throw new Error(`Access denied: path "${effectiveRef}" is outside allowed workspace directory "${root}"`)
|
|
194
|
+
}
|
|
160
195
|
|
|
161
196
|
const fileStat = await stat(absPath).catch(() => null)
|
|
162
197
|
if (!fileStat) {
|
|
163
198
|
throw new Error(`Image file not found: ${effectiveRef}`)
|
|
164
199
|
}
|
|
200
|
+
if (!fileStat.isFile()) {
|
|
201
|
+
throw new Error(`Target is not a regular file: ${effectiveRef}`)
|
|
202
|
+
}
|
|
165
203
|
if (fileStat.size === 0) {
|
|
166
204
|
throw new Error(`Image file is empty (0 bytes): ${effectiveRef}`)
|
|
167
205
|
}
|
|
168
|
-
|
|
206
|
+
const MAX_IMAGE_BYTES = 50 * 1024 * 1024
|
|
207
|
+
if (fileStat.size > MAX_IMAGE_BYTES) {
|
|
169
208
|
throw new Error(`Image file too large (${(fileStat.size / (1024 * 1024)).toFixed(1)} MB). Maximum allowed size is 50 MB.`)
|
|
170
209
|
}
|
|
171
210
|
|
|
172
211
|
const bytes = await readFile(absPath)
|
|
212
|
+
validateImageSignature(bytes, effectiveRef)
|
|
173
213
|
const mediaType = detectMediaType(absPath, bytes)
|
|
174
214
|
return {
|
|
175
215
|
bytes,
|
package/lib/vault.js
CHANGED
|
@@ -99,6 +99,37 @@ export function filterVaultEntries(entries, options = {}, existsFn = existsSync)
|
|
|
99
99
|
/**
|
|
100
100
|
* Delete entry and associated disk files (image + sidecar json).
|
|
101
101
|
*/
|
|
102
|
+
export const MAX_VAULT_BODY_BYTES = 64 * 1024 // 64 KiB bounded limit (#306)
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reads request body with a hard byte ceiling, aborting early with 413 if exceeded.
|
|
106
|
+
*/
|
|
107
|
+
export async function readBoundedRequestBody(req, maxBytes = MAX_VAULT_BODY_BYTES) {
|
|
108
|
+
const clHeader = req?.headers && (req.headers['content-length'] || req.headers['Content-Length'])
|
|
109
|
+
if (clHeader !== undefined) {
|
|
110
|
+
const cl = Number(clHeader)
|
|
111
|
+
if (Number.isFinite(cl) && cl > maxBytes) {
|
|
112
|
+
const err = new Error(`Payload Too Large: Content-Length ${cl} exceeds maximum allowed size of ${maxBytes} bytes`)
|
|
113
|
+
err.statusCode = 413
|
|
114
|
+
throw err
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let received = 0
|
|
119
|
+
const chunks = []
|
|
120
|
+
for await (const chunk of req) {
|
|
121
|
+
received += chunk.length
|
|
122
|
+
if (received > maxBytes) {
|
|
123
|
+
if (typeof req.destroy === 'function') req.destroy()
|
|
124
|
+
const err = new Error(`Payload Too Large: request body exceeded maximum allowed size of ${maxBytes} bytes`)
|
|
125
|
+
err.statusCode = 413
|
|
126
|
+
throw err
|
|
127
|
+
}
|
|
128
|
+
chunks.push(chunk)
|
|
129
|
+
}
|
|
130
|
+
return Buffer.concat(chunks)
|
|
131
|
+
}
|
|
132
|
+
|
|
102
133
|
export async function deleteVaultEntry(id) {
|
|
103
134
|
if (!id) return { success: false, error: 'Missing entry id' }
|
|
104
135
|
const entries = await readHistory()
|
|
@@ -157,11 +188,19 @@ export function registerVaultRoutes(ctx) {
|
|
|
157
188
|
|
|
158
189
|
if (!id) {
|
|
159
190
|
try {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
191
|
+
const buf = await readBoundedRequestBody(req, MAX_VAULT_BODY_BYTES)
|
|
192
|
+
if (buf && buf.length > 0) {
|
|
193
|
+
const body = JSON.parse(buf.toString('utf-8'))
|
|
194
|
+
id = body?.id
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (err?.statusCode === 413) {
|
|
198
|
+
res.writeHead(413, { 'Content-Type': 'application/json' })
|
|
199
|
+
res.end(JSON.stringify({ ok: false, error: err.message }))
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
/* body parse error falls through to missing id check */
|
|
203
|
+
}
|
|
165
204
|
}
|
|
166
205
|
|
|
167
206
|
if (!id) {
|
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.5",
|
|
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",
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
72
|
"test": "node scripts/build-client.mjs && node --check lib/client.js && node --test test/*.test.mjs",
|
|
73
|
-
"build:client": "node scripts/build-client.mjs"
|
|
73
|
+
"build:client": "node scripts/build-client.mjs",
|
|
74
|
+
"publish:github": "bash scripts/publish-github.sh"
|
|
74
75
|
},
|
|
75
76
|
"publishConfig": {
|
|
76
77
|
"access": "public"
|