@oh-my-pi/pi-coding-agent 1.341.0 → 2.1.1337

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 (158) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +10 -9
  5. package/src/bun-imports.d.ts +16 -0
  6. package/src/cli/args.ts +5 -6
  7. package/src/cli/file-processor.ts +3 -3
  8. package/src/cli/list-models.ts +2 -2
  9. package/src/cli/plugin-cli.ts +1 -1
  10. package/src/cli/session-picker.ts +2 -2
  11. package/src/cli/update-cli.ts +273 -0
  12. package/src/cli.ts +1 -1
  13. package/src/config.ts +23 -75
  14. package/src/core/agent-session.ts +158 -16
  15. package/src/core/auth-storage.ts +2 -3
  16. package/src/core/bash-executor.ts +50 -10
  17. package/src/core/compaction/branch-summarization.ts +5 -5
  18. package/src/core/compaction/compaction.ts +3 -3
  19. package/src/core/compaction/index.ts +3 -3
  20. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  21. package/src/core/custom-commands/index.ts +15 -0
  22. package/src/core/custom-commands/loader.ts +232 -0
  23. package/src/core/custom-commands/types.ts +112 -0
  24. package/src/core/custom-tools/index.ts +3 -3
  25. package/src/core/custom-tools/loader.ts +10 -8
  26. package/src/core/custom-tools/types.ts +11 -6
  27. package/src/core/custom-tools/wrapper.ts +2 -1
  28. package/src/core/exec.ts +22 -12
  29. package/src/core/export-html/index.ts +38 -123
  30. package/src/core/export-html/template.css +0 -7
  31. package/src/core/export-html/template.html +3 -4
  32. package/src/core/export-html/template.macro.ts +24 -0
  33. package/src/core/file-mentions.ts +54 -0
  34. package/src/core/hooks/index.ts +5 -5
  35. package/src/core/hooks/loader.ts +21 -16
  36. package/src/core/hooks/runner.ts +6 -6
  37. package/src/core/hooks/tool-wrapper.ts +2 -2
  38. package/src/core/hooks/types.ts +12 -15
  39. package/src/core/index.ts +6 -6
  40. package/src/core/logger.ts +112 -0
  41. package/src/core/mcp/client.ts +3 -3
  42. package/src/core/mcp/config.ts +1 -1
  43. package/src/core/mcp/index.ts +12 -12
  44. package/src/core/mcp/loader.ts +2 -2
  45. package/src/core/mcp/manager.ts +6 -6
  46. package/src/core/mcp/tool-bridge.ts +3 -3
  47. package/src/core/mcp/transports/http.ts +1 -1
  48. package/src/core/mcp/transports/index.ts +2 -2
  49. package/src/core/mcp/transports/stdio.ts +1 -1
  50. package/src/core/messages.ts +22 -0
  51. package/src/core/model-registry.ts +2 -2
  52. package/src/core/model-resolver.ts +2 -2
  53. package/src/core/plugins/doctor.ts +1 -1
  54. package/src/core/plugins/index.ts +6 -6
  55. package/src/core/plugins/installer.ts +4 -4
  56. package/src/core/plugins/loader.ts +4 -9
  57. package/src/core/plugins/manager.ts +5 -5
  58. package/src/core/plugins/paths.ts +3 -3
  59. package/src/core/sdk.ts +77 -35
  60. package/src/core/session-manager.ts +6 -6
  61. package/src/core/settings-manager.ts +16 -3
  62. package/src/core/skills.ts +5 -5
  63. package/src/core/slash-commands.ts +60 -45
  64. package/src/core/system-prompt.ts +6 -6
  65. package/src/core/title-generator.ts +2 -2
  66. package/src/core/tools/bash.ts +32 -155
  67. package/src/core/tools/context.ts +2 -2
  68. package/src/core/tools/edit-diff.ts +3 -3
  69. package/src/core/tools/edit.ts +18 -5
  70. package/src/core/tools/exa/company.ts +3 -3
  71. package/src/core/tools/exa/index.ts +16 -17
  72. package/src/core/tools/exa/linkedin.ts +3 -3
  73. package/src/core/tools/exa/mcp-client.ts +9 -9
  74. package/src/core/tools/exa/render.ts +5 -5
  75. package/src/core/tools/exa/researcher.ts +3 -3
  76. package/src/core/tools/exa/search.ts +6 -5
  77. package/src/core/tools/exa/types.ts +5 -6
  78. package/src/core/tools/exa/websets.ts +3 -3
  79. package/src/core/tools/find.ts +3 -3
  80. package/src/core/tools/grep.ts +3 -3
  81. package/src/core/tools/index.ts +48 -34
  82. package/src/core/tools/ls.ts +4 -4
  83. package/src/core/tools/lsp/client.ts +161 -90
  84. package/src/core/tools/lsp/config.ts +1 -1
  85. package/src/core/tools/lsp/edits.ts +2 -2
  86. package/src/core/tools/lsp/index.ts +15 -13
  87. package/src/core/tools/lsp/render.ts +2 -2
  88. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  89. package/src/core/tools/lsp/utils.ts +1 -1
  90. package/src/core/tools/notebook.ts +1 -1
  91. package/src/core/tools/output.ts +175 -0
  92. package/src/core/tools/read.ts +7 -7
  93. package/src/core/tools/renderers.ts +92 -13
  94. package/src/core/tools/review.ts +268 -0
  95. package/src/core/tools/task/agents.ts +22 -38
  96. package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
  97. package/src/core/tools/task/commands.ts +31 -10
  98. package/src/core/tools/task/discovery.ts +2 -2
  99. package/src/core/tools/task/executor.ts +145 -28
  100. package/src/core/tools/task/index.ts +78 -30
  101. package/src/core/tools/task/model-resolver.ts +30 -20
  102. package/src/core/tools/task/parallel.ts +1 -1
  103. package/src/core/tools/task/render.ts +219 -30
  104. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  105. package/src/core/tools/task/types.ts +36 -2
  106. package/src/core/tools/web-fetch.ts +5 -3
  107. package/src/core/tools/web-search/auth.ts +1 -1
  108. package/src/core/tools/web-search/index.ts +17 -15
  109. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  110. package/src/core/tools/web-search/providers/exa.ts +3 -5
  111. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  112. package/src/core/tools/web-search/render.ts +3 -3
  113. package/src/core/tools/write.ts +4 -4
  114. package/src/index.ts +29 -18
  115. package/src/main.ts +50 -33
  116. package/src/migrations.ts +3 -3
  117. package/src/modes/index.ts +5 -5
  118. package/src/modes/interactive/components/armin.ts +1 -1
  119. package/src/modes/interactive/components/assistant-message.ts +1 -1
  120. package/src/modes/interactive/components/bash-execution.ts +4 -4
  121. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  122. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  123. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  124. package/src/modes/interactive/components/diff.ts +1 -1
  125. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  126. package/src/modes/interactive/components/footer.ts +5 -5
  127. package/src/modes/interactive/components/hook-editor.ts +2 -2
  128. package/src/modes/interactive/components/hook-input.ts +2 -2
  129. package/src/modes/interactive/components/hook-message.ts +3 -3
  130. package/src/modes/interactive/components/hook-selector.ts +2 -2
  131. package/src/modes/interactive/components/model-selector.ts +281 -59
  132. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  133. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  134. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  135. package/src/modes/interactive/components/session-selector.ts +4 -4
  136. package/src/modes/interactive/components/settings-defs.ts +1 -1
  137. package/src/modes/interactive/components/settings-selector.ts +5 -5
  138. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  139. package/src/modes/interactive/components/theme-selector.ts +2 -2
  140. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  141. package/src/modes/interactive/components/tool-execution.ts +26 -8
  142. package/src/modes/interactive/components/tree-selector.ts +3 -3
  143. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  144. package/src/modes/interactive/components/user-message.ts +1 -1
  145. package/src/modes/interactive/components/welcome.ts +2 -2
  146. package/src/modes/interactive/interactive-mode.ts +86 -42
  147. package/src/modes/interactive/theme/theme.ts +15 -17
  148. package/src/modes/print-mode.ts +4 -3
  149. package/src/modes/rpc/rpc-client.ts +4 -4
  150. package/src/modes/rpc/rpc-mode.ts +22 -12
  151. package/src/modes/rpc/rpc-types.ts +3 -3
  152. package/src/utils/changelog.ts +2 -2
  153. package/src/utils/clipboard.ts +1 -1
  154. package/src/utils/shell-snapshot.ts +218 -0
  155. package/src/utils/shell.ts +93 -13
  156. package/src/utils/tools-manager.ts +1 -1
  157. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  158. package/src/core/tools/exa/logger.ts +0 -56
@@ -1,5 +1,5 @@
1
1
  import { Container, Markdown, Spacer } from "@oh-my-pi/pi-tui";
2
- import { getMarkdownTheme, theme } from "../theme/theme.js";
2
+ import { getMarkdownTheme, theme } from "../theme/theme";
3
3
 
4
4
  /**
5
5
  * Component that renders a user message
@@ -1,6 +1,6 @@
1
1
  import { type Component, visibleWidth } from "@oh-my-pi/pi-tui";
2
- import { APP_NAME } from "../../../config.js";
3
- import { theme } from "../theme/theme.js";
2
+ import { APP_NAME } from "../../../config";
3
+ import { theme } from "../theme/theme";
4
4
 
5
5
  export interface RecentSession {
6
6
  name: string;
@@ -23,40 +23,40 @@ import {
23
23
  TUI,
24
24
  visibleWidth,
25
25
  } from "@oh-my-pi/pi-tui";
26
- import { getAuthPath, getDebugLogPath } from "../../config.js";
27
- import type { AgentSession, AgentSessionEvent } from "../../core/agent-session.js";
28
- import type { CustomToolSessionEvent, LoadedCustomTool } from "../../core/custom-tools/index.js";
29
- import type { HookUIContext } from "../../core/hooks/index.js";
30
- import { createCompactionSummaryMessage } from "../../core/messages.js";
31
- import { getRecentSessions, type SessionContext, SessionManager } from "../../core/session-manager.js";
32
- import { loadSkills } from "../../core/skills.js";
33
- import { loadProjectContextFiles } from "../../core/system-prompt.js";
34
- import { generateSessionTitle, setTerminalTitle } from "../../core/title-generator.js";
35
- import type { TruncationResult } from "../../core/tools/truncate.js";
36
- import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
37
- import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard.js";
38
- import { ArminComponent } from "./components/armin.js";
39
- import { AssistantMessageComponent } from "./components/assistant-message.js";
40
- import { BashExecutionComponent } from "./components/bash-execution.js";
41
- import { BorderedLoader } from "./components/bordered-loader.js";
42
- import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
43
- import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
44
- import { CustomEditor } from "./components/custom-editor.js";
45
- import { DynamicBorder } from "./components/dynamic-border.js";
46
- import { FooterComponent } from "./components/footer.js";
47
- import { HookEditorComponent } from "./components/hook-editor.js";
48
- import { HookInputComponent } from "./components/hook-input.js";
49
- import { HookMessageComponent } from "./components/hook-message.js";
50
- import { HookSelectorComponent } from "./components/hook-selector.js";
51
- import { ModelSelectorComponent } from "./components/model-selector.js";
52
- import { OAuthSelectorComponent } from "./components/oauth-selector.js";
53
- import { SessionSelectorComponent } from "./components/session-selector.js";
54
- import { SettingsSelectorComponent } from "./components/settings-selector.js";
55
- import { ToolExecutionComponent } from "./components/tool-execution.js";
56
- import { TreeSelectorComponent } from "./components/tree-selector.js";
57
- import { UserMessageComponent } from "./components/user-message.js";
58
- import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
59
- import { WelcomeComponent } from "./components/welcome.js";
26
+ import { getAuthPath, getDebugLogPath } from "../../config";
27
+ import type { AgentSession, AgentSessionEvent } from "../../core/agent-session";
28
+ import type { CustomToolSessionEvent, LoadedCustomTool } from "../../core/custom-tools/index";
29
+ import type { HookUIContext } from "../../core/hooks/index";
30
+ import { createCompactionSummaryMessage } from "../../core/messages";
31
+ import { getRecentSessions, type SessionContext, SessionManager } from "../../core/session-manager";
32
+ import { loadSkills } from "../../core/skills";
33
+ import { loadProjectContextFiles } from "../../core/system-prompt";
34
+ import { generateSessionTitle, setTerminalTitle } from "../../core/title-generator";
35
+ import type { TruncationResult } from "../../core/tools/truncate";
36
+ import { getChangelogPath, parseChangelog } from "../../utils/changelog";
37
+ import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
38
+ import { ArminComponent } from "./components/armin";
39
+ import { AssistantMessageComponent } from "./components/assistant-message";
40
+ import { BashExecutionComponent } from "./components/bash-execution";
41
+ import { BorderedLoader } from "./components/bordered-loader";
42
+ import { BranchSummaryMessageComponent } from "./components/branch-summary-message";
43
+ import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message";
44
+ import { CustomEditor } from "./components/custom-editor";
45
+ import { DynamicBorder } from "./components/dynamic-border";
46
+ import { FooterComponent } from "./components/footer";
47
+ import { HookEditorComponent } from "./components/hook-editor";
48
+ import { HookInputComponent } from "./components/hook-input";
49
+ import { HookMessageComponent } from "./components/hook-message";
50
+ import { HookSelectorComponent } from "./components/hook-selector";
51
+ import { ModelSelectorComponent } from "./components/model-selector";
52
+ import { OAuthSelectorComponent } from "./components/oauth-selector";
53
+ import { SessionSelectorComponent } from "./components/session-selector";
54
+ import { SettingsSelectorComponent } from "./components/settings-selector";
55
+ import { ToolExecutionComponent } from "./components/tool-execution";
56
+ import { TreeSelectorComponent } from "./components/tree-selector";
57
+ import { UserMessageComponent } from "./components/user-message";
58
+ import { UserMessageSelectorComponent } from "./components/user-message-selector";
59
+ import { WelcomeComponent } from "./components/welcome";
60
60
  import {
61
61
  getAvailableThemes,
62
62
  getEditorTheme,
@@ -65,7 +65,7 @@ import {
65
65
  setTheme,
66
66
  type Theme,
67
67
  theme,
68
- } from "./theme/theme.js";
68
+ } from "./theme/theme";
69
69
 
70
70
  /** Interface for components that can be expanded/collapsed */
71
71
  interface Expandable {
@@ -185,7 +185,7 @@ export class InteractiveMode {
185
185
  const slashCommands: SlashCommand[] = [
186
186
  { name: "settings", description: "Open settings menu" },
187
187
  { name: "model", description: "Select model (opens selector UI)" },
188
- { name: "export", description: "Export session to HTML file" },
188
+ { name: "export", description: "Export session to HTML file or clipboard (--copy)" },
189
189
  { name: "share", description: "Share session as a secret GitHub gist" },
190
190
  { name: "copy", description: "Copy last agent message to clipboard" },
191
191
  { name: "session", description: "Show session info and stats" },
@@ -216,9 +216,15 @@ export class InteractiveMode {
216
216
  description: cmd.description ?? "(hook command)",
217
217
  }));
218
218
 
219
+ // Convert custom commands (TypeScript) to SlashCommand format
220
+ const customCommands: SlashCommand[] = this.session.customCommands.map((loaded) => ({
221
+ name: loaded.command.name,
222
+ description: `${loaded.command.description} (${loaded.source})`,
223
+ }));
224
+
219
225
  // Setup autocomplete
220
226
  const autocompleteProvider = new CombinedAutocompleteProvider(
221
- [...slashCommands, ...fileSlashCommands, ...hookCommands],
227
+ [...slashCommands, ...fileSlashCommands, ...hookCommands, ...customCommands],
222
228
  process.cwd(),
223
229
  fdPath,
224
230
  );
@@ -745,7 +751,7 @@ export class InteractiveMode {
745
751
  return;
746
752
  }
747
753
  if (text.startsWith("/export")) {
748
- this.handleExportCommand(text);
754
+ await this.handleExportCommand(text);
749
755
  this.editor.setText("");
750
756
  return;
751
757
  }
@@ -867,6 +873,19 @@ export class InteractiveMode {
867
873
  }
868
874
  }
869
875
 
876
+ // Custom commands (TypeScript slash commands) - route through session.prompt()
877
+ if (text.startsWith("/") && this.session.customCommands.length > 0) {
878
+ const spaceIndex = text.indexOf(" ");
879
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
880
+ const hasCustomCommand = this.session.customCommands.some((c) => c.command.name === commandName);
881
+ if (hasCustomCommand) {
882
+ this.editor.addToHistory(text);
883
+ this.editor.setText("");
884
+ await this.session.prompt(text);
885
+ return;
886
+ }
887
+ }
888
+
870
889
  // Queue regular messages if agent is streaming
871
890
  if (this.session.isStreaming) {
872
891
  await this.session.queueMessage(text);
@@ -1232,6 +1251,14 @@ export class InteractiveMode {
1232
1251
  this.chatContainer.addChild(component);
1233
1252
  break;
1234
1253
  }
1254
+ case "fileMention": {
1255
+ // Render compact file mention display
1256
+ for (const file of message.files) {
1257
+ const text = `${theme.fg("dim", "⎿ ")}${theme.fg("muted", "Read")} ${theme.fg("accent", file.path)} ${theme.fg("dim", `(${file.lineCount} lines)`)}`;
1258
+ this.chatContainer.addChild(new Text(text, 0, 0));
1259
+ }
1260
+ break;
1261
+ }
1235
1262
  case "user": {
1236
1263
  const textContent = this.getUserMessageText(message);
1237
1264
  if (textContent) {
@@ -2081,12 +2108,29 @@ export class InteractiveMode {
2081
2108
  // Command handlers
2082
2109
  // =========================================================================
2083
2110
 
2084
- private handleExportCommand(text: string): void {
2111
+ private async handleExportCommand(text: string): Promise<void> {
2085
2112
  const parts = text.split(/\s+/);
2086
- const outputPath = parts.length > 1 ? parts[1] : undefined;
2113
+ const arg = parts.length > 1 ? parts[1] : undefined;
2114
+
2115
+ // Check for clipboard export
2116
+ if (arg === "--copy" || arg === "clipboard" || arg === "copy") {
2117
+ try {
2118
+ const formatted = this.session.formatSessionAsText();
2119
+ if (!formatted) {
2120
+ this.showError("No messages to export yet.");
2121
+ return;
2122
+ }
2123
+ await copyToClipboard(formatted);
2124
+ this.showStatus("Session copied to clipboard");
2125
+ } catch (error: unknown) {
2126
+ this.showError(`Failed to copy session: ${error instanceof Error ? error.message : "Unknown error"}`);
2127
+ }
2128
+ return;
2129
+ }
2087
2130
 
2131
+ // HTML file export
2088
2132
  try {
2089
- const filePath = this.session.exportToHtml(outputPath);
2133
+ const filePath = await this.session.exportToHtml(arg);
2090
2134
  this.showStatus(`Session exported to: ${filePath}`);
2091
2135
  } catch (error: unknown) {
2092
2136
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -2109,7 +2153,7 @@ export class InteractiveMode {
2109
2153
  // Export to a temp file
2110
2154
  const tmpFile = path.join(os.tmpdir(), "session.html");
2111
2155
  try {
2112
- this.session.exportToHtml(tmpFile);
2156
+ await this.session.exportToHtml(tmpFile);
2113
2157
  } catch (error: unknown) {
2114
2158
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
2115
2159
  return;
@@ -5,7 +5,11 @@ import { type Static, Type } from "@sinclair/typebox";
5
5
  import { TypeCompiler } from "@sinclair/typebox/compiler";
6
6
  import chalk from "chalk";
7
7
  import { highlight, supportsLanguage } from "cli-highlight";
8
- import { getCustomThemesDir, getThemesDir } from "../../../config.js";
8
+ import { getCustomThemesDir } from "../../../config";
9
+ import { logger } from "../../../core/logger";
10
+ // Embed theme JSON files at build time
11
+ import darkThemeJson from "./dark.json" with { type: "json" };
12
+ import lightThemeJson from "./light.json" with { type: "json" };
9
13
 
10
14
  // ============================================================================
11
15
  // Types & Schema
@@ -449,18 +453,12 @@ export class Theme {
449
453
  // Theme Loading
450
454
  // ============================================================================
451
455
 
452
- let BUILTIN_THEMES: Record<string, ThemeJson> | undefined;
456
+ const BUILTIN_THEMES: Record<string, ThemeJson> = {
457
+ dark: darkThemeJson as ThemeJson,
458
+ light: lightThemeJson as ThemeJson,
459
+ };
453
460
 
454
461
  function getBuiltinThemes(): Record<string, ThemeJson> {
455
- if (!BUILTIN_THEMES) {
456
- const themesDir = getThemesDir();
457
- const darkPath = path.join(themesDir, "dark.json");
458
- const lightPath = path.join(themesDir, "light.json");
459
- BUILTIN_THEMES = {
460
- dark: JSON.parse(fs.readFileSync(darkPath, "utf-8")) as ThemeJson,
461
- light: JSON.parse(fs.readFileSync(lightPath, "utf-8")) as ThemeJson,
462
- };
463
- }
464
462
  return BUILTIN_THEMES;
465
463
  }
466
464
 
@@ -590,8 +588,8 @@ export function initTheme(themeName?: string, enableWatcher: boolean = false): v
590
588
  if (enableWatcher) {
591
589
  startThemeWatcher();
592
590
  }
593
- } catch (_error) {
594
- // Theme is invalid - fall back to dark theme silently
591
+ } catch (err) {
592
+ logger.debug("Theme loading failed, falling back to dark theme", { error: String(err) });
595
593
  currentThemeName = "dark";
596
594
  theme = loadTheme("dark");
597
595
  // Don't start watcher for fallback theme
@@ -654,8 +652,8 @@ function startThemeWatcher(): void {
654
652
  if (onThemeChangeCallback) {
655
653
  onThemeChangeCallback();
656
654
  }
657
- } catch (_error) {
658
- // Ignore errors (file might be in invalid state while being edited)
655
+ } catch (err) {
656
+ logger.debug("Theme reload error during file change", { error: String(err) });
659
657
  }
660
658
  }, 100);
661
659
  } else if (eventType === "rename") {
@@ -675,8 +673,8 @@ function startThemeWatcher(): void {
675
673
  }, 100);
676
674
  }
677
675
  });
678
- } catch (_error) {
679
- // Ignore errors starting watcher
676
+ } catch (err) {
677
+ logger.debug("Failed to start theme watcher", { error: String(err) });
680
678
  }
681
679
  }
682
680
 
@@ -7,7 +7,8 @@
7
7
  */
8
8
 
9
9
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
10
- import type { AgentSession } from "../core/agent-session.js";
10
+ import type { AgentSession } from "../core/agent-session";
11
+ import { logger } from "../core/logger";
11
12
 
12
13
  /**
13
14
  * Run in print (single-shot) mode.
@@ -70,8 +71,8 @@ export async function runPrintMode(
70
71
  },
71
72
  },
72
73
  );
73
- } catch (_err) {
74
- // Silently ignore tool errors
74
+ } catch (err) {
75
+ logger.warn("Tool onSession error", { error: String(err) });
75
76
  }
76
77
  }
77
78
  }
@@ -7,10 +7,10 @@
7
7
  import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
8
  import type { ImageContent } from "@oh-my-pi/pi-ai";
9
9
  import type { Subprocess } from "bun";
10
- import type { SessionStats } from "../../core/agent-session.js";
11
- import type { BashResult } from "../../core/bash-executor.js";
12
- import type { CompactionResult } from "../../core/compaction/index.js";
13
- import type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc-types.js";
10
+ import type { SessionStats } from "../../core/agent-session";
11
+ import type { BashResult } from "../../core/bash-executor";
12
+ import type { CompactionResult } from "../../core/compaction/index";
13
+ import type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc-types";
14
14
 
15
15
  // ============================================================================
16
16
  // Types
@@ -11,13 +11,14 @@
11
11
  * - Hook UI: Hook UI requests are emitted, client responds with hook_ui_response
12
12
  */
13
13
 
14
- import type { AgentSession } from "../../core/agent-session.js";
15
- import type { HookUIContext } from "../../core/hooks/index.js";
16
- import { theme } from "../interactive/theme/theme.js";
17
- import type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types.js";
14
+ import type { AgentSession } from "../../core/agent-session";
15
+ import type { HookUIContext } from "../../core/hooks/index";
16
+ import { logger } from "../../core/logger";
17
+ import { theme } from "../interactive/theme/theme";
18
+ import type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types";
18
19
 
19
20
  // Re-export types for consumers
20
- export type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types.js";
21
+ export type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types";
21
22
 
22
23
  /**
23
24
  * Run in RPC mode.
@@ -220,8 +221,8 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
220
221
  },
221
222
  },
222
223
  );
223
- } catch (_err) {
224
- // Silently ignore tool errors
224
+ } catch (err) {
225
+ logger.warn("Tool onSession error", { error: String(err) });
225
226
  }
226
227
  }
227
228
  }
@@ -231,6 +232,9 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
231
232
  output(event);
232
233
  });
233
234
 
235
+ // Serialize prompt commands to prevent concurrent execution
236
+ let activePrompt: Promise<void> | null = null;
237
+
234
238
  // Handle a single command
235
239
  const handleCommand = async (command: RpcCommand): Promise<RpcResponse> => {
236
240
  const id = command.id;
@@ -241,13 +245,18 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
241
245
  // =================================================================
242
246
 
243
247
  case "prompt": {
244
- // Don't await - events will stream
245
- // Hook commands and file slash commands are handled in session.prompt()
246
- session
248
+ // Serialize prompts to prevent concurrent execution
249
+ if (activePrompt) {
250
+ await activePrompt;
251
+ }
252
+ activePrompt = session
247
253
  .prompt(command.message, {
248
254
  images: command.images,
249
255
  })
250
- .catch((e) => output(error(id, "prompt", e.message)));
256
+ .catch((e) => output(error(id, "prompt", e.message)))
257
+ .finally(() => {
258
+ activePrompt = null;
259
+ });
251
260
  return success(id, "prompt");
252
261
  }
253
262
 
@@ -392,7 +401,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
392
401
  }
393
402
 
394
403
  case "export_html": {
395
- const path = session.exportToHtml(command.outputPath);
404
+ const path = await session.exportToHtml(command.outputPath);
396
405
  return success(id, "export_html", { path });
397
406
  }
398
407
 
@@ -462,6 +471,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
462
471
  const response = parsed as RpcHookUIResponse;
463
472
  const pending = pendingHookRequests.get(response.id);
464
473
  if (pending) {
474
+ // Atomic delete: remove before resolve to prevent double-resolution
465
475
  pendingHookRequests.delete(response.id);
466
476
  pending.resolve(response);
467
477
  }
@@ -7,9 +7,9 @@
7
7
 
8
8
  import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
9
9
  import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
10
- import type { SessionStats } from "../../core/agent-session.js";
11
- import type { BashResult } from "../../core/bash-executor.js";
12
- import type { CompactionResult } from "../../core/compaction/index.js";
10
+ import type { SessionStats } from "../../core/agent-session";
11
+ import type { BashResult } from "../../core/bash-executor";
12
+ import type { CompactionResult } from "../../core/compaction/index";
13
13
 
14
14
  // ============================================================================
15
15
  // RPC Commands (stdin)
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
 
3
3
  export interface ChangelogEntry {
4
4
  major: number;
@@ -96,4 +96,4 @@ export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): C
96
96
  }
97
97
 
98
98
  // Re-export getChangelogPath from paths.ts for convenience
99
- export { getChangelogPath } from "../config.js";
99
+ export { getChangelogPath } from "../config";
@@ -1,4 +1,4 @@
1
- import { platform } from "os";
1
+ import { platform } from "node:os";
2
2
 
3
3
  async function spawnWithTimeout(cmd: string[], input: string, timeoutMs: number): Promise<void> {
4
4
  const proc = Bun.spawn(cmd, { stdin: "pipe" });
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Shell environment snapshot for preserving user aliases, functions, and options.
3
+ *
4
+ * Creates a snapshot file that captures the user's shell environment from their
5
+ * .bashrc/.zshrc, which can be sourced before each command to provide a familiar
6
+ * shell experience.
7
+ */
8
+
9
+ import { existsSync, mkdirSync, unlinkSync } from "node:fs";
10
+ import { homedir, tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+
13
+ let cachedSnapshotPath: string | null = null;
14
+ let cleanupRegistered = false;
15
+
16
+ /**
17
+ * Get the user's shell config file path.
18
+ */
19
+ function getShellConfigFile(shell: string): string {
20
+ const home = homedir();
21
+ if (shell.includes("zsh")) return join(home, ".zshrc");
22
+ if (shell.includes("bash")) return join(home, ".bashrc");
23
+ return join(home, ".profile");
24
+ }
25
+
26
+ /**
27
+ * Generate the snapshot creation script.
28
+ * This script sources the user's rc file and extracts functions, aliases, and options.
29
+ * Matches Claude Code's snapshot generation logic.
30
+ */
31
+ function generateSnapshotScript(shell: string, snapshotPath: string, rcFile: string): string {
32
+ const hasRcFile = existsSync(rcFile);
33
+ const isZsh = shell.includes("zsh");
34
+
35
+ // Escape the snapshot path for shell
36
+ const escapedPath = snapshotPath.replace(/'/g, "'\\''");
37
+
38
+ // Function extraction differs between bash and zsh
39
+ const functionScript = isZsh
40
+ ? `
41
+ echo "# Functions" >> "$SNAPSHOT_FILE"
42
+ # Force autoload all functions first
43
+ typeset -f > /dev/null 2>&1
44
+ # Get user function names - filter system/private ones
45
+ typeset +f 2>/dev/null | grep -vE '^(_|__)' | while read func; do
46
+ typeset -f "$func" >> "$SNAPSHOT_FILE" 2>/dev/null
47
+ done
48
+ `
49
+ : `
50
+ echo "# Functions" >> "$SNAPSHOT_FILE"
51
+ # Force autoload all functions first
52
+ declare -f > /dev/null 2>&1
53
+ # Get user function names - filter system/private ones, use base64 for special chars
54
+ declare -F 2>/dev/null | cut -d' ' -f3 | grep -vE '^(_|__)' | while read func; do
55
+ encoded_func=$(declare -f "$func" | base64)
56
+ echo "eval \\"\\$(echo '$encoded_func' | base64 -d)\\" > /dev/null 2>&1" >> "$SNAPSHOT_FILE"
57
+ done
58
+ `;
59
+
60
+ // Shell options extraction
61
+ const optionsScript = isZsh
62
+ ? `
63
+ echo "# Shell Options" >> "$SNAPSHOT_FILE"
64
+ setopt 2>/dev/null | sed 's/^/setopt /' | head -n 1000 >> "$SNAPSHOT_FILE"
65
+ `
66
+ : `
67
+ echo "# Shell Options" >> "$SNAPSHOT_FILE"
68
+ shopt -p 2>/dev/null | head -n 1000 >> "$SNAPSHOT_FILE"
69
+ set -o 2>/dev/null | grep "on" | awk '{print "set -o " $1}' | head -n 1000 >> "$SNAPSHOT_FILE"
70
+ echo "shopt -s expand_aliases" >> "$SNAPSHOT_FILE"
71
+ `;
72
+
73
+ return `
74
+ SNAPSHOT_FILE='${escapedPath}'
75
+
76
+ # Source user's rc file if it exists
77
+ ${hasRcFile ? `source "${rcFile}" < /dev/null 2>/dev/null` : "# No user config file to source"}
78
+
79
+ # Create/clear the snapshot file
80
+ echo "# Shell snapshot - generated by pi agent" >| "$SNAPSHOT_FILE"
81
+
82
+ # Unalias everything first to avoid conflicts when sourced
83
+ echo "unalias -a 2>/dev/null || true" >> "$SNAPSHOT_FILE"
84
+
85
+ ${functionScript}
86
+
87
+ ${optionsScript}
88
+
89
+ # Export aliases (limit to 1000)
90
+ echo "# Aliases" >> "$SNAPSHOT_FILE"
91
+ # Filter out winpty aliases on Windows to avoid "stdin is not a tty" errors
92
+ if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
93
+ alias 2>/dev/null | grep -v "='winpty " | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
94
+ else
95
+ alias 2>/dev/null | sed 's/^alias //g' | sed 's/^/alias -- /' | head -n 1000 >> "$SNAPSHOT_FILE"
96
+ fi
97
+
98
+ # Export PATH
99
+ echo "export PATH='$PATH'" >> "$SNAPSHOT_FILE"
100
+
101
+ # Verify snapshot was created
102
+ if [ ! -f "$SNAPSHOT_FILE" ]; then
103
+ echo "Error: Snapshot file was not created" >&2
104
+ exit 1
105
+ fi
106
+ `.trim();
107
+ }
108
+
109
+ /**
110
+ * Create a shell snapshot, caching the result.
111
+ * Returns the path to the snapshot file, or null if creation failed.
112
+ */
113
+ export async function getOrCreateSnapshot(
114
+ shell: string,
115
+ env: Record<string, string | undefined>,
116
+ ): Promise<string | null> {
117
+ // Return cached snapshot if valid
118
+ if (cachedSnapshotPath && existsSync(cachedSnapshotPath)) {
119
+ return cachedSnapshotPath;
120
+ }
121
+
122
+ // Skip on Windows (no .bashrc in standard location)
123
+ if (process.platform === "win32") {
124
+ return null;
125
+ }
126
+
127
+ const rcFile = getShellConfigFile(shell);
128
+
129
+ // Create snapshot directory
130
+ const snapshotDir = join(tmpdir(), "pi-shell-snapshots");
131
+ try {
132
+ mkdirSync(snapshotDir, { recursive: true });
133
+ } catch {
134
+ return null;
135
+ }
136
+
137
+ // Generate unique snapshot path
138
+ const timestamp = Date.now();
139
+ const random = Math.random().toString(36).substring(2, 8);
140
+ const shellName = shell.includes("zsh") ? "zsh" : shell.includes("bash") ? "bash" : "sh";
141
+ const snapshotPath = join(snapshotDir, `snapshot-${shellName}-${timestamp}-${random}.sh`);
142
+
143
+ // Generate and execute snapshot script
144
+ const script = generateSnapshotScript(shell, snapshotPath, rcFile);
145
+
146
+ try {
147
+ const result = Bun.spawnSync([shell, "-l", "-c", script], {
148
+ stdin: "ignore",
149
+ stdout: "pipe",
150
+ stderr: "pipe",
151
+ env,
152
+ timeout: 10000, // 10 second timeout
153
+ });
154
+
155
+ if (result.exitCode === 0 && existsSync(snapshotPath)) {
156
+ cachedSnapshotPath = snapshotPath;
157
+ registerCleanup();
158
+ return snapshotPath;
159
+ }
160
+ } catch {
161
+ // Snapshot creation failed, proceed without it
162
+ }
163
+
164
+ return null;
165
+ }
166
+
167
+ /**
168
+ * Get the command prefix to source the snapshot.
169
+ * Returns empty string if no snapshot available.
170
+ */
171
+ export function getSnapshotSourceCommand(snapshotPath: string | null): string {
172
+ if (!snapshotPath) return "";
173
+ // Escape for shell
174
+ const escaped = snapshotPath.replace(/'/g, "'\\''");
175
+ return `source '${escaped}' 2>/dev/null && `;
176
+ }
177
+
178
+ /**
179
+ * Register cleanup handler to delete snapshot on process exit.
180
+ */
181
+ function registerCleanup(): void {
182
+ if (cleanupRegistered) return;
183
+ cleanupRegistered = true;
184
+
185
+ const cleanup = () => {
186
+ if (cachedSnapshotPath && existsSync(cachedSnapshotPath)) {
187
+ try {
188
+ unlinkSync(cachedSnapshotPath);
189
+ } catch {
190
+ // Ignore cleanup errors
191
+ }
192
+ }
193
+ };
194
+
195
+ process.on("exit", cleanup);
196
+ process.on("SIGINT", () => {
197
+ cleanup();
198
+ process.exit(130);
199
+ });
200
+ process.on("SIGTERM", () => {
201
+ cleanup();
202
+ process.exit(143);
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Clear the cached snapshot (for testing or forced refresh).
208
+ */
209
+ export function clearSnapshotCache(): void {
210
+ if (cachedSnapshotPath && existsSync(cachedSnapshotPath)) {
211
+ try {
212
+ unlinkSync(cachedSnapshotPath);
213
+ } catch {
214
+ // Ignore
215
+ }
216
+ }
217
+ cachedSnapshotPath = null;
218
+ }