@way_marks/server 4.0.1 → 4.2.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.
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ /**
3
+ * Express route handlers for the agent-monitor REST API.
4
+ *
5
+ * Mount in packages/server/src/api/server.ts:
6
+ *
7
+ * import { createAgentMonitorRouter } from './routes/agent-monitor';
8
+ * app.use('/api/agent-monitor', createAgentMonitorRouter(collector));
9
+ *
10
+ * Endpoints:
11
+ * GET /api/agent-monitor/sessions → all sessions (+ filter query params)
12
+ * GET /api/agent-monitor/sessions/:id → single session detail (with toolCalls, fileAccesses)
13
+ * GET /api/agent-monitor/rate-limits → Claude + Codex rate limits
14
+ * GET /api/agent-monitor/ports → agent ports + orphan ports
15
+ * GET /api/agent-monitor/snapshot → full snapshot (sessions + rateLimits + ports)
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.createAgentMonitorRouter = createAgentMonitorRouter;
19
+ const express_1 = require("express");
20
+ // ─── Router factory ───────────────────────────────────────────────────────────
21
+ /**
22
+ * Create an Express router for the agent-monitor endpoints.
23
+ *
24
+ * @param getSnapshot Function that returns the latest CollectorSnapshot.
25
+ * Pass `() => collector.tick()` for on-demand collection,
26
+ * or `() => lastSnapshot` for a cached snapshot from a timer.
27
+ */
28
+ function createAgentMonitorRouter(getSnapshot) {
29
+ const router = (0, express_1.Router)();
30
+ // ── GET /sessions ──────────────────────────────────────────────────────────
31
+ router.get('/sessions', (_req, res) => {
32
+ const snapshot = getSnapshot();
33
+ let sessions = snapshot.sessions;
34
+ const agent = _req.query['agent'];
35
+ const status = _req.query['status'];
36
+ if (agent && agent !== 'all') {
37
+ sessions = sessions.filter((s) => s.agentCli === agent);
38
+ }
39
+ if (status && status !== 'all') {
40
+ if (status === 'active') {
41
+ sessions = sessions.filter((s) => s.status === 'thinking' || s.status === 'executing');
42
+ }
43
+ else {
44
+ sessions = sessions.filter((s) => s.status === status);
45
+ }
46
+ }
47
+ res.json({
48
+ sessions: sessions.map(sessionSummary),
49
+ count: sessions.length,
50
+ collectedAt: snapshot.collectedAt,
51
+ });
52
+ });
53
+ // ── GET /sessions/:id ──────────────────────────────────────────────────────
54
+ router.get('/sessions/:id', (req, res) => {
55
+ const snapshot = getSnapshot();
56
+ const session = snapshot.sessions.find((s) => s.sessionId === req.params['id']);
57
+ if (!session) {
58
+ res.status(404).json({ error: 'Session not found' });
59
+ return;
60
+ }
61
+ // Full detail including toolCalls and fileAccesses
62
+ res.json({ session: sessionDetail(session), collectedAt: snapshot.collectedAt });
63
+ });
64
+ // ── GET /rate-limits ───────────────────────────────────────────────────────
65
+ router.get('/rate-limits', (_req, res) => {
66
+ const snapshot = getSnapshot();
67
+ res.json({
68
+ rateLimits: snapshot.rateLimits.map((r) => ({
69
+ source: r.source,
70
+ fiveHour: r.fiveHourPct != null ? {
71
+ usedPercent: r.fiveHourPct,
72
+ resetsAt: r.fiveHourResetsAt,
73
+ resetsAtIso: r.fiveHourResetsAt
74
+ ? new Date(r.fiveHourResetsAt * 1000).toISOString()
75
+ : undefined,
76
+ } : null,
77
+ sevenDay: r.sevenDayPct != null ? {
78
+ usedPercent: r.sevenDayPct,
79
+ resetsAt: r.sevenDayResetsAt,
80
+ resetsAtIso: r.sevenDayResetsAt
81
+ ? new Date(r.sevenDayResetsAt * 1000).toISOString()
82
+ : undefined,
83
+ } : null,
84
+ updatedAt: r.updatedAt,
85
+ updatedAtIso: r.updatedAt
86
+ ? new Date(r.updatedAt * 1000).toISOString()
87
+ : undefined,
88
+ })),
89
+ collectedAt: snapshot.collectedAt,
90
+ });
91
+ });
92
+ // ── GET /ports ─────────────────────────────────────────────────────────────
93
+ router.get('/ports', (_req, res) => {
94
+ const snapshot = getSnapshot();
95
+ const agentPorts = [];
96
+ for (const s of snapshot.sessions) {
97
+ for (const child of s.children) {
98
+ if (child.port != null) {
99
+ agentPorts.push({
100
+ sessionId: s.sessionId,
101
+ agentCli: s.agentCli,
102
+ pid: child.pid,
103
+ port: child.port,
104
+ command: child.command,
105
+ });
106
+ }
107
+ }
108
+ }
109
+ res.json({
110
+ agentPorts,
111
+ orphanPorts: snapshot.orphanPorts,
112
+ collectedAt: snapshot.collectedAt,
113
+ });
114
+ });
115
+ // ── GET /snapshot ──────────────────────────────────────────────────────────
116
+ // Returns the raw `CollectorSnapshot`. Two consumers depend on this exact
117
+ // shape: (1) the MCP process's `fetchSnapshotFromApi()`, whose handlers walk
118
+ // `s.subagents.length`, `s.children`, `s.totalInputTokens`, etc.; (2) the web
119
+ // dashboard's `useAgentSnapshot()` hook, whose `AgentSession` TS type is the
120
+ // raw `collectors/types.ts` shape. Earlier versions ran sessions through
121
+ // `sessionSummary()` here, which collapsed `subagents` → `subagentCount` and
122
+ // `tokens.input` etc. — silently breaking both consumers (the web limped
123
+ // along on `?? 0` guards; the MCP crashed on the missing arrays). The slim
124
+ // summary shape lives on `/sessions` for the CLI's table view.
125
+ router.get('/snapshot', (_req, res) => {
126
+ const snapshot = getSnapshot();
127
+ res.json({
128
+ sessions: snapshot.sessions,
129
+ rateLimits: snapshot.rateLimits,
130
+ orphanPorts: snapshot.orphanPorts,
131
+ collectedAt: snapshot.collectedAt,
132
+ });
133
+ });
134
+ return router;
135
+ }
136
+ function sessionSummary(s) {
137
+ return {
138
+ agentCli: s.agentCli,
139
+ pid: s.pid,
140
+ sessionId: s.sessionId,
141
+ cwd: s.cwd,
142
+ projectName: s.projectName,
143
+ startedAt: s.startedAt,
144
+ startedAtIso: new Date(s.startedAt).toISOString(),
145
+ status: s.status,
146
+ model: s.model,
147
+ effort: s.effort || undefined,
148
+ contextPercent: Math.round(s.contextPercent * 10) / 10,
149
+ contextWindow: s.contextWindow,
150
+ tokens: {
151
+ input: s.totalInputTokens,
152
+ output: s.totalOutputTokens,
153
+ cacheRead: s.totalCacheRead,
154
+ cacheCreate: s.totalCacheCreate,
155
+ total: s.totalInputTokens + s.totalOutputTokens + s.totalCacheRead + s.totalCacheCreate,
156
+ },
157
+ turnCount: s.turnCount,
158
+ currentTasks: s.currentTasks,
159
+ memMb: s.memMb,
160
+ version: s.version || undefined,
161
+ git: {
162
+ branch: s.gitBranch || undefined,
163
+ added: s.gitAdded,
164
+ modified: s.gitModified,
165
+ },
166
+ initialPrompt: s.initialPrompt || undefined,
167
+ subagentCount: s.subagents.length,
168
+ childCount: s.children.length,
169
+ };
170
+ }
171
+ function sessionDetail(s) {
172
+ return {
173
+ ...sessionSummary(s),
174
+ subagents: s.subagents,
175
+ children: s.children,
176
+ toolCalls: s.toolCalls,
177
+ fileAccesses: s.fileAccesses,
178
+ tokenHistory: s.tokenHistory,
179
+ contextHistory: s.contextHistory,
180
+ compactionCount: s.compactionCount,
181
+ memFileCount: s.memFileCount,
182
+ memLineCount: s.memLineCount,
183
+ pendingSinceMs: s.pendingSinceMs,
184
+ thinkingSinceMs: s.thinkingSinceMs,
185
+ firstAssistantText: s.firstAssistantText || undefined,
186
+ };
187
+ }
@@ -47,6 +47,8 @@ const handler_1 = require("../approvals/handler");
47
47
  const manager_2 = require("../approval/manager");
48
48
  const manager_3 = require("../escalation/manager");
49
49
  const events_1 = require("./events");
50
+ const multi_collector_1 = require("../collectors/multi-collector");
51
+ const agent_monitor_1 = require("./routes/agent-monitor");
50
52
  // Import registry for Phase 2 hub navigation
51
53
  const registryPath = path.join(process.env.HOME || process.env.USERPROFILE || '', '.waymark', 'registry.json');
52
54
  function getRegistryProjects() {
@@ -161,6 +163,17 @@ else {
161
163
  console.warn('[waymark] ui-dist/ not found — dashboard will return a setup banner. ' +
162
164
  'Run `npm run build -w @way_marks/web` to build the dashboard.');
163
165
  }
166
+ // Agent monitor — MultiCollector on a 2 s timer (mirrors abtop polling interval).
167
+ // .unref() so the timer doesn't keep the event loop alive on SIGTERM; the API
168
+ // process should exit cleanly via the http server close.
169
+ const agentCollector = new multi_collector_1.MultiCollector();
170
+ let latestAgentSnapshot = agentCollector.tick();
171
+ const agentCollectorTimer = setInterval(() => {
172
+ latestAgentSnapshot = agentCollector.tick();
173
+ }, 2000);
174
+ agentCollectorTimer.unref();
175
+ // Mount agent-monitor REST API
176
+ app.use('/api/agent-monitor', (0, agent_monitor_1.createAgentMonitorRouter)(() => latestAgentSnapshot));
164
177
  // GET /api/events — Server-Sent Events stream for live UI updates
165
178
  app.get('/api/events', (req, res) => {
166
179
  const detach = (0, events_1.attachSubscriber)(res);
@@ -50,12 +50,23 @@ async function approvePendingAction(actionId, approvedBy = 'ui') {
50
50
  let after_snapshot;
51
51
  if (action.tool_name === 'write_file') {
52
52
  const { path: filePath, content } = JSON.parse(action.input_payload);
53
- const resolvedPath = path.resolve(filePath);
53
+ // Resolve against WAYMARK_PROJECT_ROOT, not process.cwd(). The policy engine
54
+ // (loadConfig in policies/engine.ts) uses the same env var; using cwd here
55
+ // would cause a relative target_path to resolve to a different absolute
56
+ // path than the one the policy was originally evaluated against — which
57
+ // would then re-trigger a policy block on approval.
58
+ const projectRoot = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
59
+ const resolvedPath = path.isAbsolute(filePath)
60
+ ? path.resolve(filePath)
61
+ : path.resolve(projectRoot, filePath);
54
62
  // Re-check current policies — they may have changed since the action was queued
55
63
  const currentConfig = (0, engine_1.loadConfig)();
56
64
  const recheck = (0, engine_1.checkFileAction)(resolvedPath, 'write', currentConfig);
57
65
  if (recheck.decision === 'block') {
58
- return { success: false, error: `Approval blocked: policy changed (${recheck.reason})` };
66
+ return {
67
+ success: false,
68
+ error: `Approval blocked: policy changed since action was recorded (${recheck.reason}). Resolved path: ${resolvedPath}`,
69
+ };
59
70
  }
60
71
  fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
61
72
  fs.writeFileSync(resolvedPath, content, 'utf8');