fix(auto-slash-command): resolve project commands from session dir

Use the plugin session directory instead of process.cwd() when resolving project slash commands. This restores project and opencode-project slashcommand behavior when the runtime cwd differs from the actual session workspace.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-03-31 22:08:00 -07:00
parent 7f846b2da3
commit ea14a1a346
5 changed files with 71 additions and 2 deletions
+2 -1
View File
@@ -42,11 +42,12 @@ export interface ExecutorOptions {
pluginsEnabled?: boolean
enabledPluginsOverride?: Record<string, boolean>
agent?: string
directory?: string
}
async function discoverAllCommands(options?: ExecutorOptions): Promise<CommandInfo[]> {
const discoveredCommands = discoverCommandsSync(process.cwd(), {
const discoveredCommands = discoverCommandsSync(options?.directory ?? process.cwd(), {
pluginsEnabled: options?.pluginsEnabled,
enabledPluginsOverride: options?.enabledPluginsOverride,
})
+2
View File
@@ -68,6 +68,7 @@ export interface AutoSlashCommandHookOptions {
skills?: LoadedSkill[]
pluginsEnabled?: boolean
enabledPluginsOverride?: Record<string, boolean>
directory?: string
}
export function createAutoSlashCommandHook(options?: AutoSlashCommandHookOptions) {
@@ -75,6 +76,7 @@ export function createAutoSlashCommandHook(options?: AutoSlashCommandHookOptions
skills: options?.skills,
pluginsEnabled: options?.pluginsEnabled,
enabledPluginsOverride: options?.enabledPluginsOverride,
directory: options?.directory,
}
const sessionProcessedCommands = createProcessedCommandStore()
const sessionProcessedCommandExecutions = createProcessedCommandStore()
+38 -1
View File
@@ -1,4 +1,7 @@
import { describe, expect, it, beforeEach, mock, spyOn } from "bun:test"
import { describe, expect, it, beforeEach, afterEach, spyOn } from "bun:test"
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
import type {
AutoSlashCommandHookInput,
@@ -39,11 +42,45 @@ function createMockOutput(text: string): AutoSlashCommandHookOutput {
}
describe("createAutoSlashCommandHook", () => {
let tempDir = ""
let originalWorkingDirectory = ""
beforeEach(() => {
logMock.mockClear()
tempDir = mkdtempSync(join(tmpdir(), "omo-auto-slash-hook-test-"))
originalWorkingDirectory = process.cwd()
})
afterEach(() => {
process.chdir(originalWorkingDirectory)
rmSync(tempDir, { recursive: true, force: true })
})
describe("slash command replacement", () => {
it("should resolve project commands from provided directory even when cwd differs", async () => {
// given
const projectDir = join(tempDir, "project")
const commandDir = join(projectDir, ".claude", "commands")
mkdirSync(commandDir, { recursive: true })
writeFileSync(
join(commandDir, "project-only-command.md"),
`---\ndescription: Project command\n---\nExecute from project directory.\n`,
)
process.chdir("/tmp")
const hook = createAutoSlashCommandHook({ directory: projectDir })
const input = createMockInput(`test-session-project-${Date.now()}`)
const output = createMockOutput("/project-only-command")
// when
await hook["chat.message"](input, output)
// then
expect(output.parts[0].text).toContain("<auto-slash-command>")
expect(output.parts[0].text).toContain("Execute from project directory.")
expect(output.parts[0].text).toContain("**Scope**: project")
})
it("should not modify message when command not found", async () => {
// given a slash command that doesn't exist
const hook = createAutoSlashCommandHook()
+1
View File
@@ -42,6 +42,7 @@ export function createSkillHooks(args: {
skills: mergedSkills,
pluginsEnabled: pluginConfig.claude_code?.plugins ?? true,
enabledPluginsOverride: pluginConfig.claude_code?.plugins_override,
directory: ctx.directory,
}))
: null
@@ -60,4 +60,32 @@ describe("slashcommand discovery and execution compatibility", () => {
expect(result.replacementText).toContain("Execute from parent config.")
expect(result.replacementText).toContain("**Scope**: opencode")
})
it("executes project commands using the provided directory even when cwd differs", async () => {
// given
const projectDir = join(tempDir, "project")
const commandDir = join(projectDir, ".claude", "commands")
const commandName = "project-only-command"
mkdirSync(commandDir, { recursive: true })
writeFileSync(
join(commandDir, `${commandName}.md`),
`---\ndescription: Project command\n---\nExecute from project directory.\n`,
)
process.chdir("/tmp")
expect(discoverCommandsSync(projectDir).some(command => command.name === commandName)).toBe(true)
// when
const result = await executeSlashCommand({
command: commandName,
args: "",
raw: `/${commandName}`,
}, { skills: [], directory: projectDir })
// then
expect(result.success).toBe(true)
expect(result.replacementText).toContain("Execute from project directory.")
expect(result.replacementText).toContain("**Scope**: project")
})
})