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",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 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
|
||||
* 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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue