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.
- package/package.json +5 -5
- package/src/app.ts +4 -2
- package/src/assets/starter-packs/customer-support-excellence.zip +0 -0
- package/src/assets/starter-packs/devrel-growth.zip +0 -0
- package/src/assets/starter-packs/product-delivery-trio.zip +0 -0
- package/src/assets/starter-packs/revenue-gtm-b2b.zip +0 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/.bopo.yaml +129 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/README.md +3 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/agents/founder-ceo/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/agents/support-specialist/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/projects/knowledge-base/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/projects/quality/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/projects/queue/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/skills/kb-article-skeleton/SKILL.md +17 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/skills/ticket-response-playbook/SKILL.md +15 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/tasks/daily-queue-standup/TASK.md +11 -0
- package/src/assets/starter-packs/sources/customer-support-excellence/tasks/kb-gap-sweep/TASK.md +11 -0
- package/src/assets/starter-packs/sources/devrel-growth/.bopo.yaml +128 -0
- package/src/assets/starter-packs/sources/devrel-growth/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/README.md +3 -0
- package/src/assets/starter-packs/sources/devrel-growth/agents/content-producer/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/devrel-growth/agents/devrel-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/devrel-growth/agents/founder-ceo/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/devrel-growth/projects/community/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/projects/docs-education/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/projects/partners/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/devrel-growth/skills/changelog-to-post/SKILL.md +14 -0
- package/src/assets/starter-packs/sources/devrel-growth/skills/tutorial-outline/SKILL.md +15 -0
- package/src/assets/starter-packs/sources/devrel-growth/tasks/community-health-review/TASK.md +11 -0
- package/src/assets/starter-packs/sources/devrel-growth/tasks/weekly-content-plan/TASK.md +11 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/.bopo.yaml +138 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/README.md +9 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/agents/engineer-ic/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/agents/founder-ceo/HEARTBEAT.md +6 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/agents/product-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/projects/delivery/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/projects/quality/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/projects/strategy/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/skills/issue-triage/SKILL.md +21 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/skills/rca-template/SKILL.md +16 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/tasks/release-hygiene/TASK.md +11 -0
- package/src/assets/starter-packs/sources/product-delivery-trio/tasks/weekly-leadership-sync/TASK.md +11 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/.bopo.yaml +132 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/COMPANY.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/README.md +3 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/founder-ceo/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/gtm-lead/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/agents/pipeline-owner/HEARTBEAT.md +5 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/customer-success/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/deals/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/projects/pipeline/PROJECT.md +7 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/discovery-call-brief/SKILL.md +14 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/skills/icp-scoring/SKILL.md +20 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/pipeline-hygiene/TASK.md +11 -0
- package/src/assets/starter-packs/sources/revenue-gtm-b2b/tasks/weekly-revenue-review/TASK.md +11 -0
- package/src/lib/agent-issue-permissions.ts +56 -0
- package/src/lib/builtin-bopo-skills/bopodev-control-plane.md +7 -0
- package/src/realtime/office-space.ts +7 -0
- package/src/routes/agents.ts +23 -1
- package/src/routes/assistant.ts +40 -1
- package/src/routes/companies.ts +227 -15
- package/src/routes/issues.ts +48 -0
- package/src/routes/plugins.ts +393 -103
- package/src/routes/{loops.ts → routines.ts} +72 -76
- package/src/scripts/onboard-seed.ts +2 -0
- package/src/server.ts +3 -1
- package/src/services/company-assistant-context-snapshot.ts +4 -2
- package/src/services/company-assistant-service.ts +17 -15
- package/src/services/company-file-archive-service.ts +56 -3
- package/src/services/company-file-import-service.ts +210 -31
- package/src/services/governance-service.ts +58 -3
- package/src/services/heartbeat-service/heartbeat-run.ts +7 -0
- package/src/services/plugin-artifact-installer.ts +115 -0
- package/src/services/plugin-artifact-store.ts +28 -0
- package/src/services/plugin-capability-policy.ts +31 -0
- package/src/services/plugin-jobs-service.ts +74 -0
- package/src/services/plugin-manifest-loader.ts +78 -3
- package/src/services/plugin-rpc.ts +102 -0
- package/src/services/plugin-runtime.ts +240 -209
- package/src/services/plugin-worker-host.ts +167 -0
- package/src/services/starter-pack-registry.ts +68 -0
- package/src/services/template-apply-service.ts +3 -1
- package/src/services/template-catalog.ts +29 -0
- package/src/services/work-loop-service/work-loop-service.ts +18 -18
- package/src/shutdown/graceful-shutdown.ts +3 -1
- package/src/worker/scheduler.ts +21 -1
- 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
|
-
|
|
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
|
+
}
|