update
This commit is contained in:
1 parent
66158e222e
commit
67c757f481
314 files changed
+37945
-22962
No files matched your search
@@ -0,0 +1,309 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type {
|
||||
InboxItem,
|
||||
ProfileSnapshot,
|
||||
} from "@shared/types";
|
||||
import {
|
||||
type ReadAll,
|
||||
type Transaction,
|
||||
nowIso,
|
||||
} from "./helpers";
|
||||
|
||||
export function buildCompanyCrudMethods(
|
||||
getDb: () => DatabaseSync,
|
||||
readAll: ReadAll,
|
||||
transaction: Transaction,
|
||||
deps: {
|
||||
getSetting: (key: string) => string | null;
|
||||
setSetting: (key: string, value: string) => void;
|
||||
listSnapshot: (options: { companyId?: string | null }) => ProfileSnapshot;
|
||||
addComment: (input: { companyId: string; taskId: string; authorAgentId: string | null; authorName: string; body: string }) => void;
|
||||
},
|
||||
) {
|
||||
return {
|
||||
saveCompany(input: {
|
||||
id?: string | null;
|
||||
name: string;
|
||||
description: string;
|
||||
status: string;
|
||||
autoApproveHires?: boolean;
|
||||
reviewDeliverables?: boolean;
|
||||
executionMode?: string;
|
||||
}): string {
|
||||
const id = input.id && input.id.length > 0 ? input.id : randomUUID();
|
||||
const ts = nowIso();
|
||||
const autoApprove = input.autoApproveHires ? 1 : 0;
|
||||
const reviewDeliverables = input.reviewDeliverables ? 1 : 0;
|
||||
const executionMode = input.executionMode ?? "ai_ceo";
|
||||
const existing = getDb()
|
||||
.prepare("select id from companies where id = ?")
|
||||
.get(id) as { id: string } | undefined;
|
||||
if (existing) {
|
||||
getDb()
|
||||
.prepare(
|
||||
"update companies set name = ?, description = ?, status = ?, auto_approve_hires = ?, review_deliverables = ?, execution_mode = ?, updated_at = ? where id = ?",
|
||||
)
|
||||
.run(input.name, input.description, input.status, autoApprove, reviewDeliverables, executionMode, ts, id);
|
||||
} else {
|
||||
getDb()
|
||||
.prepare(
|
||||
"insert into companies (id, name, description, status, auto_approve_hires, review_deliverables, execution_mode, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.run(id, input.name, input.description, input.status, autoApprove, reviewDeliverables, executionMode, ts, ts);
|
||||
if (!deps.getSetting("current_company_id")) {
|
||||
deps.setSetting("current_company_id", id);
|
||||
}
|
||||
}
|
||||
return id;
|
||||
},
|
||||
|
||||
deleteCompany(companyId: string): void {
|
||||
transaction(() => {
|
||||
try {
|
||||
getDb()
|
||||
.prepare(
|
||||
"delete from agent_message_reads where message_id in (select id from agent_messages where company_id = ?)",
|
||||
)
|
||||
.run(companyId);
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error) || !err.message.includes("no such table")) throw err;
|
||||
}
|
||||
// New table cleanup (budget_incidents before budget_policies due to FK)
|
||||
getDb().prepare("delete from budget_incidents where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from budget_policies where company_id = ?").run(companyId);
|
||||
// agent_skill_assignments references company_skills -- delete before skills
|
||||
try {
|
||||
getDb()
|
||||
.prepare(
|
||||
"delete from agent_skill_assignments where skill_id in (select id from company_skills where company_id = ?)",
|
||||
)
|
||||
.run(companyId);
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error) || !err.message.includes("no such table")) throw err;
|
||||
}
|
||||
getDb().prepare("delete from company_skills where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from task_sessions where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from wakeup_requests where company_id = ?").run(companyId);
|
||||
// Thread runtime tables (events + participants cascade from threads via FK)
|
||||
getDb().prepare("delete from thread_wait_states where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from conversation_threads where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from cost_entries where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from activity where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from task_comments where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from runs where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from approvals where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from tasks where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from projects where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from goals where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from agents where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from secrets where company_id = ?").run(companyId);
|
||||
getDb().prepare("delete from workspaces where company_id = ?").run(companyId);
|
||||
for (const table of [
|
||||
"automation_rules",
|
||||
"automation_log",
|
||||
"agent_messages",
|
||||
"workflow_pipelines",
|
||||
"meetings",
|
||||
"documents",
|
||||
"knowledge_base",
|
||||
"sprints",
|
||||
"browser_actions",
|
||||
"social_accounts",
|
||||
]) {
|
||||
try {
|
||||
getDb().prepare(`delete from ${table} where company_id = ?`).run(companyId);
|
||||
} catch (err) {
|
||||
// Only tolerate "no such table" — re-throw anything else (disk error, etc.)
|
||||
if (!(err instanceof Error) || !err.message.includes("no such table")) throw err;
|
||||
}
|
||||
}
|
||||
getDb().prepare("delete from companies where id = ?").run(companyId);
|
||||
if (deps.getSetting("current_company_id") === companyId) {
|
||||
const next = getDb()
|
||||
.prepare("select id from companies order by created_at asc limit 1")
|
||||
.get() as { id: string } | undefined;
|
||||
if (next) {
|
||||
deps.setSetting("current_company_id", next.id);
|
||||
} else {
|
||||
deps.setSetting("current_company_id", "");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getCompanyStatus(companyId: string): string | null {
|
||||
const row = getDb()
|
||||
.prepare("select status from companies where id = ?")
|
||||
.get(companyId) as { status: string } | undefined;
|
||||
return row?.status ?? null;
|
||||
},
|
||||
|
||||
getCompanyBasic(companyId: string): { name: string; description: string } | null {
|
||||
const row = getDb()
|
||||
.prepare("select name, description from companies where id = ?")
|
||||
.get(companyId) as { name: string; description: string } | undefined;
|
||||
return row ?? null;
|
||||
},
|
||||
|
||||
listActiveCompanyIds(): Array<{ id: string; name: string }> {
|
||||
return readAll<{ id: string; name: string }>(
|
||||
"select id, name from companies where status = 'active'",
|
||||
);
|
||||
},
|
||||
|
||||
getCompanyExecutionMode(companyId: string): string {
|
||||
const row = getDb()
|
||||
.prepare("SELECT execution_mode FROM companies WHERE id = ? LIMIT 1")
|
||||
.get(companyId) as { execution_mode: string } | undefined;
|
||||
return row?.execution_mode ?? "ai_ceo";
|
||||
},
|
||||
|
||||
getCompanyApprovalSettings(companyId: string): { autoApproveHires: boolean; reviewDeliverables: boolean } | null {
|
||||
const row = getDb()
|
||||
.prepare("SELECT auto_approve_hires, review_deliverables FROM companies WHERE id = ? LIMIT 1")
|
||||
.get(companyId) as { auto_approve_hires: number; review_deliverables: number } | undefined;
|
||||
if (!row) return null;
|
||||
return {
|
||||
autoApproveHires: row.auto_approve_hires === 1,
|
||||
reviewDeliverables: row.review_deliverables === 1,
|
||||
};
|
||||
},
|
||||
|
||||
getInbox(companyId: string): InboxItem[] {
|
||||
const items: InboxItem[] = [];
|
||||
const snapshot = deps.listSnapshot({ companyId });
|
||||
const agents = snapshot.agents;
|
||||
const agentMap = new Map(agents.map((a) => [a.id, a]));
|
||||
|
||||
// Pending approvals
|
||||
for (const approval of snapshot.approvals.filter(
|
||||
(a) => a.companyId === companyId && a.state === "pending",
|
||||
)) {
|
||||
const requester = agentMap.get(approval.requestedByAgentId ?? "");
|
||||
items.push({
|
||||
id: `approval-${approval.id}`,
|
||||
type: "pending_approval",
|
||||
title: approval.payloadSummary,
|
||||
subtitle: `${approval.type.replaceAll("_", " ")} — requested by ${requester?.name ?? "Unknown"}`,
|
||||
severity: approval.type === "dangerous_command" ? "critical" : "high",
|
||||
entityType: "approval",
|
||||
entityId: approval.id,
|
||||
agentId: approval.requestedByAgentId,
|
||||
agentName: requester?.name ?? null,
|
||||
createdAt: approval.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
// Failed runs (last 24h)
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
||||
for (const run of snapshot.runs.filter(
|
||||
(r) =>
|
||||
r.companyId === companyId &&
|
||||
(r.status === "failed" || r.status === "timed_out") &&
|
||||
r.createdAt > oneDayAgo,
|
||||
)) {
|
||||
const agent = agentMap.get(run.agentId);
|
||||
items.push({
|
||||
id: `run-${run.id}`,
|
||||
type: "failed_run",
|
||||
title: run.summary || "Run failed",
|
||||
subtitle: `${agent?.name ?? "Unknown agent"} — ${run.errorMessage?.slice(0, 100) ?? "Unknown error"}`,
|
||||
severity: "high",
|
||||
entityType: "run",
|
||||
entityId: run.id,
|
||||
agentId: run.agentId,
|
||||
agentName: agent?.name ?? null,
|
||||
createdAt: run.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
// Budget warnings (agents at >80% budget)
|
||||
for (const agent of agents.filter(
|
||||
(a) => a.budgetMonthlyUsd > 0 && a.spentMonthlyUsd / a.budgetMonthlyUsd > 0.8,
|
||||
)) {
|
||||
const pct = Math.round((agent.spentMonthlyUsd / agent.budgetMonthlyUsd) * 100);
|
||||
items.push({
|
||||
id: `budget-${agent.id}`,
|
||||
type: "budget_warning",
|
||||
title: `${agent.name} at ${pct}% budget`,
|
||||
subtitle: `$${agent.spentMonthlyUsd.toFixed(2)} of $${agent.budgetMonthlyUsd.toFixed(2)} monthly limit`,
|
||||
severity: pct >= 100 ? "critical" : "high",
|
||||
entityType: "agent",
|
||||
entityId: agent.id,
|
||||
agentId: agent.id,
|
||||
agentName: agent.name,
|
||||
createdAt: agent.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
// Blocked tasks
|
||||
for (const task of snapshot.tasks.filter(
|
||||
(t) => t.companyId === companyId && t.status === "blocked",
|
||||
)) {
|
||||
const assignee = agentMap.get(task.assigneeAgentId ?? "");
|
||||
items.push({
|
||||
id: `blocked-${task.id}`,
|
||||
type: "blocked_task",
|
||||
title: task.title,
|
||||
subtitle: `Assigned to ${assignee?.name ?? "Unassigned"} — blocked`,
|
||||
severity: task.priority === "critical" ? "critical" : "medium",
|
||||
entityType: "task",
|
||||
entityId: task.id,
|
||||
agentId: task.assigneeAgentId,
|
||||
agentName: assignee?.name ?? null,
|
||||
createdAt: task.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
// Tasks needing review
|
||||
for (const task of snapshot.tasks.filter(
|
||||
(t) => t.companyId === companyId && t.status === "in_review",
|
||||
)) {
|
||||
const assignee = agentMap.get(task.assigneeAgentId ?? "");
|
||||
items.push({
|
||||
id: `review-${task.id}`,
|
||||
type: "review_needed",
|
||||
title: task.title,
|
||||
subtitle: `Submitted by ${assignee?.name ?? "Unknown"} — awaiting review`,
|
||||
severity: task.priority === "critical" ? "high" : "medium",
|
||||
entityType: "task",
|
||||
entityId: task.id,
|
||||
agentId: task.assigneeAgentId,
|
||||
agentName: assignee?.name ?? null,
|
||||
createdAt: task.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
// Unread urgent agent messages (company-wide or incident channels that the Board should see)
|
||||
for (const msg of snapshot.agentMessages.filter(
|
||||
(m) =>
|
||||
m.companyId === companyId &&
|
||||
!m.readAt &&
|
||||
(m.channel === "company" || m.channel === "incident" || m.priority === "urgent"),
|
||||
)) {
|
||||
const sender = agentMap.get(msg.fromAgentId);
|
||||
items.push({
|
||||
id: `message-${msg.id}`,
|
||||
type: "agent_message",
|
||||
title: msg.subject,
|
||||
subtitle: `${sender?.name ?? "Unknown"} — ${msg.channel === "incident" ? "INCIDENT" : msg.channel} ${msg.priority === "urgent" ? "(urgent)" : ""}`,
|
||||
severity: msg.priority === "urgent" || msg.channel === "incident" ? "critical" : "medium",
|
||||
entityType: "message",
|
||||
entityId: msg.id,
|
||||
agentId: msg.fromAgentId,
|
||||
agentName: sender?.name ?? null,
|
||||
createdAt: msg.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||
items.sort(
|
||||
(a, b) =>
|
||||
severityOrder[a.severity] - severityOrder[b.severity] ||
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
return items;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user