bopodev-api 0.1.34 → 0.1.35

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 (90) 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/realtime/office-space.ts +7 -0
  62. package/src/routes/agents.ts +23 -1
  63. package/src/routes/assistant.ts +40 -1
  64. package/src/routes/companies.ts +227 -15
  65. package/src/routes/issues.ts +48 -0
  66. package/src/routes/plugins.ts +393 -103
  67. package/src/routes/{loops.ts → routines.ts} +72 -76
  68. package/src/scripts/onboard-seed.ts +2 -0
  69. package/src/server.ts +3 -1
  70. package/src/services/company-assistant-context-snapshot.ts +4 -2
  71. package/src/services/company-assistant-service.ts +17 -15
  72. package/src/services/company-file-archive-service.ts +56 -3
  73. package/src/services/company-file-import-service.ts +210 -31
  74. package/src/services/governance-service.ts +58 -3
  75. package/src/services/heartbeat-service/heartbeat-run.ts +7 -0
  76. package/src/services/plugin-artifact-installer.ts +115 -0
  77. package/src/services/plugin-artifact-store.ts +28 -0
  78. package/src/services/plugin-capability-policy.ts +31 -0
  79. package/src/services/plugin-jobs-service.ts +74 -0
  80. package/src/services/plugin-manifest-loader.ts +78 -3
  81. package/src/services/plugin-rpc.ts +102 -0
  82. package/src/services/plugin-runtime.ts +240 -209
  83. package/src/services/plugin-worker-host.ts +167 -0
  84. package/src/services/starter-pack-registry.ts +68 -0
  85. package/src/services/template-apply-service.ts +3 -1
  86. package/src/services/template-catalog.ts +29 -0
  87. package/src/services/work-loop-service/work-loop-service.ts +18 -18
  88. package/src/shutdown/graceful-shutdown.ts +3 -1
  89. package/src/worker/scheduler.ts +21 -1
  90. package/src/services/company-export-service.ts +0 -63
@@ -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
+ }