@shawnstack/quickforge 1.7.9 → 1.7.11

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 (53) hide show
  1. package/dist/assets/{AgentProfilesPage-BSM5w3bn.js → AgentProfilesPage-Cvb1KTz5.js} +1 -1
  2. package/dist/assets/ChatPanelHost-C7zV-xE_.js +48 -0
  3. package/dist/assets/{CloudAccountSettingsPage-BJMrfgy9.js → CloudAccountSettingsPage-mAs_nbvw.js} +1 -1
  4. package/dist/assets/{PluginsPage-mJkTmVVV.js → PluginsPage-mLvwvTmL.js} +1 -1
  5. package/dist/assets/{ScheduledTasksPage-DJj_NLm9.js → ScheduledTasksPage-CuUW_9lq.js} +1 -1
  6. package/dist/assets/{SettingsWorkspacePage-7lQ0Wl63.js → SettingsWorkspacePage-0OINfEqK.js} +390 -390
  7. package/dist/assets/{ShareLinksSettingsPage-DjFq_Lnb.js → ShareLinksSettingsPage-DqeyJYoD.js} +1 -1
  8. package/dist/assets/{SharedConversationPage-BbuNpcpP.js → SharedConversationPage-C7CDEoX9.js} +1 -1
  9. package/dist/assets/TerminalDock-CZXcuMpq.js +2 -0
  10. package/dist/assets/{WorkspaceInspector-0i6x-diB.js → WorkspaceInspector-CNSBOEvP.js} +3 -3
  11. package/dist/assets/index-BgMvQW1b.js +66 -0
  12. package/dist/assets/index-CB1g2D0f.css +3 -0
  13. package/dist/assets/local-tools-Dopjg9xq.js +270 -0
  14. package/dist/assets/{mcp-servers-dialog-BG4b67ea.js → mcp-servers-dialog-B2lALtTE.js} +1 -1
  15. package/dist/assets/{skills-dialog-DIzZvQqW.js → skills-dialog-DYp35JM3.js} +1 -1
  16. package/dist/index.html +4 -4
  17. package/package.json +1 -1
  18. package/server/acp/server.mjs +3 -6
  19. package/server/agent-manager.mjs +125 -43
  20. package/server/auto-archive.mjs +55 -8
  21. package/server/auto-compaction.mjs +0 -2
  22. package/server/context-usage.mjs +9 -7
  23. package/server/index.mjs +172 -51
  24. package/server/lan-access-cutover.mjs +21 -4
  25. package/server/maintenance/downgrade-session-state-v1.mjs +50 -87
  26. package/server/maintenance/export-session-state-v1.mjs +5 -13
  27. package/server/routes/backup.mjs +8 -1
  28. package/server/routes/storage.mjs +85 -48
  29. package/server/scheduled-runs-cutover.mjs +15 -1
  30. package/server/session-index-service.mjs +57 -391
  31. package/server/session-persistence-lock.mjs +17 -6
  32. package/server/session-state-backup.mjs +60 -7
  33. package/server/session-state-import.mjs +157 -0
  34. package/server/session-state-maintenance.mjs +126 -0
  35. package/server/session-state-service.mjs +133 -194
  36. package/server/share-cutover.mjs +21 -4
  37. package/server/sqlite/database.mjs +142 -7
  38. package/server/sqlite/lan-access-repository.mjs +6 -4
  39. package/server/sqlite/migrations.mjs +109 -0
  40. package/server/sqlite/scheduled-task-runs-repository.mjs +6 -3
  41. package/server/sqlite/session-index-repository.mjs +59 -208
  42. package/server/sqlite/session-state-repository.mjs +359 -289
  43. package/server/sqlite/share-repository.mjs +56 -45
  44. package/server/startup-state.mjs +107 -0
  45. package/server/storage.mjs +240 -448
  46. package/server/utils/logger.mjs +15 -4
  47. package/server/utils/process-tree.mjs +14 -2
  48. package/dist/assets/ChatPanelHost-cd80S4EX.js +0 -48
  49. package/dist/assets/TerminalDock-CczcCJJE.js +0 -2
  50. package/dist/assets/index-D-J_8Smf.css +0 -3
  51. package/dist/assets/index-DFASc15l.js +0 -66
  52. package/dist/assets/local-tools-Dn1Y9aPe.js +0 -270
  53. package/server/session-state-cutover.mjs +0 -370
@@ -1,9 +1,23 @@
1
+ import { createHash } from 'node:crypto'
1
2
  import { getSqliteStorage } from './database.mjs'
2
3
 
4
+ // Storage v2 session index query layer. The dedicated `session_index`
5
+ // projection table is retired (renamed away by migration v11): the
6
+ // authoritative `sessions` table carries the promoted list columns
7
+ // (created_at, message_count, archived_at, pinned_at, meta_json) and this
8
+ // repository is now a read-only LIMIT/OFFSET query facade over it. There is
9
+ // nothing to sync, shadow or rebuild — the store and the index are the same
10
+ // rows.
11
+
12
+ // Sorting by lastModified stays metadata-driven: sessions.updated_at is the
13
+ // wall-clock write time, while the list UI (and the retired index) order by
14
+ // metadata.lastModified, which sessions may legitimately backdate.
15
+ const LAST_MODIFIED_EXPR = "json_extract(meta_json, '$.lastModified')"
16
+
3
17
  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
18
+ scope, project_id, session_id, created_at, updated_at,
19
+ ${LAST_MODIFIED_EXPR} AS last_modified, message_count,
20
+ pinned_at, archived_at, state_version, meta_json
7
21
  `
8
22
 
9
23
  function assertStorage(storage) {
@@ -18,89 +32,6 @@ function assertNonEmptyString(value, field) {
18
32
  return value
19
33
  }
20
34
 
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
35
  function normalizeQuery(options = {}) {
105
36
  const scopeMode = options.scopeMode ?? 'all'
106
37
  if (!['all', 'global', 'project', 'projects'].includes(scopeMode)) throw new TypeError('scopeMode is invalid')
@@ -118,33 +49,56 @@ function normalizeQuery(options = {}) {
118
49
  return { scopeMode, projectId, archive, pinnedOnly: options.pinnedOnly === true, sort, direction, limit, offset }
119
50
  }
120
51
 
121
- const SORT_COLUMNS = Object.freeze({ createdAt: 'created_at', lastModified: 'last_modified', pinnedAt: 'pinned_at' })
52
+ const SORT_COLUMNS = Object.freeze({ createdAt: 'created_at', lastModified: LAST_MODIFIED_EXPR, pinnedAt: 'pinned_at' })
122
53
 
123
54
  function buildQuery(options, { page = true } = {}) {
124
55
  const query = normalizeQuery(options)
125
- const where = ['(message_count IS NULL OR message_count <> 0)']
56
+ const where = ['message_count <> 0']
126
57
  const parameters = []
127
58
  if (query.scopeMode === 'global') where.push("scope = 'global' AND project_id = ''")
128
59
  else if (query.scopeMode === 'project') {
129
60
  where.push("scope = 'project' AND project_id = ?")
130
61
  parameters.push(query.projectId)
131
62
  } 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')
63
+ if (query.archive === 'exclude') where.push('archived_at IS NULL')
64
+ else if (query.archive === 'only') where.push('archived_at IS NOT NULL')
65
+ if (query.pinnedOnly) where.push('pinned_at IS NOT NULL')
135
66
 
136
67
  const column = SORT_COLUMNS[query.sort]
68
+ // Pinned-first ordering (metadata pinnedAt promoted to the pinned_at
69
+ // column): DESC on `pinned_at IS NOT NULL` matches the retired index's
70
+ // is_pinned DESC, and DESC on pinned_at itself keeps NULLs last.
137
71
  const order = query.sort === 'pinnedAt'
138
72
  ? `${column} ${query.direction.toUpperCase()}`
139
- : `is_pinned DESC, pinned_at DESC, ${column} ${query.direction.toUpperCase()}`
73
+ : `(pinned_at IS NOT NULL) DESC, pinned_at DESC, ${column} ${query.direction.toUpperCase()}`
140
74
  const whereSql = `WHERE ${where.join(' AND ')}`
141
75
  return {
142
76
  query,
143
77
  parameters,
144
78
  whereSql,
145
79
  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}`,
80
+ rowsSql: `SELECT ${SELECT_COLUMNS} FROM sessions ${whereSql} ORDER BY ${order}${page ? ' LIMIT ? OFFSET ?' : ''}`,
81
+ countSql: `SELECT COUNT(*) AS count FROM sessions ${whereSql}`,
82
+ }
83
+ }
84
+
85
+ function mapRow(row) {
86
+ if (!row) return null
87
+ return {
88
+ scope: row.scope,
89
+ projectId: row.scope === 'project' ? row.project_id : null,
90
+ sessionId: row.session_id,
91
+ createdAt: row.created_at,
92
+ lastModified: row.last_modified ?? null,
93
+ messageCount: row.message_count,
94
+ pinnedAt: row.pinned_at ?? null,
95
+ archivedAt: row.archived_at ?? null,
96
+ isPinned: row.pinned_at != null,
97
+ isArchived: row.archived_at != null,
98
+ stateVersion: row.state_version,
99
+ metadata: JSON.parse(row.meta_json),
100
+ metadataDigest: createHash('sha256').update(row.meta_json).digest('hex'),
101
+ indexedAt: row.updated_at,
148
102
  }
149
103
  }
150
104
 
@@ -152,121 +106,16 @@ function analyzeInto(database, options) {
152
106
  const built = buildQuery(options, { page: false })
153
107
  const aggregate = built.query.scopeMode === 'all' || built.query.scopeMode === 'projects'
154
108
  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)
109
+ ? Number(database.prepare(`SELECT COUNT(*) AS count FROM (SELECT session_id FROM sessions ${built.whereSql} GROUP BY session_id HAVING COUNT(*) > 1)`).get(...built.parameters).count)
156
110
  : 0
157
111
  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)
112
+ const tieColumns = built.query.sort === 'pinnedAt' ? column : `(pinned_at IS NOT NULL), pinned_at, ${column}`
113
+ const fullSortKeyTieCount = Number(database.prepare(`SELECT COUNT(*) AS count FROM (SELECT ${tieColumns} FROM sessions ${built.whereSql} GROUP BY ${tieColumns} HAVING COUNT(*) > 1)`).get(...built.parameters).count)
160
114
  return { duplicateSessionIdCount, fullSortKeyTieCount }
161
115
  }
162
116
 
163
117
  export function createSessionIndexRepository(storageHandle) {
164
118
  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
119
 
271
120
  function listPage(options) {
272
121
  const built = buildQuery(options)
@@ -277,10 +126,6 @@ export function createSessionIndexRepository(storageHandle) {
277
126
  }, { mode: 'deferred' })
278
127
  }
279
128
 
280
- function readMetadataEntries() {
281
- return storage.prepare('SELECT metadata_json FROM session_index').all().map((row) => JSON.parse(row.metadata_json))
282
- }
283
-
284
129
  function analyzeQuery(options) {
285
130
  return analyzeInto(storage, options)
286
131
  }
@@ -290,8 +135,14 @@ export function createSessionIndexRepository(storageHandle) {
290
135
  return storage.prepare(`EXPLAIN QUERY PLAN ${built.rowsSql}`).all(...built.parameters, built.query.limit, built.query.offset)
291
136
  }
292
137
 
138
+ function count() {
139
+ return Number(storage.prepare('SELECT COUNT(*) AS count FROM sessions').get().count)
140
+ }
141
+
293
142
  return Object.freeze({
294
- get, upsert, delete: deleteRow, applyChanges, replaceBucket, replaceAll,
295
- count, listVerification, listIntegrity, listAll, listPage, readMetadataEntries, analyzeQuery, explainQueryPlan,
143
+ listPage,
144
+ analyzeQuery,
145
+ explainQueryPlan,
146
+ count,
296
147
  })
297
148
  }