@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.2

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 (202) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/cli.js +4006 -3966
  3. package/dist/types/advisor/advise-tool.d.ts +5 -2
  4. package/dist/types/advisor/config.d.ts +14 -6
  5. package/dist/types/advisor/runtime.d.ts +20 -11
  6. package/dist/types/autolearn/controller.d.ts +1 -0
  7. package/dist/types/cli-commands.d.ts +14 -1
  8. package/dist/types/config/model-discovery.d.ts +17 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/model-resolver.d.ts +6 -2
  11. package/dist/types/config/models-config-schema.d.ts +10 -0
  12. package/dist/types/config/models-config.d.ts +6 -0
  13. package/dist/types/config/settings-schema.d.ts +32 -7
  14. package/dist/types/config/settings.d.ts +50 -0
  15. package/dist/types/cursor.d.ts +11 -0
  16. package/dist/types/discovery/helpers.d.ts +5 -2
  17. package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  20. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
  23. package/dist/types/extensibility/shared-events.d.ts +6 -0
  24. package/dist/types/extensibility/utils.d.ts +2 -2
  25. package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
  26. package/dist/types/lib/xai-http.d.ts +11 -0
  27. package/dist/types/mcp/transports/stdio.d.ts +13 -1
  28. package/dist/types/modes/acp/acp-agent.d.ts +3 -0
  29. package/dist/types/modes/acp/acp-mode.d.ts +6 -2
  30. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  31. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  32. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +23 -1
  34. package/dist/types/modes/components/hook-editor.d.ts +16 -2
  35. package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
  36. package/dist/types/modes/components/model-hub.d.ts +5 -2
  37. package/dist/types/modes/components/session-selector.d.ts +4 -0
  38. package/dist/types/modes/components/tool-execution.d.ts +8 -1
  39. package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
  40. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  42. package/dist/types/modes/interactive-mode.d.ts +6 -0
  43. package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
  44. package/dist/types/modes/print-mode.d.ts +1 -1
  45. package/dist/types/modes/types.d.ts +10 -0
  46. package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
  47. package/dist/types/modes/warp-events.d.ts +24 -0
  48. package/dist/types/modes/warp-events.test.d.ts +1 -0
  49. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  50. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  51. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  52. package/dist/types/sdk.d.ts +13 -2
  53. package/dist/types/session/agent-session.d.ts +43 -11
  54. package/dist/types/session/session-history-format.d.ts +10 -0
  55. package/dist/types/session/session-manager.d.ts +14 -0
  56. package/dist/types/session/streaming-output.d.ts +2 -0
  57. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  58. package/dist/types/slash-commands/types.d.ts +4 -4
  59. package/dist/types/system-prompt.d.ts +1 -1
  60. package/dist/types/task/label.d.ts +1 -1
  61. package/dist/types/telemetry-export.d.ts +34 -9
  62. package/dist/types/tools/approval.d.ts +8 -0
  63. package/dist/types/tools/bash.d.ts +2 -0
  64. package/dist/types/tools/browser/registry.d.ts +7 -3
  65. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  66. package/dist/types/tools/essential-tools.d.ts +29 -0
  67. package/dist/types/tools/image-gen.d.ts +2 -1
  68. package/dist/types/tools/index.d.ts +4 -1
  69. package/dist/types/tools/manage-skill.d.ts +2 -0
  70. package/dist/types/tools/xdev.d.ts +11 -2
  71. package/dist/types/utils/title-generator.d.ts +2 -1
  72. package/dist/types/web/search/index.d.ts +2 -0
  73. package/dist/types/web/search/providers/base.d.ts +3 -0
  74. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  75. package/dist/types/web/search/types.d.ts +1 -1
  76. package/package.json +21 -16
  77. package/src/advisor/__tests__/advisor.test.ts +1358 -86
  78. package/src/advisor/__tests__/config.test.ts +58 -2
  79. package/src/advisor/advise-tool.ts +7 -3
  80. package/src/advisor/config.ts +76 -24
  81. package/src/advisor/runtime.ts +445 -92
  82. package/src/autolearn/controller.ts +23 -28
  83. package/src/cli/bench-cli.ts +4 -1
  84. package/src/cli/grep-cli.ts +2 -1
  85. package/src/cli-commands.ts +57 -23
  86. package/src/cli.ts +5 -1
  87. package/src/config/model-discovery.ts +81 -21
  88. package/src/config/model-registry.ts +34 -6
  89. package/src/config/model-resolver.ts +57 -12
  90. package/src/config/models-config-schema.ts +1 -0
  91. package/src/config/settings-schema.ts +43 -6
  92. package/src/config/settings.ts +405 -25
  93. package/src/cursor.ts +20 -3
  94. package/src/debug/report-bundle.ts +40 -4
  95. package/src/discovery/codex.ts +14 -4
  96. package/src/discovery/helpers.ts +28 -5
  97. package/src/discovery/substitute-plugin-root.ts +23 -7
  98. package/src/exec/bash-executor.ts +14 -5
  99. package/src/export/html/template.js +2 -0
  100. package/src/extensibility/custom-tools/loader.ts +3 -3
  101. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  102. package/src/extensibility/extensions/loader.ts +3 -3
  103. package/src/extensibility/extensions/managed-timers.ts +83 -0
  104. package/src/extensibility/extensions/runner.ts +26 -0
  105. package/src/extensibility/extensions/types.ts +18 -0
  106. package/src/extensibility/extensions/wrapper.ts +2 -1
  107. package/src/extensibility/hooks/loader.ts +3 -3
  108. package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
  109. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  110. package/src/extensibility/plugins/manager.ts +2 -2
  111. package/src/extensibility/shared-events.ts +6 -0
  112. package/src/extensibility/utils.ts +91 -25
  113. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
  114. package/src/internal-urls/agent-protocol.ts +73 -39
  115. package/src/irc/bus.ts +22 -3
  116. package/src/launch/broker.ts +3 -2
  117. package/src/launch/client.ts +2 -2
  118. package/src/launch/presence.ts +2 -2
  119. package/src/lib/xai-http.ts +28 -2
  120. package/src/lsp/client.ts +1 -1
  121. package/src/main.ts +11 -8
  122. package/src/mcp/manager.ts +9 -3
  123. package/src/mcp/transports/stdio.ts +103 -23
  124. package/src/modes/acp/acp-agent.ts +28 -9
  125. package/src/modes/acp/acp-event-mapper.ts +8 -0
  126. package/src/modes/acp/acp-mode.ts +18 -4
  127. package/src/modes/components/advisor-config.ts +65 -3
  128. package/src/modes/components/ask-dialog.ts +1 -1
  129. package/src/modes/components/assistant-message.ts +35 -14
  130. package/src/modes/components/chat-transcript-builder.ts +2 -0
  131. package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
  132. package/src/modes/components/custom-editor.ts +62 -1
  133. package/src/modes/components/hook-editor.ts +35 -5
  134. package/src/modes/components/login-dialog.test.ts +56 -0
  135. package/src/modes/components/login-dialog.ts +7 -3
  136. package/src/modes/components/model-hub.ts +138 -42
  137. package/src/modes/components/plan-review-overlay.ts +51 -10
  138. package/src/modes/components/session-selector.ts +10 -0
  139. package/src/modes/components/status-line/component.test.ts +1 -0
  140. package/src/modes/components/status-line/component.ts +2 -4
  141. package/src/modes/components/status-line/segments.ts +21 -6
  142. package/src/modes/components/tool-execution.test.ts +101 -0
  143. package/src/modes/components/tool-execution.ts +117 -9
  144. package/src/modes/components/transcript-container.ts +30 -2
  145. package/src/modes/controllers/command-controller-shared.ts +1 -1
  146. package/src/modes/controllers/command-controller.ts +168 -48
  147. package/src/modes/controllers/event-controller.ts +6 -0
  148. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  149. package/src/modes/controllers/input-controller.ts +12 -12
  150. package/src/modes/controllers/selector-controller.ts +208 -46
  151. package/src/modes/interactive-mode.ts +205 -65
  152. package/src/modes/noninteractive-dispose.test.ts +60 -0
  153. package/src/modes/print-mode.ts +10 -5
  154. package/src/modes/rpc/host-tools.ts +2 -1
  155. package/src/modes/rpc/rpc-mode.ts +31 -7
  156. package/src/modes/theme/theme.ts +2 -2
  157. package/src/modes/types.ts +10 -0
  158. package/src/modes/utils/interactive-context-helpers.ts +3 -1
  159. package/src/modes/utils/transcript-render-helpers.ts +3 -2
  160. package/src/modes/warp-events.test.ts +794 -0
  161. package/src/modes/warp-events.ts +232 -0
  162. package/src/plan-mode/model-transition.test.ts +60 -0
  163. package/src/plan-mode/model-transition.ts +51 -0
  164. package/src/prompts/system/system-prompt.md +1 -1
  165. package/src/registry/agent-lifecycle.ts +133 -18
  166. package/src/sdk.ts +227 -44
  167. package/src/session/agent-session.ts +1364 -382
  168. package/src/session/session-history-format.ts +20 -5
  169. package/src/session/session-listing.ts +6 -5
  170. package/src/session/session-loader.ts +9 -0
  171. package/src/session/session-manager.ts +48 -0
  172. package/src/session/session-persistence.ts +11 -0
  173. package/src/session/streaming-output.ts +3 -1
  174. package/src/slash-commands/builtin-registry.ts +9 -0
  175. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  176. package/src/slash-commands/types.ts +4 -4
  177. package/src/system-prompt.ts +2 -2
  178. package/src/task/executor.ts +1 -1
  179. package/src/task/label.ts +2 -0
  180. package/src/telemetry-export.ts +453 -97
  181. package/src/tools/approval.ts +11 -0
  182. package/src/tools/bash.ts +71 -38
  183. package/src/tools/browser/registry.ts +9 -2
  184. package/src/tools/browser/tab-supervisor.ts +65 -10
  185. package/src/tools/browser.ts +4 -3
  186. package/src/tools/essential-tools.ts +45 -0
  187. package/src/tools/eval.ts +3 -0
  188. package/src/tools/gh.ts +169 -2
  189. package/src/tools/image-gen.ts +624 -513
  190. package/src/tools/index.ts +10 -6
  191. package/src/tools/manage-skill.ts +5 -3
  192. package/src/tools/output-meta.ts +7 -0
  193. package/src/tools/path-utils.ts +8 -5
  194. package/src/tools/read.ts +48 -3
  195. package/src/tools/write.ts +22 -4
  196. package/src/tools/xdev.ts +14 -3
  197. package/src/utils/title-generator.ts +15 -4
  198. package/src/web/search/index.ts +21 -6
  199. package/src/web/search/providers/base.ts +3 -0
  200. package/src/web/search/providers/kimi.ts +18 -12
  201. package/src/web/search/providers/xai.ts +40 -9
  202. package/src/web/search/types.ts +6 -1
@@ -12,7 +12,7 @@ import { loadCapability } from "../../discovery";
12
12
  import * as PiCodingAgent from "../../index";
13
13
  import type { CustomMessagePayload } from "../../session/messages";
14
14
  import * as typebox from "../typebox";
15
- import { resolvePath, withExitGuard } from "../utils";
15
+ import { resolvePath, withHostGuard } from "../utils";
16
16
  import { execCommand } from "./runner";
17
17
  import type { ExecOptions, HookAPI, HookFactory, HookMessageRenderer, RegisteredCommand } from "./types";
18
18
 
@@ -149,7 +149,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
149
149
 
150
150
  try {
151
151
  // Import the module using native Bun import
152
- const module = await withExitGuard(() => import(resolvedPath));
152
+ const module = await withHostGuard(() => import(resolvedPath));
153
153
  const factory = module.default as HookFactory;
154
154
 
155
155
  if (typeof factory !== "function") {
@@ -164,7 +164,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
164
164
  );
165
165
 
166
166
  // Call factory to register handlers
167
- await withExitGuard(async () => factory(api));
167
+ await withHostGuard(async () => factory(api));
168
168
 
169
169
  return {
170
170
  hook: {
@@ -44,9 +44,10 @@ import { ReadTool } from "../tools/read";
44
44
  import { formatBytes } from "../tools/render-utils";
45
45
  import { WriteTool } from "../tools/write";
46
46
  import { EventBus } from "../utils/event-bus";
47
- import { loadExtensionFromFactory, loadExtensions } from "./extensions";
47
+ import { discoverExtensionPaths, loadExtensionFromFactory, loadExtensions } from "./extensions";
48
48
  import { ExtensionRuntime } from "./extensions/loader";
49
49
  import type { ExtensionFactory, ToolDefinition } from "./extensions/types";
50
+ import { getEnabledPlugins, resolvePluginExtensionPaths, type ScopedInstalledPlugin } from "./plugins/loader";
50
51
  import type { Skill } from "./skills";
51
52
  import { loadSkillsFromDir } from "./skills";
52
53
  import { Type } from "./typebox";
@@ -655,6 +656,100 @@ export const SettingsManager = {
655
656
  },
656
657
  } as const;
657
658
 
659
+ /** Scope used by the legacy package manager for discovered resources. */
660
+ export type SourceScope = "user" | "project" | "temporary";
661
+
662
+ /** Discovery metadata exposed alongside a legacy package resource path. */
663
+ export interface PathMetadata {
664
+ source: string;
665
+ scope: SourceScope;
666
+ origin: "package" | "top-level";
667
+ baseDir?: string;
668
+ }
669
+
670
+ /** One extension, skill, prompt, or theme resolved by the legacy package manager. */
671
+ export interface ResolvedResource {
672
+ path: string;
673
+ enabled: boolean;
674
+ metadata: PathMetadata;
675
+ }
676
+
677
+ /** Resource groups returned by {@link DefaultPackageManager.resolve}. */
678
+ export interface ResolvedPaths {
679
+ extensions: ResolvedResource[];
680
+ skills: ResolvedResource[];
681
+ prompts: ResolvedResource[];
682
+ themes: ResolvedResource[];
683
+ }
684
+
685
+ /** Action a legacy caller requests when a configured package is unavailable. */
686
+ export type MissingSourceAction = "install" | "skip" | "error";
687
+
688
+ /** Construction inputs accepted by the legacy package manager. */
689
+ export interface DefaultPackageManagerOptions {
690
+ cwd: string;
691
+ agentDir: string;
692
+ settingsManager: Settings | Promise<Settings>;
693
+ }
694
+
695
+ /**
696
+ * Enumerates the extensions OMP would load through the historical package
697
+ * manager surface used by legacy extensions.
698
+ */
699
+ export class DefaultPackageManager {
700
+ #cwd: string;
701
+ #agentDir: string;
702
+ #settingsManager: Settings | Promise<Settings>;
703
+
704
+ constructor(options: DefaultPackageManagerOptions) {
705
+ this.#cwd = options.cwd;
706
+ this.#agentDir = options.agentDir;
707
+ this.#settingsManager = options.settingsManager;
708
+ }
709
+
710
+ /** Resolve enabled extension paths with their OMP plugin provenance. */
711
+ async resolve(_onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths> {
712
+ const settings = await this.#settingsManager;
713
+ const configuredPaths = settings.get("extensions") ?? [];
714
+ const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
715
+ const [extensionPaths, plugins] = await Promise.all([
716
+ discoverExtensionPaths(configuredPaths, this.#cwd, disabledExtensionIds),
717
+ getEnabledPlugins(this.#cwd),
718
+ ]);
719
+ const pluginByExtensionPath = new Map<string, ScopedInstalledPlugin>();
720
+ for (const plugin of plugins) {
721
+ for (const extensionPath of resolvePluginExtensionPaths(plugin)) {
722
+ pluginByExtensionPath.set(path.resolve(extensionPath), plugin);
723
+ }
724
+ }
725
+
726
+ const extensions = extensionPaths.map(extensionPath => {
727
+ const resolvedPath = path.resolve(extensionPath);
728
+ const plugin = pluginByExtensionPath.get(resolvedPath);
729
+ const agentDirRelative = path.relative(path.resolve(this.#agentDir), resolvedPath);
730
+ const metadata: PathMetadata = plugin
731
+ ? {
732
+ source: `npm:${plugin.name}`,
733
+ scope: plugin.scope,
734
+ origin: "package",
735
+ baseDir: plugin.path,
736
+ }
737
+ : {
738
+ source: "auto",
739
+ scope:
740
+ agentDirRelative === "" ||
741
+ (!agentDirRelative.startsWith("..") && !path.isAbsolute(agentDirRelative))
742
+ ? "user"
743
+ : "project",
744
+ origin: "top-level",
745
+ };
746
+ return { path: resolvedPath, enabled: true, metadata };
747
+ });
748
+
749
+ return { extensions, skills: [], prompts: [], themes: [] };
750
+ }
751
+ }
752
+
658
753
  /**
659
754
  * Resource-loader compatibility layer for legacy pi extensions.
660
755
  *
@@ -1,6 +1,6 @@
1
1
  /// <reference path="./legacy-pi-virtual-modules.d.ts" />
2
2
  import * as fs from "node:fs";
3
- import { isBuiltin } from "node:module";
3
+ import { createRequire, isBuiltin } from "node:module";
4
4
  import * as path from "node:path";
5
5
  import * as url from "node:url";
6
6
  import { isCompiledBinary, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
@@ -1111,6 +1111,74 @@ const EXTENSION_GRAPH_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["
1111
1111
  // reloads install supplemental hooks only for modules added to the graph since
1112
1112
  // the previous load.
1113
1113
  const extensionGraphHookModules = new Map<string, Set<string>>();
1114
+ const commonJsModuleSources = new Map<string, string>();
1115
+ const commonJsFallbackModulePaths = new Map<string, string>();
1116
+ const COMMONJS_REQUIRE_GLOBAL = "__ompLegacyPiRequireGraphModule";
1117
+ const commonJsModuleDefinitions = new Map<string, { source: string; filename: string; dirname: string }>();
1118
+ const commonJsModuleCache = new Map<
1119
+ string,
1120
+ {
1121
+ exports: unknown;
1122
+ filename: string;
1123
+ id: string;
1124
+ path: string;
1125
+ require: NodeJS.Require;
1126
+ loaded: boolean;
1127
+ }
1128
+ >();
1129
+ const commonJsTypeScriptTranspiler = new Bun.Transpiler({ loader: "ts" });
1130
+
1131
+ function evaluateGraphCommonJs(modulePath: string): unknown {
1132
+ const cached = commonJsModuleCache.get(modulePath);
1133
+ if (cached) {
1134
+ return cached.exports;
1135
+ }
1136
+ const definition = commonJsModuleDefinitions.get(modulePath);
1137
+ if (!definition) {
1138
+ throw new Error(`Missing graph-owned CommonJS definition: ${modulePath}`);
1139
+ }
1140
+
1141
+ const nativeRequire = createRequire(definition.filename);
1142
+ const module = {
1143
+ exports: {},
1144
+ filename: definition.filename,
1145
+ id: definition.filename,
1146
+ path: definition.dirname,
1147
+ require: nativeRequire,
1148
+ loaded: false,
1149
+ };
1150
+ commonJsModuleCache.set(modulePath, module);
1151
+ const graphRequire: NodeJS.Require = Object.assign(
1152
+ (specifier: string) => {
1153
+ const resolved = nativeRequire.resolve(specifier);
1154
+ let graphPath = resolved;
1155
+ try {
1156
+ graphPath = fs.realpathSync(resolved);
1157
+ } catch {
1158
+ // Builtins and virtual modules have no filesystem realpath.
1159
+ }
1160
+ return commonJsModuleDefinitions.has(graphPath) ? evaluateGraphCommonJs(graphPath) : nativeRequire(specifier);
1161
+ },
1162
+ {
1163
+ resolve: nativeRequire.resolve,
1164
+ cache: nativeRequire.cache,
1165
+ extensions: nativeRequire.extensions,
1166
+ main: nativeRequire.main,
1167
+ },
1168
+ );
1169
+ module.require = graphRequire;
1170
+ const execute = new Function("exports", "require", "module", "__filename", "__dirname", definition.source);
1171
+ try {
1172
+ execute.call(module.exports, module.exports, graphRequire, module, definition.filename, definition.dirname);
1173
+ module.loaded = true;
1174
+ return module.exports;
1175
+ } catch (error) {
1176
+ commonJsModuleCache.delete(modulePath);
1177
+ throw error;
1178
+ }
1179
+ }
1180
+
1181
+ Reflect.set(globalThis, COMMONJS_REQUIRE_GLOBAL, evaluateGraphCommonJs);
1114
1182
 
1115
1183
  let legacyPiLoadTag = 0;
1116
1184
 
@@ -1144,9 +1212,9 @@ async function realpathOrSelfUncached(p: string): Promise<string> {
1144
1212
  * Extension-local bare dependency entries are also included so their relative
1145
1213
  * children receive the reload mtime tag; bare imports inside those dependencies
1146
1214
  * remain native Bun resolutions to avoid taking over full third-party graphs.
1147
- * CommonJS modules reached through `require()` stay on Bun's native loader.
1148
- * The only exception is a module whose bare requires resolve to native addons:
1149
- * those require a synchronous hook that pins the addon to an absolute path.
1215
+ * CommonJS modules reached through `require()` stay on Bun's native loader
1216
+ * unless they resolve native addons. CommonJS reached through ESM imports stays
1217
+ * graph-owned so the load hook can expose its exports through an ESM default.
1150
1218
  */
1151
1219
  async function collectExtensionModules(entryRealPath: string): Promise<Map<string, string>> {
1152
1220
  const modules = new Map<string, string>();
@@ -1253,21 +1321,106 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
1253
1321
  return modules;
1254
1322
  }
1255
1323
 
1324
+ /**
1325
+ * Discovers CommonJS export names Bun normally exposes to ESM importers. The
1326
+ * bridge must declare them statically because its default export is synthetic.
1327
+ */
1328
+ function collectCommonJsNamedExports(source: string): string[] {
1329
+ const names = new Set<string>();
1330
+ const assignmentPattern = /(?:^|[;\n])\s*(?:exports|module\.exports)\.([A-Za-z_$][\w$]*)\s*=/gm;
1331
+ for (const match of source.matchAll(assignmentPattern)) {
1332
+ const name = match[1];
1333
+ if (name && name !== "default") {
1334
+ names.add(name);
1335
+ }
1336
+ }
1337
+ const objectPattern = /module\.exports\s*=\s*\{([\s\S]*?)\}/g;
1338
+ for (const objectMatch of source.matchAll(objectPattern)) {
1339
+ const propertyPattern = /(?:^|,)\s*(?:([A-Za-z_$][\w$]*)\s*(?=[:,]|$)|["']([A-Za-z_$][\w$]*)["']\s*:)/g;
1340
+ for (const propertyMatch of objectMatch[1]?.matchAll(propertyPattern) ?? []) {
1341
+ const name = propertyMatch[1] ?? propertyMatch[2];
1342
+ if (name && name !== "default") {
1343
+ names.add(name);
1344
+ }
1345
+ }
1346
+ }
1347
+ return [...names];
1348
+ }
1349
+
1350
+ /**
1351
+ * The shared evaluator gives ESM imports and sibling `require()` calls the
1352
+ * same `module.exports` value and cycle-aware cache.
1353
+ */
1354
+ function synthesizeCommonJsDefaultModule(modulePath: string, source: string, targetPath = modulePath): string {
1355
+ let commonJsSource = source;
1356
+ if (commonJsSource.startsWith("#!")) {
1357
+ const firstLineEnd = commonJsSource.indexOf("\n");
1358
+ commonJsSource = firstLineEnd === -1 ? "" : commonJsSource.slice(firstLineEnd + 1);
1359
+ }
1360
+
1361
+ const executableSource = targetPath.endsWith(".cts")
1362
+ ? commonJsTypeScriptTranspiler.transformSync(commonJsSource)
1363
+ : commonJsSource;
1364
+ commonJsModuleDefinitions.set(modulePath, {
1365
+ source: executableSource,
1366
+ filename: targetPath,
1367
+ dirname: path.dirname(targetPath),
1368
+ });
1369
+ commonJsModuleCache.delete(modulePath);
1370
+ const exportsBinding = "__ompLegacyPiCommonJsExports";
1371
+ const namedExports = collectCommonJsNamedExports(executableSource)
1372
+ .map(
1373
+ (name, index) =>
1374
+ `const __ompLegacyPiCommonJsExport${index} = ${exportsBinding}[${JSON.stringify(name)}]; export { __ompLegacyPiCommonJsExport${index} as ${name} };`,
1375
+ )
1376
+ .join("\n");
1377
+ return `const ${exportsBinding} = globalThis[${JSON.stringify(COMMONJS_REQUIRE_GLOBAL)}](${JSON.stringify(modulePath)});\nexport default ${exportsBinding};\n${namedExports}\n`;
1378
+ }
1379
+
1380
+ /**
1381
+ * Linkedom's canvas bridge uses its bundled fallback because OMP does not ship
1382
+ * native canvas.
1383
+ */
1384
+ async function prepareCommonJsDefaultModule(modulePath: string, source: string): Promise<string> {
1385
+ const packageRoot = await findPackageRoot(modulePath);
1386
+ if (!packageRoot) {
1387
+ return synthesizeCommonJsDefaultModule(modulePath, source);
1388
+ }
1389
+ const manifest = await readPackageManifest(packageRoot);
1390
+ const packageRelativePath = path.relative(packageRoot, modulePath).split(path.sep).join("/");
1391
+ if (manifest?.name !== "linkedom" || packageRelativePath !== "commonjs/canvas.cjs") {
1392
+ return synthesizeCommonJsDefaultModule(modulePath, source);
1393
+ }
1394
+
1395
+ const targetPath = path.join(packageRoot, "commonjs", "canvas-shim.cjs");
1396
+ commonJsFallbackModulePaths.set(modulePath, targetPath);
1397
+ return synthesizeCommonJsDefaultModule(modulePath, await Bun.file(targetPath).text(), targetPath);
1398
+ }
1399
+
1256
1400
  /**
1257
1401
  * Install exact-path load hooks for the current extension graph. ESM/TS source
1258
- * retains the async rewrite path. Native-addon CJS loaders use a synchronous
1259
- * hook with source pre-rewritten during graph collection; Bun rejects a CJS
1260
- * `require()` whose onLoad callback returns a promise.
1402
+ * retains the async rewrite path. CommonJS wrappers and native-addon loaders
1403
+ * stay synchronous because Bun rejects `require()` targets backed by async
1404
+ * `onLoad` callbacks.
1261
1405
  */
1262
- function installExtensionGraphHook(
1406
+ async function installExtensionGraphHook(
1263
1407
  entryRealPath: string,
1264
1408
  modules: Map<string, string>,
1265
- ): { asyncModules: Map<string, string>; syncCommonJsModules: Map<string, string> } {
1409
+ commonJsPaths: Set<string>,
1410
+ ): Promise<{ asyncModules: Map<string, string>; syncSourceModules: Map<string, string> }> {
1266
1411
  const asyncModules = new Map<string, string>();
1267
- const syncCommonJsModules = new Map<string, string>();
1412
+ const syncSourceModules = new Map<string, string>();
1268
1413
  for (const [modulePath, source] of modules) {
1269
- const destination = nativeAddonLoaderModulePaths.has(modulePath) ? syncCommonJsModules : asyncModules;
1270
- destination.set(modulePath, source);
1414
+ const extension = path.extname(modulePath);
1415
+ if (extension === ".cjs" || extension === ".cts") {
1416
+ if (!commonJsPaths.has(modulePath)) {
1417
+ throw new Error(`Missing CommonJS compatibility source: ${modulePath}`);
1418
+ }
1419
+ } else if (nativeAddonLoaderModulePaths.has(modulePath)) {
1420
+ syncSourceModules.set(modulePath, source);
1421
+ } else {
1422
+ asyncModules.set(modulePath, source);
1423
+ }
1271
1424
  }
1272
1425
 
1273
1426
  if (asyncModules.size > 0) {
@@ -1299,17 +1452,42 @@ function installExtensionGraphHook(
1299
1452
  });
1300
1453
  }
1301
1454
 
1302
- if (syncCommonJsModules.size > 0) {
1303
- const alternation = [...syncCommonJsModules.keys()].map(escapeRegExp).join("|");
1455
+ if (commonJsPaths.size > 0) {
1456
+ const alternation = [...commonJsPaths].map(escapeRegExp).join("|");
1457
+ const filter = new RegExp(`^(?:${alternation})(?:\\?mtime=\\d+)?$`);
1458
+ const hookId = Bun.hash(`${entryRealPath}\0commonjs\0${[...commonJsPaths].join("\0")}`).toString(36);
1459
+ Bun.plugin({
1460
+ name: `omp:legacy-pi-ext:${hookId}`,
1461
+ setup(build) {
1462
+ build.onLoad({ filter, namespace: "file" }, args => {
1463
+ const queryIndex = args.path.indexOf("?mtime=");
1464
+ const sourcePath = queryIndex >= 0 ? args.path.slice(0, queryIndex) : args.path;
1465
+ const source =
1466
+ commonJsModuleSources.get(sourcePath) ??
1467
+ synthesizeCommonJsDefaultModule(
1468
+ sourcePath,
1469
+ fs.readFileSync(commonJsFallbackModulePaths.get(sourcePath) ?? sourcePath, "utf8"),
1470
+ commonJsFallbackModulePaths.get(sourcePath) ?? sourcePath,
1471
+ );
1472
+ return { contents: source, loader: getLoader(sourcePath) };
1473
+ });
1474
+ },
1475
+ });
1476
+ }
1477
+
1478
+ if (syncSourceModules.size > 0) {
1479
+ const alternation = [...syncSourceModules.keys()].map(escapeRegExp).join("|");
1304
1480
  const filter = new RegExp(`^(?:${alternation})(?:\\?mtime=\\d+)?$`);
1305
- const hookId = Bun.hash(`${entryRealPath}\0sync-cjs\0${[...syncCommonJsModules.keys()].join("\0")}`).toString(36);
1481
+ const hookId = Bun.hash(`${entryRealPath}\0sync-source\0${[...syncSourceModules.keys()].join("\0")}`).toString(
1482
+ 36,
1483
+ );
1306
1484
  Bun.plugin({
1307
1485
  name: `omp:legacy-pi-ext:${hookId}`,
1308
1486
  setup(build) {
1309
1487
  build.onLoad({ filter, namespace: "file" }, args => {
1310
1488
  const queryIndex = args.path.indexOf("?mtime=");
1311
1489
  const sourcePath = queryIndex >= 0 ? args.path.slice(0, queryIndex) : args.path;
1312
- const source = syncCommonJsModules.get(sourcePath);
1490
+ const source = syncSourceModules.get(sourcePath);
1313
1491
  if (source === undefined) {
1314
1492
  throw new Error(`Missing pre-rewritten CommonJS extension source: ${sourcePath}`);
1315
1493
  }
@@ -1318,7 +1496,7 @@ function installExtensionGraphHook(
1318
1496
  },
1319
1497
  });
1320
1498
  }
1321
- return { asyncModules, syncCommonJsModules };
1499
+ return { asyncModules, syncSourceModules };
1322
1500
  }
1323
1501
 
1324
1502
  /**
@@ -1331,6 +1509,14 @@ function installExtensionGraphHook(
1331
1509
  */
1332
1510
  async function ensureExtensionGraphHook(entryRealPath: string): Promise<{ clear(): void } | undefined> {
1333
1511
  const currentModules = await collectExtensionModules(entryRealPath);
1512
+ const commonJsPaths = new Set<string>();
1513
+ for (const [modulePath, source] of currentModules) {
1514
+ const extension = path.extname(modulePath);
1515
+ if (extension === ".cjs" || extension === ".cts") {
1516
+ commonJsModuleSources.set(modulePath, await prepareCommonJsDefaultModule(modulePath, source));
1517
+ commonJsPaths.add(modulePath);
1518
+ }
1519
+ }
1334
1520
  let hookedModules = extensionGraphHookModules.get(entryRealPath);
1335
1521
  if (!hookedModules) {
1336
1522
  hookedModules = new Set<string>();
@@ -1338,23 +1524,40 @@ async function ensureExtensionGraphHook(entryRealPath: string): Promise<{ clear(
1338
1524
  }
1339
1525
 
1340
1526
  const pendingModules = new Map<string, string>();
1527
+ const pendingCommonJsPaths = new Set<string>();
1341
1528
  for (const [modulePath, source] of currentModules) {
1342
1529
  if (!hookedModules.has(modulePath)) {
1343
1530
  pendingModules.set(modulePath, source);
1531
+ if (commonJsPaths.has(modulePath)) {
1532
+ pendingCommonJsPaths.add(modulePath);
1533
+ }
1344
1534
  }
1345
1535
  }
1346
- if (pendingModules.size === 0) {
1536
+ if (pendingModules.size === 0 && commonJsPaths.size === 0) {
1347
1537
  return undefined;
1348
1538
  }
1349
1539
 
1350
- const { asyncModules, syncCommonJsModules } = installExtensionGraphHook(entryRealPath, pendingModules);
1351
- for (const modulePath of pendingModules.keys()) {
1352
- hookedModules.add(modulePath);
1540
+ let asyncModules = new Map<string, string>();
1541
+ let syncSourceModules = new Map<string, string>();
1542
+ if (pendingModules.size > 0) {
1543
+ ({ asyncModules, syncSourceModules } = await installExtensionGraphHook(
1544
+ entryRealPath,
1545
+ pendingModules,
1546
+ pendingCommonJsPaths,
1547
+ ));
1548
+ for (const modulePath of pendingModules.keys()) {
1549
+ hookedModules.add(modulePath);
1550
+ }
1353
1551
  }
1354
1552
  return {
1355
1553
  clear() {
1356
1554
  asyncModules.clear();
1357
- syncCommonJsModules.clear();
1555
+ syncSourceModules.clear();
1556
+ for (const modulePath of commonJsPaths) {
1557
+ commonJsModuleSources.delete(modulePath);
1558
+ commonJsModuleDefinitions.delete(modulePath);
1559
+ commonJsModuleCache.delete(modulePath);
1560
+ }
1358
1561
  },
1359
1562
  };
1360
1563
  }
@@ -11,7 +11,7 @@ import {
11
11
  isEnoent,
12
12
  logger,
13
13
  } from "@oh-my-pi/pi-utils";
14
- import { withExitGuard } from "../utils";
14
+ import { withHostGuard } from "../utils";
15
15
  import { refreshBunGitCache } from "./bun-git-cache";
16
16
  import { type GitSource, parseGitUrl } from "./git-url";
17
17
  import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "./legacy-pi-compat";
@@ -375,7 +375,7 @@ export class PluginManager {
375
375
  installLegacyPiSpecifierShim();
376
376
  for (const extensionPath of loadable) {
377
377
  try {
378
- const module = await withExitGuard(() => loadLegacyPiModule(extensionPath));
378
+ const module = await withHostGuard(() => loadLegacyPiModule(extensionPath));
379
379
  if (!hasExtensionFactoryExport(module)) {
380
380
  errors.push(`${extensionPath}: extension does not export a valid factory function`);
381
381
  }
@@ -191,6 +191,12 @@ export interface AgentStartEvent {
191
191
  export interface AgentEndEvent {
192
192
  type: "agent_end";
193
193
  messages: AgentMessage[];
194
+ /**
195
+ * When true, the session has already scheduled an automatic continuation
196
+ * (auto-retry, empty/unexpected-stop retry, etc.). Subscribers must not
197
+ * treat this as a user-visible terminal settle.
198
+ */
199
+ willContinue?: boolean;
194
200
  }
195
201
 
196
202
  /** Fired at the start of each turn */
@@ -44,7 +44,7 @@ export function createNoOpUIContext(): HookUIContext {
44
44
  }
45
45
 
46
46
  /**
47
- * Raised by {@link withExitGuard} when a guarded callback synchronously
47
+ * Raised by {@link withHostGuard} when a guarded callback synchronously
48
48
  * attempts to terminate the host process. Callers catch this like any other
49
49
  * load-time failure so the extension/hook is skipped with a logged error
50
50
  * instead of taking the CLI down with it.
@@ -66,22 +66,46 @@ export class ExtensionExitError extends Error {
66
66
 
67
67
  type ExitAliasName = "process.exit" | "process.reallyExit";
68
68
 
69
- let exitGuardDepth = 0;
70
- let exitGuardOriginalProcessExit: typeof process.exit | null = null;
71
- let exitGuardOriginalReallyExit: typeof process.reallyExit | null = null;
69
+ /**
70
+ * stdin events a loaded module must not be allowed to leave hijacked. A
71
+ * top-level `new StdioServerTransport()` (or a bare `process.stdin.resume()`)
72
+ * inside a `~/.claude/tools` MCP server attaches a `data` consumer and puts the
73
+ * shared stdin into flowing mode; Bun delivers one `data` event to that
74
+ * consumer and the TUI's own listener (attached later in `terminal.start()`)
75
+ * then never re-arms — every keypress after the first is swallowed (#5618).
76
+ */
77
+ const HOST_GUARD_STDIN_EVENTS = ["data", "readable", "end", "close", "error"] as const;
78
+ type StdinGuardEvent = (typeof HOST_GUARD_STDIN_EVENTS)[number];
79
+ type StdinGuardListener = (...args: unknown[]) => void;
80
+
81
+ let hostGuardDepth = 0;
82
+ let hostGuardOriginalProcessExit: typeof process.exit | null = null;
83
+ let hostGuardOriginalReallyExit: typeof process.reallyExit | null = null;
84
+ let hostGuardStdinListeners: Record<StdinGuardEvent, StdinGuardListener[]> | null = null;
85
+ let hostGuardStdinWasPaused = false;
86
+ let hostGuardStdinWasRaw = false;
72
87
 
73
88
  /**
74
- * Run `fn` with hard-exit APIs patched so any synchronous attempt to terminate
75
- * the host raises {@link ExtensionExitError} instead. Restored in `finally`.
89
+ * Run `fn` with host-owned process state fenced off from third-party module
90
+ * evaluation, restored in `finally`. Guards the dynamic-import and
91
+ * factory-invocation sites that load extension / hook / tool / plugin modules
92
+ * from user directories (including Claude Code's `~/.claude/tools`, which OMP
93
+ * slurps wholesale). Two hazards are neutralized:
76
94
  *
77
- * Guards the dynamic-import and factory-invocation sites that load third-party
78
- * extension / hook modules a `process.exit(0)` or `process.reallyExit(0)` in
79
- * a stranger's script (e.g. a Codex hook script that happens to live next to
80
- * OMP-shaped modules) would otherwise kill OMP during startup with no error
81
- * surface, since `try/catch` cannot intercept a synchronous exit.
95
+ * - **Hard exit.** `process.exit(0)` / `process.reallyExit(0)` in a stranger's
96
+ * script (e.g. a CLI-shaped module with `main()` at the bottom) would kill
97
+ * OMP during startup with no error surface, since `try/catch` cannot
98
+ * intercept a synchronous exit. Both are patched to throw
99
+ * {@link ExtensionExitError} instead.
100
+ * - **stdin hijack.** A module that attaches a stdin consumer at evaluation
101
+ * time (an MCP `StdioServerTransport`, or a bare `resume()`) steals Bun's
102
+ * single stdin reader, so the TUI goes permanently deaf after one keypress
103
+ * (#5618). Any `data`/`readable`/`end`/`close`/`error` listener the module
104
+ * adds is removed, and the stream's paused and raw-mode state is restored to
105
+ * the pre-load snapshot.
82
106
  *
83
107
  * Nested and concurrent guard windows are safe: only the outermost guard
84
- * restores the real hard-exit APIs.
108
+ * snapshots and restores host state.
85
109
  */
86
110
  function guardedExit(alias: ExitAliasName): (code?: number | string) => never {
87
111
  return (code?: number | string): never => {
@@ -89,29 +113,71 @@ function guardedExit(alias: ExitAliasName): (code?: number | string) => never {
89
113
  };
90
114
  }
91
115
 
92
- export async function withExitGuard<T>(fn: () => Promise<T>): Promise<T> {
93
- if (exitGuardDepth === 0) {
94
- exitGuardOriginalProcessExit = process.exit;
116
+ export async function withHostGuard<T>(fn: () => Promise<T>): Promise<T> {
117
+ if (hostGuardDepth === 0) {
118
+ hostGuardOriginalProcessExit = process.exit;
95
119
  process.exit = guardedExit("process.exit") as typeof process.exit;
96
120
 
97
121
  if (typeof process.reallyExit === "function") {
98
- exitGuardOriginalReallyExit = process.reallyExit;
122
+ hostGuardOriginalReallyExit = process.reallyExit;
99
123
  process.reallyExit = guardedExit("process.reallyExit") as typeof process.reallyExit;
100
124
  }
125
+
126
+ const stdin = process.stdin;
127
+ hostGuardStdinWasPaused = stdin.isPaused();
128
+ hostGuardStdinWasRaw = stdin.isRaw ?? false;
129
+ const snapshot = {} as Record<StdinGuardEvent, StdinGuardListener[]>;
130
+ for (const event of HOST_GUARD_STDIN_EVENTS) {
131
+ snapshot[event] = stdin.rawListeners(event) as StdinGuardListener[];
132
+ }
133
+ hostGuardStdinListeners = snapshot;
101
134
  }
102
- exitGuardDepth++;
135
+ hostGuardDepth++;
103
136
  try {
104
137
  return await fn();
105
138
  } finally {
106
- exitGuardDepth--;
107
- if (exitGuardDepth === 0) {
108
- if (exitGuardOriginalProcessExit) {
109
- process.exit = exitGuardOriginalProcessExit;
110
- exitGuardOriginalProcessExit = null;
139
+ hostGuardDepth--;
140
+ if (hostGuardDepth === 0) {
141
+ if (hostGuardOriginalProcessExit) {
142
+ process.exit = hostGuardOriginalProcessExit;
143
+ hostGuardOriginalProcessExit = null;
144
+ }
145
+ if (hostGuardOriginalReallyExit) {
146
+ process.reallyExit = hostGuardOriginalReallyExit;
147
+ hostGuardOriginalReallyExit = null;
111
148
  }
112
- if (exitGuardOriginalReallyExit) {
113
- process.reallyExit = exitGuardOriginalReallyExit;
114
- exitGuardOriginalReallyExit = null;
149
+ if (hostGuardStdinListeners) {
150
+ const stdin = process.stdin;
151
+ for (const event of HOST_GUARD_STDIN_EVENTS) {
152
+ const before = hostGuardStdinListeners[event];
153
+ // Reconcile the stream back to the pre-load snapshot: drop any
154
+ // listener the module added, and reinstate any snapshot listener
155
+ // it removed (e.g. a factory calling `removeAllListeners("data")`
156
+ // would otherwise permanently strip ProcessTerminal's input
157
+ // handler, leaving the parent TUI deaf). removeAllListeners then
158
+ // re-adding in snapshot order restores both membership and order.
159
+ const current = stdin.rawListeners(event) as StdinGuardListener[];
160
+ const differs =
161
+ current.length !== before.length || current.some((listener, index) => listener !== before[index]);
162
+ if (!differs) continue;
163
+ stdin.removeAllListeners(event);
164
+ for (const listener of before) {
165
+ stdin.on(event, listener);
166
+ }
167
+ }
168
+ if (
169
+ stdin.isTTY &&
170
+ typeof stdin.setRawMode === "function" &&
171
+ (stdin.isRaw ?? false) !== hostGuardStdinWasRaw
172
+ ) {
173
+ stdin.setRawMode(hostGuardStdinWasRaw);
174
+ }
175
+ if (hostGuardStdinWasPaused && !stdin.isPaused()) {
176
+ stdin.pause();
177
+ } else if (!hostGuardStdinWasPaused && stdin.isPaused()) {
178
+ stdin.resume();
179
+ }
180
+ hostGuardStdinListeners = null;
115
181
  }
116
182
  }
117
183
  }