Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [7.0.3] - 2026-10-13

### Fixed

- **Bundled `rg` (ripgrep) is now on `PATH` for `Bash` commands and hooks across all OSes.** `@vscode/ripgrep` was previously in `optionalDependencies` and only resolved by the legacy `search_files` executor; shell commands spawned by `Bash` (`executeCommand`, background commands) and hooks inherited the raw `process.env.PATH`, so `rg` failed with `command not found` on machines where ripgrep was not installed globally. `@vscode/ripgrep` is now a regular dependency (installing the matching platform binary via its per-OS/arch packages on macOS, Linux, and Windows), and its `bin/` directory is appended to `PATH` for all spawned shell commands and hooks so `rg` works out of the box while still respecting any system `rg` already on the user's `PATH`.

## [7.0.0] - 2026-10-09

### Added
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@matterailab/orbcode",
"version": "7.0.2",
"version": "7.0.3",
"description": "OrbCode CLI — agentic coding in your terminal, by MatterAI",
"type": "module",
"bin": {
Expand Down Expand Up @@ -67,7 +67,8 @@
"picomatch": "4.0.4",
"react": "^19.2.0",
"read-excel-file": "^9.2.0",
"string-width": "^7.2.0"
"string-width": "^7.2.0",
"@vscode/ripgrep": "1.18.0"
},
"devDependencies": {
"@types/node": "^20.17.0",
Expand All @@ -77,8 +78,7 @@
"typescript": "^5.7.2"
},
"optionalDependencies": {
"@ff-labs/fff-node": "0.10.1",
"@vscode/ripgrep": "1.18.0"
"@ff-labs/fff-node": "0.10.1"
},
"keywords": [
"orbcode",
Expand Down
4 changes: 2 additions & 2 deletions src/core/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawn } from "node:child_process"

import { getShell, getShellRunArgs } from "../utils/shell.js"
import { getShell, getShellRunArgs, withBundledToolsOnPath } from "../utils/shell.js"

/**
* Lifecycle hooks, modeled on Claude Code's hooks feature. Users configure
Expand Down Expand Up @@ -369,7 +369,7 @@ const HOOK_ENV_REDACT_PATTERN = /(?:^|_)(TOKEN|KEY|SECRET|PASSWORD|PASSWD|CREDEN
* credential — hooks must never see the user's API token. */
function buildHookEnv(cwd: string): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { MATTERAI_PROJECT_DIR: cwd }
for (const [key, value] of Object.entries(process.env)) {
for (const [key, value] of Object.entries(withBundledToolsOnPath(process.env))) {
if (value === undefined) continue
if (HOOK_ENV_REDACT_EXACT.has(key)) continue
if (HOOK_ENV_REDACT_PATTERN.test(key)) continue
Expand Down
4 changes: 3 additions & 1 deletion src/tools/executors/backgroundCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as fs from "node:fs"
import * as os from "node:os"
import * as path from "node:path"

import { withBundledToolsOnPath } from "../../utils/shell.js"

export interface BackgroundCommand {
id: string
/** Task that started the command; scopes listing, notices and cleanup. */
Expand Down Expand Up @@ -126,7 +128,7 @@ export function startBackgroundCommand(

const child = spawn(shell, shellArgs, {
cwd,
env: { ...process.env, TERM: "dumb" },
env: { ...withBundledToolsOnPath(process.env), TERM: "dumb" },
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
})
Expand Down
4 changes: 2 additions & 2 deletions src/tools/executors/executeCommand.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { spawn } from "node:child_process"
import * as fs from "node:fs"

import { getShell, getShellRunArgs, isCmdShell } from "../../utils/shell.js"
import { getShell, getShellRunArgs, isCmdShell, withBundledToolsOnPath } from "../../utils/shell.js"
import { type ToolContext, type ToolResult, resolveWorkspacePath } from "../types.js"
import { startBackgroundCommand } from "./backgroundCommands.js"

Expand Down Expand Up @@ -50,7 +50,7 @@ export async function executeCommand(args: Record<string, unknown>, context: Too
const isWindows = process.platform === "win32"
const child = spawn(getShell(), getShellRunArgs(command), {
cwd,
env: { ...process.env, TERM: "dumb" },
env: { ...withBundledToolsOnPath(process.env), TERM: "dumb" },
stdio: ["ignore", "pipe", "pipe"],
// Own process group, so a timeout/interrupt can kill grandchildren too
// (`find /` started by the shell would otherwise outlive the shell and
Expand Down
55 changes: 55 additions & 0 deletions src/utils/shell.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as fs from "node:fs"
import { createRequire } from "node:module"
import * as path from "node:path"

/**
Expand All @@ -9,6 +10,60 @@ import * as path from "node:path"
* - Windows: Git Bash when installed, else ComSpec (cmd.exe).
*/
let cached: string | undefined
let cachedRgBinDir: string | null | undefined

const require = createRequire(import.meta.url)

/**
* Resolve the bundled `@vscode/ripgrep` binary path, if installed for this
* platform/arch (`darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`,
* `win32-x64`, `win32-arm64`, etc.).
*/
export function getBundledRipgrepPath(): string | undefined {
try {
const mod = require("@vscode/ripgrep") as { rgPath?: unknown }
if (typeof mod.rgPath === "string" && fs.statSync(mod.rgPath, { throwIfNoEntry: false })?.isFile()) {
return mod.rgPath
}
} catch {
// Platform binary package may not be installed on an unsupported architecture.
}
return undefined
}

/**
* Directory containing the bundled `rg` (`rg.exe` on Windows) binary, so we
* can append it to `PATH` when spawning shell commands and hooks. Appending
* (rather than prepending) respects any `rg` the user already has on `PATH`
* while guaranteeing `rg` works out of the box when not installed globally.
*/
export function getBundledRipgrepDir(): string | undefined {
if (cachedRgBinDir === undefined) {
const rgPath = getBundledRipgrepPath()
cachedRgBinDir = rgPath ? path.dirname(rgPath) : null
}
return cachedRgBinDir ?? undefined
}

/**
* Return `baseEnv` with the bundled ripgrep directory appended to `PATH` (or
* `Path` on Windows when that casing is used) if not already present.
*/
export function withBundledToolsOnPath(baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
const rgDir = getBundledRipgrepDir()
if (!rgDir) return { ...baseEnv }
const env: NodeJS.ProcessEnv = { ...baseEnv }
const pathKey =
process.platform === "win32"
? (Object.keys(env).find((k) => k.toLowerCase() === "path") ?? "PATH")
: "PATH"
const current = env[pathKey] ?? ""
const entries = current.split(path.delimiter).filter(Boolean)
if (!entries.includes(rgDir)) {
env[pathKey] = entries.length > 0 ? `${current}${path.delimiter}${rgDir}` : rgDir
}
return env
}

function firstExisting(candidates: string[]): string | undefined {
return candidates.find((candidate) => {
Expand Down
19 changes: 19 additions & 0 deletions test/read-only-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,22 @@ test("shell selection prefers a bash-compatible shell", async () => {
assert.equal(isCmdShell("C:\\Windows\\System32\\cmd.exe"), true)
assert.equal(isCmdShell("/bin/bash"), false)
})

test("bundled ripgrep is exposed on PATH for Bash commands", async () => {
const { getBundledRipgrepDir, getBundledRipgrepPath, withBundledToolsOnPath } = await import("../src/utils/shell.js")
const { executeCommand } = await import("../src/tools/executors/executeCommand.js")
const rgPath = getBundledRipgrepPath()
const rgDir = getBundledRipgrepDir()
assert.ok(rgPath, "expected @vscode/ripgrep binary to resolve on this host")
assert.ok(rgDir, "expected bundled ripgrep bin directory")

const envWithTools = withBundledToolsOnPath({ PATH: "/usr/bin:/bin" })
assert.ok(envWithTools.PATH?.split(process.platform === "win32" ? ";" : ":").includes(rgDir))

const res = await executeCommand(
{ command: "rg --version" },
{ cwd: process.cwd(), token: "", getTodos: () => "", setTodos: () => {} },
)
assert.equal(res.isError, false, res.text)
assert.match(res.text, /^ripgrep \d+\.\d+\.\d+/m)
})
Loading