@agenticmail/enterprise 0.5.298 → 0.5.299

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.
@@ -2137,6 +2137,262 @@ export function createAdminRoutes(db: DatabaseAdapter) {
2137
2137
  }
2138
2138
  });
2139
2139
 
2140
+ // ─── Client Organizations ─────────────────────────────
2141
+
2142
+ api.get('/organizations', requireRole('admin'), async (c) => {
2143
+ try {
2144
+ const isPostgres = (db as any).pool;
2145
+ if (isPostgres) {
2146
+ const { rows } = await (db as any)._query(`
2147
+ SELECT o.*, COUNT(a.id) as agent_count
2148
+ FROM client_organizations o
2149
+ LEFT JOIN agents a ON a.client_org_id = o.id
2150
+ GROUP BY o.id
2151
+ ORDER BY o.created_at DESC
2152
+ `);
2153
+ return c.json({ organizations: rows });
2154
+ } else {
2155
+ const engineDb = db.getEngineDB();
2156
+ const rows = await engineDb!.all(`
2157
+ SELECT o.*, COUNT(a.id) as agent_count
2158
+ FROM client_organizations o
2159
+ LEFT JOIN agents a ON a.client_org_id = o.id
2160
+ GROUP BY o.id
2161
+ ORDER BY o.created_at DESC
2162
+ `);
2163
+ return c.json({ organizations: rows });
2164
+ }
2165
+ } catch (e: any) {
2166
+ return c.json({ error: e.message }, 500);
2167
+ }
2168
+ });
2169
+
2170
+ api.post('/organizations', requireRole('admin'), async (c) => {
2171
+ const body = await c.req.json();
2172
+ validate(body, [
2173
+ { field: 'name', type: 'string', required: true, minLength: 1, maxLength: 128 },
2174
+ { field: 'slug', type: 'string', required: true, minLength: 1, maxLength: 64, pattern: /^[a-z0-9-]+$/ },
2175
+ { field: 'contact_name', type: 'string', maxLength: 128 },
2176
+ { field: 'contact_email', type: 'email' },
2177
+ { field: 'description', type: 'string', maxLength: 512 },
2178
+ ]);
2179
+ const id = (await import('crypto')).randomUUID();
2180
+ try {
2181
+ const isPostgres = (db as any).pool;
2182
+ if (isPostgres) {
2183
+ await (db as any)._query(
2184
+ `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description) VALUES ($1, $2, $3, $4, $5, $6)`,
2185
+ [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null]
2186
+ );
2187
+ const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);
2188
+ return c.json(rows[0], 201);
2189
+ } else {
2190
+ const engineDb = db.getEngineDB();
2191
+ await engineDb!.run(
2192
+ `INSERT INTO client_organizations (id, name, slug, contact_name, contact_email, description) VALUES (?, ?, ?, ?, ?, ?)`,
2193
+ [id, body.name, body.slug, body.contact_name || null, body.contact_email || null, body.description || null]
2194
+ );
2195
+ const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);
2196
+ return c.json(row, 201);
2197
+ }
2198
+ } catch (e: any) {
2199
+ if (e.message?.includes('UNIQUE') || e.code === '23505') return c.json({ error: 'Slug already exists' }, 409);
2200
+ return c.json({ error: e.message }, 500);
2201
+ }
2202
+ });
2203
+
2204
+ api.get('/organizations/:id', requireRole('admin'), async (c) => {
2205
+ const id = c.req.param('id');
2206
+ try {
2207
+ const isPostgres = (db as any).pool;
2208
+ if (isPostgres) {
2209
+ const { rows: orgs } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);
2210
+ if (!orgs[0]) return c.json({ error: 'Organization not found' }, 404);
2211
+ const { rows: agents } = await (db as any)._query(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = $1`, [id]);
2212
+ return c.json({ ...orgs[0], agents });
2213
+ } else {
2214
+ const engineDb = db.getEngineDB();
2215
+ const org = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);
2216
+ if (!org) return c.json({ error: 'Organization not found' }, 404);
2217
+ const agents = await engineDb!.all(`SELECT id, name, email, role, status FROM agents WHERE client_org_id = ?`, [id]);
2218
+ return c.json({ ...(org as any), agents });
2219
+ }
2220
+ } catch (e: any) {
2221
+ return c.json({ error: e.message }, 500);
2222
+ }
2223
+ });
2224
+
2225
+ api.patch('/organizations/:id', requireRole('admin'), async (c) => {
2226
+ const id = c.req.param('id');
2227
+ const body = await c.req.json();
2228
+ validate(body, [
2229
+ { field: 'name', type: 'string', minLength: 1, maxLength: 128 },
2230
+ { field: 'contact_name', type: 'string', maxLength: 128 },
2231
+ { field: 'contact_email', type: 'email' },
2232
+ { field: 'description', type: 'string', maxLength: 512 },
2233
+ ]);
2234
+ try {
2235
+ const fields: string[] = [];
2236
+ const values: any[] = [];
2237
+ const isPostgres = (db as any).pool;
2238
+ let idx = 1;
2239
+ for (const key of ['name', 'contact_name', 'contact_email', 'description']) {
2240
+ if (body[key] !== undefined) {
2241
+ fields.push(isPostgres ? `${key} = $${idx++}` : `${key} = ?`);
2242
+ values.push(body[key]);
2243
+ }
2244
+ }
2245
+ if (fields.length === 0) return c.json({ error: 'No fields to update' }, 400);
2246
+ fields.push(isPostgres ? `updated_at = NOW()` : `updated_at = datetime('now')`);
2247
+ values.push(id);
2248
+ const where = isPostgres ? `$${idx}` : '?';
2249
+ const sql = `UPDATE client_organizations SET ${fields.join(', ')} WHERE id = ${where}`;
2250
+ if (isPostgres) {
2251
+ await (db as any)._query(sql, values);
2252
+ const { rows } = await (db as any)._query(`SELECT * FROM client_organizations WHERE id = $1`, [id]);
2253
+ return c.json(rows[0]);
2254
+ } else {
2255
+ const engineDb = db.getEngineDB();
2256
+ await engineDb!.run(sql, values);
2257
+ const row = await engineDb!.get(`SELECT * FROM client_organizations WHERE id = ?`, [id]);
2258
+ return c.json(row);
2259
+ }
2260
+ } catch (e: any) {
2261
+ return c.json({ error: e.message }, 500);
2262
+ }
2263
+ });
2264
+
2265
+ api.post('/organizations/:id/toggle', requireRole('admin'), async (c) => {
2266
+ const id = c.req.param('id');
2267
+ try {
2268
+ const isPostgres = (db as any).pool;
2269
+ if (isPostgres) {
2270
+ const { rows } = await (db as any)._query(`SELECT is_active FROM client_organizations WHERE id = $1`, [id]);
2271
+ if (!rows[0]) return c.json({ error: 'Organization not found' }, 404);
2272
+ const newActive = !rows[0].is_active;
2273
+ await (db as any)._query(`UPDATE client_organizations SET is_active = $1, updated_at = NOW() WHERE id = $2`, [newActive, id]);
2274
+ const newStatus = newActive ? 'active' : 'suspended';
2275
+ await (db as any)._query(`UPDATE agents SET status = $1 WHERE client_org_id = $2`, [newStatus, id]);
2276
+ return c.json({ is_active: newActive });
2277
+ } else {
2278
+ const engineDb = db.getEngineDB();
2279
+ const org = await engineDb!.get<any>(`SELECT is_active FROM client_organizations WHERE id = ?`, [id]);
2280
+ if (!org) return c.json({ error: 'Organization not found' }, 404);
2281
+ const newActive = !(org.is_active);
2282
+ await engineDb!.run(`UPDATE client_organizations SET is_active = ?, updated_at = datetime('now') WHERE id = ?`, [newActive ? 1 : 0, id]);
2283
+ const newStatus = newActive ? 'active' : 'suspended';
2284
+ await engineDb!.run(`UPDATE agents SET status = ? WHERE client_org_id = ?`, [newStatus, id]);
2285
+ return c.json({ is_active: newActive });
2286
+ }
2287
+ } catch (e: any) {
2288
+ return c.json({ error: e.message }, 500);
2289
+ }
2290
+ });
2291
+
2292
+ api.delete('/organizations/:id', requireRole('admin'), async (c) => {
2293
+ const id = c.req.param('id');
2294
+ try {
2295
+ const isPostgres = (db as any).pool;
2296
+ if (isPostgres) {
2297
+ const { rows: agents } = await (db as any)._query(`SELECT id FROM agents WHERE client_org_id = $1`, [id]);
2298
+ if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);
2299
+ await (db as any)._query(`DELETE FROM client_organizations WHERE id = $1`, [id]);
2300
+ } else {
2301
+ const engineDb = db.getEngineDB();
2302
+ const agents = await engineDb!.all(`SELECT id FROM agents WHERE client_org_id = ?`, [id]);
2303
+ if (agents.length > 0) return c.json({ error: 'Cannot delete organization with linked agents. Unassign all agents first.' }, 400);
2304
+ await engineDb!.run(`DELETE FROM client_organizations WHERE id = ?`, [id]);
2305
+ }
2306
+ return c.json({ success: true });
2307
+ } catch (e: any) {
2308
+ return c.json({ error: e.message }, 500);
2309
+ }
2310
+ });
2311
+
2312
+ // ─── Agent-Org Linking ──────────────────────────────────
2313
+
2314
+ api.post('/agents/:id/assign-org', requireRole('admin'), async (c) => {
2315
+ const agentId = c.req.param('id');
2316
+ const { orgId } = await c.req.json();
2317
+ if (!orgId) return c.json({ error: 'orgId is required' }, 400);
2318
+ try {
2319
+ const isPostgres = (db as any).pool;
2320
+ if (isPostgres) {
2321
+ await (db as any)._query(`UPDATE agents SET client_org_id = $1 WHERE id = $2`, [orgId, agentId]);
2322
+ } else {
2323
+ await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = ? WHERE id = ?`, [orgId, agentId]);
2324
+ }
2325
+ return c.json({ success: true });
2326
+ } catch (e: any) {
2327
+ return c.json({ error: e.message }, 500);
2328
+ }
2329
+ });
2330
+
2331
+ api.post('/agents/:id/unassign-org', requireRole('admin'), async (c) => {
2332
+ const agentId = c.req.param('id');
2333
+ try {
2334
+ const isPostgres = (db as any).pool;
2335
+ if (isPostgres) {
2336
+ await (db as any)._query(`UPDATE agents SET client_org_id = NULL WHERE id = $1`, [agentId]);
2337
+ } else {
2338
+ await db.getEngineDB()!.run(`UPDATE agents SET client_org_id = NULL WHERE id = ?`, [agentId]);
2339
+ }
2340
+ return c.json({ success: true });
2341
+ } catch (e: any) {
2342
+ return c.json({ error: e.message }, 500);
2343
+ }
2344
+ });
2345
+
2346
+ // ─── Agent Knowledge Access ─────────────────────────────
2347
+
2348
+ api.get('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {
2349
+ const agentId = c.req.param('id');
2350
+ try {
2351
+ const isPostgres = (db as any).pool;
2352
+ if (isPostgres) {
2353
+ const { rows } = await (db as any)._query(`SELECT * FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);
2354
+ return c.json({ grants: rows });
2355
+ } else {
2356
+ const rows = await db.getEngineDB()!.all(`SELECT * FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);
2357
+ return c.json({ grants: rows });
2358
+ }
2359
+ } catch (e: any) {
2360
+ return c.json({ error: e.message }, 500);
2361
+ }
2362
+ });
2363
+
2364
+ api.put('/agents/:id/knowledge-access', requireRole('admin'), async (c) => {
2365
+ const agentId = c.req.param('id');
2366
+ const { grants } = await c.req.json();
2367
+ if (!Array.isArray(grants)) return c.json({ error: 'grants must be an array' }, 400);
2368
+ try {
2369
+ const isPostgres = (db as any).pool;
2370
+ if (isPostgres) {
2371
+ await (db as any)._query(`DELETE FROM agent_knowledge_access WHERE agent_id = $1`, [agentId]);
2372
+ for (const g of grants) {
2373
+ const id = (await import('crypto')).randomUUID();
2374
+ await (db as any)._query(
2375
+ `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES ($1, $2, $3, $4)`,
2376
+ [id, agentId, g.knowledgeBaseId, g.accessType || 'read']
2377
+ );
2378
+ }
2379
+ } else {
2380
+ const engineDb = db.getEngineDB()!;
2381
+ await engineDb.run(`DELETE FROM agent_knowledge_access WHERE agent_id = ?`, [agentId]);
2382
+ for (const g of grants) {
2383
+ const id = (await import('crypto')).randomUUID();
2384
+ await engineDb.run(
2385
+ `INSERT INTO agent_knowledge_access (id, agent_id, knowledge_base_id, access_type) VALUES (?, ?, ?, ?)`,
2386
+ [id, agentId, g.knowledgeBaseId, g.accessType || 'read']
2387
+ );
2388
+ }
2389
+ }
2390
+ return c.json({ success: true });
2391
+ } catch (e: any) {
2392
+ return c.json({ error: e.message }, 500);
2393
+ }
2394
+ });
2395
+
2140
2396
  /** Stop and optionally delete tunnel */
2141
2397
  api.post('/tunnel/stop', requireRole('admin'), async (c) => {
2142
2398
  try {
@@ -27,6 +27,7 @@ import { VaultPage } from './pages/vault.js';
27
27
  import { OrgChartPage } from './pages/org-chart.js';
28
28
  import { TaskPipelinePage } from './pages/task-pipeline.js';
29
29
  import { DatabaseAccessPage } from './pages/database-access.js';
30
+ import { OrganizationsPage } from './pages/organizations.js';
30
31
 
31
32
  // ─── Toast System ────────────────────────────────────────
32
33
  let toastId = 0;
@@ -211,6 +212,7 @@ function App() {
211
212
  { section: 'Overview', items: [{ id: 'dashboard', icon: I.dashboard, label: 'Dashboard' }] },
212
213
  { section: 'Management', items: [
213
214
  { id: 'agents', icon: I.agents, label: 'Agents' },
215
+ { id: 'organizations', icon: I.building, label: 'Organizations' },
214
216
  { id: 'skills', icon: I.skills, label: 'Skills' },
215
217
  { id: 'community-skills', icon: I.marketplace, label: 'Community Skills' },
216
218
  { id: 'skill-connections', icon: I.link, label: 'Integrations & MCP' },
@@ -262,6 +264,7 @@ function App() {
262
264
  'org-chart': OrgChartPage,
263
265
  'task-pipeline': TaskPipelinePage,
264
266
  'database-access': DatabaseAccessPage,
267
+ organizations: OrganizationsPage,
265
268
  };
266
269
 
267
270
  const navigateToAgent = (agentId) => { _setSelectedAgentId(agentId); history.pushState(null, '', '/dashboard/agents/' + agentId); };
@@ -56,5 +56,6 @@ export const I = {
56
56
  chevronLeft: () => h('svg', S, h('polyline', { points: '15 18 9 12 15 6' })),
57
57
  chevronRight: () => h('svg', S, h('polyline', { points: '9 18 15 12 9 6' })),
58
58
  chevronDown: () => h('svg', S, h('polyline', { points: '6 9 12 15 18 9' })),
59
+ building: () => h('svg', S, h('path', { d: 'M3 21h18M3 10h18M3 7l9-4 9 4M4 10v11M20 10v11M8 14v.01M12 14v.01M16 14v.01M8 18v.01M12 18v.01M16 18v.01' })),
59
60
  edit: () => h('svg', S, h('path', { d: 'M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7' }), h('path', { d: 'M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z' })),
60
61
  };
@@ -363,6 +363,9 @@ export function OverviewSection(props) {
363
363
  )
364
364
  ),
365
365
 
366
+ // ─── Organization & Knowledge Access ──────────────────
367
+ h(OrgAndKnowledgeCards, { agentId: agentId, agent: agent, engineAgent: engineAgent, toast: toast }),
368
+
366
369
  // ─── Real-Time Status Card ────────────────────────────
367
370
  h('div', { className: 'card', style: { marginBottom: 20 } },
368
371
  h('div', { className: 'card-header', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
@@ -649,6 +652,113 @@ function _timeAgo(ts) {
649
652
  return Math.floor(diff / 3600000) + 'h ago';
650
653
  }
651
654
 
655
+ // ─── Organization & Knowledge Access Cards ────────────────
656
+ function OrgAndKnowledgeCards(props) {
657
+ var agentId = props.agentId;
658
+ var toast = props.toast;
659
+
660
+ var _orgs = useState([]);
661
+ var orgs = _orgs[0]; var setOrgs = _orgs[1];
662
+ var _currentOrg = useState(null);
663
+ var currentOrg = _currentOrg[0]; var setCurrentOrg = _currentOrg[1];
664
+ var _kbs = useState([]);
665
+ var kbs = _kbs[0]; var setKbs = _kbs[1];
666
+ var _kaGrants = useState([]);
667
+ var kaGrants = _kaGrants[0]; var setKaGrants = _kaGrants[1];
668
+ var _acting = useState('');
669
+ var acting = _acting[0]; var setActing = _acting[1];
670
+ var _selOrg = useState('');
671
+ var selOrg = _selOrg[0]; var setSelOrg = _selOrg[1];
672
+
673
+ useEffect(function() {
674
+ // Load orgs list
675
+ apiCall('/organizations').then(function(d) { setOrgs(d.organizations || []); }).catch(function() {});
676
+ // Load agent's current org
677
+ apiCall('/agents/' + agentId).then(function(a) {
678
+ if (a && a.client_org_id) {
679
+ setSelOrg(a.client_org_id);
680
+ apiCall('/organizations/' + a.client_org_id).then(function(o) { setCurrentOrg(o); }).catch(function() {});
681
+ }
682
+ }).catch(function() {});
683
+ // Load knowledge access
684
+ apiCall('/agents/' + agentId + '/knowledge-access').then(function(d) { setKaGrants(d.grants || []); }).catch(function() {});
685
+ // Load knowledge bases
686
+ engineCall('/knowledge').then(function(d) { setKbs(d.knowledgeBases || d || []); }).catch(function() {});
687
+ }, [agentId]);
688
+
689
+ var assignOrg = function(orgId) {
690
+ if (!orgId) {
691
+ setActing('unassign');
692
+ apiCall('/agents/' + agentId + '/unassign-org', { method: 'POST' }).then(function() {
693
+ setCurrentOrg(null); setSelOrg(''); toast('Organization unassigned', 'success');
694
+ }).catch(function(e) { toast(e.message, 'error'); }).finally(function() { setActing(''); });
695
+ return;
696
+ }
697
+ setActing('assign');
698
+ apiCall('/agents/' + agentId + '/assign-org', { method: 'POST', body: JSON.stringify({ orgId: orgId }) }).then(function() {
699
+ setSelOrg(orgId);
700
+ var org = orgs.find(function(o) { return o.id === orgId; });
701
+ setCurrentOrg(org || null);
702
+ toast('Organization assigned', 'success');
703
+ }).catch(function(e) { toast(e.message, 'error'); }).finally(function() { setActing(''); });
704
+ };
705
+
706
+ return h(Fragment, null,
707
+ h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 } },
708
+ // Organization card
709
+ h('div', { className: 'card' },
710
+ h('div', { className: 'card-header' }, 'Organization'),
711
+ h('div', { className: 'card-body' },
712
+ currentOrg
713
+ ? h('div', null,
714
+ h('div', { style: { fontWeight: 600, fontSize: 14, marginBottom: 4 } }, currentOrg.name),
715
+ h('div', { style: { fontSize: 12, color: 'var(--text-muted)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 8 } }, currentOrg.slug),
716
+ h('span', { className: 'badge badge-' + (currentOrg.is_active ? 'success' : 'warning') }, currentOrg.is_active ? 'Active' : 'Inactive')
717
+ )
718
+ : h('div', { style: { fontSize: 13, color: 'var(--text-muted)', marginBottom: 8 } }, 'Unassigned'),
719
+ h('div', { style: { marginTop: 10 } },
720
+ h('select', { className: 'input', value: selOrg, disabled: !!acting, onChange: function(e) { assignOrg(e.target.value); }, style: { width: '100%', fontSize: 12 } },
721
+ h('option', { value: '' }, '— No organization —'),
722
+ orgs.map(function(o) { return h('option', { key: o.id, value: o.id }, o.name); })
723
+ )
724
+ )
725
+ )
726
+ ),
727
+ // Knowledge Access card
728
+ h('div', { className: 'card' },
729
+ h('div', { className: 'card-header' }, 'Knowledge Access'),
730
+ h('div', { className: 'card-body' },
731
+ kbs.length === 0
732
+ ? h('div', { style: { fontSize: 13, color: 'var(--text-muted)' } }, 'No knowledge bases configured')
733
+ : h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
734
+ kbs.map(function(kb) {
735
+ var grant = kaGrants.find(function(g) { return g.knowledge_base_id === kb.id; });
736
+ var accessType = grant ? grant.access_type : '';
737
+ return h('div', { key: kb.id, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', borderBottom: '1px solid var(--border)' } },
738
+ h('span', { style: { fontSize: 13, fontWeight: 500 } }, kb.name || kb.id),
739
+ h('select', { className: 'input', value: accessType, style: { width: 120, fontSize: 11, padding: '2px 6px' }, onChange: function(e) {
740
+ var newVal = e.target.value;
741
+ var newGrants = kaGrants.filter(function(g) { return g.knowledge_base_id !== kb.id; });
742
+ if (newVal) newGrants.push({ knowledge_base_id: kb.id, access_type: newVal });
743
+ var payload = newGrants.map(function(g) { return { knowledgeBaseId: g.knowledge_base_id, accessType: g.access_type }; });
744
+ apiCall('/agents/' + agentId + '/knowledge-access', { method: 'PUT', body: JSON.stringify({ grants: payload }) })
745
+ .then(function() { setKaGrants(newGrants); toast('Knowledge access updated', 'success'); })
746
+ .catch(function(err) { toast(err.message, 'error'); });
747
+ }},
748
+ h('option', { value: '' }, 'No access'),
749
+ h('option', { value: 'read' }, 'Read'),
750
+ h('option', { value: 'contribute' }, 'Contribute'),
751
+ h('option', { value: 'both' }, 'Both')
752
+ )
753
+ );
754
+ })
755
+ )
756
+ )
757
+ )
758
+ )
759
+ );
760
+ }
761
+
652
762
  // Inject pulse animation CSS if not already present
653
763
  if (typeof document !== 'undefined' && !document.getElementById('_pulse_css')) {
654
764
  var style = document.createElement('style');