@oh-my-pi/pi-coding-agent 1.340.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 (153) hide show
  1. package/CHANGELOG.md +115 -1
  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 +13 -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 +189 -29
  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 +103 -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 +127 -52
  54. package/src/core/session-manager.ts +123 -20
  55. package/src/core/settings-manager.ts +106 -22
  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 +94 -0
  60. package/src/core/tools/bash.ts +33 -157
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +5 -5
  63. package/src/core/tools/edit.ts +60 -9
  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 +6 -5
  75. package/src/core/tools/index.ts +114 -40
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +204 -108
  78. package/src/core/tools/lsp/config.ts +709 -35
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +432 -30
  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/types.ts +5 -0
  84. package/src/core/tools/lsp/utils.ts +1 -1
  85. package/src/core/tools/notebook.ts +1 -1
  86. package/src/core/tools/output.ts +175 -0
  87. package/src/core/tools/read.ts +7 -7
  88. package/src/core/tools/renderers.ts +92 -13
  89. package/src/core/tools/review.ts +268 -0
  90. package/src/core/tools/task/agents.ts +1 -1
  91. package/src/core/tools/task/bundled-agents/explore.md +1 -1
  92. package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
  93. package/src/core/tools/task/discovery.ts +2 -2
  94. package/src/core/tools/task/executor.ts +145 -28
  95. package/src/core/tools/task/index.ts +78 -30
  96. package/src/core/tools/task/model-resolver.ts +72 -13
  97. package/src/core/tools/task/parallel.ts +1 -1
  98. package/src/core/tools/task/render.ts +219 -30
  99. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  100. package/src/core/tools/task/types.ts +36 -2
  101. package/src/core/tools/web-fetch.ts +5 -3
  102. package/src/core/tools/web-search/auth.ts +1 -1
  103. package/src/core/tools/web-search/index.ts +17 -15
  104. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  105. package/src/core/tools/web-search/providers/exa.ts +3 -5
  106. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  107. package/src/core/tools/web-search/render.ts +3 -3
  108. package/src/core/tools/write.ts +70 -7
  109. package/src/index.ts +33 -17
  110. package/src/main.ts +60 -34
  111. package/src/migrations.ts +3 -3
  112. package/src/modes/index.ts +5 -5
  113. package/src/modes/interactive/components/armin.ts +1 -1
  114. package/src/modes/interactive/components/assistant-message.ts +1 -1
  115. package/src/modes/interactive/components/bash-execution.ts +4 -4
  116. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  117. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  118. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  119. package/src/modes/interactive/components/diff.ts +1 -1
  120. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  121. package/src/modes/interactive/components/footer.ts +5 -5
  122. package/src/modes/interactive/components/hook-editor.ts +2 -2
  123. package/src/modes/interactive/components/hook-input.ts +2 -2
  124. package/src/modes/interactive/components/hook-message.ts +3 -3
  125. package/src/modes/interactive/components/hook-selector.ts +2 -2
  126. package/src/modes/interactive/components/model-selector.ts +341 -41
  127. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  128. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  129. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  130. package/src/modes/interactive/components/session-selector.ts +24 -11
  131. package/src/modes/interactive/components/settings-defs.ts +51 -3
  132. package/src/modes/interactive/components/settings-selector.ts +13 -16
  133. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  134. package/src/modes/interactive/components/theme-selector.ts +2 -2
  135. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  136. package/src/modes/interactive/components/tool-execution.ts +44 -8
  137. package/src/modes/interactive/components/tree-selector.ts +5 -5
  138. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  139. package/src/modes/interactive/components/user-message.ts +1 -1
  140. package/src/modes/interactive/components/welcome.ts +42 -5
  141. package/src/modes/interactive/interactive-mode.ts +169 -48
  142. package/src/modes/interactive/theme/theme.ts +8 -7
  143. package/src/modes/print-mode.ts +4 -3
  144. package/src/modes/rpc/rpc-client.ts +4 -4
  145. package/src/modes/rpc/rpc-mode.ts +21 -11
  146. package/src/modes/rpc/rpc-types.ts +3 -3
  147. package/src/utils/changelog.ts +2 -2
  148. package/src/utils/clipboard.ts +1 -1
  149. package/src/utils/shell-snapshot.ts +218 -0
  150. package/src/utils/shell.ts +93 -13
  151. package/src/utils/tools-manager.ts +1 -1
  152. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  153. package/src/core/tools/exa/logger.ts +0 -56
@@ -16,8 +16,8 @@
16
16
  import type { Agent, AgentEvent, AgentMessage, AgentState, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
17
17
  import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@oh-my-pi/pi-ai";
18
18
  import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
19
- import { getAuthPath } from "../config.js";
20
- import { type BashResult, executeBash as executeBashCommand } from "./bash-executor.js";
19
+ import { getAuthPath } from "../config";
20
+ import { type BashResult, executeBash as executeBashCommand } from "./bash-executor";
21
21
  import {
22
22
  type CompactionResult,
23
23
  calculateContextTokens,
@@ -26,9 +26,11 @@ import {
26
26
  generateBranchSummary,
27
27
  prepareCompaction,
28
28
  shouldCompact,
29
- } from "./compaction/index.js";
30
- import type { CustomToolContext, CustomToolSessionEvent, LoadedCustomTool } from "./custom-tools/index.js";
31
- import { exportSessionToHtml } from "./export-html/index.js";
29
+ } from "./compaction/index";
30
+ import type { LoadedCustomCommand } from "./custom-commands/index";
31
+ import type { CustomToolContext, CustomToolSessionEvent, LoadedCustomTool } from "./custom-tools/index";
32
+ import { exportSessionToHtml } from "./export-html/index";
33
+ import { extractFileMentions, generateFileMentionMessages } from "./file-mentions";
32
34
  import type {
33
35
  HookRunner,
34
36
  SessionBeforeBranchResult,
@@ -38,12 +40,13 @@ import type {
38
40
  TreePreparation,
39
41
  TurnEndEvent,
40
42
  TurnStartEvent,
41
- } from "./hooks/index.js";
42
- import type { BashExecutionMessage, HookMessage } from "./messages.js";
43
- import type { ModelRegistry } from "./model-registry.js";
44
- import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager.js";
45
- import type { SettingsManager, SkillsSettings } from "./settings-manager.js";
46
- import { expandSlashCommand, type FileSlashCommand } from "./slash-commands.js";
43
+ } from "./hooks/index";
44
+ import { logger } from "./logger";
45
+ import type { BashExecutionMessage, HookMessage } from "./messages";
46
+ import type { ModelRegistry } from "./model-registry";
47
+ import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
48
+ import type { SettingsManager, SkillsSettings } from "./settings-manager";
49
+ import { expandSlashCommand, type FileSlashCommand, parseCommandArgs } from "./slash-commands";
47
50
 
48
51
  /** Session-specific events that extend the core AgentEvent */
49
52
  export type AgentSessionEvent =
@@ -72,6 +75,8 @@ export interface AgentSessionConfig {
72
75
  hookRunner?: HookRunner;
73
76
  /** Custom tools for session lifecycle events */
74
77
  customTools?: LoadedCustomTool[];
78
+ /** Custom commands (TypeScript slash commands) */
79
+ customCommands?: LoadedCustomCommand[];
75
80
  skillsSettings?: Required<SkillsSettings>;
76
81
  /** Model registry for API key resolution and model discovery */
77
82
  modelRegistry: ModelRegistry;
@@ -166,6 +171,9 @@ export class AgentSession {
166
171
  // Custom tools for session lifecycle
167
172
  private _customTools: LoadedCustomTool[] = [];
168
173
 
174
+ // Custom commands (TypeScript slash commands)
175
+ private _customCommands: LoadedCustomCommand[] = [];
176
+
169
177
  private _skillsSettings: Required<SkillsSettings> | undefined;
170
178
 
171
179
  // Model registry for API key resolution
@@ -179,6 +187,7 @@ export class AgentSession {
179
187
  this._fileCommands = config.fileCommands ?? [];
180
188
  this._hookRunner = config.hookRunner;
181
189
  this._customTools = config.customTools ?? [];
190
+ this._customCommands = config.customCommands ?? [];
182
191
  this._skillsSettings = config.skillsSettings;
183
192
  this._modelRegistry = config.modelRegistry;
184
193
 
@@ -198,7 +207,9 @@ export class AgentSession {
198
207
 
199
208
  /** Emit an event to all listeners */
200
209
  private _emit(event: AgentSessionEvent): void {
201
- for (const l of this._eventListeners) {
210
+ // Copy array before iteration to avoid mutation during iteration
211
+ const listeners = [...this._eventListeners];
212
+ for (const l of listeners) {
202
213
  l(event);
203
214
  }
204
215
  }
@@ -424,6 +435,11 @@ export class AgentSession {
424
435
  return this.agent.getQueueMode();
425
436
  }
426
437
 
438
+ /** Current interrupt mode */
439
+ get interruptMode(): "immediate" | "wait" {
440
+ return this.agent.getInterruptMode();
441
+ }
442
+
427
443
  /** Current session file path, or undefined if sessions are disabled */
428
444
  get sessionFile(): string | undefined {
429
445
  return this.sessionManager.getSessionFile();
@@ -444,6 +460,11 @@ export class AgentSession {
444
460
  return this._fileCommands;
445
461
  }
446
462
 
463
+ /** Custom commands (TypeScript slash commands) */
464
+ get customCommands(): ReadonlyArray<LoadedCustomCommand> {
465
+ return this._customCommands;
466
+ }
467
+
447
468
  // =========================================================================
448
469
  // Prompting
449
470
  // =========================================================================
@@ -468,6 +489,17 @@ export class AgentSession {
468
489
  // Hook command executed, no prompt to send
469
490
  return;
470
491
  }
492
+
493
+ // Try custom commands (TypeScript slash commands)
494
+ const customResult = await this._tryExecuteCustomCommand(text);
495
+ if (customResult !== null) {
496
+ if (customResult === "") {
497
+ // Command handled, nothing to send
498
+ return;
499
+ }
500
+ // Command returned a prompt - use it instead of the original text
501
+ text = customResult;
502
+ }
471
503
  }
472
504
 
473
505
  // Validate model
@@ -511,6 +543,13 @@ export class AgentSession {
511
543
  timestamp: Date.now(),
512
544
  });
513
545
 
546
+ // Auto-read @filepath mentions
547
+ const fileMentions = extractFileMentions(expandedText);
548
+ if (fileMentions.length > 0) {
549
+ const fileMentionMessages = await generateFileMentionMessages(fileMentions, this.sessionManager.getCwd());
550
+ messages.push(...fileMentionMessages);
551
+ }
552
+
514
553
  // Emit before_agent_start hook event
515
554
  if (this._hookRunner) {
516
555
  const result = await this._hookRunner.emitBeforeAgentStart(expandedText, options?.images);
@@ -561,6 +600,43 @@ export class AgentSession {
561
600
  }
562
601
  }
563
602
 
603
+ /**
604
+ * Try to execute a custom command. Returns the prompt string if found, null otherwise.
605
+ * If the command returns void, returns empty string to indicate it was handled.
606
+ */
607
+ private async _tryExecuteCustomCommand(text: string): Promise<string | null> {
608
+ if (this._customCommands.length === 0) return null;
609
+ if (!this._hookRunner) return null; // Need hook runner for command context
610
+
611
+ // Parse command name and args
612
+ const spaceIndex = text.indexOf(" ");
613
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
614
+ const argsString = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);
615
+
616
+ // Find matching command
617
+ const loaded = this._customCommands.find((c) => c.command.name === commandName);
618
+ if (!loaded) return null;
619
+
620
+ // Get command context from hook runner (includes session control methods)
621
+ const ctx = this._hookRunner.createCommandContext();
622
+
623
+ try {
624
+ const args = parseCommandArgs(argsString);
625
+ const result = await loaded.command.execute(args, ctx);
626
+ // If result is a string, it's a prompt to send to LLM
627
+ // If void/undefined, command handled everything
628
+ return result ?? "";
629
+ } catch (err) {
630
+ // Emit error via hook runner
631
+ this._hookRunner.emitError({
632
+ hookPath: `custom-command:${commandName}`,
633
+ event: "command",
634
+ error: err instanceof Error ? err.message : String(err),
635
+ });
636
+ return ""; // Command was handled (with error)
637
+ }
638
+ }
639
+
564
640
  /**
565
641
  * Queue a message to be sent after the current response completes.
566
642
  * Use when agent is currently streaming.
@@ -701,15 +777,15 @@ export class AgentSession {
701
777
  * Validates API key, saves to session and settings.
702
778
  * @throws Error if no API key available for the model
703
779
  */
704
- async setModel(model: Model<any>): Promise<void> {
780
+ async setModel(model: Model<any>, role: string = "default"): Promise<void> {
705
781
  const apiKey = await this._modelRegistry.getApiKey(model);
706
782
  if (!apiKey) {
707
783
  throw new Error(`No API key for ${model.provider}/${model.id}`);
708
784
  }
709
785
 
710
786
  this.agent.setModel(model);
711
- this.sessionManager.appendModelChange(model.provider, model.id);
712
- this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
787
+ this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
788
+ this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
713
789
 
714
790
  // Re-clamp thinking level for new model's capabilities
715
791
  this.setThinkingLevel(this.thinkingLevel);
@@ -747,8 +823,8 @@ export class AgentSession {
747
823
 
748
824
  // Apply model
749
825
  this.agent.setModel(next.model);
750
- this.sessionManager.appendModelChange(next.model.provider, next.model.id);
751
- this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
826
+ this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
827
+ this.settingsManager.setModelRole("default", `${next.model.provider}/${next.model.id}`);
752
828
 
753
829
  // Apply thinking level (setThinkingLevel clamps to model capabilities)
754
830
  this.setThinkingLevel(next.thinkingLevel);
@@ -774,8 +850,8 @@ export class AgentSession {
774
850
  }
775
851
 
776
852
  this.agent.setModel(nextModel);
777
- this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
778
- this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
853
+ this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
854
+ this.settingsManager.setModelRole("default", `${nextModel.provider}/${nextModel.id}`);
779
855
 
780
856
  // Re-clamp thinking level for new model's capabilities
781
857
  this.setThinkingLevel(this.thinkingLevel);
@@ -861,6 +937,15 @@ export class AgentSession {
861
937
  this.settingsManager.setQueueMode(mode);
862
938
  }
863
939
 
940
+ /**
941
+ * Set interrupt mode.
942
+ * Saves to settings.
943
+ */
944
+ setInterruptMode(mode: "immediate" | "wait"): void {
945
+ this.agent.setInterruptMode(mode);
946
+ this.settingsManager.setInterruptMode(mode);
947
+ }
948
+
864
949
  // =========================================================================
865
950
  // Compaction
866
951
  // =========================================================================
@@ -1044,6 +1129,10 @@ export class AgentSession {
1044
1129
  const settings = this.settingsManager.getCompactionSettings();
1045
1130
 
1046
1131
  this._emit({ type: "auto_compaction_start", reason });
1132
+ // Properly abort and null existing controller before replacing
1133
+ if (this._autoCompactionAbortController) {
1134
+ this._autoCompactionAbortController.abort();
1135
+ }
1047
1136
  this._autoCompactionAbortController = new AbortController();
1048
1137
 
1049
1138
  try {
@@ -1217,7 +1306,8 @@ export class AgentSession {
1217
1306
  this._retryAttempt++;
1218
1307
 
1219
1308
  // Create retry promise on first attempt so waitForRetry() can await it
1220
- if (this._retryAttempt === 1 && !this._retryPromise) {
1309
+ // Ensure only one promise exists (avoid orphaned promises from concurrent calls)
1310
+ if (!this._retryPromise) {
1221
1311
  this._retryPromise = new Promise((resolve) => {
1222
1312
  this._retryResolve = resolve;
1223
1313
  });
@@ -1253,6 +1343,10 @@ export class AgentSession {
1253
1343
  }
1254
1344
 
1255
1345
  // Wait with exponential backoff (abortable)
1346
+ // Properly abort and null existing controller before replacing
1347
+ if (this._retryAbortController) {
1348
+ this._retryAbortController.abort();
1349
+ }
1256
1350
  this._retryAbortController = new AbortController();
1257
1351
  try {
1258
1352
  await this._sleep(delayMs, this._retryAbortController.signal);
@@ -1472,13 +1566,17 @@ export class AgentSession {
1472
1566
  this.agent.replaceMessages(sessionContext.messages);
1473
1567
 
1474
1568
  // Restore model if saved
1475
- if (sessionContext.model) {
1476
- const availableModels = await this._modelRegistry.getAvailable();
1477
- const match = availableModels.find(
1478
- (m) => m.provider === sessionContext.model!.provider && m.id === sessionContext.model!.modelId,
1479
- );
1480
- if (match) {
1481
- this.agent.setModel(match);
1569
+ const defaultModelStr = sessionContext.models.default;
1570
+ if (defaultModelStr) {
1571
+ const slashIdx = defaultModelStr.indexOf("/");
1572
+ if (slashIdx > 0) {
1573
+ const provider = defaultModelStr.slice(0, slashIdx);
1574
+ const modelId = defaultModelStr.slice(slashIdx + 1);
1575
+ const availableModels = await this._modelRegistry.getAvailable();
1576
+ const match = availableModels.find((m) => m.provider === provider && m.id === modelId);
1577
+ if (match) {
1578
+ this.agent.setModel(match);
1579
+ }
1482
1580
  }
1483
1581
  }
1484
1582
 
@@ -1840,6 +1938,68 @@ export class AgentSession {
1840
1938
  return text.trim() || undefined;
1841
1939
  }
1842
1940
 
1941
+ /**
1942
+ * Format the entire session as plain text for clipboard export.
1943
+ * Includes user messages, assistant text, thinking blocks, tool calls, and tool results.
1944
+ */
1945
+ formatSessionAsText(): string {
1946
+ const lines: string[] = [];
1947
+
1948
+ for (const msg of this.messages) {
1949
+ if (msg.role === "user") {
1950
+ lines.push("## User\n");
1951
+ if (typeof msg.content === "string") {
1952
+ lines.push(msg.content);
1953
+ } else {
1954
+ for (const c of msg.content) {
1955
+ if (c.type === "text") {
1956
+ lines.push(c.text);
1957
+ } else if (c.type === "image") {
1958
+ lines.push("[Image]");
1959
+ }
1960
+ }
1961
+ }
1962
+ lines.push("\n");
1963
+ } else if (msg.role === "assistant") {
1964
+ const assistantMsg = msg as AssistantMessage;
1965
+ lines.push("## Assistant\n");
1966
+
1967
+ for (const c of assistantMsg.content) {
1968
+ if (c.type === "text") {
1969
+ lines.push(c.text);
1970
+ } else if (c.type === "thinking") {
1971
+ lines.push("<thinking>");
1972
+ lines.push(c.thinking);
1973
+ lines.push("</thinking>\n");
1974
+ } else if (c.type === "toolCall") {
1975
+ lines.push(`### Tool: ${c.name}`);
1976
+ lines.push("```json");
1977
+ lines.push(JSON.stringify(c.arguments, null, 2));
1978
+ lines.push("```\n");
1979
+ }
1980
+ }
1981
+ lines.push("");
1982
+ } else if (msg.role === "toolResult") {
1983
+ lines.push(`### Tool Result: ${msg.toolName}`);
1984
+ if (msg.isError) {
1985
+ lines.push("(error)");
1986
+ }
1987
+ for (const c of msg.content) {
1988
+ if (c.type === "text") {
1989
+ lines.push("```");
1990
+ lines.push(c.text);
1991
+ lines.push("```");
1992
+ } else if (c.type === "image") {
1993
+ lines.push("[Image output]");
1994
+ }
1995
+ }
1996
+ lines.push("");
1997
+ }
1998
+ }
1999
+
2000
+ return lines.join("\n").trim();
2001
+ }
2002
+
1843
2003
  // =========================================================================
1844
2004
  // Hook System
1845
2005
  // =========================================================================
@@ -1891,8 +2051,8 @@ export class AgentSession {
1891
2051
  if (tool.onSession) {
1892
2052
  try {
1893
2053
  await tool.onSession(event, ctx);
1894
- } catch (_err) {
1895
- // Silently ignore tool errors during session events
2054
+ } catch (err) {
2055
+ logger.warn("Tool onSession error", { error: String(err) });
1896
2056
  }
1897
2057
  }
1898
2058
  }
@@ -11,14 +11,19 @@ import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import type { Subprocess } from "bun";
13
13
  import stripAnsi from "strip-ansi";
14
- import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../utils/shell.js";
15
- import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js";
14
+ import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../utils/shell";
15
+ import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
16
+ import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate";
16
17
 
17
18
  // ============================================================================
18
19
  // Types
19
20
  // ============================================================================
20
21
 
21
22
  export interface BashExecutorOptions {
23
+ /** Working directory for command execution */
24
+ cwd?: string;
25
+ /** Timeout in milliseconds */
26
+ timeout?: number;
22
27
  /** Callback for streaming output chunks (already sanitized) */
23
28
  onChunk?: (chunk: string) => void;
24
29
  /** AbortSignal for cancellation */
@@ -56,13 +61,24 @@ export interface BashResult {
56
61
  * @param options - Optional streaming callback and abort signal
57
62
  * @returns Promise resolving to execution result
58
63
  */
59
- export function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
64
+ export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
65
+ const { shell, args, env, prefix } = getShellConfig();
66
+
67
+ // Get or create shell snapshot (for aliases, functions, options)
68
+ const snapshotPath = await getOrCreateSnapshot(shell, env);
69
+ const snapshotPrefix = getSnapshotSourceCommand(snapshotPath);
70
+
71
+ // Build final command: snapshot + prefix + command
72
+ const prefixedCommand = prefix ? `${prefix} ${command}` : command;
73
+ const finalCommand = `${snapshotPrefix}${prefixedCommand}`;
74
+
60
75
  return new Promise((resolve, reject) => {
61
- const { shell, args } = getShellConfig();
62
- const child: Subprocess = Bun.spawn([shell, ...args, command], {
76
+ const child: Subprocess = Bun.spawn([shell, ...args, finalCommand], {
77
+ cwd: options?.cwd,
63
78
  stdin: "ignore",
64
79
  stdout: "pipe",
65
80
  stderr: "pipe",
81
+ env,
66
82
  });
67
83
 
68
84
  // Track sanitized output for truncation
@@ -74,16 +90,27 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
74
90
  let tempFilePath: string | undefined;
75
91
  let tempFileStream: WriteStream | undefined;
76
92
  let totalBytes = 0;
93
+ let timedOut = false;
77
94
 
78
- // Handle abort signal
95
+ // Handle abort signal and timeout
79
96
  const abortHandler = () => {
80
97
  killProcessTree(child.pid);
81
98
  };
82
99
 
100
+ // Set up timeout if specified
101
+ let timeoutHandle: Timer | undefined;
102
+ if (options?.timeout && options.timeout > 0) {
103
+ timeoutHandle = setTimeout(() => {
104
+ timedOut = true;
105
+ abortHandler();
106
+ }, options.timeout);
107
+ }
108
+
83
109
  if (options?.signal) {
84
110
  if (options.signal.aborted) {
85
111
  // Already aborted, don't even start
86
112
  child.kill();
113
+ if (timeoutHandle) clearTimeout(timeoutHandle);
87
114
  resolve({
88
115
  output: "",
89
116
  exitCode: undefined,
@@ -156,11 +183,11 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
156
183
 
157
184
  const exitCode = await child.exited;
158
185
 
159
- // Clean up abort listener
186
+ // Clean up
187
+ if (timeoutHandle) clearTimeout(timeoutHandle);
160
188
  if (options?.signal) {
161
189
  options.signal.removeEventListener("abort", abortHandler);
162
190
  }
163
-
164
191
  if (tempFileStream) {
165
192
  tempFileStream.end();
166
193
  }
@@ -169,6 +196,19 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
169
196
  const fullOutput = outputChunks.join("");
170
197
  const truncationResult = truncateTail(fullOutput);
171
198
 
199
+ // Handle timeout
200
+ if (timedOut) {
201
+ const timeoutSecs = Math.round((options?.timeout || 0) / 1000);
202
+ resolve({
203
+ output: `${fullOutput}\n\nCommand timed out after ${timeoutSecs} seconds`,
204
+ exitCode: undefined,
205
+ cancelled: true,
206
+ truncated: truncationResult.truncated,
207
+ fullOutputPath: tempFilePath,
208
+ });
209
+ return;
210
+ }
211
+
172
212
  // Non-zero exit codes or signal-killed processes are considered cancelled if killed via signal
173
213
  const cancelled = exitCode === null || (exitCode !== 0 && (options?.signal?.aborted ?? false));
174
214
 
@@ -180,11 +220,11 @@ export function executeBash(command: string, options?: BashExecutorOptions): Pro
180
220
  fullOutputPath: tempFilePath,
181
221
  });
182
222
  } catch (err) {
183
- // Clean up abort listener
223
+ // Clean up
224
+ if (timeoutHandle) clearTimeout(timeoutHandle);
184
225
  if (options?.signal) {
185
226
  options.signal.removeEventListener("abort", abortHandler);
186
227
  }
187
-
188
228
  if (tempFileStream) {
189
229
  tempFileStream.end();
190
230
  }
@@ -13,9 +13,9 @@ import {
13
13
  createBranchSummaryMessage,
14
14
  createCompactionSummaryMessage,
15
15
  createHookMessage,
16
- } from "../messages.js";
17
- import type { ReadonlySessionManager, SessionEntry } from "../session-manager.js";
18
- import { estimateTokens } from "./compaction.js";
16
+ } from "../messages";
17
+ import type { ReadonlySessionManager, SessionEntry } from "../session-manager";
18
+ import { estimateTokens } from "./compaction";
19
19
  import {
20
20
  computeFileLists,
21
21
  createFileOps,
@@ -24,7 +24,7 @@ import {
24
24
  formatFileOperations,
25
25
  SUMMARIZATION_SYSTEM_PROMPT,
26
26
  serializeConversation,
27
- } from "./utils.js";
27
+ } from "./utils";
28
28
 
29
29
  // ============================================================================
30
30
  // Types
@@ -44,7 +44,7 @@ export interface BranchSummaryDetails {
44
44
  modifiedFiles: string[];
45
45
  }
46
46
 
47
- export type { FileOperations } from "./utils.js";
47
+ export type { FileOperations } from "./utils";
48
48
 
49
49
  export interface BranchPreparation {
50
50
  /** Messages extracted for summarization, in chronological order */
@@ -8,8 +8,8 @@
8
8
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
9
9
  import type { AssistantMessage, Model, Usage } from "@oh-my-pi/pi-ai";
10
10
  import { complete, completeSimple } from "@oh-my-pi/pi-ai";
11
- import { convertToLlm, createBranchSummaryMessage, createHookMessage } from "../messages.js";
12
- import type { CompactionEntry, SessionEntry } from "../session-manager.js";
11
+ import { convertToLlm, createBranchSummaryMessage, createHookMessage } from "../messages";
12
+ import type { CompactionEntry, SessionEntry } from "../session-manager";
13
13
  import {
14
14
  computeFileLists,
15
15
  createFileOps,
@@ -18,7 +18,7 @@ import {
18
18
  formatFileOperations,
19
19
  SUMMARIZATION_SYSTEM_PROMPT,
20
20
  serializeConversation,
21
- } from "./utils.js";
21
+ } from "./utils";
22
22
 
23
23
  // ============================================================================
24
24
  // File Operation Tracking
@@ -2,6 +2,6 @@
2
2
  * Compaction and summarization utilities.
3
3
  */
4
4
 
5
- export * from "./branch-summarization.js";
6
- export * from "./compaction.js";
7
- export * from "./utils.js";
5
+ export * from "./branch-summarization";
6
+ export * from "./compaction";
7
+ export * from "./utils";