@jmylchreest/aide-plugin 0.0.39 → 0.0.42

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/src/core/types.ts CHANGED
@@ -15,6 +15,27 @@ export interface AideConfig {
15
15
  /** Auto-export on session end (default: false) */
16
16
  autoExport?: boolean;
17
17
  };
18
+ findings?: {
19
+ /** Complexity analyser settings */
20
+ complexity?: {
21
+ /** Cyclomatic complexity threshold (default: 10) */
22
+ threshold?: number;
23
+ };
24
+ /** Import coupling analyser settings */
25
+ coupling?: {
26
+ /** Fan-out threshold — max outgoing imports (default: 15) */
27
+ fanOut?: number;
28
+ /** Fan-in threshold — max incoming imports (default: 20) */
29
+ fanIn?: number;
30
+ };
31
+ /** Code clone detection settings */
32
+ clones?: {
33
+ /** Sliding window size in tokens (default: 50) */
34
+ windowSize?: number;
35
+ /** Minimum clone size in lines (default: 6) */
36
+ minLines?: number;
37
+ };
38
+ };
18
39
  }
19
40
 
20
41
  export const DEFAULT_CONFIG: AideConfig = {};
@@ -468,14 +468,6 @@ Then restart Claude Code to use the new version.`;
468
468
  };
469
469
  }
470
470
 
471
- /**
472
- * Synchronous version for simple existence check (no download, no version check)
473
- * Use ensureAideBinary() for full functionality
474
- */
475
- export function findAideBinarySync(cwd?: string): string | null {
476
- return findAideBinary(cwd);
477
- }
478
-
479
471
  // --- CLI Mode ---
480
472
  // Run as standalone script for postinstall or manual download
481
473
 
@@ -1,80 +1,70 @@
1
1
  /**
2
- * Shared utilities for Claude Code hooks
2
+ * Shared utilities for Claude Code hooks.
3
3
  *
4
- * This module provides common functions used across multiple hooks
5
- * to reduce code duplication and ensure consistent behavior.
4
+ * readStdin() is the only unique implementation here. All other functions
5
+ * are convenience wrappers around src/core/aide-client.ts that resolve the
6
+ * binary from AIDE_PLUGIN_ROOT / CLAUDE_PLUGIN_ROOT automatically.
6
7
  */
7
8
 
8
- import { execSync, execFileSync } from "child_process";
9
- import { existsSync, realpathSync } from "fs";
10
- import { join } from "path";
11
- import { debug } from "./logger.js";
9
+ import {
10
+ findAideBinary as clientFindBinary,
11
+ runAide as clientRunAide,
12
+ setState,
13
+ getState,
14
+ deleteState,
15
+ clearAgentState as clientClearAgentState,
16
+ sanitizeForLog,
17
+ shellEscape,
18
+ } from "../core/aide-client.js";
12
19
 
13
- const SOURCE = "hook-utils";
20
+ export { sanitizeForLog, shellEscape };
21
+
22
+ /** Maximum stdin payload size: 50 MiB. Prevents unbounded memory allocation. */
23
+ const MAX_STDIN_BYTES = 50 * 1024 * 1024;
14
24
 
15
25
  /**
16
- * Read JSON input from stdin (used by all hooks)
26
+ * Read JSON input from stdin (used by all hooks).
27
+ * Rejects payloads exceeding MAX_STDIN_BYTES.
17
28
  */
18
29
  export async function readStdin(): Promise<string> {
19
30
  const chunks: Buffer[] = [];
31
+ let totalBytes = 0;
20
32
  for await (const chunk of process.stdin) {
33
+ totalBytes += chunk.length;
34
+ if (totalBytes > MAX_STDIN_BYTES) {
35
+ throw new Error(
36
+ `stdin payload exceeds ${MAX_STDIN_BYTES} bytes, rejecting`,
37
+ );
38
+ }
21
39
  chunks.push(chunk);
22
40
  }
23
41
  return Buffer.concat(chunks).toString("utf-8");
24
42
  }
25
43
 
26
44
  /**
27
- * Find the aide binary canonical implementation.
28
- *
29
- * Search order:
30
- * 1. <CLAUDE_PLUGIN_ROOT>/bin/aide (CLAUDE_PLUGIN_ROOT = project root)
31
- * 2. PATH fallback (system-wide install)
45
+ * Get the plugin root directory from environment variables.
46
+ */
47
+ function getPluginRoot(): string | undefined {
48
+ return process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT;
49
+ }
50
+
51
+ /**
52
+ * Find the aide binary — Claude Code convenience wrapper.
32
53
  *
33
- * All hooks and utilities should use this single function.
54
+ * Reads AIDE_PLUGIN_ROOT / CLAUDE_PLUGIN_ROOT from the environment
55
+ * and delegates to the platform-agnostic aide-client implementation.
34
56
  */
35
57
  export function findAideBinary(cwd?: string): string | null {
36
- let pluginRoot =
37
- process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT;
38
- // Resolve symlinks (e.g., src-office -> dtkr4-cnjjf)
39
- if (pluginRoot) {
40
- try {
41
- pluginRoot = realpathSync(pluginRoot);
42
- } catch (err) {
43
- debug(SOURCE, `realpath failed for pluginRoot ${pluginRoot}: ${err}`);
44
- }
45
- }
46
- if (pluginRoot) {
47
- const pluginBinary = join(pluginRoot, "bin", "aide");
48
- if (existsSync(pluginBinary)) {
49
- return pluginBinary;
50
- }
51
- }
52
-
53
- // PATH fallback
54
- try {
55
- const result = execSync("which aide", { stdio: "pipe", timeout: 2000 })
56
- .toString()
57
- .trim();
58
- if (result) return result;
59
- } catch (err) {
60
- debug(SOURCE, `aide not found in PATH: ${err}`);
61
- }
62
-
63
- return null;
58
+ return clientFindBinary({ cwd, pluginRoot: getPluginRoot() });
64
59
  }
65
60
 
66
61
  /**
67
- * Escape a string for safe shell usage
62
+ * Run an aide command with the auto-discovered binary.
68
63
  */
69
- export function shellEscape(str: string): string {
70
- return str
71
- .replace(/\\/g, "\\\\")
72
- .replace(/"/g, '\\"')
73
- .replace(/\$/g, "\\$")
74
- .replace(/`/g, "\\`")
75
- .replace(/!/g, "\\!")
76
- .replace(/\n/g, " ")
77
- .slice(0, 1000);
64
+ export function runAide(cwd: string, args: string[]): string | null {
65
+ const binary = findAideBinary(cwd);
66
+ if (!binary) return null;
67
+ return clientRunAide(binary, cwd, args);
78
68
  }
79
69
 
80
70
  /**
@@ -88,16 +78,7 @@ export function setMemoryState(
88
78
  ): boolean {
89
79
  const binary = findAideBinary(cwd);
90
80
  if (!binary) return false;
91
-
92
- try {
93
- const args = ["state", "set", key, value];
94
- if (agentId) args.push(`--agent=${agentId}`);
95
- execFileSync(binary, args, { cwd, stdio: "pipe", timeout: 5000 });
96
- return true;
97
- } catch (err) {
98
- debug(SOURCE, `setMemoryState failed for key=${key}: ${err}`);
99
- return false;
100
- }
81
+ return setState(binary, cwd, key, value, agentId);
101
82
  }
102
83
 
103
84
  /**
@@ -110,22 +91,7 @@ export function getMemoryState(
110
91
  ): string | null {
111
92
  const binary = findAideBinary(cwd);
112
93
  if (!binary) return null;
113
-
114
- try {
115
- const args = ["state", "get", key];
116
- if (agentId) args.push(`--agent=${agentId}`);
117
- const output = execFileSync(binary, args, {
118
- cwd,
119
- encoding: "utf-8",
120
- timeout: 5000,
121
- });
122
- // Parse output format: "key = value" or "[agent] key = value"
123
- const match = output.match(/=\s*(.+)$/m);
124
- return match ? match[1].trim() : null;
125
- } catch (err) {
126
- debug(SOURCE, `getMemoryState failed for key=${key}: ${err}`);
127
- return null;
128
- }
94
+ return getState(binary, cwd, key, agentId);
129
95
  }
130
96
 
131
97
  /**
@@ -138,16 +104,7 @@ export function deleteMemoryState(
138
104
  ): boolean {
139
105
  const binary = findAideBinary(cwd);
140
106
  if (!binary) return false;
141
-
142
- try {
143
- // For agent-specific keys, we need to construct the full key
144
- const fullKey = agentId ? `agent:${agentId}:${key}` : key;
145
- execFileSync(binary, ["state", "delete", fullKey], { cwd, stdio: "pipe" });
146
- return true;
147
- } catch (err) {
148
- debug(SOURCE, `deleteMemoryState failed for key=${key}: ${err}`);
149
- return false;
150
- }
107
+ return deleteState(binary, cwd, key, agentId);
151
108
  }
152
109
 
153
110
  /**
@@ -156,30 +113,5 @@ export function deleteMemoryState(
156
113
  export function clearAgentState(cwd: string, agentId: string): boolean {
157
114
  const binary = findAideBinary(cwd);
158
115
  if (!binary) return false;
159
-
160
- try {
161
- execFileSync(binary, ["state", "clear", `--agent=${agentId}`], {
162
- cwd,
163
- stdio: "pipe",
164
- });
165
- return true;
166
- } catch (err) {
167
- debug(SOURCE, `clearAgentState failed for agent=${agentId}: ${err}`);
168
- return false;
169
- }
170
- }
171
-
172
- /**
173
- * Run an aide command with proper escaping
174
- */
175
- export function runAide(cwd: string, args: string[]): string | null {
176
- const binary = findAideBinary(cwd);
177
- if (!binary) return null;
178
-
179
- try {
180
- return execFileSync(binary, args, { cwd, encoding: "utf-8" });
181
- } catch (err) {
182
- debug(SOURCE, `runAide failed: ${args.join(" ")}: ${err}`);
183
- return null;
184
- }
116
+ return clientClearAgentState(binary, cwd, agentId);
185
117
  }
package/src/lib/hud.ts CHANGED
@@ -97,7 +97,7 @@ const ICONS = {
97
97
  };
98
98
 
99
99
  /**
100
- * Get all agent states from aide-memory
100
+ * Get all agent states from aide state store
101
101
  */
102
102
  export function getAgentStates(cwd: string): AgentState[] {
103
103
  const output = runAide(cwd, ["state", "list"]);
@@ -165,7 +165,7 @@ export function loadHudConfig(cwd: string): HudConfig {
165
165
  }
166
166
 
167
167
  /**
168
- * Get current session state from aide-memory
168
+ * Get current session state from aide state store
169
169
  */
170
170
  export function getSessionState(cwd: string): SessionState {
171
171
  const state: SessionState = {
@@ -31,7 +31,7 @@ import {
31
31
  copyFileSync,
32
32
  unlinkSync,
33
33
  } from "fs";
34
- import { join, basename } from "path";
34
+ import { join, basename, resolve } from "path";
35
35
  import { homedir } from "os";
36
36
 
37
37
  export interface SkillMetadata {
@@ -121,6 +121,30 @@ export function saveRegistry(cwd: string, registry: SkillsRegistry): void {
121
121
  writeFileSync(join(cwd, REGISTRY_FILE), JSON.stringify(registry, null, 2));
122
122
  }
123
123
 
124
+ /**
125
+ * Sanitize a skill name to prevent path traversal.
126
+ * Strips path separators and rejects names that would escape the target directory.
127
+ */
128
+ function sanitizeSkillName(name: string): string {
129
+ // Take only the basename to strip any directory components
130
+ const safe = basename(name).replace(/[^a-zA-Z0-9._-]/g, "_");
131
+ if (!safe || safe === "." || safe === "..") {
132
+ throw new Error(`Invalid skill name: ${name}`);
133
+ }
134
+ return safe;
135
+ }
136
+
137
+ /**
138
+ * Validate that a resolved path stays within the expected directory.
139
+ */
140
+ function assertWithinDir(filePath: string, dir: string): void {
141
+ const resolved = resolve(filePath);
142
+ const resolvedDir = resolve(dir);
143
+ if (!resolved.startsWith(resolvedDir + "/") && resolved !== resolvedDir) {
144
+ throw new Error(`Path traversal detected: ${filePath} escapes ${dir}`);
145
+ }
146
+ }
147
+
124
148
  /**
125
149
  * Install a skill from skills.sh or a URL
126
150
  *
@@ -201,8 +225,10 @@ export async function installSkill(
201
225
  version = meta.version || version;
202
226
  }
203
227
 
204
- // Write skill file
228
+ // Sanitize name and write skill file
229
+ name = sanitizeSkillName(name);
205
230
  const skillPath = join(targetDir, `${name}.md`);
231
+ assertWithinDir(skillPath, targetDir);
206
232
  writeFileSync(skillPath, content);
207
233
 
208
234
  // Update registry
@@ -242,8 +268,10 @@ export function uninstallSkill(cwd: string, name: string): boolean {
242
268
  return false;
243
269
  }
244
270
 
245
- // Remove file
246
- const skillPath = join(cwd, SKILLS_DIR, `${name}.md`);
271
+ // Sanitize name and remove file
272
+ const safeName = sanitizeSkillName(name);
273
+ const skillPath = join(cwd, SKILLS_DIR, `${safeName}.md`);
274
+ assertWithinDir(skillPath, join(cwd, SKILLS_DIR));
247
275
  if (existsSync(skillPath)) {
248
276
  try {
249
277
  unlinkSync(skillPath);
@@ -23,7 +23,7 @@
23
23
  * 3. Error handling and edge cases are better tested
24
24
  */
25
25
 
26
- import { execSync, execFileSync } from "child_process";
26
+ import { execFileSync } from "child_process";
27
27
  import {
28
28
  existsSync,
29
29
  mkdirSync,
@@ -34,6 +34,9 @@ import {
34
34
  statSync,
35
35
  } from "fs";
36
36
  import { join } from "path";
37
+ import { debug } from "./logger.js";
38
+
39
+ const SOURCE = "worktree";
37
40
 
38
41
  export type WorktreeStatus = "active" | "agent-complete" | "merged";
39
42
 
@@ -129,7 +132,7 @@ export function createWorktree(
129
132
  agentId: string,
130
133
  ): Worktree | null {
131
134
  if (!isGitRepo(cwd)) {
132
- console.error("Not a git repository");
135
+ debug(SOURCE, "Not a git repository");
133
136
  return null;
134
137
  }
135
138
 
@@ -184,7 +187,7 @@ export function createWorktree(
184
187
 
185
188
  return worktree;
186
189
  } catch (error) {
187
- console.error(`Failed to create worktree: ${error}`);
190
+ debug(SOURCE, `Failed to create worktree: ${error}`);
188
191
  return null;
189
192
  }
190
193
  }
@@ -197,7 +200,7 @@ export function removeWorktree(cwd: string, name: string): boolean {
197
200
  const worktree = state.active.find((w) => w.name === name);
198
201
 
199
202
  if (!worktree) {
200
- console.error(`Worktree not found: ${name}`);
203
+ debug(SOURCE, `Worktree not found: ${name}`);
201
204
  return false;
202
205
  }
203
206
 
@@ -220,7 +223,7 @@ export function removeWorktree(cwd: string, name: string): boolean {
220
223
 
221
224
  return true;
222
225
  } catch (error) {
223
- console.error(`Failed to remove worktree: ${error}`);
226
+ debug(SOURCE, `Failed to remove worktree: ${error}`);
224
227
  return false;
225
228
  }
226
229
  }
@@ -233,7 +236,7 @@ export function mergeWorktree(cwd: string, name: string): boolean {
233
236
  const worktree = state.active.find((w) => w.name === name);
234
237
 
235
238
  if (!worktree) {
236
- console.error(`Worktree not found: ${name}`);
239
+ debug(SOURCE, `Worktree not found: ${name}`);
237
240
  return false;
238
241
  }
239
242
 
@@ -250,7 +253,7 @@ export function mergeWorktree(cwd: string, name: string): boolean {
250
253
 
251
254
  return true;
252
255
  } catch (error) {
253
- console.error(`Failed to merge worktree: ${error}`);
256
+ debug(SOURCE, `Failed to merge worktree: ${error}`);
254
257
  return false;
255
258
  }
256
259
  }
@@ -315,7 +318,7 @@ export function registerWorktree(
315
318
  agentId: string,
316
319
  ): Worktree | null {
317
320
  if (!existsSync(worktreePath)) {
318
- console.error(`Worktree path does not exist: ${worktreePath}`);
321
+ debug(SOURCE, `Worktree path does not exist: ${worktreePath}`);
319
322
  return null;
320
323
  }
321
324
 
@@ -452,32 +455,3 @@ export function getWorktreesReadyForMerge(cwd: string): Worktree[] {
452
455
  const state = loadWorktreeState(cwd);
453
456
  return state.active.filter((w) => w.status === "agent-complete");
454
457
  }
455
-
456
- /**
457
- * Execute command in a worktree
458
- * WARNING: This function executes arbitrary shell commands. Only call with trusted input.
459
- * Used internally for running git commands and build tools in isolated worktrees.
460
- */
461
- export function execInWorktree(
462
- cwd: string,
463
- name: string,
464
- command: string,
465
- ): string | null {
466
- const state = loadWorktreeState(cwd);
467
- const worktree = state.active.find((w) => w.name === name);
468
-
469
- if (!worktree) {
470
- console.error(`Worktree not found: ${name}`);
471
- return null;
472
- }
473
-
474
- try {
475
- return execSync(command, {
476
- cwd: worktree.path,
477
- encoding: "utf-8",
478
- });
479
- } catch (error) {
480
- console.error(`Command failed in worktree: ${error}`);
481
- return null;
482
- }
483
- }
@@ -372,6 +372,18 @@ async function handleSessionCreated(
372
372
  if (state.initializedSessions.has(sessionId)) return;
373
373
  state.initializedSessions.add(sessionId);
374
374
 
375
+ // Defensive cap: if sessions leak without cleanup, evict oldest entries.
376
+ // Normal operation has only 1-2 concurrent sessions.
377
+ if (state.initializedSessions.size > 100) {
378
+ const entries = Array.from(state.initializedSessions);
379
+ const keepFrom = Math.floor(entries.length / 2);
380
+ state.initializedSessions.clear();
381
+ for (let i = keepFrom; i < entries.length; i++) {
382
+ state.initializedSessions.add(entries[i]);
383
+ }
384
+ debug(SOURCE, `Evicted ${keepFrom} stale entries from initializedSessions`);
385
+ }
386
+
375
387
  state.sessionState = initializeSession(sessionId, state.cwd);
376
388
 
377
389
  // Track this session for per-session context injection
@@ -380,6 +392,16 @@ async function handleSessionCreated(
380
392
  createdAt: new Date().toISOString(),
381
393
  });
382
394
 
395
+ // Defensive cap for sessionInfoMap (mirrors initializedSessions cap)
396
+ if (state.sessionInfoMap.size > 100) {
397
+ const entries = Array.from(state.sessionInfoMap.keys());
398
+ const keepFrom = Math.floor(entries.length / 2);
399
+ for (let i = 0; i < keepFrom; i++) {
400
+ state.sessionInfoMap.delete(entries[i]);
401
+ }
402
+ debug(SOURCE, `Evicted ${keepFrom} stale entries from sessionInfoMap`);
403
+ }
404
+
383
405
  // Register session as an "agent" in aide state for visibility
384
406
  if (state.binary) {
385
407
  try {
@@ -427,7 +449,11 @@ async function handleSessionIdle(
427
449
  // Check persistence: if ralph/autopilot mode is active, re-prompt the session
428
450
  if (state.binary) {
429
451
  try {
430
- const persistResult = checkPersistence(state.binary, state.cwd);
452
+ const persistResult = checkPersistence(
453
+ state.binary,
454
+ state.cwd,
455
+ sessionId,
456
+ );
431
457
  if (persistResult) {
432
458
  const activeMode = getActiveMode(state.binary, state.cwd);
433
459
  debug(