@matt82198/aesop 0.1.0-beta.4 → 0.1.0-beta.5

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 (174) hide show
  1. package/CHANGELOG.md +46 -5
  2. package/README.md +57 -1
  3. package/aesop.config.example.json +8 -1
  4. package/bin/CLAUDE.md +30 -2
  5. package/bin/cli.js +286 -73
  6. package/daemons/CLAUDE.md +4 -1
  7. package/daemons/run-watchdog.sh +5 -2
  8. package/docs/RELEASING.md +159 -0
  9. package/docs/archive/README.md +3 -0
  10. package/docs/{spikes → archive/spikes}/tiered-cognition/README.md +1 -3
  11. package/docs/case-study-portfolio.md +61 -0
  12. package/docs/self-stats-data.json +11 -0
  13. package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
  14. package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
  15. package/docs/templates/PROPOSALS-LOG.example.md +64 -0
  16. package/hooks/CLAUDE.md +23 -0
  17. package/mcp/CLAUDE.md +213 -0
  18. package/mcp/package.json +26 -0
  19. package/mcp/server.mjs +543 -0
  20. package/monitor/CHARTER.md +76 -110
  21. package/monitor/collect-signals.mjs +85 -0
  22. package/package.json +4 -1
  23. package/scan/fleet-scan.example.mjs +292 -0
  24. package/skills/healthcheck/SKILL.md +44 -0
  25. package/state_store/CLAUDE.md +39 -0
  26. package/state_store/__init__.py +24 -0
  27. package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
  28. package/state_store/__pycache__/api.cpython-314.pyc +0 -0
  29. package/state_store/__pycache__/export.cpython-314.pyc +0 -0
  30. package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
  31. package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
  32. package/state_store/__pycache__/store.cpython-314.pyc +0 -0
  33. package/state_store/api.py +35 -0
  34. package/state_store/export.py +19 -0
  35. package/state_store/ingest.py +24 -0
  36. package/state_store/projections.py +52 -0
  37. package/state_store/store.py +102 -0
  38. package/tools/CLAUDE.md +30 -149
  39. package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
  40. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  41. package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
  42. package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
  43. package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
  44. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  45. package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
  46. package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
  47. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  48. package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
  49. package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
  50. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  51. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  52. package/tools/alert_bridge.py +449 -0
  53. package/tools/ci_merge_wait.py +121 -8
  54. package/tools/fleet_prompt_extractor.py +134 -0
  55. package/tools/healthcheck.py +296 -0
  56. package/tools/secret_scan.py +8 -1
  57. package/tools/self_stats.py +509 -0
  58. package/tools/session_usage_summary.py +198 -0
  59. package/tools/svg_to_png.mjs +50 -0
  60. package/tools/transcript_replay.py +236 -0
  61. package/tools/transcript_timeline.py +184 -0
  62. package/tools/verify_dash.py +361 -542
  63. package/tools/verify_submit_encoding.py +12 -4
  64. package/ui/CLAUDE.md +57 -41
  65. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  66. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  67. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  68. package/ui/__pycache__/cost.cpython-314.pyc +0 -0
  69. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  70. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  71. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  72. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  73. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  74. package/ui/agents.py +9 -5
  75. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  76. package/ui/api/submit.py +44 -5
  77. package/ui/collectors.py +184 -35
  78. package/ui/config.py +9 -0
  79. package/ui/cost.py +260 -0
  80. package/ui/csrf.py +7 -3
  81. package/ui/handler.py +254 -4
  82. package/ui/render.py +26 -6
  83. package/ui/sse.py +16 -1
  84. package/ui/web/.gitattributes +13 -0
  85. package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
  86. package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
  87. package/ui/web/dist/index.html +14 -0
  88. package/ui/web/index.html +13 -0
  89. package/ui/web/package-lock.json +2225 -0
  90. package/ui/web/package.json +26 -0
  91. package/ui/web/src/App.test.tsx +74 -0
  92. package/ui/web/src/App.tsx +142 -0
  93. package/ui/web/src/CONTRIBUTING-UI.md +49 -0
  94. package/ui/web/src/components/AgentRow.css +187 -0
  95. package/ui/web/src/components/AgentRow.test.tsx +209 -0
  96. package/ui/web/src/components/AgentRow.tsx +207 -0
  97. package/ui/web/src/components/AgentsPanel.css +108 -0
  98. package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
  99. package/ui/web/src/components/AgentsPanel.tsx +58 -0
  100. package/ui/web/src/components/AlertsPanel.css +88 -0
  101. package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
  102. package/ui/web/src/components/AlertsPanel.tsx +67 -0
  103. package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
  104. package/ui/web/src/components/BacklogPanel.tsx +122 -0
  105. package/ui/web/src/components/CostChart.css +110 -0
  106. package/ui/web/src/components/CostChart.test.tsx +144 -0
  107. package/ui/web/src/components/CostChart.tsx +152 -0
  108. package/ui/web/src/components/CostTable.css +93 -0
  109. package/ui/web/src/components/CostTable.test.tsx +165 -0
  110. package/ui/web/src/components/CostTable.tsx +94 -0
  111. package/ui/web/src/components/EventsFeed.css +68 -0
  112. package/ui/web/src/components/EventsFeed.test.tsx +36 -0
  113. package/ui/web/src/components/EventsFeed.tsx +31 -0
  114. package/ui/web/src/components/HealthHeader.css +137 -0
  115. package/ui/web/src/components/HealthHeader.test.tsx +278 -0
  116. package/ui/web/src/components/HealthHeader.tsx +281 -0
  117. package/ui/web/src/components/InboxForm.css +135 -0
  118. package/ui/web/src/components/InboxForm.test.tsx +208 -0
  119. package/ui/web/src/components/InboxForm.tsx +116 -0
  120. package/ui/web/src/components/MessagesTail.module.css +144 -0
  121. package/ui/web/src/components/MessagesTail.test.tsx +176 -0
  122. package/ui/web/src/components/MessagesTail.tsx +94 -0
  123. package/ui/web/src/components/ReposPanel.css +90 -0
  124. package/ui/web/src/components/ReposPanel.test.tsx +45 -0
  125. package/ui/web/src/components/ReposPanel.tsx +67 -0
  126. package/ui/web/src/components/Scorecard.css +106 -0
  127. package/ui/web/src/components/Scorecard.test.tsx +117 -0
  128. package/ui/web/src/components/Scorecard.tsx +85 -0
  129. package/ui/web/src/components/Timeline.module.css +151 -0
  130. package/ui/web/src/components/Timeline.test.tsx +215 -0
  131. package/ui/web/src/components/Timeline.tsx +99 -0
  132. package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
  133. package/ui/web/src/components/TrackerBoard.tsx +107 -0
  134. package/ui/web/src/components/TrackerCard.test.tsx +180 -0
  135. package/ui/web/src/components/TrackerCard.tsx +160 -0
  136. package/ui/web/src/components/TrackerForm.test.tsx +189 -0
  137. package/ui/web/src/components/TrackerForm.tsx +144 -0
  138. package/ui/web/src/lib/api.ts +218 -0
  139. package/ui/web/src/lib/format.test.ts +89 -0
  140. package/ui/web/src/lib/format.ts +103 -0
  141. package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
  142. package/ui/web/src/lib/sanitizeUrl.ts +38 -0
  143. package/ui/web/src/lib/types.ts +230 -0
  144. package/ui/web/src/lib/useHashRoute.test.ts +60 -0
  145. package/ui/web/src/lib/useHashRoute.ts +23 -0
  146. package/ui/web/src/lib/useSSE.ts +175 -0
  147. package/ui/web/src/main.tsx +10 -0
  148. package/ui/web/src/styles/global.css +179 -0
  149. package/ui/web/src/styles/theme.css +184 -0
  150. package/ui/web/src/styles/work.css +572 -0
  151. package/ui/web/src/test/fixtures.ts +385 -0
  152. package/ui/web/src/test/setup.ts +49 -0
  153. package/ui/web/src/views/Activity.module.css +43 -0
  154. package/ui/web/src/views/Activity.test.tsx +89 -0
  155. package/ui/web/src/views/Activity.tsx +31 -0
  156. package/ui/web/src/views/Cost.css +87 -0
  157. package/ui/web/src/views/Cost.test.tsx +142 -0
  158. package/ui/web/src/views/Cost.tsx +54 -0
  159. package/ui/web/src/views/Overview.css +51 -0
  160. package/ui/web/src/views/Overview.test.tsx +76 -0
  161. package/ui/web/src/views/Overview.tsx +46 -0
  162. package/ui/web/src/views/Work.test.tsx +82 -0
  163. package/ui/web/src/views/Work.tsx +79 -0
  164. package/ui/web/src/vite-env.d.ts +10 -0
  165. package/ui/web/tsconfig.json +22 -0
  166. package/ui/web/vite.config.ts +25 -0
  167. package/ui/web/vitest.config.ts +12 -0
  168. package/ui/templates/dashboard.html +0 -1202
  169. /package/docs/{spikes → archive/spikes}/tiered-cognition/ACTIVATION.md +0 -0
  170. /package/docs/{spikes → archive/spikes}/tiered-cognition/DESIGN.md +0 -0
  171. /package/docs/{spikes → archive/spikes}/tiered-cognition/FINDINGS.md +0 -0
  172. /package/docs/{spikes → archive/spikes}/tiered-cognition/aesop-cognition.example.md +0 -0
  173. /package/docs/{spikes → archive/spikes}/tiered-cognition/force-model-policy.merged.mjs +0 -0
  174. /package/docs/{spikes → archive/spikes}/tiered-cognition/strip-tools-hook.mjs +0 -0
package/mcp/server.mjs ADDED
@@ -0,0 +1,543 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Aesop Fleet State MCP Server
4
+ *
5
+ * Read-only MCP server (stdio transport) exposing fleet operational status.
6
+ * Resolves AESOP_ROOT from env or --root flag; gracefully handles missing state files.
7
+ *
8
+ * Tools:
9
+ * fleet_status - heartbeat ages + orchestrator status + alert count
10
+ * fleet_agents - active agents from transcripts via dash-extra.mjs passthrough
11
+ * fleet_tracker - open items by lane from state/tracker.json
12
+ * fleet_cost - per-model token totals from state/ledger/OUTCOMES-LEDGER.md
13
+ *
14
+ * All tools are read-only; no state mutations, no file writes.
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import os from 'node:os';
20
+ import { spawn } from 'node:child_process';
21
+ import { createInterface } from 'node:readline';
22
+
23
+ // ============================================================================
24
+ // Configuration & Initialization
25
+ // ============================================================================
26
+
27
+ function loadConfigFile(aesopRoot) {
28
+ try {
29
+ const configPath = path.join(aesopRoot, 'aesop.config.json');
30
+ if (fs.existsSync(configPath)) {
31
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
32
+ }
33
+ } catch {
34
+ // Parse error or file doesn't exist; ignore
35
+ }
36
+ return {};
37
+ }
38
+
39
+ function expandPath(pathStr) {
40
+ if (!pathStr) return pathStr;
41
+ if (pathStr.startsWith('~')) {
42
+ return path.join(os.homedir(), pathStr.slice(1));
43
+ }
44
+ return pathStr.replace(/\$\{?([A-Z_]+)\}?/gi, (match, varName) => {
45
+ return process.env[varName] || match;
46
+ });
47
+ }
48
+
49
+ // Parse --root from argv
50
+ function parseRootArg() {
51
+ const idx = process.argv.indexOf('--root');
52
+ if (idx !== -1 && idx + 1 < process.argv.length) {
53
+ return process.argv[idx + 1];
54
+ }
55
+ return null;
56
+ }
57
+
58
+ const rootArg = parseRootArg();
59
+ const AESOP_ROOT = rootArg || process.env.AESOP_ROOT || path.join(os.homedir(), 'aesop');
60
+ const config = loadConfigFile(AESOP_ROOT);
61
+
62
+ const STATE_ROOT = path.resolve(
63
+ expandPath(
64
+ process.env.AESOP_STATE_ROOT ||
65
+ config.state_root ||
66
+ path.join(AESOP_ROOT, 'state')
67
+ )
68
+ );
69
+
70
+ const TRANSCRIPTS_ROOT = path.resolve(
71
+ expandPath(
72
+ process.env.AESOP_TRANSCRIPTS_ROOT ||
73
+ config.transcripts_root ||
74
+ path.join(os.homedir(), '.claude', 'projects')
75
+ )
76
+ );
77
+
78
+ // File paths
79
+ const WATCHDOG_HEARTBEAT = path.join(STATE_ROOT, '.watchdog-heartbeat');
80
+ const MONITOR_HEARTBEAT = path.join(STATE_ROOT, '.monitor-heartbeat');
81
+ const ALERTS_LOG = path.join(STATE_ROOT, 'SECURITY-ALERTS.log');
82
+ const TRACKER_FILE = path.join(STATE_ROOT, 'tracker.json');
83
+ const ORCH_STATUS_FILE = path.join(STATE_ROOT, 'orchestrator-status.json');
84
+ const LEDGER_FILE = path.join(STATE_ROOT, 'ledger', 'OUTCOMES-LEDGER.md');
85
+
86
+ // ============================================================================
87
+ // MCP Protocol & JSON-RPC Utilities
88
+ // ============================================================================
89
+
90
+ class MCP {
91
+ constructor() {
92
+ this.requestId = 0;
93
+ this.rl = createInterface({ input: process.stdin, output: process.stdout });
94
+ }
95
+
96
+ /**
97
+ * Read one JSON-RPC line from stdin, parse, and return
98
+ */
99
+ async readRequest() {
100
+ return new Promise((resolve) => {
101
+ this.rl.once('line', (line) => {
102
+ try {
103
+ const req = JSON.parse(line);
104
+ resolve(req);
105
+ } catch (e) {
106
+ resolve(null);
107
+ }
108
+ });
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Write one JSON-RPC response to stdout
114
+ */
115
+ writeResponse(response) {
116
+ process.stdout.write(JSON.stringify(response) + '\n');
117
+ }
118
+
119
+ /**
120
+ * Write JSON-RPC error response
121
+ */
122
+ writeError(requestId, code, message, data = null) {
123
+ const response = {
124
+ jsonrpc: '2.0',
125
+ id: requestId,
126
+ error: { code, message }
127
+ };
128
+ if (data !== null) {
129
+ response.error.data = data;
130
+ }
131
+ this.writeResponse(response);
132
+ }
133
+
134
+ /**
135
+ * Write JSON-RPC result response
136
+ */
137
+ writeResult(requestId, result) {
138
+ this.writeResponse({
139
+ jsonrpc: '2.0',
140
+ id: requestId,
141
+ result
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Close the MCP server
147
+ */
148
+ close() {
149
+ this.rl.close();
150
+ }
151
+ }
152
+
153
+ const mcp = new MCP();
154
+
155
+ // ============================================================================
156
+ // Tool Implementations (Read-Only)
157
+ // ============================================================================
158
+
159
+ /**
160
+ * fleet_status: Expose heartbeat ages, orchestrator status, alert count
161
+ */
162
+ function getFleetStatus() {
163
+ const result = {
164
+ watchdog: null,
165
+ monitor: null,
166
+ orchestrator: null,
167
+ alerts: null
168
+ };
169
+
170
+ // Read watchdog heartbeat
171
+ try {
172
+ if (fs.existsSync(WATCHDOG_HEARTBEAT)) {
173
+ const content = fs.readFileSync(WATCHDOG_HEARTBEAT, 'utf8').trim();
174
+ if (content) {
175
+ const timestamp = parseInt(content, 10);
176
+ if (!isNaN(timestamp)) {
177
+ const now = Math.floor(Date.now() / 1000);
178
+ const ageSec = now - timestamp;
179
+ const ageBucketed = Math.floor(ageSec / 3) * 3;
180
+ const alive = ageSec < 300 ? 'ALIVE' : 'STALE';
181
+ result.watchdog = {
182
+ alive,
183
+ age_seconds: ageBucketed,
184
+ threshold_seconds: 300
185
+ };
186
+ }
187
+ }
188
+ }
189
+ } catch (e) {
190
+ // Silently ignore errors
191
+ }
192
+
193
+ // Read monitor heartbeat
194
+ try {
195
+ let monitorHb = MONITOR_HEARTBEAT;
196
+ if (!fs.existsSync(monitorHb)) {
197
+ const altPath = path.join(AESOP_ROOT, 'monitor', '.monitor-heartbeat');
198
+ if (fs.existsSync(altPath)) {
199
+ monitorHb = altPath;
200
+ }
201
+ }
202
+ if (fs.existsSync(monitorHb)) {
203
+ const content = fs.readFileSync(monitorHb, 'utf8').trim();
204
+ if (content) {
205
+ const timestamp = parseInt(content, 10);
206
+ if (!isNaN(timestamp)) {
207
+ const now = Math.floor(Date.now() / 1000);
208
+ const ageSec = now - timestamp;
209
+ const ageBucketed = Math.floor(ageSec / 3) * 3;
210
+ const alive = ageSec < 3600 ? 'ALIVE' : 'STALE';
211
+ result.monitor = {
212
+ alive,
213
+ age_seconds: ageBucketed,
214
+ threshold_seconds: 3600
215
+ };
216
+ }
217
+ }
218
+ }
219
+ } catch (e) {
220
+ // Silently ignore errors
221
+ }
222
+
223
+ // Read orchestrator status if it exists
224
+ try {
225
+ if (fs.existsSync(ORCH_STATUS_FILE)) {
226
+ const content = fs.readFileSync(ORCH_STATUS_FILE, 'utf8').trim();
227
+ if (content) {
228
+ const parsed = JSON.parse(content);
229
+ result.orchestrator = parsed;
230
+ }
231
+ }
232
+ } catch (e) {
233
+ // Silently ignore errors
234
+ }
235
+
236
+ // Count alerts (skip NOTE:/RESOLVED-FP lines)
237
+ try {
238
+ if (fs.existsSync(ALERTS_LOG)) {
239
+ const lines = fs.readFileSync(ALERTS_LOG, 'utf8').trim().split('\n');
240
+ const unreviewed = lines.filter(
241
+ line => line.trim() && !line.includes('NOTE:') && !line.includes('RESOLVED-FP')
242
+ );
243
+ result.alerts = {
244
+ count: unreviewed.length,
245
+ sample_lines: unreviewed.slice(-3)
246
+ };
247
+ }
248
+ } catch (e) {
249
+ // Silently ignore errors
250
+ }
251
+
252
+ return result;
253
+ }
254
+
255
+ /**
256
+ * fleet_agents: Invoke dash-extra.mjs --json and passthrough result
257
+ */
258
+ async function getFleetAgents() {
259
+ return new Promise((resolve) => {
260
+ const dashExtraPath = path.join(AESOP_ROOT, 'dash', 'dash-extra.mjs');
261
+
262
+ if (!fs.existsSync(dashExtraPath)) {
263
+ resolve({ absent: true, agents: [] });
264
+ return;
265
+ }
266
+
267
+ try {
268
+ const proc = spawn('node', [dashExtraPath, '--json'], {
269
+ cwd: AESOP_ROOT,
270
+ env: {
271
+ ...process.env,
272
+ AESOP_ROOT,
273
+ AESOP_STATE_ROOT: STATE_ROOT,
274
+ AESOP_TRANSCRIPTS_ROOT: TRANSCRIPTS_ROOT
275
+ },
276
+ timeout: 5000
277
+ });
278
+
279
+ let stdout = '';
280
+ let stderr = '';
281
+
282
+ proc.stdout.on('data', (data) => {
283
+ stdout += data.toString();
284
+ });
285
+
286
+ proc.stderr.on('data', (data) => {
287
+ stderr += data.toString();
288
+ });
289
+
290
+ proc.on('close', (code) => {
291
+ if (code === 0 && stdout.trim()) {
292
+ try {
293
+ const agents = JSON.parse(stdout);
294
+ resolve({ absent: false, agents: Array.isArray(agents) ? agents : [] });
295
+ } catch (e) {
296
+ resolve({ absent: false, agents: [] });
297
+ }
298
+ } else {
299
+ resolve({ absent: false, agents: [] });
300
+ }
301
+ });
302
+
303
+ proc.on('error', (err) => {
304
+ resolve({ absent: false, agents: [] });
305
+ });
306
+ } catch (e) {
307
+ resolve({ absent: false, agents: [] });
308
+ }
309
+ });
310
+ }
311
+
312
+ /**
313
+ * fleet_tracker: Read items from tracker.json, grouped by lane
314
+ */
315
+ function getFleetTracker() {
316
+ const result = {
317
+ absent: !fs.existsSync(TRACKER_FILE),
318
+ by_lane: {}
319
+ };
320
+
321
+ try {
322
+ if (fs.existsSync(TRACKER_FILE)) {
323
+ const content = fs.readFileSync(TRACKER_FILE, 'utf8').trim();
324
+ if (content) {
325
+ const tracker = JSON.parse(content);
326
+ const items = tracker.items || [];
327
+
328
+ // Group by lane
329
+ const byLane = {};
330
+ for (const item of items) {
331
+ const lane = item.lane || 'unknown';
332
+ if (!byLane[lane]) {
333
+ byLane[lane] = [];
334
+ }
335
+ byLane[lane].push({
336
+ id: item.id,
337
+ title: item.title,
338
+ priority: item.priority,
339
+ status: item.status,
340
+ tags: item.tags || []
341
+ });
342
+ }
343
+ result.by_lane = byLane;
344
+ }
345
+ }
346
+ } catch (e) {
347
+ // Silently ignore errors
348
+ }
349
+
350
+ return result;
351
+ }
352
+
353
+ /**
354
+ * fleet_cost: Parse ledger and aggregate token counts by model
355
+ */
356
+ function getFleetCost() {
357
+ const result = {
358
+ absent: !fs.existsSync(LEDGER_FILE),
359
+ by_model: {},
360
+ total_tokens_in: 0,
361
+ total_tokens_out: 0
362
+ };
363
+
364
+ try {
365
+ if (fs.existsSync(LEDGER_FILE)) {
366
+ const lines = fs.readFileSync(LEDGER_FILE, 'utf8').split('\n');
367
+
368
+ for (const line of lines) {
369
+ // Parse markdown table row: | ts | agent_type | model | dur | tokens_in | tokens_out | verdict |
370
+ const match = line.match(/^\|\s*([^\|]+?)\s*\|\s*([^\|]+?)\s*\|\s*([^\|]+?)\s*\|\s*([^\|]+?)\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|/);
371
+ if (!match) continue;
372
+
373
+ const model = match[3].trim() || '-';
374
+ const tokensIn = parseInt(match[5], 10) || 0;
375
+ const tokensOut = parseInt(match[6], 10) || 0;
376
+
377
+ if (!result.by_model[model]) {
378
+ result.by_model[model] = {
379
+ tokens_in: 0,
380
+ tokens_out: 0,
381
+ total_tokens: 0,
382
+ count: 0
383
+ };
384
+ }
385
+
386
+ result.by_model[model].tokens_in += tokensIn;
387
+ result.by_model[model].tokens_out += tokensOut;
388
+ result.by_model[model].total_tokens += tokensIn + tokensOut;
389
+ result.by_model[model].count += 1;
390
+
391
+ result.total_tokens_in += tokensIn;
392
+ result.total_tokens_out += tokensOut;
393
+ }
394
+ }
395
+ } catch (e) {
396
+ // Silently ignore errors
397
+ }
398
+
399
+ return result;
400
+ }
401
+
402
+ // ============================================================================
403
+ // MCP Tool & Resource Definitions
404
+ // ============================================================================
405
+
406
+ const TOOLS = [
407
+ {
408
+ name: 'fleet_status',
409
+ description: 'Get fleet operational status: daemon/monitor heartbeats, orchestrator activity, security alerts',
410
+ inputSchema: {
411
+ type: 'object',
412
+ properties: {}
413
+ }
414
+ },
415
+ {
416
+ name: 'fleet_agents',
417
+ description: 'List active Claude agents from transcript directory',
418
+ inputSchema: {
419
+ type: 'object',
420
+ properties: {}
421
+ }
422
+ },
423
+ {
424
+ name: 'fleet_tracker',
425
+ description: 'Get fleet work items from tracker.json, grouped by lane (ranked/proposed/in-progress/done)',
426
+ inputSchema: {
427
+ type: 'object',
428
+ properties: {}
429
+ }
430
+ },
431
+ {
432
+ name: 'fleet_cost',
433
+ description: 'Get per-model token usage totals from outcomes ledger',
434
+ inputSchema: {
435
+ type: 'object',
436
+ properties: {}
437
+ }
438
+ }
439
+ ];
440
+
441
+ // ============================================================================
442
+ // MCP Request Handlers
443
+ // ============================================================================
444
+
445
+ async function handleInitialize(requestId, params) {
446
+ mcp.writeResult(requestId, {
447
+ protocolVersion: '2024-11-05',
448
+ capabilities: {
449
+ tools: {}
450
+ },
451
+ serverInfo: {
452
+ name: 'aesop-fleet',
453
+ version: '1.0.0'
454
+ }
455
+ });
456
+ }
457
+
458
+ async function handleToolsList(requestId, params) {
459
+ mcp.writeResult(requestId, {
460
+ tools: TOOLS
461
+ });
462
+ }
463
+
464
+ async function handleToolCall(requestId, params) {
465
+ const { name, arguments: args } = params;
466
+
467
+ try {
468
+ let result;
469
+ switch (name) {
470
+ case 'fleet_status':
471
+ result = getFleetStatus();
472
+ break;
473
+ case 'fleet_agents':
474
+ result = await getFleetAgents();
475
+ break;
476
+ case 'fleet_tracker':
477
+ result = getFleetTracker();
478
+ break;
479
+ case 'fleet_cost':
480
+ result = getFleetCost();
481
+ break;
482
+ default:
483
+ mcp.writeError(requestId, -32601, `Unknown tool: ${name}`);
484
+ return;
485
+ }
486
+
487
+ mcp.writeResult(requestId, {
488
+ content: [
489
+ {
490
+ type: 'text',
491
+ text: JSON.stringify(result, null, 2)
492
+ }
493
+ ]
494
+ });
495
+ } catch (err) {
496
+ mcp.writeError(requestId, -32603, `Tool execution error: ${err.message}`);
497
+ }
498
+ }
499
+
500
+ // ============================================================================
501
+ // Main Loop
502
+ // ============================================================================
503
+
504
+ async function main() {
505
+ try {
506
+ while (true) {
507
+ const request = await mcp.readRequest();
508
+
509
+ if (!request) {
510
+ continue;
511
+ }
512
+
513
+ const { jsonrpc, id, method, params } = request;
514
+
515
+ if (jsonrpc !== '2.0') {
516
+ mcp.writeError(id, -32600, 'Invalid JSON-RPC version');
517
+ continue;
518
+ }
519
+
520
+ switch (method) {
521
+ case 'initialize':
522
+ await handleInitialize(id, params || {});
523
+ break;
524
+ case 'tools/list':
525
+ await handleToolsList(id, params || {});
526
+ break;
527
+ case 'tools/call':
528
+ await handleToolCall(id, params || {});
529
+ break;
530
+ default:
531
+ mcp.writeError(id, -32601, `Unknown method: ${method}`);
532
+ }
533
+ }
534
+ } catch (err) {
535
+ process.stderr.write(`Fatal error: ${err.message}\n`);
536
+ process.exit(1);
537
+ }
538
+ }
539
+
540
+ main().catch((err) => {
541
+ process.stderr.write(`Fatal error: ${err.message}\n`);
542
+ process.exit(1);
543
+ });