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:
iliya 2026-03-15 21:05:51 +02:00
parent a685ae3e6c
commit b14cef5f09
5 changed files with 74 additions and 15 deletions

View file

@ -147,7 +147,6 @@
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"simple-git": "^3.32.3",
"ssh-config": "^5.0.4",
"ssh2": "^1.17.0",

View file

@ -254,9 +254,6 @@ importers:
remark-parse:
specifier: ^11.0.0
version: 11.0.0
remark-stringify:
specifier: ^11.0.0
version: 11.0.0
simple-git:
specifier: ^3.32.3
version: 3.32.3

View file

@ -649,8 +649,17 @@ export class TeamMemberLogsFinder {
const stream = createReadStream(filePath, { encoding: 'utf8' });
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
const escapedTaskId = taskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(`"taskId"\\s*:\\s*"${escapedTaskId}"`);
const trimmedId = taskId.trim();
// 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 {
for await (const line of rl) {
@ -926,6 +935,17 @@ export class TeamMemberLogsFinder {
const teamLower = teamName.trim().toLowerCase();
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 => {
if (typeof raw === 'string') return raw.trim();
if (typeof raw === 'number' && Number.isFinite(raw)) return String(raw);
@ -1045,7 +1065,7 @@ export class TeamMemberLogsFinder {
const inputTeam = extractTeamFromInput(input);
const rawTaskId = input.taskId ?? input.task_id;
const inputTaskId = extractTaskIdFromUnknown(rawTaskId);
if (inputTaskId && inputTaskId === taskIdStr) {
if (inputTaskId && matchesTaskId(inputTaskId)) {
// If team is present in the input, require exact match.
if (inputTeam) {
if (inputTeam.toLowerCase() === teamLower) {

View file

@ -89,6 +89,21 @@ function quoteArg(arg: string): string {
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
* shell on Windows if the normal path-based spawn fails. `binaryPath`
@ -103,11 +118,12 @@ export async function execCli(
options: ExecFileOptions = {}
): Promise<{ stdout: string; stderr: string }> {
const target = binaryPath || 'claude';
const opts = withCliEnv(options);
// attempt the normal execFile path first
if (!needsShell(target)) {
try {
const result = await execFileAsync(target, args, options);
const result = await execFileAsync(target, args, opts);
return { stdout: String(result.stdout), stderr: String(result.stderr) };
} catch (err: unknown) {
// 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)
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) };
}
@ -139,21 +155,23 @@ export function spawnCli(
args: string[],
options: SpawnOptions = {}
): ReturnType<typeof spawn> {
const opts = withCliEnv(options);
if (process.platform === 'win32' && needsShell(binaryPath)) {
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)
return spawn(cmd, { ...options, shell: true });
return spawn(cmd, { ...opts, shell: true });
}
try {
return spawn(binaryPath, args, options);
return spawn(binaryPath, args, opts);
} catch (err: unknown) {
const code =
err && typeof err === 'object' && 'code' in err ? (err as { code?: string }).code : undefined;
if (process.platform === 'win32' && code === 'EINVAL') {
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)
return spawn(cmd, { ...options, shell: true });
return spawn(cmd, { ...opts, shell: true });
}
throw err;
}

View file

@ -1,16 +1,41 @@
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';
import stripMarkdownPlugin from 'strip-markdown';
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 ESMCJS 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
* like native OS notifications.
*
* 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 {
const result = processor.processSync(text);