harness-dispatch 0.4.0

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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +582 -0
  3. package/config.default.yaml +397 -0
  4. package/data/coding_benchmarks.json +431 -0
  5. package/dist/auth.d.ts +9 -0
  6. package/dist/auth.d.ts.map +1 -0
  7. package/dist/auth.js +73 -0
  8. package/dist/auth.js.map +1 -0
  9. package/dist/billing.d.ts +7 -0
  10. package/dist/billing.d.ts.map +1 -0
  11. package/dist/billing.js +201 -0
  12. package/dist/billing.js.map +1 -0
  13. package/dist/bin.d.ts +6 -0
  14. package/dist/bin.d.ts.map +1 -0
  15. package/dist/bin.js +488 -0
  16. package/dist/bin.js.map +1 -0
  17. package/dist/breaker-store.d.ts +31 -0
  18. package/dist/breaker-store.d.ts.map +1 -0
  19. package/dist/breaker-store.js +103 -0
  20. package/dist/breaker-store.js.map +1 -0
  21. package/dist/circuit-breaker.d.ts +47 -0
  22. package/dist/circuit-breaker.d.ts.map +1 -0
  23. package/dist/circuit-breaker.js +102 -0
  24. package/dist/circuit-breaker.js.map +1 -0
  25. package/dist/config.d.ts +48 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +1248 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/dashboard/live.d.ts +49 -0
  30. package/dist/dashboard/live.d.ts.map +1 -0
  31. package/dist/dashboard/live.js +149 -0
  32. package/dist/dashboard/live.js.map +1 -0
  33. package/dist/dispatch-log.d.ts +46 -0
  34. package/dist/dispatch-log.d.ts.map +1 -0
  35. package/dist/dispatch-log.js +89 -0
  36. package/dist/dispatch-log.js.map +1 -0
  37. package/dist/dispatchers/base.d.ts +55 -0
  38. package/dist/dispatchers/base.d.ts.map +1 -0
  39. package/dist/dispatchers/base.js +80 -0
  40. package/dist/dispatchers/base.js.map +1 -0
  41. package/dist/dispatchers/generic-cli.d.ts +30 -0
  42. package/dist/dispatchers/generic-cli.d.ts.map +1 -0
  43. package/dist/dispatchers/generic-cli.js +471 -0
  44. package/dist/dispatchers/generic-cli.js.map +1 -0
  45. package/dist/dispatchers/openai-compatible.d.ts +57 -0
  46. package/dist/dispatchers/openai-compatible.d.ts.map +1 -0
  47. package/dist/dispatchers/openai-compatible.js +662 -0
  48. package/dist/dispatchers/openai-compatible.js.map +1 -0
  49. package/dist/dispatchers/shared/kill-tree.d.ts +21 -0
  50. package/dist/dispatchers/shared/kill-tree.d.ts.map +1 -0
  51. package/dist/dispatchers/shared/kill-tree.js +35 -0
  52. package/dist/dispatchers/shared/kill-tree.js.map +1 -0
  53. package/dist/dispatchers/shared/rate-limit-headers.d.ts +22 -0
  54. package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -0
  55. package/dist/dispatchers/shared/rate-limit-headers.js +124 -0
  56. package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -0
  57. package/dist/dispatchers/shared/stream-subprocess.d.ts +53 -0
  58. package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -0
  59. package/dist/dispatchers/shared/stream-subprocess.js +274 -0
  60. package/dist/dispatchers/shared/stream-subprocess.js.map +1 -0
  61. package/dist/dispatchers/shared/subprocess.d.ts +25 -0
  62. package/dist/dispatchers/shared/subprocess.d.ts.map +1 -0
  63. package/dist/dispatchers/shared/subprocess.js +110 -0
  64. package/dist/dispatchers/shared/subprocess.js.map +1 -0
  65. package/dist/dispatchers/shared/which-available.d.ts +2 -0
  66. package/dist/dispatchers/shared/which-available.d.ts.map +1 -0
  67. package/dist/dispatchers/shared/which-available.js +8 -0
  68. package/dist/dispatchers/shared/which-available.js.map +1 -0
  69. package/dist/dispatchers/shared/windows-cmd.d.ts +38 -0
  70. package/dist/dispatchers/shared/windows-cmd.d.ts.map +1 -0
  71. package/dist/dispatchers/shared/windows-cmd.js +91 -0
  72. package/dist/dispatchers/shared/windows-cmd.js.map +1 -0
  73. package/dist/http/server.d.ts +15 -0
  74. package/dist/http/server.d.ts.map +1 -0
  75. package/dist/http/server.js +495 -0
  76. package/dist/http/server.js.map +1 -0
  77. package/dist/index.d.ts +27 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +29 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/job-runner.d.ts +19 -0
  82. package/dist/job-runner.d.ts.map +1 -0
  83. package/dist/job-runner.js +42 -0
  84. package/dist/job-runner.js.map +1 -0
  85. package/dist/jobs.d.ts +116 -0
  86. package/dist/jobs.d.ts.map +1 -0
  87. package/dist/jobs.js +542 -0
  88. package/dist/jobs.js.map +1 -0
  89. package/dist/leaderboard.d.ts +62 -0
  90. package/dist/leaderboard.d.ts.map +1 -0
  91. package/dist/leaderboard.js +287 -0
  92. package/dist/leaderboard.js.map +1 -0
  93. package/dist/mcp/config-hot-reload.d.ts +64 -0
  94. package/dist/mcp/config-hot-reload.d.ts.map +1 -0
  95. package/dist/mcp/config-hot-reload.js +144 -0
  96. package/dist/mcp/config-hot-reload.js.map +1 -0
  97. package/dist/mcp/dispatcher-factory.d.ts +20 -0
  98. package/dist/mcp/dispatcher-factory.d.ts.map +1 -0
  99. package/dist/mcp/dispatcher-factory.js +30 -0
  100. package/dist/mcp/dispatcher-factory.js.map +1 -0
  101. package/dist/mcp/resources.d.ts +8 -0
  102. package/dist/mcp/resources.d.ts.map +1 -0
  103. package/dist/mcp/resources.js +45 -0
  104. package/dist/mcp/resources.js.map +1 -0
  105. package/dist/mcp/server.d.ts +36 -0
  106. package/dist/mcp/server.d.ts.map +1 -0
  107. package/dist/mcp/server.js +80 -0
  108. package/dist/mcp/server.js.map +1 -0
  109. package/dist/mcp/tools.d.ts +171 -0
  110. package/dist/mcp/tools.d.ts.map +1 -0
  111. package/dist/mcp/tools.js +658 -0
  112. package/dist/mcp/tools.js.map +1 -0
  113. package/dist/observability/index.d.ts +52 -0
  114. package/dist/observability/index.d.ts.map +1 -0
  115. package/dist/observability/index.js +111 -0
  116. package/dist/observability/index.js.map +1 -0
  117. package/dist/observability/spans.d.ts +47 -0
  118. package/dist/observability/spans.d.ts.map +1 -0
  119. package/dist/observability/spans.js +72 -0
  120. package/dist/observability/spans.js.map +1 -0
  121. package/dist/quota.d.ts +91 -0
  122. package/dist/quota.d.ts.map +1 -0
  123. package/dist/quota.js +331 -0
  124. package/dist/quota.js.map +1 -0
  125. package/dist/route-policy.d.ts +24 -0
  126. package/dist/route-policy.d.ts.map +1 -0
  127. package/dist/route-policy.js +92 -0
  128. package/dist/route-policy.js.map +1 -0
  129. package/dist/router.d.ts +162 -0
  130. package/dist/router.d.ts.map +1 -0
  131. package/dist/router.js +878 -0
  132. package/dist/router.js.map +1 -0
  133. package/dist/safety.d.ts +7 -0
  134. package/dist/safety.d.ts.map +1 -0
  135. package/dist/safety.js +33 -0
  136. package/dist/safety.js.map +1 -0
  137. package/dist/status.d.ts +100 -0
  138. package/dist/status.d.ts.map +1 -0
  139. package/dist/status.js +228 -0
  140. package/dist/status.js.map +1 -0
  141. package/dist/types.d.ts +402 -0
  142. package/dist/types.d.ts.map +1 -0
  143. package/dist/types.js +9 -0
  144. package/dist/types.js.map +1 -0
  145. package/dist/version.d.ts +2 -0
  146. package/dist/version.d.ts.map +1 -0
  147. package/dist/version.js +13 -0
  148. package/dist/version.js.map +1 -0
  149. package/dist/working-dir.d.ts +15 -0
  150. package/dist/working-dir.d.ts.map +1 -0
  151. package/dist/working-dir.js +22 -0
  152. package/dist/working-dir.js.map +1 -0
  153. package/dist/workspaces.d.ts +19 -0
  154. package/dist/workspaces.d.ts.map +1 -0
  155. package/dist/workspaces.js +352 -0
  156. package/dist/workspaces.js.map +1 -0
  157. package/package.json +96 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Leaderboard-based quality scoring for harness-dispatch.
3
+ *
4
+ * Ported from `coding_agent.leaderboard`. Fetches Arena ELO scores from the
5
+ * public wulong.dev API with a 24-hour cache. Scores are used as routing
6
+ * quality multipliers — higher ELO → higher routing priority within the
7
+ * same tier.
8
+ *
9
+ * API reference: https://blog.wulong.dev/posts/i-built-an-auto-updating-archive-of-every-ai-arena-leaderboard/
10
+ * Endpoint: https://api.wulong.dev/arena-ai-leaderboards/v1/leaderboard?name=code
11
+ */
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ // ---------------------------------------------------------------------------
16
+ // Constants — load-bearing, preserved from Python
17
+ // ---------------------------------------------------------------------------
18
+ export const LEADERBOARD_URL = "https://api.wulong.dev/arena-ai-leaderboards/v1/leaderboard?name=code";
19
+ export const CACHE_TTL_MS = 24 * 3600 * 1000; // 24 hours
20
+ export const FETCH_TIMEOUT_MS = 8000;
21
+ // Tier auto-derivation thresholds (Arena ELO, calibrated April 2026)
22
+ export const TIER1_ELO_MIN = 1350;
23
+ export const TIER2_ELO_MIN = 1200;
24
+ // High-thinking threshold relaxation
25
+ export const THINKING_THRESHOLD_BOOST = 25;
26
+ // Thinking level score multipliers
27
+ export const THINKING_MULTIPLIERS = {
28
+ high: 1.15,
29
+ medium: 1.07,
30
+ low: 1.0,
31
+ };
32
+ // ELO normalization range → quality_score in [QUALITY_MIN, QUALITY_MAX]
33
+ export const ELO_NORM_MIN = 1000;
34
+ export const ELO_NORM_MAX = 1600;
35
+ export const QUALITY_MIN = 0.6;
36
+ export const QUALITY_MAX = 1.0;
37
+ export const QUALITY_DEFAULT = 0.85;
38
+ // User-Agent required — API returns 403 without it
39
+ const USER_AGENT = "harness-dispatch/0.4 (leaderboard quality scoring)";
40
+ // ---------------------------------------------------------------------------
41
+ // Benchmark file resolution
42
+ // ---------------------------------------------------------------------------
43
+ /**
44
+ * Resolve `data/coding_benchmarks.json` relative to this module.
45
+ *
46
+ * Walks up from wherever this module lives (dist/ when built, src/ when
47
+ * running via tsx) to find the package root containing `data/`.
48
+ */
49
+ function resolveBenchmarkPath() {
50
+ const here = path.dirname(fileURLToPath(import.meta.url));
51
+ // Walk up a few levels looking for data/coding_benchmarks.json.
52
+ // Handles src/, dist/, or deeper nesting.
53
+ let dir = here;
54
+ for (let i = 0; i < 6; i += 1) {
55
+ const candidate = path.join(dir, "data", "coding_benchmarks.json");
56
+ if (existsSync(candidate)) {
57
+ return candidate;
58
+ }
59
+ const parent = path.dirname(dir);
60
+ if (parent === dir) {
61
+ break;
62
+ }
63
+ dir = parent;
64
+ }
65
+ // Fall back to a "reasonable" path — caller just sees an empty benchmark map.
66
+ return path.join(here, "..", "data", "coding_benchmarks.json");
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // Helpers (exposed for testing)
70
+ // ---------------------------------------------------------------------------
71
+ export function normalizeElo(elo) {
72
+ let ratio = (elo - ELO_NORM_MIN) / (ELO_NORM_MAX - ELO_NORM_MIN);
73
+ ratio = Math.max(0, Math.min(1, ratio));
74
+ return QUALITY_MIN + (QUALITY_MAX - QUALITY_MIN) * ratio;
75
+ }
76
+ /**
77
+ * Case-insensitive partial-match lookup.
78
+ *
79
+ * Three-tier fallback (identical to Python `_fuzzy_match`):
80
+ * 1. Exact match after lowercasing.
81
+ * 2. Query is a substring of a leaderboard name → prefer the shortest
82
+ * match (most specific entry that still contains the query).
83
+ * 3. All query words appear in the leaderboard name (order-insensitive) →
84
+ * again prefer the shortest match.
85
+ */
86
+ export function fuzzyMatch(query, scores) {
87
+ const q = query.toLowerCase().trim();
88
+ if (!q) {
89
+ return null;
90
+ }
91
+ // 1. Exact
92
+ if (Object.prototype.hasOwnProperty.call(scores, q)) {
93
+ return scores[q] ?? null;
94
+ }
95
+ // 2. Substring — query appears inside a leaderboard entry; prefer shortest.
96
+ const substrHits = [];
97
+ for (const [k, v] of Object.entries(scores)) {
98
+ if (k.includes(q)) {
99
+ substrHits.push([k, v]);
100
+ }
101
+ }
102
+ if (substrHits.length > 0) {
103
+ substrHits.sort((a, b) => a[0].length - b[0].length);
104
+ return substrHits[0][1];
105
+ }
106
+ // 3. All words appear in entry; prefer shortest.
107
+ const words = q.split(/\s+/).filter(Boolean);
108
+ const wordHits = [];
109
+ for (const [k, v] of Object.entries(scores)) {
110
+ if (words.every((w) => k.includes(w))) {
111
+ wordHits.push([k, v]);
112
+ }
113
+ }
114
+ if (wordHits.length > 0) {
115
+ wordHits.sort((a, b) => a[0].length - b[0].length);
116
+ return wordHits[0][1];
117
+ }
118
+ return null;
119
+ }
120
+ export class LeaderboardCache {
121
+ data = {};
122
+ fetchedAt = 0; // epoch ms
123
+ fetchFailed = false;
124
+ /** In-flight fetch promise — callers await it to serialize access. */
125
+ inflight = null;
126
+ benchmark = {};
127
+ benchmarkLoadedFlag = false;
128
+ benchmarkPath;
129
+ constructor(benchmarkPath) {
130
+ this.benchmarkPath = benchmarkPath ?? resolveBenchmarkPath();
131
+ this.loadBenchmarkFileSync();
132
+ }
133
+ // ------------------------------------------------------------------
134
+ // Public surface
135
+ // ------------------------------------------------------------------
136
+ benchmarkLoaded() {
137
+ return this.benchmarkLoadedFlag;
138
+ }
139
+ cacheAgeMs() {
140
+ if (this.fetchedAt === 0) {
141
+ return null;
142
+ }
143
+ return Date.now() - this.fetchedAt;
144
+ }
145
+ async getScores() {
146
+ await this.ensureFetched();
147
+ return this.data;
148
+ }
149
+ async getElo(leaderboardModel) {
150
+ if (!leaderboardModel) {
151
+ return null;
152
+ }
153
+ const scores = await this.getScores();
154
+ if (Object.keys(scores).length === 0) {
155
+ return null;
156
+ }
157
+ return fuzzyMatch(leaderboardModel, scores);
158
+ }
159
+ async getQualityScore(leaderboardModel, thinkingLevel) {
160
+ const mult = (thinkingLevel && THINKING_MULTIPLIERS[thinkingLevel]) ?? 1.0;
161
+ // 1. Blended benchmark file
162
+ if (leaderboardModel && this.benchmarkLoadedFlag) {
163
+ const bs = fuzzyMatch(leaderboardModel, this.benchmark);
164
+ if (bs !== null) {
165
+ const elo = await this.getElo(leaderboardModel);
166
+ return { qualityScore: bs * mult, elo };
167
+ }
168
+ }
169
+ // 2. Live Arena ELO
170
+ const elo = leaderboardModel ? await this.getElo(leaderboardModel) : null;
171
+ if (elo !== null) {
172
+ return { qualityScore: normalizeElo(elo) * mult, elo };
173
+ }
174
+ // 3. Default
175
+ return { qualityScore: QUALITY_DEFAULT * mult, elo: null };
176
+ }
177
+ async autoTier(leaderboardModel, thinkingLevel, fallbackTier) {
178
+ const elo = leaderboardModel ? await this.getElo(leaderboardModel) : null;
179
+ if (elo === null) {
180
+ return fallbackTier;
181
+ }
182
+ const boost = thinkingLevel === "high" ? THINKING_THRESHOLD_BOOST : 0;
183
+ if (elo + boost >= TIER1_ELO_MIN) {
184
+ return 1;
185
+ }
186
+ if (elo + boost >= TIER2_ELO_MIN) {
187
+ return 2;
188
+ }
189
+ return 3;
190
+ }
191
+ // ------------------------------------------------------------------
192
+ // Internal — fetch coordination
193
+ // ------------------------------------------------------------------
194
+ /** Ensure a fresh fetch has happened (or is in-flight); safe to call concurrently. */
195
+ async ensureFetched() {
196
+ const age = Date.now() - this.fetchedAt;
197
+ if (Object.keys(this.data).length > 0 && age < CACHE_TTL_MS) {
198
+ return;
199
+ }
200
+ if (this.fetchFailed && age < CACHE_TTL_MS) {
201
+ // Suppress repeated retries within the same TTL window.
202
+ return;
203
+ }
204
+ if (this.inflight) {
205
+ await this.inflight;
206
+ return;
207
+ }
208
+ this.inflight = this.doFetch().finally(() => {
209
+ this.inflight = null;
210
+ });
211
+ await this.inflight;
212
+ }
213
+ async doFetch() {
214
+ try {
215
+ const controller = new AbortController();
216
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
217
+ let response;
218
+ try {
219
+ response = await fetch(LEADERBOARD_URL, {
220
+ headers: { "User-Agent": USER_AGENT },
221
+ signal: controller.signal,
222
+ });
223
+ }
224
+ finally {
225
+ clearTimeout(timer);
226
+ }
227
+ if (!response.ok) {
228
+ this.fetchFailed = true;
229
+ // Update fetchedAt so the failure-suppression window starts now.
230
+ this.fetchedAt = Date.now();
231
+ return;
232
+ }
233
+ const payload = (await response.json());
234
+ const models = Array.isArray(payload.models) ? payload.models : [];
235
+ const parsed = {};
236
+ for (const m of models) {
237
+ if (typeof m.model === "string" && typeof m.score === "number") {
238
+ parsed[m.model.toLowerCase()] = m.score;
239
+ }
240
+ }
241
+ if (Object.keys(parsed).length > 0) {
242
+ this.data = parsed;
243
+ this.fetchedAt = Date.now();
244
+ this.fetchFailed = false;
245
+ }
246
+ else {
247
+ this.fetchFailed = true;
248
+ this.fetchedAt = Date.now();
249
+ }
250
+ }
251
+ catch {
252
+ this.fetchFailed = true;
253
+ this.fetchedAt = Date.now();
254
+ // Return stale data rather than crashing routing.
255
+ }
256
+ }
257
+ // ------------------------------------------------------------------
258
+ // Benchmark file
259
+ // ------------------------------------------------------------------
260
+ loadBenchmarkFileSync() {
261
+ // Sync load at construction to match Python behaviour (loaded once at
262
+ // startup). Swallow all errors — missing/malformed file just falls back
263
+ // to the Arena API.
264
+ try {
265
+ if (!existsSync(this.benchmarkPath)) {
266
+ return;
267
+ }
268
+ // Sync IO here is acceptable: constructor runs once at startup.
269
+ const raw = readFileSync(this.benchmarkPath, "utf-8");
270
+ const parsed = JSON.parse(raw);
271
+ const out = {};
272
+ const models = parsed.models ?? {};
273
+ for (const [k, v] of Object.entries(models)) {
274
+ const score = v?.coding_score;
275
+ if (typeof score === "number") {
276
+ out[k.toLowerCase()] = score;
277
+ }
278
+ }
279
+ this.benchmark = out;
280
+ this.benchmarkLoadedFlag = Object.keys(out).length > 0;
281
+ }
282
+ catch {
283
+ // Malformed — silently fall back.
284
+ }
285
+ }
286
+ }
287
+ //# sourceMappingURL=leaderboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leaderboard.js","sourceRoot":"","sources":["../src/leaderboard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Config hot-reload for the MCP server.
3
+ *
4
+ * Ported from Python `server.py:_maybe_reload_config`. Rather than file-system
5
+ * watchers (which are flaky on Windows + WSL mounts), we poll the config
6
+ * file's mtime between tool calls. A reload rebuilds the dispatcher map,
7
+ * quota cache, and router — while preserving circuit-breaker state for any
8
+ * service that still exists in the new config.
9
+ *
10
+ * Concurrency: the reload is guarded by a mutex so that simultaneous
11
+ * tool-calls can't race on global state replacement. Only the first caller
12
+ * performs the reload; the rest wait behind the mutex and pick up the
13
+ * already-refreshed state.
14
+ */
15
+ import { LeaderboardCache } from "../leaderboard.js";
16
+ import { QuotaCache } from "../quota.js";
17
+ import { Router } from "../router.js";
18
+ import type { RouterConfig } from "../types.js";
19
+ import { type DispatcherMap } from "./dispatcher-factory.js";
20
+ export interface RuntimeState {
21
+ config: RouterConfig;
22
+ dispatchers: DispatcherMap;
23
+ quota: QuotaCache;
24
+ router: Router;
25
+ leaderboard: LeaderboardCache;
26
+ mtimeMs: number;
27
+ /**
28
+ * Where this runtime's config came from (undefined = auto-detect). Jobs
29
+ * pass it to their detached runner process so a run bootstraps against
30
+ * the SAME config file as the server that started it, not whatever the
31
+ * runner's cwd happens to contain.
32
+ */
33
+ configPath?: string;
34
+ }
35
+ /** A mutable holder you pass to tool handlers so hot-reloads are picked up. */
36
+ export declare class RuntimeHolder {
37
+ state: RuntimeState;
38
+ constructor(state: RuntimeState);
39
+ replace(next: RuntimeState): void;
40
+ }
41
+ /**
42
+ * Bootstrap the full runtime from a config path. Used on server start and
43
+ * again (internally) when a reload is triggered.
44
+ */
45
+ export declare function bootstrapRuntime(opts: {
46
+ configPath?: string;
47
+ leaderboard?: LeaderboardCache;
48
+ }): Promise<RuntimeState>;
49
+ /**
50
+ * Reload helper — pairs with a RuntimeHolder for in-place swap.
51
+ *
52
+ * If the config file's mtime has not moved since we last reloaded, the call
53
+ * is a cheap no-op. Circuit-breaker state from the previous router is
54
+ * preserved for every service that still exists in the new config.
55
+ */
56
+ export declare class ConfigHotReloader {
57
+ private readonly holder;
58
+ private readonly configPath?;
59
+ private readonly mutex;
60
+ constructor(holder: RuntimeHolder, configPath?: string | undefined);
61
+ /** Returns true iff a reload actually happened. Swallows all errors. */
62
+ maybeReload(): Promise<boolean>;
63
+ }
64
+ //# sourceMappingURL=config-hot-reload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-hot-reload.d.ts","sourceRoot":"","sources":["../../src/mcp/config-hot-reload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE/E,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,EAAE,aAAa,CAAC;IAC3B,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,gBAAgB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,+EAA+E;AAC/E,qBAAa,aAAa;IACxB,KAAK,EAAE,YAAY,CAAC;gBACR,KAAK,EAAE,YAAY;IAG/B,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;CAGlC;AAYD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,gBAAgB,CAAC;CAChC,GAAG,OAAO,CAAC,YAAY,CAAC,CAmBxB;AAkBD;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAI1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IAJ9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;gBAGlB,MAAM,EAAE,aAAa,EACrB,UAAU,CAAC,EAAE,MAAM,YAAA;IAGtC,wEAAwE;IAClE,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;CAwCtC"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Config hot-reload for the MCP server.
3
+ *
4
+ * Ported from Python `server.py:_maybe_reload_config`. Rather than file-system
5
+ * watchers (which are flaky on Windows + WSL mounts), we poll the config
6
+ * file's mtime between tool calls. A reload rebuilds the dispatcher map,
7
+ * quota cache, and router — while preserving circuit-breaker state for any
8
+ * service that still exists in the new config.
9
+ *
10
+ * Concurrency: the reload is guarded by a mutex so that simultaneous
11
+ * tool-calls can't race on global state replacement. Only the first caller
12
+ * performs the reload; the rest wait behind the mutex and pick up the
13
+ * already-refreshed state.
14
+ */
15
+ import { promises as fs } from "node:fs";
16
+ import { loadConfig } from "../config.js";
17
+ import { setJobRetentionDays } from "../jobs.js";
18
+ import { LeaderboardCache } from "../leaderboard.js";
19
+ import { QuotaCache } from "../quota.js";
20
+ import { Router } from "../router.js";
21
+ import { buildDispatchers } from "./dispatcher-factory.js";
22
+ /** A mutable holder you pass to tool handlers so hot-reloads are picked up. */
23
+ export class RuntimeHolder {
24
+ state;
25
+ constructor(state) {
26
+ this.state = state;
27
+ }
28
+ replace(next) {
29
+ this.state = next;
30
+ }
31
+ }
32
+ async function statMtime(path) {
33
+ if (!path)
34
+ return 0;
35
+ try {
36
+ const stat = await fs.stat(path);
37
+ return stat.mtimeMs;
38
+ }
39
+ catch {
40
+ return 0;
41
+ }
42
+ }
43
+ /**
44
+ * Bootstrap the full runtime from a config path. Used on server start and
45
+ * again (internally) when a reload is triggered.
46
+ */
47
+ export async function bootstrapRuntime(opts) {
48
+ const config = await loadConfig(opts.configPath);
49
+ // Runs at boot AND on every hot reload, so config-driven retention tracks
50
+ // the live config.
51
+ setJobRetentionDays(config.retention?.jobsDays);
52
+ const dispatchers = await buildDispatchers(config);
53
+ const quota = new QuotaCache(dispatchers);
54
+ const leaderboard = opts.leaderboard ?? new LeaderboardCache();
55
+ const router = new Router(config, quota, dispatchers, leaderboard);
56
+ const mtimeMs = await statMtime(opts.configPath);
57
+ return {
58
+ config,
59
+ dispatchers,
60
+ quota,
61
+ router,
62
+ leaderboard,
63
+ mtimeMs,
64
+ ...(opts.configPath !== undefined ? { configPath: opts.configPath } : {}),
65
+ };
66
+ }
67
+ /** Gate that serialises concurrent reload attempts. */
68
+ class Mutex {
69
+ tail = Promise.resolve();
70
+ async run(task) {
71
+ const prev = this.tail;
72
+ let release;
73
+ this.tail = new Promise((r) => (release = r));
74
+ try {
75
+ await prev;
76
+ return await task();
77
+ }
78
+ finally {
79
+ release();
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * Reload helper — pairs with a RuntimeHolder for in-place swap.
85
+ *
86
+ * If the config file's mtime has not moved since we last reloaded, the call
87
+ * is a cheap no-op. Circuit-breaker state from the previous router is
88
+ * preserved for every service that still exists in the new config.
89
+ */
90
+ export class ConfigHotReloader {
91
+ holder;
92
+ configPath;
93
+ mutex = new Mutex();
94
+ constructor(holder, configPath) {
95
+ this.holder = holder;
96
+ this.configPath = configPath;
97
+ }
98
+ /** Returns true iff a reload actually happened. Swallows all errors. */
99
+ async maybeReload() {
100
+ if (!this.configPath)
101
+ return false;
102
+ const mtimeMs = await statMtime(this.configPath);
103
+ if (mtimeMs === 0)
104
+ return false;
105
+ if (mtimeMs <= this.holder.state.mtimeMs)
106
+ return false;
107
+ return this.mutex.run(async () => {
108
+ // Re-check after acquiring the lock — another caller may have already
109
+ // reloaded, in which case we bail without redoing the work.
110
+ const current = this.holder.state.mtimeMs;
111
+ if (mtimeMs <= current)
112
+ return false;
113
+ let next;
114
+ try {
115
+ const bootOpts = {
116
+ leaderboard: this.holder.state.leaderboard,
117
+ };
118
+ if (this.configPath !== undefined)
119
+ bootOpts.configPath = this.configPath;
120
+ next = await bootstrapRuntime(bootOpts);
121
+ }
122
+ catch {
123
+ // Malformed edits shouldn't crash the server — keep the old state.
124
+ return false;
125
+ }
126
+ // Preserve circuit-breaker state for services that still exist.
127
+ const oldRouter = this.holder.state.router;
128
+ const oldBreakerStatus = oldRouter.circuitBreakerStatus();
129
+ for (const [name, status] of Object.entries(oldBreakerStatus)) {
130
+ if (!(name in next.config.services))
131
+ continue;
132
+ const nb = next.router.getBreaker(name);
133
+ if (!nb)
134
+ continue;
135
+ if (status.tripped) {
136
+ nb.trip(status.cooldownRemainingSec ?? undefined);
137
+ }
138
+ }
139
+ this.holder.replace(next);
140
+ return true;
141
+ });
142
+ }
143
+ }
144
+ //# sourceMappingURL=config-hot-reload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-hot-reload.js","sourceRoot":"","sources":["../../src/mcp/config-hot-reload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAE,gBAAgB,EAAsB,MAAM,yBAAyB,CAAC;AAkB/E,+EAA+E;AAC/E,MAAM,OAAO,aAAa;IACxB,KAAK,CAAe;IACpB,YAAY,KAAmB;QAC7B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,CAAC,IAAkB;QACxB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;CACF;AAED,KAAK,UAAU,SAAS,CAAC,IAAwB;IAC/C,IAAI,CAAC,IAAI;QAAE,OAAO,CAAC,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAGtC;IACC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACjD,0EAA0E;IAC1E,mBAAmB;IACnB,mBAAmB,CAAC,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAChD,MAAM,WAAW,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,gBAAgB,EAAE,CAAC;IAC/D,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IACnE,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACjD,OAAO;QACL,MAAM;QACN,WAAW;QACX,KAAK;QACL,MAAM;QACN,WAAW;QACX,OAAO;QACP,GAAG,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1E,CAAC;AACJ,CAAC;AAED,uDAAuD;AACvD,MAAM,KAAK;IACD,IAAI,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAChD,KAAK,CAAC,GAAG,CAAI,IAAsB;QACjC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,OAAoB,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;QACpD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC;YACX,OAAO,MAAM,IAAI,EAAE,CAAC;QACtB,CAAC;gBAAS,CAAC;YACT,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,iBAAiB;IAIT;IACA;IAJF,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC;IAErC,YACmB,MAAqB,EACrB,UAAmB;QADnB,WAAM,GAAN,MAAM,CAAe;QACrB,eAAU,GAAV,UAAU,CAAS;IACnC,CAAC;IAEJ,wEAAwE;IACxE,KAAK,CAAC,WAAW;QACf,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO,KAAK,CAAC;QACnC,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACjD,IAAI,OAAO,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAChC,IAAI,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAEvD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;YAC/B,sEAAsE;YACtE,4DAA4D;YAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;YAC1C,IAAI,OAAO,IAAI,OAAO;gBAAE,OAAO,KAAK,CAAC;YAErC,IAAI,IAAkB,CAAC;YACvB,IAAI,CAAC;gBACH,MAAM,QAAQ,GAA4D;oBACxE,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW;iBAC3C,CAAC;gBACF,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;oBAAE,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;gBACzE,IAAI,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;YAC1C,CAAC;YAAC,MAAM,CAAC;gBACP,mEAAmE;gBACnE,OAAO,KAAK,CAAC;YACf,CAAC;YAED,gEAAgE;YAChE,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;YAC3C,MAAM,gBAAgB,GAAG,SAAS,CAAC,oBAAoB,EAAE,CAAC;YAC1D,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBAC9D,IAAI,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;oBAAE,SAAS;gBAC9C,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;gBACxC,IAAI,CAAC,EAAE;oBAAE,SAAS;gBAClB,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;oBACnB,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,oBAAoB,IAAI,SAAS,CAAC,CAAC;gBACpD,CAAC;YACH,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1B,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Dispatcher factory — shared between the CLI (`bin.ts`) and the MCP server.
3
+ *
4
+ * Exactly two dispatcher classes exist: GenericCliDispatcher for every
5
+ * `type: cli` route (built-in harness or user-added — see the shipped
6
+ * config.default.yaml for how the 4 built-ins get their default behavior)
7
+ * and OpenAICompatibleDispatcher for `type: openai_compatible`. `harness:`
8
+ * no longer selects a dispatcher class — it only picks which CLI_DEFAULTS
9
+ * entry a route inherits metadata/protocol from at config-load time (see
10
+ * config.ts). Dispatch itself never branches on harness name.
11
+ */
12
+ import type { Dispatcher } from "../dispatchers/base.js";
13
+ import type { RouterConfig, ServiceConfig } from "../types.js";
14
+ /** Map of enabled service name -> dispatcher instance. */
15
+ export type DispatcherMap = Record<string, Dispatcher>;
16
+ /** Build one dispatcher from a service config. */
17
+ export declare function makeDispatcher(svc: ServiceConfig): Dispatcher;
18
+ /** Build a dispatcher map for every enabled service in the config. */
19
+ export declare function buildDispatchers(config: RouterConfig): DispatcherMap;
20
+ //# sourceMappingURL=dispatcher-factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher-factory.d.ts","sourceRoot":"","sources":["../../src/mcp/dispatcher-factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE/D,0DAA0D;AAC1D,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAEvD,kDAAkD;AAClD,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,UAAU,CAG7D;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,aAAa,CAOpE"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Dispatcher factory — shared between the CLI (`bin.ts`) and the MCP server.
3
+ *
4
+ * Exactly two dispatcher classes exist: GenericCliDispatcher for every
5
+ * `type: cli` route (built-in harness or user-added — see the shipped
6
+ * config.default.yaml for how the 4 built-ins get their default behavior)
7
+ * and OpenAICompatibleDispatcher for `type: openai_compatible`. `harness:`
8
+ * no longer selects a dispatcher class — it only picks which CLI_DEFAULTS
9
+ * entry a route inherits metadata/protocol from at config-load time (see
10
+ * config.ts). Dispatch itself never branches on harness name.
11
+ */
12
+ import { GenericCliDispatcher } from "../dispatchers/generic-cli.js";
13
+ import { OpenAICompatibleDispatcher } from "../dispatchers/openai-compatible.js";
14
+ /** Build one dispatcher from a service config. */
15
+ export function makeDispatcher(svc) {
16
+ if (svc.type === "openai_compatible")
17
+ return new OpenAICompatibleDispatcher(svc);
18
+ return new GenericCliDispatcher(svc);
19
+ }
20
+ /** Build a dispatcher map for every enabled service in the config. */
21
+ export function buildDispatchers(config) {
22
+ const out = {};
23
+ for (const [name, svc] of Object.entries(config.services)) {
24
+ if (!svc.enabled)
25
+ continue;
26
+ out[name] = makeDispatcher(svc);
27
+ }
28
+ return out;
29
+ }
30
+ //# sourceMappingURL=dispatcher-factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher-factory.js","sourceRoot":"","sources":["../../src/mcp/dispatcher-factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AAOjF,kDAAkD;AAClD,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C,IAAI,GAAG,CAAC,IAAI,KAAK,mBAAmB;QAAE,OAAO,IAAI,0BAA0B,CAAC,GAAG,CAAC,CAAC;IACjF,OAAO,IAAI,oBAAoB,CAAC,GAAG,CAAC,CAAC;AACvC,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,gBAAgB,CAAC,MAAoB;IACnD,MAAM,GAAG,GAAkB,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1D,IAAI,CAAC,GAAG,CAAC,OAAO;YAAE,SAAS;QAC3B,GAAG,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ConfigHotReloader, RuntimeHolder } from "./config-hot-reload.js";
3
+ export interface ResourceDeps {
4
+ holder: RuntimeHolder;
5
+ reloader?: ConfigHotReloader;
6
+ }
7
+ export declare function registerResources(server: McpServer, deps: ResourceDeps): void;
8
+ //# sourceMappingURL=resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../../src/mcp/resources.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE/E,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAkBD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CA4C7E"}
@@ -0,0 +1,45 @@
1
+ import { buildStatus, renderStatusText } from "../status.js";
2
+ async function ensureFreshConfig(reloader) {
3
+ if (reloader)
4
+ await reloader.maybeReload();
5
+ }
6
+ async function currentStatus(deps) {
7
+ await ensureFreshConfig(deps.reloader);
8
+ const state = deps.holder.state;
9
+ return buildStatus(state.config, state.dispatchers, state.quota, state.router, state.leaderboard);
10
+ }
11
+ export function registerResources(server, deps) {
12
+ server.registerResource("status", "harness-dispatch://status", {
13
+ title: "Harness Router Status",
14
+ description: "Human-readable route availability, quota, and breaker state.",
15
+ mimeType: "text/plain",
16
+ }, async () => {
17
+ const status = await currentStatus(deps);
18
+ return {
19
+ contents: [
20
+ {
21
+ uri: "harness-dispatch://status",
22
+ mimeType: "text/plain",
23
+ text: renderStatusText(status),
24
+ },
25
+ ],
26
+ };
27
+ });
28
+ server.registerResource("status-json", "harness-dispatch://status.json", {
29
+ title: "Harness Router Status JSON",
30
+ description: "Structured route availability, quota, and breaker state.",
31
+ mimeType: "application/json",
32
+ }, async () => {
33
+ const status = await currentStatus(deps);
34
+ return {
35
+ contents: [
36
+ {
37
+ uri: "harness-dispatch://status.json",
38
+ mimeType: "application/json",
39
+ text: JSON.stringify(status, null, 2),
40
+ },
41
+ ],
42
+ };
43
+ });
44
+ }
45
+ //# sourceMappingURL=resources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/mcp/resources.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQ7D,KAAK,UAAU,iBAAiB,CAAC,QAAuC;IACtE,IAAI,QAAQ;QAAE,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;AAC7C,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAAkB;IAC7C,MAAM,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;IAChC,OAAO,WAAW,CAChB,KAAK,CAAC,MAAM,EACZ,KAAK,CAAC,WAAW,EACjB,KAAK,CAAC,KAAK,EACX,KAAK,CAAC,MAAM,EACZ,KAAK,CAAC,WAAW,CAClB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAiB,EAAE,IAAkB;IACrE,MAAM,CAAC,gBAAgB,CACrB,QAAQ,EACR,2BAA2B,EAC3B;QACE,KAAK,EAAE,uBAAuB;QAC9B,WAAW,EAAE,8DAA8D;QAC3E,QAAQ,EAAE,YAAY;KACvB,EACD,KAAK,IAAI,EAAE;QACT,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;QACzC,OAAO;YACL,QAAQ,EAAE;gBACR;oBACE,GAAG,EAAE,2BAA2B;oBAChC,QAAQ,EAAE,YAAY;oBACtB,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC;iBAC/B;aACF;SACF,CAAC;IACJ,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,gBAAgB,CACrB,aAAa,EACb,gCAAgC,EAChC;QACE,KAAK,EAAE,4BAA4B;QACnC,WAAW,EAAE,0DAA0D;QACvE,QAAQ,EAAE,kBAAkB;KAC7B,EACD,KAAK,IAAI,EAAE;QACT,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;QACzC,OAAO;YACL,QAAQ,EAAE;gBACR;oBACE,GAAG,EAAE,gCAAgC;oBACrC,QAAQ,EAAE,kBAAkB;oBAC5B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;iBACtC;aACF;SACF,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}