@agenticmail/enterprise 0.2.1
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/ARCHITECTURE.md +183 -0
- package/agenticmail-enterprise.db +0 -0
- package/dashboards/README.md +120 -0
- package/dashboards/dotnet/Program.cs +261 -0
- package/dashboards/express/app.js +146 -0
- package/dashboards/go/main.go +513 -0
- package/dashboards/html/index.html +535 -0
- package/dashboards/java/AgenticMailDashboard.java +376 -0
- package/dashboards/php/index.php +414 -0
- package/dashboards/python/app.py +273 -0
- package/dashboards/ruby/app.rb +195 -0
- package/dist/chunk-77IDQJL3.js +7 -0
- package/dist/chunk-7RGCCHIT.js +115 -0
- package/dist/chunk-DXNKR3TG.js +1355 -0
- package/dist/chunk-IQWA44WT.js +970 -0
- package/dist/chunk-LCUZGIDH.js +965 -0
- package/dist/chunk-N2JVTNNJ.js +2553 -0
- package/dist/chunk-O462UJBH.js +363 -0
- package/dist/chunk-PNKVD2UK.js +26 -0
- package/dist/cli.js +218 -0
- package/dist/dashboard/index.html +558 -0
- package/dist/db-adapter-DEWEFNIV.js +7 -0
- package/dist/dynamodb-CCGL2E77.js +426 -0
- package/dist/engine/index.js +1261 -0
- package/dist/index.js +522 -0
- package/dist/mongodb-ODTXIVPV.js +319 -0
- package/dist/mysql-RM3S2FV5.js +521 -0
- package/dist/postgres-LN7A6MGQ.js +518 -0
- package/dist/routes-2JEPIIKC.js +441 -0
- package/dist/routes-74ZLKJKP.js +399 -0
- package/dist/server.js +7 -0
- package/dist/sqlite-3K5YOZ4K.js +439 -0
- package/dist/turso-LDWODSDI.js +442 -0
- package/package.json +49 -0
- package/src/admin/routes.ts +331 -0
- package/src/auth/routes.ts +130 -0
- package/src/cli.ts +260 -0
- package/src/dashboard/index.html +558 -0
- package/src/db/adapter.ts +230 -0
- package/src/db/dynamodb.ts +456 -0
- package/src/db/factory.ts +51 -0
- package/src/db/mongodb.ts +360 -0
- package/src/db/mysql.ts +472 -0
- package/src/db/postgres.ts +479 -0
- package/src/db/sql-schema.ts +123 -0
- package/src/db/sqlite.ts +391 -0
- package/src/db/turso.ts +411 -0
- package/src/deploy/fly.ts +368 -0
- package/src/deploy/managed.ts +213 -0
- package/src/engine/activity.ts +474 -0
- package/src/engine/agent-config.ts +429 -0
- package/src/engine/agenticmail-bridge.ts +296 -0
- package/src/engine/approvals.ts +278 -0
- package/src/engine/db-adapter.ts +682 -0
- package/src/engine/db-schema.ts +335 -0
- package/src/engine/deployer.ts +595 -0
- package/src/engine/index.ts +134 -0
- package/src/engine/knowledge.ts +486 -0
- package/src/engine/lifecycle.ts +635 -0
- package/src/engine/openclaw-hook.ts +371 -0
- package/src/engine/routes.ts +528 -0
- package/src/engine/skills.ts +473 -0
- package/src/engine/tenant.ts +345 -0
- package/src/engine/tool-catalog.ts +189 -0
- package/src/index.ts +64 -0
- package/src/lib/resilience.ts +326 -0
- package/src/middleware/index.ts +286 -0
- package/src/server.ts +310 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine Database Adapter
|
|
3
|
+
*
|
|
4
|
+
* Extends the base DatabaseAdapter with engine-specific persistence.
|
|
5
|
+
* Works with SQLite, Postgres, MySQL, Turso — anything that speaks SQL.
|
|
6
|
+
*
|
|
7
|
+
* MongoDB and DynamoDB would need their own implementations.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ManagedAgent, AgentState, StateTransition, AgentUsage, LifecycleEvent } from './lifecycle.js';
|
|
11
|
+
import type { AgentPermissionProfile } from './skills.js';
|
|
12
|
+
import type { Organization, OrgPlan } from './tenant.js';
|
|
13
|
+
import type { ApprovalRequest, ApprovalPolicy } from './approvals.js';
|
|
14
|
+
import type { KnowledgeBase, KBDocument, KBChunk } from './knowledge.js';
|
|
15
|
+
import type { ActivityEvent, ToolCallRecord, ConversationEntry } from './activity.js';
|
|
16
|
+
import type { AgentConfig } from './agent-config.js';
|
|
17
|
+
import {
|
|
18
|
+
ENGINE_TABLES,
|
|
19
|
+
MIGRATIONS,
|
|
20
|
+
MIGRATIONS_TABLE,
|
|
21
|
+
MIGRATIONS_TABLE_POSTGRES,
|
|
22
|
+
sqliteToPostgres,
|
|
23
|
+
sqliteToMySQL,
|
|
24
|
+
type Migration,
|
|
25
|
+
type DynamicTableDef,
|
|
26
|
+
} from './db-schema.js';
|
|
27
|
+
|
|
28
|
+
// ─── Types ──────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export interface EngineDB {
|
|
31
|
+
// Execute raw SQL (adapter-specific)
|
|
32
|
+
run(sql: string, params?: any[]): Promise<void>;
|
|
33
|
+
get<T = any>(sql: string, params?: any[]): Promise<T | undefined>;
|
|
34
|
+
all<T = any>(sql: string, params?: any[]): Promise<T[]>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Engine Database Layer ──────────────────────────────
|
|
38
|
+
|
|
39
|
+
export class EngineDatabase {
|
|
40
|
+
private db: EngineDB;
|
|
41
|
+
private dialect: 'sqlite' | 'postgres' | 'mysql' | 'mongodb' | 'dynamodb' | 'turso';
|
|
42
|
+
/** Raw driver handle for NoSQL migrations (MongoClient db, DynamoDB client, etc.) */
|
|
43
|
+
private rawDriver?: any;
|
|
44
|
+
|
|
45
|
+
constructor(db: EngineDB, dialect: 'sqlite' | 'postgres' | 'mysql' | 'mongodb' | 'dynamodb' | 'turso' = 'sqlite', rawDriver?: any) {
|
|
46
|
+
this.db = db;
|
|
47
|
+
this.dialect = dialect;
|
|
48
|
+
this.rawDriver = rawDriver;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Migration System ─────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run all pending migrations in order.
|
|
55
|
+
* Creates the migration tracking table first, then applies any unapplied migrations.
|
|
56
|
+
*/
|
|
57
|
+
async migrate(): Promise<{ applied: number; total: number }> {
|
|
58
|
+
// Create migration tracking table
|
|
59
|
+
const trackingDDL = this.dialect === 'postgres' ? MIGRATIONS_TABLE_POSTGRES : MIGRATIONS_TABLE;
|
|
60
|
+
for (const stmt of this.splitStatements(trackingDDL)) {
|
|
61
|
+
await this.db.run(stmt);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get already-applied versions
|
|
65
|
+
const applied = await this.db.all<{ version: number }>('SELECT version FROM engine_migrations ORDER BY version');
|
|
66
|
+
const appliedSet = new Set(applied.map(r => r.version));
|
|
67
|
+
|
|
68
|
+
let count = 0;
|
|
69
|
+
for (const migration of MIGRATIONS) {
|
|
70
|
+
if (appliedSet.has(migration.version)) continue;
|
|
71
|
+
|
|
72
|
+
// Pick the right SQL for the dialect
|
|
73
|
+
if ((this.dialect === 'mongodb' || this.dialect === 'dynamodb') && migration.nosql) {
|
|
74
|
+
await migration.nosql(this.rawDriver, this.dialect);
|
|
75
|
+
} else {
|
|
76
|
+
const sql = this.getSqlForDialect(migration);
|
|
77
|
+
if (sql) {
|
|
78
|
+
for (const stmt of this.splitStatements(sql)) {
|
|
79
|
+
await this.db.run(stmt);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Record migration
|
|
85
|
+
await this.db.run(
|
|
86
|
+
'INSERT INTO engine_migrations (version, name) VALUES (?, ?)',
|
|
87
|
+
[migration.version, migration.name]
|
|
88
|
+
);
|
|
89
|
+
count++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { applied: count, total: MIGRATIONS.length };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Register and create a dynamic table at runtime.
|
|
97
|
+
* Used by plugins, skills, or engine extensions that need their own storage.
|
|
98
|
+
* Table name is auto-prefixed with `ext_` to avoid collisions with core tables.
|
|
99
|
+
*/
|
|
100
|
+
async createDynamicTable(def: DynamicTableDef): Promise<void> {
|
|
101
|
+
const prefixedName = def.name.startsWith('ext_') ? def.name : `ext_${def.name}`;
|
|
102
|
+
|
|
103
|
+
if (this.dialect === 'mongodb' && def.mongoSetup) {
|
|
104
|
+
await def.mongoSetup(this.rawDriver);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (this.dialect === 'dynamodb' && def.dynamoSetup) {
|
|
108
|
+
await def.dynamoSetup(this.rawDriver);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// SQL-based: pick dialect-specific DDL or auto-convert
|
|
113
|
+
let ddl: string;
|
|
114
|
+
if (this.dialect === 'postgres' && def.postgres) {
|
|
115
|
+
ddl = def.postgres;
|
|
116
|
+
} else if (this.dialect === 'mysql' && def.mysql) {
|
|
117
|
+
ddl = def.mysql;
|
|
118
|
+
} else if (this.dialect === 'postgres') {
|
|
119
|
+
ddl = sqliteToPostgres(def.sql);
|
|
120
|
+
} else if (this.dialect === 'mysql') {
|
|
121
|
+
ddl = sqliteToMySQL(def.sql);
|
|
122
|
+
} else {
|
|
123
|
+
ddl = def.sql;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Replace the table name with prefixed version
|
|
127
|
+
ddl = ddl.replace(new RegExp(`\\b${def.name}\\b`, 'g'), prefixedName);
|
|
128
|
+
|
|
129
|
+
for (const stmt of this.splitStatements(ddl)) {
|
|
130
|
+
await this.db.run(stmt);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Create any additional indexes
|
|
134
|
+
if (def.indexes) {
|
|
135
|
+
for (const idx of def.indexes) {
|
|
136
|
+
const prefixedIdx = idx.replace(new RegExp(`\\b${def.name}\\b`, 'g'), prefixedName);
|
|
137
|
+
await this.db.run(prefixedIdx);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run arbitrary SQL — for custom queries on dynamic tables.
|
|
144
|
+
* Returns rows for SELECT, void for mutations.
|
|
145
|
+
*/
|
|
146
|
+
async query<T = any>(sql: string, params?: any[]): Promise<T[]> {
|
|
147
|
+
return this.db.all<T>(sql, params);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async execute(sql: string, params?: any[]): Promise<void> {
|
|
151
|
+
return this.db.run(sql, params);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* List all dynamic (ext_*) tables currently in the database.
|
|
156
|
+
*/
|
|
157
|
+
async listDynamicTables(): Promise<string[]> {
|
|
158
|
+
if (this.dialect === 'postgres') {
|
|
159
|
+
const rows = await this.db.all<{ tablename: string }>(
|
|
160
|
+
"SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE 'ext_%'"
|
|
161
|
+
);
|
|
162
|
+
return rows.map(r => r.tablename);
|
|
163
|
+
} else if (this.dialect === 'mysql') {
|
|
164
|
+
const rows = await this.db.all<{ table_name: string }>(
|
|
165
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE 'ext_%'"
|
|
166
|
+
);
|
|
167
|
+
return rows.map(r => r.table_name);
|
|
168
|
+
} else {
|
|
169
|
+
// SQLite / Turso
|
|
170
|
+
const rows = await this.db.all<{ name: string }>(
|
|
171
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'ext_%'"
|
|
172
|
+
);
|
|
173
|
+
return rows.map(r => r.name);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Helpers ────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
private splitStatements(sql: string): string[] {
|
|
180
|
+
return sql.split(';').map(s => s.trim()).filter(s => s.length > 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private getSqlForDialect(migration: Migration): string | undefined {
|
|
184
|
+
if (this.dialect === 'postgres' && migration.postgres) return migration.postgres;
|
|
185
|
+
if (this.dialect === 'mysql' && migration.mysql) return migration.mysql;
|
|
186
|
+
if (this.dialect === 'postgres' && migration.sql) return sqliteToPostgres(migration.sql);
|
|
187
|
+
if (this.dialect === 'mysql' && migration.sql) return sqliteToMySQL(migration.sql);
|
|
188
|
+
return migration.sql;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Managed Agents ─────────────────────────────────
|
|
192
|
+
|
|
193
|
+
async upsertManagedAgent(agent: ManagedAgent): Promise<void> {
|
|
194
|
+
await this.db.run(`
|
|
195
|
+
INSERT INTO managed_agents (id, org_id, name, display_name, state, config, health, usage, permission_profile_id, version, last_deployed_at, last_health_check_at, created_at, updated_at)
|
|
196
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
197
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
198
|
+
state = excluded.state,
|
|
199
|
+
config = excluded.config,
|
|
200
|
+
health = excluded.health,
|
|
201
|
+
usage = excluded.usage,
|
|
202
|
+
permission_profile_id = excluded.permission_profile_id,
|
|
203
|
+
version = excluded.version,
|
|
204
|
+
last_deployed_at = excluded.last_deployed_at,
|
|
205
|
+
last_health_check_at = excluded.last_health_check_at,
|
|
206
|
+
updated_at = excluded.updated_at
|
|
207
|
+
`, [
|
|
208
|
+
agent.id, agent.orgId, agent.config.name, agent.config.displayName,
|
|
209
|
+
agent.state, JSON.stringify(agent.config), JSON.stringify(agent.health),
|
|
210
|
+
JSON.stringify(agent.usage), agent.config.permissionProfileId,
|
|
211
|
+
agent.version, agent.lastDeployedAt || null, agent.lastHealthCheckAt || null,
|
|
212
|
+
agent.createdAt, agent.updatedAt,
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async getManagedAgent(id: string): Promise<ManagedAgent | null> {
|
|
217
|
+
const row = await this.db.get<any>('SELECT * FROM managed_agents WHERE id = ?', [id]);
|
|
218
|
+
return row ? this.rowToManagedAgent(row) : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async getManagedAgentsByOrg(orgId: string): Promise<ManagedAgent[]> {
|
|
222
|
+
const rows = await this.db.all<any>('SELECT * FROM managed_agents WHERE org_id = ? ORDER BY created_at DESC', [orgId]);
|
|
223
|
+
return rows.map(r => this.rowToManagedAgent(r));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async getManagedAgentsByState(state: AgentState): Promise<ManagedAgent[]> {
|
|
227
|
+
const rows = await this.db.all<any>('SELECT * FROM managed_agents WHERE state = ?', [state]);
|
|
228
|
+
return rows.map(r => this.rowToManagedAgent(r));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async deleteManagedAgent(id: string): Promise<void> {
|
|
232
|
+
await this.db.run('DELETE FROM managed_agents WHERE id = ?', [id]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async countManagedAgents(orgId: string, state?: AgentState): Promise<number> {
|
|
236
|
+
const sql = state
|
|
237
|
+
? 'SELECT COUNT(*) as count FROM managed_agents WHERE org_id = ? AND state = ?'
|
|
238
|
+
: 'SELECT COUNT(*) as count FROM managed_agents WHERE org_id = ?';
|
|
239
|
+
const row = await this.db.get<any>(sql, state ? [orgId, state] : [orgId]);
|
|
240
|
+
return row?.count || 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─── State History ──────────────────────────────────
|
|
244
|
+
|
|
245
|
+
async addStateTransition(agentId: string, transition: StateTransition): Promise<void> {
|
|
246
|
+
await this.db.run(`
|
|
247
|
+
INSERT INTO agent_state_history (id, agent_id, from_state, to_state, reason, triggered_by, error, created_at)
|
|
248
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
249
|
+
`, [
|
|
250
|
+
crypto.randomUUID(), agentId, transition.from, transition.to,
|
|
251
|
+
transition.reason, transition.triggeredBy, transition.error || null, transition.timestamp,
|
|
252
|
+
]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async getStateHistory(agentId: string, limit: number = 50): Promise<StateTransition[]> {
|
|
256
|
+
const rows = await this.db.all<any>(
|
|
257
|
+
'SELECT * FROM agent_state_history WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?',
|
|
258
|
+
[agentId, limit]
|
|
259
|
+
);
|
|
260
|
+
return rows.map(r => ({
|
|
261
|
+
from: r.from_state, to: r.to_state, reason: r.reason,
|
|
262
|
+
triggeredBy: r.triggered_by, timestamp: r.created_at, error: r.error,
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ─── Permission Profiles ────────────────────────────
|
|
267
|
+
|
|
268
|
+
async upsertPermissionProfile(orgId: string, profile: AgentPermissionProfile): Promise<void> {
|
|
269
|
+
await this.db.run(`
|
|
270
|
+
INSERT INTO permission_profiles (id, org_id, name, description, config, is_preset, created_at, updated_at)
|
|
271
|
+
VALUES (?, ?, ?, ?, ?, 0, ?, ?)
|
|
272
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
273
|
+
name = excluded.name, description = excluded.description,
|
|
274
|
+
config = excluded.config, updated_at = excluded.updated_at
|
|
275
|
+
`, [
|
|
276
|
+
profile.id, orgId, profile.name, profile.description || null,
|
|
277
|
+
JSON.stringify(profile), profile.createdAt, profile.updatedAt,
|
|
278
|
+
]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async getPermissionProfile(id: string): Promise<AgentPermissionProfile | null> {
|
|
282
|
+
const row = await this.db.get<any>('SELECT * FROM permission_profiles WHERE id = ?', [id]);
|
|
283
|
+
return row ? JSON.parse(row.config) : null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async getPermissionProfilesByOrg(orgId: string): Promise<AgentPermissionProfile[]> {
|
|
287
|
+
const rows = await this.db.all<any>('SELECT config FROM permission_profiles WHERE org_id = ? ORDER BY name', [orgId]);
|
|
288
|
+
return rows.map(r => JSON.parse(r.config));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async deletePermissionProfile(id: string): Promise<void> {
|
|
292
|
+
await this.db.run('DELETE FROM permission_profiles WHERE id = ?', [id]);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── Organizations ──────────────────────────────────
|
|
296
|
+
|
|
297
|
+
async upsertOrganization(org: Organization): Promise<void> {
|
|
298
|
+
await this.db.run(`
|
|
299
|
+
INSERT INTO organizations (id, name, slug, plan, limits, usage, settings, sso_config, allowed_domains, billing, created_at, updated_at)
|
|
300
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
301
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
302
|
+
name = excluded.name, slug = excluded.slug, plan = excluded.plan,
|
|
303
|
+
limits = excluded.limits, usage = excluded.usage, settings = excluded.settings,
|
|
304
|
+
sso_config = excluded.sso_config, allowed_domains = excluded.allowed_domains,
|
|
305
|
+
billing = excluded.billing, updated_at = excluded.updated_at
|
|
306
|
+
`, [
|
|
307
|
+
org.id, org.name, org.slug, org.plan,
|
|
308
|
+
JSON.stringify(org.limits), JSON.stringify(org.usage),
|
|
309
|
+
JSON.stringify(org.settings), org.ssoConfig ? JSON.stringify(org.ssoConfig) : null,
|
|
310
|
+
JSON.stringify(org.allowedDomains), org.billing ? JSON.stringify(org.billing) : null,
|
|
311
|
+
org.createdAt, org.updatedAt,
|
|
312
|
+
]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async getOrganization(id: string): Promise<Organization | null> {
|
|
316
|
+
const row = await this.db.get<any>('SELECT * FROM organizations WHERE id = ?', [id]);
|
|
317
|
+
return row ? this.rowToOrg(row) : null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async getOrganizationBySlug(slug: string): Promise<Organization | null> {
|
|
321
|
+
const row = await this.db.get<any>('SELECT * FROM organizations WHERE slug = ?', [slug]);
|
|
322
|
+
return row ? this.rowToOrg(row) : null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async listOrganizations(): Promise<Organization[]> {
|
|
326
|
+
const rows = await this.db.all<any>('SELECT * FROM organizations ORDER BY created_at DESC');
|
|
327
|
+
return rows.map(r => this.rowToOrg(r));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async deleteOrganization(id: string): Promise<void> {
|
|
331
|
+
await this.db.run('DELETE FROM organizations WHERE id = ?', [id]);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ─── Knowledge Bases ────────────────────────────────
|
|
335
|
+
|
|
336
|
+
async upsertKnowledgeBase(kb: KnowledgeBase): Promise<void> {
|
|
337
|
+
await this.db.run(`
|
|
338
|
+
INSERT INTO knowledge_bases (id, org_id, name, description, agent_ids, config, stats, created_at, updated_at)
|
|
339
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
340
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
341
|
+
name = excluded.name, description = excluded.description,
|
|
342
|
+
agent_ids = excluded.agent_ids, config = excluded.config,
|
|
343
|
+
stats = excluded.stats, updated_at = excluded.updated_at
|
|
344
|
+
`, [
|
|
345
|
+
kb.id, kb.orgId, kb.name, kb.description || null,
|
|
346
|
+
JSON.stringify(kb.agentIds), JSON.stringify(kb.config),
|
|
347
|
+
JSON.stringify(kb.stats), kb.createdAt, kb.updatedAt,
|
|
348
|
+
]);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async getKnowledgeBase(id: string): Promise<KnowledgeBase | null> {
|
|
352
|
+
const row = await this.db.get<any>('SELECT * FROM knowledge_bases WHERE id = ?', [id]);
|
|
353
|
+
if (!row) return null;
|
|
354
|
+
const kb: any = {
|
|
355
|
+
id: row.id, orgId: row.org_id, name: row.name, description: row.description,
|
|
356
|
+
agentIds: JSON.parse(row.agent_ids), config: JSON.parse(row.config),
|
|
357
|
+
stats: JSON.parse(row.stats), createdAt: row.created_at, updatedAt: row.updated_at,
|
|
358
|
+
documents: [],
|
|
359
|
+
};
|
|
360
|
+
// Load documents
|
|
361
|
+
kb.documents = await this.getKBDocuments(id);
|
|
362
|
+
return kb;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async getKnowledgeBasesByOrg(orgId: string): Promise<KnowledgeBase[]> {
|
|
366
|
+
const rows = await this.db.all<any>('SELECT * FROM knowledge_bases WHERE org_id = ? ORDER BY name', [orgId]);
|
|
367
|
+
return rows.map(r => ({
|
|
368
|
+
id: r.id, orgId: r.org_id, name: r.name, description: r.description,
|
|
369
|
+
agentIds: JSON.parse(r.agent_ids), config: JSON.parse(r.config),
|
|
370
|
+
stats: JSON.parse(r.stats), createdAt: r.created_at, updatedAt: r.updated_at,
|
|
371
|
+
documents: [], // Loaded on demand
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async deleteKnowledgeBase(id: string): Promise<void> {
|
|
376
|
+
await this.db.run('DELETE FROM knowledge_bases WHERE id = ?', [id]);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ─── KB Documents & Chunks ──────────────────────────
|
|
380
|
+
|
|
381
|
+
async insertKBDocument(doc: KBDocument): Promise<void> {
|
|
382
|
+
await this.db.run(`
|
|
383
|
+
INSERT INTO kb_documents (id, knowledge_base_id, name, source_type, source_url, mime_type, size, metadata, status, error, created_at, updated_at)
|
|
384
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
385
|
+
`, [
|
|
386
|
+
doc.id, doc.knowledgeBaseId, doc.name, doc.sourceType,
|
|
387
|
+
doc.sourceUrl || null, doc.mimeType, doc.size,
|
|
388
|
+
JSON.stringify(doc.metadata), doc.status, doc.error || null,
|
|
389
|
+
doc.createdAt, doc.updatedAt,
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
// Insert chunks
|
|
393
|
+
for (const chunk of doc.chunks) {
|
|
394
|
+
await this.db.run(`
|
|
395
|
+
INSERT INTO kb_chunks (id, document_id, content, token_count, position, embedding, metadata)
|
|
396
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
397
|
+
`, [
|
|
398
|
+
chunk.id, doc.id, chunk.content, chunk.tokenCount,
|
|
399
|
+
chunk.position, chunk.embedding ? Buffer.from(new Float32Array(chunk.embedding).buffer) : null,
|
|
400
|
+
JSON.stringify(chunk.metadata),
|
|
401
|
+
]);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async getKBDocuments(kbId: string): Promise<KBDocument[]> {
|
|
406
|
+
const docs = await this.db.all<any>('SELECT * FROM kb_documents WHERE knowledge_base_id = ?', [kbId]);
|
|
407
|
+
const result: KBDocument[] = [];
|
|
408
|
+
for (const d of docs) {
|
|
409
|
+
const chunks = await this.db.all<any>('SELECT * FROM kb_chunks WHERE document_id = ? ORDER BY position', [d.id]);
|
|
410
|
+
result.push({
|
|
411
|
+
id: d.id, knowledgeBaseId: d.knowledge_base_id, name: d.name,
|
|
412
|
+
sourceType: d.source_type, sourceUrl: d.source_url, mimeType: d.mime_type,
|
|
413
|
+
size: d.size, metadata: JSON.parse(d.metadata), status: d.status, error: d.error,
|
|
414
|
+
createdAt: d.created_at, updatedAt: d.updated_at,
|
|
415
|
+
chunks: chunks.map((c: any) => ({
|
|
416
|
+
id: c.id, documentId: c.document_id, content: c.content,
|
|
417
|
+
tokenCount: c.token_count, position: c.position,
|
|
418
|
+
embedding: c.embedding ? Array.from(new Float32Array(c.embedding)) : undefined,
|
|
419
|
+
metadata: JSON.parse(c.metadata),
|
|
420
|
+
})),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async deleteKBDocument(docId: string): Promise<void> {
|
|
427
|
+
await this.db.run('DELETE FROM kb_documents WHERE id = ?', [docId]);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ─── Tool Calls (Activity) ──────────────────────────
|
|
431
|
+
|
|
432
|
+
async insertToolCall(record: ToolCallRecord): Promise<void> {
|
|
433
|
+
await this.db.run(`
|
|
434
|
+
INSERT INTO tool_calls (id, agent_id, org_id, session_id, tool_id, tool_name, parameters, result, timing, cost, permission, created_at)
|
|
435
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
436
|
+
`, [
|
|
437
|
+
record.id, record.agentId, record.orgId, record.sessionId || null,
|
|
438
|
+
record.toolId, record.toolName, JSON.stringify(record.parameters),
|
|
439
|
+
record.result ? JSON.stringify(record.result) : null,
|
|
440
|
+
JSON.stringify(record.timing), record.cost ? JSON.stringify(record.cost) : null,
|
|
441
|
+
JSON.stringify(record.permission), record.timing.startedAt,
|
|
442
|
+
]);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async updateToolCallResult(id: string, result: ToolCallRecord['result'], timing: ToolCallRecord['timing'], cost?: ToolCallRecord['cost']): Promise<void> {
|
|
446
|
+
await this.db.run(
|
|
447
|
+
'UPDATE tool_calls SET result = ?, timing = ?, cost = ? WHERE id = ?',
|
|
448
|
+
[JSON.stringify(result), JSON.stringify(timing), cost ? JSON.stringify(cost) : null, id]
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async getToolCalls(opts: { agentId?: string; orgId?: string; toolId?: string; since?: string; limit?: number }): Promise<ToolCallRecord[]> {
|
|
453
|
+
const conditions: string[] = [];
|
|
454
|
+
const params: any[] = [];
|
|
455
|
+
if (opts.agentId) { conditions.push('agent_id = ?'); params.push(opts.agentId); }
|
|
456
|
+
if (opts.orgId) { conditions.push('org_id = ?'); params.push(opts.orgId); }
|
|
457
|
+
if (opts.toolId) { conditions.push('tool_id = ?'); params.push(opts.toolId); }
|
|
458
|
+
if (opts.since) { conditions.push('created_at >= ?'); params.push(opts.since); }
|
|
459
|
+
const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
|
|
460
|
+
params.push(opts.limit || 50);
|
|
461
|
+
const rows = await this.db.all<any>(`SELECT * FROM tool_calls ${where} ORDER BY created_at DESC LIMIT ?`, params);
|
|
462
|
+
return rows.map(r => ({
|
|
463
|
+
id: r.id, agentId: r.agent_id, orgId: r.org_id, sessionId: r.session_id,
|
|
464
|
+
toolId: r.tool_id, toolName: r.tool_name, parameters: JSON.parse(r.parameters || '{}'),
|
|
465
|
+
result: r.result ? JSON.parse(r.result) : undefined,
|
|
466
|
+
timing: JSON.parse(r.timing), cost: r.cost ? JSON.parse(r.cost) : undefined,
|
|
467
|
+
permission: JSON.parse(r.permission),
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ─── Activity Events ────────────────────────────────
|
|
472
|
+
|
|
473
|
+
async insertActivityEvent(event: ActivityEvent): Promise<void> {
|
|
474
|
+
await this.db.run(`
|
|
475
|
+
INSERT INTO activity_events (id, agent_id, org_id, session_id, type, data, created_at)
|
|
476
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
477
|
+
`, [event.id, event.agentId, event.orgId, event.sessionId || null, event.type, JSON.stringify(event.data), event.timestamp]);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async getActivityEvents(opts: { agentId?: string; orgId?: string; types?: string[]; since?: string; limit?: number }): Promise<ActivityEvent[]> {
|
|
481
|
+
const conditions: string[] = [];
|
|
482
|
+
const params: any[] = [];
|
|
483
|
+
if (opts.agentId) { conditions.push('agent_id = ?'); params.push(opts.agentId); }
|
|
484
|
+
if (opts.orgId) { conditions.push('org_id = ?'); params.push(opts.orgId); }
|
|
485
|
+
if (opts.types?.length) { conditions.push(`type IN (${opts.types.map(() => '?').join(',')})`); params.push(...opts.types); }
|
|
486
|
+
if (opts.since) { conditions.push('created_at >= ?'); params.push(opts.since); }
|
|
487
|
+
const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
|
|
488
|
+
params.push(opts.limit || 50);
|
|
489
|
+
const rows = await this.db.all<any>(`SELECT * FROM activity_events ${where} ORDER BY created_at DESC LIMIT ?`, params);
|
|
490
|
+
return rows.map(r => ({
|
|
491
|
+
id: r.id, agentId: r.agent_id, orgId: r.org_id, sessionId: r.session_id,
|
|
492
|
+
type: r.type, data: JSON.parse(r.data), timestamp: r.created_at,
|
|
493
|
+
}));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ─── Conversations ──────────────────────────────────
|
|
497
|
+
|
|
498
|
+
async insertConversation(entry: ConversationEntry): Promise<void> {
|
|
499
|
+
await this.db.run(`
|
|
500
|
+
INSERT INTO conversations (id, agent_id, session_id, role, content, channel, token_count, tool_calls, created_at)
|
|
501
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
502
|
+
`, [
|
|
503
|
+
entry.id, entry.agentId, entry.sessionId, entry.role,
|
|
504
|
+
entry.content, entry.channel || null, entry.tokenCount,
|
|
505
|
+
entry.toolCalls ? JSON.stringify(entry.toolCalls) : null, entry.timestamp,
|
|
506
|
+
]);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async getConversation(sessionId: string, limit: number = 50): Promise<ConversationEntry[]> {
|
|
510
|
+
const rows = await this.db.all<any>(
|
|
511
|
+
'SELECT * FROM conversations WHERE session_id = ? ORDER BY created_at ASC LIMIT ?',
|
|
512
|
+
[sessionId, limit]
|
|
513
|
+
);
|
|
514
|
+
return rows.map(r => ({
|
|
515
|
+
id: r.id, agentId: r.agent_id, sessionId: r.session_id, role: r.role,
|
|
516
|
+
content: r.content, channel: r.channel, tokenCount: r.token_count,
|
|
517
|
+
toolCalls: r.tool_calls ? JSON.parse(r.tool_calls) : undefined, timestamp: r.created_at,
|
|
518
|
+
}));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ─── Approval Requests ──────────────────────────────
|
|
522
|
+
|
|
523
|
+
async insertApprovalRequest(req: ApprovalRequest, orgId: string): Promise<void> {
|
|
524
|
+
await this.db.run(`
|
|
525
|
+
INSERT INTO approval_requests (id, agent_id, agent_name, org_id, tool_id, tool_name, reason, risk_level, side_effects, parameters, context, status, decision, expires_at, created_at)
|
|
526
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
527
|
+
`, [
|
|
528
|
+
req.id, req.agentId, req.agentName, orgId, req.toolId, req.toolName,
|
|
529
|
+
req.reason, req.riskLevel, JSON.stringify(req.sideEffects),
|
|
530
|
+
req.parameters ? JSON.stringify(req.parameters) : null, req.context || null,
|
|
531
|
+
req.status, req.decision ? JSON.stringify(req.decision) : null,
|
|
532
|
+
req.expiresAt, req.createdAt,
|
|
533
|
+
]);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async updateApprovalRequest(id: string, status: string, decision?: any): Promise<void> {
|
|
537
|
+
await this.db.run(
|
|
538
|
+
'UPDATE approval_requests SET status = ?, decision = ? WHERE id = ?',
|
|
539
|
+
[status, decision ? JSON.stringify(decision) : null, id]
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async getApprovalRequests(opts: { orgId?: string; status?: string; agentId?: string; limit?: number }): Promise<ApprovalRequest[]> {
|
|
544
|
+
const conditions: string[] = [];
|
|
545
|
+
const params: any[] = [];
|
|
546
|
+
if (opts.orgId) { conditions.push('org_id = ?'); params.push(opts.orgId); }
|
|
547
|
+
if (opts.status) { conditions.push('status = ?'); params.push(opts.status); }
|
|
548
|
+
if (opts.agentId) { conditions.push('agent_id = ?'); params.push(opts.agentId); }
|
|
549
|
+
const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
|
|
550
|
+
params.push(opts.limit || 50);
|
|
551
|
+
const rows = await this.db.all<any>(`SELECT * FROM approval_requests ${where} ORDER BY created_at DESC LIMIT ?`, params);
|
|
552
|
+
return rows.map(r => ({
|
|
553
|
+
id: r.id, agentId: r.agent_id, agentName: r.agent_name, toolId: r.tool_id,
|
|
554
|
+
toolName: r.tool_name, reason: r.reason, riskLevel: r.risk_level,
|
|
555
|
+
sideEffects: JSON.parse(r.side_effects), parameters: r.parameters ? JSON.parse(r.parameters) : undefined,
|
|
556
|
+
context: r.context, status: r.status,
|
|
557
|
+
decision: r.decision ? JSON.parse(r.decision) : undefined,
|
|
558
|
+
createdAt: r.created_at, expiresAt: r.expires_at,
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ─── Approval Policies ──────────────────────────────
|
|
563
|
+
|
|
564
|
+
async upsertApprovalPolicy(orgId: string, policy: ApprovalPolicy): Promise<void> {
|
|
565
|
+
await this.db.run(`
|
|
566
|
+
INSERT INTO approval_policies (id, org_id, name, description, triggers, approvers, timeout, notify, enabled, created_at, updated_at)
|
|
567
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
568
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
569
|
+
name = excluded.name, description = excluded.description,
|
|
570
|
+
triggers = excluded.triggers, approvers = excluded.approvers,
|
|
571
|
+
timeout = excluded.timeout, notify = excluded.notify,
|
|
572
|
+
enabled = excluded.enabled, updated_at = excluded.updated_at
|
|
573
|
+
`, [
|
|
574
|
+
policy.id, orgId, policy.name, policy.description || null,
|
|
575
|
+
JSON.stringify(policy.triggers), JSON.stringify(policy.approvers),
|
|
576
|
+
JSON.stringify(policy.timeout), JSON.stringify(policy.notify),
|
|
577
|
+
policy.enabled ? 1 : 0, new Date().toISOString(), new Date().toISOString(),
|
|
578
|
+
]);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async getApprovalPolicies(orgId: string): Promise<ApprovalPolicy[]> {
|
|
582
|
+
const rows = await this.db.all<any>('SELECT * FROM approval_policies WHERE org_id = ? ORDER BY name', [orgId]);
|
|
583
|
+
return rows.map(r => ({
|
|
584
|
+
id: r.id, name: r.name, description: r.description,
|
|
585
|
+
triggers: JSON.parse(r.triggers), approvers: JSON.parse(r.approvers),
|
|
586
|
+
timeout: JSON.parse(r.timeout), notify: JSON.parse(r.notify),
|
|
587
|
+
enabled: !!r.enabled,
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async deleteApprovalPolicy(id: string): Promise<void> {
|
|
592
|
+
await this.db.run('DELETE FROM approval_policies WHERE id = ?', [id]);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ─── Aggregate Stats ───────────────────────────────
|
|
596
|
+
|
|
597
|
+
async getEngineStats(orgId: string): Promise<{
|
|
598
|
+
totalManagedAgents: number;
|
|
599
|
+
runningAgents: number;
|
|
600
|
+
totalToolCallsToday: number;
|
|
601
|
+
totalActivityToday: number;
|
|
602
|
+
pendingApprovals: number;
|
|
603
|
+
totalKnowledgeBases: number;
|
|
604
|
+
}> {
|
|
605
|
+
const today = new Date().toISOString().split('T')[0];
|
|
606
|
+
|
|
607
|
+
const [agents, running, toolCalls, activity, approvals, kbs] = await Promise.all([
|
|
608
|
+
this.db.get<any>('SELECT COUNT(*) as c FROM managed_agents WHERE org_id = ?', [orgId]),
|
|
609
|
+
this.db.get<any>('SELECT COUNT(*) as c FROM managed_agents WHERE org_id = ? AND state = ?', [orgId, 'running']),
|
|
610
|
+
this.db.get<any>('SELECT COUNT(*) as c FROM tool_calls WHERE org_id = ? AND created_at >= ?', [orgId, today]),
|
|
611
|
+
this.db.get<any>('SELECT COUNT(*) as c FROM activity_events WHERE org_id = ? AND created_at >= ?', [orgId, today]),
|
|
612
|
+
this.db.get<any>('SELECT COUNT(*) as c FROM approval_requests WHERE org_id = ? AND status = ?', [orgId, 'pending']),
|
|
613
|
+
this.db.get<any>('SELECT COUNT(*) as c FROM knowledge_bases WHERE org_id = ?', [orgId]),
|
|
614
|
+
]);
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
totalManagedAgents: agents?.c || 0,
|
|
618
|
+
runningAgents: running?.c || 0,
|
|
619
|
+
totalToolCallsToday: toolCalls?.c || 0,
|
|
620
|
+
totalActivityToday: activity?.c || 0,
|
|
621
|
+
pendingApprovals: approvals?.c || 0,
|
|
622
|
+
totalKnowledgeBases: kbs?.c || 0,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Cleanup old data based on retention
|
|
628
|
+
*/
|
|
629
|
+
async cleanup(retainDays: number): Promise<{ toolCalls: number; events: number; conversations: number }> {
|
|
630
|
+
const cutoff = new Date(Date.now() - retainDays * 86_400_000).toISOString();
|
|
631
|
+
|
|
632
|
+
const tc = await this.db.get<any>('SELECT COUNT(*) as c FROM tool_calls WHERE created_at < ?', [cutoff]);
|
|
633
|
+
const ev = await this.db.get<any>('SELECT COUNT(*) as c FROM activity_events WHERE created_at < ?', [cutoff]);
|
|
634
|
+
const cv = await this.db.get<any>('SELECT COUNT(*) as c FROM conversations WHERE created_at < ?', [cutoff]);
|
|
635
|
+
|
|
636
|
+
await this.db.run('DELETE FROM tool_calls WHERE created_at < ?', [cutoff]);
|
|
637
|
+
await this.db.run('DELETE FROM activity_events WHERE created_at < ?', [cutoff]);
|
|
638
|
+
await this.db.run('DELETE FROM conversations WHERE created_at < ?', [cutoff]);
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
toolCalls: tc?.c || 0,
|
|
642
|
+
events: ev?.c || 0,
|
|
643
|
+
conversations: cv?.c || 0,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ─── Row Mappers ────────────────────────────────────
|
|
648
|
+
|
|
649
|
+
private rowToManagedAgent(row: any): ManagedAgent {
|
|
650
|
+
return {
|
|
651
|
+
id: row.id,
|
|
652
|
+
orgId: row.org_id,
|
|
653
|
+
config: JSON.parse(row.config),
|
|
654
|
+
state: row.state,
|
|
655
|
+
stateHistory: [], // Loaded separately via getStateHistory
|
|
656
|
+
health: JSON.parse(row.health || '{}'),
|
|
657
|
+
usage: JSON.parse(row.usage || '{}'),
|
|
658
|
+
createdAt: row.created_at,
|
|
659
|
+
updatedAt: row.updated_at,
|
|
660
|
+
lastDeployedAt: row.last_deployed_at,
|
|
661
|
+
lastHealthCheckAt: row.last_health_check_at,
|
|
662
|
+
version: row.version,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private rowToOrg(row: any): Organization {
|
|
667
|
+
return {
|
|
668
|
+
id: row.id,
|
|
669
|
+
name: row.name,
|
|
670
|
+
slug: row.slug,
|
|
671
|
+
plan: row.plan as OrgPlan,
|
|
672
|
+
limits: JSON.parse(row.limits || '{}'),
|
|
673
|
+
usage: JSON.parse(row.usage || '{}'),
|
|
674
|
+
settings: JSON.parse(row.settings || '{}'),
|
|
675
|
+
ssoConfig: row.sso_config ? JSON.parse(row.sso_config) : undefined,
|
|
676
|
+
allowedDomains: JSON.parse(row.allowed_domains || '[]'),
|
|
677
|
+
billing: row.billing ? JSON.parse(row.billing) : undefined,
|
|
678
|
+
createdAt: row.created_at,
|
|
679
|
+
updatedAt: row.updated_at,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|