@shawnstack/quickforge 1.7.7 → 1.7.8

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.
Files changed (83) hide show
  1. package/dist/assets/AgentProfilesPage-BSM5w3bn.js +1 -0
  2. package/dist/assets/ChatPanelHost-cd80S4EX.js +48 -0
  3. package/dist/assets/CloudAccountSettingsPage-BJMrfgy9.js +1 -0
  4. package/dist/assets/{PluginsPage-CPE5tbbf.js → PluginsPage-mJkTmVVV.js} +1 -1
  5. package/dist/assets/ScheduledTasksPage-DJj_NLm9.js +2 -0
  6. package/dist/assets/{SettingsWorkspacePage-BE2T4d3P.js → SettingsWorkspacePage-7lQ0Wl63.js} +389 -389
  7. package/dist/assets/ShareLinksSettingsPage-DjFq_Lnb.js +1 -0
  8. package/dist/assets/SharedConversationPage-BbuNpcpP.js +1 -0
  9. package/dist/assets/TerminalDock-CczcCJJE.js +2 -0
  10. package/dist/assets/WorkspaceInspector-0i6x-diB.js +36 -0
  11. package/dist/assets/{icons-2D18YbSI.js → icons-DM_qJ7VJ.js} +1 -1
  12. package/dist/assets/{index-BiCTgUlY.css → index-D-J_8Smf.css} +1 -1
  13. package/dist/assets/index-DFASc15l.js +66 -0
  14. package/dist/assets/local-tools-Dn1Y9aPe.js +270 -0
  15. package/dist/assets/{model-display-label-C6-0eDxF.js → logger-8REuLPGK.js} +1 -1
  16. package/dist/assets/{mcp-servers-dialog-GY7sdfIr.js → mcp-servers-dialog-BG4b67ea.js} +2 -2
  17. package/dist/assets/{model-reference-o41fJ6jQ.js → model-display-label-CURwH0xM.js} +1 -1
  18. package/dist/assets/{monaco-DvXErxp2.js → monaco-bDr860xx.js} +1 -1
  19. package/dist/assets/{react-vendor-3UaA-C9W.js → react-vendor-BpWW_Nvp.js} +1 -1
  20. package/dist/assets/{skills-dialog-C1fJ1Jl7.js → skills-dialog-DIzZvQqW.js} +3 -3
  21. package/dist/index.html +8 -8
  22. package/package.json +1 -1
  23. package/server/acp/server.mjs +17 -1
  24. package/server/agent-manager.mjs +251 -31
  25. package/server/ai-http-logger.mjs +162 -19
  26. package/server/ai-provider-options.mjs +4 -1
  27. package/server/auto-archive.mjs +42 -29
  28. package/server/cloud/auto-approval.mjs +155 -0
  29. package/server/cloud/client.mjs +5 -0
  30. package/server/cloud/qf-agent-process.mjs +163 -3
  31. package/server/cloud/service-config.mjs +1 -1
  32. package/server/index.mjs +69 -13
  33. package/server/lan-access-backup.mjs +256 -0
  34. package/server/lan-access-cutover.mjs +306 -0
  35. package/server/lan-access-json-file.mjs +75 -0
  36. package/server/lan-access-service.mjs +155 -0
  37. package/server/lan-access-store.mjs +110 -9
  38. package/server/maintenance/downgrade-lan-access-v1.mjs +101 -0
  39. package/server/maintenance/downgrade-session-state-v1.mjs +130 -0
  40. package/server/maintenance/downgrade-share-v1.mjs +101 -0
  41. package/server/maintenance/export-lan-access-v1.mjs +80 -0
  42. package/server/maintenance/export-scheduled-runs-v1.mjs +45 -0
  43. package/server/maintenance/export-session-state-v1.mjs +67 -0
  44. package/server/maintenance/export-share-v1.mjs +77 -0
  45. package/server/routes/agent.mjs +37 -4
  46. package/server/routes/backup.mjs +221 -39
  47. package/server/routes/cloud.mjs +28 -1
  48. package/server/routes/scheduled-tasks.mjs +347 -134
  49. package/server/routes/shares.mjs +4 -2
  50. package/server/routes/storage.mjs +138 -8
  51. package/server/routes/workspace.mjs +265 -4
  52. package/server/scheduled-runs-backup.mjs +235 -0
  53. package/server/scheduled-runs-cutover.mjs +497 -0
  54. package/server/scheduled-task-runs-service.mjs +279 -0
  55. package/server/session-index-service.mjs +495 -0
  56. package/server/session-state-backup.mjs +277 -0
  57. package/server/session-state-cutover.mjs +370 -0
  58. package/server/session-state-service.mjs +709 -0
  59. package/server/share-backup.mjs +255 -0
  60. package/server/share-cutover.mjs +296 -0
  61. package/server/share-json-file.mjs +60 -0
  62. package/server/share-service.mjs +163 -0
  63. package/server/share-store.mjs +196 -19
  64. package/server/skills.mjs +0 -1
  65. package/server/sqlite/database.mjs +233 -0
  66. package/server/sqlite/lan-access-repository.mjs +652 -0
  67. package/server/sqlite/migrations.mjs +577 -0
  68. package/server/sqlite/scheduled-task-runs-repository.mjs +464 -0
  69. package/server/sqlite/session-index-repository.mjs +297 -0
  70. package/server/sqlite/session-state-repository.mjs +786 -0
  71. package/server/sqlite/share-repository.mjs +854 -0
  72. package/server/storage.mjs +317 -1
  73. package/dist/assets/AgentProfilesPage-BfA_sQ3A.js +0 -1
  74. package/dist/assets/ChatPanelHost-Dul1ahKj.js +0 -307
  75. package/dist/assets/CloudAccountSettingsPage-qZh0lTUN.js +0 -1
  76. package/dist/assets/ScheduledTasksPage-PTxL_PPg.js +0 -2
  77. package/dist/assets/ShareLinksSettingsPage-Bl8fP62X.js +0 -1
  78. package/dist/assets/SharedConversationPage-BK9XJnEe.js +0 -1
  79. package/dist/assets/TerminalDock-CL7HU9DI.js +0 -2
  80. package/dist/assets/WorkspaceInspector-MNBLIKMn.js +0 -36
  81. package/dist/assets/index-BTPzyq7d.js +0 -65
  82. package/dist/assets/mermaid-renderer-jEbqFaTe.js +0 -2
  83. /package/dist/assets/{plugin-api-Uu2xAB4d.js → plugin-api-BrdQeM4B.js} +0 -0
@@ -0,0 +1,297 @@
1
+ import { getSqliteStorage } from './database.mjs'
2
+
3
+ const SELECT_COLUMNS = `
4
+ scope, project_id, session_id, created_at, last_modified, message_count,
5
+ pinned_at, archived_at, is_pinned, is_archived, state_version,
6
+ metadata_json, metadata_digest, indexed_at
7
+ `
8
+
9
+ function assertStorage(storage) {
10
+ if (!storage || typeof storage.prepare !== 'function' || typeof storage.transaction !== 'function') {
11
+ throw new TypeError('Session index repository requires a SQLite storage handle')
12
+ }
13
+ return storage
14
+ }
15
+
16
+ function assertNonEmptyString(value, field) {
17
+ if (typeof value !== 'string' || value.trim().length === 0) throw new TypeError(`${field} must be a non-empty string`)
18
+ return value
19
+ }
20
+
21
+ function normalizeBucket(scope, projectId) {
22
+ if (scope === 'global') return { scope, projectId: '' }
23
+ if (scope !== 'project') throw new TypeError('scope must be global or project')
24
+ return { scope, projectId: assertNonEmptyString(projectId, 'projectId') }
25
+ }
26
+
27
+ function assertPlainObject(value, field) {
28
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
29
+ throw new TypeError(`${field} must be a plain object`)
30
+ }
31
+ return value
32
+ }
33
+
34
+ function nullableString(value, field) {
35
+ if (value === undefined || value === null) return null
36
+ if (typeof value !== 'string') throw new TypeError(`${field} must be a string or null`)
37
+ return value
38
+ }
39
+
40
+ function nullableInteger(value, field) {
41
+ if (value === undefined || value === null) return null
42
+ if (!Number.isInteger(value) || value < 0) throw new TypeError(`${field} must be a non-negative integer or null`)
43
+ return value
44
+ }
45
+
46
+ function normalizeRow(row) {
47
+ if (!row || typeof row !== 'object' || Array.isArray(row)) throw new TypeError('row must be an object')
48
+ const bucket = normalizeBucket(row.scope, row.projectId)
49
+ const metadata = assertPlainObject(row.metadata, 'metadata')
50
+ let metadataJson
51
+ try {
52
+ metadataJson = JSON.stringify(metadata)
53
+ } catch {
54
+ throw new TypeError('metadata must be JSON-serializable')
55
+ }
56
+ if (typeof row.metadataDigest !== 'string' || !/^[0-9a-f]{64}$/.test(row.metadataDigest)) {
57
+ throw new TypeError('metadataDigest must be a lowercase SHA-256 hex digest')
58
+ }
59
+ return {
60
+ ...bucket,
61
+ sessionId: assertNonEmptyString(row.sessionId, 'sessionId'),
62
+ createdAt: nullableString(row.createdAt, 'createdAt'),
63
+ lastModified: nullableString(row.lastModified, 'lastModified'),
64
+ messageCount: nullableInteger(row.messageCount, 'messageCount'),
65
+ pinnedAt: nullableString(row.pinnedAt, 'pinnedAt'),
66
+ archivedAt: nullableString(row.archivedAt, 'archivedAt'),
67
+ isPinned: row.pinnedAt ? 1 : 0,
68
+ isArchived: row.archivedAt ? 1 : 0,
69
+ stateVersion: nullableInteger(row.stateVersion, 'stateVersion'),
70
+ metadataJson,
71
+ metadataDigest: row.metadataDigest,
72
+ indexedAt: assertNonEmptyString(row.indexedAt, 'indexedAt'),
73
+ }
74
+ }
75
+
76
+ function mapRow(row) {
77
+ if (!row) return null
78
+ return {
79
+ scope: row.scope,
80
+ projectId: row.scope === 'project' ? row.project_id : null,
81
+ sessionId: row.session_id,
82
+ createdAt: row.created_at,
83
+ lastModified: row.last_modified,
84
+ messageCount: row.message_count,
85
+ pinnedAt: row.pinned_at,
86
+ archivedAt: row.archived_at,
87
+ isPinned: row.is_pinned === 1,
88
+ isArchived: row.is_archived === 1,
89
+ stateVersion: row.state_version,
90
+ metadata: JSON.parse(row.metadata_json),
91
+ metadataDigest: row.metadata_digest,
92
+ indexedAt: row.indexed_at,
93
+ }
94
+ }
95
+
96
+ function rowParameters(row) {
97
+ return [
98
+ row.scope, row.projectId, row.sessionId, row.createdAt, row.lastModified,
99
+ row.messageCount, row.pinnedAt, row.archivedAt, row.isPinned, row.isArchived,
100
+ row.stateVersion, row.metadataJson, row.metadataDigest, row.indexedAt,
101
+ ]
102
+ }
103
+
104
+ function normalizeQuery(options = {}) {
105
+ const scopeMode = options.scopeMode ?? 'all'
106
+ if (!['all', 'global', 'project', 'projects'].includes(scopeMode)) throw new TypeError('scopeMode is invalid')
107
+ const projectId = scopeMode === 'project' ? assertNonEmptyString(options.projectId, 'projectId') : null
108
+ const archive = options.archive ?? 'exclude'
109
+ if (!['exclude', 'only', 'include'].includes(archive)) throw new TypeError('archive is invalid')
110
+ const sort = options.sort ?? 'lastModified'
111
+ if (!['createdAt', 'lastModified', 'pinnedAt'].includes(sort)) throw new TypeError('sort is invalid')
112
+ const direction = options.direction ?? 'desc'
113
+ if (!['asc', 'desc'].includes(direction)) throw new TypeError('direction is invalid')
114
+ const limit = options.limit
115
+ const offset = options.offset ?? 0
116
+ if (!Number.isSafeInteger(limit) || limit <= 0) throw new TypeError('limit must be a positive safe integer')
117
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError('offset must be a non-negative safe integer')
118
+ return { scopeMode, projectId, archive, pinnedOnly: options.pinnedOnly === true, sort, direction, limit, offset }
119
+ }
120
+
121
+ const SORT_COLUMNS = Object.freeze({ createdAt: 'created_at', lastModified: 'last_modified', pinnedAt: 'pinned_at' })
122
+
123
+ function buildQuery(options, { page = true } = {}) {
124
+ const query = normalizeQuery(options)
125
+ const where = ['(message_count IS NULL OR message_count <> 0)']
126
+ const parameters = []
127
+ if (query.scopeMode === 'global') where.push("scope = 'global' AND project_id = ''")
128
+ else if (query.scopeMode === 'project') {
129
+ where.push("scope = 'project' AND project_id = ?")
130
+ parameters.push(query.projectId)
131
+ } else if (query.scopeMode === 'projects') where.push("scope = 'project'")
132
+ if (query.archive === 'exclude') where.push('is_archived = 0')
133
+ else if (query.archive === 'only') where.push('is_archived = 1')
134
+ if (query.pinnedOnly) where.push('is_pinned = 1')
135
+
136
+ const column = SORT_COLUMNS[query.sort]
137
+ const order = query.sort === 'pinnedAt'
138
+ ? `${column} ${query.direction.toUpperCase()}`
139
+ : `is_pinned DESC, pinned_at DESC, ${column} ${query.direction.toUpperCase()}`
140
+ const whereSql = `WHERE ${where.join(' AND ')}`
141
+ return {
142
+ query,
143
+ parameters,
144
+ whereSql,
145
+ order,
146
+ rowsSql: `SELECT ${SELECT_COLUMNS} FROM session_index ${whereSql} ORDER BY ${order}${page ? ' LIMIT ? OFFSET ?' : ''}`,
147
+ countSql: `SELECT COUNT(*) AS count FROM session_index ${whereSql}`,
148
+ }
149
+ }
150
+
151
+ function analyzeInto(database, options) {
152
+ const built = buildQuery(options, { page: false })
153
+ const aggregate = built.query.scopeMode === 'all' || built.query.scopeMode === 'projects'
154
+ const duplicateSessionIdCount = aggregate
155
+ ? Number(database.prepare(`SELECT COUNT(*) AS count FROM (SELECT session_id FROM session_index ${built.whereSql} GROUP BY session_id HAVING COUNT(*) > 1)`).get(...built.parameters).count)
156
+ : 0
157
+ const column = SORT_COLUMNS[built.query.sort]
158
+ const tieColumns = built.query.sort === 'pinnedAt' ? column : `is_pinned, pinned_at, ${column}`
159
+ const fullSortKeyTieCount = Number(database.prepare(`SELECT COUNT(*) AS count FROM (SELECT ${tieColumns} FROM session_index ${built.whereSql} GROUP BY ${tieColumns} HAVING COUNT(*) > 1)`).get(...built.parameters).count)
160
+ return { duplicateSessionIdCount, fullSortKeyTieCount }
161
+ }
162
+
163
+ export function createSessionIndexRepository(storageHandle) {
164
+ const storage = assertStorage(storageHandle ?? getSqliteStorage())
165
+ const upsertSql = `
166
+ INSERT INTO session_index (${SELECT_COLUMNS}) VALUES (${new Array(14).fill('?').join(', ')})
167
+ ON CONFLICT(scope, project_id, session_id) DO UPDATE SET
168
+ created_at = excluded.created_at,
169
+ last_modified = excluded.last_modified,
170
+ message_count = excluded.message_count,
171
+ pinned_at = excluded.pinned_at,
172
+ archived_at = excluded.archived_at,
173
+ is_pinned = excluded.is_pinned,
174
+ is_archived = excluded.is_archived,
175
+ state_version = excluded.state_version,
176
+ metadata_json = excluded.metadata_json,
177
+ metadata_digest = excluded.metadata_digest,
178
+ indexed_at = excluded.indexed_at
179
+ `
180
+
181
+ function upsertInto(database, row) {
182
+ const normalized = normalizeRow(row)
183
+ database.prepare(upsertSql).run(...rowParameters(normalized))
184
+ return normalized
185
+ }
186
+
187
+ function get(scope, projectId, sessionId) {
188
+ const bucket = normalizeBucket(scope, projectId)
189
+ assertNonEmptyString(sessionId, 'sessionId')
190
+ return mapRow(storage.prepare(`SELECT ${SELECT_COLUMNS} FROM session_index WHERE scope = ? AND project_id = ? AND session_id = ?`)
191
+ .get(bucket.scope, bucket.projectId, sessionId))
192
+ }
193
+
194
+ function upsert(row) {
195
+ const normalized = upsertInto(storage, row)
196
+ return get(normalized.scope, normalized.projectId, normalized.sessionId)
197
+ }
198
+
199
+ function deleteRow(scope, projectId, sessionId) {
200
+ const bucket = normalizeBucket(scope, projectId)
201
+ assertNonEmptyString(sessionId, 'sessionId')
202
+ return storage.prepare('DELETE FROM session_index WHERE scope = ? AND project_id = ? AND session_id = ?')
203
+ .run(bucket.scope, bucket.projectId, sessionId).changes > 0
204
+ }
205
+
206
+ function applyChanges({ upserts = [], deletes = [] } = {}) {
207
+ if (!Array.isArray(upserts) || !Array.isArray(deletes)) throw new TypeError('upserts and deletes must be arrays')
208
+ const normalizedUpserts = upserts.map(normalizeRow)
209
+ const normalizedDeletes = deletes.map((entry) => ({
210
+ ...normalizeBucket(entry?.scope, entry?.projectId),
211
+ sessionId: assertNonEmptyString(entry?.sessionId, 'sessionId'),
212
+ }))
213
+ return storage.transaction((database) => {
214
+ const deleteStatement = database.prepare('DELETE FROM session_index WHERE scope = ? AND project_id = ? AND session_id = ?')
215
+ for (const entry of normalizedDeletes) deleteStatement.run(entry.scope, entry.projectId, entry.sessionId)
216
+ for (const row of normalizedUpserts) database.prepare(upsertSql).run(...rowParameters(row))
217
+ return { upserted: normalizedUpserts.length, deleted: normalizedDeletes.length }
218
+ })
219
+ }
220
+
221
+ function replaceBucket(scope, projectId, rows) {
222
+ const bucket = normalizeBucket(scope, projectId)
223
+ if (!Array.isArray(rows)) throw new TypeError('rows must be an array')
224
+ const normalizedRows = rows.map((row) => normalizeRow({ ...row, ...bucket }))
225
+ return storage.transaction((database) => {
226
+ database.prepare('DELETE FROM session_index WHERE scope = ? AND project_id = ?').run(bucket.scope, bucket.projectId)
227
+ for (const row of normalizedRows) database.prepare(upsertSql).run(...rowParameters(row))
228
+ return normalizedRows.length
229
+ })
230
+ }
231
+
232
+ function replaceAll(rows) {
233
+ if (!Array.isArray(rows)) throw new TypeError('rows must be an array')
234
+ const normalizedRows = rows.map(normalizeRow)
235
+ const keys = new Set()
236
+ for (const row of normalizedRows) {
237
+ const key = `${row.scope}\0${row.projectId}\0${row.sessionId}`
238
+ if (keys.has(key)) throw new TypeError('Duplicate session index composite key')
239
+ keys.add(key)
240
+ }
241
+ return storage.transaction((database) => {
242
+ database.exec('DELETE FROM session_index')
243
+ for (const row of normalizedRows) database.prepare(upsertSql).run(...rowParameters(row))
244
+ return normalizedRows.length
245
+ })
246
+ }
247
+
248
+ function count() {
249
+ return Number(storage.prepare('SELECT COUNT(*) AS count FROM session_index').get().count)
250
+ }
251
+
252
+ function listVerification() {
253
+ return storage.prepare(`SELECT scope, project_id, session_id, metadata_digest FROM session_index ORDER BY scope, project_id, session_id`)
254
+ .all()
255
+ .map((row) => ({
256
+ scope: row.scope,
257
+ projectId: row.scope === 'project' ? row.project_id : null,
258
+ sessionId: row.session_id,
259
+ metadataDigest: row.metadata_digest,
260
+ }))
261
+ }
262
+
263
+ function listIntegrity() {
264
+ return storage.prepare(`SELECT ${SELECT_COLUMNS} FROM session_index ORDER BY scope, project_id, session_id`).all().map(mapRow)
265
+ }
266
+
267
+ function listAll() {
268
+ return storage.prepare(`SELECT ${SELECT_COLUMNS} FROM session_index ORDER BY scope, project_id, session_id`).all().map(mapRow)
269
+ }
270
+
271
+ function listPage(options) {
272
+ const built = buildQuery(options)
273
+ return storage.transaction((database) => {
274
+ const total = Number(database.prepare(built.countSql).get(...built.parameters).count)
275
+ const rows = database.prepare(built.rowsSql).all(...built.parameters, built.query.limit, built.query.offset).map(mapRow)
276
+ return { values: rows.map((row) => row.metadata), rows, total }
277
+ }, { mode: 'deferred' })
278
+ }
279
+
280
+ function readMetadataEntries() {
281
+ return storage.prepare('SELECT metadata_json FROM session_index').all().map((row) => JSON.parse(row.metadata_json))
282
+ }
283
+
284
+ function analyzeQuery(options) {
285
+ return analyzeInto(storage, options)
286
+ }
287
+
288
+ function explainQueryPlan(options) {
289
+ const built = buildQuery(options)
290
+ return storage.prepare(`EXPLAIN QUERY PLAN ${built.rowsSql}`).all(...built.parameters, built.query.limit, built.query.offset)
291
+ }
292
+
293
+ return Object.freeze({
294
+ get, upsert, delete: deleteRow, applyChanges, replaceBucket, replaceAll,
295
+ count, listVerification, listIntegrity, listAll, listPage, readMetadataEntries, analyzeQuery, explainQueryPlan,
296
+ })
297
+ }