@jmylchreest/aide-plugin 0.0.34 → 0.0.37
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/core/mcp-sync.ts +37 -8
- package/src/opencode/hooks.ts +29 -1
- package/src/opencode/index.ts +139 -4
- package/src/opencode/types.ts +48 -4
package/package.json
CHANGED
package/src/core/mcp-sync.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* back to the current assistant's config files.
|
|
7
7
|
*
|
|
8
8
|
* Supports:
|
|
9
|
-
* - Claude Code: ~/.
|
|
9
|
+
* - Claude Code: ~/.claude.json (user), .mcp.json (project) (reads legacy ~/.mcp.json)
|
|
10
10
|
* - OpenCode: ~/.config/opencode/opencode.json (user), ./opencode.json (project)
|
|
11
11
|
* - Aide canonical: ~/.aide/config/mcp.json (user), .aide/config/mcp.json (project)
|
|
12
12
|
*
|
|
@@ -43,6 +43,8 @@ export interface CanonicalMcpServer {
|
|
|
43
43
|
name: string;
|
|
44
44
|
/** "local" (stdio) or "remote" (http/sse) */
|
|
45
45
|
type: "local" | "remote";
|
|
46
|
+
/** Remote transport (http/sse) */
|
|
47
|
+
transport?: "http" | "sse";
|
|
46
48
|
/** Command to run (for local servers) */
|
|
47
49
|
command?: string;
|
|
48
50
|
/** Arguments (for local servers) */
|
|
@@ -108,14 +110,32 @@ function userJournalPath(): string {
|
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
/** Get all config file paths for a given assistant and scope. */
|
|
111
|
-
function
|
|
113
|
+
function getAssistantReadPaths(
|
|
112
114
|
platform: McpPlatform,
|
|
113
115
|
scope: McpScope,
|
|
114
116
|
cwd: string,
|
|
115
117
|
): string[] {
|
|
116
118
|
if (platform === "claude-code") {
|
|
117
119
|
return scope === "user"
|
|
118
|
-
? [join(homedir(), ".mcp.json")]
|
|
120
|
+
? [join(homedir(), ".claude.json"), join(homedir(), ".mcp.json")]
|
|
121
|
+
: [join(cwd, ".mcp.json")];
|
|
122
|
+
}
|
|
123
|
+
if (platform === "opencode") {
|
|
124
|
+
return scope === "user"
|
|
125
|
+
? [join(homedir(), ".config", "opencode", "opencode.json")]
|
|
126
|
+
: [join(cwd, "opencode.json")];
|
|
127
|
+
}
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getAssistantWritePaths(
|
|
132
|
+
platform: McpPlatform,
|
|
133
|
+
scope: McpScope,
|
|
134
|
+
cwd: string,
|
|
135
|
+
): string[] {
|
|
136
|
+
if (platform === "claude-code") {
|
|
137
|
+
return scope === "user"
|
|
138
|
+
? [join(homedir(), ".claude.json")]
|
|
119
139
|
: [join(cwd, ".mcp.json")];
|
|
120
140
|
}
|
|
121
141
|
if (platform === "opencode") {
|
|
@@ -177,12 +197,20 @@ function readClaudeConfig(path: string): Record<string, CanonicalMcpServer> {
|
|
|
177
197
|
for (const [name, def] of Object.entries(
|
|
178
198
|
(raw.mcpServers || {}) as Record<string, Record<string, unknown>>,
|
|
179
199
|
)) {
|
|
180
|
-
const claudeType =
|
|
181
|
-
|
|
200
|
+
const claudeType =
|
|
201
|
+
(def.type as string) ||
|
|
202
|
+
((def.url as string | undefined) ? "http" : "stdio");
|
|
203
|
+
const isRemote =
|
|
204
|
+
claudeType === "sse" || claudeType === "http" || !!def.url;
|
|
182
205
|
|
|
183
206
|
servers[name] = {
|
|
184
207
|
name,
|
|
185
208
|
type: isRemote ? "remote" : "local",
|
|
209
|
+
transport: isRemote
|
|
210
|
+
? claudeType === "sse"
|
|
211
|
+
? "sse"
|
|
212
|
+
: "http"
|
|
213
|
+
: undefined,
|
|
186
214
|
command: def.command as string | undefined,
|
|
187
215
|
args: def.args as string[] | undefined,
|
|
188
216
|
url: def.url as string | undefined,
|
|
@@ -236,6 +264,7 @@ function readOpenCodeConfig(path: string): Record<string, CanonicalMcpServer> {
|
|
|
236
264
|
servers[name] = {
|
|
237
265
|
name,
|
|
238
266
|
type: isRemote ? "remote" : "local",
|
|
267
|
+
transport: isRemote ? "http" : undefined,
|
|
239
268
|
command,
|
|
240
269
|
args: args?.length ? args : undefined,
|
|
241
270
|
url: def.url as string | undefined,
|
|
@@ -315,7 +344,7 @@ function writeClaudeConfig(
|
|
|
315
344
|
const entry: Record<string, unknown> = {};
|
|
316
345
|
|
|
317
346
|
if (server.type === "remote") {
|
|
318
|
-
entry.type = "sse";
|
|
347
|
+
entry.type = server.transport === "sse" ? "sse" : "http";
|
|
319
348
|
if (server.url) entry.url = server.url;
|
|
320
349
|
if (server.headers) entry.headers = server.headers;
|
|
321
350
|
} else {
|
|
@@ -536,7 +565,7 @@ function collectServers(
|
|
|
536
565
|
|
|
537
566
|
// Assistant configs
|
|
538
567
|
for (const platform of platforms) {
|
|
539
|
-
const paths =
|
|
568
|
+
const paths = getAssistantReadPaths(platform, scope, cwd);
|
|
540
569
|
for (const p of paths) {
|
|
541
570
|
const servers = readAssistantConfig(platform, p);
|
|
542
571
|
if (Object.keys(servers).length > 0) {
|
|
@@ -630,7 +659,7 @@ function syncScope(
|
|
|
630
659
|
}
|
|
631
660
|
|
|
632
661
|
// Step 7: Write to current assistant's config
|
|
633
|
-
const assistantPaths =
|
|
662
|
+
const assistantPaths = getAssistantWritePaths(platform, scope, cwd);
|
|
634
663
|
for (const p of assistantPaths) {
|
|
635
664
|
// Read existing assistant config to check if it needs updating
|
|
636
665
|
const existingAssistant = readAssistantConfig(platform, p);
|
package/src/opencode/hooks.ts
CHANGED
|
@@ -98,6 +98,8 @@ interface AideState {
|
|
|
98
98
|
worktree: string;
|
|
99
99
|
/** Root of the aide plugin package (for finding bundled skills) */
|
|
100
100
|
pluginRoot: string | null;
|
|
101
|
+
/** Skip initializing aide for non-project directories */
|
|
102
|
+
skipInit: boolean;
|
|
101
103
|
sessionState: SessionState | null;
|
|
102
104
|
memories: MemoryInjection | null;
|
|
103
105
|
welcomeContext: string | null;
|
|
@@ -122,6 +124,7 @@ export async function createHooks(
|
|
|
122
124
|
worktree: string,
|
|
123
125
|
client: OpenCodeClient,
|
|
124
126
|
pluginRoot?: string,
|
|
127
|
+
options?: { skipInit?: boolean },
|
|
125
128
|
): Promise<Hooks> {
|
|
126
129
|
const state: AideState = {
|
|
127
130
|
initialized: false,
|
|
@@ -129,6 +132,7 @@ export async function createHooks(
|
|
|
129
132
|
cwd,
|
|
130
133
|
worktree,
|
|
131
134
|
pluginRoot: pluginRoot || null,
|
|
135
|
+
skipInit: options?.skipInit ?? false,
|
|
132
136
|
sessionState: null,
|
|
133
137
|
memories: null,
|
|
134
138
|
welcomeContext: null,
|
|
@@ -143,6 +147,18 @@ export async function createHooks(
|
|
|
143
147
|
// Run one-time initialization (directories, binary, config)
|
|
144
148
|
initializeAide(state);
|
|
145
149
|
|
|
150
|
+
try {
|
|
151
|
+
await client.app.log({
|
|
152
|
+
body: {
|
|
153
|
+
service: "aide",
|
|
154
|
+
level: "info",
|
|
155
|
+
message: `aide hooks init: cwd=${state.cwd} worktree=${state.worktree} pluginRoot=${state.pluginRoot ?? "unknown"} binary=${state.binary ?? "not-found"} skipInit=${state.skipInit}`,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
debug(SOURCE, `Init log failed (non-fatal): ${err}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
146
162
|
return {
|
|
147
163
|
event: createEventHandler(state),
|
|
148
164
|
config: createConfigHandler(state),
|
|
@@ -250,6 +266,15 @@ function createCommandHandler(state: AideState): (
|
|
|
250
266
|
|
|
251
267
|
function initializeAide(state: AideState): void {
|
|
252
268
|
try {
|
|
269
|
+
if (state.skipInit) {
|
|
270
|
+
debug(
|
|
271
|
+
SOURCE,
|
|
272
|
+
`Initialization skipped (no git root detected): ${state.cwd}`,
|
|
273
|
+
);
|
|
274
|
+
state.initialized = true;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
253
278
|
ensureDirectories(state.cwd);
|
|
254
279
|
|
|
255
280
|
// Sync MCP server configs across assistants (FS only, fast)
|
|
@@ -263,7 +288,10 @@ function initializeAide(state: AideState): void {
|
|
|
263
288
|
cleanupStaleStateFiles(state.cwd);
|
|
264
289
|
resetHudState(state.cwd);
|
|
265
290
|
|
|
266
|
-
state.binary = findAideBinary({
|
|
291
|
+
state.binary = findAideBinary({
|
|
292
|
+
cwd: state.cwd,
|
|
293
|
+
pluginRoot: state.pluginRoot || undefined,
|
|
294
|
+
});
|
|
267
295
|
|
|
268
296
|
if (state.binary) {
|
|
269
297
|
const projectName = getProjectName(state.cwd);
|
package/src/opencode/index.ts
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* - As a local plugin: copy/symlink to .opencode/plugins/
|
|
9
9
|
* - As an npm package: add "@jmylchreest/aide-plugin" to opencode.json
|
|
10
10
|
*
|
|
11
|
+
* Environment variables:
|
|
12
|
+
* AIDE_FORCE_INIT=1 Force aide initialization even in non-git directories.
|
|
13
|
+
* By default, aide skips initialization when no .git/ or
|
|
14
|
+
* .aide/ directory is found. Set this in the MCP server's
|
|
15
|
+
* "environment" config to always create .aide/ in the
|
|
16
|
+
* working directory.
|
|
17
|
+
*
|
|
11
18
|
* @example opencode.json
|
|
12
19
|
* ```json
|
|
13
20
|
* {
|
|
@@ -17,7 +24,11 @@
|
|
|
17
24
|
* "aide": {
|
|
18
25
|
* "type": "local",
|
|
19
26
|
* "command": ["bunx", "-y", "@jmylchreest/aide-plugin", "mcp"],
|
|
20
|
-
* "environment": {
|
|
27
|
+
* "environment": {
|
|
28
|
+
* "AIDE_CODE_WATCH": "1",
|
|
29
|
+
* "AIDE_CODE_WATCH_DELAY": "30s",
|
|
30
|
+
* "AIDE_FORCE_INIT": "1"
|
|
31
|
+
* },
|
|
21
32
|
* "enabled": true
|
|
22
33
|
* }
|
|
23
34
|
* }
|
|
@@ -25,8 +36,9 @@
|
|
|
25
36
|
* ```
|
|
26
37
|
*/
|
|
27
38
|
|
|
28
|
-
import { dirname, join } from "path";
|
|
39
|
+
import { dirname, join, resolve } from "path";
|
|
29
40
|
import { fileURLToPath } from "url";
|
|
41
|
+
import { existsSync, statSync } from "fs";
|
|
30
42
|
import { createHooks } from "./hooks.js";
|
|
31
43
|
import type { Plugin, PluginInput, Hooks } from "./types.js";
|
|
32
44
|
|
|
@@ -37,9 +49,132 @@ const __dirname = dirname(__filename);
|
|
|
37
49
|
// index.ts lives in src/opencode/, so the package root is two levels up.
|
|
38
50
|
const pluginRoot = join(__dirname, "..", "..");
|
|
39
51
|
|
|
52
|
+
if (!process.env.AIDE_PLUGIN_ROOT) {
|
|
53
|
+
process.env.AIDE_PLUGIN_ROOT = pluginRoot;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the project root directory from the OpenCode plugin context.
|
|
58
|
+
*
|
|
59
|
+
* OpenCode provides three directory-related values:
|
|
60
|
+
* ctx.worktree — `git rev-parse --show-toplevel` (sandbox root, "/" for non-git)
|
|
61
|
+
* ctx.directory — `process.cwd()` or `--dir` (where OpenCode was invoked)
|
|
62
|
+
* ctx.project.worktree — `dirname(git rev-parse --git-common-dir)` (main repo root)
|
|
63
|
+
*
|
|
64
|
+
* Priority:
|
|
65
|
+
* 1. ctx.worktree — the git sandbox root (correct for both normal repos and worktrees)
|
|
66
|
+
* 2. ctx.directory — where OpenCode was invoked (fallback for non-git)
|
|
67
|
+
*
|
|
68
|
+
* Both ctx.worktree and ctx.directory are "/" for non-git projects, so we
|
|
69
|
+
* detect that case and skip initialization.
|
|
70
|
+
*/
|
|
71
|
+
function resolveProjectRoot(ctx: PluginInput): {
|
|
72
|
+
root: string;
|
|
73
|
+
hasProjectRoot: boolean;
|
|
74
|
+
} {
|
|
75
|
+
// ctx.worktree is the git working tree root (from `git rev-parse --show-toplevel`).
|
|
76
|
+
// For non-git projects, OpenCode sets this to "/".
|
|
77
|
+
const worktree = ctx.worktree;
|
|
78
|
+
const directory = ctx.directory;
|
|
79
|
+
|
|
80
|
+
// OpenCode sets worktree to "/" for non-git projects — treat as no project.
|
|
81
|
+
// Also guard against empty strings.
|
|
82
|
+
const isNonGitWorktree = !worktree || worktree === "/";
|
|
83
|
+
|
|
84
|
+
if (!isNonGitWorktree) {
|
|
85
|
+
// The worktree is a valid git root — use it directly.
|
|
86
|
+
// No need to walk up the filesystem; OpenCode already resolved it.
|
|
87
|
+
return { root: resolve(worktree), hasProjectRoot: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Worktree is "/" (non-git) — try to find a project root from the directory.
|
|
91
|
+
// This handles the case where the user is in a git repo but OpenCode's
|
|
92
|
+
// resolution somehow failed, or where .aide/ already exists.
|
|
93
|
+
if (directory && directory !== "/") {
|
|
94
|
+
const resolved = walkUpForProjectRoot(directory);
|
|
95
|
+
if (resolved) {
|
|
96
|
+
return { root: resolved, hasProjectRoot: true };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// No git root found anywhere.
|
|
101
|
+
// If AIDE_FORCE_INIT is set, treat the directory as the project root anyway.
|
|
102
|
+
const forceInit = !!process.env.AIDE_FORCE_INIT;
|
|
103
|
+
if (forceInit && directory && directory !== "/") {
|
|
104
|
+
return { root: resolve(directory), hasProjectRoot: true };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { root: directory || "/", hasProjectRoot: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Walk up from `startDir` looking for .aide/ or .git/ directories.
|
|
112
|
+
* Returns the project root path, or null if none found.
|
|
113
|
+
*/
|
|
114
|
+
function walkUpForProjectRoot(startDir: string): string | null {
|
|
115
|
+
let dir = resolve(startDir);
|
|
116
|
+
for (;;) {
|
|
117
|
+
if (existsSync(join(dir, ".aide"))) {
|
|
118
|
+
return dir;
|
|
119
|
+
}
|
|
120
|
+
const gitPath = join(dir, ".git");
|
|
121
|
+
if (existsSync(gitPath)) {
|
|
122
|
+
try {
|
|
123
|
+
const stat = statSync(gitPath);
|
|
124
|
+
// .git can be a directory (normal repo) or a file (worktree pointer)
|
|
125
|
+
if (stat.isDirectory() || stat.isFile()) {
|
|
126
|
+
return dir;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
return dir;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const parent = resolve(dir, "..");
|
|
133
|
+
if (parent === dir) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
dir = parent;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
40
140
|
export const AidePlugin: Plugin = async (ctx: PluginInput): Promise<Hooks> => {
|
|
41
|
-
|
|
42
|
-
|
|
141
|
+
// Log raw plugin input BEFORE any resolution for diagnostics.
|
|
142
|
+
// This is the key to understanding what OpenCode actually passes.
|
|
143
|
+
const rawLog = [
|
|
144
|
+
`aide plugin init (raw ctx):`,
|
|
145
|
+
` ctx.directory = ${JSON.stringify(ctx.directory)}`,
|
|
146
|
+
` ctx.worktree = ${JSON.stringify(ctx.worktree)}`,
|
|
147
|
+
` ctx.project = ${JSON.stringify(ctx.project, null, 2)?.split("\n").join("\n ")}`,
|
|
148
|
+
].join("\n");
|
|
149
|
+
|
|
150
|
+
// Best-effort log to OpenCode's log system
|
|
151
|
+
try {
|
|
152
|
+
await ctx.client.app.log({
|
|
153
|
+
body: { service: "aide", level: "info", message: rawLog },
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
// Plugin may be called before the client is fully ready
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Also log to stderr for direct observability
|
|
160
|
+
console.error(rawLog);
|
|
161
|
+
|
|
162
|
+
const resolved = resolveProjectRoot(ctx);
|
|
163
|
+
|
|
164
|
+
const forceInit = !!process.env.AIDE_FORCE_INIT;
|
|
165
|
+
const resolvedLog = `aide plugin resolved: root=${resolved.root} hasProjectRoot=${resolved.hasProjectRoot}${forceInit ? " (AIDE_FORCE_INIT=1)" : ""}`;
|
|
166
|
+
try {
|
|
167
|
+
await ctx.client.app.log({
|
|
168
|
+
body: { service: "aide", level: "info", message: resolvedLog },
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
// non-fatal
|
|
172
|
+
}
|
|
173
|
+
console.error(resolvedLog);
|
|
174
|
+
|
|
175
|
+
return createHooks(resolved.root, ctx.worktree, ctx.client, pluginRoot, {
|
|
176
|
+
skipInit: !resolved.hasProjectRoot,
|
|
177
|
+
});
|
|
43
178
|
};
|
|
44
179
|
|
|
45
180
|
export default AidePlugin;
|
package/src/opencode/types.ts
CHANGED
|
@@ -13,14 +13,58 @@
|
|
|
13
13
|
// Plugin Input (provided by OpenCode on plugin init)
|
|
14
14
|
// =============================================================================
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Project info as provided by OpenCode.
|
|
18
|
+
*
|
|
19
|
+
* Mirrors the Project.Info shape from sst/opencode:
|
|
20
|
+
* packages/opencode/src/project/project.ts
|
|
21
|
+
*
|
|
22
|
+
* Key fields:
|
|
23
|
+
* - id: root commit hash (or "global" for non-git)
|
|
24
|
+
* - worktree: dirname(git rev-parse --git-common-dir) — main repo root
|
|
25
|
+
* - vcs: "git" | undefined
|
|
26
|
+
* - sandboxes: list of active sandboxes / worktrees
|
|
27
|
+
*/
|
|
28
|
+
export interface OpenCodeProject {
|
|
29
|
+
id: string;
|
|
30
|
+
/** Main repository root (git common dir parent). NOT the sandbox/worktree. */
|
|
31
|
+
worktree: string;
|
|
32
|
+
/** Version control system type */
|
|
33
|
+
vcs?: string;
|
|
34
|
+
/** Optional display name */
|
|
35
|
+
name?: string;
|
|
36
|
+
/** Optional icon */
|
|
37
|
+
icon?: string;
|
|
38
|
+
/** Active sandboxes (worktree paths) */
|
|
39
|
+
sandboxes?: string[];
|
|
40
|
+
/** Timestamps */
|
|
41
|
+
time?: { created: number; updated: number };
|
|
42
|
+
/** Allow additional fields from future OpenCode versions */
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
16
46
|
export interface PluginInput {
|
|
17
47
|
/** OpenCode SDK client for API interactions */
|
|
18
48
|
client: OpenCodeClient;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Current project information.
|
|
51
|
+
*
|
|
52
|
+
* NOTE: This is a full Project.Info from OpenCode, NOT a minimal
|
|
53
|
+
* { name, directory } object. The key field for project root is
|
|
54
|
+
* `project.worktree` (the git common dir parent), not
|
|
55
|
+
* `project.directory` (which does not exist in OpenCode's type).
|
|
56
|
+
*/
|
|
57
|
+
project: OpenCodeProject;
|
|
58
|
+
/**
|
|
59
|
+
* The directory OpenCode was invoked from.
|
|
60
|
+
* Typically process.cwd() or the --dir argument.
|
|
61
|
+
*/
|
|
22
62
|
directory: string;
|
|
23
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Git sandbox / working tree root.
|
|
65
|
+
* Result of `git rev-parse --show-toplevel` from the cwd.
|
|
66
|
+
* For non-git projects this is "/".
|
|
67
|
+
*/
|
|
24
68
|
worktree: string;
|
|
25
69
|
/** URL of the running OpenCode server */
|
|
26
70
|
serverUrl: URL;
|