@jmylchreest/aide-plugin 0.0.54 → 0.0.55
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/bin/aide-wrapper.ts +353 -0
- package/package.json +6 -3
- package/src/cli/mcp.ts +17 -22
- package/src/core/aide-client.ts +26 -16
- package/src/core/context-guard.ts +101 -1
- package/src/core/read-tracking.ts +151 -0
- package/src/core/session-init.ts +2 -2
- package/src/core/session-summary-logic.ts +73 -58
- package/src/core/skill-matcher.ts +5 -12
- package/src/core/tool-tracking.ts +3 -2
- package/src/lib/hud.ts +85 -86
- package/src/lib/logger.ts +38 -9
- package/src/opencode/hooks.ts +43 -13
- package/src/opencode/index.ts +3 -2
- package/bin/aide-wrapper.sh +0 -159
- package/src/lib/skills-registry.ts +0 -362
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* aide-wrapper.ts - Ensures aide binary exists before executing
|
|
4
|
+
*
|
|
5
|
+
* Cross-platform TypeScript replacement for aide-wrapper.sh.
|
|
6
|
+
* Called by an assistant's MCP server configuration.
|
|
7
|
+
* Finds the aide binary, downloads it if missing, then delegates to it.
|
|
8
|
+
*
|
|
9
|
+
* Plugin root resolution order:
|
|
10
|
+
* 1. AIDE_PLUGIN_ROOT (canonical, platform-agnostic)
|
|
11
|
+
* 2. CLAUDE_PLUGIN_ROOT (set by Claude Code)
|
|
12
|
+
* 3. SCRIPT_DIR/.. (fallback: infer from wrapper location)
|
|
13
|
+
*
|
|
14
|
+
* Lives at: <plugin-root>/bin/aide-wrapper.ts
|
|
15
|
+
* Binary at: <plugin-root>/bin/aide[.exe]
|
|
16
|
+
*
|
|
17
|
+
* Logs written to: .aide/_logs/wrapper.log
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
21
|
+
import {
|
|
22
|
+
existsSync,
|
|
23
|
+
mkdirSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
realpathSync,
|
|
26
|
+
appendFileSync,
|
|
27
|
+
unlinkSync,
|
|
28
|
+
writeFileSync,
|
|
29
|
+
rmdirSync,
|
|
30
|
+
} from "fs";
|
|
31
|
+
import { dirname, join, resolve } from "path";
|
|
32
|
+
import { fileURLToPath } from "url";
|
|
33
|
+
|
|
34
|
+
const isWindows = process.platform === "win32";
|
|
35
|
+
const EXT = isWindows ? ".exe" : "";
|
|
36
|
+
|
|
37
|
+
// Resolve symlinks so that invoking via node_modules/.bin/aide-wrapper
|
|
38
|
+
// (which is a symlink to the real package) gives us the real package dir.
|
|
39
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
40
|
+
let scriptDir: string;
|
|
41
|
+
try {
|
|
42
|
+
scriptDir = dirname(realpathSync(__filename));
|
|
43
|
+
} catch {
|
|
44
|
+
scriptDir = dirname(__filename);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PLUGIN_ROOT =
|
|
48
|
+
process.env.AIDE_PLUGIN_ROOT ||
|
|
49
|
+
process.env.CLAUDE_PLUGIN_ROOT ||
|
|
50
|
+
resolve(scriptDir, "..");
|
|
51
|
+
|
|
52
|
+
const BINARY = join(PLUGIN_ROOT, "bin", `aide${EXT}`);
|
|
53
|
+
const BIN_DIR = join(PLUGIN_ROOT, "bin");
|
|
54
|
+
|
|
55
|
+
// Setup logging
|
|
56
|
+
const LOG_DIR = join(PLUGIN_ROOT, ".aide", "_logs");
|
|
57
|
+
const LOG_FILE = join(LOG_DIR, "wrapper.log");
|
|
58
|
+
try {
|
|
59
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
60
|
+
} catch {
|
|
61
|
+
// ignore
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function log(msg: string): void {
|
|
65
|
+
const timestamp = new Date().toISOString().replace("T", " ").replace(/\..+/, "");
|
|
66
|
+
const line = `[${timestamp}] [aide-wrapper] ${msg}`;
|
|
67
|
+
process.stderr.write(line + "\n");
|
|
68
|
+
try {
|
|
69
|
+
appendFileSync(LOG_FILE, line + "\n");
|
|
70
|
+
} catch {
|
|
71
|
+
// ignore log write failures
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Compare semantic versions: returns true if a >= b
|
|
77
|
+
*/
|
|
78
|
+
function versionGte(a: string, b: string): boolean {
|
|
79
|
+
const pa = a.split(".").map(Number);
|
|
80
|
+
const pb = b.split(".").map(Number);
|
|
81
|
+
for (let i = 0; i < 3; i++) {
|
|
82
|
+
const va = pa[i] || 0;
|
|
83
|
+
const vb = pb[i] || 0;
|
|
84
|
+
if (va > vb) return true;
|
|
85
|
+
if (va < vb) return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the version string from the aide binary
|
|
92
|
+
*/
|
|
93
|
+
function getBinaryVersion(binary: string): string | null {
|
|
94
|
+
try {
|
|
95
|
+
const output = execFileSync(binary, ["version"], {
|
|
96
|
+
stdio: "pipe",
|
|
97
|
+
timeout: 5000,
|
|
98
|
+
})
|
|
99
|
+
.toString()
|
|
100
|
+
.trim();
|
|
101
|
+
const match = output.match(
|
|
102
|
+
/(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.+]+)?)/,
|
|
103
|
+
);
|
|
104
|
+
return match ? match[1] : null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read the plugin version from plugin.json or package.json
|
|
112
|
+
*/
|
|
113
|
+
function getPluginVersion(): string | null {
|
|
114
|
+
for (const relPath of [
|
|
115
|
+
".claude-plugin/plugin.json",
|
|
116
|
+
"package.json",
|
|
117
|
+
]) {
|
|
118
|
+
try {
|
|
119
|
+
const content = readFileSync(join(PLUGIN_ROOT, relPath), "utf-8");
|
|
120
|
+
const match = content.match(/"version"\s*:\s*"(\d+\.\d+\.\d+)/);
|
|
121
|
+
if (match) return match[1];
|
|
122
|
+
} catch {
|
|
123
|
+
// skip
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if the binary exists and is executable
|
|
131
|
+
*/
|
|
132
|
+
function binaryExists(): boolean {
|
|
133
|
+
if (!existsSync(BINARY)) return false;
|
|
134
|
+
// On Windows, existence is sufficient (no execute bit)
|
|
135
|
+
if (isWindows) return true;
|
|
136
|
+
try {
|
|
137
|
+
execFileSync(BINARY, ["version"], { stdio: "pipe", timeout: 5000 });
|
|
138
|
+
return true;
|
|
139
|
+
} catch {
|
|
140
|
+
// Binary exists but can't execute — might need re-download
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Simple cross-platform file lock.
|
|
147
|
+
* Uses mkdir as an atomic operation (works on all platforms).
|
|
148
|
+
* Returns a cleanup function to release the lock.
|
|
149
|
+
*/
|
|
150
|
+
function acquireLock(lockPath: string, timeoutMs: number = 60000): () => void {
|
|
151
|
+
const start = Date.now();
|
|
152
|
+
const pollMs = 200;
|
|
153
|
+
|
|
154
|
+
while (true) {
|
|
155
|
+
try {
|
|
156
|
+
mkdirSync(lockPath);
|
|
157
|
+
// Write our PID for debugging
|
|
158
|
+
try {
|
|
159
|
+
writeFileSync(join(lockPath, "pid"), String(process.pid));
|
|
160
|
+
} catch {
|
|
161
|
+
// ignore
|
|
162
|
+
}
|
|
163
|
+
return () => {
|
|
164
|
+
try {
|
|
165
|
+
unlinkSync(join(lockPath, "pid"));
|
|
166
|
+
} catch { /* ignore */ }
|
|
167
|
+
try {
|
|
168
|
+
// rmdir only works on empty dirs — that's what we want
|
|
169
|
+
rmdirSync(lockPath);
|
|
170
|
+
} catch { /* ignore */ }
|
|
171
|
+
};
|
|
172
|
+
} catch (err: unknown) {
|
|
173
|
+
if (
|
|
174
|
+
err &&
|
|
175
|
+
typeof err === "object" &&
|
|
176
|
+
"code" in err &&
|
|
177
|
+
(err as { code: string }).code === "EEXIST"
|
|
178
|
+
) {
|
|
179
|
+
// Lock held by another process
|
|
180
|
+
if (Date.now() - start > timeoutMs) {
|
|
181
|
+
log("ERROR: Timed out waiting for download lock");
|
|
182
|
+
// Force-remove stale lock and try once more
|
|
183
|
+
try {
|
|
184
|
+
unlinkSync(join(lockPath, "pid"));
|
|
185
|
+
} catch { /* ignore */ }
|
|
186
|
+
try {
|
|
187
|
+
rmdirSync(lockPath);
|
|
188
|
+
} catch { /* ignore */ }
|
|
189
|
+
throw new Error("Timed out waiting for download lock");
|
|
190
|
+
}
|
|
191
|
+
// Poll — use Bun.sleepSync for cross-platform millisecond sleep
|
|
192
|
+
Bun.sleepSync(pollMs);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Download the aide binary using the TypeScript downloader
|
|
202
|
+
*/
|
|
203
|
+
function downloadBinary(): void {
|
|
204
|
+
const LOCKDIR = join(BIN_DIR, ".aide-download.lock");
|
|
205
|
+
|
|
206
|
+
// Resolve the downloader script — prefer src/ (dev) but fall back to dist/ (npm install)
|
|
207
|
+
let downloader: string;
|
|
208
|
+
if (existsSync(join(PLUGIN_ROOT, "src", "lib", "aide-downloader.ts"))) {
|
|
209
|
+
downloader = join(PLUGIN_ROOT, "src", "lib", "aide-downloader.ts");
|
|
210
|
+
} else if (existsSync(join(PLUGIN_ROOT, "dist", "lib", "aide-downloader.js"))) {
|
|
211
|
+
downloader = join(PLUGIN_ROOT, "dist", "lib", "aide-downloader.js");
|
|
212
|
+
} else {
|
|
213
|
+
log(`ERROR: Cannot find aide-downloader in src/ or dist/ under ${PLUGIN_ROOT}`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Remove stale/outdated binary before locking
|
|
218
|
+
if (existsSync(BINARY)) {
|
|
219
|
+
log("Removing outdated binary before download");
|
|
220
|
+
try {
|
|
221
|
+
unlinkSync(BINARY);
|
|
222
|
+
} catch {
|
|
223
|
+
// ignore
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const releaseLock = acquireLock(LOCKDIR);
|
|
228
|
+
try {
|
|
229
|
+
// Re-check after acquiring lock — another process may have finished the download
|
|
230
|
+
if (existsSync(BINARY)) {
|
|
231
|
+
log("Binary appeared while waiting for lock (downloaded by another process)");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
log("Downloading binary...");
|
|
236
|
+
log(`Using downloader: ${downloader}`);
|
|
237
|
+
|
|
238
|
+
// Determine runner based on file extension
|
|
239
|
+
let runner: string;
|
|
240
|
+
let runnerArgs: string[];
|
|
241
|
+
|
|
242
|
+
if (downloader.endsWith(".ts")) {
|
|
243
|
+
runner = "bun";
|
|
244
|
+
runnerArgs = [downloader, "--dest", BIN_DIR];
|
|
245
|
+
} else {
|
|
246
|
+
runner = "node";
|
|
247
|
+
runnerArgs = [downloader, "--dest", BIN_DIR];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Use cross-spawn for cross-platform compatibility (lazy import to
|
|
251
|
+
// survive missing node_modules during bootstrap).
|
|
252
|
+
const crossSpawn = require("cross-spawn");
|
|
253
|
+
const result = crossSpawn.sync(runner, runnerArgs, {
|
|
254
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
255
|
+
timeout: 120000,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (result.status !== 0) {
|
|
259
|
+
log("ERROR: Downloader failed");
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!existsSync(BINARY)) {
|
|
264
|
+
log("ERROR: Binary not found after download");
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
} finally {
|
|
268
|
+
releaseLock();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
log(`Binary ready at ${BINARY}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --- Ensure dependencies ---
|
|
275
|
+
|
|
276
|
+
// After a Claude Code marketplace autoUpdate (git pull), node_modules/
|
|
277
|
+
// may be missing since it's gitignored. Detect and self-heal before any
|
|
278
|
+
// imports that depend on npm packages (e.g. 'which', 'cross-spawn').
|
|
279
|
+
function ensureDependencies(): void {
|
|
280
|
+
const nodeModules = join(PLUGIN_ROOT, "node_modules");
|
|
281
|
+
if (!existsSync(nodeModules)) {
|
|
282
|
+
log("node_modules missing — running bun install to restore dependencies");
|
|
283
|
+
try {
|
|
284
|
+
const result = spawnSync("bun", ["install", "--frozen-lockfile"], {
|
|
285
|
+
cwd: PLUGIN_ROOT,
|
|
286
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
287
|
+
timeout: 60000,
|
|
288
|
+
});
|
|
289
|
+
if (result.status === 0) {
|
|
290
|
+
log("bun install completed successfully");
|
|
291
|
+
} else {
|
|
292
|
+
const stderr = result.stderr?.toString().trim();
|
|
293
|
+
log(`WARNING: bun install failed (status ${result.status}): ${stderr}`);
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
log(`WARNING: bun install error: ${err}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
ensureDependencies();
|
|
302
|
+
|
|
303
|
+
// --- Main ---
|
|
304
|
+
|
|
305
|
+
log(
|
|
306
|
+
`Starting wrapper (pid=${process.pid}, args=${process.argv.slice(2).join(" ")})`,
|
|
307
|
+
);
|
|
308
|
+
log(`PLUGIN_ROOT=${PLUGIN_ROOT}`);
|
|
309
|
+
log(`BINARY=${BINARY}`);
|
|
310
|
+
|
|
311
|
+
let needsDownload = false;
|
|
312
|
+
|
|
313
|
+
if (!binaryExists()) {
|
|
314
|
+
needsDownload = true;
|
|
315
|
+
log("Binary not found or not executable");
|
|
316
|
+
} else {
|
|
317
|
+
const binaryVersion = getBinaryVersion(BINARY);
|
|
318
|
+
log(`Binary version: ${binaryVersion ?? "unknown"}`);
|
|
319
|
+
|
|
320
|
+
if (binaryVersion && binaryVersion.includes("-dev.")) {
|
|
321
|
+
// Dev build — check base version against plugin version
|
|
322
|
+
const baseVersion = binaryVersion.split("-")[0];
|
|
323
|
+
const pluginVersion = getPluginVersion();
|
|
324
|
+
|
|
325
|
+
if (pluginVersion && versionGte(baseVersion, pluginVersion)) {
|
|
326
|
+
log(
|
|
327
|
+
`Dev build v${binaryVersion} (base ${baseVersion} >= plugin v${pluginVersion}), using local build`,
|
|
328
|
+
);
|
|
329
|
+
} else {
|
|
330
|
+
needsDownload = true;
|
|
331
|
+
log(
|
|
332
|
+
`Dev build v${binaryVersion} is older than plugin v${pluginVersion ?? "unknown"}, re-downloading`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
log(`Release binary v${binaryVersion ?? "unknown"}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (needsDownload) {
|
|
341
|
+
downloadBinary();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Execute the aide binary, replacing this process
|
|
345
|
+
log(`Executing: ${BINARY} ${process.argv.slice(2).join(" ")}`);
|
|
346
|
+
|
|
347
|
+
const result = spawnSync(BINARY, process.argv.slice(2), {
|
|
348
|
+
stdio: "inherit",
|
|
349
|
+
env: process.env,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Forward the exit code
|
|
353
|
+
process.exit(result.status ?? 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmylchreest/aide-plugin",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.55",
|
|
4
4
|
"description": "aide plugin for OpenCode — multi-agent orchestration, memory, skills, and persistence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/opencode/index.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"bin": {
|
|
22
22
|
"aide-plugin": "./src/cli/index.ts",
|
|
23
|
-
"aide-wrapper": "./bin/aide-wrapper.
|
|
23
|
+
"aide-wrapper": "./bin/aide-wrapper.ts"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"prebuild": "bun run copy-src",
|
|
@@ -48,5 +48,8 @@
|
|
|
48
48
|
"engines": {
|
|
49
49
|
"bun": ">=1.0.0"
|
|
50
50
|
},
|
|
51
|
-
"dependencies": {
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"cross-spawn": "^7.0.6",
|
|
53
|
+
"which": "^6.0.1"
|
|
54
|
+
}
|
|
52
55
|
}
|
package/src/cli/mcp.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP subcommand — delegates to aide-wrapper.
|
|
2
|
+
* MCP subcommand — delegates to aide-wrapper.ts to start the MCP server.
|
|
3
3
|
*
|
|
4
4
|
* This is the entry point used by OpenCode's MCP config:
|
|
5
5
|
* "command": ["bunx", "-y", "@jmylchreest/aide-plugin", "mcp"]
|
|
@@ -7,27 +7,27 @@
|
|
|
7
7
|
* The wrapper handles binary discovery/download, then exec's `aide mcp`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import spawn from "cross-spawn";
|
|
11
11
|
import { existsSync } from "fs";
|
|
12
12
|
import { dirname, join, resolve } from "path";
|
|
13
13
|
import { fileURLToPath } from "url";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Find the aide-wrapper.
|
|
16
|
+
* Find the aide-wrapper.ts script relative to this CLI module.
|
|
17
17
|
*
|
|
18
18
|
* Resolution: this file lives at <plugin-root>/src/cli/mcp.ts,
|
|
19
|
-
* so the wrapper is at <plugin-root>/bin/aide-wrapper.
|
|
19
|
+
* so the wrapper is at <plugin-root>/bin/aide-wrapper.ts.
|
|
20
20
|
*/
|
|
21
21
|
function findWrapper(): string {
|
|
22
22
|
// __dirname equivalent for ESM
|
|
23
23
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
24
24
|
// src/cli -> src -> plugin-root
|
|
25
25
|
const pluginRoot = resolve(thisDir, "..", "..");
|
|
26
|
-
const wrapper = join(pluginRoot, "bin", "aide-wrapper.
|
|
26
|
+
const wrapper = join(pluginRoot, "bin", "aide-wrapper.ts");
|
|
27
27
|
|
|
28
28
|
if (!existsSync(wrapper)) {
|
|
29
29
|
throw new Error(
|
|
30
|
-
`aide-wrapper.
|
|
30
|
+
`aide-wrapper.ts not found at ${wrapper}\n` +
|
|
31
31
|
`Expected plugin root: ${pluginRoot}\n` +
|
|
32
32
|
`Ensure the package is installed correctly.`,
|
|
33
33
|
);
|
|
@@ -37,27 +37,22 @@ function findWrapper(): string {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Start the MCP server by exec'ing aide-wrapper.
|
|
40
|
+
* Start the MCP server by exec'ing aide-wrapper.ts with "mcp" + any extra args.
|
|
41
41
|
* This replaces the current process so stdio is inherited directly.
|
|
42
42
|
*/
|
|
43
43
|
export async function mcp(extraArgs: string[]): Promise<void> {
|
|
44
44
|
const wrapper = findWrapper();
|
|
45
|
-
const args = ["mcp", ...extraArgs];
|
|
45
|
+
const args = [wrapper, "mcp", ...extraArgs];
|
|
46
46
|
|
|
47
|
-
// Use
|
|
47
|
+
// Use bun (via cross-spawn for Windows .cmd resolution) to run the
|
|
48
|
+
// TypeScript wrapper. stdio is inherited so the MCP JSON-RPC protocol
|
|
48
49
|
// flows directly between OpenCode and the aide binary.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// execFileSync throws on non-zero exit. If the process was killed
|
|
57
|
-
// by a signal (e.g. OpenCode shutting down), exit cleanly.
|
|
58
|
-
if (err && typeof err === "object" && "status" in err) {
|
|
59
|
-
process.exit((err as { status: number }).status ?? 1);
|
|
60
|
-
}
|
|
61
|
-
throw err;
|
|
50
|
+
const result = spawn.sync("bun", args, {
|
|
51
|
+
stdio: "inherit",
|
|
52
|
+
env: process.env,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (result.status !== 0) {
|
|
56
|
+
process.exit(result.status ?? 1);
|
|
62
57
|
}
|
|
63
58
|
}
|
package/src/core/aide-client.ts
CHANGED
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
import { execFileSync } from "child_process";
|
|
12
12
|
import { existsSync, realpathSync } from "fs";
|
|
13
|
-
import { join } from "path";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
import which from "which";
|
|
14
15
|
import type { FindBinaryOptions } from "./types.js";
|
|
15
16
|
import { debug } from "../lib/logger.js";
|
|
16
17
|
|
|
17
18
|
const SOURCE = "aide-client";
|
|
19
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
20
|
+
const BINARY_NAME = IS_WINDOWS ? "aide.exe" : "aide";
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Find the aide binary — platform-agnostic implementation.
|
|
@@ -40,44 +43,51 @@ export function findAideBinary(opts: FindBinaryOptions = {}): string | null {
|
|
|
40
43
|
|
|
41
44
|
// 1. Plugin root bin/
|
|
42
45
|
if (pluginRoot) {
|
|
43
|
-
const pluginBinary = join(pluginRoot, "bin",
|
|
46
|
+
const pluginBinary = join(pluginRoot, "bin", BINARY_NAME);
|
|
44
47
|
if (existsSync(pluginBinary)) {
|
|
48
|
+
ensureBinInPath(dirname(pluginBinary));
|
|
45
49
|
return pluginBinary;
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
// 2. Project-local .aide/bin/
|
|
50
54
|
if (cwd) {
|
|
51
|
-
const projectBinary = join(cwd, ".aide", "bin",
|
|
55
|
+
const projectBinary = join(cwd, ".aide", "bin", BINARY_NAME);
|
|
52
56
|
if (existsSync(projectBinary)) {
|
|
57
|
+
ensureBinInPath(dirname(projectBinary));
|
|
53
58
|
return projectBinary;
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
// 3. Additional paths
|
|
58
63
|
for (const searchPath of additionalPaths) {
|
|
59
|
-
const binary = join(searchPath,
|
|
64
|
+
const binary = join(searchPath, BINARY_NAME);
|
|
60
65
|
if (existsSync(binary)) {
|
|
66
|
+
ensureBinInPath(dirname(binary));
|
|
61
67
|
return binary;
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
// 4. PATH fallback
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
stdio: "pipe",
|
|
69
|
-
timeout: 2000,
|
|
70
|
-
})
|
|
71
|
-
.toString()
|
|
72
|
-
.trim();
|
|
73
|
-
if (result) return result;
|
|
74
|
-
} catch (err) {
|
|
75
|
-
debug(SOURCE, `aide not found in PATH: ${err}`);
|
|
76
|
-
}
|
|
71
|
+
// 4. PATH fallback (already on PATH, no injection needed)
|
|
72
|
+
const fromPath = which.sync("aide", { nothrow: true });
|
|
73
|
+
if (fromPath) return fromPath;
|
|
77
74
|
|
|
75
|
+
debug(SOURCE, `aide binary not found (checked pluginRoot=${pluginRoot || "none"}, cwd=${cwd || "none"}, PATH)`);
|
|
78
76
|
return null;
|
|
79
77
|
}
|
|
80
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Ensure a directory is on PATH so child processes can find the aide binary.
|
|
81
|
+
* Only prepends if not already present.
|
|
82
|
+
*/
|
|
83
|
+
function ensureBinInPath(binDir: string): void {
|
|
84
|
+
const currentPath = process.env.PATH || "";
|
|
85
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
86
|
+
if (!currentPath.split(sep).includes(binDir)) {
|
|
87
|
+
process.env.PATH = `${binDir}${sep}${currentPath}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
/**
|
|
82
92
|
* Run an aide command and return stdout, or null on failure.
|
|
83
93
|
*/
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { statSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
18
|
-
import { resolve, isAbsolute, normalize } from "path";
|
|
18
|
+
import { resolve, isAbsolute, normalize, extname } from "path";
|
|
19
19
|
import { tmpdir } from "os";
|
|
20
20
|
import { join } from "path";
|
|
21
21
|
import { debug } from "../lib/logger.js";
|
|
22
|
+
import { getPreviousRead, checkFileReadFreshness } from "./read-tracking.js";
|
|
22
23
|
|
|
23
24
|
const SOURCE = "context-guard";
|
|
24
25
|
|
|
@@ -212,3 +213,102 @@ export function checkContextGuard(
|
|
|
212
213
|
debug(SOURCE, `Advisory for ${filePath}: ${estLines} lines, ${sizeKB}KB`);
|
|
213
214
|
return { shouldAdvise: true, advisory };
|
|
214
215
|
}
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// Smart Read Hint — suggest code index tools over redundant file re-reads
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
export interface SmartReadHintResult {
|
|
222
|
+
/** Whether to inject a hint message */
|
|
223
|
+
shouldHint: boolean;
|
|
224
|
+
/** Hint message to inject */
|
|
225
|
+
hint?: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check whether a Read call should receive a smart-read hint suggesting
|
|
230
|
+
* the agent use code_outline/code_symbols/code_references instead.
|
|
231
|
+
*
|
|
232
|
+
* Triggers when:
|
|
233
|
+
* 1. The file was already read this session (tracked via state store)
|
|
234
|
+
* 2. The file hasn't changed since last indexing (mtime comparison)
|
|
235
|
+
* 3. The file is indexed with symbols (code_outline would be useful)
|
|
236
|
+
*
|
|
237
|
+
* Gated behind AIDE_CODE_WATCH=1 and requires a valid aide binary.
|
|
238
|
+
*/
|
|
239
|
+
export function checkSmartReadHint(
|
|
240
|
+
toolName: string,
|
|
241
|
+
toolInput: Record<string, unknown>,
|
|
242
|
+
cwd: string,
|
|
243
|
+
binary: string | null,
|
|
244
|
+
): SmartReadHintResult {
|
|
245
|
+
// Only advise on Read tool calls (case-insensitive for OpenCode compat)
|
|
246
|
+
if (toolName.toLowerCase() !== "read") {
|
|
247
|
+
return { shouldHint: false };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Require code watcher to be enabled
|
|
251
|
+
if (process.env.AIDE_CODE_WATCH !== "1") {
|
|
252
|
+
return { shouldHint: false };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Require aide binary
|
|
256
|
+
if (!binary) {
|
|
257
|
+
return { shouldHint: false };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Extract file path from tool input (check multiple variants)
|
|
261
|
+
// Precedence matches checkContextGuard and checkWriteGuard
|
|
262
|
+
const filePath =
|
|
263
|
+
(toolInput.filePath as string) ||
|
|
264
|
+
(toolInput.file_path as string) ||
|
|
265
|
+
(toolInput.path as string);
|
|
266
|
+
|
|
267
|
+
if (!filePath) {
|
|
268
|
+
return { shouldHint: false };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Skip targeted reads (agent already using offset/limit)
|
|
272
|
+
const offset = toolInput.offset as number | undefined;
|
|
273
|
+
const limit = toolInput.limit as number | undefined;
|
|
274
|
+
if (offset !== undefined && offset > 1) {
|
|
275
|
+
return { shouldHint: false };
|
|
276
|
+
}
|
|
277
|
+
if (limit !== undefined && limit < 100) {
|
|
278
|
+
return { shouldHint: false };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Skip non-source-code files
|
|
282
|
+
const ext = extname(filePath).toLowerCase();
|
|
283
|
+
if (SKIP_EXTENSIONS.has(ext)) {
|
|
284
|
+
return { shouldHint: false };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check if this file was already read this session
|
|
288
|
+
const previousRead = getPreviousRead(binary, cwd, filePath);
|
|
289
|
+
if (!previousRead) {
|
|
290
|
+
// First read — no hint needed
|
|
291
|
+
return { shouldHint: false };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Check if the file is indexed and fresh
|
|
295
|
+
const readCheck = checkFileReadFreshness(binary, cwd, filePath);
|
|
296
|
+
if (!readCheck) {
|
|
297
|
+
return { shouldHint: false };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (readCheck.indexed && readCheck.fresh && readCheck.outline_available) {
|
|
301
|
+
const tokens = readCheck.estimated_tokens;
|
|
302
|
+
const tokenInfo = tokens > 0 ? ` (~${tokens} tokens)` : "";
|
|
303
|
+
const hint =
|
|
304
|
+
`[aide:smart-read] This file was already read this session and hasn't changed${tokenInfo}. ` +
|
|
305
|
+
`Consider using code_outline (for structure, ~5-15% of full tokens), ` +
|
|
306
|
+
`code_symbols (for API surface), or code_references (for call sites) ` +
|
|
307
|
+
`to avoid re-reading the full file.`;
|
|
308
|
+
|
|
309
|
+
debug(SOURCE, `Smart read hint for: ${filePath} (${tokens} tokens)`);
|
|
310
|
+
return { shouldHint: true, hint };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { shouldHint: false };
|
|
314
|
+
}
|