@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmylchreest/aide-plugin",
3
- "version": "0.0.34",
3
+ "version": "0.0.37",
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",
@@ -6,7 +6,7 @@
6
6
  * back to the current assistant's config files.
7
7
  *
8
8
  * Supports:
9
- * - Claude Code: ~/.mcp.json (user), .mcp.json (project)
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 getAssistantPaths(
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 = (def.type as string) || "stdio";
181
- const isRemote = claudeType === "sse" || !!def.url;
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 = getAssistantPaths(platform, scope, cwd);
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 = getAssistantPaths(platform, scope, cwd);
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);
@@ -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({ cwd: state.cwd });
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);
@@ -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": { "AIDE_CODE_WATCH": "1", "AIDE_CODE_WATCH_DELAY": "30s" },
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
- const cwd = ctx.worktree || ctx.directory;
42
- return createHooks(cwd, ctx.worktree, ctx.client, pluginRoot);
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;
@@ -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
- /** Current project information */
20
- project: { name?: string; directory?: string };
21
- /** Current working directory */
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
- /** Git worktree root */
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;