@oh-my-pi/pi-coding-agent 17.0.1 → 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 (126) hide show
  1. package/CHANGELOG.md +92 -20
  2. package/dist/cli.js +3485 -3448
  3. package/dist/types/advisor/config.d.ts +14 -6
  4. package/dist/types/advisor/runtime.d.ts +20 -11
  5. package/dist/types/autolearn/controller.d.ts +1 -0
  6. package/dist/types/config/model-discovery.d.ts +17 -0
  7. package/dist/types/config/model-resolver.d.ts +6 -2
  8. package/dist/types/config/settings-schema.d.ts +32 -7
  9. package/dist/types/config/settings.d.ts +50 -0
  10. package/dist/types/cursor.d.ts +11 -0
  11. package/dist/types/discovery/helpers.d.ts +5 -2
  12. package/dist/types/exec/bash-executor.d.ts +2 -0
  13. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  14. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  15. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  16. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
  17. package/dist/types/extensibility/shared-events.d.ts +6 -0
  18. package/dist/types/extensibility/utils.d.ts +2 -2
  19. package/dist/types/mcp/transports/stdio.d.ts +13 -1
  20. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  21. package/dist/types/modes/components/hook-editor.d.ts +7 -0
  22. package/dist/types/modes/components/model-hub.d.ts +5 -2
  23. package/dist/types/modes/components/session-selector.d.ts +2 -0
  24. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  25. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  26. package/dist/types/modes/print-mode.d.ts +1 -1
  27. package/dist/types/modes/warp-events.d.ts +24 -0
  28. package/dist/types/modes/warp-events.test.d.ts +1 -0
  29. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  30. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  31. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  32. package/dist/types/sdk.d.ts +13 -2
  33. package/dist/types/session/agent-session.d.ts +34 -10
  34. package/dist/types/session/session-history-format.d.ts +10 -0
  35. package/dist/types/session/session-manager.d.ts +14 -0
  36. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  37. package/dist/types/task/label.d.ts +1 -1
  38. package/dist/types/telemetry-export.d.ts +34 -9
  39. package/dist/types/tools/approval.d.ts +8 -0
  40. package/dist/types/tools/bash.d.ts +2 -0
  41. package/dist/types/tools/essential-tools.d.ts +29 -0
  42. package/dist/types/tools/image-gen.d.ts +2 -1
  43. package/dist/types/tools/index.d.ts +1 -0
  44. package/dist/types/tools/xdev.d.ts +11 -2
  45. package/dist/types/utils/title-generator.d.ts +2 -1
  46. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  47. package/dist/types/web/search/types.d.ts +1 -1
  48. package/package.json +21 -16
  49. package/src/advisor/__tests__/advisor.test.ts +1304 -42
  50. package/src/advisor/__tests__/config.test.ts +58 -2
  51. package/src/advisor/config.ts +76 -24
  52. package/src/advisor/runtime.ts +445 -92
  53. package/src/autolearn/controller.ts +23 -28
  54. package/src/cli.ts +5 -1
  55. package/src/config/model-discovery.ts +81 -21
  56. package/src/config/model-registry.ts +25 -6
  57. package/src/config/model-resolver.ts +14 -7
  58. package/src/config/settings-schema.ts +42 -6
  59. package/src/config/settings.ts +405 -25
  60. package/src/cursor.ts +20 -3
  61. package/src/debug/report-bundle.ts +40 -4
  62. package/src/discovery/helpers.ts +28 -5
  63. package/src/exec/bash-executor.ts +14 -5
  64. package/src/extensibility/custom-tools/loader.ts +3 -3
  65. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  66. package/src/extensibility/extensions/loader.ts +3 -3
  67. package/src/extensibility/extensions/managed-timers.ts +83 -0
  68. package/src/extensibility/extensions/runner.ts +26 -0
  69. package/src/extensibility/extensions/types.ts +18 -0
  70. package/src/extensibility/extensions/wrapper.ts +2 -1
  71. package/src/extensibility/hooks/loader.ts +3 -3
  72. package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
  73. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  74. package/src/extensibility/plugins/manager.ts +2 -2
  75. package/src/extensibility/shared-events.ts +6 -0
  76. package/src/extensibility/utils.ts +91 -25
  77. package/src/irc/bus.ts +22 -3
  78. package/src/launch/broker.ts +3 -2
  79. package/src/launch/client.ts +2 -2
  80. package/src/launch/presence.ts +2 -2
  81. package/src/lsp/client.ts +1 -1
  82. package/src/main.ts +11 -8
  83. package/src/mcp/manager.ts +9 -3
  84. package/src/mcp/transports/stdio.ts +103 -23
  85. package/src/modes/components/advisor-config.ts +65 -3
  86. package/src/modes/components/ask-dialog.ts +1 -1
  87. package/src/modes/components/hook-editor.ts +18 -3
  88. package/src/modes/components/model-hub.ts +138 -42
  89. package/src/modes/components/session-selector.ts +4 -0
  90. package/src/modes/components/status-line/component.test.ts +1 -0
  91. package/src/modes/components/status-line/segments.ts +21 -6
  92. package/src/modes/controllers/command-controller.ts +167 -47
  93. package/src/modes/controllers/event-controller.ts +5 -0
  94. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  95. package/src/modes/controllers/input-controller.ts +12 -12
  96. package/src/modes/controllers/selector-controller.ts +191 -31
  97. package/src/modes/interactive-mode.ts +139 -54
  98. package/src/modes/print-mode.ts +3 -3
  99. package/src/modes/rpc/host-tools.ts +2 -1
  100. package/src/modes/rpc/rpc-mode.ts +19 -4
  101. package/src/modes/warp-events.test.ts +794 -0
  102. package/src/modes/warp-events.ts +232 -0
  103. package/src/plan-mode/model-transition.test.ts +60 -0
  104. package/src/plan-mode/model-transition.ts +51 -0
  105. package/src/registry/agent-lifecycle.ts +133 -18
  106. package/src/sdk.ts +221 -42
  107. package/src/session/agent-session.ts +1285 -348
  108. package/src/session/session-history-format.ts +20 -5
  109. package/src/session/session-manager.ts +48 -0
  110. package/src/slash-commands/builtin-registry.ts +7 -0
  111. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  112. package/src/task/executor.ts +1 -1
  113. package/src/task/label.ts +2 -0
  114. package/src/telemetry-export.ts +453 -97
  115. package/src/tools/approval.ts +11 -0
  116. package/src/tools/bash.ts +71 -38
  117. package/src/tools/essential-tools.ts +45 -0
  118. package/src/tools/gh.ts +169 -2
  119. package/src/tools/image-gen.ts +69 -7
  120. package/src/tools/index.ts +7 -5
  121. package/src/tools/read.ts +48 -3
  122. package/src/tools/write.ts +22 -4
  123. package/src/tools/xdev.ts +14 -3
  124. package/src/utils/title-generator.ts +15 -4
  125. package/src/web/search/providers/kimi.ts +18 -12
  126. package/src/web/search/types.ts +6 -1
@@ -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
  }
package/src/irc/bus.ts CHANGED
@@ -127,12 +127,31 @@ export class IrcBus {
127
127
  };
128
128
  }
129
129
 
130
+ // A `parked` recipient always needs the lifecycle to revive it — this is
131
+ // read from *this* bus's registry, so it holds for any registry. The
132
+ // mid-park / adopted checks below query the lifecycle's own state, which
133
+ // only describes the registry it manages: consult them only when the
134
+ // lifecycle owns this bus's registry, otherwise a custom-registry bus
135
+ // (fallen back to the global manager) would gate a live recipient on
136
+ // unrelated global park state. Main/non-adopted live peers skip the gate,
137
+ // and pending waiters still win without a session.
138
+ const lifecycle = this.#lifecycle();
139
+ const lifecycleOwnsRegistry = lifecycle.manages(this.#registry);
140
+ const needsLifecycleGate =
141
+ ref.status === "parked" ||
142
+ (lifecycleOwnsRegistry && (lifecycle.isParking(message.to) || lifecycle.has(message.to)));
143
+
144
+ const priorSession = ref.session;
130
145
  let revived = false;
131
- if (ref.status === "parked") {
146
+ if (needsLifecycleGate) {
132
147
  try {
133
- await this.#lifecycle().ensureLive(message.to);
134
- revived = true;
148
+ const liveSession = await lifecycle.ensureLive(message.to);
149
+ // Revival = we did not keep the same live instance (parked start, or
150
+ // park completed and a fresh session was rebuilt).
151
+ revived = !priorSession || liveSession !== priorSession;
135
152
  } catch (error) {
153
+ // Not revivable / released / revive failed. Do not buffer: a permanent
154
+ // failure must not inflate unread counts or pretend delivery is pending.
136
155
  return {
137
156
  to: message.to,
138
157
  outcome: "failed",
@@ -3,7 +3,7 @@ import * as net from "node:net";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { Process, type PtyRunResult, PtySession } from "@oh-my-pi/pi-natives";
6
- import { isEexist, isEnoent, logger, postmortem, sanitizeText } from "@oh-my-pi/pi-utils";
6
+ import { isEexist, isEnoent, logger, postmortem, procmgr, sanitizeText } from "@oh-my-pi/pi-utils";
7
7
  import { truncateHead, truncateHeadBytes, truncateTail, truncateTailBytes } from "../session/streaming-output";
8
8
  import { workerEnvFromParent } from "../subprocess/worker-client";
9
9
  import { daemonBrokerEndpoint } from "./paths";
@@ -555,7 +555,8 @@ class DaemonBroker {
555
555
  `printf '%s' "$$" > ${quoteShellArg(pidPath)}`,
556
556
  `exec ${argv.map(quoteShellArg).join(" ")}`,
557
557
  ].join("; ");
558
- run = session.start({ command, shell: process.env.SHELL, ...options }, onChunk);
558
+ const shell = procmgr.getShellConfig().shell;
559
+ run = session.start({ command, shell, ...options }, onChunk);
559
560
  }
560
561
  void run
561
562
  .then(result => this.#onPtyExit(record, generation, result))
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as net from "node:net";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
- import { isEexist, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
5
+ import { isEexist, isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
6
6
  import { resolveWorkerSpawnCmd, workerEnvFromParent } from "../subprocess/worker-client";
7
7
  import { daemonBrokerEndpoint, daemonRuntimeDir } from "./paths";
8
8
  import {
@@ -49,7 +49,7 @@ async function canonicalProjectDir(projectDir: string): Promise<string> {
49
49
  try {
50
50
  return await fs.realpath(resolved);
51
51
  } catch (error) {
52
- if (isEnoent(error)) return resolved;
52
+ if (isEnoent(error) || isEisdir(error)) return resolved;
53
53
  throw error;
54
54
  }
55
55
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { isEnoent, postmortem } from "@oh-my-pi/pi-utils";
3
+ import { isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
4
4
  import { daemonRuntimeDir } from "./paths";
5
5
 
6
6
  const CLIENTS_DIR = "clients";
@@ -15,7 +15,7 @@ async function canonicalProjectDir(projectDir: string): Promise<string> {
15
15
  try {
16
16
  return await fs.realpath(resolved);
17
17
  } catch (error) {
18
- if (isEnoent(error)) return resolved;
18
+ if (isEnoent(error) || isEisdir(error)) return resolved;
19
19
  throw error;
20
20
  }
21
21
  }
package/src/lsp/client.ts CHANGED
@@ -428,7 +428,7 @@ async function handleConfigurationRequest(client: LspClient, message: LspJsonRpc
428
428
  const items = params?.items ?? [];
429
429
  const result = items.map(item => {
430
430
  const section = item.section ?? "";
431
- return client.config.settings?.[section] ?? {};
431
+ return client.config.settings?.[section] ?? null;
432
432
  });
433
433
  await sendResponse(client, message.id, result, "workspace/configuration");
434
434
  }
package/src/main.ts CHANGED
@@ -59,6 +59,7 @@ import type { PrintModeOptions } from "./modes/print-mode";
59
59
  import { CURRENT_SETUP_VERSION } from "./modes/setup-version";
60
60
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
61
61
  import type { SubmittedUserInput } from "./modes/types";
62
+ import { createWarpEventBridgeExtension } from "./modes/warp-events";
62
63
  import { AgentLifecycleManager } from "./registry/agent-lifecycle";
63
64
  import {
64
65
  type CreateAgentSessionOptions,
@@ -76,7 +77,7 @@ import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
76
77
  import { shouldShowStartupSplash } from "./startup-splash";
77
78
  import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
78
79
  import { createPersistedSubagentReviverFactory } from "./task/persisted-revive";
79
- import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
80
+ import { createTelemetryExportConfig, initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
80
81
  import { concreteThinkingLevel, parseConfiguredThinkingLevel } from "./thinking";
81
82
  import type { LspStartupServerInfo } from "./tools";
82
83
  import {
@@ -1350,15 +1351,13 @@ export async function runRootCommand(
1350
1351
  sessionOptions.hasUI = isInteractive || mode === "rpc-ui";
1351
1352
  sessionOptions.settings = settingsInstance;
1352
1353
 
1353
- // OTEL: register the global OTLP trace exporter when an OTLP endpoint is
1354
- // configured via env, then switch on the agent loop's telemetry so its
1355
- // GenAI spans (invoke_agent / chat / execute_tool) are actually emitted.
1356
- // Both are no-ops when OTEL_EXPORTER_OTLP_ENDPOINT is unset. An empty config
1357
- // is enough to enable telemetry — content capture is governed by the
1358
- // standard OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var.
1354
+ // OTEL: register global OTLP exporters when an endpoint is configured via
1355
+ // env, then switch on the agent loop's telemetry hooks so traces, run-level
1356
+ // metrics, and structured logs have source events to export. Content capture
1357
+ // remains governed by OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.
1359
1358
  await logger.time("initTelemetryExport", initTelemetryExport);
1360
1359
  if (isTelemetryExportEnabled()) {
1361
- sessionOptions.telemetry = {};
1360
+ sessionOptions.telemetry = createTelemetryExportConfig(sessionOptions.telemetry);
1362
1361
  }
1363
1362
 
1364
1363
  // Handle CLI --api-key as runtime override (not persisted)
@@ -1407,6 +1406,10 @@ export async function runRootCommand(
1407
1406
  // string-flag value such as `--target @notes.md` is the flag's value, not a
1408
1407
  // file — and the same result is handed to createAgentSession via
1409
1408
  // `preloadedExtensions` so the discovery work is not repeated.
1409
+ if (isInteractive) {
1410
+ sessionOptions.extensions = [...(sessionOptions.extensions ?? []), createWarpEventBridgeExtension()];
1411
+ }
1412
+
1410
1413
  const eventBus = new EventBus();
1411
1414
  const extensionsResult = await loadSessionExtensions(sessionOptions, cwd, settingsInstance, eventBus);
1412
1415
  const extensionFlagSink: ExtensionFlagSink = {