bopodev-api 0.1.31 → 0.1.33
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 +10 -4
- package/src/app.ts +2 -0
- package/src/lib/agent-config.ts +58 -10
- package/src/lib/builtin-bopo-skills/bopodev-control-plane.md +123 -0
- package/src/lib/builtin-bopo-skills/bopodev-create-agent.md +90 -0
- package/src/lib/builtin-bopo-skills/index.ts +36 -0
- package/src/lib/builtin-bopo-skills/para-memory-files.md +48 -0
- package/src/lib/instance-paths.ts +5 -0
- package/src/routes/agents.ts +21 -7
- package/src/routes/assistant.ts +109 -0
- package/src/routes/companies.ts +112 -1
- package/src/routes/observability.ts +299 -1
- package/src/services/company-assistant-brain.ts +50 -0
- package/src/services/company-assistant-cli.ts +388 -0
- package/src/services/company-assistant-context-snapshot.ts +287 -0
- package/src/services/company-assistant-llm.ts +375 -0
- package/src/services/company-assistant-service.ts +1012 -0
- package/src/services/company-file-archive-service.ts +445 -0
- package/src/services/company-file-import-service.ts +279 -0
- package/src/services/company-skill-file-service.ts +558 -0
- package/src/services/governance-service.ts +11 -3
- package/src/services/heartbeat-service/heartbeat-run.ts +45 -8
- package/src/services/memory-file-service.ts +70 -0
- package/src/services/template-catalog.ts +19 -6
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { unzipSync } from "fflate";
|
|
4
|
+
import { parse as yamlParse } from "yaml";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { BopoDb } from "bopodev-db";
|
|
7
|
+
import { createAgent, createCompany, createProject } from "bopodev-db";
|
|
8
|
+
import { normalizeRuntimeConfig, runtimeConfigToDb, runtimeConfigToStateBlobPatch } from "../lib/agent-config";
|
|
9
|
+
import {
|
|
10
|
+
resolveAgentMemoryRootPath,
|
|
11
|
+
resolveAgentOperatingPath,
|
|
12
|
+
resolveCompanyProjectsWorkspacePath
|
|
13
|
+
} from "../lib/instance-paths";
|
|
14
|
+
import { resolveDefaultRuntimeCwdForCompany } from "../lib/workspace-policy";
|
|
15
|
+
import { ensureBuiltinPluginsRegistered } from "./plugin-runtime";
|
|
16
|
+
import { ensureCompanyBuiltinTemplateDefaults } from "./template-catalog";
|
|
17
|
+
import { addWorkLoopTrigger, createWorkLoop } from "./work-loop-service/work-loop-service";
|
|
18
|
+
|
|
19
|
+
const EXPORT_SCHEMA = "bopo/company-export/v1";
|
|
20
|
+
|
|
21
|
+
const BopoExportYamlSchema = z.object({
|
|
22
|
+
schema: z.string(),
|
|
23
|
+
company: z.object({
|
|
24
|
+
name: z.string().min(1),
|
|
25
|
+
mission: z.string().nullable().optional(),
|
|
26
|
+
slug: z.string().optional()
|
|
27
|
+
}),
|
|
28
|
+
projects: z.record(z.string(), z.object({ name: z.string().min(1), description: z.string().nullable().optional(), status: z.string().optional() })),
|
|
29
|
+
agents: z.record(
|
|
30
|
+
z.string(),
|
|
31
|
+
z.object({
|
|
32
|
+
name: z.string().min(1),
|
|
33
|
+
role: z.string().min(1),
|
|
34
|
+
roleKey: z.string().nullable().optional(),
|
|
35
|
+
title: z.string().nullable().optional(),
|
|
36
|
+
capabilities: z.string().nullable().optional(),
|
|
37
|
+
managerSlug: z.string().nullable().optional(),
|
|
38
|
+
providerType: z.string().min(1),
|
|
39
|
+
heartbeatCron: z.string().min(1),
|
|
40
|
+
canHireAgents: z.boolean().optional()
|
|
41
|
+
})
|
|
42
|
+
),
|
|
43
|
+
routines: z
|
|
44
|
+
.record(
|
|
45
|
+
z.string(),
|
|
46
|
+
z.object({
|
|
47
|
+
title: z.string().min(1),
|
|
48
|
+
description: z.string().nullable().optional(),
|
|
49
|
+
projectSlug: z.string().min(1),
|
|
50
|
+
assigneeAgentSlug: z.string().min(1),
|
|
51
|
+
triggers: z
|
|
52
|
+
.array(
|
|
53
|
+
z.object({
|
|
54
|
+
cronExpression: z.string().min(1),
|
|
55
|
+
timezone: z.string().optional(),
|
|
56
|
+
label: z.string().nullable().optional()
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
.min(1)
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
.optional()
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export class CompanyFileImportError extends Error {
|
|
66
|
+
constructor(message: string) {
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = "CompanyFileImportError";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeZipPath(key: string): string | null {
|
|
73
|
+
const t = key.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
74
|
+
if (!t || t.includes("..")) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return t;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function decodeZipEntries(buffer: Buffer): Record<string, string> {
|
|
81
|
+
let raw: Record<string, Uint8Array>;
|
|
82
|
+
try {
|
|
83
|
+
raw = unzipSync(new Uint8Array(buffer));
|
|
84
|
+
} catch {
|
|
85
|
+
throw new CompanyFileImportError("Archive is not a valid zip file.");
|
|
86
|
+
}
|
|
87
|
+
const out: Record<string, string> = {};
|
|
88
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
89
|
+
const path = normalizeZipPath(k);
|
|
90
|
+
if (!path || path.endsWith("/")) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
out[path] = new TextDecoder("utf8", { fatal: false }).decode(v);
|
|
95
|
+
} catch {
|
|
96
|
+
/* skip binary */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const PROVIDER_TYPES = new Set([
|
|
103
|
+
"claude_code",
|
|
104
|
+
"codex",
|
|
105
|
+
"cursor",
|
|
106
|
+
"opencode",
|
|
107
|
+
"gemini_cli",
|
|
108
|
+
"openai_api",
|
|
109
|
+
"anthropic_api",
|
|
110
|
+
"openclaw_gateway",
|
|
111
|
+
"http",
|
|
112
|
+
"shell"
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
type AgentProvider = NonNullable<Parameters<typeof createAgent>[1]["providerType"]>;
|
|
116
|
+
|
|
117
|
+
function coerceProviderType(raw: string): AgentProvider {
|
|
118
|
+
return PROVIDER_TYPES.has(raw) ? (raw as AgentProvider) : "shell";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Promise<{ companyId: string; name: string }> {
|
|
122
|
+
const entries = decodeZipEntries(buffer);
|
|
123
|
+
const yamlText = entries[".bopo.yaml"] ?? entries["bopo.yaml"];
|
|
124
|
+
if (!yamlText?.trim()) {
|
|
125
|
+
throw new CompanyFileImportError('Zip must contain a ".bopo.yaml" manifest at the archive root.');
|
|
126
|
+
}
|
|
127
|
+
let parsedYaml: unknown;
|
|
128
|
+
try {
|
|
129
|
+
parsedYaml = yamlParse(yamlText);
|
|
130
|
+
} catch {
|
|
131
|
+
throw new CompanyFileImportError(".bopo.yaml is not valid YAML.");
|
|
132
|
+
}
|
|
133
|
+
const parsed = BopoExportYamlSchema.safeParse(parsedYaml);
|
|
134
|
+
if (!parsed.success) {
|
|
135
|
+
throw new CompanyFileImportError(`Invalid export manifest: ${parsed.error.message}`);
|
|
136
|
+
}
|
|
137
|
+
const doc = parsed.data;
|
|
138
|
+
if (doc.schema !== EXPORT_SCHEMA) {
|
|
139
|
+
throw new CompanyFileImportError(`Unsupported export schema '${doc.schema}' (expected ${EXPORT_SCHEMA}).`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const created = await createCompany(db, {
|
|
143
|
+
name: doc.company.name,
|
|
144
|
+
mission: doc.company.mission ?? null
|
|
145
|
+
});
|
|
146
|
+
const companyId = created.id;
|
|
147
|
+
const cwd = await resolveDefaultRuntimeCwdForCompany(db, companyId);
|
|
148
|
+
await mkdir(cwd, { recursive: true });
|
|
149
|
+
await ensureBuiltinPluginsRegistered(db, [companyId]);
|
|
150
|
+
await ensureCompanyBuiltinTemplateDefaults(db, companyId);
|
|
151
|
+
|
|
152
|
+
const projectSlugToId = new Map<string, string>();
|
|
153
|
+
const projectStatuses = new Set(["planned", "active", "paused", "blocked", "completed", "archived"]);
|
|
154
|
+
for (const [projectSlug, p] of Object.entries(doc.projects)) {
|
|
155
|
+
const st = p.status?.trim();
|
|
156
|
+
const status = st && projectStatuses.has(st) ? (st as "planned") : "planned";
|
|
157
|
+
const row = await createProject(db, {
|
|
158
|
+
companyId,
|
|
159
|
+
name: p.name,
|
|
160
|
+
description: p.description ?? null,
|
|
161
|
+
status
|
|
162
|
+
});
|
|
163
|
+
if (row) {
|
|
164
|
+
projectSlugToId.set(projectSlug, row.id);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const agentSlugToId = new Map<string, string>();
|
|
169
|
+
const agentEntries = Object.entries(doc.agents);
|
|
170
|
+
const pending = new Map(agentEntries);
|
|
171
|
+
let guard = 0;
|
|
172
|
+
while (pending.size > 0 && guard < 500) {
|
|
173
|
+
guard += 1;
|
|
174
|
+
let progressed = false;
|
|
175
|
+
for (const [slug, a] of [...pending.entries()]) {
|
|
176
|
+
const mgrSlug = a.managerSlug?.trim() || null;
|
|
177
|
+
let managerId: string | null = null;
|
|
178
|
+
if (mgrSlug) {
|
|
179
|
+
managerId = agentSlugToId.get(mgrSlug) ?? null;
|
|
180
|
+
if (!managerId) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const defaultRt = normalizeRuntimeConfig({
|
|
185
|
+
defaultRuntimeCwd: cwd,
|
|
186
|
+
runtimeConfig: { runtimeModel: undefined, runtimeEnv: {} }
|
|
187
|
+
});
|
|
188
|
+
const createdAgent = await createAgent(db, {
|
|
189
|
+
companyId,
|
|
190
|
+
managerAgentId: managerId,
|
|
191
|
+
role: a.role,
|
|
192
|
+
roleKey: a.roleKey?.trim() || null,
|
|
193
|
+
title: a.title?.trim() || null,
|
|
194
|
+
capabilities: a.capabilities?.trim() || null,
|
|
195
|
+
name: a.name,
|
|
196
|
+
providerType: coerceProviderType(a.providerType),
|
|
197
|
+
heartbeatCron: a.heartbeatCron,
|
|
198
|
+
monthlyBudgetUsd: "100.0000",
|
|
199
|
+
canHireAgents: a.canHireAgents ?? false,
|
|
200
|
+
...runtimeConfigToDb(defaultRt),
|
|
201
|
+
initialState: runtimeConfigToStateBlobPatch(defaultRt)
|
|
202
|
+
});
|
|
203
|
+
agentSlugToId.set(slug, createdAgent.id);
|
|
204
|
+
pending.delete(slug);
|
|
205
|
+
progressed = true;
|
|
206
|
+
}
|
|
207
|
+
if (!progressed) {
|
|
208
|
+
throw new CompanyFileImportError("Could not resolve agent manager chain (circular or missing manager slug).");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const companyRoot = resolveCompanyProjectsWorkspacePath(companyId);
|
|
213
|
+
for (const [path, text] of Object.entries(entries)) {
|
|
214
|
+
if (path === ".bopo.yaml" || path === "bopo.yaml" || path === "COMPANY.md" || path === "README.md") {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (path.startsWith("projects/") && path.endsWith("/PROJECT.md")) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (path.startsWith("tasks/") && path.endsWith("/TASK.md")) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (path.startsWith("agents/")) {
|
|
224
|
+
const parts = path.split("/").filter(Boolean);
|
|
225
|
+
if (parts.length < 3) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const agentSlug = parts[1]!;
|
|
229
|
+
const agentId = agentSlugToId.get(agentSlug);
|
|
230
|
+
if (!agentId) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const rest = parts.slice(2).join("/");
|
|
234
|
+
const isMemory = rest.startsWith("memory/");
|
|
235
|
+
const relativePath = isMemory ? rest.slice("memory/".length) : rest;
|
|
236
|
+
const base = isMemory ? resolveAgentMemoryRootPath(companyId, agentId) : resolveAgentOperatingPath(companyId, agentId);
|
|
237
|
+
const dest = join(base, relativePath);
|
|
238
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
239
|
+
await writeFile(dest, text, "utf8");
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (path.startsWith("skills/")) {
|
|
243
|
+
const dest = join(companyRoot, path);
|
|
244
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
245
|
+
await writeFile(dest, text, "utf8");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const routines = doc.routines ?? {};
|
|
250
|
+
for (const [, r] of Object.entries(routines)) {
|
|
251
|
+
const projectId = projectSlugToId.get(r.projectSlug);
|
|
252
|
+
const assigneeId = agentSlugToId.get(r.assigneeAgentSlug);
|
|
253
|
+
if (!projectId || !assigneeId) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const loop = await createWorkLoop(db, {
|
|
257
|
+
companyId,
|
|
258
|
+
projectId,
|
|
259
|
+
title: r.title,
|
|
260
|
+
description: r.description?.trim() || null,
|
|
261
|
+
assigneeAgentId: assigneeId
|
|
262
|
+
});
|
|
263
|
+
if (!loop) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
for (const t of r.triggers) {
|
|
267
|
+
await addWorkLoopTrigger(db, {
|
|
268
|
+
companyId,
|
|
269
|
+
workLoopId: loop.id,
|
|
270
|
+
cronExpression: t.cronExpression,
|
|
271
|
+
timezone: t.timezone?.trim() || "UTC",
|
|
272
|
+
label: t.label ?? null,
|
|
273
|
+
enabled: true
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { companyId, name: doc.company.name };
|
|
279
|
+
}
|