From bbb54fa9d935d70342d8fe18f96940b957aba74e Mon Sep 17 00:00:00 2001 From: "Vicary A." Date: Tue, 7 Apr 2026 22:54:12 +0800 Subject: [PATCH] fix(packaging): resolve runtime deps from plugin package --- dnt.ts | 3 + ...ative-runtime-resolution-implementation.md | 406 ++++++++++++++++++ ...kage-relative-runtime-resolution-design.md | 237 ++++++++++ packaging.test.ts | 145 ++++++- src/config.ts | 5 +- src/services/connection-manager.ts | 6 +- 6 files changed, 777 insertions(+), 25 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-07-package-relative-runtime-resolution-implementation.md create mode 100644 docs/superpowers/specs/2026-04-07-package-relative-runtime-resolution-design.md diff --git a/dnt.ts b/dnt.ts index 23d02cf..0495eba 100644 --- a/dnt.ts +++ b/dnt.ts @@ -2,6 +2,8 @@ import { build } from "jsr:@deno/dnt@^0.42.3"; import manifest from "./deno.json" with { type: "json" }; const version = Deno.env.get("VERSION")?.trim() || manifest.version?.trim(); +const sdkVersionFromDenoJson = manifest.imports["@modelcontextprotocol/sdk"] + .replace("npm:@modelcontextprotocol/sdk@", ""); if (!version) { throw new Error('Specify $VERSION or set "version" in deno.json.'); } @@ -44,6 +46,7 @@ await build({ node: ">=20", }, dependencies: { + "@modelcontextprotocol/sdk": sdkVersionFromDenoJson, cosmiconfig: "^9.0.0", }, devDependencies: { diff --git a/docs/superpowers/plans/2026-04-07-package-relative-runtime-resolution-implementation.md b/docs/superpowers/plans/2026-04-07-package-relative-runtime-resolution-implementation.md new file mode 100644 index 0000000..5e0ddcd --- /dev/null +++ b/docs/superpowers/plans/2026-04-07-package-relative-runtime-resolution-implementation.md @@ -0,0 +1,406 @@ +# Package-Relative Runtime Resolution Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps use +> checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the installed `opencode-graphiti` package resolve `cosmiconfig` +and `@modelcontextprotocol/sdk` from the plugin package instead of +`process.cwd()`, and prove it works when OpenCode launches from an unrelated +directory. + +**Architecture:** Introduce a package-relative `createRequire(...)` anchor +derived from `import.meta.url` in the two runtime loaders, keep MCP SDK loading +lazy, and update generated npm package metadata so the published package +declares all runtime dependencies it resolves at runtime. Validate the fix with +a Node package-name regression that runs from a bare temp cwd rather than the +repository tree. + +**Tech Stack:** Deno, TypeScript, DNT, Node ESM interop, `cosmiconfig`, +`@modelcontextprotocol/sdk`, OpenCode packaging regression tests. + +**Done when:** `deno test -A packaging.test.ts` passes with the Node +package-name regression running from a bare cwd, and `deno test -A`, +`deno task check`, `deno task lint`, and `deno task fmt` all pass. + +--- + +### File Map + +**Modify:** + +- `packaging.test.ts` Responsibility: package build regression coverage and + runtime dependency assertions +- `src/config.ts` Responsibility: runtime config discovery loading through + package-relative `require` +- `src/services/connection-manager.ts` Responsibility: lazy MCP SDK runtime + loading through package-relative resolution +- `dnt.ts` Responsibility: generated `dist/package.json` dependency metadata + +**Create:** + +- None required unless the implementation needs a very small shared runtime + helper for the package-relative `createRequire(...)` anchor + +**Spec Reference:** + +- `docs/superpowers/specs/2026-04-07-package-relative-runtime-resolution-design.md` + +### Task 1: Add the Failing Packaging Regression First + +**Files:** + +- Modify: `packaging.test.ts` + +- [ ] **Step 1: Add the failing Node package-name import regression** + +In `packaging.test.ts`, add a new Node runner that imports the package by name: + +```js +import * as plugin from "opencode-graphiti"; +console.log(JSON.stringify(Object.keys(plugin).sort())); +``` + +Use a temp `node_modules/opencode-graphiti -> dist` symlink and run Node from a +separate bare temp cwd that is not the repository root. + +The existing Bun runner already imports by package name. Keep it as secondary +coverage, but add a Node package-name runner because the current Node runner +only imports the built entrypoint by absolute `file://` URL and does not +exercise the cwd-sensitive bug. + +Expected initial failure mode before the fix: + +- package import fails because runtime dependency resolution still follows + `process.cwd()` instead of the plugin package + +- [ ] **Step 2: Add the failing OpenCode package-name regression path** + +If `OPENCODE_BIN` is available, update the OpenCode regression setup so it loads +`opencode-graphiti` by package name from isolated config and launches with a cwd +outside the repository tree. + +Example config payload to write into isolated config: + +```jsonc +{ + "plugin": ["opencode-graphiti"] +} +``` + +Keep the cwd pointed at a separate temp directory without matching dependency +entries. + +- [ ] **Step 3: Keep any DNT output inspection diagnostic-only** + +If you inspect emitted `dist/esm/...` files for debugging, do not fail the test +suite solely because DNT emitted `import-meta-ponyfill-esmodule`. + +The required contract is emitted package behavior: + +- Node package-name loading from a bare cwd reproduces the current bug +- the later fix makes package-relative runtime resolution work correctly + +- [ ] **Step 4: Run the targeted packaging test and verify it fails for the + right reason** + +Run: `deno test -A packaging.test.ts` + +Expected: + +- FAIL +- failure proves the package-name runtime regression is real under the installed + package simulation + +- [ ] **Step 5: Commit the red test change** + +```bash +git add packaging.test.ts +git commit -m "test: cover package-relative runtime resolution" +``` + +### Task 2: Fix Config Runtime Resolution + +Status: local implementation started; continue from the current `src/config.ts` +state instead of redoing the old `process.cwd()` anchor change. + +**Files:** + +- Modify: `src/config.ts` + +- [ ] **Step 1: Introduce a package-relative `createRequire(...)` anchor** + +The old code was: + +```ts +const nodeRequire = createRequire( + join(process.cwd(), "graphiti.config.runtime.cjs"), +); +``` + +Continue using a module-relative anchor derived from `import.meta.url`, +targeting the plugin package location rather than the caller cwd. + +Use the same URL-based `createRequire(...)` input form intended for +`connection-manager.ts`. + +- [ ] **Step 2: Keep the implementation minimal** + +Do not change config semantics. Only change how `cosmiconfig` is resolved at +runtime. + +- [ ] **Step 3: Run the targeted packaging test to confirm the config side is no + longer the blocker** + +Run: `deno test -A packaging.test.ts` + +Expected: + +- still FAIL or partially progress because the MCP SDK path is still broken +- config-only runtime loading no longer fails through `process.cwd()` + +Add the smallest targeted check needed to make that intermediate state explicit, +for example a Node snippet that exercises only the config loader path rather +than the full plugin bootstrap. + +Do not require the emitted `config.js` to avoid DNT's `import-meta` helper; only +require the config loader to behave correctly from the installed package shape. + +- [ ] **Step 4: Commit the config fix** + +```bash +git add src/config.ts packaging.test.ts +git commit -m "fix: resolve config runtime deps from package" +``` + +### Task 3: Fix MCP SDK Runtime Resolution + +**Files:** + +- Modify: `src/services/connection-manager.ts` + +- [ ] **Step 1: Replace the cwd-anchored runtime require** + +Replace the current: + +```ts +const nodeRequire = createRequire( + pathToFileURL(join(process.cwd(), "graphiti.runtime.cjs")).href, +); +``` + +with the same package-relative anchor strategy used in `src/config.ts`. + +- [ ] **Step 2: Preserve lazy runtime loading behavior** + +Keep the current shape: + +```ts +const resolvedPath = nodeRequire.resolve(specifier); +return await import(pathToFileURL(resolvedPath).href) as T; +``` + +Do not refactor the MCP connection manager beyond what is needed for package +relative resolution. + +- [ ] **Step 3: Keep JSON manifest behavior unchanged unless it blocks the + test** + +The `deno.json` import for `manifest.name` and `manifest.version` is not part of +this change. Only touch it if the packaging regression proves it is necessary. + +- [ ] **Step 4: Run the targeted packaging test and verify the runtime + regression turns green** + +Run: `deno test -A packaging.test.ts` + +Expected: + +- PASS for the runtime regression coverage added in Task 1 +- Node package-name import succeeds from the bare temp cwd +- optional OpenCode regression succeeds when `OPENCODE_BIN` is present + +- [ ] **Step 5: Commit the MCP SDK resolution fix** + +```bash +git add src/services/connection-manager.ts packaging.test.ts +git commit -m "fix: resolve MCP runtime deps from package" +``` + +### Task 4: Update Generated Package Metadata + +**Files:** + +- Modify: `dnt.ts` + +- [ ] **Step 1: Write the failing dependency metadata assertion** + +Add an assertion that generated `dist/package.json` contains: + +```ts +assertEquals( + builtPackage.dependencies?.["@modelcontextprotocol/sdk"], + expectedSdkVersionFromDenoJson, + "generated npm package must declare the MCP SDK for runtime loading", +); +``` + +Run: `deno test -A packaging.test.ts` + +Expected before the metadata change is applied: + +- FAIL on the missing generated dependency assertion + +- [ ] **Step 2: Add generated runtime dependency metadata for the MCP SDK** + +Update `dnt.ts` package dependencies to include: + +```ts +dependencies: { + "@modelcontextprotocol/sdk": sdkVersionFromDenoJson, + cosmiconfig: "^9.0.0", +}, +``` + +Mirror the version range already declared in `deno.json`. + +- [ ] **Step 3: Keep existing generated metadata intact** + +Do not change: + +- package name/version/entrypoint metadata +- `@types/node` in `devDependencies` +- hook registration metadata + +- [ ] **Step 4: Run the targeted packaging test and verify metadata assertions + pass** + +Run: `deno test -A packaging.test.ts` + +Expected: + +- PASS +- built `dist/package.json` contains both runtime dependencies + +- [ ] **Step 5: Commit the generated package metadata change** + +```bash +git add dnt.ts packaging.test.ts +git commit -m "fix: declare MCP SDK in generated package" +``` + +### Task 5: Refactor Only If the Anchor Logic Is Clearly Duplicated + +**Files:** + +- Modify: `src/config.ts` +- Modify: `src/services/connection-manager.ts` +- Create: only if a tiny shared helper is clearly justified + +- [ ] **Step 1: Compare the final package-relative anchor logic in both files** + +If the logic is identical and awkwardly duplicated, extract the smallest helper +that keeps emitted behavior obvious. + +- [ ] **Step 2: Do not extract a helper unless it simplifies both files** + +Prefer duplication over an unnecessary abstraction if the helper would only save +one or two lines. + +- [ ] **Step 3: Re-run the focused regression after any refactor** + +Run: `deno test -A packaging.test.ts` + +Expected: PASS + +- [ ] **Step 4: Commit the refactor only if one was actually needed** + +```bash +git add src/config.ts src/services/connection-manager.ts +git commit -m "refactor: share package-relative runtime anchor" +``` + +If no refactor was needed, skip this commit. + +### Task 6: Full Verification + +**Files:** + +- No new files + +- [ ] **Step 1: Run the focused regression one more time** + +Run: `deno test -A packaging.test.ts` + +Expected: PASS + +- [ ] **Step 2: Run the full test suite** + +Run: `deno test -A` + +Expected: PASS + +- [ ] **Step 3: Run type checking** + +Run: `deno task check` + +Expected: PASS + +- [ ] **Step 4: Run linting** + +Run: `deno task lint` + +Expected: PASS + +- [ ] **Step 5: Run formatting** + +Run: `deno task fmt` + +Expected: PASS or only intentional formatting updates + +- [ ] **Step 6: If formatting changed files, re-run the focused regression** + +Run: `deno test -A packaging.test.ts` + +Expected: PASS + +- [ ] **Step 7: Commit final verification-safe cleanup** + +```bash +git add packaging.test.ts src/config.ts src/services/connection-manager.ts dnt.ts +git commit -m "fix: anchor plugin runtime deps to package" +``` + +Skip this commit if the earlier per-task commits already cleanly capture the +final state and no additional changes were made. + +### Task 7: Completion Notes + +**Files:** + +- Modify only if needed: `README.md` + +- [ ] **Step 1: Check whether docs need a narrow clarification** + +Only update `README.md` if the local development testing guidance now needs a +small clarification that package-name install simulation is the preferred local +regression path for this bug class. + +- [ ] **Step 2: Keep documentation scope narrow** + +Do not rewrite installation docs unless the implementation changed the +documented contract. + +- [ ] **Step 3: Run affected verification again if docs stayed untouched** + +No command required if code did not change. + +- [ ] **Step 4: Commit doc clarification only if you actually changed docs** + +```bash +git add README.md +git commit -m "docs: clarify local package regression workflow" +``` + +Skip this commit if no doc change was necessary. diff --git a/docs/superpowers/specs/2026-04-07-package-relative-runtime-resolution-design.md b/docs/superpowers/specs/2026-04-07-package-relative-runtime-resolution-design.md new file mode 100644 index 0000000..b3a931a --- /dev/null +++ b/docs/superpowers/specs/2026-04-07-package-relative-runtime-resolution-design.md @@ -0,0 +1,237 @@ +# Package-Relative Runtime Resolution Design + +## Goal + +Make the published `opencode-graphiti` package initialize correctly no matter +which directory launches OpenCode, by resolving runtime dependencies relative to +the plugin package rather than `process.cwd()`. + +The hard requirement is the installed package contract: + +```jsonc +{ + "plugin": ["opencode-graphiti"] +} +``` + +This change should make a package-shaped local regression the primary test flow +for this bug, while keeping any still-documented built-file coverage as +secondary compatibility coverage. + +## Why This Change + +The current DNT-generated runtime still relies on a `process.cwd()`-derived +`createRequire(...)` anchor in: + +- `src/services/connection-manager.ts` + +The same bug existed in `src/config.ts` and has already been corrected locally; +the remaining design work is to carry the same package-relative rule through the +rest of the runtime and generated package metadata. + +This is incorrect for an installed plugin package because the plugin's runtime +dependencies (`cosmiconfig` and `@modelcontextprotocol/sdk`) belong to the +plugin package, not to the directory from which OpenCode was launched. + +As a result, the plugin can fail to initialize when OpenCode is started from a +directory whose package tree does not provide those dependencies, even though +the plugin itself is properly installed. + +`src/config.ts` now derives runtime package resolution from `import.meta.url`. +`src/services/connection-manager.ts` still needs the same package-relative +treatment. The key requirement is that the generated ESM package resolves +runtime dependencies correctly from the plugin package. + +## Required Behavior + +### Installed Package Mode + +- When OpenCode loads `opencode-graphiti` by package name, the plugin must + resolve `cosmiconfig` and `@modelcontextprotocol/sdk` from the plugin package + itself. +- Launch directory must not affect whether those dependencies resolve. +- The plugin must initialize successfully from directories other than the user's + home directory, assuming its own package dependencies are present. + +### Runtime Resolution Strategy + +- Remove the dependency on `process.cwd()` for Node-side runtime dependency + resolution in generated package code. +- Anchor `createRequire(...)` to the plugin package location derived from the + current module, not from the caller's working directory. +- Standardize both runtime loaders on the same `createRequire(...)` input shape + so they do not diverge across files. +- `src/config.ts` should use this package-relative require for `cosmiconfig`. +- `src/services/connection-manager.ts` should use the same package-relative + anchor to resolve and dynamically import `@modelcontextprotocol/sdk` runtime + modules. + +### Packaging Metadata + +- The generated npm package must declare both required runtime dependencies: + - `cosmiconfig` + - `@modelcontextprotocol/sdk` +- `@types/node` remains a generated development-only dependency for package + type-checking. + +### Local Development Validation + +- Direct `file:///.../dist/esm/mod.js` loading is no longer treated as the + primary local-dev compatibility target for this issue. +- Local regression coverage should instead simulate a real installed package + shape by placing `dist/` under an isolated `node_modules/opencode-graphiti` + path. +- The OpenCode regression should launch from a directory different from the + isolated home/config root so the test explicitly covers the cwd-sensitive bug. + +## Recommended Approach + +### Option A: Package-Relative Runtime Resolution + +Recommended. + +- Compute a stable runtime anchor from `import.meta.url` so the generated code + can locate the plugin package it lives in. +- Create a `require` instance from a synthetic file path inside that package. +- Use that `require` for CommonJS/Node package resolution. +- Keep MCP SDK loading lazy at runtime so initialization behavior remains close + to the existing shape. +- Standardize on a `file://` URL-based anchor for `createRequire(...)` in both + modules so the implementation does not mix bare filesystem paths and URL + strings. +- Accept DNT's emitted `import.meta` helper wiring if needed, as long as the + generated ESM package still resolves runtime dependencies relative to the + plugin package instead of the caller's cwd. + +This approach fixes the actual bug at the correct boundary: module resolution +should follow the plugin package, not the caller's cwd. + +### Option B: Bundle Runtime Dependencies + +Not recommended. + +- Remove runtime package resolution by bundling dependency code into the build. + +This is a larger and less stable change than necessary. It increases build +complexity without improving the supported package contract. + +### Option C: Preserve Raw `file://dist` Loading as First-Class + +Not recommended. + +- Continue optimizing the runtime specifically for bare built-file loading. + +That mode is useful for ad hoc debugging, but it should not drive the package +runtime design when the supported contract is package-name installation. + +## Implementation Shape + +### `src/config.ts` + +- Preserve the local fix that replaced the old `process.cwd()`-based + `createRequire(...)` anchor. +- Derive a package-relative anchor from the current module location. +- Ensure the resulting Node resolution path works after DNT emission inside + `dist/esm/...`. +- `import.meta.url` is now part of the implementation in this file. +- Continue using `nodeRequire("cosmiconfig")` for the actual runtime load. + +### `src/services/connection-manager.ts` + +- Replace the current `process.cwd()`-based `createRequire(...)` anchor. +- Reuse the same package-relative anchoring strategy as `src/config.ts`. +- Standardize the `createRequire(...)` input form with `src/config.ts` instead + of preserving the current mismatch between bare path and `file://` URL styles. +- Keep lazy runtime resolution of: + - `@modelcontextprotocol/sdk/client/index.js` + - `@modelcontextprotocol/sdk/client/streamableHttp.js` +- Continue resolving first, then dynamic-importing the resolved file URL, so the + runtime stays compatible with the current Node/Bun packaging behavior. + +### `dnt.ts` + +- Add `@modelcontextprotocol/sdk` to generated package `dependencies`, mirroring + the version range already declared in `deno.json` unless there is a deliberate + documented reason to diverge. +- Retain `cosmiconfig` in generated package `dependencies`. +- Retain `@types/node` in `devDependencies`. + +### `packaging.test.ts` + +- Keep `deno task build` as the packaging prerequisite. +- Assert that generated `dist/package.json` contains: + - `cosmiconfig` + - `@modelcontextprotocol/sdk` +- Add a package-name regression that specifically exercises the installed + package contract under Node from a cwd that does not provide the plugin's + dependencies: + - create a temp directory + - create `temp/node_modules/opencode-graphiti` pointing at `dist/` + - configure isolated OpenCode home/config to load `opencode-graphiti` by + package name + - run OpenCode from a separate arbitrary cwd + - assert initialization does not fail due to missing plugin dependency + resolution +- Keep any existing direct `file:///.../dist/esm/mod.js` coverage only as + compatibility coverage while README continues to document local built-file + installation. +- Do not fail the regression solely because DNT emitted + `import-meta-ponyfill-esmodule`; only fail when emitted package behavior is + wrong. + +## Testing Strategy + +Follow TDD for the behavior change. + +### Red + +- Add or adjust packaging coverage so the installed-package simulation fails + with the current cwd-anchored runtime resolution. +- Add metadata assertions that fail until `@modelcontextprotocol/sdk` is present + in generated package dependencies. +- Ensure the failing regression runs from a cwd outside the repository tree; + running from the workspace can mask the bug because the workspace already has + matching `node_modules` entries. + +### Green + +- Implement only the package-relative resolution and package metadata changes + needed to satisfy the new failing test coverage. + +### Refactor + +- If the package-relative anchor logic is duplicated between modules, extract + the smallest clear helper only if it keeps the emitted/runtime behavior + obvious. + +## Validation Plan + +At minimum, verify: + +- `deno test -A packaging.test.ts` +- `deno test -A` +- `deno task check` +- `deno task lint` +- `deno task fmt` + +The critical regression evidence is that the packaged plugin loads by package +name from a non-home, non-package cwd without missing `cosmiconfig` or MCP SDK +resolution failures. + +The published package entrypoint remains the generated ESM output +(`./esm/mod.js` in `dnt.ts`). This design targets that supported ESM package +path; it does not expand support guarantees for DNT's separate CommonJS/script +output. + +The design does not require DNT to preserve native source-level +`import.meta.url` syntax verbatim in emitted ESM. It only requires the emitted +package to resolve runtime dependencies correctly relative to the plugin +package. + +## Non-Goals + +- Do not make direct `file:///.../dist/esm/mod.js` loading the primary contract + that drives this fix. +- Do not redesign the plugin's public package surface. +- Do not change the plugin's config semantics beyond the runtime dependency + resolution boundary. diff --git a/packaging.test.ts b/packaging.test.ts index 21b1669..82e02b8 100644 --- a/packaging.test.ts +++ b/packaging.test.ts @@ -2,9 +2,13 @@ import { assertEquals } from "jsr:@std/assert@^1.0.0"; import { fromFileUrl } from "jsr:@std/path@^1.0.0/from-file-url"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; +import manifest from "./deno.json" with { type: "json" }; const workspaceRoot = new URL(".", import.meta.url); const workspacePath = fromFileUrl(workspaceRoot); +const expectedSdkVersionFromDenoJson = manifest.imports[ + "@modelcontextprotocol/sdk" +].replace("npm:@modelcontextprotocol/sdk@", ""); const packagingRunPermissions = await Promise.all([ Deno.permissions.query({ name: "run", command: "deno" }), Deno.permissions.query({ name: "run", command: "node" }), @@ -67,25 +71,21 @@ Deno.test({ dependencies?: Record; devDependencies?: Record; }; - const builtConfig = await Deno.readTextFile( - join(workspacePath, "dist/esm/src/config.js"), - ); assertEquals( builtPackage.dependencies?.cosmiconfig, "^9.0.0", "generated npm package must declare cosmiconfig for runtime config loading", ); + assertEquals( + builtPackage.dependencies?.["@modelcontextprotocol/sdk"], + expectedSdkVersionFromDenoJson, + "generated npm package must declare the MCP SDK for runtime loading", + ); assertEquals( typeof builtPackage.devDependencies?.["@types/node"], "string", "generated npm package must declare Node typings for dnt typecheck", ); - assertEquals( - builtConfig.includes("import-meta-ponyfill-esmodule"), - false, - "generated config loader should not depend on DNT import-meta ponyfill", - ); - const tempDir = await Deno.makeTempDir(); try { let optionalOpenCodePath: string | undefined; @@ -95,19 +95,54 @@ Deno.test({ optionalOpenCodePath = undefined; } - const esmRunnerPath = join(tempDir, "load-esm.mjs"); - const bunRunnerPath = join(tempDir, "load-bun.mjs"); + const installRoot = join(tempDir, "package-install"); + const isolatedCwd = join(tempDir, "bare-cwd"); + const installNodeModules = join(installRoot, "node_modules"); + const packageDir = join(installNodeModules, "opencode-graphiti"); + const esmRunnerPath = join(installRoot, "load-esm.mjs"); + const configRunnerPath = join(installRoot, "load-config.mjs"); + const nodePackageRunnerPath = join(installRoot, "load-node-package.mjs"); + const bunRunnerPath = join(installRoot, "load-bun-package.mjs"); const esmEntrypoint = pathToFileURL(join(workspacePath, "dist/esm/mod.js")).href; - const packageDir = join(tempDir, "node_modules", "opencode-graphiti"); const isolatedHome = join(tempDir, "home"); const isolatedConfig = join(isolatedHome, ".config", "opencode"); + const isolatedConfigPackageDir = join( + isolatedConfig, + "node_modules", + "opencode-graphiti", + ); - await Deno.mkdir(join(tempDir, "node_modules"), { recursive: true }); + await Deno.mkdir(installNodeModules, { recursive: true }); + await Deno.mkdir(isolatedCwd, { recursive: true }); await Deno.mkdir(isolatedConfig, { recursive: true }); + await Deno.writeTextFile( + join(isolatedCwd, ".graphitirc"), + `${ + JSON.stringify( + { graphiti: { endpoint: "http://127.0.0.1:8899/mcp" } }, + null, + 2, + ) + }\n`, + ); await Deno.symlink(join(workspacePath, "dist"), packageDir, { type: "dir", }); + await Deno.mkdir(join(isolatedConfig, "node_modules"), { + recursive: true, + }); + await Deno.symlink( + join(workspacePath, "dist"), + isolatedConfigPackageDir, + { + type: "dir", + }, + ); + await Deno.writeTextFile( + join(isolatedConfig, "opencode.json"), + `${JSON.stringify({ plugin: ["opencode-graphiti"] }, null, 2)}\n`, + ); await Deno.writeTextFile( esmRunnerPath, @@ -115,18 +150,84 @@ Deno.test({ JSON.stringify(esmEntrypoint) };\nconsole.log(JSON.stringify(Object.keys(plugin).sort()));\n`, ); + await Deno.writeTextFile( + configRunnerPath, + 'import "opencode-graphiti";\n' + + `const { loadConfig } = await import(${ + JSON.stringify( + pathToFileURL(join(packageDir, "esm/src/config.js")).href, + ) + });\n` + + "const config = loadConfig(process.cwd());\n" + + "console.log(JSON.stringify({ endpoint: config.endpoint, graphiti: config.graphiti.endpoint, redis: config.redis.endpoint }));\n", + ); + await Deno.writeTextFile( + nodePackageRunnerPath, + 'import * as plugin from "opencode-graphiti";\n' + + "console.log(JSON.stringify(Object.keys(plugin).sort()));\n" + + "plugin.graphiti({ client: {}, directory: process.cwd() }).then(async () => {\n" + + " await new Promise((resolve) => setTimeout(resolve, 1000));\n" + + ' console.log("initialized");\n' + + " process.exit(0);\n" + + "}, (error) => {\n" + + " console.error(error);\n" + + " process.exit(1);\n" + + "});\n", + ); await Deno.writeTextFile( bunRunnerPath, 'import * as plugin from "opencode-graphiti";\n' + "console.log(JSON.stringify(Object.keys(plugin).sort()));\n", ); - const esmLoad = await run("node", [esmRunnerPath]); + const esmLoad = await run("node", [esmRunnerPath], isolatedCwd); assertEquals(esmLoad.code, 0, esmLoad.stderr || esmLoad.stdout); assertEquals(esmLoad.stdout.trim(), '["graphiti"]'); + const configLoad = await run("node", [configRunnerPath], isolatedCwd); + assertEquals( + configLoad.code, + 0, + [ + "config loader should resolve cosmiconfig from the plugin package instead of process.cwd()", + configLoad.stderr || configLoad.stdout, + ].filter(Boolean).join("\n\n"), + ); + assertEquals( + configLoad.stdout.trim(), + '{"endpoint":"http://127.0.0.1:8899/mcp","graphiti":"http://127.0.0.1:8899/mcp","redis":"redis://127.0.0.1:6379"}', + ); + + const nodePackageLoad = await run( + "node", + [nodePackageRunnerPath], + isolatedCwd, + ); + assertEquals( + nodePackageLoad.code, + 0, + [ + "node package-name import from a bare cwd should succeed; this is the primary regression for cwd-sensitive runtime resolution", + nodePackageLoad.stderr || nodePackageLoad.stdout, + ].filter(Boolean).join("\n\n"), + ); + assertEquals( + nodePackageLoad.stdout.trim(), + '["graphiti"]\ninitialized', + ); + assertEquals( + nodePackageLoad.stderr.includes( + "Cannot find module '@modelcontextprotocol/sdk/client/index.js'", + ), + false, + [ + "node package-name import from a bare cwd should not resolve runtime dependencies through process.cwd()", + nodePackageLoad.stderr || nodePackageLoad.stdout, + ].filter(Boolean).join("\n\n"), + ); + if (bunRunPermissionGranted && await commandExists("bun")) { - const bunLoad = await run("bun", [bunRunnerPath], tempDir); + const bunLoad = await run("bun", [bunRunnerPath], isolatedCwd); assertEquals(bunLoad.code, 0, bunLoad.stderr || bunLoad.stdout); assertEquals(bunLoad.stdout.trim(), '["graphiti"]'); } @@ -139,7 +240,7 @@ Deno.test({ optionalOpenCodePath, { args: ["--print-logs", "stats"], - cwd: workspacePath, + cwd: isolatedCwd, env: { HOME: isolatedHome, XDG_CONFIG_HOME: join(isolatedHome, ".config"), @@ -150,11 +251,23 @@ Deno.test({ ).output(); const isolatedOpenCodeOutput = decodeText(isolatedOpenCode.stdout) + decodeText(isolatedOpenCode.stderr); + assertEquals( + isolatedOpenCode.code, + 0, + isolatedOpenCodeOutput, + ); assertEquals( isolatedOpenCodeOutput.includes("Missing 'default' export"), false, isolatedOpenCodeOutput, ); + assertEquals( + isolatedOpenCodeOutput.includes( + "Cannot find module '@modelcontextprotocol/sdk/client/index.js'", + ), + false, + isolatedOpenCodeOutput, + ); } } catch { // OPENCODE_BIN is optional; keep the portable package checks above. diff --git a/src/config.ts b/src/config.ts index c6a3865..86c1fa7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,6 @@ import os from "node:os"; import { createRequire } from "node:module"; import { join } from "node:path"; -import process from "node:process"; import { redactEndpointUserInfo } from "./services/endpoint-redaction.ts"; import { notifyPluginWarning } from "./services/opencode-warning.ts"; import type { GraphitiConfig, RawGraphitiConfig } from "./types/index.ts"; @@ -61,9 +60,7 @@ export interface ConfigExplorerAdapter { type ConfigExplorerFactory = () => ConfigExplorerAdapter; -const nodeRequire = createRequire( - join(process.cwd(), "graphiti.config.runtime.cjs"), -); +const nodeRequire = createRequire(import.meta.url); const isRecord = (value: unknown): value is Record => !!value && typeof value === "object" && !Array.isArray(value); diff --git a/src/services/connection-manager.ts b/src/services/connection-manager.ts index dcc647d..87aae36 100644 --- a/src/services/connection-manager.ts +++ b/src/services/connection-manager.ts @@ -1,7 +1,5 @@ import { createRequire } from "node:module"; -import { join } from "node:path"; import { pathToFileURL } from "node:url"; -import process from "node:process"; import manifest from "../../deno.json" with { type: "json" }; import { isAbortError } from "../utils.ts"; import { redactEndpointUserInfo } from "./endpoint-redaction.ts"; @@ -28,9 +26,7 @@ type McpRuntimeModules = { StreamableHTTPClientTransport: McpTransportConstructor; }; -const nodeRequire = createRequire( - pathToFileURL(join(process.cwd(), "graphiti.runtime.cjs")).href, -); +const nodeRequire = createRequire(import.meta.url); let mcpRuntimeModulesPromise: Promise | null = null; const importResolvedModule = async (specifier: string): Promise => {