@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.
- package/CHANGELOG.md +86 -0
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +10 -9
- package/src/bun-imports.d.ts +16 -0
- package/src/cli/args.ts +5 -6
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +2 -2
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/session-picker.ts +2 -2
- package/src/cli/update-cli.ts +273 -0
- package/src/cli.ts +1 -1
- package/src/config.ts +23 -75
- package/src/core/agent-session.ts +158 -16
- package/src/core/auth-storage.ts +2 -3
- package/src/core/bash-executor.ts +50 -10
- package/src/core/compaction/branch-summarization.ts +5 -5
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/compaction/index.ts +3 -3
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +232 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +3 -3
- package/src/core/custom-tools/loader.ts +10 -8
- package/src/core/custom-tools/types.ts +11 -6
- package/src/core/custom-tools/wrapper.ts +2 -1
- package/src/core/exec.ts +22 -12
- package/src/core/export-html/index.ts +38 -123
- package/src/core/export-html/template.css +0 -7
- package/src/core/export-html/template.html +3 -4
- package/src/core/export-html/template.macro.ts +24 -0
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +5 -5
- package/src/core/hooks/loader.ts +21 -16
- package/src/core/hooks/runner.ts +6 -6
- package/src/core/hooks/tool-wrapper.ts +2 -2
- package/src/core/hooks/types.ts +12 -15
- package/src/core/index.ts +6 -6
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +3 -3
- package/src/core/mcp/config.ts +1 -1
- package/src/core/mcp/index.ts +12 -12
- package/src/core/mcp/loader.ts +2 -2
- package/src/core/mcp/manager.ts +6 -6
- package/src/core/mcp/tool-bridge.ts +3 -3
- package/src/core/mcp/transports/http.ts +1 -1
- package/src/core/mcp/transports/index.ts +2 -2
- package/src/core/mcp/transports/stdio.ts +1 -1
- package/src/core/messages.ts +22 -0
- package/src/core/model-registry.ts +2 -2
- package/src/core/model-resolver.ts +2 -2
- package/src/core/plugins/doctor.ts +1 -1
- package/src/core/plugins/index.ts +6 -6
- package/src/core/plugins/installer.ts +4 -4
- package/src/core/plugins/loader.ts +4 -9
- package/src/core/plugins/manager.ts +5 -5
- package/src/core/plugins/paths.ts +3 -3
- package/src/core/sdk.ts +77 -35
- package/src/core/session-manager.ts +6 -6
- package/src/core/settings-manager.ts +16 -3
- package/src/core/skills.ts +5 -5
- package/src/core/slash-commands.ts +60 -45
- package/src/core/system-prompt.ts +6 -6
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/bash.ts +32 -155
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +3 -3
- package/src/core/tools/edit.ts +18 -5
- package/src/core/tools/exa/company.ts +3 -3
- package/src/core/tools/exa/index.ts +16 -17
- package/src/core/tools/exa/linkedin.ts +3 -3
- package/src/core/tools/exa/mcp-client.ts +9 -9
- package/src/core/tools/exa/render.ts +5 -5
- package/src/core/tools/exa/researcher.ts +3 -3
- package/src/core/tools/exa/search.ts +6 -5
- package/src/core/tools/exa/types.ts +5 -6
- package/src/core/tools/exa/websets.ts +3 -3
- package/src/core/tools/find.ts +3 -3
- package/src/core/tools/grep.ts +3 -3
- package/src/core/tools/index.ts +48 -34
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +161 -90
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +15 -13
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +175 -0
- package/src/core/tools/read.ts +7 -7
- package/src/core/tools/renderers.ts +92 -13
- package/src/core/tools/review.ts +268 -0
- package/src/core/tools/task/agents.ts +22 -38
- package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
- package/src/core/tools/task/commands.ts +31 -10
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +145 -28
- package/src/core/tools/task/index.ts +78 -30
- package/src/core/tools/task/model-resolver.ts +30 -20
- package/src/core/tools/task/parallel.ts +1 -1
- package/src/core/tools/task/render.ts +219 -30
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +36 -2
- package/src/core/tools/web-fetch.ts +5 -3
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/web-search/index.ts +17 -15
- package/src/core/tools/web-search/providers/anthropic.ts +2 -2
- package/src/core/tools/web-search/providers/exa.ts +3 -5
- package/src/core/tools/web-search/providers/perplexity.ts +1 -1
- package/src/core/tools/web-search/render.ts +3 -3
- package/src/core/tools/write.ts +4 -4
- package/src/index.ts +29 -18
- package/src/main.ts +50 -33
- package/src/migrations.ts +3 -3
- package/src/modes/index.ts +5 -5
- package/src/modes/interactive/components/armin.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/bash-execution.ts +4 -4
- package/src/modes/interactive/components/bordered-loader.ts +2 -2
- package/src/modes/interactive/components/branch-summary-message.ts +2 -2
- package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
- package/src/modes/interactive/components/diff.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/footer.ts +5 -5
- package/src/modes/interactive/components/hook-editor.ts +2 -2
- package/src/modes/interactive/components/hook-input.ts +2 -2
- package/src/modes/interactive/components/hook-message.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +2 -2
- package/src/modes/interactive/components/model-selector.ts +281 -59
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/plugin-settings.ts +4 -4
- package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
- package/src/modes/interactive/components/session-selector.ts +4 -4
- package/src/modes/interactive/components/settings-defs.ts +1 -1
- package/src/modes/interactive/components/settings-selector.ts +5 -5
- package/src/modes/interactive/components/show-images-selector.ts +2 -2
- package/src/modes/interactive/components/theme-selector.ts +2 -2
- package/src/modes/interactive/components/thinking-selector.ts +2 -2
- package/src/modes/interactive/components/tool-execution.ts +26 -8
- package/src/modes/interactive/components/tree-selector.ts +3 -3
- package/src/modes/interactive/components/user-message-selector.ts +2 -2
- package/src/modes/interactive/components/user-message.ts +1 -1
- package/src/modes/interactive/components/welcome.ts +2 -2
- package/src/modes/interactive/interactive-mode.ts +86 -42
- package/src/modes/interactive/theme/theme.ts +15 -17
- package/src/modes/print-mode.ts +4 -3
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +22 -12
- package/src/modes/rpc/rpc-types.ts +3 -3
- package/src/utils/changelog.ts +2 -2
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +93 -13
- package/src/utils/tools-manager.ts +1 -1
- package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/exa/logger.ts +0 -56
package/src/config.ts
CHANGED
|
@@ -1,34 +1,31 @@
|
|
|
1
|
-
import { existsSync
|
|
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
|
-
//
|
|
9
|
+
// App Config (from embedded package.json)
|
|
7
10
|
// =============================================================================
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
21
|
+
// Package Directory (for optional external docs/examples)
|
|
18
22
|
// =============================================================================
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
|
-
* Get the base directory for resolving package assets (
|
|
22
|
-
*
|
|
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 (
|
|
40
|
-
return
|
|
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
|
|
20
|
-
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor
|
|
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
|
|
30
|
-
import type {
|
|
31
|
-
import {
|
|
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
|
|
42
|
-
import
|
|
43
|
-
import type {
|
|
44
|
-
import type {
|
|
45
|
-
import type {
|
|
46
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1913
|
-
|
|
2054
|
+
} catch (err) {
|
|
2055
|
+
logger.warn("Tool onSession error", { error: String(err) });
|
|
1914
2056
|
}
|
|
1915
2057
|
}
|
|
1916
2058
|
}
|
package/src/core/auth-storage.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
15
|
-
import {
|
|
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
|
|
62
|
-
|
|
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
|
|
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
|
|
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
|
|
17
|
-
import type { ReadonlySessionManager, SessionEntry } from "../session-manager
|
|
18
|
-
import { estimateTokens } from "./compaction
|
|
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
|
|
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
|
|
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
|
|
12
|
-
import type { CompactionEntry, SessionEntry } from "../session-manager
|
|
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
|
|
21
|
+
} from "./utils";
|
|
22
22
|
|
|
23
23
|
// ============================================================================
|
|
24
24
|
// File Operation Tracking
|