mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 19:50:38 +09:00
Merge remote-tracking branch 'public-gh/master' into paperclip-routines
* public-gh/master: (46 commits) chore(lockfile): refresh pnpm-lock.yaml (#1377) fix: manage codex home per company by default Ensure agent home directories exist before use Handle directory entries in imported zip archives Fix portability import and org chart test blockers Fix PR verify failures after merge fix: address greptile follow-up feedback Address remaining Greptile portability feedback docs: clarify quickstart npx usage Add guarded dev restart handling Fix PAP-576 settings toggles and transcript default Add username log censor setting fix: use standard toggle component for permission controls fix: add missing setPrincipalPermission mock in portability tests fix: use fixed 1280x640 dimensions for org chart export image Adjust default CEO onboarding task copy fix: link Agent Company to agentcompanies.io in export README fix: strip agents and projects sections from COMPANY.md export body fix: default company export page to README.md instead of first file Add default agent instructions bundle ... # Conflicts: # packages/adapters/pi-local/src/server/execute.ts # packages/db/src/migrations/meta/0039_snapshot.json # packages/db/src/migrations/meta/_journal.json # server/src/__tests__/agent-permissions-routes.test.ts # server/src/__tests__/agent-skills-routes.test.ts # server/src/services/company-portability.ts # skills/paperclip/references/company-skills.md # ui/src/api/agents.ts
This commit is contained in:
commit
e3c92a20f1
96 changed files with 15366 additions and 1684 deletions
|
|
@ -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(),
|
||||
|
|
@ -82,8 +83,17 @@ vi.mock("../services/agent-instructions.js", () => ({
|
|||
agentInstructionsService: () => agentInstructionsSvc,
|
||||
}));
|
||||
|
||||
vi.mock("../routes/org-chart-svg.js", () => ({
|
||||
renderOrgChartPng: vi.fn(async () => Buffer.from("png")),
|
||||
}));
|
||||
|
||||
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";
|
||||
|
|
@ -303,19 +313,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");
|
||||
|
|
@ -347,9 +357,45 @@ 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 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 () => {
|
||||
|
|
@ -476,9 +522,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 () => {
|
||||
|
|
@ -582,6 +628,181 @@ describe("company portability", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("imports a vendor-neutral package without .paperclip.yaml", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
companySvc.create.mockResolvedValue({
|
||||
id: "company-imported",
|
||||
name: "Imported Paperclip",
|
||||
});
|
||||
accessSvc.ensureMembership.mockResolvedValue(undefined);
|
||||
agentSvc.create.mockResolvedValue({
|
||||
id: "agent-created",
|
||||
name: "ClaudeCoder",
|
||||
});
|
||||
|
||||
const preview = await portability.previewImport({
|
||||
source: {
|
||||
type: "inline",
|
||||
rootPath: "paperclip-demo",
|
||||
files: {
|
||||
"COMPANY.md": [
|
||||
"---",
|
||||
'schema: "agentcompanies/v1"',
|
||||
'name: "Imported Paperclip"',
|
||||
'description: "Portable company package"',
|
||||
"---",
|
||||
"",
|
||||
"# Imported Paperclip",
|
||||
"",
|
||||
].join("\n"),
|
||||
"agents/claudecoder/AGENTS.md": [
|
||||
"---",
|
||||
'name: "ClaudeCoder"',
|
||||
'title: "Software Engineer"',
|
||||
"---",
|
||||
"",
|
||||
"# ClaudeCoder",
|
||||
"",
|
||||
"You write code.",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
target: {
|
||||
mode: "new_company",
|
||||
newCompanyName: "Imported Paperclip",
|
||||
},
|
||||
agents: "all",
|
||||
collisionStrategy: "rename",
|
||||
});
|
||||
|
||||
expect(preview.errors).toEqual([]);
|
||||
expect(preview.manifest.company?.name).toBe("Imported Paperclip");
|
||||
expect(preview.manifest.agents).toEqual([
|
||||
expect.objectContaining({
|
||||
slug: "claudecoder",
|
||||
name: "ClaudeCoder",
|
||||
adapterType: "process",
|
||||
}),
|
||||
]);
|
||||
expect(preview.envInputs).toEqual([]);
|
||||
|
||||
await portability.importBundle({
|
||||
source: {
|
||||
type: "inline",
|
||||
rootPath: "paperclip-demo",
|
||||
files: {
|
||||
"COMPANY.md": [
|
||||
"---",
|
||||
'schema: "agentcompanies/v1"',
|
||||
'name: "Imported Paperclip"',
|
||||
'description: "Portable company package"',
|
||||
"---",
|
||||
"",
|
||||
"# Imported Paperclip",
|
||||
"",
|
||||
].join("\n"),
|
||||
"agents/claudecoder/AGENTS.md": [
|
||||
"---",
|
||||
'name: "ClaudeCoder"',
|
||||
'title: "Software Engineer"',
|
||||
"---",
|
||||
"",
|
||||
"# ClaudeCoder",
|
||||
"",
|
||||
"You write code.",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
target: {
|
||||
mode: "new_company",
|
||||
newCompanyName: "Imported Paperclip",
|
||||
},
|
||||
agents: "all",
|
||||
collisionStrategy: "rename",
|
||||
}, "user-1");
|
||||
|
||||
expect(companySvc.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
name: "Imported Paperclip",
|
||||
description: "Portable company package",
|
||||
}));
|
||||
expect(agentSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({
|
||||
name: "ClaudeCoder",
|
||||
adapterType: "process",
|
||||
}));
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
|
|
@ -626,7 +847,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({
|
||||
|
|
@ -772,7 +994,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",
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue