@oh-my-pi/pi-coding-agent 13.19.0 → 14.0.3

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.
Files changed (205) hide show
  1. package/CHANGELOG.md +277 -2
  2. package/package.json +86 -20
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +91 -0
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +83 -125
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/model-registry.ts +17 -3
  38. package/src/config/prompt-templates.ts +44 -226
  39. package/src/config/resolve-config-value.ts +4 -2
  40. package/src/config/settings-schema.ts +54 -2
  41. package/src/config/settings.ts +25 -26
  42. package/src/dap/client.ts +674 -0
  43. package/src/dap/config.ts +150 -0
  44. package/src/dap/defaults.json +211 -0
  45. package/src/dap/index.ts +4 -0
  46. package/src/dap/session.ts +1255 -0
  47. package/src/dap/types.ts +600 -0
  48. package/src/debug/log-viewer.ts +3 -2
  49. package/src/discovery/builtin.ts +1 -2
  50. package/src/discovery/codex.ts +2 -2
  51. package/src/discovery/github.ts +2 -1
  52. package/src/discovery/helpers.ts +2 -2
  53. package/src/discovery/opencode.ts +2 -2
  54. package/src/edit/diff.ts +818 -0
  55. package/src/edit/index.ts +309 -0
  56. package/src/edit/line-hash.ts +67 -0
  57. package/src/edit/modes/chunk.ts +454 -0
  58. package/src/{patch → edit/modes}/hashline.ts +741 -361
  59. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  60. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  61. package/src/{patch → edit}/normalize.ts +97 -76
  62. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  63. package/src/exec/bash-executor.ts +4 -2
  64. package/src/exec/idle-timeout-watchdog.ts +126 -0
  65. package/src/exec/non-interactive-env.ts +5 -0
  66. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  67. package/src/extensibility/custom-commands/bundled/review/index.ts +36 -15
  68. package/src/extensibility/custom-commands/loader.ts +1 -2
  69. package/src/extensibility/custom-tools/loader.ts +34 -11
  70. package/src/extensibility/extensions/loader.ts +9 -4
  71. package/src/extensibility/extensions/runner.ts +24 -1
  72. package/src/extensibility/extensions/types.ts +1 -1
  73. package/src/extensibility/hooks/loader.ts +5 -6
  74. package/src/extensibility/hooks/types.ts +1 -1
  75. package/src/extensibility/plugins/doctor.ts +2 -1
  76. package/src/extensibility/slash-commands.ts +3 -7
  77. package/src/index.ts +2 -1
  78. package/src/internal-urls/docs-index.generated.ts +11 -11
  79. package/src/ipy/executor.ts +58 -17
  80. package/src/ipy/gateway-coordinator.ts +6 -4
  81. package/src/ipy/kernel.ts +45 -22
  82. package/src/ipy/runtime.ts +2 -2
  83. package/src/lsp/client.ts +7 -4
  84. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  85. package/src/lsp/config.ts +20 -4
  86. package/src/lsp/defaults.json +688 -154
  87. package/src/lsp/index.ts +234 -45
  88. package/src/lsp/lspmux.ts +2 -2
  89. package/src/lsp/startup-events.ts +13 -0
  90. package/src/lsp/types.ts +12 -1
  91. package/src/lsp/utils.ts +8 -1
  92. package/src/main.ts +102 -46
  93. package/src/memories/index.ts +4 -5
  94. package/src/modes/acp/acp-agent.ts +563 -163
  95. package/src/modes/acp/acp-event-mapper.ts +9 -1
  96. package/src/modes/acp/acp-mode.ts +4 -2
  97. package/src/modes/components/agent-dashboard.ts +3 -4
  98. package/src/modes/components/diff.ts +6 -7
  99. package/src/modes/components/read-tool-group.ts +6 -12
  100. package/src/modes/components/session-observer-overlay.ts +21 -12
  101. package/src/modes/components/settings-defs.ts +5 -0
  102. package/src/modes/components/tool-execution.ts +1 -1
  103. package/src/modes/components/welcome.ts +1 -1
  104. package/src/modes/controllers/btw-controller.ts +2 -2
  105. package/src/modes/controllers/command-controller.ts +3 -2
  106. package/src/modes/controllers/input-controller.ts +12 -8
  107. package/src/modes/index.ts +20 -2
  108. package/src/modes/interactive-mode.ts +94 -37
  109. package/src/modes/rpc/host-tools.ts +186 -0
  110. package/src/modes/rpc/rpc-client.ts +178 -13
  111. package/src/modes/rpc/rpc-mode.ts +73 -3
  112. package/src/modes/rpc/rpc-types.ts +53 -1
  113. package/src/modes/theme/theme.ts +80 -8
  114. package/src/modes/types.ts +2 -2
  115. package/src/prompts/review-request.md +6 -0
  116. package/src/prompts/system/system-prompt.md +2 -1
  117. package/src/prompts/tools/chunk-edit.md +223 -0
  118. package/src/prompts/tools/debug.md +43 -0
  119. package/src/prompts/tools/grep.md +3 -0
  120. package/src/prompts/tools/lsp.md +5 -5
  121. package/src/prompts/tools/read-chunk.md +17 -0
  122. package/src/prompts/tools/read.md +19 -5
  123. package/src/sdk.ts +190 -154
  124. package/src/secrets/obfuscator.ts +1 -1
  125. package/src/session/agent-session.ts +306 -256
  126. package/src/session/agent-storage.ts +12 -12
  127. package/src/session/compaction/branch-summarization.ts +3 -3
  128. package/src/session/compaction/compaction.ts +5 -6
  129. package/src/session/compaction/utils.ts +3 -3
  130. package/src/session/history-storage.ts +62 -19
  131. package/src/session/messages.ts +3 -3
  132. package/src/session/session-dump-format.ts +203 -0
  133. package/src/session/session-storage.ts +4 -2
  134. package/src/session/streaming-output.ts +1 -1
  135. package/src/session/tool-choice-queue.ts +213 -0
  136. package/src/slash-commands/builtin-registry.ts +56 -8
  137. package/src/ssh/connection-manager.ts +2 -2
  138. package/src/ssh/sshfs-mount.ts +5 -5
  139. package/src/stt/downloader.ts +4 -4
  140. package/src/stt/recorder.ts +4 -4
  141. package/src/stt/transcriber.ts +2 -2
  142. package/src/system-prompt.ts +21 -13
  143. package/src/task/agents.ts +5 -6
  144. package/src/task/commands.ts +2 -5
  145. package/src/task/executor.ts +4 -4
  146. package/src/task/index.ts +3 -4
  147. package/src/task/template.ts +2 -2
  148. package/src/task/worktree.ts +4 -4
  149. package/src/tools/ask.ts +2 -3
  150. package/src/tools/ast-edit.ts +7 -7
  151. package/src/tools/ast-grep.ts +7 -7
  152. package/src/tools/auto-generated-guard.ts +36 -41
  153. package/src/tools/await-tool.ts +2 -2
  154. package/src/tools/bash.ts +5 -23
  155. package/src/tools/browser.ts +4 -5
  156. package/src/tools/calculator.ts +2 -3
  157. package/src/tools/cancel-job.ts +2 -2
  158. package/src/tools/checkpoint.ts +3 -3
  159. package/src/tools/debug.ts +1007 -0
  160. package/src/tools/exit-plan-mode.ts +2 -3
  161. package/src/tools/fetch.ts +67 -3
  162. package/src/tools/find.ts +4 -5
  163. package/src/tools/fs-cache-invalidation.ts +5 -0
  164. package/src/tools/gemini-image.ts +13 -5
  165. package/src/tools/gh.ts +10 -11
  166. package/src/tools/grep.ts +57 -9
  167. package/src/tools/index.ts +44 -22
  168. package/src/tools/inspect-image.ts +4 -4
  169. package/src/tools/output-meta.ts +1 -1
  170. package/src/tools/python.ts +19 -6
  171. package/src/tools/read.ts +198 -67
  172. package/src/tools/render-mermaid.ts +2 -3
  173. package/src/tools/render-utils.ts +20 -6
  174. package/src/tools/renderers.ts +3 -1
  175. package/src/tools/report-tool-issue.ts +80 -0
  176. package/src/tools/resolve.ts +70 -39
  177. package/src/tools/search-tool-bm25.ts +2 -2
  178. package/src/tools/ssh.ts +2 -2
  179. package/src/tools/todo-write.ts +2 -2
  180. package/src/tools/tool-timeouts.ts +1 -0
  181. package/src/tools/write.ts +5 -6
  182. package/src/tui/tree-list.ts +3 -1
  183. package/src/utils/clipboard.ts +80 -0
  184. package/src/utils/commit-message-generator.ts +2 -3
  185. package/src/utils/edit-mode.ts +49 -0
  186. package/src/utils/file-display-mode.ts +6 -5
  187. package/src/utils/file-mentions.ts +8 -7
  188. package/src/utils/git.ts +4 -4
  189. package/src/utils/image-loading.ts +98 -0
  190. package/src/utils/title-generator.ts +2 -3
  191. package/src/utils/tools-manager.ts +6 -6
  192. package/src/web/scrapers/choosealicense.ts +1 -1
  193. package/src/web/search/index.ts +3 -3
  194. package/src/autoresearch/command-initialize.md +0 -34
  195. package/src/patch/diff.ts +0 -433
  196. package/src/patch/index.ts +0 -888
  197. package/src/patch/parser.ts +0 -532
  198. package/src/patch/types.ts +0 -292
  199. package/src/prompts/agents/oracle.md +0 -77
  200. package/src/tools/pending-action.ts +0 -49
  201. package/src/utils/child-process.ts +0 -88
  202. package/src/utils/frontmatter.ts +0 -117
  203. package/src/utils/image-input.ts +0 -274
  204. package/src/utils/mime.ts +0 -53
  205. package/src/utils/prompt-format.ts +0 -170
@@ -299,6 +299,48 @@ async function writePreludeCache(state: PreludeCacheState, helpers: PreludeHelpe
299
299
  }
300
300
  }
301
301
 
302
+ function isPythonTestEnvironment(): boolean {
303
+ return Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
304
+ }
305
+
306
+ function getPreludeIntrospectionOptions(
307
+ options: KernelSessionExecutionOptions = {},
308
+ ): Pick<KernelExecuteOptions, "signal" | "timeoutMs"> {
309
+ return {
310
+ signal: options.signal,
311
+ timeoutMs: requireRemainingTimeoutMs(options.deadlineMs),
312
+ };
313
+ }
314
+
315
+ async function cachePreludeDocs(
316
+ cwd: string,
317
+ docs: PreludeHelper[],
318
+ cacheState?: PreludeCacheState | null,
319
+ ): Promise<PreludeHelper[]> {
320
+ cachedPreludeDocs = docs;
321
+ if (!isPythonTestEnvironment() && docs.length > 0) {
322
+ const state = cacheState ?? (await buildPreludeCacheState(cwd));
323
+ await writePreludeCache(state, docs);
324
+ }
325
+ return docs;
326
+ }
327
+
328
+ async function ensurePreludeDocsLoaded(
329
+ kernel: PythonKernel,
330
+ cwd: string,
331
+ options: KernelSessionExecutionOptions = {},
332
+ cacheState?: PreludeCacheState | null,
333
+ ): Promise<PreludeHelper[]> {
334
+ if (cachedPreludeDocs && cachedPreludeDocs.length > 0) {
335
+ return cachedPreludeDocs;
336
+ }
337
+ const docs = await kernel.introspectPrelude(getPreludeIntrospectionOptions(options));
338
+ if (docs.length === 0) {
339
+ throw new Error("Python prelude helpers unavailable");
340
+ }
341
+ return cachePreludeDocs(cwd, docs, cacheState);
342
+ }
343
+
302
344
  function startCleanupTimer(): void {
303
345
  if (cleanupTimer) return;
304
346
  cleanupTimer = setInterval(() => {
@@ -366,16 +408,15 @@ export async function warmPythonEnvironment(
366
408
  useSharedGateway?: boolean,
367
409
  sessionFile?: string,
368
410
  ): Promise<{ ok: boolean; reason?: string; docs: PreludeHelper[] }> {
369
- const isTestEnv = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
370
411
  let cacheState: PreludeCacheState | null = null;
371
412
  try {
372
- await logger.timeAsync("warmPython:ensureKernelAvailable", () => ensureKernelAvailable(cwd));
413
+ await logger.time("warmPython:ensureKernelAvailable", ensureKernelAvailable, cwd);
373
414
  } catch (err: unknown) {
374
415
  const reason = err instanceof Error ? err.message : String(err);
375
416
  cachedPreludeDocs = [];
376
417
  return { ok: false, reason, docs: [] };
377
418
  }
378
- if (!isTestEnv) {
419
+ if (!isPythonTestEnvironment()) {
379
420
  try {
380
421
  cacheState = await buildPreludeCacheState(cwd);
381
422
  const cached = await readPreludeCache(cacheState);
@@ -393,17 +434,17 @@ export async function warmPythonEnvironment(
393
434
  }
394
435
  const resolvedSessionId = sessionId ?? `session:${cwd}`;
395
436
  try {
396
- const docs = await logger.timeAsync("warmPython:withKernelSession", () =>
397
- withKernelSession(resolvedSessionId, cwd, async kernel => kernel.introspectPrelude(), {
437
+ const docs = await logger.time(
438
+ "warmPython:withKernelSession",
439
+ withKernelSession,
440
+ resolvedSessionId,
441
+ cwd,
442
+ kernel => ensurePreludeDocsLoaded(kernel, cwd, { useSharedGateway, sessionFile }, cacheState),
443
+ {
398
444
  useSharedGateway,
399
445
  sessionFile,
400
- }),
446
+ },
401
447
  );
402
- cachedPreludeDocs = docs;
403
- if (!isTestEnv && docs.length > 0) {
404
- const state = cacheState ?? (await buildPreludeCacheState(cwd));
405
- await writePreludeCache(state, docs);
406
- }
407
448
  return { ok: true, docs };
408
449
  } catch (err: unknown) {
409
450
  const reason = err instanceof Error ? err.message : String(err);
@@ -461,7 +502,7 @@ async function createKernelSession(
461
502
 
462
503
  let kernel: PythonKernel;
463
504
  try {
464
- kernel = await logger.timeAsync("createKernelSession:PythonKernel.start", () => PythonKernel.start(startOptions));
505
+ kernel = await logger.time("createKernelSession:PythonKernel.start", PythonKernel.start, startOptions);
465
506
  } catch (err) {
466
507
  if (!isRetry && isResourceExhaustionError(err)) {
467
508
  await recoverFromResourceExhaustion();
@@ -541,7 +582,7 @@ async function withKernelSession<T>(
541
582
  if (options.signal?.aborted) {
542
583
  throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
543
584
  }
544
- session = await logger.timeAsync("kernel:createKernelSession", createKernelSession, sessionId, cwd, options);
585
+ session = await logger.time("kernel:createKernelSession", createKernelSession, sessionId, cwd, options);
545
586
  kernelSessions.set(sessionId, session);
546
587
  startCleanupTimer();
547
588
  }
@@ -549,18 +590,18 @@ async function withKernelSession<T>(
549
590
  const run = async (): Promise<T> => {
550
591
  session!.lastUsedAt = Date.now();
551
592
  if (session!.dead || !session!.kernel.isAlive()) {
552
- await logger.timeAsync("kernel:restartKernelSession", restartKernelSession, session!, cwd, options);
593
+ await logger.time("kernel:restartKernelSession", restartKernelSession, session!, cwd, options);
553
594
  }
554
595
  try {
555
- const result = await logger.timeAsync("kernel:withSession:handler", handler, session!.kernel);
596
+ const result = await logger.time("kernel:withSession:handler", handler, session!.kernel);
556
597
  session!.restartCount = 0;
557
598
  return result;
558
599
  } catch (err) {
559
600
  if (!session!.dead && session!.kernel.isAlive()) {
560
601
  throw err;
561
602
  }
562
- await logger.timeAsync("kernel:restartKernelSession", restartKernelSession, session!, cwd, options);
563
- const result = await logger.timeAsync("kernel:postRestart:handler", handler, session!.kernel);
603
+ await logger.time("kernel:restartKernelSession", restartKernelSession, session!, cwd, options);
604
+ const result = await logger.time("kernel:postRestart:handler", handler, session!.kernel);
564
605
  session!.restartCount = 0;
565
606
  return result;
566
607
  }
@@ -313,9 +313,9 @@ async function killGateway(pid: number, context: string): Promise<void> {
313
313
  export async function acquireSharedGateway(cwd: string): Promise<AcquireResult | null> {
314
314
  try {
315
315
  return await withGatewayLock(async () => {
316
- const existingInfo = await logger.timeAsync("acquireSharedGateway:readInfo", () => readGatewayInfo());
316
+ const existingInfo = await logger.time("acquireSharedGateway:readInfo", readGatewayInfo);
317
317
  if (existingInfo) {
318
- if (await logger.timeAsync("acquireSharedGateway:isAlive", () => isGatewayAlive(existingInfo))) {
318
+ if (await logger.time("acquireSharedGateway:isAlive", isGatewayAlive, existingInfo)) {
319
319
  localGatewayUrl = existingInfo.url;
320
320
  isCoordinatorInitialized = true;
321
321
  logger.debug("Reusing global Python gateway", { url: existingInfo.url });
@@ -329,8 +329,10 @@ export async function acquireSharedGateway(cwd: string): Promise<AcquireResult |
329
329
  await clearGatewayInfo();
330
330
  }
331
331
 
332
- const { url, pid, pythonPath, venvPath } = await logger.timeAsync("acquireSharedGateway:startGateway", () =>
333
- startGatewayProcess(cwd),
332
+ const { url, pid, pythonPath, venvPath } = await logger.time(
333
+ "acquireSharedGateway:startGateway",
334
+ startGatewayProcess,
335
+ cwd,
334
336
  );
335
337
  const info: GatewayInfo = {
336
338
  url,
package/src/ipy/kernel.ts CHANGED
@@ -421,8 +421,10 @@ export class PythonKernel {
421
421
  }
422
422
 
423
423
  static async start(options: KernelStartOptions): Promise<PythonKernel> {
424
- const availability = await logger.timeAsync("PythonKernel.start:availabilityCheck", () =>
425
- checkPythonKernelAvailability(options.cwd),
424
+ const availability = await logger.time(
425
+ "PythonKernel.start:availabilityCheck",
426
+ checkPythonKernelAvailability,
427
+ options.cwd,
426
428
  );
427
429
  if (!availability.ok) {
428
430
  throw new Error(availability.reason ?? "Python kernel unavailable");
@@ -443,14 +445,21 @@ export class PythonKernel {
443
445
  for (let attempt = 0; attempt < 2; attempt += 1) {
444
446
  throwIfAborted(startupSignal, "Python kernel startup aborted");
445
447
  try {
446
- const sharedResult = await logger.timeAsync("PythonKernel.start:acquireSharedGateway", () =>
447
- acquireSharedGateway(options.cwd),
448
+ const sharedResult = await logger.time(
449
+ "PythonKernel.start:acquireSharedGateway",
450
+ acquireSharedGateway,
451
+ options.cwd,
448
452
  );
449
453
  if (!sharedResult) {
450
454
  throw new Error("Shared Python gateway unavailable");
451
455
  }
452
- const kernel = await logger.timeAsync("PythonKernel.start:startWithSharedGateway", () =>
453
- PythonKernel.#startWithSharedGateway(sharedResult.url, options.cwd, options.env, startup),
456
+ const kernel = await logger.time(
457
+ "PythonKernel.start:startWithSharedGateway",
458
+ PythonKernel.#startWithSharedGateway,
459
+ sharedResult.url,
460
+ options.cwd,
461
+ options.env,
462
+ startup,
454
463
  );
455
464
  return kernel;
456
465
  } catch (err) {
@@ -538,13 +547,16 @@ export class PythonKernel {
538
547
  ): Promise<PythonKernel> {
539
548
  const startupSignal = combineAbortSignal(startup, undefined, "Python kernel startup aborted");
540
549
  throwIfAborted(startupSignal, "Python kernel startup aborted");
541
- const createResponse = await logger.timeAsync("startWithSharedGateway:createKernel", () =>
542
- fetch(`${gatewayUrl}/api/kernels`, {
550
+ const createResponse = await logger.time(
551
+ "startWithSharedGateway:createKernel",
552
+ fetch,
553
+ `${gatewayUrl}/api/kernels`,
554
+ {
543
555
  method: "POST",
544
556
  headers: { "Content-Type": "application/json" },
545
557
  body: JSON.stringify({ name: "python3" }),
546
558
  signal: startupSignal,
547
- }),
559
+ },
548
560
  );
549
561
 
550
562
  if (!createResponse.ok) {
@@ -557,35 +569,44 @@ export class PythonKernel {
557
569
  );
558
570
  }
559
571
 
560
- const kernelInfo = await logger.timeAsync(
572
+ const kernelInfo = (await logger.time(
561
573
  "startWithSharedGateway:parseJson",
562
- () => createResponse.json() as Promise<{ id: string }>,
563
- );
574
+ createResponse.json.bind(createResponse),
575
+ )) as { id: string };
564
576
  const kernelId = kernelInfo.id;
565
577
 
566
578
  const kernel = new PythonKernel(Snowflake.next(), kernelId, gatewayUrl, Snowflake.next(), "omp", true);
567
579
 
568
580
  try {
569
- await logger.timeAsync("startWithSharedGateway:connectWS", () => kernel.#connectWebSocket(startup));
570
- await logger.timeAsync("startWithSharedGateway:initEnv", () =>
571
- kernel.#initializeKernelEnvironment(cwd, env, startup),
581
+ await logger.time("startWithSharedGateway:connectWS", kernel.#connectWebSocket.bind(kernel), startup);
582
+ await logger.time(
583
+ "startWithSharedGateway:initEnv",
584
+ kernel.#initializeKernelEnvironment.bind(kernel),
585
+ cwd,
586
+ env,
587
+ startup,
572
588
  );
573
589
  const preludeOptions = getStartupExecuteOptions(startup);
574
- const preludeResult = await logger.timeAsync("startWithSharedGateway:prelude", () =>
575
- kernel.execute(PYTHON_PRELUDE, {
590
+ const preludeResult = await logger.time(
591
+ "startWithSharedGateway:prelude",
592
+ kernel.execute.bind(kernel),
593
+ PYTHON_PRELUDE,
594
+ {
576
595
  ...preludeOptions,
577
596
  silent: true,
578
597
  storeHistory: false,
579
- }),
598
+ },
580
599
  );
581
600
  throwIfStartupExecutionFailed(
582
601
  preludeResult,
583
602
  preludeOptions.signal,
584
603
  "Failed to initialize Python kernel prelude",
585
604
  );
586
- await logger.timeAsync("startWithSharedGateway:loadModules", () =>
587
- loadPythonModules(kernel, { cwd, signal: startup.signal, deadlineMs: startup.deadlineMs }),
588
- );
605
+ await logger.time("startWithSharedGateway:loadModules", loadPythonModules, kernel, {
606
+ cwd,
607
+ signal: startup.signal,
608
+ deadlineMs: startup.deadlineMs,
609
+ });
589
610
  return kernel;
590
611
  } catch (err: unknown) {
591
612
  await kernel.shutdown({ timeoutMs: getStartupCleanupTimeoutMs(startup.deadlineMs) });
@@ -927,11 +948,13 @@ export class PythonKernel {
927
948
  return promise;
928
949
  }
929
950
 
930
- async introspectPrelude(): Promise<PreludeHelper[]> {
951
+ async introspectPrelude(options: Pick<KernelExecuteOptions, "signal" | "timeoutMs"> = {}): Promise<PreludeHelper[]> {
931
952
  let output = "";
932
953
  const result = await this.execute(PRELUDE_INTROSPECTION_SNIPPET, {
933
954
  silent: false,
934
955
  storeHistory: false,
956
+ signal: options.signal,
957
+ timeoutMs: options.timeoutMs,
935
958
  onChunk: text => {
936
959
  output += text;
937
960
  },
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
- import { $env, getPythonEnvDir } from "@oh-my-pi/pi-utils";
9
+ import { $env, $which, getPythonEnvDir } from "@oh-my-pi/pi-utils";
10
10
 
11
11
  const DEFAULT_ENV_ALLOWLIST = new Set([
12
12
  "PATH",
@@ -210,7 +210,7 @@ export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
210
210
  };
211
211
  }
212
212
 
213
- const pythonPath = Bun.which("python") ?? Bun.which("python3");
213
+ const pythonPath = $which("python") ?? $which("python3");
214
214
  if (!pythonPath) {
215
215
  throw new Error("Python executable not found on PATH");
216
216
  }
package/src/lsp/client.ts CHANGED
@@ -3,11 +3,11 @@ import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
3
3
  import { applyWorkspaceEdit } from "./edits";
4
4
  import { getLspmuxCommand, isLspmuxSupported } from "./lspmux";
5
5
  import type {
6
- Diagnostic,
7
6
  LspClient,
8
7
  LspJsonRpcNotification,
9
8
  LspJsonRpcRequest,
10
9
  LspJsonRpcResponse,
10
+ PublishDiagnosticsParams,
11
11
  ServerConfig,
12
12
  WorkspaceEdit,
13
13
  } from "./types";
@@ -130,7 +130,7 @@ const CLIENT_CAPABILITIES = {
130
130
  },
131
131
  publishDiagnostics: {
132
132
  relatedInformation: true,
133
- versionSupport: false,
133
+ versionSupport: true,
134
134
  tagSupport: { valueSet: [1, 2] },
135
135
  codeDescriptionSupport: true,
136
136
  dataSupport: true,
@@ -261,8 +261,11 @@ async function startMessageReader(client: LspClient): Promise<void> {
261
261
  } else if ("method" in message) {
262
262
  // Server notification
263
263
  if (message.method === "textDocument/publishDiagnostics" && message.params) {
264
- const params = message.params as { uri: string; diagnostics: Diagnostic[] };
265
- client.diagnostics.set(params.uri, params.diagnostics);
264
+ const params = message.params as PublishDiagnosticsParams;
265
+ client.diagnostics.set(params.uri, {
266
+ diagnostics: params.diagnostics,
267
+ version: params.version ?? null,
268
+ });
266
269
  client.diagnosticsVersion += 1;
267
270
  }
268
271
  }
@@ -77,14 +77,14 @@ export class LspLinterClient implements LinterClient {
77
77
  const timeoutMs = 3000;
78
78
  const start = Date.now();
79
79
  while (Date.now() - start < timeoutMs) {
80
- const diagnostics = client.diagnostics.get(uri);
81
- if (diagnostics !== undefined) {
82
- return diagnostics;
80
+ const publishedDiagnostics = client.diagnostics.get(uri);
81
+ if (publishedDiagnostics !== undefined) {
82
+ return publishedDiagnostics.diagnostics;
83
83
  }
84
84
  await Bun.sleep(100);
85
85
  }
86
86
 
87
- return client.diagnostics.get(uri) ?? [];
87
+ return client.diagnostics.get(uri)?.diagnostics ?? [];
88
88
  }
89
89
 
90
90
  dispose(): void {
package/src/lsp/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { isRecord, logger } from "@oh-my-pi/pi-utils";
4
+ import { $which, isRecord, logger } from "@oh-my-pi/pi-utils";
5
5
  import { YAML } from "bun";
6
6
  import { getConfigDirPaths } from "../config";
7
7
  import { getPreloadedPluginRoots } from "../discovery/helpers";
@@ -197,6 +197,21 @@ const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
197
197
  { markers: ["go.mod", "go.sum"], binDir: "bin" },
198
198
  ];
199
199
 
200
+ const WINDOWS_LOCAL_EXECUTABLE_EXTENSIONS = [".exe", ".cmd", ".bat"] as const;
201
+
202
+ function resolveLocalCommand(basePath: string): string | null {
203
+ if (fs.existsSync(basePath)) return basePath;
204
+ if (process.platform !== "win32") return null;
205
+
206
+ // Package managers write Windows launchers with executable suffixes in node_modules/.bin.
207
+ for (const extension of WINDOWS_LOCAL_EXECUTABLE_EXTENSIONS) {
208
+ const candidate = `${basePath}${extension}`;
209
+ if (fs.existsSync(candidate)) return candidate;
210
+ }
211
+
212
+ return null;
213
+ }
214
+
200
215
  /**
201
216
  * Resolve a command to an executable path.
202
217
  * Checks project-local bin directories first, then falls back to $PATH.
@@ -210,14 +225,15 @@ export function resolveCommand(command: string, cwd: string): string | null {
210
225
  for (const { markers, binDir } of LOCAL_BIN_PATHS) {
211
226
  if (hasRootMarkers(cwd, markers)) {
212
227
  const localPath = path.join(cwd, binDir, command);
213
- if (fs.existsSync(localPath)) {
214
- return localPath;
228
+ const resolvedLocalPath = resolveLocalCommand(localPath);
229
+ if (resolvedLocalPath) {
230
+ return resolvedLocalPath;
215
231
  }
216
232
  }
217
233
  }
218
234
 
219
235
  // Fall back to $PATH
220
- return Bun.which(command);
236
+ return $which(command);
221
237
  }
222
238
 
223
239
  /**