@wardby/cli 0.3.0 → 0.4.1

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 (275) hide show
  1. package/.env.example +22 -3
  2. package/README.md +11 -9
  3. package/dist/cli-help.d.ts +1 -1
  4. package/dist/cli-help.js +1 -0
  5. package/dist/cli.js +68 -10
  6. package/dist/coding/base-commit.d.ts +6 -0
  7. package/dist/coding/base-commit.js +12 -0
  8. package/dist/coding/protocol.d.ts +17 -1
  9. package/dist/coding/protocol.js +17 -6
  10. package/dist/coding/provider.d.ts +6 -1
  11. package/dist/coding/provider.js +16 -9
  12. package/dist/config/providers.d.ts +25 -0
  13. package/dist/config/providers.js +71 -0
  14. package/dist/core/attribution.d.ts +101 -0
  15. package/dist/core/attribution.js +208 -0
  16. package/dist/core/budget-groups.d.ts +17 -10
  17. package/dist/core/budget-groups.js +15 -12
  18. package/dist/core/coding-queue.d.ts +3 -0
  19. package/dist/core/coding-queue.js +6 -2
  20. package/dist/core/coding-service-status.d.ts +11 -0
  21. package/dist/core/coding-service-status.js +17 -0
  22. package/dist/core/cost-report.d.ts +88 -0
  23. package/dist/core/cost-report.js +248 -0
  24. package/dist/core/dispatch.d.ts +42 -2
  25. package/dist/core/dispatch.js +144 -26
  26. package/dist/core/engine-native.js +17 -4
  27. package/dist/core/glob.d.ts +10 -0
  28. package/dist/core/glob.js +33 -0
  29. package/dist/core/host-events.d.ts +24 -1
  30. package/dist/core/host-events.js +341 -1
  31. package/dist/core/host-status.d.ts +19 -2
  32. package/dist/core/host-status.js +47 -30
  33. package/dist/core/issue-bridge.d.ts +60 -0
  34. package/dist/core/issue-bridge.js +189 -0
  35. package/dist/core/issue-dedupe.d.ts +70 -0
  36. package/dist/core/issue-dedupe.js +255 -0
  37. package/dist/core/issue-events.d.ts +42 -0
  38. package/dist/core/issue-events.js +155 -0
  39. package/dist/core/issue-status.d.ts +29 -0
  40. package/dist/core/issue-status.js +241 -0
  41. package/dist/core/issue-tracker-tools.d.ts +64 -0
  42. package/dist/core/issue-tracker-tools.js +850 -0
  43. package/dist/core/model-usage.d.ts +10 -0
  44. package/dist/core/model-usage.js +24 -0
  45. package/dist/core/reconciler.d.ts +8 -4
  46. package/dist/core/reconciler.js +15 -4
  47. package/dist/core/review-host-tools.js +10 -3
  48. package/dist/core/run-pricing.d.ts +61 -0
  49. package/dist/core/run-pricing.js +56 -0
  50. package/dist/core/runner.d.ts +5 -2
  51. package/dist/core/runner.js +178 -27
  52. package/dist/core/scheduler.d.ts +4 -1
  53. package/dist/core/scheduler.js +3 -2
  54. package/dist/core/self-defects.d.ts +80 -0
  55. package/dist/core/self-defects.js +180 -0
  56. package/dist/core/tool-names.js +3 -0
  57. package/dist/core/webhooks.d.ts +9 -1
  58. package/dist/core/webhooks.js +19 -1
  59. package/dist/env.js +6 -1
  60. package/dist/generated/prisma/browser.d.ts +66 -0
  61. package/dist/generated/prisma/client.d.ts +66 -0
  62. package/dist/generated/prisma/commonInputTypes.d.ts +122 -52
  63. package/dist/generated/prisma/enums.d.ts +7 -0
  64. package/dist/generated/prisma/enums.js +6 -0
  65. package/dist/generated/prisma/internal/class.d.ts +99 -0
  66. package/dist/generated/prisma/internal/class.js +4 -4
  67. package/dist/generated/prisma/internal/prismaNamespace.d.ts +826 -1
  68. package/dist/generated/prisma/internal/prismaNamespace.js +135 -2
  69. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +142 -0
  70. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -2
  71. package/dist/generated/prisma/models/Agent.d.ts +389 -1
  72. package/dist/generated/prisma/models/AgentIssueProject.d.ts +1838 -0
  73. package/dist/generated/prisma/models/AgentIssueProject.js +1 -0
  74. package/dist/generated/prisma/models/AgentRepository.d.ts +1 -1
  75. package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
  76. package/dist/generated/prisma/models/CodingProxySession.d.ts +73 -1
  77. package/dist/generated/prisma/models/CodingRun.d.ts +130 -1
  78. package/dist/generated/prisma/models/CodingRunServiceStatus.d.ts +1404 -0
  79. package/dist/generated/prisma/models/CodingRunServiceStatus.js +1 -0
  80. package/dist/generated/prisma/models/IssueFingerprint.d.ts +1183 -0
  81. package/dist/generated/prisma/models/IssueFingerprint.js +1 -0
  82. package/dist/generated/prisma/models/IssuePullRequest.d.ts +1255 -0
  83. package/dist/generated/prisma/models/IssuePullRequest.js +1 -0
  84. package/dist/generated/prisma/models/ModelCatalogEntry.d.ts +1322 -0
  85. package/dist/generated/prisma/models/ModelCatalogEntry.js +1 -0
  86. package/dist/generated/prisma/models/Run.d.ts +933 -1
  87. package/dist/generated/prisma/models/RunAttribution.d.ts +1259 -0
  88. package/dist/generated/prisma/models/RunAttribution.js +1 -0
  89. package/dist/generated/prisma/models/RunIssueStatus.d.ts +1199 -0
  90. package/dist/generated/prisma/models/RunIssueStatus.js +1 -0
  91. package/dist/generated/prisma/models/RunModelUsage.d.ts +1316 -0
  92. package/dist/generated/prisma/models/RunModelUsage.js +1 -0
  93. package/dist/generated/prisma/models/WorkItem.d.ts +1408 -0
  94. package/dist/generated/prisma/models/WorkItem.js +1 -0
  95. package/dist/generated/prisma/models.d.ts +9 -0
  96. package/dist/help-index.json +355 -16
  97. package/dist/import/neutral-schema.d.ts +16 -16
  98. package/dist/knowledge/check.d.ts +13 -0
  99. package/dist/knowledge/check.js +69 -0
  100. package/dist/knowledge/cli.d.ts +14 -0
  101. package/dist/knowledge/cli.js +67 -0
  102. package/dist/knowledge/concept.d.ts +54 -0
  103. package/dist/knowledge/concept.js +78 -0
  104. package/dist/knowledge/note.d.ts +11 -0
  105. package/dist/knowledge/note.js +39 -0
  106. package/dist/knowledge/relevance.d.ts +11 -0
  107. package/dist/knowledge/relevance.js +14 -0
  108. package/dist/knowledge/span-hash.d.ts +3 -0
  109. package/dist/knowledge/span-hash.js +16 -0
  110. package/dist/mcp/auth/access.d.ts +4 -2
  111. package/dist/mcp/auth/ownership.d.ts +9 -9
  112. package/dist/mcp/auth/resource-server.d.ts +3 -1
  113. package/dist/mcp/auth/resource-server.js +18 -3
  114. package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
  115. package/dist/mcp/auth/self-hosted/session.d.ts +5 -5
  116. package/dist/mcp/context.d.ts +3 -0
  117. package/dist/mcp/host-events/deliveries.d.ts +9 -0
  118. package/dist/mcp/host-events/deliveries.js +17 -0
  119. package/dist/mcp/host-events/github-ingress.d.ts +4 -2
  120. package/dist/mcp/host-events/github-ingress.js +4 -13
  121. package/dist/mcp/host-events/jira-ingress.d.ts +29 -0
  122. package/dist/mcp/host-events/jira-ingress.js +92 -0
  123. package/dist/mcp/index.d.ts +2 -0
  124. package/dist/mcp/index.js +87 -9
  125. package/dist/mcp/server.js +5 -2
  126. package/dist/mcp/tools/agents.js +74 -3
  127. package/dist/mcp/tools/cost-report.d.ts +8 -0
  128. package/dist/mcp/tools/cost-report.js +60 -0
  129. package/dist/mcp/tools/issue-projects.d.ts +2 -0
  130. package/dist/mcp/tools/issue-projects.js +238 -0
  131. package/dist/mcp/tools/model-catalog.d.ts +22 -0
  132. package/dist/mcp/tools/model-catalog.js +423 -0
  133. package/dist/mcp/tools/repositories.js +2 -1
  134. package/dist/mcp/tools/tools.d.ts +2 -2
  135. package/dist/mcp/tools/trigger.js +33 -5
  136. package/dist/mcp/transport/streamable-http.d.ts +5 -0
  137. package/dist/mcp/transport/streamable-http.js +23 -1
  138. package/dist/mcp/webhooks/ingress.d.ts +2 -1
  139. package/dist/mcp/webhooks/ingress.js +9 -2
  140. package/dist/providers/auth/self-hosted.d.ts +8 -1
  141. package/dist/providers/auth/self-hosted.js +39 -2
  142. package/dist/providers/coding-proxy/memory-ledger.d.ts +1 -1
  143. package/dist/providers/coding-proxy/memory-ledger.js +10 -1
  144. package/dist/providers/coding-proxy/metering.d.ts +2 -1
  145. package/dist/providers/coding-proxy/metering.js +13 -2
  146. package/dist/providers/coding-proxy/prisma-ledger.js +59 -6
  147. package/dist/providers/coding-proxy/proxy.d.ts +12 -2
  148. package/dist/providers/coding-proxy/proxy.js +92 -30
  149. package/dist/providers/coding-proxy/types.d.ts +18 -1
  150. package/dist/providers/coding-proxy/types.js +12 -1
  151. package/dist/providers/engine/types.d.ts +19 -0
  152. package/dist/providers/executor/composition.js +9 -1
  153. package/dist/providers/executor/container.d.ts +30 -2
  154. package/dist/providers/executor/container.js +98 -17
  155. package/dist/providers/executor/dbos.d.ts +2 -0
  156. package/dist/providers/executor/dbos.js +7 -5
  157. package/dist/providers/executor/routing.d.ts +6 -0
  158. package/dist/providers/executor/routing.js +5 -0
  159. package/dist/providers/executor/types.d.ts +12 -0
  160. package/dist/providers/issue-tracker/adf.d.ts +31 -0
  161. package/dist/providers/issue-tracker/adf.js +181 -0
  162. package/dist/providers/issue-tracker/index.d.ts +5 -0
  163. package/dist/providers/issue-tracker/index.js +12 -0
  164. package/dist/providers/issue-tracker/jira-client.d.ts +41 -0
  165. package/dist/providers/issue-tracker/jira-client.js +151 -0
  166. package/dist/providers/issue-tracker/jira-events.d.ts +3 -0
  167. package/dist/providers/issue-tracker/jira-events.js +98 -0
  168. package/dist/providers/issue-tracker/jira.d.ts +116 -0
  169. package/dist/providers/issue-tracker/jira.js +502 -0
  170. package/dist/providers/issue-tracker/types.d.ts +269 -0
  171. package/dist/providers/issue-tracker/types.js +16 -0
  172. package/dist/providers/jobs/docker.d.ts +5 -1
  173. package/dist/providers/jobs/docker.js +61 -33
  174. package/dist/providers/jobs/kubernetes.d.ts +3 -0
  175. package/dist/providers/jobs/kubernetes.js +44 -4
  176. package/dist/providers/jobs/service-state.d.ts +22 -0
  177. package/dist/providers/jobs/service-state.js +17 -0
  178. package/dist/providers/llm/anthropic.d.ts +3 -3
  179. package/dist/providers/llm/anthropic.js +3 -9
  180. package/dist/providers/llm/bedrock.d.ts +3 -3
  181. package/dist/providers/llm/bedrock.js +3 -9
  182. package/dist/providers/llm/catalog-lookup.d.ts +10 -0
  183. package/dist/providers/llm/catalog-lookup.js +15 -0
  184. package/dist/providers/llm/catalog-shipped.d.ts +18 -0
  185. package/dist/providers/llm/catalog-shipped.js +197 -0
  186. package/dist/providers/llm/catalog-store.d.ts +58 -0
  187. package/dist/providers/llm/catalog-store.js +138 -0
  188. package/dist/providers/llm/catalog-types.d.ts +66 -0
  189. package/dist/providers/llm/catalog-types.js +64 -0
  190. package/dist/providers/llm/catalog.d.ts +61 -0
  191. package/dist/providers/llm/catalog.js +147 -0
  192. package/dist/providers/llm/claude-provider.d.ts +10 -14
  193. package/dist/providers/llm/claude-provider.js +11 -6
  194. package/dist/providers/llm/index.d.ts +9 -6
  195. package/dist/providers/llm/index.js +8 -5
  196. package/dist/providers/llm/openai.d.ts +14 -5
  197. package/dist/providers/llm/openai.js +24 -14
  198. package/dist/providers/llm/pricing-core.d.ts +5 -3
  199. package/dist/providers/llm/registration.js +8 -12
  200. package/dist/providers/llm/routing.d.ts +18 -17
  201. package/dist/providers/llm/routing.js +40 -24
  202. package/dist/providers/review-host/github-events.js +47 -1
  203. package/dist/providers/review-host/github.js +7 -6
  204. package/dist/providers/review-host/types.d.ts +25 -0
  205. package/dist/providers/vcs/git.js +2 -22
  206. package/dist/providers/vcs/github.d.ts +20 -0
  207. package/dist/providers/vcs/github.js +28 -2
  208. package/dist/providers/vcs/types.d.ts +6 -0
  209. package/dist/quickstart/index.d.ts +8 -0
  210. package/dist/quickstart/index.js +34 -34
  211. package/dist/serve.js +8 -2
  212. package/dist/viewer/api-schema.d.ts +2757 -0
  213. package/dist/viewer/api-schema.js +165 -0
  214. package/dist/viewer/build-schemas.d.ts +2 -0
  215. package/dist/viewer/build-schemas.js +18 -0
  216. package/dist/viewer/event-bus.d.ts +38 -0
  217. package/dist/viewer/event-bus.js +232 -0
  218. package/dist/viewer/graph.d.ts +40 -0
  219. package/dist/viewer/graph.js +243 -0
  220. package/dist/viewer/http.d.ts +30 -0
  221. package/dist/viewer/http.js +133 -0
  222. package/dist/viewer/run-detail.d.ts +4 -0
  223. package/dist/viewer/run-detail.js +61 -0
  224. package/dist/wardby-bin.js +5 -0
  225. package/docs/README.md +10 -0
  226. package/docs/agent-recipes.md +383 -0
  227. package/docs/code-review-agents.md +29 -2
  228. package/docs/coding-agent-setup.md +3 -0
  229. package/docs/coding-worker-isolation.md +39 -5
  230. package/docs/getting-started-gke.md +28 -11
  231. package/docs/getting-started-identity-provider.md +49 -38
  232. package/docs/getting-started.md +14 -0
  233. package/docs/jira-agents.md +649 -0
  234. package/docs/knowledge.md +387 -0
  235. package/docs/models.md +221 -0
  236. package/docs/security-deployment.md +19 -9
  237. package/docs/viewer-api.md +142 -0
  238. package/help/admin-viewer.md +39 -0
  239. package/help/agent-recipes.md +173 -0
  240. package/help/architecture-agent.md +189 -0
  241. package/help/builder-agent.md +80 -0
  242. package/help/code-review-agents.md +6 -0
  243. package/help/cost-attribution.md +67 -0
  244. package/help/creating-agents.md +22 -0
  245. package/help/deploy-gke.md +6 -0
  246. package/help/errors/model-unavailable.md +63 -0
  247. package/help/getting-started.md +1 -0
  248. package/help/github.md +18 -0
  249. package/help/identity-and-access.md +8 -3
  250. package/help/jira.md +135 -0
  251. package/help/knowledge.md +47 -0
  252. package/help/models.md +90 -0
  253. package/help/operating-agents.md +7 -1
  254. package/help/troubleshooting/budgets.md +6 -0
  255. package/package.json +5 -2
  256. package/prisma/migrations/20260930000000_jira_issue_projects/migration.sql +34 -0
  257. package/prisma/migrations/20261001000000_jira_phase2_allowlists/migration.sql +3 -0
  258. package/prisma/migrations/20261001010000_jira_link_types_allowlist/migration.sql +2 -0
  259. package/prisma/migrations/20261002000000_jira_coding_bridge/migration.sql +28 -0
  260. package/prisma/migrations/20261002010000_jira_issue_creation/migration.sql +25 -0
  261. package/prisma/migrations/20261003000000_issue_cost_attribution/migration.sql +56 -0
  262. package/prisma/migrations/20261003010000_coding_run_service_status/migration.sql +23 -0
  263. package/prisma/migrations/20261003020000_viewer_notify/migration.sql +54 -0
  264. package/prisma/migrations/20261003030000_viewer_notify_fixes/migration.sql +47 -0
  265. package/prisma/migrations/20261003040000_viewer_indexes/migration.sql +12 -0
  266. package/prisma/migrations/20261004000000_model_catalog/migration.sql +26 -0
  267. package/prisma/schema.prisma +258 -2
  268. package/dist/mcp/tools/models.d.ts +0 -8
  269. package/dist/mcp/tools/models.js +0 -15
  270. package/dist/providers/llm/pricing-anthropic.d.ts +0 -14
  271. package/dist/providers/llm/pricing-anthropic.js +0 -48
  272. package/dist/providers/llm/pricing-bedrock-claude.d.ts +0 -20
  273. package/dist/providers/llm/pricing-bedrock-claude.js +0 -46
  274. package/dist/providers/llm/pricing.d.ts +0 -30
  275. package/dist/providers/llm/pricing.js +0 -74
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The admin viewer API's contract (docs/viewer-api.md). Every response and
3
+ * live event the API sends is defined here; build-schemas.ts publishes them as
4
+ * JSON Schema under src/viewer/schemas/, which the viewer app generates its
5
+ * types from (it never imports server source). Change a shape here, run
6
+ * `npm run build:viewer-schemas`, and commit the regenerated files.
7
+ */
8
+ import { z } from "zod";
9
+ export const RunStatusSchema = z.enum([
10
+ "pending",
11
+ "running",
12
+ "succeeded",
13
+ "failed",
14
+ "refused",
15
+ "lost",
16
+ "budget_exhausted",
17
+ "cancelled",
18
+ ]);
19
+ /** What started a run, resolved for display. A sub-agent run's cause is its parent (parentRunId). */
20
+ export const RunTriggerSchema = z.discriminatedUnion("kind", [
21
+ z.object({ kind: z.literal("manual") }),
22
+ z.object({ kind: z.literal("scheduled"), schedule: z.string().nullable() }),
23
+ z.object({ kind: z.literal("webhook") }),
24
+ z.object({ kind: z.literal("subagent") }),
25
+ z.object({
26
+ kind: z.literal("code_host"),
27
+ provider: z.string(),
28
+ repository: z.string(),
29
+ number: z.number().int().nullable(),
30
+ event: z.enum(["review", "mention"]),
31
+ }),
32
+ z.object({ kind: z.literal("issue"), provider: z.string(), issueKey: z.string() }),
33
+ z.object({ kind: z.literal("host_event") }),
34
+ ]);
35
+ /** When the outcome happened (PR opened, comment last updated, check completed); null while a check is pending. */
36
+ const OutcomeAtSchema = z.string().nullable();
37
+ export const OutcomeSchema = z.discriminatedUnion("kind", [
38
+ z.object({
39
+ kind: z.literal("pull_request"),
40
+ provider: z.string(),
41
+ repository: z.string(),
42
+ number: z.number().int(),
43
+ url: z.string(),
44
+ state: z.string().nullable(),
45
+ at: OutcomeAtSchema,
46
+ }),
47
+ z.object({
48
+ kind: z.literal("code_host_comment"),
49
+ provider: z.string(),
50
+ repository: z.string(),
51
+ number: z.number().int(),
52
+ at: OutcomeAtSchema,
53
+ }),
54
+ z.object({ kind: z.literal("issue_comment"), provider: z.string(), issueKey: z.string(), at: OutcomeAtSchema }),
55
+ z.object({
56
+ kind: z.literal("check"),
57
+ provider: z.string(),
58
+ repository: z.string(),
59
+ number: z.number().int().nullable(),
60
+ completed: z.boolean(),
61
+ at: OutcomeAtSchema,
62
+ }),
63
+ ]);
64
+ export const ServiceStatusSchema = z.object({
65
+ name: z.string(),
66
+ state: z.enum(["pending", "probing", "ready", "failed"]),
67
+ attempts: z.number().int().nullable(),
68
+ reason: z.string().nullable(),
69
+ readyAt: z.string().nullable(),
70
+ failedAt: z.string().nullable(),
71
+ createdAt: z.string(),
72
+ });
73
+ export const GraphRunSchema = z.object({
74
+ id: z.string(),
75
+ parentRunId: z.string().nullable(),
76
+ agentId: z.string(),
77
+ agentName: z.string(),
78
+ agentKind: z.enum(["native", "coding"]),
79
+ /** The coding run's model, else the agent's. */
80
+ model: z.string(),
81
+ /** The coding worker (e.g. `codex`, `claude-code`); null for native runs. */
82
+ codingProvider: z.string().nullable(),
83
+ status: RunStatusSchema,
84
+ trigger: RunTriggerSchema,
85
+ turns: z.number().int(),
86
+ tokensIn: z.number().int(),
87
+ tokensOut: z.number().int(),
88
+ costUsd: z.number(),
89
+ budgetUsd: z.number(),
90
+ startedAt: z.string(),
91
+ finishedAt: z.string().nullable(),
92
+ heartbeatAt: z.string().nullable(),
93
+ outcomes: z.array(OutcomeSchema),
94
+ /** The services a coding run was started with, recorded or not; empty for native runs. */
95
+ declaredServices: z.array(z.object({ name: z.string(), version: z.string() })),
96
+ /** Recorded readiness of the run's services (none before status tracking existed). */
97
+ services: z.array(ServiceStatusSchema),
98
+ });
99
+ export const BudgetGroupSpendSchema = z.object({
100
+ id: z.string(),
101
+ name: z.string(),
102
+ dailyBudgetUsd: z.number().nullable(),
103
+ spentTodayUsd: z.number(),
104
+ });
105
+ export const GraphSnapshotSchema = z.object({
106
+ generatedAt: z.string(),
107
+ since: z.string(),
108
+ limit: z.number().int(),
109
+ truncated: z.boolean(),
110
+ runs: z.array(GraphRunSchema),
111
+ spend: z.object({ todayUsd: z.number(), groups: z.array(BudgetGroupSpendSchema) }),
112
+ });
113
+ export const RunDetailSchema = GraphRunSchema.extend({
114
+ error: z.string().nullable(),
115
+ finalText: z.string().nullable(),
116
+ childRunIds: z.array(z.string()),
117
+ coding: z
118
+ .object({
119
+ provider: z.string(),
120
+ repository: z.string(),
121
+ baseRef: z.string(),
122
+ headRef: z.string(),
123
+ queuedAt: z.string().nullable(),
124
+ failureCategory: z.string().nullable(),
125
+ services: z.array(z.object({
126
+ name: z.string(),
127
+ version: z.string(),
128
+ image: z.string(),
129
+ envNames: z.array(z.string()),
130
+ })),
131
+ })
132
+ .nullable(),
133
+ });
134
+ export const ViewerEventSchema = z.discriminatedUnion("kind", [
135
+ z.object({
136
+ kind: z.literal("run"),
137
+ runId: z.string(),
138
+ parentRunId: z.string().nullable(),
139
+ agentId: z.string(),
140
+ status: RunStatusSchema,
141
+ turns: z.number().int(),
142
+ tokensIn: z.number().int(),
143
+ tokensOut: z.number().int(),
144
+ costUsd: z.number(),
145
+ finishedAt: z.string().nullable(),
146
+ }),
147
+ z.object({
148
+ kind: z.literal("service"),
149
+ runId: z.string(),
150
+ name: z.string(),
151
+ state: z.enum(["pending", "probing", "ready", "failed"]),
152
+ attempts: z.number().int().nullable(),
153
+ }),
154
+ z.object({
155
+ kind: z.literal("outcome"),
156
+ runId: z.string(),
157
+ /** Which table changed (the viewer NOTIFY trigger names them). */
158
+ source: z.enum(["pull_request", "host_status", "issue_status", "host_check"]),
159
+ }),
160
+ ]);
161
+ export const VIEWER_SCHEMAS = {
162
+ "graph-snapshot": GraphSnapshotSchema,
163
+ "run-detail": RunDetailSchema,
164
+ "viewer-event": ViewerEventSchema,
165
+ };
@@ -0,0 +1,2 @@
1
+ export declare const SCHEMA_DIR: string;
2
+ export declare function renderSchemas(): Record<string, string>;
@@ -0,0 +1,18 @@
1
+ /** Writes src/viewer/schemas/<name>.schema.json from VIEWER_SCHEMAS (npm run build:viewer-schemas). */
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { zodToJsonSchema } from "zod-to-json-schema";
6
+ import { VIEWER_SCHEMAS } from "./api-schema.js";
7
+ export const SCHEMA_DIR = join(dirname(fileURLToPath(import.meta.url)), "schemas");
8
+ export function renderSchemas() {
9
+ return Object.fromEntries(Object.entries(VIEWER_SCHEMAS).map(([name, schema]) => [
10
+ `${name}.schema.json`,
11
+ `${JSON.stringify(zodToJsonSchema(schema, { name, $refStrategy: "none" }), null, 2)}\n`,
12
+ ]));
13
+ }
14
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
15
+ await mkdir(SCHEMA_DIR, { recursive: true });
16
+ for (const [file, text] of Object.entries(renderSchemas()))
17
+ await writeFile(join(SCHEMA_DIR, file), text);
18
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Viewer event bus: one raw Postgres connection per server replica that LISTENs
3
+ * on `wardby_viewer` (fed by the viewer NOTIFY triggers) and fans events out to
4
+ * in-process subscribers (the SSE layer).
5
+ *
6
+ * - Lazy: no connection until the first subscriber; closed when the last leaves.
7
+ * - Resilient: Cloud SQL maintenance and proxy restarts drop idle connections,
8
+ * so the client uses TCP keepAlive (probing after 30 s idle) and a periodic
9
+ * `SELECT 1` health check that catches a half-open socket; on error, end or a
10
+ * failed check while anyone is subscribed, it reconnects on a backoff
11
+ * schedule and re-LISTENs.
12
+ * - Subscribers are not told about gaps directly; `onState` reports every
13
+ * transition to live (including the first LISTEN) and to down, so the SSE
14
+ * layer can tell its clients to refetch whatever they may have missed.
15
+ */
16
+ import pg from "pg";
17
+ import { type ViewerEvent } from "./api-schema.js";
18
+ export declare const VIEWER_CHANNEL = "wardby_viewer";
19
+ export interface ViewerEventBus {
20
+ subscribe(listener: (event: ViewerEvent) => void): () => void;
21
+ /** True while the LISTEN connection is up (for the SSE "live" indicator). */
22
+ connected(): boolean;
23
+ /**
24
+ * Fires on every transition of the LISTEN connection: `true` once it is up
25
+ * and listening (the first connect and every reconnect), `false` when it is
26
+ * lost. Events between the last `false` (or subscribing) and `true` may
27
+ * have been missed.
28
+ */
29
+ onState(listener: (live: boolean) => void): () => void;
30
+ close(): Promise<void>;
31
+ }
32
+ export declare function createViewerEventBus(options: {
33
+ connectionString: string;
34
+ connect?: (connectionString: string) => pg.Client;
35
+ reconnectDelaysMs?: readonly number[];
36
+ healthCheckIntervalMs?: number;
37
+ healthCheckTimeoutMs?: number;
38
+ }): ViewerEventBus;
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Viewer event bus: one raw Postgres connection per server replica that LISTENs
3
+ * on `wardby_viewer` (fed by the viewer NOTIFY triggers) and fans events out to
4
+ * in-process subscribers (the SSE layer).
5
+ *
6
+ * - Lazy: no connection until the first subscriber; closed when the last leaves.
7
+ * - Resilient: Cloud SQL maintenance and proxy restarts drop idle connections,
8
+ * so the client uses TCP keepAlive (probing after 30 s idle) and a periodic
9
+ * `SELECT 1` health check that catches a half-open socket; on error, end or a
10
+ * failed check while anyone is subscribed, it reconnects on a backoff
11
+ * schedule and re-LISTENs.
12
+ * - Subscribers are not told about gaps directly; `onState` reports every
13
+ * transition to live (including the first LISTEN) and to down, so the SSE
14
+ * layer can tell its clients to refetch whatever they may have missed.
15
+ */
16
+ import pg from "pg";
17
+ import { poolSettings } from "../core/db.js";
18
+ import { logger } from "../core/logger.js";
19
+ import { ViewerEventSchema } from "./api-schema.js";
20
+ export const VIEWER_CHANNEL = "wardby_viewer";
21
+ const DEFAULT_RECONNECT_DELAYS_MS = [500, 1000, 2000, 5000, 10_000];
22
+ const WARN_INTERVAL_MS = 60_000;
23
+ const KEEPALIVE_INITIAL_DELAY_MS = 30_000;
24
+ const DEFAULT_HEALTH_CHECK_INTERVAL_MS = 60_000;
25
+ const DEFAULT_HEALTH_CHECK_TIMEOUT_MS = 10_000;
26
+ /** Upper bound on waiting for a graceful disconnect (a half-open socket never answers). */
27
+ const END_TIMEOUT_MS = 2_000;
28
+ export function createViewerEventBus(options) {
29
+ const healthIntervalMs = options.healthCheckIntervalMs ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS;
30
+ const healthTimeoutMs = options.healthCheckTimeoutMs ?? DEFAULT_HEALTH_CHECK_TIMEOUT_MS;
31
+ const delays = options.reconnectDelaysMs?.length ? options.reconnectDelaysMs : DEFAULT_RECONNECT_DELAYS_MS;
32
+ const makeClient = options.connect ??
33
+ ((connectionString) => new pg.Client({
34
+ connectionString,
35
+ keepAlive: true,
36
+ keepAliveInitialDelayMillis: KEEPALIVE_INITIAL_DELAY_MS,
37
+ connectionTimeoutMillis: poolSettings(connectionString).connectionTimeoutMillis,
38
+ }));
39
+ const listeners = new Set();
40
+ const stateListeners = new Set();
41
+ let client = null; // the current attempt/connection
42
+ let isUp = false;
43
+ let closed = false;
44
+ let dropped = false; // a connection was lost (or failed) in this session
45
+ let attempt = 0;
46
+ let timer = null;
47
+ let healthTimer = null;
48
+ const lastWarn = new Map();
49
+ function warnThrottled(key, message, extra = {}) {
50
+ const now = Date.now();
51
+ if (now - (lastWarn.get(key) ?? -Infinity) < WARN_INTERVAL_MS)
52
+ return;
53
+ lastWarn.set(key, now);
54
+ logger.warn(extra, message);
55
+ }
56
+ function dispatch(payload) {
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(payload ?? "");
60
+ }
61
+ catch {
62
+ warnThrottled("invalid", "viewer event bus dropped a non-JSON notification");
63
+ return;
64
+ }
65
+ const result = ViewerEventSchema.safeParse(parsed);
66
+ if (!result.success) {
67
+ warnThrottled("invalid", "viewer event bus dropped an invalid notification");
68
+ return;
69
+ }
70
+ for (const listener of [...listeners]) {
71
+ try {
72
+ listener(result.data);
73
+ }
74
+ catch (err) {
75
+ warnThrottled("listener", "viewer event bus listener threw", { err });
76
+ }
77
+ }
78
+ }
79
+ function setLive(live) {
80
+ if (isUp === live)
81
+ return;
82
+ isUp = live;
83
+ for (const l of [...stateListeners]) {
84
+ try {
85
+ l(live);
86
+ }
87
+ catch (err) {
88
+ warnThrottled("listener", "viewer event bus state listener threw", { err });
89
+ }
90
+ }
91
+ }
92
+ /** Disconnect, bounded: `end()` waits for the server, which a half-open socket never answers. */
93
+ function endClient(c) {
94
+ // Never leave a client without an error handler: late errors must not crash.
95
+ c.on("error", () => { });
96
+ let bound;
97
+ return Promise.race([
98
+ c.end().catch(() => { }),
99
+ new Promise((resolve) => {
100
+ bound = setTimeout(resolve, END_TIMEOUT_MS);
101
+ bound.unref();
102
+ }),
103
+ ]).finally(() => clearTimeout(bound));
104
+ }
105
+ function discard(c) {
106
+ void endClient(c);
107
+ }
108
+ function stopHealthCheck() {
109
+ if (healthTimer)
110
+ clearInterval(healthTimer);
111
+ healthTimer = null;
112
+ }
113
+ /** Periodically prove the LISTEN connection still reaches the server. */
114
+ function startHealthCheck(c) {
115
+ stopHealthCheck();
116
+ let inFlight = false;
117
+ healthTimer = setInterval(() => {
118
+ if (inFlight || c !== client)
119
+ return;
120
+ inFlight = true;
121
+ let deadline;
122
+ Promise.race([
123
+ c.query("SELECT 1"),
124
+ new Promise((_, reject) => {
125
+ deadline = setTimeout(() => reject(new Error("viewer event bus health check timed out")), healthTimeoutMs);
126
+ deadline.unref();
127
+ }),
128
+ ])
129
+ .then(() => { }, (err) => drop(c, err))
130
+ .finally(() => {
131
+ clearTimeout(deadline);
132
+ inFlight = false;
133
+ });
134
+ }, healthIntervalMs);
135
+ healthTimer.unref();
136
+ }
137
+ function scheduleReconnect() {
138
+ if (closed || listeners.size === 0 || timer || client)
139
+ return;
140
+ const delay = delays[Math.min(attempt, delays.length - 1)];
141
+ attempt++;
142
+ timer = setTimeout(() => {
143
+ timer = null;
144
+ open();
145
+ }, delay);
146
+ timer.unref();
147
+ }
148
+ function drop(c, err) {
149
+ if (c !== client)
150
+ return; // stale: already replaced or stopped
151
+ client = null;
152
+ stopHealthCheck();
153
+ dropped = true;
154
+ warnThrottled("connection", "viewer event bus connection lost", err ? { err } : {});
155
+ setLive(false);
156
+ discard(c);
157
+ scheduleReconnect();
158
+ }
159
+ function open() {
160
+ if (closed || client || timer || listeners.size === 0)
161
+ return;
162
+ const c = makeClient(options.connectionString);
163
+ client = c;
164
+ c.on("notification", (msg) => {
165
+ if (c === client && msg.channel === VIEWER_CHANNEL)
166
+ dispatch(msg.payload);
167
+ });
168
+ c.on("error", (err) => drop(c, err));
169
+ c.on("end", () => drop(c));
170
+ void (async () => {
171
+ try {
172
+ await c.connect();
173
+ if (c !== client)
174
+ return discard(c); // stopped while connecting
175
+ await c.query(`LISTEN ${VIEWER_CHANNEL}`);
176
+ if (c !== client)
177
+ return discard(c);
178
+ }
179
+ catch (err) {
180
+ return drop(c, err);
181
+ }
182
+ attempt = 0;
183
+ if (dropped) {
184
+ dropped = false;
185
+ logger.info({ channel: VIEWER_CHANNEL }, "viewer event bus reconnected");
186
+ }
187
+ startHealthCheck(c);
188
+ setLive(true);
189
+ })();
190
+ }
191
+ /** Tear down the connection and any pending reconnect. */
192
+ async function stop() {
193
+ if (timer)
194
+ clearTimeout(timer);
195
+ timer = null;
196
+ stopHealthCheck();
197
+ const c = client;
198
+ client = null;
199
+ dropped = false;
200
+ attempt = 0;
201
+ setLive(false);
202
+ if (c)
203
+ await endClient(c);
204
+ }
205
+ return {
206
+ subscribe(listener) {
207
+ if (closed)
208
+ return () => { };
209
+ listeners.add(listener);
210
+ open();
211
+ return () => {
212
+ if (!listeners.delete(listener))
213
+ return;
214
+ if (listeners.size === 0)
215
+ void stop();
216
+ };
217
+ },
218
+ connected: () => isUp,
219
+ onState(listener) {
220
+ stateListeners.add(listener);
221
+ return () => {
222
+ stateListeners.delete(listener);
223
+ };
224
+ },
225
+ async close() {
226
+ closed = true;
227
+ listeners.clear();
228
+ stateListeners.clear();
229
+ await stop();
230
+ },
231
+ };
232
+ }
@@ -0,0 +1,40 @@
1
+ /** The graph snapshot read model behind GET /admin/api/graph (docs/viewer-api.md). */
2
+ import type { Prisma, PrismaClient } from "#prisma";
3
+ import type { GraphRun, GraphSnapshot } from "./api-schema.js";
4
+ export declare const DEFAULT_GRAPH_LIMIT = 500;
5
+ export declare const MAX_GRAPH_LIMIT = 2000;
6
+ /** Accepts "15m" | "1h" | "6h" | "24h" | "7d" or an ISO-8601 timestamp; null defaults to 1h. Throws otherwise. */
7
+ export declare function parseSince(value: string | null, now: Date): Date;
8
+ declare const runInclude: {
9
+ agent: true;
10
+ codingRun: {
11
+ select: {
12
+ result: true;
13
+ provider: true;
14
+ model: true;
15
+ services: true;
16
+ };
17
+ };
18
+ hostCheck: true;
19
+ hostStatus: true;
20
+ issueStatus: true;
21
+ serviceStatuses: {
22
+ orderBy: {
23
+ createdAt: "asc";
24
+ };
25
+ };
26
+ };
27
+ type RunRow = Prisma.RunGetPayload<{
28
+ include: typeof runInclude;
29
+ }>;
30
+ type PullRequestRow = Prisma.IssuePullRequestGetPayload<object>;
31
+ /** The one row -> GraphRun mapping (also used by run detail). `pullRequests` are the IssuePullRequest rows this run opened. */
32
+ export declare function toGraphRun(row: RunRow, pullRequests: readonly PullRequestRow[]): GraphRun;
33
+ /** Loads the given runs as GraphRuns (shared with run detail); unknown ids are skipped. */
34
+ export declare function loadGraphRuns(db: PrismaClient, runIds: readonly string[]): Promise<GraphRun[]>;
35
+ export declare function loadGraph(db: PrismaClient, options: {
36
+ since: Date;
37
+ limit: number;
38
+ now?: Date;
39
+ }): Promise<GraphSnapshot>;
40
+ export {};