Detailed changes
@@ -2,7 +2,7 @@ import { Type } from "@sinclair/typebox";
import type { AgentTool } from "@mariozechner/pi-ai";
import simpleGit from "simple-git";
import { ToolInputError } from "../../../util/errors.js";
-import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "../../../util/truncate.js";
+import { formatSize, truncateHead } from "../../../util/truncate.js";
const BlameSchema = Type.Object({
path: Type.String({ description: "File path relative to repo root" }),
@@ -2,7 +2,7 @@ import { Type } from "@sinclair/typebox";
import type { AgentTool } from "@mariozechner/pi-ai";
import simpleGit from "simple-git";
import { ToolInputError } from "../../../util/errors.js";
-import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "../../../util/truncate.js";
+import { formatSize, truncateHead } from "../../../util/truncate.js";
const DiffSchema = Type.Object({
ref: Type.Optional(Type.String({ description: "Base ref (optional)" })),
@@ -2,7 +2,7 @@ import { Type } from "@sinclair/typebox";
import type { AgentTool } from "@mariozechner/pi-ai";
import simpleGit from "simple-git";
import { ToolInputError } from "../../../util/errors.js";
-import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "../../../util/truncate.js";
+import { formatSize, truncateHead } from "../../../util/truncate.js";
const ShowSchema = Type.Object({
ref: Type.String({ description: "Commit hash or ref" }),
@@ -1,4 +1,5 @@
import { readFile } from "node:fs/promises";
+import { expandHomePath } from "../../util/path.js";
import { applyConfigOverrides, loadConfig } from "../../config/loader.js";
import { createWorkspace } from "../../workspace/manager.js";
import { createGrepTool } from "../../agent/tools/grep.js";
@@ -42,8 +43,7 @@ export async function runRepoCommand(options: RepoCommandOptions): Promise<void>
let systemPrompt = REPO_SYSTEM_PROMPT;
const promptPath = overrides.repo.system_prompt_path;
if (promptPath) {
- const home = process.env["HOME"] ?? "";
- systemPrompt = await readFile(promptPath.replace(/^~\//, `${home}/`), "utf8");
+ systemPrompt = await readFile(expandHomePath(promptPath), "utf8");
}
const git = simpleGit();
@@ -1,5 +1,6 @@
import { readFile } from "node:fs/promises";
import { basename } from "node:path";
+import { expandHomePath } from "../../util/path.js";
import { applyConfigOverrides, loadConfig } from "../../config/loader.js";
import { createWorkspace } from "../../workspace/manager.js";
import { writeWorkspaceFile } from "../../workspace/content.js";
@@ -53,8 +54,7 @@ export async function runWebCommand(options: WebCommandOptions): Promise<void> {
let systemPrompt = WEB_SYSTEM_PROMPT;
const promptPath = overrides.web.system_prompt_path;
if (promptPath) {
- const home = process.env["HOME"] ?? "";
- systemPrompt = await readFile(promptPath.replace(/^~\//, `${home}/`), "utf8");
+ systemPrompt = await readFile(expandHomePath(promptPath), "utf8");
}
const tools = [
@@ -4,7 +4,6 @@ import { runRepoCommand } from "./commands/repo.js";
import { RumiloError } from "../util/errors.js";
import { parseArgs } from "./parse-args.js";
-export { parseArgs };
async function main() {
const { command, options, positional } = parseArgs(process.argv);
@@ -34,9 +34,12 @@ export function parseArgs(args: string[]): ParsedArgs {
}
} else if (arg.startsWith("-")) {
const short = arg.slice(1);
- if (short === "u" && rest[i + 1] && !rest[i + 1]!.startsWith("-")) {
- options["uri"] = rest[i + 1] as string;
- i += 1;
+ if (short === "u") {
+ if (rest[i + 1] && !rest[i + 1]!.startsWith("-")) {
+ options["uri"] = rest[i + 1] as string;
+ i += 1;
+ }
+ // else: -u with no value β uri stays unset, command handler validates
} else if (short === "f") {
options["full"] = true;
} else {
@@ -0,0 +1,16 @@
+import { resolve } from "node:path";
+
+/**
+ * Expand a leading ~ in a file path to the user's home directory.
+ * Use for paths outside the workspace (e.g. system_prompt_path).
+ * Workspace-sandboxed paths should NOT use this.
+ */
+export function expandHomePath(filePath: string): string {
+ const home = process.env["HOME"];
+ if (!home) return filePath;
+
+ if (filePath === "~") return home;
+ if (filePath.startsWith("~/")) return resolve(home, filePath.slice(2));
+
+ return filePath;
+}
@@ -1,6 +1,6 @@
import { mkdir, writeFile } from "node:fs/promises";
-import { dirname, join, resolve, sep } from "node:path";
-import { ToolInputError } from "../util/errors.js";
+import { dirname, join } from "node:path";
+import { ensureWorkspacePath } from "../agent/tools/path-utils.js";
export interface WorkspaceContent {
filePath: string;
@@ -8,22 +8,13 @@ export interface WorkspaceContent {
bytes: number;
}
-function ensureContained(workspacePath: string, targetPath: string): void {
- const resolved = resolve(workspacePath, targetPath);
- const root = workspacePath.endsWith(sep) ? workspacePath : `${workspacePath}${sep}`;
-
- if (resolved !== workspacePath && !resolved.startsWith(root)) {
- throw new ToolInputError(`Path escapes workspace: ${targetPath}`);
- }
-}
-
export async function writeWorkspaceFile(
workspacePath: string,
relativePath: string,
content: string,
): Promise<WorkspaceContent> {
const filePath = join(workspacePath, relativePath);
- ensureContained(workspacePath, filePath);
+ ensureWorkspacePath(workspacePath, filePath);
await mkdir(dirname(filePath), { recursive: true });
await writeFile(filePath, content, "utf8");
@@ -26,7 +26,7 @@ describe("CLI --key=value parsing (issue #6)", () => {
describe("CLI -u short flag (issue #7)", () => {
test("-u does not swallow a following flag as its value", () => {
const result = parseArgs(["node", "script", "web", "-u", "--verbose"]);
- expect(result.options["uri"]).not.toBe("--verbose");
+ expect(result.options["uri"]).toBeUndefined();
expect(result.options["verbose"]).toBe(true);
});
@@ -37,8 +37,14 @@ describe("CLI -u short flag (issue #7)", () => {
test("-u swallowing -f as value is prevented", () => {
const result = parseArgs(["node", "script", "repo", "-u", "-f"]);
- expect(result.options["uri"]).not.toBe("-f");
- // -u becomes boolean-like (true), -f should be parsed as full flag
+ expect(result.options["uri"]).toBeUndefined();
+ // -u with no valid value is a no-op, -f should be parsed as full flag
expect(result.options["full"]).toBe(true);
});
+
+ test("-u at end of args leaves uri unset (no stray option)", () => {
+ const result = parseArgs(["node", "script", "web", "-u"]);
+ expect(result.options["uri"]).toBeUndefined();
+ expect(result.options["u"]).toBeUndefined();
+ });
});