mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-19 04:00: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
|
|
@ -42,16 +42,63 @@ 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<string, string> = {
|
||||
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<string | null, typeof agents>();
|
||||
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,
|
||||
projects: false,
|
||||
issues: false,
|
||||
skills: false,
|
||||
};
|
||||
|
||||
const DEFAULT_COLLISION_STRATEGY: CompanyPortabilityCollisionStrategy = "rename";
|
||||
|
|
@ -119,7 +166,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 +293,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);
|
||||
|
|
@ -304,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") ||
|
||||
|
|
@ -317,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") ||
|
||||
|
|
@ -515,6 +565,7 @@ function normalizeInclude(input?: Partial<CompanyPortabilityInclude>): 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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -826,6 +877,7 @@ function extractPortableEnvInputs(
|
|||
|
||||
if (isPlainRecord(binding) && binding.type === "plain") {
|
||||
const defaultValue = asString(binding.value);
|
||||
const isSensitive = isSensitiveEnvKey(key);
|
||||
const portability = defaultValue && isAbsoluteCommand(defaultValue)
|
||||
? "system_dependent"
|
||||
: "portable";
|
||||
|
|
@ -836,9 +888,9 @@ function extractPortableEnvInputs(
|
|||
key,
|
||||
description: `Optional default for ${key} on agent ${agentSlug}`,
|
||||
agentSlug,
|
||||
kind: "plain",
|
||||
kind: isSensitive ? "secret" : "plain",
|
||||
requirement: "optional",
|
||||
defaultValue: defaultValue ?? "",
|
||||
defaultValue: isSensitive ? "" : defaultValue ?? "",
|
||||
portability,
|
||||
});
|
||||
continue;
|
||||
|
|
@ -1147,6 +1199,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;
|
||||
|
|
@ -1178,7 +1231,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 +1260,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) {
|
||||
|
|
@ -1254,17 +1307,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();
|
||||
|
|
@ -1610,6 +1652,7 @@ function buildManifestFromPackageFiles(
|
|||
agents: true,
|
||||
projects: projectPaths.length > 0,
|
||||
issues: taskPaths.length > 0,
|
||||
skills: skillPaths.length > 0,
|
||||
},
|
||||
company: {
|
||||
path: resolvedCompanyPath,
|
||||
|
|
@ -2005,6 +2048,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");
|
||||
|
|
@ -2017,7 +2061,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) {
|
||||
|
|
@ -2159,19 +2203,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,
|
||||
|
|
@ -2179,7 +2210,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
|||
schema: "agentcompanies/v1",
|
||||
slug: rootPath,
|
||||
},
|
||||
companyBodySections.join("\n\n").trim(),
|
||||
"",
|
||||
);
|
||||
|
||||
if (include.company && company.logoAssetId) {
|
||||
|
|
@ -2418,10 +2449,22 @@ 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);
|
||||
|
||||
// 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,
|
||||
|
|
@ -2440,6 +2483,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);
|
||||
|
|
@ -2502,6 +2546,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") {
|
||||
|
|
@ -2962,9 +3007,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<string, string>();
|
||||
for (const importedSkill of importedSkills) {
|
||||
desiredSkillRefMap.set(importedSkill.originalKey, importedSkill.skill.key);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue