Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bdab59f22 | ||
|
|
59507500ea | ||
|
|
3a08dcaeb1 | ||
|
|
c01b21d0f8 | ||
|
|
6dd98254be | ||
|
|
55a3a6c9eb |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "2.7.1",
|
||||
"version": "2.7.2",
|
||||
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -71,6 +71,14 @@
|
||||
"created_at": "2025-12-28T09:24:03Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 306
|
||||
},
|
||||
{
|
||||
"name": "Fguedes90",
|
||||
"id": 13650239,
|
||||
"comment_id": 3695136375,
|
||||
"created_at": "2025-12-28T23:34:19Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 319
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,8 @@ const OPENCODE_JSONC = join(OPENCODE_CONFIG_DIR, "opencode.jsonc")
|
||||
const OPENCODE_PACKAGE_JSON = join(OPENCODE_CONFIG_DIR, "package.json")
|
||||
const OMO_CONFIG = join(OPENCODE_CONFIG_DIR, "oh-my-opencode.json")
|
||||
|
||||
const OPENCODE_BINARIES = ["opencode", "opencode-desktop"] as const
|
||||
|
||||
const CHATGPT_HOTFIX_REPO = "code-yeongyu/opencode-openai-codex-auth#fix/orphaned-function-call-output-with-tools"
|
||||
|
||||
export async function fetchLatestVersion(packageName: string): Promise<string | null> {
|
||||
@@ -204,31 +206,38 @@ export function writeOmoConfig(installConfig: InstallConfig): ConfigMergeResult
|
||||
}
|
||||
}
|
||||
|
||||
export async function isOpenCodeInstalled(): Promise<boolean> {
|
||||
try {
|
||||
const proc = Bun.spawn(["opencode", "--version"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
await proc.exited
|
||||
return proc.exitCode === 0
|
||||
} catch {
|
||||
return false
|
||||
interface OpenCodeBinaryResult {
|
||||
binary: string
|
||||
version: string
|
||||
}
|
||||
|
||||
async function findOpenCodeBinaryWithVersion(): Promise<OpenCodeBinaryResult | null> {
|
||||
for (const binary of OPENCODE_BINARIES) {
|
||||
try {
|
||||
const proc = Bun.spawn([binary, "--version"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const output = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
if (proc.exitCode === 0) {
|
||||
return { binary, version: output.trim() }
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function isOpenCodeInstalled(): Promise<boolean> {
|
||||
const result = await findOpenCodeBinaryWithVersion()
|
||||
return result !== null
|
||||
}
|
||||
|
||||
export async function getOpenCodeVersion(): Promise<string | null> {
|
||||
try {
|
||||
const proc = Bun.spawn(["opencode", "--version"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const output = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
return proc.exitCode === 0 ? output.trim() : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
const result = await findOpenCodeBinaryWithVersion()
|
||||
return result?.version ?? null
|
||||
}
|
||||
|
||||
export async function addAuthPlugins(config: InstallConfig): Promise<ConfigMergeResult> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test"
|
||||
|
||||
import { createTodoContinuationEnforcer } from "./todo-continuation-enforcer"
|
||||
import { setMainSession } from "../features/claude-code-session-state"
|
||||
import { setMainSession, subagentSessions } from "../features/claude-code-session-state"
|
||||
import type { BackgroundManager } from "../features/background-agent"
|
||||
|
||||
describe("todo-continuation-enforcer", () => {
|
||||
@@ -51,10 +51,12 @@ describe("todo-continuation-enforcer", () => {
|
||||
promptCalls = []
|
||||
toastCalls = []
|
||||
setMainSession(undefined)
|
||||
subagentSessions.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
setMainSession(undefined)
|
||||
subagentSessions.clear()
|
||||
})
|
||||
|
||||
test("should inject continuation when idle with incomplete todos", async () => {
|
||||
@@ -143,6 +145,25 @@ describe("todo-continuation-enforcer", () => {
|
||||
expect(promptCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should inject for background task session (subagent)", async () => {
|
||||
// #given - main session set, background task session registered
|
||||
setMainSession("main-session")
|
||||
const bgTaskSession = "bg-task-session"
|
||||
subagentSessions.add(bgTaskSession)
|
||||
|
||||
const hook = createTodoContinuationEnforcer(createMockPluginInput(), {})
|
||||
|
||||
// #when - background task session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID: bgTaskSession } },
|
||||
})
|
||||
|
||||
// #then - continuation injected for background task session
|
||||
await new Promise(r => setTimeout(r, 2500))
|
||||
expect(promptCalls.length).toBe(1)
|
||||
expect(promptCalls[0].sessionID).toBe(bgTaskSession)
|
||||
})
|
||||
|
||||
test("should skip injection after recent error", async () => {
|
||||
// #given - session that just had an error
|
||||
const sessionID = "main-error"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { existsSync, readdirSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { getMainSessionID } from "../features/claude-code-session-state"
|
||||
import { getMainSessionID, subagentSessions } from "../features/claude-code-session-state"
|
||||
import {
|
||||
findNearestMessageWithFields,
|
||||
MESSAGE_STORAGE,
|
||||
@@ -265,8 +265,11 @@ export function createTodoContinuationEnforcer(
|
||||
log(`[${HOOK_NAME}] session.idle`, { sessionID })
|
||||
|
||||
const mainSessionID = getMainSessionID()
|
||||
if (mainSessionID && sessionID !== mainSessionID) {
|
||||
log(`[${HOOK_NAME}] Skipped: not main session`, { sessionID })
|
||||
const isMainSession = sessionID === mainSessionID
|
||||
const isBackgroundTaskSession = subagentSessions.has(sessionID)
|
||||
|
||||
if (mainSessionID && !isMainSession && !isBackgroundTaskSession) {
|
||||
log(`[${HOOK_NAME}] Skipped: not main or background task session`, { sessionID })
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { spawn } from "bun"
|
||||
import {
|
||||
resolveGrepCli,
|
||||
type GrepBackend,
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
DEFAULT_LIMIT,
|
||||
DEFAULT_MAX_DEPTH,
|
||||
@@ -10,6 +11,11 @@ import {
|
||||
import type { GlobOptions, GlobResult, FileMatch } from "./types"
|
||||
import { stat } from "node:fs/promises"
|
||||
|
||||
export interface ResolvedCli {
|
||||
path: string
|
||||
backend: GrepBackend
|
||||
}
|
||||
|
||||
function buildRgArgs(options: GlobOptions): string[] {
|
||||
const args: string[] = [
|
||||
...RG_FILES_FLAGS,
|
||||
@@ -40,6 +46,25 @@ function buildFindArgs(options: GlobOptions): string[] {
|
||||
return args
|
||||
}
|
||||
|
||||
function buildPowerShellCommand(options: GlobOptions): string[] {
|
||||
const maxDepth = Math.min(options.maxDepth ?? DEFAULT_MAX_DEPTH, DEFAULT_MAX_DEPTH)
|
||||
const paths = options.paths?.length ? options.paths : ["."]
|
||||
const searchPath = paths[0] || "."
|
||||
|
||||
const escapedPath = searchPath.replace(/'/g, "''")
|
||||
const escapedPattern = options.pattern.replace(/'/g, "''")
|
||||
|
||||
let psCommand = `Get-ChildItem -Path '${escapedPath}' -File -Recurse -Depth ${maxDepth - 1} -Filter '${escapedPattern}'`
|
||||
|
||||
if (options.hidden) {
|
||||
psCommand += " -Force"
|
||||
}
|
||||
|
||||
psCommand += " -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName"
|
||||
|
||||
return ["powershell", "-NoProfile", "-Command", psCommand]
|
||||
}
|
||||
|
||||
async function getFileMtime(filePath: string): Promise<number> {
|
||||
try {
|
||||
const stats = await stat(filePath)
|
||||
@@ -49,25 +74,40 @@ async function getFileMtime(filePath: string): Promise<number> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function runRgFiles(options: GlobOptions): Promise<GlobResult> {
|
||||
const cli = resolveGrepCli()
|
||||
export async function runRgFiles(
|
||||
options: GlobOptions,
|
||||
resolvedCli?: ResolvedCli
|
||||
): Promise<GlobResult> {
|
||||
const cli = resolvedCli ?? resolveGrepCli()
|
||||
const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS)
|
||||
const limit = Math.min(options.limit ?? DEFAULT_LIMIT, DEFAULT_LIMIT)
|
||||
|
||||
const isRg = cli.backend === "rg"
|
||||
const args = isRg ? buildRgArgs(options) : buildFindArgs(options)
|
||||
const isWindows = process.platform === "win32"
|
||||
|
||||
let command: string[]
|
||||
let cwd: string | undefined
|
||||
|
||||
const paths = options.paths?.length ? options.paths : ["."]
|
||||
if (isRg) {
|
||||
const args = buildRgArgs(options)
|
||||
const paths = options.paths?.length ? options.paths : ["."]
|
||||
args.push(...paths)
|
||||
command = [cli.path, ...args]
|
||||
cwd = undefined
|
||||
} else if (isWindows) {
|
||||
command = buildPowerShellCommand(options)
|
||||
cwd = undefined
|
||||
} else {
|
||||
const args = buildFindArgs(options)
|
||||
const paths = options.paths?.length ? options.paths : ["."]
|
||||
cwd = paths[0] || "."
|
||||
command = [cli.path, ...args]
|
||||
}
|
||||
|
||||
const cwd = paths[0] || "."
|
||||
|
||||
const proc = spawn([cli.path, ...args], {
|
||||
const proc = spawn(command, {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
cwd: isRg ? undefined : cwd,
|
||||
cwd,
|
||||
})
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
@@ -106,7 +146,15 @@ export async function runRgFiles(options: GlobOptions): Promise<GlobResult> {
|
||||
break
|
||||
}
|
||||
|
||||
const filePath = isRg ? line : `${cwd}/${line}`
|
||||
let filePath: string
|
||||
if (isRg) {
|
||||
filePath = line
|
||||
} else if (isWindows) {
|
||||
filePath = line.trim()
|
||||
} else {
|
||||
filePath = `${cwd}/${line}`
|
||||
}
|
||||
|
||||
const mtime = await getFileMtime(filePath)
|
||||
files.push({ path: filePath, mtime })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { resolveGrepCli, type GrepBackend } from "../grep/constants"
|
||||
export { resolveGrepCli, resolveGrepCliWithAutoInstall, type GrepBackend } from "../grep/constants"
|
||||
|
||||
export const DEFAULT_TIMEOUT_MS = 60_000
|
||||
export const DEFAULT_LIMIT = 100
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import { runRgFiles } from "./cli"
|
||||
import { resolveGrepCliWithAutoInstall } from "./constants"
|
||||
import { formatGlobResult } from "./utils"
|
||||
|
||||
export const glob: ToolDefinition = tool({
|
||||
@@ -21,12 +22,16 @@ export const glob: ToolDefinition = tool({
|
||||
},
|
||||
execute: async (args) => {
|
||||
try {
|
||||
const cli = await resolveGrepCliWithAutoInstall()
|
||||
const paths = args.path ? [args.path] : undefined
|
||||
|
||||
const result = await runRgFiles({
|
||||
pattern: args.pattern,
|
||||
paths,
|
||||
})
|
||||
const result = await runRgFiles(
|
||||
{
|
||||
pattern: args.pattern,
|
||||
paths,
|
||||
},
|
||||
cli
|
||||
)
|
||||
|
||||
return formatGlobResult(result)
|
||||
} catch (e) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs"
|
||||
import { join, dirname } from "node:path"
|
||||
import { spawnSync } from "node:child_process"
|
||||
import { getInstalledRipgrepPath, downloadAndInstallRipgrep } from "./downloader"
|
||||
import { getDataDir } from "../../shared/data-path"
|
||||
|
||||
export type GrepBackend = "rg" | "grep"
|
||||
|
||||
@@ -36,6 +37,9 @@ function getOpenCodeBundledRg(): string | null {
|
||||
const rgName = isWindows ? "rg.exe" : "rg"
|
||||
|
||||
const candidates = [
|
||||
// OpenCode XDG data path (highest priority - where OpenCode installs rg)
|
||||
join(getDataDir(), "opencode", "bin", rgName),
|
||||
// Legacy paths relative to execPath
|
||||
join(execDir, rgName),
|
||||
join(execDir, "bin", rgName),
|
||||
join(execDir, "..", "bin", rgName),
|
||||
|
||||
@@ -163,6 +163,12 @@ export function isServerInstalled(command: string[]): boolean {
|
||||
if (command.length === 0) return false
|
||||
|
||||
const cmd = command[0]
|
||||
|
||||
// Support absolute paths (e.g., C:\Users\...\server.exe or /usr/local/bin/server)
|
||||
if (cmd.includes("/") || cmd.includes("\\")) {
|
||||
if (existsSync(cmd)) return true
|
||||
}
|
||||
|
||||
const isWindows = process.platform === "win32"
|
||||
const ext = isWindows ? ".exe" : ""
|
||||
|
||||
@@ -192,6 +198,11 @@ export function isServerInstalled(command: string[]): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
// Runtime wrappers (bun/node) are always available in oh-my-opencode context
|
||||
if (cmd === "bun" || cmd === "node") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user