bopodev-api 0.1.28 → 0.1.30

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 (46) hide show
  1. package/package.json +4 -4
  2. package/src/app.ts +17 -69
  3. package/src/lib/ceo-bootstrap-prompt.ts +1 -0
  4. package/src/lib/run-artifact-paths.ts +8 -0
  5. package/src/middleware/cors-config.ts +36 -0
  6. package/src/middleware/request-actor.ts +10 -16
  7. package/src/middleware/request-id.ts +9 -0
  8. package/src/middleware/request-logging.ts +24 -0
  9. package/src/realtime/office-space.ts +1 -0
  10. package/src/routes/agents.ts +90 -46
  11. package/src/routes/companies.ts +20 -1
  12. package/src/routes/goals.ts +7 -13
  13. package/src/routes/governance.ts +2 -5
  14. package/src/routes/heartbeats.ts +7 -25
  15. package/src/routes/issues.ts +65 -120
  16. package/src/routes/observability.ts +6 -1
  17. package/src/routes/plugins.ts +5 -17
  18. package/src/routes/projects.ts +7 -25
  19. package/src/routes/templates.ts +6 -21
  20. package/src/scripts/onboard-seed.ts +18 -8
  21. package/src/server.ts +33 -292
  22. package/src/services/company-export-service.ts +63 -0
  23. package/src/services/governance-service.ts +10 -14
  24. package/src/services/heartbeat-service/active-runs.ts +15 -0
  25. package/src/services/heartbeat-service/budget-override.ts +46 -0
  26. package/src/services/heartbeat-service/claims.ts +61 -0
  27. package/src/services/heartbeat-service/cron.ts +58 -0
  28. package/src/services/heartbeat-service/heartbeat-realtime.ts +28 -0
  29. package/src/services/heartbeat-service/heartbeat-run-summary-text.ts +53 -0
  30. package/src/services/{heartbeat-service.ts → heartbeat-service/heartbeat-run.ts} +201 -634
  31. package/src/services/heartbeat-service/index.ts +5 -0
  32. package/src/services/heartbeat-service/stop.ts +90 -0
  33. package/src/services/heartbeat-service/sweep.ts +145 -0
  34. package/src/services/heartbeat-service/types.ts +66 -0
  35. package/src/services/memory-file-service.ts +10 -2
  36. package/src/services/template-apply-service.ts +6 -0
  37. package/src/services/template-catalog.ts +37 -3
  38. package/src/shutdown/graceful-shutdown.ts +77 -0
  39. package/src/startup/database.ts +41 -0
  40. package/src/startup/deployment-validation.ts +37 -0
  41. package/src/startup/env.ts +17 -0
  42. package/src/startup/runtime-health.ts +128 -0
  43. package/src/startup/scheduler-config.ts +39 -0
  44. package/src/types/express.d.ts +13 -0
  45. package/src/types/request-actor.ts +6 -0
  46. package/src/validation/issue-routes.ts +80 -0
@@ -0,0 +1,58 @@
1
+ export function isHeartbeatDue(cronExpression: string, lastRunAt: Date | null, now: Date) {
2
+ const normalizedNow = truncateToMinute(now);
3
+ if (!matchesCronExpression(cronExpression, normalizedNow)) {
4
+ return false;
5
+ }
6
+ if (!lastRunAt) {
7
+ return true;
8
+ }
9
+ return truncateToMinute(lastRunAt).getTime() !== normalizedNow.getTime();
10
+ }
11
+
12
+ function truncateToMinute(date: Date) {
13
+ const clone = new Date(date);
14
+ clone.setSeconds(0, 0);
15
+ return clone;
16
+ }
17
+
18
+ function matchesCronExpression(expression: string, date: Date) {
19
+ const parts = expression.trim().split(/\s+/);
20
+ if (parts.length !== 5) {
21
+ return false;
22
+ }
23
+
24
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts as [string, string, string, string, string];
25
+ return (
26
+ matchesCronField(minute, date.getMinutes(), 0, 59) &&
27
+ matchesCronField(hour, date.getHours(), 0, 23) &&
28
+ matchesCronField(dayOfMonth, date.getDate(), 1, 31) &&
29
+ matchesCronField(month, date.getMonth() + 1, 1, 12) &&
30
+ matchesCronField(dayOfWeek, date.getDay(), 0, 6)
31
+ );
32
+ }
33
+
34
+ function matchesCronField(field: string, value: number, min: number, max: number) {
35
+ return field.split(",").some((part) => matchesCronPart(part.trim(), value, min, max));
36
+ }
37
+
38
+ function matchesCronPart(part: string, value: number, min: number, max: number): boolean {
39
+ if (part === "*") {
40
+ return true;
41
+ }
42
+
43
+ const stepMatch = part.match(/^\*\/(\d+)$/);
44
+ if (stepMatch) {
45
+ const step = Number(stepMatch[1]);
46
+ return Number.isInteger(step) && step > 0 ? (value - min) % step === 0 : false;
47
+ }
48
+
49
+ const rangeMatch = part.match(/^(\d+)-(\d+)$/);
50
+ if (rangeMatch) {
51
+ const start = Number(rangeMatch[1]);
52
+ const end = Number(rangeMatch[2]);
53
+ return start <= value && value <= end;
54
+ }
55
+
56
+ const exact = Number(part);
57
+ return Number.isInteger(exact) && exact >= min && exact <= max && exact === value;
58
+ }
@@ -0,0 +1,28 @@
1
+ import type { RealtimeHub } from "../../realtime/hub";
2
+ import { createHeartbeatRunsRealtimeEvent } from "../../realtime/heartbeat-runs";
3
+
4
+ export function publishHeartbeatRunStatus(
5
+ realtimeHub: RealtimeHub | undefined,
6
+ input: {
7
+ companyId: string;
8
+ runId: string;
9
+ status: "started" | "completed" | "failed" | "skipped";
10
+ message?: string | null;
11
+ startedAt?: Date;
12
+ finishedAt?: Date;
13
+ }
14
+ ) {
15
+ if (!realtimeHub) {
16
+ return;
17
+ }
18
+ realtimeHub.publish(
19
+ createHeartbeatRunsRealtimeEvent(input.companyId, {
20
+ type: "run.status.updated",
21
+ runId: input.runId,
22
+ status: input.status,
23
+ message: input.message ?? null,
24
+ startedAt: input.startedAt?.toISOString(),
25
+ finishedAt: input.finishedAt?.toISOString() ?? null
26
+ })
27
+ );
28
+ }
@@ -0,0 +1,53 @@
1
+ const AGENT_COMMENT_EMOJI_REGEX = /[\p{Extended_Pictographic}\uFE0F\u200D]/gu;
2
+
3
+ export function sanitizeAgentSummaryCommentBody(body: string) {
4
+ const sanitized = body.replace(AGENT_COMMENT_EMOJI_REGEX, "").trim();
5
+ return sanitized.length > 0 ? sanitized : "Run update.";
6
+ }
7
+
8
+ function extractSummaryFromJsonLikeText(input: string) {
9
+ const fencedMatch = input.match(/```(?:json)?\s*([\s\S]*?)```/i);
10
+ const candidate = fencedMatch?.[1]?.trim() ?? input.match(/\{[\s\S]*\}\s*$/)?.[0]?.trim();
11
+ if (!candidate) {
12
+ return null;
13
+ }
14
+ try {
15
+ const parsed = JSON.parse(candidate) as Record<string, unknown>;
16
+ const summary = parsed.summary;
17
+ if (typeof summary === "string" && summary.trim().length > 0) {
18
+ return summary.trim();
19
+ }
20
+ } catch {
21
+ // Fall through to regex extraction for loosely-formatted JSON.
22
+ }
23
+ const summaryMatch = candidate.match(/"summary"\s*:\s*"([\s\S]*?)"/);
24
+ const summary = summaryMatch?.[1]
25
+ ?.replace(/\\"/g, "\"")
26
+ .replace(/\\n/g, " ")
27
+ .replace(/\s+/g, " ")
28
+ .trim();
29
+ return summary && summary.length > 0 ? summary : null;
30
+ }
31
+
32
+ export function extractNaturalRunUpdate(executionSummary: string) {
33
+ const normalized = executionSummary.trim();
34
+ const jsonSummary = extractSummaryFromJsonLikeText(normalized);
35
+ const source = jsonSummary ?? normalized;
36
+ const lines = source
37
+ .split("\n")
38
+ .map((line) => line.trim())
39
+ .filter((line) => line.length > 0)
40
+ .filter((line) => !line.startsWith("{") && !line.startsWith("}"));
41
+ const compact = (lines.length > 0 ? lines.slice(0, 2).join(" ") : source)
42
+ .replace(/^run (failure )?summary\s*:\s*/i, "")
43
+ .replace(/^completed all assigned issue steps\s*:\s*/i, "")
44
+ .replace(/^issue status\s*:\s*/i, "")
45
+ .replace(/`+/g, "")
46
+ .replace(/\s+/g, " ")
47
+ .trim();
48
+ const bounded = compact.length > 260 ? `${compact.slice(0, 257).trimEnd()}...` : compact;
49
+ if (!bounded) {
50
+ return "Run update.";
51
+ }
52
+ return /[.!?]$/.test(bounded) ? bounded : `${bounded}.`;
53
+ }