atom-agent 0.3.0 → 1.1.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 (61) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +83 -32
  3. package/dist/App.js +2178 -318
  4. package/dist/adapters.js +146 -15
  5. package/dist/agent/gates.js +153 -0
  6. package/dist/agent/loop-guard.js +184 -0
  7. package/dist/agent/loop.js +908 -0
  8. package/dist/agent/normalize.js +144 -0
  9. package/dist/agent/types.js +1 -0
  10. package/dist/auth.js +2 -1
  11. package/dist/cli.js +68 -6
  12. package/dist/compact.js +6 -48
  13. package/dist/config.js +171 -0
  14. package/dist/context-manager.js +564 -0
  15. package/dist/kilo.js +343 -0
  16. package/dist/local-discovery.js +308 -0
  17. package/dist/policy.js +286 -0
  18. package/dist/prompt-cache.js +99 -0
  19. package/dist/providers.js +183 -2
  20. package/dist/rollback.js +21 -0
  21. package/dist/scheduler.js +247 -0
  22. package/dist/session.js +35 -3
  23. package/dist/skills.js +214 -43
  24. package/dist/snapshots.js +57 -2
  25. package/dist/system.js +8 -1
  26. package/dist/telemetry-dashboard.js +589 -0
  27. package/dist/telemetry-server.js +301 -0
  28. package/dist/telemetry.js +1056 -0
  29. package/dist/tools/dir-cache.js +207 -0
  30. package/dist/tools/filesystem.js +149 -0
  31. package/dist/tools/fingerprints.js +33 -0
  32. package/dist/tools/overflow.js +76 -0
  33. package/dist/tools/read-cache.js +160 -0
  34. package/dist/tools/registry.js +802 -0
  35. package/dist/tools/search.js +242 -0
  36. package/dist/tools/shared.js +31 -0
  37. package/dist/tools/shell.js +273 -0
  38. package/dist/tools/todo.js +191 -0
  39. package/dist/tools/web.js +454 -0
  40. package/dist/tools.js +17 -1863
  41. package/dist/ui/activity.js +51 -0
  42. package/dist/ui/diff-panel.js +55 -0
  43. package/dist/ui/diff-view.js +112 -0
  44. package/dist/ui/diff.js +422 -0
  45. package/dist/ui/errors.js +129 -0
  46. package/dist/ui/highlight.js +120 -0
  47. package/dist/ui/input-model.js +115 -0
  48. package/dist/ui/input.js +40 -0
  49. package/dist/ui/live-tail.js +15 -0
  50. package/dist/ui/markdown.js +525 -0
  51. package/dist/ui/modals.js +47 -0
  52. package/dist/ui/palette.js +70 -0
  53. package/dist/ui/pickers.js +32 -0
  54. package/dist/ui/side-by-side.js +144 -0
  55. package/dist/ui/status-bar.js +75 -0
  56. package/dist/ui/theme.js +128 -0
  57. package/dist/ui/todo-panel.js +30 -0
  58. package/dist/ui/tool-inspector.js +59 -0
  59. package/dist/ui/transcript.js +128 -0
  60. package/dist/zen.js +145 -666
  61. package/package.json +1 -1
@@ -0,0 +1,301 @@
1
+ // Local observability webUI: a tiny read-only HTTP server over the same
2
+ // telemetry store the static dashboard reads. The static `--dashboard` file
3
+ // is untouched — this is a convenience live view for the browser.
4
+ //
5
+ // Design (production-quality, minimal blast radius):
6
+ // - `node:http` builtin only. No new dependencies.
7
+ // - Loopback-only by default (`127.0.0.1`). Traces hold scrubbed previews,
8
+ // never keys — but they still describe your machine, so the server never
9
+ // binds a LAN interface unless explicitly asked (`host` option).
10
+ // - Read-only: GET only (anything else is 405), no request body is ever
11
+ // read, nothing is ever written. Every handler is guarded — a bad request
12
+ // or a corrupt store yields a status code, never a crash.
13
+ // - Live data: every request re-reads the store, so the page and the JSON
14
+ // API always reflect the latest flushed turn. Responses carry
15
+ // `Cache-Control: no-store`.
16
+ // - Same honesty rules as the static page: the HTML comes from the shared
17
+ // `buildDashboardHtml` builder (plus a meta refresh), and the JSON API
18
+ // only carries reported values with `usageReported`-style flags — absent
19
+ // stays absent, never zero-filled.
20
+ //
21
+ // Routes:
22
+ // - `GET /` → live dashboard HTML (auto-refreshes every 5s)
23
+ // - `GET /api/health` → `{ok, version, sessions, turns, service}`
24
+ // - `GET /api/aggregates` → `summarizeTelemetry(sessions)` as JSON
25
+ // - `GET /api/sessions` → per-session summaries (counts + reported usage)
26
+ // - `GET /api/sessions/:id` → one full stored session, or 404
27
+ // - anything else → 404; non-GET → 405.
28
+ import { createServer } from "node:http";
29
+ import { TELEMETRY_VERSION, loadTelemetrySessions, summarizeTelemetry, telemetryDir, } from "./telemetry.js";
30
+ import { buildDashboardHtml } from "./telemetry-dashboard.js";
31
+ export const TELEMETRY_SERVER_DEFAULT_HOST = "127.0.0.1";
32
+ // Ephemeral by default (port 0): zero collision failures, the real URL is
33
+ // printed on start. Pin with --port / ATOM_TELEMETRY_PORT when bookmarkable
34
+ // matters.
35
+ export const TELEMETRY_SERVER_DEFAULT_PORT = 0;
36
+ export const TELEMETRY_SERVER_REFRESH_SECONDS = 5;
37
+ export const TELEMETRY_SERVER_PORT_ENV = "ATOM_TELEMETRY_PORT";
38
+ // Parse a port candidate (CLI flag or env). Returns the port when it is an
39
+ // integer in range, else null (caller falls through to the next source).
40
+ export function parseTelemetryPort(value) {
41
+ if (value === undefined || value === null)
42
+ return null;
43
+ const text = String(value).trim();
44
+ if (!/^\d+$/.test(text))
45
+ return null;
46
+ const n = Number(text);
47
+ if (!Number.isSafeInteger(n) || n < 1 || n > 65535)
48
+ return null;
49
+ return n;
50
+ }
51
+ // Precedence: explicit CLI value > ATOM_TELEMETRY_PORT > ephemeral (0).
52
+ export function resolveTelemetryPort(env = process.env, cliValue) {
53
+ return (parseTelemetryPort(cliValue) ??
54
+ parseTelemetryPort(env[TELEMETRY_SERVER_PORT_ENV]) ??
55
+ TELEMETRY_SERVER_DEFAULT_PORT);
56
+ }
57
+ function emptyOutcomes() {
58
+ return {
59
+ completed: 0,
60
+ blocked: 0,
61
+ unverified: 0,
62
+ "budget-exceeded": 0,
63
+ failed: 0,
64
+ cancelled: 0,
65
+ pending: 0,
66
+ };
67
+ }
68
+ export function summarizeSession(s) {
69
+ const summary = {
70
+ sessionId: s.sessionId,
71
+ startedAt: s.startedAt,
72
+ endedAt: s.endedAt,
73
+ provider: s.provider,
74
+ model: s.model,
75
+ project: s.project,
76
+ turns: s.turns.length,
77
+ modelCalls: 0,
78
+ toolCalls: 0,
79
+ succeededToolCalls: 0,
80
+ failedToolCalls: 0,
81
+ usageReported: false,
82
+ usage: {},
83
+ outcomes: emptyOutcomes(),
84
+ retries: 0,
85
+ };
86
+ try {
87
+ for (const t of s.turns) {
88
+ if (t.outcome in summary.outcomes)
89
+ summary.outcomes[t.outcome] += 1;
90
+ summary.retries += typeof t.retryCount === "number" ? t.retryCount : 0;
91
+ summary.modelCalls += Array.isArray(t.modelCalls) ? t.modelCalls.length : 0;
92
+ if (t.usageReported) {
93
+ ["prompt_tokens", "completion_tokens", "total_tokens", "cacheReadTokens", "cacheWriteTokens"].forEach((k) => {
94
+ // Reported-only accumulation: a turn that reported nothing
95
+ // contributes nothing (never zero-filled).
96
+ if (t.usage[k] !== undefined) {
97
+ summary.usageReported = true;
98
+ summary.usage[k] = (summary.usage[k] ?? 0) + t.usage[k];
99
+ }
100
+ });
101
+ }
102
+ if (Array.isArray(t.toolCalls)) {
103
+ for (const c of t.toolCalls) {
104
+ summary.toolCalls += 1;
105
+ if (c.success)
106
+ summary.succeededToolCalls += 1;
107
+ else
108
+ summary.failedToolCalls += 1;
109
+ }
110
+ }
111
+ }
112
+ }
113
+ catch {
114
+ // summaries are best-effort; return what accumulated
115
+ }
116
+ return summary;
117
+ }
118
+ function sendJson(res, status, body) {
119
+ try {
120
+ const text = JSON.stringify(body);
121
+ res.writeHead(status, {
122
+ "Content-Type": "application/json; charset=utf-8",
123
+ "Cache-Control": "no-store",
124
+ "X-Content-Type-Options": "nosniff",
125
+ "Content-Length": Buffer.byteLength(text),
126
+ });
127
+ res.end(text);
128
+ }
129
+ catch {
130
+ try {
131
+ res.end();
132
+ }
133
+ catch {
134
+ // never throw out of a handler
135
+ }
136
+ }
137
+ }
138
+ function sendHtml(res, status, html) {
139
+ try {
140
+ res.writeHead(status, {
141
+ "Content-Type": "text/html; charset=utf-8",
142
+ "Cache-Control": "no-store",
143
+ "X-Content-Type-Options": "nosniff",
144
+ "Content-Length": Buffer.byteLength(html),
145
+ });
146
+ res.end(html);
147
+ }
148
+ catch {
149
+ try {
150
+ res.end();
151
+ }
152
+ catch {
153
+ // never throw out of a handler
154
+ }
155
+ }
156
+ }
157
+ const SESSION_ID_RE = /^[A-Za-z0-9_.-]+$/;
158
+ function handleRequest(req, res, home, refreshSeconds) {
159
+ try {
160
+ if (req.method !== "GET") {
161
+ sendJson(res, 405, { error: "method not allowed (read-only GET server)" });
162
+ return;
163
+ }
164
+ let pathname = "/";
165
+ try {
166
+ pathname = new URL(req.url ?? "/", "http://localhost").pathname;
167
+ }
168
+ catch {
169
+ sendJson(res, 400, { error: "bad request" });
170
+ return;
171
+ }
172
+ if (pathname === "/") {
173
+ const { sessions, corrupt } = loadTelemetrySessions(home);
174
+ const html = buildDashboardHtml(sessions, {
175
+ sourceDir: telemetryDir(home),
176
+ corruptFiles: corrupt,
177
+ refreshSeconds,
178
+ });
179
+ sendHtml(res, 200, html);
180
+ return;
181
+ }
182
+ if (pathname === "/api/health") {
183
+ const { sessions } = loadTelemetrySessions(home);
184
+ sendJson(res, 200, {
185
+ ok: true,
186
+ service: "atom-observability",
187
+ version: TELEMETRY_VERSION,
188
+ sessions: sessions.length,
189
+ turns: sessions.reduce((n, s) => n + s.turns.length, 0),
190
+ });
191
+ return;
192
+ }
193
+ if (pathname === "/api/aggregates") {
194
+ const { sessions } = loadTelemetrySessions(home);
195
+ sendJson(res, 200, summarizeTelemetry(sessions));
196
+ return;
197
+ }
198
+ if (pathname === "/api/sessions") {
199
+ const { sessions } = loadTelemetrySessions(home);
200
+ sendJson(res, 200, sessions.map(summarizeSession));
201
+ return;
202
+ }
203
+ if (pathname.startsWith("/api/sessions/")) {
204
+ const raw = pathname.slice("/api/sessions/".length);
205
+ let id = "";
206
+ try {
207
+ id = decodeURIComponent(raw);
208
+ }
209
+ catch {
210
+ sendJson(res, 400, { error: "bad session id" });
211
+ return;
212
+ }
213
+ // Sessions are looked up in the loaded list (never by path), but an
214
+ // invalid id is still a 404 without touching the store twice.
215
+ if (!SESSION_ID_RE.test(id)) {
216
+ sendJson(res, 404, { error: "session not found" });
217
+ return;
218
+ }
219
+ const { sessions } = loadTelemetrySessions(home);
220
+ const found = sessions.find((s) => s.sessionId === id);
221
+ if (!found) {
222
+ sendJson(res, 404, { error: "session not found" });
223
+ return;
224
+ }
225
+ sendJson(res, 200, found);
226
+ return;
227
+ }
228
+ sendJson(res, 404, { error: "not found" });
229
+ }
230
+ catch {
231
+ sendJson(res, 500, { error: "internal error" });
232
+ }
233
+ }
234
+ export function startTelemetryServer(opts = {}) {
235
+ const host = typeof opts.host === "string" && opts.host.length > 0 ? opts.host : TELEMETRY_SERVER_DEFAULT_HOST;
236
+ const port = typeof opts.port === "number" && Number.isSafeInteger(opts.port) && opts.port >= 0 && opts.port <= 65535
237
+ ? opts.port
238
+ : TELEMETRY_SERVER_DEFAULT_PORT;
239
+ const refreshSeconds = typeof opts.refreshSeconds === "number" && Number.isFinite(opts.refreshSeconds) && opts.refreshSeconds > 0
240
+ ? Math.floor(opts.refreshSeconds)
241
+ : TELEMETRY_SERVER_REFRESH_SECONDS;
242
+ const home = opts.home;
243
+ return new Promise((resolve, reject) => {
244
+ let server;
245
+ try {
246
+ server = createServer((req, res) => handleRequest(req, res, home, refreshSeconds));
247
+ }
248
+ catch (e) {
249
+ reject(e instanceof Error ? e : new Error(String(e)));
250
+ return;
251
+ }
252
+ // Track sockets so close() never hangs on keep-alive connections.
253
+ const sockets = new Set();
254
+ try {
255
+ server.on("connection", (s) => {
256
+ sockets.add(s);
257
+ s.on("close", () => sockets.delete(s));
258
+ });
259
+ }
260
+ catch {
261
+ // observer wiring must never break startup
262
+ }
263
+ const onError = (e) => {
264
+ reject(e instanceof Error ? e : new Error(String(e)));
265
+ };
266
+ server.once("error", onError);
267
+ server.listen(port, host, () => {
268
+ server.off("error", onError);
269
+ let actual = port;
270
+ try {
271
+ const addr = server.address();
272
+ if (addr !== null && typeof addr === "object")
273
+ actual = addr.port;
274
+ }
275
+ catch {
276
+ // keep the requested port in the URL on introspection failure
277
+ }
278
+ resolve({
279
+ url: `http://${host}:${actual}/`,
280
+ host,
281
+ port: actual,
282
+ close: () => new Promise((done) => {
283
+ try {
284
+ for (const s of sockets) {
285
+ try {
286
+ s.destroy();
287
+ }
288
+ catch {
289
+ // ignore per-socket failures
290
+ }
291
+ }
292
+ server.close(() => done());
293
+ }
294
+ catch {
295
+ done();
296
+ }
297
+ }),
298
+ });
299
+ });
300
+ });
301
+ }