Forge
csharpc0b81713
1using System.Reflection;
2using System.Text.Json;
3using AgentNotes.Core;
4
5namespace AgentNotesMcp.Status;
6
7internal sealed class AgentNotesStatusSnapshot
8{
9 internal required string McpVersion { get; init; }
10
11 internal required int ProcessId { get; init; }
12
13 internal required long UptimeSeconds { get; init; }
14
15 internal required DateTimeOffset StartedAtUtc { get; init; }
16
17 internal required string ConfigPath { get; init; }
18
19 internal required string StatusUrl { get; init; }
20
21 internal required string? BindWarning { get; init; }
22
23 internal required KnowledgeBlock Knowledge { get; init; }
24
25 internal required WorkspaceBlock Workspace { get; init; }
26
27 internal required IReadOnlyList<ToolSummary> Tools { get; init; }
28
29 internal required IReadOnlyList<RecentToolCall> RecentToolCalls { get; init; }
30
31 internal static AgentNotesStatusSnapshot Create(
32 NotesStorage storage,
33 DateTimeOffset startedAt,
34 string statusUrl,
35 string? bindWarning,
36 string? workspacePath,
37 bool verbose)
38 {
39 var settings = AgentNotesRuntime.Settings;
40 var configPath = AgentNotesRuntime.ConfigFilePath ?? "(unknown)";
41 var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "2.0.0";
42 var uptime = (long)Math.Max(0, (DateTimeOffset.UtcNow - startedAt).TotalSeconds);
43
44 var previewWorkspace = settings.Status.PreviewWorkspace;
45 var effectiveWorkspace = !string.IsNullOrWhiteSpace(workspacePath)
46 ? Path.GetFullPath(workspacePath.Trim())
47 : previewWorkspace;
48
49 MemoryHealthBlock? memory = null;
50 if (!string.IsNullOrWhiteSpace(effectiveWorkspace))
51 memory = MemoryHealthBlock.TryParse(storage.MemoryHealth(effectiveWorkspace, activeScope: null));
52
53 return new AgentNotesStatusSnapshot
54 {
55 McpVersion = version,
56 ProcessId = Environment.ProcessId,
57 UptimeSeconds = uptime,
58 StartedAtUtc = startedAt,
59 ConfigPath = configPath,
60 StatusUrl = statusUrl,
61 BindWarning = bindWarning,
62 Knowledge = KnowledgeBlock.FromSettings(settings, verbose),
63 Workspace = WorkspaceBlock.FromSettings(settings, effectiveWorkspace, previewWorkspace, memory),
64 Tools = ToolCatalog.ListSummaries(),
65 MemoryHealth = memory,
66 RecentToolCalls = AgentNotesToolCallRingBuffer.Snapshot()
67 .Select(e => RecentToolCall.From(e))
68 .ToArray()
69 };
70 }
71
72 internal MemoryHealthBlock? MemoryHealth { get; init; }
73
74 internal void WriteJson(Utf8JsonWriter writer, bool verbose)
75 {
76 writer.WriteStartObject();
77 writer.WriteString("mcp_version", McpVersion);
78 writer.WriteNumber("pid", ProcessId);
79 writer.WriteNumber("uptime_seconds", UptimeSeconds);
80 writer.WriteString("started_at_utc", StartedAtUtc.ToString("O"));
81 writer.WriteString("config_path", ConfigPath);
82 writer.WriteString("status_url", StatusUrl);
83 if (BindWarning is not null)
84 writer.WriteString("bind_warning", BindWarning);
85
86 Knowledge.WriteTo(writer, verbose);
87 Workspace.WriteTo(writer, verbose);
88 MemoryHealth?.WriteTo(writer, verbose);
89
90 writer.WritePropertyName("tools");
91 writer.WriteStartArray();
92 foreach (var tool in Tools)
93 {
94 writer.WriteStartObject();
95 writer.WriteString("name", tool.Name);
96 writer.WriteString("description", tool.Description);
97 writer.WriteEndObject();
98 }
99
100 writer.WriteEndArray();
101
102 writer.WritePropertyName("recent_tool_calls");
103 writer.WriteStartArray();
104 foreach (var call in RecentToolCalls)
105 call.WriteTo(writer, verbose);
106 writer.WriteEndArray();
107
108 writer.WriteEndObject();
109 }
110
111 internal sealed record RecentToolCall(
112 DateTimeOffset AtUtc,
113 string ToolName,
114 string? WorkspacePath,
115 bool IsError,
116 long DurationMs,
117 string ResultPreview)
118 {
119 internal static RecentToolCall From(AgentNotesToolCallRingBuffer.ToolCallEntry e) =>
120 new(e.AtUtc, e.ToolName, e.WorkspacePath, e.IsError, e.DurationMs, e.ResultPreview);
121
122 internal void WriteTo(Utf8JsonWriter writer, bool verbose)
123 {
124 writer.WriteStartObject();
125 writer.WriteString("at_utc", AtUtc.ToString("O"));
126 writer.WriteString("tool", ToolName);
127 if (WorkspacePath is not null)
128 writer.WriteString("workspace_path", verbose ? WorkspacePath : SummarizePath(WorkspacePath));
129 writer.WriteBoolean("is_error", IsError);
130 writer.WriteNumber("duration_ms", DurationMs);
131 writer.WriteString("result_preview", ResultPreview);
132 writer.WriteEndObject();
133 }
134 }
135
136 internal sealed class KnowledgeBlock
137 {
138 internal required string PrimaryRoot { get; init; }
139
140 internal required string NotesPath { get; init; }
141
142 internal required bool NotesExists { get; init; }
143
144 internal required IReadOnlyList<NamedRoot> NamedRoots { get; init; }
145
146 internal required IReadOnlyList<ReadOnlyRoot> ReadOnlyRoots { get; init; }
147
148 internal required bool ReadOnlyRoutingEnabled { get; init; }
149
150 internal static KnowledgeBlock FromSettings(LocalSettings settings, bool verbose)
151 {
152 var primary = settings.PrimaryKnowledgeRoot;
153 var notesPath = Path.Combine(primary, "agent-notes.md");
154 var named = settings.KnowledgeRoots
155 .Select(kv => new NamedRoot(kv.Key, kv.Value, Directory.Exists(kv.Value)))
156 .OrderBy(r => r.Id, StringComparer.Ordinal)
157 .ToArray();
158
159 var readOnly = settings.ReadOnlyKnowledgeRoots
160 .Select(r => new ReadOnlyRoot(r.Id, r.Path, Directory.Exists(r.Path)))
161 .ToArray();
162
163 return new KnowledgeBlock
164 {
165 PrimaryRoot = verbose ? primary : SummarizePath(primary),
166 NotesPath = verbose ? notesPath : SummarizePath(notesPath),
167 NotesExists = File.Exists(notesPath),
168 NamedRoots = named,
169 ReadOnlyRoots = readOnly,
170 ReadOnlyRoutingEnabled = readOnly.Length > 0
171 };
172 }
173
174 internal void WriteTo(Utf8JsonWriter writer, bool verbose)
175 {
176 writer.WritePropertyName("knowledge");
177 writer.WriteStartObject();
178 writer.WriteString("primary_root", PrimaryRoot);
179 writer.WriteString("notes_path", NotesPath);
180 writer.WriteBoolean("notes_exists", NotesExists);
181 writer.WriteBoolean("read_only_routing_enabled", ReadOnlyRoutingEnabled);
182
183 writer.WritePropertyName("named_roots");
184 writer.WriteStartArray();
185 foreach (var root in NamedRoots)
186 {
187 writer.WriteStartObject();
188 writer.WriteString("id", root.Id);
189 writer.WriteString("path", verbose ? root.Path : SummarizePath(root.Path));
190 writer.WriteBoolean("exists", root.Exists);
191 writer.WriteEndObject();
192 }
193
194 writer.WriteEndArray();
195
196 writer.WritePropertyName("read_only_roots");
197 writer.WriteStartArray();
198 foreach (var root in ReadOnlyRoots)
199 {
200 writer.WriteStartObject();
201 writer.WriteString("id", root.Id);
202 writer.WriteString("path", verbose ? root.Path : SummarizePath(root.Path));
203 writer.WriteBoolean("exists", root.Exists);
204 writer.WriteEndObject();
205 }
206
207 writer.WriteEndArray();
208 writer.WriteEndObject();
209 }
210 }
211
212 internal sealed record NamedRoot(string Id, string Path, bool Exists);
213
214 internal sealed record ReadOnlyRoot(string Id, string Path, bool Exists);
215
216 internal sealed class WorkspaceBlock
217 {
218 internal required string? EffectiveWorkspace { get; init; }
219
220 internal required string? PreviewWorkspace { get; init; }
221
222 internal required string DefaultScope { get; init; }
223
224 internal required string ScopeMapRelative { get; init; }
225
226 internal required string ScopeAliasMapRelative { get; init; }
227
228 internal required bool ScopeMapExists { get; init; }
229
230 internal required bool ScopeAliasMapExists { get; init; }
231
232 internal required string? ResolvedScope { get; init; }
233
234 internal static WorkspaceBlock FromSettings(
235 LocalSettings settings,
236 string? effectiveWorkspace,
237 string? previewWorkspace,
238 MemoryHealthBlock? memory)
239 {
240 var primary = settings.PrimaryKnowledgeRoot;
241 var scopeMapPath = Path.Combine(primary, "knowledge", settings.Workspace.ScopeMapRelative);
242 var aliasPath = Path.Combine(primary, "knowledge", settings.Workspace.ScopeAliasMapRelative);
243
244 return new WorkspaceBlock
245 {
246 EffectiveWorkspace = effectiveWorkspace,
247 PreviewWorkspace = previewWorkspace,
248 DefaultScope = settings.Workspace.DefaultScope,
249 ScopeMapRelative = settings.Workspace.ScopeMapRelative,
250 ScopeAliasMapRelative = settings.Workspace.ScopeAliasMapRelative,
251 ScopeMapExists = File.Exists(scopeMapPath),
252 ScopeAliasMapExists = File.Exists(aliasPath),
253 ResolvedScope = memory?.ResolvedScope
254 };
255 }
256
257 internal void WriteTo(Utf8JsonWriter writer, bool verbose)
258 {
259 writer.WritePropertyName("workspace");
260 writer.WriteStartObject();
261 if (EffectiveWorkspace is not null)
262 writer.WriteString("effective_path", verbose ? EffectiveWorkspace : SummarizePath(EffectiveWorkspace));
263 if (PreviewWorkspace is not null)
264 writer.WriteString("preview_path", verbose ? PreviewWorkspace : SummarizePath(PreviewWorkspace));
265 writer.WriteString("default_scope", DefaultScope);
266 writer.WriteString("scope_map", ScopeMapRelative);
267 writer.WriteBoolean("scope_map_exists", ScopeMapExists);
268 writer.WriteString("scope_aliases", ScopeAliasMapRelative);
269 writer.WriteBoolean("scope_aliases_exists", ScopeAliasMapExists);
270 if (ResolvedScope is not null)
271 writer.WriteString("resolved_scope", ResolvedScope);
272 writer.WriteEndObject();
273 }
274 }
275
276 internal sealed class MemoryHealthBlock
277 {
278 internal required string HealthLevel { get; init; }
279
280 internal required string ResolvedScope { get; init; }
281
282 internal required string NotesPath { get; init; }
283
284 internal required bool NotesExists { get; init; }
285
286 internal required int HotChars { get; init; }
287
288 internal required int HotLines { get; init; }
289
290 internal required int SectionCount { get; init; }
291
292 internal required IReadOnlyList<string> Warnings { get; init; }
293
294 internal required IReadOnlyList<string> Recommendations { get; init; }
295
296 internal required IReadOnlyList<string> HotSectionIds { get; init; }
297
298 internal static MemoryHealthBlock? TryParse(string json)
299 {
300 try
301 {
302 using var doc = JsonDocument.Parse(json);
303 var root = doc.RootElement;
304 var hot = root.GetProperty("hot_context");
305 var sectionIds = hot.TryGetProperty("section_ids", out var ids)
306 ? ids.EnumerateArray().Select(e => e.GetString() ?? "").Where(s => s.Length > 0).ToArray()
307 : Array.Empty<string>();
308
309 return new MemoryHealthBlock
310 {
311 HealthLevel = root.GetProperty("health_level").GetString() ?? "unknown",
312 ResolvedScope = root.GetProperty("resolved_scope").GetString() ?? "",
313 NotesPath = root.GetProperty("notes_path").GetString() ?? "",
314 NotesExists = root.TryGetProperty("notes_exists", out var ne) && ne.GetBoolean(),
315 HotChars = hot.GetProperty("chars").GetInt32(),
316 HotLines = hot.GetProperty("lines").GetInt32(),
317 SectionCount = root.GetProperty("section_count").GetInt32(),
318 Warnings = ReadStringArray(root, "warnings"),
319 Recommendations = ReadStringArray(root, "recommendations"),
320 HotSectionIds = sectionIds
321 };
322 }
323 catch
324 {
325 return null;
326 }
327 }
328
329 internal void WriteTo(Utf8JsonWriter writer, bool verbose)
330 {
331 writer.WritePropertyName("memory_health");
332 writer.WriteStartObject();
333 writer.WriteString("health_level", HealthLevel);
334 writer.WriteString("resolved_scope", ResolvedScope);
335 writer.WriteString("notes_path", verbose ? NotesPath : SummarizePath(NotesPath));
336 writer.WriteBoolean("notes_exists", NotesExists);
337 writer.WriteNumber("hot_chars", HotChars);
338 writer.WriteNumber("hot_lines", HotLines);
339 writer.WriteNumber("section_count", SectionCount);
340 WriteStringArray(writer, "warnings", Warnings);
341 WriteStringArray(writer, "recommendations", Recommendations);
342 WriteStringArray(writer, "hot_section_ids", HotSectionIds);
343 writer.WriteEndObject();
344 }
345
346 private static IReadOnlyList<string> ReadStringArray(JsonElement root, string name)
347 {
348 if (!root.TryGetProperty(name, out var arr) || arr.ValueKind != JsonValueKind.Array)
349 return Array.Empty<string>();
350
351 return arr.EnumerateArray()
352 .Select(e => e.GetString() ?? "")
353 .Where(s => s.Length > 0)
354 .ToArray();
355 }
356 }
357
358 internal sealed record ToolSummary(string Name, string Description);
359
360 private static string SummarizePath(string path)
361 {
362 if (path.Length <= 64)
363 return path;
364
365 var file = Path.GetFileName(path);
366 var root = Path.GetPathRoot(path) ?? "";
367 var tail = path.Length > root.Length + 20
368 ? "…" + path[^36..]
369 : path;
370 return string.IsNullOrEmpty(file) ? tail : $"{root}…{file}";
371 }
372
373 private static void WriteStringArray(Utf8JsonWriter writer, string name, IReadOnlyList<string> values)
374 {
375 writer.WritePropertyName(name);
376 writer.WriteStartArray();
377 foreach (var value in values)
378 writer.WriteStringValue(value);
379 writer.WriteEndArray();
380 }
381}
382
View only · write via MCP/CIDE