From 05c8a23a757bef97dc196f309e9ae88e5c797535 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 07:26:02 -0500 Subject: [PATCH 01/46] Add AGENTCOMPANIES_SPEC_INVENTORY.md indexing all spec-related code Inventories every file that touches the agentcompanies/v1-draft spec: spec docs, shared types/validators, server services and routes, CLI commands, UI pages/components/libraries, tests, and skills. Includes a cross-reference table mapping spec concepts to implementation files. Co-Authored-By: Paperclip --- AGENTCOMPANIES_SPEC_INVENTORY.md | 114 +++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 AGENTCOMPANIES_SPEC_INVENTORY.md diff --git a/AGENTCOMPANIES_SPEC_INVENTORY.md b/AGENTCOMPANIES_SPEC_INVENTORY.md new file mode 100644 index 00000000..0d622890 --- /dev/null +++ b/AGENTCOMPANIES_SPEC_INVENTORY.md @@ -0,0 +1,114 @@ +# Agent Companies Spec Inventory + +This document indexes every part of the Paperclip codebase that touches the [Agent Companies Specification](docs/companies/companies-spec.md) (`agentcompanies/v1-draft`). + +Use it when you need to: + +1. **Update the spec** — know which implementation code must change in lockstep. +2. **Change code that involves the spec** — find all related files quickly. +3. **Keep things aligned** — audit whether implementation matches the spec. + +--- + +## 1. Specification & Design Documents + +| File | Role | +|---|---| +| `docs/companies/companies-spec.md` | **Normative spec** — defines the markdown-first package format (COMPANY.md, TEAM.md, AGENTS.md, PROJECT.md, TASK.md, SKILL.md), reserved files, frontmatter schemas, and vendor extension conventions (`.paperclip.yaml`). | +| `doc/plans/2026-03-13-company-import-export-v2.md` | Implementation plan for the markdown-first package model cutover — phases, API changes, UI plan, and rollout strategy. | +| `doc/SPEC-implementation.md` | V1 implementation contract; references the portability system and `.paperclip.yaml` sidecar format. | +| `docs/specs/cliphub-plan.md` | Earlier blueprint bundle plan; partially superseded by the markdown-first spec (noted in the v2 plan). | +| `doc/plans/2026-02-16-module-system.md` | Module system plan; JSON-only company template sections superseded by the markdown-first model. | +| `doc/plans/2026-03-14-skills-ui-product-plan.md` | Skills UI plan; references portable skill files and `.paperclip.yaml`. | +| `doc/plans/2026-03-14-adapter-skill-sync-rollout.md` | Adapter skill sync rollout; companion to the v2 import/export plan. | + +## 2. Shared Types & Validators + +These define the contract between server, CLI, and UI. + +| File | What it defines | +|---|---| +| `packages/shared/src/types/company-portability.ts` | TypeScript interfaces: `CompanyPortabilityManifest`, `CompanyPortabilityFileEntry`, `CompanyPortabilityEnvInput`, export/import/preview request and result types, manifest entry types for agents, skills, projects, issues, companies. | +| `packages/shared/src/validators/company-portability.ts` | Zod schemas for all portability request/response shapes — used by both server routes and CLI. | +| `packages/shared/src/types/index.ts` | Re-exports portability types. | +| `packages/shared/src/validators/index.ts` | Re-exports portability validators. | + +## 3. Server — Services + +| File | Responsibility | +|---|---| +| `server/src/services/company-portability.ts` | **Core portability service.** Export (manifest generation, markdown file emission, `.paperclip.yaml` sidecars), import (graph resolution, collision handling, entity creation), preview (planned-action summary). Handles skill key derivation, task recurrence parsing, and package README generation. References `agentcompanies/v1` version string. | +| `server/src/services/company-export-readme.ts` | Generates `README.md` and Mermaid org-chart for exported company packages. | +| `server/src/services/index.ts` | Re-exports `companyPortabilityService`. | + +## 4. Server — Routes + +| File | Endpoints | +|---|---| +| `server/src/routes/companies.ts` | `POST /api/companies/:companyId/export` — legacy export bundle
`POST /api/companies/:companyId/exports/preview` — export preview
`POST /api/companies/:companyId/exports` — export package
`POST /api/companies/import/preview` — import preview
`POST /api/companies/import` — perform import | + +Route registration lives in `server/src/app.ts` via `companyRoutes(db, storage)`. + +## 5. Server — Tests + +| File | Coverage | +|---|---| +| `server/src/__tests__/company-portability.test.ts` | Unit tests for the portability service (export, import, preview, manifest shape, `agentcompanies/v1` version). | +| `server/src/__tests__/company-portability-routes.test.ts` | Integration tests for the portability HTTP endpoints. | + +## 6. CLI + +| File | Commands | +|---|---| +| `cli/src/commands/client/company.ts` | `company export` — exports a company package to disk (flags: `--out`, `--include`, `--projects`, `--issues`, `--projectIssues`).
`company import` — imports a company package from a file or folder (flags: `--from`, `--include`, `--target`, `--companyId`, `--newCompanyName`, `--agents`, `--collision`, `--dryRun`).
Reads/writes portable file entries and handles `.paperclip.yaml` filtering. | + +## 7. UI — Pages + +| File | Role | +|---|---| +| `ui/src/pages/CompanyExport.tsx` | Export UI: preview, manifest display, file tree visualization, ZIP archive creation and download. Filters `.paperclip.yaml` based on selection. Shows manifest and README in editor. | +| `ui/src/pages/CompanyImport.tsx` | Import UI: source input (upload/folder/GitHub URL/generic URL), ZIP reading, preview pane with dependency tree, entity selection checkboxes, trust/licensing warnings, secrets requirements, collision strategy, adapter config. | + +## 8. UI — Components + +| File | Role | +|---|---| +| `ui/src/components/PackageFileTree.tsx` | Reusable file tree component for both import and export. Builds tree from `CompanyPortabilityFileEntry` items, parses frontmatter, shows action indicators (create/update/skip), and maps frontmatter field labels. | + +## 9. UI — Libraries + +| File | Role | +|---|---| +| `ui/src/lib/portable-files.ts` | Helpers for portable file entries: `getPortableFileText`, `getPortableFileDataUrl`, `getPortableFileContentType`, `isPortableImageFile`. | +| `ui/src/lib/zip.ts` | ZIP archive creation (`createZipArchive`) and reading (`readZipArchive`) — implements ZIP format from scratch for company packages. CRC32, DOS date/time encoding. | +| `ui/src/lib/zip.test.ts` | Tests for ZIP utilities; exercises round-trip with portability file entries and `.paperclip.yaml` content. | + +## 10. UI — API Client + +| File | Functions | +|---|---| +| `ui/src/api/companies.ts` | `companiesApi.exportBundle`, `companiesApi.exportPreview`, `companiesApi.exportPackage`, `companiesApi.importPreview`, `companiesApi.importBundle` — typed fetch wrappers for the portability endpoints. | + +## 11. Skills & Agent Instructions + +| File | Relevance | +|---|---| +| `skills/paperclip/references/company-skills.md` | Reference doc for company skill library workflow — install, inspect, update, assign. Skill packages are a subset of the agent companies spec. | +| `server/src/services/company-skills.ts` | Company skill management service — handles SKILL.md-based imports and company-level skill library. | +| `server/src/services/agent-instructions.ts` | Agent instructions service — resolves AGENTS.md paths for agent instruction loading. | + +## 12. Quick Cross-Reference by Spec Concept + +| Spec concept | Primary implementation files | +|---|---| +| `COMPANY.md` frontmatter & body | `company-portability.ts` (export emitter + import parser) | +| `AGENTS.md` frontmatter & body | `company-portability.ts`, `agent-instructions.ts` | +| `PROJECT.md` frontmatter & body | `company-portability.ts` | +| `TASK.md` frontmatter & body | `company-portability.ts` | +| `SKILL.md` packages | `company-portability.ts`, `company-skills.ts` | +| `.paperclip.yaml` vendor sidecar | `company-portability.ts`, `CompanyExport.tsx`, `company.ts` (CLI) | +| `manifest.json` | `company-portability.ts` (generation), shared types (schema) | +| ZIP package format | `zip.ts` (UI), `company.ts` (CLI file I/O) | +| Collision resolution | `company-portability.ts` (server), `CompanyImport.tsx` (UI) | +| Env/secrets declarations | shared types (`CompanyPortabilityEnvInput`), `CompanyImport.tsx` (UI) | +| README + org chart | `company-export-readme.ts` | From cace79631e9b4d0bf57e1df00e07f752fd74400f Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 07:27:20 -0500 Subject: [PATCH 02/46] Move AGENTCOMPANIES_SPEC_INVENTORY.md to doc/ Co-Authored-By: Paperclip --- .../AGENTCOMPANIES_SPEC_INVENTORY.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename AGENTCOMPANIES_SPEC_INVENTORY.md => doc/AGENTCOMPANIES_SPEC_INVENTORY.md (100%) diff --git a/AGENTCOMPANIES_SPEC_INVENTORY.md b/doc/AGENTCOMPANIES_SPEC_INVENTORY.md similarity index 100% rename from AGENTCOMPANIES_SPEC_INVENTORY.md rename to doc/AGENTCOMPANIES_SPEC_INVENTORY.md From 16e221d03cade2e504ec7146b5e02ac418a3546e Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 07:28:26 -0500 Subject: [PATCH 03/46] Update portability tests for binary file entries Co-Authored-By: Paperclip --- .../src/__tests__/company-portability.test.ts | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/server/src/__tests__/company-portability.test.ts b/server/src/__tests__/company-portability.test.ts index af092ef4..5ee3aeca 100644 --- a/server/src/__tests__/company-portability.test.ts +++ b/server/src/__tests__/company-portability.test.ts @@ -1,5 +1,6 @@ import { Readable } from "node:stream"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { CompanyPortabilityFileEntry } from "@paperclipai/shared"; const companySvc = { getById: vi.fn(), @@ -83,6 +84,11 @@ vi.mock("../services/agent-instructions.js", () => ({ const { companyPortabilityService } = await import("../services/company-portability.js"); +function asTextFile(entry: CompanyPortabilityFileEntry | undefined) { + expect(typeof entry).toBe("string"); + return typeof entry === "string" ? entry : ""; +} + describe("company portability", () => { const paperclipKey = "paperclipai/paperclip/paperclip"; const companyPlaybookKey = "company/company-1/company-playbook"; @@ -301,19 +307,19 @@ describe("company portability", () => { }, }); - expect(exported.files["COMPANY.md"]).toContain('name: "Paperclip"'); - expect(exported.files["COMPANY.md"]).toContain('schema: "agentcompanies/v1"'); - expect(exported.files["agents/claudecoder/AGENTS.md"]).toContain("You are ClaudeCoder."); - expect(exported.files["agents/claudecoder/AGENTS.md"]).toContain("skills:"); - expect(exported.files["agents/claudecoder/AGENTS.md"]).toContain(`- "${paperclipKey}"`); - expect(exported.files["agents/cmo/AGENTS.md"]).not.toContain("skills:"); - expect(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"]).toContain("metadata:"); - expect(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"]).toContain('kind: "github-dir"'); + expect(asTextFile(exported.files["COMPANY.md"])).toContain('name: "Paperclip"'); + expect(asTextFile(exported.files["COMPANY.md"])).toContain('schema: "agentcompanies/v1"'); + expect(asTextFile(exported.files["agents/claudecoder/AGENTS.md"])).toContain("You are ClaudeCoder."); + expect(asTextFile(exported.files["agents/claudecoder/AGENTS.md"])).toContain("skills:"); + expect(asTextFile(exported.files["agents/claudecoder/AGENTS.md"])).toContain(`- "${paperclipKey}"`); + expect(asTextFile(exported.files["agents/cmo/AGENTS.md"])).not.toContain("skills:"); + expect(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"])).toContain("metadata:"); + expect(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"])).toContain('kind: "github-dir"'); expect(exported.files["skills/paperclipai/paperclip/paperclip/references/api.md"]).toBeUndefined(); - expect(exported.files["skills/company/PAP/company-playbook/SKILL.md"]).toContain("# Company Playbook"); - expect(exported.files["skills/company/PAP/company-playbook/references/checklist.md"]).toContain("# Checklist"); + expect(asTextFile(exported.files["skills/company/PAP/company-playbook/SKILL.md"])).toContain("# Company Playbook"); + expect(asTextFile(exported.files["skills/company/PAP/company-playbook/references/checklist.md"])).toContain("# Checklist"); - const extension = exported.files[".paperclip.yaml"]; + const extension = asTextFile(exported.files[".paperclip.yaml"]); expect(extension).toContain('schema: "paperclip/v1"'); expect(extension).not.toContain("promptTemplate"); expect(extension).not.toContain("instructionsFilePath"); @@ -345,9 +351,9 @@ describe("company portability", () => { expandReferencedSkills: true, }); - expect(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"]).toContain("# Paperclip"); - expect(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"]).toContain("metadata:"); - expect(exported.files["skills/paperclipai/paperclip/paperclip/references/api.md"]).toContain("# API"); + expect(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"])).toContain("# Paperclip"); + expect(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"])).toContain("metadata:"); + expect(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/references/api.md"])).toContain("# API"); }); it("exports the company logo into images/ and references it from .paperclip.yaml", async () => { @@ -474,9 +480,9 @@ describe("company portability", () => { }, }); - expect(exported.files["skills/local/release-changelog/SKILL.md"]).toContain("# Local Release Changelog"); - expect(exported.files["skills/paperclipai/paperclip/release-changelog/SKILL.md"]).toContain("metadata:"); - expect(exported.files["skills/paperclipai/paperclip/release-changelog/SKILL.md"]).toContain("paperclipai/paperclip/release-changelog"); + expect(asTextFile(exported.files["skills/local/release-changelog/SKILL.md"])).toContain("# Local Release Changelog"); + expect(asTextFile(exported.files["skills/paperclipai/paperclip/release-changelog/SKILL.md"])).toContain("metadata:"); + expect(asTextFile(exported.files["skills/paperclipai/paperclip/release-changelog/SKILL.md"])).toContain("paperclipai/paperclip/release-changelog"); }); it("builds export previews without tasks by default", async () => { From 2f076f2add410b1beba655d5465790daa811b563 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 07:57:25 -0500 Subject: [PATCH 04/46] Default new agents to managed AGENTS.md Co-Authored-By: Paperclip --- .../src/__tests__/agent-skills-routes.test.ts | 88 ++++++++++++++++++- server/src/routes/agents.ts | 47 +++++++++- 2 files changed, 130 insertions(+), 5 deletions(-) diff --git a/server/src/__tests__/agent-skills-routes.test.ts b/server/src/__tests__/agent-skills-routes.test.ts index f0b6c499..8c9101ae 100644 --- a/server/src/__tests__/agent-skills-routes.test.ts +++ b/server/src/__tests__/agent-skills-routes.test.ts @@ -16,7 +16,9 @@ const mockAccessService = vi.hoisted(() => ({ hasPermission: vi.fn(), })); -const mockApprovalService = vi.hoisted(() => ({})); +const mockApprovalService = vi.hoisted(() => ({ + create: vi.fn(), +})); const mockBudgetService = vi.hoisted(() => ({})); const mockHeartbeatService = vi.hoisted(() => ({})); const mockIssueApprovalService = vi.hoisted(() => ({ @@ -176,13 +178,26 @@ describe("agent skill routes", () => { budgetMonthlyCents: Number(input.budgetMonthlyCents ?? 0), permissions: null, })); - mockApprovalService.create = vi.fn(async (_companyId: string, input: Record) => ({ + mockApprovalService.create.mockImplementation(async (_companyId: string, input: Record) => ({ id: "approval-1", companyId: "company-1", type: "hire_agent", status: "pending", payload: input.payload ?? {}, })); + mockAgentInstructionsService.materializeManagedBundle.mockImplementation( + async (agent: Record, files: Record) => ({ + bundle: null, + adapterConfig: { + ...((agent.adapterConfig as Record | undefined) ?? {}), + instructionsBundleMode: "managed", + instructionsRootPath: `/tmp/${String(agent.id)}/instructions`, + instructionsEntryFile: "AGENTS.md", + instructionsFilePath: `/tmp/${String(agent.id)}/instructions/AGENTS.md`, + promptTemplate: files["AGENTS.md"] ?? "", + }, + }), + ); mockLogActivity.mockResolvedValue(undefined); mockAccessService.canUser.mockResolvedValue(true); mockAccessService.hasPermission.mockResolvedValue(true); @@ -289,6 +304,44 @@ describe("agent skill routes", () => { ); }); + it("materializes a managed AGENTS.md for directly created local agents", async () => { + const res = await request(createApp()) + .post("/api/companies/company-1/agents") + .send({ + name: "QA Agent", + role: "engineer", + adapterType: "claude_local", + adapterConfig: { + promptTemplate: "You are QA.", + }, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(201); + expect(mockAgentInstructionsService.materializeManagedBundle).toHaveBeenCalledWith( + expect.objectContaining({ + id: "11111111-1111-4111-8111-111111111111", + adapterType: "claude_local", + }), + { "AGENTS.md": "You are QA." }, + { entryFile: "AGENTS.md", replaceExisting: false }, + ); + expect(mockAgentService.update).toHaveBeenCalledWith( + "11111111-1111-4111-8111-111111111111", + expect.objectContaining({ + adapterConfig: expect.objectContaining({ + instructionsBundleMode: "managed", + instructionsEntryFile: "AGENTS.md", + instructionsFilePath: "/tmp/11111111-1111-4111-8111-111111111111/instructions/AGENTS.md", + }), + }), + ); + expect(mockAgentService.update.mock.calls.at(-1)?.[1]).not.toMatchObject({ + adapterConfig: expect.objectContaining({ + promptTemplate: expect.anything(), + }), + }); + }); + it("includes canonical desired skills in hire approvals", async () => { const db = createDb(true); @@ -316,4 +369,35 @@ describe("agent skill routes", () => { }), ); }); + + it("uses managed AGENTS config in hire approval payloads", async () => { + const res = await request(createApp(createDb(true))) + .post("/api/companies/company-1/agent-hires") + .send({ + name: "QA Agent", + role: "engineer", + adapterType: "claude_local", + adapterConfig: { + promptTemplate: "You are QA.", + }, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(201); + expect(mockApprovalService.create).toHaveBeenCalledWith( + "company-1", + expect.objectContaining({ + payload: expect.objectContaining({ + adapterConfig: expect.objectContaining({ + instructionsBundleMode: "managed", + instructionsEntryFile: "AGENTS.md", + instructionsFilePath: "/tmp/11111111-1111-4111-8111-111111111111/instructions/AGENTS.md", + }), + }), + }), + ); + const approvalInput = mockApprovalService.create.mock.calls.at(-1)?.[1] as + | { payload?: { adapterConfig?: Record } } + | undefined; + expect(approvalInput?.payload?.adapterConfig?.promptTemplate).toBeUndefined(); + }); }); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 80bebc2d..9481840b 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -63,7 +63,9 @@ export function agentRoutes(db: Db) { gemini_local: "instructionsFilePath", opencode_local: "instructionsFilePath", cursor: "instructionsFilePath", + pi_local: "instructionsFilePath", }; + const DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES = new Set(Object.keys(DEFAULT_INSTRUCTIONS_PATH_KEYS)); const KNOWN_INSTRUCTIONS_PATH_KEYS = new Set(["instructionsFilePath", "agentsMdPath"]); const router = Router(); @@ -328,6 +330,43 @@ export function agentRoutes(db: Db) { return path.resolve(cwd, trimmed); } + async function materializeDefaultInstructionsBundleForNewAgent(agent: T): Promise { + if (!DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES.has(agent.adapterType)) { + return agent; + } + + const adapterConfig = asRecord(agent.adapterConfig) ?? {}; + const hasExplicitInstructionsBundle = + Boolean(asNonEmptyString(adapterConfig.instructionsBundleMode)) + || Boolean(asNonEmptyString(adapterConfig.instructionsRootPath)) + || Boolean(asNonEmptyString(adapterConfig.instructionsEntryFile)) + || Boolean(asNonEmptyString(adapterConfig.instructionsFilePath)) + || Boolean(asNonEmptyString(adapterConfig.agentsMdPath)); + if (hasExplicitInstructionsBundle) { + return agent; + } + + const promptTemplate = typeof adapterConfig.promptTemplate === "string" + ? adapterConfig.promptTemplate + : ""; + const materialized = await instructions.materializeManagedBundle( + agent, + { "AGENTS.md": promptTemplate }, + { entryFile: "AGENTS.md", replaceExisting: false }, + ); + const nextAdapterConfig = { ...materialized.adapterConfig }; + delete nextAdapterConfig.promptTemplate; + + const updated = await svc.update(agent.id, { adapterConfig: nextAdapterConfig }); + return (updated as T | null) ?? { ...agent, adapterConfig: nextAdapterConfig }; + } + async function assertCanManageInstructionsPath(req: Request, targetAgent: { id: string; companyId: string }) { assertCompanyAccess(req, targetAgent.companyId); if (req.actor.type === "board") return; @@ -1035,12 +1074,13 @@ export function agentRoutes(db: Db) { const requiresApproval = company.requireBoardApprovalForNewAgents; const status = requiresApproval ? "pending_approval" : "idle"; - const agent = await svc.create(companyId, { + const createdAgent = await svc.create(companyId, { ...normalizedHireInput, status, spentMonthlyCents: 0, lastHeartbeatAt: null, }); + const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent); let approval: Awaited> | null = null; const actor = getActorInfo(req); @@ -1049,7 +1089,7 @@ export function agentRoutes(db: Db) { const requestedAdapterType = normalizedHireInput.adapterType ?? agent.adapterType; const requestedAdapterConfig = redactEventPayload( - (normalizedHireInput.adapterConfig ?? agent.adapterConfig) as Record, + (agent.adapterConfig ?? normalizedHireInput.adapterConfig) as Record, ) ?? {}; const requestedRuntimeConfig = redactEventPayload( @@ -1172,13 +1212,14 @@ export function agentRoutes(db: Db) { normalizedAdapterConfig, ); - const agent = await svc.create(companyId, { + const createdAgent = await svc.create(companyId, { ...createInput, adapterConfig: normalizedAdapterConfig, status: "idle", spentMonthlyCents: 0, lastHeartbeatAt: null, }); + const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent); const actor = getActorInfo(req); await logActivity(db, { From 8edff22c0be2023bd98be01f14ef7c5871849530 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 08:27:39 -0500 Subject: [PATCH 05/46] Add skills section to company export README Lists all skills in a markdown table with name, description, and source. GitHub and URL-sourced skills render as clickable links to their repository. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- server/src/services/company-export-readme.ts | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/server/src/services/company-export-readme.ts b/server/src/services/company-export-readme.ts index 72ebe8b1..83c18fdb 100644 --- a/server/src/services/company-export-readme.ts +++ b/server/src/services/company-export-readme.ts @@ -55,6 +55,19 @@ function mermaidEscape(s: string): string { return s.replace(/"/g, """).replace(//g, ">"); } +/** Build a display label for a skill's source, linking to GitHub when available. */ +function skillSourceLabel(skill: CompanyPortabilityManifest["skills"][number]): string { + if (skill.sourceLocator) { + // For GitHub or URL sources, render as a markdown link + if (skill.sourceType === "github" || skill.sourceType === "url") { + return `[${skill.sourceType}](${skill.sourceLocator})`; + } + return skill.sourceLocator; + } + if (skill.sourceType === "local") return "local"; + return skill.sourceType ?? "\u2014"; +} + /** * Generate the README.md content for a company export. */ @@ -127,6 +140,20 @@ export function generateReadme( lines.push(""); } + // Skills list + if (manifest.skills.length > 0) { + lines.push("### Skills"); + lines.push(""); + lines.push("| Skill | Description | Source |"); + lines.push("|-------|-------------|--------|"); + for (const skill of manifest.skills) { + const desc = skill.description ?? "\u2014"; + const source = skillSourceLabel(skill); + lines.push(`| ${skill.name} | ${desc} | ${source} |`); + } + lines.push(""); + } + // Getting Started lines.push("## Getting Started"); lines.push(""); From ce69ebd2ec4eafdf81e222ef6fa2d694517d8035 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 12:05:27 -0500 Subject: [PATCH 06/46] Add DELETE endpoint for company skills and fix skills.sh URL resolution - Add DELETE /api/companies/:companyId/skills/:skillId endpoint with same permission model as other skill mutations. Deleting a skill removes it from the DB, cleans up materialized runtime files, and automatically strips it from any agent desiredSkills that reference it. - Fix parseSkillImportSourceInput to detect skills.sh URLs (e.g. https://skills.sh/org/repo/skill) and resolve them to the underlying GitHub repo + skill slug, instead of fetching the HTML page. - Add tests for skills.sh URL resolution with and without skill slug. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- server/src/__tests__/company-skills.test.ts | 18 +++++++ server/src/routes/company-skills.ts | 29 +++++++++++ server/src/services/company-skills.ts | 56 ++++++++++++++++++++- 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/company-skills.test.ts b/server/src/__tests__/company-skills.test.ts index 362ae65c..73afd751 100644 --- a/server/src/__tests__/company-skills.test.ts +++ b/server/src/__tests__/company-skills.test.ts @@ -45,6 +45,24 @@ describe("company skill import source parsing", () => { expect(parsed.requestedSkillSlug).toBe("find-skills"); }); + it("resolves skills.sh URL with org/repo/skill to GitHub repo", () => { + const parsed = parseSkillImportSourceInput( + "https://skills.sh/google-labs-code/stitch-skills/design-md", + ); + + expect(parsed.resolvedSource).toBe("https://github.com/google-labs-code/stitch-skills"); + expect(parsed.requestedSkillSlug).toBe("design-md"); + }); + + it("resolves skills.sh URL with org/repo (no skill) to GitHub repo", () => { + const parsed = parseSkillImportSourceInput( + "https://skills.sh/vercel-labs/skills", + ); + + expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills"); + expect(parsed.requestedSkillSlug).toBeNull(); + }); + it("parses skills.sh commands whose requested skill differs from the folder name", () => { const parsed = parseSkillImportSourceInput( "npx skills add https://github.com/remotion-dev/skills --skill remotion-best-practices", diff --git a/server/src/routes/company-skills.ts b/server/src/routes/company-skills.ts index c8de035b..7b239832 100644 --- a/server/src/routes/company-skills.ts +++ b/server/src/routes/company-skills.ts @@ -221,6 +221,35 @@ export function companySkillRoutes(db: Db) { }, ); + router.delete("/companies/:companyId/skills/:skillId", async (req, res) => { + const companyId = req.params.companyId as string; + const skillId = req.params.skillId as string; + await assertCanMutateCompanySkills(req, companyId); + const result = await svc.deleteSkill(companyId, skillId); + if (!result) { + res.status(404).json({ error: "Skill not found" }); + return; + } + + const actor = getActorInfo(req); + await logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "company.skill_deleted", + entityType: "company_skill", + entityId: result.id, + details: { + slug: result.slug, + name: result.name, + }, + }); + + res.json(result); + }); + router.post("/companies/:companyId/skills/:skillId/install-update", async (req, res) => { const companyId = req.params.companyId as string; const skillId = req.params.skillId as string; diff --git a/server/src/services/company-skills.ts b/server/src/services/company-skills.ts index 87b7c11c..97d8bbf0 100644 --- a/server/src/services/company-skills.ts +++ b/server/src/services/company-skills.ts @@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url"; import { and, asc, eq } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { companySkills } from "@paperclipai/db"; -import { readPaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils"; +import { readPaperclipSkillSyncPreference, writePaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils"; import type { PaperclipSkillEntry } from "@paperclipai/adapter-utils/server-utils"; import type { CompanySkill, @@ -578,6 +578,17 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport }; } + // Detect skills.sh URLs and resolve to GitHub: https://skills.sh/org/repo/skill → org/repo/skill key + const skillsShMatch = normalizedSource.match(/^https?:\/\/(?:www\.)?skills\.sh\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:\/([A-Za-z0-9_.-]+))?(?:[?#].*)?$/i); + if (skillsShMatch) { + const [, owner, repo, skillSlugRaw] = skillsShMatch; + return { + resolvedSource: `https://github.com/${owner}/${repo}`, + requestedSkillSlug: skillSlugRaw ? normalizeSkillSlug(skillSlugRaw) : requestedSkillSlug, + warnings, + }; + } + return { resolvedSource: normalizedSource, requestedSkillSlug, @@ -2195,6 +2206,48 @@ export function companySkillService(db: Db) { return { imported, warnings }; } + async function deleteSkill(companyId: string, skillId: string): Promise { + const row = await db + .select() + .from(companySkills) + .where(and(eq(companySkills.id, skillId), eq(companySkills.companyId, companyId))) + .then((rows) => rows[0] ?? null); + if (!row) return null; + + const skill = toCompanySkill(row); + + // Remove from any agent desiredSkills that reference this skill + const agentRows = await agents.list(companyId); + const allSkills = await listFull(companyId); + for (const agent of agentRows) { + const config = agent.adapterConfig as Record; + const preference = readPaperclipSkillSyncPreference(config); + const referencesSkill = preference.desiredSkills.some((ref) => { + const resolved = resolveSkillReference(allSkills, ref); + return resolved.skill?.id === skillId; + }); + if (referencesSkill) { + const filtered = preference.desiredSkills.filter((ref) => { + const resolved = resolveSkillReference(allSkills, ref); + return resolved.skill?.id !== skillId; + }); + await agents.update(agent.id, { + adapterConfig: writePaperclipSkillSyncPreference(config, filtered), + }); + } + } + + // Delete DB row + await db + .delete(companySkills) + .where(eq(companySkills.id, skillId)); + + // Clean up materialized runtime files + await fs.rm(resolveRuntimeSkillMaterializedPath(companyId, skill), { recursive: true, force: true }); + + return skill; + } + return { list, listFull, @@ -2209,6 +2262,7 @@ export function companySkillService(db: Db) { readFile, updateFile, createLocalSkill, + deleteSkill, importFromSource, scanProjectWorkspaces, importPackageFiles, From ca3fdb3957880674ec790e48067f6d2b974fc4ce Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 14:15:35 -0500 Subject: [PATCH 07/46] Set sourceType to skills_sh for skills imported from skills.sh URLs When skills are imported via skills.sh URLs or key-style imports (org/repo/skill), the stored sourceType is now "skills_sh" with the original skills.sh URL as sourceLocator, instead of "github" with the resolved GitHub URL. - Add "skills_sh" to CompanySkillSourceType and CompanySkillSourceBadge - Track originalSkillsShUrl in parseSkillImportSourceInput - Override sourceType/sourceLocator in importFromSource for skills.sh - Handle skills_sh in key derivation, source info, update checks, file reads, portability export, and UI badge rendering Co-Authored-By: Paperclip --- packages/shared/src/types/company-skill.ts | 4 +-- .../shared/src/validators/company-skill.ts | 4 +-- server/src/__tests__/company-skills.test.ts | 18 ++++++++-- server/src/services/company-export-readme.ts | 2 +- server/src/services/company-portability.ts | 10 +++--- server/src/services/company-skills.ts | 35 +++++++++++++++++-- ui/src/pages/CompanySkills.tsx | 2 ++ 7 files changed, 59 insertions(+), 16 deletions(-) diff --git a/packages/shared/src/types/company-skill.ts b/packages/shared/src/types/company-skill.ts index 5d6ed598..12834083 100644 --- a/packages/shared/src/types/company-skill.ts +++ b/packages/shared/src/types/company-skill.ts @@ -1,10 +1,10 @@ -export type CompanySkillSourceType = "local_path" | "github" | "url" | "catalog"; +export type CompanySkillSourceType = "local_path" | "github" | "url" | "catalog" | "skills_sh"; export type CompanySkillTrustLevel = "markdown_only" | "assets" | "scripts_executables"; export type CompanySkillCompatibility = "compatible" | "unknown" | "invalid"; -export type CompanySkillSourceBadge = "paperclip" | "github" | "local" | "url" | "catalog"; +export type CompanySkillSourceBadge = "paperclip" | "github" | "local" | "url" | "catalog" | "skills_sh"; export interface CompanySkillFileInventoryEntry { path: string; diff --git a/packages/shared/src/validators/company-skill.ts b/packages/shared/src/validators/company-skill.ts index 15bd4e2a..7f1df34b 100644 --- a/packages/shared/src/validators/company-skill.ts +++ b/packages/shared/src/validators/company-skill.ts @@ -1,9 +1,9 @@ import { z } from "zod"; -export const companySkillSourceTypeSchema = z.enum(["local_path", "github", "url", "catalog"]); +export const companySkillSourceTypeSchema = z.enum(["local_path", "github", "url", "catalog", "skills_sh"]); export const companySkillTrustLevelSchema = z.enum(["markdown_only", "assets", "scripts_executables"]); export const companySkillCompatibilitySchema = z.enum(["compatible", "unknown", "invalid"]); -export const companySkillSourceBadgeSchema = z.enum(["paperclip", "github", "local", "url", "catalog"]); +export const companySkillSourceBadgeSchema = z.enum(["paperclip", "github", "local", "url", "catalog", "skills_sh"]); export const companySkillFileInventoryEntrySchema = z.object({ path: z.string().min(1), diff --git a/server/src/__tests__/company-skills.test.ts b/server/src/__tests__/company-skills.test.ts index 73afd751..77fd072e 100644 --- a/server/src/__tests__/company-skills.test.ts +++ b/server/src/__tests__/company-skills.test.ts @@ -35,32 +35,36 @@ describe("company skill import source parsing", () => { expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills"); expect(parsed.requestedSkillSlug).toBe("find-skills"); + expect(parsed.originalSkillsShUrl).toBeNull(); expect(parsed.warnings).toEqual([]); }); - it("parses owner/repo/skill shorthand as a GitHub repo plus requested skill", () => { + it("parses owner/repo/skill shorthand as skills.sh-managed", () => { const parsed = parseSkillImportSourceInput("vercel-labs/skills/find-skills"); expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills"); expect(parsed.requestedSkillSlug).toBe("find-skills"); + expect(parsed.originalSkillsShUrl).toBe("https://skills.sh/vercel-labs/skills/find-skills"); }); - it("resolves skills.sh URL with org/repo/skill to GitHub repo", () => { + it("resolves skills.sh URL with org/repo/skill to GitHub repo and preserves original URL", () => { const parsed = parseSkillImportSourceInput( "https://skills.sh/google-labs-code/stitch-skills/design-md", ); expect(parsed.resolvedSource).toBe("https://github.com/google-labs-code/stitch-skills"); expect(parsed.requestedSkillSlug).toBe("design-md"); + expect(parsed.originalSkillsShUrl).toBe("https://skills.sh/google-labs-code/stitch-skills/design-md"); }); - it("resolves skills.sh URL with org/repo (no skill) to GitHub repo", () => { + it("resolves skills.sh URL with org/repo (no skill) to GitHub repo and preserves original URL", () => { const parsed = parseSkillImportSourceInput( "https://skills.sh/vercel-labs/skills", ); expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills"); expect(parsed.requestedSkillSlug).toBeNull(); + expect(parsed.originalSkillsShUrl).toBe("https://skills.sh/vercel-labs/skills"); }); it("parses skills.sh commands whose requested skill differs from the folder name", () => { @@ -70,6 +74,14 @@ describe("company skill import source parsing", () => { expect(parsed.resolvedSource).toBe("https://github.com/remotion-dev/skills"); expect(parsed.requestedSkillSlug).toBe("remotion-best-practices"); + expect(parsed.originalSkillsShUrl).toBeNull(); + }); + + it("does not set originalSkillsShUrl for owner/repo shorthand", () => { + const parsed = parseSkillImportSourceInput("vercel-labs/skills"); + + expect(parsed.resolvedSource).toBe("https://github.com/vercel-labs/skills"); + expect(parsed.originalSkillsShUrl).toBeNull(); }); }); diff --git a/server/src/services/company-export-readme.ts b/server/src/services/company-export-readme.ts index 83c18fdb..56725024 100644 --- a/server/src/services/company-export-readme.ts +++ b/server/src/services/company-export-readme.ts @@ -59,7 +59,7 @@ function mermaidEscape(s: string): string { function skillSourceLabel(skill: CompanyPortabilityManifest["skills"][number]): string { if (skill.sourceLocator) { // For GitHub or URL sources, render as a markdown link - if (skill.sourceType === "github" || skill.sourceType === "url") { + if (skill.sourceType === "github" || skill.sourceType === "skills_sh" || skill.sourceType === "url") { return `[${skill.sourceType}](${skill.sourceLocator})`; } return skill.sourceLocator; diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index 828f68f2..5a91efeb 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -119,7 +119,7 @@ function deriveManifestSkillKey( const sourceKind = asString(metadata?.sourceKind); const owner = normalizeSkillSlug(asString(metadata?.owner)); const repo = normalizeSkillSlug(asString(metadata?.repo)); - if ((sourceType === "github" || sourceKind === "github") && owner && repo) { + if ((sourceType === "github" || sourceType === "skills_sh" || sourceKind === "github" || sourceKind === "skills_sh") && owner && repo) { return `${owner}/${repo}/${slug}`; } if (sourceKind === "paperclip_bundled") { @@ -246,10 +246,10 @@ function deriveSkillExportDirCandidates( pushSuffix("paperclip"); } - if (skill.sourceType === "github") { + if (skill.sourceType === "github" || skill.sourceType === "skills_sh") { pushSuffix(asString(metadata?.repo)); pushSuffix(asString(metadata?.owner)); - pushSuffix("github"); + pushSuffix(skill.sourceType === "skills_sh" ? "skills_sh" : "github"); } else if (skill.sourceType === "url") { try { pushSuffix(skill.sourceLocator ? new URL(skill.sourceLocator).host : null); @@ -1178,7 +1178,7 @@ async function buildSkillSourceEntry(skill: CompanySkill) { }; } - if (skill.sourceType === "github") { + if (skill.sourceType === "github" || skill.sourceType === "skills_sh") { const owner = asString(metadata?.owner); const repo = asString(metadata?.repo); const repoSkillDir = asString(metadata?.repoSkillDir); @@ -1207,7 +1207,7 @@ function shouldReferenceSkillOnExport(skill: CompanySkill, expandReferencedSkill if (expandReferencedSkills) return false; const metadata = isPlainRecord(skill.metadata) ? skill.metadata : null; if (asString(metadata?.sourceKind) === "paperclip_bundled") return true; - return skill.sourceType === "github" || skill.sourceType === "url"; + return skill.sourceType === "github" || skill.sourceType === "skills_sh" || skill.sourceType === "url"; } async function buildReferencedSkillMarkdown(skill: CompanySkill) { diff --git a/server/src/services/company-skills.ts b/server/src/services/company-skills.ts index 97d8bbf0..c927e3b7 100644 --- a/server/src/services/company-skills.ts +++ b/server/src/services/company-skills.ts @@ -66,6 +66,7 @@ export type ImportPackageSkillResult = { type ParsedSkillImportSource = { resolvedSource: string; requestedSkillSlug: string | null; + originalSkillsShUrl: string | null; warnings: string[]; }; @@ -251,7 +252,7 @@ function deriveCanonicalSkillKey( const owner = normalizeSkillSlug(asString(metadata?.owner)); const repo = normalizeSkillSlug(asString(metadata?.repo)); - if ((input.sourceType === "github" || sourceKind === "github") && owner && repo) { + if ((input.sourceType === "github" || input.sourceType === "skills_sh" || sourceKind === "github" || sourceKind === "skills_sh") && owner && repo) { return `${owner}/${repo}/${slug}`; } @@ -561,11 +562,13 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport throw unprocessable("Skill source is required."); } + // Key-style imports (org/repo/skill) originate from the skills.sh registry if (!/^https?:\/\//i.test(normalizedSource) && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalizedSource)) { const [owner, repo, skillSlugRaw] = normalizedSource.split("/"); return { resolvedSource: `https://github.com/${owner}/${repo}`, requestedSkillSlug: normalizeSkillSlug(skillSlugRaw), + originalSkillsShUrl: `https://skills.sh/${owner}/${repo}/${skillSlugRaw}`, warnings, }; } @@ -574,6 +577,7 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport return { resolvedSource: `https://github.com/${normalizedSource}`, requestedSkillSlug, + originalSkillsShUrl: null, warnings, }; } @@ -585,6 +589,7 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport return { resolvedSource: `https://github.com/${owner}/${repo}`, requestedSkillSlug: skillSlugRaw ? normalizeSkillSlug(skillSlugRaw) : requestedSkillSlug, + originalSkillsShUrl: normalizedSource, warnings, }; } @@ -592,6 +597,7 @@ export function parseSkillImportSourceInput(rawInput: string): ParsedSkillImport return { resolvedSource: normalizedSource, requestedSkillSlug, + originalSkillsShUrl: null, warnings, }; } @@ -1292,6 +1298,18 @@ function deriveSkillSourceInfo(skill: CompanySkill): { }; } + if (skill.sourceType === "skills_sh") { + const owner = asString(metadata.owner) ?? null; + const repo = asString(metadata.repo) ?? null; + return { + editable: false, + editableReason: "Skills.sh-managed skills are read-only.", + sourceLabel: skill.sourceLocator ?? (owner && repo ? `${owner}/${repo}` : null), + sourceBadge: "skills_sh", + sourcePath: null, + }; + } + if (skill.sourceType === "github") { const owner = asString(metadata.owner) ?? null; const repo = asString(metadata.repo) ?? null; @@ -1543,7 +1561,7 @@ export function companySkillService(db: Db) { const skill = await getById(skillId); if (!skill || skill.companyId !== companyId) return null; - if (skill.sourceType !== "github") { + if (skill.sourceType !== "github" && skill.sourceType !== "skills_sh") { return { supported: false, reason: "Only GitHub-managed skills support update checks.", @@ -1603,7 +1621,7 @@ export function companySkillService(db: Db) { } else { throw notFound("Skill file not found"); } - } else if (skill.sourceType === "github") { + } else if (skill.sourceType === "github" || skill.sourceType === "skills_sh") { const metadata = getSkillMeta(skill); const owner = asString(metadata.owner); const repo = asString(metadata.repo); @@ -2202,6 +2220,17 @@ export function companySkillService(db: Db) { : "No skills were found in the provided source.", ); } + // Override sourceType/sourceLocator for skills imported via skills.sh + if (parsed.originalSkillsShUrl) { + for (const skill of filteredSkills) { + skill.sourceType = "skills_sh"; + skill.sourceLocator = parsed.originalSkillsShUrl; + if (skill.metadata) { + (skill.metadata as Record).sourceKind = "skills_sh"; + } + skill.key = deriveCanonicalSkillKey(companyId, skill); + } + } const imported = await upsertImportedSkills(companyId, filteredSkills); return { imported, warnings }; } diff --git a/ui/src/pages/CompanySkills.tsx b/ui/src/pages/CompanySkills.tsx index afeeb01f..11854cfe 100644 --- a/ui/src/pages/CompanySkills.tsx +++ b/ui/src/pages/CompanySkills.tsx @@ -148,6 +148,8 @@ function sourceMeta(sourceBadge: CompanySkillSourceBadge, sourceLabel: string | normalizedLabel.includes("skills.sh") || normalizedLabel.includes("vercel-labs/skills"); switch (sourceBadge) { + case "skills_sh": + return { icon: VercelMark, label: sourceLabel ?? "skills.sh", managedLabel: "skills.sh managed" }; case "github": return isSkillsShManaged ? { icon: VercelMark, label: sourceLabel ?? "skills.sh", managedLabel: "skills.sh managed" } From 531945cfe2c5a420c143d442de2ab58e71024685 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 16:29:11 -0500 Subject: [PATCH 08/46] Add --skills flag to company export CLI and fix unsupported URL import path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add first-class --skills option to `paperclipai company export`, passing through to the existing service support for skill selection - Remove broken `type: "url"` source branch from import command — the shared schema and server only accept `inline | github`, so non-GitHub HTTP URLs now error clearly instead of failing at validation - Export isHttpUrl/isGithubUrl helpers for testability - Add server tests for skills-filtered export (selected + fallback) - Add CLI tests for URL detection helpers Co-Authored-By: Paperclip --- cli/src/__tests__/company-import-url.test.ts | 31 ++++++++++++++++ cli/src/commands/client/company.ts | 18 ++++++---- .../src/__tests__/company-portability.test.ts | 36 +++++++++++++++++++ 3 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 cli/src/__tests__/company-import-url.test.ts diff --git a/cli/src/__tests__/company-import-url.test.ts b/cli/src/__tests__/company-import-url.test.ts new file mode 100644 index 00000000..a749d57e --- /dev/null +++ b/cli/src/__tests__/company-import-url.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { isHttpUrl, isGithubUrl } from "../commands/client/company.js"; + +describe("isHttpUrl", () => { + it("matches http URLs", () => { + expect(isHttpUrl("http://example.com/foo")).toBe(true); + }); + + it("matches https URLs", () => { + expect(isHttpUrl("https://example.com/foo")).toBe(true); + }); + + it("rejects local paths", () => { + expect(isHttpUrl("/tmp/my-company")).toBe(false); + expect(isHttpUrl("./relative")).toBe(false); + }); +}); + +describe("isGithubUrl", () => { + it("matches GitHub URLs", () => { + expect(isGithubUrl("https://github.com/org/repo")).toBe(true); + }); + + it("rejects non-GitHub HTTP URLs", () => { + expect(isGithubUrl("https://example.com/foo")).toBe(false); + }); + + it("rejects local paths", () => { + expect(isGithubUrl("/tmp/my-company")).toBe(false); + }); +}); diff --git a/cli/src/commands/client/company.ts b/cli/src/commands/client/company.ts index 05cba06e..9e563387 100644 --- a/cli/src/commands/client/company.ts +++ b/cli/src/commands/client/company.ts @@ -34,6 +34,7 @@ interface CompanyDeleteOptions extends BaseClientOptions { interface CompanyExportOptions extends BaseClientOptions { out?: string; include?: string; + skills?: string; projects?: string; issues?: string; projectIssues?: string; @@ -112,11 +113,11 @@ function parseCsvValues(input: string | undefined): string[] { return Array.from(new Set(input.split(",").map((part) => part.trim()).filter(Boolean))); } -function isHttpUrl(input: string): boolean { +export function isHttpUrl(input: string): boolean { return /^https?:\/\//i.test(input.trim()); } -function isGithubUrl(input: string): boolean { +export function isGithubUrl(input: string): boolean { return /^https?:\/\/github\.com\//i.test(input.trim()); } @@ -337,6 +338,7 @@ export function registerCompanyCommands(program: Command): void { .argument("", "Company ID") .requiredOption("--out ", "Output directory") .option("--include ", "Comma-separated include set: company,agents,projects,issues", "company,agents") + .option("--skills ", "Comma-separated skill slugs/keys to export") .option("--projects ", "Comma-separated project shortnames/ids to export") .option("--issues ", "Comma-separated issue identifiers/ids to export") .option("--project-issues ", "Comma-separated project shortnames/ids whose issues should be exported") @@ -349,6 +351,7 @@ export function registerCompanyCommands(program: Command): void { `/api/companies/${companyId}/export`, { include, + skills: parseCsvValues(opts.skills), projects: parseCsvValues(opts.projects), issues: parseCsvValues(opts.issues), projectIssues: parseCsvValues(opts.projectIssues), @@ -433,13 +436,16 @@ export function registerCompanyCommands(program: Command): void { let sourcePayload: | { type: "inline"; rootPath?: string | null; files: Record } - | { type: "url"; url: string } | { type: "github"; url: string }; if (isHttpUrl(from)) { - sourcePayload = isGithubUrl(from) - ? { type: "github", url: from } - : { type: "url", url: from }; + if (!isGithubUrl(from)) { + throw new Error( + "Only GitHub URLs and local paths are supported for import. " + + "Generic HTTP URLs are not supported. Use a GitHub URL (https://github.com/...) or a local directory path.", + ); + } + sourcePayload = { type: "github", url: from }; } else { const inline = await resolveInlineSourceFromPath(from); sourcePayload = { diff --git a/server/src/__tests__/company-portability.test.ts b/server/src/__tests__/company-portability.test.ts index 5ee3aeca..a24775b4 100644 --- a/server/src/__tests__/company-portability.test.ts +++ b/server/src/__tests__/company-portability.test.ts @@ -356,6 +356,42 @@ describe("company portability", () => { expect(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/references/api.md"])).toContain("# API"); }); + it("exports only selected skills when skills filter is provided", async () => { + const portability = companyPortabilityService({} as any); + + const exported = await portability.exportBundle("company-1", { + include: { + company: true, + agents: true, + projects: false, + issues: false, + }, + skills: ["company-playbook"], + }); + + expect(exported.files["skills/company/PAP/company-playbook/SKILL.md"]).toBeDefined(); + expect(asTextFile(exported.files["skills/company/PAP/company-playbook/SKILL.md"])).toContain("# Company Playbook"); + expect(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"]).toBeUndefined(); + }); + + it("warns and exports all skills when skills filter matches nothing", async () => { + const portability = companyPortabilityService({} as any); + + const exported = await portability.exportBundle("company-1", { + include: { + company: true, + agents: true, + projects: false, + issues: false, + }, + skills: ["nonexistent-skill"], + }); + + expect(exported.warnings).toContainEqual(expect.stringContaining("nonexistent-skill")); + expect(exported.files["skills/company/PAP/company-playbook/SKILL.md"]).toBeDefined(); + expect(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"]).toBeDefined(); + }); + it("exports the company logo into images/ and references it from .paperclip.yaml", async () => { const storage = { getObject: vi.fn().mockResolvedValue({ From a9802c19621fb77dea19c11caaadfc4f6667e502 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 16:33:30 -0500 Subject: [PATCH 09/46] Resolve relative image paths in export/import markdown viewer The MarkdownBody component now accepts an optional resolveImageSrc callback that maps relative image paths (like images/org-chart.png) to base64 data URLs from the portable file entries. This fixes the export README showing a broken image instead of the org chart PNG. Applied to both CompanyExport and CompanyImport preview panes. Co-Authored-By: Paperclip --- ui/src/components/MarkdownBody.tsx | 10 +++++++++- ui/src/pages/CompanyExport.tsx | 22 +++++++++++++++++++--- ui/src/pages/CompanyImport.tsx | 19 +++++++++++++++++-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx index 683adc53..06845527 100644 --- a/ui/src/components/MarkdownBody.tsx +++ b/ui/src/components/MarkdownBody.tsx @@ -8,6 +8,8 @@ import { useTheme } from "../context/ThemeContext"; interface MarkdownBodyProps { children: string; className?: string; + /** Optional resolver for relative image paths (e.g. within export packages) */ + resolveImageSrc?: (src: string) => string | null; } let mermaidLoaderPromise: Promise | null = null; @@ -112,7 +114,7 @@ function MermaidDiagramBlock({ source, darkMode }: { source: string; darkMode: b ); } -export function MarkdownBody({ children, className }: MarkdownBodyProps) { +export function MarkdownBody({ children, className, resolveImageSrc }: MarkdownBodyProps) { const { theme } = useTheme(); return (
); }, + img: resolveImageSrc + ? ({ src, alt, ...imgProps }) => { + const resolved = src ? resolveImageSrc(src) : null; + return {alt; + } + : undefined, }} > {children} diff --git a/ui/src/pages/CompanyExport.tsx b/ui/src/pages/CompanyExport.tsx index 200ce50e..13eda7fc 100644 --- a/ui/src/pages/CompanyExport.tsx +++ b/ui/src/pages/CompanyExport.tsx @@ -470,10 +470,12 @@ function generateReadmeFromSelection( function ExportPreviewPane({ selectedFile, content, + allFiles, onSkillClick, }: { selectedFile: string | null; content: CompanyPortabilityFileEntry | null; + allFiles: Record; onSkillClick?: (skill: string) => void; }) { if (!selectedFile || content === null) { @@ -487,6 +489,20 @@ function ExportPreviewPane({ const parsed = isMarkdown && textContent ? parseFrontmatter(textContent) : null; const imageSrc = isPortableImageFile(selectedFile, content) ? getPortableFileDataUrl(selectedFile, content) : null; + // Resolve relative image paths within the export package (e.g. images/org-chart.png) + const resolveImageSrc = isMarkdown + ? (src: string) => { + // Skip absolute URLs and data URIs + if (/^(?:https?:|data:)/i.test(src)) return null; + // Resolve relative to the directory of the current markdown file + const dir = selectedFile.includes("/") ? selectedFile.slice(0, selectedFile.lastIndexOf("/") + 1) : ""; + const resolved = dir + src; + const entry = allFiles[resolved] ?? allFiles[src]; + if (!entry) return null; + return getPortableFileDataUrl(resolved in allFiles ? resolved : src, entry); + } + : undefined; + return (
@@ -496,10 +512,10 @@ function ExportPreviewPane({ {parsed ? ( <> - {parsed.body.trim() && {parsed.body}} + {parsed.body.trim() && {parsed.body}} ) : isMarkdown ? ( - {textContent ?? ""} + {textContent ?? ""} ) : imageSrc ? (
{selectedFile} @@ -924,7 +940,7 @@ export function CompanyExport() {
- +
diff --git a/ui/src/pages/CompanyImport.tsx b/ui/src/pages/CompanyImport.tsx index 20220dab..10b00266 100644 --- a/ui/src/pages/CompanyImport.tsx +++ b/ui/src/pages/CompanyImport.tsx @@ -177,11 +177,13 @@ function importFileRowClassName(_node: FileTreeNode, checked: boolean) { function ImportPreviewPane({ selectedFile, content, + allFiles, action, renamedTo, }: { selectedFile: string | null; content: CompanyPortabilityFileEntry | null; + allFiles: Record; action: string | null; renamedTo: string | null; }) { @@ -197,6 +199,18 @@ function ImportPreviewPane({ const imageSrc = isPortableImageFile(selectedFile, content) ? getPortableFileDataUrl(selectedFile, content) : null; const actionColor = action ? (ACTION_COLORS[action] ?? ACTION_COLORS.skip) : ""; + // Resolve relative image paths within the import package + const resolveImageSrc = isMarkdown + ? (src: string) => { + if (/^(?:https?:|data:)/i.test(src)) return null; + const dir = selectedFile.includes("/") ? selectedFile.slice(0, selectedFile.lastIndexOf("/") + 1) : ""; + const resolved = dir + src; + const entry = allFiles[resolved] ?? allFiles[src]; + if (!entry) return null; + return getPortableFileDataUrl(resolved in allFiles ? resolved : src, entry); + } + : undefined; + return (
@@ -223,10 +237,10 @@ function ImportPreviewPane({ {parsed ? ( <> - {parsed.body.trim() && {parsed.body}} + {parsed.body.trim() && {parsed.body}} ) : isMarkdown ? ( - {textContent ?? ""} + {textContent ?? ""} ) : imageSrc ? (
{selectedFile} @@ -1265,6 +1279,7 @@ export function CompanyImport() { From f7c766ff32424924d0fecdd3a75e4608e017e06c Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 16:41:28 -0500 Subject: [PATCH 10/46] Fix markdown image rendering without resolver Co-Authored-By: Paperclip --- ui/src/components/MarkdownBody.test.tsx | 31 ++++++++++ ui/src/components/MarkdownBody.tsx | 77 ++++++++++++------------- 2 files changed, 69 insertions(+), 39 deletions(-) create mode 100644 ui/src/components/MarkdownBody.test.tsx diff --git a/ui/src/components/MarkdownBody.test.tsx b/ui/src/components/MarkdownBody.test.tsx new file mode 100644 index 00000000..06cfc70a --- /dev/null +++ b/ui/src/components/MarkdownBody.test.tsx @@ -0,0 +1,31 @@ +// @vitest-environment node + +import { describe, expect, it } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import { ThemeProvider } from "../context/ThemeContext"; +import { MarkdownBody } from "./MarkdownBody"; + +describe("MarkdownBody", () => { + it("renders markdown images without a resolver", () => { + const html = renderToStaticMarkup( + + {"![](/api/attachments/test/content)"} + , + ); + + expect(html).toContain(''); + }); + + it("resolves relative image paths when a resolver is provided", () => { + const html = renderToStaticMarkup( + + `/resolved/${src}`}> + {"![Org chart](images/org-chart.png)"} + + , + ); + + expect(html).toContain('src="/resolved/images/org-chart.png"'); + expect(html).toContain('alt="Org chart"'); + }); +}); diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx index 06845527..0fbb52c4 100644 --- a/ui/src/components/MarkdownBody.tsx +++ b/ui/src/components/MarkdownBody.tsx @@ -1,5 +1,5 @@ import { isValidElement, useEffect, useId, useState, type CSSProperties, type ReactNode } from "react"; -import Markdown from "react-markdown"; +import Markdown, { type Components } from "react-markdown"; import remarkGfm from "remark-gfm"; import { parseProjectMentionHref } from "@paperclipai/shared"; import { cn } from "../lib/utils"; @@ -116,6 +116,42 @@ function MermaidDiagramBlock({ source, darkMode }: { source: string; darkMode: b export function MarkdownBody({ children, className, resolveImageSrc }: MarkdownBodyProps) { const { theme } = useTheme(); + const components: Components = { + pre: ({ node: _node, children: preChildren, ...preProps }) => { + const mermaidSource = extractMermaidSource(preChildren); + if (mermaidSource) { + return ; + } + return
{preChildren}
; + }, + a: ({ href, children: linkChildren }) => { + const parsed = href ? parseProjectMentionHref(href) : null; + if (parsed) { + const label = linkChildren; + return ( + + {label} + + ); + } + return ( + + {linkChildren} + + ); + }, + }; + if (resolveImageSrc) { + components.img = ({ node: _node, src, alt, ...imgProps }) => { + const resolved = src ? resolveImageSrc(src) : null; + return {alt; + }; + } + return (
- { - const mermaidSource = extractMermaidSource(preChildren); - if (mermaidSource) { - return ; - } - return
{preChildren}
; - }, - a: ({ href, children: linkChildren }) => { - const parsed = href ? parseProjectMentionHref(href) : null; - if (parsed) { - const label = linkChildren; - return ( - - {label} - - ); - } - return ( - - {linkChildren} - - ); - }, - img: resolveImageSrc - ? ({ src, alt, ...imgProps }) => { - const resolved = src ? resolveImageSrc(src) : null; - return {alt; - } - : undefined, - }} - > + {children}
From c39758a1693f75bad0fb412610a83001dd87c0db Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 17:23:27 -0500 Subject: [PATCH 11/46] Replace Mermaid org chart with PNG image in export preview The frontend generateReadmeFromSelection() was building an inline Mermaid diagram for the org chart. The server already generates a PNG at images/org-chart.png, so the preview should reference it the same way. Removed dead mermaidId/mermaidEscape/generateOrgChartMermaid helpers. Co-Authored-By: Paperclip --- ui/src/pages/CompanyExport.tsx | 37 +++------------------------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/ui/src/pages/CompanyExport.tsx b/ui/src/pages/CompanyExport.tsx index 13eda7fc..3c4f5930 100644 --- a/ui/src/pages/CompanyExport.tsx +++ b/ui/src/pages/CompanyExport.tsx @@ -343,36 +343,6 @@ const ROLE_LABELS: Record = { vp: "VP", manager: "Manager", engineer: "Engineer", agent: "Agent", }; -/** Sanitize slug for use as a Mermaid node ID. */ -function mermaidId(slug: string): string { - return slug.replace(/[^a-zA-Z0-9_]/g, "_"); -} - -/** Escape text for Mermaid node labels. */ -function mermaidEscape(s: string): string { - return s.replace(/"/g, """).replace(//g, ">"); -} - -/** Generate a Mermaid org chart from the selected agents. */ -function generateOrgChartMermaid(agents: CompanyPortabilityManifest["agents"]): string | null { - if (agents.length === 0) return null; - const lines: string[] = []; - lines.push("```mermaid"); - lines.push("graph TD"); - for (const agent of agents) { - const roleLabel = ROLE_LABELS[agent.role] ?? agent.role; - lines.push(` ${mermaidId(agent.slug)}["${mermaidEscape(agent.name)}
${mermaidEscape(roleLabel)}"]`); - } - const slugSet = new Set(agents.map((a) => a.slug)); - for (const agent of agents) { - if (agent.reportsToSlug && slugSet.has(agent.reportsToSlug)) { - lines.push(` ${mermaidId(agent.reportsToSlug)} --> ${mermaidId(agent.slug)}`); - } - } - lines.push("```"); - return lines.join("\n"); -} - /** * Regenerate README.md content based on the currently checked files. * Only counts/lists entities whose files are in the checked set. @@ -400,10 +370,9 @@ function generateReadmeFromSelection( lines.push(`> ${companyDescription}`); lines.push(""); } - // Org chart as Mermaid diagram - const mermaid = generateOrgChartMermaid(agents); - if (mermaid) { - lines.push(mermaid); + // Org chart image (generated during export as images/org-chart.png) + if (agents.length > 0) { + lines.push("![Org Chart](images/org-chart.png)"); lines.push(""); } From 53249c00cffc05901b85915ffe54f9c8fd6909a8 Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 17:39:26 -0500 Subject: [PATCH 12/46] Upgrade org chart SVG to match Warmth style with icons and descriptive labels - Larger cards (88px tall) with more breathing room (24px/56px gaps) - Descriptive role labels (Chief Executive, Technology, etc.) instead of abbreviations - SVG icon paths per role (star, terminal, globe, etc.) instead of text labels - Keeps Pango-safe rendering (no emoji) while being visually closer to Warmth HTML Co-Authored-By: Paperclip --- server/src/routes/org-chart-svg.ts | 312 +++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 server/src/routes/org-chart-svg.ts diff --git a/server/src/routes/org-chart-svg.ts b/server/src/routes/org-chart-svg.ts new file mode 100644 index 00000000..ec31df84 --- /dev/null +++ b/server/src/routes/org-chart-svg.ts @@ -0,0 +1,312 @@ +/** + * Server-side SVG renderer for Paperclip org charts. + * Renders the org tree in the "Warmth" style with Paperclip branding. + * Supports SVG output and PNG conversion via sharp. + */ +import sharp from "sharp"; + +export interface OrgNode { + id: string; + name: string; + role: string; + status: string; + reports: OrgNode[]; +} + +interface LayoutNode { + node: OrgNode; + x: number; + y: number; + width: number; + height: number; + children: LayoutNode[]; +} + +// ── Design tokens (Warmth style — matches index.html s-warm) ────── +const CARD_H = 88; +const CARD_MIN_W = 150; +const CARD_PAD_X = 22; +const CARD_RADIUS = 6; +const AVATAR_SIZE = 34; +const GAP_X = 24; +const GAP_Y = 56; +const LINE_COLOR = "#d6d3d1"; +const LINE_W = 2; +const BG_COLOR = "#fafaf9"; +const CARD_BG = "#ffffff"; +const CARD_BORDER = "#e7e5e4"; +const CARD_SHADOW_COLOR = "rgba(0,0,0,0.05)"; +const NAME_COLOR = "#1c1917"; +const ROLE_COLOR = "#78716c"; +const FONT = "'Inter', -apple-system, BlinkMacSystemFont, sans-serif"; +const PADDING = 48; +const LOGO_PADDING = 16; + +// Role config: descriptive labels, avatar colors, and SVG icon paths (Pango-safe) +const ROLE_ICONS: Record = { + ceo: { + bg: "#fef3c7", roleLabel: "Chief Executive", iconColor: "#92400e", + // Star icon + iconPath: "M8 1l2.2 4.5L15 6.2l-3.5 3.4.8 4.9L8 12.2 3.7 14.5l.8-4.9L1 6.2l4.8-.7z", + }, + cto: { + bg: "#dbeafe", roleLabel: "Technology", iconColor: "#1e40af", + // Terminal/code icon + iconPath: "M2 3l5 5-5 5M9 13h5", + }, + cmo: { + bg: "#dcfce7", roleLabel: "Marketing", iconColor: "#166534", + // Globe icon + iconPath: "M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zM1 8h14M8 1c-2 2-3 4.5-3 7s1 5 3 7c2-2 3-4.5 3-7s-1-5-3-7z", + }, + cfo: { + bg: "#fef3c7", roleLabel: "Finance", iconColor: "#92400e", + // Dollar sign icon + iconPath: "M8 1v14M5 4.5C5 3.1 6.3 2 8 2s3 1.1 3 2.5S9.7 7 8 7 5 8.1 5 9.5 6.3 12 8 12s3-1.1 3-2.5", + }, + coo: { + bg: "#e0f2fe", roleLabel: "Operations", iconColor: "#075985", + // Settings/gear icon + iconPath: "M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM13 8a5 5 0 0 1-.1.9l1.5 1.2-1.5 2.5-1.7-.7a5 5 0 0 1-1.6.9L9.3 15H6.7l-.3-2.2a5 5 0 0 1-1.6-.9l-1.7.7L1.6 10l1.5-1.2A5 5 0 0 1 3 8c0-.3 0-.6.1-.9L1.6 6l1.5-2.5 1.7.7a5 5 0 0 1 1.6-.9L6.7 1h2.6l.3 2.2c.6.2 1.1.5 1.6.9l1.7-.7L14.4 6l-1.5 1.2c.1.2.1.5.1.8z", + }, + engineer: { + bg: "#f3e8ff", roleLabel: "Engineering", iconColor: "#6b21a8", + // Code brackets icon + iconPath: "M5 3L1 8l4 5M11 3l4 5-4 5", + }, + quality: { + bg: "#ffe4e6", roleLabel: "Quality", iconColor: "#9f1239", + // Checkmark/shield icon + iconPath: "M4 8l3 3 5-6M8 1L2 4v4c0 3.5 2.6 6.8 6 8 3.4-1.2 6-4.5 6-8V4z", + }, + design: { + bg: "#fce7f3", roleLabel: "Design", iconColor: "#9d174d", + // Pen/brush icon + iconPath: "M12 2l2 2-9 9H3v-2zM9.5 4.5l2 2", + }, + finance: { + bg: "#fef3c7", roleLabel: "Finance", iconColor: "#92400e", + iconPath: "M8 1v14M5 4.5C5 3.1 6.3 2 8 2s3 1.1 3 2.5S9.7 7 8 7 5 8.1 5 9.5 6.3 12 8 12s3-1.1 3-2.5", + }, + operations: { + bg: "#e0f2fe", roleLabel: "Operations", iconColor: "#075985", + iconPath: "M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM13 8a5 5 0 0 1-.1.9l1.5 1.2-1.5 2.5-1.7-.7a5 5 0 0 1-1.6.9L9.3 15H6.7l-.3-2.2a5 5 0 0 1-1.6-.9l-1.7.7L1.6 10l1.5-1.2A5 5 0 0 1 3 8c0-.3 0-.6.1-.9L1.6 6l1.5-2.5 1.7.7a5 5 0 0 1 1.6-.9L6.7 1h2.6l.3 2.2c.6.2 1.1.5 1.6.9l1.7-.7L14.4 6l-1.5 1.2c.1.2.1.5.1.8z", + }, + default: { + bg: "#f3e8ff", roleLabel: "Agent", iconColor: "#6b21a8", + // User icon + iconPath: "M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM2 14c0-3.3 2.7-4 6-4s6 .7 6 4", + }, +}; + +function guessRoleTag(node: OrgNode): string { + const name = node.name.toLowerCase(); + const role = node.role.toLowerCase(); + if (name === "ceo" || role.includes("chief executive")) return "ceo"; + if (name === "cto" || role.includes("chief technology") || role.includes("technology")) return "cto"; + if (name === "cmo" || role.includes("chief marketing") || role.includes("marketing")) return "cmo"; + if (name === "cfo" || role.includes("chief financial")) return "cfo"; + if (name === "coo" || role.includes("chief operating")) return "coo"; + if (role.includes("engineer") || role.includes("eng")) return "engineer"; + if (role.includes("quality") || role.includes("qa")) return "quality"; + if (role.includes("design")) return "design"; + if (role.includes("finance")) return "finance"; + if (role.includes("operations") || role.includes("ops")) return "operations"; + return "default"; +} + +function measureText(text: string, fontSize: number): number { + return text.length * fontSize * 0.58; +} + +function cardWidth(node: OrgNode): number { + const tag = guessRoleTag(node); + const roleLabel = ROLE_ICONS[tag]?.roleLabel ?? node.role; + const nameW = measureText(node.name, 14) + CARD_PAD_X * 2; + const roleW = measureText(roleLabel, 11) + CARD_PAD_X * 2; + return Math.max(CARD_MIN_W, Math.max(nameW, roleW)); +} + +// ── Tree layout (top-down, centered) ──────────────────────────── + +function subtreeWidth(node: OrgNode): number { + const cw = cardWidth(node); + if (!node.reports || node.reports.length === 0) return cw; + const childrenW = node.reports.reduce( + (sum, child, i) => sum + subtreeWidth(child) + (i > 0 ? GAP_X : 0), + 0, + ); + return Math.max(cw, childrenW); +} + +function layoutTree(node: OrgNode, x: number, y: number): LayoutNode { + const w = cardWidth(node); + const sw = subtreeWidth(node); + const cardX = x + (sw - w) / 2; + + const layoutNode: LayoutNode = { + node, + x: cardX, + y, + width: w, + height: CARD_H, + children: [], + }; + + if (node.reports && node.reports.length > 0) { + let childX = x; + const childY = y + CARD_H + GAP_Y; + for (let i = 0; i < node.reports.length; i++) { + const child = node.reports[i]; + const childSW = subtreeWidth(child); + layoutNode.children.push(layoutTree(child, childX, childY)); + childX += childSW + GAP_X; + } + } + + return layoutNode; +} + +// ── SVG rendering ─────────────────────────────────────────────── + +function escapeXml(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); +} + +function renderCard(ln: LayoutNode): string { + const tag = guessRoleTag(ln.node); + const role = ROLE_ICONS[tag] || ROLE_ICONS.default; + const cx = ln.x + ln.width / 2; + + // Vertical layout: avatar circle → name → role label + const avatarCY = ln.y + 24; + const nameY = ln.y + 52; + const roleY = ln.y + 68; + + // SVG icon inside the avatar circle, scaled to fit + const iconScale = 0.7; + const iconOffset = (AVATAR_SIZE * iconScale) / 2; + const iconX = cx - iconOffset; + const iconY = avatarCY - iconOffset; + + return ` + + + + + + + + + + + ${escapeXml(ln.node.name)} + ${escapeXml(role.roleLabel)} + `; +} + +function renderConnectors(ln: LayoutNode): string { + if (ln.children.length === 0) return ""; + + const parentCx = ln.x + ln.width / 2; + const parentBottom = ln.y + ln.height; + const midY = parentBottom + GAP_Y / 2; + + let svg = ""; + + // Vertical line from parent to midpoint + svg += ``; + + if (ln.children.length === 1) { + const childCx = ln.children[0].x + ln.children[0].width / 2; + svg += ``; + } else { + const leftCx = ln.children[0].x + ln.children[0].width / 2; + const rightCx = ln.children[ln.children.length - 1].x + ln.children[ln.children.length - 1].width / 2; + svg += ``; + + for (const child of ln.children) { + const childCx = child.x + child.width / 2; + svg += ``; + } + } + + for (const child of ln.children) { + svg += renderConnectors(child); + } + + return svg; +} + +function renderCards(ln: LayoutNode): string { + let svg = renderCard(ln); + for (const child of ln.children) { + svg += renderCards(child); + } + return svg; +} + +function treeBounds(ln: LayoutNode): { minX: number; minY: number; maxX: number; maxY: number } { + let minX = ln.x; + let minY = ln.y; + let maxX = ln.x + ln.width; + let maxY = ln.y + ln.height; + for (const child of ln.children) { + const cb = treeBounds(child); + minX = Math.min(minX, cb.minX); + minY = Math.min(minY, cb.minY); + maxX = Math.max(maxX, cb.maxX); + maxY = Math.max(maxY, cb.maxY); + } + return { minX, minY, maxX, maxY }; +} + +// Paperclip logo as inline SVG path +const PAPERCLIP_LOGO_SVG = ` + + Paperclip +`; + +export function renderOrgChartSvg(orgTree: OrgNode[]): string { + let root: OrgNode; + if (orgTree.length === 1) { + root = orgTree[0]; + } else { + root = { + id: "virtual-root", + name: "Organization", + role: "Root", + status: "active", + reports: orgTree, + }; + } + + const layout = layoutTree(root, PADDING, PADDING + 24); + const bounds = treeBounds(layout); + + const svgW = bounds.maxX + PADDING; + const svgH = bounds.maxY + PADDING; + + const logoX = svgW - 110 - LOGO_PADDING; + const logoY = LOGO_PADDING; + + return ` + + + ${PAPERCLIP_LOGO_SVG} + + ${renderConnectors(layout)} + ${renderCards(layout)} +`; +} + +export async function renderOrgChartPng(orgTree: OrgNode[]): Promise { + const svg = renderOrgChartSvg(orgTree); + return sharp(Buffer.from(svg)).png().toBuffer(); +} From b20675b7b5df1e18a7fca0b8efe48e238ab28cf6 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 05:51:33 -0500 Subject: [PATCH 13/46] Add org chart image export support --- pnpm-lock.yaml | 353 +++++++++++++++++- server/package.json | 4 +- server/src/routes/agents.ts | 23 ++ server/src/services/company-export-readme.ts | 7 +- server/src/services/company-portability.ts | 57 +++ skills/paperclip/references/company-skills.md | 42 ++- 6 files changed, 469 insertions(+), 17 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b3fe946..f0ce912c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)(tsx@4.21.0) cli: dependencies: @@ -244,7 +244,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)(tsx@4.21.0) packages/plugins/create-paperclip-plugin: dependencies: @@ -294,7 +294,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)(tsx@4.21.0) packages/plugins/examples/plugin-file-browser-example: dependencies: @@ -519,6 +519,9 @@ importers: pino-pretty: specifier: ^13.1.3 version: 13.1.3 + sharp: + specifier: ^0.34.5 + version: 0.34.5 ws: specifier: ^8.19.0 version: 8.19.0 @@ -541,6 +544,9 @@ importers: '@types/node': specifier: ^24.6.0 version: 24.12.0 + '@types/sharp': + specifier: ^0.32.0 + version: 0.32.0 '@types/supertest': specifier: ^6.0.2 version: 6.0.3 @@ -564,7 +570,7 @@ importers: version: 6.4.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)(tsx@4.21.0) ui: dependencies: @@ -679,7 +685,7 @@ importers: version: 6.4.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)(tsx@4.21.0) packages: @@ -1219,6 +1225,9 @@ packages: cpu: [x64] os: [win32] + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} @@ -1710,6 +1719,143 @@ packages: '@iconify/utils@3.1.0': resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -3289,6 +3435,10 @@ packages: '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/sharp@0.32.0': + resolution: {integrity: sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==} + deprecated: This is a stub types definition. sharp provides its own type definitions, so you do not need this installed. + '@types/superagent@8.1.9': resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} @@ -5261,6 +5411,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -5275,6 +5430,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -6853,6 +7012,11 @@ snapshots: '@embedded-postgres/windows-x64@18.1.0-beta.16': optional: true + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + '@epic-web/invariant@1.0.0': {} '@esbuild-kit/core-utils@3.3.2': @@ -7124,6 +7288,102 @@ snapshots: '@iconify/types': 2.0.0 mlly: 1.8.1 + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.9.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -9018,6 +9278,10 @@ snapshots: '@types/http-errors': 2.0.5 '@types/node': 25.2.3 + '@types/sharp@0.32.0': + dependencies: + sharp: 0.34.5 + '@types/superagent@8.1.9': dependencies: '@types/cookiejar': 2.1.5 @@ -11388,6 +11652,8 @@ snapshots: semver@6.3.1: {} + semver@7.7.4: {} + send@1.2.1: dependencies: debug: 4.4.3 @@ -11417,6 +11683,37 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -11866,8 +12163,52 @@ snapshots: - terser - tsx - yaml + optional: true - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)(tsx@4.21.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vite-node: 3.2.4(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 24.12.0 + jsdom: 28.1.0(@noble/hashes@2.0.1) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)(tsx@4.21.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 diff --git a/server/package.json b/server/package.json index 57865b2f..e8ead156 100644 --- a/server/package.json +++ b/server/package.json @@ -51,7 +51,6 @@ "@paperclipai/adapter-openclaw-gateway": "workspace:*", "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-pi-local": "workspace:*", - "hermes-paperclip-adapter": "0.1.1", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/db": "workspace:*", "@paperclipai/plugin-sdk": "workspace:*", @@ -66,12 +65,14 @@ "drizzle-orm": "^0.38.4", "embedded-postgres": "^18.1.0-beta.16", "express": "^5.1.0", + "hermes-paperclip-adapter": "0.1.1", "jsdom": "^28.1.0", "multer": "^2.0.2", "open": "^11.0.0", "pino": "^9.6.0", "pino-http": "^10.4.0", "pino-pretty": "^13.1.3", + "sharp": "^0.34.5", "ws": "^8.19.0", "zod": "^3.24.2" }, @@ -81,6 +82,7 @@ "@types/jsdom": "^28.0.0", "@types/multer": "^2.0.0", "@types/node": "^24.6.0", + "@types/sharp": "^0.32.0", "@types/supertest": "^6.0.2", "@types/ws": "^8.18.1", "cross-env": "^10.1.0", diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 9481840b..45051bf5 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -47,6 +47,7 @@ import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js"; import { findServerAdapter, listAdapterModels } from "../adapters/index.js"; import { redactEventPayload } from "../redaction.js"; import { redactCurrentUserValue } from "../log-redaction.js"; +import { renderOrgChartSvg, renderOrgChartPng, type OrgNode } from "./org-chart-svg.js"; import { runClaudeLogin } from "@paperclipai/adapter-claude-local/server"; import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, @@ -821,6 +822,28 @@ export function agentRoutes(db: Db) { res.json(leanTree); }); + router.get("/companies/:companyId/org.svg", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const tree = await svc.orgForCompany(companyId); + const leanTree = tree.map((node) => toLeanOrgNode(node as Record)); + const svg = renderOrgChartSvg(leanTree as unknown as OrgNode[]); + res.setHeader("Content-Type", "image/svg+xml"); + res.setHeader("Cache-Control", "no-cache"); + res.send(svg); + }); + + router.get("/companies/:companyId/org.png", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const tree = await svc.orgForCompany(companyId); + const leanTree = tree.map((node) => toLeanOrgNode(node as Record)); + const png = await renderOrgChartPng(leanTree as unknown as OrgNode[]); + res.setHeader("Content-Type", "image/png"); + res.setHeader("Cache-Control", "no-cache"); + res.send(png); + }); + router.get("/companies/:companyId/agent-configurations", async (req, res) => { const companyId = req.params.companyId as string; await assertCanReadConfigurations(req, companyId); diff --git a/server/src/services/company-export-readme.ts b/server/src/services/company-export-readme.ts index 56725024..aa46d91a 100644 --- a/server/src/services/company-export-readme.ts +++ b/server/src/services/company-export-readme.ts @@ -87,10 +87,9 @@ export function generateReadme( lines.push(""); } - // Org chart as Mermaid diagram - const mermaid = generateOrgChartMermaid(manifest.agents); - if (mermaid) { - lines.push(mermaid); + // Org chart image (generated during export as images/org-chart.png) + if (manifest.agents.length > 0) { + lines.push("![Org Chart](images/org-chart.png)"); lines.push(""); } diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index 5a91efeb..3772fb25 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -42,11 +42,57 @@ import { agentService } from "./agents.js"; import { agentInstructionsService } from "./agent-instructions.js"; import { assetService } from "./assets.js"; import { generateReadme } from "./company-export-readme.js"; +import { renderOrgChartPng, type OrgNode } from "../routes/org-chart-svg.js"; import { companySkillService } from "./company-skills.js"; import { companyService } from "./companies.js"; import { issueService } from "./issues.js"; import { projectService } from "./projects.js"; +/** Build OrgNode tree from manifest agent list (slug + reportsToSlug). */ +function buildOrgTreeFromManifest(agents: CompanyPortabilityManifest["agents"]): OrgNode[] { + const ROLE_LABELS: Record = { + ceo: "Chief Executive", cto: "Technology", cmo: "Marketing", + cfo: "Finance", coo: "Operations", vp: "VP", manager: "Manager", + engineer: "Engineer", agent: "Agent", + }; + const bySlug = new Map(agents.map((a) => [a.slug, a])); + const childrenOf = new Map(); + for (const a of agents) { + const parent = a.reportsToSlug ?? null; + const list = childrenOf.get(parent) ?? []; + list.push(a); + childrenOf.set(parent, list); + } + const build = (parentSlug: string | null): OrgNode[] => { + const members = childrenOf.get(parentSlug) ?? []; + return members.map((m) => ({ + id: m.slug, + name: m.name, + role: ROLE_LABELS[m.role] ?? m.role, + status: "active", + reports: build(m.slug), + })); + }; + // Find roots: agents whose reportsToSlug is null or points to a non-existent slug + const roots = agents.filter((a) => !a.reportsToSlug || !bySlug.has(a.reportsToSlug)); + const rootSlugs = new Set(roots.map((r) => r.slug)); + // Start from null parent, but also include orphans + const tree = build(null); + for (const root of roots) { + if (root.reportsToSlug && !bySlug.has(root.reportsToSlug)) { + // Orphan root (parent slug doesn't exist) + tree.push({ + id: root.slug, + name: root.name, + role: ROLE_LABELS[root.role] ?? root.role, + status: "active", + reports: build(root.slug), + }); + } + } + return tree; +} + const DEFAULT_INCLUDE: CompanyPortabilityInclude = { company: true, agents: true, @@ -2422,6 +2468,17 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { resolved.manifest.envInputs = dedupeEnvInputs(envInputs); resolved.warnings.unshift(...warnings); + // Generate org chart PNG from manifest agents + if (resolved.manifest.agents.length > 0) { + try { + const orgNodes = buildOrgTreeFromManifest(resolved.manifest.agents); + const pngBuffer = await renderOrgChartPng(orgNodes); + finalFiles["images/org-chart.png"] = bufferToPortableBinaryFile(pngBuffer, "image/png"); + } catch { + // Non-fatal: export still works without the org chart image + } + } + if (!input.selectedFiles || input.selectedFiles.some((entry) => normalizePortablePath(entry) === "README.md")) { finalFiles["README.md"] = generateReadme(resolved.manifest, { companyName: company.name, diff --git a/skills/paperclip/references/company-skills.md b/skills/paperclip/references/company-skills.md index 852424cc..a0ddd064 100644 --- a/skills/paperclip/references/company-skills.md +++ b/skills/paperclip/references/company-skills.md @@ -34,7 +34,42 @@ The canonical model is: ## Install A Skill Into The Company -Import from GitHub, a local path, or a `skills.sh`-style source string: +Import using a **skills.sh URL**, a key-style source string, a GitHub URL, or a local path. + +### Source types (in order of preference) + +| Source format | Example | When to use | +|---|---|---| +| **skills.sh URL** | `https://skills.sh/google-labs-code/stitch-skills/design-md` | When a user gives you a `skills.sh` link. This is the managed skill registry — **always prefer it when available**. | +| **Key-style string** | `google-labs-code/stitch-skills/design-md` | Shorthand for the same skill — `org/repo/skill-name` format. Equivalent to the skills.sh URL. | +| **GitHub URL** | `https://github.com/vercel-labs/agent-browser` | When the skill is in a GitHub repo but not on skills.sh. | +| **Local path** | `/abs/path/to/skill-dir` | When the skill is on disk (dev/testing only). | + +**Critical:** If a user gives you a `https://skills.sh/...` URL, use that URL or its key-style equivalent (`org/repo/skill-name`) as the `source`. Do **not** convert it to a GitHub URL — skills.sh is the managed registry and the source of truth for versioning, discovery, and updates. + +### Example: skills.sh import (preferred) + +```sh +curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills/import" \ + -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "source": "https://skills.sh/google-labs-code/stitch-skills/design-md" + }' +``` + +Or equivalently using the key-style string: + +```sh +curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills/import" \ + -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "source": "google-labs-code/stitch-skills/design-md" + }' +``` + +### Example: GitHub import ```sh curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills/import" \ @@ -45,11 +80,6 @@ curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/skills/ }' ``` -You can also use a source string such as: - -- `npx skills add https://github.com/vercel-labs/agent-browser --skill agent-browser` -- `vercel-labs/agent-browser/agent-browser` - If the task is to discover skills from the company project workspaces first: ```sh From 14ee36419036b61152f35853a8d3d4f774379a77 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 05:55:33 -0500 Subject: [PATCH 14/46] Add standalone Playwright-based org chart image generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the sharp SVG→PNG approach with Playwright headless browser rendering. This solves the emoji rendering issue - browser natively renders emojis, full CSS (shadows, gradients, backdrop-filter), and produces pixel-perfect output matching the HTML preview. Generates 20 images: 5 styles (Mono, Nebula, Circuit, Warmth, Schematic) × 3 org sizes (sm/med/lg) + OG cards (1200×630). Usage: npx tsx scripts/generate-org-chart-images.ts Co-Authored-By: Paperclip --- scripts/generate-org-chart-images.ts | 694 +++++++++++++++++++++++++++ 1 file changed, 694 insertions(+) create mode 100644 scripts/generate-org-chart-images.ts diff --git a/scripts/generate-org-chart-images.ts b/scripts/generate-org-chart-images.ts new file mode 100644 index 00000000..f60e7d1f --- /dev/null +++ b/scripts/generate-org-chart-images.ts @@ -0,0 +1,694 @@ +#!/usr/bin/env npx tsx +/** + * Standalone org chart image generator. + * + * Renders each of the 5 org chart styles to PNG using Playwright (headless Chromium). + * This gives us browser-native emoji rendering, full CSS support, and pixel-perfect output. + * + * Usage: + * npx tsx scripts/generate-org-chart-images.ts + * + * Output: tmp/org-chart-images/ + +
+${tree} +${PAPERCLIP_WATERMARK} +
+`; +} + +// ── Main ─────────────────────────────────────────────────────── + +async function main() { + const outDir = path.resolve("tmp/org-chart-images"); + fs.mkdirSync(outDir, { recursive: true }); + + const browser = await chromium.launch(); + const context = await browser.newContext({ + deviceScaleFactor: 2, // retina quality + }); + + const sizes = ["sm", "med", "lg"] as const; + const results: string[] = []; + + for (const style of STYLES) { + // README sizes + for (const size of sizes) { + const page = await context.newPage(); + const html = buildHtml(style, ORGS[size], false); + await page.setContent(html, { waitUntil: "networkidle" }); + + // Wait for fonts to load + await page.waitForFunction(() => document.fonts.ready); + await page.waitForTimeout(300); + + // Fit to content + const box = await page.evaluate(() => { + const el = document.querySelector(".org-tree")!; + const rect = el.getBoundingClientRect(); + return { + width: Math.ceil(rect.width) + 32, + height: Math.ceil(rect.height) + 32, + }; + }); + + await page.setViewportSize({ + width: Math.max(box.width, 400), + height: Math.max(box.height, 300), + }); + + const filename = `${style.key}-${size}.png`; + await page.screenshot({ + path: path.join(outDir, filename), + clip: { + x: 0, + y: 0, + width: Math.max(box.width, 400), + height: Math.max(box.height, 300), + }, + }); + await page.close(); + results.push(filename); + console.log(` ✓ ${filename}`); + } + + // OG card (1200×630) + { + const page = await context.newPage(); + await page.setViewportSize({ width: 1200, height: 630 }); + const html = buildHtml(style, OG_ORG, true); + // For OG, center the tree in a fixed viewport + const ogHtml = html.replace( + "", + ``, + ); + await page.setContent(ogHtml, { waitUntil: "networkidle" }); + await page.waitForFunction(() => document.fonts.ready); + await page.waitForTimeout(300); + + const filename = `${style.key}-og.png`; + await page.screenshot({ + path: path.join(outDir, filename), + clip: { x: 0, y: 0, width: 1200, height: 630 }, + }); + await page.close(); + results.push(filename); + console.log(` ✓ ${filename}`); + } + } + + await browser.close(); + + // Build an HTML comparison page + let compHtml = ` + + +Org Chart Style Comparison + + +

Org Chart Export — Style Comparison

+

5 styles × 3 org sizes + OG cards. All rendered via Playwright (browser-native emojis, full CSS).

+`; + + for (const style of STYLES) { + compHtml += `
+

${style.name}

+
README — Small / Medium / Large
+
+ + + +
+
OG Card (1200×630)
+
+
`; + } + + compHtml += ``; + fs.writeFileSync(path.join(outDir, "comparison.html"), compHtml); + console.log(`\n✓ All done! ${results.length} images generated.`); + console.log(` Open: tmp/org-chart-images/comparison.html`); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); From 5f2b1b63c2cabc584b1e690248abca24572ca8f2 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 06:20:30 -0500 Subject: [PATCH 15/46] Add explicit skill selection to company portability --- cli/src/commands/client/company.ts | 13 +++++++------ .../shared/src/types/company-portability.ts | 1 + .../src/validators/company-portability.ts | 2 ++ .../company-portability-routes.test.ts | 2 +- .../src/__tests__/company-portability.test.ts | 6 ++++-- server/src/services/company-portability.ts | 18 ++++++++++++++---- 6 files changed, 29 insertions(+), 13 deletions(-) diff --git a/cli/src/commands/client/company.ts b/cli/src/commands/client/company.ts index 9e563387..01de4548 100644 --- a/cli/src/commands/client/company.ts +++ b/cli/src/commands/client/company.ts @@ -85,16 +85,17 @@ function normalizeSelector(input: string): string { } function parseInclude(input: string | undefined): CompanyPortabilityInclude { - if (!input || !input.trim()) return { company: true, agents: true, projects: false, issues: false }; + if (!input || !input.trim()) return { company: true, agents: true, projects: false, issues: false, skills: false }; const values = input.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean); const include = { company: values.includes("company"), agents: values.includes("agents"), projects: values.includes("projects"), - issues: values.includes("issues"), + issues: values.includes("issues") || values.includes("tasks"), + skills: values.includes("skills"), }; - if (!include.company && !include.agents && !include.projects && !include.issues) { - throw new Error("Invalid --include value. Use one or more of: company,agents,projects,issues"); + if (!include.company && !include.agents && !include.projects && !include.issues && !include.skills) { + throw new Error("Invalid --include value. Use one or more of: company,agents,projects,issues,tasks,skills"); } return include; } @@ -337,7 +338,7 @@ export function registerCompanyCommands(program: Command): void { .description("Export a company into a portable markdown package") .argument("", "Company ID") .requiredOption("--out ", "Output directory") - .option("--include ", "Comma-separated include set: company,agents,projects,issues", "company,agents") + .option("--include ", "Comma-separated include set: company,agents,projects,issues,tasks,skills", "company,agents") .option("--skills ", "Comma-separated skill slugs/keys to export") .option("--projects ", "Comma-separated project shortnames/ids to export") .option("--issues ", "Comma-separated issue identifiers/ids to export") @@ -390,7 +391,7 @@ export function registerCompanyCommands(program: Command): void { .command("import") .description("Import a portable markdown company package from local path, URL, or GitHub") .requiredOption("--from ", "Source path or URL") - .option("--include ", "Comma-separated include set: company,agents,projects,issues", "company,agents") + .option("--include ", "Comma-separated include set: company,agents,projects,issues,tasks,skills", "company,agents") .option("--target ", "Target mode: new | existing") .option("-C, --company-id ", "Existing target company ID") .option("--new-company-name ", "Name override for --target new") diff --git a/packages/shared/src/types/company-portability.ts b/packages/shared/src/types/company-portability.ts index 811c88a6..26088831 100644 --- a/packages/shared/src/types/company-portability.ts +++ b/packages/shared/src/types/company-portability.ts @@ -3,6 +3,7 @@ export interface CompanyPortabilityInclude { agents: boolean; projects: boolean; issues: boolean; + skills: boolean; } export interface CompanyPortabilityEnvInput { diff --git a/packages/shared/src/validators/company-portability.ts b/packages/shared/src/validators/company-portability.ts index c45eed6a..cae50e89 100644 --- a/packages/shared/src/validators/company-portability.ts +++ b/packages/shared/src/validators/company-portability.ts @@ -6,6 +6,7 @@ export const portabilityIncludeSchema = z agents: z.boolean().optional(), projects: z.boolean().optional(), issues: z.boolean().optional(), + skills: z.boolean().optional(), }) .partial(); @@ -119,6 +120,7 @@ export const portabilityManifestSchema = z.object({ agents: z.boolean(), projects: z.boolean(), issues: z.boolean(), + skills: z.boolean(), }), company: portabilityCompanyManifestEntrySchema.nullable(), agents: z.array(portabilityAgentManifestEntrySchema), diff --git a/server/src/__tests__/company-portability-routes.test.ts b/server/src/__tests__/company-portability-routes.test.ts index bf1c16d3..9fabef46 100644 --- a/server/src/__tests__/company-portability-routes.test.ts +++ b/server/src/__tests__/company-portability-routes.test.ts @@ -97,7 +97,7 @@ describe("company portability routes", () => { }); mockCompanyPortabilityService.previewExport.mockResolvedValue({ rootPath: "paperclip", - manifest: { agents: [], skills: [], projects: [], issues: [], envInputs: [], includes: { company: true, agents: true, projects: true, issues: false }, company: null, schemaVersion: 1, generatedAt: new Date().toISOString(), source: null }, + manifest: { agents: [], skills: [], projects: [], issues: [], envInputs: [], includes: { company: true, agents: true, projects: true, issues: false, skills: false }, company: null, schemaVersion: 1, generatedAt: new Date().toISOString(), source: null }, files: {}, fileInventory: [], counts: { files: 0, agents: 0, skills: 0, projects: 0, issues: 0 }, diff --git a/server/src/__tests__/company-portability.test.ts b/server/src/__tests__/company-portability.test.ts index a24775b4..9a79f392 100644 --- a/server/src/__tests__/company-portability.test.ts +++ b/server/src/__tests__/company-portability.test.ts @@ -666,7 +666,8 @@ describe("company portability", () => { collisionStrategy: "rename", }, "user-1"); - expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith("company-imported", exported.files, { + const textOnlyFiles = Object.fromEntries(Object.entries(exported.files).filter(([, v]) => typeof v === "string")); + expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith("company-imported", textOnlyFiles, { onConflict: "replace", }); expect(agentSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({ @@ -812,7 +813,8 @@ describe("company portability", () => { expect(accessSvc.listActiveUserMemberships).toHaveBeenCalledWith("company-1"); expect(accessSvc.copyActiveUserMemberships).toHaveBeenCalledWith("company-1", "company-imported"); expect(accessSvc.ensureMembership).not.toHaveBeenCalledWith("company-imported", "user", expect.anything(), "owner", "active"); - expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith("company-imported", exported.files, { + const textOnlyFiles = Object.fromEntries(Object.entries(exported.files).filter(([, v]) => typeof v === "string")); + expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith("company-imported", textOnlyFiles, { onConflict: "rename", }); }); diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index 3772fb25..6ae4c431 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -98,6 +98,7 @@ const DEFAULT_INCLUDE: CompanyPortabilityInclude = { agents: true, projects: false, issues: false, + skills: false, }; const DEFAULT_COLLISION_STRATEGY: CompanyPortabilityCollisionStrategy = "rename"; @@ -561,6 +562,7 @@ function normalizeInclude(input?: Partial): CompanyPo agents: input?.agents ?? DEFAULT_INCLUDE.agents, projects: input?.projects ?? DEFAULT_INCLUDE.projects, issues: input?.issues ?? DEFAULT_INCLUDE.issues, + skills: input?.skills ?? DEFAULT_INCLUDE.skills, }; } @@ -1193,6 +1195,7 @@ function applySelectedFilesToSource(source: ResolvedSource, selectedFiles?: stri agents: filtered.manifest.agents.length > 0, projects: filtered.manifest.projects.length > 0, issues: filtered.manifest.issues.length > 0, + skills: filtered.manifest.skills.length > 0, }; return filtered; @@ -1656,6 +1659,7 @@ function buildManifestFromPackageFiles( agents: true, projects: projectPaths.length > 0, issues: taskPaths.length > 0, + skills: skillPaths.length > 0, }, company: { path: resolvedCompanyPath, @@ -2051,6 +2055,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { (input.issues && input.issues.length > 0) || (input.projectIssues && input.projectIssues.length > 0) ? true : input.include?.issues, + skills: input.skills && input.skills.length > 0 ? true : input.include?.skills, }); const company = await companies.getById(companyId); if (!company) throw notFound("Company not found"); @@ -2063,7 +2068,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { const allAgentRows = include.agents ? await agents.list(companyId, { includeTerminated: true }) : []; const liveAgentRows = allAgentRows.filter((agent) => agent.status !== "terminated"); - const companySkillRows = await companySkills.listFull(companyId); + const companySkillRows = include.skills || include.agents ? await companySkills.listFull(companyId) : []; if (include.agents) { const skipped = allAgentRows.length - liveAgentRows.length; if (skipped > 0) { @@ -2464,6 +2469,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { agents: resolved.manifest.agents.length > 0, projects: resolved.manifest.projects.length > 0, issues: resolved.manifest.issues.length > 0, + skills: resolved.manifest.skills.length > 0, }; resolved.manifest.envInputs = dedupeEnvInputs(envInputs); resolved.warnings.unshift(...warnings); @@ -2497,6 +2503,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { agents: resolved.manifest.agents.length > 0, projects: resolved.manifest.projects.length > 0, issues: resolved.manifest.issues.length > 0, + skills: resolved.manifest.skills.length > 0, }; resolved.manifest.envInputs = dedupeEnvInputs(envInputs); resolved.warnings.unshift(...warnings); @@ -2559,6 +2566,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { agents: requestedInclude.agents && manifest.agents.length > 0, projects: requestedInclude.projects && manifest.projects.length > 0, issues: requestedInclude.issues && manifest.issues.length > 0, + skills: requestedInclude.skills && manifest.skills.length > 0, }; const collisionStrategy = input.collisionStrategy ?? DEFAULT_COLLISION_STRATEGY; if (mode === "agent_safe" && collisionStrategy === "replace") { @@ -3019,9 +3027,11 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { existingProjectSlugToId.set(existing.urlKey, existing.id); } - const importedSkills = await companySkills.importPackageFiles(targetCompany.id, pickTextFiles(plan.source.files), { - onConflict: resolveSkillConflictStrategy(mode, plan.collisionStrategy), - }); + const importedSkills = include.skills || include.agents + ? await companySkills.importPackageFiles(targetCompany.id, pickTextFiles(plan.source.files), { + onConflict: resolveSkillConflictStrategy(mode, plan.collisionStrategy), + }) + : []; const desiredSkillRefMap = new Map(); for (const importedSkill of importedSkills) { desiredSkillRefMap.set(importedSkill.originalKey, importedSkill.skill.key); From 6a568662b8d33e2a38b400d376ad0baf18c5a705 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 06:29:08 -0500 Subject: [PATCH 16/46] fix: remove duplicate company branding import --- server/src/routes/companies.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/routes/companies.ts b/server/src/routes/companies.ts index 4c2d4807..5112baac 100644 --- a/server/src/routes/companies.ts +++ b/server/src/routes/companies.ts @@ -7,7 +7,6 @@ import { createCompanySchema, updateCompanyBrandingSchema, updateCompanySchema, - updateCompanyBrandingSchema, } from "@paperclipai/shared"; import { forbidden } from "../errors.js"; import { validate } from "../middleware/validate.js"; From f2c42aad12eda4fdd3e86a6c86c4a50fb4f9f8ea Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 06:33:29 -0500 Subject: [PATCH 17/46] feat: multi-style pure SVG org chart renderer (no Playwright needed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrote org-chart-svg.ts to support all 5 styles (monochrome, nebula, circuit, warmth, schematic) as pure SVG — no browser or Satori needed. Routes now accept ?style= query param. Added standalone comparison script. Co-Authored-By: Paperclip --- .../generate-org-chart-satori-comparison.ts | 225 +++++++++++ server/src/routes/agents.ts | 8 +- server/src/routes/org-chart-svg.ts | 372 ++++++++++++++---- 3 files changed, 516 insertions(+), 89 deletions(-) create mode 100644 scripts/generate-org-chart-satori-comparison.ts diff --git a/scripts/generate-org-chart-satori-comparison.ts b/scripts/generate-org-chart-satori-comparison.ts new file mode 100644 index 00000000..0f967d72 --- /dev/null +++ b/scripts/generate-org-chart-satori-comparison.ts @@ -0,0 +1,225 @@ +#!/usr/bin/env npx tsx +/** + * Standalone org chart comparison generator — pure SVG (no Playwright). + * + * Generates SVG files for all 5 styles × 3 org sizes, plus a comparison HTML page. + * Uses the server-side SVG renderer directly — same code that powers the routes. + * + * Usage: + * npx tsx scripts/generate-org-chart-satori-comparison.ts + * + * Output: tmp/org-chart-svg-comparison/ + */ +import * as fs from "fs"; +import * as path from "path"; +import { + renderOrgChartSvg, + renderOrgChartPng, + type OrgNode, + type OrgChartStyle, + ORG_CHART_STYLES, +} from "../server/src/routes/org-chart-svg.js"; + +// ── Sample org data ────────────────────────────────────────────── + +const ORGS: Record = { + sm: { + id: "ceo", + name: "CEO", + role: "Chief Executive", + status: "active", + reports: [ + { id: "eng1", name: "Engineer", role: "Engineering", status: "active", reports: [] }, + { id: "des1", name: "Designer", role: "Design", status: "active", reports: [] }, + ], + }, + med: { + id: "ceo", + name: "CEO", + role: "Chief Executive", + status: "active", + reports: [ + { + id: "cto", + name: "CTO", + role: "Technology", + status: "active", + reports: [ + { id: "eng1", name: "ClaudeCoder", role: "Engineering", status: "active", reports: [] }, + { id: "eng2", name: "CodexCoder", role: "Engineering", status: "active", reports: [] }, + { id: "eng3", name: "SparkCoder", role: "Engineering", status: "active", reports: [] }, + { id: "eng4", name: "CursorCoder", role: "Engineering", status: "active", reports: [] }, + { id: "qa1", name: "QA", role: "Quality", status: "active", reports: [] }, + ], + }, + { + id: "cmo", + name: "CMO", + role: "Marketing", + status: "active", + reports: [ + { id: "des1", name: "Designer", role: "Design", status: "active", reports: [] }, + ], + }, + ], + }, + lg: { + id: "ceo", + name: "CEO", + role: "Chief Executive", + status: "active", + reports: [ + { + id: "cto", + name: "CTO", + role: "Technology", + status: "active", + reports: [ + { id: "eng1", name: "Eng 1", role: "Engineering", status: "active", reports: [] }, + { id: "eng2", name: "Eng 2", role: "Engineering", status: "active", reports: [] }, + { id: "eng3", name: "Eng 3", role: "Engineering", status: "active", reports: [] }, + { id: "qa1", name: "QA", role: "Quality", status: "active", reports: [] }, + ], + }, + { + id: "cmo", + name: "CMO", + role: "Marketing", + status: "active", + reports: [ + { id: "des1", name: "Designer", role: "Design", status: "active", reports: [] }, + { id: "wrt1", name: "Content", role: "Engineering", status: "active", reports: [] }, + ], + }, + { + id: "cfo", + name: "CFO", + role: "Finance", + status: "active", + reports: [ + { id: "fin1", name: "Analyst", role: "Finance", status: "active", reports: [] }, + ], + }, + { + id: "coo", + name: "COO", + role: "Operations", + status: "active", + reports: [ + { id: "ops1", name: "Ops 1", role: "Operations", status: "active", reports: [] }, + { id: "ops2", name: "Ops 2", role: "Operations", status: "active", reports: [] }, + { id: "devops1", name: "DevOps", role: "Operations", status: "active", reports: [] }, + ], + }, + ], + }, +}; + +const STYLE_META: Record = { + monochrome: { name: "Monochrome", vibe: "Vercel — zero color noise, dark", bestFor: "GitHub READMEs, developer docs" }, + nebula: { name: "Nebula", vibe: "Glassmorphism — cosmic gradient", bestFor: "Hero sections, marketing" }, + circuit: { name: "Circuit", vibe: "Linear/Raycast — indigo traces", bestFor: "Product pages, dev tools" }, + warmth: { name: "Warmth", vibe: "Airbnb — light, colored avatars", bestFor: "Light-mode READMEs, presentations" }, + schematic: { name: "Schematic", vibe: "Blueprint — grid bg, monospace", bestFor: "Technical docs, infra diagrams" }, +}; + +// ── Main ───────────────────────────────────────────────────────── + +async function main() { + const outDir = path.resolve("tmp/org-chart-svg-comparison"); + fs.mkdirSync(outDir, { recursive: true }); + + const sizes = ["sm", "med", "lg"] as const; + const results: string[] = []; + + for (const style of ORG_CHART_STYLES) { + for (const size of sizes) { + const svg = renderOrgChartSvg([ORGS[size]], style); + const svgFile = `${style}-${size}.svg`; + fs.writeFileSync(path.join(outDir, svgFile), svg); + results.push(svgFile); + console.log(` ✓ ${svgFile}`); + + // Also generate PNG + try { + const png = await renderOrgChartPng([ORGS[size]], style); + const pngFile = `${style}-${size}.png`; + fs.writeFileSync(path.join(outDir, pngFile), png); + results.push(pngFile); + console.log(` ✓ ${pngFile}`); + } catch (e) { + console.log(` ⚠ PNG failed for ${style}-${size}: ${(e as Error).message}`); + } + } + } + + // Build comparison HTML + let html = ` + + +Org Chart Style Comparison — Pure SVG (No Playwright) + + +

Org Chart Export — Style Comparison

+

5 styles × 3 org sizes. Pure SVG — no Playwright, no Satori, no browser needed.

+
Server-side compatible — works on any route
+`; + + for (const style of ORG_CHART_STYLES) { + const meta = STYLE_META[style]; + html += `
+

${meta.name}

+
${meta.vibe} — Best for: ${meta.bestFor}
+
Small / Medium / Large
+
+
3 agents
+
8 agents
+
14 agents
+
+
`; + } + + html += ` +
+

Why Pure SVG instead of Satori?

+

+ Satori converts JSX → SVG using Yoga (flexbox). It's great for OG cards but has limitations for org charts: + no ::before/::after pseudo-elements, no CSS grid, limited gradient support, + and connector lines between nodes would need post-processing. +

+

+ Pure SVG rendering (what we're using here) gives us full control over layout, connectors, + gradients, filters, and patterns — with zero runtime dependencies beyond sharp for PNG. + It runs on any Node.js route, generates in <10ms, and produces identical output every time. +

+

+ Routes: GET /api/companies/:id/org.svg?style=monochrome and GET /api/companies/:id/org.png?style=circuit +

+
+`; + + fs.writeFileSync(path.join(outDir, "comparison.html"), html); + console.log(`\n✓ All done! ${results.length} files generated.`); + console.log(` Open: tmp/org-chart-svg-comparison/comparison.html`); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index cc168066..7b2bdc66 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -47,7 +47,7 @@ import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js"; import { findServerAdapter, listAdapterModels } from "../adapters/index.js"; import { redactEventPayload } from "../redaction.js"; import { redactCurrentUserValue } from "../log-redaction.js"; -import { renderOrgChartSvg, renderOrgChartPng, type OrgNode } from "./org-chart-svg.js"; +import { renderOrgChartSvg, renderOrgChartPng, type OrgNode, type OrgChartStyle, ORG_CHART_STYLES } from "./org-chart-svg.js"; import { runClaudeLogin } from "@paperclipai/adapter-claude-local/server"; import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX, @@ -899,9 +899,10 @@ export function agentRoutes(db: Db) { router.get("/companies/:companyId/org.svg", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); + const style = (ORG_CHART_STYLES.includes(req.query.style as OrgChartStyle) ? req.query.style : "warmth") as OrgChartStyle; const tree = await svc.orgForCompany(companyId); const leanTree = tree.map((node) => toLeanOrgNode(node as Record)); - const svg = renderOrgChartSvg(leanTree as unknown as OrgNode[]); + const svg = renderOrgChartSvg(leanTree as unknown as OrgNode[], style); res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "no-cache"); res.send(svg); @@ -910,9 +911,10 @@ export function agentRoutes(db: Db) { router.get("/companies/:companyId/org.png", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); + const style = (ORG_CHART_STYLES.includes(req.query.style as OrgChartStyle) ? req.query.style : "warmth") as OrgChartStyle; const tree = await svc.orgForCompany(companyId); const leanTree = tree.map((node) => toLeanOrgNode(node as Record)); - const png = await renderOrgChartPng(leanTree as unknown as OrgNode[]); + const png = await renderOrgChartPng(leanTree as unknown as OrgNode[], style); res.setHeader("Content-Type", "image/png"); res.setHeader("Cache-Control", "no-cache"); res.send(png); diff --git a/server/src/routes/org-chart-svg.ts b/server/src/routes/org-chart-svg.ts index ec31df84..cfc49f88 100644 --- a/server/src/routes/org-chart-svg.ts +++ b/server/src/routes/org-chart-svg.ts @@ -1,7 +1,7 @@ /** * Server-side SVG renderer for Paperclip org charts. - * Renders the org tree in the "Warmth" style with Paperclip branding. - * Supports SVG output and PNG conversion via sharp. + * Supports 5 visual styles: monochrome, nebula, circuit, warmth, schematic. + * Pure SVG output — no browser/Playwright needed. PNG via sharp. */ import sharp from "sharp"; @@ -13,6 +13,10 @@ export interface OrgNode { reports: OrgNode[]; } +export type OrgChartStyle = "monochrome" | "nebula" | "circuit" | "warmth" | "schematic"; + +export const ORG_CHART_STYLES: OrgChartStyle[] = ["monochrome", "nebula", "circuit", "warmth", "schematic"]; + interface LayoutNode { node: OrgNode; x: number; @@ -22,85 +26,81 @@ interface LayoutNode { children: LayoutNode[]; } -// ── Design tokens (Warmth style — matches index.html s-warm) ────── -const CARD_H = 88; -const CARD_MIN_W = 150; -const CARD_PAD_X = 22; -const CARD_RADIUS = 6; -const AVATAR_SIZE = 34; -const GAP_X = 24; -const GAP_Y = 56; -const LINE_COLOR = "#d6d3d1"; -const LINE_W = 2; -const BG_COLOR = "#fafaf9"; -const CARD_BG = "#ffffff"; -const CARD_BORDER = "#e7e5e4"; -const CARD_SHADOW_COLOR = "rgba(0,0,0,0.05)"; -const NAME_COLOR = "#1c1917"; -const ROLE_COLOR = "#78716c"; -const FONT = "'Inter', -apple-system, BlinkMacSystemFont, sans-serif"; -const PADDING = 48; -const LOGO_PADDING = 16; +// ── Style theme definitions ────────────────────────────────────── + +interface StyleTheme { + bgColor: string; + cardBg: string; + cardBorder: string; + cardRadius: number; + cardShadow: string | null; + lineColor: string; + lineWidth: number; + nameColor: string; + roleColor: string; + font: string; + watermarkColor: string; + /** Extra SVG defs (filters, patterns, gradients) */ + defs: (svgW: number, svgH: number) => string; + /** Extra background elements after the main bg rect */ + bgExtras: (svgW: number, svgH: number) => string; + /** Custom card renderer — if null, uses default avatar+name+role */ + renderCard: ((ln: LayoutNode, theme: StyleTheme) => string) | null; + /** Per-card accent (top bar, border glow, etc.) */ + cardAccent: ((tag: string) => string) | null; +} + +// ── Role icons (shared across styles) ──────────────────────────── -// Role config: descriptive labels, avatar colors, and SVG icon paths (Pango-safe) const ROLE_ICONS: Record = { ceo: { - bg: "#fef3c7", roleLabel: "Chief Executive", iconColor: "#92400e", - // Star icon + bg: "#fef3c7", roleLabel: "Chief Executive", iconColor: "#92400e", accentColor: "#f0883e", iconPath: "M8 1l2.2 4.5L15 6.2l-3.5 3.4.8 4.9L8 12.2 3.7 14.5l.8-4.9L1 6.2l4.8-.7z", }, cto: { - bg: "#dbeafe", roleLabel: "Technology", iconColor: "#1e40af", - // Terminal/code icon + bg: "#dbeafe", roleLabel: "Technology", iconColor: "#1e40af", accentColor: "#58a6ff", iconPath: "M2 3l5 5-5 5M9 13h5", }, cmo: { - bg: "#dcfce7", roleLabel: "Marketing", iconColor: "#166534", - // Globe icon + bg: "#dcfce7", roleLabel: "Marketing", iconColor: "#166534", accentColor: "#3fb950", iconPath: "M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zM1 8h14M8 1c-2 2-3 4.5-3 7s1 5 3 7c2-2 3-4.5 3-7s-1-5-3-7z", }, cfo: { - bg: "#fef3c7", roleLabel: "Finance", iconColor: "#92400e", - // Dollar sign icon + bg: "#fef3c7", roleLabel: "Finance", iconColor: "#92400e", accentColor: "#f0883e", iconPath: "M8 1v14M5 4.5C5 3.1 6.3 2 8 2s3 1.1 3 2.5S9.7 7 8 7 5 8.1 5 9.5 6.3 12 8 12s3-1.1 3-2.5", }, coo: { - bg: "#e0f2fe", roleLabel: "Operations", iconColor: "#075985", - // Settings/gear icon + bg: "#e0f2fe", roleLabel: "Operations", iconColor: "#075985", accentColor: "#58a6ff", iconPath: "M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM13 8a5 5 0 0 1-.1.9l1.5 1.2-1.5 2.5-1.7-.7a5 5 0 0 1-1.6.9L9.3 15H6.7l-.3-2.2a5 5 0 0 1-1.6-.9l-1.7.7L1.6 10l1.5-1.2A5 5 0 0 1 3 8c0-.3 0-.6.1-.9L1.6 6l1.5-2.5 1.7.7a5 5 0 0 1 1.6-.9L6.7 1h2.6l.3 2.2c.6.2 1.1.5 1.6.9l1.7-.7L14.4 6l-1.5 1.2c.1.2.1.5.1.8z", }, engineer: { - bg: "#f3e8ff", roleLabel: "Engineering", iconColor: "#6b21a8", - // Code brackets icon + bg: "#f3e8ff", roleLabel: "Engineering", iconColor: "#6b21a8", accentColor: "#bc8cff", iconPath: "M5 3L1 8l4 5M11 3l4 5-4 5", }, quality: { - bg: "#ffe4e6", roleLabel: "Quality", iconColor: "#9f1239", - // Checkmark/shield icon + bg: "#ffe4e6", roleLabel: "Quality", iconColor: "#9f1239", accentColor: "#f778ba", iconPath: "M4 8l3 3 5-6M8 1L2 4v4c0 3.5 2.6 6.8 6 8 3.4-1.2 6-4.5 6-8V4z", }, design: { - bg: "#fce7f3", roleLabel: "Design", iconColor: "#9d174d", - // Pen/brush icon + bg: "#fce7f3", roleLabel: "Design", iconColor: "#9d174d", accentColor: "#79c0ff", iconPath: "M12 2l2 2-9 9H3v-2zM9.5 4.5l2 2", }, finance: { - bg: "#fef3c7", roleLabel: "Finance", iconColor: "#92400e", + bg: "#fef3c7", roleLabel: "Finance", iconColor: "#92400e", accentColor: "#f0883e", iconPath: "M8 1v14M5 4.5C5 3.1 6.3 2 8 2s3 1.1 3 2.5S9.7 7 8 7 5 8.1 5 9.5 6.3 12 8 12s3-1.1 3-2.5", }, operations: { - bg: "#e0f2fe", roleLabel: "Operations", iconColor: "#075985", + bg: "#e0f2fe", roleLabel: "Operations", iconColor: "#075985", accentColor: "#58a6ff", iconPath: "M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM13 8a5 5 0 0 1-.1.9l1.5 1.2-1.5 2.5-1.7-.7a5 5 0 0 1-1.6.9L9.3 15H6.7l-.3-2.2a5 5 0 0 1-1.6-.9l-1.7.7L1.6 10l1.5-1.2A5 5 0 0 1 3 8c0-.3 0-.6.1-.9L1.6 6l1.5-2.5 1.7.7a5 5 0 0 1 1.6-.9L6.7 1h2.6l.3 2.2c.6.2 1.1.5 1.6.9l1.7-.7L14.4 6l-1.5 1.2c.1.2.1.5.1.8z", }, default: { - bg: "#f3e8ff", roleLabel: "Agent", iconColor: "#6b21a8", - // User icon + bg: "#f3e8ff", roleLabel: "Agent", iconColor: "#6b21a8", accentColor: "#bc8cff", iconPath: "M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM2 14c0-3.3 2.7-4 6-4s6 .7 6 4", }, }; @@ -121,19 +121,205 @@ function guessRoleTag(node: OrgNode): string { return "default"; } +function getRoleInfo(node: OrgNode) { + const tag = guessRoleTag(node); + return { tag, ...(ROLE_ICONS[tag] || ROLE_ICONS.default) }; +} + +// ── Style themes ───────────────────────────────────────────────── + +const THEMES: Record = { + // 01 — Monochrome (Vercel-inspired, dark minimal) + monochrome: { + bgColor: "#18181b", + cardBg: "#18181b", + cardBorder: "#27272a", + cardRadius: 6, + cardShadow: null, + lineColor: "#3f3f46", + lineWidth: 1.5, + nameColor: "#fafafa", + roleColor: "#71717a", + font: "'Inter', system-ui, sans-serif", + watermarkColor: "rgba(255,255,255,0.25)", + defs: () => "", + bgExtras: () => "", + renderCard: null, + cardAccent: null, + }, + + // 02 — Nebula (glassmorphism on cosmic gradient) + nebula: { + bgColor: "#0f0c29", + cardBg: "rgba(255,255,255,0.07)", + cardBorder: "rgba(255,255,255,0.12)", + cardRadius: 6, + cardShadow: null, + lineColor: "rgba(255,255,255,0.25)", + lineWidth: 1.5, + nameColor: "#ffffff", + roleColor: "rgba(255,255,255,0.45)", + font: "'Inter', system-ui, sans-serif", + watermarkColor: "rgba(255,255,255,0.2)", + defs: (_w, _h) => ` + + + + + + + + + + + + + `, + bgExtras: (w, h) => ` + + + `, + renderCard: null, + cardAccent: null, + }, + + // 03 — Circuit (Linear/Raycast — indigo traces, amethyst CEO) + circuit: { + bgColor: "#0c0c0e", + cardBg: "rgba(99,102,241,0.04)", + cardBorder: "rgba(99,102,241,0.18)", + cardRadius: 5, + cardShadow: null, + lineColor: "rgba(99,102,241,0.35)", + lineWidth: 1.5, + nameColor: "#e4e4e7", + roleColor: "#6366f1", + font: "'Inter', system-ui, sans-serif", + watermarkColor: "rgba(99,102,241,0.3)", + defs: () => "", + bgExtras: () => "", + renderCard: (ln: LayoutNode, theme: StyleTheme) => { + const { tag, roleLabel, iconPath, iconColor } = getRoleInfo(ln.node); + const cx = ln.x + ln.width / 2; + const isCeo = tag === "ceo"; + const borderColor = isCeo ? "rgba(168,85,247,0.35)" : theme.cardBorder; + const bgColor = isCeo ? "rgba(168,85,247,0.06)" : theme.cardBg; + + const avatarCY = ln.y + 24; + const nameY = ln.y + 52; + const roleY = ln.y + 68; + const iconScale = 0.7; + const iconOffset = (34 * iconScale) / 2; + + return ` + + + + + + ${escapeXml(ln.node.name)} + ${escapeXml(roleLabel).toUpperCase()} + `; + }, + cardAccent: null, + }, + + // 04 — Warmth (Airbnb — light, colored avatars, soft shadows) + warmth: { + bgColor: "#fafaf9", + cardBg: "#ffffff", + cardBorder: "#e7e5e4", + cardRadius: 6, + cardShadow: "rgba(0,0,0,0.05)", + lineColor: "#d6d3d1", + lineWidth: 2, + nameColor: "#1c1917", + roleColor: "#78716c", + font: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif", + watermarkColor: "rgba(0,0,0,0.25)", + defs: () => "", + bgExtras: () => "", + renderCard: null, + cardAccent: null, + }, + + // 05 — Schematic (Blueprint — grid bg, monospace, colored top-bars) + schematic: { + bgColor: "#0d1117", + cardBg: "rgba(13,17,23,0.92)", + cardBorder: "#30363d", + cardRadius: 4, + cardShadow: null, + lineColor: "#30363d", + lineWidth: 1.5, + nameColor: "#c9d1d9", + roleColor: "#8b949e", + font: "'JetBrains Mono', 'SF Mono', monospace", + watermarkColor: "rgba(139,148,158,0.3)", + defs: (w, h) => ` + + + `, + bgExtras: (w, h) => ``, + renderCard: (ln: LayoutNode, theme: StyleTheme) => { + const { tag, accentColor, iconPath, iconColor } = getRoleInfo(ln.node); + const cx = ln.x + ln.width / 2; + + // Schematic uses monospace role labels + const schemaRoles: Record = { + ceo: "chief_executive", cto: "chief_technology", cmo: "chief_marketing", + cfo: "chief_financial", coo: "chief_operating", engineer: "engineer", + quality: "quality_assurance", design: "designer", finance: "finance", + operations: "operations", default: "agent", + }; + const roleText = schemaRoles[tag] || schemaRoles.default; + + const avatarCY = ln.y + 24; + const nameY = ln.y + 52; + const roleY = ln.y + 68; + const iconScale = 0.7; + const iconOffset = (34 * iconScale) / 2; + + return ` + + + + + + + ${escapeXml(ln.node.name)} + ${escapeXml(roleText)} + `; + }, + cardAccent: null, + }, +}; + +// ── Layout constants ───────────────────────────────────────────── + +const CARD_H = 88; +const CARD_MIN_W = 150; +const CARD_PAD_X = 22; +const AVATAR_SIZE = 34; +const GAP_X = 24; +const GAP_Y = 56; +const PADDING = 48; +const LOGO_PADDING = 16; + +// ── Text measurement ───────────────────────────────────────────── + function measureText(text: string, fontSize: number): number { return text.length * fontSize * 0.58; } function cardWidth(node: OrgNode): number { - const tag = guessRoleTag(node); - const roleLabel = ROLE_ICONS[tag]?.roleLabel ?? node.role; + const { roleLabel } = getRoleInfo(node); const nameW = measureText(node.name, 14) + CARD_PAD_X * 2; const roleW = measureText(roleLabel, 11) + CARD_PAD_X * 2; return Math.max(CARD_MIN_W, Math.max(nameW, roleW)); } -// ── Tree layout (top-down, centered) ──────────────────────────── +// ── Tree layout (top-down, centered) ───────────────────────────── function subtreeWidth(node: OrgNode): number { const cw = cardWidth(node); @@ -173,81 +359,90 @@ function layoutTree(node: OrgNode, x: number, y: number): LayoutNode { return layoutNode; } -// ── SVG rendering ─────────────────────────────────────────────── +// ── SVG rendering ──────────────────────────────────────────────── function escapeXml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } -function renderCard(ln: LayoutNode): string { - const tag = guessRoleTag(ln.node); - const role = ROLE_ICONS[tag] || ROLE_ICONS.default; +function defaultRenderCard(ln: LayoutNode, theme: StyleTheme): string { + const { roleLabel, bg, iconColor, iconPath } = getRoleInfo(ln.node); const cx = ln.x + ln.width / 2; - // Vertical layout: avatar circle → name → role label const avatarCY = ln.y + 24; const nameY = ln.y + 52; const roleY = ln.y + 68; - // SVG icon inside the avatar circle, scaled to fit const iconScale = 0.7; const iconOffset = (AVATAR_SIZE * iconScale) / 2; const iconX = cx - iconOffset; const iconY = avatarCY - iconOffset; - return ` - - - - - - - - - - - ${escapeXml(ln.node.name)} - ${escapeXml(role.roleLabel)} - `; + const filterId = `shadow-${ln.node.id}`; + const shadowFilter = theme.cardShadow + ? `filter="url(#${filterId})"` + : ""; + const shadowDef = theme.cardShadow + ? ` + + + ` + : ""; + + // For dark themes without avatars, use a subtle circle + const isLight = theme.bgColor === "#fafaf9" || theme.bgColor === "#ffffff"; + const avatarBg = isLight ? bg : "rgba(255,255,255,0.06)"; + const avatarStroke = isLight ? "" : `stroke="rgba(255,255,255,0.08)" stroke-width="1"`; + + return ` + ${shadowDef} + + + + + + ${escapeXml(ln.node.name)} + ${escapeXml(roleLabel)} + `; } -function renderConnectors(ln: LayoutNode): string { +function renderConnectors(ln: LayoutNode, theme: StyleTheme): string { if (ln.children.length === 0) return ""; const parentCx = ln.x + ln.width / 2; const parentBottom = ln.y + ln.height; const midY = parentBottom + GAP_Y / 2; + const lc = theme.lineColor; + const lw = theme.lineWidth; let svg = ""; - - // Vertical line from parent to midpoint - svg += ``; + svg += ``; if (ln.children.length === 1) { const childCx = ln.children[0].x + ln.children[0].width / 2; - svg += ``; + svg += ``; } else { const leftCx = ln.children[0].x + ln.children[0].width / 2; const rightCx = ln.children[ln.children.length - 1].x + ln.children[ln.children.length - 1].width / 2; - svg += ``; + svg += ``; for (const child of ln.children) { const childCx = child.x + child.width / 2; - svg += ``; + svg += ``; } } for (const child of ln.children) { - svg += renderConnectors(child); + svg += renderConnectors(child, theme); } - return svg; } -function renderCards(ln: LayoutNode): string { - let svg = renderCard(ln); +function renderCards(ln: LayoutNode, theme: StyleTheme): string { + const render = theme.renderCard || defaultRenderCard; + let svg = render(ln, theme); for (const child of ln.children) { - svg += renderCards(child); + svg += renderCards(child, theme); } return svg; } @@ -267,13 +462,16 @@ function treeBounds(ln: LayoutNode): { minX: number; minY: number; maxX: number; return { minX, minY, maxX, maxY }; } -// Paperclip logo as inline SVG path const PAPERCLIP_LOGO_SVG = ` Paperclip `; -export function renderOrgChartSvg(orgTree: OrgNode[]): string { +// ── Public API ─────────────────────────────────────────────────── + +export function renderOrgChartSvg(orgTree: OrgNode[], style: OrgChartStyle = "warmth"): string { + const theme = THEMES[style] || THEMES.warmth; + let root: OrgNode; if (orgTree.length === 1) { root = orgTree[0]; @@ -297,16 +495,18 @@ export function renderOrgChartSvg(orgTree: OrgNode[]): string { const logoY = LOGO_PADDING; return ` - - + ${theme.defs(svgW, svgH)} + + ${theme.bgExtras(svgW, svgH)} + ${PAPERCLIP_LOGO_SVG} - ${renderConnectors(layout)} - ${renderCards(layout)} + ${renderConnectors(layout, theme)} + ${renderCards(layout, theme)} `; } -export async function renderOrgChartPng(orgTree: OrgNode[]): Promise { - const svg = renderOrgChartSvg(orgTree); +export async function renderOrgChartPng(orgTree: OrgNode[], style: OrgChartStyle = "warmth"): Promise { + const svg = renderOrgChartSvg(orgTree, style); return sharp(Buffer.from(svg)).png().toBuffer(); } From b2c2bbd96f0110b378e54c31a4f9cc7f3aaf74bc Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 06:54:02 -0500 Subject: [PATCH 18/46] feat: add Twemoji colorful emoji rendering to org chart SVG (no browser) Embeds Twemoji SVG paths directly into the org chart cards, replacing monochrome icon paths with full-color emoji graphics (crown, laptop, globe, keyboard, microscope, wand, chart, gear). Works in pure SVG without any browser, emoji font, or Satori dependency. Twemoji graphics are CC-BY 4.0 licensed (Twitter/X). Co-Authored-By: Paperclip --- server/src/routes/org-chart-svg.ts | 103 +++++++++++++++++------------ 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/server/src/routes/org-chart-svg.ts b/server/src/routes/org-chart-svg.ts index cfc49f88..1f239516 100644 --- a/server/src/routes/org-chart-svg.ts +++ b/server/src/routes/org-chart-svg.ts @@ -50,58 +50,87 @@ interface StyleTheme { cardAccent: ((tag: string) => string) | null; } -// ── Role icons (shared across styles) ──────────────────────────── +// ── Role config with Twemoji SVG inlines (viewBox 0 0 36 36) ───── +// +// Each `emojiSvg` contains the inner SVG paths from Twemoji (CC-BY 4.0). +// These render as colorful emoji-style icons inside the avatar circle, +// without needing a browser or emoji font. const ROLE_ICONS: Record = { ceo: { - bg: "#fef3c7", roleLabel: "Chief Executive", iconColor: "#92400e", accentColor: "#f0883e", + bg: "#fef3c7", roleLabel: "Chief Executive", accentColor: "#f0883e", iconColor: "#92400e", iconPath: "M8 1l2.2 4.5L15 6.2l-3.5 3.4.8 4.9L8 12.2 3.7 14.5l.8-4.9L1 6.2l4.8-.7z", + // 👑 Crown + emojiSvg: ``, }, cto: { - bg: "#dbeafe", roleLabel: "Technology", iconColor: "#1e40af", accentColor: "#58a6ff", + bg: "#dbeafe", roleLabel: "Technology", accentColor: "#58a6ff", iconColor: "#1e40af", iconPath: "M2 3l5 5-5 5M9 13h5", + // 💻 Laptop + emojiSvg: ``, }, cmo: { - bg: "#dcfce7", roleLabel: "Marketing", iconColor: "#166534", accentColor: "#3fb950", + bg: "#dcfce7", roleLabel: "Marketing", accentColor: "#3fb950", iconColor: "#166534", iconPath: "M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zM1 8h14M8 1c-2 2-3 4.5-3 7s1 5 3 7c2-2 3-4.5 3-7s-1-5-3-7z", + // 🌐 Globe with meridians + emojiSvg: ``, }, cfo: { - bg: "#fef3c7", roleLabel: "Finance", iconColor: "#92400e", accentColor: "#f0883e", + bg: "#fef3c7", roleLabel: "Finance", accentColor: "#f0883e", iconColor: "#92400e", iconPath: "M8 1v14M5 4.5C5 3.1 6.3 2 8 2s3 1.1 3 2.5S9.7 7 8 7 5 8.1 5 9.5 6.3 12 8 12s3-1.1 3-2.5", + // 📊 Bar chart + emojiSvg: ``, }, coo: { - bg: "#e0f2fe", roleLabel: "Operations", iconColor: "#075985", accentColor: "#58a6ff", - iconPath: "M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM13 8a5 5 0 0 1-.1.9l1.5 1.2-1.5 2.5-1.7-.7a5 5 0 0 1-1.6.9L9.3 15H6.7l-.3-2.2a5 5 0 0 1-1.6-.9l-1.7.7L1.6 10l1.5-1.2A5 5 0 0 1 3 8c0-.3 0-.6.1-.9L1.6 6l1.5-2.5 1.7.7a5 5 0 0 1 1.6-.9L6.7 1h2.6l.3 2.2c.6.2 1.1.5 1.6.9l1.7-.7L14.4 6l-1.5 1.2c.1.2.1.5.1.8z", + bg: "#e0f2fe", roleLabel: "Operations", accentColor: "#58a6ff", iconColor: "#075985", + iconPath: "M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z", + // ⚙️ Gear + emojiSvg: ``, }, engineer: { - bg: "#f3e8ff", roleLabel: "Engineering", iconColor: "#6b21a8", accentColor: "#bc8cff", + bg: "#f3e8ff", roleLabel: "Engineering", accentColor: "#bc8cff", iconColor: "#6b21a8", iconPath: "M5 3L1 8l4 5M11 3l4 5-4 5", + // ⌨️ Keyboard + emojiSvg: ``, }, quality: { - bg: "#ffe4e6", roleLabel: "Quality", iconColor: "#9f1239", accentColor: "#f778ba", + bg: "#ffe4e6", roleLabel: "Quality", accentColor: "#f778ba", iconColor: "#9f1239", iconPath: "M4 8l3 3 5-6M8 1L2 4v4c0 3.5 2.6 6.8 6 8 3.4-1.2 6-4.5 6-8V4z", + // 🔬 Microscope + emojiSvg: ``, }, design: { - bg: "#fce7f3", roleLabel: "Design", iconColor: "#9d174d", accentColor: "#79c0ff", + bg: "#fce7f3", roleLabel: "Design", accentColor: "#79c0ff", iconColor: "#9d174d", iconPath: "M12 2l2 2-9 9H3v-2zM9.5 4.5l2 2", + // 🪄 Magic wand + emojiSvg: ``, }, finance: { - bg: "#fef3c7", roleLabel: "Finance", iconColor: "#92400e", accentColor: "#f0883e", + bg: "#fef3c7", roleLabel: "Finance", accentColor: "#f0883e", iconColor: "#92400e", iconPath: "M8 1v14M5 4.5C5 3.1 6.3 2 8 2s3 1.1 3 2.5S9.7 7 8 7 5 8.1 5 9.5 6.3 12 8 12s3-1.1 3-2.5", + // 📊 Bar chart (same as CFO) + emojiSvg: ``, }, operations: { - bg: "#e0f2fe", roleLabel: "Operations", iconColor: "#075985", accentColor: "#58a6ff", - iconPath: "M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM13 8a5 5 0 0 1-.1.9l1.5 1.2-1.5 2.5-1.7-.7a5 5 0 0 1-1.6.9L9.3 15H6.7l-.3-2.2a5 5 0 0 1-1.6-.9l-1.7.7L1.6 10l1.5-1.2A5 5 0 0 1 3 8c0-.3 0-.6.1-.9L1.6 6l1.5-2.5 1.7.7a5 5 0 0 1 1.6-.9L6.7 1h2.6l.3 2.2c.6.2 1.1.5 1.6.9l1.7-.7L14.4 6l-1.5 1.2c.1.2.1.5.1.8z", + bg: "#e0f2fe", roleLabel: "Operations", accentColor: "#58a6ff", iconColor: "#075985", + iconPath: "M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z", + // ⚙️ Gear (same as COO) + emojiSvg: ``, }, default: { - bg: "#f3e8ff", roleLabel: "Agent", iconColor: "#6b21a8", accentColor: "#bc8cff", + bg: "#f3e8ff", roleLabel: "Agent", accentColor: "#bc8cff", iconColor: "#6b21a8", iconPath: "M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM2 14c0-3.3 2.7-4 6-4s6 .7 6 4", + // 👤 Person silhouette + emojiSvg: ``, }, }; @@ -199,7 +228,7 @@ const THEMES: Record = { defs: () => "", bgExtras: () => "", renderCard: (ln: LayoutNode, theme: StyleTheme) => { - const { tag, roleLabel, iconPath, iconColor } = getRoleInfo(ln.node); + const { tag, roleLabel, emojiSvg } = getRoleInfo(ln.node); const cx = ln.x + ln.width / 2; const isCeo = tag === "ceo"; const borderColor = isCeo ? "rgba(168,85,247,0.35)" : theme.cardBorder; @@ -208,15 +237,10 @@ const THEMES: Record = { const avatarCY = ln.y + 24; const nameY = ln.y + 52; const roleY = ln.y + 68; - const iconScale = 0.7; - const iconOffset = (34 * iconScale) / 2; return ` - - - - + ${renderEmojiAvatar(cx, avatarCY, 17, "rgba(99,102,241,0.08)", emojiSvg, "rgba(99,102,241,0.15)")} ${escapeXml(ln.node.name)} ${escapeXml(roleLabel).toUpperCase()} `; @@ -262,7 +286,7 @@ const THEMES: Record = { `, bgExtras: (w, h) => ``, renderCard: (ln: LayoutNode, theme: StyleTheme) => { - const { tag, accentColor, iconPath, iconColor } = getRoleInfo(ln.node); + const { tag, accentColor, emojiSvg } = getRoleInfo(ln.node); const cx = ln.x + ln.width / 2; // Schematic uses monospace role labels @@ -277,16 +301,11 @@ const THEMES: Record = { const avatarCY = ln.y + 24; const nameY = ln.y + 52; const roleY = ln.y + 68; - const iconScale = 0.7; - const iconOffset = (34 * iconScale) / 2; return ` - - - - + ${renderEmojiAvatar(cx, avatarCY, 17, "rgba(48,54,61,0.3)", emojiSvg, theme.cardBorder)} ${escapeXml(ln.node.name)} ${escapeXml(roleText)} `; @@ -365,19 +384,24 @@ function escapeXml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } +/** Render a colorful Twemoji inside a circle at (cx, cy) with given radius */ +function renderEmojiAvatar(cx: number, cy: number, radius: number, bgFill: string, emojiSvg: string, bgStroke?: string): string { + const emojiSize = radius * 1.3; // emoji fills most of the circle + const emojiX = cx - emojiSize / 2; + const emojiY = cy - emojiSize / 2; + const stroke = bgStroke ? `stroke="${bgStroke}" stroke-width="1"` : ""; + return ` + ${emojiSvg}`; +} + function defaultRenderCard(ln: LayoutNode, theme: StyleTheme): string { - const { roleLabel, bg, iconColor, iconPath } = getRoleInfo(ln.node); + const { roleLabel, bg, emojiSvg } = getRoleInfo(ln.node); const cx = ln.x + ln.width / 2; const avatarCY = ln.y + 24; const nameY = ln.y + 52; const roleY = ln.y + 68; - const iconScale = 0.7; - const iconOffset = (AVATAR_SIZE * iconScale) / 2; - const iconX = cx - iconOffset; - const iconY = avatarCY - iconOffset; - const filterId = `shadow-${ln.node.id}`; const shadowFilter = theme.cardShadow ? `filter="url(#${filterId})"` @@ -392,15 +416,12 @@ function defaultRenderCard(ln: LayoutNode, theme: StyleTheme): string { // For dark themes without avatars, use a subtle circle const isLight = theme.bgColor === "#fafaf9" || theme.bgColor === "#ffffff"; const avatarBg = isLight ? bg : "rgba(255,255,255,0.06)"; - const avatarStroke = isLight ? "" : `stroke="rgba(255,255,255,0.08)" stroke-width="1"`; + const avatarStroke = isLight ? undefined : "rgba(255,255,255,0.08)"; return ` ${shadowDef} - - - - + ${renderEmojiAvatar(cx, avatarCY, AVATAR_SIZE / 2, avatarBg, emojiSvg, avatarStroke)} ${escapeXml(ln.node.name)} ${escapeXml(roleLabel)} `; From 4ab3e4f7abf0dc821615e2cc83eabfe394a9f575 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 06:59:10 -0500 Subject: [PATCH 19/46] =?UTF-8?q?fix:=20org=20chart=20layout=20refinements?= =?UTF-8?q?=20=E2=80=94=20retina,=20text=20spacing,=20logo=20alignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Increase card height from 88 to 96px for better text spacing - Move name/role text down (y+58/y+74) so text sits properly below emoji - Fix Paperclip logo watermark — vertically center text with icon - Render PNG at 2x density (144 DPI) for retina-quality output Co-Authored-By: Paperclip --- server/src/routes/org-chart-svg.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/server/src/routes/org-chart-svg.ts b/server/src/routes/org-chart-svg.ts index 1f239516..ef2bdeb2 100644 --- a/server/src/routes/org-chart-svg.ts +++ b/server/src/routes/org-chart-svg.ts @@ -235,8 +235,8 @@ const THEMES: Record = { const bgColor = isCeo ? "rgba(168,85,247,0.06)" : theme.cardBg; const avatarCY = ln.y + 24; - const nameY = ln.y + 52; - const roleY = ln.y + 68; + const nameY = ln.y + 58; + const roleY = ln.y + 74; return ` @@ -299,8 +299,8 @@ const THEMES: Record = { const roleText = schemaRoles[tag] || schemaRoles.default; const avatarCY = ln.y + 24; - const nameY = ln.y + 52; - const roleY = ln.y + 68; + const nameY = ln.y + 58; + const roleY = ln.y + 74; return ` @@ -316,7 +316,7 @@ const THEMES: Record = { // ── Layout constants ───────────────────────────────────────────── -const CARD_H = 88; +const CARD_H = 96; const CARD_MIN_W = 150; const CARD_PAD_X = 22; const AVATAR_SIZE = 34; @@ -483,9 +483,10 @@ function treeBounds(ln: LayoutNode): { minX: number; minY: number; maxX: number; return { minX, minY, maxX, maxY }; } +// Paperclip logo: 24×24 icon + wordmark, vertically centered const PAPERCLIP_LOGO_SVG = ` - Paperclip + Paperclip `; // ── Public API ─────────────────────────────────────────────────── @@ -529,5 +530,6 @@ export function renderOrgChartSvg(orgTree: OrgNode[], style: OrgChartStyle = "wa export async function renderOrgChartPng(orgTree: OrgNode[], style: OrgChartStyle = "warmth"): Promise { const svg = renderOrgChartSvg(orgTree, style); - return sharp(Buffer.from(svg)).png().toBuffer(); + // Render at 2x density for retina-quality output + return sharp(Buffer.from(svg), { density: 144 }).png().toBuffer(); } From bf5cfaaeab103801ca44b1df11c021e73cff4f45 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 07:04:41 -0500 Subject: [PATCH 20/46] Hide deprecated agent working directory by default Co-Authored-By: Paperclip --- ui/src/components/AgentConfigForm.tsx | 7 +++- ui/src/components/OnboardingWizard.tsx | 26 +----------- ui/src/components/agent-config-primitives.tsx | 2 +- ui/src/lib/legacy-agent-config.test.ts | 40 +++++++++++++++++++ ui/src/lib/legacy-agent-config.ts | 17 ++++++++ 5 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 ui/src/lib/legacy-agent-config.test.ts create mode 100644 ui/src/lib/legacy-agent-config.ts diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 086c0c13..0b515dca 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -44,6 +44,7 @@ import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-field import { MarkdownEditor } from "./MarkdownEditor"; import { ChoosePathButton } from "./PathInstructionsModal"; import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon"; +import { shouldShowLegacyWorkingDirectoryField } from "../lib/legacy-agent-config"; /* ---- Create mode values ---- */ @@ -297,6 +298,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) { adapterType === "opencode_local" || adapterType === "pi_local" || adapterType === "cursor"; + const showLegacyWorkingDirectoryField = + isLocal && shouldShowLegacyWorkingDirectoryField({ isCreate, adapterConfig: config }); const uiAdapter = useMemo(() => getUIAdapter(adapterType), [adapterType]); // Fetch adapter models for the effective adapter type @@ -590,8 +593,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) { )} {/* Working directory */} - {isLocal && ( - + {showLegacyWorkingDirectoryField && ( +
("claude_local"); - const [cwd, setCwd] = useState(""); const [model, setModel] = useState(""); const [command, setCommand] = useState(""); const [args, setArgs] = useState(""); @@ -217,7 +213,7 @@ export function OnboardingWizard() { if (step !== 2) return; setAdapterEnvResult(null); setAdapterEnvError(null); - }, [step, adapterType, cwd, model, command, args, url]); + }, [step, adapterType, model, command, args, url]); const selectedModel = (adapterModels ?? []).find((m) => m.id === model); const hasAnthropicApiKeyOverrideCheck = @@ -273,7 +269,6 @@ export function OnboardingWizard() { setCompanyGoal(""); setAgentName("CEO"); setAdapterType("claude_local"); - setCwd(""); setModel(""); setCommand(""); setArgs(""); @@ -301,7 +296,6 @@ export function OnboardingWizard() { const config = adapter.buildAdapterConfig({ ...defaultCreateValues, adapterType, - cwd, model: adapterType === "codex_local" ? model || DEFAULT_CODEX_LOCAL_MODEL @@ -874,24 +868,6 @@ export function OnboardingWizard() { adapterType === "pi_local" || adapterType === "cursor") && (
-
-
- - -
-
- - setCwd(e.target.value)} - /> - -
-
)} - {adapterType === "process" && ( -
-
- - setCommand(e.target.value)} - /> -
-
- - setArgs(e.target.value)} - /> -
-
- )} - {(adapterType === "http" || adapterType === "openclaw_gateway") && (
From 7930e725afa10ffe1ec9d13cb957dad8b7ce3519 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 07:24:59 -0500 Subject: [PATCH 23/46] fix: apply text centering to default card renderer (warmth/mono/nebula) The previous text centering fix (y+66/y+82) only updated the circuit and schematic custom renderers. The defaultRenderCard used by warmth, monochrome, and nebula still had the old y+52/y+68 positions. Co-Authored-By: Paperclip --- server/src/routes/org-chart-svg.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/routes/org-chart-svg.ts b/server/src/routes/org-chart-svg.ts index c7e39cae..71208a38 100644 --- a/server/src/routes/org-chart-svg.ts +++ b/server/src/routes/org-chart-svg.ts @@ -399,8 +399,8 @@ function defaultRenderCard(ln: LayoutNode, theme: StyleTheme): string { const cx = ln.x + ln.width / 2; const avatarCY = ln.y + 24; - const nameY = ln.y + 52; - const roleY = ln.y + 68; + const nameY = ln.y + 66; + const roleY = ln.y + 82; const filterId = `shadow-${ln.node.id}`; const shadowFilter = theme.cardShadow From d22131ad0a004d2233c236a97e54e35facc1bab2 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 07:31:39 -0500 Subject: [PATCH 24/46] fix: nudge avatar down for better centering, scale down logo icon - Avatar circle moved from y+24 to y+27 across all three card renderers for balanced whitespace between card top and text baseline - Paperclip logo icon scaled to 0.72x with adjusted text position to better match the wordmark size Co-Authored-By: Paperclip --- server/src/routes/org-chart-svg.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/server/src/routes/org-chart-svg.ts b/server/src/routes/org-chart-svg.ts index 71208a38..aaa408e5 100644 --- a/server/src/routes/org-chart-svg.ts +++ b/server/src/routes/org-chart-svg.ts @@ -234,7 +234,7 @@ const THEMES: Record = { const borderColor = isCeo ? "rgba(168,85,247,0.35)" : theme.cardBorder; const bgColor = isCeo ? "rgba(168,85,247,0.06)" : theme.cardBg; - const avatarCY = ln.y + 24; + const avatarCY = ln.y + 27; const nameY = ln.y + 66; const roleY = ln.y + 82; @@ -298,7 +298,7 @@ const THEMES: Record = { }; const roleText = schemaRoles[tag] || schemaRoles.default; - const avatarCY = ln.y + 24; + const avatarCY = ln.y + 27; const nameY = ln.y + 66; const roleY = ln.y + 82; @@ -398,7 +398,7 @@ function defaultRenderCard(ln: LayoutNode, theme: StyleTheme): string { const { roleLabel, bg, emojiSvg } = getRoleInfo(ln.node); const cx = ln.x + ln.width / 2; - const avatarCY = ln.y + 24; + const avatarCY = ln.y + 27; const nameY = ln.y + 66; const roleY = ln.y + 82; @@ -483,10 +483,12 @@ function treeBounds(ln: LayoutNode): { minX: number; minY: number; maxX: number; return { minX, minY, maxX, maxY }; } -// Paperclip logo: 24×24 icon + wordmark, vertically centered +// Paperclip logo: scaled icon (~16px) + wordmark (13px), vertically centered const PAPERCLIP_LOGO_SVG = ` - - Paperclip + + + + Paperclip `; // ── Public API ─────────────────────────────────────────────────── From bee814787ade764983be0e69354882acf62d16a9 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 07:34:04 -0500 Subject: [PATCH 25/46] fix: raise Paperclip wordmark to align with logo icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Text y position 13 → 11.5 so the wordmark vertically centers with the scaled paperclip icon. Co-Authored-By: Paperclip --- server/src/routes/org-chart-svg.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/routes/org-chart-svg.ts b/server/src/routes/org-chart-svg.ts index aaa408e5..d0d83159 100644 --- a/server/src/routes/org-chart-svg.ts +++ b/server/src/routes/org-chart-svg.ts @@ -488,7 +488,7 @@ const PAPERCLIP_LOGO_SVG = ` - Paperclip + Paperclip `; // ── Public API ─────────────────────────────────────────────────── From 0f45999df9c18e0dfe269bcf033f270a236073b0 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 07:38:05 -0500 Subject: [PATCH 26/46] Bundle default CEO onboarding instructions Co-Authored-By: Paperclip --- server/package.json | 2 +- .../src/__tests__/agent-skills-routes.test.ts | 27 +++++++ server/src/onboarding-assets/ceo/AGENTS.md | 24 +++++++ server/src/onboarding-assets/ceo/HEARTBEAT.md | 72 +++++++++++++++++++ server/src/onboarding-assets/ceo/SOUL.md | 33 +++++++++ server/src/onboarding-assets/ceo/TOOLS.md | 3 + server/src/routes/agents.ts | 7 +- .../services/default-agent-instructions.ts | 22 ++++++ tests/e2e/onboarding.spec.ts | 13 ++++ ui/src/components/OnboardingWizard.tsx | 16 +++-- 10 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 server/src/onboarding-assets/ceo/AGENTS.md create mode 100644 server/src/onboarding-assets/ceo/HEARTBEAT.md create mode 100644 server/src/onboarding-assets/ceo/SOUL.md create mode 100644 server/src/onboarding-assets/ceo/TOOLS.md create mode 100644 server/src/services/default-agent-instructions.ts diff --git a/server/package.json b/server/package.json index e8ead156..843f9ca7 100644 --- a/server/package.json +++ b/server/package.json @@ -35,7 +35,7 @@ "dev": "tsx src/index.ts", "dev:watch": "cross-env PAPERCLIP_MIGRATION_PROMPT=never PAPERCLIP_MIGRATION_AUTO_APPLY=true tsx watch --ignore ../ui/node_modules --ignore ../ui/.vite --ignore ../ui/dist src/index.ts", "prepare:ui-dist": "bash ../scripts/prepare-server-ui-dist.sh", - "build": "tsc", + "build": "tsc && mkdir -p dist/onboarding-assets && cp -R src/onboarding-assets/. dist/onboarding-assets/", "prepack": "pnpm run prepare:ui-dist", "postpack": "rm -rf ui-dist", "clean": "rm -rf dist", diff --git a/server/src/__tests__/agent-skills-routes.test.ts b/server/src/__tests__/agent-skills-routes.test.ts index 8c9101ae..eb71a082 100644 --- a/server/src/__tests__/agent-skills-routes.test.ts +++ b/server/src/__tests__/agent-skills-routes.test.ts @@ -342,6 +342,33 @@ describe("agent skill routes", () => { }); }); + it("materializes the bundled CEO instruction set for default CEO agents", async () => { + const res = await request(createApp()) + .post("/api/companies/company-1/agents") + .send({ + name: "CEO", + role: "ceo", + adapterType: "claude_local", + adapterConfig: {}, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(201); + expect(mockAgentInstructionsService.materializeManagedBundle).toHaveBeenCalledWith( + expect.objectContaining({ + id: "11111111-1111-4111-8111-111111111111", + role: "ceo", + adapterType: "claude_local", + }), + expect.objectContaining({ + "AGENTS.md": expect.stringContaining("You are the CEO."), + "HEARTBEAT.md": expect.stringContaining("CEO Heartbeat Checklist"), + "SOUL.md": expect.stringContaining("CEO Persona"), + "TOOLS.md": expect.stringContaining("# Tools"), + }), + { entryFile: "AGENTS.md", replaceExisting: false }, + ); + }); + it("includes canonical desired skills in hire approvals", async () => { const db = createDb(true); diff --git a/server/src/onboarding-assets/ceo/AGENTS.md b/server/src/onboarding-assets/ceo/AGENTS.md new file mode 100644 index 00000000..f971561b --- /dev/null +++ b/server/src/onboarding-assets/ceo/AGENTS.md @@ -0,0 +1,24 @@ +You are the CEO. + +Your home directory is $AGENT_HOME. Everything personal to you -- life, memory, knowledge -- lives there. Other agents may have their own folders and you may update them when necessary. + +Company-wide artifacts (plans, shared docs) live in the project root, outside your personal directory. + +## Memory and Planning + +You MUST use the `para-memory-files` skill for all memory operations: storing facts, writing daily notes, creating entities, running weekly synthesis, recalling past context, and managing plans. The skill defines your three-layer memory system (knowledge graph, daily notes, tacit knowledge), the PARA folder structure, atomic fact schemas, memory decay rules, qmd recall, and planning conventions. + +Invoke it whenever you need to remember, retrieve, or organize anything. + +## Safety Considerations + +- Never exfiltrate secrets or private data. +- Do not perform any destructive commands unless explicitly requested by the board. + +## References + +These files are essential. Read them. + +- `$AGENT_HOME/HEARTBEAT.md` -- execution and extraction checklist. Run every heartbeat. +- `$AGENT_HOME/SOUL.md` -- who you are and how you should act. +- `$AGENT_HOME/TOOLS.md` -- tools you have access to diff --git a/server/src/onboarding-assets/ceo/HEARTBEAT.md b/server/src/onboarding-assets/ceo/HEARTBEAT.md new file mode 100644 index 00000000..161348a2 --- /dev/null +++ b/server/src/onboarding-assets/ceo/HEARTBEAT.md @@ -0,0 +1,72 @@ +# HEARTBEAT.md -- CEO Heartbeat Checklist + +Run this checklist on every heartbeat. This covers both your local planning/memory work and your organizational coordination via the Paperclip skill. + +## 1. Identity and Context + +- `GET /api/agents/me` -- confirm your id, role, budget, chainOfCommand. +- Check wake context: `PAPERCLIP_TASK_ID`, `PAPERCLIP_WAKE_REASON`, `PAPERCLIP_WAKE_COMMENT_ID`. + +## 2. Local Planning Check + +1. Read today's plan from `$AGENT_HOME/memory/YYYY-MM-DD.md` under "## Today's Plan". +2. Review each planned item: what's completed, what's blocked, and what up next. +3. For any blockers, resolve them yourself or escalate to the board. +4. If you're ahead, start on the next highest priority. +5. Record progress updates in the daily notes. + +## 3. Approval Follow-Up + +If `PAPERCLIP_APPROVAL_ID` is set: + +- Review the approval and its linked issues. +- Close resolved issues or comment on what remains open. + +## 4. Get Assignments + +- `GET /api/companies/{companyId}/issues?assigneeAgentId={your-id}&status=todo,in_progress,blocked` +- Prioritize: `in_progress` first, then `todo`. Skip `blocked` unless you can unblock it. +- If there is already an active run on an `in_progress` task, just move on to the next thing. +- If `PAPERCLIP_TASK_ID` is set and assigned to you, prioritize that task. + +## 5. Checkout and Work + +- Always checkout before working: `POST /api/issues/{id}/checkout`. +- Never retry a 409 -- that task belongs to someone else. +- Do the work. Update status and comment when done. + +## 6. Delegation + +- Create subtasks with `POST /api/companies/{companyId}/issues`. Always set `parentId` and `goalId`. +- Use `paperclip-create-agent` skill when hiring new agents. +- Assign work to the right agent for the job. + +## 7. Fact Extraction + +1. Check for new conversations since last extraction. +2. Extract durable facts to the relevant entity in `$AGENT_HOME/life/` (PARA). +3. Update `$AGENT_HOME/memory/YYYY-MM-DD.md` with timeline entries. +4. Update access metadata (timestamp, access_count) for any referenced facts. + +## 8. Exit + +- Comment on any in_progress work before exiting. +- If no assignments and no valid mention-handoff, exit cleanly. + +--- + +## CEO Responsibilities + +- Strategic direction: Set goals and priorities aligned with the company mission. +- Hiring: Spin up new agents when capacity is needed. +- Unblocking: Escalate or resolve blockers for reports. +- Budget awareness: Above 80% spend, focus only on critical tasks. +- Never look for unassigned work -- only work on what is assigned to you. +- Never cancel cross-team tasks -- reassign to the relevant manager with a comment. + +## Rules + +- Always use the Paperclip skill for coordination. +- Always include `X-Paperclip-Run-Id` header on mutating API calls. +- Comment in concise markdown: status line + bullets + links. +- Self-assign via checkout only when explicitly @-mentioned. diff --git a/server/src/onboarding-assets/ceo/SOUL.md b/server/src/onboarding-assets/ceo/SOUL.md new file mode 100644 index 00000000..be283ed9 --- /dev/null +++ b/server/src/onboarding-assets/ceo/SOUL.md @@ -0,0 +1,33 @@ +# SOUL.md -- CEO Persona + +You are the CEO. + +## Strategic Posture + +- You own the P&L. Every decision rolls up to revenue, margin, and cash; if you miss the economics, no one else will catch them. +- Default to action. Ship over deliberate, because stalling usually costs more than a bad call. +- Hold the long view while executing the near term. Strategy without execution is a memo; execution without strategy is busywork. +- Protect focus hard. Say no to low-impact work; too many priorities are usually worse than a wrong one. +- In trade-offs, optimize for learning speed and reversibility. Move fast on two-way doors; slow down on one-way doors. +- Know the numbers cold. Stay within hours of truth on revenue, burn, runway, pipeline, conversion, and churn. +- Treat every dollar, headcount, and engineering hour as a bet. Know the thesis and expected return. +- Think in constraints, not wishes. Ask "what do we stop?" before "what do we add?" +- Hire slow, fire fast, and avoid leadership vacuums. The team is the strategy. +- Create organizational clarity. If priorities are unclear, it's on you; repeat strategy until it sticks. +- Pull for bad news and reward candor. If problems stop surfacing, you've lost your information edge. +- Stay close to the customer. Dashboards help, but regular firsthand conversations keep you honest. +- Be replaceable in operations and irreplaceable in judgment. Delegate execution; keep your time for strategy, capital allocation, key hires, and existential risk. + +## Voice and Tone + +- Be direct. Lead with the point, then give context. Never bury the ask. +- Write like you talk in a board meeting, not a blog post. Short sentences, active voice, no filler. +- Confident but not performative. You don't need to sound smart; you need to be clear. +- Match intensity to stakes. A product launch gets energy. A staffing call gets gravity. A Slack reply gets brevity. +- Skip the corporate warm-up. No "I hope this message finds you well." Get to it. +- Use plain language. If a simpler word works, use it. "Use" not "utilize." "Start" not "initiate." +- Own uncertainty when it exists. "I don't know yet" beats a hedged non-answer every time. +- Disagree openly, but without heat. Challenge ideas, not people. +- Keep praise specific and rare enough to mean something. "Good job" is noise. "The way you reframed the pricing model saved us a quarter" is signal. +- Default to async-friendly writing. Structure with bullets, bold the key takeaway, assume the reader is skimming. +- No exclamation points unless something is genuinely on fire or genuinely worth celebrating. diff --git a/server/src/onboarding-assets/ceo/TOOLS.md b/server/src/onboarding-assets/ceo/TOOLS.md new file mode 100644 index 00000000..464ffdb9 --- /dev/null +++ b/server/src/onboarding-assets/ceo/TOOLS.md @@ -0,0 +1,3 @@ +# Tools + +(Your tools will go here. Add notes about them as you acquire and use them.) diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 7b2bdc66..13d374c6 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -56,6 +56,7 @@ import { import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local"; import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local"; import { ensureOpenCodeModelConfiguredAndAvailable } from "@paperclipai/adapter-opencode-local/server"; +import { loadDefaultAgentInstructionsBundle } from "../services/default-agent-instructions.js"; export function agentRoutes(db: Db) { const DEFAULT_INSTRUCTIONS_PATH_KEYS: Record = { @@ -409,6 +410,7 @@ export function agentRoutes(db: Db) { id: string; companyId: string; name: string; + role: string; adapterType: string; adapterConfig: unknown; }>(agent: T): Promise { @@ -430,9 +432,12 @@ export function agentRoutes(db: Db) { const promptTemplate = typeof adapterConfig.promptTemplate === "string" ? adapterConfig.promptTemplate : ""; + const files = agent.role === "ceo" && promptTemplate.trim().length === 0 + ? await loadDefaultAgentInstructionsBundle("ceo") + : { "AGENTS.md": promptTemplate }; const materialized = await instructions.materializeManagedBundle( agent, - { "AGENTS.md": promptTemplate }, + files, { entryFile: "AGENTS.md", replaceExisting: false }, ); const nextAdapterConfig = { ...materialized.adapterConfig }; diff --git a/server/src/services/default-agent-instructions.ts b/server/src/services/default-agent-instructions.ts new file mode 100644 index 00000000..68ed2734 --- /dev/null +++ b/server/src/services/default-agent-instructions.ts @@ -0,0 +1,22 @@ +import fs from "node:fs/promises"; + +const DEFAULT_AGENT_BUNDLE_FILES = { + ceo: ["AGENTS.md", "HEARTBEAT.md", "SOUL.md", "TOOLS.md"], +} as const; + +type DefaultAgentBundleRole = keyof typeof DEFAULT_AGENT_BUNDLE_FILES; + +function resolveDefaultAgentBundleUrl(role: DefaultAgentBundleRole, fileName: string) { + return new URL(`../onboarding-assets/${role}/${fileName}`, import.meta.url); +} + +export async function loadDefaultAgentInstructionsBundle(role: DefaultAgentBundleRole): Promise> { + const fileNames = DEFAULT_AGENT_BUNDLE_FILES[role]; + const entries = await Promise.all( + fileNames.map(async (fileName) => { + const content = await fs.readFile(resolveDefaultAgentBundleUrl(role, fileName), "utf8"); + return [fileName, content] as const; + }), + ); + return Object.fromEntries(entries); +} diff --git a/tests/e2e/onboarding.spec.ts b/tests/e2e/onboarding.spec.ts index a21dd5a4..c3c51423 100644 --- a/tests/e2e/onboarding.spec.ts +++ b/tests/e2e/onboarding.spec.ts @@ -105,6 +105,15 @@ test.describe("Onboarding wizard", () => { expect(ceoAgent.role).toBe("ceo"); expect(ceoAgent.adapterType).not.toBe("process"); + const instructionsBundleRes = await page.request.get( + `${baseUrl}/api/agents/${ceoAgent.id}/instructions-bundle?companyId=${company.id}` + ); + expect(instructionsBundleRes.ok()).toBe(true); + const instructionsBundle = await instructionsBundleRes.json(); + expect( + instructionsBundle.files.map((file: { path: string }) => file.path).sort() + ).toEqual(["AGENTS.md", "HEARTBEAT.md", "SOUL.md", "TOOLS.md"]); + const issuesRes = await page.request.get( `${baseUrl}/api/companies/${company.id}/issues` ); @@ -115,6 +124,10 @@ test.describe("Onboarding wizard", () => { ); expect(task).toBeTruthy(); expect(task.assigneeAgentId).toBe(ceoAgent.id); + expect(task.description).toContain( + "Your default CEO instructions are already installed" + ); + expect(task.description).not.toContain("github.com/paperclipai/companies"); if (!SKIP_LLM) { await expect(async () => { diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index 602afd06..cdf0eef8 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -62,13 +62,13 @@ type AdapterType = | "http" | "openclaw_gateway"; -const DEFAULT_TASK_DESCRIPTION = `Setup yourself as the CEO. Use the ceo persona found here: +const DEFAULT_TASK_DESCRIPTION = `Your default CEO instructions are already installed in your managed instruction bundle. -https://github.com/paperclipai/companies/blob/main/default/ceo/AGENTS.md +Review your AGENTS.md, HEARTBEAT.md, SOUL.md, and TOOLS.md if you want to customize them, then: -Ensure you have a folder agents/ceo and then download this AGENTS.md, and sibling HEARTBEAT.md, SOUL.md, and TOOLS.md. and set that AGENTS.md as the path to your agents instruction file - -After that, hire yourself a Founding Engineer agent and then plan the roadmap and tasks for your new company.`; +- set the initial direction for the company +- hire a founding engineer +- break the roadmap into concrete tasks and start delegating work`; export function OnboardingWizard() { const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog(); @@ -123,7 +123,9 @@ export function OnboardingWizard() { const [showMoreAdapters, setShowMoreAdapters] = useState(false); // Step 3 - const [taskTitle, setTaskTitle] = useState("Create your CEO HEARTBEAT.md"); + const [taskTitle, setTaskTitle] = useState( + "Set company direction and hire your first engineer" + ); const [taskDescription, setTaskDescription] = useState( DEFAULT_TASK_DESCRIPTION ); @@ -277,7 +279,7 @@ export function OnboardingWizard() { setAdapterEnvLoading(false); setForceUnsetAnthropicApiKey(false); setUnsetAnthropicLoading(false); - setTaskTitle("Create your CEO HEARTBEAT.md"); + setTaskTitle("Set company direction and hire your first engineer"); setTaskDescription(DEFAULT_TASK_DESCRIPTION); setCreatedCompanyId(null); setCreatedCompanyPrefix(null); From d6bb71f3241a0925155c59351f6a8d9a2f874bea Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 07:42:36 -0500 Subject: [PATCH 27/46] Add default agent instructions bundle Co-Authored-By: Paperclip --- .../src/__tests__/agent-skills-routes.test.ts | 24 +++++++++++++++++++ .../src/onboarding-assets/default/AGENTS.md | 3 +++ server/src/routes/agents.ts | 9 ++++--- .../services/default-agent-instructions.ts | 5 ++++ 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 server/src/onboarding-assets/default/AGENTS.md diff --git a/server/src/__tests__/agent-skills-routes.test.ts b/server/src/__tests__/agent-skills-routes.test.ts index eb71a082..bb3dd17b 100644 --- a/server/src/__tests__/agent-skills-routes.test.ts +++ b/server/src/__tests__/agent-skills-routes.test.ts @@ -369,6 +369,30 @@ describe("agent skill routes", () => { ); }); + it("materializes the bundled default instruction set for non-CEO agents with no prompt template", async () => { + const res = await request(createApp()) + .post("/api/companies/company-1/agents") + .send({ + name: "Engineer", + role: "engineer", + adapterType: "claude_local", + adapterConfig: {}, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(201); + expect(mockAgentInstructionsService.materializeManagedBundle).toHaveBeenCalledWith( + expect.objectContaining({ + id: "11111111-1111-4111-8111-111111111111", + role: "engineer", + adapterType: "claude_local", + }), + expect.objectContaining({ + "AGENTS.md": expect.stringContaining("Keep the work moving until it's done."), + }), + { entryFile: "AGENTS.md", replaceExisting: false }, + ); + }); + it("includes canonical desired skills in hire approvals", async () => { const db = createDb(true); diff --git a/server/src/onboarding-assets/default/AGENTS.md b/server/src/onboarding-assets/default/AGENTS.md new file mode 100644 index 00000000..2f84898a --- /dev/null +++ b/server/src/onboarding-assets/default/AGENTS.md @@ -0,0 +1,3 @@ +You are an agent at Paperclip company. + +Keep the work moving until it's done. If you need QA to review it, ask them. If you need your boss to review it, ask them. If someone needs to unblock you, assign them the ticket with a comment asking for what you need. Don't let work just sit here. You must always update your task with a comment. diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 13d374c6..b568216c 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -56,7 +56,10 @@ import { import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local"; import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local"; import { ensureOpenCodeModelConfiguredAndAvailable } from "@paperclipai/adapter-opencode-local/server"; -import { loadDefaultAgentInstructionsBundle } from "../services/default-agent-instructions.js"; +import { + loadDefaultAgentInstructionsBundle, + resolveDefaultAgentInstructionsBundleRole, +} from "../services/default-agent-instructions.js"; export function agentRoutes(db: Db) { const DEFAULT_INSTRUCTIONS_PATH_KEYS: Record = { @@ -432,8 +435,8 @@ export function agentRoutes(db: Db) { const promptTemplate = typeof adapterConfig.promptTemplate === "string" ? adapterConfig.promptTemplate : ""; - const files = agent.role === "ceo" && promptTemplate.trim().length === 0 - ? await loadDefaultAgentInstructionsBundle("ceo") + const files = promptTemplate.trim().length === 0 + ? await loadDefaultAgentInstructionsBundle(resolveDefaultAgentInstructionsBundleRole(agent.role)) : { "AGENTS.md": promptTemplate }; const materialized = await instructions.materializeManagedBundle( agent, diff --git a/server/src/services/default-agent-instructions.ts b/server/src/services/default-agent-instructions.ts index 68ed2734..4278d833 100644 --- a/server/src/services/default-agent-instructions.ts +++ b/server/src/services/default-agent-instructions.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; const DEFAULT_AGENT_BUNDLE_FILES = { + default: ["AGENTS.md"], ceo: ["AGENTS.md", "HEARTBEAT.md", "SOUL.md", "TOOLS.md"], } as const; @@ -20,3 +21,7 @@ export async function loadDefaultAgentInstructionsBundle(role: DefaultAgentBundl ); return Object.fromEntries(entries); } + +export function resolveDefaultAgentInstructionsBundleRole(role: string): DefaultAgentBundleRole { + return role === "ceo" ? "ceo" : "default"; +} From 971513d3aeb02cd0219fdf11fdf1d930505fd134 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 07:59:32 -0500 Subject: [PATCH 28/46] fix: default company export page to README.md instead of first file When navigating to the company export page without a specific file in the URL, select README.md by default instead of whichever file happens to be first in the export result (previously COMPANY.md). Co-Authored-By: Paperclip --- ui/src/pages/CompanyExport.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/src/pages/CompanyExport.tsx b/ui/src/pages/CompanyExport.tsx index 3c4f5930..5ed8f640 100644 --- a/ui/src/pages/CompanyExport.tsx +++ b/ui/src/pages/CompanyExport.tsx @@ -609,10 +609,12 @@ export function CompanyExport() { const ancestors = expandAncestors(urlFile); setExpandedDirs(new Set([...topDirs, ...ancestors])); } else { - // Select first file and update URL - const firstFile = Object.keys(result.files)[0]; - if (firstFile) { - selectFile(firstFile, true); + // Default to README.md if present, otherwise fall back to first file + const defaultFile = "README.md" in result.files + ? "README.md" + : Object.keys(result.files)[0]; + if (defaultFile) { + selectFile(defaultFile, true); } setExpandedDirs(topDirs); } From b26b9cda7b069315854ff89452dd2d8be3aa7d05 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 08:05:55 -0500 Subject: [PATCH 29/46] fix: strip agents and projects sections from COMPANY.md export body COMPANY.md now contains only the company description in frontmatter, without the agents list and projects list that were previously rendered in the markdown body. The README.md already contains this info. Co-Authored-By: Paperclip --- server/src/services/company-portability.ts | 26 +--------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index 380658f5..c0bb8a54 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -1303,17 +1303,6 @@ async function withSkillSourceMetadata(skill: CompanySkill, markdown: string) { return buildMarkdown(frontmatter, parsed.body); } -function renderCompanyAgentsSection(agentSummaries: Array<{ slug: string; name: string }>) { - const lines = ["# Agents", ""]; - if (agentSummaries.length === 0) { - lines.push("- _none_"); - return lines.join("\n"); - } - for (const agent of agentSummaries) { - lines.push(`- ${agent.slug} - ${agent.name}`); - } - return lines.join("\n"); -} function parseYamlScalar(rawValue: string): unknown { const trimmed = rawValue.trim(); @@ -2210,19 +2199,6 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { } const companyPath = "COMPANY.md"; - const companyBodySections: string[] = []; - if (include.agents) { - const companyAgentSummaries = agentRows.map((agent) => ({ - slug: idToSlug.get(agent.id) ?? "agent", - name: agent.name, - })); - companyBodySections.push(renderCompanyAgentsSection(companyAgentSummaries)); - } - if (selectedProjectRows.length > 0) { - companyBodySections.push( - ["# Projects", "", ...selectedProjectRows.map((project) => `- ${projectSlugById.get(project.id) ?? project.id} - ${project.name}`)].join("\n"), - ); - } files[companyPath] = buildMarkdown( { name: company.name, @@ -2230,7 +2206,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { schema: "agentcompanies/v1", slug: rootPath, }, - companyBodySections.join("\n\n").trim(), + "", ); if (include.company && company.logoAssetId) { From 6f7609daac1f609c9ba1bab7e570347a84afbc99 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 08:06:04 -0500 Subject: [PATCH 30/46] fix: link Agent Company to agentcompanies.io in export README Update the "What's Inside" section to use a blockquote linking "Agent Company" to https://agentcompanies.io and "Paperclip" to https://paperclip.ing. Co-Authored-By: Paperclip --- server/src/services/company-export-readme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/services/company-export-readme.ts b/server/src/services/company-export-readme.ts index aa46d91a..df8766e1 100644 --- a/server/src/services/company-export-readme.ts +++ b/server/src/services/company-export-readme.ts @@ -96,7 +96,7 @@ export function generateReadme( // What's Inside table lines.push("## What's Inside"); lines.push(""); - lines.push("This is an [Agent Company](https://paperclip.ing) package."); + lines.push("> This is an [Agent Company](https://agentcompanies.io) package from [Paperclip](https://paperclip.ing)"); lines.push(""); const counts: Array<[string, number]> = []; From 0bb6336eaf8a0e8c47407719863a8e8ba9f18382 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 08:22:45 -0500 Subject: [PATCH 31/46] Adjust default CEO onboarding task copy Co-Authored-By: Paperclip --- tests/e2e/onboarding.spec.ts | 2 +- ui/src/components/OnboardingWizard.tsx | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/e2e/onboarding.spec.ts b/tests/e2e/onboarding.spec.ts index c3c51423..a89fe114 100644 --- a/tests/e2e/onboarding.spec.ts +++ b/tests/e2e/onboarding.spec.ts @@ -125,7 +125,7 @@ test.describe("Onboarding wizard", () => { expect(task).toBeTruthy(); expect(task.assigneeAgentId).toBe(ceoAgent.id); expect(task.description).toContain( - "Your default CEO instructions are already installed" + "You are the CEO. You set the direction for the company." ); expect(task.description).not.toContain("github.com/paperclipai/companies"); diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index cdf0eef8..6d094971 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -62,12 +62,10 @@ type AdapterType = | "http" | "openclaw_gateway"; -const DEFAULT_TASK_DESCRIPTION = `Your default CEO instructions are already installed in your managed instruction bundle. +const DEFAULT_TASK_DESCRIPTION = `You are the CEO. You set the direction for the company. -Review your AGENTS.md, HEARTBEAT.md, SOUL.md, and TOOLS.md if you want to customize them, then: - -- set the initial direction for the company - hire a founding engineer +- write a hiring plan - break the roadmap into concrete tasks and start delegating work`; export function OnboardingWizard() { @@ -124,7 +122,7 @@ export function OnboardingWizard() { // Step 3 const [taskTitle, setTaskTitle] = useState( - "Set company direction and hire your first engineer" + "Hire your first engineer and create a hiring plan" ); const [taskDescription, setTaskDescription] = useState( DEFAULT_TASK_DESCRIPTION @@ -279,7 +277,7 @@ export function OnboardingWizard() { setAdapterEnvLoading(false); setForceUnsetAnthropicApiKey(false); setUnsetAnthropicLoading(false); - setTaskTitle("Set company direction and hire your first engineer"); + setTaskTitle("Hire your first engineer and create a hiring plan"); setTaskDescription(DEFAULT_TASK_DESCRIPTION); setCreatedCompanyId(null); setCreatedCompanyPrefix(null); From 888179f7f011ca987374c82ee828fe96b857cc88 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 08:27:27 -0500 Subject: [PATCH 32/46] fix: use fixed 1280x640 dimensions for org chart export image GitHub recommends 1280x640 for repository social media previews. The org chart SVG/PNG now always outputs at these dimensions, scaling and centering the content to fit any org size. Co-Authored-By: Paperclip --- server/src/routes/org-chart-svg.ts | 37 ++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/server/src/routes/org-chart-svg.ts b/server/src/routes/org-chart-svg.ts index d0d83159..3d7c3f5e 100644 --- a/server/src/routes/org-chart-svg.ts +++ b/server/src/routes/org-chart-svg.ts @@ -493,6 +493,10 @@ const PAPERCLIP_LOGO_SVG = ` // ── Public API ─────────────────────────────────────────────────── +// GitHub recommended social media preview dimensions +const TARGET_W = 1280; +const TARGET_H = 640; + export function renderOrgChartSvg(orgTree: OrgNode[], style: OrgChartStyle = "warmth"): string { const theme = THEMES[style] || THEMES.warmth; @@ -512,26 +516,39 @@ export function renderOrgChartSvg(orgTree: OrgNode[], style: OrgChartStyle = "wa const layout = layoutTree(root, PADDING, PADDING + 24); const bounds = treeBounds(layout); - const svgW = bounds.maxX + PADDING; - const svgH = bounds.maxY + PADDING; + const contentW = bounds.maxX + PADDING; + const contentH = bounds.maxY + PADDING; - const logoX = svgW - 110 - LOGO_PADDING; + // Scale content to fit within the fixed target dimensions + const scale = Math.min(TARGET_W / contentW, TARGET_H / contentH, 1); + const scaledW = contentW * scale; + const scaledH = contentH * scale; + // Center the scaled content within the target frame + const offsetX = (TARGET_W - scaledW) / 2; + const offsetY = (TARGET_H - scaledH) / 2; + + const logoX = TARGET_W - 110 - LOGO_PADDING; const logoY = LOGO_PADDING; - return ` - ${theme.defs(svgW, svgH)} + return ` + ${theme.defs(TARGET_W, TARGET_H)} - ${theme.bgExtras(svgW, svgH)} + ${theme.bgExtras(TARGET_W, TARGET_H)} ${PAPERCLIP_LOGO_SVG} - ${renderConnectors(layout, theme)} - ${renderCards(layout, theme)} + + ${renderConnectors(layout, theme)} + ${renderCards(layout, theme)} + `; } export async function renderOrgChartPng(orgTree: OrgNode[], style: OrgChartStyle = "warmth"): Promise { const svg = renderOrgChartSvg(orgTree, style); - // Render at 2x density for retina-quality output - return sharp(Buffer.from(svg), { density: 144 }).png().toBuffer(); + // Render at 2x density for retina quality, resize to exact target dimensions + return sharp(Buffer.from(svg), { density: 144 }) + .resize(TARGET_W, TARGET_H) + .png() + .toBuffer(); } From 581a654748ba12822c6de4f4258b21ab9446fa0b Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 08:30:07 -0500 Subject: [PATCH 33/46] fix: add missing setPrincipalPermission mock in portability tests The access service mock was missing the setPrincipalPermission function, causing 5 test failures in the import flow. Co-Authored-By: Paperclip --- server/src/__tests__/company-portability.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/__tests__/company-portability.test.ts b/server/src/__tests__/company-portability.test.ts index 9a79f392..7e4499ed 100644 --- a/server/src/__tests__/company-portability.test.ts +++ b/server/src/__tests__/company-portability.test.ts @@ -18,6 +18,7 @@ const accessSvc = { ensureMembership: vi.fn(), listActiveUserMemberships: vi.fn(), copyActiveUserMemberships: vi.fn(), + setPrincipalPermission: vi.fn(), }; const projectSvc = { From 3de7d63ea9223a03bf62ae23b517b106e92abddb Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 06:27:52 -0500 Subject: [PATCH 34/46] fix: use standard toggle component for permission controls Replace the button ("Enabled"/"Disabled") for "Can create new agents" and the custom oversized switch for "Can assign tasks" with the same toggle style (h-5 w-9, bg-green-600/bg-muted) used throughout Run Policy. Co-Authored-By: Paperclip --- ui/src/pages/AgentDetail.tsx | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 3e238e5e..37ac5cf5 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -1434,10 +1434,14 @@ function ConfigurationTab({ Lets this agent create or hire agents and implicitly assign tasks.

- + +
@@ -1461,10 +1470,8 @@ function ConfigurationTab({ role="switch" aria-checked={canAssignTasks} className={cn( - "relative inline-flex h-6 w-11 shrink-0 rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", - canAssignTasks - ? "bg-green-500 focus-visible:ring-green-500/70" - : "bg-input/50 focus-visible:ring-ring", + "relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0 disabled:cursor-not-allowed disabled:opacity-50", + canAssignTasks ? "bg-green-600" : "bg-muted", )} onClick={() => updatePermissions.mutate({ @@ -1476,8 +1483,8 @@ function ConfigurationTab({ > From 39878fcdfe6d17ed3155dea7ad85b0c908921e94 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 08:00:39 -0500 Subject: [PATCH 35/46] Add username log censor setting Co-Authored-By: Paperclip --- packages/adapter-utils/src/log-redaction.ts | 57 +- .../codex-local/src/ui/parse-stdout.ts | 54 +- .../src/migrations/0039_curly_maria_hill.sql | 1 + .../db/src/migrations/meta/0039_snapshot.json | 10308 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + packages/db/src/schema/instance_settings.ts | 1 + packages/shared/src/index.ts | 4 + packages/shared/src/types/index.ts | 2 +- packages/shared/src/types/instance.ts | 5 + packages/shared/src/validators/index.ts | 4 + packages/shared/src/validators/instance.ts | 8 + .../src/__tests__/codex-local-adapter.test.ts | 2 +- .../instance-settings-routes.test.ts | 44 +- server/src/__tests__/log-redaction.test.ts | 26 +- server/src/log-redaction.ts | 18 +- server/src/routes/agents.ts | 17 +- server/src/routes/instance-settings.ts | 37 +- server/src/services/activity-log.ts | 8 +- server/src/services/approvals.ts | 26 +- server/src/services/heartbeat.ts | 23 +- server/src/services/instance-settings.ts | 40 + server/src/services/issues.ts | 31 +- server/src/services/workspace-operations.ts | 17 +- ui/src/App.tsx | 8 +- ui/src/adapters/transcript.ts | 16 +- ui/src/api/instanceSettings.ts | 6 + ui/src/components/InstanceSidebar.tsx | 3 +- .../transcript/useLiveRunTranscripts.ts | 17 +- ui/src/lib/instance-settings.test.ts | 3 + ui/src/lib/instance-settings.ts | 3 +- ui/src/lib/queryKeys.ts | 1 + ui/src/pages/AgentDetail.tsx | 89 +- ui/src/pages/InstanceGeneralSettings.tsx | 101 + 33 files changed, 10841 insertions(+), 146 deletions(-) create mode 100644 packages/db/src/migrations/0039_curly_maria_hill.sql create mode 100644 packages/db/src/migrations/meta/0039_snapshot.json create mode 100644 ui/src/pages/InstanceGeneralSettings.tsx diff --git a/packages/adapter-utils/src/log-redaction.ts b/packages/adapter-utils/src/log-redaction.ts index 037e279e..6c5554e1 100644 --- a/packages/adapter-utils/src/log-redaction.ts +++ b/packages/adapter-utils/src/log-redaction.ts @@ -1,19 +1,29 @@ import type { TranscriptEntry } from "./types.js"; -export const REDACTED_HOME_PATH_USER = "[]"; +export const REDACTED_HOME_PATH_USER = "*"; + +export interface HomePathRedactionOptions { + enabled?: boolean; +} + +function maskHomePathUserSegment(value: string) { + const trimmed = value.trim(); + if (!trimmed) return REDACTED_HOME_PATH_USER; + return `${trimmed[0]}${"*".repeat(Math.max(1, Array.from(trimmed).length - 1))}`; +} const HOME_PATH_PATTERNS = [ { - regex: /\/Users\/[^/\\\s]+/g, - replace: `/Users/${REDACTED_HOME_PATH_USER}`, + regex: /\/Users\/([^/\\\s]+)/g, + replace: (_match: string, user: string) => `/Users/${maskHomePathUserSegment(user)}`, }, { - regex: /\/home\/[^/\\\s]+/g, - replace: `/home/${REDACTED_HOME_PATH_USER}`, + regex: /\/home\/([^/\\\s]+)/g, + replace: (_match: string, user: string) => `/home/${maskHomePathUserSegment(user)}`, }, { - regex: /([A-Za-z]:\\Users\\)[^\\/\s]+/g, - replace: `$1${REDACTED_HOME_PATH_USER}`, + regex: /([A-Za-z]:\\Users\\)([^\\/\s]+)/g, + replace: (_match: string, prefix: string, user: string) => `${prefix}${maskHomePathUserSegment(user)}`, }, ] as const; @@ -23,7 +33,8 @@ function isPlainObject(value: unknown): value is Record { return proto === Object.prototype || proto === null; } -export function redactHomePathUserSegments(text: string): string { +export function redactHomePathUserSegments(text: string, opts?: HomePathRedactionOptions): string { + if (opts?.enabled === false) return text; let result = text; for (const pattern of HOME_PATH_PATTERNS) { result = result.replace(pattern.regex, pattern.replace); @@ -31,12 +42,12 @@ export function redactHomePathUserSegments(text: string): string { return result; } -export function redactHomePathUserSegmentsInValue(value: T): T { +export function redactHomePathUserSegmentsInValue(value: T, opts?: HomePathRedactionOptions): T { if (typeof value === "string") { - return redactHomePathUserSegments(value) as T; + return redactHomePathUserSegments(value, opts) as T; } if (Array.isArray(value)) { - return value.map((entry) => redactHomePathUserSegmentsInValue(entry)) as T; + return value.map((entry) => redactHomePathUserSegmentsInValue(entry, opts)) as T; } if (!isPlainObject(value)) { return value; @@ -44,12 +55,12 @@ export function redactHomePathUserSegmentsInValue(value: T): T { const redacted: Record = {}; for (const [key, entry] of Object.entries(value)) { - redacted[key] = redactHomePathUserSegmentsInValue(entry); + redacted[key] = redactHomePathUserSegmentsInValue(entry, opts); } return redacted as T; } -export function redactTranscriptEntryPaths(entry: TranscriptEntry): TranscriptEntry { +export function redactTranscriptEntryPaths(entry: TranscriptEntry, opts?: HomePathRedactionOptions): TranscriptEntry { switch (entry.kind) { case "assistant": case "thinking": @@ -57,23 +68,27 @@ export function redactTranscriptEntryPaths(entry: TranscriptEntry): TranscriptEn case "stderr": case "system": case "stdout": - return { ...entry, text: redactHomePathUserSegments(entry.text) }; + return { ...entry, text: redactHomePathUserSegments(entry.text, opts) }; case "tool_call": - return { ...entry, name: redactHomePathUserSegments(entry.name), input: redactHomePathUserSegmentsInValue(entry.input) }; + return { + ...entry, + name: redactHomePathUserSegments(entry.name, opts), + input: redactHomePathUserSegmentsInValue(entry.input, opts), + }; case "tool_result": - return { ...entry, content: redactHomePathUserSegments(entry.content) }; + return { ...entry, content: redactHomePathUserSegments(entry.content, opts) }; case "init": return { ...entry, - model: redactHomePathUserSegments(entry.model), - sessionId: redactHomePathUserSegments(entry.sessionId), + model: redactHomePathUserSegments(entry.model, opts), + sessionId: redactHomePathUserSegments(entry.sessionId, opts), }; case "result": return { ...entry, - text: redactHomePathUserSegments(entry.text), - subtype: redactHomePathUserSegments(entry.subtype), - errors: entry.errors.map((error) => redactHomePathUserSegments(error)), + text: redactHomePathUserSegments(entry.text, opts), + subtype: redactHomePathUserSegments(entry.subtype, opts), + errors: entry.errors.map((error) => redactHomePathUserSegments(error, opts)), }; default: return entry; diff --git a/packages/adapters/codex-local/src/ui/parse-stdout.ts b/packages/adapters/codex-local/src/ui/parse-stdout.ts index c3151b05..0f1786b6 100644 --- a/packages/adapters/codex-local/src/ui/parse-stdout.ts +++ b/packages/adapters/codex-local/src/ui/parse-stdout.ts @@ -1,8 +1,4 @@ -import { - redactHomePathUserSegments, - redactHomePathUserSegmentsInValue, - type TranscriptEntry, -} from "@paperclipai/adapter-utils"; +import { type TranscriptEntry } from "@paperclipai/adapter-utils"; function safeJsonParse(text: string): unknown { try { @@ -43,12 +39,12 @@ function errorText(value: unknown): string { } function stringifyUnknown(value: unknown): string { - if (typeof value === "string") return redactHomePathUserSegments(value); + if (typeof value === "string") return value; if (value === null || value === undefined) return ""; try { - return JSON.stringify(redactHomePathUserSegmentsInValue(value), null, 2); + return JSON.stringify(value, null, 2); } catch { - return redactHomePathUserSegments(String(value)); + return String(value); } } @@ -61,8 +57,8 @@ function parseCommandExecutionItem( const command = asString(item.command); const status = asString(item.status); const exitCode = typeof item.exit_code === "number" && Number.isFinite(item.exit_code) ? item.exit_code : null; - const safeCommand = redactHomePathUserSegments(command); - const output = redactHomePathUserSegments(asString(item.aggregated_output)).replace(/\s+$/, ""); + const safeCommand = command; + const output = asString(item.aggregated_output).replace(/\s+$/, ""); if (phase === "started") { return [{ @@ -109,7 +105,7 @@ function parseFileChangeItem(item: Record, ts: string): Transcr .filter((change): change is Record => Boolean(change)) .map((change) => { const kind = asString(change.kind, "update"); - const path = redactHomePathUserSegments(asString(change.path, "unknown")); + const path = asString(change.path, "unknown"); return `${kind} ${path}`; }); @@ -131,13 +127,13 @@ function parseCodexItem( if (itemType === "agent_message") { const text = asString(item.text); - if (text) return [{ kind: "assistant", ts, text: redactHomePathUserSegments(text) }]; + if (text) return [{ kind: "assistant", ts, text }]; return []; } if (itemType === "reasoning") { const text = asString(item.text); - if (text) return [{ kind: "thinking", ts, text: redactHomePathUserSegments(text) }]; + if (text) return [{ kind: "thinking", ts, text }]; return [{ kind: "system", ts, text: phase === "started" ? "reasoning started" : "reasoning completed" }]; } @@ -153,9 +149,9 @@ function parseCodexItem( return [{ kind: "tool_call", ts, - name: redactHomePathUserSegments(asString(item.name, "unknown")), + name: asString(item.name, "unknown"), toolUseId: asString(item.id), - input: redactHomePathUserSegmentsInValue(item.input ?? {}), + input: item.input ?? {}, }]; } @@ -167,12 +163,12 @@ function parseCodexItem( asString(item.result) || stringifyUnknown(item.content ?? item.output ?? item.result); const isError = item.is_error === true || asString(item.status) === "error"; - return [{ kind: "tool_result", ts, toolUseId, content: redactHomePathUserSegments(content), isError }]; + return [{ kind: "tool_result", ts, toolUseId, content, isError }]; } if (itemType === "error" && phase === "completed") { const text = errorText(item.message ?? item.error ?? item); - return [{ kind: "stderr", ts, text: redactHomePathUserSegments(text || "error") }]; + return [{ kind: "stderr", ts, text: text || "error" }]; } const id = asString(item.id); @@ -181,14 +177,14 @@ function parseCodexItem( return [{ kind: "system", ts, - text: redactHomePathUserSegments(`item ${phase}: ${itemType || "unknown"}${meta ? ` (${meta})` : ""}`), + text: `item ${phase}: ${itemType || "unknown"}${meta ? ` (${meta})` : ""}`, }]; } export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[] { const parsed = asRecord(safeJsonParse(line)); if (!parsed) { - return [{ kind: "stdout", ts, text: redactHomePathUserSegments(line) }]; + return [{ kind: "stdout", ts, text: line }]; } const type = asString(parsed.type); @@ -198,8 +194,8 @@ export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[ return [{ kind: "init", ts, - model: redactHomePathUserSegments(asString(parsed.model, "codex")), - sessionId: redactHomePathUserSegments(threadId), + model: asString(parsed.model, "codex"), + sessionId: threadId, }]; } @@ -221,15 +217,15 @@ export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[ return [{ kind: "result", ts, - text: redactHomePathUserSegments(asString(parsed.result)), + text: asString(parsed.result), inputTokens, outputTokens, cachedTokens, costUsd: asNumber(parsed.total_cost_usd), - subtype: redactHomePathUserSegments(asString(parsed.subtype)), + subtype: asString(parsed.subtype), isError: parsed.is_error === true, errors: Array.isArray(parsed.errors) - ? parsed.errors.map(errorText).map(redactHomePathUserSegments).filter(Boolean) + ? parsed.errors.map(errorText).filter(Boolean) : [], }]; } @@ -243,21 +239,21 @@ export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[ return [{ kind: "result", ts, - text: redactHomePathUserSegments(asString(parsed.result)), + text: asString(parsed.result), inputTokens, outputTokens, cachedTokens, costUsd: asNumber(parsed.total_cost_usd), - subtype: redactHomePathUserSegments(asString(parsed.subtype, "turn.failed")), + subtype: asString(parsed.subtype, "turn.failed"), isError: true, - errors: message ? [redactHomePathUserSegments(message)] : [], + errors: message ? [message] : [], }]; } if (type === "error") { const message = errorText(parsed.message ?? parsed.error ?? parsed); - return [{ kind: "stderr", ts, text: redactHomePathUserSegments(message || line) }]; + return [{ kind: "stderr", ts, text: message || line }]; } - return [{ kind: "stdout", ts, text: redactHomePathUserSegments(line) }]; + return [{ kind: "stdout", ts, text: line }]; } diff --git a/packages/db/src/migrations/0039_curly_maria_hill.sql b/packages/db/src/migrations/0039_curly_maria_hill.sql new file mode 100644 index 00000000..178f6546 --- /dev/null +++ b/packages/db/src/migrations/0039_curly_maria_hill.sql @@ -0,0 +1 @@ +ALTER TABLE "instance_settings" ADD COLUMN "general" jsonb DEFAULT '{}'::jsonb NOT NULL; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0039_snapshot.json b/packages/db/src/migrations/meta/0039_snapshot.json new file mode 100644 index 00000000..af5b20b9 --- /dev/null +++ b/packages/db/src/migrations/meta/0039_snapshot.json @@ -0,0 +1,10308 @@ +{ + "id": "1006727d-476b-474c-932b-51f1ba9626fb", + "prevId": "cb7f5c2d-8be7-4bd7-8adc-6d942a4f2589", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_incidents": { + "name": "budget_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "threshold_type": { + "name": "threshold_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_limit": { + "name": "amount_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_observed": { + "name": "amount_observed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_incidents_company_status_idx": { + "name": "budget_incidents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_company_scope_idx": { + "name": "budget_incidents_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_policy_window_threshold_idx": { + "name": "budget_incidents_policy_window_threshold_idx", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "threshold_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"budget_incidents\".\"status\" <> 'dismissed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_incidents_company_id_companies_id_fk": { + "name": "budget_incidents_company_id_companies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_policy_id_budget_policies_id_fk": { + "name": "budget_incidents_policy_id_budget_policies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "budget_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_approval_id_approvals_id_fk": { + "name": "budget_incidents_approval_id_approvals_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_policies": { + "name": "budget_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'billed_cents'" + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warn_percent": { + "name": "warn_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 80 + }, + "hard_stop_enabled": { + "name": "hard_stop_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_enabled": { + "name": "notify_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_policies_company_scope_active_idx": { + "name": "budget_policies_company_scope_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_window_idx": { + "name": "budget_policies_company_window_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_scope_metric_unique_idx": { + "name": "budget_policies_company_scope_metric_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_policies_company_id_companies_id_fk": { + "name": "budget_policies_company_id_companies_id_fk", + "tableFrom": "budget_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "brand_color": { + "name": "brand_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_logos": { + "name": "company_logos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_revisions_document_revision_uq": { + "name": "document_revisions_document_revision_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_revisions_company_document_created_idx": { + "name": "document_revisions_company_document_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "latest_body": { + "name": "latest_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspaces": { + "name": "execution_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy_type": { + "name": "strategy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_fs'" + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "derived_from_execution_workspace_id": { + "name": "derived_from_execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_eligible_at": { + "name": "cleanup_eligible_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_reason": { + "name": "cleanup_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspaces_company_project_status_idx": { + "name": "execution_workspaces_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_project_workspace_status_idx": { + "name": "execution_workspaces_company_project_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_source_issue_idx": { + "name": "execution_workspaces_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_last_used_idx": { + "name": "execution_workspaces_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_branch_idx": { + "name": "execution_workspaces_company_branch_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspaces_company_id_companies_id_fk": { + "name": "execution_workspaces_company_id_companies_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "execution_workspaces_project_id_projects_id_fk": { + "name": "execution_workspaces_project_id_projects_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspaces_project_workspace_id_project_workspaces_id_fk": { + "name": "execution_workspaces_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_source_issue_id_issues_id_fk": { + "name": "execution_workspaces_source_issue_id_issues_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk": { + "name": "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "derived_from_execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finance_events": { + "name": "finance_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost_event_id": { + "name": "cost_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'debit'" + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_adapter_type": { + "name": "execution_adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing_tier": { + "name": "pricing_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_invoice_id": { + "name": "external_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "finance_events_company_occurred_idx": { + "name": "finance_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_biller_occurred_idx": { + "name": "finance_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_kind_occurred_idx": { + "name": "finance_events_company_kind_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_direction_occurred_idx": { + "name": "finance_events_company_direction_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_heartbeat_run_idx": { + "name": "finance_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_cost_event_idx": { + "name": "finance_events_company_cost_event_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "tableTo": "cost_events", + "columnsFrom": [ + "cost_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_pid": { + "name": "process_pid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_started_at": { + "name": "process_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retry_of_run_id": { + "name": "retry_of_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "process_loss_retry_count": { + "name": "process_loss_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "context_snapshot": { + "name": "context_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "retry_of_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "singleton_key": { + "name": "singleton_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "general": { + "name": "general", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "experimental": { + "name": "experimental", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_settings_singleton_key_idx": { + "name": "instance_settings_singleton_key_idx", + "columns": [ + { + "expression": "singleton_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_documents": { + "name": "issue_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_documents_company_issue_key_uq": { + "name": "issue_documents_company_issue_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_company_issue_updated_idx": { + "name": "issue_documents_company_issue_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_work_products": { + "name": "issue_work_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "runtime_service_id": { + "name": "runtime_service_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_state": { + "name": "review_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_work_products_company_issue_type_idx": { + "name": "issue_work_products_company_issue_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_execution_workspace_type_idx": { + "name": "issue_work_products_company_execution_workspace_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_provider_external_id_idx": { + "name": "issue_work_products_company_provider_external_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_updated_idx": { + "name": "issue_work_products_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_work_products_company_id_companies_id_fk": { + "name": "issue_work_products_company_id_companies_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_work_products_project_id_projects_id_fk": { + "name": "issue_work_products_project_id_projects_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_issue_id_issues_id_fk": { + "name": "issue_work_products_issue_id_issues_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_work_products_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issue_work_products_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk": { + "name": "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "workspace_runtime_services", + "columnsFrom": [ + "runtime_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_work_products_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_preference": { + "name": "execution_workspace_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_settings": { + "name": "execution_workspace_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_workspace_idx": { + "name": "issues_company_project_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_execution_workspace_idx": { + "name": "issues_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_workspace_id_project_workspaces_id_fk": { + "name": "issues_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issues_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_company_settings": { + "name": "plugin_company_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_config_plugin_id_idx": { + "name": "plugin_config_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_entities_plugin_idx": { + "name": "plugin_entities_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_scope_idx": { + "name": "plugin_entities_scope_idx", + "columns": [ + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_job_runs": { + "name": "plugin_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_job_runs_job_idx": { + "name": "plugin_job_runs_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_jobs": { + "name": "plugin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_key": { + "name": "job_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_jobs_plugin_idx": { + "name": "plugin_jobs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_unique_idx": { + "name": "plugin_jobs_unique_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_logs": { + "name": "plugin_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_logs_plugin_time_idx": { + "name": "plugin_logs_plugin_time_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_ref": { + "name": "default_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "setup_command": { + "name": "setup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_command": { + "name": "cleanup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_provider": { + "name": "remote_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_workspace_ref": { + "name": "remote_workspace_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_workspace_key": { + "name": "shared_workspace_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_source_type_idx": { + "name": "project_workspaces_project_source_type_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_company_shared_key_idx": { + "name": "project_workspaces_company_shared_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_workspace_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_remote_ref_idx": { + "name": "project_workspaces_project_remote_ref_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_workspace_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_policy": { + "name": "execution_workspace_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operations": { + "name": "workspace_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operations_company_run_started_idx": { + "name": "workspace_operations_company_run_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_started_idx": { + "name": "workspace_operations_company_workspace_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operations_company_id_companies_id_fk": { + "name": "workspace_operations_company_id_companies_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_operations_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_operations_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_runtime_services": { + "name": "workspace_runtime_services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_by_run_id": { + "name": "started_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stop_policy": { + "name": "stop_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_runtime_services_company_workspace_status_idx": { + "name": "workspace_runtime_services_company_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_execution_workspace_status_idx": { + "name": "workspace_runtime_services_company_execution_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_project_status_idx": { + "name": "workspace_runtime_services_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_run_idx": { + "name": "workspace_runtime_services_run_idx", + "columns": [ + { + "expression": "started_by_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_updated_idx": { + "name": "workspace_runtime_services_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk": { + "name": "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk": { + "name": "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "started_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index 7e282cb3..680b9cfd 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -274,6 +274,13 @@ "when": 1773931592563, "tag": "0038_careless_iron_monger", "breakpoints": true + }, + { + "idx": 39, + "version": "7", + "when": 1774011294562, + "tag": "0039_curly_maria_hill", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/instance_settings.ts b/packages/db/src/schema/instance_settings.ts index a9085ea4..002df259 100644 --- a/packages/db/src/schema/instance_settings.ts +++ b/packages/db/src/schema/instance_settings.ts @@ -5,6 +5,7 @@ export const instanceSettings = pgTable( { id: uuid("id").primaryKey().defaultRandom(), singletonKey: text("singleton_key").notNull().default("default"), + general: jsonb("general").$type>().notNull().default({}), experimental: jsonb("experimental").$type>().notNull().default({}), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 98850f9e..b0171955 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -121,6 +121,7 @@ export { export type { Company, InstanceExperimentalSettings, + InstanceGeneralSettings, InstanceSettings, Agent, AgentAccessState, @@ -248,6 +249,9 @@ export type { } from "./types/index.js"; export { + instanceGeneralSettingsSchema, + patchInstanceGeneralSettingsSchema, + type PatchInstanceGeneralSettings, instanceExperimentalSettingsSchema, patchInstanceExperimentalSettingsSchema, type PatchInstanceExperimentalSettings, diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index f573db4d..694cd542 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -1,5 +1,5 @@ export type { Company } from "./company.js"; -export type { InstanceExperimentalSettings, InstanceSettings } from "./instance.js"; +export type { InstanceExperimentalSettings, InstanceGeneralSettings, InstanceSettings } from "./instance.js"; export type { Agent, AgentAccessState, diff --git a/packages/shared/src/types/instance.ts b/packages/shared/src/types/instance.ts index f243ef61..3449f46d 100644 --- a/packages/shared/src/types/instance.ts +++ b/packages/shared/src/types/instance.ts @@ -1,9 +1,14 @@ +export interface InstanceGeneralSettings { + censorUsernameInLogs: boolean; +} + export interface InstanceExperimentalSettings { enableIsolatedWorkspaces: boolean; } export interface InstanceSettings { id: string; + general: InstanceGeneralSettings; experimental: InstanceExperimentalSettings; createdAt: Date; updatedAt: Date; diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index 6979e467..5669c8ae 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -1,4 +1,8 @@ export { + instanceGeneralSettingsSchema, + patchInstanceGeneralSettingsSchema, + type InstanceGeneralSettings, + type PatchInstanceGeneralSettings, instanceExperimentalSettingsSchema, patchInstanceExperimentalSettingsSchema, type InstanceExperimentalSettings, diff --git a/packages/shared/src/validators/instance.ts b/packages/shared/src/validators/instance.ts index 955db002..5511e655 100644 --- a/packages/shared/src/validators/instance.ts +++ b/packages/shared/src/validators/instance.ts @@ -1,10 +1,18 @@ import { z } from "zod"; +export const instanceGeneralSettingsSchema = z.object({ + censorUsernameInLogs: z.boolean().default(false), +}).strict(); + +export const patchInstanceGeneralSettingsSchema = instanceGeneralSettingsSchema.partial(); + export const instanceExperimentalSettingsSchema = z.object({ enableIsolatedWorkspaces: z.boolean().default(false), }).strict(); export const patchInstanceExperimentalSettingsSchema = instanceExperimentalSettingsSchema.partial(); +export type InstanceGeneralSettings = z.infer; +export type PatchInstanceGeneralSettings = z.infer; export type InstanceExperimentalSettings = z.infer; export type PatchInstanceExperimentalSettings = z.infer; diff --git a/server/src/__tests__/codex-local-adapter.test.ts b/server/src/__tests__/codex-local-adapter.test.ts index 18479e43..84c806cd 100644 --- a/server/src/__tests__/codex-local-adapter.test.ts +++ b/server/src/__tests__/codex-local-adapter.test.ts @@ -117,7 +117,7 @@ describe("codex_local ui stdout parser", () => { { kind: "system", ts, - text: "file changes: update /Users/[]/project/ui/src/pages/AgentDetail.tsx", + text: "file changes: update /Users/paperclipuser/project/ui/src/pages/AgentDetail.tsx", }, ]); }); diff --git a/server/src/__tests__/instance-settings-routes.test.ts b/server/src/__tests__/instance-settings-routes.test.ts index 3014e668..8b56fd67 100644 --- a/server/src/__tests__/instance-settings-routes.test.ts +++ b/server/src/__tests__/instance-settings-routes.test.ts @@ -5,7 +5,9 @@ import { errorHandler } from "../middleware/index.js"; import { instanceSettingsRoutes } from "../routes/instance-settings.js"; const mockInstanceSettingsService = vi.hoisted(() => ({ + getGeneral: vi.fn(), getExperimental: vi.fn(), + updateGeneral: vi.fn(), updateExperimental: vi.fn(), listCompanyIds: vi.fn(), })); @@ -31,9 +33,18 @@ function createApp(actor: any) { describe("instance settings routes", () => { beforeEach(() => { vi.clearAllMocks(); + mockInstanceSettingsService.getGeneral.mockResolvedValue({ + censorUsernameInLogs: false, + }); mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: false, }); + mockInstanceSettingsService.updateGeneral.mockResolvedValue({ + id: "instance-settings-1", + general: { + censorUsernameInLogs: true, + }, + }); mockInstanceSettingsService.updateExperimental.mockResolvedValue({ id: "instance-settings-1", experimental: { @@ -66,6 +77,29 @@ describe("instance settings routes", () => { expect(mockLogActivity).toHaveBeenCalledTimes(2); }); + it("allows local board users to read and update general settings", async () => { + const app = createApp({ + type: "board", + userId: "local-board", + source: "local_implicit", + isInstanceAdmin: true, + }); + + const getRes = await request(app).get("/api/instance/settings/general"); + expect(getRes.status).toBe(200); + expect(getRes.body).toEqual({ censorUsernameInLogs: false }); + + const patchRes = await request(app) + .patch("/api/instance/settings/general") + .send({ censorUsernameInLogs: true }); + + expect(patchRes.status).toBe(200); + expect(mockInstanceSettingsService.updateGeneral).toHaveBeenCalledWith({ + censorUsernameInLogs: true, + }); + expect(mockLogActivity).toHaveBeenCalledTimes(2); + }); + it("rejects non-admin board users", async () => { const app = createApp({ type: "board", @@ -75,10 +109,10 @@ describe("instance settings routes", () => { companyIds: ["company-1"], }); - const res = await request(app).get("/api/instance/settings/experimental"); + const res = await request(app).get("/api/instance/settings/general"); expect(res.status).toBe(403); - expect(mockInstanceSettingsService.getExperimental).not.toHaveBeenCalled(); + expect(mockInstanceSettingsService.getGeneral).not.toHaveBeenCalled(); }); it("rejects agent callers", async () => { @@ -90,10 +124,10 @@ describe("instance settings routes", () => { }); const res = await request(app) - .patch("/api/instance/settings/experimental") - .send({ enableIsolatedWorkspaces: true }); + .patch("/api/instance/settings/general") + .send({ censorUsernameInLogs: true }); expect(res.status).toBe(403); - expect(mockInstanceSettingsService.updateExperimental).not.toHaveBeenCalled(); + expect(mockInstanceSettingsService.updateGeneral).not.toHaveBeenCalled(); }); }); diff --git a/server/src/__tests__/log-redaction.test.ts b/server/src/__tests__/log-redaction.test.ts index a1da7a2e..35915bfc 100644 --- a/server/src/__tests__/log-redaction.test.ts +++ b/server/src/__tests__/log-redaction.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { - CURRENT_USER_REDACTION_TOKEN, + maskUserNameForLogs, redactCurrentUserText, redactCurrentUserValue, } from "../log-redaction.js"; @@ -8,6 +8,7 @@ import { describe("log redaction", () => { it("redacts the active username inside home-directory paths", () => { const userName = "paperclipuser"; + const maskedUserName = maskUserNameForLogs(userName); const input = [ `cwd=/Users/${userName}/paperclip`, `home=/home/${userName}/workspace`, @@ -19,14 +20,15 @@ describe("log redaction", () => { homeDirs: [`/Users/${userName}`, `/home/${userName}`, `C:\\Users\\${userName}`], }); - expect(result).toContain(`cwd=/Users/${CURRENT_USER_REDACTION_TOKEN}/paperclip`); - expect(result).toContain(`home=/home/${CURRENT_USER_REDACTION_TOKEN}/workspace`); - expect(result).toContain(`win=C:\\Users\\${CURRENT_USER_REDACTION_TOKEN}\\paperclip`); + expect(result).toContain(`cwd=/Users/${maskedUserName}/paperclip`); + expect(result).toContain(`home=/home/${maskedUserName}/workspace`); + expect(result).toContain(`win=C:\\Users\\${maskedUserName}\\paperclip`); expect(result).not.toContain(userName); }); it("redacts standalone username mentions without mangling larger tokens", () => { const userName = "paperclipuser"; + const maskedUserName = maskUserNameForLogs(userName); const result = redactCurrentUserText( `user ${userName} said ${userName}/project should stay but apaperclipuserz should not change`, { @@ -36,12 +38,13 @@ describe("log redaction", () => { ); expect(result).toBe( - `user ${CURRENT_USER_REDACTION_TOKEN} said ${CURRENT_USER_REDACTION_TOKEN}/project should stay but apaperclipuserz should not change`, + `user ${maskedUserName} said ${maskedUserName}/project should stay but apaperclipuserz should not change`, ); }); it("recursively redacts nested event payloads", () => { const userName = "paperclipuser"; + const maskedUserName = maskUserNameForLogs(userName); const result = redactCurrentUserValue({ cwd: `/Users/${userName}/paperclip`, prompt: `open /Users/${userName}/paperclip/ui`, @@ -55,12 +58,17 @@ describe("log redaction", () => { }); expect(result).toEqual({ - cwd: `/Users/${CURRENT_USER_REDACTION_TOKEN}/paperclip`, - prompt: `open /Users/${CURRENT_USER_REDACTION_TOKEN}/paperclip/ui`, + cwd: `/Users/${maskedUserName}/paperclip`, + prompt: `open /Users/${maskedUserName}/paperclip/ui`, nested: { - author: CURRENT_USER_REDACTION_TOKEN, + author: maskedUserName, }, - values: [CURRENT_USER_REDACTION_TOKEN, `/home/${CURRENT_USER_REDACTION_TOKEN}/project`], + values: [maskedUserName, `/home/${maskedUserName}/project`], }); }); + + it("skips redaction when disabled", () => { + const input = "cwd=/Users/paperclipuser/paperclip"; + expect(redactCurrentUserText(input, { enabled: false })).toBe(input); + }); }); diff --git a/server/src/log-redaction.ts b/server/src/log-redaction.ts index 07a28c58..ab59b3e4 100644 --- a/server/src/log-redaction.ts +++ b/server/src/log-redaction.ts @@ -1,8 +1,9 @@ import os from "node:os"; -export const CURRENT_USER_REDACTION_TOKEN = "[]"; +export const CURRENT_USER_REDACTION_TOKEN = "*"; -interface CurrentUserRedactionOptions { +export interface CurrentUserRedactionOptions { + enabled?: boolean; replacement?: string; userNames?: string[]; homeDirs?: string[]; @@ -39,6 +40,12 @@ function replaceLastPathSegment(pathValue: string, replacement: string) { return `${normalized.slice(0, lastSeparator + 1)}${replacement}`; } +export function maskUserNameForLogs(value: string, fallback = CURRENT_USER_REDACTION_TOKEN) { + const trimmed = value.trim(); + if (!trimmed) return fallback; + return `${trimmed[0]}${"*".repeat(Math.max(1, Array.from(trimmed).length - 1))}`; +} + function defaultUserNames() { const candidates = [ process.env.USER, @@ -99,21 +106,22 @@ function resolveCurrentUserCandidates(opts?: CurrentUserRedactionOptions) { export function redactCurrentUserText(input: string, opts?: CurrentUserRedactionOptions) { if (!input) return input; + if (opts?.enabled === false) return input; const { userNames, homeDirs, replacement } = resolveCurrentUserCandidates(opts); let result = input; for (const homeDir of [...homeDirs].sort((a, b) => b.length - a.length)) { const lastSegment = splitPathSegments(homeDir).pop() ?? ""; - const replacementDir = userNames.includes(lastSegment) - ? replaceLastPathSegment(homeDir, replacement) + const replacementDir = lastSegment + ? replaceLastPathSegment(homeDir, maskUserNameForLogs(lastSegment, replacement)) : replacement; result = result.split(homeDir).join(replacementDir); } for (const userName of [...userNames].sort((a, b) => b.length - a.length)) { const pattern = new RegExp(`(? | null | undefined }) { if (!agent.permissions || typeof agent.permissions !== "object") return false; return Boolean((agent.permissions as Record).canCreateAgents); @@ -1597,7 +1605,7 @@ export function agentRoutes(db: Db) { return; } assertCompanyAccess(req, run.companyId); - res.json(redactCurrentUserValue(run)); + res.json(redactCurrentUserValue(run, await getCurrentUserRedactionOptions())); }); router.post("/heartbeat-runs/:runId/cancel", async (req, res) => { @@ -1632,11 +1640,12 @@ export function agentRoutes(db: Db) { const afterSeq = Number(req.query.afterSeq ?? 0); const limit = Number(req.query.limit ?? 200); const events = await heartbeat.listEvents(runId, Number.isFinite(afterSeq) ? afterSeq : 0, Number.isFinite(limit) ? limit : 200); + const currentUserRedactionOptions = await getCurrentUserRedactionOptions(); const redactedEvents = events.map((event) => redactCurrentUserValue({ ...event, payload: redactEventPayload(event.payload), - }), + }, currentUserRedactionOptions), ); res.json(redactedEvents); }); @@ -1672,7 +1681,7 @@ export function agentRoutes(db: Db) { const context = asRecord(run.contextSnapshot); const executionWorkspaceId = asNonEmptyString(context?.executionWorkspaceId); const operations = await workspaceOperations.listForRun(runId, executionWorkspaceId); - res.json(redactCurrentUserValue(operations)); + res.json(redactCurrentUserValue(operations, await getCurrentUserRedactionOptions())); }); router.get("/workspace-operations/:operationId/log", async (req, res) => { @@ -1768,7 +1777,7 @@ export function agentRoutes(db: Db) { } res.json({ - ...redactCurrentUserValue(run), + ...redactCurrentUserValue(run, await getCurrentUserRedactionOptions()), agentId: agent.id, agentName: agent.name, adapterType: agent.adapterType, diff --git a/server/src/routes/instance-settings.ts b/server/src/routes/instance-settings.ts index 93ae9f6a..1c9493ca 100644 --- a/server/src/routes/instance-settings.ts +++ b/server/src/routes/instance-settings.ts @@ -1,6 +1,6 @@ import { Router, type Request } from "express"; import type { Db } from "@paperclipai/db"; -import { patchInstanceExperimentalSettingsSchema } from "@paperclipai/shared"; +import { patchInstanceExperimentalSettingsSchema, patchInstanceGeneralSettingsSchema } from "@paperclipai/shared"; import { forbidden } from "../errors.js"; import { validate } from "../middleware/validate.js"; import { instanceSettingsService, logActivity } from "../services/index.js"; @@ -20,6 +20,41 @@ export function instanceSettingsRoutes(db: Db) { const router = Router(); const svc = instanceSettingsService(db); + router.get("/instance/settings/general", async (req, res) => { + assertCanManageInstanceSettings(req); + res.json(await svc.getGeneral()); + }); + + router.patch( + "/instance/settings/general", + validate(patchInstanceGeneralSettingsSchema), + async (req, res) => { + assertCanManageInstanceSettings(req); + const updated = await svc.updateGeneral(req.body); + const actor = getActorInfo(req); + const companyIds = await svc.listCompanyIds(); + await Promise.all( + companyIds.map((companyId) => + logActivity(db, { + companyId, + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, + action: "instance.settings.general_updated", + entityType: "instance_settings", + entityId: updated.id, + details: { + general: updated.general, + changedKeys: Object.keys(req.body).sort(), + }, + }), + ), + ); + res.json(updated.general); + }, + ); + router.get("/instance/settings/experimental", async (req, res) => { assertCanManageInstanceSettings(req); res.json(await svc.getExperimental()); diff --git a/server/src/services/activity-log.ts b/server/src/services/activity-log.ts index 16758b94..cc608a74 100644 --- a/server/src/services/activity-log.ts +++ b/server/src/services/activity-log.ts @@ -8,6 +8,7 @@ import { redactCurrentUserValue } from "../log-redaction.js"; import { sanitizeRecord } from "../redaction.js"; import { logger } from "../middleware/logger.js"; import type { PluginEventBus } from "./plugin-event-bus.js"; +import { instanceSettingsService } from "./instance-settings.js"; const PLUGIN_EVENT_SET: ReadonlySet = new Set(PLUGIN_EVENT_TYPES); @@ -34,8 +35,13 @@ export interface LogActivityInput { } export async function logActivity(db: Db, input: LogActivityInput) { + const currentUserRedactionOptions = { + enabled: (await instanceSettingsService(db).getGeneral()).censorUsernameInLogs, + }; const sanitizedDetails = input.details ? sanitizeRecord(input.details) : null; - const redactedDetails = sanitizedDetails ? redactCurrentUserValue(sanitizedDetails) : null; + const redactedDetails = sanitizedDetails + ? redactCurrentUserValue(sanitizedDetails, currentUserRedactionOptions) + : null; await db.insert(activityLog).values({ companyId: input.companyId, actorType: input.actorType, diff --git a/server/src/services/approvals.ts b/server/src/services/approvals.ts index f2bdb227..bf101e23 100644 --- a/server/src/services/approvals.ts +++ b/server/src/services/approvals.ts @@ -6,22 +6,24 @@ import { redactCurrentUserText } from "../log-redaction.js"; import { agentService } from "./agents.js"; import { budgetService } from "./budgets.js"; import { notifyHireApproved } from "./hire-hook.js"; - -function redactApprovalComment(comment: T): T { - return { - ...comment, - body: redactCurrentUserText(comment.body), - }; -} +import { instanceSettingsService } from "./instance-settings.js"; export function approvalService(db: Db) { const agentsSvc = agentService(db); const budgets = budgetService(db); + const instanceSettings = instanceSettingsService(db); const canResolveStatuses = new Set(["pending", "revision_requested"]); const resolvableStatuses = Array.from(canResolveStatuses); type ApprovalRecord = typeof approvals.$inferSelect; type ResolutionResult = { approval: ApprovalRecord; applied: boolean }; + function redactApprovalComment(comment: T, censorUsernameInLogs: boolean): T { + return { + ...comment, + body: redactCurrentUserText(comment.body, { enabled: censorUsernameInLogs }), + }; + } + async function getExistingApproval(id: string) { const existing = await db .select() @@ -230,6 +232,7 @@ export function approvalService(db: Db) { listComments: async (approvalId: string) => { const existing = await getExistingApproval(approvalId); + const { censorUsernameInLogs } = await instanceSettings.getGeneral(); return db .select() .from(approvalComments) @@ -240,7 +243,7 @@ export function approvalService(db: Db) { ), ) .orderBy(asc(approvalComments.createdAt)) - .then((comments) => comments.map(redactApprovalComment)); + .then((comments) => comments.map((comment) => redactApprovalComment(comment, censorUsernameInLogs))); }, addComment: async ( @@ -249,7 +252,10 @@ export function approvalService(db: Db) { actor: { agentId?: string; userId?: string }, ) => { const existing = await getExistingApproval(approvalId); - const redactedBody = redactCurrentUserText(body); + const currentUserRedactionOptions = { + enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs, + }; + const redactedBody = redactCurrentUserText(body, currentUserRedactionOptions); return db .insert(approvalComments) .values({ @@ -260,7 +266,7 @@ export function approvalService(db: Db) { body: redactedBody, }) .returning() - .then((rows) => redactApprovalComment(rows[0])); + .then((rows) => redactApprovalComment(rows[0], currentUserRedactionOptions.enabled)); }, }; } diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index c162e187..52de2134 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -720,6 +720,9 @@ function resolveNextSessionState(input: { export function heartbeatService(db: Db) { const instanceSettings = instanceSettingsService(db); + const getCurrentUserRedactionOptions = async () => ({ + enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs, + }); const runLogStore = getRunLogStore(); const secretsSvc = secretService(db); @@ -1318,8 +1321,13 @@ export function heartbeatService(db: Db) { payload?: Record; }, ) { - const sanitizedMessage = event.message ? redactCurrentUserText(event.message) : event.message; - const sanitizedPayload = event.payload ? redactCurrentUserValue(event.payload) : event.payload; + const currentUserRedactionOptions = await getCurrentUserRedactionOptions(); + const sanitizedMessage = event.message + ? redactCurrentUserText(event.message, currentUserRedactionOptions) + : event.message; + const sanitizedPayload = event.payload + ? redactCurrentUserValue(event.payload, currentUserRedactionOptions) + : event.payload; await db.insert(heartbeatRunEvents).values({ companyId: run.companyId, @@ -2252,8 +2260,9 @@ export function heartbeatService(db: Db) { }) .where(eq(heartbeatRuns.id, runId)); + const currentUserRedactionOptions = await getCurrentUserRedactionOptions(); const onLog = async (stream: "stdout" | "stderr", chunk: string) => { - const sanitizedChunk = redactCurrentUserText(chunk); + const sanitizedChunk = redactCurrentUserText(chunk, currentUserRedactionOptions); if (stream === "stdout") stdoutExcerpt = appendExcerpt(stdoutExcerpt, sanitizedChunk); if (stream === "stderr") stderrExcerpt = appendExcerpt(stderrExcerpt, sanitizedChunk); const ts = new Date().toISOString(); @@ -2503,6 +2512,7 @@ export function heartbeatService(db: Db) { ? null : redactCurrentUserText( adapterResult.errorMessage ?? (outcome === "timed_out" ? "Timed out" : "Adapter failed"), + currentUserRedactionOptions, ), errorCode: outcome === "timed_out" @@ -2570,7 +2580,10 @@ export function heartbeatService(db: Db) { } await finalizeAgentStatus(agent.id, outcome); } catch (err) { - const message = redactCurrentUserText(err instanceof Error ? err.message : "Unknown adapter failure"); + const message = redactCurrentUserText( + err instanceof Error ? err.message : "Unknown adapter failure", + await getCurrentUserRedactionOptions(), + ); logger.error({ err, runId }, "heartbeat execution failed"); let logSummary: { bytes: number; sha256?: string; compressed: boolean } | null = null; @@ -3608,7 +3621,7 @@ export function heartbeatService(db: Db) { store: run.logStore, logRef: run.logRef, ...result, - content: redactCurrentUserText(result.content), + content: redactCurrentUserText(result.content, await getCurrentUserRedactionOptions()), }; }, diff --git a/server/src/services/instance-settings.ts b/server/src/services/instance-settings.ts index e60154a4..bbc4df3d 100644 --- a/server/src/services/instance-settings.ts +++ b/server/src/services/instance-settings.ts @@ -1,8 +1,11 @@ import type { Db } from "@paperclipai/db"; import { companies, instanceSettings } from "@paperclipai/db"; import { + instanceGeneralSettingsSchema, + type InstanceGeneralSettings, instanceExperimentalSettingsSchema, type InstanceExperimentalSettings, + type PatchInstanceGeneralSettings, type InstanceSettings, type PatchInstanceExperimentalSettings, } from "@paperclipai/shared"; @@ -10,6 +13,18 @@ import { eq } from "drizzle-orm"; const DEFAULT_SINGLETON_KEY = "default"; +function normalizeGeneralSettings(raw: unknown): InstanceGeneralSettings { + const parsed = instanceGeneralSettingsSchema.safeParse(raw ?? {}); + if (parsed.success) { + return { + censorUsernameInLogs: parsed.data.censorUsernameInLogs ?? false, + }; + } + return { + censorUsernameInLogs: false, + }; +} + function normalizeExperimentalSettings(raw: unknown): InstanceExperimentalSettings { const parsed = instanceExperimentalSettingsSchema.safeParse(raw ?? {}); if (parsed.success) { @@ -25,6 +40,7 @@ function normalizeExperimentalSettings(raw: unknown): InstanceExperimentalSettin function toInstanceSettings(row: typeof instanceSettings.$inferSelect): InstanceSettings { return { id: row.id, + general: normalizeGeneralSettings(row.general), experimental: normalizeExperimentalSettings(row.experimental), createdAt: row.createdAt, updatedAt: row.updatedAt, @@ -45,6 +61,7 @@ export function instanceSettingsService(db: Db) { .insert(instanceSettings) .values({ singletonKey: DEFAULT_SINGLETON_KEY, + general: {}, experimental: {}, createdAt: now, updatedAt: now, @@ -63,11 +80,34 @@ export function instanceSettingsService(db: Db) { return { get: async (): Promise => toInstanceSettings(await getOrCreateRow()), + getGeneral: async (): Promise => { + const row = await getOrCreateRow(); + return normalizeGeneralSettings(row.general); + }, + getExperimental: async (): Promise => { const row = await getOrCreateRow(); return normalizeExperimentalSettings(row.experimental); }, + updateGeneral: async (patch: PatchInstanceGeneralSettings): Promise => { + const current = await getOrCreateRow(); + const nextGeneral = normalizeGeneralSettings({ + ...normalizeGeneralSettings(current.general), + ...patch, + }); + const now = new Date(); + const [updated] = await db + .update(instanceSettings) + .set({ + general: { ...nextGeneral }, + updatedAt: now, + }) + .where(eq(instanceSettings.id, current.id)) + .returning(); + return toInstanceSettings(updated ?? current); + }, + updateExperimental: async (patch: PatchInstanceExperimentalSettings): Promise => { const current = await getOrCreateRow(); const nextExperimental = normalizeExperimentalSettings({ diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 1f1a4961..362d18a5 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -97,13 +97,6 @@ type IssueUserContextInput = { updatedAt: Date | string; }; -function redactIssueComment(comment: T): T { - return { - ...comment, - body: redactCurrentUserText(comment.body), - }; -} - function sameRunLock(checkoutRunId: string | null, actorRunId: string | null) { if (actorRunId) return checkoutRunId === actorRunId; return checkoutRunId == null; @@ -320,6 +313,13 @@ function withActiveRuns( export function issueService(db: Db) { const instanceSettings = instanceSettingsService(db); + function redactIssueComment(comment: T, censorUsernameInLogs: boolean): T { + return { + ...comment, + body: redactCurrentUserText(comment.body, { enabled: censorUsernameInLogs }), + }; + } + async function assertAssignableAgent(companyId: string, agentId: string) { const assignee = await db .select({ @@ -1215,7 +1215,8 @@ export function issueService(db: Db) { ); const comments = limit ? await query.limit(limit) : await query; - return comments.map(redactIssueComment); + const { censorUsernameInLogs } = await instanceSettings.getGeneral(); + return comments.map((comment) => redactIssueComment(comment, censorUsernameInLogs)); }, getCommentCursor: async (issueId: string) => { @@ -1247,14 +1248,15 @@ export function issueService(db: Db) { }, getComment: (commentId: string) => - db + instanceSettings.getGeneral().then(({ censorUsernameInLogs }) => + db .select() .from(issueComments) .where(eq(issueComments.id, commentId)) .then((rows) => { const comment = rows[0] ?? null; - return comment ? redactIssueComment(comment) : null; - }), + return comment ? redactIssueComment(comment, censorUsernameInLogs) : null; + })), addComment: async (issueId: string, body: string, actor: { agentId?: string; userId?: string }) => { const issue = await db @@ -1265,7 +1267,10 @@ export function issueService(db: Db) { if (!issue) throw notFound("Issue not found"); - const redactedBody = redactCurrentUserText(body); + const currentUserRedactionOptions = { + enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs, + }; + const redactedBody = redactCurrentUserText(body, currentUserRedactionOptions); const [comment] = await db .insert(issueComments) .values({ @@ -1283,7 +1288,7 @@ export function issueService(db: Db) { .set({ updatedAt: new Date() }) .where(eq(issues.id, issueId)); - return redactIssueComment(comment); + return redactIssueComment(comment, currentUserRedactionOptions.enabled); }, createAttachment: async (input: { diff --git a/server/src/services/workspace-operations.ts b/server/src/services/workspace-operations.ts index 3bbace29..b20a9ed7 100644 --- a/server/src/services/workspace-operations.ts +++ b/server/src/services/workspace-operations.ts @@ -5,6 +5,7 @@ import type { WorkspaceOperation, WorkspaceOperationPhase, WorkspaceOperationSta import { asc, desc, eq, inArray, isNull, or, and } from "drizzle-orm"; import { notFound } from "../errors.js"; import { redactCurrentUserText, redactCurrentUserValue } from "../log-redaction.js"; +import { instanceSettingsService } from "./instance-settings.js"; import { getWorkspaceOperationLogStore } from "./workspace-operation-log-store.js"; type WorkspaceOperationRow = typeof workspaceOperations.$inferSelect; @@ -69,6 +70,7 @@ export interface WorkspaceOperationRecorder { } export function workspaceOperationService(db: Db) { + const instanceSettings = instanceSettingsService(db); const logStore = getWorkspaceOperationLogStore(); async function getById(id: string) { @@ -105,6 +107,9 @@ export function workspaceOperationService(db: Db) { }, async recordOperation(recordInput) { + const currentUserRedactionOptions = { + enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs, + }; const startedAt = new Date(); const id = randomUUID(); const handle = await logStore.begin({ @@ -116,7 +121,7 @@ export function workspaceOperationService(db: Db) { let stderrExcerpt = ""; const append = async (stream: "stdout" | "stderr" | "system", chunk: string | null | undefined) => { if (!chunk) return; - const sanitizedChunk = redactCurrentUserText(chunk); + const sanitizedChunk = redactCurrentUserText(chunk, currentUserRedactionOptions); if (stream === "stdout") stdoutExcerpt = appendExcerpt(stdoutExcerpt, sanitizedChunk); if (stream === "stderr") stderrExcerpt = appendExcerpt(stderrExcerpt, sanitizedChunk); await logStore.append(handle, { @@ -137,7 +142,10 @@ export function workspaceOperationService(db: Db) { status: "running", logStore: handle.store, logRef: handle.logRef, - metadata: redactCurrentUserValue(recordInput.metadata ?? null) as Record | null, + metadata: redactCurrentUserValue( + recordInput.metadata ?? null, + currentUserRedactionOptions, + ) as Record | null, startedAt, }); createdIds.push(id); @@ -162,6 +170,7 @@ export function workspaceOperationService(db: Db) { logCompressed: finalized.compressed, metadata: redactCurrentUserValue( combineMetadata(recordInput.metadata, result.metadata), + currentUserRedactionOptions, ) as Record | null, finishedAt, updatedAt: finishedAt, @@ -241,7 +250,9 @@ export function workspaceOperationService(db: Db) { store: operation.logStore, logRef: operation.logRef, ...result, - content: redactCurrentUserText(result.content), + content: redactCurrentUserText(result.content, { + enabled: (await instanceSettings.getGeneral()).censorUsernameInLogs, + }), }; }, }; diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 05aa5381..dc1acdf0 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -23,6 +23,7 @@ import { Activity } from "./pages/Activity"; import { Inbox } from "./pages/Inbox"; import { CompanySettings } from "./pages/CompanySettings"; import { DesignGuide } from "./pages/DesignGuide"; +import { InstanceGeneralSettings } from "./pages/InstanceGeneralSettings"; import { InstanceSettings } from "./pages/InstanceSettings"; import { InstanceExperimentalSettings } from "./pages/InstanceExperimentalSettings"; import { PluginManager } from "./pages/PluginManager"; @@ -171,7 +172,7 @@ function InboxRootRedirect() { function LegacySettingsRedirect() { const location = useLocation(); - return ; + return ; } function OnboardingRoutePage() { @@ -296,9 +297,10 @@ export function App() { }> } /> } /> - } /> + } /> }> - } /> + } /> + } /> } /> } /> } /> diff --git a/ui/src/adapters/transcript.ts b/ui/src/adapters/transcript.ts index 545c94f4..238764ae 100644 --- a/ui/src/adapters/transcript.ts +++ b/ui/src/adapters/transcript.ts @@ -2,6 +2,7 @@ import { redactHomePathUserSegments, redactTranscriptEntryPaths } from "@papercl import type { TranscriptEntry, StdoutLineParser } from "./types"; export type RunLogChunk = { ts: string; stream: "stdout" | "stderr" | "system"; chunk: string }; +type TranscriptBuildOptions = { censorUsernameInLogs?: boolean }; export function appendTranscriptEntry(entries: TranscriptEntry[], entry: TranscriptEntry) { if ((entry.kind === "thinking" || entry.kind === "assistant") && entry.delta) { @@ -21,17 +22,22 @@ export function appendTranscriptEntries(entries: TranscriptEntry[], incoming: Tr } } -export function buildTranscript(chunks: RunLogChunk[], parser: StdoutLineParser): TranscriptEntry[] { +export function buildTranscript( + chunks: RunLogChunk[], + parser: StdoutLineParser, + opts?: TranscriptBuildOptions, +): TranscriptEntry[] { const entries: TranscriptEntry[] = []; let stdoutBuffer = ""; + const redactionOptions = { enabled: opts?.censorUsernameInLogs ?? true }; for (const chunk of chunks) { if (chunk.stream === "stderr") { - entries.push({ kind: "stderr", ts: chunk.ts, text: redactHomePathUserSegments(chunk.chunk) }); + entries.push({ kind: "stderr", ts: chunk.ts, text: redactHomePathUserSegments(chunk.chunk, redactionOptions) }); continue; } if (chunk.stream === "system") { - entries.push({ kind: "system", ts: chunk.ts, text: redactHomePathUserSegments(chunk.chunk) }); + entries.push({ kind: "system", ts: chunk.ts, text: redactHomePathUserSegments(chunk.chunk, redactionOptions) }); continue; } @@ -41,14 +47,14 @@ export function buildTranscript(chunks: RunLogChunk[], parser: StdoutLineParser) for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; - appendTranscriptEntries(entries, parser(trimmed, chunk.ts).map(redactTranscriptEntryPaths)); + appendTranscriptEntries(entries, parser(trimmed, chunk.ts).map((entry) => redactTranscriptEntryPaths(entry, redactionOptions))); } } const trailing = stdoutBuffer.trim(); if (trailing) { const ts = chunks.length > 0 ? chunks[chunks.length - 1]!.ts : new Date().toISOString(); - appendTranscriptEntries(entries, parser(trailing, ts).map(redactTranscriptEntryPaths)); + appendTranscriptEntries(entries, parser(trailing, ts).map((entry) => redactTranscriptEntryPaths(entry, redactionOptions))); } return entries; diff --git a/ui/src/api/instanceSettings.ts b/ui/src/api/instanceSettings.ts index 0b7a8f24..ef50ce47 100644 --- a/ui/src/api/instanceSettings.ts +++ b/ui/src/api/instanceSettings.ts @@ -1,10 +1,16 @@ import type { InstanceExperimentalSettings, + InstanceGeneralSettings, + PatchInstanceGeneralSettings, PatchInstanceExperimentalSettings, } from "@paperclipai/shared"; import { api } from "./client"; export const instanceSettingsApi = { + getGeneral: () => + api.get("/instance/settings/general"), + updateGeneral: (patch: PatchInstanceGeneralSettings) => + api.patch("/instance/settings/general", patch), getExperimental: () => api.get("/instance/settings/experimental"), updateExperimental: (patch: PatchInstanceExperimentalSettings) => diff --git a/ui/src/components/InstanceSidebar.tsx b/ui/src/components/InstanceSidebar.tsx index 451678d1..dbd8381b 100644 --- a/ui/src/components/InstanceSidebar.tsx +++ b/ui/src/components/InstanceSidebar.tsx @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { Clock3, FlaskConical, Puzzle, Settings } from "lucide-react"; +import { Clock3, FlaskConical, Puzzle, Settings, SlidersHorizontal } from "lucide-react"; import { NavLink } from "@/lib/router"; import { pluginsApi } from "@/api/plugins"; import { queryKeys } from "@/lib/queryKeys"; @@ -22,6 +22,7 @@ export function InstanceSidebar() {
@@ -375,7 +390,13 @@ function WorkspaceOperationLogViewer({ operation }: { operation: WorkspaceOperat ); } -function WorkspaceOperationsSection({ operations }: { operations: WorkspaceOperation[] }) { +function WorkspaceOperationsSection({ + operations, + censorUsernameInLogs, +}: { + operations: WorkspaceOperation[]; + censorUsernameInLogs: boolean; +}) { if (operations.length === 0) return null; return ( @@ -440,7 +461,7 @@ function WorkspaceOperationsSection({ operations }: { operations: WorkspaceOpera
stderr excerpt
-                    {redactHomePathUserSegments(operation.stderrExcerpt)}
+                    {redactPathText(operation.stderrExcerpt, censorUsernameInLogs)}
                   
)} @@ -448,11 +469,16 @@ function WorkspaceOperationsSection({ operations }: { operations: WorkspaceOpera
stdout excerpt
-                    {redactHomePathUserSegments(operation.stdoutExcerpt)}
+                    {redactPathText(operation.stdoutExcerpt, censorUsernameInLogs)}
                   
)} - {operation.logRef && } + {operation.logRef && ( + + )}
); })} @@ -2472,13 +2498,21 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin }; }, [isLive, run.companyId, run.id, run.agentId]); + const censorUsernameInLogs = useQuery({ + queryKey: queryKeys.instance.generalSettings, + queryFn: () => instanceSettingsApi.getGeneral(), + }).data?.censorUsernameInLogs === true; + const adapterInvokePayload = useMemo(() => { const evt = events.find((e) => e.eventType === "adapter.invoke"); - return redactHomePathUserSegmentsInValue(asRecord(evt?.payload ?? null)); - }, [events]); + return redactPathValue(asRecord(evt?.payload ?? null), censorUsernameInLogs); + }, [censorUsernameInLogs, events]); const adapter = useMemo(() => getUIAdapter(adapterType), [adapterType]); - const transcript = useMemo(() => buildTranscript(logLines, adapter.parseStdoutLine), [logLines, adapter]); + const transcript = useMemo( + () => buildTranscript(logLines, adapter.parseStdoutLine, { censorUsernameInLogs }), + [adapter, censorUsernameInLogs, logLines], + ); useEffect(() => { setTranscriptMode("nice"); @@ -2506,7 +2540,10 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin return (
- + {adapterInvokePayload && (
Invocation
@@ -2548,8 +2585,8 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
Prompt
                 {typeof adapterInvokePayload.prompt === "string"
-                  ? redactHomePathUserSegments(adapterInvokePayload.prompt)
-                  : JSON.stringify(redactHomePathUserSegmentsInValue(adapterInvokePayload.prompt), null, 2)}
+                  ? redactPathText(adapterInvokePayload.prompt, censorUsernameInLogs)
+                  : JSON.stringify(redactPathValue(adapterInvokePayload.prompt, censorUsernameInLogs), null, 2)}
               
)} @@ -2557,7 +2594,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
Context
-                {JSON.stringify(redactHomePathUserSegmentsInValue(adapterInvokePayload.context), null, 2)}
+                {JSON.stringify(redactPathValue(adapterInvokePayload.context, censorUsernameInLogs), null, 2)}
               
)} @@ -2565,7 +2602,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
Environment
-                {formatEnvForDisplay(adapterInvokePayload.env)}
+                {formatEnvForDisplay(adapterInvokePayload.env, censorUsernameInLogs)}
               
)} @@ -2641,14 +2678,14 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin {run.error && (
Error: - {redactHomePathUserSegments(run.error)} + {redactPathText(run.error, censorUsernameInLogs)}
)} {run.stderrExcerpt && run.stderrExcerpt.trim() && (
stderr excerpt
-                {redactHomePathUserSegments(run.stderrExcerpt)}
+                {redactPathText(run.stderrExcerpt, censorUsernameInLogs)}
               
)} @@ -2656,7 +2693,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
adapter result JSON
-                {JSON.stringify(redactHomePathUserSegmentsInValue(run.resultJson), null, 2)}
+                {JSON.stringify(redactPathValue(run.resultJson, censorUsernameInLogs), null, 2)}
               
)} @@ -2664,7 +2701,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
stdout excerpt
-                {redactHomePathUserSegments(run.stdoutExcerpt)}
+                {redactPathText(run.stdoutExcerpt, censorUsernameInLogs)}
               
)} @@ -2691,9 +2728,9 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin {evt.message - ? redactHomePathUserSegments(evt.message) + ? redactPathText(evt.message, censorUsernameInLogs) : evt.payload - ? JSON.stringify(redactHomePathUserSegmentsInValue(evt.payload)) + ? JSON.stringify(redactPathValue(evt.payload, censorUsernameInLogs)) : ""}
diff --git a/ui/src/pages/InstanceGeneralSettings.tsx b/ui/src/pages/InstanceGeneralSettings.tsx new file mode 100644 index 00000000..b73f488e --- /dev/null +++ b/ui/src/pages/InstanceGeneralSettings.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { SlidersHorizontal } from "lucide-react"; +import { instanceSettingsApi } from "@/api/instanceSettings"; +import { useBreadcrumbs } from "../context/BreadcrumbContext"; +import { queryKeys } from "../lib/queryKeys"; +import { cn } from "../lib/utils"; + +export function InstanceGeneralSettings() { + const { setBreadcrumbs } = useBreadcrumbs(); + const queryClient = useQueryClient(); + const [actionError, setActionError] = useState(null); + + useEffect(() => { + setBreadcrumbs([ + { label: "Instance Settings" }, + { label: "General" }, + ]); + }, [setBreadcrumbs]); + + const generalQuery = useQuery({ + queryKey: queryKeys.instance.generalSettings, + queryFn: () => instanceSettingsApi.getGeneral(), + }); + + const toggleMutation = useMutation({ + mutationFn: async (enabled: boolean) => + instanceSettingsApi.updateGeneral({ censorUsernameInLogs: enabled }), + onSuccess: async () => { + setActionError(null); + await queryClient.invalidateQueries({ queryKey: queryKeys.instance.generalSettings }); + }, + onError: (error) => { + setActionError(error instanceof Error ? error.message : "Failed to update general settings."); + }, + }); + + if (generalQuery.isLoading) { + return
Loading general settings...
; + } + + if (generalQuery.error) { + return ( +
+ {generalQuery.error instanceof Error + ? generalQuery.error.message + : "Failed to load general settings."} +
+ ); + } + + const censorUsernameInLogs = generalQuery.data?.censorUsernameInLogs === true; + + return ( +
+
+
+ +

General

+
+

+ Configure instance-wide defaults that affect how operator-visible logs are displayed. +

+
+ + {actionError && ( +
+ {actionError} +
+ )} + +
+
+
+

Censor username in logs

+

+ Hide the username segment in home-directory paths and similar log output. This is off by default. +

+
+ +
+
+
+ ); +} From dd44f69e2bae6f82d4cb134ccc573c953c485409 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 08:17:00 -0500 Subject: [PATCH 36/46] Fix PAP-576 settings toggles and transcript default Co-Authored-By: Paperclip --- ui/src/adapters/transcript.test.ts | 30 +++++++++++++++++++ ui/src/adapters/transcript.ts | 2 +- ui/src/pages/InstanceExperimentalSettings.tsx | 6 ++-- ui/src/pages/InstanceGeneralSettings.tsx | 6 ++-- 4 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 ui/src/adapters/transcript.test.ts diff --git a/ui/src/adapters/transcript.test.ts b/ui/src/adapters/transcript.test.ts new file mode 100644 index 00000000..8b56163e --- /dev/null +++ b/ui/src/adapters/transcript.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { buildTranscript, type RunLogChunk } from "./transcript"; + +describe("buildTranscript", () => { + const ts = "2026-03-20T13:00:00.000Z"; + const chunks: RunLogChunk[] = [ + { ts, stream: "stdout", chunk: "opened /Users/dotta/project\n" }, + { ts, stream: "stderr", chunk: "stderr /Users/dotta/project" }, + ]; + + it("defaults username censoring to off when options are omitted", () => { + const entries = buildTranscript(chunks, (line, entryTs) => [{ kind: "stdout", ts: entryTs, text: line }]); + + expect(entries).toEqual([ + { kind: "stdout", ts, text: "opened /Users/dotta/project" }, + { kind: "stderr", ts, text: "stderr /Users/dotta/project" }, + ]); + }); + + it("still redacts usernames when explicitly enabled", () => { + const entries = buildTranscript(chunks, (line, entryTs) => [{ kind: "stdout", ts: entryTs, text: line }], { + censorUsernameInLogs: true, + }); + + expect(entries).toEqual([ + { kind: "stdout", ts, text: "opened /Users/d****/project" }, + { kind: "stderr", ts, text: "stderr /Users/d****/project" }, + ]); + }); +}); diff --git a/ui/src/adapters/transcript.ts b/ui/src/adapters/transcript.ts index 238764ae..98b19454 100644 --- a/ui/src/adapters/transcript.ts +++ b/ui/src/adapters/transcript.ts @@ -29,7 +29,7 @@ export function buildTranscript( ): TranscriptEntry[] { const entries: TranscriptEntry[] = []; let stdoutBuffer = ""; - const redactionOptions = { enabled: opts?.censorUsernameInLogs ?? true }; + const redactionOptions = { enabled: opts?.censorUsernameInLogs ?? false }; for (const chunk of chunks) { if (chunk.stream === "stderr") { diff --git a/ui/src/pages/InstanceExperimentalSettings.tsx b/ui/src/pages/InstanceExperimentalSettings.tsx index 0a19ffd2..ac04ae07 100644 --- a/ui/src/pages/InstanceExperimentalSettings.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.tsx @@ -83,15 +83,15 @@ export function InstanceExperimentalSettings() { aria-label="Toggle isolated workspaces experimental setting" disabled={toggleMutation.isPending} className={cn( - "relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60", + "relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60", enableIsolatedWorkspaces ? "bg-green-600" : "bg-muted", )} onClick={() => toggleMutation.mutate(!enableIsolatedWorkspaces)} > diff --git a/ui/src/pages/InstanceGeneralSettings.tsx b/ui/src/pages/InstanceGeneralSettings.tsx index b73f488e..9720c98f 100644 --- a/ui/src/pages/InstanceGeneralSettings.tsx +++ b/ui/src/pages/InstanceGeneralSettings.tsx @@ -82,15 +82,15 @@ export function InstanceGeneralSettings() { aria-label="Toggle username log censoring" disabled={toggleMutation.isPending} className={cn( - "relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60", + "relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60", censorUsernameInLogs ? "bg-green-600" : "bg-muted", )} onClick={() => toggleMutation.mutate(!censorUsernameInLogs)} > From 8fc399f5112cacaaecab8721468c0548ea2eaaf1 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 08:43:47 -0500 Subject: [PATCH 37/46] Add guarded dev restart handling Co-Authored-By: Paperclip --- doc/DEVELOPING.md | 2 + packages/shared/src/types/instance.ts | 1 + packages/shared/src/validators/instance.ts | 1 + scripts/dev-runner.mjs | 423 ++++++++++++++++-- .../src/__tests__/dev-server-status.test.ts | 66 +++ .../instance-settings-routes.test.ts | 25 +- server/src/dev-server-status.ts | 103 +++++ server/src/routes/health.ts | 24 +- server/src/services/instance-settings.ts | 2 + ui/src/api/health.ts | 15 + ui/src/components/DevRestartBanner.tsx | 89 ++++ ui/src/components/Layout.tsx | 7 + ui/src/pages/InstanceExperimentalSettings.tsx | 43 +- 13 files changed, 758 insertions(+), 43 deletions(-) create mode 100644 server/src/__tests__/dev-server-status.test.ts create mode 100644 server/src/dev-server-status.ts create mode 100644 ui/src/components/DevRestartBanner.tsx diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index b39839c1..42e70fff 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -39,6 +39,8 @@ This starts: `pnpm dev` runs the server in watch mode and restarts on changes from workspace packages (including adapter packages). Use `pnpm dev:once` to run without file watching. +`pnpm dev:once` now tracks backend-relevant file changes and pending migrations. When the current boot is stale, the board UI shows a `Restart required` banner. You can also enable guarded auto-restart in `Instance Settings > Experimental`, which waits for queued/running local agent runs to finish before restarting the dev server. + Tailscale/private-auth dev mode: ```sh diff --git a/packages/shared/src/types/instance.ts b/packages/shared/src/types/instance.ts index 3449f46d..562c55b3 100644 --- a/packages/shared/src/types/instance.ts +++ b/packages/shared/src/types/instance.ts @@ -4,6 +4,7 @@ export interface InstanceGeneralSettings { export interface InstanceExperimentalSettings { enableIsolatedWorkspaces: boolean; + autoRestartDevServerWhenIdle: boolean; } export interface InstanceSettings { diff --git a/packages/shared/src/validators/instance.ts b/packages/shared/src/validators/instance.ts index 5511e655..05ee4323 100644 --- a/packages/shared/src/validators/instance.ts +++ b/packages/shared/src/validators/instance.ts @@ -8,6 +8,7 @@ export const patchInstanceGeneralSettingsSchema = instanceGeneralSettingsSchema. export const instanceExperimentalSettingsSchema = z.object({ enableIsolatedWorkspaces: z.boolean().default(false), + autoRestartDevServerWhenIdle: z.boolean().default(false), }).strict(); export const patchInstanceExperimentalSettingsSchema = instanceExperimentalSettingsSchema.partial(); diff --git a/scripts/dev-runner.mjs b/scripts/dev-runner.mjs index 391ddb44..3df034e0 100644 --- a/scripts/dev-runner.mjs +++ b/scripts/dev-runner.mjs @@ -1,10 +1,54 @@ #!/usr/bin/env node import { spawn } from "node:child_process"; +import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; +import path from "node:path"; import { createInterface } from "node:readline/promises"; import { stdin, stdout } from "node:process"; +import { fileURLToPath } from "node:url"; const mode = process.argv[2] === "watch" ? "watch" : "dev"; const cliArgs = process.argv.slice(3); +const scanIntervalMs = 1500; +const autoRestartPollIntervalMs = 2500; +const gracefulShutdownTimeoutMs = 10_000; +const changedPathSampleLimit = 5; +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const devServerStatusFilePath = path.join(repoRoot, ".paperclip", "dev-server-status.json"); + +const watchedDirectories = [ + ".paperclip", + "cli", + "scripts", + "server", + "packages/adapter-utils", + "packages/adapters", + "packages/db", + "packages/plugins/sdk", + "packages/shared", +].map((relativePath) => path.join(repoRoot, relativePath)); + +const watchedFiles = [ + ".env", + "package.json", + "pnpm-workspace.yaml", + "tsconfig.base.json", + "tsconfig.json", + "vitest.config.ts", +].map((relativePath) => path.join(repoRoot, relativePath)); + +const ignoredDirectoryNames = new Set([ + ".git", + ".turbo", + ".vite", + "coverage", + "dist", + "node_modules", + "ui-dist", +]); + +const ignoredRelativePaths = new Set([ + ".paperclip/dev-server-status.json", +]); const tailscaleAuthFlagNames = new Set([ "--tailscale-auth", @@ -34,6 +78,10 @@ const env = { PAPERCLIP_UI_DEV_MIDDLEWARE: "true", }; +if (mode === "dev") { + env.PAPERCLIP_DEV_SERVER_STATUS_FILE = devServerStatusFilePath; +} + if (mode === "watch") { env.PAPERCLIP_MIGRATION_PROMPT ??= "never"; env.PAPERCLIP_MIGRATION_AUTO_APPLY ??= "true"; @@ -50,6 +98,19 @@ if (tailscaleAuth) { } const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; +let previousSnapshot = collectWatchedSnapshot(); +let dirtyPaths = new Set(); +let pendingMigrations = []; +let lastChangedAt = null; +let lastRestartAt = null; +let scanInFlight = false; +let restartInFlight = false; +let shuttingDown = false; +let childExitWasExpected = false; +let child = null; +let childExitPromise = null; +let scanTimer = null; +let autoRestartTimer = null; function toError(error, context = "Dev runner command failed") { if (error instanceof Error) return error; @@ -82,9 +143,110 @@ function formatPendingMigrationSummary(migrations) { : migrations.join(", "); } +function exitForSignal(signal) { + if (signal === "SIGINT") { + process.exit(130); + } + if (signal === "SIGTERM") { + process.exit(143); + } + process.exit(1); +} + +function toRelativePath(absolutePath) { + return path.relative(repoRoot, absolutePath).split(path.sep).join("/"); +} + +function readSignature(absolutePath) { + const stats = statSync(absolutePath); + return `${Math.trunc(stats.mtimeMs)}:${stats.size}`; +} + +function addFileToSnapshot(snapshot, absolutePath) { + const relativePath = toRelativePath(absolutePath); + if (ignoredRelativePaths.has(relativePath)) return; + snapshot.set(relativePath, readSignature(absolutePath)); +} + +function walkDirectory(snapshot, absoluteDirectory) { + if (!existsSync(absoluteDirectory)) return; + + for (const entry of readdirSync(absoluteDirectory, { withFileTypes: true })) { + if (ignoredDirectoryNames.has(entry.name)) continue; + + const absolutePath = path.join(absoluteDirectory, entry.name); + if (entry.isDirectory()) { + walkDirectory(snapshot, absolutePath); + continue; + } + if (entry.isFile() || entry.isSymbolicLink()) { + addFileToSnapshot(snapshot, absolutePath); + } + } +} + +function collectWatchedSnapshot() { + const snapshot = new Map(); + + for (const absoluteDirectory of watchedDirectories) { + walkDirectory(snapshot, absoluteDirectory); + } + for (const absoluteFile of watchedFiles) { + if (!existsSync(absoluteFile)) continue; + addFileToSnapshot(snapshot, absoluteFile); + } + + return snapshot; +} + +function diffSnapshots(previous, next) { + const changed = new Set(); + + for (const [relativePath, signature] of next) { + if (previous.get(relativePath) !== signature) { + changed.add(relativePath); + } + } + for (const relativePath of previous.keys()) { + if (!next.has(relativePath)) { + changed.add(relativePath); + } + } + + return [...changed].sort(); +} + +function ensureDevStatusDirectory() { + mkdirSync(path.dirname(devServerStatusFilePath), { recursive: true }); +} + +function writeDevServerStatus() { + if (mode !== "dev") return; + + ensureDevStatusDirectory(); + const changedPaths = [...dirtyPaths].sort(); + writeFileSync( + devServerStatusFilePath, + `${JSON.stringify({ + dirty: changedPaths.length > 0 || pendingMigrations.length > 0, + lastChangedAt, + changedPathCount: changedPaths.length, + changedPathsSample: changedPaths.slice(0, changedPathSampleLimit), + pendingMigrations, + lastRestartAt, + }, null, 2)}\n`, + "utf8", + ); +} + +function clearDevServerStatus() { + if (mode !== "dev") return; + rmSync(devServerStatusFilePath, { force: true }); +} + async function runPnpm(args, options = {}) { return await new Promise((resolve, reject) => { - const child = spawn(pnpmBin, args, { + const spawned = spawn(pnpmBin, args, { stdio: options.stdio ?? ["ignore", "pipe", "pipe"], env: options.env ?? process.env, shell: process.platform === "win32", @@ -93,19 +255,19 @@ async function runPnpm(args, options = {}) { let stdoutBuffer = ""; let stderrBuffer = ""; - if (child.stdout) { - child.stdout.on("data", (chunk) => { + if (spawned.stdout) { + spawned.stdout.on("data", (chunk) => { stdoutBuffer += String(chunk); }); } - if (child.stderr) { - child.stderr.on("data", (chunk) => { + if (spawned.stderr) { + spawned.stderr.on("data", (chunk) => { stderrBuffer += String(chunk); }); } - child.on("error", reject); - child.on("exit", (code, signal) => { + spawned.on("error", reject); + spawned.on("exit", (code, signal) => { resolve({ code: code ?? 0, signal, @@ -116,9 +278,7 @@ async function runPnpm(args, options = {}) { }); } -async function maybePreflightMigrations() { - if (mode !== "watch") return; - +async function getMigrationStatusPayload() { const status = await runPnpm( ["--filter", "@paperclipai/db", "exec", "tsx", "src/migration-status.ts", "--json"], { env }, @@ -132,9 +292,8 @@ async function maybePreflightMigrations() { process.exit(status.code); } - let payload; try { - payload = JSON.parse(status.stdout.trim()); + return JSON.parse(status.stdout.trim()); } catch (error) { process.stderr.write( status.stderr || @@ -143,15 +302,31 @@ async function maybePreflightMigrations() { ); throw toError(error, "Unable to parse migration-status JSON output"); } +} - if (payload.status !== "needsMigrations" || payload.pendingMigrations.length === 0) { +async function refreshPendingMigrations() { + const payload = await getMigrationStatusPayload(); + pendingMigrations = + payload.status === "needsMigrations" && Array.isArray(payload.pendingMigrations) + ? payload.pendingMigrations.filter((entry) => typeof entry === "string" && entry.trim().length > 0) + : []; + writeDevServerStatus(); + return payload; +} + +async function maybePreflightMigrations(options = {}) { + const interactive = options.interactive ?? mode === "watch"; + const autoApply = options.autoApply ?? env.PAPERCLIP_MIGRATION_AUTO_APPLY === "true"; + const exitOnDecline = options.exitOnDecline ?? mode === "watch"; + + const payload = await refreshPendingMigrations(); + if (payload.status !== "needsMigrations" || pendingMigrations.length === 0) { return; } - const autoApply = env.PAPERCLIP_MIGRATION_AUTO_APPLY === "true"; let shouldApply = autoApply; - if (!autoApply) { + if (!autoApply && interactive) { if (!stdin.isTTY || !stdout.isTTY) { shouldApply = true; } else { @@ -159,7 +334,7 @@ async function maybePreflightMigrations() { try { const answer = ( await prompt.question( - `Apply pending migrations (${formatPendingMigrationSummary(payload.pendingMigrations)}) now? (y/N): `, + `Apply pending migrations (${formatPendingMigrationSummary(pendingMigrations)}) now? (y/N): `, ) ) .trim() @@ -172,11 +347,14 @@ async function maybePreflightMigrations() { } if (!shouldApply) { - process.stderr.write( - `[paperclip] Pending migrations detected (${formatPendingMigrationSummary(payload.pendingMigrations)}). ` + - "Refusing to start watch mode against a stale schema.\n", - ); - process.exit(1); + if (exitOnDecline) { + process.stderr.write( + `[paperclip] Pending migrations detected (${formatPendingMigrationSummary(pendingMigrations)}). ` + + "Refusing to start watch mode against a stale schema.\n", + ); + process.exit(1); + } + return; } const migrate = spawn(pnpmBin, ["db:migrate"], { @@ -188,15 +366,15 @@ async function maybePreflightMigrations() { migrate.on("exit", (code, signal) => resolve({ code: code ?? 0, signal })); }); if (exit.signal) { - process.kill(process.pid, exit.signal); + exitForSignal(exit.signal); return; } if (exit.code !== 0) { process.exit(exit.code); } -} -await maybePreflightMigrations(); + await refreshPendingMigrations(); +} async function buildPluginSdk() { console.log("[paperclip] building plugin sdk..."); @@ -205,7 +383,7 @@ async function buildPluginSdk() { { stdio: "inherit" }, ); if (result.signal) { - process.kill(process.pid, result.signal); + exitForSignal(result.signal); return; } if (result.code !== 0) { @@ -214,19 +392,192 @@ async function buildPluginSdk() { } } -await buildPluginSdk(); +async function markChildAsCurrent() { + previousSnapshot = collectWatchedSnapshot(); + dirtyPaths = new Set(); + lastChangedAt = null; + lastRestartAt = new Date().toISOString(); + await refreshPendingMigrations(); +} -const serverScript = mode === "watch" ? "dev:watch" : "dev"; -const child = spawn( - pnpmBin, - ["--filter", "@paperclipai/server", serverScript, ...forwardedArgs], - { stdio: "inherit", env, shell: process.platform === "win32" }, -); +async function scanForBackendChanges() { + if (mode !== "dev" || scanInFlight || restartInFlight) return; + scanInFlight = true; + try { + const nextSnapshot = collectWatchedSnapshot(); + const changed = diffSnapshots(previousSnapshot, nextSnapshot); + previousSnapshot = nextSnapshot; + if (changed.length === 0) return; -child.on("exit", (code, signal) => { - if (signal) { - process.kill(process.pid, signal); + for (const relativePath of changed) { + dirtyPaths.add(relativePath); + } + lastChangedAt = new Date().toISOString(); + await refreshPendingMigrations(); + } finally { + scanInFlight = false; + } +} + +async function getDevHealthPayload() { + const serverPort = env.PORT ?? process.env.PORT ?? "3100"; + const response = await fetch(`http://127.0.0.1:${serverPort}/api/health`); + if (!response.ok) { + throw new Error(`Health request failed (${response.status})`); + } + return await response.json(); +} + +async function waitForChildExit() { + if (!childExitPromise) { + return { code: 0, signal: null }; + } + return await childExitPromise; +} + +async function stopChildForRestart() { + if (!child) return { code: 0, signal: null }; + childExitWasExpected = true; + child.kill("SIGTERM"); + const killTimer = setTimeout(() => { + if (child) { + child.kill("SIGKILL"); + } + }, gracefulShutdownTimeoutMs); + try { + return await waitForChildExit(); + } finally { + clearTimeout(killTimer); + } +} + +async function startServerChild() { + await buildPluginSdk(); + + const serverScript = mode === "watch" ? "dev:watch" : "dev"; + child = spawn( + pnpmBin, + ["--filter", "@paperclipai/server", serverScript, ...forwardedArgs], + { stdio: "inherit", env, shell: process.platform === "win32" }, + ); + + childExitPromise = new Promise((resolve, reject) => { + child.on("error", reject); + child.on("exit", (code, signal) => { + const expected = childExitWasExpected; + childExitWasExpected = false; + child = null; + childExitPromise = null; + resolve({ code: code ?? 0, signal }); + + if (restartInFlight || expected || shuttingDown) { + return; + } + if (signal) { + exitForSignal(signal); + return; + } + process.exit(code ?? 0); + }); + }); + + await markChildAsCurrent(); +} + +async function maybeAutoRestartChild() { + if (mode !== "dev" || restartInFlight || !child) return; + if (dirtyPaths.size === 0 && pendingMigrations.length === 0) return; + + let health; + try { + health = await getDevHealthPayload(); + } catch { return; } - process.exit(code ?? 0); + + const devServer = health?.devServer; + if (!devServer?.enabled || devServer.autoRestartEnabled !== true) return; + if ((devServer.activeRunCount ?? 0) > 0) return; + + try { + restartInFlight = true; + await maybePreflightMigrations({ + autoApply: true, + interactive: false, + exitOnDecline: false, + }); + await stopChildForRestart(); + await startServerChild(); + } catch (error) { + const err = toError(error, "Auto-restart failed"); + process.stderr.write(`${err.stack ?? err.message}\n`); + process.exit(1); + } finally { + restartInFlight = false; + } +} + +function installDevIntervals() { + if (mode !== "dev") return; + + scanTimer = setInterval(() => { + void scanForBackendChanges(); + }, scanIntervalMs); + autoRestartTimer = setInterval(() => { + void maybeAutoRestartChild(); + }, autoRestartPollIntervalMs); +} + +function clearDevIntervals() { + if (scanTimer) { + clearInterval(scanTimer); + scanTimer = null; + } + if (autoRestartTimer) { + clearInterval(autoRestartTimer); + autoRestartTimer = null; + } +} + +async function shutdown(signal) { + if (shuttingDown) return; + shuttingDown = true; + clearDevIntervals(); + clearDevServerStatus(); + + if (!child) { + if (signal) { + exitForSignal(signal); + return; + } + process.exit(0); + } + + childExitWasExpected = true; + child.kill(signal); + const exit = await waitForChildExit(); + if (exit.signal) { + exitForSignal(exit.signal); + return; + } + process.exit(exit.code ?? 0); +} + +process.on("SIGINT", () => { + void shutdown("SIGINT"); }); +process.on("SIGTERM", () => { + void shutdown("SIGTERM"); +}); + +await maybePreflightMigrations(); +await startServerChild(); +installDevIntervals(); + +if (mode === "watch") { + const exit = await waitForChildExit(); + if (exit.signal) { + exitForSignal(exit.signal); + } + process.exit(exit.code ?? 0); +} diff --git a/server/src/__tests__/dev-server-status.test.ts b/server/src/__tests__/dev-server-status.test.ts new file mode 100644 index 00000000..d178f941 --- /dev/null +++ b/server/src/__tests__/dev-server-status.test.ts @@ -0,0 +1,66 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { readPersistedDevServerStatus, toDevServerHealthStatus } from "../dev-server-status.js"; + +const tempDirs = []; + +function createTempStatusFile(payload: unknown) { + const dir = mkdtempSync(path.join(os.tmpdir(), "paperclip-dev-status-")); + tempDirs.push(dir); + const filePath = path.join(dir, "dev-server-status.json"); + writeFileSync(filePath, `${JSON.stringify(payload)}\n`, "utf8"); + return filePath; +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +describe("dev server status helpers", () => { + it("reads and normalizes persisted supervisor state", () => { + const filePath = createTempStatusFile({ + dirty: true, + lastChangedAt: "2026-03-20T12:00:00.000Z", + changedPathCount: 4, + changedPathsSample: ["server/src/app.ts", "packages/shared/src/index.ts"], + pendingMigrations: ["0040_restart_banner.sql"], + lastRestartAt: "2026-03-20T11:30:00.000Z", + }); + + expect(readPersistedDevServerStatus({ PAPERCLIP_DEV_SERVER_STATUS_FILE: filePath })).toEqual({ + dirty: true, + lastChangedAt: "2026-03-20T12:00:00.000Z", + changedPathCount: 4, + changedPathsSample: ["server/src/app.ts", "packages/shared/src/index.ts"], + pendingMigrations: ["0040_restart_banner.sql"], + lastRestartAt: "2026-03-20T11:30:00.000Z", + }); + }); + + it("derives waiting-for-idle health state", () => { + const health = toDevServerHealthStatus( + { + dirty: true, + lastChangedAt: "2026-03-20T12:00:00.000Z", + changedPathCount: 2, + changedPathsSample: ["server/src/app.ts"], + pendingMigrations: [], + lastRestartAt: "2026-03-20T11:30:00.000Z", + }, + { autoRestartEnabled: true, activeRunCount: 3 }, + ); + + expect(health).toMatchObject({ + enabled: true, + restartRequired: true, + reason: "backend_changes", + autoRestartEnabled: true, + activeRunCount: 3, + waitingForIdle: true, + }); + }); +}); diff --git a/server/src/__tests__/instance-settings-routes.test.ts b/server/src/__tests__/instance-settings-routes.test.ts index 8b56fd67..9668d1bf 100644 --- a/server/src/__tests__/instance-settings-routes.test.ts +++ b/server/src/__tests__/instance-settings-routes.test.ts @@ -38,6 +38,7 @@ describe("instance settings routes", () => { }); mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: false, + autoRestartDevServerWhenIdle: false, }); mockInstanceSettingsService.updateGeneral.mockResolvedValue({ id: "instance-settings-1", @@ -49,6 +50,7 @@ describe("instance settings routes", () => { id: "instance-settings-1", experimental: { enableIsolatedWorkspaces: true, + autoRestartDevServerWhenIdle: false, }, }); mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1", "company-2"]); @@ -64,7 +66,10 @@ describe("instance settings routes", () => { const getRes = await request(app).get("/api/instance/settings/experimental"); expect(getRes.status).toBe(200); - expect(getRes.body).toEqual({ enableIsolatedWorkspaces: false }); + expect(getRes.body).toEqual({ + enableIsolatedWorkspaces: false, + autoRestartDevServerWhenIdle: false, + }); const patchRes = await request(app) .patch("/api/instance/settings/experimental") @@ -77,6 +82,24 @@ describe("instance settings routes", () => { expect(mockLogActivity).toHaveBeenCalledTimes(2); }); + it("allows local board users to update guarded dev-server auto-restart", async () => { + const app = createApp({ + type: "board", + userId: "local-board", + source: "local_implicit", + isInstanceAdmin: true, + }); + + await request(app) + .patch("/api/instance/settings/experimental") + .send({ autoRestartDevServerWhenIdle: true }) + .expect(200); + + expect(mockInstanceSettingsService.updateExperimental).toHaveBeenCalledWith({ + autoRestartDevServerWhenIdle: true, + }); + }); + it("allows local board users to read and update general settings", async () => { const app = createApp({ type: "board", diff --git a/server/src/dev-server-status.ts b/server/src/dev-server-status.ts new file mode 100644 index 00000000..aecb0fc9 --- /dev/null +++ b/server/src/dev-server-status.ts @@ -0,0 +1,103 @@ +import { existsSync, readFileSync } from "node:fs"; + +export type PersistedDevServerStatus = { + dirty: boolean; + lastChangedAt: string | null; + changedPathCount: number; + changedPathsSample: string[]; + pendingMigrations: string[]; + lastRestartAt: string | null; +}; + +export type DevServerHealthStatus = { + enabled: true; + restartRequired: boolean; + reason: "backend_changes" | "pending_migrations" | "backend_changes_and_pending_migrations" | null; + lastChangedAt: string | null; + changedPathCount: number; + changedPathsSample: string[]; + pendingMigrations: string[]; + autoRestartEnabled: boolean; + activeRunCount: number; + waitingForIdle: boolean; + lastRestartAt: string | null; +}; + +function normalizeStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value + .filter((entry): entry is string => typeof entry === "string") + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0); +} + +function normalizeTimestamp(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +export function readPersistedDevServerStatus( + env: NodeJS.ProcessEnv = process.env, +): PersistedDevServerStatus | null { + const filePath = env.PAPERCLIP_DEV_SERVER_STATUS_FILE?.trim(); + if (!filePath || !existsSync(filePath)) return null; + + try { + const raw = JSON.parse(readFileSync(filePath, "utf8")) as Record; + const changedPathsSample = normalizeStringArray(raw.changedPathsSample).slice(0, 5); + const pendingMigrations = normalizeStringArray(raw.pendingMigrations); + const changedPathCountRaw = raw.changedPathCount; + const changedPathCount = + typeof changedPathCountRaw === "number" && Number.isFinite(changedPathCountRaw) + ? Math.max(0, Math.trunc(changedPathCountRaw)) + : changedPathsSample.length; + const dirtyRaw = raw.dirty; + const dirty = + typeof dirtyRaw === "boolean" + ? dirtyRaw + : changedPathCount > 0 || pendingMigrations.length > 0; + + return { + dirty, + lastChangedAt: normalizeTimestamp(raw.lastChangedAt), + changedPathCount, + changedPathsSample, + pendingMigrations, + lastRestartAt: normalizeTimestamp(raw.lastRestartAt), + }; + } catch { + return null; + } +} + +export function toDevServerHealthStatus( + persisted: PersistedDevServerStatus, + opts: { autoRestartEnabled: boolean; activeRunCount: number }, +): DevServerHealthStatus { + const hasPathChanges = persisted.changedPathCount > 0; + const hasPendingMigrations = persisted.pendingMigrations.length > 0; + const reason = + hasPathChanges && hasPendingMigrations + ? "backend_changes_and_pending_migrations" + : hasPendingMigrations + ? "pending_migrations" + : hasPathChanges + ? "backend_changes" + : null; + const restartRequired = persisted.dirty || reason !== null; + + return { + enabled: true, + restartRequired, + reason, + lastChangedAt: persisted.lastChangedAt, + changedPathCount: persisted.changedPathCount, + changedPathsSample: persisted.changedPathsSample, + pendingMigrations: persisted.pendingMigrations, + autoRestartEnabled: opts.autoRestartEnabled, + activeRunCount: opts.activeRunCount, + waitingForIdle: restartRequired && opts.autoRestartEnabled && opts.activeRunCount > 0, + lastRestartAt: persisted.lastRestartAt, + }; +} diff --git a/server/src/routes/health.ts b/server/src/routes/health.ts index 59897a89..0bf6e92f 100644 --- a/server/src/routes/health.ts +++ b/server/src/routes/health.ts @@ -1,8 +1,10 @@ import { Router } from "express"; import type { Db } from "@paperclipai/db"; -import { and, count, eq, gt, isNull, sql } from "drizzle-orm"; -import { instanceUserRoles, invites } from "@paperclipai/db"; +import { and, count, eq, gt, inArray, isNull, sql } from "drizzle-orm"; +import { heartbeatRuns, instanceUserRoles, invites } from "@paperclipai/db"; import type { DeploymentExposure, DeploymentMode } from "@paperclipai/shared"; +import { readPersistedDevServerStatus, toDevServerHealthStatus } from "../dev-server-status.js"; +import { instanceSettingsService } from "../services/instance-settings.js"; import { serverVersion } from "../version.js"; export function healthRoutes( @@ -55,6 +57,23 @@ export function healthRoutes( } } + const persistedDevServerStatus = readPersistedDevServerStatus(); + let devServer: ReturnType | undefined; + if (persistedDevServerStatus) { + const instanceSettings = instanceSettingsService(db); + const experimentalSettings = await instanceSettings.getExperimental(); + const activeRunCount = await db + .select({ count: count() }) + .from(heartbeatRuns) + .where(inArray(heartbeatRuns.status, ["queued", "running"])) + .then((rows) => Number(rows[0]?.count ?? 0)); + + devServer = toDevServerHealthStatus(persistedDevServerStatus, { + autoRestartEnabled: experimentalSettings.autoRestartDevServerWhenIdle ?? false, + activeRunCount, + }); + } + res.json({ status: "ok", version: serverVersion, @@ -66,6 +85,7 @@ export function healthRoutes( features: { companyDeletionEnabled: opts.companyDeletionEnabled, }, + ...(devServer ? { devServer } : {}), }); }); diff --git a/server/src/services/instance-settings.ts b/server/src/services/instance-settings.ts index bbc4df3d..ccefea7c 100644 --- a/server/src/services/instance-settings.ts +++ b/server/src/services/instance-settings.ts @@ -30,10 +30,12 @@ function normalizeExperimentalSettings(raw: unknown): InstanceExperimentalSettin if (parsed.success) { return { enableIsolatedWorkspaces: parsed.data.enableIsolatedWorkspaces ?? false, + autoRestartDevServerWhenIdle: parsed.data.autoRestartDevServerWhenIdle ?? false, }; } return { enableIsolatedWorkspaces: false, + autoRestartDevServerWhenIdle: false, }; } diff --git a/ui/src/api/health.ts b/ui/src/api/health.ts index b1573805..e2725b20 100644 --- a/ui/src/api/health.ts +++ b/ui/src/api/health.ts @@ -1,3 +1,17 @@ +export type DevServerHealthStatus = { + enabled: true; + restartRequired: boolean; + reason: "backend_changes" | "pending_migrations" | "backend_changes_and_pending_migrations" | null; + lastChangedAt: string | null; + changedPathCount: number; + changedPathsSample: string[]; + pendingMigrations: string[]; + autoRestartEnabled: boolean; + activeRunCount: number; + waitingForIdle: boolean; + lastRestartAt: string | null; +}; + export type HealthStatus = { status: "ok"; version?: string; @@ -9,6 +23,7 @@ export type HealthStatus = { features?: { companyDeletionEnabled?: boolean; }; + devServer?: DevServerHealthStatus; }; export const healthApi = { diff --git a/ui/src/components/DevRestartBanner.tsx b/ui/src/components/DevRestartBanner.tsx new file mode 100644 index 00000000..5f8ba8a0 --- /dev/null +++ b/ui/src/components/DevRestartBanner.tsx @@ -0,0 +1,89 @@ +import { AlertTriangle, RotateCcw, TimerReset } from "lucide-react"; +import type { DevServerHealthStatus } from "../api/health"; + +function formatRelativeTimestamp(value: string | null): string | null { + if (!value) return null; + const timestamp = new Date(value).getTime(); + if (Number.isNaN(timestamp)) return null; + + const deltaMs = Date.now() - timestamp; + if (deltaMs < 60_000) return "just now"; + const deltaMinutes = Math.round(deltaMs / 60_000); + if (deltaMinutes < 60) return `${deltaMinutes}m ago`; + const deltaHours = Math.round(deltaMinutes / 60); + if (deltaHours < 24) return `${deltaHours}h ago`; + const deltaDays = Math.round(deltaHours / 24); + return `${deltaDays}d ago`; +} + +function describeReason(devServer: DevServerHealthStatus): string { + if (devServer.reason === "backend_changes_and_pending_migrations") { + return "backend files changed and migrations are pending"; + } + if (devServer.reason === "pending_migrations") { + return "pending migrations need a fresh boot"; + } + return "backend files changed since this server booted"; +} + +export function DevRestartBanner({ devServer }: { devServer?: DevServerHealthStatus }) { + if (!devServer?.enabled || !devServer.restartRequired) return null; + + const changedAt = formatRelativeTimestamp(devServer.lastChangedAt); + const sample = devServer.changedPathsSample.slice(0, 3); + + return ( +
+
+
+
+ + Restart Required + {devServer.autoRestartEnabled ? ( + + Auto-Restart On + + ) : null} +
+

+ {describeReason(devServer)} + {changedAt ? ` · updated ${changedAt}` : ""} +

+
+ {sample.length > 0 ? ( + + Changed: {sample.join(", ")} + {devServer.changedPathCount > sample.length ? ` +${devServer.changedPathCount - sample.length} more` : ""} + + ) : null} + {devServer.pendingMigrations.length > 0 ? ( + + Pending migrations: {devServer.pendingMigrations.slice(0, 2).join(", ")} + {devServer.pendingMigrations.length > 2 ? ` +${devServer.pendingMigrations.length - 2} more` : ""} + + ) : null} +
+
+ +
+ {devServer.waitingForIdle ? ( +
+ + Waiting for {devServer.activeRunCount} live run{devServer.activeRunCount === 1 ? "" : "s"} to finish +
+ ) : devServer.autoRestartEnabled ? ( +
+ + Auto-restart will trigger when the instance is idle +
+ ) : ( +
+ + Restart `pnpm dev:once` after the active work is safe to interrupt +
+ )} +
+
+
+ ); +} diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 8bae6920..8761b71c 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -15,6 +15,7 @@ import { NewAgentDialog } from "./NewAgentDialog"; import { ToastViewport } from "./ToastViewport"; import { MobileBottomNav } from "./MobileBottomNav"; import { WorktreeBanner } from "./WorktreeBanner"; +import { DevRestartBanner } from "./DevRestartBanner"; import { useDialog } from "../context/DialogContext"; import { usePanel } from "../context/PanelContext"; import { useCompany } from "../context/CompanyContext"; @@ -78,6 +79,11 @@ export function Layout() { queryKey: queryKeys.health, queryFn: () => healthApi.get(), retry: false, + refetchInterval: (query) => { + const data = query.state.data as { devServer?: { enabled?: boolean } } | undefined; + return data?.devServer?.enabled ? 2000 : false; + }, + refetchIntervalInBackground: true, }); useEffect(() => { @@ -266,6 +272,7 @@ export function Layout() { Skip to Main Content +
{isMobile && sidebarOpen && ( +
+
); } From 0f4a5716eac5f6de9082fc23d6b1682c351ee3a9 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 08:49:46 -0500 Subject: [PATCH 38/46] docs: clarify quickstart npx usage --- docs/start/quickstart.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/start/quickstart.md b/docs/start/quickstart.md index 9488b3c7..1ad30fcd 100644 --- a/docs/start/quickstart.md +++ b/docs/start/quickstart.md @@ -13,9 +13,19 @@ npx paperclipai onboard --yes This walks you through setup, configures your environment, and gets Paperclip running. +To start Paperclip again later: + +```sh +npx paperclipai run +``` + +> **Note:** If you used `npx` for setup, always use `npx paperclipai` to run commands. The `pnpm paperclipai` form only works inside a cloned copy of the Paperclip repository (see Local Development below). + ## Local Development -Prerequisites: Node.js 20+ and pnpm 9+. +For contributors working on Paperclip itself. Prerequisites: Node.js 20+ and pnpm 9+. + +Clone the repository, then: ```sh pnpm install @@ -26,7 +36,7 @@ This starts the API server and UI at [http://localhost:3100](http://localhost:31 No external database required — Paperclip uses an embedded PostgreSQL instance by default. -## One-Command Bootstrap +When working from the cloned repo, you can also use: ```sh pnpm paperclipai run From 79652da520464643b224da1a280f2c7a121ee4b9 Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 08:55:10 -0500 Subject: [PATCH 39/46] Address remaining Greptile portability feedback --- .../src/__tests__/company-portability.test.ts | 57 +++++++++++++++++++ server/src/__tests__/company-skills.test.ts | 39 +++++++++++++ server/src/services/company-portability.ts | 3 + server/src/services/company-skills.ts | 36 +++++++++--- ui/src/pages/CompanyImport.tsx | 5 +- 5 files changed, 131 insertions(+), 9 deletions(-) diff --git a/server/src/__tests__/company-portability.test.ts b/server/src/__tests__/company-portability.test.ts index 7e4499ed..81d3d4eb 100644 --- a/server/src/__tests__/company-portability.test.ts +++ b/server/src/__tests__/company-portability.test.ts @@ -623,6 +623,63 @@ describe("company portability", () => { ]); }); + it("treats no-separator auth and api key env names as secrets during export", async () => { + const portability = companyPortabilityService({} as any); + + agentSvc.list.mockResolvedValue([ + { + id: "agent-1", + name: "ClaudeCoder", + status: "idle", + role: "engineer", + title: "Software Engineer", + icon: "code", + reportsTo: null, + capabilities: "Writes code", + adapterType: "claude_local", + adapterConfig: { + promptTemplate: "You are ClaudeCoder.", + env: { + APIKEY: { + type: "plain", + value: "sk-plain-api", + }, + GITHUBAUTH: { + type: "plain", + value: "gh-auth-token", + }, + PRIVATEKEY: { + type: "plain", + value: "private-key-value", + }, + }, + }, + runtimeConfig: {}, + budgetMonthlyCents: 0, + permissions: {}, + metadata: null, + }, + ]); + + const exported = await portability.exportBundle("company-1", { + include: { + company: true, + agents: true, + projects: false, + issues: false, + }, + }); + + const extension = asTextFile(exported.files[".paperclip.yaml"]); + expect(extension).toContain("APIKEY:"); + expect(extension).toContain("GITHUBAUTH:"); + expect(extension).toContain("PRIVATEKEY:"); + expect(extension).not.toContain("sk-plain-api"); + expect(extension).not.toContain("gh-auth-token"); + expect(extension).not.toContain("private-key-value"); + expect(extension).toContain('kind: "secret"'); + }); + it("imports packaged skills and restores desired skill refs on agents", async () => { const portability = companyPortabilityService({} as any); diff --git a/server/src/__tests__/company-skills.test.ts b/server/src/__tests__/company-skills.test.ts index 77fd072e..17da7804 100644 --- a/server/src/__tests__/company-skills.test.ts +++ b/server/src/__tests__/company-skills.test.ts @@ -138,6 +138,45 @@ describe("project workspace skill discovery", () => { expect(imported.fileInventory.map((entry) => entry.kind)).toContain("script"); expect(imported.metadata?.sourceKind).toBe("project_scan"); }); + + it("parses inline object array items in skill frontmatter metadata", async () => { + const workspace = await makeTempDir("paperclip-inline-skill-yaml-"); + await fs.mkdir(workspace, { recursive: true }); + await fs.writeFile( + path.join(workspace, "SKILL.md"), + [ + "---", + "name: Inline Metadata Skill", + "metadata:", + " sources:", + " - kind: github-dir", + " repo: paperclipai/paperclip", + " path: skills/paperclip", + "---", + "", + "# Inline Metadata Skill", + "", + ].join("\n"), + "utf8", + ); + + const imported = await readLocalSkillImportFromDirectory( + "33333333-3333-4333-8333-333333333333", + workspace, + { inventoryMode: "full" }, + ); + + expect(imported.metadata).toMatchObject({ + sourceKind: "local_path", + sources: [ + { + kind: "github-dir", + repo: "paperclipai/paperclip", + path: "skills/paperclip", + }, + ], + }); + }); }); describe("missing local skill reconciliation", () => { diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index c0bb8a54..58f73d30 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -351,10 +351,12 @@ function isSensitiveEnvKey(key: string) { normalized === "token" || normalized.endsWith("_token") || normalized.endsWith("-token") || + normalized.includes("apikey") || normalized.includes("api_key") || normalized.includes("api-key") || normalized.includes("access_token") || normalized.includes("access-token") || + normalized.includes("auth") || normalized.includes("auth_token") || normalized.includes("auth-token") || normalized.includes("authorization") || @@ -364,6 +366,7 @@ function isSensitiveEnvKey(key: string) { normalized.includes("password") || normalized.includes("credential") || normalized.includes("jwt") || + normalized.includes("privatekey") || normalized.includes("private_key") || normalized.includes("private-key") || normalized.includes("cookie") || diff --git a/server/src/services/company-skills.ts b/server/src/services/company-skills.ts index c927e3b7..19aeab04 100644 --- a/server/src/services/company-skills.ts +++ b/server/src/services/company-skills.ts @@ -377,6 +377,28 @@ function parseYamlBlock( index = nested.nextIndex; continue; } + const inlineObjectSeparator = remainder.indexOf(":"); + if ( + inlineObjectSeparator > 0 && + !remainder.startsWith("\"") && + !remainder.startsWith("{") && + !remainder.startsWith("[") + ) { + const key = remainder.slice(0, inlineObjectSeparator).trim(); + const rawValue = remainder.slice(inlineObjectSeparator + 1).trim(); + const nextObject: Record = { + [key]: parseYamlScalar(rawValue), + }; + if (index < lines.length && lines[index]!.indent > indentLevel) { + const nested = parseYamlBlock(lines, index, indentLevel + 2); + if (isPlainRecord(nested.value)) { + Object.assign(nextObject, nested.value); + } + index = nested.nextIndex; + } + values.push(nextObject); + continue; + } values.push(parseYamlScalar(remainder)); } return { value: values, nextIndex: index }; @@ -804,12 +826,11 @@ export async function readLocalSkillImportFromDirectory( const markdown = await fs.readFile(skillFilePath, "utf8"); const parsed = parseFrontmatterMarkdown(markdown); const slug = deriveImportedSkillSlug(parsed.frontmatter, path.basename(resolvedSkillDir)); - const skillKey = readCanonicalSkillKey( - parsed.frontmatter, - isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null, - ); + const parsedMetadata = isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null; + const skillKey = readCanonicalSkillKey(parsed.frontmatter, parsedMetadata); const metadata = { ...(skillKey ? { skillKey } : {}), + ...(parsedMetadata ?? {}), sourceKind: "local_path", ...(options?.metadata ?? {}), }; @@ -877,12 +898,11 @@ async function readLocalSkillImports(companyId: string, sourcePath: string): Pro const markdown = await fs.readFile(resolvedPath, "utf8"); const parsed = parseFrontmatterMarkdown(markdown); const slug = deriveImportedSkillSlug(parsed.frontmatter, path.basename(path.dirname(resolvedPath))); - const skillKey = readCanonicalSkillKey( - parsed.frontmatter, - isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null, - ); + const parsedMetadata = isPlainRecord(parsed.frontmatter.metadata) ? parsed.frontmatter.metadata : null; + const skillKey = readCanonicalSkillKey(parsed.frontmatter, parsedMetadata); const metadata = { ...(skillKey ? { skillKey } : {}), + ...(parsedMetadata ?? {}), sourceKind: "local_path", }; const inventory: CompanySkillFileInventoryEntry[] = [ diff --git a/ui/src/pages/CompanyImport.tsx b/ui/src/pages/CompanyImport.tsx index 10b00266..44d1d9a3 100644 --- a/ui/src/pages/CompanyImport.tsx +++ b/ui/src/pages/CompanyImport.tsx @@ -655,6 +655,9 @@ export function CompanyImport() { return ceo?.adapterType ?? "claude_local"; }, [companyAgents]); + const localZipHelpText = + "Upload a .zip exported directly from Paperclip. Re-zipped archives created by Finder, Explorer, or other zip tools may not import correctly."; + useEffect(() => { setBreadcrumbs([ { label: "Org Chart", href: "/org" }, @@ -1093,7 +1096,7 @@ export function CompanyImport() {
{!localPackage && (

- Upload a `.zip` exported from Paperclip that contains COMPANY.md and the related package files. + {localZipHelpText}

)}
From 360a7fc17b3c2d286769c57ab0494efc4b28f0dc Mon Sep 17 00:00:00 2001 From: dotta Date: Fri, 20 Mar 2026 13:18:29 -0500 Subject: [PATCH 40/46] fix: address greptile follow-up feedback --- scripts/dev-runner.mjs | 13 ++++++++++--- ui/src/components/DevRestartBanner.tsx | 2 +- ui/src/pages/InstanceExperimentalSettings.tsx | 2 +- ui/src/pages/InstanceGeneralSettings.tsx | 4 +++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/dev-runner.mjs b/scripts/dev-runner.mjs index 3df034e0..a0910430 100644 --- a/scripts/dev-runner.mjs +++ b/scripts/dev-runner.mjs @@ -488,19 +488,26 @@ async function maybeAutoRestartChild() { if (mode !== "dev" || restartInFlight || !child) return; if (dirtyPaths.size === 0 && pendingMigrations.length === 0) return; + restartInFlight = true; let health; try { health = await getDevHealthPayload(); } catch { + restartInFlight = false; return; } const devServer = health?.devServer; - if (!devServer?.enabled || devServer.autoRestartEnabled !== true) return; - if ((devServer.activeRunCount ?? 0) > 0) return; + if (!devServer?.enabled || devServer.autoRestartEnabled !== true) { + restartInFlight = false; + return; + } + if ((devServer.activeRunCount ?? 0) > 0) { + restartInFlight = false; + return; + } try { - restartInFlight = true; await maybePreflightMigrations({ autoApply: true, interactive: false, diff --git a/ui/src/components/DevRestartBanner.tsx b/ui/src/components/DevRestartBanner.tsx index 5f8ba8a0..2ff666d9 100644 --- a/ui/src/components/DevRestartBanner.tsx +++ b/ui/src/components/DevRestartBanner.tsx @@ -79,7 +79,7 @@ export function DevRestartBanner({ devServer }: { devServer?: DevServerHealthSta ) : (
- Restart `pnpm dev:once` after the active work is safe to interrupt + Restart pnpm dev:once after the active work is safe to interrupt
)}
diff --git a/ui/src/pages/InstanceExperimentalSettings.tsx b/ui/src/pages/InstanceExperimentalSettings.tsx index 236d4544..07728a63 100644 --- a/ui/src/pages/InstanceExperimentalSettings.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.tsx @@ -76,7 +76,7 @@ export function InstanceExperimentalSettings() {
-

Enabled Isolated Workspaces

+

Enable Isolated Workspaces

Show execution workspace controls in project configuration and allow isolated workspace behavior for new and existing issue runs. diff --git a/ui/src/pages/InstanceGeneralSettings.tsx b/ui/src/pages/InstanceGeneralSettings.tsx index 9720c98f..4f0d1cae 100644 --- a/ui/src/pages/InstanceGeneralSettings.tsx +++ b/ui/src/pages/InstanceGeneralSettings.tsx @@ -74,7 +74,9 @@ export function InstanceGeneralSettings() {

Censor username in logs

- Hide the username segment in home-directory paths and similar log output. This is off by default. + Hide the username segment in home-directory paths and similar operator-visible log output. Standalone + username mentions outside of paths are not yet masked in the live transcript view. This is off by + default.