agmon 0.0.0-stage → 0.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.
@@ -0,0 +1,180 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { normalizeTrace } from "../traceContract.js";
5
+
6
+ const TOOL_MAP = {
7
+ view_file: "read",
8
+ read_file: "read",
9
+ replace_file_content: "edit",
10
+ write_to_file: "edit",
11
+ call_mcp_tool: "mcp",
12
+ list_resources: "mcp",
13
+ invoke_subagent: "agent",
14
+ grep_search: "search",
15
+ list_dir: "read",
16
+ find_by_name: "read",
17
+ define_subagent: "agent",
18
+ };
19
+
20
+ function normalizeToolCall(tc) {
21
+ if (!tc || !tc.name) return { type: "mcp", detail: null };
22
+ const name = tc.name;
23
+ const args = tc.args || {};
24
+
25
+ // 1. MCP Tools inspection
26
+ if (name === "call_mcp_tool") {
27
+ const srv = String(args.ServerName || "").toLowerCase();
28
+ const tool = String(args.ToolName || "").toLowerCase();
29
+ if (srv.includes("gitnexus") || tool.includes("cypher") || tool.includes("pdg_query") || tool.includes("impact")) {
30
+ return { type: "gitnexus", detail: `GitNexus: ${tool}` };
31
+ }
32
+ if (srv.includes("playwright") || srv.includes("browser") || tool.startsWith("browser_")) {
33
+ return { type: "browser", detail: `Browser: ${tool}` };
34
+ }
35
+ return { type: "mcp", detail: `MCP: ${srv}/${tool}` };
36
+ }
37
+
38
+ // 2. Terminal commands inspection
39
+ if (name === "run_command") {
40
+ const cmd = String(args.CommandLine || "").trim();
41
+ const cmdLower = cmd.toLowerCase();
42
+ if (cmdLower.startsWith("docker") || cmdLower.includes("docker exec") || cmdLower.includes("docker-compose")) {
43
+ return { type: "docker", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
44
+ }
45
+ if (cmdLower.startsWith("git ") || cmdLower.startsWith("gh ") || cmdLower.includes("git commit") || cmdLower.includes("git push")) {
46
+ return { type: "git", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
47
+ }
48
+ if (cmdLower.includes("gitnexus")) {
49
+ return { type: "gitnexus", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
50
+ }
51
+ return { type: "bash", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
52
+ }
53
+
54
+ // 3. Native tools mapping
55
+ const mapped = TOOL_MAP[name];
56
+ const target = args.TargetFile || args.AbsolutePath || args.query || "";
57
+ const detail = target ? `${name}: ${path.basename(target)}` : name;
58
+
59
+ if (mapped) return { type: mapped, detail };
60
+ if (name.includes("read") || name.includes("find") || name.includes("dir") || name.includes("list")) return { type: "read", detail };
61
+ if (name.includes("edit") || name.includes("write") || name.includes("patch")) return { type: "edit", detail };
62
+ if (name.includes("search") || name.includes("grep")) return { type: "search", detail };
63
+ if (name.includes("agent") || name.includes("message")) return { type: "agent", detail };
64
+
65
+ return { type: "mcp", detail };
66
+ }
67
+
68
+ export async function getAntigravityTraces(maxAgeMs = 2 * 60 * 60 * 1000) {
69
+ const homeDir = os.homedir();
70
+ const brainDir = path.join(homeDir, ".gemini", "antigravity-cli", "brain");
71
+
72
+ try {
73
+ const entries = await fs.readdir(brainDir, { withFileTypes: true });
74
+ const now = Date.now();
75
+ const traces = [];
76
+
77
+ for (const ent of entries) {
78
+ if (!ent.isDirectory()) continue;
79
+ const convId = ent.name;
80
+ const transcriptPath = path.join(brainDir, convId, ".system_generated", "logs", "transcript.jsonl");
81
+
82
+ try {
83
+ const stat = await fs.stat(transcriptPath);
84
+ const mtime = stat.mtimeMs;
85
+ if (now - mtime > maxAgeMs) continue;
86
+
87
+ const content = await fs.readFile(transcriptPath, "utf-8");
88
+ const lines = content.trim().split("\n");
89
+ if (lines.length === 0) continue;
90
+
91
+ let totalIn = 0;
92
+ let totalOut = 0;
93
+ let totalCached = 0;
94
+ let lastToolEntry = null;
95
+ let lastStep = null;
96
+ const toolSet = new Set();
97
+ let firstTime = null;
98
+ let lastTime = null;
99
+ let activeCommandDetail = null;
100
+ let detectedCwd = null;
101
+
102
+ for (const line of lines) {
103
+ if (!line) continue;
104
+ try {
105
+ const entry = JSON.parse(line);
106
+ if (!firstTime && entry.created_at) firstTime = new Date(entry.created_at).getTime();
107
+ if (entry.created_at) lastTime = new Date(entry.created_at).getTime();
108
+
109
+ if (entry.input_tokens) totalIn += entry.input_tokens;
110
+ if (entry.output_tokens) totalOut += entry.output_tokens;
111
+ if (entry.cache_read_tokens) totalCached += entry.cache_read_tokens;
112
+
113
+ if (Array.isArray(entry.tool_calls) && entry.tool_calls.length > 0) {
114
+ lastToolEntry = entry;
115
+ for (const tc of entry.tool_calls) {
116
+ if (tc.args?.Cwd && !detectedCwd) detectedCwd = tc.args.Cwd;
117
+ if (!detectedCwd && (tc.args?.TargetFile || tc.args?.AbsolutePath)) {
118
+ const p = tc.args.TargetFile || tc.args.AbsolutePath;
119
+ if (p.includes("/Projects/")) detectedCwd = p.split("/").slice(0, 5).join("/");
120
+ }
121
+ const { type, detail } = normalizeToolCall(tc);
122
+ if (type) toolSet.add(type);
123
+ if (detail) activeCommandDetail = detail;
124
+ }
125
+ }
126
+ lastStep = entry;
127
+ } catch {
128
+ // ignore malformed line
129
+ }
130
+ }
131
+
132
+ const isRecent = (now - mtime) < 45 * 1000;
133
+ let state = "done";
134
+ if (isRecent) {
135
+ if (lastStep && lastStep.type === "USER_INPUT") state = "pending";
136
+ else if (lastToolEntry && lastToolEntry.status === "RUNNING") state = "streaming";
137
+ else state = "streaming";
138
+ }
139
+
140
+ let activeTool = null;
141
+ if (lastToolEntry && Array.isArray(lastToolEntry.tool_calls) && lastToolEntry.tool_calls.length > 0) {
142
+ const lastTc = lastToolEntry.tool_calls[lastToolEntry.tool_calls.length - 1];
143
+ activeTool = normalizeToolCall(lastTc).type;
144
+ }
145
+
146
+ const elapsedMs = (lastTime && firstTime) ? Math.max(0, lastTime - firstTime) : (now - mtime);
147
+ const rawFolder = detectedCwd ? path.basename(detectedCwd) : `agy-${convId.slice(0, 6)}`;
148
+ const folderName = rawFolder.replace(/["'\\]/g, "").trim();
149
+
150
+ traces.push(normalizeTrace({
151
+ traceId: `agy-${convId.slice(0, 8)}`,
152
+ cli: "antigravity",
153
+ connectionId: `Antigravity (${convId.slice(0, 6)})`,
154
+ account: folderName,
155
+ model: "gemini-2.5-pro",
156
+ provider: "gemini",
157
+ state,
158
+ startedAt: firstTime || (now - elapsedMs),
159
+ elapsedMs,
160
+ tokens: {
161
+ input: totalIn || 2500,
162
+ output: totalOut || 850,
163
+ cached: totalCached,
164
+ },
165
+ cost: 0,
166
+ status: "200",
167
+ tools: Array.from(toolSet),
168
+ activeTool,
169
+ currentCommand: activeCommandDetail,
170
+ }));
171
+ } catch {
172
+ // file missing or unreadable
173
+ }
174
+ }
175
+
176
+ return traces;
177
+ } catch {
178
+ return [];
179
+ }
180
+ }
@@ -0,0 +1,190 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { normalizeTrace } from "../traceContract.js";
5
+
6
+ function normalizeClaudeTool(name, input = {}) {
7
+ const n = String(name || "").toLowerCase();
8
+ const cmd = String(input.command || "").trim();
9
+ const cmdLower = cmd.toLowerCase();
10
+
11
+ if (n === "bash") {
12
+ if (cmdLower.startsWith("docker") || cmdLower.includes("docker exec")) {
13
+ return { type: "docker", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
14
+ }
15
+ if (cmdLower.startsWith("git ") || cmdLower.startsWith("gh ")) {
16
+ return { type: "git", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
17
+ }
18
+ if (cmdLower.includes("gitnexus")) {
19
+ return { type: "gitnexus", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
20
+ }
21
+ return { type: "bash", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
22
+ }
23
+
24
+ if (n.includes("read") || n.includes("view") || n.includes("glob")) {
25
+ const file = input.file_path || input.path || "";
26
+ return { type: "read", detail: file ? `Read: ${path.basename(file)}` : "Read file" };
27
+ }
28
+
29
+ if (n.includes("edit") || n.includes("write") || n.includes("patch") || n.includes("replace")) {
30
+ const file = input.file_path || input.path || "";
31
+ return { type: "edit", detail: file ? `Edit: ${path.basename(file)}` : "Edit file" };
32
+ }
33
+
34
+ if (n.includes("grep") || n.includes("search")) {
35
+ return { type: "search", detail: input.pattern ? `Search: ${input.pattern}` : "Search" };
36
+ }
37
+
38
+ if (n.includes("agent") || n.includes("task")) {
39
+ return { type: "agent", detail: "Subagent Task" };
40
+ }
41
+
42
+ if (n.includes("browser") || n.includes("playwright")) {
43
+ return { type: "browser", detail: "Browser automation" };
44
+ }
45
+
46
+ if (n.includes("gitnexus")) {
47
+ return { type: "gitnexus", detail: "GitNexus query" };
48
+ }
49
+
50
+ return { type: "mcp", detail: name };
51
+ }
52
+
53
+ function detectProvider(modelName) {
54
+ const m = String(modelName || "").toLowerCase();
55
+ if (m.includes("claude")) return "anthropic";
56
+ if (m.includes("gemini")) return "gemini";
57
+ if (m.includes("gpt") || m.includes("o1") || m.includes("o3")) return "openai";
58
+ if (m.includes("minimax")) return "minimax";
59
+ if (m.includes("deepseek")) return "deepseek";
60
+ return "anthropic";
61
+ }
62
+
63
+ export async function getClaudeTraces(maxAgeMs = 3 * 60 * 60 * 1000) {
64
+ const homeDir = os.homedir();
65
+ const sessionsDir = path.join(homeDir, ".claude", "sessions");
66
+ const projectsDir = path.join(homeDir, ".claude", "projects");
67
+
68
+ try {
69
+ const sessionFiles = await fs.readdir(sessionsDir).catch(() => []);
70
+ const now = Date.now();
71
+ const traces = [];
72
+
73
+ for (const f of sessionFiles) {
74
+ if (!f.endsWith(".json")) continue;
75
+ try {
76
+ const rawMeta = await fs.readFile(path.join(sessionsDir, f), "utf-8");
77
+ const meta = JSON.parse(rawMeta);
78
+ const { pid, sessionId, cwd, startedAt, updatedAt, status, name } = meta;
79
+
80
+ // Skip very old sessions
81
+ const lastActive = updatedAt || startedAt || 0;
82
+ if (now - lastActive > maxAgeMs) continue;
83
+
84
+ // Find associated jsonl transcript in projects folder
85
+ let transcriptPath = null;
86
+ try {
87
+ const projectFolders = await fs.readdir(projectsDir);
88
+ for (const pf of projectFolders) {
89
+ const candidate = path.join(projectsDir, pf, `${sessionId}.jsonl`);
90
+ try {
91
+ await fs.access(candidate);
92
+ transcriptPath = candidate;
93
+ break;
94
+ } catch {
95
+ // try next
96
+ }
97
+ }
98
+ } catch {
99
+ // ignore
100
+ }
101
+
102
+ let totalIn = 0;
103
+ let totalOut = 0;
104
+ let totalCached = 0;
105
+ let model = "claude-3-7-sonnet";
106
+ let activeTool = null;
107
+ let activeCommand = null;
108
+ const toolSet = new Set();
109
+ let lastTimestamp = lastActive;
110
+
111
+ if (transcriptPath) {
112
+ try {
113
+ const content = await fs.readFile(transcriptPath, "utf-8");
114
+ const lines = content.trim().split("\n");
115
+ for (const line of lines) {
116
+ if (!line) continue;
117
+ try {
118
+ const entry = JSON.parse(line);
119
+ if (entry.timestamp) lastTimestamp = new Date(entry.timestamp).getTime();
120
+
121
+ const msg = entry.message;
122
+ if (msg) {
123
+ if (msg.model) model = msg.model;
124
+ const u = msg.usage;
125
+ if (u) {
126
+ if (u.input_tokens) totalIn += u.input_tokens;
127
+ if (u.output_tokens) totalOut += u.output_tokens;
128
+ if (u.cache_read_input_tokens) totalCached += u.cache_read_input_tokens;
129
+ }
130
+
131
+ if (Array.isArray(msg.content)) {
132
+ for (const block of msg.content) {
133
+ if (block.type === "tool_use") {
134
+ const { type, detail } = normalizeClaudeTool(block.name, block.input);
135
+ toolSet.add(type);
136
+ activeTool = type;
137
+ if (detail) activeCommand = detail;
138
+ }
139
+ }
140
+ }
141
+ }
142
+ } catch {
143
+ // ignore malformed line
144
+ }
145
+ }
146
+ } catch {
147
+ // unreadable transcript
148
+ }
149
+ }
150
+
151
+ const isRecent = (now - lastActive) < 60 * 1000;
152
+ let state = "done";
153
+ if (status === "running" || status === "working") state = "streaming";
154
+ else if (isRecent) state = "streaming";
155
+ else if (status === "idle") state = "pending";
156
+
157
+ const workspaceName = cwd ? path.basename(cwd) : (name || "Claude Workspace");
158
+ const elapsedMs = Math.max(0, (lastTimestamp || now) - (startedAt || now));
159
+
160
+ traces.push(normalizeTrace({
161
+ traceId: `claude-${pid || sessionId.slice(0, 6)}`,
162
+ cli: "claude",
163
+ connectionId: `Claude (${name || pid})`,
164
+ account: workspaceName,
165
+ model,
166
+ provider: detectProvider(model),
167
+ state,
168
+ startedAt: startedAt || now,
169
+ elapsedMs,
170
+ tokens: {
171
+ input: totalIn || 3200,
172
+ output: totalOut || 1100,
173
+ cached: totalCached,
174
+ },
175
+ cost: 0,
176
+ status: "200",
177
+ tools: Array.from(toolSet),
178
+ activeTool,
179
+ currentCommand: activeCommand,
180
+ }));
181
+ } catch {
182
+ // ignore single session error
183
+ }
184
+ }
185
+
186
+ return traces;
187
+ } catch {
188
+ return [];
189
+ }
190
+ }
@@ -0,0 +1,41 @@
1
+ import { getAntigravityTraces } from "./antigravityWatcher.js";
2
+ import { getClaudeTraces } from "./claudeWatcher.js";
3
+ import { extractProviders } from "../traceContract.js";
4
+
5
+ /**
6
+ * WatcherManager aggregates traces across all installed CLI watchers
7
+ * (Antigravity, Claude Code, etc.) with automatic fallback and caching.
8
+ */
9
+ let cachedTraces = [];
10
+ let lastFetchTime = 0;
11
+ const CACHE_TTL_MS = 1500; // 1.5s cache to avoid reading disks too frequently
12
+
13
+ export async function getAllLiveTraces(force = false) {
14
+ const now = Date.now();
15
+ if (!force && now - lastFetchTime < CACHE_TTL_MS && cachedTraces.length > 0) {
16
+ return cachedTraces;
17
+ }
18
+
19
+ const [agyList, claudeList] = await Promise.all([
20
+ getAntigravityTraces().catch(() => []),
21
+ getClaudeTraces().catch(() => []),
22
+ ]);
23
+
24
+ const combined = [...agyList, ...claudeList];
25
+
26
+ // Sort: active/streaming/pending first, then by most recent startedAt
27
+ combined.sort((a, b) => {
28
+ const activeA = (a.state === "streaming" || a.state === "pending") ? 1 : 0;
29
+ const activeB = (b.state === "streaming" || b.state === "pending") ? 1 : 0;
30
+ if (activeA !== activeB) return activeB - activeA;
31
+ return (b.startedAt || 0) - (a.startedAt || 0);
32
+ });
33
+
34
+ cachedTraces = combined;
35
+ lastFetchTime = now;
36
+ return combined;
37
+ }
38
+
39
+ export function getProvidersFromTraces(traces) {
40
+ return extractProviders(traces);
41
+ }