@oh-my-pi/pi-coding-agent 8.12.2 → 8.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "8.12.2",
3
+ "version": "8.12.4",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -58,10 +58,6 @@
58
58
  "types": "./src/internal-urls/*.ts",
59
59
  "import": "./src/internal-urls/*.ts"
60
60
  },
61
- "./vendor/photon/*": {
62
- "types": "./src/vendor/photon/*",
63
- "import": "./src/vendor/photon/*"
64
- },
65
61
  "./*": {
66
62
  "types": "./src/*.ts",
67
63
  "import": "./src/*.ts"
@@ -83,11 +79,12 @@
83
79
  "test": "bun test"
84
80
  },
85
81
  "dependencies": {
86
- "@oh-my-pi/omp-stats": "8.12.2",
87
- "@oh-my-pi/pi-agent-core": "8.12.2",
88
- "@oh-my-pi/pi-ai": "8.12.2",
89
- "@oh-my-pi/pi-tui": "8.12.2",
90
- "@oh-my-pi/pi-utils": "8.12.2",
82
+ "@oh-my-pi/omp-stats": "8.12.4",
83
+ "@oh-my-pi/pi-agent-core": "8.12.4",
84
+ "@oh-my-pi/pi-ai": "8.12.4",
85
+ "@oh-my-pi/pi-natives": "8.12.4",
86
+ "@oh-my-pi/pi-tui": "8.12.4",
87
+ "@oh-my-pi/pi-utils": "8.12.4",
91
88
  "@openai/agents": "^0.4.4",
92
89
  "@sinclair/typebox": "^0.34.48",
93
90
  "ajv": "^8.17.1",
@@ -72,6 +72,13 @@ export interface NotificationSettings {
72
72
  onComplete?: NotificationMethod; // default: "auto"
73
73
  }
74
74
 
75
+ export interface AskSettings {
76
+ /** Timeout in seconds for ask tool selections (0 or null to disable, default: 30) */
77
+ timeout?: number | null;
78
+ /** Notification method when ask tool is waiting for input (default: "auto") */
79
+ notification?: NotificationMethod;
80
+ }
81
+
75
82
  export interface ExaSettings {
76
83
  enabled?: boolean; // default: true (master toggle for all Exa tools)
77
84
  enableSearch?: boolean; // default: true (search, deep, code, crawl)
@@ -242,6 +249,7 @@ export interface Settings {
242
249
  showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
243
250
  normativeRewrite?: boolean; // default: false (rewrite tool call arguments to normalized format in session history)
244
251
  readLineNumbers?: boolean; // default: false (prepend line numbers to read tool output by default)
252
+ ask?: AskSettings;
245
253
  }
246
254
 
247
255
  export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
@@ -307,6 +315,7 @@ const DEFAULT_SETTINGS: Settings = {
307
315
  terminal: { showImages: true },
308
316
  images: { autoResize: true },
309
317
  notifications: { onComplete: "auto" },
318
+ ask: { timeout: 30, notification: "auto" },
310
319
  exa: {
311
320
  enabled: true,
312
321
  enableSearch: true,
@@ -1117,6 +1126,33 @@ export class SettingsManager {
1117
1126
  await this.save();
1118
1127
  }
1119
1128
 
1129
+ /** Get ask tool timeout in milliseconds (0 or null = disabled) */
1130
+ getAskTimeout(): number | null {
1131
+ const timeout = this.settings.ask?.timeout;
1132
+ if (timeout === null || timeout === 0) return null;
1133
+ return (timeout ?? 30) * 1000;
1134
+ }
1135
+
1136
+ async setAskTimeout(seconds: number | null): Promise<void> {
1137
+ if (!this.globalSettings.ask) {
1138
+ this.globalSettings.ask = {};
1139
+ }
1140
+ this.globalSettings.ask.timeout = seconds;
1141
+ await this.save();
1142
+ }
1143
+
1144
+ getAskNotification(): NotificationMethod {
1145
+ return this.settings.ask?.notification ?? "auto";
1146
+ }
1147
+
1148
+ async setAskNotification(method: NotificationMethod): Promise<void> {
1149
+ if (!this.globalSettings.ask) {
1150
+ this.globalSettings.ask = {};
1151
+ }
1152
+ this.globalSettings.ask.notification = method;
1153
+ await this.save();
1154
+ }
1155
+
1120
1156
  getImageAutoResize(): boolean {
1121
1157
  return this.settings.images?.autoResize ?? true;
1122
1158
  }
package/src/config.ts CHANGED
@@ -93,11 +93,6 @@ export function getToolsDir(): string {
93
93
  return path.join(getAgentDir(), "tools");
94
94
  }
95
95
 
96
- /** Get path to managed binaries directory (fd, rg) */
97
- export function getBinDir(): string {
98
- return path.join(getAgentDir(), "bin");
99
- }
100
-
101
96
  /** Get path to slash commands directory */
102
97
  export function getCommandsDir(): string {
103
98
  return path.join(getAgentDir(), "commands");
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Provides unified bash execution for AgentSession.executeBash() and direct calls.
5
5
  */
6
- import { cspawn, Exception, ptree } from "@oh-my-pi/pi-utils";
6
+ import { Exception, ptree } from "@oh-my-pi/pi-utils";
7
7
  import { OutputSink } from "../session/streaming-output";
8
8
  import { getShellConfig } from "../utils/shell";
9
9
  import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
@@ -50,11 +50,12 @@ export async function executeBash(command: string, options?: BashExecutorOptions
50
50
  artifactId: options?.artifactId,
51
51
  });
52
52
 
53
- const child = cspawn([shell, ...args, finalCommand], {
53
+ using child = ptree.spawn([shell, ...args, finalCommand], {
54
54
  cwd: options?.cwd,
55
55
  env: finalEnv,
56
56
  signal: options?.signal,
57
57
  timeout: options?.timeout,
58
+ detached: true,
58
59
  });
59
60
 
60
61
  // Pump streams - errors during abort/timeout are expected
@@ -65,9 +66,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
65
66
 
66
67
  // Wait for process exit
67
68
  try {
68
- await child.exited;
69
69
  return {
70
- exitCode: child.exitCode ?? 0,
70
+ exitCode: await child.exited,
71
71
  cancelled: false,
72
72
  ...(await sink.dump()),
73
73
  };
package/src/exec/exec.ts CHANGED
@@ -35,22 +35,19 @@ export async function execCommand(
35
35
  cwd: string,
36
36
  options?: ExecOptions,
37
37
  ): Promise<ExecResult> {
38
- const proc = ptree.cspawn([command, ...args], {
38
+ const result = await ptree.exec([command, ...args], {
39
39
  cwd,
40
40
  signal: options?.signal,
41
41
  timeout: options?.timeout,
42
+ allowNonZero: true,
43
+ allowAbort: true,
44
+ stderr: "full",
42
45
  });
43
- // Read streams before awaiting exit to avoid data loss if streams close
44
- const [stdoutText, stderrText] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
45
- try {
46
- await proc.exited;
47
- } catch {
48
- // ChildProcess rejects on non-zero exit; we handle it below
49
- }
46
+
50
47
  return {
51
- stdout: stdoutText,
52
- stderr: stderrText,
53
- code: proc.exitCode ?? 0,
54
- killed: proc.exitReason instanceof ptree.AbortError,
48
+ stdout: result.stdout,
49
+ stderr: result.stderr,
50
+ code: result.exitCode ?? 0,
51
+ killed: Boolean(result.exitError?.aborted),
55
52
  };
56
53
  }
@@ -6,8 +6,6 @@ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
6
6
 
7
7
  // Check external tools
8
8
  const tools = [
9
- { name: "fd", description: "File finder" },
10
- { name: "rg", description: "Ripgrep" },
11
9
  { name: "sd", description: "Find-replace" },
12
10
  { name: "sg", description: "AST-grep" },
13
11
  { name: "git", description: "Version control" },
package/src/ipy/kernel.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createServer } from "node:net";
2
2
  import * as path from "node:path";
3
- import { logger } from "@oh-my-pi/pi-utils";
4
- import { $, type Subprocess } from "bun";
3
+ import { logger, ptree } from "@oh-my-pi/pi-utils";
4
+ import { $ } from "bun";
5
5
  import { nanoid } from "nanoid";
6
- import { getShellConfig, killProcessTree } from "../utils/shell";
6
+ import { getShellConfig } from "../utils/shell";
7
7
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
8
8
  import { time } from "../utils/timings";
9
9
  import { htmlToBasicMarkdown } from "../web/scrapers/types";
@@ -452,7 +452,7 @@ export function serializeWebSocketMessage(msg: JupyterMessage): ArrayBuffer {
452
452
  export class PythonKernel {
453
453
  readonly id: string;
454
454
  readonly kernelId: string;
455
- readonly gatewayProcess: Subprocess | null;
455
+ readonly gatewayProcess: ptree.ChildProcess | null;
456
456
  readonly gatewayUrl: string;
457
457
  readonly sessionId: string;
458
458
  readonly username: string;
@@ -469,7 +469,7 @@ export class PythonKernel {
469
469
  private constructor(
470
470
  id: string,
471
471
  kernelId: string,
472
- gatewayProcess: Subprocess | null,
472
+ gatewayProcess: ptree.ChildProcess | null,
473
473
  gatewayUrl: string,
474
474
  sessionId: string,
475
475
  username: string,
@@ -633,14 +633,14 @@ export class PythonKernel {
633
633
  kernelEnv.PYTHONPATH = pythonPathParts;
634
634
  }
635
635
 
636
- let gatewayProcess: Subprocess | null = null;
636
+ let gatewayProcess: ptree.ChildProcess | null = null;
637
637
  let gatewayUrl: string | null = null;
638
638
  let lastError: string | null = null;
639
639
 
640
640
  for (let attempt = 0; attempt < GATEWAY_STARTUP_ATTEMPTS; attempt += 1) {
641
641
  const gatewayPort = await allocatePort();
642
642
  const candidateUrl = `http://127.0.0.1:${gatewayPort}`;
643
- const candidateProcess = Bun.spawn(
643
+ const candidateProcess = ptree.spawn(
644
644
  [
645
645
  runtime.pythonPath,
646
646
  "-m",
@@ -653,10 +653,8 @@ export class PythonKernel {
653
653
  ],
654
654
  {
655
655
  cwd: options.cwd,
656
- stdin: "ignore",
657
- stdout: "pipe",
658
- stderr: "pipe",
659
656
  env: kernelEnv,
657
+ detached: true,
660
658
  },
661
659
  );
662
660
 
@@ -687,7 +685,7 @@ export class PythonKernel {
687
685
 
688
686
  if (gatewayProcess && gatewayUrl) break;
689
687
 
690
- await killProcessTree(candidateProcess.pid);
688
+ candidateProcess.kill();
691
689
  lastError = exited ? "Kernel gateway process exited during startup" : "Kernel gateway failed to start";
692
690
  }
693
691
 
@@ -702,7 +700,7 @@ export class PythonKernel {
702
700
  });
703
701
 
704
702
  if (!createResponse.ok) {
705
- await killProcessTree(gatewayProcess.pid);
703
+ gatewayProcess.kill();
706
704
  throw new Error(`Failed to create kernel: ${await createResponse.text()}`);
707
705
  }
708
706
 
@@ -1118,7 +1116,7 @@ export class PythonKernel {
1118
1116
  await releaseSharedGateway();
1119
1117
  } else if (this.gatewayProcess) {
1120
1118
  try {
1121
- await killProcessTree(this.gatewayProcess.pid);
1119
+ this.gatewayProcess.kill();
1122
1120
  } catch (err: unknown) {
1123
1121
  logger.warn("Failed to terminate gateway process", {
1124
1122
  error: err instanceof Error ? err.message : String(err),
package/src/migrations.ts CHANGED
@@ -5,7 +5,7 @@ import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import { isEnoent, logger } from "@oh-my-pi/pi-utils";
7
7
  import chalk from "chalk";
8
- import { getAgentDbPath, getAgentDir, getBinDir } from "./config";
8
+ import { getAgentDbPath, getAgentDir } from "./config";
9
9
  import { AgentStorage } from "./session/agent-storage";
10
10
  import type { AuthCredential } from "./session/auth-storage";
11
11
 
@@ -144,50 +144,6 @@ export async function migrateSessionsFromAgentRoot(): Promise<void> {
144
144
  }
145
145
  }
146
146
 
147
- /**
148
- * Move fd/rg binaries from tools/ to bin/ if they exist.
149
- */
150
- async function migrateToolsToBin(): Promise<void> {
151
- const agentDir = getAgentDir();
152
- const toolsDir = path.join(agentDir, "tools");
153
- const binDir = getBinDir();
154
-
155
- if (!fs.existsSync(toolsDir)) return;
156
-
157
- const binaries = ["fd", "rg", "fd.exe", "rg.exe"];
158
- let movedAny = false;
159
-
160
- for (const bin of binaries) {
161
- const oldPath = path.join(toolsDir, bin);
162
- const newPath = path.join(binDir, bin);
163
- if (!fs.existsSync(oldPath)) continue;
164
-
165
- if (!fs.existsSync(binDir)) {
166
- await fs.promises.mkdir(binDir, { recursive: true });
167
- }
168
-
169
- if (!fs.existsSync(newPath)) {
170
- try {
171
- await fs.promises.rename(oldPath, newPath);
172
- movedAny = true;
173
- } catch (error) {
174
- logger.warn("Failed to migrate binary", { from: oldPath, to: newPath, error: String(error) });
175
- }
176
- } else {
177
- // Target exists, just delete the old one
178
- try {
179
- await fs.promises.rm(oldPath, { force: true });
180
- } catch {
181
- // Ignore
182
- }
183
- }
184
- }
185
-
186
- if (movedAny) {
187
- console.log(chalk.green(`Migrated managed binaries tools/ → bin/`));
188
- }
189
- }
190
-
191
147
  /**
192
148
  * Run all migrations. Called once on startup.
193
149
  *
@@ -201,7 +157,6 @@ export async function runMigrations(_cwd: string): Promise<{
201
157
  // Then: run data migrations
202
158
  const migratedAuthProviders = await migrateAuthToAgentDb();
203
159
  await migrateSessionsFromAgentRoot();
204
- await migrateToolsToBin();
205
160
 
206
161
  return { migratedAuthProviders, deprecationWarnings: [] };
207
162
  }
@@ -189,6 +189,29 @@ export const SETTINGS_DEFS: SettingDef[] = [
189
189
  get: sm => sm.getNotificationOnComplete(),
190
190
  set: (sm, v) => sm.setNotificationOnComplete(v as NotificationMethod),
191
191
  },
192
+ {
193
+ id: "askTimeout",
194
+ tab: "behavior",
195
+ type: "enum",
196
+ label: "Ask tool timeout",
197
+ description: "Auto-select recommended option after timeout (disabled in plan mode)",
198
+ values: ["off", "15", "30", "60", "120"],
199
+ get: sm => {
200
+ const timeout = sm.getAskTimeout();
201
+ return timeout === null ? "off" : String(timeout / 1000);
202
+ },
203
+ set: (sm, v) => sm.setAskTimeout(v === "off" ? null : Number.parseInt(v, 10)),
204
+ },
205
+ {
206
+ id: "askNotification",
207
+ tab: "behavior",
208
+ type: "enum",
209
+ label: "Ask notification",
210
+ description: "Notify when ask tool is waiting for input",
211
+ values: ["auto", "bell", "osc99", "osc9", "off"],
212
+ get: sm => sm.getAskNotification(),
213
+ set: (sm, v) => sm.setAskNotification(v as NotificationMethod),
214
+ },
192
215
  {
193
216
  id: "startupQuiet",
194
217
  tab: "behavior",
@@ -112,7 +112,7 @@ export class RpcClient {
112
112
  args.push(...this.options.args);
113
113
  }
114
114
 
115
- this.process = ptree.cspawn(["bun", cliPath, ...args], {
115
+ this.process = ptree.spawn(["bun", cliPath, ...args], {
116
116
  cwd: this.options.cwd,
117
117
  env: { ...process.env, ...this.options.env },
118
118
  stdin: "pipe",
@@ -140,7 +140,7 @@ export class RpcClient {
140
140
  await Bun.sleep(100);
141
141
 
142
142
  try {
143
- const exitCode = await Promise.race([this.process.exited, Bun.sleep(50).then(() => null)]);
143
+ const exitCode = await Promise.race([this.process.exited, Bun.sleep(500).then(() => null)]);
144
144
  if (exitCode !== null) {
145
145
  throw new Error(
146
146
  `Agent process exited immediately with code ${exitCode}. Stderr: ${this.process.peekStderr()}`,
@@ -154,11 +154,11 @@ export class RpcClient {
154
154
  /**
155
155
  * Stop the RPC agent process.
156
156
  */
157
- async stop(): Promise<void> {
157
+ stop() {
158
158
  if (!this.process) return;
159
159
 
160
160
  this.lineReader?.cancel();
161
- await this.process.killAndWait();
161
+ this.process.kill();
162
162
 
163
163
  this.process = null;
164
164
  this.lineReader = null;
@@ -133,6 +133,14 @@ export function calculateContextTokens(usage: Usage): number {
133
133
  return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
134
134
  }
135
135
 
136
+ export function calculatePromptTokens(usage: Usage): number {
137
+ const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
138
+ if (promptTokens > 0) {
139
+ return promptTokens;
140
+ }
141
+ return calculateContextTokens(usage);
142
+ }
143
+
136
144
  /**
137
145
  * Get usage from an assistant message if available.
138
146
  * Skips aborted and error messages as they don't have valid usage data.
@@ -237,6 +245,17 @@ export function estimateTokens(message: AgentMessage): number {
237
245
  return 0;
238
246
  }
239
247
 
248
+ function estimateEntriesTokens(entries: SessionEntry[], startIndex: number, endIndex: number): number {
249
+ let total = 0;
250
+ for (let i = startIndex; i < endIndex; i++) {
251
+ const msg = getMessageFromEntry(entries[i]);
252
+ if (msg) {
253
+ total += estimateTokens(msg);
254
+ }
255
+ }
256
+ return total;
257
+ }
258
+
240
259
  /**
241
260
  * Find valid cut points: indices of user, assistant, custom, or bashExecution messages.
242
261
  * Never cut at tool results (they must follow their tool call).
@@ -616,8 +635,17 @@ export function prepareCompaction(
616
635
 
617
636
  const lastUsage = getLastAssistantUsage(pathEntries);
618
637
  const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;
638
+ let keepRecentTokens = settings.keepRecentTokens;
639
+ if (lastUsage) {
640
+ const estimatedTokens = estimateEntriesTokens(pathEntries, boundaryStart, boundaryEnd);
641
+ const promptTokens = calculatePromptTokens(lastUsage);
642
+ const ratio = estimatedTokens > 0 ? promptTokens / estimatedTokens : 0;
643
+ if (Number.isFinite(ratio) && ratio > 1) {
644
+ keepRecentTokens = Math.max(1, Math.floor(keepRecentTokens / ratio));
645
+ }
646
+ }
619
647
 
620
- const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
648
+ const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, keepRecentTokens);
621
649
 
622
650
  // Get UUID of first kept entry
623
651
  const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];
@@ -742,8 +770,8 @@ export async function compact(
742
770
  ]);
743
771
  // Merge into single summary
744
772
  summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
745
- } else {
746
- // Just generate history summary
773
+ } else if (messagesToSummarize.length > 0) {
774
+ // Generate history summary from messages to summarize
747
775
  summary = await generateSummary(
748
776
  messagesToSummarize,
749
777
  model,
@@ -754,6 +782,12 @@ export async function compact(
754
782
  previousSummary,
755
783
  summaryOptions,
756
784
  );
785
+ } else if (previousSummary) {
786
+ // No new messages to summarize, preserve previous summary
787
+ summary = previousSummary;
788
+ } else {
789
+ // No messages and no previous summary
790
+ summary = "No prior history.";
757
791
  }
758
792
 
759
793
  const shortSummary = await generateShortSummary(
@@ -1,4 +1,4 @@
1
- import { cspawn, logger, ptree } from "@oh-my-pi/pi-utils";
1
+ import { logger, ptree } from "@oh-my-pi/pi-utils";
2
2
  import { OutputSink } from "../session/streaming-output";
3
3
  import { buildRemoteCommand, ensureConnection, ensureHostInfo, type SSHConnectionTarget } from "./connection-manager";
4
4
  import { hasSshfs, mountRemote } from "./sshfs-mount";
@@ -76,7 +76,7 @@ export async function executeSSH(
76
76
  }
77
77
  }
78
78
 
79
- const child = cspawn(["ssh", ...(await buildRemoteCommand(host, resolvedCommand))], {
79
+ using child = ptree.spawn(["ssh", ...(await buildRemoteCommand(host, resolvedCommand))], {
80
80
  signal: options?.signal,
81
81
  timeout: options?.timeout,
82
82
  });
@@ -92,10 +92,8 @@ export async function executeSSH(
92
92
  );
93
93
 
94
94
  try {
95
- await child.exited;
96
- const exitCode = child.exitCode ?? 0;
97
95
  return {
98
- exitCode,
96
+ exitCode: await child.exited,
99
97
  cancelled: false,
100
98
  ...(await sink.dump()),
101
99
  };
@@ -4,7 +4,8 @@
4
4
  import * as fs from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
- import { globPaths } from "@oh-my-pi/pi-utils";
7
+ import { find as wasmFind } from "@oh-my-pi/pi-natives";
8
+ import { untilAborted } from "@oh-my-pi/pi-utils";
8
9
  import { $ } from "bun";
9
10
  import chalk from "chalk";
10
11
  import { contextFileCapability } from "./capability/context-file";
@@ -201,17 +202,21 @@ type ProjectTreeScan = {
201
202
  const GLOB_TIMEOUT_MS = 5000;
202
203
 
203
204
  /**
204
- * Scan project tree using fs.promises.glob with exclusion filters.
205
- * Returns null if glob fails.
205
+ * Scan project tree using ripgrep-wasm find with exclusion filters.
206
+ * Returns null if scan fails.
206
207
  */
207
208
  async function scanProjectTreeWithGlob(root: string): Promise<ProjectTreeScan | null> {
208
209
  let entries: string[];
210
+ const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
209
211
  try {
210
- entries = await globPaths("**/*", {
211
- cwd: root,
212
- gitignore: true,
213
- timeoutMs: GLOB_TIMEOUT_MS,
214
- });
212
+ const result = await untilAborted(timeoutSignal, () =>
213
+ wasmFind({
214
+ pattern: "**/*",
215
+ path: root,
216
+ fileType: "file",
217
+ }),
218
+ );
219
+ entries = result.matches.map(match => match.path).filter(entry => entry.length > 0);
215
220
  } catch {
216
221
  return null;
217
222
  }
@@ -222,7 +227,7 @@ async function scanProjectTreeWithGlob(root: string): Promise<ProjectTreeScan |
222
227
  dirContents.set(root, new Map());
223
228
 
224
229
  for (const entry of entries) {
225
- const filePath = entry.trim();
230
+ const filePath = entry;
226
231
  if (!filePath) continue;
227
232
  const absolutePath = path.join(root, filePath);
228
233
  // Check static ignores on path components