@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
package/src/config.ts CHANGED
@@ -1,34 +1,31 @@
1
- import { existsSync, readFileSync } from "fs";
2
- import { homedir } from "os";
3
- import { dirname, join, resolve } from "path";
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+
5
+ // Embed package.json at build time for config
6
+ import packageJson from "../package.json" with { type: "json" };
4
7
 
5
8
  // =============================================================================
6
- // Package Detection
9
+ // App Config (from embedded package.json)
7
10
  // =============================================================================
8
11
 
9
- /**
10
- * Detect if we're running as a Bun compiled binary.
11
- * Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path)
12
- */
13
- export const isBunBinary =
14
- import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN");
12
+ export const APP_NAME: string = (packageJson as { piConfig?: { name?: string } }).piConfig?.name || "pi";
13
+ export const CONFIG_DIR_NAME: string =
14
+ (packageJson as { piConfig?: { configDir?: string } }).piConfig?.configDir || ".pi";
15
+ export const VERSION: string = (packageJson as { version: string }).version;
16
+
17
+ // e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
18
+ export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
15
19
 
16
20
  // =============================================================================
17
- // Package Asset Paths (shipped with executable)
21
+ // Package Directory (for optional external docs/examples)
18
22
  // =============================================================================
19
23
 
20
24
  /**
21
- * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md).
22
- * - For Bun binary: returns the directory containing the executable
23
- * - For Node.js (dist/): returns __dirname (the dist/ directory)
24
- * - For tsx (src/): returns parent directory (the package root)
25
+ * Get the base directory for resolving optional package assets (docs, examples).
26
+ * Walk up from import.meta.dir until we find package.json, or fall back to cwd.
25
27
  */
26
28
  export function getPackageDir(): string {
27
- if (isBunBinary) {
28
- // Bun binary: process.execPath points to the compiled executable
29
- return dirname(process.execPath);
30
- }
31
- // Node.js: walk up from import.meta.dir until we find package.json
32
29
  let dir = import.meta.dir;
33
30
  while (dir !== dirname(dir)) {
34
31
  if (existsSync(join(dir, "package.json"))) {
@@ -36,79 +33,30 @@ export function getPackageDir(): string {
36
33
  }
37
34
  dir = dirname(dir);
38
35
  }
39
- // Fallback (shouldn't happen)
40
- return import.meta.dir;
36
+ // Fallback to cwd (docs/examples won't be found, but that's fine)
37
+ return process.cwd();
41
38
  }
42
39
 
43
- /**
44
- * Get path to built-in themes directory (shipped with package)
45
- * - For Bun binary: theme/ next to executable
46
- * - For Node.js (dist/): dist/modes/interactive/theme/
47
- * - For tsx (src/): src/modes/interactive/theme/
48
- */
49
- export function getThemesDir(): string {
50
- if (isBunBinary) {
51
- return join(dirname(process.execPath), "theme");
52
- }
53
- // Theme is in modes/interactive/theme/ relative to src/ or dist/
54
- const packageDir = getPackageDir();
55
- const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
56
- return join(packageDir, srcOrDist, "modes", "interactive", "theme");
57
- }
58
-
59
- /**
60
- * Get path to HTML export template directory (shipped with package)
61
- * - For Bun binary: export-html/ next to executable
62
- * - For Node.js (dist/): dist/core/export-html/
63
- * - For tsx (src/): src/core/export-html/
64
- */
65
- export function getExportTemplateDir(): string {
66
- if (isBunBinary) {
67
- return join(dirname(process.execPath), "export-html");
68
- }
69
- const packageDir = getPackageDir();
70
- const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
71
- return join(packageDir, srcOrDist, "core", "export-html");
72
- }
73
-
74
- /** Get path to package.json */
75
- export function getPackageJsonPath(): string {
76
- return join(getPackageDir(), "package.json");
77
- }
78
-
79
- /** Get path to README.md */
40
+ /** Get path to README.md (optional, may not exist in binary) */
80
41
  export function getReadmePath(): string {
81
42
  return resolve(join(getPackageDir(), "README.md"));
82
43
  }
83
44
 
84
- /** Get path to docs directory */
45
+ /** Get path to docs directory (optional, may not exist in binary) */
85
46
  export function getDocsPath(): string {
86
47
  return resolve(join(getPackageDir(), "docs"));
87
48
  }
88
49
 
89
- /** Get path to examples directory */
50
+ /** Get path to examples directory (optional, may not exist in binary) */
90
51
  export function getExamplesPath(): string {
91
52
  return resolve(join(getPackageDir(), "examples"));
92
53
  }
93
54
 
94
- /** Get path to CHANGELOG.md */
55
+ /** Get path to CHANGELOG.md (optional, may not exist in binary) */
95
56
  export function getChangelogPath(): string {
96
57
  return resolve(join(getPackageDir(), "CHANGELOG.md"));
97
58
  }
98
59
 
99
- // =============================================================================
100
- // App Config (from package.json piConfig)
101
- // =============================================================================
102
-
103
- const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
104
-
105
- export const APP_NAME: string = pkg.piConfig?.name || "pi";
106
- export const CONFIG_DIR_NAME: string = pkg.piConfig?.configDir || ".pi";
107
- export const VERSION: string = pkg.version;
108
-
109
- // e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
110
- export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
111
-
112
60
  // =============================================================================
113
61
  // User Config Paths (~/.pi/agent/*)
114
62
  // =============================================================================
@@ -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
  }
@@ -449,6 +460,11 @@ export class AgentSession {
449
460
  return this._fileCommands;
450
461
  }
451
462
 
463
+ /** Custom commands (TypeScript slash commands) */
464
+ get customCommands(): ReadonlyArray<LoadedCustomCommand> {
465
+ return this._customCommands;
466
+ }
467
+
452
468
  // =========================================================================
453
469
  // Prompting
454
470
  // =========================================================================
@@ -473,6 +489,17 @@ export class AgentSession {
473
489
  // Hook command executed, no prompt to send
474
490
  return;
475
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
+ }
476
503
  }
477
504
 
478
505
  // Validate model
@@ -516,6 +543,13 @@ export class AgentSession {
516
543
  timestamp: Date.now(),
517
544
  });
518
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
+
519
553
  // Emit before_agent_start hook event
520
554
  if (this._hookRunner) {
521
555
  const result = await this._hookRunner.emitBeforeAgentStart(expandedText, options?.images);
@@ -566,6 +600,43 @@ export class AgentSession {
566
600
  }
567
601
  }
568
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
+
569
640
  /**
570
641
  * Queue a message to be sent after the current response completes.
571
642
  * Use when agent is currently streaming.
@@ -1058,6 +1129,10 @@ export class AgentSession {
1058
1129
  const settings = this.settingsManager.getCompactionSettings();
1059
1130
 
1060
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
+ }
1061
1136
  this._autoCompactionAbortController = new AbortController();
1062
1137
 
1063
1138
  try {
@@ -1231,7 +1306,8 @@ export class AgentSession {
1231
1306
  this._retryAttempt++;
1232
1307
 
1233
1308
  // Create retry promise on first attempt so waitForRetry() can await it
1234
- if (this._retryAttempt === 1 && !this._retryPromise) {
1309
+ // Ensure only one promise exists (avoid orphaned promises from concurrent calls)
1310
+ if (!this._retryPromise) {
1235
1311
  this._retryPromise = new Promise((resolve) => {
1236
1312
  this._retryResolve = resolve;
1237
1313
  });
@@ -1267,6 +1343,10 @@ export class AgentSession {
1267
1343
  }
1268
1344
 
1269
1345
  // Wait with exponential backoff (abortable)
1346
+ // Properly abort and null existing controller before replacing
1347
+ if (this._retryAbortController) {
1348
+ this._retryAbortController.abort();
1349
+ }
1270
1350
  this._retryAbortController = new AbortController();
1271
1351
  try {
1272
1352
  await this._sleep(delayMs, this._retryAbortController.signal);
@@ -1820,7 +1900,7 @@ export class AgentSession {
1820
1900
  * @param outputPath Optional output path (defaults to session directory)
1821
1901
  * @returns Path to exported file
1822
1902
  */
1823
- exportToHtml(outputPath?: string): string {
1903
+ async exportToHtml(outputPath?: string): Promise<string> {
1824
1904
  const themeName = this.settingsManager.getTheme();
1825
1905
  return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
1826
1906
  }
@@ -1858,6 +1938,68 @@ export class AgentSession {
1858
1938
  return text.trim() || undefined;
1859
1939
  }
1860
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
+
1861
2003
  // =========================================================================
1862
2004
  // Hook System
1863
2005
  // =========================================================================
@@ -1909,8 +2051,8 @@ export class AgentSession {
1909
2051
  if (tool.onSession) {
1910
2052
  try {
1911
2053
  await tool.onSession(event, ctx);
1912
- } catch (_err) {
1913
- // Silently ignore tool errors during session events
2054
+ } catch (err) {
2055
+ logger.warn("Tool onSession error", { error: String(err) });
1914
2056
  }
1915
2057
  }
1916
2058
  }
@@ -3,7 +3,7 @@
3
3
  * Handles loading, saving, and refreshing credentials from auth.json.
4
4
  */
5
5
 
6
- import { chmodSync, existsSync, mkdirSync } from "node:fs";
6
+ import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
7
7
  import { dirname } from "node:path";
8
8
  import {
9
9
  getEnvApiKey,
@@ -73,8 +73,7 @@ export class AuthStorage {
73
73
  return;
74
74
  }
75
75
  try {
76
- const file = Bun.file(this.authPath);
77
- this.data = JSON.parse(file.text() as unknown as string);
76
+ this.data = JSON.parse(readFileSync(this.authPath, "utf-8"));
78
77
  } catch {
79
78
  this.data = {};
80
79
  }
@@ -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";