@proletariat/cli 0.3.82 → 0.3.83
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/dist/lib/database/drizzle-schema.d.ts +301 -0
- package/dist/lib/database/drizzle-schema.js +22 -0
- package/dist/lib/database/drizzle-schema.js.map +1 -1
- package/dist/lib/database/index.d.ts +3 -0
- package/dist/lib/database/index.js +556 -345
- package/dist/lib/database/index.js.map +1 -1
- package/dist/lib/telemetry/analytics.d.ts +5 -5
- package/dist/lib/telemetry/analytics.js +87 -21
- package/dist/lib/telemetry/analytics.js.map +1 -1
- package/oclif.manifest.json +534 -534
- package/package.json +2 -1
|
@@ -1,33 +1,81 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
+
import { eq, and, or, isNull, sql, asc, desc, like } from 'drizzle-orm';
|
|
2
3
|
import * as fs from 'node:fs';
|
|
3
4
|
import * as path from 'node:path';
|
|
4
5
|
import { getThemePersistentDir, isEphemeralAgentName } from '../themes.js';
|
|
5
6
|
import { throwIfNativeBindingError } from './native-validation.js';
|
|
6
7
|
import { runDrizzleMigrations } from './migrator.js';
|
|
7
8
|
import { ALL_MIGRATIONS } from './migrations/index.js';
|
|
9
|
+
import { createDrizzleConnection } from './drizzle.js';
|
|
10
|
+
import { workspace as workspaceTable, repositories as repositoriesTable, agents as agentsTable, agentThemes as agentThemesTable, agentThemeNames as agentThemeNamesTable, agentWorktrees as agentWorktreesTable, workspaceSettings as workspaceSettingsTable, mediaItems as mediaItemsTable, } from './drizzle-schema.js';
|
|
8
11
|
// Re-export CREATE_TABLES_SQL from its canonical location
|
|
9
12
|
export { CREATE_TABLES_SQL } from './workspace-schema.js';
|
|
10
|
-
//
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Internal helpers
|
|
15
|
+
// =============================================================================
|
|
11
16
|
/**
|
|
12
|
-
*
|
|
17
|
+
* Open the workspace database, wrap it with Drizzle, run a function,
|
|
18
|
+
* and close the connection. Handles the open/close lifecycle.
|
|
19
|
+
*/
|
|
20
|
+
function withDrizzle(workspacePath, fn) {
|
|
21
|
+
const sqliteDb = openWorkspaceDatabase(workspacePath);
|
|
22
|
+
const ddb = createDrizzleConnection(sqliteDb);
|
|
23
|
+
try {
|
|
24
|
+
return fn(ddb, sqliteDb);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
sqliteDb.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Map a Drizzle agent row to the Agent interface.
|
|
32
|
+
* Handles default values for backwards compatibility with old databases.
|
|
33
|
+
*/
|
|
34
|
+
function toAgent(row) {
|
|
35
|
+
return {
|
|
36
|
+
name: row.name,
|
|
37
|
+
type: (row.type || 'persistent'),
|
|
38
|
+
status: (row.status || 'active'),
|
|
39
|
+
base_name: row.baseName,
|
|
40
|
+
theme_id: row.themeId,
|
|
41
|
+
worktree_path: row.worktreePath,
|
|
42
|
+
mount_mode: (row.mountMode || 'worktree'),
|
|
43
|
+
created_at: row.createdAt,
|
|
44
|
+
cleaned_at: row.cleanedAt,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Map a Drizzle theme row to the AgentTheme interface.
|
|
49
|
+
*/
|
|
50
|
+
function toAgentTheme(row) {
|
|
51
|
+
return {
|
|
52
|
+
id: row.id,
|
|
53
|
+
name: row.name,
|
|
54
|
+
display_name: row.displayName,
|
|
55
|
+
description: row.description,
|
|
56
|
+
builtin: Boolean(row.builtin),
|
|
57
|
+
created_at: row.createdAt,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Ensure ephemeral agents are correctly typed based on their worktree path or naming pattern.
|
|
62
|
+
* Uses raw SQL because it relies on SQLite-specific GLOB operator and sqlite_master introspection.
|
|
13
63
|
*/
|
|
14
64
|
function ensureEphemeralAgentTypes(db) {
|
|
15
|
-
// Check if agents table exists
|
|
65
|
+
// Check if agents table exists (sqlite_master introspection — no Drizzle equivalent)
|
|
16
66
|
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='agents'").get();
|
|
17
67
|
if (!tableExists) {
|
|
18
68
|
return;
|
|
19
69
|
}
|
|
20
70
|
// Agents in temp directory should be ephemeral
|
|
21
71
|
db.exec("UPDATE agents SET type = 'ephemeral' WHERE worktree_path LIKE 'agents/temp/%' AND type != 'ephemeral'");
|
|
22
|
-
// Detect ephemeral agents by naming pattern
|
|
23
|
-
// Staff agents are single names like 'lecun', 'musk', 'gates'
|
|
72
|
+
// Detect ephemeral agents by naming pattern using SQLite GLOB (no Drizzle equivalent)
|
|
24
73
|
db.exec(`
|
|
25
74
|
UPDATE agents SET type = 'ephemeral'
|
|
26
75
|
WHERE type != 'ephemeral'
|
|
27
76
|
AND name GLOB '*-*-[0-9]*'
|
|
28
77
|
`);
|
|
29
78
|
// Also detect numberless ephemeral names (e.g., bold-bezos) using isEphemeralAgentName()
|
|
30
|
-
// This catches agents that match the adjective-name pattern but don't have a number suffix
|
|
31
79
|
const potentialEphemeral = db.prepare(`
|
|
32
80
|
SELECT name FROM agents
|
|
33
81
|
WHERE type != 'ephemeral'
|
|
@@ -73,7 +121,7 @@ export function openWorkspaceDatabase(workspacePath) {
|
|
|
73
121
|
db.pragma('busy_timeout = 5000'); // Wait up to 5 seconds if database is locked
|
|
74
122
|
// Run Drizzle migrations (creates tracking table, applies pending migrations)
|
|
75
123
|
runDrizzleMigrations(db, ALL_MIGRATIONS);
|
|
76
|
-
// Ensure ephemeral agents are correctly typed
|
|
124
|
+
// Ensure ephemeral agents are correctly typed (raw SQL — uses SQLite GLOB)
|
|
77
125
|
ensureEphemeralAgentTypes(db);
|
|
78
126
|
return db;
|
|
79
127
|
}
|
|
@@ -107,11 +155,15 @@ export function createWorkspaceDatabase(workspacePath, type, workspaceName, hasP
|
|
|
107
155
|
db.pragma('foreign_keys = ON');
|
|
108
156
|
// Run all migrations (baseline creates core workspace + PMO tables)
|
|
109
157
|
runDrizzleMigrations(db, ALL_MIGRATIONS);
|
|
110
|
-
// Insert workspace data
|
|
111
|
-
db
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
158
|
+
// Insert workspace data using Drizzle
|
|
159
|
+
const ddb = createDrizzleConnection(db);
|
|
160
|
+
ddb.insert(workspaceTable).values({
|
|
161
|
+
id: 1,
|
|
162
|
+
type,
|
|
163
|
+
workspaceName,
|
|
164
|
+
hasPmo: hasPMO,
|
|
165
|
+
createdAt: new Date().toISOString(),
|
|
166
|
+
}).run();
|
|
115
167
|
return db;
|
|
116
168
|
}
|
|
117
169
|
/**
|
|
@@ -119,10 +171,19 @@ export function createWorkspaceDatabase(workspacePath, type, workspaceName, hasP
|
|
|
119
171
|
*/
|
|
120
172
|
export function getWorkspaceConfig(workspacePath) {
|
|
121
173
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
174
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
175
|
+
const row = ddb.select().from(workspaceTable).limit(1).get();
|
|
176
|
+
if (!row)
|
|
177
|
+
return null;
|
|
178
|
+
return {
|
|
179
|
+
id: row.id ?? 1,
|
|
180
|
+
type: row.type,
|
|
181
|
+
workspace_name: row.workspaceName,
|
|
182
|
+
has_pmo: Boolean(row.hasPmo),
|
|
183
|
+
active_theme_id: row.activeThemeId,
|
|
184
|
+
created_at: row.createdAt,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
126
187
|
}
|
|
127
188
|
catch {
|
|
128
189
|
return null;
|
|
@@ -139,12 +200,12 @@ export function getActiveTheme(workspacePath) {
|
|
|
139
200
|
return getTheme(workspacePath, config.active_theme_id);
|
|
140
201
|
}
|
|
141
202
|
// Auto-detect from existing agents
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
203
|
+
const agentList = getWorkspaceAgents(workspacePath);
|
|
204
|
+
if (agentList.length === 0) {
|
|
144
205
|
return null;
|
|
145
206
|
}
|
|
146
207
|
// Check if any agent has a theme_id set
|
|
147
|
-
const themedAgent =
|
|
208
|
+
const themedAgent = agentList.find(a => a.theme_id);
|
|
148
209
|
if (themedAgent?.theme_id) {
|
|
149
210
|
const theme = getTheme(workspacePath, themedAgent.theme_id);
|
|
150
211
|
if (theme) {
|
|
@@ -159,7 +220,7 @@ export function getActiveTheme(workspacePath) {
|
|
|
159
220
|
const themeNames = getThemeNames(workspacePath, theme.id);
|
|
160
221
|
const themeNameSet = new Set(themeNames.map(n => n.name.toLowerCase()));
|
|
161
222
|
// If any existing agent matches this theme's names
|
|
162
|
-
const matchingAgent =
|
|
223
|
+
const matchingAgent = agentList.find(a => themeNameSet.has(a.name.toLowerCase()));
|
|
163
224
|
if (matchingAgent) {
|
|
164
225
|
// Auto-set it for future use
|
|
165
226
|
setActiveTheme(workspacePath, theme.id);
|
|
@@ -172,82 +233,125 @@ export function getActiveTheme(workspacePath) {
|
|
|
172
233
|
* Set the active theme for a workspace
|
|
173
234
|
*/
|
|
174
235
|
export function setActiveTheme(workspacePath, themeId) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
236
|
+
withDrizzle(workspacePath, (ddb) => {
|
|
237
|
+
if (themeId) {
|
|
238
|
+
// Validate theme exists
|
|
239
|
+
const theme = ddb.select({ id: agentThemesTable.id })
|
|
240
|
+
.from(agentThemesTable)
|
|
241
|
+
.where(eq(agentThemesTable.id, themeId))
|
|
242
|
+
.get();
|
|
243
|
+
if (!theme) {
|
|
244
|
+
throw new Error(`Theme "${themeId}" not found`);
|
|
245
|
+
}
|
|
182
246
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
247
|
+
ddb.update(workspaceTable)
|
|
248
|
+
.set({ activeThemeId: themeId })
|
|
249
|
+
.where(eq(workspaceTable.id, 1))
|
|
250
|
+
.run();
|
|
251
|
+
});
|
|
186
252
|
}
|
|
187
253
|
/**
|
|
188
254
|
* Add repositories to database
|
|
189
255
|
*/
|
|
190
256
|
export function addRepositoriesToDatabase(workspacePath, repos) {
|
|
191
|
-
|
|
192
|
-
const insertRepo = db.prepare(`
|
|
193
|
-
INSERT OR REPLACE INTO repositories (name, path, type, source_url, action, added_at)
|
|
194
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
195
|
-
`);
|
|
196
|
-
const transaction = db.transaction(() => {
|
|
257
|
+
withDrizzle(workspacePath, (ddb) => {
|
|
197
258
|
for (const repo of repos) {
|
|
198
|
-
|
|
259
|
+
ddb.insert(repositoriesTable)
|
|
260
|
+
.values({
|
|
261
|
+
name: repo.name,
|
|
262
|
+
path: repo.path,
|
|
263
|
+
type: 'main',
|
|
264
|
+
sourceUrl: repo.source_url || null,
|
|
265
|
+
action: repo.action || null,
|
|
266
|
+
addedAt: new Date().toISOString(),
|
|
267
|
+
})
|
|
268
|
+
.onConflictDoUpdate({
|
|
269
|
+
target: repositoriesTable.name,
|
|
270
|
+
set: {
|
|
271
|
+
path: repo.path,
|
|
272
|
+
type: 'main',
|
|
273
|
+
sourceUrl: repo.source_url || null,
|
|
274
|
+
action: repo.action || null,
|
|
275
|
+
addedAt: new Date().toISOString(),
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
.run();
|
|
199
279
|
}
|
|
200
280
|
});
|
|
201
|
-
transaction();
|
|
202
|
-
db.close();
|
|
203
281
|
}
|
|
204
282
|
/**
|
|
205
283
|
* Add agents to database (case-insensitive uniqueness)
|
|
206
284
|
*/
|
|
207
285
|
export function addAgentsToDatabase(workspacePath, agentNames, themeId, mountMode = 'worktree') {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
:
|
|
245
|
-
|
|
286
|
+
withDrizzle(workspacePath, (ddb, sqliteDb) => {
|
|
287
|
+
// Get workspace config to determine paths
|
|
288
|
+
const wsRow = ddb.select().from(workspaceTable).get();
|
|
289
|
+
if (!wsRow)
|
|
290
|
+
throw new Error('No workspace config found');
|
|
291
|
+
// Get all repos for this workspace
|
|
292
|
+
const repos = ddb.select({ name: repositoriesTable.name }).from(repositoriesTable).all();
|
|
293
|
+
// Determine the effective theme ID (provided or active theme)
|
|
294
|
+
const effectiveThemeId = themeId || wsRow.activeThemeId || undefined;
|
|
295
|
+
const persistentDir = getThemePersistentDir(effectiveThemeId);
|
|
296
|
+
const transaction = sqliteDb.transaction(() => {
|
|
297
|
+
for (const agentName of agentNames) {
|
|
298
|
+
// Check for existing agents (case-insensitive) via Drizzle sql
|
|
299
|
+
const existing = ddb.select({ name: agentsTable.name })
|
|
300
|
+
.from(agentsTable)
|
|
301
|
+
.where(sql `LOWER(${agentsTable.name}) = LOWER(${agentName})`)
|
|
302
|
+
.get();
|
|
303
|
+
if (existing) {
|
|
304
|
+
continue; // Agent already exists with same name (different case)
|
|
305
|
+
}
|
|
306
|
+
const now = new Date().toISOString();
|
|
307
|
+
// Determine worktree path for the agent
|
|
308
|
+
const agentWorktreePath = wsRow.type === 'hq'
|
|
309
|
+
? `agents/${persistentDir}/${agentName}`
|
|
310
|
+
: agentName;
|
|
311
|
+
// Add agent (persistent type for manually added agents)
|
|
312
|
+
ddb.insert(agentsTable).values({
|
|
313
|
+
name: agentName,
|
|
314
|
+
type: 'persistent',
|
|
315
|
+
baseName: null,
|
|
316
|
+
themeId: effectiveThemeId || null,
|
|
317
|
+
worktreePath: agentWorktreePath,
|
|
318
|
+
mountMode,
|
|
319
|
+
createdAt: now,
|
|
320
|
+
}).onConflictDoUpdate({
|
|
321
|
+
target: agentsTable.name,
|
|
322
|
+
set: {
|
|
323
|
+
type: 'persistent',
|
|
324
|
+
baseName: null,
|
|
325
|
+
themeId: effectiveThemeId || null,
|
|
326
|
+
worktreePath: agentWorktreePath,
|
|
327
|
+
mountMode,
|
|
328
|
+
createdAt: now,
|
|
329
|
+
},
|
|
330
|
+
}).run();
|
|
331
|
+
// Add worktrees for all repos
|
|
332
|
+
for (const repo of repos) {
|
|
333
|
+
const worktreePath = wsRow.type === 'hq'
|
|
334
|
+
? `agents/${persistentDir}/${agentName}/${repo.name}`
|
|
335
|
+
: `${agentName}/${repo.name}`;
|
|
336
|
+
ddb.insert(agentWorktreesTable).values({
|
|
337
|
+
agentName,
|
|
338
|
+
repoName: repo.name,
|
|
339
|
+
worktreePath,
|
|
340
|
+
branch: `agent-${agentName}`,
|
|
341
|
+
createdAt: now,
|
|
342
|
+
}).onConflictDoUpdate({
|
|
343
|
+
target: [agentWorktreesTable.agentName, agentWorktreesTable.repoName],
|
|
344
|
+
set: {
|
|
345
|
+
worktreePath,
|
|
346
|
+
branch: `agent-${agentName}`,
|
|
347
|
+
createdAt: now,
|
|
348
|
+
},
|
|
349
|
+
}).run();
|
|
350
|
+
}
|
|
246
351
|
}
|
|
247
|
-
}
|
|
352
|
+
});
|
|
353
|
+
transaction();
|
|
248
354
|
});
|
|
249
|
-
transaction();
|
|
250
|
-
db.close();
|
|
251
355
|
}
|
|
252
356
|
/**
|
|
253
357
|
* Add an ephemeral agent to the database.
|
|
@@ -269,26 +373,27 @@ export function addEphemeralAgentToDatabase(workspacePath, agentName, baseName,
|
|
|
269
373
|
* simply gets null and can retry with a different name.
|
|
270
374
|
*/
|
|
271
375
|
export function tryAddEphemeralAgentToDatabase(workspacePath, agentName, baseName, themeId, mountMode = 'worktree') {
|
|
272
|
-
const
|
|
376
|
+
const sqliteDb = openWorkspaceDatabase(workspacePath);
|
|
377
|
+
const ddb = createDrizzleConnection(sqliteDb);
|
|
273
378
|
try {
|
|
274
379
|
const now = new Date().toISOString();
|
|
275
380
|
const worktreePath = `agents/temp/${agentName}`;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
381
|
+
ddb.insert(agentsTable).values({
|
|
382
|
+
name: agentName,
|
|
383
|
+
type: 'ephemeral',
|
|
384
|
+
status: 'active',
|
|
385
|
+
baseName,
|
|
386
|
+
themeId: themeId || null,
|
|
387
|
+
worktreePath,
|
|
388
|
+
mountMode,
|
|
389
|
+
createdAt: now,
|
|
390
|
+
}).run();
|
|
391
|
+
const agent = ddb.select().from(agentsTable)
|
|
392
|
+
.where(eq(agentsTable.name, agentName))
|
|
393
|
+
.get();
|
|
394
|
+
if (!agent)
|
|
395
|
+
return null;
|
|
396
|
+
return toAgent(agent);
|
|
292
397
|
}
|
|
293
398
|
catch (err) {
|
|
294
399
|
const sqliteErr = err;
|
|
@@ -298,48 +403,50 @@ export function tryAddEphemeralAgentToDatabase(workspacePath, agentName, baseNam
|
|
|
298
403
|
throw err;
|
|
299
404
|
}
|
|
300
405
|
finally {
|
|
301
|
-
|
|
406
|
+
sqliteDb.close();
|
|
302
407
|
}
|
|
303
408
|
}
|
|
304
409
|
/**
|
|
305
410
|
* Get all ephemeral agent names from the database
|
|
306
411
|
*/
|
|
307
412
|
export function getEphemeralAgentNames(workspacePath) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
413
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
414
|
+
const rows = ddb.select({ name: agentsTable.name })
|
|
415
|
+
.from(agentsTable)
|
|
416
|
+
.where(eq(agentsTable.type, 'ephemeral'))
|
|
417
|
+
.all();
|
|
418
|
+
return new Set(rows.map(a => a.name.toLowerCase()));
|
|
419
|
+
});
|
|
312
420
|
}
|
|
313
421
|
/**
|
|
314
422
|
* Remove an ephemeral agent from the database
|
|
315
423
|
*/
|
|
316
424
|
export function removeEphemeralAgent(workspacePath, agentName) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
425
|
+
withDrizzle(workspacePath, (ddb) => {
|
|
426
|
+
ddb.delete(agentsTable)
|
|
427
|
+
.where(and(eq(agentsTable.name, agentName), eq(agentsTable.type, 'ephemeral')))
|
|
428
|
+
.run();
|
|
429
|
+
});
|
|
320
430
|
}
|
|
321
431
|
/**
|
|
322
432
|
* Get all agents in workspace
|
|
323
433
|
*/
|
|
324
434
|
export function getWorkspaceAgents(workspacePath, includeCleanedUp = false) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
created_at: row.created_at,
|
|
341
|
-
cleaned_at: row.cleaned_at,
|
|
342
|
-
}));
|
|
435
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
436
|
+
let rows;
|
|
437
|
+
if (includeCleanedUp) {
|
|
438
|
+
rows = ddb.select().from(agentsTable)
|
|
439
|
+
.orderBy(asc(agentsTable.createdAt))
|
|
440
|
+
.all();
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
rows = ddb.select().from(agentsTable)
|
|
444
|
+
.where(or(eq(agentsTable.status, 'active'), isNull(agentsTable.status)))
|
|
445
|
+
.orderBy(asc(agentsTable.createdAt))
|
|
446
|
+
.all();
|
|
447
|
+
}
|
|
448
|
+
return rows.map(toAgent);
|
|
449
|
+
});
|
|
343
450
|
}
|
|
344
451
|
/**
|
|
345
452
|
* Get an agent by directory path.
|
|
@@ -356,38 +463,31 @@ export function getAgentByPath(workspacePath, absolutePath) {
|
|
|
356
463
|
}
|
|
357
464
|
// Get relative path from workspace root
|
|
358
465
|
const relativePath = path.relative(normalizedWorkspace, normalizedPath);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
type: (row.type || 'persistent'),
|
|
370
|
-
status: (row.status || 'active'),
|
|
371
|
-
base_name: row.base_name,
|
|
372
|
-
theme_id: row.theme_id,
|
|
373
|
-
worktree_path: row.worktree_path,
|
|
374
|
-
mount_mode: (row.mount_mode || 'worktree'),
|
|
375
|
-
created_at: row.created_at,
|
|
376
|
-
cleaned_at: row.cleaned_at,
|
|
377
|
-
};
|
|
466
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
467
|
+
const rows = ddb.select().from(agentsTable)
|
|
468
|
+
.where(or(eq(agentsTable.status, 'active'), isNull(agentsTable.status)))
|
|
469
|
+
.all();
|
|
470
|
+
// Find agent whose worktree_path matches or contains the relative path
|
|
471
|
+
for (const row of rows) {
|
|
472
|
+
if (row.worktreePath) {
|
|
473
|
+
if (relativePath === row.worktreePath || relativePath.startsWith(row.worktreePath + '/')) {
|
|
474
|
+
return toAgent(row);
|
|
475
|
+
}
|
|
378
476
|
}
|
|
379
477
|
}
|
|
380
|
-
|
|
381
|
-
|
|
478
|
+
return null;
|
|
479
|
+
});
|
|
382
480
|
}
|
|
383
481
|
/**
|
|
384
482
|
* Mark an agent as cleaned up (keeps the record for history)
|
|
385
483
|
*/
|
|
386
484
|
export function markAgentCleaned(workspacePath, agentName) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
485
|
+
withDrizzle(workspacePath, (ddb) => {
|
|
486
|
+
ddb.update(agentsTable)
|
|
487
|
+
.set({ status: 'cleaned', cleanedAt: new Date().toISOString() })
|
|
488
|
+
.where(eq(agentsTable.name, agentName))
|
|
489
|
+
.run();
|
|
490
|
+
});
|
|
391
491
|
}
|
|
392
492
|
/**
|
|
393
493
|
* Sync agents in database with what exists on disk.
|
|
@@ -395,9 +495,9 @@ export function markAgentCleaned(workspacePath, agentName) {
|
|
|
395
495
|
* Returns list of agents that were cleaned up.
|
|
396
496
|
*/
|
|
397
497
|
export function syncAgentsWithDisk(workspacePath) {
|
|
398
|
-
const
|
|
498
|
+
const agentList = getWorkspaceAgents(workspacePath, false); // Only active agents
|
|
399
499
|
const cleanedAgents = [];
|
|
400
|
-
for (const agent of
|
|
500
|
+
for (const agent of agentList) {
|
|
401
501
|
// Determine expected directory path
|
|
402
502
|
let agentDir;
|
|
403
503
|
if (agent.worktree_path) {
|
|
@@ -431,9 +531,8 @@ export function discoverAgentsOnDisk(workspacePath) {
|
|
|
431
531
|
const activeNames = new Set(activeAgents.map(a => a.name.toLowerCase()));
|
|
432
532
|
// Get ALL agents including cleaned (for reactivation)
|
|
433
533
|
const allAgents = getWorkspaceAgents(workspacePath, true);
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
try {
|
|
534
|
+
const cleanedAgentsMap = new Map(allAgents.filter(a => a.status === 'cleaned').map(a => [a.name.toLowerCase(), a]));
|
|
535
|
+
withDrizzle(workspacePath, (ddb) => {
|
|
437
536
|
// Scan staff directory
|
|
438
537
|
const staffDir = path.join(workspacePath, 'agents', 'staff');
|
|
439
538
|
if (fs.existsSync(staffDir)) {
|
|
@@ -444,21 +543,24 @@ export function discoverAgentsOnDisk(workspacePath) {
|
|
|
444
543
|
if (!activeNames.has(nameLower)) {
|
|
445
544
|
const worktreePath = `agents/staff/${entry.name}`;
|
|
446
545
|
const now = new Date().toISOString();
|
|
447
|
-
|
|
448
|
-
const cleanedAgent = cleanedAgents.get(nameLower);
|
|
546
|
+
const cleanedAgent = cleanedAgentsMap.get(nameLower);
|
|
449
547
|
if (cleanedAgent) {
|
|
450
548
|
// Reactivate the cleaned agent
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
549
|
+
ddb.update(agentsTable)
|
|
550
|
+
.set({ status: 'active', cleanedAt: null, worktreePath })
|
|
551
|
+
.where(sql `LOWER(${agentsTable.name}) = LOWER(${entry.name})`)
|
|
552
|
+
.run();
|
|
455
553
|
}
|
|
456
554
|
else {
|
|
457
|
-
// Register new agent
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
555
|
+
// Register new agent
|
|
556
|
+
ddb.insert(agentsTable).values({
|
|
557
|
+
name: entry.name,
|
|
558
|
+
type: 'persistent',
|
|
559
|
+
status: 'active',
|
|
560
|
+
worktreePath,
|
|
561
|
+
mountMode: 'worktree',
|
|
562
|
+
createdAt: now,
|
|
563
|
+
}).run();
|
|
462
564
|
}
|
|
463
565
|
result.discovered.push({ name: entry.name, type: 'persistent', path: worktreePath });
|
|
464
566
|
activeNames.add(nameLower);
|
|
@@ -476,21 +578,24 @@ export function discoverAgentsOnDisk(workspacePath) {
|
|
|
476
578
|
if (!activeNames.has(nameLower)) {
|
|
477
579
|
const worktreePath = `agents/temp/${entry.name}`;
|
|
478
580
|
const now = new Date().toISOString();
|
|
479
|
-
|
|
480
|
-
const cleanedAgent = cleanedAgents.get(nameLower);
|
|
581
|
+
const cleanedAgent = cleanedAgentsMap.get(nameLower);
|
|
481
582
|
if (cleanedAgent) {
|
|
482
583
|
// Reactivate the cleaned agent
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
584
|
+
ddb.update(agentsTable)
|
|
585
|
+
.set({ status: 'active', cleanedAt: null, worktreePath })
|
|
586
|
+
.where(sql `LOWER(${agentsTable.name}) = LOWER(${entry.name})`)
|
|
587
|
+
.run();
|
|
487
588
|
}
|
|
488
589
|
else {
|
|
489
|
-
// Register new agent
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
590
|
+
// Register new agent
|
|
591
|
+
ddb.insert(agentsTable).values({
|
|
592
|
+
name: entry.name,
|
|
593
|
+
type: 'ephemeral',
|
|
594
|
+
status: 'active',
|
|
595
|
+
worktreePath,
|
|
596
|
+
mountMode: 'worktree',
|
|
597
|
+
createdAt: now,
|
|
598
|
+
}).run();
|
|
494
599
|
}
|
|
495
600
|
result.discovered.push({ name: entry.name, type: 'ephemeral', path: worktreePath });
|
|
496
601
|
activeNames.add(nameLower);
|
|
@@ -498,79 +603,123 @@ export function discoverAgentsOnDisk(workspacePath) {
|
|
|
498
603
|
}
|
|
499
604
|
}
|
|
500
605
|
}
|
|
501
|
-
}
|
|
502
|
-
finally {
|
|
503
|
-
db.close();
|
|
504
|
-
}
|
|
606
|
+
});
|
|
505
607
|
return result;
|
|
506
608
|
}
|
|
507
609
|
/**
|
|
508
610
|
* Get all repositories in workspace
|
|
509
611
|
*/
|
|
510
612
|
export function getWorkspaceRepositories(workspacePath) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
613
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
614
|
+
const rows = ddb.select().from(repositoriesTable)
|
|
615
|
+
.orderBy(asc(repositoriesTable.addedAt))
|
|
616
|
+
.all();
|
|
617
|
+
return rows.map(row => ({
|
|
618
|
+
name: row.name,
|
|
619
|
+
path: row.path,
|
|
620
|
+
type: (row.type || 'main'),
|
|
621
|
+
source_url: row.sourceUrl ?? undefined,
|
|
622
|
+
action: (row.action ?? undefined),
|
|
623
|
+
added_at: row.addedAt,
|
|
624
|
+
}));
|
|
625
|
+
});
|
|
515
626
|
}
|
|
516
627
|
/**
|
|
517
628
|
* Get worktrees for a specific agent
|
|
518
629
|
*/
|
|
519
630
|
export function getAgentWorktrees(workspacePath, agentName) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
631
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
632
|
+
const rows = ddb.select().from(agentWorktreesTable)
|
|
633
|
+
.where(eq(agentWorktreesTable.agentName, agentName))
|
|
634
|
+
.all();
|
|
635
|
+
return rows.map(row => ({
|
|
636
|
+
agent_name: row.agentName,
|
|
637
|
+
repo_name: row.repoName,
|
|
638
|
+
worktree_path: row.worktreePath,
|
|
639
|
+
branch: row.branch,
|
|
640
|
+
created_at: row.createdAt,
|
|
641
|
+
last_commit_hash: row.lastCommitHash ?? undefined,
|
|
642
|
+
commits_ahead: row.commitsAhead,
|
|
643
|
+
is_clean: Boolean(row.isClean),
|
|
644
|
+
last_checked: row.lastChecked ?? undefined,
|
|
645
|
+
}));
|
|
646
|
+
});
|
|
524
647
|
}
|
|
525
648
|
/**
|
|
526
649
|
* Find agent worktrees matching a branch pattern (case-insensitive LIKE).
|
|
527
650
|
*/
|
|
528
651
|
export function findWorktreesByBranch(workspacePath, branchPattern) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
652
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
653
|
+
const rows = ddb.select().from(agentWorktreesTable)
|
|
654
|
+
.where(like(sql `LOWER(${agentWorktreesTable.branch})`, branchPattern))
|
|
655
|
+
.all();
|
|
656
|
+
return rows.map(row => ({
|
|
657
|
+
agent_name: row.agentName,
|
|
658
|
+
repo_name: row.repoName,
|
|
659
|
+
worktree_path: row.worktreePath,
|
|
660
|
+
branch: row.branch,
|
|
661
|
+
created_at: row.createdAt,
|
|
662
|
+
last_commit_hash: row.lastCommitHash ?? undefined,
|
|
663
|
+
commits_ahead: row.commitsAhead,
|
|
664
|
+
is_clean: Boolean(row.isClean),
|
|
665
|
+
last_checked: row.lastChecked ?? undefined,
|
|
666
|
+
}));
|
|
667
|
+
});
|
|
533
668
|
}
|
|
534
669
|
/**
|
|
535
670
|
* Get agent worktrees for a specific repository.
|
|
536
671
|
*/
|
|
537
672
|
export function getWorktreesForRepo(workspacePath, repoName) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
673
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
674
|
+
const rows = ddb.select({
|
|
675
|
+
agent_name: agentWorktreesTable.agentName,
|
|
676
|
+
is_clean: sql `${agentWorktreesTable.isClean}`,
|
|
677
|
+
commits_ahead: agentWorktreesTable.commitsAhead,
|
|
678
|
+
branch: agentWorktreesTable.branch,
|
|
679
|
+
}).from(agentWorktreesTable)
|
|
680
|
+
.where(eq(agentWorktreesTable.repoName, repoName))
|
|
681
|
+
.all();
|
|
682
|
+
return rows;
|
|
683
|
+
});
|
|
542
684
|
}
|
|
543
685
|
/**
|
|
544
686
|
* Upsert a workspace setting (key-value pair).
|
|
545
687
|
*/
|
|
546
688
|
export function upsertWorkspaceSetting(db, key, value) {
|
|
547
|
-
db
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
689
|
+
const ddb = createDrizzleConnection(db);
|
|
690
|
+
ddb.insert(workspaceSettingsTable)
|
|
691
|
+
.values({ key, value })
|
|
692
|
+
.onConflictDoUpdate({
|
|
693
|
+
target: workspaceSettingsTable.key,
|
|
694
|
+
set: { value },
|
|
695
|
+
})
|
|
696
|
+
.run();
|
|
552
697
|
}
|
|
553
698
|
/**
|
|
554
699
|
* Remove agents from database
|
|
555
700
|
*/
|
|
556
701
|
export function removeAgentsFromDatabase(workspacePath, agentNames) {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
702
|
+
withDrizzle(workspacePath, (ddb, sqliteDb) => {
|
|
703
|
+
// Note: agent_worktrees will be deleted automatically due to CASCADE
|
|
704
|
+
const transaction = sqliteDb.transaction(() => {
|
|
705
|
+
for (const agentName of agentNames) {
|
|
706
|
+
ddb.delete(agentsTable)
|
|
707
|
+
.where(eq(agentsTable.name, agentName))
|
|
708
|
+
.run();
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
transaction();
|
|
564
712
|
});
|
|
565
|
-
transaction();
|
|
566
|
-
db.close();
|
|
567
713
|
}
|
|
568
714
|
// =============================================================================
|
|
569
715
|
// PMO Bootstrapping Operations
|
|
716
|
+
// Raw SQL is required here because these operate before migrations run
|
|
717
|
+
// or perform DDL operations that Drizzle doesn't support.
|
|
570
718
|
// =============================================================================
|
|
571
719
|
/**
|
|
572
720
|
* Check if PMO tables exist and get basic stats.
|
|
573
721
|
* Used by pmo init to detect existing PMO before storage layer is available.
|
|
722
|
+
* Raw SQL: uses sqlite_master introspection (pre-migration bootstrap).
|
|
574
723
|
*/
|
|
575
724
|
export function checkPMOExists(dbPath) {
|
|
576
725
|
let db;
|
|
@@ -601,6 +750,7 @@ export function checkPMOExists(dbPath) {
|
|
|
601
750
|
/**
|
|
602
751
|
* Get a PMO setting from the pmo_settings table.
|
|
603
752
|
* Used for bootstrapping queries before storage layer is available.
|
|
753
|
+
* Raw SQL: pre-migration bootstrap query.
|
|
604
754
|
*/
|
|
605
755
|
export function getPMOSetting(dbPath, key) {
|
|
606
756
|
let db;
|
|
@@ -625,6 +775,7 @@ export function getPMOSetting(dbPath, key) {
|
|
|
625
775
|
/**
|
|
626
776
|
* Drop PMO tables from the database.
|
|
627
777
|
* Used during PMO reinitialization.
|
|
778
|
+
* Raw SQL: DDL operations (DROP TABLE) are not supported by Drizzle.
|
|
628
779
|
*/
|
|
629
780
|
export function dropPMOTables(dbPath, tables) {
|
|
630
781
|
let db;
|
|
@@ -656,61 +807,79 @@ export function dropPMOTables(dbPath, tables) {
|
|
|
656
807
|
* Get all themes
|
|
657
808
|
*/
|
|
658
809
|
export function getThemes(workspacePath) {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
810
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
811
|
+
const rows = ddb.select().from(agentThemesTable)
|
|
812
|
+
.orderBy(desc(agentThemesTable.builtin), asc(agentThemesTable.name))
|
|
813
|
+
.all();
|
|
814
|
+
return rows.map(toAgentTheme);
|
|
815
|
+
});
|
|
663
816
|
}
|
|
664
817
|
/**
|
|
665
818
|
* Get a theme by ID
|
|
666
819
|
*/
|
|
667
820
|
export function getTheme(workspacePath, themeId) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
821
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
822
|
+
const row = ddb.select().from(agentThemesTable)
|
|
823
|
+
.where(eq(agentThemesTable.id, themeId))
|
|
824
|
+
.get();
|
|
825
|
+
return row ? toAgentTheme(row) : null;
|
|
826
|
+
});
|
|
672
827
|
}
|
|
673
828
|
/**
|
|
674
829
|
* Create a new theme
|
|
675
830
|
*/
|
|
676
831
|
export function createTheme(workspacePath, theme) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
832
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
833
|
+
const now = new Date().toISOString();
|
|
834
|
+
ddb.insert(agentThemesTable).values({
|
|
835
|
+
id: theme.id,
|
|
836
|
+
name: theme.name,
|
|
837
|
+
displayName: theme.displayName,
|
|
838
|
+
description: theme.description || null,
|
|
839
|
+
builtin: theme.builtin || false,
|
|
840
|
+
createdAt: now,
|
|
841
|
+
}).run();
|
|
842
|
+
const created = ddb.select().from(agentThemesTable)
|
|
843
|
+
.where(eq(agentThemesTable.id, theme.id))
|
|
844
|
+
.get();
|
|
845
|
+
return toAgentTheme(created);
|
|
846
|
+
});
|
|
686
847
|
}
|
|
687
848
|
/**
|
|
688
849
|
* Delete a theme (cannot delete builtin themes)
|
|
689
850
|
*/
|
|
690
851
|
export function deleteTheme(workspacePath, themeId) {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
852
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
853
|
+
const theme = ddb.select({ builtin: agentThemesTable.builtin })
|
|
854
|
+
.from(agentThemesTable)
|
|
855
|
+
.where(eq(agentThemesTable.id, themeId))
|
|
856
|
+
.get();
|
|
857
|
+
if (!theme) {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
if (theme.builtin) {
|
|
861
|
+
throw new Error('Cannot delete built-in themes');
|
|
862
|
+
}
|
|
863
|
+
ddb.delete(agentThemesTable)
|
|
864
|
+
.where(eq(agentThemesTable.id, themeId))
|
|
865
|
+
.run();
|
|
866
|
+
return true;
|
|
867
|
+
});
|
|
705
868
|
}
|
|
706
869
|
/**
|
|
707
870
|
* Get names for a theme
|
|
708
871
|
*/
|
|
709
872
|
export function getThemeNames(workspacePath, themeId) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
873
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
874
|
+
const rows = ddb.select().from(agentThemeNamesTable)
|
|
875
|
+
.where(eq(agentThemeNamesTable.themeId, themeId))
|
|
876
|
+
.orderBy(asc(agentThemeNamesTable.name))
|
|
877
|
+
.all();
|
|
878
|
+
return rows.map(row => ({
|
|
879
|
+
theme_id: row.themeId,
|
|
880
|
+
name: row.name,
|
|
881
|
+
}));
|
|
882
|
+
});
|
|
714
883
|
}
|
|
715
884
|
/**
|
|
716
885
|
* Get available names for a theme.
|
|
@@ -719,141 +888,183 @@ export function getThemeNames(workspacePath, themeId) {
|
|
|
719
888
|
* 2. The agent exists but its worktree directory is missing (manually deleted)
|
|
720
889
|
*/
|
|
721
890
|
export function getAvailableThemeNames(workspacePath, themeId) {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
891
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
892
|
+
// Get all theme names
|
|
893
|
+
const names = ddb.select({ name: agentThemeNamesTable.name })
|
|
894
|
+
.from(agentThemeNamesTable)
|
|
895
|
+
.where(eq(agentThemeNamesTable.themeId, themeId))
|
|
896
|
+
.orderBy(asc(agentThemeNamesTable.name))
|
|
897
|
+
.all();
|
|
898
|
+
// Get existing staff agents with their worktree paths (persistent type only)
|
|
899
|
+
const existingAgents = ddb.select({
|
|
900
|
+
name: sql `LOWER(${agentsTable.name})`,
|
|
901
|
+
worktreePath: agentsTable.worktreePath,
|
|
902
|
+
})
|
|
903
|
+
.from(agentsTable)
|
|
904
|
+
.where(and(eq(agentsTable.type, 'persistent'), or(eq(agentsTable.status, 'active'), isNull(agentsTable.status))))
|
|
905
|
+
.all();
|
|
906
|
+
// Build a set of names that are truly in use (agent exists AND worktree exists)
|
|
907
|
+
const inUseNames = new Set();
|
|
908
|
+
for (const agent of existingAgents) {
|
|
909
|
+
if (agent.worktreePath) {
|
|
910
|
+
const fullPath = path.join(workspacePath, agent.worktreePath);
|
|
911
|
+
if (fs.existsSync(fullPath)) {
|
|
912
|
+
inUseNames.add(agent.name);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
// No worktree path means we can't verify - treat as in use to be safe
|
|
738
917
|
inUseNames.add(agent.name);
|
|
739
918
|
}
|
|
740
919
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
746
|
-
// Filter out names that are truly in use
|
|
747
|
-
return names
|
|
748
|
-
.map(n => n.name)
|
|
749
|
-
.filter(name => !inUseNames.has(name.toLowerCase()));
|
|
920
|
+
// Filter out names that are truly in use
|
|
921
|
+
return names
|
|
922
|
+
.map(n => n.name)
|
|
923
|
+
.filter(name => !inUseNames.has(name.toLowerCase()));
|
|
924
|
+
});
|
|
750
925
|
}
|
|
751
926
|
/**
|
|
752
927
|
* Add names to a theme (case-insensitive uniqueness)
|
|
753
928
|
*/
|
|
754
929
|
export function addThemeNames(workspacePath, themeId, names) {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
930
|
+
withDrizzle(workspacePath, (ddb, sqliteDb) => {
|
|
931
|
+
const transaction = sqliteDb.transaction(() => {
|
|
932
|
+
for (const name of names) {
|
|
933
|
+
// Check for existing name (case-insensitive)
|
|
934
|
+
const existing = ddb.select({ name: agentThemeNamesTable.name })
|
|
935
|
+
.from(agentThemeNamesTable)
|
|
936
|
+
.where(and(eq(agentThemeNamesTable.themeId, themeId), sql `LOWER(${agentThemeNamesTable.name}) = LOWER(${name})`))
|
|
937
|
+
.get();
|
|
938
|
+
if (existing) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
ddb.insert(agentThemeNamesTable).values({
|
|
942
|
+
themeId,
|
|
943
|
+
name,
|
|
944
|
+
}).run();
|
|
768
945
|
}
|
|
769
|
-
|
|
770
|
-
|
|
946
|
+
});
|
|
947
|
+
transaction();
|
|
771
948
|
});
|
|
772
|
-
transaction();
|
|
773
|
-
db.close();
|
|
774
949
|
}
|
|
775
950
|
/**
|
|
776
951
|
* Add a media item to the database
|
|
777
952
|
*/
|
|
778
953
|
export function addMediaItemToDatabase(workspacePath, item) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
954
|
+
withDrizzle(workspacePath, (ddb) => {
|
|
955
|
+
const now = new Date().toISOString();
|
|
956
|
+
ddb.insert(mediaItemsTable)
|
|
957
|
+
.values({
|
|
958
|
+
name: item.name,
|
|
959
|
+
path: item.path,
|
|
960
|
+
sourcePath: item.source_path || null,
|
|
961
|
+
mediaType: item.media_type,
|
|
962
|
+
frameInterval: item.frame_interval || 30,
|
|
963
|
+
addedAt: now,
|
|
964
|
+
})
|
|
965
|
+
.onConflictDoUpdate({
|
|
966
|
+
target: mediaItemsTable.name,
|
|
967
|
+
set: {
|
|
968
|
+
path: item.path,
|
|
969
|
+
sourcePath: item.source_path || null,
|
|
970
|
+
mediaType: item.media_type,
|
|
971
|
+
frameInterval: item.frame_interval || 30,
|
|
972
|
+
addedAt: now,
|
|
973
|
+
},
|
|
974
|
+
})
|
|
975
|
+
.run();
|
|
976
|
+
});
|
|
785
977
|
}
|
|
786
978
|
/**
|
|
787
979
|
* Update media item after preprocessing
|
|
788
980
|
*/
|
|
789
981
|
export function updateMediaItemStatus(workspacePath, name, updates) {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
values.push(new Date().toISOString());
|
|
816
|
-
}
|
|
817
|
-
values.push(name);
|
|
818
|
-
db.prepare(`UPDATE media_items SET ${sets.join(', ')} WHERE name = ?`).run(...values);
|
|
819
|
-
db.close();
|
|
982
|
+
withDrizzle(workspacePath, (ddb) => {
|
|
983
|
+
const setValues = { status: updates.status };
|
|
984
|
+
if (updates.duration_seconds !== undefined) {
|
|
985
|
+
setValues.durationSeconds = updates.duration_seconds;
|
|
986
|
+
}
|
|
987
|
+
if (updates.resolution !== undefined) {
|
|
988
|
+
setValues.resolution = updates.resolution;
|
|
989
|
+
}
|
|
990
|
+
if (updates.frame_count !== undefined) {
|
|
991
|
+
setValues.frameCount = updates.frame_count;
|
|
992
|
+
}
|
|
993
|
+
if (updates.has_transcript !== undefined) {
|
|
994
|
+
setValues.hasTranscript = updates.has_transcript;
|
|
995
|
+
}
|
|
996
|
+
if (updates.error_message !== undefined) {
|
|
997
|
+
setValues.errorMessage = updates.error_message;
|
|
998
|
+
}
|
|
999
|
+
if (updates.status === 'ready' || updates.status === 'error') {
|
|
1000
|
+
setValues.processedAt = new Date().toISOString();
|
|
1001
|
+
}
|
|
1002
|
+
ddb.update(mediaItemsTable)
|
|
1003
|
+
.set(setValues)
|
|
1004
|
+
.where(eq(mediaItemsTable.name, name))
|
|
1005
|
+
.run();
|
|
1006
|
+
});
|
|
820
1007
|
}
|
|
821
1008
|
/**
|
|
822
1009
|
* Get all media items in workspace
|
|
823
1010
|
*/
|
|
824
1011
|
export function getWorkspaceMediaItems(workspacePath) {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1012
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
1013
|
+
const rows = ddb.select().from(mediaItemsTable)
|
|
1014
|
+
.orderBy(asc(mediaItemsTable.addedAt))
|
|
1015
|
+
.all();
|
|
1016
|
+
return rows.map(row => ({
|
|
1017
|
+
name: row.name,
|
|
1018
|
+
path: row.path,
|
|
1019
|
+
source_path: row.sourcePath,
|
|
1020
|
+
media_type: row.mediaType,
|
|
1021
|
+
duration_seconds: row.durationSeconds,
|
|
1022
|
+
resolution: row.resolution,
|
|
1023
|
+
frame_count: row.frameCount,
|
|
1024
|
+
has_transcript: Boolean(row.hasTranscript),
|
|
1025
|
+
frame_interval: row.frameInterval,
|
|
1026
|
+
status: row.status,
|
|
1027
|
+
error_message: row.errorMessage,
|
|
1028
|
+
added_at: row.addedAt,
|
|
1029
|
+
processed_at: row.processedAt,
|
|
1030
|
+
}));
|
|
1031
|
+
});
|
|
834
1032
|
}
|
|
835
1033
|
/**
|
|
836
1034
|
* Get a single media item by name
|
|
837
1035
|
*/
|
|
838
1036
|
export function getMediaItem(workspacePath, name) {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1037
|
+
return withDrizzle(workspacePath, (ddb) => {
|
|
1038
|
+
const row = ddb.select().from(mediaItemsTable)
|
|
1039
|
+
.where(eq(mediaItemsTable.name, name))
|
|
1040
|
+
.get();
|
|
1041
|
+
if (!row)
|
|
1042
|
+
return null;
|
|
1043
|
+
return {
|
|
1044
|
+
name: row.name,
|
|
1045
|
+
path: row.path,
|
|
1046
|
+
source_path: row.sourcePath,
|
|
1047
|
+
media_type: row.mediaType,
|
|
1048
|
+
duration_seconds: row.durationSeconds,
|
|
1049
|
+
resolution: row.resolution,
|
|
1050
|
+
frame_count: row.frameCount,
|
|
1051
|
+
has_transcript: Boolean(row.hasTranscript),
|
|
1052
|
+
frame_interval: row.frameInterval,
|
|
1053
|
+
status: row.status,
|
|
1054
|
+
error_message: row.errorMessage,
|
|
1055
|
+
added_at: row.addedAt,
|
|
1056
|
+
processed_at: row.processedAt,
|
|
1057
|
+
};
|
|
1058
|
+
});
|
|
850
1059
|
}
|
|
851
1060
|
/**
|
|
852
1061
|
* Remove a media item from the database
|
|
853
1062
|
*/
|
|
854
1063
|
export function removeMediaItemFromDatabase(workspacePath, name) {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1064
|
+
withDrizzle(workspacePath, (ddb) => {
|
|
1065
|
+
ddb.delete(mediaItemsTable)
|
|
1066
|
+
.where(eq(mediaItemsTable.name, name))
|
|
1067
|
+
.run();
|
|
1068
|
+
});
|
|
858
1069
|
}
|
|
859
1070
|
//# sourceMappingURL=index.js.map
|