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
@@ -4,7 +4,7 @@ import { unzipSync } from "fflate";
4
4
  import { parse as yamlParse } from "yaml";
5
5
  import { z } from "zod";
6
6
  import type { BopoDb } from "bopodev-db";
7
- import { createAgent, createCompany, createProject } from "bopodev-db";
7
+ import { createAgent, createCompany, createGoal, createProject, updateAgent, updateGoal } from "bopodev-db";
8
8
  import { normalizeRuntimeConfig, runtimeConfigToDb, runtimeConfigToStateBlobPatch } from "../lib/agent-config";
9
9
  import {
10
10
  resolveAgentMemoryRootPath,
@@ -16,16 +16,22 @@ import { ensureBuiltinPluginsRegistered } from "./plugin-runtime";
16
16
  import { ensureCompanyBuiltinTemplateDefaults } from "./template-catalog";
17
17
  import { addWorkLoopTrigger, createWorkLoop } from "./work-loop-service/work-loop-service";
18
18
 
19
- const EXPORT_SCHEMA = "bopo/company-export/v1";
19
+ export const EXPORT_SCHEMA = "bopo/company-export/v1";
20
20
 
21
- const BopoExportYamlSchema = z.object({
21
+ const goalLevelSchema = z.enum(["company", "project", "agent"]);
22
+ const goalStatusSchema = z.enum(["draft", "active", "completed", "archived"]);
23
+
24
+ export const BopoExportYamlSchema = z.object({
22
25
  schema: z.string(),
23
26
  company: z.object({
24
27
  name: z.string().min(1),
25
28
  mission: z.string().nullable().optional(),
26
29
  slug: z.string().optional()
27
30
  }),
28
- projects: z.record(z.string(), z.object({ name: z.string().min(1), description: z.string().nullable().optional(), status: z.string().optional() })),
31
+ projects: z.record(
32
+ z.string(),
33
+ z.object({ name: z.string().min(1), description: z.string().nullable().optional(), status: z.string().optional() })
34
+ ),
29
35
  agents: z.record(
30
36
  z.string(),
31
37
  z.object({
@@ -37,9 +43,27 @@ const BopoExportYamlSchema = z.object({
37
43
  managerSlug: z.string().nullable().optional(),
38
44
  providerType: z.string().min(1),
39
45
  heartbeatCron: z.string().min(1),
40
- canHireAgents: z.boolean().optional()
46
+ canHireAgents: z.boolean().optional(),
47
+ canAssignAgents: z.boolean().optional(),
48
+ canCreateIssues: z.boolean().optional(),
49
+ bootstrapPrompt: z.string().nullable().optional(),
50
+ monthlyBudgetUsd: z.union([z.string(), z.number()]).optional()
41
51
  })
42
52
  ),
53
+ goals: z
54
+ .record(
55
+ z.string(),
56
+ z.object({
57
+ level: goalLevelSchema,
58
+ title: z.string().min(1),
59
+ description: z.string().nullable().optional(),
60
+ status: goalStatusSchema.optional(),
61
+ projectSlug: z.string().nullable().optional(),
62
+ parentGoalSlug: z.string().nullable().optional(),
63
+ ownerAgentSlug: z.string().nullable().optional()
64
+ })
65
+ )
66
+ .optional(),
43
67
  routines: z
44
68
  .record(
45
69
  z.string(),
@@ -62,6 +86,13 @@ const BopoExportYamlSchema = z.object({
62
86
  .optional()
63
87
  });
64
88
 
89
+ export type BopoExportDoc = z.infer<typeof BopoExportYamlSchema>;
90
+
91
+ export type ParsedCompanyPackage = {
92
+ doc: BopoExportDoc;
93
+ entries: Record<string, string>;
94
+ };
95
+
65
96
  export class CompanyFileImportError extends Error {
66
97
  constructor(message: string) {
67
98
  super(message);
@@ -77,7 +108,7 @@ function normalizeZipPath(key: string): string | null {
77
108
  return t;
78
109
  }
79
110
 
80
- function decodeZipEntries(buffer: Buffer): Record<string, string> {
111
+ export function decodeZipEntries(buffer: Buffer): Record<string, string> {
81
112
  let raw: Record<string, Uint8Array>;
82
113
  try {
83
114
  raw = unzipSync(new Uint8Array(buffer));
@@ -99,6 +130,30 @@ function decodeZipEntries(buffer: Buffer): Record<string, string> {
99
130
  return out;
100
131
  }
101
132
 
133
+ /** Parse and validate a company zip; throws CompanyFileImportError on failure. */
134
+ export function parseCompanyZipBuffer(buffer: Buffer): ParsedCompanyPackage {
135
+ const entries = decodeZipEntries(buffer);
136
+ const yamlText = entries[".bopo.yaml"] ?? entries["bopo.yaml"];
137
+ if (!yamlText?.trim()) {
138
+ throw new CompanyFileImportError('Zip must contain a ".bopo.yaml" manifest at the archive root.');
139
+ }
140
+ let parsedYaml: unknown;
141
+ try {
142
+ parsedYaml = yamlParse(yamlText);
143
+ } catch {
144
+ throw new CompanyFileImportError(".bopo.yaml is not valid YAML.");
145
+ }
146
+ const parsed = BopoExportYamlSchema.safeParse(parsedYaml);
147
+ if (!parsed.success) {
148
+ throw new CompanyFileImportError(`Invalid export manifest: ${parsed.error.message}`);
149
+ }
150
+ const doc = parsed.data;
151
+ if (doc.schema !== EXPORT_SCHEMA) {
152
+ throw new CompanyFileImportError(`Unsupported export schema '${doc.schema}' (expected ${EXPORT_SCHEMA}).`);
153
+ }
154
+ return { doc, entries };
155
+ }
156
+
102
157
  const PROVIDER_TYPES = new Set([
103
158
  "claude_code",
104
159
  "codex",
@@ -118,36 +173,60 @@ function coerceProviderType(raw: string): AgentProvider {
118
173
  return PROVIDER_TYPES.has(raw) ? (raw as AgentProvider) : "shell";
119
174
  }
120
175
 
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.');
176
+ function formatMonthlyBudgetUsdFromManifest(raw: string | number | undefined): string {
177
+ if (raw === undefined) {
178
+ return "100.0000";
126
179
  }
127
- let parsedYaml: unknown;
128
- try {
129
- parsedYaml = yamlParse(yamlText);
130
- } catch {
131
- throw new CompanyFileImportError(".bopo.yaml is not valid YAML.");
180
+ if (typeof raw === "number" && Number.isFinite(raw)) {
181
+ return raw.toFixed(4);
132
182
  }
133
- const parsed = BopoExportYamlSchema.safeParse(parsedYaml);
134
- if (!parsed.success) {
135
- throw new CompanyFileImportError(`Invalid export manifest: ${parsed.error.message}`);
183
+ const s = String(raw).trim();
184
+ if (!s) {
185
+ return "100.0000";
136
186
  }
137
- const doc = parsed.data;
138
- if (doc.schema !== EXPORT_SCHEMA) {
139
- throw new CompanyFileImportError(`Unsupported export schema '${doc.schema}' (expected ${EXPORT_SCHEMA}).`);
187
+ const n = Number(s);
188
+ return Number.isFinite(n) ? n.toFixed(4) : "100.0000";
189
+ }
190
+
191
+ function sortGoalSlugsForImport(
192
+ goals: Record<string, { parentGoalSlug?: string | null | undefined }>
193
+ ): string[] {
194
+ const slugs = Object.keys(goals);
195
+ const slugSet = new Set(slugs);
196
+ const remaining = new Set(slugs);
197
+ const order: string[] = [];
198
+ while (remaining.size > 0) {
199
+ const ready = [...remaining].filter((slug) => {
200
+ const p = goals[slug]?.parentGoalSlug?.trim();
201
+ if (!p) {
202
+ return true;
203
+ }
204
+ if (!slugSet.has(p)) {
205
+ throw new CompanyFileImportError(`Goal '${slug}' references unknown parent goal slug '${p}'.`);
206
+ }
207
+ return !remaining.has(p);
208
+ });
209
+ if (ready.length === 0) {
210
+ throw new CompanyFileImportError("Circular goal parent chain in manifest.");
211
+ }
212
+ ready.sort((a, b) => a.localeCompare(b));
213
+ for (const s of ready) {
214
+ order.push(s);
215
+ remaining.delete(s);
216
+ }
140
217
  }
218
+ return order;
219
+ }
141
220
 
142
- const created = await createCompany(db, {
143
- name: doc.company.name,
144
- mission: doc.company.mission ?? null
145
- });
146
- const companyId = created.id;
221
+ /**
222
+ * Seeds projects, agents, workspace files, goals, and routines for an existing company.
223
+ * Does not create the company row or call ensureCompanyBuiltinTemplateDefaults.
224
+ */
225
+ export async function seedOperationalDataFromPackage(db: BopoDb, companyId: string, parsed: ParsedCompanyPackage): Promise<void> {
226
+ const { doc, entries } = parsed;
147
227
  const cwd = await resolveDefaultRuntimeCwdForCompany(db, companyId);
148
228
  await mkdir(cwd, { recursive: true });
149
229
  await ensureBuiltinPluginsRegistered(db, [companyId]);
150
- await ensureCompanyBuiltinTemplateDefaults(db, companyId);
151
230
 
152
231
  const projectSlugToId = new Map<string, string>();
153
232
  const projectStatuses = new Set(["planned", "active", "paused", "blocked", "completed", "archived"]);
@@ -183,7 +262,11 @@ export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Pr
183
262
  }
184
263
  const defaultRt = normalizeRuntimeConfig({
185
264
  defaultRuntimeCwd: cwd,
186
- runtimeConfig: { runtimeModel: undefined, runtimeEnv: {} }
265
+ runtimeConfig: {
266
+ runtimeModel: undefined,
267
+ runtimeEnv: {},
268
+ bootstrapPrompt: a.bootstrapPrompt?.trim() || undefined
269
+ }
187
270
  });
188
271
  const createdAgent = await createAgent(db, {
189
272
  companyId,
@@ -195,8 +278,10 @@ export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Pr
195
278
  name: a.name,
196
279
  providerType: coerceProviderType(a.providerType),
197
280
  heartbeatCron: a.heartbeatCron,
198
- monthlyBudgetUsd: "100.0000",
281
+ monthlyBudgetUsd: formatMonthlyBudgetUsdFromManifest(a.monthlyBudgetUsd),
199
282
  canHireAgents: a.canHireAgents ?? false,
283
+ canAssignAgents: a.canAssignAgents ?? true,
284
+ canCreateIssues: a.canCreateIssues ?? true,
200
285
  ...runtimeConfigToDb(defaultRt),
201
286
  initialState: runtimeConfigToStateBlobPatch(defaultRt)
202
287
  });
@@ -243,6 +328,60 @@ export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Pr
243
328
  const dest = join(companyRoot, path);
244
329
  await mkdir(dirname(dest), { recursive: true });
245
330
  await writeFile(dest, text, "utf8");
331
+ continue;
332
+ }
333
+ if (path.startsWith("knowledge/")) {
334
+ const dest = join(companyRoot, path);
335
+ await mkdir(dirname(dest), { recursive: true });
336
+ await writeFile(dest, text, "utf8");
337
+ }
338
+ }
339
+
340
+ const goalsManifest = doc.goals ?? {};
341
+ const goalSlugToId = new Map<string, string>();
342
+ for (const slug of sortGoalSlugsForImport(goalsManifest)) {
343
+ const g = goalsManifest[slug]!;
344
+ const level = g.level;
345
+ const projectSlug = g.projectSlug?.trim() || null;
346
+ const projectId = projectSlug ? projectSlugToId.get(projectSlug) ?? null : null;
347
+ if (level === "project" && !projectId) {
348
+ throw new CompanyFileImportError(`Goal '${slug}' (project level) references unknown project slug '${projectSlug ?? ""}'.`);
349
+ }
350
+ if (level === "company" && projectId) {
351
+ throw new CompanyFileImportError(`Goal '${slug}' is company-level but specifies a project.`);
352
+ }
353
+ const parentSlug = g.parentGoalSlug?.trim() || null;
354
+ const parentGoalId = parentSlug ? goalSlugToId.get(parentSlug) ?? null : null;
355
+ if (parentSlug && !parentGoalId) {
356
+ throw new CompanyFileImportError(`Goal '${slug}' references unknown parent goal slug '${parentSlug}'.`);
357
+ }
358
+ const ownerSlug = g.ownerAgentSlug?.trim() || null;
359
+ const ownerAgentId = ownerSlug ? agentSlugToId.get(ownerSlug) ?? null : null;
360
+ if (ownerSlug && !ownerAgentId) {
361
+ throw new CompanyFileImportError(`Goal '${slug}' references unknown owner agent slug '${ownerSlug}'.`);
362
+ }
363
+ const agentLevelProjectId = level === "agent" ? projectId : null;
364
+ if (level === "agent" && g.projectSlug?.trim() && !projectId) {
365
+ throw new CompanyFileImportError(`Goal '${slug}' (agent level) references unknown project slug '${g.projectSlug.trim()}'.`);
366
+ }
367
+
368
+ const created = await createGoal(db, {
369
+ companyId,
370
+ projectId: level === "project" ? projectId : agentLevelProjectId,
371
+ parentGoalId,
372
+ ownerAgentId,
373
+ level,
374
+ title: g.title,
375
+ description: g.description?.trim() || undefined
376
+ });
377
+ goalSlugToId.set(slug, created.id);
378
+ const st = g.status?.trim();
379
+ if (st && st !== "draft") {
380
+ await updateGoal(db, {
381
+ companyId,
382
+ id: created.id,
383
+ status: st
384
+ });
246
385
  }
247
386
  }
248
387
 
@@ -266,7 +405,7 @@ export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Pr
266
405
  for (const t of r.triggers) {
267
406
  await addWorkLoopTrigger(db, {
268
407
  companyId,
269
- workLoopId: loop.id,
408
+ routineId: loop.id,
270
409
  cronExpression: t.cronExpression,
271
410
  timezone: t.timezone?.trim() || "UTC",
272
411
  label: t.label ?? null,
@@ -274,6 +413,57 @@ export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Pr
274
413
  });
275
414
  }
276
415
  }
416
+ }
417
+
418
+ export function assertManifestHasCeoAgent(doc: BopoExportDoc): void {
419
+ const hasCeo = Object.values(doc.agents).some((a) => (a.roleKey ?? "").trim().toLowerCase() === "ceo");
420
+ if (!hasCeo) {
421
+ throw new CompanyFileImportError("Company package must include an agent with roleKey 'ceo'.");
422
+ }
423
+ }
424
+
425
+ export function summarizeCompanyPackageForPreview(parsed: ParsedCompanyPackage): {
426
+ companyName: string;
427
+ counts: {
428
+ projects: number;
429
+ agents: number;
430
+ goals: number;
431
+ routines: number;
432
+ skillFiles: number;
433
+ knowledgeFiles: number;
434
+ };
435
+ hasCeo: boolean;
436
+ } {
437
+ const doc = parsed.doc;
438
+ const skillFiles = Object.keys(parsed.entries).filter((k) => k.startsWith("skills/") && !k.endsWith("/")).length;
439
+ const knowledgeFiles = Object.keys(parsed.entries).filter(
440
+ (k) => k.startsWith("knowledge/") && !k.endsWith("/")
441
+ ).length;
442
+ const hasCeo = Object.values(doc.agents).some((a) => (a.roleKey ?? "").trim().toLowerCase() === "ceo");
443
+ return {
444
+ companyName: doc.company.name,
445
+ counts: {
446
+ projects: Object.keys(doc.projects).length,
447
+ agents: Object.keys(doc.agents).length,
448
+ goals: Object.keys(doc.goals ?? {}).length,
449
+ routines: Object.keys(doc.routines ?? {}).length,
450
+ skillFiles,
451
+ knowledgeFiles
452
+ },
453
+ hasCeo
454
+ };
455
+ }
456
+
457
+ export async function importCompanyFromZipBuffer(db: BopoDb, buffer: Buffer): Promise<{ companyId: string; name: string }> {
458
+ const parsed = parseCompanyZipBuffer(buffer);
459
+ const doc = parsed.doc;
277
460
 
461
+ const created = await createCompany(db, {
462
+ name: doc.company.name,
463
+ mission: doc.company.mission ?? null
464
+ });
465
+ const companyId = created.id;
466
+ await ensureCompanyBuiltinTemplateDefaults(db, companyId);
467
+ await seedOperationalDataFromPackage(db, companyId, parsed);
278
468
  return { companyId, name: doc.company.name };
279
469
  }