Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
node_modules/
dist/
!plugins/omo/components/*/dist/
!plugins/omo/components/*/dist/**
.env
.env.*
*.tgz
Expand Down
2 changes: 2 additions & 0 deletions plugins/omo/components/comment-checker/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ node_modules/
.env.*
coverage/
.vitest/
!dist/
!dist/**
7 changes: 7 additions & 0 deletions plugins/omo/components/comment-checker/dist/apply-patch.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { CommentCheckRequest } from "./types.js";
export declare function extractApplyPatchRequests(event: {
details?: unknown;
input: Record<string, unknown>;
toolName: string;
}): CommentCheckRequest[];
export declare function parseApplyPatchRequests(patch: string, sourceToolName?: string): CommentCheckRequest[];
173 changes: 173 additions & 0 deletions plugins/omo/components/comment-checker/dist/apply-patch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { getString, isRecord } from "./record.js";
export function extractApplyPatchRequests(event) {
const metadataRequests = extractApplyPatchMetadataRequests(event.details, event.toolName);
if (metadataRequests.length > 0)
return metadataRequests;
const patch = getString(event.input, ["input", "patch", "command"]);
if (!patch)
return [];
return parseApplyPatchRequests(patch, event.toolName);
}
export function parseApplyPatchRequests(patch, sourceToolName = "apply_patch") {
const requests = [];
let current;
const flush = () => {
if (!current)
return;
if (current.operation === "add") {
const content = joinPatchLines(current.newLines);
if (content.length > 0) {
requests.push({
sourceToolName,
toolName: "Write",
filePath: current.filePath,
toolInput: {
file_path: current.filePath,
content,
},
});
}
}
if (current.operation === "update") {
const newString = joinPatchLines(current.newLines);
if (newString.length > 0) {
const filePath = current.movePath ?? current.filePath;
requests.push({
sourceToolName,
toolName: "Edit",
filePath,
toolInput: {
file_path: filePath,
old_string: joinPatchLines(current.oldLines),
new_string: newString,
},
});
}
}
current = undefined;
};
for (const line of patch.split(/\r?\n/)) {
if (line === "*** Begin Patch" || line === "*** End Patch")
continue;
if (line.startsWith("*** Add File: ")) {
flush();
current = makeAccumulator("add", line.slice("*** Add File: ".length).trim());
continue;
}
if (line.startsWith("*** Update File: ")) {
flush();
current = makeAccumulator("update", line.slice("*** Update File: ".length).trim());
continue;
}
if (line.startsWith("*** Delete File: ")) {
flush();
current = makeAccumulator("delete", line.slice("*** Delete File: ".length).trim());
continue;
}
if (line.startsWith("*** Move to: ")) {
if (current?.operation === "update")
current.movePath = line.slice("*** Move to: ".length).trim();
continue;
}
if (!current)
continue;
if (line.startsWith("@@"))
continue;
if (current.operation === "add") {
if (line.startsWith("+"))
current.newLines.push(line.slice(1));
continue;
}
if (current.operation === "update") {
if (line.startsWith("+"))
current.newLines.push(line.slice(1));
if (line.startsWith("-"))
current.oldLines.push(line.slice(1));
}
}
flush();
return requests;
}
function extractApplyPatchMetadataRequests(details, sourceToolName) {
const metadataFiles = getApplyPatchMetadataFiles(details);
if (metadataFiles.length === 0)
return [];
const requests = [];
for (const file of metadataFiles) {
if (file.type === "delete")
continue;
const filePath = file.movePath ?? file.filePath;
if (file.before.length === 0) {
requests.push({
sourceToolName,
toolName: "Write",
filePath,
toolInput: {
file_path: filePath,
content: file.after,
},
});
continue;
}
requests.push({
sourceToolName,
toolName: "Edit",
filePath,
toolInput: {
file_path: filePath,
old_string: file.before,
new_string: file.after,
},
});
}
return requests;
}
function getApplyPatchMetadataFiles(details) {
if (!isRecord(details))
return [];
const direct = readApplyPatchMetadataFiles(details["files"]);
if (direct.length > 0)
return direct;
const resultDetails = details["result"];
const result = isRecord(resultDetails) ? readApplyPatchMetadataFiles(resultDetails["files"]) : [];
if (result.length > 0)
return result;
const metadataDetails = details["metadata"];
const metadata = isRecord(metadataDetails) ? readApplyPatchMetadataFiles(metadataDetails["files"]) : [];
return metadata;
}
function readApplyPatchMetadataFiles(value) {
if (!Array.isArray(value))
return [];
const files = [];
for (const item of value) {
if (!isRecord(item))
continue;
const filePath = getString(item, ["filePath", "file_path", "path"]);
const movePath = getString(item, ["movePath", "move_path"]);
const before = getString(item, ["before", "old", "oldString", "old_string"]);
const after = getString(item, ["after", "new", "newString", "new_string"]);
const type = getString(item, ["type", "operation"]);
if (!filePath || before === undefined || after === undefined)
continue;
files.push({
filePath,
before,
after,
...(movePath === undefined ? {} : { movePath }),
...(type === undefined ? {} : { type }),
});
}
return files;
}
function makeAccumulator(operation, filePath) {
return {
operation,
filePath,
oldLines: [],
newLines: [],
};
}
function joinPatchLines(lines) {
return lines.length === 0 ? "" : `${lines.join("\n")}\n`;
}
2 changes: 2 additions & 0 deletions plugins/omo/components/comment-checker/dist/cli.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};
10 changes: 10 additions & 0 deletions plugins/omo/components/comment-checker/dist/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env node
import { runCodexHookCli } from "./codex-hook.js";
const [command, subcommand] = process.argv.slice(2);
if (command === "hook" && subcommand === "post-tool-use") {
await runCodexHookCli();
}
else {
process.stderr.write("Usage: omo-comment-checker hook post-tool-use\n");
process.exitCode = 2;
}
22 changes: 22 additions & 0 deletions plugins/omo/components/comment-checker/dist/codex-hook.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type CommentCheckRequest } from "./core.js";
import { type CommentCheckerRunner } from "./runner.js";
export type CodexPostToolUseInput = {
session_id: string;
turn_id: string;
transcript_path: string | null;
cwd: string;
hook_event_name: "PostToolUse";
model: string;
permission_mode: string;
tool_name: string;
tool_input: Record<string, unknown>;
tool_response: unknown;
tool_use_id: string;
};
export type CodexHookOptions = {
run?: CommentCheckerRunner;
};
export declare function extractCodexCommentCheckRequests(input: CodexPostToolUseInput): CommentCheckRequest[];
export declare function runCommentCheckerPostToolUse(input: CodexPostToolUseInput, options?: CodexHookOptions): Promise<string>;
export declare function runCodexHookCli(): Promise<void>;
export declare function parseCodexPostToolUseInput(input: string): CodexPostToolUseInput | undefined;
165 changes: 165 additions & 0 deletions plugins/omo/components/comment-checker/dist/codex-hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { readFileSync } from "node:fs";
import { stdin as processStdin, stdout as processStdout } from "node:process";
import { extractCommentCheckRequests, isRecord, toHookInput, } from "./core.js";
import { runCommentChecker } from "./runner.js";
const DEFAULT_MAX_HOOK_FEEDBACK_CHARS = 8000;
const CONTEXT_PRESSURE_MAX_HOOK_FEEDBACK_CHARS = 1200;
const CONTEXT_PRESSURE_MARKERS = [
"context compacted",
"context_length_exceeded",
"skill descriptions were shortened",
"context_too_large",
"codex ran out of room in the model's context window",
"your input exceeds the context window",
"long threads and multiple compactions",
];
export function extractCodexCommentCheckRequests(input) {
return extractCommentCheckRequests(toToolResultLike(input));
}
export async function runCommentCheckerPostToolUse(input, options = {}) {
const requests = extractCodexCommentCheckRequests(input);
if (requests.length === 0)
return "";
const runner = options.run ?? runCommentChecker;
const warnings = [];
for (const request of requests) {
const context = {
sessionId: input.session_id,
cwd: input.cwd,
...(input.transcript_path === null ? {} : { transcriptPath: input.transcript_path }),
};
const result = await runner(toHookInput(request, context));
if (result.status === "missing" || result.status === "pass")
continue;
if (result.status === "error")
continue;
const message = normalizeHookText(result.message);
if (message.length > 0) {
warnings.push({ filePath: request.filePath, message });
}
}
if (warnings.length === 0)
return "";
return JSON.stringify({
decision: "block",
reason: limitHookText(formatWarnings(warnings), hookFeedbackLimit(input.transcript_path)),
});
}
export async function runCodexHookCli() {
const input = await readStdin();
if (input.trim().length === 0)
return;
const parsed = parseCodexPostToolUseInput(input);
if (!parsed)
return;
const output = await runCommentCheckerPostToolUse(parsed);
if (output.length > 0) {
processStdout.write(output);
processStdout.write("\n");
}
}
export function parseCodexPostToolUseInput(input) {
let parsed;
try {
parsed = JSON.parse(input);
}
catch {
return undefined;
}
return isCodexPostToolUseInput(parsed) ? parsed : undefined;
}
function toToolResultLike(input) {
return {
toolName: input.tool_name,
input: normalizeToolInput(input.tool_name, input.tool_input),
content: normalizeToolResponse(input.tool_response),
isError: isErrorResponse(input.tool_response),
details: isRecord(input.tool_response) ? input.tool_response : undefined,
};
}
function normalizeToolInput(toolName, toolInput) {
if (toolName === "apply_patch" && typeof toolInput["command"] === "string") {
return {
...toolInput,
input: toolInput["command"],
patch: toolInput["command"],
};
}
return toolInput;
}
function normalizeToolResponse(toolResponse) {
if (typeof toolResponse === "string") {
return [{ type: "text", text: toolResponse }];
}
if (isRecord(toolResponse) && typeof toolResponse["text"] === "string") {
return [{ type: "text", text: toolResponse["text"] }];
}
return [];
}
function isErrorResponse(toolResponse) {
return isRecord(toolResponse) && toolResponse["is_error"] === true;
}
function formatWarnings(warnings) {
return warnings
.map((warning) => `comment-checker found issues in ${warning.filePath}:\n${warning.message}`)
.join("\n\n");
}
function normalizeHookText(value) {
return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
}
function hookFeedbackLimit(transcriptPath) {
return isContextPressureTranscript(transcriptPath)
? CONTEXT_PRESSURE_MAX_HOOK_FEEDBACK_CHARS
: DEFAULT_MAX_HOOK_FEEDBACK_CHARS;
}
function isContextPressureTranscript(transcriptPath) {
if (transcriptPath === null)
return false;
try {
return hasContextPressureMarker(readFileSync(transcriptPath, "utf8"));
}
catch (error) {
if (error instanceof Error)
return false;
throw error;
}
}
function hasContextPressureMarker(text) {
const normalizedText = text.toLowerCase();
return CONTEXT_PRESSURE_MARKERS.some((marker) => normalizedText.includes(marker));
}
function limitHookText(text, maxChars) {
if (text.length <= maxChars)
return text;
const marker = `\n\n[Truncated hook output to ${maxChars} chars to avoid Codex context overflow.]`;
if (marker.length >= maxChars)
return marker.slice(0, maxChars);
const head = text.slice(0, maxChars - marker.length).replace(/[ \t\r\n]+$/, "");
return `${head}${marker}`;
}
function isCodexPostToolUseInput(value) {
return (isRecord(value) &&
value["hook_event_name"] === "PostToolUse" &&
typeof value["session_id"] === "string" &&
typeof value["turn_id"] === "string" &&
(typeof value["transcript_path"] === "string" || value["transcript_path"] === null) &&
typeof value["cwd"] === "string" &&
typeof value["model"] === "string" &&
typeof value["permission_mode"] === "string" &&
typeof value["tool_name"] === "string" &&
isRecord(value["tool_input"]) &&
typeof value["tool_use_id"] === "string");
}
function readStdin() {
return new Promise((resolve, reject) => {
let data = "";
processStdin.setEncoding("utf-8");
processStdin.on("data", (chunk) => {
data += chunk;
});
processStdin.once("error", reject);
processStdin.once("end", () => {
resolve(data);
});
});
}
Loading
Loading