Rebuild AgentCompany Electron app

This commit is contained in:
ZacharyZhang-NY committed 2026-05-20 23:27:00 -04:00
1 parent bc5daa6eb8
commit fcf6d90731
56 files changed
+8667

No files matched your search

@@ -0,0 +1,66 @@
import type { DatabaseSync } from "node:sqlite";
import type { CreateApprovalInput, DecideApprovalInput } from "../../shared/commands";
import { APPROVAL_STATE } from "../../shared/domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
export class ApprovalService {
private readonly audit: AuditService;
constructor(private readonly db: DatabaseSync) {
this.audit = new AuditService(db);
}
create(input: CreateApprovalInput): string {
const id = createId("app");
const now = new Date().toISOString();
this.db.prepare(`
insert into approvals(
id, org_id, kind, title, details, risk, requested_by_type, requested_by_id,
state, decided_by_employee_id, created_at, decided_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
input.orgId,
input.kind.trim(),
input.title.trim(),
input.details.trim(),
input.risk,
input.requestedByType,
input.requestedById,
APPROVAL_STATE.PENDING,
null,
now,
null,
);
this.audit.record({
orgId: input.orgId,
entityType: "approval",
entityId: id,
actorType: input.requestedByType,
actorId: input.requestedById,
action: "requested",
payload: { kind: input.kind, risk: input.risk },
});
return id;
}
decide(input: DecideApprovalInput): void {
const now = new Date().toISOString();
const result = this.db.prepare(`
update approvals
set state = ?, decided_by_employee_id = ?, decided_at = ?
where id = ? and org_id = ? and state = ?
`).run(input.state, input.decidedByEmployeeId, now, input.approvalId, input.orgId, APPROVAL_STATE.PENDING);
if (result.changes === 0) throw new Error("Pending approval was not found.");
this.audit.record({
orgId: input.orgId,
entityType: "approval",
entityId: input.approvalId,
actorType: "employee",
actorId: input.decidedByEmployeeId,
action: input.state,
payload: { decidedAt: now },
});
}
}
@@ -0,0 +1,31 @@
import type { DatabaseSync } from "node:sqlite";
import { createId } from "../../shared/ids";
export class AuditService {
constructor(private readonly db: DatabaseSync) {}
record(input: {
orgId: string;
entityType: string;
entityId: string;
actorType: string;
actorId: string | null;
action: string;
payload: unknown;
}): void {
this.db.prepare(`
insert into audit_events(id, org_id, entity_type, entity_id, actor_type, actor_id, action, payload_json, created_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
createId("aud"),
input.orgId,
input.entityType,
input.entityId,
input.actorType,
input.actorId,
input.action,
JSON.stringify(input.payload),
new Date().toISOString(),
);
}
}
@@ -0,0 +1,45 @@
import type { DatabaseSync } from "node:sqlite";
import type { SendMessageInput } from "../../shared/commands";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
export class ChatService {
private readonly audit: AuditService;
constructor(private readonly db: DatabaseSync) {
this.audit = new AuditService(db);
}
sendMessage(input: SendMessageInput): string {
const body = input.bodyMd.trim();
if (!body) throw new Error("Message body is required.");
const id = createId("msg");
const now = new Date().toISOString();
this.db.prepare(`
insert into chat_messages(
id, org_id, space_id, thread_id, sender_type, sender_id, body_md, created_at, edited_at, deleted_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
input.orgId,
input.spaceId,
input.threadId,
input.senderType,
input.senderId,
body,
now,
null,
null,
);
this.audit.record({
orgId: input.orgId,
entityType: "chat_message",
entityId: id,
actorType: input.senderType,
actorId: input.senderId,
action: "sent",
payload: { spaceId: input.spaceId, threadId: input.threadId },
});
return id;
}
}
@@ -0,0 +1,77 @@
import { createHash } from "node:crypto";
import { copyFileSync, mkdirSync, readFileSync, statSync } from "node:fs";
import { basename, extname, join } from "node:path";
import type { DatabaseSync } from "node:sqlite";
import { dialog } from "electron";
import type { ImportFileInput } from "../../shared/commands";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
function fileKind(path: string): string {
const ext = extname(path).toLowerCase().replace(".", "");
if (!ext) return "file";
if (["md", "markdown"].includes(ext)) return "md";
if (["pdf"].includes(ext)) return "pdf";
if (["png", "jpg", "jpeg", "gif", "webp", "svg"].includes(ext)) return "img";
if (["csv", "xlsx", "xls"].includes(ext)) return "sheet";
if (["doc", "docx", "txt"].includes(ext)) return "doc";
return ext;
}
export class FileService {
private readonly audit: AuditService;
constructor(
private readonly db: DatabaseSync,
private readonly artifactRoot: string,
) {
this.audit = new AuditService(db);
mkdirSync(this.artifactRoot, { recursive: true });
}
async importFile(input: ImportFileInput): Promise<string | null> {
const result = await dialog.showOpenDialog({
properties: ["openFile"],
securityScopedBookmarks: true,
});
if (result.canceled || result.filePaths.length === 0) return null;
return this.importPath(input.orgId, result.filePaths[0]);
}
importPath(orgId: string, sourcePath: string): string {
const id = createId("fil");
const now = new Date().toISOString();
const stats = statSync(sourcePath);
if (!stats.isFile()) throw new Error("Selected path is not a file.");
const hash = createHash("sha256").update(readFileSync(sourcePath)).digest("hex");
const orgDir = join(this.artifactRoot, orgId);
mkdirSync(orgDir, { recursive: true });
const target = join(orgDir, `${id}-${basename(sourcePath)}`);
copyFileSync(sourcePath, target);
this.db.prepare(`
insert into file_objects(id, org_id, name, kind, size_bytes, sha256, storage_path, owner_type, owner_id, created_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
orgId,
basename(sourcePath),
fileKind(sourcePath),
stats.size,
hash,
target,
"system",
null,
now,
);
this.audit.record({
orgId,
entityType: "file_object",
entityId: id,
actorType: "system",
actorId: null,
action: "imported",
payload: { sha256: hash, sizeBytes: stats.size },
});
return id;
}
}
@@ -0,0 +1,168 @@
import type { DatabaseSync } from "node:sqlite";
import { APPROVAL_STATE, RISK_LEVEL, TASK_STATUS } from "../../shared/domain";
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
import { createId } from "../../shared/ids";
import type { CreateAgentInput, CreateOrganizationInput, CreateTaskInput } from "../../shared/commands";
import { AuditService } from "./audit-service";
export class OrganizationService {
private readonly audit: AuditService;
constructor(private readonly db: DatabaseSync) {
this.audit = new AuditService(db);
}
createOrganization(input: CreateOrganizationInput): string {
const orgId = createId("org");
const employeeId = createId("emp");
const now = new Date().toISOString();
this.db.prepare("insert into organizations(id, name, mission, goal, created_at) values (?, ?, ?, ?, ?)")
.run(orgId, input.organizationName.trim(), input.mission.trim(), input.goal.trim(), now);
this.db.prepare("insert into employees(id, org_id, display_name, role, created_at) values (?, ?, ?, ?, ?)")
.run(employeeId, orgId, input.employeeName.trim(), "Board Chair", now);
for (const space of [
["company", "general", "Company coordination", "workspace"],
["department", "product", "Product decisions", "workspace"],
["department", "engineering", "Engineering delivery", "workspace"],
["company", "board", "Board decisions", "restricted"],
]) {
const spaceId = createId("spc");
this.db.prepare(`
insert into chat_spaces(id, org_id, space_type, name, topic, visibility, created_at)
values (?, ?, ?, ?, ?, ?, ?)
`).run(spaceId, orgId, space[0], space[1], space[2], space[3], now);
this.db.prepare(`
insert into chat_memberships(space_id, principal_type, principal_id, role, mute_state)
values (?, ?, ?, ?, ?)
`).run(spaceId, "employee", employeeId, space[1] === "board" ? "approver" : "member", "unmuted");
}
for (const provider of PROVIDER_CATALOG) {
this.db.prepare(`
insert into providers(id, org_id, provider_key, label, auth_type, model, credential_id, enabled, created_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
createId("prv"),
orgId,
provider.key,
provider.label,
provider.authType,
provider.defaultModel,
null,
0,
now,
);
}
this.audit.record({
orgId,
entityType: "organization",
entityId: orgId,
actorType: "employee",
actorId: employeeId,
action: "created",
payload: { name: input.organizationName.trim() },
});
return orgId;
}
createAgent(input: CreateAgentInput): string {
const id = createId("agt");
const now = new Date().toISOString();
this.db.prepare(`
insert into agents(
id, org_id, name, role, department, model_provider_id, model_id,
budget_cents, manager_agent_id, lifecycle, created_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
input.orgId,
input.name.trim(),
input.role.trim(),
input.department.trim(),
input.modelProviderId,
input.modelId,
input.budgetCents,
input.managerAgentId,
"active",
now,
);
this.audit.record({
orgId: input.orgId,
entityType: "agent",
entityId: id,
actorType: "system",
actorId: null,
action: "created",
payload: { role: input.role, department: input.department },
});
return id;
}
createTask(input: CreateTaskInput): string {
const id = createId("tsk");
const now = new Date().toISOString();
this.db.prepare(`
insert into tasks(id, org_id, title, description, status, assignee_agent_id, risk, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
input.orgId,
input.title.trim(),
input.description.trim(),
input.status,
input.assigneeAgentId,
input.risk,
now,
now,
);
if (input.risk === RISK_LEVEL.HIGH) {
this.db.prepare(`
insert into approvals(
id, org_id, kind, title, details, risk, requested_by_type,
requested_by_id, state, decided_by_employee_id, created_at, decided_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
createId("app"),
input.orgId,
"task_risk",
input.title.trim(),
input.description.trim(),
RISK_LEVEL.HIGH,
"system",
null,
APPROVAL_STATE.PENDING,
null,
now,
null,
);
}
this.audit.record({
orgId: input.orgId,
entityType: "task",
entityId: id,
actorType: "system",
actorId: null,
action: "created",
payload: { status: TASK_STATUS.BACKLOG },
});
return id;
}
updateTaskStatus(orgId: string, taskId: string, status: string): void {
const result = this.db.prepare("update tasks set status = ?, updated_at = ? where id = ? and org_id = ?")
.run(status, new Date().toISOString(), taskId, orgId);
if (result.changes === 0) throw new Error("Task was not found.");
this.audit.record({
orgId,
entityType: "task",
entityId: taskId,
actorType: "system",
actorId: null,
action: "status_changed",
payload: { status },
});
}
}
@@ -0,0 +1,53 @@
import type { DatabaseSync } from "node:sqlite";
import type { SaveProviderCredentialInput } from "../../shared/commands";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
import { SecretVault } from "./secret-vault";
function displayHint(secret: string): string {
const clean = secret.trim();
if (clean.length <= 10) return `${clean.slice(0, 2)}...`;
return `${clean.slice(0, 6)}...${clean.slice(-4)}`;
}
export class ProviderService {
private readonly audit: AuditService;
private readonly vault = new SecretVault();
constructor(private readonly db: DatabaseSync) {
this.audit = new AuditService(db);
}
saveCredential(input: SaveProviderCredentialInput): void {
const provider = this.db.prepare("select * from providers where id = ? and org_id = ?")
.get(input.providerId, input.orgId) as { provider_key: string; auth_type: string } | undefined;
if (!provider) throw new Error("Provider was not found.");
const secret = input.secret.trim();
if (!secret) throw new Error("Credential is required.");
const credentialId = createId("cred");
const now = new Date().toISOString();
this.db.prepare(`
insert into credentials(id, org_id, provider_key, kind, ciphertext, display_hint, created_at)
values (?, ?, ?, ?, ?, ?, ?)
`).run(
credentialId,
input.orgId,
provider.provider_key,
provider.auth_type,
this.vault.encryptSecret(secret),
displayHint(secret),
now,
);
this.db.prepare("update providers set credential_id = ?, model = ?, enabled = 1 where id = ? and org_id = ?")
.run(credentialId, input.model, input.providerId, input.orgId);
this.audit.record({
orgId: input.orgId,
entityType: "provider",
entityId: input.providerId,
actorType: "system",
actorId: null,
action: "credential_saved",
payload: { providerKey: provider.provider_key, authType: provider.auth_type },
});
}
}
@@ -0,0 +1,17 @@
import { safeStorage } from "electron";
export class SecretVault {
encryptSecret(secret: string): Buffer {
if (!safeStorage.isEncryptionAvailable()) {
throw new Error("OS credential encryption is unavailable.");
}
return safeStorage.encryptString(secret);
}
decryptSecret(ciphertext: Buffer): string {
if (!safeStorage.isEncryptionAvailable()) {
throw new Error("OS credential encryption is unavailable.");
}
return safeStorage.decryptString(ciphertext);
}
}
@@ -0,0 +1,130 @@
import { createHash } from "node:crypto";
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { basename, join } from "node:path";
import type { DatabaseSync } from "node:sqlite";
import type { BindSkillInput, ImportSkillDirectoryInput } from "../../shared/commands";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
interface SkillManifest {
name: string;
version: string;
entrypoint: string;
capabilities: string[];
requiredTools: string[];
compatibleModels: string[];
riskLevel: string;
}
function slugify(value: string): string {
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
function parseSkillName(markdown: string, directoryPath: string): string {
const frontmatterName = markdown.match(/^---[\s\S]*?\nname:\s*["']?([^"'\n]+)["']?/m)?.[1];
if (frontmatterName) return frontmatterName.trim();
const heading = markdown.match(/^#\s+(.+)$/m)?.[1];
return heading?.trim() || basename(directoryPath);
}
function hashFiles(directoryPath: string): string {
const hash = createHash("sha256");
const files = readdirSync(directoryPath).filter((name) => !name.startsWith(".")).sort();
for (const file of files) {
const path = join(directoryPath, file);
const stats = statSync(path);
if (stats.isFile()) {
hash.update(file);
hash.update(readFileSync(path));
}
}
return hash.digest("hex");
}
export class SkillService {
private readonly audit: AuditService;
constructor(private readonly db: DatabaseSync) {
this.audit = new AuditService(db);
}
importDirectory(input: ImportSkillDirectoryInput): string {
const skillPath = join(input.directoryPath, "SKILL.md");
if (!existsSync(skillPath)) throw new Error("SKILL.md was not found in the selected directory.");
const markdown = readFileSync(skillPath, "utf8");
const name = parseSkillName(markdown, input.directoryPath);
const slug = slugify(name);
if (!slug) throw new Error("Skill name could not be derived.");
const now = new Date().toISOString();
const skillId = createId("skl");
const revisionId = createId("skr");
const checksum = hashFiles(input.directoryPath);
const manifest: SkillManifest = {
name,
version: input.versionSemver,
entrypoint: "SKILL.md",
capabilities: [],
requiredTools: [],
compatibleModels: ["openai/*", "anthropic/*", "xai/*"],
riskLevel: "medium",
};
this.db.prepare(`
insert into skill_definitions(id, org_id, slug, name, owner_type, status, created_at)
values (?, ?, ?, ?, ?, ?, ?)
on conflict(org_id, slug) do update set name = excluded.name, status = 'active'
`).run(skillId, input.orgId, slug, name, "organization", "active", now);
const existing = this.db.prepare("select id from skill_definitions where org_id = ? and slug = ?")
.get(input.orgId, slug) as { id: string };
this.db.prepare(`
insert into skill_revisions(
id, skill_id, version_semver, manifest_json, source_type, source_ref, published_at, immutable_checksum
) values (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
revisionId,
existing.id,
input.versionSemver,
JSON.stringify(manifest),
"local_directory",
input.directoryPath,
now,
checksum,
);
this.db.prepare(`
insert into skill_artifacts(id, revision_id, artifact_type, object_key, sha256, size_bytes)
values (?, ?, ?, ?, ?, ?)
`).run(createId("ska"), revisionId, "markdown", skillPath, checksum, Buffer.byteLength(markdown));
this.audit.record({
orgId: input.orgId,
entityType: "skill_revision",
entityId: revisionId,
actorType: "system",
actorId: null,
action: "published",
payload: { slug, version: input.versionSemver },
});
return revisionId;
}
bind(input: BindSkillInput): string {
const id = createId("skb");
this.db.prepare(`
insert into skill_bindings(id, org_id, agent_id, skill_id, pinned_revision_id, binding_mode, priority, enabled)
values (?, ?, ?, ?, ?, ?, ?, ?)
on conflict(agent_id, skill_id) do update set pinned_revision_id = excluded.pinned_revision_id, enabled = 1
`).run(id, input.orgId, input.agentId, input.skillId, input.revisionId, "pinned", 100, 1);
this.audit.record({
orgId: input.orgId,
entityType: "skill_binding",
entityId: id,
actorType: "system",
actorId: null,
action: "bound",
payload: { agentId: input.agentId, skillId: input.skillId, revisionId: input.revisionId },
});
return id;
}
}
@@ -0,0 +1,57 @@
import type { DatabaseSync, StatementSync } from "node:sqlite";
import type { AppSnapshot } from "../../shared/domain";
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
type Row = Record<string, unknown>;
function all<T>(statement: StatementSync): T[] {
return statement.all() as T[];
}
function toBool(value: unknown): boolean {
return value === 1 || value === true;
}
function camel(row: Row): Row {
const next: Row = {};
for (const [key, value] of Object.entries(row)) {
next[key.replace(/_([a-z])/g, (_m, ch: string) => ch.toUpperCase())] = value;
}
return next;
}
function list<T>(db: DatabaseSync, table: string, orderBy: string): T[] {
return all<Row>(db.prepare(`select * from ${table} order by ${orderBy}`)).map((row) => camel(row) as T);
}
export class SnapshotService {
constructor(private readonly db: DatabaseSync) {}
load(): AppSnapshot {
const organizations = list<AppSnapshot["organizations"][number]>(this.db, "organizations", "created_at asc");
const activeOrgId = organizations[0]?.id ?? null;
const providers = list<AppSnapshot["providers"][number]>(this.db, "providers", "label asc")
.map((provider) => ({ ...provider, enabled: toBool(provider.enabled) }));
const skillBindings = list<AppSnapshot["skillBindings"][number]>(this.db, "skill_bindings", "priority asc")
.map((binding) => ({ ...binding, enabled: toBool(binding.enabled) }));
return {
activeOrgId,
organizations,
employees: list(this.db, "employees", "created_at asc"),
agents: list(this.db, "agents", "created_at asc"),
tasks: list(this.db, "tasks", "created_at asc"),
approvals: list(this.db, "approvals", "created_at desc"),
chatSpaces: list(this.db, "chat_spaces", "created_at asc"),
chatMessages: list(this.db, "chat_messages", "created_at asc"),
files: list(this.db, "file_objects", "created_at desc"),
providers,
providerCatalog: PROVIDER_CATALOG,
skillDefinitions: list(this.db, "skill_definitions", "created_at asc"),
skillRevisions: list(this.db, "skill_revisions", "published_at desc"),
skillBindings,
socialAccounts: list(this.db, "social_accounts", "created_at asc"),
auditEvents: list<AppSnapshot["auditEvents"][number]>(this.db, "audit_events", "created_at desc").slice(0, 200),
};
}
}
@@ -0,0 +1,113 @@
import type { DatabaseSync } from "node:sqlite";
import { BrowserWindow, session } from "electron";
import type { CreateSocialAccountInput } from "../../shared/commands";
import { SOCIAL_PLATFORM, type SocialPlatform } from "../../shared/domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
const PLATFORM_LOGIN_URL: Record<SocialPlatform, string> = {
[SOCIAL_PLATFORM.X]: "https://x.com/i/flow/login",
[SOCIAL_PLATFORM.LINKEDIN]: "https://www.linkedin.com/login",
[SOCIAL_PLATFORM.YOUTUBE]: "https://accounts.google.com/",
[SOCIAL_PLATFORM.INSTAGRAM]: "https://www.instagram.com/accounts/login/",
};
const ALLOWED_HOSTS: Record<SocialPlatform, string[]> = {
[SOCIAL_PLATFORM.X]: ["x.com", "twitter.com"],
[SOCIAL_PLATFORM.LINKEDIN]: ["linkedin.com", "www.linkedin.com"],
[SOCIAL_PLATFORM.YOUTUBE]: ["accounts.google.com", "youtube.com", "www.youtube.com"],
[SOCIAL_PLATFORM.INSTAGRAM]: ["instagram.com", "www.instagram.com"],
};
export class SocialService {
private readonly audit: AuditService;
constructor(private readonly db: DatabaseSync) {
this.audit = new AuditService(db);
}
createAccount(input: CreateSocialAccountInput): string {
const id = createId("soc");
const now = new Date().toISOString();
this.db.prepare(`
insert into social_accounts(id, org_id, platform, handle, display_name, authorized_employee_id, created_at)
values (?, ?, ?, ?, ?, ?, ?)
`).run(
id,
input.orgId,
input.platform,
input.handle.trim(),
input.displayName.trim(),
input.authorizedEmployeeId,
now,
);
if (input.authorizedEmployeeId) {
this.db.prepare(`
insert into social_account_grants(id, org_id, social_account_id, employee_id, role, scope_json, created_at)
values (?, ?, ?, ?, ?, ?, ?)
`).run(
createId("sgr"),
input.orgId,
id,
input.authorizedEmployeeId,
"operator",
JSON.stringify({ actions: ["login", "draft", "approve"] }),
now,
);
}
this.audit.record({
orgId: input.orgId,
entityType: "social_account",
entityId: id,
actorType: "system",
actorId: null,
action: "created",
payload: { platform: input.platform, handle: input.handle.trim() },
});
return id;
}
launchLoginWindow(accountId: string): void {
const account = this.db.prepare("select * from social_accounts where id = ?")
.get(accountId) as { id: string; org_id: string; platform: SocialPlatform; handle: string } | undefined;
if (!account) throw new Error("Social account was not found.");
const partition = `persist:agentcompany-social-${account.id}`;
const socialSession = session.fromPartition(partition);
socialSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
socialSession.setPermissionCheckHandler(() => false);
const win = new BrowserWindow({
width: 1280,
height: 860,
title: `${account.handle}`,
webPreferences: {
session: socialSession,
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
},
});
this.attachNavigationPolicy(win, account.platform);
win.loadURL(PLATFORM_LOGIN_URL[account.platform]);
this.audit.record({
orgId: account.org_id,
entityType: "social_account",
entityId: account.id,
actorType: "system",
actorId: null,
action: "login_window_opened",
payload: { platform: account.platform },
});
}
private attachNavigationPolicy(win: BrowserWindow, platform: SocialPlatform): void {
const allowed = ALLOWED_HOSTS[platform];
win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
win.webContents.on("will-navigate", (event, url) => {
const host = new URL(url).hostname.toLowerCase();
if (!allowed.some((entry) => host === entry || host.endsWith(`.${entry}`))) {
event.preventDefault();
}
});
}
}