diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index b5e8cfd..0000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize, ready_for_review, reopened] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' - plugins: 'code-review@claude-code-plugins' - prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - diff --git a/README.md b/README.md index 19da222..41516ea 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,10 @@ is safe to re-run; if anything is missing, add it manually: "opencode-supermemory", // ... other OpenCode 2 plugins ], - "permissions": [ - { "action": "supermemory_recall", "resource": "*", "effect": "allow" }, - ], + // OpenCode V1 rejects V2 "permissions"; OpenCode 2 reads this as the same rule + "permission": { + "supermemory_recall": "allow", + }, } ``` @@ -220,9 +221,7 @@ OpenCode 2 side by side: { "plugin": ["opencode-supermemory@latest"], "plugins": ["opencode-supermemory"], - "permissions": [ - { "action": "supermemory_recall", "resource": "*", "effect": "allow" }, - ], + "permission": { "supermemory_recall": "allow" }, } ``` diff --git a/src/services/opencode-config.test.ts b/src/services/opencode-config.test.ts index 79b1a48..d27fc47 100644 --- a/src/services/opencode-config.test.ts +++ b/src/services/opencode-config.test.ts @@ -18,7 +18,8 @@ describe("OpenCode config editor", () => { expect(result.changed).toBe(true); expect(config.plugin).toEqual([V1_PLUGIN_ENTRY]); expect(config.plugins).toEqual([V2_PLUGIN_ENTRY]); - expect(config.permissions).toEqual([RECALL_PERMISSION]); + expect(config.permission).toEqual({ supermemory_recall: "allow" }); + expect(config.permissions).toBeUndefined(); expect(result.warnings).toEqual([]); }); @@ -36,7 +37,7 @@ describe("OpenCode config editor", () => { expect(first.content).not.toContain(V1_PLUGIN_ENTRY); const config = JSON.parse(first.content.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1")); expect(config.plugins).toEqual([V2_PLUGIN_ENTRY]); - expect(config.permissions).toEqual([RECALL_PERMISSION]); + expect(config.permission).toEqual({ supermemory_recall: "allow" }); const second = editOpenCodeConfig(first.content); expect(second.changed).toBe(false); @@ -55,19 +56,39 @@ describe("OpenCode config editor", () => { const content = JSON.stringify({ plugin: ["opencode-supermemory@2.0.13"], plugins: [{ package: "opencode-supermemory@latest", options: {} }], - permissions: [RECALL_PERMISSION], + permission: { supermemory_recall: "allow" }, }); expect(editOpenCodeConfig(content).changed).toBe(false); }); + test("moves the V2 recall rule that OpenCode V1 rejects into V1 permission rules", () => { + const other = { action: "bash", resource: "*", effect: "ask" }; + const onlyRecall = JSON.parse( + editOpenCodeConfig(JSON.stringify({ permissions: [RECALL_PERMISSION] })).content, + ); + expect(onlyRecall.permissions).toBeUndefined(); + expect(onlyRecall.permission).toEqual({ supermemory_recall: "allow" }); + + const withOther = JSON.parse( + editOpenCodeConfig(JSON.stringify({ permissions: [RECALL_PERMISSION, other] })).content, + ); + expect(withOther.permissions).toEqual([other]); + expect(withOther.permission).toEqual({ supermemory_recall: "allow" }); + }); + test("keeps an explicit recall deny and warns instead of overriding it", () => { const deny = { ...RECALL_PERMISSION, effect: "deny" }; const result = editOpenCodeConfig(JSON.stringify({ permissions: [deny] })); const config = JSON.parse(result.content); expect(config.permissions).toEqual([deny]); + expect(config.permission).toBeUndefined(); expect(result.warnings).toHaveLength(1); expect(readOpenCodeRegistration(result.content).recallDenied).toBe(true); + + const v1Deny = editOpenCodeConfig(JSON.stringify({ permission: { supermemory_recall: "deny" } })); + expect(JSON.parse(v1Deny.content).permission).toEqual({ supermemory_recall: "deny" }); + expect(v1Deny.warnings).toHaveLength(1); }); test("rejects configs whose arrays are not arrays", () => { diff --git a/src/services/opencode-config.ts b/src/services/opencode-config.ts index c486259..4a6838d 100644 --- a/src/services/opencode-config.ts +++ b/src/services/opencode-config.ts @@ -151,16 +151,23 @@ function isRecallPermission(value: unknown, effect: "allow" | "deny"): boolean { ); } +function getV1RecallRule(config: JsonObject): unknown { + return isObject(config.permission) ? config.permission[RECALL_PERMISSION.action] : undefined; +} + export function readOpenCodeRegistration(content: string): OpenCodeRegistration { const config = parseOpenCodeConfig(content); const plugin = Array.isArray(config.plugin) ? config.plugin : []; const plugins = Array.isArray(config.plugins) ? config.plugins : []; const permissions = Array.isArray(config.permissions) ? config.permissions : []; + const v1Rule = getV1RecallRule(config); return { v1: plugin.some(isSupermemoryPluginEntry), v2: plugins.some(isSupermemoryPluginEntry), - recallAllowed: permissions.some((entry) => isRecallPermission(entry, "allow")), - recallDenied: permissions.some((entry) => isRecallPermission(entry, "deny")), + recallAllowed: + v1Rule === "allow" || permissions.some((entry) => isRecallPermission(entry, "allow")), + recallDenied: + v1Rule === "deny" || permissions.some((entry) => isRecallPermission(entry, "deny")), }; } @@ -178,26 +185,32 @@ export function editOpenCodeConfig(rawContent: string): OpenCodeConfigEditResult content = addArrayEntry(content, "plugin", V1_PLUGIN_ENTRY, isSupermemoryPluginEntry); content = addArrayEntry(content, "plugins", V2_PLUGIN_ENTRY, isSupermemoryPluginEntry); - const config = parseOpenCodeConfig(content); + let config = parseOpenCodeConfig(content); const permissions = config.permissions; if (permissions !== undefined && !Array.isArray(permissions)) { throw new Error('OpenCode config property "permissions" must be an array'); } - const permissionEntries = permissions ?? []; - if (permissionEntries.some((entry) => isRecallPermission(entry, "deny"))) { - warnings.push( - 'OpenCode 2 permission "supermemory_recall" is explicitly denied; preserving the deny instead of adding an allow.', - ); - } else if ( - !permissionEntries.some((entry) => isRecallPermission(entry, "allow")) - ) { - content = addArrayEntry( + const legacyAllow = (permissions ?? []).findIndex((entry) => isRecallPermission(entry, "allow")); + if (legacyAllow !== -1) { + content = applyModification( content, - "permissions", - RECALL_PERMISSION, - (entry) => isRecallPermission(entry, "allow"), + permissions!.length === 1 ? ["permissions"] : ["permissions", legacyAllow], + undefined, + ); + config = parseOpenCodeConfig(content); + } + + const v1Rule = getV1RecallRule(config); + const recallDenied = + v1Rule === "deny" || + (permissions ?? []).some((entry) => isRecallPermission(entry, "deny")); + if (recallDenied) { + warnings.push( + 'OpenCode permission "supermemory_recall" is explicitly denied; preserving the deny instead of adding an allow.', ); + } else if (v1Rule === undefined && (config.permission === undefined || isObject(config.permission))) { + content = applyModification(content, ["permission", RECALL_PERMISSION.action], "allow"); } return {