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.
- 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/lib/instance-paths.ts +5 -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 +82 -3
- package/src/routes/observability.ts +222 -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 +81 -6
- package/src/services/company-file-import-service.ts +221 -31
- package/src/services/company-knowledge-file-service.ts +361 -0
- package/src/services/company-skill-file-service.ts +151 -2
- 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/validation/issue-routes.ts +19 -2
- package/src/worker/scheduler.ts +21 -1
- package/src/services/company-export-service.ts +0 -63
|
@@ -139,6 +139,7 @@ async function loadOfficeOccupantForAgent(db: BopoDb, companyId: string, agentId
|
|
|
139
139
|
companyId: agents.companyId,
|
|
140
140
|
name: agents.name,
|
|
141
141
|
avatarSeed: agents.avatarSeed,
|
|
142
|
+
lucideIconName: agents.lucideIconName,
|
|
142
143
|
role: agents.role,
|
|
143
144
|
status: agents.status,
|
|
144
145
|
providerType: agents.providerType,
|
|
@@ -222,6 +223,7 @@ function deriveAgentOccupant(
|
|
|
222
223
|
companyId: string;
|
|
223
224
|
name: string;
|
|
224
225
|
avatarSeed?: string | null;
|
|
226
|
+
lucideIconName?: string | null;
|
|
225
227
|
role: string;
|
|
226
228
|
status: string;
|
|
227
229
|
providerType: string;
|
|
@@ -300,6 +302,7 @@ function deriveAgentOccupant(
|
|
|
300
302
|
taskLabel: `${formatActionLabel(pendingApproval.action)} approval`,
|
|
301
303
|
providerType: normalizeProviderType(agent.providerType),
|
|
302
304
|
avatarSeed: agent.avatarSeed ?? null,
|
|
305
|
+
lucideIconName: agent.lucideIconName?.trim() ? agent.lucideIconName.trim() : null,
|
|
303
306
|
focusEntityType: "approval",
|
|
304
307
|
focusEntityId: pendingApproval.id,
|
|
305
308
|
updatedAt: pendingApproval.createdAt.toISOString()
|
|
@@ -320,6 +323,7 @@ function deriveAgentOccupant(
|
|
|
320
323
|
taskLabel: claimedIssues[0]?.title ?? "Checking in on work",
|
|
321
324
|
providerType: normalizeProviderType(agent.providerType),
|
|
322
325
|
avatarSeed: agent.avatarSeed ?? null,
|
|
326
|
+
lucideIconName: agent.lucideIconName?.trim() ? agent.lucideIconName.trim() : null,
|
|
323
327
|
focusEntityType: claimedIssues[0] ? "issue" : "agent",
|
|
324
328
|
focusEntityId: claimedIssues[0]?.id ?? agent.id,
|
|
325
329
|
updatedAt: activeRun.startedAt.toISOString()
|
|
@@ -340,6 +344,7 @@ function deriveAgentOccupant(
|
|
|
340
344
|
taskLabel: "Paused",
|
|
341
345
|
providerType: normalizeProviderType(agent.providerType),
|
|
342
346
|
avatarSeed: agent.avatarSeed ?? null,
|
|
347
|
+
lucideIconName: agent.lucideIconName?.trim() ? agent.lucideIconName.trim() : null,
|
|
343
348
|
focusEntityType: "agent",
|
|
344
349
|
focusEntityId: agent.id,
|
|
345
350
|
updatedAt: agent.updatedAt.toISOString()
|
|
@@ -359,6 +364,7 @@ function deriveAgentOccupant(
|
|
|
359
364
|
taskLabel: nextAssignedIssue ? `Up next: ${nextAssignedIssue.title}` : "Waiting for work",
|
|
360
365
|
providerType: normalizeProviderType(agent.providerType),
|
|
361
366
|
avatarSeed: agent.avatarSeed ?? null,
|
|
367
|
+
lucideIconName: agent.lucideIconName?.trim() ? agent.lucideIconName.trim() : null,
|
|
362
368
|
focusEntityType: nextAssignedIssue ? "issue" : "agent",
|
|
363
369
|
focusEntityId: nextAssignedIssue?.id ?? agent.id,
|
|
364
370
|
updatedAt: nextAssignedIssue?.updatedAt.toISOString() ?? agent.updatedAt.toISOString()
|
|
@@ -396,6 +402,7 @@ function deriveHireCandidateOccupant(approval: {
|
|
|
396
402
|
taskLabel: "Awaiting hire approval",
|
|
397
403
|
providerType,
|
|
398
404
|
avatarSeed: null,
|
|
405
|
+
lucideIconName: null,
|
|
399
406
|
focusEntityType: "approval",
|
|
400
407
|
focusEntityId: approval.id,
|
|
401
408
|
updatedAt: approval.createdAt.toISOString()
|
package/src/routes/agents.ts
CHANGED
|
@@ -108,6 +108,8 @@ const UPDATE_AGENT_ALLOWED_KEYS = new Set([
|
|
|
108
108
|
"heartbeatCron",
|
|
109
109
|
"monthlyBudgetUsd",
|
|
110
110
|
"canHireAgents",
|
|
111
|
+
"canAssignAgents",
|
|
112
|
+
"canCreateIssues",
|
|
111
113
|
"runtimeConfig",
|
|
112
114
|
"runtimeCommand",
|
|
113
115
|
"runtimeArgs",
|
|
@@ -120,7 +122,9 @@ const UPDATE_AGENT_ALLOWED_KEYS = new Set([
|
|
|
120
122
|
"interruptGraceSec",
|
|
121
123
|
"runtimeEnv",
|
|
122
124
|
"runPolicy",
|
|
123
|
-
"enabledSkillIds"
|
|
125
|
+
"enabledSkillIds",
|
|
126
|
+
"lucideIconName",
|
|
127
|
+
"avatarSeed"
|
|
124
128
|
]);
|
|
125
129
|
const UPDATE_RUNTIME_CONFIG_ALLOWED_KEYS = new Set([
|
|
126
130
|
"runtimeCommand",
|
|
@@ -485,6 +489,8 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
485
489
|
heartbeatCron: parsed.data.heartbeatCron,
|
|
486
490
|
monthlyBudgetUsd: parsed.data.monthlyBudgetUsd.toFixed(4),
|
|
487
491
|
canHireAgents: parsed.data.canHireAgents,
|
|
492
|
+
canAssignAgents: parsed.data.canAssignAgents,
|
|
493
|
+
canCreateIssues: parsed.data.canCreateIssues,
|
|
488
494
|
...runtimeConfigToDb(runtimeConfig),
|
|
489
495
|
initialState: runtimeConfigToStateBlobPatch(runtimeConfig)
|
|
490
496
|
});
|
|
@@ -537,6 +543,12 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
537
543
|
if (parsed.data.canHireAgents !== undefined) {
|
|
538
544
|
forbiddenFieldUpdates.push("canHireAgents");
|
|
539
545
|
}
|
|
546
|
+
if (parsed.data.canAssignAgents !== undefined) {
|
|
547
|
+
forbiddenFieldUpdates.push("canAssignAgents");
|
|
548
|
+
}
|
|
549
|
+
if (parsed.data.canCreateIssues !== undefined) {
|
|
550
|
+
forbiddenFieldUpdates.push("canCreateIssues");
|
|
551
|
+
}
|
|
540
552
|
if (parsed.data.status !== undefined) {
|
|
541
553
|
forbiddenFieldUpdates.push("status");
|
|
542
554
|
}
|
|
@@ -616,10 +628,18 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
616
628
|
if (requiresRuntimeCwd(effectiveProviderType) && hasText(effectiveRuntimeCwd)) {
|
|
617
629
|
await mkdir(effectiveRuntimeCwd, { recursive: true });
|
|
618
630
|
}
|
|
631
|
+
const nextLucideIconName =
|
|
632
|
+
parsed.data.lucideIconName === undefined ? undefined : parsed.data.lucideIconName.trim();
|
|
633
|
+
|
|
634
|
+
const nextAvatarSeed =
|
|
635
|
+
parsed.data.avatarSeed === undefined ? undefined : parsed.data.avatarSeed.trim();
|
|
636
|
+
|
|
619
637
|
const agent = await updateAgent(ctx.db, {
|
|
620
638
|
companyId: req.companyId!,
|
|
621
639
|
id: req.params.agentId,
|
|
622
640
|
managerAgentId: parsed.data.managerAgentId,
|
|
641
|
+
lucideIconName: nextLucideIconName,
|
|
642
|
+
avatarSeed: nextAvatarSeed,
|
|
623
643
|
role:
|
|
624
644
|
parsed.data.role !== undefined || parsed.data.roleKey !== undefined || parsed.data.title !== undefined
|
|
625
645
|
? resolveAgentRoleText(
|
|
@@ -639,6 +659,8 @@ export function createAgentsRouter(ctx: AppContext) {
|
|
|
639
659
|
monthlyBudgetUsd:
|
|
640
660
|
typeof parsed.data.monthlyBudgetUsd === "number" ? parsed.data.monthlyBudgetUsd.toFixed(4) : undefined,
|
|
641
661
|
canHireAgents: parsed.data.canHireAgents,
|
|
662
|
+
canAssignAgents: parsed.data.canAssignAgents,
|
|
663
|
+
canCreateIssues: parsed.data.canCreateIssues,
|
|
642
664
|
...runtimeConfigToDb(nextRuntime),
|
|
643
665
|
stateBlob: runtimeConfigToStateBlobPatch(nextRuntime)
|
|
644
666
|
});
|
package/src/routes/assistant.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { Router } from "express";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
createAssistantThread,
|
|
5
|
+
deleteAssistantThread,
|
|
5
6
|
getAssistantThreadById,
|
|
6
7
|
getOrCreateAssistantThread,
|
|
7
|
-
listAssistantMessages
|
|
8
|
+
listAssistantMessages,
|
|
9
|
+
listAssistantThreadsForCompany
|
|
8
10
|
} from "bopodev-db";
|
|
9
11
|
import type { AppContext } from "../context";
|
|
10
12
|
import { sendError, sendOk } from "../http";
|
|
@@ -30,6 +32,20 @@ export function createAssistantRouter(ctx: AppContext) {
|
|
|
30
32
|
return sendOk(res, { brains: listAskAssistantBrains() });
|
|
31
33
|
});
|
|
32
34
|
|
|
35
|
+
router.get("/threads", async (req, res) => {
|
|
36
|
+
const companyId = req.companyId!;
|
|
37
|
+
const rawLimit = Number(req.query.limit ?? 50);
|
|
38
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 1), 100) : 50;
|
|
39
|
+
const rows = await listAssistantThreadsForCompany(ctx.db, companyId, limit);
|
|
40
|
+
return sendOk(res, {
|
|
41
|
+
threads: rows.map((t) => ({
|
|
42
|
+
id: t.id,
|
|
43
|
+
updatedAt: t.updatedAt.toISOString(),
|
|
44
|
+
preview: t.previewBody ? truncateAssistantPreview(t.previewBody) : null
|
|
45
|
+
}))
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
33
49
|
router.get("/messages", async (req, res) => {
|
|
34
50
|
const companyId = req.companyId!;
|
|
35
51
|
const qThread =
|
|
@@ -97,6 +113,19 @@ export function createAssistantRouter(ctx: AppContext) {
|
|
|
97
113
|
return sendOk(res, { threadId: thread.id });
|
|
98
114
|
});
|
|
99
115
|
|
|
116
|
+
router.delete("/threads/:threadId", async (req, res) => {
|
|
117
|
+
const companyId = req.companyId!;
|
|
118
|
+
const threadId = typeof req.params.threadId === "string" ? req.params.threadId.trim() : "";
|
|
119
|
+
if (!threadId) {
|
|
120
|
+
return sendError(res, "threadId is required.", 422);
|
|
121
|
+
}
|
|
122
|
+
const ok = await deleteAssistantThread(ctx.db, companyId, threadId);
|
|
123
|
+
if (!ok) {
|
|
124
|
+
return sendError(res, "Chat thread not found.", 404);
|
|
125
|
+
}
|
|
126
|
+
return sendOk(res, { deleted: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
100
129
|
return router;
|
|
101
130
|
}
|
|
102
131
|
|
|
@@ -107,3 +136,13 @@ function safeJsonParse(raw: string): unknown {
|
|
|
107
136
|
return null;
|
|
108
137
|
}
|
|
109
138
|
}
|
|
139
|
+
|
|
140
|
+
const ASSISTANT_THREAD_PREVIEW_MAX = 120;
|
|
141
|
+
|
|
142
|
+
function truncateAssistantPreview(body: string): string {
|
|
143
|
+
const singleLine = body.replace(/\s+/g, " ").trim();
|
|
144
|
+
if (singleLine.length <= ASSISTANT_THREAD_PREVIEW_MAX) {
|
|
145
|
+
return singleLine;
|
|
146
|
+
}
|
|
147
|
+
return `${singleLine.slice(0, ASSISTANT_THREAD_PREVIEW_MAX - 1)}…`;
|
|
148
|
+
}
|
package/src/routes/companies.ts
CHANGED
|
@@ -3,8 +3,18 @@ import type { NextFunction, Request, Response } from "express";
|
|
|
3
3
|
import { Router } from "express";
|
|
4
4
|
import multer from "multer";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { CompanySchema } from "bopodev-contracts";
|
|
7
|
-
import {
|
|
6
|
+
import { CompanySchema, TemplateManifestSchema } from "bopodev-contracts";
|
|
7
|
+
import {
|
|
8
|
+
createAgent,
|
|
9
|
+
createCompany,
|
|
10
|
+
deleteCompany,
|
|
11
|
+
getCurrentTemplateVersion,
|
|
12
|
+
getTemplateBySlug,
|
|
13
|
+
listAgents,
|
|
14
|
+
listCompanies,
|
|
15
|
+
updateAgent,
|
|
16
|
+
updateCompany
|
|
17
|
+
} from "bopodev-db";
|
|
8
18
|
import type { AppContext } from "../context";
|
|
9
19
|
import { sendError, sendOk, sendOkValidated } from "../http";
|
|
10
20
|
import { normalizeRuntimeConfig, resolveRuntimeModelForProvider, runtimeConfigToDb, runtimeConfigToStateBlobPatch } from "../lib/agent-config";
|
|
@@ -19,10 +29,22 @@ import {
|
|
|
19
29
|
pipeCompanyExportZip,
|
|
20
30
|
readCompanyExportFileText
|
|
21
31
|
} from "../services/company-file-archive-service";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
import {
|
|
33
|
+
assertManifestHasCeoAgent,
|
|
34
|
+
CompanyFileImportError,
|
|
35
|
+
importCompanyFromZipBuffer,
|
|
36
|
+
parseCompanyZipBuffer,
|
|
37
|
+
seedOperationalDataFromPackage,
|
|
38
|
+
summarizeCompanyPackageForPreview
|
|
39
|
+
} from "../services/company-file-import-service";
|
|
40
|
+
import { ensureBuiltinPluginsRegistered } from "../services/plugin-runtime";
|
|
41
|
+
import { listStarterPackMetadata, readStarterPackZipBuffer, resolveStarterPackDefinition } from "../services/starter-pack-registry";
|
|
42
|
+
import { TemplateApplyError, applyTemplateManifest } from "../services/template-apply-service";
|
|
43
|
+
import {
|
|
44
|
+
ensureCompanyBuiltinTemplateDefaults,
|
|
45
|
+
getBuiltinStarterTemplateBySlug,
|
|
46
|
+
type BuiltinStarterTemplateDefinition
|
|
47
|
+
} from "../services/template-catalog";
|
|
26
48
|
|
|
27
49
|
const zipUpload = multer({
|
|
28
50
|
storage: multer.memoryStorage(),
|
|
@@ -43,7 +65,8 @@ const createCompanySchema = z.object({
|
|
|
43
65
|
providerType: z
|
|
44
66
|
.enum(["codex", "claude_code", "cursor", "gemini_cli", "opencode", "openai_api", "anthropic_api", "http", "shell"])
|
|
45
67
|
.optional(),
|
|
46
|
-
runtimeModel: z.string().optional()
|
|
68
|
+
runtimeModel: z.string().optional(),
|
|
69
|
+
starterPackId: z.string().min(1).optional()
|
|
47
70
|
});
|
|
48
71
|
|
|
49
72
|
const updateCompanySchema = z
|
|
@@ -73,6 +96,36 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
73
96
|
);
|
|
74
97
|
});
|
|
75
98
|
|
|
99
|
+
router.get("/starter-packs", requireBoardRole, async (_req, res) => {
|
|
100
|
+
return sendOk(res, { starterPacks: listStarterPackMetadata() });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
router.post("/import/files/preview", requireBoardRole, zipUpload.single("archive"), async (req, res) => {
|
|
104
|
+
const file = req.file;
|
|
105
|
+
if (!file?.buffer) {
|
|
106
|
+
return sendError(res, 'Upload a .zip file in field "archive".', 422);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const parsed = parseCompanyZipBuffer(file.buffer);
|
|
110
|
+
const summary = summarizeCompanyPackageForPreview(parsed);
|
|
111
|
+
const warnings: string[] = [];
|
|
112
|
+
if (!summary.hasCeo) {
|
|
113
|
+
warnings.push("No agent with roleKey 'ceo' found; the company may not be ready to run until you add a CEO.");
|
|
114
|
+
}
|
|
115
|
+
return sendOk(res, { ok: true, ...summary, errors: [] as string[], warnings });
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const message = err instanceof CompanyFileImportError ? err.message : String(err);
|
|
118
|
+
return sendOk(res, {
|
|
119
|
+
ok: false,
|
|
120
|
+
companyName: "",
|
|
121
|
+
counts: { projects: 0, agents: 0, goals: 0, routines: 0, skillFiles: 0, knowledgeFiles: 0 },
|
|
122
|
+
hasCeo: false,
|
|
123
|
+
errors: [message],
|
|
124
|
+
warnings: [] as string[]
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
76
129
|
router.post("/import/files", requireBoardRole, zipUpload.single("archive"), async (req, res) => {
|
|
77
130
|
const file = req.file;
|
|
78
131
|
if (!file?.buffer) {
|
|
@@ -173,11 +226,11 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
173
226
|
if (!canAccessCompany(req, companyId)) {
|
|
174
227
|
return sendError(res, "Actor does not have access to this company.", 403);
|
|
175
228
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
229
|
+
return sendError(
|
|
230
|
+
res,
|
|
231
|
+
"JSON company export was removed. Use POST /companies/:companyId/export/files/zip for the portable company zip.",
|
|
232
|
+
410
|
|
233
|
+
);
|
|
181
234
|
});
|
|
182
235
|
|
|
183
236
|
router.post("/", requireBoardRole, async (req, res) => {
|
|
@@ -185,14 +238,142 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
185
238
|
if (!parsed.success) {
|
|
186
239
|
return sendError(res, parsed.error.message, 422);
|
|
187
240
|
}
|
|
188
|
-
const company = await createCompany(ctx.db, parsed.data);
|
|
189
241
|
const providerType =
|
|
190
242
|
parseAgentProvider(parsed.data.providerType) ??
|
|
191
243
|
parseAgentProvider(process.env[DEFAULT_AGENT_PROVIDER_ENV]) ??
|
|
192
244
|
"shell";
|
|
245
|
+
const requestedModel = parsed.data.runtimeModel?.trim() || process.env[DEFAULT_AGENT_MODEL_ENV]?.trim() || undefined;
|
|
246
|
+
|
|
247
|
+
const companyInput = {
|
|
248
|
+
name: parsed.data.name,
|
|
249
|
+
mission: parsed.data.mission ?? null
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (parsed.data.starterPackId?.trim()) {
|
|
253
|
+
const packId = parsed.data.starterPackId.trim();
|
|
254
|
+
const builtinTemplate = getBuiltinStarterTemplateBySlug(packId);
|
|
255
|
+
const zipPack = resolveStarterPackDefinition(packId);
|
|
256
|
+
if (!builtinTemplate && !zipPack) {
|
|
257
|
+
return sendError(res, `Unknown starter pack: ${packId}`, 422);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const company = await createCompany(ctx.db, companyInput);
|
|
261
|
+
await ensureCompanyBuiltinTemplateDefaults(ctx.db, company.id);
|
|
262
|
+
await ensureBuiltinPluginsRegistered(ctx.db, [company.id]);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
if (builtinTemplate) {
|
|
266
|
+
const templateRow = await getTemplateBySlug(ctx.db, company.id, packId);
|
|
267
|
+
if (!templateRow) {
|
|
268
|
+
return sendError(res, `Starter template '${packId}' was not registered.`, 500);
|
|
269
|
+
}
|
|
270
|
+
const templateVersion = await getCurrentTemplateVersion(ctx.db, company.id, templateRow.id);
|
|
271
|
+
if (!templateVersion) {
|
|
272
|
+
return sendError(res, `Starter template '${packId}' has no current version.`, 500);
|
|
273
|
+
}
|
|
274
|
+
let manifestRaw: unknown;
|
|
275
|
+
try {
|
|
276
|
+
manifestRaw = JSON.parse(templateVersion.manifestJson) as unknown;
|
|
277
|
+
} catch {
|
|
278
|
+
return sendError(res, `Starter template '${packId}' has invalid manifest JSON.`, 500);
|
|
279
|
+
}
|
|
280
|
+
const manifestParsed = TemplateManifestSchema.safeParse(manifestRaw);
|
|
281
|
+
if (!manifestParsed.success) {
|
|
282
|
+
return sendError(res, `Starter template '${packId}' has invalid manifest: ${manifestParsed.error.message}`, 422);
|
|
283
|
+
}
|
|
284
|
+
const variables = buildStarterTemplateVariables(builtinTemplate, companyInput);
|
|
285
|
+
await applyTemplateManifest(ctx.db, {
|
|
286
|
+
companyId: company.id,
|
|
287
|
+
templateId: templateRow.id,
|
|
288
|
+
templateVersion: templateVersion.version,
|
|
289
|
+
templateVersionId: templateVersion.id,
|
|
290
|
+
manifest: manifestParsed.data,
|
|
291
|
+
variables
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
let packBuffer: Buffer;
|
|
295
|
+
try {
|
|
296
|
+
packBuffer = await readStarterPackZipBuffer(packId);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
return sendError(res, `Failed to read starter pack: ${String(err)}`, 500);
|
|
299
|
+
}
|
|
300
|
+
let parsedPackage;
|
|
301
|
+
try {
|
|
302
|
+
parsedPackage = parseCompanyZipBuffer(packBuffer);
|
|
303
|
+
assertManifestHasCeoAgent(parsedPackage.doc);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const message = err instanceof CompanyFileImportError ? err.message : String(err);
|
|
306
|
+
return sendError(res, message, 422);
|
|
307
|
+
}
|
|
308
|
+
await seedOperationalDataFromPackage(ctx.db, company.id, parsedPackage);
|
|
309
|
+
}
|
|
310
|
+
} catch (err) {
|
|
311
|
+
const message =
|
|
312
|
+
err instanceof TemplateApplyError
|
|
313
|
+
? err.message
|
|
314
|
+
: err instanceof CompanyFileImportError
|
|
315
|
+
? err.message
|
|
316
|
+
: String(err);
|
|
317
|
+
return sendError(res, message, 422);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const agents = await listAgents(ctx.db, company.id);
|
|
321
|
+
const rk = (a: (typeof agents)[number]) => (a.roleKey ?? "").toLowerCase();
|
|
322
|
+
const leaderAgent =
|
|
323
|
+
agents.find((a) => rk(a) === "ceo") ??
|
|
324
|
+
agents.find((a) => rk(a) === "cmo") ??
|
|
325
|
+
agents.find((a) => a.canHireAgents) ??
|
|
326
|
+
agents[0] ??
|
|
327
|
+
null;
|
|
328
|
+
if (!leaderAgent) {
|
|
329
|
+
return sendError(res, "Starter did not yield an agent to attach the selected CEO runtime to.", 422);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, company.id);
|
|
333
|
+
const resolvedRuntimeModel = resolveRuntimeModelForProvider(
|
|
334
|
+
providerType,
|
|
335
|
+
await resolveOpencodeRuntimeModel(
|
|
336
|
+
providerType,
|
|
337
|
+
normalizeRuntimeConfig({
|
|
338
|
+
defaultRuntimeCwd,
|
|
339
|
+
runtimeConfig: {
|
|
340
|
+
runtimeModel: requestedModel,
|
|
341
|
+
runtimeEnv: resolveSeedRuntimeEnv(providerType)
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
const bootstrapPrompt = leaderAgent.bootstrapPrompt?.trim()
|
|
347
|
+
? leaderAgent.bootstrapPrompt
|
|
348
|
+
: buildDefaultCeoBootstrapPrompt();
|
|
349
|
+
const defaultRuntimeConfig = normalizeRuntimeConfig({
|
|
350
|
+
defaultRuntimeCwd,
|
|
351
|
+
runtimeConfig: {
|
|
352
|
+
runtimeModel: resolvedRuntimeModel,
|
|
353
|
+
bootstrapPrompt,
|
|
354
|
+
runtimeEnv: resolveSeedRuntimeEnv(providerType),
|
|
355
|
+
...(providerType === "shell"
|
|
356
|
+
? {
|
|
357
|
+
runtimeCommand: "echo",
|
|
358
|
+
runtimeArgs: ["ceo bootstrap heartbeat"]
|
|
359
|
+
}
|
|
360
|
+
: {})
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
await updateAgent(ctx.db, {
|
|
364
|
+
companyId: company.id,
|
|
365
|
+
id: leaderAgent.id,
|
|
366
|
+
providerType,
|
|
367
|
+
...runtimeConfigToDb(defaultRuntimeConfig),
|
|
368
|
+
stateBlob: runtimeConfigToStateBlobPatch(defaultRuntimeConfig)
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return sendOk(res, company);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const company = await createCompany(ctx.db, companyInput);
|
|
193
375
|
const defaultRuntimeCwd = await resolveDefaultRuntimeCwdForCompany(ctx.db, company.id);
|
|
194
376
|
await mkdir(defaultRuntimeCwd, { recursive: true });
|
|
195
|
-
const requestedModel = parsed.data.runtimeModel?.trim() || process.env[DEFAULT_AGENT_MODEL_ENV]?.trim() || undefined;
|
|
196
377
|
const resolvedRuntimeModel = resolveRuntimeModelForProvider(
|
|
197
378
|
providerType,
|
|
198
379
|
await resolveOpencodeRuntimeModel(
|
|
@@ -232,10 +413,12 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
232
413
|
heartbeatCron: "*/5 * * * *",
|
|
233
414
|
monthlyBudgetUsd: "100.0000",
|
|
234
415
|
canHireAgents: true,
|
|
416
|
+
canAssignAgents: true,
|
|
417
|
+
canCreateIssues: true,
|
|
235
418
|
...runtimeConfigToDb(defaultRuntimeConfig),
|
|
236
419
|
initialState: runtimeConfigToStateBlobPatch(defaultRuntimeConfig)
|
|
237
420
|
});
|
|
238
|
-
await
|
|
421
|
+
await ensureBuiltinPluginsRegistered(ctx.db, [company.id]);
|
|
239
422
|
await ensureCompanyBuiltinTemplateDefaults(ctx.db, company.id);
|
|
240
423
|
return sendOk(res, company);
|
|
241
424
|
});
|
|
@@ -272,6 +455,35 @@ export function createCompaniesRouter(ctx: AppContext) {
|
|
|
272
455
|
return router;
|
|
273
456
|
}
|
|
274
457
|
|
|
458
|
+
function buildStarterTemplateVariables(
|
|
459
|
+
definition: BuiltinStarterTemplateDefinition,
|
|
460
|
+
input: { name: string; mission: string | null }
|
|
461
|
+
): Record<string, unknown> {
|
|
462
|
+
const name = input.name.trim();
|
|
463
|
+
const missionTail = (input.mission ?? "").trim() || name;
|
|
464
|
+
const defaults: Record<string, string> = {
|
|
465
|
+
brandName: name,
|
|
466
|
+
productName: name,
|
|
467
|
+
targetAudience: missionTail,
|
|
468
|
+
primaryChannel: "LinkedIn"
|
|
469
|
+
};
|
|
470
|
+
const out: Record<string, unknown> = {};
|
|
471
|
+
for (const v of definition.variables) {
|
|
472
|
+
const key = v.key;
|
|
473
|
+
if (defaults[key] !== undefined) {
|
|
474
|
+
out[key] = defaults[key]!;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const dv = v.defaultValue;
|
|
478
|
+
if (dv !== undefined && dv !== null && String(dv).length > 0) {
|
|
479
|
+
out[key] = dv;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
out[key] = missionTail;
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
|
|
275
487
|
function requireCompanyWriteAccess(req: Request, res: Response, next: NextFunction) {
|
|
276
488
|
const targetCompanyId = readCompanyIdParam(req);
|
|
277
489
|
if (!targetCompanyId) {
|
package/src/routes/issues.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
deleteIssue,
|
|
17
17
|
desc,
|
|
18
18
|
eq,
|
|
19
|
+
getCompanyAgent,
|
|
19
20
|
getIssue,
|
|
20
21
|
heartbeatRuns,
|
|
21
22
|
getIssueAttachment,
|
|
@@ -40,10 +41,16 @@ import {
|
|
|
40
41
|
type CommentRecipientInput,
|
|
41
42
|
type PersistedCommentRecipient
|
|
42
43
|
} from "../lib/comment-recipients";
|
|
44
|
+
import {
|
|
45
|
+
agentIssueCreateAssigneeForbiddenMessage,
|
|
46
|
+
agentIssueCreateForbiddenMessage,
|
|
47
|
+
agentIssuePutAssigneeChangeForbiddenMessage
|
|
48
|
+
} from "../lib/agent-issue-permissions";
|
|
43
49
|
import { isInsidePath, normalizeCompanyWorkspacePath, resolveProjectWorkspacePath } from "../lib/instance-paths";
|
|
44
50
|
import { requireCompanyScope } from "../middleware/company-scope";
|
|
45
51
|
import { enforcePermission } from "../middleware/request-actor";
|
|
46
52
|
import { triggerIssueCommentDispatchWorker } from "../services/comment-recipient-dispatch-service";
|
|
53
|
+
import { knowledgeFileExists } from "../services/company-knowledge-file-service";
|
|
47
54
|
import { publishAttentionSnapshot } from "../realtime/attention";
|
|
48
55
|
import {
|
|
49
56
|
createIssueCommentLegacySchema,
|
|
@@ -102,10 +109,26 @@ function normalizeOptionalExternalLink(value: string | null | undefined) {
|
|
|
102
109
|
return trimmed.length > 0 ? trimmed : null;
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
async function validateIssueKnowledgePaths(companyId: string, paths: string[]): Promise<string | null> {
|
|
113
|
+
for (const p of paths) {
|
|
114
|
+
if (!(await knowledgeFileExists({ companyId, relativePath: p }))) {
|
|
115
|
+
return `Knowledge file not found: ${p}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
function toIssueResponse(issue: Record<string, unknown>, goalIds: string[] = []) {
|
|
106
122
|
const labels = parseStringArray(issue.labelsJson);
|
|
107
123
|
const tags = parseStringArray(issue.tagsJson);
|
|
108
|
-
const
|
|
124
|
+
const knowledgePaths = parseStringArray(issue.knowledgePathsJson);
|
|
125
|
+
const {
|
|
126
|
+
labelsJson: _labelsJson,
|
|
127
|
+
tagsJson: _tagsJson,
|
|
128
|
+
knowledgePathsJson: _knowledgePathsJson,
|
|
129
|
+
goalId: _legacyGoalId,
|
|
130
|
+
...rest
|
|
131
|
+
} = issue as Record<string, unknown> & {
|
|
109
132
|
goalId?: unknown;
|
|
110
133
|
};
|
|
111
134
|
const externalRaw = rest.externalLink;
|
|
@@ -116,7 +139,8 @@ function toIssueResponse(issue: Record<string, unknown>, goalIds: string[] = [])
|
|
|
116
139
|
externalLink,
|
|
117
140
|
labels,
|
|
118
141
|
tags,
|
|
119
|
-
goalIds
|
|
142
|
+
goalIds,
|
|
143
|
+
knowledgePaths
|
|
120
144
|
};
|
|
121
145
|
}
|
|
122
146
|
|
|
@@ -170,6 +194,28 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
170
194
|
if (!parsed.success) {
|
|
171
195
|
return sendError(res, parsed.error.message, 422);
|
|
172
196
|
}
|
|
197
|
+
if (req.actor?.type === "agent") {
|
|
198
|
+
const agentRow = await getCompanyAgent(ctx.db, req.companyId!, req.actor.id);
|
|
199
|
+
if (!agentRow) {
|
|
200
|
+
return sendError(res, "Agent not found.", 403);
|
|
201
|
+
}
|
|
202
|
+
const orchestration = {
|
|
203
|
+
canAssignAgents: agentRow.canAssignAgents ?? true,
|
|
204
|
+
canCreateIssues: agentRow.canCreateIssues ?? true
|
|
205
|
+
};
|
|
206
|
+
const createDeny = agentIssueCreateForbiddenMessage(orchestration);
|
|
207
|
+
if (createDeny) {
|
|
208
|
+
return sendError(res, createDeny, 403);
|
|
209
|
+
}
|
|
210
|
+
const assignDeny = agentIssueCreateAssigneeForbiddenMessage(
|
|
211
|
+
orchestration,
|
|
212
|
+
req.actor.id,
|
|
213
|
+
parsed.data.assigneeAgentId ?? null
|
|
214
|
+
);
|
|
215
|
+
if (assignDeny) {
|
|
216
|
+
return sendError(res, assignDeny, 403);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
173
219
|
if (parsed.data.assigneeAgentId) {
|
|
174
220
|
const assignmentValidation = await validateIssueAssignmentScope(
|
|
175
221
|
ctx,
|
|
@@ -181,6 +227,12 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
181
227
|
return sendError(res, assignmentValidation, 422);
|
|
182
228
|
}
|
|
183
229
|
}
|
|
230
|
+
if (parsed.data.knowledgePaths.length > 0) {
|
|
231
|
+
const kpErr = await validateIssueKnowledgePaths(req.companyId!, parsed.data.knowledgePaths);
|
|
232
|
+
if (kpErr) {
|
|
233
|
+
return sendError(res, kpErr, 422);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
184
236
|
const issue = await createIssue(ctx.db, {
|
|
185
237
|
companyId: req.companyId!,
|
|
186
238
|
projectId: parsed.data.projectId,
|
|
@@ -193,7 +245,8 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
193
245
|
priority: parsed.data.priority,
|
|
194
246
|
assigneeAgentId: parsed.data.assigneeAgentId,
|
|
195
247
|
labels: parsed.data.labels,
|
|
196
|
-
tags: parsed.data.tags
|
|
248
|
+
tags: parsed.data.tags,
|
|
249
|
+
knowledgePaths: parsed.data.knowledgePaths
|
|
197
250
|
});
|
|
198
251
|
await appendActivity(ctx.db, {
|
|
199
252
|
companyId: req.companyId!,
|
|
@@ -538,6 +591,26 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
538
591
|
if (!existingIssue) {
|
|
539
592
|
return sendError(res, "Issue not found.", 404);
|
|
540
593
|
}
|
|
594
|
+
if (req.actor?.type === "agent" && parsed.data.assigneeAgentId !== undefined) {
|
|
595
|
+
const agentRow = await getCompanyAgent(ctx.db, req.companyId!, req.actor.id);
|
|
596
|
+
if (!agentRow) {
|
|
597
|
+
return sendError(res, "Agent not found.", 403);
|
|
598
|
+
}
|
|
599
|
+
const orchestration = {
|
|
600
|
+
canAssignAgents: agentRow.canAssignAgents ?? true,
|
|
601
|
+
canCreateIssues: agentRow.canCreateIssues ?? true
|
|
602
|
+
};
|
|
603
|
+
const prevAssignee = existingIssue.assigneeAgentId;
|
|
604
|
+
const putDeny = agentIssuePutAssigneeChangeForbiddenMessage(
|
|
605
|
+
orchestration,
|
|
606
|
+
req.actor.id,
|
|
607
|
+
prevAssignee,
|
|
608
|
+
parsed.data.assigneeAgentId!
|
|
609
|
+
);
|
|
610
|
+
if (putDeny) {
|
|
611
|
+
return sendError(res, putDeny, 403);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
541
614
|
const effectiveProjectId = parsed.data.projectId ?? existingIssue.projectId;
|
|
542
615
|
const effectiveAssigneeAgentId =
|
|
543
616
|
parsed.data.assigneeAgentId === undefined ? existingIssue.assigneeAgentId : parsed.data.assigneeAgentId;
|
|
@@ -558,6 +631,12 @@ export function createIssuesRouter(ctx: AppContext) {
|
|
|
558
631
|
if (updateBody.externalLink !== undefined) {
|
|
559
632
|
updateBody.externalLink = normalizeOptionalExternalLink(updateBody.externalLink) ?? null;
|
|
560
633
|
}
|
|
634
|
+
if (updateBody.knowledgePaths !== undefined && updateBody.knowledgePaths.length > 0) {
|
|
635
|
+
const kpErr = await validateIssueKnowledgePaths(req.companyId!, updateBody.knowledgePaths);
|
|
636
|
+
if (kpErr) {
|
|
637
|
+
return sendError(res, kpErr, 422);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
561
640
|
const issue = await updateIssue(ctx.db, { companyId: req.companyId!, id: req.params.issueId, ...updateBody });
|
|
562
641
|
if (!issue) {
|
|
563
642
|
return sendError(res, "Issue not found.", 404);
|