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
17 changes: 13 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Plugin, Hooks } from "@opencode-ai/plugin";
import { join } from "path";
import { load, config } from "./config";
import { ensureProject, isFirstRun } from "./db";
import * as temporal from "./temporal";
Expand Down Expand Up @@ -67,6 +68,14 @@ export function buildRecoveryMessage(
].join("\n");
}

/**
* Check whether a project path is valid for file operations (e.g. AGENTS.md export/import).
* Returns false for root ("/"), empty, or falsy paths to prevent writing to the filesystem root.
*/
export function isValidProjectPath(p: string): boolean {
return !!p && p !== "/";
}

export const LorePlugin: Plugin = async (ctx) => {
const projectPath = ctx.worktree || ctx.directory;
try {
Expand All @@ -88,8 +97,8 @@ export const LorePlugin: Plugin = async (ctx) => {
// (hand-written entries, edits from other machines, or merge conflicts).
{
const cfg = config();
if (cfg.knowledge.enabled && cfg.agentsFile.enabled) {
const filePath = `${projectPath}/${cfg.agentsFile.path}`;
if (isValidProjectPath(projectPath) && cfg.knowledge.enabled && cfg.agentsFile.enabled) {
const filePath = join(projectPath, cfg.agentsFile.path);
if (shouldImport({ projectPath, filePath })) {
try {
importFromFile({ projectPath, filePath });
Expand Down Expand Up @@ -424,8 +433,8 @@ export const LorePlugin: Plugin = async (ctx) => {
// Export curated knowledge to AGENTS.md after distillation + curation.
try {
const agentsCfg = cfg.agentsFile;
if (cfg.knowledge.enabled && agentsCfg.enabled) {
const filePath = `${projectPath}/${agentsCfg.path}`;
if (isValidProjectPath(projectPath) && cfg.knowledge.enabled && agentsCfg.enabled) {
const filePath = join(projectPath, agentsCfg.path);
exportToFile({ projectPath, filePath });
}
} catch (e) {
Expand Down
44 changes: 43 additions & 1 deletion test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect, beforeEach } from "bun:test";
import { isContextOverflow, buildRecoveryMessage, LorePlugin } from "../src/index";
import { isContextOverflow, buildRecoveryMessage, LorePlugin, isValidProjectPath } from "../src/index";
import * as ltm from "../src/ltm";
import type { Plugin } from "@opencode-ai/plugin";

Expand Down Expand Up @@ -622,3 +622,45 @@ describe("LTM session cache", () => {
}
});
});

// ── isValidProjectPath tests ─────────────────────────────────────────

describe("isValidProjectPath", () => {
test("returns false for root path '/'", () => {
expect(isValidProjectPath("/")).toBe(false);
});

test("returns false for empty string", () => {
expect(isValidProjectPath("")).toBe(false);
});

test("returns true for a normal project path", () => {
expect(isValidProjectPath("/home/user/project")).toBe(true);
});

test("returns true for a relative path", () => {
expect(isValidProjectPath("./my-project")).toBe(true);
});
});

// ── Plugin with invalid project path ─────────────────────────────────

describe("LorePlugin — invalid project path", () => {
test("initializes without crashing when projectPath is '/'", async () => {
const { client } = createMockClient();

// Simulate launching outside a git repo: no worktree, directory is "/"
const hooks = await LorePlugin({
client,
project: { id: "test", path: "/" } as any,
directory: "/",
worktree: "",
serverUrl: new URL("http://localhost:0"),
$: {} as any,
});

// Plugin should return hooks without crashing
expect(hooks).toBeTruthy();
expect(hooks.event).toBeDefined();
});
});
Loading