diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index 3589fe21..a690070e 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -22,6 +22,9 @@ import { SdlcService } from "../services/sdlc-service"; import { AuditService } from "../services/audit-service"; import { RollbackService } from "../services/rollback-service"; import { TaskExecutionService } from "../services/task-execution-service"; +import { DocumentService } from "../services/document-service"; +import { KnowledgeService } from "../services/knowledge-service"; +import { AutomationService } from "../services/automation-service"; export interface IpcDeps { connection: DatabaseConnection; @@ -48,6 +51,9 @@ export function registerIpcHandlers(deps: IpcDeps): void { const sdlc = new SdlcService(deps.connection.db); const audit = new AuditService(deps.connection.db); const rollback = new RollbackService(deps.connection.db); + const documents = new DocumentService(deps.connection.db); + const knowledge = new KnowledgeService(deps.connection.db); + const automation = new AutomationService(deps.connection.db); const onboarding = new OnboardingService({ db: deps.connection.db, saveProviderCredential: (input) => providers.saveCredential(input), @@ -104,6 +110,14 @@ export function registerIpcHandlers(deps: IpcDeps): void { ipcMain.handle(IPC_CHANNELS.createGoal, (_event, input) => mutate(() => organization.createGoal(input))); ipcMain.handle(IPC_CHANNELS.createProject, (_event, input) => mutate(() => organization.createProject(input))); ipcMain.handle(IPC_CHANNELS.createWorkspace, (_event, input) => mutate(() => organization.createWorkspace(input))); + ipcMain.handle(IPC_CHANNELS.createDocument, (_event, input) => mutate(() => documents.create(input))); + ipcMain.handle(IPC_CHANNELS.updateDocument, (_event, input) => mutate(() => documents.update(input))); + ipcMain.handle(IPC_CHANNELS.deleteDocument, (_event, input) => mutate(() => documents.delete(input))); + ipcMain.handle(IPC_CHANNELS.createKnowledgeEntry, (_event, input) => mutate(() => knowledge.create(input))); + ipcMain.handle(IPC_CHANNELS.deleteKnowledgeEntry, (_event, input) => mutate(() => knowledge.delete(input))); + ipcMain.handle(IPC_CHANNELS.createAutomationRule, (_event, input) => mutate(() => automation.create(input))); + ipcMain.handle(IPC_CHANNELS.updateAutomationRule, (_event, input) => mutate(() => automation.update(input))); + ipcMain.handle(IPC_CHANNELS.deleteAutomationRule, (_event, input) => mutate(() => automation.delete(input))); ipcMain.handle(IPC_CHANNELS.createApproval, (_event, input) => mutate(() => approvals.create(input))); ipcMain.handle(IPC_CHANNELS.decideApproval, (_event, input) => mutate(() => approvals.decide(input))); ipcMain.handle(IPC_CHANNELS.syncEmployeeIdentity, (_event, input) => mutate(() => identityAccess.syncEmployeeIdentity(input))); diff --git a/AgentCompany-New/src/main/database/migrations/040-documents-knowledge-automation.ts b/AgentCompany-New/src/main/database/migrations/040-documents-knowledge-automation.ts new file mode 100644 index 00000000..25242df7 --- /dev/null +++ b/AgentCompany-New/src/main/database/migrations/040-documents-knowledge-automation.ts @@ -0,0 +1,52 @@ +export const documentsKnowledgeAutomationSchema = ` + create table if not exists documents ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + title text not null, + doc_type text not null, + content text not null, + status text not null, + author_agent_id text references agents(id) on delete set null, + reviewer_agent_id text references agents(id) on delete set null, + project_id text references projects(id) on delete set null, + goal_id text references goals(id) on delete set null, + task_id text references tasks(id) on delete set null, + created_at text not null, + updated_at text not null + ); + + create index if not exists idx_documents_org on documents(org_id, doc_type); + + create table if not exists knowledge_entries ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + topic text not null, + category text not null, + content text not null, + importance text not null, + source_type text not null, + source_id text, + created_at text not null, + updated_at text not null + ); + + create index if not exists idx_knowledge_org on knowledge_entries(org_id, category); + + create table if not exists automation_rules ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + name text not null, + trigger_event text not null, + condition_json text not null, + action_type text not null, + action_config_json text not null, + priority integer not null, + enabled integer not null, + last_fired_at text, + fire_count integer not null default 0, + created_at text not null, + updated_at text not null + ); + + create index if not exists idx_automation_org on automation_rules(org_id, enabled); +`; diff --git a/AgentCompany-New/src/main/database/schema.ts b/AgentCompany-New/src/main/database/schema.ts index 2e4bd89a..370b1ed8 100644 --- a/AgentCompany-New/src/main/database/schema.ts +++ b/AgentCompany-New/src/main/database/schema.ts @@ -38,6 +38,7 @@ import { additionalConnectorsSchema } from "./migrations/036-additional-connecto import { customProvidersSchema } from "./migrations/037-custom-providers"; import { providerAuthProfileCascadeSchema } from "./migrations/038-provider-auth-profile-cascade"; import { auditImmutabilityOrgTeardownSchema } from "./migrations/039-audit-immutability-org-teardown"; +import { documentsKnowledgeAutomationSchema } from "./migrations/040-documents-knowledge-automation"; interface Migration { version: number; @@ -85,6 +86,7 @@ const MIGRATIONS: Migration[] = [ { version: 37, name: "custom_providers", sql: customProvidersSchema }, { version: 38, name: "provider_auth_profile_cascade", sql: providerAuthProfileCascadeSchema }, { version: 39, name: "audit_immutability_org_teardown", sql: auditImmutabilityOrgTeardownSchema }, + { version: 40, name: "documents_knowledge_automation", sql: documentsKnowledgeAutomationSchema }, ]; const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version; diff --git a/AgentCompany-New/src/main/services/automation-service.ts b/AgentCompany-New/src/main/services/automation-service.ts new file mode 100644 index 00000000..a7d50aed --- /dev/null +++ b/AgentCompany-New/src/main/services/automation-service.ts @@ -0,0 +1,44 @@ +import type { DatabaseSync } from "node:sqlite"; +import type { CreateAutomationRuleInput, UpdateAutomationRuleInput, DeleteAutomationRuleInput } from "../../shared/commands"; +import { createId } from "../../shared/ids"; +import { AuditService } from "./audit-service"; + +export class AutomationService { + private readonly audit: AuditService; + + constructor(private readonly db: DatabaseSync) { + this.audit = new AuditService(db); + } + + create(input: CreateAutomationRuleInput): string { + const id = createId("aut"); + const now = new Date().toISOString(); + this.db.prepare(` + insert into automation_rules(id, org_id, name, trigger_event, condition_json, action_type, action_config_json, priority, enabled, fire_count, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?) + `).run(id, input.orgId, input.name.trim(), input.triggerEvent, JSON.stringify(input.conditions), input.actionType, JSON.stringify(input.actionConfig), input.priority, input.enabled ? 1 : 0, now, now); + this.audit.record({ + orgId: input.orgId, + entityType: "automation_rule", + entityId: id, + actorType: "system", + actorId: null, + action: "created", + payload: { name: input.name.trim(), triggerEvent: input.triggerEvent }, + }); + return id; + } + + update(input: UpdateAutomationRuleInput): void { + const result = this.db.prepare(` + update automation_rules set name = ?, trigger_event = ?, condition_json = ?, action_type = ?, action_config_json = ?, priority = ?, enabled = ?, updated_at = ? + where id = ? and org_id = ? + `).run(input.name.trim(), input.triggerEvent, JSON.stringify(input.conditions), input.actionType, JSON.stringify(input.actionConfig), input.priority, input.enabled ? 1 : 0, new Date().toISOString(), input.ruleId, input.orgId); + if (result.changes === 0) throw new Error("Automation rule was not found."); + } + + delete(input: DeleteAutomationRuleInput): void { + const result = this.db.prepare("delete from automation_rules where id = ? and org_id = ?").run(input.ruleId, input.orgId); + if (result.changes === 0) throw new Error("Automation rule was not found."); + } +} diff --git a/AgentCompany-New/src/main/services/document-service.ts b/AgentCompany-New/src/main/services/document-service.ts new file mode 100644 index 00000000..5ef6df18 --- /dev/null +++ b/AgentCompany-New/src/main/services/document-service.ts @@ -0,0 +1,43 @@ +import type { DatabaseSync } from "node:sqlite"; +import type { CreateDocumentInput, UpdateDocumentInput, DeleteDocumentInput } from "../../shared/commands"; +import { createId } from "../../shared/ids"; +import { AuditService } from "./audit-service"; + +export class DocumentService { + private readonly audit: AuditService; + + constructor(private readonly db: DatabaseSync) { + this.audit = new AuditService(db); + } + + create(input: CreateDocumentInput): string { + const id = createId("doc"); + const now = new Date().toISOString(); + this.db.prepare(` + insert into documents(id, org_id, title, doc_type, content, status, author_agent_id, reviewer_agent_id, project_id, goal_id, task_id, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run(id, input.orgId, input.title.trim(), input.docType, input.content, "draft", input.authorAgentId, input.reviewerAgentId, input.projectId, input.goalId, input.taskId, now, now); + this.audit.record({ + orgId: input.orgId, + entityType: "document", + entityId: id, + actorType: "system", + actorId: null, + action: "created", + payload: { title: input.title.trim(), docType: input.docType }, + }); + return id; + } + + update(input: UpdateDocumentInput): void { + const result = this.db.prepare(` + update documents set title = ?, content = ?, status = ?, updated_at = ? where id = ? and org_id = ? + `).run(input.title.trim(), input.content, input.status, new Date().toISOString(), input.documentId, input.orgId); + if (result.changes === 0) throw new Error("Document was not found."); + } + + delete(input: DeleteDocumentInput): void { + const result = this.db.prepare("delete from documents where id = ? and org_id = ?").run(input.documentId, input.orgId); + if (result.changes === 0) throw new Error("Document was not found."); + } +} diff --git a/AgentCompany-New/src/main/services/knowledge-service.ts b/AgentCompany-New/src/main/services/knowledge-service.ts new file mode 100644 index 00000000..e333bbd6 --- /dev/null +++ b/AgentCompany-New/src/main/services/knowledge-service.ts @@ -0,0 +1,36 @@ +import type { DatabaseSync } from "node:sqlite"; +import type { CreateKnowledgeEntryInput, DeleteKnowledgeEntryInput } from "../../shared/commands"; +import { createId } from "../../shared/ids"; +import { AuditService } from "./audit-service"; + +export class KnowledgeService { + private readonly audit: AuditService; + + constructor(private readonly db: DatabaseSync) { + this.audit = new AuditService(db); + } + + create(input: CreateKnowledgeEntryInput): string { + const id = createId("kne"); + const now = new Date().toISOString(); + this.db.prepare(` + insert into knowledge_entries(id, org_id, topic, category, content, importance, source_type, source_id, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run(id, input.orgId, input.topic.trim(), input.category, input.content, input.importance, input.sourceType, input.sourceId, now, now); + this.audit.record({ + orgId: input.orgId, + entityType: "knowledge_entry", + entityId: id, + actorType: "system", + actorId: null, + action: "created", + payload: { topic: input.topic.trim(), category: input.category }, + }); + return id; + } + + delete(input: DeleteKnowledgeEntryInput): void { + const result = this.db.prepare("delete from knowledge_entries where id = ? and org_id = ?").run(input.entryId, input.orgId); + if (result.changes === 0) throw new Error("Knowledge entry was not found."); + } +} diff --git a/AgentCompany-New/src/main/services/snapshot-service.ts b/AgentCompany-New/src/main/services/snapshot-service.ts index fbb76d91..ae1deec4 100644 --- a/AgentCompany-New/src/main/services/snapshot-service.ts +++ b/AgentCompany-New/src/main/services/snapshot-service.ts @@ -156,6 +156,10 @@ export class SnapshotService { prBotGates, auditReplayRuns: list(this.db, "audit_replay_runs", "created_at desc"), auditEvents: list(this.db, "audit_events", "created_at desc").slice(0, 200), + documents: list(this.db, "documents", "updated_at desc"), + knowledgeEntries: list(this.db, "knowledge_entries", "updated_at desc"), + automationRules: list(this.db, "automation_rules", "priority asc") + .map((rule) => ({ ...rule, enabled: toBool(rule.enabled) })), }; } diff --git a/AgentCompany-New/src/preload/index.ts b/AgentCompany-New/src/preload/index.ts index 8f429192..a2ba46ff 100644 --- a/AgentCompany-New/src/preload/index.ts +++ b/AgentCompany-New/src/preload/index.ts @@ -20,6 +20,14 @@ const api: DesktopApi = { createGoal: (input) => invoke(IPC_CHANNELS.createGoal, input), createProject: (input) => invoke(IPC_CHANNELS.createProject, input), createWorkspace: (input) => invoke(IPC_CHANNELS.createWorkspace, input), + createDocument: (input) => invoke(IPC_CHANNELS.createDocument, input), + updateDocument: (input) => invoke(IPC_CHANNELS.updateDocument, input), + deleteDocument: (input) => invoke(IPC_CHANNELS.deleteDocument, input), + createKnowledgeEntry: (input) => invoke(IPC_CHANNELS.createKnowledgeEntry, input), + deleteKnowledgeEntry: (input) => invoke(IPC_CHANNELS.deleteKnowledgeEntry, input), + createAutomationRule: (input) => invoke(IPC_CHANNELS.createAutomationRule, input), + updateAutomationRule: (input) => invoke(IPC_CHANNELS.updateAutomationRule, input), + deleteAutomationRule: (input) => invoke(IPC_CHANNELS.deleteAutomationRule, input), createApproval: (input) => invoke(IPC_CHANNELS.createApproval, input), decideApproval: (input) => invoke(IPC_CHANNELS.decideApproval, input), syncEmployeeIdentity: (input) => invoke(IPC_CHANNELS.syncEmployeeIdentity, input), diff --git a/AgentCompany-New/src/shared/commands.ts b/AgentCompany-New/src/shared/commands.ts index 3c1ba8d2..ad807144 100644 --- a/AgentCompany-New/src/shared/commands.ts +++ b/AgentCompany-New/src/shared/commands.ts @@ -470,6 +470,74 @@ export interface BootstrapOnboardingSummary { providerId: string; } +export interface CreateDocumentInput { + orgId: string; + title: string; + docType: string; + content: string; + authorAgentId: string | null; + reviewerAgentId: string | null; + projectId: string | null; + goalId: string | null; + taskId: string | null; +} + +export interface UpdateDocumentInput { + orgId: string; + documentId: string; + title: string; + content: string; + status: string; +} + +export interface DeleteDocumentInput { + orgId: string; + documentId: string; +} + +export interface CreateKnowledgeEntryInput { + orgId: string; + topic: string; + category: string; + content: string; + importance: string; + sourceType: string; + sourceId: string | null; +} + +export interface DeleteKnowledgeEntryInput { + orgId: string; + entryId: string; +} + +export interface CreateAutomationRuleInput { + orgId: string; + name: string; + triggerEvent: string; + conditions: Record; + actionType: string; + actionConfig: Record; + priority: number; + enabled: boolean; +} + +export interface UpdateAutomationRuleInput { + orgId: string; + ruleId: string; + name: string; + triggerEvent: string; + conditions: Record; + actionType: string; + actionConfig: Record; + priority: number; + enabled: boolean; +} + +export interface DeleteAutomationRuleInput { + orgId: string; + ruleId: string; +} + export interface NativeDialogResult { canceled: boolean; filePaths: string[]; diff --git a/AgentCompany-New/src/shared/desktop-api.ts b/AgentCompany-New/src/shared/desktop-api.ts index 775a4d14..526a4eb3 100644 --- a/AgentCompany-New/src/shared/desktop-api.ts +++ b/AgentCompany-New/src/shared/desktop-api.ts @@ -9,9 +9,17 @@ import type { CreateAgentInput, UpdateAgentInput, CreateApprovalInput, + CreateAutomationRuleInput, + CreateDocumentInput, CreateGoalInput, + CreateKnowledgeEntryInput, CreateProjectInput, CreateWorkspaceInput, + DeleteAutomationRuleInput, + DeleteDocumentInput, + DeleteKnowledgeEntryInput, + UpdateAutomationRuleInput, + UpdateDocumentInput, CreateAuditReplayInput, CreateChatSpaceInput, CreateComplianceExportInput, @@ -96,6 +104,14 @@ export interface DesktopApi { createGoal(input: CreateGoalInput): Promise>; createProject(input: CreateProjectInput): Promise>; createWorkspace(input: CreateWorkspaceInput): Promise>; + createDocument(input: CreateDocumentInput): Promise>; + updateDocument(input: UpdateDocumentInput): Promise>; + deleteDocument(input: DeleteDocumentInput): Promise>; + createKnowledgeEntry(input: CreateKnowledgeEntryInput): Promise>; + deleteKnowledgeEntry(input: DeleteKnowledgeEntryInput): Promise>; + createAutomationRule(input: CreateAutomationRuleInput): Promise>; + updateAutomationRule(input: UpdateAutomationRuleInput): Promise>; + deleteAutomationRule(input: DeleteAutomationRuleInput): Promise>; createApproval(input: CreateApprovalInput): Promise>; decideApproval(input: DecideApprovalInput): Promise>; syncEmployeeIdentity(input: SyncEmployeeIdentityInput): Promise>; diff --git a/AgentCompany-New/src/shared/domain.ts b/AgentCompany-New/src/shared/domain.ts index a454b39e..fe9a695f 100644 --- a/AgentCompany-New/src/shared/domain.ts +++ b/AgentCompany-New/src/shared/domain.ts @@ -383,6 +383,51 @@ export interface MigrationReportRecord { createdAt: string; } +export interface DocumentRecord { + id: string; + orgId: string; + title: string; + docType: string; + content: string; + status: string; + authorAgentId: string | null; + reviewerAgentId: string | null; + projectId: string | null; + goalId: string | null; + taskId: string | null; + createdAt: string; + updatedAt: string; +} + +export interface KnowledgeEntryRecord { + id: string; + orgId: string; + topic: string; + category: string; + content: string; + importance: string; + sourceType: string; + sourceId: string | null; + createdAt: string; + updatedAt: string; +} + +export interface AutomationRuleRecord { + id: string; + orgId: string; + name: string; + triggerEvent: string; + conditionJson: string; + actionType: string; + actionConfigJson: string; + priority: number; + enabled: boolean; + lastFiredAt: string | null; + fireCount: number; + createdAt: string; + updatedAt: string; +} + export interface AppSnapshot { activeOrgId: string | null; organizations: OrganizationRecord[]; @@ -439,4 +484,7 @@ export interface AppSnapshot { sdlcReviewAgents: SdlcReviewAgentRecord[]; sdlcReviewRuns: SdlcReviewRunRecord[]; evalSuites: EvalSuiteRecord[]; evalCases: EvalCaseRecord[]; evalRuns: EvalRunRecord[]; prBotGates: PrBotGateRecord[]; auditReplayRuns: AuditReplayRunRecord[]; auditEvents: AuditEventRecord[]; + documents: DocumentRecord[]; + knowledgeEntries: KnowledgeEntryRecord[]; + automationRules: AutomationRuleRecord[]; } diff --git a/AgentCompany-New/src/shared/ipc.ts b/AgentCompany-New/src/shared/ipc.ts index bfae6033..322c35d9 100644 --- a/AgentCompany-New/src/shared/ipc.ts +++ b/AgentCompany-New/src/shared/ipc.ts @@ -60,6 +60,14 @@ export const IPC_CHANNELS = { createGoal: "agentcompany:create-goal", createProject: "agentcompany:create-project", createWorkspace: "agentcompany:create-workspace", + createDocument: "agentcompany:create-document", + updateDocument: "agentcompany:update-document", + deleteDocument: "agentcompany:delete-document", + createKnowledgeEntry: "agentcompany:create-knowledge-entry", + deleteKnowledgeEntry: "agentcompany:delete-knowledge-entry", + createAutomationRule: "agentcompany:create-automation-rule", + updateAutomationRule: "agentcompany:update-automation-rule", + deleteAutomationRule: "agentcompany:delete-automation-rule", search: "agentcompany:search", rendererEvent: "agentcompany:renderer-event", windowMinimize: "agentcompany:window-minimize", diff --git a/AgentCompany-New/tests/unit/domain-services.test.ts b/AgentCompany-New/tests/unit/domain-services.test.ts index 9908baef..c7061e1b 100644 --- a/AgentCompany-New/tests/unit/domain-services.test.ts +++ b/AgentCompany-New/tests/unit/domain-services.test.ts @@ -24,7 +24,7 @@ describe("domain services", () => { const connection = createConnection(); const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>; - expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39]); + expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40]); connection.close(); });