refactor: remove remark-stringify and enhance task ID matching in TeamMemberLogsFinder
- Removed `remark-stringify` from the project to avoid ESM→CJS interop issues in Electron's main process. - Updated the text formatting pipeline to use a custom plain-text compiler instead of `remark-stringify`. - Enhanced task ID matching logic in `TeamMemberLogsFinder` to accommodate both full UUIDs and their short display forms, improving flexibility in task identification. - Added comments to clarify the changes in task ID handling and regex construction.
This commit is contained in:
parent
a685ae3e6c
commit
b14cef5f09
5 changed files with 74 additions and 15 deletions
|
|
@ -147,7 +147,6 @@
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-stringify": "^11.0.0",
|
|
||||||
"simple-git": "^3.32.3",
|
"simple-git": "^3.32.3",
|
||||||
"ssh-config": "^5.0.4",
|
"ssh-config": "^5.0.4",
|
||||||
"ssh2": "^1.17.0",
|
"ssh2": "^1.17.0",
|
||||||
|
|
|
||||||
|
|
@ -254,9 +254,6 @@ importers:
|
||||||
remark-parse:
|
remark-parse:
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.0.0
|
version: 11.0.0
|
||||||
remark-stringify:
|
|
||||||
specifier: ^11.0.0
|
|
||||||
version: 11.0.0
|
|
||||||
simple-git:
|
simple-git:
|
||||||
specifier: ^3.32.3
|
specifier: ^3.32.3
|
||||||
version: 3.32.3
|
version: 3.32.3
|
||||||
|
|
|
||||||
|
|
@ -649,8 +649,17 @@ export class TeamMemberLogsFinder {
|
||||||
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
||||||
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
||||||
|
|
||||||
const escapedTaskId = taskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const trimmedId = taskId.trim();
|
||||||
const pattern = new RegExp(`"taskId"\\s*:\\s*"${escapedTaskId}"`);
|
// CLI agents may use displayId (first 8 chars of UUID) in tool inputs.
|
||||||
|
// Build regex that matches either form.
|
||||||
|
const displayId =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmedId)
|
||||||
|
? trimmedId.slice(0, 8).toLowerCase()
|
||||||
|
: null;
|
||||||
|
const idAlternation = displayId
|
||||||
|
? `(?:${trimmedId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${displayId})`
|
||||||
|
: trimmedId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const pattern = new RegExp(`"taskId"\\s*:\\s*"${idAlternation}"`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const line of rl) {
|
for await (const line of rl) {
|
||||||
|
|
@ -926,6 +935,17 @@ export class TeamMemberLogsFinder {
|
||||||
const teamLower = teamName.trim().toLowerCase();
|
const teamLower = teamName.trim().toLowerCase();
|
||||||
const taskIdStr = taskId.trim();
|
const taskIdStr = taskId.trim();
|
||||||
|
|
||||||
|
// CLI agents often use the short displayId (first 8 chars of UUID) in tool inputs,
|
||||||
|
// while the UI passes the full UUID. Match both forms to bridge this gap.
|
||||||
|
const taskIdDisplayForm =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(taskIdStr)
|
||||||
|
? taskIdStr.slice(0, 8).toLowerCase()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const matchesTaskId = (candidate: string): boolean =>
|
||||||
|
candidate === taskIdStr ||
|
||||||
|
(taskIdDisplayForm !== null && candidate.toLowerCase() === taskIdDisplayForm);
|
||||||
|
|
||||||
const extractTaskIdFromUnknown = (raw: unknown): string | null => {
|
const extractTaskIdFromUnknown = (raw: unknown): string | null => {
|
||||||
if (typeof raw === 'string') return raw.trim();
|
if (typeof raw === 'string') return raw.trim();
|
||||||
if (typeof raw === 'number' && Number.isFinite(raw)) return String(raw);
|
if (typeof raw === 'number' && Number.isFinite(raw)) return String(raw);
|
||||||
|
|
@ -1045,7 +1065,7 @@ export class TeamMemberLogsFinder {
|
||||||
const inputTeam = extractTeamFromInput(input);
|
const inputTeam = extractTeamFromInput(input);
|
||||||
const rawTaskId = input.taskId ?? input.task_id;
|
const rawTaskId = input.taskId ?? input.task_id;
|
||||||
const inputTaskId = extractTaskIdFromUnknown(rawTaskId);
|
const inputTaskId = extractTaskIdFromUnknown(rawTaskId);
|
||||||
if (inputTaskId && inputTaskId === taskIdStr) {
|
if (inputTaskId && matchesTaskId(inputTaskId)) {
|
||||||
// If team is present in the input, require exact match.
|
// If team is present in the input, require exact match.
|
||||||
if (inputTeam) {
|
if (inputTeam) {
|
||||||
if (inputTeam.toLowerCase() === teamLower) {
|
if (inputTeam.toLowerCase() === teamLower) {
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,21 @@ function quoteArg(arg: string): string {
|
||||||
return arg;
|
return arg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Env vars injected into every spawned Claude CLI process. */
|
||||||
|
const CLI_ENV_DEFAULTS: Record<string, string> = {
|
||||||
|
CLAUDE_HOOK_JUDGE_MODE: 'true',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Merge CLI_ENV_DEFAULTS into spawn/exec options.env (or process.env if absent). */
|
||||||
|
function withCliEnv<T extends { env?: NodeJS.ProcessEnv | Record<string, string | undefined> }>(
|
||||||
|
options: T
|
||||||
|
): T {
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
env: { ...(options.env ?? process.env), ...CLI_ENV_DEFAULTS },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a CLI binary, falling back to running the command through a
|
* Execute a CLI binary, falling back to running the command through a
|
||||||
* shell on Windows if the normal path-based spawn fails. `binaryPath`
|
* shell on Windows if the normal path-based spawn fails. `binaryPath`
|
||||||
|
|
@ -103,11 +118,12 @@ export async function execCli(
|
||||||
options: ExecFileOptions = {}
|
options: ExecFileOptions = {}
|
||||||
): Promise<{ stdout: string; stderr: string }> {
|
): Promise<{ stdout: string; stderr: string }> {
|
||||||
const target = binaryPath || 'claude';
|
const target = binaryPath || 'claude';
|
||||||
|
const opts = withCliEnv(options);
|
||||||
|
|
||||||
// attempt the normal execFile path first
|
// attempt the normal execFile path first
|
||||||
if (!needsShell(target)) {
|
if (!needsShell(target)) {
|
||||||
try {
|
try {
|
||||||
const result = await execFileAsync(target, args, options);
|
const result = await execFileAsync(target, args, opts);
|
||||||
return { stdout: String(result.stdout), stderr: String(result.stderr) };
|
return { stdout: String(result.stdout), stderr: String(result.stderr) };
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
// fall through to shell fallback only when the error matches the
|
// fall through to shell fallback only when the error matches the
|
||||||
|
|
@ -124,7 +140,7 @@ export async function execCli(
|
||||||
|
|
||||||
// shell fallback (Windows only; others shouldn't reach here)
|
// shell fallback (Windows only; others shouldn't reach here)
|
||||||
const cmd = [target, ...args].map(quoteArg).join(' ');
|
const cmd = [target, ...args].map(quoteArg).join(' ');
|
||||||
const shellResult = await execShellAsync(cmd, options as unknown as ExecOptions);
|
const shellResult = await execShellAsync(cmd, opts as unknown as ExecOptions);
|
||||||
return { stdout: String(shellResult.stdout), stderr: String(shellResult.stderr) };
|
return { stdout: String(shellResult.stdout), stderr: String(shellResult.stderr) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,21 +155,23 @@ export function spawnCli(
|
||||||
args: string[],
|
args: string[],
|
||||||
options: SpawnOptions = {}
|
options: SpawnOptions = {}
|
||||||
): ReturnType<typeof spawn> {
|
): ReturnType<typeof spawn> {
|
||||||
|
const opts = withCliEnv(options);
|
||||||
|
|
||||||
if (process.platform === 'win32' && needsShell(binaryPath)) {
|
if (process.platform === 'win32' && needsShell(binaryPath)) {
|
||||||
const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
|
const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
|
||||||
// eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
|
// eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
|
||||||
return spawn(cmd, { ...options, shell: true });
|
return spawn(cmd, { ...opts, shell: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return spawn(binaryPath, args, options);
|
return spawn(binaryPath, args, opts);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const code =
|
const code =
|
||||||
err && typeof err === 'object' && 'code' in err ? (err as { code?: string }).code : undefined;
|
err && typeof err === 'object' && 'code' in err ? (err as { code?: string }).code : undefined;
|
||||||
if (process.platform === 'win32' && code === 'EINVAL') {
|
if (process.platform === 'win32' && code === 'EINVAL') {
|
||||||
const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
|
const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
|
||||||
// eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
|
// eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
|
||||||
return spawn(cmd, { ...options, shell: true });
|
return spawn(cmd, { ...opts, shell: true });
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,41 @@
|
||||||
import remarkParse from 'remark-parse';
|
import remarkParse from 'remark-parse';
|
||||||
import remarkStringify from 'remark-stringify';
|
|
||||||
import stripMarkdownPlugin from 'strip-markdown';
|
import stripMarkdownPlugin from 'strip-markdown';
|
||||||
import { unified } from 'unified';
|
import { unified } from 'unified';
|
||||||
|
|
||||||
const processor = unified().use(remarkParse).use(stripMarkdownPlugin).use(remarkStringify);
|
/**
|
||||||
|
* Minimal plain-text compiler for unified.
|
||||||
|
* After strip-markdown, the MDAST contains only text nodes —
|
||||||
|
* this compiler simply concatenates their values.
|
||||||
|
*
|
||||||
|
* Replaces remark-stringify to avoid ESM→CJS interop issues
|
||||||
|
* in Electron's main process (CJS output format).
|
||||||
|
*/
|
||||||
|
function plainTextCompiler(this: ReturnType<typeof unified>) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(this as any).compiler = (tree: any): string => {
|
||||||
|
const parts: string[] = [];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
function visit(node: any): void {
|
||||||
|
if ('value' in node && typeof node.value === 'string') {
|
||||||
|
parts.push(node.value);
|
||||||
|
}
|
||||||
|
if ('children' in node && Array.isArray(node.children)) {
|
||||||
|
node.children.forEach(visit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(tree);
|
||||||
|
return parts.join('');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const processor = unified().use(remarkParse).use(stripMarkdownPlugin).use(plainTextCompiler);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strips markdown formatting from text for use in plain-text contexts
|
* Strips markdown formatting from text for use in plain-text contexts
|
||||||
* like native OS notifications.
|
* like native OS notifications.
|
||||||
*
|
*
|
||||||
* Uses remark ecosystem (strip-markdown plugin) for reliable parsing.
|
* Uses remark ecosystem (strip-markdown plugin) for reliable parsing.
|
||||||
* Pipeline: remarkParse → stripMarkdown (transform) → remarkStringify (compile to plain text).
|
* Pipeline: remarkParse → stripMarkdown (transform) → plainTextCompiler (extract text).
|
||||||
*/
|
*/
|
||||||
export function stripMarkdown(text: string): string {
|
export function stripMarkdown(text: string): string {
|
||||||
const result = processor.processSync(text);
|
const result = processor.processSync(text);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue