@oh-my-pi/pi-coding-agent 1.341.0 → 2.0.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 (151) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +5 -3
  5. package/src/cli/args.ts +5 -6
  6. package/src/cli/file-processor.ts +3 -3
  7. package/src/cli/list-models.ts +2 -2
  8. package/src/cli/plugin-cli.ts +1 -1
  9. package/src/cli/session-picker.ts +2 -2
  10. package/src/cli.ts +1 -1
  11. package/src/config.ts +3 -3
  12. package/src/core/agent-session.ts +157 -15
  13. package/src/core/bash-executor.ts +50 -10
  14. package/src/core/compaction/branch-summarization.ts +5 -5
  15. package/src/core/compaction/compaction.ts +3 -3
  16. package/src/core/compaction/index.ts +3 -3
  17. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  18. package/src/core/custom-commands/index.ts +15 -0
  19. package/src/core/custom-commands/loader.ts +232 -0
  20. package/src/core/custom-commands/types.ts +112 -0
  21. package/src/core/custom-tools/index.ts +3 -3
  22. package/src/core/custom-tools/loader.ts +10 -8
  23. package/src/core/custom-tools/types.ts +11 -6
  24. package/src/core/custom-tools/wrapper.ts +2 -1
  25. package/src/core/exec.ts +22 -12
  26. package/src/core/export-html/index.ts +5 -5
  27. package/src/core/file-mentions.ts +54 -0
  28. package/src/core/hooks/index.ts +5 -5
  29. package/src/core/hooks/loader.ts +21 -16
  30. package/src/core/hooks/runner.ts +6 -6
  31. package/src/core/hooks/tool-wrapper.ts +2 -2
  32. package/src/core/hooks/types.ts +12 -15
  33. package/src/core/index.ts +6 -6
  34. package/src/core/logger.ts +112 -0
  35. package/src/core/mcp/client.ts +3 -3
  36. package/src/core/mcp/config.ts +1 -1
  37. package/src/core/mcp/index.ts +12 -12
  38. package/src/core/mcp/loader.ts +2 -2
  39. package/src/core/mcp/manager.ts +6 -6
  40. package/src/core/mcp/tool-bridge.ts +3 -3
  41. package/src/core/mcp/transports/http.ts +1 -1
  42. package/src/core/mcp/transports/index.ts +2 -2
  43. package/src/core/mcp/transports/stdio.ts +1 -1
  44. package/src/core/messages.ts +22 -0
  45. package/src/core/model-registry.ts +2 -2
  46. package/src/core/model-resolver.ts +2 -2
  47. package/src/core/plugins/doctor.ts +1 -1
  48. package/src/core/plugins/index.ts +6 -6
  49. package/src/core/plugins/installer.ts +4 -4
  50. package/src/core/plugins/loader.ts +4 -9
  51. package/src/core/plugins/manager.ts +5 -5
  52. package/src/core/plugins/paths.ts +3 -3
  53. package/src/core/sdk.ts +77 -35
  54. package/src/core/session-manager.ts +6 -6
  55. package/src/core/settings-manager.ts +16 -3
  56. package/src/core/skills.ts +5 -5
  57. package/src/core/slash-commands.ts +60 -45
  58. package/src/core/system-prompt.ts +6 -6
  59. package/src/core/title-generator.ts +2 -2
  60. package/src/core/tools/bash.ts +32 -155
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +3 -3
  63. package/src/core/tools/edit.ts +18 -5
  64. package/src/core/tools/exa/company.ts +3 -3
  65. package/src/core/tools/exa/index.ts +16 -17
  66. package/src/core/tools/exa/linkedin.ts +3 -3
  67. package/src/core/tools/exa/mcp-client.ts +9 -9
  68. package/src/core/tools/exa/render.ts +5 -5
  69. package/src/core/tools/exa/researcher.ts +3 -3
  70. package/src/core/tools/exa/search.ts +6 -5
  71. package/src/core/tools/exa/types.ts +5 -6
  72. package/src/core/tools/exa/websets.ts +3 -3
  73. package/src/core/tools/find.ts +3 -3
  74. package/src/core/tools/grep.ts +3 -3
  75. package/src/core/tools/index.ts +48 -34
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +161 -90
  78. package/src/core/tools/lsp/config.ts +1 -1
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +15 -13
  81. package/src/core/tools/lsp/render.ts +2 -2
  82. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  83. package/src/core/tools/lsp/utils.ts +1 -1
  84. package/src/core/tools/notebook.ts +1 -1
  85. package/src/core/tools/output.ts +175 -0
  86. package/src/core/tools/read.ts +7 -7
  87. package/src/core/tools/renderers.ts +92 -13
  88. package/src/core/tools/review.ts +268 -0
  89. package/src/core/tools/task/agents.ts +1 -1
  90. package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
  91. package/src/core/tools/task/discovery.ts +2 -2
  92. package/src/core/tools/task/executor.ts +145 -28
  93. package/src/core/tools/task/index.ts +78 -30
  94. package/src/core/tools/task/model-resolver.ts +30 -20
  95. package/src/core/tools/task/parallel.ts +1 -1
  96. package/src/core/tools/task/render.ts +219 -30
  97. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  98. package/src/core/tools/task/types.ts +36 -2
  99. package/src/core/tools/web-fetch.ts +5 -3
  100. package/src/core/tools/web-search/auth.ts +1 -1
  101. package/src/core/tools/web-search/index.ts +17 -15
  102. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  103. package/src/core/tools/web-search/providers/exa.ts +3 -5
  104. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  105. package/src/core/tools/web-search/render.ts +3 -3
  106. package/src/core/tools/write.ts +4 -4
  107. package/src/index.ts +29 -18
  108. package/src/main.ts +37 -32
  109. package/src/migrations.ts +3 -3
  110. package/src/modes/index.ts +5 -5
  111. package/src/modes/interactive/components/armin.ts +1 -1
  112. package/src/modes/interactive/components/assistant-message.ts +1 -1
  113. package/src/modes/interactive/components/bash-execution.ts +4 -4
  114. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  115. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  116. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  117. package/src/modes/interactive/components/diff.ts +1 -1
  118. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  119. package/src/modes/interactive/components/footer.ts +5 -5
  120. package/src/modes/interactive/components/hook-editor.ts +2 -2
  121. package/src/modes/interactive/components/hook-input.ts +2 -2
  122. package/src/modes/interactive/components/hook-message.ts +3 -3
  123. package/src/modes/interactive/components/hook-selector.ts +2 -2
  124. package/src/modes/interactive/components/model-selector.ts +281 -59
  125. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  126. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  127. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  128. package/src/modes/interactive/components/session-selector.ts +4 -4
  129. package/src/modes/interactive/components/settings-defs.ts +1 -1
  130. package/src/modes/interactive/components/settings-selector.ts +5 -5
  131. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  132. package/src/modes/interactive/components/theme-selector.ts +2 -2
  133. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  134. package/src/modes/interactive/components/tool-execution.ts +26 -8
  135. package/src/modes/interactive/components/tree-selector.ts +3 -3
  136. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  137. package/src/modes/interactive/components/user-message.ts +1 -1
  138. package/src/modes/interactive/components/welcome.ts +2 -2
  139. package/src/modes/interactive/interactive-mode.ts +85 -41
  140. package/src/modes/interactive/theme/theme.ts +8 -7
  141. package/src/modes/print-mode.ts +4 -3
  142. package/src/modes/rpc/rpc-client.ts +4 -4
  143. package/src/modes/rpc/rpc-mode.ts +21 -11
  144. package/src/modes/rpc/rpc-types.ts +3 -3
  145. package/src/utils/changelog.ts +2 -2
  146. package/src/utils/clipboard.ts +1 -1
  147. package/src/utils/shell-snapshot.ts +218 -0
  148. package/src/utils/shell.ts +93 -13
  149. package/src/utils/tools-manager.ts +1 -1
  150. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  151. 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 = 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"}`);
@@ -5,7 +5,8 @@ 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, getThemesDir } from "../../../config";
9
+ import { logger } from "../../../core/logger";
9
10
 
10
11
  // ============================================================================
11
12
  // Types & Schema
@@ -590,8 +591,8 @@ export function initTheme(themeName?: string, enableWatcher: boolean = false): v
590
591
  if (enableWatcher) {
591
592
  startThemeWatcher();
592
593
  }
593
- } catch (_error) {
594
- // Theme is invalid - fall back to dark theme silently
594
+ } catch (err) {
595
+ logger.debug("Theme loading failed, falling back to dark theme", { error: String(err) });
595
596
  currentThemeName = "dark";
596
597
  theme = loadTheme("dark");
597
598
  // Don't start watcher for fallback theme
@@ -654,8 +655,8 @@ function startThemeWatcher(): void {
654
655
  if (onThemeChangeCallback) {
655
656
  onThemeChangeCallback();
656
657
  }
657
- } catch (_error) {
658
- // Ignore errors (file might be in invalid state while being edited)
658
+ } catch (err) {
659
+ logger.debug("Theme reload error during file change", { error: String(err) });
659
660
  }
660
661
  }, 100);
661
662
  } else if (eventType === "rename") {
@@ -675,8 +676,8 @@ function startThemeWatcher(): void {
675
676
  }, 100);
676
677
  }
677
678
  });
678
- } catch (_error) {
679
- // Ignore errors starting watcher
679
+ } catch (err) {
680
+ logger.debug("Failed to start theme watcher", { error: String(err) });
680
681
  }
681
682
  }
682
683
 
@@ -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
 
@@ -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
+ }