@muyichengshayu/promptx 0.1.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.
@@ -0,0 +1,484 @@
1
+ import { customAlphabet } from 'nanoid'
2
+ import {
3
+ BLOCK_TYPES,
4
+ buildRawTaskText,
5
+ clampText,
6
+ deriveTitleFromBlocks,
7
+ getExpiryValue,
8
+ normalizeExpiry,
9
+ normalizeVisibility,
10
+ resolveExpiresAt,
11
+ slugifyTitle,
12
+ summarizeTask,
13
+ } from '../../../packages/shared/src/index.js'
14
+ import { all, get, run, transaction } from './db.js'
15
+
16
+ const slugTail = customAlphabet('abcdefghijkmnpqrstuvwxyz23456789', 6)
17
+ const tokenId = customAlphabet('abcdefghijkmnpqrstuvwxyz23456789', 20)
18
+
19
+ function toBlock(row) {
20
+ return {
21
+ id: Number(row.id),
22
+ type: row.type,
23
+ content: row.content,
24
+ sortOrder: Number(row.sort_order),
25
+ meta: JSON.parse(row.meta_json || '{}'),
26
+ }
27
+ }
28
+
29
+ function toTask(row, blocks = [], options = {}) {
30
+ const codexRunCount = Math.max(0, Number(options.codexRunCount) || 0)
31
+ const displayTitle = row.title || row.auto_title || deriveTitleFromBlocks(blocks)
32
+ return {
33
+ id: Number(row.id),
34
+ slug: row.slug,
35
+ title: row.title,
36
+ autoTitle: row.auto_title || '',
37
+ lastPromptPreview: row.last_prompt_preview || '',
38
+ codexSessionId: row.codex_session_id || '',
39
+ displayTitle,
40
+ visibility: row.visibility,
41
+ expiresAt: row.expires_at,
42
+ expiry: getExpiryValue(row.expires_at),
43
+ codexRunCount,
44
+ createdAt: row.created_at,
45
+ updatedAt: row.updated_at,
46
+ blocks,
47
+ }
48
+ }
49
+
50
+ function ensureSlug(title) {
51
+ const base = slugifyTitle(title)
52
+ let slug = `${base}-${slugTail()}`
53
+ while (get('SELECT 1 FROM tasks WHERE slug = ?', [slug])) {
54
+ slug = `${base}-${slugTail()}`
55
+ }
56
+ return slug
57
+ }
58
+
59
+ function isExpired(task) {
60
+ return Boolean(task.expiresAt && new Date(task.expiresAt).getTime() <= Date.now())
61
+ }
62
+
63
+ function loadBlocks(taskId) {
64
+ return all(
65
+ `SELECT id, type, content, sort_order, meta_json
66
+ FROM blocks
67
+ WHERE task_id = ?
68
+ ORDER BY sort_order ASC, id ASC`,
69
+ [taskId]
70
+ ).map(toBlock)
71
+ }
72
+
73
+ function loadBlocksForTasks(taskIds = []) {
74
+ if (!taskIds.length) {
75
+ return new Map()
76
+ }
77
+
78
+ const placeholders = taskIds.map(() => '?').join(', ')
79
+ const rows = all(
80
+ `SELECT task_id, type, content, sort_order, id
81
+ FROM blocks
82
+ WHERE task_id IN (${placeholders})
83
+ ORDER BY task_id ASC, sort_order ASC, id ASC`,
84
+ taskIds
85
+ )
86
+
87
+ const grouped = new Map()
88
+ rows.forEach((row) => {
89
+ const taskId = Number(row.task_id)
90
+ if (!grouped.has(taskId)) {
91
+ grouped.set(taskId, [])
92
+ }
93
+ grouped.get(taskId).push({
94
+ type: row.type,
95
+ content: row.content,
96
+ })
97
+ })
98
+
99
+ return grouped
100
+ }
101
+
102
+ function loadListMetadata(taskIds = []) {
103
+ if (!taskIds.length) {
104
+ return {
105
+ blockCountByTaskId: new Map(),
106
+ firstTextByTaskId: new Map(),
107
+ }
108
+ }
109
+
110
+ const placeholders = taskIds.map(() => '?').join(', ')
111
+ const countRows = all(
112
+ `SELECT task_id, COUNT(*) AS block_count
113
+ FROM blocks
114
+ WHERE task_id IN (${placeholders})
115
+ GROUP BY task_id`,
116
+ taskIds
117
+ )
118
+ const firstTextRows = all(
119
+ `SELECT task_id, content
120
+ FROM (
121
+ SELECT
122
+ task_id,
123
+ content,
124
+ ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY sort_order ASC, id ASC) AS row_num
125
+ FROM blocks
126
+ WHERE task_id IN (${placeholders})
127
+ AND type IN (?, ?)
128
+ AND TRIM(content) != ''
129
+ ) ranked
130
+ WHERE row_num = 1`,
131
+ [...taskIds, BLOCK_TYPES.TEXT, BLOCK_TYPES.IMPORTED_TEXT]
132
+ )
133
+
134
+ return {
135
+ blockCountByTaskId: new Map(
136
+ countRows.map((row) => [Number(row.task_id), Number(row.block_count)])
137
+ ),
138
+ firstTextByTaskId: new Map(
139
+ firstTextRows.map((row) => [Number(row.task_id), row.content || ''])
140
+ ),
141
+ }
142
+ }
143
+
144
+ function getCodexRunCountByTaskSlug(taskSlug = '') {
145
+ return Math.max(
146
+ 0,
147
+ Number(
148
+ get(
149
+ `SELECT COUNT(*) AS count
150
+ FROM codex_runs
151
+ WHERE task_slug = ?`,
152
+ [String(taskSlug || '').trim()]
153
+ )?.count || 0
154
+ )
155
+ )
156
+ }
157
+
158
+ function loadCodexRunCounts(taskSlugs = []) {
159
+ const normalizedSlugs = [...new Set(taskSlugs.map((slug) => String(slug || '').trim()).filter(Boolean))]
160
+ if (!normalizedSlugs.length) {
161
+ return new Map()
162
+ }
163
+
164
+ const placeholders = normalizedSlugs.map(() => '?').join(', ')
165
+ const rows = all(
166
+ `SELECT task_slug, COUNT(*) AS count
167
+ FROM codex_runs
168
+ WHERE task_slug IN (${placeholders})
169
+ GROUP BY task_slug`,
170
+ normalizedSlugs
171
+ )
172
+
173
+ return new Map(
174
+ rows.map((row) => [String(row.task_slug || '').trim(), Math.max(0, Number(row.count) || 0)])
175
+ )
176
+ }
177
+
178
+ function collectImagePaths(blocks = []) {
179
+ return blocks
180
+ .filter((block) => block.type === BLOCK_TYPES.IMAGE && block.content)
181
+ .map((block) => block.content)
182
+ }
183
+
184
+ function mapTaskSummary(row, firstText = '', blockCount = 0, codexRunCount = 0) {
185
+ const textBlock = firstText
186
+ ? [{ type: BLOCK_TYPES.TEXT, content: firstText }]
187
+ : []
188
+
189
+ return {
190
+ slug: row.slug,
191
+ title: row.title || '',
192
+ autoTitle: row.auto_title || deriveTitleFromBlocks(textBlock) || '',
193
+ lastPromptPreview: row.last_prompt_preview || '',
194
+ codexSessionId: row.codex_session_id || '',
195
+ createdAt: row.created_at,
196
+ updatedAt: row.updated_at,
197
+ visibility: row.visibility,
198
+ expiresAt: row.expires_at,
199
+ preview: summarizeTask({ blocks: textBlock }),
200
+ codexRunCount: Math.max(0, Number(codexRunCount) || 0),
201
+ blockCount,
202
+ }
203
+ }
204
+
205
+ function normalizeBlockInput(block = {}) {
206
+ const type =
207
+ block.type === BLOCK_TYPES.IMAGE
208
+ ? BLOCK_TYPES.IMAGE
209
+ : block.type === BLOCK_TYPES.IMPORTED_TEXT
210
+ ? BLOCK_TYPES.IMPORTED_TEXT
211
+ : BLOCK_TYPES.TEXT
212
+ const content = clampText(
213
+ block.content || '',
214
+ type === BLOCK_TYPES.IMAGE ? 1000 : 50000
215
+ )
216
+ const meta =
217
+ type === BLOCK_TYPES.IMPORTED_TEXT
218
+ ? {
219
+ fileName: clampText(block.meta?.fileName || '', 180),
220
+ collapsed: Boolean(block.meta?.collapsed),
221
+ }
222
+ : {}
223
+
224
+ return {
225
+ id: Number.isInteger(Number(block.id)) ? Number(block.id) : null,
226
+ type,
227
+ content,
228
+ meta,
229
+ metaJson: JSON.stringify(meta),
230
+ }
231
+ }
232
+
233
+ export function listTasks(limit = 30) {
234
+ const rows = all(
235
+ `SELECT id, slug, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at
236
+ FROM tasks
237
+ ORDER BY updated_at DESC
238
+ LIMIT ?`,
239
+ [Math.max(1, Number(limit) || 30)]
240
+ )
241
+
242
+ const taskIds = rows.map((row) => Number(row.id))
243
+ const {
244
+ blockCountByTaskId,
245
+ firstTextByTaskId,
246
+ } = loadListMetadata(taskIds)
247
+ const codexRunCountBySlug = loadCodexRunCounts(rows.map((row) => row.slug))
248
+
249
+ return rows.map((row) =>
250
+ mapTaskSummary(
251
+ row,
252
+ firstTextByTaskId.get(Number(row.id)) || '',
253
+ blockCountByTaskId.get(Number(row.id)) || 0,
254
+ codexRunCountBySlug.get(String(row.slug || '').trim()) || 0
255
+ )
256
+ )
257
+ }
258
+
259
+ export function getTaskBySlug(slug) {
260
+ const row = get(
261
+ `SELECT id, slug, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at
262
+ FROM tasks
263
+ WHERE slug = ?`,
264
+ [slug]
265
+ )
266
+
267
+ if (!row) {
268
+ return null
269
+ }
270
+
271
+ const task = toTask(row, loadBlocks(row.id), {
272
+ codexRunCount: getCodexRunCountByTaskSlug(row.slug),
273
+ })
274
+ return isExpired(task) ? { ...task, expired: true } : task
275
+ }
276
+
277
+ export function createTask(input = {}) {
278
+ const now = new Date().toISOString()
279
+ const title = clampText(input.title || '', 140)
280
+ const autoTitle = clampText(input.autoTitle || '', 140)
281
+ const lastPromptPreview = clampText(input.lastPromptPreview || '', 280)
282
+ const codexSessionId = clampText(input.codexSessionId || '', 120)
283
+ const visibility = normalizeVisibility(input.visibility)
284
+ const expiresAt = resolveExpiresAt(normalizeExpiry(input.expiry || 'none'))
285
+ const slug = ensureSlug(title)
286
+ const editToken = tokenId()
287
+
288
+ transaction(() => {
289
+ run(
290
+ `INSERT INTO tasks (slug, edit_token, title, auto_title, last_prompt_preview, codex_session_id, visibility, expires_at, created_at, updated_at)
291
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
292
+ [slug, editToken, title, autoTitle, lastPromptPreview, codexSessionId, visibility, expiresAt, now, now]
293
+ )
294
+ })
295
+
296
+ return {
297
+ ...getTaskBySlug(slug),
298
+ editToken,
299
+ }
300
+ }
301
+
302
+ export function updateTask(slug, input = {}) {
303
+ const existing = get(
304
+ `SELECT id, edit_token, codex_session_id
305
+ FROM tasks
306
+ WHERE slug = ?`,
307
+ [slug]
308
+ )
309
+ if (!existing) {
310
+ return { error: 'not_found' }
311
+ }
312
+
313
+ const title = clampText(input.title || '', 140)
314
+ const autoTitle = clampText(input.autoTitle || '', 140)
315
+ const lastPromptPreview = clampText(input.lastPromptPreview || '', 280)
316
+ const codexSessionId = Object.prototype.hasOwnProperty.call(input, 'codexSessionId')
317
+ ? clampText(input.codexSessionId || '', 120)
318
+ : String(existing.codex_session_id || '')
319
+ const visibility = normalizeVisibility(input.visibility)
320
+ const expiresAt = resolveExpiresAt(normalizeExpiry(input.expiry || 'none'))
321
+ const updatedAt = new Date().toISOString()
322
+ const blocks = Array.isArray(input.blocks) ? input.blocks.map(normalizeBlockInput) : []
323
+ const currentBlocks = loadBlocks(existing.id)
324
+ const currentBlockMap = new Map(currentBlocks.map((block) => [block.id, block]))
325
+
326
+ transaction(() => {
327
+ run(
328
+ `UPDATE tasks
329
+ SET title = ?, auto_title = ?, last_prompt_preview = ?, codex_session_id = ?, visibility = ?, expires_at = ?, updated_at = ?
330
+ WHERE slug = ?`,
331
+ [title, autoTitle, lastPromptPreview, codexSessionId, visibility, expiresAt, updatedAt, slug]
332
+ )
333
+
334
+ const incomingIds = new Set()
335
+
336
+ blocks.forEach((block, index) => {
337
+ const currentBlock = block.id ? currentBlockMap.get(block.id) : null
338
+
339
+ if (currentBlock) {
340
+ incomingIds.add(currentBlock.id)
341
+
342
+ const currentMetaJson = JSON.stringify(currentBlock.meta || {})
343
+ const unchanged =
344
+ currentBlock.type === block.type
345
+ && currentBlock.content === block.content
346
+ && currentBlock.sortOrder === index
347
+ && currentMetaJson === block.metaJson
348
+
349
+ if (!unchanged) {
350
+ run(
351
+ `UPDATE blocks
352
+ SET type = ?, content = ?, sort_order = ?, meta_json = ?
353
+ WHERE id = ? AND task_id = ?`,
354
+ [block.type, block.content, index, block.metaJson, currentBlock.id, existing.id]
355
+ )
356
+ }
357
+ return
358
+ }
359
+
360
+ run(
361
+ `INSERT INTO blocks (task_id, type, content, sort_order, meta_json, created_at)
362
+ VALUES (?, ?, ?, ?, ?, ?)`,
363
+ [existing.id, block.type, block.content, index, block.metaJson, updatedAt]
364
+ )
365
+ })
366
+
367
+ currentBlocks.forEach((block) => {
368
+ if (!incomingIds.has(block.id)) {
369
+ run('DELETE FROM blocks WHERE id = ? AND task_id = ?', [block.id, existing.id])
370
+ }
371
+ })
372
+ })
373
+
374
+ return getTaskBySlug(slug)
375
+ }
376
+
377
+ export function deleteTask(slug) {
378
+ const row = get('SELECT id, edit_token FROM tasks WHERE slug = ?', [slug])
379
+ if (!row) {
380
+ return { error: 'not_found' }
381
+ }
382
+
383
+ const blocks = loadBlocks(row.id)
384
+ const removedAssets = collectImagePaths(blocks)
385
+
386
+ transaction(() => {
387
+ run('DELETE FROM tasks WHERE slug = ?', [slug])
388
+ })
389
+
390
+ return { ok: true, removedAssets }
391
+ }
392
+
393
+ export function purgeExpiredTasks(now = new Date().toISOString()) {
394
+ const rows = all(
395
+ `SELECT id
396
+ FROM tasks
397
+ WHERE expires_at IS NOT NULL
398
+ AND expires_at <= ?`,
399
+ [now]
400
+ )
401
+
402
+ if (!rows.length) {
403
+ return { removedAssets: [], removedCount: 0 }
404
+ }
405
+
406
+ const taskIds = rows.map((row) => Number(row.id))
407
+ const blocksByTaskId = loadBlocksForTasks(taskIds)
408
+ const removedAssets = taskIds.flatMap((taskId) =>
409
+ collectImagePaths(blocksByTaskId.get(taskId) || [])
410
+ )
411
+
412
+ const placeholders = taskIds.map(() => '?').join(', ')
413
+ transaction(() => {
414
+ run(`DELETE FROM tasks WHERE id IN (${placeholders})`, taskIds)
415
+ })
416
+
417
+ return {
418
+ removedAssets,
419
+ removedCount: taskIds.length,
420
+ }
421
+ }
422
+
423
+ export function buildTaskExports(task) {
424
+ return {
425
+ raw: buildRawTaskText(task),
426
+ }
427
+ }
428
+
429
+ export function canEditTask(slug) {
430
+ return Boolean(get('SELECT 1 FROM tasks WHERE slug = ?', [slug]))
431
+ }
432
+
433
+ export function updateTaskCodexSession(slug, codexSessionId = '') {
434
+ const existing = get('SELECT slug FROM tasks WHERE slug = ?', [slug])
435
+ if (!existing) {
436
+ return null
437
+ }
438
+
439
+ const normalizedSessionId = String(codexSessionId || '').trim()
440
+ const updatedAt = new Date().toISOString()
441
+
442
+ transaction(() => {
443
+ run(
444
+ `UPDATE tasks
445
+ SET codex_session_id = ?, updated_at = ?
446
+ WHERE slug = ?`,
447
+ [normalizedSessionId, updatedAt, slug]
448
+ )
449
+ })
450
+
451
+ return getTaskBySlug(slug)
452
+ }
453
+
454
+ export function clearTaskCodexSessionReferences(codexSessionId = '') {
455
+ const normalizedSessionId = String(codexSessionId || '').trim()
456
+ if (!normalizedSessionId) {
457
+ return []
458
+ }
459
+
460
+ const matchedRows = all(
461
+ `SELECT slug
462
+ FROM tasks
463
+ WHERE codex_session_id = ?`,
464
+ [normalizedSessionId]
465
+ )
466
+ const matchedTaskSlugs = matchedRows
467
+ .map((row) => String(row?.slug || '').trim())
468
+ .filter(Boolean)
469
+
470
+ if (!matchedTaskSlugs.length) {
471
+ return []
472
+ }
473
+
474
+ transaction(() => {
475
+ run(
476
+ `UPDATE tasks
477
+ SET codex_session_id = ''
478
+ WHERE codex_session_id = ?`,
479
+ [normalizedSessionId]
480
+ )
481
+ })
482
+
483
+ return matchedTaskSlugs
484
+ }
@@ -0,0 +1,69 @@
1
+ export function createSseHub(options = {}) {
2
+ const clients = new Set()
3
+ const pingIntervalMs = Math.max(1000, Number(options.pingIntervalMs) || 15000)
4
+ let nextEventId = 0
5
+
6
+ function createSseMessage(payload) {
7
+ return `id: ${++nextEventId}\ndata: ${JSON.stringify(payload)}\n\n`
8
+ }
9
+
10
+ function write(target, payload) {
11
+ if (!target || target.destroyed || target.writableEnded) {
12
+ return false
13
+ }
14
+
15
+ try {
16
+ target.write(createSseMessage(payload))
17
+ return true
18
+ } catch {
19
+ return false
20
+ }
21
+ }
22
+
23
+ function broadcast(type, payload = {}) {
24
+ const message = {
25
+ type: String(type || '').trim(),
26
+ sentAt: new Date().toISOString(),
27
+ ...payload,
28
+ }
29
+
30
+ for (const client of [...clients]) {
31
+ if (!write(client, message)) {
32
+ clients.delete(client)
33
+ }
34
+ }
35
+ }
36
+
37
+ function addClient(target) {
38
+ if (!target) {
39
+ return () => {}
40
+ }
41
+
42
+ clients.add(target)
43
+ return () => {
44
+ clients.delete(target)
45
+ }
46
+ }
47
+
48
+ const pingTimer = setInterval(() => {
49
+ for (const client of [...clients]) {
50
+ if (!client || client.destroyed || client.writableEnded) {
51
+ clients.delete(client)
52
+ continue
53
+ }
54
+
55
+ try {
56
+ client.write(': ping\n\n')
57
+ } catch {
58
+ clients.delete(client)
59
+ }
60
+ }
61
+ }, pingIntervalMs)
62
+ pingTimer.unref?.()
63
+
64
+ return {
65
+ addClient,
66
+ broadcast,
67
+ write,
68
+ }
69
+ }
@@ -0,0 +1,22 @@
1
+ import path from 'node:path'
2
+ import { nanoid } from 'nanoid'
3
+
4
+ export function normalizeUploadFileName(fileName = '', fallback = 'file') {
5
+ const normalized = String(fileName || '')
6
+ .trim()
7
+ .split(/[\\/]/)
8
+ .pop()
9
+ return normalized || fallback
10
+ }
11
+
12
+ export function getSafeTempExtension(fileName = '', fallback = '') {
13
+ const extension = path.extname(normalizeUploadFileName(fileName)).toLowerCase()
14
+ if (/^\.[a-z0-9]{1,10}$/.test(extension)) {
15
+ return extension
16
+ }
17
+ return fallback
18
+ }
19
+
20
+ export function createTempFilePath(tmpDir, fileName = '', fallbackExt = '') {
21
+ return path.join(tmpDir, `${nanoid(12)}${getSafeTempExtension(fileName, fallbackExt)}`)
22
+ }