refactor(packages): extract agents-md-core package
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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
@@ -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:*",
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export * from "./src/index";
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -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 = "]";
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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/**/*"]
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user