@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.
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Search Enrichment Hook (PreToolUse)
4
+ *
5
+ * Enriches Grep tool calls with code index context — symbol definitions,
6
+ * file locations, and reference counts. This gives the agent structural
7
+ * awareness without additional tool calls.
8
+ *
9
+ * This is a soft advisory — it never blocks, only injects additive context.
10
+ *
11
+ * Core logic is in src/core/search-enrichment.ts for cross-platform reuse.
12
+ */
13
+
14
+ import { readStdin } from "../lib/hook-utils.js";
15
+ import { debug } from "../lib/logger.js";
16
+ import { checkSearchEnrichment } from "../core/search-enrichment.js";
17
+ import { findAideBinary } from "../core/aide-client.js";
18
+ import { recordTokenEvent } from "../core/read-tracking.js";
19
+
20
+ const SOURCE = "search-enrichment";
21
+
22
+ interface HookInput {
23
+ hook_event_name: string;
24
+ session_id: string;
25
+ cwd: string;
26
+ tool_name?: string;
27
+ agent_name?: string;
28
+ agent_id?: string;
29
+ tool_input?: Record<string, unknown>;
30
+ }
31
+
32
+ interface HookOutput {
33
+ continue: boolean;
34
+ message?: string;
35
+ hookSpecificOutput?: {
36
+ hookEventName: string;
37
+ additionalContext?: string;
38
+ };
39
+ }
40
+
41
+ async function main(): Promise<void> {
42
+ try {
43
+ const input = await readStdin();
44
+ if (!input.trim()) {
45
+ console.log(JSON.stringify({ continue: true }));
46
+ return;
47
+ }
48
+
49
+ const data: HookInput = JSON.parse(input);
50
+ const toolName = data.tool_name || "";
51
+ const toolInput = data.tool_input || {};
52
+ const cwd = data.cwd || process.cwd();
53
+
54
+ const binary = findAideBinary({
55
+ cwd,
56
+ pluginRoot:
57
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
58
+ });
59
+
60
+ const result = checkSearchEnrichment(toolName, toolInput, cwd, binary);
61
+
62
+ if (result.shouldEnrich && result.enrichment) {
63
+ debug(SOURCE, `Enriching grep with code index context`);
64
+
65
+ // Record token event for search enrichment
66
+ if (binary) {
67
+ try {
68
+ const tokens = Math.round(result.enrichment.length / 3.0);
69
+ recordTokenEvent(binary, cwd, "context_injected", "enrichment", "search-enrichment", tokens);
70
+ } catch {
71
+ // Non-fatal
72
+ }
73
+ }
74
+
75
+ const output: HookOutput = {
76
+ continue: true,
77
+ hookSpecificOutput: {
78
+ hookEventName: "PreToolUse",
79
+ additionalContext: result.enrichment,
80
+ },
81
+ };
82
+ console.log(JSON.stringify(output));
83
+ } else {
84
+ console.log(JSON.stringify({ continue: true }));
85
+ }
86
+ } catch (error) {
87
+ debug(SOURCE, `Hook error: ${error}`);
88
+ console.log(JSON.stringify({ continue: true }));
89
+ }
90
+ }
91
+
92
+ process.on("uncaughtException", (err) => {
93
+ debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
94
+ try {
95
+ console.log(JSON.stringify({ continue: true }));
96
+ } catch {
97
+ console.log('{"continue":true}');
98
+ }
99
+ process.exit(0);
100
+ });
101
+ process.on("unhandledRejection", (reason) => {
102
+ debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
103
+ try {
104
+ console.log(JSON.stringify({ continue: true }));
105
+ } catch {
106
+ console.log('{"continue":true}');
107
+ }
108
+ process.exit(0);
109
+ });
110
+
111
+ main();
@@ -26,6 +26,7 @@ import { homedir } from "os";
26
26
  import { Logger, debug, setDebugCwd } from "../lib/logger.js";
27
27
  import { readStdin, detectPlatform } from "../lib/hook-utils.js";
28
28
  import { findAideBinary, ensureAideBinary } from "../lib/aide-downloader.js";
29
+ import { recordTokenEvent } from "../core/read-tracking.js";
29
30
  import {
30
31
  ensureDirectories as coreEnsureDirectories,
31
32
  loadConfig as coreLoadConfig,
@@ -458,6 +459,28 @@ async function main(): Promise<void> {
458
459
  log.end("buildWelcomeContext");
459
460
  debugLog(`buildWelcomeContext complete (${Date.now() - hookStart}ms)`);
460
461
 
462
+ // Record token events for context injection
463
+ try {
464
+ const binary = findAideBinary(cwd);
465
+ if (binary && context) {
466
+ const memoryTokens = Math.round(
467
+ ([...memories.static.global, ...memories.static.project, ...memories.dynamic.sessions]
468
+ .join("").length) / 3.0
469
+ );
470
+ const decisionTokens = Math.round(
471
+ memories.static.decisions.join("").length / 3.0
472
+ );
473
+ if (memoryTokens > 0) {
474
+ recordTokenEvent(binary, cwd, "context_injected", "memory", "session-start", memoryTokens);
475
+ }
476
+ if (decisionTokens > 0) {
477
+ recordTokenEvent(binary, cwd, "context_injected", "decision", "session-start", decisionTokens);
478
+ }
479
+ }
480
+ } catch {
481
+ // Non-fatal — don't break session start for token tracking
482
+ }
483
+
461
484
  log.end("total");
462
485
  log.info("Session start complete");
463
486
  debugLog(`Flushing logs to ${log.getLogFile()}...`);
@@ -25,6 +25,8 @@ import {
25
25
  formatSkillsContext as coreFormatSkillsContext,
26
26
  } from "../core/skill-matcher.js";
27
27
  import type { Skill } from "../core/types.js";
28
+ import { findAideBinary } from "../core/aide-client.js";
29
+ import { recordObserveEvent } from "../core/read-tracking.js";
28
30
 
29
31
  const SOURCE = "skill-injector";
30
32
 
@@ -201,6 +203,33 @@ async function main(): Promise<void> {
201
203
  log.info(
202
204
  `Injecting ${matched.length} skills: ${matched.map((s) => s.name).join(", ")}`,
203
205
  );
206
+
207
+ const skillContext = formatSkillsContext(matched);
208
+
209
+ // Record one observe event per matched skill so the dashboard can
210
+ // attribute injected tokens to the specific skills (not just a
211
+ // single "skill-injector" aggregate). Subtype="skill" keeps the
212
+ // category roll-up; Name carries the per-skill identifier.
213
+ try {
214
+ const binary = findAideBinary({ cwd });
215
+ if (binary) {
216
+ for (const skill of matched) {
217
+ const text = `### ${skill.name}\n${skill.description ?? ""}\n${skill.content}`;
218
+ const tokens = Math.round(text.length / 3.0);
219
+ recordObserveEvent(binary, cwd, {
220
+ kind: "injection",
221
+ name: skill.name,
222
+ category: "inject",
223
+ subtype: "skill",
224
+ tokens,
225
+ file: SOURCE,
226
+ });
227
+ }
228
+ }
229
+ } catch {
230
+ // Non-fatal
231
+ }
232
+
204
233
  debugLog(`Flushing logs...`);
205
234
  log.flush();
206
235
  debugLog(`Hook complete (${Date.now() - hookStart}ms total)`);
@@ -209,7 +238,7 @@ async function main(): Promise<void> {
209
238
  continue: true,
210
239
  hookSpecificOutput: {
211
240
  hookEventName: "UserPromptSubmit",
212
- additionalContext: formatSkillsContext(matched),
241
+ additionalContext: skillContext,
213
242
  },
214
243
  };
215
244
  console.log(JSON.stringify(output));
@@ -20,12 +20,6 @@ import { Logger } from "../lib/logger.js";
20
20
  import { readStdin, setMemoryState } from "../lib/hook-utils.js";
21
21
  import { findAideBinary } from "../core/aide-client.js";
22
22
  import { refreshHud } from "../lib/hud.js";
23
- import {
24
- getWorktreeForAgent,
25
- markWorktreeComplete,
26
- discoverWorktrees,
27
- Worktree,
28
- } from "../lib/worktree.js";
29
23
 
30
24
  // Global logger instance
31
25
  let log: Logger | null = null;
@@ -207,36 +201,15 @@ function fetchSubagentMemories(cwd: string): {
207
201
  /**
208
202
  * Build context for subagent injection
209
203
  */
210
- function buildSubagentContext(
211
- memories: {
212
- global: string[];
213
- project: string[];
214
- decisions: string[];
215
- },
216
- worktree?: Worktree,
217
- ): string {
204
+ function buildSubagentContext(memories: {
205
+ global: string[];
206
+ project: string[];
207
+ decisions: string[];
208
+ }): string {
218
209
  const lines: string[] = [];
219
210
 
220
211
  lines.push("<aide-subagent-context>");
221
212
 
222
- // Inject worktree information if this is a swarm agent
223
- if (worktree) {
224
- lines.push("");
225
- lines.push("## Swarm Worktree");
226
- lines.push("");
227
- lines.push(`You are working in an isolated git worktree for swarm mode.`);
228
- lines.push(`- **Worktree Path**: ${worktree.path}`);
229
- lines.push(`- **Branch**: ${worktree.branch}`);
230
- lines.push(`- **Story ID**: ${worktree.taskId || "unknown"}`);
231
- lines.push("");
232
- lines.push(
233
- `**IMPORTANT**: All file operations should be performed in: ${worktree.path}`,
234
- );
235
- lines.push(
236
- `Commit your changes to the ${worktree.branch} branch when complete.`,
237
- );
238
- }
239
-
240
213
  if (memories.global.length > 0) {
241
214
  lines.push("");
242
215
  lines.push("## User Preferences");
@@ -349,46 +322,6 @@ async function processSubagentStart(
349
322
  refreshHud(cwd, session_id);
350
323
  log?.end("refreshHud");
351
324
 
352
- // Auto-discover any worktrees created by the orchestrator via git commands
353
- // This ensures we track worktrees even if they weren't created via our library
354
- log?.start("discoverWorktrees");
355
- const discovered = discoverWorktrees(cwd);
356
- if (discovered.length > 0) {
357
- log?.info(`Auto-discovered ${discovered.length} worktrees`);
358
- }
359
- log?.end("discoverWorktrees", { discovered: discovered.length });
360
-
361
- // Check if this agent has an associated worktree (swarm mode)
362
- // Match by agent_id or by pattern in worktree name
363
- log?.start("checkWorktree");
364
- let worktree = getWorktreeForAgent(cwd, agent_id);
365
-
366
- // If no direct match, try to match by agent_id pattern in worktree name
367
- // This handles cases where worktree was created before agent_id was known
368
- if (!worktree) {
369
- const { loadWorktreeState } = await import("../lib/worktree.js");
370
- const state = loadWorktreeState(cwd);
371
- // Look for worktree with matching name pattern (e.g., "story-auth" matches "agent-auth")
372
- const agentPattern = agent_id.replace(/^agent-/, "");
373
- worktree = state.active.find(
374
- (w) => w.name.includes(agentPattern) && !w.agentId,
375
- );
376
- if (worktree) {
377
- // Assign this agent to the worktree
378
- worktree.agentId = agent_id;
379
- const { saveWorktreeState } = await import("../lib/worktree.js");
380
- saveWorktreeState(cwd, state);
381
- log?.info(`Assigned worktree ${worktree.name} to agent ${agent_id}`);
382
- }
383
- }
384
-
385
- if (worktree) {
386
- log?.info(
387
- `Found worktree for agent ${agent_id}: ${worktree.path} (branch: ${worktree.branch})`,
388
- );
389
- }
390
- log?.end("checkWorktree", { hasWorktree: !!worktree });
391
-
392
325
  // Fetch memories for subagent context injection
393
326
  log?.start("fetchMemories");
394
327
  const memories = fetchSubagentMemories(cwd);
@@ -399,9 +332,9 @@ async function processSubagentStart(
399
332
  });
400
333
 
401
334
  // Always build and inject context (messaging section is unconditional)
402
- const context = buildSubagentContext(memories, worktree);
335
+ const context = buildSubagentContext(memories);
403
336
  log?.info(
404
- `Injecting context for subagent: ${memories.global.length} preferences, ${memories.project.length} project, ${memories.decisions.length} decisions, worktree=${!!worktree}`,
337
+ `Injecting context for subagent: ${memories.global.length} preferences, ${memories.project.length} project, ${memories.decisions.length} decisions`,
405
338
  );
406
339
  return context;
407
340
  }
@@ -422,17 +355,6 @@ async function processSubagentStop(data: SubagentStopInput): Promise<void> {
422
355
  setAgentState(cwd, agent_id, "endedAt", new Date().toISOString());
423
356
  log?.end("updateAgentStatus");
424
357
 
425
- // Mark worktree as agent-complete if this agent had one (swarm mode)
426
- // The worktree stays for merge review - cleanup happens after worktree-resolve
427
- log?.start("checkWorktreeComplete");
428
- const worktreeMarked = markWorktreeComplete(cwd, agent_id);
429
- if (worktreeMarked) {
430
- log?.info(
431
- `Marked worktree as agent-complete for ${agent_id} - ready for merge review`,
432
- );
433
- }
434
- log?.end("checkWorktreeComplete", { worktreeMarked });
435
-
436
358
  // Refresh HUD to remove the completed agent
437
359
  log?.start("refreshHud");
438
360
  refreshHud(cwd, session_id);
@@ -98,23 +98,6 @@ function commandSucceeds(cmd: string, cwd: string): boolean {
98
98
  return true;
99
99
  }
100
100
 
101
- /**
102
- * Get command output or null on failure (cross-platform via cross-spawn)
103
- */
104
- function getCommandOutput(cmd: string, cwd: string): string | null {
105
- const [bin, args] = splitCommand(cmd);
106
- if (!which.sync(bin, { nothrow: true })) {
107
- debug(SOURCE, `Binary not found in PATH: ${bin}`);
108
- return null;
109
- }
110
- const result = spawn.sync(bin, args, { cwd, stdio: "pipe", timeout: 30000 });
111
- if (result.status !== 0 || !result.stdout) {
112
- debug(SOURCE, `getCommandOutput failed for: ${cmd}: exit ${result.status}`);
113
- return null;
114
- }
115
- return result.stdout.toString().trim();
116
- }
117
-
118
101
  /**
119
102
  * Detect project type (typescript, go, python)
120
103
  */
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tool Observe Hook (PostToolUse)
4
+ *
5
+ * Single-purpose: record every Claude-native tool invocation as an
6
+ * observe.KindToolCall event. Mirror image of the MCP middleware on the Go
7
+ * side — together they give the dashboard complete tool-call coverage.
8
+ *
9
+ * All taxonomy (tool → category/subtype) and the recording itself live in
10
+ * src/core/tool-observe.ts so the OpenCode tool.execute.after handler can
11
+ * reuse the same logic.
12
+ */
13
+
14
+ import { readStdin } from "../lib/hook-utils.js";
15
+ import { findAideBinary } from "../core/aide-client.js";
16
+ import { recordToolEvent } from "../core/tool-observe.js";
17
+ import { debug } from "../lib/logger.js";
18
+
19
+ const SOURCE = "tool-observe";
20
+
21
+ interface HookInput {
22
+ hook_event_name: string;
23
+ session_id: string;
24
+ cwd: string;
25
+ tool_name?: string;
26
+ tool_input?: Record<string, unknown>;
27
+ tool_result?: { success: boolean };
28
+ // Claude Code passes the tool's actual output payload as tool_response.
29
+ // Shape varies per tool (string for Bash, object for others).
30
+ tool_response?: unknown;
31
+ }
32
+
33
+ function outputContinue(): void {
34
+ try {
35
+ console.log(JSON.stringify({ continue: true }));
36
+ } catch {
37
+ console.log('{"continue":true}');
38
+ }
39
+ }
40
+
41
+ async function main(): Promise<void> {
42
+ try {
43
+ const input = await readStdin();
44
+ if (!input.trim()) {
45
+ outputContinue();
46
+ return;
47
+ }
48
+ const data: HookInput = JSON.parse(input);
49
+ const cwd = data.cwd || process.cwd();
50
+ const toolName = data.tool_name;
51
+ if (!toolName) {
52
+ outputContinue();
53
+ return;
54
+ }
55
+ const binary = findAideBinary({
56
+ cwd,
57
+ pluginRoot:
58
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
59
+ });
60
+ if (!binary) {
61
+ outputContinue();
62
+ return;
63
+ }
64
+ recordToolEvent(binary, cwd, {
65
+ toolName,
66
+ toolInput: data.tool_input as ToolInput,
67
+ toolResponse: data.tool_response,
68
+ success: data.tool_result?.success,
69
+ sessionId: data.session_id,
70
+ });
71
+ } catch (err) {
72
+ debug(SOURCE, `Hook error: ${err}`);
73
+ }
74
+ outputContinue();
75
+ }
76
+
77
+ type ToolInput = {
78
+ file_path?: string;
79
+ offset?: number;
80
+ limit?: number;
81
+ command?: string;
82
+ pattern?: string;
83
+ };
84
+
85
+ process.on("uncaughtException", (err) => {
86
+ debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
87
+ outputContinue();
88
+ process.exit(0);
89
+ });
90
+
91
+ process.on("unhandledRejection", (reason) => {
92
+ debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
93
+ outputContinue();
94
+ process.exit(0);
95
+ });
96
+
97
+ main();
@@ -193,14 +193,6 @@ export function getDownloadUrls(): string[] {
193
193
  return [`https://github.com/jmylchreest/aide/releases/latest/download/${binaryName}`];
194
194
  }
195
195
 
196
- /**
197
- * Get the download URL for the current platform (first priority URL).
198
- * @deprecated Use getDownloadUrls() for fallback support.
199
- */
200
- export function getDownloadUrl(): string {
201
- return getDownloadUrls()[0];
202
- }
203
-
204
196
  /**
205
197
  * Get the plugin root directory (where package.json lives)
206
198
  */
@@ -146,19 +146,6 @@ export function getMemoryState(
146
146
  return getState(binary, cwd, key, agentId);
147
147
  }
148
148
 
149
- /**
150
- * Delete a state key from aide
151
- */
152
- export function deleteMemoryState(
153
- cwd: string,
154
- key: string,
155
- agentId?: string,
156
- ): boolean {
157
- const binary = findAideBinary(cwd);
158
- if (!binary) return false;
159
- return deleteState(binary, cwd, key, agentId);
160
- }
161
-
162
149
  /**
163
150
  * Clear all state for an agent
164
151
  */
package/src/lib/logger.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  * touch .aide/.debug # sentinel file (live toggle)
20
20
  */
21
21
 
22
- import { existsSync, mkdirSync, appendFileSync, writeFileSync } from "fs";
22
+ import { existsSync, mkdirSync, appendFileSync } from "fs";
23
23
  import { join } from "path";
24
24
 
25
25
  export type LogLevel = "debug" | "info" | "warn" | "error";
@@ -195,21 +195,6 @@ export class Logger {
195
195
  }
196
196
  }
197
197
 
198
- /**
199
- * Time a synchronous operation
200
- */
201
- timeSync<T>(label: string, fn: () => T): T {
202
- this.start(label);
203
- try {
204
- const result = fn();
205
- this.end(label);
206
- return result;
207
- } catch (err) {
208
- this.end(label, { error: String(err) });
209
- throw err;
210
- }
211
- }
212
-
213
198
  /**
214
199
  * Format entries for file output
215
200
  */
@@ -265,43 +250,6 @@ export class Logger {
265
250
  }
266
251
  }
267
252
 
268
- /**
269
- * Write to a custom log file (relative to .aide/_logs/)
270
- */
271
- writeToFile(filename: string, content: string): void {
272
- if (!this.enabled) return;
273
-
274
- try {
275
- this.ensureLogDir();
276
- const filepath = join(this.logDir, filename);
277
- writeFileSync(filepath, content);
278
- } catch {
279
- // Silently fail
280
- }
281
- }
282
-
283
- /**
284
- * Append to a custom log file (relative to .aide/_logs/)
285
- */
286
- appendToFile(filename: string, content: string): void {
287
- if (!this.enabled) return;
288
-
289
- try {
290
- this.ensureLogDir();
291
- const filepath = join(this.logDir, filename);
292
- appendFileSync(filepath, content);
293
- } catch {
294
- // Silently fail
295
- }
296
- }
297
-
298
- /**
299
- * Get the log directory path
300
- */
301
- getLogDir(): string {
302
- return this.logDir;
303
- }
304
-
305
253
  /**
306
254
  * Get the main log file path
307
255
  */
@@ -318,21 +266,6 @@ export class Logger {
318
266
  }
319
267
  }
320
268
 
321
- /**
322
- * Create a singleton logger instance for quick use
323
- *
324
- * Note: Currently not used by hooks. Hooks create Logger instances
325
- * directly for more control. Exported for potential future use.
326
- */
327
- let defaultLogger: Logger | null = null;
328
-
329
- export function getLogger(source: string, cwd?: string): Logger {
330
- if (!defaultLogger || defaultLogger["source"] !== source) {
331
- defaultLogger = new Logger(source, cwd);
332
- }
333
- return defaultLogger;
334
- }
335
-
336
269
  // Debug log state - tracks cwd for file-based logging
337
270
  let debugLogCwd = process.cwd();
338
271
 
package/src/lib/usage.ts CHANGED
@@ -50,12 +50,6 @@ export interface RealtimeUsage {
50
50
  messagesToday: number;
51
51
  }
52
52
 
53
- export interface UsageSummary {
54
- limits: APILimits | null;
55
- realtime: RealtimeUsage;
56
- timestamp: string;
57
- }
58
-
59
53
  interface OAuthCredentials {
60
54
  claudeAiOauth?: {
61
55
  accessToken: string;
@@ -89,14 +83,7 @@ let apiLimitsCache: CacheEntry<APILimits> | null = null;
89
83
  const API_CACHE_TTL = 30_000; // 30 seconds
90
84
 
91
85
  let realtimeCache: CacheEntry<RealtimeUsage> | null = null;
92
- let realtimeCacheTTL = 60_000; // 1 minute default, configurable
93
-
94
- /**
95
- * Set the cache TTL for realtime usage data (JSONL scanning).
96
- */
97
- export function setRealtimeCacheTTL(ms: number): void {
98
- realtimeCacheTTL = ms;
99
- }
86
+ const realtimeCacheTTL = 60_000;
100
87
 
101
88
  // =============================================================================
102
89
  // OAuth API
@@ -430,22 +417,6 @@ async function scanSingleFile(
430
417
  // Combined Usage
431
418
  // =============================================================================
432
419
 
433
- /**
434
- * Get combined usage data: API limits + local token counts.
435
- */
436
- export async function getUsage(): Promise<UsageSummary> {
437
- const [limits, realtime] = await Promise.all([
438
- fetchAPILimits(),
439
- scanTokenUsage(),
440
- ]);
441
-
442
- return {
443
- limits: limits.error ? null : limits,
444
- realtime,
445
- timestamp: new Date().toISOString(),
446
- };
447
- }
448
-
449
420
  /**
450
421
  * Get usage formatted for HUD display.
451
422
  * Prioritizes API percentages, falls back to token counts.