@jmylchreest/aide-plugin 0.0.60 → 0.0.62

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmylchreest/aide-plugin",
3
- "version": "0.0.60",
3
+ "version": "0.0.62",
4
4
  "description": "aide plugin for OpenCode and Codex CLI — multi-agent orchestration, memory, skills, and persistence",
5
5
  "type": "module",
6
6
  "main": "./src/opencode/index.ts",
@@ -183,6 +183,11 @@ function generateHooksJson(hookPrefix: string): CodexHooksJson {
183
183
  command: `${hookPrefix} context-guard`,
184
184
  timeout: 2,
185
185
  },
186
+ {
187
+ type: "command",
188
+ command: `${hookPrefix} search-enrichment`,
189
+ timeout: 3,
190
+ },
186
191
  ],
187
192
  },
188
193
  ],
@@ -190,6 +195,16 @@ function generateHooksJson(hookPrefix: string): CodexHooksJson {
190
195
  {
191
196
  matcher: "*",
192
197
  hooks: [
198
+ {
199
+ type: "command",
200
+ command: `${hookPrefix} tool-observe`,
201
+ timeout: 3,
202
+ },
203
+ {
204
+ type: "command",
205
+ command: `${hookPrefix} hud-updater`,
206
+ timeout: 3,
207
+ },
193
208
  {
194
209
  type: "command",
195
210
  command: `${hookPrefix} comment-checker`,
package/src/cli/hook.ts CHANGED
@@ -23,7 +23,9 @@ const HOOK_MAP: Record<string, string> = {
23
23
  "write-guard": "write-guard.ts",
24
24
  "pre-tool-enforcer": "pre-tool-enforcer.ts",
25
25
  "context-guard": "context-guard.ts",
26
+ "search-enrichment": "search-enrichment.ts",
26
27
  "hud-updater": "hud-updater.ts",
28
+ "tool-observe": "tool-observe.ts",
27
29
  "comment-checker": "comment-checker.ts",
28
30
  "context-pruning": "context-pruning.ts",
29
31
  "persistence": "persistence.ts",
@@ -1047,22 +1047,6 @@ export function syncMcpServers(
1047
1047
  return { user, project };
1048
1048
  }
1049
1049
 
1050
- /**
1051
- * Get the list of currently synced MCP servers (for display/logging).
1052
- */
1053
- export function listSyncedServers(cwd: string): {
1054
- user: string[];
1055
- project: string[];
1056
- } {
1057
- const userServers = readAideConfig(aideUserMcpPath());
1058
- const projectServers = readAideConfig(aideProjectMcpPath(cwd));
1059
-
1060
- return {
1061
- user: Object.keys(userServers),
1062
- project: Object.keys(projectServers),
1063
- };
1064
- }
1065
-
1066
1050
  /**
1067
1051
  * Get the current removed (blocked) server names.
1068
1052
  * Derived from the v2 journal: a server is "removed" if its latest
@@ -143,9 +143,43 @@ export function recordTokenEvent(
143
143
  }
144
144
 
145
145
  /**
146
- * Estimate tokens for a file by its size, using the default ratio.
147
- * This is a rough client-side estimate; the Go binary has per-language ratios.
146
+ * Record an arbitrary observe event via `aide observe record`.
147
+ * Use when you need richer fields than recordTokenEvent (per-skill name with
148
+ * a stable subtype, attrs, etc.). Fire-and-forget.
148
149
  */
149
- export function estimateTokensFromSize(sizeBytes: number): number {
150
- return Math.round(sizeBytes / 3.0);
150
+ export function recordObserveEvent(
151
+ binary: string,
152
+ cwd: string,
153
+ opts: {
154
+ kind: string;
155
+ name: string;
156
+ category?: string;
157
+ subtype?: string;
158
+ tokens?: number;
159
+ saved?: number;
160
+ file?: string;
161
+ session?: string;
162
+ attrs?: Record<string, string>;
163
+ },
164
+ ): void {
165
+ try {
166
+ const args = ["observe", "record", `--kind=${opts.kind}`, `--name=${opts.name}`];
167
+ if (opts.category) args.push(`--category=${opts.category}`);
168
+ if (opts.subtype) args.push(`--subtype=${opts.subtype}`);
169
+ if (opts.tokens !== undefined) args.push(`--tokens=${opts.tokens}`);
170
+ if (opts.saved !== undefined) args.push(`--saved=${opts.saved}`);
171
+ if (opts.file) args.push(`--file=${opts.file}`);
172
+ if (opts.session) args.push(`--session=${opts.session}`);
173
+ for (const [k, v] of Object.entries(opts.attrs ?? {})) {
174
+ args.push(`--attr=${k}=${v}`);
175
+ }
176
+ execFileSync(binary, args, {
177
+ cwd,
178
+ timeout: 3000,
179
+ stdio: ["pipe", "pipe", "pipe"],
180
+ });
181
+ debug(SOURCE, `Observe event: ${opts.kind} ${opts.name} subtype=${opts.subtype ?? ""} tokens=${opts.tokens ?? 0}`);
182
+ } catch (err) {
183
+ debug(SOURCE, `Failed to record observe event: ${err}`);
184
+ }
151
185
  }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Search Enrichment — platform-agnostic core logic.
3
+ *
4
+ * Enriches Grep tool calls with structural context from the code index.
5
+ * When an agent greps for a symbol name, this appends metadata about
6
+ * matching symbol definitions (file, kind, ref count) so the agent
7
+ * knows where the symbol is defined and how widely it's used — without
8
+ * making additional tool calls.
9
+ *
10
+ * Behaviour:
11
+ * - Triggers on Grep tool calls where the pattern looks like a symbol name
12
+ * - Calls `aide code search <pattern> --json --limit=5` to find definitions
13
+ * - For each match, calls `aide code references <name> --json --limit=0` for ref count
14
+ * - Returns a compact enrichment string (~50-150 tokens)
15
+ * - Never blocks — purely additive context
16
+ *
17
+ * Gated behind AIDE_CODE_WATCH=1 (requires code index to be populated).
18
+ *
19
+ * Used by both Claude Code hooks (PreToolUse) and OpenCode plugin.
20
+ */
21
+
22
+ import { execFileSync } from "child_process";
23
+ import { debug } from "../lib/logger.js";
24
+
25
+ const SOURCE = "search-enrichment";
26
+
27
+ /** Minimum pattern length to attempt enrichment (avoid single-char patterns) */
28
+ const MIN_PATTERN_LENGTH = 3;
29
+
30
+ /** Maximum time to wait for aide binary responses */
31
+ const EXEC_TIMEOUT_MS = 3000;
32
+
33
+ /**
34
+ * Patterns that are clearly regex, not symbol names.
35
+ * Skip enrichment for these — the code index won't have useful matches.
36
+ */
37
+ const REGEX_INDICATORS = /[.*+?^${}()|[\]\\]/;
38
+
39
+ export interface SearchEnrichmentResult {
40
+ /** Whether to inject enrichment context */
41
+ shouldEnrich: boolean;
42
+ /** Enrichment context to append */
43
+ enrichment?: string;
44
+ }
45
+
46
+ interface SymbolHit {
47
+ name: string;
48
+ kind: string;
49
+ file: string;
50
+ start: number;
51
+ end: number;
52
+ signature: string;
53
+ lang: string;
54
+ }
55
+
56
+ /**
57
+ * Check whether a Grep tool call should receive code index enrichment.
58
+ *
59
+ * Extracts the search pattern, looks it up in the code index, and returns
60
+ * a compact summary of matching symbol definitions with ref counts.
61
+ */
62
+ export function checkSearchEnrichment(
63
+ toolName: string,
64
+ toolInput: Record<string, unknown>,
65
+ cwd: string,
66
+ binary: string | null,
67
+ ): SearchEnrichmentResult {
68
+ const normalizedTool = toolName.toLowerCase();
69
+
70
+ // Only enrich Grep tool calls
71
+ if (normalizedTool !== "grep") {
72
+ return { shouldEnrich: false };
73
+ }
74
+
75
+ // Require code watcher to be enabled (implies code index exists)
76
+ if (process.env.AIDE_CODE_WATCH !== "1") {
77
+ return { shouldEnrich: false };
78
+ }
79
+
80
+ if (!binary) {
81
+ return { shouldEnrich: false };
82
+ }
83
+
84
+ // Extract the search pattern
85
+ const pattern =
86
+ (toolInput.pattern as string) ||
87
+ (toolInput.query as string) ||
88
+ (toolInput.search as string);
89
+
90
+ if (!pattern || pattern.length < MIN_PATTERN_LENGTH) {
91
+ return { shouldEnrich: false };
92
+ }
93
+
94
+ // Skip patterns that are clearly regex (not symbol names)
95
+ if (REGEX_INDICATORS.test(pattern)) {
96
+ return { shouldEnrich: false };
97
+ }
98
+
99
+ // Skip patterns with spaces (likely searching for phrases, not symbols)
100
+ if (pattern.includes(" ")) {
101
+ return { shouldEnrich: false };
102
+ }
103
+
104
+ // Look up matching symbols in the code index
105
+ const symbols = searchSymbols(binary, cwd, pattern);
106
+ if (symbols.length === 0) {
107
+ return { shouldEnrich: false };
108
+ }
109
+
110
+ // Build compact enrichment string
111
+ const lines: string[] = [];
112
+ lines.push(`[aide:code-index] Symbol definitions matching "${pattern}":`);
113
+
114
+ for (const sym of symbols) {
115
+ const refCount = countReferences(binary, cwd, sym.name);
116
+ const refs = refCount > 0 ? `, ${refCount} refs` : ", 0 refs";
117
+ lines.push(` ${sym.kind} ${sym.name} — ${sym.file}:${sym.start}${refs}`);
118
+ }
119
+
120
+ if (symbols.length > 0) {
121
+ lines.push(
122
+ `Use code_read_symbol for source, code_references for call sites.`,
123
+ );
124
+ }
125
+
126
+ const enrichment = lines.join("\n");
127
+ debug(SOURCE, `Enriching grep for "${pattern}": ${symbols.length} symbols`);
128
+
129
+ return { shouldEnrich: true, enrichment };
130
+ }
131
+
132
+ /**
133
+ * Search the code index for symbol definitions matching a pattern.
134
+ */
135
+ function searchSymbols(
136
+ binary: string,
137
+ cwd: string,
138
+ pattern: string,
139
+ ): SymbolHit[] {
140
+ try {
141
+ const output = execFileSync(
142
+ binary,
143
+ ["code", "search", pattern, "--json", "--limit=5"],
144
+ {
145
+ cwd,
146
+ encoding: "utf-8",
147
+ timeout: EXEC_TIMEOUT_MS,
148
+ stdio: ["pipe", "pipe", "pipe"],
149
+ },
150
+ );
151
+
152
+ const trimmed = output.trim();
153
+ if (!trimmed || trimmed.startsWith("No matching")) {
154
+ return [];
155
+ }
156
+
157
+ const parsed = JSON.parse(trimmed);
158
+ if (!Array.isArray(parsed)) return [];
159
+
160
+ return parsed.map(
161
+ (s: Record<string, unknown>): SymbolHit => ({
162
+ name: (s.name as string) || "",
163
+ kind: (s.kind as string) || "",
164
+ file: (s.file as string) || "",
165
+ start: (s.start as number) || 0,
166
+ end: (s.end as number) || 0,
167
+ signature: (s.signature as string) || "",
168
+ lang: (s.lang as string) || "",
169
+ }),
170
+ );
171
+ } catch (err) {
172
+ debug(SOURCE, `Symbol search failed: ${err}`);
173
+ return [];
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Count references to a symbol name in the code index.
179
+ * Returns the count, or 0 on error.
180
+ */
181
+ function countReferences(
182
+ binary: string,
183
+ cwd: string,
184
+ symbolName: string,
185
+ ): number {
186
+ try {
187
+ const output = execFileSync(
188
+ binary,
189
+ ["code", "references", symbolName, "--json", "--limit=100"],
190
+ {
191
+ cwd,
192
+ encoding: "utf-8",
193
+ timeout: EXEC_TIMEOUT_MS,
194
+ stdio: ["pipe", "pipe", "pipe"],
195
+ },
196
+ );
197
+
198
+ const trimmed = output.trim();
199
+ if (!trimmed || trimmed.startsWith("No references")) {
200
+ return 0;
201
+ }
202
+
203
+ const parsed = JSON.parse(trimmed);
204
+ return Array.isArray(parsed) ? parsed.length : 0;
205
+ } catch {
206
+ return 0;
207
+ }
208
+ }
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Tool observability — single source of truth for native tool → observe.Event
3
+ * mapping. Used by both the Claude Code PostToolUse hook and the OpenCode
4
+ * tool.execute.after handler so dashboard categorisation stays consistent
5
+ * across plugins.
6
+ *
7
+ * Mirror image of the MCP-side mcpToolTaxonomy in cmd_mcp.go: native tools
8
+ * (Read, Edit, Bash, ...) flow through here; MCP tools (code_outline,
9
+ * findings_search, ...) flow through the middleware. Together they give
10
+ * complete tool-call coverage in the observe store.
11
+ */
12
+
13
+ import { execFileSync } from "child_process";
14
+ import { statSync } from "fs";
15
+ import { isAbsolute, resolve } from "path";
16
+ import { debug } from "../lib/logger.js";
17
+ import { recordFileRead } from "./read-tracking.js";
18
+
19
+ const SOURCE = "tool-observe";
20
+
21
+ /**
22
+ * Category + subtype for one native tool. Categories mirror the MCP taxonomy:
23
+ * consume — pulls content into context (Read)
24
+ * modify — changes files (Edit, Write, NotebookEdit)
25
+ * search — finds things without consuming much (Grep, Glob)
26
+ * execute — runs external commands (Bash)
27
+ * network — fetches over the network (WebFetch, WebSearch)
28
+ * coordinate— delegates work (Task)
29
+ * navigate — read-only state queries (TodoWrite read-side, etc.)
30
+ */
31
+ interface ToolTax {
32
+ category: string;
33
+ subtype: string;
34
+ }
35
+
36
+ /**
37
+ * Native tool → (category, subtype). Names use Claude Code's canonical casing
38
+ * (PascalCase). The OpenCode call site lowercases before lookup so the same
39
+ * table serves both ("read" → "Read").
40
+ */
41
+ const NATIVE_TOOL_TAXONOMY: Record<string, ToolTax> = {
42
+ Read: { category: "consume", subtype: "file" },
43
+ Edit: { category: "modify", subtype: "file" },
44
+ Write: { category: "modify", subtype: "file" },
45
+ NotebookEdit: { category: "modify", subtype: "notebook" },
46
+ Grep: { category: "search", subtype: "content" },
47
+ Glob: { category: "search", subtype: "path" },
48
+ Bash: { category: "execute", subtype: "shell" },
49
+ WebFetch: { category: "network", subtype: "fetch" },
50
+ WebSearch: { category: "network", subtype: "search" },
51
+ Task: { category: "coordinate", subtype: "subagent" },
52
+ TodoWrite: { category: "coordinate", subtype: "todo" },
53
+ };
54
+
55
+ /**
56
+ * Cross-harness aliases. Codex and other harnesses name the same primitives
57
+ * differently — `update`/`apply_patch` for Edit, `view` for Read, `shell`
58
+ * for Bash, etc. Mapping them to Claude Code's canonical names keeps the
59
+ * dashboard's per-tool aggregation coherent across plugins (no separate
60
+ * "update" + "Edit" buckets that mean the same thing).
61
+ *
62
+ * Lookup is case-insensitive; aliases here are the lowercase form.
63
+ */
64
+ const TOOL_ALIASES: Record<string, string> = {
65
+ // Codex / OpenAI-style tool names
66
+ update: "Edit",
67
+ apply_patch: "Edit",
68
+ str_replace_editor: "Edit",
69
+ view: "Read",
70
+ read_file: "Read",
71
+ get: "Read",
72
+ create: "Write",
73
+ shell: "Bash",
74
+ exec: "Bash",
75
+ fetch: "WebFetch",
76
+ search_web: "WebSearch",
77
+ };
78
+
79
+ /** Tools whose tokens we estimate from on-disk file size (the Read path). */
80
+ const FILE_SIZED_TOOLS = new Set(["Read"]);
81
+
82
+ /**
83
+ * Tools whose token cost is the size of content the agent *writes* — the
84
+ * `new_string` for Edit, the `content` for Write. We track these so the
85
+ * "modify" category in per-tool efficiency surfaces something other than
86
+ * a flat zero.
87
+ */
88
+ const CONTENT_WRITE_TOOLS: Record<string, string> = {
89
+ Edit: "new_string",
90
+ Write: "content",
91
+ NotebookEdit: "new_source",
92
+ };
93
+
94
+ /**
95
+ * Tools whose cost is the size of the *output* they produce — Bash stdout,
96
+ * WebFetch page body, WebSearch results, Grep match lines. The PostToolUse
97
+ * payload carries the tool's response so we can estimate the tokens that
98
+ * flowed back into the agent's context.
99
+ */
100
+ const OUTPUT_SIZED_TOOLS = new Set(["Bash", "WebFetch", "WebSearch", "Grep"]);
101
+
102
+ /**
103
+ * Pull the textual output from a tool_response / tool_result payload. The
104
+ * shape varies by tool and by harness (Claude Code passes string for Bash,
105
+ * objects for others; OpenCode wraps things differently), so we try the
106
+ * common keys defensively and return "" when there's no text to count.
107
+ */
108
+ function extractOutputText(payload: unknown): string {
109
+ if (!payload) return "";
110
+ if (typeof payload === "string") return payload;
111
+ if (typeof payload === "object") {
112
+ const obj = payload as Record<string, unknown>;
113
+ for (const key of ["output", "stdout", "content", "text", "result"]) {
114
+ const v = obj[key];
115
+ if (typeof v === "string") return v;
116
+ }
117
+ }
118
+ return "";
119
+ }
120
+
121
+ export interface ToolObserveInput {
122
+ toolName: string;
123
+ toolInput?: {
124
+ file_path?: string;
125
+ offset?: number;
126
+ limit?: number;
127
+ command?: string;
128
+ pattern?: string;
129
+ new_string?: string;
130
+ content?: string;
131
+ new_source?: string;
132
+ [key: string]: unknown;
133
+ };
134
+ /**
135
+ * The tool's response payload, used to estimate output token cost for
136
+ * Bash/WebFetch/WebSearch/Grep. Shape varies per tool and per harness;
137
+ * extractOutputText handles the common cases.
138
+ */
139
+ toolResponse?: unknown;
140
+ success?: boolean;
141
+ sessionId?: string;
142
+ }
143
+
144
+ /**
145
+ * Resolve a native tool name (any casing) to its taxonomy entry. Returns
146
+ * `null` for tools we don't classify — callers skip recording rather than
147
+ * pollute the dashboard with an "other" bucket.
148
+ *
149
+ * Lookup order: exact → case-insensitive → cross-harness alias.
150
+ */
151
+ function lookupTool(name: string): ToolTax | null {
152
+ if (NATIVE_TOOL_TAXONOMY[name]) return NATIVE_TOOL_TAXONOMY[name];
153
+ const lower = name.toLowerCase();
154
+ for (const [k, v] of Object.entries(NATIVE_TOOL_TAXONOMY)) {
155
+ if (k.toLowerCase() === lower) return v;
156
+ }
157
+ const canonical = TOOL_ALIASES[lower];
158
+ if (canonical && NATIVE_TOOL_TAXONOMY[canonical]) {
159
+ return NATIVE_TOOL_TAXONOMY[canonical];
160
+ }
161
+ return null;
162
+ }
163
+
164
+ /**
165
+ * Resolve to the canonical tool name (Edit/Read/Write/...) so observe
166
+ * events from different harnesses aggregate into the same bucket on the
167
+ * dashboard. Falls back to the original name when no alias matches.
168
+ */
169
+ function canonicalToolName(name: string): string {
170
+ if (NATIVE_TOOL_TAXONOMY[name]) return name;
171
+ const lower = name.toLowerCase();
172
+ for (const k of Object.keys(NATIVE_TOOL_TAXONOMY)) {
173
+ if (k.toLowerCase() === lower) return k;
174
+ }
175
+ return TOOL_ALIASES[lower] ?? name;
176
+ }
177
+
178
+ /**
179
+ * Estimate tokens for the Read tool. If offset/limit are present, scale by
180
+ * the portion actually read. Returns 0 on stat failure (caller still records
181
+ * the event so the call shows up in the timeline).
182
+ */
183
+ function estimateReadTokens(
184
+ cwd: string,
185
+ filePath: string,
186
+ offset?: number,
187
+ limit?: number,
188
+ ): number {
189
+ try {
190
+ const abs = isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
191
+ const stat = statSync(abs);
192
+ const fullTokens = Math.round(stat.size / 3.0);
193
+ if (limit !== undefined && limit > 0 && stat.size > 0) {
194
+ const estTotalLines = Math.max(1, Math.round(stat.size / 35));
195
+ const linesRead = Math.min(limit, estTotalLines - (offset || 0));
196
+ return Math.round(fullTokens * (linesRead / estTotalLines));
197
+ }
198
+ return fullTokens;
199
+ } catch {
200
+ return 0;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Record a native tool invocation as an observe.KindToolCall event. Pure
206
+ * fire-and-forget: failures are logged but never thrown so this is safe to
207
+ * call from tight hook hot paths. Callers should pass success=true; we still
208
+ * record on success=false so failed invocations are visible in the timeline.
209
+ */
210
+ export function recordToolEvent(
211
+ binary: string,
212
+ cwd: string,
213
+ input: ToolObserveInput,
214
+ ): void {
215
+ const tax = lookupTool(input.toolName);
216
+ if (!tax) {
217
+ debug(SOURCE, `Skipping unclassified tool: ${input.toolName}`);
218
+ return;
219
+ }
220
+
221
+ const filePath = input.toolInput?.file_path as string | undefined;
222
+ let tokens = 0;
223
+ let startLine: number | undefined;
224
+ let endLine: number | undefined;
225
+ if (FILE_SIZED_TOOLS.has(input.toolName) && filePath) {
226
+ const offset = input.toolInput?.offset as number | undefined;
227
+ const limit = input.toolInput?.limit as number | undefined;
228
+ tokens = estimateReadTokens(cwd, filePath, offset, limit);
229
+ // Read tool offset/limit are line-based (1-based when present, default
230
+ // 1..end). Persist the range so the dashboard's file viewer can
231
+ // scroll/highlight the slice the agent actually consumed.
232
+ startLine = offset && offset > 0 ? offset : 1;
233
+ if (limit && limit > 0) {
234
+ endLine = startLine + limit - 1;
235
+ }
236
+ // Smart-read-hint state: record that this file was read so subsequent
237
+ // re-reads can be flagged as candidates for code_outline/code_symbols.
238
+ // No-op when AIDE_CODE_WATCH is unset.
239
+ recordFileRead(binary, cwd, filePath);
240
+ } else if (CONTENT_WRITE_TOOLS[input.toolName]) {
241
+ // Modify tools: the cost is the new content the agent generates,
242
+ // not the existing file. Same chars/3 estimator the Read path uses.
243
+ const field = CONTENT_WRITE_TOOLS[input.toolName];
244
+ const content = input.toolInput?.[field];
245
+ if (typeof content === "string" && content.length > 0) {
246
+ tokens = Math.round(content.length / 3.0);
247
+ }
248
+ } else if (OUTPUT_SIZED_TOOLS.has(input.toolName)) {
249
+ // Output-sized tools: cost = how much text came back into context.
250
+ // Stays 0 when the harness didn't pass a tool_response (some hooks
251
+ // strip it for size). That's still useful — we get the call count.
252
+ const text = extractOutputText(input.toolResponse);
253
+ if (text.length > 0) {
254
+ tokens = Math.round(text.length / 3.0);
255
+ }
256
+ }
257
+
258
+ try {
259
+ const args = [
260
+ "observe",
261
+ "record",
262
+ "--kind=tool_call",
263
+ `--name=${input.toolName}`,
264
+ `--category=${tax.category}`,
265
+ `--subtype=${tax.subtype}`,
266
+ ];
267
+ if (tokens > 0) args.push(`--tokens=${tokens}`);
268
+ if (filePath) args.push(`--file=${filePath}`);
269
+ if (input.sessionId) args.push(`--session=${input.sessionId}`);
270
+ if (startLine !== undefined) args.push(`--attr=start_line=${startLine}`);
271
+ if (endLine !== undefined) args.push(`--attr=end_line=${endLine}`);
272
+ execFileSync(binary, args, {
273
+ cwd,
274
+ timeout: 3000,
275
+ stdio: ["pipe", "pipe", "pipe"],
276
+ });
277
+ debug(
278
+ SOURCE,
279
+ `Recorded ${input.toolName} ${tax.category}/${tax.subtype} tokens=${tokens}`,
280
+ );
281
+ } catch (err) {
282
+ debug(SOURCE, `Failed to record ${input.toolName}: ${err}`);
283
+ }
284
+ }
@@ -8,8 +8,6 @@
8
8
  * Output is written to .aide/state/hud.txt for the terminal to display.
9
9
  */
10
10
 
11
- import { statSync } from "fs";
12
- import { resolve, isAbsolute } from "path";
13
11
  import { Logger, debug } from "../lib/logger.js";
14
12
  import { readStdin } from "../lib/hook-utils.js";
15
13
 
@@ -17,7 +15,6 @@ const SOURCE = "hud-updater";
17
15
  import { findAideBinary } from "../core/aide-client.js";
18
16
  import { updateToolStats } from "../core/tool-tracking.js";
19
17
  import { storePartialMemory } from "../core/partial-memory.js";
20
- import { recordFileRead, recordTokenEvent, estimateTokensFromSize } from "../core/read-tracking.js"; // estimateTokensFromSize used for read events
21
18
  import {
22
19
  getAgentStates,
23
20
  loadHudConfig,
@@ -90,26 +87,6 @@ async function main(): Promise<void> {
90
87
  success: data.tool_result?.success,
91
88
  });
92
89
 
93
- // Record file reads for smart-read-hint feature
94
- if (
95
- toolName === "Read" &&
96
- data.tool_result?.success &&
97
- data.tool_input?.file_path
98
- ) {
99
- const fp = data.tool_input.file_path as string;
100
- recordFileRead(binary, cwd, fp);
101
-
102
- // Record token event for the read (estimate from file size)
103
- try {
104
- const abs = isAbsolute(fp) ? fp : resolve(cwd, fp);
105
- const stat = statSync(abs);
106
- const tokens = estimateTokensFromSize(stat.size);
107
- recordTokenEvent(binary, cwd, "read", "Read", fp, tokens);
108
- } catch {
109
- // stat failed — skip token recording
110
- }
111
- }
112
-
113
90
  }
114
91
  log.end("updateSessionState");
115
92
  }