bopodev-api 0.1.34 → 0.1.36

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.
Files changed (95) hide show
  1. package/package.json +5 -5
  2. package/src/app.ts +4 -2
  3. package/src/assets/starter-packs/customer-support-excellence.zip +0 -0
  4. package/src/assets/starter-packs/devrel-growth.zip +0 -0
  5. package/src/assets/starter-packs/product-delivery-trio.zip +0 -0
  6. package/src/assets/starter-packs/revenue-gtm-b2b.zip +0 -0
  7. package/src/assets/starter-packs/sources/customer-support-excellence/.bopo.yaml +129 -0
  8. package/src/assets/starter-packs/sources/customer-support-excellence/COMPANY.md +7 -0
  9. package/src/assets/starter-packs/sources/customer-support-excellence/README.md +3 -0
  10. package/src/assets/starter-packs/sources/customer-support-excellence/agents/founder-ceo/HEARTBEAT.md +5 -0
  11. package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-lead/HEARTBEAT.md +5 -0
  12. package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-specialist/HEARTBEAT.md +5 -0
  13. package/src/assets/starter-packs/sources/customer-support-excellence/projects/knowledge-base/PROJECT.md +7 -0
  14. package/src/assets/starter-packs/sources/customer-support-excellence/projects/quality/PROJECT.md +7 -0
  15. package/src/assets/starter-packs/sources/customer-support-excellence/projects/queue/PROJECT.md +7 -0
  16. package/src/assets/starter-packs/sources/customer-support-excellence/skills/kb-article-skeleton/SKILL.md +17 -0
  17. package/src/assets/starter-packs/sources/customer-support-excellence/skills/ticket-response-playbook/SKILL.md +15 -0
  18. package/src/assets/starter-packs/sources/customer-support-excellence/tasks/daily-queue-standup/TASK.md +11 -0
  19. package/src/assets/starter-packs/sources/customer-support-excellence/tasks/kb-gap-sweep/TASK.md +11 -0
  20. package/src/assets/starter-packs/sources/devrel-growth/.bopo.yaml +128 -0
  21. package/src/assets/starter-packs/sources/devrel-growth/COMPANY.md +7 -0
  22. package/src/assets/starter-packs/sources/devrel-growth/README.md +3 -0
  23. package/src/assets/starter-packs/sources/devrel-growth/agents/content-producer/HEARTBEAT.md +5 -0
  24. package/src/assets/starter-packs/sources/devrel-growth/agents/devrel-lead/HEARTBEAT.md +5 -0
  25. package/src/assets/starter-packs/sources/devrel-growth/agents/founder-ceo/HEARTBEAT.md +5 -0
  26. package/src/assets/starter-packs/sources/devrel-growth/projects/community/PROJECT.md +7 -0
  27. package/src/assets/starter-packs/sources/devrel-growth/projects/docs-education/PROJECT.md +7 -0
  28. package/src/assets/starter-packs/sources/devrel-growth/projects/partners/PROJECT.md +7 -0
  29. package/src/assets/starter-packs/sources/devrel-growth/skills/changelog-to-post/SKILL.md +14 -0
  30. package/src/assets/starter-packs/sources/devrel-growth/skills/tutorial-outline/SKILL.md +15 -0
  31. package/src/assets/starter-packs/sources/devrel-growth/tasks/community-health-review/TASK.md +11 -0
  32. package/src/assets/starter-packs/sources/devrel-growth/tasks/weekly-content-plan/TASK.md +11 -0
  33. package/src/assets/starter-packs/sources/product-delivery-trio/.bopo.yaml +138 -0
  34. package/src/assets/starter-packs/sources/product-delivery-trio/COMPANY.md +7 -0
  35. package/src/assets/starter-packs/sources/product-delivery-trio/README.md +9 -0
  36. package/src/assets/starter-packs/sources/product-delivery-trio/agents/engineer-ic/HEARTBEAT.md +5 -0
  37. package/src/assets/starter-packs/sources/product-delivery-trio/agents/founder-ceo/HEARTBEAT.md +6 -0
  38. package/src/assets/starter-packs/sources/product-delivery-trio/agents/product-lead/HEARTBEAT.md +5 -0
  39. package/src/assets/starter-packs/sources/product-delivery-trio/projects/delivery/PROJECT.md +7 -0
  40. package/src/assets/starter-packs/sources/product-delivery-trio/projects/quality/PROJECT.md +7 -0
  41. package/src/assets/starter-packs/sources/product-delivery-trio/projects/strategy/PROJECT.md +7 -0
  42. package/src/assets/starter-packs/sources/product-delivery-trio/skills/issue-triage/SKILL.md +21 -0
  43. package/src/assets/starter-packs/sources/product-delivery-trio/skills/rca-template/SKILL.md +16 -0
  44. package/src/assets/starter-packs/sources/product-delivery-trio/tasks/release-hygiene/TASK.md +11 -0
  45. package/src/assets/starter-packs/sources/product-delivery-trio/tasks/weekly-leadership-sync/TASK.md +11 -0
  46. package/src/assets/starter-packs/sources/revenue-gtm-b2b/.bopo.yaml +132 -0
  47. package/src/assets/starter-packs/sources/revenue-gtm-b2b/COMPANY.md +7 -0
  48. package/src/assets/starter-packs/sources/revenue-gtm-b2b/README.md +3 -0
  49. package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/founder-ceo/HEARTBEAT.md +5 -0
  50. package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/gtm-lead/HEARTBEAT.md +5 -0
  51. package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/pipeline-owner/HEARTBEAT.md +5 -0
  52. package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/customer-success/PROJECT.md +7 -0
  53. package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/deals/PROJECT.md +7 -0
  54. package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/pipeline/PROJECT.md +7 -0
  55. package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/discovery-call-brief/SKILL.md +14 -0
  56. package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/icp-scoring/SKILL.md +20 -0
  57. package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/pipeline-hygiene/TASK.md +11 -0
  58. package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/weekly-revenue-review/TASK.md +11 -0
  59. package/src/lib/agent-issue-permissions.ts +56 -0
  60. package/src/lib/builtin-bopo-skills/bopodev-control-plane.md +7 -0
  61. package/src/lib/instance-paths.ts +5 -0
  62. package/src/realtime/office-space.ts +7 -0
  63. package/src/routes/agents.ts +23 -1
  64. package/src/routes/assistant.ts +40 -1
  65. package/src/routes/companies.ts +227 -15
  66. package/src/routes/issues.ts +82 -3
  67. package/src/routes/observability.ts +222 -0
  68. package/src/routes/plugins.ts +393 -103
  69. package/src/routes/{loops.ts → routines.ts} +72 -76
  70. package/src/scripts/onboard-seed.ts +2 -0
  71. package/src/server.ts +3 -1
  72. package/src/services/company-assistant-context-snapshot.ts +4 -2
  73. package/src/services/company-assistant-service.ts +17 -15
  74. package/src/services/company-file-archive-service.ts +81 -6
  75. package/src/services/company-file-import-service.ts +221 -31
  76. package/src/services/company-knowledge-file-service.ts +361 -0
  77. package/src/services/company-skill-file-service.ts +151 -2
  78. package/src/services/governance-service.ts +58 -3
  79. package/src/services/heartbeat-service/heartbeat-run.ts +7 -0
  80. package/src/services/plugin-artifact-installer.ts +115 -0
  81. package/src/services/plugin-artifact-store.ts +28 -0
  82. package/src/services/plugin-capability-policy.ts +31 -0
  83. package/src/services/plugin-jobs-service.ts +74 -0
  84. package/src/services/plugin-manifest-loader.ts +78 -3
  85. package/src/services/plugin-rpc.ts +102 -0
  86. package/src/services/plugin-runtime.ts +240 -209
  87. package/src/services/plugin-worker-host.ts +167 -0
  88. package/src/services/starter-pack-registry.ts +68 -0
  89. package/src/services/template-apply-service.ts +3 -1
  90. package/src/services/template-catalog.ts +29 -0
  91. package/src/services/work-loop-service/work-loop-service.ts +18 -18
  92. package/src/shutdown/graceful-shutdown.ts +3 -1
  93. package/src/validation/issue-routes.ts +19 -2
  94. package/src/worker/scheduler.ts +21 -1
  95. package/src/services/company-export-service.ts +0 -63
@@ -723,6 +723,8 @@ export async function runHeartbeatForAgent(
723
723
  agentId: agent.id,
724
724
  heartbeatRunId: runId,
725
725
  canHireAgents: agent.canHireAgents,
726
+ canAssignAgents: agent.canAssignAgents ?? true,
727
+ canCreateIssues: agent.canCreateIssues ?? true,
726
728
  wakeContext: options?.wakeContext
727
729
  }),
728
730
  ...(linkedMaterialized.root ? { BOPODEV_MATERIALIZED_LINKED_SKILLS_ROOT: linkedMaterialized.root } : {})
@@ -4077,6 +4079,8 @@ function buildHeartbeatRuntimeEnv(input: {
4077
4079
  agentId: string;
4078
4080
  heartbeatRunId: string;
4079
4081
  canHireAgents: boolean;
4082
+ canAssignAgents: boolean;
4083
+ canCreateIssues: boolean;
4080
4084
  wakeContext?: HeartbeatWakeContext;
4081
4085
  }) {
4082
4086
  const companyWorkspaceRoot = resolveCompanyWorkspaceRootPath(input.companyId);
@@ -4085,6 +4089,7 @@ function buildHeartbeatRuntimeEnv(input: {
4085
4089
  const apiBaseUrl = resolveControlPlaneApiBaseUrl();
4086
4090
  // agents:write is required for PUT /agents/:self (bootstrapPrompt, runtimeConfig). Route handlers
4087
4091
  // still forbid agents from updating other agents' rows and from POST /agents unless canHireAgents.
4092
+ // Issue create/assignee changes for agent actors are gated by canCreateIssues / canAssignAgents (see issues routes).
4088
4093
  const actorPermissions = ["issues:write", "agents:write"].join(",");
4089
4094
  const actorHeaders = JSON.stringify({
4090
4095
  "x-company-id": input.companyId,
@@ -4115,6 +4120,8 @@ function buildHeartbeatRuntimeEnv(input: {
4115
4120
  BOPODEV_REQUEST_HEADERS_JSON: actorHeaders,
4116
4121
  BOPODEV_REQUEST_APPROVAL_DEFAULT: "true",
4117
4122
  BOPODEV_CAN_HIRE_AGENTS: input.canHireAgents ? "true" : "false",
4123
+ BOPODEV_CAN_ASSIGN_AGENTS: input.canAssignAgents ? "true" : "false",
4124
+ BOPODEV_CAN_CREATE_ISSUES: input.canCreateIssues ? "true" : "false",
4118
4125
  ...(input.wakeContext?.reason ? { BOPODEV_WAKE_REASON: input.wakeContext.reason } : {}),
4119
4126
  ...(input.wakeContext?.commentId ? { BOPODEV_WAKE_COMMENT_ID: input.wakeContext.commentId } : {}),
4120
4127
  ...(input.wakeContext?.issueIds?.length ? { BOPODEV_LINKED_ISSUE_IDS: input.wakeContext.issueIds.join(",") } : {}),
@@ -0,0 +1,115 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdtemp, mkdir, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { resolve } from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import { PluginManifestV2Schema, type PluginManifestV2 } from "bopodev-contracts";
8
+ import { buildPluginArtifactInstallDir, ensurePluginArtifactsDir, sanitizeArtifactSegment } from "./plugin-artifact-store";
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ type NpmPackResultRow = {
13
+ filename: string;
14
+ integrity?: string;
15
+ };
16
+
17
+ export async function installPluginArtifactFromNpm(input: { packageName: string; version?: string }) {
18
+ const packageRef = input.version?.trim() ? `${input.packageName}@${input.version.trim()}` : input.packageName.trim();
19
+ if (!packageRef) {
20
+ throw new Error("packageName is required.");
21
+ }
22
+ const artifactsRoot = await ensurePluginArtifactsDir();
23
+ const tempRoot = await mkdtemp(resolve(tmpdir(), "bopo-plugin-pack-"));
24
+ try {
25
+ const { stdout } = await execFileAsync("npm", ["pack", packageRef, "--json"], {
26
+ cwd: tempRoot,
27
+ maxBuffer: 5 * 1024 * 1024
28
+ });
29
+ const parsed = JSON.parse(stdout.trim()) as NpmPackResultRow[] | NpmPackResultRow;
30
+ const row = Array.isArray(parsed) ? parsed[0] : parsed;
31
+ if (!row?.filename) {
32
+ throw new Error("npm pack did not return a filename.");
33
+ }
34
+ const tarballPath = resolve(tempRoot, row.filename);
35
+ const tarball = await readFile(tarballPath);
36
+ const buildHash = createHash("sha256").update(tarball).digest("hex");
37
+ const installDir = buildPluginArtifactInstallDir({
38
+ pluginId: sanitizeArtifactSegment(input.packageName),
39
+ version: input.version?.trim() || "latest",
40
+ buildHash
41
+ });
42
+ await mkdir(installDir, { recursive: true });
43
+ await execFileAsync("tar", ["-xzf", tarballPath, "-C", installDir], {
44
+ maxBuffer: 5 * 1024 * 1024
45
+ });
46
+ const packageRoot = resolve(installDir, "package");
47
+ const packageJsonRaw = await readFile(resolve(packageRoot, "package.json"), "utf8");
48
+ const packageJsonParsed = JSON.parse(packageJsonRaw) as Record<string, unknown>;
49
+ const manifestPath = resolveManifestPath(packageRoot, packageJsonParsed);
50
+ const manifestRaw = await readFile(manifestPath, "utf8");
51
+ const manifestParsed = PluginManifestV2Schema.parse(JSON.parse(manifestRaw) as unknown);
52
+ const normalizedManifest = normalizeManifestEntrypoints(packageRoot, manifestParsed, {
53
+ packageName: input.packageName.trim(),
54
+ packageRef,
55
+ integrity: row.integrity,
56
+ buildHash,
57
+ artifactPath: packageRoot
58
+ });
59
+ return {
60
+ manifest: normalizedManifest,
61
+ packageRoot,
62
+ packageRef,
63
+ buildHash,
64
+ integrity: row.integrity
65
+ };
66
+ } finally {
67
+ await rm(tempRoot, { recursive: true, force: true });
68
+ await mkdir(artifactsRoot, { recursive: true });
69
+ }
70
+ }
71
+
72
+ function resolveManifestPath(packageRoot: string, packageJson: Record<string, unknown>) {
73
+ const bopo = packageJson.bopo;
74
+ if (typeof bopo === "object" && bopo !== null) {
75
+ const maybe = (bopo as Record<string, unknown>).pluginManifest;
76
+ if (typeof maybe === "string" && maybe.trim()) {
77
+ return resolve(packageRoot, maybe);
78
+ }
79
+ }
80
+ const legacy = packageJson.bopoPluginManifest;
81
+ if (typeof legacy === "string" && legacy.trim()) {
82
+ return resolve(packageRoot, legacy);
83
+ }
84
+ return resolve(packageRoot, "plugin.json");
85
+ }
86
+
87
+ function normalizeManifestEntrypoints(
88
+ packageRoot: string,
89
+ manifest: PluginManifestV2,
90
+ input: {
91
+ packageName: string;
92
+ packageRef: string;
93
+ integrity?: string;
94
+ buildHash: string;
95
+ artifactPath: string;
96
+ }
97
+ ): PluginManifestV2 {
98
+ return {
99
+ ...manifest,
100
+ apiVersion: "2",
101
+ entrypoints: {
102
+ worker: resolve(packageRoot, manifest.entrypoints.worker),
103
+ ui: manifest.entrypoints.ui ? resolve(packageRoot, manifest.entrypoints.ui) : undefined
104
+ },
105
+ install: {
106
+ sourceType: "registry",
107
+ sourceRef: input.packageRef,
108
+ integrity: input.integrity,
109
+ buildHash: input.buildHash || randomUUID(),
110
+ installedAt: new Date().toISOString(),
111
+ artifactPath: input.artifactPath,
112
+ packageName: input.packageName
113
+ }
114
+ };
115
+ }
@@ -0,0 +1,28 @@
1
+ import { mkdir, rm } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+
4
+ export function resolvePluginArtifactsDir() {
5
+ return process.env.BOPO_PLUGIN_ARTIFACTS_DIR || resolve(process.cwd(), ".bopo", "plugin-artifacts");
6
+ }
7
+
8
+ export async function ensurePluginArtifactsDir() {
9
+ const dir = resolvePluginArtifactsDir();
10
+ await mkdir(dir, { recursive: true });
11
+ return dir;
12
+ }
13
+
14
+ export function sanitizeArtifactSegment(value: string) {
15
+ return value.replace(/[^a-zA-Z0-9._-]/g, "-");
16
+ }
17
+
18
+ export function buildPluginArtifactInstallDir(input: { pluginId: string; version: string; buildHash: string }) {
19
+ const root = resolvePluginArtifactsDir();
20
+ const plugin = sanitizeArtifactSegment(input.pluginId);
21
+ const version = sanitizeArtifactSegment(input.version);
22
+ const hash = sanitizeArtifactSegment(input.buildHash.slice(0, 16));
23
+ return resolve(root, plugin, `${version}-${hash}`);
24
+ }
25
+
26
+ export async function removePluginArtifactInstallDir(path: string) {
27
+ await rm(path, { recursive: true, force: true });
28
+ }
@@ -0,0 +1,31 @@
1
+ import { PLUGIN_CAPABILITY_RISK, type PluginCapabilityNamespace } from "bopodev-contracts";
2
+
3
+ const LEGACY_HIGH_RISK = new Set(["network", "queue_publish", "issue_write", "write_memory"]);
4
+
5
+ export type PluginTrustLevel = "dev_local" | "verified" | "restricted";
6
+
7
+ function resolveTrustLevel(): PluginTrustLevel {
8
+ const raw = process.env.BOPO_PLUGIN_TRUST_LEVEL;
9
+ if (raw === "dev_local" || raw === "verified" || raw === "restricted") {
10
+ return raw;
11
+ }
12
+ return "verified";
13
+ }
14
+
15
+ export function legacyCapabilitiesRequireApproval(capabilities: string[]) {
16
+ return capabilities.some((cap) => LEGACY_HIGH_RISK.has(cap));
17
+ }
18
+
19
+ export function namespacedCapabilitiesRequireApproval(capabilities: PluginCapabilityNamespace[]) {
20
+ const trustLevel = resolveTrustLevel();
21
+ return capabilities.some((cap) => {
22
+ const risk = PLUGIN_CAPABILITY_RISK[cap];
23
+ if (risk === "restricted") {
24
+ return true;
25
+ }
26
+ if (risk === "elevated") {
27
+ return trustLevel !== "dev_local";
28
+ }
29
+ return false;
30
+ });
31
+ }
@@ -0,0 +1,74 @@
1
+ import type { PluginManifestV2 } from "bopodev-contracts";
2
+ import { PluginInvocationResultSchema, PluginManifestV2Schema } from "bopodev-contracts";
3
+ import type { BopoDb } from "bopodev-db";
4
+ import { appendPluginRun, listCompanyPluginConfigs } from "bopodev-db";
5
+ import { pluginWorkerHost } from "./plugin-worker-host";
6
+
7
+ const lastJobRun = new Map<string, number>();
8
+
9
+ function shouldRunJob(companyId: string, pluginId: string, jobKey: string) {
10
+ const key = `${companyId}:${pluginId}:${jobKey}`;
11
+ const now = Date.now();
12
+ const previous = lastJobRun.get(key) ?? 0;
13
+ if (now - previous < 55_000) {
14
+ return false;
15
+ }
16
+ lastJobRun.set(key, now);
17
+ return true;
18
+ }
19
+
20
+ export async function runPluginJobSweep(db: BopoDb, companyId: string) {
21
+ const rows = await listCompanyPluginConfigs(db, companyId);
22
+ for (const row of rows) {
23
+ if (!row.enabled) continue;
24
+ const manifest = parseManifest(row.manifestJson);
25
+ if (!manifest || manifest.jobs.length === 0) continue;
26
+ for (const job of manifest.jobs) {
27
+ if (!shouldRunJob(companyId, row.pluginId, job.jobKey)) continue;
28
+ const startedAt = Date.now();
29
+ try {
30
+ const result = await pluginWorkerHost.invoke(manifest, {
31
+ method: "plugin.job",
32
+ params: {
33
+ companyId,
34
+ pluginId: row.pluginId,
35
+ jobKey: job.jobKey,
36
+ schedule: job.schedule
37
+ }
38
+ });
39
+ const validated = PluginInvocationResultSchema.parse(result);
40
+ await appendPluginRun(db, {
41
+ companyId,
42
+ runId: null,
43
+ pluginId: row.pluginId,
44
+ hook: `job:${job.jobKey}`,
45
+ status: validated.status,
46
+ durationMs: Date.now() - startedAt,
47
+ diagnosticsJson: JSON.stringify(validated.diagnostics ?? {}),
48
+ error: validated.status === "failed" || validated.status === "blocked" ? validated.summary : null
49
+ });
50
+ } catch (error) {
51
+ await appendPluginRun(db, {
52
+ companyId,
53
+ runId: null,
54
+ pluginId: row.pluginId,
55
+ hook: `job:${job.jobKey}`,
56
+ status: "failed",
57
+ durationMs: Date.now() - startedAt,
58
+ error: String(error)
59
+ });
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ function parseManifest(value: string | null | undefined): PluginManifestV2 | null {
66
+ if (!value) return null;
67
+ try {
68
+ const parsed = JSON.parse(value) as unknown;
69
+ const result = PluginManifestV2Schema.safeParse(parsed);
70
+ return result.success ? result.data : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { existsSync, readdirSync } from "node:fs";
2
3
  import { resolve } from "node:path";
3
- import { PluginManifestSchema, type PluginManifest } from "bopodev-contracts";
4
+ import { PluginManifestSchema, PluginManifestV2Schema, type PluginManifest } from "bopodev-contracts";
4
5
 
5
6
  export type FilesystemPluginManifestLoadResult = {
6
7
  manifests: PluginManifest[];
@@ -29,7 +30,7 @@ export async function loadFilesystemPluginManifests(): Promise<FilesystemPluginM
29
30
  }
30
31
  try {
31
32
  const parsed = JSON.parse(raw) as unknown;
32
- const manifest = PluginManifestSchema.parse(parsed);
33
+ const manifest = normalizeFilesystemManifest(manifestPath, PluginManifestSchema.parse(parsed));
33
34
  manifests.push(manifest);
34
35
  } catch (error) {
35
36
  warnings.push(`Invalid plugin manifest at '${manifestPath}': ${String(error)}`);
@@ -40,7 +41,24 @@ export async function loadFilesystemPluginManifests(): Promise<FilesystemPluginM
40
41
  }
41
42
 
42
43
  export function resolvePluginManifestsDir() {
43
- return process.env.BOPO_PLUGIN_MANIFESTS_DIR || resolve(process.cwd(), "plugins");
44
+ if (process.env.BOPO_PLUGIN_MANIFESTS_DIR) {
45
+ return process.env.BOPO_PLUGIN_MANIFESTS_DIR;
46
+ }
47
+ const localPlugins = resolve(process.cwd(), "plugins");
48
+ if (directoryHasPluginManifests(localPlugins)) {
49
+ return localPlugins;
50
+ }
51
+ const repoRootPlugins = resolve(process.cwd(), "..", "..", "plugins");
52
+ if (directoryHasPluginManifests(repoRootPlugins)) {
53
+ return repoRootPlugins;
54
+ }
55
+ if (existsSync(localPlugins)) {
56
+ return localPlugins;
57
+ }
58
+ if (existsSync(repoRootPlugins)) {
59
+ return repoRootPlugins;
60
+ }
61
+ return localPlugins;
44
62
  }
45
63
 
46
64
  export async function writePluginManifestToFilesystem(manifest: PluginManifest) {
@@ -53,6 +71,29 @@ export async function writePluginManifestToFilesystem(manifest: PluginManifest)
53
71
  return manifestPath;
54
72
  }
55
73
 
74
+ export async function writePackagedPluginManifestToFilesystem(
75
+ manifest: PluginManifest,
76
+ input: {
77
+ sourceType: "builtin" | "registry" | "local_path" | "archive_url";
78
+ sourceRef?: string;
79
+ integrity?: string;
80
+ buildHash?: string;
81
+ }
82
+ ) {
83
+ const nextManifest = {
84
+ ...manifest,
85
+ apiVersion: "2",
86
+ install: {
87
+ sourceType: input.sourceType,
88
+ sourceRef: input.sourceRef,
89
+ integrity: input.integrity,
90
+ buildHash: input.buildHash,
91
+ installedAt: new Date().toISOString()
92
+ }
93
+ } as PluginManifest;
94
+ return writePluginManifestToFilesystem(nextManifest);
95
+ }
96
+
56
97
  export async function deletePluginManifestFromFilesystem(pluginId: string) {
57
98
  const pluginRoot = resolvePluginManifestsDir();
58
99
  const safeDirName = sanitizePluginDirectoryName(pluginId);
@@ -63,3 +104,37 @@ export async function deletePluginManifestFromFilesystem(pluginId: string) {
63
104
  function sanitizePluginDirectoryName(pluginId: string) {
64
105
  return pluginId.replace(/[^a-zA-Z0-9._-]/g, "-");
65
106
  }
107
+
108
+ function directoryHasPluginManifests(dir: string) {
109
+ if (!existsSync(dir)) {
110
+ return false;
111
+ }
112
+ try {
113
+ const entries = readdirSync(dir, { withFileTypes: true });
114
+ return entries.some((entry) => entry.isDirectory() && existsSync(resolve(dir, entry.name, "plugin.json")));
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ function normalizeFilesystemManifest(manifestPath: string, manifest: PluginManifest): PluginManifest {
121
+ const pluginDir = resolve(manifestPath, "..");
122
+ const parsedV2 = PluginManifestV2Schema.safeParse(manifest);
123
+ if (!parsedV2.success) {
124
+ return manifest;
125
+ }
126
+ const v2 = parsedV2.data;
127
+ const worker = resolve(pluginDir, v2.entrypoints.worker);
128
+ const ui = v2.entrypoints.ui ? resolve(pluginDir, v2.entrypoints.ui) : undefined;
129
+ return {
130
+ ...v2,
131
+ runtime: {
132
+ ...v2.runtime,
133
+ entrypoint: v2.runtime.type === "stdio" || v2.runtime.type === "http" ? resolve(pluginDir, v2.runtime.entrypoint) : v2.runtime.entrypoint
134
+ },
135
+ entrypoints: {
136
+ worker,
137
+ ui
138
+ }
139
+ };
140
+ }
@@ -0,0 +1,102 @@
1
+ import { EventEmitter } from "node:events";
2
+
3
+ export type PluginRpcRequest = {
4
+ jsonrpc: "2.0";
5
+ id: string;
6
+ method: string;
7
+ params?: unknown;
8
+ };
9
+
10
+ export type PluginRpcSuccess = {
11
+ jsonrpc: "2.0";
12
+ id: string;
13
+ result: unknown;
14
+ };
15
+
16
+ export type PluginRpcFailure = {
17
+ jsonrpc: "2.0";
18
+ id: string;
19
+ error: {
20
+ code: number;
21
+ message: string;
22
+ data?: unknown;
23
+ };
24
+ };
25
+
26
+ export type PluginRpcResponse = PluginRpcSuccess | PluginRpcFailure;
27
+
28
+ export function createPluginRpcRequest(method: string, params: unknown, id: string): PluginRpcRequest {
29
+ return {
30
+ jsonrpc: "2.0",
31
+ id,
32
+ method,
33
+ params
34
+ };
35
+ }
36
+
37
+ export function encodePluginRpcMessage(value: PluginRpcRequest | PluginRpcResponse): string {
38
+ return `${JSON.stringify(value)}\n`;
39
+ }
40
+
41
+ export function decodePluginRpcMessage(line: string): PluginRpcResponse | null {
42
+ let parsed: unknown;
43
+ try {
44
+ parsed = JSON.parse(line) as unknown;
45
+ } catch {
46
+ return null;
47
+ }
48
+ if (!parsed || typeof parsed !== "object") {
49
+ return null;
50
+ }
51
+ const obj = parsed as Record<string, unknown>;
52
+ if (obj.jsonrpc !== "2.0" || typeof obj.id !== "string") {
53
+ return null;
54
+ }
55
+ if ("result" in obj) {
56
+ return {
57
+ jsonrpc: "2.0",
58
+ id: obj.id,
59
+ result: obj.result
60
+ };
61
+ }
62
+ if ("error" in obj && obj.error && typeof obj.error === "object") {
63
+ const err = obj.error as Record<string, unknown>;
64
+ if (typeof err.code === "number" && typeof err.message === "string") {
65
+ return {
66
+ jsonrpc: "2.0",
67
+ id: obj.id,
68
+ error: {
69
+ code: err.code,
70
+ message: err.message,
71
+ data: err.data
72
+ }
73
+ };
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ export class PluginRpcLineBuffer extends EventEmitter<{
80
+ message: [PluginRpcResponse];
81
+ }> {
82
+ private buffer = "";
83
+
84
+ push(chunk: string) {
85
+ this.buffer += chunk;
86
+ while (true) {
87
+ const idx = this.buffer.indexOf("\n");
88
+ if (idx === -1) {
89
+ break;
90
+ }
91
+ const line = this.buffer.slice(0, idx).trim();
92
+ this.buffer = this.buffer.slice(idx + 1);
93
+ if (!line) {
94
+ continue;
95
+ }
96
+ const parsed = decodePluginRpcMessage(line);
97
+ if (parsed) {
98
+ this.emit("message", parsed);
99
+ }
100
+ }
101
+ }
102
+ }