@boringos/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/dist/admin-routes.d.ts +12 -0
  3. package/dist/admin-routes.d.ts.map +1 -0
  4. package/dist/admin-routes.js +836 -0
  5. package/dist/admin-routes.js.map +1 -0
  6. package/dist/auth-routes.d.ts +9 -0
  7. package/dist/auth-routes.d.ts.map +1 -0
  8. package/dist/auth-routes.js +112 -0
  9. package/dist/auth-routes.js.map +1 -0
  10. package/dist/auth.d.ts +32 -0
  11. package/dist/auth.d.ts.map +1 -0
  12. package/dist/auth.js +102 -0
  13. package/dist/auth.js.map +1 -0
  14. package/dist/boringos.d.ts +52 -0
  15. package/dist/boringos.d.ts.map +1 -0
  16. package/dist/boringos.js +301 -0
  17. package/dist/boringos.js.map +1 -0
  18. package/dist/connector-routes.d.ts +5 -0
  19. package/dist/connector-routes.d.ts.map +1 -0
  20. package/dist/connector-routes.js +77 -0
  21. package/dist/connector-routes.js.map +1 -0
  22. package/dist/device-auth-routes.d.ts +12 -0
  23. package/dist/device-auth-routes.d.ts.map +1 -0
  24. package/dist/device-auth-routes.js +68 -0
  25. package/dist/device-auth-routes.js.map +1 -0
  26. package/dist/index.d.ts +17 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +8 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/notifications.d.ts +30 -0
  31. package/dist/notifications.d.ts.map +1 -0
  32. package/dist/notifications.js +61 -0
  33. package/dist/notifications.js.map +1 -0
  34. package/dist/plugin-routes.d.ts +12 -0
  35. package/dist/plugin-routes.d.ts.map +1 -0
  36. package/dist/plugin-routes.js +70 -0
  37. package/dist/plugin-routes.js.map +1 -0
  38. package/dist/plugin-system.d.ts +57 -0
  39. package/dist/plugin-system.d.ts.map +1 -0
  40. package/dist/plugin-system.js +62 -0
  41. package/dist/plugin-system.js.map +1 -0
  42. package/dist/plugins/github.d.ts +18 -0
  43. package/dist/plugins/github.d.ts.map +1 -0
  44. package/dist/plugins/github.js +97 -0
  45. package/dist/plugins/github.js.map +1 -0
  46. package/dist/realtime.d.ts +15 -0
  47. package/dist/realtime.d.ts.map +1 -0
  48. package/dist/realtime.js +38 -0
  49. package/dist/realtime.js.map +1 -0
  50. package/dist/routes.d.ts +12 -0
  51. package/dist/routes.d.ts.map +1 -0
  52. package/dist/routes.js +151 -0
  53. package/dist/routes.js.map +1 -0
  54. package/dist/scheduler.d.ts +13 -0
  55. package/dist/scheduler.d.ts.map +1 -0
  56. package/dist/scheduler.js +77 -0
  57. package/dist/scheduler.js.map +1 -0
  58. package/dist/sse-routes.d.ts +4 -0
  59. package/dist/sse-routes.d.ts.map +1 -0
  60. package/dist/sse-routes.js +37 -0
  61. package/dist/sse-routes.js.map +1 -0
  62. package/dist/types.d.ts +81 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +2 -0
  65. package/dist/types.js.map +1 -0
  66. package/package.json +53 -0
@@ -0,0 +1,836 @@
1
+ import { Hono } from "hono";
2
+ import { eq, and, desc, sql } from "drizzle-orm";
3
+ import { tenants, agents, tasks, taskComments, taskWorkProducts, agentRuns, runtimes, approvals, costEvents, activityLog, budgetPolicies, budgetIncidents, routines, companySkills, agentSkills, projects, goals, labels, taskLabels, taskReadStates, driveFiles, driveSkillRevisions, onboardingState, evals, evalRuns, inboxItems, entityReferences, } from "@boringos/db";
4
+ import { generateId } from "@boringos/shared";
5
+ export function createAdminRoutes(db, engine, adminKey, realtimeBus) {
6
+ function emit(type, tenantId, data) {
7
+ realtimeBus?.publish({ type, tenantId, data, timestamp: new Date().toISOString() });
8
+ }
9
+ async function logActivity(tenantId, action, entityType, entityId, metadata) {
10
+ await db.insert(activityLog).values({
11
+ id: generateId(),
12
+ tenantId,
13
+ action,
14
+ entityType,
15
+ entityId,
16
+ actorType: "user",
17
+ metadata: metadata ?? null,
18
+ }).catch(() => { });
19
+ }
20
+ const app = new Hono();
21
+ // Auth middleware — supports API key OR session token
22
+ app.use("/*", async (c, next) => {
23
+ const apiKey = c.req.header("X-API-Key");
24
+ const bearer = c.req.header("Authorization")?.replace("Bearer ", "");
25
+ // Option 1: API key auth
26
+ if (apiKey && apiKey === adminKey) {
27
+ const tenantId = c.req.header("X-Tenant-Id") ?? c.req.query("tenantId") ?? "";
28
+ if (!tenantId)
29
+ return c.json({ error: "Missing X-Tenant-Id header" }, 400);
30
+ c.set("tenantId", tenantId);
31
+ return next();
32
+ }
33
+ // Option 2: Session token auth (from login)
34
+ if (bearer) {
35
+ const { validateSession } = await import("./auth.js");
36
+ const session = await validateSession(db, bearer);
37
+ if (session) {
38
+ c.set("tenantId", session.tenantId);
39
+ return next();
40
+ }
41
+ }
42
+ return c.json({ error: "Invalid or missing authentication" }, 401);
43
+ });
44
+ // ── Agents ──────────────────────────────────────────────────────────────
45
+ app.get("/agents", async (c) => {
46
+ const rows = await db.select().from(agents).where(eq(agents.tenantId, c.get("tenantId")));
47
+ return c.json({ agents: rows });
48
+ });
49
+ app.get("/agents/:id", async (c) => {
50
+ const rows = await db.select().from(agents).where(and(eq(agents.id, c.req.param("id")), eq(agents.tenantId, c.get("tenantId")))).limit(1);
51
+ if (!rows[0])
52
+ return c.json({ error: "Agent not found" }, 404);
53
+ return c.json(rows[0]);
54
+ });
55
+ app.post("/agents", async (c) => {
56
+ const body = await c.req.json();
57
+ const id = generateId();
58
+ await db.insert(agents).values({
59
+ id,
60
+ tenantId: c.get("tenantId"),
61
+ name: body.name,
62
+ role: body.role ?? "general",
63
+ instructions: body.instructions,
64
+ runtimeId: body.runtimeId,
65
+ });
66
+ const rows = await db.select().from(agents).where(eq(agents.id, id)).limit(1);
67
+ emit("agent:created", c.get("tenantId"), { agentId: id, name: body.name });
68
+ await logActivity(c.get("tenantId"), "agent.created", "agent", id, { name: body.name, role: body.role });
69
+ return c.json(rows[0], 201);
70
+ });
71
+ app.patch("/agents/:id", async (c) => {
72
+ const body = await c.req.json();
73
+ const values = { updatedAt: new Date() };
74
+ if (body.name !== undefined)
75
+ values.name = body.name;
76
+ if (body.role !== undefined)
77
+ values.role = body.role;
78
+ if (body.instructions !== undefined)
79
+ values.instructions = body.instructions;
80
+ if (body.status !== undefined)
81
+ values.status = body.status;
82
+ if (body.runtimeId !== undefined)
83
+ values.runtimeId = body.runtimeId;
84
+ if (body.fallbackRuntimeId !== undefined)
85
+ values.fallbackRuntimeId = body.fallbackRuntimeId;
86
+ await db.update(agents).set(values).where(and(eq(agents.id, c.req.param("id")), eq(agents.tenantId, c.get("tenantId"))));
87
+ const rows = await db.select().from(agents).where(eq(agents.id, c.req.param("id"))).limit(1);
88
+ return c.json(rows[0]);
89
+ });
90
+ app.post("/agents/:id/wake", async (c) => {
91
+ const body = (await c.req.json().catch(() => ({})));
92
+ const outcome = await engine.wake({
93
+ agentId: c.req.param("id"),
94
+ tenantId: c.get("tenantId"),
95
+ reason: "manual_request",
96
+ taskId: body.taskId,
97
+ });
98
+ if (outcome.kind === "created") {
99
+ await engine.enqueue(outcome.wakeupRequestId);
100
+ }
101
+ return c.json(outcome);
102
+ });
103
+ app.get("/agents/:id/runs", async (c) => {
104
+ const rows = await db.select().from(agentRuns).where(and(eq(agentRuns.agentId, c.req.param("id")), eq(agentRuns.tenantId, c.get("tenantId")))).orderBy(desc(agentRuns.createdAt)).limit(50);
105
+ return c.json({ runs: rows });
106
+ });
107
+ // ── Tasks ───────────────────────────────────────────────────────────────
108
+ app.get("/tasks", async (c) => {
109
+ const status = c.req.query("status");
110
+ const assignee = c.req.query("assigneeAgentId");
111
+ let query = db.select().from(tasks).where(eq(tasks.tenantId, c.get("tenantId")));
112
+ // Note: drizzle doesn't chain .where easily, so we filter in-memory for optional params
113
+ const rows = await query.orderBy(desc(tasks.createdAt)).limit(100);
114
+ let filtered = rows;
115
+ if (status)
116
+ filtered = filtered.filter((t) => t.status === status);
117
+ if (assignee)
118
+ filtered = filtered.filter((t) => t.assigneeAgentId === assignee);
119
+ return c.json({ tasks: filtered });
120
+ });
121
+ app.get("/tasks/:id", async (c) => {
122
+ const taskId = c.req.param("id");
123
+ const taskRows = await db.select().from(tasks).where(and(eq(tasks.id, taskId), eq(tasks.tenantId, c.get("tenantId")))).limit(1);
124
+ if (!taskRows[0])
125
+ return c.json({ error: "Task not found" }, 404);
126
+ const comments = await db.select().from(taskComments)
127
+ .where(eq(taskComments.taskId, taskId))
128
+ .orderBy(desc(taskComments.createdAt));
129
+ const workProducts = await db.select().from(taskWorkProducts)
130
+ .where(eq(taskWorkProducts.taskId, taskId));
131
+ return c.json({ task: taskRows[0], comments, workProducts });
132
+ });
133
+ app.post("/tasks", async (c) => {
134
+ const body = await c.req.json();
135
+ const id = generateId();
136
+ const tenantId = c.get("tenantId");
137
+ const projectId = body.projectId;
138
+ // Auto-generate identifier if not provided
139
+ let identifier = body.identifier;
140
+ if (!identifier) {
141
+ if (projectId) {
142
+ // Use project prefix + counter
143
+ const projRows = await db.select().from(projects).where(eq(projects.id, projectId)).limit(1);
144
+ const proj = projRows[0];
145
+ if (proj) {
146
+ const prefix = proj.prefix ?? proj.name.slice(0, 3).toUpperCase();
147
+ const num = parseInt(proj.nextIssueNumber) || 1;
148
+ identifier = `${prefix}-${String(num).padStart(3, "0")}`;
149
+ await db.update(projects).set({ nextIssueNumber: String(num + 1) }).where(eq(projects.id, projectId));
150
+ }
151
+ }
152
+ else {
153
+ // Use tenant-level counter from settings
154
+ const { tenantSettings } = await import("@boringos/db");
155
+ const counterRows = await db.select().from(tenantSettings).where(and(eq(tenantSettings.tenantId, tenantId), eq(tenantSettings.key, "task_counter"))).limit(1);
156
+ const counter = parseInt(counterRows[0]?.value ?? "0") + 1;
157
+ identifier = `BOS-${String(counter).padStart(3, "0")}`;
158
+ if (counterRows[0]) {
159
+ await db.update(tenantSettings).set({ value: String(counter) }).where(eq(tenantSettings.id, counterRows[0].id));
160
+ }
161
+ else {
162
+ const { tenantSettings: ts } = await import("@boringos/db");
163
+ await db.insert(ts).values({ id: generateId(), tenantId, key: "task_counter", value: String(counter) });
164
+ }
165
+ }
166
+ }
167
+ await db.insert(tasks).values({
168
+ id,
169
+ tenantId,
170
+ title: body.title,
171
+ description: body.description,
172
+ status: body.status ?? "todo",
173
+ priority: body.priority ?? "medium",
174
+ assigneeAgentId: body.assigneeAgentId,
175
+ parentId: body.parentId,
176
+ identifier,
177
+ originKind: "manual",
178
+ });
179
+ const rows = await db.select().from(tasks).where(eq(tasks.id, id)).limit(1);
180
+ emit("task:created", c.get("tenantId"), { taskId: id, title: body.title });
181
+ await logActivity(c.get("tenantId"), "task.created", "task", id, { title: body.title });
182
+ return c.json(rows[0], 201);
183
+ });
184
+ app.patch("/tasks/:id", async (c) => {
185
+ const body = await c.req.json();
186
+ const values = { updatedAt: new Date() };
187
+ if (body.title !== undefined)
188
+ values.title = body.title;
189
+ if (body.description !== undefined)
190
+ values.description = body.description;
191
+ if (body.status !== undefined)
192
+ values.status = body.status;
193
+ if (body.priority !== undefined)
194
+ values.priority = body.priority;
195
+ if (body.assigneeAgentId !== undefined)
196
+ values.assigneeAgentId = body.assigneeAgentId;
197
+ await db.update(tasks).set(values).where(and(eq(tasks.id, c.req.param("id")), eq(tasks.tenantId, c.get("tenantId"))));
198
+ const rows = await db.select().from(tasks).where(eq(tasks.id, c.req.param("id"))).limit(1);
199
+ return c.json(rows[0]);
200
+ });
201
+ app.delete("/tasks/:id", async (c) => {
202
+ await db.delete(tasks).where(and(eq(tasks.id, c.req.param("id")), eq(tasks.tenantId, c.get("tenantId"))));
203
+ return c.json({ ok: true });
204
+ });
205
+ app.post("/tasks/:id/comments", async (c) => {
206
+ const body = await c.req.json();
207
+ const id = generateId();
208
+ await db.insert(taskComments).values({
209
+ id,
210
+ taskId: c.req.param("id"),
211
+ tenantId: c.get("tenantId"),
212
+ body: body.body,
213
+ authorUserId: body.authorUserId,
214
+ });
215
+ emit("task:comment_added", c.get("tenantId"), { taskId: c.req.param("id"), commentId: id });
216
+ await logActivity(c.get("tenantId"), "comment.created", "task_comment", id, { taskId: c.req.param("id") });
217
+ return c.json({ id }, 201);
218
+ });
219
+ app.post("/tasks/:id/assign", async (c) => {
220
+ const body = await c.req.json();
221
+ const agentId = body.agentId;
222
+ const taskId = c.req.param("id");
223
+ await db.update(tasks).set({
224
+ assigneeAgentId: agentId,
225
+ updatedAt: new Date(),
226
+ }).where(eq(tasks.id, taskId));
227
+ // Optionally wake the agent
228
+ if (body.wake) {
229
+ const outcome = await engine.wake({
230
+ agentId,
231
+ tenantId: c.get("tenantId"),
232
+ reason: "manual_request",
233
+ taskId,
234
+ });
235
+ if (outcome.kind === "created") {
236
+ await engine.enqueue(outcome.wakeupRequestId);
237
+ }
238
+ return c.json({ assigned: true, wakeup: outcome });
239
+ }
240
+ return c.json({ assigned: true });
241
+ });
242
+ // ── Runs ────────────────────────────────────────────────────────────────
243
+ app.get("/runs", async (c) => {
244
+ const agentId = c.req.query("agentId");
245
+ const status = c.req.query("status");
246
+ const rows = await db.select().from(agentRuns)
247
+ .where(eq(agentRuns.tenantId, c.get("tenantId")))
248
+ .orderBy(desc(agentRuns.createdAt))
249
+ .limit(100);
250
+ let filtered = rows;
251
+ if (agentId)
252
+ filtered = filtered.filter((r) => r.agentId === agentId);
253
+ if (status)
254
+ filtered = filtered.filter((r) => r.status === status);
255
+ return c.json({ runs: filtered });
256
+ });
257
+ app.get("/runs/:id", async (c) => {
258
+ const rows = await db.select().from(agentRuns).where(and(eq(agentRuns.id, c.req.param("id")), eq(agentRuns.tenantId, c.get("tenantId")))).limit(1);
259
+ if (!rows[0])
260
+ return c.json({ error: "Run not found" }, 404);
261
+ return c.json(rows[0]);
262
+ });
263
+ app.post("/runs/:id/cancel", async (c) => {
264
+ await engine.cancel(c.req.param("id"));
265
+ return c.json({ ok: true });
266
+ });
267
+ // ── Runtimes ────────────────────────────────────────────────────────────
268
+ app.get("/runtimes", async (c) => {
269
+ const rows = await db.select().from(runtimes).where(eq(runtimes.tenantId, c.get("tenantId")));
270
+ return c.json({ runtimes: rows });
271
+ });
272
+ app.post("/runtimes", async (c) => {
273
+ const body = await c.req.json();
274
+ const id = generateId();
275
+ await db.insert(runtimes).values({
276
+ id,
277
+ tenantId: c.get("tenantId"),
278
+ name: body.name,
279
+ type: body.type,
280
+ config: body.config ?? {},
281
+ model: body.model,
282
+ });
283
+ const rows = await db.select().from(runtimes).where(eq(runtimes.id, id)).limit(1);
284
+ return c.json(rows[0], 201);
285
+ });
286
+ app.patch("/runtimes/:id", async (c) => {
287
+ const body = await c.req.json();
288
+ const values = { updatedAt: new Date() };
289
+ if (body.name !== undefined)
290
+ values.name = body.name;
291
+ if (body.config !== undefined)
292
+ values.config = body.config;
293
+ if (body.model !== undefined)
294
+ values.model = body.model;
295
+ await db.update(runtimes).set(values).where(and(eq(runtimes.id, c.req.param("id")), eq(runtimes.tenantId, c.get("tenantId"))));
296
+ const rows = await db.select().from(runtimes).where(eq(runtimes.id, c.req.param("id"))).limit(1);
297
+ return c.json(rows[0]);
298
+ });
299
+ app.delete("/runtimes/:id", async (c) => {
300
+ await db.delete(runtimes).where(and(eq(runtimes.id, c.req.param("id")), eq(runtimes.tenantId, c.get("tenantId"))));
301
+ return c.json({ ok: true });
302
+ });
303
+ app.post("/runtimes/:id/default", async (c) => {
304
+ const tenantId = c.get("tenantId");
305
+ // Unset all defaults first
306
+ await db.update(runtimes).set({ isDefault: false }).where(eq(runtimes.tenantId, tenantId));
307
+ // Set this one as default
308
+ await db.update(runtimes).set({ isDefault: true, updatedAt: new Date() }).where(and(eq(runtimes.id, c.req.param("id")), eq(runtimes.tenantId, tenantId)));
309
+ return c.json({ ok: true });
310
+ });
311
+ // ── Approvals ───────────────────────────────────────────────────────────
312
+ app.get("/approvals", async (c) => {
313
+ const status = c.req.query("status") ?? "pending";
314
+ const rows = await db.select().from(approvals)
315
+ .where(and(eq(approvals.tenantId, c.get("tenantId")), eq(approvals.status, status)))
316
+ .orderBy(desc(approvals.createdAt));
317
+ return c.json({ approvals: rows });
318
+ });
319
+ app.get("/approvals/:id", async (c) => {
320
+ const rows = await db.select().from(approvals).where(and(eq(approvals.id, c.req.param("id")), eq(approvals.tenantId, c.get("tenantId")))).limit(1);
321
+ if (!rows[0])
322
+ return c.json({ error: "Approval not found" }, 404);
323
+ return c.json(rows[0]);
324
+ });
325
+ app.post("/approvals/:id/approve", async (c) => {
326
+ const body = (await c.req.json().catch(() => ({})));
327
+ await db.update(approvals).set({
328
+ status: "approved",
329
+ decisionNote: body.note,
330
+ decidedAt: new Date(),
331
+ updatedAt: new Date(),
332
+ }).where(eq(approvals.id, c.req.param("id")));
333
+ emit("approval:decided", c.get("tenantId"), { approvalId: c.req.param("id"), status: "approved" });
334
+ await logActivity(c.get("tenantId"), "approval.approved", "approval", c.req.param("id"));
335
+ return c.json({ ok: true });
336
+ });
337
+ app.post("/approvals/:id/reject", async (c) => {
338
+ const body = (await c.req.json().catch(() => ({})));
339
+ await db.update(approvals).set({
340
+ status: "rejected",
341
+ decisionNote: body.reason,
342
+ decidedAt: new Date(),
343
+ updatedAt: new Date(),
344
+ }).where(eq(approvals.id, c.req.param("id")));
345
+ emit("approval:decided", c.get("tenantId"), { approvalId: c.req.param("id"), status: "rejected" });
346
+ await logActivity(c.get("tenantId"), "approval.rejected", "approval", c.req.param("id"));
347
+ return c.json({ ok: true });
348
+ });
349
+ // ── Activity Log ────────────────────────────────────────────────────────
350
+ app.get("/activity", async (c) => {
351
+ const rows = await db.select().from(activityLog)
352
+ .where(eq(activityLog.tenantId, c.get("tenantId")))
353
+ .orderBy(desc(activityLog.createdAt))
354
+ .limit(100);
355
+ return c.json({ activity: rows });
356
+ });
357
+ // ── Projects ─────────────────────────────────────────────────────────────
358
+ app.get("/projects", async (c) => {
359
+ const rows = await db.select().from(projects).where(eq(projects.tenantId, c.get("tenantId")));
360
+ return c.json({ projects: rows });
361
+ });
362
+ app.get("/projects/:id", async (c) => {
363
+ const rows = await db.select().from(projects).where(and(eq(projects.id, c.req.param("id")), eq(projects.tenantId, c.get("tenantId")))).limit(1);
364
+ if (!rows[0])
365
+ return c.json({ error: "Project not found" }, 404);
366
+ return c.json(rows[0]);
367
+ });
368
+ app.post("/projects", async (c) => {
369
+ const body = await c.req.json();
370
+ const id = generateId();
371
+ await db.insert(projects).values({
372
+ id,
373
+ tenantId: c.get("tenantId"),
374
+ name: body.name,
375
+ description: body.description,
376
+ prefix: body.prefix,
377
+ repoUrl: body.repoUrl,
378
+ defaultBranch: body.defaultBranch,
379
+ branchTemplate: body.branchTemplate,
380
+ });
381
+ const rows = await db.select().from(projects).where(eq(projects.id, id)).limit(1);
382
+ emit("task:created", c.get("tenantId"), { projectId: id, name: body.name });
383
+ return c.json(rows[0], 201);
384
+ });
385
+ app.patch("/projects/:id", async (c) => {
386
+ const body = await c.req.json();
387
+ const values = { updatedAt: new Date() };
388
+ if (body.name !== undefined)
389
+ values.name = body.name;
390
+ if (body.description !== undefined)
391
+ values.description = body.description;
392
+ if (body.status !== undefined)
393
+ values.status = body.status;
394
+ if (body.repoUrl !== undefined)
395
+ values.repoUrl = body.repoUrl;
396
+ await db.update(projects).set(values).where(and(eq(projects.id, c.req.param("id")), eq(projects.tenantId, c.get("tenantId"))));
397
+ const rows = await db.select().from(projects).where(eq(projects.id, c.req.param("id"))).limit(1);
398
+ return c.json(rows[0]);
399
+ });
400
+ // ── Goals ───────────────────────────────────────────────────────────────
401
+ app.get("/goals", async (c) => {
402
+ const rows = await db.select().from(goals).where(eq(goals.tenantId, c.get("tenantId")));
403
+ return c.json({ goals: rows });
404
+ });
405
+ app.post("/goals", async (c) => {
406
+ const body = await c.req.json();
407
+ const id = generateId();
408
+ await db.insert(goals).values({
409
+ id,
410
+ tenantId: c.get("tenantId"),
411
+ title: body.title,
412
+ description: body.description,
413
+ });
414
+ const rows = await db.select().from(goals).where(eq(goals.id, id)).limit(1);
415
+ return c.json(rows[0], 201);
416
+ });
417
+ app.patch("/goals/:id", async (c) => {
418
+ const body = await c.req.json();
419
+ const values = { updatedAt: new Date() };
420
+ if (body.title !== undefined)
421
+ values.title = body.title;
422
+ if (body.description !== undefined)
423
+ values.description = body.description;
424
+ if (body.status !== undefined)
425
+ values.status = body.status;
426
+ await db.update(goals).set(values).where(and(eq(goals.id, c.req.param("id")), eq(goals.tenantId, c.get("tenantId"))));
427
+ const rows = await db.select().from(goals).where(eq(goals.id, c.req.param("id"))).limit(1);
428
+ return c.json(rows[0]);
429
+ });
430
+ // ── Labels ──────────────────────────────────────────────────────────────
431
+ app.get("/labels", async (c) => {
432
+ const rows = await db.select().from(labels).where(eq(labels.tenantId, c.get("tenantId")));
433
+ return c.json({ labels: rows });
434
+ });
435
+ app.post("/labels", async (c) => {
436
+ const body = await c.req.json();
437
+ const id = generateId();
438
+ await db.insert(labels).values({
439
+ id,
440
+ tenantId: c.get("tenantId"),
441
+ name: body.name,
442
+ color: body.color,
443
+ });
444
+ const rows = await db.select().from(labels).where(eq(labels.id, id)).limit(1);
445
+ return c.json(rows[0], 201);
446
+ });
447
+ app.post("/tasks/:taskId/labels/:labelId", async (c) => {
448
+ const id = generateId();
449
+ await db.insert(taskLabels).values({
450
+ id,
451
+ taskId: c.req.param("taskId"),
452
+ labelId: c.req.param("labelId"),
453
+ });
454
+ return c.json({ ok: true }, 201);
455
+ });
456
+ app.delete("/tasks/:taskId/labels/:labelId", async (c) => {
457
+ await db.delete(taskLabels).where(and(eq(taskLabels.taskId, c.req.param("taskId")), eq(taskLabels.labelId, c.req.param("labelId"))));
458
+ return c.json({ ok: true });
459
+ });
460
+ // ── Task Read States ────────────────────────────────────────────────────
461
+ app.post("/tasks/:taskId/read", async (c) => {
462
+ const body = (await c.req.json().catch(() => ({})));
463
+ const userId = body.userId ?? "unknown";
464
+ const id = generateId();
465
+ await db.insert(taskReadStates).values({
466
+ id,
467
+ taskId: c.req.param("taskId"),
468
+ userId,
469
+ });
470
+ return c.json({ ok: true });
471
+ });
472
+ // ── Skills ───────────────────────────────────────────────────────────────
473
+ app.get("/skills", async (c) => {
474
+ const rows = await db.select().from(companySkills).where(eq(companySkills.tenantId, c.get("tenantId")));
475
+ return c.json({ skills: rows });
476
+ });
477
+ app.post("/skills", async (c) => {
478
+ const body = await c.req.json();
479
+ const id = generateId();
480
+ await db.insert(companySkills).values({
481
+ id,
482
+ tenantId: c.get("tenantId"),
483
+ key: body.key,
484
+ name: body.name,
485
+ description: body.description,
486
+ sourceType: body.sourceType,
487
+ sourceConfig: body.sourceConfig ?? {},
488
+ trustLevel: body.trustLevel ?? "markdown_only",
489
+ });
490
+ const rows = await db.select().from(companySkills).where(eq(companySkills.id, id)).limit(1);
491
+ return c.json(rows[0], 201);
492
+ });
493
+ app.post("/skills/:id/attach/:agentId", async (c) => {
494
+ const id = generateId();
495
+ await db.insert(agentSkills).values({
496
+ id,
497
+ skillId: c.req.param("id"),
498
+ agentId: c.req.param("agentId"),
499
+ });
500
+ return c.json({ ok: true }, 201);
501
+ });
502
+ app.delete("/skills/:id/attach/:agentId", async (c) => {
503
+ await db.delete(agentSkills).where(and(eq(agentSkills.skillId, c.req.param("id")), eq(agentSkills.agentId, c.req.param("agentId"))));
504
+ return c.json({ ok: true });
505
+ });
506
+ // ── Routines ─────────────────────────────────────────────────────────────
507
+ app.get("/routines", async (c) => {
508
+ const rows = await db.select().from(routines).where(eq(routines.tenantId, c.get("tenantId")));
509
+ return c.json({ routines: rows });
510
+ });
511
+ app.post("/routines", async (c) => {
512
+ const body = await c.req.json();
513
+ const id = generateId();
514
+ await db.insert(routines).values({
515
+ id,
516
+ tenantId: c.get("tenantId"),
517
+ title: body.title,
518
+ description: body.description,
519
+ assigneeAgentId: body.assigneeAgentId,
520
+ cronExpression: body.cronExpression,
521
+ timezone: body.timezone ?? "UTC",
522
+ concurrencyPolicy: body.concurrencyPolicy ?? "skip_if_active",
523
+ });
524
+ const rows = await db.select().from(routines).where(eq(routines.id, id)).limit(1);
525
+ return c.json(rows[0], 201);
526
+ });
527
+ app.patch("/routines/:id", async (c) => {
528
+ const body = await c.req.json();
529
+ const values = { updatedAt: new Date() };
530
+ if (body.title !== undefined)
531
+ values.title = body.title;
532
+ if (body.cronExpression !== undefined)
533
+ values.cronExpression = body.cronExpression;
534
+ if (body.status !== undefined)
535
+ values.status = body.status;
536
+ if (body.concurrencyPolicy !== undefined)
537
+ values.concurrencyPolicy = body.concurrencyPolicy;
538
+ await db.update(routines).set(values).where(and(eq(routines.id, c.req.param("id")), eq(routines.tenantId, c.get("tenantId"))));
539
+ const rows = await db.select().from(routines).where(eq(routines.id, c.req.param("id"))).limit(1);
540
+ return c.json(rows[0]);
541
+ });
542
+ app.delete("/routines/:id", async (c) => {
543
+ await db.delete(routines).where(and(eq(routines.id, c.req.param("id")), eq(routines.tenantId, c.get("tenantId"))));
544
+ return c.json({ ok: true });
545
+ });
546
+ app.post("/routines/:id/trigger", async (c) => {
547
+ const rows = await db.select().from(routines).where(and(eq(routines.id, c.req.param("id")), eq(routines.tenantId, c.get("tenantId")))).limit(1);
548
+ const routine = rows[0];
549
+ if (!routine)
550
+ return c.json({ error: "Routine not found" }, 404);
551
+ const outcome = await engine.wake({
552
+ agentId: routine.assigneeAgentId,
553
+ tenantId: c.get("tenantId"),
554
+ reason: "routine_triggered",
555
+ });
556
+ if (outcome.kind === "created") {
557
+ await engine.enqueue(outcome.wakeupRequestId);
558
+ }
559
+ return c.json(outcome);
560
+ });
561
+ // ── Budgets ──────────────────────────────────────────────────────────────
562
+ app.get("/budgets", async (c) => {
563
+ const rows = await db.select().from(budgetPolicies)
564
+ .where(eq(budgetPolicies.tenantId, c.get("tenantId")));
565
+ return c.json({ policies: rows });
566
+ });
567
+ app.post("/budgets", async (c) => {
568
+ const body = await c.req.json();
569
+ const id = generateId();
570
+ await db.insert(budgetPolicies).values({
571
+ id,
572
+ tenantId: c.get("tenantId"),
573
+ agentId: body.agentId,
574
+ scope: body.scope ?? "tenant",
575
+ period: body.period ?? "monthly",
576
+ limitCents: body.limitCents,
577
+ warnThresholdPct: body.warnThresholdPct ?? 80,
578
+ });
579
+ const rows = await db.select().from(budgetPolicies).where(eq(budgetPolicies.id, id)).limit(1);
580
+ return c.json(rows[0], 201);
581
+ });
582
+ app.delete("/budgets/:id", async (c) => {
583
+ await db.delete(budgetPolicies).where(and(eq(budgetPolicies.id, c.req.param("id")), eq(budgetPolicies.tenantId, c.get("tenantId"))));
584
+ return c.json({ ok: true });
585
+ });
586
+ app.get("/budgets/incidents", async (c) => {
587
+ const rows = await db.select().from(budgetIncidents)
588
+ .where(eq(budgetIncidents.tenantId, c.get("tenantId")))
589
+ .orderBy(desc(budgetIncidents.createdAt))
590
+ .limit(50);
591
+ return c.json({ incidents: rows });
592
+ });
593
+ // ── Tenants ─────────────────────────────────────────────────────────────
594
+ app.get("/tenants/current", async (c) => {
595
+ const rows = await db.select().from(tenants).where(eq(tenants.id, c.get("tenantId"))).limit(1);
596
+ if (!rows[0])
597
+ return c.json({ error: "Tenant not found" }, 404);
598
+ return c.json(rows[0]);
599
+ });
600
+ app.post("/tenants", async (c) => {
601
+ const body = await c.req.json();
602
+ const id = generateId();
603
+ await db.insert(tenants).values({
604
+ id,
605
+ name: body.name,
606
+ slug: body.slug,
607
+ });
608
+ const rows = await db.select().from(tenants).where(eq(tenants.id, id)).limit(1);
609
+ return c.json(rows[0], 201);
610
+ });
611
+ // ── Drive ───────────────────────────────────────────────────────────────
612
+ app.get("/drive/list", async (c) => {
613
+ const prefix = c.req.query("path");
614
+ const rows = await db.select().from(driveFiles).where(eq(driveFiles.tenantId, c.get("tenantId")));
615
+ let filtered = rows;
616
+ if (prefix)
617
+ filtered = rows.filter((r) => r.path.startsWith(prefix));
618
+ return c.json({ files: filtered });
619
+ });
620
+ app.get("/drive/skill", async (c) => {
621
+ // Read from most recent revision or return null
622
+ const rows = await db.select().from(driveSkillRevisions)
623
+ .where(eq(driveSkillRevisions.tenantId, c.get("tenantId")))
624
+ .orderBy(desc(driveSkillRevisions.createdAt))
625
+ .limit(1);
626
+ return c.json({ skill: rows[0]?.content ?? null });
627
+ });
628
+ app.patch("/drive/skill", async (c) => {
629
+ const body = await c.req.json();
630
+ await db.insert(driveSkillRevisions).values({
631
+ id: generateId(),
632
+ tenantId: c.get("tenantId"),
633
+ content: body.content,
634
+ changedBy: body.changedBy ?? null,
635
+ });
636
+ return c.json({ ok: true });
637
+ });
638
+ app.get("/drive/skill/revisions", async (c) => {
639
+ const rows = await db.select().from(driveSkillRevisions)
640
+ .where(eq(driveSkillRevisions.tenantId, c.get("tenantId")))
641
+ .orderBy(desc(driveSkillRevisions.createdAt))
642
+ .limit(20);
643
+ return c.json({ revisions: rows });
644
+ });
645
+ // ── Onboarding ──────────────────────────────────────────────────────────
646
+ app.get("/onboarding", async (c) => {
647
+ const rows = await db.select().from(onboardingState)
648
+ .where(eq(onboardingState.tenantId, c.get("tenantId")))
649
+ .limit(1);
650
+ if (!rows[0]) {
651
+ // Auto-create onboarding state
652
+ const id = generateId();
653
+ await db.insert(onboardingState).values({ id, tenantId: c.get("tenantId") });
654
+ return c.json({ currentStep: 1, totalSteps: 5, completedSteps: [], completed: false });
655
+ }
656
+ return c.json({
657
+ currentStep: rows[0].currentStep,
658
+ totalSteps: rows[0].totalSteps,
659
+ completedSteps: rows[0].completedSteps,
660
+ completed: !!rows[0].completedAt,
661
+ metadata: rows[0].metadata,
662
+ });
663
+ });
664
+ app.post("/onboarding/complete-step", async (c) => {
665
+ const body = await c.req.json();
666
+ const tenantId = c.get("tenantId");
667
+ const rows = await db.select().from(onboardingState)
668
+ .where(eq(onboardingState.tenantId, tenantId)).limit(1);
669
+ if (!rows[0]) {
670
+ return c.json({ error: "Onboarding not started" }, 404);
671
+ }
672
+ const completed = [...rows[0].completedSteps];
673
+ if (!completed.includes(body.step))
674
+ completed.push(body.step);
675
+ const nextStep = body.step + 1;
676
+ const isComplete = completed.length >= rows[0].totalSteps;
677
+ const updates = {
678
+ currentStep: isComplete ? rows[0].totalSteps : nextStep,
679
+ completedSteps: completed,
680
+ updatedAt: new Date(),
681
+ };
682
+ if (body.metadata) {
683
+ const existing = rows[0].metadata;
684
+ updates.metadata = { ...existing, [`step${body.step}`]: body.metadata };
685
+ }
686
+ if (isComplete)
687
+ updates.completedAt = new Date();
688
+ await db.update(onboardingState).set(updates).where(eq(onboardingState.id, rows[0].id));
689
+ return c.json({ step: body.step, completed: isComplete, nextStep: isComplete ? null : nextStep });
690
+ });
691
+ // ── Evals ───────────────────────────────────────────────────────────────
692
+ app.get("/evals", async (c) => {
693
+ const rows = await db.select().from(evals).where(eq(evals.tenantId, c.get("tenantId")));
694
+ return c.json({ evals: rows });
695
+ });
696
+ app.post("/evals", async (c) => {
697
+ const body = await c.req.json();
698
+ const id = generateId();
699
+ await db.insert(evals).values({
700
+ id,
701
+ tenantId: c.get("tenantId"),
702
+ name: body.name,
703
+ description: body.description,
704
+ testCases: body.testCases ?? [],
705
+ });
706
+ const rows = await db.select().from(evals).where(eq(evals.id, id)).limit(1);
707
+ return c.json(rows[0], 201);
708
+ });
709
+ app.get("/evals/:id/runs", async (c) => {
710
+ const rows = await db.select().from(evalRuns)
711
+ .where(and(eq(evalRuns.evalId, c.req.param("id")), eq(evalRuns.tenantId, c.get("tenantId"))))
712
+ .orderBy(desc(evalRuns.startedAt));
713
+ return c.json({ runs: rows });
714
+ });
715
+ app.post("/evals/:id/run", async (c) => {
716
+ const body = await c.req.json();
717
+ const evalRows = await db.select().from(evals).where(eq(evals.id, c.req.param("id"))).limit(1);
718
+ if (!evalRows[0])
719
+ return c.json({ error: "Eval not found" }, 404);
720
+ const id = generateId();
721
+ const testCases = evalRows[0].testCases;
722
+ await db.insert(evalRuns).values({
723
+ id,
724
+ tenantId: c.get("tenantId"),
725
+ evalId: c.req.param("id"),
726
+ agentId: body.agentId,
727
+ totalCases: testCases.length,
728
+ status: "pending",
729
+ });
730
+ return c.json({ runId: id, totalCases: testCases.length }, 201);
731
+ });
732
+ // ── Inbox ───────────────────────────────────────────────────────────────
733
+ app.get("/inbox", async (c) => {
734
+ const status = c.req.query("status") ?? "unread";
735
+ const rows = await db.select().from(inboxItems)
736
+ .where(and(eq(inboxItems.tenantId, c.get("tenantId")), eq(inboxItems.status, status)))
737
+ .orderBy(desc(inboxItems.createdAt))
738
+ .limit(100);
739
+ return c.json({ items: rows });
740
+ });
741
+ app.get("/inbox/:id", async (c) => {
742
+ const rows = await db.select().from(inboxItems).where(and(eq(inboxItems.id, c.req.param("id")), eq(inboxItems.tenantId, c.get("tenantId")))).limit(1);
743
+ if (!rows[0])
744
+ return c.json({ error: "Inbox item not found" }, 404);
745
+ // Mark as read
746
+ if (rows[0].status === "unread") {
747
+ await db.update(inboxItems).set({ status: "read", updatedAt: new Date() }).where(eq(inboxItems.id, rows[0].id));
748
+ }
749
+ return c.json(rows[0]);
750
+ });
751
+ app.post("/inbox/:id/archive", async (c) => {
752
+ await db.update(inboxItems).set({
753
+ status: "archived",
754
+ archivedAt: new Date(),
755
+ updatedAt: new Date(),
756
+ }).where(and(eq(inboxItems.id, c.req.param("id")), eq(inboxItems.tenantId, c.get("tenantId"))));
757
+ return c.json({ ok: true });
758
+ });
759
+ app.post("/inbox/:id/create-task", async (c) => {
760
+ const itemRows = await db.select().from(inboxItems).where(eq(inboxItems.id, c.req.param("id"))).limit(1);
761
+ if (!itemRows[0])
762
+ return c.json({ error: "Inbox item not found" }, 404);
763
+ const item = itemRows[0];
764
+ const taskId = generateId();
765
+ await db.insert(tasks).values({
766
+ id: taskId,
767
+ tenantId: c.get("tenantId"),
768
+ title: item.subject,
769
+ description: item.body ?? undefined,
770
+ status: "todo",
771
+ priority: "medium",
772
+ originKind: "inbox",
773
+ originId: item.id,
774
+ });
775
+ await db.update(inboxItems).set({ linkedTaskId: taskId, updatedAt: new Date() }).where(eq(inboxItems.id, item.id));
776
+ return c.json({ taskId }, 201);
777
+ });
778
+ // ── Costs ───────────────────────────────────────────────────────────────
779
+ app.get("/costs", async (c) => {
780
+ const rows = await db.select().from(costEvents)
781
+ .where(eq(costEvents.tenantId, c.get("tenantId")))
782
+ .orderBy(desc(costEvents.createdAt))
783
+ .limit(100);
784
+ return c.json({ costs: rows });
785
+ });
786
+ // ── Entity References ────────────────────────────────────────────────────
787
+ app.post("/entities/link", async (c) => {
788
+ const body = await c.req.json();
789
+ const id = generateId();
790
+ await db.insert(entityReferences).values({
791
+ id,
792
+ tenantId: c.get("tenantId"),
793
+ entityType: body.entityType,
794
+ entityId: body.entityId,
795
+ refType: body.refType,
796
+ refId: body.refId,
797
+ });
798
+ return c.json({ id }, 201);
799
+ });
800
+ app.get("/entities/:type/:id/refs", async (c) => {
801
+ const rows = await db.select().from(entityReferences).where(and(eq(entityReferences.tenantId, c.get("tenantId")), eq(entityReferences.entityType, c.req.param("type")), eq(entityReferences.entityId, c.req.param("id"))));
802
+ // Group by refType
803
+ const grouped = {};
804
+ for (const row of rows) {
805
+ if (!grouped[row.refType])
806
+ grouped[row.refType] = [];
807
+ grouped[row.refType].push(row.refId);
808
+ }
809
+ return c.json({ entityType: c.req.param("type"), entityId: c.req.param("id"), refs: grouped });
810
+ });
811
+ app.delete("/entities/link/:id", async (c) => {
812
+ await db.delete(entityReferences).where(and(eq(entityReferences.id, c.req.param("id")), eq(entityReferences.tenantId, c.get("tenantId"))));
813
+ return c.json({ ok: true });
814
+ });
815
+ // ── Search ──────────────────────────────────────────────────────────────
816
+ app.get("/search", async (c) => {
817
+ const q = c.req.query("q");
818
+ if (!q)
819
+ return c.json({ error: "Missing q parameter" }, 400);
820
+ const tenantId = c.get("tenantId");
821
+ const pattern = `%${q}%`;
822
+ // Search across multiple tables in parallel
823
+ const [taskResults, agentResults, inboxResults] = await Promise.all([
824
+ db.execute(sql `SELECT id, title, status, identifier FROM tasks WHERE tenant_id = ${tenantId} AND (title ILIKE ${pattern} OR description ILIKE ${pattern}) LIMIT 20`),
825
+ db.execute(sql `SELECT id, name, role, status FROM agents WHERE tenant_id = ${tenantId} AND (name ILIKE ${pattern} OR role ILIKE ${pattern}) LIMIT 20`),
826
+ db.execute(sql `SELECT id, subject, source, status FROM inbox_items WHERE tenant_id = ${tenantId} AND (subject ILIKE ${pattern} OR body ILIKE ${pattern}) LIMIT 20`),
827
+ ]);
828
+ return c.json({
829
+ tasks: taskResults,
830
+ agents: agentResults,
831
+ inboxItems: inboxResults,
832
+ });
833
+ });
834
+ return app;
835
+ }
836
+ //# sourceMappingURL=admin-routes.js.map