refactor(packages): extract agents-md-core package

This commit is contained in:
YeonGyu-Kim
2026-05-21 02:45:20 +09:00
parent 2748009ff2
commit 7c7aa28160
18 changed files with 276 additions and 101 deletions
File diff suppressed because one or more lines are too long
+11
View File
@@ -24,6 +24,7 @@
"vscode-jsonrpc": "^8.2.1",
},
"devDependencies": {
"@oh-my-opencode/agents-md-core": "workspace:*",
"@oh-my-opencode/ast-grep-core": "workspace:*",
"@oh-my-opencode/ast-grep-mcp": "workspace:*",
"@oh-my-opencode/boulder-state": "workspace:*",
@@ -55,6 +56,14 @@
"zod": "^4.0.0",
},
},
"packages/agents-md-core": {
"name": "@oh-my-opencode/agents-md-core",
"version": "0.1.0",
"dependencies": {
"@oh-my-opencode/rules-core": "workspace:*",
"@oh-my-opencode/utils": "workspace:*",
},
},
"packages/ast-grep-core": {
"name": "@oh-my-opencode/ast-grep-core",
"version": "0.1.0",
@@ -178,6 +187,8 @@
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="],
"@oh-my-opencode/agents-md-core": ["@oh-my-opencode/agents-md-core@workspace:packages/agents-md-core"],
"@oh-my-opencode/ast-grep-core": ["@oh-my-opencode/ast-grep-core@workspace:packages/ast-grep-core"],
"@oh-my-opencode/ast-grep-mcp": ["@oh-my-opencode/ast-grep-mcp@workspace:packages/ast-grep-mcp"],
+4 -2
View File
@@ -12,7 +12,8 @@
"packages/utils",
"packages/model-core",
"packages/comment-checker-core",
"packages/boulder-state"
"packages/boulder-state",
"packages/agents-md-core"
],
"bin": {
"oh-my-opencode": "bin/oh-my-opencode.js",
@@ -47,7 +48,7 @@
"prepublishOnly": "bun run clean && bun run build:lsp-tools-mcp && bun run build",
"test:model-capabilities": "bun test src/shared/model-capability-aliases.test.ts src/shared/model-capability-guardrails.test.ts src/shared/model-capabilities.test.ts src/cli/doctor/checks/model-resolution.test.ts --bail",
"typecheck": "tsgo --noEmit && bun run typecheck:packages",
"typecheck:packages": "tsgo --noEmit -p packages/rules-core/tsconfig.json && tsgo --noEmit -p packages/ast-grep-core/tsconfig.json && tsgo --noEmit -p packages/ast-grep-mcp/tsconfig.json && tsgo --noEmit -p packages/utils/tsconfig.json && tsgo --noEmit -p packages/model-core/tsconfig.json && tsgo --noEmit -p packages/comment-checker-core/tsconfig.json && tsgo --noEmit -p packages/boulder-state/tsconfig.json",
"typecheck:packages": "tsgo --noEmit -p packages/rules-core/tsconfig.json && tsgo --noEmit -p packages/ast-grep-core/tsconfig.json && tsgo --noEmit -p packages/ast-grep-mcp/tsconfig.json && tsgo --noEmit -p packages/utils/tsconfig.json && tsgo --noEmit -p packages/model-core/tsconfig.json && tsgo --noEmit -p packages/comment-checker-core/tsconfig.json && tsgo --noEmit -p packages/boulder-state/tsconfig.json && tsgo --noEmit -p packages/agents-md-core/tsconfig.json",
"typecheck:script": "tsgo --noEmit -p script/tsconfig.json",
"test": "bun test",
"build:ast-grep-mcp": "bun run --cwd packages/ast-grep-mcp build"
@@ -93,6 +94,7 @@
"devDependencies": {
"@oh-my-opencode/ast-grep-core": "workspace:*",
"@oh-my-opencode/ast-grep-mcp": "workspace:*",
"@oh-my-opencode/agents-md-core": "workspace:*",
"@oh-my-opencode/boulder-state": "workspace:*",
"@oh-my-opencode/comment-checker-core": "workspace:*",
"@oh-my-opencode/model-core": "workspace:*",
+1
View File
@@ -0,0 +1 @@
export * from "./src/index";
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@oh-my-opencode/agents-md-core",
"version": "0.1.0",
"type": "module",
"private": true,
"description": "Pure TypeScript AGENTS.md discovery and injection core for oh-my-opencode.",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./src/index.ts"
}
},
"types": "./index.d.ts",
"scripts": {
"typecheck": "tsgo --noEmit -p tsconfig.json",
"test": "bun test src/*.test.ts"
},
"dependencies": {
"@oh-my-opencode/rules-core": "workspace:*",
"@oh-my-opencode/utils": "workspace:*"
}
}
+6
View File
@@ -0,0 +1,6 @@
export const AGENTS_FILENAME = "AGENTS.md";
export const TRUNCATION_NOTICE_PREFIX =
"\n\n[Note: Content was truncated to save context window space. For full context, please read the file directly: ";
export const TRUNCATION_NOTICE_SUFFIX = "]";
+20
View File
@@ -0,0 +1,20 @@
import {
findAgentsMdUp as findAgentsMdUpCore,
} from "@oh-my-opencode/rules-core";
import { isAbsolute, resolve } from "node:path";
import type { AgentsMdDiscoveryInput } from "./types";
export function resolveFilePath(rootDirectory: string, path: string): string | null {
if (!path) return null;
if (isAbsolute(path)) return path;
return resolve(rootDirectory, path);
}
export async function findAgentsMdUp(input: AgentsMdDiscoveryInput): Promise<string[]> {
return findAgentsMdUpCore({
startDir: input.startDir,
rootDir: input.rootDir,
cache: input.cache,
});
}
+15
View File
@@ -0,0 +1,15 @@
import {
TRUNCATION_NOTICE_PREFIX,
TRUNCATION_NOTICE_SUFFIX,
} from "./constants";
export function formatAgentsMdContextBlock(input: {
readonly agentsPath: string;
readonly content: string;
readonly truncated: boolean;
}): string {
const truncationNotice = input.truncated
? `${TRUNCATION_NOTICE_PREFIX}${input.agentsPath}${TRUNCATION_NOTICE_SUFFIX}`
: "";
return `\n\n[Directory Context: ${input.agentsPath}]\n${input.content}${truncationNotice}`;
}
+12
View File
@@ -0,0 +1,12 @@
export { AGENTS_FILENAME } from "./constants";
export { findAgentsMdUp, resolveFilePath } from "./finder";
export { formatAgentsMdContextBlock } from "./formatter";
export { getSessionCache } from "./injection-cache";
export { processFilePathForAgentsInjection } from "./injector";
export type {
AgentsMdContextOutput,
AgentsMdDiscoveryInput,
AgentsMdInjectedPathsStorage,
AgentsMdTruncator,
TruncationResult,
} from "./types";
@@ -0,0 +1,14 @@
import type { AgentsMdInjectedPathsStorage } from "./types";
export function getSessionCache(input: {
readonly sessionCaches: Map<string, Set<string>>;
readonly sessionID: string;
readonly storage: Pick<AgentsMdInjectedPathsStorage, "loadInjectedPaths">;
}): Set<string> {
const existing = input.sessionCaches.get(input.sessionID);
if (existing) return existing;
const loaded = input.storage.loadInjectedPaths(input.sessionID);
input.sessionCaches.set(input.sessionID, loaded);
return loaded;
}
+67
View File
@@ -0,0 +1,67 @@
import type { AgentsMdCache } from "@oh-my-opencode/rules-core";
import { promises as fsPromises } from "node:fs";
import { dirname } from "node:path";
import { findAgentsMdUp, resolveFilePath } from "./finder";
import { formatAgentsMdContextBlock } from "./formatter";
import { getSessionCache } from "./injection-cache";
import type {
AgentsMdContextOutput,
AgentsMdInjectedPathsStorage,
AgentsMdTruncator,
} from "./types";
export async function processFilePathForAgentsInjection(input: {
readonly rootDirectory: string;
readonly truncator: AgentsMdTruncator;
readonly sessionCaches: Map<string, Set<string>>;
readonly storage: AgentsMdInjectedPathsStorage;
readonly agentsMdCache?: AgentsMdCache;
readonly filePath: string;
readonly sessionID: string;
readonly output: AgentsMdContextOutput;
}): Promise<void> {
if (typeof input.output.output !== "string") return;
const resolved = resolveFilePath(input.rootDirectory, input.filePath);
if (!resolved) return;
const dir = dirname(resolved);
const cache = getSessionCache({
sessionCaches: input.sessionCaches,
sessionID: input.sessionID,
storage: input.storage,
});
const agentsPaths = await findAgentsMdUp({
startDir: dir,
rootDir: input.rootDirectory,
cache: input.agentsMdCache,
});
let dirty = false;
for (const agentsPath of agentsPaths) {
const agentsDir = dirname(agentsPath);
if (cache.has(agentsDir)) continue;
const content = await fsPromises.readFile(agentsPath, "utf-8").catch(() => null);
if (content === null) continue;
cache.add(agentsDir);
const { result, truncated } = await input.truncator.truncate(
input.sessionID,
content,
);
input.output.output += formatAgentsMdContextBlock({
agentsPath,
content: result,
truncated,
});
dirty = true;
}
if (dirty) {
input.storage.saveInjectedPaths(input.sessionID, cache);
}
}
+27
View File
@@ -0,0 +1,27 @@
import type { AgentsMdCache } from "@oh-my-opencode/rules-core";
export interface TruncationResult {
readonly result: string;
readonly truncated: boolean;
}
export interface AgentsMdTruncator {
truncate(sessionID: string, content: string): Promise<TruncationResult>;
}
export interface AgentsMdContextOutput {
readonly title: string;
output: string;
readonly metadata: unknown;
}
export interface AgentsMdInjectedPathsStorage {
loadInjectedPaths(sessionID: string): Set<string>;
saveInjectedPaths(sessionID: string, paths: Set<string>): void;
}
export interface AgentsMdDiscoveryInput {
readonly startDir: string;
readonly rootDir: string;
readonly cache?: AgentsMdCache;
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ESNext"],
"types": ["bun-types"]
},
"include": ["src/**/*"]
}
+4 -17
View File
@@ -1,17 +1,4 @@
import { findAgentsMdUp as findAgentsMdUpCore } from "@oh-my-opencode/rules-core";
import type { AgentsMdCache } from "@oh-my-opencode/rules-core";
import { isAbsolute, resolve } from "node:path";
export function resolveFilePath(rootDirectory: string, path: string): string | null {
if (!path) return null;
if (isAbsolute(path)) return path;
return resolve(rootDirectory, path);
}
export async function findAgentsMdUp(input: {
readonly startDir: string;
readonly rootDir: string;
readonly cache?: AgentsMdCache;
}): Promise<string[]> {
return findAgentsMdUpCore({ startDir: input.startDir, rootDir: input.rootDir, cache: input.cache });
}
export {
findAgentsMdUp,
resolveFilePath,
} from "@oh-my-opencode/agents-md-core";
+7 -2
View File
@@ -4,7 +4,11 @@ import type { PluginInput } from "@opencode-ai/plugin";
import { createDynamicTruncator } from "../../shared/dynamic-truncator";
import { resolveSessionEventID } from "../../shared/event-session-id";
import { processFilePathForAgentsInjection } from "./injector";
import { clearInjectedPaths } from "./storage";
import {
clearInjectedPaths,
loadInjectedPaths,
saveInjectedPaths,
} from "./storage";
interface ToolExecuteInput {
tool: string;
@@ -44,9 +48,10 @@ export function createDirectoryAgentsInjectorHook(
if (toolName === "read") {
await processFilePathForAgentsInjection({
ctx,
rootDirectory: ctx.directory,
truncator,
sessionCaches,
storage: { loadInjectedPaths, saveInjectedPaths },
agentsMdCache,
filePath: output.title,
sessionID: input.sessionID,
@@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto"
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import type { PluginInput } from "@opencode-ai/plugin"
import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
const storageMaps = new Map<string, Set<string>>()
@@ -35,6 +34,13 @@ const truncator = {
}),
}
const storage = {
loadInjectedPaths: (sessionID: string) => storageMaps.get(sessionID) ?? new Set<string>(),
saveInjectedPaths: (sessionID: string, paths: Set<string>) => {
storageMaps.set(sessionID, paths)
},
}
describe("processFilePathForAgentsInjection", () => {
let testRoot = ""
let srcDirectory = ""
@@ -71,9 +77,10 @@ describe("processFilePathForAgentsInjection", () => {
// when
await processFilePathForAgentsInjection({
ctx: { directory: testRoot } as PluginInput,
rootDirectory: testRoot,
truncator,
sessionCaches: new Map(),
storage,
filePath: join(srcDirectory, "file.ts"),
sessionID: "session-parent",
output,
@@ -110,9 +117,10 @@ describe("processFilePathForAgentsInjection", () => {
// when
await processFilePathForAgentsInjection({
ctx: { directory: testRoot } as PluginInput,
rootDirectory: testRoot,
truncator,
sessionCaches: new Map(),
storage,
filePath: join(testRoot, "file.ts"),
sessionID: "session-root-skip",
output,
@@ -130,9 +138,10 @@ describe("processFilePathForAgentsInjection", () => {
// when
await processFilePathForAgentsInjection({
ctx: { directory: testRoot } as PluginInput,
rootDirectory: testRoot,
truncator,
sessionCaches: new Map(),
storage,
filePath: join(componentsDirectory, "button.ts"),
sessionID: "session-multiple",
output,
@@ -151,18 +160,20 @@ describe("processFilePathForAgentsInjection", () => {
// when
await processFilePathForAgentsInjection({
ctx: { directory: testRoot } as PluginInput,
rootDirectory: testRoot,
truncator,
sessionCaches,
storage,
filePath: join(componentsDirectory, "button.ts"),
sessionID: "session-cache",
output,
})
const outputAfterFirstCall = output.output
await processFilePathForAgentsInjection({
ctx: { directory: testRoot } as PluginInput,
rootDirectory: testRoot,
truncator,
sessionCaches,
storage,
filePath: join(componentsDirectory, "button.ts"),
sessionID: "session-cache",
output,
@@ -191,9 +202,10 @@ describe("processFilePathForAgentsInjection", () => {
// when
await processFilePathForAgentsInjection({
ctx: { directory: testRoot } as PluginInput,
rootDirectory: testRoot,
truncator: truncatedTruncator,
sessionCaches: new Map(),
storage,
filePath: join(srcDirectory, "file.ts"),
sessionID: "session-truncated",
output,
@@ -211,9 +223,10 @@ describe("processFilePathForAgentsInjection", () => {
// when
await processFilePathForAgentsInjection({
ctx: { directory: testRoot } as PluginInput,
rootDirectory: testRoot,
truncator,
sessionCaches: new Map(),
storage,
filePath: "",
sessionID: "session-empty-path",
output,
@@ -1,72 +1 @@
import type { PluginInput } from "@opencode-ai/plugin";
import type { AgentsMdCache } from "@oh-my-opencode/rules-core";
import { promises as fsPromises } from "node:fs";
import { dirname } from "node:path";
import type { createDynamicTruncator } from "../../shared/dynamic-truncator";
import { findAgentsMdUp, resolveFilePath } from "./finder";
import { loadInjectedPaths, saveInjectedPaths } from "./storage";
type DynamicTruncator = ReturnType<typeof createDynamicTruncator>;
function getSessionCache(
sessionCaches: Map<string, Set<string>>,
sessionID: string,
): Set<string> {
if (!sessionCaches.has(sessionID)) {
sessionCaches.set(sessionID, loadInjectedPaths(sessionID));
}
const cache = sessionCaches.get(sessionID);
if (cache) return cache;
const loaded = loadInjectedPaths(sessionID);
sessionCaches.set(sessionID, loaded);
return loaded;
}
export async function processFilePathForAgentsInjection(input: {
ctx: PluginInput;
truncator: DynamicTruncator;
sessionCaches: Map<string, Set<string>>;
agentsMdCache?: AgentsMdCache;
filePath: string;
sessionID: string;
output: { title: string; output: string; metadata: unknown };
}): Promise<void> {
// Guard: output.output may be non-string at runtime (e.g. MCP bridge format changes).
// Consistent with the pattern used in tool-output-truncator and other hooks.
if (typeof input.output.output !== "string") return;
const resolved = resolveFilePath(input.ctx.directory, input.filePath);
if (!resolved) return;
const dir = dirname(resolved);
const cache = getSessionCache(input.sessionCaches, input.sessionID);
const agentsPaths = await findAgentsMdUp({
startDir: dir,
rootDir: input.ctx.directory,
cache: input.agentsMdCache,
});
let dirty = false;
for (const agentsPath of agentsPaths) {
const agentsDir = dirname(agentsPath);
if (cache.has(agentsDir)) continue;
const content = await fsPromises.readFile(agentsPath, "utf-8").catch(() => null);
if (content === null) continue;
cache.add(agentsDir);
const { result, truncated } = await input.truncator.truncate(
input.sessionID,
content,
);
const truncationNotice = truncated
? `\n\n[Note: Content was truncated to save context window space. For full context, please read the file directly: ${agentsPath}]`
: "";
input.output.output += `\n\n[Directory Context: ${agentsPath}]\n${result}${truncationNotice}`;
dirty = true;
}
if (dirty) {
saveInjectedPaths(input.sessionID, cache);
}
}
export { processFilePathForAgentsInjection } from "@oh-my-opencode/agents-md-core";