@jmylchreest/aide-plugin 0.0.59 → 0.0.61
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 +1 -1
- package/src/cli/codex-config.ts +15 -0
- package/src/cli/hook.ts +2 -0
- package/src/core/mcp-sync.ts +0 -16
- package/src/core/read-tracking.ts +38 -4
- package/src/core/search-enrichment.ts +208 -0
- package/src/core/tool-observe.ts +284 -0
- package/src/hooks/hud-updater.ts +0 -23
- package/src/hooks/search-enrichment.ts +111 -0
- package/src/hooks/session-start.ts +23 -0
- package/src/hooks/skill-injector.ts +30 -1
- package/src/hooks/subagent-tracker.ts +7 -85
- package/src/hooks/task-completed.ts +0 -17
- package/src/hooks/tool-observe.ts +97 -0
- package/src/lib/aide-downloader.ts +0 -8
- package/src/lib/hook-utils.ts +0 -13
- package/src/lib/logger.ts +1 -68
- package/src/lib/usage.ts +1 -30
- package/src/opencode/hooks.ts +36 -14
- package/src/lib/worktree.ts +0 -457
package/package.json
CHANGED
package/src/cli/codex-config.ts
CHANGED
|
@@ -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",
|
package/src/core/mcp-sync.ts
CHANGED
|
@@ -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
|
-
*
|
|
147
|
-
*
|
|
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
|
|
150
|
-
|
|
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
|
+
}
|
package/src/hooks/hud-updater.ts
CHANGED
|
@@ -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
|
}
|