@paperclipai/server 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/codex-models.d.ts +4 -0
- package/dist/adapters/codex-models.d.ts.map +1 -0
- package/dist/adapters/codex-models.js +98 -0
- package/dist/adapters/codex-models.js.map +1 -0
- package/dist/adapters/http/execute.d.ts +3 -0
- package/dist/adapters/http/execute.d.ts.map +1 -0
- package/dist/adapters/http/execute.js +39 -0
- package/dist/adapters/http/execute.js.map +1 -0
- package/dist/adapters/http/index.d.ts +3 -0
- package/dist/adapters/http/index.d.ts.map +1 -0
- package/dist/adapters/http/index.js +20 -0
- package/dist/adapters/http/index.js.map +1 -0
- package/dist/adapters/http/test.d.ts +3 -0
- package/dist/adapters/http/test.d.ts.map +1 -0
- package/dist/adapters/http/test.js +106 -0
- package/dist/adapters/http/test.js.map +1 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/process/execute.d.ts +3 -0
- package/dist/adapters/process/execute.d.ts.map +1 -0
- package/dist/adapters/process/execute.js +63 -0
- package/dist/adapters/process/execute.js.map +1 -0
- package/dist/adapters/process/index.d.ts +3 -0
- package/dist/adapters/process/index.d.ts.map +1 -0
- package/dist/adapters/process/index.js +23 -0
- package/dist/adapters/process/index.js.map +1 -0
- package/dist/adapters/process/test.d.ts +3 -0
- package/dist/adapters/process/test.d.ts.map +1 -0
- package/dist/adapters/process/test.js +77 -0
- package/dist/adapters/process/test.js.map +1 -0
- package/dist/adapters/registry.d.ts +9 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +63 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/adapters/utils.d.ts +10 -0
- package/dist/adapters/utils.d.ts.map +1 -0
- package/dist/adapters/utils.js +14 -0
- package/dist/adapters/utils.js.map +1 -0
- package/dist/agent-auth-jwt.d.ts +14 -0
- package/dist/agent-auth-jwt.d.ts.map +1 -0
- package/dist/agent-auth-jwt.js +117 -0
- package/dist/agent-auth-jwt.js.map +1 -0
- package/dist/app.d.ts +20 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +127 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/better-auth.d.ts +23 -0
- package/dist/auth/better-auth.d.ts.map +1 -0
- package/dist/auth/better-auth.js +80 -0
- package/dist/auth/better-auth.js.map +1 -0
- package/dist/board-claim.d.ts +23 -0
- package/dist/board-claim.d.ts.map +1 -0
- package/dist/board-claim.js +115 -0
- package/dist/board-claim.js.map +1 -0
- package/dist/config-file.d.ts +3 -0
- package/dist/config-file.d.ts.map +1 -0
- package/dist/config-file.js +16 -0
- package/dist/config-file.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +114 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/home-paths.d.ts +11 -0
- package/dist/home-paths.d.ts.map +1 -0
- package/dist/home-paths.js +54 -0
- package/dist/home-paths.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +439 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +12 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +124 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/board-mutation-guard.d.ts +3 -0
- package/dist/middleware/board-mutation-guard.d.ts.map +1 -0
- package/dist/middleware/board-mutation-guard.js +60 -0
- package/dist/middleware/board-mutation-guard.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +3 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +22 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/index.d.ts +4 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +4 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/logger.d.ts +4 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +37 -0
- package/dist/middleware/logger.js.map +1 -0
- package/dist/middleware/private-hostname-guard.d.ts +11 -0
- package/dist/middleware/private-hostname-guard.d.ts.map +1 -0
- package/dist/middleware/private-hostname-guard.js +78 -0
- package/dist/middleware/private-hostname-guard.js.map +1 -0
- package/dist/middleware/validate.d.ts +4 -0
- package/dist/middleware/validate.d.ts.map +1 -0
- package/dist/middleware/validate.js +7 -0
- package/dist/middleware/validate.js.map +1 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +31 -0
- package/dist/paths.js.map +1 -0
- package/dist/realtime/live-events-ws.d.ts +10 -0
- package/dist/realtime/live-events-ws.d.ts.map +1 -0
- package/dist/realtime/live-events-ws.js +185 -0
- package/dist/realtime/live-events-ws.js.map +1 -0
- package/dist/redaction.d.ts +4 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +63 -0
- package/dist/redaction.js.map +1 -0
- package/dist/routes/access.d.ts +9 -0
- package/dist/routes/access.d.ts.map +1 -0
- package/dist/routes/access.js +887 -0
- package/dist/routes/access.js.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +87 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/agents.d.ts +3 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +1132 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/approvals.d.ts +3 -0
- package/dist/routes/approvals.d.ts.map +1 -0
- package/dist/routes/approvals.js +271 -0
- package/dist/routes/approvals.js.map +1 -0
- package/dist/routes/assets.d.ts +4 -0
- package/dist/routes/assets.d.ts.map +1 -0
- package/dist/routes/assets.js +138 -0
- package/dist/routes/assets.js.map +1 -0
- package/dist/routes/authz.d.ts +15 -0
- package/dist/routes/authz.d.ts.map +1 -0
- package/dist/routes/authz.js +40 -0
- package/dist/routes/authz.js.map +1 -0
- package/dist/routes/companies.d.ts +3 -0
- package/dist/routes/companies.d.ts.map +1 -0
- package/dist/routes/companies.js +159 -0
- package/dist/routes/companies.js.map +1 -0
- package/dist/routes/costs.d.ts +3 -0
- package/dist/routes/costs.d.ts.map +1 -0
- package/dist/routes/costs.js +113 -0
- package/dist/routes/costs.js.map +1 -0
- package/dist/routes/dashboard.d.ts +3 -0
- package/dist/routes/dashboard.d.ts.map +1 -0
- package/dist/routes/dashboard.js +15 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/goals.d.ts +3 -0
- package/dist/routes/goals.d.ts.map +1 -0
- package/dist/routes/goals.js +95 -0
- package/dist/routes/goals.js.map +1 -0
- package/dist/routes/health.d.ts +9 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +38 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +15 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +15 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/issues.d.ts +4 -0
- package/dist/routes/issues.d.ts.map +1 -0
- package/dist/routes/issues.js +973 -0
- package/dist/routes/issues.js.map +1 -0
- package/dist/routes/llms.d.ts +3 -0
- package/dist/routes/llms.d.ts.map +1 -0
- package/dist/routes/llms.js +78 -0
- package/dist/routes/llms.js.map +1 -0
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +253 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/secrets.d.ts +3 -0
- package/dist/routes/secrets.d.ts.map +1 -0
- package/dist/routes/secrets.js +128 -0
- package/dist/routes/secrets.js.map +1 -0
- package/dist/routes/sidebar-badges.d.ts +3 -0
- package/dist/routes/sidebar-badges.d.ts.map +1 -0
- package/dist/routes/sidebar-badges.js +47 -0
- package/dist/routes/sidebar-badges.js.map +1 -0
- package/dist/secrets/external-stub-providers.d.ts +5 -0
- package/dist/secrets/external-stub-providers.d.ts.map +1 -0
- package/dist/secrets/external-stub-providers.js +21 -0
- package/dist/secrets/external-stub-providers.js.map +1 -0
- package/dist/secrets/local-encrypted-provider.d.ts +3 -0
- package/dist/secrets/local-encrypted-provider.d.ts.map +1 -0
- package/dist/secrets/local-encrypted-provider.js +116 -0
- package/dist/secrets/local-encrypted-provider.js.map +1 -0
- package/dist/secrets/provider-registry.d.ts +5 -0
- package/dist/secrets/provider-registry.d.ts.map +1 -0
- package/dist/secrets/provider-registry.js +20 -0
- package/dist/secrets/provider-registry.js.map +1 -0
- package/dist/secrets/types.d.ts +21 -0
- package/dist/secrets/types.d.ts.map +1 -0
- package/dist/secrets/types.js +2 -0
- package/dist/secrets/types.js.map +1 -0
- package/dist/services/access.d.ts +81 -0
- package/dist/services/access.d.ts.map +1 -0
- package/dist/services/access.js +187 -0
- package/dist/services/access.js.map +1 -0
- package/dist/services/activity-log.d.ts +14 -0
- package/dist/services/activity-log.d.ts.map +1 -0
- package/dist/services/activity-log.js +32 -0
- package/dist/services/activity-log.js.map +1 -0
- package/dist/services/activity.d.ts +764 -0
- package/dist/services/activity.d.ts.map +1 -0
- package/dist/services/activity.js +105 -0
- package/dist/services/activity.js.map +1 -0
- package/dist/services/agent-permissions.d.ts +6 -0
- package/dist/services/agent-permissions.d.ts.map +1 -0
- package/dist/services/agent-permissions.js +18 -0
- package/dist/services/agent-permissions.js.map +1 -0
- package/dist/services/agents.d.ts +1494 -0
- package/dist/services/agents.d.ts.map +1 -0
- package/dist/services/agents.js +454 -0
- package/dist/services/agents.js.map +1 -0
- package/dist/services/approvals.d.ts +540 -0
- package/dist/services/approvals.d.ts.map +1 -0
- package/dist/services/approvals.js +173 -0
- package/dist/services/approvals.js.map +1 -0
- package/dist/services/assets.d.ts +33 -0
- package/dist/services/assets.d.ts.map +1 -0
- package/dist/services/assets.js +17 -0
- package/dist/services/assets.js.map +1 -0
- package/dist/services/companies.d.ts +503 -0
- package/dist/services/companies.d.ts.map +1 -0
- package/dist/services/companies.js +120 -0
- package/dist/services/companies.js.map +1 -0
- package/dist/services/company-portability.d.ts +8 -0
- package/dist/services/company-portability.d.ts.map +1 -0
- package/dist/services/company-portability.js +851 -0
- package/dist/services/company-portability.js.map +1 -0
- package/dist/services/costs.d.ts +50 -0
- package/dist/services/costs.d.ts.map +1 -0
- package/dist/services/costs.js +166 -0
- package/dist/services/costs.js.map +1 -0
- package/dist/services/dashboard.d.ts +21 -0
- package/dist/services/dashboard.d.ts.map +1 -0
- package/dist/services/dashboard.js +96 -0
- package/dist/services/dashboard.js.map +1 -0
- package/dist/services/goals.d.ts +407 -0
- package/dist/services/goals.d.ts.map +1 -0
- package/dist/services/goals.js +29 -0
- package/dist/services/goals.js.map +1 -0
- package/dist/services/heartbeat.d.ts +1666 -0
- package/dist/services/heartbeat.d.ts.map +1 -0
- package/dist/services/heartbeat.js +1752 -0
- package/dist/services/heartbeat.js.map +1 -0
- package/dist/services/index.d.ts +20 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +20 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/issue-approvals.d.ts +56 -0
- package/dist/services/issue-approvals.d.ts.map +1 -0
- package/dist/services/issue-approvals.js +153 -0
- package/dist/services/issue-approvals.js.map +1 -0
- package/dist/services/issues.d.ts +756 -0
- package/dist/services/issues.d.ts.map +1 -0
- package/dist/services/issues.js +917 -0
- package/dist/services/issues.js.map +1 -0
- package/dist/services/live-events.d.ts +12 -0
- package/dist/services/live-events.d.ts.map +1 -0
- package/dist/services/live-events.js +24 -0
- package/dist/services/live-events.js.map +1 -0
- package/dist/services/projects.d.ts +66 -0
- package/dist/services/projects.d.ts.map +1 -0
- package/dist/services/projects.js +472 -0
- package/dist/services/projects.js.map +1 -0
- package/dist/services/run-log-store.d.ts +34 -0
- package/dist/services/run-log-store.d.ts.map +1 -0
- package/dist/services/run-log-store.js +112 -0
- package/dist/services/run-log-store.js.map +1 -0
- package/dist/services/secrets.d.ts +506 -0
- package/dist/services/secrets.d.ts.map +1 -0
- package/dist/services/secrets.js +284 -0
- package/dist/services/secrets.js.map +1 -0
- package/dist/services/sidebar-badges.d.ts +9 -0
- package/dist/services/sidebar-badges.d.ts.map +1 -0
- package/dist/services/sidebar-badges.js +33 -0
- package/dist/services/sidebar-badges.js.map +1 -0
- package/dist/startup-banner.d.ts +27 -0
- package/dist/startup-banner.d.ts.map +1 -0
- package/dist/startup-banner.js +112 -0
- package/dist/startup-banner.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +29 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/local-disk-provider.d.ts +3 -0
- package/dist/storage/local-disk-provider.d.ts.map +1 -0
- package/dist/storage/local-disk-provider.js +79 -0
- package/dist/storage/local-disk-provider.js.map +1 -0
- package/dist/storage/provider-registry.d.ts +4 -0
- package/dist/storage/provider-registry.d.ts.map +1 -0
- package/dist/storage/provider-registry.js +15 -0
- package/dist/storage/provider-registry.js.map +1 -0
- package/dist/storage/s3-provider.d.ts +11 -0
- package/dist/storage/s3-provider.d.ts.map +1 -0
- package/dist/storage/s3-provider.js +123 -0
- package/dist/storage/s3-provider.js.map +1 -0
- package/dist/storage/service.d.ts +3 -0
- package/dist/storage/service.d.ts.map +1 -0
- package/dist/storage/service.js +120 -0
- package/dist/storage/service.js.map +1 -0
- package/dist/storage/types.d.ts +55 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { normalizeAgentUrlKey, portabilityManifestSchema } from "@paperclipai/shared";
|
|
4
|
+
import { notFound, unprocessable } from "../errors.js";
|
|
5
|
+
import { accessService } from "./access.js";
|
|
6
|
+
import { agentService } from "./agents.js";
|
|
7
|
+
import { companyService } from "./companies.js";
|
|
8
|
+
const DEFAULT_INCLUDE = {
|
|
9
|
+
company: true,
|
|
10
|
+
agents: true,
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_COLLISION_STRATEGY = "rename";
|
|
13
|
+
const SENSITIVE_ENV_KEY_RE = /(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)/i;
|
|
14
|
+
const RUNTIME_DEFAULT_RULES = [
|
|
15
|
+
{ path: ["heartbeat", "cooldownSec"], value: 10 },
|
|
16
|
+
{ path: ["heartbeat", "intervalSec"], value: 3600 },
|
|
17
|
+
{ path: ["heartbeat", "wakeOnOnDemand"], value: true },
|
|
18
|
+
{ path: ["heartbeat", "wakeOnAssignment"], value: true },
|
|
19
|
+
{ path: ["heartbeat", "wakeOnAutomation"], value: true },
|
|
20
|
+
{ path: ["heartbeat", "wakeOnDemand"], value: true },
|
|
21
|
+
{ path: ["heartbeat", "maxConcurrentRuns"], value: 3 },
|
|
22
|
+
];
|
|
23
|
+
const ADAPTER_DEFAULT_RULES_BY_TYPE = {
|
|
24
|
+
codex_local: [
|
|
25
|
+
{ path: ["timeoutSec"], value: 0 },
|
|
26
|
+
{ path: ["graceSec"], value: 15 },
|
|
27
|
+
],
|
|
28
|
+
claude_local: [
|
|
29
|
+
{ path: ["timeoutSec"], value: 0 },
|
|
30
|
+
{ path: ["graceSec"], value: 15 },
|
|
31
|
+
{ path: ["maxTurnsPerRun"], value: 80 },
|
|
32
|
+
],
|
|
33
|
+
openclaw: [
|
|
34
|
+
{ path: ["method"], value: "POST" },
|
|
35
|
+
{ path: ["timeoutSec"], value: 30 },
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
function isPlainRecord(value) {
|
|
39
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
function asString(value) {
|
|
42
|
+
if (typeof value !== "string")
|
|
43
|
+
return null;
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
46
|
+
}
|
|
47
|
+
function toSafeSlug(input, fallback) {
|
|
48
|
+
return normalizeAgentUrlKey(input) ?? fallback;
|
|
49
|
+
}
|
|
50
|
+
function uniqueSlug(base, used) {
|
|
51
|
+
if (!used.has(base)) {
|
|
52
|
+
used.add(base);
|
|
53
|
+
return base;
|
|
54
|
+
}
|
|
55
|
+
let idx = 2;
|
|
56
|
+
while (true) {
|
|
57
|
+
const candidate = `${base}-${idx}`;
|
|
58
|
+
if (!used.has(candidate)) {
|
|
59
|
+
used.add(candidate);
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
idx += 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function uniqueNameBySlug(baseName, existingSlugs) {
|
|
66
|
+
const baseSlug = normalizeAgentUrlKey(baseName) ?? "agent";
|
|
67
|
+
if (!existingSlugs.has(baseSlug))
|
|
68
|
+
return baseName;
|
|
69
|
+
let idx = 2;
|
|
70
|
+
while (true) {
|
|
71
|
+
const candidateName = `${baseName} ${idx}`;
|
|
72
|
+
const candidateSlug = normalizeAgentUrlKey(candidateName) ?? `agent-${idx}`;
|
|
73
|
+
if (!existingSlugs.has(candidateSlug))
|
|
74
|
+
return candidateName;
|
|
75
|
+
idx += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function normalizeInclude(input) {
|
|
79
|
+
return {
|
|
80
|
+
company: input?.company ?? DEFAULT_INCLUDE.company,
|
|
81
|
+
agents: input?.agents ?? DEFAULT_INCLUDE.agents,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function ensureMarkdownPath(pathValue) {
|
|
85
|
+
const normalized = pathValue.replace(/\\/g, "/");
|
|
86
|
+
if (!normalized.endsWith(".md")) {
|
|
87
|
+
throw unprocessable(`Manifest file path must end in .md: ${pathValue}`);
|
|
88
|
+
}
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
function normalizePortableEnv(agentSlug, envValue, requiredSecrets) {
|
|
92
|
+
if (typeof envValue !== "object" || envValue === null || Array.isArray(envValue))
|
|
93
|
+
return {};
|
|
94
|
+
const env = envValue;
|
|
95
|
+
const next = {};
|
|
96
|
+
for (const [key, binding] of Object.entries(env)) {
|
|
97
|
+
if (SENSITIVE_ENV_KEY_RE.test(key)) {
|
|
98
|
+
requiredSecrets.push({
|
|
99
|
+
key,
|
|
100
|
+
description: `Set ${key} for agent ${agentSlug}`,
|
|
101
|
+
agentSlug,
|
|
102
|
+
providerHint: null,
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
next[key] = binding;
|
|
107
|
+
}
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
function normalizePortableConfig(value, agentSlug, requiredSecrets) {
|
|
111
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
112
|
+
return {};
|
|
113
|
+
const input = value;
|
|
114
|
+
const next = {};
|
|
115
|
+
for (const [key, entry] of Object.entries(input)) {
|
|
116
|
+
if (key === "cwd" || key === "instructionsFilePath")
|
|
117
|
+
continue;
|
|
118
|
+
if (key === "env") {
|
|
119
|
+
next[key] = normalizePortableEnv(agentSlug, entry, requiredSecrets);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
next[key] = entry;
|
|
123
|
+
}
|
|
124
|
+
return next;
|
|
125
|
+
}
|
|
126
|
+
function jsonEqual(left, right) {
|
|
127
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
128
|
+
}
|
|
129
|
+
function isPathDefault(pathSegments, value, rules) {
|
|
130
|
+
return rules.some((rule) => jsonEqual(rule.path, pathSegments) && jsonEqual(rule.value, value));
|
|
131
|
+
}
|
|
132
|
+
function pruneDefaultLikeValue(value, opts) {
|
|
133
|
+
const pathSegments = opts.path ?? [];
|
|
134
|
+
if (opts.defaultRules && isPathDefault(pathSegments, value, opts.defaultRules)) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(value)) {
|
|
138
|
+
return value.map((entry) => pruneDefaultLikeValue(entry, { ...opts, path: pathSegments }));
|
|
139
|
+
}
|
|
140
|
+
if (isPlainRecord(value)) {
|
|
141
|
+
const out = {};
|
|
142
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
143
|
+
const next = pruneDefaultLikeValue(entry, {
|
|
144
|
+
...opts,
|
|
145
|
+
path: [...pathSegments, key],
|
|
146
|
+
});
|
|
147
|
+
if (next === undefined)
|
|
148
|
+
continue;
|
|
149
|
+
out[key] = next;
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
if (value === undefined)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (opts.dropFalseBooleans && value === false)
|
|
156
|
+
return undefined;
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
function renderYamlScalar(value) {
|
|
160
|
+
if (value === null)
|
|
161
|
+
return "null";
|
|
162
|
+
if (typeof value === "boolean" || typeof value === "number")
|
|
163
|
+
return String(value);
|
|
164
|
+
if (typeof value === "string")
|
|
165
|
+
return JSON.stringify(value);
|
|
166
|
+
return JSON.stringify(value);
|
|
167
|
+
}
|
|
168
|
+
function isEmptyObject(value) {
|
|
169
|
+
return isPlainRecord(value) && Object.keys(value).length === 0;
|
|
170
|
+
}
|
|
171
|
+
function renderYamlBlock(value, indentLevel) {
|
|
172
|
+
const indent = " ".repeat(indentLevel);
|
|
173
|
+
if (Array.isArray(value)) {
|
|
174
|
+
if (value.length === 0)
|
|
175
|
+
return [`${indent}[]`];
|
|
176
|
+
const lines = [];
|
|
177
|
+
for (const entry of value) {
|
|
178
|
+
const scalar = entry === null ||
|
|
179
|
+
typeof entry === "string" ||
|
|
180
|
+
typeof entry === "boolean" ||
|
|
181
|
+
typeof entry === "number" ||
|
|
182
|
+
Array.isArray(entry) && entry.length === 0 ||
|
|
183
|
+
isEmptyObject(entry);
|
|
184
|
+
if (scalar) {
|
|
185
|
+
lines.push(`${indent}- ${renderYamlScalar(entry)}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
lines.push(`${indent}-`);
|
|
189
|
+
lines.push(...renderYamlBlock(entry, indentLevel + 1));
|
|
190
|
+
}
|
|
191
|
+
return lines;
|
|
192
|
+
}
|
|
193
|
+
if (isPlainRecord(value)) {
|
|
194
|
+
const entries = Object.entries(value);
|
|
195
|
+
if (entries.length === 0)
|
|
196
|
+
return [`${indent}{}`];
|
|
197
|
+
const lines = [];
|
|
198
|
+
for (const [key, entry] of entries) {
|
|
199
|
+
const scalar = entry === null ||
|
|
200
|
+
typeof entry === "string" ||
|
|
201
|
+
typeof entry === "boolean" ||
|
|
202
|
+
typeof entry === "number" ||
|
|
203
|
+
Array.isArray(entry) && entry.length === 0 ||
|
|
204
|
+
isEmptyObject(entry);
|
|
205
|
+
if (scalar) {
|
|
206
|
+
lines.push(`${indent}${key}: ${renderYamlScalar(entry)}`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
lines.push(`${indent}${key}:`);
|
|
210
|
+
lines.push(...renderYamlBlock(entry, indentLevel + 1));
|
|
211
|
+
}
|
|
212
|
+
return lines;
|
|
213
|
+
}
|
|
214
|
+
return [`${indent}${renderYamlScalar(value)}`];
|
|
215
|
+
}
|
|
216
|
+
function renderFrontmatter(frontmatter) {
|
|
217
|
+
const lines = ["---"];
|
|
218
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
219
|
+
const scalar = value === null ||
|
|
220
|
+
typeof value === "string" ||
|
|
221
|
+
typeof value === "boolean" ||
|
|
222
|
+
typeof value === "number" ||
|
|
223
|
+
Array.isArray(value) && value.length === 0 ||
|
|
224
|
+
isEmptyObject(value);
|
|
225
|
+
if (scalar) {
|
|
226
|
+
lines.push(`${key}: ${renderYamlScalar(value)}`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
lines.push(`${key}:`);
|
|
230
|
+
lines.push(...renderYamlBlock(value, 1));
|
|
231
|
+
}
|
|
232
|
+
lines.push("---");
|
|
233
|
+
return `${lines.join("\n")}\n`;
|
|
234
|
+
}
|
|
235
|
+
function buildMarkdown(frontmatter, body) {
|
|
236
|
+
const cleanBody = body.replace(/\r\n/g, "\n").trim();
|
|
237
|
+
if (!cleanBody) {
|
|
238
|
+
return `${renderFrontmatter(frontmatter)}\n`;
|
|
239
|
+
}
|
|
240
|
+
return `${renderFrontmatter(frontmatter)}\n${cleanBody}\n`;
|
|
241
|
+
}
|
|
242
|
+
function renderCompanyAgentsSection(agentSummaries) {
|
|
243
|
+
const lines = ["# Agents", ""];
|
|
244
|
+
if (agentSummaries.length === 0) {
|
|
245
|
+
lines.push("- _none_");
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
for (const agent of agentSummaries) {
|
|
249
|
+
lines.push(`- ${agent.slug} - ${agent.name}`);
|
|
250
|
+
}
|
|
251
|
+
return lines.join("\n");
|
|
252
|
+
}
|
|
253
|
+
function parseFrontmatterMarkdown(raw) {
|
|
254
|
+
const normalized = raw.replace(/\r\n/g, "\n");
|
|
255
|
+
if (!normalized.startsWith("---\n")) {
|
|
256
|
+
return { frontmatter: {}, body: normalized.trim() };
|
|
257
|
+
}
|
|
258
|
+
const closing = normalized.indexOf("\n---\n", 4);
|
|
259
|
+
if (closing < 0) {
|
|
260
|
+
return { frontmatter: {}, body: normalized.trim() };
|
|
261
|
+
}
|
|
262
|
+
const frontmatterRaw = normalized.slice(4, closing).trim();
|
|
263
|
+
const body = normalized.slice(closing + 5).trim();
|
|
264
|
+
const frontmatter = {};
|
|
265
|
+
for (const line of frontmatterRaw.split("\n")) {
|
|
266
|
+
const idx = line.indexOf(":");
|
|
267
|
+
if (idx <= 0)
|
|
268
|
+
continue;
|
|
269
|
+
const key = line.slice(0, idx).trim();
|
|
270
|
+
const rawValue = line.slice(idx + 1).trim();
|
|
271
|
+
if (!key)
|
|
272
|
+
continue;
|
|
273
|
+
if (rawValue === "null") {
|
|
274
|
+
frontmatter[key] = null;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (rawValue === "true" || rawValue === "false") {
|
|
278
|
+
frontmatter[key] = rawValue === "true";
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (/^-?\d+(\.\d+)?$/.test(rawValue)) {
|
|
282
|
+
frontmatter[key] = Number(rawValue);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
frontmatter[key] = JSON.parse(rawValue);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
frontmatter[key] = rawValue;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return { frontmatter, body };
|
|
294
|
+
}
|
|
295
|
+
async function fetchJson(url) {
|
|
296
|
+
const response = await fetch(url);
|
|
297
|
+
if (!response.ok) {
|
|
298
|
+
throw unprocessable(`Failed to fetch ${url}: ${response.status}`);
|
|
299
|
+
}
|
|
300
|
+
return response.json();
|
|
301
|
+
}
|
|
302
|
+
async function fetchText(url) {
|
|
303
|
+
const response = await fetch(url);
|
|
304
|
+
if (!response.ok) {
|
|
305
|
+
throw unprocessable(`Failed to fetch ${url}: ${response.status}`);
|
|
306
|
+
}
|
|
307
|
+
return response.text();
|
|
308
|
+
}
|
|
309
|
+
function dedupeRequiredSecrets(values) {
|
|
310
|
+
const seen = new Set();
|
|
311
|
+
const out = [];
|
|
312
|
+
for (const value of values) {
|
|
313
|
+
const key = `${value.agentSlug ?? ""}:${value.key.toUpperCase()}`;
|
|
314
|
+
if (seen.has(key))
|
|
315
|
+
continue;
|
|
316
|
+
seen.add(key);
|
|
317
|
+
out.push(value);
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
function parseGitHubTreeUrl(rawUrl) {
|
|
322
|
+
const url = new URL(rawUrl);
|
|
323
|
+
if (url.hostname !== "github.com") {
|
|
324
|
+
throw unprocessable("GitHub source must use github.com URL");
|
|
325
|
+
}
|
|
326
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
327
|
+
if (parts.length < 2) {
|
|
328
|
+
throw unprocessable("Invalid GitHub URL");
|
|
329
|
+
}
|
|
330
|
+
const owner = parts[0];
|
|
331
|
+
const repo = parts[1].replace(/\.git$/i, "");
|
|
332
|
+
let ref = "main";
|
|
333
|
+
let basePath = "";
|
|
334
|
+
if (parts[2] === "tree") {
|
|
335
|
+
ref = parts[3] ?? "main";
|
|
336
|
+
basePath = parts.slice(4).join("/");
|
|
337
|
+
}
|
|
338
|
+
return { owner, repo, ref, basePath };
|
|
339
|
+
}
|
|
340
|
+
function resolveRawGitHubUrl(owner, repo, ref, filePath) {
|
|
341
|
+
const normalizedFilePath = filePath.replace(/^\/+/, "");
|
|
342
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${normalizedFilePath}`;
|
|
343
|
+
}
|
|
344
|
+
async function readAgentInstructions(agent) {
|
|
345
|
+
const config = agent.adapterConfig;
|
|
346
|
+
const instructionsFilePath = asString(config.instructionsFilePath);
|
|
347
|
+
if (instructionsFilePath) {
|
|
348
|
+
const workspaceCwd = asString(process.env.PAPERCLIP_WORKSPACE_CWD);
|
|
349
|
+
const candidates = new Set();
|
|
350
|
+
if (path.isAbsolute(instructionsFilePath)) {
|
|
351
|
+
candidates.add(instructionsFilePath);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
if (workspaceCwd)
|
|
355
|
+
candidates.add(path.resolve(workspaceCwd, instructionsFilePath));
|
|
356
|
+
candidates.add(path.resolve(process.cwd(), instructionsFilePath));
|
|
357
|
+
}
|
|
358
|
+
for (const candidate of candidates) {
|
|
359
|
+
try {
|
|
360
|
+
const stat = await fs.stat(candidate);
|
|
361
|
+
if (!stat.isFile() || stat.size > 1024 * 1024)
|
|
362
|
+
continue;
|
|
363
|
+
const body = await Promise.race([
|
|
364
|
+
fs.readFile(candidate, "utf8"),
|
|
365
|
+
new Promise((_, reject) => {
|
|
366
|
+
setTimeout(() => reject(new Error("timed out reading instructions file")), 1500);
|
|
367
|
+
}),
|
|
368
|
+
]);
|
|
369
|
+
return { body, warning: null };
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
// try next candidate
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const promptTemplate = asString(config.promptTemplate);
|
|
377
|
+
if (promptTemplate) {
|
|
378
|
+
const warning = instructionsFilePath
|
|
379
|
+
? `Agent ${agent.name} instructionsFilePath was not readable; fell back to promptTemplate.`
|
|
380
|
+
: null;
|
|
381
|
+
return {
|
|
382
|
+
body: promptTemplate,
|
|
383
|
+
warning,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
body: "_No AGENTS instructions were resolved from current agent config._",
|
|
388
|
+
warning: `Agent ${agent.name} has no resolvable instructionsFilePath/promptTemplate; exported placeholder AGENTS.md.`,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
export function companyPortabilityService(db) {
|
|
392
|
+
const companies = companyService(db);
|
|
393
|
+
const agents = agentService(db);
|
|
394
|
+
const access = accessService(db);
|
|
395
|
+
async function resolveSource(source) {
|
|
396
|
+
if (source.type === "inline") {
|
|
397
|
+
return {
|
|
398
|
+
manifest: portabilityManifestSchema.parse(source.manifest),
|
|
399
|
+
files: source.files,
|
|
400
|
+
warnings: [],
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
if (source.type === "url") {
|
|
404
|
+
const manifestJson = await fetchJson(source.url);
|
|
405
|
+
const manifest = portabilityManifestSchema.parse(manifestJson);
|
|
406
|
+
const base = new URL(".", source.url);
|
|
407
|
+
const files = {};
|
|
408
|
+
const warnings = [];
|
|
409
|
+
if (manifest.company?.path) {
|
|
410
|
+
const companyPath = ensureMarkdownPath(manifest.company.path);
|
|
411
|
+
files[companyPath] = await fetchText(new URL(companyPath, base).toString());
|
|
412
|
+
}
|
|
413
|
+
for (const agent of manifest.agents) {
|
|
414
|
+
const filePath = ensureMarkdownPath(agent.path);
|
|
415
|
+
files[filePath] = await fetchText(new URL(filePath, base).toString());
|
|
416
|
+
}
|
|
417
|
+
return { manifest, files, warnings };
|
|
418
|
+
}
|
|
419
|
+
const parsed = parseGitHubTreeUrl(source.url);
|
|
420
|
+
let ref = parsed.ref;
|
|
421
|
+
const manifestRelativePath = [parsed.basePath, "paperclip.manifest.json"].filter(Boolean).join("/");
|
|
422
|
+
let manifest = null;
|
|
423
|
+
const warnings = [];
|
|
424
|
+
try {
|
|
425
|
+
manifest = portabilityManifestSchema.parse(await fetchJson(resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, manifestRelativePath)));
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
if (ref === "main") {
|
|
429
|
+
ref = "master";
|
|
430
|
+
warnings.push("GitHub ref main not found; falling back to master.");
|
|
431
|
+
manifest = portabilityManifestSchema.parse(await fetchJson(resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, manifestRelativePath)));
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
throw err;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const files = {};
|
|
438
|
+
if (manifest.company?.path) {
|
|
439
|
+
files[manifest.company.path] = await fetchText(resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, [parsed.basePath, manifest.company.path].filter(Boolean).join("/")));
|
|
440
|
+
}
|
|
441
|
+
for (const agent of manifest.agents) {
|
|
442
|
+
files[agent.path] = await fetchText(resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, [parsed.basePath, agent.path].filter(Boolean).join("/")));
|
|
443
|
+
}
|
|
444
|
+
return { manifest, files, warnings };
|
|
445
|
+
}
|
|
446
|
+
async function exportBundle(companyId, input) {
|
|
447
|
+
const include = normalizeInclude(input.include);
|
|
448
|
+
const company = await companies.getById(companyId);
|
|
449
|
+
if (!company)
|
|
450
|
+
throw notFound("Company not found");
|
|
451
|
+
const files = {};
|
|
452
|
+
const warnings = [];
|
|
453
|
+
const requiredSecrets = [];
|
|
454
|
+
const generatedAt = new Date().toISOString();
|
|
455
|
+
const manifest = {
|
|
456
|
+
schemaVersion: 1,
|
|
457
|
+
generatedAt,
|
|
458
|
+
source: {
|
|
459
|
+
companyId: company.id,
|
|
460
|
+
companyName: company.name,
|
|
461
|
+
},
|
|
462
|
+
includes: include,
|
|
463
|
+
company: null,
|
|
464
|
+
agents: [],
|
|
465
|
+
requiredSecrets: [],
|
|
466
|
+
};
|
|
467
|
+
const allAgentRows = include.agents ? await agents.list(companyId) : [];
|
|
468
|
+
const agentRows = allAgentRows.filter((agent) => agent.status !== "terminated");
|
|
469
|
+
if (include.agents) {
|
|
470
|
+
const skipped = allAgentRows.length - agentRows.length;
|
|
471
|
+
if (skipped > 0) {
|
|
472
|
+
warnings.push(`Skipped ${skipped} terminated agent${skipped === 1 ? "" : "s"} from export.`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const usedSlugs = new Set();
|
|
476
|
+
const idToSlug = new Map();
|
|
477
|
+
for (const agent of agentRows) {
|
|
478
|
+
const baseSlug = toSafeSlug(agent.name, "agent");
|
|
479
|
+
const slug = uniqueSlug(baseSlug, usedSlugs);
|
|
480
|
+
idToSlug.set(agent.id, slug);
|
|
481
|
+
}
|
|
482
|
+
if (include.company) {
|
|
483
|
+
const companyPath = "COMPANY.md";
|
|
484
|
+
const companyAgentSummaries = agentRows.map((agent) => ({
|
|
485
|
+
slug: idToSlug.get(agent.id) ?? "agent",
|
|
486
|
+
name: agent.name,
|
|
487
|
+
}));
|
|
488
|
+
files[companyPath] = buildMarkdown({
|
|
489
|
+
kind: "company",
|
|
490
|
+
name: company.name,
|
|
491
|
+
description: company.description ?? null,
|
|
492
|
+
brandColor: company.brandColor ?? null,
|
|
493
|
+
requireBoardApprovalForNewAgents: company.requireBoardApprovalForNewAgents,
|
|
494
|
+
}, renderCompanyAgentsSection(companyAgentSummaries));
|
|
495
|
+
manifest.company = {
|
|
496
|
+
path: companyPath,
|
|
497
|
+
name: company.name,
|
|
498
|
+
description: company.description ?? null,
|
|
499
|
+
brandColor: company.brandColor ?? null,
|
|
500
|
+
requireBoardApprovalForNewAgents: company.requireBoardApprovalForNewAgents,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
if (include.agents) {
|
|
504
|
+
for (const agent of agentRows) {
|
|
505
|
+
const slug = idToSlug.get(agent.id);
|
|
506
|
+
const instructions = await readAgentInstructions(agent);
|
|
507
|
+
if (instructions.warning)
|
|
508
|
+
warnings.push(instructions.warning);
|
|
509
|
+
const agentPath = `agents/${slug}/AGENTS.md`;
|
|
510
|
+
const secretStart = requiredSecrets.length;
|
|
511
|
+
const adapterDefaultRules = ADAPTER_DEFAULT_RULES_BY_TYPE[agent.adapterType] ?? [];
|
|
512
|
+
const portableAdapterConfig = pruneDefaultLikeValue(normalizePortableConfig(agent.adapterConfig, slug, requiredSecrets), {
|
|
513
|
+
dropFalseBooleans: true,
|
|
514
|
+
defaultRules: adapterDefaultRules,
|
|
515
|
+
});
|
|
516
|
+
const portableRuntimeConfig = pruneDefaultLikeValue(normalizePortableConfig(agent.runtimeConfig, slug, requiredSecrets), {
|
|
517
|
+
dropFalseBooleans: true,
|
|
518
|
+
defaultRules: RUNTIME_DEFAULT_RULES,
|
|
519
|
+
});
|
|
520
|
+
const portablePermissions = pruneDefaultLikeValue(agent.permissions ?? {}, { dropFalseBooleans: true });
|
|
521
|
+
const agentRequiredSecrets = dedupeRequiredSecrets(requiredSecrets
|
|
522
|
+
.slice(secretStart)
|
|
523
|
+
.filter((requirement) => requirement.agentSlug === slug));
|
|
524
|
+
const reportsToSlug = agent.reportsTo ? (idToSlug.get(agent.reportsTo) ?? null) : null;
|
|
525
|
+
files[agentPath] = buildMarkdown({
|
|
526
|
+
name: agent.name,
|
|
527
|
+
slug,
|
|
528
|
+
role: agent.role,
|
|
529
|
+
adapterType: agent.adapterType,
|
|
530
|
+
kind: "agent",
|
|
531
|
+
icon: agent.icon ?? null,
|
|
532
|
+
capabilities: agent.capabilities ?? null,
|
|
533
|
+
reportsTo: reportsToSlug,
|
|
534
|
+
runtimeConfig: portableRuntimeConfig,
|
|
535
|
+
permissions: portablePermissions,
|
|
536
|
+
adapterConfig: portableAdapterConfig,
|
|
537
|
+
requiredSecrets: agentRequiredSecrets,
|
|
538
|
+
}, instructions.body);
|
|
539
|
+
manifest.agents.push({
|
|
540
|
+
slug,
|
|
541
|
+
name: agent.name,
|
|
542
|
+
path: agentPath,
|
|
543
|
+
role: agent.role,
|
|
544
|
+
title: agent.title ?? null,
|
|
545
|
+
icon: agent.icon ?? null,
|
|
546
|
+
capabilities: agent.capabilities ?? null,
|
|
547
|
+
reportsToSlug,
|
|
548
|
+
adapterType: agent.adapterType,
|
|
549
|
+
adapterConfig: portableAdapterConfig,
|
|
550
|
+
runtimeConfig: portableRuntimeConfig,
|
|
551
|
+
permissions: portablePermissions,
|
|
552
|
+
budgetMonthlyCents: agent.budgetMonthlyCents ?? 0,
|
|
553
|
+
metadata: agent.metadata ?? null,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
manifest.requiredSecrets = dedupeRequiredSecrets(requiredSecrets);
|
|
558
|
+
return {
|
|
559
|
+
manifest,
|
|
560
|
+
files,
|
|
561
|
+
warnings,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
async function buildPreview(input) {
|
|
565
|
+
const include = normalizeInclude(input.include);
|
|
566
|
+
const source = await resolveSource(input.source);
|
|
567
|
+
const manifest = source.manifest;
|
|
568
|
+
const collisionStrategy = input.collisionStrategy ?? DEFAULT_COLLISION_STRATEGY;
|
|
569
|
+
const warnings = [...source.warnings];
|
|
570
|
+
const errors = [];
|
|
571
|
+
if (include.company && !manifest.company) {
|
|
572
|
+
errors.push("Manifest does not include company metadata.");
|
|
573
|
+
}
|
|
574
|
+
const selectedSlugs = input.agents && input.agents !== "all"
|
|
575
|
+
? Array.from(new Set(input.agents))
|
|
576
|
+
: manifest.agents.map((agent) => agent.slug);
|
|
577
|
+
const selectedAgents = manifest.agents.filter((agent) => selectedSlugs.includes(agent.slug));
|
|
578
|
+
const selectedMissing = selectedSlugs.filter((slug) => !manifest.agents.some((agent) => agent.slug === slug));
|
|
579
|
+
for (const missing of selectedMissing) {
|
|
580
|
+
errors.push(`Selected agent slug not found in manifest: ${missing}`);
|
|
581
|
+
}
|
|
582
|
+
if (include.agents && selectedAgents.length === 0) {
|
|
583
|
+
warnings.push("No agents selected for import.");
|
|
584
|
+
}
|
|
585
|
+
for (const agent of selectedAgents) {
|
|
586
|
+
const filePath = ensureMarkdownPath(agent.path);
|
|
587
|
+
const markdown = source.files[filePath];
|
|
588
|
+
if (typeof markdown !== "string") {
|
|
589
|
+
errors.push(`Missing markdown file for agent ${agent.slug}: ${filePath}`);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const parsed = parseFrontmatterMarkdown(markdown);
|
|
593
|
+
if (parsed.frontmatter.kind !== "agent") {
|
|
594
|
+
warnings.push(`Agent markdown ${filePath} does not declare kind: agent in frontmatter.`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
let targetCompanyId = null;
|
|
598
|
+
let targetCompanyName = null;
|
|
599
|
+
if (input.target.mode === "existing_company") {
|
|
600
|
+
const targetCompany = await companies.getById(input.target.companyId);
|
|
601
|
+
if (!targetCompany)
|
|
602
|
+
throw notFound("Target company not found");
|
|
603
|
+
targetCompanyId = targetCompany.id;
|
|
604
|
+
targetCompanyName = targetCompany.name;
|
|
605
|
+
}
|
|
606
|
+
const agentPlans = [];
|
|
607
|
+
const existingSlugToAgent = new Map();
|
|
608
|
+
const existingSlugs = new Set();
|
|
609
|
+
if (input.target.mode === "existing_company") {
|
|
610
|
+
const existingAgents = await agents.list(input.target.companyId);
|
|
611
|
+
for (const existing of existingAgents) {
|
|
612
|
+
const slug = normalizeAgentUrlKey(existing.name) ?? existing.id;
|
|
613
|
+
if (!existingSlugToAgent.has(slug))
|
|
614
|
+
existingSlugToAgent.set(slug, existing);
|
|
615
|
+
existingSlugs.add(slug);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
for (const manifestAgent of selectedAgents) {
|
|
619
|
+
const existing = existingSlugToAgent.get(manifestAgent.slug) ?? null;
|
|
620
|
+
if (!existing) {
|
|
621
|
+
agentPlans.push({
|
|
622
|
+
slug: manifestAgent.slug,
|
|
623
|
+
action: "create",
|
|
624
|
+
plannedName: manifestAgent.name,
|
|
625
|
+
existingAgentId: null,
|
|
626
|
+
reason: null,
|
|
627
|
+
});
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (collisionStrategy === "replace") {
|
|
631
|
+
agentPlans.push({
|
|
632
|
+
slug: manifestAgent.slug,
|
|
633
|
+
action: "update",
|
|
634
|
+
plannedName: existing.name,
|
|
635
|
+
existingAgentId: existing.id,
|
|
636
|
+
reason: "Existing slug matched; replace strategy.",
|
|
637
|
+
});
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (collisionStrategy === "skip") {
|
|
641
|
+
agentPlans.push({
|
|
642
|
+
slug: manifestAgent.slug,
|
|
643
|
+
action: "skip",
|
|
644
|
+
plannedName: existing.name,
|
|
645
|
+
existingAgentId: existing.id,
|
|
646
|
+
reason: "Existing slug matched; skip strategy.",
|
|
647
|
+
});
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
const renamed = uniqueNameBySlug(manifestAgent.name, existingSlugs);
|
|
651
|
+
existingSlugs.add(normalizeAgentUrlKey(renamed) ?? manifestAgent.slug);
|
|
652
|
+
agentPlans.push({
|
|
653
|
+
slug: manifestAgent.slug,
|
|
654
|
+
action: "create",
|
|
655
|
+
plannedName: renamed,
|
|
656
|
+
existingAgentId: existing.id,
|
|
657
|
+
reason: "Existing slug matched; rename strategy.",
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
const preview = {
|
|
661
|
+
include,
|
|
662
|
+
targetCompanyId,
|
|
663
|
+
targetCompanyName,
|
|
664
|
+
collisionStrategy,
|
|
665
|
+
selectedAgentSlugs: selectedAgents.map((agent) => agent.slug),
|
|
666
|
+
plan: {
|
|
667
|
+
companyAction: input.target.mode === "new_company"
|
|
668
|
+
? "create"
|
|
669
|
+
: include.company
|
|
670
|
+
? "update"
|
|
671
|
+
: "none",
|
|
672
|
+
agentPlans,
|
|
673
|
+
},
|
|
674
|
+
requiredSecrets: manifest.requiredSecrets ?? [],
|
|
675
|
+
warnings,
|
|
676
|
+
errors,
|
|
677
|
+
};
|
|
678
|
+
return {
|
|
679
|
+
preview,
|
|
680
|
+
source,
|
|
681
|
+
include,
|
|
682
|
+
collisionStrategy,
|
|
683
|
+
selectedAgents,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
async function previewImport(input) {
|
|
687
|
+
const plan = await buildPreview(input);
|
|
688
|
+
return plan.preview;
|
|
689
|
+
}
|
|
690
|
+
async function importBundle(input, actorUserId) {
|
|
691
|
+
const plan = await buildPreview(input);
|
|
692
|
+
if (plan.preview.errors.length > 0) {
|
|
693
|
+
throw unprocessable(`Import preview has errors: ${plan.preview.errors.join("; ")}`);
|
|
694
|
+
}
|
|
695
|
+
const sourceManifest = plan.source.manifest;
|
|
696
|
+
const warnings = [...plan.preview.warnings];
|
|
697
|
+
const include = plan.include;
|
|
698
|
+
let targetCompany = null;
|
|
699
|
+
let companyAction = "unchanged";
|
|
700
|
+
if (input.target.mode === "new_company") {
|
|
701
|
+
const companyName = asString(input.target.newCompanyName) ??
|
|
702
|
+
sourceManifest.company?.name ??
|
|
703
|
+
sourceManifest.source?.companyName ??
|
|
704
|
+
"Imported Company";
|
|
705
|
+
const created = await companies.create({
|
|
706
|
+
name: companyName,
|
|
707
|
+
description: include.company ? (sourceManifest.company?.description ?? null) : null,
|
|
708
|
+
brandColor: include.company ? (sourceManifest.company?.brandColor ?? null) : null,
|
|
709
|
+
requireBoardApprovalForNewAgents: include.company
|
|
710
|
+
? (sourceManifest.company?.requireBoardApprovalForNewAgents ?? true)
|
|
711
|
+
: true,
|
|
712
|
+
});
|
|
713
|
+
await access.ensureMembership(created.id, "user", actorUserId ?? "board", "owner", "active");
|
|
714
|
+
targetCompany = created;
|
|
715
|
+
companyAction = "created";
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
targetCompany = await companies.getById(input.target.companyId);
|
|
719
|
+
if (!targetCompany)
|
|
720
|
+
throw notFound("Target company not found");
|
|
721
|
+
if (include.company && sourceManifest.company) {
|
|
722
|
+
const updated = await companies.update(targetCompany.id, {
|
|
723
|
+
name: sourceManifest.company.name,
|
|
724
|
+
description: sourceManifest.company.description,
|
|
725
|
+
brandColor: sourceManifest.company.brandColor,
|
|
726
|
+
requireBoardApprovalForNewAgents: sourceManifest.company.requireBoardApprovalForNewAgents,
|
|
727
|
+
});
|
|
728
|
+
targetCompany = updated ?? targetCompany;
|
|
729
|
+
companyAction = "updated";
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (!targetCompany)
|
|
733
|
+
throw notFound("Target company not found");
|
|
734
|
+
const resultAgents = [];
|
|
735
|
+
const importedSlugToAgentId = new Map();
|
|
736
|
+
const existingSlugToAgentId = new Map();
|
|
737
|
+
const existingAgents = await agents.list(targetCompany.id);
|
|
738
|
+
for (const existing of existingAgents) {
|
|
739
|
+
existingSlugToAgentId.set(normalizeAgentUrlKey(existing.name) ?? existing.id, existing.id);
|
|
740
|
+
}
|
|
741
|
+
if (include.agents) {
|
|
742
|
+
for (const planAgent of plan.preview.plan.agentPlans) {
|
|
743
|
+
const manifestAgent = plan.selectedAgents.find((agent) => agent.slug === planAgent.slug);
|
|
744
|
+
if (!manifestAgent)
|
|
745
|
+
continue;
|
|
746
|
+
if (planAgent.action === "skip") {
|
|
747
|
+
resultAgents.push({
|
|
748
|
+
slug: planAgent.slug,
|
|
749
|
+
id: planAgent.existingAgentId,
|
|
750
|
+
action: "skipped",
|
|
751
|
+
name: planAgent.plannedName,
|
|
752
|
+
reason: planAgent.reason,
|
|
753
|
+
});
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const markdownRaw = plan.source.files[manifestAgent.path];
|
|
757
|
+
if (!markdownRaw) {
|
|
758
|
+
warnings.push(`Missing AGENTS markdown for ${manifestAgent.slug}; imported without prompt template.`);
|
|
759
|
+
}
|
|
760
|
+
const markdown = markdownRaw ? parseFrontmatterMarkdown(markdownRaw) : { frontmatter: {}, body: "" };
|
|
761
|
+
const adapterConfig = {
|
|
762
|
+
...manifestAgent.adapterConfig,
|
|
763
|
+
promptTemplate: markdown.body || asString(manifestAgent.adapterConfig.promptTemplate) || "",
|
|
764
|
+
};
|
|
765
|
+
delete adapterConfig.instructionsFilePath;
|
|
766
|
+
const patch = {
|
|
767
|
+
name: planAgent.plannedName,
|
|
768
|
+
role: manifestAgent.role,
|
|
769
|
+
title: manifestAgent.title,
|
|
770
|
+
icon: manifestAgent.icon,
|
|
771
|
+
capabilities: manifestAgent.capabilities,
|
|
772
|
+
reportsTo: null,
|
|
773
|
+
adapterType: manifestAgent.adapterType,
|
|
774
|
+
adapterConfig,
|
|
775
|
+
runtimeConfig: manifestAgent.runtimeConfig,
|
|
776
|
+
budgetMonthlyCents: manifestAgent.budgetMonthlyCents,
|
|
777
|
+
permissions: manifestAgent.permissions,
|
|
778
|
+
metadata: manifestAgent.metadata,
|
|
779
|
+
};
|
|
780
|
+
if (planAgent.action === "update" && planAgent.existingAgentId) {
|
|
781
|
+
const updated = await agents.update(planAgent.existingAgentId, patch);
|
|
782
|
+
if (!updated) {
|
|
783
|
+
warnings.push(`Skipped update for missing agent ${planAgent.existingAgentId}.`);
|
|
784
|
+
resultAgents.push({
|
|
785
|
+
slug: planAgent.slug,
|
|
786
|
+
id: null,
|
|
787
|
+
action: "skipped",
|
|
788
|
+
name: planAgent.plannedName,
|
|
789
|
+
reason: "Existing target agent not found.",
|
|
790
|
+
});
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
importedSlugToAgentId.set(planAgent.slug, updated.id);
|
|
794
|
+
existingSlugToAgentId.set(normalizeAgentUrlKey(updated.name) ?? updated.id, updated.id);
|
|
795
|
+
resultAgents.push({
|
|
796
|
+
slug: planAgent.slug,
|
|
797
|
+
id: updated.id,
|
|
798
|
+
action: "updated",
|
|
799
|
+
name: updated.name,
|
|
800
|
+
reason: planAgent.reason,
|
|
801
|
+
});
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const created = await agents.create(targetCompany.id, patch);
|
|
805
|
+
importedSlugToAgentId.set(planAgent.slug, created.id);
|
|
806
|
+
existingSlugToAgentId.set(normalizeAgentUrlKey(created.name) ?? created.id, created.id);
|
|
807
|
+
resultAgents.push({
|
|
808
|
+
slug: planAgent.slug,
|
|
809
|
+
id: created.id,
|
|
810
|
+
action: "created",
|
|
811
|
+
name: created.name,
|
|
812
|
+
reason: planAgent.reason,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
// Apply reporting links once all imported agent ids are available.
|
|
816
|
+
for (const manifestAgent of plan.selectedAgents) {
|
|
817
|
+
const agentId = importedSlugToAgentId.get(manifestAgent.slug);
|
|
818
|
+
if (!agentId)
|
|
819
|
+
continue;
|
|
820
|
+
const managerSlug = manifestAgent.reportsToSlug;
|
|
821
|
+
if (!managerSlug)
|
|
822
|
+
continue;
|
|
823
|
+
const managerId = importedSlugToAgentId.get(managerSlug) ?? existingSlugToAgentId.get(managerSlug) ?? null;
|
|
824
|
+
if (!managerId || managerId === agentId)
|
|
825
|
+
continue;
|
|
826
|
+
try {
|
|
827
|
+
await agents.update(agentId, { reportsTo: managerId });
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
warnings.push(`Could not assign manager ${managerSlug} for imported agent ${manifestAgent.slug}.`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
company: {
|
|
836
|
+
id: targetCompany.id,
|
|
837
|
+
name: targetCompany.name,
|
|
838
|
+
action: companyAction,
|
|
839
|
+
},
|
|
840
|
+
agents: resultAgents,
|
|
841
|
+
requiredSecrets: sourceManifest.requiredSecrets ?? [],
|
|
842
|
+
warnings,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
exportBundle,
|
|
847
|
+
previewImport,
|
|
848
|
+
importBundle,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
//# sourceMappingURL=company-portability.js.map
|