@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.
- package/CHANGELOG.md +46 -5
- package/README.md +57 -1
- package/aesop.config.example.json +8 -1
- package/bin/CLAUDE.md +30 -2
- package/bin/cli.js +286 -73
- package/daemons/CLAUDE.md +4 -1
- package/daemons/run-watchdog.sh +5 -2
- package/docs/RELEASING.md +159 -0
- package/docs/archive/README.md +3 -0
- package/docs/{spikes → archive/spikes}/tiered-cognition/README.md +1 -3
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +23 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/CHARTER.md +76 -110
- package/monitor/collect-signals.mjs +85 -0
- package/package.json +4 -1
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +30 -149
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/ci_merge_wait.py +121 -8
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/secret_scan.py +8 -1
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +361 -542
- package/tools/verify_submit_encoding.py +12 -4
- package/ui/CLAUDE.md +57 -41
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/agents.py +9 -5
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/submit.py +44 -5
- package/ui/collectors.py +184 -35
- package/ui/config.py +9 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +7 -3
- package/ui/handler.py +254 -4
- package/ui/render.py +26 -6
- package/ui/sse.py +16 -1
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
- package/ui/templates/dashboard.html +0 -1202
- /package/docs/{spikes → archive/spikes}/tiered-cognition/ACTIVATION.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/DESIGN.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/FINDINGS.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/aesop-cognition.example.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/force-model-policy.merged.mjs +0 -0
- /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
|
+
});
|