@goodandready/dsh-image-gen 0.10.34 → 0.11.0

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/vault.js ADDED
@@ -0,0 +1,183 @@
1
+ import { existsSync, unlinkSync } from 'node:fs'
2
+ import { readHistory, writeHistory } from './history.js'
3
+ import { isTrustedLocalRequest } from './security.js'
4
+
5
+ /**
6
+ * Match image dimensions against an aspect ratio label or ratio value.
7
+ */
8
+ export function matchAspectRatio(width, height, targetAspect) {
9
+ if (!targetAspect || targetAspect === 'any' || targetAspect === 'all') return true
10
+ if (!width || !height) return false
11
+ const ratio = width / height
12
+
13
+ switch (targetAspect) {
14
+ case '1:1':
15
+ case 'square':
16
+ return Math.abs(ratio - 1.0) < 0.1
17
+ case '16:9':
18
+ case 'landscape':
19
+ return Math.abs(ratio - (16 / 9)) < 0.15
20
+ case '9:16':
21
+ case 'portrait':
22
+ return Math.abs(ratio - (9 / 16)) < 0.15
23
+ case '4:3':
24
+ return Math.abs(ratio - (4 / 3)) < 0.15
25
+ case '3:4':
26
+ return Math.abs(ratio - (3 / 4)) < 0.15
27
+ case '21:9':
28
+ case 'ultrawide':
29
+ return Math.abs(ratio - (21 / 9)) < 0.2
30
+ default:
31
+ return true
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Filter, sort, and paginate vault entries with path sanitization.
37
+ */
38
+ export function filterVaultEntries(entries, options = {}, existsFn = existsSync) {
39
+ const {
40
+ q = '',
41
+ provider = '',
42
+ aspect = '',
43
+ sort = 'newest',
44
+ offset = 0,
45
+ limit = 30,
46
+ } = options
47
+
48
+ const safeOffset = Math.max(0, Number.parseInt(offset, 10) || 0)
49
+ const safeLimit = Math.min(100, Math.max(1, Number.parseInt(limit, 10) || 30))
50
+ const cleanQ = String(q).trim().toLowerCase()
51
+ const cleanProvider = String(provider).trim().toLowerCase()
52
+
53
+ const existing = (Array.isArray(entries) ? entries : []).filter((e) => e && e.path && existsFn(e.path))
54
+
55
+ const filtered = existing.filter((e) => {
56
+ if (cleanQ && !String(e.prompt || '').toLowerCase().includes(cleanQ)) {
57
+ return false
58
+ }
59
+ if (cleanProvider && String(e.provider || '').toLowerCase() !== cleanProvider) {
60
+ return false
61
+ }
62
+ if (aspect && !matchAspectRatio(e.width, e.height, aspect)) {
63
+ return false
64
+ }
65
+ return true
66
+ })
67
+
68
+ filtered.sort((a, b) => {
69
+ const timeA = a.createdAt ? Date.parse(a.createdAt) : 0
70
+ const timeB = b.createdAt ? Date.parse(b.createdAt) : 0
71
+ if (sort === 'oldest') {
72
+ return timeA - timeB
73
+ }
74
+ return timeB - timeA
75
+ })
76
+
77
+ const total = filtered.length
78
+ const sliced = filtered.slice(safeOffset, safeOffset + safeLimit)
79
+
80
+ const items = sliced.map((e) => {
81
+ const { path: _discardPath, ...safeEntry } = e
82
+ return {
83
+ id: e.id || e.attachmentId || String(e.createdAt || Math.random()),
84
+ ...safeEntry,
85
+ thumbnailUrl: e.thumbnailUrl || (e.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(e.attachmentId)}` : ''),
86
+ url: e.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(e.attachmentId)}` : '',
87
+ }
88
+ })
89
+
90
+ return {
91
+ items,
92
+ total,
93
+ offset: safeOffset,
94
+ limit: safeLimit,
95
+ hasMore: safeOffset + safeLimit < total,
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Delete entry and associated disk files (image + sidecar json).
101
+ */
102
+ export async function deleteVaultEntry(id) {
103
+ if (!id) return { success: false, error: 'Missing entry id' }
104
+ const entries = await readHistory()
105
+ const idx = entries.findIndex((e) => e.id === id || e.attachmentId === id)
106
+ if (idx === -1) {
107
+ return { success: false, error: 'Vault entry not found' }
108
+ }
109
+
110
+ const [entry] = entries.splice(idx, 1)
111
+ if (entry && entry.path) {
112
+ try {
113
+ unlinkSync(entry.path)
114
+ } catch (_) { /* ignore missing file */ }
115
+ try {
116
+ unlinkSync(entry.path.replace(/\.[^.]+$/, '.json'))
117
+ } catch (_) { /* ignore missing sidecar */ }
118
+ }
119
+
120
+ await writeHistory(entries)
121
+ return { success: true, id }
122
+ }
123
+
124
+ /**
125
+ * Register vault HTTP routes on DSH webServer.
126
+ */
127
+ export function registerVaultRoutes(ctx) {
128
+ ctx.effect(() => ctx.webServer.register({
129
+ kind: 'exact',
130
+ path: '/dsh-image-gen/vault',
131
+ handler: async (req, res) => {
132
+ if (!isTrustedLocalRequest(req)) {
133
+ res.writeHead(403, { 'Content-Type': 'application/json' })
134
+ res.end(JSON.stringify({ error: 'forbidden' }))
135
+ return
136
+ }
137
+
138
+ if (req.method === 'GET') {
139
+ const u = new URL(req.url, 'http://localhost')
140
+ const q = u.searchParams.get('q') || ''
141
+ const provider = u.searchParams.get('provider') || ''
142
+ const aspect = u.searchParams.get('aspect') || ''
143
+ const sort = u.searchParams.get('sort') || 'newest'
144
+ const offset = u.searchParams.get('offset') || '0'
145
+ const limit = u.searchParams.get('limit') || '30'
146
+
147
+ const entries = await readHistory()
148
+ const result = filterVaultEntries(entries, { q, provider, aspect, sort, offset, limit })
149
+ res.writeHead(200, { 'Content-Type': 'application/json' })
150
+ res.end(JSON.stringify({ ok: true, ...result }))
151
+ return
152
+ }
153
+
154
+ if (req.method === 'DELETE') {
155
+ const u = new URL(req.url, 'http://localhost')
156
+ let id = u.searchParams.get('id')
157
+
158
+ if (!id) {
159
+ try {
160
+ const chunks = []
161
+ for await (const chunk of req) chunks.push(chunk)
162
+ const body = JSON.parse(Buffer.concat(chunks).toString('utf-8'))
163
+ id = body.id
164
+ } catch (_) { /* body parse error */ }
165
+ }
166
+
167
+ if (!id) {
168
+ res.writeHead(400, { 'Content-Type': 'application/json' })
169
+ res.end(JSON.stringify({ ok: false, error: 'Missing id parameter' }))
170
+ return
171
+ }
172
+
173
+ const result = await deleteVaultEntry(id)
174
+ res.writeHead(result.success ? 200 : 404, { 'Content-Type': 'application/json' })
175
+ res.end(JSON.stringify({ ok: result.success, ...result }))
176
+ return
177
+ }
178
+
179
+ res.writeHead(405, { 'Content-Type': 'application/json' })
180
+ res.end(JSON.stringify({ error: 'Method not allowed' }))
181
+ },
182
+ }), 'dsh-image-gen: vault route')
183
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.10.34",
3
+ "version": "0.11.0",
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",